> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Author an extension

> Build a wrapper or transformer.

Author an extension plugin — a wrapper or transformer over a tool.

An extension is registered as a factory: the platform hands it a tool's callable, its current name, and its description, and the factory returns a new-named callable bound as a branch tool alongside the original. You declare the extension's kind, which fixes the schema rules the platform enforces.

## Choose a kind

`ExtensionKind` fixes what your branch's input schema may be:

* **`WRAPPER`** — presents the wrapped tool's input schema unchanged, apart from its own declared control kwargs. Stackable. Use it to add a cross-cutting behaviour (caching, metering, routing) without changing the tool's inputs.
* **`TRANSFORMER`** — presents its own composed input schema, built with `makefun.create_function`. Stackable, applied left to right. Use it when the branch takes different inputs (batching a tool over a list, chaining two tools).
* **`BACKEND`** — one strategy per tool; no input-schema rule.

## Register through the handle

Decorate a factory with `@tai42_app.extensions.extension`, naming the kind and the extension name. The factory returns a new callable named `<tool>_<extension>`.

```python examples/extension/retry_wrapper.py theme={null}
"""A minimal fictional extension: a ``retry`` wrapper.

A WRAPPER presents the wrapped tool's input schema unchanged, apart from its own
declared control kwargs. This one branches any tool into a ``<tool>_retry``
variant that re-invokes the tool a few times before giving up, and adds one
control kwarg — ``attempts`` — declared as a reserved param so the apply site
still treats the branch as schema-preserving.
"""

import inspect

from makefun import create_function
from tai42_contract.app import tai42_app
from tai42_contract.extensions import ExtensionKind


@tai42_app.extensions.extension(kind=ExtensionKind.WRAPPER, name="retry")
def retry(func, name, description):
    """Branch ``func`` into a re-invoking ``<name>_retry`` variant."""

    async def wrapper(*args, **kwargs):
        attempts = kwargs.pop("attempts", 3)
        last_error: Exception | None = None
        for _ in range(max(1, attempts)):
            try:
                if inspect.iscoroutinefunction(func):
                    return await func(*args, **kwargs)
                return func(*args, **kwargs)
            except Exception as error:
                last_error = error
        raise last_error if last_error is not None else RuntimeError("retry exhausted")

    # Present the wrapped tool's signature plus the reserved ``attempts`` kwarg,
    # inserted before any ``**kwargs`` so the synthesized signature stays valid.
    signature = inspect.signature(func)
    params = list(signature.parameters.values())
    attempts_param = inspect.Parameter("attempts", inspect.Parameter.KEYWORD_ONLY, default=3, annotation=int)
    insert_at = next(
        (i for i, p in enumerate(params) if p.kind is inspect.Parameter.VAR_KEYWORD),
        len(params),
    )
    params.insert(insert_at, attempts_param)

    new_name = f"{name}_{retry.__name__}"
    return create_function(
        func_signature=signature.replace(parameters=params),
        func_impl=wrapper,
        func_name=new_name,
        qualname=new_name,
        doc=description,
    )


# The apply site subtracts these reserved control kwargs from the branch's input
# schema before checking that a WRAPPER preserves the wrapped tool's schema.
retry.reserved_params = frozenset({"attempts"})
```

## Declare a wrapper's control kwargs

A wrapper may add control kwargs for itself. Declare them as a `reserved_params` frozenset on the factory; the apply site subtracts each name from the branch's derived schema before checking that the wrapper preserved the wrapped tool's schema. The `retry` wrapper above adds one `attempts` control kwarg — its `reserved_params` names the kwarg the apply site excludes before the schema-preservation check.

A reserved param's type must be schema-inline (a primitive or a plain container of primitives), and a reserved name that also appears on the wrapped tool's signature is a hard error at apply time.

## Load it

The host enables an extension module under `extensions_modules`, then a tool attaches it through its `extensions` map. See [Apply an extension](/guides/apply-an-extension).

```yaml manifest.yml theme={null}
extensions_modules:
  - your_package.extensions.cache
```

## Shipped implementations

* [`tai42-toolbox`](https://github.com/tai42ai/tai-toolbox) — `cache`, `proxy`, and `prometheus_metrics` (wrappers); `batch`, `chain`, and `output_schema` (transformers).

## See also

* [Tools and extensions](/concepts/tools-and-extensions) — the extension model and stacking rules.
* [Apply an extension](/guides/apply-an-extension) — attach one to a tool.
* [Python SDK reference](/reference/python-sdk) — `ExtensionKind` and the `tai42_app.extensions` surface.
* [Ecosystem catalog](/reference/catalog) — every shipped extension.
