Why mid-turn steering is not possible yet
There is a clean checkpoint between the tool node and the next model call. You still cannot put a person's words through it, because of what the message list looks like at exactly that moment. The investigation, so nobody has to repeat it. Release notes 2026-09-15 — vocion-core#352.

Status: shipped, and the finding is negative. vocion-core #352 (bfb5b3b1) shipped a queue on the chat composer — the user-facing half is here. It did not ship mid-turn steering, and this post is the write-up of why, committed in the repo as §12 of docs/agent-chat-surface.md. Nothing described under "the smallest unlock" below is built.
The ask was reasonable: let a person type “and skip the ones already closed” into a running turn and have the agent take it into account on its next step, rather than after the whole turn finishes. Claude Code feels like it does this. It is worth knowing exactly where the wall is.
1. There is a natural checkpoint, and it is cheap
LangChain v1 middleware exposes beforeModel and wrapModelCall, which run between the tools node and the next model call, and createDeepAgent takes a middleware option. A hook there could drain a per-run mailbox before each model step. No checkpointer, no LangGraph state surgery, no protocol change on the stream.
That part is a day's work. (Verified against the installed dependencies: langchain@1.4.0 defines BeforeModelHook and WrapModelCallHook; deepagents@1.10.1's CreateDeepAgentParams accepts middleware.)
2. The blocker is the shape of the message list at that exact moment
At that checkpoint the messages end:
[ …, AIMessage(tool_calls), ToolMessage ]
A ToolMessage goes on the wire as a user turn whose content is tool_result blocks. So appending the interjection as a HumanMessage puts two consecutive user turns on the wire.
The provider SDK does not rescue you. @langchain/anthropic's mergeMessages — dist/utils/message_inputs.js, installed version 1.3.29 — merges two adjacent messages only when both are tool-result messages: role === 'user' and every content item is type === 'tool_result'. A plain-text HumanMessage fails that test, so it is not merged; it is sent as-is, and the Messages API requires alternating roles.
There is exactly one way to land the words in context without a redesign: append them to the last tool result. That makes the persisted audit trail say that a tool returned something a person typed. For a product whose whole proposition is that you can read what the agent did and why, that is not a trade worth making for a convenience feature.
3. It would only ever work on one harness target
Vocion runs an agent turn on one of four targets (runsOn in models/Schema.ts):
| Target | How a turn is invoked | Inbound channel mid-turn |
|---|---|---|
in-process | we own the loop | yes, in principle |
agentcore-container | one-shot POST /invocations into the container | none |
aws-managed-harness | an AWS SDK harness invocation | none |
external-worker | a queued worker_run, answered with a receipt | none |
Only in-process is a loop we are inside. For the other three, steering is a protocol change on both sides of a boundary we do not own. A feature that works on one of four deployment shapes and silently does nothing on the other three is worse than a feature that works nowhere, because you cannot tell which one you are on from the chat box.
The smallest unlock
It is (2), not (1). A steeringMiddleware whose wrapModelCall rewrites request.messages so the interjection rides as an extra text block on the tool-result user turn — which keeps role alternation legal — plus a product decision about whether the persisted tool output carries it, and how the transcript shows that it was a person and not the tool.
The mailbox itself is easy. Key it by the streamId the stream route already mints (app/[locale]/rpc/agent/stream/route.ts calls crypto.randomUUID(); libs/streams/buffer.ts is the same in-process, single-container scope), and add a POST /rpc/agent/steer to post into it.
None of that is shipped. A half-wired mailbox that nothing drains is dead code, and a transcript that pretends a tool said something a person typed is worse than a queue. So the queue is what shipped.
Where this lives in the repo
vocion-core#352, on release/2026-09-15-rail (bfb5b3b1).
- The write-up:
docs/agent-chat-surface.md§12 - Harness targets:
packages/core/src/models/Schema.ts(runsOn), dispatched inservices/AgentService.ts; the container contract ispackages/agent-runtime/src/server.ts - Stream ids and buffer:
packages/core/src/app/[locale]/rpc/agent/stream/route.ts,packages/core/src/libs/streams/buffer.ts - What did ship instead:
features/dashboard/chat/queueReducer.ts,useSendQueue.ts,ChatComposer.tsx
Two honesty notes on the argument itself. The mergeMessages behaviour above was read in the installed package and is exact; the claim that the provider then rejects the request is the documented alternating-role requirement of the Messages API, not something that file asserts. And the table's grouping of all three remote targets under "one-shot invocation" is an architectural simplification — only agentcore-container is literally an HTTP POST /invocations. The conclusion — no inbound channel mid-turn — holds for all three.
Related: The composer never locks and What streams through the conversation surface.