@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
@@ -101,6 +101,12 @@ export declare class UsageError extends AmaError {
101
101
  }
102
102
  export { helpText } from "./help-text.js";
103
103
  export declare function emptyArgs(): ParsedArgs;
104
+ /**
105
+ * ACP terminal 型认证方法的入口(docs/acp.md「无模型时」):客户端把方法的 `args` **追加**到配置好的
106
+ * Agent 启动命令后面(规范原文 append),实际得到 `ama --mode acp … --acp-terminal-auth <id>`。
107
+ * 见到这个标志就不再按启动参数解析,转成对应的 `auth` 子命令;同一条命令里的 `--auth-file`、`--lang` 一并带上。
108
+ */
109
+ export declare const ACP_TERMINAL_AUTH_FLAG = "--acp-terminal-auth";
104
110
  /** 解析 argv(不含 node 与脚本路径);用法错误抛 UsageError(退出码 2)。 */
105
111
  export declare function parseArgs(argv: readonly string[]): ParseResult;
106
112
  /** 子命令内部的小解析器:`--name value` / `--name=value` / 位置参数。 */
package/dist/cli/args.js CHANGED
@@ -311,9 +311,55 @@ function validate(args) {
311
311
  throw new UsageError(m.imagePrintOnly);
312
312
  }
313
313
  }
314
+ /**
315
+ * ACP terminal 型认证方法的入口(docs/acp.md「无模型时」):客户端把方法的 `args` **追加**到配置好的
316
+ * Agent 启动命令后面(规范原文 append),实际得到 `ama --mode acp … --acp-terminal-auth <id>`。
317
+ * 见到这个标志就不再按启动参数解析,转成对应的 `auth` 子命令;同一条命令里的 `--auth-file`、`--lang` 一并带上。
318
+ */
319
+ export const ACP_TERMINAL_AUTH_FLAG = "--acp-terminal-auth";
320
+ const ACP_TERMINAL_AUTH_ARGV = {
321
+ chatgpt: ["login", "chatgpt"],
322
+ "api-key": ["set"],
323
+ };
324
+ function acpTerminalAuth(argv) {
325
+ let id;
326
+ let lang;
327
+ let authFile;
328
+ for (let i = 0; i < argv.length; i++) {
329
+ const token = argv[i] ?? "";
330
+ if (token === "--")
331
+ break;
332
+ if (token === ACP_TERMINAL_AUTH_FLAG)
333
+ id = argv[++i] ?? "";
334
+ else if (token.startsWith(`${ACP_TERMINAL_AUTH_FLAG}=`))
335
+ id = token.slice(ACP_TERMINAL_AUTH_FLAG.length + 1);
336
+ else if (token === "--auth-file")
337
+ authFile = argv[++i];
338
+ else if (token.startsWith("--auth-file="))
339
+ authFile = token.slice("--auth-file=".length);
340
+ else if (token === "--lang" && argv[i + 1] !== undefined)
341
+ lang = choice("lang", argv[++i], LOCALES);
342
+ else if (token.startsWith("--lang="))
343
+ lang = choice("lang", token.slice(7), LOCALES);
344
+ }
345
+ if (id === undefined)
346
+ return undefined;
347
+ const sub = ACP_TERMINAL_AUTH_ARGV[id];
348
+ if (sub === undefined)
349
+ throw new UsageError(msg().cli.args.unknownTerminalAuth(id));
350
+ return {
351
+ kind: "subcommand",
352
+ name: "auth",
353
+ argv: [...sub, ...(authFile !== undefined ? ["--auth-file", authFile] : [])],
354
+ ...(lang !== undefined ? { lang } : {}),
355
+ };
356
+ }
314
357
  /** 解析 argv(不含 node 与脚本路径);用法错误抛 UsageError(退出码 2)。 */
315
358
  export function parseArgs(argv) {
316
359
  const lead = leadingLang(argv);
360
+ const terminalAuth = acpTerminalAuth(argv);
361
+ if (terminalAuth !== undefined)
362
+ return terminalAuth;
317
363
  const first = argv[lead.next];
318
364
  if (first !== undefined && SUBCOMMANDS.includes(first)) {
319
365
  return {
@@ -400,14 +400,25 @@ export async function runCli(argv, deps, io) {
400
400
  io.stderr(msg().cli.bootstrap.runtimeNotAssembled);
401
401
  return ExitCode.RuntimeError;
402
402
  }
403
+ // [ACP-A] ACP 的 stdout 只给协议:bootstrap 之前就接管(宿主 / Hook 加载期的 console.log 也改写到 stderr)
404
+ const early = acp ? takeOverStdout() : undefined;
403
405
  let runtime;
404
406
  try {
405
407
  runtime = await bootstrap(args, deps, io);
406
408
  }
407
409
  catch (error) {
408
- return reportError(error, io);
410
+ try {
411
+ // [ACP-A] --mode acp 没有可用模型:认证门照常握手,会话方法重试 bootstrap(docs/acp-plan.md §2.2)
412
+ if (acp && error instanceof StartupError && error.exitCode === ExitCode.NoModel)
413
+ return await (await import("../modes/acp/acp-auth-gate.js")).runAcpAuthGate(args, deps, io, error);
414
+ return reportError(error, io);
415
+ }
416
+ finally {
417
+ early?.();
418
+ }
409
419
  }
410
- const restore = runtime.mode === "print" || runtime.mode === "rpc" ? takeOverStdout() : () => undefined;
420
+ const restore = early ??
421
+ (runtime.mode === "print" || runtime.mode === "rpc" ? takeOverStdout() : () => undefined);
411
422
  let cleanupFrom = () => undefined;
412
423
  try {
413
424
  if (runtime.mode !== "interactive")
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 组装根:会话事件 → 宿主事件总线(实施计划 §1.4 表)。[B6,ACP-C0 从 compose-session.ts 迁出]
3
+ *
4
+ * hook_executed、session_start / session_shutdown 由 bootstrap 与会话切换发,不在这里桥接;
5
+ * 第三波加 `cache_miss` / `context_pressure`,第五波加子 Agent 与计划事件。
6
+ */
7
+ import type { SessionEvent } from "../agent/types.js";
8
+ import type { AgentEventBus } from "../host/api-impl.js";
9
+ /** §1.4:SessionEvent → AgentEvents。 */
10
+ export declare function bridgeEvent(event: SessionEvent, bus: AgentEventBus): void;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 组装根:会话事件 → 宿主事件总线(实施计划 §1.4 表)。[B6,ACP-C0 从 compose-session.ts 迁出]
3
+ *
4
+ * hook_executed、session_start / session_shutdown 由 bootstrap 与会话切换发,不在这里桥接;
5
+ * 第三波加 `cache_miss` / `context_pressure`,第五波加子 Agent 与计划事件。
6
+ */
7
+ /** §1.4:SessionEvent → AgentEvents。 */
8
+ export function bridgeEvent(event, bus) {
9
+ switch (event.type) {
10
+ case "before_agent_start":
11
+ void bus.emit("before_agent_start", { prompt: event.prompt });
12
+ return;
13
+ case "agent_start":
14
+ case "turn_start":
15
+ case "turn_end":
16
+ case "agent_before_settle":
17
+ void bus.emit(event.type, {});
18
+ return;
19
+ case "agent_end":
20
+ void bus.emit("agent_end", { stopReason: event.stopReason, willRetry: event.willRetry });
21
+ return;
22
+ case "agent_settled":
23
+ void bus.emit("agent_settled", event.warning === undefined ? {} : { warning: event.warning });
24
+ return;
25
+ case "tool_execution_start":
26
+ void bus.emit("tool_call", {
27
+ toolCallId: event.toolCallId,
28
+ toolName: event.toolName,
29
+ input: event.args,
30
+ });
31
+ return;
32
+ case "tool_execution_end":
33
+ void bus.emit("tool_result", {
34
+ toolCallId: event.toolCallId,
35
+ toolName: event.toolName,
36
+ isError: event.isError,
37
+ });
38
+ return;
39
+ case "permission_request":
40
+ void bus.emit("tool_approval_requested", {
41
+ requestId: event.requestId,
42
+ toolName: event.toolName,
43
+ });
44
+ return;
45
+ case "permission_resolved":
46
+ void bus.emit("tool_approval_resolved", {
47
+ requestId: event.requestId,
48
+ decision: event.decision,
49
+ });
50
+ return;
51
+ case "compaction_end":
52
+ if (event.result !== undefined)
53
+ void bus.emit("session_compact", { tokensBefore: event.result.tokensBefore });
54
+ return;
55
+ case "model_changed":
56
+ void bus.emit("model_select", { model: event.model });
57
+ return;
58
+ case "cache_miss": {
59
+ const { type: _type, ...miss } = event;
60
+ void bus.emit("cache_miss", miss);
61
+ return;
62
+ }
63
+ case "context_pressure": {
64
+ const { type: _type, ...pressure } = event;
65
+ void bus.emit("context_pressure", pressure);
66
+ return;
67
+ }
68
+ case "quota_update": {
69
+ const { type: _type, ...quota } = event;
70
+ void bus.emit("quota_update", quota);
71
+ return;
72
+ }
73
+ // [W5-C0] 子 Agent 与计划事件
74
+ case "subagent_start": {
75
+ const { type: _type, ...payload } = event;
76
+ void bus.emit("subagent_start", payload);
77
+ return;
78
+ }
79
+ case "subagent_end": {
80
+ const { type: _type, ...payload } = event;
81
+ void bus.emit("subagent_end", payload);
82
+ return;
83
+ }
84
+ case "plan_proposed": {
85
+ const { type: _type, ...payload } = event;
86
+ void bus.emit("plan_proposed", payload);
87
+ return;
88
+ }
89
+ case "plan_resolved": {
90
+ const { type: _type, ...payload } = event;
91
+ void bus.emit("plan_resolved", payload);
92
+ return;
93
+ }
94
+ default:
95
+ return;
96
+ }
97
+ }
@@ -14,18 +14,21 @@
14
14
  * `AMA_CACHE_RETENTION` 覆盖 → 会话的 `cache` 设置;宿主 `cache.onWarmingDecision` 每次现取。
15
15
  * - 会话切换:`switchSession()` 复用同一份装配材料建新会话,`assembly.onSessionReplaced(next)`
16
16
  * 让宿主访问、Hook 公共字段与退出 dispose 跟随新会话。
17
+ * - [ACP-C0] 多会话:`createSessionAlongside()` 建兄弟会话(不替换前台、不 dispose 旧会话),
18
+ * `setForegroundSession()` 把宿主 / Hook / 工具工厂的「当前会话」切过去,`disposeSessionAlongside()`
19
+ * 释放一个兄弟会话。事件桥接在 compose-events.ts。
17
20
  */
18
21
  import type { ProviderRegistryApi } from "../ai/types.js";
19
22
  import type { BashSandbox } from "../sandbox/bash.js";
20
23
  import { AgentSessionImpl } from "../agent/session.js";
21
- import type { AgentSession, CacheSettings, SessionEvent } from "../agent/types.js";
24
+ import type { AgentSession, CacheSettings } from "../agent/types.js";
22
25
  import { type AmaConfig } from "../config/types.js";
23
- import type { AgentEventBus } from "../host/api-impl.js";
24
26
  import type { Skill } from "../skills/discover.js";
25
27
  import { type PromptTemplate } from "../skills/templates.js";
26
28
  import type { MemoryRuntime } from "../memory/runtime.js";
27
29
  import type { SessionAssembly } from "./deps.js";
28
30
  import type { Runtime } from "./runtime.js";
31
+ export { bridgeEvent } from "./compose-events.js";
29
32
  /** createRuntimeDeps 闭包里跨步骤共享的状态。 */
30
33
  export interface ComposeState {
31
34
  /** 第 10 步发现的完整 Skill(`/skill:` 展开与索引用)。 */
@@ -60,8 +63,6 @@ export declare function cacheSettingsFrom(config: Pick<AmaConfig, "cache">, env?
60
63
  * (协议层取缺省 300 000)。0 关闭;非法的环境变量值忽略并 warning。
61
64
  */
62
65
  export declare function idleTimeoutFrom(config: Pick<AmaConfig, "request">, env?: NodeJS.ProcessEnv, warn?: (message: string) => void): number | undefined;
63
- /** §1.4:SessionEvent → AgentEvents。 */
64
- export declare function bridgeEvent(event: SessionEvent, bus: AgentEventBus): void;
65
66
  /** 第 14 步:组装主会话。 */
66
67
  export declare function composeSession(assembly: SessionAssembly, state?: ComposeState, options?: SessionComposeOptions): AgentSessionImpl;
67
68
  /** 模式层当前应操作的会话(`switchSession` 之后不再是 `Runtime.session`)。 */
@@ -83,3 +84,23 @@ export type SwitchRequest = {
83
84
  * dispose,新会话复用装配材料,宿主总线发 session_start,再跑 SessionStart Hook。
84
85
  */
85
86
  export declare function switchSession(target: Runtime | AgentSession, request: SwitchRequest): Promise<AgentSessionImpl>;
87
+ /**
88
+ * [ACP-C0] 不替换当前会话、不 dispose、不跑 SessionEnd Hook 地再建一个会话(ACP 多会话)。
89
+ * 宿主总线发 `session_start`,再以新会话的公共字段跑 SessionStart Hook(additionalContext 只给它)。
90
+ * 会话自身的 `session_start` 事件由模式在订阅之后调 `announceStart` 发。
91
+ */
92
+ export declare function createSessionAlongside(target: Runtime | AgentSession, request: Extract<SwitchRequest, {
93
+ kind: "new" | "resume";
94
+ }>, options?: {
95
+ createIfMissing?: boolean;
96
+ }): Promise<AgentSessionImpl>;
97
+ /**
98
+ * [ACP-C0] 把宿主(HostApi `session.*`)、Hook 公共字段与工具工厂的「当前会话」切到 `session`,
99
+ * 清掉「本会话允许」的记忆。返回是否真的切换了(已是前台时什么都不做)。
100
+ */
101
+ export declare function setForegroundSession(target: Runtime | AgentSession, session: AgentSessionImpl): boolean;
102
+ /**
103
+ * [ACP-C0] 释放一个兄弟会话:以它的公共字段跑 SessionEnd Hook(reason `switch`)后 dispose。
104
+ * 它是前台时先把前台切到 `fallback`。运行中的会话要先 abort 并等空闲。
105
+ */
106
+ export declare function disposeSessionAlongside(target: Runtime | AgentSession, session: AgentSessionImpl, fallback: AgentSessionImpl): Promise<void>;
@@ -14,6 +14,9 @@
14
14
  * `AMA_CACHE_RETENTION` 覆盖 → 会话的 `cache` 设置;宿主 `cache.onWarmingDecision` 每次现取。
15
15
  * - 会话切换:`switchSession()` 复用同一份装配材料建新会话,`assembly.onSessionReplaced(next)`
16
16
  * 让宿主访问、Hook 公共字段与退出 dispose 跟随新会话。
17
+ * - [ACP-C0] 多会话:`createSessionAlongside()` 建兄弟会话(不替换前台、不 dispose 旧会话),
18
+ * `setForegroundSession()` 把宿主 / Hook / 工具工厂的「当前会话」切过去,`disposeSessionAlongside()`
19
+ * 释放一个兄弟会话。事件桥接在 compose-events.ts。
17
20
  */
18
21
  import { readFileSync } from "node:fs";
19
22
  import { resolveLimits } from "../agent/limits.js";
@@ -33,10 +36,12 @@ import { formatSkillIndex } from "../skills/index-prompt.js";
33
36
  import { expandPromptCommand } from "../skills/templates.js";
34
37
  import { PresetToolRegistry } from "../tools/presets.js";
35
38
  import { openSession } from "./compose-store.js";
39
+ import { bridgeEvent } from "./compose-events.js";
36
40
  import { composeExtensions } from "./compose-extensions.js";
37
41
  import { attachMemory, withMemorySection } from "./compose-memory.js";
38
42
  import { ExitCode } from "./exit-codes.js";
39
43
  import { msg } from "../i18n/index.js";
44
+ export { bridgeEvent } from "./compose-events.js";
40
45
  export function emptyComposeState() {
41
46
  return { skills: [], templates: [], warnings: [], session: undefined };
42
47
  }
@@ -91,7 +96,7 @@ function contextFiles(record) {
91
96
  export function replyLanguageRule(language) {
92
97
  return `Reply to the user in ${language}.`;
93
98
  }
94
- function systemInput(record, active) {
99
+ function systemInput(record, active, hookContextOf) {
95
100
  const system = { contextFiles: contextFiles(record) };
96
101
  const override = record.assembly.overrides?.systemPrompt;
97
102
  if (override?.mode === "replace")
@@ -111,7 +116,7 @@ function systemInput(record, active) {
111
116
  const host = hostInstructions(record);
112
117
  if (host.length > 0)
113
118
  system.hostInstructions = host;
114
- const hookContext = record.hookContext();
119
+ const hookContext = hookContextOf();
115
120
  if (hookContext !== undefined && hookContext !== "")
116
121
  system.hookContext = hookContext;
117
122
  return system;
@@ -119,7 +124,7 @@ function systemInput(record, active) {
119
124
  /** 提示展开前刷新会晚到的系统提示材料(值不变则不产生补丁)。 */
120
125
  function refreshSystem(record, session) {
121
126
  const patch = {};
122
- const hookContext = record.hookContext();
127
+ const hookContext = (record.hookContexts.get(session) ?? record.hookContext)();
123
128
  if (hookContext !== undefined && hookContext !== "")
124
129
  patch.hookContext = hookContext;
125
130
  const host = hostInstructions(record);
@@ -177,98 +182,11 @@ export function idleTimeoutFrom(config, env = process.env, warn = () => undefine
177
182
  }
178
183
  return config.request?.idleTimeoutMs;
179
184
  }
180
- /** §1.4:SessionEvent → AgentEvents。 */
181
- export function bridgeEvent(event, bus) {
182
- switch (event.type) {
183
- case "before_agent_start":
184
- void bus.emit("before_agent_start", { prompt: event.prompt });
185
- return;
186
- case "agent_start":
187
- case "turn_start":
188
- case "turn_end":
189
- case "agent_before_settle":
190
- void bus.emit(event.type, {});
191
- return;
192
- case "agent_end":
193
- void bus.emit("agent_end", { stopReason: event.stopReason, willRetry: event.willRetry });
194
- return;
195
- case "agent_settled":
196
- void bus.emit("agent_settled", event.warning === undefined ? {} : { warning: event.warning });
197
- return;
198
- case "tool_execution_start":
199
- void bus.emit("tool_call", {
200
- toolCallId: event.toolCallId,
201
- toolName: event.toolName,
202
- input: event.args,
203
- });
204
- return;
205
- case "tool_execution_end":
206
- void bus.emit("tool_result", {
207
- toolCallId: event.toolCallId,
208
- toolName: event.toolName,
209
- isError: event.isError,
210
- });
211
- return;
212
- case "permission_request":
213
- void bus.emit("tool_approval_requested", {
214
- requestId: event.requestId,
215
- toolName: event.toolName,
216
- });
217
- return;
218
- case "permission_resolved":
219
- void bus.emit("tool_approval_resolved", {
220
- requestId: event.requestId,
221
- decision: event.decision,
222
- });
223
- return;
224
- case "compaction_end":
225
- if (event.result !== undefined)
226
- void bus.emit("session_compact", { tokensBefore: event.result.tokensBefore });
227
- return;
228
- case "model_changed":
229
- void bus.emit("model_select", { model: event.model });
230
- return;
231
- case "cache_miss": {
232
- const { type: _type, ...miss } = event;
233
- void bus.emit("cache_miss", miss);
234
- return;
235
- }
236
- case "context_pressure": {
237
- const { type: _type, ...pressure } = event;
238
- void bus.emit("context_pressure", pressure);
239
- return;
240
- }
241
- case "quota_update": {
242
- const { type: _type, ...quota } = event;
243
- void bus.emit("quota_update", quota);
244
- return;
245
- }
246
- // [W5-C0] 子 Agent 与计划事件
247
- case "subagent_start": {
248
- const { type: _type, ...payload } = event;
249
- void bus.emit("subagent_start", payload);
250
- return;
251
- }
252
- case "subagent_end": {
253
- const { type: _type, ...payload } = event;
254
- void bus.emit("subagent_end", payload);
255
- return;
256
- }
257
- case "plan_proposed": {
258
- const { type: _type, ...payload } = event;
259
- void bus.emit("plan_proposed", payload);
260
- return;
261
- }
262
- case "plan_resolved": {
263
- const { type: _type, ...payload } = event;
264
- void bus.emit("plan_resolved", payload);
265
- return;
266
- }
267
- default:
268
- return;
269
- }
270
- }
271
- function buildSession(record, manager, model, thinkingLevel) {
185
+ function buildSession(record, manager, model, thinkingLevel,
186
+ /** 这个会话的 SessionStart additionalContext;缺省沿用前台的。 */
187
+ hookContext = record.hookContext,
188
+ /** false:兄弟会话,不改工具工厂的当前会话。 */
189
+ foreground = true) {
272
190
  const { assembly } = record;
273
191
  const registry = assembly.tools;
274
192
  const all = registry
@@ -290,7 +208,7 @@ function buildSession(record, manager, model, thinkingLevel) {
290
208
  unattended: assembly.unattended,
291
209
  approvalTimeoutMs: approvalTimeout(record.options),
292
210
  hooks: assembly.hooks,
293
- system: withMemorySection(systemInput(record, active), record.state.memory, manager),
211
+ system: withMemorySection(systemInput(record, active, hookContext), record.state.memory, manager),
294
212
  compaction: { ...config.compaction },
295
213
  retry: { ...config.retry },
296
214
  expandPrompt: (text) => expandPrompt(record, session, text),
@@ -337,7 +255,10 @@ function buildSession(record, manager, model, thinkingLevel) {
337
255
  attachMemory(session, record.state.memory);
338
256
  session.subscribe((event) => bridgeEvent(event, assembly.events));
339
257
  records.set(session, record);
340
- record.state.session = session;
258
+ record.hookContexts.set(session, hookContext);
259
+ record.live.add(session);
260
+ if (foreground)
261
+ record.state.session = session;
341
262
  return session;
342
263
  }
343
264
  /** 第 14 步:组装主会话。 */
@@ -366,6 +287,8 @@ export function composeSession(assembly, state = emptyComposeState(), options =
366
287
  options,
367
288
  chain,
368
289
  hookContext: () => assembly.sessionStartContext(),
290
+ hookContexts: new WeakMap(),
291
+ live: new Set(),
369
292
  hostTexts: new Map(),
370
293
  log: options.log ?? stderrLog(),
371
294
  };
@@ -376,11 +299,13 @@ export function composeSession(assembly, state = emptyComposeState(), options =
376
299
  // 宿主在会话创建之后注册的工具:追加进当前会话,下次请求以 system 补丁声明(只在末尾追加)。
377
300
  registry.onRegister((tool) => {
378
301
  const active = registry.active().some((t) => t.name === tool.name);
379
- try {
380
- record.current.addTool(tool, active);
381
- }
382
- catch (error) {
383
- record.log("warn", msg().cli.composeSession.appendToolFailed(tool.name, error.message));
302
+ for (const live of record.live) {
303
+ try {
304
+ live.addTool(tool, active);
305
+ }
306
+ catch (error) {
307
+ record.log("warn", msg().cli.composeSession.appendToolFailed(tool.name, error.message));
308
+ }
384
309
  }
385
310
  });
386
311
  }
@@ -437,11 +362,12 @@ export async function switchSession(target, request) {
437
362
  .run("SessionEnd", { reason: request.kind === "new" ? "new" : "switch" })
438
363
  .catch(() => undefined);
439
364
  await old.dispose();
365
+ record.live.delete(old);
440
366
  if (assembly.permission instanceof PermissionPipeline)
441
367
  assembly.permission.clearSessionGrants();
442
368
  let hookContext;
443
369
  record.hookContext = () => hookContext;
444
- const next = buildSession(record, manager, old.model(), old.thinkingLevel());
370
+ const next = buildSession(record, manager, old.model(), old.thinkingLevel(), record.hookContext);
445
371
  record.current = next;
446
372
  assembly.onSessionReplaced(next);
447
373
  const file = next.state.sessionFile;
@@ -456,3 +382,104 @@ export async function switchSession(target, request) {
456
382
  // 会话自身的 session_start 由模式在重新订阅之后调 `announceStart` 发(否则订阅者收不到)。
457
383
  return next;
458
384
  }
385
+ function requireRecord(target) {
386
+ const record = recordOf(target);
387
+ if (record === undefined)
388
+ throw new AmaError("not_implemented", msg().cli.composeSession.notComposed);
389
+ return record;
390
+ }
391
+ /** 按某个会话覆盖 Hook 公共字段(兄弟会话的 SessionStart / SessionEnd 不该带前台会话的 id)。 */
392
+ function hookFieldsOf(session) {
393
+ const file = session.state.sessionFile;
394
+ return {
395
+ sessionId: session.state.sessionId,
396
+ sessionFile: file,
397
+ transcriptPath: file,
398
+ cwd: session.state.cwd,
399
+ model: session.state.model,
400
+ permissionMode: session.state.permissionMode,
401
+ };
402
+ }
403
+ /**
404
+ * [ACP-C0] 不替换当前会话、不 dispose、不跑 SessionEnd Hook 地再建一个会话(ACP 多会话)。
405
+ * 宿主总线发 `session_start`,再以新会话的公共字段跑 SessionStart Hook(additionalContext 只给它)。
406
+ * 会话自身的 `session_start` 事件由模式在订阅之后调 `announceStart` 发。
407
+ */
408
+ export async function createSessionAlongside(target, request, options = {}) {
409
+ const record = requireRecord(target);
410
+ const current = record.current;
411
+ const { assembly } = record;
412
+ const sessionDir = assembly.paths.sessionDir;
413
+ const fresh = (id) => assembly.overrides?.noSession === true
414
+ ? SessionManager.inMemory(current.cwd)
415
+ : SessionManager.create(sessionDirForCwd(sessionDir, current.cwd), current.cwd, id !== undefined ? { id } : {});
416
+ let manager;
417
+ if (request.kind === "new")
418
+ manager = fresh();
419
+ else {
420
+ try {
421
+ manager = openSession({ kind: "resume", id: request.id }, { sessionDir, cwd: current.cwd });
422
+ }
423
+ catch (error) {
424
+ // [ACP] 空会话不落盘:进程重启后客户端再打开它时按原 id 新建一个空会话(调用方负责校验 id)
425
+ if (options.createIfMissing !== true ||
426
+ !(error instanceof AmaError && error.code === "session_not_found"))
427
+ throw error;
428
+ manager = fresh(request.id);
429
+ }
430
+ }
431
+ let hookContext;
432
+ const next = buildSession(record, manager, current.model(), current.thinkingLevel(), () => hookContext, false);
433
+ const file = next.state.sessionFile;
434
+ await assembly.events.emit("session_start", {
435
+ sessionId: next.state.sessionId,
436
+ ...(file !== undefined ? { sessionFile: file } : {}),
437
+ cwd: next.cwd,
438
+ reason: request.kind,
439
+ });
440
+ const outcome = await assembly.hooks
441
+ .run("SessionStart", { source: request.kind }, undefined, hookFieldsOf(next))
442
+ .catch(() => undefined);
443
+ hookContext = outcome?.additionalContext;
444
+ return next;
445
+ }
446
+ /**
447
+ * [ACP-C0] 把宿主(HostApi `session.*`)、Hook 公共字段与工具工厂的「当前会话」切到 `session`,
448
+ * 清掉「本会话允许」的记忆。返回是否真的切换了(已是前台时什么都不做)。
449
+ */
450
+ export function setForegroundSession(target, session) {
451
+ const record = requireRecord(target);
452
+ if (records.get(session) !== record || !record.live.has(session))
453
+ throw new AmaError("invalid_arguments", msg().cli.composeSession.notLiveSession);
454
+ if (record.current === session)
455
+ return false;
456
+ record.current = session;
457
+ record.state.session = session;
458
+ record.hookContext = record.hookContexts.get(session) ?? (() => undefined);
459
+ record.assembly.onSessionReplaced(session);
460
+ const permission = record.assembly.permission;
461
+ if (permission instanceof PermissionPipeline)
462
+ permission.clearSessionGrants();
463
+ return true;
464
+ }
465
+ /**
466
+ * [ACP-C0] 释放一个兄弟会话:以它的公共字段跑 SessionEnd Hook(reason `switch`)后 dispose。
467
+ * 它是前台时先把前台切到 `fallback`。运行中的会话要先 abort 并等空闲。
468
+ */
469
+ export async function disposeSessionAlongside(target, session, fallback) {
470
+ const record = requireRecord(target);
471
+ if (!record.live.has(session))
472
+ return;
473
+ if (session.state.isStreaming)
474
+ throw new AmaError("busy", msg().cli.composeSession.switchWhileStreaming);
475
+ if (record.current === session) {
476
+ if (fallback === session)
477
+ throw new AmaError("invalid_arguments", msg().cli.composeSession.notLiveSession);
478
+ setForegroundSession(target, fallback);
479
+ }
480
+ await record.assembly.hooks
481
+ .run("SessionEnd", { reason: "switch" }, undefined, hookFieldsOf(session))
482
+ .catch(() => undefined);
483
+ await session.dispose();
484
+ record.live.delete(session);
485
+ }
@@ -253,7 +253,12 @@ export function createRuntimeDeps(options = {}) {
253
253
  description: s.description,
254
254
  location: s.location,
255
255
  })),
256
- prompts: prompts.templates.map((t) => ({ name: t.name, path: t.path })),
256
+ prompts: prompts.templates.map((t) => ({
257
+ name: t.name,
258
+ path: t.path,
259
+ description: t.description,
260
+ ...(t.argumentHint !== undefined ? { argumentHint: t.argumentHint } : {}),
261
+ })),
257
262
  warnings,
258
263
  };
259
264
  },
@@ -43,10 +43,12 @@ export interface LoadedResources {
43
43
  description: string;
44
44
  location: string;
45
45
  }[];
46
- /** 提示模板(命令名 → 文件)。 */
46
+ /** 提示模板(命令名 → 文件;`description` / `argumentHint` 取自 frontmatter,ACP 命令表用)。 */
47
47
  prompts: readonly {
48
48
  name: string;
49
49
  path: string;
50
+ description?: string;
51
+ argumentHint?: string;
50
52
  }[];
51
53
  instructions: readonly InstructionSource[];
52
54
  }
@@ -3,13 +3,25 @@
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 { type AuthCliDeps } from "../../auth/chatgpt/cli.js";
12
+ import { type ChoiceOption } from "../choice-prompt.js";
10
13
  import type { CliIo } from "../deps.js";
11
14
  /** 用法文本(按当前语言)。 */
12
15
  export declare function authUsage(): string;
16
+ /** `auth set` 选择器列的供应商:内置、需要 API key、有 key 环境变量(不含 OAuth 的 chatgpt)。 */
17
+ export declare function keyProviders(): {
18
+ id: string;
19
+ name: string;
20
+ }[];
21
+ export interface AuthDeps extends AuthCliDeps {
22
+ /** `auth set` 不给 provider 时的选择器(测试注入);返回下标,取消返回 undefined。缺省方向键选择。 */
23
+ chooseProvider?(question: string, options: readonly ChoiceOption[]): Promise<number | undefined>;
24
+ }
13
25
  /** 取第一行并去掉首尾空白;stdin 为空返回 ""。 */
14
26
  export declare function extractKey(raw: string): string;
15
- export declare function runAuth(argv: readonly string[], io: CliIo, deps?: AuthCliDeps): Promise<number>;
27
+ export declare function runAuth(argv: readonly string[], io: CliIo, deps?: AuthDeps): Promise<number>;