{"slug": "claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product", "title": "Claude Agent SDK vs rolling your own tool loop: which one for your product?", "summary": "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.", "body_md": "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?\n\nThis 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.\n\nFacts 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.\n\n`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.\nThe 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.\n\nThe example is a support agent with one tool that looks up an order. In a real product the tool would query your database.\n\n``` python\nimport Anthropic from \"@anthropic-ai/sdk\";\n\nconst client = new Anthropic();\nconst MODEL = process.env.MODEL ?? \"claude-sonnet-5-5\";\n\nconst tools: Anthropic.Tool[] = [\n  {\n    name: \"get_order\",\n    description: \"Look up an order by id. Returns status and total in USD.\",\n    input_schema: {\n      type: \"object\",\n      properties: { order_id: { type: \"string\" } },\n      required: [\"order_id\"],\n      additionalProperties: false,\n    },\n    strict: true,\n  },\n];\n\nasync function runTool(name: string, input: unknown): Promise<string> {\n  if (name === \"get_order\") {\n    const { order_id } = input as { order_id: string };\n    return JSON.stringify({ order_id, status: \"shipped\", total_usd: 99 });\n  }\n  throw new Error(`Unknown tool: ${name}`);\n}\n\nexport async function agent(prompt: string, maxTurns = 10) {\n  const messages: Anthropic.MessageParam[] = [{ role: \"user\", content: prompt }];\n\n  for (let turn = 0; turn < maxTurns; turn++) {\n    const res = await client.messages.create({\n      model: MODEL,\n      max_tokens: 4096,\n      system: [{ type: \"text\", text: \"You are a support agent for an online shop.\", cache_control: { type: \"ephemeral\" } }],\n      tools,\n      messages,\n    });\n    console.log(`turn ${turn}`, res.stop_reason, res.usage); // meter here: one row per request\n\n    messages.push({ role: \"assistant\", content: res.content });\n    if (res.stop_reason !== \"tool_use\") return res;\n\n    // Run every tool call from this turn, then send ALL results back in ONE user message.\n    const calls = res.content.filter((b): b is Anthropic.ToolUseBlock => b.type === \"tool_use\");\n    const results: Anthropic.ToolResultBlockParam[] = await Promise.all(\n      calls.map(async (c) => {\n        try {\n          return { type: \"tool_result\", tool_use_id: c.id, content: await runTool(c.name, c.input) };\n        } catch (err) {\n          return { type: \"tool_result\", tool_use_id: c.id, content: String(err), is_error: true };\n        }\n      }),\n    );\n    messages.push({ role: \"user\", content: results });\n  }\n  throw new Error(\"Agent hit maxTurns without finishing\");\n}\n\nconst final = await agent(\"Where is order A-1042 and how much was it?\");\nconsole.log(final.content.filter((b) => b.type === \"text\").map((b) => b.text).join(\"\"));\n```\n\nFour details in those lines are easy to get wrong:\n\n`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.\nThe 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 `pause_turn` if you add server tools such as web search.\n\nSame tool, same model, no loop code. Tools are defined with Zod and carry their own `run` function.\n\n``` python\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { betaZodTool } from \"@anthropic-ai/sdk/helpers/beta/zod\";\nimport { z } from \"zod\";\n\nconst client = new Anthropic();\nconst MODEL = process.env.MODEL ?? \"claude-sonnet-5-5\";\n\nconst getOrder = betaZodTool({\n  name: \"get_order\",\n  description: \"Look up an order by id. Returns status and total in USD.\",\n  inputSchema: z.object({ order_id: z.string() }),\n  run: async ({ order_id }) => JSON.stringify({ order_id, status: \"shipped\", total_usd: 99 }),\n});\n\nconst runner = client.beta.messages.toolRunner({\n  model: MODEL,\n  max_tokens: 4096,\n  max_iterations: 10,\n  system: \"You are a support agent for an online shop.\",\n  tools: [getOrder],\n  messages: [{ role: \"user\", content: \"Where is order A-1042 and how much was it?\" }],\n});\n\n// Each iteration yields one API response, so you can still meter per request.\nfor await (const message of runner) {\n  console.log(message.stop_reason, message.usage);\n}\nconst final = await runner.done();\nconsole.log(final.content.filter((b) => b.type === \"text\").map((b) => b.text).join(\"\"));\n```\n\nIterating 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.\n\nThe trade-off is that this is a beta surface. Pin the SDK version and read the changelog before upgrading.\n\nHere 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`.\n\n``` js\nimport { query } from \"@anthropic-ai/claude-agent-sdk\";\n\nfor await (const message of query({\n  prompt: \"List every TODO comment under src/ with file and line number.\",\n  options: {\n    model: process.env.MODEL ?? \"claude-sonnet-5-5\",\n    cwd: process.env.REPO_DIR ?? process.cwd(),\n    allowedTools: [\"Read\", \"Glob\", \"Grep\"], // read-only: no Edit, no Bash\n    maxTurns: 10,\n  },\n})) {\n  if (message.type === \"result\") {\n    console.log(message.subtype === \"success\" ? message.result : message.subtype);\n    console.log({ turns: message.num_turns, cost_usd: message.total_cost_usd, usage: message.usage });\n  }\n}\n```\n\nIt 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.\n\nOne 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.\n\nWe 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.\n\n|  | Requests | Input tokens | Output tokens | Cost | \n|---|---|---|---|---|\n| Own loop | 2 | under 1,000 | 134 | under $0.0002 | \n| Tool Runner | 2 | about 1,000 | 114 | under $0.0002 | \n| Agent SDK | 2 | 86,630 (43,487 cache write, 43,139 cache read) | 483 | $0.0095 | \n\nHaiku 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](https://platform.claude.com/docs/en/about-claude/pricing)).\n\nThe 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.\n\nThe 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.\n\nPer-token prices are identical across the three options. What differs is how many tokens each one spends per request, and who decides.\n\n|  | Own loop | Tool Runner | Agent SDK | \n|---|---|---|---|\n| System prompt | Yours, every byte | Yours | Claude Code's, which you can customize | \n| Tools | Only yours | Only yours | Built-in file, shell and web tools, plus yours via MCP | \n| Where tools run | Your code | Your code | The machine running the SDK (files, shell) | \n| Per-request hooks | Anything | Per-iteration access | Lifecycle hooks (before and after tool calls, and more) | \n| Fixed overhead per request | Your prompt and tools | Your prompt and tools | About 43,000 tokens, cached | \n| Dependency risk | Stable Messages API | Beta helper | A separate SDK that tracks Claude Code releases | \n| Runs on edge or serverless | Yes | Yes | Needs a Node or Python process that can run a binary | \n\nIf 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.\n\nIt 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.\n\nMost 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.\n\nYour own loop also wins when:\n\n`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.\n\n| You are building | Start with | Why | \n|---|---|---|\n| A coding agent or developer tool | Agent SDK | Files, shell, permissions and subagents already exist | \n| An internal automation that edits a repo | Agent SDK | The prefix cost is noise next to the time saved | \n| A customer-facing feature with your own tools | Own loop or Tool Runner | Small prompt, full caching, per-request metering | \n| A prototype this afternoon | Tool Runner | Least code that still uses your tools | \n| Something that must run on the edge | Own loop | No binary, no beta dependency | \n\nIf 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.\n\n**Questions or corrections?** Please leave them in the [comments on the original post](https://agentwright.app/blog/claude-agent-sdk-vs-your-own-tool-loop/#comments). We check those every day and answer there, while replies here on DEV can take a while.\n\n*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.*\n\n*Originally published on the [Agentwright blog](https://agentwright.app/blog/claude-agent-sdk-vs-your-own-tool-loop/).*", "url": "https://wpnews.pro/news/claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product", "canonical_source": "https://dev.to/agentwright/claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product-33lg", "published_at": "2026-11-05 21:18:12+00:00", "updated_at": "2026-11-05 21:23:15.242406+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models"], "entities": ["Anthropic", "Claude", "Claude Agent SDK", "@anthropic-ai/sdk", "Claude Code", "TypeScript"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product", "markdown": "https://wpnews.pro/news/claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product.md", "text": "https://wpnews.pro/news/claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product.txt", "jsonld": "https://wpnews.pro/news/claude-agent-sdk-vs-rolling-your-own-tool-loop-which-one-for-your-product.jsonld"}}