@armadra/agent 0.6.6 → 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 +16 -0
- package/CHANGELOG.zh-CN.md +14 -0
- package/dist/agents/external.d.ts +7 -1
- package/dist/agents/external.js +21 -3
- package/dist/bundle/ama.cjs +75 -11
- package/dist/drivers/acp/client.d.ts +23 -4
- package/dist/drivers/acp/client.js +69 -5
- package/dist/drivers/acp/testing/fake-agent-main.d.ts +1 -1
- package/dist/drivers/acp/testing/fake-agent-main.js +3 -2
- package/dist/drivers/acp/testing/fake-agent.d.ts +9 -0
- package/dist/drivers/acp/testing/fake-agent.js +91 -5
- package/dist/drivers/acp/types.d.ts +85 -2
- package/dist/drivers/acp/types.js +4 -2
- package/docs/acp.md +8 -1
- package/docs/agents.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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
|
+
|
|
17
|
+
## 0.6.7 (2026-10-03)
|
|
18
|
+
|
|
19
|
+
- **A host runner with id `ama` is used**: when the host registers a runner for `ama` via `HostApi.runners.provide`
|
|
20
|
+
(for example another ama node on an Armadra canvas), `task(agent="ama")` now goes to it instead of being unknown; without
|
|
21
|
+
one nothing changes, and the built-in types (`general` / `explore` / `plan`) are always ama sub-sessions. Docs:
|
|
22
|
+
docs/agents.md.
|
|
23
|
+
|
|
8
24
|
## 0.6.6 (2026-10-03)
|
|
9
25
|
|
|
10
26
|
- **Local search hints**: when both `grep` and `glob` are directly available, the system prompt's rules gain one line
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
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
|
+
|
|
15
|
+
## 0.6.7(2026-10-03)
|
|
16
|
+
|
|
17
|
+
- **宿主注入的 `ama` runner 生效**:宿主经 `HostApi.runners.provide` 注入 id 为 `ama` 的 runner(如 Armadra 画布上另一个
|
|
18
|
+
ama 节点)时,`task(agent="ama")` 交给它;没注入时行为不变,内置类型(`general` / `explore` / `plan`)始终是 ama 子会话。
|
|
19
|
+
文档:docs/agents.md。
|
|
20
|
+
|
|
7
21
|
## 0.6.6(2026-10-03)
|
|
8
22
|
|
|
9
23
|
- **本地检索提示**:`grep` 与 `glob` 都直接可用时,系统提示的规则节多一句「先 grep / glob 定位再 read,不要猜路径」;`grep` /
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* 外部 Agent 接入 `task(agent=…)`(docs/wave5-plan.md §5.3–§5.5、§7.3、§7.6,D13、D15、D17)。[W5-EG]
|
|
3
3
|
*
|
|
4
4
|
* - 名字:`claude` / `codex` / `acp:<program>` / 驱动表里的其它 id(`gemini` …,`ama` 除外——那是子会话
|
|
5
|
-
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id
|
|
5
|
+
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id(宿主注入的 `ama` 在类型里的 runner
|
|
6
|
+
* 名是 `host:ama`,交给宿主;没注入时 `ama` 照旧不是外部 Agent)。启动时 PATH 上找得到的
|
|
6
7
|
* claude / codex 与已注入的宿主 runner 登记进类型目录(进 task 工具描述);其余按需解析(不进描述,
|
|
7
8
|
* 描述在会话内字节不变)。
|
|
8
9
|
* - 每个主会话一个 {@link ExternalAgents}:审批接 `requestApproval`(只走 broker 链,不经 gateToolCall
|
|
@@ -34,6 +35,11 @@ export declare const externalTesting: {
|
|
|
34
35
|
export declare function isExternalSpec(name: string): boolean;
|
|
35
36
|
/** 外部 CLI 的类型(`task(agent="claude")`)。 */
|
|
36
37
|
export declare function externalDefinition(spec: string): AgentDefinition;
|
|
38
|
+
/**
|
|
39
|
+
* 宿主注入的 id 恰为 `ama` 的 runner(画布上另一个 ama 节点)在类型里的 runner 名。裸 `ama` 是子会话
|
|
40
|
+
* 类型的 runner 名(注册表按它起子会话),不能共用;名字仍是 `ama`,`task(agent="ama")` 照常解析。
|
|
41
|
+
*/
|
|
42
|
+
export declare const HOST_AMA_RUNNER = "host:ama";
|
|
37
43
|
export declare function hostDefinition(runner: HostRunner): AgentDefinition;
|
|
38
44
|
/** 只查 PATH(不起进程):候选链里任一程序在 PATH 上。 */
|
|
39
45
|
export declare function externalOnPath(spec: string, env: NodeJS.ProcessEnv): boolean;
|
package/dist/agents/external.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* 外部 Agent 接入 `task(agent=…)`(docs/wave5-plan.md §5.3–§5.5、§7.3、§7.6,D13、D15、D17)。[W5-EG]
|
|
3
3
|
*
|
|
4
4
|
* - 名字:`claude` / `codex` / `acp:<program>` / 驱动表里的其它 id(`gemini` …,`ama` 除外——那是子会话
|
|
5
|
-
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id
|
|
5
|
+
* 类型的 runner 名;ama 自己经 ACP 用 `acp:ama`)/ 宿主注入的 runner id(宿主注入的 `ama` 在类型里的 runner
|
|
6
|
+
* 名是 `host:ama`,交给宿主;没注入时 `ama` 照旧不是外部 Agent)。启动时 PATH 上找得到的
|
|
6
7
|
* claude / codex 与已注入的宿主 runner 登记进类型目录(进 task 工具描述);其余按需解析(不进描述,
|
|
7
8
|
* 描述在会话内字节不变)。
|
|
8
9
|
* - 每个主会话一个 {@link ExternalAgents}:审批接 `requestApproval`(只走 broker 链,不经 gateToolCall
|
|
@@ -58,8 +59,17 @@ function definition(name, runner, description, source) {
|
|
|
58
59
|
export function externalDefinition(spec) {
|
|
59
60
|
return definition(spec, spec, `External ${labelOf(spec)} CLI (its own login, model and permission policy; full instructions in prompt).`, "builtin");
|
|
60
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 宿主注入的 id 恰为 `ama` 的 runner(画布上另一个 ama 节点)在类型里的 runner 名。裸 `ama` 是子会话
|
|
64
|
+
* 类型的 runner 名(注册表按它起子会话),不能共用;名字仍是 `ama`,`task(agent="ama")` 照常解析。
|
|
65
|
+
*/
|
|
66
|
+
export const HOST_AMA_RUNNER = "host:ama";
|
|
67
|
+
/** 宿主 runner id → 类型里的 runner 名。 */
|
|
68
|
+
function hostRunnerName(id) {
|
|
69
|
+
return id === "ama" ? HOST_AMA_RUNNER : id;
|
|
70
|
+
}
|
|
61
71
|
export function hostDefinition(runner) {
|
|
62
|
-
return definition(runner.id, runner.id, runner.description, "host");
|
|
72
|
+
return definition(runner.id, hostRunnerName(runner.id), runner.description, "host");
|
|
63
73
|
}
|
|
64
74
|
/** 只查 PATH(不起进程):候选链里任一程序在 PATH 上。 */
|
|
65
75
|
export function externalOnPath(spec, env) {
|
|
@@ -207,8 +217,16 @@ export class SessionExternalAgents {
|
|
|
207
217
|
runner(agent) {
|
|
208
218
|
if (agent.runner === "ama")
|
|
209
219
|
return undefined;
|
|
210
|
-
|
|
220
|
+
// 宿主注入的 `ama` 走宿主;宿主已注销时报不可用,不退回自起(驱动表里也有一个 ama)。
|
|
221
|
+
// 宿主 id 不在 AgentRunnerSpec 的字面量里(见 definition),按字符串比
|
|
222
|
+
const runnerName = agent.runner;
|
|
223
|
+
const hostAma = runnerName === HOST_AMA_RUNNER;
|
|
224
|
+
const spec = hostAma ? "ama" : agent.runner;
|
|
211
225
|
const host = this.wiring.hostRunners?.get(spec);
|
|
226
|
+
if (hostAma && host === undefined) {
|
|
227
|
+
const error = new AmaError("agent_host_only", `"ama" was provided by the host and is no longer available`);
|
|
228
|
+
return { id: spec, start: () => Promise.reject(error) };
|
|
229
|
+
}
|
|
212
230
|
let inner;
|
|
213
231
|
try {
|
|
214
232
|
inner = this.agents.resolve(spec);
|
package/dist/bundle/ama.cjs
CHANGED
|
@@ -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.
|
|
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: {
|
|
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
|
-
/**
|
|
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
|
});
|
|
@@ -33541,8 +33592,11 @@ function externalDefinition(spec2) {
|
|
|
33541
33592
|
"builtin"
|
|
33542
33593
|
);
|
|
33543
33594
|
}
|
|
33595
|
+
function hostRunnerName(id) {
|
|
33596
|
+
return id === "ama" ? HOST_AMA_RUNNER : id;
|
|
33597
|
+
}
|
|
33544
33598
|
function hostDefinition(runner) {
|
|
33545
|
-
return definition(runner.id, runner.id, runner.description, "host");
|
|
33599
|
+
return definition(runner.id, hostRunnerName(runner.id), runner.description, "host");
|
|
33546
33600
|
}
|
|
33547
33601
|
function externalOnPath(spec2, env) {
|
|
33548
33602
|
const found = candidatesFor(spec2);
|
|
@@ -33594,7 +33648,7 @@ function mergeAgentInfos(base4, external) {
|
|
|
33594
33648
|
}
|
|
33595
33649
|
return out;
|
|
33596
33650
|
}
|
|
33597
|
-
var import_node_crypto14, LISTED_EXTERNAL_AGENTS, NO_AGENT_PROBE_ENV, externalTesting, ExternalTaskHandle, infoCache, SessionExternalAgents;
|
|
33651
|
+
var import_node_crypto14, LISTED_EXTERNAL_AGENTS, NO_AGENT_PROBE_ENV, externalTesting, HOST_AMA_RUNNER, ExternalTaskHandle, infoCache, SessionExternalAgents;
|
|
33598
33652
|
var init_external = __esm({
|
|
33599
33653
|
"src/agents/external.ts"() {
|
|
33600
33654
|
"use strict";
|
|
@@ -33611,6 +33665,7 @@ var init_external = __esm({
|
|
|
33611
33665
|
LISTED_EXTERNAL_AGENTS = ["claude", "codex"];
|
|
33612
33666
|
NO_AGENT_PROBE_ENV = "AMA_NO_AGENT_PROBE";
|
|
33613
33667
|
externalTesting = {};
|
|
33668
|
+
HOST_AMA_RUNNER = "host:ama";
|
|
33614
33669
|
ExternalTaskHandle = class {
|
|
33615
33670
|
constructor(inner, reopen) {
|
|
33616
33671
|
this.inner = inner;
|
|
@@ -33689,8 +33744,17 @@ var init_external = __esm({
|
|
|
33689
33744
|
/** 注册表的 `runners(agent)`:ama 子会话类型返回 undefined。 */
|
|
33690
33745
|
runner(agent) {
|
|
33691
33746
|
if (agent.runner === "ama") return void 0;
|
|
33692
|
-
const
|
|
33747
|
+
const runnerName = agent.runner;
|
|
33748
|
+
const hostAma = runnerName === HOST_AMA_RUNNER;
|
|
33749
|
+
const spec2 = hostAma ? "ama" : agent.runner;
|
|
33693
33750
|
const host = this.wiring.hostRunners?.get(spec2);
|
|
33751
|
+
if (hostAma && host === void 0) {
|
|
33752
|
+
const error2 = new AmaError(
|
|
33753
|
+
"agent_host_only",
|
|
33754
|
+
`"ama" was provided by the host and is no longer available`
|
|
33755
|
+
);
|
|
33756
|
+
return { id: spec2, start: () => Promise.reject(error2) };
|
|
33757
|
+
}
|
|
33694
33758
|
let inner;
|
|
33695
33759
|
try {
|
|
33696
33760
|
inner = this.agents.resolve(spec2);
|
|
@@ -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
|
-
/**
|
|
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: {
|
|
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
|
-
/**
|
|
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,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
|
-
|
|
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 {
|
|
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/docs/agents.md
CHANGED
|
@@ -259,7 +259,7 @@ ama 能以各 CLI 自己的账户、模型与权限策略驱动外部编码 Agen
|
|
|
259
259
|
|
|
260
260
|
### 嵌入宿主
|
|
261
261
|
|
|
262
|
-
有宿主(`--host` 或 profile 的 `host`,如嵌入 Armadra)时 ama **不自己启动外部 CLI**:内置外部 Agent 一律不可用,不写进 task 描述,`task(agent="claude")` 以「由宿主提供」失败;只有宿主经 `HostApi.runners.provide(runner)` 注入的 runner 可用,它以同一个 `task(agent=<id>)` 入口出现,同名时替换内置的。`create()` 时已注入的 runner 写进 task 描述(`- <id>: <description>`),之后注入的也能用但不进描述。runner 的 `start` 收到 `prompt`、`cwd`、`mode`(父会话当前模式)、`taskId`、`signal`、`onEvent
|
|
262
|
+
有宿主(`--host` 或 profile 的 `host`,如嵌入 Armadra)时 ama **不自己启动外部 CLI**:内置外部 Agent 一律不可用,不写进 task 描述,`task(agent="claude")` 以「由宿主提供」失败;只有宿主经 `HostApi.runners.provide(runner)` 注入的 runner 可用,它以同一个 `task(agent=<id>)` 入口出现,同名时替换内置的。`create()` 时已注入的 runner 写进 task 描述(`- <id>: <description>`),之后注入的也能用但不进描述。runner 的 `start` 收到 `prompt`、`cwd`、`mode`(父会话当前模式)、`taskId`、`signal`、`onEvent`;不经首次确认。宿主注入的 runner id 也可以是 `ama`(例如画布上另一个 ama 节点):`task(agent="ama")` 交给宿主,不起子会话;没注入时 `ama` 照旧不是可用类型,内置类型(`general` / `explore` / `plan`)始终是 ama 子会话。
|
|
263
263
|
|
|
264
264
|
**在 Armadra 画布终端里直接运行 ama(没有 `--host`)**:此时 ama 按独立模式工作,自己启动的外部 Agent 是这个终端节点里的子进程,不会出现在画布上,也不经连线授权;需要让多个 Agent 在画布上协同,就在画布上连线 Agent 节点,或以宿主模式嵌入 ama。终端带着该节点的身份变量(`ARMADRA_NODE_ID`、`ARMADRA_SESSION_ID`、Hook 端点、`ARMADRA_CANVAS_CONTROL` 等),ama 起子进程时一律剥离(askpass 除外),否则 Armadra 装在 Claude / Codex 上的 Hook 会把子 Agent 的事件记到这个节点名下。
|
|
265
265
|
|