扩展系统¶
把 Python 文件放进 $XUN_HOME/extensions/,每个智能体在 initialize() 时都会重放它们的副作用:注册工具、挂钩子、加命令、改配置。无需注册表,也无需在调用方接线。
扩展是可信代码
扩展在导入期与初始化期以完整进程权限运行,性质等同 shell 的 rc 文件。只放你自己信任的代码。
两种形态¶
| 形态 | 路径 | 说明 |
|---|---|---|
| 包 | extensions/{name}/setup_extension.py |
入口文件名固定;可以相对导入同目录的兄弟模块 |
| 扁平 | extensions/{name}.py |
零仪式;不支持相对导入 |
- 目录里没有
setup_extension.py会告警并跳过;两种形态同名时包优先,扁平文件被屏蔽。 - 以
.或__开头的条目被忽略;非.py文件(例如目录里的README.md)静默跳过。 - 应用顺序按名称排序,两种形态统一排序,结果确定。
入口函数¶
模块必须提供可调用的 setup_extension(ctx):
"""Log every tool call.""" # docstring 首行 = 扩展描述,/extensions 会显示
from xun import ExtensionContext
def setup_extension(ctx: ExtensionContext) -> None:
ctx.agent.hooks.before_tool_call.add(lambda args: print(args.tool_calls))
ExtensionContext 暴露的公开 API 只有两项:
| 成员 | 说明 |
|---|---|
ctx.agent |
正在初始化的智能体(Agent[Agent.T.Uninit]) |
ctx.name |
扩展名(目录名或文件名) |
一切效果都通过 ctx.agent 完成:toolbox.register(...)、hooks.*.add(...)、command.register(...)、config 就地改、state、system(...)、display、workspace。
加载时序¶
flowchart TD
A["Agent.initialize()"] --> B{"启用扩展?"}
B -->|否| Z["跳过"]
B -->|是| C["按名称顺序扫描扩展"]
C --> D["导入模块"]
D --> E["执行 setup_extension"]
E --> F["继续初始化"]
关键语义:
- 模块每个进程只导入一次(模块级副作用只跑一次,模块状态保留),
setup_extension则每个智能体各跑一次——包括子智能体与摘要/风险评估这类内部辅助智能体。 - 扩展在模型名自动探测之前执行,因此改
config.model.name会生效。 - 导入失败或
setup_extension抛异常只打印Extension warning:,不阻断启动,也不影响其它扩展;但同一个 setup 内后续语句会随异常中断。 - 已加载的模块不会卸载(子智能体重放依赖它们)。
- 关闭方式:
config.enable_extensions = false(配置或扩展里改),或Agent.config.enable_extensions = False后再initialize()。
查看已加载扩展¶
>>> /extensions
shown: Show me.
web_display_log: Console log for web display
仓库内示例¶
"""Console log for web display."""
from xun import ExtensionContext, WebDisplay
def setup_extension(ctx: ExtensionContext) -> None:
if not isinstance(ctx.agent.display, WebDisplay):
return
agent_id = f"{ctx.agent.name} ({ctx.agent.identifier})"
ctx.agent.hooks.before_display_error.add(
lambda args: print(f"{agent_id} Error: {args.message}")
)
先看显示层类型再挂钩子,是避免在无关智能体上产生副作用的常用写法。
"""Override web_search with an external Web Search tool."""
from xun import ExtensionContext, tool_attr
try:
from some_sdk import Client
_client = Client()
except Exception as exc: # 依赖或密钥缺失时整体退化为不生效
_client = None
print(f"extension disabled: {exc}")
def setup_extension(ctx: ExtensionContext) -> None:
if _client is None:
return
@tool_attr(name="web_search", override=True)
def my_search(query: str, limit: int = 5) -> list[dict]:
"""Search the web."""
return _client.search(query, count=limit)
ctx.agent.toolbox.register(my_search)
override=True 让同名工具静默替换内置实现;把可选依赖的探测放在模块级,setup 里只判断是否可用。
与其它机制的关系¶
| 想做的事 | 更合适的机制 |
|---|---|
| 只给某个智能体加东西 | 直接调用 agent.toolbox.register() / agent.hooks.*.add() |
| 给所有智能体加东西、跨项目共享 | 扩展(本文) |
| 只改配置 | 配置文件;或扩展里改 ctx.agent.config |
| 换系统提示词 | agent.system(),或 setup_agent(default_system_prompt=False) |