V3 Issue #255: Shared typed runtime events
Issue: #255 — Unify typed runtime events across execution and interfaces
Requirements: R153–R160
Branch: Evan/v3-255-runtime-events, based on Evan/v3-236-execution-receipts
Dependency: #236, PR #281 (open, unmerged; implementation is present on this branch)
Delivery: one signed PR targeting the #236 branch; do not merge either PR.
Goal and boundaries
Establish one immutable runtime-owned event envelope and publisher, then adapt existing event callbacks and CLI/TUI/Web/SSE wire shapes at the interface edges. Preserve durable ownership of task, interaction, usage, and receipt state. Observers remain failure-isolated and cannot authorize work. Keep AgentEngine migration, database redesign, and provider continuation payloads out of scope.
The current compatibility path has a dictionary callback in openkyrozen/learning/failures.py, a ContextVar callback in openkyrozen/app/state.py, text events emitted throughout agent modules, and separate filtering/rendering logic in CLI/TUI/Web. Provider stream events already originate as typed provider records. The change will bridge these existing paths through the canonical publisher instead of adding another event store.
Indexed implementation plan
Task 1 — RuntimeEvent contract and publisher
Files:
- Add openkyrozen/agent/events.py for immutable RuntimeEvent validation, identity generation, serialization, and the runtime event publisher.
- Modify openkyrozen/agent/runtime.py, openkyrozen/app/state.py, openkyrozen/learning/failures.py, and agent exports.
- Test new behavior in tests/test_runtime_events.py.
Contract: versioned immutable records contain event ID, kind, timestamp, sequence, user/workspace/session identity, optional turn/model-request/call/task IDs, and a validated defensive JSON payload. A publisher owns monotonic sequence numbers per turn, isolates observer exceptions, and produces the existing dictionary shape only through a named compatibility adapter. Runtime callbacks receive on_runtime_event; on_event remains compatible.
- [x] Add regressions for immutable payloads, invalid JSON values, fresh turn/model-request IDs, per-turn sequence order, concurrent-session isolation, and observer exceptions.
- [x] Implement the event record and publisher; allocate a runtime-owned turn ID at chat entry and model-request IDs at the shared provider request boundary.
- [x] Preserve
on_eventdictionary consumers through one explicit adapter and verify they cannot mutate publisher state.
Task 2 — Correlate turn, model, tool, approval, usage, and task events
Files:
- Modify openkyrozen/agent/loop.py, openkyrozen/agent/initial_response.py, openkyrozen/agent/action_rounds.py, openkyrozen/agent/executor.py, openkyrozen/agent/protocol.py, and openkyrozen/agent/modes.py.
- Modify the shared provider boundary in openkyrozen/providers/calls.py and openkyrozen/providers/retry.py only where correlation must cross bounded workers.
- Modify approval emit/resolve paths in openkyrozen/security/ and durable plan/task event adapters in openkyrozen/tasks/.
- Test in tests/test_runtime_events.py, tests/test_execution_receipts.py, tests/test_provider_stream_events.py, and task/approval suites.
- [x] Add regressions for exactly one terminal turn event on success/error/cancellation, event correlation across model requests and tool calls, denied calls without execution-start, and correlation of approval request/resolution.
- [x] Translate typed provider stream events and cumulative usage snapshots into canonical runtime events without including opaque continuations, signatures, credentials, or executors.
- [x] Redact tool-call argument fragments from observer events; expose identity and size only because Web/TUI observers receive the same payloads.
- [x] Emit tool-call, authorized execution-start, and receipt-backed result events from the shared execution boundary. Preserve #236 receipt identity and projection.
- [x] Publish plan/task notifications only after their existing durable writes succeed. Keep task and plan persistence authoritative; do not create duplicate state.
- [x] Derive terminal turn status from explicit outcomes/exceptions, never response text; task acceptance remains independent.
- [x] Verify usage observer events reuse ledger snapshots and do not charge usage.
Task 3 — Native text pass-through and isolated legacy protocol adapter
Files:
- Modify openkyrozen/agent/types.py, openkyrozen/agent/response_recovery.py, and openkyrozen/agent/initial_response.py to route text protocol handling through an explicit legacy adapter.
- Modify affected compatibility callers and provider/runtime tests.
- [x] Add tests proving text from requests using native tool schemas containing literal
Action:andTaskList:is emitted unchanged and cannot be interpreted as control; keep the no-tools textual foreground protocol on its compatibility path until #238. - [x] Keep legacy filtering in one explicit adapter, and ensure native structured tool calls remain typed and partial calls are never executable.
- [x] Patch textual parsers/filters to fail in typed-stream tests and prove native event publication does not call them.
Task 4 — CLI, TUI, Web, SSE adapters and final acceptance
Files:
- Modify CLI event rendering under openkyrozen/interfaces/cli/.
- Modify openkyrozen/interfaces/tui/turns.py and its protocol tests.
- Modify openkyrozen/interfaces/web/chat.py, relevant Web chat modules, and SSE tests.
- Update runtime event docs and exported API surfaces; do not change persistence schema.
- [x] Add regressions asserting equivalent event ordering and correlation for CLI/TUI/Web/SSE adapters, with no duplicate text/result rendering and with existing compatibility wire fields preserved.
- [x] Verify redaction, session isolation, cancellation, and observer-failure isolation across each interface.
- [x] Run full Python 3.12 tests with browser tests enabled,
make check,make lint,make docs-check, Go TUI tests, and offline agent/subagent/TUI acceptance. Run focused suites on Python 3.13. - [x] Obtain independent review and fix material findings.
- [ ] GPG-sign commits, verify locally and on GitHub, attach the dependent PR, and wait for final-head CI and reviews. Leave #255 and PR open and unmerged.
Validation record
| Result | Evidence |
|---|---|
| PASS | Python 3.12: full make test (798 tests; browser tests enabled, no skips); focused runtime/provider/SSE/TUI suite (53 tests). Python 3.13: focused runtime/provider/SSE/TUI suite (37 tests). Final diff passed make check, make lint, make docs-check, make agent-acceptance, make subagent-acceptance, and make tui-acceptance. Independent review findings fixed and covered by regressions. |
| FAIL | None after fixes. |
| BLOCKED | GitHub PR checks and signature verification remain pending publication. |
Compatibility and risks
Existing on_event consumers retain dictionaries through a compatibility adapter. CLI/TUI/Web/SSE wire fields remain compatible while sourcing them from canonical events. Text protocol filtering remains available only at the legacy boundary; native response text is not inspected for control syntax. Event payloads are JSON-safe, bounded/redacted projections. Durable stores and usage ledgers remain the sources of truth. Cancellation and observer failures must not alter execution outcomes.