Investigations
AI-assisted root-cause analysis: generate hypotheses and prompt fixes for recurring call issues, test them, and apply the winner.
An investigation takes a cluster of related issue reports on one agent and works toward a prompt fix: the AI generates hypotheses about the root cause, drafts prompt candidates (you can add your own), each candidate can be tested in an experiment (replaying the failing calls' inputs against the candidate prompt), and the winning candidate is applied to the agent.
Status flow: idle → generating_hypotheses → awaiting_candidates
→ searching → ready → applied (or dismissed at any point).
Starting an investigation kicks off the whole flow automatically —
diagnosis, candidate prompt changes, and replay testing for each one.
Steps run asynchronously — poll the detail endpoint.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/investigations | List investigations (?agent_id=, ?status=) |
POST | /v1/investigations | Start an investigation |
GET | /v1/investigations/{investigation_id} | Full detail with candidates + experiments |
PATCH | /v1/investigations/{investigation_id} | Update experiment_config, or dismiss |
POST | /v1/investigations/{investigation_id}/hypotheses | Re-kick diagnosis on an idle investigation |
POST | /v1/investigations/{investigation_id}/candidates/manual | Add your own candidate prompt |
POST | /v1/investigations/{investigation_id}/candidates/{candidate_id}/experiments | Run an experiment for a candidate |
GET | /v1/investigations/{investigation_id}/experiments/{experiment_id} | Poll one experiment |
POST | /v1/investigations/{investigation_id}/apply | Apply a candidate's prompt to the agent |
All ids on this page are UUIDs except agent_id and call_ids.
Investigation object
{
"id": "e7a91c2d-…",
"issue_pattern_id": "long-pauses-before-answering",
"issue_trace_ids": ["5f9a…", "77b1…"],
"agent_id": 12,
"baseline_prompt": "You are a helpful support agent…",
"call_ids": [987654321, 987654322],
"status": "ready",
"experiment_config": { "sample_size": 5 },
"hypotheses": [
{ "id": "h1", "title": "Prompt over-constrains responses", "rationale": "…" }
],
"baseline_results": null,
"candidates": [
{
"id": "c3d4…",
"source": "ai",
"hypothesis_id": "h1",
"label": "Loosen response constraints",
"prompt": "You are a helpful support agent… (revised)",
"rationale": "…",
"created_at": "2026-04-20T18:30:00.000Z",
"latest_experiment_id": "a1b2…"
}
],
"experiments": [
{
"id": "a1b2…",
"candidate_id": "c3d4…",
"status": "completed",
"config": { "sample_size": 5 },
"samples": [
{
"id": "s1…",
"call_id": 987654321,
"sample_index": 0,
"user_input": "…",
"original_output": "…",
"replayed_output": "…",
"grade": { "verdict": "improved" },
"created_at": "2026-04-20T18:31:00.000Z"
}
],
"summary": { "improved": 4, "regressed": 0, "unchanged": 1 },
"created_at": "2026-04-20T18:30:30.000Z",
"completed_at": "2026-04-20T18:32:10.000Z",
"error_message": null
}
],
"applied_candidate_id": null,
"applied_agent_version": null,
"error_message": null,
"created_at": "2026-04-20T18:24:10.113Z",
"updated_at": "2026-04-20T18:32:10.000Z"
}The list endpoint returns {"results": [...]} with the same
fields minus the nested candidates and experiments.
| Field | Type | Description |
|---|---|---|
id | UUID | Investigation id |
issue_pattern_id | string | Your identifier for the issue cluster (≤ 255 chars) |
issue_trace_ids | array of string | The issue reports under investigation |
agent_id | integer | The agent being fixed |
baseline_prompt | string | Snapshot of the agent prompt when the investigation started |
call_ids | array of integer | Example failing calls used as evidence |
status | string | idle, generating_hypotheses, awaiting_candidates, generating_candidates, ready, applied, dismissed |
experiment_config | object | Default config passed to experiments |
hypotheses | array | AI-generated root-cause hypotheses |
candidates | array | Prompt candidates — source is ai or manual; hypothesis_id links back to a hypothesis |
experiments | array | All experiments across candidates, newest first. Sample grade compares original_output vs replayed_output |
applied_candidate_id | UUID | null | Set once applied |
applied_agent_version | integer | null | The agent version revision the apply created |
Start an investigation
curl -X POST https://api.thunderphone.com/v1/investigations \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"issue_pattern_id": "long-pauses-before-answering",
"issue_trace_ids": ["5f9a…", "77b1…"],
"agent_id": 12,
"call_ids": [987654321, 987654322]
}'| Field | Type | Required | Description |
|---|---|---|---|
issue_pattern_id | string | yes | ≤ 255 chars |
issue_trace_ids | array of string | yes | Non-empty |
agent_id | integer | yes | Must belong to your org (404 otherwise) |
call_ids | array of integer | yes | Non-empty |
experiment_config | object | no | Defaults to {} |
Returns 201 with the investigation already running
(status="generating_hypotheses") — creation launches the full flow
automatically: diagnosis and baseline replays, then the autonomous fix
search. Poll GET …/{investigation_id} until status="ready" and read
search_result.
Drive the workflow
- Poll
GET …/{investigation_id}— the investigation advances throughgenerating_hypotheses→awaiting_candidates→searching→readyon its own. POST …/candidates/manual— add your own candidate at any non-terminal point:{"label", "prompt", "rationale"?}→201with the new candidate. Manual candidates join the next search round, or can be tested directly (next step).POST …/candidates/{candidate_id}/experiments— body{"config": {…}}(optional). Replays the failing calls' inputs against the candidate prompt asynchronously. Returns201with the experiment (status: "running"); pollGET …/experiments/{experiment_id}untilcompleted/failed.POST …/apply— body{"candidate_id": "…"}. Writes the candidate's prompt to the agent, records an agent config version, sets statusapplied, and auto-resolves the investigation's linked issue reports. Allowed fromready/awaiting_candidates(409otherwise). Returns the final investigation.POST …/hypotheses— re-kick diagnosis on an investigation that endedidle(for example after an interrupted run).409when a run is already in progress.
To abandon an investigation:
PATCH /v1/investigations/{id} with {"status": "dismissed"} —
dismissed is the only status a client may set directly.
Run the fix search
POST /v1/investigations/{investigation_id}/search
Runs the autonomous fix search: the AI drafts diverse prompt changes (and,
for eligible agents, a tier upgrade), screens each one in parallel replays of
the AI turn, culls weak variants, and confirms the best result on fresh
replays plus calls without the issue. The investigation's search_result
carries the outcome — tier is fixed (eliminated in a confirmation round),
improved (a confirmed reduction), or null when no change was confirmed.
A completed search with no winner flags the cluster to ThunderPhone with the
replay evidence; when stopped_early is "testing_incomplete" or
"replay_budget_exhausted" the search could not finish verifying its
candidates, nothing is flagged, and rerunning the search is the right next
step (previous searches' best unconfirmed changes are carried into the new
round automatically).