UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

241 lines (240 loc) • 8.16 kB
import { PEER_IDLE_FIRE_DEBOUNCE_MS, PEER_IDLE_HELD_BACKOFF_MS, PEER_IDLE_OUTSTANDING_CAP, PEER_IDLE_PRIORS_KEPT, PEER_IDLE_SUBSCRIBER_TABLE_CAP, PEER_IDLE_SUBSCRIPTION_TTL_MS } from "./peer-notices.js"; import { realPeerClock } from "./peer-held-queue.js"; import { peerSessionBoxHandle } from "./peer-directory.js"; export function createPeerIdleTarget(opts = {}) { const clock = opts.clock ?? realPeerClock; const cap = opts.tableCap ?? PEER_IDLE_SUBSCRIBER_TABLE_CAP; const ttl = opts.ttlMs ?? PEER_IDLE_SUBSCRIPTION_TTL_MS; const subs = []; let sink; let idleState; let pending; let exitedFlag = false; const safe = (f) => { try { f?.(); } catch { } }; const cancelPending = () => { if (pending !== undefined) { clock.clearTimeout(pending); pending = undefined; } }; const sweep = (now = clock.now()) => { let n = 0; for (let i = subs.length - 1; i >= 0; i--) { if (now - subs[i].requestedAt > ttl) { subs.splice(i, 1); n += 1; } } return n; }; const fire = (kind, finishedAt) => { if (sink === undefined || subs.length === 0) return; sweep(); const batch = subs.splice(0, subs.length); if (batch.length === 0) return; let detail; if (kind === "idle") { try { detail = sink.lastTurnText?.(); } catch { detail = undefined; } } for (const sub of batch) { let deliverDetail = false; if (detail !== undefined) { try { deliverDetail = sink.judge(sub).verdict === "deliver"; } catch { deliverDetail = false; } } const notice = { kind, ...(finishedAt !== undefined ? { finishedAt } : {}), ...(deliverDetail && detail !== undefined ? { detail } : {}) }; safe(() => sink.notify(sub, notice)); } }; const schedule = (delay) => { cancelPending(); pending = clock.setTimeout(() => { pending = undefined; if (idleState === undefined || sink === undefined || subs.length === 0) return; let held = 0; try { held = sink.heldCount?.() ?? 0; } catch { held = 0; } if (held > 0) { schedule(PEER_IDLE_HELD_BACKOFF_MS); return; } fire("idle", idleState.finishedAt); }, delay); }; return { configure(next) { sink = next; exitedFlag = false; }, configured: () => sink !== undefined, subscribe(sub) { if (exitedFlag) return "full"; sweep(); const key = sub.requester.toLowerCase(); const i = subs.findIndex((s) => s.requester.toLowerCase() === key); let outcome; if (i !== -1) { subs.splice(i, 1); outcome = "refreshed"; } else { if (subs.length >= cap) return "full"; outcome = "recorded"; } subs.push(sub); safe(() => sink?.onSubscribed?.(sub, outcome, subs.length)); if (idleState !== undefined) schedule(PEER_IDLE_FIRE_DEBOUNCE_MS); return outcome; }, sweep, busy() { idleState = undefined; cancelPending(); }, idle(finishedAt) { idleState = { finishedAt }; if (subs.length > 0) schedule(PEER_IDLE_FIRE_DEBOUNCE_MS); }, exited(finishedAt) { exitedFlag = true; cancelPending(); let held = 0; try { held = sink?.heldCount?.() ?? 0; } catch { held = 0; } if (idleState !== undefined && held === 0) fire("idle", idleState.finishedAt); else fire("exited", finishedAt ?? clock.now()); idleState = undefined; }, list: () => subs.map((s) => ({ ...s })), size: () => subs.length, }; } export function createPeerIdleRequester(opts = {}) { const clock = opts.clock ?? realPeerClock; const cap = opts.cap ?? PEER_IDLE_OUTSTANDING_CAP; const priorsKept = opts.priorsKept ?? PEER_IDLE_PRIORS_KEPT; const ttl = opts.ttlMs ?? PEER_IDLE_SUBSCRIPTION_TTL_MS; const outstanding = []; let sink; const fold = (s) => s.toLowerCase(); const drop = (o) => { const i = outstanding.indexOf(o); if (i !== -1) outstanding.splice(i, 1); clock.clearTimeout(o.handle); }; const forTarget = (target) => outstanding.filter((o) => fold(o.target) === fold(target)); return { configure(next) { sink = next; }, configured: () => sink !== undefined, request(target, label) { const priors = forTarget(target); while (priors.length >= priorsKept) drop(priors.shift()); if (priors.length === 0) { const distinct = new Set(outstanding.map((o) => fold(o.target))); if (distinct.size >= cap) return { ok: false, reason: "cap" }; } const entry = { target, label, requestedAt: clock.now(), handle: undefined, }; entry.handle = clock.setTimeout(() => { if (!outstanding.includes(entry)) return; drop(entry); if (outstanding.some((o) => fold(o.target) === fold(entry.target) && o.requestedAt > entry.requestedAt)) return; for (const o of forTarget(entry.target)) drop(o); try { sink?.expired({ target: entry.target, label: entry.label, requestedAt: entry.requestedAt }); } catch { } }, ttl); outstanding.push(entry); return { ok: true, priors: priors.length }; }, settle(target, kind) { const mine = forTarget(target); if (mine.length === 0) return { cleared: 0 }; const label = mine[mine.length - 1].label; if (kind === "unavailable") { drop(mine[mine.length - 1]); return { cleared: 1, label }; } for (const o of mine) drop(o); return { cleared: mine.length, label }; }, labelFor: (target) => { const mine = forTarget(target); return mine.length === 0 ? undefined : mine[mine.length - 1].label; }, list: () => outstanding.map(({ handle: _h, ...o }) => o), size: () => outstanding.length, }; } const machines = new Map(); export function peerIdleMachineKey(scope, sessionId) { return JSON.stringify([scope, peerSessionBoxHandle(sessionId)]); } export function peerIdleMachineFor(scope, sessionId, opts = {}) { const key = peerIdleMachineKey(scope, sessionId); let m = machines.get(key); if (m === undefined) { m = { key, target: createPeerIdleTarget(opts), requester: createPeerIdleRequester(opts) }; machines.set(key, m); } return m; } export function peerIdleMachineIfAny(scope, sessionId) { return machines.get(peerIdleMachineKey(scope, sessionId)); } export function resetPeerIdleMachinesForTests() { for (const m of machines.values()) { m.target.busy(); for (const o of m.requester.list()) m.requester.settle(o.target, "idle"); } machines.clear(); }