@armadra/agent 0.6.8 → 0.7.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CHANGELOG.zh-CN.md +43 -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/bundle/ama.cjs +48757 -47456
  14. package/dist/cli/args.d.ts +6 -0
  15. package/dist/cli/args.js +46 -0
  16. package/dist/cli/bootstrap.js +13 -2
  17. package/dist/cli/compose-events.d.ts +10 -0
  18. package/dist/cli/compose-events.js +97 -0
  19. package/dist/cli/compose-session.d.ts +25 -4
  20. package/dist/cli/compose-session.js +130 -103
  21. package/dist/cli/compose.js +6 -1
  22. package/dist/cli/runtime.d.ts +3 -1
  23. package/dist/cli/subcommands/auth.d.ts +13 -1
  24. package/dist/cli/subcommands/auth.js +28 -1
  25. package/dist/drivers/acp/client.d.ts +7 -2
  26. package/dist/drivers/acp/client.js +10 -1
  27. package/dist/drivers/acp/driver.d.ts +15 -4
  28. package/dist/drivers/acp/driver.js +110 -33
  29. package/dist/drivers/acp/testing/fake-agent-main.d.ts +1 -1
  30. package/dist/drivers/acp/testing/fake-agent-main.js +9 -2
  31. package/dist/drivers/acp/testing/fake-agent.d.ts +17 -2
  32. package/dist/drivers/acp/testing/fake-agent.js +108 -25
  33. package/dist/drivers/acp/types.d.ts +76 -12
  34. package/dist/drivers/acp/types.js +11 -3
  35. package/dist/drivers/jsonrpc.d.ts +13 -3
  36. package/dist/drivers/jsonrpc.js +40 -7
  37. package/dist/drivers/turn.d.ts +15 -2
  38. package/dist/drivers/turn.js +33 -4
  39. package/dist/i18n/catalog.d.ts +59 -16
  40. package/dist/i18n/catalog.js +4 -1
  41. package/dist/i18n/messages/acp.d.ts +125 -0
  42. package/dist/i18n/messages/acp.js +126 -0
  43. package/dist/i18n/messages/auth.d.ts +2 -0
  44. package/dist/i18n/messages/auth.js +4 -2
  45. package/dist/i18n/messages/cli-args.d.ts +2 -0
  46. package/dist/i18n/messages/cli-args.js +2 -0
  47. package/dist/i18n/messages/cli.d.ts +4 -0
  48. package/dist/i18n/messages/cli.js +2 -0
  49. package/dist/i18n/messages/print.d.ts +3 -33
  50. package/dist/i18n/messages/print.js +3 -33
  51. package/dist/modes/acp/acp-auth-gate.d.ts +40 -0
  52. package/dist/modes/acp/acp-auth-gate.js +201 -0
  53. package/dist/modes/acp/acp-config.d.ts +43 -0
  54. package/dist/modes/acp/acp-config.js +151 -0
  55. package/dist/modes/acp/acp-connection.d.ts +29 -0
  56. package/dist/modes/acp/acp-connection.js +37 -0
  57. package/dist/modes/acp/acp-events.d.ts +59 -23
  58. package/dist/modes/acp/acp-events.js +154 -88
  59. package/dist/modes/acp/acp-mode.d.ts +13 -2
  60. package/dist/modes/acp/acp-mode.js +23 -8
  61. package/dist/modes/acp/acp-server.d.ts +59 -32
  62. package/dist/modes/acp/acp-server.js +322 -128
  63. package/dist/modes/acp/acp-sessions.d.ts +80 -0
  64. package/dist/modes/acp/acp-sessions.js +157 -0
  65. package/dist/modes/acp/acp-tool-text.d.ts +23 -0
  66. package/dist/modes/acp/acp-tool-text.js +83 -0
  67. package/dist/modes/print/json-event.d.ts +2 -1
  68. package/dist/modes/print/json-event.js +6 -1
  69. package/dist/tools/edit.d.ts +6 -2
  70. package/dist/tools/edit.js +19 -2
  71. package/dist/tools/types.d.ts +11 -0
  72. package/dist/tools/types.js +2 -0
  73. package/dist/tools/write.d.ts +1 -0
  74. package/dist/tools/write.js +21 -3
  75. package/docs/acp.md +143 -29
  76. package/docs/agents.md +2 -0
  77. package/docs/codemode.md +1 -1
  78. package/docs/en/acp.md +192 -0
  79. package/package.json +1 -1
@@ -1,46 +1,82 @@
1
1
  /**
2
- * `ama --mode acp`:ama 的会话事件 → ACP `session/update`(docs/wave5-plan.md §5.6)。[W5-E]
2
+ * `ama --mode acp`:ama 的会话事件 → ACP `session/update`(docs/wave5-plan.md §5.6、docs/acp-plan.md §2.3)。
3
3
  *
4
- * | ama | ACP |
5
- * | ------------------------------------ | ---------------------------------------------------------- |
6
- * | message_update text_delta | agent_message_chunk |
7
- * | message_update thinking_delta | agent_thought_chunk |
8
- * | message_update toolcall_end | tool_call(pending,带 rawInput 与 locations) |
9
- * | tool_execution_start / end | tool_call_update(in_progress → completed / failed) |
10
- * | todo_updated | plan |
11
- * | turn_end | usage_update(上下文用量、窗口、会话累计美元) |
12
- * | permission_mode_changed | current_mode_update |
4
+ * | ama | ACP |
5
+ * | ----------------------------------------- | ---------------------------------------------------------------- |
6
+ * | message_update text_delta | agent_message_chunk |
7
+ * | message_update thinking_delta | agent_thought_chunk |
8
+ * | message_update toolcall_end | tool_call(pending,带 name、rawInput 与 locations) |
9
+ * | tool_execution_start(带 parent) | tool_call(pending,title 带 codemode 前缀,`_meta.ama.parentToolCallId`) |
10
+ * | tool_execution_start | tool_call_update(in_progress) |
11
+ * | permission_request(toolCallId 已公布) | tool_call_update(pending) |
12
+ * | permission_resolved(allow / allow_session)| tool_call_update(in_progress) |
13
+ * | tool_execution_end | tool_call_update(completed / failed,`[diff?, text]`,locations[].line) |
14
+ * | todo_updated | plan |
15
+ * | turn_end | usage_update(上下文用量、窗口、会话累计美元) |
16
+ * | permission_mode_changed | current_mode_update + config_option_update |
17
+ * | model_changed / thinking_level_changed | config_option_update |
13
18
  *
14
- * 工具结果只回前 4 KB 文本(完整结果在 ama 会话里),codemode 内层调用(带 parentToolCallId)
15
- * 不单列。`session/load` 回放同一套映射(用户消息 → user_message_chunk)。
19
+ * 工具结果只回前 4 KB 文本(完整结果在 ama 会话里);diff 只在实时事件里有(`fileChange` 不落盘)。
20
+ * `session/load` 回放同一套映射(用户消息 → user_message_chunk,工具结果带文本、无 diff)。
16
21
  */
17
22
  import type { AgentMessage } from "../../session/types.js";
18
23
  import type { AgentSession, SessionEvent } from "../../agent/types.js";
19
- import type { AcpSessionUpdate, AcpToolCallLocation, AcpToolKind } from "../../drivers/acp/types.js";
20
- export declare const TOOL_OUTPUT_LIMIT: number;
21
- export declare function toolKind(name: string): AcpToolKind;
22
- export declare function toolTitle(name: string, args: unknown): string;
23
- export declare function toolLocations(args: unknown, cwd: string): AcpToolCallLocation[] | undefined;
24
+ import { type AcpAvailableCommand, type AcpSessionConfigOption, type AcpSessionUpdate } from "../../drivers/acp/types.js";
25
+ export { TOOL_OUTPUT_LIMIT, toolKind, toolLocations, toolTitle } from "./acp-tool-text.js";
24
26
  export declare function permissionModes(currentModeId: string): {
25
27
  currentModeId: string;
26
28
  availableModes: {
27
29
  id: import("../../permissions/types.js").PermissionMode;
28
- name: import("../../permissions/types.js").PermissionMode;
30
+ name: string;
31
+ description: string;
29
32
  }[];
30
33
  };
31
- /** 一个会话的事件映射器(记住模型发出的工具调用,供权限请求关联 toolCallId)。 */
34
+ /** 一个会话的事件映射器(记住已公布的工具调用,供权限请求关联 toolCallId 与状态)。 */
32
35
  export declare class AcpEventMapper {
33
36
  private readonly cwd;
34
37
  private readonly emit;
35
38
  private readonly session;
36
- /** 模型已发出、尚未执行完的工具调用。 */
39
+ /** 会话的配置项与命令表({@link announce} 与配置变化时取;[ACP-D] 填实际内容)。 */
40
+ protected readonly extras: () => {
41
+ configOptions: AcpSessionConfigOption[];
42
+ commands: AcpAvailableCommand[];
43
+ };
44
+ /** 模型已发出、尚未执行完的工具调用(按工具名 + 参数匹配的后备)。 */
37
45
  private readonly pendingCalls;
38
- constructor(cwd: string, emit: (update: AcpSessionUpdate) => void, session: () => AgentSession);
39
- /** 权限请求对应的工具调用 id(按工具名与参数匹配最近一个)。 */
46
+ /** 已经以 `tool_call` 公布、尚未结束的调用:id → title(含 codemode 内层)。 */
47
+ private readonly published;
48
+ /** 挂起的权限请求:requestId → 已公布的 toolCallId。 */
49
+ private readonly permissionCalls;
50
+ /**
51
+ * ama 的工具调用 id → 线上 toolCallId。ACP 要求 id 在会话内唯一,上游给的 id 却可能在后续回合重复
52
+ * (假模型、个别兼容接口的兜底 id);重复时加 `#n` 后缀,之后的开始 / 结束 / 审批都按最近一次映射。
53
+ */
54
+ private readonly wireIds;
55
+ private readonly usedWireIds;
56
+ /** 上次 `session_info_update` 带出的标题。 */
57
+ private lastTitle;
58
+ constructor(cwd: string, emit: (update: AcpSessionUpdate) => void, session: () => AgentSession,
59
+ /** 会话的配置项与命令表({@link announce} 与配置变化时取;[ACP-D] 填实际内容)。 */
60
+ extras?: () => {
61
+ configOptions: AcpSessionConfigOption[];
62
+ commands: AcpAvailableCommand[];
63
+ });
64
+ /** 会话 new / load / resume 的响应发出后由服务端调:`available_commands_update` 与 `config_option_update`。 */
65
+ announce(): void;
66
+ /** 回合结束后由服务端调:`session_info_update`(标题与上次不同才带 title)。 */
67
+ emitSessionInfo(title: string | null, updatedAt: string): void;
68
+ /** 权限请求对应的工具调用 id(按工具名与参数匹配最近一个;有 `context.toolCallId` 时不需要)。 */
40
69
  toolCallIdFor(toolName: string, input: unknown): string | undefined;
70
+ /** 已公布且未结束的工具调用的标题(codemode 内层带前缀);未公布返回 undefined。 */
71
+ titleFor(toolCallId: string): string | undefined;
72
+ /** ama 的工具调用 id 在线上用的 toolCallId(没公布过的原样返回)。 */
73
+ wireId(toolCallId: string): string;
74
+ private claimWireId;
41
75
  onEvent(event: SessionEvent): void;
42
76
  emitUsage(): void;
43
- /** `session/load`:按消息回放历史。 */
77
+ /** `session/load`:按消息回放历史(工具结果带前 4 KB 文本;diff 不落盘,回放没有)。 */
44
78
  replay(messages: readonly AgentMessage[]): void;
79
+ private status;
80
+ /** 公布一次工具调用(pending);`parentToolCallId` 给了即 codemode 内层调用。返回 title。 */
45
81
  private emitToolCall;
46
82
  }
@@ -1,89 +1,80 @@
1
1
  /**
2
- * `ama --mode acp`:ama 的会话事件 → ACP `session/update`(docs/wave5-plan.md §5.6)。[W5-E]
2
+ * `ama --mode acp`:ama 的会话事件 → ACP `session/update`(docs/wave5-plan.md §5.6、docs/acp-plan.md §2.3)。
3
3
  *
4
- * | ama | ACP |
5
- * | ------------------------------------ | ---------------------------------------------------------- |
6
- * | message_update text_delta | agent_message_chunk |
7
- * | message_update thinking_delta | agent_thought_chunk |
8
- * | message_update toolcall_end | tool_call(pending,带 rawInput 与 locations) |
9
- * | tool_execution_start / end | tool_call_update(in_progress → completed / failed) |
10
- * | todo_updated | plan |
11
- * | turn_end | usage_update(上下文用量、窗口、会话累计美元) |
12
- * | permission_mode_changed | current_mode_update |
4
+ * | ama | ACP |
5
+ * | ----------------------------------------- | ---------------------------------------------------------------- |
6
+ * | message_update text_delta | agent_message_chunk |
7
+ * | message_update thinking_delta | agent_thought_chunk |
8
+ * | message_update toolcall_end | tool_call(pending,带 name、rawInput 与 locations) |
9
+ * | tool_execution_start(带 parent) | tool_call(pending,title 带 codemode 前缀,`_meta.ama.parentToolCallId`) |
10
+ * | tool_execution_start | tool_call_update(in_progress) |
11
+ * | permission_request(toolCallId 已公布) | tool_call_update(pending) |
12
+ * | permission_resolved(allow / allow_session)| tool_call_update(in_progress) |
13
+ * | tool_execution_end | tool_call_update(completed / failed,`[diff?, text]`,locations[].line) |
14
+ * | todo_updated | plan |
15
+ * | turn_end | usage_update(上下文用量、窗口、会话累计美元) |
16
+ * | permission_mode_changed | current_mode_update + config_option_update |
17
+ * | model_changed / thinking_level_changed | config_option_update |
13
18
  *
14
- * 工具结果只回前 4 KB 文本(完整结果在 ama 会话里),codemode 内层调用(带 parentToolCallId)
15
- * 不单列。`session/load` 回放同一套映射(用户消息 → user_message_chunk)。
19
+ * 工具结果只回前 4 KB 文本(完整结果在 ama 会话里);diff 只在实时事件里有(`fileChange` 不落盘)。
20
+ * `session/load` 回放同一套映射(用户消息 → user_message_chunk,工具结果带文本、无 diff)。
16
21
  */
17
- import { isAbsolute, resolve } from "node:path";
22
+ import { ACP_META_KEY, } from "../../drivers/acp/types.js";
18
23
  import { PERMISSION_MODES_STRICT_FIRST } from "../../permissions/types.js";
19
- import { oneLine } from "../../drivers/turn.js";
20
- import { msg } from "../../i18n/index.js";
21
- export const TOOL_OUTPUT_LIMIT = 4 * 1024;
22
- const KINDS = {
23
- read: "read",
24
- write: "edit",
25
- edit: "edit",
26
- bash: "execute",
27
- grep: "search",
28
- glob: "search",
29
- ls: "search",
30
- find: "search",
31
- web_fetch: "fetch",
32
- web_search: "fetch",
33
- todo: "think",
34
- };
35
- export function toolKind(name) {
36
- return KINDS[name] ?? "other";
37
- }
38
- function argOf(args, ...keys) {
39
- if (args === null || typeof args !== "object")
40
- return undefined;
41
- for (const key of keys) {
42
- const value = args[key];
43
- if (typeof value === "string" && value !== "")
44
- return value;
45
- }
46
- return undefined;
47
- }
48
- export function toolTitle(name, args) {
49
- const detail = argOf(args, "command") ??
50
- argOf(args, "path", "file_path", "filePath") ??
51
- argOf(args, "pattern", "query", "url", "prompt");
52
- return detail === undefined ? name : `${name}: ${oneLine(detail, 100)}`;
53
- }
54
- export function toolLocations(args, cwd) {
55
- const path = argOf(args, "path", "file_path", "filePath");
56
- if (path === undefined)
57
- return undefined;
58
- return [{ path: isAbsolute(path) ? path : resolve(cwd, path) }];
59
- }
60
- function resultText(result) {
61
- const text = typeof result.content === "string"
62
- ? result.content
63
- : result.content.map((b) => (b.type === "text" ? b.text : `[${b.type}]`)).join("");
64
- return text.length > TOOL_OUTPUT_LIMIT
65
- ? `${text.slice(0, TOOL_OUTPUT_LIMIT)}\n${msg().print.acp.truncated(text.length)}`
66
- : text;
67
- }
24
+ import { PERMISSION_MODE_INFO, permissionModeLabel } from "../../permissions/modes.js";
25
+ import { resultContent, resultText, toolKind, toolLocations, toolTitle } from "./acp-tool-text.js";
26
+ export { TOOL_OUTPUT_LIMIT, toolKind, toolLocations, toolTitle } from "./acp-tool-text.js";
68
27
  export function permissionModes(currentModeId) {
69
28
  return {
70
29
  currentModeId,
71
- availableModes: PERMISSION_MODES_STRICT_FIRST.map((id) => ({ id, name: id })),
30
+ availableModes: PERMISSION_MODES_STRICT_FIRST.map((id) => ({
31
+ id,
32
+ name: permissionModeLabel(id),
33
+ description: PERMISSION_MODE_INFO[id].description,
34
+ })),
72
35
  };
73
36
  }
74
- /** 一个会话的事件映射器(记住模型发出的工具调用,供权限请求关联 toolCallId)。 */
37
+ /** 一个会话的事件映射器(记住已公布的工具调用,供权限请求关联 toolCallId 与状态)。 */
75
38
  export class AcpEventMapper {
76
39
  cwd;
77
40
  emit;
78
41
  session;
79
- /** 模型已发出、尚未执行完的工具调用。 */
42
+ extras;
43
+ /** 模型已发出、尚未执行完的工具调用(按工具名 + 参数匹配的后备)。 */
80
44
  pendingCalls = [];
81
- constructor(cwd, emit, session) {
45
+ /** 已经以 `tool_call` 公布、尚未结束的调用:id → title(含 codemode 内层)。 */
46
+ published = new Map();
47
+ /** 挂起的权限请求:requestId → 已公布的 toolCallId。 */
48
+ permissionCalls = new Map();
49
+ /**
50
+ * ama 的工具调用 id → 线上 toolCallId。ACP 要求 id 在会话内唯一,上游给的 id 却可能在后续回合重复
51
+ * (假模型、个别兼容接口的兜底 id);重复时加 `#n` 后缀,之后的开始 / 结束 / 审批都按最近一次映射。
52
+ */
53
+ wireIds = new Map();
54
+ usedWireIds = new Set();
55
+ /** 上次 `session_info_update` 带出的标题。 */
56
+ lastTitle = null;
57
+ constructor(cwd, emit, session,
58
+ /** 会话的配置项与命令表({@link announce} 与配置变化时取;[ACP-D] 填实际内容)。 */
59
+ extras = () => ({ configOptions: [], commands: [] })) {
82
60
  this.cwd = cwd;
83
61
  this.emit = emit;
84
62
  this.session = session;
63
+ this.extras = extras;
64
+ }
65
+ /** 会话 new / load / resume 的响应发出后由服务端调:`available_commands_update` 与 `config_option_update`。 */
66
+ announce() {
67
+ const { configOptions, commands } = this.extras();
68
+ this.emit({ sessionUpdate: "available_commands_update", availableCommands: commands });
69
+ this.emit({ sessionUpdate: "config_option_update", configOptions });
85
70
  }
86
- /** 权限请求对应的工具调用 id(按工具名与参数匹配最近一个)。 */
71
+ /** 回合结束后由服务端调:`session_info_update`(标题与上次不同才带 title)。 */
72
+ emitSessionInfo(title, updatedAt) {
73
+ const changed = title !== this.lastTitle;
74
+ this.lastTitle = title;
75
+ this.emit({ sessionUpdate: "session_info_update", ...(changed ? { title } : {}), updatedAt });
76
+ }
77
+ /** 权限请求对应的工具调用 id(按工具名与参数匹配最近一个;有 `context.toolCallId` 时不需要)。 */
87
78
  toolCallIdFor(toolName, input) {
88
79
  const args = JSON.stringify(input ?? null);
89
80
  for (let i = this.pendingCalls.length - 1; i >= 0; i--) {
@@ -93,6 +84,22 @@ export class AcpEventMapper {
93
84
  }
94
85
  return undefined;
95
86
  }
87
+ /** 已公布且未结束的工具调用的标题(codemode 内层带前缀);未公布返回 undefined。 */
88
+ titleFor(toolCallId) {
89
+ return this.published.get(this.wireId(toolCallId));
90
+ }
91
+ /** ama 的工具调用 id 在线上用的 toolCallId(没公布过的原样返回)。 */
92
+ wireId(toolCallId) {
93
+ return this.wireIds.get(toolCallId) ?? toolCallId;
94
+ }
95
+ claimWireId(toolCallId) {
96
+ let wire = toolCallId;
97
+ for (let n = 2; this.usedWireIds.has(wire); n++)
98
+ wire = `${toolCallId}#${n}`;
99
+ this.usedWireIds.add(wire);
100
+ this.wireIds.set(toolCallId, wire);
101
+ return wire;
102
+ }
96
103
  onEvent(event) {
97
104
  switch (event.type) {
98
105
  case "message_update": {
@@ -109,38 +116,68 @@ export class AcpEventMapper {
109
116
  });
110
117
  else if (e.type === "toolcall_end") {
111
118
  const call = e.toolCall;
119
+ const wire = this.claimWireId(call.id);
112
120
  this.pendingCalls.push({
113
- id: call.id,
121
+ id: wire,
114
122
  name: call.name,
115
123
  args: JSON.stringify(call.arguments),
116
124
  });
117
- this.emitToolCall(call.id, call.name, call.arguments, "pending");
125
+ this.published.set(wire, this.emitToolCall(wire, call.name, call.arguments));
118
126
  }
119
127
  return;
120
128
  }
121
- case "tool_execution_start":
122
- if (event.parentToolCallId !== undefined)
123
- return;
124
- this.emit({
125
- sessionUpdate: "tool_call_update",
126
- toolCallId: event.toolCallId,
127
- status: "in_progress",
128
- });
129
+ case "tool_execution_start": {
130
+ let wire;
131
+ if (event.parentToolCallId !== undefined) {
132
+ wire = this.claimWireId(event.toolCallId);
133
+ const title = this.emitToolCall(wire, event.toolName, event.args, this.wireId(event.parentToolCallId));
134
+ this.published.set(wire, title);
135
+ }
136
+ else
137
+ wire = this.wireId(event.toolCallId);
138
+ this.status(wire, "in_progress");
129
139
  return;
140
+ }
130
141
  case "tool_execution_end": {
131
- if (event.parentToolCallId !== undefined)
132
- return;
133
- const at = this.pendingCalls.findIndex((c) => c.id === event.toolCallId);
142
+ const wire = this.wireId(event.toolCallId);
143
+ const at = this.pendingCalls.findIndex((c) => c.id === wire);
134
144
  if (at >= 0)
135
145
  this.pendingCalls.splice(at, 1);
146
+ this.published.delete(wire);
147
+ const { content, locations } = resultContent(event.result);
136
148
  this.emit({
137
149
  sessionUpdate: "tool_call_update",
138
- toolCallId: event.toolCallId,
150
+ toolCallId: wire,
139
151
  status: event.isError ? "failed" : "completed",
140
- content: [{ type: "content", content: { type: "text", text: resultText(event.result) } }],
152
+ content,
153
+ ...(locations !== undefined ? { locations } : {}),
141
154
  });
142
155
  return;
143
156
  }
157
+ case "permission_request": {
158
+ // 只认本会话已公布的调用:子 Agent / 外部 Agent 的请求(depth、origin)的 id 不在本会话的列表里
159
+ const context = event.context;
160
+ if (context?.toolCallId === undefined)
161
+ return;
162
+ if ((context.depth ?? 0) > 0 || context.origin !== undefined)
163
+ return;
164
+ const id = this.wireId(context.toolCallId);
165
+ if (!this.published.has(id))
166
+ return;
167
+ this.permissionCalls.set(event.requestId, id);
168
+ this.status(id, "pending");
169
+ return;
170
+ }
171
+ case "permission_resolved": {
172
+ const id = this.permissionCalls.get(event.requestId);
173
+ if (id === undefined)
174
+ return;
175
+ this.permissionCalls.delete(event.requestId);
176
+ // 拒绝时由随后的 tool_execution_end(failed)收口
177
+ if (event.decision !== "deny" && this.published.has(id))
178
+ this.status(id, "in_progress");
179
+ return;
180
+ }
144
181
  case "todo_updated":
145
182
  this.emit({
146
183
  sessionUpdate: "plan",
@@ -155,7 +192,19 @@ export class AcpEventMapper {
155
192
  this.emitUsage();
156
193
  return;
157
194
  case "permission_mode_changed":
195
+ // 只认 configOptions 的客户端(Zed)看 mode 配置项,只认 modes 的看 current_mode_update
158
196
  this.emit({ sessionUpdate: "current_mode_update", currentModeId: event.mode });
197
+ this.emit({
198
+ sessionUpdate: "config_option_update",
199
+ configOptions: this.extras().configOptions,
200
+ });
201
+ return;
202
+ case "model_changed":
203
+ case "thinking_level_changed":
204
+ this.emit({
205
+ sessionUpdate: "config_option_update",
206
+ configOptions: this.extras().configOptions,
207
+ });
159
208
  return;
160
209
  default:
161
210
  return;
@@ -172,8 +221,13 @@ export class AcpEventMapper {
172
221
  ...(stats.cost !== undefined ? { cost: { amount: stats.cost, currency: "USD" } } : {}),
173
222
  });
174
223
  }
175
- /** `session/load`:按消息回放历史。 */
224
+ /** `session/load`:按消息回放历史(工具结果带前 4 KB 文本;diff 不落盘,回放没有)。 */
176
225
  replay(messages) {
226
+ // 回放从头编号(客户端会重建整条线程);有调用在跑时沿用现有映射,免得和实时更新对不上
227
+ if (this.published.size === 0) {
228
+ this.wireIds.clear();
229
+ this.usedWireIds.clear();
230
+ }
177
231
  for (const message of messages) {
178
232
  if (!("role" in message))
179
233
  continue;
@@ -194,29 +248,41 @@ export class AcpEventMapper {
194
248
  content: { type: "text", text: block.thinking },
195
249
  });
196
250
  else if (block.type === "toolCall")
197
- this.emitToolCall(block.id, block.name, block.arguments, "pending");
251
+ this.emitToolCall(this.claimWireId(block.id), block.name, block.arguments);
198
252
  }
199
253
  }
200
254
  else if (message.role === "toolResult") {
201
255
  this.emit({
202
256
  sessionUpdate: "tool_call_update",
203
- toolCallId: message.toolCallId,
257
+ toolCallId: this.wireId(message.toolCallId),
204
258
  status: message.isError ? "failed" : "completed",
259
+ content: [
260
+ { type: "content", content: { type: "text", text: resultText(message.content) } },
261
+ ],
205
262
  });
206
263
  }
207
264
  }
208
265
  }
209
- emitToolCall(id, name, args, status) {
266
+ status(toolCallId, status) {
267
+ this.emit({ sessionUpdate: "tool_call_update", toolCallId, status });
268
+ }
269
+ /** 公布一次工具调用(pending);`parentToolCallId` 给了即 codemode 内层调用。返回 title。 */
270
+ emitToolCall(id, name, args, parentToolCallId) {
210
271
  const locations = toolLocations(args, this.cwd);
272
+ const title = toolTitle(name, args, parentToolCallId !== undefined);
273
+ const meta = parentToolCallId !== undefined ? { parentToolCallId } : undefined;
211
274
  this.emit({
212
275
  sessionUpdate: "tool_call",
213
276
  toolCallId: id,
214
- title: toolTitle(name, args),
277
+ name,
278
+ title,
215
279
  kind: toolKind(name),
216
- status,
280
+ status: "pending",
217
281
  rawInput: args,
218
282
  ...(locations !== undefined ? { locations } : {}),
283
+ ...(meta !== undefined ? { _meta: { [ACP_META_KEY]: meta } } : {}),
219
284
  });
285
+ return title;
220
286
  }
221
287
  }
222
288
  function contentOf(content) {
@@ -3,15 +3,26 @@
3
3
  *
4
4
  * - stdin / stdout 是 ACP(JSON-RPC 2.0 over NDJSON),不发 hello(ACP 由客户端 `initialize` 起头);
5
5
  * 诊断与宿主通知写 stderr。
6
+ * - 连接(JSON-RPC 对等端 + 协商结果)由 `createAcpConnection` 建,服务端只处理消息。
6
7
  * - 审批:挂 {@link AcpServer.broker} 为 UI broker,ama 的审批请求以 `session/request_permission`
7
8
  * 交给客户端。
8
- * - 退出语义同 rpc:stdin 关闭 → 撤下审批、等在途请求与已开始的运行结束 → 退出 0;
9
- * SIGINT / SIGTERM → abort 后退出 130 / 143。
9
+ * - 退出语义同 rpc:stdin 关闭 → 撤下审批、排队的提示回 cancelled、等已开始的运行结束、释放全部
10
+ * 会话(前台会话由 Runtime.dispose 收尾)→ 退出 0;SIGINT / SIGTERM → abort 后退出 130 / 143。
10
11
  */
11
12
  import type { ModeContext } from "../../cli/deps.js";
12
13
  import type { Runtime } from "../../cli/runtime.js";
14
+ import { type AcpConnection } from "./acp-connection.js";
15
+ import { AcpServer } from "./acp-server.js";
13
16
  export interface AcpModeOptions {
14
17
  stdin?: NodeJS.ReadableStream;
15
18
  stdout?: NodeJS.WritableStream;
19
+ /**
20
+ * [ACP-A] 认证门交接:沿用门控建好的连接(可能已 `initialize`),不再新建;服务端建好后同步交给
21
+ * `attach`,门控此后把请求与通知都转给它。
22
+ */
23
+ handover?: {
24
+ connection: AcpConnection;
25
+ attach(server: AcpServer): void;
26
+ };
16
27
  }
17
28
  export declare function runAcpMode(runtime: Runtime, context: ModeContext, options?: AcpModeOptions): Promise<number>;
@@ -3,18 +3,36 @@
3
3
  *
4
4
  * - stdin / stdout 是 ACP(JSON-RPC 2.0 over NDJSON),不发 hello(ACP 由客户端 `initialize` 起头);
5
5
  * 诊断与宿主通知写 stderr。
6
+ * - 连接(JSON-RPC 对等端 + 协商结果)由 `createAcpConnection` 建,服务端只处理消息。
6
7
  * - 审批:挂 {@link AcpServer.broker} 为 UI broker,ama 的审批请求以 `session/request_permission`
7
8
  * 交给客户端。
8
- * - 退出语义同 rpc:stdin 关闭 → 撤下审批、等在途请求与已开始的运行结束 → 退出 0;
9
- * SIGINT / SIGTERM → abort 后退出 130 / 143。
9
+ * - 退出语义同 rpc:stdin 关闭 → 撤下审批、排队的提示回 cancelled、等已开始的运行结束、释放全部
10
+ * 会话(前台会话由 Runtime.dispose 收尾)→ 退出 0;SIGINT / SIGTERM → abort 后退出 130 / 143。
10
11
  */
11
12
  import { ExitCode } from "../../cli/exit-codes.js";
12
13
  import { onTerminationSignals } from "../shared.js";
14
+ import { createAcpConnection } from "./acp-connection.js";
13
15
  import { AcpServer } from "./acp-server.js";
16
+ import { RpcError } from "../../drivers/jsonrpc.js";
17
+ import { RPC_ERRORS } from "../../drivers/acp/types.js";
14
18
  export async function runAcpMode(runtime, context, options = {}) {
15
19
  const stdin = options.stdin ?? process.stdin;
16
20
  const stdout = options.stdout ?? process.stdout;
17
- const server = new AcpServer(runtime, { input: stdin, output: stdout }, (message) => context.io.stderr(`ama: ${message}\n`));
21
+ const log = (message) => context.io.stderr(`ama: ${message}\n`);
22
+ // 连接先建(handlers 转给服务端);JsonRpcPeer 的读取是异步的,构造完服务端之前不会有消息到达。
23
+ const target = {};
24
+ const connection = options.handover?.connection ??
25
+ createAcpConnection({ input: stdin, output: stdout }, log, {
26
+ onRequest: async (method, params, ctx) => {
27
+ if (target.server === undefined)
28
+ throw new RpcError(RPC_ERRORS.internalError, method);
29
+ return target.server.handle(method, (params ?? {}), ctx);
30
+ },
31
+ onNotification: (method, params) => target.server?.handleNotification(method, params),
32
+ });
33
+ const server = new AcpServer(runtime, connection, log);
34
+ target.server = server;
35
+ options.handover?.attach(server);
18
36
  runtime.approvals.setUiBroker(server.broker);
19
37
  runtime.notifier.set((message, level) => context.io.stderr(`ama: [${level}] ${message}\n`));
20
38
  return new Promise((resolve) => {
@@ -26,13 +44,10 @@ export async function runAcpMode(runtime, context, options = {}) {
26
44
  offSignals();
27
45
  stdin.pause?.();
28
46
  runtime.approvals.setUiBroker(undefined);
29
- const session = server.session();
30
- if (abort)
31
- await session.abort().catch(() => undefined);
32
- await session.waitForIdle().catch(() => undefined);
47
+ // 全部会话:排队的回 cancelled,等在跑的结束(信号时先 abort),兄弟会话依次 dispose
48
+ await server.dispose(abort);
33
49
  await server.peer.flush();
34
50
  server.peer.close();
35
- server.dispose();
36
51
  runtime.notifier.set(undefined);
37
52
  resolve(code);
38
53
  };