UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

471 lines (470 loc) • 19.2 kB
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import "./error-runtime-BmospGvZ.mjs"; import { n as isAgentsApiTerminalTurn } from "./agentsapi-client-DPYkWMkx.mjs"; import { n as isAgentsApiOptionalHistoryReadFailure, r as isAgentsApiTransportDisconnect, t as AgentsApiError } from "./agentsapi-errors-DmeH76ku.mjs"; import { setTimeout } from "node:timers/promises"; import { APIUserAbortError } from "openai"; //#region extensions/agentsapi/agentsapi-session.ts /** Native input receipts and session idle, together, establish Agents API completion. */ function createAgentsApiSession(options) { const { client, cleanupClient, sessionId, signal, assertCurrent } = options; let streamController = new AbortController(); let submitted = options.initialInputSubmitted ?? false; let inputAdmissionClosed = false; let stopped = false; let closed = false; let settled = false; let rootTurn; let turnFailure; let cancelled = false; let submission = Promise.resolve(); let admittedSubmission = Promise.resolve(); let cancellation; let admittedMessageCount = submitted ? 1 : 0; let baselineTurnId; let baselineCaptured = submitted; let observedInputItems = /* @__PURE__ */ new Set(); const coordinatorTurnIds = /* @__PURE__ */ new Set(); const excludedTurnIds = /* @__PURE__ */ new Set(); const itemTurnIds = /* @__PURE__ */ new Map(); const excludedItemIds = /* @__PURE__ */ new Set(); let latestInputTurnId; let usageTurns; let terminatedByTool = false; const isAvailable = () => submitted && !inputAdmissionClosed && !stopped && !settled && !rootTurn && !signal.aborted; const submit = (text, persistInput) => { assertCurrent(); signal.throwIfAborted(); if (inputAdmissionClosed || stopped || settled || rootTurn) throw new Error("Agents API turn is stopped"); submission = submission.then(async () => { assertCurrent(); if (stopped || settled || rootTurn || signal.aborted) throw new Error("Agents API turn settled before input was submitted"); await persistInput?.(); assertCurrent(); signal.throwIfAborted(); if (stopped || settled || rootTurn) throw new Error("Agents API turn settled before input was submitted"); admittedMessageCount++; submitted = true; admittedSubmission = client.message(sessionId, text, AbortSignal.timeout(6e4)); return admittedSubmission; }); submission.catch(() => {}); return submission; }; const cancel = () => { stopped = true; streamController.abort(); if (!submitted || settled) return Promise.resolve(); cancellation ??= (async () => { let submissionError; try { await admittedSubmission; } catch (error) { submissionError = error; } await cleanupClient.cancel(sessionId, AbortSignal.timeout(3e4)); settled = true; if (submissionError !== void 0) throw submissionError instanceof Error ? submissionError : new Error(formatErrorMessage(submissionError), { cause: submissionError }); })(); cancellation.catch(() => {}); return cancellation; }; const onAbort = () => { cancel(); }; signal.addEventListener("abort", onAbort, { once: true }); const readAdmittedTurns = async (readClient, readSignal) => { const turns = await readClient.turns(sessionId, readSignal, baselineTurnId); readSignal.throwIfAborted(); for (const turn of turns) { coordinatorTurnIds.add(turn.id); excludedTurnIds.delete(turn.id); } const latest = turns.at(-1); latestInputTurnId = latest?.id; rootTurn = latest && isAgentsApiTerminalTurn(latest.status) ? latest : void 0; turnFailure = latest?.status === "failed" ? new AgentsApiError(latest.error?.message ?? "Agents API turn failed", latest.error ?? {}) : void 0; cancelled = !terminatedByTool && latest?.status === "cancelled"; return turns; }; const readItemsByTurn = async (readClient, readSignal) => { const savedItems = await readClient.items(sessionId, void 0, readSignal); readSignal.throwIfAborted(); const itemsByTurn = /* @__PURE__ */ new Map(); for (const item of savedItems) { if (!item.turn_id) continue; const items = itemsByTurn.get(item.turn_id) ?? []; items.push(item); itemsByTurn.set(item.turn_id, items); } return itemsByTurn; }; const readSavedState = async (readClient, readSignal) => { const turns = await readAdmittedTurns(readClient, readSignal); const entries = []; const inputItems = /* @__PURE__ */ new Set(); const itemsByTurn = turns.length || baselineTurnId ? await readItemsByTurn(readClient, readSignal) : /* @__PURE__ */ new Map(); for (const turn of turns) { const items = itemsByTurn.get(turn.id) ?? []; for (const item of items) { rememberItemTurn(item.id, turn.id); if (item.type === "message" && item.role === "user") inputItems.add(item.id); } entries.push({ turn, items }); } observedInputItems = inputItems; return { turns, entries, itemsByTurn }; }; const projectSavedState = async (entries, readSignal) => { let transcriptReady = true; for (const { turn, items } of entries) { readSignal.throwIfAborted(); transcriptReady = await options.onReconcile?.(turn, items) !== false && transcriptReady; readSignal.throwIfAborted(); } return transcriptReady; }; const reconcilePriorHistory = async (readClient, readSignal, itemsByTurn) => { if (!baselineTurnId || !options.onReconcileHistory) return; const turns = await readClient.turns(sessionId, readSignal); readSignal.throwIfAborted(); const baselineIndex = turns.findIndex((turn) => turn.id === baselineTurnId); if (baselineIndex < 0) throw new Error("Agents API historical reconciliation lost its baseline turn"); const priorTurns = turns.slice(0, baselineIndex + 1).filter((turn) => isAgentsApiTerminalTurn(turn.status)); if (!priorTurns.length) return; await options.onReconcileHistory(priorTurns.map((turn) => ({ turn, items: itemsByTurn.get(turn.id) ?? [] }))); readSignal.throwIfAborted(); }; const rememberItemTurn = (itemId, turnId) => { const previous = itemTurnIds.get(itemId); if (previous && previous !== turnId) throw new Error("Agents API item belongs to different admitted turns"); itemTurnIds.set(itemId, turnId); excludedItemIds.delete(itemId); }; return { isAvailable, wasSubmitted: () => submitted, isSettled: () => settled, queueMessage: submit, readUsageTurns() { if (!submitted || !settled) return Promise.resolve([]); return usageTurns ??= (async () => { const usageSignal = AbortSignal.timeout(5e3); let turns = []; try { while (true) { turns = await cleanupClient.turns(sessionId, usageSignal, baselineTurnId); const recordedIds = new Set(turns.map((turn) => turn.id)); if (turns.length > 0 && [...coordinatorTurnIds].every((id) => recordedIds.has(id)) && turns.every((turn) => turn.usage !== null)) return turns; await setTimeout(500, void 0, { signal: usageSignal }); } } catch (error) { if (!usageSignal.aborted) options.onUsageError?.(error); return turns; } })(); }, async run(prompt, persistInput, onSubmitted) { signal.throwIfAborted(); if (!options.initialInputSubmitted) { baselineTurnId = (await client.turns(sessionId, signal, void 0, true))[0]?.id; baselineCaptured = true; if (baselineTurnId) excludedTurnIds.add(baselineTurnId); } const callAdmissions = /* @__PURE__ */ new Map(); const relayFunctions = async () => { assertCurrent(); signal.throwIfAborted(); if (!options.executeFunction) throw new Error("Agents API MVP cannot continue: agent.session.requires_action"); let submissionFence = submission; await submissionFence; assertCurrent(); const inputCount = admittedMessageCount; const calls = await client.pendingFunctionCalls(sessionId, signal); if (!calls.length) return; for (const call of calls) { const identity = `${sessionId}:${call.turn_id}:${call.call_id}`; if (!callAdmissions.has(identity)) callAdmissions.set(identity, { inputCount, relayed: false }); } const turns = await readAdmittedTurns(client, signal); assertCurrent(); if (submissionFence !== submission) return relayFunctions(); const latestTurn = turns.at(-1); if (!latestTurn) throw new Error("Agents API function request has no current attempt root turn"); latestInputTurnId = latestTurn.id; const admittedCount = admittedMessageCount; let itemsByTurn; const prefixSignal = AbortSignal.any([signal, AbortSignal.timeout(5e3)]); try { itemsByTurn = await readItemsByTurn(client, prefixSignal); assertCurrent(); } catch (error) { signal.throwIfAborted(); assertCurrent(); if (!(prefixSignal.aborted && (error === prefixSignal.reason || error instanceof APIUserAbortError)) && !isAgentsApiOptionalHistoryReadFailure(error)) throw error; options.onTranscriptOrderingGap?.(); assertCurrent(); } for (const call of calls) { if (call.turn_id !== latestTurn.id || !["in_progress", "waiting"].includes(latestTurn.status)) throw new Error("Agents API function request belongs to a different or settled root turn"); const identity = `${sessionId}:${call.turn_id}:${call.call_id}`; const admission = callAdmissions.get(identity); if (admission.relayed) continue; const items = itemsByTurn?.get(call.turn_id); const callIndex = items?.findIndex((item) => item.type === "function_call" && item.call_id === call.call_id) ?? -1; if (items) { const prefix = callIndex >= 0 ? items.slice(0, callIndex) : items; const transcriptReady = await projectSavedState(turns.map((turn) => ({ turn, items: turn.id === call.turn_id ? prefix : itemsByTurn.get(turn.id) ?? [] })), signal); assertCurrent(); if (!transcriptReady || callIndex < 0) { options.onTranscriptOrderingGap?.(); assertCurrent(); } } else { options.onTranscriptOrderingGap?.(); assertCurrent(); } if (submissionFence !== submission || admittedCount !== admittedMessageCount) return relayFunctions(); admission.relayed = true; const result = await options.executeFunction(call); assertCurrent(); signal.throwIfAborted(); const terminate = result.terminate || result.sourceReplyDelivered; if (terminate) inputAdmissionClosed = true; submission = submission.then(() => { assertCurrent(); signal.throwIfAborted(); admittedSubmission = client.toolResult(sessionId, call, result, AbortSignal.timeout(6e4)); return admittedSubmission; }); submission.catch(() => {}); const acknowledgementFence = submission; await acknowledgementFence; submissionFence = acknowledgementFence; await options.onFunctionResult?.(call, result); assertCurrent(); if (terminate) { if (admittedMessageCount !== admission.inputCount) { inputAdmissionClosed = false; return relayFunctions(); } await cleanupClient.cancel(sessionId, AbortSignal.timeout(3e4)); terminatedByTool = true; await settleFromSavedState(); if (!settled || !rootTurn || !["completed", "cancelled"].includes(rootTurn.status ?? "")) throw new Error("Agents API tool termination did not settle its native root turn and inputs"); streamController.abort(); return; } } }; let events = await client.subscribe(sessionId, AbortSignal.any([signal, streamController.signal])); let nextEvent = events.next(); nextEvent.catch(() => {}); const settleFromSavedState = async (recover = false) => { const submissionFence = submission; await submissionFence; assertCurrent(); const admittedCount = admittedMessageCount; const snapshot = await readSavedState(client, signal); assertCurrent(); const session = await client.session(sessionId, signal); assertCurrent(); if (session.status === "failed") throw new Error(session.error ?? "Agents API session failed"); if (session.status === "requires_action") { await relayFunctions(); if (settled) return; } settled = Boolean(rootTurn && rootTurn.id === snapshot.turns.at(-1)?.id && session.status === "idle" && submissionFence === submission && admittedCount === admittedMessageCount && observedInputItems.size === admittedCount); if (settled) options.onSettled?.(); if (settled || recover) { if (settled) { await reconcilePriorHistory(client, signal, snapshot.itemsByTurn); assertCurrent(); } await projectSavedState(snapshot.entries, signal); assertCurrent(); } }; const belongsToAttempt = async (event) => { if (event.item && event.item_id && event.item.id !== event.item_id) throw new Error("Agents API event contains different item identities"); const itemId = event.item?.id ?? event.item_id; const turnIds = new Set([ event.turn?.id, event.turn_id, event.item?.turn_id, itemId ? itemTurnIds.get(itemId) : void 0 ].filter((id) => typeof id === "string" && id.length > 0)); if (turnIds.size > 1) throw new Error("Agents API event contains different turn identities"); const turnId = turnIds.values().next().value; if (!turnId) { if (itemId) { if (excludedItemIds.has(itemId)) return false; const snapshot = await readSavedState(client, signal); assertCurrent(); if (!itemTurnIds.has(itemId)) { excludedItemIds.add(itemId); return false; } await projectSavedState(snapshot.entries, signal); assertCurrent(); return true; } if (event.item || event.type === "agent.output.command_execution_output.delta") throw new Error("Agents API output event is missing its turn ID"); return true; } if (excludedTurnIds.has(turnId)) { if (itemId) excludedItemIds.add(itemId); return false; } if (!coordinatorTurnIds.has(turnId)) { await readAdmittedTurns(client, signal); assertCurrent(); if (!coordinatorTurnIds.has(turnId)) { excludedTurnIds.add(turnId); if (itemId) excludedItemIds.add(itemId); return false; } } if (event.item) rememberItemTurn(event.item.id, turnId); return true; }; try { await persistInput(); assertCurrent(); signal.throwIfAborted(); if (!options.initialInputSubmitted) await submit(prompt); onSubmitted(); if (options.initialInputSubmitted) await settleFromSavedState(true); while (true) { if (settled) break; let chunk; try { if (options.initialInputSubmitted) { const refresh = new AbortController(); try { chunk = await Promise.race([nextEvent, setTimeout(1e3, void 0, { signal: AbortSignal.any([signal, refresh.signal]) })]); } finally { refresh.abort(); } } else chunk = await nextEvent; } catch (error) { signal.throwIfAborted(); assertCurrent(); if (!isAgentsApiTransportDisconnect(error)) throw error; chunk = { done: true, value: void 0 }; } if (!chunk) { await settleFromSavedState(true); continue; } if (chunk.done) { streamController.abort(); await events.return(void 0).catch((error) => { if (!isAgentsApiTransportDisconnect(error)) throw error; }); while (true) { await setTimeout(500, void 0, { signal }); assertCurrent(); streamController = new AbortController(); try { events = await client.subscribe(sessionId, AbortSignal.any([signal, streamController.signal])); break; } catch (error) { streamController.abort(); signal.throwIfAborted(); assertCurrent(); if (!isAgentsApiTransportDisconnect(error)) throw error; } } nextEvent = events.next(); nextEvent.catch(() => {}); await settleFromSavedState(true); continue; } const event = chunk.value; nextEvent = events.next(); nextEvent.catch(() => {}); assertCurrent(); if (!await belongsToAttempt(event)) continue; assertCurrent(); await options.onEvent(event); assertCurrent(); if (event.type === "agent.session.idle") { await settleFromSavedState(); continue; } if ((event.type === "agent.session.turn.item.added" || event.type === "agent.session.turn.item.done") && event.item?.type === "message" && event.item.role === "user" && event.item.id && !observedInputItems.has(event.item.id)) { observedInputItems.add(event.item.id); if (!(event.item.turn_id ?? event.turn_id ?? itemTurnIds.get(event.item.id))) throw new Error("Agents API input item is missing its turn ID"); } if (event.type === "error") throw new AgentsApiError(event.error?.message ?? "Agents API stream error", event.error); if (event.type === "agent.session.requires_action") { await relayFunctions(); continue; } if (["agent.session.failed", "agent.session.environment.failed"].includes(event.type)) { const nativeError = event.environment?.error ?? event.error; throw new AgentsApiError(nativeError?.message ?? event.session?.error ?? `Agents API cannot continue: ${event.type}`, nativeError ?? {}); } if ((event.type === "agent.session.turn.completed" || event.type === "agent.session.turn.failed" || event.type === "agent.session.turn.cancelled") && event.turn?.subagent_id === null && event.turn.id === latestInputTurnId) { rootTurn = event.turn; turnFailure = event.type.endsWith(".failed") ? new AgentsApiError(event.turn.error?.message ?? "Agents API turn failed", event.turn.error ?? {}) : void 0; cancelled = event.type.endsWith(".cancelled"); await settleFromSavedState(); } } } finally { streamController.abort(); await events.return(void 0); } if (!rootTurn || !settled) throw new Error("Agents API stream closed before the root turn settled; reset or inspect the session before retrying"); if (turnFailure) throw turnFailure; return { turn: rootTurn, cancelled, terminatedByTool }; }, async reconcileAfterClose(cleanupSignal) { if (!closed) throw new Error("Agents API canonical cleanup requires a closed session attempt"); if (!submitted || !baselineCaptured) return; cleanupSignal.throwIfAborted(); const session = await cleanupClient.session(sessionId, cleanupSignal); cleanupSignal.throwIfAborted(); if (session.status !== "idle" && session.status !== "failed") throw new Error("Agents API canonical cleanup requires native work to be retired"); const snapshot = await readSavedState(cleanupClient, cleanupSignal); await reconcilePriorHistory(cleanupClient, cleanupSignal, snapshot.itemsByTurn); await projectSavedState(snapshot.entries, cleanupSignal); return snapshot.turns.at(-1); }, async close() { signal.removeEventListener("abort", onAbort); streamController.abort(); if (submitted && !settled) await cancel(); await cancellation; stopped = true; closed = true; } }; } //#endregion export { createAgentsApiSession as t };