@oh-my-pi/pi-coding-agent 13.3.13 → 13.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 (63) hide show
  1. package/CHANGELOG.md +97 -7
  2. package/examples/sdk/README.md +22 -0
  3. package/package.json +7 -7
  4. package/src/capability/index.ts +1 -11
  5. package/src/commit/analysis/index.ts +4 -4
  6. package/src/config/settings-schema.ts +18 -15
  7. package/src/config/settings.ts +2 -20
  8. package/src/discovery/index.ts +1 -11
  9. package/src/exa/index.ts +1 -10
  10. package/src/extensibility/custom-commands/index.ts +2 -15
  11. package/src/extensibility/custom-tools/index.ts +3 -18
  12. package/src/extensibility/custom-tools/loader.ts +28 -5
  13. package/src/extensibility/custom-tools/types.ts +18 -1
  14. package/src/extensibility/extensions/index.ts +9 -130
  15. package/src/extensibility/extensions/types.ts +2 -1
  16. package/src/extensibility/hooks/index.ts +3 -14
  17. package/src/extensibility/plugins/index.ts +6 -31
  18. package/src/index.ts +28 -220
  19. package/src/internal-urls/docs-index.generated.ts +3 -2
  20. package/src/internal-urls/index.ts +11 -16
  21. package/src/mcp/index.ts +11 -37
  22. package/src/mcp/tool-bridge.ts +3 -42
  23. package/src/mcp/transports/index.ts +2 -2
  24. package/src/modes/components/extensions/index.ts +3 -3
  25. package/src/modes/components/index.ts +35 -40
  26. package/src/modes/interactive-mode.ts +4 -1
  27. package/src/modes/rpc/rpc-mode.ts +1 -7
  28. package/src/modes/theme/theme.ts +11 -10
  29. package/src/modes/types.ts +1 -1
  30. package/src/patch/index.ts +4 -20
  31. package/src/prompts/system/system-prompt.md +18 -4
  32. package/src/prompts/tools/ast-edit.md +33 -0
  33. package/src/prompts/tools/ast-grep.md +34 -0
  34. package/src/prompts/tools/bash.md +2 -2
  35. package/src/prompts/tools/hashline.md +1 -0
  36. package/src/prompts/tools/resolve.md +8 -0
  37. package/src/sdk.ts +27 -7
  38. package/src/session/agent-session.ts +25 -36
  39. package/src/session/session-manager.ts +0 -30
  40. package/src/slash-commands/builtin-registry.ts +4 -2
  41. package/src/stt/index.ts +3 -3
  42. package/src/task/types.ts +2 -2
  43. package/src/tools/ast-edit.ts +480 -0
  44. package/src/tools/ast-grep.ts +435 -0
  45. package/src/tools/bash.ts +3 -2
  46. package/src/tools/gemini-image.ts +3 -3
  47. package/src/tools/grep.ts +26 -8
  48. package/src/tools/index.ts +55 -57
  49. package/src/tools/pending-action.ts +33 -0
  50. package/src/tools/render-utils.ts +10 -0
  51. package/src/tools/renderers.ts +6 -4
  52. package/src/tools/resolve.ts +156 -0
  53. package/src/tools/submit-result.ts +1 -1
  54. package/src/web/search/index.ts +6 -4
  55. package/src/web/search/providers/anthropic.ts +2 -2
  56. package/src/web/search/providers/base.ts +3 -0
  57. package/src/web/search/providers/exa.ts +11 -5
  58. package/src/web/search/providers/gemini.ts +112 -24
  59. package/src/patch/normative.ts +0 -72
  60. package/src/prompts/tools/ast-find.md +0 -20
  61. package/src/prompts/tools/ast-replace.md +0 -21
  62. package/src/tools/ast-find.ts +0 -316
  63. package/src/tools/ast-replace.ts +0 -294
@@ -15,8 +15,8 @@ import type { AgentOutputManager } from "../task/output-manager";
15
15
  import type { EventBus } from "../utils/event-bus";
16
16
  import { SearchTool } from "../web/search";
17
17
  import { AskTool } from "./ask";
18
- import { AstFindTool } from "./ast-find";
19
- import { AstReplaceTool } from "./ast-replace";
18
+ import { AstEditTool } from "./ast-edit";
19
+ import { AstGrepTool } from "./ast-grep";
20
20
  import { AwaitTool } from "./await-tool";
21
21
  import { BashTool } from "./bash";
22
22
  import { BrowserTool } from "./browser";
@@ -30,6 +30,7 @@ import { NotebookTool } from "./notebook";
30
30
  import { wrapToolWithMetaNotice } from "./output-meta";
31
31
  import { PythonTool } from "./python";
32
32
  import { ReadTool } from "./read";
33
+ import { ResolveTool } from "./resolve";
33
34
  import { reportFindingTool } from "./review";
34
35
  import { loadSshTool } from "./ssh";
35
36
  import { SubmitResultTool } from "./submit-result";
@@ -38,50 +39,36 @@ import { WriteTool } from "./write";
38
39
 
39
40
  // Exa MCP tools (22 tools)
40
41
 
41
- export { exaTools } from "../exa";
42
- export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "../exa/types";
43
- export {
44
- type FileDiagnosticsResult,
45
- type FileFormatResult,
46
- getLspStatus,
47
- type LspServerStatus,
48
- LspTool,
49
- type LspToolDetails,
50
- type LspWarmupOptions,
51
- type LspWarmupResult,
52
- warmupLspServers,
53
- } from "../lsp";
54
- export { EditTool, type EditToolDetails } from "../patch";
42
+ export * from "../exa";
43
+ export type * from "../exa/types";
44
+ export * from "../lsp";
45
+ export * from "../patch";
55
46
  export * from "../session/streaming-output";
56
- export { BUNDLED_AGENTS, TaskTool } from "../task";
47
+ export * from "../task";
57
48
  export * from "../web/search";
58
- export { AskTool, type AskToolDetails } from "./ask";
59
- export { AstFindTool, type AstFindToolDetails } from "./ast-find";
60
- export { AstReplaceTool, type AstReplaceToolDetails } from "./ast-replace";
61
- export { AwaitTool, type AwaitToolDetails } from "./await-tool";
62
- export { BashTool, type BashToolDetails, type BashToolInput, type BashToolOptions } from "./bash";
63
- export { BrowserTool, type BrowserToolDetails } from "./browser";
64
- export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
65
- export { CancelJobTool, type CancelJobToolDetails } from "./cancel-job";
66
- export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
67
- export { FetchTool, type FetchToolDetails } from "./fetch";
68
- export { type FindOperations, FindTool, type FindToolDetails, type FindToolInput, type FindToolOptions } from "./find";
69
- export { setPreferredImageProvider } from "./gemini-image";
70
- export { GrepTool, type GrepToolDetails, type GrepToolInput } from "./grep";
71
- export { NotebookTool, type NotebookToolDetails } from "./notebook";
72
- export { PythonTool, type PythonToolDetails, type PythonToolOptions } from "./python";
73
- export { ReadTool, type ReadToolDetails, type ReadToolInput } from "./read";
74
- export { reportFindingTool, type SubmitReviewDetails } from "./review";
75
- export { loadSshTool, type SSHToolDetails, SshTool } from "./ssh";
76
- export { SubmitResultTool } from "./submit-result";
77
- export {
78
- getLatestTodoPhasesFromEntries,
79
- type TodoItem,
80
- type TodoPhase,
81
- TodoWriteTool,
82
- type TodoWriteToolDetails,
83
- } from "./todo-write";
84
- export { WriteTool, type WriteToolDetails, type WriteToolInput } from "./write";
49
+ export * from "./ask";
50
+ export * from "./ast-edit";
51
+ export * from "./ast-grep";
52
+ export * from "./await-tool";
53
+ export * from "./bash";
54
+ export * from "./browser";
55
+ export * from "./calculator";
56
+ export * from "./cancel-job";
57
+ export * from "./exit-plan-mode";
58
+ export * from "./fetch";
59
+ export * from "./find";
60
+ export * from "./gemini-image";
61
+ export * from "./grep";
62
+ export * from "./notebook";
63
+ export * from "./pending-action";
64
+ export * from "./python";
65
+ export * from "./read";
66
+ export * from "./resolve";
67
+ export * from "./review";
68
+ export * from "./ssh";
69
+ export * from "./submit-result";
70
+ export * from "./todo-write";
71
+ export * from "./write";
85
72
 
86
73
  /** Tool type (AgentTool from pi-ai) */
87
74
  export type Tool = AgentTool<any, any, any>;
@@ -154,13 +141,15 @@ export interface ToolSession {
154
141
  getTodoPhases?: () => TodoPhase[];
155
142
  /** Replace cached todo phases for this session. */
156
143
  setTodoPhases?: (phases: TodoPhase[]) => void;
144
+ /** Pending action store for preview/apply workflows */
145
+ pendingActionStore?: import("./pending-action").PendingActionStore;
157
146
  }
158
147
 
159
148
  type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
160
149
 
161
150
  export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
162
- ast_find: s => new AstFindTool(s),
163
- ast_replace: s => new AstReplaceTool(s),
151
+ ast_grep: s => new AstGrepTool(s),
152
+ ast_edit: s => new AstEditTool(s),
164
153
  ask: AskTool.createIf,
165
154
  bash: s => new BashTool(s),
166
155
  python: s => new PythonTool(s),
@@ -186,6 +175,7 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
186
175
  submit_result: s => new SubmitResultTool(s),
187
176
  report_finding: () => reportFindingTool,
188
177
  exit_plan_mode: s => new ExitPlanModeTool(s),
178
+ resolve: s => new ResolveTool(s),
189
179
  };
190
180
 
191
181
  export type ToolName = keyof typeof BUILTIN_TOOLS;
@@ -289,8 +279,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
289
279
  if (name === "todo_write") return !includeSubmitResult && session.settings.get("todo.enabled");
290
280
  if (name === "find") return session.settings.get("find.enabled");
291
281
  if (name === "grep") return session.settings.get("grep.enabled");
292
- if (name === "ast_find") return session.settings.get("astFind.enabled");
293
- if (name === "ast_replace") return session.settings.get("astReplace.enabled");
282
+ if (name === "ast_grep") return session.settings.get("astGrep.enabled");
283
+ if (name === "ast_edit") return session.settings.get("astEdit.enabled");
294
284
  if (name === "notebook") return session.settings.get("notebook.enabled");
295
285
  if (name === "fetch") return session.settings.get("fetch.enabled");
296
286
  if (name === "web_search") return session.settings.get("web_search.enabled");
@@ -309,24 +299,32 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
309
299
  }
310
300
 
311
301
  const filteredRequestedTools = requestedTools?.filter(name => name in allTools && isToolAllowed(name));
312
-
313
- const entries =
302
+ const baseEntries =
314
303
  filteredRequestedTools !== undefined
315
- ? filteredRequestedTools.map(name => [name, allTools[name]] as const)
304
+ ? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
316
305
  : [
317
306
  ...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
318
307
  ...(includeSubmitResult ? ([["submit_result", HIDDEN_TOOLS.submit_result]] as const) : []),
319
308
  ...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
320
309
  ];
321
310
 
322
- const results = await Promise.all(
323
- entries.map(async ([name, factory]) => {
324
- if (filteredRequestedTools && !filteredRequestedTools.includes(name)) {
325
- return null;
326
- }
311
+ const baseResults = await Promise.all(
312
+ baseEntries.map(async ([name, factory]) => {
327
313
  const tool = await logger.timeAsync(`createTools:${name}`, factory, session);
328
314
  return tool ? wrapToolWithMetaNotice(tool) : null;
329
315
  }),
330
316
  );
331
- return results.filter((r): r is Tool => r !== null);
317
+ const tools = baseResults.filter((r): r is Tool => r !== null);
318
+ const hasDeferrableTools = tools.some(tool => tool.deferrable === true);
319
+ if (!hasDeferrableTools) {
320
+ return tools;
321
+ }
322
+ if (tools.some(tool => tool.name === "resolve")) {
323
+ return tools;
324
+ }
325
+ const resolveTool = await logger.timeAsync("createTools:resolve", HIDDEN_TOOLS.resolve, session);
326
+ if (resolveTool) {
327
+ tools.push(wrapToolWithMetaNotice(resolveTool));
328
+ }
329
+ return tools;
332
330
  }
@@ -0,0 +1,33 @@
1
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+
3
+ export interface PendingAction {
4
+ label: string;
5
+ sourceToolName: string;
6
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
7
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
8
+ details?: unknown;
9
+ }
10
+
11
+ export class PendingActionStore {
12
+ #actions: PendingAction[] = [];
13
+
14
+ push(action: PendingAction): void {
15
+ this.#actions.push(action);
16
+ }
17
+
18
+ peek(): PendingAction | null {
19
+ return this.#actions.at(-1) ?? null;
20
+ }
21
+
22
+ pop(): PendingAction | null {
23
+ return this.#actions.pop() ?? null;
24
+ }
25
+
26
+ clear(): void {
27
+ this.#actions = [];
28
+ }
29
+
30
+ get hasPending(): boolean {
31
+ return this.#actions.length > 0;
32
+ }
33
+ }
@@ -530,3 +530,13 @@ export function shortenPath(filePath: string, homeDir?: string): string {
530
530
  export function wrapBrackets(text: string, theme: Theme): string {
531
531
  return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
532
532
  }
533
+
534
+ export const PARSE_ERRORS_LIMIT = 20;
535
+
536
+ export function formatParseErrors(errors: string[]): string[] {
537
+ if (errors.length === 0) return [];
538
+ const capped = errors.slice(0, PARSE_ERRORS_LIMIT);
539
+ const header =
540
+ errors.length > PARSE_ERRORS_LIMIT ? `Parse issues (${PARSE_ERRORS_LIMIT} / ${errors.length}):` : "Parse issues:";
541
+ return [header, ...capped.map(err => `- ${err}`)];
542
+ }
@@ -11,8 +11,8 @@ import { editToolRenderer } from "../patch";
11
11
  import { taskToolRenderer } from "../task/render";
12
12
  import { webSearchToolRenderer } from "../web/search/render";
13
13
  import { askToolRenderer } from "./ask";
14
- import { astFindToolRenderer } from "./ast-find";
15
- import { astReplaceToolRenderer } from "./ast-replace";
14
+ import { astEditToolRenderer } from "./ast-edit";
15
+ import { astGrepToolRenderer } from "./ast-grep";
16
16
  import { bashToolRenderer } from "./bash";
17
17
  import { calculatorToolRenderer } from "./calculator";
18
18
  import { fetchToolRenderer } from "./fetch";
@@ -21,6 +21,7 @@ import { grepToolRenderer } from "./grep";
21
21
  import { notebookToolRenderer } from "./notebook";
22
22
  import { pythonToolRenderer } from "./python";
23
23
  import { readToolRenderer } from "./read";
24
+ import { resolveToolRenderer } from "./resolve";
24
25
  import { sshToolRenderer } from "./ssh";
25
26
  import { todoWriteToolRenderer } from "./todo-write";
26
27
  import { writeToolRenderer } from "./write";
@@ -40,8 +41,8 @@ type ToolRenderer = {
40
41
 
41
42
  export const toolRenderers: Record<string, ToolRenderer> = {
42
43
  ask: askToolRenderer as ToolRenderer,
43
- ast_find: astFindToolRenderer as ToolRenderer,
44
- ast_replace: astReplaceToolRenderer as ToolRenderer,
44
+ ast_grep: astGrepToolRenderer as ToolRenderer,
45
+ ast_edit: astEditToolRenderer as ToolRenderer,
45
46
  bash: bashToolRenderer as ToolRenderer,
46
47
  python: pythonToolRenderer as ToolRenderer,
47
48
  calc: calculatorToolRenderer as ToolRenderer,
@@ -51,6 +52,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
51
52
  lsp: lspToolRenderer as ToolRenderer,
52
53
  notebook: notebookToolRenderer as ToolRenderer,
53
54
  read: readToolRenderer as ToolRenderer,
55
+ resolve: resolveToolRenderer as ToolRenderer,
54
56
  ssh: sshToolRenderer as ToolRenderer,
55
57
  task: taskToolRenderer as ToolRenderer,
56
58
  todo_write: todoWriteToolRenderer as ToolRenderer,
@@ -0,0 +1,156 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import type { Component } from "@oh-my-pi/pi-tui";
3
+ import { Text } from "@oh-my-pi/pi-tui";
4
+ import { untilAborted } from "@oh-my-pi/pi-utils";
5
+ import { type Static, Type } from "@sinclair/typebox";
6
+ import { renderPromptTemplate } from "../config/prompt-templates";
7
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
+ import type { Theme } from "../modes/theme/theme";
9
+ import resolveDescription from "../prompts/tools/resolve.md" with { type: "text" };
10
+ import { Ellipsis, padToWidth, renderStatusLine, truncateToWidth } from "../tui";
11
+ import type { ToolSession } from ".";
12
+ import { replaceTabs } from "./render-utils";
13
+ import { ToolError } from "./tool-errors";
14
+ import { toolResult } from "./tool-result";
15
+
16
+ const resolveSchema = Type.Object({
17
+ action: Type.Union([Type.Literal("apply"), Type.Literal("discard")]),
18
+ reason: Type.String({ description: "Why you're applying or discarding" }),
19
+ });
20
+
21
+ type ResolveParams = Static<typeof resolveSchema>;
22
+
23
+ export interface ResolveToolDetails {
24
+ action: "apply" | "discard";
25
+ reason: string;
26
+ sourceToolName?: string;
27
+ label?: string;
28
+ }
29
+
30
+ function resolveReasonPreview(reason?: string): string | undefined {
31
+ const trimmed = reason?.trim();
32
+ if (!trimmed) return undefined;
33
+ return truncateToWidth(trimmed, 72, Ellipsis.Omit);
34
+ }
35
+
36
+ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolDetails> {
37
+ readonly name = "resolve";
38
+ readonly label = "Resolve";
39
+ readonly hidden = true;
40
+ readonly description: string;
41
+ readonly parameters = resolveSchema;
42
+ readonly strict = true;
43
+
44
+ constructor(private readonly session: ToolSession) {
45
+ this.description = renderPromptTemplate(resolveDescription);
46
+ }
47
+
48
+ async execute(
49
+ _toolCallId: string,
50
+ params: ResolveParams,
51
+ signal?: AbortSignal,
52
+ _onUpdate?: AgentToolUpdateCallback<ResolveToolDetails>,
53
+ _context?: AgentToolContext,
54
+ ): Promise<AgentToolResult<ResolveToolDetails>> {
55
+ return untilAborted(signal, async () => {
56
+ const store = this.session.pendingActionStore;
57
+ if (!store || !store.hasPending) {
58
+ throw new ToolError("No pending action to resolve. Nothing to apply or discard.");
59
+ }
60
+
61
+ const pendingAction = store.pop();
62
+ if (!pendingAction) {
63
+ throw new ToolError("No pending action to resolve. Nothing to apply or discard.");
64
+ }
65
+ const resolveDetails: ResolveToolDetails = {
66
+ action: params.action,
67
+ reason: params.reason,
68
+ sourceToolName: pendingAction.sourceToolName,
69
+ label: pendingAction.label,
70
+ };
71
+
72
+ if (params.action === "apply") {
73
+ const applyResult = await pendingAction.apply(params.reason);
74
+ const appliedText = applyResult.content
75
+ .filter(part => part.type === "text")
76
+ .map(part => part.text)
77
+ .filter(text => text != null && text.length > 0)
78
+ .join("\n");
79
+ const baseResult = toolResult()
80
+ .text(appliedText || `Applied: ${pendingAction.label}.`)
81
+ .done();
82
+ return { ...baseResult, details: resolveDetails };
83
+ }
84
+
85
+ if (params.action === "discard" && pendingAction.reject != null) {
86
+ const discardResult = await pendingAction.reject(params.reason);
87
+ if (discardResult != null) {
88
+ return { ...discardResult, details: resolveDetails };
89
+ }
90
+ }
91
+ const discardResult = toolResult().text(`Discarded: ${pendingAction.label}. Reason: ${params.reason}`).done();
92
+ return { ...discardResult, details: resolveDetails };
93
+ });
94
+ }
95
+ }
96
+
97
+ export const resolveToolRenderer = {
98
+ renderCall(args: ResolveParams, _options: RenderResultOptions, uiTheme: Theme): Component {
99
+ const reason = resolveReasonPreview(args.reason);
100
+ const text = renderStatusLine(
101
+ {
102
+ icon: "pending",
103
+ title: "Resolve",
104
+ description: args.action,
105
+ badge: {
106
+ label: args.action === "apply" ? "proposed -> resolved" : "proposed -> rejected",
107
+ color: args.action === "apply" ? "success" : "warning",
108
+ },
109
+ meta: reason ? [uiTheme.fg("muted", reason)] : undefined,
110
+ },
111
+ uiTheme,
112
+ );
113
+ return new Text(text, 0, 0);
114
+ },
115
+
116
+ renderResult(
117
+ result: { content: Array<{ type: string; text?: string }>; details?: ResolveToolDetails; isError?: boolean },
118
+ _options: RenderResultOptions,
119
+ uiTheme: Theme,
120
+ ): Component {
121
+ const details = result.details;
122
+ const label = replaceTabs(details?.label ?? "pending action");
123
+ const reason = replaceTabs(details?.reason?.trim() || "No reason provided");
124
+ const action = details?.action ?? "apply";
125
+ const isApply = action === "apply" && !result.isError;
126
+ const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
127
+ const icon = isApply ? uiTheme.status.success : uiTheme.status.error;
128
+ const verb = isApply ? "Accept" : "Discard";
129
+ const separator = ": ";
130
+ const separatorIndex = label.indexOf(separator);
131
+ const sourceLabel = separatorIndex > 0 ? label.slice(0, separatorIndex).trim() : undefined;
132
+ const summaryLabel = separatorIndex > 0 ? label.slice(separatorIndex + separator.length).trim() : label;
133
+ const sourceBadge = sourceLabel
134
+ ? uiTheme.bold(`${uiTheme.format.bracketLeft}${sourceLabel}${uiTheme.format.bracketRight}`)
135
+ : undefined;
136
+ const headerLine = `${icon} ${uiTheme.bold(`${verb}:`)} ${summaryLabel}${sourceBadge ? ` ${sourceBadge}` : ""}`;
137
+ const lines = ["", headerLine, "", uiTheme.italic(reason), ""];
138
+
139
+ return {
140
+ render(width: number) {
141
+ const lineWidth = Math.max(3, width);
142
+ const innerWidth = Math.max(1, lineWidth - 2);
143
+ return lines.map(line => {
144
+ const truncated = truncateToWidth(line, innerWidth, Ellipsis.Omit);
145
+ const framed = ` ${padToWidth(truncated, innerWidth)} `;
146
+ const padded = padToWidth(framed, lineWidth);
147
+ return uiTheme.inverse(uiTheme.fg(bgColor, padded));
148
+ });
149
+ },
150
+ invalidate() {},
151
+ };
152
+ },
153
+
154
+ inline: true,
155
+ mergeCallAndResult: true,
156
+ };
@@ -4,7 +4,7 @@
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 { sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/typebox-helpers";
7
+ import { sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/schema";
8
8
  import type { Static, TSchema } from "@sinclair/typebox";
9
9
  import { Type } from "@sinclair/typebox";
10
10
  import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
@@ -46,6 +46,9 @@ export const webSearchSchema = Type.Object({
46
46
  }),
47
47
  ),
48
48
  limit: Type.Optional(Type.Number({ description: "Max results to return" })),
49
+ max_tokens: Type.Optional(Type.Number({ description: "Maximum output tokens" })),
50
+ temperature: Type.Optional(Type.Number({ description: "Sampling temperature" })),
51
+ num_search_results: Type.Optional(Type.Number({ description: "Number of search results to retrieve" })),
49
52
  });
50
53
 
51
54
  export type SearchParams = {
@@ -70,7 +73,7 @@ export type SearchParams = {
70
73
  temperature?: number;
71
74
  /** Number of search results to retrieve. Defaults to 10. */
72
75
  num_search_results?: number;
73
- /** Disable provider fallback when explicit provider is selected (CLI/debug use). */
76
+ /** Deprecated CLI flag; explicit provider fallback now happens only when provider is unavailable. */
74
77
  no_fallback?: boolean;
75
78
  };
76
79
 
@@ -185,12 +188,11 @@ async function executeSearch(
185
188
  params: SearchParams,
186
189
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
187
190
  const providers =
188
- params.provider && params.provider !== "auto" && params.no_fallback
191
+ params.provider && params.provider !== "auto"
189
192
  ? (await getSearchProvider(params.provider).isAvailable())
190
193
  ? [getSearchProvider(params.provider)]
191
- : []
194
+ : await resolveProviderChain("auto")
192
195
  : await resolveProviderChain(params.provider);
193
-
194
196
  if (providers.length === 0) {
195
197
  const message = "No web search provider configured.";
196
198
  return {
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  type AnthropicAuthConfig,
9
9
  type AnthropicSystemBlock,
10
- buildAnthropicHeaders,
10
+ buildAnthropicSearchHeaders,
11
11
  buildAnthropicSystemBlocks,
12
12
  buildAnthropicUrl,
13
13
  findAnthropicAuth,
@@ -87,7 +87,7 @@ async function callSearch(
87
87
  temperature?: number,
88
88
  ): Promise<AnthropicApiResponse> {
89
89
  const url = buildAnthropicUrl(auth);
90
- const headers = buildAnthropicHeaders(auth);
90
+ const headers = buildAnthropicSearchHeaders(auth);
91
91
 
92
92
  const systemBlocks = buildSystemBlocks(auth, model, systemPrompt);
93
93
 
@@ -10,6 +10,9 @@ export interface SearchParams {
10
10
  maxOutputTokens?: number;
11
11
  numSearchResults?: number;
12
12
  temperature?: number;
13
+ googleSearch?: Record<string, unknown>;
14
+ codeExecution?: Record<string, unknown>;
15
+ urlContext?: Record<string, unknown>;
13
16
  }
14
17
 
15
18
  /** Base class for web search providers. */
@@ -7,6 +7,7 @@
7
7
  * them into a combined `answer` string on the SearchResponse.
8
8
  */
9
9
  import { getEnvApiKey } from "@oh-my-pi/pi-ai";
10
+ import { callExaTool, findApiKey, isSearchResponse } from "../../../exa/mcp-client";
10
11
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
11
12
  import { SearchProviderError } from "../../../web/search/types";
12
13
  import { dateToAgeSeconds } from "../utils";
@@ -121,14 +122,19 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
121
122
  return response.json() as Promise<ExaSearchResponse>;
122
123
  }
123
124
 
125
+ async function callExaMcpSearch(params: ExaSearchParams): Promise<ExaSearchResponse> {
126
+ const response = await callExaTool("web_search_exa", { ...params }, findApiKey());
127
+ if (!isSearchResponse(response)) {
128
+ throw new Error("Exa MCP search returned unexpected response shape.");
129
+ }
130
+
131
+ return response as ExaSearchResponse;
132
+ }
133
+
124
134
  /** Execute Exa web search */
125
135
  export async function searchExa(params: ExaSearchParams): Promise<SearchResponse> {
126
136
  const apiKey = getEnvApiKey("exa");
127
- if (!apiKey) {
128
- throw new Error("EXA_API_KEY not found. Set it in environment or .env file.");
129
- }
130
-
131
- const response = await callExaSearch(apiKey, params);
137
+ const response = apiKey ? await callExaSearch(apiKey, params) : await callExaMcpSearch(params);
132
138
 
133
139
  // Convert to unified SearchResponse
134
140
  const sources: SearchSource[] = [];