UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

353 lines (352 loc) • 14.2 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import "./text-utility-runtime-UZWYbiiX.mjs"; import { r as projectAgentToolActivity } from "./agent-activity-events-CKa-4ICH.mjs"; import { u as sanitizeToolResult } from "./embedded-agent-tool-results-Diy6K9Rh.mjs"; import { a as formatNativeToolOutput, c as truncateNativeToolTranscriptText, i as TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS, n as NativeToolOutputAccumulator, o as formatNativeToolSummary, r as TOOL_PROGRESS_OUTPUT_MAX_CHARS, s as formatToolProgressOutput } from "./projection-tool-output-Dk6G4NEy.mjs"; import "./agent-harness-runtime-ADe6s8a3.mjs"; import "./agent-harness-attempt-runtime-DclhpkiN.mjs"; import { i as agentsApiNativeToolOutput, n as agentsApiNativeToolDetails, r as agentsApiNativeToolOutcome, t as agentsApiNativeTool } from "./agentsapi-native-items-C9R1hHDa.mjs"; import { o as recordAgentsApiNativeToolInvocation, s as recordAgentsApiNativeToolTranscript } from "./agentsapi-transcript-Bg_UtQuK.mjs"; import { createHash } from "node:crypto"; //#region extensions/agentsapi/agentsapi-native-tool-projection.ts /** Native tools own lifecycle, bounded output, and canonical transcript facts. */ var AgentsApiNativeToolProjection = class { constructor(params, remoteSessionId, emit, assertCurrent, nextTimestamp, isPresentationEnabled) { this.params = params; this.remoteSessionId = remoteSessionId; this.emit = emit; this.assertCurrent = assertCurrent; this.nextTimestamp = nextTimestamp; this.isPresentationEnabled = isPresentationEnabled; this.items = /* @__PURE__ */ new Map(); this.turnByItem = /* @__PURE__ */ new Map(); this.output = new NativeToolOutputAccumulator("Agents API"); this.metas = /* @__PURE__ */ new Map(); } get toolMetas() { return [...this.metas.values()]; } get lastToolError() { return this.nativeToolError; } get itemLifecycle() { const states = [...this.items.values()]; const completedCount = states.filter((state) => state.terminal).length; return { startedCount: states.length, completedCount, activeCount: states.length - completedCount }; } resolveTurnId(itemId) { return this.turnByItem.get(itemId); } async recordItem(turnId, item, terminal, enclosingStatus, canonical = false, recordTranscript = true) { this.assertCurrent(); const tool = agentsApiNativeTool(item, this.params); if (!tool) return false; const id = this.identity(turnId, item.id); let state = this.items.get(id); if (state?.terminal && !canonical) return true; if (!state) { state = { turnId, item, tool, terminal: false, startProjected: false, callRecorded: false, resultRecorded: false, provisionalTerminalObserved: false, canonicalTerminalObserved: false, recoveredPartial: false, outputProgressChars: 0, outputProgressMessages: 0, commandOutputChars: 0, commandOutputMessages: 0 }; this.items.set(id, state); this.turnByItem.set(item.id, turnId); } state.item = item; state.tool = tool; if (canonical) state.canonicalItem = item; if (canonical && !terminal) { state.recoveredPartial = true; if (item.type === "command_execution" && typeof item.output === "string") { state.recoveredOutput = agentsApiNativeToolOutput(item); state.recoveredOutputTruncated = item.output.length > TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS; } } await this.startTool(state); if (canonical && recordTranscript && !state.callRecorded) state.callRecorded = await recordAgentsApiNativeToolInvocation(this.params, this.remoteSessionId, turnId, item, this.assertCurrent, this.nextTimestamp); if (terminal) await this.finishTool(state, enclosingStatus, canonical, recordTranscript); state.terminal = terminal; return true; } async reconcileRemaining(turnId, status, currentItemIds) { this.assertCurrent(); let transcriptReady = true; for (const state of this.items.values()) { if (state.turnId !== turnId || state.resultRecorded && state.canonicalTerminalObserved) continue; if (!state.resultRecorded && !currentItemIds.has(state.item.id)) transcriptReady = false; await this.finishTool(state, status, true); state.terminal = true; } return transcriptReady; } async observeOutput(event) { this.assertCurrent(); const state = this.eventItem(event); if (!state || state.terminal || state.recoveredPartial || state.item.type !== "command_execution" || !event.delta) return; const id = this.identity(state.turnId, state.item.id); const delta = sanitizeToolResult(event.delta); this.output.append(id, delta); if (state.commandOutputChars < 8e3 && state.commandOutputMessages < 20) { const remaining = TOOL_PROGRESS_OUTPUT_MAX_CHARS - state.commandOutputChars; const text = truncateUtf16Safe(delta, remaining); state.commandOutputChars += text.length; state.commandOutputMessages += 1; await this.emit({ stream: "command_output", data: { itemId: id, toolCallId: id, phase: "delta", name: state.tool.name, title: typeof state.tool.args.command === "string" ? state.tool.args.command : "Command output", output: delta.length > remaining ? `${text}\n...(truncated)...` : text, status: "running", ...typeof state.tool.args.cwd === "string" ? { cwd: state.tool.args.cwd } : {} } }); } await this.emitToolOutput(state, event.delta, false); } async startTool(state) { if (state.startProjected) return; const id = this.identity(state.turnId, state.item.id); const { name, args, meta, commandBearing } = state.tool; state.startProjected = true; this.metas.set(id, { toolCallId: id, toolName: name, ...meta ? { meta } : {} }); await this.emit({ stream: "item", data: projectAgentToolActivity({ toolCallId: id, name, phase: "start", args, meta }) }); await this.emit({ stream: "tool", data: { phase: "start", itemId: id, toolCallId: id, name, args, ...meta ? { meta } : {}, ...commandBearing ? { commandBearing: true } : {} } }); if (this.shouldEmitToolResult()) await this.emitToolProgress(id, formatNativeToolSummary(name, this.formattedMeta(state.tool))); } async finishTool(state, enclosingStatus, canonical = false, recordTranscript = true) { const id = this.identity(state.turnId, state.item.id); const item = canonical ? state.canonicalItem ?? state.item : state.item; const tool = canonical && state.canonicalItem ? agentsApiNativeTool(state.canonicalItem, this.params) : state.tool; if (!tool) return; const { name, args, meta, commandBearing } = tool; const savedItemUnavailable = canonical && !state.canonicalItem; const outcome = savedItemUnavailable ? { status: enclosingStatus === "cancelled" ? "cancelled" : "unknown", isError: true, outcomeUnknown: enclosingStatus !== "cancelled", error: enclosingStatus === "cancelled" ? "Agents API native tool was cancelled" : "Agents API native tool item is unavailable in saved state" } : agentsApiNativeToolOutcome(item, enclosingStatus); const capturedOutput = state.recoveredOutput ?? this.output.textByItem.get(id); const output = agentsApiNativeToolOutput(item, capturedOutput); const details = { ...agentsApiNativeToolDetails(this.remoteSessionId, state.turnId, item, outcome, capturedOutput), ...savedItemUnavailable ? { savedItemAvailability: "unavailable", ...item.type !== "web_search_call" ? { outputAvailability: output === void 0 ? "unavailable" : "partial" } : {} } : {} }; const captureTruncated = item.output == null && (state.recoveredOutputTruncated || this.output.isTruncated(id)); this.metas.set(id, { toolCallId: id, toolName: name, ...meta ? { meta } : {}, isError: outcome.isError }); if (canonical && recordTranscript && state.canonicalItem && !state.resultRecorded) state.resultRecorded = await recordAgentsApiNativeToolTranscript(this.params, this.remoteSessionId, state.turnId, state.canonicalItem, this.assertCurrent, this.nextTimestamp, { enclosingStatus, capturedOutput, captureTruncated }); if (canonical && (state.canonicalItem ? !state.canonicalTerminalObserved : !state.provisionalTerminalObserved)) { this.assertCurrent(); const resolution = this.params.observeToolTerminal?.({ toolCallId: id, toolName: name, arguments: args, ...meta ? { meta } : {}, executionStarted: true, outcome: outcome.isError ? "failure" : "success", ...outcome.isError ? { failure: { ...outcome.error ? { error: outcome.error } : {}, ...outcome.errorCode ? { errorCode: outcome.errorCode } : {} } } : {}, nativeMutation: { mutatingAction: item.type !== "web_search_call", replaySafe: item.type === "web_search_call" } }); this.assertCurrent(); if (state.canonicalItem) state.canonicalTerminalObserved = true; else state.provisionalTerminalObserved = true; if (resolution) this.nativeToolError = resolution.lastToolError; else if (outcome.isError) this.nativeToolError = { toolName: name, ...meta ? { meta } : {}, ...outcome.error ? { error: outcome.error } : {}, ...outcome.errorCode ? { errorCode: outcome.errorCode } : {}, ...item.type !== "web_search_call" ? { mutatingAction: true } : {} }; else if (this.nativeToolError?.mutatingAction !== true) this.nativeToolError = void 0; } this.assertCurrent(); if (!this.isPresentationEnabled()) return; const commandFacts = commandBearing ? { title: typeof args.command === "string" ? args.command : "Command output", ...!savedItemUnavailable && typeof item.exit_code === "number" ? { exitCode: item.exit_code } : {}, ...!savedItemUnavailable && typeof item.duration_ms === "number" ? { durationMs: item.duration_ms } : {}, ...typeof args.cwd === "string" ? { cwd: args.cwd } : {} } : void 0; const presentationOutput = output !== void 0 && commandBearing ? formatToolProgressOutput(output) ?? "" : output; const toolSnapshot = { phase: "result", itemId: id, toolCallId: id, name, args, status: outcome.status, isError: outcome.isError, result: details, ...presentationOutput !== void 0 ? { output: presentationOutput } : {}, ...meta ? { meta } : {}, ...commandBearing ? { commandBearing: true, ...commandFacts } : {} }; const itemSnapshot = projectAgentToolActivity({ toolCallId: id, name, phase: "result", args, meta, status: outcome.status === "cancelled" ? "unknown" : outcome.status, result: { details }, isError: outcome.isError }); const commandSnapshot = commandFacts ? { itemId: id, toolCallId: id, phase: "end", name, ...commandFacts, ...presentationOutput !== void 0 ? { output: presentationOutput } : {}, status: outcome.status } : void 0; const projectionHash = nativeTerminalProjectionHash({ tool: toolSnapshot, item: itemSnapshot, command: commandSnapshot }); if (state.terminalProjectionHash === projectionHash) return; const replacement = { replaceable: true, ...state.terminalProjectionHash ? { replace: true } : {} }; await this.emit({ stream: "tool", data: { ...toolSnapshot, ...replacement } }); await this.emit({ stream: "item", data: { ...itemSnapshot, ...replacement } }); if (commandSnapshot) await this.emit({ stream: "command_output", data: { ...commandSnapshot, ...replacement } }); state.terminalProjectionHash = projectionHash; if (output !== void 0 && state.outputProgressMessages === 0) await this.emitToolOutput(state, output, true, outcome.isError); } async emitToolOutput(state, output, terminal, isError = false) { if (!this.shouldEmitToolOutput() || state.outputProgressChars >= 8e3 || state.outputProgressMessages >= 20) return; const remaining = TOOL_PROGRESS_OUTPUT_MAX_CHARS - state.outputProgressChars; const sanitized = sanitizeToolResult(output); const truncated = sanitized.length > remaining; const text = truncateUtf16Safe(sanitized, remaining); state.outputProgressChars += text.length; state.outputProgressMessages += 1; await this.emitToolProgress(this.identity(state.turnId, state.item.id), formatNativeToolOutput(state.tool.name, this.formattedMeta(state.tool), truncated ? `${text}\n...(truncated)...` : text), terminal && isError); } async emitToolProgress(itemId, text, isError = false) { this.assertCurrent(); if (!this.isPresentationEnabled()) return; await this.params.onToolResult?.({ text: truncateNativeToolTranscriptText(text, "Agents API"), ...this.params.messageChannel || this.params.messageProvider ? { channelData: { openclawToolProgressId: `tool:${itemId}` } } : {}, ...isError ? { isError: true } : {} }); this.assertCurrent(); } formattedMeta(tool) { return !tool.commandBearing || !(this.params.messageChannel ?? this.params.messageProvider) && this.shouldEmitToolOutput() ? tool.meta : void 0; } shouldEmitToolResult() { this.assertCurrent(); if (!this.isPresentationEnabled()) return false; return this.params.shouldEmitToolResult?.() ?? (this.params.verboseLevel === "on" || this.params.verboseLevel === "full"); } shouldEmitToolOutput() { this.assertCurrent(); if (!this.isPresentationEnabled()) return false; return this.params.shouldEmitToolOutput?.() ?? this.params.verboseLevel === "full"; } eventItem(event) { if (!event.item_id) throw new Error("Agents API output event has no item identity"); const turnId = event.turn_id ?? this.turnByItem.get(event.item_id); return turnId ? this.items.get(this.identity(turnId, event.item_id)) : void 0; } identity(turnId, itemId) { return `agentsapi:${this.remoteSessionId}:${turnId}:${itemId}`; } }; function nativeTerminalProjectionHash(snapshot) { return createHash("sha256").update(JSON.stringify(snapshot, (_key, value) => { if (value === null || typeof value !== "object" || Array.isArray(value)) return value; return Object.fromEntries(Object.entries(value).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)); })).digest("hex"); } //#endregion export { AgentsApiNativeToolProjection as t };