agent-trajectory
The engine-neutral trajectory contract — an append-only, replayable event log for agent runs, with digest-ref payload discipline, usage honesty, deterministic replay, and Claude/Codex usage adapters.
@caisson-sh/agent-trajectory is the trajectory contract: one append-only, replayable event
schema every governed agent run records into — run → step → model call → tool proposal → approval → tool result → checkpoint. It is the observation substrate the ai-kit gateway and
governed agent loop record into; the log itself never calls a model or executes a tool.
What it does
- An eleven-kind,
.strict()event envelope.TrajectoryEventis a ZoddiscriminatedUniononkind—run.started,run.finished,step.started,step.finished,model.call,model.usage,tool.proposed,tool.approved,tool.denied,tool.result,checkpoint. Every envelope carrieseventId,runId,seq,version(TRAJECTORY_VERSION),occurredAt, and a payload shaped by itskind; an unknown envelope or payload key is aValidationError. - Sensitive bodies never inline. Prompt text, tool argument/result bodies, and checkpoint
state travel only as a
DigestRef—{ digest: <sha256-hex>, byteLength, encRef? }— a content-address and size, never the raw content or key material. - Append-only, idempotent, gap-free.
seqis a monotonic 0-based integer per run.append()is idempotent on(runId, seq)for a byte-identical retry; a different event at an already-recorded seq, or a seq beyond the next expected slot, throwsConflictError. - Usage honesty (
billingStatus). Everymodel.usageevent declares how much to trust its numbers:metered(the ledger's own settled integers),priced(a pricebook-computed cost statement, never ledger-settled),estimated(real adapter-extracted counts, not price-normalized), orunsupported(no validated usage contract). The schema enforces the invariant:credits > 0is only legal onmetered/priced. - Deterministic replay.
project(events)folds a log to its canonicalRunProjection— status, step tree, per-billingStatususage totals, checkpoints — sorted byseqfirst, so the same events yield a byte-identical projection regardless of arrival order.projectToolCalls(events)is a sibling fold: one entry pertoolCallId, with its proposal, approval/denial, and result. - Two entry points.
.is the full, node-capable surface (everything below, plus the two Postgres-backed stores)../browseris the browser-safe subset — the schema, the in-memory store, the run-state port, both projections, and the Claude-transcript adapter — importable inside a client bundle; every name on./browseris also on..
Install
bun add @caisson-sh/agent-trajectoryQuickstart
import { createMemoryTrajectoryStore, project } from "@caisson-sh/agent-trajectory";
const store = createMemoryTrajectoryStore();
await store.append({
eventId: crypto.randomUUID(),
runId,
seq: 0,
version: 1,
occurredAt: new Date().toISOString(),
kind: "run.started",
payload: { agentId: "support-agent" },
});
const events = await store.read(runId);
const view = project(events);
// view.status / view.steps / view.usageTotals / view.checkpointscreateMemoryTrajectoryStore() is for tests and single-process runs. createPgTrajectoryStore(tx, accountId) is the Postgres-backed implementation with the same append-only contract, tenant-RLS
scoped; apply its migration (src/migrations/0001_trajectory_event.sql) before using it. See
Governed agent loop for the full wiring alongside
ai-kit's runToolLoop.
Adapters: turning a transcript into usage events
parseClaudeTranscript(jsonl, { runId }) reads Claude Code JSONL transcript lines (main and
subagent turns alike) and emits one model.usage event per usage-bearing assistant turn, with
real token counts and the model id, billingStatus: "estimated". A malformed line — invalid
JSON, or a turn with no usage — is skipped and counted in linesSkipped, never thrown.
The ./usage subpath adds a sibling Codex adapter and the price-normalization pass that upgrades
estimated events to priced:
import { parseCodexRollout, priceUsage } from "@caisson-sh/agent-trajectory/usage";
const { events, unsupported } = parseCodexRollout(rolloutJsonl, { runId });
// unsupported: true means the rollout carried no usage data at all — treat it as
// unsupported, not zero usage.
const priced = priceUsage(events); // estimated -> priced wherever the model resolves
// to a BUNDLED_PRICE_BOOK row through resolveModelAlias(); everything else passes through
// unchanged. Never guesses: an unaliased or unpriced model stays `estimated`.resolveModelAlias(provider, model) is the one bridge between a reported, dated model id (e.g.
claude-sonnet-4-5-20250514) and @caisson-sh/ai-meter's stable price-book rows — an unaliased
model resolves to null, never a nearest-match guess.
Run state: the parked/approved/denied sibling
RunStateStore (park/approve/deny/claimResume/finish/read) is the durable, mutable
sibling of the append-only log — where a run's paused-awaiting-approval status and its resumable
snapshot live. createMemoryRunStateStore() is the in-memory implementation;
createPgRunStateStore(tx, accountId, crypto) is the Postgres-backed one, encrypted at rest
(crypto is a FieldCryptoContext, from @caisson-sh/field-crypto), with its own two migrations (0002_agent_run_state.sql,
0003_agent_run_state_parked_state_encrypted.sql). Every transition is a compare-and-swap and
every decision (approve/deny) is idempotent on a repeat of the same call — see Governed agent
loop for the tool-approval flow this port backs.
Composing with the base
agent-trajectory depends on @caisson-sh/ai-meter (the price book priceUsage reuses
verbatim), @caisson-sh/field-crypto and @caisson-sh/tenancy-rls (the Postgres stores'
encryption and RLS scoping), and @caisson-sh/kernel (ConflictError, NotFoundError,
parseStrict). @caisson-sh/ai-kit's gateway and governed agent loop are its principal
producers; @caisson-sh/ai-evals reads projectToolCalls()'s shape for trajectory-quality
scoring.
tool-exec
Governed tool-call / sandboxed-exec primitive, a default-deny command allowlist, Zod-strict argv validation, and execFile arg-arrays only, never a shell.
Provenance
Cryptographic provenance, detached signing over a canonical manifest, an append-only WORM audit chain, and per-tenant field encryption. Proof, not a promise.