Skip to content

agentkit.tools

The Tool Protocol, FunctionTool, the @tool decorator, the ToolRegistry, and the as_tool adapter that turns an Agent into a tool callable by another agent.

Tool Protocol stack — peer of agentkit.state.memory and agentkit.agents.skills.

The Tool Protocol is the Command-pattern surface every action an agent can request must satisfy. FunctionTool is the canonical impl wrapping a Python callable, with the @tool decorator as the ergonomic front door. ToolRegistry is the Composite holding many tools behind a single lookup-by-name surface.

Tool

Bases: Protocol

An action an agent can request — Command pattern done as a Protocol.

Any object satisfying this shape IS a Tool: it carries a stable name + description, a JSON schema describing its input args, an optional output_schema for return-value validation, flags for side_effecting and requires_approval so the runtime can make dispatch decisions, and an async run(args, ctx) that actually executes.

FunctionTool is the canonical impl wrapping a Python callable. Others may implement directly — remote-procedure tools, MCP tools, skill-as-tool adapters. The framework dispatches tools through the middleware chain (audit / approval-gate / output-coerce / retry) regardless of which concrete impl backs them.

schema may be None (loop-invisible tool advertising no schema) or a ToolSchema; output_schema accepts the same shapes FunctionTool.output_schema does — a JSON-Schema dict, a Pydantic / dataclass / attrs class, or None (no check). run.args is Mapping[str, Any], and the widest type is deliberate: a plain dict, the FrozenDict the runtime normally hands down (ToolRequest.arguments — a dict SUBCLASS that refuses mutation, see kernel._frozen), and any other mapping a caller supplies all satisfy it. An impl READING args needs to know only that it is a mapping. An impl WRITING to it is the case the freeze exists to stop: measured, a Tool.run reached through the Invoker receives a FrozenDict and args["x"] = 1 inside the tool raises TypeError. Copy first if you need to edit — dict(args) — and note that copy is SHALLOW, so nested values stay frozen.

ToolArgumentError

ToolArgumentError(
    tool_name: str,
    *,
    unexpected: tuple[str, ...] = (),
    missing: tuple[str, ...] = (),
    accepted: tuple[str, ...] = (),
    invalid: tuple[tuple[str, str], ...] = (),
)

Bases: ValueError

A tool CALL named arguments the tool does not accept, or omitted required ones.

The mirror of :class:ToolShapeError: that one is a bad value coming OUT of a tool, this one is a bad call going IN. Both are raised at call time and both are meant to be reflected back to the model, which authored the call and is the only party that can fix it.

Why this is an error rather than a shrug: unknown keys used to be dropped silently, and a parameter with a DEFAULT then ran with that default. A model calling notify(message="page the on-call, prod is down") against def notify(msg: str = "default message") got back "sent: default message" — a side-effecting tool reporting success for something it never did. Nothing downstream could tell.

The message names the tool, the offending arguments and the accepted set, so the retry middleware can hand the model something it can act on. A tool that genuinely wants arbitrary keys declares **kwargs and receives them.

Subclasses ValueError: the failure mode is a bad value, and existing except ValueError isolation around tool calls keeps working.

invalid is the third flavour of bad call, and it is the same kind of mistake as the other two: the argument NAME was right, the VALUE was not one the advertised schema permits. unit="kelvin" against {"enum": ["celsius", "fahrenheit"]} used to be neither reported nor converted — the string went straight into a body annotated Unit. It belongs on this type rather than a new one precisely because the recovery is identical: reflect the message, let the model reissue the call. Each entry is (argument name, why it was rejected), and the reason carries the legal values so the repair turn does not have to guess.

ToolDefinitionError

Bases: ValueError

Raised at decoration/registration time when a tool fails the framework's wiring contract: missing/thin docstring, missing side_effecting= declaration, or other static defects the framework can catch before the agent ever runs. Subclasses ValueError so existing except ValueError paths still trip — the failure mode is genuinely a bad value.

ToolShapeError

ToolShapeError(
    tool_name: str,
    expected: str,
    raw: Any,
    errors: list[str] | None = None,
)

Bases: Exception

A tool's result didn't match its declared output_schema.

Raised by :meth:FunctionTool.run AFTER the function executed successfully but the result failed schema validation. The retry middleware catches this and reflects the error back to the model as a tool-call failure — the model sees a structured "tool returned a value that doesn't match its declared output" message and can re-issue the call (often with different args) or pivot.

Distinct from :class:OutputCoercionError (which is about MODEL response coercion); tool shape mismatches have a different fire site (after the tool function ran) and a different recovery shape on the model side (the model didn't author the bad value — the tool did).

FunctionTool dataclass

FunctionTool(
    name: str,
    fn: Callable[[Any, Any], Any],
    description: str,
    side_effecting: bool,
    schema: Any = None,
    idempotent: bool = False,
    requires_approval: bool = False,
    caps: tuple[str, ...] = (),
    url_arg: str | None = None,
    output_schema: Any = None,
)

output_schema class-attribute instance-attribute

output_schema: Any = None

Optional schema the tool's RESULT must match. Pydantic BaseModel / dataclass / attrs class / raw JSON Schema dict — same accepted shapes as Agent.output=. When set, the tool's return value is validated through adapt(output_schema).validate(result) before the framework hands it back to the model.

A mismatch drops a tool.shape_mismatch span event on the open execute_tool span and raises :class:ToolShapeError — catchable by the retry middleware so the model can see the structured error and recover (re-issue the call with different args or pivot).

None (default) means NO check — fast path. Tools that already return well-typed Python objects get zero overhead. from_callable auto-sets this from the function's return-type annotation when the annotation is a Pydantic / dataclass / attrs class; the caller can override (or opt out via output_schema=None) on the @tool decorator.

run async

run(args: Any, ctx: Ctx) -> Any

Always-async run — bridges a sync fn off the event loop so a blocking tool never stalls it.

When an output_schema is wired on this tool, the result is run through adapter.validate(result) AFTER the function returns. A schema mismatch drops a tool.shape_mismatch event on the currently-open span (the execute_tool span the tracing middleware just opened) and raises :class:ToolShapeError so the retry middleware can reflect the error back to the model.

Fast path: no adapter wired → one if branch, zero overhead.

from_callable classmethod

from_callable(
    func: Callable[..., Any],
    *,
    name: str | None = None,
    description: str | None = None,
    side_effecting: bool = False,
    idempotent: bool = False,
    requires_approval: bool = False,
    caps: tuple[str, ...] = (),
    url_arg: str | None = None,
    output_schema: Any = _OUTPUT_AUTO,
) -> FunctionTool

Turn a plain Python function into a tool: inspect its signature + type hints → a JSON-schema ToolSchema, and wrap execution (sync → off the loop via to_thread, async → awaited). The model calls it by name with JSON args; a parameter named ctx/context is injected with the RunContext (and not advertised). The function's return value is the tool result; exceptions propagate to the framework's per-tool isolation + typed Failure — no result is silently swallowed.

The function MUST carry a docstring (or be passed an explicit description=) of at least _MIN_DESCRIPTION_LEN chars — otherwise raises ToolDefinitionError at registration time. side_effecting defaults to False here for compatibility with the @tool decorator which enforces explicit declaration; direct callers should always pass it knowingly.

output_schema controls tool-result validation: - default (_OUTPUT_AUTO sentinel): auto-infer from the function's return-type annotation. A Pydantic BaseModel / dataclass / attrs class return type triggers validation on every call; everything else (primitives, Any, generics) gets no check. - explicit class / dict: use this as the output schema (overrides auto-inference). - explicit None: OPT OUT of validation even if the return type looks enforceable.

FileTool

FileTool(backend: Any = None, *, root: str = '/memories')

The memory(command=…) tool. Declares description and output_schema so isinstance(FileTool(), Tool) holds under the @runtime_checkable :class:Tool Protocol.

The class is FileTool and the tool is memory on purpose: the class name is for the reader (it is a file-tree tool, and it sits next to agentkit.state.memory, which is a different thing), while the tool name is for the MODEL and is fixed by Anthropic's memory-tool contract. The module docstring has the full argument for why that asymmetry is the right one and why renaming the tool is not a tidy-up.

InMemoryFiles

InMemoryFiles(root: str = '/memories')

A minimal in-memory file tree (path -> text). The backend protocol is async so a real durable/filesystem backend does its blocking I/O off the loop (via to_thread) without stalling it — in-memory ops are instant but stay async def to keep the seam uniform (async-first).

create async

create(
    path: str, file_text: str, *, overwrite: bool = False
) -> str

Create a file. Refuses to silently clobber an existing path — pass overwrite=True to replace deliberately. An unconditional write would actively lie about what happened when the path already held a different note from a prior run. Pairs with rename's no-clobber semantics so all destructive writes are explicit.

StoreFiles

StoreFiles(
    store: StorePort,
    *,
    namespace: str,
    root: str = "/memories",
    max_bytes: int = 8388608,
    max_files: int = 4096,
)

A FileTool/FileMemory backend with atomic tree mutations.

The host supplies a fixed tenant namespace; model paths never select another namespace or become operating-system paths. Each tree is one JSON document updated with the store's compare-and-set primitive, including rename and recursive delete. Durability and cross-process atomicity follow the supplied store: FileStore is process-local; Redis/Postgres provide their respective distributed guarantees. Concurrent FileStore users must share the same FileStore instance.

ToolRegistry

ToolRegistry()

register

register(tool: Any, *, replace: bool = False) -> Tool

Register a FunctionTool — or a plain callable, auto-converted via from_callable. A plain callable goes through the same tool-writing contract as @tool (docstring floor, explicit side_effecting if you want to deviate from the read-only default); a callable whose docstring is too thin surfaces a ToolDefinitionError here, not at runtime.

Raises :class:ValueError on name collision unless replace=True. The model advertises tools by name; a silent overwrite would change the implementation under the agent without any signal.

from_tools classmethod

from_tools(items: Iterable[Any]) -> ToolRegistry

Build a registry from a list mixing FunctionTools and plain functions ([get_weather, …]). Each plain callable is validated by FunctionTool.from_callable; a thin docstring or other contract violation raises ToolDefinitionError here instead of silently wrapping a bad tool.

Name collisions across the list propagate the same :class:ValueError as register.

schemas

schemas() -> list[Any]

Advertised tools, stable order (so a cacheable system+tools prefix stays byte-identical).

as_tool

as_tool(
    runnable: Any,
    *,
    name: str,
    description: str = "",
    side_effecting: bool = False,
    requires_approval: bool = False,
    render: Callable[[Any], str] | None = None,
) -> FunctionTool

Wrap any runnable (leaf Agent / coordinator Agent / Workflow — anything with async run(task, ctx) -> result) as a FunctionTool an Agent can call. The sub-run executes on a ctx.child() (so depth/budget/cancellation/observation all flow), and its result is rendered to text for the loop. This is the explicit/coordinator-inside-emergent composition.

render_result

render_result(res: Any) -> str

Render any run result to text for re-entry into a loop: AgentResult.output or a WorkflowResult's terminal outputs.

tool

tool(
    func: Callable[..., Any] | None = None,
    *,
    side_effecting: Any = _REQUIRED,
    idempotent: bool = False,
    name: str | None = None,
    description: str | None = None,
    requires_approval: bool = False,
    caps: tuple[str, ...] = (),
    url_arg: str | None = None,
    output_schema: Any = _OUTPUT_AUTO,
) -> Any

Decorator / converter: @tool(side_effecting=False), @tool(side_effecting=True, idempotent=False), or tool(fn, side_effecting=...) → a FunctionTool. side_effecting= is REQUIRED — the framework's gating + idempotency primitives rely on knowing whether a tool mutates the world. The decorated callable MUST carry a docstring of >=30 chars (the model needs enough to understand the tool); both failure modes raise ToolDefinitionError at decoration time (not at call time).

output_schema= is opt-in tool-result schema validation. By default (the _OUTPUT_AUTO sentinel) it auto-infers from the function's return-type annotation — a Pydantic / dataclass / attrs class triggers validation on every tool result; primitives and generics are skipped. Pass an explicit class to override; pass output_schema=None to opt out entirely even when the return type looks enforceable.