Skip to content
Spec
HAPPI/1.5
Accepts
happi/1.0 – 1.4
Records
idr · context · continuity
Status
Open specification
Build a Runtime · HAPPI/1.5

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

started delta completed tool_call tool_result sub_request error

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

idr context continuity

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

version cite.verify compose echo spec.describe envelope.validate idr.emit context.append continuity.append pr.reference hypothesis.register quine.spawn

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

Install: pip install anthropic
Env: export your Anthropic key as ANTHROPIC_KEY
Run: 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

Install: npm install @anthropic-ai/sdk ts-node typescript
Env: export your Anthropic key as ANTHROPIC_KEY
Run: 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

No external deps for the skeleton — uses only the standard library. Wire in 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

Cargo.toml deps: 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

Requires: curl, python3 (JSON parsing), your Anthropic key in ANTHROPIC_KEY
Run: 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

Accepts valid JSON objects on stdin, one per line
Echoes "id" unchanged on every emitted event, and stamps "v" with the runtime's own version — not the envelope's
Ignores unknown envelope fields without error
Emits error event (not crash) on malformed JSON
Supports empty args array gracefully
Handles multiple envelopes sequentially on the same stdin
Accepts happi/1.0 through happi/1.5 envelopes unchanged

Event emission

First event for each request is always started
Last streaming event for a successful request is completed — only an idr or context terminator may follow it
completed includes usage.input_tokens and usage.output_tokens
Each delta event carries a non-empty text field
ts is monotonically non-decreasing within a single request
Events are flushed to stdout immediately (no line-buffering)

Error handling

An unsupported cmd produces an error event with code:"unsupported_cmd", never a crash or a traceback
error 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 it
A provider HTTP failure is reported as a structured error event and exits non-zero — the event explains, the exit status signals
A stream that never errors exits 0 after stdin closes

Signed records (v1.1 – v1.4) — optional

A continuity event is emitted before the dispatch it governs — never as a terminator
A continuity-verdict names the commitment it settles in settles, or is refused
An unrecognised continuity_ref.kind is rejected, not written through
continuity_ref.sha256 excludes the volatile id, ts and audit fields
Two bodies identical but for id / ts / audit produce the same content address
idr and context events, when emitted, follow completed or error
Unknown event types and unknown fields are forwarded inertly, never rejected

Quick 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.