> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lasscyber.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Interpreting results

> Field-by-field walkthrough of the response from POST /api/v1/analyze/.

This page explains what `analyze()` returns. The shape is the same
whether you call the HTTP endpoint, the Python SDK, or the TypeScript
SDK; the SDKs add a small ergonomic layer on top.

## High-level shape

```json theme={null}
{
  "request_id": "5b3f6c7e-7d24-4d40-9b12-3a59c01c6e91",
  "policy_id": "cdc7…",
  "policy_slug": "default-inbound",
  "overall_status": "TERMINATED_EARLY",
  "terminated_early": true,
  "termination_reason": {
    "analyzer": "adversarial_detection_analyzer",
    "rule": "score >= 0.85 AND output_match INJECTION/JAILBREAK"
  },
  "analyzer_results": {
    "adversarial_detection_analyzer": { ... },
    "safety_moderation_analyzer":     { "status": "SKIPPED" },
    "dlp_analyzer":                   { "status": "SKIPPED" },
    "url_analyzer":                   { "status": "SKIPPED" },
    "yara_analyzer":                  { "status": "SKIPPED" }
  },
  "aggregated_metrics": {
    "total_processing_time_ms": 38.4,
    "total_cost_usd": 0.0002
  }
}
```

The exact field names follow the OpenAPI schema in
[`sdk/openapi/openapi.json`](https://github.com/lasscyber/agnes-docs/tree/main/policy-fixtures);
the auto-rendered [API reference](/api-reference/overview) is the
authoritative source.

## Top-level fields

| Field                       | Meaning                                                                                                                  |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `request_id`                | Echo of the `X-Request-ID` response header. Always quote this in support tickets.                                        |
| `policy_id` / `policy_slug` | Which combined policy ran.                                                                                               |
| `overall_status`            | `OK` (every analyzer passed), `TERMINATED_EARLY` (a termination rule fired), or `ERROR` (one or more analyzers errored). |
| `terminated_early`          | Boolean shortcut for `overall_status == "TERMINATED_EARLY"`.                                                             |
| `termination_reason`        | Present when `terminated_early == true`. Names the analyzer and the rule that fired.                                     |
| `analyzer_results`          | Per-analyzer block. Status is `OK`, `TERMINATED_EARLY`, `ERROR`, or `SKIPPED`.                                           |
| `aggregated_metrics`        | Sum of per-analyzer wall-clock and cost. Only present when the policy's `default_telemetry` is `true`.                   |

## SDK ergonomics

The SDKs unpack the body into a `Decision` object:

<CodeGroup>
  ```python Python theme={null}
  decision = agnes.analyze("...", policy="default-inbound")

  decision.allowed         # bool — False if terminated_early else True
  decision.blocked_by      # tuple[str, ...] — canonical names of analyzers that blocked
  decision.reasons         # tuple[str, ...] — human-readable reasons
  decision.request_id      # str
  decision.raw             # dict — the full response body
  ```

  ```typescript TypeScript theme={null}
  const decision = await agnes.analyze("...", { policy: "default-inbound" });

  decision.allowed;        // boolean
  decision.blockedBy;      // string[] — canonical analyzer names
  decision.reasons;        // string[]
  decision.requestId;      // string
  decision.raw;            // unknown — full server response
  ```
</CodeGroup>

`blocked_by` / `blockedBy` is the easiest lever for branching in
application code:

```python theme={null}
if not decision.allowed:
    if "prompt-injection-jailbreak" in decision.blocked_by:
        return safe_refusal()
    if "sensitive-data" in decision.blocked_by:
        return scrub_and_retry(decision.raw)
    return generic_block_message()
```

The SDK translates server keys (`adversarial_detection_analyzer`) to
canonical names (`prompt-injection-jailbreak`) so your code stays
stable even if the server-side key flips later. See
[Versioning](/sdks/versioning).

## Per-analyzer blocks

Every analyzer reports its result in `analyzer_results.<server_key>`.
The shape is consistent across analyzers:

| Field           | Meaning                                                                  |
| --------------- | ------------------------------------------------------------------------ |
| `status`        | `OK`, `TERMINATED_EARLY`, `ERROR`, `SKIPPED`.                            |
| `output`        | Analyzer-specific structured output. See the per-analyzer page.          |
| `metrics`       | Map of metric name → value. The dashboard editor exposes the same names. |
| `terminated_by` | Present when this analyzer is the one that terminated the run.           |
| `error`         | Present when `status == "ERROR"`.                                        |

Skipped analyzers carry only `status: "SKIPPED"` — they were declared
in the policy but never reached because an earlier analyzer
terminated.

### Example: an inbound block by the prompt-injection classifier

```json theme={null}
"analyzer_results": {
  "adversarial_detection_analyzer": {
    "status": "TERMINATED_EARLY",
    "output": {
      "label": "INJECTION/JAILBREAK",
      "score": 0.97
    },
    "metrics": {
      "score": 0.97,
      "inference_time_ms": 38.4
    },
    "terminated_by": {
      "rule": "score >= 0.85 AND output_match INJECTION/JAILBREAK",
      "metric": "score",
      "value": 0.97,
      "operator": ">="
    }
  },
  "safety_moderation_analyzer": { "status": "SKIPPED" },
  "dlp_analyzer":                { "status": "SKIPPED" },
  "url_analyzer":                { "status": "SKIPPED" },
  "yara_analyzer":               { "status": "SKIPPED" }
}
```

The classifier scored `0.97` on `INJECTION/JAILBREAK`, which crossed
both the score threshold (`>= 0.85`) and the output match in
`default-inbound`. The execution engine terminated immediately and
flipped `overall_status` to `TERMINATED_EARLY`. The remaining
analyzers were skipped.

### Example: outbound flag by safety + SDP

```json theme={null}
"analyzer_results": {
  "safety_moderation_analyzer": {
    "status": "TERMINATED_EARLY",
    "output": {
      "is_safe": false,
      "categories": [
        { "name": "Hate Speech", "score": 0.88, "verdict": "violation" }
      ]
    },
    "metrics": {
      "max_violation_score": 0.88,
      "violation_category_count": 1,
      "inference_time_ms": 142.0
    }
  }
}
```

The safety judge fired on hate speech with confidence 0.88. The
combined policy's `is_safe` boolean rule terminated the run.

## Errors at the analyzer level

A single analyzer error does not always mean the whole call errored.
Behavior depends on the step type:

* In a **sequential** step, the first analyzer that errors stops the
  step and flips `overall_status` to `ERROR`.
* In an **asynchronous** step, every analyzer in the group still runs
  to completion. Any error in the group flips `overall_status` to
  `ERROR`, but you'll see the other analyzers' results too.

When `overall_status == "ERROR"` and the cause is upstream
infrastructure (model service unreachable, DLP API down), the API
response is **HTTP 503 with `code: "analyzer_unavailable"`** instead of
a 200 with embedded errors. SDKs retry these automatically. See
[`analyzer_unavailable`](/errors/analyzer_unavailable).

## Termination reason

When `terminated_early == true`, the response carries a structured
`termination_reason`:

| Field                           | Meaning                                                      |
| ------------------------------- | ------------------------------------------------------------ |
| `analyzer`                      | The server key of the analyzer whose rule fired.             |
| `rule`                          | A human-readable description of which signal terminated.     |
| `match`                         | Present when `output_match` was used. The matched substring. |
| `metric` / `value` / `operator` | Present when a threshold rule fired.                         |

This is the easiest way to surface a precise message to your end
user without parsing every analyzer block.

## Aggregated metrics

When the policy's `default_telemetry` is `true`, the response carries
totals across the run:

| Metric                     | Meaning                                                   |
| -------------------------- | --------------------------------------------------------- |
| `total_processing_time_ms` | Sum of analyzer wall-clock. Useful for SLO tracking.      |
| `total_cost_usd`           | Sum of analyzer cost (when each analyzer reports a cost). |

The numbers are summed *only over analyzers that ran*; skipped
analyzers contribute zero.

## Headers worth inspecting

* `X-Request-ID` — same value as `request_id` in the body.
* `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` —
  current rate-limit window state.
* `X-Billing-Status`, `X-Subscription-Status` — subscription health.
  `active` is normal; `past_due` / `canceled` flag attention.
* `X-Agnes-Test-Mode: true` — sandbox response. Exclude from billing
  dashboards.

## Common questions

**Why does `blocked_by` have multiple analyzers?** Asynchronous steps
can have more than one analyzer terminate in the same step. The SDK
reports every analyzer that fired a `terminate_immediately` rule.

**Where is the prompt in the response?** It is not echoed back. The
SDKs keep the input you sent locally; the server does not persist it
unless you explicitly ingest into the threat-intel store.

**Can I get the cost in tokens, not dollars?** `total_cost_usd` is the
public field. The metered usage report at
[`agnes.lasscyber.com/agnes-info/billing`](https://agnes.lasscyber.com/agnes-info/billing)
shows token-level metering for billing reconciliation.

## Next

* [Errors](/errors/overview) — what every error response looks like.
* [Combined analyzer](/concepts/combined-analyzer) — author the
  termination rules that decide what `terminated_early` means.
* [API reference](/api-reference/overview) — full auto-rendered
  schema with interactive playground.
