Skip to content

Sequential tool calls fail with Google Gemini #92

@atharvacoolkni

Description

@atharvacoolkni

Bug: Sequential tool calls fail with Google Gemini - UNEXPECTED_TOOL_CALL

Description

When using Google Gemini models (2.5+) with agents that require sequential tool calls, the second tool call fails with FinishReason.UNEXPECTED_TOOL_CALL and returns empty content.

Environment

  • Library: 10xscale-agentflow
  • Provider: Google Gemini (provider="google")
  • Model: gemini-2.5-flash (also affects other Gemini 2.5+ models)
  • Python: 3.10+

Steps to Reproduce

  1. Create an agent with multiple tools:
from agentflow.core.graph import StateGraph, Agent
from agentflow.core import ToolNode

def tool_a(param: str) -> str:
    """First tool - retrieves data"""
    return "data from tool A"

def tool_b(param: str) -> str:
    """Second tool - processes data"""
    return "processed by tool B"

tool_node = ToolNode([tool_a, tool_b])
agent = Agent(
    model="gemini-2.5-flash",
    provider="google",
    output_type="text",
    tool_node=tool_node
)
  1. Design a prompt that requires the agent to:

    • First call tool_a to retrieve data
    • Then call tool_b to process/submit that data
  2. Run the agent

Expected Behavior

Both tool calls should execute successfully:

  • tool_a → returns result
  • tool_b → returns result
  • Agent returns final response

Actual Behavior

First tool call works, second fails:

[First LLM call] - Tools passed ✓
[tool_a called] - Returns successfully ✓
[Second LLM call] - Tools NOT passed ✗
Response: {
    finish_reason: 'FinishReason.UNEXPECTED_TOOL_CALL',
    tools_calls: None,
    content: []
}

Root Cause

In agentflow/core/graph/agent_internal/execution.py around line 331:

if state.context and state.context[-1].role == "tool":
    response = await self._call_llm_with_retry(messages=messages, stream=is_stream)
else:
    tools = await self._resolve_tools(container)
    response = await self._call_llm_with_retry(
        messages=messages,
        tools=tools if tools else None,
        stream=is_stream,
    )

When the last message is a tool result (role == "tool"), the code skips passing tools to the LLM. This was likely an optimization assuming the model would just process the result and respond.

However, Gemini 2.5+ models with reasoning/thinking often want to make additional tool calls after receiving tool results. When tools aren't passed, Gemini returns UNEXPECTED_TOOL_CALL.

From Google's API documentation:

UNEXPECTED_TOOL_CALL - "The model generated a tool call, but no tools were enabled in the request."

Proposed Fix

Always pass tools to the LLM, regardless of the previous message type:

# Always resolve tools - even after tool results, the model may want to call
# additional tools (e.g., Gemini 2.5+ with sequential tool calls)
tools = await self._resolve_tools(container)
response = await self._call_llm_with_retry(
    messages=messages,
    tools=tools if tools else None,
    stream=is_stream,
)

Impact

Affected workflows:

  • Any agent requiring sequential tool calls (A → B → C)
  • Data retrieval followed by data submission
  • Multi-step validation processes
  • Any "read then write" pattern with tools

Not affected:

  • Single tool call workflows
  • Agents without tools
  • Non-Google providers (OpenAI, Anthropic) - not verified

Workaround

Manually patch the file in your venv until an official fix is released.

Additional Context

I discovered this while building a Sprint Update Validator that needs to:

  1. Call get_sprint_updates_tool to retrieve history
  2. Call submit_update_tool to save the validated update
  3. Return JSON response

The first tool worked, but the second always failed with UNEXPECTED_TOOL_CALL.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions