Reference
Architecture
Mission
Make intelligent, long-running, human-in-the-loop software as durable, inspectable, and reproducible as a database transaction — without giving up the flexibility of code.
Loom treats an agentic workflow as a process that unfolds over time, not a request that returns once. Every decision it makes is recorded as an immutable fact, so any execution can be paused for a human, resumed on another machine, replayed bit-for-bit for debugging, forked to explore alternatives, and evaluated against its own history. The intelligence is yours to write in plain TypeScript; Loom provides the durable spine that makes it trustworthy in production.
Table of contents
- The problem
- Core principles
- The big picture
- The execution model
- Subsystems
- Package map
- Decision records — reasoning & advantages
- Trade-offs & non-goals
1. The problem
Modern applications increasingly embed intelligence — LLM calls, tool use, multi-agent coordination — inside flows that:
- run for a long time (seconds to days), often pausing for a human to approve a risky action;
- call non-deterministic, expensive, and fallible services (models, APIs, other agents);
- must survive process restarts, deploys, and crashes without losing or double-executing work;
- need to be explained after the fact (“why did the agent do that?”);
- want to explore alternatives (“what if we tried the other remediation?”).
The dominant tools force a bad trade. A request/response framework is easy to write but forgets everything the moment a process dies and cannot pause for a human. A heavyweight workflow engine is durable but pushes you into a restrictive DSL, an external server, and a deployment model that fights your UI. Neither gives you deterministic replay — the ability to reconstruct exactly what happened — which is precisely what you need when an AI does something surprising.
Loom’s thesis: the durable event log is the moat. If every meaningful thing that happens is appended to an ordered, immutable log, then durability, resumability, replay, branching, evaluation, and time-travel debugging are all just different lenses on the same log. Build that spine well, keep the authoring surface plain TypeScript, and everything else composes.
mindmap
root((Durable event log))
Durability
survive crashes
survive deploys
Resumability
pause for humans
resume anywhere
Replay
deterministic reconstruction
time-travel debug
Branching
fork / simulate / promote
Evaluation
log-as-fixture evals
deploy simulation
Distribution
run on any compute
2. Core principles
These are the non-negotiable rules the whole codebase is designed around. Every architectural decision in §7 traces back to one or more of them.
P1 — The log is the source of truth (event sourcing)
Execution state is never stored directly. It is derived by folding an append-only sequence of events. The current state of any execution is fold(events). This makes state reconstructable, auditable, and diffable, and it is what unlocks replay, resume, and fork.
P2 — Replay is deterministic and pure
Reconstructing state from events is a pure fold — the same events always produce the same state,
with no side effects, no clock, no randomness. Determinism is a hard constraint, not a nicety: a
resume on a fresh machine, a fork, and a replay-for-debugging must all reconstruct identical
state. This is why the replay engine is a pure function and why library code avoids
Date.now()/Math.random() on paths that must be reproducible (hashing is used instead).
P3 — Suspension is a first-class, normal outcome
A workflow pausing to wait for a human (or a timer, or an external signal) is not an error or a special mode — it is an ordinary result of running. ctx.suspend() writes a durable checkpoint and returns control to the caller; a later resume() continues from exactly that point. The runtime is built so that “suspended” is as routine as “completed.”
P4 — The intelligence stays as plain code
Loom does not invent a workflow DSL. A flow’s orchestration is an ordinary async TypeScript function ((ctx, input) => …) that calls ctx.run(step), ctx.suspend(), and ctx.parallel(). Your business logic, prompts, and agent loops are just functions. Loom wraps them with durability; it does not replace them. Sugar, not a cage.
P5 — Wiring is static and replay-safe
Everything that determines what runs — flow registration, effect registration, middleware, surfaces — is wired statically at build/boot time, from explicit imports. No runtime filesystem globbing, no environment-conditional registration. A resume, a remote worker, or a replay must be able to reconstruct an identical set of flows and effects, which is only possible if wiring is deterministic. (Folder conventions are honored via build-time codegen, not runtime discovery.)
P6 — Composition over inheritance; sugar over replacement
Reusable abstractions are functions and small objects you compose, never base classes you must extends. Every helper returns or consumes the same primitives the core already produces (WorkflowRuntime, EventStore, defineConfig). You can always drop down a layer. Nothing hides the thing that makes your app valuable.
P7 — Pure cores, thin edges
Each capability is a pure, unit-testable core (a fold, a format, a validate, a codegen, a plan) wrapped by a thin I/O shell (CLI argv, HTTP handler, filesystem). This keeps logic testable without spawning processes and keeps the surface area honest.
P8 — The core is isomorphic; the edges are pluggable
The durable runtime has no environment assumptions. Storage, compute placement, LLM providers, and renderers are all interfaces (EventStore, WorkerRuntime, LlmProvider, renderers). The same flow runs in-process in a test, in a browser tab, on a Node server with SQLite, or on Cloud Run — because the core depends only on interfaces, and adapters supply the edges.
P9 — Non-determinism is allowed, but only when logged (Loom 2.0)
A runtime may route between models, elevate to a stronger one, learn, or optimize — but every such
choice is emitted as a durable decision event (DECISION_RECORDED) and served from the log on
replay, exactly as completed effects are. Replay therefore stays a pure fold (P2 is untouched):
a given execution reconstructs bit-for-bit forever, even as outcomes improve run-to-run. This
evolves P2 to admit intelligence without regressing determinism. See
ADR 0016.
P10 — Minimal kernel; intelligence lives in an advisory plane (Loom 2.0)
The durable core stays small and boring (execution + event log + determinism + effect isolation).
Everything “smart” — scheduling, reasoning, memory, prediction, optimization — is an
optional, composable module in an ephemeral advisory plane that can affect outcomes only
through logged decisions (P9) or latency only through off-log warming, and is
never a hidden input to the fold. You never pay for a runtime you did not opt into; the simple
path (ctx.run) stays a one-liner and the zero-overhead default is preserved. See
ADR 0016.
P11 — AI-native DX: natural language compiles to code, not runtime (Loom 2.0)
Natural language is a build-time convenience, never a runtime input. @loom/appgen is
brownfield-aware: it scans the existing app (scanApp → AppModel), plans a delta from an
intent via a deterministic, offline directive parser (planChangesDeterministic → ChangeSpec, the
only stage that reads NL), then applies it with surgical, formatting-preserving edits
(applyChanges) — new flows from templates, existing ones edited only where a handler still hashes
to the generated stub. The output is ordinary flow$/step$ wired through defineConfig in the
folder convention. There is no runtime interpretation of NL: the only “understanding” step is the
swappable planner producing a spec, and codegen is pure and template-based (P4/P5 preserved).
The plan stage is provider-agnostic — loom appgen --model <spec> reasons with the model of
your choice (anthropic: / openai: / gemini: via @loom/llm, or a local .gguf via the
optional @loom/appgen-model (loom/docs/tools/appgen-model)), and always falls back to the deterministic
parser. See ADR 0042,
ADR 0050, and
ADR 0016.
3. The big picture
At the coarsest level, Loom is three concentric rings: authoring (what you write), the durable core (the spine), and pluggable edges (where it runs, what it talks to, how it renders).
flowchart TB
subgraph Authoring["✍️ Authoring — plain TypeScript"]
A1["step$ / handler$ — units of work"]
A2["flow$ + runner — orchestration (async fn)"]
A3["agent$ · tool$ · skill$ — agentic"]
A4["projection$ · schedule$ · memory$ · policy$ — UI · cron · memory · authority"]
end
subgraph Core["🧵 Durable core (isomorphic, deterministic)"]
C1["WorkflowRuntime<br/>start · resume · fork"]
C2["ExecutionJournal<br/>sole sequencer + writer"]
C3["ReplayVM + foldEvents<br/>pure state reconstruction"]
C4["EffectRuntime + middleware<br/>isolate side effects · protection ring (K2–K12)"]
end
subgraph Edges["🔌 Pluggable edges"]
E1["EventStore / SnapshotStore<br/>(memory · SQLite · Postgres…)"]
E2["ProjectionRuntime + agent-surface<br/>→ React / WebComponents / terminal"]
E3["Dispatcher + WorkerRuntime<br/>(in-process · HTTP · Cloud Run)"]
E4["LLM · MCP · A2A · agents"]
end
Authoring --> Core
Core --> Edges
C2 --> E1
C2 --> E2
C1 --> E3
C4 --> E4
Reading the diagram: you author flows/steps/agents as functions. WorkflowRuntime runs them;
the ExecutionJournal turns every meaningful moment into a sequenced, durably-written event; the
ReplayVM folds those events into state. Side effects are quarantined behind EffectRuntime.
Everything below the core is an interface with swappable adapters — that’s what makes the same code
run in a test, a browser, or the cloud.
4. The execution model
4.1 One event log per execution
Every execution is an ordered list of events (EventEnvelope), each with a monotonically increasing sequence. The current state is foldEvents(events):
flowchart LR
E0["#0 EXECUTION_STARTED"] --> E1["#1 STEP_STARTED"] --> E2["#2 STEP_COMPLETED"] --> E3["#3 EXECUTION_SUSPENDED"] --> E4["#4 EXECUTION_RESUMED"] --> E5["#5 STEP_COMPLETED"] --> E6["#6 EXECUTION_COMPLETED"]
E6 --> F["foldEvents(events) → ExecutionState<br/>{ status, frames, variables, sequence }"]
The fold is a pure, deterministic left-fold: each event type has a handler (state, event) => state. Nothing else defines state. (For performance, the fold uses copy-on-write structural sharing — see ADR 0015.)
4.2 The durable write pipeline
The ExecutionJournal is the single most important invariant in the system: it is the sole sequence allocator and the only thing that appends to the store. This is what guarantees a per-execution total order with no multi-writer races.
sequenceDiagram
participant Flow as Flow runner (your async fn)
participant Ctx as WorkflowContext
participant J as ExecutionJournal
participant VM as ReplayVM (in-memory)
participant Store as EventStore (durable)
Flow->>Ctx: ctx.run(step, input)
Ctx->>J: commit("STEP_STARTED", …)
J->>J: ++sequence (sole allocator)
J->>VM: applyEvent (synchronous, authoritative)
J-->>Store: append (async, order-preserving tail)
Ctx->>Ctx: run the effect (via EffectRuntime)
Ctx->>J: commit("STEP_COMPLETED", output)
J->>VM: applyEvent
J-->>Store: append
Ctx-->>Flow: return output
Key properties:
- Synchronous fold, async durable write. The in-memory
ReplayVMis updated synchronously so the flow always sees authoritative state; the durable append is chained on an order-preserving promise tail. A write rejection aborts the run and surfaces at the next barrier. - Single allocator. Because only the journal allocates sequences and appends, there is never a gap or a duplicate — the foundation of replay correctness.
4.3 Suspend & resume
When a flow calls ctx.suspend({ on, correlationKey, timeout }), the runtime commits EXECUTION_SUSPENDED, persists a snapshot, and returns control to the caller. The execution now lives entirely in durable storage. A later resume(executionId, { eventType, payload }) reloads state and continues from the checkpoint — the payload becomes the return value of the original suspend() call.
stateDiagram-v2
[*] --> running: start()
running --> suspended: ctx.suspend() (writes checkpoint + snapshot)
suspended --> running: resume(id, event) (payload → suspend() return)
running --> completed: return
running --> failed: throw
suspended --> [*]: (durable — may outlive the process)
completed --> [*]
Crucially, on resume the flow function re-runs from the top, but every already-completed step is
served from the log (its STEP_COMPLETED event is replayed, the effect is not re-executed) — so
effects happen exactly once and the suspend point is reached deterministically. This is P2
(deterministic replay) doing the heavy lifting. The same holds for the flow’s own
ctx.emit/ctx.stream/surface(ctx) calls: each is served from the log only when the log proves
it was already made (a stamped emit of the same content in the same gate segment, with every syscall
of the segment so far served), so a domain event or a surface patch made before a gate is appended
once per execution, not once per resume — and an emit loom cannot match is appended, never dropped
(ADR 0102).
4.3.1 Step identity & the interpreter pattern
Replay’s substitution is keyed by step id (frames[stepId]) — not by log position. That single fact is both the freedom and the obligation of dynamic flows (ADR 0061):
The sequence of durable-step identities an execution produces must be a pure function of logged inputs (flow input, step outputs, decisions, resume payloads). A step invoked more than once with different intent must mint a deterministic distinct id —
`${id}#${index}`for loops,`${id}#retry:${n}`for deliberate re-tries (saga uses#compensate:${i}). Reusing an id returns/substitutes the first recorded outcome — completed output and typed failures alike (ADR 0060).
The freedom: a statically compiled interpreter flow may issue ctx.run inside loops bounded by
runtime-loaded data (a blueprint, a runbook) and remain fully replayable — the id sequence
re-derives identically from the logged input, crash recovery re-executes only the frames that never
completed, and flowContentHash (K11.1) is unaffected because dynamic steps are deliberately
outside the structural hash (ADR 0113: replay-check treats id#n as family id, and the
replay-divergence detector watches the body’s id sequence against the log). Dynamic steps bind a
static toolId to a registered effect; the effect registry, not the compiler manifest, is their
load-bearing map.
The obligation: a loop that reuses one id is silently wrong — iteration 2+ is served iteration 1’s
memoized output. The compiler’s analyzeLoopIdentity pass flags loop-scoped ctx.run/ctx.execute
whose id cannot vary per iteration (surfaced through @loom/verify’s determinism rule and the
compiler manifest’s loopIdentity warnings), and the kernel’s interpreter crash-matrix test pins
the contract.
Typed failures (ADR 0060) are the failure twin of the memoized success — recorded once, substituted forever:
sequenceDiagram
participant F as flow runner
participant X as execute seam
participant E as effect
participant L as event log
Note over F,L: live run
F->>X: ctx.run(step, input)
X->>E: invoke
E-->>X: fail("schemaDrift", payload)
X->>L: STEP_FAILED { channel, payload } (Zod-validated, ≤16 KiB)
X-->>F: throw TypedFailure — flow catches, branches (repair/suspend)
Note over F,L: replay / resume / recovery
F->>X: ctx.run(step, input)
X->>L: frame has errorChannel?
L-->>X: { channel, payload } (recorded)
X-->>F: rethrow TypedFailure — same branch, ZERO re-invocation
4.4 Recovery & snapshots
A crash mid-flight is just another resume. On redispatch the worker folds the durable state and decides idempotently: still running → resume; already completed/suspended → return as-is. No step re-runs.
To keep replay cheap on long executions, snapshots checkpoint the folded state so a resume folds only the tail after the latest snapshot instead of the whole log. Snapshots are written at suspend and fork, and — when configured with snapshotEvery: N — every N committed events, bounding recovery to O(N) regardless of log length (ADR 0015).
4.5 Branching (fork / simulate / promote)
Because state is fold(events), a fork is just copying the event prefix [0..sequence] into a new execution id and resuming it differently. The parent is untouched; branches are independent executions that can run concurrently. Promotion is a pointer move in a BranchRegistry (git-style refs) — there is no merge.
flowchart LR
P["parent execution<br/>… suspended at #k (gate)"]
P -->|fork @k| B1["branch: strategy A<br/>resume(simulate:true)"]
P -->|fork @k| B2["branch: strategy B<br/>resume(simulate:true)"]
P -->|fork @k| B3["branch: strategy C<br/>resume(simulate:true)"]
B1 --> C{compare projected outcomes}
B2 --> C
B3 --> C
C -->|promote winner| M["fork @k → resume(simulate:false)<br/>ref 'main' → committed"]
B1 -.losing branches kept as audit trail.-> AUD[(immutable history)]
B2 -.-> AUD
This is the same mechanism behind time-travel debugging and deploy-simulation: replay a prefix, then diverge.
5. Subsystems
5.1 Core runtime (@loom/core, @loom/workflow-runtime, @loom/replay-vm, @loom/event-runtime, @loom/snapshot-runtime)
@loom/core— the authoring builders (step$,handler$,flow$,tool$,agent$,projection$, …) and shared types. Pure declarations, no runtime.@loom/workflow-runtime—WorkflowRuntime(start/resume/fork/loadState), theExecutionJournal, andWorkflowContext(ctx.run/suspend/parallel/emit/ui).@loom/replay-vm—foldEvents+ReplayVM: the pure, deterministic state reconstruction engine (P2).@loom/event-runtime— theEventStoreinterface + in-memory and SQLite implementations, plus theBranchRegistryfor git-style refs.@loom/snapshot-runtime— theSnapshotStoreinterface + implementations for bounded-tail replay.
5.2 Effects & middleware (@loom/effect-runtime, @loom/middleware)
All side effects — tool calls, LLM calls, human waits, agent-to-agent calls — go through the
EffectRuntime, keyed by toolId. This is the isolation boundary that makes replay possible:
during replay, completed effects are served from the log, not re-executed. It is also the
authority boundary (Loom 2.0 K2): given an optional CapabilityAuthorizer (e.g. a
least-privilege GrantTable), the EffectRuntime authorizes every invocation and denies
ungranted effects — the effect never runs, an EFFECT_DENIED fact is logged (P9), and a
CapabilityDeniedError fails the step. This is the one protection ring a library can enforce; it is
opt-in, so omitting an authorizer keeps the permissive default. The imperative GrantTable has a
declarative companion, policy$: a PolicyAuthorizer enforces policy$ definitions (deny-wins
allow/deny effect lists + glob matchers on effect id / tenant / step) through the same ring.
Policies attach to a flow (flow$({ policies })), an agent (agent$({ policies }), bound to the
flow it runs), or app-globally (defineConfig({ security: { policies } })), and compose under
any authorizer — defense-in-depth: a policy can tighten a grant, never widen it
(ADR 0048).
Middleware wraps effect execution with cross-cutting concerns (retries with backoff, resource
metering, and later logging/idempotency/tracing) as a composable (ctx, input, next) => … chain —
the same shape as HTTP middleware, applied to effects
(ADR 0006). meteringMiddleware (Loom 2.0 K3) meters
each effect’s tokens/cost/time as a durable RESOURCE_METERED fact and enforces per-execution
budgets by admission control (over-budget ⇒ the next effect is denied) — the scarce resource for
intelligent software being tokens/$/latency, not CPU. safetyMiddleware (Loom 2.1 K6) is the same
shape applied to behavior: pluggable validators check an effect’s input before it runs and its
output before it propagates, and a violation is a durable SAFETY_BLOCKED fact that fails the step
— so the kernel governs not just who may act and how much it costs, but whether the
action/output is safe (ADR 0017). claimCheckMiddleware
(ADR 0054) keeps the log itself lean: an effect result over an
inline cap is stored content-addressed and the log records { ref, size, digest } as the value
— the fold never resolves it, so replay never touches the content store and bulk telemetry stays out
of the authoritative log (the digest, a per-tool declared pure projection, is what orchestration may
branch on). Stream-typed results (streamResult, ADR 0059) extend the same contract to payloads too
large for memory: the tool streams into the content store inside the effect and the envelope is
still the recorded value.
flowchart LR
Call["ctx.run(step)"] --> Auth{"CapabilityAuthorizer<br/>(K2 — granted?)"}
Auth -->|denied| Deny["EFFECT_DENIED<br/>CapabilityDeniedError → step fails"]
Auth -->|allowed| M1["retryMiddleware"]
M1 --> M2["meteringMiddleware<br/>(K3 — budget → QUOTA_EXCEEDED)"]
M2 --> M3["safetyMiddleware<br/>(K6 — validate in/out → SAFETY_BLOCKED)"]
M3 --> M4["redactionMiddleware<br/>(K10 — resolve secretRefs)"]
M4 --> M5["claimCheckMiddleware<br/>(ADR 0054 — over-cap result → ref+digest)"]
M5 --> Eff["Effect.execute<br/>tool · llm · human · a2a<br/>(K7 IsolatedToolEffect for tools kept off ambient authority)"]
Every middleware after retryMiddleware is opt-in; the authorizer, metering, safety, and redaction rings are each omittable and byte-identical to the permissive path when unwired (P10). The full ring set is catalogued in §5.7.
5.3 Projections & UI (@loom/projection-runtime, @loom/agent-surface, @loom/blueprint-runtime, renderers)
State for humans is a read model, folded from the same events by the ProjectionRuntime —
completely decoupled from durability. Flows push UI updates via surface(ctx).set/merge/append (the
agent-surface patch protocol — Loom’s native surface mechanism, not the external Google A2UI
wire protocol despite the historical name), which fold into surfaces. Surfaces render through
pluggable renderers (React, terminal) — bring-your-own-renderer. For interop,
@loom/plugin-a2ui projects a surface onto the real A2UI (Agent-to-UI) wire protocol v1.0 so
any A2UI-compliant client can render it — egress, the inbound action leg, and the callFunction
bidirectional RPC (the agent invokes a named client function; its result re-enters as a recorded
fact, keyed by a deterministic callId), kernel untouched
(ADR 0069,
ADR 0070). The durable log never depends on the
UI.
A projection can be declared with projection$ and discovered by loom gen. When the compiler
builds one, it extracts the whole handlers fold-map into its own import() chunk and records the
handled event-type keys — so the fold code loads on demand, event-type-gated: a projection whose
events never fire never loads its code (the same lazy-chunk mechanism as handler$/instructions$,
ADR 0049). At the config level,
defineConfig({ agentSurface: true }) registers one agent surface per agent$
(surfaceId = agent.id) so each agent drives its own panel, and a2a: true derives each agent’s
discoverable A2A card — both from the same agent$ definitions, no hand-wired cards/surfaces.
flowchart LR
Ev["event log"] --> PR["ProjectionRuntime<br/>(fold events → surfaces)"]
Ui["surface(ctx).append('timeline', …)"] --> PR
PR --> R1["React renderer"]
PR --> R2["Web Components"]
PR --> R3["terminal"]
5.4 Agentic layer (@loom/agent, @loom/plugin-a2a, @loom/llm, @loom/plugin-mcp, @loom/reasoning, @loom/capability, @loom/intent, @loom/planner, @loom/memory, @loom/knowledge)
The intelligence pipeline, all composable and all running inside durable flows:
@loom/llm— provider-agnostic model abstraction (LlmProvider), withAnthropicProvider,OpenAiProvider, andGeminiProvidershipped (and a VCR provider for tests); any provider drops in.@loom/agent— a ReAct-style tool-using loop; aSkillRegistryselectsskill$skills whose instruction bodies load lazily.@loom/plugin-mcp— Model Context Protocol client for external tools.@loom/plugin-a2a— Agent-to-Agent protocol (client + server): call external agents (ADK, LangGraph, CrewAI) and expose Loom flows to them;LoomAgentExecutoradapts a flow into a stateless A2A task (ADR 0007).defineConfig({ a2a: true })auto-advertises everyagent$as a discoverable card (no hand-built card table).@loom/reasoning— the decision seam’s policy side (M3, ADR 0016): model routing, elevation and confidence, plus the exploration strategies (ucb1,epsilonGreedy) a learned router draws from (ADR 0078). Every choice it makes goes throughctx.decide, so it is advisory at author time and a recorded fact at run time.@loom/capability— flows exposed as intent-addressable capabilities.@loom/intent— routes natural-language intent to a capability.@loom/planner— turns a goal into a plan: the original flatPlannerRuntime.plan, and (R16, ADR 0123)planGoal/executePlan/makeReviserfor journaled, validated plan DAGs with argument bindings and bounded replanning, plus a principal-scopedGoalStore(runGoal).@loom/memory— (R18, ADR 0124: an optionalEmbeddingProvider/VectorIndexSPI andEmbeddingMemoryStore, a semanticMemoryStoreover any store) scoped, TTL/size-bounded agent memory, over a pluggableMemoryStore(in-memory by default;SqliteMemoryStore/PostgresMemoryStoremake it survive a restart and add relevancesearch, whichTieredMemory.recallRelevantturns into a recorded decision). An entry carries an optional caller-assignedidthat every backend must round-trip, and the optionalremovedeletes by it — the verb a MUTABLE corpus needs, sincesearchranks within one key while a document is a subset of it (ADR 0091,MEMORY_STORE_SPI_VERSION2.1.0 — 2.1.0 bounds the QUERY: a term-based backend derives its terms withqueryTerms, deduplicated and capped, because OR-ing one word 20,000 times cannot change which entries match and took SQLite 32.7 seconds to say so).@loom/knowledge— document corpora over a memory tier (ADR 0091):KnowledgeBaseupserts andsyncs DOCUMENTS onto the PASSAGES they are searched in (so a revision supersedes its predecessor rather than duplicating it),groundedRequestassembles a prompt that keeps retrieved passages in the one positionguardRequestaccepts, andcheckCitationsrejects a citation to a document that was never retrieved. It owns no storage and no ranking — both are theMemoryStore’s. A corpus may have several writers (source): a reconcile only ever deletes its own documents, so what an agent learned from feedback survives the nightly sync of an authored corpus.@loom/orchestrator— composabledecideApproval(risk gate) +dispatchPlanprimitives (ADR 0013).
5.5 Compute & distribution (@loom/dispatcher, @loom/worker-runtime*, @loom/plugin-worker-remote, @loom/plugin-worker-server, @loom/plugin-cloudrun, @loom/graph-executor, @loom/scheduler)
Placement is a seam between the orchestrator and the runtime, so executions can run anywhere without touching the core (ADRs 0001–0005 (loom/docs/adr/)):
flowchart LR
Orq["orchestrator"] --> D["ExecutionDispatcher<br/>(where does it run?)"]
D --> W1["worker-runtime-local<br/>in-process"]
D --> W2["plugin-worker-remote<br/>HTTP → plugin-worker-server"]
D --> W3["plugin-cloudrun<br/>CloudRunWorkerRuntime"]
W1 --> RT["WorkflowRuntime (shared stores)"]
W2 --> RT
W3 --> RT
- An
ExecutionPackagecarries resume coordinates (identity + sequence), never code or closures — the worker ships its own code bundle. Because code can’t be serialized, workers reference flows/effects by id from a statically-wired registry (P5). @loom/graph-executorfans a graph of nodes into per-node executions (one writer each), joined via theBranchRegistry— never multi-writer on one execution (ADR 0005).@loom/scheduleris an ephemeral cooperative scheduler (priority, bounded concurrency, cancellation) for the agentic layer — replay-safe and not part of durable state.
5.6 DX tooling (@loom/cli, @loom/appgen, @loom/eval, @loom/deploy-sim, @loom/testing, @loom/scaffold, @loom/compiler)
Every one of these is a different lens on the durable log:
@loom/eval— replay a recorded log as a test fixture; diff/score runs (ADR 0008); and (R18, ADR 0124) an LLM evaluation harness —runEvalSuitewith exact/regex/JSON-Schema/LLM-judge graders (the judge through the decide scope), a replay-onlyCassetteProvider, andcompareReportsfor regressions.@loom/deploy-sim— replay historical logs against new flow definitions to catch breaking changes before promoting.@loom/testing—createTestApp,waitForPhase,captureGolden, and a VCR LLM provider (ADR 0009).@loom/cli(loom) —logs(tail the log),debug(time-travel to any sequence),eval,check,gen/new(build-time codegen —gendiscovers all seven convention folders: flows, agents, tools, skills, schedules, memories, projections, ADR 0011),appgen(brownfield NL → code, model-of-your-choice),deploy check,inspect(web timeline + slider) (ADRs 0010–0014 (loom/docs/adr/)).@loom/appgen(+ optional@loom/appgen-model) — brownfield-aware NL → code: scan → plan (provider-agnostic, ADR 0050) → surgical apply → validate; the NL stage is isolated and the output is plain, statically-wired code (P11).@loom/compiler— optional Vite plugin/CLI:handler$/instructions$/projection$extraction into lazy chunks, flow-graph analysis, resumability metadata.
5.7 Kernel-hardening rings (K1–K12, T1–T2)
The rings noted inline above (K2/K3/K6) are part of a larger set that makes Loom earn the word “kernel.” Each guardrail ring is opt-in and inert when unwired (P10; the durability defaults such as heartbeat leases and the recorded principal are on without configuration), and each new durable verdict is folded like any other event (P9). The full set is implemented:
- K1 — versioned syscall ABI (
KERNEL_ABI,packages/workflow-runtime/src/abi.ts); K2 — capability grants / authority ring (EFFECT_DENIED); K3 — resource metering & quotas (RESOURCE_METERED/QUOTA_EXCEEDED); K4 — fault supervision (SupervisingDispatcher, restart/backoff + circuit breaker); K5 — event-schema ABI governance (EVENT_SCHEMA_VERSION,knownEventTypes()); K6 — behavioral safety (safetyMiddleware,SAFETY_BLOCKED). - K7 — data-plane isolation & brokered egress (
@loom/isolate, child-process boundary,EGRESS_DENIED); K8 — cancellation, deadlines & bounded effects; K9 — effect-delivery contract: idempotency keys, in-doubt parking (EFFECT_INDOUBT), fencing; K10 — log lifecycle: retention, crypto-shredding (PAYLOAD_SHREDDED), redaction & at-rest encryption (@loom/plugin-crypto), and the K10.6 pin seam (PinPredicate+ pinned content GC — findings’ evidence outlives raw-telemetry TTL and run archival, ADR 0055); K11 — flow-code versioning for in-flight executions (flowContentHash,EXECUTION_MIGRATED) plus K11.5 contract-versioned state migration onresumeAs(the recordedmigratedInputreplaces the folded input, ADR 0056); K12 — identity, tenancy & delegated authority (APPROVAL_GRANTED). - T1 — durable timers & scheduled executions (
ctx.sleep/sleepUntil,TIMER_FIRED,schedule$cron); T2 — wire-level kernel ABI (@loom/wire-abi, a JSON-RPC projection of K1 for language neutrality).
flowchart TB
Sys["Minimal kernel — K1 syscall ABI · K5 event-schema governance<br/>execute · run · decide · negotiateBudget · parallel · suspend<br/>sleep · sleepUntil · receive · select · continueAsNew · emit · stream · ui (deprecated)"]
subgraph Rings["Enforcement rings — opt-in, byte-identical when unwired (P10)"]
K2["K2 authority → EFFECT_DENIED"]
K3["K3 quotas → QUOTA_EXCEEDED"]
K6["K6 safety → SAFETY_BLOCKED"]
K7["K7 isolation / egress → EGRESS_DENIED"]
K8["K8 cancel / deadline → EXECUTION_CANCELLED"]
K9["K9 delivery / fencing → EFFECT_INDOUBT"]
K10["K10 log lifecycle / crypto / K10.6 pins → PAYLOAD_SHREDDED"]
K11["K11 flow versioning / K11.5 state migration → EXECUTION_MIGRATED"]
K12["K12 identity / tenancy → APPROVAL_GRANTED"]
end
Svc["Kernel services — T1 durable timers (TIMER_FIRED) · T2 wire ABI (JSON-RPC projection of K1)"]
Sys --> Rings
Sys --> Svc
Rings --> Log[("event log — every verdict folds as a durable fact (P9)")]
Svc --> Log
Each ring is a distinct plane — authority (K2/K12), resource (K3), behavior (K6), data
(K7/K10), and lifecycle (K8/K9/K11) — and each verdict is an ordinary folded event, so the log is
the audit trail with no extra machinery. See
04 — Kernel Hardening (loom/docs/loom-2.0/04-kernel-hardening.md) for the ring-by-ring detail, the API
reference → Kernel hardening & security for each ring’s
export surface, and ADRs 0016–0026 (loom/docs/adr/).
5.8 The three-direction arc — simulation testing, coordination, multi-tenant isolation
Three frontiers built as opt-in packages over the stable syscall surface (ADRs 0036–0041). The
throughline is P10: only ONE of the six touched a kernel contract — an additive
EXECUTION_DENIED event (EVENT_SCHEMA_VERSION 1.12.0); KERNEL_ABI is unchanged at 1.1.0. Two
plan-proposed kernel changes (a CHILD_COMPLETED event and a ctx.compensate syscall) were found
unnecessary and dropped — the work composes from existing primitives.
- Deterministic Simulation Testing —
@loom/dst: a seeded virtual clock + fault-injecting store (generalizingcrashingEventStore: crash / appendReject / msgLoss / msgDup, explore→record / replay modes) drive the real kernel under adversarial schedules; five invariants +checkPropertysearch→shrink→replaySchedule. Test/dev-only ⇒ zero production overhead by construction. - Multi-tenant isolation —
@loom/plugin-audit-chain(fold-invisible hash chain + optional HMAC, tamper detection viaverifyChain),@loom/fair-scheduler(weighted round-robin across tenants behind the concurrency cap; drop-inScheduler), and execution-lifecycle authorization (a pluggableExecutionAuthorizergating start/resume/cancel/query at the trust boundary → durableEXECUTION_DENIED). - Post-hoc inspection —
@loom/replay-inspector(ADR 0071): snapshot-anchored state reconstruction at any log position, transition diffs, and projection slices, all under the version the execution is pinned to. Read-only by type (Pick<EventStore, "read" | "readFrom" | "readRange">—appendand evensubscribeare out of scope), so it cannot perturb what it inspects. Two findings came out of building it: the durable pin isvariables._flowHash, notExecutionState.workflowVersion(whichfoldStateseeds from currently-registered code), and projection state has no snapshot representation, so a projection fold always replays from zero. - Coordination primitives —
@loom/live-query(loadState +EventStore.subscribe),@loom/orchestration(spawn child workflows and durably await them via decide/suspend/resume + a completion-correlating supervisor —awaitChildfor one,awaitChildrenfor a concurrent wait-allChildResult[],awaitAllfor all-or-nothing; child failure propagates as a typedchildFailedthat composes up the tree), and@loom/saga(register compensators, run them in reverse on failure as replay-safectx.runsteps).
flowchart TB
Sys["Stable syscall surface — KERNEL_ABI 1.3.0<br/>execute · run · decide · negotiateBudget · parallel · suspend<br/>sleep · sleepUntil · receive · select · continueAsNew · emit · stream · ui (deprecated)"]
subgraph Iso["Multi-tenant isolation (Direction C)"]
AZ["execution-lifecycle authz → EXECUTION_DENIED (only schema touch: 1.12.0)"]
FS["@loom/fair-scheduler — per-tenant weighted RR"]
AC["@loom/plugin-audit-chain — hash chain + HMAC (fold-invisible)"]
end
subgraph Coord["Coordination primitives (Direction B)"]
LQ["@loom/live-query — push state on each event"]
OR["@loom/orchestration — child spawn + await (no new syscall)"]
SG["@loom/saga — reverse-order compensation (ctx.run steps)"]
end
DST["@loom/dst — Deterministic Simulation Testing (test/dev-only)<br/>seeded clock + fault store · invariants · shrink · replay"]
Sys --> Iso
Sys --> Coord
DST -.validates.-> Iso
DST -.validates.-> Coord
DST -.validates.-> Sys
DST is the throughline the other two lean on: audit-chain integrity, and saga’s no-double-compensation, are each proven by a DST property under seeded crashes. See ADRs 0036–0041 (loom/docs/adr/) and the maturity badges in docs/spec/maturity.md.
6. Package map
The workspace is split by tier (ADR 0043): packages/ holds the kernel (mechanism/ABI), the
SPI-contract packages, and the advisory modules; tools/ holds first-party dev-tooling —
leaf packages consumed only at build/test/authoring time (cli, compiler, appgen,
appgen-model, deploy-sim, scaffold, eval, testing, dst, bench-throughput,
create-loom-app);
plugins/ holds the first-party adapters — concrete backends for a specific external system — as @loom/plugin-*
(sqlite, postgres, crypto, content, durable-state, read-model, sentinel,
audit-chain, cloudrun, worker-remote, worker-server, mcp, a2a, renderer-react,
renderer-terminal). A CI layering guard forbids any
kernel/SPI package from depending on a plugins/* adapter, so the kernel names no vendor and ships
no native driver. Concrete backends are supplied as presets (sqliteStorage(), …) or adapters
passed to defineConfig — a third-party plugin (loom-plugin-*) wires identically. Plugin
authoring is supported by @loom/plugin-kit (typed presets) and @loom/conformance (SPI
conformance kits). See plugins.md and ADR 0043.
flowchart TB
core["@loom/core (builders + types)"]
replay["@loom/replay-vm"]
events["@loom/event-runtime"]
snap["@loom/snapshot-runtime"]
effects["@loom/effect-runtime"]
wrt["@loom/workflow-runtime"]
app["@loom/app (defineConfig)"]
core --> wrt
replay --> wrt
events --> wrt
snap --> wrt
effects --> wrt
wrt --> app
events --> app
snap --> app
effects --> app
mw["@loom/middleware<br/>(K3/K6/K10/K12)"] --> app
proj["@loom/projection-runtime"] --> app
a2a["@loom/plugin-a2a"] --> app
iso["@loom/isolate (K7)"] --> effects
crypto["@loom/plugin-crypto (K10)"] --> events
crypto --> snap
wire["@loom/wire-abi (T2)"] -. "projects K1 ABI" .-> wrt
app --> examples["examples / your app"]
cli["@loom/cli"] --> examples
testing["@loom/testing"] --> examples
disp["@loom/dispatcher + worker-runtime*"] --> app
audit["@loom/plugin-audit-chain"] -. wraps .-> events
content["@loom/plugin-content<br/>(ContentStore + K10.6 pinned GC)"] -. "refs in payloads" .-> events
dstate["@loom/plugin-durable-state<br/>(carry state as recorded effects)"] --> effects
rm["@loom/plugin-read-model<br/>(durable cross-execution event index)"] -. "tails onEvent + catch-up" .-> events
sentinel["@loom/plugin-sentinel<br/>(the pattern as one preset)"] --> dstate
fair["@loom/fair-scheduler"] -. "drop-in Scheduler" .-> disp
lq["@loom/live-query"] --> wrt
orch["@loom/orchestration"] --> wrt
saga["@loom/saga"] --> wrt
dst["@loom/dst (test/dev)"] -. drives .-> wrt
| Group | Packages |
|---|---|
| Core runtime | core, workflow-runtime, replay-vm, event-runtime, snapshot-runtime |
| Composition | app (defineConfig), registry, sdk (the public re-export barrel) |
| Effects | effect-runtime, middleware |
| Projection / UI | projection-runtime, agent-surface, blueprint-runtime, plugin-a2ui, plugin-renderer-react, plugin-renderer-terminal, plugin-renderer-dom |
| Agentic | agent, llm, reasoning, capability, intent, planner, memory, knowledge, orchestrator, plugin-a2a, plugin-mcp, plugin-mcp-app, sandbox (the backend SPI, ADR 0144), plugin-sandbox, plugin-sentinel (the perpetual-agent pattern as one preset) |
| Compute / distribution | dispatcher, worker-runtime, worker-runtime-local, graph-executor, scheduler, plugin-worker-remote, plugin-worker-server, plugin-cloudrun |
| Storage backends | plugin-sqlite, plugin-postgres, plugin-durable-state, plugin-read-model |
| Governance / verification | spec (the kernel manifest + layering guard), conformance (SPI kits), verify, replay-inspector (ADR 0071) |
| Kernel hardening / security | effect-runtime (K2 authz + K12 delegation), middleware (K3 metering, K6 safety, K10 redaction, K12 nested budgets), isolate (K7 data-plane isolation & egress), plugin-crypto (K10 encryption & crypto-shredding), event-runtime/snapshot-runtime (K12 tenant scoping), wire-abi (T2), telemetry (K10-R6 egress scrubbing) |
| Data protection & compliance | classify (K10-R1 secret/sensitive/internal/externalized markers + the ADR 0085 tainted() integrity axis), compliance (T3 subject index + export + erasure, tenant-scoped), plugin-content (ADR 0046 content-store seam) — see data classification & compliance |
| Intelligence / DX | analytics, learning (off-policy evaluation + the 3P Policy seam, ADR 0080), appgen (provider-agnostic planner), appgen-model (optional local .gguf option), cli, eval, deploy-sim, testing, scaffold, compiler, bench-throughput, telemetry |
| Three-direction arc | dst (simulation testing), plugin-audit-chain + fair-scheduler + execution-authz in workflow-runtime (multi-tenant isolation), live-query + orchestration + saga (coordination) — ADRs 0036–0041 |
Adapter packages carry the
plugin-prefix and live inplugins/(loom/docs/plugins/), notpackages/— the 1P/3P separation of ADR 0043. A CI guard fails the build if a kernel or SPI package takes a dependency on one. See the plugin guide for the full first-party inventory.
Loom 2.0. The packages in bold-new groups above —
reasoning,plugin-postgres,analytics,appgen— plus deepenings ofworkflow-runtime,effect-runtime,middleware,dispatcher,memory,capability, andagent, constitute the execution-kernel evolution. Its full data structures, algorithms, and diagrams live indocs/loom-2.0/(loom/docs/loom-2.0/00-overview.md); the decision record is ADR 0016.
7. Decision records — reasoning & advantages
Each subsection is a decision, why it was made, and what it buys you. The distributed-compute and DX decisions are captured in full in docs/adr/ (loom/docs/adr/); this section explains the foundational choices and links the ADRs.
D1 — Event sourcing as the substrate
- Context. Long-running, human-in-the-loop, non-deterministic AI flows must survive crashes and be explainable.
- Decision. Never store execution state directly; store an append-only log of events and derive state by folding (P1).
- Reasoning. A single source of truth that is immutable and ordered turns hard problems (durability, audit, resume, replay) into consequences of one design. State-in-place storage would require bespoke solutions for each.
- Advantages. Free audit trail; time-travel to any point; resume from any checkpoint; the same log powers evals, deploy-sim, and debugging. Storage is append-only, which is cheap and contention-free.
D2 — Deterministic, pure replay
- Context. A resume, a fork, and a debug-replay must reconstruct identical state; AI outputs are non-deterministic.
- Decision. The fold is a pure function; effects are quarantined behind
EffectRuntimeand served from the log during replay (P2). - Reasoning. If reconstruction had side effects or read a clock, two replays could diverge and the whole model would collapse. Purity is what makes “replay” trustworthy.
- Advantages. Exactly-once effects across resumes/crashes; reproducible debugging (“run it again, get the same thing”); logs become deterministic test fixtures (ADR 0008).
D3 — The ExecutionJournal is the sole sequencer/writer
- Context. Concurrent commits could race on sequence numbers and corrupt order.
- Decision. One journal per execution owns sequence allocation and is the only appender; the in-memory fold is synchronous, the durable write is an async ordered tail.
- Reasoning. A single allocator makes gaps/duplicates structurally impossible; separating the sync fold from the async write keeps the flow fast while preserving order and surfacing storage failures at barriers.
- Advantages. Correct order with no locks; a backend never needs a read-then-write to allocate a sequence (so network stores like Postgres plug in cleanly); write failures abort deterministically.
- Non-idempotent steps. The write-behind tail is drained (an awaited store round trip) after
STEP_STARTEDand before anidempotent: falseeffect is dispatched, so the in-doubt marker is durable before any side effect (ADR 0021 A3 amendment).
D4 — Orchestration is a plain async function, not a DSL (P4)
- Context. Workflow engines usually impose a graph DSL or decorators.
- Decision. A flow’s runner is
(ctx, input) => Promiseusing ordinary control flow;ctx.run/suspend/parallelare the only durability touch-points. - Reasoning. Developers already know
async/await,if,for. A DSL adds a learning curve, limits expressiveness, and fights the type system. Loom adds durability around normal code instead of replacing it. - Advantages. Full language power and type inference; trivial to test as functions; near-zero conceptual overhead; your logic isn’t trapped in a proprietary format.
D5 — Static, build-time wiring; conventions via codegen (P5)
- Context. Convenience features (folder routing, auto-registration) tempt you toward runtime discovery.
- Decision. All registration is static from explicit imports; the folder convention is honored by
loom genemitting a staticloom.gen.ts, not by runtime globbing (ADR 0011). - Reasoning. A remote worker or a replay must reconstruct an identical set of flows/effects. Runtime discovery makes that environment-dependent and breaks determinism.
- Advantages. Replay-safe distribution; no “works on my machine” registration drift; codegen output is inspectable and diffable.
D6 — defineConfig as the composition root; middleware seam
- Context. Wiring a runtime, effects, projections, and A2A transport was ~35 lines copy-pasted per app.
- Decision.
defineConfig(LoomConfig): LoomAppassembles everything from a static config; effects gain a composable middleware chain (ADR 0006). - Reasoning. One declarative root removes boilerplate without hiding the primitives (it returns the runtime/stores). Middleware is the idiomatic place for retries/logging/idempotency.
- Advantages. Apps shrink dramatically; cross-cutting concerns are opt-in and testable; the empty-middleware case is byte-identical to before (no behavior change).
D7 — Projections decoupled from durability; bring-your-own-renderer
- Context. UI concerns tend to leak into persistence.
- Decision. Read models are folded independently by the
ProjectionRuntime; flows emit UI via the agent-surface patch protocol; renderers (React/WC/terminal) are pluggable (P8). - Reasoning. The durable log must not depend on how (or whether) anything is rendered. Separating projection from persistence lets the same backend drive any front end — proven by the same app rendered in React, Web Components, and Angular.
- Advantages. UI-framework independence; headless operation; live surfaces without coupling; the log stays clean.
D8 — Pluggable compute via a placement seam
- Context. Executions should be able to run in-process, over HTTP, or on Cloud Run without changing the core.
- Decision. Insert
ExecutionDispatcher+WorkerRuntimeSPI between orchestrator and runtime; anExecutionPackagecarries resume coordinates (never code); workers reference flows by id (ADRs 0001–0004 (loom/docs/adr/)). - Reasoning. Closures can’t be serialized, and the core must stay cloud-agnostic. Shipping coordinates (not code) and resolving flows by id from a static registry keeps the public SDK unchanged while enabling remote execution. Suspension is a normal outcome across a network boundary.
- Advantages. The same flow runs locally or on Cloud Run; stateless workers recover from crashes by folding shared stores; the public SDK is untouched.
D9 — Distributed fan-out via per-branch executions
- Context. Parallel graph nodes must not violate the single-writer invariant (D3).
- Decision.
graph-executorgives each node its ownexecutionId(one writer each) and joins lineage through theBranchRegistry(ADR 0005). - Reasoning. Multiple writers on one execution would break sequence ordering. Per-node executions preserve D3 while still enabling concurrency.
- Advantages. Safe distributed fan-out/join; each node independently durable, resumable, and replayable.
D10 — Git-style branching (fork/simulate/promote), no merge
- Context. Teams want to explore alternative agent decisions and compare outcomes.
- Decision. Fork = copy the event prefix into a new execution and resume differently; promote = a ref pointer move; no merge (ADR 0005 (
loom/docs/adr/), examplesincident-war-room,schema-migration-planner). - Reasoning. Because state is
fold(events), branching is nearly free and needs no new persistence model. Merging divergent AI executions has no sound semantics, so it’s deliberately omitted; promotion is a decision, recorded. - Advantages. Cheap “what-if” exploration; losing branches kept as an immutable audit trail; the winning path is committed for real via one more fork with
simulate:false.
D11 — Tooling is lenses on the log, not new infrastructure
- Context. Evals, deploy-safety, logs, and time-travel debugging are usually separate systems.
- Decision. Build them all as pure functions over the event log:
evalreplays it,deploy-simdiffs it against new flows,logstails it,debug/inspectfold it at any sequence (ADRs 0008, 0010–0014 (loom/docs/adr/)). - Reasoning. The log already contains everything. A pure fold at a chosen sequence is time-travel; replaying a stored log is an eval. Reusing one engine (
foldAt) keeps them consistent. - Advantages. Minimal new surface area; all tools agree because they share the fold; CI gates (eval, deploy check) fall out for free.
D12 — Algorithms tuned on the hot paths, contract unchanged
- Context. The fold runs on every commit/resume; a few tooling paths were quadratic.
- Decision. Copy-on-write fold, periodic snapshots, bulk fork copy, and O(N) tooling passes — with the public SDK untouched (ADR 0015).
- Reasoning. The most-executed operation deserves the tightest constant; correctness is pinned by tests for each invariant.
- Advantages. ~7.7× faster wide-flow folds, bounded recovery, no JS re-serialization on fork, and no breaking changes.
8. Trade-offs & non-goals
Being honest about the edges:
- Not a general job queue. Loom optimizes long-lived, stateful, human-in-the-loop processes. For fire-and-forget high-throughput jobs, a queue is simpler.
- No branch merge. Branching is fork/simulate/promote by design (D10). Merging divergent executions is intentionally out of scope.
- Determinism constrains the core. Library code on replay paths avoids wall-clock/randomness; non-determinism belongs inside effects (which are logged), not in orchestration.
- Static wiring over dynamic discovery. You register flows/effects explicitly (or via
loom gen). This is a deliberate cost paid for replay-safety (P5). - Compiler is optional.
handler$extraction and resumability metadata are a build-time enhancement; the runtime works without it. - Snapshots trade writes for replay speed.
snapshotEveryis a knob: smaller N = faster recovery, more writes.
See also
- Getting Started — from zero to a running, suspend/resume flow in minutes.
- Usage — build a real app step by step.
- User Guide — every feature area with examples.
- Data classification — field-sensitivity markers (
secret()/sensitive()/internal()/externalized()) that drive encryption, redaction, and egress scrubbing (K10-R1/R6). - Compliance — data-subject rights: subject index, export (Art. 15/20), and erasure (Art. 17), tenant-scoped (T3).
- ADRs (
loom/docs/adr/) — the full decision history (0001–0147), one line per range:- 0016–0026 cover the Loom 2.0 execution-kernel evolution and the K7–K12/T1–T2 hardening series
- 0036–0041 the three-direction arc
- 0042 the AI-native brownfield
appgen - 0043–0044 the plugin packaging + 1P/3P separation
- 0046 the content-store seam
- 0047 the
tools/*dev-tooling tier - 0051–0057 the perpetual-agents series — schedule-driven agents with durable carry state, claim-checked effect data, pinned retention, state migration on resume, and the multi-gate suspend fix
- 0058–0059 the db-copilot infrastructure follow-on — durable cross-execution read models and streaming content ingestion
- 0060–0065 the GenUI dynamic-execution series — typed failure channels, the step-identity/interpreter contract, batched content resolution, MCP dynamic dispatch with schema-hash pinning, the QuickJS sandbox plugin, and the group-commit deferral
- 0066 the transient-effect-invocation façade (
invokeTransient) - 0067 the reactive-graph-resumability rejection
- 0068 the projection-selector subscription (
subscribeSelector) - 0069–0070 the agent-surface rename and the A2UI egress adapter with
callFunctionbidirectional RPC - 0071 the read-only replay inspector
- 0072 MCP Apps placement — ingress fidelity, an MCP server, and
ui://egress as one static content-pinned template, with the host role rejected - 0073 the effect-mode boundary (
journaledvsambient) that keeps user-driven enrichment reads out of the log - 0074–0075 storage scale-out decorators and shard rebalancing
- 0076–0077 two boundaries that looked enforced and were not (wildcard classification paths, typed resume payloads)
- 0078–0080 the learning substrate — exploration in the routing seam,
context/policy.propensityonDECISION_RECORDED(EVENT_SCHEMA 1.18.0), and@loom/learningwith the third-partyPolicySPI - 0081–0082 the Companion workload’s two kernel promotions, both refused
- 0083 the second-consumer rule, an outward gate on first-party surface
- 0084 effect classes — a declared, statically-checked ceiling on what a flow may reach, beside K2’s runtime floor
- 0085 the integrity axis — untrusted content as a value wrapper, refused in every prompt instruction position
- 0086 runner-body reach analysis, which closed the ‘reachable = declared steps’ bound and proved two suspension-safety checks wrong once they could fire
- 0087 the durable agent loop —
agentLoop$as a runner body, so a turn is a frame and skills/memory/routing go through the decision seam - 0088 blueprint conformance —
validateBlueprint/catalogCoverageover a declared widget catalogue - 0089 the sandbox worker pool
- 0090 knowledge is retrieval over memory, not a construct — durable MemoryStore backends,
searchas one optional verb, and the refusals that keep knowledge in application code - 0091 the correction to 0090’s premise — a corpus is MUTABLE, so entry identity plus
remove(MemoryStore SPI 2.0.0) and@loom/knowledgefor the document↔passage mapping, while theknowledge$construct stays rejected - 0092 adaptive operator profiles — placement only, with the expiry that would have made a fold impure moved into an event and the resolved profile journaled by VALUE
- 0093 row-granular
purgeon the read-model store SPI (1.1.0), so erasing one subject costs one subject rather than a whole-model rebuild - 0094 node identity across a structural change —
preservesNodesover id sets, because the second consumer’s components are a flat list rather than a tree - 0095 the binding-semantics pin — a composed surface’s content address freezes a binding’s NAME, so what that name MEANT is declared (
version) and compared (checkVersionPins) rather than assumed - 0096 the Blueprint as loom’s stable rendering IR —
BLUEPRINT_IR_VERSIONbeside a shape manifest thattscchecks against the types and a test pins by digest per version, and a kind-aware terminal renderer whose default registry covers the catalogue - 0097 Blueprint IR 2.0.0 — grid cells, loom-owned binding status, widget source, binding sensitivity, with a reader-side normaliser for 1.0.0 artifacts
- 0098 Blueprint IR 2.1.0 — optional grid-cell placement (
row/rowSpan/column) and atitleon every layout, a minor bump that reads 2.0.0 artifacts as they are - 0099 Blueprint IR 2.2.0 — an optional
roleon every layout, the open container axisWidget.kindis for widgets, which loom’s own renderers ignore - 0100 Blueprint IR 2.3.0 — an
emptybinding state, so a widget whose upstream answered with nothing shows “no data” rather than an error, degrading to the loading row on an older reader - 0101 Blueprint IR 2.4.0 — an optional
idon every layout, one node-id namespace with widget ids, so a container keeps its identity (and the state under it) when a recompose inserts or reorders containers, withpreservesBlueprintNodescovering it - 0102 a kernel replay fix — flow-level
ctx.emit/stream/uiappend once per execution, matched by content digest against the log (never by timing; an unprovable match appends), rather than once per re-run (EVENT_SCHEMA 1.19.0, KERNEL_ABI unchanged) - 0103 a kernel fix — a
start()with a caller-chosen execution id makes itsEXECUTION_STARTEDdurable before the body runs, so a duplicate start is refused withExecutionAlreadyExistsErrorbefore any effect runs (EVENT_SCHEMA and KERNEL_ABI unchanged) - 0104 a gate is answered only by a resume addressed to it — a crash re-drive, an in-doubt verdict, a human step’s answer, a migration and userspace kernel-type emits no longer open the NEXT gate, pre-0104 logs replay as written and are flagged (EVENT_SCHEMA 1.20.0)
- 0105 a kernel fold fix — a step failure the flow catches no longer folds the EXECUTION to
failed(onlyEXECUTION_FAILEDdoes), so a crash after the catch is recoverable, cancellable and not pruned as finished, a parked execution whose sibling failed stays resumable (anullresume after that failure stays status-only, so no snapshot changes a gate’s answer), and pre-1.21.0 snapshots are repaired on read (EVENT_SCHEMA 1.21.0, KERNEL_ABI unchanged) - 0106 compaction integrity — a checkpoint names its execution, a re-compaction folds on the earlier checkpoint, and an app’s pinned event kinds survive compaction in place (EventStore SPI 1.5.0,
truncatePrefixKeeping) - 0107 its two residuals — a fork point inside compacted history is served exactly from a snapshot sitting at it or refused (
CompactedForkPointError), and the events the pre-0106 bug wrote to execution""are re-homed where a snapshot proves their owner and listed, never guessed, where none does (loom repair blank-stream; EVENT_SCHEMA 1.22.0EVENTS_REHOMED), with""now reserved - 0108 one order for execution ids — code point order with id cursors (EventStore SPI 2.0.0), a
COLLATE "C"Postgres column, and a fail-soft policy for boot migrations that take an exclusive lock - 0109–0116 and 0118 the Wave 2 coordinated kernel design (each Accepted at its item PR; 0109,
0110, 0111, 0112, 0114, 0115, 0116 and 0118 are) — bounded history and continue-as-new (0109,
behind a
kernel.featuresflag), a durable inbox with kernel waits on inbox topics, gates and timers, an authorizedruntime.sendand concurrent waits (0110, behindkernel.features.inbox), the durable principal (0111), structured concurrency (0112), a body-aware code fingerprint with replay-divergence detection (0113),surface(ctx)leaving the kernel (0114), open effect/task/metering vocabularies (0115), late outcome attach (0116) and a conditional acquire-if-parked fence (0118), planned as ONE bump (KERNEL_ABI 1.3.0, EVENT_SCHEMA 1.23.0, EventStore SPI 2.1.0) - 0117 approvals bound to what the approver saw
- 0119 streaming deltas through
DecideScope.invokeTransient— a per-call live-only sink onEffectContext.onDelta, governed identically to the non-streaming call (additive 1P, EVENT_SCHEMA and KERNEL_ABI unchanged) - 0121 one canonical agent loop —
agentLoop$gains tool-error feedback,parallelTools,signal/deadlines, memorywriteBackand a journaledreasoningresponse, andAgentRunner/agentStep$are deprecated (1P@loom/agent, kernel unchanged) - 0122 a conversation layer, a streaming transport and token-aware budgeting — a dialogue is
one chained execution (
conversation$,@loom/conversation) whose user turns arrive on the inbox, a new turn barges in on a running one through a host-side signal read only atagentLoop$’s recorded checkpoints, the transcript is a set of decisions read back with bounded range reads across rotations, the prompt is fitted to a window by a recorded verdict, and@loom/plugin-conversation-sseis the authenticated, resumable SSE gateway (1P, kernel unchanged) - 0123 planning depth — a plan is a validated DAG of steps with arguments,
$frombindings, dependencies and preconditions, planned as a recorded decision with no silent fallback, driven by a dependency-aware wave executor over concurrent scopes, revised by a bounded replanner that never re-runs completed steps, and tied to a principal-scoped durable goal store (all in@loom/planner, kernel unchanged) - 0124 the learning lifecycle — a principal-scoped policy registry (immutable versions,
promotion only through a passing verdict, shadow/canary/full rollouts with a deterministic unit
split recorded in the decision, one-write rollback), content-addressed prompt/model versions
recorded in the decision’s policy stamp, doubly-robust evaluation with seeded bootstrap
intervals and a CI-lower-bound gate, a scoped, taint- and erasure-aware JSONL
trajectory/preference export, an embedding SPI with a reference index and a semantic
MemoryStore, and an LLM eval harness (1P@loom/learning/@loom/memory/tools/eval, kernel unchanged) - 0125 tool search — an opt-in
search_toolsmeta-tool inagentLoop$whose results are a recorded decision, over only the tools the run is already authorized for, ranked by a bounded deterministic BM25F index (name > aliases/tags > description) behind a pluggableToolRanker, with trusted author/host aliases closing the vocabulary gap and remote descriptions kept tainted and unranked (1P@loom/agent, kernel unchanged) - 0126 Unicode lexical scoring, prompt overrides and schedule time zones (R19) — the
capability/skill keyword scorers tokenize any script (ASCII byte-identical, skill selection
already recorded), the planner/intent prompts are overridable with the untrusted-content note
always appended, and
schedule$({ timeZone })reads cron on an IANA zone’s wall clock with a stated DST rule (1P, kernel unchanged) - 0127
@loom/plugin-renderer-dom— a framework-free DOM renderer for the Blueprint (a web surface without React or loom-ui): the terminal renderer’s API plus a live, keyed-reconcilemountBlueprint, the same eight kinds as React and terminal asserted by a parity test, every element built through an allowlistingh()so data is never parsed as markup (1P plugin, kernel unchanged) - 0128 carrying decision order through checkpoints (a recorded
seqon each folded decision; EVENT_SCHEMA 1.24.0) - 0129 closing the
creationMarkerABA with a store-assigned incarnation kept out of the envelope (EventStore SPI 2.3.0) - 0130 the sealed-payload delivery guard and its residuals, all Accepted: the kernel refuses
to deliver a sealed (
__loomEnc) inbox payload to a node that cannot open it (parks, never journals the envelope;sendrefuses a forged one); a per-envelope capability markermetadata.seal(D7; EVENT_SCHEMA 1.25.0); whole-state snapshot sealing (sealState) with the kernel refusing a plaintext checkpoint (D8,PlaintextCheckpointError); a journaled per-topic hold,quarantineInbox/releaseInbox(D9); a replay refusal that leaves a wake row the poll re-drives (D10); and heartbeat leases (D11, slices S7a-S7g) — every driver holds a renewed lease from a store co-located with the event store (durability.heartbeat,LeaseStoreSPI 2.2.0,LeaseKeeper), so an expired lease is evidence of an abandoned driver: an unexpired lease vetoesisAbandoned, the timer poll re-drives by default on an expired lease over a cluster-visible store, ghost leases are garbage-collected, and acquisition is lazy for a freshstartby default (S7g amendment of 2026-10-15: eager measured 25-30% no-op overhead on Postgres). Residual risks stay documented in the ADR. Operating it: operations - 0131 loom’s own evals (Accepted): retrieval gates and cassette-replayed model evals for the
first-party agentic layer, in a repo-internal unpublished workspace that dogfoods
@loom/eval, with committed regression thresholds and manual-only live re-recording (kernel unchanged) - 0132 folded-state read models (Accepted): durable per-execution
projection$folds in@loom/plugin-read-model— one state row whose sequence is the watermark, a monotonic compare-and-set so nodes converge without locks, a sensitive-state guard, a bounded live-path cache, andReadModelStoreSPI 1.2.0 (kernel unchanged) - 0133 model reasoning as audit evidence (Accepted): provider reasoning captured at the
@loom/llmseam into an erasable sidecar content store, referenced by a userspaceLLM_REASONING_CAPTUREDevent the fold ignores; the step value is unchanged (kernel unchanged) - 0134 snapshot pruning (Accepted): optional
SnapshotStore.pruneSnapshots(SPI 1.2.0) andLogLifecycle.pruneSnapshots, off by default (amended 2026-11-02: optionalSnapshotStore.listSequences, SPI 1.3.0, andpruneSnapshots({ dryRun })that counts what a prune would delete); keeps the latest, every compaction checkpoint and the fork points it serves; deltas and Merkle state deferred (kernel unchanged) - 0135
waitForSuspend()rejects on a terminal run (Accepted):ExecutionTerminatedError(state + cause) instead of a promise that never settles; settles afterwaitForCompletion(), so races are unchanged (kernel unchanged) - 0136 Postgres snapshot lz4 (Accepted): opt-in
snapshotCompression: "lz4"forsnapshots.state, fail-soft with anonWarnfallback; measured ~1.5x MORE bytes but ~20% faster reads than pglz, so it is not the default; events column measured only (kernel unchanged) - 0137 agent state growth (Accepted):
agentLoop$warns once per run when an uncompacted transcript crosseswarnTranscriptBytes(256 KiB default,falsedisables), andcreateSummarizerEffect(provider)is a ready-madecompaction.toolIdshared with the conversation layer’ssummarizerToolId; compaction stays opt-in (kernel unchanged) - 0138 compaction archives what it removes (Accepted):
LogLifecycle.compactMOVES the removed events into a dedicatedArchiveStoreSPI (1.0.0, 1.1.0 with ADR 0139; in-memory, SQLite, Postgres; sealed under the log’s KeyStore for an encrypting store) and REFUSES without one;readArchivedis the explicit audit read andreadRangenever touches it; the fold stops copyinginputinto settled frames (EVENT_SCHEMA minor) - 0139 reserved streams archive what a trim or a delete removes (Accepted):
trimInbox, the topic-stream trim, the quarantine trim and the terminaldeleteInboxMOVE the envelopes they remove into the sameArchiveStore(keyed by the reserved stream id) and REFUSE without one; a re-created stream starts above the archive, a straggler goes to a late run, the terminal delete is bounded by the new optionalEventStore.deleteThrough(EVENT_STORE_SPI 2.6.0), the incarnation is the chain owner’s bootstrap, the body is sealed under the owner’s subject and the sender under the SENDER’s, erasure deletes plaintext archived copies, and the sweep,readReservedWithArchiveandexportSubjectsee it (no kernel ABI or schema change) - 0140 agent claim-check opt-in (Accepted):
defineConfig({ execution: { claimCheck: { store, toolIds } } })claim-checks the named tools’ outputs over a 1 KiB cap (default{ preview }digest of the first 256 characters, a declared digest wins, taint is never previewed); explicit tool list (no “all”), outside redaction and user middleware, off unless set; replay serves the envelope with zero content reads; measured 4.2x less folded state at 200 turns (kernel unchanged) - 0141 opt-in zstd snapshot codec (Accepted): a format byte + zstd-3 JSON frame in a Postgres
state_z BYTEA(EXTERNAL;statestays NOT NULL and holds a fixed sentinel so an older node fails loudly; fail-soft, short-lock-wait provisioning) / the SQLitestatecolumn as a BLOB / the in-memory store, reads dispatch on the stored format and nothing is rewritten, compressed BEFORE sealing underEncryptingSnapshotStore(requireslengthLeak: "accept"), refuses loudly on Node < 22.15,loom snapshot show;durability.snapshots.codec; measured 0.79x (plain) / 0.49x (claim-checked) of JSONB pglz bytes and a faster resume read (SNAPSHOT_STORE_SPI unchanged) - 0142 agent lean tool-call log (Accepted):
agentLoop$({ toolCallLog: "lean" })replaces eachtoolCallLogentry’stoolOutputwith{ status, bytes, digest, stepId }(digest = sha256 of the canonical JSON;stepId= the tool frame id), also in theagentInterruptedpayload; default"full"unchanged and byte-identical; an explicit mode is journaled and a resume with the other value throws; measured 43% less folded state on a completed 200-turn run (kernel unchanged) - 0143
EncryptingContentStore(Accepted): ContentStore SPI 1.5.0 adds an optionalContentAccess { executionId, workflowId }argument onput/putStream/get/getManyand asealsContentmarker; the store seals each blob (AES-256-GCM, key and nonce derived from the subject key, nonce = keyed hash of the subject and the plaintext) so the address (the inner store’s hash of the ciphertext) is subject-scoped (no cross-subject equality, dedupe only within a subject) and a crypto-shred makes every blob unreadable; the claim-check middleware,resolveClaimCheck*andexportSubjectthread the owner;defineConfigacceptsexecution.claimCheckwithsecurity.cryptofor such a store built over the SAME KeyStore/subjectOf;eraseSubjectjournals the sealed erasure and deletes the ciphertext; fail closed without the owner, noputStream,CachingContentStore/ContentResolvingEventStorerefuse a sealing store (kernel unchanged) - 0144 sandbox backend SPI (Accepted):
@loom/sandboxdefinesSandboxBackend(the engine only, a closedSandboxOutcome) andSandboxEffect, which keeps every limit, channel, bound and the JSON replay form; backends declare capabilities and the effect refuses egress or a reused environment unless opted in; QuickJS is the reference backend,runSandboxBackendConformancethe kit,loom plugin new sandboxthe door. No kernel change;@loom/isolatestays separate. - 0145 app-owned retention policy (Accepted):
retentionPolicy()(@loom/workflow-runtime) is a validated, frozen value the app keeps in its own module (terminalTtlMs, optionalsnapshots.keepLatestandcontent.unpinnedAfterMs, unknown keys rejected, content TTL>=log TTL as an advisory consistency check within one policy).LogLifecycle.applyRetention/evaluate/pruneSnapshotsandsweepUnpinnedContentconsume it (the sweep re-validates structurally: unknown keys, integer ranges,unpinnedAfterMs >= terminalTtlMswhen present;LogLifecycleruns the full validator), andloom retention run/loom sweep contentgain--policy <module>(--ttloverrides). Not indefineConfig, no timer; with no policy every pass behaves as before. Amended 2026-11-03:awaitedByParent(@loom/orchestration), wired by default intodefineConfigandloom retention run, keeps an orchestration child while its parent is alive (store-only, O(1) reads, fails safe to pinned). No kernel change. - 0146 content ages from the citing execution’s end (Accepted): a TTL-reclaimable ref’s age clock in
sweepUnpinnedContentruns frommax(citation, the citing execution's end)for a terminal, not re-opened execution (running executions and reserved streams keep the citation clock), so withunpinnedAfterMs >= terminalTtlMsa reclaimable blob is never reclaimable before retention could delete the log citing it (exceptions in the ADR: retention gates, running or re-opened executions, the retention archive’s copy, legacy compacted logs), and the sweep never reclaims what the old clock keeps;ContentSweepReport.refsAgedByEndand, on a dry run,heldByEndClock; a re-opened execution keeps the citation clock and is surfaced asrefsOfReopened(dry run:reclaimedWhileReopened), and erases the store-clock guard refuses are counted inkeptByTouch(withtouchCutoff). No kernel, ABI, EVENT_SCHEMA or SPI change. - 0147
onDurableEvent(Accepted): an optional callback that fires after an event’s durable write, so it never reports an event the log does not hold; at most once, in order per writer.onEventkeeps its timing, because supervisors, the compliance index and surfaces depend on it.
- Presentation (
loom/docs/presentation/loom-deck.md) — the deck.
Source: docs/architecture.md in the loom repository.