@astralyn/pi 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/bundle/chunks/anthropic-messages-HWL6AS7T.js +6 -0
  3. package/dist/bundle/chunks/azure-openai-responses-4YYPDYV5.js +2 -0
  4. package/dist/bundle/chunks/bedrock-converse-stream.js +8 -8
  5. package/dist/bundle/chunks/chunk-24FY4C5S.js +2 -0
  6. package/dist/bundle/chunks/chunk-6JS2SOFO.js +11 -0
  7. package/dist/bundle/chunks/chunk-ANCZPKX7.js +2 -0
  8. package/dist/bundle/chunks/{chunk-R2KES7LS.js → chunk-BMTEURL3.js} +102 -87
  9. package/dist/bundle/chunks/chunk-MMNIHCU7.js +2 -0
  10. package/dist/bundle/chunks/{chunk-KSCGI7JW.js → chunk-MZJC45TM.js} +2 -2
  11. package/dist/bundle/chunks/{chunk-J75WV3FL.js → chunk-OV2VMHBX.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-YHRHHXFG.js → chunk-TJIPCBZF.js} +1 -1
  13. package/dist/bundle/chunks/chunk-TPVFPFJP.js +2 -0
  14. package/dist/bundle/chunks/chunk-ZAIGBRV5.js +16 -0
  15. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-7BKATOEJ.js +2 -0
  16. package/dist/bundle/chunks/github-copilot.js +1 -1
  17. package/dist/bundle/chunks/google-generative-ai-HTUSU45F.js +2 -0
  18. package/dist/bundle/chunks/google-vertex-AVQV4P76.js +2 -0
  19. package/dist/bundle/chunks/{llama-cpp-classify-H3IZDR5O.js → llama-cpp-classify-27QYLJZY.js} +2 -2
  20. package/dist/bundle/chunks/{mistral-conversations-6E6SS3WF.js → mistral-conversations-V2JJTMJ5.js} +1 -1
  21. package/dist/bundle/chunks/openai-codex-responses-Q25MHDFX.js +10 -0
  22. package/dist/bundle/chunks/openai-completions-TXZXILJR.js +7 -0
  23. package/dist/bundle/chunks/{openai-decisions-TVCDIPZX.js → openai-decisions-3WYFPGSK.js} +1 -1
  24. package/dist/bundle/chunks/openai-responses-E4BNZIQK.js +4 -0
  25. package/dist/bundle/chunks/openrouter-images-CBP3OPOD.js +2 -0
  26. package/dist/bundle/chunks/pi-messages-LBJQ4NGP.js +8 -0
  27. package/dist/bundle/chunks/typesafe-system-one-BNICP2LS.js +2 -0
  28. package/dist/bundle/chunks/virtual-modules-3CTTTQQN.js +2 -0
  29. package/dist/bundle/cli.js +1 -1
  30. package/dist/bundle/index.js +1 -1
  31. package/dist/bundle/rpc-entry.js +1 -1
  32. package/dist/core/agent-session.d.ts +7 -1
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +61 -15
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/client-identity.d.ts +8 -0
  37. package/dist/core/client-identity.d.ts.map +1 -0
  38. package/dist/core/client-identity.js +15 -0
  39. package/dist/core/client-identity.js.map +1 -0
  40. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  41. package/dist/core/compaction/branch-summarization.js +1 -2
  42. package/dist/core/compaction/branch-summarization.js.map +1 -1
  43. package/dist/core/compaction/compaction.d.ts +2 -2
  44. package/dist/core/compaction/compaction.d.ts.map +1 -1
  45. package/dist/core/compaction/compaction.js +20 -15
  46. package/dist/core/compaction/compaction.js.map +1 -1
  47. package/dist/core/export-html/index.d.ts.map +1 -1
  48. package/dist/core/export-html/index.js +2 -0
  49. package/dist/core/export-html/index.js.map +1 -1
  50. package/dist/core/export-html/template.html +1 -0
  51. package/dist/core/export-html/template.js +2 -1
  52. package/dist/core/export-html/vendor/marked-cjk-friendly.min.js +6 -0
  53. package/dist/core/export-html/vendor/marked.min.js +30 -29
  54. package/dist/core/extensions/loader.d.ts.map +1 -1
  55. package/dist/core/extensions/loader.js +3 -0
  56. package/dist/core/extensions/loader.js.map +1 -1
  57. package/dist/core/extensions/runner.d.ts +0 -2
  58. package/dist/core/extensions/runner.d.ts.map +1 -1
  59. package/dist/core/extensions/runner.js +0 -18
  60. package/dist/core/extensions/runner.js.map +1 -1
  61. package/dist/core/extensions/types.d.ts +0 -6
  62. package/dist/core/extensions/types.d.ts.map +1 -1
  63. package/dist/core/extensions/types.js.map +1 -1
  64. package/dist/core/extensions/virtual-modules.d.ts.map +1 -1
  65. package/dist/core/extensions/virtual-modules.js +3 -0
  66. package/dist/core/extensions/virtual-modules.js.map +1 -1
  67. package/dist/core/model-config.d.ts +915 -0
  68. package/dist/core/model-config.d.ts.map +1 -1
  69. package/dist/core/model-config.js +48 -24
  70. package/dist/core/model-config.js.map +1 -1
  71. package/dist/core/model-resolver.js +7 -7
  72. package/dist/core/model-resolver.js.map +1 -1
  73. package/dist/core/model-runtime.d.ts.map +1 -1
  74. package/dist/core/model-runtime.js +6 -3
  75. package/dist/core/model-runtime.js.map +1 -1
  76. package/dist/core/provider-composer.d.ts +2 -1
  77. package/dist/core/provider-composer.d.ts.map +1 -1
  78. package/dist/core/provider-composer.js +7 -3
  79. package/dist/core/provider-composer.js.map +1 -1
  80. package/dist/core/sdk.d.ts.map +1 -1
  81. package/dist/core/sdk.js +9 -10
  82. package/dist/core/sdk.js.map +1 -1
  83. package/dist/core/settings-manager.d.ts +0 -4
  84. package/dist/core/settings-manager.d.ts.map +1 -1
  85. package/dist/core/settings-manager.js +0 -36
  86. package/dist/core/settings-manager.js.map +1 -1
  87. package/dist/core/settings-schema.d.ts +0 -1
  88. package/dist/core/settings-schema.d.ts.map +1 -1
  89. package/dist/core/settings-schema.js +0 -3
  90. package/dist/core/settings-schema.js.map +1 -1
  91. package/dist/core/tasks/presentation.d.ts.map +1 -1
  92. package/dist/core/tasks/presentation.js +3 -1
  93. package/dist/core/tasks/presentation.js.map +1 -1
  94. package/dist/core/tasks/reminder.d.ts +15 -0
  95. package/dist/core/tasks/reminder.d.ts.map +1 -0
  96. package/dist/core/tasks/reminder.js +54 -0
  97. package/dist/core/tasks/reminder.js.map +1 -0
  98. package/dist/core/tasks/runtime.d.ts +4 -1
  99. package/dist/core/tasks/runtime.d.ts.map +1 -1
  100. package/dist/core/tasks/runtime.js +37 -11
  101. package/dist/core/tasks/runtime.js.map +1 -1
  102. package/dist/core/tasks/session.d.ts +9 -0
  103. package/dist/core/tasks/session.d.ts.map +1 -1
  104. package/dist/core/tasks/session.js +24 -0
  105. package/dist/core/tasks/session.js.map +1 -1
  106. package/dist/core/tasks/types.d.ts +6 -1
  107. package/dist/core/tasks/types.d.ts.map +1 -1
  108. package/dist/core/tasks/types.js.map +1 -1
  109. package/dist/core/tools/grep.d.ts +0 -2
  110. package/dist/core/tools/grep.d.ts.map +1 -1
  111. package/dist/core/tools/grep.js +3 -3
  112. package/dist/core/tools/grep.js.map +1 -1
  113. package/dist/core/tools/shell-tool.js +2 -2
  114. package/dist/core/tools/shell-tool.js.map +1 -1
  115. package/dist/core/tools/tasks/actions.d.ts +1 -1
  116. package/dist/core/tools/tasks/actions.d.ts.map +1 -1
  117. package/dist/core/tools/tasks/actions.js +98 -5
  118. package/dist/core/tools/tasks/actions.js.map +1 -1
  119. package/dist/core/tools/tasks/schema.d.ts +1 -1
  120. package/dist/core/tools/tasks/schema.d.ts.map +1 -1
  121. package/dist/core/tools/tasks/schema.js +9 -6
  122. package/dist/core/tools/tasks/schema.js.map +1 -1
  123. package/dist/core/tools/tasks/types.d.ts +9 -3
  124. package/dist/core/tools/tasks/types.d.ts.map +1 -1
  125. package/dist/core/tools/tasks/types.js.map +1 -1
  126. package/dist/core/tools/tasks.d.ts +1 -1
  127. package/dist/core/tools/tasks.d.ts.map +1 -1
  128. package/dist/core/tools/tasks.js +2 -2
  129. package/dist/core/tools/tasks.js.map +1 -1
  130. package/dist/extensions/codemode/execute.d.ts.map +1 -1
  131. package/dist/extensions/codemode/execute.js +99 -24
  132. package/dist/extensions/codemode/execute.js.map +1 -1
  133. package/dist/extensions/mcp/config.d.ts.map +1 -1
  134. package/dist/extensions/mcp/config.js +3 -2
  135. package/dist/extensions/mcp/config.js.map +1 -1
  136. package/dist/modes/interactive/interactive-mode.d.ts +11 -15
  137. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  138. package/dist/modes/interactive/interactive-mode.js +68 -109
  139. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  140. package/dist/modes/interactive/tasks/render.d.ts.map +1 -1
  141. package/dist/modes/interactive/tasks/render.js +17 -9
  142. package/dist/modes/interactive/tasks/render.js.map +1 -1
  143. package/dist/package-manager-cli.d.ts.map +1 -1
  144. package/dist/package-manager-cli.js +2 -1
  145. package/dist/package-manager-cli.js.map +1 -1
  146. package/dist/utils/clipboard-command.d.ts.map +1 -1
  147. package/dist/utils/clipboard-command.js +3 -0
  148. package/dist/utils/clipboard-command.js.map +1 -1
  149. package/dist/utils/pi-user-agent.d.ts +1 -0
  150. package/dist/utils/pi-user-agent.d.ts.map +1 -1
  151. package/dist/utils/pi-user-agent.js +3 -2
  152. package/dist/utils/pi-user-agent.js.map +1 -1
  153. package/dist/utils/shell.d.ts.map +1 -1
  154. package/dist/utils/shell.js +45 -13
  155. package/dist/utils/shell.js.map +1 -1
  156. package/dist/utils/tools-manager.d.ts.map +1 -1
  157. package/dist/utils/tools-manager.js +4 -3
  158. package/dist/utils/tools-manager.js.map +1 -1
  159. package/docs/bundled/tasks.md +17 -7
  160. package/docs/codemode.md +2 -2
  161. package/docs/compaction.md +4 -2
  162. package/docs/custom-provider.md +53 -1
  163. package/docs/docs.json +4 -0
  164. package/docs/environment-variables.md +1 -0
  165. package/docs/extensions.md +98 -3
  166. package/docs/models-configuration.md +196 -0
  167. package/docs/models.md +9 -5
  168. package/docs/sdk.md +2 -2
  169. package/docs/session-format.md +2 -2
  170. package/docs/settings.md +2 -2
  171. package/docs/tui.md +71 -0
  172. package/docs/windows.md +28 -0
  173. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  174. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  175. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  176. package/examples/extensions/gondolin/package-lock.json +2 -2
  177. package/examples/extensions/gondolin/package.json +1 -1
  178. package/examples/extensions/sandbox/package-lock.json +2 -2
  179. package/examples/extensions/sandbox/package.json +1 -1
  180. package/examples/extensions/with-deps/package-lock.json +2 -2
  181. package/examples/extensions/with-deps/package.json +1 -1
  182. package/npm-shrinkwrap.json +150 -137
  183. package/package.json +7 -6
  184. package/schemas/models.schema.json +83 -29
  185. package/schemas/settings.schema.json +0 -12
  186. package/dist/bundle/chunks/anthropic-messages-MSB6JBS2.js +0 -6
  187. package/dist/bundle/chunks/azure-openai-responses-ATZ757I2.js +0 -2
  188. package/dist/bundle/chunks/chunk-4XZYHGVN.js +0 -11
  189. package/dist/bundle/chunks/chunk-7UBIBLPN.js +0 -2
  190. package/dist/bundle/chunks/chunk-LUYFHZ6W.js +0 -11
  191. package/dist/bundle/chunks/chunk-NM2KGRFS.js +0 -2
  192. package/dist/bundle/chunks/chunk-QDDSFJ35.js +0 -2
  193. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-HGOVFZDC.js +0 -2
  194. package/dist/bundle/chunks/google-generative-ai-QRLQ4MCJ.js +0 -2
  195. package/dist/bundle/chunks/google-vertex-L3T6EGTL.js +0 -2
  196. package/dist/bundle/chunks/openai-codex-responses-4GUO76XJ.js +0 -10
  197. package/dist/bundle/chunks/openai-completions-O4TKZCIM.js +0 -7
  198. package/dist/bundle/chunks/openai-responses-VBQKJFNO.js +0 -3
  199. package/dist/bundle/chunks/openrouter-images-TDLJV5N4.js +0 -2
  200. package/dist/bundle/chunks/pi-messages-3POWZ43C.js +0 -8
  201. package/dist/bundle/chunks/typesafe-system-one-LWPJYTJP.js +0 -2
  202. package/dist/bundle/chunks/virtual-modules-TG53AOL4.js +0 -2
  203. package/dist/core/provider-attribution.d.ts +0 -4
  204. package/dist/core/provider-attribution.d.ts.map +0 -1
  205. package/dist/core/provider-attribution.js +0 -72
  206. package/dist/core/provider-attribution.js.map +0 -1
@@ -355,6 +355,74 @@ exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
355
355
  └─► session_shutdown
356
356
  ```
357
357
 
358
+ A normal prompt starts a run. The run repeats turns, then checks whether more work starts another low-level agent run:
359
+
360
+ ```mermaid
361
+ flowchart TD
362
+ Input["input"] --> Before["before_agent_start"]
363
+ Before --> AgentStart["agent_start"]
364
+ AgentStart --> Turns[["turns"]]
365
+ Turns --> AgentEnd["agent_end"]
366
+ AgentEnd --> MoreRuns{"More work?"}
367
+ MoreRuns -->|yes| AgentStart
368
+ MoreRuns -->|no| BeforeSettle["agent_before_settle"]
369
+ BeforeSettle --> Continue{"Continuation requested?"}
370
+ Continue -->|yes| AgentStart
371
+ Continue -->|no| Settled["agent_settled"]
372
+ ```
373
+
374
+ More work means a retry, recovery, compaction retry, or queued work.
375
+
376
+ Each turn sends one model request and executes the tool calls in the assistant response:
377
+
378
+ ```mermaid
379
+ flowchart TD
380
+ TurnStart["turn_start"] --> Request[["model request"]]
381
+ Request --> ToolDecision{"Tool calls?"}
382
+ ToolDecision -->|yes| Tools[["tool execution"]]
383
+ Tools --> TurnEnd["turn_end"]
384
+ ToolDecision -->|no| TurnEnd
385
+ TurnEnd --> MoreTurns{"Another turn?"}
386
+ MoreTurns -->|yes| TurnStart
387
+ MoreTurns -->|no| AgentEnd(["agent_end"])
388
+ ```
389
+
390
+ The model request runs these hooks in order: `context`, `context_with_system`, `before_provider_headers`, `before_provider_request`, `after_provider_response`, then `provider_stream_event` and assistant message events. Tool execution runs `tool_execution_start`, `tool_call`, `tool_execution_update` while the tool executes, `tool_result`, and `tool_execution_end`.
391
+
392
+ The lifecycle involves these event groups:
393
+
394
+ | Area | Events |
395
+ |---|---|
396
+ | Input preparation | `input`, `before_agent_start` |
397
+ | Agent run | `agent_start`, `agent_end` |
398
+ | Model turn | `turn_start`, `context`, `context_with_system`, `turn_end` |
399
+ | Provider call | `before_provider_headers`, `before_provider_request`, `after_provider_response`, `provider_stream_event` |
400
+ | Message lifecycle | `message_start`, `message_update`, `message_end` |
401
+ | Tool execution | `tool_execution_start`, `tool_call`, `tool_execution_update`, `tool_result`, `tool_execution_end` |
402
+ | Settlement | `agent_before_settle`, `agent_settled` |
403
+
404
+ Pi counts as busy while a run starts, including the compaction check and `before_agent_start`, so messages sent meanwhile go into the starting run. Message lifecycle events also cover user and tool-result messages; the model request list places only the assistant stream where it occurs in each turn.
405
+
406
+ Retries, recovery, compaction, and queued messages can start another agent run after `agent_end`, so `agent_end` is not a final-idle signal. Reload and session replacement invalidate the old runtime. Code after `await ctx.reload()` still belongs to the old runtime and must not reuse its session-bound state.
407
+
408
+ ### Choose an event by intent
409
+
410
+ | Goal | Event |
411
+ |---|---|
412
+ | Inspect, transform, or consume raw user input | `input` |
413
+ | Change prompt sections, active tools, or add context before a run | `before_agent_start` |
414
+ | Observe one low-level agent run | `agent_start`, `agent_end` |
415
+ | Act after one model response and its tool calls | `turn_end` |
416
+ | Filter conversation messages for one provider request | `context` |
417
+ | Transform the complete transcript, including system messages | `context_with_system` |
418
+ | Change request headers or the serialized provider payload | `before_provider_headers`, `before_provider_request` |
419
+ | Observe provider response metadata or parsed stream events | `after_provider_response`, `provider_stream_event` |
420
+ | Authorize or modify a tool call | `tool_call` |
421
+ | Redact, annotate, or replace a tool result | `tool_result` |
422
+ | Append final entries or request one last continuation | `agent_before_settle` |
423
+ | Run notification work after automatic processing has stopped | `agent_settled` |
424
+ | Initialize or release session-scoped resources | `session_start`, `session_shutdown` |
425
+
358
426
  ### Startup Events
359
427
 
360
428
  #### project_trust
@@ -1068,6 +1136,19 @@ pi.on("input", async (event, ctx) => {
1068
1136
 
1069
1137
  Transforms chain across handlers. See [input-transform.ts](../examples/extensions/input-transform.ts) and [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) for `streamingBehavior`-aware routing.
1070
1138
 
1139
+ ### Understand handler composition
1140
+
1141
+ | Pattern | Behavior |
1142
+ |---|---|
1143
+ | Notification | Return values are ignored. Use the event for observation only. |
1144
+ | In-place mutation | Later handlers see changes to values such as `before_provider_headers.headers`, `before_agent_start.systemPromptOptions`, and `tool_call.input`. |
1145
+ | Chained transformation | Later handlers receive the value returned by earlier `input`, `context`, `context_with_system`, `message_end`, `tool_result`, and `before_provider_request` handlers. |
1146
+ | Gate | A handled input, blocked tool call, cancelled session operation, or handled `user_bash` command stops that operation's normal fallback path. |
1147
+ | Actionable boundary | Each `turn_end` or `agent_before_settle` handler sees the entries and continuation decision proposed so far. |
1148
+ | Last override | The last `cache_warming_decision` handler that returns an action wins. |
1149
+
1150
+ Notification handlers should not perform slow work on a streaming event. Transform handlers should preserve fields they do not intentionally own, and gate handlers should fail closed when their safety decision cannot be made.
1151
+
1071
1152
  ## ExtensionContext
1072
1153
 
1073
1154
  All handlers receive `ctx: ExtensionContext`.
@@ -1167,7 +1248,8 @@ Session-bound `TasksContext` supervises native and extension-owned execution. Fo
1167
1248
  | `execute<T>(execution)` | Returns `{ kind: "result", result, status?, error? }` or `{ kind: "background", task }` |
1168
1249
  | `list()` / `get(id)` | Bounded, read-only (frozen) retained snapshots; IDs support unique prefixes |
1169
1250
  | `read(id, options?)` | Bounded head, tail, or byte-offset output |
1170
- | `wait(id, timeoutMs?, signal?)` | Observation only; timeout or cancellation ends the wait, not execution |
1251
+ | `wait(id, timeoutMs?, signal?, interruptSignal?)` | Observation only; timeout, cancellation, or `interruptSignal` ends the wait, not execution |
1252
+ | `waitAny(timeoutMs?, signal?, interruptSignal?)` | Observes the first task running at call time to settle; returns `undefined` for an empty cohort, timeout, interruption, or closed host. Later tasks are excluded |
1171
1253
  | `kill(id)` | Requests cancellation; `stopping` is not a terminal status |
1172
1254
  | `detach(id)` / `detachForeground()` | Hands off one task, or eligible foreground tasks oldest first while background slots remain, without restarting them; throws when every background slot is taken |
1173
1255
  | `canDetach(id)` | Reports whether an execution can move now, including host policy and available background slots; does not change the execution |
@@ -1181,7 +1263,7 @@ Session-bound `TasksContext` supervises native and extension-owned execution. Fo
1181
1263
 
1182
1264
  `TaskControl<T>` exposes `id`, `signal`, `mode`, `accept()`, `publish(result)`, `setOutputPath(path, cleanup?)`, `onModeChange(listener)`, and `requestCancel()`. `publish` replaces the task's bounded text result, which the `/tasks` panel, `tasks read`, and the completion notification show; a task with an output path is read from that log instead. Call `accept()` after whole-invocation preflight, before allowing a background handoff. Use the control's signal throughout execution. Parent-call cancellation is disconnected after handoff, while runtime shutdown still cancels the task. Mode subscriptions concern only this task; executors do not need the global registry to observe their own handoff or cancel themselves.
1183
1265
 
1184
- Completion messages store a self-contained `TaskCompletionSnapshot`; only their bounded `content` enters model context. Built-in `tasks wait` binds delivery to the actual tool call, and the host confirms it after successful persistence. Ordinary extension reads/waits do not acknowledge delivery, and arbitrary tool-result detail fields cannot acknowledge a task.
1266
+ Completion messages store a self-contained `TaskCompletionSnapshot`; only their bounded `content` enters model context, with an 8 KiB output tail while replay details retain 40 KiB. Built-in `tasks wait` binds delivery to the actual tool call, and the host confirms it after successful persistence; a settled waitAny binds only its winning task. Ordinary extension reads/waits, including `waitAny`, do not acknowledge delivery, and arbitrary tool-result detail fields cannot acknowledge a task.
1185
1267
 
1186
1268
  ```typescript
1187
1269
  const outcome = await ctx.tasks.execute({
@@ -3206,7 +3288,20 @@ Use `ctx.mode === "tui"` before TUI-specific features (`custom()`, component fac
3206
3288
 
3207
3289
  ## Examples
3208
3290
 
3209
- The complete, maintained catalog is [examples/extensions/README.md](../examples/extensions/README.md). Use it to find a focused starting point by capability.
3291
+ The checked [extension examples](../examples/extensions/) are the implementation reference. Start with the smallest example matching the behavior you need:
3292
+
3293
+ | Goal | Start with | Why |
3294
+ |---|---|---|
3295
+ | Confirm or block tool calls | [`permission-gate.ts`](../examples/extensions/permission-gate.ts) | Combines `tool_call`, mode-aware UI, and fail-closed behavior. |
3296
+ | Modify prompt sections for a run | [`prompt-customizer.ts`](../examples/extensions/prompt-customizer.ts) | Uses structured prompt options instead of replacing the complete prompt. |
3297
+ | Register tools after startup | [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts) | Shares registration logic between lifecycle and command handlers. |
3298
+ | Run cancellable model work from a command | [`qna.ts`](../examples/extensions/qna.ts) | Combines nested model calls, cancellation, and custom UI. |
3299
+ | Replace the main editor | [`modal-editor.ts`](../examples/extensions/modal-editor.ts) | Extends `CustomEditor` while preserving application controls. |
3300
+ | Run finalization at shutdown | [`auto-commit-on-exit.ts`](../examples/extensions/auto-commit-on-exit.ts) | Uses the session shutdown hook for finalization work. |
3301
+ | Route built-in tools to another machine | [`ssh.ts`](../examples/extensions/ssh.ts) | Reuses tool operation interfaces instead of recreating tool contracts. |
3302
+ | Add OAuth and provider-specific streaming | [`custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/) | Shows a complete provider integration while delegating supported protocols. |
3303
+
3304
+ Use [Custom Providers](custom-provider.md) for model-service integrations, [Terminal UI](tui.md) for custom components, and [Pi Packages](packages.md) to install or distribute extensions with other resources. Use the exported declarations for exact signatures, payloads, and result schemas; this guide focuses on lifecycle choices, composition, and durable implementation patterns.
3210
3305
 
3211
3306
  ## Tool orchestration
3212
3307
 
@@ -0,0 +1,196 @@
1
+ # Models Configuration
2
+
3
+ <!-- This file is generated from TypeScript source. Do not edit it manually; run npm run generate:references. -->
4
+
5
+ This reference lists the fields of `models.json`. See [Configure a compatible endpoint](models.md#minimal-example) for examples; the file lives at `~/.pi/agent/models.json` or `<project-dir>/.pi/models.json`.
6
+
7
+ Add `"$schema": "https://raw.githubusercontent.com/ming-kang/pi/main/schemas/models.schema.json"` to `models.json` for editor validation and completion.
8
+
9
+ ## Providers
10
+
11
+ Each key under `providers` is a provider ID. A built-in provider ID changes that provider. Any other ID adds a provider, which needs `baseUrl` and an `api` at provider or model level.
12
+
13
+ | Field | Type | Default | Description |
14
+ |---|---|---|---|
15
+ | `name` | string | | Display name. |
16
+ | `baseUrl` | string | | API base URL. Also applies to the provider's built-in models. |
17
+ | `apiKey` | string | | Literal key, "$NAME" environment interpolation, or "!command" output. |
18
+ | `api` | string | | API for the provider's models, such as "openai-completions", "openai-responses", "anthropic-messages", or "google-generative-ai". |
19
+ | `oauth` | `"radius"` | | OAuth login flow. Requires baseUrl. |
20
+ | `headers` | object | | Request headers. Values resolve like apiKey. |
21
+ | `compat` | [object](#compatibility) | | Provider and model compatibility overrides. |
22
+ | `authHeader` | boolean | `false` | Whether to send the resolved API key as Authorization: Bearer. |
23
+ | `models` | [`object[]`](#models) | | |
24
+ | `modelOverrides` | [object](#model-overrides) | | |
25
+
26
+ ## Models
27
+
28
+ Each `models` entry adds a model to the provider. An entry whose `id` matches an existing model replaces it.
29
+
30
+ | Field | Type | Default | Description |
31
+ |---|---|---|---|
32
+ | `id` | string | | Required. Model ID sent to the API. |
33
+ | `name` | string | | Display name, also matched by --model patterns. |
34
+ | `api` | string | | Overrides the provider api. |
35
+ | `baseUrl` | string | | Overrides the provider baseUrl. |
36
+ | `reasoning` | boolean | `false` | Whether the model supports thinking. |
37
+ | `thinkingLevelMap` | object | | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |
38
+ | `input` | `("text" \| "image")[]` | `["text"]` | |
39
+ | `inputLimits` | [object](#input-limits) | | |
40
+ | `cost` | object | | |
41
+ | `cost.input` | number | | Required. Input cost in USD per million tokens. |
42
+ | `cost.output` | number | | Required. Output cost in USD per million tokens. |
43
+ | `cost.cacheRead` | number | | Required. Cache-read cost in USD per million tokens. |
44
+ | `cost.cacheWrite` | number | | Required. Cache-write cost in USD per million tokens. |
45
+ | `cost.tiers` | `object[]` | | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |
46
+ | `cost.tiers[].inputTokensAbove` | number | | Required. Use this tier when total request input exceeds this token count. |
47
+ | `cost.tiers[].input` | number | | Required. Input cost in USD per million tokens. |
48
+ | `cost.tiers[].output` | number | | Required. Output cost in USD per million tokens. |
49
+ | `cost.tiers[].cacheRead` | number | | Required. Cache-read cost in USD per million tokens. |
50
+ | `cost.tiers[].cacheWrite` | number | | Required. Cache-write cost in USD per million tokens. |
51
+ | `promptCache` | object | | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |
52
+ | `contextWindow` | number | `128000` | Context window in tokens. |
53
+ | `maxTokens` | number | `16384` | Maximum output tokens. |
54
+ | `samplingParams` | object | | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |
55
+ | `samplingParamsByThinkingLevel` | object | | samplingParams per thinking level, merged over samplingParams. |
56
+ | `headers` | object | | Request headers. Values resolve like apiKey. |
57
+ | `compat` | [object](#compatibility) | | Provider and model compatibility overrides. |
58
+
59
+ ## Model overrides
60
+
61
+ Each `modelOverrides` key is the ID of a built-in or extension model. Unknown IDs are ignored. Object fields, such as `cost`, merge with the model's value field by field; other fields replace it.
62
+
63
+ | Field | Type | Default | Description |
64
+ |---|---|---|---|
65
+ | `name` | string | | Display name, also matched by --model patterns. |
66
+ | `reasoning` | boolean | | Whether the model supports thinking. |
67
+ | `thinkingLevelMap` | object | | Provider value sent for each thinking level. null marks a level unsupported. Levels the model does not map use the provider's default value through high; xhigh and max are unsupported. |
68
+ | `input` | `("text" \| "image")[]` | | |
69
+ | `inputLimits` | [object](#input-limits) | | |
70
+ | `cost` | object | | |
71
+ | `cost.input` | number | | Input cost in USD per million tokens. |
72
+ | `cost.output` | number | | Output cost in USD per million tokens. |
73
+ | `cost.cacheRead` | number | | Cache-read cost in USD per million tokens. |
74
+ | `cost.cacheWrite` | number | | Cache-write cost in USD per million tokens. |
75
+ | `cost.tiers` | `object[]` | | Request-wide pricing tiers. The highest matching input threshold applies to the full request. |
76
+ | `cost.tiers[].inputTokensAbove` | number | | Required. Use this tier when total request input exceeds this token count. |
77
+ | `cost.tiers[].input` | number | | Required. Input cost in USD per million tokens. |
78
+ | `cost.tiers[].output` | number | | Required. Output cost in USD per million tokens. |
79
+ | `cost.tiers[].cacheRead` | number | | Required. Cache-read cost in USD per million tokens. |
80
+ | `cost.tiers[].cacheWrite` | number | | Required. Cache-write cost in USD per million tokens. |
81
+ | `promptCache` | object | | Best-effort prompt cache lifetime in seconds for the short and long retention tiers. Pi warms only tiers with a known lifetime. |
82
+ | `contextWindow` | number | | Context window in tokens. |
83
+ | `maxTokens` | number | | Maximum output tokens. |
84
+ | `samplingParams` | object | | Fields merged into every request body. Applies only to OpenAI-compatible APIs. |
85
+ | `samplingParamsByThinkingLevel` | object | | samplingParams per thinking level, merged over samplingParams. |
86
+ | `headers` | object | | Request headers. Values resolve like apiKey. |
87
+ | `compat` | [object](#compatibility) | | Provider and model compatibility overrides. |
88
+
89
+ ## Input limits
90
+
91
+ See [Describe model input and caching](models.md#describe-model-input-and-caching).
92
+
93
+ | Field | Type | Default | Description |
94
+ |---|---|---|---|
95
+ | `maxRequestBytes` | integer | | Maximum serialized provider request size in bytes. |
96
+ | `images` | object | | |
97
+ | `images.resize` | object | | Cache-safe resize profile applied before a new image enters conversation history. |
98
+ | `images.resize.maxWidth` | integer | | |
99
+ | `images.resize.maxHeight` | integer | | |
100
+ | `images.resize.maxBytes` | integer | | Maximum base64-encoded payload size in bytes. |
101
+ | `images.resize.jpegQuality` | integer | | |
102
+ | `images.maxPerMessage` | integer | | Maximum images accepted in one provider message. |
103
+ | `images.maxPerRequest` | integer | | Maximum images accepted across one provider request. |
104
+
105
+ ## Compatibility
106
+
107
+ `compat` describes verified differences from an API's standard request and response behavior. Model values override provider values per field. The fields that apply depend on the model's `api`.
108
+
109
+ ### `openai-completions`
110
+
111
+ | Field | Type | Default | Description |
112
+ |---|---|---|---|
113
+ | `supportsStore` | boolean | | Whether the provider supports the store field. Default: auto-detected from URL. |
114
+ | `supportsDeveloperRole` | boolean | | Whether the provider supports the developer role instead of system. Default: auto-detected from URL. |
115
+ | `supportsReasoningEffort` | boolean | | Whether the provider supports reasoning_effort. Default: auto-detected from URL. |
116
+ | `supportsUsageInStreaming` | boolean | `true` | Whether the provider supports stream_options.include_usage for token usage in streaming responses. |
117
+ | `supportsFinishReason` | boolean | `true` | Whether streamed responses include finish_reason. When false, pi infers stop or toolUse when the stream ends. |
118
+ | `maxTokensField` | `"max_completion_tokens" \| "max_tokens"` | | Which field to use for max tokens. Default: auto-detected from URL. |
119
+ | `requiresToolResultName` | boolean | | Whether tool results require the name field. Default: auto-detected from URL. |
120
+ | `requiresAssistantAfterToolResult` | boolean | | Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. |
121
+ | `requiresThinkingAsText` | boolean | | Whether thinking blocks must be converted to text blocks with &lt;thinking> delimiters. Default: auto-detected from URL. |
122
+ | `requiresReasoningContentOnAssistantMessages` | boolean | | Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. |
123
+ | `thinkingFormat` | `"openai" \| "openrouter" \| "deepseek" \| "together" \| "baseten" \| "zai" \| "qwen" \| "chat-template" \| "qwen-chat-template" \| "string-thinking" \| "ant-ling"` | | Format for reasoning or thinking parameters. When omitted, Pi auto-detects the format from the provider URL. "openai" uses reasoning_effort, "openrouter" uses reasoning.effort, "deepseek" uses thinking.type plus reasoning_effort when supported, "together" uses reasoning.enabled plus reasoning_effort when supported, "baseten" uses configurable chat_template_args plus reasoning_effort when supported, "zai" uses thinking.type, "qwen" uses top-level enable_thinking, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking, and "ant-ling" uses reasoning.effort only when the mapped effort is non-null. |
124
+ | `chatTemplateKwargs` | object | | Kwargs sent as chat_template_kwargs when thinkingFormat is "chat-template". Use $var with "thinking.enabled", "thinking.effort", or "thinking.budget" for pi-controlled values. |
125
+ | `chatTemplateArgs` | object | | Arguments sent as chat_template_args when thinkingFormat is "baseten". Use $var with "thinking.enabled", "thinking.effort", or "thinking.budget" for pi-controlled values. |
126
+ | `openRouterRouting` | object | | OpenRouter provider routing preferences. Controls which upstream providers OpenRouter routes requests to. Sent as the provider field in the OpenRouter API request body. See https://openrouter.ai/docs/guides/routing/provider-selection. |
127
+ | `vercelGatewayRouting` | object | | Vercel AI Gateway routing preferences. Controls which upstream providers the gateway routes requests to. See https://vercel.com/docs/ai-gateway/models-and-providers/provider-options. |
128
+ | `zaiToolStream` | boolean | `false` | Whether z.ai supports top-level tool_stream for streaming tool call deltas. |
129
+ | `thinkingTokenBudgetField` | `"thinking_token_budget" \| "thinking_budget" \| "thinking_budget_tokens"` | | Top-level request field used to cap reasoning tokens from thinkingBudgets. Reasoning and the answer share max_tokens on these endpoints. "thinking_token_budget" is vLLM, "thinking_budget" is Qwen, DashScope, or SGLang, and "thinking_budget_tokens" is llama.cpp. Off by default and not set on the generated catalog. |
130
+ | `supportsThinkingTokenBudget` | boolean | `false` | Alias for thinkingTokenBudgetField: "thinking_token_budget" (vLLM). Prefer thinkingTokenBudgetField. |
131
+ | `supportsOpenAIGrammarTools` | boolean | `false` | Whether the provider supports OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |
132
+ | `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system or developer messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |
133
+ | `supportsMidConvoToolAdditions` | boolean | `false` | Whether system messages can introduce additional tools mid-conversation. Requires supportsMidConvoSystemMessages. The generated catalog enables this for capable models. |
134
+ | `supportsStrictMode` | boolean | `false` | Whether the provider supports the strict field in tool definitions. Generated capable models enable it explicitly. |
135
+ | `cacheControlFormat` | `"anthropic"` | | Cache control convention for prompt caching. Anthropic applies cache_control markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content. |
136
+ | `sendSessionAffinityHeaders` | boolean | | Whether to send session-affinity data from options.sessionId. Default: true for OpenRouter endpoints, false otherwise. |
137
+ | `sessionAffinityFormat` | `"openai" \| "openai-nosession" \| "openrouter"` | | Session-affinity header format. openai sends session_id, x-client-request-id, and x-session-affinity; openai-nosession sends x-client-request-id and x-session-affinity; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |
138
+ | `supportsLongCacheRetention` | boolean | | Whether the provider supports long prompt cache retention (prompt_cache_retention: "24h" or Anthropic-style cache_control.ttl: "1h", depending on format). Default: auto-detected from provider and URL. |
139
+ | `vllmPriority` | number | | vLLM scheduler priority sent as the top-level priority request field. Lower values are handled earlier and the server default is 0. Only meaningful with --scheduling-policy priority. Off by default and not set on the generated catalog. |
140
+
141
+ ### `openai-responses`
142
+
143
+ Also applies to `azure-openai-responses` and `openai-codex-responses`.
144
+
145
+ | Field | Type | Default | Description |
146
+ |---|---|---|---|
147
+ | `supportsDeveloperRole` | boolean | `true` | Whether the provider supports the developer role instead of system. |
148
+ | `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts developer or system messages after the conversation has started. When false, later system messages are folded into the leading system message. The generated catalog enables this for verified models. |
149
+ | `sessionAffinityFormat` | `"openai" \| "openai-nosession" \| "openrouter"` | | Session-affinity header format. openai sends session_id and x-client-request-id; openai-nosession sends x-client-request-id; openrouter sends x-session-id. Does not affect prompt_cache_key. Default: auto-detected. |
150
+ | `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports long prompt cache retention. This uses prompt_cache_options.ttl: "30m" on GPT-5.6+ and prompt_cache_retention: "24h" on earlier models. |
151
+ | `supportsStrictMode` | boolean | | Whether the provider supports strict JSON-schema function tools. Defaults are API-specific; generated OpenAI models enable it explicitly. |
152
+ | `supportsOpenAIGrammarTools` | boolean | `false` | Whether to emit OpenAI custom tools with Lark or regex grammar formats. When false, grammar-constrained tools fall back to normal function tools. The generated catalog enables this for capable models. |
153
+ | `supportsAdditionalTools` | boolean | `false` | Whether the model supports message-anchored additional_tools input items. |
154
+ | `supportsToolSearch` | boolean | `false` | Whether the model supports client-executed tool search for transcript-anchored additions. |
155
+ | `supportsExplicitPromptCacheMode` | boolean | `false` | Whether the model accepts prompt_cache_options. Older OpenAI models reject the parameter. |
156
+ | `supportsMidConvoEffort` | boolean | `false` | Whether thinking level changes are sent as configuration_update input items instead of changing reasoning.effort, so a change keeps the prompt cache. Only used by the openai-responses API, and ignored with Sign in with ChatGPT on the openai provider. |
157
+ | `supportsMaxOutputTokens` | boolean | `true` | Whether the provider accepts max_output_tokens. Some Codex-protocol gateways reject it. |
158
+
159
+ ### `anthropic-messages`
160
+
161
+ | Field | Type | Default | Description |
162
+ |---|---|---|---|
163
+ | `supportsEagerToolInputStreaming` | boolean | `true` | Whether the provider accepts per-tool eager_input_streaming. When false, the Anthropic provider omits tools[].eager_input_streaming and sends the legacy fine-grained-tool-streaming-2025-05-14 beta header for tool-enabled requests. |
164
+ | `supportsLongCacheRetention` | boolean | `true` | Whether the provider supports Anthropic long cache retention through cache_control.ttl. |
165
+ | `sendSessionAffinityHeaders` | boolean | | Whether to send x-session-affinity from options.sessionId when caching is enabled. Required for providers like Fireworks that use session affinity for prompt cache routing; requests to the same replica maximize cache hits. Default: true for OpenRouter endpoints, false otherwise. |
166
+ | `sessionAffinityFormat` | `"openrouter"` | | Session-affinity format. openrouter sends x-session-id; when unset, sends x-session-affinity. |
167
+ | `supportsCacheControlOnTools` | boolean | `true` | Whether the provider supports Anthropic-style cache_control markers on tool definitions. When false, cache_control is omitted from tool parameters. Some Anthropic-compatible providers, such as Fireworks, do not support this field on tools and may reject or ignore it. |
168
+ | `supportsTemperature` | boolean | `true` | Whether the model accepts the Anthropic temperature request field. Claude Opus 4.7+ rejects non-default values. |
169
+ | `forceAdaptiveThinking` | boolean | `false` | Whether to force adaptive thinking (thinking.type: adaptive plus output_config.effort) regardless of model ID. Built-in models that require adaptive thinking set this in generated metadata. Custom Anthropic-compatible providers can set this to true for any model whose upstream requires the adaptive format. Set false to opt out on overridden built-in models. |
170
+ | `allowEmptySignature` | boolean | `false` | Whether to replay empty thinking signatures instead of converting thinking to text. |
171
+ | `supportsStrictTools` | boolean | `false` | Whether the provider supports Anthropic strict tool schemas. Generated Anthropic models enable it explicitly. |
172
+ | `supportsMidConvoEffort` | boolean | `false` | Whether the exact model transport supports effort-only system messages and thinking binding controls. |
173
+ | `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system-role messages inside the conversation. When false, later system messages are folded into the top-level system prompt. |
174
+ | `supportsMidConvoToolChanges` | boolean | `false` | Whether the exact model accepts mid-conversation tool_addition and tool_removal blocks. Requires supportsMidConvoSystemMessages. |
175
+ | `allowedFallbackModels` | `object[]` | | Models Anthropic accepts for server-side refusal fallback, with local pricing metadata for returned fallback responses. When absent or empty, callers must omit fallbacks; Anthropic rejects the field for models with no permitted fallback targets. |
176
+
177
+ ### `bedrock-converse-stream`
178
+
179
+ | Field | Type | Default | Description |
180
+ |---|---|---|---|
181
+ | `supportsStrictMode` | boolean | `false` | Whether the model supports Bedrock strict tool schemas. |
182
+
183
+ ### `mistral-conversations`
184
+
185
+ | Field | Type | Default | Description |
186
+ |---|---|---|---|
187
+ | `supportsMidConvoSystemMessages` | boolean | `false` | Whether the exact model accepts system messages after the conversation has started. When false, later system messages are folded into the leading system message. |
188
+
189
+ ### `typesafe-system-one`
190
+
191
+ Also applies to `cloudflare-workers-ai-system-one`.
192
+
193
+ | Field | Type | Default | Description |
194
+ |---|---|---|---|
195
+ | `imageInput` | `"assets" \| "images" \| "state-image-urls"` | | How the service takes classifier images. "assets" sends TypeSafe's assets map (PNG and JPEG) and references the images from the state, as TypeSafe and Jev on Workers AI expect. "images" sends Clef's images list of data URLs (PNG, JPEG, and WebP, at most four), as Clef on Workers AI expects. "state-image-urls" places image_url parts before the state, as OpenRouter expects; OpenRouter drops assets without an error. Auto-detected from the provider, baseUrl, and model ID. Classifier models read it from the provider-level compat; models and modelOverrides entries apply to chat models only. |
196
+ | `sendImageOptInHeader` | boolean | | Whether requests with images send the TypeSafe-Danger-Zone-Images: true header, which TypeSafe's own API requires for its experimental image input. Defaults to true for the typesafe-system-one API unless the model is detected as OpenRouter. |
package/docs/models.md CHANGED
@@ -72,7 +72,7 @@ Some OpenAI-compatible servers do not understand the `developer` role Pi uses fo
72
72
  }
73
73
  ```
74
74
 
75
- Provider `compat` supplies defaults for its models; a model's `compat` refines those defaults. See [Compatibility Options](#compatibility-options).
75
+ Provider `compat` supplies defaults for its models; a model's `compat` refines those defaults. See [Compatibility Options](#compatibility-options) and the generated [Models Configuration](models-configuration.md) reference for every field.
76
76
 
77
77
  ## Full Example
78
78
 
@@ -704,20 +704,20 @@ For `bedrock-converse-stream`, `compat.supportsStrictMode` controls Bedrock stri
704
704
 
705
705
  ## Use classifier models
706
706
 
707
- Classifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef and Clef Flash models from Workers AI, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):
707
+ Classifier models do not chat. They answer typed questions about JSON state: pick one of several choices, answer yes or no, or give a score, each with probabilities. Pi includes TypeSafe's Jev model from these providers, Cloudflare's Clef, Clef Flash, and Clef Omni models from Workers AI and OpenRouter, and OpenAI's GPT-6 Luna through the [Decisions API](https://developers.openai.com/api/docs/guides/decisions):
708
708
 
709
709
  | Provider | Model IDs | Authentication |
710
710
  |---|---|---|
711
711
  | `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |
712
- | `openrouter` | `typesafe/jev-1.13`, `~typesafe/jev-latest` | `OPENROUTER_API_KEY` or `/login` |
713
- | `cloudflare-workers-ai` | `typesafe/jev`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
712
+ | `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or `/login` |
713
+ | `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
714
714
  | `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
715
715
  | `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
716
716
  | `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
717
717
 
718
718
  Chat models on a [llama.cpp router](llama-cpp.md#classification) are also listed as classifier models.
719
719
 
720
- OpenAI's Decisions API needs an API key. Sign in with ChatGPT credentials do not work with it, so `gpt-6-luna` is not listed as available while `openai` is logged in through `/login`, even when `OPENAI_API_KEY` is set; log out of `openai` to use the key. GPT-6 Luna also judges images passed in `images` (see [Codemode](codemode.md#classify)); other classifier models return an error for them. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout.
720
+ OpenAI's Decisions API needs an API key. Sign in with ChatGPT credentials do not work with it, so `gpt-6-luna` is not listed as available while `openai` is logged in through `/login`, even when `OPENAI_API_KEY` is set; log out of `openai` to use the key. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout.
721
721
 
722
722
  Classifier models do not appear in `/model`. The model reaches them through the [`codemode`](usage.md#enable-codemode) tool, which is off unless an MCP server turned it on. Enable it with `"defaultTools": ["+codemode"]` in [settings](settings.md#tools). Scripts then list classifier models with `models.getAvailableOfType("classifier")` and call `models.classify(model, { state, questions })`:
723
723
 
@@ -738,6 +738,10 @@ return result.answers;
738
738
 
739
739
  [Codemode](codemode.md#classify) describes the question and answer types.
740
740
 
741
+ Classifier models whose `input` includes `"image"` also judge images passed in `images`; other classifier models return an error for them. Image input works with GPT-6 Luna, with Jev from `typesafe` and `cloudflare-workers-ai`, with Clef from `cloudflare-workers-ai`, and with the OpenRouter models whose catalog lists image input. Jev takes PNG and JPEG images, and image input is experimental on TypeSafe's own API. Clef takes PNG, JPEG, and WebP images, at most four per request. Jev from `vercel-ai-gateway` and `opencode` takes text only.
742
+
743
+ Each System One service takes images in its own request format, set by the model's `compat.imageInput`. Built-in models set it, so overriding a provider's `baseUrl` keeps it. A custom provider that proxies another service's classifiers sets it in its provider-level `compat` in `models.json` (see [System One compatibility](models-configuration.md#typesafe-system-one)). OpenRouter in particular answers requests in TypeSafe's format without an error but drops the images.
744
+
741
745
  When the service reports token counts, as all System One services do, `result.usage` carries them with their cost. Pi adds the usage of a script's classifier calls to the `codemode` tool result, so it counts toward the session cost in the footer and `/session`. The cost uses the model's catalog price; models without one, such as TypeSafe's direct `jev-latest`, report tokens at no cost.
742
746
 
743
747
  Extensions call classifiers through `ctx.modelRegistry.classify()`, without codemode. [Virtual models](virtual-models.md#route-requests) can use them to route requests; see the `jev-router.ts` example.
package/docs/sdk.md CHANGED
@@ -254,7 +254,7 @@ The host must choose:
254
254
 
255
255
  - **Lifetime and cancellation:** a settled main-agent turn is not the end of background work. `session.abort()` cancels foreground-owned work, not detached work. Use `session.tasks.kill(id)` for a whole execution, or `await session.tasks.shutdown(graceMs?)` to close admission, cancel all owned work and drain within a bounded grace period (default 2 seconds, maximum 60 seconds). A non-cooperating executor can outlive that grace period.
256
256
  - **Result-driven turns and cost:** automatic completions join the host's prompt queue, so a running turn receives them as steering and an idle session starts a completion turn. Use the nestable `session.pauseTaskNotifications()` to defer delivery; its returned release function is idempotent. Pausing does not stop execution or its costs. Budget worker activity and completion-triggered turns explicitly.
257
- - **Observation and delivery:** `session.tasks` exposes the [public supervision methods](extensions.md#ctxtasks). `read()` and `wait()` observe; a terminal snapshot alone does not acknowledge model delivery. The built-in `tasks wait` tool binds a delivery receipt to its tool-call ID; the host confirms it only after persisting the result, even if extensions rewrite its details. A direct SDK waiter that consumes the result outside model history can explicitly call `session.tasks.markDelivered(task.id)` after successful consumption. A timeout or cancelled wait does not stop the execution. Failed notification delivery leaves the result available for inspection; after fixing the host failure, call `session.retryTaskNotifications()` explicitly. Pi does not retry a failed delivery forever on a timer.
257
+ - **Observation and delivery:** `session.tasks` exposes the [public supervision methods](extensions.md#ctxtasks). `read()`, `wait()`, and `waitAny()` observe; a terminal snapshot alone does not acknowledge model delivery. `waitAny()` covers only tasks running at call time and returns `undefined` for an empty cohort, timeout, interruption, or closed host. The optional `interruptSignal` ends only a wait. The built-in `tasks wait` tool binds a delivery receipt to its tool-call ID; the host confirms it only after persisting the result, even if extensions rewrite its details. A direct SDK waiter that consumes the result outside model history can explicitly call `session.tasks.markDelivered(task.id)` after successful consumption. A timeout or cancelled wait does not stop the execution. Failed notification delivery leaves the result available for inspection; after fixing the host failure, call `session.retryTaskNotifications()` explicitly. Pi does not retry a failed delivery forever on a timer.
258
258
  - **Shutdown:** `await runtime.dispose()` handles background shutdown and extension lifecycle when using the runtime API. For a directly owned session, drain Background, abort the main agent, then dispose; synchronous `session.dispose()` alone is not an awaited drain.
259
259
 
260
260
  A deliberately single-turn embedding can suppress completion-triggered turns and cancel leftover work on exit:
@@ -280,7 +280,7 @@ try {
280
280
 
281
281
  For a longer-lived embedding, keep the host alive, subscribe to snapshots and choose bounded waits or release notification delivery according to your model-turn policy. Do not use a panel or repeated reads as the billing or cleanup mechanism.
282
282
 
283
- Automatic completion messages carry a version-2 `TaskCompletionSnapshot` in `details`, with the command, the tail of the output, diagnostics and explicit truncation flags. This snapshot is self-contained for transcript replay and is not sent to the model; `content` contains the separately bounded prose generated from it. Current terminal history uses version-2 `task-result` records, with no migration of earlier result records. The usage ledger remains independent. See [Background records](session-format.md#background-records).
283
+ Automatic completion messages carry a version-2 `TaskCompletionSnapshot` in `details`, with the command, a 40 KiB output tail, diagnostics and explicit truncation flags. This snapshot is self-contained for transcript replay and is not sent to the model; `content` contains separately bounded prose with an 8 KiB output tail. After compaction, one request-local reminder lists still-active background tasks to prevent duplicate work; it is not persisted and does not start a turn. Current terminal history uses version-2 `task-result` records, with no migration of earlier result records. The usage ledger remains independent. See [Background records](session-format.md#background-records).
284
284
 
285
285
  `backgroundAllowed: false` on `createAgentSession()` is a host capability ceiling, independent of task kind. It prevents enabling background execution through `bindExtensions` or direct runtime activation, including after reload. Requests for `background: true` are rejected before startup; foreground execution remains available. Omitting this option permits host opt-in but does not enable background execution by itself. This capability restriction is not an OS sandbox.
286
286
 
@@ -351,11 +351,11 @@ The retained tool result contains bounded text (up to 48 KiB) and no executor-pr
351
351
 
352
352
  Terminal history is restored only from the selected branch's valid version-2 result records; older fields such as `format` and `projection` are ignored. Version-1 result records are ignored without migration; their independent usage entries still participate in accounting. This is read-only history: no processes or workers are restarted or reattached, and restoration does not replay usage settlement or completion events. Forks can carry historical entries on their copied path, never live execution handles.
353
353
 
354
- These `custom` entries do not enter model context. Automatic delivery creates a `custom_message` with `customType: "task-completion"`, `display: true`, and a self-contained `TaskCompletionSnapshot` in `details`. The session formats `content` from the same snapshot, with a total limit of 48 KiB and 2,000 lines, keeping the end of the output. Only `content` is sent to the model.
354
+ These `custom` entries do not enter model context. Automatic delivery creates a `custom_message` with `customType: "task-completion"`, `display: true`, and a self-contained `TaskCompletionSnapshot` in `details`. The session formats `content` from the same snapshot, with a total limit of 48 KiB and 2,000 lines, keeping only an 8 KiB output tail for the automatic model turn. Only `content` is sent to the model.
355
355
 
356
356
  Completion details use one format, `version: 2`, bounded to 120 KiB of serialized JSON including escaping. It has `version`, `kind`, `taskId`, `title`, terminal `status`, numeric millisecond `startedAt` and `endedAt`, and `output: { text, truncated }`, plus optional `error`, `command: { text, truncated }`, `cwd`, `outputPath`, and `exitCode` (a number, or `null` when signal-reaped).
357
357
 
358
- `output` is the end of the final result text, bounded to 40 KiB including JSON escaping. Source flags survive further clipping. Missing shell metadata is left unspecified. The renderer sanitizes terminal control sequences and applies separate display row limits.
358
+ `output` is the end of the final result text, bounded to 40 KiB including JSON escaping for replay and expanded rendering; the smaller model-facing projection does not change this stored field. Source flags survive further clipping. Missing shell metadata is left unspecified. The renderer sanitizes terminal control sequences and applies separate display row limits.
359
359
 
360
360
  A shell completion's `details` can look like this:
361
361
 
package/docs/settings.md CHANGED
@@ -142,7 +142,7 @@ Version checks read the latest `@astralyn/pi` version from npm. Set `PI_SKIP_VER
142
142
  |---------|------|---------|-------------|
143
143
  | `compaction.enabled` | boolean | `true` | Enable auto-compaction |
144
144
  | `compaction.triggerPercent` | number | `85` | Percentage of the context window that triggers auto-compaction (clamped to 20–95) |
145
- | `compaction.reserveTokens` | number | derived | Fixed reserve below the context window; overrides `triggerPercent` |
145
+ | `compaction.reserveTokens` | number | derived | Free context tokens below which automatic compaction starts; overrides `triggerPercent` |
146
146
  | `compaction.keepRecentTokens` | number | `20000` | Recent-message retention target, capped at half the trigger line when the reserve is derived |
147
147
  | `compaction.modelOverrides` | object | - | Per-model `reserveTokens` / `keepRecentTokens` overrides keyed by exact `"provider/modelId"` |
148
148
 
@@ -156,7 +156,7 @@ Version checks read the latest `@astralyn/pi` version from npm. Set `PI_SKIP_VER
156
156
  }
157
157
  ```
158
158
 
159
- Compaction triggers at `contextWindow − reserveTokens`. Unless `reserveTokens` is set, the reserve is derived from `triggerPercent` for the active model's window, so large-window models compact at 85% usage instead of upstream's ~98%. Setting `reserveTokens` restores a fixed reserve.
159
+ Compaction triggers at `contextWindow − reserveTokens`. Unless `reserveTokens` is set, the reserve is derived from `triggerPercent` for the active model's window, so large-window models compact at 85% usage instead of upstream's ~98%. Setting `reserveTokens` restores a fixed reserve. Either way, summarization uses the model's output limit rather than the reserve.
160
160
 
161
161
  The retention target shrinks with a derived reserve without changing the saved settings. Whole messages and tool-call/result groups can exceed the retention target. See [Compaction](compaction.md#how-it-works) for the budget rules and examples.
162
162
 
package/docs/tui.md CHANGED
@@ -105,6 +105,77 @@ const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done
105
105
 
106
106
  The factory receives Pi's current `TUI`, semantic `Theme`, and configured `KeybindingsManager`. It may return a component synchronously or as a promise. The `custom()` promise resolves only when the component calls `done(result)`.
107
107
 
108
+
109
+ ### Build a minimal custom screen
110
+
111
+ This complete extension shows the custom-component lifecycle: render within the supplied width, use injected keybindings, invalidate cached output after state changes, request a render, and finish through `done()`.
112
+
113
+ ```typescript
114
+ import type { ExtensionAPI } from "@astralyn/pi";
115
+ import { truncateToWidth } from "@earendil-works/pi-tui";
116
+
117
+ export default function (pi: ExtensionAPI) {
118
+ pi.registerCommand("pick-target", {
119
+ description: "Pick a deployment target",
120
+ handler: async (_args, ctx) => {
121
+ if (ctx.mode !== "tui") {
122
+ ctx.ui.notify("pick-target requires interactive mode", "error");
123
+ return;
124
+ }
125
+
126
+ const choices = ["development", "staging", "production"];
127
+ const selected = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
128
+ let selectedIndex = 0;
129
+ let cachedWidth: number | undefined;
130
+ let cachedLines: string[] | undefined;
131
+
132
+ const invalidate = () => {
133
+ cachedWidth = undefined;
134
+ cachedLines = undefined;
135
+ };
136
+
137
+ return {
138
+ render(width: number): string[] {
139
+ if (cachedLines && cachedWidth === width) return cachedLines;
140
+
141
+ cachedWidth = width;
142
+ cachedLines = [
143
+ theme.fg("accent", "Choose a deployment target"),
144
+ ...choices.map((choice, index) => `${index === selectedIndex ? "> " : " "}${choice}`),
145
+ theme.fg("dim", "up/down move, enter selects, escape cancels"),
146
+ ].map((line) => truncateToWidth(line, width));
147
+ return cachedLines;
148
+ },
149
+ handleInput(data: string): void {
150
+ if (keybindings.matches(data, "tui.select.up")) {
151
+ selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
152
+ } else if (keybindings.matches(data, "tui.select.down")) {
153
+ selectedIndex = (selectedIndex + 1) % choices.length;
154
+ } else if (keybindings.matches(data, "tui.select.confirm")) {
155
+ done(choices[selectedIndex]);
156
+ return;
157
+ } else if (keybindings.matches(data, "tui.select.cancel")) {
158
+ done(undefined);
159
+ return;
160
+ } else {
161
+ return;
162
+ }
163
+
164
+ invalidate();
165
+ tui.requestRender();
166
+ },
167
+ invalidate,
168
+ };
169
+ });
170
+
171
+ if (selected) ctx.ui.notify(`Selected ${selected}`, "info");
172
+ },
173
+ });
174
+ }
175
+ ```
176
+
177
+ This component does not display a text cursor, so it does not need `Focusable`. A component containing `Input` or `Editor` must implement `Focusable` and propagate its `focused` value to that child.
178
+
108
179
  ### Disposal
109
180
 
110
181
  A component returned from `ctx.ui.custom()`, `setWidget()`, `setFooter()`, or `setHeader()` may also implement `dispose(): void`. Use it to stop timers, abort owned work, and unsubscribe listeners. Pi calls it after a custom component finishes, or when a widget, footer, or header is replaced or cleared.
package/docs/windows.md CHANGED
@@ -8,10 +8,32 @@ Pi uses Git Bash by default on Windows. Checked locations (in order):
8
8
 
9
9
  For most users, [Git for Windows](https://git-scm.com/download/win) is sufficient.
10
10
 
11
+ Pi resolves Bash in this order:
12
+
13
+ 1. `shellPath` from `~/.pi/agent/settings.json`
14
+ 2. Git Bash at `%ProgramFiles%\Git\bin\bash.exe`
15
+ 3. Git Bash at `%ProgramFiles(x86)%\Git\bin\bash.exe`
16
+ 4. `bash.exe` on `PATH`, including Cygwin, MSYS2, or legacy WSL Bash
17
+
18
+ Start Pi and enter this command to verify the shell:
19
+
20
+ ```text
21
+ !echo "$BASH_VERSION"
22
+ ```
23
+
24
+ If Pi cannot find Bash, it reports the locations it checked. Install Git for Windows, set `shellPath`, or add Bash to `PATH`.
25
+
11
26
  ## PowerShell Tool
12
27
 
13
28
  The optional `powershell` tool runs commands through `pwsh.exe` when available, otherwise Windows PowerShell. It starts PowerShell with `-NoProfile -NonInteractive -ExecutionPolicy Bypass`. Administrator-enforced execution policies can still take precedence.
14
29
 
30
+ PowerShell discovery checks these locations in order:
31
+
32
+ 1. `pwsh.exe` on `PATH`
33
+ 2. `%ProgramFiles%\PowerShell\7\pwsh.exe`
34
+ 3. `powershell.exe` on `PATH`
35
+ 4. `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`
36
+
15
37
  Use `defaultTools` to replace the model-facing `bash` tool:
16
38
 
17
39
  ```json
@@ -30,6 +52,12 @@ Or enable both while comparing behavior:
30
52
 
31
53
  The `!` and `!!` editor commands still use Bash.
32
54
 
55
+ ## WSL
56
+
57
+ WSL path conversion on Windows and extra Git branch polling in WSL recognize `/mnt/<drive>`, not custom mount roots.
58
+
59
+ When using the legacy WSL `bash.exe` launcher from Windows, Pi sends commands over stdin to avoid command-line quoting problems.
60
+
33
61
  ## Custom Bash Path
34
62
 
35
63
  ```json
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-custom-provider",
9
- "version": "1.3.0",
9
+ "version": "1.4.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",