Repository navigation
Description
Summary
When the function invocation loop reaches max_function_calls or max_iterations, the core loop correctly stops additional tool calls and makes a final no-tool model call. With the Anthropic provider, that final fallback call can return stop_reason=end_turn with an empty content array, producing a blank final response.
Why this matters
The safety limit itself works: tool calls are capped. The problem is the user-visible final turn. A production agent can hit a function-call/iteration limit and return no text instead of explaining that the limit was reached.
Observed behavior
Using agent-framework-anthropic with Anthropic Opus 4.7:
- Tool function returns
call againuntil called 3 times. - Client config sets either:
client.function_invocation_configuration["max_function_calls"] = 1, orclient.function_invocation_configuration["max_iterations"] = 1
- SDK executes exactly one tool call, then performs the final fallback call with
tool_choice={"type":"none"}. - Raw Anthropic final response is:
stop_reason="end_turn"content=[]
The resulting Agent.run(...) response has no text.
Relevant SDK behavior
The core loop appears to intend a final no-tool answer after limit exhaustion:
- Non-streaming path sets
mutable_options["tool_choice"] = "none"and calls the underlying chat client again after loop exhaustion. - Streaming path does the same with a final inner stream.
- Core tests mock this final call as producing fallback text.
That assumption does not always hold for Anthropic when the last tool result says the model should continue.
Minimal shape of the final Anthropic request
The final request sent by the SDK looked like this conceptually:
{
"tool_choice": {"type": "none"},
"tools": [{"type": "custom", "name": "continue_until_three", "input_schema": {}}],
"messages": [
{"role": "user", "content": [{"type": "text", "text": "Start the loop."}]},
{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_...", "name": "continue_until_three", "input": {"marker": "1"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_...", "content": [{"type": "text", "text": "call again"}], "is_error": false}]}
]
}Anthropic returned content=[].
Direct Anthropic comparison showed:
- Same messages +
tool_choice=none-> empty content. - Same messages with no tools -> empty content.
- Same messages plus explicit user instruction like
Tool call limit reached. Do not call more tools. Explain what happened in one sentence.-> text response.
Expected behavior
When the function invocation loop stops because a configured limit is reached, the SDK should not return an empty final assistant message. Possible fixes:
- Append an explicit final instruction before the no-tool fallback call, e.g. explain that the tool/iteration limit was reached and produce a final answer without further tool use.
- If the fallback call still returns empty content, synthesize a deterministic limit-reached assistant message.
- Expose enough metadata to callers so they can reliably detect
limit_reachedand apply their own deterministic fallback.
Environment
- Python SDK from
microsoft/agent-frameworklocal clone agent-framework-anthropicdependency pin locally loosened to allowanthropic>=0.97.0,<1anthropic==0.101.0- Model:
claude-opus-4-7 - Thinking:
{"type":"adaptive","display":"omitted"} - Output config:
{"effort":"low"}
Local proof
This was found while evaluating Microsoft Agent Framework adoption in a production Azure Functions assistant runtime. We are keeping a local guard to avoid returning blank user responses when a tool/iteration limit is reached.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon May 12, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro: The function invocation loop in packages/core/agent_framework/_tools.py (non-streaming ~line 2483-2506, streaming ~line 2632-2641) makes a final model call with tool_choice="none" after max_iterations or max_function_calls exhaustion, but does not validate the response content. When a provider returns content=[] (as Anthropic does), the SDK produces a blank final message. Minimal repro: construct a BaseChatClient subclass whose _inner_get_response returns ChatResponse with contents=[] when tool_choice="none", set max_iterations=1, call get_response with a tool that triggers one function call, and assert response.messages[-1].text is non-empty — it will be empty string.
python/packages/core/tests/core/test_issue_5769_empty_fallback.py1.3.0eavanvalkenburg commentedon Jun 29, 2026
I opened #6794 with the chosen approach for this case.
The fix keeps the existing final no-tool provider call after
max_iterations/max_function_calls, and only synthesizes a deterministic assistant fallback when that final provider response has no visible content. That keeps normal provider-authored final answers intact while avoiding an empty final response when the loop stops because the invocation limit was reached.We considered adding extra instructions to the final no-tool call so the model explains that tool execution stopped. That is a larger behavioral change: it would affect every provider that uses the shared function-invocation layer, and it adds complexity around hidden instruction injection, provider-specific behavior, and preserving existing prompt semantics. The deterministic fallback is narrower and only applies to the blank-response failure mode.