@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/examples/custom-tools/README.md +11 -7
  3. package/examples/custom-tools/hello/index.ts +2 -2
  4. package/examples/extensions/README.md +19 -8
  5. package/examples/extensions/api-demo.ts +15 -19
  6. package/examples/extensions/hello.ts +5 -6
  7. package/examples/extensions/plan-mode.ts +1 -1
  8. package/examples/extensions/reload-runtime.ts +4 -3
  9. package/examples/extensions/with-deps/index.ts +4 -3
  10. package/examples/sdk/06-extensions.ts +4 -2
  11. package/package.json +7 -17
  12. package/src/autoresearch/tools/init-experiment.ts +38 -41
  13. package/src/autoresearch/tools/log-experiment.ts +32 -41
  14. package/src/autoresearch/tools/run-experiment.ts +3 -3
  15. package/src/autoresearch/tools/update-notes.ts +11 -11
  16. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  17. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  18. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  19. package/src/commit/agentic/tools/git-overview.ts +4 -4
  20. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  21. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  22. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  23. package/src/commit/agentic/tools/schemas.ts +28 -28
  24. package/src/commit/agentic/tools/split-commit.ts +22 -21
  25. package/src/commit/analysis/summary.ts +4 -4
  26. package/src/commit/changelog/generate.ts +7 -11
  27. package/src/commit/shared-llm.ts +22 -34
  28. package/src/config/config-file.ts +35 -13
  29. package/src/config/model-registry.ts +9 -190
  30. package/src/config/models-config-schema.ts +166 -0
  31. package/src/config/settings-schema.ts +18 -0
  32. package/src/edit/index.ts +2 -2
  33. package/src/edit/modes/apply-patch.ts +7 -6
  34. package/src/edit/modes/patch.ts +18 -25
  35. package/src/edit/modes/replace.ts +18 -20
  36. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  37. package/src/eval/py/executor.ts +233 -623
  38. package/src/eval/py/kernel.ts +27 -2
  39. package/src/exa/factory.ts +5 -4
  40. package/src/exa/mcp-client.ts +1 -1
  41. package/src/exa/researcher.ts +9 -20
  42. package/src/exa/search.ts +26 -52
  43. package/src/exa/types.ts +1 -1
  44. package/src/exa/websets.ts +54 -53
  45. package/src/exec/bash-executor.ts +2 -1
  46. package/src/extensibility/custom-commands/loader.ts +5 -3
  47. package/src/extensibility/custom-commands/types.ts +4 -2
  48. package/src/extensibility/custom-tools/loader.ts +5 -3
  49. package/src/extensibility/custom-tools/types.ts +7 -6
  50. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  51. package/src/extensibility/extensions/loader.ts +7 -3
  52. package/src/extensibility/extensions/types.ts +9 -5
  53. package/src/extensibility/extensions/wrapper.ts +1 -2
  54. package/src/extensibility/hooks/loader.ts +3 -1
  55. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  56. package/src/extensibility/hooks/types.ts +4 -2
  57. package/src/extensibility/plugins/legacy-pi-compat.ts +30 -0
  58. package/src/extensibility/shared-events.ts +1 -1
  59. package/src/extensibility/typebox.ts +391 -0
  60. package/src/goals/tools/goal-tool.ts +6 -12
  61. package/src/hashline/types.ts +4 -4
  62. package/src/hindsight/state.ts +2 -2
  63. package/src/index.ts +0 -2
  64. package/src/internal-urls/docs-index.generated.ts +7 -7
  65. package/src/lsp/types.ts +30 -38
  66. package/src/mcp/manager.ts +1 -1
  67. package/src/mcp/tool-bridge.ts +1 -1
  68. package/src/modes/components/session-observer-overlay.ts +12 -1
  69. package/src/modes/components/status-line/segments.ts +2 -1
  70. package/src/modes/controllers/command-controller.ts +27 -2
  71. package/src/modes/controllers/event-controller.ts +3 -4
  72. package/src/modes/interactive-mode.ts +1 -1
  73. package/src/modes/rpc/host-tools.ts +1 -1
  74. package/src/modes/rpc/rpc-client.ts +1 -1
  75. package/src/modes/rpc/rpc-types.ts +1 -1
  76. package/src/modes/theme/theme.ts +111 -117
  77. package/src/modes/types.ts +1 -1
  78. package/src/modes/utils/context-usage.ts +2 -2
  79. package/src/sdk.ts +31 -8
  80. package/src/session/agent-session.ts +74 -104
  81. package/src/session/messages.ts +16 -51
  82. package/src/session/session-manager.ts +22 -2
  83. package/src/session/streaming-output.ts +16 -6
  84. package/src/task/executor.ts +208 -86
  85. package/src/task/index.ts +15 -11
  86. package/src/task/render.ts +32 -5
  87. package/src/task/types.ts +54 -39
  88. package/src/tools/ask.ts +12 -12
  89. package/src/tools/ast-edit.ts +11 -15
  90. package/src/tools/ast-grep.ts +9 -10
  91. package/src/tools/bash.ts +9 -23
  92. package/src/tools/browser.ts +39 -53
  93. package/src/tools/calculator.ts +12 -11
  94. package/src/tools/checkpoint.ts +7 -7
  95. package/src/tools/debug.ts +40 -43
  96. package/src/tools/eval.ts +6 -8
  97. package/src/tools/find.ts +10 -13
  98. package/src/tools/gh.ts +71 -128
  99. package/src/tools/hindsight-recall.ts +4 -6
  100. package/src/tools/hindsight-reflect.ts +5 -5
  101. package/src/tools/hindsight-retain.ts +15 -17
  102. package/src/tools/image-gen.ts +32 -82
  103. package/src/tools/index.ts +4 -1
  104. package/src/tools/inspect-image.ts +8 -9
  105. package/src/tools/irc.ts +15 -27
  106. package/src/tools/job.ts +14 -21
  107. package/src/tools/read.ts +7 -8
  108. package/src/tools/recipe/index.ts +7 -9
  109. package/src/tools/render-mermaid.ts +12 -12
  110. package/src/tools/report-tool-issue.ts +4 -4
  111. package/src/tools/resolve.ts +11 -11
  112. package/src/tools/review.ts +14 -26
  113. package/src/tools/search-tool-bm25.ts +7 -9
  114. package/src/tools/search.ts +19 -22
  115. package/src/tools/ssh.ts +7 -7
  116. package/src/tools/todo-write.ts +26 -34
  117. package/src/tools/vim.ts +10 -26
  118. package/src/tools/write.ts +5 -5
  119. package/src/tools/yield.ts +100 -54
  120. package/src/web/search/index.ts +9 -24
  121. package/src/prompts/compaction/branch-summary-context.md +0 -5
  122. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  123. package/src/prompts/compaction/branch-summary.md +0 -30
  124. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  125. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  126. package/src/prompts/compaction/compaction-summary.md +0 -38
  127. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  128. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  129. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  130. package/src/prompts/system/file-operations.md +0 -10
  131. package/src/prompts/system/handoff-document.md +0 -49
  132. package/src/prompts/system/summarization-system.md +0 -3
  133. package/src/session/compaction/branch-summarization.ts +0 -324
  134. package/src/session/compaction/compaction.ts +0 -1420
  135. package/src/session/compaction/errors.ts +0 -31
  136. package/src/session/compaction/index.ts +0 -8
  137. package/src/session/compaction/pruning.ts +0 -91
  138. package/src/session/compaction/utils.ts +0 -184
package/CHANGELOG.md CHANGED
@@ -2,6 +2,61 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.1.0] - 2026-05-15
6
+ ### Breaking Changes
7
+
8
+ - Changed the extension and hook runtime API by moving schema typing from direct TypeBox imports to `TSchema` from `@oh-my-pi/pi-ai`, requiring callers who use TypeScript imports of `Type` to migrate via provided injected modules
9
+
10
+ ### Added
11
+
12
+ - Added a cancellable handoff progress indicator in `/handoff` that displays while handoff generation runs and can be aborted with `Esc`
13
+ - Added `apiKey` as a supported provider override field in model config, allowing API-key-only overrides to provide fallback credentials for built-in models
14
+ - Added `supportsMultipleSystemMessages`, `allowsSyntheticReasoningContentForToolCalls`, `disableReasoningOnToolChoice`, and `levels` model-thinking compatibility fields to model configuration schemas
15
+ - Added `zod` to the Extension, Custom Tool, Hook, and Custom Command APIs as `pi.zod` so extension and plugin authors can define tool schemas with Zod without separate imports
16
+ - Added `pi.zod` as a canonical schema API for examples and extension plugins while keeping `typebox` available as legacy compatibility
17
+ - Added a `telemetry` option to `createAgentSession` for passing OpenTelemetry configuration through to the underlying Agent
18
+
19
+ ### Changed
20
+
21
+ - Changed handoff generation to run as a one-shot handoff request and switch to the new session only after it completes, avoiding an extra assistant handoff turn in chat history
22
+ - Changed `pi.typebox.Type.Composite` to merge all object schemas in the provided list, enabling more than two object inputs
23
+ - Changed `pi.typebox.Type.Record` to validate record keys against the provided key schema instead of forcing string keys
24
+ - Changed `pi.typebox.Type.Array` with `uniqueItems: true` to reject duplicate items while preserving the constraint in wire schemas
25
+ - Changed `pi.typebox.Type.Object` with `additionalProperties: false` to reject unknown properties during parsing
26
+ - Changed `pi.typebox.Type.Enum` in the compatibility shim to preserve numeric TypeScript enum values
27
+ - Changed tool parameter schemas across the agent to use the shared Pi schema pipeline (`TSchema` plus Zod/JSON Schema validation) instead of direct AJV/TypeBox compilation for stricter schema validation compatibility
28
+ - Changed GitHub tool input schema shape to expose operation fields in a flat schema form without legacy `run_watch`-style nesting
29
+ - Changed Python session pooling to remove the previous 4-session retention cap and 5-minute idle-session eviction, so kernels now stay alive for a session until explicitly disposed via `disposeKernelSessionsByOwner` or `disposeAllKernelSessions`
30
+ - Changed kernel cleanup behavior to avoid automatic eviction by idle timeout and capacity pressure, so additional Python sessions are not queued behind retained-session shutdown retries
31
+ - Replaced the bundled `@sinclair/typebox` runtime dependency with an in-repo Zod-backed shim exposed through `pi.typebox.Type.*`. Common builders (`Object`, `String`, `Number`, `Integer`, `Boolean`, `Array`, `Tuple`, `Union`, `Intersect`, `Literal`, `Enum`, `Optional`, `Nullable`, `Record`, `Partial`, `Required`, `Pick`, `Omit`, `Composite`, …) keep their existing call signatures but now return Zod schemas that flow through the same validation/wire pipeline as `pi.zod`. Bare `@sinclair/typebox` imports inside extensions are transparently remapped to the same shim by the runtime plugin shim, so plugins that authored against `import { Type } from "@sinclair/typebox"` keep working unchanged. Plugins that relied on TypeBox-only submodule APIs (`@sinclair/typebox/compiler`, `@sinclair/typebox/value`, `TypeRegistry`, the `Symbol(TypeBox.Kind)` marker) must vendor `@sinclair/typebox` in their own package — only the root import is remapped.
32
+
33
+ ### Deprecated
34
+
35
+ - Deprecated direct TypeBox-only examples for plugin schemas by updating example documentation to prefer `pi.zod`
36
+
37
+ ### Fixed
38
+
39
+ - Fixed auto-triggered handoff flow to perform only a single handoff-generation model call instead of an extra prompt cycle
40
+ - Fixed handoff cancellation behavior so a pre-cancelled signal returns `Handoff cancelled` without starting generation and aborting handoff now propagates through the handoff request signal
41
+ - Fixed `create_conventional_analysis` parsing to ignore harmless extra fields and still parse the required conventional fields
42
+ - Fixed BashTool async request validation flow so async execution remains disabled and returns the explicit `Async bash execution is disabled` error
43
+ - Fixed `task.simple` invalid `schema` and `context` argument handling to still reject unsupported fields after tool-argument validation
44
+ - Fixed subagent execution hangs by enforcing `task.maxRuntimeMs` as a wall-clock limit even when inference streaming stalls, so stuck subagents now abort and report runtime-limit exceeded
45
+ - Fixed tool schema compatibility validation by routing TypeBox schemas through shared conversion and Zod-based validation to avoid strict-schema provider mismatches
46
+ - Fixed Python execution cancellation and timeouts by escalating to kernel shutdown if `SIGINT` did not terminate a running cell within 2 seconds, preventing indefinite hangs in queued or stuck sessions
47
+ - Fixed cleanup blocking during long-running executions by forcing a kernel shutdown path when interrupt-based cancellation is ignored
48
+ - Fixed bash output emitting a spurious `[… 0 lines elided (NB) …]` marker (and reordering the artifact link before the command output) when the shell minimizer rewrote a small command's output. After `OutputSink.replace()` swapped the minimized text into the buffer, the subsequent `sink.push("[raw output: artifact://N]\n")` chunk was funneled back into the (now empty) head-retention window while the pre-replace `#totalBytes` still tracked the original raw stream — so `dump()` composed `<head=artifact-link> + <middle-elision marker against stale totals> + <tail=minimized text>` instead of `<minimized text> + <artifact link>`. `replace()` now realigns `#totalBytes`/`#totalLines`/`#sawData`/`#truncated` to the authoritative buffer and disables head retention for the lifetime of the sink, so further pushes append to the tail buffer in order. The bash executor also drops the leading `\n` on the artifact-link push when the minimized text already ends with one so the separator stays single-newline.
49
+ - Fixed legacy plugin extensions failing to load on Windows when they import a bare-specifier dependency from their own `node_modules` (e.g. `import YAML from "yaml"` in `supipowers`). The legacy-pi mirror resolved the dependency to its absolute path and then ran the path through `isUrlLikeSpecifier`, whose `^[A-Za-z][A-Za-z\d+.-]*:` regex matched the Windows drive letter (`C:`) and short-circuited the `pathToFileURL` conversion. The raw path was emitted into the mirrored TS source as `import x from "C:\\Users\\...\\dep\\dist\\index.js"`, where `\n`, `\U`, `\y` and other backslash sequences were eaten by the TS string-literal parser, producing nonsense package specifiers like `C:Usersjames.ompagentextensionssupipowers\node_modulesyamldistindex.js` that Bun's resolver rejected with `Cannot find package …`. `isUrlLikeSpecifier` now rejects `^[A-Za-z]:[\\/]` first, so Windows absolute paths flow through `pathToFileURL` like every other absolute path and reach the mirror as proper `file:///C:/...` URLs.
50
+ - Fixed Python session queued executions silently resurrecting kernels after `disposeAllKernelSessions` or `disposeKernelSessionsByOwner` removed the session: queued work now checks the session is still registered before replacing or executing on a kernel and rejects with cancellation otherwise
51
+ - Fixed Python session disposal treating an unconfirmed `PythonKernel.shutdown()` result as success: sessions whose kernel shutdown returns `{ confirmed: false }` (or rejects) are now retained in the registry and a `warn` is logged so a later dispose can retry instead of orphaning the subprocess
52
+ - Fixed `task.maxRuntimeMs` losing wall-clock aborts that fired during pre-prompt session setup by re-checking the abort signal immediately before issuing the model prompt, so a stalled subagent now exits with the runtime-limit reason instead of hanging through setup races
53
+ - Fixed late `yield` events landing after a wall-clock timeout from flipping a timed-out subagent to a successful exit, so the reported `aborted` flag and exit code now always reflect the runtime-limit breach while yield payloads remain in `extractedToolData`
54
+ - Fixed async-task progress consumer to copy `contextTokens` and `contextWindow` from the completed `SingleResult` onto `AgentProgress`, so UI gauges keep showing per-turn context after a backgrounded task finishes
55
+ - Fixed the status-line `path` segment ignoring `stripWorkPrefix: false` when selecting the folder icon for scratch directories. The icon selection now respects the same gate as the scratch path stripping, so disabling `stripWorkPrefix` keeps the regular `folder` icon even when the project directory is inside a scratch root.
56
+ - Fixed `YieldTool` constructor to fall back to the loose record schema when the session `outputSchema` contains unresolved `$ref` strings (e.g. external or cyclic references that survive dereferencing), instead of installing a validator that would reject every payload with an unresolved-reference error
57
+ - Fixed `pi.typebox.Type.String` dropping `minLength`/`maxLength`/`pattern` constraints when a `format` (e.g. `email`, `url`, `uuid`) was also supplied; length and pattern checks are now applied to the format-specific schema instead of being gated on an `instanceof z.ZodString` check that never matched the format subclasses.
58
+ - Fixed `pi.typebox.Type.Object` stripping unknown properties when constructed without explicit `additionalProperties`. TypeBox preserves extras by default, so the shim now installs `.loose()` for the omitted/`true` cases while keeping `.strict()` for `additionalProperties: false` and `.catchall(schema)` for a schema value.
59
+
5
60
  ## [15.0.2] - 2026-05-15
6
61
 
7
62
  ### Added
@@ -94,7 +149,7 @@
94
149
  - Added `pr://<N>/diff`, `pr://<N>/diff/<i>`, and `pr://<N>/diff/all` internal-URL shapes covering changed-file listings, per-file slices, and the full unified diff. They share one `pr-diff` SQLite cache row with the same TTL knobs as `pr://<N>` views (`github.cache.softTtlSec` / `github.cache.hardTtlSec` / `github.cache.enabled`). Single PR views now advertise the diff entry point via a `Diff: pr://<owner>/<repo>/<N>/diff` note. Cache schema bumped to `user_version = 3`; older rows are dropped on first open to add credential-scoped keys and relax the `kind` CHECK constraint.
95
150
  - Added `issue://` / `pr://` internal-URL schemes that share a SQLite-backed cache with the rest of the `github` tool. Single-item reads (`issue://<N>`, `issue://<owner>/<repo>/<N>`) return rendered markdown and within `github.cache.softTtlSec` (default 5 minutes) skip the `gh` round-trip entirely; within `github.cache.hardTtlSec` (default 7 days) the cached row is returned and a background refresh is scheduled. Root and repo-scoped reads (`issue://`, `pr://owner/repo`) issue a live `gh issue list` / `gh pr list` for browsing, supporting `?state=open|closed|all` for issues, `?state=open|closed|merged|all` for PRs, and `?limit=`, `?author=`, `?label=` query params. Rendered output lands in `~/.omp/cache/github-cache.db` (override via `OMP_GITHUB_CACHE_DB`); disable the cache entirely with `github.cache.enabled = false`. Cwd→default-repo lookups (`gh repo view`) are memoized per-process.
96
151
  - Added new `Approve and compact context` choice to the ExitPlanMode approval selector. Sits between `Approve and execute` (purge session) and `Approve and keep context` (full transcript) — runs `/compact` on the plan-mode transcript with a planning-specific summarization hint, then dispatches the plan-approved execution turn so it lands on a fresh cache anchor with the summarized rationale carried over. Cancelling the compaction (Esc or any other abort source) defers the execution dispatch and surfaces a warning so the operator can resubmit manually; non-abort failures proceed best-effort.
97
- - Added `CompactionCancelledError` typed sentinel and `CompactionOutcome` (`"ok" | "cancelled" | "failed"`) return type to `@oh-my-pi/pi-coding-agent/session/compaction`. `CommandController.executeCompaction` and `handleCompactCommand` now return the outcome instead of `void` so callers can discriminate user-driven aborts from generic failures without inspecting error messages.
152
+ - Added `CompactionCancelledError` typed sentinel and `CompactionOutcome` (`"ok" | "cancelled" | "failed"`) return type to `@oh-my-pi/pi-agent-core/compaction`. `CommandController.executeCompaction` and `handleCompactCommand` now return the outcome instead of `void` so callers can discriminate user-driven aborts from generic failures without inspecting error messages.
98
153
  - Added a `credential_disabled` extension event so extensions can subscribe via `pi.on("credential_disabled", handler)` and react when `AuthStorage` automatically soft-disables a credential (e.g. OAuth `invalid_grant`). Replaces the current `agent_end` errorMessage regex pattern downstream extensions have to match against. Handler payload is `{ type, provider, disabledCause }`. `createAgentSession()` subscribes the per-session extension runner to the shared `AuthStorage` via `authStorage.onCredentialDisabled(...)` at the very top of session creation — before any startup model probes run — so events fire on every disable regardless of whether the embedder also has a constructor `onCredentialDisabled` handler attached. The SDK forwards through `ExtensionRunner.emitCredentialDisabled(event)`, which buffers events until `runner.initialize(...)` runs in the mode controller and then flushes them through `emit()` so extension handlers see populated UI/runtime context (rather than the constructor's no-op default with `hasUI=false`, an unset model, and no-op runtime actions). On `session.dispose()` the subscription is unsubscribed; the embedder's constructor-attached listener keeps firing through its own permanent subscription. The outer `createAgentSession()` catch also releases the subscription if startup throws before the dispose-wrap is wired, so repeated retries don't accumulate dead listeners.
99
154
  - Added `omp acp` subcommand for launching as an ACP (Agent Client Protocol) server over stdio
100
155
  - Added explicit `type` discriminators to ACP `initialize` auth methods, including a `terminal` setup method gated on `clientCapabilities.auth.terminal`
@@ -47,7 +47,6 @@ See [docs/custom-tools.md](../../docs/custom-tools.md) for full documentation.
47
47
  **Factory pattern:**
48
48
 
49
49
  ```typescript
50
- import { Type } from "@sinclair/typebox";
51
50
  import { StringEnum } from "@oh-my-pi/pi-ai";
52
51
  import { Text } from "@oh-my-pi/pi-tui";
53
52
  import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
@@ -56,7 +55,7 @@ const factory: CustomToolFactory = (pi) => ({
56
55
  name: "my_tool",
57
56
  label: "My Tool",
58
57
  description: "Tool description for LLM",
59
- parameters: Type.Object({
58
+ parameters: pi.zod.object({
60
59
  action: StringEnum(["list", "add"] as const),
61
60
  }),
62
61
 
@@ -78,6 +77,8 @@ const factory: CustomToolFactory = (pi) => ({
78
77
  export default factory;
79
78
  ```
80
79
 
80
+ **Legacy:** `parameters: pi.typebox.Type.Object({ ... })` still works; the injected `typebox` is a small Zod-backed shim, and schemas flow through the same Zod pipeline as `pi.zod` schemas.
81
+
81
82
  **Custom rendering:**
82
83
 
83
84
  ```typescript
@@ -96,14 +97,17 @@ renderResult(result, { expanded, isPartial }, theme) {
96
97
  },
97
98
  ```
98
99
 
99
- **Use StringEnum for string parameters** (required for Google API compatibility):
100
+ **Use `StringEnum` for discriminated string tool args** (required for Google API compatibility):
100
101
 
101
102
  ```typescript
102
103
  import { StringEnum } from "@oh-my-pi/pi-ai";
103
104
 
104
- // Good
105
- action: StringEnum(["list", "add"] as const);
105
+ const { z } = pi.zod;
106
+
107
+ // Good — Google-safe enum wiring
108
+ parameters: z.object({
109
+ action: StringEnum(["list", "add"] as const),
110
+ });
106
111
 
107
- // Bad - doesn't work with Google
108
- action: Type.Union([Type.Literal("list"), Type.Literal("add")]);
112
+ // Avoid raw union-of-literals patterns that don't degrade well for strict JSON Schema providers
109
113
  ```
@@ -4,8 +4,8 @@ const factory: CustomToolFactory = pi => ({
4
4
  name: "hello",
5
5
  label: "Hello",
6
6
  description: "A simple greeting tool",
7
- parameters: pi.typebox.Type.Object({
8
- name: pi.typebox.Type.String({ description: "Name to greet" }),
7
+ parameters: pi.zod.object({
8
+ name: pi.zod.string().describe("Name to greet"),
9
9
  }),
10
10
 
11
11
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
@@ -71,9 +71,10 @@ See [docs/extensions.md](../../docs/extensions.md) for full documentation.
71
71
 
72
72
  ```typescript
73
73
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
74
- import { Type } from "@sinclair/typebox";
75
74
 
76
75
  export default function (pi: ExtensionAPI) {
76
+ const z = pi.zod;
77
+
77
78
  // Subscribe to lifecycle events
78
79
  pi.on("tool_call", async (event, ctx) => {
79
80
  if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
@@ -87,8 +88,8 @@ export default function (pi: ExtensionAPI) {
87
88
  name: "greet",
88
89
  label: "Greeting",
89
90
  description: "Generate a greeting",
90
- parameters: Type.Object({
91
- name: Type.String({ description: "Name to greet" }),
91
+ parameters: z.object({
92
+ name: z.string().describe("Name to greet"),
92
93
  }),
93
94
  async execute(toolCallId, params, onUpdate, ctx, signal) {
94
95
  return {
@@ -108,18 +109,28 @@ export default function (pi: ExtensionAPI) {
108
109
  }
109
110
  ```
110
111
 
112
+ **Legacy TypeBox-style schemas** (`pi.typebox`) remain available for older extensions and are backed by a tiny Zod-shim — prefer `pi.zod` directly for new code.
113
+
114
+ ```typescript
115
+ const { Type } = pi.typebox;
116
+ parameters: Type.Object({ name: Type.String() });
117
+ ```
118
+
111
119
  ## Key Patterns
112
120
 
113
- **Use StringEnum for string parameters** (required for Google API compatibility):
121
+ **Use `StringEnum` for discriminated string tool args** (required for Google API compatibility):
114
122
 
115
123
  ```typescript
116
124
  import { StringEnum } from "@oh-my-pi/pi-ai";
117
125
 
118
- // Good
119
- action: StringEnum(["list", "add"] as const);
126
+ const { z } = pi.zod;
127
+
128
+ // Good — Google-safe enum wiring
129
+ parameters: z.object({
130
+ action: StringEnum(["list", "add"] as const),
131
+ });
120
132
 
121
- // Bad - doesn't work with Google
122
- action: Type.Union([Type.Literal("list"), Type.Literal("add")]);
133
+ // Avoid raw union-of-literals patterns that don't degrade well for strict JSON Schema providers
123
134
  ```
124
135
 
125
136
  **State persistence via details:**
@@ -1,39 +1,35 @@
1
1
  /**
2
2
  * API Demo Extension
3
3
  *
4
- * Demonstrates using ExtensionAPI's logger, typebox, and pi module access.
4
+ * Demonstrates using ExtensionAPI's logger, injected `pi.zod`, and pi module access.
5
5
  * These features are now exposed directly on the ExtensionAPI, matching
6
6
  * the CustomToolAPI interface.
7
7
  */
8
8
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
9
9
 
10
10
  export default function (pi: ExtensionAPI) {
11
- // 1. Access TypeBox directly from pi.typebox (no separate import needed)
12
- const { Type } = pi.typebox;
11
+ const { z } = pi.zod;
13
12
 
14
- // 2. Access the logger for debugging
15
- pi.logger.debug("API demo extension loaded");
16
-
17
- // 3. Register a tool that uses all three API features
18
- // Import StringEnum from typebox helpers
13
+ // Access shared schema helpers from package exports (e.g. StringEnum for Google-safe enums)
19
14
  const { StringEnum } = pi.pi;
20
15
 
16
+ // Access the logger for debugging
17
+ pi.logger.debug("API demo extension loaded");
18
+
21
19
  pi.registerTool({
22
20
  name: "api_demo",
23
21
  label: "API Demo",
24
- description: "Demonstrates ExtensionAPI capabilities: logger, typebox, and pi module access",
25
- parameters: Type.Object({
26
- message: Type.String({ description: "Test message" }),
27
- logLevel: Type.Optional(
28
- StringEnum(["error", "warn", "debug"], {
29
- description: "Log level to use",
30
- default: "debug",
31
- }),
32
- ),
22
+ description: "Demonstrates ExtensionAPI capabilities: logger, zod, and pi module access",
23
+ parameters: z.object({
24
+ message: z.string().describe("Test message"),
25
+ logLevel: StringEnum(["error", "warn", "debug"], {
26
+ description: "Log level to use",
27
+ default: "debug",
28
+ }),
33
29
  }),
34
30
 
35
31
  async execute(_toolCallId, params, _onUpdate, ctx, _signal) {
36
- const { message, logLevel = "debug" } = params as { message: string; logLevel?: "error" | "warn" | "debug" };
32
+ const { message, logLevel } = params;
37
33
 
38
34
  // Use logger at specified level
39
35
  pi.logger[logLevel]("API demo tool executed", { message, logLevel });
@@ -58,7 +54,7 @@ export default function (pi: ExtensionAPI) {
58
54
  ``,
59
55
  `Features demonstrated:`,
60
56
  `1. ✓ Logger access via pi.logger`,
61
- `2. ✓ TypeBox access via pi.typebox`,
57
+ `2. ✓ Zod access via pi.zod`,
62
58
  `3. ✓ Pi module access via pi.pi`,
63
59
  ``,
64
60
  `Context:`,
@@ -1,24 +1,23 @@
1
1
  /**
2
2
  * Hello Tool - Minimal custom tool example
3
3
  *
4
- * Demonstrates using ExtensionAPI's logger, typebox, and pi module access.
4
+ * Demonstrates using ExtensionAPI's logger, injected `pi.zod`, and pi module access.
5
5
  */
6
6
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
7
7
 
8
8
  export default function (pi: ExtensionAPI) {
9
- // Access TypeBox via pi.typebox (no need to import separately)
10
- const { Type } = pi.typebox;
9
+ const { z } = pi.zod;
11
10
 
12
11
  pi.registerTool({
13
12
  name: "hello",
14
13
  label: "Hello",
15
14
  description: "A simple greeting tool",
16
- parameters: Type.Object({
17
- name: Type.String({ description: "Name to greet" }),
15
+ parameters: z.object({
16
+ name: z.string().describe("Name to greet"),
18
17
  }),
19
18
 
20
19
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
21
- const { name } = params as { name: string };
20
+ const { name } = params;
22
21
 
23
22
  // Use logger for debugging
24
23
  pi.logger.debug("Hello tool executed", { name });
@@ -426,7 +426,7 @@ Execute each step in order.`,
426
426
 
427
427
  // Extract todos from last message
428
428
  const messages = event.messages;
429
- const lastAssistant = [...messages].reverse().find(m => m.role === "assistant");
429
+ const lastAssistant = messages.findLast(m => m.role === "assistant");
430
430
  if (lastAssistant && Array.isArray(lastAssistant.content)) {
431
431
  const textContent = lastAssistant.content
432
432
  .filter(
@@ -5,10 +5,11 @@
5
5
  * tool that queues a follow-up command to trigger reload.
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
- import { Type } from "@sinclair/typebox";
8
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
10
9
 
11
10
  export default function (pi: ExtensionAPI) {
11
+ const { z } = pi.zod;
12
+
12
13
  // Command entrypoint for reload.
13
14
  // Treat reload as terminal for this handler.
14
15
  pi.registerCommand("reload-runtime", {
@@ -25,7 +26,7 @@ export default function (pi: ExtensionAPI) {
25
26
  name: "reload_runtime",
26
27
  label: "Reload Runtime",
27
28
  description: "Reload extensions, skills, prompts, and themes",
28
- parameters: Type.Object({}),
29
+ parameters: z.object({}),
29
30
  async execute() {
30
31
  pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
31
32
  return {
@@ -5,17 +5,18 @@
5
5
  * Requires: npm install in this directory
6
6
  */
7
7
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
8
- import { Type } from "@sinclair/typebox";
9
8
  import ms from "ms";
10
9
 
11
10
  export default function (pi: ExtensionAPI) {
11
+ const { z } = pi.zod;
12
+
12
13
  // Register a tool that uses ms
13
14
  pi.registerTool({
14
15
  name: "parse_duration",
15
16
  label: "Parse Duration",
16
17
  description: "Parse a human-readable duration string (e.g., '2 days', '1h', '5m') to milliseconds",
17
- parameters: Type.Object({
18
- duration: Type.String({ description: "Duration string like '2 days', '1h', '5m'" }),
18
+ parameters: z.object({
19
+ duration: z.string().describe("Duration string like '2 days', '1h', '5m'"),
19
20
  }),
20
21
  execute: async (_toolCallId, params) => {
21
22
  const result = ms(params.duration as ms.StringValue);
@@ -41,6 +41,8 @@ console.log();
41
41
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
42
42
 
43
43
  export default function (pi: ExtensionAPI) {
44
+ const { z } = pi.zod;
45
+
44
46
  pi.on("agent_start", async () => {
45
47
  console.log("[Extension] Agent starting");
46
48
  });
@@ -60,8 +62,8 @@ export default function (pi: ExtensionAPI) {
60
62
  name: "my_tool",
61
63
  label: "My Tool",
62
64
  description: "Does something useful",
63
- parameters: Type.Object({
64
- input: Type.String(),
65
+ parameters: z.object({
66
+ input: z.string(),
65
67
  }),
66
68
  execute: async (_toolCallId, params, _onUpdate, _ctx, _signal) => ({
67
69
  content: [{ type: "text", text: \`Processed: \${params.input}\` }],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.0.2",
4
+ "version": "15.1.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,17 +47,15 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "15.0.2",
51
- "@oh-my-pi/pi-agent-core": "15.0.2",
52
- "@oh-my-pi/pi-ai": "15.0.2",
53
- "@oh-my-pi/pi-natives": "15.0.2",
54
- "@oh-my-pi/pi-tui": "15.0.2",
55
- "@oh-my-pi/pi-utils": "15.0.2",
50
+ "@oh-my-pi/omp-stats": "15.1.1",
51
+ "@oh-my-pi/pi-agent-core": "15.1.1",
52
+ "@oh-my-pi/pi-ai": "15.1.1",
53
+ "@oh-my-pi/pi-natives": "15.1.1",
54
+ "@oh-my-pi/pi-tui": "15.1.1",
55
+ "@oh-my-pi/pi-utils": "15.1.1",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
- "@sinclair/typebox": "^0.34.49",
58
57
  "@types/turndown": "5.0.6",
59
58
  "@xterm/headless": "^6.0.0",
60
- "ajv": "^8.20.0",
61
59
  "chalk": "^5.6.2",
62
60
  "diff": "^9.0.0",
63
61
  "fflate": "0.8.2",
@@ -473,14 +471,6 @@
473
471
  "types": "./src/session/*.ts",
474
472
  "import": "./src/session/*.ts"
475
473
  },
476
- "./session/compaction": {
477
- "types": "./src/session/compaction/index.ts",
478
- "import": "./src/session/compaction/index.ts"
479
- },
480
- "./session/compaction/*": {
481
- "types": "./src/session/compaction/*.ts",
482
- "import": "./src/session/compaction/*.ts"
483
- },
484
474
  "./slash-commands/*": {
485
475
  "types": "./src/slash-commands/*.ts",
486
476
  "import": "./src/slash-commands/*.ts"
@@ -1,7 +1,7 @@
1
1
  import * as path from "node:path";
2
- import { StringEnum } from "@oh-my-pi/pi-ai";
2
+
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
- import { Type } from "@sinclair/typebox";
4
+ import * as z from "zod/v4";
5
5
  import type { ToolDefinition } from "../../extensibility/extensions";
6
6
  import type { Theme } from "../../modes/theme/theme";
7
7
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -16,46 +16,43 @@ export const HARNESS_FILENAME = "autoresearch.sh";
16
16
  export const DEFAULT_HARNESS_COMMAND = `bash ${HARNESS_FILENAME}`;
17
17
  const HARNESS_COMMIT_TITLE = "autoresearch: harness setup";
18
18
 
19
- const initExperimentSchema = Type.Object({
20
- name: Type.String({ description: "Human-readable experiment name." }),
21
- goal: Type.Optional(Type.String({ description: "Free-form description of what this session optimizes." })),
22
- primary_metric: Type.String({
23
- description:
19
+ const initExperimentSchema = z.object({
20
+ name: z.string().describe("Human-readable experiment name."),
21
+ goal: z.string().describe("Free-form description of what this session optimizes.").optional(),
22
+ primary_metric: z
23
+ .string()
24
+ .describe(
24
25
  "Primary metric name shown in the dashboard. Match the `METRIC <name>=<value>` lines printed by the benchmark.",
25
- }),
26
- metric_unit: Type.Optional(
27
- Type.String({ description: "Unit for the primary metric (e.g. ms, µs, mb). Empty when unitless." }),
28
- ),
29
- direction: Type.Optional(
30
- StringEnum(["lower", "higher"], { description: "Whether lower or higher values are better. Defaults to lower." }),
31
- ),
32
- secondary_metrics: Type.Optional(
33
- Type.Array(Type.String(), {
34
- description: "Names of secondary metrics tracked alongside the primary metric.",
35
- }),
36
- ),
37
- scope_paths: Type.Optional(
38
- Type.Array(Type.String(), {
39
- description:
40
- "Files or directories the agent expects to modify. Used post-hoc to flag scope deviations on log_experiment; never used to block edits.",
41
- }),
42
- ),
43
- off_limits: Type.Optional(
44
- Type.Array(Type.String(), {
45
- description:
46
- "Paths the agent SHOULD NOT modify. Used post-hoc to flag scope deviations on log_experiment; never used to block edits.",
47
- }),
48
- ),
49
- constraints: Type.Optional(
50
- Type.Array(Type.String(), { description: "Free-form constraints (e.g. 'no api break')." }),
51
- ),
52
- max_iterations: Type.Optional(Type.Number({ description: "Soft cap on iterations per segment. Optional." })),
53
- new_segment: Type.Optional(
54
- Type.Boolean({
55
- description:
56
- "When true, bump to a new segment even when an active session exists. New baselines and best-metric reset.",
57
- }),
58
- ),
26
+ ),
27
+ metric_unit: z.string().describe("Unit for the primary metric (e.g. ms, µs, mb). Empty when unitless.").optional(),
28
+ direction: z
29
+ .enum(["lower", "higher"] as const)
30
+ .describe("Whether lower or higher values are better. Defaults to lower.")
31
+ .optional(),
32
+ secondary_metrics: z
33
+ .array(z.string())
34
+ .describe("Names of secondary metrics tracked alongside the primary metric.")
35
+ .optional(),
36
+ scope_paths: z
37
+ .array(z.string())
38
+ .describe(
39
+ "Files or directories the agent expects to modify. Used post-hoc to flag scope deviations on log_experiment; never used to block edits.",
40
+ )
41
+ .optional(),
42
+ off_limits: z
43
+ .array(z.string())
44
+ .describe(
45
+ "Paths the agent SHOULD NOT modify. Used post-hoc to flag scope deviations on log_experiment; never used to block edits.",
46
+ )
47
+ .optional(),
48
+ constraints: z.array(z.string()).describe("Free-form constraints (e.g. 'no api break').").optional(),
49
+ max_iterations: z.number().describe("Soft cap on iterations per segment. Optional.").optional(),
50
+ new_segment: z
51
+ .boolean()
52
+ .describe(
53
+ "When true, bump to a new segment even when an active session exists. New baselines and best-metric reset.",
54
+ )
55
+ .optional(),
59
56
  });
60
57
 
61
58
  interface InitExperimentDetails {
@@ -1,8 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { StringEnum } from "@oh-my-pi/pi-ai";
3
+
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
- import { Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import type { ToolDefinition } from "../../extensibility/extensions";
7
7
  import type { Theme } from "../../modes/theme/theme";
8
8
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -36,46 +36,37 @@ import type {
36
36
 
37
37
  const EXPERIMENT_TOOL_NAMES = ["init_experiment", "run_experiment", "log_experiment", "update_notes"];
38
38
 
39
- const logExperimentSchema = Type.Object({
40
- metric: Type.Number({
41
- description: "Primary metric value for this run. May differ from the parsed value; deviation is recorded.",
42
- }),
43
- status: StringEnum(["keep", "discard", "crash", "checks_failed"], {
44
- description: "Outcome for this run.",
45
- }),
46
- description: Type.String({ description: "Short description of the experiment." }),
47
- metrics: Type.Optional(
48
- Type.Record(Type.String(), Type.Number(), { description: "Secondary metrics for this run." }),
49
- ),
50
- asi: Type.Optional(
51
- Type.Object(
52
- {},
53
- {
54
- additionalProperties: Type.Unknown(),
55
- description: "Free-form structured metadata captured for this run (hypothesis, learnings, etc.).",
56
- },
57
- ),
58
- ),
59
- commit: Type.Optional(
60
- Type.String({ description: "Override the commit hash recorded for this run. Defaults to the current HEAD." }),
61
- ),
62
- justification: Type.Optional(
63
- Type.String({
64
- description:
65
- "Required when the run modifies paths outside scope or inside off-limits and you still want it kept. Free-form explanation.",
66
- }),
67
- ),
68
- flag_runs: Type.Optional(
69
- Type.Array(
70
- Type.Object({
71
- run_id: Type.Number({ description: "Run id (#) of a previously logged run to flag as suspect." }),
72
- reason: Type.String({
73
- description: "Why this earlier run is suspect (e.g. reward-hacked, broken metric).",
74
- }),
39
+ const logExperimentSchema = z.object({
40
+ metric: z
41
+ .number()
42
+ .describe("Primary metric value for this run. May differ from the parsed value; deviation is recorded."),
43
+ status: z.enum(["keep", "discard", "crash", "checks_failed"] as const).describe("Outcome for this run."),
44
+ description: z.string().describe("Short description of the experiment."),
45
+ metrics: z.record(z.string(), z.number()).describe("Secondary metrics for this run.").optional(),
46
+ asi: z
47
+ .object({})
48
+ .passthrough()
49
+ .describe("Free-form structured metadata captured for this run (hypothesis, learnings, etc.).")
50
+ .optional(),
51
+ commit: z
52
+ .string()
53
+ .describe("Override the commit hash recorded for this run. Defaults to the current HEAD.")
54
+ .optional(),
55
+ justification: z
56
+ .string()
57
+ .describe(
58
+ "Required when the run modifies paths outside scope or inside off-limits and you still want it kept. Free-form explanation.",
59
+ )
60
+ .optional(),
61
+ flag_runs: z
62
+ .array(
63
+ z.object({
64
+ run_id: z.number().describe("Run id (#) of a previously logged run to flag as suspect."),
65
+ reason: z.string().describe("Why this earlier run is suspect (e.g. reward-hacked, broken metric)."),
75
66
  }),
76
- { description: "Mark earlier runs as flagged. Flagged runs are excluded from baseline and best-metric math." },
77
- ),
78
- ),
67
+ )
68
+ .describe("Mark earlier runs as flagged. Flagged runs are excluded from baseline and best-metric math.")
69
+ .optional(),
79
70
  });
80
71
 
81
72
  export function createLogExperimentTool(
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
5
  import { formatBytes } from "@oh-my-pi/pi-utils";
6
- import { Type } from "@sinclair/typebox";
6
+ import * as z from "zod/v4";
7
7
  import type { ToolDefinition } from "../../extensibility/extensions";
8
8
  import type { Theme } from "../../modes/theme/theme";
9
9
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateTail } from "../../session/streaming-output";
@@ -26,8 +26,8 @@ import { openAutoresearchStorageIfExists } from "../storage";
26
26
  import type { AutoresearchToolFactoryOptions, RunDetails, RunExperimentProgressDetails } from "../types";
27
27
  import { DEFAULT_HARNESS_COMMAND } from "./init-experiment";
28
28
 
29
- const runExperimentSchema = Type.Object({
30
- timeout_seconds: Type.Optional(Type.Number({ description: "Timeout in seconds. Defaults to 600." })),
29
+ const runExperimentSchema = z.object({
30
+ timeout_seconds: z.number().describe("Timeout in seconds. Defaults to 600.").optional(),
31
31
  });
32
32
 
33
33
  interface ProcessExecutionResult {