Put Piggy on Prime Agent, and let it write to the book
CI / verify (push) Successful in 7m6s
CI / publish (push) Has been skipped

Piggy was a hand-rolled OpenAI tool loop. It is now a Prime Agent session —
Prime Intellect's own harness, embedded as a Node library — answering from
PIG's tools and, for the first time, able to put information into the CRM
rather than only read it out.

The harness is a coding agent, so the first job was taking the coding agent
away from it. `noTools: 'all'` plus an explicit allowlist leaves the model
with PIG's ten `pig_*` tools and no bash, no filesystem, no IPython. That
holds under attack: a hostile extension, a skill and a settings file planted
in the agent's own directory, then `setActiveToolsByName` called with every
built-in, still leaves ten tools, all ours. Both lines are load-bearing —
`noTools` alone registers nothing, and the allowlist is what admits our own.

Writing is gated rather than assumed. A change is proposed, not made: the
tool returns a description, the transcript renders a diff card, and nothing
reaches the database until someone presses Apply. Contracts, commitments,
allocations and compliance always stop for a human whatever the mode. Every
write runs through `executeMutation` as the calling user, so their
capabilities and the audit trail apply exactly as they would to a human's.

Four things about the SDK are wrong in its own documentation and cost a
debugging cycle each: models.json does not resolve an env var name for
`apiKey`, it sends the literal string; there is no built-in prime-inference
provider in 0.84.1; a ResourceLoader you pass in is never reloaded for you;
and the stock system prompt is a coding-assistant prompt that must be
replaced — but replacing it also silently removes the tool list, because the
harness only renders that section when it owns the prompt. AGENTS.md records
all four.

The expensive one was thinking level. The harness defaults to `medium`, and
nemotron spent an entire 4,096-token budget reasoning and returned an empty
answer. `low` was worse; `off` omits the parameter so the endpoint's default
wins. An explicit `reasoning_effort: none` via `thinkingLevelMap` took a turn
from 6,195 output tokens to 149.

And a turn is now bounded. The harness loop is `while (true)` with no
iteration cap; a runaway on a frontier model would have eaten the credit it
is supposed to report on. Ceilings on model calls and tokens, enforced both
through the harness hook and independently from the event stream, plus a
per-user daily spend limit — and the ledger now records spend on turns that
fail, which it previously discarded.

Signing in lands on /piggy, which is a workspace: conversations down one
side, the agent in the middle, what it did and what it cost beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude
2026-08-14 05:26:28 -07:00
parent 99d165b5e5
commit f0173440e4
77 changed files with 28108 additions and 1672 deletions
+71 -3
View File
@@ -3,7 +3,7 @@
*/
import { lazy, Suspense, useEffect, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { ApiError, get, getSupabase, loadPublicConfig, patch, type PublicConfig } from '@/lib/api';
import { ThemeProvider } from '@/lib/theme';
@@ -232,7 +232,39 @@ function AppRoutes() {
return (
<Routes>
<Route element={<Shell />}>
<Route index element={<RoutePage><Overview /></RoutePage>} />
{/*
`/` is the front door, and the front door is Piggy.
--------------------------------------------------
Signing in does not navigate: the auth gate simply starts rendering
these routes at whatever address the browser is already on, which for
anyone arriving fresh is `/`. So "land on Piggy after sign-in" and
"`/` is Piggy" are the same sentence, and this is the only line that
decides it. A post-sign-in `navigate()` was rejected: it fires on one
path through the gate and not on a hard refresh, so the product would
open somewhere different depending on how you got there.
It is a redirect rather than Piggy mounted at the index, because the
workspace needs ONE address. Two paths rendering it would leave the
sidebar row unlit on `/`, the breadcrumb blank, and a shared link
ambiguous. `replace` keeps `/` out of history, so Back leaves the app
instead of bouncing between the two, and the logo — which points at
`/` and means "home" — lands on the same screen it always did, only
home is Piggy now.
Overview moves to `/overview` rather than being displaced: it is the
exec's page, it keeps its place at the top of Intelligence, it keeps
its tab on the phone, and it is one click from anywhere. What it
loses is being the thing you are shown before you have asked for
anything, which is the whole point of the change — a report is what
you open when you have a question about the business, and Piggy is
where you ask it.
Nothing else moves. Every other path is registered exactly as before,
so `/accounts/:id`, `/margin` and every bookmark and Piggy record link
into them still resolve directly, with no pass through here.
*/}
<Route index element={<Navigate to="/piggy" replace />} />
<Route path="overview" element={<RoutePage><Overview /></RoutePage>} />
<Route path="margin" element={<RoutePage><Margin /></RoutePage>} />
<Route path="growth" element={<RoutePage><Growth /></RoutePage>} />
<Route path="calendar" element={<RoutePage><Calendar /></RoutePage>} />
@@ -250,7 +282,7 @@ function AppRoutes() {
<Route path="accounts/:id" element={<RoutePage><Account /></RoutePage>} />
<Route path="contracts" element={<RoutePage><Contracts /></RoutePage>} />
<Route path="imports" element={<RoutePage><Imports /></RoutePage>} />
<Route path="piggy" element={<RoutePage><Piggy /></RoutePage>} />
<Route path="piggy" element={<WorkspaceRoute><Piggy /></WorkspaceRoute>} />
<Route path="team" element={<Team />} />
<Route path="facts" element={<RoutePage><FactReview /></RoutePage>} />
<Route path="settings" element={<RoutePage><Settings /></RoutePage>} />
@@ -268,6 +300,42 @@ function RoutePage({ children }: { children: React.ReactNode }) {
);
}
/**
* A route that FILLS the content pane instead of flowing down it.
*
* Shell puts every page inside `mx-auto max-w-7xl px-4 py-5 …`, which is right
* for a document and wrong for a workspace: an agent surface with a
* conversation list, a transcript and an activity panel wants the whole pane,
* a floor it can pin a composer to, and no page scrollbar behind the two
* panels that already scroll.
*
* `absolute inset-0` is how it gets that without a second shell. SidebarInset
* is `relative` (see ui/sidebar), so this box is laid out against the content
* pane itself — full width whatever the container capped, full height whatever
* the container did not stretch to — while the capped container stays exactly
* as it is for the twelve pages that want it. Taking it out of flow is also
* what makes `overflow-hidden` safe here: the page cannot grow, so the panels
* inside must own their own scrolling, which is the contract a workspace wants
* anyway.
*
* The bottom padding is the one thing that has to be restated. An absolutely
* positioned child is laid out against its ancestor's PADDING box, so the
* inset's own tab-bar clearance does not apply to it, and without this the
* composer would sit underneath the phone tab bar — the exact control a phone
* user came here to reach. `lg` matches where the tab bar gives way.
*/
function WorkspaceRoute({ children }: { children: React.ReactNode }) {
return (
<div className="absolute inset-0 flex min-h-0 flex-col overflow-hidden pb-[calc(4.5rem+var(--safe-bottom))] lg:pb-0">
{/* `flex-1` on the fallback, or the spinner for a pane this tall sits up
against the header while the rest of it stays empty. */}
<Suspense fallback={<div className="flex flex-1 items-center justify-center"><RouteLoading /></div>}>
{children}
</Suspense>
</div>
);
}
function RouteLoading() {
return (
<div