前回の記事では、ひとつの agent を信頼できるようにする層を扱いました。run loop、tool routing、context compaction、クラッシュしても壊れない session です。あの層が答えるのは「単体の agent がどうタスクを最後まで走り切るか」でした。今回はその上の層、ひとつの agent では足りず、仕事をチームへ分けるときに何が起きるかを見ます。
これが一般に multi-agent orchestration と呼ばれるものです。会話を所有する主 agent が依頼を分解し、それぞれの断片を専門のサブ agent に渡し、必要な前工程が終わるまで待ち、結果を後段へ渡し、どこかが失敗したときも全体を軌道に戻します。Orkas では、この編成層もユーザー自身のマシン上で動きます。
主 Agent とサブ Agent
心の中のモデルは、指揮系統のはっきりした小さなチームです。主 agent、Orkas では commander と呼ぶものが、会話と全体文脈を持ちます。主 agent は全部を自分でやるのではなく、何を、どの順番で、誰がやるべきかを決めます。サブ agent は専門家です。それぞれが独自の system prompt、許可された tools、skills を持ち、特定の作業片を担当します。
ここで重要なのは二点です。第一に、サブ agent と skills はプロンプト上の小技ではなく、一級の単位です。サブ agent は個別に設定された実体であり、そこへの dispatch は独立した文脈を持つ本当の引き渡しです。第二に、調整をモデルの善意に任せません。システムが正しさを保つ明示的な成果物、つまり plan が中心になります。
計画はスクリプトではなくグラフ
主 agent が依頼に複数ステップが必要だと判断すると、plan を書きます。これは自由文でも直線的な checklist でもなく、小さな依存グラフ、つまり DAG です。各 node は step で、dispatch に必要な情報を持ちます。
interface PlanStep {
index: number; // 1 始まりで安定し、再番号付けしない
title: string; // UI に出す人間向けのタイトル
assignee: string; // "user" | "commander" | サブ agent
input?: string; // dispatch payload、テンプレート
wait_for?: number[]; // 依存する前段 step。既定は [index - 1]
on_failure?: "abort_plan" | "continue" | "ask_commander";
status: "pending" | "in_progress" | "done" | "failed" | "skipped" | "blocked";
output_summary?: string;
output_files?: string[];
failure_reason?: string;
}wait_for が list を graph に変えます。通常は前の step を待つだけですが、「市場調査」と「競合調査」の両方を待ってから「要約」を書く、といった菱形の依存も表せます。
真実を持つのはモデルではなく Orchestrator
モデルが最初に埋めるのは意図です。title、assignee、input、依存関係です。一方で、status、output_summary、failure_reason のような実行状態は orchestrator だけが書きます。モデルは計画を提案しますが、自分の step を勝手に done にできません。
この境界が設計上もっとも大切です。言語モデルは、失敗した step を平気で「完了」と言ってしまうことがあります。長い会話の途中で未完了の step を見失うこともあります。状態をモデルの頭の中に置くと、計画は現実からずれていきます。実行状態を runtime が持つ構造化 artifact にすると、plan は実際に起きたことの鏡であり続けます。
Ready な step を dispatch する
plan ができると、executor が進行を管理します。中核操作は「ready な step を見つけて dispatch する」ことです。step がまだ pending で、依存するすべての step が十分成功した終端状態に達しているとき、その step は ready になります。
function findReadySteps(plan): PlanStep[] {
return plan.steps.filter((s) => {
if (s.status !== "pending") return false;
const deps = s.wait_for ?? (s.index > 1 ? [s.index - 1] : []);
return deps.every((d) => isTerminal(plan.step(d).status)); // done or skipped
});
}これは timer で走るのではなく、event に反応します。サブ agent が返ってきた、commander がまとめを終えた、というたびに executor が reconcile します。終わった step の結果を記録し、それによって新しく ready になった step を探し、次を dispatch します。
Dispatch は同じ会話を通る
Orkas では step の dispatch に隠れた RPC を使いません。ユーザーが見ている同じ group conversation に、主 agent からサブ agent への @mention として投稿します。サブ agent から見ると、dispatch されたことは chat で呼ばれたことと同じです。実行経路が二つに分かれないので、片方だけがずれる心配がありません。
assignee は三種類です。サブ agent の場合は commander から @<agent> <rendered input> が送られます。user の場合は form や質問として plan が止まり、回答を待ちます。commander 自身の場合は、収集済み文脈を持って内部的に一 turn 走り、まとめや判断を行います。
Step 間で文脈を渡す
チームが意味を持つのは、仕事がメンバー間で流れるからです。その仕組みが input テンプレートです。主 agent が step を書くとき、input は固定文字列ではありません。前段の結果を参照し、dispatch 時に executor が展開します。
// step 3.input
"以下の調査結果を使って、ローンチ告知文を下書きしてください。\n\n{{step_1.output_summary}}"
// dispatch 時にサブ agent が受け取るもの
"以下の調査結果を使って、ローンチ告知文を下書きしてください。\n\n- 市場は前年比約20%で成長、主要競合が2社..."渡すのは完全な transcript ではなく、output_summary という短い要約です。前段の全履歴を後段へ渡すと context はすぐ膨らみ、コストも増えます。要約なら、必要な情報だけを安く渡せます。最初のユーザー依頼と添付も運ばれるので、数 step 後の agent も元の目的を失いません。
既定は直列、その理由
複数の branch が同時に ready になったら、すべて並列に走らせたくなります。Orkas は現時点では、ready な step を index 順にひとつずつ dispatch します。保守的ですが、理由は concurrency 下の正しさです。
二つのサブ agent がほぼ同時に終わると、二つの reconcile が同じ plan を読み、同じ下流 step が pending だと見て、同じ step を二重 dispatch してしまう可能性があります。これを防ぐため、ひとつの conversation に対する read-modify-dispatch は per-conversation lock の下で直列化します。
planLock(uid, cid).runExclusive(async () => {
const plan = await readPlan(uid, cid);
applyOutcomeOfFinishedStep(plan);
await dispatchReady(plan);
});この lock により、「何が終わったかを記録する」と「次を決める」が不可分になります。並列 fan-out はこの上に乗せられる最適化ですが、土台はまず race-free であるべきです。
Step が失敗したら
外部モデル API と実マシン上で動く以上、失敗は普通に起こります。orchestrator はまず transient error かどうかを見ます。接続断、rate limit、短い障害なら、retry budget の範囲で step を pending に戻し、次の reconcile で再 dispatch します。
本当の失敗なら、step の on_failure が方針を決めます。
abort_planは、下流が成立しない重要 step の失敗です。残りの pending step を skipped にして、計画をきれいに止めます。continueは任意 step の失敗です。skipped として扱い、下流を進めます。ask_commanderは既定です。主 agent を起こして、別ルートで retry するか、迂回するか、ユーザーに聞くかを判断させます。
blocked という状態もあります。サブ agent が途中でユーザーの入力を必要とした場合、step は失敗ではなく blocked になります。plan は一時停止し、ユーザーの回答が来たら続きから再開します。
各 step は完全な agent run
orchestrator がサブ agent へ step を渡しても、そのサブ agent は簡易版の routine を走るわけではありません。完全な harness loop を使い、自分の streaming run loop、tool calls、context compaction、crash-safe session を持って動きます。編成層は単体 agent runtime の上に座り、内部には手を入れません。
だから二つの記事はつながります。harness はひとつの agent を信頼できるようにし、orchestrator は信頼できる agent たちをチームにします。
大切だった設計判断
実行状態は orchestrator、意図はモデル。 この境界が、plan をモデルの楽観ではなく現実の鏡にします。
dispatch は同じ会話を通す。 隠れた経路がないので、ユーザーもチームの動きを同じ thread で見られます。
step 間は transcript ではなく要約を渡す。 長い chain でも context budget を保ち、各サブ agent が必要な情報に集中できます。
並列化より先に正しさ。 per-conversation lock と一つずつの dispatch は地味ですが、二重 dispatch race を避ける最も分かりやすい土台です。
まとめ
Orkas の orchestration layer の中心に奇抜なアルゴリズムはありません。価値は、いくつかの境界をきちんと守ることにあります。plan は script ではなく dependency graph、実行状態はモデルではなく runtime が持つ、dispatch はユーザーが見ている同じ chat を通る、文脈は要約として流れる、executor は concurrency より correctness を優先する。これらが合わさって、ひとつの信頼できる agent は、分担し、結果を渡し、失敗から戻れるチームになります。
この下の層を知りたい場合は、単体 agent をどう信頼できる実行環境にしているかを読んでください。各 agent が使うほど良くなる仕組みは、Orkas agent が自分の仕事からどう学ぶかで説明しています。この層を自分で作るより指揮したい場合、Orkas は自分のマシンで動くオープンソースの AI エージェント編成として提供しています。