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. An extension whose branches can pause — a branch that dispatches a pausing tool and passes its park through — registers with pauses=True:
A branch can pause when the tool it wraps declares tai42/pauses or when any extension in its stack registered with pauses=True; any other branch is plain, and a park it returns is refused like any undeclared tool’s.

Settings

An extension that takes settings declares their JSON Schema (draft 2020-12, root "type": "object") with config_schema= when it registers, and its factory takes a config parameter. A factory that takes config must declare a schema and one without config must not; registration refuses either mismatch and a schema that breaks the JSON Schema rules. An extension without a schema accepts no settings.
A combo element binds the settings as {"name": "label", "config": {"label": "nightly"}}. The server checks them against the declared schema before any write is stored and at every bind. Every manifest or preset write also binds each combo it names once before it is stored, and discards that branch, so a value the factory refuses is refused at the door that wrote it, naming the extension and the tool. A factory is therefore called again for a combo it already bound: whatever it does beyond building its branch must be repeatable. The extension catalog serves each schema, and the Studio draws the settings form from it; a setting declared as a $ref to the draft 2020-12 meta-schema is itself a JSON Schema and gets the schema editor. A setting stored as an !ENV marker is served by the tool’s extension reads as the marker, never its value, and the Studio shows it sealed, by its variable name.

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.