Unified autonomous steering — Deep Dive
Unified autonomous steering replaces hidden gate-specific correction paths with one coordinator-owned routing mechanism. Human review, RAI, Rubberduck, Build & Test, agent-originated guidance, coordinator-originated guidance, and workflow-step feedback all normalize to a SteeringSignal. The coordinator then records a conscious decision before any action executes.
For event payloads and routes, see the reference. For the operator workflow, see the experience guide.
Mental model

The key invariant is visibility before effect. CoordinatorSteeringService.SubmitSteeringAsync persists and queues the signal, emits coordinator.steering_received, and does not execute recovery or reset any subtask (apps/Agentweaver.Api/Coordinator/CoordinatorSteeringService.cs:484). CoordinatorSteeringDecider.DecideAsync then records the action and emits coordinator.steering_decision before in-place steering or fresh dispatch runs (apps/Agentweaver.Api/Coordinator/CoordinatorSteeringDecider.cs:105, :201).
There is no feature flag. Unified steering is the behavior in the assembly path.
One signal shape
SteeringSignal is the normalized envelope for all correction feedback (apps/Agentweaver.Api/Coordinator/SteeringSignal.cs:8). It carries:
source:human-review,rai,rubberduck,build-test,agent,coordinator, orstep;severity:advisory,request-changes, orblocking;- target scope: run, work plan, subtask ids, and optional child run id;
- feedback text;
- optional tree hash and explicit file hints.
Feedback text is reasoning context, not a routing heuristic. The old behavior that parsed prose and automatically reset subtasks is no longer the gate path.
Coordinator choices
The coordinator chooses among four directions (SteeringSignal.cs:129):
| Direction | User-facing effect |
|---|---|
in_place_steer | Resume the existing child run as a revision turn, preserving session, worktree, and context. |
dispatch_fresh | Reset selected subtasks and launch fresh child runs. This is explicit, logged, and visible. |
proceed | Proceed toward review or a terminal/blocked state. |
advisory | Surface the signal and take no action. |
The shipped deterministic fallback policy prefers advisory no-op for advisory signals, proceeds when budgets are exhausted or feedback is blocking/stale, steers in place when the target is resumable, and dispatches fresh only when request-changes feedback targets a non-resumable path (apps/Agentweaver.Api/Coordinator/CoordinatorSteeringDecider.cs:31).
Resumability probe (pod-per-run vs in-api)
Whether a target is resumable is decided by an ISteeringResumabilityProbe. The in-api default (DefaultResumabilityProbe) reports a target resumable from durable DB state alone — at least one target subtask still references a ChildRunId inside its retention window (apps/Agentweaver.Api/Coordinator/CoordinatorSteeringDecider.cs:678). In pod-per-run mode that heuristic lies: when a subtask reaches a successful terminal (assemble_ready / completed) its AgentHost pod is released (SandboxClaim deleted on suspend), yet ChildRunId persists. The assembly gate hands its decision to PodPerRunResumabilityProbe, which reuses the default predicate but excludes those pod-released terminal subtasks when running pod-per-run, so the reaped session is never chosen for in-place resume (apps/Agentweaver.Api/Coordinator/CoordinatorSteeringDecider.cs:706, wired at CoordinatorAssemblyService.cs:1920). The exclusion is gated on the actual pod-release condition — IsPodPerRun && ReleasePodOnSuspend (matching RunWatchLoopService.ReleasePodOnSuspendSafeAsync); under the supported non-default warm-pool config (ReleasePodOnSuspend=false) the pod stays alive, so the probe keeps the in-place resume path and matches DefaultResumabilityProbe. Without this, resuming a released pod 404s instantly, and at the assembly gate — where every child is assemble_ready, so every pod is gone — the failures cascade into a full-plan replan loop that discards all completed work (issue #220).
Behaviour change: in pod-per-run + single-agent squads the first assembly-gate rejection now routes straight to human review (there is no other eligible rotation author) instead of running N wasteful replan cycles. This is intended.
Assembly gate integration
Assembly gate request-changes no longer calls a separate assembly-gate reset route. RouteAssemblyGateThroughSteeringAsync stamps assembly_steering, submits the signal, claims it for the inline decider, and then executes the chosen direction (apps/Agentweaver.Api/Coordinator/CoordinatorAssemblyService.cs:1680).
- A: in-place steer.
ExecuteInPlaceSteerAsyncresumes the existing child run withRunOrchestrator.StartRevisionAsync, keeps theChildRunId, and returns the plan to dispatching so the resumed child can finish (CoordinatorAssemblyService.cs:1810). - B: dispatch fresh. Only after a
dispatch_freshdecision event, the coordinator calls the reset/re-dispatch path (CoordinatorAssemblyService.cs:1751). - C: proceed. Budget exhaustion records
assembly_blockedwith reasonsteering_budget_exhausted(CoordinatorAssemblyService.cs:1770). - D: advisory. The assembly stage is restored and the gate loop continues (
CoordinatorAssemblyService.cs:1795).
The assembly decision lease is recoverable. AssemblySteering routes back to assembly recovery rather than dispatch, and the reconciler scans it as an assembly state (apps/Agentweaver.Api/Coordinator/CoordinatorRecoveryRouter.cs:55, apps/Agentweaver.Api/Coordinator/CoordinatorReconciler.cs:128).
Failure recovery & reliability
In-place steering is now recovery-aware all the way through the assembly-gate revision path. When an assembly-gate revision commit succeeds, the coordinator resumes the same child run and worktree, returns the plan to dispatching, and re-arms assembly after that child reaches assemble_ready (apps/Agentweaver.Api/Coordinator/CoordinatorAssemblyService.cs:1935, :1966). This is the context-preserving path: no fresh pod is introduced unless the coordinator later makes a visible dispatch_fresh decision.
The revision executor retries transient post-turn git commit failures three times with short backoff, covering stale index.lock and similar worktree-state races from lingering child processes (packages/Agentweaver.AgentRuntime/Workflow/AgentTurnExecutor.cs:142, :203). If all attempts fail, it emits a failed workflow.step and rethrows instead of pretending the revision produced no changes (AgentTurnExecutor.cs:160). The child watch loop then terminalizes child executor failures as run.failed with reason child_executor_failed:{executor} and records the executor's failed step (apps/Agentweaver.Api/Runs/RunWatchLoopService.cs:296, :313). That replaces the old stream-end fallback watch_stream_completed_without_terminal_event and gives the coordinator an authoritative failed subtask to react to.
A failed in-place revision does not silently wedge assembly or drop feedback. DriveOutstandingSteeringExecutionAsync detects failed target subtasks and emits a visible coordinator.steering_decision before consciously falling back to dispatch_fresh for the failed targets only (CoordinatorAssemblyService.cs:2170, :2218). The original steering instruction is preserved and re-run on fresh work.
Crash-window hardening is strict: an in-place directive is marked applied only after every target subtask is assembly-eligible (assemble_ready or completed) and every target child has a confirmed SteeringRevisionExecution effect marker for that directive attempt (CoordinatorAssemblyService.cs:2233, apps/Agentweaver.Api/Coordinator/CoordinatorSteeringDecider.cs:403). A crash before the revision launches leaves no confirmed marker, so recovery re-drives the missing child instead of silently dropping the feedback.
Loop bounds and idempotency
Steering cannot loop forever:
- per-subtask recovery attempts are capped at
CoordinatorSteeringService.MaxRecoveryAttempts = 3(CoordinatorSteeringService.cs:811); - per-plan steering iterations default to
6(CoordinatorSteeringDecider.cs:81).
The decider increments the budget exactly once for in-place or fresh-dispatch actions and degrades to proceed when the cap is reached (CoordinatorSteeringDecider.cs:164). In-place steering uses attempt-specific revision effect records, so recovery can prove whether the revision actually ran before re-driving or marking the directive applied (CoordinatorSteeringDecider.cs:219).
Source
| Concern | File |
|---|---|
| Steering signal schema and directions | apps/Agentweaver.Api/Coordinator/SteeringSignal.cs |
| Unified steering submission and received event | apps/Agentweaver.Api/Coordinator/CoordinatorSteeringService.cs |
| Coordinator decision policy, budgets, decision event | apps/Agentweaver.Api/Coordinator/CoordinatorSteeringDecider.cs |
| Assembly gate steering-only routing | apps/Agentweaver.Api/Coordinator/CoordinatorAssemblyService.cs |
Recovery routing for assembly_steering | apps/Agentweaver.Api/Coordinator/CoordinatorRecoveryRouter.cs, CoordinatorReconciler.cs |
| Event type constants | packages/Agentweaver.Domain/EventTypes.cs |
| Timeline rendering | apps/web/src/components/LifecycleEventCard.tsx |
See also
- Unified autonomous steering — Reference
- Unified autonomous steering — User Guide
- Resilient assembly-review loop — Deep Dive — how budget exhaustion escalates to human review, context-preserving revisions, and the reviewer-rejection lockout built on top of unified steering.
- Coordinator internals
- Events & observability
