◉ANTIGRAVITY LABEN
記事一覧/Agents & Manager
◈ Agents & Manager/2026-04-26上級

Antigravity エージェントに「失敗の理由が一発で分かる」トレースを設計する — 可観測性の実践

自前のエージェント実行ループに OpenTelemetry のスパンを入れ、失敗の理由を数行で読めるようにするための設計です。スパン構造、属性の線引き、failure_class の分類、リトライ表、ダッシュボード、コスト属性、PII の扱いまでを、動くコードと一緒に整理します。

Antigravity386エージェント74可観測性トレース運用13OpenTelemetry

✦ プレミアム記事

エージェントが夜のうちに走って、朝に見ると失敗していました。ログには error の一行があるだけで、どのサイクルの、どのツール呼び出しで、なぜ落ちたのかが読み取れません。console.log を散らして上から目で追ううちに、実行回数が増えた途端、その方法は通用しなくなりました。

この記事は、そこから引いた線をまとめたものです。計測した数値を並べられる記事ではありません——「サイクルの境界を記録しておけば、失敗の読み方が変わる」と実感した、その設計だけを書き残します。

前提を一つだけ置きます。Antigravity の内部には手を入れられないので、ここでの計測は自分で書いたエージェント実行ループ(スクリプトやワーカー、エージェントが呼ぶツール側)に入れるものです。Antigravity 本体が標準でこのスパン構造を出すわけではありません。

なぜエージェントは「失敗の理由が分からない」状態に陥るのか

原因は、おおよそ次の 3 つに集約されると考えています。

ひとつ目は、ステップ境界が曖昧なケースです。エージェントは「ツール呼び出し → LLM 推論 → 状態更新」のサイクルを回しますが、この境界を記録していないと、エラーが「どのサイクルの、どのステップで」起きたのかが追えません。タイムスタンプ付きの平文ログを眺めても、サイクル間の構造は見えてきません。

ふたつ目は、入出力が黒箱になっているケースです。LLM 呼び出しの中で何が起きたかは記録するのに、渡したコンテキストと受け取った生レスポンスが残っていない状態です。エラーは「LLM が変な返事をした」で終わり、再現できません。

みっつ目は、失敗の分類が無いケースです。エラーは出ているのに、リトライすべきものか、ユーザーに伝えるべきものか、コードのバグかを後から判断できません。アラート疲れとリトライ方針の破綻に、そのままつながります。

可観測性の設計とは、この 3 つを構造化して残す仕組みを、先に作っておくことです。

トレースの基本単位 — スパン構造を決める

OpenTelemetry の概念に揃えて、エージェント実行を次の階層でスパン化します。Antigravity 固有の構造ではなく汎用的なものですが、エージェント特有の「サイクル」を 1 階層挟むのが要です。

- agent.run               (ルートスパン: 1 回のエージェント実行全体)
  - agent.cycle           (推論・実行サイクル 1 回ぶん)
    - agent.plan          (LLM による次アクション決定)
    - agent.tool_call     (ツール実行 — 個別ツールごとに別スパン)
    - agent.observe       (実行結果を観測してコンテキストに反映)
  - agent.cycle
    - ...

agent.cycle を独立したスパンにする理由は単純です。8 サイクル回って 5 サイクル目で落ちたとき、サイクル単位のスパンが無いと、「5 サイクル目でツールが落ちた」という事実を取り出すために、人間がログを目で追うことになります。

実装は、サイクルの開始点で start_as_current_span("agent.cycle") を呼ぶだけです。Python では次の形になります。上限サイクルに達して終わった場合も、失敗として分類して残す点に注目してください。

from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
 
tracer = trace.get_tracer("antigravity.agent")
 
class AgentError(Exception):
    def __init__(self, message, failure_class):
        super().__init__(message)
        self.failure_class = failure_class
 
def run_agent(task):
    with tracer.start_as_current_span("agent.run") as run_span:
        run_span.set_attribute("task.id", task.id)
        run_span.set_attribute("task.kind", task.kind)
        run_span.set_attribute("agent.policy", task.policy_name)
        run_span.set_attribute("agent.tools_available", list(task.tools))
 
        try:
            for cycle_idx in range(MAX_CYCLES):
                with tracer.start_as_current_span("agent.cycle") as cycle_span:
                    cycle_span.set_attribute("cycle.index", cycle_idx)
                    action = plan_next_action(task)
                    if action is None:
                        cycle_span.set_attribute("cycle.terminated", True)
                        run_span.set_attribute("run.cycle_count", cycle_idx + 1)
                        run_span.set_attribute("result.status", "success")
                        return success(task)
 
                    cycle_span.set_attribute("cycle.action_type", action.kind)
                    result = execute_tool(action)
                    observe(task, result)
 
            # ループを使い切った場合も、失敗として分類して残す
            raise AgentError("max cycles exceeded", "cycle_budget_exceeded")
        except AgentError as e:
            run_span.set_status(Status(StatusCode.ERROR, str(e)))
            run_span.set_attribute("run.cycle_count", MAX_CYCLES)
            run_span.set_attribute("result.status", "failure")
            run_span.set_attribute("result.failure_class", e.failure_class)
            raise

with 文の中で例外が出ると、OpenTelemetry の Python SDK は既定でその例外をスパンに記録し、ステータスも ERROR にします。それでも failure_class だけは自分で付ける必要があります。SDK には、失敗の「意味」までは分からないためです。

TypeScript(Node.js)では、@opentelemetry/api で同じ構造を書きます。startActiveSpan に渡したスパンは自動では閉じられないので、finally で必ず end() を呼びます。

import { trace, SpanStatusCode } from "@opentelemetry/api";
 
const tracer = trace.getTracer("antigravity.agent");
 
export async function runAgent(task: Task): Promise<Result> {
  return await tracer.startActiveSpan("agent.run", async (runSpan) => {
    runSpan.setAttribute("task.id", task.id);
    runSpan.setAttribute("task.kind", task.kind);
    runSpan.setAttribute("agent.policy", task.policyName);
 
    try {
      for (let cycleIdx = 0; cycleIdx < MAX_CYCLES; cycleIdx++) {
        const done = await tracer.startActiveSpan(
          "agent.cycle",
          async (cycleSpan) => {
            try {
              cycleSpan.setAttribute("cycle.index", cycleIdx);
              const action = await planNextAction(task);
              if (action === null) {
                cycleSpan.setAttribute("cycle.terminated", true);
                return true;
              }
              cycleSpan.setAttribute("cycle.action_type", action.kind);
              const result = await executeTool(action);
              observe(task, result);
              return false;
            } finally {
              cycleSpan.end();
            }
          }
        );
        if (done) {
          runSpan.setAttribute("run.cycle_count", cycleIdx + 1);
          runSpan.setAttribute("result.status", "success");
          return success(task);
        }
      }
      throw new AgentError("max cycles exceeded", "cycle_budget_exceeded");
    } catch (e) {
      runSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
      runSpan.setAttribute("result.status", "failure");
      runSpan.setAttribute("result.failure_class", classify(e));
      throw e;
    } finally {
      runSpan.end();
    }
  });
}

agent.cycle には、必ず cycle.index を属性として乗せておきます。後から「3 サイクル目の挙動だけ見たい」というクエリが、ほぼ確実に必要になるためです。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦OpenTelemetry に揃えた 4 階層のスパン構造(agent.run / agent.cycle / agent.plan / agent.tool_call)を、Python と TypeScript の実装コードで提示します
✦失敗を 8 種類に分ける failure_class と、リトライ・通知の方針を 1 枚の対応表にまとめる設計を共有します
✦ダッシュボード 3 種のクエリ例と、生の入出力・PII をトレースの外に逃がす保管設計まで踏み込みます
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

◈ Agents & Manager2026-10-16
エージェントに .zip を渡す前に、.git を除いて 29 MB を 15 MB にし、秘密情報の警告 28 件を見分けました
Antigravity アプリ v2.23.0 で .zip を添付できるようになりました。自分のリポジトリを実際に固めて .git の重さと秘密情報検査の誤検知を数え、除外・検査・マニフェストを一度に行う Python スクリプトの全文を残します。
◈ Agents & Manager2026-09-08
/boost が「検証済み」と返してきたとき、その worktree に何が無かったのかを先に数えています
Antigravity の /boost は使い捨ての隔離ワークツリーでテストを走らせます。手元のビルドが git に入っていないファイルへ依存していると、その検証は別の木の上で通ります。ずれを先に数える手順を書き残します。
◈ Agents & Manager2026-09-06
エージェントの定期実行で、失敗より先に「走らなかった」を疑うようになりました
定期実行の台帳は成功率100%のまま、夕方の枠だけが二週間走っていませんでした。期待発火表と実行台帳を突合して欠落を出す監査を、cron 展開・終了コード・台帳の書き方の実測とあわせて書き残します。