From 4bd9abdebc04ae4d2a3337213ca958620d2e551a Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sat, 18 Jul 2026 00:15:30 -0700 Subject: [PATCH] docs(contract): re-canonicalize #1 SSE event vocab against code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add awaiting_llm_first_token (#201) and affect_update (#204) to issue #1's Event union and TESTS via a dated amendment. Both events are parsed by _envelope_for_type and covered in tests/test_sse_client.py, but issue #1's Output union + full_event_vocab test were frozen at the v0.19.0 baseline's 8-event set — contract-vs-code drift surfaced during the Worldtree #371 SDK parity-matrix pass. Documentation-only: no code change, no version bump. --- docs/contracts/issues/1.contract.md | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/contracts/issues/1.contract.md b/docs/contracts/issues/1.contract.md index 90128ed..865b0bc 100644 --- a/docs/contracts/issues/1.contract.md +++ b/docs/contracts/issues/1.contract.md @@ -345,3 +345,48 @@ TESTS: buffer_expired_propagates [error]: attempt 1 drops after 42:1; reconnect returns 412 → ResumeBufferExpired propagates (not retried as a transient drop). zero_budget_no_resume [adversarial]: max_reconnects=0; attempt 1 drops after 42:1 → SseConnectionDropped propagates immediately (no reconnect attempted). ``` + +## Amendment 2026-07-18 — event-vocab catch-up: `awaiting_llm_first_token` + `affect_update` (contract-vs-code drift) + +The §Data flow Output union and the `full_event_vocab` TEST (FN `stream_turn`) +were frozen at the v0.19.0 baseline's 9-variant `Event` union and 8-event +happy-path vocab. Two SSE events were added to the wire AFTER that baseline and +are parsed by `_envelope_for_type` today, but their TESTS landed in the +*consuming* contracts (cli #3, tui #4, affect web-proxy #18) and in +`tests/test_sse_client.py` — never in this contract, so #1 under-described its own +module. Surfaced during the Worldtree #371 SDK parity-matrix pass (worldtree-dev +confirmed the gap: `full_event_vocab` stops at the 8-event set). This amendment +re-canonicalizes #1 against the code. It is **documentation-only** — the code and +its `test_sse_client.py` coverage already exist and pass; no code change, no +version bump. + +**Corrected `Event` union** (supersedes the §Data flow Output list — 9 → 11): + + WorkerPhase | Thinking | Text | TextBoundary | ToolStart | ToolResult + | Done | Error | Cancelled | AwaitingLlmFirstToken | AffectUpdate + +The two additions (both **non-terminal** — they do NOT satisfy INV-001's terminal +requirement; the stream still ends at exactly one `Done`/`Error`/`Cancelled`): + +- `AwaitingLlmFirstToken(sse_id, turn_id, elapsed_ms_since_building_prompt: float)` + — SSE `awaiting_llm_first_token` (Worldtree #201, v0.29.0). Heartbeat during the + BuildingPrompt→CallingLLM gap (default 5 s); `elapsed_ms_since_building_prompt` + is server-authoritative monotonic. First-gap only (INV-201-5) — tool round-trips + do not re-fire. +- `AffectUpdate(sse_id, status: str, turn_id, snapshot: dict | None)` — SSE + `affect_update` (Worldtree #204, v0.28.0). `status="current"` at turn start + carries the full persona snapshot; `status="scheduled"` after post-turn appraisal + kickoff omits it (`snapshot is None`). Suppressed for persona-disabled agents, + Tier-3 consumer-defined agents, and ephemeral sessions. + +Unknown event `type` still raises `ValueError` in `_envelope_for_type` (the +base-contract behavior). NOTE for the eventual repin: the #371 SDK pins a +deliberate divergence here (D-1 — skip-with-diagnostics-hook instead of raising), +which Ratatoskr adopts only when it repins onto the SDK, not before. + +TESTS (already present in `tests/test_sse_client.py` — `TestAwaitingLlmFirstToken` ++ `TestAffectUpdate`; mirrored here for contract completeness): + single_heartbeat_parsed [tracer]: mock emits one `awaiting_llm_first_token` (turn_id, elapsed_ms_since_building_prompt=5012.3) → consumer yields AwaitingLlmFirstToken carrying both fields + parsed sse_id + heartbeat_sequence_monotonic [trace]: mock emits a heartbeat sequence → each yields AwaitingLlmFirstToken; elapsed_ms_since_building_prompt is monotonically non-decreasing across the sequence + current_status_parsed_with_snapshot [tracer]: mock emits `affect_update` status="current" with a full snapshot dict → consumer yields AffectUpdate(status="current", snapshot=) + scheduled_status_parsed_no_snapshot [trace]: mock emits `affect_update` status="scheduled" (no snapshot) → consumer yields AffectUpdate(status="scheduled", snapshot is None)