- Spec
- HAPPI/1.5
- Accepts
- happi/1.0 – 1.4
- Records
- idr · context · continuity
- Status
- Open specification
Build your own HAPPI runtime.
One stdin loop. Seven streaming events, three signed records. Any provider. Working starters in Python, TypeScript, Go, Rust, and Bash — copy, paste, run.
- Section
- 01
- Wire
- JSON in · NDJSON out
- Transport
- stdin / stdout
- Events
- 7 streaming + 3 signed
The minimal contract
A HAPPI runtime reads JSON envelopes from stdin, dispatches to a provider, and writes NDJSON event lines to stdout. That is the entire interface.
Envelope (stdin) — required fields
"v": "happi/1.5"
Protocol version. A v1.5 runtime accepts happi/1.0 through happi/1.5 unchanged, and emits its own version on every event it writes.
"id": "<string>"
Request ID — echoed on every event
"cmd": "<string>"
Provider command, e.g. anthropic.messages.create
"args": [...]
Positional args — first element is usually the prompt
One envelope in, one event stream out. That is the whole
transport contract, and it is worth stating plainly because the obvious
guess — that stdin is a stream of newline-delimited envelopes —
is wrong. NDJSON is the shape of the output, not the input. A
runtime handed two envelopes on one stream is entitled to fail, and the
reference runtime does: parse_error, exit 1. To dispatch
several in one call, use compose — which is also where
sub_request comes from: the parent announces each child
envelope, then inlines that child's own events into the parent's stream.
The starters below wrap their dispatcher in a read-loop, which is a
convenience for piping several requests through one process. Convenient,
and a superset — do not mistake it for the contract.
Seven streaming event types (stdout) — emit these in order
A minimal synchronous runtime needs only started, delta, completed, and error. The remaining three enable tool-calling and agent recursion. Every stream ends with completed or error.
Four standard error codes — a closed set
"parse_error"
The envelope, or an argument inside it, could not be read. Also the code for a record the runtime refuses to write — a malformed continuity commitment lands here.
"unsupported_cmd"
The envelope parsed, but this runtime implements no such cmd. Not a bug in the caller's JSON — a gap in the runtime, which is why it is its own code.
"auth_error"
The provider rejected the credentials. Distinguished from a generic failure so a caller can re-authenticate rather than retry.
"runtime_error"
Anything else that went wrong during dispatch — the catch-all, and the only one a starter should reach for by default.
Invent a fifth and a consumer that switches on code has no branch for it. The starters below use exactly these four. An error event also means the process exits non-zero: the event is the structured explanation, the exit status is the signal a shell pipeline can act on. Both, not one or the other.
Three signed-record event types — the audit family
Each carries a content address rather than content. idr (v1.1) records what was decided, context (v1.1) what was believed, and continuity (v1.4) what was assumed. All three are optional: a runtime that implements only the streaming seven is conformant, and a consumer that does not recognise these types forwards them inertly rather than rejecting them. How to implement them →
{"v":"happi/1.5","id":"req-1","type":"started","ts":0}
{"v":"happi/1.5","id":"req-1","type":"delta","ts":12,"text":"Hello"}
{"v":"happi/1.5","id":"req-1","type":"delta","ts":24,"text":", world"}
{"v":"happi/1.5","id":"req-1","type":"completed","ts":180,"usage":{"input_tokens":3,"output_tokens":2}}
The starters below accept a happi/1.0 envelope on stdin and emit happi/1.5 events. That asymmetry is the back-compat rule, not an oversight: the envelope carries the version the caller speaks, and every event carries the version the runtime speaks.
- Section
- 02
- Added
- v1.1 · v1.4
- Kinds
- continuity-commit
continuity-verdict - Verdicts
- CONGRUENT
INCONGRUENT
UNVERIFIABLE
Signed records v1.1 – v1.4
idr records what was decided; context records
what was believed. A continuity event records what was
assumed. Implement the streaming seven and your runtime
is conformant. Implement these three and it is auditable — a reader
can reconstruct not just what came out, but what the agent was standing
on when it decided.
continuity — pre-registered falsifiable commitment (v1.4)
A continuity-commit names a load-bearing premise together with the observation that would refute it, and is emitted before the dispatch whose reasoning depends on it. Reasoning recorded after the result can always be fitted to it; reasoning recorded before it cannot. So unlike idr and context, which are terminators, a continuity event is not a terminator — and that is the whole design. The ordering is the guarantee: a reader verifies from the stream alone that the reasoning predates the outcome. A later continuity-verdict settles the commitment it names.
{"v":"happi/1.5","id":"c1","type":"continuity","ts":41,"continuity_ref":{"sha256":"sha256:b426ef402f294189acdeebf2013b2d72c55bb2d6b301893642b9c509b8f11b1f","kind":"continuity-commit","predecessor_continuity":null,"settles":null,"model_versions":["claude-opus-4-7"]}}
continuity_ref — the five fields
"sha256"required
Content address of the commitment body — "sha256:<hex>" over canonical JSON with sorted keys, excluding the volatile id, ts and audit fields. Byte-identical to the context address algorithm, so one implementation serves both.
"kind"required
continuity-commit or continuity-verdict. Nothing else — an unrecognised kind is rejected.
"predecessor_continuity"optional
The id of the prior continuity node — the backward chain link. null at genesis.
"settles"verdicts only
The sha256 of the commitment this verdict settles. Required on a continuity-verdict: a settlement that names no commitment cannot be joined back to a premise, so it proves nothing.
"model_versions"author-required
The model identifiers that produced the record — the audit chain. An array of model names, e.g. ["claude-opus-4-7"]; supplied through flags.model_versions. Required of the author, but not enforced by the runtime: the reference implementation coerces a missing value to [] and still exits 0, so a record with an empty audit chain is written rather than refused. Omit it and the record cannot answer which model believed this.
The two bodies
continuity-commit
The premise: claim, falsifier, deps, severity.
continuity-verdict
The settlement: verdict — one of CONGRUENT, INCONGRUENT, UNVERIFIABLE — plus axis, evidence, verifier.
continuity.append — commit first, settle later
# 1. Pre-register the premise BEFORE the dispatch that rests on it.
# kind defaults to continuity-commit.
echo '{"v":"happi/1.5","id":"c1","cmd":"continuity.append","flags":{"model_versions":["claude-opus-4-7"]},"args":["{\"claim\":\"the index is current\",\"falsifier\":\"a stale mtime in the run log\",\"deps\":[\"index-v3\"],\"severity\":\"high\"}"]}' \
| <your-runtime>
# Expect: continuity(kind="continuity-commit") / completed(content_addr, kind)
# 2. ...do the work the premise governs...
# 3. Settle it. flags.settles names the commitment address from step 1.
echo '{"v":"happi/1.5","id":"c2","cmd":"continuity.append","flags":{"kind":"continuity-verdict","settles":"sha256:b426ef402f294189acdeebf2013b2d72c55bb2d6b301893642b9c509b8f11b1f"},"args":["{\"verdict\":\"CONGRUENT\",\"axis\":\"index-freshness\",\"evidence\":\"run log line 41\",\"verifier\":\"ci\"}"]}' \
| <your-runtime>
# Expect: continuity(kind="continuity-verdict") / completed
Two checks fail closed, because a malformed record is worse than no record: an unrecognised kind is rejected rather than written through, and a verdict with no settles is refused. Both emit an error event with code:"parse_error" and exit non-zero — never a half-written record.
That address in settles is not a placeholder. It is what step 1 emits, and you can check it: run step 1 against any conforming runtime and the sha256 will be …b8f11b1f on your machine too, because the content address is taken over the commitment body with the volatile id, ts and audit fields excluded. The ts in your output will differ from ours. The address will not. That is the whole point of addressing the body rather than the event — and it is why step 3 can name step 1 at all.
Runtime cmd roster
A cmd your runtime does not implement returns error with code:"unsupported_cmd" and a non-zero exit — it never silently succeeds, and it never reports a gap in the runtime as a fault in the caller's JSON.
What stays out of the protocol. HAPPI carries the record, never the policy. Dependency-contamination closure, settlement worklists, and any braking or halting behaviour built on these records live in the harness — exactly the division idr and context already keep. Your runtime's job is to write a record someone else can check.
- Section
- 03
- Language
- Python
- Deps
- pip install anthropic
- Env
- ANTHROPIC_KEY
Python runtime starter
pip install anthropicEnv: export your Anthropic key as
ANTHROPIC_KEYRun:
echo '{"v":"happi/1.0","id":"1","cmd":"anthropic.messages.create","args":["Hello"]}' | python3 happi_runtime.py
#!/usr/bin/env python3
"""Minimal HAPPI/1.5 runtime in Python.
Reads JSON envelopes from stdin, emits NDJSON events to stdout.
Set your provider key in the environment before running.
"""
import json
import os
import sys
import anthropic
def emit(req_id: str, **fields) -> None:
sys.stdout.write(json.dumps({"v": "happi/1.5", "id": req_id, **fields}) + "\n")
sys.stdout.flush()
def dispatch(envelope: dict) -> None:
req_id = envelope["id"]
cmd = envelope.get("cmd", "")
args = envelope.get("args", [])
prompt = args[0] if args else ""
emit(req_id, type="started", ts=0)
try:
if cmd.startswith("anthropic."):
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_KEY"])
ts = 0
with client.messages.stream(
model="claude-opus-4-7",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
) as stream:
for text in stream.text_stream:
ts += 1
emit(req_id, type="delta", ts=ts, text=text)
usage = stream.get_final_message().usage
emit(req_id, type="completed", ts=ts + 1,
usage={"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens})
else:
emit(req_id, type="error", ts=0, code="unsupported_cmd",
message=f"cmd not supported by this runtime: {cmd!r}")
except Exception as exc:
emit(req_id, type="error", ts=0,
code="runtime_error", message=str(exc))
if __name__ == "__main__":
for raw in sys.stdin:
raw = raw.strip()
if raw:
dispatch(json.loads(raw))
- Section
- 04
- Language
- TypeScript
- Deps
- @anthropic-ai/sdk
ts-node · typescript - Env
- ANTHROPIC_KEY
TypeScript runtime starter
npm install @anthropic-ai/sdk ts-node typescriptEnv: export your Anthropic key as
ANTHROPIC_KEYRun:
echo '{"v":"happi/1.0","id":"1","cmd":"anthropic.messages.create","args":["Hello"]}' | ts-node happi_runtime.ts
#!/usr/bin/env ts-node
/**
* Minimal HAPPI/1.5 runtime in TypeScript/Node.js.
*
* Reads JSON envelopes from stdin, emits NDJSON events to stdout.
* Set your provider key in the environment before running.
*/
import { createInterface } from 'readline'
import Anthropic from '@anthropic-ai/sdk'
const rl = createInterface({ input: process.stdin, terminal: false })
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_KEY })
function emit(id: string, fields: Record<string, unknown>): void {
process.stdout.write(JSON.stringify({ v: 'happi/1.5', id, ...fields }) + '\n')
}
rl.on('line', async (line: string) => {
if (!line.trim()) return
const envelope = JSON.parse(line)
const { id, cmd, args = [] } = envelope
const prompt: string = (args as string[])[0] ?? ''
emit(id, { type: 'started', ts: 0 })
try {
if (cmd.startsWith('anthropic.')) {
let ts = 0
const stream = client.messages.stream({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }],
})
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
emit(id, { type: 'delta', ts: ++ts, text: event.delta.text })
}
}
const msg = await stream.getFinalMessage()
emit(id, { type: 'completed', ts: ++ts, usage: msg.usage })
} else {
emit(id, {
type: 'error', ts: 0,
code: 'unsupported_cmd',
message: `cmd not supported by this runtime: ${cmd}`,
})
}
} catch (err: unknown) {
emit(id, {
type: 'error', ts: 0,
code: 'runtime_error', message: String(err),
})
}
})
- Section
- 05
- Language
- Go
- Deps
- standard library only
- Env
- —
Go runtime starter
github.com/anthropics/anthropic-sdk-go inside dispatch()
for a real provider call.Run:
go run happi_runtime.go
// Minimal HAPPI/1.5 runtime in Go — standard library only.
// Wire a real provider SDK inside dispatch() for live inference.
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"time"
)
// Envelope is the HAPPI request shape — one JSON object per stdin line.
type Envelope struct {
V string `json:"v"`
ID string `json:"id"`
Cmd string `json:"cmd"`
Args []any `json:"args,omitempty"`
}
// emit writes a single HAPPI event line to stdout.
func emit(id, evType string, extra map[string]any) {
ev := map[string]any{
"v": "happi/1.5",
"id": id,
"type": evType,
"ts": time.Now().UnixMilli(),
}
for k, v := range extra {
ev[k] = v
}
b, _ := json.Marshal(ev)
fmt.Println(string(b))
}
// dispatch handles one envelope and emits the event stream.
func dispatch(env Envelope) {
emit(env.ID, "started", nil)
switch {
case env.Cmd == "echo":
// Built-in echo — no provider needed; useful for smoke-testing.
arg := ""
if len(env.Args) > 0 {
if s, ok := env.Args[0].(string); ok {
arg = s
}
}
emit(env.ID, "delta", map[string]any{"text": arg})
emit(env.ID, "completed", map[string]any{
"usage": map[string]int{"input_tokens": 0, "output_tokens": 0},
})
default:
emit(env.ID, "error", map[string]any{
"code": "unsupported_cmd",
"message": "cmd not implemented: " + env.Cmd,
})
}
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
var env Envelope
if err := json.Unmarshal(scanner.Bytes(), &env); err != nil {
continue // skip malformed lines
}
dispatch(env)
}
}
- Section
- 06
- Language
- Rust
- Deps
- serde · serde_json
- Env
- —
Rust runtime starter
serde = { version = "1", features = ["derive"] },
serde_json = "1"Run:
cargo run
// Minimal HAPPI/1.5 runtime in Rust.
// Cargo.toml [dependencies]:
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
//
// Add reqwest or an Anthropic SDK crate for live provider calls.
use serde::Deserialize;
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};
/// The HAPPI envelope read from stdin (one JSON object per line).
#[derive(Deserialize)]
struct Envelope {
#[allow(dead_code)]
v: String,
id: String,
cmd: String,
#[serde(default)]
args: Vec<Value>,
}
/// Write a single HAPPI event line to stdout.
fn emit(id: &str, fields: Value) {
let mut ev = json!({ "v": "happi/1.5", "id": id });
if let (Some(obj), Some(extra)) = (ev.as_object_mut(), fields.as_object()) {
for (k, v) in extra {
obj.insert(k.clone(), v.clone());
}
}
println!("{}", ev);
io::stdout().flush().ok();
}
/// Handle one envelope and emit the event stream.
fn dispatch(env: &Envelope) {
emit(&env.id, json!({ "type": "started", "ts": 0 }));
match env.cmd.as_str() {
"echo" => {
// Built-in echo — useful for smoke-testing without a provider.
let text = env.args.first().and_then(Value::as_str).unwrap_or("");
emit(&env.id, json!({ "type": "delta", "ts": 1, "text": text }));
emit(&env.id, json!({
"type": "completed", "ts": 2,
"usage": { "input_tokens": 0, "output_tokens": 0 }
}));
}
_ => {
emit(&env.id, json!({
"type": "error", "ts": 0,
"code": "unsupported_cmd",
"message": format!("cmd not implemented: {}", env.cmd)
}));
}
}
}
fn main() {
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line.expect("stdin read error");
if line.trim().is_empty() {
continue;
}
match serde_json::from_str::<Envelope>(&line) {
Ok(env) => dispatch(&env),
Err(err) => eprintln!("parse error: {err}"),
}
}
}
- Section
- 07
- Language
- Bash
- Deps
- curl · python3
- Env
- ANTHROPIC_KEY
Bash runtime starter
curl, python3 (JSON parsing), your Anthropic key in ANTHROPIC_KEYRun:
echo '{"v":"happi/1.0","id":"1","cmd":"anthropic.messages.create","args":["Hello"]}' | bash happi_runtime.sh
#!/usr/bin/env bash
# Minimal HAPPI/1.5 runtime in Bash.
# Calls the Anthropic REST endpoint via curl.
# Requires: curl, python3, and your key in ANTHROPIC_KEY.
set -euo pipefail
HAPPI_MODEL="${HAPPI_MODEL:-claude-opus-4-7}"
# emit <id> <type> <ts> [extra_json]
emit() {
local id="$1" type="$2" ts="$3" extra="${4-}"
printf '{"v":"happi/1.5","id":"%s","type":"%s","ts":%d%s}\n' \
"$id" "$type" "$ts" "${extra:+,$extra}"
}
# Extract a JSON field without jq
jget() { python3 -c "import sys,json; e=json.load(sys.stdin); print(e$1)"; }
jenc() { python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))"; }
dispatch() {
local line="$1"
local id cmd prompt
id=$(printf '%s' "$line" | jget "['id']")
cmd=$(printf '%s' "$line" | jget "['cmd']")
prompt=$(printf '%s' "$line" | jget ".get('args',[''])[0]")
emit "$id" "started" 0
if [[ "$cmd" == anthropic.* ]]; then
local body response text in_tok out_tok prompt_json text_json
prompt_json=$(printf '%s' "$prompt" | jenc)
body=$(printf \
'{"model":"%s","max_tokens":1024,"messages":[{"role":"user","content":%s}]}' \
"$HAPPI_MODEL" "$prompt_json")
response=$(curl -sS "https://api.anthropic.com/v1/messages" \
-H "x-api-key: ${ANTHROPIC_KEY}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data-binary "$body")
text=$(printf '%s' "$response" | jget "['content'][0]['text']")
in_tok=$(printf '%s' "$response" | jget "['usage']['input_tokens']")
out_tok=$(printf '%s' "$response" | jget "['usage']['output_tokens']")
text_json=$(printf '%s' "$text" | jenc)
emit "$id" "delta" 1 "\"text\":$text_json"
emit "$id" "completed" 2 \
"\"usage\":{\"input_tokens\":$in_tok,\"output_tokens\":$out_tok}"
else
local cmd_json
cmd_json=$(printf '%s' "$cmd" | jenc)
emit "$id" "error" 0 \
"\"code\":\"unsupported_cmd\",\"message\":\"cmd not supported: $cmd_json\""
fi
}
while IFS= read -r line; do
[[ -n "${line// }" ]] && dispatch "$line"
done
- Section
- 08
- Panels
- 4 + smoke test
- Required
- parsing · emission · errors
- Optional
- signed records
Conformance checklist
Run your runtime through these checks to verify it is HAPPI/1.5 compliant. A runtime that passes every item in the first three panels is a conformant implementation; the fourth panel applies only if you emit signed records.
Envelope parsing
"id" unchanged on every emitted event, and stamps "v" with the runtime's own version — not the envelope'serror event (not crash) on malformed JSONargs array gracefullyhappi/1.0 through happi/1.5 envelopes unchangedEvent emission
startedcompleted — only an idr or context terminator may follow itcompleted includes usage.input_tokens and usage.output_tokensdelta event carries a non-empty text fieldts is monotonically non-decreasing within a single requestError handling
cmd produces an error event with code:"unsupported_cmd", never a crash or a tracebackerror events include code (machine) and message (human)code is one of the four standard values — a runtime that invents a fifth breaks every consumer that switches on iterror event and exits non-zero — the event explains, the exit status signalsSigned records (v1.1 – v1.4) — optional
continuity event is emitted before the dispatch it governs — never as a terminatorcontinuity-verdict names the commitment it settles in settles, or is refusedcontinuity_ref.kind is rejected, not written throughcontinuity_ref.sha256 excludes the volatile id, ts and audit fieldsid / ts / audit produce the same content addressidr and context events, when emitted, follow completed or errorQuick smoke test
# Test 1: built-in echo (no provider key needed)
echo '{"v":"happi/1.0","id":"t1","cmd":"echo","args":["hello"]}' \
| <your-runtime>
# Expect: started / delta("hello") / completed
# Test 2: unknown command — must produce error event, not crash
echo '{"v":"happi/1.0","id":"t2","cmd":"no.such.cmd","args":[]}' \
| <your-runtime>
# Expect: started / error(code="unsupported_cmd"), exit 1
# Test 3: many envelopes, one dispatch — this is what `compose` is for.
# Note the sub_request events: the parent announces each child, then
# the child's own stream is inlined into the parent's.
echo '{"v":"happi/1.5","id":"p","cmd":"compose","flags":{"envelopes":[{"v":"happi/1.5","id":"a","cmd":"echo","args":["first"]},{"v":"happi/1.5","id":"b","cmd":"echo","args":["second"]}]}}' \
| <your-runtime>
# Expect: started(p) / sub_request(a) / started(a) delta("first") completed(a)
# / sub_request(b) / ...b... / completed(p, usage.children=2)
# Test 4: v1.4 fail-closed — a verdict that settles nothing must be refused
echo '{"v":"happi/1.5","id":"t4","cmd":"continuity.append","flags":{"kind":"continuity-verdict"},"args":["{\"verdict\":\"CONGRUENT\"}"]}' \
| <your-runtime>
# Expect: error(code="parse_error"), exit 1 — never a record that settles nothing
# Test 5: v1.4 fail-closed — an unrecognised kind must be refused, not written through
echo '{"v":"happi/1.5","id":"t5","cmd":"continuity.append","flags":{"kind":"continuity-guess"},"args":["{\"claim\":\"x\"}"]}' \
| <your-runtime>
# Expect: error(code="parse_error"), exit 1 — kind is a closed set of two
# Test 6: the content address ignores id and ts — same body, same address.
# Two SEPARATE runs, because one envelope per stream is the contract.
echo '{"v":"happi/1.5","id":"x1","cmd":"continuity.append","args":["{\"claim\":\"same body\"}"]}' | <your-runtime>
echo '{"v":"happi/1.5","id":"x2","cmd":"continuity.append","args":["{\"claim\":\"same body\"}"]}' | <your-runtime>
# Expect: different id, IDENTICAL continuity_ref.sha256 — that is the whole
# claim of content addressing, and it is one command to falsify.
Built a conformant runtime? Open an issue on the GitHub repository (coming) to get it listed on the HAPPI ecosystem page. The v1.0 contract is frozen and every version since has been purely additive — a v1.5 runtime still dispatches a v1.0 envelope unchanged, and a consumer that has never heard of continuity forwards it inertly. Your implementation will not break.