Extension Attributes¶
@extension_attr declares an extension's metadata on its entry function; the framework uses it for version
gating, data-directory versioning and CLI registration.
from xun import ExtensionContext, extension_attr
@extension_attr(
api_min_version="1.4.0", # lowest xun version required
api_max_version="1.9.9", # highest xun version required (inclusive)
data_version="2", # data layout version
cli=(build_parser(), run), # parser and handler handed to `xune <name>`
)
def setup_extension(ctx: ExtensionContext) -> None: ...
All four parameters are optional; the decorator returns the original function and attaches the metadata to the
function attribute __xun_extension_attr.
Version gate¶
| Parameter | Semantics |
|---|---|
api_min_version |
Current version below it → skipped |
api_max_version |
Current version above it → skipped |
Comparison looks only at the numeric core: 1.2rc3 equals 1.2, 1.4 equals 1.4.0 (shorter versions are
zero-padded), and suffixes (dev, rc, post) take no part. Both ends of the range are inclusive.
When the gate blocks an extension the status is skipped, not failed, and reason looks like
requires xun >= 1.4.0, running 1.3.0, visible in /extensions.
No version check when running from source
xun_version() reads package metadata (importlib.metadata.version("xun-agent")) and returns None when xun
runs straight from source (no installed distribution), in which case the version check passes outright.
So local development never skips an extension over its version declaration; the gate only bites once a package is installed.
data_version¶
data_version is not a version of your data contents but a version of the directory layout: once declared, the
data directory gains one more layer, data_v_<version>.
| Declaration | Actual directory |
|---|---|
| Not declared | $XUN_HOME/extension_data/<extension>/ |
data_version="2" |
$XUN_HOME/extension_data/<extension>/data_v_2/ |
- The value must be a single safe path segment:
\w[\w.+-]*, with no leading dot and no path separator, otherwise constructingExtensionAttrfails immediately. - Raising the version means starting from scratch: the old directory is neither deleted nor migrated — relocating data is on you.
extension_data_dir(name, data_version)is the public form of the same rule, so external tools (thexuneCLI, for example) can locate the data directory without entering an extension context.
cli¶
cli=(parser, handler): parser is an argparse.ArgumentParser, handler receives the parsed Namespace, and an
int return value becomes the process exit code. It runs through xune <name>; see
Extension CLI entry xune.
ExtensionContext¶
setup_extension(ctx) receives this single object, and it is new on every initialize():
| Member | Description |
|---|---|
ctx.agent |
The not-yet-initialized Agent; every effect an extension has is applied here |
ctx.name |
Extension name, also the key used in extension_settings and for the data directory |
ctx.settings |
agent.config.extension_settings[ctx.name], {} when unset; values already have environment variables substituted |
ctx.data_dir(_create=True) |
Private data directory (layered by data_version), created by default; _create=False only resolves the path without touching disk (for tests) |
{
"extension_settings": {
"my-extension": {
"endpoint": "${XUN_MY_ENDPOINT}",
"verbose": true
}
}
}
def setup_extension(ctx: ExtensionContext) -> None:
endpoint = ctx.settings.get("endpoint", "")
cache = ctx.data_dir() / "cache.json"
Status overview¶
/extensions (and ShowExtensionsEvent on the web) lists one row per extension:
| Field | Source |
|---|---|
| Name | File name or directory name |
| Description | Docstring of the entry function |
| Path | Source file path |
| Status | uninitialized / loaded / skipped / failed |
| Reason | Explanation for skipped and failed |
The same data in-process: ExtensionLoader.infos(); only the successfully imported ones: imported().
Declarations of the three built-in extensions¶
| Extension | Declaration |
|---|---|
mcp |
api_min_version="1.3.0" |
skills |
api_min_version="1.4.0" |
stat |
api_min_version="1.4.0", data_version=DATA_VERSION, cli=(build_parser(), cli.run) |
Copying the closest one and adapting it is usually faster than writing from scratch.