Antigravity エージェントが途中で落ちたとき、Retry ボタンを押し続けて同じ場所で詰まる、という経験をされた方は多いのではないでしょうか。個人プロジェクトであれば「もう一回回す」で済むかもしれません。しかし、クライアント案件の本番環境や、自分の稼働プロダクトでエージェントを自動実行しているとき、この素朴な Retry は静かに牙を剥きます。同じ失敗が 20 回続けば、トークン代は 20 倍です。副作用のある処理(メール送信、決済、データ書き込み)を Retry してしまえば、被害は金額だけでは終わりません。
私もアーティスト活動と並行してアプリを運用する中で、夜中にエージェントジョブが暴走し、朝起きたら想定の 5 倍の API コストが発生していたことがあります。その経験から学んだのは、Retry は「機能」ではなく「設計対象」だということです。ここで扱うのはAntigravity エージェントの Retry を、趣味レベルから本番品質に引き上げるための設計図を、冪等性・バックオフ・コスト上限・失敗トリアージの 4 本柱に分けて実装コードつきで解説します。読み終わる頃には、自分のプロジェクトに今日から組み込める具体的な再実行フローができているはずです。
Retry が「運試し」から「設計」に変わる瞬間
多くの開発者にとって、最初の Retry は UI ボタンです。Antigravity の Manager Surface や CLI で失敗したタスクに Retry を押す。これは開発中のインタラクティブな使い方としては正しいのですが、「誰かが見ていない時間に動くエージェント」に対しては不十分です。
本番品質の Retry に求められる条件は、次の 4 つに集約できます。
- 冪等性(Idempotency): 同じ入力で何回実行しても、副作用は 1 回だけ発生する
- 指数バックオフとジッター: 外部依存の過負荷を悪化させない再試行間隔
- コスト上限(Cost Ceiling): トークン・API 呼び出し回数・金額の上限を事前に定義する
- 失敗トリアージ: 再試行すべきエラー、即座に止めるべきエラー、人間にエスカレートするエラーを分類する
この 4 つを揃えていないエージェントを自動化フローに乗せるのは、ブレーキの効かない車で高速道路に入るようなものです。逆に、この 4 つさえ設計できていれば、夜中に Antigravity のスケジュールタスクを回しても、朝の珈琲を落ち着いて飲めます。以降、それぞれを順に組み立てていきましょう。
冪等性(Idempotency)を設計する — 同じ入力でも副作用を2度起こさない
Retry 設計で最も誤解されやすいのがここです。「ただ関数を呼び直すだけ」と思うかもしれませんが、エージェントは外部の世界に対して副作用を起こします。ファイル書き込み、API 呼び出し、決済、メール、DB 更新。これらを 2 度走らせないために、エージェントが扱うすべての副作用アクションに「このアクションはすでに実行済みか?」を判定できる仕組みを持たせます。
実装パターンはシンプルです。アクションごとに一意の Idempotency Key を生成し、実行前にキーで重複チェックを行います。TypeScript での最小実装例を示します。
// src/retry/idempotency.ts
// エージェントが起こす副作用を、冪等キーで守るためのラッパー。
// 実行前にキーをチェックし、完了したキーは結果付きで 24 時間保存する。
import { createHash } from "node:crypto";
type ExecutionResult<T> = {
status: "completed" | "failed";
data?: T;
error?: string;
completedAt: string;
};
export class IdempotencyGuard {
// 本番では Redis / KV を使うこと。ここでは学習目的でメモリ実装。
private store = new Map<string, ExecutionResult<unknown>>();
private makeKey(action: string, payload: unknown): string {
const body = JSON.stringify({ action, payload });
return createHash("sha256").update(body).digest("hex").slice(0, 32);
}
async run<T>(
action: string,
payload: unknown,
execute: () => Promise<T>,
): Promise<T> {
const key = this.makeKey(action, payload);
const cached = this.store.get(key);
if (cached?.status === "completed") {
// 2 回目以降は保存済みの結果をそのまま返す。副作用は実行しない。
return cached.data as T;
}
try {
const data = await execute();
this.store.set(key, {
status: "completed",
data,
completedAt: new Date().toISOString(),
});
return data;
} catch (err) {
// 失敗は記録するが、キーには結び付けない。
// 次回は入力が同じでも再実行される。
this.store.set(key, {
status: "failed",
error: err instanceof Error ? err.message : String(err),
completedAt: new Date().toISOString(),
});
throw err;
}
}
}
// 使い方
const guard = new IdempotencyGuard();
await guard.run("send_welcome_email", { userId: "u_123" }, async () => {
await mailer.send({ to: "user@example.com", template: "welcome" });
return { queued: true };
});
// → 1 回目はメール送信、2 回目以降は送信せずに { queued: true } を返す
期待する出力としては、同じ action + payload の組み合わせで 2 回目以降を呼び出しても、メール送信が走らず即座に { queued: true } が返ることを必ずテストで確認してください。これが Retry 設計の土台です。
なぜキーを「アクション名+入力のハッシュ」にするか
タスク ID や UUID を Idempotency Key に使ってしまうと、エージェントが「同じ意図の処理」を別 ID で何度も投げてしまった時に、すべて別物として扱われます。アクション名と入力の本質的な内容からハッシュを作ることで、「同じ仕事を同じ条件でやろうとしている」ことを検知できます。これは決済 SDK(Stripe の Idempotency Keys)と同じ設計思想です。
本番ストレージでの注意点
メモリストアは再起動で消えるので、本番では必ず外部ストレージを使います。Cloudflare Workers であれば KV、Firebase を使っているなら Firestore の単一ドキュメント + TTL、Redis なら SET key value NX EX 86400 で一発です。TTL は 24〜72 時間が現実的で、これより短いと再実行時に二重送信、長すぎるとストレージが肥大化します。
指数バックオフとジッターで「嵐」を避ける
失敗したら即座に再試行、というパターンは、障害時に外部サービスへの攻撃になります。特に LLM API は障害時にレートリミットで絞ってくることが多く、即 Retry は傷口を広げるだけです。ここで使うのが、指数バックオフ(Exponential Backoff)と、それに重ねるジッター(Jitter)です。
指数バックオフは「待ち時間を指数関数的に伸ばす」パターンで、base * 2^attempt のように計算します。ジッターはさらに乱数を加えて、同時刻に復帰した多数のクライアントが一斉に再試行するのを防ぎます。これは AWS が推奨する Exponential Backoff and Jitter というよく知られた設計パターンです。
// src/retry/backoff.ts
// 指数バックオフ + ジッター + 最大試行回数 + 最大待ち時間の組み合わせ。
// これを「再試行してよい」と判断したエラーにだけ適用する。
export type RetryOptions = {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
shouldRetry: (err: unknown, attempt: number) => boolean;
onRetry?: (err: unknown, attempt: number, delayMs: number) => void;
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
opts: RetryOptions,
): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (attempt === opts.maxAttempts || !opts.shouldRetry(err, attempt)) {
throw err;
}
// full jitter: 0 〜 base*2^attempt の範囲でランダム化
const expo = Math.min(opts.baseDelayMs * 2 ** attempt, opts.maxDelayMs);
const delay = Math.floor(Math.random() * expo);
opts.onRetry?.(err, attempt, delay);
await sleep(delay);
}
}
throw lastError;
}
// 使い方
await retryWithBackoff(
() => fetch("https://api.example.com/data").then((r) => r.json()),
{
maxAttempts: 5,
baseDelayMs: 500,
maxDelayMs: 30_000,
shouldRetry: (err) => {
// 後述する「失敗トリアージ」でここを埋める
return isTransientError(err);
},
onRetry: (err, attempt, delay) => {
console.log(`retry attempt=${attempt} delay=${delay}ms error=${String(err)}`);
},
},
);
期待する動作は、成功したらその時点で return、失敗しても shouldRetry が true を返す間は指数バックオフで待って再試行、maxAttempts に達するか shouldRetry が false になったら例外を投げる、というものです。onRetry はログ・メトリクスを仕込む場所として必ず使ってください。ここがブラックボックスだと、後で事故原因を追えなくなります。
「full jitter」を使う理由
ジッターには「equal jitter(半分固定+半分乱数)」や「decorrelated jitter(前回値と現在値の間で乱数)」などのバリエーションがあります。個人開発の Antigravity エージェントで最も無難なのは full jitter(0 からその時点の最大値の間で一様分布)です。実装がシンプルで、同時復帰の集中を強く抑えてくれます。複雑なバリエーションを持ち込む前に、まず full jitter で測定することをおすすめします。
コスト上限(Cost Ceiling)を仕掛ける — 無限ループが口座を溶かす前に
夜中にエージェントが暴走して朝までトークンを燃やし続ける、という事故は、実際に起きます。私自身、初期の頃に円安も相まって想定の 5 倍の請求が来たことがあり、そこから本気でコスト上限を設計するようになりました。Antigravity のエージェントは外部 LLM API を呼ぶので、Retry の回数ではなく「どれだけトークンを消費したか」を上限として持つのが現実的です。
設計方針は次の 3 点です。
- タスク単位で「トークン予算」を事前に決める
- Retry のたびに消費トークンを加算し、予算超過で即停止する
- 超過時はアラート + チェックポイントに戻して人間の判断を待つ
// src/retry/budget.ts
// タスク単位のコスト上限をエージェント実行ループに組み込む。
// LLM レスポンスから usage を取り出し、合計が超えたら BudgetExceeded を投げる。
export class BudgetExceeded extends Error {
constructor(
public readonly used: number,
public readonly budget: number,
) {
super(`budget exceeded: used=${used} budget=${budget}`);
}
}
export class TokenBudget {
private used = 0;
constructor(
private readonly budget: number,
private readonly onWarn?: (used: number, budget: number) => void,
) {}
track(usage: { prompt_tokens: number; completion_tokens: number }) {
this.used += usage.prompt_tokens + usage.completion_tokens;
if (this.used > this.budget) {
throw new BudgetExceeded(this.used, this.budget);
}
// 80% 超えで警告(Slack 通知・メトリクス発火などに繋げる)
if (this.used > this.budget * 0.8) {
this.onWarn?.(this.used, this.budget);
}
}
remaining(): number {
return Math.max(0, this.budget - this.used);
}
}
// 使い方(エージェント実行ループの内側)
const budget = new TokenBudget(200_000, (used, total) => {
console.warn(`budget at 80%: ${used}/${total}`);
});
for (const step of plannedSteps) {
try {
const response = await callLLM(step.prompt);
budget.track(response.usage);
applyResult(response);
} catch (err) {
if (err instanceof BudgetExceeded) {
await notifyHuman("budget exceeded, human review required", { err });
await saveCheckpoint(step);
throw err; // ループ全体を止める
}
throw err;
}
}
期待する動作は、トークン消費が予算の 80% を超えた時点で警告が飛び、100% を超えた時点で例外を投げて以降の処理を即停止、最後のチェックポイントと共にアラートが届く、というものです。これを入れておけば、最悪でも「予算の 1 倍分」の事故で済みます。
予算の決め方
初めて設計する場合、次のガイドが実用的です。
- 短いタスク(1 ファイル編集、ドキュメント更新): 10,000 〜 30,000 トークン
- 中規模タスク(複数ファイル横断のリファクタリング): 50,000 〜 150,000 トークン
- 大きいタスク(新機能開発、マイグレーション): 300,000 〜 500,000 トークン
これを超えた時点で「そのタスクはエージェントに任せる粒度ではない」と疑ってください。Planning Mode で分割するか、人間が手を動かすかに切り替える判断軸になります。この粒度分割の考え方は、Antigravity の Planning Mode を大規模プロジェクトで活かす戦略ガイド にまとめましたので、あわせて参考にしてください。
失敗トリアージ — どの失敗は再試行するか
Retry が暴発する最大の原因は、「すべての失敗を同じように扱ってしまう」ことです。実際にはエラーは次の 3 種類に分かれます。
- 一時的エラー(transient): ネットワーク瞬断、LLM のレートリミット、タイムアウト → 再試行でほぼ回復
- 構造的エラー(permanent): 認証失敗、権限不足、入力バリデーション、未知のモデル名 → 何回試しても同じ結果
- 曖昧なエラー(ambiguous): 500 エラー、JSON パース失敗、部分的な結果 → 一度だけ再試行して、駄目なら人間にエスカレート
この 3 分類を実装に落とすと、「再試行していい」という判断が明確になります。
// src/retry/triage.ts
// エラーを transient / permanent / ambiguous に分類するトリアージ関数。
// このロジックは実運用で必ず育てる。新種エラーが来るたびに分類を追加する。
export type ErrorClass = "transient" | "permanent" | "ambiguous";
export function classifyError(err: unknown): ErrorClass {
if (!(err instanceof Error)) return "ambiguous";
const msg = err.message.toLowerCase();
// --- 認証・権限・入力エラーは再試行しない ---
if (/unauthorized|forbidden|invalid_api_key|authentication/.test(msg)) {
return "permanent";
}
if (/validation|invalid request|bad request|schema/.test(msg)) {
return "permanent";
}
if (/model_not_found|unknown model/.test(msg)) {
return "permanent";
}
// --- 一時的なネットワーク・レートリミットは再試行する ---
if (/rate.?limit|too many requests|429/.test(msg)) {
return "transient";
}
if (/timeout|econnreset|etimedout|network/.test(msg)) {
return "transient";
}
if (/503|service unavailable|overloaded/.test(msg)) {
return "transient";
}
// --- 曖昧なものは一度だけ再試行する ---
if (/500|internal server|json|parse/.test(msg)) {
return "ambiguous";
}
return "ambiguous";
}
// 使い方: retry ループの shouldRetry に組み込む
export function shouldRetryFromTriage(err: unknown, attempt: number): boolean {
const klass = classifyError(err);
if (klass === "permanent") return false;
if (klass === "transient") return attempt < 5;
if (klass === "ambiguous") return attempt < 2; // 一度だけ
return false;
}
このトリアージは「最初から完璧」ではなく、運用しながら育てるものです。新しいエラーが出るたびに、「これは transient / permanent / ambiguous のどれか?」を決め、正規表現に 1 行追加します。これを続けていくと、自分のプロジェクトに最適化されたトリアージが手元に残ります。
チェックポイントと Retry を組み合わせて巨大タスクを守る
トリアージと予算を備えても、100 ステップのエージェントタスクが 95 ステップ目で落ちたとき、最初からやり直すのは時間もお金も無駄です。ここで役立つのがチェックポイント(途中状態の保存)です。Antigravity は Manager Surface で自動的にチェックポイントを取りますが、スクリプトから呼び出す自作エージェントでは明示的に設計する必要があります。
設計のポイントは 3 つです。
- ステップ単位で「完了済みステップの ID」と「次に実行するステップの入力」を保存する
- Retry 時は最後の成功点から再開する(最初からやり直さない)
- 副作用のあるステップは冪等性ガード(前述)で守る
// src/retry/checkpoint.ts
// チェックポイントと Retry を組み合わせた「再開可能なエージェント」の最小実装。
// 保存先は JSON ファイル / KV / Firestore など、耐久性のあるストレージを使うこと。
import fs from "node:fs/promises";
type Step = { id: string; run: () => Promise<void> };
type Checkpoint = {
completed: string[];
updatedAt: string;
};
export async function runResumable(
steps: Step[],
checkpointPath: string,
): Promise<void> {
let cp: Checkpoint;
try {
cp = JSON.parse(await fs.readFile(checkpointPath, "utf-8"));
} catch {
cp = { completed: [], updatedAt: new Date().toISOString() };
}
for (const step of steps) {
if (cp.completed.includes(step.id)) {
console.log(`skip already-completed step: ${step.id}`);
continue;
}
try {
await step.run();
cp.completed.push(step.id);
cp.updatedAt = new Date().toISOString();
await fs.writeFile(checkpointPath, JSON.stringify(cp, null, 2));
} catch (err) {
// 失敗時もチェックポイントを保存し、次回はここから再開できるようにする
await fs.writeFile(checkpointPath, JSON.stringify(cp, null, 2));
throw err;
}
}
}
期待する動作は、途中で落ちた次の実行で、すでに完了したステップを全てスキップして失敗地点から再開すること。大きなエージェントタスクほどこの設計の恩恵は大きく、再実行コストがほぼゼロになります。より踏み込んだ設計については、Antigravity のチェックポイントとロールバックを使いこなす完全ガイド も参考になさってください。
本番での観測 — ログ・メトリクス・アラート
Retry は「見えないところで回るもの」なので、観測性を欠かせません。最低限必要な 4 つを挙げます。
- 構造化ログ: 試行回数、待ち時間、エラーのクラス、消費トークン
- メトリクス: retry.count / retry.success / retry.fail / retry.budget.exceeded の 4 本
- アラート: 予算超過、連続失敗、permanent エラー発生
- 分散トレース: Antigravity のスパンと外部 API 呼び出しを紐づける
観測ツールは好みでよく、Langfuse や Helicone、あるいは自前の OpenTelemetry 収集でも構いません。私は個人開発では Cloudflare の Workers Analytics Engine + Slack Webhook を使って、最低限のメトリクスだけ飛ばしています。重要なのは「ダッシュボードを作り込むこと」ではなく、「Retry が暴発したら 5 分以内に気づける」ことです。
Antigravity のエージェントログに自作ロガーを差し込む具体的な方法は、Antigravity × Langfuse でエージェントの可観測性を高める運用ガイド にまとめましたので、観測基盤を整えるときの足がかりとしてお使いください。
チーム運用 — Retry ポリシーを明文化してレビューする
個人開発でも、ましてチーム開発ならなおさら、Retry の判断は「なんとなく」で回してはいけません。プロジェクトルートに RETRY_POLICY.md のようなファイルを置き、次の 5 項目を明文化しておくことを強くおすすめします。
- タスク種別ごとの
maxAttempts
- 待ち時間の
baseDelayMs / maxDelayMs / ジッター戦略
- タスク種別ごとのトークン予算(短/中/大)
- エラー分類の原則(どのメッセージは permanent か)
- 予算超過・permanent 検出時の通知先と人間の対応手順
このファイルはコードよりも先に書き、レビューしてから実装するのが順序として正しいです。実装だけあってポリシーがない状態は、「信号の色が決まっていない交差点」と同じで、事故の温床になります。エージェント固有の話ではありませんが、「暗黙のルールを明示に変える」思考は普遍的です。
よくある失敗パターンと対処
最後に、本番運用で実際にぶつかりやすい失敗を 3 つ挙げます。
1. 冪等性ガードをつけ忘れて、Retry でメールが二重送信された
もっとも多い事故です。send_email / create_payment / post_to_slack のような副作用の強いアクションは、必ず Idempotency Key で守ります。新しい副作用アクションを足すときは、テストコードにも「2 回呼んでも 1 回しか実行されない」ケースを書いておくことが予防策です。
2. permanent エラーを Retry し続けて、予算を使い果たした
認証エラーや権限不足は、何回試しても同じ結果です。ここを transient 扱いしてしまうと、エージェントは「無限ループ + 課金」を静かに続けます。classifyError のテストに unauthorized / forbidden / invalid_api_key を含めておき、CI で shouldRetry === false になることを自動確認しましょう。
3. 予算超過後もアラートが飛ばず、翌朝に気づいた
onWarn の通知先がダミーのままだった、あるいは Slack Webhook の URL が失効していた、という地味なパターンです。通知自体の死活監視を、週に 1 度でもよいので走らせてください。テスト用の「必ず警告が飛ぶダミータスク」を 1 つ常設しておくのが、最小労力で最大の効果がある運用策です。
ここまで、Retry を機能ではなく設計対象として捉え直し、4 本柱(冪等性・バックオフ・コスト上限・失敗トリアージ)を実装コード付きで組み立ててきました。記事を読んでいただいた上で、今日すぐに始めていただきたい一歩はシンプルです。自分のエージェントスクリプトの中から「副作用を起こすアクション」を 1 つだけ選び、そこに IdempotencyGuard を差し込んでみてください。たったそれだけで、「Retry で同じメールが二重に飛ぶ」という、最も痛い事故の確率が大きく下がります。そこから少しずつ、バックオフ、予算、トリアージと広げていけば、1 〜 2 週間でエージェントの信頼性は別物になります。
Antigravity は、適切に設計すれば夜中に回しておける相棒です。手綱は私たちの側で握る必要がありますが、一度きちんと設計してしまえば、後は静かに働いてくれます。