扩展快速开发¶
1. 建文件¶
单文件扩展就一个 Python 文件:
mkdir -p .xun/extensions
from xun import ExtensionContext, extension_attr
@extension_attr(api_min_version="1.4.0")
def setup_extension(ctx: ExtensionContext) -> None:
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
ctx.agent.toolbox.register(greet)
xun
/tools # 应能看到 greet
再进一步,加一条会话命令:
from xun import Command, ExtensionContext, extension_attr
@extension_attr(api_min_version="1.4.0")
def setup_extension(ctx: ExtensionContext) -> None:
def hello(agent, args: list[str]) -> None:
"""Say hello to the given name."""
agent.info(f"Hello, {' '.join(args) or 'world'}!")
ctx.agent.command.register(Command.from_function(hello))
/hello Xun
2. 排查¶
| 症状 | 先看哪里 |
|---|---|
| 命令 / 工具没出现 | /extensions 看状态:没有这一行说明没被发现(目录、文件名、setup_extension 是否存在) |
状态 skipped |
版本门没过,reason 会写明要求的版本区间 |
状态 failed |
导入或 setup 抛了异常,终端有 extension '<名字>' setup failed 告警 |
| 改了代码不生效 | 扫描按目录缓存,重启进程或 clear_scan_cache() |
| 子智能体里少了一半功能 | 多半是工具重名冲突中止了后续设置,把注册改成幂等即可 |
扩展导入失败不会阻断启动,因此现场常常只剩一条被刷屏的告警——先跑 /extensions 最省事。
3. 能在扩展里做什么¶
ExtensionContext 提供的内容不多,真正的能力都在 ctx.agent 上:
| 想做的事 | 做法 |
|---|---|
| 加外部能力 | ctx.agent.toolbox.register(fn) 注册普通函数;动态来源(比如 MCP)用 register_raw(Function(...)) 现造 schema |
| 关掉某个工具 | ctx.agent.toolbox.pop(name) / disable("fs_*") |
| 加会话命令 | ctx.agent.command.register(Command(name=..., description=..., handler=...)) |
| 观测 / 干预执行 | ctx.agent.hooks.<钩子>.add(fn),见 Hooks |
| 给系统提示加一段 | ctx.agent.conversation.set_system_persistent_section("skills", "...")(同名覆盖,压缩时受保护) |
| 存扩展自己的状态 | ctx.agent.state["my_flag"] = JsonEntry(...) 或 RuntimeEntry(...) |
| 往界面推内容 | ctx.agent.display_event(HTMLInfoEvent(html=...)),见 显示扩展 |
| 读扩展配置 | ctx.settings,即 config.extension_settings[<扩展名>] |
| 读写扩展数据 | ctx.data_dir() 返回私有目录,已自动按 data_v_<版本> 分层 |
setup 会被重放
每个智能体(包括派生的子智能体)都会跑一遍 setup_extension。
昂贵或只该做一次的事(连服务端、建库、建目录)请缓存在模块级变量上,
注册代码要能容忍重复执行。
4. 从内置扩展学结构¶
extensions/ 下三个扩展是三种典型形状:
| 扩展 | 规模 | 演示的要点 |
|---|---|---|
stat |
~1000 行 | 四个钩子写 SQLite、/stat 查询、xune stat 管预算、data_version 与 cli 一起用 |
mcp |
~1000 行 | 同步 API 上架异步协议:后台线程跑 asyncio;register_raw + pop + is_disabled 做连接级可见性 |
skills |
~750 行 | 系统提示的持久段落 + 渐进披露:先只列名字,用到才读全文;工具与命令双入口 |
stat 的骨架最能说明「扩展 = 挂钩子 + 挂命令」:
@extension_attr(api_min_version="1.4.0", data_version=DATA_VERSION, cli=(build_parser(), cli.run))
def setup_extension(ctx: ExtensionContext) -> None:
db_path = init_db(ctx.data_dir() / "stat.db") # 建库放这儿,不在 import 期
def log_tool_calls(args: HookArgs.BeforeToolCallArgs) -> None: ...
def log_token_usage(args: HookArgs.TokenMetrics) -> None: ...
def check_budget(args: HookArgs.BeforeExecutionArgs) -> None: ...
def stat_command(agent, args: list[str]) -> None: ...
ctx.agent.hooks.before_tool_call.add(log_tool_calls)
ctx.agent.hooks.completion_token_update.add(log_token_usage)
ctx.agent.hooks.before_execution.add(check_budget)
ctx.agent.command.register(Command.from_function(stat_command))
预算超限时 check_budget 直接抛异常——这就是「扩展拦下一轮执行」的标准写法
(普通异常会被钩子吞掉,想拦住执行要抛 CancelledError,见 Hooks)。
5. 发布前自查¶
- 声明了
api_min_version(用了较新 API 时)。 setup_extension幂等,昂贵资源缓存在模块级。- 数据落在
ctx.data_dir(),配置读自ctx.settings,没有写死绝对路径。 - 出错路径有明确异常信息,能被
/extensions的reason解释清楚。 - 提供了
README.md:用途、配置示例、依赖、最小用法。
相关文档¶
- 加载与失败的精确语义:机制与加载
- 装饰器字段逐项说明:Extension Attributes