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

# AgentFailurePattern

> The recurring ways a team's coding agents go wrong, clustered from real corrections and counted without naming anyone.

Every team running coding agents has the same blind spot. Individuals know which prompts keep going sideways, and nobody knows which failures recur across the team, because the evidence is scattered through private sessions and nobody wants their own corrections read back to them.

Exo collects that evidence as signals and clusters it into patterns. Counts are aggregate by construction, and example text is redacted before it is stored.

<Note>
  `AgentFailurePattern` is defined in the v1 contract because the shape is stable and a pilot surface depends on it. The team rollup route that returns it is **not** in the v1 route set. What ships in v1 is the ingest half, [`POST /v1/ingest/agent-signals`](/api-reference/sessions/post-ingest-agent-signals). This page teaches the object and promises no endpoint.
</Note>

Here is the object.

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "patternId": "pat_31c9",
  "label": "Edits the wrong file in a monorepo",
  "summary": "The agent applies a change to a same-named module in a sibling package instead of the one under discussion.",
  "summaryOverride": null,
  "verdict": null,
  "signalCount": 47,
  "distinctSessions": 22,
  "distinctUsers": 6,
  "signalTypeMix": { "correction": 31, "reprompt": 12, "frustration": 4 },
  "exampleTexts": [
    "no, the one in packages/api, not packages/worker",
    "you edited the wrong [user] path again, check the import root first"
  ],
  "trend": { "7d": 9, "30d": 41 }
}
```

## What it is

**Signal**, one moment where a person pushed back on an agent (in practice, a correction, a re-prompt of the same request, or an expression of frustration).

**Pattern**, a cluster of signals that mean the same thing (in practice, a named recurring failure mode with counts and a couple of redacted examples).

**Verdict**, an admin's curation of a pattern (in practice, a way to mark a cluster as noise without deleting the evidence behind it).

A pattern is evidence about agents, not about people. That constraint shows up in every field: counts are distinct-user tallies rather than identities, and examples pass through redaction before storage.

## Where it comes from

The three signal types are detected on the client, inside the session, and posted in batches.

| `signalType`  | What triggers it                                    |
| ------------- | --------------------------------------------------- |
| `correction`  | The person tells the agent it got something wrong   |
| `reprompt`    | The person asks for the same thing again, rephrased |
| `frustration` | The person expresses that this is not working       |

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X POST https://api.get-exo.com/v1/ingest/agent-signals \
    -H "X-Exo-API-Key: $EXO_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "signals": [
        {
          "signalId": "sig_9a20",
          "signalType": "correction",
          "text": "no, the one in packages/api, not packages/worker",
          "sessionId": "sess_44e0",
          "project": "acme-platform",
          "contentTs": "2026-07-14T09:12:03Z"
        }
      ]
    }'
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import os, requests

  r = requests.post(
      "https://api.get-exo.com/v1/ingest/agent-signals",
      headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
      json={
          "signals": [
              {
                  "signalId": "sig_9a20",
                  "signalType": "correction",
                  "text": "no, the one in packages/api, not packages/worker",
                  "sessionId": "sess_44e0",
                  "project": "acme-platform",
                  "contentTs": "2026-07-14T09:12:03Z",
              }
          ]
      },
  )
  r.raise_for_status()
  print(r.json())
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const r = await fetch("https://api.get-exo.com/v1/ingest/agent-signals", {
    method: "POST",
    headers: {
      "X-Exo-API-Key": process.env.EXO_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      signals: [
        {
          signalId: "sig_9a20",
          signalType: "correction",
          text: "no, the one in packages/api, not packages/worker",
          sessionId: "sess_44e0",
          project: "acme-platform",
          contentTs: "2026-07-14T09:12:03Z",
        },
      ],
    }),
  });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  console.log(await r.json());
  ```
</CodeGroup>

```json 202 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{ "ingested": 1, "deduped": 0 }
```

Signal records carry no identity of their own. The credential decides the org and the user, so a record cannot claim to come from someone else. Re-posting the same `signalId` is counted as `deduped` rather than stored twice, which makes a retried batch safe.

A `reprompt` may include `priorText`, the earlier version of the request. The server uses it to confirm the two really are the same ask, then discards it. `priorText` is never stored.

Clustering runs in the background. A new signal joins an existing pattern when it is close enough to that pattern's centre; whatever is left over is clustered fresh. Only the largest new clusters get a written label and summary on each pass, which bounds the cost. The rest keep accumulating evidence and are named later.

## Field reference

| Field              | Type            | What it means                                                              |
| ------------------ | --------------- | -------------------------------------------------------------------------- |
| `patternId`        | string          | Stable id for the cluster.                                                 |
| `label`            | string or null  | Short name for the failure mode. Null while the cluster is unlabelled.     |
| `summary`          | string or null  | What goes wrong, in a sentence or two.                                     |
| `summaryOverride`  | string or null  | An admin's rewrite. Takes precedence over `summary` when present.          |
| `verdict`          | string or null  | An admin's curation verdict, or null while uncurated.                      |
| `signalCount`      | integer         | Total pieces of evidence behind the pattern.                               |
| `distinctSessions` | integer         | How many separate sessions it appears in.                                  |
| `distinctUsers`    | integer         | How many distinct people hit it. **Never says which people.**              |
| `signalTypeMix`    | object          | Count per evidence type, for example `{"correction": 31, "reprompt": 12}`. |
| `exampleTexts`     | array of string | Redacted snippets illustrating the pattern.                                |
| `trend`            | object          | Counts over trailing windows, keyed `7d` and `30d`.                        |

Read `summaryOverride` first and fall back to `summary`. An admin who rewrote a summary did it because the generated one was wrong.

The pair worth watching is `signalCount` against `distinctUsers`. Forty-seven signals from one person is that person's workflow. Forty-seven from six people is a team-level problem with the agent.

## How the privacy shape works

Three mechanisms, and it is worth knowing all three before putting this in front of a team.

* **Aggregate counts only.** `distinctUsers` is a tally. No field on the object names a person, and none is planned.
* **Redaction at rest.** Email addresses and home-directory usernames are stripped from example text before it is stored, which is why `[user]` appears mid-path in the example above. The same redaction runs again on read, so the two cannot drift apart.
* **Example selection.** Very short filler and very long pasted dumps are rejected as examples even when the cluster itself is real, so what you see is representative prose rather than the first two rows.

<AccordionGroup>
  <Accordion title="Why the rollup route is not in v1" icon="circle-question">
    The clustering, the redaction and the aggregate shape are built and running as a pilot. What is not settled is the read surface: who inside an organization should see it, at what granularity, and under which scope. Shipping a route and then changing it would break the rule that no shipped route is ever renamed. The object is documented now so pilot callers have a stable contract, and the route follows once its authorization posture is decided.
  </Accordion>

  <Accordion title="Can a small team be de-anonymised by the counts?" icon="triangle-alert">
    On a two-person team, `distinctUsers: 1` narrows the field to two people, and a distinctive example can narrow it further. Redaction removes identifiers, not style. Treat this as team-level analytics for teams, and do not present it to a group small enough for attribution by elimination.
  </Accordion>

  <Accordion title="Does a verdict delete the evidence?" icon="circle-question">
    No. A verdict marks the cluster and the underlying signals stay. That is deliberate: a pattern dismissed as noise one month can turn out to be a real regression the next, and the evidence has to survive the first judgement for that to be visible. A verdict can be undone.
  </Accordion>

  <Accordion title="Where session content goes, as opposed to signals" icon="circle-question">
    Signals are a separate, narrow channel. Session content itself flows through the coding-session routes and becomes ordinary graph nodes joined by `session_sequence` and `spawned_subagent` edges. Signals never become nodes and are not retrievable as sources. See [Coding session history](/integrations/sessions).
  </Accordion>
</AccordionGroup>

## Limits

* No route in the v1 set returns `AgentFailurePattern`. Ingest ships; the rollup does not.
* Signals are detected on the client. Exo does not mine them from transcripts it already holds, so a team that does not send them has no patterns.
* `priorText` is used for confirmation and never persisted, so no route can return it.
* There is no per-person breakdown, by design.
* The team routes that do ship, [`GET /v1/team`](/api-reference/team/get-team) and [`GET /v1/team/members`](/api-reference/team/list-team-members), return organization and membership data only. Neither returns agent insights, and `GET /v1/team/members` requires an admin key.

## Next

<Columns cols={2}>
  <Card title="Coding session history" icon="terminal" href="/integrations/sessions">
    Getting sessions into the graph in the first place.
  </Card>

  <Card title="Ingest agent signals" icon="code" href="/api-reference/sessions/post-ingest-agent-signals">
    The shipped route, its batch shape and its dedup behaviour.
  </Card>
</Columns>
