Trace an answer and its cost
Every agent turn leaves a provenance record: a graph of the turn, from the user's message through each model call and tool call to the answer. It names the model of each call and the version of each tool, and, beside the graph, each call's tokens and cost. Use it to answer "where did this answer come from?" and "what did it cost?".
Read a turn's record
Section titled “Read a turn's record”Fetch it by the run's id:
kindgi provenance get a64ce71c-9cfc-4263-87b1-b5562f09079cOver HTTP it's GET /v1/provenance/<run-id>. With kindgi dev,
$KINDGI_API_URL and $KINDGI_API_TOKEN are the URL and token it prints
(they're also in the pack's .kindgirc.json):
curl "$KINDGI_API_URL/v1/provenance/a64ce71c-9cfc-4263-87b1-b5562f09079c" \ -H "Authorization: Bearer $KINDGI_API_TOKEN"Both answer the record:
{ "id": "9f0f7385-5270-484d-9ee4-13716c4edb6e", "runId": "a64ce71c-9cfc-4263-87b1-b5562f09079c", … "dag": { "nodes": [ {"id": "input:0", "kind": "input", "timestamp": "2026-10-08T15:13:29.826Z"}, {"id": "model-call:1", "kind": "model-call", "timestamp": "2026-10-08T15:13:31.345Z", "modelVersion": "anthropic/claude-sonnet-5-5", "attributes": {"step": 1, "model": "claude-sonnet-5-5", "callId": "975d170b-d14f-4fbf-b662-85bcf26b9431", "providerId": "anthropic", "finishReason": "tool-use"}}, {"id": "tool-call:toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "kind": "tool-call", "timestamp": "2026-10-08T15:13:31.369Z", "attributes": {"toolId": "acme.echo", "toolVersion": "0.1.0", "invocationId": "toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "toolVersionRange": "0.1.0"}}, {"id": "tool-result:toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "kind": "tool-result", "timestamp": "2026-10-08T15:13:31.369Z"}, {"id": "model-call:2", "kind": "model-call", "timestamp": "2026-10-08T15:13:32.418Z", "modelVersion": "anthropic/claude-sonnet-5-5", "attributes": {"step": 2, "model": "claude-sonnet-5-5", "callId": "99929d2e-05e2-4452-853e-b6c6f0deed6e", "providerId": "anthropic", "finishReason": "stop"}}, {"id": "model-output:2", "kind": "model-output", "timestamp": "2026-10-08T15:13:32.442Z", "modelVersion": "anthropic/claude-sonnet-5-5"} ], "edges": [ {"from": "model-call:1", "to": "input:0", "kind": "caused-by"}, {"from": "tool-call:toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "to": "model-call:1", "kind": "invoked"}, {"from": "tool-result:toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "to": "tool-call:toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "kind": "produced"}, {"from": "model-call:2", "to": "input:0", "kind": "caused-by"}, {"from": "model-call:2", "to": "tool-result:toolu_01LiLE7Xk9BK16Hkr2tLBEq4", "kind": "influenced-by"}, {"from": "model-output:2", "to": "model-call:2", "kind": "produced"} ] }, "callUsage": { "975d170b-d14f-4fbf-b662-85bcf26b9431": {"usage": {"promptTokens": 645, "completionTokens": 132, "cacheReadTokens": 0, "cacheWriteTokens": 0}, "costUsd": 0.0026100000000000003, "durationMs": 1460, "servedModel": "claude-sonnet-5-5"}, "99929d2e-05e2-4452-853e-b6c6f0deed6e": {"usage": {"promptTokens": 740, "completionTokens": 42, "cacheReadTokens": 0, "cacheWriteTokens": 0}, "costUsd": 0.0019, "durationMs": 1012, "servedModel": "claude-sonnet-5-5"} }}Read it from the answer backwards: the answer (model-output:2) was
produced by the second model call. That call answered the user's message
(caused-by input:0) and read the echo tool's result (influenced-by), which
the tool call the first model call made (invoked) produced.
| Node | What it is |
|---|---|
input |
The message the turn answered. |
model-call |
One call to a model: modelVersion is provider/model; attributes has its step, model, providerId, why it stopped (finishReason) and its callId. |
tool-call, tool-result |
A tool the model called, with the exact tool version that ran, and what it returned. |
model-output |
The answer. |
wait, resume |
A tool call that waited for an approval: the wait, and the decision that ended it. |
Edges point from an effect to its cause: caused-by, invoked, produced,
and influenced-by from a model call to each tool result it read.
callUsage holds each model call's tokens, cost in USD, duration and the
model version that answered (servedModel), by the call's callId. A token
count the provider reports is there, 0 included (cacheReadTokens: 0); one it
doesn't report is absent.
A turn that waited for an approval is recorded whole, before and after the
wait. The wait sits between the tool call and its result: the tool-call
waited-on a wait node, which was resumed-from a resume node, and the
tool-result was caused-by the resume. The resume node's actor is who
decided, and its attributes have the decision, the rationale and the
approval's id. A call a reviewer rejected:
{"id": "tool-hitl-gate-resume:toolu_01MAfC8Fb7e24iY12Cenoh11", "kind": "resume", "timestamp": "2026-10-05T16:26:25.504Z", "actor": "user:26fc414a-b49f-4e1f-a022-162865770178", "attributes": {"gate": "tool-call", "decision": "reject", "rationale": "Not confirmed yet", "approvalId": "12c38101-4cac-4241-8fc7-57323e4f63b9"}}Its tool-call is there too, and its tool-result is the rejection the model
read.
kindgi runs get <run-id> shows the same graph, under output.provenance, without callUsage.
From your app
Section titled “From your app”import { createClient } from '@kindgi/sdk/client';import type { RunId } from '@kindgi/sdk/types';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
const record = await kindgi.provenance.get(process.argv[2] as RunId);for (const node of record.dag.nodes) { const { callId } = (node.attributes ?? {}) as { callId?: string }; const call = typeof callId === 'string' ? record.callUsage?.[callId] : undefined; console.log(node.kind, node.modelVersion ?? '', call ? `${call.usage.promptTokens}+${call.usage.completionTokens} tokens, ${call.costUsd} USD` : '');}import sys
from kindgi.client import Kindgi
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
record = kindgi.provenance.get(sys.argv[1])for node in record.dag.nodes: call_id = (node.attributes or {}).get("callId") call = (record.call_usage or {}).get(call_id) if call_id else None usage = f"{call.usage.prompt_tokens}+{call.usage.completion_tokens} tokens, {call.cost_usd} USD" if call else "" print(node.kind, node.model_version or "", usage)Both print each node, with the tokens and cost of each model call:
inputmodel-call anthropic/claude-sonnet-5-5 645+132 tokens, 0.0026100000000000003 USDtool-calltool-resultmodel-call anthropic/claude-sonnet-5-5 740+42 tokens, 0.0019 USDmodel-output anthropic/claude-sonnet-5-5What a turn cost
Section titled “What a turn cost”Each model call's cost, in callUsage, is its tokens times the prices its
provider was registered with (see Connect a model). The whole
turn's total is in the run's output, under usage:
kindgi runs get <run-id> "usage": { "steps": 2, "durationMs": 4971, "promptTokens": 1671, "totalCostUsd": 0.002156, "completionTokens": 97 },For the cost of several runs, a flow's whole run, or one customer's month, read the cost API: Cost per run and per customer.
A flow's answers
Section titled “A flow's answers”A flow run has no record of its own:
{"error":{"code":"provenance-not-found","message":"No provenance record for run \"41027166-71ec-4bd6-b516-cc47c0787da4\"","details":{"runId":"41027166-71ec-4bd6-b516-cc47c0787da4"},"requestId":"req-997f000a-22f6-470a-b52b-acdd21d6e228"}}Each agent step is a run of its own, with its own record. The step's output in the flow's journal has that run's id:
kindgi runs journal <flow-run-id> { "sequence": 9, "kind": "step.completed", "nodeId": "respond", "payload": { "output": { "text": "⚠ dev-echo isn't a real model: …\n\nTool responded: …", "runId": "4bdae2f9-ff67-48a3-9549-49028d290cc1", …Fetch the record for that run: kindgi provenance get 4bdae2f9-….
Find records
Section titled “Find records”kindgi provenance list lists records, newest first, without their graphs.
--created-after=<time> keeps the newer ones, --run=<run-id> one run's,
--agent=<agent-id> one agent's turns at any version, and --project=<id>
or --org=<id> one project's or one org's. --table prints a row per
record:
kindgi provenance list --created-after=2026-10-06T17:00:00Z --limit=3 --tableRUN CREATED FLOW SIGNED──────────────────────────────────── ──────────────────────── ──── ──────e4cb91b4-d0b1-499a-a282-c75b83c7693c 2026-10-06T17:12:02.090Z noOver HTTP it's GET /v1/provenance, filtered by runId, agentId or
createdAfter:
curl "$KINDGI_API_URL/v1/provenance?createdAfter=2026-10-05T16:25:00Z&limit=3" \ -H "Authorization: Bearer $KINDGI_API_TOKEN"{"data":[{"id":"e87e13ae-472f-404b-8dfe-94ec738980cd","runId":"f91ff793-b029-48e8-9ce4-f9fa70bd0c40","tenantId":"9126df1e-094b-496d-9ea8-7daab93c3f4f","version":"1.0.0","createdAt":"2026-10-05T16:26:30.533Z","signed":false,"projectId":"13b7cfca-8b77-4983-858f-149c16afe56d"},…]}Each record carries its run's projectId. scopeKind=project&scopeId=<id>
lists one project's records, and scopeKind=org&scopeId=<id> every
project's in an org (scope in the clients). Records from before Kindgi
0.1.3 have no project and don't name their agent, so only a list without a
scope or --agent shows them.
signed is false for now: the records themselves aren't signed as
they're written. To hand a run's provenance to someone, export it signed
by the deployment, kindgi provenance export <run-id> > provenance.json,
and check it with kindgi exports verify provenance.json: see
Export signed evidence.