Skip to content

Run the tool loop

import anyinfer as ai
from pathlib import Path

@ai.tool
def read_file(path: str) -> str:
    """Read a project file."""
    return Path(path).read_text(encoding="utf-8")

@ai.tool
def list_files(directory: str = ".") -> list:
    """List files in a directory."""
    return [p.name for p in Path(directory).iterdir()]

result = client.run_tools(
    "What does README.md say about installation?",
    tools=[read_file, list_files],
    target="anthropic:claude-sonnet-4-5",
    max_rounds=8,
)
print(result.text)

The decorator derives the JSON schema from your signature and the description from your docstring, so a tool is declared once rather than kept in sync with a hand-written schema.

Supported parameter types

str, int, float, bool, list, dict, their parameterized forms (list[str]), and Optional[T]. Anything else raises ToolLoopError when the tool is declared — far better than shipping a schema that misdescribes the tool to a model.

Parameters with defaults are optional; the rest are required.

Errors reach the model, not you

A tool that raises becomes an error-flagged result the model can react to:

@ai.tool
def fetch(url: str) -> str:
    """Fetch a URL."""
    return httpx2.get(url).text        # may raise

The model sees ConnectError: connection refused as a tool result and can apologize, try a different URL, or give up — all ordinary conversation. Only loop-level faults raise to you: an unknown tool, or an exhausted round budget.

The round bound

max_rounds (default 8) bounds the loop, because a model that keeps calling tools would otherwise never terminate:

try:
    result = client.run_tools(prompt, tools=tools, target=target, max_rounds=4)
except ai.ToolLoopError as error:
    log.warning("%s (%s)", error.detail, error.hint)

Execution is sequential

v1 dispatches tool calls one at a time, in the order the model requested. Parallel execution is deliberately deferred: it raises cancellation and ordering questions that no current consumer needs answered.

Naming and overrides

@ai.tool(name="search_docs", description="Search the documentation index.")
def search(query: str, limit: int = 10) -> list:
    ...

Plain functions work too — run_tools(tools=[my_function]) wraps them automatically.

Async

result = await async_client.run_tools(prompt, tools=[read_file], target=target)

Safety

The loop executes whatever the model asks for, within the tools you provide. Treat tool implementations as a security boundary: validate paths, bound sizes and durations, and do not expose a tool that runs arbitrary commands unless that is genuinely your intent.