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

TypeScript의 typed tool 루프

~18 min · tool-use, typed, registry

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

Typed registry로서의 tools

TypeScript에선 tool registry를 strict 타입으로 인코드 가능 — 각 tool의 input 모양이 Zod 스키마나 TypeScript interface가 되고, 핸들러 시그니처가 거기서 흘러나옴. Python이 런타임에 찾는 tool-loop 버그 절반을 컴파일 타임 체크가 잡아.

같은 두-라운드 모양

프로토콜은 Python과 동일. Assistant content 리스트엔 tool_use 블록 포함; 후속 user 턴은 tool_use_id마다 tool_result 운반. 다른 점 단 하나 — TypeScript가 malformed 메시지 빌드를 허용 안 해, 타입이 막아.

Input validation엔 Zod

Tools가 input_schema(JSON Schema) 가져옴. Handler 호출 전에 모델의 tool input을 Zod로 validate — 모델이 가끔 args에 창의적이고, Zod parse 단계가 'maybe-string maybe-number'를 런타임 계약으로.

원칙: Input validation 없는 tool 루프는 모델 의도 추측. Validate, 아니면 버그 수용.

Code

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

const client = new Anthropic();

// 1. Zod 스키마 — 런타임 validation + TS 타입 한 방.
const weatherInput = z.object({ city: z.string().min(1) });
type WeatherInput = z.infer<typeof weatherInput>;

// 2. Tool name으로 키된 핸들러.
const handlers = {
  get_weather: async (input: WeatherInput) => ({
    city: input.city,
    temp_c: 22,
    condition: 'clear',
  }),
} as const;

// 3. 모델한테 보내는 tool 정의.
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 toolUseBlocks = resp.content.filter(b => b.type === 'tool_use');
    const results = await Promise.all(
      toolUseBlocks.map(async b => {
        if (b.type !== 'tool_use') throw new Error('unreachable');
        if (b.name !== 'get_weather') throw new Error(`unknown tool: ${b.name}`);
        const input = weatherInput.parse(b.input); // 런타임 validate
        const out = await handlers.get_weather(input);
        return {
          type: 'tool_result' as const,
          tool_use_id: b.id,
          content: JSON.stringify(out),
        };
      })
    );
    messages.push({ role: 'user', content: results });
  }
  throw new Error('tool loop exceeded maxIters');
}

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

External links

Exercise

TypeScript tool 루프에 input용 Zod 스키마 가진 두 번째 도구 추가. Malformed input 공급하는 unit test 작성, 루프가 핸들러 호출 거부 확인.
Hint
Zod의 .parse()는 mismatch에 throw; .safeParse()는 분기 가능한 discriminated 결과 반환.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.