fix(tools): resume paused turns in the beta tool runner#1648
Open
slegarraga wants to merge 1 commit into
Open
Conversation
When a `tool_runner()` response contained only server tool use blocks (e.g. web_search / web_fetch) with stop_reason "pause_turn", the runner exited the loop early and `until_done()` returned the intermediate `server_tool_use` message instead of the model's final answer. `generate_tool_call_response()` returns `None` whenever there are no client-side `tool_use` blocks to satisfy, and the loop treated that as "done" regardless of the stop reason. A "pause_turn" means the turn is still in progress, so the runner now resumes it by sending the paused assistant message back unchanged (the documented continuation contract) and only exits on a genuinely terminal stop reason. Applied to both the sync and async runners. Closes anthropics#1170
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
client.beta.messages.tool_runner(...)exits the loop too early when a response contains only server-side tool use blocks (e.g.web_search,web_fetch) withstop_reason: "pause_turn".until_done()then returns the intermediateserver_tool_usemessage instead of the model's final answer, so accessing e.g.response.content[0].textraisesAttributeError.Fixes #1170.
Root cause
After each turn,
__run__callsgenerate_tool_call_response(), which returnsNonewhenever there are no client-sidetool_useblocks to satisfy. The loop treatedNoneas "the model is done" and returned — regardless ofstop_reason. But"pause_turn"means the turn is still in progress (the server is running a long-running tool); per the documented contract you continue it by sending the partial response back:Fix
When there are no client-side tool calls and
stop_reason == "pause_turn", the runner now appends the paused assistant message unchanged and continues the loop, only exiting on a genuinely terminal stop reason. Applied symmetrically to the sync (BaseSyncToolRunner) and async (BaseAsyncToolRunner) runners._messages_modifiedguard is preserved, so callers that manage their own message history are unaffected.pause_turniterations increment_iteration_count, so anymax_iterationsceiling bounds them exactly like regular tool-use iterations.Tests
Added offline regression tests (sync + async, via
respx) intests/lib/tools/test_runners.py:test_pause_turn_resumes_loop_sync/test_pause_turn_resumes_loop_async— apause_turnresponse (server tool only) followed by anend_turnanswer; assert the runner makes the second request, returns the final text, and re-sends the paused assistant turn.test_pause_turn_chained_sync— twopause_turnresponses beforeend_turn, covering a long server-tool wait.These fail on
main(the runner stops after the first response) and pass with the fix.ruff,pyright, andmypyare all clean.