@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.
- package/CHANGELOG.md +61 -0
- package/CHANGELOG.zh-CN.md +43 -0
- package/README.md +4 -3
- package/README.zh-CN.md +1 -1
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +2 -2
- package/dist/ai/apis/shared.d.ts +7 -1
- package/dist/ai/apis/shared.js +10 -0
- package/dist/ai/fake/fake-provider.js +5 -1
- package/dist/ai/fake/fake-script.d.ts +5 -2
- package/dist/ai/fake/fake-script.js +6 -2
- package/dist/ai/types.d.ts +2 -1
- package/dist/bundle/ama.cjs +48757 -47456
- package/dist/cli/args.d.ts +6 -0
- package/dist/cli/args.js +46 -0
- package/dist/cli/bootstrap.js +13 -2
- package/dist/cli/compose-events.d.ts +10 -0
- package/dist/cli/compose-events.js +97 -0
- package/dist/cli/compose-session.d.ts +25 -4
- package/dist/cli/compose-session.js +130 -103
- package/dist/cli/compose.js +6 -1
- package/dist/cli/runtime.d.ts +3 -1
- package/dist/cli/subcommands/auth.d.ts +13 -1
- package/dist/cli/subcommands/auth.js +28 -1
- package/dist/drivers/acp/client.d.ts +7 -2
- package/dist/drivers/acp/client.js +10 -1
- package/dist/drivers/acp/driver.d.ts +15 -4
- package/dist/drivers/acp/driver.js +110 -33
- package/dist/drivers/acp/testing/fake-agent-main.d.ts +1 -1
- package/dist/drivers/acp/testing/fake-agent-main.js +9 -2
- package/dist/drivers/acp/testing/fake-agent.d.ts +17 -2
- package/dist/drivers/acp/testing/fake-agent.js +108 -25
- package/dist/drivers/acp/types.d.ts +76 -12
- package/dist/drivers/acp/types.js +11 -3
- package/dist/drivers/jsonrpc.d.ts +13 -3
- package/dist/drivers/jsonrpc.js +40 -7
- package/dist/drivers/turn.d.ts +15 -2
- package/dist/drivers/turn.js +33 -4
- package/dist/i18n/catalog.d.ts +59 -16
- package/dist/i18n/catalog.js +4 -1
- package/dist/i18n/messages/acp.d.ts +125 -0
- package/dist/i18n/messages/acp.js +126 -0
- package/dist/i18n/messages/auth.d.ts +2 -0
- package/dist/i18n/messages/auth.js +4 -2
- package/dist/i18n/messages/cli-args.d.ts +2 -0
- package/dist/i18n/messages/cli-args.js +2 -0
- package/dist/i18n/messages/cli.d.ts +4 -0
- package/dist/i18n/messages/cli.js +2 -0
- package/dist/i18n/messages/print.d.ts +3 -33
- package/dist/i18n/messages/print.js +3 -33
- package/dist/modes/acp/acp-auth-gate.d.ts +40 -0
- package/dist/modes/acp/acp-auth-gate.js +201 -0
- package/dist/modes/acp/acp-config.d.ts +43 -0
- package/dist/modes/acp/acp-config.js +151 -0
- package/dist/modes/acp/acp-connection.d.ts +29 -0
- package/dist/modes/acp/acp-connection.js +37 -0
- package/dist/modes/acp/acp-events.d.ts +59 -23
- package/dist/modes/acp/acp-events.js +154 -88
- package/dist/modes/acp/acp-mode.d.ts +13 -2
- package/dist/modes/acp/acp-mode.js +23 -8
- package/dist/modes/acp/acp-server.d.ts +59 -32
- package/dist/modes/acp/acp-server.js +322 -128
- package/dist/modes/acp/acp-sessions.d.ts +80 -0
- package/dist/modes/acp/acp-sessions.js +157 -0
- package/dist/modes/acp/acp-tool-text.d.ts +23 -0
- package/dist/modes/acp/acp-tool-text.js +83 -0
- package/dist/modes/print/json-event.d.ts +2 -1
- package/dist/modes/print/json-event.js +6 -1
- package/dist/tools/edit.d.ts +6 -2
- package/dist/tools/edit.js +19 -2
- package/dist/tools/types.d.ts +11 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/write.d.ts +1 -0
- package/dist/tools/write.js +21 -3
- package/docs/acp.md +143 -29
- package/docs/agents.md +2 -0
- package/docs/codemode.md +1 -1
- package/docs/en/acp.md +192 -0
- 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)。
|
|
2
|
+
* `ama --mode acp`:ama 的会话事件 → ACP `session/update`(docs/wave5-plan.md §5.6、docs/acp-plan.md §2.3)。
|
|
3
3
|
*
|
|
4
|
-
* | ama
|
|
5
|
-
* |
|
|
6
|
-
* | message_update text_delta
|
|
7
|
-
* | message_update thinking_delta
|
|
8
|
-
* | message_update toolcall_end
|
|
9
|
-
* | tool_execution_start
|
|
10
|
-
* |
|
|
11
|
-
* |
|
|
12
|
-
* |
|
|
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
|
|
15
|
-
*
|
|
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
|
|
20
|
-
export
|
|
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:
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
29
32
|
}[];
|
|
30
33
|
};
|
|
31
|
-
/**
|
|
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
|
-
|
|
39
|
-
|
|
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)。
|
|
2
|
+
* `ama --mode acp`:ama 的会话事件 → ACP `session/update`(docs/wave5-plan.md §5.6、docs/acp-plan.md §2.3)。
|
|
3
3
|
*
|
|
4
|
-
* | ama
|
|
5
|
-
* |
|
|
6
|
-
* | message_update text_delta
|
|
7
|
-
* | message_update thinking_delta
|
|
8
|
-
* | message_update toolcall_end
|
|
9
|
-
* | tool_execution_start
|
|
10
|
-
* |
|
|
11
|
-
* |
|
|
12
|
-
* |
|
|
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
|
|
15
|
-
*
|
|
19
|
+
* 工具结果只回前 4 KB 文本(完整结果在 ama 会话里);diff 只在实时事件里有(`fileChange` 不落盘)。
|
|
20
|
+
* `session/load` 回放同一套映射(用户消息 → user_message_chunk,工具结果带文本、无 diff)。
|
|
16
21
|
*/
|
|
17
|
-
import {
|
|
22
|
+
import { ACP_META_KEY, } from "../../drivers/acp/types.js";
|
|
18
23
|
import { PERMISSION_MODES_STRICT_FIRST } from "../../permissions/types.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
export
|
|
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) => ({
|
|
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
|
-
/**
|
|
37
|
+
/** 一个会话的事件映射器(记住已公布的工具调用,供权限请求关联 toolCallId 与状态)。 */
|
|
75
38
|
export class AcpEventMapper {
|
|
76
39
|
cwd;
|
|
77
40
|
emit;
|
|
78
41
|
session;
|
|
79
|
-
|
|
42
|
+
extras;
|
|
43
|
+
/** 模型已发出、尚未执行完的工具调用(按工具名 + 参数匹配的后备)。 */
|
|
80
44
|
pendingCalls = [];
|
|
81
|
-
|
|
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
|
-
/**
|
|
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:
|
|
121
|
+
id: wire,
|
|
114
122
|
name: call.name,
|
|
115
123
|
args: JSON.stringify(call.arguments),
|
|
116
124
|
});
|
|
117
|
-
this.emitToolCall(
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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:
|
|
150
|
+
toolCallId: wire,
|
|
139
151
|
status: event.isError ? "failed" : "completed",
|
|
140
|
-
content
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 关闭 →
|
|
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 关闭 →
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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
|
};
|