@coda-console/plugin-check 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -18
- package/bin/check.mjs +4 -3
- package/check/agent.mjs +29 -19
- package/check/coda/builtin.json +1273 -0
- package/check/coda/components/plugins/agent-plugins.mjs +88 -0
- package/check/coda/components/plugins/plugin-extensions.mjs +171 -0
- package/check/coda/components/plugins/plugin-rules.mjs +819 -0
- package/check/coda/components/plugins/plugin-source-rules.mjs +362 -0
- package/check/coda/components/plugins/skill-document.mjs +35 -0
- package/check/coda/components/prompt-recovery/prompt-recovery.mjs +56 -0
- package/check/coda/components/session-config/commands.mjs +111 -0
- package/check/coda/components/workflows/executor-labels.mjs +12 -0
- package/check/coda/contract.json +29 -0
- package/check/coda/domain/agent-protocol.mjs +45 -0
- package/check/coda/domain/duration.mjs +11 -0
- package/check/coda/domain/errors.mjs +11 -0
- package/check/coda/domain/values.mjs +39 -0
- package/check/coda/kernel/agent-failures.mjs +16 -0
- package/check/coda/resources/plugins/resources/panels/command.mjs +46 -0
- package/check/coda/resources/plugins/resources/panels/panels.mjs +225 -0
- package/check/coda/resources/plugins/resources/panels/rules.mjs +242 -0
- package/check/coda/resources/plugins/resources/panels/sign-in.mjs +131 -0
- package/check/index.mjs +3 -4
- package/check/leftovers.mjs +9 -3
- package/check/manifest.mjs +198 -433
- package/check/panel.mjs +49 -100
- package/check/waits.mjs +5 -4
- package/package.json +4 -2
- package/check/coda.mjs +0 -165
package/README.md
CHANGED
|
@@ -14,11 +14,14 @@ plugins/grok/ Grok:转发运行机上 Grok CLI 的 ACP,并适配
|
|
|
14
14
|
plugins/opencode/ OpenCode:在运行机的 opencode acp 与 Coda 之间转发,送入会话指令与所选模型服务,模型按来源分组
|
|
15
15
|
plugins/codex-accounts/ Codex 多账号:管理登录、自动续期与账号选择,接管 Codex 额度面板
|
|
16
16
|
plugins/cursor/ Cursor:在运行机的 cursor-agent acp 与 Coda 之间转发,送入会话指令
|
|
17
|
+
plugins/host/ 宿主资源:Xcode 构建、Apple 模拟器与项目要求排队的命令共用这台机器的一组名额(组 host、持锁执行 run-held、host-sim),面板「构建与模拟器名额」
|
|
17
18
|
plugins/magpie/ Magpie:把本机的 Magpie 网关作为模型服务提供给 Pi 等插件,安装时装好 Magpie 并让网关常驻
|
|
19
|
+
plugins/network-log/ 网络记录:只记录不拦截,每秒读连接表记下建连卡住的连接(进程、对端、网卡、mihomo 有没有收到),每分钟分段探测常用服务
|
|
18
20
|
plugins/review/ PR 生命流程:会话开的 PR 先跑项目的检查、再交叉评审,你批准后合并、部署与发布;也是写 PR 流程的样例
|
|
21
|
+
plugins/testbed/ 测试机:长插在 Mac 上的 iPhone、iPad 只经 use-testbed 使用,排到后装 App、交给会话操作、用完切黑屏;依赖宿主资源
|
|
19
22
|
templates/agent/ 最小的 Agent 插件:手写 ACP 的回声 Agent,演示会话指令、插话、Agent 活动与自主轮次
|
|
20
23
|
templates/panel/ 最小的面板插件:一张卡片、一条进度条、一个按钮
|
|
21
|
-
bin/、check/ 契约检查(仓库根的 package.json 就是它,@coda-console/plugin-check
|
|
24
|
+
bin/、check/ 契约检查(仓库根的 package.json 就是它,@coda-console/plugin-check);check/coda/ 是 Coda 同步来的规则副本,不要手改
|
|
22
25
|
test/ 检查包的测试;test/plugins/ 是各插件的测试(插件目录里只放运行要用的文件)
|
|
23
26
|
docs/agent-protocol.md Coda 接入协议正文的公开副本
|
|
24
27
|
.coda/workflows/ 本仓库自己的流程:sync-coda 等新版本发到 npm,再给 Coda 开升级依赖的 PR
|
|
@@ -71,14 +74,51 @@ Claude 追加在 `claude_code` 预设的系统提示词后面;Codex 经 `threa
|
|
|
71
74
|
(codex-acp 按 ACP 的 schema 读请求会丢掉 `systemPrompt`,入口改放在请求的 `_meta` 里交给打过补丁的 codex-acp)。
|
|
72
75
|
Kimi、Grok、OpenCode、Cursor 的 CLI 不接受它:入口不转给 CLI,在 `session/new` 建立的原生会话第一次 `session/prompt` 时,
|
|
73
76
|
把指令包在 `<coda-session-instructions>` 里作为单独的一段文本放在用户输入前面,之后与 `session/load` 恢复的会话不再送;
|
|
74
|
-
第一条是斜杠命令时留到下一条。Codex 另声明插话:一轮已经结束时收到的 `_session/steering` 回 `promptRequired`,不另开一轮。
|
|
77
|
+
第一条是斜杠命令时留到下一条。Codex 与 Claude 另声明插话:一轮已经结束时收到的 `_session/steering` 回 `promptRequired`,不另开一轮。
|
|
78
|
+
Claude 把一轮进行中的插话标上 `priority: 'next'` 交给 Claude Code,回 `injected`:Claude Code 在这一轮的下一个工具间隙把它并进去,
|
|
79
|
+
不打断正在跑的工具;这一轮没再调工具就结束时,Claude Code 紧接着为它另跑一轮,入口把那一轮也算进原来的 `session/prompt`,跑完才答复。
|
|
80
|
+
这一轮被停止或失败时,还没并进去的插话随之撤掉(`interrupt` 带 `cancelQueued`),不会在之后再跑,原来的 `session/prompt` 等撤掉了才答复。
|
|
81
|
+
结果最多列出 64 条它接收的消息,所以同时在途的插话最多 32 条,再多的回 `promptRequired`,由 Coda 在这一轮结束后作为下一轮发出。
|
|
82
|
+
|
|
83
|
+
Claude 的额度耗尽按[执行失败](docs/agent-protocol.md#额度不足)报 `quota_exhausted`,保留上游给出的重置时间;
|
|
84
|
+
只认明确的额度原因,或本轮 `rate_limit` 错误配合订阅窗口的 `rejected`,不把普通 429、登录失败或额度预警算进去。
|
|
85
|
+
SDK 作为 `assistant` 消息发来的 API 错误不当成回复正文;由本轮 `result` 报告失败。
|
|
86
|
+
后台结果或定时唤醒后的自主轮次没有 `session/prompt` 可回复,失败时以自己的活动卡片报 `failed` 和原因,额度耗尽的卡片标题为「额度不足」;
|
|
87
|
+
唤醒它的任务保留任务自己的结果,周期定时保留下次触发。
|
|
75
88
|
|
|
76
89
|
OpenCode 遇到模型服务的 429,会照 `Retry-After` 等(OpenCode 2 每次最多 15 分钟、最多 10 次),并在 ACP 里用 `session_info_update` 的
|
|
77
|
-
`_meta["opencode/retry"]`
|
|
78
|
-
|
|
90
|
+
`_meta["opencode/retry"]` 说下次什么时候重试、服务报了什么。子 Agent(OpenCode 的 `subagent`)从子会话事件报告等待,入口保留它所属的 ACP 轮次与发起调用:
|
|
91
|
+
只有当前轮次仍沿前台调用链等待它时,才按主轮次处理重试;后台、已返回、跨轮次或无法确定归属的子会话只转发状态,不取消主请求。
|
|
92
|
+
子会话事件没有发起工具的 ID,同时有多个可能的调用时,仅在它们全部仍是同一轮的前台调用时才处理,无法确定的长等待仍由 OpenCode 自己管理。
|
|
93
|
+
Coda 自己对服务故障只等一分钟以内,所以主请求或它仍在等待的前台子会话要等超过一分钟才重试时,入口先 `session/cancel` 停下这一轮(前台子 Agent 随之停下),
|
|
79
94
|
再把这一轮答成失败,原因是服务给的原文加上 OpenCode 要等多久;停下后 OpenCode 回的是什么都不转出。一分钟以内的等待照旧由 OpenCode 自己处理。
|
|
80
95
|
不在入口里按厂商的错误文字判断额度耗尽。
|
|
81
96
|
|
|
97
|
+
OpenCode 的原生子会话经 ACP 的 `opencode/child-session-updates` 能力接入(入口 `agent/children.mjs`,核对 OpenCode 2.1.4):
|
|
98
|
+
`opencode/session/child_update` 的创建与运行状态登记成 `_coda/wait`,带原生 `childSessionId`;子会话的回复、思考与工具调用各归自己的过程,
|
|
99
|
+
嵌套子会话归发起它的子会话。主会话答复以后仍保留后台子会话,收到原生完成、失败、被中断事件才了结;错误原文留在卡片详情。
|
|
100
|
+
审批从 OpenCode 附带的 `opencode/child-session` 元数据归到子会话,审批响应的身份不变。
|
|
101
|
+
停止按钮走 `session/close`:OpenCode 的这个接口中断原生会话而不删除历史;`session/cancel` 只能取消 ACP 正在执行的 prompt,不能单独停止原生子会话。
|
|
102
|
+
上游再次报告同一子会话开始执行时,用新的 `waitId` 登记同一张卡片的下一次运行。
|
|
103
|
+
|
|
104
|
+
这条接口的边界也沿用 OpenCode 2.1.4:它只跟踪当前 ACP prompt 里新建的子会话,恢复历史或在后续 prompt 续用已有子会话时不补发它们的过程;
|
|
105
|
+
后台子会话结束后主 Agent 自主继续的输出也不经此接口发来,所以入口不凭子会话完成事件自造 `handling` 轮次。
|
|
106
|
+
原生表单提问没有子会话身份元数据,仍显示在主会话。以上需要上游补充事件;普通 Coda `spawn` 的独立会话不受这些限制。
|
|
107
|
+
协议公开副本记录的是同步时的插件行为,OpenCode 当前的接入以本节为准。
|
|
108
|
+
|
|
109
|
+
OpenCode 的后台 shell 命令由随插件带的原生观察器 `runtime/server.mjs` 接入(OpenCode 2.1.4 的公开插件 API):
|
|
110
|
+
入口只在这个进程的 `OPENCODE_CONFIG_CONTENT.plugins` 里追加观察器,保留已有插件和配置,不写运行机的配置文件。
|
|
111
|
+
观察器通过私有临时目录中的 Unix socket 交回 shell 的创建、退出、结果通知的投递与原生执行结束事件;不轮询进程、日志或数据库。
|
|
112
|
+
后台工具返回前先确认 `_coda/wait` 已登记,带启动工具的 `toolCallId` 和原生输出文件,子会话起的命令归子会话。
|
|
113
|
+
工具返回 `running` 不等于命令结束;主 prompt 结束也不关闭任务。命令退出后,等原生结果通知被处理,才按退出码、超时或停止事实了结。
|
|
114
|
+
轮外接手结果时报告 `handling`,处理失败显示失败原因;停止这次自主处理走原生插件的 `session.interrupt`,等原生中断事件才结束,并保留 ACP 会话供下一轮继续。
|
|
115
|
+
观察器被 OpenCode 配置禁止加载或事件连接断开时,卡片明确显示结果未确认,不伪造成功。
|
|
116
|
+
|
|
117
|
+
这部分仍有上游边界:公开插件 API 没有独立取消 shell 的操作,因此运行中的命令不显示单独停止按钮;
|
|
118
|
+
原生自动续跑的正文、工具、审批和提问仍未由 ACP 转来,观察器只补足活动状态,不自行拼装另一套对话协议。
|
|
119
|
+
任务的结果文件可查看原始命令输出。有限的后台实验应直接用 `background: true`,不要再套 `nohup … &`:
|
|
120
|
+
原生生命周期对应外层 shell,不能据此断言已经脱离它的后代进程也完成。观察器在 shell 工具说明里补充了这一点。
|
|
121
|
+
|
|
82
122
|
Kimi、Grok、OpenCode、Cursor 带设置动作(`contributes.setup`,`scripts/setup.mjs`),用户安装或更新插件时 Coda 在每台机器上运行:
|
|
83
123
|
CLI 不在或运行不了就用它自己的安装程序安装,低于插件要求的版本就升级,较新的版本保留。要求的版本:Kimi 2.1.1(入口读得懂的会话文件布局)、
|
|
84
124
|
Grok 1.0.52(审批模式通知核对过的版本)、OpenCode 2.1.4(入口核对过的版本;OpenCode 1 遇到模型服务的 `Retry-After` 会一声不响地等,多久都等,见下文。用 OpenCode 2 自己的安装程序 `https://opencode.ai/v2/install`,装在同一个 `~/.opencode/bin`,OpenCode 1 的会话由它按原 ID 接过去);Cursor 没有已知的下限,能运行就行(它自己会更新)。
|
|
@@ -97,7 +137,11 @@ Claude、Codex 与 OpenCode 各有一项设置「模型服务」(`claude.model
|
|
|
97
137
|
勾了别的接口的服务(旧设置留下的),入口只在标准错误记一行,不列它的模型。
|
|
98
138
|
- **Claude / Codex 模型**:会话打开时从每个服务的「地址/models」列出模型(各服务同时问),问法同 Coda 的「检查可用模型」(Claude 带 `x-api-key`,Codex 带 `Authorization: Bearer`),
|
|
99
139
|
按勾选的先后排在 Agent 自己的模型后面:ID 是 `<服务>/<模型>`(如 `magpie/anthropic/deepseek/deepseek-v4-pro`),名字只是模型(如 `deepseek/deepseek-v4-pro`),
|
|
100
|
-
|
|
140
|
+
在服务那一组里,没有推理强度可选。Codex 的分组在 codex-acp 生成模型配置项的地方打补丁做。
|
|
141
|
+
- **列不出来时**(三个插件一样):每个服务列出的模型记在插件状态目录(`CODA_PLUGIN_STATE`)的 `service-models/` 下(文件名是服务名,`/` 换成 `.`),
|
|
142
|
+
有变化才重写;某次问不到(那台机器在重启、网络暂时不通、超时)就沿用上次列出的,在标准错误记一行,那一组照常出现,
|
|
143
|
+
选它的模型时请求照常失败。从没列出过、或后端没有状态目录的,这次就没有它那一组,其他的照旧。
|
|
144
|
+
这样 Coda 的模型目录(读到后保留一小时)不会因为一次没问到就整整一小时缺这一组。
|
|
101
145
|
- **Claude / Codex 何时换**:选模型只是记下(Coda 读模型目录时会把每个模型都选一遍,不该因此启动什么);下一轮开始前,所选模型的服务与会话正在用的不同(换到另一个服务、或回到自己的),才把会话换过去。
|
|
102
146
|
- **Claude**:以 `resume` 新起一个 SDK 会话(还没说过话的会话用同一个会话 ID 新起),flag settings 的 `env` 设 `ANTHROPIC_BASE_URL`、
|
|
103
147
|
`ANTHROPIC_AUTH_TOKEN`(服务的 Key,没有 Key 写 `none`)与 `ANTHROPIC_MODEL`、`ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL`、
|
|
@@ -118,7 +162,7 @@ Claude、Codex 与 OpenCode 各有一项设置「模型服务」(`claude.model
|
|
|
118
162
|
切模型不用重启;服务的标题作为 Provider 名。入口把 OpenCode 报给 Coda 的模型配置项按 Provider 分组(OpenCode 写作「Provider ID/模型名」,
|
|
119
163
|
组内去掉前缀):服务的组名是它完整的标题,OpenCode 自己的 Provider 各成一组,组名是 Provider ID(OpenCode 只报这个);模型 ID 不变。Chat 用 `@ai-sdk/openai-compatible`,Responses 用 `@ai-sdk/openai`,Anthropic 用 `@ai-sdk/anthropic`,
|
|
120
164
|
都是 OpenCode 自带的 SDK。Anthropic 请求模型列表用服务给的地址,生成请求的地址若未以 `/v1` 结尾则补上。
|
|
121
|
-
Key 只用服务的;无 Key 时显式传 `none`,避免 SDK
|
|
165
|
+
Key 只用服务的;无 Key 时显式传 `none`,避免 SDK 回退到机器自己的环境变量凭据。接口不支持或 Key 文件读不到,就不添加那一个;列不出模型时同上沿用上次列出的,从没列出过的才不添加。其他的与原来的模型照旧。
|
|
122
166
|
已有的 inline 配置支持 JSONC(注释、尾逗号),合并时保留用户自己的变量与字符串转义;服务生成片段中的 `{` 在 JSON 字符串内编码成 `\u007b`,
|
|
123
167
|
防止模型 ID、标题等服务数据触发 OpenCode 的 `{env:…}` / `{file:…}` 展开。真正无效的配置仍明确报错。机器与项目的 Provider 启停限制也仍然有效。
|
|
124
168
|
用户未加引号的布尔值、数字等变量节点在解析时暂用等长占位节点,合并后原样还原,由 OpenCode 自己展开;合并器不读取它们引用的环境变量或文件。
|
|
@@ -127,7 +171,33 @@ Claude、Codex 与 OpenCode 各有一项设置「模型服务」(`claude.model
|
|
|
127
171
|
`/models` 没有上下文与输出上限时暂用 32768 / 4096 tokens,
|
|
128
172
|
服务给出 `context_length` / `max_output_tokens` 时用它们;不根据模型名猜测图片与推理能力。
|
|
129
173
|
真 CLI 的本地验证(不使用实际模型服务与登录):`OPENCODE_TEST_BINARY=/absolute/path/to/opencode node --test test/plugins/opencode/native.test.mjs`,
|
|
130
|
-
覆盖三种接口的请求、服务与原生模型切换、进程重启后恢复,服务模型的变量模板、用户 JSONC 和 Provider 对象模板配置,以及主会话与子 Agent 里长短两种 `Retry-After
|
|
174
|
+
覆盖三种接口的请求、服务与原生模型切换、进程重启后恢复,服务模型的变量模板、用户 JSONC 和 Provider 对象模板配置,以及主会话与子 Agent 里长短两种 `Retry-After`;
|
|
175
|
+
原生子会话另覆盖过程与工具、审批归属、后台完成、独立停止和嵌套父子关系。模型服务全由本地 HTTP fixture 提供。
|
|
176
|
+
|
|
177
|
+
### Claude 缓存过期前压缩
|
|
178
|
+
|
|
179
|
+
Claude 的设置「缓存过期前压缩」(`claude.idleCompaction`,默认关):会话停下后还要接着用时,在主线程的一小时提示词缓存过期前压缩一次上下文,
|
|
180
|
+
同 `/compact`。摘要趁缓存还热时生成,回来接着做的那一轮只需缓存摘要,不必按原价重新缓存整段对话。判断全在插件的流程里,Agent 只报数据、只执行。
|
|
181
|
+
|
|
182
|
+
- **流程**:插件自带的生命周期流程「Claude 缓存过期前压缩」(`.coda/workflows/claude-compaction.bpmn`)以 `coda:agents="claude-sdk"`
|
|
183
|
+
只跟随 Claude SDK 的会话,一个会话一条运行,平时不占内存,只在会话事实变化或自己的计时到点时被唤醒;做了什么、为什么没做,都记在运行里。
|
|
184
|
+
它只读会话事实、只请 Agent 压缩,不改会话自己的状态:不登记活动、不改等待、不发布阶段。
|
|
185
|
+
- **条件**:设置打开;进程在、没在跑一轮;还要接着用(置顶了,或在等你、等 Coda 跑的流程与子会话、等 Agent 自己的后台命令、子 Agent 与定时:
|
|
186
|
+
事实 `pinned`、`input`、`waitsForSystem`、`background`、`scheduled`);缓存是一小时的、上下文不少于 15 万 token;最后一次请求之后没有压缩过
|
|
187
|
+
(最近一件事已经是压缩就不再压,不管是谁压的),也还没为这次请求请过。照 Claude Code 2.1.296 自带的空闲压缩
|
|
188
|
+
(启不启用由 Anthropic 的远程开关定,默认关,设置 `idleCompaction` 只能关不能开):在缓存寿命的 9/10,即最后一次请求后 54 分钟,执行动作
|
|
189
|
+
`compact-session`;期间事实变了就重看,最后一次请求没变时期限不变;到点已过 58 分钟(机器睡过、后端停过,缓存多半已失效)就不压。
|
|
190
|
+
Claude Code 那边还看额度是否接近上限,Coda 看不到额度,这里不看。
|
|
191
|
+
- **Agent 报什么**:每轮结束时,入口在 `usage_update` 的 `_meta.coda` 里带上主线程缓存的情况,Coda 原样作为会话事实 `usage`:
|
|
192
|
+
`lastRequestAt`(最后一次请求模型的时刻,缓存最后被用到;压缩那一轮不算)、`contextTokens`(那次请求带的上下文 token 数)、
|
|
193
|
+
`cacheTtl`(`PT1H` 或 `PT5M`,从回复写缓存的明细看出)、`compactedAt`(最近一次压缩,Claude Code 自动的、`/compact` 的、Coda 请的都算)。
|
|
194
|
+
`usage_update` 的 `used` 是这一轮所有请求加起来的,不是上下文大小。
|
|
195
|
+
- **压缩那一轮**:入口在 `initialize` 的 `agentCapabilities._meta.coda` 里声明 `compact: {}`,收到 `_coda/session/compact`(`{ sessionId }`,
|
|
196
|
+
两轮之间也可以发)就往 SDK 的输入送一条 `/compact`。这一轮不归 Coda 的哪个 `session/prompt`,也不登记活动;Coda 只看到 `compaction_update`,
|
|
197
|
+
命令输出不转成回复。期间 Coda 发来的一轮排在它后面照常回复,插话回 `promptRequired`。压完回答 `{ compacted: true, before, after }`(前后的
|
|
198
|
+
上下文 token 数);会话没在运行、正在跑一轮、压缩失败或没压成,回 `{ compacted: false, reason }`。
|
|
199
|
+
- 会话事实 `pinned`、`usage`,动作 `compact-session` 与流程属性 `coda:agents` 由 Coda 提供(见 Coda 仓 docs/workflows.md「生命周期流程」与
|
|
200
|
+
docs/agent-protocol.md),Coda 还没有这些时,这条流程读不进来,不会运行。
|
|
131
201
|
|
|
132
202
|
### Grok 审批模式
|
|
133
203
|
|
|
@@ -225,7 +295,7 @@ Coda 根上的同一条 overrides 由 `sync-coda` 一起改(npm 只认根的
|
|
|
225
295
|
**清单**(`package.json`)
|
|
226
296
|
|
|
227
297
|
- `name` 是包名;另写 `id` 时以它为 Coda 插件 ID,否则以 `name` 为 ID。ID 只能用小写字母、数字和 `-`,以字母开头,至多 40 个字符。
|
|
228
|
-
本仓库的包名是 `@coda-console/<id>`,设置名与 Agent ID 仍使用原来的插件 ID。可以与 Coda
|
|
298
|
+
本仓库的包名是 `@coda-console/<id>`,设置名与 Agent ID 仍使用原来的插件 ID。可以与 Coda 内置插件同名;从其他来源替换时,须先在目标机器卸载内置插件。契约检查照刚装好的 Coda 读:与它默认装着的内置插件同名、又不是那个插件的同一来源的,判不通过。
|
|
229
299
|
- `displayName`、`description`、`version` 显示在安装确认与插件页。
|
|
230
300
|
- `contributes` 只能有 Coda 认识的贡献:`agents`、`panels`、`configuration`、`capabilities`、`instructions`、`extensionPoints`、`extensions`、`groups`、`commands`、`services`、`setup`;读不懂的键一律报错,不会被悄悄忽略。
|
|
231
301
|
- `setup` 是插件的设置动作:`{ "command": "…" }`,一行、不超过 4000 字(与面板命令同样的规则),没有别的键。
|
|
@@ -309,8 +379,10 @@ gh workflow run publish.yml --repo coda-console/coda-plugins --ref main
|
|
|
309
379
|
|
|
310
380
|
## 契约检查
|
|
311
381
|
|
|
312
|
-
扮演 Coda
|
|
313
|
-
|
|
382
|
+
扮演 Coda 去读一个插件目录:清单用 Coda 自己的代码读,Agent 照 Coda 的样子启动、说 Coda 说的话,面板由 Coda 的资源插件模块读一次。
|
|
383
|
+
规则只在 Coda 里写:`check/coda/` 是 Coda 代码的原样副本(见[契约副本](#契约副本)),检查包手写的只有怎样扮演 Coda、怎样判。
|
|
384
|
+
清单照「从仓库添加…」的顺序读,Coda 在第一处拒绝就停,检查也只报这一处;扮演的是一台刚装好的 Coda(只有它自带与默认装的插件),
|
|
385
|
+
装进别的 Coda 时版本或已装的插件不同,以那台的读取为准。
|
|
314
386
|
|
|
315
387
|
```sh
|
|
316
388
|
npm ci
|
|
@@ -322,8 +394,8 @@ npx github:coda-console/coda-plugins#<提交> <插件目录>
|
|
|
322
394
|
|
|
323
395
|
| 级 | 要不要登录 | 查什么 |
|
|
324
396
|
| --- | --- | --- |
|
|
325
|
-
| 清单(总是) | 不要 | `package.json
|
|
326
|
-
| 握手(默认) | 不要 | `sh -c` 启动 `command
|
|
397
|
+
| 清单(总是) | 不要 | Coda 读仓库的顺序:目录扫得到、名字合规;目录里无符号链接与子模块、不超过 10 MiB(`treeProblem`);`readPlugin` 读得成插件;带 npm 依赖要有对得上的 `package-lock.json`(`manifestProblem`);与刚装好的 Coda 的插件不冲突、要一起装的找得到(`candidate`:默认装着的内置插件的 ID、别的插件声明的 Agent、组、扩展点等)。本仓库 `plugins/<id>` 里的内置插件按内置插件的同一来源读(新版本可以盖过它)。另有只提醒的:分类、版本、图标页面太小、旧的 Agent ID `claude`、没提交的文件。面板声明由资源插件的 `panelEntries` 读 |
|
|
398
|
+
| 握手(默认) | 不要 | `sh -c` 启动 `command`(环境是 Coda 的 `pluginEnv` 给的:`CODA_NODE`、`CODA_PLUGIN_PATH`、临时的 `CODA_PLUGIN_STATE`、设置默认值的 `CODA_PLUGIN_SETTINGS`;依赖的插件在同一仓库里时 `CODA_PLUGIN_DEPENDENCIES` 给它的目录,Coda 自带的不在这里、不给),发与 Coda 相同的 `initialize`,要求 `protocolVersion` 为 1、声明会话指令(`sessionCapabilities.systemPrompt`,没声明算失败);`session/new` 带会话指令,给出会话与当前模型,回 -32000 记为未登录、跳过后面的检查(不算失败);像 Coda 一样对进程组发 SIGTERM,5 秒内要退出(超过 0.8 秒提醒);另起一个进程,关掉标准输入后 5 秒内不退出只提醒。面板由 Coda 资源插件的 `Panels` 读一次(30 秒),输出合格式 |
|
|
327
399
|
| 一轮(`--live`) | 要 | 第一轮问会话指令里的检查词(一个随机词),回复里要有它;再发一轮提示词(默认「只回复 ok」)收到 `stopReason`;每条 `session/update` 过 ACP SDK 的 schema;`_coda/wait` 按 Coda 的 `validWait` 与协议的规则核对(子 Agent 不带 `childSessionId`、定时不带 `at` 只提醒);审批与提问一律回拒绝,在一轮之外发来的算失败;再发一轮并 `session/cancel`,30 秒内要结束,读到取消后还在做事或拖过 5 秒才答复却不回 `cancelled` 算失败(见下);有自主轮次的也取消一次;声明插话的,一轮结束后插话要回 `promptRequired` 且不自己另开一轮,一轮进行中插话要回 `injected` 或 `promptRequired`(`injected` 的那一轮照常回 `stopReason`);声明 `loadSession` 的,新开进程 `session/load`(同样带会话指令)要成功,随后在新进程里再跑一轮也要收到 `stopReason` |
|
|
328
400
|
|
|
329
401
|
**取消后的 `stopReason` 怎么判**:检查发 `session/cancel` 时紧跟一个只会得到 -32601 的探测请求(`_codaCheck/barrier`)。Agent 按顺序读,
|
|
@@ -343,16 +415,18 @@ npx github:coda-console/coda-plugins#<提交> <插件目录>
|
|
|
343
415
|
- run: npx --yes github:coda-console/coda-plugins#<提交> . --live
|
|
344
416
|
```
|
|
345
417
|
|
|
346
|
-
##
|
|
418
|
+
## 契约副本
|
|
347
419
|
|
|
348
|
-
Coda
|
|
420
|
+
Coda 主仓库不公开。它的插件规则(读清单、读面板、插件进程的环境、ACP 与 `_coda/wait` 的约定等,按 Coda `backend/src/` 下的路径放在 `check/coda/`)、
|
|
421
|
+
它自带的插件(`check/coda/builtin.json`)与协议正文 `docs/agent-protocol.md` 都是 Coda 的原样副本,由 Coda 的部署流程 `sync-plugin-check`
|
|
422
|
+
(脚本 `scripts/plugin-contract.mjs`)同步:它在分支 `sync/coda` 上给这里开 PR。**不要手改**;规则要改先改 Coda。
|
|
423
|
+
`check/coda/contract.json` 记着同步自哪个提交、每个文件的 sha256 和副本用到的包的版本,CI 用它核对:
|
|
349
424
|
|
|
350
425
|
```sh
|
|
351
|
-
node scripts/
|
|
352
|
-
node scripts/sync-protocol.mjs --check # CI:副本声明的 Coda 提交、ACP 协议版本、SDK 版本与检查包一致,正文没被手改
|
|
426
|
+
node scripts/check-contract.mjs # 每个文件与 Coda 同步的一致,check/coda/ 没有别的文件,依赖版本与 Coda 相同
|
|
353
427
|
```
|
|
354
428
|
|
|
355
|
-
Coda
|
|
429
|
+
这里手写的只有扮演 Coda 的部分(`check/*.mjs`):照 Coda 的样子启动 Agent、说 Coda 说的话,以及怎样判、怎样说。
|
|
356
430
|
|
|
357
431
|
## 开发
|
|
358
432
|
|
|
@@ -363,7 +437,7 @@ npm test # node --test 'test/**/*.test.mjs':
|
|
|
363
437
|
node bin/check.mjs templates/agent --live --prompt "wait 看看自主轮次"
|
|
364
438
|
```
|
|
365
439
|
|
|
366
|
-
|
|
440
|
+
检查包的依赖是契约副本用到的包,版本与 Coda 后端相同(`check/coda/contract.json` 的 `packages`:`@agentclientprotocol/sdk`、`yaml`、`@0dep/piso`);`zod` 是 SDK 的 peer 依赖。
|
|
367
441
|
`test/plugins/` 的测试从插件目录自己的 `node_modules` 找依赖,没装就报错提示先跑 `install-plugins.mjs`;
|
|
368
442
|
`test/plugins/contract.test.mjs` 用假的对端(Claude 的假 SDK、Codex 的假 app-server、充当 `kimi acp` 的假 Agent)对真实入口跑检查包的三级,不用登录。
|
|
369
443
|
CI 先装各插件的依赖再跑全部测试,`plugins/*` 只查清单:真实的握手与一轮要各家的 CLI 与登录,不在 CI 跑。
|
package/bin/check.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import pkg from '../package.json' with { type: 'json' };
|
|
6
|
-
import { ACP_PROTOCOL_VERSION
|
|
6
|
+
import { ACP_PROTOCOL_VERSION } from '../check/agent.mjs';
|
|
7
|
+
import contract from '../check/coda/contract.json' with { type: 'json' };
|
|
7
8
|
import { checkPlugin } from '../check/index.mjs';
|
|
8
9
|
import { format } from '../check/report.mjs';
|
|
9
10
|
|
|
@@ -33,6 +34,6 @@ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => process.exi
|
|
|
33
34
|
const dir = resolve(positionals[0]);
|
|
34
35
|
const { report, plugin } = await checkPlugin(dir, { agent: values.agent ?? null, manifestOnly: !!values.manifest, live: !!values.live, prompt: values.prompt });
|
|
35
36
|
const shown = positionals[0];
|
|
36
|
-
console.log(format(report, { title: `coda-plugin-check ${pkg.version} · 插件 ${plugin?.id ?? '?'}(${shown})·
|
|
37
|
-
console.log('
|
|
37
|
+
console.log(format(report, { title: `coda-plugin-check ${pkg.version} · 插件 ${plugin?.id ?? '?'}(${shown})· 规则是 Coda ${contract.commit.slice(0, 7)} 的代码 · ACP 协议版本 ${ACP_PROTOCOL_VERSION}` }));
|
|
38
|
+
console.log('清单照一台刚装好的 Coda 读(只有它自带与默认装的插件);装进别的 Coda 时,版本或已装的插件不同,以那台「设置 › 插件 › 从仓库添加…」的读取为准。');
|
|
38
39
|
process.exit(report.ok ? 0 : 1);
|
package/check/agent.mjs
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
// The handshake and live levels for one agent: the checker plays Coda (backend/src/adapters/acp.mjs AcpSession), starts the
|
|
2
2
|
// agent's command the way Coda does, says what Coda says, and holds what the agent says to ACP's schema and to
|
|
3
3
|
// docs/agent-protocol.md. Where Coda would quietly drop or refuse something, the check fails and says what Coda does.
|
|
4
|
+
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
|
4
5
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
5
6
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
7
|
import { mkdirSync } from 'node:fs';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import { cleared } from './coda/components/plugins/plugin-rules.mjs';
|
|
10
|
+
import { RETRY_DELAYS, continuationPrompt, serviceFailure } from './coda/components/prompt-recovery/prompt-recovery.mjs';
|
|
11
|
+
import { NOT_STEERED, OPEN_LIMIT, SIGNED_OUT, STEERED, STEERING, STOP_LIMIT, TERM_GRACE, clientCapabilities, currentModel, steeringParams, supportsSteering, systemPrompt,
|
|
12
|
+
takesInstructions } from './coda/domain/agent-protocol.mjs';
|
|
13
|
+
import { agentErrorMessage } from './coda/kernel/agent-failures.mjs';
|
|
10
14
|
import { temporary } from './leftovers.mjs';
|
|
11
15
|
import { AgentProcess, Exited, Timeout } from './process.mjs';
|
|
12
16
|
import { explain, schemas } from './schema.mjs';
|
|
13
17
|
import { Waits } from './waits.mjs';
|
|
14
18
|
|
|
19
|
+
// The ACP version Coda speaks: the SDK's, at the version Coda pins (check/coda/contract.json `packages`, as package.json here).
|
|
20
|
+
export const ACP_PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
21
|
+
// What Coda answers an agent request it has no handler for (JSON-RPC's, as the SDK sends it): it handles only approvals and forms.
|
|
22
|
+
const METHOD_NOT_FOUND = -32601;
|
|
23
|
+
// What an agent answers a message sent into a turn: it joined the turn, or the turn had ended.
|
|
24
|
+
const STEERING_OUTCOMES = [STEERED, NOT_STEERED];
|
|
15
25
|
// `exit`: how long an agent may take to go after SIGTERM, or after its input ends. `cancelReply`: how soon a turn whose work
|
|
16
26
|
// was done answers once it has read the cancel. `pause`: a quiet moment between two turns, as between two messages of a
|
|
17
27
|
// person, for what an agent says after a turn to be heard outside it. `term`: how long Coda's session host waits after
|
|
18
28
|
// SIGTERM before SIGKILL. `steer`: how long a steering request may take to be answered; `steerQuiet`: how long an agent that
|
|
19
29
|
// answered one outside a turn is watched for a turn of its own.
|
|
20
|
-
export const LIMITS = { open: OPEN_LIMIT, turn: 180_000, settle: 60_000, cancel:
|
|
30
|
+
export const LIMITS = { open: OPEN_LIMIT, turn: 180_000, settle: 60_000, cancel: STOP_LIMIT, exit: 5_000, cancelDelay: 500, cancelReply: 5_000,
|
|
21
31
|
pause: 500, term: TERM_GRACE, retryDelays: RETRY_DELAYS, steer: 30_000, steerQuiet: 2_000 };
|
|
22
32
|
// A request of the checker's own, sent right behind a `session/cancel`; nothing answers it but an error (Method not found).
|
|
23
33
|
// The agent reads it after the cancel, so its answer is written after the agent has read the cancel.
|
|
@@ -38,13 +48,13 @@ const STEER_TURN = '从 1 数到 20,每个数字单独一行。', STEER_TEXT =
|
|
|
38
48
|
const secs = ms => (ms / 1000).toFixed(1);
|
|
39
49
|
const short = (text, max = 80) => (text.length > max ? `${text.slice(0, max)}…` : text).replace(/\s+/g, ' ');
|
|
40
50
|
|
|
41
|
-
// The environment Coda starts the agent with: the session's (here the checker's), and its plugin's
|
|
42
|
-
//
|
|
43
|
-
const agentEnv = (
|
|
51
|
+
// The environment Coda starts the agent with: the session's (here the checker's), and what Coda gives its plugin's processes
|
|
52
|
+
// (`checked.env`, components/plugins/plugin-rules.mjs pluginEnv) with the state directory made for it.
|
|
53
|
+
const agentEnv = (checked, base) => ({ ...cleared(process.env), ...checked.env(join(base, 'state')) });
|
|
44
54
|
|
|
45
55
|
// One process of the agent with the session it opens, and what it says, taken in order.
|
|
46
56
|
class Session {
|
|
47
|
-
constructor({ agent,
|
|
57
|
+
constructor({ agent, checked, base, say, limits, instructions }) {
|
|
48
58
|
this.say = say; this.limits = limits; this.instructions = instructions;
|
|
49
59
|
this.waits = new Waits({ fail: (rule, message) => this.at.fail(rule, message), warn: (rule, message) => this.at.warn(rule, message) });
|
|
50
60
|
this.turn = null; this.autonomous = null; this.autonomousTurns = 0; this.loading = true; this.pendingWaits = [];
|
|
@@ -53,7 +63,7 @@ class Session {
|
|
|
53
63
|
this.cwd = join(base, 'work');
|
|
54
64
|
for (const dir of [this.cwd, join(base, 'state')]) mkdirSync(dir, { recursive: true });
|
|
55
65
|
this.proc = new AgentProcess({ command: agent.command, cwd: this.cwd,
|
|
56
|
-
env: agentEnv(
|
|
66
|
+
env: agentEnv(checked, base),
|
|
57
67
|
onMessage: message => { this.receive(message); this.changed(); }, onNoise: (line, why) => this.noise(line, why) });
|
|
58
68
|
this.proc.exited.then(() => this.changed());
|
|
59
69
|
}
|
|
@@ -309,8 +319,8 @@ class Session {
|
|
|
309
319
|
// The end of its input, in a process of its own once it has answered `initialize`: what an agent sees when whatever runs
|
|
310
320
|
// it is gone without stopping it (Coda's session host killed outright). Coda stops agents with SIGTERM and does not rely on
|
|
311
321
|
// it, so one that stays is only worth a look.
|
|
312
|
-
async function inputEnd({ agent,
|
|
313
|
-
const proc = new AgentProcess({ command: agent.command, cwd: join(base, 'work'), env: agentEnv(
|
|
322
|
+
async function inputEnd({ agent, checked, base, limits, say }) {
|
|
323
|
+
const proc = new AgentProcess({ command: agent.command, cwd: join(base, 'work'), env: agentEnv(checked, base) });
|
|
314
324
|
try {
|
|
315
325
|
try { await proc.request('initialize', { protocolVersion: ACP_PROTOCOL_VERSION, clientCapabilities: clientCapabilities() }, { timeout: limits.open }); }
|
|
316
326
|
catch { return; }
|
|
@@ -344,7 +354,7 @@ async function steering(session, limits, say) {
|
|
|
344
354
|
if (session.proc.exit) return;
|
|
345
355
|
await delay(limits.steerQuiet);
|
|
346
356
|
const work = session.idleWork.slice(before);
|
|
347
|
-
if (late.outcome !==
|
|
357
|
+
if (late.outcome !== NOT_STEERED) say.fail('live.steering', `一轮已经结束时发 ${STEERING},回了 ${answer(late)}:要回 promptRequired(reason noRunningTurn),消息不收、也不自己另开一轮;Coda 把其他答复记为结果未知,不再把消息作为下一轮发出`);
|
|
348
358
|
else if (work.length) say.fail('live.steering', `一轮已经结束时发 ${STEERING},回了 promptRequired,随后却自己说起话来(${work.length} 条 session/update:${[...new Set(work)].join('、')}):promptRequired 的消息没被接收,Agent 不得自己另开一轮,Coda 会把它作为下一轮发出`);
|
|
349
359
|
else say.pass('live.steering', `一轮已经结束时插话:回 promptRequired${late.reason ? `(${late.reason})` : ''},${secs(limits.steerQuiet)} 秒里没有自己另开一轮`);
|
|
350
360
|
await delay(limits.pause);
|
|
@@ -353,7 +363,7 @@ async function steering(session, limits, say) {
|
|
|
353
363
|
const steered = await turn.steered;
|
|
354
364
|
if (!STEERING_OUTCOMES.includes(steered.outcome)) say.fail('live.steering', `一轮进行中发 ${STEERING},回了 ${answer(steered)}:要么 injected(加进这一轮),要么 promptRequired(这一轮已经结束);Coda 把其他答复记为结果未知`);
|
|
355
365
|
else if (!stop) return;
|
|
356
|
-
else if (steered.outcome ===
|
|
366
|
+
else if (steered.outcome === STEERED) say.pass('live.steering', `一轮进行中插话:injected,原来的 session/prompt 照常结束(stopReason ${stop},${turn.updates} 条 session/update${turn.text ? `,回复「${short(turn.text)}」` : ''})`);
|
|
357
367
|
else say.info('live.steering', `一轮进行中插话:回 promptRequired,插话到时这一轮已经结束(stopReason ${stop})`);
|
|
358
368
|
}
|
|
359
369
|
|
|
@@ -381,24 +391,24 @@ async function live(session, { prompt, word }, limits, say) {
|
|
|
381
391
|
if (!session.malformed) say.pass('live.updates', `session/update 都合 ACP schema(SDK 1.4.0):${kinds || '无'}`);
|
|
382
392
|
}
|
|
383
393
|
|
|
384
|
-
// Checks one declared agent of the plugin
|
|
385
|
-
//
|
|
386
|
-
export async function checkAgent({ agent,
|
|
394
|
+
// Checks one declared agent of the plugin as the manifest level read it (`checked`, manifest.mjs checkManifest), started as
|
|
395
|
+
// Coda starts it; `say(level)` gives the report scope for a level.
|
|
396
|
+
export async function checkAgent({ agent, checked, live: running = false, prompt = '只回复 ok', limits: given = {}, say }) {
|
|
387
397
|
const limits = { ...LIMITS, ...given }, temp = temporary('coda-plugin-check-'), base = temp.dir;
|
|
388
|
-
const scopes = { handshake: say('handshake'), live: say('live') }, sessions = []
|
|
398
|
+
const scopes = { handshake: say('handshake'), live: say('live') }, sessions = [];
|
|
389
399
|
const word = `coda-${randomBytes(3).toString('hex')}`, instructions = instructionsFor(word);
|
|
390
400
|
try {
|
|
391
|
-
const session = new Session({ agent,
|
|
401
|
+
const session = new Session({ agent, checked, base, say: scopes, limits, instructions });
|
|
392
402
|
sessions.push(session);
|
|
393
403
|
const opened = await session.open();
|
|
394
404
|
if (opened === 'ok' && running) await live(session, { prompt, word }, limits, scopes.live);
|
|
395
405
|
else if (running) scopes.live.skip('live.skipped', opened === 'signed-out' ? '未登录,一轮跳过' : '握手没过,一轮跳过');
|
|
396
406
|
else scopes.live.skip('live.skipped', '一轮要 --live(会真的发一条提示词,可能要登录、花额度)');
|
|
397
407
|
await session.finish(scopes.handshake);
|
|
398
|
-
if (opened !== 'failed') await inputEnd({ agent,
|
|
408
|
+
if (opened !== 'failed') await inputEnd({ agent, checked, base, limits, say: scopes.handshake });
|
|
399
409
|
if (opened !== 'ok' || !running) return;
|
|
400
410
|
if (!session.canLoad) { scopes.live.skip('live.load', '没声明 loadSession:Coda 不会要求恢复,session/load 不查'); return; }
|
|
401
|
-
const again = new Session({ agent,
|
|
411
|
+
const again = new Session({ agent, checked, base, say: scopes, limits, instructions });
|
|
402
412
|
sessions.push(again);
|
|
403
413
|
again.live = true;
|
|
404
414
|
if (await again.open(session.sessionId) === 'ok') {
|