REST API
Traces & Reconstruction
On this page
The core of Evigauge. A trace is one end-to-end agent execution. Evigauge reconstructs from the raw spans why the agent did what it did, not merely what it did — each sub-resource below is one reconstruction engine's output.
Reconstruction runs asynchronously after ingestion. A trace appears in
GET /traces within seconds, but its engine sub-resources
populate over the following seconds to minutes. A null or empty engine payload
means not yet computed, not failed.
List traces
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces
Cursor-paginated list of traces, newest first. The primary entry point for any trace browser.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
Query parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
page_size | integer | ➖ | 50 | Items per page |
cursor | string | null | ➖ | null | Opaque cursor from the previous response's next_cursor |
start_time | string | null | ➖ | null | ISO 8601 UTC lower bound (inclusive) |
end_time | string | null | ➖ | null | ISO 8601 UTC upper bound (exclusive) |
grade_min | string | null | ➖ | null | Minimum reliability grade — see below |
project | string | null | ➖ | null | Filter to one project_id |
include_empty | boolean | ➖ | false | Include traces that carry no LLM content |
grade_min values
Grades are ranked; grade_min returns every trace at that rank or better.
| Grade | D | C | C+ | B- | B | B+ | A- | A | A+ |
|---|---|---|---|---|---|---|---|---|---|
| Rank | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
So grade_min=B returns B, B+, A-, A, A+. Any other value returns
400, validated before the query runs.
Unscored traces are excluded whenever
grade_minis set — they rank-1, below every real grade. That is the intended semantic ("only show me traces I have a grade for"), but it means addinggrade_mincan drop traces that are merely still being scored, not bad.
Response 200 — not schema-modelled
Row keys are the ClickHouse SELECT aliases, so they are stable:
{
"data": [
{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"ws_uuid_out": "3f2b1c8e-9a4d-4f11-b7c2-1e5a8d3f6b90",
"project_id_out": "checkout-agent",
"trace_start": "2026-09-01T14:23:05Z",
"trace_end": "2026-09-01T14:23:13Z",
"cost_usd_total": 0.0847,
"input_tokens": 14100,
"output_tokens": 4350,
"grade": "B",
"score": 82,
"scorer_version": "2.1.0",
"span_count": 37
}
],
"next_cursor": "eyJ0cyI6MTc1...",
"meta": {
"workspace_id": "3f2b1c8e-9a4d-4f11-b7c2-1e5a8d3f6b90",
"scorer_version": "2.1.0",
"schema_version": "1.0",
"request_id": "9f2c4b1e8a3d4f11b7c21e5a8d3f6b90"
}
}
| Field | Meaning |
|---|---|
trace_start / trace_end | min(start_time) / max(end_time) across the trace. Compute duration from these — there is no duration_ms. |
cost_usd_total | Summed across all spans in the trace, not just the root |
input_tokens / output_tokens | Summed from the gen_ai child spans |
grade | Letter grade — latest scored value |
score | Numeric reliability score, 0–100 |
scorer_version | Distinguishes scored but ungradeable (grade: "", version set) from never scored (grade: "", version "") |
Rows are ordered
trace_start DESC, trace_id DESC.
Examples
# One page
page = evigauge.get(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces",
params={"page_size": 50, "grade_min": "C"},
)
print(page["data"], page["next_cursor"])
# Every trace in a window, transparently paginated
for t in evigauge.paginate(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces",
start_time="2026-09-01T00:00:00Z",
end_time="2026-09-02T00:00:00Z",
page_size=100,
):
print(t["trace_id"], t["cost_usd_total"])
// One page
const page = await evigauge.get(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces",
{ page_size: 50, grade_min: "C" },
);
console.log(page.data, page.next_cursor);
// Every trace in a window, transparently paginated
for await (const t of evigauge.paginate(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces",
{ start_time: "2026-09-01T00:00:00Z", end_time: "2026-09-02T00:00:00Z", page_size: 100 },
)) {
console.log(t.trace_id, t.cost_usd_total);
}
Errors
| Status | Cause |
|---|---|
400 | Malformed cursor, or unparseable start_time / end_time |
403 | API key bound to a different workspace |
404 | Unknown org or workspace slug |
Get a trace
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces/{trace_id}
Full detail for one trace, including its span tree.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
trace_id | string | ✅ | Trace ID (32-char hex, as emitted by OpenTelemetry) |
Response 200 — not schema-modelled
{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"project_id": "checkout-agent",
"start_time": "2026-09-01T14:23:05Z",
"duration_ms": 8420,
"total_cost_usd": 0.0847,
"grade": "B",
"spans": [
{
"span_id": "00f067aa0ba902b7",
"parent_span_id": null,
"name": "agent.run",
"kind": "SERVER",
"start_time": "2026-09-01T14:23:05Z",
"duration_ms": 8420,
"status_code": "OK",
"attributes": { "opexia.project_id": "checkout-agent" }
}
]
}
Examples
trace = evigauge.get(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces/4bf92f3577b34da6a3ce929d0e0e4736"
)
for s in trace["spans"]:
indent = " " if s["parent_span_id"] else ""
print(f"{indent}{s['name']:30} {s['duration_ms']}ms {s['status_code']}")
const trace = await evigauge.get(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces/4bf92f3577b34da6a3ce929d0e0e4736",
);
for (const s of trace.spans) {
const indent = s.parent_span_id ? " " : "";
console.log(`${indent}${s.name.padEnd(30)} ${s.duration_ms}ms ${s.status_code}`);
}
Errors
| Status | Cause |
|---|---|
404 | Unknown trace, or the trace has aged out of the workspace's retention window |
Trace content reliability
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces/{trace_id}/content-reliability
Per-claim hallucination assessment for the trace's output. Each claim the agent made is extracted and scored against the context that was actually available to it.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
trace_id | string | ✅ | Trace ID |
Response 200 — not schema-modelled
{
"trace_id": "4bf92f...",
"grade": "B",
"score": 0.82,
"claims": [
{
"claim": "The customer's last order shipped on 2026-08-14.",
"supported": true,
"confidence": 0.94,
"evidence_span_id": "00f067aa0ba902b7"
},
{
"claim": "Their loyalty tier is Gold.",
"supported": false,
"confidence": 0.31,
"evidence_span_id": null
}
]
}
An unsupported claim with no evidence_span_id is the signal to investigate:
the agent asserted something no retrieved context backed.
Examples
rel = evigauge.get(
"/v1/observ/orgs/{org}/workspaces/{ws}/traces/{}/content-reliability".format(trace_id)
)
unsupported = [c for c in rel["claims"] if not c["supported"]]
print(f"grade={rel['grade']} {len(unsupported)} unsupported claim(s)")
for c in unsupported:
print(" ⚠", c["claim"])
const rel = await evigauge.get(
`/v1/observ/orgs/{org}/workspaces/{ws}/traces/${traceId}/content-reliability`,
);
const unsupported = rel.claims.filter((c: any) => !c.supported);
console.log(`grade=${rel.grade} ${unsupported.length} unsupported claim(s)`);
unsupported.forEach((c: any) => console.log(" ⚠", c.claim));
Trace decision trace
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces/{trace_id}/decision-trace
The reconstructed reasoning DAG — every decision point the agent passed through, the options it had, what it chose, and the basis for that choice. This answers why an agent branched the way it did.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
trace_id | string | ✅ | Trace ID |
Response 200 — not schema-modelled
{
"trace_id": "4bf92f...",
"nodes": [
{
"id": "d1",
"span_id": "00f067aa0ba902b7",
"decision": "route_to_tool",
"chosen": "lookup_order",
"alternatives": ["search_kb", "ask_user"],
"basis": "user message contained an order number",
"confidence": 0.88
}
],
"edges": [{ "from": "d1", "to": "d2" }]
}
Examples
dt = evigauge.get(
f"/v1/observ/orgs/{{org}}/workspaces/{{ws}}/traces/{trace_id}/decision-trace"
)
for n in dt["nodes"]:
alts = ", ".join(n["alternatives"])
print(f"{n['decision']}: chose {n['chosen']} over [{alts}] — {n['basis']}")
const dt = await evigauge.get(
`/v1/observ/orgs/{org}/workspaces/{ws}/traces/${traceId}/decision-trace`,
);
for (const n of dt.nodes) {
console.log(`${n.decision}: chose ${n.chosen} over [${n.alternatives.join(", ")}] — ${n.basis}`);
}
Trace decomposition
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces/{trace_id}/decomposition
How the agent broke the incoming request into sub-tasks, and how each sub-task resolved. Use this to find requests that were mis-decomposed at the first step — a common root cause of an otherwise inexplicable bad answer.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
trace_id | string | ✅ | Trace ID |
Response 200 — not schema-modelled
{
"trace_id": "4bf92f...",
"original_query": "Where is my order and can I still change the address?",
"sub_tasks": [
{ "id": "s1", "text": "locate order status", "resolved": true, "span_ids": ["00f0..."] },
{ "id": "s2", "text": "check address mutability", "resolved": false, "span_ids": [] }
],
"coverage": 0.5
}
coverage below 1.0 means part of the user's request was never addressed.
Examples
d = evigauge.get(
f"/v1/observ/orgs/{{org}}/workspaces/{{ws}}/traces/{trace_id}/decomposition"
)
if d["coverage"] < 1.0:
missed = [s["text"] for s in d["sub_tasks"] if not s["resolved"]]
print("Unaddressed:", missed)
const d = await evigauge.get(
`/v1/observ/orgs/{org}/workspaces/{ws}/traces/${traceId}/decomposition`,
);
if (d.coverage < 1.0) {
console.log("Unaddressed:", d.sub_tasks.filter((s: any) => !s.resolved).map((s: any) => s.text));
}
Trace sources matrix
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces/{trace_id}/sources-matrix
Every source the agent consulted, scored for authority and actual influence on the final answer, with a per-source explainer. Reveals the case where an agent retrieved good sources and then ignored them.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
trace_id | string | ✅ | Trace ID |
Response 200 — not schema-modelled
{
"trace_id": "4bf92f...",
"sources": [
{
"url": "https://docs.internal/returns-policy",
"kind": "internal_kb",
"authority_score": 0.91,
"influence_score": 0.12,
"verification_level": "attribution",
"explainer": "High-authority internal policy doc retrieved but barely reflected in the answer."
}
]
}
Examples
sm = evigauge.get(
f"/v1/observ/orgs/{{org}}/workspaces/{{ws}}/traces/{trace_id}/sources-matrix"
)
# High authority but low influence = retrieved and then ignored.
for s in sm["sources"]:
if s["authority_score"] > 0.8 and s["influence_score"] < 0.3:
print("Ignored authoritative source:", s["url"])
const sm = await evigauge.get(
`/v1/observ/orgs/{org}/workspaces/{ws}/traces/${traceId}/sources-matrix`,
);
sm.sources
.filter((s: any) => s.authority_score > 0.8 && s.influence_score < 0.3)
.forEach((s: any) => console.log("Ignored authoritative source:", s.url));
Trace optimization
GET /v1/observ/orgs/{org}/workspaces/{ws}/traces/{trace_id}/optimization
Per-trace optimization findings from the correlation engine — where this specific execution spent more model, tokens, or latency than the outcome required.
Auth: API key or JWT · Role: viewer
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
org | string | ✅ | Organization slug |
ws | string | ✅ | Workspace slug |
trace_id | string | ✅ | Trace ID |
Response 200 — not schema-modelled
{
"trace_id": "4bf92f...",
"findings": [
{
"kind": "model_downgrade",
"span_id": "00f067aa0ba902b7",
"current_model": "claude-opus-4-6",
"suggested_model": "claude-sonnet-4-6",
"rationale": "Deterministic extraction step; output matched on replay.",
"estimated_savings_usd": 0.031
}
],
"total_estimated_savings_usd": 0.031
}
Examples
opt = evigauge.get(
f"/v1/observ/orgs/{{org}}/workspaces/{{ws}}/traces/{trace_id}/optimization"
)
print(f"Potential saving: ${opt['total_estimated_savings_usd']:.4f}")
for f in opt["findings"]:
print(f" {f['kind']}: {f['current_model']} → {f['suggested_model']}")
const opt = await evigauge.get(
`/v1/observ/orgs/{org}/workspaces/{ws}/traces/${traceId}/optimization`,
);
console.log(`Potential saving: $${opt.total_estimated_savings_usd.toFixed(4)}`);
opt.findings.forEach((f: any) =>
console.log(` ${f.kind}: ${f.current_model} → ${f.suggested_model}`));