コンテンツにスキップ

Claude Agent SDKでインシデント・コレクターを構築する

このガイドをオフラインで持ち帰る

TypeScriptでAIインシデント・コレクターをClaude Agent SDKを使って構築します。まずAIインシデントを検索・検証する単一エージェントとして。次に、発見と検証をサブエージェントに委任し、その成果を統合するマルチエージェント・システムとして。

何を構築するか

バージョン1 — シングルエージェント:

Single Incident Collector Agent
→ searches for AI incidents
→ verifies evidence
→ writes data/incidents.jsonl
→ writes 01-incident-registry.md

バージョン2 — サブエージェント付きのマルチエージェント:

Incident Collector parent
→ spawns Agent subagents for discovery and verification
→ merges their results
→ writes the final artifacts

両バージョンは同じツール、システムプロンプト、出力形式を共有します。シングルエージェント版が動けば、マルチエージェントへの変換は機械的です。

合計時間:60〜90分(TypeScriptとClaude SDKへの習熟度による)。

前提条件

  • Node.js 20 以降 — node --version で確認
  • TypeScriptの基本知識(型、import、async)
  • Anthropic APIキー、またはVertex / Bedrock経由のClaude — ルーティングオプションはClaude Codeセットアップを参照
  • node_modules 用に約200MBのディスク空き容量

1. プロジェクトを作成する

新規フォルダを作成します。

Terminal window
mkdir claude-agent-sdk-training
cd claude-agent-sdk-training

package.json を作成します。

{
"name": "claude-agent-sdk-training",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "latest"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest"
}
}

インストール。

Terminal window
pnpm install

tsconfig.json を作成します。

{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}

ソースフォルダを作成します。

Terminal window
mkdir -p src

2. SDKをインポートする

src/index.ts を作成します。

SDKインポートから始めます。

import { mkdir } from "node:fs/promises";
import { resolve } from "node:path";
import {
query,
type AgentDefinition,
type Options,
type SDKMessage,
type SDKResultMessage,
} from "@anthropic-ai/claude-agent-sdk";

この処理の役割。

  • query — Claude Agent SDKの実行を一度開始します。

  • Options — モデル、作業ディレクトリ、ツール、権限、プロンプト、サブエージェントを設定します。

  • SDKMessage — エージェントループからストリーミングされるイベントを表します。

  • SDKResultMessage — 実行の最終結果を表します。

  • AgentDefinitionAgent ツールから呼び出せるサブエージェントを定義します。

3. ランタイム設定を追加する

これをimportの下に貼り付けます。

type Mode = "single" | "multi";
interface RunConfig {
mode: Mode;
workspaceDir: string;
outputDir: string;
incidentCount: number;
recencyWindow: string;
model: string;
subagentModel: string;
}
function resolveRunConfig(mode: Mode): RunConfig {
const workspaceDir = resolve(process.cwd(), "outputs", mode);
return {
mode,
workspaceDir,
outputDir: resolve(workspaceDir, "workshop-outputs"),
incidentCount: Number.parseInt(process.env.INCIDENT_COUNT ?? "5", 10),
recencyWindow: process.env.INCIDENT_RECENCY ?? "last 6 months",
model: process.env.CLAUDE_AGENT_SDK_MODEL ?? "opus",
subagentModel: process.env.CLAUDE_AGENT_SDK_SUBAGENT_MODEL ?? "sonnet",
};
}
async function prepareWorkspace(config: RunConfig): Promise<void> {
await mkdir(resolve(config.outputDir, "data"), { recursive: true });
}

この処理の役割。

  • 作業ディレクトリは outputs 配下に隔離します。
  • これはエージェントがファイルを書き込めるため重要です。
  • モデルとインシデント数は環境変数で、コードを編集せず変更できるようにしています。

4. ツールグループを追加する

これを設定の下に貼り付けます。

const SAFE_RESEARCH_TOOLS = [
"WebSearch",
"WebFetch",
"Read",
"Glob",
"Grep",
] as const;
const WRITING_TOOLS = ["Write", "Edit"] as const;

この処理の役割。

  • インシデント・コレクターは証拠を集めるため検索・取得ツールが必要です。
  • 書き込みツールは最終成果物の生成にのみ必要です。
  • 後ほどサブエージェントには研究ツールのみを与え、最終ファイルを書けないようにします。

5. 共有システムプロンプトを追加する

貼り付けます。

function buildSystemAppend(config: RunConfig): string {
return [
"You are the Incident Collector Agent for an AI Incident Analysis and Guardrail Design workshop.",
"Collect only incidents relevant to GenAI, LLM applications, coding agents, agentic workflows, AI infrastructure, or AI development guardrails.",
"Reject broad generic AI incidents that do not teach guardrail lessons for LLM or agent systems.",
"Never invent source URLs, dates, affected organizations, impacts, CVEs, vendors, or statistics.",
"If evidence is weak or conflicting, reject the case or mark confidence clearly.",
`Requested recency window: ${config.recencyWindow}.`,
].join("\n");
}

この処理の役割。

  • これは安定した役割指示です。
  • ユーザータスクは変わってもよいですが、エージェントの身元と証拠ルールは安定しているべきです。

6. シングルエージェント・プロンプトを追加する

貼り付けます。

function buildSingleAgentPrompt(config: RunConfig): string {
return [
`Collect ${config.incidentCount} high-quality incidents for the workshop.`,
"",
"Work method:",
"1. Search for candidate incidents from public sources.",
"2. Fetch and verify source pages before accepting an incident.",
"3. Deduplicate overlapping reports.",
"4. Keep only incidents relevant to LLM, GenAI, coding-agent, or agentic AI guardrails.",
"5. Write the final artifacts yourself.",
"",
"Output files, relative to the current working directory:",
"- `workshop-outputs/data/incidents.jsonl`",
"- `workshop-outputs/01-incident-registry.md`",
"",
"Each JSONL record must include:",
"- id",
"- title",
"- date",
"- sources",
"- affectedOrganization",
"- affectedSystem",
"- aiSystemType",
"- failureCategory",
"- severity",
"- summary",
"- observedImpact",
"- guardrailRelevance",
"- evidenceQuality",
"- tags",
"- downstreamNotes",
"",
"If you cannot find enough verified incidents, write fewer verified incidents and explain the gap. Do not pad with weak or invented incidents.",
].join("\n");
}

この処理の役割。

  • プロンプトは作業内容を記述します。
  • オプションはランタイムの境界を記述します。
  • 設計上、両者は分離しておきます。

7. SDKランナーを追加する

貼り付けます。

const MAX_ASSISTANT_CHARS = 420;
const MAX_TOOL_INPUT_CHARS = 180;
const MAX_TOOL_RESULT_CHARS = 650;
interface ToolCallContext {
toolName: string;
scope: string;
subagentName: string | null;
}
async function runClaudeAgent(
label: string,
prompt: string,
options: Options,
): Promise<SDKResultMessage> {
let result: SDKResultMessage | null = null;
const toolCalls = new Map<string, ToolCallContext>();
logLine(label, "START", "agent run");
for await (const message of query({ prompt, options })) {
if (message.type === "system") {
logLine(label, "SYS", "session initialized");
continue;
}
if (message.type === "assistant") {
logAssistantMessage(label, message, toolCalls);
continue;
}
if (message.type === "user") {
logToolResultMessage(label, message, toolCalls);
continue;
}
if (message.type === "result") {
result = message;
logLine(
label,
"DONE",
`${message.subtype} turns=${message.num_turns} cost=$${message.total_cost_usd.toFixed(4)}`,
);
}
}
if (!result) {
throw new Error(`[${label}] finished without a result`);
}
if (result.subtype !== "success") {
throw new Error(`[${label}] failed: ${result.subtype}`);
}
return result;
}
function logAssistantMessage(
rootLabel: string,
message: Extract<SDKMessage, { type: "assistant" }>,
toolCalls: Map<string, ToolCallContext>,
): void {
const scope = resolveScope(rootLabel, message.parent_tool_use_id, toolCalls);
for (const block of message.message.content) {
if (block.type === "text") {
logLine(scope, "A", truncateOneLine(block.text, MAX_ASSISTANT_CHARS));
continue;
}
if (block.type === "tool_use") {
const subagentName = extractSubagentName(block.input);
const toolScope = subagentName ? `${rootLabel}/${subagentName}` : scope;
toolCalls.set(block.id, {
toolName: block.name,
scope,
subagentName,
});
logLine(scope, "T", formatToolCall(block.name, block.input));
if (block.name === "Agent" && subagentName) {
logLine(toolScope, "AGENT", "delegated");
}
}
}
}
function logToolResultMessage(
rootLabel: string,
message: Extract<SDKMessage, { type: "user" }>,
toolCalls: Map<string, ToolCallContext>,
): void {
if (message.tool_use_result !== undefined) {
const context = message.parent_tool_use_id
? toolCalls.get(message.parent_tool_use_id)
: undefined;
logToolResult(rootLabel, context, message.tool_use_result);
}
const content = message.message.content;
if (Array.isArray(content)) {
for (const block of content) {
if (!isRecord(block) || block.type !== "tool_result") continue;
const toolUseId =
typeof block.tool_use_id === "string" ? block.tool_use_id : null;
const context = toolUseId ? toolCalls.get(toolUseId) : undefined;
logToolResult(rootLabel, context, block.content, block.is_error === true);
}
}
}
function logToolResult(
rootLabel: string,
context: ToolCallContext | undefined,
content: unknown,
isError = false,
): void {
const resultScope = context?.subagentName
? `${rootLabel}/${context.subagentName}`
: context?.scope ?? rootLabel;
const toolName = context?.toolName ?? "tool";
const status = isError ? " error" : "";
const preview = truncateOneLine(
formatToolResultContent(content),
MAX_TOOL_RESULT_CHARS,
);
logLine(resultScope, "R", `${toolName}${status}: ${preview}`);
}
function formatToolCall(toolName: string, input: unknown): string {
if (toolName === "Agent") {
const subagentName = extractSubagentName(input) ?? "agent";
const description = extractString(input, ["description"]);
const suffix = description
? ` ${truncateOneLine(description, MAX_TOOL_INPUT_CHARS)}`
: "";
return `Agent -> ${subagentName}${suffix}`;
}
const summary = summarizeToolInput(input);
return summary ? `${toolName} ${summary}` : toolName;
}
function summarizeToolInput(input: unknown): string {
if (!isRecord(input)) {
return truncateOneLine(formatUnknown(input), MAX_TOOL_INPUT_CHARS);
}
const preferredKeys = [
"query",
"url",
"file_path",
"path",
"pattern",
"command",
];
for (const key of preferredKeys) {
if (typeof input[key] === "string") {
return `${key}=${truncateOneLine(input[key], MAX_TOOL_INPUT_CHARS)}`;
}
}
const firstScalar = Object.entries(input).find(([, value]) =>
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
);
if (!firstScalar) return "";
const [key, value] = firstScalar;
return `${key}=${truncateOneLine(String(value), MAX_TOOL_INPUT_CHARS)}`;
}
function extractSubagentName(input: unknown): string | null {
return extractString(input, [
"subagent_type",
"agent_type",
"agent",
"name",
]);
}
function extractString(input: unknown, keys: string[]): string | null {
if (!isRecord(input)) return null;
for (const key of keys) {
if (typeof input[key] === "string" && input[key].trim().length > 0) {
return input[key];
}
}
return null;
}
function resolveScope(
rootLabel: string,
parentToolUseId: string | null,
toolCalls: Map<string, ToolCallContext>,
): string {
if (!parentToolUseId) return rootLabel;
const context = toolCalls.get(parentToolUseId);
if (!context) return rootLabel;
return context.subagentName
? `${rootLabel}/${context.subagentName}`
: context.scope;
}
function formatToolResultContent(content: unknown): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content.map(formatToolResultContentBlock).join("\n");
}
return formatUnknown(content);
}
function formatToolResultContentBlock(block: unknown): string {
if (!isRecord(block)) return formatUnknown(block);
if (block.type === "text" && typeof block.text === "string") {
return block.text;
}
if (block.type === "image") {
return "[image result]";
}
return formatUnknown(block);
}
function formatUnknown(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function truncateOneLine(value: string, maxChars: number): string {
const compact = value.replace(/\s+/g, " ").trim();
if (compact.length <= maxChars) return compact;
return `${compact.slice(0, maxChars)}...`;
}
function logLine(scope: string, kind: string, message: string): void {
console.info(`[${scope}] ${kind} ${message}`);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

この処理の役割。

  • これはSDKの実行ループです。
  • query() がエージェントを開始します。
  • for await がライブメッセージを受信します。
  • ログは意図的にコンパクトです。
    • START: 実行開始
    • SYS: SDKセッション初期化済み
    • A: アシスタントメッセージ
    • T: ツール呼び出し
    • R: ツール結果
    • AGENT: サブエージェント委任
    • DONE: 最終結果
  • マルチエージェント・モードでは、サブエージェントの行は [multi/incident-verifier] のようなスコープ付きラベルになります。
  • LifeOSAIでは、このストリームがライブ実行メッセージ、ステータス更新、ファイル更新、UIイベントになります。

8. シングルエージェントのメイン関数を構築する

貼り付けます。

async function runSingleAgent(): Promise<void> {
const config = resolveRunConfig("single");
await prepareWorkspace(config);
const options: Options = {
cwd: config.workspaceDir,
model: config.model,
permissionMode: "acceptEdits",
maxTurns: 18,
persistSession: false,
settingSources: [],
systemPrompt: {
type: "preset",
preset: "claude_code",
append: buildSystemAppend(config),
},
tools: [...SAFE_RESEARCH_TOOLS, ...WRITING_TOOLS],
allowedTools: [...SAFE_RESEARCH_TOOLS, ...WRITING_TOOLS],
};
await runClaudeAgent(
"single-incident-collector",
buildSingleAgentPrompt(config),
options,
);
console.info(`\nSingle-agent output: ${config.outputDir}`);
}

重要なオプション。

  • cwd — エージェントの作業ディレクトリ。

  • permissionMode — ツール権限の動作。

  • maxTurns — エージェントループの安全境界。

  • systemPrompt — 安定した役割指示。

  • tools — エージェントが使用できるもの。

  • allowedTools — 自動承認できるもの。

9. エントリーポイントを追加する

末尾にこれを貼り付けます。

runSingleAgent().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});

実行。

Terminal window
pnpm start

確認。

Terminal window
ls outputs/single/workshop-outputs

期待される出力。

01-incident-registry.md
data

10. マルチエージェントへ変換する

この時点で。

  • シングルエージェントは動作しますが、1つのエージェントが発見、検証、書き込みをすべて行っています。
  • 親のインシデント・コレクターは1つのまま残し、専門のサブエージェントを与えます。

サブエージェント定義をエントリーポイントの上に貼り付けます。

function createIncidentCollectorSubagents(
config: RunConfig,
): Record<string, AgentDefinition> {
return {
"incident-database-discovery": {
description:
"Find candidate GenAI, LLM, and agentic AI incidents from incident databases, public reports, vendor posts, and reputable news.",
prompt: [
"You are a discovery subagent for the Incident Collector.",
`Find candidate incidents matching the workshop scope.`,
`Respect this recency window: ${config.recencyWindow}.`,
"Return candidates with title, date, source URLs, affected system, relevance, and evidence quality.",
"Do not write files. Do not invent details.",
].join("\n"),
tools: [...SAFE_RESEARCH_TOOLS],
model: config.subagentModel,
effort: "medium",
maxTurns: 10,
},
"coding-agent-incident-discovery": {
description:
"Find incidents involving coding agents, Claude Code, MCP, autonomous tool use, AI infrastructure, and developer guardrail failures.",
prompt: [
"You are a specialist discovery subagent for coding-agent and agentic development incidents.",
"Search for AI coding tools, autonomous agents, MCP/tool execution, secrets exposure, supply chain failures, production damage, prompt injection, and data exfiltration.",
`Respect this recency window: ${config.recencyWindow}.`,
"Return candidate incidents with source URLs, observed impact, and guardrail relevance.",
"Do not write files. Do not invent details.",
].join("\n"),
tools: [...SAFE_RESEARCH_TOOLS],
model: config.subagentModel,
effort: "medium",
maxTurns: 10,
},
"incident-verifier": {
description:
"Verify candidate incidents for source validity, scope fit, duplicate overlap, dates, impact claims, and workshop usefulness.",
prompt: [
"You are the verification subagent for the Incident Collector.",
"Verify source URLs where possible.",
"Reject weak evidence, out-of-scope incidents, and duplicates.",
"Return accepted, rejected, and uncertain lists.",
"Do not write files.",
].join("\n"),
tools: [...SAFE_RESEARCH_TOOLS],
model: config.subagentModel,
effort: "high",
maxTurns: 12,
},
};
}

この処理の役割。

  • 各サブエージェントには役割があります。
  • 各サブエージェントは独自のプロンプトとツールアクセスを持ちます。
  • 発見サブエージェントはファイルを書き込めません。
  • 親が最終成果物を所有します。

11. マルチエージェント・プロンプトを追加する

貼り付けます。

function buildMultiAgentPrompt(config: RunConfig): string {
return [
`Collect ${config.incidentCount} high-quality incidents for the workshop using your subagents.`,
"",
"Required orchestration:",
"1. Use the `incident-database-discovery` Agent for broad incident discovery.",
"2. Use the `coding-agent-incident-discovery` Agent for coding-agent and agentic AI incidents.",
"3. Merge candidate lists yourself.",
"4. Use the `incident-verifier` Agent to verify sources, dates, scope fit, duplicates, and evidence quality.",
"5. Write only the verified final artifacts yourself.",
"",
"Subagents should return candidate or verification notes only. They should not write final output files.",
"",
"Output files, relative to the current working directory:",
"- `workshop-outputs/data/incidents.jsonl`",
"- `workshop-outputs/01-incident-registry.md`",
"- `workshop-outputs/collection-method.md`",
].join("\n");
}

この処理の役割。

  • マルチエージェント・システムにはオーケストレーション・ルールが必要です。
  • ただサブエージェントを作って連携を期待するだけではいけません。
  • 親プロンプトが順序と最終所有権を定義します。

12. マルチエージェントのメイン関数を追加する

貼り付けます。

async function runMultiAgent(): Promise<void> {
const config = resolveRunConfig("multi");
await prepareWorkspace(config);
const options: Options = {
cwd: config.workspaceDir,
model: config.model,
permissionMode: "acceptEdits",
maxTurns: 25,
persistSession: false,
settingSources: [],
systemPrompt: {
type: "preset",
preset: "claude_code",
append: buildSystemAppend(config),
},
tools: ["Agent", ...SAFE_RESEARCH_TOOLS, ...WRITING_TOOLS],
allowedTools: ["Agent", ...SAFE_RESEARCH_TOOLS, ...WRITING_TOOLS],
agents: createIncidentCollectorSubagents(config),
};
await runClaudeAgent(
"multi-agent-incident-collector",
buildMultiAgentPrompt(config),
options,
);
console.info(`\nMulti-agent output: ${config.outputDir}`);
}

シングルエージェント専用のエントリーポイントを、次のものに置き換えます。

const mode = process.argv.includes("--multi") ? "multi" : "single";
const run = mode === "multi" ? runMultiAgent : runSingleAgent;
run().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});

シングル実行。

Terminal window
pnpm start

マルチ実行。

Terminal window
pnpm start -- --multi

確認。

Terminal window
ls outputs/multi/workshop-outputs

期待される出力。

01-incident-registry.md
collection-method.md
data

13. チェックポイント:シングル vs マルチエージェント

両バージョン完了後、コードと出力を比較してください。

  • Agent ツールを追加しました。
  • agents レジストリを追加しました。
  • サブエージェントは独自のプロンプトを持ちました。
  • サブエージェントは制限されたツールを得ました。
  • 親は最終的な書き込み責任を保持しました。
  • 検証が明示的なため、出力はより監査しやすくなっているはずです。

14. なぜこれがLifeOSAIにとって重要か

Claude Agent SDKは実行プリミティブを提供します。LifeOSAIはこれを運用システムに変えます。

Claude Agent SDK
-> one agent loop
-> streamed SDK messages
-> tools
-> subagents
LifeOSAI
-> company agents
-> assigned tasks
-> issue comments
-> file artifacts
-> dashboards
-> live events
-> audit trail

ここで構築したインシデント・コレクターは、より大きなワークショップ・プロトタイプの最初のエージェントです。

これ以降、LifeOSAIは生成されたインシデント・コーパスの周りに、Root Cause、Threat Modeling、Guardrail Design、Policy-as-Code、Claude Hooks、Evidence、Criticのエージェントを連携させることができます。

公式リファレンス

次に読む

次: Day 2 · オーケストレーション + 9エージェント →