@coda-console/protocol 2.5.0 → 3.0.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/CHANGELOG.md +37 -0
- package/README.md +39 -11
- package/docs/agent-protocol.md +26 -21
- package/docs/protocol.md +363 -0
- package/package.json +5 -4
- package/protocol.mjs +140 -0
- package/schemas/agent/wait.json +4 -0
- package/schemas/plugin/extension-points.json +2 -0
- package/schemas/plugin/plugin.json +2 -0
- package/schemas/plugin/protocol-implementations.json +10 -0
- package/schemas/plugin/protocols.json +11 -0
- package/schemas/protocol/v1/agent.json +69 -0
- package/schemas/protocol/v1/definition.json +57 -0
- package/schemas/protocol/v1/environment.json +105 -0
- package/schemas/protocol/v1/executor.json +279 -0
- package/schemas/protocol/v1/request.json +29 -0
- package/schemas/protocol/v1/response.json +25 -0
- package/schemas/protocol/v1/run.json +75 -0
- package/schemas/protocol/v1/session.json +206 -0
- package/validate.mjs +4 -1
- package/workflow/actions.json +0 -2
- package/workflow/fields.json +1 -1
- package/workflow/keywords.mjs +6 -0
- package/workflow/moddle.json +1 -2
- package/workflow/values.json +2 -2
- package/docs/executor-protocol.md +0 -261
- package/executor.mjs +0 -21
- package/schemas/executor/v1/common.json +0 -89
- package/schemas/executor/v1/execution.json +0 -246
- package/schemas/executor/v1/executor.json +0 -98
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# 协议定义、调用与核心协议 v1
|
|
2
|
+
|
|
3
|
+
从 3.0.0 起,协议不再只是插件清单里的一组类型:一份协议有名字、记录格式和操作,插件可以定义自己的协议,也可以声明实现别人的协议。调用一条记录的操作只交给这条记录的 provider;被调用方在自己的 `coda/run@1` 记录里报告运行,调用方 watch 它,不再由调用方或被点名的一方写别人的 status。
|
|
4
|
+
|
|
5
|
+
本包定义:
|
|
6
|
+
|
|
7
|
+
- 协议定义文档的 JSON Schema 与校验:`schemas/protocol/v1/definition.json`、`@coda-console/protocol/protocol`;
|
|
8
|
+
- 统一调用信封:`schemas/protocol/v1/request.json`、`response.json`;
|
|
9
|
+
- 五个核心协议:`coda/run@1`、`coda/executor@1`、`coda/agent@1`、`coda/session@1`、`coda/environment@1`;
|
|
10
|
+
- 插件清单里的 `contributes.protocols` 与 `contributes.protocolImplementations`。
|
|
11
|
+
|
|
12
|
+
`executor` 与 `execution` 两个旧对象协议和 `@coda-console/protocol/executor` 在 3.0.0 删除;`execution` 的输入并入 `coda/executor@1` 的 `execute` 操作,运行由 `coda/run@1` 表达。
|
|
13
|
+
|
|
14
|
+
## 名字
|
|
15
|
+
|
|
16
|
+
协议名是 `命名空间/名字@大版本`:
|
|
17
|
+
|
|
18
|
+
- 命名空间与名字只用小写字母、数字和「-」。命名空间以小写字母开头,长度 1–40;名字以小写字母或数字开头,长度 1–40。
|
|
19
|
+
- 大版本是正整数,写在 `@` 后面。
|
|
20
|
+
- 短名至多 63 个字符(Kubernetes CRD 复数名的 DNS-1035 标签上限);超过的协议名不合法。协议名本身最多 61 个字符,带 `-` 的名字按展开后的短名再检查。
|
|
21
|
+
- 插件只能在自己的命名空间下定义协议;命名空间就是插件 ID。命名空间 `coda` 保留给核心协议。
|
|
22
|
+
- 同一大版本内只加可选字段、新操作;改含义、删字段或删操作就升大版本。新旧大版本可以并存,由实现声明支持哪个。
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { parseProtocolName, formatProtocolName, protocolNameProblem } from '@coda-console/protocol/protocol';
|
|
26
|
+
|
|
27
|
+
parseProtocolName('coda/run@1'); // { namespace: 'coda', name: 'run', major: 1 }
|
|
28
|
+
formatProtocolName({ namespace: 'my-plugin', name: 'task', major: 1 }); // 'my-plugin/task@1'
|
|
29
|
+
protocolNameProblem('My/run@1'); // 说明哪里不合
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 短名
|
|
33
|
+
|
|
34
|
+
给存储表、Kubernetes CRD 的复数名(`<复数>.<group>` 里的 `<复数>`)和变化流 section 用同一个确定、DNS 安全的短名。Kubernetes CRD 的复数名须是 DNS-1035 标签,至多 63 个字符,因此短名也限制在 63 个字符以内;超过的协议名不合法,不截断也不哈希。推导规则只有三条替换:
|
|
35
|
+
|
|
36
|
+
| 协议名里的字符 | 短名里写成 |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| 字面 `-` | `-0` |
|
|
39
|
+
| 命名空间与名字之间的 `/` | `-1` |
|
|
40
|
+
| 版本前的 `@` | `-2` |
|
|
41
|
+
|
|
42
|
+
其余小写字母和数字原样保留。例:
|
|
43
|
+
|
|
44
|
+
| 协议名 | 短名 |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `coda/run@1` | `coda-1run-21` |
|
|
47
|
+
| `my-plugin/tool@2` | `my-0plugin-1tool-22` |
|
|
48
|
+
| `a-/b-@9` | `a-0-1b-0-29` |
|
|
49
|
+
|
|
50
|
+
因为原协议名里的每个 `-` 都变成 `-0`,短名里出现的每个 `-` 都开始一次替换:`-0` 还原为 `-`,`-1` 还原为 `/`,`-2` 还原为 `@`,再按协议名解析。替换是前缀码,一种协议名只对应一个短名,一个短名也只对应一种协议名。短名只含小写字母、数字和 `-`,以命名空间的字母开头、以大版本的数字结尾,最长 63 个字符;`parseProtocolName` 和 `definitionProblem` 会按展开后的实际长度拒绝超长的协议名。
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import { protocolShortName } from '@coda-console/protocol/protocol';
|
|
54
|
+
|
|
55
|
+
protocolShortName('coda/run@1'); // 'coda-1run-21'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 定义文档
|
|
59
|
+
|
|
60
|
+
一份定义是一个 JSON 对象:
|
|
61
|
+
|
|
62
|
+
| 字段 | 含义 |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `name` | 协议名,必填 |
|
|
65
|
+
| `title` | 给人看的名字,可选 |
|
|
66
|
+
| `description` | 一句话说明,可选 |
|
|
67
|
+
| `spec` | 记录的 spec 的 JSON Schema,必填 |
|
|
68
|
+
| `status` | 记录的 status 的 JSON Schema,必填 |
|
|
69
|
+
| `operations` | 操作名 → `{ description?, input, result, safe? }` 或 `{ description?, input, run: true }`,必填;不定义操作写 `{}` |
|
|
70
|
+
|
|
71
|
+
`spec`、`status`、`input`、`result` 都是 JSON Schema draft 2020-12,只引用自己文档里的 `#/$defs/...`,不引用别的文件。`input` 必有。操作的输出二选一:
|
|
72
|
+
|
|
73
|
+
- `result`:立即返回结果的 schema;
|
|
74
|
+
- `run: true`:返回一个运行引用,运行记录由被调用方持有。
|
|
75
|
+
|
|
76
|
+
`safe: true` 只能和 `result` 一起写,不能和 `run` 一起写,表示只读、没有副作用、立即给结果的操作;调用方和被调用方都不按 `requestId` 留它的请求和回答(见「调用信封」)。
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"name": "my-plugin/task@1",
|
|
81
|
+
"title": "任务",
|
|
82
|
+
"spec": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {"title": {"type": "string"}},
|
|
85
|
+
"required": ["title"],
|
|
86
|
+
"additionalProperties": false
|
|
87
|
+
},
|
|
88
|
+
"status": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {"state": {"enum": ["working", "done"]}},
|
|
91
|
+
"required": ["state"],
|
|
92
|
+
"additionalProperties": false
|
|
93
|
+
},
|
|
94
|
+
"operations": {
|
|
95
|
+
"start": {
|
|
96
|
+
"input": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {"title": {"type": "string"}},
|
|
99
|
+
"required": ["title"],
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
},
|
|
102
|
+
"run": true
|
|
103
|
+
},
|
|
104
|
+
"peek": {
|
|
105
|
+
"input": {"type": "object", "maxProperties": 0},
|
|
106
|
+
"result": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {"title": {"type": "string"}},
|
|
109
|
+
"required": ["title"],
|
|
110
|
+
"additionalProperties": false
|
|
111
|
+
},
|
|
112
|
+
"safe": true
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { definitionProblem } from '@coda-console/protocol/protocol';
|
|
120
|
+
|
|
121
|
+
definitionProblem(definition); // null,或「位置:说明」
|
|
122
|
+
definitionProblem(definition, { namespace: 'my-plugin' }); // 同时核对命名空间
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 调用信封
|
|
126
|
+
|
|
127
|
+
记录引用 `{ kind, id }` 的 `kind` 是协议名,如 `coda/run@1`、`coda/executor@1`、`git/snapshot@1`、`git/source@1`;`id` 是记录的不透明 ID。Coda 自己还没定义成协议的东西(会话、流程运行等)暂用它现有的资源种类名,如 `threads`、`workflow-runs`,等后续定义成协议再改用协议名。
|
|
128
|
+
|
|
129
|
+
一次调用是:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"record": {"kind": "coda/executor@1", "id": "host"},
|
|
134
|
+
"operation": "execute",
|
|
135
|
+
"input": {"task": {"command": "npm", "args": ["test"]}},
|
|
136
|
+
"requestId": "request-1",
|
|
137
|
+
"parent": {"kind": "coda/run@1", "id": "caller-run"},
|
|
138
|
+
"awaited": true
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`parent` 是发起这次调用的一方(调用方的运行、会话或其他记录),`awaited` 表示调用方是否在等它。被调用方建运行时把这两项照抄进 `coda/run@1` 的 spec。
|
|
143
|
+
|
|
144
|
+
回答二选一:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{"result": {"accepted": true}}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{"run": {"kind": "coda/run@1", "id": "run-1"}}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`requestId` 由调用方给,重发同一个请求只做一次。`safe` 的操作例外:调用双方都不按 `requestId` 留请求和回答,重发就再读一次。读回来的是对方的数据,比如群消息、环境里的文件内容,留在调用双方的库里就成了镜像。立即结果用 `result`;慢的操作用 `run` 指向被调用方自己的 `coda/run@1` 记录,调用方 watch 这条记录取结果,被调用方不推送。取消也是一次调用(`coda/run@1` 的 `cancel` 操作),不是写别人的记录。
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { invocationRequestProblem, invocationResponseProblem } from '@coda-console/protocol/protocol';
|
|
158
|
+
|
|
159
|
+
invocationRequestProblem(request); // null 或说明
|
|
160
|
+
invocationResponseProblem(response); // 恰好有 result 或 run 之一
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 插件清单
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"id": "my-plugin",
|
|
168
|
+
"contributes": {
|
|
169
|
+
"protocols": [
|
|
170
|
+
{"name": "my-plugin/task@1", "spec": {}, "status": {}, "operations": {}}
|
|
171
|
+
],
|
|
172
|
+
"protocolImplementations": ["coda/run@1", "my-plugin/task@1"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`contributes.protocols` 是插件拥有的定义,每份都按定义文档 schema 校验;定义里的命名空间必须等于插件 ID。`contributes.protocolImplementations` 是插件实现的协议名列表,可以包含别的命名空间下的协议(包括 `coda/...`),不重复。
|
|
178
|
+
|
|
179
|
+
`manifestProblem` 校验清单的形状。命名空间约束需要插件 ID,单独用:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import { ownedProtocolsProblem } from '@coda-console/protocol/protocol';
|
|
183
|
+
|
|
184
|
+
ownedProtocolsProblem(manifest); // null 或说明
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## coda/run@1
|
|
188
|
+
|
|
189
|
+
被调用方为一次调用开出的运行记录,由被调用方持有和写入。调用方 watch 它取结果;它不会被复制回调用方的记录。
|
|
190
|
+
|
|
191
|
+
spec:
|
|
192
|
+
|
|
193
|
+
| 字段 | 含义 |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `record` | 调用的是哪条记录,`{ kind, id }` |
|
|
196
|
+
| `operation` | 操作名 |
|
|
197
|
+
| `input` | 原样保存的输入 |
|
|
198
|
+
| `requestId` | 调用方的请求 ID |
|
|
199
|
+
| `parent` | 谁让它存在:发起它的记录引用,必填 |
|
|
200
|
+
| `awaited` | 父是否在等它,布尔 |
|
|
201
|
+
|
|
202
|
+
`parent` 与 `awaited` 来自调用请求,被调用方建运行时照抄这两项。
|
|
203
|
+
|
|
204
|
+
status:
|
|
205
|
+
|
|
206
|
+
| 字段 | 含义 |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `state` | `working`、`input-required`、`completed`、`failed`、`canceled`;后三个是终态 |
|
|
209
|
+
| `result` | 完成、失败或取消时的结果:`completed`/`failed` 必带;`canceled` 可带取消前说过的话和留下的东西;`working`/`input-required` 不带 |
|
|
210
|
+
| `progress` | 进展说明,可选 |
|
|
211
|
+
| `awaiting` | 等输入时那张卡片的引用;`input-required` 必填,其他状态不写 |
|
|
212
|
+
|
|
213
|
+
`failed` 的 `result` 约定为 `{ code, message }`。`code: "unknown"` 表示结局不明(例如执行连接中断),调用方据此显示「结果未知」,不当作失败。
|
|
214
|
+
|
|
215
|
+
终态是最后的写入:`runStatusProblem(previous, next)` 在 `previous` 已是终态时返回说明,同一状态再写也不行。进行中的状态可以互相转换(`working` 与 `input-required`),也可以进入任一终态。
|
|
216
|
+
|
|
217
|
+
操作 `cancel` 的输入是空对象,立即返回空结果;是否真的停下看 status。
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"spec": {
|
|
222
|
+
"record": {"kind": "coda/executor@1", "id": "host"},
|
|
223
|
+
"operation": "execute",
|
|
224
|
+
"input": {"task": {"command": "npm", "args": ["test"]}},
|
|
225
|
+
"requestId": "request-1",
|
|
226
|
+
"parent": {"kind": "coda/run@1", "id": "parent-run"},
|
|
227
|
+
"awaited": true
|
|
228
|
+
},
|
|
229
|
+
"status": {"state": "working", "progress": "准备中"}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## coda/executor@1
|
|
234
|
+
|
|
235
|
+
一台机器上的执行环境。记录格式取自 3.0.0 之前的 `executor`,版本改由协议名表达,spec 不再写 `protocolVersion`:spec 是 `enabled`、`capacity`、`labels` 和可选的 `title`;status 是探测到的标签、能力、容量与占用、`accepting`、观察时间和 `executionFeatures`。空 status `{}` 表示尚未观察到。
|
|
236
|
+
|
|
237
|
+
操作 `execute` 的输入是一份执行工作:`source`、`requirements`、`resources`、`exclusiveResources`、`context`、`task`、`workingDirectory`、`env`、`timeoutMs`、`cancelGraceMs`。它返回 `{ run: { kind, id } }`,运行记录由执行器持有。不再有 `target`、`executor` 或 `assignmentId` 绑定,也不回写调用方的记录;取消走运行记录的 `cancel` 操作。
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"spec": {"enabled": true, "capacity": {"slots": 2}, "labels": ["build-lab"], "title": "Mac mini"},
|
|
242
|
+
"status": {}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## coda/agent@1
|
|
247
|
+
|
|
248
|
+
一台机器上装着的 Agent 程序,记录格式照 Coda 的 Agent 资源 spec:
|
|
249
|
+
|
|
250
|
+
| 字段 | 含义 |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `agent` | Agent 的 ID,必填 |
|
|
253
|
+
| `title` | 给人看的名字 |
|
|
254
|
+
| `defaultModel` | 不指定模型时用的模型 ID;读到目录前为空 |
|
|
255
|
+
| `models` | `[{ id, name?, shortName?, group?, efforts, options }]` |
|
|
256
|
+
| `modes` | `[{ id, name?, shortName? }]` |
|
|
257
|
+
|
|
258
|
+
`models[].efforts` 是 `[{ id, name?, shortName? }]`;`models[].options` 是 `{ id, name?, shortName?, type: "boolean"|"select", choices? }`,`select` 带 `choices`,`boolean` 不带。旧 spec 里的 `plugin` 字段删除,由记录的 provider 表达。status 是空对象;本版本不定义操作。
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"spec": {
|
|
263
|
+
"agent": "codex",
|
|
264
|
+
"title": "Codex",
|
|
265
|
+
"defaultModel": "gpt-5",
|
|
266
|
+
"models": [{"id": "gpt-5", "efforts": [], "options": []}],
|
|
267
|
+
"modes": [{"id": "auto", "name": "自动"}]
|
|
268
|
+
},
|
|
269
|
+
"status": {}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## coda/session@1
|
|
274
|
+
|
|
275
|
+
插件提供的外部会话,比如一个微信群:参与者、消息和发消息。
|
|
276
|
+
|
|
277
|
+
spec:
|
|
278
|
+
|
|
279
|
+
| 字段 | 含义 |
|
|
280
|
+
| --- | --- |
|
|
281
|
+
| `title` | 给人看的名字 |
|
|
282
|
+
| `container` | `{ id, title }`:Provider 给的容器,例如插件登录的微信号;列表按 provider 与 `container.id` 分组 |
|
|
283
|
+
| `environment` | 可选,`{ kind: "coda/environment@1", id }`:外部会话自带的环境 |
|
|
284
|
+
|
|
285
|
+
status:
|
|
286
|
+
|
|
287
|
+
| 字段 | 含义 |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| `head` | 最新一条消息的游标,不透明;Provider 每追加一条就写,还没有消息时不写 |
|
|
290
|
+
| `participants` | `[{ id, name, kind: "person"\|"agent"\|"bot", self? }]`;`self: true` 是这个 Provider 登录的账号,即 Coda 的 Bot |
|
|
291
|
+
| `state` | 可选,`active` 或 `unreachable` |
|
|
292
|
+
| `reason` | 可选,状态说明 |
|
|
293
|
+
|
|
294
|
+
观察方 watch 这条记录,`head` 变了就用 `read` 读游标之后的消息。两个操作都是立即结果,不开运行;`read` 是 `safe` 的:
|
|
295
|
+
|
|
296
|
+
- `read`:输入 `{ after?, before?, limit? }`,`after`、`before` 是游标,`limit` 为 1 到 100;结果 `{ messages, cursor, more }`,没有消息时 `cursor` 为 `null`。
|
|
297
|
+
- `post`:输入 `{ text, parts?, replyTo?, mentions? }`;结果 `{ message: { id, cursor } }`。发送者固定是 `self`;Provider 按调用的 `requestId` 只发一次,发送失败就报错。
|
|
298
|
+
|
|
299
|
+
消息是 `{ id, cursor, sender, text, parts?, replyTo?, mentions?, about?, createdAt? }`:
|
|
300
|
+
|
|
301
|
+
- `sender` 与 `mentions` 写参与者 ID,`replyTo` 写消息 ID;
|
|
302
|
+
- 回复一条消息就算 @ 了它的发送者,Provider 把被回复消息的发送者也写进 `mentions`:只有 Provider 知道被回复的那条是谁发的,观察方不用记住 Bot 发过哪些消息;
|
|
303
|
+
- `text` 是纯文本兜底,看不懂 `parts` 的一方只看它;
|
|
304
|
+
- 撤回、编辑、表情回应都是一条新消息,`about: { message, change: "edit"|"recall"|"reaction", value? }` 指向旧消息;
|
|
305
|
+
- `createdAt` 只供显示,顺序看 `cursor`。
|
|
306
|
+
|
|
307
|
+
`parts` 的每一项是下面之一:
|
|
308
|
+
|
|
309
|
+
- `{ type: "text", text }`;
|
|
310
|
+
- `{ type: "image"|"video"|"file", name, size?, mime?, service? }`:大文件只给引用,`service` 是插件登记的流式服务 ID,本版本只定字段;
|
|
311
|
+
- `{ type: "thinking"|"tool", text }`。
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"spec": {"title": "项目群", "container": {"id": "wx-1", "title": "我的微信"}},
|
|
316
|
+
"status": {
|
|
317
|
+
"head": "c2",
|
|
318
|
+
"participants": [
|
|
319
|
+
{"id": "u1", "name": "小王", "kind": "person"},
|
|
320
|
+
{"id": "bot", "name": "Coda", "kind": "bot", "self": true}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`read` 的一条结果,第二条撤回了第一条:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"messages": [
|
|
331
|
+
{"id": "m1", "cursor": "c1", "sender": "u1", "text": "@Coda 看下构建", "mentions": ["bot"]},
|
|
332
|
+
{"id": "m2", "cursor": "c2", "sender": "u1", "text": "", "about": {"message": "m1", "change": "recall"}}
|
|
333
|
+
],
|
|
334
|
+
"cursor": "c2",
|
|
335
|
+
"more": false
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## coda/environment@1
|
|
340
|
+
|
|
341
|
+
插件提供的外部环境,比如别家 Agent 自带的云主机。本版本只读。
|
|
342
|
+
|
|
343
|
+
spec 是 `{ title, container?, actions }`:`container` 同 `coda/session@1` 的 `{ id, title }`;`actions` 是 `read`、`checkout`、`delete`、`exec` 的子集,界面按它显示能做什么,本版本只认 `read`。以后同一大版本里只加这几个动作对应的操作。
|
|
344
|
+
|
|
345
|
+
status 是 `{ state: "available"|"unreachable", reason? }`。
|
|
346
|
+
|
|
347
|
+
两个操作都是立即结果,都是 `safe` 的;路径是 Provider 自己的命名空间,Coda 不解析:
|
|
348
|
+
|
|
349
|
+
- `list`:输入 `{ path, after? }`,`path` 为 `""` 表示根,`after` 是上一页的 `next`;结果 `{ entries: [{ name, type: "file"|"dir"|"link", size? }], next }`,没有下一页时 `next` 为 `null`。
|
|
350
|
+
- `read`:输入 `{ path, offset?, length? }`,`length` 至多 1 MiB(1048576);结果 `{ path, size, base64, more }`,`size` 是文件总字节数,`base64` 是这一段的内容,`more` 表示后面还有。
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"spec": {"title": "云主机", "actions": ["read"]},
|
|
355
|
+
"status": {"state": "available"}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## 版本
|
|
360
|
+
|
|
361
|
+
- 协议名里的大版本是协议自己的版本,和 npm 包版本、插件清单的 v2 独立。
|
|
362
|
+
- 同一大版本只加可选字段和新操作;改含义、删字段或删操作就升大版本。新旧版本可并存,插件在 `contributes.protocolImplementations` 里声明实现哪个。
|
|
363
|
+
- 3.0.0 删除了 `executor`、`execution` 两个旧协议与 `@coda-console/protocol/executor`,是不兼容变更;不提供旧格式兼容代码。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coda-console/protocol",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Coda 的协议:插件清单的类型(JSON Schema
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Coda 的协议:插件清单的类型(JSON Schema)、通用校验器、协议定义与调用信封、流程方言与 Agent 接入协议",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"workflow",
|
|
19
19
|
"workflow.mjs",
|
|
20
20
|
"docs",
|
|
21
|
-
"
|
|
21
|
+
"protocol.mjs",
|
|
22
|
+
"CHANGELOG.md"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
24
25
|
"./validate": "./validate.mjs",
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
"./docs/*": "./docs/*",
|
|
29
30
|
"./package.json": "./package.json",
|
|
30
31
|
"./agent": "./agent.mjs",
|
|
31
|
-
"./
|
|
32
|
+
"./protocol": "./protocol.mjs"
|
|
32
33
|
},
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": ">=24"
|
package/protocol.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Protocol definitions, names and the single invocation envelope. A protocol is a name (`namespace/name@major`), a
|
|
2
|
+
// record format (spec/status JSON Schemas) and operations (input plus an immediate result schema or a run reference).
|
|
3
|
+
// Nothing here reads a live Coda store or decides who may call what.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { explain, newAjv } from './validate.mjs';
|
|
6
|
+
|
|
7
|
+
const base = new URL('./schemas/protocol/v1/', import.meta.url);
|
|
8
|
+
const read = file => JSON.parse(readFileSync(new URL(file, base), 'utf8'));
|
|
9
|
+
|
|
10
|
+
export const PROTOCOL_DEFINITION = read('definition.json');
|
|
11
|
+
export const INVOCATION_REQUEST = read('request.json');
|
|
12
|
+
export const INVOCATION_RESPONSE = read('response.json');
|
|
13
|
+
export const RUN_PROTOCOL = read('run.json');
|
|
14
|
+
export const EXECUTOR_PROTOCOL = read('executor.json');
|
|
15
|
+
export const AGENT_PROTOCOL = read('agent.json');
|
|
16
|
+
export const SESSION_PROTOCOL = read('session.json');
|
|
17
|
+
export const ENVIRONMENT_PROTOCOL = read('environment.json');
|
|
18
|
+
export const CORE_PROTOCOLS = Object.freeze(Object.fromEntries(
|
|
19
|
+
[RUN_PROTOCOL, EXECUTOR_PROTOCOL, AGENT_PROTOCOL, SESSION_PROTOCOL, ENVIRONMENT_PROTOCOL].map(protocol => [protocol.name, protocol])));
|
|
20
|
+
|
|
21
|
+
const ajv = newAjv({ ownProperties: true, addUsedSchema: false, strict: false });
|
|
22
|
+
const definitionCheck = ajv.compile(PROTOCOL_DEFINITION);
|
|
23
|
+
const requestCheck = ajv.compile(INVOCATION_REQUEST);
|
|
24
|
+
const responseCheck = ajv.compile(INVOCATION_RESPONSE);
|
|
25
|
+
const runStatusCheck = ajv.compile(RUN_PROTOCOL.status);
|
|
26
|
+
|
|
27
|
+
const NAMESPACE = /^[a-z][a-z0-9-]{0,39}$/, NAME = /^[a-z0-9][a-z0-9-]{0,39}$/, MAJOR = /^[1-9][0-9]*$/;
|
|
28
|
+
const SHORT_NAME_MAX = 63;
|
|
29
|
+
const NAME_PROBLEM = '须为「命名空间/名字@大版本」:命名空间以小写字母开头,名字以小写字母或数字开头,两者只用小写字母、数字和「-」(各至多 40 个字符),大版本是正整数,短名至多 63 个字符';
|
|
30
|
+
|
|
31
|
+
function shortName({ namespace, name, major }) {
|
|
32
|
+
return `${namespace.replaceAll('-', '-0')}-1${name.replaceAll('-', '-0')}-2${major}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseProtocolName(value) {
|
|
36
|
+
if (typeof value !== 'string') return null;
|
|
37
|
+
const slash = value.indexOf('/'), at = value.lastIndexOf('@');
|
|
38
|
+
if (slash <= 0 || at <= slash + 1 || at === value.length - 1) return null;
|
|
39
|
+
const namespace = value.slice(0, slash), name = value.slice(slash + 1, at);
|
|
40
|
+
if (!NAMESPACE.test(namespace) || !NAME.test(name) || !MAJOR.test(value.slice(at + 1))) return null;
|
|
41
|
+
const major = Number(value.slice(at + 1));
|
|
42
|
+
if (!Number.isSafeInteger(major)) return null;
|
|
43
|
+
const parsed = { namespace, name, major };
|
|
44
|
+
return shortName(parsed).length <= SHORT_NAME_MAX ? parsed : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatProtocolName(parts) {
|
|
48
|
+
if (parts === null || typeof parts !== 'object') return null;
|
|
49
|
+
const { namespace, name, major } = parts;
|
|
50
|
+
if (typeof namespace !== 'string' || !NAMESPACE.test(namespace)) return null;
|
|
51
|
+
if (typeof name !== 'string' || !NAME.test(name)) return null;
|
|
52
|
+
if (!Number.isSafeInteger(major) || major < 1) return null;
|
|
53
|
+
const parsed = { namespace, name, major };
|
|
54
|
+
return shortName(parsed).length <= SHORT_NAME_MAX ? `${namespace}/${name}@${major}` : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function protocolNameProblem(value, where = '协议名') {
|
|
58
|
+
return parseProtocolName(value) ? null : `${where}:${NAME_PROBLEM}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// DNS-safe and reversible: every `-` in the canonical name is written `-0`, `/` is `-1`, `@` is `-2`, and every other
|
|
62
|
+
// character stays as it is. Because a literal `-` always starts an escape, the result is injective.
|
|
63
|
+
export function protocolShortName(value) {
|
|
64
|
+
const parsed = parseProtocolName(value);
|
|
65
|
+
return parsed ? shortName(parsed) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function schemaProblem(schema, where) {
|
|
69
|
+
try {
|
|
70
|
+
ajv.compile(schema);
|
|
71
|
+
return null;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return `${where}:不是可用的 JSON Schema:${error.message}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// `namespace`: the plugin ID a manifest owns definitions under; omitted for core definitions and standalone checks.
|
|
78
|
+
export function definitionProblem(definition, { namespace = null, where = '协议定义' } = {}) {
|
|
79
|
+
if (!definitionCheck(definition))
|
|
80
|
+
return explain(definitionCheck.errors, where, [PROTOCOL_DEFINITION],
|
|
81
|
+
{ style: 'description', required: 'property', schemaBase: PROTOCOL_DEFINITION.$id, leadingDot: true });
|
|
82
|
+
const parsed = parseProtocolName(definition.name);
|
|
83
|
+
if (!parsed) return `${where}.name:${NAME_PROBLEM}`;
|
|
84
|
+
if (namespace !== null && parsed.namespace !== namespace)
|
|
85
|
+
return `${where}.name:命名空间须为「${namespace}」,不能在自己的命名空间之外定义协议`;
|
|
86
|
+
const parts = [['spec', definition.spec], ['status', definition.status]];
|
|
87
|
+
for (const [operation, value] of Object.entries(definition.operations)) {
|
|
88
|
+
parts.push([`operations.${operation}.input`, value.input]);
|
|
89
|
+
if (value.result !== undefined) parts.push([`operations.${operation}.result`, value.result]);
|
|
90
|
+
}
|
|
91
|
+
for (const [key, schema] of parts) {
|
|
92
|
+
const problem = schemaProblem(schema, `${where}.${key}`);
|
|
93
|
+
if (problem) return problem;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A plugin owns only its own namespace: check each contributes.protocols definition against its id (or name).
|
|
99
|
+
export function ownedProtocolsProblem(manifest) {
|
|
100
|
+
const protocols = manifest?.contributes?.protocols;
|
|
101
|
+
if (protocols === undefined) return null;
|
|
102
|
+
if (!Array.isArray(protocols)) return 'contributes.protocols:须为数组';
|
|
103
|
+
const owner = manifest.id ?? manifest.name;
|
|
104
|
+
if (typeof owner !== 'string') return 'contributes.protocols:插件须有 id 或 name,定义只能放在自己的命名空间下';
|
|
105
|
+
if (owner === 'coda') return 'contributes.protocols:命名空间 coda 保留给核心协议';
|
|
106
|
+
for (const [index, definition] of protocols.entries()) {
|
|
107
|
+
const problem = definitionProblem(definition, { namespace: owner, where: `contributes.protocols[${index}]` });
|
|
108
|
+
if (problem) return problem;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const RUN_STATES = Object.freeze(['working', 'input-required', 'completed', 'failed', 'canceled']);
|
|
114
|
+
export const RUN_TERMINAL_STATES = Object.freeze(['completed', 'failed', 'canceled']);
|
|
115
|
+
|
|
116
|
+
function statusProblem(value, where) {
|
|
117
|
+
if (runStatusCheck(value)) return null;
|
|
118
|
+
return explain(runStatusCheck.errors, where, [RUN_PROTOCOL.status],
|
|
119
|
+
{ style: 'description', required: 'property', schemaBase: RUN_PROTOCOL.status.$id, leadingDot: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Terminal states are final: a status write after one is refused, even one that would write the same state again.
|
|
123
|
+
export function runStatusProblem(before, after, where = '运行状态') {
|
|
124
|
+
const problem = statusProblem(before, `${where}(之前)`) ?? statusProblem(after, `${where}(之后)`);
|
|
125
|
+
if (problem) return problem;
|
|
126
|
+
return RUN_TERMINAL_STATES.includes(before.state) ? `${where}:${before.state} 是终态,不能再变` : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function invocationProblem(check, schema, value, where) {
|
|
130
|
+
if (check(value)) return null;
|
|
131
|
+
return explain(check.errors, where, [schema], { style: 'description', required: 'property', schemaBase: schema.$id, leadingDot: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function invocationRequestProblem(value, where = '调用请求') {
|
|
135
|
+
return invocationProblem(requestCheck, INVOCATION_REQUEST, value, where);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function invocationResponseProblem(value, where = '调用回答') {
|
|
139
|
+
return invocationProblem(responseCheck, INVOCATION_RESPONSE, value, where);
|
|
140
|
+
}
|
package/schemas/agent/wait.json
CHANGED
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"views": {"$ref": "views.json"},
|
|
53
53
|
"modelProviders": {"$ref": "model-providers.json"},
|
|
54
54
|
"executors": {"$ref": "executors.json"},
|
|
55
|
+
"protocols": {"$ref": "protocols.json"},
|
|
56
|
+
"protocolImplementations": {"$ref": "protocol-implementations.json"},
|
|
55
57
|
"composerModes": {"$ref": "composer-modes.json"}
|
|
56
58
|
},
|
|
57
59
|
"propertyNames": {"description": "contributes 的键:字母和数字,以小写字母开头", "pattern": "^[a-z][A-Za-z0-9]{0,39}$"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://coda.pub/schemas/plugin/v2/protocol-implementations.json",
|
|
4
|
+
"title": "contributes.protocolImplementations:插件实现的协议",
|
|
5
|
+
"description": "插件实现哪些协议,写协议名「命名空间/名字@大版本」,不重复。可以实现在别的命名空间下定义的协议。",
|
|
6
|
+
"type": "array",
|
|
7
|
+
"maxItems": 100,
|
|
8
|
+
"uniqueItems": true,
|
|
9
|
+
"items": {"$ref": "https://coda.pub/schemas/protocol/v1/definition.json#/$defs/name"}
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://coda.pub/schemas/plugin/v2/protocols.json",
|
|
4
|
+
"title": "contributes.protocols:插件定义并拥有的协议",
|
|
5
|
+
"description": "每个是一份协议定义;定义里的命名空间须是本插件的 ID。宿主读清单时用 @coda-console/protocol 的 ownedProtocolsProblem 一并核对该约束。",
|
|
6
|
+
"type": "array",
|
|
7
|
+
"maxItems": 50,
|
|
8
|
+
"uniqueItems": true,
|
|
9
|
+
"items": {"$ref": "https://coda.pub/schemas/protocol/v1/definition.json"},
|
|
10
|
+
"x-unique-by": ["name"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coda/agent@1",
|
|
3
|
+
"title": "Agent",
|
|
4
|
+
"description": "一台机器上装着的 Agent 程序及其模型与审批模式,照 Coda 的 Agent 资源 spec。插件不再写在 spec 里,由记录的 provider 表达;本版本不定义操作。",
|
|
5
|
+
"spec": {
|
|
6
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"$defs": {
|
|
9
|
+
"level": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 200},
|
|
13
|
+
"name": {"type": "string", "maxLength": 200},
|
|
14
|
+
"shortName": {"type": "string", "maxLength": 40}
|
|
15
|
+
},
|
|
16
|
+
"required": ["id"],
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
},
|
|
19
|
+
"option": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 200},
|
|
23
|
+
"name": {"type": "string", "maxLength": 200},
|
|
24
|
+
"shortName": {"type": "string", "maxLength": 40},
|
|
25
|
+
"type": {"enum": ["boolean", "select"], "description": "开关,或几个选项里选一个"},
|
|
26
|
+
"choices": {"type": "array", "items": {"$ref": "#/$defs/level"}, "description": "能选的选项"}
|
|
27
|
+
},
|
|
28
|
+
"required": ["id", "type"],
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"if": {"properties": {"type": {"const": "select"}}, "required": ["type"]},
|
|
31
|
+
"then": {"required": ["choices"]},
|
|
32
|
+
"else": {"not": {"required": ["choices"]}}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"properties": {
|
|
36
|
+
"agent": {"type": "string", "minLength": 1, "maxLength": 200, "description": "Agent 的 ID:spawn 的 agent 写它"},
|
|
37
|
+
"title": {"type": "string", "maxLength": 200, "description": "给人看的名字"},
|
|
38
|
+
"defaultModel": {"type": "string", "maxLength": 200, "description": "不指定模型时用的模型 ID;读到目录前为空"},
|
|
39
|
+
"models": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "它在那台能用的模型,最近读到的目录",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"id": {"type": "string", "minLength": 1, "maxLength": 200},
|
|
46
|
+
"name": {"type": "string", "maxLength": 200},
|
|
47
|
+
"shortName": {"type": "string", "maxLength": 40},
|
|
48
|
+
"group": {"type": "string", "maxLength": 200, "description": "来源:Agent 自带的没有,模型服务的是服务的标题"},
|
|
49
|
+
"efforts": {"type": "array", "items": {"$ref": "#/$defs/level"}},
|
|
50
|
+
"options": {"type": "array", "items": {"$ref": "#/$defs/option"}}
|
|
51
|
+
},
|
|
52
|
+
"required": ["id", "efforts", "options"],
|
|
53
|
+
"additionalProperties": false
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"modes": {"type": "array", "description": "它的审批模式", "items": {"$ref": "#/$defs/level"}}
|
|
57
|
+
},
|
|
58
|
+
"required": ["agent", "title", "defaultModel", "models", "modes"],
|
|
59
|
+
"additionalProperties": false
|
|
60
|
+
},
|
|
61
|
+
"status": {
|
|
62
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
63
|
+
"type": "object",
|
|
64
|
+
"maxProperties": 0,
|
|
65
|
+
"additionalProperties": false,
|
|
66
|
+
"description": "Agent 记录没有 status 字段"
|
|
67
|
+
},
|
|
68
|
+
"operations": {}
|
|
69
|
+
}
|