UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

101 lines (100 loc) • 4.13 kB
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; } }