RUNTIME / SDK QUICKSTART
Compose your own agent runner.
The runtime package exports the loop, prompt builder, built-in tools, session store, and memory. You provide an AI SDK v5 LanguageModel and decide how to approve changes and present the run.
Install the runtime
Install the runtime, not the CLI. They are separate packages: @astracollab/nah 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 @astracollab/not-another-harness ai @ai-sdk/anthropic zodThe runtime declares AI SDK and Zod as peer dependencies. Use versions compatible with the installed runtime.
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 '@astracollab/not-another-harness';
import { createNodeEnvironment } from '@astracollab/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,
});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 } from '@astracollab/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;
await session.append(result.messages);To give a session memory across runs, construct CognitiveMemory with an onPersist writer, restore it with loadSnapshot at startup, and prepend planInjection() to the system prompt each turn. See cognitive memory.