run-llama/llama_index · error · ValueError

No tool calls found, cannot aggregate results.

Error message

No tool calls found, cannot aggregate results.

What it means

FunctionTool.__init__ raises when metadata is None after fn/async_fn handling: a FunctionTool must carry ToolMetadata (name, description) because agents and LLMs rely on it for tool selection. Unlike from_defaults, plain construction does not infer metadata from the function signature, so omitting it is fatal.

Source

Thrown at llama-index-core/llama_index/core/agent/workflow/base_agent.py:668

        result_ev = ToolCallResult(
            tool_name=ev.tool_name,
            tool_kwargs=ev.tool_kwargs,
            tool_id=ev.tool_id,
            tool_output=result,
            return_direct=tool.metadata.return_direct if tool else False,
        )

        ctx.write_event_to_stream(result_ev)
        return result_ev

    @step
    async def aggregate_tool_results(
        self, ctx: Context, ev: ToolCallResult
    ) -> Union[AgentInput, StopEvent, None]:
        """Aggregate tool results and return the next agent input."""
        num_tool_calls = await ctx.store.get("num_tool_calls", default=0)
        if num_tool_calls == 0:
            raise ValueError("No tool calls found, cannot aggregate results.")

        tool_call_results: list[ToolCallResult] = ctx.collect_events(  # type: ignore
            ev, expected=[ToolCallResult] * num_tool_calls
        )
        if not tool_call_results:
            return None

        memory: BaseMemory = await ctx.store.get("memory")

        # track tool calls made during a .run() call
        cur_tool_calls: List[ToolCallResult] = await ctx.store.get(
            "current_tool_calls", default=[]
        )
        cur_tool_calls.extend(tool_call_results)
        await ctx.store.set("current_tool_calls", cur_tool_calls)

        await self.handle_tool_call_results(ctx, tool_call_results, memory)

View on GitHub (pinned to afd0fef371)

Solutions

  1. Supply ToolMetadata: FunctionTool(fn=f, metadata=ToolMetadata(name='f', description='does f')).
  2. Or use FunctionTool.from_defaults(fn=f, name='f', description='...') which builds metadata for you.
  3. In bulk-construction loops, validate each metadata entry is not None before constructing.

Example fix

# before
tool = FunctionTool(fn=search_web)  # ValueError: metadata must be provided

# after
from llama_index.core.tools import FunctionTool, ToolMetadata
tool = FunctionTool(
    fn=search_web,
    metadata=ToolMetadata(name="search_web", description="Search the web"),
)
Defensive patterns

Strategy: validation

Validate before calling

from llama_index.core.tools import ToolMetadata
if metadata is None:
    metadata = ToolMetadata(name=fn.__name__, description=fn.__doc__ or fn.__name__)
tool = FunctionTool(fn=fn, metadata=metadata)

Type guard

from llama_index.core.tools.tool_metadata import ToolMetadata

def valid_metadata(md) -> bool:
    return isinstance(md, ToolMetadata)

Prevention

When it happens

Trigger: FunctionTool(fn=my_func) with no metadata argument; passing metadata=None explicitly; code ported from FunctionTool.from_defaults(fn=...) (which auto-derives metadata) to direct construction without adding metadata.

Common situations: Copy-paste from examples that use from_defaults while switching to the class constructor; refactors that drop the metadata kwarg; building many tools in a loop where one iteration's metadata dict lookup returns None.

Related errors


AI-assisted analysis of run-llama/llama_index@afd0fef371 (2026-08-15). Data as JSON: /api/errors/f69cb4c6307d8e99. Report an issue: GitHub.