- Add input_image and input_file support with SSRF protection - Add client-side tools (Hosted Tools) support - Add turn-based tool flow with function_call_output handling - Export buildAgentPrompt for testing
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import type {
|
|
AgentTool,
|
|
AgentToolResult,
|
|
AgentToolUpdateCallback,
|
|
} from "@mariozechner/pi-agent-core";
|
|
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js";
|
|
import { logDebug, logError } from "../logger.js";
|
|
import { normalizeToolName } from "./tool-policy.js";
|
|
import { jsonResult } from "./tools/common.js";
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
|
|
type AnyAgentTool = AgentTool<any, unknown>;
|
|
|
|
function describeToolExecutionError(err: unknown): {
|
|
message: string;
|
|
stack?: string;
|
|
} {
|
|
if (err instanceof Error) {
|
|
const message = err.message?.trim() ? err.message : String(err);
|
|
return { message, stack: err.stack };
|
|
}
|
|
return { message: String(err) };
|
|
}
|
|
|
|
export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
|
|
return tools.map((tool) => {
|
|
const name = tool.name || "tool";
|
|
const normalizedName = normalizeToolName(name);
|
|
return {
|
|
name,
|
|
label: tool.label ?? name,
|
|
description: tool.description ?? "",
|
|
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema from pi-agent-core uses a different module instance.
|
|
parameters: tool.parameters as any,
|
|
execute: async (
|
|
toolCallId,
|
|
params,
|
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
_ctx,
|
|
signal,
|
|
): Promise<AgentToolResult<unknown>> => {
|
|
// KNOWN: pi-coding-agent `ToolDefinition.execute` has a different signature/order
|
|
// than pi-agent-core `AgentTool.execute`. This adapter keeps our existing tools intact.
|
|
try {
|
|
return await tool.execute(toolCallId, params, signal, onUpdate);
|
|
} catch (err) {
|
|
if (signal?.aborted) throw err;
|
|
const name =
|
|
err && typeof err === "object" && "name" in err
|
|
? String((err as { name?: unknown }).name)
|
|
: "";
|
|
if (name === "AbortError") throw err;
|
|
const described = describeToolExecutionError(err);
|
|
if (described.stack && described.stack !== described.message) {
|
|
logDebug(`tools: ${normalizedName} failed stack:\n${described.stack}`);
|
|
}
|
|
logError(`[tools] ${normalizedName} failed: ${described.message}`);
|
|
return jsonResult({
|
|
status: "error",
|
|
tool: normalizedName,
|
|
error: described.message,
|
|
});
|
|
}
|
|
},
|
|
} satisfies ToolDefinition;
|
|
});
|
|
}
|
|
|
|
// Convert client tools (OpenResponses hosted tools) to ToolDefinition format
|
|
// These tools are intercepted to return a "pending" result instead of executing
|
|
export function toClientToolDefinitions(
|
|
tools: ClientToolDefinition[],
|
|
onClientToolCall?: (toolName: string, params: Record<string, unknown>) => void,
|
|
): ToolDefinition[] {
|
|
return tools.map((tool) => {
|
|
const func = tool.function;
|
|
return {
|
|
name: func.name,
|
|
label: func.name,
|
|
description: func.description ?? "",
|
|
parameters: func.parameters as any,
|
|
execute: async (
|
|
toolCallId,
|
|
params,
|
|
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
_ctx,
|
|
_signal,
|
|
): Promise<AgentToolResult<unknown>> => {
|
|
// Notify handler that a client tool was called
|
|
if (onClientToolCall) {
|
|
onClientToolCall(func.name, params as Record<string, unknown>);
|
|
}
|
|
// Return a pending result - the client will execute this tool
|
|
return jsonResult({
|
|
status: "pending",
|
|
tool: func.name,
|
|
message: "Tool execution delegated to client",
|
|
});
|
|
},
|
|
} satisfies ToolDefinition;
|
|
});
|
|
}
|