@ssheleg/agent-stack 0.8.0 → 0.10.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 +132 -0
- package/README.md +18 -4
- package/package.json +1 -1
- package/plugins/agent-stack/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-stack/skills/agent-evals/SKILL.md +44 -3
- package/plugins/agent-stack/skills/agent-harness/SKILL.md +37 -9
- package/plugins/agent-stack/skills/agent-harness/references/layers.md +4 -0
- package/plugins/agent-stack/skills/agent-harness/references/pi-sdk.md +318 -0
- package/plugins/agent-stack/skills/agent-harness/references/pi.md +241 -0
- package/plugins/agent-stack/skills/agent-harness/scripts/audit_agent.py +100 -32
- package/plugins/agent-stack/skills/agent-orchestrator/SKILL.md +100 -101
- package/plugins/agent-stack/skills/agent-orchestrator/references/graph-engineering.md +327 -0
- package/plugins/agent-stack/skills/agent-orchestrator/references/llm-proxy-billing.md +1 -1
- package/plugins/agent-stack/skills/agent-orchestrator/references/patterns.md +38 -14
|
@@ -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.
|