Gemini emits unregistered parallel tool calls with ToolStrategy and nested structured output

I’m using LangChain with ChatVertexAI and Gemini 2.5 Flash. A specialist agent returns a nested Pydantic response using ToolStrategy, and also has one real tool available on some requests.

Simplified setup:

class SegmentRecommendation(BaseModel):
    code: str
    score: float
    reasoning: str

class AgentTurnOutput(BaseModel):
    status: str
    reply: str
    segments: list[SegmentRecommendation] | None

agent = create_agent(
    model=ChatVertexAI(model="gemini-2.5-flash", ...),
    tools=[count_api],
    response_format=ToolStrategy(AgentTurnOutput),
)

n the failing trace, Gemini returned two parallel function calls with names that were not registered tools: SegmentRecommendation and google:python_interpreter. Their arguments contained the segment data, but LangChain returned an “is not a valid tool” error and invoked the model again. The model repeated the same calls until LangGraph raised GraphRecursionError. The real count_api tool was not called.

The prompt also described SegmentRecommendation entries and requested JSON-only output. The provider request was in forced tool-calling mode for structured output.

I found a similar symptom in google/adk-python#4173, where the suggested ADK recovery is a bounded reflect-and-retry plugin. I realize that issue is not evidence of the same root cause here.

Questions:

  1. Is an unregistered function name in this situation expected to reach LangChain’s tool loop, or could this indicate a Vertex adapter/provider issue?
  2. What is the recommended LangChain 1.x extension point for intercepting unknown tool calls and either giving the model one correction attempt or stopping immediately?
  3. For nested structured output alongside tools, is it preferable to do tool work first and then make a separate tool-free structured-output call?
  4. Are there known Vertex/Gemini limitations or schema constraints for ToolStrategy with nested Pydantic models?

I’m testing a change that removes tools from the recommendation output call, uses Vertex controlled JSON output, and executes counts deterministically in application code. I’d appreciate guidance on whether that is the recommended separation, and how to prevent invalid tool calls from causing an unbounded retry loop.

Hi @saytoaditya ,

Thanks for the write-up here, and agree with your assessment. This doesn’t appear to be a Vertex adapter bug, it’s model behavior that the agent loop doesn’t bound by default.

With ToolStrategy, the output schema is bound as a tool and the model is forced into tool-calling mode. The handle_errors retry only kicks in for calls that target AgentTurnOutput. A call to some other name like SegmentRecommendation goes to the regular tool node, which returns “not a valid tool” and hands control back to the model. Since the model can’t respond with plain text in forced mode, it just tries again until the recursion limit. My guess is the nested model’s name showing up in both the schema and your prompt is what nudged Gemini into calling it as a function.

For bounding it, an after_model hook catches unknown tool names before they hit the tool node, so you can give one correction and then stop:

from langchain.agents.middleware import after_model
from langchain_core.messages import AIMessage, ToolMessage

ALLOWED = {"count_api", "AgentTurnOutput"}
GUARD = "Not a tool. Put segment data in the segments field of AgentTurnOutput."

@after_model(can_jump_to=\["end"\])
def guard_unknown_tools(state, runtime):
last = state\["messages"\]\[-1\]
if not isinstance(last, AIMessage):
return None
bad = \[tc for tc in last.tool_calls if tc\["name"\] not in ALLOWED\]
if not bad:
return None
already_warned = any(
isinstance(m, ToolMessage) and m.content == GUARD for m in state\["messages"\]
)
if already_warned:
return {"jump_to": "end"}
return {"messages": \[
ToolMessage(content=GUARD, tool_call_id=tc\["id"\], name=tc\["name"\]) for tc in bad
\]}

Pair it with ModelCallLimitMiddleware(run_limit=..., exit_behavior="error") as a hard backstop.

On splitting the work: yes, that’s what I’d do on Gemini 2.5. Those models don’t support native structured output alongside tools, ProviderStrategy for them when toolsare bound. Run the tool step first (or just compute the counts in code), then make a separate tool-free call with native JSON outt of forced tool mode for the stepthat was failing. Gemini 3 can combine the two in one agent.

If you do stay on ToolStrategy, drop the “JSON only” instruction (the output is the tool call, not text) and avoid naming the nested types in the prompt.