Release
Steer the agent mid-run — no abort, no re-prompt
You're watching the agent head down the wrong path. You used to have one option: Ctrl-C, lose the run, and re-prompt from scratch. Now you just keep typing — your message queues while the agent generates and is injected at the next turn boundary.
Mid-run steering lets you redirect the agent while it is generating, without aborting. It ships in two layers — the interactive REPL, and the headless --input-format stream-json path for SDK and automation use.
In the REPL: just keep typing
While the agent is working, the prompt stays live. Type a correction and press ⏎ — instead of starting a new run, it queues. The queued message shows as a dimmed ⋯ <text> · esc to editline just above the input. It's not in the agent's context yet — it's waiting for a safe moment.
When the agent reaches the next turn boundary, the queued message is injected and commits to the scrollback as a normal ❯user message — exactly as if you'd sent it there. Two more behaviors make it feel natural:
- Esc-to-edit. Pressing
escon an empty prompt peels the most recently queued message back into the input so you can fix it, and re-syncs the queue — rather than interrupting the run. A non-empty draft is never clobbered: there,escstill interrupts. - Nothing lost silently.If a run ends (aborted, errored, or hit max-turns) while messages are still queued, they surface as a faint "not delivered — resend if still needed" note and are cleared.
Headless: stream-json steering
The same capability is available to automation. With --input-format stream-json, harnext keeps stdin open and reads newline-delimited JSON user messages incrementally, instead of draining stdin into one prompt.
harnext -p --input-format stream-json --output-format stream-json// stdin — the first line starts the run:
{"type":"user","message":{"role":"user","content":"Refactor the auth module to use async/await"}}
// …while it's still working, send another line to steer it:
{"type":"user","message":{"role":"user","content":"Actually, keep the public API unchanged"}}The timing of each line decides what it does:
- First message — starts the run.
- Arrives while generating — injected as a steering message, delivered at the next turn boundary.
- Arrives while idle — continues the session as a new turn.
Output stays per-run for text, json, and stream-json — the streaming form emits one init envelope, then assistant / user / result envelopes.
Driving it from code is just spawning that process and writing a line whenever you want to steer:
import { spawn } from 'node:child_process';
const child = spawn(
'harnext',
['-p', '--input-format', 'stream-json', '--output-format', 'stream-json'],
{ stdio: ['pipe', 'pipe', 'inherit'] },
);
const send = (content: string) =>
child.stdin.write(
JSON.stringify({ type: 'user', message: { role: 'user', content } }) + '\n',
);
send('Refactor the auth module to use async/await'); // first line → starts the run
// …later, while it's still generating:
send('Actually, keep the public API unchanged'); // steers it mid-runThe docs show the full version that reads the output envelopes to know when to steer and when the run is done.
Read the docs
The REPL queue / commit / Esc-to-edit flow and the headless stream-json message shape, semantics, and output envelopes.
Mid-run steering landed in QualityUnit/harnext#50.