Skip to Content
DocsToolsOverview

Tools

A tool is a regular Python callable the agent can invoke. The @tool decorator derives its JSON schema from type hints + docstring; sync functions are automatically dispatched to a worker thread so they never block the event loop.

from loomflow import Agent, tool @tool async def get_weather(city: str) -> str: """Look up the current weather for a city.""" return f"Sunny, 72°F in {city}." @tool(destructive=True) def delete_file(path: str) -> str: """Delete a file. Marked destructive so default permissions ask first.""" import os os.remove(path) return f"deleted {path}" agent = Agent( "You are a productivity assistant.", model="claude-opus-4-7", tools=[get_weather, delete_file], )

Schema derivation

Python annotationJSON schema
str{"type": "string"}
int{"type": "integer"}
float{"type": "number"}
bool{"type": "boolean"}
list[T]{"type": "array", "items": {schema for T}}
dict[str, T]{"type": "object", "additionalProperties": ...}
T | Nonenullable union
Pydantic BaseModelthe model’s JSON schema
Literal["a", "b"]{"type": "string", "enum": ["a", "b"]}

The function’s docstring becomes the tool description; per-arg descriptions can be added via Annotated[type, "description"].

Parallel dispatch

Tool calls in the same model turn run concurrently under a single anyio.create_task_group. Tool results are appended in arrival order , deterministic for the model’s next turn. A failing tool doesn’t poison the others: each exception is captured in its own ToolResult(ok=False) and the loop continues.

Reading run scope inside a tool

Don’t plumb user_id / session_id through every tool signature. Use get_run_context():

from loomflow import get_run_context, tool @tool async def fetch_orders() -> str: ctx = get_run_context() # RunContext return await db.query("orders", user_id=ctx.user_id)

ctx.user_id and ctx.session_id are always populated for tools called inside an agent run.

Marking tools destructive

@tool(destructive=True) is a hint to the permissions layer:

  • With StandardPermissions(mode=Mode.DEFAULT), destructive tools return Decision.ask_(...). Routed through the approval_handler.
  • With Mode.ACCEPT_EDITS, destructive tools auto-approve.
  • With Mode.BYPASS, every call passes (CI / sandbox only).

See Approval handlers.

Single tool, no list-wrapping

Pass one callable directly:

agent = Agent("...", model="...", tools=get_weather)

Lists still work for multiple tools.

InProcessToolHost

When you need a programmable host (custom tool dispatch logic, dynamic registration, metrics), use InProcessToolHost:

from loomflow import Agent from loomflow.tools import InProcessToolHost host = InProcessToolHost([get_weather, delete_file]) host.register(extra_tool) host.unregister("delete_file") agent = Agent("...", model="...", tools=host)

For external tool sources (MCP servers, LangChain bridges) implement the ToolHost protocol directly. Three methods.

Where to next

Tools in skills. Skills (Mode B / C) ship their own tools that auto-register when the skill loads. The framework adds a skill__ prefix automatically. No name collisions across skills. See Three skill modes.

Last updated on