插件开发¶
插件是一个导出 CapabilitySpec 的 Python 模块(例如 my-plugin/plugin.py),与内建能力走同一装配层。
CapabilitySpec 字段:cap_id、title、mode、deps=()、apply=None、release=None、
provides=None。
from phone_agent.v2.capabilities import CapabilitySpec
def apply(ctx):
def on_observe(payload):
print(f"[my-plugin] {payload}")
ctx.on("observe", on_observe)
def release(ctx):
print("[my-plugin] released")
CAPABILITY = CapabilitySpec(
cap_id="my_plugin",
title="My Plugin",
mode="on",
deps=(),
apply=apply,
release=release,
)
deps 同时约束装配顺序与状态:依赖 off 或待定时本能力为 pending,装配时依赖先行。provides 与
ctx.register_service(name, value) 配套,声明了服务却没有真正注册会在装配期报错。ctx.on(...) 会把监听器
归属到当前 capability;正常 release 或 apply 失败时,装配层都会清理该监听器。
目录只需一个导出 CAPABILITY 的 plugin.py:
安装¶
plugin add 默认把条目写入项目 .taskwizard.toml;list、remove、update、search 管理清单与本地/社区包:
add:本地路径以目录名登记path=,不做 pip 安装;包名走pip install并以包名登记;remove NAME:包条目先pip uninstall -y再从清单删除,本地路径条目只删登记;update NAME或update --all:对已启用的包条目执行pip install -U;add/remove都接受--user/--project选择清单作用域(默认项目级);search [term]:读PHONE_AGENT_PLUGIN_INDEX(URL 或本地 json)。
pip 包则声明 entry point 后被自动发现,entry point 名必须等于清单条目名(plugin add <包名> 即以包名登记):
entry point 也可以直接指向一个 CapabilitySpec 对象,不必经过 CAPABILITY 属性。
Danger
plugin add 即代码执行授权:该目录的 plugin.py 会在下次启动时执行,与 pip install 同级信任。只添加你审过源码的目录或包。
CLI 与 runner 使用同一批授权入口,runner 启动装配时 import/apply(Web 空闲/启动只序列化配置、不执行插件代码)。
plugin list 在装配之外还会把每个启用条目加载两次(一次探测装配状态、一次取 cap_id),路径插件每次加载
都重新 exec_module;其余子命令不加载插件模块。插件不支持运行中热重载,内建能力同样是静态装配,要变更需
重启进程。ctx.on_dispose(...) 可注册 release 或失败清理回调,归属当前 capability。
授权与发现¶
| 机制 | 内容 |
|---|---|
| entry points | pip 包声明 taskwizard.capabilities 组即被自动发现 |
| manifest | 用户级 ~/.taskwizard/profile.toml 与项目级 <repo>/.taskwizard.toml(可用 PHONE_AGENT_PLUGIN_MANIFEST 覆盖路径);同名条目项目级优先 |
| 清单写入 | plugin add 只做两件事:pip 安装,或在 manifest 里登记本地路径(不注入其它资源,包括应用词表);相对路径按项目清单目录解析 |
| 失败可见 | 没有宽松开关:已启用条目加载失败、API 版本不匹配或未提供 CapabilitySpec 一律抛错;只有显式 enabled=false 才跳过 |
| 总开关 | PHONE_AGENT_PLUGINS=false 关闭全部外部插件,内建能力不受影响 |
manifest 条目字段:name、version、enabled(默认 true)、path、[plugin.config] 键值表。读写往返支持
[plugin.config],但 emitter 只接受 string/bool 值;加载路径不把 config 传给插件——plugin.py 当前拿不到
manifest 里配的值(已知限制)。
REQUIRES_API 是受限 PEP-440 子集:逗号组合 >=、<=、==、=、>、<、!=,裸版本号视为 ==,
缺省或空字符串放行任意版本。模块级 REQUIRES_API 优先于 spec 对象上的同名属性;不满足即报错。
Web 进程没有插件加载路径,决定是否激活的是 runner 装配阶段,每个 run 装配一次;需要给 App-KB 注入词表时走
main_v2.py --learn-alias,不经插件通道。
事件¶
观察型监听器 fn(payload) -> None;洋葱型 fn(payload, next) -> result,先注册者居外,不调 next() 即短路,内层短路的结果对外层照常可见。自定义事件名必须匹配 ^[a-z]+/[a-z_]+$,内置事件常量属于保留集、始终合法;emit 监听器抛错只记日志、被吞掉,不阻断发事件方。各事件 payload 契约见 phone_agent/v2/events.py 模块 docstring。
| 事件 | 类型 | 返回契约 |
|---|---|---|
run/start / run/end / observe / app/launched / taskdoc/completed / model/post_request / agent/after |
emit | 忽略 |
tool/pre_execute |
洋葱 | 与 tool/execute 同形;已定义、保留并导出,当前没有生产代码 emit |
tool/execute |
洋葱 | next(request) 的结果,或返回 REJECT 短路(桥转成"已拦截"ToolMessage) |
model/pre_request |
洋葱 | 返回变换后的完整消息列表;插件不得返回 RemoveMessage,也不要用 jump_to 做流程控制。桥接器对历史形态(含 jump_to:"end" 的 dict)保持宽容兼容,JUMP_END 是 core 熔断用的哨兵——兼容不等于推荐用法 |
model/request |
洋葱 | next(request) 的结果(包模型调用:计时、观测) |
app/launched payload 为 {"package", "device_id", "source"}:source 取 launch_app(设备确认的启动成功)或
foreground(已提交观测里前台包变化到尚未播报的包,系统包不播报),每 run 每包至多发一次。
taskdoc/completed payload 为 {"item_ids", "screen_seq", "epoch"}:一次提交的路线项
in_progress → completed 迁移即发一次,fail-open,不改变工具回执。
内建嵌套顺序(插件监听器装配期注册,位于 safety 之内;sibling_receipts 装配后追加,是 tool/execute 最内层):
tool/execute: trace → diagnostic → admission → control_hitl → budget → safety → sibling_receipts(最内;纯呈现层,rejection / terminal skip 不运行)
model/pre_request: compact → taskdoc → budget → boundary_compact → procedure → diagnostic → model_limit
model/request: trace → diagnostic → budget(最内)
budget 的 tool 闸以 prepend=True 注册,位于 safety 之外;safety_mode=off 时 tool/execute 没有 safety 节点;
compact_enabled=false 时 model/pre_request 最外层是一条只做图像/marks 清理的 prune-only 监听器(没有 compact
监听器)。procedure 是 recall 能力的预请求注入器,进场投递在 app/launched 时暂存。
其他接缝¶
除监听器外,apply(ctx) 里还可以挂:
- 工具(
register_tool,模型可见):同名工具覆盖 baseline,release 后 baseline 回到原位; - 提示块(
add_prompt_block):只有system_suffix与system_message两种 placement,provider 可返回裸字符串(按system_message处理); - run hooks(
add_run_hook,相位start/end):内建排序 start 为 taskdoc 10 → app_kb 20 → experience 35 → recall 40,end 为 experience 40 → recall 50 → dream 90,未列出的 owner 默认 50、同序按注册先后; - CLI 子命令(
add_cli_command); - 服务(
register_service):进入共享服务命名空间、按cap_id记录 owner,release 时零残留;同名 key 被两个挂载能力注册即 fail-visible 冲突; ctx.on(..., prepend=True)把监听器插到最外层,返回的 disposer 幂等可重复调用。
core 侧另有 register_core_middleware / register_core_tool / add_core_run_hook:同一有序集合、owner 为
__core__,能力 release 不会摘除它们。register_middleware 与 MiddlewareReplacement 是给第三方桥接中间件
预留的接缝(默认 order=50),当前没有内建调用方、没有测试;策略行为仍必须走事件总线。插件注册的进程级资源
(如 register_api_builder 的传输)同样不被 release 管理,需自行 ctx.on_dispose 清理。
跨模块共享的 pinned id 前缀从 phone_agent/v2/pins.py 导入(TASKDOC_ID_PREFIX、COMPACT_ID_PREFIX),不要
硬编码字符串。
provider 侧还有可选的模型上下文支持接缝:自定义 API builder 用 bind_context_support 挂 profile/estimate/
prepare/usage 支持对象,缓存参数白名单见模型提供方与路由。
装配契约¶
phone_agent/v2/capabilities.py 是唯一装配器:十三个内建能力(providers、taskdoc、safety、budget、compact、boundary_compact、finish_verify、deliverable、app_kb、dream、experience、recall、obs_archive)经五条接缝挂载——register_middleware、register_tool、add_prompt_block、add_run_hook、add_cli_command;register_service 是第六接缝,把能力服务发布进 harness 服务命名空间。内建策略全部是事件总线监听器,因此策略顺序由注册顺序决定。
- core 监听器顺序:见上表;插件在能力链之后注册,因此插件的
tool/execute监听器排在 safety 之内; - 归属与释放:
ctx.on/ctx.on_dispose把订阅与清理绑定到当前 capability。release先反序跑清理回调,再摘除该能力注册的中间件、工具、提示块、run hooks、CLI 命令与服务;正常模式变更在该 release 之后仍会 apply 新能力,只有在清理报错时才不再替换; - 依赖状态:能力的对外状态由依赖推导(off 优先;依赖为 off 或待定时为 pending;就绪且档位为 shadow 时为影子;否则生效);
- 静态装配:内建能力无文件监视、无运行中工具表重建,变更需重启进程;
- provider bootstrap:只支持声明
providers与外部 helper,缺失、循环依赖或依赖 runtime 内建能力都在启动时可见失败,不做静默降级; - in-run 控制:runner 唯一的运行中能力变更控制是
revoke_lesson——撤销 lesson 只影响后续投递,已经发送出去的上下文不可撤回;停止与 HITL 等既有控制照常存在。
打包分发¶
plugin add 只做 pip 安装或登记 path=,不注入其它资源(包括应用词表);社区仓库 plugins/index.json
暂未上架任何包。
Note
PLUGIN_API_VERSION = 1 为 provisional:出现第二个真实实现方并完成验证前,事件与契约可能调整。