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.

bashnah
npm install @astracollab/not-another-harness ai @ai-sdk/anthropic zod

The 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.

tsnah
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.

tsnah
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.

tsnah
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.

tsnah
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.