Skip to content

feat(diagnostics): Bus Inspector module — recorder, filter, screen#205

Open
runyaga wants to merge 3 commits into
soliplex:mainfrom
runyaga:feat/bus-inspector-diagnostics
Open

feat(diagnostics): Bus Inspector module — recorder, filter, screen#205
runyaga wants to merge 3 commits into
soliplex:mainfrom
runyaga:feat/bus-inspector-diagnostics

Conversation

@runyaga
Copy link
Copy Markdown
Contributor

@runyaga runyaga commented May 1, 2026

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

What this changes

Self-contained diagnostics module under
`lib/src/modules/diagnostics/` providing an in-app inspector for the
per-thread `StateBus` and the AG-UI event stream that drives it.
All new files; no existing call sites change. Wiring into the
shell happens in #205.

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 pass through verbatim. Event records derive their own
    tag from the runtime type (`ActivitySnapshotEvent` →
    `agui.activitysnapshot`).
  • `snapshot_diff.dart` — pure-Dart structural differ. 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. `tag:foo*` is a prefix
    glob. `suggestionsFor(...)` powers autocomplete chips.
  • `BusInspectorScreen` — master-detail UI. Sidebar lists "All
    events" plus one row per active thread. 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. Event row detail renders the event's `toJson()` payload
    via `JsonTreeView`.

One Flutter quirk worth flagging

`TextField.onTapOutside: (_) {}` is a no-op so the field keeps focus
while users tap suggestion chips. Without this, the rebuild
triggered by Flutter's default unfocus-on-outside-tap unmounted the
chip before its `onPressed` could fire — chips were unselectable.

Testing

~50 unit tests across the differ, filter parser/matcher, suggestion
generator, and `BusInspector` recording semantics. Widget tests for
the screen are not included in this PR; manual smoke testing on
macOS confirmed the workflow end-to-end.

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. This PR — Bus Inspector module
  4. `feat/wire-bus-inspector` — wire into shell

runyaga added 3 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.
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