插件 SDK 参考
本章面向已经完成入门插件的开发者,说明 SDK v1 的契约、生命周期和边界。示例优先使用扁平 plugin.yaml,它也是仓库内置插件采用的形式。
加载过程
一个目录插件按以下顺序进入系统:
- Registry 发现并解析
plugin.yaml,此时不导入代码。 - 校验插件 ID、SDK 版本、能力、依赖声明和入口格式。
- Install 保存已验证的包;Enable 才导入
entrypoint。 - 创建
ScientificPlugin实例,并把磁盘清单绑定为该已加载插件的权威 manifest。 - 首次执行前调用
start(),关闭 Runtime 时调用stop()。 - 每次能力调用经输入和产物引用验证后进入
execute()。
CLI 在配置的 plugins 中直接使用目录时,会在创建运行期间完成发现和加载。桌面自定义 ZIP 则把 Install 与 Enable 分为两个明确步骤。
plugin.yaml
插件级字段
| 字段 | 必需 | 说明 |
|---|---|---|
name |
是 | 稳定插件 ID;建议反向域式命名,如 lab.spectrometer |
display_name |
否 | UI 显示名;省略时由 ID 生成 |
version |
是 | 插件版本;建议语义化版本 |
type |
是 | 实现类型,如 domain、evaluator、analyzer、reporter |
description |
否 | 一两句用户可读说明 |
category |
否 | core 或 domain;domain 插件应同时写 domain |
domain |
Domain 建议 | 如 classical_mechanics 或 accelerator_physics |
sdk_version |
是 | 当前只支持字符串 "1" |
entrypoint |
可执行插件是 | python.module:ClassName |
dependencies |
否 | 必须满足的依赖声明;不会自动安装 |
optional_dependencies |
否 | 兼容旧清单的可选依赖声明 |
access_role |
否 | research(默认)或 trusted_evaluator |
trusted_metrics |
否 | 返回指标是否具有可信评估权威,默认 false |
required_artifacts |
否 | 插件总体需要的科学 artifact kind |
produced_artifacts |
否 | 插件总体产出的科学 artifact kind |
runtime_requirements |
否 | JSON 对象,例如 GPU、内存或外部运行时说明 |
capabilities |
是 | 至少一个且名称唯一的能力 |
插件 ID 和能力 ID 必须以字母开头,并只使用字母、数字、下划线、点、冒号或短横线。发布后不要复用一个 ID 表示不兼容的新含义。
dependencies 是展示与可用性检查的声明,不是安装脚本。插件不得在 import、start() 或 execute() 中自行运行包管理器。
能力字段
| 字段 | 必需 | 说明 |
|---|---|---|
name |
是 | 稳定、带命名空间的能力 ID |
description |
建议 | Reasoner 与用户可读的操作定义 |
input_schema |
否 | 普通输入名到简单类型的映射 |
required_inputs |
否 | input_schema 中的必填键 |
input_guidance |
否 | 嵌套字段、范围、单位和选择规则 |
artifact_inputs |
否 | 逻辑输入名到所需产物说明的映射;"*" 可接收额外产物 |
required_artifact_inputs |
否 | 必须提供的逻辑产物输入名 |
output_schema |
否 | 输出各部分的简单形状说明 |
output_summary |
建议 | 一句话说明结果和产物 |
metric_outputs |
验收指标需要 | 可能返回的规范化指标名 |
example |
强烈建议 | 一个小型且合法的输入样例 |
cost_hints |
否 | 确定性资源提示,如 solver_call: 1 |
immutable_key_input |
否 | 参与不可变范围选择的输入键 |
immutable_output_scope |
否 | 对应的冻结输出范围标识 |
provides |
否 | 高层语义标签,用于能力匹配 |
consumes_artifact_kinds |
否 | 消费的科学 artifact kind |
produces_artifact_kinds |
否 | 产出的科学 artifact kind |
scientific_roles |
否 | 如 numerical_simulation、scientific_analysis |
简单 schema 类型只有:
string number integer boolean object array
这是能力边界检查,不是完整 JSON Schema。嵌套对象的字段、单位、范围与互斥规则写进 input_guidance 和 example,并在插件中做领域验证。未知普通输入会被拒绝;Python 的 bool 也不会被当作 number 或 integer。
Artifact kind 与 ArtifactType
两者用途不同:
ArtifactType是 Runtime 存储分类,例如DATASET、TRAINED_MODEL、PREDICTOR_OUTPUT、ANALYSIS_RESULT和SCIENTIFIC_REPORT。- artifact kind 是领域语义,例如
three_body_trajectory、evaluation_result或oscillator_assessment,通常放在科学 metadata 中并由能力清单声明。
一个分析文件可使用 ArtifactType.ANALYSIS_RESULT,同时具有领域 kind oscillator_assessment。不要为了每个领域概念扩展存储枚举。
ScientificPlugin
最小实现:
class MyPlugin(ScientificPlugin):
@property
def manifest(self) -> PluginManifest:
...
async def execute(
self,
capability: str,
inputs: JsonObject,
context: PluginExecutionContext,
) -> PluginResult:
...
可选钩子:
async def start(self) -> None: ...
async def stop(self) -> None: ...
def create_reasoner(self) -> Reasoner | None: ...
def estimate_cost(
self, capability: str, inputs: JsonObject
) -> dict[str, int | float]: ...
生命周期建议
- 构造函数和
manifest应快速、确定性且无网络副作用。 start()可加载模型、建立本地连接或准备只读资源;应可安全调用一次。execute()不能依赖另一调用正在同时修改的全局状态。stop()即使部分初始化失败也应能安全释放资源。- 不要在这些钩子中修改研究合同、事件文件或 Runtime 的产物目录。
estimate_cost()
该方法在执行前运行,用于阻止明显超预算的调用。它必须快速、确定性且不执行真正实验。例如:
def estimate_cost(self, capability, inputs):
if capability == "lab.solver.generate_dataset":
return {
"training_samples": int(inputs["num_samples"]),
"solver_call": 1,
}
return {}
预估不是实际用量。完成后仍应在 PluginResult.resource_usage 报告可测量消耗。当前预算直接识别的常见用量包括训练样本;action 类型还会分别计入实验、训练运行和求解器调用。
PluginExecutionContext
| 属性 | 用途 |
|---|---|
run_id |
当前运行 ID |
action_id |
当前动作 ID |
workspace |
当前执行可使用的工作目录 |
resolved_artifacts |
逻辑输入名到经验证本地路径的映射 |
artifact_references |
对应公开产物引用与 metadata |
private_artifacts |
仅 trusted evaluator 可见的私有路径 |
private_artifact_references |
私有引用 metadata |
artifact_graph |
当前可见科学产物图,只读 |
read_artifact(id) |
通过 Runtime 边界按 ID 读取并校验产物 |
优先使用 resolved_artifacts["logical_name"] 处理清单声明的直接输入。需要沿谱系查找时才使用 artifact_graph 和 read_artifact()。
不要:
- 从 artifact URI 猜磁盘路径;
- 扫描整个 data directory;
- 打开其他 run 的目录;
- 用
workspace.parent绕出工作区; - 在普通
research插件中尝试读取 private 字段。
Runtime 会限制同运行、可见性和 hash,但插件仍要验证文件格式、大小、编码和领域 schema。
PluginResult
PluginResult(
success=True,
metrics={"rmse": 0.04},
observations={"samples": 512},
artifacts=(...),
resource_usage={"training_samples": 512},
)
规则:
- 成功结果不能同时有
error;失败结果必须有error。 - metrics 的名称忽略大小写后必须唯一,值必须可作为数字持久化。
- observations、metadata 和 resource usage 必须是 JSON 可序列化内容。
- resource usage 不能是负数或布尔值。
- 同一次返回中的产物文件名必须唯一。
预期的输入错误、求解失败或外部工具失败使用失败 PluginResult。真正的插件 bug 或不可恢复初始化错误可以抛异常,Runtime 会记录为执行失败;错误文本不得包含密钥、私有标签或大段输入数据。
PluginArtifact
二进制产物:
PluginArtifact(
name="predictions.csv",
artifact_type=ArtifactType.PREDICTOR_OUTPUT,
content=csv_bytes,
metadata={"schema": "predictions-v1"},
)
文本辅助构造器:
PluginArtifact.text(
name="report.md",
artifact_type=ArtifactType.SCIENTIFIC_REPORT,
content=markdown,
metadata={"format": "markdown"},
)
name 必须是普通文件名,不能含目录。多个新产物可用 parent_names 建立同一次结果内的谱系;父产物必须排在子产物之前:
artifacts=(
PluginArtifact.text("data.json", ArtifactType.DATASET, data),
PluginArtifact.text(
"analysis.json",
ArtifactType.ANALYSIS_RESULT,
analysis,
parent_names=("data.json",),
),
)
默认 visibility=ArtifactVisibility.RESEARCH。EVALUATOR_PRIVATE 只应用于经过设计和审查的受信任评估流程,不能作为普通插件隐藏不希望用户看到的结果。
受信任评估器
清单字段为:
type: evaluator
access_role: trusted_evaluator
trusted_metrics: true
受信任评估器应满足更严格的设计:
- 预测与公开评估特征通过普通 artifact inputs 传入;
- 私有目标由 Runtime 根据不可变
evaluation_id解析; - 样本 ID、shape、schema 和 evaluation identity 必须逐项匹配;
- 输出只包含聚合指标、允许公开的诊断和评估产物;
- 不能把私有标签复制进 observations、日志或研究可见产物;
- 测试证明普通插件和 Reasoner 无法读取私有内容。
在清单中自称 trusted 并不能替代代码审查和部署信任。第三方评估器启用前必须按高权限代码审核。
Executor 插件
执行后端继承 ExecutorPlugin,核心方法是:
async def execute_job(
self,
job: JobSpec,
context: PluginExecutionContext,
) -> JobResult:
...
可选实现 submit()、poll()、cancel()、collect_outputs()、collect_usage() 和 cleanup(),用于异步或远程后端。execute() 已提供 job_spec 到这些生命周期方法的兼容转换。
一个 Executor 必须执行 JobSpec 中声明的命令、输入和输出协议,保留 stdout/stderr 与退出状态,支持取消,并确保输出仍位于工作区。远程调度器还要持久化外部 job ID,才能在进程重启后恢复轮询。
不要把 Executor 当成 Domain 插件:领域插件描述“做什么科学操作”,Executor 描述“在哪儿以及如何运行一个已声明作业”。
版本与兼容性
sdk_version变化代表 SDK 契约变化;当前为1。- patch 版本用于不改变输入输出的 bug 修复。
- minor 版本可增加向后兼容的可选字段或能力。
- 删除能力、重命名 metric、改变必填输入或产物 schema 应升 major。
- 已持久化运行依赖旧能力身份;不要就地改变旧版本语义。
若必须迁移,为新能力使用新 ID,并在发行说明中给出配置转换示例。能力别名只用于有意的兼容路由,不能掩盖不兼容变更。
ZIP 包限制
自定义插件包必须满足:
.zip扩展名;- 压缩内容最多 10 MiB;
- 解压内容最多 50 MiB;
- 最多 512 个文件;
- 一个明确的
plugin.yaml、plugin.yml或plugin.json; - 无绝对路径、
..穿越、符号链接、加密、重复或冲突条目。
不要打包虚拟环境、模型缓存、Git 历史、测试输出或密钥。大型模型和数据应作为独立、可校验的部署资源,而不是塞进插件 ZIP。
推荐测试层次
- Manifest 测试:解析清单,核对能力 ID、metric producer 和 entrypoint。
- 纯单元测试:直接调用
execute(),覆盖正常值、边界值和错误输入。 - Artifact 测试:内容、metadata、父子关系和 hash 后读取。
- Registry 测试:从真实目录发现并加载,不依赖测试专用 import path。
- ZIP 测试:安装、启用、禁用;验证恶意路径与超限包被拒绝。
- 端到端运行:用很小预算创建合同并达到真实验收条件。
- 恢复测试:若有长作业,测试暂停、进程中断、恢复与取消。
- 权限测试:普通插件不能读取 evaluator-private 或其他 run 的产物。
代码检查可从以下命令开始:
ruff check path/to/plugin
pytest -q path/to/plugin/tests
scientific-agent --data-dir /tmp/plugin-e2e run plugin-example.yaml