@sema-agent/core
Version:
Stateless, task-oriented AI agent core
159 lines (158 loc) • 8.28 kB
JavaScript
import { readMember } from "./thrown-value.js";
export const closeNeverExecuted = (errorKind) => errorKind === "interrupted_never_started";
const INTERRUPTED_UNKNOWN = "[INTERRUPTED] The previous run ended before this tool call's result was recorded. Its outcome " +
"is UNKNOWN — the action may have completed, partially completed, or never run. Do not assume " +
"success or failure. Before relying on it: if it is safe to read, query the current state to " +
"check; if this was a write or irreversible action, confirm with the user before repeating it.";
const INTERRUPTED_SAFE = "[INTERRUPTED] The previous run ended before this tool call's result was recorded. This tool is " +
"read-only/idempotent, so its outcome carries no risk: simply call it again if you still need " +
"the result.";
const INTERRUPTED_IDEMPOTENT = "[INTERRUPTED] The previous run ended before this tool call's result was recorded. The call may or " +
"may not have taken effect. It is safe to REPLAY: re-issuing the same call converges the state to " +
"what you intended. If you decide not to re-issue it, verify the current state first.";
const INTERRUPTED_NEVER_STARTED = "[INTERRUPTED] The run was aborted before this tool call started. It was never executed and had " +
"no side effects. If the run was stopped by a pending approval or an unresolved gate, wait for it " +
"to be resolved before re-issuing; re-issuing is otherwise safe if you still need this call.";
const INTERRUPTED_REPEAT_UNKNOWN = "[INTERRUPTED] Same as an earlier interrupted call in this batch: no result was recorded and the outcome is " +
"UNKNOWN — verify before relying on it.";
const INTERRUPTED_REPEAT_SAFE = "[INTERRUPTED] Same as an earlier interrupted call in this batch: this tool is read-only/idempotent, so just " +
"call it again if you still need the result.";
const INTERRUPTED_REPEAT_IDEMPOTENT = "[INTERRUPTED] Same as an earlier interrupted call in this batch: it is safe to REPLAY — re-issue it, or verify " +
"the current state if you decide not to.";
const INTERRUPTED_REPEAT_NEVER_STARTED = "[INTERRUPTED] Same as an earlier interrupted call in this batch: it never started and had no side effects — " +
"re-issuing is safe once whatever stopped the run is resolved.";
const REPEAT_TEXT = {
never_started: INTERRUPTED_REPEAT_NEVER_STARTED,
read: INTERRUPTED_REPEAT_SAFE,
idempotent: INTERRUPTED_REPEAT_IDEMPOTENT,
unknown: INTERRUPTED_REPEAT_UNKNOWN,
};
export function findOrphanToolCalls(messages, suspendedBatch) {
const resolvedAt = new Map();
const callSitesAt = new Map();
messages.forEach((m, i) => {
if (m.role === "toolResult") {
const at = resolvedAt.get(m.toolCallId);
if (at === undefined)
resolvedAt.set(m.toolCallId, [i]);
else
at.push(i);
}
else if (m.role === "assistant") {
for (const part of m.content) {
if (part.type !== "toolCall")
continue;
const at = callSitesAt.get(part.id);
if (at === undefined)
callSitesAt.set(part.id, [i]);
else
at.push(i);
}
}
});
const nextReuseAfter = (id, from) => callSitesAt.get(id)?.find((j) => j > from) ?? Number.POSITIVE_INFINITY;
const orphans = [];
messages.forEach((m, i) => {
if (m.role === "assistant") {
for (const part of m.content) {
if (part.type !== "toolCall" || suspendedBatch?.has(part.id))
continue;
const bound = nextReuseAfter(part.id, i);
const closed = (resolvedAt.get(part.id) ?? []).some((at) => at > i && at < bound);
if (!closed)
orphans.push({ toolCallId: part.id, toolName: part.name });
}
}
else if (m.role === "toolResult") {
if (suspendedBatch?.has(m.toolCallId))
return;
const sites = callSitesAt.get(m.toolCallId) ?? [];
const results = resolvedAt.get(m.toolCallId) ?? [];
let paired = false;
for (const j of sites) {
if (j >= i)
break;
if (!results.some((at) => at > j && at < i)) {
paired = true;
break;
}
}
if (!paired)
orphans.push({ toolCallId: m.toolCallId, toolName: m.toolName, kind: "result" });
}
});
return orphans;
}
export const INTERRUPTED_BY_USER_MARKER = "[Request interrupted by user]";
export const INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER = "[Request interrupted by user for tool use]";
export async function appendInterruptionMarker(session, opts) {
return await session.appendMessage({
role: "user",
content: [
{
type: "text",
text: opts.toolUseInFlight ? INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER : INTERRUPTED_BY_USER_MARKER,
},
],
provenance: "engine-note",
timestamp: Date.now(),
});
}
export function interruptedResults(calls, toolEffects, startedToolCallIds) {
const statedInFull = new Set();
return calls.map(({ toolCallId, toolName }) => {
const effect = toolEffects?.get(toolName) ?? "write";
const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(toolCallId);
const cls = neverStarted ? "never_started" : effect === "read" ? "read" : effect === "idempotent" ? "idempotent" : "unknown";
const text = statedInFull.has(cls) ? REPEAT_TEXT[cls] : neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
statedInFull.add(cls);
return { toolCallId, toolName, text, errorKind: neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown" };
});
}
export function isInterruptedClose(message) {
const kind = readMember(readMember(message, "details"), "errorKind");
return kind === "interrupted_never_started" || kind === "interrupted_outcome_unknown";
}
export function interruptedResultMessage(result, timestamp) {
return {
role: "toolResult",
toolCallId: result.toolCallId,
toolName: result.toolName,
content: [{ type: "text", text: result.text }],
details: { errorKind: result.errorKind, ...(closeNeverExecuted(result.errorKind) ? { notExecuted: true } : {}) },
isError: true,
timestamp,
};
}
function closableByAppend(messages, suspendedBatch) {
const lastSite = new Map();
const lastResult = new Map();
let order = 0;
messages.forEach((m, i) => {
if (m.role === "assistant") {
for (const part of m.content) {
if (part.type === "toolCall" && !suspendedBatch?.has(part.id) && lastSite.get(part.id)?.at !== i)
lastSite.set(part.id, { at: i, order: order++, toolName: part.name, args: part.arguments });
}
}
else if (m.role === "toolResult") {
lastResult.set(m.toolCallId, i);
}
});
return [...lastSite]
.filter(([id, site]) => (lastResult.get(id) ?? -1) < site.at)
.sort((a, b) => a[1].order - b[1].order)
.map(([toolCallId, site]) => ({ toolCallId, toolName: site.toolName, args: site.args }));
}
export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
const { messages } = await session.buildContext();
const orphans = closableByAppend(messages, suspendedBatch);
const recovered = [];
const results = interruptedResults(orphans, toolEffects, startedToolCallIds);
for (const [i, result] of results.entries()) {
const entryId = await session.appendMessage(interruptedResultMessage(result, Date.now()));
recovered.push({ toolCallId: result.toolCallId, toolName: result.toolName, entryId, text: result.text, errorKind: result.errorKind, args: orphans[i].args });
}
return { recovered };
}