import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; import { extractToolResultMediaArtifact, filterToolResultMediaUrls, isMessagingTool, isMessagingToolSendAction, type AnyAgentTool, type MessagingToolSend, } from "openclaw/plugin-sdk/agent-harness"; import { type CodexDynamicToolCallOutputContentItem, type CodexDynamicToolCallParams, type CodexDynamicToolCallResponse, type CodexDynamicToolSpec, type JsonValue, } from "./protocol.js"; export type CodexDynamicToolBridge = { specs: CodexDynamicToolSpec[]; handleToolCall: (params: CodexDynamicToolCallParams) => Promise; telemetry: { didSendViaMessagingTool: boolean; messagingToolSentTexts: string[]; messagingToolSentMediaUrls: string[]; messagingToolSentTargets: MessagingToolSend[]; toolMediaUrls: string[]; toolAudioAsVoice: boolean; successfulCronAdds?: number; }; }; export function createCodexDynamicToolBridge(params: { tools: AnyAgentTool[]; signal: AbortSignal; }): CodexDynamicToolBridge { const toolMap = new Map(params.tools.map((tool) => [tool.name, tool])); const telemetry: CodexDynamicToolBridge["telemetry"] = { didSendViaMessagingTool: false, messagingToolSentTexts: [], messagingToolSentMediaUrls: [], messagingToolSentTargets: [], toolMediaUrls: [], toolAudioAsVoice: false, }; return { specs: params.tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: toJsonValue(tool.parameters), })), telemetry, handleToolCall: async (call) => { const tool = toolMap.get(call.tool); if (!tool) { return { contentItems: [{ type: "inputText", text: `Unknown OpenClaw tool: ${call.tool}` }], success: false, }; } const args = jsonObjectToRecord(call.arguments); try { const preparedArgs = tool.prepareArguments ? tool.prepareArguments(args) : args; const result = await tool.execute(call.callId, preparedArgs, params.signal); collectToolTelemetry({ toolName: tool.name, args, result, telemetry, isError: false, }); return { contentItems: result.content.flatMap(convertToolContent), success: true, }; } catch (error) { collectToolTelemetry({ toolName: tool.name, args, result: undefined, telemetry, isError: true, }); return { contentItems: [ { type: "inputText", text: error instanceof Error ? error.message : String(error), }, ], success: false, }; } }, }; } function collectToolTelemetry(params: { toolName: string; args: Record; result: AgentToolResult | undefined; telemetry: CodexDynamicToolBridge["telemetry"]; isError: boolean; }): void { if (params.isError) { return; } if (!params.isError && params.toolName === "cron" && isCronAddAction(params.args)) { params.telemetry.successfulCronAdds = (params.telemetry.successfulCronAdds ?? 0) + 1; } if (!params.isError && params.result) { const media = extractToolResultMediaArtifact(params.result); if (media) { const mediaUrls = filterToolResultMediaUrls(params.toolName, media.mediaUrls, params.result); const seen = new Set(params.telemetry.toolMediaUrls); for (const mediaUrl of mediaUrls) { if (!seen.has(mediaUrl)) { seen.add(mediaUrl); params.telemetry.toolMediaUrls.push(mediaUrl); } } if (media.audioAsVoice) { params.telemetry.toolAudioAsVoice = true; } } } if ( !isMessagingTool(params.toolName) || !isMessagingToolSendAction(params.toolName, params.args) ) { return; } params.telemetry.didSendViaMessagingTool = true; const text = readFirstString(params.args, ["text", "message", "body", "content"]); if (text) { params.telemetry.messagingToolSentTexts.push(text); } params.telemetry.messagingToolSentMediaUrls.push(...collectMediaUrls(params.args)); params.telemetry.messagingToolSentTargets.push({ tool: params.toolName, provider: readFirstString(params.args, ["provider", "channel"]) ?? params.toolName, accountId: readFirstString(params.args, ["accountId", "account_id"]), to: readFirstString(params.args, ["to", "target", "recipient"]), threadId: readFirstString(params.args, ["threadId", "thread_id", "messageThreadId"]), }); } function convertToolContent( content: TextContent | ImageContent, ): CodexDynamicToolCallOutputContentItem[] { if (content.type === "text") { return [{ type: "inputText", text: content.text }]; } return [ { type: "inputImage", imageUrl: `data:${content.mimeType};base64,${content.data}`, }, ]; } function toJsonValue(value: unknown): JsonValue { try { const text = JSON.stringify(value); if (!text) { return {}; } return JSON.parse(text) as JsonValue; } catch { return {}; } } function jsonObjectToRecord(value: JsonValue | undefined): Record { if (!value || typeof value !== "object" || Array.isArray(value)) { return {}; } return value as Record; } function readFirstString(record: Record, keys: string[]): string | undefined { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } if (typeof value === "number" && Number.isFinite(value)) { return String(value); } } return undefined; } function collectMediaUrls(record: Record): string[] { const urls: string[] = []; for (const key of ["mediaUrl", "media_url", "imageUrl", "image_url"]) { const value = record[key]; if (typeof value === "string" && value.trim()) { urls.push(value.trim()); } } for (const key of ["mediaUrls", "media_urls", "imageUrls", "image_urls"]) { const value = record[key]; if (!Array.isArray(value)) { continue; } for (const entry of value) { if (typeof entry === "string" && entry.trim()) { urls.push(entry.trim()); } } } return urls; } function isCronAddAction(args: Record): boolean { const action = args.action; return typeof action === "string" && action.trim().toLowerCase() === "add"; }