BUILD AN AGENT / SDK GUIDE
Compose an agent for your product.
This guide wires a model, system prompt, tools, and run policy into an agent you can embed in a TypeScript service. NAH owns the loop; your app owns the capabilities and user experience.
Install the runtime
Install the runtime, not the CLI. They are separate packages: nah-ai is the terminal application and re-exports nothing, so importing the loop from it fails. The Node environment adapter is a subpath of the runtime.
npm install not-another-harness ai @ai-sdk/anthropic zodThe runtime declares AI SDK and Zod as peer dependencies. Use versions compatible with the installed runtime.
Embedding in a web app
This guide shows the direct runAgent API for composing a run. If you are building a browser chat and want HTTP routes, SSE streaming, reconnects, session adapters, and a React hook, use nah-js—the Web runtime built on the same harness.
npm install nah-js not-another-harness ai zodFollow the nah-js web integration guide for Next.js App Router and examples for Hono, Remix, SvelteKit, Astro, and Web API servers.
Create a run
Build a model with your chosen AI SDK provider, create tools rooted at your workspace, then call runAgent. The core loop accepts a complete system prompt and a tool map.
import {
buildSystemPrompt,
createCodingTools,
runAgent,
} from 'not-another-harness';
import { createNodeEnvironment } from 'not-another-harness/node';
import type { LanguageModel } from 'ai';
declare const model: LanguageModel; // create with an AI SDK v5 provider
const cwd = process.cwd();
const tools = createCodingTools(createNodeEnvironment(cwd), {
withBash: false,
approveToolCall: async (toolName, input) =>
requestApproval(toolName, input),
});
const run = runAgent({
model,
prompt: 'explain the session refresh path',
system: buildSystemPrompt({ cwdLabel: cwd }),
tools,
maxSteps: 20,
});Write the system instructions
Pass a complete system string. buildSystemPrompt is an optional helper for coding agents; product agents can provide their own instructions and include the rules that matter for their domain.
const system = [
"You are the support assistant for Acme.",
"Use account tools only for the authenticated account.",
"Ask before changing a subscription or closing a case.",
"When a tool returns no result, say so instead of guessing.",
].join("\n");System instructions guide behavior, while the tool map and server-side checks enforce what the agent can actually do. See tool configuration for schemas, closures, context, and approvals.
Choose the run policy
Run options control runtime behavior. Select limits for your latency and spend envelope, pass cancellation from the request lifecycle, and provide domain rules as callbacks.
const controller = new AbortController();
request.signal.addEventListener("abort", () => controller.abort(), { once: true });
const run = runAgent({
model, prompt, system, tools,
abortSignal: controller.signal,
maxSteps: 24,
maxSpendUsd: 0.50,
rates: { input: 3, output: 15 },
shouldStop: ({ stepNumber, steps }) =>
stepNumber > 18 && steps.some((step) => step.toolNames.includes("submit")),
});Use prepareStep to alter tools or tool choice for a specific request. Use shouldStop when application state can identify an end condition that the generic loop cannot know.
Steer a running turn
A run is steerable. Messages are appended at the next step boundary — never injected into a request that is already streaming — so the in-flight model call is never cut off.
run.steer('actually use TypeScript'); // next step boundary
run.followUp('then update the changelog'); // only if it would otherwise finish
run.interrupt(); // abort now
run.pending(); // { steer: [], followUp: [] }A pending message prevents the run from ending, and a delivered message grants a fresh step window so maxSteps cannot discard something a human deliberately sent. steer() and followUp() return false once the run has settled rather than accepting input that would be lost.
Consume events and result
Listen to the async stream for live progress. Then await the result for the final text, stop reason, usage totals, transcript, and number of compactions.
for await (const event of run.events) {
if (event.type === 'text-delta') {
renderText(event.text);
} else if (event.type === 'tool-call') {
showToolCall(event.toolName, event.input);
} else if (event.type === 'tool-result') {
showToolResult(event.output, event.isError);
}
}
const result = await run.result;
showCompletion(result.reason, result.usage);For all event variants and cancellation behavior, see streaming events. For the defaults behind maxSteps and compaction, see the agent loop.
Persist sessions
The core runtime returns the full transcript in the result and accepts prior AI SDK messages through the messages option. For JSONL persistence the package exports createJsonlSessionStore, with append and load methods. The CLI uses that store for its session behavior.
import { createJsonlSessionStore, sessionUpdate } from 'not-another-harness';
const session = createJsonlSessionStore('.nah/session.jsonl');
const previousMessages = await session.load();
const run = runAgent({ ...options, messages: previousMessages });
const result = await run.result;
const update = sessionUpdate(previousMessages, result);
if (update.mode === 'replace') await session.replace(update.messages);
else await session.append(update.messages);The transcript itself is your session’s normal conversational state; persist it independently with the session store as described in conversation state. For durable context across sessions, the HTTP runtime accepts an optional NahMemory implementation plus a state store and key. It calls getPromptContext() before each turn and postTurnAsync() after it. See cognitive memory for the configurable backend.