@sema-agent/core
Version:
Stateless, task-oriented AI agent core
171 lines (170 loc) • 5.95 kB
JavaScript
import { mkdirSync, readFileSync } from "node:fs";
import { assertRetentionPolicy } from "../core/retention-policy.js";
import { atomicWriteFile } from "../stores/file/fs-atomic.js";
import { dirname } from "node:path";
import { normalizeAgentName } from "../core/task-registry.js";
import { createSafeNotifier } from "../core/safe-notify.js";
export function entryAccessible(e, access) {
if (e.scope === undefined || access.scope === undefined || access.scope !== e.scope)
return false;
if (e.owner === undefined)
return false;
if (access.owner === e.owner)
return true;
if (e.sessionScoped === true && access.sessionId !== undefined && access.sessionId === e.owner)
return true;
return false;
}
function latestOf(pool) {
let best;
for (const e of pool)
if (best === undefined || e.createdAt >= best.createdAt)
best = e;
return best;
}
function resolveIn(entries, name, access) {
const key = normalizeAgentName(name);
if (key === "")
return undefined;
return latestOf(entries.filter((e) => normalizeAgentName(e.name) === key && entryAccessible(e, access)));
}
function freshOnly(entries, maxAgeMs, now = Date.now()) {
if (maxAgeMs === undefined)
return entries;
return entries.filter((e) => now - e.createdAt <= maxAgeMs);
}
function capped(entries, opts) {
const max = opts?.maxEntries;
if (max === undefined || entries.length <= max)
return entries;
const notifier = createSafeNotifier(opts?.onNotifyError !== undefined ? { onError: opts.onNotifyError } : undefined);
const byAge = [...entries].sort((a, b) => a.createdAt - b.createdAt);
const dropCount = Math.max(0, entries.length - Math.max(0, max));
const dropped = new Set(byAge.slice(0, dropCount));
for (const e of dropped) {
notifier.notify(() => opts?.onEvicted?.(e), "rosterStore.onEvicted");
}
return entries.filter((e) => !dropped.has(e));
}
export class MemoryRosterStore {
entries = [];
maxAgeMs;
gc;
constructor(opts) {
assertRetentionPolicy("RosterStore", opts);
this.maxAgeMs = opts?.maxAgeMs;
this.gc = opts;
}
record(entry) {
const key = upsertKey(entry);
this.entries = freshOnly(this.entries, this.maxAgeMs).filter((e) => upsertKey(e) !== key);
this.entries.push({ ...entry });
this.entries = capped(this.entries, this.gc);
}
resolve(name, access) {
return resolveIn(freshOnly(this.entries, this.maxAgeMs), name, access);
}
list() {
return freshOnly(this.entries, this.maxAgeMs).map((e) => ({ ...e }));
}
releaseAgent(agentId) {
this.entries = this.entries.filter((e) => e.agentId !== agentId);
}
}
function upsertKey(e) {
return `${normalizeAgentName(e.name)}\u0000${e.owner ?? ""}\u0000${e.scope ?? ""}`;
}
export class FileRosterStore {
path;
maxAgeMs;
gc;
onCorruptRead;
constructor(path, opts) {
assertRetentionPolicy("RosterStore", opts);
this.path = path;
this.maxAgeMs = opts?.maxAgeMs;
this.gc = opts;
this.onCorruptRead = opts?.onCorruptRead;
}
discloseCorrupt(reason) {
try {
this.onCorruptRead?.({ kind: "file", path: this.path, reason });
}
catch {
}
}
read(mode) {
let text;
try {
text = readFileSync(this.path, "utf8");
}
catch (e) {
if (e.code === "ENOENT")
return [];
if (mode === "lenient") {
this.discloseCorrupt(`read failed (non-ENOENT): ${e.message}`);
return [];
}
throw e;
}
if (text.trim() === "")
return [];
try {
const rawU = JSON.parse(text);
if (typeof rawU !== "object" || rawU === null)
throw new Error("roster file: not a JSON object");
const raw = rawU;
if (!Array.isArray(raw.entries))
throw new Error("roster file: entries is not an array");
const kept = raw.entries.filter((e) => typeof e === "object" && e !== null && typeof e.name === "string" && typeof e.agentId === "string" && typeof e.createdAt === "number");
const dropped = raw.entries.length - kept.length;
if (dropped > 0) {
this.discloseCorrupt(`${dropped} schema-invalid roster rows dropped (of ${raw.entries.length}); a subsequent write persists the filtered document`);
}
return kept;
}
catch (e) {
if (mode === "lenient") {
this.discloseCorrupt(`corrupt roster document: ${e.message}`);
return [];
}
throw e;
}
}
write(entries) {
mkdirSync(dirname(this.path), { recursive: true });
atomicWriteFile(dirname(this.path), this.path, JSON.stringify({ version: 1, entries }, null, 2));
}
record(entry) {
const key = upsertKey(entry);
const entries = freshOnly(this.read("strict"), this.maxAgeMs).filter((e) => upsertKey(e) !== key);
entries.push({ ...entry });
this.write(capped(entries, this.gc));
}
resolve(name, access) {
return resolveIn(freshOnly(this.read("lenient"), this.maxAgeMs), name, access);
}
list() {
return freshOnly(this.read("lenient"), this.maxAgeMs);
}
releaseAgent(agentId) {
this.write(this.read("strict").filter((e) => e.agentId !== agentId));
}
}
export function recordRosterSpawn(roster, entry, onError) {
if (!roster)
return;
const disclose = (err) => {
try {
onError?.(err);
}
catch {
}
};
try {
void Promise.resolve(roster.record(entry)).catch(disclose);
}
catch (err) {
disclose(err);
}
}