You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.gom.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:
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.
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).
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."
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.
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.gom.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
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:
#228built 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 wholeMessagesslice. Injecting a steer is just appending aMessage{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:User input reaches the loop only as the
userText/partsargument atRunContententry — i.e., once per run start. There is:Engine.Run;dispatchand the nextrunTurn;Converse,Cancel,Approve*, team verbs only — noSteer/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 survivesValidateToolPairing/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:
Engine: a run-scoped steer inbox. Add a channel/queue on
Run(e.g.Depsor aRunmethodEnqueueSteer(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 asinjectBackgroundNotice). The model then sees it on the very next replay, mid-run.Wire: a
Steer/SendInputRPC (or extendConverse's client stream with a mid-run input frame) → routes to the liveRun's inbox. Must be no-op-safe if the run already ended (race with terminal).Service layer:
StartRunContent-adjacent path to deliver a steer to aIsLivesession without reopening/re-leasing; distinguish "run is live → steer" from "run is idle → new prompt."TUI: flip the queue semantics from terminal-drain to live-inject:
enterwhile running sends aSteerinstead 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.)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 throughBeginTurnsoMaxTurnsstill bounds it).Open questions for discussion
dispatchbetween tool calls (more responsive, riskier)?awaitingstate?SteerRPC; and the HTTP/SSE surface equivalent.Labels
tui+enhancementseem right (the user-visible behavior is TUI-first), though the core change is engine.