@sema-agent/core
Version:
Stateless, task-oriented AI agent core
98 lines (97 loc) • 3.95 kB
JavaScript
import { terminalProjection } from "./runner/terminal-projection.js";
export const MAX_JOURNAL_RESULT_BYTES = 5 * 1024 * 1024;
export function oversizeJournalResult(serialized) {
return Buffer.byteLength(serialized, "utf8") > MAX_JOURNAL_RESULT_BYTES;
}
export function callKeyOrdinal(callKey) {
const i = callKey.indexOf(":");
const n = Number.parseInt(i >= 0 ? callKey.slice(0, i) : callKey, 10);
return Number.isFinite(n) ? n : 0;
}
export const JOURNAL_OVERSIZE_ERROR_CODE = "workflow.journal_oversize";
export class WorkflowJournalOversizeError extends Error {
callKey;
bytes;
cap;
code = JOURNAL_OVERSIZE_ERROR_CODE;
constructor(callKey, bytes, cap = MAX_JOURNAL_RESULT_BYTES) {
super(`workflow journal: entry "${callKey}" is ${bytes} bytes, over the ${cap}-byte per-entry cap — refused, nothing journaled`);
this.callKey = callKey;
this.bytes = bytes;
this.cap = cap;
this.name = "WorkflowJournalOversizeError";
}
}
export function assertJournalEntryFits(entry) {
const serialized = JSON.stringify(entry.parked !== undefined ? entry.parked : entry.result);
if (oversizeJournalResult(serialized))
throw new WorkflowJournalOversizeError(entry.callKey, Buffer.byteLength(serialized, "utf8"));
}
export function journalOversizeTombstone(result, bytes) {
const message = `[journal] result omitted: ${bytes} bytes exceeds the ${MAX_JOURNAL_RESULT_BYTES}-byte per-entry journal cap; ` +
`the agent's real terminal status was "${terminalProjection(result.terminal).status}". A resume re-runs this agent (and everything after it) live.`;
return {
taskId: result.taskId,
sessionId: result.sessionId,
terminal: { kind: "failed", code: JOURNAL_OVERSIZE_ERROR_CODE, message },
result: message,
stats: result.stats,
};
}
function snapshot(entry) {
try {
return structuredClone(entry);
}
catch {
return JSON.parse(JSON.stringify(entry));
}
}
export const IN_MEMORY_RESUME_CLAIM_TTL_MS = 60 * 60 * 1000;
export class InMemoryWorkflowJournalStore {
runs = new Map();
resumeClaims = new Map();
claimsForScope(scope) {
let m = this.resumeClaims.get(scope);
if (m === undefined) {
m = new Map();
this.resumeClaims.set(scope, m);
}
return m;
}
async resumeClaim(input) {
const table = this.claimsForScope(input.scope);
const now = Date.now();
const existing = table.get(input.sourceRunId);
if (existing !== undefined && existing.expiresAt > now && existing.holder !== input.newRunId) {
return { granted: false, holder: existing.holder };
}
table.set(input.sourceRunId, { holder: input.newRunId, expiresAt: now + IN_MEMORY_RESUME_CLAIM_TTL_MS });
return { granted: true };
}
async releaseResumeClaim(input) {
const table = this.resumeClaims.get(input.scope);
if (table === undefined)
return;
const existing = table.get(input.sourceRunId);
if (existing !== undefined && existing.holder === input.newRunId)
table.delete(input.sourceRunId);
}
async load(runId, scope) {
const rec = this.runs.get(runId);
if (!rec || rec.scope !== scope)
return [];
return [...rec.byOrdinal.entries()].sort((a, b) => a[0] - b[0]).map(([, e]) => snapshot(e));
}
async append(runId, scope, entry) {
assertJournalEntryFits(entry);
let rec = this.runs.get(runId);
if (!rec) {
rec = { scope, byOrdinal: new Map() };
this.runs.set(runId, rec);
}
else if (rec.scope !== scope) {
throw new Error(`workflow journal: runId ${runId} already recorded under a different scope`);
}
rec.byOrdinal.set(callKeyOrdinal(entry.callKey), snapshot(entry));
}
}