UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

261 lines (260 loc) • 8.99 kB
import { PEER_HELD_QUEUE_CAP, PEER_HELD_REVIEW_CAUSES } from "./peer-notices.js"; import { peerSessionBoxHandle } from "./peer-directory.js"; export const realPeerClock = { now: () => Date.now(), setTimeout: (fn, ms) => { const h = setTimeout(fn, ms); h.unref?.(); return h; }, clearTimeout: (h) => clearTimeout(h), }; const crashHandlers = new WeakMap(); export const PEER_HELD_DECISION_MEMORY = PEER_HELD_QUEUE_CAP * 4; export function createPeerHeldQueue(key, opts = {}) { const cap = opts.cap ?? PEER_HELD_QUEUE_CAP; const clock = opts.clock ?? realPeerClock; const entries = []; const deadlines = new Map(); const decisions = new Map(); let cfg; const sinkOrThrow = () => { if (cfg === undefined) throw new Error("PeerHeldQueue: not configured — bind a run (configure) before holding messages"); return cfg.sink; }; const safe = (f) => { try { f?.(); } catch { } }; const clearDeadline = (seq) => { const h = deadlines.get(seq); if (h !== undefined) { clock.clearTimeout(h); deadlines.delete(seq); } }; const remove = (entry) => { const i = entries.indexOf(entry); if (i !== -1) entries.splice(i, 1); clearDeadline(entry.seq); }; const settleOut = (entry, settlement, reason, receipt) => { const sink = sinkOrThrow(); remove(entry); if (settlement !== "delivered") { decisions.set(entry.seq, settlement); while (decisions.size > PEER_HELD_DECISION_MEMORY) { const oldest = decisions.keys().next(); if (oldest.done === true) break; decisions.delete(oldest.value); } } if (receipt !== undefined) safe(() => sink.receipt(entry, receipt)); safe(() => sink.onSettled?.(entry, settlement, reason, entries.length)); }; const armDeadline = (entry) => { if (cfg === undefined || cfg.dialogExpiryMs === null || deadlines.has(entry.seq)) return; if (entry.released === true) return; if (!PEER_HELD_REVIEW_CAUSES.includes(entry.cause)) return; const h = clock.setTimeout(() => { deadlines.delete(entry.seq); if (!entries.includes(entry)) return; rejudge(); if (!entries.includes(entry)) return; settleOut(entry, "expired", "deadline", "expired"); }, cfg.dialogExpiryMs); deadlines.set(entry.seq, h); }; const tryDeliver = (entry, reason) => { const sink = sinkOrThrow(); if (sink.inject === undefined) return "no_leg"; try { if (sink.inject(entry) === "deferred") return "no_leg"; } catch { settleOut(entry, "dropped", "inject_failed", "dropped"); return "dropped"; } settleOut(entry, "delivered", reason, "delivered"); return "delivered"; }; const reconcile = (present) => { const sink = sinkOrThrow(); let retired = 0; for (const entry of [...entries]) { if (present.has(entry.seq)) continue; remove(entry); retired += 1; safe(() => sink.onSettled?.(entry, "expired", "superseded", entries.length)); } return retired; }; const rejudge = () => { const sink = sinkOrThrow(); let released = 0; let refused = 0; const reannounce = []; for (const entry of [...entries]) { let verdict; try { verdict = sink.judge(entry); } catch { continue; } if (verdict.verdict === "refuse") { settleOut(entry, "refused", "rejudge", "refused"); refused += 1; continue; } if (verdict.verdict === "deliver" || entry.released === true) { const r = tryDeliver(entry, entry.released === true ? "approved" : "rejudge"); if (r === "no_leg") entry.released = true; else if (r === "delivered") released += 1; continue; } if (verdict.cause !== entry.cause) { entry.cause = verdict.cause; reannounce.push([entry, verdict.cause]); clearDeadline(entry.seq); armDeadline(entry); } } for (const [entry, cause] of reannounce) safe(() => sink.onHeld?.(entry, entries.length, cause, true)); return { released, refused, held: entries.length }; }; const queue = { key, configure(next) { cfg = next; for (const entry of entries) { if (next.dialogExpiryMs === null) clearDeadline(entry.seq); else armDeadline(entry); } }, detachLeg() { if (cfg === undefined) return; const { inject: _inject, ...rest } = cfg.sink; void _inject; cfg = { ...cfg, sink: rest }; }, configured: () => cfg !== undefined, hold(input) { const sink = sinkOrThrow(); if (entries.some((e) => e.box === input.box && e.seq === input.seq)) return; const entry = { ...input, heldAt: clock.now() }; if (entries.length >= cap) { const oldest = entries[0]; settleOut(oldest, "expired", "evicted", "expired"); } entries.push(entry); armDeadline(entry); safe(() => sink.onHeld?.(entry, entries.length, entry.cause, false)); safe(() => sink.receipt(entry, "held")); }, rejudge, reconcile, settle(seq, decision) { const sink = sinkOrThrow(); const entry = entries.find((e) => e.seq === seq); if (entry === undefined) return "gone"; if (decision === "deny") { settleOut(entry, "denied", "denied", "denied"); return "denied"; } if (decision === "cancel") { settleOut(entry, "expired", "cancelled", "expired"); return "expired"; } let verdict; try { verdict = sink.judge(entry); } catch { entry.released = true; return "released_pending"; } if (verdict.verdict === "refuse") { settleOut(entry, "refused", "approved", "refused"); return "dropped_by_policy"; } const r = tryDeliver(entry, "approved"); if (r === "no_leg") { entry.released = true; clearDeadline(entry.seq); return "released_pending"; } return r; }, list: () => entries.map((e) => ({ ...e, ...(e.hopChain !== undefined ? { hopChain: [...e.hopChain] } : {}), ...(e.peerMeta !== undefined ? { peerMeta: { ...e.peerMeta } } : {}) })), size: () => entries.length, decisions: () => decisions, forgetDecisions(seqs) { for (const seq of seqs) decisions.delete(seq); }, shutdown() { if (cfg === undefined) { entries.length = 0; for (const h of deadlines.values()) clock.clearTimeout(h); deadlines.clear(); return; } for (const entry of [...entries]) settleOut(entry, "expired", "shutdown", "expired"); }, }; crashHandlers.set(queue, () => { decisions.clear(); entries.length = 0; for (const h of deadlines.values()) clock.clearTimeout(h); deadlines.clear(); }); return queue; } const heldQueues = new Map(); export function peerHeldQueueKey(scope, sessionId) { return JSON.stringify([scope, peerSessionBoxHandle(sessionId)]); } export function peerHeldQueueFor(scope, sessionId, opts = {}) { const key = peerHeldQueueKey(scope, sessionId); let q = heldQueues.get(key); if (q === undefined) { q = createPeerHeldQueue(key, opts); heldQueues.set(key, q); } return q; } export function peerHeldQueueIfAny(scope, sessionId) { return heldQueues.get(peerHeldQueueKey(scope, sessionId)); } export function resetPeerHeldQueuesForTests() { for (const q of heldQueues.values()) crashHandlers.get(q)?.(); heldQueues.clear(); }