Subagent を本格的に並列運用しはじめると、ほぼ全員が同じ壁にぶつかります。「親エージェントと子エージェントの間で、どうやって状態を渡せばいいのか」という壁です。Claude Code の Agent ツールはステートレスで、子エージェントは渡されたプロンプトだけを手がかりに動きます。親が知っている文脈を共有しないと、子は同じ調査をゼロからやり直してしまいます。
私自身、2014年から個人開発でアプリ事業を続けており、ここ数年はメディア運営も並行しています。Claude Lab を含む 4 サイトのブログ自動生成パイプラインを 4 ヶ月運用してきて、状態の引き渡しだけで何度も書き直しが発生しました。今回はその過程で残った 3 つのパターンと、どのスケール域でどれを選ぶべきかをまとめます。
なぜ Subagent の状態管理は難しいのか
Claude Code の Subagent モデルは、Unix のサブシェルに似ています。親プロセスが子を fork するわけではなく、純粋にプロンプト文字列を渡して新しいセッションを起動します。子セッションは独立したコンテキストウィンドウを持ち、終了時に最終メッセージだけを親に返します。
この設計は安全性とコンテキスト分離という意味では非常に良いのですが、副作用として「親から渡せる情報量にはプロンプトサイズの上限がある」という制約が生まれます。私が運用しているブログ自動生成パイプラインでは、1 サイトあたりの記事生成プロセスで以下の情報を子に渡す必要があります。
- 過去 14 日に生成した記事のスラッグ一覧(重複回避用)
- 直近の GSC データ(クリック数・表示回数・CTR)
- カテゴリ別の記事数バランス
- 進行中の実体験題材(AdMob / Crashlytics / Xcode などの観察ログ)
- 4 サイトの相互参照を避けるための除外スラッグ集
これを全部プロンプトに直書きすると、子セッションの起動時点でコンテキストが膨れあがり、肝心の記事執筆に使えるトークンが減ってしまいます。実測で 3 万トークン中、状態渡しだけで 1.2 万トークンを消費していた時期がありました。これではプレミアム記事に必要な「実用性シグナル 3 つ以上」を入れる余裕が残りません。
パターン1: JSON ファイル経由(推奨レンジ: 〜100件/日)
最初に採用したのが、親エージェントが JSON ファイルに状態を書き出し、子エージェントが Read ツールで読み出すパターンです。
実装コード
// 親側: state を JSON で永続化
import { writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
type PipelineState = {
pipelineId: string;
timestamp: number;
recentSlugs: string[];
gscData: Record<string, { clicks: number; impressions: number }>;
excludedSlugs: string[];
categoryBalance: Record<string, number>;
};
const STATE_DIR = '/tmp/claude-pipeline-state';
function writeState(state: PipelineState): string {
const path = join(STATE_DIR, `${state.pipelineId}.json`);
// 原子的書き込み: tmp に書いてから rename
const tmpPath = `${path}.tmp.${process.pid}`;
writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
require('fs').renameSync(tmpPath, path);
return path;
}
function readState(pipelineId: string): PipelineState {
const path = join(STATE_DIR, `${pipelineId}.json`);
return JSON.parse(readFileSync(path, 'utf-8'));
}
// 子エージェントに渡すプロンプトには「パス」だけを書く
const childPrompt = `
重複回避のため、まず以下のパスから状態を Read してください:
${writeState(currentState)}
その後、トピック選定 → 記事生成 → JSON への結果書き戻しを行ってください。
`;
4ヶ月運用での観察
このパターンのオーバーヘッドは、Linux サンドボックス内で実測 12ms でした。プロンプトに直書きする方式と比べてトークン消費は 1.2 万 → 0.4 万まで圧縮できました。トークン換算で月 30 ドル前後の節約になります(Sonnet 4.6 の入力単価で計算)。
ただし運用 2 ヶ月目に問題が発生しました。Cowork のスケジュールタスクで複数サイトの生成が時間的に重なったとき、同じ JSON ファイルに対する同時書き込みが発生し、片方の更新が失われる事象が 1 週間で 3 件起きました。
対処として上記コードのように tmp ファイル経由の rename による原子的書き込みに切り替えました。Linux の rename(2) は同一ファイルシステム内で原子的に動くため、これだけで競合は解消できます。
このパターンが向く場面
私の場合、1 サイトあたり 1 日 4 本のペースでは JSON 方式が最も扱いやすく、現在もこのパターンを採用しています。状態が 100KB を超えない範囲・同時実行が 4 並列程度まで・ファイルシステムが永続的に使える環境では、最初の選択肢として推奨します。
パターン2: 環境変数経由(推奨レンジ: 小さく速い参照)
次に試したのは、起動時の環境変数で短い識別子だけを子に渡し、本体の状態は KV ストアから読ませるパターンです。Cloudflare Workers KV を組み合わせて使う場面で重宝します。
実装コード
# 親側: pipelineId だけを env で渡し、状態本体は KV に置く
PIPELINE_ID="run-$(date +%s)-${RANDOM}"
wrangler kv key put --binding=PIPELINE_STATE \
"${PIPELINE_ID}" "$(cat current_state.json)" \
--remote
# 子に渡すプロンプトは環境変数の参照だけ
export CLAUDE_PIPELINE_ID="${PIPELINE_ID}"
claude-code -p "$(cat <<EOF
環境変数 CLAUDE_PIPELINE_ID から ID を取得し、
以下のエンドポイントで状態を取得してください:
https://internal.example.com/state/\${CLAUDE_PIPELINE_ID}
EOF
)"
落とし穴: 環境変数の 32KB 制限
最初は「環境変数に状態を直接シリアライズして渡せば、ファイル I/O すら省ける」と考えました。実測でオーバーヘッドは 0.3ms と非常に小さく、ベンチマーク上は理想的に見えました。
ところが本番運用で 32KB 前後に達したとき、子プロセスが起動直後にクラッシュする事象が出ました。Linux カーネルの ARG_MAX には実質的な上限があり、環境変数を含む argv 全体が 128KB(ディストリビューションにより異なる)を超えると E2BIG で失敗します。状態が大きくなる可能性がある場面では、環境変数には ID だけを入れて本体は別ストアに置くべきです。
このパターンが向く場面
少量の参照情報(ユーザー ID・実行 ID・トークン残量)を子に伝えたいときに最適です。私は GSC データキャッシュの世代番号を環境変数で渡し、子エージェントが「キャッシュが古ければ再取得」を判断する用途で使っています。
パターン3: Context Bundle(推奨レンジ: 1,000件超の長期パイプライン)
3 つ目は、状態を Markdown 形式でひとつの「Context Bundle」にまとめ、毎回 Subagent のプロンプトに添付するパターンです。Anthropic 自身が Skills で推奨している、参照ドキュメントを丸ごと添付するスタイルに近い設計です。
実装コード
// 親側: 状態を読みやすい Markdown に整形
function buildContextBundle(state: PipelineState): string {
return `
# Pipeline Context (生成: ${new Date(state.timestamp).toISOString()})
## 過去14日に生成済みのスラッグ(重複回避)
${state.recentSlugs.map(s => `- ${s}`).join('\n')}
## 現在のカテゴリ別本数バランス
${Object.entries(state.categoryBalance)
.sort((a, b) => b[1] - a[1])
.map(([cat, count]) => `- ${cat}: ${count}件`)
.join('\n')}
## 直近 GSC データ(上位10件のみ)
${Object.entries(state.gscData)
.sort((a, b) => b[1].clicks - a[1].clicks)
.slice(0, 10)
.map(([slug, data]) => `- /${slug}: ${data.clicks} clicks, CTR ${(data.clicks/data.impressions*100).toFixed(1)}%`)
.join('\n')}
## 除外スラッグ(4サイト相互参照禁止)
${state.excludedSlugs.slice(0, 50).map(s => `- ${s}`).join('\n')}
${state.excludedSlugs.length > 50 ? `\n(他 ${state.excludedSlugs.length - 50} 件は省略)` : ''}
`;
}
// 子に渡すプロンプトに丸ごと添付
const childPrompt = `
${buildContextBundle(state)}
---
上記のコンテキストを踏まえて、新規プレミアム記事のトピックを選定してください。
`;
オーバーヘッドは大きいが代替不能な利点がある
実測でこのパターンのオーバーヘッドは 380ms、トークン消費も他のパターンより約 3 倍多くなります。それでも採用する価値があるのは、子エージェントが「状態を読みに行く動作」を一切しなくて済むという点です。Read ツールの呼び出しもネットワーク I/O もなく、起動直後からトピック選定の思考に集中できます。
私は週次の「振り返り記事」生成にこのパターンを使っています。週次の振り返りでは過去 1 週間の全記事のメタデータを参照する必要があり、データ量が大きい代わりに実行頻度が低いため、トークンコストよりも子エージェントの推論精度を優先する判断です。
注意点: トークン爆発のリスク
ある月に Context Bundle のサイズ管理を忘れて月次振り返りを動かしたところ、1 回の Subagent 呼び出しで 4.5 万トークンを消費する事故が起きました。本番運用では必ず以下のガードを入れています。
const MAX_BUNDLE_TOKENS = 8000; // 概算: 1トークン ≒ 4文字
const MAX_BUNDLE_CHARS = MAX_BUNDLE_TOKENS * 4;
function buildContextBundleSafe(state: PipelineState): string {
const bundle = buildContextBundle(state);
if (bundle.length > MAX_BUNDLE_CHARS) {
// 古いスラッグから順に削る
const trimmedState = {
...state,
recentSlugs: state.recentSlugs.slice(0, 30),
excludedSlugs: state.excludedSlugs.slice(0, 20),
};
return buildContextBundle(trimmedState);
}
return bundle;
}
3パターンの比較表
実運用で得た数値をまとめます。
- JSON ファイル: オーバーヘッド 12ms / 適正スケール 〜100件・日 / 同時実行注意 / コスト: 親側 I/O のみ
- 環境変数: オーバーヘッド 0.3ms / 適正スケール ID・小トークン / 32KB 上限あり / コスト: ほぼゼロ
- Context Bundle: オーバーヘッド 380ms / 適正スケール 1,000件超・長期 / トークン爆発注意 / コスト: トークン約 3 倍
どれを選ぶか — 私の判断軸
新しいパイプラインを設計するとき、私は次の順で考えます。
- 状態が 1KB 未満で「ID と数個のフラグ」だけなら、迷わず環境変数を使います
- 状態が 1KB〜100KB で同時実行が 4 並列以下なら、JSON ファイル方式を選びます
- 状態が 100KB を超えるか、子エージェントが状態を「思考の出発点」として扱う必要があるなら、Context Bundle を使います
スケールが伸びてきたら、JSON ファイルから Context Bundle にハイブリッドで移行する設計を取っています。たとえば日次パイプラインはずっと JSON 方式のまま、月次の集計だけ Context Bundle に切り替える、という分け方です。
本番運用で得た 3 つの教訓
最後に、4 ヶ月の運用で書き留めておいたメモから 3 つだけ共有します。
最初の教訓は、状態の引き渡し方式を「最初から1つに決め打ちしない」ことです。スケジュールタスクで同時並列が起きるかどうか、状態量が今後どう成長するかは、走らせてみないと分かりません。3 パターンを切り替えられる薄い抽象層を 1 枚かませておくと、後で破壊的変更を避けられます。
2 つ目は、状態の永続化と TTL を必ずセットで考えることです。私は最初、JSON ファイルを /tmp に置いて TTL を設定し忘れ、VM 再起動の度に状態が消えて重複記事を生成してしまったことがあります。永続化が必要なら Dropbox 同期ディレクトリや KV ストアを使うべきです。
3 つ目は、子エージェントが状態を「読まずに済む設計」が思考精度を最も上げる、ということです。私のメインのアプリ事業では累計5,000万DLを超えており、自動化が常識のように受け入れられている領域です。それでも子エージェントが「最初の 1 秒で何を考えるか」を整える価値は、いまだに想像以上に大きいと感じています。
同じパイプラインを設計している方の参考になれば幸いです。お読みいただきありがとうございました。