跳转至

扩展快速开发

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. 发布前自查

  1. 声明了 api_min_version(用了较新 API 时)。
  2. setup_extension 幂等,昂贵资源缓存在模块级。
  3. 数据落在 ctx.data_dir(),配置读自 ctx.settings,没有写死绝对路径。
  4. 出错路径有明确异常信息,能被 /extensions 的 reason 解释清楚。
  5. 提供了 README.md:用途、配置示例、依赖、最小用法。

相关文档