@aliou/pi-dev-kit 0.6.0 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-dev-kit",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -39,14 +39,14 @@
39
39
  "@sinclair/typebox": "^0.34.41"
40
40
  },
41
41
  "peerDependencies": {
42
- "@mariozechner/pi-coding-agent": "0.64.0",
43
- "@mariozechner/pi-tui": "0.64.0"
42
+ "@mariozechner/pi-coding-agent": "0.65.2",
43
+ "@mariozechner/pi-tui": "0.65.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@biomejs/biome": "^2.3.13",
47
47
  "@changesets/cli": "^2.27.11",
48
- "@mariozechner/pi-coding-agent": "0.64.0",
49
- "@mariozechner/pi-tui": "0.64.0",
48
+ "@mariozechner/pi-coding-agent": "0.65.2",
49
+ "@mariozechner/pi-tui": "0.65.2",
50
50
  "@types/node": "^25.0.10",
51
51
  "husky": "^9.1.7",
52
52
  "typescript": "^5.9.3"
@@ -11,10 +11,10 @@ Guide for creating and maintaining Pi extensions. Read the relevant reference fi
11
11
 
12
12
  Pi injects these packages via jiti at runtime. Extensions do not need to install them — they are available as peer dependencies:
13
13
 
14
- - `@mariozechner/pi-coding-agent` — core types, utilities, `Type`/`Static` (re-exported from TypeBox)
14
+ - `@mariozechner/pi-coding-agent` — core types, utilities, and extension APIs
15
15
  - `@mariozechner/pi-tui` — TUI components
16
16
  - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
17
- - `@sinclair/typebox` — schema definitions (also re-exported from `pi-coding-agent`, prefer the re-export)
17
+ - `@sinclair/typebox` — schema definitions for tool parameters and related types
18
18
 
19
19
  ```typescript
20
20
  // Tool UI components (from @aliou/pi-utils-ui)
@@ -53,7 +53,7 @@ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
53
53
  6. If the extension tracks state: Read `references/state.md`.
54
54
  7. For less common APIs: Read `references/additional-apis.md`.
55
55
  8. If the extension has user-configurable settings: Use `registerSettingsCommand` from `@aliou/pi-utils-settings`. Read `references/structure.md` for settings command and auth wizard patterns.
56
- 9. If the extension adds a tool that competes with a natural bash fallback: use `promptSnippet` and `promptGuidelines` on the tool definition for simple guidance. Use system prompt hooks only for complex cross-tool orchestration. Read the **Guidance** section in `references/additional-apis.md`.
56
+ 9. If the extension adds a tool that competes with a natural bash fallback: use `promptSnippet` and `promptGuidelines` on the tool definition for simple guidance. Write `promptGuidelines` as standalone bullets that name the exact tool, because pi injects them verbatim into the shared global `Guidelines` section. Use system prompt hooks only for complex cross-tool orchestration. Read the **Guidance** section in `references/additional-apis.md`.
57
57
  10. Before publishing: Read `references/publish.md` and `references/documentation.md`.
58
58
 
59
59
  ### Modifying an Existing Extension
@@ -68,11 +68,11 @@ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
68
68
  | File | Content |
69
69
  |---|---|
70
70
  | `references/structure.md` | Project layout, package.json, tsconfig, biome.json, config.ts, entry point patterns (including acceptable exceptions), API key pattern, imports |
71
- | `references/tools.md` | Tool registration, execute signature, parameters, streaming, rendering, naming, renderCall/renderResult UI guidelines |
71
+ | `references/tools.md` | Tool registration, execute signature, parameters, `prepareArguments`, path normalization, file mutation queueing, streaming, rendering, naming, renderCall/renderResult UI guidelines |
72
72
  | `references/hooks.md` | Events, blocking/cancelling, input transformation, system prompt modification, bash spawn hooks (command rewriting) |
73
73
  | `references/commands.md` | Command registration, three-tier pattern, component extraction |
74
74
  | `references/components.md` | TUI components (pi-tui + pi-coding-agent), custom(), theme styling, keyboard handling |
75
- | `references/providers.md` | Provider registration, model definition, compat field, API key gating |
75
+ | `references/providers.md` | Current `pi.registerProvider(name, config)` API, model definition, provider override/registration patterns, API key gating |
76
76
  | `references/modes.md` | Mode behavior matrix, ctx.hasUI, dialog vs fire-and-forget, three-tier pattern |
77
77
  | `references/messages.md` | sendMessage, registerMessageRenderer, notify, when to use each |
78
78
  | `references/state.md` | appendEntry, state reconstruction, appendEntry vs sendMessage |
@@ -105,11 +105,11 @@ When implementing, look at these existing extensions for patterns:
105
105
  9. **Long args placement**: Put long prompt/task/question/context strings on following lines. Keep first line scannable.
106
106
  10. **Result layout**: In `renderResult(result, options, theme)`, handle `isPartial` first with a stable tool-scoped message. Detect errors by checking for missing expected fields in `details` (framework sets `details: {}` on throw). Use `ToolBody` from `@aliou/pi-utils-ui` with `showCollapsed` fields. Use `ToolFooter` conditionally (omit when empty). Use `Container`/`Markdown` for rich content.
107
107
  11. **Typed param alias**: Define `type MyToolParams = Static<typeof parameters>` at the top of each tool file. Use it everywhere instead of repeating `Static<typeof parameters>`.
108
- 12. **Tool metadata**: Every tool must have `label` (required). Add `promptSnippet` for system prompt tool listing. Add `promptGuidelines` for usage instructions. These replace system-prompt hooks for simple tools.
108
+ 12. **Tool metadata**: Every tool must have `label` (required). Add `promptSnippet` for system prompt tool listing. Add `promptGuidelines` for usage instructions, but write them as standalone global bullets that name the exact tool. These replace system-prompt hooks for simple tools.
109
109
  13. **Output truncation**: For tools returning large text, use `truncateHead()` from `@mariozechner/pi-coding-agent`. Write full content to temp file. Append footer with line/byte counts and temp file path.
110
110
  14. **Core/lib pattern**: Extract domain logic into modules (`client.ts`, `manager.ts`) that don't import from Pi. Tools are thin wrappers. Core modules are unit-testable with vitest.
111
111
  15. **Humanize messages**: Show display names first, IDs in dim/parens. `"Started \"backend\" (proc_42)"` not `"Started proc_42"`.
112
- 16. **peerDependencies**: Pi injects `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, and `@sinclair/typebox` via jiti at runtime. Any of these that your extension imports must be listed in `peerDependencies` with `optional: true` in `peerDependenciesMeta`. Without `optional: true`, npm 7+ auto-installs peers, adding hundreds of packages on every install even though Pi already provides them. Keep them in `devDependencies` too for local type checking — `pnpm install` installs peers, so development is unaffected. Use `>=CURRENT_VERSION` range, not `*`. Prefer importing `Type`/`Static` from `@mariozechner/pi-coding-agent` rather than `@sinclair/typebox` directly.
112
+ 16. **peerDependencies**: Pi injects `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, and `@sinclair/typebox` via jiti at runtime. Any of these that your extension imports must be listed in `peerDependencies` with `optional: true` in `peerDependenciesMeta`. Without `optional: true`, npm 7+ auto-installs peers, adding hundreds of packages on every install even though Pi already provides them. Keep them in `devDependencies` too for local type checking — `pnpm install` installs peers, so development is unaffected. Use `>=CURRENT_VERSION` range, not `*`.
113
113
  17. **Check existing components**: Before creating a new TUI component, check if `pi-tui` or `pi-coding-agent` already exports one that fits.
114
114
  18. **Forward abort signals**: Always pass `signal` through to `fetch()`, `pi.exec()`, and API client methods. A tool that ignores its signal prevents cancellation from reaching the underlying operation. Never prefix with `_signal` unless the tool truly has no async work to cancel.
115
115
  19. **Never use Node child_process APIs**: Do not use `child_process.exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, or `execFileSync` to run binaries or shell scripts. Always use `pi.exec()`. `pi.exec` handles CWD, signal propagation, and output capture consistently. The only exception is if you need a long-lived streaming process with stdin/stdout piping that `pi.exec` cannot support — document the reason in code comments.
@@ -133,9 +133,12 @@ Before considering an extension complete:
133
133
  - [ ] `renderResult` uses `ToolBody` with `showCollapsed` fields.
134
134
  - [ ] `renderResult` uses `ToolFooter` conditionally (omits when empty).
135
135
  - [ ] Every tool has `label` field.
136
- - [ ] Tools have `promptSnippet` and/or `promptGuidelines` when appropriate.
136
+ - [ ] Tools have `promptSnippet` and/or `promptGuidelines` when appropriate, and `promptGuidelines` bullets name the exact tool instead of saying `this tool`.
137
137
  - [ ] Large output tools use `truncateHead()` + temp file pattern.
138
138
  - [ ] Domain logic is extracted to testable core modules.
139
+ - [ ] File-mutating custom tools use `withFileMutationQueue()` for the full read-modify-write window.
140
+ - [ ] Path-taking custom tools normalize a leading `@` before resolving paths.
141
+ - [ ] Tool overrides re-declare `promptSnippet`/`promptGuidelines` if they need inherited prompt behavior.
139
142
  - [ ] `ctx.ui.custom()` calls have RPC fallback, and interactive close/cancel paths do not rely on `done(undefined)` when fallback detection uses `result === undefined`.
140
143
  - [ ] `tool_call` hooks check `ctx.hasUI` before dialog methods.
141
144
  - [ ] Fire-and-forget methods (notify, setStatus, etc.) are used without hasUI guards.
@@ -136,7 +136,7 @@ There are two ways to inject guidance, depending on complexity:
136
136
  For most tools, use the SDK-level `promptSnippet` and `promptGuidelines` fields directly on the tool definition. No hook is needed.
137
137
 
138
138
  - **`promptSnippet`** — Injected into the "Available tools" system prompt section. Use for a concise (1–2 sentence) description of when to prefer this tool.
139
- - **`promptGuidelines`** — Appended to the "Guidelines" section. Use for a short list of usage rules.
139
+ - **`promptGuidelines`** — Appended verbatim to the global "Guidelines" section. Use for a short list of usage rules that still make sense without extra tool-local context.
140
140
 
141
141
  ```typescript
142
142
  const myTool = {
@@ -145,8 +145,8 @@ const myTool = {
145
145
  description: "...",
146
146
  promptSnippet: "Manage background processes without blocking the conversation.",
147
147
  promptGuidelines: [
148
- "Use this tool for long-running commands instead of bash.",
149
- "After starting a process, continue other work instead of waiting.",
148
+ "Use my_tool for long-running commands instead of bash.",
149
+ "After starting my_tool, continue other work instead of waiting.",
150
150
  ],
151
151
  parameters: ...,
152
152
  execute: ...,
@@ -155,6 +155,8 @@ const myTool = {
155
155
 
156
156
  This is the simplest approach and works well when guidance is specific to a single tool.
157
157
 
158
+ Because these bullets are merged into the shared global `Guidelines` section, avoid vague phrasing like `Use this tool...`. Name the exact tool (`my_tool`, `process`, `linkup_web_search`) so the bullet remains clear after injection.
159
+
158
160
  #### Tier 2: System Prompt Hook (For Complex Cross-Tool Orchestration)
159
161
 
160
162
  Use the `before_agent_start` hook when:
@@ -8,13 +8,11 @@ Hooks let extensions react to lifecycle events. They are registered with `pi.on(
8
8
 
9
9
  | Event | When | Can Cancel | Payload |
10
10
  |---|---|---|---|
11
- | `session_start` | New session created | No | `{}` |
12
- | `session_switch` | Switched to different session | No | `{ reason: "new" \| "switch" \| "fork" }` |
13
- | `session_before_switch` | Before switching sessions | Yes (`{ cancel: true }`) | `{ reason: "new" \| "switch" \| "fork" }` |
14
- | `session_before_fork` | Before forking a session | Yes (`{ cancel: true }`) | `{}` |
15
- | `session_fork` | After session was forked | No | `{}` |
16
- | `session_shutdown` | Pi is shutting down | No | `{}` |
17
- | `session_before_compact` | Before compaction | Yes (return custom summary string) | `{ summary: string }` |
11
+ | `session_start` | Session starts, reloads, or is replaced | No | `{ reason: "startup" \| "reload" \| "new" \| "resume" \| "fork", previousSessionFile? }` |
12
+ | `session_before_switch` | Before `/new` or `/resume` replaces the current session | Yes (`{ cancel: true }`) | `{ reason: "new" \| "resume", targetSessionFile? }` |
13
+ | `session_before_fork` | Before forking a session | Yes (`{ cancel: true }`) | `{ entryId }` |
14
+ | `session_shutdown` | Current session runtime is shutting down or being replaced | No | `{}` |
15
+ | `session_before_compact` | Before compaction | Yes (cancel or provide custom compaction) | event-specific compaction data |
18
16
 
19
17
  ### Agent Events
20
18
 
@@ -98,8 +96,13 @@ pi.on("session_before_switch", async (event, ctx) => {
98
96
 
99
97
  ```typescript
100
98
  pi.on("session_before_compact", async (event, ctx) => {
101
- // Return a custom summary string to replace the default compaction
102
- return `Custom summary: ${event.summary.slice(0, 200)}...`;
99
+ return {
100
+ compaction: {
101
+ summary: "Custom summary",
102
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
103
+ tokensBefore: event.preparation.tokensBefore,
104
+ },
105
+ };
103
106
  });
104
107
  ```
105
108
 
@@ -1,134 +1,159 @@
1
1
  # Providers
2
2
 
3
- Providers add LLM backends to pi. They connect pi to model APIs (OpenAI-compatible or custom).
3
+ Providers add LLM backends to pi. They connect pi to model APIs, proxies, gateways, and custom streaming implementations.
4
+
5
+ This reference tracks the current `pi.registerProvider(name, config)` API from pi-mono. For full provider details and advanced examples, also read pi-mono `packages/coding-agent/docs/custom-provider.md`.
4
6
 
5
7
  ## Registration
6
8
 
7
9
  ```typescript
8
- import { Type, type ExtensionAPI, type ProviderDefinition } from "@mariozechner/pi-coding-agent";
9
-
10
- const myProvider: ProviderDefinition = {
11
- name: "my-provider",
12
- models: () => {
13
- const apiKey = process.env.MY_API_KEY;
14
- if (!apiKey) return [];
15
-
16
- return [
17
- {
18
- id: "my-provider/model-name",
19
- name: "Model Name",
20
- provider: "my-provider",
21
- canStream: true,
22
- contextLength: 128000,
23
- maxOutputTokens: 8192,
24
- pricing: { inputPerMillion: 3.0, outputPerMillion: 15.0 },
25
- compat: {
26
- type: "openai-completions",
27
- maxTokensField: "max_tokens",
28
- supportsDeveloperRole: false,
29
- },
30
- },
31
- ];
32
- },
33
- apiKey: () => process.env.MY_API_KEY,
34
- baseUrl: () => "https://api.my-provider.com/v1",
10
+ import type { ExtensionAPI, ProviderConfig } from "@mariozechner/pi-coding-agent";
11
+
12
+ const myProviderConfig: ProviderConfig = {
13
+ baseUrl: "https://api.example.com/v1",
14
+ apiKey: "MY_API_KEY",
15
+ api: "openai-completions",
16
+ models: [
17
+ {
18
+ id: "my-model",
19
+ name: "My Model",
20
+ reasoning: false,
21
+ input: ["text", "image"],
22
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
23
+ contextWindow: 128000,
24
+ maxTokens: 4096,
25
+ },
26
+ ],
35
27
  };
36
28
 
37
29
  export default function (pi: ExtensionAPI) {
38
- pi.registerProvider(myProvider);
30
+ pi.registerProvider("my-provider", myProviderConfig);
39
31
  }
40
32
  ```
41
33
 
42
- ## Provider Definition
34
+ ## Common Registration Patterns
43
35
 
44
- | Field | Type | Description |
45
- |---|---|---|
46
- | `name` | `string` | Unique provider identifier. Used as prefix in model IDs. |
47
- | `models` | `() => ProviderModelConfig[]` | Returns available models. Called when pi needs the model list. Return `[]` if the API key is missing. |
48
- | `apiKey` | `() => string \| undefined` | Returns the API key. Pi calls this when making requests. |
49
- | `baseUrl` | `() => string \| undefined` | Returns the base URL for the API. |
36
+ ### Override an existing provider
50
37
 
51
- The `models` function is the right place to check for API key presence. If the key is missing, return an empty array and the provider will appear registered but offer no models.
52
-
53
- ## Model Definition
38
+ ```typescript
39
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
54
40
 
55
- | Field | Type | Description |
56
- |---|---|---|
57
- | `id` | `string` | Unique model ID. Convention: `provider/model-name`. |
58
- | `name` | `string` | Display name shown in model picker. |
59
- | `provider` | `string` | Must match the provider's `name`. |
60
- | `canStream` | `boolean` | Whether the model supports streaming responses. |
61
- | `contextLength` | `number` | Maximum context window in tokens. |
62
- | `maxOutputTokens` | `number` | Maximum output tokens per response. |
63
- | `pricing` | `object` | `{ inputPerMillion, outputPerMillion }` in USD. Used for cost display. |
64
- | `compat` | `object` | OpenAI compatibility settings. See below. |
41
+ export default function (pi: ExtensionAPI) {
42
+ pi.registerProvider("anthropic", {
43
+ baseUrl: "https://proxy.example.com",
44
+ });
45
+ }
46
+ ```
65
47
 
66
- ## Compat Field
48
+ Use this when you want to keep the built-in provider and model list, but change the endpoint and/or headers.
67
49
 
68
- The `compat` field tells pi how to talk to the model's API. Most third-party APIs are OpenAI-compatible but differ in which features they support.
50
+ ### Register a new provider
69
51
 
70
52
  ```typescript
71
- compat: {
72
- type: "openai-completions",
73
-
74
- // Which field name the API uses for max output tokens
75
- maxTokensField: "max_tokens" | "max_completion_tokens",
53
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
76
54
 
77
- // Whether the API supports the 'developer' role (vs 'system')
78
- supportsDeveloperRole: boolean,
79
-
80
- // Whether the API supports the 'store' parameter
81
- supportsStore: boolean,
55
+ export default function (pi: ExtensionAPI) {
56
+ pi.registerProvider("my-provider", {
57
+ baseUrl: "https://api.example.com/v1",
58
+ apiKey: "MY_API_KEY",
59
+ api: "openai-completions",
60
+ models: [
61
+ {
62
+ id: "my-model",
63
+ name: "My Model",
64
+ reasoning: false,
65
+ input: ["text"],
66
+ cost: { input: 0.5, output: 1.5, cacheRead: 0, cacheWrite: 0 },
67
+ contextWindow: 128000,
68
+ maxTokens: 8192,
69
+ },
70
+ ],
71
+ });
72
+ }
73
+ ```
82
74
 
83
- // Whether the API supports reasoning_effort parameter
84
- supportsReasoningEffort: boolean,
75
+ ### Unregister a provider
85
76
 
86
- // Whether usage stats are included in streaming responses
87
- supportsUsageInStreaming: boolean,
77
+ ```typescript
78
+ pi.unregisterProvider("my-provider");
79
+ ```
88
80
 
89
- // Whether tool results must include a 'name' field
90
- requiresToolResultName: boolean,
81
+ This takes effect immediately at runtime.
91
82
 
92
- // Whether an assistant message is required after tool results
93
- requiresAssistantAfterToolResult: boolean,
83
+ ## ProviderConfig Fields
94
84
 
95
- // Whether thinking/reasoning must be sent as text content
96
- requiresThinkingAsText: boolean,
85
+ | Field | Type | Description |
86
+ |---|---|---|
87
+ | `baseUrl` | `string` | Base URL for the provider or proxy. |
88
+ | `headers` | `Record<string, string>` | Optional static headers to add to requests. |
89
+ | `apiKey` | `string` | Environment variable name containing the API key. |
90
+ | `api` | `"openai-completions" \| "openai-responses"` | Compatibility mode for request/response handling. |
91
+ | `models` | `ProviderModelConfig[]` | Model definitions exposed by this provider. |
92
+ | `streamSimple` | `function` | Optional custom streaming implementation for non-standard APIs. |
93
+ | `oauth` | `object` | Optional OAuth config for providers that need browser-based auth. |
97
94
 
98
- // Mistral-specific tool ID requirements
99
- requiresMistralToolIds: boolean,
95
+ Use the built-in OpenAI-compatible path when possible. Reach for `streamSimple` only when the upstream API is not compatible enough.
100
96
 
101
- // Format for thinking/reasoning blocks
102
- thinkingFormat: "openai" | "zai" | "qwen",
97
+ ## Model Definition
103
98
 
104
- // OpenRouter-specific routing hints
105
- openRouterRouting: object,
99
+ The exact model type has more fields, but these are the ones you will usually need:
106
100
 
107
- // Vercel AI Gateway routing
108
- vercelGatewayRouting: object,
109
- }
110
- ```
101
+ | Field | Type | Description |
102
+ |---|---|---|
103
+ | `id` | `string` | Model identifier within the provider config. |
104
+ | `name` | `string` | Display name shown in model selection UI. |
105
+ | `reasoning` | `boolean` | Whether the model is a reasoning model. |
106
+ | `input` | `Array<"text" \| "image" \| "audio" \| "pdf">` | Input modalities supported by the model. |
107
+ | `cost` | `object` | `{ input, output, cacheRead, cacheWrite }` cost values. |
108
+ | `contextWindow` | `number` | Maximum context window. |
109
+ | `maxTokens` | `number` | Maximum output tokens. |
111
110
 
112
- All fields in `compat` are optional except `type`. Start with the minimum and add fields as needed based on API behavior.
111
+ ## API Key Gating
113
112
 
114
- There is also `type: "openai-responses"` for providers using the OpenAI Responses API, which currently has no additional compat fields.
113
+ Provider registration and extension tool registration are separate concerns.
115
114
 
116
- ## Provider with API Key Gate
115
+ For providers:
116
+ - Register the provider with `pi.registerProvider(name, config)`.
117
+ - Point `apiKey` at the environment variable name that holds the credential.
118
+ - If the provider should exist even when tools are disabled, still register it.
117
119
 
118
- Register the provider unconditionally but gate tools/commands on the API key:
120
+ For tools and commands that require the same credential:
121
+ - Gate those registrations separately in your extension entry point.
119
122
 
120
123
  ```typescript
121
124
  export default function (pi: ExtensionAPI) {
122
- // Provider always registered -- models() returns [] if no key
123
- pi.registerProvider(myProvider);
125
+ pi.registerProvider("my-provider", {
126
+ baseUrl: "https://api.example.com/v1",
127
+ apiKey: "MY_API_KEY",
128
+ api: "openai-completions",
129
+ models: [
130
+ {
131
+ id: "my-model",
132
+ name: "My Model",
133
+ reasoning: false,
134
+ input: ["text"],
135
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
136
+ contextWindow: 128000,
137
+ maxTokens: 4096,
138
+ },
139
+ ],
140
+ });
124
141
 
125
- const apiKey = process.env.MY_API_KEY;
126
- if (!apiKey) return;
142
+ if (!process.env.MY_API_KEY) return;
127
143
 
128
- // Only register tools that need the key
129
- pi.registerTool(createSearchTool(apiKey));
130
- pi.registerCommand(createQuotasCommand(apiKey));
144
+ pi.registerTool(mySearchTool);
145
+ pi.registerCommand("quota", { handler: showQuota });
131
146
  }
132
147
  ```
133
148
 
134
- This way the provider appears in pi's provider list even without a key, and users see a clear "no models available" state rather than a missing provider.
149
+ That pattern keeps provider setup accurate while still hiding tools that cannot work without credentials.
150
+
151
+ ## When to read the upstream docs
152
+
153
+ Also read pi-mono `packages/coding-agent/docs/custom-provider.md` when you need:
154
+ - custom streaming via `streamSimple`
155
+ - OAuth support
156
+ - proxying existing providers
157
+ - header injection
158
+ - provider teardown with `pi.unregisterProvider()`
159
+ - advanced model config details
@@ -1,42 +1,53 @@
1
1
  # State Management
2
2
 
3
- Extensions can persist state in the session history using `appendEntry`. State is reconstructed by replaying entries when a session is loaded.
3
+ Extensions can persist state in the session history. In modern Pi extensions, the usual pattern is to store reconstructible state in tool result `details` and rebuild it from the current branch on `session_start`.
4
4
 
5
- ## appendEntry
5
+ ## Recommended Pattern: Store State in Tool Result Details
6
6
 
7
- Adds an entry to the session conversation. Unlike `sendMessage`, entries from `appendEntry` are explicitly for state tracking and are rendered via the tool result rendering system.
7
+ When a tool changes extension state, return the latest state in `details`. That keeps the state aligned with normal tool history, branching, and reconstruction.
8
8
 
9
9
  ```typescript
10
- pi.appendEntry({
11
- toolName: "todo",
12
- toolCallId: `todo-${Date.now()}`,
13
- input: { action: "add", text: "Buy groceries" },
14
- output: "Added: Buy groceries",
15
- display: true,
16
- details: { items: ["Buy groceries"] },
17
- });
18
- ```
10
+ export default function (pi: ExtensionAPI) {
11
+ let items: string[] = [];
19
12
 
20
- | Field | Type | Description |
21
- |---|---|---|
22
- | `toolName` | `string` | Which tool this entry is associated with. Used for rendering. |
23
- | `toolCallId` | `string` | Unique ID for this entry. |
24
- | `input` | `object` | The "input" shown in the entry (as if the tool was called with these params). |
25
- | `output` | `string` | The text output (what the LLM sees). |
26
- | `display` | `boolean` | Whether to show in TUI. |
27
- | `details` | `object` | Rich data for the tool's `renderResult`. |
13
+ pi.on("session_start", async (_event, ctx) => {
14
+ items = [];
15
+ for (const entry of ctx.sessionManager.getBranch()) {
16
+ if (entry.type === "message" && entry.message.role === "toolResult") {
17
+ if (entry.message.toolName === "todo") {
18
+ items = entry.message.details?.items ?? [];
19
+ }
20
+ }
21
+ }
22
+ });
23
+
24
+ pi.registerTool({
25
+ name: "todo",
26
+ // ...
27
+ async execute() {
28
+ items.push("Buy groceries");
29
+ return {
30
+ content: [{ type: "text", text: "Added todo item" }],
31
+ details: { items: [...items] },
32
+ };
33
+ },
34
+ });
35
+ }
36
+ ```
28
37
 
29
38
  ## Reconstructing State from Session
30
39
 
31
- When a session loads, you can reconstruct state by iterating over existing entries. This is typically done in a `session_start` or `session_switch` handler:
40
+ When a session loads, reconstruct state in `session_start` by iterating over the current branch or full session through `ctx.sessionManager`:
32
41
 
33
42
  ```typescript
34
43
  pi.on("session_start", async (_event, ctx) => {
35
- // Rebuild state from session entries
36
- const entries = ctx.getEntries();
37
- for (const entry of entries) {
38
- if (entry.toolName === "todo" && entry.details) {
39
- todoItems = entry.details.items;
44
+ todoItems = [];
45
+
46
+ for (const entry of ctx.sessionManager.getBranch()) {
47
+ if (entry.type === "message" && entry.message.role === "toolResult") {
48
+ if (entry.message.toolName === "todo") {
49
+ todoItems = entry.message.details?.items ?? [];
50
+ }
40
51
  }
41
52
  }
42
53
  });
@@ -53,4 +64,4 @@ This pattern makes state survive session reloads, forks, and compactions (as lon
53
64
  | Use for | State changes, action logs | Information display, command results |
54
65
  | LLM sees | The `output` field | The `content` field |
55
66
 
56
- Use `appendEntry` when you are tracking state changes that need to be replayed. Use `sendMessage` when you are displaying a one-time result.
67
+ Use tool result `details` when the state naturally belongs to a tool call and should follow normal conversation branching. Use `appendEntry` for extension-specific state/history that does not fit a normal tool result. Use `sendMessage` when you are displaying a one-time result.
@@ -79,12 +79,14 @@ Not every extension needs every directory. A simple extension with one tool migh
79
79
  "peerDependencies": {
80
80
  "@mariozechner/pi-coding-agent": ">=CURRENT_VERSION",
81
81
  "@mariozechner/pi-ai": ">=CURRENT_VERSION",
82
- "@mariozechner/pi-tui": ">=CURRENT_VERSION"
82
+ "@mariozechner/pi-tui": ">=CURRENT_VERSION",
83
+ "@sinclair/typebox": ">=0.34.0"
83
84
  },
84
85
  "peerDependenciesMeta": {
85
86
  "@mariozechner/pi-coding-agent": { "optional": true },
86
87
  "@mariozechner/pi-ai": { "optional": true },
87
- "@mariozechner/pi-tui": { "optional": true }
88
+ "@mariozechner/pi-tui": { "optional": true },
89
+ "@sinclair/typebox": { "optional": true }
88
90
  },
89
91
  "devDependencies": {
90
92
  "@aliou/biome-plugins": "^0.3.0",
@@ -93,6 +95,7 @@ Not every extension needs every directory. A simple extension with one tool migh
93
95
  "@mariozechner/pi-ai": "CURRENT_VERSION",
94
96
  "@mariozechner/pi-coding-agent": "CURRENT_VERSION",
95
97
  "@mariozechner/pi-tui": "CURRENT_VERSION",
98
+ "@sinclair/typebox": "0.34.41",
96
99
  "@types/node": "^25.0.0",
97
100
  "husky": "^9.0.0",
98
101
  "typescript": "^5.8.0"
@@ -135,10 +138,10 @@ Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompt
135
138
 
136
139
  **`peerDependencies`**: Declares the minimum pi version required. Pi ships these packages and injects them via jiti at runtime, so extensions never need to install them:
137
140
 
138
- - `@mariozechner/pi-coding-agent` — core types, utilities, `Type` (re-exported from TypeBox)
141
+ - `@mariozechner/pi-coding-agent` — core types, utilities, and extension APIs
139
142
  - `@mariozechner/pi-tui` — TUI components
140
143
  - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
141
- - `@sinclair/typebox` — schema definitions (also re-exported from `pi-coding-agent`)
144
+ - `@sinclair/typebox` — schema definitions for tool parameters and related types
142
145
 
143
146
  List any of these you import at runtime in `peerDependencies` as optional peers. This prevents npm from installing duplicate copies when a user installs your extension. Use `>=` with the current version when creating.
144
147
 
@@ -18,7 +18,7 @@ import type {
18
18
  } from "@mariozechner/pi-coding-agent";
19
19
  import { getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
20
20
  import { Container, Markdown, Text } from "@mariozechner/pi-tui";
21
- import { type Static, Type } from "@mariozechner/pi-coding-agent";
21
+ import { type Static, Type } from "@sinclair/typebox";
22
22
  ```
23
23
 
24
24
  ## Registration
@@ -29,9 +29,9 @@ const myTool = {
29
29
  label: "My Tool", // Required: human-readable name for UI
30
30
  description: "What this tool does. The LLM reads this to decide when to call it.",
31
31
  promptSnippet: "Search for items by query", // One-liner for "Available tools" system prompt
32
- promptGuidelines: [ // Guideline bullets for "Guidelines" system prompt when active
33
- "Use this tool when the user asks about search",
34
- "Prefer specific queries over broad ones",
32
+ promptGuidelines: [ // Guideline bullets appended verbatim to the global "Guidelines" section when this tool is active
33
+ "Use my_tool when the user asks about search.",
34
+ "Prefer specific queries over broad ones when calling my_tool.",
35
35
  ],
36
36
  parameters: Type.Object({
37
37
  query: Type.String({ description: "Search query" }),
@@ -73,7 +73,7 @@ export default function (pi: ExtensionAPI) {
73
73
  | `description` | `string` | Yes | What the tool does (LLM reads this) |
74
74
  | `parameters` | `TSchema` | Yes | TypeBox schema for arguments |
75
75
  | `promptSnippet` | `string` | No | One-liner injected into "Available tools" system prompt. Custom tools without this are omitted from that section. |
76
- | `promptGuidelines` | `string[]` | No | Guideline bullets appended to "Guidelines" system prompt when this tool is active |
76
+ | `promptGuidelines` | `string[]` | No | Guideline bullets appended verbatim to the global "Guidelines" system prompt section when this tool is active. Write each bullet so it still makes sense standalone. |
77
77
  | `execute` | `function` | Yes | Implementation |
78
78
  | `renderCall` | `function` | No | Custom call rendering |
79
79
  | `renderResult` | `function` | No | Custom result rendering |
@@ -118,6 +118,8 @@ The `onUpdate` parameter can be `undefined`. Calling it without optional chainin
118
118
 
119
119
  If you override a built-in tool or wrap another tool, audit any delegated `tool.execute(...)` calls during upgrades. These forwarders often pass through `signal`, `onUpdate`, or `ctx` and can silently break when the execute signature changes. Always recheck the delegate call parameter order and include optional parameters that the target tool expects.
120
120
 
121
+ Prompt metadata is not inherited automatically when you override a built-in tool. If the original tool had `promptSnippet` or `promptGuidelines` and you still want that system prompt behavior, define those fields explicitly on the override.
122
+
121
123
  ## Return Value
122
124
 
123
125
  ```typescript
@@ -222,7 +224,7 @@ Both approaches work. Approach 1 is more common in published extensions. Approac
222
224
  Use TypeBox (`Type.*`) for parameter schemas. The LLM sees the schema to know what arguments to provide.
223
225
 
224
226
  ```typescript
225
- import { Type } from "@mariozechner/pi-coding-agent";
227
+ import { Type } from "@sinclair/typebox";
226
228
 
227
229
  // Required string
228
230
  Type.String({ description: "File path to read" })
@@ -248,6 +250,89 @@ Type.Array(Type.String(), { description: "List of tags" })
248
250
 
249
251
  Always provide `description` on parameters. The LLM uses these to understand what to pass.
250
252
 
253
+ ## Prompt Metadata
254
+
255
+ `promptSnippet` and `promptGuidelines` affect different parts of the default system prompt:
256
+
257
+ - `promptSnippet` adds a one-line entry to `Available tools`.
258
+ - `promptGuidelines` appends raw bullets to the global `Guidelines` section.
259
+
260
+ Important implications:
261
+
262
+ - `promptGuidelines` bullets are not wrapped with the tool name.
263
+ - Write bullets so they still make sense when read out of context.
264
+ - Prefer explicit tool names over phrases like `this tool`.
265
+
266
+ Good:
267
+
268
+ ```typescript
269
+ promptGuidelines: [
270
+ "Use my_tool to search project docs before broader web research.",
271
+ "Prefer specific queries when calling my_tool.",
272
+ ]
273
+ ```
274
+
275
+ Weak:
276
+
277
+ ```typescript
278
+ promptGuidelines: [
279
+ "Use this tool for docs.",
280
+ "Prefer specific queries.",
281
+ ]
282
+ ```
283
+
284
+ Use `promptGuidelines` for short, tool-local rules. If the guidance needs cross-tool sequencing, comparisons against several tools, or dynamic config context, use a `before_agent_start` hook instead.
285
+
286
+ ## Argument Compatibility and Path Handling
287
+
288
+ Use `prepareArguments(args)` when you need a compatibility shim before schema validation, for example to support an old parameter shape during a migration.
289
+
290
+ ```typescript
291
+ prepareArguments(args) {
292
+ if (!args || typeof args !== "object") return args;
293
+ const input = args as { action?: string; oldAction?: string };
294
+ if (typeof input.oldAction === "string" && input.action === undefined) {
295
+ return { ...input, action: input.oldAction };
296
+ }
297
+ return args;
298
+ }
299
+ ```
300
+
301
+ If your custom tool accepts filesystem paths, normalize a leading `@` before resolving the path. Some models include `@` in path arguments, and the built-in file tools already strip it.
302
+
303
+ ```typescript
304
+ const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
305
+ ```
306
+
307
+ ## File-Mutating Tools and Concurrency
308
+
309
+ Tool calls can run in parallel. If your custom tool mutates files, use `withFileMutationQueue()` so it participates in the same per-file queue as built-in `edit` and `write`.
310
+
311
+ ```typescript
312
+ import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
313
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
314
+ import { dirname, resolve } from "node:path";
315
+
316
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
317
+ const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
318
+ const absolutePath = resolve(ctx.cwd, normalizedPath);
319
+
320
+ return withFileMutationQueue(absolutePath, async () => {
321
+ await mkdir(dirname(absolutePath), { recursive: true });
322
+ const current = await readFile(absolutePath, "utf8");
323
+ const next = current.replace(params.oldText, params.newText);
324
+ await writeFile(absolutePath, next, "utf8");
325
+
326
+ return {
327
+ content: [{ type: "text", text: `Updated ${normalizedPath}` }],
328
+ details: {},
329
+ };
330
+ });
331
+ }
332
+ ```
333
+
334
+ Queue the whole read-modify-write window, not just the final write.
335
+
251
336
  ## Streaming Updates
252
337
 
253
338
  Use `onUpdate` to stream partial results while the tool executes. This gives the user feedback during long operations.
@@ -753,7 +838,7 @@ import type {
753
838
  } from "@mariozechner/pi-coding-agent";
754
839
  import { keyHint, formatSize } from "@mariozechner/pi-coding-agent";
755
840
  import { Container, Text } from "@mariozechner/pi-tui";
756
- import { type Static, Type } from "@mariozechner/pi-coding-agent";
841
+ import { type Static, Type } from "@sinclair/typebox";
757
842
 
758
843
  // Schema
759
844
  const parameters = Type.Object({
@@ -780,8 +865,8 @@ const repoTreeTool = {
780
865
  description: "List files and directories in a GitHub repository.",
781
866
  promptSnippet: "Browse repository file structure",
782
867
  promptGuidelines: [
783
- "Use this to explore repository structure before reading files",
784
- "Start with root path, then drill down into directories",
868
+ "Use repo_tree to explore repository structure before reading files.",
869
+ "Start repo_tree at the root path, then drill down into directories.",
785
870
  ],
786
871
  parameters,
787
872