メインコンテンツへスキップ
ブログ一覧

Claude Agent SDKで自律型AIエージェントを構築する実践ガイド — CLI spawn方式からの移行で得られる型安全性とストリーミング制御

Claude Agent SDKAIエージェントTypeScript自律開発Claude Code

Claude Agent SDKとは何か — CLIラッパーの正体を理解する

CLI spawn方式との決定的な違い

claude -p --dangerously-skip-permissions でCLIを叩き続けてきた開発者は多いだろう。筆者もその一人で、24時間365日稼働する自律開発デーモンのコアに child_process.spawn によるCLI呼び出しを据えてきた。

Claude Agent SDK(旧称Claude Code SDK、2025年9月に改名)は、このCLI呼び出しを型安全なインターフェースに置き換えるTypeScript/Python向けSDKだ。パッケージ名は @anthropic-ai/claude-agent-sdk(TS)/ claude-agent-sdk(Python)。最新バージョンはTypeScript v0.2.63(2026-02-28時点)。

まず理解すべき重要な事実がある。SDKの実体はClaude Code CLIの子プロセスラッパーである。Anthropic Messages APIを直接叩くのではなく、query() 呼び出しごとにCLIプロセスをspawnする。つまりCLIバイナリが別途インストール済みであることが前提条件だ。

SDKの内部アーキテクチャ(子プロセスモデル)

CLI直接spawnとSDKの違いを比較してみよう。

従来: CLI spawn方式(claude-runner.ts相当)

typescript
import { spawn } from "child_process";

const child = spawn("claude", ["-p", "--dangerously-skip-permissions"], {
  cwd,
  env: getCleanEnv(),
  stdio: ["pipe", "pipe", "pipe"],
});

let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => { stdout += data.toString(); });
child.stderr.on("data", (data) => { stderr += data.toString(); });
child.stdin.write(prompt);
child.stdin.end();

child.on("close", (exitCode) => {
  const rateLimited = isRateLimited(exitCode, stderr, stdout);
  // 文字列パースでレート制限を判定...
});

SDK方式: query()による型付き呼び出し

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

for await (const message of query({
  prompt: "プロジェクトのビルドエラーを修正して",
  options: {
    cwd: "/path/to/project",
    permissionMode: "bypassPermissions",
    allowDangerouslySkipPermissions: true,
  },
})) {
  if (message.type === "result") {
    console.log(message.subtype); // "success" | "error_max_turns" | ...
  }
}

決定的な違いは、stdoutの文字列パースが不要になる点だ。SDKは構造化された SDKMessage オブジェクトを返すため、エラー判定もレート制限検出も型レベルで保証される。

ビルトインツールは Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, Task, AskUserQuestion の10種が中心で、CLIと同じ能力をプログラマブルに利用できる。


セットアップと最初のエージェント — 5分で動かす最小構成

インストールと前提条件

bash
npm install @anthropic-ai/claude-agent-sdk

前提として claude(Claude Code CLI)がPATHに存在し、認証済みである必要がある。

最小限のquery()呼び出し

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

for await (const message of query({
  prompt: "Hello, Agent SDK!",
  options: {
    systemPrompt: { type: "preset", preset: "claude_code" },
    settingSources: ["project"],
    permissionMode: "bypassPermissions",
    allowDangerouslySkipPermissions: true,
    cwd: process.cwd(),
  },
})) {
  if (message.type === "assistant") {
    console.log(message.message.content);
  }
  if (message.type === "result") {
    console.log("Cost:", message.cost_usd);
  }
}

Breaking Changesへの対応 — ここでハマる

正直、最初は戸惑った。公式ドキュメントやブログ記事のコードをそのまま動かしても期待通りに動かないケースがある。原因はv0.1.0で導入された2つのBreaking Changeだ。

1. systemPromptのデフォルトが最小限に変更された

旧バージョンではClaude Codeのフルシステムプロンプトがデフォルトで適用されていたが、現在は最小限のプロンプトしか設定されない。CLIと同等の動作を期待するなら、以下の明示指定が必須だ。

typescript
systemPrompt: { type: "preset", preset: "claude_code" }

2. settingSourcesのデフォルトが空配列に変更された

CLAUDE.mdや .claude/settings.json を読み込ませるには、明示的に指定する必要がある。

typescript
settingSources: ["user", "project", "local"]

この変更はCI/CDやマルチテナント環境での予測可能性を確保するための設計判断だが、ローカル開発で「なぜかCLAUDE.mdが効かない」と悩む原因になりやすい。

permissionMode'default'(標準)、'acceptEdits'(ファイル編集を自動承認)、'bypassPermissions'(全許可バイパス)から選択できる。自律デーモン用途なら bypassPermissions + allowDangerouslySkipPermissions: true が現実的だが、信頼できるワークフロー以外では使わないこと。


カスタムツール定義とサブエージェント構成 — SDKの真価を引き出す

型安全なカスタムツールの定義

SDKの最大の魅力は、Zodスキーマによる型安全なカスタムツール定義だ。CLI方式では実現できなかった入出力の型チェックが、SDKならビルドタイムで保証される。

typescript
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const slackNotifyTool = tool(
  "notify_slack",
  "Slackチャンネルにメッセージを送信する",
  {
    channel: z.string().describe("送信先チャンネル名"),
    message: z.string().describe("メッセージ本文"),
  },
  async ({ channel, message }) => {
    await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: "POST",
      body: JSON.stringify({ text: `[${channel}] ${message}` }),
    });
    return { content: [{ type: "text", text: "通知完了" }] };
  }
);

const customServer = createSdkMcpServer({
  name: "my-tools",
  version: "1.0.0",
  tools: [slackNotifyTool],
});

カスタムツールはMCPサーバーとしてバンドルし、query()mcpServers オプションに渡す。ツール名は mcp__{サーバー名}__{ツール名} の形式で allowedTools に指定する。

allowedTools を使えばビルトインツールの有効/無効も細かく制御できる。たとえば allowedTools: ["Read", "Glob", "Grep"] と指定すれば、読み取り専用のエージェントを簡単に作れる。

Taskツールによるサブエージェント委譲

SDKの agents オプションでサブエージェントを定義できる。自律開発パイプラインへの応用を考えると、各ステージ(アイデア生成→scaffold→実装→ビルド→テスト→デプロイ)をサブエージェントとして分離することで、失敗時のリトライ粒度を細かくできる。

typescript
options: {
  agents: {
    "code-reviewer": {
      description: "コード品質レビュー専門エージェント",
      prompt: "コード品質を分析し、改善点を提案してください。",
      tools: ["Read", "Glob", "Grep"],
      model: "haiku",
      maxTurns: 10,
    },
    "builder": {
      description: "ビルド・テスト実行エージェント",
      prompt: "プロジェクトをビルドし、テストを実行してください。",
      tools: ["Read", "Bash", "Edit"],
      model: "sonnet",
    },
  },
}

CLI方式ではこうした分離は別プロセスの起動と結果の文字列パースで実現するしかなかったが、SDKなら宣言的に定義できる。これは地味に便利だ。


ストリーミング制御とセッション管理 — CLI方式では得られなかった制御力

ストリーミングイベントのハンドリング

query()AsyncGenerator<SDKMessage> を返す。メッセージの type フィールドで分岐すれば、エージェントの状態をリアルタイムに把握できる。

typescript
const controller = new AbortController();

// 10分でタイムアウト
setTimeout(() => controller.abort(), 10 * 60 * 1000);

for await (const msg of query({
  prompt: "全テストを実行して結果を報告して",
  options: { abortController: controller },
})) {
  switch (msg.type) {
    case "assistant":
      // エージェントの応答テキストをSlackに中継
      await notifySlack(`Agent: ${JSON.stringify(msg.message.content).slice(0, 200)}`);
      break;
    case "result":
      await notifySlack(`完了: ${msg.subtype}, コスト: $${msg.cost_usd}`);
      break;
  }
}

abortController による中断制御は、CLI方式の child.kill("SIGTERM") → SIGKILLフォールバックのパターンと比べて格段にクリーンだ。

セッション継続(sessionId)によるコンテキスト保持

resume オプションに前回のセッションIDを渡すことで、コンテキストを引き継いだマルチターン会話が可能になる。

typescript
// 1回目: セッション開始
let sessionId: string;
for await (const msg of query({ prompt: "package.jsonを確認して" })) {
  if (msg.type === "system" && msg.subtype === "init") {
    sessionId = msg.sessionId;
  }
}

// 2回目: セッション継続
for await (const msg of query({
  prompt: "さっき確認した依存関係を最新に更新して",
  options: { resume: sessionId },
})) {
  // 前回のコンテキストを保持した状態で実行
}

CLI spawn方式ではstdout/stderrの文字列パースに依存していた部分が、構造化イベントに置き換わることで堅牢性が劇的に向上する。


CLI spawn方式からの移行チェックリスト — 自律開発デーモンを例に

移行時の注意点と判断基準

観点 CLI spawn方式 Agent SDK
型安全性 stdout/stderrは文字列 SDKMessage型で構造化
エラー判定 正規表現パターンマッチ message.type / subtype
ストリーミング data イベントで断片受信 AsyncGeneratorで構造化イベント
許可管理 --dangerously-skip-permissions 一択 permissionMode で段階的制御
カスタムツール 不可 Zodスキーマで型安全に定義
起動コスト CLIのコールドスタートのみ query()ごとに約12秒のオーバーヘッド
マルチターン 都度新規プロセス sessionId / streaming inputで継続可能

query()ごとの約12秒オーバーヘッドは見逃せないコストだ(GitHub Issue #34で報告・確認済み)。ただし、Streaming Input Mode(prompt にAsyncIterableを渡す方式)を使えば初回のみ12秒、以降は2-3秒に短縮される。

CLI方式を残すべきケース

すべてをSDKに移行する必要はない。以下のケースではCLI方式の維持が合理的だ。

  • 単発の長時間タスク: 1回のプロンプトで30分以上動かすようなケース。起動オーバーヘッドが相対的に無視できる
  • 既存パイプラインが安定稼働中: stderrパースやレート制限検出が十分に枯れているなら、動いているものを壊す必要はない
  • 段階的移行のアプローチ: 新規パイプラインからSDKを採用し、既存は安定性を見て順次移行するのが現実的だ

まとめ

Claude Agent SDK(v0.2.x)は、CLIのspawn方式で感じていた「stdoutパースの脆さ」「エラーハンドリングの煩雑さ」「型安全性の欠如」を根本的に解決する。一方でCLI子プロセスモデルゆえの起動オーバーヘッドは存在し、万能ではない。

重要なのは、CLIで培った自律エージェント設計の知見——タスク分割、リトライ戦略、レート制限対応——はSDK移行後もそのまま活きるということだ。新規パイプラインからSDKを採用し、段階的に制御力を高めていくのが現実的なアプローチだろう。

関連リソース

もっと読む他の技術記事も読む