RUNTIME / AGENT LOOP
A turn, from prompt to finish.
NAH keeps the coding-agent cycle in ordinary code you can inspect: prepare context, stream one model response, execute its tool calls, record the result, then decide whether to continue.
Configure a run
runAgent is the runtime entry point. Pass the model, task, system instructions, and tools on every run; then add only the policies your application needs. It accepts an AI SDK language model and does not choose a provider or own a UI.
const run = runAgent({
model, prompt, system, tools,
maxSteps: 24,
maxOutputTokens: 8_000,
maxContextTokens: 100_000,
shouldStop: ({ stepNumber }) => stepNumber > 20 && noProgress(),
});The CLI assembles an application around this core: it resolves credentials and a model, discovers project instructions, adds memory and optional delegation, selects tools, applies a permission policy, and persists the conversation.
Build an agent with the SDK →What happens during a turn
- Prepare the request. The prompt, prior session messages, the system prompt, discovered
AGENTS.md/CLAUDE.mdcontext, and — when memory is enabled — the memory index for this turn. - Start a step. One AI SDK streaming call with the current transcript and tool map. One step is one model round-trip, including any tool calls in that response.
- Stream and run tools. Text deltas and tool calls/results become typed events as they arrive. Tools enforce schemas, output caps, workspace boundaries, and read-before-write for existing files.
- Record usage and check stop conditions. The assistant response joins the transcript. The loop stops if there were no tool calls, a budget was reached, the response hit its output limit, or cancellation occurred.
- Continue with compacted context when needed. Older messages are summarized or truncated before the next step when the conversation itself gets large.
- Persist. The transcript is appended to the session file and, when memory is on, the turn is extracted and the snapshot saved.
prompt + prior messages + system + tools + memory index
→ streamed model step
→ text / tool calls / tool results
→ append response and account usage
├─ no tool calls or a stop condition → finish
└─ tools remain and budget allows → compact → next stepSet limits and context handling
Set limits explicitly for your product’s latency and cost envelope. There is no default step ceiling; maxSteps is an opt-in hard cap. maxSpendUsd applies a cumulative dollar ceiling when you also provide model rates. maxOutputTokens caps one model response and defaults to 16,384 tokens.
const run = runAgent({
model, prompt, system, tools,
maxSteps: 30,
maxSpendUsd: 0.75,
rates: { input: 3, output: 15 }, // USD per million tokens
maxContextTokens: 100_000,
compactAtTokens: 80_000,
compaction: "model", // or "truncate" / "off"
compactKeepRecent: 8,
});Compaction is triggered from the latest request’s reported input size, not cumulative token usage. It runs after a tool step when there is older context to summarize. Tool call/result pairs stay together at the boundary so the next provider request remains valid.
Change behavior per step
Use prepareStep when a request needs a different tool set, tool choice, or context on a particular step. Each override applies to that step only; the next step starts from the run’s original options.
const run = runAgent({
model, prompt, system, tools,
prepareStep: ({ stepNumber }) => {
if (stepNumber === 1) {
return { toolChoice: "required" };
}
return { toolChoice: "auto" };
},
});For domain-specific stop conditions, use shouldStop. If your app can tell that a completed model response is premature, use shouldContinue to request another step. These hooks keep application policy in code instead of burying it in prompt instructions.
Steering a running turn
A run is steerable from the host or the CLI. A message sent mid-run is appended to the transcript at the next step boundary — after the current step's tools settle, before the next model request — so it is never injected into a request that is already streaming.
const run = runAgent(options);
run.steer("actually use TypeScript, not JavaScript");
run.followUp("then update the changelog"); // only if the run would otherwise finish
run.interrupt(); // abort now
run.pending(); // { steer: [], followUp: [] }
Steers jump ahead of follow-ups, and each keeps its order. A steer never aborts the in-flight model call — interrupt() is the separate action for that. Two details matter for reliability: a pending message prevents the run from ending (otherwise a message typed while the model was writing its final answer would be silently discarded), and a delivered message grants a fresh step window so maxSteps cannot drop something a human deliberately sent.
Steers reach the model as ordinary user messages, indistinguishable from any other turn. Nothing marks them.
Stopping a turn that will not stop
Tool execution receives the run's abort signal, so interrupting reaches a running command instead of merely looking like it did. A shell command that reads stdin gets EOF immediately rather than blocking until its timeout, and a timed-out command is killed as a process group so no grandchildren survive.
Ctrl-C stop the running turn and say so
Ctrl-C press again to exit the TUI
Ctrl-D exit on an empty promptTools and permissions
The standard tools are read, list, grep, glob, edit, write, and bash. Read-only tools are never approval-gated. Edit, write, and bash pass through the configured policy; the SDK allows them if no callback is supplied.
In an interactive session NAH asks before mutations. Answer y once, a to allow that whole tool for the session, or A for only that exact call. Scoping “always” to the exact command string meant a fresh prompt for every slightly different invocation, which read as the choice not being remembered.
readonly denies mutations and yolo allows them. Print and JSON modes default to yolo because they cannot pause for a prompt, so pass --permissions readonly for a read-only one-shot run.
Observe and use the result
runAgent returns an async event stream and a result promise. The stream reports run-start, step boundaries, text deltas, tool activity, mid-turn user messages, compaction, and finish/error events. The result contains final text, stop reason, usage, transcript, step count and compaction count.
const run = runAgent(options);
for await (const event of run.events) {
renderProgress(event);
}
const result = await run.result;
console.log(result.reason, result.steps, result.usage);The CLI adds session controls around a turn: continue or branch saved JSONL history, inspect usage and the task plan, view diffs, and undo supported workspace changes.
CLI modes, sessions, and the TUI →