openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
471 lines (470 loc) • 19.2 kB
JavaScript
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 };