UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

243 lines (242 loc) • 16.4 kB
import { strict as assert } from "node:assert"; import { STALE_RUNNING_REAP_ATTRIBUTION, } from "../background-agent-store.js"; import { beginContract } from "./contract-harness.js"; export const BACKGROUND_AGENT_CONTRACT_SCOPE = "tenant-A"; function record(over = {}) { return { handle: "a0000000000000001", scope: BACKGROUND_AGENT_CONTRACT_SCOPE, owner: "task-1", sessionScoped: false, parentSessionId: "sess-1", writerId: "writer-1", spawnedAt: 1_000, updatedAt: 1_000, status: "running", rev: 0, ...over, }; } async function withStores(mk, fn) { const live = []; const make = () => { const s = mk(); live.push(s); return s; }; try { await fn(make); } finally { for (const s of live.splice(0)) s.close?.(); } } async function held(store, handle, scope = BACKGROUND_AGENT_CONTRACT_SCOPE) { const row = await store.get(handle, scope); assert.notEqual(row, null, `row ${handle} should exist in scope ${scope}`); return row; } export async function backgroundAgentStoreContract(mk, runAssertion) { const { run: runRaw, settle } = beginContract(runAssertion); const run = (name, fn) => runRaw(name, () => withStores(mk, fn)); const S = BACKGROUND_AGENT_CONTRACT_SCOPE; run("put is create-once and scope-partitioned; get round-trips the row and is null cross-scope", async (make) => { const store = make(); await store.put(record({ handle: "a1", finalOutput: "result-text" })); await assert.rejects(store.put(record({ handle: "a1" })), (e) => e.code === "agent_record.already_exists", "a second put under the same (handle, scope) must throw the typed already_exists error"); await store.put(record({ handle: "a1", scope: "tenant-B" })); const row = await held(store, "a1"); assert.equal(row.finalOutput, "result-text"); assert.equal(row.owner, "task-1"); assert.equal(await store.get("a1", "tenant-C"), null, "an unrelated scope must not see the row"); assert.equal(await store.get("a-missing", S), null); }); run("the org-admission fields round-trip, and ABSENT stays absent (a backend must not invent a verdict)", async (make) => { const store = make(); await store.put(record({ handle: "a1", admittedOrgScopes: ["org:acme", "org:beta"], admittedOrgWriteScope: "org:acme" })); const withVerdict = await held(store, "a1"); assert.deepEqual(withVerdict.admittedOrgScopes, ["org:acme", "org:beta"]); assert.equal(withVerdict.admittedOrgWriteScope, "org:acme"); await store.put(record({ handle: "a2", admittedOrgScopes: [], admittedOrgWriteScope: null })); const readOnly = await held(store, "a2"); assert.deepEqual(readOnly.admittedOrgScopes, []); assert.equal(readOnly.admittedOrgWriteScope, null); await store.put(record({ handle: "a3" })); const untouched = await held(store, "a3"); assert.equal(untouched.admittedOrgScopes, undefined); assert.equal(untouched.admittedOrgWriteScope, undefined); await store.update("a3", S, { ...untouched, admittedOrgScopes: ["org:acme"], admittedOrgWriteScope: null }, { rev: untouched.rev }); const afterCas = await held(store, "a3"); assert.deepEqual(afterCas.admittedOrgScopes, ["org:acme"]); assert.equal(afterCas.admittedOrgWriteScope, null); }); run("put refuses a scopeless row (a row with no tenant partition would be world-readable)", async (make) => { const store = make(); await assert.rejects(store.put(record({ scope: "" })), "an empty scope must be refused loudly, not stored"); }); run("a row carrying a key this build does not declare round-trips: no refusal, every declared field intact", async (make) => { const store = make(); await store.put({ ...record({ handle: "a-extra", name: "worker", agentType: "reader" }), teamName: "retired-column" }); const row = await held(store, "a-extra"); assert.equal(row.name, "worker"); assert.equal(row.agentType, "reader"); assert.equal(row.status, "running"); await store.update("a-extra", S, { ...row, status: "completed" }, { rev: row.rev }); assert.equal((await held(store, "a-extra")).status, "completed", "a CAS update over such a row still lands"); }); run("get returns DETACHED copies: mutating a read never reaches the stored row", async (make) => { const store = make(); await store.put(record({ handle: "a1", usage: { totalTokens: 5 } })); const first = await held(store, "a1"); first.status = "failed"; first.usage.totalTokens = 999; const second = await held(store, "a1"); assert.equal(second.status, "running"); assert.equal(second.usage?.totalTokens, 5); }); run("update is a rev-CAS: stale rev loses, wrong scope loses, the winner bumps rev by EXACTLY 1", async (make) => { const store = make(); await store.put(record({ handle: "a1" })); const row = await held(store, "a1"); assert.equal(await store.update("a1", "tenant-B", { ...row, status: "completed" }, { rev: row.rev }), false, "wrong scope"); assert.equal(await store.update("a1", S, { ...row, status: "completed" }, { rev: row.rev + 5 }), false, "rev ahead"); assert.equal(await store.update("a-missing", S, { ...row, status: "completed" }, { rev: row.rev }), false, "missing row"); assert.equal(await store.update("a1", S, { ...row, status: "completed" }, { rev: row.rev }), true); const after = await held(store, "a1"); assert.equal(after.status, "completed"); assert.equal(after.rev, row.rev + 1); assert.equal(await store.update("a1", S, { ...row, status: "failed" }, { rev: row.rev }), false); assert.equal((await held(store, "a1")).status, "completed"); }); run("update keeps the KEY authoritative: a payload naming another handle/scope cannot repartition the row", async (make) => { const store = make(); await store.put(record({ handle: "a1" })); const row = await held(store, "a1"); assert.equal(await store.update("a1", S, { ...row, handle: "a-other", scope: "tenant-B", status: "failed" }, { rev: row.rev }), true); const after = await held(store, "a1"); assert.equal(after.handle, "a1", "the key's handle wins over the payload's"); assert.equal(after.scope, S, "scope is the partition key and an update must never rewrite it"); assert.equal(await store.get("a-other", "tenant-B"), null, "no second row may appear"); }); run("updateIf keeps the rev guard AND adds the field guards (status / checkpoint token / claim id)", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "parked", parkedCheckpointToken: "cp-1" })); const row = await held(store, "a1"); assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev + 3 }), false, "stale rev"); assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, status: "running" }), false, "status mismatch"); assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, status: "parked", parkedCheckpointToken: "cp-OTHER" }), false, "checkpoint token mismatch"); assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, parkedCheckpointToken: null }), false, "null must mean 'this field is absent', and it is present"); assert.equal(await store.updateIf("a1", S, { ...row, status: "running" }, { rev: row.rev, parkClaimId: "claim-1" }), false, "claim id expected but absent"); const next = { ...row, status: "running", parkClaimId: "claim-1" }; assert.equal(await store.updateIf("a1", S, next, { rev: row.rev, status: "parked", parkedCheckpointToken: "cp-1", parkClaimId: null }), true, "matching guards (incl. null-as-absent on the claim id) must win"); const after = await held(store, "a1"); assert.equal(after.status, "running"); assert.equal(after.parkClaimId, "claim-1"); assert.equal(after.rev, row.rev + 1); }); run("listBySession anchors on (sessionScoped ? owner : parentSessionId) OR rootSessionId, newest-first", async (make) => { const store = make(); await store.put(record({ handle: "a1", spawnedAt: 1_000, finalOutput: "SECRET", summary: "s", error: "e", recentSteps: [] })); await store.put(record({ handle: "a2", owner: "sess-1", sessionScoped: true, parentSessionId: undefined, spawnedAt: 2_000 })); await store.put(record({ handle: "a3", parentSessionId: "sess-mid", rootSessionId: "sess-1", spawnedAt: 3_000 })); await store.put(record({ handle: "a4", parentSessionId: "sess-other", spawnedAt: 4_000 })); const rows = await store.listBySession(S, "sess-1"); assert.deepEqual(rows.map((r) => r.handle), ["a3", "a2", "a1"], "newest-first by spawnedAt"); for (const row of rows) { for (const field of ["finalOutput", "summary", "error", "recentSteps", "editedFiles"]) { assert.equal(Object.hasOwn(row, field), false, `list projections must not carry ${field}`); } } assert.deepEqual((await store.listBySession(S, "sess-1", { limit: 2 })).map((r) => r.handle), ["a3", "a2"], "limit keeps the NEWEST n"); assert.deepEqual(await store.listBySession("tenant-B", "sess-1"), [], "another tenant sees nothing"); assert.deepEqual((await store.listBySession(S, "sess-mid")).map((r) => r.handle), ["a3"], "the intermediate anchor still lists its direct child"); }); run("listBySession/listByScope filter one status; listByScope enumerates the whole tenant", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "running", spawnedAt: 1_000 })); await store.put(record({ handle: "a2", status: "completed", spawnedAt: 2_000 })); await store.put(record({ handle: "a3", status: "completed", parentSessionId: "sess-other", spawnedAt: 3_000 })); await store.put(record({ handle: "a4", status: "completed", scope: "tenant-B", spawnedAt: 4_000 })); assert.deepEqual((await store.listByScope(S)).map((r) => r.handle), ["a3", "a2", "a1"], "scope-wide, newest-first"); assert.deepEqual((await store.listByScope(S, { status: "completed" })).map((r) => r.handle), ["a3", "a2"]); assert.deepEqual((await store.listByScope(S, { status: "completed", limit: 1 })).map((r) => r.handle), ["a3"]); assert.deepEqual((await store.listBySession(S, "sess-1", { status: "completed" })).map((r) => r.handle), ["a2"]); assert.deepEqual((await store.listByScope("tenant-B")).map((r) => r.handle), ["a4"], "tenants are separate ledgers"); }); run("delete is row-precise and CONDITIONAL when a rev is supplied (a concurrent revival must win)", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "completed" })); await store.put(record({ handle: "a2", status: "completed" })); const row = await held(store, "a1"); assert.equal(await store.delete("a1", S, { rev: row.rev + 1 }), false, "a stale rev must not delete"); assert.notEqual(await store.get("a1", S), null); assert.equal(await store.delete("a1", "tenant-B"), false, "cross-scope delete is a miss"); assert.equal(await store.delete("a1", S, { rev: row.rev }), true); assert.equal(await store.get("a1", S), null); assert.equal(await store.delete("a1", S), false, "deleting a gone row reports false, not true"); assert.equal(await store.delete("a2", S), true, "an unconditional delete still works"); }); run("reap with NO policy is a no-op (retention is always explicit)", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "completed", settledAt: 1_000 })); assert.equal(await store.reap(S, 100_000), 0); assert.equal(await store.reap(S, 100_000, {}), 0); assert.notEqual(await store.get("a1", S), null); }); run("reap deletes terminal rows by age and by keep-bound; running and parked rows survive BOTH", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "completed", spawnedAt: 1_000, settledAt: 1_000 })); await store.put(record({ handle: "a2", status: "running", spawnedAt: 1_000, updatedAt: 1_000 })); await store.put(record({ handle: "a3", status: "parked", spawnedAt: 1_000, updatedAt: 1_000, parkedCheckpointToken: "cp-1" })); assert.equal(await store.reap(S, 100_000, { maxAgeMs: 10 }), 1, "only the terminal row ages out"); assert.equal(await store.get("a1", S), null); assert.equal((await held(store, "a2")).status, "running"); assert.equal((await held(store, "a3")).status, "parked"); for (const i of [1, 2, 3]) await store.put(record({ handle: `b${i}`, status: "completed", spawnedAt: 1_000 + i, settledAt: 2_000 + i })); assert.equal(await store.reap(S, 100_000, { keep: 2 }), 1); assert.deepEqual((await store.listByScope(S, { status: "completed" })).map((r) => r.handle), ["b3", "b2"]); assert.notEqual(await store.get("a2", S), null); assert.notEqual(await store.get("a3", S), null); }); run("reap's stale-running arm flips to a terminal `failed` WITH the shared attribution, and never overwrites real failure text", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "running", updatedAt: 1_000 })); await store.put(record({ handle: "a2", status: "running", updatedAt: 1_000, error: "the child reported a real failure" })); assert.equal(await store.reap(S, 100_000, { staleRunningMaxAgeMs: 50_000 }), 2); const flipped = await held(store, "a1"); assert.equal(flipped.status, "failed", "an interrupted host's row settles as a terminal, not a perpetual running"); assert.equal(flipped.stoppedBy, "system"); assert.equal(flipped.settledAt, 100_000); assert.equal(flipped.updatedAt, 100_000); assert.equal(flipped.error, STALE_RUNNING_REAP_ATTRIBUTION); assert.equal(flipped.summary, STALE_RUNNING_REAP_ATTRIBUTION); assert.equal((await held(store, "a2")).error, "the child reported a real failure"); await store.put(record({ handle: "a3", status: "running", updatedAt: 99_000 })); assert.equal(await store.reap(S, 100_000, { staleRunningMaxAgeMs: 50_000 }), 0); assert.equal((await held(store, "a3")).status, "running"); }); run("reap counts each ROW once even when the same sweep flips it AND removes it", async (make) => { const store = make(); await store.put(record({ handle: "a1", status: "running", spawnedAt: 1_000, updatedAt: 1_000 })); assert.equal(await store.reap(S, 100_000, { staleRunningMaxAgeMs: 50_000, keep: 0 }), 1, "one row changed, so the count is 1"); assert.equal(await store.get("a1", S), null, "and it really is gone (flipped, then trimmed)"); }); await settle(); } export async function backgroundAgentStoreScopesContract(mk, runAssertion) { const { run: runRaw, settle } = beginContract(runAssertion); const run = (name, fn) => runRaw(name, () => withStores(mk, fn)); run("listScopes enumerates the scopes that currently HOLD rows, deduplicated and sorted", async (make) => { const store = make(); assert.equal(typeof store.listScopes, "function", "this layer is only for backends that implement listScopes"); await store.put(record({ handle: "a1", scope: "tenant-B" })); await store.put(record({ handle: "a2", scope: "tenant-A" })); await store.put(record({ handle: "a3", scope: "tenant-A" })); assert.deepEqual(await store.listScopes(), ["tenant-A", "tenant-B"], "deduplicated and sorted — a retention loop drives itself off this"); await store.delete("a1", "tenant-B"); assert.deepEqual(await store.listScopes(), ["tenant-A"]); }); await settle(); }