@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
@@ -17,9 +17,15 @@
17
17
  * | | 没声明 `elicitation` 能力时回 `elicit: unsupported`;`cancel` 时本回合 `cancelled` |
18
18
  * | `[model]` | 回 `model <当前模型>`(`configOptions` 关着时是 `model none`) |
19
19
  * | `[env NAME]` | 回 `env NAME <值的 sha256 | absent>`(测环境变量到没到,不回显值) |
20
+ * | `[cancel-request]` | 同 `[permission]` 发权限请求,挂起 `cancelRequestMs`(缺省 2 s)后自己发 |
21
+ * | | `$/cancel_request` 撤回;`tool_call_update` failed,回 `permission withdrawn`、`end_turn` |
20
22
  *
21
- * `configOptions: true`(可执行入口 `--config-options`):开会话答一个 `model` 配置项(`small`,分组
22
- * `big` 里有 `large`),`session/set_config_option` 可改;缺省不答,线路与之前相同。
23
+ * `configOptions: true`(可执行入口 `--config-options`):开会话答一个 `model` 配置项(分组 `fast` 里有
24
+ * `small`、`big` 里有 `large`,当前 `small`),`session/set_config_option` 可改;缺省不答,线路与之前相同。
25
+ * `configOnly: true`(`--config-only`):开会话不答 `modes`,改在 `configOptions` 里给 category `mode` 的
26
+ * 选择项(id `mode`),经 `session/set_config_option` 切换;可与 `configOptions` 同开(两项都给)。
27
+ * `authRequired: true`(`--auth-required`):`initialize` 给一条 terminal 型认证方法,开会话
28
+ * (new / load / resume)一律回 -32000。
23
29
  *
24
30
  * 会话:`session/new` 发 `fake-<n>`;`session/resume` / `session/load` 接受任何 `fake-` 开头的 id
25
31
  * (load 先回放一条用户消息与一条回复);`session/list` 列本进程建过的会话。
@@ -27,15 +33,25 @@
27
33
  import { createHash } from "node:crypto";
28
34
  import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS } from "../types.js";
29
35
  import { JsonRpcPeer, RpcError } from "../../jsonrpc.js";
36
+ /** `authRequired` 时 `initialize` 给的认证方法。 */
37
+ export const FAKE_AUTH_METHOD = {
38
+ type: "terminal",
39
+ id: "login",
40
+ name: "Log in",
41
+ description: "Run the fake agent's login command",
42
+ args: ["--login"],
43
+ };
30
44
  const MODES = [
31
45
  { id: "default", name: "Default" },
32
46
  { id: "plan", name: "Plan" },
33
47
  ];
48
+ /** 规范要求选项全部平铺或全部分组(不能混排),所以 `small` 也放进一组。 */
34
49
  const MODELS = [
35
- { value: "small", name: "Small" },
50
+ { group: "fast", name: "Fast", options: [{ value: "small", name: "Small" }] },
36
51
  { group: "big", name: "Big", options: [{ value: "large", name: "Large", description: "slow" }] },
37
52
  ];
38
53
  const MODEL_VALUES = ["small", "large"];
54
+ const MODE_VALUES = MODES.map((m) => m.id);
39
55
  /** 一个表单:必填的颜色(枚举)与可选的数量。 */
40
56
  const ELICIT_SCHEMA = {
41
57
  type: "object",
@@ -62,17 +78,40 @@ export function runFakeAcpAgent(input, output, options = {}) {
62
78
  /** 客户端在 initialize 里声明了 elicitation。 */
63
79
  let canElicit = false;
64
80
  const configOf = (s) => [
65
- {
66
- id: "model",
67
- name: "Model",
68
- category: "model",
69
- type: "select",
70
- currentValue: s.model,
71
- options: MODELS,
72
- },
81
+ ...(options.configOnly === true
82
+ ? [
83
+ {
84
+ id: "mode",
85
+ name: "Mode",
86
+ category: "mode",
87
+ type: "select",
88
+ currentValue: s.mode,
89
+ options: MODES.map((m) => ({ value: m.id, name: m.name })),
90
+ },
91
+ ]
92
+ : []),
93
+ ...(options.configOptions === true
94
+ ? [
95
+ {
96
+ id: "model",
97
+ name: "Model",
98
+ category: "model",
99
+ type: "select",
100
+ currentValue: s.model,
101
+ options: MODELS,
102
+ },
103
+ ]
104
+ : []),
73
105
  ];
106
+ const hasConfig = options.configOptions === true || options.configOnly === true;
74
107
  /** 开会话答复里的 `configOptions`(关着时不加这个键)。 */
75
- const config = (s) => options.configOptions === true ? { configOptions: configOf(s) } : {};
108
+ const config = (s) => hasConfig ? { configOptions: configOf(s) } : {};
109
+ /** 开会话答复里的 `modes`(`configOnly` 时不给)。 */
110
+ const modes = (s) => options.configOnly === true ? {} : { modes: { currentModeId: s.mode, availableModes: MODES } };
111
+ const requireAuth = () => {
112
+ if (options.authRequired === true)
113
+ throw new RpcError(RPC_ERRORS.authRequired, "authentication required");
114
+ };
76
115
  const update = (sessionId, value) => peer.notify(ACP_METHODS.sessionUpdate, { sessionId, update: value });
77
116
  const session = (id) => {
78
117
  const found = typeof id === "string" ? sessions.get(id) : undefined;
@@ -146,6 +185,42 @@ export function runFakeAcpAgent(input, output, options = {}) {
146
185
  await waitAbort(turn.signal);
147
186
  return done("cancelled");
148
187
  }
188
+ if (text.includes("[cancel-request]")) {
189
+ const toolCallId = `call-${s.turns}`;
190
+ await update(s.id, {
191
+ sessionUpdate: "tool_call",
192
+ toolCallId,
193
+ title: "Write note.txt",
194
+ kind: "edit",
195
+ status: "pending",
196
+ });
197
+ const withdraw = new AbortController();
198
+ const timer = setTimeout(() => withdraw.abort(), options.cancelRequestMs ?? 2000);
199
+ const asked = peer
200
+ .request(ACP_METHODS.requestPermission, {
201
+ sessionId: s.id,
202
+ toolCall: { toolCallId, title: "Write note.txt", kind: "edit", status: "pending" },
203
+ options: [
204
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
205
+ { optionId: "reject", name: "Reject", kind: "reject_once" },
206
+ ],
207
+ }, withdraw.signal)
208
+ .then((answer) => answer, () => undefined);
209
+ const answer = await Promise.race([asked, waitAbort(turn.signal).then(() => undefined)]);
210
+ clearTimeout(timer);
211
+ if (turn.signal.aborted) {
212
+ withdraw.abort();
213
+ return done("cancelled");
214
+ }
215
+ if (answer === undefined) {
216
+ await update(s.id, { sessionUpdate: "tool_call_update", toolCallId, status: "failed" });
217
+ await say("permission withdrawn");
218
+ return done("end_turn");
219
+ }
220
+ await update(s.id, { sessionUpdate: "tool_call_update", toolCallId, status: "completed" });
221
+ await say(`answered ${answer.outcome.outcome}`);
222
+ return done("end_turn");
223
+ }
149
224
  if (text.includes("[permission]")) {
150
225
  const toolCallId = `call-${s.turns}`;
151
226
  await update(s.id, {
@@ -205,6 +280,8 @@ export function runFakeAcpAgent(input, output, options = {}) {
205
280
  const peer = new JsonRpcPeer({
206
281
  input,
207
282
  output,
283
+ // 只有 [cancel-request] 会 abort 出站请求;入站撤回目前不影响假 Agent 的处理器
284
+ cancelRequests: true,
208
285
  async onRequest(method, raw) {
209
286
  const params = (raw ?? {});
210
287
  switch (method) {
@@ -220,23 +297,21 @@ export function runFakeAcpAgent(input, output, options = {}) {
220
297
  promptCapabilities: { image: false, embeddedContext: false },
221
298
  sessionCapabilities: { list: {}, resume: {}, close: {} },
222
299
  },
223
- authMethods: [],
300
+ authMethods: options.authRequired === true ? [FAKE_AUTH_METHOD] : [],
224
301
  agentInfo: { name: options.name ?? "fake-acp-agent", version: "1.0.0" },
225
302
  };
226
303
  }
227
304
  case ACP_METHODS.sessionNew: {
305
+ requireAuth();
228
306
  counter += 1;
229
307
  const s = open(`fake-${counter}`, String(params["cwd"] ?? ""));
230
308
  if (options.minimal)
231
309
  return { sessionId: s.id, ...config(s) };
232
- return {
233
- sessionId: s.id,
234
- modes: { currentModeId: s.mode, availableModes: MODES },
235
- ...config(s),
236
- };
310
+ return { sessionId: s.id, ...modes(s), ...config(s) };
237
311
  }
238
312
  case ACP_METHODS.sessionResume:
239
313
  case ACP_METHODS.sessionLoad: {
314
+ requireAuth();
240
315
  const id = String(params["sessionId"] ?? "");
241
316
  if (options.minimal || !id.startsWith("fake-"))
242
317
  throw new RpcError(RPC_ERRORS.resourceNotFound, `unknown session: ${id}`);
@@ -251,7 +326,7 @@ export function runFakeAcpAgent(input, output, options = {}) {
251
326
  content: { type: "text", text: "echo: earlier" },
252
327
  });
253
328
  }
254
- return { modes: { currentModeId: s.mode, availableModes: MODES }, ...config(s) };
329
+ return { ...modes(s), ...config(s) };
255
330
  }
256
331
  case ACP_METHODS.sessionList:
257
332
  return {
@@ -268,15 +343,23 @@ export function runFakeAcpAgent(input, output, options = {}) {
268
343
  return {};
269
344
  }
270
345
  case ACP_METHODS.sessionSetConfigOption: {
271
- if (options.configOptions !== true)
346
+ if (!hasConfig)
272
347
  throw new RpcError(RPC_ERRORS.methodNotFound, `method not found: ${method}`);
273
348
  const s = session(params["sessionId"]);
274
- if (params["configId"] !== "model")
275
- throw new RpcError(RPC_ERRORS.invalidParams, `unknown config: ${String(params["configId"])}`);
276
349
  const value = String(params["value"]);
277
- if (!MODEL_VALUES.includes(value))
278
- throw new RpcError(RPC_ERRORS.invalidParams, `unknown model: ${value}`);
279
- s.model = value;
350
+ if (params["configId"] === "model" && options.configOptions === true) {
351
+ if (!MODEL_VALUES.includes(value))
352
+ throw new RpcError(RPC_ERRORS.invalidParams, `unknown model: ${value}`);
353
+ s.model = value;
354
+ }
355
+ else if (params["configId"] === "mode" && options.configOnly === true) {
356
+ if (!MODE_VALUES.includes(value))
357
+ throw new RpcError(RPC_ERRORS.invalidParams, `unknown mode: ${value}`);
358
+ s.mode = value;
359
+ }
360
+ else {
361
+ throw new RpcError(RPC_ERRORS.invalidParams, `unknown config: ${String(params["configId"])}`);
362
+ }
280
363
  return { configOptions: configOf(s) };
281
364
  }
282
365
  case ACP_METHODS.sessionPrompt:
@@ -2,9 +2,9 @@
2
2
  * ACP(Agent Client Protocol)v1 的子集类型(docs/wave5-plan.md §5.1,D14)。[W5-E]
3
3
  *
4
4
  * 手写、零依赖;只收 ama 作为客户端(驱动外部 Agent)与服务端(`ama --mode acp`)两侧用到的部分:
5
- * `initialize`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode|set_config_option`、
6
- * `session/update`(含 `usage_update`)、`session/request_permission`、`elicitation/create`。字段名与规范一致(camelCase),
7
- * 未知字段一律保留不报错;`_meta` 不解释。
5
+ * `initialize`、`authenticate`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode|set_config_option`、
6
+ * `session/update`(含 `usage_update`)、`session/request_permission`、`elicitation/create`、`$/cancel_request`。
7
+ * 字段名与规范一致(camelCase),未知字段一律保留不报错;`_meta` 不解释(ama 自己发的只用 {@link ACP_META_KEY})。
8
8
  *
9
9
  * 不声明 `fs` / `terminal` 客户端能力(与 Armadra Q5 一致):外部 Agent 自己读写、自己跑命令,
10
10
  * 用它自己的权限策略;它要问人的才经 `session/request_permission` 回到 ama。
@@ -14,6 +14,8 @@ export declare const ACP_PROTOCOL_VERSION: 1;
14
14
  /** 方法名(客户端 → Agent 的请求 / 通知,Agent → 客户端的请求 / 通知)。 */
15
15
  export declare const ACP_METHODS: {
16
16
  readonly initialize: "initialize";
17
+ /** ama 不实现(只给 terminal 型认证方法,规范要求这类方法不经 authenticate)。 */
18
+ readonly authenticate: "authenticate";
17
19
  readonly sessionNew: "session/new";
18
20
  readonly sessionLoad: "session/load";
19
21
  readonly sessionResume: "session/resume";
@@ -26,6 +28,8 @@ export declare const ACP_METHODS: {
26
28
  readonly sessionUpdate: "session/update";
27
29
  readonly requestPermission: "session/request_permission";
28
30
  readonly elicitationCreate: "elicitation/create";
31
+ /** 协议级取消(双向通知,`requestId` 指对端发来的、尚未答复的请求)。 */
32
+ readonly cancelRequest: "$/cancel_request";
29
33
  };
30
34
  /** JSON-RPC 错误码(规范沿用 JSON-RPC 2.0;`-32000` 为 ACP 的 auth_required)。 */
31
35
  export declare const RPC_ERRORS: {
@@ -36,6 +40,8 @@ export declare const RPC_ERRORS: {
36
40
  readonly internalError: -32603;
37
41
  readonly authRequired: -32000;
38
42
  readonly resourceNotFound: -32002;
43
+ /** 请求被 `$/cancel_request` 撤回。 */
44
+ readonly requestCancelled: -32800;
39
45
  };
40
46
  export interface AcpTextContent {
41
47
  type: "text";
@@ -80,6 +86,16 @@ export interface AcpClientCapabilities {
80
86
  terminal?: boolean;
81
87
  /** 能接 `elicitation/create`(存在即支持)。 */
82
88
  elicitation?: Record<string, unknown>;
89
+ /** 会话相关的客户端能力;`configOptions.boolean` 存在即能显示 boolean 型配置项。 */
90
+ session?: {
91
+ configOptions?: {
92
+ boolean?: Record<string, unknown> | null;
93
+ } | null;
94
+ } | null;
95
+ /** `terminal: true`:客户端能替 Agent 起终端跑 terminal 型认证方法。 */
96
+ auth?: {
97
+ terminal?: boolean;
98
+ };
83
99
  }
84
100
  export interface AcpInitializeParams {
85
101
  protocolVersion: number;
@@ -103,11 +119,30 @@ export interface AcpAgentCapabilities {
103
119
  resume?: Record<string, unknown> | null;
104
120
  close?: Record<string, unknown> | null;
105
121
  };
122
+ /** Agent 侧认证能力(`logout` 存在即支持 `logout`)。 */
123
+ auth?: {
124
+ logout?: Record<string, unknown> | null;
125
+ };
106
126
  }
107
- export interface AcpAuthMethod {
127
+ /**
128
+ * 认证方法:缺省 / `agent` 型经 `authenticate` 完成;`terminal` 型由客户端起终端跑
129
+ * `<agent 命令> <args…>`(带 `env`),规范要求这类方法不经 `authenticate`。
130
+ */
131
+ export type AcpAuthMethod = {
132
+ type?: "agent";
133
+ id: string;
134
+ name: string;
135
+ description?: string | null;
136
+ } | {
137
+ type: "terminal";
108
138
  id: string;
109
139
  name: string;
110
140
  description?: string | null;
141
+ args?: string[];
142
+ env?: Record<string, string>;
143
+ };
144
+ export interface AcpAuthenticateParams {
145
+ methodId: string;
111
146
  }
112
147
  export interface AcpInitializeResult {
113
148
  protocolVersion: number;
@@ -176,16 +211,19 @@ export interface AcpConfigSelectGroup {
176
211
  name: string;
177
212
  options: AcpConfigSelectOption[];
178
213
  }
179
- /** `session/new|load|resume` 答的 `configOptions[]` 的一项(目前规范只有 `select`)。 */
214
+ /** 配置项的语义分类:只是给客户端排版的提示,不参与正确性。 */
215
+ export type AcpConfigCategory = "mode" | "model" | "model_config" | "thought_level" | (string & {});
216
+ /** `session/new|load|resume` 答的 `configOptions[]` 的一项(ama 只发 `select`)。 */
180
217
  export interface AcpSessionConfigOption {
181
218
  id: string;
182
219
  name: string;
183
220
  description?: string | null;
184
221
  /** `mode` / `model` / `thought_level`,或 Agent 自己的。 */
185
- category?: string | null;
222
+ category?: AcpConfigCategory | null;
186
223
  type: "select" | (string & {});
187
224
  currentValue: string;
188
- options: (AcpConfigSelectOption | AcpConfigSelectGroup)[];
225
+ /** 全部平铺或全部分组,规范不允许混排。 */
226
+ options: AcpConfigSelectOption[] | AcpConfigSelectGroup[];
189
227
  }
190
228
  export interface AcpSetConfigOptionParams {
191
229
  sessionId: string;
@@ -269,7 +307,10 @@ export interface AcpPromptParams {
269
307
  sessionId: string;
270
308
  prompt: AcpContentBlock[];
271
309
  }
272
- /** 本回合用量(可选字段,按 2026-06 稳定的 usage 提案)。 */
310
+ /**
311
+ * 本回合用量。UNSTABLE:1.24.1 仍只在 schema.unstable.json 里(稳定 schema 的 PromptResponse
312
+ * 不列它,但允许附加字段,故照发);客户端不应依赖。
313
+ */
273
314
  export interface AcpPromptUsage {
274
315
  totalTokens?: number;
275
316
  inputTokens?: number;
@@ -285,6 +326,17 @@ export interface AcpPromptResult {
285
326
  export interface AcpCancelParams {
286
327
  sessionId: string;
287
328
  }
329
+ /** `$/cancel_request` 的参数:被撤回的请求 id。 */
330
+ export interface AcpCancelRequestParams {
331
+ requestId: string | number | null;
332
+ }
333
+ /** ama 自己的 `_meta` 命名空间(客户端不得假设其含义)。 */
334
+ export declare const ACP_META_KEY: "ama";
335
+ /** `_meta.ama` 的内容。 */
336
+ export interface AcpAmaMeta {
337
+ /** codemode 内层调用所属的外层工具调用。 */
338
+ parentToolCallId?: string;
339
+ }
288
340
  export type { AcpToolKind };
289
341
  export type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed";
290
342
  export interface AcpToolCallLocation {
@@ -312,6 +364,10 @@ export interface AcpToolCall {
312
364
  locations?: AcpToolCallLocation[];
313
365
  rawInput?: unknown;
314
366
  rawOutput?: unknown;
367
+ /** 工具名(规范新增,供客户端按工具分组 / 记忆授权)。 */
368
+ name?: string | null;
369
+ /** 扩展位;ama 只用 `{ [ACP_META_KEY]: AcpAmaMeta }`。 */
370
+ _meta?: Record<string, unknown> | null;
315
371
  }
316
372
  /** `tool_call_update` 与权限请求里的 toolCall:除 id 外都可缺。 */
317
373
  export type AcpToolCallUpdate = Partial<AcpToolCall> & {
@@ -353,15 +409,23 @@ export type AcpSessionUpdate = {
353
409
  } | null;
354
410
  } | {
355
411
  sessionUpdate: "available_commands_update";
356
- availableCommands: {
357
- name: string;
358
- description: string;
359
- }[];
412
+ availableCommands: AcpAvailableCommand[];
413
+ } | {
414
+ sessionUpdate: "config_option_update";
415
+ configOptions: AcpSessionConfigOption[];
360
416
  } | {
361
417
  sessionUpdate: "session_info_update";
362
418
  title?: string | null;
363
419
  updatedAt?: string | null;
364
420
  };
421
+ /** 客户端可列给人选的斜杠命令(`/<name>`);`input.hint` 是参数提示。 */
422
+ export interface AcpAvailableCommand {
423
+ name: string;
424
+ description: string;
425
+ input?: {
426
+ hint: string;
427
+ } | null;
428
+ }
365
429
  export interface AcpSessionNotification {
366
430
  sessionId: string;
367
431
  update: AcpSessionUpdate;
@@ -2,9 +2,9 @@
2
2
  * ACP(Agent Client Protocol)v1 的子集类型(docs/wave5-plan.md §5.1,D14)。[W5-E]
3
3
  *
4
4
  * 手写、零依赖;只收 ama 作为客户端(驱动外部 Agent)与服务端(`ama --mode acp`)两侧用到的部分:
5
- * `initialize`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode|set_config_option`、
6
- * `session/update`(含 `usage_update`)、`session/request_permission`、`elicitation/create`。字段名与规范一致(camelCase),
7
- * 未知字段一律保留不报错;`_meta` 不解释。
5
+ * `initialize`、`authenticate`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode|set_config_option`、
6
+ * `session/update`(含 `usage_update`)、`session/request_permission`、`elicitation/create`、`$/cancel_request`。
7
+ * 字段名与规范一致(camelCase),未知字段一律保留不报错;`_meta` 不解释(ama 自己发的只用 {@link ACP_META_KEY})。
8
8
  *
9
9
  * 不声明 `fs` / `terminal` 客户端能力(与 Armadra Q5 一致):外部 Agent 自己读写、自己跑命令,
10
10
  * 用它自己的权限策略;它要问人的才经 `session/request_permission` 回到 ama。
@@ -13,6 +13,8 @@ export const ACP_PROTOCOL_VERSION = 1;
13
13
  /** 方法名(客户端 → Agent 的请求 / 通知,Agent → 客户端的请求 / 通知)。 */
14
14
  export const ACP_METHODS = {
15
15
  initialize: "initialize",
16
+ /** ama 不实现(只给 terminal 型认证方法,规范要求这类方法不经 authenticate)。 */
17
+ authenticate: "authenticate",
16
18
  sessionNew: "session/new",
17
19
  sessionLoad: "session/load",
18
20
  sessionResume: "session/resume",
@@ -25,6 +27,8 @@ export const ACP_METHODS = {
25
27
  sessionUpdate: "session/update",
26
28
  requestPermission: "session/request_permission",
27
29
  elicitationCreate: "elicitation/create",
30
+ /** 协议级取消(双向通知,`requestId` 指对端发来的、尚未答复的请求)。 */
31
+ cancelRequest: "$/cancel_request",
28
32
  };
29
33
  /** JSON-RPC 错误码(规范沿用 JSON-RPC 2.0;`-32000` 为 ACP 的 auth_required)。 */
30
34
  export const RPC_ERRORS = {
@@ -35,4 +39,8 @@ export const RPC_ERRORS = {
35
39
  internalError: -32603,
36
40
  authRequired: -32000,
37
41
  resourceNotFound: -32002,
42
+ /** 请求被 `$/cancel_request` 撤回。 */
43
+ requestCancelled: -32800,
38
44
  };
45
+ /** ama 自己的 `_meta` 命名空间(客户端不得假设其含义)。 */
46
+ export const ACP_META_KEY = "ama";
@@ -5,7 +5,10 @@
5
5
  * - 分帧复用 `modes/rpc/jsonl.ts`(只按 `\n` 切行、64 KiB 分片写、背压等 drain),写入串行;
6
6
  * - 双向:本端可发请求 / 通知,也处理对端的请求(`onRequest` 抛 {@link RpcError} 即回错误)与通知;
7
7
  * - `jsonrpcField: false` 时不写 `"jsonrpc":"2.0"`(Codex app-server 的线上形状),读取两种都认;
8
- * - 连接关闭(输入流结束)时所有挂起的请求以 `connection_closed` 失败。
8
+ * - 连接关闭(输入流结束)时所有挂起的请求以 `connection_closed` 失败;
9
+ * - `cancelRequests: true`(ACP 两侧)时支持协议级取消 `$/cancel_request`:本端请求的 `signal` abort
10
+ * 会通知对端;对端撤回本端正在处理的请求时 abort 该请求的 `ctx.signal`,处理器此后抛错一律回 -32800。
11
+ * 缺省关闭(Codex app-server 不认),线上形状与之前逐字节相同。
9
12
  */
10
13
  export type RpcId = string | number;
11
14
  export declare class RpcError extends Error {
@@ -15,7 +18,7 @@ export declare class RpcError extends Error {
15
18
  }
16
19
  export interface IncomingRequestContext {
17
20
  readonly id: RpcId;
18
- /** 连接关闭时 abort。 */
21
+ /** 连接关闭时 abort;`cancelRequests` 开着时对端发 `$/cancel_request` 撤回本请求也 abort。 */
19
22
  readonly signal: AbortSignal;
20
23
  }
21
24
  export interface JsonRpcPeerOptions {
@@ -29,10 +32,14 @@ export interface JsonRpcPeerOptions {
29
32
  onClose?(): void;
30
33
  /** 无法解析的行(不影响连接)。 */
31
34
  onProtocolError?(line: string, reason: string): void;
35
+ /** 协议级取消(ACP `$/cancel_request`):缺省 false(Codex app-server 不认)。 */
36
+ cancelRequests?: boolean;
32
37
  }
33
38
  export declare class JsonRpcPeer {
34
39
  private readonly options;
35
40
  private readonly pending;
41
+ /** 正在处理的对端请求(只在 `cancelRequests` 时记录),供 `$/cancel_request` 撤回。 */
42
+ private readonly inflight;
36
43
  private readonly reader;
37
44
  private readonly lifetime;
38
45
  private nextId;
@@ -43,7 +50,10 @@ export declare class JsonRpcPeer {
43
50
  readonly closed: Promise<void>;
44
51
  constructor(options: JsonRpcPeerOptions);
45
52
  get isOpen(): boolean;
46
- /** 发请求;`signal` abort 时本端不再等(不通知对端,协议级取消由调用方另发)。 */
53
+ /**
54
+ * 发请求;`signal` abort 时本端不再等。`cancelRequests` 开着时同时给对端发
55
+ * `$/cancel_request { requestId }`(已发出的请求才发),否则不通知对端。
56
+ */
47
57
  request<T = unknown>(method: string, params?: unknown, signal?: AbortSignal): Promise<T>;
48
58
  /** 输入流结束后仍可写(对端可能只关了自己的写端,还在读);写失败静默。 */
49
59
  notify(method: string, params?: unknown): Promise<void>;
@@ -5,7 +5,10 @@
5
5
  * - 分帧复用 `modes/rpc/jsonl.ts`(只按 `\n` 切行、64 KiB 分片写、背压等 drain),写入串行;
6
6
  * - 双向:本端可发请求 / 通知,也处理对端的请求(`onRequest` 抛 {@link RpcError} 即回错误)与通知;
7
7
  * - `jsonrpcField: false` 时不写 `"jsonrpc":"2.0"`(Codex app-server 的线上形状),读取两种都认;
8
- * - 连接关闭(输入流结束)时所有挂起的请求以 `connection_closed` 失败。
8
+ * - 连接关闭(输入流结束)时所有挂起的请求以 `connection_closed` 失败;
9
+ * - `cancelRequests: true`(ACP 两侧)时支持协议级取消 `$/cancel_request`:本端请求的 `signal` abort
10
+ * 会通知对端;对端撤回本端正在处理的请求时 abort 该请求的 `ctx.signal`,处理器此后抛错一律回 -32800。
11
+ * 缺省关闭(Codex app-server 不认),线上形状与之前逐字节相同。
9
12
  */
10
13
  import { createLineReader, writeChunked } from "../modes/rpc/jsonl.js";
11
14
  import { RPC_ERRORS } from "./acp/types.js";
@@ -19,9 +22,13 @@ export class RpcError extends Error {
19
22
  this.name = "RpcError";
20
23
  }
21
24
  }
25
+ /** `$/cancel_request` 的方法名(与 ACP_METHODS.cancelRequest 相同;这里不依赖 ACP 词汇表)。 */
26
+ const CANCEL_REQUEST = "$/cancel_request";
22
27
  export class JsonRpcPeer {
23
28
  options;
24
29
  pending = new Map();
30
+ /** 正在处理的对端请求(只在 `cancelRequests` 时记录),供 `$/cancel_request` 撤回。 */
31
+ inflight = new Map();
25
32
  reader;
26
33
  lifetime = new AbortController();
27
34
  nextId = 1;
@@ -39,14 +46,19 @@ export class JsonRpcPeer {
39
46
  get isOpen() {
40
47
  return !this.isClosed;
41
48
  }
42
- /** 发请求;`signal` abort 时本端不再等(不通知对端,协议级取消由调用方另发)。 */
49
+ /**
50
+ * 发请求;`signal` abort 时本端不再等。`cancelRequests` 开着时同时给对端发
51
+ * `$/cancel_request { requestId }`(已发出的请求才发),否则不通知对端。
52
+ */
43
53
  request(method, params, signal) {
44
54
  if (this.isClosed)
45
55
  return Promise.reject(closedError(method));
46
56
  const id = this.nextId++;
47
57
  return new Promise((resolve, reject) => {
48
58
  const onAbort = () => {
49
- this.pending.delete(id);
59
+ const sent = this.pending.delete(id);
60
+ if (sent && this.options.cancelRequests === true && !this.isClosed)
61
+ void this.notify(CANCEL_REQUEST, { requestId: id });
50
62
  reject(new RpcError(RPC_ERRORS.internalError, `${method}: aborted`));
51
63
  };
52
64
  if (signal?.aborted === true)
@@ -116,6 +128,14 @@ export class JsonRpcPeer {
116
128
  return;
117
129
  }
118
130
  if (typeof message.method === "string") {
131
+ if (message.method === CANCEL_REQUEST &&
132
+ this.options.cancelRequests === true &&
133
+ (message.id === undefined || message.id === null)) {
134
+ const requestId = message.params?.requestId;
135
+ if (typeof requestId === "string" || typeof requestId === "number")
136
+ this.inflight.get(requestId)?.abort();
137
+ return;
138
+ }
119
139
  if (message.id !== undefined && message.id !== null) {
120
140
  void this.handleRequest(message.id, message.method, message.params);
121
141
  }
@@ -154,14 +174,23 @@ export class JsonRpcPeer {
154
174
  });
155
175
  return;
156
176
  }
177
+ let cancel;
178
+ let signal = this.lifetime.signal;
179
+ if (this.options.cancelRequests === true) {
180
+ cancel = new AbortController();
181
+ this.inflight.set(id, cancel);
182
+ signal = AbortSignal.any([this.lifetime.signal, cancel.signal]);
183
+ }
157
184
  try {
158
- const result = await handler(method, params, { id, signal: this.lifetime.signal });
185
+ const result = await handler(method, params, { id, signal });
159
186
  await this.send({ id, result: result ?? null });
160
187
  }
161
188
  catch (error) {
162
- const rpc = error instanceof RpcError
163
- ? error
164
- : new RpcError(RPC_ERRORS.internalError, error instanceof Error ? error.message : String(error));
189
+ const rpc = cancel?.signal.aborted === true
190
+ ? new RpcError(RPC_ERRORS.requestCancelled, `${method}: request cancelled`)
191
+ : error instanceof RpcError
192
+ ? error
193
+ : new RpcError(RPC_ERRORS.internalError, error instanceof Error ? error.message : String(error));
165
194
  await this.send({
166
195
  id,
167
196
  error: {
@@ -171,6 +200,10 @@ export class JsonRpcPeer {
171
200
  },
172
201
  });
173
202
  }
203
+ finally {
204
+ if (cancel !== undefined && this.inflight.get(id) === cancel)
205
+ this.inflight.delete(id);
206
+ }
174
207
  }
175
208
  }
176
209
  function closedError(method) {
@@ -2,7 +2,8 @@
2
2
  * 一个回合的结果汇总(各驱动共用)。[W5-E]
3
3
  *
4
4
  * 驱动把归一化后的 {@link DriverEvent} 交给收集器:它转发给 `onEvent`,同时累计最终文本、
5
- * 工具摘要(≤ 20 行,§5.2)、触及的文件(edit / delete / move 类工具的 locations)与用量。
5
+ * 工具摘要(≤ 20 行,§5.2)、触及的文件(edit / delete / move 类工具的 locations,以及带 diff 内容的
6
+ * 工具改动的路径——不论种类,见 {@link TurnCollector.noteDiff})与用量。
6
7
  * 原始事件只在内存里流过,不落盘(§5.4 敏感数据)。
7
8
  */
8
9
  import type { AcpToolKind, DriverEvent, DriverTurnResult } from "./types.js";
@@ -12,6 +13,10 @@ interface ToolState {
12
13
  kind: AcpToolKind;
13
14
  status: "pending" | "in_progress" | "completed" | "failed";
14
15
  locations: string[];
16
+ /** 工具内容里 diff 的路径(ACP `content[].type === "diff"`)。 */
17
+ diffPaths?: string[];
18
+ /** 已转发过的状态(去重用)。 */
19
+ reported: Set<ToolState["status"]>;
15
20
  }
16
21
  export declare class TurnCollector {
17
22
  private readonly onEvent;
@@ -20,8 +25,16 @@ export declare class TurnCollector {
20
25
  private readonly order;
21
26
  private usage;
22
27
  constructor(onEvent: (event: DriverEvent) => void);
23
- /** 归一化事件:累计后转发给 onEvent。 */
28
+ /**
29
+ * 归一化事件:累计后转发给 onEvent。同一工具调用回到已报过的状态(如审批前后
30
+ * `in_progress → pending → in_progress`)且标题、种类、位置都没变时不再转发,免得下游重复报进度。
31
+ */
24
32
  push(event: DriverEvent): void;
33
+ /**
34
+ * 记下工具改动的文件(diff 内容的路径):工具完成后计入 filesTouched,不论它报的种类。
35
+ * 在 `push` 该工具的 `tool_call` 之后调(未知的 id 忽略)。[ACP-D]
36
+ */
37
+ noteDiff(id: string, paths: readonly string[]): void;
25
38
  /** 工具调用的最近状态(驱动做 tool_call_update 时补全标题与种类)。 */
26
39
  tool(id: string): Readonly<ToolState> | undefined;
27
40
  /** 回合结束时的累计用量合并(`result.usage` 等)。 */