fabro/apps/fabro-web/app/lib
fabro-sh-0530[bot] 79f89165f6
Wire end-to-end steering for running agents (#209)
## Summary
This makes the advertised mid-run steering path real: users can send
append or interrupt steering messages through the API, CLI, and web UI,
and the worker delivers them to live API-mode agent sessions or buffers
them for the next session. The change adds the control protocol, session
interrupt machinery, workflow hub, server route/OpenAPI/client updates,
and UI feedback needed for the whole path.

### Plan Summary
- Add `SteerKind`/`run.steer` wire protocol and `POST /runs/{id}/steer`
- Deliver steers through subprocess JSONL or the in-process
`SteeringHub`
- Support append and interrupt behavior in agent sessions, with bounded
buffering and events
- Expose steering in the CLI/web UI and surface SSE toasts

## Flow

```mermaid
flowchart TB
  UI["CLI / Web UI"] --> API["POST /runs/{id}/steer"]
  API -->|"subprocess transport"| Control["Worker control JSONL"]
  API -->|"in-process transport"| Hub["SteeringHub"]
  Control --> Hub
  Hub -->|"active API sessions"| Session["SessionControlHandle"]
  Hub -->|"no active session"| Pending["Pending buffer"]
  Pending -->|"first future API session"| Session
  Session --> Agent["Session round loop"]
  Agent --> Events["RunEvent stream"]
  Events --> UI
```

## What changed and why

- Agent sessions now expose a lightweight `SessionControlHandle`, drain
steering at the top of each round, and use a replaceable round
cancellation token for interrupts. LLM waits are cancelled promptly,
while tool execution observes cancellation cooperatively so every
committed `tool_use` still gets a matching `tool_result`.
- `SteeringHub` owns active API session registration, broadcast
delivery, pending buffering, FIFO queue caps, and steering
lifecycle/drop events. A completion coordinator closes the
final-response race without introducing a workflow dependency into the
agent crate.
- The server route replaces the 501 stub, validates run state and
best-effort CLI-only steerability, and forwards through either
subprocess control JSONL or the in-process hub. OpenAPI and generated
clients now include the request type.
- The CLI and web UI can send append or interrupt steers. Run detail and
board views open the new composer, and shared SSE subscriptions now
support per-subscriber event callbacks so invalidation and steering
toasts can coexist on one EventSource.

## Review notes

- Steering actors stay on top-level `RunEvent.actor`; event props only
carry steering kind/drop metadata.
- Buffered steers replay as append messages to the first API session
that registers after an empty-active period. Per-stage targeting remains
out of scope.
- CLI-mode agent stages are still not steerable; the server returns a
best-effort 409 when all active agent stages are CLI-mode, while the
worker hub remains the authoritative safety net.
- No persistence or schema migration is required; active and pending
steering state is in memory.
- New tests focus on protocol round-trips, hub buffering/bounds, session
steering-loop behavior, SSE fanout, and basic server rejection paths.

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:34:16 -04:00
..
api-client.test.ts refactor(web): move server state to SWR 2026-04-25 07:16:41 -04:00
api-client.ts Render stage activity from scoped events endpoint (#212) 2026-05-05 09:16:52 -04:00
board-events.test.tsx fix(web): recover cross-tab SSE coordination after fallback 2026-05-04 15:52:18 -04:00
board-events.ts fix(web): recover cross-tab SSE coordination after fallback 2026-05-04 15:52:18 -04:00
cross-tab-sse.test.ts fix(web): recover cross-tab SSE coordination after fallback 2026-05-04 15:52:18 -04:00
cross-tab-sse.ts fix(web): recover cross-tab SSE coordination after fallback 2026-05-04 15:52:18 -04:00
demo-mode.test.tsx feat: wire web UI to real server with demo mode toggle 2026-04-08 04:46:08 -04:00
demo-mode.tsx feat: wire web UI to real server with demo mode toggle 2026-04-08 04:46:08 -04:00
format.ts refactor(web): simplify run logs view 2026-04-26 17:47:10 -04:00
graph-theme.ts refactor(web): drop light mode, go dark-only 2026-04-20 09:27:08 -04:00
mutations.ts Wire end-to-end steering for running agents (#209) 2026-05-05 15:34:16 -04:00
queries.ts Render stage activity from scoped events endpoint (#212) 2026-05-05 09:16:52 -04:00
query-keys.test.ts Read billing and stages from RunProjection with live runtimes (#213) 2026-05-05 09:32:33 -04:00
query-keys.ts Render stage activity from scoped events endpoint (#212) 2026-05-05 09:16:52 -04:00
run-actions.test.ts feat(web): add interview dock for answering blocked runs from the UI 2026-05-02 15:39:01 -04:00
run-actions.ts feat(web): add interview dock for answering blocked runs from the UI 2026-05-02 15:39:01 -04:00
run-events.test.tsx Wire end-to-end steering for running agents (#209) 2026-05-05 15:34:16 -04:00
run-events.ts Wire end-to-end steering for running agents (#209) 2026-05-05 15:34:16 -04:00
run-settings-snapshot.ts refactor(web): narrow event property reads via shared helpers 2026-04-30 22:45:38 -04:00
sse.ts Wire end-to-end steering for running agents (#209) 2026-05-05 15:34:16 -04:00
stage-sidebar.test.ts Encode stage visits in run stage URLs (#206) 2026-05-05 08:27:11 -04:00
stage-sidebar.ts Read billing and stages from RunProjection with live runtimes (#213) 2026-05-05 09:32:33 -04:00
time.ts Read billing and stages from RunProjection with live runtimes (#213) 2026-05-05 09:32:33 -04:00
unknown.ts refactor(web): narrow event property reads via shared helpers 2026-04-30 22:45:38 -04:00
workflow-api.ts refactor(web): regenerate client for precise stage states 2026-04-30 06:33:12 -04:00