@armadra/agent 0.10.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CHANGELOG.zh-CN.md +31 -0
  3. package/README.md +5 -0
  4. package/README.zh-CN.md +4 -0
  5. package/dist/acp.d.ts +2 -2
  6. package/dist/acp.js +2 -2
  7. package/dist/bundle/ama.cjs +1730 -446
  8. package/dist/cli/compose-agents.js +8 -0
  9. package/dist/cli/compose.d.ts +3 -0
  10. package/dist/cli/compose.js +1 -0
  11. package/dist/drivers/acp/client.d.ts +54 -5
  12. package/dist/drivers/acp/client.js +96 -9
  13. package/dist/drivers/acp/types.d.ts +69 -3
  14. package/dist/drivers/acp/types.js +30 -1
  15. package/dist/drivers/agents.js +1 -1
  16. package/dist/drivers/host-runners.d.ts +26 -1
  17. package/dist/drivers/host-runners.js +147 -0
  18. package/dist/hooks/matcher.d.ts +2 -2
  19. package/dist/hooks/matcher.js +3 -3
  20. package/dist/host/api-impl.d.ts +3 -0
  21. package/dist/host/api-impl.js +7 -2
  22. package/dist/host/types.d.ts +37 -1
  23. package/dist/host/types.js +5 -0
  24. package/dist/host.d.ts +1 -1
  25. package/dist/host.js +1 -1
  26. package/dist/i18n/catalog.d.ts +5 -0
  27. package/dist/i18n/messages/approval.d.ts +5 -0
  28. package/dist/i18n/messages/approval.js +5 -0
  29. package/dist/i18n/messages/drivers.d.ts +2 -0
  30. package/dist/i18n/messages/drivers.js +2 -0
  31. package/dist/i18n/messages/interactive-view.d.ts +5 -0
  32. package/dist/i18n/messages/interactive-view.js +5 -0
  33. package/dist/i18n/messages/interactive.d.ts +4 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/modes/commands-core.d.ts +7 -0
  37. package/dist/modes/commands-core.js +23 -5
  38. package/dist/modes/interactive/approval-dialog.js +17 -4
  39. package/dist/modes/interactive/commands.d.ts +2 -0
  40. package/dist/modes/interactive/commands.js +5 -1
  41. package/dist/modes/interactive/interactive-mode.d.ts +7 -0
  42. package/dist/modes/interactive/interactive-mode.js +14 -6
  43. package/dist/modes/interactive/tool-summary.d.ts +2 -0
  44. package/dist/modes/interactive/tool-summary.js +27 -0
  45. package/dist/modes/rpc/commands.js +4 -4
  46. package/dist/permissions/auto-safe.d.ts +3 -0
  47. package/dist/permissions/auto-safe.js +10 -3
  48. package/dist/permissions/classifier.js +1 -1
  49. package/dist/permissions/network-class.d.ts +28 -0
  50. package/dist/permissions/network-class.js +91 -0
  51. package/dist/permissions/pipeline.d.ts +1 -1
  52. package/dist/permissions/pipeline.js +24 -7
  53. package/dist/permissions/preview.js +3 -0
  54. package/dist/permissions/readonly-bash.d.ts +2 -2
  55. package/dist/permissions/readonly-bash.js +2 -2
  56. package/dist/permissions/rules.d.ts +1 -0
  57. package/dist/permissions/rules.js +5 -1
  58. package/dist/tools/presets.d.ts +8 -1
  59. package/dist/tools/presets.js +12 -3
  60. package/dist/tools/registry.d.ts +3 -1
  61. package/dist/tools/registry.js +4 -2
  62. package/dist/tools/task-ctl.d.ts +9 -0
  63. package/dist/tools/task-ctl.js +22 -3
  64. package/dist/tools/types.d.ts +4 -1
  65. package/dist/tools/web/fetch.d.ts +48 -0
  66. package/dist/tools/web/fetch.js +307 -0
  67. package/dist/tools/web/html-to-markdown.d.ts +31 -0
  68. package/dist/tools/web/html-to-markdown.js +522 -0
  69. package/dist/tools/web/url-guard.d.ts +59 -0
  70. package/dist/tools/web/url-guard.js +221 -0
  71. package/docs/en/guides/permissions.md +33 -11
  72. package/docs/en/reference/acp.md +7 -3
  73. package/docs/en/reference/host-api.md +61 -19
  74. package/docs/en/reference/rpc.md +7 -7
  75. package/docs/guides/agents.md +18 -1
  76. package/docs/guides/hooks.md +86 -8
  77. package/docs/guides/permissions.md +33 -11
  78. package/docs/guides/sandbox.md +6 -0
  79. package/docs/reference/acp.md +7 -3
  80. package/docs/reference/host-api.md +61 -19
  81. package/docs/reference/rpc.md +7 -7
  82. package/package.json +1 -1
@@ -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
  }
@@ -30,6 +30,7 @@ import type { Rule } from "../permissions/types.js";
30
30
  import { type SandboxCapability } from "../codemode/capability.js";
31
31
  import { type BashSandbox } from "../sandbox/bash.js";
32
32
  import { PresetToolRegistry } from "../tools/presets.js";
33
+ import type { WebFetchOptions } from "../tools/web/fetch.js";
33
34
  import type { MemoryRuntime } from "../memory/runtime.js";
34
35
  import type { ToolDefinition, ToolRegistryApi } from "../tools/types.js";
35
36
  import { type ComposeState, type LogFn } from "./compose-session.js";
@@ -57,6 +58,8 @@ export interface ComposeOptions {
57
58
  apis?: ApiRegistry;
58
59
  /** SDK 追加的工具(来源 sdk,总在活动集里)。 */
59
60
  extraTools?: ToolDefinition[];
61
+ /** 测试注入:web_fetch 的解析与请求(不出网)。 */
62
+ webFetch?: WebFetchOptions;
60
63
  /** 缺省 DEFAULT_TOOL_FACTORIES。 */
61
64
  toolFactories?: readonly ToolFactory[];
62
65
  /**
@@ -88,6 +88,7 @@ export function createTools(input, options, state) {
88
88
  };
89
89
  const builtins = builtinTools({
90
90
  bash,
91
+ ...(options.webFetch !== undefined ? { webFetch: options.webFetch } : {}),
91
92
  read: {
92
93
  supportsImages: (ctx) => modelAcceptsImages(state, ctx.model),
93
94
  imageOptions: (ctx) => imageFitOptionsFor(state.providers, ctx.model, input.config.images),
@@ -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
  }
@@ -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
- * 不声明 `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
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 = { mcpServers: true, elicitation: true, configOptions: true, terminal: true };
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.onElicitation !== undefined ? { elicitation: {} } : {}),
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
- listSessions(cwd, cursor) {
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
- closeSession(sessionId) {
154
- return this.peer.request(ACP_METHODS.sessionClose, { sessionId });
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
- /** ama 不实现(只给 terminal 型认证方法,规范要求这类方法不经 authenticate)。 */
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, string | number | boolean>;
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
- /** ama 不实现(只给 terminal 型认证方法,规范要求这类方法不经 authenticate)。 */
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";
@@ -82,7 +82,7 @@ export class ExternalAgents {
82
82
  }
83
83
  /** `task(agent=<spec>)` 的 runner;不可用时抛 AmaError(`agent_host_only` / `agent_unknown`)。 */
84
84
  resolve(spec) {
85
- const hosted = this.hostRunners.get(spec);
85
+ const hosted = this.hostRunners.resolve(spec);
86
86
  if (hosted !== undefined)
87
87
  return hosted;
88
88
  if (this.options.hosted)
@@ -4,15 +4,40 @@
4
4
  * 一个宿主适配器一份(host/api-impl.ts 建,经 {@link ExternalAgents} 的 `hostRunners` 选项交给每个
5
5
  * 主会话):宿主可以在 `create()` 里或之后任何时候 `provide`,同名替换;返回的函数注销。
6
6
  * `onChange` 供 `/agents` / RPC `get_agents` 的缓存刷新。
7
+ *
8
+ * [#226] `onDone`(`HostApi.runners.onDone`):任务入口经 {@link HostRunnerRegistry.resolve} 拿到的是
9
+ * 观察包装——句柄的每一轮(`start` 之后、每次 `send` / `interrupt` / `message` 之后)在 `wait()` 落定时
10
+ * 发一次 {@link RunnerDoneEvent}。包装只观察:`wait()` 的结果与异常原样返回,其余成员原样转给宿主句柄;
11
+ * 同一轮里多次 `wait()` 只发一次,落定后没有新一轮的 `wait()` 不再发。
7
12
  */
8
- import type { HostRunner } from "../host/types.js";
13
+ import type { HostRunner, RunnerDoneEvent } from "../host/types.js";
14
+ export type RunnerDoneHandler = (event: RunnerDoneEvent) => void | Promise<void>;
15
+ export interface HostRunnerRegistryOptions {
16
+ /** 毫秒时钟(测试注入)。 */
17
+ now?: () => number;
18
+ /** `onDone` 处理器异常的去处;缺省丢弃。 */
19
+ log?: (message: string, error: unknown) => void;
20
+ }
9
21
  export declare class HostRunnerRegistry {
10
22
  private readonly runners;
11
23
  private readonly listeners;
24
+ private readonly doneHandlers;
25
+ private readonly observed;
26
+ private readonly now;
27
+ private readonly log;
28
+ constructor(options?: HostRunnerRegistryOptions);
12
29
  provide(runner: HostRunner): () => void;
13
30
  get(id: string): HostRunner | undefined;
31
+ /** [#226] 任务入口用:同 {@link get},但返回发 `onDone` 的观察包装(同一 runner 包装复用)。 */
32
+ resolve(id: string): HostRunner | undefined;
14
33
  list(): HostRunner[];
15
34
  /** 注入 / 注销时回调;返回取消订阅。 */
16
35
  onChange(listener: () => void): () => void;
36
+ /** [#226] 每轮结束时回调(`HostApi.runners.onDone`);返回注销函数。 */
37
+ onDone(handler: RunnerDoneHandler): () => void;
17
38
  private changed;
39
+ private emitDone;
40
+ private fail;
41
+ private observe;
42
+ private observeHandle;
18
43
  }