@sema-agent/core
Version:
Stateless, task-oriented AI agent core
241 lines (240 loc) • 8.16 kB
JavaScript
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();
}