Skip to content

fix(tools): resume paused turns in the beta tool runner#1648

Open
slegarraga wants to merge 1 commit into
anthropics:mainfrom
slegarraga:fix/tool-runner-pause-turn
Open

fix(tools): resume paused turns in the beta tool runner#1648
slegarraga wants to merge 1 commit into
anthropics:mainfrom
slegarraga:fix/tool-runner-pause-turn

Conversation

@slegarraga

Copy link
Copy Markdown

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) with stop_reason: "pause_turn". until_done() then returns the intermediate server_tool_use message instead of the model's final answer, so accessing e.g. response.content[0].text raises AttributeError.

Fixes #1170.

Root cause

After each turn, __run__ calls generate_tool_call_response(), which returns None whenever there are no client-side tool_use blocks to satisfy. The loop treated None as "the model is done" and returned — regardless of stop_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:

"pause_turn": we paused a long-running turn. You may provide the response back as-is in a subsequent request to let the model continue.

BetaMessage.stop_reason docstring

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.

  • The existing _messages_modified guard is preserved, so callers that manage their own message history are unaffected.
  • pause_turn iterations increment _iteration_count, so any max_iterations ceiling bounds them exactly like regular tool-use iterations.

Tests

Added offline regression tests (sync + async, via respx) in tests/lib/tools/test_runners.py:

  • test_pause_turn_resumes_loop_sync / test_pause_turn_resumes_loop_async — a pause_turn response (server tool only) followed by an end_turn answer; assert the runner makes the second request, returns the final text, and re-sends the paused assistant turn.
  • test_pause_turn_chained_sync — two pause_turn responses before end_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, and mypy are all clean.

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
@slegarraga slegarraga requested a review from a team as a code owner June 4, 2026 15:37
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.

Tool runner exits early when response contains only server tool use blocks (e.g. web_search, web_fetch) and stop reason: pause_turn

1 participant