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
@@ -0,0 +1,341 @@
/**
* What Piggy is allowed to do, chosen before the question is asked.
*
* This is the only control in PIG that decides whether a language model may
* write to the company's book, so it is written to be read rather than to be
* clever. Three things follow from that and are deliberate:
*
* names — the segments say "Read only", "Ask first" and "Auto", not
* `read_only` / `confirm` / `auto`. The enum is the wire's language
* and nobody choosing a permission should have to learn it.
* consequence — the sentence under the segments describes the mode that is
* selected NOW, and changes as the selection does. A toggle whose
* meaning lives in documentation is a toggle people set once and then
* misremember.
* the exceptions — `auto` still stops at a contract, a commitment, an
* allocation and anything compliance-shaped. That is `requiresApproval`'s
* rule, and if the control does not say so, the first person to choose
* auto will reasonably assume nothing stops, and will either be
* frightened of the mode or trust it further than it deserves.
*
* The mode is NOT enforced here. `requiresApproval` in @pig/core is the single
* source of truth and the agent applies it server-side; this control only tells
* the relay what the user picked. Treating it as a guard would put the
* authorisation in the browser, where the user can edit it.
*/
import { useCallback, useEffect, useId, useRef, useState, type JSX } from 'react';
import { Eye, ListChecks, TriangleAlert, Zap, type LucideIcon } from 'lucide-react';
import {
PIGGY_ALWAYS_CONFIRM_KINDS,
type PiggyGuardedKind,
type PiggyMode,
} from '@pig/core';
import { PIGGY_DEFAULT_MODE } from '@/lib/piggy-chat';
import { useOptionalIdentity } from '@/lib/identity';
import { cn } from '@/components/ui';
// ------------------------------------------------------------------- copy
interface ModeOption {
value: PiggyMode;
/** The user's word for it. */
label: string;
icon: LucideIcon;
/** What choosing this mode means, in one sentence, present tense. */
sentence: string;
/** True when picking it hands an agent the ability to write unattended. */
consequential?: boolean;
}
/**
* The four kinds `requiresApproval` refuses to automate, spelled for a person.
*
* Derived from `PIGGY_ALWAYS_CONFIRM_KINDS` rather than typed out, because the
* sentence is a promise about policy: if a fifth guarded kind is added upstream
* and this copy were a literal, the control would quietly go on promising four.
* The map is exhaustive by type, so adding one there fails the build here.
*/
const GUARDED_KIND_LABELS: Record<PiggyGuardedKind, string> = {
contract: 'contracts',
commitment: 'commitments',
allocation: 'allocations',
compliance: 'compliance',
};
const GUARDED_SENTENCE = (() => {
const names = PIGGY_ALWAYS_CONFIRM_KINDS.map((kind) => GUARDED_KIND_LABELS[kind]);
// en-GB: "contracts, commitments, allocations and compliance".
const list = new Intl.ListFormat('en-GB', { style: 'long', type: 'conjunction' }).format(names);
return `${list.charAt(0).toUpperCase()}${list.slice(1)} still stop for your approval.`;
})();
const READ_ONLY_OPTION: ModeOption = {
value: 'read_only',
label: 'Read only',
icon: Eye,
sentence: 'Piggy answers from your CRM and is offered no tool that could change it.',
};
const MODE_OPTIONS: readonly ModeOption[] = [
READ_ONLY_OPTION,
{
value: 'confirm',
label: 'Ask first',
icon: ListChecks,
sentence: 'Piggy proposes each change and nothing is saved until you press Apply.',
},
{
value: 'auto',
label: 'Auto',
icon: Zap,
sentence: 'Piggy makes changes to your CRM itself, without asking first.',
consequential: true,
},
];
/** Why the write modes are unavailable. Shown, never merely implied. */
const NO_WRITE_REASON =
'Your access does not allow changing records, so Piggy can only read.';
/**
* The user's word for a mode, and its icon, for a control that summarises this
* one rather than replacing it — the workspace header's trigger.
*
* Exported rather than restated at the call site: the trigger says what the
* segments say, and a second copy of "Ask first" is a second opinion waiting to
* disagree with this file the first time the copy is edited.
*/
export function piggyModeSummary(mode: PiggyMode): { label: string; icon: LucideIcon } {
const option = optionFor(mode);
return { label: option.label, icon: option.icon };
}
function optionFor(mode: PiggyMode): ModeOption {
// The union is closed and the array covers it; the fallback exists so a mode
// read back from storage on a future build cannot render an empty control.
return MODE_OPTIONS.find((option) => option.value === mode) ?? READ_ONLY_OPTION;
}
// ---------------------------------------------------------------- control
export function PiggyModeControl({
value,
onChange,
compact = false,
canWrite,
}: {
value: PiggyMode;
onChange: (mode: PiggyMode) => void;
compact?: boolean;
canWrite: boolean;
}): JSX.Element {
const describedBy = useId();
const buttons = useRef(new Map<PiggyMode, HTMLButtonElement>());
/**
* What is drawn as selected. Not necessarily what the parent holds: a stored
* `auto` outlives the capability that justified it, so someone whose write
* grant was removed would otherwise open the composer being told Piggy is
* about to edit records it will now be refused.
*/
const selected: PiggyMode = canWrite ? value : 'read_only';
useEffect(() => {
// The correction is pushed up rather than kept local, because the parent is
// what puts `mode` on the wire. Showing read-only while sending `auto`
// would be the one disagreement this control must never have. It cannot
// loop: the parent's next value satisfies the condition.
if (!canWrite && value !== 'read_only') onChange('read_only');
}, [canWrite, value, onChange]);
const choices = MODE_OPTIONS.filter((option) => canWrite || option.value === 'read_only');
const step = useCallback(
(direction: 1 | -1) => {
const index = choices.findIndex((option) => option.value === selected);
const next = choices[(index + direction + choices.length) % choices.length];
if (!next) return;
onChange(next.value);
buttons.current.get(next.value)?.focus();
},
[choices, onChange, selected],
);
const active = optionFor(selected);
return (
<div className={cn('flex min-w-0 flex-col', compact ? 'gap-1.5' : 'gap-2')}>
{compact ? null : (
<span className="text-xs font-medium uppercase tracking-wide text-muted">
What Piggy may do
</span>
)}
<div
role="radiogroup"
aria-label="What Piggy may do"
aria-describedby={describedBy}
className="grid grid-cols-3 gap-1 rounded-xl border border-border bg-surface-2 p-1"
onKeyDown={(event) => {
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
event.preventDefault();
step(1);
}
if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
event.preventDefault();
step(-1);
}
}}
>
{MODE_OPTIONS.map((option) => {
const isSelected = option.value === selected;
const disabled = !canWrite && option.value !== 'read_only';
const Icon = option.icon;
return (
<button
key={option.value}
ref={(node) => {
if (node) buttons.current.set(option.value, node);
else buttons.current.delete(option.value);
}}
type="button"
role="radio"
aria-checked={isSelected}
// Roving tabstop: a radio group is one stop in the tab order, and
// the arrow keys move within it.
tabIndex={isSelected ? 0 : -1}
disabled={disabled}
title={disabled ? NO_WRITE_REASON : option.sentence}
onClick={() => onChange(option.value)}
className={cn(
'flex min-h-[44px] min-w-0 items-center justify-center rounded-lg',
'font-medium transition-colors touch-manipulation select-none',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent',
// Tight enough that "Read only" survives whole in a dock
// narrower than the 22rem one; the label is what makes this
// control legible, so it is the last thing allowed to truncate.
compact ? 'gap-1 px-1 text-[11px]' : 'gap-1.5 px-2 text-xs sm:text-sm',
isSelected
? 'bg-surface text-fg shadow-sm'
: 'text-muted hover:text-fg disabled:hover:text-muted',
disabled && 'cursor-not-allowed opacity-50',
)}
>
<Icon
aria-hidden
className={cn(
'shrink-0',
compact ? 'h-3 w-3' : 'h-4 w-4',
isSelected && option.consequential ? 'text-warning' : undefined,
)}
/>
<span className="truncate">{option.label}</span>
</button>
);
})}
</div>
{/*
Announced on change, because the consequence arrives a beat after the
press and a screen-reader user gets no colour to tell them the tone of
the panel changed.
*/}
<div id={describedBy} aria-live="polite" className="min-w-0">
{active.consequential ? (
<p
className={cn(
'flex items-start gap-2 rounded-lg border border-warning/40 bg-warning/10',
compact ? 'px-2 py-1.5 text-[11px]' : 'px-2.5 py-2 text-xs',
'leading-snug text-fg',
)}
>
<TriangleAlert aria-hidden className="mt-px h-3.5 w-3.5 shrink-0 text-warning" />
<span>
{active.sentence} <span className="font-medium">{GUARDED_SENTENCE}</span>
</span>
</p>
) : (
<p className={cn('leading-snug text-muted', compact ? 'text-[11px]' : 'text-xs')}>
{active.sentence}
</p>
)}
{canWrite ? null : (
<p className={cn('mt-1 leading-snug text-muted', compact ? 'text-[11px]' : 'text-xs')}>
{NO_WRITE_REASON}
</p>
)}
</div>
</div>
);
}
// -------------------------------------------------------------- preference
/**
* Per user, not per browser.
*
* Two people share a laptop far more often than a CRM's security model likes to
* admit, and a single `pig.piggy.mode` key would hand the second one an agent
* already licensed to write by the first. The signed-in id is part of the key
* for that reason alone.
*/
const MODE_STORAGE_PREFIX = 'pig.piggy.mode.';
function isMode(value: unknown): value is PiggyMode {
return MODE_OPTIONS.some((option) => option.value === value);
}
function readStoredMode(key: string | null): PiggyMode | null {
if (!key) return null;
try {
const raw = localStorage.getItem(key);
// Validated rather than cast: a value written by an older build, or edited
// by hand, would otherwise travel to the relay as a mode and collect a 400
// on every turn until someone cleared their storage.
return isMode(raw) ? raw : null;
} catch {
// Private browsing throws on access. The default is the safe one anyway.
return null;
}
}
/**
* The stored answer to "what may Piggy do", defaulting to `read_only`.
*
* `PIGGY_DEFAULT_MODE` is imported rather than restated so this cannot become a
* second opinion on what "safe" means; it is read-only, which is both the
* safest mode and a useful one — Piggy still answers every question it can
* answer, and the only thing withheld is the ability to change records, which
* is exactly the thing a person should turn on knowingly. Defaulting to
* `confirm` would be defensible on the grounds that it never writes unasked,
* but it puts write tools in front of the model on first use for someone who
* never asked for them, and the relay would then be told so on every turn.
*/
export function usePiggyMode(): { mode: PiggyMode; setMode: (mode: PiggyMode) => void } {
const identity = useOptionalIdentity();
const key = identity ? `${MODE_STORAGE_PREFIX}${identity.id}` : null;
const [mode, setModeState] = useState<PiggyMode>(() => readStoredMode(key) ?? PIGGY_DEFAULT_MODE);
useEffect(() => {
// Re-read whenever the person changes. Falling back to the default rather
// than keeping what is on screen matters here: a new signed-in user with no
// stored preference must not inherit the last one's `auto`.
setModeState(readStoredMode(key) ?? PIGGY_DEFAULT_MODE);
}, [key]);
const setMode = useCallback(
(next: PiggyMode) => {
setModeState(next);
if (!key) return;
try {
localStorage.setItem(key, next);
} catch {
// Non-fatal: the choice simply does not survive the tab, and the next
// one opens read-only, which is the harmless direction to fail in.
}
},
[key],
);
return { mode, setMode };
}