Skip to content

Anthropic function limit fallback can return empty final response #5769

Closed

Description

@rg-ve

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 again until called 3 times.
  • Client config sets either:
    • client.function_invocation_configuration["max_function_calls"] = 1, or
    • client.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:

  1. 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.
  2. If the fallback call still returns empty content, synthesize a deterministic limit-reached assistant message.
  3. Expose enough metadata to callers so they can reliably detect limit_reached and apply their own deterministic fallback.

Environment

  • Python SDK from microsoft/agent-framework local clone
  • agent-framework-anthropic dependency pin locally loosened to allow anthropic>=0.97.0,<1
  • anthropic==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.

Activity

  1. added
    bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)
    on May 12, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on May 12, 2026
  3. github-actions commented on May 12, 2026

    @github-actions
    Contributor

    🤖 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.

    • Failing test: python/packages/core/tests/core/test_issue_5769_empty_fallback.py
    • Files examined: python/packages/core/agent_framework/_tools.py, python/packages/core/tests/core/conftest.py, python/packages/core/tests/core/test_function_invocation_logic.py, python/packages/anthropic/agent_framework_anthropic/_chat_client.py, python/packages/core/pyproject.toml, python/packages/anthropic/pyproject.toml
    • Tests run: python/packages/core/tests/core/test_issue_5769_empty_fallback.py::test_empty_content_on_max_iterations_fallback, python/packages/core/tests/core/test_issue_5769_empty_fallback.py::test_empty_content_on_max_function_calls_fallback, python/packages/core/tests/core/test_issue_5769_empty_fallback.py::test_empty_content_on_max_iterations_streaming
    • Current version: 1.3.0
  4. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jun 9, 2026
  5. eavanvalkenburg commented on Jun 29, 2026

    @eavanvalkenburg
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions