RUNTIME / TOOLS

Give your agent the right tools.

Tools are the agent’s interface to your product. Define their inputs, implement their effects, and pass only the capabilities this run should have. NAH uses the AI SDK v5 tool format, so your tools stay ordinary TypeScript.

A tool has a schema and an implementation

Define a tool with the AI SDK tool() helper. The input schema tells the model what arguments it may provide; execute validates those arguments and performs your operation. Return a concise, serializable result the model can use in its next step.

tsnah
import { tool } from "ai";
import { z } from "zod";

const lookupIssue = tool({
  description: "Look up an issue by its key, such as APP-42.",
  inputSchema: z.object({
    key: z.string().describe("Issue key, for example APP-42"),
  }),
  execute: async ({ key }) => {
    const issue = await issues.get(key);
    if (!issue) return { found: false, key };
    return {
      found: true,
      key: issue.key,
      title: issue.title,
      status: issue.status,
    };
  },
});

Descriptions and field descriptions are part of the model-facing interface. State when to use the tool, what inputs mean, and any important constraints. Keep results focused: return the fields needed for the task instead of entire database records.

Pass a tool map to runAgent

Tools are properties on a plain object. Property names become the names the model calls. Compose only the tools the task needs, then pass that map to runAgent.

tsnah
import { runAgent } from "not-another-harness";

const tools = { lookupIssue, searchDocs };
const run = runAgent({
  model,
  prompt: "Find the issue and summarize the documented fix.",
  system: "Use the available tools to answer accurately.",
  tools,
});

for await (const event of run.events) {
  if (event.type === "tool-call") {
    console.log(`Calling ${event.toolName}`, event.input);
  }
}
const result = await run.result;

Use your own tool map alone for a focused assistant, or combine it with selected built-ins. You do not have to expose every tool returned by createCodingTools.

tsnah
import { createCodingTools, runAgent } from "not-another-harness";
import { createNodeEnvironment } from "not-another-harness/node";

const coding = createCodingTools(createNodeEnvironment(cwd), {
  withBash: false,
  withWebFetch: false,
});
const tools = {
  read: coding.read,
  grep: coding.grep,
  lookupIssue,
};

const run = runAgent({ model, prompt, system, tools });

Scope tools to the current user

For values fixed for the whole run, close over them when creating the tool. This keeps tenant and permission checks next to the operation and avoids asking the model to supply trusted identity data.

tsnah
function makeWorkspaceTools(actor: Actor, workspaceId: string) {
  return {
    searchWorkspace: tool({
      description: "Search files in the current workspace.",
      inputSchema: z.object({ query: z.string() }),
      execute: async ({ query }) => {
        await access.assertCanRead(actor, workspaceId);
        return workspace.search({ workspaceId, query });
      },
    }),
  };
}

const tools = makeWorkspaceTools(currentActor, activeWorkspace.id);
const run = runAgent({ model, prompt, system, tools });

When a value genuinely changes per model step, provide toolsContext, keyed by tool name. The SDK passes that slice to the tool as options.context.

tsnah
const tools = {
  searchWorkspace: tool({
    description: "Search the workspace.",
    inputSchema: z.object({ query: z.string() }),
    execute: async ({ query }, { context }) => {
      const { requestId } = context as { requestId: string };
      return searchWorkspace(query, { requestId });
    },
  }),
};

const run = runAgent({
  model, prompt, system, tools,
  toolsContext: { searchWorkspace: { requestId } },
});

Require approval for sensitive tools

Use AI SDK v5 toolApproval to hold calls until a person decides. Name the gated tools explicitly; tools omitted from this object run without this approval gate. An approval request ends the run with awaiting-approval, so persist the transcript, collect a decision, append the approval response, and resume the run.

tsnah
import { appendApprovalResponses, runAgent } from "not-another-harness";

const tools = { lookupIssue, deleteIssue };
const options = {
  model, prompt, system, tools,
  toolApproval: { deleteIssue: "user-approval" },
};

const first = await runAgent(options).result;
if (first.reason === "awaiting-approval") {
  const decisions: Record<string, { approved: boolean }> = {};
  for (const pending of first.pendingApprovals) {
    decisions[pending.approvalId] = { approved: await askHuman(pending) };
  }
  const resumed = runAgent({
    ...options, prompt: "",
    messages: appendApprovalResponses(first.messages, decisions),
  });
  const result = await resumed.result;
}

For a permission callback that runs immediately before a built-in mutation, use createCodingTools’s approveToolCall option instead. This is distinct from the transcript-based toolApproval flow.

tsnah
const coding = createCodingTools(environment, {
  approveToolCall: async (toolName, input) =>
    askForApproval({ toolName, input }),
});

See the SDK quickstart for the full runtime setup and the event guide for rendering tool calls and results.

Built-in coding tools

createCodingTools(environment, options) returns a ready-to-use AI SDK tool map rooted at the provided environment. The defaults are intended for repository work; disable capabilities the agent should not have.

ToolPurpose
readRead a text file with 1-based line numbers and paging.
listList a directory tree with depth and output limits.
globFind files by name or path pattern: *, **, ?, {a,b}.
grepSearch file contents and return capped path:line matches.
editApply an exact replacement to a file.
writeCreate or replace a text file.
bashRun a shell command with a timeout; included by default.
outlineSummarize the repository structure when the environment supports it.
web_fetchFetch a URL and return bounded page text.
recallSearch this project's memory. Read-only, and CLI-registered.
tsnah
const tools = createCodingTools(environment, {
  withBash: false,             // omit shell access
  withGlob: true,              // pattern-based discovery
  withOutline: false,          // omit repository outline
  withWebFetch: false,         // disable network fetches
  requireReadBeforeWrite: true,
  caps: { read: { maxLines: 300 } },
});

Read-only tools — read, list, grep, and glob — are not approval-gated. Edit, write, bash, and web_fetch use approveToolCall when you provide it; without a callback they are allowed. The filesystem environment roots paths to the selected workspace and rejects paths that escape it.

Tool design checklist

  • Give each tool one clear action and a precise description.
  • Validate every model-provided value with a schema, including IDs and ranges.
  • Derive identity, tenant, and permission from trusted server state.
  • Return small structured results and handle expected failures explicitly.
  • Put irreversible actions behind an approval or application policy.
  • Expose only the tools relevant to this agent and run.

For filesystem discovery patterns, output bounds, and the built-in approval callback in detail, continue to the SDK quickstart and the agent loop.