import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; import { createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, extractToolResultMediaArtifact, filterToolResultMediaUrls, HEARTBEAT_RESPONSE_TOOL_NAME, type EmbeddedRunAttemptParams, isToolWrappedWithBeforeToolCallHook, isMessagingTool, isMessagingToolSendAction, normalizeHeartbeatToolResponse, runAgentHarnessAfterToolCallHook, type AnyAgentTool, type HeartbeatToolResponse, type MessagingToolSend, wrapToolWithBeforeToolCallHook, } from "openclaw/plugin-sdk/agent-harness-runtime"; import type { CodexDynamicToolsLoading } from "./config.js"; import { type CodexDynamicToolCallOutputContentItem, type CodexDynamicToolCallParams, type CodexDynamicToolCallResponse, type CodexDynamicToolSpec, type JsonValue, } from "./protocol.js"; type CodexDynamicToolHookContext = { agentId?: string; config?: EmbeddedRunAttemptParams["config"]; sessionId?: string; sessionKey?: string; runId?: string; }; type CodexToolResultHookContext = Omit; export type CodexDynamicToolBridge = { specs: CodexDynamicToolSpec[]; handleToolCall: ( params: CodexDynamicToolCallParams, options?: { signal?: AbortSignal }, ) => Promise; telemetry: { didSendViaMessagingTool: boolean; messagingToolSentTexts: string[]; messagingToolSentMediaUrls: string[]; messagingToolSentTargets: MessagingToolSend[]; heartbeatToolResponse?: HeartbeatToolResponse; toolMediaUrls: string[]; toolAudioAsVoice: boolean; successfulCronAdds?: number; }; }; export const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw"; const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set(["sessions_yield"]); export function createCodexDynamicToolBridge(params: { tools: AnyAgentTool[]; signal: AbortSignal; hookContext?: CodexDynamicToolHookContext; loading?: CodexDynamicToolsLoading; directToolNames?: Iterable; }): CodexDynamicToolBridge { const toolResultHookContext = toToolResultHookContext(params.hookContext); const tools = params.tools.map((tool) => isToolWrappedWithBeforeToolCallHook(tool) ? tool : wrapToolWithBeforeToolCallHook(tool, params.hookContext), ); const toolMap = new Map(tools.map((tool) => [tool.name, tool])); const telemetry: CodexDynamicToolBridge["telemetry"] = { didSendViaMessagingTool: false, messagingToolSentTexts: [], messagingToolSentMediaUrls: [], messagingToolSentTargets: [], toolMediaUrls: [], toolAudioAsVoice: false, }; const middlewareRunner = createAgentToolResultMiddlewareRunner({ runtime: "codex", ...toolResultHookContext, }); const legacyExtensionRunner = createCodexAppServerToolResultExtensionRunner(toolResultHookContext); const directToolNames = new Set([ ...ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES, ...(params.directToolNames ?? []), ]); return { specs: tools.map((tool) => createCodexDynamicToolSpec({ tool, loading: params.loading ?? "searchable", directToolNames, }), ), telemetry, handleToolCall: async (call, options) => { 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); const startedAt = Date.now(); const signal = composeAbortSignals(params.signal, options?.signal); try { const preparedArgs = tool.prepareArguments ? tool.prepareArguments(args) : args; const rawResult = await tool.execute(call.callId, preparedArgs, signal); const rawIsError = isToolResultError(rawResult); const middlewareResult = await middlewareRunner.applyToolResultMiddleware({ threadId: call.threadId, turnId: call.turnId, toolCallId: call.callId, toolName: tool.name, args, isError: rawIsError, result: rawResult, }); const result = await legacyExtensionRunner.applyToolResultExtensions({ threadId: call.threadId, turnId: call.turnId, toolCallId: call.callId, toolName: tool.name, args, result: middlewareResult, }); const resultIsError = rawIsError || isToolResultError(result); collectToolTelemetry({ toolName: tool.name, args, result, mediaTrustResult: rawResult, telemetry, isError: resultIsError, }); void runAgentHarnessAfterToolCallHook({ toolName: tool.name, toolCallId: call.callId, runId: toolResultHookContext.runId, agentId: toolResultHookContext.agentId, sessionId: toolResultHookContext.sessionId, sessionKey: toolResultHookContext.sessionKey, startArgs: args, result, startedAt, }); return { contentItems: result.content.flatMap(convertToolContent), success: !resultIsError, }; } catch (error) { collectToolTelemetry({ toolName: tool.name, args, result: undefined, telemetry, isError: true, }); void runAgentHarnessAfterToolCallHook({ toolName: tool.name, toolCallId: call.callId, runId: toolResultHookContext.runId, agentId: toolResultHookContext.agentId, sessionId: toolResultHookContext.sessionId, sessionKey: toolResultHookContext.sessionKey, startArgs: args, error: error instanceof Error ? error.message : String(error), startedAt, }); return { contentItems: [ { type: "inputText", text: error instanceof Error ? error.message : String(error), }, ], success: false, }; } }, }; } function createCodexDynamicToolSpec(params: { tool: AnyAgentTool; loading: CodexDynamicToolsLoading; directToolNames: ReadonlySet; }): CodexDynamicToolSpec { const base = { name: params.tool.name, description: params.tool.description, inputSchema: toJsonValue(params.tool.parameters), }; if (params.loading === "direct" || params.directToolNames.has(params.tool.name)) { return base; } return { ...base, namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE, deferLoading: true, }; } function toToolResultHookContext( ctx: CodexDynamicToolHookContext | undefined, ): CodexToolResultHookContext { const { agentId, sessionId, sessionKey, runId } = ctx ?? {}; return { ...(agentId && { agentId }), ...(sessionId && { sessionId }), ...(sessionKey && { sessionKey }), ...(runId && { runId }), }; } function composeAbortSignals(...signals: Array): AbortSignal { const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal)); if (activeSignals.length === 0) { return new AbortController().signal; } if (activeSignals.length === 1) { return activeSignals[0]; } return AbortSignal.any(activeSignals); } function collectToolTelemetry(params: { toolName: string; args: Record; result: AgentToolResult | undefined; mediaTrustResult?: AgentToolResult; 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.toolName === HEARTBEAT_RESPONSE_TOOL_NAME) { const response = normalizeHeartbeatToolResponse(params.result?.details); if (response) { params.telemetry.heartbeatToolResponse = response; } } if (!params.isError && params.result) { const media = extractToolResultMediaArtifact(params.result); if (media) { const mediaUrls = filterToolResultMediaUrls( params.toolName, media.mediaUrls, params.mediaTrustResult ?? 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); } const mediaUrls = collectMediaUrls(params.args); params.telemetry.messagingToolSentMediaUrls.push(...mediaUrls); 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"]), ...(text ? { text } : {}), ...(mediaUrls.length > 0 ? { mediaUrls } : {}), }); } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function isToolResultError(result: AgentToolResult): boolean { const details = result.details; if (!isRecord(details)) { return false; } if (details.timedOut === true) { return true; } if (typeof details.exitCode === "number" && details.exitCode !== 0) { return true; } if (typeof details.status !== "string") { return false; } const status = details.status.trim().toLowerCase(); return ( status !== "" && status !== "0" && status !== "ok" && status !== "success" && status !== "completed" && status !== "recorded" && status !== "running" ); } 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"; }