Dual Emitter Contract
kahn intermediate 5 min read
ELI5
Two parallel postal forms — one for parcels of CI status, one for parcels of agent thoughts. Same paper size, same stamps, same drop-box ritual; only the contents differ. Keeping them as separate forms means a sender only carries the form they need.
Technical Deep Dive
contracts/kahn_emit.py (CI-prototype) and contracts/kahn_agent_emit.py (agent product surface) are sibling files in lockstep on shared concerns and deliberately diverged on schema-shape concerns.
Mirrored Concerns (verbatim)
| Concern | Both files |
|---|---|
| Timestamp shape | UTC, millisecond precision, Z suffix; now_ts() byte-identical |
| Append mode | "ab", one write per call, newline terminator, UTF-8; flush()+fsync() on __exit__ |
| Unknown-field passthrough | Builders never strip unknown keys |
| Validation gating | validate=False default; jsonschema imported lazily inside _build_validator() |
Deliberate Divergences
classDiagram class kahn_emit { +Status: pending..blocked +Outcome: clean..catastrophic +run_start() +node_transition() +node_attempt() +run_end() +Emitter ~no agent_id } class kahn_agent_emit { +AgentStatus: thinking..failed +AgentOutcome: converged..aborted +AuditResult: pass|fail|warn +agent_run_start() +agent_transition() +tool_invocation() +audit_checkpoint() +agent_run_end() +AgentEmitter +agent_id on every event } class transitions_schema_json class agent_transitions_schema_json kahn_emit ..> transitions_schema_json: validates against kahn_agent_emit ..> agent_transitions_schema_json: validates againstWhy Sibling, Not Merged
Per agent-fleet.md: merging would conflate Status + AgentStatus into one enum and re-introduce the drift the realignment fixed. Producers vendor exactly one file; mixing CI + agent symbols would force every consumer to filter.
Vendoring Contract
- Stdlib-only runtime. Zero non-stdlib deps unless
validate=True. - A producer in a sister repo copies one file into its tree and emits.
- The
audit:contract-shapecheckpoint (audit/contract-shape.json) verifies both emitters expose every event their respective schemas declare — drift here means events pass the constructor but fail downstream validation.
Key Terms
- lockstep → Shared concerns are mirrored without intentional divergence; touching one without the other is a bug.
- Frozen →
kahn_emit.pyis at v0.4.0-ci-prototype; do not extend with agent semantics. - dep-light producer → A sister repo that vendors the emitter with no extra Python deps.
Q&A
Q: Why is jsonschema imported lazily?
A: So vendoring stays cheap. A dep-light producer that never opts into validation never pays for jsonschema.
Q: Can I add a new event to kahn_emit.py?
A: No — the CI surface is frozen. New agent semantics go to kahn_agent_emit.py.
Q: What enforces lockstep across the two emitters?
A: Convention + the audit:contract-shape checkpoint. There is no shared base class — duplication is intentional so each emitter can be vendored alone.
Examples
A producer in choco-hq that wants to emit agent transitions copies only kahn_agent_emit.py plus agent_transitions.schema.json. Its pyproject.toml gains zero runtime deps; if it later opts into validation it adds jsonschema to its own extras.
neighbors on the map
- Producer / Reader Split onboarding a new producer repo