[fix] Paused live streams carry the terminal stop reason in the finish frame - #5065
Conversation
…op reason The live runner emits its 'done' event with no stopReason (the engine settles paused-vs-ended after the event stream closes, onto the terminal result only), so a HITL-paused SSE stream ended with a generic finish frame while the batch envelope already carried stop_reason: paused. - agent_event_stream appends a corrective terminal 'done' when the result's stop_reason disagrees with the streamed one (the streaming analogue of agent_batch's fold(events, stop_reason=result.stop_reason)) - both vercel adapters prefer the last non-null stop reason, and the dev-only AgentStream variant now prefers the terminal result unconditionally (its old fallback-only read meant terminal-wins never applied) Display was never affected (the FE keys off the interaction_request part); this restores wire fidelity for headless/protocol consumers of the stream.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR changes stop-reason precedence handling in the Vercel streaming adapter and the agent handler. Done events with null stopReason no longer overwrite previously captured non-null values, terminal results take precedence when non-null, and the handler emits a corrective done event when streamed and terminal stop reasons disagree. Tests were added. ChangesStop-reason precedence handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Harness as Runner Harness
participant Handler as agent_event_stream
participant Adapter as Vercel Adapter
participant Client
Harness->>Handler: done event (stopReason may be null)
Handler->>Handler: record streamed stopReason
Harness->>Handler: stream ends
Handler->>Harness: run.result()
Harness-->>Handler: terminal stop_reason
alt terminal differs from streamed
Handler->>Adapter: corrective done event
end
Adapter->>Adapter: resolve final stop_reason (terminal wins if non-null)
Adapter-->>Client: finish frame with resolved finishReason
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
|
Review ask: this is the streaming twin of the #5041 batch fold fix. The judgment call to check: the corrective terminal |
✅ Action performedReview finished.
|
Context
A live (SSE) agent stream that pauses for a tool approval ends with a generic finish frame instead of
finishReasonreflecting the pause. The batch envelope already reportsstop_reason: "paused"after #5041. The cause is the same one #5041 fixed on the batch path: the runner emits itsdoneevent with nostopReason(the engine settles paused-vs-ended only after the event stream closes, on the terminal result), and the stream adapter read only thedoneevent. The playground never noticed because it keys the approval UI off theinteraction_requestpart; headless and protocol consumers of the stream were the ones getting a wrong finish frame.Changes
agent_event_stream(the one layer that holds the terminal result) now appends a corrective terminaldoneevent when the result'sstop_reasondisagrees with the streamed one. Both Vercel adapters keep the last non-null stop reason, so the corrective value wins and a nulldonenever clobbers a real one. This mirrors the batch precedence exactly (fold(events, stop_reason=result.stop_reason)).Before: paused live stream ends with
finishand no pause signal.After: paused live stream ends with
finishReason: "other"derived from the terminalpausedstop reason, same as batch.The dev-only
agent_run_to_vercel_partsvariant had the precedence backwards (it read the terminal result only as a fallback), fixed for consistency.Scope / risk
SDK-only; no runner or wire-schema change. A normal turn emits no extra
done(the corrective event fires only on disagreement). The FE is unaffected either way.Tests
test_vercel_stream_finish_reason.py: paused-with-empty-done, normal turn unchanged, done-only fallback honored, corrective-done emission and non-duplication, end-to-end handler-to-adapter.test_vercel_stream_park.pywith the same three cases against the dev adapter.How to QA
Prerequisites: EE dev stack, a Pi agent with a tool set to Permission "Ask".
Steps:
/messagesresponse).Expected: the final
finishframe carries a non-stopfinishReasonderived from the paused stop reason. Approve; the resumed turn's stream finishes normally.Automated tests:
Edge cases: a stream that errors before the terminal result (no corrective event, error frame unchanged); a
doneevent that itself carries a stop reason with no terminal value (still honored).https://claude.ai/code/session_01DGj7GKafjkZeQXMsryWhb2