第 6 章 第一部分:快速上手

模块管理与扩展开发

nexus modules CLI、TUI、写自定义 Task/Agent/Tool 的完整流程、pyproject.toml 元数据规范

Nexus 模块 扩展开发 CLI pyproject.toml

nexus modules CLI

Nexus 的模块管理是框架的一个亮点——你不需要修改任何核心代码就能添加新功能。

列出所有模块

nexus modules

输出按类别分组(Agent / Task / Tool / LLM Client / Executor / Provider / Processor),每个模块显示 Host 和 Runtime 依赖的安装状态:

Agent (16)
  base                  3/3 ✓    0 ✓    1 agent
  browser_gym           4/4 ✓    0 ✓    1 agent
  claude_code           0 ✓      0 ✓    1 agent
  ...

Task (28)
  swe                   5/5 ✓    2/2 ✓  5 tasks
  webarena              0 ✓      0 ✓    1 task
  ...

56 modules, all OK

安装缺失依赖

# 安装指定模块的 Host 依赖
nexus modules install swe_agent

# 安装指定模块的 Host + Runtime 依赖
nexus modules install swe_agent --runtime

# 安装所有模块的依赖
nexus modules install --all

交互式 TUI

nexus modules --tui

启动一个基于 Textual 的终端 UI,可以浏览模块、查看依赖、一键安装。在 Terminal 里操作比 CLI 更直观。

模块的组织方式

Nexus 的模块分布在 8 个扫描根目录下:

nexus/
├── extensions/
│   ├── agents/        # Agent 模块
│   ├── tasks/         # Task 模块
│   ├── tools/         # Tool 模块
│   ├── projects/      # Project 模块(自包含的完整项目)
│   └── openai_clients/# LLM Client 模块
├── runtime/
│   ├── providers/     # Runtime Provider 模块
│   └── executors/     # Executor 模块
└── trace/
    └── processors/    # Trace Processor 模块

每个模块是一个目录(含 pyproject.toml)或单文件(含 PEP 723 inline metadata)。

写一个自定义 Tool

从最简单的开始。假设你要写一个计算器工具。

步骤 1:创建目录

nexus/extensions/tools/calculator/
├── pyproject.toml
├── __init__.py
└── tool.py

步骤 2:写 pyproject.toml

[project]
name = "calculator"
dependencies = []

[tool.nexus]
tools = { calculator = "nexus.extensions.tools.calculator.tool::calculate" }

[tool.nexus] 是关键——它告诉模块发现系统:这个模块注册了一个名为 calculator 的 Tool,import path 是 nexus.extensions.tools.calculator.tool::calculate。

步骤 3:写工具函数

# tool.py
from pydantic import BaseModel
from nexus.core import ToolContext, ToolResponse, tool_registry


class CalculateInput(BaseModel):
    """计算数学表达式"""
    expression: str


@tool_registry.register('calculator', meta={
    'description': 'Evaluate a mathematical expression',
    'tool_type': 'local',
})
async def calculate(params: CalculateInput, context: ToolContext) -> ToolResponse:
    try:
        result = eval(params.expression)  # 生产环境请用 safe eval
        return ToolResponse(output=[{'type': 'text', 'text': str(result)}])
    except Exception as e:
        return ToolResponse(output=[{'type': 'text', 'text': f'Error: {e}'}])

Tool 函数必须满足:

  1. 是 async 函数
  2. 第一个参数是 Pydantic BaseModel 子类(自动生成 OpenAI function schema)
  3. 第二个参数是 ToolContext
  4. 返回 ToolResponse

步骤 4:验证

nexus modules  # 应该能看到新的 calculator Tool

然后在 Agent 配置中使用:

agent_config:
  tools:
    - "calculator"

写一个自定义 Agent

Agent 的骨架更简单——就是一个 async 函数。

# my_agent/agent.py
from nexus.core import (
    AgentRunInput, AgentRunOutput, WithUserPrompt, WithMainTrajectory,
    agent_registry, require_context,
)

class MyAgentInput(AgentRunInput, WithUserPrompt):
    max_iterations: int = 10

class MyAgentOutput(AgentRunOutput, WithMainTrajectory):
    final_answer: str

@agent_registry.register('my_agent')
async def run_my_agent(agent_input: MyAgentInput) -> MyAgentOutput:
    ctx = require_context()
    client = ctx.openai_client
    runtime = await ctx.get_runtime()

    messages = [
        {'role': 'system', 'content': 'You are a helpful assistant.'},
        {'role': 'user', 'content': agent_input.user_prompt},
    ]

    for i in range(agent_input.max_iterations):
        completion = await client.create_completion(messages=messages)
        assistant_msg = completion.choices[0].message

        if not assistant_msg.tool_calls:
            # 没有工具调用,Agent 认为任务完成
            break

        # 处理工具调用...
        messages.append(assistant_msg.model_dump())

    return MyAgentOutput(
        final_answer=assistant_msg.content or '',
        main_trajectory=messages,
    )

对应的 pyproject.toml:

[project]
name = "my_agent"

[tool.nexus]
agents = { my_agent = "nexus.extensions.agents.my_agent.agent::run_my_agent" }

Agent 函数的约束:

  1. 是 async 函数
  2. 恰好一个参数,类型是 AgentRunInput 子类
  3. 返回类型是 AgentRunOutput 子类
  4. 通过 require_context() 访问 Runtime 和 LLM Client

写一个自定义 Task

Task 是最上层的编排单元。

# my_bench/task.py
from nexus.core import (
    TaskRunInput, TaskRunOutput, WithAgentRunner, WithScore,
    task_registry, require_context,
)

class MyBenchInput(TaskRunInput, WithAgentRunner):
    question: str
    expected_answer: str

class MyBenchOutput(TaskRunOutput, WithScore):
    agent_answer: str
    correct: bool

@task_registry.register('my_bench')
async def run_my_bench(task_input: MyBenchInput) -> MyBenchOutput:
    # 1. 运行 Agent
    agent_output = await task_input.run_agent(
        user_prompt=task_input.question,
    )

    # 2. 从 Agent 输出中提取答案
    answer = agent_output.final_answer if hasattr(agent_output, 'final_answer') else ''

    # 3. 评分
    correct = answer.strip().lower() == task_input.expected_answer.strip().lower()

    return MyBenchOutput(
        agent_answer=answer,
        correct=correct,
        score=1.0 if correct else 0.0,
    )

Task 函数的约束:

  1. 是 async 函数
  2. 恰好一个参数,类型是 TaskRunInput 子类
  3. 返回类型是 TaskRunOutput 子类
  4. 如果需要 Agent,input 继承 WithAgentRunner,通过 task_input.run_agent() 调用

pyproject.toml 元数据规范

[tool.nexus] section 的完整格式:

[project]
name = "my_module"
description = "My awesome module"
version = "0.1.0"
dependencies = [           # Host + Runtime 共享依赖
    "requests>=2.28.0",
]

[project.optional-dependencies]
host = [                   # 仅 Host 侧需要的依赖
    "rich>=13.0",
]
runtime = [                # 仅 Runtime 侧需要的依赖
    "playwright>=1.40.0",
]

[tool.nexus]
# 注册对象:{ 注册名 = "import_path" }
agents = { my_agent = "nexus.extensions.agents.my_agent.agent::run_my_agent" }
tasks = { my_task = "nexus.extensions.tasks.my_task.task::run_my_task" }
tools = { my_tool = "nexus.extensions.tools.my_tool.tool::my_tool_func" }
executors = { my_exec = "nexus.runtime.executors.my_exec::my_executor" }
openai_clients = { my_client = "nexus.extensions.openai_clients.my_client" }
runtime_providers = { my_provider = "nexus.runtime.providers.my_provider" }
span_processors = { my_processor = "nexus.trace.processors.my_processor" }

三层依赖

Nexus 将依赖分为三层:

层级位置说明
dependenciesHost + Runtime两端都需要的包
optional-dependencies.host仅 HostHost 侧额外需要的包(如 rich、textual)
optional-dependencies.runtime仅 RuntimeRuntime 侧额外需要的包(如 playwright)

这种分层的原因:Runtime 镜像通过 Nix volume 挂载,不能随意 pip install。Host 侧的重依赖不应该污染 Runtime 环境。nexus modules install --runtime 会安装 dependencies + runtime 的合集。

单文件模块(PEP 723)

对于简单模块,不需要创建目录,可以用 PEP 723 inline metadata 直接在 .py 文件中声明:

# nexus/runtime/providers/local.py
# /// script
# dependencies = []
#
# [tool.nexus]
# runtime_providers = { local = "nexus.runtime.providers.local" }
# ///

from nexus.runtime.providers.base import RuntimeProvider
# ...

# /// script 和 # /// 之间的注释会被解析为 TOML 格式的元数据。

模块注册范式对比

Python 生态有三种主流的模块注册范式:

范式代表注册时机适用规模局限
元类自动注册metaclass-registry子类定义即注册,零样板小型项目与继承深度绑定,不适合跨包发现
import string 懒加载lazyregistry显式 registry["name"] = "mod::func"库内使用无框架级发现,无验证管道
TOML 元数据驱动Nexus [tool.nexus]discover_and_register() 读 TOML框架级(8 根目录 56+ 模块)需要约定的目录结构
Agent 框架级插件Genkit Agents API + Qwen Agent Plugins v1统一 chat() 接口 / plugin.json+mcp.json+SKILL.md 便携包npm/pip 包 + 配置文件生态级(开源社区驱动);v1 包格式不支持 commands / agents / hooks(见下)

Nexus 选择 TOML 驱动是因为需要跨 8 个扫描根目录发现 56+ 模块——元类和 import string 范式不适合这种规模。TOML 元数据让发现过程完全不执行 Python 代码,从根本上解耦了”声明”和”实现”。Agent 框架级插件范式(qwen-code 25k★ / Genkit Agents API)则代表了另一个维度——不是代码级的模块注册,而是 Agent 生态级的可替换组件——对评测框架而言,两种粒度可能需要同时兼容。

Agent 框架级插件范式的两个最新进展值得补充:(1) Genkit Agents API(2026-07-16~08-01 发布)将 Google Genkit 从”插件”升级为”统一 Agent 接口”——统一 chat() 接口(preview)+ middleware 体系在 TS/Go/Dart 达到 GA,Agent Skills 以 SKILL.md + progressive disclosure 跨 TS/Go/Dart/Python 分发。

⚠️ 更正(2026-10-03):上面这段里”统一 chat() 接口(preview)“的说法已经过时,而且方向说反了。Genkit 1.39.0(npm 实测发布 2026-06-26)是一次破坏性变更,官方发布说明原文是「Removal of the beta Chat API: Chat class, ai.chat(...), and session.chat(...) are deleted. Migrate to ai.defineAgent(...) and use agent.chat() instead.」也就是说:chat() 不是”统一后的新接口”,而是已被替换掉的旧接口;“preview”这个限定词既过时(它不再是预览特性,因为它已经不存在了)又误导(它暗示旧路径还留着)。正确的现状是——Agents API 是唯一路径,状态化多轮对话、Zod 类型化的自定义 state、可插拔持久化、流式 patch、interrupts/resume、abort/detach 都由它提供,并配套 agents() / artifacts() 中间件。

后续版本(npm genkit 当前 latest = 1.42.0,发布于 2026-09-01,GitHub release 记为 09-02):1.42 新增 A2UI 插件(@genkit-ai/a2ui,Agent 以增量方式向客户端流式输出可交互 UI 表面,中间件注入 catalog 能力并校验 a2ui block),并把 OpenTelemetry logs 改为默认开启(GENKIT_OTEL_ENABLE_LOGS flag 被移除)。官方路线图的首要目标是让 Agents API 跨 TS/Go/Dart/Python 达到 stable。

⚠️ 时间戳提醒:本轮核验发现,二手发布聚合站对 Genkit 1.42.0 的标注是”2026-10-01~02”,与 npm registry 的 2026-09-01 差整整一个月;1.39.0 也被标成”约 2 个月前”,实际是 2026-06-26(约三个多月前)。版本号与日期一律以 npm registry 的 time 字段为准。

这套 SKILL.md 约定的实际覆盖面已有公开统计:截至 2026-09-14,vercel-labs/skills(npm 包 skills,当前 1.5.26)的 README 列出 68 个 AI coding agent 的 skills 路径约定;其特性对比矩阵覆盖 18 个 agent,其中 context: fork 目前仅 Claude Code 支持。⚠️ 该数字增长很快(早期版本的公开统计约 39,本轮未回源核实其出处与日期 ),引用时必须带快照日期。路径约定本身也高度不统一——Amp / Replit / Universal 与 Antigravity 走 .agents/skills/,AstrBot 走 data/skills/,另有若干 agent 使用自家目录——这正是”通用 agent”与”需 symlink 适配”之分。

🔄 锚点刷新(2026-10-03):这一段自带的警示在 19 天后应验了。截至 2026-10-03,npm skills 的 latest 已是 1.7.0(1.6.0 与 1.7.0 同日发布于 2026-09-17,前一个是 09-11 的 1.5.26);README 顶部自述句改成「Supports OpenCode, Claude Code, Codex, Cursor, and 75 more」,即合计 79 个 agent。

⚠️ 口径差:79 只能引自述句,不能自己数表。 本轮用正则解析 README 的 agent 表格只得到 67 行(含表头),与顶部自述的 79 对不上——两个数字无法互相印证。引用时只取顶部自述数并带快照日期。

复核后仍成立的两条:特性矩阵仍是 18 个 agent 列,context: fork 仍然只有 Claude Code 支持(其余 17 个全为 No)。矩阵本轮新增了 Hooks 行(Claude Code / Cline / Command Code 为 Yes)。

另有三个新出现的技术细节值得记:① 技能发现有深度上限——README 原文是每个技能容器目录「walked up to three levels deep」,超出部分要用 --full-depth 才扫得到(例如 examples/、tests/ 下的);② 支持从 manifest(.claude-plugin/marketplace.json 或 plugin.json 里的 source / skills 字段)声明技能,声明的路径按其声明深度搜索、不受 depth-3 约束,README 明说这是为兼容 Claude Code plugin marketplace;③ 这套约定现在有了正式规范出口——Agent Skills specification,规范站为 https://agentskills.io ,README 称各 agent 的 skills「follow a shared Agent Skills specification」。

第 ③ 条要加一个限定:本轮实测该站可访问(HTTP 200),其 GitHub 仓库是独立组织 agentskills/agentskills(2025-12-16 创建,约 2.6 万 star,自述”Specification and documentation for Agent Skills”),不是 vercel-labs 的单方站点;但本站未逐页读规范正文,也未核实其是否为多厂商共同发布,因此不把它写成”行业标准”,只写成”已有正式规范出口”。

顺带一句对本章论点的支撑:这些细节恰好说明统一的是规范,不统一的是落地目录——规范站只有一份,而扫描根目录仍在不断增加(本轮 README 的发现目录列表已含 .codebuddy/skills/、.commandcode/skills/ 等)。

(2) Qwen Code Agent Plugins v1(Qwen Code 0.24.7;npm registry 实测,2026-10-03,发布于 2026-09-29 )原生加载可移植的 v1 包,保留标准 plugin.json / mcp.json / SKILL.md,不生成 qwen-extension.json、不重写可移植文件;根清单须指向规范 schema https://agent-plugins.org/schemas/1.0.0/plugin.schema.json。

要分清”支持什么 / 不支持什么 / 不是什么”三层:

  • 支持:直接子级 skills/*/SKILL.md、stdio MCP、Streamable HTTP MCP。

  • 不支持——做成 v1 包后这些不生效:

    能力行为
    Commands、agents、hooks目录被忽略
    旧版 HTTP+SSE MCP条目被跳过(无静默的传输层替换)
    Qwen context / settings / channels / apps忽略
    extensions.* 客户端命名空间未实现的命名空间被忽略
  • 不是什么:官方文档结尾原话是「Agent Plugins v1 is a package format, not a marketplace integration」(Agent Plugins v1 是一种包格式,而非市场集成)。Qwen Code 确实能从 Gemini CLI Extensions Gallery / Claude Code Marketplace / Qoder 安装兼容包,但那是 Qwen Code 自身扩展体系的另一条能力,与 v1 包格式是两条独立路径——不要因为装上了 v1 包,就假定 commands / hooks 也会跟着生效。

三个实现细节值得留意:① 实验性的 allowed-tools 字段「被识别为字符串,但不会授予预批准的 Qwen tools」,权限仍走 Qwen 自身的权限策略(与 Ch04 讨论的 Installed Agent 权限边界直接相关);② 对 stdio MCP,${PLUGIN_ROOT} / ${PLUGIN_DATA} 在 args / env / cwd 中只展开一次,PLUGIN_DATA 是可写的、每安装独立的目录,内容在更新与重装后保留;③ 远程 MCP 端点必须使用 HTTPS(环回 HTTP 端点除外)。

前者代表”从插件到统一接口”的定位升级,后者代表”跨平台、声明式开放打包标准”——两者与 Nexus 的 [tool.nexus] TOML 元数据方案形成对照:TOML 元数据面向框架内模块发现,Agent Plugins v1 面向跨平台扩展分发。

Project 模块

extensions/projects/ 是一种特殊的模块类型——自包含的完整项目,同时定义 Task + Agent + Tool。

内置的 7 个 Project:

Project说明
calculator计算器(教学示例,常用于 RL 训练)
maze迷宫寻宝(RL 训练环境)
agtsAGTS Agent 训练系统
deepeyesDeepEyes 视觉 Agent
search_r1_demoSearch R1 Demo
videocut视频剪辑 Agent
android_demoAndroid 操作示例

Project 的 pyproject.toml 会同时注册 agents、tasks、tools:

[tool.nexus]
agents = { calculator_agent = "...agent::run_calculator_agent" }
tasks = { calculator_task = "...task::calculator_task" }
tools = { calculator_add = "...tools::add", calculator_multiply = "...tools::multiply" }

使用时需要显式导入 Project 模块:

imports:
  - "nexus.extensions.projects.calculator"
task_name: "calculator_task"
agent_name: "calculator_agent"

这是因为 discover_and_register() 默认会注册所有模块,但 Project 的工具可能与通用工具冲突,所以设计为按需导入。

模块规模扩展的挑战

当前 56 个模块的规模下,nexus modules TUI 浏览体验尚可,依赖冲突概率可控。但当模块数量扩展到 500+ 时,会面临三个挑战:

  1. 浏览体验下降:TUI 的分类列表在数百条目下变得冗长,需要搜索和筛选能力
  2. 依赖冲突增加:不同模块可能依赖同一包的不同版本,目前的三层依赖模型无法处理版本冲突
  3. 全量扫描低效:启动时扫描 500+ 模块的 TOML 虽然不执行代码,但 I/O 开销不可忽略

S1-NexusAgent(arXiv:2602.01550)提出的意图感知动态工具检索提供了一种解法——用子任务级意图嵌入与 Faiss 索引匹配,按需选择和热插拔工具,而非全量扫描。可能的演进方向是:模块市场 + 语义搜索 + 按需安装,从”发现所有”转向”按需检索”。

这个方向已经不是设想了——对照产品里已经有了。 qwen-code 的 0.24 线(2026-09-16 的 0.24.0 到 2026-09-29 的 0.24.7,19 天内跨完)连续合入两条正对着上面三个挑战的改动:

  • align Code Mode text with lazy tool discovery(#12990)——工具按需要才进入上下文,对应挑战 1 与 3;
  • share a per-turn token budget from a +Nk directive(#11917)——每轮有显式 token 预算,对应挑战 3 的 I/O 与上下文开销。

同一批还合入了 add budget-based ACP child admission(#11911)与 cap web_search calls per session(#11852),都是把”每轮/每会话能用多少”变成显式配额。这把本章的演进方向从”可能的发展路径”降格为”同行业已经在做的事”,读者评估 Nexus 的 500+ 模块方案时可以直接照这个尺子量。

⚠️ 上面四条均取自 release notes 的条目标题;lazy tool discovery 只在 nightly 的 fix 标题里反复出现,其具体语义(工具是否真的延迟加载、还是仅文案对齐)本轮未读 PR 正文,“已实现”应限定为”标题层面可见”。

另有一条与本章”三层依赖 / 沙箱边界”同族的对照:qwen-code 0.24.0 新增 add bwrap kernel sandbox backend for Linux(#11614)。沙箱内的执行边界是同一族问题——本章 Runtime 镜像经 Nix volume 挂载后不能随意 pip install,与”沙箱里能跑什么”是同一条约束的两种表述。

一个值得关注的工业对照是 Google Genkit 的插件架构(2026-07 Agents API 预览)——它选择了更粗粒度的”模型 Provider + 中间件”二维分类而非三维,避免了 Nexus 面临的 Task/Agent/Tool 交叉注册复杂度。这种”减法”策略对评测框架的模块管理有参考价值。


至此,使用篇结束。如果你已经能跑通任务、切换 Agent、产出训练数据、写自定义模块,那”赶时间”路径的目标已经达成。

从第 7 章开始,我们进入原理篇——拆解 Nexus 的核心设计。