JuliusBrussee/caveman · error · TypeError

Native FunctionTool returned an unexpected output

Error message

Native FunctionTool returned an unexpected output

What it means

Error from the completed() step of the LlamaIndex recovery flow: the native FunctionTool returned an output whose shape the middleware does not recognize. Recovery depends on reading the tool's output structure, so an unexpected payload cannot be interpreted and is surfaced as an error.

Solutions

  1. Pass the ToolOutput returned by the framework's tool invocation, not the raw function result
  2. Wrap manual calls with tool.call(...)/call_and_return so a ToolOutput is produced
  3. If you have a raw result, construct ToolOutput(content=..., tool_name=..., raw_input=..., raw_output=...)

Example fix

// before
executor.completed(call, tool.fn(**call.tool_kwargs))
// after
result = tool.call(call.tool_kwargs)
executor.completed(call, result)
Defensive patterns

Strategy: type-guard

Validate before calling

from llama_index.core.tools import ToolOutput
if not isinstance(result, ToolOutput):
    result = ToolOutput(content=str(result), tool_name=call.tool_name, raw_input=call.tool_kwargs, raw_output=result)

Type guard

def is_tool_output(result) -> bool:
    return isinstance(result, ToolOutput)

Try / catch

try:
    executor.completed(call, result)
except TypeError as e:
    log.error("expected ToolOutput: %s", e)

Prevention

When it happens

Trigger: Calling completed() with the raw function return value, a string, or a dict instead of the ToolOutput the tool call produced.

Common situations: Custom tool wrappers that bypass LlamaIndex's call_and_return, or manually invoking tool.fn and passing its result to completed().

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/26ffc6f075cc100f. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/llama_index.py:190

            and self.metadata.get_parameters_dict() == RECOVERY_SCHEMA)

    def invocation(self):
        return _Invocation(self, self.scope, self.tools, self.successful, True)

    def selected(self, call: ToolSelection):
        if self.binding is not None and not self.registered(self.tools, self.invocation()):
            raise ValueError("Native executor registration changed")
        if not isinstance(call, ToolSelection) or not isinstance(call.tool_id, str) or not call.tool_id or not isinstance(call.tool_kwargs, dict):
            raise TypeError("Expected a native ToolSelection")
        matches = [tool for tool in self.tools if tool.metadata.name == call.tool_name]
        if len(matches) != 1:
            raise ValueError("Native tool is not registered")
        self.successful.pop(call.tool_id, None)
        return matches[0]

    def completed(self, call, result):
        if not isinstance(result, ToolOutput):
            raise TypeError("Native FunctionTool returned an unexpected output")
        if not result.is_error and call.tool_name != "caveman_retrieve" and len(self.successful) < 4096:
            self.successful[call.tool_id] = call.tool_name
        return result


@dataclass(frozen=True)
class CavemanLLMTools:
    """Native model and tools for an application-owned loop; no scheduler."""
    model: LLM
    tools: tuple[FunctionTool, ...]
    _registration: Any

    def execute(self, call: ToolSelection) -> ToolOutput:
        tool = self._registration.selected(call)
        return self._registration.completed(call, tool.call(**call.tool_kwargs))

    async def aexecute(self, call: ToolSelection) -> ToolOutput:
        tool = self._registration.selected(call)

View on GitHub (pinned to 3ee70a1026)