mcpField
A
agent-observability-session-classify

agent-observability-session-classify

github.com/datadog-labsテスト・品質180540 回インストール6 日前に更新
リポジトリ

agent-observability-session-classify について

Datadog Agent Observability のセッションやトレースを、ユーザー満足度で分類するスキルです。段階的に会話内容を取得し、MCP または pup を使って yes、partial、no の判定と理由を返します。

主な機能

  1. 01セッションとトレースの満足度分類
  2. 02LLM アプリからセッションを抽出
  3. 03段階的なフォールバックで内容を取得
  4. 04Datadog MCP と pup に対応

活用シーン

  1. 01Datadog アシスタントのセッション評価
  2. 02特定トレースの結果確認
  3. 03評価用に LLM アプリのトレースを抽出

MCP 対応

Datadog MCP ツールが利用可能な場合、span と会話内容の取得に使用します。利用できない場合は pup コマンドラインに切り替えます。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/datadog-labs/agent-skills --skill agent-observability-session-classify

インストール後、Claude Code で/agent-observability-session-classifyを入力して読み込まれたか確認します。

詳細

バックエンド

検出 — 各呼び出しの開始時に、何らかの操作を行う前に、使用するバックエンドを判定します。

  1. ユーザーが呼び出しのどこかで --backend pup を指定した場合 → MCP ツールの有無にかかわらず、直ちに pup モードを使用します。手順 2~4 はスキップします。
  2. 有効なツールリストに MCP ツールがあるか確認します。判定の目安は、利用可能なツールに mcp__datadog-llmo-mcp__search_llmobs_spans が含まれているかどうかです。
  3. MCP ツールがある場合 → 以降は MCP モードを使用します。このスキルのワークフローの各セクションに記載された名前どおりに、MCP ツールを呼び出します。
  4. MCP ツールがない場合 → pup が実行可能か確認します。Bash 経由で pup --version を実行します。"version" を含む JSON レスポンスが返れば、pup を使用できます。
  5. pup が応答する場合 → 以降は pup モードを使用します。このファイル末尾のツールリファレンスの付録に従い、すべての MCP ツール呼び出しを対応する pup コマンドに置き換えます。
  6. どちらも利用できない場合 → 停止して、ユーザーに次のように伝えます。

    "Datadog MCP サーバーも pup CLI も利用できません。MCP サーバー(claude mcp add --scope user --transport http datadog-llmo-mcp 'https://mcp.datadoghq.com/v1/mcp?toolsets=llmobs,rum')を接続するか、pup をインストールしてください。"

--backend pup は呼び出し引数のどこに指定しても受け付けられ、スキルのロジックに残りの引数を渡す前に取り除かれます。

pup の呼び出しルール:

  • Bash 経由で呼び出します:pup llm-obs <subcommand> [flags]
  • pup は常に JSON を出力します。そのまま解析してください。MCP の結果のように、JSON が [{"type": "text", "text": "<json>"}] にラップされることはありません。
  • pup が認証エラーを返した場合は、ユーザーに pup auth login の実行を案内し、停止します。
  • 並列実行:1 回のメッセージで複数の Bash ツール呼び出しを行います(呼び出しごとに pup コマンドを 1 つ実行します)。
  • 時間指定フラグ:pup は期間を表す文字列(1h、7d、30m)と RFC3339 タイムスタンプを受け付けます。now- で始まる文字列は使用しないでください。スキルの --timeframe 引数から変換する場合は、接頭辞を取り除きます:now-7d → 7d、now-24h → 24h、now-30d → 30d。
  • pup llm-obs spans search の --summary は、ペイロードフィールドを必須メタデータのみに絞り込みます。コンテンツが不要な一括検索や検索フェーズで使用してください。

エントリーモード別の pup モードに関する注意事項:

  • session_id モード:手順 1~3 と手順 5 は完全に動作します。手順 4(RUM)では analyze_rum_events の代わりに pup rum aggregate --user-email EMAIL を使用します。ツールリファレンスを参照してください。手順 4b(監査証跡)は pup ネイティブで、OAuth 経由でアクティブユーザー自身の組織を照会します。
  • trace_id モード:MCP モードと完全に同等です。
  • ml_app モード:pup ではオプション A(aggregate_spans)を利用できません。これをスキップして、オプション B に進んでください。

呼び出し ID: 各呼び出しの開始時、MCP ツールを呼び出す前に、8 文字の 16 進数の呼び出し ID を生成します(例:3a9f1c2b)。呼び出し全体を通して同じ ID を使用します。

インテントのタグ付け: すべての MCP ツール呼び出しで、telemetry.intent の先頭に skill:agent-observability-session-classify[<inv_id>] — を付け、その後にツールを呼び出す理由を記述します。最初の MCP ツール呼び出しのみ、代わりに skill:agent-observability-session-classify:start[<inv_id>] — を使用します(:start サフィックスに注意してください)。最初の呼び出しの例:skill:agent-observability-session-classify:start[3a9f1c2b] — Step 1: enumerate turn root spans for session abc-123

スキル:eval-session-classify

Datadog Agent Observability のセッションとトレースを分類するスキルです。簡潔な理由を添えて、満足度の判定(yes/partial/no)を出力します。単独で呼び出すことも、eval-pipeline から呼び出すこともできます。


入力

入力モード必須説明
session_idsession_id モードはいDatadog CMD+I アシスタントセッションの UUID
trace_idtrace_id モードはいAgent Observability の Trace ID
ml_appml_app モードはいサンプリング元の LLM アプリ名
timeframeml_app モードいいえ(デフォルト:now-7d)サンプリング対象の期間
sample_limitml_app モードいいえ(デフォルト:20、上限:50)分類するセッションまたはトレースの数

session_id、trace_id、ml_app のいずれも指定されていない場合 → 直ちに停止して次を返します:

{
  "error": "missing_input",
  "detail": "Provide one of: session_id, trace_id, or ml_app."
}

フェーズ 0 — モードの判定

  • session_id が指定されている → session_id モード → セッションモードへ進みます
  • trace_id が指定されている(session_id なし) → trace_id モード → トレースモードへ進みます
  • ml_app が指定されている(session_id と trace_id なし) → ml_app モード → ML アプリモードへ進みます
  • 何も指定されていない → エラーを出力します(上記を参照)

出力形式

デフォルト(コンパクト)— 呼び出し元が詳細を要求しない限り、すべてのモードで使用します:

verdict: yes | partial | no
reason: <one sentence>

詳細 — Markdown の完全なレポート(各モードのセクション末尾を参照)。入力に verbose: true を含めるか、詳細なレポートを要求すると詳細形式になります。

ml_app モードでは、各トレースのコンパクトなブロックの後に、必ずサマリーテーブルを追加します。


コンテンツ取得カスケード

会話コンテンツの読み取りは、すべてのモードでこのカスケードに従います。判定を行う前に、ステップ 1(セッションモード)またはステップ T1(トレースモード)で特定したすべてのターンルートスパンについて、カスケードを実行してください。

完全性ゲート:すべてのターンでカスケードを実行するまで、RUM(セッションモード)または分類(すべてのモード)に進まないでください。search_llmobs_spans のプレビュー(input.preview / output.preview、約 200 文字に切り詰められています)は、このゲートを満たしません。これらで特定できるのはターンの境界だけであり、それ以上の情報はありません。get_llmobs_agent_loop、expand_llmobs_spans + get_llmobs_span_content、または get_llmobs_span_content(field="input"/"output") から取得した実際のコンテンツのみが対象です。

短いセッション(20 ターン以下):すべてのターンで C1/C2 を実行します。選別や「重要なターン」のヒューリスティックは使用しません。

長いセッション(20 ターン超):最初の 5 ターンと最後の 5 ターンに加え、input.preview または output.preview が異常(エラー、修正ループ、意図の繰り返し、予期しないツール)を示すターンで C1/C2 を実行します。スキップする前に、残りすべてのターンのプレビューを確認し、異常がないか調べてください。

並列処理:すべての N 件の get_llmobs_agent_loop 呼び出しを、1 つのメッセージで発行してください。各ターンにつき 1 回の呼び出しを、同じバッチ内で実行します。N ターンのセッションでは、C1 を N 回同時に呼び出すことになります。ターンを順番に処理してはならず、すべての結果がそろう前に部分的な判定を行ってはなりません。あるターンの C1 が iterations: [] を返した場合にのみ、そのターンで C2 に進んでください。

C1 — get_llmobs_agent_loop(trace_id, agent_span_id)

最も情報量の多い情報源です。完全なシステムプロンプト、ユーザーメッセージ、ツール呼び出しの引数と結果、アシスタントの応答、各イテレーションのトークンコストを取得できます。すべてのエージェントスパンで、最初にこれを試してください。

get_llmobs_agent_loop(
  trace_id           = "<TRACE_ID>",
  span_id            = "<AGENT_SPAN_ID>",
  from               = "now-90d",
  to                 = "now",
  max_content_length = 2000
)
  • iterations: [] かつ timeline: null → アプリの LLM スパンが、エージェントスパンの直接の子としてではなく、中間ワークフローレイヤー(例:get_answer_from_model_step)を経由しています。C2 に進みます。
  • 404 → スパン ID がトレースストア内で解決できません。C2 に進みます。
  • コンテンツフィールドに <REDACTED_INPUT> または <MASKED_STREAMING_RESPONSE> が含まれる → フィーチャーフラグによって IO トレーシングが無効になっています。構造(イテレーション数、ツール名、トークン使用量)は引き続き有用なため、その旨を記録してください。実際のコンテンツを取得するため、C2 に進みます。

C1 の結果がコンテキストに収まらずディスクに保存された場合(長いセッションでよく発生します):ファイル全体を Read しようとしないでください。トークン上限を超えてしまいます。対象を絞った Python 抽出を実行し、ターンごとの構造化サマリーを作成してください:

import json
with open('<persisted_path>') as f:
    data = json.loads(json.load(f)[0]['text'])
iters = data['iterations']
for it in iters:
    tcs = [tc['name'] for tc in it.get('tool_calls', [])]
    content = it.get('content', '') or ''
    # Skip the system prompt body — it's verbose and not what we're classifying.
    if len(content) > 4000 and content.lstrip().startswith('#'):
        content = '[system prompt]'
    print(f"iter {it['iteration']} [{it.get('status')}] tools={tcs}"
          f" err={it.get('error_message','')} content={content[:300]}")

サイズが大きくても解析可能な C1 の結果は実際のコンテンツです。C1 が iterations: [] を返した場合にのみ、C2 に進んでください。

C2 — LLM 子跨度上的 get_llmobs_span_content(field="messages")

C1 で iterations: [] が返される場合、LLM スパンは通常、ルートエージェントスパンの 2 階層下にあります。つまり、エージェントループ API がたどらないワークフローラッパー(例:get_answer_from_model_step)の下です。各ターンの具体的なツリーは次のとおりです。

<root agent span>                          ← one per turn
  <workflow wrapper>                       ← one per LLM round-trip
    <llm span, e.g. anthropic.request>     ← call `field="messages"` here

get_llmobs_trace は深さ 1 の子のみを返し、ワークフローの下にネストされた LLM スパンには到達できないため、LLM スパン ID をたどるには expand_llmobs_spans を使用します。

expand_llmobs_spans(
  trace_id  = "<TRACE_ID>",
  span_ids  = ["<ROOT_AGENT_SPAN_ID>"],
  max_depth = 2,          # root → workflow → llm span
  from      = "now-90d",  # required: default is now-1d, silently returns empty for older spans
  to        = "now"
)

返されたツリーから、span_kind=llm かつ has_input=true のすべてのノードを収集します(一般的な名前は anthropic.request、openai.request、chat_completion-call、messages-call です)。それぞれに対して get_llmobs_span_content(field="messages") を呼び出します。

JSONPath と切り詰めに関する注意点:path は max_tokens による切り詰めの後に適用されます。各 LLM 呼び出しスパンは通常、複数 KB のシステムプロンプトで始まるため、max_tokens の値が小さいと、JSONPath フィルターはシステムプロンプトのみの内容に対して処理され、誤ったメッセージが気付かれないまま返されます。

意味のある末尾(ユーザークエリ、ツール呼び出し、最終回答)を抽出するには:

  1. path を指定せずに 1 回呼び出し、レスポンスから total_tokens_approx を読み取ります。
  2. path = "$.[-5:]" および max_tokens = total_tokens_approx + 500 を指定して、再度呼び出します。
get_llmobs_span_content(
  trace_id   = "<TRACE_ID>",
  span_id    = "<LLM_SPAN_ID>",
  field      = "messages",
  path       = "$.[-5:]",
  max_tokens = <total_tokens_approx + 500>
)

これにより、最後の 5 つのメッセージが返されます。通常は[ユーザーのコンテキスト+クエリ、推論、アシスタントのテキスト、アシスタントの tool_call、ツールの結果]で、ターンの内容を理解するには十分です。

  • メッセージが "REDACTED" になっている → IO トレーシングが無効です。C3 に進みます。
  • content_info マップに messages が含まれていない → チャットスパンではありません。C3 に進みます。
C3 — ルートスパンで get_llmobs_span_content(field="input") および field="output" を使用

ルートスパンには、アプリが書き込んだ合成サマリーが含まれていることがよくあります(例:input としての生のユーザークエリ、output としての最終レスポンス、または "Investigate error for issue: <id>" / "Investigation completed with status: completed")。得られる情報は最小限ですが、タスクの内容と完了したかどうかを確認するには十分です。

C4 — 構造的なシグナルのみ

すべてのコンテンツにアクセスできない場合は、スパンのメタデータのみから分類します。

  • status(ok / error)、stop_reason、response_truncated
  • 子スパンの名前(ツール名、classify、generate-summary、suggest-action などのワークフローのステップ名)
  • iteration タグの数(LLM のラウンド総数)
  • 利用可能な場合は、所要時間とトークン数
ドリルダウン — 疑わしいツール結果を対象を絞って抽出

サマリーを確認した後、ツール一覧にクライアントでレンダリングされるツール、書き込み操作、または error_message が付いた反復処理が含まれるターンを詳しく確認します。これらのチェックにより、サマリーの確認ループでは検出できないサイレントな失敗を見つけられます。

トリガー:

  • クライアントでレンダリングされるツール(ブラウザーで処理されるもの。例:show_content、create_datadog_notebook、edit_datadog_notebook)— 存在する場合は必ず確認してください。サーバーがイベントを発行し、クライアントが表示を処理します。結果から、レンダリングが委任されたのか、確認されたのかがわかります。

    • "This tool is handled by the client application..." → ブラウザーに委任されており、ユーザーが何かを見たというサーバー側の確認はありません。次のターンの入力に client_tool_response があるか確認してください。存在しない場合、または次のターンでユーザーが困惑している様子が見られる場合は、出力が表示されなかったものとして扱ってください。これは強い否定的シグナルです。
    • それ以外の結果 → サーバー側で実行され、出力はレスポンスストリームに含まれています。
  • 書き込み操作(例:call_datadog_api、upsert_datadog_dashboard、upsert_widget、モニター/ノートブックの作成・更新)— アシスタントは実際の HTTP の結果にかかわらず、「X を更新しました」と説明することがよくあります。次の結果を確認してください。

    • 403/"not allowlisted"/"PUT not allowlisted" → 書き込みがブロックされており、変更は適用されていません。
    • 404 → アセットが見つかりません。存在しないものを編集しようとしています。
    • 500/"internal server error" → 書き込みの成否は不明で、反映されていない可能性があります。
    • "Session not found" → ツールの実行前にリソースの有効期限が切れています。
    • ペイロードを伴う 200/201 → 成功が確認されています。
  • 結果が空のデータクエリ(例:search_datadog_metrics、get_datadog_metric、search_datadog_logs、search_datadog_spans)— サマリーに同じクエリツールの反復処理が多数(5 回超)表示される場合は、結果が空だったか確認してください。NO_DATA/空の配列をもとに推論したアシスタントが、もっともらしく聞こえるものの捏造された分析を出すことがあります(hallucination の失敗モード)。

  • エラーが発生した反復処理 — error_message が設定されている反復処理をすべて確認し、正確なエラー、エラーを引き起こしたツール、アシスタントが再試行して成功したか、または断念したかを確認してください。一時的なエラーへの再試行は中立的です。恒久的な API 制限や同一の失敗の繰り返しは否定的です。

CLIENT_TOOLS = {'show_content', 'create_datadog_notebook', 'edit_datadog_notebook'}
WRITE_TOOLS  = {'call_datadog_api', 'upsert_datadog_dashboard', 'upsert_widget'}
FAIL_STRINGS = {'403', '404', '500', 'not allowlisted', 'not found', 'session not found', 'error'}

for it in iters:
    if it.get('error_message'):
        print(f"ERROR  iter={it['iteration']}: {it['error_message'][:200]}")
    for tc in it.get('tool_calls', []):
        name   = tc['name']
        result = (tc.get('result') or '')
        if name in CLIENT_TOOLS:
            print(f"CLIENT iter={it['iteration']} {name}: {result[:300]}")
        elif name in WRITE_TOOLS or any(s in result.lower() for s in FAIL_STRINGS):
            print(f"WRITE  iter={it['iteration']} {name}: {result[:300]}")

解釈の早見表:

結果のパターン評価のシグナル
"This tool is handled by the client application"レンダリング未確認 → 次のターンの client_tool_response を確認
403/"not allowlisted"書き込みがブロック — アシスタントでは本来の目的を達成できない可能性があります
"Session not found"リソースの有効期限切れ — ツール呼び出しの効果はありません
データクエリでの NO_DATA/空のリスト実質的なシグナルなし — アシスタントの回答にハルシネーションがないか確認
書き込み操作での 500不確実 — 反映されているかどうかは不明です
ペイロードを伴う 200/201成功が確認されています

上記のツール名一覧は、Datadog のアシスタントアプリ向けに調整した例です。他のアプリでは、C1 の tool_calls に表示されるツール名を確認し、その意味(クライアントでのレンダリング、サーバー側の副作用、読み取り専用のクエリ)を考慮してください。


セッションモード

単一の Datadog CMD+I アシスタントセッションを分類します。RUM の行動シグナルを使って、トレースに基づく評価を確認します。

ステップ 1 — ターンのルートスパンを列挙する
search_llmobs_spans(
  tags            = {"session_id": "<SESSION_ID>"},
  root_spans_only = True,
  from            = "now-90d",
  to              = "now",
  limit           = 500
)

pup モード:pup llm-obs spans search --query "@session.id:<SESSION_ID>" --root-spans-only --limit 500 --from 90d

query 文字列形式ではなく、tags={...} を使用してください。自由形式の query="session_id:<id>" は全文検索の動作を引き起こし、各ターンについてトレースバンドル全体を返します(スパン数が 5 倍になり、構造フィルターは黙って無視されます)。その結果、長いセッションでは行の上限に達します。tags={"session_id": "<id>"} はインデックス化されたタグフィルターに一致し、ほかのパラメーターも適用されます。

root_spans_only=True を指定すると、すべてのワークフロー/LLM/ツールの子孫スパンではなく、ユーザーの各ターンにつき 1 つのスパン(通常はルートエージェントスパン)が返されます。セッションに 500 ターンを超えるターンがある場合は、next_cursor を使ってページネーションしてください。

返された各ルートスパンについて、次の情報を記録します:

  • trace_id、span_id
  • start_ms、duration_ms
  • タグ:user_handle、user_id、org_id、product_area、message_id
  • ルートの name と span_kind(通常は agent/assistant ですが、アプリによって異なります)
  • input.preview、output.preview(ターン境界の特定にのみ使用します。以下の警告を参照してください)

ターンを start_ms の昇順に並べます。最初のターンのタグから、セッション単位の識別情報(user_handle、user_id、org_id、product_area、セッションの start_ms)を取得します。

プレビューの切り詰めに関する警告:input.preview と output.preview は約 200 文字に切り詰められています。これらはターン境界の特定や明らかな異常の検出には使えますが、判定の根拠としては不十分です。プレビューを見てすでに「結論が明らか」に見えるからといって、コンテンツ取得カスケードを省略しないでください。

search_llmobs_spans が結果を返さない場合 → 停止し、エラー llmobs_not_found を返します。

ターンごとの構造的な詳細(反復回数、ツール名、stop_reason、mcp フラグ、response_truncated)は、後ほどステップ 3(C1)で各ターンの get_llmobs_agent_loop の結果から取得します。

ステップ 2 — 評価とメタデータを取得する
get_llmobs_span_details(
  trace_id  = "<TRACE_ID>",
  span_ids  = ["<AGENT_SPAN_ID>"],
  from/to   = <same window>
)

evaluations マップのすべてのキーを順に確認し、各判定者の .value、.reasoning、.tags を記録します。 content_info.metadata からは、query_string、referrer_path、referrer_url、entities_json、user_info_json を取得します。

get_llmobs_span_details が失敗するか、空の結果を返す場合 → 何もせずにスキップし、ステップ 3 に進みます。

content_info に metadata が存在する場合は、get_llmobs_span_content(field="metadata") を使って取得し、実際の query_string と referrer_path を確認します。これにより、エージェントループを参照せずに、ユーザーの質問とアクセスしていたページを把握できます。

ステップ 3 — 会話全体を読む

ステップ 1 で列挙したすべてのターンのルートスパンについて、コンテンツ取得カスケードに従います。カスケードの並列処理ルールに従い、ターンごとのすべての C1 呼び出し(get_llmobs_agent_loop)を 1 回の並列バッチで実行し、C1 が iterations: [] を返したターンについては C2 に進みます。

LLM スパンがワークフローラッパーの配下にあるアプリ(例:get_answer_from_model_step を介して anthropic.request が孫スパンとなる assistant_api)では、C1 が空の反復結果を返すのはよくあることで、想定内です。カスケードは、それらのターンを C2 の expand_llmobs_spans ナビゲーションに振り分けて処理します。LLM 呼び出しのコンテンツを取得できない場合は、C3(ルートスパンのメタデータと出力)が最後の手段になります。

ステップ 4a — RUM セッション ID を特定する

メインの RUM クエリを実行する前に、最初のターンの start_ms を基準に前後 2 分の範囲を絞り込んで検索し、ユーザーが利用していた正確なブラウザーセッションを特定します。これにより RUM の session_id を切り分け、時間範囲を広くして @usr.email のみでフィルターした場合に発生するセッション間の混入を防げます(1 日の間に複数のタブを使っているユーザーの場合、それらすべてのイベントが返されます)。

analyze_rum_events(
  event_type    = "action",
  filter        = "@usr.email:<user_handle> @action.type:custom",
  from          = <start_ms - 120000>,   # 2 minutes before first turn's start_ms (epoch ms)
  to            = <start_ms + 120000>,   # 2 minutes after
  sql_query     = 'SELECT DISTINCT session_id FROM rum LIMIT 10'
)

重複しない session_id の値をすべて抽出し、rum_session_ids に保存します。以降のすべてのクエリで使う RUM フィルター(以下の <rum_filter>)は、結果に応じて次のように決定します。

  • 結果が 0 件 → <rum_filter> = @usr.email:<user_handle>(メールアドレスと時間範囲にフォールバックします)。
  • 結果が 1 件 → <rum_filter> = @session.id:<rum_session_id>。
  • 結果が複数件 → <rum_filter> = @session.id:(<id1> OR <id2>)。

pup モード:pup rum aggregate --user-email <user_handle> --query "@action.type:custom" --from <start_ms - 120000> --to <start_ms + 120000> --compute count --group-by @session.id を実行します。--group-by フラグ名は pup rum aggregate --help で確認してください。

ステップ 4 — RUM の行動シグナルを取得する

pup モード:analyze_rum_events の呼び出しを pup rum aggregate の呼び出しに置き換えます(「ツールリファレンス」を参照)。ページビュー:pup rum aggregate --user-email USER_HANDLE --from START_MS --to END_MS --compute count --group-by @session.id。カスタムアクション:pup rum aggregate --user-email USER_HANDLE --query "@action.type:custom" --from START_MS --to END_MS --compute count --group-by @evt.name。API がクエリを拒否した場合は、イベントタイプのフィルターフラグを pup rum aggregate --help で確認してください。

start_ms と(ステップ 4a の)<rum_filter> を使い、時間範囲を次のように定義します。

  • pre:[start_ms − 30min, start_ms]
  • during:[start_ms, start_ms + session_duration_ms]
  • post:[start_ms + session_duration_ms, start_ms + session_duration_ms + 60min]

並行して実行します。

RUM クエリ A — ページビューのタイムライン:

analyze_rum_events(
  event_type    = "view",
  filter        = "<rum_filter>",
  from/to       = <pre to post>,
  sql_query     = "SELECT timestamp, view_url, \"@view.time_spent\" FROM rum ORDER BY timestamp LIMIT 200",
  extra_columns = [{"name": "@view.time_spent", "type": "int64"}]
)

@view.time_spent の単位はナノ秒です。

RUM クエリ B — カスタムアクション(製品領域で絞り込み):

analyze_rum_events(
  event_type    = "action",
  filter        = "@action.type:custom <rum_filter>",
  from/to       = <pre to post>,
  sql_query     = """
    SELECT timestamp, "@action.name", view_url FROM rum
    WHERE (
      "@action.name" LIKE 'command-assistant%'
      OR "@action.name" LIKE 'workbench%'
      OR "@action.name" LIKE 'ai-experiences%'
      OR "@action.name" = 'click on Bad response'
      OR "@action.name" = 'click on Incorrect result'
      OR "@action.name" = 'click on Submit'
    )
    ORDER BY timestamp LIMIT 200
  """,
  extra_columns = [{"name": "@action.name", "type": "string"}]
)

上記の LIKE フィルターは Datadog アシスタントの例です。分類対象のアプリで意味のあるアクション名のプレフィックスに合わせて絞り込むか、フィルターを削除してセッション上のすべてのカスタムアクションを読み取ってください。

いずれかのクエリで 0 行が返された場合、または権限/認証エラーが発生した場合も、分類を中断してはいけません。RUM の欠落や不完全なデータはカバレッジの不足であり、行動の証拠ではありません。これを否定的な証拠として扱うことは、文書化されている失敗パターンです。

条件対応
セッション上の RUM 行が 0 件で、ユーザーにはより広い 30 日間の範囲で SOME RUM データがある(セッション日の Web RUM の欠落)劣化モードで処理します。RUM なしで続行し、トレースの証拠(ステップ 3)と監査シグナル(該当する場合はステップ 4b)を使用します。出力に rum_coverage: partial — web RUM gap on session date を設定します。
analyze_rum_events/pup rum が 1 つ以上のクエリで権限または認証エラーを返す(外部組織、SOC2 で分離されたテナントでよく発生)劣化モードで処理します。行が返された RUM クエリを使用し、不足部分はトレースと監査にフォールバックします。出力に rum_coverage: partial — auth error on <query> を設定します。
セッション上の RUM 行が 0 件で、30 日間にユーザーの RUM データがどこにもない劣化モードで処理します。トレースと監査を使って続行します。出力に rum_coverage: unavailable — no RUM data for user を設定します。終端エラーとして rum_unavailable を出力してはいけません。

厳守事項: RUM が存在しないことを、否定的な判定の証拠として使ってはいけません。トレースと監査のシグナルが満足を示している場合、RUM がないことでその判定を覆してはいけません。トレースのシグナルが曖昧な場合は、行動データが欠落していることを理由に不利な判断をせず、トレースのみの判定をデフォルトとします。

rum_unavailable エラーコードは、分類プロトコルで RUM が必須であり(たとえば、トレースの内容が大幅にマスキングされ、RUM だけが残されたシグナルである場合)、かつ RUM が存在しないという、復旧不可能なケースに限って使用します。それ以外の場合は、劣化モードで続行します。

シグナル解釈フレームワーク

RUM アクションは、どれも同じ証拠になるわけではありません。3 つのティアに分類し、各アクションの重みをステップ 5 でティアに応じて調整します。以下のカテゴリはエージェントに依存しません。ご利用のアプリに固有の RUM アクション名を、これらのカテゴリに対応付けてください。満足度との相関を示す証拠が得られるまでは、認識されないアクションを「曖昧」として扱ってください。

ティア A — 単独で十分な証拠(存在する場合、トレースの判定を覆す、または確定できる)

抽象カテゴリ方向アクション名のパターン例
応答の完了前にストリームの中断が発生ネガティブ*.chat-cancel、*.stream-abort、*.cancel-generation
ストリーム中にパネル/ウィンドウが閉じられ、1 秒以内に再度開かれないネガティブエージェントの LLM スパンがまだアクティブな状態での *.panel.close
エージェントが応答内で明示したアセット ID を含む URL へのナビゲーションポジティブエージェントが参照した正確なリソースパスの表示
セッション終了から 60 秒以内に、エージェントの出力に対して行われた成果物アクションポジティブ生成されたアセットを対象としたエクスポート、保存、変更、共有
同じインテントで 15 分以内に新しいセッションを開始(ステップ 1b の分類器による再試行)ネガティブ近接セッション分析で検出
エージェントの計画を全面的に拒否(reject_all または同等の操作)ネガティブ一括拒否の操作、ストリーム中の「停止」アクション

ティア B — 補強材料。単独では決定的な証拠にならない

抽象カテゴリ方向
エージェントのビューの滞在時間が短い(合計 30 秒未満)ネガティブ
セッション後に外部のヘルプ(ドキュメント、サポート、コミュニティ)へ移動ネガティブ
応答前にパネル/ビューを繰り返し開くネガティブ
エージェントのビューの滞在時間が長い(60 秒超)ポジティブ
エージェントが表示したマークダウンリンクをクリックポジティブ
セッション後にトピック関連のリソースへ移動(正確なアセットとの一致はなし)ポジティブ

曖昧 — 単独の証拠として使用しないでください

抽象カテゴリ曖昧な理由ルール
中間状態(ツール呼び出しパネル、推論ブロック)の表示/展開操作熱心に読み込んでいる場合にも、低評価を付ける前に精査している場合にも発生します。両者を区別する合理的なルールはありません同じ方向を示すティア A、または対応するティア B のシグナルと組み合わせてください
ツール呼び出しを 1 件承認(N 件の提案のうち 1 件に対する accept)期待される UX 上の手順です。平凡な応答でもユーザーは「承認」をクリックします弱いポジティブとして扱い、「はい」の主要な証拠には決してしないでください
ツール呼び出しを 1 件拒否(N 件の提案のうち 1 件に対する reject)「ユーザーが不満」ではなく、「ユーザーがより良い方法へ誘導した」可能性がありますユーザーがその後メッセージを送らなくなった場合、または後続の呼び出しがすべて拒否された場合にのみ、ネガティブとしてカウントしてください
ストリーム中であることが不明な状態でのパネル/ビューのクローズ「セッションが自然に終了した」と「途中で放棄された」を区別できませんストリームがアクティブだったタイミングの情報が必要です。ティア A を参照してください

ご利用のエージェントの場合: 分類する前に、アプリの RUM アクション名をこれらのカテゴリに対応付けてください。Datadog CMD+I アシスタントの対応付け例は @rum-actions-bits-assistant.md にあります。ご利用のアプリの RUM イベント分類で確認すべき項目のテンプレートとして使用してください。認識されないアクション名は、デフォルトで「曖昧」とします。満足度との相関を示す証拠(ラベル付きセッションまたは製品知識)が得られた後に限り、ティア B またはティア A に昇格させてください。

Datadog アシスタントのアクション名を具体的に解釈する際は、@rum-actions-bits-assistant.md を参照してください。

ステップ 4b — 監査証跡のシグナルを取得する(アセットの作成・編集があった場合)

監査証跡には、アシスタントの操作がサーバーで確認された結果(反映された書き込み、定着した変更、削除、後続の編集)と、同じアセットに対するセッション後のユーザーの行動が記録されます。アシスタントが Datadog アセット(ダッシュボード、ノートブック、モニター、SLO など)を作成または編集したセッションでは、これは最も信頼性の高い行動シグナルです。RUM のクリックよりも直接的です。

主なツールは pup audit-logs search です。OAuth を使って、アクティブなユーザー自身の組織を照会します(pup auth login)。

このステップを実行するタイミング:

  • トレース内のツール呼び出しに書き込み操作(upsert_*、PUT/POST を伴う call_datadog_api、モニター/ノートブックの作成または更新)が含まれる場合 → 実行します。
  • セッションに既知のアセット ID(ツール呼び出しの引数、dashboard_id タグ、セッションの referrer_path、またはユーザーのクエリから取得)がある場合 → 実行します。
  • トレースが完全な読み取り専用(書き込みなし、アセット ID なし)の場合 → スキップします。ステップ 4 に加える監査情報はありません。

クエリの優先順位(シグナルが強い順、ノイズが少ない順):

  1. @asset.id — 完全な差分を含む変更/削除履歴です。最も信頼性が高い情報です。アセット ID が既知の場合は必須です。
  2. Q3(対象を絞った HTTP パス) — 同じアセットに対する書き込み回数とタイミングです。アセット ID が既知の場合は必須です。
  3. Q1/Q2(広範なシグナルを対象とする HTTP 監査) — 代替手段としてのみ使用します。アクティブなユーザーではノイズが多くなります。
@asset.id — 変更履歴
pup audit-logs search \
  --query '@asset.id:<asset_id>' \
  --from "<session_start - 30d, ISO>" --to now --limit 50

各行の本文は row["attributes"]["attributes"] に格納されています。フィールド:

  • action — created/modified/deleted/accessed
  • usr.email
  • asset.type(dashboard、notebook、monitor、…)、asset.id、asset.name
  • asset.new_value/asset.prev_value — 変更前後の状態全体をネストされた辞書として格納します(そのまま使用でき、JSON の解析は不要です)
  • timestamp — ISO8601 形式の文字列

Pup はデフォルトで降順の結果を返します。セッションの時系列を確認できるよう、昇順に並べ替えます。

rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])

シグナル:

session_end 後のイベント判定のシグナル
deleted イベント強い否定的シグナル — アセットが削除されました
ユーザーによる modified で、ウィジェット/セル/項目の数が prev_value より少ない否定的シグナル — ユーザーがアシスタントの変更を元に戻しました
ユーザーによる modified で、項目の数が prev_value より多い肯定的シグナル — ユーザーがアシスタントの作成物を拡張しました
クエリ/しきい値が変更された modified(モニター)否定的シグナル — ユーザーはアシスタントが設定した値に同意しませんでした
メッセージ/タグのみが変更された modified中立 — 外観上の変更です
イベントなし中立から肯定的 — アセットに変更はありません

監査シグナルを過剰に解釈しないでください。 監査で測定するのはバックエンドの正確性であり、ユーザーの満足度ではありません。書き込みが見当たらないことやイベントがないことだけでは、不満の証拠にはなりません。例外はユーザーによる明示的な操作です。deleted イベント、ウィジェット/セルを削除する手動の modified、またはエージェントの生成内容を上書きする手動の書き込みは、ユーザーの意図を直接示すため、判定を覆す可能性があります。書き込みがゼロなのに受け入れられた場合や PUT が見当たらない場合など、バックエンドの症状を示すシグナルは、トレースと RUM を裏付けるためにのみ使用し、それだけで判定を覆してはいけません。

ダッシュボードでは、ウィジェット数の差分が最も役立ちます。

nv = row["attributes"]["attributes"]["asset"]["new_value"]
pv = row["attributes"]["attributes"]["asset"]["prev_value"]
delta = len(nv["dashboard_definition"]["widgets"]) - len(pv["dashboard_definition"]["widgets"])
Q3 — 対象を絞った HTTP パス(書き込み数とタイミング)

@asset.id で差分を確認できる一方、Q3 ではリソースへの書き込みの HTTP イベント数を確認できます。トレース内のツール呼び出しを照合したり、セッション後に手動で編集された箇所を見つけたりするのに役立ちます。

# Dashboard:
pup audit-logs search \
  --query '@usr.email:<user_handle> @http.url_details.path:/api/v1/dashboard/<dashboard_id>' \
  --from "<session_start - 30min, ISO>" --to "<session_end + 2h, ISO>" --limit 200

# Notebook:
pup audit-logs search \
  --query '@usr.email:<user_handle> @http.url_details.path:/api/v2/notebook/<notebook_id>' \
  --from ... --to ... --limit 200

イベントをフェーズごとに分類すると、セッション中とセッション後に何が起きたかを確認できます。

from collections import Counter
phase_method = Counter()
rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])
for r in rows:
    a = r["attributes"]["attributes"]
    ts = a["timestamp"]; m = a["http"]["method"]; s = a["http"]["status_code"]
    if   ts < session_start_iso:  phase = "pre-session"
    elif ts < session_end_iso:    phase = "during"
    elif ts < feedback_iso:       phase = "session_end->feedback"  # only when feedback time is known
    else:                         phase = "post"
    phase_method[(phase, m, s)] += 1

PUT 1 件が 1 回の書き込みです。200/201 は成功が確認されたことを示します。status_code: 0 は、ブラウザーからリクエストが送信されたものの、レスポンスが届く前にページ遷移したことを意味します。サーバーではほぼ確実に処理されているため、200 と合わせて数えてください。

解釈:

  • during PUT 200 の件数は、トレース内の upsert ツール呼び出し数と一致するはずです。
  • session_end の 10~30 秒後に記録され、最終ターンで予想される書き込み数と一致する書き込みは、非同期ツール実行の遅延によるものです。ユーザーが開始したものではありません。
  • session_end の 30 秒後を超えて発生した書き込みは、ユーザーによる手動編集です。セッション後の書き込みとして数えてください。短時間にまとまって発生する少数の書き込み(例:1 分ほどで 5~10 件の PUT)は微調整です(やや良好な兆候です。ユーザーは結果を採用し、さらに改善しています)。セッション後の PUT がゼロなら、問題なく受け入れられたことを示します。
  • キャンセルが確認されたセッションでは、during PUT イベントはゼロです。
Q1/Q2 — 広範なシグナルを対象にした HTTP 監査(代替手段)

アセット ID が不明な場合、または対象を絞ったクエリの後に再試行やサポートチケットのシグナルを探す場合にのみ使用してください。

# Q1: broad signal, session ± window
pup audit-logs search \
  --query '@usr.email:<user_handle>' \
  --from "<session_start - 15min, ISO>" --to "<session_end + 30min, ISO>" --limit 200

# Q2: post-session, strip the highest-volume noise paths
pup audit-logs search \
  --query '@usr.email:<user_handle> [email protected]_details.path:/api/ui/query/scalar [email protected]_details.path:/api/ui/query/timeseries' \
  --from "<session_end, ISO>" --to "<session_end + 30min, ISO>" --limit 200

アクティブユーザーではノイズが大半を占めます。 200 行の上限は実際に適用され、ページネーション用のカーソルもありません。必要に応じて除外する、その他の高ボリュームなパス:/api/ui/frontend_telemetry/metrics、/api/v1/logs-analytics/aggregate、/api/v1/logs-analytics/list、/api/v1/logs-analytics/facet_info、/api/v2/watchdog/insights/search/*、/api/v2/notifications/notify/*、/api/v2/notifications/presence/*。これらを除外しても上限に達する場合は、時間範囲を狭めてください。

次の項目を探してください:サポートチケットの作成(POST /api/v2/cases)、アシスタント呼び出しの再試行(POST /api/v2/assistant)、ドキュメントへの移動(ここではなく RUM のステップ 4 で確認できます)。

レスポンス抽出ヘルパー

二重にネストされたエンベロープ(row["attributes"]["attributes"])は冗長です。スクリプトごとに一度正規化してください。

import json, subprocess

def pup_audit(query, frm, to, limit=200):
    res = subprocess.run([
        "pup", "audit-logs", "search",
        "--query", query, "--from", frm, "--to", to, "--limit", str(limit),
    ], capture_output=True, text=True, check=True)
    d = json.loads(res.stdout)
    rows = []
    for r in d.get("data", []):
        a = r["attributes"]["attributes"]
        rows.append({
            "ts":     a.get("timestamp"),
            "method": a.get("http", {}).get("method"),
            "path":   a.get("http", {}).get("url_details", {}).get("path"),
            "status": a.get("http", {}).get("status_code"),
            "action": a.get("action"),
            "asset":  a.get("asset", {}),
            "usr":    a.get("usr", {}).get("email"),
        })
    rows.sort(key=lambda r: r["ts"])
    return rows
グレースフルデグラデーション

クエリの結果が 0 行で、かつセッションが外部顧客の組織で発生している場合は、エラーにしないでください。詳細出力に audit_unavailable を記録し、トレースと RUM による調査を続けてください。一部の外部顧客の監査証跡(特に AP1/SOC2 分離組織)は pup からアクセスできません。トレースと RUM だけでも判定には十分です。

ステップ 4b — 監査証跡のシグナルを取得する(アセットが作成または編集された場合)

監査証跡では、アシスタントのアクションによってサーバーで確認された結果(反映された書き込み、適用された変更、削除、後続の編集)と、同じアセットに対するセッション後のユーザー行動が確認できます。アシスタントが Datadog アセット(ダッシュボード、ノートブック、モニター、SLO など)を作成または編集したセッションでは、これは最も信頼性の高い行動シグナルであり、RUM のクリックよりも直接的です。

主なツール:pup audit-logs search。OAuth(pup auth login)を使用して、現在のユーザー自身の組織にクエリします。

このステップを実行する場合:

  • トレースのツール呼び出しに書き込み操作(upsert_*、PUT/POST を伴う call_datadog_api、モニター/ノートブックの作成または更新)が含まれている場合 → 実行してください。
  • セッションのアセット ID が判明している場合(ツール呼び出しの引数、dashboard_id タグ、セッションの referrer_path、ユーザーのクエリから特定できる場合) → 実行してください。
  • トレースが読み取り専用のみ(書き込みなし、アセット ID なし)の場合 → スキップしてください。ステップ 4 に追加できる監査情報はありません。

クエリの優先順位(シグナルが強い順、ノイズが少ない順):

  1. @asset.id — 詳細な差分を含む変更/削除履歴。最も信頼性が高い情報です。アセット ID が判明している場合は必須です。
  2. Q3(対象を絞った HTTP パス) — 同じアセットの書き込み数とタイミング。アセット ID が判明している場合は必須です。
  3. Q1/Q2(広範なシグナルを対象にした HTTP 監査) — 代替手段としてのみ使用します。アクティブユーザーではノイズが多くなります。
@asset.id — 変更履歴
pup audit-logs search \
  --query '@asset.id:<asset_id>' \
  --from "<session_start - 30d, ISO>" --to now --limit 50

各行の本文は row["attributes"]["attributes"] に格納されています。フィールド:

  • action — created / modified / deleted / accessed
  • usr.email
  • asset.type(dashboard、notebook、monitor、…)、asset.id、asset.name
  • asset.new_value / asset.prev_value — 変更前後の状態全体をネストされた辞書として格納しています(そのまま使用でき、JSON の解析は不要です)
  • timestamp — ISO8601 形式の文字列

Pup はデフォルトで降順に返します。時系列でセッションの推移を確認するには、昇順に並べ替えます。

rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])

シグナル:

session_end 後のイベント判定シグナル
deleted イベント強いマイナス — アセットが削除されました
ユーザーによる modified で、prev_value よりウィジェット/セル/アイテムが少ないマイナス — ユーザーがアシスタントの変更を取り消しました
ユーザーによる modified で、prev_value よりアイテムが多いプラス — ユーザーがアシスタントの作成物を拡張しました
クエリ/しきい値が変更された modified(monitor)マイナス — ユーザーがアシスタントの値に同意しませんでした
メッセージ/タグのみが変更された modified中立 — 外観上の変更です
イベントなし中立からプラス — アセットに変更はありません

ダッシュボードでは、ウィジェット数の差分が最も有用です:

nv = row["attributes"]["attributes"]["asset"]["new_value"]
pv = row["attributes"]["attributes"]["asset"]["prev_value"]
delta = len(nv["dashboard_definition"]["widgets"]) - len(pv["dashboard_definition"]["widgets"])
Q3 — 対象を絞った HTTP パス(書き込み数+タイミング)

@asset.id で差分を確認できる一方、Q3 ではリソースへの書き込みに関するHTTP イベント数を確認できます。トレース内のツール呼び出しを照合したり、セッション後の手動編集を特定したりするのに役立ちます。

# Dashboard:
pup audit-logs search \
  --query '@usr.email:<user_handle> @http.url_details.path:/api/v1/dashboard/<dashboard_id>' \
  --from "<session_start - 30min, ISO>" --to "<session_end + 2h, ISO>" --limit 200

# Notebook:
pup audit-logs search \
  --query '@usr.email:<user_handle> @http.url_details.path:/api/v2/notebook/<notebook_id>' \
  --from ... --to ... --limit 200

イベントをフェーズごとに分類して、セッション中とセッション後に何が起きたかを確認します:

from collections import Counter
phase_method = Counter()
rows = sorted(d["data"], key=lambda r: r["attributes"]["attributes"]["timestamp"])
for r in rows:
    a = r["attributes"]["attributes"]
    ts = a["timestamp"]; m = a["http"]["method"]; s = a["http"]["status_code"]
    if   ts < session_start_iso:  phase = "pre-session"
    elif ts < session_end_iso:    phase = "during"
    elif ts < feedback_iso:       phase = "session_end->feedback"  # only when feedback time is known
    else:                         phase = "post"
    phase_method[(phase, m, s)] += 1

各 PUT は1回の書き込みです。200/201 は成功が確認されたことを示します。status_code: 0 は、ブラウザーがリクエストを送信したものの、応答が届く前に画面遷移したことを意味します。サーバーではほぼ確実に処理されているため、200 と合わせて数えます。

解釈:

  • during PUT 200 の件数は、トレースの upsert ツール呼び出し回数と一致するはずです。
  • session_end の10~30秒後に記録され、件数が最後のターンで予想される書き込み数と一致する書き込みは、非同期ツール実行の遅延です。ユーザーが開始したものではありません。
  • session_end の30秒を過ぎてからの書き込みは、ユーザーによる手動編集です。セッション後の書き込みとして数えます。短時間に集中した少数の書き込み(例:1分ほどで5~10件の PUT)は微調整です(ややプラス — ユーザーは結果を維持し、改善しました)。セッション後の PUT がゼロなら、問題なく受け入れられています。
  • キャンセルが確認されたセッションでは、during PUT イベントはゼロです。
Q1 / Q2 — 広範なシグナルを対象とした HTTP 監査(フォールバック)

アセット ID が不明な場合、または対象を絞ったクエリの後に再試行/サポートチケットのシグナルを探す場合にのみ使用します。

# Q1: broad signal, session ± window
pup audit-logs search \
  --query '@usr.email:<user_handle>' \
  --from "<session_start - 15min, ISO>" --to "<session_end + 30min, ISO>" --limit 200

# Q2: post-session, strip the highest-volume noise paths
pup audit-logs search \
  --query '@usr.email:<user_handle> [email protected]_details.path:/api/ui/query/scalar [email protected]_details.path:/api/ui/query/timeseries' \
  --from "<session_end, ISO>" --to "<session_end + 30min, ISO>" --limit 200

アクティブユーザーではノイズが大半を占めます。 200行の上限は固定で、ページネーションカーソルはありません。必要に応じて除外する、他の大量に発生するパス:/api/ui/frontend_telemetry/metrics、/api/v1/logs-analytics/aggregate、/api/v1/logs-analytics/list、/api/v1/logs-analytics/facet_info、/api/v2/watchdog/insights/search/*、/api/v2/notifications/notify/*、/api/v2/notifications/presence/*。除外後も上限に達する場合は、時間範囲を狭めてください。

次の項目を探します:サポートチケットの作成(POST /api/v2/cases)、アシスタント呼び出しの再試行(POST /api/v2/assistant)、ドキュメントへの移動(ここではなく RUM Step 4 に表示されます)。

応答抽出ヘルパー

二重にネストされたエンベロープ(row["attributes"]["attributes"])は冗長です。スクリプトごとに一度だけ正規化します:

import json, subprocess

def pup_audit(query, frm, to, limit=200):
    res = subprocess.run([
        "pup", "audit-logs", "search",
        "--query", query, "--from", frm, "--to", to, "--limit", str(limit),
    ], capture_output=True, text=True, check=True)
    d = json.loads(res.stdout)
    rows = []
    for r in d.get("data", []):
        a = r["attributes"]["attributes"]
        rows.append({
            "ts":     a.get("timestamp"),
            "method": a.get("http", {}).get("method"),
            "path":   a.get("http", {}).get("url_details", {}).get("path"),
            "status": a.get("http", {}).get("status_code"),
            "action": a.get("action"),
            "asset":  a.get("asset", {}),
            "usr":    a.get("usr", {}).get("email"),
        })
    rows.sort(key=lambda r: r["ts"])
    return rows
グレースフルデグラデーション

クエリの結果が0行で、かつセッションが外部顧客の組織に属している場合は、エラーにせず、詳細出力に audit_unavailable を設定して、トレース+RUM による処理を続行します。一部の外部顧客の監査証跡(特に AP1/SOC2 で分離された組織)は pup 経由では参照できません。判定にはトレース+RUM だけでも十分です。

ステップ 5 — 分類する

会話(ステップ 3)、評価(ステップ 2)、RUM シグナル(ステップ 4)、監査シグナル(実行した場合はステップ 4b)を使用します。

満足度の判定:

  • yes:最終応答がユーザーの意図に直接答えており、否定的なフィードバックや離脱シグナルがありません
  • partial:一部は正しいものの、重要な点が欠けているか、ユーザーが追加の手間をかけて初めて問題を解決できました
  • no:否定的なフィードバックがあった、ユーザーが離脱した、または根本的な意図を構造上実現できません

失敗モードコード(yes の場合は省略):

コード意味
wrong_answer事実に反する主張
incomplete_answer正しいものの、重要な経路を見落とした
broke_existing_stateユーザーが利用していた状態を損なった
excessive_turns目標は達成したものの、やり取りが多すぎた
context_loss以前のコンテキストを忘れた、または同じ間違いを繰り返した
wrong_tool_use不適切なツールまたはパラメーターを使用した
hallucinationツールの結果にない ID、URL、事実を捏造した
other: <describe>

簡潔な出力(デフォルト):

verdict: yes | partial | no
reason: <one sentence>

詳細な出力(要求された場合のみ):

# Classification: <session_id>

## Session metadata
- **Trace ID:** <trace_id>
- **Agent span ID:** <span_id>
- **RUM session ID(s):** <resolved in Step 4a, or "fallback to email filter" if Step 4a returned 0>
- **Start:** <UTC timestamp>
- **Duration:** <seconds>s
- **Turns:** <N>
- **User:** <user_handle>
- **Product area:** <tag value>
- **Model:** <matched_model_name>
- **Iterations:** <N> (stop reason: <end_turn|tool_use>)
- **Tools called:** <names, counts>
- **Evaluations:** <name: value — "reasoning excerpt"> for each judge
- **Referrer page:** <referrer_path>
- **Content source:** agent_loop | span_content_messages | span_content_io | structural

## User intent
One sentence.

## What the assistant did
- Bullet 1
- Bullet 2

## Was the core intent satisfied?
**yes / partial / no** — one sentence justification.

## Failure mode
- `code`: explanation  (omit if yes)

## RUM behavioral signals

### Pre-session context (from ROUTE_CONTEXT or referrer_path)
What the user was working on before the session.

### Assistant panel actions
| Time | Action | Page |
|------|--------|------|

### Post-session navigation
| Time | URL | Dwell |
|------|-----|-------|

### RUM verdict
One sentence: does behavioral evidence support or contradict the trace-only verdict?

## Audit-trail findings (if Step 4b ran)
- **Asset:** <asset_id> (<type>)
- **In-session writes:** <PUT count> (<status breakdown>)
- **Post-session manual edits:** <count, with timing>
- **Asset deleted or torn down:** yes / no
- **Diff signal:** <e.g. "widget count: 8 → 11 (user extended)" or "audit_unavailable">

## Revised satisfaction verdict (with RUM + audit)
yes / partial / no

トレースモード

trace_id を指定して、単一の Agent Observability トレースを分類します。RUM は使用しません。あらゆる LLM アプリで動作します。

ステップ T1 — スパン構造を取得する
search_llmobs_spans(
  query  = "trace_id:<TRACE_ID>",
  from   = "now-30d",
  to     = "now",
  limit  = 50
)

結果から、スパン階層全体を把握します。

  • ルートスパン(parent_id: undefined のもの)を特定します
  • ルートスパンの span_kind を確認します。agent、workflow、llm、その他の場合があります
  • 存在するすべてのスパン種別を抽出します:agent、llm、tool、workflow、retrieval、embedding
  • タグを抽出します:ml_app、service、session_id、investigation.id、issue_id、user_handle、org_id、matched_model_name、stop_reason、current_turn、iteration

スパン構造からアプリの種類を判定します:

シグナルアプリの種類
ルートが embedding のみエンベディングパイプライン — 以下の分類不能ガードを参照
span_kind=agent がどこかに存在するエージェントアプリ
span_kind=llm のスパンのみLLM/チャットアプリ
ルートが span_kind=retrieval または span_kind=workflow で、LLM の子スパンがあるパイプラインアプリ

エージェントスパンを探します(存在する場合): エージェントスパンが常にルートとは限りません。すべてのスパンから span_kind=agent を探します。workflow のルート(例:investigate → Agent workflow → generate-summary)の子または孫スパンの場合があります。エージェントループの呼び出しには、最も深い、または最も意味のあるエージェントスパンを使用します。

スパンが見つからない場合 → 停止し、エラー llmobs_not_found を返します。

分類不能ガード: すべてのスパンが span_kind=embedding で、messages の内容がない場合は、停止して次を返します。

{
  "error": "unclassifiable_app_type",
  "detail": "All spans are embedding kind with no conversational content. Use status:ok/error rates instead."
}
ステップ T2 — スパンの詳細と評価を取得する

ルートスパン(およびルートとは異なる場合はエージェントスパン)に対して get_llmobs_span_details を呼び出します。以下を抽出します。

  • children_ids → 階層を確認します
  • evaluations map → 判定者による判定があるかを確認します(すべてのキーを順に確認し、WARN ステータスの評価は該当なしとして扱います)
  • content_info → 存在するフィールドとそのサイズを確認します
ステップ T3 — 内容を読み取る

エージェントスパン(C1)、次に LLM の子スパン(C2)、次にルートスパン(C3)、最後に構造シグナル(C4)の順に、コンテンツ取得カスケードに従います。

観測された具体的なパターンに関する注意:

  • service:ai_gateway の LLM スパン(bits-copilot、assistant_api):AI Gateway プロキシによる独立した chat_completion-call または messages-call スパンです。これらでは get_llmobs_span_content(messages) が安定して機能します。
  • generate-summary エージェントスパン(error-tracking-investigator):エージェントループは機能しますが、IO トレースがフィーチャーフラグで制御されている場合、内容が REDACTED になることがよくあります。構造シグナルに切り替えます。子スパン名(classify、suggest-action、add-markdown-links)、ツール呼び出し、current_turn タグから得られるターン数、全体の status を確認します。
  • assistant エージェントスパン(assistant_api):LLM スパンが get_answer_from_model_step ワークフローを介して孫スパンになっているため、エージェントループの反復処理は空になります。query_string には get_llmobs_span_content(metadata) を、応答には get_llmobs_span_content(output) を使用します。
ステップ T4 — 分類

内容と評価を使用します。

満足度の基準:

  • yes:出力がタスクに直接かつ完全に対応しています。エラー、途中での切り捨て、拒否はありません。
  • partial:タスクの一部に対応しており、正確ですが不完全、または品質が低下しています。
  • no:タスクに対応できていません。幻覚、エラー、空の応答、誤ったツールの使用、構造的に実現不可能な場合などです。
  • コンテンツが完全に REDACTED の場合は、構造上のシグナルから分類します。status:error → no、想定される子スパン名を含めて正常に完了 → yes 寄りとし、信頼度が低いことに留意します。

失敗モードコード: セッションモードと同じ分類体系です(上記を参照)。

コンパクトな出力(デフォルト):

verdict: yes | partial | no
reason: <one sentence>
confidence: high | medium | low   ← include when content was partially or fully unavailable

詳細な出力(要求された場合):

# Classification: <trace_id>

## Trace metadata
- **Trace ID:** <trace_id>
- **Root span:** <name> (<kind>)
- **Agent span:** <name> (<span_id>) — or "none"
- **ML app:** <ml_app>
- **Service:** <service>
- **Start:** <UTC timestamp>
- **Duration:** <seconds>s
- **Status:** ok | error
- **Session/business ID:** <session_id or investigation.id or issue_id if present>
- **Org:** <org_id if present>
- **Models:** <matched_model_name values>
- **Evaluations:** <name: value — "reasoning excerpt"> or "none"
- **Content source:** agent_loop | span_content_messages | span_content_io | structural

## Task / User query
One sentence.

## What the app produced
- Bullet 1
- Bullet 2

## Was the task completed?
**yes / partial / no** — one sentence justification.

## Failure mode
- `code`: explanation  (omit if yes)

ML アプリモード

指定された LLM アプリから複数のセッションまたはトレースをサンプリングして分類します。チームごとの多様なインストルメンテーションパターンに対応します。

ステップ M1 — スパンをサンプリングし、アプリを把握する
search_llmobs_spans(
  query           = "@ml_app:\"<ml_app>\" @status:ok",
  root_spans_only = true,
  from            = "<timeframe>",
  to              = "now",
  limit           = 100,
)

スパンが見つからない場合 → 停止して、次を返します:

{
  "error": "no_traces_found",
  "detail": "No spans found for ml_app '<ml_app>' in the given timeframe. Check the app name and timeframe."
}

分類不能ガード: すべてのルートスパンが span_kind=embedding の場合 → 停止して、次を返します:

{
  "error": "unclassifiable_app_type",
  "detail": "<ml_app> only contains embedding spans. No conversational content to classify. Use status:ok/error rates instead."
}

妥当性チェック: 分類する前に、サンプリングしたルートスパンにざっと目を通します。service:、スパン名、タグを確認し、想定しているアプリのスパンであることを確かめます。無関係に見える外れ値は、先に除外してください。

ルートスパン種別の分析:

ルートスパン種別意味対応
agentエージェントスパンがルートです分類単位としてそのまま使用します
workflow / taskパイプラインのラッパーです。エージェントはネストされている場合がありますサンプルトレースで search_llmobs_spans(trace_id=<id>) を呼び出し、ネストされたエージェントスパンを見つけます
llmオーケストレーションのないフラットな LLM アプリですLLM スパンを直接使用します
embedding のみ埋め込みパイプラインです分類不能です(上記のガードを参照)
トレース間で混在複数のサービスまたはモードがありますservice: タグでグループ化し、個別に扱います

LLM Experiments のトレース:ルートスパンが span_kind: experiment を持ち、構造化フィールド input、output、expected_output を含む場合、Datadog LLM Experiments のトレースを確認しています。各 experiment スパンは、データセット内の1レコードの実行を表します。内容はルートスパンの構造化 I/O フィールドにあります。LLM サブスパンのメッセージカスケード(C1/C2)をたどるのではなく、ルートスパンで get_llmobs_span_content(field="input"/"output"/"expected_output") を使用してください。experiment のトレース内の LLM サブスパンには、スタブまたはプレースホルダーの内容が含まれる場合があります。

グループ化の粒度 — セッションとトレース:

まず、グループ化に使用するビジネス ID タグを特定します。次の手順に従ってください。

オプション A — aggregate_spans(推奨): アプリが APM スパンを出力する場合(ほとんどの Go/Java/Python サービスが該当します)、APM 集計を使って、すべての一意な ID を1回の呼び出しで取得します。pup モードでは aggregate_spans を利用できないため、オプション A をスキップして、オプション B に進んでください。

aggregate_spans(
  query    = "service:<service_name> resource_name:<root_span_name>",
  computes = [{ field: "*", aggregation: COUNT, output: "count" }],
  group_by = { fields: ["@session_id"], limit: 1000 },   # or @investigation.id, @chatid, etc.
  from     = "<timeframe>",
  to       = "now"
)

まず @session_id を試します。バケットが空の場合は、Agent Observability スパンで確認したビジネス ID タグ(@investigation.id、@chatid、@correlation_id など)を試してください。これにより、ID ごとの件数とともに、最大 1000 個の一意な ID を1回の呼び出しで取得できます。Agent Observability の検索結果をページネーションするより、はるかに効率的です。

オプション B — search_llmobs_spans によるフォールバック: アプリに APM のインストルメンテーションがない場合、または aggregate_spans が何も返さない場合は、Agent Observability のルートスパンをページネーションし、クライアント側でビジネス ID タグの重複を除外します。

search_llmobs_spans(
  query           = "@ml_app:\"<ml_app>\"",
  root_spans_only = true,
  from            = "<timeframe>",
  to              = "now",
  limit           = 50          # max per page; use cursor to paginate
)

session_id、investigation.id、chatid、またはスパン間で一貫しているタグの一意な値を収集します。

粒度の決定(ID の収集後):

  1. session_id(UUID 形式)がある → セッション単位で、session_id によってグループ化します。
  2. 一貫したビジネス ID(investigation.id、chatid、correlation_id など)がある → セッションと同等の ID として使用します。
  3. どちらもない → トレース単位です。ルートスパン1つを分類単位とします。

収集した ID の中から、最大 sample_limit 個の一意な ID をサンプリングします。フォールバックのカスケードは自動的に行われ、ユーザー入力は不要です。

ステップ M2 — 各ユニットを分類する(並列)

コンテンツ取得の呼び出しは、すべて1つのメッセージで発行します。サンプリングした各ユニットについて、次の手順を実行します。

セッション粒度: 各 session_id について、セッションモードの手順1~5を実行します。user_handle が利用可能な場合のみ RUM を含め、利用できない場合は RUM を問題なくスキップします。

トレース粒度: 各トレースについて、コンテンツ取得カスケードを含むトレースモードの手順 T1~T4 を実行します。

どちらの場合も、コンテンツ取得カスケード(C1→C4)が適用されます。各ユニットの結果は完了次第出力し、すべてのユニットが完了するまで待たないでください。

ユニットごとのコンパクトなブロック:

[session|trace]: <id_short>  verdict: yes | partial | no  reason: <one sentence>  content: <agent_loop|messages|io|structural>
ステップ M3 — サマリーを出力する

すべてのユニットの分類が完了したら、サマリーを出力します。# Session Classification Summary ヘッダーは後続のスキル(eval-trace-rca、eval-pipeline)が検出に使用する検出センチネルです。次のとおり正確に出力してください。

# Session Classification Summary

**App:** `<ml_app>`  |  **Timeframe:** <from> → now  |  **Granularity:** session | trace  |  **Sampled:** <N>
**Root span kind:** <kind>  |  **Content source:** <dominant source across units>

## Verdict Distribution

| Verdict | Count | % |
|---------|------:|:-:|
| yes     | N     | % |
| partial | N     | % |
| no      | N     | % |

## Failure Mode Frequency

| Failure Mode | Count | % of failures |
|-------------|------:|:-------------:|
| <mode>      | N     | %             |

## Per-Unit Details

| Trace/Session ID | Type | Verdict | Failure Mode | Reason | Content source |
|-----------------|------|---------|-------------|--------|----------------|
| [<first8>...<last8>](<url>) | session\|trace | yes\|partial\|no | none\|<mode> | <one sentence> | agent_loop\|messages\|io\|structural |
  • ID 列:<first8>...<last8>(表示文字数16文字)の形式で表示し、完全なトレース/セッション URL へのリンクを設定します。
    • セッションリンク:https://app.datadoghq.com/llm/traces?query=session_id:<full_id>
    • トレースリンク:https://app.datadoghq.com/llm/traces?query=trace_id:<full_id>
  • Reason 列:コンパクトなブロックに記載した、分類理由の1文を使用します。判定が yes の場合は none を使用します。

エラー出力スキーマ

{
  "mode": "session_id | trace_id | ml_app",
  "id": "<the input id or app name>",
  "error": "<error_code>",
  "detail": "<human-readable explanation>"
}

エラーコード:

CodeTrigger
missing_inputsession_id、trace_id、ml_app のいずれも指定されていない
llmobs_not_foundsearch_llmobs_spans がスパンを返さなかった
llmobs_content_expiredget_llmobs_agent_loop が404を返し、他のすべてのコンテンツソースも失敗した
llmobs_content_maskedすべてのコンテンツフィールドが <REDACTED_INPUT>/<MASKED_STREAMING_RESPONSE> を返し、構造的なシグナルも利用できない
rum_unavailable予約済み — プロトコル上 RUM が必須の場合(例:トレースが完全にマスクされ、RUM が唯一残されたシグナルの場合)に限り出力し、かつ RUM が存在しない場合に使用します。一般的な0行または認証エラーの場合は、このエラーを出力しないでください。代わりに縮退モードでトレース+監査を続行し、出力で rum_coverage: partial | unavailable を設定します(ステップ4を参照)。
audit_unavailable外部顧客の組織でステップ4bが0行を返した。致命的なエラーではありません — 詳細出力にフラグを設定し、トレース+RUMに基づく分類を続行します。
no_traces_foundml_app モード:指定されたアプリと期間でスパンが見つからなかった
unclassifiable_app_typeアプリに埋め込みスパンしかない、または会話コンテンツのない自動パイプラインである

ツールリファレンス

この付録はpup モードでのみ適用されます。MCP モードでは、ワークフローの各セクションに記載されたツール名を直接使用してください。

スパンとトレース
MCP Toolpup Command
search_llmobs_spans(query, ml_app, from, to, limit, cursor, root_spans_only, span_kind, summary)pup llm-obs spans search --query "@ml_app:A [other_filters]" [--from F] [--to T] [--limit N] [--cursor C] [--root-spans-only] [--span-kind K] [--summary] — ml_app で絞り込むには、必ず --query "@ml_app:A" を使用してください。--ml-app A フラグは信頼できず、他のアプリのスパンを黙って返すことがあります。
get_llmobs_span_details(trace_id, span_ids, from, to)pup llm-obs spans get-details --trace-id T --span-ids S1,S2,...
get_llmobs_span_content(trace_id, span_id, field, path)pup llm-obs spans get-content --trace-id T --span-id S --field F [--path P]
get_llmobs_trace(trace_id, include_tree)pup llm-obs spans get-trace --trace-id T [--include-tree]
get_llmobs_agent_loop(trace_id, span_id)pup llm-obs spans get-agent-loop --trace-id T [--span-id S]
find_llmobs_error_spans(trace_id)pup llm-obs spans find-errors --trace-id T
expand_llmobs_spans(trace_id, span_ids, max_depth, filter_kind)pup llm-obs spans expand --trace-id T --span-ids S1,S2,... [--max-depth N] [--filter-kind K]
RUM
MCP ツールpup コマンド
analyze_rum_events(event_type="view", filter="@usr.email:EMAIL", ...)pup rum aggregate --user-email EMAIL --from F --to T --compute count --group-by @session.id
analyze_rum_events(event_type="action", filter="@action.type:custom @usr.email:EMAIL", ...)pup rum aggregate --user-email EMAIL --query "@action.type:custom" --from F --to T --compute count --group-by @evt.name

--user-email EMAIL はクエリに @usr.email:EMAIL を先頭に追加し、すでに設定されている --query と適切に組み合わせます。必要に応じて、イベントタイプのフィルターフラグを pup rum aggregate --help で確認してください。

監査証跡(ステップ 4b)

どちらのバックエンドでも監査証跡は pup ネイティブです。MCP サーバーには analyze_audit_events に相当する機能はありません。MCP が利用できるかどうかにかかわらず、常に Bash 経由で pup audit-logs search を実行してください。

用途pup コマンド
@asset.id の変更履歴pup audit-logs search --query '@asset.id:<id>' --from <iso|duration> --to <iso|now> --limit 50
Q3 対象 HTTP パスpup audit-logs search --query '@usr.email:<u> @http.url_details.path:<path>' --from F --to T --limit 200
Q1 広範なシグナルpup audit-logs search --query '@usr.email:<u>' --from F --to T --limit 200
Q2 セッション後のノイズを除去した結果pup audit-logs search --query '@usr.email:<u> [email protected]_details.path:/api/ui/query/scalar [email protected]_details.path:/api/ui/query/timeseries' --from F --to T --limit 200

レスポンスの形式:data[].attributes.attributes.{action,asset,http,usr,timestamp,...} — attributes の下に二重にネストされています。timestamp は ISO8601 形式です。asset.new_value/asset.prev_value はネストされた辞書です。Pup はデフォルトで降順に返すため、コード内で sorted() を使って昇順にしてください。ページネーションカーソルはありません。上限は --limit です(デフォルトは 100。広範なクエリでは 200 を指定してください)。

pup に相当する機能がないツール
MCP ツールpup モードでの動作
get_llmobs_bits_sessionスキップしてください。bits 以外のセッションでは不要です。
aggregate_spans(ステップ M1 オプション A)利用できません。オプション A をスキップして、オプション B に直接進んでください。

運用ルール

  • MCP 結果の解析に関する安全性:MCP ツールの結果のフィールドを反復処理またはアクセスするスクリプト(Python、jq など)を書く前に、生の構造を確認してください。type(result)、トップレベルのキー、ペイロードがコンテンツブロック(例:[{'type': 'text', 'text': '<json>'}])内にネストされているかを調べます。必要に応じて、解析前に内側のペイロードを抽出し、json.loads() で読み込んでください。MCP の結果がそのままの辞書やリストであると決めつけないでください。

コメント