* Tool wrappers for extensions.
*/
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
import type { Settings } from "../../config/settings";
import type { Theme } from "../../modes/theme/theme";
import { type ApprovalMode, formatApprovalPrompt, requiresApproval } from "../../tools/approval";
import { applyToolProxy } from "../tool-proxy";
import type { ExtensionRunner } from "./runner";
import type { RegisteredTool, ToolCallEventResult } from "./types";
* Adapts a RegisteredTool into an AgentTool.
*/
export class RegisteredToolAdapter implements AgentTool<any, any, any> {
declare name: string;
declare description: string;
declare parameters: any;
declare label: string;
declare strict: boolean;
renderCall?: (args: any, options: any, theme: any) => any;
renderResult?: (result: any, options: any, theme: any, args?: any) => any;
constructor(
private registeredTool: RegisteredTool,
private runner: ExtensionRunner,
) {
applyToolProxy(registeredTool.definition, this);
if (registeredTool.definition.renderCall) {
this.renderCall = (args: any, options: any, theme: any) =>
registeredTool.definition.renderCall!(args, options, theme as Theme);
}
if (registeredTool.definition.renderResult) {
this.renderResult = (result: any, options: any, theme: any, args?: any) =>
registeredTool.definition.renderResult!(
result,
{ expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
theme as Theme,
args,
);
}
}
async execute(
toolCallId: string,
params: any,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<any>,
_context?: AgentToolContext,
) {
return this.registeredTool.definition.execute(toolCallId, params, signal, onUpdate, this.runner.createContext());
}
}
* Backward-compatible factory function wrapper.
*/
export function wrapRegisteredTool(registeredTool: RegisteredTool, runner: ExtensionRunner): AgentTool {
return new RegisteredToolAdapter(registeredTool, runner);
}
* Wrap all registered tools into AgentTools.
*/
export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: ExtensionRunner): AgentTool[] {
return registeredTools.map(rt => wrapRegisteredTool(rt, runner));
}
* Wraps a tool with extension callbacks for interception.
* - Emits tool_call event before execution (can block)
* - Emits tool_result event after execution (can modify result)
*/
export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetails = unknown>
implements AgentTool<TParameters, TDetails>
{
declare name: string;
declare description: string;
declare parameters: TParameters;
declare label: string;
declare strict: boolean;
constructor(
private tool: AgentTool<TParameters, TDetails>,
private runner: ExtensionRunner,
) {
applyToolProxy(tool, this);
}
* Forward browser mode changes when available.
*/
restartForModeChange(): Promise<void> {
const target = this.tool as { restartForModeChange?: () => Promise<void> };
if (!target.restartForModeChange) return Promise.resolve();
return target.restartForModeChange();
}
async execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
context?: AgentToolContext,
) {
const cliAutoApprove = context?.autoApprove === true;
const settings: Settings | undefined = context?.settings;
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
const approvalCheck = requiresApproval(this.tool, params, approvalMode, userPolicies);
if (approvalCheck.required) {
if (!this.runner.hasUI()) {
throw new Error(
`Tool "${this.tool.name}" requires approval but no interactive UI available.\n` +
`Options:\n` +
` 1. Set tools.approvalMode: yolo in /settings\n` +
` 2. Add tools.approval.${this.tool.name}: allow to config\n` +
` 3. Use an interactive UI to approve the tool call`,
);
}
const uiContext = this.runner.getUIContext();
const choice = await uiContext.select(formatApprovalPrompt(this.tool, params, approvalCheck.reason), [
"Approve",
"Deny",
]);
if (choice !== "Approve") {
throw new Error(`Tool call denied by user: ${this.tool.name}`);
}
}
if (this.runner.hasHandlers("tool_call")) {
try {
const callResult = (await this.runner.emitToolCall({
type: "tool_call",
toolName: this.tool.name,
toolCallId,
input: params as Record<string, unknown>,
})) as ToolCallEventResult | undefined;
if (callResult?.block) {
const reason = callResult.reason || "Tool execution was blocked by an extension";
throw new Error(reason);
}
} catch (err) {
if (err instanceof Error) {
throw err;
}
throw new Error(`Extension failed, blocking execution: ${String(err)}`);
}
}
let result: { content: any; details?: TDetails };
let executionError: Error | undefined;
try {
result = await this.tool.execute(toolCallId, params, signal, onUpdate, context);
} catch (err) {
executionError = err instanceof Error ? err : new Error(String(err));
result = {
content: [{ type: "text", text: executionError.message }],
details: undefined as TDetails,
};
}
if (this.runner.hasHandlers("tool_result")) {
const resultResult = await this.runner.emitToolResult({
type: "tool_result",
toolName: this.tool.name,
toolCallId,
input: params as Record<string, unknown>,
content: result.content,
details: result.details,
isError: !!executionError,
});
if (resultResult) {
const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
const modifiedDetails = (resultResult.details ?? result.details) as TDetails;
if (resultResult.isError === true && !executionError) {
const textBlocks = (modifiedContent ?? []).filter((c): c is TextContent => c.type === "text");
const errorText = textBlocks.map(t => t.text).join("\n") || "Tool result marked as error by extension";
throw new Error(errorText);
}
if (resultResult.isError === false && executionError) {
return { content: modifiedContent, details: modifiedDetails };
}
if (executionError) {
throw executionError;
}
return { content: modifiedContent, details: modifiedDetails };
}
}
if (executionError) {
throw executionError;
}
return result;
}
}