@armadra/agent 0.6.8 → 0.7.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 (79) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CHANGELOG.zh-CN.md +43 -0
  3. package/README.md +4 -3
  4. package/README.zh-CN.md +1 -1
  5. package/dist/acp.d.ts +2 -2
  6. package/dist/acp.js +2 -2
  7. package/dist/ai/apis/shared.d.ts +7 -1
  8. package/dist/ai/apis/shared.js +10 -0
  9. package/dist/ai/fake/fake-provider.js +5 -1
  10. package/dist/ai/fake/fake-script.d.ts +5 -2
  11. package/dist/ai/fake/fake-script.js +6 -2
  12. package/dist/ai/types.d.ts +2 -1
  13. package/dist/bundle/ama.cjs +48757 -47456
  14. package/dist/cli/args.d.ts +6 -0
  15. package/dist/cli/args.js +46 -0
  16. package/dist/cli/bootstrap.js +13 -2
  17. package/dist/cli/compose-events.d.ts +10 -0
  18. package/dist/cli/compose-events.js +97 -0
  19. package/dist/cli/compose-session.d.ts +25 -4
  20. package/dist/cli/compose-session.js +130 -103
  21. package/dist/cli/compose.js +6 -1
  22. package/dist/cli/runtime.d.ts +3 -1
  23. package/dist/cli/subcommands/auth.d.ts +13 -1
  24. package/dist/cli/subcommands/auth.js +28 -1
  25. package/dist/drivers/acp/client.d.ts +7 -2
  26. package/dist/drivers/acp/client.js +10 -1
  27. package/dist/drivers/acp/driver.d.ts +15 -4
  28. package/dist/drivers/acp/driver.js +110 -33
  29. package/dist/drivers/acp/testing/fake-agent-main.d.ts +1 -1
  30. package/dist/drivers/acp/testing/fake-agent-main.js +9 -2
  31. package/dist/drivers/acp/testing/fake-agent.d.ts +17 -2
  32. package/dist/drivers/acp/testing/fake-agent.js +108 -25
  33. package/dist/drivers/acp/types.d.ts +76 -12
  34. package/dist/drivers/acp/types.js +11 -3
  35. package/dist/drivers/jsonrpc.d.ts +13 -3
  36. package/dist/drivers/jsonrpc.js +40 -7
  37. package/dist/drivers/turn.d.ts +15 -2
  38. package/dist/drivers/turn.js +33 -4
  39. package/dist/i18n/catalog.d.ts +59 -16
  40. package/dist/i18n/catalog.js +4 -1
  41. package/dist/i18n/messages/acp.d.ts +125 -0
  42. package/dist/i18n/messages/acp.js +126 -0
  43. package/dist/i18n/messages/auth.d.ts +2 -0
  44. package/dist/i18n/messages/auth.js +4 -2
  45. package/dist/i18n/messages/cli-args.d.ts +2 -0
  46. package/dist/i18n/messages/cli-args.js +2 -0
  47. package/dist/i18n/messages/cli.d.ts +4 -0
  48. package/dist/i18n/messages/cli.js +2 -0
  49. package/dist/i18n/messages/print.d.ts +3 -33
  50. package/dist/i18n/messages/print.js +3 -33
  51. package/dist/modes/acp/acp-auth-gate.d.ts +40 -0
  52. package/dist/modes/acp/acp-auth-gate.js +201 -0
  53. package/dist/modes/acp/acp-config.d.ts +43 -0
  54. package/dist/modes/acp/acp-config.js +151 -0
  55. package/dist/modes/acp/acp-connection.d.ts +29 -0
  56. package/dist/modes/acp/acp-connection.js +37 -0
  57. package/dist/modes/acp/acp-events.d.ts +59 -23
  58. package/dist/modes/acp/acp-events.js +154 -88
  59. package/dist/modes/acp/acp-mode.d.ts +13 -2
  60. package/dist/modes/acp/acp-mode.js +23 -8
  61. package/dist/modes/acp/acp-server.d.ts +59 -32
  62. package/dist/modes/acp/acp-server.js +322 -128
  63. package/dist/modes/acp/acp-sessions.d.ts +80 -0
  64. package/dist/modes/acp/acp-sessions.js +157 -0
  65. package/dist/modes/acp/acp-tool-text.d.ts +23 -0
  66. package/dist/modes/acp/acp-tool-text.js +83 -0
  67. package/dist/modes/print/json-event.d.ts +2 -1
  68. package/dist/modes/print/json-event.js +6 -1
  69. package/dist/tools/edit.d.ts +6 -2
  70. package/dist/tools/edit.js +19 -2
  71. package/dist/tools/types.d.ts +11 -0
  72. package/dist/tools/types.js +2 -0
  73. package/dist/tools/write.d.ts +1 -0
  74. package/dist/tools/write.js +21 -3
  75. package/docs/acp.md +143 -29
  76. package/docs/agents.md +2 -0
  77. package/docs/codemode.md +1 -1
  78. package/docs/en/acp.md +192 -0
  79. package/package.json +1 -1
@@ -3,15 +3,19 @@
3
3
  * `ama auth login|logout|status chatgpt`(docs/wave6-plan.md §4.2)。[W6-O] 实现在 auth/chatgpt/cli.ts。
4
4
  *
5
5
  * - set:key 从 stdin 读取(TTY 下不回显),不经命令行参数,避免进 shell 历史;写 auth.json 0600。
6
+ * [ACP-A] 不给 provider 且 stdin 是 TTY 时先用方向键选内置的需 key 供应商(ACP 终端登录方法
7
+ * `ama auth set` 就这样跑);非 TTY 仍是用法错误。
6
8
  * - list:只列供应商与 key 形态(字面量 / `!command` / `$ENV` 引用 / oauth 的 flavor 与计划),从不输出 key 或 token。
7
9
  * - `--auth-file <path>` 改变目标文件(缺省 `<configDir>/auth.json`)。
8
10
  */
9
11
  import { resolve } from "node:path";
12
+ import { BUILTIN_PROVIDERS } from "../../ai/providers/builtin.js";
10
13
  import { runLogin, runLogout, runStatus } from "../../auth/chatgpt/cli.js";
11
14
  import { PROVIDER_ID_PATTERN, defaultAuthFilePath, describeAuthFile, readAuthFile, removeAuthKey, setAuthKey, } from "../../config/auth-file.js";
12
15
  import { resolveConfigDir } from "../../config/paths.js";
13
16
  import { msg } from "../../i18n/index.js";
14
17
  import { parseSubArgs, UsageError } from "../args.js";
18
+ import { canPromptChoice, promptChoice } from "../choice-prompt.js";
15
19
  import { ExitCode } from "../exit-codes.js";
16
20
  /** 用法文本(按当前语言)。 */
17
21
  export function authUsage() {
@@ -43,6 +47,25 @@ function providerArg(positionals, action) {
43
47
  throw new UsageError(m.badProvider(provider));
44
48
  return provider;
45
49
  }
50
+ /** `auth set` 选择器列的供应商:内置、需要 API key、有 key 环境变量(不含 OAuth 的 chatgpt)。 */
51
+ export function keyProviders() {
52
+ return BUILTIN_PROVIDERS.filter((p) => p.requiresApiKey && (p.envKeys ?? []).length > 0).map((p) => ({ id: p.id, name: p.name }));
53
+ }
54
+ /** `auth set` 的 provider:给了就校验;没给且 stdin 是 TTY → 选择器(取消返回 undefined)。 */
55
+ async function setProvider(positionals, io, deps) {
56
+ if (positionals.length > 1)
57
+ return providerArg(positionals, "set");
58
+ const m = msg().auth;
59
+ const choose = deps.chooseProvider ??
60
+ (canPromptChoice()
61
+ ? (question, options) => promptChoice({ question, options, env: io.env })
62
+ : undefined);
63
+ if (!io.stdinIsTTY || choose === undefined)
64
+ throw new UsageError(m.needProvider("set"));
65
+ const providers = keyProviders();
66
+ const index = await choose(m.pickProvider, providers.map((p) => ({ label: `${p.name} (${p.id})` })));
67
+ return index === undefined ? undefined : providers[index]?.id;
68
+ }
46
69
  /** 取第一行并去掉首尾空白;stdin 为空返回 ""。 */
47
70
  export function extractKey(raw) {
48
71
  const line = raw.split(/\r?\n/).find((l) => l.trim() !== "") ?? "";
@@ -73,7 +96,11 @@ export async function runAuth(argv, io, deps = {}) {
73
96
  case "status":
74
97
  return runStatus(cli, io, deps);
75
98
  case "set": {
76
- const provider = providerArg(positionals, "set");
99
+ const provider = await setProvider(positionals, io, deps);
100
+ if (provider === undefined) {
101
+ io.stderr(msg().subcommands.common.cancelled);
102
+ return ExitCode.Ok;
103
+ }
77
104
  if (io.stdinIsTTY)
78
105
  io.stderr(m.setPrompt(provider));
79
106
  const key = extractKey(await io.readStdin());
@@ -14,9 +14,12 @@
14
14
  * 没给时线路与旧版相同(不声明,Agent 发来的请求回 method not found)。`cancel(sessionId)`、连接关闭时
15
15
  * 挂起的 elicitation 一律回 `{ action: "cancel" }`;ama 从不替人填表。
16
16
  * - `setConfigOption(sessionId, configId, value)` 发 `session/set_config_option`;开会话答的 `configOptions`
17
- * 原样交回。
17
+ * 原样交回。`initialize` 声明 `clientCapabilities.session.configOptions: {}`(只认 select,不声明 boolean);
18
+ * 不声明 `auth.terminal`(ama 作客户端没有可借给 Agent 的交互终端)。[ACP-D]
19
+ * - 线路两侧开 `$/cancel_request`:本端请求的 `signal` abort 时通知 Agent;Agent 撤回挂起的
20
+ * `session/request_permission` / `elicitation/create` 时处理器的 `signal` abort,回 cancelled / cancel。[ACP-D]
18
21
  */
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";
22
+ 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";
20
23
  export interface AcpClientHandlers {
21
24
  onUpdate?(notification: AcpSessionNotification): void;
22
25
  /** `signal`:该会话被 cancel、连接关闭时 abort(此时应尽快回 cancelled)。 */
@@ -60,6 +63,8 @@ export declare class AcpClient {
60
63
  get isOpen(): boolean;
61
64
  get agentCapabilities(): AcpAgentCapabilities;
62
65
  get agentInfo(): AcpImplementationInfo | undefined;
66
+ /** `initialize` 答的认证方法(开会话回 -32000 时给人看)。 */
67
+ get authMethods(): readonly AcpAuthMethod[];
63
68
  initialize(signal?: AbortSignal): Promise<AcpInitializeResult>;
64
69
  supportsResume(): boolean;
65
70
  supportsLoad(): boolean;
@@ -14,7 +14,10 @@
14
14
  * 没给时线路与旧版相同(不声明,Agent 发来的请求回 method not found)。`cancel(sessionId)`、连接关闭时
15
15
  * 挂起的 elicitation 一律回 `{ action: "cancel" }`;ama 从不替人填表。
16
16
  * - `setConfigOption(sessionId, configId, value)` 发 `session/set_config_option`;开会话答的 `configOptions`
17
- * 原样交回。
17
+ * 原样交回。`initialize` 声明 `clientCapabilities.session.configOptions: {}`(只认 select,不声明 boolean);
18
+ * 不声明 `auth.terminal`(ama 作客户端没有可借给 Agent 的交互终端)。[ACP-D]
19
+ * - 线路两侧开 `$/cancel_request`:本端请求的 `signal` abort 时通知 Agent;Agent 撤回挂起的
20
+ * `session/request_permission` / `elicitation/create` 时处理器的 `signal` abort,回 cancelled / cancel。[ACP-D]
18
21
  */
19
22
  import { JsonRpcPeer, RpcError } from "../jsonrpc.js";
20
23
  import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "./types.js";
@@ -61,6 +64,7 @@ export class AcpClient {
61
64
  this.peer = new JsonRpcPeer({
62
65
  input: options.input,
63
66
  output: options.output,
67
+ cancelRequests: true,
64
68
  onRequest: (method, params, ctx) => this.onRequest(method, params, ctx.signal),
65
69
  onNotification: (method, params) => {
66
70
  if (method === ACP_METHODS.sessionUpdate)
@@ -92,12 +96,17 @@ export class AcpClient {
92
96
  get agentInfo() {
93
97
  return this.initResult?.agentInfo;
94
98
  }
99
+ /** `initialize` 答的认证方法(开会话回 -32000 时给人看)。 */
100
+ get authMethods() {
101
+ return this.initResult?.authMethods ?? [];
102
+ }
95
103
  async initialize(signal) {
96
104
  const result = await this.peer.request(ACP_METHODS.initialize, {
97
105
  protocolVersion: ACP_PROTOCOL_VERSION,
98
106
  clientCapabilities: {
99
107
  fs: { readTextFile: false, writeTextFile: false },
100
108
  terminal: false,
109
+ session: { configOptions: {} },
101
110
  ...(this.options.onElicitation !== undefined ? { elicitation: {} } : {}),
102
111
  },
103
112
  ...(this.options.clientInfo !== undefined ? { clientInfo: this.options.clientInfo } : {}),
@@ -4,22 +4,33 @@
4
4
  * open:起进程 → `initialize` → 续聊优先 `session/resume`(不回放),其次 `session/load`
5
5
  * (回放的历史通知在 open 期间丢弃),都不支持则新开并发 notice「已新开」→ 按 ama 模式
6
6
  * `session/set_mode`(只读模式没有映射时拒绝启动,避免以 Agent 缺省模式跑写操作)。
7
- * prompt:`session/update` → DriverEvent;`session/request_permission` → hooks.onPermission
8
- * (只交给人);回合以 `session/prompt` 的 stopReason 结束。
9
- * cancel:`session/cancel`,挂起的权限请求回 cancelled,15 s 内无回合结束 → 关 stdin → 杀进程树。
7
+ * Agent 不给 `modes` 时退到 `configOptions` 里 category `mode` 的选择项,经 `session/set_config_option` 设置;
8
+ * 开会话回 -32000(需要登录)时报 `agent_auth_required`,文案列出 Agent 的认证方法(terminal 型附命令)。
9
+ * prompt:`session/update` → DriverEvent(diff 内容的路径计入 filesTouched);`session/request_permission`
10
+ * → hooks.onPermission(只交给人;Agent 以 `$/cancel_request` 撤回时 signal abort);回合以
11
+ * `session/prompt` 的 stopReason 结束。
12
+ * cancel:`session/cancel`,挂起的权限请求回 cancelled,15 s 内无回合结束 → 关 stdin → 杀进程树;
13
+ * 取消后 `session/prompt` 回 -32800(请求被撤回)也当作 cancelled。
10
14
  */
15
+ import { AmaError } from "../../errors.js";
11
16
  import type { ContentBlock } from "../../ai/types.js";
12
17
  import type { PermissionMode } from "../../permissions/types.js";
13
18
  import { type DriverDeps } from "../base.js";
14
19
  import type { CatalogCandidate } from "../catalog.js";
15
20
  import { TurnCollector } from "../turn.js";
16
21
  import type { AgentDriver, DriverOpenOptions, DriverProbe, DriverSession } from "../types.js";
17
- import type { AcpContentBlock, AcpSessionModeState, AcpSessionUpdate } from "./types.js";
22
+ import { type AcpAuthMethod, type AcpContentBlock, type AcpSessionConfigOption, type AcpSessionModeState, type AcpSessionUpdate } from "./types.js";
18
23
  export declare function toAcpContent(blocks: readonly ContentBlock[], images: boolean): AcpContentBlock[];
19
24
  /** session/update → DriverEvent(用户消息回放、命令表等忽略)。 */
20
25
  export declare function pushAcpUpdate(update: AcpSessionUpdate, turn: TurnCollector): void;
21
26
  /** ama 模式 → 该 Agent 的模式 id(显式映射 > 同名)。 */
22
27
  export declare function pickModeId(mode: PermissionMode, candidate: CatalogCandidate, state: AcpSessionModeState | null | undefined): string | undefined;
28
+ /** `configOptions` 里 category `mode` 的选择项(没有 `modes` 的 Agent 用它表达模式)。 */
29
+ export declare function modeConfigOption(options: readonly AcpSessionConfigOption[] | null | undefined): AcpSessionConfigOption | undefined;
30
+ /** 与 {@link pickModeId} 同一映射,在 mode 类配置项的可选值里找(分组的展开)。 */
31
+ export declare function pickConfigModeValue(mode: PermissionMode, candidate: CatalogCandidate, option: AcpSessionConfigOption | undefined): string | undefined;
32
+ /** 开会话回 -32000 → `agent_auth_required`,列出 `initialize` 给的认证方法。 */
33
+ export declare function authRequiredError(agentId: string, methods: readonly AcpAuthMethod[], command: readonly string[], cause: unknown): AmaError;
23
34
  export declare class AcpDriver implements AgentDriver {
24
35
  readonly agentId: string;
25
36
  private readonly candidate;
@@ -4,15 +4,21 @@
4
4
  * open:起进程 → `initialize` → 续聊优先 `session/resume`(不回放),其次 `session/load`
5
5
  * (回放的历史通知在 open 期间丢弃),都不支持则新开并发 notice「已新开」→ 按 ama 模式
6
6
  * `session/set_mode`(只读模式没有映射时拒绝启动,避免以 Agent 缺省模式跑写操作)。
7
- * prompt:`session/update` → DriverEvent;`session/request_permission` → hooks.onPermission
8
- * (只交给人);回合以 `session/prompt` 的 stopReason 结束。
9
- * cancel:`session/cancel`,挂起的权限请求回 cancelled,15 s 内无回合结束 → 关 stdin → 杀进程树。
7
+ * Agent 不给 `modes` 时退到 `configOptions` 里 category `mode` 的选择项,经 `session/set_config_option` 设置;
8
+ * 开会话回 -32000(需要登录)时报 `agent_auth_required`,文案列出 Agent 的认证方法(terminal 型附命令)。
9
+ * prompt:`session/update` → DriverEvent(diff 内容的路径计入 filesTouched);`session/request_permission`
10
+ * → hooks.onPermission(只交给人;Agent 以 `$/cancel_request` 撤回时 signal abort);回合以
11
+ * `session/prompt` 的 stopReason 结束。
12
+ * cancel:`session/cancel`,挂起的权限请求回 cancelled,15 s 内无回合结束 → 关 stdin → 杀进程树;
13
+ * 取消后 `session/prompt` 回 -32800(请求被撤回)也当作 cancelled。
10
14
  */
11
15
  import { AmaError } from "../../errors.js";
12
16
  import { armCancelWatchdog, isReadOnlyMode, probeCandidate, spawnOf, } from "../base.js";
13
17
  import { stderrHint } from "../process.js";
14
18
  import { TurnCollector, oneLine } from "../turn.js";
19
+ import { RpcError } from "../jsonrpc.js";
15
20
  import { AcpClient } from "./client.js";
21
+ import { RPC_ERRORS, } from "./types.js";
16
22
  import { AMA_VERSION } from "../../version.js";
17
23
  import { msg } from "../../i18n/index.js";
18
24
  export function toAcpContent(blocks, images) {
@@ -28,6 +34,14 @@ export function toAcpContent(blocks, images) {
28
34
  function locationsOf(locations) {
29
35
  return locations === undefined ? undefined : locations.map((l) => l.path);
30
36
  }
37
+ /** 工具内容里 diff 的路径。 */
38
+ function diffPathsOf(content) {
39
+ const paths = [];
40
+ for (const item of content ?? [])
41
+ if (item.type === "diff" && typeof item.path === "string" && !paths.includes(item.path))
42
+ paths.push(item.path);
43
+ return paths;
44
+ }
31
45
  /** session/update → DriverEvent(用户消息回放、命令表等忽略)。 */
32
46
  export function pushAcpUpdate(update, turn) {
33
47
  switch (update.sessionUpdate) {
@@ -42,7 +56,11 @@ export function pushAcpUpdate(update, turn) {
42
56
  case "tool_call":
43
57
  case "tool_call_update": {
44
58
  const known = turn.tool(update.toolCallId);
45
- const locations = locationsOf(update.locations);
59
+ const diffs = diffPathsOf(update.content);
60
+ let locations = locationsOf(update.locations);
61
+ // diff 的路径并入 locations(没给 locations 时并入已知的)
62
+ if (diffs.length > 0)
63
+ locations = [...new Set([...(locations ?? known?.locations ?? []), ...diffs])];
46
64
  turn.push({
47
65
  type: "tool_call",
48
66
  id: update.toolCallId,
@@ -51,6 +69,7 @@ export function pushAcpUpdate(update, turn) {
51
69
  status: update.status ?? known?.status ?? "pending",
52
70
  ...(locations !== undefined ? { locations } : {}),
53
71
  });
72
+ turn.noteDiff(update.toolCallId, diffs);
54
73
  return;
55
74
  }
56
75
  case "plan":
@@ -71,13 +90,43 @@ export function pushAcpUpdate(update, turn) {
71
90
  return;
72
91
  }
73
92
  }
74
- /** ama 模式 → 该 Agent 的模式 id(显式映射 > 同名)。 */
75
- export function pickModeId(mode, candidate, state) {
93
+ /** ama 模式在 Agent 那边想要的模式 id,按优先级(显式映射 > 同名)。 */
94
+ function wantedModeIds(mode, candidate) {
76
95
  const wanted = [candidate.modes?.[mode] ?? mode];
77
96
  // allowlist 在外部 Agent 那边没有等价物:退到只读的 plan
78
97
  if (mode === "allowlist")
79
98
  wanted.push(candidate.modes?.plan ?? "plan");
80
- return wanted.find((id) => state?.availableModes.some((m) => m.id === id) === true);
99
+ return wanted;
100
+ }
101
+ /** ama 模式 → 该 Agent 的模式 id(显式映射 > 同名)。 */
102
+ export function pickModeId(mode, candidate, state) {
103
+ return wantedModeIds(mode, candidate).find((id) => state?.availableModes.some((m) => m.id === id) === true);
104
+ }
105
+ /** `configOptions` 里 category `mode` 的选择项(没有 `modes` 的 Agent 用它表达模式)。 */
106
+ export function modeConfigOption(options) {
107
+ return options?.find((o) => o.category === "mode" && o.type === "select");
108
+ }
109
+ /** 与 {@link pickModeId} 同一映射,在 mode 类配置项的可选值里找(分组的展开)。 */
110
+ export function pickConfigModeValue(mode, candidate, option) {
111
+ if (option === undefined)
112
+ return undefined;
113
+ const values = option.options.flatMap((o) => "options" in o ? o.options.map((x) => x.value) : [o.value]);
114
+ return wantedModeIds(mode, candidate).find((id) => values.includes(id));
115
+ }
116
+ /** 一条认证方法给人看的样子:terminal 型附上要在终端里跑的命令。 */
117
+ function authMethodText(method, command) {
118
+ if (method.type !== "terminal")
119
+ return method.name;
120
+ const words = [...command, ...(method.args ?? [])].map((w) => /[\s"']/.test(w) ? JSON.stringify(w) : w);
121
+ return msg().acp.client.authTerminal(method.name, words.join(" "));
122
+ }
123
+ /** 开会话回 -32000 → `agent_auth_required`,列出 `initialize` 给的认证方法。 */
124
+ export function authRequiredError(agentId, methods, command, cause) {
125
+ const m = msg().acp.client;
126
+ const text = methods.length === 0
127
+ ? m.authNoMethods(agentId)
128
+ : m.authRequired(agentId, methods.map((method) => authMethodText(method, command)));
129
+ return new AmaError("agent_auth_required", text, { cause });
81
130
  }
82
131
  export class AcpDriver {
83
132
  agentId;
@@ -104,8 +153,9 @@ export class AcpDriver {
104
153
  env: options.env,
105
154
  });
106
155
  const session = new AcpDriverSession(this.agentId, transport, this.deps);
156
+ const command = [probed.path ?? this.candidate.program, ...this.candidate.args];
107
157
  try {
108
- await session.start(options, this.candidate);
158
+ await session.start(options, this.candidate, command);
109
159
  }
110
160
  catch (error) {
111
161
  await session.close();
@@ -129,6 +179,8 @@ class AcpDriverSession {
129
179
  running;
130
180
  closed = false;
131
181
  notices = [];
182
+ /** 本回合已发过 cancel。 */
183
+ cancelling = false;
132
184
  constructor(agentId, transport, deps) {
133
185
  this.agentId = agentId;
134
186
  this.transport = transport;
@@ -137,7 +189,8 @@ class AcpDriverSession {
137
189
  get sessionId() {
138
190
  return this.id;
139
191
  }
140
- async start(options, candidate) {
192
+ /** `command`:启动 Agent 的程序与参数(terminal 型认证方法的提示用)。 */
193
+ async start(options, candidate, command) {
141
194
  this.client = new AcpClient({
142
195
  input: this.transport.stdout,
143
196
  output: this.transport.stdin,
@@ -148,43 +201,60 @@ class AcpDriverSession {
148
201
  });
149
202
  const signal = options.signal;
150
203
  await this.client.initialize(signal);
151
- let modes;
152
- if (options.resume !== undefined) {
153
- if (this.client.supportsResume()) {
154
- modes = (await this.client.resumeSession(options.resume, options.cwd, signal)).modes;
155
- this.id = options.resume;
156
- }
157
- else if (this.client.supportsLoad()) {
158
- this.replaying = true;
159
- try {
160
- modes = (await this.client.loadSession(options.resume, options.cwd, signal)).modes;
161
- }
162
- finally {
163
- this.replaying = false;
164
- }
165
- this.id = options.resume;
166
- }
167
- else {
168
- this.notices.push(msg().drivers.agent.resumeUnsupported(this.agentId));
169
- }
204
+ let opened;
205
+ try {
206
+ opened = await this.openSession(options);
170
207
  }
171
- if (this.id === "") {
172
- const created = await this.client.newSession(options.cwd, signal);
173
- this.id = created.sessionId;
174
- modes = created.modes;
208
+ catch (error) {
209
+ if (error instanceof RpcError && error.code === RPC_ERRORS.authRequired)
210
+ throw authRequiredError(this.agentId, this.client.authMethods, command, error);
211
+ throw error;
175
212
  }
213
+ const modes = opened.modes;
176
214
  const modeId = pickModeId(options.mode, candidate, modes);
215
+ const modeOption = modeId === undefined ? modeConfigOption(opened.configOptions) : undefined;
216
+ const configMode = pickConfigModeValue(options.mode, candidate, modeOption);
177
217
  if (modeId !== undefined) {
178
218
  if (modes?.currentModeId !== modeId)
179
219
  await this.client.setMode(this.id, modeId);
180
220
  }
221
+ else if (modeOption !== undefined && configMode !== undefined) {
222
+ if (modeOption.currentValue !== configMode)
223
+ await this.client.setConfigOption(this.id, modeOption.id, configMode);
224
+ }
181
225
  else if (isReadOnlyMode(options.mode)) {
182
226
  throw new AmaError("agent_mode_unsupported", `${this.agentId} has no read-only mode matching ama's "${options.mode}"; its default mode cannot stand in for it`);
183
227
  }
184
- else if (modes != null) {
228
+ else if (modes != null || modeOption !== undefined) {
185
229
  this.notices.push(msg().drivers.agent.noMatchingMode(this.agentId, options.mode));
186
230
  }
187
231
  }
232
+ /** 续聊优先 resume(不回放),其次 load(回放丢弃),都不行就新开;设好 `this.id`。 */
233
+ async openSession(options) {
234
+ const signal = options.signal;
235
+ if (options.resume !== undefined) {
236
+ if (this.client.supportsResume()) {
237
+ const resumed = await this.client.resumeSession(options.resume, options.cwd, signal);
238
+ this.id = options.resume;
239
+ return resumed;
240
+ }
241
+ if (this.client.supportsLoad()) {
242
+ this.replaying = true;
243
+ try {
244
+ const loaded = await this.client.loadSession(options.resume, options.cwd, signal);
245
+ this.id = options.resume;
246
+ return loaded;
247
+ }
248
+ finally {
249
+ this.replaying = false;
250
+ }
251
+ }
252
+ this.notices.push(msg().drivers.agent.resumeUnsupported(this.agentId));
253
+ }
254
+ const created = await this.client.newSession(options.cwd, signal);
255
+ this.id = created.sessionId;
256
+ return created;
257
+ }
188
258
  async prompt(content, hooks) {
189
259
  if (this.closed)
190
260
  throw new AmaError("agent_closed", `${this.agentId} session is closed`);
@@ -195,6 +265,7 @@ class AcpDriverSession {
195
265
  turn.push({ type: "notice", level: "info", text });
196
266
  this.hooks = hooks;
197
267
  this.turn = turn;
268
+ this.cancelling = false;
198
269
  const images = this.client.supportsImages();
199
270
  if (!images && content.some((b) => b.type === "image"))
200
271
  turn.push({
@@ -206,6 +277,11 @@ class AcpDriverSession {
206
277
  this.running = request;
207
278
  try {
208
279
  const result = await request.catch((error) => {
280
+ // 取消后 Agent 以 -32800(请求被撤回)答 session/prompt:与 stopReason cancelled 同义
281
+ if (this.cancelling &&
282
+ error instanceof RpcError &&
283
+ error.code === RPC_ERRORS.requestCancelled)
284
+ return { stopReason: "cancelled" };
209
285
  if (this.client.isOpen)
210
286
  throw error;
211
287
  const hint = stderrHint(this.transport);
@@ -233,6 +309,7 @@ class AcpDriverSession {
233
309
  const running = this.running;
234
310
  if (running === undefined || this.closed)
235
311
  return;
312
+ this.cancelling = true;
236
313
  await this.client.cancel(this.id).catch(() => undefined);
237
314
  armCancelWatchdog(this.transport, running.catch(() => undefined), this.deps);
238
315
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal] [--config-options]`。[W5-E]
2
+ * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal] [--config-options] [--config-only] [--auth-required]`。[W5-E]
3
3
  * 行为见 fake-agent.ts;stdin 结束即退出。
4
4
  */
5
5
  export {};
@@ -1,8 +1,15 @@
1
1
  /**
2
- * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal] [--config-options]`。[W5-E]
2
+ * 假 ACP Agent 的进程入口:`node dist/drivers/acp/testing/fake-agent-main.js [--minimal] [--config-options] [--config-only] [--auth-required]`。[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
7
  const configOptions = process.argv.includes("--config-options");
8
- void runFakeAcpAgent(process.stdin, process.stdout, { minimal, configOptions }).then(() => process.exit(0));
8
+ const configOnly = process.argv.includes("--config-only");
9
+ const authRequired = process.argv.includes("--auth-required");
10
+ void runFakeAcpAgent(process.stdin, process.stdout, {
11
+ minimal,
12
+ configOptions,
13
+ configOnly,
14
+ authRequired,
15
+ }).then(() => process.exit(0));
@@ -17,13 +17,20 @@
17
17
  * | | 没声明 `elicitation` 能力时回 `elicit: unsupported`;`cancel` 时本回合 `cancelled` |
18
18
  * | `[model]` | 回 `model <当前模型>`(`configOptions` 关着时是 `model none`) |
19
19
  * | `[env NAME]` | 回 `env NAME <值的 sha256 | absent>`(测环境变量到没到,不回显值) |
20
+ * | `[cancel-request]` | 同 `[permission]` 发权限请求,挂起 `cancelRequestMs`(缺省 2 s)后自己发 |
21
+ * | | `$/cancel_request` 撤回;`tool_call_update` failed,回 `permission withdrawn`、`end_turn` |
20
22
  *
21
- * `configOptions: true`(可执行入口 `--config-options`):开会话答一个 `model` 配置项(`small`,分组
22
- * `big` 里有 `large`),`session/set_config_option` 可改;缺省不答,线路与之前相同。
23
+ * `configOptions: true`(可执行入口 `--config-options`):开会话答一个 `model` 配置项(分组 `fast` 里有
24
+ * `small`、`big` 里有 `large`,当前 `small`),`session/set_config_option` 可改;缺省不答,线路与之前相同。
25
+ * `configOnly: true`(`--config-only`):开会话不答 `modes`,改在 `configOptions` 里给 category `mode` 的
26
+ * 选择项(id `mode`),经 `session/set_config_option` 切换;可与 `configOptions` 同开(两项都给)。
27
+ * `authRequired: true`(`--auth-required`):`initialize` 给一条 terminal 型认证方法,开会话
28
+ * (new / load / resume)一律回 -32000。
23
29
  *
24
30
  * 会话:`session/new` 发 `fake-<n>`;`session/resume` / `session/load` 接受任何 `fake-` 开头的 id
25
31
  * (load 先回放一条用户消息与一条回复);`session/list` 列本进程建过的会话。
26
32
  */
33
+ import { type AcpAuthMethod } from "../types.js";
27
34
  export interface FakeAcpAgentOptions {
28
35
  /** `initialize` 回的 agentInfo.name,缺省 `fake-acp-agent`。 */
29
36
  name?: string;
@@ -31,6 +38,14 @@ export interface FakeAcpAgentOptions {
31
38
  minimal?: boolean;
32
39
  /** 开会话答 `configOptions`(一个 `model` 选择项)并接 `session/set_config_option`。 */
33
40
  configOptions?: boolean;
41
+ /** 不答 `modes`,模式改由 `configOptions` 里 category `mode` 的选择项给出。 */
42
+ configOnly?: boolean;
43
+ /** `initialize` 给 terminal 型认证方法,开会话回 -32000。 */
44
+ authRequired?: boolean;
45
+ /** `[cancel-request]` 撤回权限请求前等多久(毫秒,缺省 2000)。 */
46
+ cancelRequestMs?: number;
34
47
  }
48
+ /** `authRequired` 时 `initialize` 给的认证方法。 */
49
+ export declare const FAKE_AUTH_METHOD: AcpAuthMethod;
35
50
  /** 在给定流上跑假 Agent;输入流结束时 resolve。 */
36
51
  export declare function runFakeAcpAgent(input: NodeJS.ReadableStream, output: NodeJS.WritableStream, options?: FakeAcpAgentOptions): Promise<void>;