並行で6本のエージェントを走らせていた朝、そのうち1本が誤ったファイルを書き換えていました。問題は、ログを見ても「どのエージェントの、どの段階で起きたのか」が分からなかったことです。6本分の出力が同じコンソールに時系列で混ざって流れ込み、一本の壊れた処理だけを抜き出せませんでした。原因を切り分けるのに40分かかり、その大半は「読むべきログ行を探す」時間でした。
Antigravity 2.0 のデスクトップは、複数のエージェントを並行で動かし、バックグラウンドでスケジュール実行できます。一人で複数のアプリとサイトを回す個人開発では、この並行性が生産性を大きく押し上げます。けれど並行になった瞬間、ログは直線的な物語ではなくなります。可観測性、つまり「後から実行を辿れること」を先に設計しておかないと、並行性はそのまま「追えなさ」に変わってしまいます。
print デバッグが並行で破綻する理由
一本のエージェントを順番に動かしているうちは、上から下へlog を読めば物語が追えます。並行になると、複数の物語が同じ画面に交互に挿し込まれます。「いま読んでいるこの行は、どのエージェントのものか」が分からなければ、ログは情報ではなく雑音になります。
さらに厄介なのは、失敗が必ずしもログの最後に現れないことです。落ちたエージェントの最後の行と、別の正常なエージェントの行が隣り合っていると、無関係な行を原因と誤読します。私はこれで何度も見当違いの修正に時間を使いました。
解決の方向は一つです。すべてのログ行に、それが「どの実行の・どのエージェントの・どの段階か」を機械可読な形で必ず添えることです。
すべてのログに run_id と agent_id を貼る
まず、バッチ全体の実行に1つの run_id を、各エージェントに agent_id を割り当て、あらゆるログにこの2つを必ず含めます。人間が読むための文字列ではなく、後で機械的に絞り込むためのキーとして扱います。
interface LogContext {
run_id: string; // バッチ実行ごとに1つ
agent_id: string; // エージェントごとに1つ
span?: string; // 現在のフェーズ名
}
function createLogger(ctx: LogContext) {
const emit = (level: string, msg: string, extra: Record<string, unknown> = {}) => {
// 1行1 JSON。grep ではなく機械で絞り込める形にする
console.log(JSON.stringify({
ts: new Date().toISOString(),
level,
...ctx,
msg,
...extra,
}));
};
return {
info: (m: string, e?: Record<string, unknown>) => emit("info", m, e),
error: (m: string, e?: Record<string, unknown>) => emit("error", m, e),
child: (agent_id: string) => createLogger({ ...ctx, agent_id }),
};
}
JSON 1行1ログにしておくと、run_id と agent_id で後から完全に絞り込めます。先ほどの「6本が混線した」問題は、agent_id でフィルタするだけで該当エージェントの行だけが抜き出せるようになります。プレーンテキストの print を JSON 構造化ログに替える、この一手だけで切り分けの体感が変わります。
span で「計画→実行→検証」を区切る
エージェントの実行は、おおむね計画・コード実行・検証といったフェーズに分かれます。どのフェーズで時間がかかり、どこで失敗したかを測るために、各フェーズを span(区間)として開始と終了を記録します。
async function withSpan<T>(
logger: ReturnType<typeof createLogger>,
span: string,
fn: () => Promise<T>,
): Promise<T> {
const start = performance.now();
logger.info("span_start", { span });
try {
const result = await fn();
const ms = Math.round(performance.now() - start);
logger.info("span_end", { span, ms, ok: true });
return result;
} catch (err) {
const ms = Math.round(performance.now() - start);
logger.error("span_end", { span, ms, ok: false, err: String(err) });
throw err;
}
}
// 使い方
await withSpan(logger, "plan", () => agent.plan(task));
await withSpan(logger, "execute", () => agent.execute());
await withSpan(logger, "verify", () => agent.verify());
span を入れると、「失敗が verify で起きた」のか「execute で起きた」のかが一目で分かります。私の運用では、span_end の ok: false を検索するだけで、全エージェント横断で失敗フェーズの分布が取れます。検証フェーズでの失敗が多いと分かれば、検証の指示文を見直すべきだと判断できます。
相関IDで親から子へ辿る
Antigravity 2.0 では、親エージェントがサブエージェントを起動して並行作業を束ねる構成がよく出てきます。サブエージェントの失敗を、それを起こした親の文脈まで遡れるようにしておくと、原因究明が一気に速くなります。
そのために、親の run_id を子に必ず引き継ぎ、子には固有の agent_id を与えます。先ほどの child() メソッドがこの役割を果たします。
const parent = createLogger({ run_id: runId, agent_id: "orchestrator" });
parent.info("spawn", { child: "writer-1" });
const child = parent.child("writer-1"); // run_id は引き継ぎ、agent_id だけ差し替え
await withSpan(child, "execute", () => writer.run());
こうしておくと、run_id で1回の実行全体を、agent_id で個別エージェントを、span でフェーズを、それぞれ独立に絞り込めます。「この run のなかで、writer-1 の execute だけを見たい」という問い合わせが、3つのキーの組み合わせで即座に答えられます。
失敗を再現可能にする入力スナップショット
ログで「どこで失敗したか」が分かっても、「なぜ失敗したか」は入力が分からないと再現できません。並行エージェントは外部状態(ファイル・API 応答)に依存するため、同じ指示でも入力が違えば結果が変わります。
私は失敗時に、そのエージェントへの入力(タスク定義・参照したファイルのハッシュ・モデルへの最終プロンプトの要約)を1件のスナップショットとして残すようにしました。
async function snapshotOnFailure(
logger: ReturnType<typeof createLogger>,
input: { task: string; files: string[]; promptDigest: string },
fn: () => Promise<void>,
) {
try {
await fn();
} catch (err) {
logger.error("failure_snapshot", {
task: input.task,
file_count: input.files.length,
prompt_digest: input.promptDigest, // 全文ではなく要約・ハッシュ
err: String(err),
});
throw err;
}
}
スナップショットには、プロンプト全文ではなく要約やハッシュを残します。全文を残すと機密や肥大化の問題が出るからです。それでも「どの入力で落ちたか」が分かれば、手元で再現して直せます。これを入れてから、再現できずに放置される失敗がほぼなくなりました。
ダッシュボード化と運用ルール
構造化ログが揃えば、あとは見方を決めるだけです。私は毎回の run の終わりに、エージェント別の span_end を集計した1枚のサマリを出しています。
私自身が日々使っている運用ルールを、3点に整理します。
- 成功時は静かに、失敗時だけ目立たせることです。
ok: false が1件でもあった run だけ、サマリの先頭に失敗フェーズと agent_id を太字で出すようにしました。これで、ログ全体を読まずとも、見るべき1行に最初から目が行きます。
- span の所要時間(ms)を run をまたいで記録し、普段の中央値から大きく外れた回を異常として拾うことです。突然 execute が3倍遅くなった回は、たいていモデルのレート制限か外部 API の劣化が裏にあります。失敗していなくても、遅延の異常は早めに気づけます。
run_id を最終成果物(コミットや投稿)にも刻むことです。あとから「この投稿はどの run で作られたか」を逆引きできると、問題のある成果物から実行ログへ一直線に辿れます。
3つに共通するのは、人間が読む量を減らす方向の工夫だという点です。並行運用では、注意を向ける先を機械側で絞り込んでおくことが、そのまま運用の余力につながります。
これらを入れる前後で、障害の原因切り分けにかかる時間は平均40分から5分まで縮みました。短くなったのは、賢い解析ツールを足したからではなく、最初から「辿れる形」でログを出すようにしただけです。
並行で何かを任せるなら、任せる前に「後から辿れること」を仕込んでおくのが結局いちばん速い、というのが私の実感です。まずは print を JSON 1行ログに替え、run_id と agent_id を貼るところから始めてみてください。