Skip to content

Steer-while-running: inject queued user input into an in-flight turn #512

Description

@jbeda

Summary

In Claude Code, text typed while the agent is running is folded into the running turn: when the model's current response finishes and its tool calls execute, the queued message is injected as a user message between the tool results and the next model replay — so the agent steers mid-task without a separate "turn."

mecatl does not do this. mecati's queue (/tmp/..., cmd/mecatui/ui/update.go m.queued, enqueuePrompt/drainQueue) is entirely client-side and turn-terminal: a typed line is staged, and only when the current run ends (healthy stop) is the merged queue submitted as a brand-new follow-up run. A long multi-tool run can't be nudged mid-flight.

This issue is to discuss whether we want steer-while-running, and what it would take.

What other harnesses do

Harness Behavior
Claude Code Queued input is drained at every turn boundary, including between tool calls within one run. The injected text becomes a user message in the in-flight conversation; the next model replay sees it. This is the reference behavior.
Codex CLI Has "steer" — you can send input while a turn runs; it's surfaced to the model at the next opportunity (between steps), not queued to the very end.
OpenCode / Aider Mostly turn-terminal: input during a run is queued and sent as the next prompt after the run settles (≈ mecatl today). Some variants allow interrupt-then-inject.
Cursor / Cline / Goose Mix of interrupt-and-restart and boundary-injection; not uniform.

The distinguishing axis: boundary-injection (Claude Code, Codex) vs terminal-queue (mecatl, OpenCode). Boundary-injection is the more capable UX for long agentic runs.

The relevant prior art within mecatl: #228 built the client-side queue UX (merge-always, edit-back, transient auto-resume) — but deliberately scoped to turn-end drain, not mid-turn injection.

Key technical fact

No provider API support is required. mecatl's LLM adapters are stateless (store:false, full replay each turn — AGENTS.md). Anthropic Messages, OpenAI Responses, and Chat Completions all work the same way: each call replays the whole Messages slice. Injecting a steer is just appending a Message{Role: user} to the history before the next replay — the provider cannot tell an injected message from an original one. So this is 100% a harness/loop concern, portable across every provider mecatl supports.

Why mecatl can't do it today (the structural gap)

Engine.runLoop (engine/agent/loop.go:1057) is turn-sequential with a fixed shape:

for {
    Step 2a: inject background-completion notices / drain delivery queue   ← the ONLY injection seam
    Step 2:  stop conditions (limits / cancel / token budget)
    BeginTurn → runTurn (model stream, ONE call)
    RecordAssistant
    if no tool calls → finishTurnNoTools (answer / nudge / stop)
    else → dispatch(toolCalls) → RecordToolResults → save → loop
}

User input reaches the loop only as the userText/parts argument at RunContent entry — i.e., once per run start. There is:

  • no channel carrying new user text into a running Engine.Run;
  • no injection seam for user-authored content between the dispatch and the next runTurn;
  • no proto RPC to submit one (Converse, Cancel, Approve*, team verbs only — no Steer/SendInput).

The two Step 2a seams (injectBackgroundNotice, drainPendingDelivery) prove the pattern is safe — they record an ordinary harness-framed user continuation at a point where history ends on a user-prompt/tool-result/nudge, so it replays cleanly and survives ValidateToolPairing/compaction. A steer seam would reuse exactly this, but sourced from live user input rather than internal bookkeeping.

What would need to change

Rough sketch, cheapest-to-hardest:

  1. Engine: a run-scoped steer inbox. Add a channel/queue on Run (e.g. Deps or a Run method EnqueueSteer(text string)) that the loop drains at a new Step 2a sibling — recording the steer as an ordinary harness-framed user continuation (same provider-legal seam as injectBackgroundNotice). The model then sees it on the very next replay, mid-run.

  2. Wire: a Steer/SendInput RPC (or extend Converse's client stream with a mid-run input frame) → routes to the live Run's inbox. Must be no-op-safe if the run already ended (race with terminal).

  3. Service layer: StartRunContent-adjacent path to deliver a steer to a IsLive session without reopening/re-leasing; distinguish "run is live → steer" from "run is idle → new prompt."

  4. TUI: flip the queue semantics from terminal-drain to live-inject: enter while running sends a Steer instead of staging; keep the existing queue as the fallback for when no run is live. (mecatui: queued follow-ups — merge into one prompt, allow edit-back, and auto-resume on transient errors #228's merge/edit-back UX mostly carries over.)

  5. Invariants to preserve: read-parallel/mutate-serial dispatch; ValidateToolPairing (steer must land where history is well-formed); the recorded==streamed==model-view rule; compaction pairing; and the budget/turn limits (a steer turn still goes through BeginTurn so MaxTurns still bounds it).

Open questions for discussion

  • One steer seam at the model boundary (simplest), or also mid-dispatch between tool calls (more responsive, riskier)?
  • Does a steer interrupt the current streamed response (cancel + replay with the injected msg), or only take effect after the current response+tools settle? Claude Code does the latter; the former is snappier but breaks no-replay-after-first-chunk.
  • How does steer interact with plan mode and the pending-approval awaiting state?
  • Ordering with the existing Step 2a seams (background notices / delivery drain) — precedence when both fire on one boundary?
  • gRPC bidi vs a unary Steer RPC; and the HTTP/SSE surface equivalent.

Labels

tui + enhancement seem right (the user-visible behavior is TUI-first), though the core change is engine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requesttuimecatui terminal UI (rendering, keybindings, footer, panes, scrollback)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions