К основному содержанию
скилл CX Codex

agent-observability-session-classify — скилл для Claude Code

Оценивает удовлетворённость пользователя в трейсах и сессиях Datadog Agent Observability. Возвращает вердикт yes, partial или no с коротким пояснением.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add datadog-labs/agent-skills --skill agent-observability-session-classify -a claude-code
Открыть на GitHub

Что делает agent-observability-session-classify

Скилл проверяет, решил ли ИИ-ассистент задачу пользователя в конкретной сессии Datadog Agent Observability. Работает в трёх режимах: по session_id для разбора CMD+I сессий вместе с RUM, по trace_id без RUM либо по ml_app для пакетной выборки диалогов приложения. По умолчанию формирует краткий результат: статус yes, partial или no и одну строку с причиной.

Перед запуском инструмент проверяет бэкенд. Если передан флаг --backend pup, скилл выполняет команды через CLI pup. В остальных случаях он ищет MCP-инструмент search_llmobs_spans или проверяет наличие pup через терминал. Чтобы не получить поверхностный вердикт по обрезанным превью, скилл выгружает полный контекст шагов через каскад запросов.

Без настроенного MCP-сервера Datadog (datadog-llmo-mcp) или установленной утилиты pup скилл прервёт работу с ошибкой.

Какие задачи решает

  • проверить, закрыл ли агент запрос пользователя в конкретном трейсе
  • разметить выборку сессий из LLM-приложения по флагу ml_app
  • собрать данные об удовлетворённости для пайплайнов оценки качества
  • сопоставить события RUM и шаги ассистента в сессиях CMD+I

Примеры запросов агенту

  • Классифицируй сессию session_id 3a9f1c2b-0000-0000-0000-000000000000
  • Проверь удовлетворённость пользователя в трейсе trace_id 778899112233
  • Собери выборку из 10 сессий для ml_app support-copilot за последние 24h

Как установить agent-observability-session-classify

1Через Skills CLI (Vercel)

npx skills add datadog-labs/agent-skills --skill agent-observability-session-classify -a claude-code

2Вручную: положить файлы

~/.claude/skills/agent-observability-session-classify/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/agent-observability-session-classify/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Из того же набора20

dd-browser-sdk Помогает настроить Datadog Browser SDK, настроить выгрузку sourcemaps и обновить клиентский код при переходе между версиями v4, v5, v6 и v7. CX Codex +1 577установок agent-observability-trace-rca Ищет причину сбоя LLM-приложения в продакшн-трейсах: идёт по дереву спанов от симптома к корню проблемы. Пригодится, когда падают оценки или сыпятся ошибки. CX Codex +1 552установок agent-observability-eval-bootstrap Генерирует эвалюаторы качества LLM по продакшен-трейсам Datadog. Создаёт черновики онлайн-судей, код на Python для тестов или датасеты. CX Codex +1 547установок agent-observability-eval-pipeline Ведёт инструментированное ml_app по шести фазам: классификация трейсов, разбор сбоев, бутстрап эвалуаторов, датасет, эксперимент и анализ. CX Codex +1 540установок service-remapping Скилл для работы с правилами переименования сервисов Datadog APM: схлопывает шумные inferred-сущности, чистит автоимена, помогает после переименования сервиса. CX Codex +1 478установок dd-audit-compliance-report Собирает доказательства из Datadog Audit Trail под требования SOC 2 и PCI DSS и выдаёт готовый к проверке отчёт. CX Codex +1 477установок dd-audit-cost-spike-investigation Скилл связывает скачок потребления в Datadog с изменениями конфигурации из Audit Trail и показывает, кто и что поменял перед всплеском. CX Codex +1 473установок upgrade-browser-sdk-v7 Ведёт проект с Datadog Browser SDK с версии 6 на 7: находит устаревшие опции, меняет CDN-ссылки и API. Пригодится, когда после обновления сыпятся ошибки конфигурации. CX Codex +1 459установок

Похожие

setup-matt-pocock-skills Matt Pocock Скилл настраивает репозиторий для инженерных скиллов Matt Pocock: задаёт трекер задач, метки триажа и расположение доменной документации. Запустите его перед первым использованием остальных скиллов. 1 052 851установок ask-matt Matt Pocock Помогает выбрать подходящий скилл или рабочий поток из набора Matt Pocock. Пригодится, когда непонятно, с чего начать задачу: от идеи до разбора бага. 766 587установок to-tickets Matt Pocock Разбивает план, спецификацию или переписку на небольшие сквозные задачи и отмечает, какие из них блокируют остальные. Пригодится, когда нужно согласовать порядок работы до публикации задач в трекере. 711 230установок writing-plans Jesse Vincent Скилл превращает требования к многошаговой задаче в подробный план реализации до начала работы с кодом. Пригодится, когда исполнителю нужно зафиксировать файлы, интерфейсы и проверки. CX Codex 282 859установок caveman-review JuliusBrussee Пишет замечания к коду одной строкой: место, проблема, исправление. Пригодится, когда ревью диффа или PR надо пройти быстро и без воды. CX Codex +1 411 800установок caveman-compress JuliusBrussee Сжимает CLAUDE.md, списки задач и другие текстовые файлы в краткий формат, сохраняя читаемую резервную копию. CX Codex +1 411 500установок

Рядом в теме «Настройка агента: память, контекст, субагенты»

Все скиллы по теме «Настройка агента: память, контекст, субагенты» →

Вопросы о agent-observability-session-classify

Как установить agent-observability-session-classify в Claude Code?

Выполните команду npx skills add datadog-labs/agent-skills --skill agent-observability-session-classify -a claude-code. Скилл запишется в ~/.claude/skills/agent-observability-session-classify/SKILL.md.

Что делает скилл agent-observability-session-classify?

Он анализирует шаги диалога агента в Datadog Agent Observability и выносит вердикт yes, partial или no. Ответ содержит статус решения задачи и одно предложение с объяснением.

Что нужно для работы скилла?

Требуется доступ к MCP-серверу Datadog (datadog-llmo-mcp) либо утилита pup с пройденной авторизацией через pup auth login. Если ни один бэкенд не найден, скилл вернёт ошибку.

Исходный текст

SKILL.md · MIT
---
name: agent-observability-session-classify
description: >
  Classify whether user intent was satisfied in a Datadog Agent Observability trace or session.
  Three modes: (1) session_id — classify a single CMD+I assistant session with RUM;
  (2) trace_id — classify a single Agent Observability trace without RUM; (3) ml_app — sample
  and classify multiple sessions or traces from a given LLM app. Output is compact
  by default (verdict + one-sentence reason). Use when evaluating satisfaction,
  classifying sessions/traces, labeling data, or generating signal for
  agent-observability-eval-pipeline or agent-observability-trace-rca.
---

## Backend

**Detection** — At the start of every invocation, before taking any action, determine which backend to use:

1. If the user passed `--backend pup` anywhere in their invocation → use **pup mode** immediately, regardless of whether MCP tools are present. Skip steps 2–4.
2. Check whether MCP tools are present in your active tool list. The canonical signal is whether `mcp__datadog-llmo-mcp__search_llmobs_spans` appears in your available tools.
3. If MCP tools are present → use **MCP mode** throughout. Call MCP tools exactly as named in this skill's workflow sections.
4. If MCP tools are absent → check whether `pup` is executable: run `pup --version` via Bash. A JSON response containing `"version"` confirms pup is available.
5. If pup responds → use **pup mode** throughout. Translate every MCP tool call to its pup equivalent using the Tool Reference appendix at the bottom of this file.
6. If neither is available → stop and tell the user:
   > "Neither the Datadog MCP server nor the pup CLI is available. Connect the MCP server (`claude mcp add --scope user --transport http datadog-llmo-mcp 'https://mcp.datadoghq.com/v1/mcp?toolsets=llmobs,rum'`) or install pup."

`--backend pup` is accepted anywhere in the invocation arguments and is stripped before passing remaining args to the skill logic.

**pup invocation rules:**
- Invoke via Bash: `pup llm-obs <subcommand> [flags]`
- pup always outputs JSON. Parse directly — no content-block unwrapping (unlike MCP results, which may wrap JSON in `[{"type": "text", "text": "<json>"}]`).
- If pup returns an auth error, tell the user to run `pup auth login` and stop.
- Parallelization: issue multiple Bash tool calls in a single message (one pup command per call).
- Time flags: pup accepts bare duration strings (`1h`, `7d`, `30m`) and RFC3339 timestamps. Do **not** use `now-`-prefixed strings — strip the prefix when converting from a skill `--timeframe` argument: `now-7d` → `7d`, `now-24h` → `24h`, `now-30d` → `30d`.
- `--summary` on `pup llm-obs spans search` strips payload fields to essential metadata only. Use it in bulk/search phases where content is not needed.

**pup mode notes by entry mode:**
- `session_id` mode: Steps 1–3 and Step 5 work fully. Step 4 (RUM) uses `pup rum aggregate --user-email EMAIL` instead of `analyze_rum_events` — see Tool Reference. Step 4b (audit trail) is pup-native and queries the active user's own org via OAuth.
- `trace_id` mode: Full parity with MCP mode.
- `ml_app` mode: Option A (`aggregate_spans`) is unavailable in pup — skip it and proceed directly to Option B.

**Invocation ID:** At the very start of each invocation, before any MCP tool call, generate an 8-character hex invocation ID (e.g., `3a9f1c2b`). Keep it constant for the entire invocation.

**Intent tagging:** On every MCP tool call, prefix `telemetry.intent` with `skill:agent-observability-session-classify[<inv_id>] — ` followed by a description of why the tool is being called. On the **first MCP tool call only**, use `skill:agent-observability-session-classify:start[<inv_id>] — ` instead (note the `:start` suffix). Example first call: `skill:agent-observability-session-classify:start[3a9f1c2b] — Step 1: enumerate turn root spans for session abc-123`

# Skill: eval-session-classify

Classification skill for Datadog Agent Observability sessions and traces. Produces a satisfaction verdict (`yes` / `partial` / `no`) with a brief reasoning string. Designed to be called standalone or by `eval-pipeline`.

---

## Inputs

| Input | Mode | Required | Description |
|-------|------|----------|-------------|
| `session_id` | session_id mode | Yes | UUID of a Datadog CMD+I assistant session |
| `trace_id` | trace_id mode | Yes | Trace ID from Agent Observability |
| `ml_app` | ml_app mode | Yes | LLM app name to sample from |
| `timeframe` | ml_app mode | No (default: `now-7d`) | How far back to sample |
| `sample_limit` | ml_app mode | No (default: `20`, cap: `50`) | Number of sessions or traces to classify |

**If none of `session_id`, `trace_id`, or `ml_app` is provided → stop immediately and return:**

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

---

## Phase 0 — Mode Detection

- `session_id` provided → **session_id mode** → go to [Session Mode](#session-mode)
- `trace_id` provided (no `session_id`) → **trace_id mode** → go to [Trace Mode](#trace-mode)
- `ml_app` provided (no `session_id`, no `trace_id`) → **ml_app mode** → go to [ML App Mode](#ml-app-mode)
- Nothing provided → emit error (see above)

---

## Output Format

**Default (compact) — used in all modes unless the caller requests verbose:**

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

**Verbose** — full markdown report (see end of each mode section). Request verbose by including `verbose: true` in input or asking for a detailed report.

In ml_app mode, a summary table is always appended after the per-trace compact blocks.

---

## Content Retrieval Cascade

Reading conversation content follows this cascade across all modes. Run the cascade for every turn root span identified in Step 1 (session mode) or Step T1 (trace mode) before forming a verdict.

> **Completeness gate**: do not proceed to RUM (session mode) or classification (any mode) until the cascade has run for every turn. Previews from `search_llmobs_spans` (`input.preview` / `output.preview`, truncated to ~200 chars) do NOT satisfy this gate — they identify turn boundaries, nothing more. Only actual content from `get_llmobs_agent_loop`, `expand_llmobs_spans` + `get_llmobs_span_content`, or `get_llmobs_span_content(field="input"/"output")` counts.
>
> **Short sessions** (≤ 20 turns): run C1/C2 for every turn — no selection, no "key turns" heuristic.
>
> **Long sessions** (> 20 turns): run C1/C2 for the **first 5** and **last 5** turns, plus any turn whose `input.preview` or `output.preview` flags an anomaly (error, correction loop, repeated intent, unexpected tool). Scan all remaining turns' previews for anomalies before skipping them.

**Parallelism**: issue all N `get_llmobs_agent_loop` calls in a single message — one call per turn, all in the same batch. For an N-turn session this means N simultaneous C1 calls. Do not process turns sequentially and do not form partial verdicts before all results are in hand. Move to C2 for a turn only when C1 returns `iterations: []` for that turn.

### C1 — `get_llmobs_agent_loop(trace_id, agent_span_id)`

The richest source: full system prompt, user message, tool call arguments + results, assistant response, and token economics per iteration. Attempt this first for every agent span.

```
get_llmobs_agent_loop(
  trace_id           = "<TRACE_ID>",
  span_id            = "<AGENT_SPAN_ID>",
  from               = "now-90d",
  to                 = "now",
  max_content_length = 2000
)
```

- **`iterations: []` and `timeline: null`** → the app's LLM spans go through an intermediate workflow layer (e.g. `get_answer_from_model_step`) rather than as direct children of the agent span. Fall through to C2.
- **404** → span ID does not resolve in the trace store. Fall through to C2.
- **Content fields contain `<REDACTED_INPUT>` or `<MASKED_STREAMING_RESPONSE>`** → IO tracing is disabled by a feature flag. Structure (iteration count, tool names, token usage) is still useful — note it. Fall through to C2 for actual content.

**When a C1 result is too large for context and gets persisted to disk** (common on long sessions): do NOT try to Read the full file — it will exceed the token limit. Run a targeted Python extraction to build a structured per-turn summary:

```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]}")
```

A large-but-parseable C1 result is real content; only fall through to C2 if C1 returned `iterations: []`.

### C2 — `get_llmobs_span_content(field="messages")` on LLM child spans

When C1 returns `iterations: []`, the LLM spans typically sit 2 levels below the root agent span — under a workflow wrapper (e.g. `get_answer_from_model_step`) that the agent-loop API does not descend into. The concrete tree per turn:

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

Use `expand_llmobs_spans` to navigate to the LLM span IDs — **not** `get_llmobs_trace`, which only returns depth-1 children and cannot reach LLM spans nested under a workflow:

```python
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"
)
```

From the returned tree, collect all nodes with `span_kind=llm` and `has_input=true` (commonly named `anthropic.request`, `openai.request`, `chat_completion-call`, `messages-call`). Call `get_llmobs_span_content(field="messages")` on each.

**JSONPath + truncation trap**: `path` is applied *after* `max_tokens` truncation. Each LLM-call span typically starts with a multi-KB system prompt, so a low `max_tokens` means the JSONPath filter operates on system-prompt-only content and silently returns the wrong messages.

To extract the meaningful tail (user query, tool calls, final answer):
1. Call once without `path` to read `total_tokens_approx` from the response.
2. Re-call with `path = "$.[-5:]"` and `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>
)
```

This returns the last 5 messages: typically [user context+query, reasoning, assistant text, assistant tool_call, tool result] — enough to understand what the turn did.

- **Messages are `"REDACTED"`** → IO tracing disabled. Fall through to C3.
- **`content_info` map does not include `messages`** → not a chat span. Fall through to C3.

### C3 — `get_llmobs_span_content(field="input")` and `field="output"` on the root span

The root span often carries synthetic summaries written by the app (e.g. the raw user query as `input`, the final response as `output`, or `"Investigate error for issue: <id>"` / `"Investigation completed with status: completed"`). Minimal signal, but enough to confirm what the task was and whether it completed.

### C4 — Structural signals only

When all content is inaccessible, classify from span metadata alone:
- `status` (ok / error), `stop_reason`, `response_truncated`
- Child span names (tool names, workflow step names like `classify`, `generate-summary`, `suggest-action`)
- `iteration` tag count (total LLM rounds)
- Duration and token counts if available

### Drill-down — targeted extraction for suspicious tool results

After the summary pass, drill into any turn whose tool list includes a client-rendered tool, a write operation, or an iteration flagged with `error_message`. These checks surface silent failures the summary loop cannot detect.

**Triggers:**

- **Client-rendered tools** (browser-handled, e.g. `show_content`, `create_datadog_notebook`, `edit_datadog_notebook`) — **always mandatory** when present. The server fires an event; the client handles display. The result tells you whether rendering was delegated or confirmed.
  - `"This tool is handled by the client application..."` → delegated to browser, **no server-side confirmation the user saw anything**. Check for a `client_tool_response` in the next turn's input. If absent, or if the next turn shows user confusion, treat as invisible output — strong negative signal.
  - Any other result → executed server-side; output was in the response stream.

- **Write operations** (e.g. `call_datadog_api`, `upsert_datadog_dashboard`, `upsert_widget`, monitor/notebook create-or-update) — the assistant often narrates "I updated X" regardless of the actual HTTP outcome. Check the result for:
  - `403` / `"not allowlisted"` / `"PUT not allowlisted"` → write blocked; the change was **never applied**.
  - `404` → asset not found; editing something that doesn't exist.
  - `500` / `"internal server error"` → write uncertain, may not have landed.
  - `"Session not found"` → resource expired before the tool ran.
  - `200` / `201` with payload → confirmed success.

- **Data queries returning empty** (e.g. `search_datadog_metrics`, `get_datadog_metric`, `search_datadog_logs`, `search_datadog_spans`) — when the summary shows many iterations (> 5) of the same query tool, check whether results were empty. An assistant reasoning from `NO_DATA` / empty arrays may produce plausible-sounding but fabricated analysis (`hallucination` failure mode).

- **Error iterations** — any iteration where `error_message` is set: confirm the exact error, which tool triggered it, and whether the assistant retried successfully or gave up. A transient retry is neutral; a permanent API restriction or repeated identical failure is negative.

```python
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]}")
```

**Interpretation cheat sheet:**

| Result pattern | Verdict signal |
|---|---|
| `"This tool is handled by the client application"` | Unconfirmed rendering → check next-turn `client_tool_response` |
| `403` / `"not allowlisted"` | Write blocked — core intent may be unachievable via assistant |
| `"Session not found"` | Resource expired — tool call had no effect |
| `NO_DATA` / empty list on data query | No real signal — check for hallucination in assistant's answer |
| `500` on write op | Uncertain — may or may not have landed |
| `200` / `201` with payload | Confirmed success |

The tool-name lists above are examples calibrated for Datadog assistant apps. For other apps, look at the tool names that appear in C1's `tool_calls` and reason about their semantics (client rendering vs server side-effect vs read-only query).

---

## Session Mode

Classifies a single Datadog CMD+I assistant session. Uses RUM behavioral signals to confirm the trace-based verdict.

### Step 1 — Enumerate turn root spans

```
search_llmobs_spans(
  tags            = {"session_id": "<SESSION_ID>"},
  root_spans_only = True,
  from            = "now-90d",
  to              = "now",
  limit           = 500
)
```

> **pup mode**: `pup llm-obs spans search --query "@session.id:<SESSION_ID>" --root-spans-only --limit 500 --from 90d`

Use `tags={...}` (not the `query` string form). The free-form `query="session_id:<id>"` triggers full-text behavior that returns the entire trace bundle for every turn (5× the spans, structural filters silently ignored) and exhausts the row budget on long sessions. `tags={"session_id": "<id>"}` hits the indexed tag filter and respects the other parameters.

`root_spans_only=True` returns one span per user turn — typically the root agent span — rather than every workflow/LLM/tool descendant. Paginate via `next_cursor` if the session has more than 500 turns.

For each returned root span, record:
- `trace_id`, `span_id`
- `start_ms`, `duration_ms`
- Tags: `user_handle`, `user_id`, `org_id`, `product_area`, `message_id`
- `name` and `span_kind` of the root (usually `agent` / `assistant`, but app-dependent)
- `input.preview`, `output.preview` (for turn-boundary identification only — see warning below)

Sort the turns ascending by `start_ms`. From the first turn's tags, capture session-level identity: `user_handle`, `user_id`, `org_id`, `product_area`, session `start_ms`.

> **Preview-truncation warning**: `input.preview` and `output.preview` are truncated to ~200 characters. They identify turn boundaries and surface obvious anomalies — they are NOT sufficient for verdict formation. Never skip the [Content Retrieval Cascade](#content-retrieval-cascade) because the previews already "look conclusive."

**If `search_llmobs_spans` returns no results** → stop, return error `llmobs_not_found`.

Per-turn structural details (iteration count, tool names, `stop_reason`, `mcp` flag, `response_truncated`) are recovered later from each turn's `get_llmobs_agent_loop` result in Step 3 (C1).

### Step 2 — Get evaluations and metadata

```
get_llmobs_span_details(
  trace_id  = "<TRACE_ID>",
  span_ids  = ["<AGENT_SPAN_ID>"],
  from/to   = <same window>
)
```

From the `evaluations` map: iterate all keys, note `.value`, `.reasoning`, `.tags` for each judge.
From `content_info.metadata`: `query_string`, `referrer_path`, `referrer_url`, `entities_json`, `user_info_json`.

If `get_llmobs_span_details` fails or returns empty → skip silently, proceed to step 3.

If `content_info` shows `metadata` is present, fetch it via `get_llmobs_span_content(field="metadata")` to get the actual `query_string` and `referrer_path` — these give the user's question and the page they were on without needing the agent loop.

### Step 3 — Read the full conversation

For every turn root span enumerated in Step 1, follow the [Content Retrieval Cascade](#content-retrieval-cascade). Issue all per-turn C1 calls (`get_llmobs_agent_loop`) in a single parallel batch per the cascade's parallelism rule, then fall through to C2 for any turns where C1 returns `iterations: []`.

For apps where LLM spans sit under a workflow wrapper (e.g. `assistant_api`, where `anthropic.request` is a grandchild via `get_answer_from_model_step`), C1 will routinely return empty iterations — this is expected and the cascade handles it by routing those turns through C2's `expand_llmobs_spans` navigation. C3 (metadata + output on the root span) is the last resort when no LLM-call content is available.

### Step 4a — Resolve RUM session ID(s)

Before the main RUM queries, run a narrow ±2min lookup around the first turn's `start_ms` to find the exact browser session(s) the user was on. This isolates the RUM `session_id`(s) and avoids cross-session bleed that occurs when filtering only by `@usr.email` over a wide time window (a user with multiple tabs across the day will return events from all of them).

```
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'
)
```

Extract all distinct `session_id` values → store as `rum_session_ids`. The RUM filter for all subsequent queries (`<rum_filter>` below) is determined by the result:

- **0 results** → `<rum_filter>` = `@usr.email:<user_handle>` (fallback to email + time window).
- **1 result** → `<rum_filter>` = `@session.id:<rum_session_id>`.
- **Multiple results** → `<rum_filter>` = `@session.id:(<id1> OR <id2>)`.

> **pup mode**: `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`. Confirm the `--group-by` flag name with `pup rum aggregate --help`.

### Step 4 — Get RUM behavioral signals

> **pup mode**: Replace `analyze_rum_events` calls with `pup rum aggregate` calls (see Tool Reference). Page views: `pup rum aggregate --user-email USER_HANDLE --from START_MS --to END_MS --compute count --group-by @session.id`. Custom actions: `pup rum aggregate --user-email USER_HANDLE --query "@action.type:custom" --from START_MS --to END_MS --compute count --group-by @evt.name`. Confirm event-type filtering flag with `pup rum aggregate --help` if the API rejects the query.

With `start_ms` and `<rum_filter>` (from Step 4a), define the window:
- **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]`

Run in parallel:

**RUM Query A — Page view timeline:**
```
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` is in nanoseconds.

**RUM Query B — Custom actions (product-area narrowed):**
```
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"}]
)
```

The `LIKE` filters above are a Datadog assistant example — narrow to whatever action-name prefix is meaningful for the app under classification, or drop the filter to read all custom actions on the session.

**If either query returns 0 rows or hits a permission/auth error**, do NOT abort the classification. Missing or partial RUM is a coverage gap, not behavioral evidence — treating it as negative is a documented failure mode.

| Condition | Action |
|---|---|
| 0 RUM rows on the session AND user has SOME RUM data in a wider 30-day window (web RUM gap on the session date) | Degraded. Proceed without RUM; rely on trace evidence (Step 3) and audit signals (Step 4b, if applicable). Set `rum_coverage: partial — web RUM gap on session date` in the output. |
| `analyze_rum_events` / `pup rum` returns permission or auth errors on one or more queries (common for external orgs, SOC2-isolated tenants) | Degraded. Use whichever RUM queries returned rows; fall back to trace + audit for the missing pieces. Set `rum_coverage: partial — auth error on <query>` in the output. |
| 0 RUM rows on the session AND no RUM data anywhere for the user in 30 days | Degraded. Proceed with trace + audit. Set `rum_coverage: unavailable — no RUM data for user` in the output. Do NOT emit `rum_unavailable` as a terminal error. |

**Hard rule:** never use the absence of RUM as evidence for a negative verdict. If trace and audit signals point to satisfaction, the absence of RUM does not override them; if trace signals are ambiguous, default to the trace-only verdict rather than penalizing for missing behavioral data.

The `rum_unavailable` error code is reserved for the unrecoverable case where RUM was *required* by the classification protocol (e.g., the trace is so content-redacted that RUM is the only remaining signal) AND no RUM exists. Otherwise, proceed in degraded mode.

#### Signal interpretation framework

RUM actions are not equal evidence. Stratify them into three tiers and let the tier control how much weight each carries in Step 5. The categories below are **agent-agnostic** — map your specific app's RUM action names onto them. Treat unrecognized actions as Ambiguous until you have evidence for how they correlate with satisfaction.

**Tier-A — sufficient on its own (when present, can flip or confirm the trace verdict)**

| Abstract category | Direction | Example action-name patterns |
|---|---|---|
| Stream-abort fired before the response completed | negative | `*.chat-cancel`, `*.stream-abort`, `*.cancel-generation` |
| Panel/window closed mid-stream with no reopen within 1s | negative | `*.panel.close` while the agent's LLM span is still active |
| Navigation to a URL containing an asset ID the agent explicitly named in its response | positive | View on the exact resource path the agent referenced |
| Artifact action within 60s of session end on the agent's output | positive | Export, save, modify, share targeting the produced asset |
| New session within 15 min with the same intent (retry per Step 1b classifier) | negative | Detected by neighbor-session analysis |
| Wholesale rejection of the agent's plan (`reject_all` or equivalent) | negative | Bulk-reject affordance, "stop" action mid-stream |

**Tier-B — corroborating, never decisive on its own**

| Abstract category | Direction |
|---|---|
| Short dwell on the agent's view (< 30s total) | negative |
| Post-session navigation to external help (docs, support, community) | negative |
| Repeated panel/view opens before any response | negative |
| Long dwell time (> 60s) on the agent's view | positive |
| Click on a markdown link the agent rendered | positive |
| Post-session navigation to a topic-adjacent resource (not exact-asset match) | positive |

**Ambiguous — DO NOT use as sole evidence**

| Abstract category | Why ambiguous | Rule |
|---|---|---|
| Reveal/expand interactions on intermediate state (tool-call panels, reasoning blocks) | Fires in both engaged-reading and pre-thumbs-down-scrutiny patterns; no principled rule discriminates | Pair with a Tier-A or matching Tier-B signal of the same direction |
| Single approval of a tool call (`accept` on one of N proposals) | The expected UX step — users click accept even on mediocre responses | Treat as WEAK positive; never primary evidence for `yes` |
| Single rejection of a tool call (`reject` on one of N proposals) | Could be "user redirected to a better path", not "user dissatisfied" | Count as negative ONLY if user also stopped sending messages OR all subsequent calls were rejected |
| Panel/view close without mid-stream context | "Session ended naturally" indistinguishable from "abandoned" | Need stream-active timing — see Tier-A |

**For your specific agent:** before classifying, build a mapping from your app's RUM action names onto these categories. A reference mapping for the Datadog CMD+I assistant is in `@rum-actions-bits-assistant.md` — use it as a template for what to look up in your own app's RUM event taxonomy. Unrecognized action names default to Ambiguous; promote to Tier-B or Tier-A only after you have evidence (from labeled sessions or product knowledge) for how the action correlates with satisfaction.

For interpreting Datadog assistant action names specifically, consult @rum-actions-bits-assistant.md.

### Step 4b — Get audit-trail signals (when an asset was created or edited)

The audit trail surfaces server-confirmed effects of the assistant's actions (writes that landed, modifications that stuck, deletions, follow-up edits) and post-session user behavior on the same asset. For sessions where the assistant created or edited a Datadog asset (dashboard, notebook, monitor, SLO, etc.), this is the most authoritative behavioral signal — more direct than RUM clicks.

Primary tool: `pup audit-logs search`, which queries the active user's own org via OAuth (`pup auth login`).

**When to run this step:**
- The trace's tool calls include any write op (`upsert_*`, `call_datadog_api` with `PUT`/`POST`, monitor/notebook create-or-update) → run it.
- The session has a known asset ID (from a tool-call argument, the `dashboard_id` tag, the session's `referrer_path`, or the user query) → run it.
- The trace is pure read-only (no writes, no asset ID) → skip; nothing for audit to add over Step 4.

**Query priority** (most signal first, least noise last):

1. **`@asset.id`** — modification/deletion history with full diff. Most authoritative. Required when an asset ID is known.
2. **Q3 (targeted HTTP path)** — write count and timing for the same asset. Required when an asset ID is known.
3. **Q1 / Q2 (broad-signal HTTP audit)** — fallback only. Noisy on active users.

#### `@asset.id` — modification history

```bash
pup audit-logs search \
  --query '@asset.id:<asset_id>' \
  --from "<session_start - 30d, ISO>" --to now --limit 50
```

Each row's body lives at `row["attributes"]["attributes"]`. Fields:
- `action` — `created` / `modified` / `deleted` / `accessed`
- `usr.email`
- `asset.type` (`dashboard`, `notebook`, `monitor`, …), `asset.id`, `asset.name`
- `asset.new_value` / `asset.prev_value` — full before/after state as **nested dicts** (use directly; no JSON parsing needed)
-...

© datadog-labs · MIT · datadog-labs/agent-skills

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой