@sema-agent/core
Version:
Stateless, task-oriented AI agent core
101 lines (100 loc) • 4.13 kB
JavaScript
import { assertRetentionPolicy } from "./retention-policy.js";
export function summarizeWorkflowRun(run) {
const latestPhase = run.phases.at(-1);
return {
id: run.id,
scope: run.scope,
...(run.originatingSessionId !== undefined ? { originatingSessionId: run.originatingSessionId } : {}),
...(run.name !== undefined ? { name: run.name } : {}),
...(run.description !== undefined ? { description: run.description } : {}),
status: run.status,
...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
...(run.budgetOvershoot !== undefined ? { budgetOvershoot: { ...run.budgetOvershoot } } : {}),
...(run.timeoutInterruption !== undefined ? { timeoutInterruption: { ...run.timeoutInterruption } } : {}),
...(latestPhase !== undefined ? { currentPhase: latestPhase.title } : {}),
phaseCount: run.phases.length,
agentCount: run.agents.length,
tokens: run.stats.tokens + run.stats.nested.tokens,
startedAt: run.startedAt,
...(run.endedAt !== undefined ? { endedAt: run.endedAt } : {}),
createdAt: run.createdAt,
};
}
export function isTerminalWorkflowStatus(status) {
return status === "completed" || status === "failed";
}
export class WorkflowRunStoreError extends Error {
code;
constructor(code, message) {
super(message);
this.code = code;
this.name = "WorkflowRunStoreError";
}
}
export function nextWorkflowRunOnUpdate(cur, id, scope, run, expect) {
if (!cur || cur.scope !== scope)
return null;
if (expect !== undefined && (cur.rev ?? 0) !== expect.rev)
return null;
const next = structuredClone(run);
next.id = id;
next.scope = scope;
next.rev = (cur.rev ?? 0) + 1;
return next;
}
export function queryWorkflowRuns(runs, scope, opts) {
const rows = [...runs].filter((r) => r.scope === scope &&
(opts?.status === undefined || r.status === opts.status) &&
(opts?.session === undefined || r.originatingSessionId === opts.session));
rows.sort((a, b) => b.createdAt - a.createdAt);
const limited = opts?.limit !== undefined ? rows.slice(0, Math.max(0, opts.limit)) : rows;
return limited.map(summarizeWorkflowRun);
}
export class InMemoryWorkflowRunStore {
runs = new Map();
async put(id, run) {
if (this.runs.has(id)) {
throw new WorkflowRunStoreError("workflow_run.already_exists", `workflow run id already exists`);
}
const stored = structuredClone(run);
stored.id = id;
stored.rev = run.rev ?? 0;
this.runs.set(id, stored);
}
async get(id) {
const run = this.runs.get(id);
return run ? structuredClone(run) : null;
}
async update(id, scope, run, expect) {
const next = nextWorkflowRunOnUpdate(this.runs.get(id), id, scope, run, expect);
if (!next)
return false;
this.runs.set(id, next);
return true;
}
async listByScope(scope, opts) {
return queryWorkflowRuns(this.runs.values(), scope, opts);
}
async reap(scope, now, opts) {
assertRetentionPolicy("WorkflowRunStore.reap", opts);
if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
return 0;
const terminal = [...this.runs.values()].filter((r) => r.scope === scope && isTerminalWorkflowStatus(r.status));
terminal.sort((a, b) => b.createdAt - a.createdAt);
const doomed = new Set();
for (let i = 0; i < terminal.length; i++) {
const r = terminal[i];
const tooOld = opts.maxAgeMs !== undefined && (r.endedAt ?? r.createdAt) < now - opts.maxAgeMs;
const overKeep = opts.keep !== undefined && i >= opts.keep;
if (tooOld || overKeep)
doomed.add(r.id);
}
for (const id of doomed)
this.runs.delete(id);
return doomed.size;
}
get size() {
return this.runs.size;
}
}