mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
## 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>
49 lines
2.9 KiB
Text
49 lines
2.9 KiB
Text
---
|
|
title: "Steering"
|
|
description: "Guide running agents with real-time course corrections"
|
|
---
|
|
|
|
Steering lets you send guidance to an agent while it's working — without waiting for a human gate or stopping the run. If you see the agent heading down the wrong path, you can nudge it back on track mid-stage.
|
|
|
|
## How steering works
|
|
|
|
A steering message is injected into the agent's conversation as a user-role message. The agent sees it on its next LLM turn and can adjust its approach immediately.
|
|
|
|
The delivery flow:
|
|
|
|
1. You send a steering request with your guidance text
|
|
2. The message is queued on the agent session's steering queue
|
|
3. Before the next LLM call, Fabro drains the queue and injects each message as a `Steering` turn in the conversation history
|
|
4. The LLM sees the guidance alongside its existing context and adjusts accordingly
|
|
|
|
Steering is **asynchronous** — the agent picks up the message at its next natural pause point.
|
|
|
|
When you send steering with `interrupt=true`, Fabro first cancels the active API-mode agent round, then queues the steering text in the same worker-control operation. The agent resumes with that steering text as the next user turn. A standalone interrupt through the API cancels the active round without text and keeps the session waiting until a later steer arrives.
|
|
|
|
## When steering is delivered
|
|
|
|
Steering messages are drained from the queue at two points during the agent loop:
|
|
|
|
1. **Before the first LLM call** — any messages queued before the agent starts its first turn
|
|
2. **After each interrupted or completed round** — before the next LLM call
|
|
|
|
This means there is a natural latency between sending a plain steering message and the agent seeing it. If the agent is in the middle of a long-running shell command, the message waits until that command finishes and the next LLM turn begins. Use interrupting steering when the current round should stop before the message is delivered.
|
|
|
|
Multiple steering messages sent in quick succession are all delivered together at the next drain point.
|
|
|
|
## Steering vs. human gates
|
|
|
|
Steering and [human gates](/workflows/human-in-the-loop) serve different purposes:
|
|
|
|
| | Steering | Human gates |
|
|
|---|---|---|
|
|
| **When** | Any time during an agent stage | At a defined point in the workflow graph |
|
|
| **Blocks execution** | No — agent continues working | Yes — workflow pauses until a choice is made |
|
|
| **Defined in the graph** | No — sent ad hoc via the API | Yes — `hexagon` nodes with edge options |
|
|
| **Use case** | Course corrections, hints, focus changes | Approval decisions, strategy selection, go/no-go |
|
|
|
|
Use human gates for structured decisions that are part of the workflow design. Use steering for reactive guidance when you're watching a run and want to intervene.
|
|
|
|
<Frame caption="The Files Changed tab shows a side-by-side diff of all changes made during the run.">
|
|
<img src="/images/web/run-files-changed.png" alt="Fabro web UI Files Changed tab showing a side-by-side diff" />
|
|
</Frame>
|