UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

497 lines (496 loc) • 20.1 kB
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import "./error-runtime-oJAVOxQN.mjs"; import { n as isAgentsApiTerminalTurn } from "./agentsapi-client-4xhmrPo7.mjs"; import { n as isAgentsApiOptionalHistoryReadFailure, r as isAgentsApiTransportDisconnect, t as AgentsApiError } from "./agentsapi-errors-Ck7jiWFW.mjs"; import { n as waitForAgentsApiEventOrRecovery, t as createAgentsApiSessionHistory } from "./agentsapi-session-history-CPXbSTyg.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; const waitForRecovery = options.waitForRecovery ?? ((ms, recoverySignal) => setTimeout(ms, void 0, { signal: recoverySignal })); 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; const priorInputItemIds = /* @__PURE__ */ new Set(); 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; let reconciliationPending = options.initialInputSubmitted ?? 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 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); }; const history = createAgentsApiSessionHistory({ sessionId, cleanupClient, getBaselineTurnId: () => baselineTurnId, coordinatorTurnIds, priorInputItemIds, readAdmittedTurns, rememberItemTurn, observeInputItems: (items) => { observedInputItems = items; }, callbacks: options }); return { isAvailable, wasSubmitted: () => submitted, isSettled: () => settled, queueMessage: submit, readUsageTurns() { if (!submitted || !settled) return Promise.resolve([]); return usageTurns ??= history.readUsageTurns(); }, async run(prompt, persistInput, onSubmitted) { signal.throwIfAborted(); if (!options.initialInputSubmitted) { const latest = (await client.turns(sessionId, signal, void 0, true))[0]; assertCurrent(); baselineTurnId = latest?.id; if (latest && !isAgentsApiTerminalTurn(latest.status)) { const turns = await client.turns(sessionId, signal); assertCurrent(); const latestIndex = turns.findIndex((turn) => turn.id === latest.id); if (latestIndex < 0) throw new Error("Agents API continuation lost its active root turn"); baselineTurnId = turns[latestIndex - 1]?.id; const items = await client.items(sessionId, latest.id, signal); assertCurrent(); for (const item of items) if (item.type === "message" && item.role === "user") priorInputItemIds.add(item.id); } baselineCaptured = true; if (baselineTurnId) excludedTurnIds.add(baselineTurnId); } const callAdmissions = /* @__PURE__ */ new Map(); const relayFunctions = async () => { assertCurrent(); signal.throwIfAborted(); let submissionFence = submission; await awaitReceipt(submissionFence); assertCurrent(); const inputCount = admittedMessageCount; const calls = await client.pendingFunctionCalls(sessionId, signal, options.connectEnvironment); if (!calls.length) return; if (!options.executeFunction) throw new Error("Agents API MVP cannot continue: agent.session.requires_action"); 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 history.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 history.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; 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 awaitReceipt(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 bufferedEvents = []; const reconnectEvents = async () => { reconciliationPending = true; streamController.abort(); await events.return(void 0).catch((error) => { if (!isAgentsApiTransportDisconnect(error)) throw error; }); while (true) { await waitForRecovery(500, 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(() => {}); if (options.connectEnvironment) { await client.pendingFunctionCalls(sessionId, signal, options.connectEnvironment); assertCurrent(); } }; let initialConnectionReconciled = false; const awaitReceipt = async (receipt) => { if (!initialConnectionReconciled && options.connectEnvironment) { initialConnectionReconciled = true; await client.pendingFunctionCalls(sessionId, signal, options.connectEnvironment); assertCurrent(); } const acknowledged = receipt.then(() => ({ kind: "acknowledged" }), (error) => ({ kind: "failed", error })); while (true) { signal.throwIfAborted(); let chunk; try { chunk = await Promise.race([acknowledged, nextEvent]); } catch (error) { signal.throwIfAborted(); assertCurrent(); if (!isAgentsApiTransportDisconnect(error)) throw error; chunk = { done: true, value: void 0 }; } if ("kind" in chunk) { if (chunk.kind === "failed") throw chunk.error; return; } if (chunk.done) { await reconnectEvents(); continue; } bufferedEvents.push(chunk.value); nextEvent = events.next(); nextEvent.catch(() => {}); if (chunk.value.type === "agent.session.requires_action" && options.connectEnvironment) { await client.pendingFunctionCalls(sessionId, signal, options.connectEnvironment); assertCurrent(); } } }; const settleFromSavedState = async (recover = false) => { const submissionFence = submission; await awaitReceipt(submissionFence); assertCurrent(); const admittedCount = admittedMessageCount; const snapshot = await history.readSavedState(client, signal); assertCurrent(); const session = await client.session(sessionId, signal); assertCurrent(); if (session.status === "failed") { await options.onSessionFailed?.(); 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 history.reconcilePriorHistory(client, signal, snapshot.itemsByTurn); assertCurrent(); } await history.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 history.readSavedState(client, signal); assertCurrent(); if (!itemTurnIds.has(itemId)) { excludedItemIds.add(itemId); return false; } await history.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 awaitReceipt(submit(prompt)); onSubmitted(); if (options.initialInputSubmitted) await settleFromSavedState(true); while (true) { if (settled) break; let chunk; let consumedBufferedEvent = false; try { const buffered = bufferedEvents.shift(); if (buffered) { consumedBufferedEvent = true; chunk = { done: false, value: buffered }; } else if (reconciliationPending) chunk = await waitForAgentsApiEventOrRecovery(nextEvent, signal, waitForRecovery); 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) { await reconnectEvents(); await settleFromSavedState(true); continue; } const event = chunk.value; if (!consumedBufferedEvent) { nextEvent = events.next(); nextEvent.catch(() => {}); } assertCurrent(); if (!await belongsToAttempt(event)) continue; assertCurrent(); await options.onEvent(event); assertCurrent(); if (event.type === "agent.session.idle") { reconciliationPending = true; 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 && !priorInputItemIds.has(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"); await settleFromSavedState(); } 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 (event.type === "agent.session.failed") { const session = await client.session(sessionId, signal); assertCurrent(); if (session.status !== "failed") continue; await options.onSessionFailed?.(); throw new AgentsApiError(session.error ?? "Agents API session failed", event.error); } if (event.type === "agent.session.environment.failed") { 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) { reconciliationPending = true; 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 === "failed") await options.onSessionFailed?.(); if (session.status !== "idle" && session.status !== "failed") throw new Error("Agents API canonical cleanup requires native work to be retired"); const snapshot = await history.readSavedState(cleanupClient, cleanupSignal); await history.reconcilePriorHistory(cleanupClient, cleanupSignal, snapshot.itemsByTurn); await history.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 };