@counterposition/skill-pi 0.87.1 → 0.99.1
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/README.md +8 -7
- package/package.json +15 -4
- package/skills/pi/README.md +8 -7
- package/skills/pi/SKILL.md +12 -6
- package/skills/pi/evals/evals.json +81 -0
- package/skills/pi/examples/classifier-gate.ts +46 -0
- package/skills/pi/examples/nested-tools.ts +52 -0
- package/skills/pi/examples/structured-tool.ts +61 -0
- package/skills/pi/references/codemode.md +88 -0
- package/skills/pi/references/extensions.md +36 -3
- package/skills/pi/references/mcp.md +122 -0
- package/skills/pi/references/packages.md +11 -5
- package/skills/pi/references/patterns.md +34 -0
- package/skills/pi/references/providers.md +100 -11
- package/skills/pi/references/sdk.md +78 -12
- package/skills/pi/references/settings.md +28 -11
package/README.md
CHANGED
|
@@ -28,15 +28,16 @@ If you develop inside this repo, Pi already loads the skill via `.pi/settings.js
|
|
|
28
28
|
|
|
29
29
|
## What this covers
|
|
30
30
|
|
|
31
|
-
Pi's architecture and package layout. Skill authoring, discovery, and routing. Extensions, tools, and UI hooks. Settings, models, providers, and packages. Sessions, forks, and compaction. The SDK and RPC interface.
|
|
31
|
+
Pi's architecture and package layout. Skill authoring, discovery, and routing. Extensions, tools, and UI hooks. MCP servers, codemode, and nested tool calls. Settings, models, providers, classifiers, and packages. Sessions, forks, and compaction. The SDK and RPC interface. Checked against Pi 0.99.1.
|
|
32
32
|
|
|
33
33
|
The reference material lives in `references/` as task-specific documents — not a single monolith, but pieces sized for injection into a conversation when relevant.
|
|
34
34
|
|
|
35
35
|
## Files
|
|
36
36
|
|
|
37
|
-
| Path | Purpose
|
|
38
|
-
| ------------------ |
|
|
39
|
-
| `SKILL.md` | Trigger rules and routing description
|
|
40
|
-
| `references/` | Task-specific Pi reference documents
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
37
|
+
| Path | Purpose |
|
|
38
|
+
| ------------------ | --------------------------------------------- |
|
|
39
|
+
| `SKILL.md` | Trigger rules and routing description |
|
|
40
|
+
| `references/` | Task-specific Pi reference documents |
|
|
41
|
+
| `examples/` | Extension examples that type-check against Pi |
|
|
42
|
+
| `evals/evals.json` | Example evaluation cases |
|
|
43
|
+
| `LICENSE.md` | GPLv3 license text |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@counterposition/skill-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.1",
|
|
4
4
|
"description": "Pi coding agent reference skill — extensions, settings, providers, SDK/RPC, packages, and .pi/ config",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-skill",
|
|
@@ -25,9 +25,18 @@
|
|
|
25
25
|
"LICENSE.md",
|
|
26
26
|
"package.json"
|
|
27
27
|
],
|
|
28
|
+
"type": "module",
|
|
28
29
|
"publishConfig": {
|
|
29
30
|
"access": "public"
|
|
30
31
|
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@earendil-works/pi-ai": "^0.99.1",
|
|
34
|
+
"@earendil-works/pi-coding-agent": "^0.99.1",
|
|
35
|
+
"@types/node": "^25.5.0",
|
|
36
|
+
"typebox": "^1.3.16",
|
|
37
|
+
"typescript": "^6.0.2",
|
|
38
|
+
"vitest": "^4.1.2"
|
|
39
|
+
},
|
|
31
40
|
"engines": {
|
|
32
41
|
"node": ">=24"
|
|
33
42
|
},
|
|
@@ -40,9 +49,11 @@
|
|
|
40
49
|
"lint": "pnpm run lint:docs",
|
|
41
50
|
"lint:docs": "markdownlint-cli2 \"README.md\"",
|
|
42
51
|
"lint:docs:fix": "markdownlint-cli2 --fix \"README.md\"",
|
|
43
|
-
"format": "oxfmt \"README.md\" \"package.json\"",
|
|
44
|
-
"format:check": "oxfmt \"README.md\" \"package.json\" --check",
|
|
45
|
-
"
|
|
52
|
+
"format": "oxfmt \"README.md\" \"package.json\" \"tests\" \"vitest.config.ts\"",
|
|
53
|
+
"format:check": "oxfmt \"README.md\" \"package.json\" \"tests\" \"vitest.config.ts\" --check",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"check": "pnpm run lint && pnpm run format:check && pnpm run typecheck && pnpm run test && pnpm run pack:check",
|
|
46
57
|
"pack:check": "pnpm pack --pack-destination ../../.pack"
|
|
47
58
|
}
|
|
48
59
|
}
|
package/skills/pi/README.md
CHANGED
|
@@ -28,15 +28,16 @@ If you develop inside this repo, Pi already loads the skill via `.pi/settings.js
|
|
|
28
28
|
|
|
29
29
|
## What this covers
|
|
30
30
|
|
|
31
|
-
Pi's architecture and package layout. Skill authoring, discovery, and routing. Extensions, tools, and UI hooks. Settings, models, providers, and packages. Sessions, forks, and compaction. The SDK and RPC interface.
|
|
31
|
+
Pi's architecture and package layout. Skill authoring, discovery, and routing. Extensions, tools, and UI hooks. MCP servers, codemode, and nested tool calls. Settings, models, providers, classifiers, and packages. Sessions, forks, and compaction. The SDK and RPC interface. Checked against Pi 0.99.1.
|
|
32
32
|
|
|
33
33
|
The reference material lives in `references/` as task-specific documents — not a single monolith, but pieces sized for injection into a conversation when relevant.
|
|
34
34
|
|
|
35
35
|
## Files
|
|
36
36
|
|
|
37
|
-
| Path | Purpose
|
|
38
|
-
| ------------------ |
|
|
39
|
-
| `SKILL.md` | Trigger rules and routing description
|
|
40
|
-
| `references/` | Task-specific Pi reference documents
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
37
|
+
| Path | Purpose |
|
|
38
|
+
| ------------------ | --------------------------------------------- |
|
|
39
|
+
| `SKILL.md` | Trigger rules and routing description |
|
|
40
|
+
| `references/` | Task-specific Pi reference documents |
|
|
41
|
+
| `examples/` | Extension examples that type-check against Pi |
|
|
42
|
+
| `evals/evals.json` | Example evaluation cases |
|
|
43
|
+
| `LICENSE.md` | GPLv3 license text |
|
package/skills/pi/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi
|
|
3
|
-
description: "Provides Pi-specific guidance for extensions, skills, `.pi/` config, `settings.json`, `models.json`, packages, providers, themes, SDK/RPC, sessions, and compaction in Pi, the coding agent. Use when the user mentions Pi or Pi-specific terms such as `.pi/`, `SKILL.md`, `createAgentSession()`, `thinkingLevel`, `session_before_compact`, `pi-ai`, `pi-tui`, `pi-agent-core`, or `pi-coding-agent`. Not for Raspberry Pi hardware, the math constant, or unrelated generic tooling."
|
|
3
|
+
description: "Provides Pi-specific guidance for extensions, skills, `.pi/` config, `settings.json`, `models.json`, `mcp.json`, MCP servers, codemode, packages, providers, themes, SDK/RPC, sessions, and compaction in Pi, the coding agent. Use when the user mentions Pi or Pi-specific terms such as `.pi/`, `SKILL.md`, `createAgentSession()`, `thinkingLevel`, `session_before_compact`, `ctx.executeTool`, `pi-ai`, `pi-tui`, `pi-agent-core`, or `pi-coding-agent`. Not for Raspberry Pi hardware, the math constant, or unrelated generic tooling."
|
|
4
4
|
license: GPLv3
|
|
5
5
|
compatibility: "Pi-specific guidance for agents that support Agent Skills"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Pi Coding Agent
|
|
9
9
|
|
|
10
|
-
Pi is a minimal terminal coding harness. Default tools: `read`, `bash`, `edit`, `write`; `grep`, `find`, `ls`, and the Windows `powershell` tool
|
|
10
|
+
Pi is a minimal terminal coding harness. Default tools: `read`, `bash`, `edit`, `write`; `grep`, `find`, `ls`, and the Windows `powershell` tool are opt-in via the `defaultTools` setting or `--tools`. Built-in extensions (Pi 0.99.0) add MCP servers, the `codemode` tool (model-written JavaScript that calls other tools), and `tool_search`. `codemode` and `tool_search` start inactive: enable them with `defaultTools`/`--tools`, or let MCP activate them when a server needs them. Modes: interactive (`pi`), print (`pi -p`), JSON (`--mode json`), RPC (`--mode rpc`), or embedded (`createAgentSession()`). Sub-agents, plan mode, and permission flows are intentionally left to extensions and Pi packages. Skill synced with Pi 0.99.1.
|
|
11
11
|
|
|
12
12
|
Pi's philosophy: **adapt Pi to your workflows, not the other way around**.
|
|
13
13
|
|
|
@@ -21,7 +21,8 @@ Core packages on npm (source: [github.com/earendil-works/pi](https://github.com/
|
|
|
21
21
|
| `@earendil-works/pi-agent-core` | Agent runtime with tool execution and state |
|
|
22
22
|
| `@earendil-works/pi-tui` | Terminal UI components |
|
|
23
23
|
| `@earendil-works/pi-coding-agent` | CLI, extensions, skills, sessions, settings (also exports `./client` for remote sessions and `./rpc-entry`) |
|
|
24
|
-
| `@earendil-works/pi-
|
|
24
|
+
| `@earendil-works/pi-codemode` | QuickJS sandbox that runs model-written JavaScript against injected tools (Pi 0.99.0) |
|
|
25
|
+
| `@earendil-works/pi-mcp` | Standalone MCP client: stdio and streamable HTTP, OAuth (Pi 0.99.0) |
|
|
25
26
|
| `@earendil-works/pi-client` / `@earendil-works/pi-protocol` | Experimental remote-session client and wire protocol (Pi 0.84.0) |
|
|
26
27
|
| `@earendil-works/pi-telemetry` | Vendor-neutral telemetry contracts (Pi 0.84.0) |
|
|
27
28
|
|
|
@@ -32,6 +33,7 @@ Core packages on npm (source: [github.com/earendil-works/pi](https://github.com/
|
|
|
32
33
|
├── settings.json # Global settings
|
|
33
34
|
├── auth.json # Credentials (0600 perms)
|
|
34
35
|
├── models.json # Custom provider/model definitions
|
|
36
|
+
├── mcp.json # MCP servers (mcp-auth.json holds their OAuth tokens)
|
|
35
37
|
├── keybindings.json # Keyboard shortcuts
|
|
36
38
|
├── extensions/ # Auto-discovered
|
|
37
39
|
├── skills/ # Auto-discovered
|
|
@@ -42,6 +44,7 @@ Core packages on npm (source: [github.com/earendil-works/pi](https://github.com/
|
|
|
42
44
|
<project>/
|
|
43
45
|
└── .pi/ # Project-local config (overrides global)
|
|
44
46
|
├── settings.json
|
|
47
|
+
├── mcp.json # Project MCP servers (read only once trusted)
|
|
45
48
|
├── extensions/
|
|
46
49
|
├── skills/
|
|
47
50
|
├── prompts/
|
|
@@ -53,6 +56,7 @@ Core packages on npm (source: [github.com/earendil-works/pi](https://github.com/
|
|
|
53
56
|
|
|
54
57
|
- **Extensions** — TypeScript modules with full system access. Hook into Pi's lifecycle to register tools, intercept calls, add commands, build UI. → `references/extensions.md`
|
|
55
58
|
- **Skills** — Markdown capability packages (`SKILL.md` + frontmatter) following the Agent Skills standard. Pi loads names + descriptions into the system prompt; bodies load on demand. → `references/skills.md`
|
|
59
|
+
- **MCP & codemode** — Built-in MCP client configured by `mcp.json` or `pi.registerMcpServer()`; server tools default to `codemode` exposure, reachable from `codemode` scripts rather than declared to the model. Codemode scripts get a tool's `structuredContent` when it declares `outputSchema`, and their nested calls still reach `tool_call` gates. → `references/mcp.md` and `references/codemode.md`
|
|
56
60
|
- **Settings** — Hierarchical JSON: project `.pi/settings.json` merges over global `~/.pi/agent/settings.json`. → `references/settings.md`
|
|
57
61
|
- **Packages** — Bundles of extensions/skills/prompts/themes via npm, git, or local paths. Installed with `pi install`. → `references/packages.md`
|
|
58
62
|
- **Project trust** — Pi 0.79+ asks before loading project-local settings, resources, and packages; decisions persist in `~/.pi/agent/trust.json`. `--approve`/`--no-approve` override per run; `defaultProjectTrust` sets the non-interactive fallback. → `references/settings.md`
|
|
@@ -66,11 +70,13 @@ Core packages on npm (source: [github.com/earendil-works/pi](https://github.com/
|
|
|
66
70
|
| Task | Reference |
|
|
67
71
|
|------|-----------|
|
|
68
72
|
| Writing an extension (tools, commands, events, UI) | `references/extensions.md` |
|
|
73
|
+
| Connecting MCP servers (`mcp.json`, `pi mcp`, exposure) | `references/mcp.md` |
|
|
74
|
+
| Codemode scripts, tool exposure, nested tool calls, permission gates for them | `references/codemode.md` |
|
|
69
75
|
| Creating a skill | `references/skills.md` |
|
|
70
76
|
| Configuring Pi (settings, env vars, CLI flags) | `references/settings.md` |
|
|
71
77
|
| Building a shareable package | `references/packages.md` |
|
|
72
78
|
| Embedding Pi programmatically | `references/sdk.md` |
|
|
73
|
-
| Adding LLM providers or models | `references/providers.md` |
|
|
74
|
-
| Looking for a recipe or pattern | `references/patterns.md` |
|
|
79
|
+
| Adding LLM providers or models, classifiers, image models, virtual models | `references/providers.md` |
|
|
80
|
+
| Looking for a recipe or pattern | `references/patterns.md` (compilable examples in `examples/`) |
|
|
75
81
|
|
|
76
|
-
For exact CLI flags run `pi --help` or read the relevant reference. Resource flags (`--no-extensions`, `--no-skills`, `--no-context-files`, `--no-builtin-tools`, etc.) are documented in `references/settings.md
|
|
82
|
+
For exact CLI flags run `pi --help` or read the relevant reference. Resource flags (`--no-extensions`, `--no-skills`, `--no-context-files`, `--no-builtin-tools`, etc.) are documented in `references/settings.md`; `--no-extensions` also disables the built-in extensions, and `-e builtin:<name>` loads one back.
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"Provides models.json with Ollama baseUrl containing localhost:11434",
|
|
28
28
|
"Uses api type openai-completions (correct for Ollama)",
|
|
29
29
|
"Includes model entry with id llama3.2",
|
|
30
|
+
"Writes models.json providers as an object keyed by provider id (e.g. \"ollama\"), not an array",
|
|
30
31
|
"Provides settings.json with compaction.enabled set to false",
|
|
31
32
|
"Sets sessionDir to ~/pi-sessions",
|
|
32
33
|
"Correct file paths: ~/.pi/agent/models.json and ~/.pi/agent/settings.json"
|
|
@@ -45,6 +46,86 @@
|
|
|
45
46
|
"SKILL.md uses relative paths to reference files",
|
|
46
47
|
"Body routes to the correct reference doc based on the user's framework choice"
|
|
47
48
|
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": 4,
|
|
52
|
+
"prompt": "Add the GitHub MCP server (https://api.githubcopilot.com/mcp/, token in GITHUB_TOKEN) to Pi for all my projects. It has ~90 tools, so keep them out of the model's tool list, but make search_code directly callable. Check that it works.",
|
|
53
|
+
"expected_output": "An entry in ~/.pi/agent/mcp.json under mcpServers with the url, an Authorization header referencing ${GITHUB_TOKEN}, a non-direct server exposure (codemode, codemode-deferred, or deferred), and toolExposure making search_code direct; then pi mcp list to verify and /reload for a running session.",
|
|
54
|
+
"files": [],
|
|
55
|
+
"assertions": [
|
|
56
|
+
"Writes the server to ~/.pi/agent/mcp.json (global), not settings.json and not the project .pi/mcp.json",
|
|
57
|
+
"Uses the mcpServers object with a url field (streamable HTTP), not an sse type",
|
|
58
|
+
"Keeps the secret out of the file with \"Authorization\": \"Bearer ${GITHUB_TOKEN}\" (or pi mcp add --bearer-token-env-var GITHUB_TOKEN)",
|
|
59
|
+
"Sets server exposure to codemode, codemode-deferred, or deferred rather than direct",
|
|
60
|
+
"Uses toolExposure with \"search_code\": \"direct\"",
|
|
61
|
+
"Runs pi mcp list to verify the connection",
|
|
62
|
+
"Tells the user to /reload or start a new session so a running session connects",
|
|
63
|
+
"Does not tell the user to install an MCP extension or package, since MCP is built in"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": 5,
|
|
68
|
+
"prompt": "My Pi permission extension asks before every bash call. It works for direct calls, but when the model runs bash from a codemode script the approval lookup fails because my code searches the last assistant message for event.toolCallId. Fix the extension so codemode calls are gated too.",
|
|
69
|
+
"expected_output": "A tool_call handler that judges each call from the event itself, recognises nested calls by event.parentToolCallId (ids like <parent>/<n>), does not require the id to exist in the assistant message, does not auto-approve children because the codemode call was approved, and blocks when there is no UI.",
|
|
70
|
+
"files": [],
|
|
71
|
+
"assertions": [
|
|
72
|
+
"Keeps using the tool_call event and checks event.toolName === 'bash' (or isToolCallEventType('bash', event))",
|
|
73
|
+
"Explains that nested calls carry event.parentToolCallId and have ids of the form <parent id>/<n> that never appear in the transcript",
|
|
74
|
+
"Stops requiring the tool call id to be found in the last assistant message for nested calls",
|
|
75
|
+
"Reads the command from event.input for the nested call instead of parsing the codemode script",
|
|
76
|
+
"Does not approve a nested call just because the parent codemode call was approved",
|
|
77
|
+
"Does not rely on the nestedCalls record on the tool result for approval",
|
|
78
|
+
"Checks ctx.hasUI and returns { block: true, reason } when no UI is available",
|
|
79
|
+
"Notes that a blocked nested call makes the script's tools.bash() promise reject"
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": 6,
|
|
84
|
+
"prompt": "I have a Pi extension tool `list_issues` that returns a text table. When the model calls it from codemode, the script gets a string it has to parse. Change the tool so codemode scripts get the issue objects, and keep the model's view readable.",
|
|
85
|
+
"expected_output": "The tool declares outputSchema (TypeBox) and returns structuredContent matching it on every result, alongside the existing text content; failures that still carry data use isError: true; read-only annotations are set.",
|
|
86
|
+
"files": [],
|
|
87
|
+
"assertions": [
|
|
88
|
+
"Adds an outputSchema built with typebox Type.Object/Type.Array",
|
|
89
|
+
"Returns structuredContent matching the schema on every result",
|
|
90
|
+
"Keeps model-facing text in content",
|
|
91
|
+
"Explains that codemode scripts receive structuredContent instead of the text only when outputSchema is declared",
|
|
92
|
+
"Uses isError: true (not a thrown error) for a failure that should still return data, or throws for plain failures",
|
|
93
|
+
"Keeps structuredContent JSON-compatible (no Date objects, Maps, or class instances)",
|
|
94
|
+
"Marks the tool with annotations such as readOnlyHint: true if it does not modify anything",
|
|
95
|
+
"Does not claim the model itself receives structuredContent"
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": 7,
|
|
100
|
+
"prompt": "Write a Pi extension that uses the Jev classifier to decide whether a user prompt is a question or a coding task, and sets a status line accordingly. It should not break when TypeSafe is down or the key is missing.",
|
|
101
|
+
"expected_output": "An extension that finds the classifier with ctx.modelRegistry.findOfType('classifier', 'typesafe', 'jev-latest'), calls ctx.modelRegistry.classify with a choice or bool question and ctx.signal, checks stopReason and the answer type before using probabilities, and falls back cleanly when the model is missing or the result is an error.",
|
|
102
|
+
"files": [],
|
|
103
|
+
"assertions": [
|
|
104
|
+
"Uses ctx.modelRegistry.findOfType('classifier', 'typesafe', 'jev-latest') or getModelOfType with type 'classifier'",
|
|
105
|
+
"Handles the classifier model being undefined",
|
|
106
|
+
"Calls ctx.modelRegistry.classify(model, { state, questions }, { signal })",
|
|
107
|
+
"Uses a valid question shape: type 'choice' with a criteria record, or type 'bool' with criteria { true, false }",
|
|
108
|
+
"Checks result.stopReason === 'stop' before reading answers, and handles 'error' and 'aborted'",
|
|
109
|
+
"Does not wrap classify() in try/catch as the only error handling, since it returns error results instead of rejecting",
|
|
110
|
+
"Checks the answer type (choice/bool) before reading probabilities or probability",
|
|
111
|
+
"Mentions TYPESAFE_API_KEY (or another Jev provider credential) as the auth requirement"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": 8,
|
|
116
|
+
"prompt": "I'm embedding Pi with the SDK. My host sends user input with session.prompt() while idle and session.steer() while streaming, then waits for agent_settled before showing 'done'. Sometimes it hangs forever on inputs that one of my extensions handles as a command. I also drive a separate Pi over RPC. How do I fix both?",
|
|
117
|
+
"expected_output": "Explains dispositions: session.steer()/followUp() return 'queued' or 'handled'; session.prompt() returns nothing and resolves after an accepted run finishes, so the SDK host should await it instead of waiting for agent_settled; RPC prompt responses carry data.disposition 'started' | 'queued' | 'handled' (steer/follow_up 'queued' | 'handled'); on 'handled' no run started, so the client must not wait for agent_settled. Shows code that branches on the disposition.",
|
|
118
|
+
"files": [],
|
|
119
|
+
"assertions": [
|
|
120
|
+
"States that session.steer() and session.followUp() return 'queued' or 'handled'",
|
|
121
|
+
"States that an RPC prompt response has data.disposition of 'started', 'queued', or 'handled'",
|
|
122
|
+
"Explains that 'handled' means no run started for that input, so the client must not wait for agent_settled",
|
|
123
|
+
"Uses agent_settled (not agent_end) as the completion signal when a run did start",
|
|
124
|
+
"Shows RpcClient.prompt()/steer()/followUp() returning the disposition, or reads data.disposition from the raw response",
|
|
125
|
+
"Notes that session.prompt() resolves after an accepted run finishes",
|
|
126
|
+
"States that session.steer() and session.followUp() throw for extension commands, which must go through session.prompt()",
|
|
127
|
+
"Does not suggest a timeout as the fix"
|
|
128
|
+
]
|
|
48
129
|
}
|
|
49
130
|
]
|
|
50
131
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// A permission gate that asks a classifier model whether a bash command is destructive.
|
|
2
|
+
import type { ClassifierContext } from "@earendil-works/pi-ai";
|
|
3
|
+
import { type ExtensionAPI, isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const QUESTIONS = {
|
|
6
|
+
destructive: {
|
|
7
|
+
type: "bool",
|
|
8
|
+
instructions: "Could running `command` delete or overwrite data the user may want to keep?",
|
|
9
|
+
criteria: { true: "Destructive", false: "Not destructive" },
|
|
10
|
+
},
|
|
11
|
+
} satisfies ClassifierContext["questions"];
|
|
12
|
+
|
|
13
|
+
// Highest P(destructive) that runs without asking. Tune it for your model and question.
|
|
14
|
+
const MAX_ALLOWED_PROBABILITY = 0.2;
|
|
15
|
+
|
|
16
|
+
export default function (pi: ExtensionAPI) {
|
|
17
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
18
|
+
// Also fires for calls codemode scripts make; those carry event.parentToolCallId.
|
|
19
|
+
if (!isToolCallEventType("bash", event)) return;
|
|
20
|
+
|
|
21
|
+
const jev = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
|
|
22
|
+
let probability: number | undefined;
|
|
23
|
+
if (jev) {
|
|
24
|
+
// classify() never rejects: errors and aborts come back as stopReason.
|
|
25
|
+
const result = await ctx.modelRegistry.classify(
|
|
26
|
+
jev,
|
|
27
|
+
{ state: { command: event.input.command }, questions: QUESTIONS },
|
|
28
|
+
{ signal: ctx.signal },
|
|
29
|
+
);
|
|
30
|
+
if (result.stopReason === "aborted") return { block: true, reason: "Cancelled" };
|
|
31
|
+
const answer = result.stopReason === "stop" ? result.answers.destructive : undefined;
|
|
32
|
+
if (answer?.type === "bool" && Number.isFinite(answer.probability)) {
|
|
33
|
+
probability = answer.probability;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Allow only a valid answer at or below the threshold; anything else needs a person.
|
|
38
|
+
if (probability !== undefined && probability >= 0 && probability <= MAX_ALLOWED_PROBABILITY) return;
|
|
39
|
+
|
|
40
|
+
const reason =
|
|
41
|
+
probability === undefined ? "Classifier unavailable" : "Classifier flagged the command";
|
|
42
|
+
if (!ctx.hasUI) return { block: true, reason: `${reason}; no UI to confirm` };
|
|
43
|
+
const ok = await ctx.ui.confirm(`${reason}. Run it?`, event.input.command);
|
|
44
|
+
if (!ok) return { block: true, reason: "Blocked by user" };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// A tool that runs other tools with ctx.executeTool() and keeps abort distinct from failure.
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
export default function (pi: ExtensionAPI) {
|
|
6
|
+
pi.registerTool({
|
|
7
|
+
name: "read_many",
|
|
8
|
+
label: "Read many",
|
|
9
|
+
description: "Read several files in parallel through the read tool.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
paths: Type.Array(Type.String(), { minItems: 1, maxItems: 20 }),
|
|
12
|
+
}),
|
|
13
|
+
// Declared to the model but never callable from other tools, so codemode cannot recurse into it.
|
|
14
|
+
exposure: "model-only",
|
|
15
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
16
|
+
// ctx.tools lists what ctx.executeTool() can reach: active direct tools plus every
|
|
17
|
+
// codemode/deferred tool. An inactive direct tool is not callable.
|
|
18
|
+
if (!ctx.tools.some((tool) => tool.name === "read")) {
|
|
19
|
+
throw new Error("read_many needs the read tool to be active");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let finished = 0;
|
|
23
|
+
const outcomes = await Promise.all(
|
|
24
|
+
params.paths.map(async (path) => {
|
|
25
|
+
// Each call runs tool_call/tool_result handlers (permission gates included) with
|
|
26
|
+
// parentToolCallId set to this call's id. It never rejects; failures come back as isError.
|
|
27
|
+
const outcome = await ctx.executeTool("read", { path }, { signal });
|
|
28
|
+
finished += 1;
|
|
29
|
+
onUpdate?.({
|
|
30
|
+
content: [{ type: "text", text: `Read ${finished}/${params.paths.length}` }],
|
|
31
|
+
details: undefined,
|
|
32
|
+
});
|
|
33
|
+
return { path, outcome };
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Aborted nested calls also come back as isError. Rethrow the abort instead of reporting
|
|
38
|
+
// every file as failed.
|
|
39
|
+
signal?.throwIfAborted();
|
|
40
|
+
|
|
41
|
+
const failed = outcomes.filter(({ outcome }) => outcome.isError).map(({ path }) => path);
|
|
42
|
+
return {
|
|
43
|
+
content: outcomes.flatMap(({ path, outcome }) => [
|
|
44
|
+
{ type: "text" as const, text: `--- ${path}${outcome.isError ? " (failed)" : ""}` },
|
|
45
|
+
...outcome.result.content,
|
|
46
|
+
]),
|
|
47
|
+
details: { failed },
|
|
48
|
+
isError: failed.length === params.paths.length,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// A tool that returns data: codemode scripts receive `structuredContent`, the model reads `content`.
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type Static, Type } from "typebox";
|
|
7
|
+
|
|
8
|
+
const FileStatsResult = Type.Object({
|
|
9
|
+
files: Type.Array(
|
|
10
|
+
Type.Object({
|
|
11
|
+
path: Type.String(),
|
|
12
|
+
bytes: Type.Number(),
|
|
13
|
+
modified: Type.String({ description: "ISO 8601 timestamp" }),
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
missing: Type.Array(Type.String()),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type FileStatsResult = Static<typeof FileStatsResult>;
|
|
20
|
+
|
|
21
|
+
export default function (pi: ExtensionAPI) {
|
|
22
|
+
pi.registerTool({
|
|
23
|
+
name: "file_stats",
|
|
24
|
+
label: "File stats",
|
|
25
|
+
description: "Return the size and modification time of files in the working directory.",
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
paths: Type.Array(Type.String(), { minItems: 1, maxItems: 100 }),
|
|
28
|
+
}),
|
|
29
|
+
outputSchema: FileStatsResult,
|
|
30
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
31
|
+
namespace: { name: "fs_info", description: "Read-only file metadata" },
|
|
32
|
+
// Default exposure is "direct". "codemode" would keep it out of the model's tool list
|
|
33
|
+
// while scripts can still call it.
|
|
34
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
35
|
+
const result: FileStatsResult = { files: [], missing: [] };
|
|
36
|
+
for (const path of params.paths) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
try {
|
|
39
|
+
const stats = await stat(resolve(ctx.cwd, path));
|
|
40
|
+
result.files.push({ path, bytes: stats.size, modified: stats.mtime.toISOString() });
|
|
41
|
+
} catch (error: unknown) {
|
|
42
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
43
|
+
result.missing.push(path);
|
|
44
|
+
} else {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lines = result.files.map((file) => `${file.path}: ${file.bytes} bytes`);
|
|
51
|
+
if (result.missing.length > 0) lines.push(`Missing: ${result.missing.join(", ")}`);
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
54
|
+
details: undefined,
|
|
55
|
+
structuredContent: result,
|
|
56
|
+
// A failure that still carries data: the model sees an error, scripts still get the object.
|
|
57
|
+
isError: result.files.length === 0,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Codemode and Tool Search
|
|
2
|
+
|
|
3
|
+
`codemode` is a built-in tool (Pi 0.99) that runs model-written JavaScript in a QuickJS sandbox. Scripts call Pi's other tools, filter their results, and return only what the model needs. `tool_search` declares tools the model cannot see yet. Checked against Pi v0.99.1: [cli.md#tools](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/cli.md#tools), [extensions.md#tool-exposure](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/extensions.md#tool-exposure), [settings.md#tools](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/settings.md#tools), [codemode tool source](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/src/extensions/codemode/tool.ts).
|
|
4
|
+
|
|
5
|
+
## Enable
|
|
6
|
+
|
|
7
|
+
Both tools are registered inactive. The default tools stay `read`, `bash`, `edit`, `write`.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{ "defaultTools": ["+codemode"] }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `+name` adds to the default selection; `--tools` replaces it, so list everything: `pi --tools read,bash,edit,write,codemode`. `--tools` is an allowlist over every tool, including extension and MCP `mcp__*` tools: unlisted tools are not registered, so scripts can call only the listed ones.
|
|
14
|
+
- The MCP extension activates `codemode` for servers with `codemode`/`codemode-deferred` exposure and `tool_search` for `deferred` servers (see `references/mcp.md`).
|
|
15
|
+
- SDK sessions need `createCodemodeExtension()` / `createToolSearchExtension()` in the resource loader (see `references/sdk.md`).
|
|
16
|
+
|
|
17
|
+
| Setting | Default | Effect |
|
|
18
|
+
|---------|---------|--------|
|
|
19
|
+
| `codemode.mode` | `"on"` | `on`: declared tools stay declared and their descriptions show the script call; `codemode` lists only undeclared tools. `only`: active built-in and extension tools are hidden from the model and listed in the `codemode` description instead |
|
|
20
|
+
| `codemode.inlineBudget` | `3000` | Estimated tokens the `codemode` description may spend on tool declarations; the rest are found with `searchTools()`. `0` lists only namespaces |
|
|
21
|
+
|
|
22
|
+
## Script API
|
|
23
|
+
|
|
24
|
+
The script body is an async function: top-level `await` and `return` work.
|
|
25
|
+
|
|
26
|
+
- `await tools.<name>(args)` calls a tool. Names that are not identifiers are mangled (`my-tool` → `tools.my_tool`); `tools["my-tool"]` also works.
|
|
27
|
+
- `ALL_TOOLS` lists `{ name, description }`. `await searchTools(query, { limit, namespace })` ranks tools with BM25; `await describeTool(name)` returns one declaration.
|
|
28
|
+
- Output: `text(value)`, `image(dataUrlOrImageContent)`, `console.*`, and the returned value. `exit()` ends early and keeps output.
|
|
29
|
+
- `store(key, value)` / `load(key)` keep JSON values across `codemode` calls. Successful scripts append a `codemode-store` custom entry, so values follow the session branch.
|
|
30
|
+
- `models.getModelsOfType`, `getAvailableOfType`, `getModelOfType`, and `models.classify(model, { state, questions })` reach the model catalog and classifiers with session credentials (at most four classifications at a time). Their usage and cost are added to the `codemode` result.
|
|
31
|
+
- An optional first line `// @options: {"max_output_tokens": 2000, "timeout_ms": 60000}`. Output is capped at 10000 estimated tokens by default (start and end kept, full text in a temp file); there is no timeout unless set.
|
|
32
|
+
- No timers, `fetch`, `process`, `require`, or modules.
|
|
33
|
+
|
|
34
|
+
The result starts with `Script completed` or `Script failed`, the wall time, and the output. A failed script keeps its partial output followed by `Script error:`.
|
|
35
|
+
|
|
36
|
+
## What a Script Receives
|
|
37
|
+
|
|
38
|
+
| Tool | `await tools.x(...)` resolves to |
|
|
39
|
+
|------|--------------------------------|
|
|
40
|
+
| Declares `outputSchema` | Its `structuredContent`, also when the result has `isError: true` |
|
|
41
|
+
| MCP tool | The whole `CallToolResult` (`content`, `structuredContent`, `isError`) |
|
|
42
|
+
| `bash` | `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`, also for non-zero exits; `output` holds up to 1 MiB |
|
|
43
|
+
| Anything else | Its text content as one string |
|
|
44
|
+
|
|
45
|
+
A failed, blocked, or invalid call without structured content rejects with an `Error` carrying the tool's error text. Catch it in the script (`Promise.allSettled` works) when partial results are useful.
|
|
46
|
+
|
|
47
|
+
## Which Tools Are Callable
|
|
48
|
+
|
|
49
|
+
"Active" means declared to the model (`pi.getActiveTools()`). "Callable" means reachable through `ctx.executeTool()`, which is how codemode calls tools. They are separate:
|
|
50
|
+
|
|
51
|
+
| Tool `exposure` | Declared to the model | Callable by codemode/`ctx.executeTool()` |
|
|
52
|
+
|-----------------|----------------------|-------------------------------------------|
|
|
53
|
+
| `direct` (default) | While active | Only while active |
|
|
54
|
+
| `model-only` | While active | Never; for tools that orchestrate other tools or ask the user |
|
|
55
|
+
| `codemode` | Only if explicitly activated | Always, and listed in the `codemode` description |
|
|
56
|
+
| `deferred` | After `tool_search` loads it | Always, but not listed; found by `tool_search`/`searchTools()` |
|
|
57
|
+
| `hidden` | Never | Never |
|
|
58
|
+
|
|
59
|
+
So an inactive `codemode`/`deferred` tool is still callable, while an inactive `direct` tool is unreachable from scripts. Because `codemode`/`deferred` tools do not depend on the active set, they stay callable after `/tree`, resume, and fork. `ctx.tools` in a tool's `execute()` lists exactly the callable tools.
|
|
60
|
+
|
|
61
|
+
## Nested Calls and Permissions
|
|
62
|
+
|
|
63
|
+
Each script call runs through `ctx.executeTool()`, the same pipeline as a model-issued call:
|
|
64
|
+
|
|
65
|
+
- Argument validation, then `tool_call` and `tool_result` handlers, then `tool_execution_start`/`update`/`end` events.
|
|
66
|
+
- Every event carries `parentToolCallId` (the `codemode` call's id); the nested call's `toolCallId` is `<parent id>/<n>`. Deeper calls nest the same way.
|
|
67
|
+
- Nested calls are not transcript tool calls or results. Only the calling tool's result reaches the model. The session keeps a bounded `nestedCalls` record on that result (name, arguments, status, duration, error; never results; arguments over 8 KiB per call or 32 KiB per result omitted; at most 256 calls; `complete: false` when anything was dropped).
|
|
68
|
+
- A `tool_call` block makes the script's `await tools.x()` reject. The script can continue.
|
|
69
|
+
|
|
70
|
+
For a permission extension this means:
|
|
71
|
+
|
|
72
|
+
- Judge a nested call from the live `tool_call` event (`event.toolName`, `event.input`, `event.parentToolCallId`). Do not look the id up in the last assistant message; it is not there.
|
|
73
|
+
- Do not approve a child because its parent `codemode` call was approved, and do not parse the script to predict children.
|
|
74
|
+
- `nestedCalls` is written after execution and may omit arguments; do not use it as the approval record.
|
|
75
|
+
- In print/JSON mode (`ctx.hasUI === false`) there is no one to ask: block or apply a non-interactive policy.
|
|
76
|
+
- `pi.getAllTools()` reports `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). They are unverified hints, useful for deciding which calls to confirm, not authority to skip policy.
|
|
77
|
+
|
|
78
|
+
## Tool Search
|
|
79
|
+
|
|
80
|
+
`tool_search` searches tools that are not declared (`codemode` and `deferred` exposure, such as MCP tools) with the same ranking as `searchTools()`, and declares the matches from the next model call on. Loaded tools are recorded like other tool changes and stay declared on that branch. Enable it with `"defaultTools": ["+tool_search"]` when no MCP server does.
|
|
81
|
+
|
|
82
|
+
## Replacing or Disabling
|
|
83
|
+
|
|
84
|
+
- An extension that registers a tool named `codemode` or `tool_search` replaces the built-in one.
|
|
85
|
+
- `"extensions": ["-builtin:codemode"]` or `["-builtin:tool-search"]` disables one (note the hyphen in the extension name); `pi config` lists them under Built-in.
|
|
86
|
+
- Both built-ins use only `exposure`, `prepareLoadout()`, and `ctx.executeTool()`, so an extension can build the same behavior under another name.
|
|
87
|
+
|
|
88
|
+
`@earendil-works/pi-codemode` publishes the sandbox (`CodemodeSandbox`) without Pi dependencies, for exposing any functions to model-written scripts.
|
|
@@ -103,6 +103,7 @@ Useful agent events:
|
|
|
103
103
|
- `before_provider_headers` (Pi 0.80.4) — mutate `event.headers` in place after outgoing HTTP headers are assembled: set a key to a string to add/override, `null` to delete. Runs once per provider request; retries reuse the same headers
|
|
104
104
|
- `before_provider_request`
|
|
105
105
|
- `after_provider_response` — inspect the provider HTTP status and headers before stream consumption
|
|
106
|
+
- `provider_stream_event` (Pi 0.99.0) — notification-only view of each parsed provider stream event before Pi normalizes it (`provider`, `api`, `model`, `data`). Treat `data` as read-only; handlers are awaited in stream order, so keep them fast. Not persisted
|
|
106
107
|
- `model_select`
|
|
107
108
|
- `thinking_level_select` — observe interactive thinking-level changes
|
|
108
109
|
- `ui_prompt_start` / `ui_prompt_end` (Pi 0.84.4) — distinguish active agent work from time spent waiting on blocking `ctx.ui` prompts (`kind`: `select`/`confirm`/`input`/`editor`/`custom`)
|
|
@@ -121,6 +122,8 @@ Useful tool events:
|
|
|
121
122
|
- `tool_result`
|
|
122
123
|
- `tool_execution_end`
|
|
123
124
|
|
|
125
|
+
Calls another tool makes through `ctx.executeTool()` (codemode scripts, orchestrating tools) fire the same tool events, so permission gates see them; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
|
|
126
|
+
|
|
124
127
|
Since Pi 0.86.0, tool-call `arguments` and result `details` must be JSON-compatible values (no `Date`, `Map`, class instances, `undefined` array slots); use `details: undefined` when there is nothing structured to keep. Tools registered without a parameter schema are rejected at registration.
|
|
125
128
|
|
|
126
129
|
Tool results may include `terminate: true` to end the current tool batch without an automatic follow-up LLM turn — useful for tools that produce a structured final answer (see `examples/extensions/structured-output.ts` in the Pi repo). Since Pi 0.84.1, blocked `tool_call` results may also return `terminate: true`; the agent stops early only when every finalized result in the batch is terminating. Tools that make nested LLM calls can return their combined `Usage` as `usage` on the tool result — Pi persists it and includes it in footer, `/session`, and RPC session totals (Pi 0.81.0), and `tool_result` handlers may inspect or replace that value.
|
|
@@ -137,6 +140,7 @@ Tool results may include `terminate: true` to end the current tool batch without
|
|
|
137
140
|
- `ctx.sessionManager` (read-only; `buildContextEntries()` returns active-branch entries with compaction applied, Pi 0.80.4)
|
|
138
141
|
- `ctx.modelRegistry` / `ctx.model` / `ctx.thinkingLevel` / `ctx.scopedModels` (Pi 0.83.0) — `ctx.modelRegistry` is the synchronous extension-facing facade; its `refresh()` became `Promise<void>` in Pi 0.80.8 (await it before synchronous registry reads). Since Pi 0.84.x it also exposes `getProvider(id)` (effective pi-ai provider) and `getProviderAuth(id)` (API key, headers, base URL, provider-scoped env — no loaded model required). `ctx.scopedModels` is the read-only session model scope resolved from `--models`/`enabledModels` (`{ model, thinkingLevel? }[]`, empty when unscoped); prefer it over enumerating `getAvailable()` for model pickers
|
|
139
142
|
- `ctx.modelRegistry.stream()` / `streamSimple()` / `complete()` (Pi 0.86.0) — nested model calls through configured providers with request-time auth; prefer `streamSimple()` for provider-neutral calls, pass `ctx.signal`, and report the combined `usage` on the tool result
|
|
143
|
+
- `ctx.modelRegistry.findOfType(type, provider, id)`, `getModelsOfType()`, `getAvailableOfType()`, and `classify(model, { state, questions }, { signal })` (Pi 0.99.0) — classifier and image models live beside chat models; `classify()` never rejects, so check `result.stopReason` (see `references/providers.md`)
|
|
140
144
|
- `ctx.hasUI` — `true` in TUI and RPC modes
|
|
141
145
|
- `ctx.signal` — the active agent abort signal (or `undefined` when idle); pass it to `fetch`/model calls for abort-aware nested work
|
|
142
146
|
- `ctx.isIdle()` / `ctx.hasPendingMessages()`
|
|
@@ -146,6 +150,8 @@ Tool results may include `terminate: true` to end the current tool batch without
|
|
|
146
150
|
- `ctx.getSystemPrompt()`
|
|
147
151
|
- `ctx.isProjectTrusted()` — the effective project trust decision, including temporary `--approve`/`--no-approve` decisions
|
|
148
152
|
|
|
153
|
+
Tool `execute()` receives `ExtensionToolContext`, which adds `ctx.tools` (the tools this call can reach) and `ctx.executeTool(name, args, { signal?, onUpdate? })` (Pi 0.99.0). `executeTool()` runs validation and the `tool_call`/`tool_result` hooks like a model call and never rejects: unknown tools, validation errors, blocks, and thrown errors come back as `{ isError: true }`, including after an abort. The signal defaults to the calling tool's. Nested usage is added to the calling tool's result, so report only the tool's own `usage`.
|
|
154
|
+
|
|
149
155
|
Use `CONFIG_DIR_NAME` (exported from `@earendil-works/pi-coding-agent`) instead of hardcoding `.pi` when building project config paths, e.g. `join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json")`.
|
|
150
156
|
|
|
151
157
|
Command handlers receive `ExtensionCommandContext`, which extends the above with session-control methods that would deadlock from event handlers: `ctx.getSystemPromptOptions()` (inspect the base system-prompt inputs), `ctx.waitForIdle()`, `ctx.newSession()`, `ctx.fork()`, `ctx.switchSession()`, `ctx.navigateTree()`, and `ctx.reload()`.
|
|
@@ -181,12 +187,27 @@ Important rules:
|
|
|
181
187
|
|
|
182
188
|
1. Truncate large output.
|
|
183
189
|
2. Respect `signal.aborted`.
|
|
184
|
-
3. Throw on failure instead of returning fake success.
|
|
185
|
-
4. Use the file-mutation queue for write/edit style tools.
|
|
190
|
+
3. Throw on failure instead of returning fake success, or return `isError: true` when the failure still carries data. A returned object without `isError` counts as success.
|
|
191
|
+
4. Use the file-mutation queue (`withFileMutationQueue()`) for write/edit style tools.
|
|
186
192
|
5. Put reconstructable state in `details`; it persists in session history.
|
|
187
193
|
|
|
194
|
+
## Tool Exposure & Structured Results
|
|
195
|
+
|
|
196
|
+
Pi 0.99.0 added fields for tools that other tools, codemode scripts, and permission extensions consume ([docs](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/extensions.md#tool-exposure), [types](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/src/core/extensions/types.ts)):
|
|
197
|
+
|
|
198
|
+
- `outputSchema` (TypeBox) plus `structuredContent` on every result: the model still reads `content`; codemode scripts receive `structuredContent` instead of text. Without `outputSchema`, scripts get the text. `structuredContent` must be JSON-compatible. A `tool_result` handler that replaces `content` must also return `structuredContent`, or it is dropped.
|
|
199
|
+
- `isError: true` on a returned result: the model sees an error, while `details` and `structuredContent` are kept for the UI and scripts.
|
|
200
|
+
- `exposure`: `direct` (default), `model-only`, `codemode`, `deferred`, or `hidden`. `direct` and `model-only` tools activate on registration; the others do not (`defaultActive: false` also keeps a `direct` tool inactive until `--tools`/`defaultTools`/`setActiveTools()` names it). Tools cannot be unregistered: re-register with `exposure: "hidden"` to withdraw one. The table of what each value declares and allows is in `references/codemode.md`.
|
|
201
|
+
- `namespace: { name, description? }` groups related tools under one heading in the `codemode` description, as MCP servers do.
|
|
202
|
+
- `annotations`: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, with MCP meanings. Missing hints mean "not read-only, may be destructive, open world". They are unverified; mark writes neither read-only nor casually idempotent.
|
|
203
|
+
- `prepareLoadout(loadout)`: for tools that orchestrate others. Runs whenever the active tools change with `declared`, `callable`, and `registered` tools, and returns replacement `descriptions` and `hiddenDeclarations` (active tools left out of requests but still callable). The built-in `codemode` and `tool_search` use only this, `exposure`, and `ctx.executeTool()`.
|
|
204
|
+
|
|
205
|
+
`pi.getAllTools()` reports each tool's `exposure`, `namespace`, and `annotations`. `pi.setActiveTools()` ignores unknown and `hidden` names. Compilable examples: `examples/structured-tool.ts` and `examples/nested-tools.ts`.
|
|
206
|
+
|
|
188
207
|
## Dynamic Tool Loading
|
|
189
208
|
|
|
209
|
+
For tools the model should find on demand, prefer `exposure: "deferred"` with the built-in `tool_search` (Pi 0.99.0), or `exposure: "codemode"` for tools scripts call. `tool_search` is inactive by default: enable it with `"defaultTools": ["+tool_search"]` or `--tools`, or let a connected MCP server with `deferred` exposure turn it on. Without it, the model can reach `deferred` tools only through codemode. The manual loader pattern below still works.
|
|
210
|
+
|
|
190
211
|
Pi 0.80.7 lets an extension register many tools while keeping only a small initial set active, then add more during execution — cache-friendly on models with native deferred loading. Lifecycle:
|
|
191
212
|
|
|
192
213
|
1. Register every tool with `pi.registerTool()` (all appear in `pi.getAllTools()`).
|
|
@@ -311,6 +332,14 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
311
332
|
});
|
|
312
333
|
```
|
|
313
334
|
|
|
335
|
+
## MCP Servers
|
|
336
|
+
|
|
337
|
+
`pi.registerMcpServer(name, config)` adds a server for the current session with the `mcp.json` entry shape; `pi.unregisterMcpServer(name)` closes it. Registrations are not saved, and an `mcp.json` server with the same name wins. Extensions that implement their own MCP client read `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`. See `references/mcp.md`.
|
|
338
|
+
|
|
339
|
+
## Virtual Models
|
|
340
|
+
|
|
341
|
+
`pi.registerVirtualModel({ provider, id, name, thinkingLevels?, route(request, ctx) })` (experimental, Pi 0.99.0) adds a selectable model that picks a physical model and thinking level for every request. See `references/providers.md#virtual-models`.
|
|
342
|
+
|
|
314
343
|
## Provider Integration
|
|
315
344
|
|
|
316
345
|
Extensions can register or override providers:
|
|
@@ -341,6 +370,8 @@ pi.unregisterProvider("my-provider");
|
|
|
341
370
|
|
|
342
371
|
Dynamic providers can implement `refreshModels(context)` (Pi 0.80.8) for model discovery: Pi calls it during catalog refresh (`/model`, `pi update --models`) and publishes the returned list; its models replace extension-provided `models`. Since Pi 0.84.0 the context is read-snapshot + generation-checked publication: read `context.stored` (the persisted provider snapshot) instead of `context.store`, and persist through `context.publish({ update?, persist? })` — `persist` omitted leaves storage unchanged, a `ModelsStoreEntry` writes it, `persist: null` deletes it. Config-form callbacks that only return models remain unchanged.
|
|
343
372
|
|
|
373
|
+
A config-form `models` list replaces the provider's models across chat, image, and classifier operations (Pi 0.99.0). Omitted `type` means `"chat"`; image and classifier entries need an explicit `type` plus implementations keyed by `api` through `images`/`classifiers` (see `references/providers.md`).
|
|
374
|
+
|
|
344
375
|
Since Pi 0.81.0, extensions can also register a complete pi-ai `Provider` via `pi.registerProvider(createProvider({...}))` — native auth (`login`/`resolve`), `getModels`, `refreshModels`, `filterModels`, and custom streaming. The registered provider becomes the composition base; `models.json` overrides still apply above it. Prefer this when you need real authentication or stream behavior rather than just a model list. The legacy config form (`name`, `baseUrl`, `apiKey`, `models`, `oauth`, `streamSimple`) remains supported. OAuth `refreshToken(credentials, signal)` callbacks must accept and honor the abort signal since Pi 0.84.0.
|
|
345
376
|
|
|
346
377
|
If you need auth for a specific model request, use:
|
|
@@ -355,7 +386,9 @@ Custom `streamSimple` implementations receive a normalized `TranscriptContext` s
|
|
|
355
386
|
|
|
356
387
|
## Handy Patterns
|
|
357
388
|
|
|
358
|
-
- Permission gates via `tool_call`
|
|
389
|
+
- Permission gates via `tool_call` (they also see nested calls from codemode; see `references/codemode.md`)
|
|
390
|
+
- Data tools for codemode via `outputSchema` + `structuredContent`
|
|
391
|
+
- Orchestrating tools via `ctx.executeTool()` with `exposure: "model-only"`
|
|
359
392
|
- Prompt injection via `before_agent_start` (`systemPromptOptions.sections`)
|
|
360
393
|
- Model-context pruning via `context`, or durable omission via `context_edit` entries from `turn_end`/`agent_before_settle`
|
|
361
394
|
- Branch or compaction customization via `session_before_tree` / `session_before_compact`
|