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:
| Shape | Where it lives | Why it's contract |
|---|---|---|
| HTTP + SSE wire | /api/harness/* routes | A native client speaks these to interop with a server-hosted runtime |
StreamEvent discriminated union | One SSE data: frame each | A non-TS client deserializes these into its own types |
AuditableCall row | harness_auditable_calls table | Audit consumers (eval, SOC2 evidence, billing) query this shape |
| Checkpoint envelope | harness_checkpoints rows + the in-memory shape | A native runtime that round-trips through this shape can interop with the TS runtime's checkpoints |
| Version vector | session_state.versionVector JSON | Sync 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 shape | Contract status |
|---|---|
GET /sessions query params, response shape | Contract |
POST /sessions body, response | Contract |
GET/PUT/DELETE /sessions/[id] | Contract |
POST /sessions/[id]/sync body + response | Contract |
POST /sessions/[id]/execute body + SSE stream | Contract |
GET /health response shape | Contract |
The literal path prefix /api/harness/ | Convention |
| Authorization header parsing | Implementation 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
typefield's literal values (message.delta,tool.completed,sync.conflict, etc.). - The payload shape for each
typevalue. - The
namespace?: string[]field that tags subagent-emitted events.
Implementation details (not contract):
- TypeScript-specific representations (which
as constliterals, 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 / field | Contract |
|---|---|
record_id (ULID, Crockford Base-32, 26 chars) | Yes |
audit_record_version | Yes; bumps coordinate per Versioning |
session_id, turn_id, stage_id, seq_within_turn | Yes; idempotency key |
created_at, actor_kind, session_auth_method | Yes |
call_class, provider, model, transport | Yes |
status, latency_ms, finish_reason, http_status | Yes |
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 throughvalueis 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:
| Operation | Behavior |
|---|---|
mergeVectors(a, b) | Element-wise max |
compareVectors(a, b) | One of equal / ancestor / descendant / concurrent |
hasSeen(v, change) | v[change.clientId] >= change.version |
| Increment | A 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
| Surface | Why not contract |
|---|---|
SessionRuntimeConfig field set | TS-specific construction; equivalent fields exist in Go but the constructor shape isn't shared |
defineTool / HarnessPlugin interfaces | TS-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 hooks | Browser-specific; not part of the runtime contract |
setHarnessModuleLoader | TS-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:
- Shared test fixtures. A corpus of recorded turns
(
AuditableCallrows, checkpoint envelopes, event sequences) that any conforming runtime round-trips identically. The fixtures are framework-agnostic JSON; the TS runtime authors them. - 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.
- 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.
- Domain-string purity gate.
audit:domain-string-purityscanspackages/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:
- Speak the HTTP + SSE wire for the routes you implement.
You can implement a subset — e.g. just
executeandhealth— and conform partially. - Round-trip the
StreamEventunion for the variants you produce or consume. Unknown variants in your input should be ignored, not error. - Append-only writes to
harness_auditable_callswith the row shape above. - Version-vector semantics matching the operations above if you implement sync.
- 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
Concept clusters
Seven cluster triplets organize the runtime — session/turn/row on top, then execution-graph, routing, audit-ledger, lifecycle, and persistence one click in.
Graph
The declarative topology that drives a turn — Annotation channel schema, StateGraph builder, Send fan-out, compile() runner, and the four-stage lattice gate that keeps every node attributable.