UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

146 lines (145 loc) • 6.4 kB
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 }; }