@sema-agent/core
Version:
Stateless, task-oriented AI agent core
146 lines (145 loc) • 6.4 kB
JavaScript
import { interruptedResultMessage, interruptedResults, isInterruptedClose } from "../../core/session-reconcile.js";
export const TOOL_TURN_REPAIR_FORMS = Object.freeze(["trailing_text", "late_result", "missing_result", "duplicate_result", "duplicate_call", "orphan_result"]);
export const TOOL_TURN_REPAIR_DROPS = {
trailing_text: false,
late_result: false,
missing_result: false,
duplicate_result: false,
duplicate_call: true,
orphan_result: true,
};
export const TOOL_TURN_INCOMPLETE_CAUSES = Object.freeze(["truncated", "unnamed", "bad_frame", "missing_result_synthesized"]);
const STREAM_DROPS_DIAGNOSTIC = "stream_drops";
const STREAM_DROP_CAUSES = new Set(["truncated", "unnamed", "bad_frame"]);
export function stampStreamDrops(message, drops) {
if (drops.length === 0)
return;
message.diagnostics = [...(message.diagnostics ?? []), { type: STREAM_DROPS_DIAGNOSTIC, timestamp: Date.now(), details: { drops: drops.map(({ cause, count }) => ({ cause, count })) } }];
}
export function streamDropsOf(message) {
const out = [];
try {
const diagnostics = message.diagnostics;
if (!Array.isArray(diagnostics))
return out;
const entries = diagnostics;
for (const entry of entries) {
if (typeof entry !== "object" || entry === null || entry.type !== STREAM_DROPS_DIAGNOSTIC)
continue;
const drops = (entry.details ?? {}).drops;
if (!Array.isArray(drops))
continue;
const items = drops;
for (const d of items) {
const cause = d?.cause;
const count = d?.count;
if (typeof cause === "string" && STREAM_DROP_CAUSES.has(cause) && typeof count === "number" && Number.isInteger(count) && count > 0)
out.push({ cause: cause, count });
}
}
}
catch {
return [];
}
return out;
}
function callsOf(m) {
return m.role === "assistant" && Array.isArray(m.content) ? m.content.filter((c) => c?.type === "toolCall") : [];
}
export const SECOND_RESULT_SEPARATOR = "[another result was recorded for this same call:]";
export function ensureToolTurnInvariants(messages, priorMessages) {
const anchor = [];
messages.forEach((m, i) => anchor.push(m.role === "toolResult" && i > 0 ? anchor[i - 1] : i));
const sites = new Map();
const ownerOf = new Map();
const resultsOf = new Map();
const orphans = new Set();
messages.forEach((m, i) => {
if (m.role === "assistant") {
for (const call of callsOf(m)) {
const list = sites.get(call.id) ?? [];
if (list.at(-1) !== i)
list.push(i);
sites.set(call.id, list);
}
return;
}
if (m.role !== "toolResult")
return;
const all = sites.get(m.toolCallId) ?? [];
if (all.length === 0) {
orphans.add(i);
return;
}
const owner = all.at(-1);
ownerOf.set(i, owner);
const batch = resultsOf.get(owner);
if (batch === undefined)
resultsOf.set(owner, [i]);
else
batch.push(i);
});
const repairs = [];
const out = [];
messages.forEach((m, i) => {
if (ownerOf.has(i) || orphans.has(i))
return;
const calls = callsOf(m);
if (calls.length === 0) {
out.push(m);
return;
}
const assistant = m;
const byId = new Map();
const repeated = [];
for (const call of calls) {
if (!byId.has(call.id))
byId.set(call.id, call);
else if (!repeated.includes(call.id))
repeated.push(call.id);
}
const ids = [...byId.keys()];
const firstCall = assistant.content.findIndex((c) => c?.type === "toolCall");
const trails = assistant.content.slice(firstCall).some((c) => c?.type !== "toolCall");
out.push(trails || repeated.length > 0 ? { ...assistant, content: [...assistant.content.filter((c) => c?.type !== "toolCall"), ...byId.values()] } : assistant);
if (trails)
repairs.push({ form: "trailing_text", toolCallIds: ids });
if (repeated.length > 0)
repairs.push({ form: "duplicate_call", toolCallIds: repeated });
const recorded = new Map();
for (const r of resultsOf.get(i) ?? []) {
const id = messages[r].toolCallId;
recorded.set(id, [...(recorded.get(id) ?? []), r]);
}
const sent = new Map();
const late = [];
const duplicate = [];
for (const [id, at] of recorded) {
const real = at.filter((r) => !isInterruptedClose(messages[r]));
const chosen = real.length > 0 ? real : at.slice(0, 1);
sent.set(id, chosen.map((r) => messages[r]).reduce(foldResults));
if (anchor[chosen[0]] !== i)
late.push(id);
if (at.length > 1)
duplicate.push(id);
}
out.push(...sent.values());
const missing = [...byId.values()].filter((c) => !sent.has(c.id));
for (const result of interruptedResults(missing.map((c) => ({ toolCallId: c.id, toolName: c.name })))) {
out.push(interruptedResultMessage(result, assistant.timestamp));
}
const inCallOrder = (set) => ids.filter((id) => set.includes(id));
if (late.length > 0)
repairs.push({ form: "late_result", toolCallIds: inCallOrder(late) });
if (missing.length > 0)
repairs.push({ form: "missing_result", toolCallIds: missing.map((c) => c.id), ...(priorMessages?.has(m) === true ? { beforeRun: true } : {}) });
if (duplicate.length > 0)
repairs.push({ form: "duplicate_result", toolCallIds: inCallOrder(duplicate) });
});
if (orphans.size > 0)
repairs.push({ form: "orphan_result", toolCallIds: [...new Set([...orphans].map((i) => messages[i].toolCallId))] });
return repairs.length === 0 ? { messages, repairs } : { messages: out, repairs };
}
function foldResults(first, second) {
return { ...first, content: [...first.content, { type: "text", text: SECOND_RESULT_SEPARATOR }, ...second.content], isError: first.isError && second.isError };
}