feat(runtime): add RuntimeHost command boundary #97

Merged
lost-rob0t merged 14 commits from feature/runtime-host into master 2026-08-12 15:49:03 +00:00
lost-rob0t commented 2026-08-12 15:28:47 +00:00 (Migrated from github.com)

Closes #83.

Summary

Adds the second desktop P0 boundary: a Qt-neutral RuntimeHost and application command vocabulary over the existing Zara runtime.

  • add typed commands for turn submit/cancel, voice, speech mute, tool approval/rejection, restart and shutdown
  • add a backend contract plus a thin adapter over the existing AgentManager; no second LLM/tool implementation is introduced
  • run runtime initialization and turn execution on a dedicated asyncio worker thread
  • use the existing Pykka TurnCoordinator as the canonical source of turn IDs, cancellation and stale-result rejection
  • preserve both canonical turn_id and conversation_id all the way into the existing AgentManager/LangGraph event stream
  • return immediate command receipts so UI callers never block waiting for a full assistant turn
  • suppress late results after cancellation even when a backend catches/swallow asyncio cancellation
  • keep startup failures alive in a degraded state so explicit restart remains possible
  • queue explicit shutdown requests while STARTING, including initial startup and runtime restart, with the queue + STARTING→RUNNING transition protected by the same lock
  • add clean cancel/drain/backend/coordinator shutdown semantics
  • add zara.desktop.qt_bridge.QtRuntimeBridge, a UI-free PySide6 adapter that drains runtime-event queues on the Qt thread and maps command futures to queued Qt signals
  • suppress late Qt completion/error signals after the bridge is closed
  • keep all Qt imports outside zara.runtime

Architecture

Qt main thread
  |
  | RuntimeCommand + queued Qt signals
  v
QtRuntimeBridge
  |
  | concurrent Future / RuntimeEvent queue
  v
RuntimeHost worker thread + asyncio loop
  |
  +-- existing TurnCoordinator actor
  +-- RuntimeBackend
        |
        +-- existing AgentManager / providers / tools

RuntimeHost is usable without QApplication and zara.runtime remains Qt-free. The bridge contains no windows, tray, or assistant logic.

Cancellation invariant

SubmitTurn first asks the existing TurnCoordinator for the canonical turn ID. CancelTurn marks that coordinator state cancelled before cancelling the asyncio task. Before any completed backend result can publish ResponseText / OutputReady, RuntimeHost asks the coordinator whether that turn is still active. A backend that swallows cancellation therefore still cannot leak stale output.

Startup/quit invariant

An explicit ShutdownRuntime received while RuntimeHost is STARTING is queued instead of rejected. The shutdown queue is consumed atomically with the STARTING→RUNNING transition, so a quit request cannot fall through a state-transition race. This applies both to initial startup and to RestartRuntime; a queued quit tears the backend/coordinator down and does not emit a misleading new RuntimeStarted event.

Integration note

The default AgentRuntimeBackend reuses the existing text-agent path and supports injected manager factories. Full desktop bootstrap will supply the existing integrated Zara services as subsequent P0 slices land; this PR deliberately does not duplicate wake.py voice/Prolog startup or pretend context attachments are already wired. Non-empty context IDs are rejected until #88 instead of silently ignored.

Tests

Adds deterministic coverage for:

  • backend initialization/turn/stop thread affinity away from the caller thread
  • canonical turn correlation and event sequence
  • idempotent cancellation
  • stale-result suppression when a backend returns after cancellation
  • degraded startup + explicit restart recovery
  • explicit shutdown during initial backend startup
  • explicit shutdown during runtime restart
  • unsupported optional commands without host death
  • pre-start rejection and clean shutdown
  • runtime package remaining Qt-free
  • Qt event draining on the Qt application thread
  • Qt command completion/failure queued back to the Qt thread
  • bridge close/unsubscribe behavior

GitHub Actions / nix develop -c bash scripts/test-all.sh is the authoritative merge gate because the local /home/unseen/Documents/Projects checkout is not mounted in this execution environment.

Closes #83. ## Summary Adds the second desktop P0 boundary: a Qt-neutral `RuntimeHost` and application command vocabulary over the existing Zara runtime. - add typed commands for turn submit/cancel, voice, speech mute, tool approval/rejection, restart and shutdown - add a backend contract plus a thin adapter over the existing `AgentManager`; no second LLM/tool implementation is introduced - run runtime initialization and turn execution on a dedicated asyncio worker thread - use the existing Pykka `TurnCoordinator` as the canonical source of turn IDs, cancellation and stale-result rejection - preserve both canonical `turn_id` and `conversation_id` all the way into the existing AgentManager/LangGraph event stream - return immediate command receipts so UI callers never block waiting for a full assistant turn - suppress late results after cancellation even when a backend catches/swallow asyncio cancellation - keep startup failures alive in a degraded state so explicit restart remains possible - queue explicit shutdown requests while STARTING, including initial startup and runtime restart, with the queue + STARTING→RUNNING transition protected by the same lock - add clean cancel/drain/backend/coordinator shutdown semantics - add `zara.desktop.qt_bridge.QtRuntimeBridge`, a UI-free PySide6 adapter that drains runtime-event queues on the Qt thread and maps command futures to queued Qt signals - suppress late Qt completion/error signals after the bridge is closed - keep all Qt imports outside `zara.runtime` ## Architecture ```text Qt main thread | | RuntimeCommand + queued Qt signals v QtRuntimeBridge | | concurrent Future / RuntimeEvent queue v RuntimeHost worker thread + asyncio loop | +-- existing TurnCoordinator actor +-- RuntimeBackend | +-- existing AgentManager / providers / tools ``` `RuntimeHost` is usable without `QApplication` and `zara.runtime` remains Qt-free. The bridge contains no windows, tray, or assistant logic. ## Cancellation invariant `SubmitTurn` first asks the existing `TurnCoordinator` for the canonical turn ID. `CancelTurn` marks that coordinator state cancelled before cancelling the asyncio task. Before any completed backend result can publish `ResponseText` / `OutputReady`, RuntimeHost asks the coordinator whether that turn is still active. A backend that swallows cancellation therefore still cannot leak stale output. ## Startup/quit invariant An explicit `ShutdownRuntime` received while RuntimeHost is STARTING is queued instead of rejected. The shutdown queue is consumed atomically with the STARTING→RUNNING transition, so a quit request cannot fall through a state-transition race. This applies both to initial startup and to `RestartRuntime`; a queued quit tears the backend/coordinator down and does not emit a misleading new `RuntimeStarted` event. ## Integration note The default `AgentRuntimeBackend` reuses the existing text-agent path and supports injected manager factories. Full desktop bootstrap will supply the existing integrated Zara services as subsequent P0 slices land; this PR deliberately does not duplicate `wake.py` voice/Prolog startup or pretend context attachments are already wired. Non-empty context IDs are rejected until #88 instead of silently ignored. ## Tests Adds deterministic coverage for: - backend initialization/turn/stop thread affinity away from the caller thread - canonical turn correlation and event sequence - idempotent cancellation - stale-result suppression when a backend returns after cancellation - degraded startup + explicit restart recovery - explicit shutdown during initial backend startup - explicit shutdown during runtime restart - unsupported optional commands without host death - pre-start rejection and clean shutdown - runtime package remaining Qt-free - Qt event draining on the Qt application thread - Qt command completion/failure queued back to the Qt thread - bridge close/unsubscribe behavior GitHub Actions / `nix develop -c bash scripts/test-all.sh` is the authoritative merge gate because the local `/home/unseen/Documents/Projects` checkout is not mounted in this execution environment.
Sign in to join this conversation.
No description provided.