@coda-console/protocol 0.0.0 → 1.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/README.md +47 -2
- package/docs/agent-protocol.md +384 -0
- package/package.json +19 -2
- package/schemas/plugin/agents.json +50 -0
- package/schemas/plugin/capabilities.json +16 -0
- package/schemas/plugin/commands.json +21 -0
- package/schemas/plugin/common.json +25 -0
- package/schemas/plugin/composer-modes.json +79 -0
- package/schemas/plugin/configuration.json +23 -0
- package/schemas/plugin/endpoints.json +19 -0
- package/schemas/plugin/environment.json +22 -0
- package/schemas/plugin/events.json +23 -0
- package/schemas/plugin/executors.json +26 -0
- package/schemas/plugin/extension-points.json +57 -0
- package/schemas/plugin/extensions.json +13 -0
- package/schemas/plugin/git-credentials.json +17 -0
- package/schemas/plugin/groups.json +21 -0
- package/schemas/plugin/instructions.json +9 -0
- package/schemas/plugin/mcp-servers.json +29 -0
- package/schemas/plugin/model-providers.json +39 -0
- package/schemas/plugin/path.json +16 -0
- package/schemas/plugin/plugin.json +46 -0
- package/schemas/plugin/repositories.json +7 -0
- package/schemas/plugin/services.json +9 -0
- package/schemas/plugin/setting.json +242 -0
- package/schemas/plugin/setup.json +7 -0
- package/schemas/plugin/skills.json +15 -0
- package/schemas/plugin/views.json +30 -0
- package/validate.mjs +182 -0
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Coda 协议
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Coda 与插件之间的约定,Coda 和插件都引用它,独立演进:
|
|
4
|
+
|
|
5
|
+
- **插件清单的类型**(`schemas/plugin/`):插件目录 `package.json` 里 `contributes` 的每个键是一种类型,各有一份 JSON Schema(draft 2020-12,`$id` 为 `https://coda.pub/schemas/plugin/v1/<文件>`)。`plugin.json` 是整份清单,`setting.json` 是一项设置的声明。插件写的合乎它用到的类型就对;写给别的插件扩展点的键由那个插件声明。
|
|
6
|
+
- **通用校验器**(`validate.mjs`):用 Ajv 对清单做 schema 校验。JSON Schema 说不了的只用五个通用关键字(`x-unique-by`、`x-max-contains`、`x-ref`、`x-placeholders`、`x-file`,照 Kubernetes 的 `x-kubernetes-*`),每个在哪里写都是同一个意思,写法见文件开头。校验器不随功能变长:类型加字段就是改 schema。
|
|
7
|
+
- **Agent 接入协议**(`docs/agent-protocol.md`):Coda 怎样用 ACP 接 Agent,以及 Coda 加的扩展(会话指令、插话、`_coda/wait`、执行失败、工具调用的审核)。
|
|
8
|
+
|
|
9
|
+
## 用法
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @coda-console/protocol
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { manifestProblem, valueProblem, TYPES, SCHEMAS } from '@coda-console/protocol/validate';
|
|
17
|
+
|
|
18
|
+
// 整份清单;给出插件目录时,连同它点名的文件一起查(x-file)。合乎类型返回 null,否则是一句说明:哪里、schema 怎么说。
|
|
19
|
+
const problem = manifestProblem(JSON.parse(readFileSync('plugin/package.json', 'utf8')), 'plugin');
|
|
20
|
+
// 某个类型里的一部分,如输入框模式的参数(插件运行时给出的)。
|
|
21
|
+
valueProblem('composer-modes.json#/$defs/parameters', parameters, '插件给出的参数');
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`TYPES` 是 Coda 自己的类型(`contributes` 的键),`SCHEMAS` 是全部 schema(按文件名)。
|
|
25
|
+
|
|
26
|
+
## 谁在用
|
|
27
|
+
|
|
28
|
+
- **Coda**:读插件时用它校验清单,再整理成自己的结构;只另看插件身份(ID、设置名在 ID 之下),以及装到一台机器上才知道的(来源、已装插件的冲突)。
|
|
29
|
+
- **coda-plugins 的契约检查包**(`@coda-console/plugin-check`):在写插件的地方用同一个校验器判清单。
|
|
30
|
+
|
|
31
|
+
## 版本
|
|
32
|
+
|
|
33
|
+
按 semver,Coda 与检查包都钉一个确切版本:
|
|
34
|
+
|
|
35
|
+
- 只加东西(新的可选字段、新的类型、放宽限制)升次版本;
|
|
36
|
+
- 改名、删除、收紧(旧的清单会变得不合)升主版本,`$id` 里的 `v1` 随之变成 `v2`。
|
|
37
|
+
|
|
38
|
+
改协议就在这里开 PR,说明写清动了哪个类型、旧插件受不受影响;发布后 Coda 与检查包各自升依赖。
|
|
39
|
+
|
|
40
|
+
## 发布
|
|
41
|
+
|
|
42
|
+
`main` 上版本号变了,`publish.yml` 经 npm 可信发布(OIDC)发出去,不存令牌。第一次发布前,维护者要先占名并配置可信发布:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm login
|
|
46
|
+
node scripts/publish-npm.mjs --bootstrap
|
|
47
|
+
gh workflow run publish.yml --repo coda-console/coda-protocol --ref main
|
|
48
|
+
```
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Agent 接入协议
|
|
2
|
+
|
|
3
|
+
Coda 用 [ACP](https://agentclientprotocol.com)(Agent Client Protocol)接 Agent,要求 Agent 接受[会话指令](#会话指令)(ACP 的 `systemPrompt`),
|
|
4
|
+
可选支持[插话](#插话)(`_session/steering`),另收 Agent 活动报告(`_coda/wait`)、[执行失败事实](#执行失败)(ACP 错误数据里的 `_meta.coda.failure`)和[工具调用的审核](#工具调用的审核)(`_meta.coda.review`)。本文是这份协议的唯一正文,写给要让一个 Agent(比如 Cursor 的)接入 Coda 的人:照它实现,
|
|
5
|
+
Coda 就知道这个 Agent 在一轮之外还在做什么,如实显示、不回收它的进程,把它处理这些东西时的输出、审批和提问放进正确的一轮,
|
|
6
|
+
并把它之后说的作为补交交回正确的调用。产品上这些规则的来由见会话行为契约(Coda 的 docs/session-contracts.md)与完成唤醒契约(Coda 的 docs/subagent-design.md「完成唤醒契约」)。
|
|
7
|
+
|
|
8
|
+
**改协议**:本文在 `coda-console/coda-protocol`,与插件清单的类型(`schemas/`)同一个仓库、同一个版本(npm 包 `@coda-console/protocol`)。Coda 与公开仓库 `coda-console/coda-plugins` 的契约检查包都依赖这个包:改协议就在这里开 PR,发布新版本后两边各自升依赖。检查要换判定方式时(新的请求怎么发、怎样算合格),另给 coda-plugins 开 PR 改检查本身。文中提到的 Coda 文档与源码在 Coda 的仓库里。
|
|
9
|
+
|
|
10
|
+
## ACP 部分
|
|
11
|
+
|
|
12
|
+
Coda 是 ACP 的客户端。每个会话的 Agent 进程由一个会话托管进程(Coda 的 docs/session-host.md)持有,经标准输入输出说 JSON-RPC;
|
|
13
|
+
后端重启只断开连接,进程照跑,后端不在期间 Agent 发来的消息记在托管日志里,新后端按顺序续读,每条只处理一次。
|
|
14
|
+
|
|
15
|
+
| 方法 | 方向 | Coda 怎么用 |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `initialize` | Coda → Agent | 握手。`clientCapabilities` 带 `elicitation.form`、`elicitation.url`、`session.compaction`、`session.configOptions.boolean`、`_meta.coda.waits`(见[能力协商](#能力协商))与 `_meta.coda.remote`(见[远端入口](#远端入口)) |
|
|
18
|
+
| `session/new`、`session/load` | Coda → Agent | 新建或恢复原生会话,会话 ID 由 Agent 给;Agent 声明 `loadSession` 才会被要求恢复。带 `systemPrompt`([会话指令](#会话指令))。结果里要报出当前模型(见下)。恢复期间重放的历史 Coda 不收(它有自己的),只收命令与标题 |
|
|
19
|
+
| `session/prompt` | Coda → Agent | 一轮:Coda 送出用户的消息或它自己合成的输入(子会话结果等),Agent 回复 `stopReason` 即本轮结束 |
|
|
20
|
+
| `_session/steering` | Coda → Agent | [插话](#插话):把用户的消息加进正在进行的一轮;只对声明支持的 Agent 发 |
|
|
21
|
+
| `session/cancel` | Coda → Agent | 停止:本轮在进行时取消本轮;Agent 在一轮之外处理活动(`handling`)时,停止处理 |
|
|
22
|
+
| `_coda/session/compact` | Coda → Agent | [压缩上下文](#压缩上下文):请 Agent 压缩这个会话的上下文;只对声明支持的 Agent 发,两轮之间也发 |
|
|
23
|
+
| `session/set_config_option`、`session/set_mode`、`session/set_model` | Coda → Agent | 模型、推理强度与模式 |
|
|
24
|
+
| `session/update` | Agent → Coda | 回复、思考、工具调用(可带它的[审核](#工具调用的审核))、计划、用量(可带给流程的[数据](#用量里的数据))、可用命令、会话信息;只收本会话与它的子会话的 |
|
|
25
|
+
| `session/request_permission`、`elicitation/create`(`mode: form` 或 `mode: url`) | Agent → Coda | 审批与提问,作为卡片交给用户;只在一轮里受理(见[规则](#规则)第 3 条) |
|
|
26
|
+
| `_coda/remote/code`、`_coda/remote` | Agent → Coda、Coda → Agent | [远端入口](#远端入口):Agent 要绑定码;Coda 转交远端来的请求 |
|
|
27
|
+
| `$/cancel_request` | Agent → Coda | 撤回它发出、还没得到回答的审批或提问:卡片失效,Coda 回 `cancelled`(表单回 `cancel`),之后用户的回答不再转交 |
|
|
28
|
+
|
|
29
|
+
ACP 只允许在一轮(`session/prompt`)进行时交互。Agent 在两轮之间发来的审批或提问,Coda 拒绝(审批回 `cancelled`,表单回 `cancel`)
|
|
30
|
+
并在会话里记一条说明;它正在处理某项活动时例外,那时请求属于处理它的那一轮。两轮之间的 `session/update` 照收。
|
|
31
|
+
|
|
32
|
+
- **审批**:选项由 Agent 列出,Coda 原样展示,用户选哪个回哪个;授权的范围、对象与期限写在选项名里,Coda 不另分栏、不自行扩展授权。
|
|
33
|
+
- **表单**:`requestedSchema` 是标准 JSON Schema,Coda 按它展示并校验回答;单选、多选用
|
|
34
|
+
`enum` 或 `oneOf`/`anyOf` 的 `{const, title}`,不认私有键。
|
|
35
|
+
- **链接**(`mode: url`):带 `url` 与 `elicitationId`,没有要填的字段;Coda 展示链接,用户打开、办完后确认,回 `{action}`(`accept`、`decline` 或 `cancel`)。
|
|
36
|
+
- **模型的分组**:`category: model` 的配置项可以按来源分组,用 ACP 选择型配置项的分组写法:`options` 是一组 `{ group, name, options }`,
|
|
37
|
+
如 Agent 自己的模型一组、它接的每个模型服务一组(模型服务(Coda 的 docs/plugins.md「模型服务」))。Coda 记下每个模型所在组的 `name`,两端选模型时按组分段、
|
|
38
|
+
以组名为段标题,组内与输入框上只写模型的 `name`;不分组的照旧列成一串。模型 ID 在各组之间不重复,设模型照旧按 ID。
|
|
39
|
+
- **当前模型**:`session/new`、`session/load` 的结果要报出会话当前用的模型:配置项里 `category: model` 那一项的 `currentValue`,或 `models.currentModelId`;
|
|
40
|
+
之后 `config_option_update` 与 `session/set_config_option`、`session/set_model` 的结果改了它,以改后的为准。Coda 每轮开始前把会话设置设给 Agent,
|
|
41
|
+
再读当前模型记作这一轮的实际模型;读不到就拒绝这一轮(「适配器未报告实际模型」),不发 `session/prompt`。
|
|
42
|
+
- **设置**:会话设置里选了的模型、推理强度与模式,每轮开始前设给 Agent;没选的保持会话现状。当前模式是空字符串表示还没有选模式(Agent 按它自己的配置),按没有设置显示。模型或推理强度的选择型配置项里,
|
|
43
|
+
可以有一项带 `_meta.coda.default: true`,表示交给 Agent 自己的默认:会话设置里没选时,Coda 把配置项切回这一项。这一项不进模型或强度的列表,
|
|
44
|
+
当前值是它时按没有设置显示。
|
|
45
|
+
- **其他配置项**:模型、推理强度与模式之外的配置项,Coda 原样列给用户:选择型是一组选项,开关型(`type: boolean`)是一个开关;Coda 在 `initialize` 的
|
|
46
|
+
`clientCapabilities.session.configOptions.boolean` 里声明接受开关型。会话设置里按配置项 ID 给的值,每轮开始前设给 Agent;当前模型没有这一项就不设。
|
|
47
|
+
Coda 设过、会话设置里又去掉的(用户选回「Agent 默认」),切回 Agent 标了 `_meta.coda.default` 的那一项,没标就切回 Coda 第一次设它之前、
|
|
48
|
+
同一模型下 Agent 报的值,两样都不知道就不改。Agent 自己改的 Coda 不去改回;用户在会话里用命令改的(如 Codex 的 `/fast`)记进会话设置。名字与说明照 Agent 给的显示。
|
|
49
|
+
- **未登录**:Agent 在 `initialize`、`session/new` 或 `session/load` 回错误 -32000(ACP 的 `auth_required`)时,Coda 告诉用户这个 Agent 未登录,
|
|
50
|
+
到「资源」里添加(面板的登录(Coda 的 docs/plugins.md「面板的登录」));Coda 不调用 `authenticate`,也不按错误文字猜。
|
|
51
|
+
- **Agent 版本**:`initialize` 结果的 `agentInfo`(ACP 的 `name`、`version`)填**实际运行的那个 Agent**:包装了别家 CLI 或 SDK 的插件,
|
|
52
|
+
报那个 CLI 或 SDK 的名字与版本(如 `codex`、`0.130.0`),不报插件自己的版本(插件版本 Coda 已从清单知道)。
|
|
53
|
+
Coda 记下每个 Agent 最近一次握手报的值,随后端遥测上报(Coda 遥测服务(Coda 的 docs/../telemetry/README.md「后端上报」)),用于复盘 Agent 版本带来的兼容问题;
|
|
54
|
+
只取版本号式的字符(字母、数字与 `.+()-_` 空格,至多 64 个),其他写法不记。不报不影响会话。
|
|
55
|
+
|
|
56
|
+
## 会话指令
|
|
57
|
+
|
|
58
|
+
Coda 给每个会话一份指令:回复怎样在 Mac 与 iPhone 上显示、默认回复语言,再接目录启用的插件说明(插件(Coda 的 docs/plugins.md)的 `contributes.instructions`),
|
|
59
|
+
正文在 `backend/src/components/session-config/client-instructions.mjs`。它走 ACP 的 [`systemPrompt`](https://agentclientprotocol.com/protocol/draft/session-setup#system-prompt-unstable)(unstable),
|
|
60
|
+
**接入 Coda 的 Agent 必须支持**:
|
|
61
|
+
|
|
62
|
+
- **声明**:`initialize` 结果的 `agentCapabilities.sessionCapabilities.systemPrompt: {}`。Coda 只用 `append` 模式,不需要声明 `mode.override`。
|
|
63
|
+
- **传入**:`session/new` 与 `session/load` 都带同一份 `{"mode": "append", "content": [{"type": "text", "text": …}]}`,只有一个文本块。
|
|
64
|
+
每次打开时按目录当时启用的插件生成,插件说明变了,下次打开会话时带的就是新的。
|
|
65
|
+
- **生效**:在会话的第一轮执行之前进入上下文。Agent 有原生的系统提示词或 developer instructions 入口就放在那里(Claude 追加到系统提示词,Codex 是 `developerInstructions`);
|
|
66
|
+
没有原生入口的,由插件在这个原生会话**第一次** `session/prompt` 时把指令作为单独、明确分隔的一段放在用户输入前面一起送入,之后不再送(第一轮是斜杠命令时顺延到下一轮,免得命令不再被认出);
|
|
67
|
+
`session/load` 恢复的会话已经送过,不再送。Coda 显示与保存的用户原文不变。首条消息里的指令分量不如系统提示词,这是没有原生入口时的近似。
|
|
68
|
+
- **失败**:应用不了(不认识的模式、内容超出上限)时 `session/new`、`session/load` 回错误,不建立会话(ACP 建议 -32602)。
|
|
69
|
+
- 没声明的 Agent,Coda 照 ACP 的规矩不发 `systemPrompt`,会话照常打开,只是没有 Coda 的指令;契约检查判它不合格。
|
|
70
|
+
|
|
71
|
+
Coda 的 MCP 服务器只提供工具,不带 `instructions`。
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{"jsonrpc":"2.0","id":1,"method":"session/new","params":{"cwd":"/work/app","mcpServers":[],"systemPrompt":{"mode":"append","content":[{"type":"text","text":"# Coda client\n\nYour replies are shown in Coda…"}]}}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 插话
|
|
78
|
+
|
|
79
|
+
用户在一轮进行中「立即插话」时,支持插话的 Agent 把这条消息加进正在进行的一轮,不打断它;不支持的,Coda 停止这一轮,
|
|
80
|
+
在它结束后把消息作为下一轮送出(会话行为契约(Coda 的 docs/session-contracts.md「发送行为」))。父会话给正为它跑一轮的子会话发消息(`send`)时同样插话;
|
|
81
|
+
不支持的 Agent,这条消息只排到这一轮结束,Coda 不为它停止(工具面(Coda 的 docs/subagent-design.md「工具面」))。插话用生态里已有的 `_session/steering` 扩展:
|
|
82
|
+
|
|
83
|
+
- **声明**:`initialize` 结果顶层的 `_meta.steering.supported: true`(与 `agentCapabilities` 并列,沿用上游的写法)。声明了就要支持下面的 `promptRequired`。
|
|
84
|
+
- **请求**:`_session/steering`,参数 `{sessionId, prompt, _meta: {steering: {idleBehavior: "promptRequired"}}}`。`prompt` 与 `session/prompt` 的一样(Coda 只发一个文本块,附件写成路径)。
|
|
85
|
+
Coda 只在自己发起的一轮(`session/prompt`)进行中、Agent 已开始执行时发,斜杠命令那一轮不发。
|
|
86
|
+
- **回复**:
|
|
87
|
+
- `{"outcome": "injected"}`:消息加进了正在进行的一轮。之后的输出、审批与结束都属于原来那次 `session/prompt`,Coda 把这条消息记进那一轮。
|
|
88
|
+
- `{"outcome": "promptRequired", "reason": "noRunningTurn"}`:那一轮已经结束,消息**没有**被接收。Agent 不得自己另开一轮;Coda 在那一轮结束后把它作为下一轮送出。
|
|
89
|
+
- 其他(`startedNewTurn`、`failed`)或错误:Coda 不知道这条消息有没有进到原生会话,记为结果未知,不自动重发,用户可以自己再发。
|
|
90
|
+
- 同一会话同时只有一条插话在途:Coda 等上一条的回复再发下一条。
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{"jsonrpc":"2.0","id":"steer-1","method":"_session/steering","params":{"sessionId":"s1","prompt":[{"type":"text","text":"先别改测试,只看实现"}],"_meta":{"steering":{"idleBehavior":"promptRequired"}}}}
|
|
94
|
+
{"jsonrpc":"2.0","id":"steer-1","result":{"outcome":"injected"}}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 执行失败
|
|
98
|
+
|
|
99
|
+
插件负责将上游错误归为通用类别,Coda 负责显示与重试策略;Coda 不依赖 Agent 名字、错误文字或私有上游错误码。
|
|
100
|
+
`session/prompt` 的 JSON-RPC 错误通过 `error.data._meta.coda.failure` 报告类别。标准 `error.message` 保留可读的失败原因;
|
|
101
|
+
其他 ACP 客户端可以忽略扩展。未标记、未知或不合格式的类别按普通错误处理,不自动重试。
|
|
102
|
+
|
|
103
|
+
### 额度不足
|
|
104
|
+
|
|
105
|
+
`kind: quota_exhausted` 表示可用额度已耗尽,例如订阅用量窗口已满或余额不足。插件须有明确的上游原因,不能仅凭 HTTP 403、
|
|
106
|
+
HTTP 429 或上游的认证错误分类判断。真正的认证失败仍沿用 ACP `auth_required`,不得报为额度不足。
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{"jsonrpc":"2.0","id":"prompt-1","error":{"code":-32603,"message":"当前用量窗口的额度已耗尽,<t:2026-10-24T18:10:00.000Z> 重置","data":{"_meta":{"coda":{"failure":{"kind":"quota_exhausted"}}}}}}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Coda 按该类别显示「额度不足」,保留完整原因,不安排自动重试;这个类别不要求 `input`,即使带了也不进入服务故障重试。
|
|
113
|
+
该回执确认本轮因额度不足而结束,轮次记为 `ended`、`stopReason: quota_exhausted` 并保留 `error`,操作记为 `completed`;
|
|
114
|
+
操作已结算不代表任务成功,子会话向调用方交回 `failed`。两端同一原因只显示一次,不再标作「执行结果未知」。
|
|
115
|
+
交回 `failed` 的状态文案为「失败」,涵盖准备与执行阶段。调用交回后,后续轮次的额度错误仍连同原因补交给调用方,即使没有正文或还有其他后台工作在进行。
|
|
116
|
+
插件以普通执行错误返回(例如 -32603),不得把额度耗尽伪装成要求重新登录的 -32000。
|
|
117
|
+
恢复条件(等待窗口重置、补充额度等)由插件放在原因中,重置时刻按[文字里的时刻](#文字里的时刻)写;上游没有给出确切恢复时间时,不推算时间。
|
|
118
|
+
|
|
119
|
+
### 服务暂不可用
|
|
120
|
+
|
|
121
|
+
插件负责识别服务端临时故障;**重试策略归 Coda**,不依赖 Agent 名字、错误文字或私有上游错误码。
|
|
122
|
+
`session/prompt` 的 JSON-RPC 错误可以在 `error.data._meta.coda.failure` 中报告:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{"jsonrpc":"2.0","id":"prompt-1","error":{"code":-32603,"message":"服务暂不可用","data":{"_meta":{"coda":{"failure":{"kind":"service_unavailable","input":"accepted"}}}}}}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- `kind: service_unavailable`:服务端暂时无法处理;额度耗尽、未登录、配置或参数错误不得这样报告。
|
|
129
|
+
- `input: accepted`:输入已经进入原生会话;故障回执确认本次原生执行已结束。Coda 在同一会话发送从已有进度继续的指令。
|
|
130
|
+
- `input: not_accepted`:明确拒绝,输入尚未进入原生会话,也没有执行它的操作。仅当原输入从未被接收时重投相同内容与附件;已有接收证据(包括已保存的 `delivered`)时只续跑。
|
|
131
|
+
- 可选 `retryAfterMs`:上游要求或原生 Agent 实际安排的剩余等待,单位毫秒,须为非负有限数;不是预计恢复时间。
|
|
132
|
+
Coda 不早于这段等待重试;超过一分钟时不安排自动重试。缺省沿用 Coda 的退避,格式不合法的标记按普通错误处理。
|
|
133
|
+
|
|
134
|
+
输入是否被接收或执行是否已结束无法确定时,不带此标记。错误文字始终放在标准 `error.message`;其他 ACP 客户端可以忽略扩展。
|
|
135
|
+
Agent 自己仍在重连或重试时,不得提前返回本次 `session/prompt` 的失败;Coda 只在失败回执后安排重试。
|
|
136
|
+
Agent 自己的重试要等一分钟以上(比 Coda 的退避更久,例如服务的 `Retry-After` 指向用量窗口重置)时,不得一直等而不报告:
|
|
137
|
+
停下原生执行后以失败结束本轮,在原因中写明服务给出的恢复条件和原生 Agent 实际安排的等待。
|
|
138
|
+
上游明确属于服务临时故障时,带 `service_unavailable`、`input: accepted` 与 `retryAfterMs`,由 Coda 判定不再自动重试;
|
|
139
|
+
上游明确说明额度耗尽时,按[额度不足](#额度不足)报告;只有长等待而没有明确原因时仍按普通错误报告。
|
|
140
|
+
|
|
141
|
+
`input: accepted` 的服务故障回执在不再重试时确认本轮已结束:轮次记为 `ended`、`stopReason: service_unavailable` 并保留原因,
|
|
142
|
+
操作记为 `completed`,子会话交回 `failed`。两端显示「服务暂不可用」,同一原因只显示一次,不再标作「执行结果未知」。
|
|
143
|
+
重试用尽或斜杠命令不重试时也如此;未接收输入的回执不单凭失败类别认定输入已送达。
|
|
144
|
+
|
|
145
|
+
Coda 统一按 15、30、60 秒退避,每次执行最多重试三次,沿用原生会话、执行设置、原输入与轮次身份,
|
|
146
|
+
每次 ACP 请求使用新 ID。等待中保持执行状态,可随时停止;斜杠命令不自动重试。次数耗尽或遇到未标记错误时结束并保留最后原因。
|
|
147
|
+
退避只在当前执行期间保留,后端重启或连接中断即结束等待;已发出的请求、整机中断沿用现有会话接管和恢复规则。
|
|
148
|
+
|
|
149
|
+
## 用量里的数据
|
|
150
|
+
|
|
151
|
+
Agent 在 `session/update` 的 `usage_update` 里可以带 `_meta.coda`:一个 JSON 对象,放生命周期流程用得着的数据。Coda 不解释里面的字段,
|
|
152
|
+
原样作为会话事实 `usage` 交给生命周期流程(Coda 的 docs/workflows.md「生命周期流程」),由流程(通常是同一个插件带的)读、判断。
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"s1","update":{"sessionUpdate":"usage_update","used":53000,"size":200000,"_meta":{"coda":{"lastRequestAt":"2026-10-28T01:06:12.418Z","contextTokens":53000,"cacheTtl":"PT1H","compactedAt":"2026-10-27T22:40:03.120Z"}}}}}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- Coda 只收会话本身(不是子 Agent 的会话)报来的,按会话记下最新的一个,后端重启后还在;之后的 `usage_update` 不带 `_meta.coda` 的,记下的不变。
|
|
159
|
+
- 不是对象的丢弃;序列化后超过 4 KiB 的丢弃,并在后端日志里记一行。内容变了,Coda 重新发布会话事实。没报过时事实 `usage` 为 `null`。
|
|
160
|
+
- 时刻写 ISO 8601,Coda 原样转交、不换算,流程可以直接拿来当计时起点(`{{facts.usage.lastRequestAt}}/PT54M`)或比较。
|
|
161
|
+
- Claude 插件(Agent `claude-sdk`)每轮结束发 `usage_update` 时都带上,自主轮次也带:
|
|
162
|
+
`lastRequestAt`(主线程最后一次请求模型的时刻,提示词缓存从这一刻起算;比一轮结束早一段输出的时间)、`contextTokens`(那次请求带的上下文 token 数)、
|
|
163
|
+
`cacheTtl`(缓存时长,`PT1H` 或 `PT5M`)、`compactedAt`(最近一次压缩的时刻)。
|
|
164
|
+
|
|
165
|
+
## 工具调用的审核
|
|
166
|
+
|
|
167
|
+
Agent 运行一个工具调用之前自己做的检查(如 Codex 的自动审核:按风险决定放不放行),是这个调用的一部分:
|
|
168
|
+
在这个调用的 `tool_call` 或 `tool_call_update` 里带 `_meta.coda.review`,不另报一个工具调用。它不是[审批](#acp-部分):审批由用户回答,审核由 Agent 自己判断。
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"s1","update":{"sessionUpdate":"tool_call_update","toolCallId":"exec-1","_meta":{"coda":{"review":{"state":"denied","reason":"会删除工作树外的文件","risk":"high"}}}}}}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| 字段 | 必有 | 含义 |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| `state` | 是 | `reviewing`:审核中,调用还没运行;`approved`:放行;`denied`:没放行(拒绝、审核超时或中止),调用不照原样运行 |
|
|
177
|
+
| `reason` | 否 | 审核给的理由,给人看,至多 4000 个字符;时刻按[文字里的时刻](#文字里的时刻)写 |
|
|
178
|
+
| `risk` | 否 | 审核判断的风险:`low`、`medium`、`high` 或 `critical` |
|
|
179
|
+
|
|
180
|
+
- 审核在调用报出之前就开始的,先报这个调用(`tool_call`,`status: pending`,带上审核),之后照常更新它。
|
|
181
|
+
- Coda 记下每个调用最近一次报的审核;之后不带 `_meta.coda.review` 的更新不改它。读不出的丢弃(`state` 不在上表、`reason` 过长),不认识的 `risk` 不记。
|
|
182
|
+
- 两端客户端在调用上写「审核中」或「审核未通过」,放行的不另加标记;点开调用看审核的结果、风险与理由。
|
|
183
|
+
|
|
184
|
+
## 压缩上下文
|
|
185
|
+
|
|
186
|
+
Coda 可以请 Agent 压缩会话的上下文(同 Claude Code 的 `/compact`),由生命周期流程的 `compact-session`(Coda 的 docs/workflows.md「coda-的动作」) 发起;
|
|
187
|
+
什么时候压、值不值得压由流程按[用量里的数据](#用量里的数据)判断,Agent 只执行。
|
|
188
|
+
|
|
189
|
+
- **声明**:Agent 在 `initialize` 结果的 `agentCapabilities` 里带 `_meta.coda.compact: {}`,Coda 才发;没声明的,这一步不发请求,结果是没压缩。
|
|
190
|
+
- **请求** `_coda/session/compact`,Coda → Agent,参数 `{ "sessionId": … }`。它是 Agent 与 Coda 之间的一来一回,不是一轮:Coda 不开轮次、不改会话显示,
|
|
191
|
+
两轮之间也发(与 [`_coda/wait/stop`](#_codawaitstop) 一样,不受「只在一轮里交互」的限制);压缩期间 Agent 发来的 `session/update`(如 `compaction_update`)照收。
|
|
192
|
+
压缩期间 Coda 发来 `session/prompt` 的,Agent 排在压缩之后处理。
|
|
193
|
+
- **回答**:会话没在运行、正在跑一轮,或压缩失败时回 `compacted: false` 和原因;其余情况都压缩,回 `compacted: true`:
|
|
194
|
+
|
|
195
|
+
| 字段 | 必有 | 含义 |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `compacted` | 是 | 这次压缩了没有 |
|
|
198
|
+
| `reason` | 否 | 没压的原因,给人看;时刻按[文字里的时刻](#文字里的时刻)写 |
|
|
199
|
+
| `before`、`after` | 否 | 压缩前后的上下文 token 数 |
|
|
200
|
+
|
|
201
|
+
别的字段 Coda 不收。Coda 最多等 10 分钟;超时、出错、等待期间进程结束,都当作没压缩,原因由 Coda 写。
|
|
202
|
+
|
|
203
|
+
## 文字里的时刻
|
|
204
|
+
|
|
205
|
+
Agent 交给 Coda、给人看的文字里,具体的时刻写成 `<t:ISO 8601>`,如 `<t:2026-10-24T18:10:00.000Z>`:日期、时分秒与时区(`Z` 或 `+08:00`)都要有,
|
|
206
|
+
JavaScript 的 `toISOString()` 就是这个写法。不要自己写成本地时间:Agent 所在机器的时区与看的人未必相同。
|
|
207
|
+
Coda 存的就是这段文字,两端客户端显示时按看的那台设备的时区写出(如 `2026/10/25 02:10`);文字被拼进交回父会话的结果时标记跟着走,
|
|
208
|
+
父会话的 Agent 读到的是 ISO 时间。读不出时间的标记原样显示。
|
|
209
|
+
|
|
210
|
+
用于失败原因(`error.message`)与[活动报告](#_codawait)的 `title`、`detail`;面板里的文字同样这样写(插件:面板(Coda 的 docs/plugins.md「面板」))。
|
|
211
|
+
不确切的时间(「一小时左右」)照常写字,不加标记。
|
|
212
|
+
|
|
213
|
+
## Agent 活动
|
|
214
|
+
|
|
215
|
+
`session/prompt` 的回复结束一轮,也就是 Agent 说眼下的信息它处理完了:Coda 据此宣告完成,把子会话这次调用的结果交回父会话。
|
|
216
|
+
一轮之后 Agent 还可能有东西在进行:放到后台的命令、派出去的子 Agent、到点会叫醒它的定时。ACP 不说这些,Coda 看不见就会把会话
|
|
217
|
+
显示成空闲、回收它的进程,也不知道它们之后叫醒 Agent 的那一轮属于哪次调用。Agent 活动报告让 Agent 把这些告诉 Coda,
|
|
218
|
+
每一样一项,从开始到结束;它们不挡完成([规则](#规则)第 2 条)。
|
|
219
|
+
|
|
220
|
+
### 能力协商
|
|
221
|
+
|
|
222
|
+
- Coda 在 `initialize` 的 `clientCapabilities` 里总是带 `_meta.coda.waits: {}`。没看到它的客户端不认识这条扩展,Agent 不应对它发 `_coda/wait`。
|
|
223
|
+
- Agent 在 `initialize` 结果的 `agentCapabilities` 里带 `_meta.coda.waits: {}`,表示它会登记。这只是声明:Coda 把它记在托管握手里,
|
|
224
|
+
健康检查(`GET /v1/health` 的会话项 `waits`)报出来;不声明的 Agent 发来的 `_coda/wait` 也照收。
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"_meta":{"coda":{"waits":{}}}}}}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `_coda/wait`
|
|
231
|
+
|
|
232
|
+
Agent 发给 Coda 的通知(没有 `id`,不回复),和 `session/update` 走同一个顺序:说在前面的先处理。
|
|
233
|
+
|
|
234
|
+
| 字段 | 必填 | 说明 |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| `sessionId` | 是 | 它属于哪个会话:本会话,或 Coda 已经认得的子会话(之前某项活动的 `childSessionId`)。子 Agent 自己起的活动从子 Agent 的会话报,起因记为那个子 Agent([规则](#规则)第 1 条)。别的会话的一律丢弃 |
|
|
237
|
+
| `waitId` | 是 | Agent 自己取的身份,同一会话内不重复,至多 200 字 |
|
|
238
|
+
| `state` | 是 | `waiting`、`handling`、`done`、`failed`、`cancelled`,见下 |
|
|
239
|
+
| `kind` | 否 | `task`(默认)、`subagent`、`timer`,或别的词(按 `task` 算);第一次报告时定下,之后可以省略,改了也按第一次的算 |
|
|
240
|
+
| `title` | 否 | 给人看的名字,至多 500 字 |
|
|
241
|
+
| `detail` | 否 | 说明:任务在做什么,定时按什么计划触发;至多 4000 字。其中的时刻按[文字里的时刻](#文字里的时刻)写 |
|
|
242
|
+
| `stoppable` | 否 | Coda 能不能经 [`_coda/wait/stop`](#_codawaitstop) 停它,默认不能 |
|
|
243
|
+
| `childSessionId` | `subagent` 知道时带 | 子 Agent 自己的会话。从此这个会话的 `session/update`、审批与提问也收,记为这个子 Agent 的。不带时子 Agent 照样登记,但 Coda 不认得它的会话:那个会话发来的 `session/update`、`_coda/wait` 丢弃,审批与提问拒绝,委派清单里这一项点开没有过程,也不能照[规则](#规则)第 5 条继续同一个子 Agent |
|
|
244
|
+
| `model` | 否 | 子 Agent 的实际模型 |
|
|
245
|
+
| `outputFile` | 否 | 任务输出所在的文件,任务详情里显示 |
|
|
246
|
+
| `toolCallId` | 否 | 启动它的那次工具调用,同一会话里的 `toolCallId`:任务详情据此显示启动命令;它没结束时,那次调用缺结果也不被收束为结果未确认。后台命令的原调用保留 Agent 报的状态,记录 `backgroundTaskId` 关联任务;展示为已转入后台,不再计入前台执行 |
|
|
247
|
+
| `at` | `timer` 知道时带 | 下次触发的时刻,ISO 8601。不带时会话底部的定时一行只写 `detail`(没有就是标题),不显示下次时间;定时照样登记,到点仍由 Agent 报 `handling` |
|
|
248
|
+
| `recurring` | 否 | 定时是否反复触发 |
|
|
249
|
+
|
|
250
|
+
缺必填字段、`state` 不在上表、字段过长或 `at` 读不出时间的,整条丢弃,不报错。之后的报告只需带变了的字段,没带的保持原样。
|
|
251
|
+
|
|
252
|
+
| `state` | 含义 |
|
|
253
|
+
| --- | --- |
|
|
254
|
+
| `waiting` | 在等,Agent 没在处理它 |
|
|
255
|
+
| `handling` | Agent 正在处理它:后台命令结束了、子 Agent 交回了、定时到点了,Agent 据此干活。见[规则](#规则)第 3 条 |
|
|
256
|
+
| `done`、`failed`、`cancelled` | 结束了:做完、失败、被停止。终态是最终的,同一个 `waitId` 之后再说什么都不算;第一次报告就是终态的不登记 |
|
|
257
|
+
|
|
258
|
+
`kind` 决定 Coda 把它当成什么,不只是怎么显示:
|
|
259
|
+
|
|
260
|
+
| `kind` | Coda 当它是 | 显示 | 会话的事实(Coda 的 docs/session-contracts.md「事件」) |
|
|
261
|
+
| --- | --- | --- | --- |
|
|
262
|
+
| `task` 与其他词 | 后台命令 | 发起记录留在原位,任务等待时沉底、结束时留在自己的结束位置;`stoppable` 时有停止按钮;圆环「后台任务运行中」 | `background` |
|
|
263
|
+
| `subagent` | 委派 | 委派清单(Coda 的 docs/subagent-design.md「委派清单」)里的一项(`kind: task`),点开是它的过程视图,子会话的输出都归它;圆环「等待后续结果」,同子会话 | `background` |
|
|
264
|
+
| `timer` | 定时唤醒 | 会话底部的定时一行(计划、是否反复);不进历史 | `scheduled`,不算 `background` |
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{"jsonrpc":"2.0","method":"_coda/wait","params":{"sessionId":"s1","waitId":"build-1","kind":"task","state":"waiting","title":"构建","detail":"npm run build","stoppable":true,"outputFile":"/tmp/build-1.log"}}
|
|
268
|
+
{"jsonrpc":"2.0","method":"_coda/wait","params":{"sessionId":"s1","waitId":"build-1","state":"handling"}}
|
|
269
|
+
{"jsonrpc":"2.0","method":"_coda/wait","params":{"sessionId":"s1","waitId":"build-1","state":"done"}}
|
|
270
|
+
{"jsonrpc":"2.0","method":"_coda/wait","params":{"sessionId":"s1","waitId":"review","kind":"subagent","state":"waiting","title":"评审改动","childSessionId":"s1/review","stoppable":true}}
|
|
271
|
+
{"jsonrpc":"2.0","method":"_coda/wait","params":{"sessionId":"s1","waitId":"hourly","kind":"timer","state":"waiting","title":"每小时看一次 CI","at":"2026-10-12T10:00:00Z","recurring":true}}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### `_coda/wait/stop`
|
|
275
|
+
|
|
276
|
+
Coda 发给 Agent 的请求,参数 `{ "sessionId": …, "waitId": … }`,Agent 回 `{}`。只对声明了 `stoppable` 的活动发:
|
|
277
|
+
用户点了任务或子 Agent 的停止、父会话取消这次调用(含已经交回、还在为补交跟进的)时它树下的项。
|
|
278
|
+
回复只表示收到;Coda 不当它已经停了,直到 Agent 随后用 `_coda/wait` 报它结束(通常是 `cancelled`)。
|
|
279
|
+
|
|
280
|
+
### 规则
|
|
281
|
+
|
|
282
|
+
1. **起因**:一项活动的起因是登记它时正在进行的那一轮;从子 Agent 的会话报来的(子 Agent 自己起的后台命令、再派的子 Agent),
|
|
283
|
+
起因是那个子 Agent 的这次运行,不论报来时有没有轮次在进行。没有轮次在进行、又不是子 Agent 起的,起因未知(见起因(Coda 的 docs/session-contracts.md「起因」)):
|
|
284
|
+
Agent 在一轮之外自己开始干活,就要在这一轮里报 `handling` 说明是为哪一项(第 3 条);说不出来时自造的那一项起因未知。
|
|
285
|
+
它归起因沿链走得到的那次调用:父会话派来的那一轮里登记的,归那次调用;用户直接发的消息那一轮里登记的,不归父会话的调用;
|
|
286
|
+
起因未知的算每次调用的。
|
|
287
|
+
2. **不挡完成**:活动在进行时会话照样静止(会话静止(Coda 的 docs/session-contracts.md「会话静止」)):这一轮结束就宣告完成、交回调用,
|
|
288
|
+
交回的结果写明这次调用还有哪些活动在进行;它们之后叫醒 Agent 的那一轮结束时,那一轮说的作为这次调用的补交交回,
|
|
289
|
+
都结束时再说一次(补交(Coda 的 docs/subagent-design.md「补交」))。活动在进行时进程不被空闲回收,`!` 命令不执行,会话照实显示。
|
|
290
|
+
3. **处理(`handling`)**:Agent 在一轮之外处理活动时,Coda 为它开一轮自主轮次。这一轮的起因是最先进入 `handling` 的那一项,
|
|
291
|
+
所以这一轮属于那一项的调用,说的话作为它的补交交回。这一轮进行中别的项进入 `handling`,并入这一轮,起因不变。所有项都离开 `handling`
|
|
292
|
+
(回到 `waiting`,或结束)时这一轮结束。这一轮里 Agent 的输出、审批与提问都归它,子会话发来的也一样;用户这时发的消息排队,等这一轮结束再送。
|
|
293
|
+
在 Coda 发起的一轮(`session/prompt`)进行中进入 `handling` 的,算在那一轮里;那一轮结束时还有项在 `handling`,接着开一轮自主轮次。
|
|
294
|
+
用户停止这一轮时,Coda 对本会话发 `session/cancel`:Agent 应停止处理,把这些项改回 `waiting` 或报结束;这一轮随之结束,
|
|
295
|
+
因为分不清停止和做完谁先到,结果记为未知。30 秒内这一轮还没结束,Coda 关掉托管连接与它的进程组,结果未知,不自动重跑。
|
|
296
|
+
4. **定时**:`kind: timer` 带 `recurring`,知道下次触发的时刻就带 `at`。到点时报 `handling`,处理完反复的回到 `waiting`(可以带新的 `at`),
|
|
297
|
+
一次性的报 `done`。`at` 是触发时刻,不是期限:到点没报不算过期。父会话取消这次调用时,它树下 `stoppable` 的定时经 `_coda/wait/stop` 停掉,
|
|
298
|
+
其余的 Coda 不删除,这次调用不再为它们补交,并如实告诉父会话留下了哪几个(要删得请 Agent 自己删)。
|
|
299
|
+
5. **子会话与继续**:`childSessionId` 让 Coda 认得那个会话。子 Agent 结束后,Agent 以新的 `waitId`、同一个 `childSessionId` 再登记一次,
|
|
300
|
+
就是继续同一个子 Agent:委派清单里还是那一项,多一次运行,这一次的起因是再登记时进行的那一轮。
|
|
301
|
+
6. **进程结束**:Agent 进程结束(崩溃、停止超时被关、空闲回收、机器重启),它所有没结束的活动以 `disconnected` 结束,结果未知,
|
|
302
|
+
Coda 不重放;为它们跟进的调用随之补交一次,写明结果未知。后端重启而托管还在时一切照旧:活动、`handling` 的那一轮都接回,
|
|
303
|
+
后端不在期间的 `_coda/wait` 从托管日志续读,各只算一次。
|
|
304
|
+
7. **会话加载中**:`session/new`、`session/load` 期间发来的 `_coda/wait` 先存着,会话身份确定后按原顺序处理,只收本会话与这些登记里公告的子会话。
|
|
305
|
+
所以 Agent 恢复会话时可以把还在等的东西重报一遍:同一个 `waitId` 再报 `waiting` 不会多登记一项。
|
|
306
|
+
|
|
307
|
+
## 远端入口
|
|
308
|
+
|
|
309
|
+
有的 Agent 不在这台机器上干活:它是别处的一个 Bot(比如 Grok Bot,在它自己的云电脑上),会话里的适配器只是它的代言。
|
|
310
|
+
Bot 需要随时进到自己的会话里来:说话、调 Coda 的工具、开一轮。远端入口给它一条只通往这个会话的路。
|
|
311
|
+
|
|
312
|
+
- **能力**:Coda 在 `initialize` 的 `clientCapabilities._meta.coda.remote` 里带 `{}`;Agent 在 `agentCapabilities._meta.coda.remote` 里声明 `{}`,
|
|
313
|
+
才会被转交远端请求。
|
|
314
|
+
- **绑定码**:Agent 向 Coda 发请求 `_coda/remote/code`(`{ sessionId }`,只能为本会话),Coda 回 `{ code, expiresAt, addresses }`:
|
|
315
|
+
一次性的绑定码,10 分钟内有效;`addresses` 是这台后端在 tailnet 里的入口(`http://<节点地址>:8080`),这台的网络节点不在 tailnet 里时为空。
|
|
316
|
+
怎么把码交给 Bot 是 Agent 的事(比如在会话里给出一段让用户贴给 Bot 的话)。
|
|
317
|
+
- **绑定**:Bot 以 `POST /v1/remote-agent/bind`、正文 `{ code }` 换得 `{ token, session }`。不需凭据,每分钟最多 30 次;码用过即失效。
|
|
318
|
+
同一会话再绑定,新令牌取代旧的。令牌与配对设备的凭据存在一起(`devices.sqlite`),只存摘要。
|
|
319
|
+
- **调用**:Bot 以 `POST /v1/remote-agent`,头 `Authorization: Bearer <token>`,正文 `{ method, params }`(`params` 是对象,可省略,正文至多 256 KiB)。
|
|
320
|
+
Coda 找到令牌对应的会话,Agent 进程没在运行时先恢复它(`session/load`,不开轮次;与同时到的一轮共用这一次启动),再向它发请求 `_coda/remote`:
|
|
321
|
+
`{ sessionId, method, params }`,`method`、`params` 原样,Coda 不解释。Agent 的回答原样作为 `{ result }` 回给 Bot。
|
|
322
|
+
- **拒绝**:令牌不对 401;会话所在的目录已删除、会话还没开始过(`invalid_state`),会话家族已归档(子会话随根会话,`archived`),或 Agent 没声明能力(`remote_unsupported`)409;
|
|
323
|
+
Agent 回 JSON-RPC 错误时 502(`agent_error`,`message` 照 Agent 说的);Agent 进程在回答前结束 503(`connection_lost`)。Coda 不替 Agent 设回答期限;
|
|
324
|
+
从收到请求(含为它启动 Agent)到 Agent 回答,会话算在忙:进程不因空闲被回收,所在目录也不能删除。
|
|
325
|
+
- **令牌只通往这个会话**:它不是设备凭据,进不了别的接口。Bot 在会话里做的事经 Agent 走:说话、开轮次照[Agent 活动](#agent-活动);
|
|
326
|
+
调 Coda 的工具由 Agent 代它调,给别的会话发消息照常出授权卡片。远端客户端发起独立工作用 `new_session`。
|
|
327
|
+
- **入口在哪**:手机入口(手机连接(Coda 的 docs/ios-development.md「手机连接」)),这台的网络节点把 tailnet 里到 8080 的请求转给它。
|
|
328
|
+
|
|
329
|
+
### 远端客户端新建独立会话
|
|
330
|
+
|
|
331
|
+
只给 Agent 声明了 `agentCapabilities._meta.coda.remote` 的会话提供 MCP 工具 `new_session`;
|
|
332
|
+
会话启动时生成工具列表,其他会话不列出它,直接调用也拒绝。
|
|
333
|
+
Bot 是工作的发起者:批准后开的是普通根会话,不是它的子会话,也不是一次会话间调用。
|
|
334
|
+
|
|
335
|
+
参数为 `project`、`agent`、`prompt`、`requestId`、`turn`,以及可选的 `model`、`effort`、`mode`、`options`、`title`。
|
|
336
|
+
`project` 按 `spawn` 的规则找:本机用名字或 ID,其他机器用 ID、经状态层定位。
|
|
337
|
+
`turn` 是 `status` 给的轮次票据,新请求要有效;同一 `requestId`、相同内容重试恢复原请求,不重复出卡片或创建,
|
|
338
|
+
已存在请求的回放不再校验票据,不同内容报冲突。省略标题用 prompt 的前 80 字,省略配置项用目标 Agent 默认值。
|
|
339
|
+
|
|
340
|
+
每个请求都在发起会话里出授权卡片,包括在自己的目录里新建:注明机器、目录名与路径、Agent、模型、强度、审批档位、
|
|
341
|
+
其他配置、标题和任务全文;其他配置逐项写进卡片说明,让两端在批准前都能看到。只有「允许」「拒绝」,没有「都允许」,也不沿用 `spawn` / `send` 的授权。
|
|
342
|
+
工具出卡片后立即返回「已请用户批准;结果会作为新消息告诉你。不要轮询或重复调用;没有其他工作就结束本轮。」
|
|
343
|
+
不在调用里等用户,卡片的提醒、归档失效沿用会话授权卡片。
|
|
344
|
+
|
|
345
|
+
允许后在目标目录的原目录建根会话,第一条消息就是 `prompt`,审批档位保留卡片批准的参数,不跟随 Bot;标题固定为批准的标题。
|
|
346
|
+
没有 parent、没有调用或结果投递;首条输入为用户的任务,结果、通知和 PR 事件的 `coda.session` 都归新会话自己。
|
|
347
|
+
会话数据的 `initiatedBy` 保存 `{threadId, title, reference}`,表示「由哪个会话发起」,不转移回复权或附带旧上下文;
|
|
348
|
+
`reference` 是发起会话的 `coda://threads/…`。两端客户端尚未显示这个来源字段。
|
|
349
|
+
|
|
350
|
+
决定以一条 Coda 通知唤醒发起会话:允许为「已开出:标题(Coda 的 docs/coda://threads/<新会话 ID>)」,拒绝为「用户拒绝了:<标题>」,
|
|
351
|
+
新会话后续的结果不回传给 Bot。已决定请求的工具重试返回这句结果,仍保留应交的通知。
|
|
352
|
+
其他机器上的创建走状态层转发的普通首次发送接口;发送前持久化允许决定与提交标识,卡片进入 `submitting`。
|
|
353
|
+
目标暂不可达或应答丢失时,使用原提交标识与允许决定重试恢复同一次创建,不再接受拒绝或其他提交;
|
|
354
|
+
工具重试返回「用户已批准,正在确认新会话;结果会作为新消息告诉你。」,收到创建结果后才通知已开出。
|
|
355
|
+
目标明确拒绝(例如批准前目录已删除)则结算为失败,通知「未能开出:<标题>(<原因>)」;
|
|
356
|
+
原请求重试返回该失败,不再尝试创建,也不把它说成用户拒绝。
|
|
357
|
+
|
|
358
|
+
## 内置 Agent 的入口进程
|
|
359
|
+
|
|
360
|
+
Coda 自带的 Agent 都是内置插件(清单见插件:Agent(Coda 的 docs/plugins.md「agent」)),照本协议接入。源码与入口自己的测试在公开仓库
|
|
361
|
+
coda-console/coda-plugins 的 `plugins/<插件>/`、`test/plugins/<插件>/`,发布为 npm 包 `@coda-console/<插件>`,Coda 以钉住版本的依赖装在 `backend/node_modules/` 里
|
|
362
|
+
(开发与验证:coda-plugins 维护的内置插件(Coda 的 docs/development.md「coda-plugins-维护的内置插件」))。各家要做的特殊处理在它自己的入口进程里(插件的 `agent/`),
|
|
363
|
+
入口只用插件目录里的代码、它 `package.json` 声明的 npm 依赖与 Node 自带的模块,不引用 Coda 后端的模块。怎么启动写在清单的 `command` 里:
|
|
364
|
+
插件自带的入口由 Coda 的 Node(`CODA_NODE`)从运行时副本里的插件目录运行,其余是运行机上的命令。
|
|
365
|
+
从仓库安装(Coda 的 docs/plugins.md「从仓库安装」)的插件,入口从它的版本目录运行,只能用插件目录里的代码、它自己的 npm 依赖(Coda 的 docs/plugins.md「npm-依赖」)与 Node 自带的模块。
|
|
366
|
+
直接运行运行机命令的插件(上游 ACP 不接受 `systemPrompt`)也带一个薄入口:转发 ACP,声明会话指令,并在原生会话第一轮把指令放在用户输入前面送入([会话指令](#会话指令))。
|
|
367
|
+
|
|
368
|
+
| Agent | 入口 | 它在协议之外替 Agent 做的 |
|
|
369
|
+
| --- | --- | --- |
|
|
370
|
+
| Codex(插件 `codex`) | `agent/entry.mjs`:在同一进程里运行锁定版本的 codex-acp,加载 Coda 的源码补丁(适配器一变就启动失败) | 向 codex-acp 声明 JetBrains AIR 的 `nativeSubagentSessions` 与 `asyncTasks`,把 AIR 报的子 Agent 与转入后台的命令翻成 `_coda/wait`:命令带 `toolCallId`,codex-acp 说能停的经 AIR 的停止(发给根会话)停;子 Agent 在 codex-acp 说能取消时经取消它的会话停。codex-acp 把继续的子 Agent 叫 `X:generation:N`,对 Coda 一律是 `X`,每次运行一个新的 `waitId`。会话默认以只读模式开始(`INITIAL_AGENT_MODE`,运行机设了就用它的);Coda 选的账号经账号桥接上。Codex 没有自主轮次,不报 `handling`。会话指令经 codex-acp 的 `thread/start`、`thread/resume` 作为 `developerInstructions` 交给 Codex,接在运行机配置自己的 `developer_instructions` 后面;声明插话,一轮已经结束时收到的插话回 `promptRequired`(Coda 的补丁,codex-acp 原本会自己另开一轮) |
|
|
371
|
+
| Claude(插件 `claude`,Agent `claude-sdk`) | `agent/entry.mjs`:Coda 基于 Claude Agent SDK 写的 ACP Agent,一个进程一个 SDK 会话;SDK 是插件依赖的 `@anthropic-ai/claude-agent-sdk` | 会话 ID 由入口生成;模型一律是具体 ID(SDK 列出的 `resolvedModel` 加上下文变体,不接受别名)。后台任务、子 Agent 与 Stop hook 列出的定时唤醒报成 `_coda/wait`(定时的计划不是五段 cron、算不出下次时间时不带 `at`):子 Agent 的会话是启动它的工具调用 ID,SendMessage 续聊是同一会话的新一次运行;子 Agent 起的后台任务与子 Agent 在它自己的会话里报;没有轮次时结束的任务,等一轮接手或会话空闲才了结。Claude 自己开始的一轮,先看开场消息指名的:到点的定时、结束通知所属的任务、交回报告的子 Agent(它已结束时才算;还在跑的子 Agent 发来进展不算)。指名的已处理过(Claude Code 为子 Agent 的结束通知另开的、不调模型的一轮)或还在跑,这一轮不处理任何一项;开场什么都没指名,才依次找已结束的任务、唯一到点的定时。认定的项在任何输出与请求之前报 `handling`;什么都没认定的,等这一轮第一次说话、调用工具、审批或提问时才报自造的「Claude 自主继续」,什么都不说的一轮不报。审批的选项是仅允许本次、每条建议规则一个 `allow_always`、拒绝;SDK 不再等回答时发 `$/cancel_request`。MCP 服务器的表单原样转来,链接用 `mode: url`。额度写进插件状态目录的 `limits.json`(内置插件:Claude(Coda 的 docs/plugins.md「内置插件claude」)),标题经 `session_info_update` 报。会话指令追加在 `claude_code` 预设的系统提示词后面 |
|
|
372
|
+
| Kimi(插件 `kimi`) | `agent/entry.mjs`:把 `kimi acp` 作为子进程,在两者之间逐条转发 | 读 Kimi 的会话文件,把入口启动之后开始的后台命令与子 Agent 报成 `_coda/wait`(子 Agent 的会话是它的 agentId,过程报成那个会话的 `session/update`;按运行先后交付登记、过程与了结,同一子 Agent 的下一次运行等上一次了结才报,从而接着用同一张卡片);在 Coda 发起的一轮里结束的任务直接了结,因为主 Agent 已在该轮处理结果;轮外结束且结果要交给主 Agent 的才从结束起报 `handling`,主 Agent 在那之后结束一轮时了结。不能停,不带 `toolCallId`。只认 Kimi CLI 2.1.1 的文件布局,见会话行为契约(Coda 的 docs/session-contracts.md「停止恢复与进程回收」)。会话指令放在原生会话第一轮的输入前面 |
|
|
373
|
+
| Grok、OpenCode(插件 `grok`、`opencode`) | `agent/entry.mjs`:把运行机上的 `grok agent --no-leader stdio`(先找 `~/.grok/bin/grok`)、`opencode acp` 作为子进程逐条转发 | 只做会话指令:声明它,并放在原生会话第一轮的输入前面;不报告活动 |
|
|
374
|
+
|
|
375
|
+
Claude 的提问(AskUserQuestion)是这样一张表单:第 i 个问题是必填字段 `q{i}`,`title` 是问题、`description` 是它的标题;单选为
|
|
376
|
+
`type: string` 加 `oneOf: [{const, title}]`,多选为 `type: array`、`items.anyOf`、`minItems: 1`。每个问题的选项最后多一个
|
|
377
|
+
`{"const": "coda:other", "title": "其他(写在下面)"}`,另有可选字段 `q{i}_other`(`type: string`)写其他的内容:表单说不出「两个字段至少填一个」,
|
|
378
|
+
所以用这个选项表示答案在旁边。
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{"type":"object","required":["q0"],"properties":{
|
|
382
|
+
"q0":{"type":"string","title":"用哪种方案?","description":"方案","oneOf":[{"const":"小改","title":"小改 — 只动接口"},{"const":"coda:other","title":"其他(写在下面)"}]},
|
|
383
|
+
"q0_other":{"type":"string","title":"其他"}}}
|
|
384
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coda-console/protocol",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Coda 的协议:插件清单的类型(JSON Schema)、通用校验器与 Agent 接入协议",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -9,5 +10,21 @@
|
|
|
9
10
|
},
|
|
10
11
|
"publishConfig": {
|
|
11
12
|
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": ["schemas", "validate.mjs", "docs"],
|
|
15
|
+
"exports": {
|
|
16
|
+
"./validate": "./validate.mjs",
|
|
17
|
+
"./schemas/*": "./schemas/*",
|
|
18
|
+
"./docs/*": "./docs/*",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"ajv": "8.20.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test 'test/**/*.test.mjs'"
|
|
12
29
|
}
|
|
13
30
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://coda.pub/schemas/plugin/v1/agents.json",
|
|
4
|
+
"title": "contributes.agents:插件带来的 Agent",
|
|
5
|
+
"description": "每个是一个讲 ACP(加 Coda 的等待项)的程序:它是谁、客户端怎么画它、Coda 怎么启动它。",
|
|
6
|
+
"type": "array",
|
|
7
|
+
"items": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": {"$ref": "common.json#/$defs/id"},
|
|
11
|
+
"displayName": {"$ref": "common.json#/$defs/line40"},
|
|
12
|
+
"icon": {
|
|
13
|
+
"description": "图标:light(必填)与 dark 是插件目录里的 PDF,各不超过 32 KiB;template 是开或关",
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {"light": {"$ref": "#/$defs/icon"}, "dark": {"$ref": "#/$defs/icon"}, "template": {"type": "boolean"}},
|
|
16
|
+
"required": ["light"],
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
},
|
|
19
|
+
"command": {"$ref": "common.json#/$defs/command"},
|
|
20
|
+
"modes": {
|
|
21
|
+
"description": "审批档位的说法:title 不超过 10 字,unset 不超过 20 字,note 不超过 100 字,auto、full 各不超过 200 字",
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"title": {"$ref": "common.json#/$defs/line10"},
|
|
25
|
+
"unset": {"$ref": "common.json#/$defs/line20"},
|
|
26
|
+
"note": {"$ref": "common.json#/$defs/line100"},
|
|
27
|
+
"auto": {"$ref": "common.json#/$defs/line200"},
|
|
28
|
+
"full": {"$ref": "common.json#/$defs/line200"}
|
|
29
|
+
},
|
|
30
|
+
"additionalProperties": false
|
|
31
|
+
},
|
|
32
|
+
"skillsDir": {
|
|
33
|
+
"description": "Agent 读 Skill 的目录:工作区里不离开它的相对路径,至多 3 段",
|
|
34
|
+
"allOf": [{"$ref": "common.json#/$defs/line1000"}, {"pattern": "^(?!/)(?!(.*/)?\\.\\.(/|$))"}, {"pattern": "^[^/]+(/+[^/]+){0,2}/*$"}]
|
|
35
|
+
},
|
|
36
|
+
"configFiles": {"description": "Agent 读模型的配置文件,至多 16 个", "type": "array", "maxItems": 16, "items": {"$ref": "common.json#/$defs/line1000"}},
|
|
37
|
+
"setupHint": {"$ref": "common.json#/$defs/line200"}
|
|
38
|
+
},
|
|
39
|
+
"required": ["id", "displayName", "icon", "command"],
|
|
40
|
+
"additionalProperties": false
|
|
41
|
+
},
|
|
42
|
+
"x-unique-by": ["id"],
|
|
43
|
+
"$defs": {
|
|
44
|
+
"icon": {
|
|
45
|
+
"description": "插件目录里的 PDF,不超过 32 KiB",
|
|
46
|
+
"allOf": [{"$ref": "common.json#/$defs/line1000"}],
|
|
47
|
+
"x-file": {"type": "file", "maxBytes": 32768, "magic": "%PDF-"}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://coda.pub/schemas/plugin/v1/capabilities.json",
|
|
4
|
+
"title": "contributes.capabilities:插件带来的能力",
|
|
5
|
+
"description": "能力名的数组,每个是 .coda/capabilities/<名>.bpmn;只能被调用(workflow_run、callActivity),没有触发器。",
|
|
6
|
+
"type": "array",
|
|
7
|
+
"uniqueItems": true,
|
|
8
|
+
"items": {
|
|
9
|
+
"description": "能力名:小写字母、数字和「-」,以字母开头,有对应的 .coda/capabilities/<名>.bpmn,不能同时是流程(.coda/workflows/<名>.bpmn)",
|
|
10
|
+
"$ref": "common.json#/$defs/name",
|
|
11
|
+
"allOf": [
|
|
12
|
+
{"x-file": {"type": "file", "under": ".coda/capabilities", "suffix": ".bpmn"}},
|
|
13
|
+
{"x-file": {"type": "file", "under": ".coda/workflows", "suffix": ".bpmn", "absent": true}}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://coda.pub/schemas/plugin/v1/commands.json",
|
|
4
|
+
"title": "contributes.commands:放在会话与步骤 PATH 最前的命令",
|
|
5
|
+
"description": "命令名 → 由 sh 运行的一行命令(带上参数)。",
|
|
6
|
+
"type": "array",
|
|
7
|
+
"items": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"description": "命令名:小写字母、数字和「-」,以字母开头,不能是 Coda 自己的 coda-run、coda-event、coda-resources",
|
|
12
|
+
"$ref": "common.json#/$defs/id",
|
|
13
|
+
"not": {"enum": ["coda-run", "coda-event", "coda-resources"]}
|
|
14
|
+
},
|
|
15
|
+
"command": {"$ref": "common.json#/$defs/command"}
|
|
16
|
+
},
|
|
17
|
+
"required": ["name", "command"],
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
},
|
|
20
|
+
"x-unique-by": ["name"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://coda.pub/schemas/plugin/v1/common.json",
|
|
4
|
+
"title": "插件清单里共用的写法",
|
|
5
|
+
"$defs": {
|
|
6
|
+
"id": {"description": "小写字母、数字和「-」,以字母开头,至多 40 个字符", "type": "string", "pattern": "^[a-z][a-z0-9-]{0,39}$"},
|
|
7
|
+
"name": {"description": "小写字母、数字和「-」,以字母开头,至多 64 个字符", "type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$"},
|
|
8
|
+
"line10": {"description": "不超过 10 字的一行", "type": "string", "maxLength": 10, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
9
|
+
"line20": {"description": "不超过 20 字的一行", "type": "string", "maxLength": 20, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
10
|
+
"line40": {"description": "不超过 40 字的一行", "type": "string", "maxLength": 40, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
11
|
+
"line100": {"description": "不超过 100 字的一行", "type": "string", "maxLength": 100, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
12
|
+
"line200": {"description": "不超过 200 字的一行", "type": "string", "maxLength": 200, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
13
|
+
"line1000": {"description": "不超过 1000 字的一行", "type": "string", "maxLength": 1000, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
14
|
+
"line4096": {"description": "不超过 4096 字的一行", "type": "string", "maxLength": 4096, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
15
|
+
"command": {"description": "一行命令,由 sh 运行,不超过 4000 字", "type": "string", "maxLength": 4000, "pattern": "^(?=.*\\S)[^\\u0000\\r\\n]*$"},
|
|
16
|
+
"symbol": {"description": "SF Symbol 的名字", "type": "string", "maxLength": 100, "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)*$"},
|
|
17
|
+
"commandHolder": {
|
|
18
|
+
"description": "{ command }:command 是一行命令",
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {"command": {"$ref": "#/$defs/command"}},
|
|
21
|
+
"required": ["command"],
|
|
22
|
+
"additionalProperties": false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|