UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

124 lines (123 loc) • 4.58 kB
import { mintGateOutcome } from "./gate-exit.js"; export const occurrenceClock = () => (typeof performance !== "undefined" ? performance.now() : Date.now()); const TERMINAL = { announced: false, released: false, settled: true, interrupted: true }; export function createToolOccurrenceTable() { const byId = new Map(); const attempts = new Map(); let listener; const held = (toolCallId, verb) => { const occ = byId.get(toolCallId); if (occ === undefined) throw new Error(`tool occurrence: ${verb} for call "${toolCallId}", which this leg does not hold`); return occ; }; const open = (start) => { const attempt = (attempts.get(start.toolCallId) ?? 0) + 1; attempts.set(start.toolCallId, attempt); const now = occurrenceClock(); const occ = { toolCallId: start.toolCallId, spelledName: start.spelledName, ...(start.wireName !== undefined ? { wireName: start.wireName } : {}), ...(start.label !== undefined ? { label: start.label } : {}), args: start.args, attempt, life: "announced", awaitingUser: 0, gateRounds: [], startEmitted: false, startedAt: now, lastHeardAt: now, beating: true, }; byId.delete(start.toolCallId); byId.set(start.toolCallId, occ); listener?.(); return occ; }; const awaitUser = (toolCallId) => { const occ = byId.get(toolCallId); if (occ === undefined) return () => { }; occ.awaitingUser += 1; let returned = false; return () => { if (returned) return; returned = true; if (byId.get(toolCallId) !== occ) return; occ.awaitingUser -= 1; if (occ.awaitingUser === 0) occ.lastHeardAt = occurrenceClock(); }; }; return { awaitUser, whileAwaitingUser(toolCallId, ask) { const resumeFromUser = awaitUser(toolCallId); let answer; try { answer = ask(); } catch (err) { resumeFromUser(); throw err; } if (answer instanceof Promise) answer.then(resumeFromUser, resumeFromUser); else resumeFromUser(); return answer; }, get: (toolCallId) => byId.get(toolCallId), live: () => [...byId.values()], open, ensure: (start) => byId.get(start.toolCallId) ?? open(start), noteStartEmitted(toolCallId) { held(toolCallId, "a start frame").startEmitted = true; }, release(toolCallId) { const occ = held(toolCallId, "release"); if (occ.life !== "announced") throw new Error(`tool occurrence: call "${toolCallId}" (attempt ${occ.attempt}) cannot be released from "${occ.life}"`); occ.life = "released"; }, gateRound(toolCallId, outcome, finalInput) { const occ = held(toolCallId, "a gate round"); occ.gateRounds.push(outcome); if (finalInput !== undefined) occ.finalInput = finalInput; }, refuse(toolCallId, layer) { const occ = held(toolCallId, "a refusal"); const latest = occ.gateRounds.at(-1); const round = mintGateOutcome({ deniedBy: layer, ...(latest?.settlement !== undefined && latest.origin !== undefined ? { settled: { settlement: latest.settlement, origin: latest.origin } } : {}) }); occ.gateRounds.push(round); return round; }, heard(toolCallId) { const occ = byId.get(toolCallId); if (occ !== undefined) occ.lastHeardAt = occurrenceClock(); }, close(toolCallId, life) { const occ = byId.get(toolCallId); if (occ === undefined) return undefined; if (TERMINAL[occ.life]) throw new Error(`tool occurrence: call "${toolCallId}" (attempt ${occ.attempt}) is already ${occ.life}`); occ.life = life; occ.beating = false; byId.delete(toolCallId); return occ; }, quiesce() { for (const occ of byId.values()) occ.beating = false; }, onOpen(l) { listener = l; }, }; }