pleach
Architecture

Language-agnostic contract

Which shapes are the contract — the wire shapes, checkpoint envelope, audit row, sync vectors, event log — versus implementation details. What an independent client must implement.

@pleach/core is a TypeScript reference implementation of a contract that doesn't depend on TypeScript. The runtime substrate's load-bearing primitives are wire shapes: SSE event frames, checkpoint envelopes, audit ledger rows, sync version vectors, HTTP routes. An independent client in another language that implements those shapes correctly is a conforming runtime.

A Go implementation has been built against the same contract and round-trips a shared corpus of recorded turns — that's the test that catches anything TypeScript-flavored leaking into the wire. Both implementations write AuditableCall rows into the same harness_auditable_calls table shape and consume the same Checkpoint envelope JSON, and a session started under one and resumed under the other hydrates to a byte-identical SessionState. The Go implementation isn't published as a SKU yet — an official @pleach Go runtime is the next planned published implementation. This page documents which shapes are contract — and which are implementation details a non-TypeScript consumer can ignore.

What's in the contract

Five shapes are load-bearing:

ShapeWhere it livesWhy it's contract
HTTP + SSE wire/api/harness/* routesA native client speaks these to interop with a server-hosted runtime
StreamEvent discriminated unionOne SSE data: frame eachA non-TS client deserializes these into its own types
AuditableCall rowharness_auditable_calls tableAudit consumers (eval, SOC2 evidence, billing) query this shape
Checkpoint envelopeharness_checkpoints rows + the in-memory shapeA native runtime that round-trips through this shape can interop with the TS runtime's checkpoints
Version vectorsession_state.versionVector JSONSync conflict detection works across clients with matching vector math

Each shape is documented in its own page on this site; this page walks the contract status of each.

HTTP + SSE wire

The 8 routes documented in API routes are the canonical wire protocol. Path prefix is convention; relative shapes are contract:

Route shapeContract status
GET /sessions query params, response shapeContract
POST /sessions body, responseContract
GET/PUT/DELETE /sessions/[id]Contract
POST /sessions/[id]/sync body + responseContract
POST /sessions/[id]/execute body + SSE streamContract
GET /health response shapeContract
The literal path prefix /api/harness/Convention
Authorization header parsingImplementation detail

An independent client mounts the same shapes under a different path and conforms.

StreamEvent discriminated union

The full event union documented in Stream events is contract. Specifically:

  • The type field's literal values (message.delta, tool.completed, sync.conflict, etc.).
  • The payload shape for each type value.
  • The namespace?: string[] field that tags subagent-emitted events.

Implementation details (not contract):

  • TypeScript-specific representations (which as const literals, which & intersections). A Go client implements the same union with its own type system.
  • Internal field ordering inside the JSON. Canonicalized ordering matters for the fingerprint, not for the wire.

Adding a new type is non-breaking — clients with a default-arm switch keep working. Removing a type is a breaking change. A concrete example: adding subagent.spawned in a minor cycle is fine because a Go consumer that switches on the existing union falls through to its default arm and the SSE stream keeps decoding; removing tool.completed is a major because every consumer that relied on the lifecycle pair (tool.started / tool.completed) suddenly has half a lifecycle.

AuditableCall row

The row shape documented in AuditableCall row is the contract for audit consumers. Both the SQL column shape and the in-memory JS shape:

Column / fieldContract
record_id (ULID, Crockford Base-32, 26 chars)Yes
audit_record_versionYes; bumps coordinate per Versioning
session_id, turn_id, stage_id, seq_within_turnYes; idempotency key
created_at, actor_kind, session_auth_methodYes
call_class, provider, model, transportYes
status, latency_ms, finish_reason, http_statusYes
payload JSONB shape (typed sub-objects)Yes — keyed on payload.kind
Underlying storage choice (Supabase, IndexedDB, S3)Implementation detail

A Go consumer that reads harness_auditable_calls via direct SQL gets the same data the TS ProviderDecisionLedger adapter returns. Both are conforming consumers.

The append-only invariant is contract

ProviderDecisionLedger.recordCall has no update or delete primitive. Adapters in any language MUST be append-only — an adapter that exposes mutation is non-conforming and a wire-format break.

Bumps to audit_record_version are the only sanctioned way to change the row shape; that's why AUDIT_RECORD_VERSION_HISTORY is part of the public API.

Checkpoint envelope

A checkpoint is a typed envelope carrying a session state plus per-channel snapshots. The envelope shape is contract; the storage adapter (MemorySaver, SupabaseSaver, custom) is not.

interface Checkpoint {
  id:           string;        // ULID
  sessionId:    string;
  stageId:      "anchor-plan" | "tool-loop" | "synthesize" | "post-turn";
  createdAt:    string;        // ISO 8601
  schemaVersion: number;
  channels:     Record<string, ChannelSnapshot>;
  parentCheckpointId?: string;
}

interface ChannelSnapshot {
  kind:    "last_value" | "binary_op_aggregate" | "topic" | "ephemeral_value" | "named_barrier" | "data_channel";
  version: number;
  value:   unknown;             // shape depends on `kind`
}

A non-TS runtime that consumes a checkpoint table populated by the TS runtime, restores its own state to match the snapshot shape, and continues a turn is a conforming consumer.

Specifically not contract:

  • Which channel kinds you implement. A subset is fine if your runtime doesn't expose the un-implemented kinds.
  • The wire representation of channel values when the kind's shape is opaque (e.g. DataChannel's internal LRU layout). Round-trip through value is what matters.

Version vector

The version vector format is Record<string, number> — a JSON object keyed by client id with monotonic-incrementing integer values. The comparison semantics documented in Sync are contract:

OperationBehavior
mergeVectors(a, b)Element-wise max
compareVectors(a, b)One of equal / ancestor / descendant / concurrent
hasSeen(v, change)v[change.clientId] >= change.version
IncrementA client increments its own entry on every write

Two clients with conforming vector math detect concurrent writes correctly regardless of language. A client that uses different semantics (last-write-wins, lamport timestamps, etc.) is not conforming.

What's NOT in the contract

SurfaceWhy not contract
SessionRuntimeConfig field setTS-specific construction; equivalent fields exist in Go but the constructor shape isn't shared
defineTool / HarnessPlugin interfacesTS-side authoring contracts; Go has its own equivalents
Channel internals (LRU eviction policy, reducer implementations)Per-runtime; only the snapshot round-trip is contract
Specific transport types in providers (AI SDK / Anthropic SDK wrappers)TS-side conveniences
React hooksBrowser-specific; not part of the runtime contract
setHarnessModuleLoaderTS-side migration scaffold; Go runtimes don't have this concept

These surfaces exist because they're how TypeScript consumers work. A Go consumer never sees them — it interacts with the runtime through the wire shapes.

How the contract stays honest

Four load-bearing mechanisms keep the TS reference and the Go implementation from drifting:

  1. Shared test fixtures. A corpus of recorded turns (AuditableCall rows, checkpoint envelopes, event sequences) that any conforming runtime round-trips identically. The fixtures are framework-agnostic JSON; the TS runtime authors them.
  2. Cross-runtime replay. A turn recorded against the TS runtime replays byte-identical against the Go implementation when both are pointed at the same provider, same model id, same fingerprint inputs.
  3. Schema gate. The schema bundle is the canonical source for the wire-table shapes. Any runtime that drifts from the bundle is wrong by definition.
  4. Domain-string purity gate. audit:domain-string-purity scans packages/core/src/** for ~50 forbidden literal patterns across five families (host vocabulary, vendor backend names, sandbox tool prefixes, identity discriminators, domain phrasing). A leak fails CI. The TS reference can't accidentally embed a consumer's domain vocabulary that the Go runtime would then have to mirror — the substrate stays consumer-agnostic by structure, not by review discipline. Plugins remain the only legal channel for consumer-specific content.

The Go implementation makes the "wire is language-agnostic" claim falsifiable rather than aspirational. The acceptance test: a recorded turn against the TS runtime produces an AuditableCall row sequence ordered by (turn_id, seq_within_turn); replaying that turn against the Go implementation with the same fingerprint inputs produces the same row sequence under the same key, or the schema-gate CI flags the runtime that diverged. The Go implementation isn't published as a SKU yet — an official @pleach Go runtime is the next planned published implementation.

Implementing a conforming client

Minimum viable conformance for an independent client:

  1. Speak the HTTP + SSE wire for the routes you implement. You can implement a subset — e.g. just execute and health — and conform partially.
  2. Round-trip the StreamEvent union for the variants you produce or consume. Unknown variants in your input should be ignored, not error.
  3. Append-only writes to harness_auditable_calls with the row shape above.
  4. Version-vector semantics matching the operations above if you implement sync.
  5. Checkpoint envelope round-trip for the channel kinds you support, if you implement checkpointing.

A client that does only (1) and (2) is a streaming-only implementation — like an SSE-consuming dashboard. A client that does all five is a full runtime peer.

Where to go next

On this page