@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,36 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ama --mode acp`:把 ama 的会话暴露为 ACP Agent(docs/wave5-plan.md §5.6,D14
|
|
2
|
+
* `ama --mode acp`:把 ama 的会话暴露为 ACP Agent(docs/wave5-plan.md §5.6,D14;多会话见
|
|
3
|
+
* docs/acp-plan.md D1、D2、D11、D12、§2.1)。[W5-E,ACP-B]
|
|
3
4
|
*
|
|
4
|
-
* 与 `--mode rpc` 同一引擎(Runtime +
|
|
5
|
-
* -
|
|
6
|
-
* `session/
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
5
|
+
* 与 `--mode rpc` 同一引擎(Runtime + 组装根)。多会话:
|
|
6
|
+
* - 每个 ACP sessionId 一个常驻会话(会话池,acp-sessions.ts),各有自己的事件映射器与权限模式;
|
|
7
|
+
* 启动时那个空会话由第一个 `session/new` 认领,之后的 new / load / resume 建兄弟会话(不 dispose 旧的)。
|
|
8
|
+
* - 同一时刻只跑一个回合:其它会话的 `session/prompt` 进 FIFO 队列;出队时把该会话切为前台
|
|
9
|
+
* (宿主 / Hook / 工具工厂跟随,清「本会话允许」记忆),并把它自己的权限模式重放到共享管线。
|
|
10
|
+
* - `session/cancel`:在跑 → abort;排队中 → 直接回 cancelled。`session/close`:中断、释放并出池,
|
|
11
|
+
* 之后对该 id 发 prompt 回 -32002。`$/cancel_request` 撤回 prompt 等价 cancel,答复 -32800。
|
|
12
|
+
* - `session/set_mode`:记在会话上;是前台就立即生效,否则出队时生效(仍发 `current_mode_update`)。
|
|
13
|
+
* - `session/list`:按 cwd 过滤(别的目录 → 空列表),每页 50 条,cursor 翻页;标题去掉嵌入资源块。
|
|
14
|
+
* - 回合结束:stopReason(中断 → cancelled、截断 → max_tokens、拒答 → refusal、出错 → JSON-RPC 错误),
|
|
15
|
+
* 并发 `session_info_update`。
|
|
16
|
+
* - 审批经 `session/request_permission` 交给客户端;不声明 / 不使用客户端的 `fs`、`terminal` 能力。
|
|
17
|
+
* - cwd 固定为启动目录;客户端给的 `mcpServers` / `additionalDirectories` 忽略(stderr 一行)。
|
|
15
18
|
*/
|
|
16
19
|
import { realpathSync } from "node:fs";
|
|
17
20
|
import { resolve } from "node:path";
|
|
18
|
-
import {
|
|
21
|
+
import { stopReasonOf } from "../../ai/apis/shared.js";
|
|
22
|
+
import { createSessionAlongside, currentSession, disposeSessionAlongside, setForegroundSession, } from "../../cli/compose-session.js";
|
|
19
23
|
import { listSessions } from "../../cli/compose-store.js";
|
|
20
24
|
import { AgentSessionImpl } from "../../agent/session.js";
|
|
21
|
-
import {
|
|
25
|
+
import { RpcError } from "../../drivers/jsonrpc.js";
|
|
22
26
|
import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "../../drivers/acp/types.js";
|
|
23
27
|
import { isPermissionMode } from "../../permissions/modes.js";
|
|
24
28
|
import { AMA_VERSION } from "../../version.js";
|
|
25
29
|
import { errorText } from "../shared.js";
|
|
26
30
|
import { AcpEventMapper, permissionModes, toolKind, toolLocations, toolTitle, } from "./acp-events.js";
|
|
27
31
|
import { msg } from "../../i18n/index.js";
|
|
32
|
+
import { CONFIG_IDS, applyConfigOption, availableCommands, buildConfigOptions, prepareConfigOptions, } from "./acp-config.js";
|
|
33
|
+
import { clientCapabilitiesOf } from "./acp-connection.js";
|
|
34
|
+
import { PromptQueue, cancelledResult, pageSessions, sessionTitle, } from "./acp-sessions.js";
|
|
28
35
|
export const ACP_AGENT_CAPABILITIES = {
|
|
29
36
|
loadSession: true,
|
|
30
37
|
promptCapabilities: { image: true, audio: false, embeddedContext: true },
|
|
31
38
|
mcpCapabilities: { http: false, sse: false },
|
|
32
39
|
sessionCapabilities: { list: {}, resume: {}, close: {} },
|
|
33
40
|
};
|
|
41
|
+
/** ama 生成的会话 id(`randomUUID()`)。 */
|
|
42
|
+
const SESSION_ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
34
43
|
/** 解析符号链接后的绝对路径(macOS 的 /var → /private/var 等);不存在时退回 resolve。 */
|
|
35
44
|
function canonical(path) {
|
|
36
45
|
try {
|
|
@@ -43,13 +52,13 @@ function canonical(path) {
|
|
|
43
52
|
function str(params, key) {
|
|
44
53
|
const value = params[key];
|
|
45
54
|
if (typeof value !== "string" || value === "")
|
|
46
|
-
throw new RpcError(RPC_ERRORS.invalidParams, msg().
|
|
55
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.missingParam(key));
|
|
47
56
|
return value;
|
|
48
57
|
}
|
|
49
58
|
/** ACP 提示 → ama 的文本与图片。 */
|
|
50
59
|
export function promptOf(blocks) {
|
|
51
60
|
if (!Array.isArray(blocks))
|
|
52
|
-
throw new RpcError(RPC_ERRORS.invalidParams, msg().
|
|
61
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.promptNotArray);
|
|
53
62
|
const parts = [];
|
|
54
63
|
const images = [];
|
|
55
64
|
for (const block of blocks) {
|
|
@@ -64,61 +73,118 @@ export function promptOf(blocks) {
|
|
|
64
73
|
}
|
|
65
74
|
return { text: parts.join("\n"), images };
|
|
66
75
|
}
|
|
76
|
+
function countOf(value) {
|
|
77
|
+
return Array.isArray(value) ? value.length : 0;
|
|
78
|
+
}
|
|
67
79
|
export class AcpServer {
|
|
68
80
|
runtime;
|
|
81
|
+
connection;
|
|
69
82
|
log;
|
|
70
83
|
peer;
|
|
71
|
-
activeId;
|
|
72
|
-
/** 启动时的空会话还没被 session/new 认领。 */
|
|
73
|
-
freshUnclaimed = true;
|
|
74
|
-
mapper;
|
|
75
|
-
unsubscribe = () => undefined;
|
|
76
|
-
cancelRequested = false;
|
|
77
84
|
cwd;
|
|
78
|
-
|
|
85
|
+
/** ACP sessionId → 常驻会话。 */
|
|
86
|
+
pool = new Map();
|
|
87
|
+
/** 还没交给客户端的会话(启动时那个;关掉最后一个前台会话后补的):`session/new` 认领。 */
|
|
88
|
+
standby;
|
|
89
|
+
/** 新会话的初始权限模式(启动参数 / 配置给的)。 */
|
|
90
|
+
initialMode;
|
|
91
|
+
queue = new PromptQueue((job) => this.runPrompt(job));
|
|
92
|
+
/**
|
|
93
|
+
* 连接由调用方建(`createAcpConnection`),其 handlers 转到 {@link handle} 与
|
|
94
|
+
* {@link handleNotification};认证门在 bootstrap 成功后以同一条连接构造服务端。
|
|
95
|
+
*/
|
|
96
|
+
constructor(runtime, connection, log = () => undefined) {
|
|
79
97
|
this.runtime = runtime;
|
|
98
|
+
this.connection = connection;
|
|
80
99
|
this.log = log;
|
|
81
100
|
this.cwd = canonical(runtime.paths.cwd);
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
onNotification: (method, params) => {
|
|
88
|
-
if (method === ACP_METHODS.sessionCancel)
|
|
89
|
-
void this.cancel((params ?? {}));
|
|
90
|
-
},
|
|
91
|
-
onProtocolError: (_line, reason) => this.log(msg().print.acp.unparsable(reason)),
|
|
92
|
-
});
|
|
93
|
-
this.subscribe();
|
|
101
|
+
this.peer = connection.peer;
|
|
102
|
+
const startup = currentSession(runtime);
|
|
103
|
+
if (startup instanceof AgentSessionImpl)
|
|
104
|
+
this.standby = startup;
|
|
105
|
+
this.initialMode = startup.state.permissionMode;
|
|
94
106
|
}
|
|
107
|
+
/** 前台会话(宿主 / Hook / 工具工厂当前操作的那个)。 */
|
|
95
108
|
session() {
|
|
96
109
|
return currentSession(this.runtime);
|
|
97
110
|
}
|
|
111
|
+
/** 会话池里的全部会话(含未认领的待命会话);stdin 关闭时逐个收尾。 */
|
|
112
|
+
sessions() {
|
|
113
|
+
const all = [...this.pool.values()].map((entry) => entry.session);
|
|
114
|
+
if (this.standby !== undefined)
|
|
115
|
+
all.push(this.standby);
|
|
116
|
+
return all;
|
|
117
|
+
}
|
|
98
118
|
/** 审批交给客户端:ama 的 UI broker。 */
|
|
99
119
|
broker = {
|
|
100
120
|
ask: (request, signal) => this.askClient(request, signal),
|
|
101
121
|
};
|
|
102
|
-
|
|
103
|
-
return
|
|
122
|
+
extras(session) {
|
|
123
|
+
return {
|
|
124
|
+
configOptions: this.configOptions(session),
|
|
125
|
+
commands: availableCommands(this.runtime.resources),
|
|
126
|
+
};
|
|
104
127
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
configOptions(session) {
|
|
129
|
+
const entry = [...this.pool.values()].find((e) => e.session === session);
|
|
130
|
+
return buildConfigOptions(session, this.runtime.providers, process.env, {
|
|
131
|
+
enabled: this.runtime.config.models?.enabled,
|
|
132
|
+
mode: entry?.mode,
|
|
133
|
+
});
|
|
109
134
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
135
|
+
/** 把会话放进池:自己的映射器(`session/update` 带它自己的 id)与订阅。 */
|
|
136
|
+
adopt(session, mode) {
|
|
137
|
+
const id = session.state.sessionId;
|
|
138
|
+
const mapper = new AcpEventMapper(this.cwd, (update) => void this.peer.notify(ACP_METHODS.sessionUpdate, { sessionId: id, update }), () => session, () => this.extras(session));
|
|
139
|
+
const unsubscribe = session.subscribe((event) => {
|
|
140
|
+
// 会话自己换了模式(plan 流程等)也记下,出队重放时不退回旧模式
|
|
141
|
+
if (event.type === "permission_mode_changed")
|
|
142
|
+
entry.mode = event.mode;
|
|
143
|
+
mapper.onEvent(event);
|
|
144
|
+
});
|
|
145
|
+
const entry = { id, session, mapper, unsubscribe, mode };
|
|
146
|
+
this.pool.set(id, entry);
|
|
147
|
+
return entry;
|
|
114
148
|
}
|
|
115
|
-
|
|
116
|
-
|
|
149
|
+
/**
|
|
150
|
+
* 释放全部会话(stdin 关闭后由模式调用):排队的回 cancelled,等在跑的结束,兄弟会话依次
|
|
151
|
+
* dispose(跑 SessionEnd Hook);前台会话留给 `Runtime.dispose`。
|
|
152
|
+
*/
|
|
153
|
+
async dispose(abort = false) {
|
|
154
|
+
this.queue.cancelQueued();
|
|
155
|
+
if (abort)
|
|
156
|
+
await this.session()
|
|
157
|
+
.abort()
|
|
158
|
+
.catch(() => undefined);
|
|
159
|
+
await this.queue.settled();
|
|
160
|
+
for (const session of this.sessions())
|
|
161
|
+
await session.waitForIdle().catch(() => undefined);
|
|
162
|
+
const foreground = this.session();
|
|
163
|
+
for (const entry of this.pool.values())
|
|
164
|
+
entry.unsubscribe();
|
|
165
|
+
for (const session of this.sessions()) {
|
|
166
|
+
if (session === foreground || !(session instanceof AgentSessionImpl))
|
|
167
|
+
continue;
|
|
168
|
+
if (!(foreground instanceof AgentSessionImpl))
|
|
169
|
+
continue;
|
|
170
|
+
await disposeSessionAlongside(this.runtime, session, foreground).catch(() => undefined);
|
|
171
|
+
}
|
|
172
|
+
this.pool.clear();
|
|
173
|
+
this.standby = undefined;
|
|
117
174
|
}
|
|
118
|
-
|
|
175
|
+
/** 客户端通知(`session/cancel`)。 */
|
|
176
|
+
handleNotification(method, params) {
|
|
177
|
+
if (method === ACP_METHODS.sessionCancel)
|
|
178
|
+
void this.cancel((params ?? {}));
|
|
179
|
+
}
|
|
180
|
+
/** 客户端请求;认证门交接时也由它处理当次请求。 */
|
|
181
|
+
async handle(method, params, ctx) {
|
|
119
182
|
switch (method) {
|
|
120
183
|
case ACP_METHODS.initialize:
|
|
121
184
|
return this.initialize(params);
|
|
185
|
+
case ACP_METHODS.authenticate:
|
|
186
|
+
// 与认证门同一答复:ama 只有 terminal 型方法,按规范不经 authenticate(交接前后一致)
|
|
187
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.auth.notAgentMethod);
|
|
122
188
|
case ACP_METHODS.sessionNew:
|
|
123
189
|
return this.newSession(params);
|
|
124
190
|
case ACP_METHODS.sessionLoad:
|
|
@@ -126,20 +192,23 @@ export class AcpServer {
|
|
|
126
192
|
case ACP_METHODS.sessionResume:
|
|
127
193
|
return this.openSession(params, false);
|
|
128
194
|
case ACP_METHODS.sessionList:
|
|
129
|
-
return this.list();
|
|
195
|
+
return this.list(params);
|
|
130
196
|
case ACP_METHODS.sessionClose:
|
|
131
197
|
return this.close(params);
|
|
132
198
|
case ACP_METHODS.sessionPrompt:
|
|
133
|
-
return this.prompt(params);
|
|
199
|
+
return this.prompt(params, ctx?.signal);
|
|
134
200
|
case ACP_METHODS.sessionSetMode:
|
|
135
201
|
return this.setMode(params);
|
|
202
|
+
case ACP_METHODS.sessionSetConfigOption:
|
|
203
|
+
return this.setConfigOption(params);
|
|
136
204
|
default:
|
|
137
205
|
throw new RpcError(RPC_ERRORS.methodNotFound, `method not found: ${method}`);
|
|
138
206
|
}
|
|
139
207
|
}
|
|
140
208
|
initialize(params) {
|
|
141
209
|
if (typeof params["protocolVersion"] !== "number")
|
|
142
|
-
throw new RpcError(RPC_ERRORS.invalidParams, msg().
|
|
210
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.missingProtocolVersion);
|
|
211
|
+
this.connection.markInitialized(clientCapabilitiesOf(params));
|
|
143
212
|
return {
|
|
144
213
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
145
214
|
agentCapabilities: ACP_AGENT_CAPABILITIES,
|
|
@@ -147,97 +216,164 @@ export class AcpServer {
|
|
|
147
216
|
agentInfo: { name: "ama", title: "ama", version: AMA_VERSION },
|
|
148
217
|
};
|
|
149
218
|
}
|
|
150
|
-
|
|
219
|
+
/** 开会话请求的公共检查:cwd 固定;`mcpServers` / `additionalDirectories` 忽略并在 stderr 说一行。 */
|
|
220
|
+
checkOpen(params) {
|
|
151
221
|
const cwd = params["cwd"];
|
|
152
|
-
if (cwd
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
222
|
+
if (cwd !== undefined && (typeof cwd !== "string" || canonical(cwd) !== this.cwd))
|
|
223
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.fixedCwd(this.cwd, String(cwd)));
|
|
224
|
+
const mcp = countOf(params["mcpServers"]);
|
|
225
|
+
if (mcp > 0)
|
|
226
|
+
this.log(msg().acp.session.ignoredMcp(mcp));
|
|
227
|
+
const dirs = countOf(params["additionalDirectories"]);
|
|
228
|
+
if (dirs > 0)
|
|
229
|
+
this.log(msg().acp.session.ignoredDirs(dirs));
|
|
156
230
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
session.announceStart(reason);
|
|
231
|
+
/** new / load / resume 的答复:模式与配置项;答复发出后再公布命令表与配置项。 */
|
|
232
|
+
opened(entry) {
|
|
233
|
+
const configOptions = this.configOptions(entry.session);
|
|
234
|
+
setImmediate(() => entry.mapper.announce());
|
|
235
|
+
return {
|
|
236
|
+
modes: permissionModes(entry.mode),
|
|
237
|
+
...(configOptions.length > 0 ? { configOptions } : {}),
|
|
238
|
+
};
|
|
166
239
|
}
|
|
167
240
|
async newSession(params) {
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
241
|
+
this.checkOpen(params);
|
|
242
|
+
await prepareConfigOptions(this.runtime.providers);
|
|
243
|
+
let session;
|
|
244
|
+
if (this.standby !== undefined && this.standby.messages.length === 0) {
|
|
245
|
+
session = this.standby;
|
|
246
|
+
this.standby = undefined;
|
|
172
247
|
}
|
|
173
248
|
else {
|
|
174
|
-
this.
|
|
175
|
-
await this.switchTo(switchSession(this.runtime, { kind: "new" }), "new");
|
|
249
|
+
session = await createSessionAlongside(this.runtime, { kind: "new" });
|
|
176
250
|
}
|
|
177
|
-
|
|
178
|
-
|
|
251
|
+
const entry = this.adopt(session, this.initialMode);
|
|
252
|
+
// 兄弟会话自己的 session_start 在订阅之后发(认领的启动会话早已发过)
|
|
253
|
+
if (session !== currentSession(this.runtime))
|
|
254
|
+
session.announceStart("new");
|
|
255
|
+
return { sessionId: entry.id, ...this.opened(entry) };
|
|
179
256
|
}
|
|
180
257
|
async openSession(params, replay) {
|
|
181
258
|
const id = str(params, "sessionId");
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
259
|
+
this.checkOpen(params);
|
|
260
|
+
await prepareConfigOptions(this.runtime.providers);
|
|
261
|
+
let entry = this.pool.get(id);
|
|
262
|
+
if (entry === undefined) {
|
|
263
|
+
if (this.standby !== undefined && this.standby.state.sessionId === id) {
|
|
264
|
+
entry = this.adopt(this.standby, this.initialMode);
|
|
265
|
+
this.standby = undefined;
|
|
187
266
|
}
|
|
188
|
-
|
|
189
|
-
|
|
267
|
+
else {
|
|
268
|
+
let session;
|
|
269
|
+
try {
|
|
270
|
+
// 本服务端发出的 id 都是 UUID;只有这种形状才在文件不存在时按原 id 新建(空会话不落盘,
|
|
271
|
+
// 进程重启后客户端——如 Zed 的 Reload Agent——会带着它回来),其它一律 -32002
|
|
272
|
+
session = await createSessionAlongside(this.runtime, { kind: "resume", id }, { createIfMissing: SESSION_ID_SHAPE.test(id) });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw new RpcError(RPC_ERRORS.resourceNotFound, msg().acp.core.sessionNotFound(id, errorText(error)));
|
|
276
|
+
}
|
|
277
|
+
entry = this.pool.get(id) ?? this.adopt(session, this.initialMode);
|
|
278
|
+
session.announceStart("resume");
|
|
190
279
|
}
|
|
191
280
|
}
|
|
192
|
-
this.freshUnclaimed = false;
|
|
193
|
-
this.activeId = id;
|
|
194
281
|
if (replay) {
|
|
195
|
-
|
|
282
|
+
entry.mapper.replay(entry.session.messages);
|
|
196
283
|
await this.peer.flush();
|
|
197
284
|
}
|
|
198
|
-
return
|
|
285
|
+
return this.opened(entry);
|
|
199
286
|
}
|
|
200
|
-
list() {
|
|
287
|
+
list(params) {
|
|
288
|
+
const cwd = params["cwd"];
|
|
289
|
+
if (cwd !== undefined &&
|
|
290
|
+
cwd !== null &&
|
|
291
|
+
(typeof cwd !== "string" || canonical(cwd) !== this.cwd))
|
|
292
|
+
return { sessions: [] };
|
|
201
293
|
// 会话目录按 ama 启动时的 cwd 字串分组(不是 realpath)
|
|
202
294
|
const items = listSessions({
|
|
203
295
|
sessionDir: this.runtime.paths.sessionDir,
|
|
204
296
|
cwd: this.runtime.paths.cwd,
|
|
205
297
|
});
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
298
|
+
return pageSessions(items, params["cursor"]);
|
|
299
|
+
}
|
|
300
|
+
entryOf(params) {
|
|
301
|
+
const id = str(params, "sessionId");
|
|
302
|
+
const entry = this.pool.get(id);
|
|
303
|
+
if (entry === undefined)
|
|
304
|
+
throw new RpcError(RPC_ERRORS.resourceNotFound, msg().acp.session.notOpen(id));
|
|
305
|
+
return entry;
|
|
214
306
|
}
|
|
215
307
|
async close(params) {
|
|
216
308
|
const id = str(params, "sessionId");
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
309
|
+
const entry = this.pool.get(id);
|
|
310
|
+
if (entry === undefined)
|
|
311
|
+
return {};
|
|
312
|
+
this.pool.delete(id);
|
|
313
|
+
this.queue.cancelQueued(id);
|
|
314
|
+
const running = this.queue.running;
|
|
315
|
+
if (running?.sessionId === id) {
|
|
316
|
+
running.cancelRequested = true;
|
|
317
|
+
await entry.session.abort().catch(() => undefined);
|
|
318
|
+
await this.queue.settled(id);
|
|
221
319
|
}
|
|
320
|
+
entry.unsubscribe();
|
|
321
|
+
let fallback = this.session();
|
|
322
|
+
if (fallback === entry.session) {
|
|
323
|
+
const next = [...this.pool.values()][0]?.session ?? this.standby;
|
|
324
|
+
if (next !== undefined)
|
|
325
|
+
fallback = next;
|
|
326
|
+
else {
|
|
327
|
+
// 关掉的是唯一的会话:补一个待命会话当前台,下一个 session/new 认领它
|
|
328
|
+
this.standby = await createSessionAlongside(this.runtime, { kind: "new" });
|
|
329
|
+
fallback = this.standby;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (fallback instanceof AgentSessionImpl)
|
|
333
|
+
await disposeSessionAlongside(this.runtime, entry.session, fallback);
|
|
222
334
|
return {};
|
|
223
335
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw new RpcError(RPC_ERRORS.invalidRequest, msg().print.acp.busy);
|
|
227
|
-
}
|
|
228
|
-
async prompt(params) {
|
|
229
|
-
const id = str(params, "sessionId");
|
|
230
|
-
if (id !== this.activeId)
|
|
231
|
-
await this.openSession({ sessionId: id }, false);
|
|
336
|
+
async prompt(params, signal) {
|
|
337
|
+
const entry = this.entryOf(params);
|
|
232
338
|
const { text, images } = promptOf(params["prompt"]);
|
|
233
|
-
const
|
|
339
|
+
const result = new Promise((resolve, reject) => {
|
|
340
|
+
const job = {
|
|
341
|
+
sessionId: entry.id,
|
|
342
|
+
text,
|
|
343
|
+
images,
|
|
344
|
+
cancelRequested: false,
|
|
345
|
+
resolve,
|
|
346
|
+
reject,
|
|
347
|
+
};
|
|
348
|
+
// 客户端以 $/cancel_request 撤回:等价 session/cancel,答复由对等端改成 -32800。
|
|
349
|
+
// 连接关闭(stdin 结束)也会 abort 这个 signal——那时不算撤回,已开始的运行照常跑完。
|
|
350
|
+
signal?.addEventListener("abort", () => {
|
|
351
|
+
if (this.peer.isOpen)
|
|
352
|
+
this.cancelJob(job);
|
|
353
|
+
}, { once: true });
|
|
354
|
+
this.queue.push(job);
|
|
355
|
+
});
|
|
356
|
+
const answer = await result;
|
|
357
|
+
if (signal?.aborted === true && this.peer.isOpen)
|
|
358
|
+
throw new RpcError(RPC_ERRORS.requestCancelled, "session/prompt: request cancelled");
|
|
359
|
+
return answer;
|
|
360
|
+
}
|
|
361
|
+
/** 出队:切前台、重放权限模式、跑回合、算 stopReason,再发 `session_info_update`。 */
|
|
362
|
+
async runPrompt(job) {
|
|
363
|
+
const entry = this.pool.get(job.sessionId);
|
|
364
|
+
if (entry === undefined || job.cancelRequested)
|
|
365
|
+
return cancelledResult();
|
|
366
|
+
const { session } = entry;
|
|
367
|
+
setForegroundSession(this.runtime, session);
|
|
368
|
+
if (this.runtime.permission.mode !== entry.mode)
|
|
369
|
+
session.setPermissionMode(entry.mode);
|
|
234
370
|
const before = session.getStats().tokens;
|
|
235
|
-
|
|
371
|
+
let failure;
|
|
236
372
|
try {
|
|
237
|
-
await session.prompt(text, images.length > 0 ? { images } : {});
|
|
373
|
+
await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
|
|
238
374
|
}
|
|
239
375
|
catch (error) {
|
|
240
|
-
|
|
376
|
+
failure = error;
|
|
241
377
|
}
|
|
242
378
|
await this.peer.flush();
|
|
243
379
|
const after = session.getStats().tokens;
|
|
@@ -248,54 +384,112 @@ export class AcpServer {
|
|
|
248
384
|
cachedWriteTokens: after.cacheWrite - before.cacheWrite,
|
|
249
385
|
totalTokens: after.total - before.total,
|
|
250
386
|
};
|
|
387
|
+
entry.mapper.emitSessionInfo(sessionTitle(session.state.sessionName, session.messages), new Date().toISOString());
|
|
388
|
+
if (job.cancelRequested)
|
|
389
|
+
return { stopReason: "cancelled", usage };
|
|
390
|
+
if (failure !== undefined)
|
|
391
|
+
throw new RpcError(RPC_ERRORS.internalError, errorText(failure));
|
|
251
392
|
const last = [...session.messages].reverse().find((m) => "role" in m && m.role === "assistant");
|
|
252
|
-
const reason = last !== undefined && "stopReason" in last ? last
|
|
253
|
-
if (
|
|
393
|
+
const reason = last !== undefined && "stopReason" in last ? stopReasonOf(last) : "stop";
|
|
394
|
+
if (reason === "aborted")
|
|
254
395
|
return { stopReason: "cancelled", usage };
|
|
396
|
+
if (reason === "refusal")
|
|
397
|
+
return { stopReason: "refusal", usage };
|
|
255
398
|
if (reason === "error") {
|
|
256
399
|
const message = last !== undefined && "errorMessage" in last ? last.errorMessage : undefined;
|
|
257
|
-
throw new RpcError(RPC_ERRORS.internalError, message ?? msg().
|
|
400
|
+
throw new RpcError(RPC_ERRORS.internalError, message ?? msg().acp.core.modelFailed);
|
|
258
401
|
}
|
|
259
402
|
return { stopReason: reason === "length" ? "max_tokens" : "end_turn", usage };
|
|
260
403
|
}
|
|
404
|
+
/** 中断一个提示:在跑 → abort;排队中 → 出队后立即回 cancelled(`runPrompt` 见标记直接返回)。 */
|
|
405
|
+
cancelJob(job) {
|
|
406
|
+
job.cancelRequested = true;
|
|
407
|
+
if (this.queue.running === job) {
|
|
408
|
+
const entry = this.pool.get(job.sessionId);
|
|
409
|
+
void entry?.session.abort().catch(() => undefined);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
261
412
|
async cancel(params) {
|
|
262
|
-
|
|
413
|
+
const id = params["sessionId"];
|
|
414
|
+
if (typeof id !== "string")
|
|
263
415
|
return;
|
|
264
|
-
this.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
.
|
|
416
|
+
this.queue.cancelQueued(id);
|
|
417
|
+
const running = this.queue.running;
|
|
418
|
+
if (running?.sessionId === id)
|
|
419
|
+
this.cancelJob(running);
|
|
268
420
|
}
|
|
269
421
|
setMode(params) {
|
|
270
|
-
|
|
422
|
+
const entry = this.entryOf(params);
|
|
271
423
|
const mode = params["modeId"];
|
|
272
424
|
if (!isPermissionMode(mode))
|
|
273
|
-
throw new RpcError(RPC_ERRORS.invalidParams, msg().
|
|
274
|
-
this.
|
|
425
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.unknownMode(String(mode)));
|
|
426
|
+
this.applyMode(entry, mode);
|
|
275
427
|
return {};
|
|
276
428
|
}
|
|
429
|
+
/** `session/set_mode` 与 `mode` 配置项共用:按会话记,前台立即生效,否则出队时生效。 */
|
|
430
|
+
applyMode(entry, mode) {
|
|
431
|
+
entry.mode = mode;
|
|
432
|
+
if (entry.session === this.session() && this.runtime.permission.mode !== mode) {
|
|
433
|
+
entry.session.setPermissionMode(mode); // 映射器发 current_mode_update + config_option_update
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const notify = (update) => void this.peer.notify(ACP_METHODS.sessionUpdate, { sessionId: entry.id, update });
|
|
437
|
+
notify({ sessionUpdate: "current_mode_update", currentModeId: mode });
|
|
438
|
+
notify({
|
|
439
|
+
sessionUpdate: "config_option_update",
|
|
440
|
+
configOptions: this.configOptions(entry.session),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async setConfigOption(params) {
|
|
444
|
+
const entry = this.entryOf(params);
|
|
445
|
+
const configId = str(params, "configId");
|
|
446
|
+
if (configId === CONFIG_IDS.mode) {
|
|
447
|
+
const mode = params["value"];
|
|
448
|
+
if (!isPermissionMode(mode))
|
|
449
|
+
throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.config.invalidValue(configId, String(mode)));
|
|
450
|
+
this.applyMode(entry, mode);
|
|
451
|
+
}
|
|
452
|
+
else
|
|
453
|
+
await applyConfigOption(entry.session, params);
|
|
454
|
+
return { configOptions: this.configOptions(entry.session) };
|
|
455
|
+
}
|
|
277
456
|
async askClient(request, signal) {
|
|
278
|
-
|
|
279
|
-
|
|
457
|
+
// 审批来自在跑的那个回合(它就是前台);不在回合里时按前台会话
|
|
458
|
+
const running = this.queue.running;
|
|
459
|
+
const foreground = this.session();
|
|
460
|
+
const entry = running !== undefined
|
|
461
|
+
? this.pool.get(running.sessionId)
|
|
462
|
+
: [...this.pool.values()].find((e) => e.session === foreground);
|
|
463
|
+
if (entry === undefined || !this.peer.isOpen)
|
|
280
464
|
return undefined;
|
|
281
|
-
|
|
465
|
+
// 本会话(含 codemode 内层)的调用 id 就是已发 tool_call 的 id;子 Agent(depth > 0)的 id 属于
|
|
466
|
+
// 子会话、客户端没见过,退回按工具名 + 参数匹配
|
|
467
|
+
const context = request.context;
|
|
468
|
+
const own = (context?.depth ?? 0) === 0 && context?.taskId === undefined;
|
|
469
|
+
const toolCallId = (own && context?.toolCallId !== undefined
|
|
470
|
+
? entry.mapper.wireId(context.toolCallId)
|
|
471
|
+
: undefined) ??
|
|
472
|
+
entry.mapper.toolCallIdFor(request.toolName, request.input) ??
|
|
473
|
+
request.requestId;
|
|
474
|
+
// 映射器记得的标题优先(codemode 内层带前缀,客户端不会被改成无前缀的标题)
|
|
475
|
+
const title = entry.mapper.titleFor(toolCallId) ?? toolTitle(request.toolName, request.input);
|
|
282
476
|
const locations = toolLocations(request.input, this.cwd);
|
|
283
477
|
let answer;
|
|
284
478
|
try {
|
|
285
479
|
answer = await this.peer.request(ACP_METHODS.requestPermission, {
|
|
286
|
-
sessionId,
|
|
480
|
+
sessionId: entry.id,
|
|
287
481
|
toolCall: {
|
|
288
482
|
toolCallId,
|
|
289
|
-
title
|
|
483
|
+
title,
|
|
290
484
|
kind: toolKind(request.toolName),
|
|
291
485
|
status: "pending",
|
|
292
486
|
rawInput: request.input,
|
|
293
487
|
...(locations !== undefined ? { locations } : {}),
|
|
294
488
|
},
|
|
295
489
|
options: [
|
|
296
|
-
{ optionId: "allow_once", name: msg().
|
|
297
|
-
{ optionId: "allow_always", name: msg().
|
|
298
|
-
{ optionId: "reject_once", name: msg().
|
|
490
|
+
{ optionId: "allow_once", name: msg().acp.core.allowOnce, kind: "allow_once" },
|
|
491
|
+
{ optionId: "allow_always", name: msg().acp.core.allowAlways, kind: "allow_always" },
|
|
492
|
+
{ optionId: "reject_once", name: msg().acp.core.rejectOnce, kind: "reject_once" },
|
|
299
493
|
],
|
|
300
494
|
}, signal);
|
|
301
495
|
}
|