UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

216 lines (215 loc) • 9.06 kB
import { assertRetentionPolicy } from "./retention-policy.js"; export const MAILBOX_PEER_FROM_MODES = Object.freeze(["bypass", "prompting"]); export const MAILBOX_PEER_RECORD_KINDS = Object.freeze(["peer_message", "idle_notice", "delivery_notice", "idle_subscription", "engine_frame"]); export const MAILBOX_PEER_NOTICE_STATES = Object.freeze(["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"]); export const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta"; export const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe"; const PEER_META_STRING_KEYS = ["fromSession", "fromRoot", "senderKey", "fromScope", "gateReceiptId"]; export function readMailboxPeerMeta(raw) { if (raw === undefined) return undefined; const refuse = (why) => { throw new MailboxStoreError(MAILBOX_INVALID_PEER_META_CODE, `MailboxStore.append: peerMeta ${why}`); }; if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return refuse("must be a plain object"); const proto = Object.getPrototypeOf(raw); if (proto !== Object.prototype && proto !== null) return refuse("must be a plain object (a class instance, Date, Map or similar is not)"); const out = {}; for (const key of Reflect.ownKeys(raw)) { if (typeof key !== "string") return refuse("carries a symbol-keyed member"); const k = key; const v = raw[k]; if (v === undefined) continue; if (k === "fromMode") { if (!MAILBOX_PEER_FROM_MODES.includes(v)) return refuse(`fromMode must be one of ${MAILBOX_PEER_FROM_MODES.join("|")}`); out.fromMode = v; } else if (k === "kind") { if (!MAILBOX_PEER_RECORD_KINDS.includes(v)) return refuse(`kind must be one of ${MAILBOX_PEER_RECORD_KINDS.join("|")}`); out.kind = v; } else if (PEER_META_STRING_KEYS.includes(k)) { if (typeof v !== "string" || v === "") return refuse(`${k} must be a non-empty string`); out[k] = v; } else if (k === "notice") { out.notice = readMailboxPeerNoticeMeta(v, refuse); } else if (k === "urgent") { if (typeof v !== "boolean") return refuse("urgent must be a boolean"); out.urgent = v; } else { return refuse(`carries an unknown key ${JSON.stringify(k)}`); } } return out; } function readMailboxPeerNoticeMeta(raw, refuse) { if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return refuse("notice must be a plain object"); const proto = Object.getPrototypeOf(raw); if (proto !== Object.prototype && proto !== null) return refuse("notice must be a plain object (a class instance, Date, Map or similar is not)"); const out = {}; for (const key of Reflect.ownKeys(raw)) { if (typeof key !== "string") return refuse("notice carries a symbol-keyed member"); const v = raw[key]; if (v === undefined) continue; if (key === "state") { if (!MAILBOX_PEER_NOTICE_STATES.includes(v)) return refuse(`notice.state must be one of ${MAILBOX_PEER_NOTICE_STATES.join("|")}`); out.state = v; } else if (key === "recipient" || key === "dropReason" || key === "detail" || key === "target") { if (typeof v !== "string" || v === "") return refuse(`notice.${key} must be a non-empty string`); out[key] = v; } else if (key === "refSeq" || key === "finishedAt") { if (typeof v !== "number" || !Number.isFinite(v) || v < 0) return refuse(`notice.${key} must be a non-negative finite number`); out[key] = v; } else { return refuse(`notice carries an unknown key ${JSON.stringify(key)}`); } } if (out.state === undefined) return refuse("notice.state is required"); return out; } export function cloneMailboxPeerMeta(meta) { return meta === undefined ? undefined : { ...meta, ...(meta.notice !== undefined ? { notice: { ...meta.notice } } : {}) }; } export const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tombstoned"; export class MailboxStoreError extends Error { code; constructor(code, message) { super(message); this.code = code; this.name = "MailboxStoreError"; } } export function mailboxCrossProcessMountVerdict(store) { const declared = store.crossProcessSafe; if (declared === true) return { ok: true }; const reason = declared === undefined ? "the mailbox backend does not declare cross-process safety (crossProcessSafe is absent): several terminal sessions would share one session box on luck" : declared === false ? "the mailbox backend declares crossProcessSafe: false (process-local by design): it cannot serve a session box shared across OS processes" : `the mailbox backend's crossProcessSafe declaration is malformed (${typeof declared}, expected a boolean)`; return { ok: false, code: MAILBOX_CROSS_PROCESS_UNSAFE_CODE, reason }; } export function newestSentAt(messages) { let newest; for (const m of messages) { if (!Number.isFinite(m.sentAt)) return undefined; if (newest === undefined || m.sentAt > newest) newest = m.sentAt; } return newest; } export function detachMailboxMessage(m) { return { ...m, ...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}), ...(m.peerMeta !== undefined ? { peerMeta: cloneMailboxPeerMeta(m.peerMeta) } : {}), }; } export class InMemoryMailboxStore { crossProcessSafe = false; boxes = new Map(); key(scope, handle) { if (handle.includes("\u0000")) { throw new Error(`MailboxStore: unsafe handle ${JSON.stringify(handle)} — a NUL byte makes the composite key ambiguous`); } return `${scope}\u0000${handle.toLowerCase()}`; } box(scope, handle) { const k = this.key(scope, handle); let b = this.boxes.get(k); if (!b) { b = { scope, messages: [], nextSeq: 1 }; this.boxes.set(k, b); } return b; } async append(scope, handle, msg) { if (scope === undefined || scope === "") throw new Error("MailboxStore.append: refusing a message without a scope"); const peerMeta = readMailboxPeerMeta(msg.peerMeta); const b = this.box(scope, handle); const seq = b.nextSeq++; b.messages.push({ seq, ...(msg.from !== undefined ? { from: msg.from } : {}), content: msg.content, sentAt: msg.sentAt, ...(msg.hopChain !== undefined ? { hopChain: [...msg.hopChain] } : {}), ...(peerMeta !== undefined ? { peerMeta } : {}), }); return seq; } async claimLease(scope, handle, owner, ttlMs, now = Date.now()) { const b = this.boxes.get(this.key(scope, handle)); if (!b || b.messages.length === 0) return null; if (b.lease !== undefined && b.lease.expiresAt > now && b.lease.owner !== owner) return null; const maxSeq = b.messages[b.messages.length - 1].seq; b.lease = { owner, expiresAt: now + ttlMs, maxSeq }; return { messages: b.messages.map((m) => detachMailboxMessage(m)), maxSeq }; } async ack(scope, handle, owner, upToSeq) { const b = this.boxes.get(this.key(scope, handle)); if (!b) return { acked: false }; if (b.lease === undefined || b.lease.owner !== owner) return { acked: false }; b.messages = b.messages.filter((m) => m.seq > upToSeq); if (b.lease.maxSeq <= upToSeq) b.lease = undefined; return { acked: true }; } async releaseLease(scope, handle, owner) { const b = this.boxes.get(this.key(scope, handle)); if (b?.lease?.owner === owner) b.lease = undefined; } async peekCount(scope, handle) { return this.boxes.get(this.key(scope, handle))?.messages.length ?? 0; } async drop(scope, handle) { this.boxes.delete(this.key(scope, handle)); } async reap(scope, now, opts) { assertRetentionPolicy("MailboxStore.reap", opts); if (opts?.maxAgeMs === undefined) return 0; let dropped = 0; for (const b of this.boxes.values()) { if (b.scope !== scope) continue; const newest = newestSentAt(b.messages); if (newest !== undefined && newest < now - opts.maxAgeMs) { b.messages = []; b.lease = undefined; dropped++; } } return dropped; } }