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.

harnext
Refactor the auth module to use async/await
⠹ working… editing src/auth.ts
 
⋯ Actually, keep the public API unchanged · esc to edit

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 esc on 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, esc still 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.

Shell
harnext -p --input-format stream-json --output-format stream-json
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:

TypeScript
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-run

The docs show the full version that reads the output envelopes to know when to steer and when the run is done.

Runnable sample
examples/steering-client.mjs spawns the CLI, sends an initial multi-turn task, then steers it mid-run — a complete reference for wiring steering into your own client.

Read the docs


← All posts