cd /news/ai-agents/claude-agent-sdk-vs-rolling-your-own… · home › topics › ai-agents › article
[ARTICLE · art-164906] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Claude Agent SDK vs rolling your own tool loop: which one for your product?

A developer published a decision guide comparing three ways to build a Claude-powered agent: a hand-written tool loop using client.messages.create(), the beta client.beta.messages.toolRunner() helper in @anthropic-ai/sdk, and the @anthropic-ai/claude-agent-sdk, which bundles the Claude Code harness with built-in file, shell and web tools plus permissions, hooks, subagents and sessions. The guide includes runnable TypeScript for each option, token usage measured against the live API on 2026-11-03 with @anthropic-ai/sdk 1.4.0 and @anthropic-ai/claude-agent-sdk 0.3.317, and a table mapping product types to an option. It stresses that the Tool Runner and the Agent SDK are distinct products and that neither hosts anything for the developer.

by read10 min views2 publishedNov 5, 2026

You have decided your product needs an agent: Claude should call tools, look at the results and decide what to do next. Before you write any code there is a choice that shapes everything after it. Do you write the tool loop yourself, let the Anthropic SDK run it for you, or embed the Claude Agent SDK and get Claude Code's whole harness as a library?

This post is a decision guide, not a review. Each option is the right answer for some products. Below is runnable TypeScript for all three, what each one leaves you in control of, the token usage we measured when running them, and a table at the end that maps "what you are building" to an option.

Facts and code were checked against the official docs and run against the live API on 2026-11-03 with @anthropic-ai/sdk 1.4.0 and @anthropic-ai/claude-agent-sdk 0.3.317.

client.messages.create(), check stop_reason, run the tools Claude asked for, send the results back, repeat. About 50 lines. You own every request.client.beta.messages.toolRunner() runs the loop. You write tool functions, it calls them and feeds the results back until Claude is done. It is a beta helper.@anthropic-ai/claude-agent-sdk). It ships built-in tools for reading, writing and editing files, running shell commands and searching the web, plus permissions, hooks, subagents and sessions. You call query() with a prompt and options. The common confusion: the Tool Runner and the Agent SDK sound alike but are different products. The Tool Runner lives in the regular API SDK and only loops over tools you define. The Agent SDK runs the Claude Code binary (the npm package bundles it) with its own tools and its own system prompt. Neither hosts anything for you; both run wherever your code runs.

The example is a support agent with one tool that looks up an order. In a real product the tool would query your database.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const MODEL = process.env.MODEL ?? "claude-sonnet-5-5";

const tools: Anthropic.Tool[] = [
  {
    name: "get_order",
    description: "Look up an order by id. Returns status and total in USD.",
    input_schema: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
      additionalProperties: false,
    },
    strict: true,
  },
];

async function runTool(name: string, input: unknown): Promise<string> {
  if (name === "get_order") {
    const { order_id } = input as { order_id: string };
    return JSON.stringify({ order_id, status: "shipped", total_usd: 99 });
  }
  throw new Error(`Unknown tool: ${name}`);
}

export async function agent(prompt: string, maxTurns = 10) {
  const messages: Anthropic.MessageParam[] = [{ role: "user", content: prompt }];

  for (let turn = 0; turn < maxTurns; turn++) {
    const res = await client.messages.create({
      model: MODEL,
      max_tokens: 4096,
      system: [{ type: "text", text: "You are a support agent for an online shop.", cache_control: { type: "ephemeral" } }],
      tools,
      messages,
    });
    console.log(`turn ${turn}`, res.stop_reason, res.usage); // meter here: one row per request

    messages.push({ role: "assistant", content: res.content });
    if (res.stop_reason !== "tool_use") return res;

    // Run every tool call from this turn, then send ALL results back in ONE user message.
    const calls = res.content.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use");
    const results: Anthropic.ToolResultBlockParam[] = await Promise.all(
      calls.map(async (c) => {
        try {
          return { type: "tool_result", tool_use_id: c.id, content: await runTool(c.name, c.input) };
        } catch (err) {
          return { type: "tool_result", tool_use_id: c.id, content: String(err), is_error: true };
        }
      }),
    );
    messages.push({ role: "user", content: results });
  }
  throw new Error("Agent hit maxTurns without finishing");
}

const final = await agent("Where is order A-1042 and how much was it?");
console.log(final.content.filter((b) => b.type === "text").map((b) => b.text).join(""));

Four details in those lines are easy to get wrong:

tool_result, with is_error: true, so Claude can recover or explain. maxTurns, a confused model and a tool that keeps failing can loop until your bill notices.strict: true`` runTool is safe. The loop treats any stop_reason other than tool_use as the end. In production you also want explicit branches for max_tokens (the answer was cut off) and refusal, and for _turn if you add server tools such as web search.

Same tool, same model, no loop code. Tools are defined with Zod and carry their own run function.

import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();
const MODEL = process.env.MODEL ?? "claude-sonnet-5-5";

const getOrder = betaZodTool({
  name: "get_order",
  description: "Look up an order by id. Returns status and total in USD.",
  inputSchema: z.object({ order_id: z.string() }),
  run: async ({ order_id }) => JSON.stringify({ order_id, status: "shipped", total_usd: 99 }),
});

const runner = client.beta.messages.toolRunner({
  model: MODEL,
  max_tokens: 4096,
  max_iterations: 10,
  system: "You are a support agent for an online shop.",
  tools: [getOrder],
  messages: [{ role: "user", content: "Where is order A-1042 and how much was it?" }],
});

// Each iteration yields one API response, so you can still meter per request.
for await (const message of runner) {
  console.log(message.stop_reason, message.usage);
}
const final = await runner.done();
console.log(final.content.filter((b) => b.type === "text").map((b) => b.text).join(""));

Iterating over the runner gives you each API response as it arrives, which is where per-request metering, logging and approval checks go. If you only want the answer, await runner.runUntilDone() does it in one line. Approval gates do not need a manual loop either: check inside the tool's run and return "the user declined" as the result.

The trade-off is that this is a beta surface. Pin the SDK version and read the changelog before upgrading.

Here the task is different on purpose, because this is what the Agent SDK is built for: work on files. No tool code at all; the agent uses its built-in Glob, Grep and Read.

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "List every TODO comment under src/ with file and line number.",
  options: {
    model: process.env.MODEL ?? "claude-sonnet-5-5",
    cwd: process.env.REPO_DIR ?? process.cwd(),
    allowedTools: ["Read", "Glob", "Grep"], // read-only: no Edit, no Bash
    maxTurns: 10,
  },
})) {
  if (message.type === "result") {
    console.log(message.subtype === "success" ? message.result : message.subtype);
    console.log({ turns: message.num_turns, cost_usd: message.total_cost_usd, usage: message.usage });
  }
}

It reads ANTHROPIC_API_KEY from the environment (it does not load .env files) and needs Node.js 18.14 or later. The final result message reports the turn count, the token usage and a cost estimate in dollars, so you do not have to compute it yourself.

One option name is a trap. allowedTools only pre-approves tools so they run without a permission prompt; it does not remove the others from Claude's context. To limit which tools exist at all, use the tools option, or disallowedTools to remove specific ones.

We ran each of the three examples once on Claude Haiku 5.5 (MODEL=claude-haiku-5-5), the cheapest current model, and read usage from the responses.

Requests Input tokens Output tokens Cost
Own loop 2 under 1,000 134 under $0.0002
Tool Runner 2 about 1,000 114 under $0.0002
Agent SDK 2 86,630 (43,487 cache write, 43,139 cache read) 483 $0.0095

Haiku 5.5 list prices on 2026-11-03, per million tokens: $0.10 input, $0.20 for a 1-hour cache write, $0.01 for a cache read, $0.50 output (official pricing).

The two tasks are different, so read the table for its shape, not its ratio. The shape is clear: the Agent SDK sends about 43,000 tokens of its own system prompt and tool definitions with every request. The SDK caches that prefix with a 1-hour cache, so after the first request it is billed at the cache-read price. On Haiku it barely matters. On Claude Sonnet 5.5 ($2 input, $4 one-hour cache write, $0.10 cache read) the same prefix costs about $0.17 to write once per hour and about $0.004 per request after that, before any of your own tokens.

The own loop and the Tool Runner cost the same, because they send the same request. The runner saves you code, not tokens. Also note that their small system prompt was not cached at all (cache_read_input_tokens: 0): prompts shorter than the model's minimum cacheable length are silently not cached. With a real system prompt and a handful of tools, you cross that threshold.

Per-token prices are identical across the three options. What differs is how many tokens each one spends per request, and who decides.

Own loop Tool Runner Agent SDK
System prompt Yours, every byte Yours Claude Code's, which you can customize
Tools Only yours Only yours Built-in file, shell and web tools, plus yours via MCP
Where tools run Your code Your code The machine running the SDK (files, shell)
Per-request hooks Anything Per-iteration access Lifecycle hooks (before and after tool calls, and more)
Fixed overhead per request Your prompt and tools Your prompt and tools About 43,000 tokens, cached
Dependency risk Stable Messages API Beta helper A separate SDK that tracks Claude Code releases
Runs on edge or serverless Yes Yes Needs a Node or Python process that can run a binary

If your agent works on files and code, the Agent SDK saves you weeks. Reading, searching and editing files, running commands, asking for permission before anything destructive, splitting work into subagents, resuming a session tomorrow: all of that already exists and has been hardened by everyone who uses Claude Code. A bot that opens a pull request to fix lint errors, an internal tool that triages failing CI runs, an agent that migrates a config format across a repo: these are a weekend with the Agent SDK and a month by hand.

It also suits internal tools, where the person running the agent is a developer and a 43,000-token prefix is noise next to the value of the result.

Most customer-facing SaaS features are not coding agents. They search your database, call your API, draft an email. For those you want a system prompt of a few hundred tokens, five tools you wrote, a cache hit on every turn, and one row in a usage_events table per request. That is the case for options 1 and 2.

Your own loop also wins when:

messages.create() call. We wrote up the whole metering layer in Pick the Tool Runner when you want everything from option 1 except the loop code. Zod schemas double as input validation, the loop handles parallel tool calls and errors for you, and iterating over the runner still gives you each response for metering. Accept that it is beta: pin the version, and keep the manual loop above in mind as the escape hatch, since both send identical requests.

You are building Start with Why
A coding agent or developer tool Agent SDK Files, shell, permissions and subagents already exist
An internal automation that edits a repo Agent SDK The prefix cost is noise next to the time saved
A customer-facing feature with your own tools Own loop or Tool Runner Small prompt, full caching, per-request metering
A prototype this afternoon Tool Runner Least code that still uses your tools
Something that must run on the edge Own loop No binary, no beta dependency

If you are unsure, start with the Tool Runner. Moving from it to a manual loop is a small refactor, because the requests are the same.

Questions or corrections? Please leave them in the comments on the original post. We check those every day and answer there, while replies here on DEV can take a while.

Disclosure: this article was written with the help of AI (Claude). Facts, prices and code were checked against the official documentation and reviewed before publishing.

Originally published on the Agentwright blog.

── more in #ai-agents 4 stories · sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-agent-sdk-vs-…] indexed:0 read:10min 2026-11-05 · —