@armadra/agent 0.6.7 → 0.6.8

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,15 @@ 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.6.8 (2026-10-04)
9
+
10
+ - **ACP client: elicitation and session config options**: `AcpClient` takes an optional `onElicitation(params, signal)`;
11
+ when given, `initialize` declares `clientCapabilities.elicitation` and `elicitation/create` goes to it (answers normalized
12
+ to `accept` / `decline` / `cancel`; pending ones resolve `cancel` on `cancel(sessionId)` or connection close). New
13
+ `setConfigOption(sessionId, configId, value)` and `configOptions` on session-open results. `AcpClient.features` gains
14
+ `elicitation` and `configOptions`. Without a handler the wire is unchanged. The fake ACP agent adds `[elicit]`, `[model]`
15
+ and `[env NAME]` markers and `--config-options`. Docs: docs/acp.md.
16
+
8
17
  ## 0.6.7 (2026-10-03)
9
18
 
10
19
  - **A host runner with id `ama` is used**: when the host registers a runner for `ama` via `HostApi.runners.provide`
@@ -4,6 +4,14 @@
4
4
 
5
5
  > 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
6
6
 
7
+ ## 0.6.8(2026-10-04)
8
+
9
+ - **ACP 客户端:elicitation 与会话配置项**:`AcpClient` 可选构造参数 `onElicitation(params, signal)`,给了才在 `initialize`
10
+ 声明 `clientCapabilities.elicitation` 并把 `elicitation/create` 交给它(答复收成 `accept` / `decline` / `cancel`;
11
+ `cancel(sessionId)` 或连接关闭时挂起的回 `cancel`)。新增 `setConfigOption(sessionId, configId, value)`,开会话答复带
12
+ `configOptions`。`AcpClient.features` 多 `elicitation` 与 `configOptions`。不给处理器时线路不变。假 ACP Agent 加
13
+ `[elicit]`、`[model]`、`[env NAME]` 标记与 `--config-options`。文档:docs/acp.md。
14
+
7
15
  ## 0.6.7(2026-10-03)
8
16
 
9
17
  - **宿主注入的 `ama` runner 生效**:宿主经 `HostApi.runners.provide` 注入 id 为 `ama` 的 runner(如 Armadra 画布上另一个
@@ -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.6.7" : readPackageVersion();
44
+ AMA_VERSION = true ? "0.6.8" : readPackageVersion();
45
45
  }
46
46
  });
47
47
 
@@ -30592,8 +30592,10 @@ var init_types9 = __esm({
30592
30592
  sessionPrompt: "session/prompt",
30593
30593
  sessionCancel: "session/cancel",
30594
30594
  sessionSetMode: "session/set_mode",
30595
+ sessionSetConfigOption: "session/set_config_option",
30595
30596
  sessionUpdate: "session/update",
30596
- requestPermission: "session/request_permission"
30597
+ requestPermission: "session/request_permission",
30598
+ elicitationCreate: "elicitation/create"
30597
30599
  };
30598
30600
  RPC_ERRORS = {
30599
30601
  parseError: -32700,
@@ -30790,18 +30792,25 @@ var init_jsonrpc = __esm({
30790
30792
  function mcpServersOf(options) {
30791
30793
  return [...options?.mcpServers ?? []];
30792
30794
  }
30795
+ function elicitationAnswer(answer) {
30796
+ const action = answer?.action;
30797
+ if (action === void 0 || !ELICITATION_ACTIONS.has(action)) return { action: "cancel" };
30798
+ if (action !== "accept" || answer?.content === void 0) return { action };
30799
+ return { action, content: answer.content };
30800
+ }
30793
30801
  function unattendedOutcome(params) {
30794
30802
  const reject2 = params.options.find((o) => o.kind === "reject_once");
30795
30803
  return {
30796
30804
  outcome: reject2 !== void 0 ? { outcome: "selected", optionId: reject2.optionId } : { outcome: "cancelled" }
30797
30805
  };
30798
30806
  }
30799
- var AcpClient;
30807
+ var ELICITATION_ACTIONS, AcpClient;
30800
30808
  var init_client = __esm({
30801
30809
  "src/drivers/acp/client.ts"() {
30802
30810
  "use strict";
30803
30811
  init_jsonrpc();
30804
30812
  init_types9();
30813
+ ELICITATION_ACTIONS = /* @__PURE__ */ new Set(["accept", "decline", "cancel"]);
30805
30814
  AcpClient = class {
30806
30815
  constructor(options) {
30807
30816
  this.options = options;
@@ -30815,6 +30824,7 @@ var init_client = __esm({
30815
30824
  },
30816
30825
  onClose: () => {
30817
30826
  for (const set of this.pendingPermissions.values()) for (const c of set) c.abort();
30827
+ for (const set of this.pendingElicitations.values()) for (const c of set) c.abort();
30818
30828
  options.onClose?.();
30819
30829
  },
30820
30830
  ...options.onProtocolError !== void 0 ? { onProtocolError: options.onProtocolError } : {}
@@ -30822,13 +30832,17 @@ var init_client = __esm({
30822
30832
  }
30823
30833
  options;
30824
30834
  /**
30825
- * 本版客户端支持的可选能力,供宿主做特性检测(旧版没有这个字段):
30826
- * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器。
30835
+ * 本版客户端支持的可选能力,供宿主做特性检测(旧版没有这个字段,旧一点的只有 `mcpServers`):
30836
+ * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器;
30837
+ * - `elicitation`:构造参数 `onElicitation` 接 `elicitation/create`;
30838
+ * - `configOptions`:`setConfigOption` 与开会话答的 `configOptions`。
30827
30839
  */
30828
- static features = { mcpServers: true };
30840
+ static features = { mcpServers: true, elicitation: true, configOptions: true };
30829
30841
  peer;
30830
30842
  /** sessionId → 挂起权限请求的取消器。 */
30831
30843
  pendingPermissions = /* @__PURE__ */ new Map();
30844
+ /** sessionId(没给就是空串)→ 挂起 elicitation 的取消器。 */
30845
+ pendingElicitations = /* @__PURE__ */ new Map();
30832
30846
  initResult;
30833
30847
  get closed() {
30834
30848
  return this.peer.closed;
@@ -30847,7 +30861,11 @@ var init_client = __esm({
30847
30861
  ACP_METHODS.initialize,
30848
30862
  {
30849
30863
  protocolVersion: ACP_PROTOCOL_VERSION,
30850
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
30864
+ clientCapabilities: {
30865
+ fs: { readTextFile: false, writeTextFile: false },
30866
+ terminal: false,
30867
+ ...this.options.onElicitation !== void 0 ? { elicitation: {} } : {}
30868
+ },
30851
30869
  ...this.options.clientInfo !== void 0 ? { clientInfo: this.options.clientInfo } : {}
30852
30870
  },
30853
30871
  signal
@@ -30913,15 +30931,22 @@ var init_client = __esm({
30913
30931
  setMode(sessionId, modeId) {
30914
30932
  return this.peer.request(ACP_METHODS.sessionSetMode, { sessionId, modeId });
30915
30933
  }
30916
- /** 协议级取消:发通知,并让挂起的权限请求回 cancelled。 */
30934
+ /** 改一个会话配置项(如模型);答复是全部配置项的新状态。 */
30935
+ setConfigOption(sessionId, configId, value) {
30936
+ return this.peer.request(ACP_METHODS.sessionSetConfigOption, { sessionId, configId, value });
30937
+ }
30938
+ /** 协议级取消:发通知,并让挂起的权限请求回 cancelled、挂起的 elicitation 回 cancel。 */
30917
30939
  async cancel(sessionId) {
30918
30940
  for (const controller of this.pendingPermissions.get(sessionId) ?? []) controller.abort();
30941
+ for (const controller of this.pendingElicitations.get(sessionId) ?? []) controller.abort();
30919
30942
  await this.peer.notify(ACP_METHODS.sessionCancel, { sessionId });
30920
30943
  }
30921
30944
  close() {
30922
30945
  this.peer.close();
30923
30946
  }
30924
30947
  async onRequest(method, params, connection) {
30948
+ if (method === ACP_METHODS.elicitationCreate && this.options.onElicitation !== void 0)
30949
+ return this.onElicitation(params, connection);
30925
30950
  if (method !== ACP_METHODS.requestPermission)
30926
30951
  throw new RpcError(RPC_ERRORS.methodNotFound, `client does not support ${method}`);
30927
30952
  const request = params;
@@ -30953,6 +30978,32 @@ var init_client = __esm({
30953
30978
  set.delete(controller);
30954
30979
  }
30955
30980
  }
30981
+ async onElicitation(params, connection) {
30982
+ const request = params;
30983
+ if (request === null || typeof request !== "object" || typeof request.message !== "string")
30984
+ throw new RpcError(RPC_ERRORS.invalidParams, "invalid elicitation/create params");
30985
+ const handler = this.options.onElicitation;
30986
+ const key = typeof request.sessionId === "string" ? request.sessionId : "";
30987
+ const controller = new AbortController();
30988
+ const set = this.pendingElicitations.get(key) ?? /* @__PURE__ */ new Set();
30989
+ set.add(controller);
30990
+ this.pendingElicitations.set(key, set);
30991
+ const signal = AbortSignal.any([controller.signal, connection]);
30992
+ try {
30993
+ const answer = await Promise.race([
30994
+ handler(request, signal),
30995
+ new Promise((resolve39) => {
30996
+ const cancelled = () => resolve39({ action: "cancel" });
30997
+ if (signal.aborted) cancelled();
30998
+ else signal.addEventListener("abort", cancelled, { once: true });
30999
+ })
31000
+ ]);
31001
+ if (signal.aborted) return { action: "cancel" };
31002
+ return elicitationAnswer(answer);
31003
+ } finally {
31004
+ set.delete(controller);
31005
+ }
31006
+ }
30956
31007
  };
30957
31008
  }
30958
31009
  });
@@ -10,12 +10,22 @@
10
10
  * (规范要求:客户端取消回合后必须以 cancelled 回答挂起的请求)。
11
11
  * - 开会话(new / resume / load)的 `mcpServers` 缺省为空数组;宿主可经 {@link AcpSessionOptions}
12
12
  * 传入,原样转发。
13
+ * - `elicitation/create` 交给 `onElicitation`;给了它 `initialize` 才声明 `clientCapabilities.elicitation`,
14
+ * 没给时线路与旧版相同(不声明,Agent 发来的请求回 method not found)。`cancel(sessionId)`、连接关闭时
15
+ * 挂起的 elicitation 一律回 `{ action: "cancel" }`;ama 从不替人填表。
16
+ * - `setConfigOption(sessionId, configId, value)` 发 `session/set_config_option`;开会话答的 `configOptions`
17
+ * 原样交回。
13
18
  */
14
- import { type AcpAgentCapabilities, type AcpContentBlock, type AcpImplementationInfo, type AcpInitializeResult, type AcpListSessionsResult, type AcpLoadSessionResult, type AcpNewSessionResult, type AcpPromptResult, type AcpRequestPermissionParams, type AcpRequestPermissionResult, type AcpSessionNotification, type AcpSessionOptions } from "./types.js";
19
+ import { type AcpAgentCapabilities, 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";
15
20
  export interface AcpClientHandlers {
16
21
  onUpdate?(notification: AcpSessionNotification): void;
17
22
  /** `signal`:该会话被 cancel、连接关闭时 abort(此时应尽快回 cancelled)。 */
18
23
  onPermission?(params: AcpRequestPermissionParams, signal: AbortSignal): Promise<AcpRequestPermissionResult>;
24
+ /**
25
+ * `elicitation/create`:Agent 向人要结构化输入。给了它才声明 `clientCapabilities.elicitation`。
26
+ * `signal`:该会话被 cancel、连接关闭时 abort(此时回 `{ action: "cancel" }`,不必再答)。
27
+ */
28
+ onElicitation?(params: AcpElicitationParams, signal: AbortSignal): Promise<AcpElicitationResult>;
19
29
  onProtocolError?(line: string, reason: string): void;
20
30
  onClose?(): void;
21
31
  }
@@ -29,15 +39,21 @@ export declare function unattendedOutcome(params: AcpRequestPermissionParams): A
29
39
  export declare class AcpClient {
30
40
  private readonly options;
31
41
  /**
32
- * 本版客户端支持的可选能力,供宿主做特性检测(旧版没有这个字段):
33
- * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器。
42
+ * 本版客户端支持的可选能力,供宿主做特性检测(旧版没有这个字段,旧一点的只有 `mcpServers`):
43
+ * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器;
44
+ * - `elicitation`:构造参数 `onElicitation` 接 `elicitation/create`;
45
+ * - `configOptions`:`setConfigOption` 与开会话答的 `configOptions`。
34
46
  */
35
47
  static readonly features: {
36
48
  readonly mcpServers: true;
49
+ readonly elicitation: true;
50
+ readonly configOptions: true;
37
51
  };
38
52
  private readonly peer;
39
53
  /** sessionId → 挂起权限请求的取消器。 */
40
54
  private readonly pendingPermissions;
55
+ /** sessionId(没给就是空串)→ 挂起 elicitation 的取消器。 */
56
+ private readonly pendingElicitations;
41
57
  private initResult;
42
58
  constructor(options: AcpClientOptions);
43
59
  get closed(): Promise<void>;
@@ -59,8 +75,11 @@ export declare class AcpClient {
59
75
  closeSession(sessionId: string): Promise<unknown>;
60
76
  prompt(sessionId: string, prompt: AcpContentBlock[], signal?: AbortSignal): Promise<AcpPromptResult>;
61
77
  setMode(sessionId: string, modeId: string): Promise<unknown>;
62
- /** 协议级取消:发通知,并让挂起的权限请求回 cancelled。 */
78
+ /** 改一个会话配置项(如模型);答复是全部配置项的新状态。 */
79
+ setConfigOption(sessionId: string, configId: string, value: string): Promise<AcpSetConfigOptionResult>;
80
+ /** 协议级取消:发通知,并让挂起的权限请求回 cancelled、挂起的 elicitation 回 cancel。 */
63
81
  cancel(sessionId: string): Promise<void>;
64
82
  close(): void;
65
83
  private onRequest;
84
+ private onElicitation;
66
85
  }
@@ -10,6 +10,11 @@
10
10
  * (规范要求:客户端取消回合后必须以 cancelled 回答挂起的请求)。
11
11
  * - 开会话(new / resume / load)的 `mcpServers` 缺省为空数组;宿主可经 {@link AcpSessionOptions}
12
12
  * 传入,原样转发。
13
+ * - `elicitation/create` 交给 `onElicitation`;给了它 `initialize` 才声明 `clientCapabilities.elicitation`,
14
+ * 没给时线路与旧版相同(不声明,Agent 发来的请求回 method not found)。`cancel(sessionId)`、连接关闭时
15
+ * 挂起的 elicitation 一律回 `{ action: "cancel" }`;ama 从不替人填表。
16
+ * - `setConfigOption(sessionId, configId, value)` 发 `session/set_config_option`;开会话答的 `configOptions`
17
+ * 原样交回。
13
18
  */
14
19
  import { JsonRpcPeer, RpcError } from "../jsonrpc.js";
15
20
  import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./types.js";
@@ -17,6 +22,16 @@ import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./types.js";
17
22
  function mcpServersOf(options) {
18
23
  return [...(options?.mcpServers ?? [])];
19
24
  }
25
+ const ELICITATION_ACTIONS = new Set(["accept", "decline", "cancel"]);
26
+ /** 处理器的答复收成规范形状:动作不认识当 cancel,`content` 只随 accept。 */
27
+ function elicitationAnswer(answer) {
28
+ const action = answer?.action;
29
+ if (action === undefined || !ELICITATION_ACTIONS.has(action))
30
+ return { action: "cancel" };
31
+ if (action !== "accept" || answer?.content === undefined)
32
+ return { action };
33
+ return { action, content: answer.content };
34
+ }
20
35
  /** 无人值守的回答:首个 reject_once,没有就 cancelled。 */
21
36
  export function unattendedOutcome(params) {
22
37
  const reject = params.options.find((o) => o.kind === "reject_once");
@@ -29,13 +44,17 @@ export function unattendedOutcome(params) {
29
44
  export class AcpClient {
30
45
  options;
31
46
  /**
32
- * 本版客户端支持的可选能力,供宿主做特性检测(旧版没有这个字段):
33
- * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器。
47
+ * 本版客户端支持的可选能力,供宿主做特性检测(旧版没有这个字段,旧一点的只有 `mcpServers`):
48
+ * - `mcpServers`:开会话时可经 {@link AcpSessionOptions} 传 MCP 服务器;
49
+ * - `elicitation`:构造参数 `onElicitation` 接 `elicitation/create`;
50
+ * - `configOptions`:`setConfigOption` 与开会话答的 `configOptions`。
34
51
  */
35
- static features = { mcpServers: true };
52
+ static features = { mcpServers: true, elicitation: true, configOptions: true };
36
53
  peer;
37
54
  /** sessionId → 挂起权限请求的取消器。 */
38
55
  pendingPermissions = new Map();
56
+ /** sessionId(没给就是空串)→ 挂起 elicitation 的取消器。 */
57
+ pendingElicitations = new Map();
39
58
  initResult;
40
59
  constructor(options) {
41
60
  this.options = options;
@@ -51,6 +70,9 @@ export class AcpClient {
51
70
  for (const set of this.pendingPermissions.values())
52
71
  for (const c of set)
53
72
  c.abort();
73
+ for (const set of this.pendingElicitations.values())
74
+ for (const c of set)
75
+ c.abort();
54
76
  options.onClose?.();
55
77
  },
56
78
  ...(options.onProtocolError !== undefined
@@ -73,7 +95,11 @@ export class AcpClient {
73
95
  async initialize(signal) {
74
96
  const result = await this.peer.request(ACP_METHODS.initialize, {
75
97
  protocolVersion: ACP_PROTOCOL_VERSION,
76
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
98
+ clientCapabilities: {
99
+ fs: { readTextFile: false, writeTextFile: false },
100
+ terminal: false,
101
+ ...(this.options.onElicitation !== undefined ? { elicitation: {} } : {}),
102
+ },
77
103
  ...(this.options.clientInfo !== undefined ? { clientInfo: this.options.clientInfo } : {}),
78
104
  }, signal);
79
105
  if (result.protocolVersion !== ACP_PROTOCOL_VERSION)
@@ -122,16 +148,24 @@ export class AcpClient {
122
148
  setMode(sessionId, modeId) {
123
149
  return this.peer.request(ACP_METHODS.sessionSetMode, { sessionId, modeId });
124
150
  }
125
- /** 协议级取消:发通知,并让挂起的权限请求回 cancelled。 */
151
+ /** 改一个会话配置项(如模型);答复是全部配置项的新状态。 */
152
+ setConfigOption(sessionId, configId, value) {
153
+ return this.peer.request(ACP_METHODS.sessionSetConfigOption, { sessionId, configId, value });
154
+ }
155
+ /** 协议级取消:发通知,并让挂起的权限请求回 cancelled、挂起的 elicitation 回 cancel。 */
126
156
  async cancel(sessionId) {
127
157
  for (const controller of this.pendingPermissions.get(sessionId) ?? [])
128
158
  controller.abort();
159
+ for (const controller of this.pendingElicitations.get(sessionId) ?? [])
160
+ controller.abort();
129
161
  await this.peer.notify(ACP_METHODS.sessionCancel, { sessionId });
130
162
  }
131
163
  close() {
132
164
  this.peer.close();
133
165
  }
134
166
  async onRequest(method, params, connection) {
167
+ if (method === ACP_METHODS.elicitationCreate && this.options.onElicitation !== undefined)
168
+ return this.onElicitation(params, connection);
135
169
  if (method !== ACP_METHODS.requestPermission)
136
170
  throw new RpcError(RPC_ERRORS.methodNotFound, `client does not support ${method}`);
137
171
  const request = params;
@@ -168,4 +202,34 @@ export class AcpClient {
168
202
  set.delete(controller);
169
203
  }
170
204
  }
205
+ async onElicitation(params, connection) {
206
+ const request = params;
207
+ if (request === null || typeof request !== "object" || typeof request.message !== "string")
208
+ throw new RpcError(RPC_ERRORS.invalidParams, "invalid elicitation/create params");
209
+ const handler = this.options.onElicitation;
210
+ const key = typeof request.sessionId === "string" ? request.sessionId : "";
211
+ const controller = new AbortController();
212
+ const set = this.pendingElicitations.get(key) ?? new Set();
213
+ set.add(controller);
214
+ this.pendingElicitations.set(key, set);
215
+ const signal = AbortSignal.any([controller.signal, connection]);
216
+ try {
217
+ const answer = await Promise.race([
218
+ handler(request, signal),
219
+ new Promise((resolve) => {
220
+ const cancelled = () => resolve({ action: "cancel" });
221
+ if (signal.aborted)
222
+ cancelled();
223
+ else
224
+ signal.addEventListener("abort", cancelled, { once: true });
225
+ }),
226
+ ]);
227
+ if (signal.aborted)
228
+ return { action: "cancel" };
229
+ return elicitationAnswer(answer);
230
+ }
231
+ finally {
232
+ set.delete(controller);
233
+ }
234
+ }
171
235
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal]`。[W5-E]
2
+ * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal] [--config-options]`。[W5-E]
3
3
  * 行为见 fake-agent.ts;stdin 结束即退出。
4
4
  */
5
5
  export {};
@@ -1,7 +1,8 @@
1
1
  /**
2
- * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal]`。[W5-E]
2
+ * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal] [--config-options]`。[W5-E]
3
3
  * 行为见 fake-agent.ts;stdin 结束即退出。
4
4
  */
5
5
  import { runFakeAcpAgent } from "./fake-agent.js";
6
6
  const minimal = process.argv.includes("--minimal");
7
- void runFakeAcpAgent(process.stdin, process.stdout, { minimal }).then(() => process.exit(0));
7
+ const configOptions = process.argv.includes("--config-options");
8
+ void runFakeAcpAgent(process.stdin, process.stdout, { minimal, configOptions }).then(() => process.exit(0));
@@ -13,6 +13,13 @@
13
13
  * | `[plan]` | 先发 `plan`(两条)再回文本 |
14
14
  * | `[think]` | 先发 `agent_thought_chunk` |
15
15
  * | `[refuse]` | `refusal` |
16
+ * | `[elicit]` | `elicitation/create`(选颜色的表单)→ 回 `elicit: <action> <content JSON>`;客户端 |
17
+ * | | 没声明 `elicitation` 能力时回 `elicit: unsupported`;`cancel` 时本回合 `cancelled` |
18
+ * | `[model]` | 回 `model <当前模型>`(`configOptions` 关着时是 `model none`) |
19
+ * | `[env NAME]` | 回 `env NAME <值的 sha256 | absent>`(测环境变量到没到,不回显值) |
20
+ *
21
+ * `configOptions: true`(可执行入口 `--config-options`):开会话答一个 `model` 配置项(`small`,分组
22
+ * `big` 里有 `large`),`session/set_config_option` 可改;缺省不答,线路与之前相同。
16
23
  *
17
24
  * 会话:`session/new` 发 `fake-<n>`;`session/resume` / `session/load` 接受任何 `fake-` 开头的 id
18
25
  * (load 先回放一条用户消息与一条回复);`session/list` 列本进程建过的会话。
@@ -22,6 +29,8 @@ export interface FakeAcpAgentOptions {
22
29
  name?: string;
23
30
  /** 不声明 resume / list / close / loadSession(测降级路径)。 */
24
31
  minimal?: boolean;
32
+ /** 开会话答 `configOptions`(一个 `model` 选择项)并接 `session/set_config_option`。 */
33
+ configOptions?: boolean;
25
34
  }
26
35
  /** 在给定流上跑假 Agent;输入流结束时 resolve。 */
27
36
  export declare function runFakeAcpAgent(input: NodeJS.ReadableStream, output: NodeJS.WritableStream, options?: FakeAcpAgentOptions): Promise<void>;
@@ -13,16 +13,38 @@
13
13
  * | `[plan]` | 先发 `plan`(两条)再回文本 |
14
14
  * | `[think]` | 先发 `agent_thought_chunk` |
15
15
  * | `[refuse]` | `refusal` |
16
+ * | `[elicit]` | `elicitation/create`(选颜色的表单)→ 回 `elicit: <action> <content JSON>`;客户端 |
17
+ * | | 没声明 `elicitation` 能力时回 `elicit: unsupported`;`cancel` 时本回合 `cancelled` |
18
+ * | `[model]` | 回 `model <当前模型>`(`configOptions` 关着时是 `model none`) |
19
+ * | `[env NAME]` | 回 `env NAME <值的 sha256 | absent>`(测环境变量到没到,不回显值) |
20
+ *
21
+ * `configOptions: true`(可执行入口 `--config-options`):开会话答一个 `model` 配置项(`small`,分组
22
+ * `big` 里有 `large`),`session/set_config_option` 可改;缺省不答,线路与之前相同。
16
23
  *
17
24
  * 会话:`session/new` 发 `fake-<n>`;`session/resume` / `session/load` 接受任何 `fake-` 开头的 id
18
25
  * (load 先回放一条用户消息与一条回复);`session/list` 列本进程建过的会话。
19
26
  */
27
+ import { createHash } from "node:crypto";
20
28
  import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS } from "../types.js";
21
29
  import { JsonRpcPeer, RpcError } from "../../jsonrpc.js";
22
30
  const MODES = [
23
31
  { id: "default", name: "Default" },
24
32
  { id: "plan", name: "Plan" },
25
33
  ];
34
+ const MODELS = [
35
+ { value: "small", name: "Small" },
36
+ { group: "big", name: "Big", options: [{ value: "large", name: "Large", description: "slow" }] },
37
+ ];
38
+ const MODEL_VALUES = ["small", "large"];
39
+ /** 一个表单:必填的颜色(枚举)与可选的数量。 */
40
+ const ELICIT_SCHEMA = {
41
+ type: "object",
42
+ properties: {
43
+ color: { type: "string", enum: ["red", "blue"] },
44
+ count: { type: "integer", minimum: 1 },
45
+ },
46
+ required: ["color"],
47
+ };
26
48
  function textOf(blocks) {
27
49
  return blocks.map((b) => (b.type === "text" ? b.text : `[${b.type}]`)).join("");
28
50
  }
@@ -37,6 +59,20 @@ function waitAbort(signal) {
37
59
  export function runFakeAcpAgent(input, output, options = {}) {
38
60
  const sessions = new Map();
39
61
  let counter = 0;
62
+ /** 客户端在 initialize 里声明了 elicitation。 */
63
+ let canElicit = false;
64
+ const configOf = (s) => [
65
+ {
66
+ id: "model",
67
+ name: "Model",
68
+ category: "model",
69
+ type: "select",
70
+ currentValue: s.model,
71
+ options: MODELS,
72
+ },
73
+ ];
74
+ /** 开会话答复里的 `configOptions`(关着时不加这个键)。 */
75
+ const config = (s) => options.configOptions === true ? { configOptions: configOf(s) } : {};
40
76
  const update = (sessionId, value) => peer.notify(ACP_METHODS.sessionUpdate, { sessionId, update: value });
41
77
  const session = (id) => {
42
78
  const found = typeof id === "string" ? sessions.get(id) : undefined;
@@ -45,7 +81,7 @@ export function runFakeAcpAgent(input, output, options = {}) {
45
81
  return found;
46
82
  };
47
83
  const open = (id, cwd) => {
48
- const created = { id, cwd, mode: "default", turns: 0 };
84
+ const created = { id, cwd, mode: "default", model: "small", turns: 0 };
49
85
  sessions.set(id, created);
50
86
  return created;
51
87
  };
@@ -75,6 +111,37 @@ export function runFakeAcpAgent(input, output, options = {}) {
75
111
  });
76
112
  if (text.includes("[refuse]"))
77
113
  return done("refusal");
114
+ const say = (line) => update(s.id, { sessionUpdate: "agent_message_chunk", content: { type: "text", text: line } });
115
+ if (text.includes("[elicit]")) {
116
+ if (!canElicit) {
117
+ await say("elicit: unsupported");
118
+ return done("end_turn");
119
+ }
120
+ let answer;
121
+ try {
122
+ answer = await peer.request(ACP_METHODS.elicitationCreate, {
123
+ sessionId: s.id,
124
+ message: "Pick a color",
125
+ requestedSchema: ELICIT_SCHEMA,
126
+ });
127
+ }
128
+ catch {
129
+ return done("cancelled");
130
+ }
131
+ await say(`elicit: ${answer.action} ${JSON.stringify(answer.content ?? null)}`);
132
+ return done(answer.action === "cancel" ? "cancelled" : "end_turn");
133
+ }
134
+ if (text.includes("[model]")) {
135
+ await say(`model ${options.configOptions === true ? s.model : "none"}`);
136
+ return done("end_turn");
137
+ }
138
+ const env = /\[env ([A-Z0-9_]+)\]/.exec(text);
139
+ if (env !== null) {
140
+ const value = process.env[env[1]];
141
+ const digest = value === undefined ? "absent" : createHash("sha256").update(value).digest("hex");
142
+ await say(`env ${env[1]} ${digest}`);
143
+ return done("end_turn");
144
+ }
78
145
  if (text.includes("[slow]")) {
79
146
  await waitAbort(turn.signal);
80
147
  return done("cancelled");
@@ -141,7 +208,9 @@ export function runFakeAcpAgent(input, output, options = {}) {
141
208
  async onRequest(method, raw) {
142
209
  const params = (raw ?? {});
143
210
  switch (method) {
144
- case ACP_METHODS.initialize:
211
+ case ACP_METHODS.initialize: {
212
+ const capabilities = params["clientCapabilities"];
213
+ canElicit = capabilities?.["elicitation"] != null;
145
214
  return {
146
215
  protocolVersion: ACP_PROTOCOL_VERSION,
147
216
  agentCapabilities: options.minimal
@@ -154,12 +223,17 @@ export function runFakeAcpAgent(input, output, options = {}) {
154
223
  authMethods: [],
155
224
  agentInfo: { name: options.name ?? "fake-acp-agent", version: "1.0.0" },
156
225
  };
226
+ }
157
227
  case ACP_METHODS.sessionNew: {
158
228
  counter += 1;
159
229
  const s = open(`fake-${counter}`, String(params["cwd"] ?? ""));
160
230
  if (options.minimal)
161
- return { sessionId: s.id };
162
- return { sessionId: s.id, modes: { currentModeId: s.mode, availableModes: MODES } };
231
+ return { sessionId: s.id, ...config(s) };
232
+ return {
233
+ sessionId: s.id,
234
+ modes: { currentModeId: s.mode, availableModes: MODES },
235
+ ...config(s),
236
+ };
163
237
  }
164
238
  case ACP_METHODS.sessionResume:
165
239
  case ACP_METHODS.sessionLoad: {
@@ -177,7 +251,7 @@ export function runFakeAcpAgent(input, output, options = {}) {
177
251
  content: { type: "text", text: "echo: earlier" },
178
252
  });
179
253
  }
180
- return { modes: { currentModeId: s.mode, availableModes: MODES } };
254
+ return { modes: { currentModeId: s.mode, availableModes: MODES }, ...config(s) };
181
255
  }
182
256
  case ACP_METHODS.sessionList:
183
257
  return {
@@ -193,6 +267,18 @@ export function runFakeAcpAgent(input, output, options = {}) {
193
267
  await update(s.id, { sessionUpdate: "current_mode_update", currentModeId: s.mode });
194
268
  return {};
195
269
  }
270
+ case ACP_METHODS.sessionSetConfigOption: {
271
+ if (options.configOptions !== true)
272
+ throw new RpcError(RPC_ERRORS.methodNotFound, `method not found: ${method}`);
273
+ const s = session(params["sessionId"]);
274
+ if (params["configId"] !== "model")
275
+ throw new RpcError(RPC_ERRORS.invalidParams, `unknown config: ${String(params["configId"])}`);
276
+ const value = String(params["value"]);
277
+ if (!MODEL_VALUES.includes(value))
278
+ throw new RpcError(RPC_ERRORS.invalidParams, `unknown model: ${value}`);
279
+ s.model = value;
280
+ return { configOptions: configOf(s) };
281
+ }
196
282
  case ACP_METHODS.sessionPrompt:
197
283
  return prompt(raw);
198
284
  default:
@@ -2,8 +2,8 @@
2
2
  * ACP(Agent Client Protocol)v1 的子集类型(docs/wave5-plan.md §5.1,D14)。[W5-E]
3
3
  *
4
4
  * 手写、零依赖;只收 ama 作为客户端(驱动外部 Agent)与服务端(`ama --mode acp`)两侧用到的部分:
5
- * `initialize`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode`、
6
- * `session/update`(含 `usage_update`)、`session/request_permission`。字段名与规范一致(camelCase),
5
+ * `initialize`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode|set_config_option`、
6
+ * `session/update`(含 `usage_update`)、`session/request_permission`、`elicitation/create`。字段名与规范一致(camelCase),
7
7
  * 未知字段一律保留不报错;`_meta` 不解释。
8
8
  *
9
9
  * 不声明 `fs` / `terminal` 客户端能力(与 Armadra Q5 一致):外部 Agent 自己读写、自己跑命令,
@@ -22,8 +22,10 @@ export declare const ACP_METHODS: {
22
22
  readonly sessionPrompt: "session/prompt";
23
23
  readonly sessionCancel: "session/cancel";
24
24
  readonly sessionSetMode: "session/set_mode";
25
+ readonly sessionSetConfigOption: "session/set_config_option";
25
26
  readonly sessionUpdate: "session/update";
26
27
  readonly requestPermission: "session/request_permission";
28
+ readonly elicitationCreate: "elicitation/create";
27
29
  };
28
30
  /** JSON-RPC 错误码(规范沿用 JSON-RPC 2.0;`-32000` 为 ACP 的 auth_required)。 */
29
31
  export declare const RPC_ERRORS: {
@@ -76,6 +78,8 @@ export interface AcpClientCapabilities {
76
78
  writeTextFile?: boolean;
77
79
  };
78
80
  terminal?: boolean;
81
+ /** 能接 `elicitation/create`(存在即支持)。 */
82
+ elicitation?: Record<string, unknown>;
79
83
  }
80
84
  export interface AcpInitializeParams {
81
85
  protocolVersion: number;
@@ -149,6 +153,7 @@ export interface AcpNewSessionParams {
149
153
  export interface AcpNewSessionResult {
150
154
  sessionId: string;
151
155
  modes?: AcpSessionModeState | null;
156
+ configOptions?: AcpSessionConfigOption[] | null;
152
157
  }
153
158
  export interface AcpLoadSessionParams {
154
159
  sessionId: string;
@@ -157,6 +162,84 @@ export interface AcpLoadSessionParams {
157
162
  }
158
163
  export interface AcpLoadSessionResult {
159
164
  modes?: AcpSessionModeState | null;
165
+ configOptions?: AcpSessionConfigOption[] | null;
166
+ }
167
+ /** 配置项里一个可选值。 */
168
+ export interface AcpConfigSelectOption {
169
+ value: string;
170
+ name: string;
171
+ description?: string | null;
172
+ }
173
+ /** 规范允许把可选值分组。 */
174
+ export interface AcpConfigSelectGroup {
175
+ group: string;
176
+ name: string;
177
+ options: AcpConfigSelectOption[];
178
+ }
179
+ /** `session/new|load|resume` 答的 `configOptions[]` 的一项(目前规范只有 `select`)。 */
180
+ export interface AcpSessionConfigOption {
181
+ id: string;
182
+ name: string;
183
+ description?: string | null;
184
+ /** `mode` / `model` / `thought_level`,或 Agent 自己的。 */
185
+ category?: string | null;
186
+ type: "select" | (string & {});
187
+ currentValue: string;
188
+ options: (AcpConfigSelectOption | AcpConfigSelectGroup)[];
189
+ }
190
+ export interface AcpSetConfigOptionParams {
191
+ sessionId: string;
192
+ configId: string;
193
+ value: string;
194
+ }
195
+ /** 答复带全部配置项的新状态(改一项可能连带别的)。 */
196
+ export interface AcpSetConfigOptionResult {
197
+ configOptions?: AcpSessionConfigOption[] | null;
198
+ }
199
+ /** 表单里的一个字段:扁平的原始类型,规范只允许这几种。 */
200
+ export type AcpElicitationField = {
201
+ type: "string";
202
+ title?: string;
203
+ description?: string;
204
+ enum?: string[];
205
+ enumNames?: string[];
206
+ format?: string;
207
+ minLength?: number;
208
+ maxLength?: number;
209
+ default?: string;
210
+ } | {
211
+ type: "number" | "integer";
212
+ title?: string;
213
+ description?: string;
214
+ minimum?: number;
215
+ maximum?: number;
216
+ default?: number;
217
+ } | {
218
+ type: "boolean";
219
+ title?: string;
220
+ description?: string;
221
+ default?: boolean;
222
+ };
223
+ export interface AcpElicitationSchema {
224
+ type: "object";
225
+ properties: Record<string, AcpElicitationField>;
226
+ required?: string[];
227
+ }
228
+ /** Agent 发来的 `elicitation/create` 参数;未知字段保留。 */
229
+ export interface AcpElicitationParams {
230
+ sessionId?: string;
231
+ message: string;
232
+ /** `form`(缺省)或 `url`。 */
233
+ mode?: "form" | "url" | (string & {});
234
+ requestedSchema?: AcpElicitationSchema;
235
+ url?: string;
236
+ [key: string]: unknown;
237
+ }
238
+ export type AcpElicitationAction = "accept" | "decline" | "cancel";
239
+ export interface AcpElicitationResult {
240
+ action: AcpElicitationAction;
241
+ /** 只随 `accept`。 */
242
+ content?: Record<string, string | number | boolean>;
160
243
  }
161
244
  export type AcpResumeSessionParams = AcpLoadSessionParams;
162
245
  export type AcpResumeSessionResult = AcpLoadSessionResult;
@@ -2,8 +2,8 @@
2
2
  * ACP(Agent Client Protocol)v1 的子集类型(docs/wave5-plan.md §5.1,D14)。[W5-E]
3
3
  *
4
4
  * 手写、零依赖;只收 ama 作为客户端(驱动外部 Agent)与服务端(`ama --mode acp`)两侧用到的部分:
5
- * `initialize`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode`、
6
- * `session/update`(含 `usage_update`)、`session/request_permission`。字段名与规范一致(camelCase),
5
+ * `initialize`、`session/new|load|resume|list|close`、`session/prompt|cancel|set_mode|set_config_option`、
6
+ * `session/update`(含 `usage_update`)、`session/request_permission`、`elicitation/create`。字段名与规范一致(camelCase),
7
7
  * 未知字段一律保留不报错;`_meta` 不解释。
8
8
  *
9
9
  * 不声明 `fs` / `terminal` 客户端能力(与 Armadra Q5 一致):外部 Agent 自己读写、自己跑命令,
@@ -21,8 +21,10 @@ export const ACP_METHODS = {
21
21
  sessionPrompt: "session/prompt",
22
22
  sessionCancel: "session/cancel",
23
23
  sessionSetMode: "session/set_mode",
24
+ sessionSetConfigOption: "session/set_config_option",
24
25
  sessionUpdate: "session/update",
25
26
  requestPermission: "session/request_permission",
27
+ elicitationCreate: "elicitation/create",
26
28
  };
27
29
  /** JSON-RPC 错误码(规范沿用 JSON-RPC 2.0;`-32000` 为 ACP 的 auth_required)。 */
28
30
  export const RPC_ERRORS = {
package/docs/acp.md CHANGED
@@ -67,12 +67,19 @@ stdin 关闭后等已开始的运行结束再退出(0);SIGINT / SIGTERM
67
67
  - 开会话(`newSession` / `resumeSession` / `loadSession`)的 `mcpServers` 缺省为空数组;宿主可传第三个参数
68
68
  `{ mcpServers }`(如 stdio 的 `{ name, command, args, env: [{ name, value }] }`),原样转发给 Agent。
69
69
  `AcpClient.features.mcpServers === true` 表示支持(旧版没有 `features`)。ama 自己作客户端时仍不传。
70
+ - `elicitation/create`(Agent 向人要结构化输入):构造参数给了 `onElicitation(params, signal)` 才在 `initialize` 声明
71
+ `clientCapabilities.elicitation` 并接这个请求;没给时不声明、请求回 method not found(与旧版相同)。答复收成
72
+ `{ action: "accept" | "decline" | "cancel", content? }`(`content` 只随 accept,不认识的动作当 cancel);`cancel(sessionId)`
73
+ 与连接关闭时挂起的一律回 `{ action: "cancel" }`。ama 自己作客户端时不给处理器,从不替人填表。
74
+ - 会话配置项:开会话(new / load / resume)答的 `configOptions` 原样交回;`setConfigOption(sessionId, configId, value)` 发
75
+ `session/set_config_option`,答复是全部配置项的新状态。
76
+ - `AcpClient.features`:`{ mcpServers, elicitation, configOptions }`,宿主据此做特性检测。
70
77
 
71
78
  `AcpDriver` 在客户端之上实现驱动契约(`AgentDriver`):续接优先 `session/resume`,其次 `session/load`(回放的历史丢弃),都不支持就新开并提示;按 ama 模式 `session/set_mode`,只读模式找不到对应模式 id 时拒绝启动。
72
79
 
73
80
  ## 测试替身
74
81
 
75
- `runFakeAcpAgent(input, output)` 是进程内的假 ACP Agent,`fakeAcpAgentPath()` 是它的可执行入口(`node <path> [--minimal]`)。行为由提示里的标记决定:`[permission]`(请求权限,四个选项)、`[slow]`(等到 cancel)、`[plan]`、`[think]`、`[refuse]`,其余回 `echo: <文本>`。`--minimal` 不声明 resume / load / list / close,也不给模式,用来测降级路径。
82
+ `runFakeAcpAgent(input, output)` 是进程内的假 ACP Agent,`fakeAcpAgentPath()` 是它的可执行入口(`node <path> [--minimal]`)。行为由提示里的标记决定:`[permission]`(请求权限,四个选项)、`[slow]`(等到 cancel)、`[plan]`、`[think]`、`[refuse]`,其余回 `echo: <文本>`。`--minimal` 不声明 resume / load / list / close,也不给模式,用来测降级路径。另有 `[elicit]`(发 `elicitation/create`,客户端没声明能力时回 `elicit: unsupported`)、`[model]`、`[env NAME]`(只回值的 sha256)三个标记;`--config-options`(进程内 `{ configOptions: true }`)让开会话答一个 `model` 配置项并接 `session/set_config_option`。
76
83
 
77
84
  黄金记录在 `test/fixtures/acp/`:`driver-{allow,reject,cancel}.jsonl`(ama 驱动假 Agent 的三条路径)与 `mode-prompt.jsonl`(`ama --mode acp` 一轮往返)。`UPDATE_GOLDEN=1` 重写。
78
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@armadra/agent",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "A coding and coordination agent that runs standalone or embedded in Armadra",
5
5
  "type": "module",
6
6
  "keywords": [