Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/substrate/tasks/turn-lifecycle-and-sensorium-completion.md
Fimeg e480809c70 docs: rescue the agent-substrate tree out of a gitignored directory
219 files, 2.0 MB, untracked in souveraine/docs and existing nowhere else.
The volume is at 100% with no snapshots.
2026-07-26 12:11:50 -04:00

7.1 KiB
Raw Permalink Blame History

task_id title status assignee priority phase references
turn-lifecycle-001 Turn Lifecycle Events + Sensorium Completion — unblock Matrix outbound scoping high 2.5
src/core/nervous/turn_dispatcher.rs
src/core/sensorium/mod.rs
src/backend/local/turn.rs
docs/tasks/matrix-sensorium.md
docs/tasks/federation-summon.md

Task: Turn Lifecycle Events & Sensorium Completion

Status: already built vs still needed

A careful audit against the research shows the picture is different from what the task docs assumed.

What already exists

TurnEventDispatcher (src/core/nervous/turn_dispatcher.rs) — fully built. 8 emit methods, 207 lines, 4 tests. Constructed in turn.rs:194 and wired at every integration point: reasoning chunks, tool start/end, stream segment, turn finish, primary complete, interrupt. It fires SensorEvents namespaced as turn:* onto the EventBus.

Missing from the dispatcher: emit_tool_call for the full tool call details (arguments), and emit_n1_start/emit_n1_end for the subconscious pass lifecycle. These are currently fired as raw SensorEvent::send() calls in turn.rs:618-627 and turn.rs:656-665 rather than going through the structured dispatcher.

Sensorium trait (src/core/sensorium/mod.rs) — already redesigned with the run(EventBus, CancellationToken) pattern. Methods align with letta-code's ChannelAdapter interface. SensoriumCoordinator manages spawn/shutdown lifecycle. 575 lines, 4 tests.

Stub sensoriaTuiSensorium::run() and MobileSensorium::run() both use run_event_loop() which subscribes to the EventBus. They're debugs-only but structurally correct.

What's actually missing

  1. MatrixSensorium — the concrete implementation. Inbound path is wired (SensoriumInputHandler). Outbound is stubbed: send_message exists on the trait but MatrixSensorium doesn't implement it meaningfully.

  2. Matrix client sharing — the matrix_client lives inside run()'s closure. Need Option<Arc<MatrixClient>> on the struct so send_message() can reach it.

  3. Turn lifecyle events are sparse — the dispatcher emits turn:segment for every 10-char chunk (fine), but there's no structured tool-call event with arguments (the TUI card needs it), and no N+1 lifecycle events through the dispatcher.

  4. No sensorium consumes turn events yet — TuiSensorium's run just logs them. The real TUI renders via ratatui's Frame directly, not through the EventBus. Matrix would be the first surface that genuinely drives itself from the event stream.

Research synthesis

Harness Pattern What Souveraine already has
letta-code ChannelPluginChannelAdapter, lifecycle events via dispatchTurnLifecycleEvent() Sensorium trait matches ChannelAdapter. TurnEventDispatcher matches lifecycle dispatch.
hermes-agent BasePlatformAdapter(ABC) + GatewayStreamConsumer (sync→async queue bridge) EventBus is the broadcast channel. Missing: a sync→async bridge for streaming (the LLM call is sync, the surface is async).
OpenHarness StreamEvent union type, three callbacks per surface BackendEvent is Souveraine's equivalent. The TUI consumes it. Matrix would consume SensorEvent off the bus instead.
jcode Bus::global() + ServerEvent canonicalized stream EventBus. TurnEventDispatcher is the canonicalizer.
letta-code upstream (new) Custom webhook adapter — 130 lines, no inbound Direct model for MatrixSensorium::send_message

Key insight: three registries, not one

hermes-agent separates concerns cleanly:

  • Platform Registry — what surfaces exist (Telegram, Matrix, CLI)
  • Provider Registry — what LLM backends exist (OpenAI, Anthropic, Ollama)
  • Transport Registry — how to speak each wire format (chat_completions, anthropic_messages)

Souveraine already has the Bifrost client as the single transport. The question is whether to build a provider registry analogous to hermes-agent's ProviderProfile + ProviderTransport when multi-provider BYOK lands. That's a separate scope decision — this task is about the surface side.

Scope

Part A: Tighten the TurnEventDispatcher (1 session)

  1. Add emit_tool_call() with full arguments (for TUI card rendering via EventBus)
  2. Move raw SensorEvent::send() calls for N+1 start/end into the dispatcher (emit_n1_start(), emit_n1_end())
  3. Verify all integration points in turn.rs use the dispatcher consistently

Part B: MatrixSensorium outbound (2 sessions)

The concrete Matrix adapter that:

  1. Stores matrix_client as Option<Arc<MatrixClient>> on the struct (set by run())
  2. run() subscribes to EventBus for turn:* events keyed by this room's conversation_id
  3. send_message() posts via matrix_client
  4. Streaming model: first chunk creates a new Matrix message via send_message(), subsequent chunks call edit() via m.replace at 150ms throttle (the letta-code streaming.ts pattern)
  5. handle_turn_event() dispatches by event type: turn:segment → message edit, turn:reasoning → thinking placeholder, turn:tool_start → tool card

Part C: SensoriumCoordinator wiring (1 session)

Wire SensoriumCoordinator into LocalBackend so constructed sensoria actually run. Currently the coordinator exists but nothing calls register() or run_all().

Part D: Not in scope

  • Provider registry / multi-provider BYOK — the Bifrost client stays as the single transport
  • Lettа-style plugin discovery (channel.json + plugin.mjs) — not needed until third-party surfaces
  • Declarative config schema (7 field types) — deferred until a settings UI needs it
  • In-process Lite→Full upgrade for federation — deferred in federation-summon.md

Files affected

File Change
src/core/nervous/turn_dispatcher.rs Add emit_tool_call, emit_n1_start, emit_n1_end
src/backend/local/turn.rs Replace raw SensorEvent::send() for N+1 events with dispatcher calls
src/core/sensorium/matrix/mod.rs Full MatrixSensorium impl (feature-gated)
src/core/sensorium/matrix/client.rs Build client + sync loop
src/core/sensorium/matrix/turn.rs MatrixTurn coordinator (port from your letta-code turn/)
src/core/sensorium/matrix/stream.rs Streaming message edits via m.replace
src/core/sensorium/matrix/blocks.rs Tool/thinking block rendering
src/core/sensorium/matrix/html.rs Markdown → Matrix HTML formatter
src/backend/local/mod.rs Wire SensoriumCoordinator
src/core/sensorium/mod.rs Minor: TuiSensorium/MobileSensorium plumbing

Build sequence

  1. Part A — tighten dispatcher. Pure additive, no risk. 4 new emit methods.
  2. Part C — wire coordinator. The Matrix sensorium has nothing to subscribe to otherwise.
  3. Part B — build MatrixSensorium. Blocked on Part A (the dispatcher is the event source) and Part C (the coordinator is the runtime harness). But the struct + client sharing + sync loop can be built independently and tested with cargo test --features matrix.