-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Fix Anthropic pause_turn handling to allow agent continuation #3051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
4d76ed1
to
a5ae73a
Compare
@kazmer97 Thanks for working on this! Looks like most of the changes are missing though :) |
just started, will move to draft. trying to fix in my own project first in parallel |
@kazmer97 Sounds good, ping me once this is ready for review! |
quick question while i've got your attention, pydantic_ai_slim/pydantic_ai/_agent_graph.py:590 else:
# we got an empty response with no tool calls, text, thinking, or built-in tool calls.
# this sometimes happens with anthropic (and perhaps other models)
# when the model has already returned text along side tool calls
# in this scenario, if text responses are allowed, we return text from the most recent model
# response, if any
if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
for message in reversed(ctx.state.message_history):
if isinstance(message, _messages.ModelResponse):
text = ''
for part in message.parts:
if isinstance(part, _messages.TextPart):
text += part.content
elif isinstance(part, _messages.BuiltinToolCallPart):
# Text parts before a built-in tool call are essentially thoughts,
# not part of the final result output, so we reset the accumulated text
text = '' # pragma: no cover
if text:
self._next_node = await self._handle_text_response(ctx, text)
return
# Go back to the model request node with an empty request, which means we'll essentially
# resubmit the most recent request that resulted in an empty response,
# as the empty response and request will not create any items in the API payload,
# in the hope the model will return a non-empty response this time. Setting the pause_turn mapping to None according to my reading would land us into this else statement which leads to a retry with unaltered message history which I believe we want. So I think the solution might actually be ready, but correct me please if I missed something obvious, still wrapping my head around the agent graph steps. monkeypatching the mapper in my project seems to have solved the behaviour. |
@kazmer97 We'd want to automatically do a new request right? I'd expect that to be implemented inside |
0d880f5
to
625f1c1
Compare
I went with a slightly different approach, let me know what you think. |
625f1c1
to
0ffbf24
Compare
'content_filter', | ||
'tool_call', | ||
'error', | ||
'incomplete', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't add this here, unfortunately, as these values need to match those supported by the OpenTelemetry gen_ai
spec.
So if we want to implement this generically in the agent graph, we'd need to add a new incomplete=True
boolean to ModelResponse
. I'm hesitant to do that though, for something that so far only Anthropic requires -- we typically don't add fields unless they're supported by 2 major providers.
So if it all possible, can you find a way to implement this exclusively inside the AnthropicModel
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can look into that, but as far as I understand the project design so far, isn't the agent graph that drives the api requests to the model? So far model classes have only been responsible for message translation. Wouldn't it pollute the class if we started introducing api requests into at the model level? Or do you have another mechanism in mind that would allow to manipulate the message chain before it reaches the agent graph?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To address your points:
- As far as I can read the gen_ai spec of otel the pydantic finish reasons are already extending over the examples that are documented.
- Other models are not exhibiting this behaviour yet, but it is a very likely pattern if the trend of provider hosted tools becoming more common continues. Moonshotai has their own internal websearchTool, openai has their internal websearchTool along with goggle. It is a likely pattern to emerge for web search and code execution tools that run longer server side as pause turn allows the client application to prevent timeouts on the api.
Introduces a new official 'incomplete' finish reason to the FinishReason enum to represent cases where a model pauses mid-execution and will continue (e.g., Anthropic's pause_turn during long-running builtin tools). Changes: - Add 'incomplete' to FinishReason TypeAlias with documentation - Map Anthropic's 'pause_turn' to 'incomplete' instead of None - Update agent graph to recognize 'incomplete' and continue with empty request Benefits: - Provider-agnostic: other models can use 'incomplete' for similar behavior - Proper separation of concerns: agent graph doesn't check provider-specific details The agent graph now checks for finish_reason == 'incomplete' instead of checking provider_details for 'pause_turn', maintaining clean architectural boundaries. Fix: Don't increment retries for 'incomplete' finish reason The 'incomplete' finish reason indicates expected mid-execution pausing (e.g., Anthropic's pause_turn during long-running builtin tools), not an error condition. This is normal behavior where the model is saying 'I'm working on something, let me continue' rather than 'something went wrong'.
0ffbf24
to
3b45734
Compare
Summary
Map Anthropic's
pause_turn
finish reason toincomplete
instead of'stop'
so the agent automatically continues with a new request when this finish reason is encountered.Background
When using Anthropic's builtin tools (like
web_search
) withoutput_type
set, long-running operations may trigger thepause_turn
finish reason. Previously, this was mapped to'stop'
, causing the agent to halt execution and attempt a retry. This led to malformed requests with errors like:Changes:
Benefits:
The agent graph now checks for finish_reason == 'incomplete' instead of checking
provider_details for 'pause_turn', maintaining clean architectural boundaries.
Fix: Don't increment retries for 'incomplete' finish reason
The 'incomplete' finish reason indicates expected mid-execution pausing
(e.g., Anthropic's pause_turn during long-running builtin tools), not an
error condition. This is normal behavior where the model is saying 'I'm
working on something, let me continue' rather than 'something went wrong'.
Fixes #2600