Skip to main content
A tool is the atom of work: a plain Python function you decorate, a tool from a mounted MCP server, or a management operation the runtime projects. An extension is a clip-on power that wraps or transforms a tool into a new variant. Everything else the platform does is built around these two.

Tools

You register a function as a tool with one decorator on the shared tai42_app handle. The function’s signature becomes the tool’s input schema, and its docstring becomes the tool’s description:
The tool registry keys each requested tool by its base name and maps it to its extension combos. The dispatch adapters turn a vendor tool — an MCP tool or a langchain tool — into a plain callable, so a tool from a mounted MCP server behaves like a local one. A tool becomes live because the manifest names its module. A third source registers through the same binding: the management operations — reload a tool, replace the manifest, re-probe a failed MCP server. Each operation projects as a first-class tool (unless api_tools curates it out), so an extension wraps it, a preset bakes over it, and tai tools run or an agent dispatches it exactly as with any other tool. A tool can declare a registration tier (read / write / fenced / secret); a fenced or secret tool is admin-only to author a preset over and to run, enforced at the shared tool-run chokepoint on every door — see fenced tools.

Retrying a tool call

A tool declares its own retry policy, and the host dispatch seam honours it. With no declaration a dispatch is one attempt, exactly — retry is opt-in per tool, never a platform-wide default.
max_attempts is the total attempt budget with the first attempt included, so 1 means no retry. idempotent is the author’s explicit claim that re-firing the body is safe: it is required, and a retrying policy without it is rejected — the double-send guard. retryable is the classification door, an explicit allowlist rather than a blanket. True retries the default transient kinds, a tuple of ErrorKind retries exactly those kinds, and False turns kind-based retry off. In every mode an error that carries its own boolean retryable verdict wins in both directions — it admits a failure that is off the list and vetoes one that is on it, because the raiser knows its own failure better than any kind bucket. An error with neither a verdict nor an allowlisted kind is never retried. Backoff is exponential: attempt n waits min(cap_seconds, initial_seconds * multiplier ** (n - 1)) before the next one. A server that answers with its own retry_after widens that wait when it asks for longer — the cap bounds the platform’s own growth, not the server’s explicit request.
A policy is declared on the registered base tool name. A preset runs the base tool’s body with baked constants, so an undeclared preset inherits the first declared ancestor’s policy — the idempotency claim travels with the body. An extension branch inherits nothing: its stack can compose or relocate execution the base never spoke for, so only an exact-name declaration arms it.
Every attempt is visible in observability rather than collapsed into one result, and the whole dispatch stays a single runs-index row whatever the attempt count. See the Python SDK reference for ToolRetryPolicy, ToolRetryBackoff, and the retryable-kind constants.

Extensions

An extension is a plain callable registered against an ExtensionKind and applied to a tool. There are three kinds:
  • WRAPPER — wraps the call around the tool without changing its input schema (for example, tracing or caching the call).
  • TRANSFORMER — transforms the tool into a new shape, changing what inputs it presents.
  • BACKEND — routes the tool’s execution through a backend.
The extension registry holds the registered extensions and enforces each kind’s cardinality rule. Applying an extension branches the base tool into a new named variant rather than mutating it: attaching monitor to mytool binds a mytool_monitor branch, leaving mytool itself untouched.

Clipping an extension on

Extensions are attached in the manifest, separately from tool selection. A tool’s extensions map names the branch to build:
A value may be one combo ([ext]) or a list of combos ([[ext], [ext, other]]), and a stacked combo ([[a, b]]) layers both extensions onto one branch. The extension’s module must also be loaded — through extensions_modules — before you can attach it.

Built-in and contrib extensions

The skeleton ships two built-in extensions that depend on skeleton features: monitor (a WRAPPER that traces a standalone tool call as one live span) and ask_external (a TRANSFORMER that turns a callback-URL tool into an external interaction). Generic, self-contained extensions — chain, batch, cache, and more — live in the tai42-toolbox contrib package and load the same way. The standard toolbox guide lists them.
An extension extends a single tool. A plugin — a connector, storage backend, or config provider — extends the whole platform. The two are different things.
See the Python SDK reference for the ExtensionKind enum and the tool registry surface, and the guides on building a tool and applying an extension.