@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
@@ -1,9 +1,10 @@
1
1
  /**
2
- * 消息目录:print(键名规范见 docs/i18n.md)。[W6-C0 建空壳,W6-I3 迁入 `src/modes/{print,rpc,acp}/**`]
2
+ * 消息目录:print(键名规范见 docs/i18n.md)。[W6-C0 建空壳,W6-I3 迁入 `src/modes/{print,rpc}/**`;
3
+ * ACP 的文案在 [ACP-C0] 迁到 acp.ts]
3
4
  *
4
5
  * en 是形状源;zh 用 `satisfies Messages<typeof en>`,缺键 / 多键 / 参数不符在 tsc 期报错。
5
6
  * 整句一个键、禁止片段拼接;插值写成函数,条件分支写进函数体。
6
- * RPC / ACP 的 `error` / `message` 是人读文本,宿主按 `code` 判断(docs/rpc.md);JSON 字段名不在这里。
7
+ * RPC 的 `error` / `message` 是人读文本,宿主按 `code` 判断(docs/rpc.md);JSON 字段名不在这里。
7
8
  */
8
9
  import { plural } from "../format.js";
9
10
  export const en = {
@@ -36,22 +37,6 @@ export const en = {
36
37
  noPlan: "this session has no plan extension",
37
38
  badMode: (modes) => `mode must be ${modes.join(" | ")}`,
38
39
  },
39
- /** ACP 服务端(`ama --mode acp`)。 */
40
- acp: {
41
- missingParam: (key) => `missing ${key}`,
42
- promptNotArray: "prompt must be an array of content blocks",
43
- unparsable: (reason) => `ACP: unparsable input (${reason})`,
44
- missingProtocolVersion: "missing protocolVersion",
45
- fixedCwd: (cwd, got) => `the session directory of ama --mode acp is fixed to the start directory ${cwd} (got ${got})`,
46
- sessionNotFound: (id, error) => `session ${id} not found: ${error}`,
47
- busy: "the current session is running; send session/cancel first",
48
- modelFailed: "model request failed",
49
- unknownMode: (mode) => `unknown mode: ${mode}`,
50
- allowOnce: "Allow",
51
- allowAlways: "Allow for this session",
52
- rejectOnce: "Deny",
53
- truncated: (length) => `…(truncated, ${plural(length, "character")} in total)`,
54
- },
55
40
  };
56
41
  export const zh = {
57
42
  print: {
@@ -81,19 +66,4 @@ export const zh = {
81
66
  noPlan: "该会话没有装配 plan 扩展",
82
67
  badMode: (modes) => `mode 应为 ${modes.join(" | ")}`,
83
68
  },
84
- acp: {
85
- missingParam: (key) => `缺少 ${key}`,
86
- promptNotArray: "prompt 应为内容块数组",
87
- unparsable: (reason) => `ACP:无法解析的输入(${reason})`,
88
- missingProtocolVersion: "缺少 protocolVersion",
89
- fixedCwd: (cwd, got) => `ama --mode acp 的会话目录固定为启动目录 ${cwd}(收到 ${got})`,
90
- sessionNotFound: (id, error) => `找不到会话 ${id}:${error}`,
91
- busy: "当前会话正在运行,先 session/cancel",
92
- modelFailed: "模型请求失败",
93
- unknownMode: (mode) => `未知模式:${mode}`,
94
- allowOnce: "允许",
95
- allowAlways: "本会话允许",
96
- rejectOnce: "拒绝",
97
- truncated: (length) => `…(已截断,共 ${length} 字符)`,
98
- },
99
69
  };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `ama --mode acp` 的认证门(docs/acp-plan.md §2.2、D3、D4)。[ACP-A]
3
+ *
4
+ * bootstrap 因没有可用模型(退出码 4)失败时不退出,改为在同一条 stdio 上应答 ACP:
5
+ * - `initialize` → 正常握手;客户端声明 `clientCapabilities.auth.terminal` 时给两条 terminal 型
6
+ * 认证方法(`ama auth login chatgpt` / `ama auth set`,启动时的 `--auth-file` / profile 的
7
+ * `authFile` 一并带上,R4),否则 `authMethods: []`。
8
+ * - `authenticate` → -32602(terminal 方法规定不经 authenticate)。
9
+ * - 会话方法 → 距上次失败 ≥ 1 s 则重试整段 bootstrap(并发请求共用同一次重试):成功则把同一条
10
+ * 连接交给 `runAcpMode`(服务端处理本次与之后的全部请求,客户端不必重新 initialize);仍无模型
11
+ * → -32000(message = 无模型引导,`data.authMethods` = 已给方法的 id);其它启动错误 → -32603。
12
+ * - 其它方法 → -32601;交接前的通知忽略。
13
+ * - stdin 关闭:已交接则走 ACP 模式的退出(0);否则直接退出 0。SIGINT / SIGTERM → 130 / 143。
14
+ *
15
+ * stdout 由 runCli 在第一次 bootstrap 之前接管(宿主 / Hook 的 console.log 改写到 stderr)。
16
+ */
17
+ import { type ParsedArgs } from "../../cli/args.js";
18
+ import type { CliIo, RuntimeDeps } from "../../cli/deps.js";
19
+ import { type AcpAuthMethod } from "../../drivers/acp/types.js";
20
+ /** 两次 bootstrap 重试的最小间隔。 */
21
+ export declare const AUTH_RETRY_INTERVAL_MS = 1000;
22
+ /**
23
+ * terminal 型认证方法(D4):客户端把 `args` **追加**到启动 Agent 的那条命令后面、在终端里起子进程
24
+ * (规范:append to the configured agent invocation),所以 args 是 `--acp-terminal-auth <id>`,
25
+ * 由 `parseArgs` 认出后转成 `auth login chatgpt` / `auth set`,原有的 `--mode acp` 等参数不再生效。
26
+ * `authFile`:启动时的 `--auth-file` 或 profile 的 `authFile`(绝对路径),让登录写到同一个文件。
27
+ */
28
+ export declare function terminalAuthMethods(authFile?: string): AcpAuthMethod[];
29
+ /** 登录应写入的 auth 文件:`--auth-file`(相对启动目录)> profile 的 `authFile`;都没有 → 缺省位置。 */
30
+ export declare function gateAuthFile(args: ParsedArgs, cwd: string): string | undefined;
31
+ export interface AcpAuthGateOptions {
32
+ stdin?: NodeJS.ReadableStream;
33
+ stdout?: NodeJS.WritableStream;
34
+ /** 重试间隔(测试缩短);缺省 {@link AUTH_RETRY_INTERVAL_MS}。 */
35
+ retryIntervalMs?: number;
36
+ now?: () => number;
37
+ }
38
+ export declare function runAcpAuthGate(args: ParsedArgs, deps: RuntimeDeps, io: CliIo,
39
+ /** bootstrap 第一次失败的错误(NoModel)。 */
40
+ cause: unknown, options?: AcpAuthGateOptions): Promise<number>;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * `ama --mode acp` 的认证门(docs/acp-plan.md §2.2、D3、D4)。[ACP-A]
3
+ *
4
+ * bootstrap 因没有可用模型(退出码 4)失败时不退出,改为在同一条 stdio 上应答 ACP:
5
+ * - `initialize` → 正常握手;客户端声明 `clientCapabilities.auth.terminal` 时给两条 terminal 型
6
+ * 认证方法(`ama auth login chatgpt` / `ama auth set`,启动时的 `--auth-file` / profile 的
7
+ * `authFile` 一并带上,R4),否则 `authMethods: []`。
8
+ * - `authenticate` → -32602(terminal 方法规定不经 authenticate)。
9
+ * - 会话方法 → 距上次失败 ≥ 1 s 则重试整段 bootstrap(并发请求共用同一次重试):成功则把同一条
10
+ * 连接交给 `runAcpMode`(服务端处理本次与之后的全部请求,客户端不必重新 initialize);仍无模型
11
+ * → -32000(message = 无模型引导,`data.authMethods` = 已给方法的 id);其它启动错误 → -32603。
12
+ * - 其它方法 → -32601;交接前的通知忽略。
13
+ * - stdin 关闭:已交接则走 ACP 模式的退出(0);否则直接退出 0。SIGINT / SIGTERM → 130 / 143。
14
+ *
15
+ * stdout 由 runCli 在第一次 bootstrap 之前接管(宿主 / Hook 的 console.log 改写到 stderr)。
16
+ */
17
+ import { resolve } from "node:path";
18
+ import { ACP_TERMINAL_AUTH_FLAG } from "../../cli/args.js";
19
+ import { bootstrap } from "../../cli/bootstrap.js";
20
+ import { ExitCode } from "../../cli/exit-codes.js";
21
+ import { loadProfile } from "../../config/profile.js";
22
+ import { StartupError } from "../../errors.js";
23
+ import { RpcError } from "../../drivers/jsonrpc.js";
24
+ import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "../../drivers/acp/types.js";
25
+ import { formatModelRef } from "../../ai/providers/channels.js";
26
+ import { msg } from "../../i18n/index.js";
27
+ import { AMA_VERSION } from "../../version.js";
28
+ import { onTerminationSignals } from "../shared.js";
29
+ import { clientCapabilitiesOf, createAcpConnection } from "./acp-connection.js";
30
+ import { runAcpMode } from "./acp-mode.js";
31
+ import { ACP_AGENT_CAPABILITIES } from "./acp-server.js";
32
+ /** 两次 bootstrap 重试的最小间隔。 */
33
+ export const AUTH_RETRY_INTERVAL_MS = 1000;
34
+ /** 会触发重试的方法(交接后由服务端处理)。 */
35
+ const SESSION_METHODS = new Set([
36
+ ACP_METHODS.sessionNew,
37
+ ACP_METHODS.sessionLoad,
38
+ ACP_METHODS.sessionResume,
39
+ ACP_METHODS.sessionList,
40
+ ACP_METHODS.sessionClose,
41
+ ACP_METHODS.sessionPrompt,
42
+ ACP_METHODS.sessionSetMode,
43
+ ACP_METHODS.sessionSetConfigOption,
44
+ ]);
45
+ /**
46
+ * terminal 型认证方法(D4):客户端把 `args` **追加**到启动 Agent 的那条命令后面、在终端里起子进程
47
+ * (规范:append to the configured agent invocation),所以 args 是 `--acp-terminal-auth <id>`,
48
+ * 由 `parseArgs` 认出后转成 `auth login chatgpt` / `auth set`,原有的 `--mode acp` 等参数不再生效。
49
+ * `authFile`:启动时的 `--auth-file` 或 profile 的 `authFile`(绝对路径),让登录写到同一个文件。
50
+ */
51
+ export function terminalAuthMethods(authFile) {
52
+ const m = msg().acp.auth;
53
+ const extra = authFile !== undefined ? ["--auth-file", authFile] : [];
54
+ return [
55
+ {
56
+ type: "terminal",
57
+ id: "chatgpt",
58
+ name: m.chatgptName,
59
+ description: m.chatgptDescription,
60
+ args: [ACP_TERMINAL_AUTH_FLAG, "chatgpt", ...extra],
61
+ },
62
+ {
63
+ type: "terminal",
64
+ id: "api-key",
65
+ name: m.apiKeyName,
66
+ description: m.apiKeyDescription,
67
+ args: [ACP_TERMINAL_AUTH_FLAG, "api-key", ...extra],
68
+ },
69
+ ];
70
+ }
71
+ /** 登录应写入的 auth 文件:`--auth-file`(相对启动目录)> profile 的 `authFile`;都没有 → 缺省位置。 */
72
+ export function gateAuthFile(args, cwd) {
73
+ if (args.authFile !== undefined)
74
+ return resolve(cwd, args.authFile);
75
+ if (args.profile === undefined)
76
+ return undefined;
77
+ try {
78
+ return loadProfile(args.profile, cwd).authFile;
79
+ }
80
+ catch {
81
+ return undefined; // bootstrap 已读过 profile;这里失败只是不带 --auth-file
82
+ }
83
+ }
84
+ function isNoModel(error) {
85
+ return error instanceof StartupError && error.exitCode === ExitCode.NoModel;
86
+ }
87
+ function messageOf(error) {
88
+ return error instanceof Error ? error.message : String(error);
89
+ }
90
+ export async function runAcpAuthGate(args, deps, io,
91
+ /** bootstrap 第一次失败的错误(NoModel)。 */
92
+ cause, options = {}) {
93
+ const stdin = options.stdin ?? process.stdin;
94
+ const stdout = options.stdout ?? process.stdout;
95
+ const now = options.now ?? Date.now;
96
+ const interval = options.retryIntervalMs ?? AUTH_RETRY_INTERVAL_MS;
97
+ const log = (message) => io.stderr(`ama: ${message}\n`);
98
+ const authFile = gateAuthFile(args, io.cwd);
99
+ log(msg().acp.auth.waiting(messageOf(cause)));
100
+ let lastError = cause;
101
+ let lastFailure = now();
102
+ let attempt;
103
+ /** 交接后的服务端(attach 时同步填入)。 */
104
+ const target = {};
105
+ const serverOf = () => target.server;
106
+ let served;
107
+ let runtime;
108
+ const offeredMethods = (caps) => caps.auth?.terminal === true ? terminalAuthMethods(authFile) : [];
109
+ /** 重试 bootstrap(节流;并发请求共用一次);成功返回 Runtime 并已交接。 */
110
+ const retry = () => {
111
+ if (attempt !== undefined)
112
+ return attempt;
113
+ if (now() - lastFailure < interval)
114
+ return Promise.resolve(undefined);
115
+ attempt = bootstrap(args, deps, io).then((ready) => {
116
+ handover(ready);
117
+ return ready;
118
+ }, (error) => {
119
+ lastError = error;
120
+ lastFailure = now();
121
+ attempt = undefined;
122
+ if (!isNoModel(error))
123
+ log(messageOf(error));
124
+ return undefined;
125
+ });
126
+ return attempt;
127
+ };
128
+ const connection = createAcpConnection({ input: stdin, output: stdout }, log, {
129
+ onRequest: async (method, raw, ctx) => {
130
+ const params = (raw ?? {});
131
+ if (target.server !== undefined)
132
+ return target.server.handle(method, params, ctx);
133
+ if (method === ACP_METHODS.initialize)
134
+ return initialize(params);
135
+ if (method === ACP_METHODS.authenticate)
136
+ throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.auth.notAgentMethod);
137
+ if (!SESSION_METHODS.has(method))
138
+ throw new RpcError(RPC_ERRORS.methodNotFound, `method not found: ${method}`);
139
+ const handed = (await retry()) !== undefined ? serverOf() : undefined;
140
+ if (handed !== undefined)
141
+ return handed.handle(method, params, ctx);
142
+ if (!isNoModel(lastError))
143
+ throw new RpcError(RPC_ERRORS.internalError, messageOf(lastError));
144
+ throw new RpcError(RPC_ERRORS.authRequired, messageOf(lastError), {
145
+ authMethods: offeredMethods(connection.clientCapabilities).map((m) => m.id),
146
+ });
147
+ },
148
+ onNotification: (method, params) => target.server?.handleNotification(method, params),
149
+ });
150
+ function initialize(params) {
151
+ if (typeof params["protocolVersion"] !== "number")
152
+ throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.missingProtocolVersion);
153
+ const caps = clientCapabilitiesOf(params);
154
+ connection.markInitialized(caps);
155
+ return {
156
+ protocolVersion: ACP_PROTOCOL_VERSION,
157
+ agentCapabilities: ACP_AGENT_CAPABILITIES,
158
+ authMethods: offeredMethods(caps),
159
+ agentInfo: { name: "ama", title: "ama", version: AMA_VERSION },
160
+ };
161
+ }
162
+ // 退出:交接前 stdin 关闭 → 0、信号 → 130 / 143;交接后等 ACP 模式退出
163
+ let settled = false;
164
+ let finish = () => undefined;
165
+ const exit = new Promise((done) => (finish = done));
166
+ const offSignals = onTerminationSignals((code) => {
167
+ if (target.server === undefined) {
168
+ settled = true;
169
+ finish(code);
170
+ }
171
+ });
172
+ void connection.peer.closed.then(async () => {
173
+ await attempt; // 收尾中的重试:落定后交接了就交给 ACP 模式退出
174
+ if (served === undefined) {
175
+ settled = true;
176
+ finish(ExitCode.Ok);
177
+ }
178
+ });
179
+ function handover(ready) {
180
+ if (settled) {
181
+ void ready.dispose("exit").catch(() => undefined);
182
+ return;
183
+ }
184
+ runtime = ready;
185
+ offSignals();
186
+ for (const w of ready.warnings)
187
+ io.stderr(msg().cli.bootstrap.warning(w));
188
+ log(msg().acp.auth.ready(formatModelRef({ provider: ready.model.provider, id: ready.model.id })));
189
+ served = runAcpMode(ready, { args, prompt: undefined, io }, { stdin, stdout, handover: { connection, attach: (s) => void (target.server = s) } });
190
+ void served.then(finish, () => finish(ExitCode.RuntimeError));
191
+ }
192
+ const code = await exit;
193
+ offSignals();
194
+ if (served === undefined) {
195
+ stdin.pause?.();
196
+ await connection.peer.flush();
197
+ connection.peer.close();
198
+ }
199
+ await runtime?.dispose("exit").catch(() => undefined);
200
+ return code;
201
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `ama --mode acp` 的会话配置项与命令表(docs/acp-plan.md §1.6、D8、D9)。[ACP-C0 建壳,ACP-D 实现]
3
+ *
4
+ * - `buildConfigOptions`:`mode`(select,category `mode`,值是 ama 的权限模式,与 `modes` 同一状态)、
5
+ * `model`(select,category `model`,按供应商分组,值 `provider/model-id`)与 `thinking`(select,
6
+ * category `thought_level`,只列当前模型支持的级别);没有 boolean 项。规范:Agent 给了 `configOptions`,
7
+ * 客户端 SHOULD 用它代替 `modes`(Zed 给了就不再看 `modes`),所以模式也必须在这里;`modes` 照给,留给
8
+ * 只认 `modes` 的客户端。`mode` 的设置由服务端处理(按会话记、只对前台会话生效),不经 `applyConfigOption`。
9
+ * 模型清单与 TUI `/model` 的「已配置」视图同一口径(`catalogItems`):只列有 key、OAuth 已登录或本地的
10
+ * 供应商,`models.enabled` 设置时只列清单内的;`fake` 按 `hideFakeProvider` 规则藏起;当前模型总在列。
11
+ * 判断有没有 key 要异步解析,所以先 `await prepareConfigOptions(providers)` 缓存各供应商的可用状态,
12
+ * 之后 `buildConfigOptions` 同步取(映射器的 `extras()` 是同步的)。没准备过时只列本地供应商与当前模型。
13
+ * - `applyConfigOption`:`session/set_config_option` → `setModel()` / `setThinkingLevel()`;未知 id、
14
+ * 非字符串值、找不到的模型、不认识的级别回 invalid params。
15
+ * - `availableCommands`:skills → `skill:<name>`,提示模板 → `<name>`(`argumentHint` → `input.hint`)。
16
+ * 内置斜杠命令(`/new`、`/compact`…)在 ACP 下不执行,不列。
17
+ */
18
+ import type { AgentSession } from "../../agent/types.js";
19
+ import type { ProviderRegistryApi } from "../../ai/types.js";
20
+ import type { LoadedResources } from "../../cli/runtime.js";
21
+ import { type AcpAvailableCommand, type AcpSessionConfigOption, type AcpSetConfigOptionParams } from "../../drivers/acp/types.js";
22
+ import type { PermissionMode } from "../../permissions/types.js";
23
+ /** 配置项 id(机器字段,不翻译)。 */
24
+ export declare const CONFIG_IDS: {
25
+ readonly mode: "mode";
26
+ readonly model: "model";
27
+ readonly thinking: "thinking";
28
+ };
29
+ /**
30
+ * 解析各供应商的可用状态并缓存(读 key / OAuth,毫秒级);开会话前调一次,登录后再调一次可刷新。
31
+ * 缓存挂在传入的注册表上,`buildConfigOptions` 须传同一个对象。
32
+ */
33
+ export declare function prepareConfigOptions(providers: ProviderRegistryApi): Promise<void>;
34
+ export interface ConfigOptionsExtras {
35
+ /** `models.enabled`(用户级清单);不给或空表示不限。 */
36
+ enabled?: readonly string[] | undefined;
37
+ /** 这个会话的权限模式(服务端按会话记);不给时取 `session.state.permissionMode`。 */
38
+ mode?: PermissionMode | undefined;
39
+ }
40
+ export declare function buildConfigOptions(session: AgentSession, providers: ProviderRegistryApi, env: NodeJS.ProcessEnv, extras?: ConfigOptionsExtras): AcpSessionConfigOption[];
41
+ /** 未知 id / value → RpcError invalidParams。 */
42
+ export declare function applyConfigOption(session: AgentSession, params: AcpSetConfigOptionParams): Promise<void>;
43
+ export declare function availableCommands(resources: LoadedResources): AcpAvailableCommand[];
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `ama --mode acp` 的会话配置项与命令表(docs/acp-plan.md §1.6、D8、D9)。[ACP-C0 建壳,ACP-D 实现]
3
+ *
4
+ * - `buildConfigOptions`:`mode`(select,category `mode`,值是 ama 的权限模式,与 `modes` 同一状态)、
5
+ * `model`(select,category `model`,按供应商分组,值 `provider/model-id`)与 `thinking`(select,
6
+ * category `thought_level`,只列当前模型支持的级别);没有 boolean 项。规范:Agent 给了 `configOptions`,
7
+ * 客户端 SHOULD 用它代替 `modes`(Zed 给了就不再看 `modes`),所以模式也必须在这里;`modes` 照给,留给
8
+ * 只认 `modes` 的客户端。`mode` 的设置由服务端处理(按会话记、只对前台会话生效),不经 `applyConfigOption`。
9
+ * 模型清单与 TUI `/model` 的「已配置」视图同一口径(`catalogItems`):只列有 key、OAuth 已登录或本地的
10
+ * 供应商,`models.enabled` 设置时只列清单内的;`fake` 按 `hideFakeProvider` 规则藏起;当前模型总在列。
11
+ * 判断有没有 key 要异步解析,所以先 `await prepareConfigOptions(providers)` 缓存各供应商的可用状态,
12
+ * 之后 `buildConfigOptions` 同步取(映射器的 `extras()` 是同步的)。没准备过时只列本地供应商与当前模型。
13
+ * - `applyConfigOption`:`session/set_config_option` → `setModel()` / `setThinkingLevel()`;未知 id、
14
+ * 非字符串值、找不到的模型、不认识的级别回 invalid params。
15
+ * - `availableCommands`:skills → `skill:<name>`,提示模板 → `<name>`(`argumentHint` → `input.hint`)。
16
+ * 内置斜杠命令(`/new`、`/compact`…)在 ACP 下不执行,不列。
17
+ */
18
+ import { formatModelRef } from "../../ai/providers/channels.js";
19
+ import { providerAccess } from "../../ai/providers/model-visibility.js";
20
+ import { clampThinkingLevel, getSupportedLevels, isThinkingLevel, THINKING_LEVELS, } from "../../ai/thinking.js";
21
+ import { hideFakeProvider } from "../../cli/fake-visibility.js";
22
+ import { RPC_ERRORS, } from "../../drivers/acp/types.js";
23
+ import { RpcError } from "../../drivers/jsonrpc.js";
24
+ import { AmaError } from "../../errors.js";
25
+ import { msg } from "../../i18n/index.js";
26
+ import { catalogItems } from "../interactive/model-items.js";
27
+ import { permissionModes } from "./acp-events.js";
28
+ /** 配置项 id(机器字段,不翻译)。 */
29
+ export const CONFIG_IDS = { mode: "mode", model: "model", thinking: "thinking" };
30
+ /** 各注册表上次解析出的供应商可用状态(providerId → access)。 */
31
+ const accessCache = new WeakMap();
32
+ /**
33
+ * 解析各供应商的可用状态并缓存(读 key / OAuth,毫秒级);开会话前调一次,登录后再调一次可刷新。
34
+ * 缓存挂在传入的注册表上,`buildConfigOptions` 须传同一个对象。
35
+ */
36
+ export async function prepareConfigOptions(providers) {
37
+ const access = new Map();
38
+ for (const provider of providers.list())
39
+ access.set(provider.id, await providerAccess(providers, provider));
40
+ accessCache.set(providers, access);
41
+ }
42
+ export function buildConfigOptions(session, providers, env, extras = {}) {
43
+ const m = msg().acp.config;
44
+ const current = session.state.model === undefined ? undefined : formatModelRef(session.state.model);
45
+ const known = accessCache.get(providers);
46
+ const catalog = hideFakeProvider(providers, env)
47
+ .list()
48
+ .map((provider) => ({
49
+ provider,
50
+ access: known?.get(provider.id) ?? (provider.requiresApiKey ? "none" : "local"),
51
+ }));
52
+ const items = catalogItems({ providers: catalog }, { view: "configured", enabled: extras.enabled, current, hints: false });
53
+ // 按供应商重新分组(`catalogItems` 的组标题带可用状态,当前模型另成一组)
54
+ const groups = new Map();
55
+ for (const item of items) {
56
+ const providerId = item.value.slice(0, item.value.indexOf("/"));
57
+ let group = groups.get(providerId);
58
+ if (group === undefined) {
59
+ group = {
60
+ group: providerId,
61
+ name: providers.get(providerId)?.name ?? providerId,
62
+ options: [],
63
+ };
64
+ groups.set(providerId, group);
65
+ }
66
+ if (group.options.some((o) => o.value === item.value))
67
+ continue;
68
+ group.options.push({
69
+ value: item.value,
70
+ name: item.label,
71
+ ...(item.description !== undefined ? { description: item.description } : {}),
72
+ });
73
+ }
74
+ const modes = permissionModes(extras.mode ?? session.state.permissionMode);
75
+ const options = [
76
+ {
77
+ id: CONFIG_IDS.mode,
78
+ name: m.mode,
79
+ category: "mode",
80
+ type: "select",
81
+ currentValue: modes.currentModeId,
82
+ options: modes.availableModes.map(({ id, name, description }) => ({
83
+ value: id,
84
+ name,
85
+ description,
86
+ })),
87
+ },
88
+ ];
89
+ if (current !== undefined)
90
+ options.push({
91
+ id: CONFIG_IDS.model,
92
+ name: m.model,
93
+ category: "model",
94
+ type: "select",
95
+ currentValue: current,
96
+ options: [...groups.values()],
97
+ });
98
+ const lookup = current === undefined ? undefined : providers.findModel(current);
99
+ const model = lookup?.ok === true ? lookup.model : undefined;
100
+ const levels = model === undefined ? [...THINKING_LEVELS] : getSupportedLevels(model);
101
+ const level = session.state.thinkingLevel;
102
+ options.push({
103
+ id: CONFIG_IDS.thinking,
104
+ name: m.thinking,
105
+ category: "thought_level",
106
+ type: "select",
107
+ currentValue: model === undefined || levels.includes(level) ? level : clampThinkingLevel(model, level),
108
+ options: levels.map((value) => ({ value, name: m.levels[value] })),
109
+ });
110
+ return options;
111
+ }
112
+ /** 未知 id / value → RpcError invalidParams。 */
113
+ export async function applyConfigOption(session, params) {
114
+ const m = msg().acp.config;
115
+ const value = params.value;
116
+ switch (params.configId) {
117
+ case CONFIG_IDS.model:
118
+ if (typeof value !== "string" || value === "")
119
+ throw new RpcError(RPC_ERRORS.invalidParams, m.invalidValue(params.configId, String(value)));
120
+ try {
121
+ await session.setModel(value);
122
+ }
123
+ catch (error) {
124
+ if (error instanceof AmaError && error.code === "model_not_found")
125
+ throw new RpcError(RPC_ERRORS.invalidParams, m.unknownModel(value));
126
+ throw error;
127
+ }
128
+ return;
129
+ case CONFIG_IDS.thinking:
130
+ if (!isThinkingLevel(value))
131
+ throw new RpcError(RPC_ERRORS.invalidParams, m.invalidValue(params.configId, String(value)));
132
+ if (value !== session.state.thinkingLevel)
133
+ session.setThinkingLevel(value);
134
+ return;
135
+ default:
136
+ throw new RpcError(RPC_ERRORS.invalidParams, msg().acp.core.unknownConfigOption(String(params.configId)));
137
+ }
138
+ }
139
+ export function availableCommands(resources) {
140
+ const commands = resources.skills.map((skill) => ({
141
+ name: `skill:${skill.name}`,
142
+ description: skill.description,
143
+ }));
144
+ for (const prompt of resources.prompts)
145
+ commands.push({
146
+ name: prompt.name,
147
+ description: prompt.description ?? "",
148
+ ...(prompt.argumentHint !== undefined ? { input: { hint: prompt.argumentHint } } : {}),
149
+ });
150
+ return commands;
151
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `ama --mode acp` 的连接:JSON-RPC 对等端 + `initialize` 协商结果(docs/acp-plan.md §1.6)。[ACP-C0]
3
+ *
4
+ * 认证门(无模型时的握手,acp-auth-gate.ts)与服务端(acp-server.ts)共用同一条连接:门控在
5
+ * bootstrap 成功后把它交给 `AcpServer`,客户端不必重新 `initialize`。对等端开着协议级取消
6
+ * (`$/cancel_request`):本端撤回挂起的请求会通知客户端,客户端撤回的请求 abort `ctx.signal`。
7
+ */
8
+ import { JsonRpcPeer, type IncomingRequestContext } from "../../drivers/jsonrpc.js";
9
+ import type { AcpClientCapabilities } from "../../drivers/acp/types.js";
10
+ export interface AcpConnectionHandlers {
11
+ onRequest(method: string, params: unknown, ctx: IncomingRequestContext): Promise<unknown>;
12
+ onNotification(method: string, params: unknown): void;
13
+ }
14
+ export interface AcpConnection {
15
+ /** `cancelRequests: true`。 */
16
+ readonly peer: JsonRpcPeer;
17
+ /** 客户端在 `initialize` 里声明的能力;之前为空对象。 */
18
+ clientCapabilities: AcpClientCapabilities;
19
+ /** 已处理过 `initialize`。 */
20
+ readonly initialized: boolean;
21
+ /** 记录 `initialize` 的协商结果(门控与服务端谁先处理谁记)。 */
22
+ markInitialized(capabilities: AcpClientCapabilities): void;
23
+ }
24
+ export declare function createAcpConnection(streams: {
25
+ input: NodeJS.ReadableStream;
26
+ output: NodeJS.WritableStream;
27
+ }, log: (message: string) => void, handlers: AcpConnectionHandlers): AcpConnection;
28
+ /** `initialize` 参数里的客户端能力(缺省或不是对象 → 空对象;不校验子字段)。 */
29
+ export declare function clientCapabilitiesOf(params: Record<string, unknown>): AcpClientCapabilities;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `ama --mode acp` 的连接:JSON-RPC 对等端 + `initialize` 协商结果(docs/acp-plan.md §1.6)。[ACP-C0]
3
+ *
4
+ * 认证门(无模型时的握手,acp-auth-gate.ts)与服务端(acp-server.ts)共用同一条连接:门控在
5
+ * bootstrap 成功后把它交给 `AcpServer`,客户端不必重新 `initialize`。对等端开着协议级取消
6
+ * (`$/cancel_request`):本端撤回挂起的请求会通知客户端,客户端撤回的请求 abort `ctx.signal`。
7
+ */
8
+ import { JsonRpcPeer, } from "../../drivers/jsonrpc.js";
9
+ import { msg } from "../../i18n/index.js";
10
+ export function createAcpConnection(streams, log, handlers) {
11
+ let initialized = false;
12
+ const options = {
13
+ input: streams.input,
14
+ output: streams.output,
15
+ cancelRequests: true,
16
+ onRequest: (method, params, ctx) => handlers.onRequest(method, params, ctx),
17
+ onNotification: (method, params) => handlers.onNotification(method, params),
18
+ onProtocolError: (_line, reason) => log(msg().acp.core.unparsable(reason)),
19
+ };
20
+ const connection = {
21
+ peer: new JsonRpcPeer(options),
22
+ clientCapabilities: {},
23
+ get initialized() {
24
+ return initialized;
25
+ },
26
+ markInitialized(capabilities) {
27
+ connection.clientCapabilities = capabilities;
28
+ initialized = true;
29
+ },
30
+ };
31
+ return connection;
32
+ }
33
+ /** `initialize` 参数里的客户端能力(缺省或不是对象 → 空对象;不校验子字段)。 */
34
+ export function clientCapabilitiesOf(params) {
35
+ const value = params["clientCapabilities"];
36
+ return value !== null && typeof value === "object" ? value : {};
37
+ }