@oh-my-pi/pi-coding-agent 15.0.1 → 15.1.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 (168) hide show
  1. package/CHANGELOG.md +94 -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 +8 -18
  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/commands/commit.ts +10 -0
  17. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  18. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  19. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  20. package/src/commit/agentic/tools/git-overview.ts +4 -4
  21. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  22. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  23. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  24. package/src/commit/agentic/tools/schemas.ts +28 -28
  25. package/src/commit/agentic/tools/split-commit.ts +22 -21
  26. package/src/commit/analysis/summary.ts +4 -4
  27. package/src/commit/changelog/generate.ts +7 -11
  28. package/src/commit/shared-llm.ts +22 -34
  29. package/src/config/config-file.ts +35 -13
  30. package/src/config/model-registry.ts +40 -191
  31. package/src/config/models-config-schema.ts +166 -0
  32. package/src/config/settings-schema.ts +29 -0
  33. package/src/discovery/claude-plugins.ts +19 -7
  34. package/src/edit/index.ts +2 -2
  35. package/src/edit/modes/apply-patch.ts +7 -6
  36. package/src/edit/modes/patch.ts +18 -25
  37. package/src/edit/modes/replace.ts +18 -20
  38. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  39. package/src/eval/py/executor.ts +233 -623
  40. package/src/eval/py/kernel.ts +27 -2
  41. package/src/eval/py/runner.py +42 -11
  42. package/src/eval/py/runtime.ts +1 -0
  43. package/src/exa/factory.ts +5 -4
  44. package/src/exa/mcp-client.ts +1 -1
  45. package/src/exa/researcher.ts +9 -20
  46. package/src/exa/search.ts +26 -52
  47. package/src/exa/types.ts +1 -1
  48. package/src/exa/websets.ts +54 -53
  49. package/src/exec/bash-executor.ts +2 -1
  50. package/src/extensibility/custom-commands/loader.ts +5 -3
  51. package/src/extensibility/custom-commands/types.ts +4 -2
  52. package/src/extensibility/custom-tools/loader.ts +5 -3
  53. package/src/extensibility/custom-tools/types.ts +7 -6
  54. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  55. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  56. package/src/extensibility/extensions/loader.ts +7 -3
  57. package/src/extensibility/extensions/types.ts +9 -5
  58. package/src/extensibility/extensions/wrapper.ts +1 -2
  59. package/src/extensibility/hooks/loader.ts +3 -1
  60. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  61. package/src/extensibility/hooks/types.ts +4 -2
  62. package/src/extensibility/plugins/legacy-pi-compat.ts +78 -31
  63. package/src/extensibility/shared-events.ts +1 -1
  64. package/src/extensibility/typebox.ts +391 -0
  65. package/src/goals/tools/goal-tool.ts +6 -12
  66. package/src/hashline/input.ts +2 -1
  67. package/src/hashline/parser.ts +27 -3
  68. package/src/hashline/types.ts +4 -4
  69. package/src/hindsight/state.ts +2 -2
  70. package/src/index.ts +0 -2
  71. package/src/internal-urls/docs-index.generated.ts +15 -15
  72. package/src/internal-urls/router.ts +8 -0
  73. package/src/internal-urls/types.ts +21 -0
  74. package/src/lsp/config.ts +15 -6
  75. package/src/lsp/defaults.json +6 -2
  76. package/src/lsp/types.ts +30 -38
  77. package/src/mcp/manager.ts +1 -1
  78. package/src/mcp/tool-bridge.ts +1 -1
  79. package/src/modes/acp/acp-agent.ts +248 -50
  80. package/src/modes/components/session-observer-overlay.ts +12 -1
  81. package/src/modes/components/status-line/segments.ts +39 -4
  82. package/src/modes/controllers/command-controller.ts +27 -2
  83. package/src/modes/controllers/event-controller.ts +3 -4
  84. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  85. package/src/modes/interactive-mode.ts +1 -1
  86. package/src/modes/rpc/host-tools.ts +1 -1
  87. package/src/modes/rpc/host-uris.ts +235 -0
  88. package/src/modes/rpc/rpc-client.ts +1 -1
  89. package/src/modes/rpc/rpc-mode.ts +27 -1
  90. package/src/modes/rpc/rpc-types.ts +58 -1
  91. package/src/modes/runtime-init.ts +2 -1
  92. package/src/modes/theme/defaults/dark-poimandres.json +1 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +1 -0
  94. package/src/modes/theme/theme.ts +117 -117
  95. package/src/modes/types.ts +1 -1
  96. package/src/modes/utils/context-usage.ts +2 -2
  97. package/src/prompts/tools/github.md +4 -4
  98. package/src/prompts/tools/hashline.md +22 -26
  99. package/src/prompts/tools/read.md +55 -37
  100. package/src/sdk.ts +31 -8
  101. package/src/session/agent-session.ts +74 -104
  102. package/src/session/messages.ts +16 -51
  103. package/src/session/session-manager.ts +22 -2
  104. package/src/session/streaming-output.ts +16 -6
  105. package/src/task/discovery.ts +5 -2
  106. package/src/task/executor.ts +210 -87
  107. package/src/task/index.ts +15 -11
  108. package/src/task/render.ts +32 -5
  109. package/src/task/types.ts +54 -39
  110. package/src/tools/ask.ts +12 -12
  111. package/src/tools/ast-edit.ts +11 -15
  112. package/src/tools/ast-grep.ts +9 -10
  113. package/src/tools/bash-command-fixup.ts +47 -0
  114. package/src/tools/bash.ts +48 -38
  115. package/src/tools/browser/render.ts +2 -2
  116. package/src/tools/browser.ts +39 -53
  117. package/src/tools/calculator.ts +12 -11
  118. package/src/tools/checkpoint.ts +7 -7
  119. package/src/tools/debug.ts +40 -43
  120. package/src/tools/eval.ts +16 -10
  121. package/src/tools/find.ts +10 -13
  122. package/src/tools/gh.ts +108 -132
  123. package/src/tools/hindsight-recall.ts +4 -6
  124. package/src/tools/hindsight-reflect.ts +5 -5
  125. package/src/tools/hindsight-retain.ts +15 -17
  126. package/src/tools/image-gen.ts +31 -81
  127. package/src/tools/index.ts +4 -1
  128. package/src/tools/inspect-image.ts +8 -9
  129. package/src/tools/irc.ts +15 -27
  130. package/src/tools/job.ts +30 -28
  131. package/src/tools/output-meta.ts +26 -0
  132. package/src/tools/read.ts +39 -12
  133. package/src/tools/recipe/index.ts +7 -9
  134. package/src/tools/render-mermaid.ts +12 -12
  135. package/src/tools/report-tool-issue.ts +4 -4
  136. package/src/tools/resolve.ts +11 -11
  137. package/src/tools/review.ts +14 -26
  138. package/src/tools/search-tool-bm25.ts +7 -9
  139. package/src/tools/search.ts +19 -22
  140. package/src/tools/ssh.ts +10 -9
  141. package/src/tools/todo-write.ts +26 -34
  142. package/src/tools/vim.ts +10 -26
  143. package/src/tools/write.ts +25 -5
  144. package/src/tools/yield.ts +100 -54
  145. package/src/web/search/index.ts +9 -24
  146. package/src/web/search/providers/anthropic.ts +5 -0
  147. package/src/web/search/providers/exa.ts +3 -0
  148. package/src/web/search/providers/gemini.ts +5 -0
  149. package/src/web/search/providers/jina.ts +5 -2
  150. package/src/web/search/providers/zai.ts +5 -2
  151. package/src/prompts/compaction/branch-summary-context.md +0 -5
  152. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  153. package/src/prompts/compaction/branch-summary.md +0 -30
  154. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  155. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  156. package/src/prompts/compaction/compaction-summary.md +0 -38
  157. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  158. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  159. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  160. package/src/prompts/system/file-operations.md +0 -10
  161. package/src/prompts/system/handoff-document.md +0 -49
  162. package/src/prompts/system/summarization-system.md +0 -3
  163. package/src/session/compaction/branch-summarization.ts +0 -324
  164. package/src/session/compaction/compaction.ts +0 -1420
  165. package/src/session/compaction/errors.ts +0 -31
  166. package/src/session/compaction/index.ts +0 -8
  167. package/src/session/compaction/pruning.ts +0 -91
  168. package/src/session/compaction/utils.ts +0 -184
@@ -1,10 +1,9 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { StringEnum } from "@oh-my-pi/pi-ai";
3
2
  import type { Component } from "@oh-my-pi/pi-tui";
4
3
  import { Text } from "@oh-my-pi/pi-tui";
5
4
  import { prompt } from "@oh-my-pi/pi-utils";
6
- import { type Static, Type } from "@sinclair/typebox";
7
5
  import chalk from "chalk";
6
+ import * as z from "zod/v4";
8
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
8
  import type { Theme } from "../modes/theme/theme";
10
9
  import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
@@ -45,45 +44,38 @@ export interface TodoWriteToolDetails {
45
44
  // Schema
46
45
  // =============================================================================
47
46
 
48
- const TodoOp = StringEnum(["init", "start", "done", "rm", "drop", "append", "note"] as const, {
49
- description: "operation to apply",
50
- });
47
+ const TodoOp = z
48
+ .enum(["init", "start", "done", "rm", "drop", "append", "note"] as const)
49
+ .describe("operation to apply");
51
50
 
52
- const InitListEntry = Type.Object({
53
- phase: Type.String({ description: "phase name (short noun phrase)", examples: ["Foundation", "Auth"] }),
54
- items: Type.Array(Type.String({ description: "task content (5-10 words)" }), {
55
- minItems: 1,
56
- description: "tasks for this phase, in execution order; all start as pending",
57
- }),
51
+ const InitListEntry = z.object({
52
+ phase: z.string().describe("phase name (short noun phrase)"),
53
+ items: z
54
+ .array(z.string().describe("task content (5-10 words)"))
55
+ .min(1)
56
+ .describe("tasks for this phase, in execution order; all start as pending"),
58
57
  });
59
58
 
60
- const TodoOpEntry = Type.Object({
59
+ const TodoOpEntry = z.object({
61
60
  op: TodoOp,
62
- list: Type.Optional(Type.Array(InitListEntry, { description: "phased task list for op=init" })),
63
- task: Type.Optional(
64
- Type.String({ description: "task content for start/done/rm/drop/note", examples: ["Run tests"] }),
65
- ),
66
- phase: Type.Optional(Type.String({ description: "phase name for done/rm/drop/append", examples: ["Auth"] })),
67
- items: Type.Optional(
68
- Type.Array(Type.String({ description: "task content (5-10 words)" }), {
69
- minItems: 1,
70
- description: "tasks to append to `phase` for op=append",
71
- }),
72
- ),
73
- text: Type.Optional(Type.String({ description: "note text for op=note (appended with newline)" })),
61
+ list: z.array(InitListEntry).optional().describe("phased task list for op=init"),
62
+ task: z.string().optional().describe("task content for start/done/rm/drop/note"),
63
+ phase: z.string().optional().describe("phase name for done/rm/drop/append"),
64
+ items: z
65
+ .array(z.string().describe("task content (5-10 words)"))
66
+ .min(1)
67
+ .optional()
68
+ .describe("tasks to append to `phase` for op=append"),
69
+ text: z.string().optional().describe("note text for op=note (appended with newline)"),
74
70
  });
75
71
 
76
- const todoWriteSchema = Type.Object(
77
- {
78
- ops: Type.Array(TodoOpEntry, {
79
- minItems: 1,
80
- description: "ordered todo operations",
81
- }),
82
- },
83
- { description: "Apply ordered todo operations" },
84
- );
72
+ const todoWriteSchema = z
73
+ .object({
74
+ ops: z.array(TodoOpEntry).min(1).describe("ordered todo operations"),
75
+ })
76
+ .describe("Apply ordered todo operations");
85
77
 
86
- type TodoWriteParams = Static<typeof todoWriteSchema>;
78
+ type TodoWriteParams = z.infer<typeof todoWriteSchema>;
87
79
  type TodoOpEntryValue = TodoWriteParams["ops"][number];
88
80
 
89
81
  // =============================================================================
package/src/tools/vim.ts CHANGED
@@ -2,8 +2,8 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { extractSegments, sliceWithWidth, Text } from "@oh-my-pi/pi-tui";
4
4
  import { isEnoent, logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
- import { type Static, Type } from "@sinclair/typebox";
6
5
  import * as Diff from "diff";
6
+ import * as z from "zod/v4";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
9
9
  import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
@@ -36,35 +36,19 @@ import { toolResult } from "./tool-result";
36
36
  const INTERNAL_URL_PREFIX = /^(agent|artifact|skill|rule|local|mcp):\/\//;
37
37
  const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
38
38
 
39
- const vimStepSchema = Type.Object({
40
- kbd: Type.Array(Type.String(), {
41
- description: "vim key sequences",
42
- examples: [["ggdGi"], ["3Go"], ["dd"]],
43
- }),
44
- insert: Type.Optional(
45
- Type.String({
46
- description: "raw text to insert",
47
- examples: ["hello world"],
48
- }),
49
- ),
39
+ const vimStepSchema = z.object({
40
+ kbd: z.array(z.string()).describe("vim key sequences"),
41
+ insert: z.string().optional().describe("raw text to insert"),
50
42
  });
51
43
 
52
- const vimSchema = Type.Object({
53
- file: Type.String({ description: "file path", examples: ["src/foo.ts"] }),
54
- steps: Type.Optional(
55
- Type.Array(vimStepSchema, {
56
- description: "editing steps",
57
- }),
58
- ),
59
- pause: Type.Optional(
60
- Type.Boolean({
61
- description: "skip auto-save",
62
- }),
63
- ),
44
+ const vimSchema = z.object({
45
+ file: z.string().describe("file path"),
46
+ steps: z.array(vimStepSchema).optional().describe("editing steps"),
47
+ pause: z.boolean().optional().describe("skip auto-save"),
64
48
  });
65
49
 
66
- type VimParams = Static<typeof vimSchema>;
67
- type VimStep = Static<typeof vimStepSchema>;
50
+ type VimParams = z.infer<typeof vimSchema>;
51
+ type VimStep = z.infer<typeof vimStepSchema>;
68
52
 
69
53
  interface VimRenderStep {
70
54
  kbd?: string[];
@@ -5,9 +5,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
7
7
  import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
8
- import { type Static, Type } from "@sinclair/typebox";
8
+ import * as z from "zod/v4";
9
9
  import { stripHashlinePrefixes } from "../edit";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
+ import { InternalUrlRouter } from "../internal-urls";
12
+ import { parseInternalUrl } from "../internal-urls/parse";
11
13
  import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
12
14
  import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
13
15
  import writeDescription from "../prompts/tools/write.md" with { type: "text" };
@@ -56,12 +58,12 @@ async function loadFflate(): Promise<typeof import("fflate")> {
56
58
  return fflateModulePromise;
57
59
  }
58
60
 
59
- const writeSchema = Type.Object({
60
- path: Type.String({ description: "file path", examples: ["src/new.ts"] }),
61
- content: Type.String({ description: "file content" }),
61
+ const writeSchema = z.object({
62
+ path: z.string().describe("file path"),
63
+ content: z.string().describe("file content"),
62
64
  });
63
65
 
64
- export type WriteToolInput = Static<typeof writeSchema>;
66
+ export type WriteToolInput = z.infer<typeof writeSchema>;
65
67
 
66
68
  /** Details returned by the write tool for TUI rendering */
67
69
  export interface WriteToolDetails {
@@ -658,6 +660,24 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
658
660
  return untilAborted(signal, async () => {
659
661
  // Strip hashline display prefixes (LINE+ID|) if the model copied them from read output
660
662
  const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
663
+ const internalRouter = InternalUrlRouter.instance();
664
+ if (internalRouter.canHandle(path)) {
665
+ const parsed = parseInternalUrl(path);
666
+ const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
667
+ const handler = internalRouter.getHandler(scheme);
668
+ if (handler?.write) {
669
+ await handler.write(parsed, cleanContent, { cwd: this.session.cwd, signal });
670
+ let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
671
+ if (stripped) {
672
+ resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
673
+ }
674
+ return { content: [{ type: "text", text: resultText }], details: {} };
675
+ }
676
+ // Schemes without a `write` hook fall through to existing logic
677
+ // (local:// resolves to a backing file via plan-mode-guard) or are
678
+ // rejected downstream when no backing file exists.
679
+ }
680
+
661
681
  const conflictUri = parseConflictUri(path);
662
682
  if (conflictUri) {
663
683
  if (conflictUri.scope) {
@@ -4,10 +4,15 @@
4
4
  * Subagents must call this tool to finish and return structured JSON output.
5
5
  */
6
6
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
- import { dereferenceJsonSchema, sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/schema";
8
- import type { Static, TSchema } from "@sinclair/typebox";
9
- import { Type } from "@sinclair/typebox";
10
- import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
7
+ import type { TSchema } from "@oh-my-pi/pi-ai/types";
8
+ import {
9
+ dereferenceJsonSchema,
10
+ isValidJsonSchema,
11
+ type JsonSchemaValidationIssue,
12
+ type JsonSchemaValidationResult,
13
+ sanitizeSchemaForStrictMode,
14
+ validateJsonSchemaValue,
15
+ } from "@oh-my-pi/pi-ai/utils/schema";
11
16
  import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
12
17
  import type { ToolSession } from ".";
13
18
  import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
@@ -18,8 +23,6 @@ export interface YieldDetails {
18
23
  error?: string;
19
24
  }
20
25
 
21
- const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
22
-
23
26
  function formatSchema(schema: unknown): string {
24
27
  if (schema === undefined) return "No schema provided.";
25
28
  if (typeof schema === "string") return schema;
@@ -30,16 +33,72 @@ function formatSchema(schema: unknown): string {
30
33
  }
31
34
  }
32
35
 
33
- function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
34
- if (!errors || errors.length === 0) return "Unknown schema validation error.";
35
- return errors
36
- .map(err => {
37
- const path = err.instancePath ? `${err.instancePath}: ` : "";
38
- return `${path}${err.message ?? "invalid"}`;
36
+ function formatJsonSchemaIssues(issues: ReadonlyArray<JsonSchemaValidationIssue> | undefined): string {
37
+ if (!issues || issues.length === 0) return "Unknown schema validation error.";
38
+ return issues
39
+ .map(issue => {
40
+ const path = issue.path.length === 0 ? "" : `${issue.path.map(seg => String(seg)).join("/")}: `;
41
+ return `${path}${issue.message}`;
39
42
  })
40
43
  .join("; ");
41
44
  }
42
45
 
46
+ function looseRecordSchema(description: string): Record<string, unknown> {
47
+ return {
48
+ type: "object",
49
+ additionalProperties: true,
50
+ description,
51
+ };
52
+ }
53
+
54
+ function hasUnresolvedRefs(schema: unknown): boolean {
55
+ if (schema == null) return false;
56
+ if (Array.isArray(schema)) {
57
+ for (const item of schema) {
58
+ if (hasUnresolvedRefs(item)) return true;
59
+ }
60
+ return false;
61
+ }
62
+ if (typeof schema !== "object") return false;
63
+ const record = schema as Record<string, unknown>;
64
+ if (typeof record.$ref === "string") return true;
65
+ for (const key in record) {
66
+ if (key === "const" || key === "default" || key === "enum" || key === "examples") continue;
67
+ if (hasUnresolvedRefs(record[key])) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function wrapYieldParameters(dataSchema: Record<string, unknown>): Record<string, unknown> {
73
+ return {
74
+ type: "object",
75
+ additionalProperties: false,
76
+ description: "submit data or error",
77
+ properties: {
78
+ result: {
79
+ anyOf: [
80
+ {
81
+ type: "object",
82
+ additionalProperties: false,
83
+ description: "task succeeded",
84
+ properties: { data: dataSchema },
85
+ required: ["data"],
86
+ },
87
+ {
88
+ type: "object",
89
+ additionalProperties: false,
90
+ properties: {
91
+ error: { type: "string", description: "error message" },
92
+ },
93
+ required: ["error"],
94
+ },
95
+ ],
96
+ },
97
+ },
98
+ required: ["result"],
99
+ };
100
+ }
101
+
43
102
  export class YieldTool implements AgentTool<TSchema, YieldDetails> {
44
103
  readonly name = "yield";
45
104
  readonly label = "Submit Result";
@@ -52,33 +111,15 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
52
111
  readonly intent = "omit" as const;
53
112
  lenientArgValidation = true;
54
113
 
55
- readonly #validate?: ValidateFunction;
114
+ readonly #validate?: (value: unknown) => JsonSchemaValidationResult;
56
115
  #schemaValidationFailures = 0;
57
116
 
58
117
  constructor(session: ToolSession) {
59
- const createParameters = (dataSchema: TSchema): TSchema =>
60
- Type.Object(
61
- {
62
- result: Type.Union([
63
- Type.Object({ data: dataSchema }, { description: "task succeeded" }),
64
- Type.Object({
65
- error: Type.String({ description: "error message" }),
66
- }),
67
- ]),
68
- },
69
- {
70
- additionalProperties: false,
71
- description: "submit data or error",
72
- },
73
- ) as TSchema;
74
-
75
- let validate: ValidateFunction | undefined;
76
- let dataSchema: TSchema;
118
+ let validate: ((value: unknown) => JsonSchemaValidationResult) | undefined;
77
119
  let parameters: TSchema;
78
120
 
79
121
  try {
80
122
  const schemaResult = normalizeSchema(session.outputSchema);
81
- // Convert JTD to JSON Schema if needed (auto-detected)
82
123
  const normalizedSchema =
83
124
  schemaResult.normalized !== undefined ? jtdToJsonSchema(schemaResult.normalized) : undefined;
84
125
  let schemaError = schemaResult.error;
@@ -88,10 +129,10 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
88
129
  }
89
130
 
90
131
  if (normalizedSchema !== undefined && normalizedSchema !== false && !schemaError) {
91
- try {
92
- validate = ajv.compile(normalizedSchema as Record<string, unknown> | boolean);
93
- } catch (err) {
94
- schemaError = err instanceof Error ? err.message : String(err);
132
+ if (!isValidJsonSchema(normalizedSchema)) {
133
+ schemaError = "invalid JSON schema";
134
+ } else {
135
+ validate = value => validateJsonSchemaValue(normalizedSchema, value);
95
136
  }
96
137
  }
97
138
 
@@ -109,27 +150,29 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
109
150
  ? {}
110
151
  : undefined;
111
152
 
153
+ let dataSchema: Record<string, unknown>;
112
154
  if (sanitizedSchema !== undefined) {
113
155
  const resolved = dereferenceJsonSchema({
114
156
  ...sanitizedSchema,
115
157
  description: schemaDescription,
116
- });
117
- dataSchema = Type.Unsafe(resolved as Record<string, unknown>);
158
+ }) as Record<string, unknown>;
159
+ if (hasUnresolvedRefs(resolved)) {
160
+ throw new Error("schema contains unresolved $ref after dereferencing");
161
+ }
162
+ dataSchema = resolved;
118
163
  } else {
119
- dataSchema = Type.Record(Type.String(), Type.Any(), {
120
- description: schemaError ? schemaDescription : "Structured JSON output (no schema specified)",
121
- });
164
+ dataSchema = looseRecordSchema(
165
+ schemaError ? schemaDescription : "Structured JSON output (no schema specified)",
166
+ );
122
167
  }
123
- parameters = createParameters(dataSchema);
168
+ parameters = wrapYieldParameters(dataSchema);
124
169
  JSON.stringify(parameters);
125
- // Verify the final parameters compile with AJV (catches unresolved $ref, etc.)
126
- ajv.compile(parameters as Record<string, unknown>);
170
+ if (!isValidJsonSchema(parameters)) throw new Error("yield parameters schema is invalid");
127
171
  } catch (err) {
128
172
  const errorMsg = err instanceof Error ? err.message : String(err);
129
- dataSchema = Type.Record(Type.String(), Type.Any(), {
130
- description: `Structured JSON output (schema processing failed: ${errorMsg})`,
131
- });
132
- parameters = createParameters(dataSchema);
173
+ parameters = wrapYieldParameters(
174
+ looseRecordSchema(`Structured JSON output (schema processing failed: ${errorMsg})`),
175
+ );
133
176
  validate = undefined;
134
177
  this.strict = false;
135
178
  }
@@ -140,7 +183,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
140
183
 
141
184
  async execute(
142
185
  _toolCallId: string,
143
- params: Static<TSchema>,
186
+ params: unknown,
144
187
  _signal?: AbortSignal,
145
188
  _onUpdate?: AgentToolUpdateCallback<YieldDetails>,
146
189
  _context?: AgentToolContext,
@@ -170,12 +213,15 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
170
213
  if (data === undefined || data === null) {
171
214
  throw new Error("data is required when yield indicates success");
172
215
  }
173
- if (this.#validate && !this.#validate(data)) {
174
- this.#schemaValidationFailures++;
175
- if (this.#schemaValidationFailures <= 1) {
176
- throw new Error(`Output does not match schema: ${formatAjvErrors(this.#validate.errors)}`);
216
+ if (this.#validate) {
217
+ const parsed = this.#validate(data);
218
+ if (!parsed.success) {
219
+ this.#schemaValidationFailures++;
220
+ if (this.#schemaValidationFailures <= 1) {
221
+ throw new Error(`Output does not match schema: ${formatJsonSchemaIssues(parsed.issues)}`);
222
+ }
223
+ schemaValidationOverridden = true;
177
224
  }
178
- schemaValidationOverridden = true;
179
225
  }
180
226
  }
181
227
 
@@ -6,9 +6,8 @@
6
6
  *
7
7
  */
8
8
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
9
- import { StringEnum } from "@oh-my-pi/pi-ai";
10
9
  import { prompt } from "@oh-my-pi/pi-utils";
11
- import { Type } from "@sinclair/typebox";
10
+ import * as z from "zod/v4";
12
11
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
13
12
  import type { Theme } from "../../modes/theme/theme";
14
13
  import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
@@ -21,30 +20,16 @@ import type { SearchProviderId, SearchResponse } from "./types";
21
20
  import { SearchProviderError } from "./types";
22
21
 
23
22
  /** Web search tool parameters schema */
24
- export const webSearchSchema = Type.Object({
25
- query: Type.String({ description: "Search query" }),
26
- recency: Type.Optional(
27
- StringEnum(["day", "week", "month", "year"], {
28
- description: "Recency filter (Brave, Perplexity)",
29
- }),
30
- ),
31
- limit: Type.Optional(Type.Number({ description: "Max results to return" })),
32
- max_tokens: Type.Optional(Type.Number({ description: "Maximum output tokens" })),
33
- temperature: Type.Optional(Type.Number({ description: "Sampling temperature" })),
34
- num_search_results: Type.Optional(Type.Number({ description: "Number of search results to retrieve" })),
23
+ export const webSearchSchema = z.object({
24
+ query: z.string().describe("Search query"),
25
+ recency: z.enum(["day", "week", "month", "year"]).describe("Recency filter (Brave, Perplexity)").optional(),
26
+ limit: z.number().describe("Max results to return").optional(),
27
+ max_tokens: z.number().describe("Maximum output tokens").optional(),
28
+ temperature: z.number().describe("Sampling temperature").optional(),
29
+ num_search_results: z.number().describe("Number of search results to retrieve").optional(),
35
30
  });
36
31
 
37
- export type SearchToolParams = {
38
- query: string;
39
- recency?: "day" | "week" | "month" | "year";
40
- limit?: number;
41
- /** Maximum output tokens. Defaults to 4096. */
42
- max_tokens?: number;
43
- /** Sampling temperature (0–1). Lower = more focused/factual. Defaults to 0.2. */
44
- temperature?: number;
45
- /** Number of search results to retrieve. Defaults to 10. */
46
- num_search_results?: number;
47
- };
32
+ export type SearchToolParams = z.infer<typeof webSearchSchema>;
48
33
 
49
34
  export interface SearchQueryParams extends SearchToolParams {
50
35
  provider?: SearchProviderId | "auto";
@@ -38,6 +38,7 @@ export interface AnthropicSearchParams {
38
38
  max_tokens?: number;
39
39
  /** Sampling temperature (0–1). Lower = more focused/factual. */
40
40
  temperature?: number;
41
+ signal?: AbortSignal;
41
42
  }
42
43
 
43
44
  /**
@@ -86,6 +87,7 @@ async function callSearch(
86
87
  systemPrompt?: string,
87
88
  maxTokens?: number,
88
89
  temperature?: number,
90
+ signal?: AbortSignal,
89
91
  ): Promise<AnthropicApiResponse> {
90
92
  const url = buildAnthropicUrl(auth);
91
93
  const headers = buildAnthropicSearchHeaders(auth);
@@ -116,6 +118,7 @@ async function callSearch(
116
118
  method: "POST",
117
119
  headers,
118
120
  body: JSON.stringify(body),
121
+ signal,
119
122
  });
120
123
 
121
124
  if (!response.ok) {
@@ -253,6 +256,7 @@ export async function searchAnthropic(params: AnthropicSearchParams): Promise<Se
253
256
  params.system_prompt,
254
257
  params.max_tokens,
255
258
  params.temperature,
259
+ params.signal,
256
260
  );
257
261
 
258
262
  const result = parseResponse(response);
@@ -281,6 +285,7 @@ export class AnthropicProvider extends SearchProvider {
281
285
  num_results: params.numSearchResults ?? params.limit,
282
286
  max_tokens: params.maxOutputTokens,
283
287
  temperature: params.temperature,
288
+ signal: params.signal,
284
289
  });
285
290
  }
286
291
  }
@@ -29,6 +29,7 @@ export interface ExaSearchParams {
29
29
  exclude_domains?: string[];
30
30
  start_published_date?: string;
31
31
  end_published_date?: string;
32
+ signal?: AbortSignal;
32
33
  }
33
34
 
34
35
  interface ExaSearchResult {
@@ -179,6 +180,7 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
179
180
  "x-api-key": apiKey,
180
181
  },
181
182
  body: JSON.stringify(body),
183
+ signal: params.signal,
182
184
  });
183
185
 
184
186
  if (!response.ok) {
@@ -259,6 +261,7 @@ export class ExaProvider extends SearchProvider {
259
261
  return searchExa({
260
262
  query: params.query,
261
263
  num_results: params.numSearchResults ?? params.limit,
264
+ signal: params.signal,
262
265
  });
263
266
  }
264
267
  }
@@ -39,6 +39,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
39
39
  max_output_tokens?: number;
40
40
  /** Sampling temperature (0–1). Lower = more focused/factual. */
41
41
  temperature?: number;
42
+ signal?: AbortSignal;
42
43
  }
43
44
 
44
45
  export function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<string, Record<string, unknown>>> {
@@ -235,6 +236,7 @@ async function callGeminiSearch(
235
236
  maxOutputTokens?: number,
236
237
  temperature?: number,
237
238
  toolParams: GeminiToolParams = {},
239
+ signal?: AbortSignal,
238
240
  ): Promise<{
239
241
  answer: string;
240
242
  sources: SearchSource[];
@@ -308,6 +310,7 @@ async function callGeminiSearch(
308
310
  ...headers,
309
311
  },
310
312
  body: JSON.stringify(requestBody),
313
+ signal,
311
314
  });
312
315
  const urlFor = (attempt: number) =>
313
316
  `${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
@@ -500,6 +503,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
500
503
  code_execution: params.code_execution,
501
504
  url_context: params.url_context,
502
505
  },
506
+ params.signal,
503
507
  );
504
508
 
505
509
  let sources = result.sources;
@@ -539,6 +543,7 @@ export class GeminiProvider extends SearchProvider {
539
543
  google_search: params.googleSearch,
540
544
  code_execution: params.codeExecution,
541
545
  url_context: params.urlContext,
546
+ signal: params.signal,
542
547
  });
543
548
  }
544
549
  }
@@ -17,6 +17,7 @@ const JINA_SEARCH_URL = "https://s.jina.ai";
17
17
  export interface JinaSearchParams {
18
18
  query: string;
19
19
  num_results?: number;
20
+ signal?: AbortSignal;
20
21
  }
21
22
 
22
23
  interface JinaSearchResult {
@@ -33,13 +34,14 @@ export function findApiKey(): string | null {
33
34
  }
34
35
 
35
36
  /** Call Jina Reader search API. */
36
- async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearchResponse> {
37
+ async function callJinaSearch(apiKey: string, query: string, signal?: AbortSignal): Promise<JinaSearchResponse> {
37
38
  const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}`;
38
39
  const response = await fetch(requestUrl, {
39
40
  headers: {
40
41
  Accept: "application/json",
41
42
  Authorization: `Bearer ${apiKey}`,
42
43
  },
44
+ signal,
43
45
  });
44
46
 
45
47
  if (!response.ok) {
@@ -58,7 +60,7 @@ export async function searchJina(params: JinaSearchParams): Promise<SearchRespon
58
60
  throw new Error("JINA_API_KEY not found. Set it in environment or .env file.");
59
61
  }
60
62
 
61
- const response = await callJinaSearch(apiKey, params.query);
63
+ const response = await callJinaSearch(apiKey, params.query, params.signal);
62
64
  const sources: SearchSource[] = [];
63
65
 
64
66
  for (const result of response) {
@@ -91,6 +93,7 @@ export class JinaProvider extends SearchProvider {
91
93
  return searchJina({
92
94
  query: params.query,
93
95
  num_results: params.numSearchResults ?? params.limit,
96
+ signal: params.signal,
94
97
  });
95
98
  }
96
99
  }
@@ -20,6 +20,7 @@ const DEFAULT_NUM_RESULTS = 10;
20
20
  export interface ZaiSearchParams {
21
21
  query: string;
22
22
  num_results?: number;
23
+ signal?: AbortSignal;
23
24
  }
24
25
 
25
26
  interface ZaiSearchResult {
@@ -55,7 +56,7 @@ export async function findApiKey(): Promise<string | null> {
55
56
  return findCredential(getEnvApiKey("zai"), "zai");
56
57
  }
57
58
 
58
- async function callZaiTool(apiKey: string, args: Record<string, unknown>): Promise<unknown> {
59
+ async function callZaiTool(apiKey: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
59
60
  const response = await fetch(ZAI_MCP_URL, {
60
61
  method: "POST",
61
62
  headers: {
@@ -72,6 +73,7 @@ async function callZaiTool(apiKey: string, args: Record<string, unknown>): Promi
72
73
  arguments: args,
73
74
  },
74
75
  }),
76
+ signal,
75
77
  });
76
78
 
77
79
  if (!response.ok) {
@@ -157,7 +159,7 @@ async function callZaiSearch(apiKey: string, params: ZaiSearchParams): Promise<u
157
159
  let lastError: unknown;
158
160
  for (let i = 0; i < attempts.length; i++) {
159
161
  try {
160
- return await callZaiTool(apiKey, attempts[i]);
162
+ return await callZaiTool(apiKey, attempts[i], params.signal);
161
163
  } catch (error) {
162
164
  lastError = error;
163
165
  const isLastAttempt = i === attempts.length - 1;
@@ -302,6 +304,7 @@ export class ZaiProvider extends SearchProvider {
302
304
  return searchZai({
303
305
  query: params.query,
304
306
  num_results: params.numSearchResults ?? params.limit,
307
+ signal: params.signal,
305
308
  });
306
309
  }
307
310
  }
@@ -1,5 +0,0 @@
1
- The following is a summary of a branch that this conversation came back from:
2
-
3
- <summary>
4
- {{summary}}
5
- </summary>
@@ -1,2 +0,0 @@
1
- The user explored a different conversation branch before returning here.
2
- Summary of that exploration: