Workflow ツール:スクリプトによるマルチエージェントオーケストレーション
Claude Code の Workflow ツールを解説 — agent()、pipeline()、parallel()、スキーマ、resume を備えた、サブエージェントの決定論的 JavaScript オーケストレーション。
学ぶこと
Fable 5 時代における最大のアーキテクチャ上の追加が Workflow ツール(ワークフロー)です。Claude Code がプレーンな JavaScript のオーケストレーション(orchestration、複数エージェントの指揮・編成)スクリプトを書き、ハーネスがそれを実行します — 多数のサブエージェントを、モデルの即興ではなくコードとしてのループ・条件分岐・ファンアウトで協調させるのです。
このセッションを終えると、次のことがわかります:
- なぜ大規模になると、スクリプトによるオーケストレーションが即興的な委譲に勝るのか
metaブロックのルールとプリミティブ群:agent()、pipeline()、parallel()、phase()、log()、args- JSON Schema がサブエージェントから検証済みの構造化出力を強制的に引き出す仕組み
pipeline()とparallel()の使い分けを決める「バリア規律」- ハードリミット、禁止された非決定性、そして resume(再開)の仕組み
課題
すでに2つの委譲モデルを知っているはずです。セッション 4では、メインエージェントが1回に1つずつ判断しながらサブエージェントを起動しました — 結果を読み、考え、次を起動する。セッション 9では、エージェント同士がメッセージで協調しました。どちらも即興です:制御フローがモデルのワーキングメモリの中に存在しています。
即興は規模が大きくなると破綻します。エージェントに「変更された40ファイルすべてをレビューし、すべての指摘を検証し、途中でやめないこと」と頼むのは、言語モデルが結果をコンテキストの中で管理しながら40回のループを実行してくれると信頼することです。逸脱するかもしれないし、重複排除が一貫しないかもしれないし、28ファイル目で静かに止まるかもしれません。モデルは判断 — これはバグか?この証拠は成立するか? — には優れていますが、帳簿付けには不向きです。
コードは正反対のプロファイルを持ちます。for ループは28回目の反復で疲れたりしません。だから Fable 5 時代は、仕事を分割します:
Improvised delegation Scripted orchestration
───────────────────── ──────────────────────
Model decides each step Script decides each step
Loop = model discipline Loop = JavaScript
Fan-out = model memory Fan-out = pipeline(items, ...)
Hard to reproduce Deterministic, resumable
仕組み
コアモデル
メインエージェントがプレーンな JavaScript スクリプト(TypeScript ではありません)を書きます。ハーネスはそれをバックグラウンドで実行し、タスク ID を返します。完了時には通知が届き、/workflows でライブの進捗を確認できます。
スクリプト本体は、標準の JS 組み込み機能が使える非同期コンテキストで実行されます。ただし、意図的に欠けているものが2つあります:
- スクリプト自体にはファイルシステムも Node.js API もありません。 ツールを持つのはエージェントであって、スクリプトではありません。世界に触れる操作はすべて
agent()を通します。 - 非決定性は禁止。
Date.now()、Math.random()、引数なしのnew Date()はすべて例外を投げます。理由は「決定論と Resume」の節で説明します。
meta ブロック
すべてのスクリプトは、次の記述で始まらなければなりません:
export const meta = {
name: 'review-changes',
description: 'Review changed files, verify findings, summarize',
phases: [
{ title: 'Collect' },
{ title: 'Review' },
{ title: 'Report' },
],
};
meta ブロックは純粋なリテラルでなければなりません — 変数も、関数呼び出しも、スプレッドも禁止です。そして phases のタイトルは本体の phase() 呼び出しと一致している必要があります。これによってハーネスが正確な進捗を表示できるのです。
プリミティブ
agent(prompt, opts?) はサブエージェントを起動し、その最終テキストを返します。オプションは label、phase、schema、model、effort、isolation: 'worktree'、agentType。ユーザーがエージェントをスキップした場合や、回復不能な API エラーで終了した場合は null を返します — 必ずこれをハンドリングしてください。model(sonnet | opus | haiku | fable)と effort(low から max)の上書きにより、ティアを混在させられます:機械的なステージには安価なモデルと低い effort、検証と統合には最上位ティアと高い effort(セッション 26参照)。
isolation: 'worktree' はエージェントに新しい git worktree を与えます — コストが高い(エージェントあたりおよそ 200〜500 ms のセットアップに加えてディスク消費)ため、エージェントが並列にファイルを変更する場合にのみ使いましょう。変更のなかった worktree は自動的に削除されます。
pipeline(items, stage1, stage2, ...) は各アイテムをすべてのステージに独立して、ステージ間のバリアなしで通します。アイテム B がまだステージ 1 にいる間に、アイテム A はステージ 3 にいることができます。これが多段階の作業のデフォルトです。実時間(wall-clock time)は最も遅い単一アイテムのチェーンに等しく、ステージ全体の待ち時間の合計にはなりません。
parallel(thunks) はサンク(thunk、遅延実行する関数)をバリア付きで並行実行します — すべての完了を待つのです。例外を投げたサンクは null に解決される(この呼び出しは決して reject しない)ため、結果は .filter(Boolean) でフィルタしてください:
const results = (await parallel([
() => agent('Check the API layer for the bug'),
() => agent('Check the data layer for the bug'),
])).filter(Boolean);
phase(title) と log(message) は進捗をグループ化し、ユーザーに向けて経過を語ります。args は Workflow 呼び出しに渡された値を運ぶので、スクリプトをパラメータ化できます。workflow(nameOrRef, args?) は別のワークフローをインラインで実行します — 名前で保存されたものでも、スクリプトパスでも指定可能 — ただしネストは1段階のみで、子は親の並行数上限、中断シグナル、バジェットを共有します。
スキーマによる構造化出力
opts.schema(JSON Schema)を渡すと、agent() は自由テキストの代わりに検証済みオブジェクトを返します:サブエージェントは StructuredOutput ツールを強制的に経由し、不一致時にはツール呼び出し層でリトライされます。これこそがスクリプトによる制御フローを実用にするものです — files の存在と配列であることが保証されているからこそ、スクリプトは result.files.length で分岐できるのです。
pipeline と parallel:バリア規律
オーケストレーションで最もよくある間違いは、不要なバリアを挿入することです。ルールはこうです:
バリアが正当化されるのは、ステージ N がステージ N−1 のすべてからアイテム横断のコンテキストを必要とする場合のみ。
正当なバリアの例:全体セットをまたぐ重複排除やマージ、合計がゼロのときの早期終了、「これを他の指摘と比較せよ」というプロンプト。
臭いを嗅ぎ分けるテスト:「先に flatten/map/filter が必要」や「ステージが概念的に別物」はバリアを正当化しません — その変換は pipeline のステージ内で行ってください。正当化されないバリアはひとつ増えるごとに、バッチ全体を最も遅いメンバーに合わせて待たせます。
決定論と Resume
なぜ Date.now() と Math.random() を禁止するのか?すべての実行がスクリプトを永続化し、resumeFromRunId で再起動すると、変更のない最長のプレフィックスに含まれる agent() 呼び出しがキャッシュから再生されるからです — ライブで実行されるのは編集された呼び出しと新しい呼び出しだけ。トランスクリプトディレクトリの journal.jsonl が、各エージェントの実際の戻り値を記録します。もしスクリプトが再生時に異なる値を生み出したら、キャッシュされたプレフィックスは現実から乖離してしまいます。決定論こそが、大きなワークフローの修正を安価にするのです:壊れたステージを編集して resume すれば、その前段はすべてキャッシュから供給されます。
制限
- ワークフローあたりの並行エージェント数:min(16, CPU コア数 − 2)。超過分の呼び出しはキューに入ります。
- 生涯上限:ワークフローあたり 1,000 エージェント — 暴走ループに対する安全弁です。
- 単一の
pipeline()またはparallel()呼び出し:最大 4,096 アイテム。静かな切り捨てではなく、明示的なエラーとして強制されます。
保存されたワークフローとオプトイン
名前付きワークフローは .claude/workflows/ に置かれ、名前で呼び出せます。そしてワークフローはオプトインです:明示的なシグナル — 「ultracode」キーワード、プロンプト内の「ワークフローを使って」、あるいはそれを指示するスキル — がない限り、Claude Code は個別のサブエージェントを使うか、単独で作業します。
ハンズオン
review-changes ワークフローの標準形を、1行ずつ読み解いていきましょう:
export const meta = {
name: 'review-changes',
description: 'Review changed files, verify findings, summarize',
phases: [
{ title: 'Collect' },
{ title: 'Review' },
{ title: 'Report' },
],
};
phase('Collect');
const changes = await agent(
'List the source files changed on this branch relative to main ' +
'(git diff --name-only). Return only files that exist.',
{
label: 'collect-changes',
phase: 'Collect',
model: 'haiku',
effort: 'low',
schema: {
type: 'object',
properties: {
files: { type: 'array', items: { type: 'string' } },
},
required: ['files'],
},
}
);
if (!changes || changes.files.length === 0) {
log('No changed files — nothing to review.');
} else {
phase('Review');
log(`Reviewing ${changes.files.length} changed files`);
const reviews = await pipeline(
changes.files,
(file) => agent(
`Review ${file} for correctness bugs introduced on this branch. ` +
'For each suspected bug, give the location and a concrete failure scenario.',
{ label: `review:${file}`, phase: 'Review', model: 'sonnet' }
),
(review) => review && agent(
`A reviewer reported:\n${review}\n\nTry to REFUTE each finding by ` +
'reading the actual code. Return only findings that survive.',
{ label: 'verify', phase: 'Review', effort: 'high' }
)
);
phase('Report');
const confirmed = reviews.filter(Boolean);
const report = await agent(
'Merge these verified review results into one report, deduplicating ' +
`overlapping findings:\n${confirmed.join('\n---\n')}`,
{ label: 'summarize', phase: 'Report', model: 'fable', effort: 'high' }
);
log(report || 'Summary agent returned nothing.');
}
meta ブロックは純粋なリテラルで、3つのフェーズタイトルは3つの phase() 呼び出しと正確に一致しています。
Collect はスキーマを使うので、スクリプトが受け取るのは解析すべき散文ではなく { files: [...] } です。機械的なリストアップ作業には haiku を low effort で。!changes ガードが重要です:エージェントがスキップされたり終了したりすると、agent() は null を返すからです。
Review はバリアなしの2ステージ pipeline() です — あるファイルのレビューが終わった瞬間、他のファイルがまだレビュー中でも、そのファイルの検証が始まります。verify ステージは各指摘を、反駁を試みるよう指示された懐疑役に high effort で渡します。トークンを費やすべき場所は判断だからです。review && ガードは、クラッシュする代わりに null をチェーンの先へ伝播させます。
Report はバリアが正当化される唯一の場所です:マージと重複排除には検証済み結果のすべてが必要 — そして pipeline を await することが、まさにそのバリアを提供します。統合には最上位ティアの fable モデルを充てます。そしてここには非決定的なものが何もないので、後で summarize プロンプトの調整が必要になっても、resume は Collect と Review をキャッシュから再生し、最後のエージェントだけを再実行します。
このスクリプトでは指摘ごとに懐疑役を1人しか使っていません。セッション 28で、検証を本格的にスケールアップします。
何が変わったか
| 即興的な委譲(s04/s09) | スクリプト化されたワークフロー(Fable 5 時代) |
|---|---|
| 制御フローはモデルのコンテキストの中 | 制御フローは決定論的な JavaScript の中 |
| ファンアウトはモデルの帳簿付け能力が上限 | pipeline() で最大 4,096 アイテム |
| 抜け落ちた反復は気づかれない | ループはアイテムをスキップできない |
| 失敗した実行 = 最初からやり直し | Resume はキャッシュ済みプレフィックスを再生し、残りだけ再実行 |
| 自由テキストの結果を、祈りながらパース | スキーマで検証された構造化出力 |
| すべてに単一のモデルティア | エージェントごとの model と effort の上書き |
サブエージェントとエージェントチームが消えたわけではありません — 探索的な作業や小規模な作業では、依然としてそれらが正しい道具です。作業の形が既知で、規模が大きいときに、ワークフローが引き継ぐのです。
キーインサイト
制御フローはコードに、判断はモデルに。 この一文が設計のすべてです。
モデルが大規模では苦手なこと — 数えること、ループすること、途中でやめないこと、40件すべての結果を覚えていること — はすべて、それらを完璧にこなす JavaScript が担います。コードにできないこと — diff がバグかどうかを判断する、もっともらしい指摘に反駁する、レポートを統合する — はすべて agent() 呼び出しに委譲され、各呼び出しにはそのステージにふさわしいモデルティアと effort が与えられます。
最初は窮屈に感じるルール(純粋リテラルとしての meta、Math.random() 禁止、ファイルシステムアクセスなし)が、保証を買っているのです:再現可能な実行、キャッシュに裏打ちされた resume、そして混沌なき並列性を。
次のセッション
セッション 28 ではオーケストレーションの品質パターン — これらのプリミティブの上に築かれる標準的な構造 — を扱います:敵対的検証、判定パネル、枯れるまでループ、マルチモーダルスイープ、網羅性クリティック。回答の品質とは、実はモデルだけでなくハーネスの構造的な性質なのです。