# Architecture (/docs/architecture)



Every LLM call in `@pleach/core` routes through one of four seams,
resolves against a family-locked model matrix, and lands as one
append-only `AuditableCall` row. Six pieces compose that path:
`SessionRuntime`, `CompiledGraph`, `Channels`, `Seams`, the
`AuditableCall` ledger, and the event log — the lattice constrains
where calls live, the ledger carries what they did.

These six are the structural members of the hedge — the rigid
frame the agent's branches grow against. The four-stage lattice
says where a call can go; the channels say what a call can read
and write; the seams say which model a call routes to; the
ledger says where its outcome lands. No piece is optional; no
piece is replaced silently. CI gates fail the build when any of
them drifts.

This page is the substrate-wide map. Each section names a piece,
the role it plays, and the page that owns the deep-dive. The
[boundary rules](#10-boundary-rules) at the bottom are unique to
this page — they're the architectural invariants the rest of the
design depends on.

This page mirrors the [`@pleach/core` architecture deep-dive](https://github.com/pleachhq/core/blob/main/docs/ARCHITECTURE.md)
in the package repo. If the two disagree, the repo wins — file
an issue against [pleachhq/core](https://github.com/pleachhq/core/issues).

## TL;DR — six pieces, one substrate [#tldr--six-pieces-one-substrate]

<Mermaid
  chart="flowchart LR
    SR[SessionRuntime] -->|owns| G[CompiledGraph]
    G -->|reads/writes| Ch[Channels]
    G -->|invokes| S[Seams]
    S -->|memoizes via| C[Cache]
    C -->|resolves via| M[modelfamily matrix]
    S -->|records| A[AuditableCall ledger]
    G -->|emits| E[EventLog]
    Ch -.->|snapshot| Cp[Checkpointer]"
/>

1. **`SessionRuntime`** owns per-session state, the compiled graph,
   and the lifecycle.
2. **`CompiledGraph`** is the declarative topology — nodes wired to
   channels, edges constrained to a four-stage lattice.
3. **Channels** are typed reactive state slots. A node fires when one
   of its subscribed channels advances; concurrent writes have well-
   defined reducer semantics.
4. **Seams** are per-call-class provider entry points. Every LLM call
   goes through exactly one seam; the `callClass` literal is lint-
   restricted to the seam factories.
5. **Modelfamily matrix** resolves `(family × callClass)` → model id +
   transport. Family is locked at session start; the matrix never
   silently widens.
6. **Audit ledger** + **event log** carry the two write streams: every
   addressable decision lands in the ledger; every observable event
   lands in the log.

## The execution-graph cluster [#the-execution-graph-cluster]

Three of the six pieces — `CompiledGraph`, `Channels`, and the
nodes the graph compiles — form a tighter cluster: the per-turn
execution substrate that lives *inside* the lattice. The full
triplet framing lives at
[Concept clusters → Execution-graph](/docs/concept-clusters#execution-graph-cluster);
the deep-dive pages are [Graph](/docs/graph), [Nodes](/docs/nodes),
and [Channels](/docs/channels).

The piece that *runs* that cluster is the **engine** — the
`engine/` scheduler (`SuperstepRunner` and its primitives). Where
`CompiledGraph` is the declarative topology, the engine is the
deterministic superstep executor that drives it: each superstep
fires the set of triggered nodes in parallel
(`shouldTriggerNode` resolves channel-version advances into node
activations), accumulates their writes, and commits the resulting
channel updates atomically at the step boundary. That Pregel-style
discipline — run, accumulate, commit — is what makes graph
scheduling deterministic and race-free between concurrent writers,
and it underwrites the byte-replay property in §9. The engine also
owns the cross-cutting execution plumbing the graph depends on:
retry-with-backoff (`RetryPolicy`), abort-signal composition
(`AbortComposer`), and graph↔session state mirroring
(`GraphStateSynchronizer`). It ships in the main `@pleach/core`
entry today, not a separate subpath.

## 1. Stage lattice [#1-stage-lattice]

<Mermaid
  chart="flowchart LR
    AP[anchor-plan] --> TL[tool-loop]
    TL -->|decide / dispatch| TL
    TL --> SY[synthesize]
    TL -->|recovery dispatch| PT[post-turn]
    SY -->|messageId-guarded retry| SY
    SY --> PT
    PT -.->|next turn| AP"
/>

Every node in the compiled graph belongs to exactly one of four
stages — `anchor-plan` → `tool-loop` → `synthesize` → `post-turn`.
`ALLOWED_EDGE_PATTERNS` enumerates nine legal
`(from-stage, to-stage)` pairs: five cross-stage transitions (the
three forward edges, the `tool-loop → post-turn` recovery-dispatch
edge, and the `post-turn → anchor-plan`
next-turn rollover) and four intra-stage chains — `anchor-plan`,
`tool-loop`, `post-turn`, plus the `messageId`-guarded
`synthesize → synthesize` retry. Every other pair is forbidden.
The lattice is structural, not advisory — `audit:graph-stages`
fails CI on any out-of-lattice edge.

Why structural matters: once stages are structural, *cost
allocation*, *observability*, and *time-travel* become structural
too. A `GROUP BY stage_id` on the audit ledger returns per-stage
spend for any turn against a row shape that exists by construction
— every row carries a non-null `stage_id` because a node that
doesn't declare a stage fails CI before it ever fires.

See [Graph](/docs/graph) for the substrate API, [Nodes](/docs/nodes)
for the per-node metadata, and [Turn lifecycle](/docs/turn-lifecycle)
for the per-stage stream-event and ledger-payload tables.

## 2. Call classes [#2-call-classes]

Every LLM call declares one of four classes — `utility`,
`reasoning`, `converse`, `synthesize` — which the seam factory
carries through to the matrix. The class drives three things: the
seam that carries the invocation, the per-turn allotment, and the
audit-row slice the call lands in. `synthesize` is structurally
capped at exactly one per turn (singleton seam + idempotent
counter) so the rendered string and the audited string are the
same string.

See [Call classes](/docs/call-classes) for the four-class taxonomy,
the per-class roles, and the `lint:callclass-literals` gate that
restricts the literal.

## 3. Seams [#3-seams]

A seam is the per-call-class provider entry point. Four factories
live in `graph/seams/`, one per call class. The seam carries the
locked `callClass`, threads it as a type parameter into matrix
resolution, and dispatches a sync per-chunk observer ladder on the
inbound stream — observers return `continue`, `amend`, `emit`, or
`halt`. `onChunk` is sync-only because async would race on replay.

See [Seams](/docs/seams) for the four factories, the singleton
synthesize seam, the observer verdict ladder, and how a node
consumes a seam.

## 4. Family-locked routing [#4-family-locked-routing]

<Mermaid
  chart="flowchart TD
    Start[pickNextInFamily] --> R1[anthropic rung 1]
    R1 -.->|fail| R2[anthropic rung 2]
    R2 -.->|fail| R3[anthropic rung 3]
    R3 -.->|fail| FE[family-exhausted]
    FE --> Stop[runtime stops, asks user]
    Other[other family column]
    FE -.-x Other"
/>

`(ProviderFamily × CallClass)` keys the model resolution matrix.
Family and transport lock at session start. When a provider call
fails, the cascade walks the next rung in the same family
(`pickNextInFamily`); when the column is exhausted, the runtime
emits `family-exhausted` and surfaces the state to the host. No
silent cross-family widening. The only carve-out is BYOK and
other non-matrix-resolvable models, which never participated in a
lock to begin with.

See [Family-locked routing](/docs/family-lock) for the lock
contract, the four properties it freezes (tokenizer, prompt-cache
key, tool-call dialect, refusal pattern), and the family-strict
cascade. The per-family per-callclass lookup lives at
[Model resolution matrix](/docs/model-resolution-matrix).

## 5. Audit ledger [#5-audit-ledger]

<Mermaid
  chart="flowchart LR
    US[utilitySeam] --> L[(AuditableCall ledger)]
    RS[reasoningSeam] --> L
    CS[converseSeam] --> L
    SS[synthesizeSeam] --> L
    L -.-> ID[identity tuple: sessionId, turnId, stageId, seqWithinTurn]"
/>

Every seam writes one row per call. The four-field identity tuple
is non-null by construction. The row is append-only by interface
contract — no `update`, no `delete`; a row that needs to mutate is
a wire-format break that bumps `auditRecordVersion` instead. The
ledger is also what makes per-axis attribution work *inside* one
Anthropic Workspace or OpenAI Project: every row carries the
opaque `tenantId` the host wires to its billing axis, and the
rollup is `GROUP BY tenant_id`.

Three compliance plug-points (`TamperEvidence`, `PIIRedactor`,
`GDPRSoftDelete`) ride on top of the ledger. Each ships as a
no-op default in `@pleach/core`; production implementations land
in `@pleach/compliance@0.1.0` (Phase A: scrubber cohort,
C8 redaction substrate, attestation Phase A). Hash-chain
verification lives in `@pleach/core/eventLog`
(`verifyChainForChat`, `generateProof`); `@pleach/replay@0.1.0`
composes them.

`@pleach/core/attestation` is the cryptographic signing layer on
top of the canonical row hash — ed25519 over RFC 8785-canonicalized
payload bytes, with a pluggable `AttestationKeyStore` (AWS KMS and
Vault Transit stubs at v1 contract; a file-backed test adapter
under the `/testing` subpath). A signed row makes any post-hoc
mutation detectable per-row; the hash chain makes deletion or
reordering detectable across rows. See [Attestation](/docs/attestation).

See [Audit ledger](/docs/audit-ledger) for the write interface and
the plug-points, [AuditableCall row](/docs/auditable-call-row) for
the row shape, and [Hash chain](/docs/hash-chain) for the
tamper-evidence layer.

## 6. Event log [#6-event-log]

Distinct from the ledger. The event log is the broader stream of
*observable events* — messages, tool dispatches, interrupts,
subagent spawns, exports, plugin domain events. Three layers:
`EventLogWriter` (fire-and-forget enqueue), `durableFlush`
(`waitUntil`-routed flush with status-code-aware retries,
idempotent on client-generated `id`), and `hydrateFromEvents` (a
walk that rebuilds session state from a slice of events).

Every write passes through the `Scrubber` allowlist before
persistence. The `audit:c8-event-type-allowlist-coverage` CI gate
enforces full scrubber coverage — an event type that lands without
a scrubber fails the build (see [Scrubbers](/docs/scrubbers)).

See [Event log](/docs/event-log) for the full per-layer contract.

## 7. Sessions, storage, sync [#7-sessions-storage-sync]

`SessionRuntime` is constructed with three swappable backends —
storage adapter, checkpointer, and (optional) sync. Memory /
IndexedDB / Supabase implementations of each share one interface;
swapping is a one-line change.

The runtime also ships `memoryCacheBackend` as the default
`cacheBackend` — runtime-side memoization of prepared LLM inputs,
default-constructed in the `SessionRuntime` constructor since PA-2
C2 Phase 3 and threaded through the four seam factories. On by
day zero, no opt-in required. This is distinct from provider-side
prompt caching (see [Cache](/docs/cache) and
[Prompt caching](/docs/prompt-caching)).

`SessionRuntime` exposes its capabilities through named accessor
groups (`runtime.sessions`, `runtime.events`, `runtime.spans`,
`runtime.tenant`, `runtime.graph.{recovery, heuristics, config}`),
which is what makes the runtime LLM-agent friendly — an agent
reading `runtime.<TAB>` sees a grouped surface rather than a flat
40-method dump. `runtime.spans` in particular is the in-process
OTel facet, exposing `inFlightCount`, `isShutdown`, and `snapshot`
introspection over the four read-side span types — useful for
graceful-shutdown gates and live dashboards that need to know
whether the in-process exporter has drained (see
[OTel observability](/docs/otel-observability)). The cross-session
dependency graph — typed edges between sessions, artifact
provenance — is read-side observability sitting alongside the
span surface; see [Lineage](/docs/lineage). See [Facets](/docs/facets)
for the full inventory and the audit gates that enforce coverage.

Sync uses version vectors per session — `compareVectors` detects
concurrent writes at push time and the coordinator surfaces them
through `sync.conflict` rather than overwriting. See
[Storage](/docs/storage), [Checkpointing](/docs/checkpointing),
and [Sync](/docs/sync) for the per-axis deep-dives.

## 8. Plugins [#8-plugins]

`HarnessPlugin` is the consumer extension contract. A plugin can
register tier nodes into the lattice's enrichment slots, stream
observers, prompt contributors, safety contributions, and domain
event handlers — and crucially, cannot add an out-of-lattice edge,
bypass the singleton synthesize seam, reach across the `seams/`
boundary, or register an async observer dispatch.

Sibling SKUs (`@pleach/compliance`, `@pleach/eval`,
`@pleach/gateway`, `@pleach/replay`, `@pleach/mcp`,
`@pleach/coding-agent`, `@pleach/sandbox`, `@pleach/langchain`,
`@pleach/base-tools`, `@pleach/observe`, `@pleach/recipes`) land
as plugins on top of this contract. Every shipping sibling
publishes at `0.1.0 · FSL-1.1-Apache-2.0` in the first-wave cut;
`@pleach/trust-pack` alone remains a reserved npm name. See
[Packages](/docs/packages) for the canonical per-SKU status table.
The plugin contract is stable so a host implementing the same
plug-points manually swaps into the sibling SKUs without
graph-shape changes.

See [Plugin contract](/docs/plugin-contract) for the full
`definePleachPlugin` surface, every slot, and the breadcrumbs that
fire when a capability isn't contributed.

## 9. Determinism [#9-determinism]

Several pieces of the substrate are deliberately deterministic —
the fingerprint module, channel reducers, stream observers,
prompt composition, and the `engine/` superstep scheduler
(`SuperstepRunner`). Together they buy the
byte-replay property: a recorded turn replayed against the same
package version + the same input produces a byte-identical
fingerprint stream. This is the property `@pleach/eval@0.1.0`
and `@pleach/replay@0.1.0` are built around (both shipping
real bodies for the entry-point surfaces; remaining slices throw
typed `NotImplemented` sentinels per [Packages](/docs/packages)).

See [Determinism](/docs/determinism) for the five contracts and
[Fingerprint](/docs/fingerprint) for the pure-function fingerprint
that platform-uniformly hashes prepared inputs.

## 10. Boundary rules [#10-boundary-rules]

Lint gates enforce the architectural boundaries:

| Gate                                     | Forbids                                                                                                                                                                                                                           |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lint:harness-boundary`                  | Imports from `seams/**` into `modelfamily/**`; imports from app-layer into core                                                                                                                                                   |
| `lint:callclass-literals`                | `callClass: "..."` literal outside the four seam factories + matrix module                                                                                                                                                        |
| `lint:no-model-reassignment-in-fallback` | Reintroduction of `tier1Model` / `tier2Model` / `SYNTHESIS_CANDIDATES`                                                                                                                                                            |
| `lint:stream-observer-registration`      | Direct seam-binding of detectors (must go through plugin)                                                                                                                                                                         |
| `audit:graph-stages`                     | Out-of-lattice edges                                                                                                                                                                                                              |
| `audit:auditable-call`                   | Audit record version drift                                                                                                                                                                                                        |
| `audit:domain-string-purity`             | Domain-specific literals in `packages/core/src/**` — host vocabulary, vendor backend names, sandbox tool prefixes, identity discriminators, domain phrasing (five pattern families; \~50 patterns; baseline-gated `:strict` mode) |

These aren't bureaucracy; they're the structural guarantees the rest
of the design depends on. If `callClass` could appear anywhere, the
seam model leaks — a node could call `synthesize` outside the
singleton seam and the rendered string would diverge from the
audited string. If family could silently widen, the modelfamily lock
is advisory — a tool that worked on `anthropic`'s tool-call dialect
would start failing mid-session when the cascade reached an
`openai`-shaped call. If observers could be async, replay determinism
is gone — `@pleach/replay` strict mode would throw
`ReplayDivergenceError` on the first race between two observer
promises resolving in a different order on replay than on record.
And if a host name, a vendor backend, or a sandbox tool prefix
could land in `packages/core/src/**`, the language-agnostic claim
leaks — the Go runtime would have to mirror string literals that
only mean something to one TS consumer.
`audit:domain-string-purity` makes plugins the only legitimate
channel for consumer-specific content.

The full inventory of CI- and PR-time gates — package shape,
plugin contracts, tool coverage, event-log integrity, runtime
soak ledgers, cross-repo reservations — lives at [Audit gates](/docs/audit-gates).

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="SessionRuntime" href="/docs/session-runtime" description="Construct the runtime, create sessions, execute messages, abort turns." />

  <Card title="Audit ledger" href="/docs/audit-ledger" description="The harness_auditable_calls schema and the three compliance plug-points." />

  <Card title="The AuditableCall row" href="/docs/auditable-call-row" description="Per-call row shape — turnId, toolName, subagentDepth, tokenUsage." />

  <Card title="Plugin contract" href="/docs/plugin-contract" description="The HarnessPlugin extension surface — what plugins can do and what they can't." />
</Cards>
