@ssheleg/agent-stack 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0] — 2026-08-15
4
+
5
+ ### Added
6
+
7
+ - **`agent-harness/references/pi.md` and `pi-sdk.md` — the harness doctrine as a worked
8
+ implementation.** Every other reference in that skill states a rule; **Pi** is small enough
9
+ to read and complete enough to have made each of those decisions in public. So each section
10
+ says what Pi does and then **which rule it is an instance of** — the second half is the
11
+ point, and where Pi disagrees with this pack, that is said rather than smoothed over.
12
+
13
+ Read from `pi.dev/docs/latest` on 2026-08-15: sixteen doc pages plus the package source.
14
+ All sixteen verified reachable (`200`); the only non-resolving URLs in either file are the
15
+ two deliberate placeholders inside custom-provider examples.
16
+
17
+ - **`pi.md`** — the four ways to run it; **sessions as a JSONL tree** (8-hex `id`, `parentId`,
18
+ version 3, `BranchSummaryEntry`) matched to *time travel and forking* in
19
+ `agent-orchestrator/references/runtime.md`; **compaction with the real numbers**
20
+ (`contextTokens > contextWindow - reserveTokens`, defaults 16,384 and 20,000, tool results
21
+ truncated to 2,000 chars while summarizing) matched to the compaction ladder, with what the
22
+ ladder adds that Pi leaves to you; settings precedence that **merges rather than replaces**;
23
+ skills, prompt templates and packages; and the trust model.
24
+
25
+ Its sharpest section is **the deliberate absence of a sandbox**, quoted: *"prompt injection
26
+ from repository files … is expected local-agent risk and cannot be reliably prevented by
27
+ pi."* That is `layers.md`'s delegation thesis stated by the project itself — and for an
28
+ audit it changes the finding, because "no permission model" here is a delegation, not a
29
+ defect. Three containerization patterns are compared by **where credentials end up**, which
30
+ is the question that actually decides between them.
31
+
32
+ - **`pi-sdk.md`** — `createAgentSession()`, `ModelRuntime`, `SessionManager`, `defineTool()`,
33
+ `DefaultResourceLoader`; the RPC protocol with its command groups, its full event lifecycle
34
+ and the **`\n`-only JSONL framing warning**; JSON mode's delta-only records and why;
35
+ the `ExtensionAPI` surface; and **the eight seams** — `tool_call` (can block),
36
+ `tool_result` (a middleware chain), `context`, `before_agent_start`, the three provider
37
+ hooks, and the compaction pair — each matched to the doctrine it lets you implement.
38
+ `tool_call` blocking is called out as the single most important one for an audit: it is
39
+ where a per-tool, per-caller policy can actually live.
40
+
41
+ - Noted with its evidence and its limit: **Pi discovers skills from `~/.agents/skills/`**,
42
+ which on this machine is the ssheleg hub — 72 entries, every family skill carrying the
43
+ `name` and `description` front matter Pi requires. Stated as a fact about the path and the
44
+ front matter, **not** as an observed load: Pi is not installed here. The reference also
45
+ names Pi's documented divergence from the Agent Skills standard (a skill name may differ
46
+ from its directory) and warns that `make-skill`'s validator enforces the strict rule.
47
+
48
+ ### Changed
49
+
50
+ - `references/layers.md` points at the two new files as the worked example of the kernel
51
+ layer it describes abstractly.
52
+ - `agent-harness`'s description gains the embedding triggers (`agent SDK`, `embed an agent`,
53
+ `Pi harness`, `встроить агента`) — 807/1024, inside the family's 970 working budget. The
54
+ repository's own front-matter gate caught a first draft at 1066 and refused the write.
55
+
56
+
3
57
  ## [0.8.0] — 2026-08-14
4
58
 
5
59
  ### Added
package/README.md CHANGED
@@ -11,7 +11,7 @@ Part of the [ssheleg skill family](https://github.com/ssheleg/sshlg-skills).
11
11
 
12
12
  Four skills — `agent-orchestrator` for wiring the loop, `agent-evals` for proving it
13
13
  behaves, `agent-interop` for everything it talks to outside its own process,
14
- `agent-harness` for what it is **told** — and sixteen references they load on demand,
14
+ `agent-harness` for what it is **told** — and eighteen references they load on demand,
15
15
  plus one scanner.
16
16
 
17
17
  **The orchestrator** (`SKILL.md`) — what the agent reads first:
@@ -61,13 +61,16 @@ ACP, AGNTCY, AP2, Agent Skills — so an agent stops guessing.
61
61
  model, and the one where most agent bugs actually live: *the biggest performance
62
62
  improvements often come from clearly explaining tool usage in the system prompt*, and *even
63
63
  small refinements to tool descriptions can yield dramatic improvements*. Before adding a
64
- retry or a sub-agent, it asks four questions about the text. Five references —
64
+ retry or a sub-agent, it asks four questions about the text. Seven references —
65
65
  `system-prompt.md` (right altitude, enumerated vocabulary, and the three things reasoning
66
66
  models changed — starting with **do not add chain-of-thought**), `tools.md` (the
67
67
  agent–computer interface, with a worked before/after and poka-yoke), `techniques.md`
68
68
  (fifteen techniques, a verdict each **for production** rather than a benchmark),
69
69
  `layers.md` (which layer you are building at, and why permission boundaries are usually the
70
- environment's job), `audit.md` (seven tracks, evidence tiers, a plan instead of a score).
70
+ environment's job), `audit.md` (seven tracks, evidence tiers, a plan instead of a score)
71
+ plus **`pi.md` and `pi-sdk.md`, the doctrine as a worked implementation**: Pi read end to
72
+ end, each mechanism matched to the rule it instantiates, its divergences named, and the
73
+ eight extension seams where a permission gate or a context rewrite can actually live.
71
74
 
72
75
  It runs in both directions: **building a harness and auditing somebody else's are one
73
76
  checklist read forwards and backwards.** `scripts/audit_agent.py` is the mechanical half —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-stack",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Production patterns for AI agent orchestrators \u2014 tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay \u2014 plus the wallet side of reselling LLM access. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "agent-stack": "bin/agent-stack.js"
@@ -2,7 +2,7 @@
2
2
  "name": "agent-stack",
3
3
  "displayName": "Agent Stack",
4
4
  "description": "Two skills: agent-orchestrator \u2014 tool-calling loops, multi-stage pipelines with checkpoints, provider routing with fallback, four-layer memory, context engineering, plus the wallet side of reselling LLM access; and agent-evals \u2014 run/trace/thread evals, judges, and fixtures grown from production.",
5
- "version": "0.8.0",
5
+ "version": "0.9.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -2,15 +2,15 @@
2
2
  name: agent-harness
3
3
  description: >-
4
4
  Use when the question is what the agent is TOLD rather than how its loop is wired — writing
5
- or fixing a system prompt, shaping tools so the model actually picks the right one,
6
- deciding whether a job wants a workflow or an agent, or choosing between ReAct, reflection,
7
- planning and voting. Also the audit direction: reviewing an agent system somebody else
8
- built, with tracks, evidence tiers and a prioritized plan instead of a score, plus a
9
- scanner for the defects that are mechanically visible. Triggers - "system prompt", "tool
10
- description", "the agent picks the wrong tool", "agent loops forever", "prompt engineering",
11
- "ReAct", "reflection", "workflow or agent", "audit this agent", "review our agent system",
12
- "системный промпт", "агент не вызывает тул", "аудит агента", "воркфлоу или агент". Not for
13
- the loop's plumbing, its evals, or the protocols it speaks — those are the sibling skills.
5
+ or fixing a system prompt, shaping tools so the model picks the right one, deciding whether
6
+ a job wants a workflow or an agent, or choosing between ReAct, reflection and voting. Also
7
+ auditing an agent system somebody else built: tracks, evidence tiers and a prioritized plan
8
+ instead of a score, plus a scanner. Carries Pi as a worked kernel implementation SDK, RPC
9
+ and extension seams — for embedding or extending a harness. Triggers - "system prompt",
10
+ "tool description", "agent picks the wrong tool", "agent loops forever", "prompt
11
+ engineering", "ReAct", "workflow or agent", "audit this agent", "embed an agent", "agent
12
+ SDK", "Pi harness", "системный промпт", "агент не вызывает тул", "аудит агента", "встроить
13
+ агента". Not for the loop's plumbing, its evals, or its protocols — those are siblings.
14
14
  ---
15
15
 
16
16
  # Agent harness — what the agent is told, and how to audit what someone else told theirs
@@ -95,6 +95,8 @@ and `test/validate.py` fails the build on a reference that does not say when it
95
95
  | [`references/techniques.md`](references/techniques.md) | you are choosing between **ReAct, reflection, voting, planning** and the rest — every entry carries a verdict for production, not a benchmark score |
96
96
  | [`references/layers.md`](references/layers.md) | deciding **what your harness owns** — kernel, workbench and product layers, and why permission boundaries are usually somebody else's job |
97
97
  | [`references/audit.md`](references/audit.md) | reviewing **an agent system you did not build** — seven tracks, evidence tiers, and a prioritized plan |
98
+ | [`references/pi.md`](references/pi.md) | you want the doctrine above as a **worked implementation** — Pi's sessions, compaction, config, skills, trust and containerization, each matched to the rule it implements, and the places it deliberately disagrees |
99
+ | [`references/pi-sdk.md`](references/pi-sdk.md) | you are **embedding or extending** a harness — the SDK, the RPC protocol, JSON mode, and the eight extension seams where a permission gate, a context rewrite or a cost hook can actually live |
98
100
 
99
101
  **`scripts/audit_agent.py`** — the mechanical half of the audit. It finds what is visible
100
102
  without understanding intent (an unbounded loop, a tool with no description, a swallowed
@@ -62,6 +62,10 @@ surrounds it. Read as a weakness, it looks like a missing feature. Read as archi
62
62
  is a clear statement: *a harness that also claims to be a sandbox is claiming a guarantee it
63
63
  cannot keep*, because it runs in the same process as the code it would be confining.
64
64
 
65
+ **Worked out in full in `pi.md` and `pi-sdk.md`** — the kernel layer read end to end, with
66
+ each mechanism matched to the rule it implements and its divergences named. Use it as the
67
+ concrete example when this file's abstractions need a body.
68
+
65
69
  Two consequences worth stating plainly:
66
70
 
67
71
  - **A harness advertising "safe tool execution" without an OS-level or container boundary is
@@ -0,0 +1,318 @@
1
+ # Building on Pi — SDK, RPC, and the extension seams
2
+
3
+ **Load this when:** embedding an agent in your own process, driving one from another
4
+ language, or extending a harness — and you want a real API surface rather than a sketch.
5
+
6
+ **Spec pinned:** `@earendil-works/pi-coding-agent`, `pi.dev/docs/latest` (sdk, rpc, json, extensions, custom-provider) · read 2026-08-15
7
+
8
+ Read `pi.md` first for the harness itself. This file is the programmable half, and its
9
+ value is the **seams**: the eight or so places a real harness lets you intervene, each
10
+ matched to the rule it lets you implement.
11
+
12
+ ## Contents
13
+
14
+ - Choosing a way in
15
+ - The SDK
16
+ - Custom tools
17
+ - RPC: driving it from any language
18
+ - JSON mode, and why it differs
19
+ - The extension API
20
+ - The seams that matter
21
+ - Custom providers
22
+ - Traps
23
+
24
+ ## Choosing a way in
25
+
26
+ | You want | Use | Because |
27
+ |---|---|---|
28
+ | your process owns the loop, in Node | **SDK** | direct objects, no serialization |
29
+ | to drive an agent from Python, Go, a UI | **RPC** | JSONL over stdin/stdout, bidirectional |
30
+ | to consume a run's events, one shot | **JSON mode** | delta-only stream, linear in size |
31
+ | a scripted answer | **print** | `pi -p` |
32
+ | to change behaviour rather than call it | **an extension** | it runs inside the loop |
33
+
34
+ **The distinction people get wrong:** RPC and JSON both emit events, but only RPC accepts
35
+ commands. If you need to steer mid-run, it is RPC.
36
+
37
+ ## The SDK
38
+
39
+ ```bash
40
+ npm install @earendil-works/pi-coding-agent
41
+ ```
42
+
43
+ ```javascript
44
+ import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
45
+
46
+ const modelRuntime = await ModelRuntime.create();
47
+ const { session } = await createAgentSession({
48
+ sessionManager: SessionManager.inMemory(),
49
+ modelRuntime,
50
+ });
51
+ ```
52
+
53
+ **`AgentSession`** — `prompt(text, options?)`, `steer(text)` and `followUp(text)` to queue
54
+ during streaming, `subscribe(listener)`, `setModel()`, `setThinkingLevel()`, `compact()`,
55
+ `abort()`, `dispose()`.
56
+
57
+ **Events** arrive structured:
58
+
59
+ ```javascript
60
+ session.subscribe((event) => {
61
+ if (event.type === "message_update" &&
62
+ event.assistantMessageEvent.type === "text_delta") {
63
+ process.stdout.write(event.assistantMessageEvent.delta);
64
+ }
65
+ });
66
+ ```
67
+
68
+ **`SessionManager`** factories decide persistence: `inMemory()`, `create(cwd)`,
69
+ `continueRecent(cwd)`, `open(filePath)`. **`AgentSessionRuntime`** handles replacement —
70
+ `newSession()`, `switchSession(path)`, `fork(entryId)`.
71
+
72
+ **`createAgentSession()` options** worth knowing: `model` (from `@earendil-works/pi-ai`),
73
+ `thinkingLevel` (`"off"` … `"max"`), `tools` (names to enable), `cwd`, `agentDir`
74
+ (defaults `~/.pi/agent`), `resourceLoader`, `settingsManager`.
75
+
76
+ **`ModelRuntime`** carries credentials and availability:
77
+
78
+ ```javascript
79
+ const modelRuntime = await ModelRuntime.create({
80
+ allowModelNetwork: true,
81
+ modelRefreshTimeoutMs: 15_000,
82
+ });
83
+ await modelRuntime.setRuntimeApiKey("anthropic", "sk-key");
84
+ const available = await modelRuntime.getAvailable();
85
+ ```
86
+
87
+ Resolution: runtime overrides → `auth.json` → environment.
88
+
89
+ **`DefaultResourceLoader`** discovers extensions, skills and prompts, and is where you
90
+ override the system prompt for an embedded agent:
91
+
92
+ ```javascript
93
+ const loader = new DefaultResourceLoader({
94
+ cwd: process.cwd(),
95
+ additionalExtensionPaths: ["/path/to/extension.ts"],
96
+ systemPromptOverride: () => "Custom system prompt",
97
+ });
98
+ await loader.reload();
99
+ ```
100
+
101
+ **`allowModelNetwork` and `systemPromptOverride` are the two options an embedded agent
102
+ almost always needs** — the first because a server should not discover models at runtime
103
+ unless you meant it, the second because the default prompt is a coding agent's and yours
104
+ probably is not.
105
+
106
+ ## Custom tools
107
+
108
+ ```javascript
109
+ const myTool = defineTool({
110
+ name: "my_tool",
111
+ description: "Does something useful",
112
+ parameters: Type.Object({ input: Type.String() }),
113
+ execute: async (_id, params) => ({
114
+ content: [{ type: "text", text: `Result: ${params.input}` }],
115
+ details: {},
116
+ }),
117
+ });
118
+ ```
119
+
120
+ Passed as `customTools: [myTool]`. Built-ins: `read`, `bash`, `edit`, `write`, `grep`,
121
+ `find`, `ls`.
122
+
123
+ **`details` is not decoration.** It is how a tool result carries structured state into the
124
+ session, and Pi's own guidance is to rebuild in-memory state after a restart by walking
125
+ `ctx.sessionManager.getBranch()` and reading it. That is **structured note-taking**
126
+ (`techniques.md`) with a durable home.
127
+
128
+ Write the `description` to `tools.md`'s standard — this is the same field, and the same
129
+ leverage.
130
+
131
+ ## RPC: driving it from any language
132
+
133
+ ```bash
134
+ pi --mode rpc [--provider … --model … --name … --no-session --session-dir …]
135
+ ```
136
+
137
+ JSON Lines over stdin/stdout: **commands** in, **responses** (`type: "response"`)
138
+ acknowledging them, **events** streaming asynchronously.
139
+
140
+ > **Framing warning, quoted because it bites in exactly one language at a time:** *"Split
141
+ > records on `\n` only; accept optional `\r\n` input by stripping a trailing `\r`."* Some
142
+ > standard line readers split on Unicode separators too, and a model that emits one inside
143
+ > a string will then desynchronize your parser.
144
+
145
+ **Commands**, by group:
146
+
147
+ | Group | Commands |
148
+ |---|---|
149
+ | Prompting | `prompt`, `steer` (delivered after the current tool), `follow_up`, `abort` |
150
+ | State | `get_state`, `get_messages`, `set_model`, `cycle_model`, `set_thinking_level`, `set_steering_mode`, `set_follow_up_mode` |
151
+ | Sessions | `new_session`, `switch_session`, `fork`, `clone`, `get_session_stats`, `export_html`, `set_session_name` |
152
+ | Execution | `bash`, `compact`, `set_auto_compaction`, `set_auto_retry` |
153
+ | Introspection | `get_available_models`, `get_commands`, `get_fork_messages`, `get_entries`, `get_tree` |
154
+
155
+ **Event lifecycle**, in order:
156
+
157
+ ```
158
+ agent_start → turn_start → message_start → message_update* → message_end
159
+ → tool_execution_start → tool_execution_update* → tool_execution_end
160
+ → turn_end → agent_end → agent_settled
161
+ ```
162
+
163
+ Plus `queue_update`, `compaction_start/end`, `auto_retry_start/end`,
164
+ `bash_execution_update` (correlated by the command's `id`), and `extension_error`.
165
+
166
+ **`agent_settled` is the one to wait on, not `agent_end`** — it means no further auto-retry
167
+ is queued. A client that treats `agent_end` as final will occasionally act on a run that is
168
+ about to continue.
169
+
170
+ Message shapes are stable and worth matching: `UserMessage`, `AssistantMessage` (with
171
+ `model`, `usage`, `stopReason`), `ToolResultMessage` (`toolCallId`, `toolName`, `isError`),
172
+ `BashExecutionMessage` (`command`, `output`, `exitCode`, `cancelled`).
173
+
174
+ **Extensions can ask the user something over RPC**, which is the part most integrations
175
+ forget. `extension_ui_request` events carry `select`, `confirm`, `input`, `editor` and
176
+ expect an `extension_ui_response` with the matching `id`; `notify`, `setStatus`, `setWidget`
177
+ are fire-and-forget.
178
+
179
+ ```json
180
+ {"type": "extension_ui_request", "id": "uuid-1", "method": "select", "title": "Choose", "options": ["A", "B"]}
181
+ {"type": "extension_ui_response", "id": "uuid-1", "value": "A"}
182
+ ```
183
+
184
+ **A client that ignores these hangs the agent** whenever an extension asks a question. Not
185
+ implementing them is a decision; not knowing about them is an outage.
186
+
187
+ ## JSON mode, and why it differs
188
+
189
+ `pi --mode json "…"` streams the same lifecycle, opening with a header:
190
+
191
+ ```json
192
+ {"type":"session","version":3,"id":"uuid","timestamp":"...","cwd":"/path"}
193
+ ```
194
+
195
+ **`message_update` records are delta-only** — they omit the cumulative `message` field and
196
+ `assistantMessageEvent.partial` *"to keep stream size linear."* Consumers assemble text from
197
+ `contentIndex` and `delta`.
198
+
199
+ That is a deliberate trade: RPC gives you snapshots you can resync from, JSON gives you a
200
+ stream that does not grow quadratically. **Pick JSON for pipelines, RPC for UIs.**
201
+
202
+ ## The extension API
203
+
204
+ Auto-discovered from `~/.pi/agent/extensions/*.ts` (global), `.pi/extensions/*.ts`
205
+ (project, after trust) or an `extensions` array in settings. A file, a directory with
206
+ `index.ts`, or a package with its own `node_modules`.
207
+
208
+ ```typescript
209
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
210
+
211
+ export default function (pi: ExtensionAPI) {
212
+ // may be async — do initialization here, not resource startup
213
+ }
214
+ ```
215
+
216
+ **Registration surface:**
217
+
218
+ ```typescript
219
+ pi.registerTool({ name, label, description, promptSnippet, promptGuidelines,
220
+ parameters, prepareArguments?, execute, renderCall?, renderResult? })
221
+ pi.registerCommand(name, { description, getArgumentCompletions?, handler })
222
+ pi.registerProvider(name, config) / pi.unregisterProvider(name)
223
+ pi.registerMessageRenderer / registerEntryRenderer / registerMarkdownTransformer
224
+ pi.registerShortcut(shortcut, options) / pi.registerFlag(name, options)
225
+ pi.on(eventName, handler) / pi.events.on|emit
226
+ pi.getActiveTools() / getAllTools() / setActiveTools(names)
227
+ pi.setModel(model) / getThinkingLevel() / setThinkingLevel(level)
228
+ pi.sendMessage / sendUserMessage / appendEntry / setSessionName / setLabel
229
+ pi.exec(command, args, options?)
230
+ ```
231
+
232
+ **`promptSnippet` and `promptGuidelines` deserve attention**: a tool contributes not only a
233
+ schema but a line to the system prompt and a set of guidelines. That is `system-prompt.md`'s
234
+ *"tool policy belongs in the prompt"* built into the registration call, which is the right
235
+ place for it — the policy cannot drift from the tool because they are declared together.
236
+
237
+ **Context (`ctx`)** in every handler: `ui`, `mode` (`"tui" | "rpc" | "json" | "print"`),
238
+ `hasUI`, `cwd`, `isProjectTrusted()`, `sessionManager`, `modelRegistry`, `model`,
239
+ `thinkingLevel`, `signal`, `isIdle()`, `abort()`, `getContextUsage()`, `compact(options)`,
240
+ `getSystemPrompt()`. Commands additionally get `newSession()`, `fork()`, `navigateTree()`,
241
+ `switchSession()`, `waitForIdle()`, `reload()`.
242
+
243
+ **`ctx.mode` and `ctx.hasUI` are how an extension stays honest** across surfaces: an
244
+ extension that calls `ctx.ui.confirm()` unconditionally works in the TUI and hangs in a
245
+ pipeline unless the client implements the UI sub-protocol.
246
+
247
+ ## The seams that matter
248
+
249
+ Pi exposes ~30 events. These are the ones that let you implement doctrine this pack
250
+ otherwise only describes:
251
+
252
+ | Event | What it lets you do | Implements |
253
+ |---|---|---|
254
+ | **`tool_call`** — *can block* | refuse a call before it runs, per caller, per argument | the per-hop permission gate of `agent-orchestrator/references/governance.md`; track 6 of `audit.md` |
255
+ | **`tool_result`** — *middleware chain* | rewrite, truncate or annotate a result; handlers see the previous handler's output | tool-output offload and token efficiency (`tools.md`) |
256
+ | **`context`** | modify messages **before** the provider call | the compaction ladder's upper rungs, and typed carryover |
257
+ | **`before_agent_start`** | inject a message, modify the system prompt | capability-aware prompt assembly (`system-prompt.md`) |
258
+ | **`before_provider_headers` / `before_provider_request` / `after_provider_response`** | mutate headers, inspect or replace the payload, handle the response | provider routing, proxying and cost attribution |
259
+ | **`session_before_compact` / `session_compact`** | decide what survives | *preserve decisions and open questions, not the discussion* |
260
+ | **`input`** — *can intercept* | rewrite or absorb a user message | routing before the loop |
261
+ | **`resources_discover`** | add skill/prompt/theme paths at runtime | dynamic capability |
262
+ | **`project_trust`** | act on the trust decision | the delegation boundary of `layers.md` |
263
+
264
+ **`tool_call` blocking is the single most important one for an audit.** It is where a
265
+ per-tool, per-caller policy can actually live in a Pi-based system — so its absence is a
266
+ finding, and its presence is where you read the policy.
267
+
268
+ **Lifecycle discipline**, from Pi's own guidance and worth generalizing: start background
269
+ resources in `session_start` and clean up in `session_shutdown`, **never from the factory**;
270
+ after `/new`, `/resume` or `/fork` a fresh context arrives and **stale `ctx` objects must not
271
+ be reused**; and tools that mutate files should use `withFileMutationQueue()` to avoid
272
+ racing the built-ins.
273
+
274
+ ## Custom providers
275
+
276
+ ```typescript
277
+ // route an existing provider through a proxy — baseUrl and/or headers only,
278
+ // and the existing model list is preserved
279
+ pi.registerProvider("anthropic", { baseUrl: "https://proxy.example.com" });
280
+
281
+ // or a whole new one
282
+ pi.registerProvider("my-llm", {
283
+ baseUrl: "https://api.my-llm.com/v1",
284
+ apiKey: "$MY_LLM_API_KEY",
285
+ api: "openai-completions",
286
+ models: [{
287
+ id: "my-llm-large", name: "My LLM Large", reasoning: true,
288
+ input: ["text", "image"],
289
+ cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
290
+ contextWindow: 200000, maxTokens: 16384,
291
+ }],
292
+ });
293
+ ```
294
+
295
+ For a non-standard API, implement `streamSimple`, pushing an `AssistantMessageEventStream`:
296
+ start → content (text, thinking blocks, tool calls) → done or error, updating usage and cost.
297
+
298
+ **The `cost` block is the hook for everything in
299
+ `agent-orchestrator/references/llm-proxy-billing.md`.** A provider that declares its per-token
300
+ cost makes attribution arithmetic rather than estimation — and a custom provider that omits
301
+ it silently makes every downstream number a guess.
302
+
303
+ Auth supports API keys with env interpolation, and OAuth with refresh, browser and
304
+ device-code flows.
305
+
306
+ ## Traps
307
+
308
+ - **Waiting on `agent_end` instead of `agent_settled`**, and acting on a run that continues.
309
+ - **Ignoring `extension_ui_request`** in a non-TUI client, and hanging the first time an
310
+ extension asks a question.
311
+ - **Splitting JSONL on anything but `\n`.** The docs warn about it; the failure is rare,
312
+ data-dependent and looks like corruption.
313
+ - **Reusing a `ctx` after a session replacement.** It points at the old session.
314
+ - **Starting background work in the extension factory** rather than `session_start`, so it
315
+ outlives the session and doubles on reload.
316
+ - **Registering a custom provider with no `cost`**, then trusting the spend numbers.
317
+ - **Assuming an extension is a boundary.** It runs in the Pi process, with the Pi process's
318
+ permissions — see `pi.md` → *trust*.
@@ -0,0 +1,241 @@
1
+ # Pi — a harness you can read, and what each of its parts implements
2
+
3
+ **Load this when:** you want a **worked example** of the harness doctrine, are choosing a
4
+ kernel to build on, or are auditing a system built on Pi.
5
+
6
+ **Spec pinned:** Pi (`@earendil-works/pi-coding-agent`, MIT, Earendil Inc.), `pi.dev/docs/latest` · read 2026-08-15
7
+
8
+ **Why this file exists.** Everything else in this skill states a rule; Pi is small enough
9
+ to read and complete enough to have made every one of those decisions in public. So each
10
+ section below says **what Pi does** and then **which rule it is an instance of** — the
11
+ value is in the second half. Where Pi disagrees with the doctrine, that is said too.
12
+
13
+ This is not a substitute for `pi.dev`. It moves faster than this file; the stamp above is
14
+ the honest boundary.
15
+
16
+ ## Contents
17
+
18
+ - What Pi is, and the stance underneath it
19
+ - Four ways to run it
20
+ - Sessions are a tree, not a log
21
+ - Compaction, with the actual numbers
22
+ - Configuration and precedence
23
+ - Skills, prompts and packages
24
+ - Trust, and the deliberate absence of a sandbox
25
+ - Containerization — three patterns, three threat models
26
+ - Providers and credentials
27
+ - Where Pi and this pack's doctrine differ
28
+ - Traps
29
+
30
+ ## What Pi is, and the stance underneath it
31
+
32
+ *"A minimal agent harness."* Its stated position is **primitives, not features**: it ships
33
+ `read`, `write`, `edit`, `bash` and a loop, and deliberately omits sub-agents and plan mode,
34
+ expecting you to build them as extensions.
35
+
36
+ **This is the kernel layer of `layers.md`, made concrete.** The omissions are the argument:
37
+ a kernel that shipped a plan mode would have chosen your planning shape for you. When
38
+ comparing Pi against a workbench, remember the comparison is across layers and will not
39
+ converge.
40
+
41
+ ## Four ways to run it
42
+
43
+ | Mode | Invocation | For |
44
+ |---|---|---|
45
+ | **Interactive TUI** | `pi` | a human at a terminal |
46
+ | **Print** | `pi -p "…"` | one shot, text out |
47
+ | **JSON** | `pi --mode json "…"` | events as JSON lines, for another tool's UI |
48
+ | **RPC** | `pi --mode rpc` | a long-lived subprocess you drive both ways |
49
+ | **Embedded** | the SDK | your process owns the loop |
50
+
51
+ **One agent, five front doors.** That separation — a core that does not know which surface
52
+ is attached — is the same shape `agent-orchestrator` describes when it insists the loop
53
+ must not know which provider answered. Details of the last three: `pi-sdk.md`.
54
+
55
+ ## Sessions are a tree, not a log
56
+
57
+ Sessions persist to `~/.pi/agent/sessions/` as **JSONL, one entry per line**, each carrying
58
+ an 8-character hex `id` and a `parentId`. The current position is a leaf; context is built
59
+ by walking leaf→root.
60
+
61
+ | Entry type | Holds |
62
+ |---|---|
63
+ | `session` | the header: `version` (currently **3**), `id`, `timestamp`, `cwd` |
64
+ | `SessionMessageEntry` | a message with its role and content |
65
+ | `ModelChangeEntry` / `ThinkingLevelChangeEntry` | mid-conversation switches, recorded rather than implied |
66
+ | `CompactionEntry` | a summary, with an optional `retainedTail` |
67
+ | `BranchSummaryEntry` | what an abandoned branch was about |
68
+ | `CustomEntry` / `CustomMessageEntry` | extension data — the second participates in context, the first does not |
69
+
70
+ Commands: `/tree` navigates within one file, `/fork` starts a new session from an earlier
71
+ prompt, `/clone` duplicates the active branch, `/export` writes HTML, `/share` uploads a
72
+ private gist. Flags: `pi -c` continues, `pi -r` browses, `--no-session` keeps nothing.
73
+
74
+ **This implements `agent-orchestrator/references/runtime.md` → *time travel and forking*.**
75
+ That file argues you must be able to fork a past checkpoint and debug **through the real
76
+ loop** rather than a reconstruction. A parent-pointer tree is what makes that cheap: no
77
+ copy, no replay, and the abandoned branch leaves a `BranchSummaryEntry` behind so the
78
+ context is not simply lost.
79
+
80
+ **Two design details worth stealing.** Model and thinking-level changes are *entries*, so a
81
+ session explains its own cost curve. And the version field is honest about migration —
82
+ v1 was linear, v2 introduced the tree, v3 unified role naming.
83
+
84
+ ## Compaction, with the actual numbers
85
+
86
+ Auto-compaction fires when `contextTokens > contextWindow - reserveTokens`.
87
+
88
+ | Setting | Default | Meaning |
89
+ |---|---|---|
90
+ | `reserveTokens` | 16,384 | held back for the response |
91
+ | `keepRecentTokens` | 20,000 | recent tail never summarized |
92
+
93
+ Preserved messages run from `firstKeptEntryId` onward and are sent alongside the summary.
94
+ `/compact [instructions]` runs it manually and the instructions steer the summary. Setting
95
+ `"enabled": false` disables the automatic path while leaving the manual one.
96
+
97
+ **Two caveats stated in the docs and worth carrying:** tool results are **truncated to 2,000
98
+ characters** while summarizing, and a turn larger than `keepRecentTokens` produces two
99
+ summaries that are then merged.
100
+
101
+ **This is the ladder from `agent-orchestrator/references/context-engineering.md` with one
102
+ rung.** Pi reserves, keeps a tail, and summarizes the rest. What that file adds and Pi
103
+ leaves to you: clearing old tool results *before* paying a summarizer, offloading a large
104
+ tool result to a file and keeping the path, and **typed carryover** — the observation that a
105
+ summarizer keeps the discussion and drops the state. Pi's `BranchSummaryEntry` and
106
+ `retainedTail` are the seams to hang that on.
107
+
108
+ ## Configuration and precedence
109
+
110
+ | File | Scope |
111
+ |---|---|
112
+ | `~/.pi/agent/settings.json` | global |
113
+ | `.pi/settings.json` | project — **overrides global, merging nested objects** |
114
+
115
+ Keys cluster into model and thinking (`defaultProvider`, `defaultModel`,
116
+ `defaultThinkingLevel`, `thinkingBudgets`), UI, network and retry (`retry` with `enabled`,
117
+ `maxRetries`, `baseDelayMs`; `httpProxy`, `transport`, timeouts), content handling
118
+ (`shellPath`, `npmCommand`, `defaultTools`), and resources (`packages`, `extensions`,
119
+ `skills`, `prompts`, `themes`).
120
+
121
+ **Merge, not replace, is the part that matters.** A project that wants one different model
122
+ should not have to restate the whole file — and a harness that replaced wholesale would
123
+ make every project config a copy that drifts.
124
+
125
+ ## Skills, prompts and packages
126
+
127
+ **Pi implements the Agent Skills standard**, with progressive disclosure: at startup it
128
+ scans skill locations and takes only `name` and `description` into the system prompt as XML;
129
+ the full `SKILL.md` loads when a task matches.
130
+
131
+ It discovers skills from `~/.pi/agent/skills/`, **`~/.agents/skills/`**, `.pi/skills/` and
132
+ `.agents/skills/` (project paths only after the project is trusted), from packages, from a
133
+ `skills` array in settings, and from `--skill <path>`.
134
+
135
+ > **Concretely relevant here: `~/.agents/skills/` is the ssheleg hub.** On the machine this
136
+ > file was written on, that directory holds 72 entries including every family skill, each
137
+ > with the `name` and `description` front matter Pi requires — so the family is already in a
138
+ > directory Pi reads. **Not verified by running Pi**, which is not installed here; this is a
139
+ > statement about the path and the front matter, not an observation of a load.
140
+
141
+ **Pi documents one deliberate divergence from the standard:** it allows a skill's `name` to
142
+ differ from its directory, calling that rule *"suboptimal for shared skill directories used
143
+ across multiple agent harnesses."* Which is exactly what `~/.agents/skills/` is. Note the
144
+ asymmetry before relying on it — `make-skill`'s validator enforces the strict rule, so a
145
+ skill built to Pi's leniency fails the family gate.
146
+
147
+ **Prompt templates** are Markdown in `~/.pi/agent/prompts/*.md`; the filename becomes the
148
+ command (`review.md` → `/review`). Front matter takes `description` and `argument-hint`
149
+ (`<required>`, `[optional]`). Arguments substitute as `$1`, `$@` / `$ARGUMENTS`,
150
+ `${1:-default}`, `${@:N}` and `${@:N:L}`. Discovery is **not recursive**.
151
+
152
+ **Packages** bundle extensions, skills, prompts and themes over npm or git, declared under a
153
+ `pi` key in `package.json` or by convention (`extensions/`, `skills/`, `prompts/`,
154
+ `themes/`). Installed with `pi install npm:@foo/bar@1.0.0`, `git:…`, an https URL, or a
155
+ path; `-l` writes to project settings for a team. Resource lists take globs with `!`
156
+ exclusions, `[]` for none, `+path` / `-path` to force.
157
+
158
+ ## Trust, and the deliberate absence of a sandbox
159
+
160
+ Pi *"runs with the permissions of the user account that starts it"* and treats files that
161
+ user can write as inside the same trust boundary.
162
+
163
+ **Project trust** is asked for when a repository carries `.pi/settings.json`, local
164
+ extensions, skills, prompts or themes, a `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md`, or
165
+ project agent skills in ancestor directories. Decisions persist in `~/.pi/agent/trust.json`.
166
+
167
+ What it buys, in the docs' own words: it *"prevents a repository from silently changing pi's
168
+ settings or extensions before you approve it"* — and explicitly **does not** protect against
169
+ untrusted code, prompts, or model output.
170
+
171
+ **There is no built-in sandbox, on purpose.** The stated reasons: a partial in-process
172
+ sandbox creates false assumptions, real isolation needs an OS or container boundary, and Pi
173
+ is meant to invoke project toolchains with full local access. And the sentence worth
174
+ quoting to anyone who claims otherwise about any harness:
175
+
176
+ > *"prompt injection from repository files, comments, documentation, context files, or build
177
+ > output is expected local-agent risk and cannot be reliably prevented by pi."*
178
+
179
+ **This is `layers.md` → *what a harness should delegate*, stated by the project itself.** A
180
+ harness that also claimed to be a sandbox would be claiming a guarantee it cannot keep from
181
+ inside the same process. **For an audit this changes the finding**: "no permission model" is
182
+ not a defect here, it is a delegation — so audit what surrounds the process (`audit.md`,
183
+ track 6).
184
+
185
+ ## Containerization — three patterns, three threat models
186
+
187
+ | Pattern | Isolates | Credentials live |
188
+ |---|---|---|
189
+ | **Gondolin extension** | built-in tools and `!` commands, in a micro-VM | on the host — auth never enters the boundary |
190
+ | **Plain Docker** | the whole Pi process | **inside the container** |
191
+ | **OpenShell** | filesystem, process, network, credentials by policy | per policy; local or remote gateway |
192
+
193
+ ```bash
194
+ docker run --rm -it -e ANTHROPIC_API_KEY -v "$PWD:/workspace" pi-sandbox
195
+ ```
196
+
197
+ **The distinction that decides which you want: extensions execute wherever the Pi process
198
+ runs.** Host-side Pi routing tools into a micro-VM keeps auth local and isolates execution;
199
+ containerized Pi needs the key inside the boundary. Mounting `/root/.pi/agent` as a named
200
+ volume keeps settings isolated — mounting your host directory *"exposes host auth and
201
+ session files to the container"*, which is the opposite of the intent.
202
+
203
+ ## Providers and credentials
204
+
205
+ Two paths: **subscription OAuth** via `/login` (ChatGPT Plus/Pro, Claude Pro/Max, GitHub
206
+ Copilot, xAI, OpenRouter, Radius) with refresh handled, and **API keys**. 30+ providers.
207
+
208
+ Resolution order — **CLI `--api-key` → `auth.json` → environment variable → custom provider
209
+ keys in `models.json`**. `auth.json` is written `0600` and takes priority over the
210
+ environment, which is the ordering you want: an explicit file beats an inherited variable.
211
+
212
+ Keys support literals, `$ENV_VAR` interpolation, and shell commands
213
+ (`!security find-generic-password …`) — so a key can live in a system keychain rather than a
214
+ file. Registering a custom provider is `pi-sdk.md`.
215
+
216
+ ## Where Pi and this pack's doctrine differ
217
+
218
+ Named rather than smoothed over:
219
+
220
+ - **No iteration guard is documented as a first-class setting.** `agent-orchestrator` treats
221
+ a bounded loop as non-negotiable. Pi has `auto_retry` and abort; a max-iteration ceiling
222
+ is yours to add. Check this first when auditing a Pi-based system.
223
+ - **No sub-agents.** Deliberate. `agent-orchestrator`'s sub-agent protocol and
224
+ `techniques.md`'s "distilled summary, never a transcript" are things you build here.
225
+ - **Skill naming leniency** contradicts the standard `make-skill` enforces (above).
226
+ - **Compaction is one rung**, not the ladder.
227
+
228
+ None of these is a defect in a kernel. They are the difference between a harness and a
229
+ platform, and they are the work you are signing up for.
230
+
231
+ ## Traps
232
+
233
+ - **Reading the omissions as gaps.** They are the layer boundary. If you need all of them
234
+ filled, you wanted a workbench.
235
+ - **Assuming trust means safety.** It means the repository did not silently change your
236
+ configuration. Nothing more, and the docs say so.
237
+ - **Mounting your host `~/.pi/agent` into a container** and calling the result isolated.
238
+ - **Building on skill-name leniency** and then failing a stricter harness's validator.
239
+ - **Forgetting `keepRecentTokens` against a large turn.** One turn bigger than the budget
240
+ becomes two summaries merged — surprising if you are diffing summaries.
241
+ - **Treating the docs here as current.** Check the stamp; Pi moves.