LocalRun 插件开发
从零开发、安装和调试 external localrun provider 插件。
LocalRun 插件开发
这份文档面向 third-party 或内部工具开发者,说明如何实现一个可被 wujie run <issue> -- <provider> [args...] 调用的 external localrun provider 插件。协议 v1 始终是 Issue-first;需要支持 wujie discuss -- <provider> [args...] 时必须实现协议 v2。
如果你要修改 Wujie 内置 provider 的 Go 实现,先读 Local Run Provider。本页只覆盖外部插件的包结构、manifest、运行方式输入、stdout NDJSON 协议和本地验收方法。
插件包结构
一个插件目录至少包含:
fake-provider/
provider.json
wujie-localrun-fakeprovider.json是 manifest,声明 provider 名称、协议和 adapter 命令。wujie-localrun-fake是本地可执行 adapter,可以是 shell、Node.js、Python、Go、Rust 或任何可执行文件。command如果是相对路径,会按provider.json所在目录解析。
安装后,CLI 会把插件复制到当前 profile/config 的 state dir 下,并记录到 localrun_providers 配置。
仓库样板:Grok Build
仓库提供可安装样板 localrun-providers/grok/,对齐 Hermes 的 stdout=NDJSON / TUI→stderr 模式:
localrun-providers/grok/
provider.json
wujie-localrun-grok
test_wujie_localrun_grok.py安装与验收:
wujie local-run provider install ./localrun-providers/grok
wujie local-run provider list # kind=plugin, name=grok
wujie run <id> -- grok
wujie discuss -- grok
python3 localrun-providers/grok/test_wujie_localrun_grok.pyAdapter 行为摘要:
- 自建 PTY 启动用户原始
grok ...(拒绝-p/ streaming-json / 手动--rules) - 绑定 issue 时从
WUJIE_ISSUE_ID注入--rules只读上下文 - 新会话注入唯一
--session-id并锁定对应 transcript;命令行 resume/continue 首次定位后不再跟随其他终端的 session - 当前 TUI 提交
/resume后短暂允许一次重绑定,只切换到随后确实追加事件的同 cwd transcript;跳过历史行并继续同步新 prompt、回复和 usage - 发现并 tail
~/.grok/sessions/<url.PathEscape(cwd)>/<sessionId>/updates.jsonl - 映射
user_message_chunk/agent_thought_chunk/agent_message_chunk/tool_call*/turn_completed为 NDJSON 事件 - 启动时 snapshot baseline,resume 会话不重复上报历史行
- 使用 v2 协议,在仅有
WUJIE_LOCALRUN_SESSION_ID时也能启动讨论,并输出原生session事件
provider.json
最小 manifest:
{
"schema_version": 1,
"name": "fake-provider",
"display_name": "Fake Provider",
"command": "./wujie-localrun-fake",
"protocol": "wujie-localrun-provider.v2",
"capabilities": {
"interactive_pty": false,
"structured_events": true,
"issue_context": "env",
"usage": true,
"native_plan_handoff": false
}
}字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
schema_version | 是 | 当前只支持 1。 |
name | 是 | Provider 调用名,例如 fake-provider。会被规范化为小写并去掉首尾空白。不能覆盖内置 provider 名称。 |
display_name | 否 | wujie local-run provider list 展示用名称。 |
command | 是 | Adapter 可执行文件。包含路径分隔符或以 . 开头时按 manifest 目录解析;否则按 PATH 查找。 |
protocol | 是 | v1 只支持 Issue-first;v2 必须同时支持前置讨论,并可选支持原生 handoff。 |
capabilities | 否 | 运行能力声明。前置讨论由协议版本决定,不再使用独立 opt-in 字段。 |
capabilities 当前约定字段:
| 字段 | 说明 |
|---|---|
interactive_pty | Adapter 是否需要交互式终端能力。 |
structured_events | Adapter 是否能输出结构化事件。外部插件应设为 true。 |
issue_context | Issue context 来源。当前插件通过环境变量获得运行上下文,可写 "env"。 |
usage | Adapter 是否会上报 token usage。 |
native_plan_handoff | 是否会发出 provider 原生的当前上下文/清空上下文方案执行动作;仅 v2 可设为 true。 |
执行模型
用户执行:
wujie run WUJ-123 -- fake-provider --model demoWujie 会:
- 创建或绑定一个 local run。
- 解析
fake-provider:先查内置 provider,再查localrun_providers配置,最后查已安装目录。 - 启动 manifest 中的
command。 - 把 provider 后面的参数传给 adapter。以上例为例,adapter 收到的 argv 是
--model demo。 - 从 adapter stdout 按行读取 NDJSON 事件,并同步为 local run message 或 usage。
Adapter 的工作目录是用户执行 wujie run 时的当前目录。
执行环境变量
Wujie 会注入以下上下文:
| 环境变量 | 说明 |
|---|---|
WUJIE_RUN_ID | 当前 local run ID;前置讨论完成绑定前不存在。 |
WUJIE_ISSUE_ID | 当前 issue ID;前置讨论完成绑定前不存在。 |
WUJIE_LOCALRUN_SESSION_ID | v2 前置讨论的稳定 session ID;同一个 discussion 恢复后保持不变。 |
WUJIE_WORKSPACE_ID | 当前 workspace ID。 |
WUJIE_SERVER_URL | 当前 Wujie server URL。 |
WUJIE_TOKEN | 当前运行可用的 Wujie token;未提供时会注入禁用占位 token。 |
插件元数据使用 WUJIE_LOCALRUN_PROVIDER_*:
| 环境变量 | 说明 |
|---|---|
WUJIE_LOCALRUN_PROVIDER_NAME | manifest 中规范化后的 provider 名称。 |
WUJIE_LOCALRUN_PROVIDER_PROTOCOL | manifest 声明的 wujie-localrun-provider.v1 或 .v2。 |
WUJIE_LOCALRUN_PROVIDER_ORIGINAL_COMMAND | 用户在 wujie run 后选择的 provider 命令名。 |
WUJIE_LOCALRUN_PROVIDER_ORIGINAL_ARGS | 原始 provider argv 的 JSON 数组,包括 provider 命令名本身。 |
不要把 WUJIE_TOKEN 打到 stdout 或日志里。
前置讨论环境
当 manifest 使用 wujie-localrun-provider.v2 且用户执行 wujie discuss 时:
WUJIE_LOCALRUN_SESSION_ID必定存在。- Issue 创建前,
WUJIE_RUN_ID和WUJIE_ISSUE_ID不存在。 - 父 Wujie CLI 负责保存 discussion、创建 Issue、创建 local run 和切换 reporter;adapter 不应自行创建或查询 Issue。
- 选择继续当前上下文后,原 provider 进程已经拥有完整对话。父进程只传递绑定结果并继续执行,不会要求 adapter 从 Issue 重建上下文。
- stdout 仍使用同一套 NDJSON 事件。服务端只把
user_input、完整final和明确的用户问题/方案保存为讨论;thinking 和工具事件会被过滤。 - 交互终端输入完整达到
/wujie时,父 CLI 会先把触发字符写入 provider PTY,并在最多 100ms 内等待可见输出;检测到新输出后以 10ms 静默期确认本轮回显完成,再暂停 provider 并绘制候选菜单。Bracketed paste 会等到完整ESC[201~结束帧已经写入 PTY 后才检查触发。父 CLI 会跨任意输入分块识别以 BEL/ST 终止的 OSC、DCS、APC、PM、SOS,以及 CPR、DA、DSR、模式报告、窗口尺寸等 CSI 终端回复;回复原样转发给 adapter,但不进入用户输入状态,也不会关闭已打开的菜单。菜单打开时,焦点切换和 SGR/X10 鼠标事件仍由父 CLI 丢弃,不会延迟转发给 adapter。Adapter 不应依赖父 CLI 伪造或重绘 provider 的输入行。 - 候选菜单和 Issue 确认卡都使用输入框下方的专用插入区。父 CLI 会保存光标,先向下越过两行 adapter 自有区域,再插入所需行;这两行只移动锚点,不属于插入区。Issue 卡片会从终端可用高度中同步扣除这两行。完成、取消或失败时,父 CLI 回到同一锚点删除插入行并恢复原光标;不会删除两行 margin、覆盖 adapter 已渲染的终端区域或进入 alternate screen。
- Adapter 新建或切换原生会话后,应输出
{"event":"session","session_id":"<provider-session-id>"}。父 CLI 会按顺序保存该 ID,使后续 provider 原生 resume 自动命中同一条未完成 discussion。
仓库中的 Hermes/Grok 样板 adapter 都使用 v2:它们能接受 WUJIE_LOCALRUN_SESSION_ID,在发现原生会话后输出 session 事件,因此可以直接用于前置讨论。两者当前没有声明 native_plan_handoff;用户通过 /wujiecreateissue 完成提升,或由将来支持握手的 adapter 发出原生 handoff。
v2 原生 handoff
需要把 Provider 自己的“保留上下文执行 / 清空上下文执行”动作映射到 Issue 创建时,在 v2 manifest 中声明 native_plan_handoff: true。这项能力是可选的;不声明不会影响普通 v2 前置讨论与 /wujiecreateissue。
Adapter 在已准备好原生执行动作、但尚未真正执行前,通过 stdout 输出:
{"event":"handoff_ready","request_id":"handoff-1","mode":"native_clear"}mode 只能是 current_context 或 native_clear。父 CLI 会暂停该动作、显示确认卡并先完成 Issue/Run 绑定。原生确认卡不提供“继续讨论”:用户只能完成创建,或按 Ctrl+C 取消并结束 adapter;未完成 discussion 会保留,可从 provider 原生 session 恢复。普通 /wujiecreateissue 仍可返回讨论。随后通过 Adapter stdin 写入一条以 ASCII Record Separator 开头的控制帧:
\x1eWUJIE {"event":"handoff_decision","request_id":"handoff-1","accepted":true,"mode":"native_clear","issue_id":"...","issue_key":"OPE-123","run_id":"..."}Adapter 必须从普通终端输入中剥离并解析该帧。只有 accepted=true 时才能继续原生动作;取消或创建失败时会返回 accepted=false 和脱敏后的 error。用户取消时,父 CLI 会先写出 accepted=false,再结束 adapter,并把 provider 的信号退出视为正常取消。控制帧不携带 Issue 描述或方案正文,Provider 继续使用自己已经持有的上下文。
stdout NDJSON 协议
Adapter 必须把标准事件以单行 JSON 写到 stdout:每行一个事件,不能跨行。空行会被忽略。
stdout 只写协议事件;普通日志、调试信息、第三方 CLI 原始输出都写 stderr。stdout 出现非 JSON 行会让本次 local run 失败。
消息事件可以省略 event,也可以写 "event": "message":
{"type":"text","content":"开始处理","source":"fake","source_key":"turn-1:text-1"}支持的消息类型:
type | 字段 | 说明 |
|---|---|---|
text | content | Agent 面向用户的普通输出。 |
thinking | content | Agent 思考内容。只有你的 provider 语义确实支持时才写。 |
tool_use | tool, input | 工具开始执行。input 是 JSON object。 |
tool_result | tool, output | 工具执行结果。output 是字符串。 |
user_input | content | 用户在本地 CLI 中输入的普通消息,会同步成 Wujie 用户评论。 |
question | content | Agent 明确向用户提出的问题。讨论阶段会保存,普通 local run 仍按可见文本处理。 |
plan | content | 已完成的 Markdown 方案。讨论阶段优先把它作为创建 Issue 的最终方案。 |
final | content | Agent 对当前用户输入的最终回复,会同步成 agent 回复评论。 |
error | content | 可进入执行日志的错误消息。进程失败本身仍应通过退出码表达。 |
question / plan 推荐使用标准消息包络:
{"event":"message","type":"question","content":"数据库选 PostgreSQL 还是 SQLite?","source":"fake","source_key":"turn-1:question"}
{"event":"message","type":"plan","content":"# 方案\n\n- 使用 PostgreSQL","source":"fake","source_key":"turn-1:plan"}v2 讨论只持久化 user_input、question、plan 和完整 final。thinking、tool_use、tool_result、usage、session 和控制事件不会进入讨论详情。
所有消息事件都应带:
| 字段 | 说明 |
|---|---|
source | Provider 自己的稳定来源名,例如 fake-provider 或上游 CLI 名称。 |
source_key | 当前事件的稳定幂等键。必须在同一 run 内唯一且可重放。 |
usage 事件通过 usage 字段上报;当前实现只记录以下字段:
{
"usage": {
"provider": "fake-provider",
"model": "demo",
"input_tokens": 12,
"output_tokens": 34,
"cache_read_tokens": 0,
"cache_write_tokens": 0
}
}字段说明:
| 字段 | 说明 |
|---|---|
provider | 实际计费或模型 provider 名称。 |
model | 模型名称。 |
input_tokens | 输入 token 数。 |
output_tokens | 输出 token 数。 |
cache_read_tokens | 可选,cache read token 数。 |
cache_write_tokens | 可选,cache write token 数。 |
一个事件可以同时包含可识别的 type 和 usage,但建议分开发送,便于调试。
幂等规则
Wujie reporter 会使用 source / source_key 去重和重试。插件必须提供稳定值:
source用固定 provider 名称或上游系统名称。source_key用上游事件 ID、session ID + turn ID、tool call ID、transcript 行 ID 等确定性信息。- 不要用当前时间、随机数、进程内递增计数作为唯一依据。
- 重跑或重试同一个上游事件时,应输出相同
source_key。
推荐格式:
session:<session-id>:turn:<turn-id>:user
session:<session-id>:turn:<turn-id>:tool:<tool-call-id>:use
session:<session-id>:turn:<turn-id>:tool:<tool-call-id>:result
session:<session-id>:turn:<turn-id>:final最小 fake plugin
创建目录:
mkdir -p fake-provider写入 fake-provider/provider.json:
{
"schema_version": 1,
"name": "fake-provider",
"display_name": "Fake Provider",
"command": "./wujie-localrun-fake",
"protocol": "wujie-localrun-provider.v1",
"capabilities": {
"structured_events": true,
"issue_context": "env",
"usage": true
}
}写入 fake-provider/wujie-localrun-fake:
#!/bin/sh
set -eu
if [ -z "${WUJIE_RUN_ID:-}" ]; then
echo "WUJIE_RUN_ID is required" >&2
exit 2
fi
printf '%s\n' '{"type":"user_input","content":"请验证 fake provider","source":"fake-provider","source_key":"fake:turn-1:user"}'
printf '%s\n' '{"type":"text","content":"fake provider 已启动","source":"fake-provider","source_key":"fake:turn-1:text-1"}'
printf '%s\n' '{"type":"tool_use","tool":"exec_command","input":{"command":"echo hello"},"source":"fake-provider","source_key":"fake:turn-1:tool-1:use"}'
printf '%s\n' '{"type":"tool_result","tool":"exec_command","output":"hello","source":"fake-provider","source_key":"fake:turn-1:tool-1:result"}'
printf '%s\n' '{"type":"final","content":"fake provider 验证完成。","source":"fake-provider","source_key":"fake:turn-1:final"}'
printf '%s\n' '{"usage":{"provider":"fake-provider","model":"fake-model","input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}'赋予执行权限:
chmod +x fake-provider/wujie-localrun-fake安装并查看:
wujie local-run provider install ./fake-provider
wujie local-run provider list
wujie local-run provider list --output json执行验证:
wujie run WUJ-123 -- fake-provider验收点:
wujie local-run provider install ./fake-provider成功。wujie local-run provider list能看到fake-provider。wujie run WUJ-123 -- fake-provider能产生user_input、tool_use、tool_result、final。- 插件日志只出现在 stderr,stdout 没有非 JSON 行。
手动配置开发中插件
开发时可以不复制安装目录,直接在当前 CLI config 里配置 manifest 路径:
{
"localrun_providers": {
"fake-provider": {
"manifest_path": "/Users/me/dev/fake-provider/provider.json",
"enabled": true
}
}
}这种方式适合边改 adapter 边调试。manifest_path 支持 ~ 和环境变量展开。
禁用或移除:
wujie local-run provider disable fake-provider
wujie local-run provider remove fake-provider内置 provider 不能被外部插件覆盖,也不能通过 provider 子命令禁用或移除。
开发 checklist
provider.json存在,JSON 合法。schema_version是1。protocol是受支持的 v1 或 v2,并与目标执行模型一致。name与localrun_providers配置 key 或安装目录名一致。command能从 manifest 目录解析到可执行文件。- Adapter stdout 只输出单行 JSON 事件。
- 普通日志、第三方 CLI 原始日志、调试信息全部写 stderr。
- 每个消息事件都有稳定
source和source_key。 user_input只代表用户真实输入,不代表 bootstrap prompt 或 issue context。final只代表对当前用户输入的最终回复,不写 token delta 或状态文案。- 若使用 v2,在仅有
WUJIE_LOCALRUN_SESSION_ID时也能启动、恢复并输出稳定幂等键;发现原生 session 后输出session事件。 - 若声明
native_plan_handoff,必须在真正执行前等待匹配的handoff_decision;accepted=false时不得执行方案,并应结束 adapter。 tool_use.input是 JSON object,tool_result.output是字符串。- usage 只使用当前支持的字段。
- 非 0 退出码用于表达 adapter 失败。
常见错误
| 错误 | 现象 | 修复 |
|---|---|---|
| stdout 写了普通日志 | parse localrun plugin event 失败 | 日志改写 stderr。 |
protocol 写错 | 安装或执行时报 unsupported protocol | Issue-first 使用 v1;需要讨论使用 v2。 |
v1 插件执行 wujie discuss | CLI 提示保持 Issue-first 合约 | 升级 adapter 与 manifest 到 v2。 |
schema_version 不是 1 | 安装或执行时报 unsupported schema | 固定写 1。 |
command 不可执行或路径错误 | 启动 provider 失败 | 使用相对 manifest 目录的路径,并确认权限。 |
| 插件名覆盖内置 provider | 发现 provider 时报 conflict | 换一个 name。 |
缺少稳定 source_key | 重试或断线后消息可能重复 | 使用上游事件 ID 或 session/turn/tool ID 组合。 |
把 token delta 当 final | 评论里出现碎片化回复 | 只在完整 assistant message 结束后写 final。 |
把 issue context 写成 user_input | 自动同步出多余用户评论 | 只有用户真实输入才写 user_input。 |