Skip to content

windlass.tools

tools

Tools — turning Python functions into agent capabilities.

The whole surface is one decorator::

from windlass import tool

@tool
def get_weather(city: str, units: str = "celsius") -> dict:
    # Docstring summary becomes the model-facing description, and a
    # Google-style argument section becomes the per-parameter descriptions.
    # See :func:`tool` for a fully documented example.
    return {"city": city, "temp": 18, "units": units}

The schema comes from the type hints, the description from the docstring, and the result is a :class:~windlass.interfaces.tool.Tool that any Windlass agent can bind. Async functions work identically; sync functions are run on a worker thread so a slow tool never blocks the event loop.

This module also provides :class:ToolRegistry, which agents use to hold their bound tools and execute calls — including parallel execution when a model requests several at once.

FunctionTool

FunctionTool(
    fn: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    **config: Any
)

Bases: Tool

A :class:~windlass.interfaces.tool.Tool backed by a Python function.

Normally produced by the :func:tool decorator rather than constructed directly.

Parameters:

Name Type Description Default
fn Callable[..., Any]

The function to wrap. May be sync or async.

required
name str | None

Tool name. Defaults to the function's name.

None
description str | None

Model-facing description. Defaults to the docstring summary.

None
parameters dict[str, Any] | None

JSON Schema override. Derived from the signature by default.

None
timeout float | None

Seconds before the call is abandoned.

None
requires_approval bool

Pause for human approval before running.

False
**config Any

Forwarded to :class:~windlass.interfaces.tool.Tool.

{}

Attributes:

Name Type Description
fn

The wrapped function, still callable directly.

is_async

Whether the function is a coroutine function.

Example

def add(a: int, b: int) -> int: ... '''Add two integers.''' ... return a + b t = FunctionTool(add) t.run(a=2, b=3).data 5

Source code in src\windlass\tools\__init__.py
def __init__(
    self,
    fn: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    **config: Any,
) -> None:
    summary, _ = parse_docstring(inspect.getdoc(fn))
    super().__init__(
        name=name or fn.__name__,
        description=description or summary or f"Call {fn.__name__}.",
        parameters=parameters or function_schema(fn),
        timeout=timeout,
        requires_approval=requires_approval,
        **config,
    )
    self.fn = fn
    self.is_async = asyncio.iscoroutinefunction(fn)
    self.__doc__ = fn.__doc__
    self.__name__ = getattr(fn, "__name__", self.name)

acall async

acall(**kwargs: Any) -> Any

Invoke the wrapped function.

Sync functions run on a worker thread, so a blocking HTTP call inside a tool cannot stall the agent's event loop.

Parameters:

Name Type Description Default
**kwargs Any

Arguments matching the schema.

{}

Returns:

Type Description
Any

The function's return value.

Raises:

Type Description
ValidationError

When a required argument is missing.

Exception

Anything the function raises.

Source code in src\windlass\tools\__init__.py
async def acall(self, **kwargs: Any) -> Any:
    """Invoke the wrapped function.

    Sync functions run on a worker thread, so a blocking HTTP call inside a
    tool cannot stall the agent's event loop.

    Args:
        **kwargs: Arguments matching the schema.

    Returns:
        The function's return value.

    Raises:
        ValidationError: When a required argument is missing.
        Exception: Anything the function raises.
    """
    self._validate(kwargs)
    if self.is_async:
        return await self.fn(**kwargs)
    return await to_thread(self.fn, **kwargs)

native

native() -> Any

Return the wrapped function.

Source code in src\windlass\tools\__init__.py
def native(self) -> Any:
    """Return the wrapped function."""
    return self.fn

ToolRegistry

ToolRegistry(tools: Sequence[Tool] | None = None)

The set of tools bound to one agent.

Holds tools by name, renders provider-specific schemas, and executes calls — in parallel when the model requests several at once, which is where a lot of agent latency quietly hides.

Parameters:

Name Type Description Default
tools Sequence[Tool] | None

Initial tools to bind.

None

Attributes:

Name Type Description
tools dict[str, Tool]

Bound tools, keyed by name.

Example

@tool ... def double(x: int) -> int: ... '''Double a number.''' ... return x * 2 registry = ToolRegistry([double]) registry.execute(ToolCall(name="double", arguments={"x": 4})).data 8

Source code in src\windlass\tools\__init__.py
def __init__(self, tools: Sequence[Tool] | None = None) -> None:
    self.tools: dict[str, Tool] = {}
    self._lock = threading.RLock()
    for item in tools or ():
        self.add(item)

add

add(item: Tool | Callable[..., Any]) -> Tool

Bind a tool.

Parameters:

Name Type Description Default
item Tool | Callable[..., Any]

A :class:~windlass.interfaces.tool.Tool, or a plain function which is wrapped automatically.

required

Returns:

Type Description
Tool

The bound tool.

Raises:

Type Description
TypeError

If item is neither a tool nor callable.

Source code in src\windlass\tools\__init__.py
def add(self, item: Tool | Callable[..., Any]) -> Tool:
    """Bind a tool.

    Args:
        item: A :class:`~windlass.interfaces.tool.Tool`, or a plain function
            which is wrapped automatically.

    Returns:
        The bound tool.

    Raises:
        TypeError: If ``item`` is neither a tool nor callable.
    """
    if isinstance(item, Tool):
        built = item
    elif callable(item):
        built = FunctionTool(item)
    else:
        raise TypeError(f"Expected a Tool or a callable, got {type(item).__name__}.")
    with self._lock:
        if built.name in self.tools and self.tools[built.name] is not built:
            _log.debug("Replacing already-bound tool %r.", built.name)
        self.tools[built.name] = built
    return built

extend

extend(items: Sequence[Tool | Callable[..., Any]]) -> ToolRegistry

Bind several tools and return self.

Source code in src\windlass\tools\__init__.py
def extend(self, items: Sequence[Tool | Callable[..., Any]]) -> ToolRegistry:
    """Bind several tools and return ``self``."""
    for item in items:
        self.add(item)
    return self

remove

remove(name: str) -> None

Unbind a tool. No-op when it is not bound.

Source code in src\windlass\tools\__init__.py
def remove(self, name: str) -> None:
    """Unbind a tool. No-op when it is not bound."""
    with self._lock:
        self.tools.pop(name, None)

get

get(name: str) -> Tool

Return a bound tool.

Parameters:

Name Type Description Default
name str

Tool name.

required

Returns:

Type Description
Tool

The tool.

Raises:

Type Description
ToolNotFoundError

When no tool of that name is bound. The message lists what is available.

Source code in src\windlass\tools\__init__.py
def get(self, name: str) -> Tool:
    """Return a bound tool.

    Args:
        name: Tool name.

    Returns:
        The tool.

    Raises:
        ToolNotFoundError: When no tool of that name is bound. The message
            lists what is available.
    """
    with self._lock:
        found = self.tools.get(name)
        if found is None:
            raise ToolNotFoundError(name, list(self.tools))
        return found

names

names() -> list[str]

Return the bound tool names, sorted.

Source code in src\windlass\tools\__init__.py
def names(self) -> list[str]:
    """Return the bound tool names, sorted."""
    with self._lock:
        return sorted(self.tools)

schemas

schemas(style: str = 'openai') -> list[dict[str, Any]]

Return every bound tool's definition in a provider's format.

Parameters:

Name Type Description Default
style str

"openai", "anthropic" or "gemini".

'openai'

Returns:

Type Description
list[dict[str, Any]]

Tool definitions, or [] when nothing is bound — which is what

list[dict[str, Any]]

lets an agent pass tools=None cleanly.

Source code in src\windlass\tools\__init__.py
def schemas(self, style: str = "openai") -> list[dict[str, Any]]:
    """Return every bound tool's definition in a provider's format.

    Args:
        style: ``"openai"``, ``"anthropic"`` or ``"gemini"``.

    Returns:
        Tool definitions, or ``[]`` when nothing is bound — which is what
        lets an agent pass ``tools=None`` cleanly.
    """
    with self._lock:
        return [t.schema(style=style) for t in self.tools.values()]

aexecute async

aexecute(call: ToolCall) -> ToolResult

Execute one tool call.

An unknown tool produces an error :class:~windlass.core.types.ToolResult rather than an exception, so the agent can tell the model it hallucinated a tool and let it recover.

Parameters:

Name Type Description Default
call ToolCall

The call to execute.

required

Returns:

Type Description
ToolResult

The result.

Source code in src\windlass\tools\__init__.py
async def aexecute(self, call: ToolCall) -> ToolResult:
    """Execute one tool call.

    An unknown tool produces an error :class:`~windlass.core.types.ToolResult`
    rather than an exception, so the agent can tell the model it hallucinated
    a tool and let it recover.

    Args:
        call: The call to execute.

    Returns:
        The result.
    """
    try:
        target = self.get(call.name)
    except ToolNotFoundError as exc:
        return ToolResult(
            call_id=call.id,
            name=call.name,
            content=str(exc),
            is_error=True,
        )
    return await target.ainvoke(call)

execute

execute(call: ToolCall) -> ToolResult

Blocking :meth:aexecute.

Source code in src\windlass\tools\__init__.py
def execute(self, call: ToolCall) -> ToolResult:
    """Blocking :meth:`aexecute`."""
    from windlass.core.concurrency import run_sync

    return run_sync(self.aexecute(call))

aexecute_many async

aexecute_many(
    calls: Sequence[ToolCall], *, parallel: bool = True, limit: int = 8
) -> list[ToolResult]

Execute several tool calls.

Parameters:

Name Type Description Default
calls Sequence[ToolCall]

The calls to execute.

required
parallel bool

Run them concurrently. Models routinely request three or four independent lookups in one turn; running those serially triples the user's wait for no reason.

True
limit int

Maximum concurrent executions.

8

Returns:

Type Description
list[ToolResult]

Results in the same order as calls.

Example

import asyncio @tool ... def echo(text: str) -> str: ... '''Echo text.''' ... return text registry = ToolRegistry([echo]) calls = [ToolCall(name="echo", arguments={"text": t}) for t in "ab"][r.content for r in asyncio.run(registry.aexecute_many(calls))] ['a', 'b']

Source code in src\windlass\tools\__init__.py
async def aexecute_many(
    self, calls: Sequence[ToolCall], *, parallel: bool = True, limit: int = 8
) -> list[ToolResult]:
    """Execute several tool calls.

    Args:
        calls: The calls to execute.
        parallel: Run them concurrently. Models routinely request three or
            four independent lookups in one turn; running those serially
            triples the user's wait for no reason.
        limit: Maximum concurrent executions.

    Returns:
        Results in the same order as ``calls``.

    Example:
        >>> import asyncio
        >>> @tool
        ... def echo(text: str) -> str:
        ...     '''Echo text.'''
        ...     return text
        >>> registry = ToolRegistry([echo])
        >>> calls = [ToolCall(name="echo", arguments={"text": t}) for t in "ab"]
        >>> [r.content for r in asyncio.run(registry.aexecute_many(calls))]
        ['a', 'b']
    """
    if not calls:
        return []
    if not parallel or len(calls) == 1:
        return [await self.aexecute(call) for call in calls]
    return await gather_bounded([self.aexecute(c) for c in calls], limit=limit)

execute_many

execute_many(calls: Sequence[ToolCall], *, parallel: bool = True) -> list[ToolResult]

Blocking :meth:aexecute_many.

Source code in src\windlass\tools\__init__.py
def execute_many(self, calls: Sequence[ToolCall], *, parallel: bool = True) -> list[ToolResult]:
    """Blocking :meth:`aexecute_many`."""
    from windlass.core.concurrency import run_sync

    return run_sync(self.aexecute_many(calls, parallel=parallel))

needs_approval

needs_approval(calls: Sequence[ToolCall]) -> list[ToolCall]

Return the calls whose tools require human approval.

Parameters:

Name Type Description Default
calls Sequence[ToolCall]

Calls the model requested.

required

Returns:

Type Description
list[ToolCall]

The subset that must be approved before running.

Source code in src\windlass\tools\__init__.py
def needs_approval(self, calls: Sequence[ToolCall]) -> list[ToolCall]:
    """Return the calls whose tools require human approval.

    Args:
        calls: Calls the model requested.

    Returns:
        The subset that must be approved before running.
    """
    approvals: list[ToolCall] = []
    for call in calls:
        found = self.tools.get(call.name)
        if found is not None and found.requires_approval:
            approvals.append(call)
    return approvals

describe

describe() -> list[dict[str, Any]]

Return a JSON-safe summary of every bound tool.

Source code in src\windlass\tools\__init__.py
def describe(self) -> list[dict[str, Any]]:
    """Return a JSON-safe summary of every bound tool."""
    with self._lock:
        return [t.describe() for t in self.tools.values()]

render_result

render_result(value: Any) -> str

Render a tool's return value as text for the model.

Strings pass through; everything else is JSON-encoded when possible and repr-ed otherwise. Keeping this in one place means every tool's output looks the same to the model regardless of who wrote it.

Parameters:

Name Type Description Default
value Any

The tool's return value.

required

Returns:

Type Description
str

A string safe to place in a tool message.

Example

render_result({"a": 1}) '{"a": 1}' render_result("plain") 'plain'

Source code in src\windlass\interfaces\tool.py
def render_result(value: Any) -> str:
    """Render a tool's return value as text for the model.

    Strings pass through; everything else is JSON-encoded when possible and
    ``repr``-ed otherwise. Keeping this in one place means every tool's output
    looks the same to the model regardless of who wrote it.

    Args:
        value: The tool's return value.

    Returns:
        A string safe to place in a ``tool`` message.

    Example:
        >>> render_result({"a": 1})
        '{"a": 1}'
        >>> render_result("plain")
        'plain'
    """
    if value is None:
        return "null"
    if isinstance(value, str):
        return value
    try:
        return json.dumps(value, ensure_ascii=False, default=str)
    except (TypeError, ValueError):  # pragma: no cover - exotic objects
        return repr(value)

function_schema

function_schema(
    fn: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    skip: tuple[str, ...] = ("self", "cls")
) -> dict[str, Any]

Build a JSON Schema parameters object for a function.

Parameters:

Name Type Description Default
fn Callable[..., Any]

The function to inspect. Type hints are resolved against its module globals, so from __future__ import annotations works fine.

required
name str | None

Override for the tool name. Defaults to fn.__name__.

None
description str | None

Override for the description. Defaults to the docstring summary.

None
skip tuple[str, ...]

Parameter names to omit.

('self', 'cls')

Returns:

Type Description
dict[str, Any]

A JSON Schema object with type, properties, required and

dict[str, Any]

additionalProperties: false.

Raises:

Type Description
TypeError

If fn is not callable.

Note

*args and **kwargs are skipped — providers cannot express them, and a tool that needs them almost always wants an explicit list or dict parameter instead.

Example

def greet(name: str, excited: bool = False) -> str: ... '''Greet someone. ... ... Args: ... name: Who to greet. ... excited: Add an exclamation mark. ... ''' ... return name schema = function_schema(greet) schema["properties"]["name"]["description"] 'Who to greet.' schema["required"]['name']

Source code in src\windlass\tools\schema.py
def function_schema(
    fn: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    skip: tuple[str, ...] = ("self", "cls"),
) -> dict[str, Any]:
    """Build a JSON Schema ``parameters`` object for a function.

    Args:
        fn: The function to inspect. Type hints are resolved against its module
            globals, so ``from __future__ import annotations`` works fine.
        name: Override for the tool name. Defaults to ``fn.__name__``.
        description: Override for the description. Defaults to the docstring
            summary.
        skip: Parameter names to omit.

    Returns:
        A JSON Schema object with ``type``, ``properties``, ``required`` and
        ``additionalProperties: false``.

    Raises:
        TypeError: If ``fn`` is not callable.

    Note:
        ``*args`` and ``**kwargs`` are skipped — providers cannot express them,
        and a tool that needs them almost always wants an explicit list or dict
        parameter instead.

    Example:
        >>> def greet(name: str, excited: bool = False) -> str:
        ...     '''Greet someone.
        ...
        ...     Args:
        ...         name: Who to greet.
        ...         excited: Add an exclamation mark.
        ...     '''
        ...     return name
        >>> schema = function_schema(greet)
        >>> schema["properties"]["name"]["description"]
        'Who to greet.'
        >>> schema["required"]
        ['name']
    """
    if not callable(fn):
        raise TypeError(f"Expected a callable, got {type(fn).__name__}.")

    signature = inspect.signature(fn)
    hints = _resolve_hints(fn)

    _, arg_docs = parse_docstring(inspect.getdoc(fn))

    properties: dict[str, Any] = {}
    required: list[str] = []

    for parameter in signature.parameters.values():
        if parameter.name in skip:
            continue
        if parameter.kind in (parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD):
            continue

        annotation = hints.get(parameter.name, parameter.annotation)
        schema = python_type_to_schema(annotation)

        doc = arg_docs.get(parameter.name)
        if doc:
            schema["description"] = doc

        if parameter.default is not inspect.Parameter.empty:
            if _is_jsonable(parameter.default):
                schema["default"] = parameter.default
        else:
            required.append(parameter.name)

        properties[parameter.name] = schema or {}

    out: dict[str, Any] = {
        "type": "object",
        "properties": properties,
        "additionalProperties": False,
    }
    if required:
        out["required"] = required
    return out

parse_docstring

parse_docstring(doc: str | None) -> tuple[str, dict[str, str]]

Split a Google-style docstring into a summary and per-argument descriptions.

Parameters:

Name Type Description Default
doc str | None

The raw docstring, or None.

required

Returns:

Type Description
str

A (summary, {arg_name: description}) pair. The summary is everything

dict[str, str]

before the first section header.

Example

summary, args = parse_docstring( ... "Add numbers.\n\nArgs:\n a: First.\n b: Second.\n" ... ) summary, args["a"] ('Add numbers.', 'First.')

Source code in src\windlass\tools\schema.py
def parse_docstring(doc: str | None) -> tuple[str, dict[str, str]]:
    r"""Split a Google-style docstring into a summary and per-argument descriptions.

    Args:
        doc: The raw docstring, or ``None``.

    Returns:
        A ``(summary, {arg_name: description})`` pair. The summary is everything
        before the first section header.

    Example:
        >>> summary, args = parse_docstring(
        ...     "Add numbers.\n\nArgs:\n    a: First.\n    b: Second.\n"
        ... )
        >>> summary, args["a"]
        ('Add numbers.', 'First.')
    """
    if not doc:
        return "", {}

    text = inspect.cleandoc(doc)
    match = _ARGS_SECTION_RE.search(text)
    summary = (text[: match.start()] if match else text).strip()
    summary = " ".join(summary.split("\n\n")[0].split())

    if not match:
        return summary, {}

    descriptions: dict[str, str] = {}
    current: str | None = None
    for line in text[match.end() :].splitlines():
        if not line.strip():
            continue
        if _SECTION_RE.match(line):
            break
        arg = _ARG_RE.match(line)
        if arg and not line.startswith(" " * 9):
            current = arg.group(1).lstrip("*")
            descriptions[current] = arg.group(2).strip()
        elif current:
            descriptions[current] = f"{descriptions[current]} {line.strip()}".strip()
    return summary, descriptions

python_type_to_schema

python_type_to_schema(annotation: Any) -> dict[str, Any]

Convert a Python type annotation into a JSON Schema fragment.

Parameters:

Name Type Description Default
annotation Any

The annotation to convert. inspect.Parameter.empty and bare Any produce a permissive {}.

required

Returns:

Type Description
dict[str, Any]

A JSON Schema fragment.

Example

python_type_to_schema(list[str]) {'type': 'array', 'items': {'type': 'string'}} python_type_to_schema(int | None)

Source code in src\windlass\tools\schema.py
def python_type_to_schema(annotation: Any) -> dict[str, Any]:
    """Convert a Python type annotation into a JSON Schema fragment.

    Args:
        annotation: The annotation to convert. ``inspect.Parameter.empty`` and
            bare ``Any`` produce a permissive ``{}``.

    Returns:
        A JSON Schema fragment.

    Example:
        >>> python_type_to_schema(list[str])
        {'type': 'array', 'items': {'type': 'string'}}
        >>> python_type_to_schema(int | None)
        {'type': 'integer'}
    """
    if annotation is inspect.Parameter.empty or annotation is Any:
        return {}

    # Annotated[T, "description", ...] -> unwrap, keeping any string metadata.
    if get_origin(annotation) is typing.Annotated:
        base, *metadata = get_args(annotation)
        schema: dict[str, Any] = python_type_to_schema(base)
        for item in metadata:
            if isinstance(item, str):
                schema.setdefault("description", item)
            elif isinstance(item, dict):
                schema.update(item)
        return schema

    if annotation in _SCALARS:
        return {"type": _SCALARS[annotation]}

    origin = get_origin(annotation)

    if origin is Literal:
        options = list(get_args(annotation))
        kinds = {type(o) for o in options}
        schema = {"enum": options}
        if len(kinds) == 1:
            only = kinds.pop()
            if only in _SCALARS:
                schema["type"] = _SCALARS[only]
        return schema

    if origin in (Union, types.UnionType):
        members = [a for a in get_args(annotation) if a is not type(None)]
        if len(members) == 1:
            return python_type_to_schema(members[0])
        variants = [python_type_to_schema(m) for m in members]
        return {"anyOf": [v for v in variants if v]}

    if origin in (list, set, frozenset):
        args = get_args(annotation)
        items = python_type_to_schema(args[0]) if args else {}
        schema = {"type": "array"}
        if items:
            schema["items"] = items
        if origin in (set, frozenset):
            schema["uniqueItems"] = True
        return schema

    if origin is tuple:
        args = get_args(annotation)
        if args and args[-1] is Ellipsis:
            return {"type": "array", "items": python_type_to_schema(args[0])}
        return {
            "type": "array",
            "prefixItems": [python_type_to_schema(a) for a in args],
            "minItems": len(args),
            "maxItems": len(args),
        }

    if origin is dict:
        args = get_args(annotation)
        schema = {"type": "object"}
        if len(args) == 2:
            values = python_type_to_schema(args[1])
            if values:
                schema["additionalProperties"] = values
        return schema

    if isinstance(annotation, type):
        if issubclass(annotation, enum.Enum):
            members = [member.value for member in annotation]
            schema = {"enum": members}
            kinds = {type(v) for v in members}
            if len(kinds) == 1 and kinds.copy().pop() in _SCALARS:
                schema["type"] = _SCALARS[kinds.pop()]
            return schema

        if _is_pydantic_model(annotation):
            # Duck-typed rather than importing pydantic: this module must stay
            # usable for tools whose annotations are plain classes.
            return _clean_model_schema(annotation.model_json_schema())  # type: ignore[attr-defined]

        if dataclasses.is_dataclass(annotation):
            return _dataclass_schema(annotation)

        if annotation in (list, dict, set, tuple):
            return {
                "type": {"list": "array", "set": "array", "tuple": "array"}.get(
                    annotation.__name__, "object"
                )
            }

    # Unknown annotation: stay permissive rather than emitting a wrong schema.
    return {}

tool

tool(fn: _F) -> FunctionTool
tool(
    *,
    name: str | None = ...,
    description: str | None = ...,
    parameters: dict[str, Any] | None = ...,
    timeout: float | None = ...,
    requires_approval: bool = ...,
    register: bool = ...
) -> Callable[[_F], FunctionTool]
tool(
    fn: _F | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    register: bool = False
) -> Any

Turn a Python function into an agent-callable tool.

Works bare (@tool) or with arguments (@tool(timeout=5)).

Parameters:

Name Type Description Default
fn _F | None

The function, when used bare.

None
name str | None

Tool name shown to the model. Defaults to the function name.

None
description str | None

Model-facing description. Defaults to the docstring summary. Write a good one — it is how the model decides when to call this.

None
parameters dict[str, Any] | None

JSON Schema override. Derived from type hints by default.

None
timeout float | None

Seconds before the call is abandoned.

None
requires_approval bool

Pause the agent for human approval before running. Use this for anything that spends money or changes state.

False
register bool

Also add the tool to the global registry under name, so it can be referenced as a string.

False

Returns:

Name Type Description
A Any

class:FunctionTool, or a decorator producing one.

Raises:

Type Description
ValidationError

If the resulting name is not provider-legal.

Note

The docstring is not decoration. Its summary becomes the model-facing description, and its Google-style argument section becomes the per-parameter descriptions in the JSON schema. Both are prompt text — the model decides when to call the tool, and what to pass, from them.

Example

Bare::

@tool
def search(query: str) -> list[str]:
    # Docstring: "Search the product catalogue." plus an argument
    # section describing `query`.
    return db.search(query)

Configured::

@tool(name="charge_card", requires_approval=True, timeout=30)
async def charge(amount_cents: int, customer_id: str) -> dict:
    # Approval-gated: the agent pauses before this ever runs.
    return await payments.charge(customer_id, amount_cents)
Source code in src\windlass\tools\__init__.py
def tool(
    fn: _F | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    parameters: dict[str, Any] | None = None,
    timeout: float | None = None,
    requires_approval: bool = False,
    register: bool = False,
) -> Any:
    """Turn a Python function into an agent-callable tool.

    Works bare (``@tool``) or with arguments (``@tool(timeout=5)``).

    Args:
        fn: The function, when used bare.
        name: Tool name shown to the model. Defaults to the function name.
        description: Model-facing description. Defaults to the docstring summary.
            **Write a good one** — it is how the model decides when to call this.
        parameters: JSON Schema override. Derived from type hints by default.
        timeout: Seconds before the call is abandoned.
        requires_approval: Pause the agent for human approval before running.
            Use this for anything that spends money or changes state.
        register: Also add the tool to the global registry under ``name``, so it
            can be referenced as a string.

    Returns:
        A :class:`FunctionTool`, or a decorator producing one.

    Raises:
        ValidationError: If the resulting name is not provider-legal.

    Note:
        The docstring is not decoration. Its summary becomes the model-facing
        description, and its Google-style argument section becomes the
        per-parameter descriptions in the JSON schema. Both are prompt text — the
        model decides *when* to call the tool, and *what to pass*, from them.

    Example:
        Bare::

            @tool
            def search(query: str) -> list[str]:
                # Docstring: "Search the product catalogue." plus an argument
                # section describing `query`.
                return db.search(query)

        Configured::

            @tool(name="charge_card", requires_approval=True, timeout=30)
            async def charge(amount_cents: int, customer_id: str) -> dict:
                # Approval-gated: the agent pauses before this ever runs.
                return await payments.charge(customer_id, amount_cents)
    """

    def wrap(target: _F) -> FunctionTool:
        built = FunctionTool(
            target,
            name=name,
            description=description,
            parameters=parameters,
            timeout=timeout,
            requires_approval=requires_approval,
        )
        if register:
            REGISTRY.register(
                "tool",
                built.name,
                lambda **_: built,
                description=built.description,
                origin="user",
                override=True,
            )
        return built

    if fn is not None:
        return wrap(fn)
    return wrap