Skip to content

feat: wire Bus Inspector into shell#206

Open
runyaga wants to merge 4 commits into
soliplex:mainfrom
runyaga:feat/wire-bus-inspector
Open

feat: wire Bus Inspector into shell#206
runyaga wants to merge 4 commits into
soliplex:mainfrom
runyaga:feat/wire-bus-inspector

Conversation

@runyaga
Copy link
Copy Markdown
Contributor

@runyaga runyaga commented May 1, 2026

Part of #202 (4 of 4 stacked PRs). Depends on #203, #204, and #205 — merge those first then rebase this.

What this changes

Surfaces the Bus Inspector module as a live debug UI in the running
app. Mirrors how Network Inspector is wired today.

  • `DiagnosticsAppModule` accepts a `BusInspector` alongside the
    existing `NetworkInspector`. Override exposes
    `busInspectorProvider`; new `GoRoute` at `/diagnostics/bus` builds
    `BusInspectorScreen`. Inspector is disposed when the module
    disposes.
  • `AgentRuntimeManager` accepts and forwards optional `busObserver`
    and `eventObserver` parameters to every `AgentRuntime` it
    materialises, so all per-thread bus commits and raw AG-UI events
    flow into the inspector regardless of which server hosts the
    thread.
  • `standard()` constructs `BusInspector`, wires
    `busInspector.record` as the runtime manager's `busObserver`,
    `busInspector.recordEvent` as the `eventObserver`, and passes the
    inspector to `DiagnosticsAppModule`.
  • `ThreadSidebar` and `ServerSidebar` render a "Bus Inspector"
    `TextButton.icon` directly below the existing "Network Inspector"
    entry. `RoomScreen` and `LobbyScreen` push `/diagnostics/bus` via
    `go_router` from the new callback.
  • Existing widget tests updated to pass an `onBusInspector` no-op
    so the new required parameter does not break them.

Testing

`flutter analyze` clean, `flutter test` 1206/1206 pass on the full
stack. Manual smoke tested on macOS: open a room, click "Bus
Inspector", filter by `tag:agui.activitysnapshot` etc., verify the
unified timeline shows both bus commits and AG-UI events with
correct tags and per-thread color stripes.

Stack

  1. feat(client): StateBus observer + tag API #203 — StateBus observer + tag API
  2. feat(agent): bus + event observers on AgentRuntime #204 — bus + event observers on AgentRuntime
  3. feat(diagnostics): Bus Inspector module — recorder, filter, screen #205 — Bus Inspector module
  4. This PR — wire into shell

runyaga added 4 commits May 1, 2026 14:10
Adds an additive `addObserver` hook on `StateBus` that fires after
every successful commit, plus an optional `tag` argument on
`setAgentState` and `update` so writers can label the source of each
write. Observers receive `(tag, snapshot)`; the snapshot is the
frozen post-commit map already exposed via `agentState`.

`addObserver` returns a disposer for symmetric tear-down. Detaching
during dispatch is safe (siblings still fire). Adding to a disposed
bus is a no-op.

Pure additive: existing call sites continue to work without
modification, and the new `BusObserver` typedef is exported via the
existing `application` barrel.

11 tests cover the observer dispatch ordering, dispose semantics,
self-detaching observers, tag preservation, and disposed-bus
safety.

Foundation for an upcoming Bus Inspector debug surface that needs
to record every state commit on a per-thread basis without forcing
a bus dependency on the agent layer's diagnostics.
Adds two optional observer hooks at the runtime layer for
diagnostics consumers (e.g. an in-app Bus Inspector) without
coupling the agent to any specific consumer:

- `ThreadBusObserver` — receives every per-thread `StateBus`
  commit with the originating `ThreadKey`. Wired into the bus the
  first time the runtime materialises a `ThreadState`. Supports
  the `tag:` argument added in the StateBus observer PR so seed
  paths and other writers label their commits.
- `ThreadEventObserver` — receives every raw AG-UI `BaseEvent`
  paired with the `ThreadKey` it was processed on. Forwarded from
  `AgentSession._bridgeBaseEvent` via a package-private
  `notifyThreadEvent` helper. The runtime makes no behavioural
  decision on the event; it only fans out.

Tags `seedThreadState` and `seedThreadHistory` writes as
`seed.initial` and `seed.history` respectively so consumers can
distinguish initial state restoration from in-session updates.

Bus writes inside `AgentSession._onStateChange` remain untagged at
this layer — diagnostics consumers correlate them with the most
recent AG-UI event observed via `eventObserver` to infer whether
the commit came from a `StateSnapshotEvent`, `StateDeltaEvent`,
or a non-state run-state transition. This keeps the agent
diagnostics-agnostic.

Test exercises the bus-observer wiring end-to-end via the seed
APIs and verifies the per-thread context (key + tag + snapshot)
is delivered to the observer.
Self-contained diagnostics module under
`lib/src/modules/diagnostics/` providing an in-app inspector for
the per-thread `StateBus` plus the AG-UI event stream that drives
it. All new files; no existing call sites change.

Components:

- `BusInspector` — `ChangeNotifier` recorder with two bounded
  queues (commits + raw events). Sinks shaped to match
  `ThreadBusObserver` (`record`) and `ThreadEventObserver`
  (`recordEvent`) so the host flavor wires it through
  `AgentRuntime` directly.
- Tag inference — `recordEvent` tracks the most recent
  `StateSnapshotEvent` / `StateDeltaEvent` per thread; when an
  untagged bus commit arrives, the inspector labels it
  `agui.snapshot`, `agui.delta`, or `agui.run-state` accordingly.
  Explicit tags (e.g. `seed.initial` / `seed.history` from
  `AgentRuntime`) pass through verbatim. Event records derive
  their own tag from the runtime type
  (`ActivitySnapshotEvent` → `agui.activitysnapshot`).
- `snapshot_diff.dart` — pure-Dart structural differ. Compares
  two `Map<String, dynamic>` snapshots and emits
  added/removed/replaced changes with slash-joined paths. Walks
  added/removed subtrees recursively so a fresh nested branch
  surfaces as one leaf path per value, not a single coarse
  `/parent` change.
- `bus_filter.dart` — small filter mini-language with prefixes
  `thread:`, `room:`, `server:`, `tag:`, `path:`, and
  `kind:bus|event`, plus bare-text fallback that matches against
  any of the above (and changed paths for bus rows). `tag:foo*`
  is a prefix glob. `suggestionsFor(...)` powers the autocomplete
  chip strip in the screen, drawing values from the live event
  buffer.
- `BusInspectorScreen` — master-detail UI. Sidebar lists "All
  events" plus one row per active thread (color dot, room id,
  short thread id, last-event time, count). Right pane shows a
  filter `TextField` with autocomplete chips and active-filter
  pills (each removable via X), then a unified timestamp-ordered
  timeline mixing bus rows and event rows distinguished by a
  `bus`/`event` kind badge. Bus row detail renders the diff plus
  a collapsible full snapshot via the existing `JsonTreeView`;
  event row detail renders the event's `toJson()` payload via the
  same view. `TextField.onTapOutside` is a no-op so the field
  keeps focus while users tap suggestion chips — without this the
  rebuild triggered by the focus change unmounts the chip before
  its `onPressed` can fire.

Tests: ~50 unit tests across the differ, filter parser/matcher,
suggestion generator, and `BusInspector` recording semantics.
Surfaces the diagnostics module as a live debug UI:

- `DiagnosticsAppModule` accepts a `BusInspector` alongside the
  existing `NetworkInspector`. Override exposes
  `busInspectorProvider`; new `GoRoute` at `/diagnostics/bus`
  builds `BusInspectorScreen`. Inspector is disposed when the
  module disposes.
- `AgentRuntimeManager` accepts and forwards optional
  `busObserver` and `eventObserver` parameters to every
  `AgentRuntime` it materialises, so all per-thread bus commits
  and raw AG-UI events flow into the inspector regardless of
  which server hosts the thread.
- `standard()` constructs `BusInspector`, wires
  `busInspector.record` as the runtime manager's `busObserver`,
  `busInspector.recordEvent` as the `eventObserver`, and passes
  the inspector to `DiagnosticsAppModule`.
- `ThreadSidebar` and `ServerSidebar` render a "Bus Inspector"
  `TextButton.icon` directly below the existing "Network
  Inspector" entry. `RoomScreen` and `LobbyScreen` push
  `/diagnostics/bus` via `go_router` from the new callback.
- Existing widget tests updated to pass an `onBusInspector`
  no-op so the new required parameter does not break them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant