@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 +5 -5
- package/src/skills/pi-extension/SKILL.md +11 -8
- package/src/skills/pi-extension/references/additional-apis.md +5 -3
- package/src/skills/pi-extension/references/hooks.md +12 -9
- package/src/skills/pi-extension/references/providers.md +118 -93
- package/src/skills/pi-extension/references/state.md +38 -27
- package/src/skills/pi-extension/references/structure.md +7 -4
- package/src/skills/pi-extension/references/tools.md +94 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-dev-kit",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
43
|
-
"@mariozechner/pi-tui": "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.
|
|
49
|
-
"@mariozechner/pi-tui": "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,
|
|
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
|
|
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` |
|
|
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 `*`.
|
|
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
|
|
149
|
-
"After starting
|
|
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` |
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
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 {
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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(
|
|
30
|
+
pi.registerProvider("my-provider", myProviderConfig);
|
|
39
31
|
}
|
|
40
32
|
```
|
|
41
33
|
|
|
42
|
-
##
|
|
34
|
+
## Common Registration Patterns
|
|
43
35
|
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
## Model Definition
|
|
38
|
+
```typescript
|
|
39
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
54
40
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
### Register a new provider
|
|
69
51
|
|
|
70
52
|
```typescript
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
supportsReasoningEffort: boolean,
|
|
75
|
+
### Unregister a provider
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
```typescript
|
|
78
|
+
pi.unregisterProvider("my-provider");
|
|
79
|
+
```
|
|
88
80
|
|
|
89
|
-
|
|
90
|
-
requiresToolResultName: boolean,
|
|
81
|
+
This takes effect immediately at runtime.
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
requiresAssistantAfterToolResult: boolean,
|
|
83
|
+
## ProviderConfig Fields
|
|
94
84
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
thinkingFormat: "openai" | "zai" | "qwen",
|
|
97
|
+
## Model Definition
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
openRouterRouting: object,
|
|
99
|
+
The exact model type has more fields, but these are the ones you will usually need:
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
+
## API Key Gating
|
|
113
112
|
|
|
114
|
-
|
|
113
|
+
Provider registration and extension tool registration are separate concerns.
|
|
115
114
|
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
if (!apiKey) return;
|
|
142
|
+
if (!process.env.MY_API_KEY) return;
|
|
127
143
|
|
|
128
|
-
|
|
129
|
-
pi.
|
|
130
|
-
pi.registerCommand(createQuotasCommand(apiKey));
|
|
144
|
+
pi.registerTool(mySearchTool);
|
|
145
|
+
pi.registerCommand("quota", { handler: showQuota });
|
|
131
146
|
}
|
|
132
147
|
```
|
|
133
148
|
|
|
134
|
-
|
|
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
|
|
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
|
-
##
|
|
5
|
+
## Recommended Pattern: Store State in Tool Result Details
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
for (const entry of
|
|
38
|
-
if (entry.
|
|
39
|
-
|
|
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 `
|
|
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,
|
|
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
|
|
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 "@
|
|
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
|
|
33
|
-
"Use
|
|
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 "@
|
|
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 "@
|
|
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
|
|
784
|
-
"Start
|
|
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
|
|