Guide
The Claude Agent SDK runs the same loop as Claude Code: Claude reads files, runs tools and calls the model until the task is done, then hands you one ResultMessage with the usage per model and why it stopped. Agent Ping turns that session into a run, so the cost lands against the customer and feature it was for, and a session that ends badly pages someone.
Install the SDK and point it at your Claude Agent SDK agent. One record lands per run with the cost, status, latency and output behind it.
pip install agentping-io claude-agent-sdk
import agentping
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
agentping.init(api_key="apk_…")
# One query() is one run. The ResultMessage at the end carries the
# usage per model and how the session ended; report both and finish.
async with agentping.run(agent="pr-reviewer", customer_id="acme-corp") as run:
async for message in query(prompt="Review the open pull request", options=ClaudeAgentOptions()):
if isinstance(message, ResultMessage):
for model, usage in (message.model_usage or {}).items():
run.event("llm_call", {
"provider": "anthropic", "model": model,
"input_tokens": usage["inputTokens"] + usage["cacheReadInputTokens"] + usage["cacheCreationInputTokens"],
"output_tokens": usage["outputTokens"],
"cached_input_tokens": usage["cacheReadInputTokens"],
})
run.finish(status="failed" if message.is_error else "success",
output={"result": message.result, "subtype": message.subtype})
The SDK reports usage once, at the end, and ResultMessage.model_usage is the only field that includes subagent calls. One llm_call per model from it, priced server side from the rate card, is the whole integration. Full reference is in the docs.
A Claude Agent SDK session makes many model calls, some of them inside subagents, and reports them once when the loop ends. Which field you read decides whether your numbers are right.
ResultMessage.usage covers the top-level agent loop only; tokens spent inside subagents are missing from it. ResultMessage.model_usage is keyed by model and includes the whole tree, with inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens and a client-side costUSD estimate. Emit one llm_call per model from it and Agent Ping prices the tokens from its own rate card, so the dashboard reconciles with the Anthropic invoice rather than with an estimate.
Agent Ping expects input_tokens to include the cached prefix, the way Anthropic bills it, with cached_input_tokens broken out so the cache discount is applied. Add cacheReadInputTokens and cacheCreationInputTokens to inputTokens before you send, as the snippet above does. Claude Code sessions reuse long contexts, so cache reads are usually most of the input and the discount is large.
ClaudeAgentOptions takes max_turns and max_budget_usd. When either trips, the ResultMessage arrives with subtype error_max_turns or error_max_budget_usd and is_error set, and model_usage still includes the response that crossed the line. Map the subtype to a status so a capped session is a failed run, not a quiet success. Agent Ping budgets work across runs: give the agent or customer a monthly budget and the alert fires before the twentieth session, not inside one.
options = ClaudeAgentOptions(max_turns=25, max_budget_usd=2.00)
STATUS = {
"success": "success",
"error_max_turns": "failed",
"error_max_budget_usd": "failed",
"error_during_execution": "failed",
}
async with agentping.run(agent="pr-reviewer", customer_id="acme-corp") as run:
async for message in query(prompt="…", options=options):
if isinstance(message, ResultMessage):
run.finish(
status=STATUS.get(message.subtype, "failed"),
output={"subtype": message.subtype, "num_turns": message.num_turns,
"duration_ms": message.duration_ms, "result": message.result},
)
Hooks run inside the session on each event. A PostToolUse hook that appends a tool_call event gives the run a timeline: which files were read, which commands ran, what failed, in order.
ClaudeAgentOptions.hooks maps an event name to a list of HookMatcher entries. A matcher with no pattern fires for every tool; set matcher="Bash" or "Write|Edit" to narrow it. The callback receives the hook input (tool_name, tool_input, tool_response), the tool use id and a context, and returns a dict. Return an empty dict to let the session continue unchanged.
PostToolUse fires only when the tool succeeded. Register the same callback for PostToolUseFailure to record the error, and count those events when you decide the run status. A session that finished with subtype success after three failed Bash commands is worth a look.
Hooks sit on the session path. run.event() enqueues and returns; the Agent Ping worker ships events in the background with a hard timeout, so the hook adds microseconds, not a network round trip.
from claude_agent_sdk import HookContext, HookMatcher
run = agentping.run(agent="pr-reviewer", customer_id="acme-corp")
async def record_tool_use(input_data, tool_use_id, context: HookContext):
failed = input_data["hook_event_name"] == "PostToolUseFailure"
run.event("tool_call", {
"name": input_data["tool_name"],
"tool_invocation_id": tool_use_id,
"status": "error" if failed else "ok",
})
return {}
options = ClaudeAgentOptions(
hooks={
"PostToolUse": [HookMatcher(hooks=[record_tool_use])],
"PostToolUseFailure": [HookMatcher(hooks=[record_tool_use])],
},
)
async with run:
async for message in query(prompt="…", options=options):
...
ClaudeSDKClient keeps a conversation open across several query() calls, and a resumed session continues an earlier one. Pick the Agent Ping shape that matches, and read the totals the way the SDK reports them.
Each receive_response() ends with its own ResultMessage, but its model_usage and total_cost_usd are running totals for the whole call so far, not that turn alone. Summing them double counts. Use agentping.conversation() with a session id you keep alongside the client, record a step per turn, and emit the llm_call events once from the last ResultMessage when the client closes. The conversation lands as one run with its combined cost.
An agent that runs from cron or a queue is the one that fails silently. Set an expected schedule and a grace window on the agent and Agent Ping raises a missed-run alert when the next session does not arrive. A session that crashes before its ResultMessage is caught too: the run was opened and never finished, and the async with block closes it as failed when the exception propagates.
If the agent is a person at a terminal rather than a program calling the SDK, skip the Python and run npx agentping-claude-code init. It wires Claude Code hooks so each session reports as a run, with the first prompt as its goal and every tool use as a step, and nothing in the session can block on it.
from claude_agent_sdk import ClaudeSDKClient
session_id = agentping.new_session_id()
chat = agentping.conversation("support-copilot", session_id, customer_id="acme-corp")
last = None
async with ClaudeSDKClient() as client:
for turn, prompt in enumerate(prompts, start=1):
await client.query(prompt)
async for message in client.receive_response():
if isinstance(message, ResultMessage):
last = message
chat.event("step", {"kind": "turn", "subtype": message.subtype}, turn=turn)
# model_usage on the last result is the running total for the whole call.
for model, usage in (last.model_usage or {}).items():
chat.event("llm_call", {
"provider": "anthropic", "model": model,
"input_tokens": usage["inputTokens"] + usage["cacheReadInputTokens"] + usage["cacheCreationInputTokens"],
"output_tokens": usage["outputTokens"],
"cached_input_tokens": usage["cacheReadInputTokens"],
})
chat.finish(status="failed" if last.is_error else "success")
Add the SDK, send one run, and watch cost, status and quality land in the dashboard.
How to monitor LangGraph agents → How to monitor OpenAI Agents SDK agents → How to monitor CrewAI agents →