@armadra/agent 0.6.8 → 0.7.1

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/CHANGELOG.zh-CN.md +50 -0
  3. package/README.md +4 -3
  4. package/README.zh-CN.md +1 -1
  5. package/dist/acp.d.ts +2 -2
  6. package/dist/acp.js +2 -2
  7. package/dist/ai/apis/shared.d.ts +7 -1
  8. package/dist/ai/apis/shared.js +10 -0
  9. package/dist/ai/fake/fake-provider.js +5 -1
  10. package/dist/ai/fake/fake-script.d.ts +5 -2
  11. package/dist/ai/fake/fake-script.js +6 -2
  12. package/dist/ai/types.d.ts +2 -1
  13. package/dist/auth/oauth/token-store.d.ts +2 -0
  14. package/dist/auth/oauth/token-store.js +7 -2
  15. package/dist/bundle/ama.cjs +48975 -47637
  16. package/dist/checkpoints/shadow-git.js +2 -1
  17. package/dist/cli/args.d.ts +6 -0
  18. package/dist/cli/args.js +46 -0
  19. package/dist/cli/bootstrap.js +13 -2
  20. package/dist/cli/compose-events.d.ts +10 -0
  21. package/dist/cli/compose-events.js +97 -0
  22. package/dist/cli/compose-session.d.ts +25 -4
  23. package/dist/cli/compose-session.js +130 -103
  24. package/dist/cli/compose.js +6 -1
  25. package/dist/cli/runtime.d.ts +3 -1
  26. package/dist/cli/subcommands/auth.d.ts +13 -1
  27. package/dist/cli/subcommands/auth.js +28 -1
  28. package/dist/config/auth-file.js +10 -2
  29. package/dist/config/fs-retry.d.ts +18 -0
  30. package/dist/config/fs-retry.js +33 -0
  31. package/dist/drivers/acp/client.d.ts +7 -2
  32. package/dist/drivers/acp/client.js +10 -1
  33. package/dist/drivers/acp/driver.d.ts +15 -4
  34. package/dist/drivers/acp/driver.js +110 -33
  35. package/dist/drivers/acp/testing/fake-agent-main.d.ts +1 -1
  36. package/dist/drivers/acp/testing/fake-agent-main.js +9 -2
  37. package/dist/drivers/acp/testing/fake-agent.d.ts +17 -2
  38. package/dist/drivers/acp/testing/fake-agent.js +108 -25
  39. package/dist/drivers/acp/types.d.ts +76 -12
  40. package/dist/drivers/acp/types.js +11 -3
  41. package/dist/drivers/jsonrpc.d.ts +13 -3
  42. package/dist/drivers/jsonrpc.js +40 -7
  43. package/dist/drivers/turn.d.ts +15 -2
  44. package/dist/drivers/turn.js +33 -4
  45. package/dist/i18n/catalog.d.ts +59 -16
  46. package/dist/i18n/catalog.js +4 -1
  47. package/dist/i18n/messages/acp.d.ts +125 -0
  48. package/dist/i18n/messages/acp.js +126 -0
  49. package/dist/i18n/messages/auth.d.ts +2 -0
  50. package/dist/i18n/messages/auth.js +4 -2
  51. package/dist/i18n/messages/cli-args.d.ts +2 -0
  52. package/dist/i18n/messages/cli-args.js +2 -0
  53. package/dist/i18n/messages/cli.d.ts +4 -0
  54. package/dist/i18n/messages/cli.js +2 -0
  55. package/dist/i18n/messages/print.d.ts +3 -33
  56. package/dist/i18n/messages/print.js +3 -33
  57. package/dist/modes/acp/acp-auth-gate.d.ts +40 -0
  58. package/dist/modes/acp/acp-auth-gate.js +201 -0
  59. package/dist/modes/acp/acp-config.d.ts +43 -0
  60. package/dist/modes/acp/acp-config.js +151 -0
  61. package/dist/modes/acp/acp-connection.d.ts +29 -0
  62. package/dist/modes/acp/acp-connection.js +37 -0
  63. package/dist/modes/acp/acp-events.d.ts +59 -23
  64. package/dist/modes/acp/acp-events.js +154 -88
  65. package/dist/modes/acp/acp-mode.d.ts +13 -2
  66. package/dist/modes/acp/acp-mode.js +23 -8
  67. package/dist/modes/acp/acp-server.d.ts +59 -32
  68. package/dist/modes/acp/acp-server.js +322 -128
  69. package/dist/modes/acp/acp-sessions.d.ts +80 -0
  70. package/dist/modes/acp/acp-sessions.js +157 -0
  71. package/dist/modes/acp/acp-tool-text.d.ts +23 -0
  72. package/dist/modes/acp/acp-tool-text.js +83 -0
  73. package/dist/modes/print/json-event.d.ts +2 -1
  74. package/dist/modes/print/json-event.js +6 -1
  75. package/dist/tools/edit.d.ts +6 -2
  76. package/dist/tools/edit.js +19 -2
  77. package/dist/tools/types.d.ts +11 -0
  78. package/dist/tools/types.js +2 -0
  79. package/dist/tools/write.d.ts +1 -0
  80. package/dist/tools/write.js +21 -3
  81. package/docs/acp.md +143 -29
  82. package/docs/agents.md +2 -0
  83. package/docs/codemode.md +1 -1
  84. package/docs/en/acp.md +192 -0
  85. package/docs/en/sessions.md +1 -1
  86. package/docs/sessions.md +1 -1
  87. package/package.json +1 -1
package/docs/acp.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ACP(Agent Client Protocol)
2
2
 
3
+ [English](en/acp.md) · 简体中文
4
+
3
5
  ama 在 ACP 两侧都能用:
4
6
 
5
7
  - **服务端**:`ama --mode acp` 把 ama 暴露为 ACP Agent,供 Zed、JetBrains、Armadra 的 ACP 节点驱动;
@@ -17,42 +19,128 @@ JSON-RPC 2.0 over NDJSON(stdio):只按 `\n` 切行,64 KiB 分片写并
17
19
  ama --mode acp # 与 -p 互斥;其余参数(--model、--profile、--trust 等)照常
18
20
  ```
19
21
 
20
- | 方法 | ama 的行为 |
21
- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
- | `initialize` | `protocolVersion: 1`;`loadSession: true`,`sessionCapabilities: { list, resume, close }`,`promptCapabilities: { image: true, embeddedContext: true }`;不要认证 |
23
- | `session/new` | 新开会话(启动时那个空会话第一次直接认领);`cwd` 必须是 ama 的启动目录(按 realpath 比较),否则 invalid params |
24
- | `session/load` | 切到该会话并以 `session/update` 回放历史(用户消息、回复、思考、工具调用) |
25
- | `session/resume` | 切到该会话,不回放 |
26
- | `session/list` | 启动目录下的会话(标题取会话名或首条提示) |
27
- | `session/close` | 中断运行并释放活动会话 |
28
- | `session/prompt` | 文本与图片照收;`resource_link` 以 `@uri` 文本给出,嵌入资源取文本。回合结束:中断 → `cancelled`,输出截断 → `max_tokens`,出错 → JSON-RPC 错误 |
29
- | `session/cancel`(通知) | 中断当前回合 |
30
- | `session/set_mode` | 模式 id 就是 ama 的权限模式(`plan`、`allowlist`、`default`、`auto-edit`、`auto`、`full-auto`) |
31
-
32
- 一次只有一个活动会话;对非活动会话发 `session/prompt` 时(空闲)先切过去,运行中切换报 invalid request。
22
+ | 方法 | ama 的行为 |
23
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `initialize` | `protocolVersion: 1`;`loadSession: true`,`sessionCapabilities: { list, resume, close }`,`promptCapabilities: { image: true, embeddedContext: true }`;有模型时 `authMethods` 为空(无模型见下文「无模型时」) |
25
+ | `authenticate` | -32602:ama 只给 terminal 型认证方法,按规范不经 `authenticate`(有无模型都一样) |
26
+ | `session/new` | 新开会话(启动时那个空会话第一次直接认领),运行中也可调;`cwd` 必须是 ama 的启动目录(按 realpath 比较),否则 invalid params |
27
+ | `session/load` | 打开该会话并以 `session/update` 回放历史(用户消息、回复、思考、工具调用);已打开的直接从内存回放 |
28
+ | `session/resume` | 打开该会话,不回放。load / resume 的 id 找不到会话文件时:是 UUID 就按原 id 新建一个空会话(空会话不落盘,ama 重启后 Zed 的 Reload Agent 等会带着它回来),否则 -32002 |
29
+ | `session/list` | 启动目录下的会话:`cwd` 给了别的目录回空列表;每页 50 条(`updatedAt` 降序),`nextCursor` 翻页,非法 `cursor` → invalid params;标题取会话名或首条提示(去掉嵌入资源块后的首行,≤ 80 字) |
30
+ | `session/close` | 中断该会话的运行(排队的提示回 `cancelled`),释放并移出本连接;之后对这个 id 发请求回 -32002,要再用先 `session/load` / `session/resume` |
31
+ | `session/prompt` | 文本与图片照收;`resource_link` 以 `@uri` 文本给出,嵌入资源取文本。别的会话在跑时排队;未打开的 id 回 -32002。回合结束:中断 → `cancelled`,输出截断 → `max_tokens`,拒答 → `refusal`,出错 → JSON-RPC 错误 |
32
+ | `session/cancel`(通知) | 在跑 → 中断;排队中 → 直接回 `cancelled` |
33
+ | `$/cancel_request`(通知) | 撤回一个挂起的 `session/prompt`:等同 `session/cancel`,该请求答 -32800。反方向见「审批」 |
34
+ | `session/set_mode` | 模式 id 就是 ama 的权限模式(`plan`、`allowlist`、`default`、`auto-edit`、`auto`、`full-auto`);按会话记,见下文「多会话」 |
35
+ | `session/set_config_option` | 改会话配置项,答复是全部配置项的新状态;见下文「配置项与命令」 |
36
+
37
+ `session/new` / `load` / `resume` 的 `mcpServers`、`additionalDirectories` 不生效:非空时 stderr 记一行后照常打开会话(理由见「偏离与不做」)。未实现的方法(`session/delete`、`logout` 等)回 -32601。
38
+
39
+ ### 多会话
40
+
41
+ 一个 `ama --mode acp` 进程可以同时打开多个会话(Zed 的多个线程共用一个连接):
42
+
43
+ - 每个打开的会话常驻内存,没发过消息的空会话切走再切回也找得到(空会话不落盘)。
44
+ - **同一时刻只跑一个回合**:别的会话在跑时,`session/prompt` 进先进先出队列,前一个结束后再开始,不再报 busy。
45
+ `session/new`、`load`、`resume`、`list`、`set_mode`、`set_config_option`、`close` 随时可调。
46
+ - 回合开始时该会话切为「前台」:宿主(`HostApi.session.*`)、Hook 的公共字段、工具看到的会话都换成它,并清掉
47
+ 「本会话允许」的记忆(切回来要重新允许,与 TUI `/resume` 一致)。
48
+ - 权限模式按会话记:对前台会话 `set_mode` 立即生效;对其它会话只记下(照样发 `current_mode_update`),轮到它跑时
49
+ 再应用到权限管线,并再发一条 `current_mode_update`。所以对排队中的会话改模式不会影响正在跑的那个。
50
+ - 每个回合结束后发 `session_info_update`(`updatedAt`,标题变了才带 `title`)。
51
+ - 关掉一个会话只释放它(跑 SessionEnd Hook);stdin 关闭时排队的提示回 `cancelled`,等在跑的结束,再依次释放全部会话。
33
52
 
34
53
  ### 事件映射
35
54
 
36
- | ama | `session/update` |
37
- | ------------------- | ---------------------------------------------------------------------------------- |
38
- | 文本增量 / 思考增量 | `agent_message_chunk` / `agent_thought_chunk` |
39
- | 模型发出工具调用 | `tool_call`(`pending`,带 `rawInput`、`kind`、`locations`) |
40
- | 工具开始 / 结束 | `tool_call_update`(`in_progress` → `completed` / `failed`,结果只带前 4 KB 文本) |
41
- | `todo` 更新 | `plan` |
42
- | 每轮结束 | `usage_update`(上下文已用、窗口、会话累计美元) |
43
- | 权限模式变化 | `current_mode_update` |
55
+ | ama | `session/update` |
56
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
57
+ | 文本增量 / 思考增量 | `agent_message_chunk` / `agent_thought_chunk` |
58
+ | 模型发出工具调用 | `tool_call`(`pending`,带 `name`、`rawInput`、`kind`、`locations`) |
59
+ | codemode 脚本里的内层调用开始 | `tool_call`(`pending`,`title` 前缀 `codemode › `,`_meta.ama.parentToolCallId` 指向外层 `codemode` 调用),随后 `in_progress` |
60
+ | 工具开始 | `tool_call_update`(`in_progress`) |
61
+ | 工具结束(含内层) | `tool_call_update`(`completed` / `failed`);`content` 为 `[diff?, 文本]`:edit / write 带 `diff`(`path`、`oldText`、`newText`,新文件 `oldText: null`),文本只带前 4 KB;`locations[].line` 是首个改动行;不发 `rawOutput` |
62
+ | `todo` 更新 | `plan` |
63
+ | 每轮结束 | `usage_update`(上下文已用、窗口、会话累计美元) |
64
+ | 权限模式变化 | `current_mode_update` |
65
+ | 模型 / 思考级别变化 | `config_option_update`(全部配置项) |
66
+ | 会话开出(new / load / resume)之后 | `available_commands_update` 与 `config_option_update` |
67
+ | 回合结束之后 | `session_info_update`(`updatedAt`;标题与上次不同才带 `title`) |
68
+
69
+ diff 的改前 / 改后全文只随实时事件走,不写进会话文件:任一侧超过 256 KiB 不带 diff,`session/load` 回放的工具结果只有前 4 KB 文本。模式列表的 `name` 是显示名(如 `Manual`、`Accept edits`),`description` 随界面语言。`session/prompt` 的结果带本回合 token 用量 `usage`(`inputTokens`、`outputTokens`、`cachedReadTokens`、`cachedWriteTokens`、`totalTokens`);该字段在 schema 1.24.1 里仍是 UNSTABLE(只在不稳定 schema 中),客户端可以忽略,以 `usage_update` 为准。
44
70
 
45
- codemode 内层调用不单列。`session/prompt` 的结果带本回合 token 用量(`inputTokens`、`outputTokens`、`cachedReadTokens`、`cachedWriteTokens`、`totalTokens`)。
71
+ `toolCallId` 在会话内唯一:上游供应商给的 id 若在后续回合重复(个别兼容接口的兜底 id、测试用假模型),线上 id 加 `#2`、`#3`… 区分,之后的状态更新、审批与回放都按这个映射(客户端按 id 合并条目,不区分会把不同调用合成一条)。
46
72
 
47
73
  ### 审批
48
74
 
49
- ama 需要询问的调用经 `session/request_permission` 交给客户端,三个选项:`allow_once`(允许)、`allow_always`(本会话允许)、`reject_once`(拒绝)。`toolCall.toolCallId` 关联到先前 `tool_call` 的 id。客户端回 `cancelled`、连接断开或回合被中断时,按无人作答处理(拒绝)。auto 模式下 ama 自己的分类器照常工作——这只影响 ama 自己的工具;ama 驱动的外部 Agent 发来的请求只交给人(见 [agents.md](agents.md))。
75
+ ama 需要询问的调用经 `session/request_permission` 交给客户端,三个选项:`allow_once`(允许)、`allow_always`(本会话允许)、`reject_once`(拒绝)。`toolCall.toolCallId` 关联到先前 `tool_call` 的 id——codemode 内层调用也是,指向那条内层 `tool_call`,不是外层 `codemode`。询问期间该调用的状态回到 `pending`,允许后再 `in_progress`(所以有审批的调用依次是 `pending → in_progress → pending → in_progress → completed`);拒绝时直接 `failed`。客户端回 `cancelled`、连接断开或回合被中断时,按无人作答处理(拒绝)。ama 这边不再需要答复时(回合被 `session/cancel` / `$/cancel_request` 中断、审批 10 分钟超时),以 `$/cancel_request { requestId }` 撤回挂起的 `session/request_permission`,客户端可以关掉对话框。auto 模式下 ama 自己的分类器照常工作——这只影响 ama 自己的工具;ama 驱动的外部 Agent 发来的请求只交给人(见 [agents.md](agents.md))。
76
+
77
+ ### 无模型时
78
+
79
+ 没有可用模型(没配 key、没 `--model`、`config.defaultModel` 不可用)时 `ama --mode acp` 不再以退出码 4 结束,而是照常握手,等用户登录:
80
+
81
+ | 请求 | 无模型时的行为 |
82
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
83
+ | `initialize` | 照常回答;客户端声明 `clientCapabilities.auth.terminal` 时 `authMethods` 给两条 terminal 方法,否则为空 |
84
+ | 会话方法(`session/*`) | 先重试整段启动(距上次失败至少 1 s,并发请求共用一次):成功则同一条连接交给正常的服务端处理本次与之后的请求(不必重新 `initialize`);仍无模型 → `-32000`,`message` 是无模型引导(列出 key 环境变量与 `ama auth set`),`data.authMethods` 是已给方法的 id |
85
+ | `authenticate` | `-32602`:terminal 方法按规范不经 `authenticate` |
86
+ | 其它 | `-32601`;交接前的通知忽略 |
87
+
88
+ 两条 terminal 方法。按规范,客户端把 `args` **追加**到配置好的启动命令后面、在终端里起子进程(例如 `ama --mode acp --acp-terminal-auth api-key`);ama 见到 `--acp-terminal-auth` 就忽略其余启动参数(`--mode acp`、`--model` 等),改跑对应的 `auth` 子命令,同一条命令里的 `--auth-file`、`--lang` 照用:
89
+
90
+ | id | `args` | 等同于 | 作用 |
91
+ | --------- | ----------------------------- | ------------------------ | -------------------------------------------------------------------- |
92
+ | `chatgpt` | `--acp-terminal-auth chatgpt` | `ama auth login chatgpt` | ChatGPT 订阅登录(浏览器 OAuth) |
93
+ | `api-key` | `--acp-terminal-auth api-key` | `ama auth set` | 方向键选内置的需 key 供应商,再粘贴 key(不回显,写 auth.json 0600) |
94
+
95
+ 启动时给了 `--auth-file`(或 profile 的 `authFile`)时两条方法都追加 `--auth-file <绝对路径>`,登录写到 ama 读的同一个文件。登录完成后客户端再开会话即可,不用重启 ama。stdin 关闭时退出 0(已交接则同下节)。失败的重试停在模型解析这一步,不加载宿主、不跑 SessionStart Hook。
96
+
97
+ Zed 的配置(`settings.json`):
98
+
99
+ ```json
100
+ {
101
+ "agent_servers": {
102
+ "ama": {
103
+ "type": "custom",
104
+ "command": "ama",
105
+ "args": ["--mode", "acp"],
106
+ "env": {}
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ 没配模型时 Zed 打开 ama 的线程会提示登录,点登录方式后 Zed 在它的终端里跑上表的登录流程,成功退出后自动重试开会话;也可以先在任意终端 `ama auth set` / `ama auth login chatgpt`,或在 `env` 里给 key 环境变量。
113
+
114
+ ### 配置项与命令
50
115
 
51
- 不声明、也不使用客户端的 `fs` / `terminal` 能力:ama 自己读写、自己跑命令,按自己的权限管线。
116
+ 开会话(new / load / resume)的答复带 `configOptions`,之后发一条 `available_commands_update`:
117
+
118
+ | 配置项 id | category | 可选值 |
119
+ | ---------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `mode` | `mode` | ama 的权限模式(`plan`、`allowlist`、`default`、`auto-edit`、`auto`、`full-auto`),与 `modes` 同一状态 |
121
+ | `model` | `model` | 按供应商分组,值 `provider/model-id`(多渠道的渠道行带 `@渠道`);口径与 TUI `/model` 的「已配置」视图相同:只列有 key、OAuth 已登录或本地的供应商,设了 `models.enabled` 只列清单内的,测试供应商 `fake` 缺省藏起;当前模型总在列。没配 key 的供应商不列,先 `ama auth set` |
122
+ | `thinking` | `thought_level` | 当前模型支持的思考级别(`off`…`xhigh`,非推理模型只有 `off`) |
123
+
124
+ - `mode` 与 `modes` / `session/set_mode` 是同一状态:规范要求客户端有 `configOptions` 时用它代替 `modes`(Zed 给了就不再看 `modes`),所以模式也放进配置项;`modes` 照给,留给只认 `modes` 的客户端。设 `mode` 与 `session/set_mode` 一样按会话记,变化时 `current_mode_update` 与 `config_option_update` 都发。没有 boolean 型配置项。
125
+ - `session/set_config_option`:`mode` → 改权限模式,`model` → 切模型,`thinking` → 改思考级别,答复是全部配置项的新状态;未知 id、找不到的模型、不认识的级别回 invalid params(-32602)。模型或级别在会话里变了(含 plan 流程自动切换)时发 `config_option_update`。
126
+ - 命令表:Skill 列为 `skill:<名字>`,提示模板列为 `<名字>`(frontmatter 的 `argument-hint` 作 `input.hint`)。这两类在 prompt 文本里本来就会展开(`/skill:<名字> …`、`/<名字> …`)。`/new`、`/compact` 等内置斜杠命令在 ACP 下不执行,不列。
52
127
 
53
128
  ### 退出
54
129
 
55
- stdin 关闭后等已开始的运行结束再退出(0);SIGINT / SIGTERM 中断后退出 130 / 143。宿主看到的模式是 `rpc`(`HostApi.mode`)。SDK 直接 `bootstrap(--mode acp)` 时得到 `mode: "rpc"` 的 Runtime,再交给 `runAcpMode`。
130
+ stdin 关闭后等已开始的运行结束再退出(0);`ama` 进程的 stdout 从启动第一步起只给协议(宿主 / Hook 加载期的 `console.log` 改写到 stderr);SIGINT / SIGTERM 中断后退出 130 / 143。宿主看到的模式是 `rpc`(`HostApi.mode`)。SDK 直接 `bootstrap(--mode acp)` 时得到 `mode: "rpc"` 的 Runtime,再交给 `runAcpMode`。
131
+
132
+ ## 偏离与不做
133
+
134
+ 以下是有意的取舍,不是遗漏:
135
+
136
+ | 项目 | ama 的做法与理由 |
137
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
+ | 客户端的 `fs/*`、`terminal/*` | 不使用(客户端声明了也不用)。ama 自己读写文件、自己跑命令,全部经自己的权限管线、沙箱与检查点;借客户端的文件系统或终端会绕开这些,也会让 TUI / RPC / ACP 三处行为不一致。 |
139
+ | MCP(`mcpServers`,含规范要求必须支持的 stdio) | 不连接,`mcpCapabilities` 的 `http` / `sse` 为 false;收到非空 `mcpServers` 时 stderr 记一行后照常开会话。**这偏离了规范「Agent 必须支持 stdio MCP」的 MUST**:ama 的工具只来自自身与宿主(profile),扩展走 Skill;接 MCP 会把外部工具描述放进请求前缀,破坏逐字节稳定的提示词缓存,也绕开权限管线对工具的分类。 |
140
+ | elicitation(Agent 向人要结构化输入) | 作服务端时不发 `elicitation/create`:ama 需要人决定的只有审批,走 `session/request_permission`。作客户端时支持(见下文)。 |
141
+ | `session/delete` | 不实现(-32601)。会话文件的清理走 `ama sessions prune`。 |
142
+ | `logout` | 不实现、不声明 `agentCapabilities.auth.logout`(-32601)。退出登录用 `ama auth logout chatgpt` / `ama auth remove <供应商>`。 |
143
+ | 会话 `cwd` | 固定为 ama 的启动目录,`session/new` 给了别的目录回 -32602,`additionalDirectories` 忽略:信任、项目配置、会话目录与沙箱都按启动目录判定,同一进程里换目录会让它们失效。要换目录就在那个目录另起一个 `ama --mode acp`。 |
56
144
 
57
145
  ## 作为客户端
58
146
 
@@ -60,7 +148,10 @@ stdin 关闭后等已开始的运行结束再退出(0);SIGINT / SIGTERM
60
148
 
61
149
  `AcpClient`(`@armadra/agent/acp`):`initialize`、`newSession`、`resumeSession`(优先,不回放)、`loadSession`、`listSessions`、`closeSession`、`prompt`、`setMode`、`cancel`。
62
150
 
63
- - 声明的客户端能力为空:Agent 发来的 `fs/*`、`terminal/*` 请求回 method not found。
151
+ - 声明的客户端能力:`session.configOptions: {}`(接 select 型配置项,不声明 boolean);不声明 `fs` / `terminal`(Agent 发来的
152
+ `fs/*`、`terminal/*` 请求回 method not found),也不声明 `auth.terminal`(ama 没有可借给 Agent 的交互终端)。
153
+ - 线路两侧开 `$/cancel_request`:本端请求的 `signal` 在发出后 abort 会通知 Agent 撤回;Agent 撤回挂起的
154
+ `session/request_permission` / `elicitation/create` 时处理器的 `signal` abort,答 `cancelled` / `cancel`(处理器之后给的选择不作数)。
64
155
  - `session/request_permission` 交给 `onPermission`;没有处理器时回首个 `reject_once`(无人值守)。
65
156
  - `cancel(sessionId)` 发 `session/cancel`,并让该会话挂起的权限请求回 `cancelled`(规范要求)。
66
157
  - 只接受 Agent 自己给出的 `optionId`。
@@ -75,14 +166,37 @@ stdin 关闭后等已开始的运行结束再退出(0);SIGINT / SIGTERM
75
166
  `session/set_config_option`,答复是全部配置项的新状态。
76
167
  - `AcpClient.features`:`{ mcpServers, elicitation, configOptions }`,宿主据此做特性检测。
77
168
 
78
- `AcpDriver` 在客户端之上实现驱动契约(`AgentDriver`):续接优先 `session/resume`,其次 `session/load`(回放的历史丢弃),都不支持就新开并提示;按 ama 模式 `session/set_mode`,只读模式找不到对应模式 id 时拒绝启动。
169
+ `AcpDriver` 在客户端之上实现驱动契约(`AgentDriver`):
170
+
171
+ - 续接优先 `session/resume`,其次 `session/load`(回放的历史丢弃),都不支持就新开并提示。
172
+ - 按 ama 模式 `session/set_mode`;Agent 不给 `modes` 时退到 `configOptions` 里 category `mode` 的选择项,同一映射找值后经
173
+ `session/set_config_option` 设置。两处都找不到对应模式时:只读模式拒绝启动,其它模式用 Agent 的缺省模式并提示。
174
+ - 开会话回 -32000(需要登录)时报 `agent_auth_required`,文案列出 `initialize` 给的认证方法;terminal 型附上要在终端里跑的
175
+ 命令(Agent 程序 + 它的参数 + 方法的 `args`)。ama 不替人登录。
176
+ - 取消回合后 Agent 以 -32800(请求被撤回)答 `session/prompt` 时视为 `cancelled`;没取消时照常报错。
177
+ - 工具内容里 `diff` 的 `path` 并入该调用的 `locations`,调用完成后计入 `filesTouched`(不论 Agent 报的工具种类)。
79
178
 
80
179
  ## 测试替身
81
180
 
82
- `runFakeAcpAgent(input, output)` 是进程内的假 ACP Agent,`fakeAcpAgentPath()` 是它的可执行入口(`node <path> [--minimal]`)。行为由提示里的标记决定:`[permission]`(请求权限,四个选项)、`[slow]`(等到 cancel)、`[plan]`、`[think]`、`[refuse]`,其余回 `echo: <文本>`。`--minimal` 不声明 resume / load / list / close,也不给模式,用来测降级路径。另有 `[elicit]`(发 `elicitation/create`,客户端没声明能力时回 `elicit: unsupported`)、`[model]`、`[env NAME]`(只回值的 sha256)三个标记;`--config-options`(进程内 `{ configOptions: true }`)让开会话答一个 `model` 配置项并接 `session/set_config_option`。
181
+ `runFakeAcpAgent(input, output, options?)` 是进程内的假 ACP Agent,`fakeAcpAgentPath()` 是它的可执行入口(`node <path> [参数]`)。行为由提示里的标记与启动参数决定,其余提示回 `echo: <文本>`:
182
+
183
+ | 标记 / 参数 | 行为 |
184
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
185
+ | `[permission]` | 请求权限(四个选项) |
186
+ | `[slow]` | 等到 `session/cancel` |
187
+ | `[plan]` / `[think]` / `[refuse]` | 先发 `plan`(两条)/ 先发 `agent_thought_chunk` / 以 `refusal` 结束 |
188
+ | `[elicit]` | 发 `elicitation/create`;客户端没声明能力时回 `elicit: unsupported` |
189
+ | `[model]` / `[env NAME]` | 回 `model <当前模型>` / `env NAME <值的 sha256 或 absent>`(不回显值) |
190
+ | `--minimal`(`{ minimal: true }`) | 不声明 resume / load / list / close,也不给模式,用来测降级路径 |
191
+ | `--config-options`(`{ configOptions: true }`) | 开会话答一个 `model` 配置项(选项按组给出),接 `session/set_config_option` |
192
+ | `--config-only`(`{ configOnly: true }`) | 开会话不给 `modes`,改在 `configOptions` 里给 category `mode` 的选择项(id `mode`),经 `session/set_config_option` 切换;可与 `--config-options` 同开 |
193
+ | `--auth-required`(`{ authRequired: true }`) | `initialize` 给一条 terminal 型认证方法(id `login`),`session/new` / `load` / `resume` 一律回 -32000 |
194
+ | `[cancel-request]`(`{ cancelRequestMs }` 缺省 2 s) | 发权限请求,挂起到期后 Agent 自己发 `$/cancel_request` 撤回,工具调用 failed,回 `permission withdrawn` 与 `end_turn` |
195
+
196
+ 假 Agent 的线路两侧都开着 `$/cancel_request`。仓库内的 `test/helpers/acp-schema.ts` 用官方 v1 schema(1.24.1,`test/fixtures/acp/schema-v1.24.1.json`)逐条校验线路:`assertAcpWire(wire)`。
83
197
 
84
198
  黄金记录在 `test/fixtures/acp/`:`driver-{allow,reject,cancel}.jsonl`(ama 驱动假 Agent 的三条路径)与 `mode-prompt.jsonl`(`ama --mode acp` 一轮往返)。`UPDATE_GOLDEN=1` 重写。
85
199
 
86
200
  ## 兼容性
87
201
 
88
- 按 ACP v1(含 2026 年稳定的 `session/list`、`session/resume`、`session/close`、`usage_update`)。v2 计划取消 `session/load`,ama 作客户端时已优先 `resume`。
202
+ 对照官方 ACP v1 schema **1.24.1**(稳定部分,含 `session/list`、`session/resume`、`session/close`、`$/cancel_request`、`usage_update`、`session_info_update`、`config_option_update` 与 terminal 型认证方法)实现;schema 原文随仓库在 `test/fixtures/acp/schema-v1.24.1.json`,测试与黄金记录的每条线路都按它校验。唯一用到的不稳定字段是 `session/prompt` 结果的 `usage`(见「事件映射」)。v2 计划取消 `session/load`,ama 作客户端时已优先 `resume`。
package/docs/agents.md CHANGED
@@ -230,6 +230,8 @@ ama 能以各 CLI 自己的账户、模型与权限策略驱动外部编码 Agen
230
230
  | `auto` | `auto` | `on-request` / `workspace-write` | 同上 |
231
231
  | `full-auto` | `auto`(从不给 `bypassPermissions`) | `never` / `workspace-write`(从不给 `danger-full-access`) | 同上 |
232
232
 
233
+ ACP Agent 不给 `modes`、改用 category `mode` 的配置项表达模式时,按上表同一映射在配置项的可选值里找,经 `session/set_config_option` 设置(见 [acp.md](acp.md)「作为客户端」)。
234
+
233
235
  一次性打印模式不能审批,只在只读任务下用。
234
236
 
235
237
  ### 环境与账户
package/docs/codemode.md CHANGED
@@ -87,7 +87,7 @@
87
87
 
88
88
  - `codemode` 本身作为一次工具调用经过 PreToolUse、权限与 PostToolUse。
89
89
  - 脚本里的每次 `tools.*` 再各自经过完整流程,Hook 按**真实工具名**匹配(`bash`,不是 `codemode`);Hook 输入多两个字段:`viaCodemode: true` 与 `parentToolCallId`(外层 `codemode` 调用的 id)。
90
- - 事件:`tool_execution_update` 透传脚本输出(最近 4000 字符);内层调用发 `tool_execution_start / end`,带 `parentToolCallId`,不进转录、不进模型上下文。
90
+ - 事件:`tool_execution_update` 透传脚本输出(最近 4000 字符);内层调用发 `tool_execution_start / end`,带 `parentToolCallId`,不进转录、不进模型上下文。`ama --mode acp` 把每次内层调用单列为一条 `tool_call`(标题前缀 `codemode › `,`_meta.ama.parentToolCallId` 指向外层),其权限请求关联到这条内层调用(见 [acp.md](acp.md))。
91
91
 
92
92
  ## 沙箱
93
93
 
package/docs/en/acp.md ADDED
@@ -0,0 +1,192 @@
1
+ # ACP (Agent Client Protocol)
2
+
3
+ English · [简体中文](../acp.md)
4
+
5
+ > Translated from the Chinese [docs/acp.md](../acp.md) as of commit `bd37706`. When the two differ, the Chinese version is
6
+ > authoritative.
7
+
8
+ ama works on both sides of ACP:
9
+
10
+ - **Agent**: `ama --mode acp` exposes ama as an ACP agent for Zed, JetBrains and Armadra's ACP nodes;
11
+ - **Client**: ama drives external agents over ACP (native ACP agents such as Gemini CLI, OpenCode, Kimi and Copilot, or Claude Code / Codex with an adapter installed); see [agents.md](../agents.md) (Chinese).
12
+
13
+ The protocol stack is hand-written with no dependencies and maintained in one place: `@armadra/agent/acp` exports the types, framing, client and fake agent, and Armadra reuses them directly. The design rationale is in [wave5-plan.md](../wave5-plan.md) §5 (D14, Chinese).
14
+
15
+ ## Wire
16
+
17
+ JSON-RPC 2.0 over NDJSON (stdio): lines are split on `\n` only, large lines are written in 64 KiB chunks with backpressure, the same as the "Wire" section of [rpc.md](rpc.md). stdout carries protocol lines only; diagnostics go to stderr. The client starts with `initialize`; there is no `hello`.
18
+
19
+ ## `ama --mode acp`
20
+
21
+ ```sh
22
+ ama --mode acp # excludes -p; other flags (--model, --profile, --trust, …) work as usual
23
+ ```
24
+
25
+ | Method | What ama does |
26
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
27
+ | `initialize` | `protocolVersion: 1`; `loadSession: true`, `sessionCapabilities: { list, resume, close }`, `promptCapabilities: { image: true, embeddedContext: true }`; with a model, `authMethods` is empty (without one, see "Without a model" below) |
28
+ | `authenticate` | -32602: ama only offers terminal auth methods, which the spec says are not passed to `authenticate` (the same with or without a model) |
29
+ | `session/new` | Opens a session (the first one claims the empty session created at start-up); also allowed while a turn runs; `cwd` must be ama's start directory (compared by realpath), otherwise invalid params |
30
+ | `session/load` | Opens the session and replays its history as `session/update` (user messages, replies, thinking, tool calls); a session that is already open is replayed from memory |
31
+ | `session/resume` | Opens the session without replay. When no session file matches the id of a load / resume: a UUID gets a new empty session with that id (empty sessions are not written to disk, and after ama restarts Zed's Reload Agent and the like come back with them); anything else answers -32002 |
32
+ | `session/list` | Sessions of the start directory: a different `cwd` gives an empty list; 50 per page (newest `updatedAt` first) with `nextCursor`, an invalid `cursor` is invalid params; the title is the session name or the first prompt (first line after removing embedded resource blocks, ≤ 80 characters) |
33
+ | `session/close` | Interrupts the session's run (its queued prompts answer `cancelled`), releases it and removes it from this connection; later requests for that id answer -32002 — open it again with `session/load` / `session/resume` first |
34
+ | `session/prompt` | Text and images are accepted; `resource_link` is passed as `@uri` text, embedded resources contribute their text. Queued while another session runs; an id that is not open answers -32002. End of turn: interrupted → `cancelled`, output truncated → `max_tokens`, refusal → `refusal`, failure → JSON-RPC error |
35
+ | `session/cancel` (notif.) | Running → interrupted; queued → answers `cancelled` at once |
36
+ | `$/cancel_request` (notif.) | Withdraws a pending `session/prompt`: same as `session/cancel`, and that request answers -32800. For the other direction see "Approvals" |
37
+ | `session/set_mode` | Mode ids are ama's permission modes (`plan`, `allowlist`, `default`, `auto-edit`, `auto`, `full-auto`); kept per session, see "Multiple sessions" below |
38
+ | `session/set_config_option` | Changes a session config option; the answer is the new state of all options; see "Config options and commands" below |
39
+
40
+ `mcpServers` and `additionalDirectories` of `session/new` / `load` / `resume` have no effect: when non-empty, one line goes to stderr and the session opens as usual (reasons under "Deviations and non-goals"). Methods ama does not implement (`session/delete`, `logout`, …) answer -32601.
41
+
42
+ ### Multiple sessions
43
+
44
+ One `ama --mode acp` process can keep several sessions open (Zed's threads share one connection):
45
+
46
+ - Every open session stays in memory; an empty session that never received a message can be switched away from and back to (empty sessions are not written to disk).
47
+ - **One turn runs at a time**: while another session runs, `session/prompt` goes into a FIFO queue and starts when the previous turn ends; it no longer fails busy. `session/new`, `load`, `resume`, `list`, `set_mode`, `set_config_option` and `close` can be called at any time.
48
+ - When a turn starts, its session becomes the "foreground" session: the host (`HostApi.session.*`), the common fields of hooks and the session seen by tools all switch to it, and "allowed for this session" grants are cleared (switching back means allowing again, as with `/resume` in the TUI).
49
+ - Permission modes are kept per session: `set_mode` on the foreground session applies at once; on another session it is only recorded (a `current_mode_update` is still sent) and applied to the permission pipeline when that session's turn starts, with another `current_mode_update`. So changing the mode of a queued session does not affect the one that is running.
50
+ - After every turn a `session_info_update` is sent (`updatedAt`; `title` only when it changed).
51
+ - Closing a session releases only that session (running the SessionEnd hook); when stdin closes, queued prompts answer `cancelled`, the running turn is allowed to finish, then all sessions are released in turn.
52
+
53
+ ### Event mapping
54
+
55
+ | ama | `session/update` |
56
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | Text delta / thinking delta | `agent_message_chunk` / `agent_thought_chunk` |
58
+ | The model emits a tool call | `tool_call` (`pending`, with `name`, `rawInput`, `kind`, `locations`) |
59
+ | A call inside a codemode script starts | `tool_call` (`pending`, `title` prefixed `codemode › `, `_meta.ama.parentToolCallId` pointing at the outer `codemode` call), then `in_progress` |
60
+ | Tool starts | `tool_call_update` (`in_progress`) |
61
+ | Tool ends (inner calls included) | `tool_call_update` (`completed` / `failed`); `content` is `[diff?, text]`: edit / write carry a `diff` (`path`, `oldText`, `newText`; `oldText: null` for a new file), the text is limited to the first 4 KB; `locations[].line` is the first changed line; no `rawOutput` |
62
+ | `todo` update | `plan` |
63
+ | End of each turn | `usage_update` (context used, window size, session cost in USD) |
64
+ | Permission mode change | `current_mode_update` |
65
+ | Model / thinking level change | `config_option_update` (all options) |
66
+ | After a session opens (new / load / resume) | `available_commands_update` and `config_option_update` |
67
+ | After a turn | `session_info_update` (`updatedAt`; `title` only when it differs from the last one) |
68
+
69
+ The full before / after text of a diff only travels with live events and is never written to the session file: above 256 KiB on either side there is no diff, and tool results replayed by `session/load` carry only their first 4 KB of text. The `name` of each mode is its display name (such as `Manual` or `Accept edits`); the `description` follows the UI language. The `session/prompt` result carries the turn's token usage `usage` (`inputTokens`, `outputTokens`, `cachedReadTokens`, `cachedWriteTokens`, `totalTokens`); in schema 1.24.1 this field is still UNSTABLE (only in the unstable schema), so clients may ignore it and rely on `usage_update`.
70
+
71
+ `toolCallId` is unique within a session: when the upstream provider reuses an id in a later turn (fallback ids of a few compatible APIs, the fake test model), the wire id gets `#2`, `#3`, … and later status updates, approvals and replay follow that mapping (clients merge entries by id, so without this different calls would collapse into one).
72
+
73
+ ### Approvals
74
+
75
+ Calls that ama needs to ask about go to the client as `session/request_permission` with three options: `allow_once` (Allow), `allow_always` (Allow for this session) and `reject_once` (Deny). `toolCall.toolCallId` refers to the id of an earlier `tool_call` — including calls inside codemode, which point at the inner `tool_call`, not the outer `codemode` one. While the question is open the call goes back to `pending`, then `in_progress` once allowed (so an approved call goes `pending → in_progress → pending → in_progress → completed`); a denied call goes straight to `failed`. When the client answers `cancelled`, the connection drops or the turn is interrupted, it counts as unanswered (denied). When ama no longer needs an answer (the turn was interrupted by `session/cancel` / `$/cancel_request`, or the 10-minute approval timeout passed), it withdraws the pending `session/request_permission` with `$/cancel_request { requestId }` so the client can close its dialog. In auto mode ama's own classifier still works — this only applies to ama's own tools; requests from external agents that ama drives go to a human only (see [agents.md](../agents.md), Chinese).
76
+
77
+ ### Without a model
78
+
79
+ When no model is available (no key, no `--model`, `config.defaultModel` unusable), `ama --mode acp` no longer exits with code 4; it completes the handshake and waits for the user to sign in:
80
+
81
+ | Request | Behavior without a model |
82
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | `initialize` | Answered as usual; when the client declares `clientCapabilities.auth.terminal`, `authMethods` has the two terminal methods, otherwise it is empty |
84
+ | Session methods (`session/*`) | Start-up is retried first (at least 1 s after the last failure; concurrent requests share one retry): on success the same connection is handed to the normal server for this and later requests (no new `initialize`); still no model → `-32000` whose `message` is the no-model guidance (key environment variables and `ama auth set`) and whose `data.authMethods` are the ids of the offered methods |
85
+ | `authenticate` | `-32602`: per the spec, terminal methods are not passed to `authenticate` |
86
+ | Anything else | `-32601`; notifications before the handover are ignored |
87
+
88
+ The two terminal methods. Per the spec, the client **appends** `args` to the configured agent command and runs it in a terminal (for example `ama --mode acp --acp-terminal-auth api-key`); when ama sees `--acp-terminal-auth` it ignores the other start-up arguments (`--mode acp`, `--model`, …) and runs the matching `auth` subcommand, still honouring `--auth-file` and `--lang` from the same command:
89
+
90
+ | id | `args` | Same as | What it does |
91
+ | --------- | ----------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `chatgpt` | `--acp-terminal-auth chatgpt` | `ama auth login chatgpt` | ChatGPT subscription login (browser OAuth) |
93
+ | `api-key` | `--acp-terminal-auth api-key` | `ama auth set` | Pick a built-in provider that needs a key with the arrow keys, then paste the key (not echoed; written to auth.json with mode 0600) |
94
+
95
+ When `--auth-file` (or a profile's `authFile`) was given at start-up, both methods append `--auth-file <absolute path>`, so the login writes the file ama reads. After signing in, the client just opens a session again; ama does not need a restart. Closing stdin exits 0 (after a handover, as in "Exit" below). A failed retry stops at model resolution: no host is loaded and no SessionStart hook runs.
96
+
97
+ Zed configuration (`settings.json`):
98
+
99
+ ```json
100
+ {
101
+ "agent_servers": {
102
+ "ama": {
103
+ "type": "custom",
104
+ "command": "ama",
105
+ "args": ["--mode", "acp"],
106
+ "env": {}
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ Without a model, opening an ama thread in Zed asks you to sign in; pick a method and Zed runs the login flow above in its terminal, then retries opening the session once it exits successfully. You can also run `ama auth set` / `ama auth login chatgpt` in any terminal beforehand, or put a key environment variable in `env`.
113
+
114
+ ### Config options and commands
115
+
116
+ Session-open answers (new / load / resume) carry `configOptions`, followed by an `available_commands_update`:
117
+
118
+ | Option id | category | Values |
119
+ | ---------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `mode` | `mode` | ama's permission modes (`plan`, `allowlist`, `default`, `auto-edit`, `auto`, `full-auto`), the same state as `modes` |
121
+ | `model` | `model` | Grouped by provider, values `provider/model-id` (channel rows of multi-channel providers carry `@channel`); the same view as "configured" in the TUI `/model` picker: only providers with a key, an OAuth login or local ones, only the list in `models.enabled` when set, the test provider `fake` hidden by default; the current model is always listed. Providers without a key are not listed — run `ama auth set` first |
122
+ | `thinking` | `thought_level` | The thinking levels the current model supports (`off`…`xhigh`; non-reasoning models only have `off`) |
123
+
124
+ - `mode` is the same state as `modes` / `session/set_mode`: the spec says a client that gets `configOptions` should use them instead of `modes` (Zed then ignores `modes`), so the mode is a config option too; `modes` is still sent for clients that only know `modes`. Setting `mode` is per session like `session/set_mode`, and a change sends both `current_mode_update` and `config_option_update`. There are no boolean options.
125
+ - `session/set_config_option`: `model` switches the model, `thinking` changes the thinking level, and the answer is the new state of all options; unknown ids, unknown models and unknown levels answer invalid params (-32602). When the model or level changes during a session (including automatic switches in the plan flow), `config_option_update` is sent.
126
+ - Command list: skills are listed as `skill:<name>`, prompt templates as `<name>` (the frontmatter `argument-hint` becomes `input.hint`). Both already expand in prompt text (`/skill:<name> …`, `/<name> …`). Built-in slash commands such as `/new` and `/compact` do not run under ACP and are not listed.
127
+
128
+ ### Exit
129
+
130
+ After stdin closes, ama waits for the run in progress to finish, then exits (0); from the first start-up step the stdout of the `ama` process carries only the protocol (`console.log` while hosts / hooks load is redirected to stderr); after SIGINT / SIGTERM it exits 130 / 143. Hosts see the mode as `rpc` (`HostApi.mode`). When the SDK calls `bootstrap(--mode acp)` directly it gets a Runtime with `mode: "rpc"` and hands it to `runAcpMode`.
131
+
132
+ ## Deviations and non-goals
133
+
134
+ These are deliberate choices, not omissions:
135
+
136
+ | Item | What ama does and why |
137
+ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
+ | The client's `fs/*` and `terminal/*` | Not used (even when the client declares them). ama reads and writes files and runs commands itself, all through its own permission pipeline, sandbox and checkpoints; borrowing the client's file system or terminal would bypass them and make TUI / RPC / ACP behave differently. |
139
+ | MCP (`mcpServers`, including stdio, which the spec requires) | Not connected; `mcpCapabilities.http` / `sse` are false; a non-empty `mcpServers` gets one stderr line and the session opens as usual. **This deviates from the spec's MUST "agents must support stdio MCP"**: ama's tools come only from itself and the host (profile), and extensions are skills; MCP would put external tool descriptions into the request prefix, breaking the byte-stable prompt cache, and bypass how the permission pipeline classifies tools. |
140
+ | elicitation (the agent asking a human for structured input) | As an agent, ama never sends `elicitation/create`: the only thing it needs a human to decide is approvals, which go through `session/request_permission`. As a client it is supported (see below). |
141
+ | `session/delete` | Not implemented (-32601). Clean up session files with `ama sessions prune`. |
142
+ | `logout` | Not implemented, and `agentCapabilities.auth.logout` is not declared (-32601). Sign out with `ama auth logout chatgpt` / `ama auth remove <provider>`. |
143
+ | Session `cwd` | Fixed to ama's start directory: `session/new` with another directory answers -32602 and `additionalDirectories` is ignored. Trust, project config, the session directory and the sandbox are all decided by the start directory, and changing directories inside one process would invalidate them. To work in another directory, start another `ama --mode acp` there. |
144
+
145
+ ## As a client
146
+
147
+ The model uses ACP agents through `task(agent="acp:<program>")` (ama itself is `task(agent="acp:ama")`; see "Using them in task" in [agents.md](../agents.md), Chinese).
148
+
149
+ `AcpClient` (`@armadra/agent/acp`): `initialize`, `newSession`, `resumeSession` (preferred, no replay), `loadSession`, `listSessions`, `closeSession`, `prompt`, `setMode`, `cancel`.
150
+
151
+ - Declared client capabilities: `session.configOptions: {}` (select options; boolean is not declared); `fs` / `terminal` are not declared (`fs/*` and `terminal/*` requests from the agent answer method not found), and neither is `auth.terminal` (ama has no interactive terminal to lend to the agent).
152
+ - `$/cancel_request` is on in both directions: aborting the `signal` of a request after it was sent tells the agent to withdraw it; when the agent withdraws a pending `session/request_permission` / `elicitation/create`, the handler's `signal` aborts and the answer is `cancelled` / `cancel` (a choice the handler makes afterwards does not count).
153
+ - `session/request_permission` goes to `onPermission`; without a handler the first `reject_once` is answered (unattended).
154
+ - `cancel(sessionId)` sends `session/cancel` and answers `cancelled` to that session's pending permission requests (required by the spec).
155
+ - Only `optionId`s offered by the agent itself are accepted.
156
+ - `mcpServers` of session-open calls (`newSession` / `resumeSession` / `loadSession`) defaults to an empty array; a host can pass a third argument `{ mcpServers }` (for stdio, `{ name, command, args, env: [{ name, value }] }`), which is forwarded to the agent as is. `AcpClient.features.mcpServers === true` signals support (older versions have no `features`). ama itself, as a client, still passes none.
157
+ - `elicitation/create` (the agent asking a human for structured input): only when the constructor is given `onElicitation(params, signal)` does `initialize` declare `clientCapabilities.elicitation` and accept this request; otherwise it is not declared and the request answers method not found (as in older versions). Answers are normalized to `{ action: "accept" | "decline" | "cancel", content? }` (`content` only with accept; unknown actions count as cancel); pending ones answer `{ action: "cancel" }` on `cancel(sessionId)` and when the connection closes. ama itself, as a client, gives no handler and never fills in forms for a human.
158
+ - Session config options: the `configOptions` of session-open answers (new / load / resume) are passed through; `setConfigOption(sessionId, configId, value)` sends `session/set_config_option`, and the answer is the new state of all options.
159
+ - `AcpClient.features`: `{ mcpServers, elicitation, configOptions }`, for feature detection by hosts.
160
+
161
+ `AcpDriver` implements the driver contract (`AgentDriver`) on top of the client:
162
+
163
+ - Continuing prefers `session/resume`, then `session/load` (the replayed history is discarded); if neither is supported a new session is opened with a notice.
164
+ - `session/set_mode` follows ama's mode; when the agent gives no `modes`, it falls back to a select option of category `mode` in `configOptions`, finds the value with the same mapping and sets it with `session/set_config_option`. When neither has a matching mode: read-only modes refuse to start, other modes use the agent's default mode with a notice.
165
+ - When opening a session answers -32000 (sign-in required), it reports `agent_auth_required`, listing the auth methods from `initialize`; terminal ones include the command to run in a terminal (the agent program + its arguments + the method's `args`). ama does not sign in for you.
166
+ - When the agent answers `session/prompt` with -32800 (request withdrawn) after a cancel, the turn counts as `cancelled`; without a cancel it is an error as usual.
167
+ - The `path` of `diff` tool content joins the call's `locations` and counts in `filesTouched` once the call completes (whatever tool kind the agent reports).
168
+
169
+ ## Test double
170
+
171
+ `runFakeAcpAgent(input, output, options?)` is an in-process fake ACP agent and `fakeAcpAgentPath()` its executable entry (`node <path> [flags]`). Its behavior follows markers in the prompt and start-up flags; any other prompt answers `echo: <text>`:
172
+
173
+ | Marker / flag | Behavior |
174
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
175
+ | `[permission]` | Requests permission (four options) |
176
+ | `[slow]` | Waits until `session/cancel` |
177
+ | `[plan]` / `[think]` / `[refuse]` | Sends a `plan` (two entries) first / sends `agent_thought_chunk` first / ends with `refusal` |
178
+ | `[elicit]` | Sends `elicitation/create`; answers `elicit: unsupported` when the client did not declare the capability |
179
+ | `[model]` / `[env NAME]` | Answers `model <current model>` / `env NAME <sha256 of the value, or absent>` (the value is never echoed) |
180
+ | `--minimal` (`{ minimal: true }`) | Declares no resume / load / list / close and gives no modes, for testing fallbacks |
181
+ | `--config-options` (`{ configOptions: true }`) | Session-open answers carry a `model` config option (grouped options) and `session/set_config_option` is accepted |
182
+ | `--config-only` (`{ configOnly: true }`) | Session-open answers give no `modes` but a select option of category `mode` (id `mode`) in `configOptions`, switched with `session/set_config_option`; can be combined with `--config-options` |
183
+ | `--auth-required` (`{ authRequired: true }`) | `initialize` offers one terminal auth method (id `login`); `session/new` / `load` / `resume` always answer -32000 |
184
+ | `[cancel-request]` (`{ cancelRequestMs }`, default 2 s) | Requests permission, and once the wait expires the agent withdraws it with `$/cancel_request`; the tool call fails and the turn answers `permission withdrawn` and `end_turn` |
185
+
186
+ Both sides of the fake agent's wire have `$/cancel_request` on. In the repository, `test/helpers/acp-schema.ts` validates every wire line against the official v1 schema (1.24.1, `test/fixtures/acp/schema-v1.24.1.json`): `assertAcpWire(wire)`.
187
+
188
+ The golden recordings are in `test/fixtures/acp/`: `driver-{allow,reject,cancel}.jsonl` (three paths of ama driving the fake agent) and `mode-prompt.jsonl` (one round trip of `ama --mode acp`). `UPDATE_GOLDEN=1` rewrites them.
189
+
190
+ ## Compatibility
191
+
192
+ Implemented against the official ACP v1 schema **1.24.1** (the stable part, including `session/list`, `session/resume`, `session/close`, `$/cancel_request`, `usage_update`, `session_info_update`, `config_option_update` and terminal auth methods); the schema ships with the repository as `test/fixtures/acp/schema-v1.24.1.json`, and every wire line in the tests and golden recordings is validated against it. The only unstable field in use is `usage` in the `session/prompt` result (see "Event mapping"). v2 plans to drop `session/load`; as a client ama already prefers `resume`.
@@ -178,7 +178,7 @@ Rolling back code (`/rewind`, design in [rewind-plan.md](../rewind-plan.md), Chi
178
178
  - **Snapshots**: at the start of a new turn, `git add -A` + `write-tree` + `commit-tree` (parent: the previous shadow commit), with the commit id recorded as the checkpoint's `shadowCommit`. The shadow repository uses a fixed identity and empty global / system config (your signing, hooks, filters and templates are not read), `core.autocrlf=false` with line-ending conversion off, so raw disk bytes are stored; `gc.auto=0`.
179
179
  - **Ignores**: `.gitignore` in the working directory applies; when the working directory is inside a git repository, paths your repository ignores (parent `.gitignore` files, `info/exclude`, the global ignore file) stay out of the shadow repository too; `.git` is always excluded.
180
180
  - **Restore**: the current working directory is written as a tree and compared with the target commit, so only differing files are processed; conflict and safety checks are the same as in `tools` (symbolic links, hard links, non-regular files and directories on the path replaced by links are skipped). The "known" current content = what is in the latest shadow snapshot, what ama last wrote or what the latest checkpoint recorded; anything else counts as a change outside the turn and is skipped by default. Ignored files are left alone; files changed by edit / write but not in the shadow repository (outside the working directory, ignored) restore from the `tools` records. When the target checkpoint has no shadow commit (after a downgrade, or the shadow repository was deleted) the whole restore follows `tools`.
181
- - **Guards**: in these cases the session downgrades to `tools` with a one-time notice: `git` is not on PATH; the working directory (ignored files excluded) has more than 20 000 files (checked before the first snapshot); a single snapshot takes longer than 3 seconds (that commit is kept). It is not enabled when the working directory is the home directory or the file-system root.
181
+ - **Guards**: in these cases the session downgrades to `tools` with a one-time notice: `git` is not on PATH; the working directory (ignored files excluded) has more than 20 000 files (checked before the first snapshot); a single snapshot takes longer than 3 seconds (that commit is kept; creating the shadow repository the first time does not count). It is not enabled when the working directory is the home directory or the file-system root.
182
182
  - **Limitations**:
183
183
  - bash changes in the latest turn enter a snapshot only when the next turn starts; rolling back before that, they cannot be told apart from manual changes and count as conflicts (overwriting is an option).
184
184
  - git only records the executable bit: restore only adjusts the executable bit and keeps other permission bits; symbolic links and submodules are not restored.
package/docs/sessions.md CHANGED
@@ -160,7 +160,7 @@ ama sessions trace <id|文件> [--html [文件]] [--json] [--output <文件>] [-
160
160
  - **快照**:新回合开始时 `git add -A` + `write-tree` + `commit-tree`(父为上一个影子提交),提交 id 记进检查点的 `shadowCommit`。影子仓库用固定身份、空的全局 / 系统配置(不读你的签名、钩子、过滤器与模板),`core.autocrlf=false` 且关掉换行转换,存的是磁盘原字节;`gc.auto=0`。
161
161
  - **忽略**:工作目录里的 `.gitignore` 生效;工作目录在 git 仓库里时,你的仓库判定为忽略的路径(上级目录的 `.gitignore`、`info/exclude`、全局忽略文件)同样不进影子仓库;`.git` 一律排除。
162
162
  - **恢复**:把当前工作目录写成树,与目标提交比较,只处理有差异的文件;冲突与安全检查与 `tools` 相同(符号链接、硬链接、非普通文件、路径上的目录被换成链接都跳过)。「已知」的当前内容 = 最近一个影子快照里的、ama 最后写入的或最近检查点记录的,其余视为回合外的改动,缺省跳过。被忽略的文件不碰;edit / write 改过、但不在影子仓库里的文件(工作目录外、被忽略)按 `tools` 记录恢复。目标检查点没有影子提交(降级之后、或影子仓库已删)时整次按 `tools` 恢复。
163
- - **护栏**:以下情况本会话降级为 `tools` 并提示一次——PATH 里找不到 `git`;工作目录(不含忽略的)超过 20 000 个文件(第一次快照前检查);单次快照超过 3 秒(这次的提交保留)。工作目录是家目录或文件系统根目录时不启用。
163
+ - **护栏**:以下情况本会话降级为 `tools` 并提示一次——PATH 里找不到 `git`;工作目录(不含忽略的)超过 20 000 个文件(第一次快照前检查);单次快照超过 3 秒(这次的提交保留;不含第一次建影子仓库的时间)。工作目录是家目录或文件系统根目录时不启用。
164
164
  - **限制**:
165
165
  - 最近一个回合里 bash 的改动要到下一个回合开始才进快照;在那之前回滚,这些改动与手动修改分不开,算冲突(可选择覆盖)。
166
166
  - git 只记可执行位:恢复时只调整可执行位,其它权限位保留;符号链接与子模块不恢复。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@armadra/agent",
3
- "version": "0.6.8",
3
+ "version": "0.7.1",
4
4
  "description": "A coding and coordination agent that runs standalone or embedded in Armadra",
5
5
  "type": "module",
6
6
  "keywords": [