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
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. meta carries
out-of-band signals (rate-limit headers, model version) and never affects
the primary output.
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 --> 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
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.
Interleaved
tool_call
Tool round-trips suspend the delta sequence and resume it transparently.
Out-of-band
meta
Carries headers and diagnostics. Never part of the primary text output.
03 · 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
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.