@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.
- package/CHANGELOG.md +61 -0
- package/CHANGELOG.zh-CN.md +43 -0
- package/README.md +4 -3
- package/README.zh-CN.md +1 -1
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +2 -2
- package/dist/ai/apis/shared.d.ts +7 -1
- package/dist/ai/apis/shared.js +10 -0
- package/dist/ai/fake/fake-provider.js +5 -1
- package/dist/ai/fake/fake-script.d.ts +5 -2
- package/dist/ai/fake/fake-script.js +6 -2
- package/dist/ai/types.d.ts +2 -1
- package/dist/bundle/ama.cjs +48757 -47456
- package/dist/cli/args.d.ts +6 -0
- package/dist/cli/args.js +46 -0
- package/dist/cli/bootstrap.js +13 -2
- package/dist/cli/compose-events.d.ts +10 -0
- package/dist/cli/compose-events.js +97 -0
- package/dist/cli/compose-session.d.ts +25 -4
- package/dist/cli/compose-session.js +130 -103
- package/dist/cli/compose.js +6 -1
- package/dist/cli/runtime.d.ts +3 -1
- package/dist/cli/subcommands/auth.d.ts +13 -1
- package/dist/cli/subcommands/auth.js +28 -1
- package/dist/drivers/acp/client.d.ts +7 -2
- package/dist/drivers/acp/client.js +10 -1
- package/dist/drivers/acp/driver.d.ts +15 -4
- package/dist/drivers/acp/driver.js +110 -33
- package/dist/drivers/acp/testing/fake-agent-main.d.ts +1 -1
- package/dist/drivers/acp/testing/fake-agent-main.js +9 -2
- package/dist/drivers/acp/testing/fake-agent.d.ts +17 -2
- package/dist/drivers/acp/testing/fake-agent.js +108 -25
- package/dist/drivers/acp/types.d.ts +76 -12
- package/dist/drivers/acp/types.js +11 -3
- package/dist/drivers/jsonrpc.d.ts +13 -3
- package/dist/drivers/jsonrpc.js +40 -7
- package/dist/drivers/turn.d.ts +15 -2
- package/dist/drivers/turn.js +33 -4
- package/dist/i18n/catalog.d.ts +59 -16
- package/dist/i18n/catalog.js +4 -1
- package/dist/i18n/messages/acp.d.ts +125 -0
- package/dist/i18n/messages/acp.js +126 -0
- package/dist/i18n/messages/auth.d.ts +2 -0
- package/dist/i18n/messages/auth.js +4 -2
- package/dist/i18n/messages/cli-args.d.ts +2 -0
- package/dist/i18n/messages/cli-args.js +2 -0
- package/dist/i18n/messages/cli.d.ts +4 -0
- package/dist/i18n/messages/cli.js +2 -0
- package/dist/i18n/messages/print.d.ts +3 -33
- package/dist/i18n/messages/print.js +3 -33
- package/dist/modes/acp/acp-auth-gate.d.ts +40 -0
- package/dist/modes/acp/acp-auth-gate.js +201 -0
- package/dist/modes/acp/acp-config.d.ts +43 -0
- package/dist/modes/acp/acp-config.js +151 -0
- package/dist/modes/acp/acp-connection.d.ts +29 -0
- package/dist/modes/acp/acp-connection.js +37 -0
- package/dist/modes/acp/acp-events.d.ts +59 -23
- package/dist/modes/acp/acp-events.js +154 -88
- package/dist/modes/acp/acp-mode.d.ts +13 -2
- package/dist/modes/acp/acp-mode.js +23 -8
- package/dist/modes/acp/acp-server.d.ts +59 -32
- package/dist/modes/acp/acp-server.js +322 -128
- package/dist/modes/acp/acp-sessions.d.ts +80 -0
- package/dist/modes/acp/acp-sessions.js +157 -0
- package/dist/modes/acp/acp-tool-text.d.ts +23 -0
- package/dist/modes/acp/acp-tool-text.js +83 -0
- package/dist/modes/print/json-event.d.ts +2 -1
- package/dist/modes/print/json-event.js +6 -1
- package/dist/tools/edit.d.ts +6 -2
- package/dist/tools/edit.js +19 -2
- package/dist/tools/types.d.ts +11 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/write.d.ts +1 -0
- package/dist/tools/write.js +21 -3
- package/docs/acp.md +143 -29
- package/docs/agents.md +2 -0
- package/docs/codemode.md +1 -1
- package/docs/en/acp.md +192 -0
- 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 =
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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`
|
|
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>;
|