@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 CHANGED
@@ -5,6 +5,19 @@ English · [简体中文](CHANGELOG.zh-CN.md)
5
5
  > This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
6
6
  > [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
7
7
 
8
+ ## 0.12.0 (2026-11-02)
9
+
10
+ ### Tools and permissions
11
+
12
+ - **The `coordinator` preset never blocks**: when `task` / `task_ctl` are exposed, `task` defaults to background (regardless of `-p` and `subagents.background`; an explicit `background` argument and an agent definition's `background:` still win) and `task_ctl wait` is capped at 5 s (the default and larger `timeoutMs` included); the timed-out reply says the wait was capped and that a `<task-notification>` follows. Other presets, tool descriptions and schemas are byte-identical (#242).
13
+ - **`env` / `printenv` are no longer auto-safe**: the bash subprocess inherits the whole process environment (provider API keys included), so in Auto mode a bare `env`, `printenv`, `env FOO=1` or `env | grep …` now goes to the classifier / approval instead of being allowed statically; `env FOO=1 cmd` is still judged as `cmd`. The read-only list for Plan already excluded them, so both lists now agree (#240).
14
+
15
+ ### ACP client
16
+
17
+ - **Authentication**: `AcpClient.authenticate(methodId)` and `logout()` (`supportsLogout()`), `isAuthRequiredError` for -32000, and the `authTerminal` option that declares `clientCapabilities.auth.terminal`. `authMethods` keeps id, name, description, type and, for terminal methods, `args` / `env`; a known terminal method is refused locally by `authenticate` (-32602) as the spec requires. `isTerminalAuthMethod` and the types `AcpAuthenticateResult`, `AcpLogoutResult`, `AcpTerminalAuthMethod` are exported (#245).
18
+ - **`elicitation/complete` and richer forms**: `onElicitationComplete` handles the notification (declaring `elicitation: { form: {}, url: {} }`) and dismisses a still-pending request with the same `elicitationId`; form fields gain `array` multi-select and titled `oneOf` / `anyOf`, `content` values may be string arrays, and `multiSelectOptions` / `singleSelectOptions` normalize the option lists (#245).
19
+ - **Sessions**: `listSessions` takes a `signal`, `listAllSessions` follows `nextCursor`, `closeSession` takes a `signal` and first answers the session's pending permission and elicitation requests with cancel; `AcpSessionInfo` gains `additionalDirectories` and `_meta`. `AcpClient.features` gains `auth` and `elicitationComplete`. Default wire output is unchanged unless the new options are used (#245).
20
+
8
21
  ## 0.11.0 (2026-10-30)
9
22
 
10
23
  ### Tools and permissions
@@ -4,6 +4,19 @@
4
4
 
5
5
  > 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
6
6
 
7
+ ## 0.12.0(2026-11-02)
8
+
9
+ ### 工具与权限
10
+
11
+ - **`coordinator` 预设不阻塞**:暴露 `task` / `task_ctl` 时,`task` 缺省后台(不受 `-p` 与 `subagents.background` 影响,调用参数 `background` 与类型定义的 `background:` 仍优先),`task_ctl wait` 的超时封顶 5 秒(含缺省与更大的 `timeoutMs`),超时回复说明被封顶、完成后会有 `<task-notification>`。其它预设、工具描述与 schema 逐字节不变(#242)。
12
+ - **`env` / `printenv` 不再免审批**:bash 子进程继承整个进程环境(含供应商 API key),Auto 模式下裸 `env`、`printenv`、`env FOO=1`、`env | grep …` 现在交给分类器 / 审批,不再静态放行;`env FOO=1 cmd` 仍按 `cmd` 判断。Plan 的只读名单本来就没有它们,两份名单现在一致(#240)。
13
+
14
+ ### ACP 客户端
15
+
16
+ - **认证**:`AcpClient.authenticate(methodId)` 与 `logout()`(`supportsLogout()`)、判断 -32000 的 `isAuthRequiredError`,以及声明 `clientCapabilities.auth.terminal` 的构造参数 `authTerminal`。`authMethods` 保留 id、name、description、type,terminal 型还有 `args` / `env`;已知是 terminal 型的方法按规范由 `authenticate` 本地拒绝(-32602)。新增导出 `isTerminalAuthMethod` 与类型 `AcpAuthenticateResult`、`AcpLogoutResult`、`AcpTerminalAuthMethod`(#245)。
17
+ - **`elicitation/complete` 与更丰富的表单**:`onElicitationComplete` 接这条通知(声明 `elicitation: { form: {}, url: {} }`),并收掉同 `elicitationId` 还挂着的请求;表单字段新增 `array` 多选与带标题的 `oneOf` / `anyOf`,`content` 的值可以是字符串数组,`multiSelectOptions` / `singleSelectOptions` 把几种写法收成统一的选项列表(#245)。
18
+ - **会话**:`listSessions` 可传 `signal`,新增跟 `nextCursor` 翻页的 `listAllSessions`;`closeSession` 可传 `signal`,并先把该会话挂起的权限与 elicitation 请求答成取消;`AcpSessionInfo` 补 `additionalDirectories`、`_meta`。`AcpClient.features` 增加 `auth` 与 `elicitationComplete`。不用新选项时默认线路输出不变(#245)。
19
+
7
20
  ## 0.11.0(2026-10-30)
8
21
 
9
22
  ### 工具与权限
package/dist/acp.d.ts CHANGED
@@ -8,10 +8,10 @@
8
8
  */
9
9
  export type * from "./drivers/types.js";
10
10
  export type * from "./drivers/acp/types.js";
11
- export { ACP_META_KEY, ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./drivers/acp/types.js";
11
+ export { ACP_META_KEY, isTerminalAuthMethod, multiSelectOptions, singleSelectOptions, ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./drivers/acp/types.js";
12
12
  export { WRITE_CHUNK_BYTES, createLineReader, writeChunked, type LineReader, } from "./modes/rpc/jsonl.js";
13
13
  export { JsonRpcPeer, RpcError, type IncomingRequestContext, type JsonRpcPeerOptions, type RpcId, } from "./drivers/jsonrpc.js";
14
- export { AcpClient, unattendedOutcome, type AcpClientHandlers, type AcpClientOptions, } from "./drivers/acp/client.js";
14
+ export { AcpClient, isAuthRequiredError, unattendedOutcome, type AcpClientHandlers, type AcpClientOptions, } from "./drivers/acp/client.js";
15
15
  export { AcpDriver } from "./drivers/acp/driver.js";
16
16
  export { runFakeAcpAgent, type FakeAcpAgentOptions } from "./drivers/acp/testing/fake-agent.js";
17
17
  /** 假 ACP Agent 的可执行入口(`node <path> [--minimal] [--config-options] [--config-only] [--auth-required]`);只在已编译的包里存在。 */
package/dist/acp.js CHANGED
@@ -7,10 +7,10 @@
7
7
  * - 假 ACP Agent(进程内 `runFakeAcpAgent`,或 `fakeAcpAgentPath()` 起子进程),用于黄金记录。
8
8
  */
9
9
  import { fileURLToPath } from "node:url";
10
- export { ACP_META_KEY, ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./drivers/acp/types.js";
10
+ export { ACP_META_KEY, isTerminalAuthMethod, multiSelectOptions, singleSelectOptions, ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./drivers/acp/types.js";
11
11
  export { WRITE_CHUNK_BYTES, createLineReader, writeChunked, } from "./modes/rpc/jsonl.js";
12
12
  export { JsonRpcPeer, RpcError, } from "./drivers/jsonrpc.js";
13
- export { AcpClient, unattendedOutcome, } from "./drivers/acp/client.js";
13
+ export { AcpClient, isAuthRequiredError, unattendedOutcome, } from "./drivers/acp/client.js";
14
14
  export { AcpDriver } from "./drivers/acp/driver.js";
15
15
  export { runFakeAcpAgent } from "./drivers/acp/testing/fake-agent.js";
16
16
  /** 假 ACP Agent 的可执行入口(`node <path> [--minimal] [--config-options] [--config-only] [--auth-required]`);只在已编译的包里存在。 */
@@ -41,7 +41,7 @@ var AMA_VERSION;
41
41
  var init_version = __esm({
42
42
  "src/version.ts"() {
43
43
  "use strict";
44
- AMA_VERSION = true ? "0.11.0" : readPackageVersion();
44
+ AMA_VERSION = true ? "0.12.0" : readPackageVersion();
45
45
  }
46
46
  });
47
47
 
@@ -15493,6 +15493,9 @@ var init_jsonl = __esm({
15493
15493
  });
15494
15494
 
15495
15495
  // src/drivers/acp/types.ts
15496
+ function isTerminalAuthMethod(method) {
15497
+ return method.type === "terminal";
15498
+ }
15496
15499
  var ACP_PROTOCOL_VERSION, ACP_METHODS, RPC_ERRORS, ACP_META_KEY;
15497
15500
  var init_types7 = __esm({
15498
15501
  "src/drivers/acp/types.ts"() {
@@ -15500,8 +15503,10 @@ var init_types7 = __esm({
15500
15503
  ACP_PROTOCOL_VERSION = 1;
15501
15504
  ACP_METHODS = {
15502
15505
  initialize: "initialize",
15503
- /** ama 不实现(只给 terminal 型认证方法,规范要求这类方法不经 authenticate)。 */
15506
+ /** 客户端 → Agent:agent 型认证方法经它完成;terminal 型不经它(规范)。 */
15504
15507
  authenticate: "authenticate",
15508
+ /** 客户端 → Agent:登出当前账号(Agent 声明 `agentCapabilities.auth.logout` 才可用)。 */
15509
+ logout: "logout",
15505
15510
  sessionNew: "session/new",
15506
15511
  sessionLoad: "session/load",
15507
15512
  sessionResume: "session/resume",
@@ -15514,6 +15519,8 @@ var init_types7 = __esm({
15514
15519
  sessionUpdate: "session/update",
15515
15520
  requestPermission: "session/request_permission",
15516
15521
  elicitationCreate: "elicitation/create",
15522
+ /** Agent → 客户端通知:URL 型 elicitation 已在带外完成。 */
15523
+ elicitationComplete: "elicitation/complete",
15517
15524
  /** 协议级取消(双向通知,`requestId` 指对端发来的、尚未答复的请求)。 */
15518
15525
  cancelRequest: "$/cancel_request"
15519
15526
  };
@@ -19372,6 +19379,9 @@ var init_task_control = __esm({
19372
19379
  });
19373
19380
 
19374
19381
  // src/tools/task-ctl.ts
19382
+ function bindTaskCtlWaitCap(tool, capMs) {
19383
+ if (tool !== void 0) waitCaps.set(tool, capMs);
19384
+ }
19375
19385
  function fail4(message) {
19376
19386
  return { content: message, isError: true };
19377
19387
  }
@@ -19398,7 +19408,7 @@ async function send(input2, ctx) {
19398
19408
  return { content: result.text, isError: result.isError };
19399
19409
  }
19400
19410
  function createTaskCtlTool() {
19401
- return {
19411
+ const tool = {
19402
19412
  name: TASK_CTL_TOOL,
19403
19413
  label: "TaskCtl",
19404
19414
  description: "Manage sub-agent tasks started by task: list them, wait for one, stop it, read its output, or send it a follow-up message (runs in background).",
@@ -19449,14 +19459,17 @@ ${body}` };
19449
19459
  };
19450
19460
  }
19451
19461
  case "wait": {
19452
- const timeout = Math.min(Math.max(input2.timeoutMs ?? DEFAULT_WAIT_MS, 0), MAX_WAIT_MS);
19462
+ const cap = Math.min(waitCaps.get(tool) ?? MAX_WAIT_MS, MAX_WAIT_MS);
19463
+ const wanted = Math.min(Math.max(input2.timeoutMs ?? DEFAULT_WAIT_MS, 0), MAX_WAIT_MS);
19464
+ const timeout = Math.min(wanted, cap);
19453
19465
  const signal = control.detachSignal(taskId);
19454
19466
  const result = await control.wait(taskId, timeout, { signal });
19455
19467
  if (result === void 0 && signal.aborted) return { content: waitDetachedText(taskId) };
19456
19468
  if (result === void 0) {
19457
19469
  const turns = control.get(taskId)?.turns ?? 0;
19470
+ const capped2 = wanted > timeout ? ` Waits are capped at ${Math.round(timeout / 1e3)}s here; do not block on it: keep working, a <task-notification> follows when it finishes.` : "";
19458
19471
  return {
19459
- content: `Task ${taskId} is still running (${turns} turns so far). Wait again or stop it.`
19472
+ content: `Task ${taskId} is still running (${turns} turns so far). Wait again or stop it.${capped2}`
19460
19473
  };
19461
19474
  }
19462
19475
  return {
@@ -19470,8 +19483,9 @@ ${result.text}`,
19470
19483
  }
19471
19484
  }
19472
19485
  };
19486
+ return tool;
19473
19487
  }
19474
- var TASK_CTL_TOOL, DEFAULT_WAIT_MS, MAX_WAIT_MS;
19488
+ var TASK_CTL_TOOL, DEFAULT_WAIT_MS, MAX_WAIT_MS, COORDINATOR_WAIT_MAX_MS, waitCaps;
19475
19489
  var init_task_ctl = __esm({
19476
19490
  "src/tools/task-ctl.ts"() {
19477
19491
  "use strict";
@@ -19481,6 +19495,8 @@ var init_task_ctl = __esm({
19481
19495
  TASK_CTL_TOOL = "task_ctl";
19482
19496
  DEFAULT_WAIT_MS = 3e4;
19483
19497
  MAX_WAIT_MS = 6e5;
19498
+ COORDINATOR_WAIT_MAX_MS = 5e3;
19499
+ waitCaps = /* @__PURE__ */ new WeakMap();
19484
19500
  }
19485
19501
  });
19486
19502
 
@@ -20364,7 +20380,10 @@ function effectiveCodemodeMode(config, strict) {
20364
20380
  return resolveCodemodeMode(config, strict).mode;
20365
20381
  }
20366
20382
  function codemodeCallableFilter(config) {
20367
- return canonicalPreset(config.tools?.preset) === "coordinator" ? "active" : void 0;
20383
+ return isCoordinatorPreset(config) ? "active" : void 0;
20384
+ }
20385
+ function isCoordinatorPreset(config) {
20386
+ return canonicalPreset(config.tools?.preset) === "coordinator";
20368
20387
  }
20369
20388
  function applyToolAdjustments(base4, adjustments) {
20370
20389
  if (adjustments === void 0 || adjustments.length === 0) return [...base4];
@@ -25563,6 +25582,7 @@ function cdTarget(argv, cwd) {
25563
25582
  function parseSegment(segment) {
25564
25583
  const redirects = extractRedirects(segment);
25565
25584
  return {
25585
+ first: shellWords(redirects.rest.replace(/^[({\s]+/, ""))[0],
25566
25586
  argv: commandWords(redirects.rest),
25567
25587
  withSudo: commandWords(redirects.rest, true),
25568
25588
  redirects
@@ -25586,7 +25606,9 @@ function ruleAsk(info2, options) {
25586
25606
  }
25587
25607
  function safeSegment(info2, extra) {
25588
25608
  const argv = info2.withSudo;
25589
- if (argv.length === 0) return void 0;
25609
+ if (argv.length === 0) {
25610
+ return info2.first === void 0 ? "empty command" : `${base2(info2.first)} is not in the auto safe list`;
25611
+ }
25590
25612
  for (const word of argv) {
25591
25613
  if (word.includes("$")) return `variable expansion in ${word}`;
25592
25614
  if (GLOB.test(word) && /(^|\/)\.[^./]/.test(word)) return `glob over dotfiles (${word})`;
@@ -25663,7 +25685,6 @@ var init_auto_safe = __esm({
25663
25685
  "echo",
25664
25686
  "printf",
25665
25687
  "which",
25666
- "printenv",
25667
25688
  "true",
25668
25689
  "false",
25669
25690
  "cut",
@@ -35016,6 +35037,7 @@ var init_client = __esm({
35016
35037
  onNotification: (method, params) => {
35017
35038
  if (method === ACP_METHODS.sessionUpdate)
35018
35039
  options.onUpdate?.(params);
35040
+ else if (method === ACP_METHODS.elicitationComplete) this.onElicitationComplete(params);
35019
35041
  },
35020
35042
  onClose: () => {
35021
35043
  for (const set of this.pendingPermissions.values()) for (const c of set) c.abort();
@@ -35031,14 +35053,25 @@ var init_client = __esm({
35031
35053
  * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器;
35032
35054
  * - `elicitation`:构造参数 `onElicitation` 接 `elicitation/create`;
35033
35055
  * - `configOptions`:`setConfigOption` 与开会话答的 `configOptions`;
35034
- * - `terminal`:构造参数 `onTerminal` 接 `terminal/*`。
35056
+ * - `terminal`:构造参数 `onTerminal` 接 `terminal/*`;
35057
+ * - `auth`:`authenticate` / `logout`、构造参数 `authTerminal`、`isAuthRequiredError`;
35058
+ * - `elicitationComplete`:构造参数 `onElicitationComplete` 接 `elicitation/complete`,表单字段含多选与 `oneOf`。
35035
35059
  */
35036
- static features = { mcpServers: true, elicitation: true, configOptions: true, terminal: true };
35060
+ static features = {
35061
+ mcpServers: true,
35062
+ elicitation: true,
35063
+ configOptions: true,
35064
+ terminal: true,
35065
+ auth: true,
35066
+ elicitationComplete: true
35067
+ };
35037
35068
  peer;
35038
35069
  /** sessionId → 挂起权限请求的取消器。 */
35039
35070
  pendingPermissions = /* @__PURE__ */ new Map();
35040
35071
  /** sessionId(没给就是空串)→ 挂起 elicitation 的取消器。 */
35041
35072
  pendingElicitations = /* @__PURE__ */ new Map();
35073
+ /** elicitationId → 挂起 elicitation 的取消器(`elicitation/complete` 用)。 */
35074
+ elicitationsById = /* @__PURE__ */ new Map();
35042
35075
  initResult;
35043
35076
  get closed() {
35044
35077
  return this.peer.closed;
@@ -35065,7 +35098,10 @@ var init_client = __esm({
35065
35098
  fs: { readTextFile: false, writeTextFile: false },
35066
35099
  terminal: this.options.onTerminal !== void 0,
35067
35100
  session: { configOptions: {} },
35068
- ...this.options.onElicitation !== void 0 ? { elicitation: {} } : {}
35101
+ ...this.options.authTerminal === true ? { auth: { terminal: true } } : {},
35102
+ ...this.options.onElicitation !== void 0 || this.options.onElicitationComplete ? {
35103
+ elicitation: this.options.onElicitationComplete !== void 0 ? { form: {}, url: {} } : {}
35104
+ } : {}
35069
35105
  },
35070
35106
  ...this.options.clientInfo !== void 0 ? { clientInfo: this.options.clientInfo } : {}
35071
35107
  },
@@ -35091,9 +35127,30 @@ var init_client = __esm({
35091
35127
  supportsClose() {
35092
35128
  return this.agentCapabilities.sessionCapabilities?.close != null;
35093
35129
  }
35130
+ /** Agent 声明了 `auth.logout`(存在即支持)。 */
35131
+ supportsLogout() {
35132
+ return this.agentCapabilities.auth?.logout != null;
35133
+ }
35094
35134
  supportsImages() {
35095
35135
  return this.agentCapabilities.promptCapabilities?.image === true;
35096
35136
  }
35137
+ /**
35138
+ * 用 agent 型认证方法登录(`methodId` 取自 `authMethods`)。terminal 型方法不经 `authenticate`:已知是
35139
+ * terminal 型时抛 -32602,由宿主在终端里跑该方法的 `args` / `env`。失败的错误码原样抛出。
35140
+ */
35141
+ async authenticate(methodId, signal) {
35142
+ const known = this.authMethods.find((method) => method.id === methodId);
35143
+ if (known !== void 0 && isTerminalAuthMethod(known))
35144
+ throw new RpcError(
35145
+ RPC_ERRORS.invalidParams,
35146
+ `auth method ${methodId} is a terminal method: run it in a terminal, not through authenticate`
35147
+ );
35148
+ return this.peer.request(ACP_METHODS.authenticate, { methodId }, signal);
35149
+ }
35150
+ /** 登出当前账号;Agent 没声明 `auth.logout`(见 `supportsLogout`)时会答 method not found。 */
35151
+ logout(signal) {
35152
+ return this.peer.request(ACP_METHODS.logout, {}, signal);
35153
+ }
35097
35154
  newSession(cwd, signal, options) {
35098
35155
  return this.peer.request(
35099
35156
  ACP_METHODS.sessionNew,
@@ -35117,14 +35174,34 @@ var init_client = __esm({
35117
35174
  signal
35118
35175
  );
35119
35176
  }
35120
- listSessions(cwd, cursor) {
35121
- return this.peer.request(ACP_METHODS.sessionList, {
35122
- ...cwd !== void 0 ? { cwd } : {},
35123
- ...cursor !== void 0 ? { cursor } : {}
35124
- });
35177
+ /** 一页会话;`cwd` 过滤(绝对路径),`cursor` 取自上一页的 `nextCursor`。 */
35178
+ listSessions(cwd, cursor, signal) {
35179
+ return this.peer.request(
35180
+ ACP_METHODS.sessionList,
35181
+ {
35182
+ ...cwd !== void 0 ? { cwd } : {},
35183
+ ...cursor !== void 0 ? { cursor } : {}
35184
+ },
35185
+ signal
35186
+ );
35125
35187
  }
35126
- closeSession(sessionId) {
35127
- return this.peer.request(ACP_METHODS.sessionClose, { sessionId });
35188
+ /** 跟 `nextCursor` 翻页取全(`maxPages` 防对端游标不终止,缺省 100 页)。 */
35189
+ async listAllSessions(cwd, signal, maxPages = 100) {
35190
+ const sessions = [];
35191
+ let cursor;
35192
+ for (let page2 = 0; page2 < maxPages; page2++) {
35193
+ const result = await this.listSessions(cwd, cursor, signal);
35194
+ sessions.push(...result.sessions ?? []);
35195
+ if (typeof result.nextCursor !== "string" || result.nextCursor === "") break;
35196
+ cursor = result.nextCursor;
35197
+ }
35198
+ return sessions;
35199
+ }
35200
+ /** 关闭会话:规范要求 Agent 先取消进行中的工作,所以本端先把该会话挂起的请求收掉。 */
35201
+ closeSession(sessionId, signal) {
35202
+ for (const controller of this.pendingPermissions.get(sessionId) ?? []) controller.abort();
35203
+ for (const controller of this.pendingElicitations.get(sessionId) ?? []) controller.abort();
35204
+ return this.peer.request(ACP_METHODS.sessionClose, { sessionId }, signal);
35128
35205
  }
35129
35206
  prompt(sessionId, prompt, signal) {
35130
35207
  return this.peer.request(ACP_METHODS.sessionPrompt, { sessionId, prompt }, signal);
@@ -35191,6 +35268,8 @@ var init_client = __esm({
35191
35268
  const set = this.pendingElicitations.get(key) ?? /* @__PURE__ */ new Set();
35192
35269
  set.add(controller);
35193
35270
  this.pendingElicitations.set(key, set);
35271
+ const id = typeof request.elicitationId === "string" ? request.elicitationId : void 0;
35272
+ if (id !== void 0) this.elicitationsById.set(id, controller);
35194
35273
  const signal = AbortSignal.any([controller.signal, connection]);
35195
35274
  try {
35196
35275
  const answer = await Promise.race([
@@ -35205,8 +35284,17 @@ var init_client = __esm({
35205
35284
  return elicitationAnswer(answer);
35206
35285
  } finally {
35207
35286
  set.delete(controller);
35287
+ if (id !== void 0 && this.elicitationsById.get(id) === controller)
35288
+ this.elicitationsById.delete(id);
35208
35289
  }
35209
35290
  }
35291
+ /** 通知没有答复:参数不对就忽略;还挂着的同 id elicitation 先收掉,再交给宿主。 */
35292
+ onElicitationComplete(params) {
35293
+ const id = params?.elicitationId;
35294
+ if (typeof id !== "string") return;
35295
+ this.elicitationsById.get(id)?.abort();
35296
+ this.options.onElicitationComplete?.(params);
35297
+ }
35210
35298
  };
35211
35299
  }
35212
35300
  });
@@ -38784,6 +38872,7 @@ function subagentEnvironment(deps, catalog) {
38784
38872
  if (retain !== void 0) env.retain = retain;
38785
38873
  const sub = config.subagents;
38786
38874
  env.background = resolveTaskBackground(sub?.background, deps.assembly.unattended === true);
38875
+ if (isCoordinatorPreset(config)) env.background = true;
38787
38876
  const after = sub?.autoBackgroundAfterMs;
38788
38877
  if (typeof after === "number" && after > 0) env.autoBackgroundAfterMs = after;
38789
38878
  return env;
@@ -38836,6 +38925,8 @@ function createSubagentsFactory(deps) {
38836
38925
  bindTaskAgents(task, catalog);
38837
38926
  const env = subagentEnvironment(deps, catalog);
38838
38927
  bindTaskBackground(task, env.background === true);
38928
+ if (isCoordinatorPreset(deps.assembly.config))
38929
+ bindTaskCtlWaitCap(deps.assembly.tools.get("task_ctl"), COORDINATOR_WAIT_MAX_MS);
38839
38930
  return ({ core }) => core.depth > 0 ? void 0 : createSubagentExtension(core, env, wiring);
38840
38931
  }
38841
38932
  var warned;
@@ -38847,6 +38938,8 @@ var init_compose_agents = __esm({
38847
38938
  init_discover();
38848
38939
  init_external();
38849
38940
  init_api_impl();
38941
+ init_presets2();
38942
+ init_task_ctl();
38850
38943
  init_task();
38851
38944
  init_subagent_background();
38852
38945
  init_i18n();
@@ -65827,6 +65920,7 @@ function runInteractiveMode(runtime, context, options = {}) {
65827
65920
  tui.addChild(status);
65828
65921
  tui.addChild(area.quota);
65829
65922
  let finished = false;
65923
+ const hintTimers = options.hintTimers ?? realHintTimers;
65830
65924
  let hintTimer;
65831
65925
  let resolveExit = () => void 0;
65832
65926
  const render = () => tui.requestRender();
@@ -65836,14 +65930,13 @@ function runInteractiveMode(runtime, context, options = {}) {
65836
65930
  };
65837
65931
  const showHint = (text, ms = HINT_MS) => {
65838
65932
  hint.setText(text === "" ? "" : theme.fg("dim", text));
65839
- if (hintTimer !== void 0) clearTimeout(hintTimer);
65933
+ if (hintTimer !== void 0) hintTimers.clear(hintTimer);
65840
65934
  hintTimer = void 0;
65841
65935
  if (text !== "") {
65842
- hintTimer = setTimeout(() => {
65936
+ hintTimer = hintTimers.set(() => {
65843
65937
  hint.setText("");
65844
65938
  render();
65845
65939
  }, ms);
65846
- hintTimer.unref?.();
65847
65940
  }
65848
65941
  render();
65849
65942
  };
@@ -66096,7 +66189,7 @@ function runInteractiveMode(runtime, context, options = {}) {
66096
66189
  if (finished) return;
66097
66190
  finished = true;
66098
66191
  logoAnimation?.finish();
66099
- if (hintTimer !== void 0) clearTimeout(hintTimer);
66192
+ if (hintTimer !== void 0) hintTimers.clear(hintTimer);
66100
66193
  loader.stop();
66101
66194
  area.dispose();
66102
66195
  osc7501.exit();
@@ -66162,7 +66255,7 @@ function runInteractiveMode(runtime, context, options = {}) {
66162
66255
  }
66163
66256
  });
66164
66257
  }
66165
- var import_node_path101, HINT_MS;
66258
+ var import_node_path101, HINT_MS, realHintTimers;
66166
66259
  var init_interactive_mode = __esm({
66167
66260
  "src/modes/interactive/interactive-mode.ts"() {
66168
66261
  "use strict";
@@ -66201,6 +66294,14 @@ var init_interactive_mode = __esm({
66201
66294
  init_tool_view();
66202
66295
  init_task_background();
66203
66296
  HINT_MS = 2500;
66297
+ realHintTimers = {
66298
+ set: (fn, ms) => {
66299
+ const handle = setTimeout(fn, ms);
66300
+ handle.unref?.();
66301
+ return handle;
66302
+ },
66303
+ clear: (handle) => clearTimeout(handle)
66304
+ };
66204
66305
  }
66205
66306
  });
66206
66307
 
@@ -18,6 +18,8 @@ import { AgentCatalog } from "../agents/catalog.js";
18
18
  import { agentSources, discoverAgents } from "../agents/discover.js";
19
19
  import { NO_AGENT_PROBE_ENV, SessionExternalAgents, registerExternalAgents, } from "../agents/external.js";
20
20
  import { hostRunnersOf } from "../host/api-impl.js";
21
+ import { isCoordinatorPreset } from "../tools/presets.js";
22
+ import { COORDINATOR_WAIT_MAX_MS, bindTaskCtlWaitCap } from "../tools/task-ctl.js";
21
23
  import { bindTaskAgents, bindTaskBackground } from "../tools/task.js";
22
24
  import { resolveTaskBackground } from "../agent/subagent-background.js";
23
25
  import { msg } from "../i18n/index.js";
@@ -62,6 +64,9 @@ export function subagentEnvironment(deps, catalog) {
62
64
  // [W7-B1] 缺省后台与自动转后台(配置键 W7-B2 登记于 config/types-w5.ts)
63
65
  const sub = config.subagents;
64
66
  env.background = resolveTaskBackground(sub?.background, deps.assembly.unattended === true);
67
+ // [#242] 协调者不阻塞:缺省后台,不受 subagents.background / -p 影响(调用参数与类型定义仍优先)
68
+ if (isCoordinatorPreset(config))
69
+ env.background = true;
65
70
  const after = sub?.autoBackgroundAfterMs;
66
71
  if (typeof after === "number" && after > 0)
67
72
  env.autoBackgroundAfterMs = after;
@@ -120,5 +125,8 @@ export function createSubagentsFactory(deps) {
120
125
  bindTaskAgents(task, catalog);
121
126
  const env = subagentEnvironment(deps, catalog);
122
127
  bindTaskBackground(task, env.background === true);
128
+ // [#242] 协调者预设:task_ctl wait 超时封顶(task 与 task_ctl 同进退)
129
+ if (isCoordinatorPreset(deps.assembly.config))
130
+ bindTaskCtlWaitCap(deps.assembly.tools.get("task_ctl"), COORDINATOR_WAIT_MAX_MS);
123
131
  return ({ core }) => (core.depth > 0 ? undefined : createSubagentExtension(core, env, wiring));
124
132
  }
@@ -14,13 +14,29 @@
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
- * 不声明 `auth.terminal`(ama 作客户端没有可借给 Agent 的交互终端)。[ACP-D]
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
- import { type AcpAgentCapabilities, type AcpAuthMethod, type AcpContentBlock, type AcpElicitationParams, type AcpElicitationResult, type AcpImplementationInfo, type AcpInitializeResult, type AcpListSessionsResult, type AcpLoadSessionResult, type AcpNewSessionResult, type AcpPromptResult, type AcpRequestPermissionParams, type AcpRequestPermissionResult, type AcpSessionNotification, type AcpSessionOptions, type AcpSetConfigOptionResult } from "./types.js";
38
+ import { RpcError } from "../jsonrpc.js";
39
+ import { type AcpAgentCapabilities, type AcpAuthMethod, type AcpAuthenticateResult, type AcpLogoutResult, type AcpCompleteElicitationParams, type AcpContentBlock, type AcpElicitationParams, type AcpElicitationResult, type AcpImplementationInfo, type AcpInitializeResult, type AcpListSessionsResult, type AcpCloseSessionResult, type AcpSessionInfo, type AcpLoadSessionResult, type AcpNewSessionResult, type AcpPromptResult, type AcpRequestPermissionParams, type AcpRequestPermissionResult, type AcpSessionNotification, type AcpSessionOptions, type AcpSetConfigOptionResult } from "./types.js";
24
40
  export interface AcpClientHandlers {
25
41
  onUpdate?(notification: AcpSessionNotification): void;
26
42
  /** `signal`:该会话被 cancel、连接关闭时 abort(此时应尽快回 cancelled)。 */
@@ -30,6 +46,11 @@ export interface AcpClientHandlers {
30
46
  * `signal`:该会话被 cancel、连接关闭时 abort(此时回 `{ action: "cancel" }`,不必再答)。
31
47
  */
32
48
  onElicitation?(params: AcpElicitationParams, signal: AbortSignal): Promise<AcpElicitationResult>;
49
+ /**
50
+ * `elicitation/complete`:URL 型 elicitation 已在带外完成(`params.elicitationId` 对应此前
51
+ * `elicitation/create` 的同名字段),宿主据此收掉对应卡片。给了它,`initialize` 才声明 `elicitation.url`。
52
+ */
53
+ onElicitationComplete?(params: AcpCompleteElicitationParams): void;
33
54
  /**
34
55
  * `terminal/*`(create / output / wait_for_exit / kill / release):宿主替 Agent 起终端。给了它才声明
35
56
  * `clientCapabilities.terminal: true` 并把这些请求交给它;`method` 是完整方法名,`params` 原样交回,返回值即
@@ -43,7 +64,14 @@ export interface AcpClientOptions extends AcpClientHandlers {
43
64
  input: NodeJS.ReadableStream;
44
65
  output: NodeJS.WritableStream;
45
66
  clientInfo?: AcpImplementationInfo;
67
+ /**
68
+ * 宿主能在交互终端里复现 Agent 的启动命令:声明 `clientCapabilities.auth.terminal`,Agent 才会在
69
+ * `authMethods` 里给 terminal 型方法。缺省不声明。
70
+ */
71
+ authTerminal?: boolean;
46
72
  }
73
+ /** 错误是 ACP 的 auth_required(-32000):开会话或发提示前需要先认证 / 登录。 */
74
+ export declare function isAuthRequiredError(error: unknown): error is RpcError;
47
75
  /** 无人值守的回答:首个 reject_once,没有就 cancelled。 */
48
76
  export declare function unattendedOutcome(params: AcpRequestPermissionParams): AcpRequestPermissionResult;
49
77
  export declare class AcpClient {
@@ -53,19 +81,25 @@ export declare class AcpClient {
53
81
  * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器;
54
82
  * - `elicitation`:构造参数 `onElicitation` 接 `elicitation/create`;
55
83
  * - `configOptions`:`setConfigOption` 与开会话答的 `configOptions`;
56
- * - `terminal`:构造参数 `onTerminal` 接 `terminal/*`。
84
+ * - `terminal`:构造参数 `onTerminal` 接 `terminal/*`;
85
+ * - `auth`:`authenticate` / `logout`、构造参数 `authTerminal`、`isAuthRequiredError`;
86
+ * - `elicitationComplete`:构造参数 `onElicitationComplete` 接 `elicitation/complete`,表单字段含多选与 `oneOf`。
57
87
  */
58
88
  static readonly features: {
59
89
  readonly mcpServers: true;
60
90
  readonly elicitation: true;
61
91
  readonly configOptions: true;
62
92
  readonly terminal: true;
93
+ readonly auth: true;
94
+ readonly elicitationComplete: true;
63
95
  };
64
96
  private readonly peer;
65
97
  /** sessionId → 挂起权限请求的取消器。 */
66
98
  private readonly pendingPermissions;
67
99
  /** sessionId(没给就是空串)→ 挂起 elicitation 的取消器。 */
68
100
  private readonly pendingElicitations;
101
+ /** elicitationId → 挂起 elicitation 的取消器(`elicitation/complete` 用)。 */
102
+ private readonly elicitationsById;
69
103
  private initResult;
70
104
  constructor(options: AcpClientOptions);
71
105
  get closed(): Promise<void>;
@@ -79,14 +113,27 @@ export declare class AcpClient {
79
113
  supportsLoad(): boolean;
80
114
  supportsList(): boolean;
81
115
  supportsClose(): boolean;
116
+ /** Agent 声明了 `auth.logout`(存在即支持)。 */
117
+ supportsLogout(): boolean;
82
118
  supportsImages(): boolean;
119
+ /**
120
+ * 用 agent 型认证方法登录(`methodId` 取自 `authMethods`)。terminal 型方法不经 `authenticate`:已知是
121
+ * terminal 型时抛 -32602,由宿主在终端里跑该方法的 `args` / `env`。失败的错误码原样抛出。
122
+ */
123
+ authenticate(methodId: string, signal?: AbortSignal): Promise<AcpAuthenticateResult>;
124
+ /** 登出当前账号;Agent 没声明 `auth.logout`(见 `supportsLogout`)时会答 method not found。 */
125
+ logout(signal?: AbortSignal): Promise<AcpLogoutResult>;
83
126
  newSession(cwd: string, signal?: AbortSignal, options?: AcpSessionOptions): Promise<AcpNewSessionResult>;
84
127
  /** 不回放历史(优先,R13:v2 计划取消 load)。 */
85
128
  resumeSession(sessionId: string, cwd: string, signal?: AbortSignal, options?: AcpSessionOptions): Promise<AcpLoadSessionResult>;
86
129
  /** 回放历史(以 session/update 通知)。 */
87
130
  loadSession(sessionId: string, cwd: string, signal?: AbortSignal, options?: AcpSessionOptions): Promise<AcpLoadSessionResult>;
88
- listSessions(cwd?: string, cursor?: string): Promise<AcpListSessionsResult>;
89
- closeSession(sessionId: string): Promise<unknown>;
131
+ /** 一页会话;`cwd` 过滤(绝对路径),`cursor` 取自上一页的 `nextCursor`。 */
132
+ listSessions(cwd?: string, cursor?: string, signal?: AbortSignal): Promise<AcpListSessionsResult>;
133
+ /** 跟 `nextCursor` 翻页取全(`maxPages` 防对端游标不终止,缺省 100 页)。 */
134
+ listAllSessions(cwd?: string, signal?: AbortSignal, maxPages?: number): Promise<AcpSessionInfo[]>;
135
+ /** 关闭会话:规范要求 Agent 先取消进行中的工作,所以本端先把该会话挂起的请求收掉。 */
136
+ closeSession(sessionId: string, signal?: AbortSignal): Promise<AcpCloseSessionResult>;
90
137
  prompt(sessionId: string, prompt: AcpContentBlock[], signal?: AbortSignal): Promise<AcpPromptResult>;
91
138
  setMode(sessionId: string, modeId: string): Promise<unknown>;
92
139
  /** 改一个会话配置项(如模型);答复是全部配置项的新状态。 */
@@ -96,4 +143,6 @@ export declare class AcpClient {
96
143
  close(): void;
97
144
  private onRequest;
98
145
  private onElicitation;
146
+ /** 通知没有答复:参数不对就忽略;还挂着的同 id elicitation 先收掉,再交给宿主。 */
147
+ private onElicitationComplete;
99
148
  }