Skip to content

Python: fix(claude): propagate usage details and finish_reason from ResultMessage#6927

Open
droideronline wants to merge 2 commits into
microsoft:mainfrom
droideronline:fix/claude-agent-response-metadata
Open

Python: fix(claude): propagate usage details and finish_reason from ResultMessage#6927
droideronline wants to merge 2 commits into
microsoft:mainfrom
droideronline:fix/claude-agent-response-metadata

Conversation

@droideronline

@droideronline droideronline commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #6929

Summary

The Claude Agent SDK's ResultMessage carries token usage statistics (input_tokens, output_tokens, cache tokens) and a stop_reason that were previously discarded by the agent framework. As a result, AgentResponse.usage_details was always None and AgentResponse.finish_reason was always None when using the Claude agent.

Problem

When calling agent.run() with the Claude agent, AgentResponse.usage_details and AgentResponse.finish_reason are always None, even though the underlying Claude Agent SDK's ResultMessage contains this information.

Direct SDK call returns:

ResultMessage(
    session_id="...",
    stop_reason="end_turn",
    usage={"input_tokens": 42, "output_tokens": 18, "cache_read_input_tokens": 5},
)

Agent Framework before this fix:

response.usage_details  # None ❌
response.finish_reason  # None ❌

Agent Framework after this fix:

response.usage_details  # {"input_token_count": 42, "output_token_count": 18, "total_token_count": 60, "cache_read_input_token_count": 5} ✅
response.finish_reason  # "end_turn" ✅

Changes

python/packages/claude/agent_framework_claude/_agent.py

  1. Adds UsageDetails to imports from agent_framework.
  2. Adds _parse_claude_usage_details() — converts the Claude API usage dict to the framework's UsageDetails TypedDict.
  3. Emits usage metadata from ResultMessage — in _get_stream(), when ResultMessage arrives the handler now:
    • Converts message.usageUsageDetails and yields an AgentResponseUpdate with Content.from_usage() so the standard _process_update() path populates AgentResponse.usage_details
    • Maps message.stop_reasonfinish_reason on the update so AgentResponse.finish_reason is set
    • No extra update is emitted when ResultMessage has no usage or stop_reason (backward-compatible no-op)

python/packages/claude/tests/test_claude_agent.py

Adds 4 tests:

  • test_run_propagates_usage_details_from_result_message
  • test_run_propagates_finish_reason_from_result_message
  • test_run_stream_propagates_usage_details
  • test_run_no_usage_no_stop_reason_no_extra_update

Testing

All 65 existing tests pass. 4 new tests added (69 total).

…sage

The Claude Agent SDK's ResultMessage carries token usage statistics
(input_tokens, output_tokens, cache tokens) and a stop_reason that were
previously discarded.  This commit:

* Adds _parse_claude_usage_details() to convert the SDK usage dict to
  the framework's UsageDetails TypedDict.
* Emits an AgentResponseUpdate with a Content.from_usage() payload and
  the stop_reason mapped to finish_reason when ResultMessage arrives,
  so that AgentResponse.usage_details and AgentResponse.finish_reason
  are populated by the standard _process_update() aggregation path.
* Adds tests covering usage propagation, finish_reason propagation,
  streaming usage, and the no-metadata case.
Copilot AI review requested due to automatic review settings July 6, 2026 07:44
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 6, 2026
@github-actions github-actions Bot changed the title fix(claude): propagate usage details and finish_reason from ResultMessage Python: fix(claude): propagate usage details and finish_reason from ResultMessage Jul 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Python Claude agent integration to propagate token usage statistics and the model stop reason from the Claude Agent SDK’s ResultMessage into the Agent Framework’s AgentResponse, so callers can access usage_details and finish_reason when using Claude.

Changes:

  • Added _parse_claude_usage_details() to map Claude usage dict keys into the framework’s UsageDetails.
  • Emitted an AgentResponseUpdate at ResultMessage time to flow usage (via Content.from_usage) and finish_reason through the standard update processing path.
  • Added test coverage to validate usage_details and finish_reason propagation for both non-streaming and streaming runs, plus a no-op scenario.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/claude/agent_framework_claude/_agent.py Parses Claude usage into UsageDetails and emits a final update containing usage/finish metadata from ResultMessage.
python/packages/claude/tests/test_claude_agent.py Adds tests ensuring usage_details and finish_reason propagate (and no extra update is emitted when absent).

Comment thread python/packages/claude/agent_framework_claude/_agent.py Outdated
…resent

Review feedback: total_token_count was only set when the sum was non-zero,
so a response with both input_tokens=0 and output_tokens=0 would omit it
despite the individual counts being present.  Fix the guard to check
whether either token count was returned by the SDK rather than whether
their sum is truthy.
@droideronline

Copy link
Copy Markdown
Contributor Author

@moonbox3 @eavanvalkenburg - kindly take a look when you get a chance. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: bug(claude): AgentResponse.usage_details and finish_reason always None when using ClaudeAgent

3 participants