Skip to content

Surface finish_reason on assistant messages and token usage events#2254

Open
trungutt wants to merge 1 commit intodocker:mainfrom
trungutt:trungutt/surface-finish-reason
Open

Surface finish_reason on assistant messages and token usage events#2254
trungutt wants to merge 1 commit intodocker:mainfrom
trungutt:trungutt/surface-finish-reason

Conversation

@trungutt
Copy link
Contributor

@trungutt trungutt commented Mar 25, 2026

Summary

Surface the LLM's finish_reason through the streaming pipeline so API consumers can identify the root agent's final response during live streaming.

Motivation

finish_reason is a standard field returned by all major LLM providers (OpenAI, Anthropic, Google) indicating why the model stopped generating. Our provider adapters already parse it correctly, but handleStream() collapses it to a Stopped: bool, losing the distinction between stop, tool_calls, and length.

Clients consuming the SSE API need to distinguish the agent's final answer from intermediate tool-call turns — for example, to avoid grouping the final response with working activity. Today there is no way to do this at the right time:

  • Message shape is unreliable: a sub-agent's response has no tool_calls and looks identical to the root agent's final answer. A Stopped: true response can still contain tool calls (fire-and-forget). An EOF without finish reason can produce a content-only message that isn't final.

  • stream_stopped arrives too late: by the time it fires, the message is already rendered and grouped with intermediate steps.

  • token_usage fires at the right moment (immediately after each assistant turn), and already carries session_id. With finish_reason added, a client can test:

    token_usage.session_id === rootSessionId
      && usage.last_message.finish_reason === "stop"
    

    This identifies the root agent's final response before grouping, while correctly ignoring sub-agent stop turns (different session_id).

Changes

finish_reason is now available in two places:

  • MessageUsage.FinishReason — on token_usage SSE events, available during live streaming
  • chat.Message.FinishReason — persisted in session history, available when loading past sessions

@trungutt trungutt marked this pull request as ready for review March 26, 2026 07:36
@trungutt trungutt requested a review from a team as a code owner March 26, 2026 07:36
@trungutt trungutt closed this Mar 26, 2026
@trungutt trungutt reopened this Mar 26, 2026
@trungutt trungutt marked this pull request as draft March 26, 2026 17:02
@trungutt trungutt marked this pull request as ready for review March 26, 2026 19:42
derekmisler
derekmisler previously approved these changes Mar 26, 2026
docker-agent[bot]

This comment was marked as resolved.

docker-agent[bot]

This comment was marked as resolved.

docker-agent[bot]

This comment was marked as outdated.

@trungutt trungutt marked this pull request as draft March 27, 2026 08:36
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
docker-agent[bot]

This comment was marked as outdated.

@docker docker deleted a comment from docker-agent bot Mar 27, 2026
docker-agent[bot]

This comment was marked as outdated.

@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
Copy link

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

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

Assessment: 🟡 NEEDS ATTENTION

This PR surfaces finish_reason through the streaming pipeline. One medium-severity issue found in session restoration logic.

Copy link

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

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

Assessment: 🟡 NEEDS ATTENTION

This PR surfaces finish_reason through the streaming pipeline. Two medium-severity issues were found in the changed code.

@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
@docker docker deleted a comment from docker-agent bot Mar 27, 2026
Add FinishReason to chat.Message and MessageUsage so API consumers can
distinguish the root agent's final response from intermediate tool-call
turns during live streaming.

- Propagate provider's explicit finish_reason through the streaming
  pipeline (stop/length via early return, tool_calls tracked and
  preserved after the stream loop)
- Infer finish_reason when the provider sends a bare EOF: tool calls
  present → tool_calls, content present → stop, nothing → null
- Validate finish_reason against actual stream output (tool_calls
  requires tool calls, stop is overridden when tool calls exist)
- Reconstruct LastMessage on session restore so FinishReason is
  available for historical sessions (scoped to parent session only)
@trungutt trungutt force-pushed the trungutt/surface-finish-reason branch from b718ef7 to 25bea5c Compare March 27, 2026 15:44
@trungutt trungutt marked this pull request as ready for review March 27, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants