Put Piggy on Prime Agent, and let it write to the book
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:
@@ -292,7 +292,7 @@ In production you must additionally set **either** `SUPABASE_URL` **or**
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---|---|---|
|
||||
| `PRIME_API_KEY` | unset | Scope it to `Availability → Read` only |
|
||||
| `PRIME_API_KEY` | unset | Scope it to `Availability → Read`, plus inference if Piggy is on — the same key buys the agent's tokens. Nothing that can provision |
|
||||
| `PRIME_API_BASE` | `https://api.primeintellect.ai` | The compute/pods host. Inference is a *different* host — see below |
|
||||
| `PRIME_SYNC_ENABLED` | `false` | Warns if on without a key |
|
||||
| `PRIME_SYNC_INTERVAL_MINUTES` | `30` | |
|
||||
@@ -309,9 +309,15 @@ means editing `.env` and restarting the container.
|
||||
| Variable | Default | Read by | Notes |
|
||||
|---|---|---|---|
|
||||
| `PIGGY_ENABLED` | `false` | API, `deploy.sh` | Gates the chat surface, and tells `scripts/deploy.sh` to ship the `piggy` Compose profile with the app |
|
||||
| **`PIGGY_INFERENCE_API_KEY`** | — | Piggy | Required by the Piggy process. Missing, it exits at boot and crash-loops. The model credential never reaches the API container |
|
||||
| `PIGGY_INFERENCE_BASE` | `https://api.pinference.ai/api/v1` | both | OpenAI-compatible |
|
||||
| `PIGGY_MODEL` | `nvidia/nemotron-3-nano-30b-a3b` | both | The API reads it to display; Piggy reads it to call |
|
||||
| **`PRIME_API_KEY`** | — | API, Piggy | One key, two hosts: the availability sync calls `api.primeintellect.ai`, the agent calls `api.pinference.ai`. Required by the Piggy process; missing, it exits at boot and crash-loops |
|
||||
| `PIGGY_INFERENCE_API_KEY` | — | Piggy | The legacy spelling of `PRIME_API_KEY`, still accepted so a `.env` written before the harness swap keeps starting. Set one, not two |
|
||||
| `PIGGY_AGENT_MODEL` | `nvidia/nemotron-3-nano-30b-a3b` | Piggy | The default answer model. Must be one of the five ids in `apps/piggy/src/agent/models.json`, or Piggy refuses to start — an unlisted model is not registered with the harness and would fail on a user's first question instead |
|
||||
| `PIGGY_AGENT_MODE` | `confirm` | Piggy | `read_only`, `confirm` or `auto`. Contracts, commitments, allocations and compliance require a click in every mode |
|
||||
| `PIGGY_AGENT_THINKING` | `off` | Piggy | `off`…`max`. **Read [the trap](#the-thinking-level-trap) before raising it or changing the model** |
|
||||
| `PIGGY_AGENT_MAX_TOKENS` | `4096` | Piggy | Output tokens per agent turn, reasoning included. Clamped down to the model's own ceiling |
|
||||
| `PIGGY_AGENT_DIR` | `~/.pig/piggy-agent` | Piggy | The harness's own directory. Compose pins it to `/var/lib/piggy-agent`; it must never be a checkout, because the harness reads context files from its cwd |
|
||||
| `PIGGY_INFERENCE_BASE` | `https://api.pinference.ai/api/v1` | both | OpenAI-compatible. The agent reads its base URL from `models.json`; this one still drives the queue worker |
|
||||
| `PIGGY_MODEL` | `nvidia/nemotron-3-nano-30b-a3b` | both | The queue worker's model. The agent uses `PIGGY_AGENT_MODEL` and the picker |
|
||||
| `PIGGY_LEASE_SECONDS` | `300` | both | Queue lease duration |
|
||||
| `PIGGY_POLL_INTERVAL_MS` | `2000` | Piggy | How often an idle worker looks for a task |
|
||||
| `PIGGY_MAX_TOKENS` | `1024` | Piggy | Per queued task |
|
||||
@@ -327,6 +333,24 @@ means editing `.env` and restarting the container.
|
||||
| `PIGGY_CHAT_PORT` | `8931` | Piggy | Never published to the host |
|
||||
| `PIGGY_CHAT_ALLOW_NON_LOOPBACK` | `false` | Piggy | Compose sets `true`, because the API reaches it across the Compose network |
|
||||
|
||||
##### The thinking-level trap
|
||||
|
||||
Worth its own heading, because it costs an afternoon otherwise.
|
||||
|
||||
The harness defaults `thinkingLevel` to `medium`, which is tuned for a coding
|
||||
agent. On the default nemotron model that produced **6,195 output tokens of
|
||||
reasoning and an empty answer** — the turn hit its ceiling mid-thought and
|
||||
returned `finish_reason: length`. `low` was worse. `off` maps, for that model,
|
||||
to the endpoint's `reasoning_effort: none`, and the same question came back
|
||||
correct in **149 output tokens**.
|
||||
|
||||
The mapping is per model, in `thinkingLevelMap` in
|
||||
`apps/piggy/src/agent/models.json`. The nemotron entries have one; deepseek,
|
||||
opus and gpt-5.6 do not, so at `off` they send no reasoning parameter at all and
|
||||
inherit the endpoint's default. **If you change `PIGGY_AGENT_MODEL` and start
|
||||
getting empty or truncated answers, this is why** — give the new model a
|
||||
`thinkingLevelMap` before touching `PIGGY_AGENT_THINKING`.
|
||||
|
||||
#### Integrations — all optional, all validated as a group
|
||||
|
||||
Setting one member of a group without the others fails at boot rather than
|
||||
@@ -350,7 +374,8 @@ across five packages, green CI.
|
||||
apps/
|
||||
web/ React 19 + Vite + Tailwind + shadcn-idiom components
|
||||
api/ Hono HTTP API — auth, validation, capacity and contract services
|
||||
piggy/ The agent: a lease-based queue worker plus a private chat server
|
||||
piggy/ The agent: a Prime Agent session over the CRM tools, served by a
|
||||
private chat server, plus a lease-based queue worker
|
||||
mcp/ MCP server (stdio) — 9 tools
|
||||
cli/ `pig`, the HTTP surface for scripts and agent kernels
|
||||
packages/
|
||||
@@ -438,7 +463,23 @@ degraded view of the other. There are two distinct surfaces.
|
||||
|
||||
### Piggy — the in-app agent
|
||||
|
||||
`apps/piggy` is one image running two processes' worth of behaviour:
|
||||
`apps/piggy` runs **Prime Agent** — Prime Intellect's own agent harness
|
||||
(`@earendil-works/pi-coding-agent`, MIT), embedded as a Node library rather than
|
||||
shelled out to — with PIG's CRM tools and nothing else. Models come from Prime
|
||||
Intellect inference (`api.pinference.ai`) on `PRIME_API_KEY`; the picker offers
|
||||
five, defined in `apps/piggy/src/agent/models.json`, priced and sized in the one
|
||||
file the runtime and the UI both read.
|
||||
|
||||
**The harness has no shell, no filesystem and no Python.** It is constructed
|
||||
with `noTools: 'all'` and an explicit allowlist, and there are three independent
|
||||
gates behind that: PIG's own boundary check on the tool list before a session
|
||||
opens, a comparison of the harness's live `state.tools` against exactly what was
|
||||
handed in — a startup error if they differ, so a future harness release cannot
|
||||
widen the set quietly — and a test that pins the same comparison. Prompt
|
||||
templates, skills, extensions and context-file discovery are all disabled, and
|
||||
the harness's cwd is a dedicated directory that holds no code.
|
||||
|
||||
It is one image running two processes' worth of behaviour:
|
||||
|
||||
- **The queue worker** claims a task with `SELECT … FOR UPDATE SKIP LOCKED`
|
||||
inside a transaction, holds a renewable lease (default 300s, renewed at half
|
||||
@@ -449,20 +490,36 @@ degraded view of the other. There are two distinct surfaces.
|
||||
counts and either a summary or the error. Its tool set is exactly two:
|
||||
`pig_get_subject` and `pig_record_fact`, and a fact is refused without both a
|
||||
source URL and an evidence excerpt.
|
||||
- **The chat server** listens on `127.0.0.1:8931` and is never published to the
|
||||
host. The API authenticates the user, forwards bounded context, and calls it
|
||||
with a shared internal bearer token. Chat is **read-only**: seven tools
|
||||
(`pig_get_record`, `pig_get_account_lifecycle` and five page-scoped
|
||||
summaries), each of which aggregates first and returns at most a handful of
|
||||
exemplar rows, because interactive chat runs at 2048 max tokens across at
|
||||
most four turns. Ambient coding tools are rejected before inference by an
|
||||
explicit boundary check.
|
||||
- **The chat server** listens on `8931` and is never published to the host. The
|
||||
API authenticates the user, forwards bounded context and the caller's
|
||||
principal, and calls it with a shared internal bearer token. Its tools are
|
||||
scoped to what the user is looking at: a focused record reader or one of the
|
||||
page-scoped summaries, plus lookups (`pig_search_records`,
|
||||
`pig_get_record_by_id`, `pig_list_renewals`, `pig_list_inventory`,
|
||||
`pig_get_account_lifecycle`). Each aggregates first and returns at most a
|
||||
handful of exemplar rows.
|
||||
|
||||
**Chat can now write**, which it could not before: `pig_log_activity`,
|
||||
`pig_create_contact`, `pig_create_task`, `pig_update_deal_stage` and
|
||||
`pig_update_record_fields`. Every one of them runs through the same
|
||||
`executeMutation` path the HTTP API uses, as the calling user's own
|
||||
`Principal` — so Piggy holds no privilege of its own and cannot touch a record
|
||||
its user could not. `PIGGY_AGENT_MODE` decides how far it may go on its own
|
||||
(`read_only`, `confirm`, `auto`), and in `confirm` a change is proposed as a
|
||||
card the user applies. Contracts, commitments, allocations and compliance
|
||||
records require a click in **every** mode; that rule is one function,
|
||||
`requiresApproval` in `packages/core/src/piggy-protocol.ts`, so it cannot be
|
||||
true in one place and false in another.
|
||||
|
||||
Conversations persist in `piggy_conversations` and `piggy_messages`
|
||||
(migration 0014), and `/piggy` is a full workspace rather than a docked panel
|
||||
alone.
|
||||
|
||||
Piggy is off by default. `PIGGY_ENABLED` defaults to `false` and the Compose
|
||||
service sits behind `profiles: ['piggy']`, so a default `docker compose up`
|
||||
starts the CRM without it. Turning it on is three values in `.env` —
|
||||
`PIGGY_ENABLED=true`, `PIGGY_INFERENCE_API_KEY` and a 32-character
|
||||
`PIGGY_INTERNAL_TOKEN` — and then a deploy:
|
||||
`PIGGY_ENABLED=true`, `PRIME_API_KEY` (or the legacy `PIGGY_INFERENCE_API_KEY`)
|
||||
and a 32-character `PIGGY_INTERNAL_TOKEN` — and then a deploy:
|
||||
|
||||
```bash
|
||||
bash scripts/deploy.sh
|
||||
@@ -559,8 +616,12 @@ ever written to `agent_tasks` (both from record creation). `write_brief`,
|
||||
nothing produces them. Piggy therefore does far less than the queue implies —
|
||||
not because the machinery is missing, but because nothing asks.
|
||||
|
||||
**Piggy chat cannot write.** By design for now, but worth stating: the
|
||||
interactive agent reads and cites; it cannot create or update a CRM record.
|
||||
**Piggy's write surface is five tools, not the whole CRM.** It can log an
|
||||
activity, create a contact or a task, move a deal stage and update fields on a
|
||||
record it can already read. Everything else — creating an account, a
|
||||
commitment, a contract, an allocation — is still a human's job in the UI, and
|
||||
the mutations it does have route through the same `executeMutation` path and
|
||||
the same capability checks as the HTTP API.
|
||||
|
||||
**The MCP server is stdio only.** There is no Streamable HTTP transport and no
|
||||
`/mcp` endpoint on the API, so remote MCP clients cannot connect over the
|
||||
|
||||
Reference in New Issue
Block a user