C.W.K.
Stream
Lesson 03 of 06 · published

Typed Tool Loops in TypeScript

~18 min · tool-use, typed, registry

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

Tools as a typed registry

In TypeScript, you can encode the tool registry with strict types: each tool's input shape becomes a Zod schema or a TypeScript interface, and the handler signature flows from there. The compile-time check catches half the tool-loop bugs Python finds at runtime.

The same two-round shape

The protocol is identical to Python. Your assistant content list must include the tool_use block; your follow-up user turn carries one tool_result per tool_use_id. The only difference is that TypeScript will not let you build a malformed message — types prevent it.

Zod for input validation

Tools come with input_schema (JSON Schema). Use Zod to validate the model's tool input before invoking the handler — the model is sometimes creative with its arguments, and a Zod parse step turns 'maybe-string maybe-number' into a runtime contract.

Principle: Tool loops without input validation are a guess at the model's intentions. Validate, or accept the bugs.

Code

Typed registry + Zod validation·typescript
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';

const client = new Anthropic();

// 1. Zod schemas — runtime validation + TS types in one shot.
const weatherInput = z.object({ city: z.string().min(1) });
type WeatherInput = z.infer<typeof weatherInput>;

// 2. Handlers keyed by tool name.
const handlers = {
  get_weather: async (input: WeatherInput) => ({
    city: input.city,
    temp_c: 22,
    condition: 'clear',
  }),
} as const;

// 3. Tool definitions sent to the model.
const tools: Anthropic.Tool[] = [
  {
    name: 'get_weather',
    description: 'Get current weather for a city.',
    input_schema: {
      type: 'object',
      properties: { city: { type: 'string' } },
      required: ['city'],
    },
  },
];

async function runLoop(userText: string, maxIters = 10): Promise<string> {
  const messages: Anthropic.MessageParam[] = [{ role: 'user', content: userText }];
  for (let i = 0; i < maxIters; i++) {
    const resp = await client.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 1024,
      tools,
      messages,
    });
    messages.push({ role: 'assistant', content: resp.content });

    if (resp.stop_reason !== 'tool_use') {
      const text = resp.content.find(b => b.type === 'text');
      if (text && text.type === 'text') return text.text;
      throw new Error('expected text in final assistant turn');
    }

    const results: Anthropic.MessageParam = {
      role: 'user',
      content: resp.content
        .filter(b => b.type === 'tool_use')
        .map(b => {
          if (b.type !== 'tool_use') throw new Error('unreachable');
          if (b.name === 'get_weather') {
            const input = weatherInput.parse(b.input); // validate at runtime
            return handlers.get_weather(input).then(out => ({
              type: 'tool_result' as const,
              tool_use_id: b.id,
              content: JSON.stringify(out),
            }));
          }
          throw new Error(`unknown tool: ${b.name}`);
        })
        // Above maps to Promises; await all before pushing.
        .reduce(async (accP, p) => {
          const acc = await accP;
          acc.push(await p);
          return acc;
        }, Promise.resolve([] as Anthropic.ToolResultBlockParam[])),
    } as Anthropic.MessageParam;
    messages.push(results);
  }
  throw new Error('tool loop exceeded maxIters');
}

console.log(await runLoop('What is the weather in Seoul?'));

External links

Exercise

Add a second tool to your TypeScript tool loop with a Zod schema for its input. Write a unit test that supplies a malformed input and confirms the loop refuses to invoke the handler.
Hint
Zod's .parse() throws on mismatch; .safeParse() returns a discriminated result you can branch on.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.