@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.
- package/CHANGELOG.md +69 -0
- package/CHANGELOG.zh-CN.md +50 -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/auth/oauth/token-store.d.ts +2 -0
- package/dist/auth/oauth/token-store.js +7 -2
- package/dist/bundle/ama.cjs +48975 -47637
- package/dist/checkpoints/shadow-git.js +2 -1
- 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/config/auth-file.js +10 -2
- package/dist/config/fs-retry.d.ts +18 -0
- package/dist/config/fs-retry.js +33 -0
- 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/docs/en/sessions.md +1 -1
- package/docs/sessions.md +1 -1
- 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`
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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) =>
|
|
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
|
|
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 (
|
|
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 (
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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?:
|
|
222
|
+
category?: AcpConfigCategory | null;
|
|
186
223
|
type: "select" | (string & {});
|
|
187
224
|
currentValue: string;
|
|
188
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
358
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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>;
|
package/dist/drivers/jsonrpc.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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 =
|
|
163
|
-
?
|
|
164
|
-
:
|
|
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) {
|
package/dist/drivers/turn.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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` 等)。 */
|