Skip to content
Spec
HAPPI
Version
1.5
Diagrams
04
Status
Current

How HAPPI Works

Four diagrams covering the request flow, the event lifecycle, the signed-record family, and the transport layer — everything needed to implement a conforming runtime.

01 Request flow

Client → Runtime → Provider → Stream

A HAPPI call starts with a single JSON envelope written to the runtime's stdin (or POSTed to /dispatch). The runtime validates the envelope, routes it to the correct provider adapter, and opens a streaming connection to that provider. Each provider chunk is normalised into a HAPPI event and flushed immediately to the client as an NDJSON line. The client never sees provider-specific framing.

flowchart LR Client(["Client\n(any language)"]) Envelope["HAPPI Envelope\n{v, id, cmd, args}"] Runtime(["HAPPI\nRuntime"]) Router["Provider\nRouter"] Anthropic["Anthropic\nAdapter"] OpenAI["OpenAI\nAdapter"] Gemini["Gemini\nAdapter"] Stream["NDJSON\nEvent Stream"] Client -->|"stdin / HTTP POST"| Envelope Envelope --> Runtime Runtime --> Router Router -->|cmd starts with\nanthropicXXX| Anthropic Router -->|cmd starts with\nopenaiXXX| OpenAI Router -->|cmd starts with\ngeminiXXX| Gemini Anthropic -->|normalise| Stream OpenAI -->|normalise| Stream Gemini -->|normalise| Stream Stream -->|"stdout / HTTP stream"| Client

Drag the diagram to pan →

Entry point
stdin
Canonical transport. One line of JSON in, NDJSON stream out.
Routing key
cmd
The cmd field selects the provider adapter. No magic inference.
Normalisation
per adapter
Each adapter converts provider-specific SSE frames into HAPPI events.
Latency cost
< 1 ms
Runtime overhead is a JSON parse + a dispatch table lookup.

02 Event lifecycle

started → delta × N → completed

Every HAPPI response follows a deterministic state machine. A started event is always emitted first — it anchors the id and sets ts=0. Zero or more delta events carry incremental text. The stream closes with exactly one terminal event: completed (success) or error (failure). tool_call and tool_result pairs may interleave with deltas when the provider uses tool use, and a sub_request marks a child envelope dispatched mid-stream — the parent stream resumes once the child's own stream closes.

Seven streaming events, three signed records. The seven above are the whole of the streaming vocabulary, and Axiom II holds that they are sufficient. The other three — idr, context and continuity — sit alongside this machine rather than inside it: they record what a run decided, believed and assumed. idr and context follow the terminator; continuity deliberately precedes the work it governs. Diagram 03 covers all three.

stateDiagram-v2 [*] --> started : runtime accepts envelope started --> delta : first token arrives delta --> delta : more tokens delta --> tool_call : model invokes a tool tool_call --> tool_result : client executes tool tool_result --> delta : model resumes delta --> sub_request : child envelope dispatched sub_request --> delta : child stream closes delta --> completed : provider signals stop delta --> error : provider signals failure started --> error : upstream connection fails completed --> [*] error --> [*] note right of started ts = 0 id anchored end note note right of completed usage.in_tokens usage.out_tokens ts = total ms end note

Drag the diagram to pan →

Always first
started
Guaranteed. A client can treat its absence as a transport error.
Always last
completed / error
Exactly one terminal event closes every stream. No ambiguous half-open states.
Mid-stream
tool_call
Tool round-trips suspend the delta sequence and resume it transparently.
Nested
sub_request
Carries a child envelope. A dispatch can fan out and still emit one flat stream.

03 Signed records

Decided, believed, assumed

idr records what was decided; context records what was believed. A continuity event, added in HAPPI/1.5, records what was assumed — the load-bearing premise an agent is about to act on, named together with the observation that would refute it. It completes the family: decision, belief, and the reasoning path between them.

The event is not a terminator, and that is the whole design. A continuity-commit is emitted BEFORE the dispatch whose reasoning depends on it, so a reader checking the stream can see that the premise and its falsifier predate the outcome. Reasoning recorded after the result can always be fitted to it; reasoning recorded before it cannot. A later continuity-verdict settles the commitment it names, with one of CONGRUENT, INCONGRUENT or UNVERIFIABLE.

flowchart LR Commit["continuity-commit\nclaim · falsifier\ndeps · severity"] Dispatch(["The dispatch whose\nreasoning depends on it"]) Stream["started → delta × N\n→ completed"] Verdict["continuity-verdict\nCONGRUENT · INCONGRUENT\nUNVERIFIABLE"] Commit -->|"emitted BEFORE"| Dispatch Dispatch --> Stream Stream -->|"outcome observed"| Verdict Verdict -.->|"settles: sha256 of the commitment"| Commit

Drag the diagram to pan →

A runtime emits the record with the continuity.append cmd. The address is a content hash over the commitment body — canonical JSON with sorted keys, excluding the volatile id, ts and audit fields — and it is byte-identical to the context address algorithm, so one implementation serves both. The runtime rejects an unrecognised kind, and rejects a verdict with no settles, rather than writing a record that cannot be checked.

{"v":"happi/1.5","id":…,"type":"continuity","ts":…,
 "continuity_ref":{
   "sha256":                 "sha256:<hex>",
   "kind":                   "continuity-commit" | "continuity-verdict",
   "predecessor_continuity": <id of the prior node, null at genesis>,
   "settles":                <sha256 of the commitment — required on a verdict>,
   "model_versions":         [ … ]
 }}

Emitted, not described — bash happi.md run

$ echo '{"v":"happi/1.5","id":"c1","cmd":"continuity.append",
   "args":["{\"claim\":\"staging schema matches prod\",\"falsifier\":\"a column in prod is absent in staging\",\"deps\":[\"migration-0142\"],\"severity\":\"high\"}"],
   "flags":{"kind":"continuity-commit"}}' | bash happi.md run

{"v":"happi/1.5","id":"c1","type":"started","ts":0}
{"v":"happi/1.5","id":"c1","type":"continuity","ts":0,"continuity_ref":{"sha256":"sha256:10acdeb6d5dd6e30b73fa0a35497ce464614f1568948573c363ab9f4f983f2cd","kind":"continuity-commit","predecessor_continuity":null,"settles":null,"model_versions":[]}}
{"v":"happi/1.5","id":"c1","type":"completed","ts":0,"usage":{"content_addr":"sha256:10acdeb6d5dd6e30b73fa0a35497ce464614f1568948573c363ab9f4f983f2cd","kind":"continuity-commit"}}

Read the order, because the order is the claim. The continuity line sits between started and completed — it does not close the stream, completed does. That is what lets a real run put its premise and its falsifier on the wire first and then carry on: anything emitted afterwards is already committed against. Settling it takes a second envelope carrying flags.settles set to that sha256 — the literal string printed above, which is what makes the two records join.

The settlement — settles is the address above, verbatim

$ echo '{"v":"happi/1.5","id":"c2","cmd":"continuity.append",
   "args":["{\"verdict\":\"INCONGRUENT\",\"axis\":\"schema\",\"evidence\":\"prod.orders.tax_code absent in staging\",\"verifier\":\"migration-diff\"}"],
   "flags":{"kind":"continuity-verdict","predecessor_continuity":"c1",
     "settles":"sha256:10acdeb6d5dd6e30b73fa0a35497ce464614f1568948573c363ab9f4f983f2cd",
     "model_versions":["claude-opus-4-7"]}}' | bash happi.md run

{"v":"happi/1.5","id":"c2","type":"started","ts":0}
{"v":"happi/1.5","id":"c2","type":"continuity","ts":0,"continuity_ref":{"sha256":"sha256:7eb4a426c4f0498be0769800cf35df1992aa03d4a1a253869212f5ae7f426388","kind":"continuity-verdict","predecessor_continuity":"c1","settles":"sha256:10acdeb6d5dd6e30b73fa0a35497ce464614f1568948573c363ab9f4f983f2cd","model_versions":["claude-opus-4-7"]}}
{"v":"happi/1.5","id":"c2","type":"completed","ts":0,"usage":{"content_addr":"sha256:7eb4a426c4f0498be0769800cf35df1992aa03d4a1a253869212f5ae7f426388","kind":"continuity-verdict"}}

Run both yourself and the addresses come back identical, because the address is a hash of the body and nothing else — not the clock, not the run. That is the whole point of a content address: the commitment you can reproduce is the commitment that was made. Note also what the first record shows about model_versions: the spec table marks it required, but the commit above passed none and the runtime emitted [] and exited zero. It is required of the author, not enforced by the runtime.

And what it refuses — both checks, quoted from the runtime

$ echo '{"v":"happi/1.5","id":"x","cmd":"continuity.append","args":["{\"verdict\":\"CONGRUENT\"}"],"flags":{"kind":"continuity-verdict"}}' | bash happi.md run

{"v":"happi/1.5","id":"x","type":"started","ts":0}
{"v":"happi/1.5","id":"x","type":"error","ts":0,"code":"parse_error","message":"continuity.append: a continuity-verdict must set flags.settles to the content address of the commitment it settles"}
# exit 1

$ echo '{"v":"happi/1.5","id":"x","cmd":"continuity.append","args":["{\"claim\":\"a\"}"],"flags":{"kind":"continuity-guess"}}' | bash happi.md run

{"v":"happi/1.5","id":"x","type":"started","ts":0}
{"v":"happi/1.5","id":"x","type":"error","ts":0,"code":"parse_error","message":"continuity.append: kind must be one of continuity-commit, continuity-verdict, got 'continuity-guess'"}
# exit 1

A verdict that names no commitment cannot be joined back to a premise, and a kind the runtime does not know cannot be replayed by anything reading the chain. Both would produce a record that looks like evidence and is not, so the runtime declines to write either.

The protocol carries the record, never the policy. Dependency-contamination closure, settlement worklists, predictor stacks and any braking or halting behaviour built on these records live in the harness — exactly the division idr and context already keep. HAPPI/1.5 is fully backward compatible: the runtime accepts happi/1.0 through happi/1.5 and emits happi/1.5.

New event type
continuity
Carries continuity_ref. Not a terminator — the ordering is the guarantee.
New cmd
continuity.append
Joins version, cite.verify, compose, echo, spec.describe, envelope.validate, idr.emit, context.append, pr.reference, hypothesis.register and quine.spawn.
Two kinds
commit / verdict
A commit names claim, falsifier, deps and severity. A verdict names verdict, axis, evidence and verifier.
Compatibility
1.0 – 1.4
A consumer that does not recognise the event forwards it inertly. Unknown types are ignored, never rejected.

04 Transport layer

Same contract, any wire

The HAPPI contract is transport-agnostic. The same envelope object and the same event stream appear identically whether the bytes travel over a Unix pipe, an HTTP connection, a WebSocket, or a message queue. Runtimes MUST expose at least stdio. Additional transports are additive — a client written for stdio works against HTTP without modification because the framing is the contract, not the transport.

flowchart TD Contract["HAPPI Contract\nOne envelope in → NDJSON stream out"] subgraph Transports ["Transport Layer (all produce identical events)"] stdio["stdio\nCanonical · pipe JSON in\nread NDJSON out"] http["HTTP / SSE\nPOST /dispatch\napplication/x-ndjson"] ws["WebSocket\nOne message in\nstream of messages out"] queue["Message Queue\nPublish envelope\nConsume event stream"] end Contract --> stdio Contract --> http Contract --> ws Contract --> queue stdio --> Provider(["Provider\nAdapter"]) http --> Provider ws --> Provider queue --> Provider

Drag the diagram to pan →

Canonical
stdio
The reference implementation. Works in every shell and language with zero dependencies.
Web-native
HTTP / SSE
POST the envelope, receive application/x-ndjson. Fetch-compatible.
Browser
WebSocket
Natural for browser clients — one message in, stream of messages out.
Async
Queue
Decouple sender from receiver. The event stream is still NDJSON on the consumer side.