@armadra/agent 0.11.0 → 0.12.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 +13 -0
- package/CHANGELOG.zh-CN.md +13 -0
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +2 -2
- package/dist/bundle/ama.cjs +125 -24
- package/dist/cli/compose-agents.js +8 -0
- package/dist/drivers/acp/client.d.ts +54 -5
- package/dist/drivers/acp/client.js +96 -9
- package/dist/drivers/acp/types.d.ts +69 -3
- package/dist/drivers/acp/types.js +30 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.js +12 -4
- package/dist/permissions/auto-safe.d.ts +3 -0
- package/dist/permissions/auto-safe.js +10 -3
- package/dist/permissions/readonly-bash.d.ts +2 -2
- package/dist/permissions/readonly-bash.js +2 -2
- package/dist/tools/presets.d.ts +7 -0
- package/dist/tools/presets.js +10 -1
- package/dist/tools/task-ctl.d.ts +9 -0
- package/dist/tools/task-ctl.js +22 -3
- package/docs/en/guides/permissions.md +10 -10
- package/docs/en/reference/acp.md +7 -3
- package/docs/guides/agents.md +4 -1
- package/docs/guides/permissions.md +10 -10
- package/docs/reference/acp.md +7 -3
- package/package.json +1 -1
|
@@ -14,14 +14,33 @@
|
|
|
14
14
|
* - `elicitation/create` 交给 `onElicitation`;给了它 `initialize` 才声明 `clientCapabilities.elicitation`,
|
|
15
15
|
* 没给时线路与旧版相同(不声明,Agent 发来的请求回 method not found)。`cancel(sessionId)`、连接关闭时
|
|
16
16
|
* 挂起的 elicitation 一律回 `{ action: "cancel" }`;ama 从不替人填表。
|
|
17
|
+
* - `elicitation/complete`(Agent → 客户端通知,URL 型 elicitation 在带外完成)交给 `onElicitationComplete`;给了它
|
|
18
|
+
* `initialize` 声明 `elicitation: { form: {}, url: {} }`(只给 `onElicitation` 时仍是 `{}`,与旧版相同)。同一
|
|
19
|
+
* `elicitationId` 若还挂着(处理器没答),它的 `signal` 随之 abort、答 `{ action: "cancel" }`,宿主据此收掉卡片。
|
|
20
|
+
* 表单字段含字符串多选(`type: "array"`)与带标题的 `oneOf` / `anyOf`,答复 `content` 里多选是字符串数组;
|
|
21
|
+
* {@link multiSelectOptions} / {@link singleSelectOptions} 把两种写法收成统一的 `{ const, title }` 列表。
|
|
22
|
+
* - 会话管理:`listSessions(cwd?, cursor?, signal?)` 发 `session/list`(要 Agent 声明 `sessionCapabilities.list`,见
|
|
23
|
+
* `supportsList`),`listAllSessions` 跟 `nextCursor` 翻页取全;`closeSession(sessionId, signal?)` 发 `session/close`
|
|
24
|
+
* (`supportsClose`),按规范等同先取消:该会话挂起的权限请求答 `cancelled`、elicitation 答 `cancel`,再发请求。
|
|
25
|
+
* 没声明能力的 Agent 会答 -32601,开会话遇 -32000 同样原样抛出({@link isAuthRequiredError})。
|
|
17
26
|
* - `setConfigOption(sessionId, configId, value)` 发 `session/set_config_option`;开会话答的 `configOptions`
|
|
18
27
|
* 原样交回。`initialize` 声明 `clientCapabilities.session.configOptions: {}`(只认 select,不声明 boolean);
|
|
19
|
-
*
|
|
28
|
+
* `auth.terminal` 缺省不声明(ama 作客户端没有可借给 Agent 的交互终端),见下 `authTerminal`。[ACP-D]
|
|
29
|
+
* - 认证:`authenticate(methodId)` 发 `authenticate`(agent 型方法)、`logout()` 发 `logout`(Agent 声明
|
|
30
|
+
* `auth.logout` 才可用,见 `supportsLogout`);terminal 型方法规范禁止经 `authenticate`,已知是 terminal 型时本地
|
|
31
|
+
* 拒绝(-32602),宿主自己在终端里跑 `args` / `env`。构造参数 `authTerminal: true` 才在 `initialize` 声明
|
|
32
|
+
* `clientCapabilities.auth.terminal`(宿主有交互终端可借;Agent 据此才给 terminal 型方法),缺省线路与旧版相同。
|
|
33
|
+
* 开会话遇 -32000(auth_required)用 {@link isAuthRequiredError} 判断,`authMethods` 保留 id / name / description / type
|
|
34
|
+
* / args / env 原样。
|
|
20
35
|
* - 线路两侧开 `$/cancel_request`:本端请求的 `signal` abort 时通知 Agent;Agent 撤回挂起的
|
|
21
36
|
* `session/request_permission` / `elicitation/create` 时处理器的 `signal` abort,回 cancelled / cancel。[ACP-D]
|
|
22
37
|
*/
|
|
23
38
|
import { JsonRpcPeer, RpcError } from "../jsonrpc.js";
|
|
24
|
-
import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./types.js";
|
|
39
|
+
import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, isTerminalAuthMethod, } from "./types.js";
|
|
40
|
+
/** 错误是 ACP 的 auth_required(-32000):开会话或发提示前需要先认证 / 登录。 */
|
|
41
|
+
export function isAuthRequiredError(error) {
|
|
42
|
+
return error instanceof RpcError && error.code === RPC_ERRORS.authRequired;
|
|
43
|
+
}
|
|
25
44
|
/** 开会话参数里的 `mcpServers`:拷一份,缺省空数组。 */
|
|
26
45
|
function mcpServersOf(options) {
|
|
27
46
|
return [...(options?.mcpServers ?? [])];
|
|
@@ -52,14 +71,25 @@ export class AcpClient {
|
|
|
52
71
|
* - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器;
|
|
53
72
|
* - `elicitation`:构造参数 `onElicitation` 接 `elicitation/create`;
|
|
54
73
|
* - `configOptions`:`setConfigOption` 与开会话答的 `configOptions`;
|
|
55
|
-
* - `terminal`:构造参数 `onTerminal` 接 `terminal
|
|
74
|
+
* - `terminal`:构造参数 `onTerminal` 接 `terminal/*`;
|
|
75
|
+
* - `auth`:`authenticate` / `logout`、构造参数 `authTerminal`、`isAuthRequiredError`;
|
|
76
|
+
* - `elicitationComplete`:构造参数 `onElicitationComplete` 接 `elicitation/complete`,表单字段含多选与 `oneOf`。
|
|
56
77
|
*/
|
|
57
|
-
static features = {
|
|
78
|
+
static features = {
|
|
79
|
+
mcpServers: true,
|
|
80
|
+
elicitation: true,
|
|
81
|
+
configOptions: true,
|
|
82
|
+
terminal: true,
|
|
83
|
+
auth: true,
|
|
84
|
+
elicitationComplete: true,
|
|
85
|
+
};
|
|
58
86
|
peer;
|
|
59
87
|
/** sessionId → 挂起权限请求的取消器。 */
|
|
60
88
|
pendingPermissions = new Map();
|
|
61
89
|
/** sessionId(没给就是空串)→ 挂起 elicitation 的取消器。 */
|
|
62
90
|
pendingElicitations = new Map();
|
|
91
|
+
/** elicitationId → 挂起 elicitation 的取消器(`elicitation/complete` 用)。 */
|
|
92
|
+
elicitationsById = new Map();
|
|
63
93
|
initResult;
|
|
64
94
|
constructor(options) {
|
|
65
95
|
this.options = options;
|
|
@@ -71,6 +101,8 @@ export class AcpClient {
|
|
|
71
101
|
onNotification: (method, params) => {
|
|
72
102
|
if (method === ACP_METHODS.sessionUpdate)
|
|
73
103
|
options.onUpdate?.(params);
|
|
104
|
+
else if (method === ACP_METHODS.elicitationComplete)
|
|
105
|
+
this.onElicitationComplete(params);
|
|
74
106
|
},
|
|
75
107
|
onClose: () => {
|
|
76
108
|
for (const set of this.pendingPermissions.values())
|
|
@@ -109,7 +141,12 @@ export class AcpClient {
|
|
|
109
141
|
fs: { readTextFile: false, writeTextFile: false },
|
|
110
142
|
terminal: this.options.onTerminal !== undefined,
|
|
111
143
|
session: { configOptions: {} },
|
|
112
|
-
...(this.options.
|
|
144
|
+
...(this.options.authTerminal === true ? { auth: { terminal: true } } : {}),
|
|
145
|
+
...(this.options.onElicitation !== undefined || this.options.onElicitationComplete
|
|
146
|
+
? {
|
|
147
|
+
elicitation: this.options.onElicitationComplete !== undefined ? { form: {}, url: {} } : {},
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
113
150
|
},
|
|
114
151
|
...(this.options.clientInfo !== undefined ? { clientInfo: this.options.clientInfo } : {}),
|
|
115
152
|
}, signal);
|
|
@@ -130,9 +167,27 @@ export class AcpClient {
|
|
|
130
167
|
supportsClose() {
|
|
131
168
|
return this.agentCapabilities.sessionCapabilities?.close != null;
|
|
132
169
|
}
|
|
170
|
+
/** Agent 声明了 `auth.logout`(存在即支持)。 */
|
|
171
|
+
supportsLogout() {
|
|
172
|
+
return this.agentCapabilities.auth?.logout != null;
|
|
173
|
+
}
|
|
133
174
|
supportsImages() {
|
|
134
175
|
return this.agentCapabilities.promptCapabilities?.image === true;
|
|
135
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* 用 agent 型认证方法登录(`methodId` 取自 `authMethods`)。terminal 型方法不经 `authenticate`:已知是
|
|
179
|
+
* terminal 型时抛 -32602,由宿主在终端里跑该方法的 `args` / `env`。失败的错误码原样抛出。
|
|
180
|
+
*/
|
|
181
|
+
async authenticate(methodId, signal) {
|
|
182
|
+
const known = this.authMethods.find((method) => method.id === methodId);
|
|
183
|
+
if (known !== undefined && isTerminalAuthMethod(known))
|
|
184
|
+
throw new RpcError(RPC_ERRORS.invalidParams, `auth method ${methodId} is a terminal method: run it in a terminal, not through authenticate`);
|
|
185
|
+
return this.peer.request(ACP_METHODS.authenticate, { methodId }, signal);
|
|
186
|
+
}
|
|
187
|
+
/** 登出当前账号;Agent 没声明 `auth.logout`(见 `supportsLogout`)时会答 method not found。 */
|
|
188
|
+
logout(signal) {
|
|
189
|
+
return this.peer.request(ACP_METHODS.logout, {}, signal);
|
|
190
|
+
}
|
|
136
191
|
newSession(cwd, signal, options) {
|
|
137
192
|
return this.peer.request(ACP_METHODS.sessionNew, { cwd, mcpServers: mcpServersOf(options) }, signal);
|
|
138
193
|
}
|
|
@@ -144,14 +199,33 @@ export class AcpClient {
|
|
|
144
199
|
loadSession(sessionId, cwd, signal, options) {
|
|
145
200
|
return this.peer.request(ACP_METHODS.sessionLoad, { sessionId, cwd, mcpServers: mcpServersOf(options) }, signal);
|
|
146
201
|
}
|
|
147
|
-
|
|
202
|
+
/** 一页会话;`cwd` 过滤(绝对路径),`cursor` 取自上一页的 `nextCursor`。 */
|
|
203
|
+
listSessions(cwd, cursor, signal) {
|
|
148
204
|
return this.peer.request(ACP_METHODS.sessionList, {
|
|
149
205
|
...(cwd !== undefined ? { cwd } : {}),
|
|
150
206
|
...(cursor !== undefined ? { cursor } : {}),
|
|
151
|
-
});
|
|
207
|
+
}, signal);
|
|
208
|
+
}
|
|
209
|
+
/** 跟 `nextCursor` 翻页取全(`maxPages` 防对端游标不终止,缺省 100 页)。 */
|
|
210
|
+
async listAllSessions(cwd, signal, maxPages = 100) {
|
|
211
|
+
const sessions = [];
|
|
212
|
+
let cursor;
|
|
213
|
+
for (let page = 0; page < maxPages; page++) {
|
|
214
|
+
const result = await this.listSessions(cwd, cursor, signal);
|
|
215
|
+
sessions.push(...(result.sessions ?? []));
|
|
216
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor === "")
|
|
217
|
+
break;
|
|
218
|
+
cursor = result.nextCursor;
|
|
219
|
+
}
|
|
220
|
+
return sessions;
|
|
152
221
|
}
|
|
153
|
-
|
|
154
|
-
|
|
222
|
+
/** 关闭会话:规范要求 Agent 先取消进行中的工作,所以本端先把该会话挂起的请求收掉。 */
|
|
223
|
+
closeSession(sessionId, signal) {
|
|
224
|
+
for (const controller of this.pendingPermissions.get(sessionId) ?? [])
|
|
225
|
+
controller.abort();
|
|
226
|
+
for (const controller of this.pendingElicitations.get(sessionId) ?? [])
|
|
227
|
+
controller.abort();
|
|
228
|
+
return this.peer.request(ACP_METHODS.sessionClose, { sessionId }, signal);
|
|
155
229
|
}
|
|
156
230
|
prompt(sessionId, prompt, signal) {
|
|
157
231
|
return this.peer.request(ACP_METHODS.sessionPrompt, { sessionId, prompt }, signal);
|
|
@@ -225,6 +299,9 @@ export class AcpClient {
|
|
|
225
299
|
const set = this.pendingElicitations.get(key) ?? new Set();
|
|
226
300
|
set.add(controller);
|
|
227
301
|
this.pendingElicitations.set(key, set);
|
|
302
|
+
const id = typeof request.elicitationId === "string" ? request.elicitationId : undefined;
|
|
303
|
+
if (id !== undefined)
|
|
304
|
+
this.elicitationsById.set(id, controller);
|
|
228
305
|
const signal = AbortSignal.any([controller.signal, connection]);
|
|
229
306
|
try {
|
|
230
307
|
const answer = await Promise.race([
|
|
@@ -243,6 +320,16 @@ export class AcpClient {
|
|
|
243
320
|
}
|
|
244
321
|
finally {
|
|
245
322
|
set.delete(controller);
|
|
323
|
+
if (id !== undefined && this.elicitationsById.get(id) === controller)
|
|
324
|
+
this.elicitationsById.delete(id);
|
|
246
325
|
}
|
|
247
326
|
}
|
|
327
|
+
/** 通知没有答复:参数不对就忽略;还挂着的同 id elicitation 先收掉,再交给宿主。 */
|
|
328
|
+
onElicitationComplete(params) {
|
|
329
|
+
const id = params?.elicitationId;
|
|
330
|
+
if (typeof id !== "string")
|
|
331
|
+
return;
|
|
332
|
+
this.elicitationsById.get(id)?.abort();
|
|
333
|
+
this.options.onElicitationComplete?.(params);
|
|
334
|
+
}
|
|
248
335
|
}
|
|
@@ -14,8 +14,10 @@ export declare const ACP_PROTOCOL_VERSION: 1;
|
|
|
14
14
|
/** 方法名(客户端 → Agent 的请求 / 通知,Agent → 客户端的请求 / 通知)。 */
|
|
15
15
|
export declare const ACP_METHODS: {
|
|
16
16
|
readonly initialize: "initialize";
|
|
17
|
-
/**
|
|
17
|
+
/** 客户端 → Agent:agent 型认证方法经它完成;terminal 型不经它(规范)。 */
|
|
18
18
|
readonly authenticate: "authenticate";
|
|
19
|
+
/** 客户端 → Agent:登出当前账号(Agent 声明 `agentCapabilities.auth.logout` 才可用)。 */
|
|
20
|
+
readonly logout: "logout";
|
|
19
21
|
readonly sessionNew: "session/new";
|
|
20
22
|
readonly sessionLoad: "session/load";
|
|
21
23
|
readonly sessionResume: "session/resume";
|
|
@@ -28,6 +30,8 @@ export declare const ACP_METHODS: {
|
|
|
28
30
|
readonly sessionUpdate: "session/update";
|
|
29
31
|
readonly requestPermission: "session/request_permission";
|
|
30
32
|
readonly elicitationCreate: "elicitation/create";
|
|
33
|
+
/** Agent → 客户端通知:URL 型 elicitation 已在带外完成。 */
|
|
34
|
+
readonly elicitationComplete: "elicitation/complete";
|
|
31
35
|
/** 协议级取消(双向通知,`requestId` 指对端发来的、尚未答复的请求)。 */
|
|
32
36
|
readonly cancelRequest: "$/cancel_request";
|
|
33
37
|
};
|
|
@@ -93,6 +97,7 @@ export interface AcpClientCapabilities {
|
|
|
93
97
|
} | null;
|
|
94
98
|
} | null;
|
|
95
99
|
/** `terminal: true`:客户端能替 Agent 起终端跑 terminal 型认证方法。 */
|
|
100
|
+
/** 能在交互终端里复现 Agent 的启动命令;声明后 Agent 才会给 terminal 型认证方法。 */
|
|
96
101
|
auth?: {
|
|
97
102
|
terminal?: boolean;
|
|
98
103
|
};
|
|
@@ -144,6 +149,19 @@ export type AcpAuthMethod = {
|
|
|
144
149
|
export interface AcpAuthenticateParams {
|
|
145
150
|
methodId: string;
|
|
146
151
|
}
|
|
152
|
+
/** `authenticate` / `logout` 的答复:规范里只有 `_meta`。 */
|
|
153
|
+
export type AcpAuthenticateResult = {
|
|
154
|
+
_meta?: Record<string, unknown> | null;
|
|
155
|
+
};
|
|
156
|
+
export type AcpLogoutResult = {
|
|
157
|
+
_meta?: Record<string, unknown> | null;
|
|
158
|
+
};
|
|
159
|
+
/** terminal 型认证方法(客户端在终端里跑 `<agent 命令> <args…>`,带 `env`)。 */
|
|
160
|
+
export type AcpTerminalAuthMethod = Extract<AcpAuthMethod, {
|
|
161
|
+
type: "terminal";
|
|
162
|
+
}>;
|
|
163
|
+
/** 是 terminal 型认证方法:这类方法不经 `authenticate`。 */
|
|
164
|
+
export declare function isTerminalAuthMethod(method: AcpAuthMethod): method is AcpTerminalAuthMethod;
|
|
147
165
|
export interface AcpInitializeResult {
|
|
148
166
|
protocolVersion: number;
|
|
149
167
|
agentCapabilities?: AcpAgentCapabilities;
|
|
@@ -234,14 +252,23 @@ export interface AcpSetConfigOptionParams {
|
|
|
234
252
|
export interface AcpSetConfigOptionResult {
|
|
235
253
|
configOptions?: AcpSessionConfigOption[] | null;
|
|
236
254
|
}
|
|
237
|
-
/**
|
|
255
|
+
/** 带标题的枚举项(`oneOf` / `anyOf` 里的一项):`const` 是提交的值,`title` 是显示名。 */
|
|
256
|
+
export interface AcpEnumOption {
|
|
257
|
+
const: string;
|
|
258
|
+
title: string;
|
|
259
|
+
}
|
|
260
|
+
/** 表单里的一个字段:扁平的原始类型或字符串多选,规范只允许这几种;未知 `type` 保留原样、不当已知控件渲染。 */
|
|
238
261
|
export type AcpElicitationField = {
|
|
239
262
|
type: "string";
|
|
240
263
|
title?: string;
|
|
241
264
|
description?: string;
|
|
265
|
+
/** 单选:只有取值。 */
|
|
242
266
|
enum?: string[];
|
|
243
267
|
enumNames?: string[];
|
|
268
|
+
/** 单选:带标题的取值(`const` / `title`)。 */
|
|
269
|
+
oneOf?: AcpEnumOption[];
|
|
244
270
|
format?: string;
|
|
271
|
+
pattern?: string;
|
|
245
272
|
minLength?: number;
|
|
246
273
|
maxLength?: number;
|
|
247
274
|
default?: string;
|
|
@@ -257,7 +284,27 @@ export type AcpElicitationField = {
|
|
|
257
284
|
title?: string;
|
|
258
285
|
description?: string;
|
|
259
286
|
default?: boolean;
|
|
287
|
+
} | {
|
|
288
|
+
/** 字符串多选:提交值是 `string[]`。 */
|
|
289
|
+
type: "array";
|
|
290
|
+
title?: string;
|
|
291
|
+
description?: string;
|
|
292
|
+
minItems?: number;
|
|
293
|
+
maxItems?: number;
|
|
294
|
+
/** `{ type: "string", enum }`(只有取值)或 `{ anyOf: [{ const, title }] }`(带标题)。 */
|
|
295
|
+
items: {
|
|
296
|
+
type?: "string";
|
|
297
|
+
enum?: string[];
|
|
298
|
+
anyOf?: AcpEnumOption[];
|
|
299
|
+
};
|
|
300
|
+
default?: string[];
|
|
260
301
|
};
|
|
302
|
+
/** 表单字段的提交值:多选是字符串数组。 */
|
|
303
|
+
export type AcpElicitationValue = string | number | boolean | string[];
|
|
304
|
+
/** 多选字段可选的项:带标题的 `anyOf` 或只有取值的 `enum`(标题取值本身);不是多选字段返回 undefined。 */
|
|
305
|
+
export declare function multiSelectOptions(field: AcpElicitationField): AcpEnumOption[] | undefined;
|
|
306
|
+
/** 单选字符串字段可选的项:`oneOf`(带标题)优先,其次 `enum`(可配 `enumNames`);没有枚举返回 undefined。 */
|
|
307
|
+
export declare function singleSelectOptions(field: AcpElicitationField): AcpEnumOption[] | undefined;
|
|
261
308
|
export interface AcpElicitationSchema {
|
|
262
309
|
type: "object";
|
|
263
310
|
properties: Record<string, AcpElicitationField>;
|
|
@@ -266,6 +313,12 @@ export interface AcpElicitationSchema {
|
|
|
266
313
|
/** Agent 发来的 `elicitation/create` 参数;未知字段保留。 */
|
|
267
314
|
export interface AcpElicitationParams {
|
|
268
315
|
sessionId?: string;
|
|
316
|
+
/** 关联的工具调用(会话范围的 elicitation 可带)。 */
|
|
317
|
+
toolCallId?: string | null;
|
|
318
|
+
/** 不在会话里时关联的请求 id(请求范围)。 */
|
|
319
|
+
requestId?: string | number;
|
|
320
|
+
/** URL 型 elicitation 的 id;之后 Agent 以 `elicitation/complete` 通知它在带外完成。 */
|
|
321
|
+
elicitationId?: string;
|
|
269
322
|
message: string;
|
|
270
323
|
/** `form`(缺省)或 `url`。 */
|
|
271
324
|
mode?: "form" | "url" | (string & {});
|
|
@@ -273,11 +326,16 @@ export interface AcpElicitationParams {
|
|
|
273
326
|
url?: string;
|
|
274
327
|
[key: string]: unknown;
|
|
275
328
|
}
|
|
329
|
+
/** `elicitation/complete` 通知的参数:URL 型 elicitation 已完成。 */
|
|
330
|
+
export interface AcpCompleteElicitationParams {
|
|
331
|
+
elicitationId: string;
|
|
332
|
+
_meta?: Record<string, unknown> | null;
|
|
333
|
+
}
|
|
276
334
|
export type AcpElicitationAction = "accept" | "decline" | "cancel";
|
|
277
335
|
export interface AcpElicitationResult {
|
|
278
336
|
action: AcpElicitationAction;
|
|
279
337
|
/** 只随 `accept`。 */
|
|
280
|
-
content?: Record<string,
|
|
338
|
+
content?: Record<string, AcpElicitationValue>;
|
|
281
339
|
}
|
|
282
340
|
export type AcpResumeSessionParams = AcpLoadSessionParams;
|
|
283
341
|
export type AcpResumeSessionResult = AcpLoadSessionResult;
|
|
@@ -288,8 +346,12 @@ export interface AcpListSessionsParams {
|
|
|
288
346
|
export interface AcpSessionInfo {
|
|
289
347
|
sessionId: string;
|
|
290
348
|
cwd: string;
|
|
349
|
+
/** Agent 报告的额外工作区根(绝对路径);缺省与空数组等价。 */
|
|
350
|
+
additionalDirectories?: string[];
|
|
291
351
|
title?: string | null;
|
|
352
|
+
/** ISO 8601 的最近活动时间。 */
|
|
292
353
|
updatedAt?: string | null;
|
|
354
|
+
_meta?: Record<string, unknown> | null;
|
|
293
355
|
}
|
|
294
356
|
export interface AcpListSessionsResult {
|
|
295
357
|
sessions: AcpSessionInfo[];
|
|
@@ -298,6 +360,10 @@ export interface AcpListSessionsResult {
|
|
|
298
360
|
export interface AcpCloseSessionParams {
|
|
299
361
|
sessionId: string;
|
|
300
362
|
}
|
|
363
|
+
/** `session/close` 的答复:规范里只有 `_meta`。 */
|
|
364
|
+
export type AcpCloseSessionResult = {
|
|
365
|
+
_meta?: Record<string, unknown> | null;
|
|
366
|
+
};
|
|
301
367
|
export interface AcpSetModeParams {
|
|
302
368
|
sessionId: string;
|
|
303
369
|
modeId: string;
|
|
@@ -13,8 +13,10 @@ export const ACP_PROTOCOL_VERSION = 1;
|
|
|
13
13
|
/** 方法名(客户端 → Agent 的请求 / 通知,Agent → 客户端的请求 / 通知)。 */
|
|
14
14
|
export const ACP_METHODS = {
|
|
15
15
|
initialize: "initialize",
|
|
16
|
-
/**
|
|
16
|
+
/** 客户端 → Agent:agent 型认证方法经它完成;terminal 型不经它(规范)。 */
|
|
17
17
|
authenticate: "authenticate",
|
|
18
|
+
/** 客户端 → Agent:登出当前账号(Agent 声明 `agentCapabilities.auth.logout` 才可用)。 */
|
|
19
|
+
logout: "logout",
|
|
18
20
|
sessionNew: "session/new",
|
|
19
21
|
sessionLoad: "session/load",
|
|
20
22
|
sessionResume: "session/resume",
|
|
@@ -27,6 +29,8 @@ export const ACP_METHODS = {
|
|
|
27
29
|
sessionUpdate: "session/update",
|
|
28
30
|
requestPermission: "session/request_permission",
|
|
29
31
|
elicitationCreate: "elicitation/create",
|
|
32
|
+
/** Agent → 客户端通知:URL 型 elicitation 已在带外完成。 */
|
|
33
|
+
elicitationComplete: "elicitation/complete",
|
|
30
34
|
/** 协议级取消(双向通知,`requestId` 指对端发来的、尚未答复的请求)。 */
|
|
31
35
|
cancelRequest: "$/cancel_request",
|
|
32
36
|
};
|
|
@@ -42,5 +46,30 @@ export const RPC_ERRORS = {
|
|
|
42
46
|
/** 请求被 `$/cancel_request` 撤回。 */
|
|
43
47
|
requestCancelled: -32800,
|
|
44
48
|
};
|
|
49
|
+
/** 是 terminal 型认证方法:这类方法不经 `authenticate`。 */
|
|
50
|
+
export function isTerminalAuthMethod(method) {
|
|
51
|
+
return method.type === "terminal";
|
|
52
|
+
}
|
|
53
|
+
/** 多选字段可选的项:带标题的 `anyOf` 或只有取值的 `enum`(标题取值本身);不是多选字段返回 undefined。 */
|
|
54
|
+
export function multiSelectOptions(field) {
|
|
55
|
+
if (field.type !== "array")
|
|
56
|
+
return undefined;
|
|
57
|
+
const items = field.items;
|
|
58
|
+
if (Array.isArray(items?.anyOf))
|
|
59
|
+
return items.anyOf.map((o) => ({ const: o.const, title: o.title }));
|
|
60
|
+
if (Array.isArray(items?.enum))
|
|
61
|
+
return items.enum.map((value) => ({ const: value, title: value }));
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/** 单选字符串字段可选的项:`oneOf`(带标题)优先,其次 `enum`(可配 `enumNames`);没有枚举返回 undefined。 */
|
|
65
|
+
export function singleSelectOptions(field) {
|
|
66
|
+
if (field.type !== "string")
|
|
67
|
+
return undefined;
|
|
68
|
+
if (Array.isArray(field.oneOf))
|
|
69
|
+
return field.oneOf.map((o) => ({ const: o.const, title: o.title }));
|
|
70
|
+
if (Array.isArray(field.enum))
|
|
71
|
+
return field.enum.map((value, i) => ({ const: value, title: field.enumNames?.[i] ?? value }));
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
45
74
|
/** ama 自己的 `_meta` 命名空间(客户端不得假设其含义)。 */
|
|
46
75
|
export const ACP_META_KEY = "ama";
|
|
@@ -32,6 +32,11 @@ import { StatusArea } from "./status-area.js";
|
|
|
32
32
|
import type { StatusBar } from "./status-bar.js";
|
|
33
33
|
import { type ProgramStatusOptions } from "./program-status-ui.js";
|
|
34
34
|
import { ToolTracker } from "./tool-view.js";
|
|
35
|
+
/** 底部提示行的清除计时器(测试注入手动时钟,帧黄金不再依赖墙钟)。 */
|
|
36
|
+
export interface HintTimers {
|
|
37
|
+
set(fn: () => void, ms: number): unknown;
|
|
38
|
+
clear(handle: unknown): void;
|
|
39
|
+
}
|
|
35
40
|
export interface InteractiveModeOptions {
|
|
36
41
|
/** 缺省 `ProcessTerminal`(测试注入 MemoryTerminal)。 */
|
|
37
42
|
terminal?: Terminal;
|
|
@@ -49,6 +54,8 @@ export interface InteractiveModeOptions {
|
|
|
49
54
|
onReady?(handle: InteractiveHandle): void;
|
|
50
55
|
/** 剪贴板读取的注入(测试;缺省调系统命令)。 */
|
|
51
56
|
clipboard?: ClipboardDeps;
|
|
57
|
+
/** 提示行清除计时器的注入(测试;缺省真实 `setTimeout`)。 */
|
|
58
|
+
hintTimers?: HintTimers;
|
|
52
59
|
/** 终端程序状态 OSC 7501(缺省只在真实终端启用;测试注入终端时给了才启用)。 */
|
|
53
60
|
programStatus?: ProgramStatusOptions;
|
|
54
61
|
}
|
|
@@ -56,6 +56,14 @@ import { loadKeys, processTerminal } from "./terminal-setup.js";
|
|
|
56
56
|
import { ToolTracker } from "./tool-view.js";
|
|
57
57
|
import { backgroundTasks, blockingTasks, keyLabel, runningBackground } from "./task-background.js";
|
|
58
58
|
const HINT_MS = 2500;
|
|
59
|
+
const realHintTimers = {
|
|
60
|
+
set: (fn, ms) => {
|
|
61
|
+
const handle = setTimeout(fn, ms);
|
|
62
|
+
handle.unref?.();
|
|
63
|
+
return handle;
|
|
64
|
+
},
|
|
65
|
+
clear: (handle) => clearTimeout(handle),
|
|
66
|
+
};
|
|
59
67
|
export function runInteractiveMode(runtime, context, options = {}) {
|
|
60
68
|
const terminal = options.terminal ?? processTerminal();
|
|
61
69
|
const ui = runtime.config.ui ?? {};
|
|
@@ -142,6 +150,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
142
150
|
tui.addChild(area.quota);
|
|
143
151
|
// ---- 小工具 ---------------------------------------------------------------
|
|
144
152
|
let finished = false;
|
|
153
|
+
const hintTimers = options.hintTimers ?? realHintTimers;
|
|
145
154
|
let hintTimer;
|
|
146
155
|
let resolveExit = () => undefined;
|
|
147
156
|
const render = () => tui.requestRender();
|
|
@@ -152,14 +161,13 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
152
161
|
const showHint = (text, ms = HINT_MS) => {
|
|
153
162
|
hint.setText(text === "" ? "" : theme.fg("dim", text));
|
|
154
163
|
if (hintTimer !== undefined)
|
|
155
|
-
|
|
164
|
+
hintTimers.clear(hintTimer);
|
|
156
165
|
hintTimer = undefined;
|
|
157
166
|
if (text !== "") {
|
|
158
|
-
hintTimer =
|
|
167
|
+
hintTimer = hintTimers.set(() => {
|
|
159
168
|
hint.setText("");
|
|
160
169
|
render();
|
|
161
170
|
}, ms);
|
|
162
|
-
hintTimer.unref?.();
|
|
163
171
|
}
|
|
164
172
|
render();
|
|
165
173
|
};
|
|
@@ -423,7 +431,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
|
|
|
423
431
|
finished = true;
|
|
424
432
|
logoAnimation?.finish();
|
|
425
433
|
if (hintTimer !== undefined)
|
|
426
|
-
|
|
434
|
+
hintTimers.clear(hintTimer);
|
|
427
435
|
loader.stop();
|
|
428
436
|
area.dispose();
|
|
429
437
|
osc7501.exit();
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - 写入目标:重定向(`>`、`>>`、`&>`)、tee、cp / mv / mkdir / touch / ln / chmod 等的目标在项目外或受保护;
|
|
9
9
|
* - 参数里出现机密路径(`cat .env`、`< ~/.ssh/id_rsa`、`--env-file=.env`)。
|
|
10
10
|
*
|
|
11
|
+
* `env` / `printenv` 不在安全名单里:子进程继承整个进程环境(供应商 API key 等),打印出来就会进
|
|
12
|
+
* tool_result 与会话文件。段里只剩环境赋值或包装命令(`env`、`env FOO=1`)同样不放行。
|
|
13
|
+
*
|
|
11
14
|
* 静态判定(全部满足即放行):每一段都命中安全名单({@link AUTO_SAFE_COMMANDS} + 用户追加),整条命令
|
|
12
15
|
* 没有命令替换、参数没有变量展开 `$X`、没有可能匹配点文件的通配、没有超深嵌套。
|
|
13
16
|
*
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - 写入目标:重定向(`>`、`>>`、`&>`)、tee、cp / mv / mkdir / touch / ln / chmod 等的目标在项目外或受保护;
|
|
9
9
|
* - 参数里出现机密路径(`cat .env`、`< ~/.ssh/id_rsa`、`--env-file=.env`)。
|
|
10
10
|
*
|
|
11
|
+
* `env` / `printenv` 不在安全名单里:子进程继承整个进程环境(供应商 API key 等),打印出来就会进
|
|
12
|
+
* tool_result 与会话文件。段里只剩环境赋值或包装命令(`env`、`env FOO=1`)同样不放行。
|
|
13
|
+
*
|
|
11
14
|
* 静态判定(全部满足即放行):每一段都命中安全名单({@link AUTO_SAFE_COMMANDS} + 用户追加),整条命令
|
|
12
15
|
* 没有命令替换、参数没有变量展开 `$X`、没有可能匹配点文件的通配、没有超深嵌套。
|
|
13
16
|
*
|
|
@@ -38,7 +41,6 @@ export const AUTO_SAFE_COMMANDS = [
|
|
|
38
41
|
"echo",
|
|
39
42
|
"printf",
|
|
40
43
|
"which",
|
|
41
|
-
"printenv",
|
|
42
44
|
"true",
|
|
43
45
|
"false",
|
|
44
46
|
"cut",
|
|
@@ -451,6 +453,7 @@ function cdTarget(argv, cwd) {
|
|
|
451
453
|
function parseSegment(segment) {
|
|
452
454
|
const redirects = extractRedirects(segment);
|
|
453
455
|
return {
|
|
456
|
+
first: shellWords(redirects.rest.replace(/^[({\s]+/, ""))[0],
|
|
454
457
|
argv: commandWords(redirects.rest),
|
|
455
458
|
withSudo: commandWords(redirects.rest, true),
|
|
456
459
|
redirects,
|
|
@@ -478,8 +481,12 @@ function ruleAsk(info, options) {
|
|
|
478
481
|
}
|
|
479
482
|
function safeSegment(info, extra) {
|
|
480
483
|
const argv = info.withSudo;
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
// 只剩环境赋值或包装命令:`env` / `env FOO=1` 会打印整个环境(含密钥),不放行。
|
|
485
|
+
if (argv.length === 0) {
|
|
486
|
+
return info.first === undefined
|
|
487
|
+
? "empty command"
|
|
488
|
+
: `${base(info.first)} is not in the auto safe list`;
|
|
489
|
+
}
|
|
483
490
|
for (const word of argv) {
|
|
484
491
|
if (word.includes("$"))
|
|
485
492
|
return `variable expansion in ${word}`;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 判定 = auto 的静态判定(`analyzeBashForAuto`:分词、嵌套展开、规则层的网络 / 删除 / 写入目标 /
|
|
5
5
|
* 机密路径、命令替换、变量展开、点文件通配)**全部通过**,再加四条更窄的限制:
|
|
6
|
-
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 /
|
|
7
|
-
*
|
|
6
|
+
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 / 构建运行器;`env` /
|
|
7
|
+
* `printenv` 这类会把环境变量里的密钥打进上下文的命令两边都没有);
|
|
8
8
|
* - 没有输出重定向(`/dev/null` 等设备除外);
|
|
9
9
|
* - 没有嵌套 shell(`sh -c`、`eval`、`xargs`、`find -exec`)与进程替换;
|
|
10
10
|
* - 段首没有环境赋值或包装命令(`GIT_EXTERNAL_DIFF=… git diff`、`env …`、`nohup …`)。
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 判定 = auto 的静态判定(`analyzeBashForAuto`:分词、嵌套展开、规则层的网络 / 删除 / 写入目标 /
|
|
5
5
|
* 机密路径、命令替换、变量展开、点文件通配)**全部通过**,再加四条更窄的限制:
|
|
6
|
-
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 /
|
|
7
|
-
*
|
|
6
|
+
* - 每段命令都命中 {@link READONLY_BASH}(比 auto 安全名单窄:去掉测试 / 构建运行器;`env` /
|
|
7
|
+
* `printenv` 这类会把环境变量里的密钥打进上下文的命令两边都没有);
|
|
8
8
|
* - 没有输出重定向(`/dev/null` 等设备除外);
|
|
9
9
|
* - 没有嵌套 shell(`sh -c`、`eval`、`xargs`、`find -exec`)与进程替换;
|
|
10
10
|
* - 段首没有环境赋值或包装命令(`GIT_EXTERNAL_DIFF=… git diff`、`env …`、`nohup …`)。
|
package/dist/tools/presets.d.ts
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* `codemode.mode` 显式配置覆盖(缺省配置不写这个键,以后调整映射能惠及老用户)。
|
|
24
24
|
* - `coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限活动集(read + 宿主工具),见
|
|
25
25
|
* `codemodeCallableFilter`:协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
|
|
26
|
+
* - (#242)`coordinator` 预设不阻塞:`task` 缺省后台(无视 `subagents.background`,调用参数 `background`
|
|
27
|
+
* 与类型定义的 `background:` 仍优先)、`task_ctl wait` 的超时封顶 5 秒;其它预设不变。
|
|
26
28
|
* - codemode 工具由 B10 经组装根的 `toolFactories` 注册(生效模式为 off 时不注册);不可用
|
|
27
29
|
* (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode-only 预设**回退到 default 并
|
|
28
30
|
* warning**(不报错:零配置用户不应因此起不来),显式 `on` 同样忽略;跟随预设得到的 `on` 静默回退。
|
|
@@ -56,6 +58,11 @@ export declare function effectiveCodemodeMode(config: Pick<AmaConfig, "tools" |
|
|
|
56
58
|
* 不限(`undefined`;`on` 模式下脚本还能调没直接暴露的 ls / task / todo 等)。
|
|
57
59
|
*/
|
|
58
60
|
export declare function codemodeCallableFilter(config: Pick<AmaConfig, "tools">): "active" | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* [#242] 是否 `coordinator` 预设:协调者只分派、不阻塞——`task` 缺省后台、`task_ctl wait` 超时封顶
|
|
63
|
+
* (组装根据此绑定,见 cli/compose-agents.ts)。其它预设不受影响。
|
|
64
|
+
*/
|
|
65
|
+
export declare function isCoordinatorPreset(config: Pick<AmaConfig, "tools">): boolean;
|
|
59
66
|
/**
|
|
60
67
|
* `tools.default` 微调:不带前缀的名字整组替换 `base`,再依次应用 `+name` / `-name`。
|
|
61
68
|
* 返回去重后的名字(保持首次出现顺序)。
|
package/dist/tools/presets.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* `codemode.mode` 显式配置覆盖(缺省配置不写这个键,以后调整映射能惠及老用户)。
|
|
24
24
|
* - `coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限活动集(read + 宿主工具),见
|
|
25
25
|
* `codemodeCallableFilter`:协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
|
|
26
|
+
* - (#242)`coordinator` 预设不阻塞:`task` 缺省后台(无视 `subagents.background`,调用参数 `background`
|
|
27
|
+
* 与类型定义的 `background:` 仍优先)、`task_ctl wait` 的超时封顶 5 秒;其它预设不变。
|
|
26
28
|
* - codemode 工具由 B10 经组装根的 `toolFactories` 注册(生效模式为 off 时不注册);不可用
|
|
27
29
|
* (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode-only 预设**回退到 default 并
|
|
28
30
|
* warning**(不报错:零配置用户不应因此起不来),显式 `on` 同样忽略;跟随预设得到的 `on` 静默回退。
|
|
@@ -72,7 +74,14 @@ export function effectiveCodemodeMode(config, strict) {
|
|
|
72
74
|
* 不限(`undefined`;`on` 模式下脚本还能调没直接暴露的 ls / task / todo 等)。
|
|
73
75
|
*/
|
|
74
76
|
export function codemodeCallableFilter(config) {
|
|
75
|
-
return
|
|
77
|
+
return isCoordinatorPreset(config) ? "active" : undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* [#242] 是否 `coordinator` 预设:协调者只分派、不阻塞——`task` 缺省后台、`task_ctl wait` 超时封顶
|
|
81
|
+
* (组装根据此绑定,见 cli/compose-agents.ts)。其它预设不受影响。
|
|
82
|
+
*/
|
|
83
|
+
export function isCoordinatorPreset(config) {
|
|
84
|
+
return canonicalPreset(config.tools?.preset) === "coordinator";
|
|
76
85
|
}
|
|
77
86
|
/**
|
|
78
87
|
* `tools.default` 微调:不带前缀的名字整组替换 `base`,再依次应用 `+name` / `-name`。
|
package/dist/tools/task-ctl.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* 与父一致,运行时拒绝;
|
|
8
8
|
* - `wait` / `output` 是轮询类调用(`annotations.pollable`,重复调用检测豁免);
|
|
9
9
|
* - `send` = 续聊放后台:等价于 `task{taskId, prompt, background:true}`,完成后同样收到通知。
|
|
10
|
+
* - [#242] `coordinator` 预设下 `wait` 的超时封顶 `COORDINATOR_WAIT_MAX_MS`(协调者不阻塞,见
|
|
11
|
+
* `bindTaskCtlWaitCap`);其它预设不变。
|
|
10
12
|
* - [W7-B1] `wait` 被转后台(`background()`,TUI `Ctrl+B` / RPC `background_task`)打断时立即返回固定
|
|
11
13
|
* 文案,要求模型不要再等。
|
|
12
14
|
*/
|
|
@@ -14,6 +16,13 @@ import type { ToolDefinition } from "./types.js";
|
|
|
14
16
|
export declare const TASK_CTL_TOOL = "task_ctl";
|
|
15
17
|
export declare const DEFAULT_WAIT_MS = 30000;
|
|
16
18
|
export declare const MAX_WAIT_MS = 600000;
|
|
19
|
+
/** [#242] `coordinator` 预设下 `wait` 的超时上限(毫秒)。 */
|
|
20
|
+
export declare const COORDINATOR_WAIT_MAX_MS = 5000;
|
|
21
|
+
/**
|
|
22
|
+
* [#242] 组装根把 `wait` 的超时上限绑到 `task_ctl` 工具(`coordinator` 预设);没绑定按 `MAX_WAIT_MS`。
|
|
23
|
+
* 工具描述与参数 schema 不变(其它预设逐字节不变);被封顶时在超时回复里说明。
|
|
24
|
+
*/
|
|
25
|
+
export declare function bindTaskCtlWaitCap(tool: ToolDefinition | undefined, capMs: number): void;
|
|
17
26
|
export interface TaskCtlInput {
|
|
18
27
|
action: "list" | "wait" | "stop" | "output" | "send";
|
|
19
28
|
taskId?: string;
|