mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
218 lines
6.4 KiB
TypeScript
218 lines
6.4 KiB
TypeScript
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<CodexDynamicToolCallResponse>;
|
|
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<string, unknown>;
|
|
result: AgentToolResult<unknown> | undefined;
|
|
telemetry: CodexDynamicToolBridge["telemetry"];
|
|
isError: boolean;
|
|
}): void {
|
|
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<string, unknown> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return {};
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function readFirstString(record: Record<string, unknown>, 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, unknown>): 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<string, unknown>): boolean {
|
|
const action = args.action;
|
|
return typeof action === "string" && action.trim().toLowerCase() === "add";
|
|
}
|