@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.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 (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +34 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +62 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +49 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +228 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +237 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +1 -1
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +73 -44
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +24 -0
  83. package/src/modes/interactive/components/tool-execution.ts +34 -6
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -4,22 +4,26 @@
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
6
  import * as path from "node:path";
7
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
7
9
  import type { KeyId } from "@oh-my-pi/pi-tui";
10
+ import type { TSchema } from "@sinclair/typebox";
8
11
  import * as TypeBox from "@sinclair/typebox";
9
12
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
10
13
  import { loadCapability } from "../../discovery";
11
14
  import { expandPath, getExtensionNameFromPath } from "../../discovery/helpers";
12
15
  import * as piCodingAgent from "../../index";
13
- import { createEventBus, type EventBus } from "../event-bus";
16
+ import { EventBus } from "../event-bus";
14
17
  import type { ExecOptions } from "../exec";
15
18
  import { execCommand } from "../exec";
16
19
  import { logger } from "../logger";
20
+ import type { CustomMessage } from "../messages";
17
21
  import type {
18
22
  Extension,
19
23
  ExtensionAPI,
20
24
  ExtensionContext,
21
25
  ExtensionFactory,
22
- ExtensionRuntime,
26
+ ExtensionRuntime as IExtensionRuntime,
23
27
  LoadExtensionsResult,
24
28
  MessageRenderer,
25
29
  RegisteredCommand,
@@ -36,147 +40,183 @@ function resolvePath(extPath: string, cwd: string): string {
36
40
 
37
41
  type HandlerFn = (...args: unknown[]) => Promise<unknown>;
38
42
 
43
+ export class ExtensionRuntimeNotInitializedError extends Error {
44
+ constructor() {
45
+ super("Extension runtime not initialized. Action methods cannot be called during extension loading.");
46
+ }
47
+ }
48
+
39
49
  /**
40
- * Create a runtime with throwing stubs for action methods.
41
- * Runner.initialize() replaces these with real implementations.
50
+ * Extension runtime with throwing stubs for action methods.
51
+ * These are replaced with real implementations during initialization.
42
52
  */
43
- export function createExtensionRuntime(): ExtensionRuntime {
44
- const notInitialized = () => {
45
- throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
46
- };
53
+ export class ExtensionRuntime implements IExtensionRuntime {
54
+ flagValues = new Map<string, boolean | string>();
47
55
 
48
- return {
49
- sendMessage: notInitialized,
50
- sendUserMessage: notInitialized,
51
- appendEntry: notInitialized,
52
- setLabel: notInitialized,
53
- getActiveTools: notInitialized,
54
- getAllTools: notInitialized,
55
- setActiveTools: notInitialized,
56
- setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
57
- getThinkingLevel: notInitialized,
58
- setThinkingLevel: notInitialized,
59
- flagValues: new Map(),
60
- };
56
+ sendMessage(): void {
57
+ throw new ExtensionRuntimeNotInitializedError();
58
+ }
59
+
60
+ sendUserMessage(): void {
61
+ throw new ExtensionRuntimeNotInitializedError();
62
+ }
63
+
64
+ appendEntry(): void {
65
+ throw new ExtensionRuntimeNotInitializedError();
66
+ }
67
+
68
+ setLabel(): void {
69
+ throw new ExtensionRuntimeNotInitializedError();
70
+ }
71
+
72
+ getActiveTools(): string[] {
73
+ throw new ExtensionRuntimeNotInitializedError();
74
+ }
75
+
76
+ getAllTools(): string[] {
77
+ throw new ExtensionRuntimeNotInitializedError();
78
+ }
79
+
80
+ setActiveTools(): Promise<void> {
81
+ throw new ExtensionRuntimeNotInitializedError();
82
+ }
83
+
84
+ setModel(): Promise<boolean> {
85
+ throw new ExtensionRuntimeNotInitializedError();
86
+ }
87
+
88
+ getThinkingLevel(): ThinkingLevel {
89
+ throw new ExtensionRuntimeNotInitializedError();
90
+ }
91
+
92
+ setThinkingLevel(): void {
93
+ throw new ExtensionRuntimeNotInitializedError();
94
+ }
61
95
  }
62
96
 
63
97
  /**
64
- * Create the ExtensionAPI for an extension.
98
+ * ExtensionAPI implementation for an extension.
65
99
  * Registration methods write to the extension object.
66
100
  * Action methods delegate to the shared runtime.
67
101
  */
68
- function createExtensionAPI(
69
- extension: Extension,
70
- runtime: ExtensionRuntime,
71
- cwd: string,
72
- eventBus: EventBus,
73
- ): ExtensionAPI {
74
- const api = {
75
- logger,
76
- typebox: TypeBox,
77
- pi: piCodingAgent,
78
-
79
- on(event: string, handler: HandlerFn): void {
80
- const list = extension.handlers.get(event) ?? [];
81
- list.push(handler);
82
- extension.handlers.set(event, list);
83
- },
84
-
85
- registerTool(tool: ToolDefinition): void {
86
- extension.tools.set(tool.name, {
87
- definition: tool,
88
- extensionPath: extension.path,
89
- });
90
- },
102
+ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
103
+ readonly logger = logger;
104
+ readonly typebox = TypeBox;
105
+ readonly pi = piCodingAgent;
106
+ readonly events: EventBus;
107
+ readonly flagValues = new Map<string, boolean | string>();
108
+
109
+ constructor(
110
+ private extension: Extension,
111
+ private runtime: IExtensionRuntime,
112
+ private cwd: string,
113
+ eventBus: EventBus,
114
+ ) {
115
+ this.events = eventBus;
116
+ }
91
117
 
92
- registerCommand(
93
- name: string,
94
- options: {
95
- description?: string;
96
- getArgumentCompletions?: RegisteredCommand["getArgumentCompletions"];
97
- handler: RegisteredCommand["handler"];
98
- },
99
- ): void {
100
- extension.commands.set(name, { name, ...options });
101
- },
118
+ on<F extends HandlerFn>(event: string, handler: F): void {
119
+ const list = this.extension.handlers.get(event) ?? [];
120
+ list.push(handler);
121
+ this.extension.handlers.set(event, list);
122
+ }
102
123
 
103
- setLabel(label: string): void {
104
- extension.label = label;
105
- },
124
+ registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void {
125
+ this.extension.tools.set(tool.name, {
126
+ definition: tool,
127
+ extensionPath: this.extension.path,
128
+ });
129
+ }
106
130
 
107
- registerShortcut(
108
- shortcut: KeyId,
109
- options: {
110
- description?: string;
111
- handler: (ctx: ExtensionContext) => Promise<void> | void;
112
- },
113
- ): void {
114
- extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
131
+ registerCommand(
132
+ name: string,
133
+ options: {
134
+ description?: string;
135
+ getArgumentCompletions?: RegisteredCommand["getArgumentCompletions"];
136
+ handler: RegisteredCommand["handler"];
115
137
  },
138
+ ): void {
139
+ this.extension.commands.set(name, { name, ...options });
140
+ }
116
141
 
117
- registerFlag(
118
- name: string,
119
- options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
120
- ): void {
121
- extension.flags.set(name, { name, extensionPath: extension.path, ...options });
122
- if (options.default !== undefined) {
123
- runtime.flagValues.set(name, options.default);
124
- }
125
- },
142
+ setLabel(label: string): void {
143
+ this.extension.label = label;
144
+ }
126
145
 
127
- registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
128
- extension.messageRenderers.set(customType, renderer as MessageRenderer);
146
+ registerShortcut(
147
+ shortcut: KeyId,
148
+ options: {
149
+ description?: string;
150
+ handler: (ctx: ExtensionContext) => Promise<void> | void;
129
151
  },
152
+ ): void {
153
+ this.extension.shortcuts.set(shortcut, { shortcut, extensionPath: this.extension.path, ...options });
154
+ }
130
155
 
131
- getFlag(name: string): boolean | string | undefined {
132
- if (!extension.flags.has(name)) return undefined;
133
- return runtime.flagValues.get(name);
134
- },
156
+ registerFlag(
157
+ name: string,
158
+ options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
159
+ ): void {
160
+ this.extension.flags.set(name, { name, extensionPath: this.extension.path, ...options });
161
+ if (options.default !== undefined) {
162
+ this.runtime.flagValues.set(name, options.default);
163
+ }
164
+ }
135
165
 
136
- sendMessage(message, options): void {
137
- runtime.sendMessage(message, options);
138
- },
166
+ registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
167
+ this.extension.messageRenderers.set(customType, renderer as MessageRenderer);
168
+ }
139
169
 
140
- sendUserMessage(content, options): void {
141
- runtime.sendUserMessage(content, options);
142
- },
170
+ getFlag(name: string): boolean | string | undefined {
171
+ if (!this.extension.flags.has(name)) return undefined;
172
+ return this.runtime.flagValues.get(name);
173
+ }
143
174
 
144
- appendEntry(customType: string, data?: unknown): void {
145
- runtime.appendEntry(customType, data);
146
- },
175
+ sendMessage<T = unknown>(
176
+ message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
177
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
178
+ ): void {
179
+ this.runtime.sendMessage(message, options);
180
+ }
147
181
 
148
- exec(command: string, args: string[], options?: ExecOptions) {
149
- return execCommand(command, args, options?.cwd ?? cwd, options);
150
- },
182
+ sendUserMessage(
183
+ content: string | (TextContent | ImageContent)[],
184
+ options?: { deliverAs?: "steer" | "followUp" },
185
+ ): void {
186
+ this.runtime.sendUserMessage(content, options);
187
+ }
151
188
 
152
- getActiveTools(): string[] {
153
- return runtime.getActiveTools();
154
- },
189
+ appendEntry(customType: string, data?: unknown): void {
190
+ this.runtime.appendEntry(customType, data);
191
+ }
155
192
 
156
- getAllTools(): string[] {
157
- return runtime.getAllTools();
158
- },
193
+ exec(command: string, args: string[], options?: ExecOptions) {
194
+ return execCommand(command, args, options?.cwd ?? this.cwd, options);
195
+ }
159
196
 
160
- setActiveTools(toolNames: string[]): void {
161
- runtime.setActiveTools(toolNames);
162
- },
197
+ getActiveTools(): string[] {
198
+ return this.runtime.getActiveTools();
199
+ }
163
200
 
164
- setModel(model) {
165
- return runtime.setModel(model);
166
- },
201
+ getAllTools(): string[] {
202
+ return this.runtime.getAllTools();
203
+ }
167
204
 
168
- getThinkingLevel() {
169
- return runtime.getThinkingLevel();
170
- },
205
+ setActiveTools(toolNames: string[]): Promise<void> {
206
+ return this.runtime.setActiveTools(toolNames);
207
+ }
171
208
 
172
- setThinkingLevel(level) {
173
- runtime.setThinkingLevel(level);
174
- },
209
+ setModel(model: Model<any>): Promise<boolean> {
210
+ return this.runtime.setModel(model);
211
+ }
175
212
 
176
- events: eventBus,
177
- } as ExtensionAPI;
213
+ getThinkingLevel(): ThinkingLevel {
214
+ return this.runtime.getThinkingLevel();
215
+ }
178
216
 
179
- return api;
217
+ setThinkingLevel(level: ThinkingLevel): void {
218
+ this.runtime.setThinkingLevel(level);
219
+ }
180
220
  }
181
221
 
182
222
  /**
@@ -199,7 +239,7 @@ async function loadExtension(
199
239
  extensionPath: string,
200
240
  cwd: string,
201
241
  eventBus: EventBus,
202
- runtime: ExtensionRuntime,
242
+ runtime: IExtensionRuntime,
203
243
  ): Promise<{ extension: Extension | null; error: string | null }> {
204
244
  const resolvedPath = resolvePath(extensionPath, cwd);
205
245
 
@@ -215,7 +255,7 @@ async function loadExtension(
215
255
  }
216
256
 
217
257
  const extension = createExtension(extensionPath, resolvedPath);
218
- const api = createExtensionAPI(extension, runtime, cwd, eventBus);
258
+ const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
219
259
  await factory(api);
220
260
 
221
261
  return { extension, error: null };
@@ -232,11 +272,11 @@ export async function loadExtensionFromFactory(
232
272
  factory: ExtensionFactory,
233
273
  cwd: string,
234
274
  eventBus: EventBus,
235
- runtime: ExtensionRuntime,
275
+ runtime: IExtensionRuntime,
236
276
  name = "<inline>",
237
277
  ): Promise<Extension> {
238
278
  const extension = createExtension(name, name);
239
- const api = createExtensionAPI(extension, runtime, cwd, eventBus);
279
+ const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
240
280
  await factory(api);
241
281
  return extension;
242
282
  }
@@ -247,8 +287,8 @@ export async function loadExtensionFromFactory(
247
287
  export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
248
288
  const extensions: Extension[] = [];
249
289
  const errors: Array<{ path: string; error: string }> = [];
250
- const resolvedEventBus = eventBus ?? createEventBus();
251
- const runtime = createExtensionRuntime();
290
+ const resolvedEventBus = eventBus ?? new EventBus();
291
+ const runtime = new ExtensionRuntime();
252
292
 
253
293
  for (const extPath of paths) {
254
294
  const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
@@ -30,7 +30,7 @@ import type {
30
30
  } from "../session-manager";
31
31
  import type { BashToolDetails, FindToolDetails, GrepToolDetails, LsToolDetails, ReadToolDetails } from "../tools";
32
32
  import type { BashOperations } from "../tools/bash";
33
- import type { EditToolDetails } from "../tools/edit";
33
+ import type { EditToolDetails } from "../tools/patch";
34
34
 
35
35
  export type { ExecOptions, ExecResult } from "../exec";
36
36
  export type { AgentToolResult, AgentToolUpdateCallback };
@@ -779,8 +779,8 @@ export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
779
779
  // Loaded Extension Types
780
780
  // ============================================================================
781
781
 
782
- export interface RegisteredTool {
783
- definition: ToolDefinition;
782
+ export interface RegisteredTool<TParams extends TSchema = TSchema, TDetails = unknown> {
783
+ definition: ToolDefinition<TParams, TDetails>;
784
784
  extensionPath: string;
785
785
  }
786
786
 
@@ -877,7 +877,7 @@ export interface Extension {
877
877
  resolvedPath: string;
878
878
  label?: string;
879
879
  handlers: Map<string, HandlerFn[]>;
880
- tools: Map<string, RegisteredTool>;
880
+ tools: Map<string, RegisteredTool<any, any>>;
881
881
  messageRenderers: Map<string, MessageRenderer>;
882
882
  commands: Map<string, RegisteredCommand>;
883
883
  flags: Map<string, ExtensionFlag>;
@@ -9,27 +9,53 @@ import type { ExtensionRunner } from "./runner";
9
9
  import type { RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
10
10
 
11
11
  /**
12
- * Wrap a RegisteredTool into an AgentTool.
12
+ * Adapts a RegisteredTool into an AgentTool.
13
+ */
14
+ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
15
+ readonly name: string;
16
+ readonly label: string;
17
+ readonly description: string;
18
+ readonly parameters: any;
19
+
20
+ constructor(
21
+ private registeredTool: RegisteredTool,
22
+ private runner: ExtensionRunner,
23
+ ) {
24
+ const { definition } = registeredTool;
25
+ this.name = definition.name;
26
+ this.label = definition.label || "";
27
+ this.description = definition.description;
28
+ this.parameters = definition.parameters;
29
+ }
30
+
31
+ async execute(
32
+ toolCallId: string,
33
+ params: any,
34
+ signal?: AbortSignal,
35
+ onUpdate?: AgentToolUpdateCallback<any>,
36
+ _context?: AgentToolContext,
37
+ ) {
38
+ return this.registeredTool.definition.execute(toolCallId, params, onUpdate, this.runner.createContext(), signal);
39
+ }
40
+
41
+ renderCall?(args: any, theme: any) {
42
+ return this.registeredTool.definition.renderCall?.(args, theme as Theme);
43
+ }
44
+
45
+ renderResult?(result: any, options: any, theme: any) {
46
+ return this.registeredTool.definition.renderResult?.(
47
+ result,
48
+ { expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
49
+ theme as Theme,
50
+ );
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Backward-compatible factory function wrapper.
13
56
  */
14
57
  export function wrapRegisteredTool(registeredTool: RegisteredTool, runner: ExtensionRunner): AgentTool {
15
- const { definition } = registeredTool;
16
- return {
17
- name: definition.name,
18
- label: definition.label,
19
- description: definition.description,
20
- parameters: definition.parameters,
21
- execute: (toolCallId, params, signal, onUpdate) =>
22
- definition.execute(toolCallId, params, onUpdate, runner.createContext(), signal),
23
- renderCall: definition.renderCall ? (args, theme) => definition.renderCall?.(args, theme as Theme) : undefined,
24
- renderResult: definition.renderResult
25
- ? (result, options, theme) =>
26
- definition.renderResult?.(
27
- result,
28
- { expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
29
- theme as Theme,
30
- )
31
- : undefined,
32
- };
58
+ return new RegisteredToolAdapter(registeredTool, runner);
33
59
  }
34
60
 
35
61
  /**
@@ -40,98 +66,121 @@ export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: E
40
66
  }
41
67
 
42
68
  /**
43
- * Wrap a tool with extension callbacks for interception.
69
+ * Wraps a tool with extension callbacks for interception.
44
70
  * - Emits tool_call event before execution (can block)
45
71
  * - Emits tool_result event after execution (can modify result)
46
72
  */
47
- export function wrapToolWithExtensions<T>(tool: AgentTool<any, T>, runner: ExtensionRunner): AgentTool<any, T> {
48
- return {
49
- ...tool,
50
- execute: async (
51
- toolCallId: string,
52
- params: Record<string, unknown>,
53
- signal?: AbortSignal,
54
- onUpdate?: AgentToolUpdateCallback<T>,
55
- context?: AgentToolContext,
56
- ) => {
57
- // Emit tool_call event - extensions can block execution
58
- if (runner.hasHandlers("tool_call")) {
59
- try {
60
- const callResult = (await runner.emitToolCall({
61
- type: "tool_call",
62
- toolName: tool.name,
63
- toolCallId,
64
- input: params,
65
- })) as ToolCallEventResult | undefined;
66
-
67
- if (callResult?.block) {
68
- const reason = callResult.reason || "Tool execution was blocked by an extension";
69
- throw new Error(reason);
70
- }
71
- } catch (err) {
72
- if (err instanceof Error) {
73
- throw err;
74
- }
75
- throw new Error(`Extension failed, blocking execution: ${String(err)}`);
76
- }
77
- }
73
+ export class ExtensionToolWrapper<T> implements AgentTool<any, T> {
74
+ name: string;
75
+ label: string;
76
+ description: string;
77
+ parameters: unknown;
78
+ renderCall?: AgentTool["renderCall"];
79
+ renderResult?: AgentTool["renderResult"];
78
80
 
79
- // Execute the actual tool
80
- let result: { content: any; details: T };
81
- let executionError: Error | undefined;
81
+ constructor(
82
+ private tool: AgentTool<any, T>,
83
+ private runner: ExtensionRunner,
84
+ ) {
85
+ this.name = tool.name;
86
+ this.label = tool.label ?? "";
87
+ this.description = tool.description;
88
+ this.parameters = tool.parameters;
89
+ this.renderCall = tool.renderCall;
90
+ this.renderResult = tool.renderResult;
91
+ }
82
92
 
93
+ async execute(
94
+ toolCallId: string,
95
+ params: Record<string, unknown>,
96
+ signal?: AbortSignal,
97
+ onUpdate?: AgentToolUpdateCallback<T>,
98
+ context?: AgentToolContext,
99
+ ) {
100
+ // Emit tool_call event - extensions can block execution
101
+ if (this.runner.hasHandlers("tool_call")) {
83
102
  try {
84
- result = await tool.execute(toolCallId, params, signal, onUpdate, context);
103
+ const callResult = (await this.runner.emitToolCall({
104
+ type: "tool_call",
105
+ toolName: this.tool.name,
106
+ toolCallId,
107
+ input: params,
108
+ })) as ToolCallEventResult | undefined;
109
+
110
+ if (callResult?.block) {
111
+ const reason = callResult.reason || "Tool execution was blocked by an extension";
112
+ throw new Error(reason);
113
+ }
85
114
  } catch (err) {
86
- executionError = err instanceof Error ? err : new Error(String(err));
87
- result = {
88
- content: [{ type: "text", text: executionError.message }],
89
- details: undefined as T,
90
- };
115
+ if (err instanceof Error) {
116
+ throw err;
117
+ }
118
+ throw new Error(`Extension failed, blocking execution: ${String(err)}`);
91
119
  }
120
+ }
92
121
 
93
- // Emit tool_result event - extensions can modify the result and error status
94
- if (runner.hasHandlers("tool_result")) {
95
- const resultResult = (await runner.emit({
96
- type: "tool_result",
97
- toolName: tool.name,
98
- toolCallId,
99
- input: params,
100
- content: result.content,
101
- details: result.details,
102
- isError: !!executionError,
103
- })) as ToolResultEventResult | undefined;
104
-
105
- if (resultResult) {
106
- const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
107
- const modifiedDetails = (resultResult.details ?? result.details) as T;
108
-
109
- // Extension can override error status
110
- if (resultResult.isError === true && !executionError) {
111
- // Extension marks a successful result as error
112
- const textBlocks = (modifiedContent ?? []).filter((c): c is TextContent => c.type === "text");
113
- const errorText =
114
- textBlocks.map((t) => t.text).join("\n") || "Tool result marked as error by extension";
115
- throw new Error(errorText);
116
- }
117
- if (resultResult.isError === false && executionError) {
118
- // Extension clears the error - return success
119
- return { content: modifiedContent, details: modifiedDetails };
120
- }
121
-
122
- // Error status unchanged, but content/details may be modified
123
- if (executionError) {
124
- throw executionError;
125
- }
122
+ // Execute the actual tool
123
+ let result: { content: any; details?: T };
124
+ let executionError: Error | undefined;
125
+
126
+ try {
127
+ result = await this.tool.execute(toolCallId, params, signal, onUpdate, context);
128
+ } catch (err) {
129
+ executionError = err instanceof Error ? err : new Error(String(err));
130
+ result = {
131
+ content: [{ type: "text", text: executionError.message }],
132
+ details: undefined as T,
133
+ };
134
+ }
135
+
136
+ // Emit tool_result event - extensions can modify the result and error status
137
+ if (this.runner.hasHandlers("tool_result")) {
138
+ const resultResult = (await this.runner.emit({
139
+ type: "tool_result",
140
+ toolName: this.tool.name,
141
+ toolCallId,
142
+ input: params,
143
+ content: result.content,
144
+ details: result.details,
145
+ isError: !!executionError,
146
+ })) as ToolResultEventResult | undefined;
147
+
148
+ if (resultResult) {
149
+ const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
150
+ const modifiedDetails = (resultResult.details ?? result.details) as T;
151
+
152
+ // Extension can override error status
153
+ if (resultResult.isError === true && !executionError) {
154
+ // Extension marks a successful result as error
155
+ const textBlocks = (modifiedContent ?? []).filter((c): c is TextContent => c.type === "text");
156
+ const errorText = textBlocks.map((t) => t.text).join("\n") || "Tool result marked as error by extension";
157
+ throw new Error(errorText);
158
+ }
159
+ if (resultResult.isError === false && executionError) {
160
+ // Extension clears the error - return success
126
161
  return { content: modifiedContent, details: modifiedDetails };
127
162
  }
128
- }
129
163
 
130
- // No extension modification
131
- if (executionError) {
132
- throw executionError;
164
+ // Error status unchanged, but content/details may be modified
165
+ if (executionError) {
166
+ throw executionError;
167
+ }
168
+ return { content: modifiedContent, details: modifiedDetails };
133
169
  }
134
- return result;
135
- },
136
- };
170
+ }
171
+
172
+ // No extension modification
173
+ if (executionError) {
174
+ throw executionError;
175
+ }
176
+ return result;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Wrap a tool with extension callbacks for interception.
182
+ * @deprecated Use `new ExtensionToolWrapper()` directly
183
+ */
184
+ export function wrapToolWithExtensions<T>(tool: AgentTool<any, T>, runner: ExtensionRunner): AgentTool<any, T> {
185
+ return new ExtensionToolWrapper(tool, runner);
137
186
  }