@armadra/agent 0.6.2 → 0.6.4

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 (147) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/CHANGELOG.zh-CN.md +59 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/queue.d.ts +9 -0
  6. package/dist/agent/queue.js +27 -0
  7. package/dist/agent/session-subagent.d.ts +1 -0
  8. package/dist/agent/session-subagent.js +9 -0
  9. package/dist/agent/session.d.ts +11 -0
  10. package/dist/agent/session.js +29 -2
  11. package/dist/agent/subagent-background.d.ts +75 -0
  12. package/dist/agent/subagent-background.js +209 -0
  13. package/dist/agent/subagent-direct.d.ts +5 -2
  14. package/dist/agent/subagent-direct.js +34 -1
  15. package/dist/agent/subagent-registry.d.ts +26 -6
  16. package/dist/agent/subagent-registry.js +66 -94
  17. package/dist/agent/types-w5.d.ts +11 -1
  18. package/dist/agent/types.d.ts +12 -0
  19. package/dist/agents/builtin.js +0 -1
  20. package/dist/agents/external.js +0 -1
  21. package/dist/agents/parse.js +4 -3
  22. package/dist/agents/result.d.ts +7 -1
  23. package/dist/agents/result.js +20 -1
  24. package/dist/agents/task-control.d.ts +13 -2
  25. package/dist/agents/task-record.d.ts +16 -4
  26. package/dist/agents/task-record.js +32 -0
  27. package/dist/agents/types.d.ts +5 -1
  28. package/dist/ai/apis/chatgpt-rate-limits.js +7 -2
  29. package/dist/ai/providers/discovered-cache.d.ts +10 -4
  30. package/dist/ai/providers/discovered-cache.js +30 -14
  31. package/dist/ai/types.d.ts +2 -1
  32. package/dist/auth/chatgpt/backend-client.d.ts +6 -5
  33. package/dist/auth/chatgpt/backend-client.js +8 -7
  34. package/dist/bundle/ama.cjs +2034 -552
  35. package/dist/cli/compose-agents.d.ts +2 -1
  36. package/dist/cli/compose-agents.js +11 -2
  37. package/dist/cli/subcommands/models-discover.d.ts +1 -0
  38. package/dist/cli/subcommands/models-discover.js +12 -2
  39. package/dist/config/json-schema.js +10 -2
  40. package/dist/config/key-docs.js +9 -2
  41. package/dist/config/merge.d.ts +1 -1
  42. package/dist/config/merge.js +20 -3
  43. package/dist/config/schema-w5.js +4 -2
  44. package/dist/config/schema.js +4 -0
  45. package/dist/config/settings-registry.js +5 -0
  46. package/dist/config/types-w5.d.ts +10 -0
  47. package/dist/config/types-w5.js +2 -0
  48. package/dist/config/types.d.ts +5 -1
  49. package/dist/drivers/runner.js +29 -2
  50. package/dist/git/info.d.ts +19 -0
  51. package/dist/git/info.js +64 -8
  52. package/dist/i18n/catalog.d.ts +58 -8
  53. package/dist/i18n/messages/agents.d.ts +60 -0
  54. package/dist/i18n/messages/agents.js +62 -2
  55. package/dist/i18n/messages/config-keys.d.ts +8 -0
  56. package/dist/i18n/messages/config-keys.js +18 -10
  57. package/dist/i18n/messages/config.d.ts +8 -0
  58. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  59. package/dist/i18n/messages/interactive-startup.js +0 -16
  60. package/dist/i18n/messages/interactive-view.d.ts +8 -0
  61. package/dist/i18n/messages/interactive-view.js +10 -0
  62. package/dist/i18n/messages/interactive.d.ts +33 -16
  63. package/dist/i18n/messages/interactive.js +31 -0
  64. package/dist/i18n/messages/print.d.ts +4 -0
  65. package/dist/i18n/messages/print.js +4 -0
  66. package/dist/i18n/messages/report.d.ts +8 -0
  67. package/dist/i18n/messages/report.js +12 -4
  68. package/dist/i18n/messages/settings.d.ts +8 -0
  69. package/dist/i18n/messages/settings.js +8 -0
  70. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  71. package/dist/i18n/messages/subcommands-config.js +4 -0
  72. package/dist/i18n/messages/subcommands.d.ts +4 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/modes/commands-core.js +19 -4
  75. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  76. package/dist/modes/interactive/agent-bar.js +12 -5
  77. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  78. package/dist/modes/interactive/agent-ui.js +82 -12
  79. package/dist/modes/interactive/agent-view.d.ts +9 -0
  80. package/dist/modes/interactive/agent-view.js +28 -4
  81. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  82. package/dist/modes/interactive/approval-dock.js +112 -0
  83. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  84. package/dist/modes/interactive/approval-ui.js +64 -0
  85. package/dist/modes/interactive/commands.js +4 -1
  86. package/dist/modes/interactive/event-notices.d.ts +6 -1
  87. package/dist/modes/interactive/event-notices.js +7 -1
  88. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  89. package/dist/modes/interactive/interactive-mode.js +46 -39
  90. package/dist/modes/interactive/interrupt-send.d.ts +25 -0
  91. package/dist/modes/interactive/interrupt-send.js +32 -0
  92. package/dist/modes/interactive/key-dispatch.d.ts +29 -5
  93. package/dist/modes/interactive/key-dispatch.js +97 -8
  94. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  95. package/dist/modes/interactive/line/line-mode.js +34 -7
  96. package/dist/modes/interactive/run-indicator.d.ts +34 -2
  97. package/dist/modes/interactive/run-indicator.js +62 -8
  98. package/dist/modes/interactive/session-events.js +5 -1
  99. package/dist/modes/interactive/startup-header.d.ts +29 -14
  100. package/dist/modes/interactive/startup-header.js +93 -59
  101. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  102. package/dist/modes/interactive/startup-logo.js +183 -0
  103. package/dist/modes/interactive/status-area.d.ts +18 -0
  104. package/dist/modes/interactive/status-area.js +67 -1
  105. package/dist/modes/interactive/status-bar.d.ts +13 -2
  106. package/dist/modes/interactive/status-bar.js +60 -17
  107. package/dist/modes/interactive/status-line.d.ts +11 -8
  108. package/dist/modes/interactive/status-line.js +52 -39
  109. package/dist/modes/interactive/status-quota.d.ts +58 -0
  110. package/dist/modes/interactive/status-quota.js +155 -0
  111. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  112. package/dist/modes/interactive/subagent-view.js +8 -0
  113. package/dist/modes/interactive/task-background.d.ts +31 -0
  114. package/dist/modes/interactive/task-background.js +68 -0
  115. package/dist/modes/interactive/tool-view.d.ts +7 -1
  116. package/dist/modes/interactive/tool-view.js +24 -1
  117. package/dist/modes/print/print-mode.d.ts +11 -0
  118. package/dist/modes/print/print-mode.js +36 -1
  119. package/dist/modes/rpc/commands.d.ts +4 -1
  120. package/dist/modes/rpc/commands.js +24 -2
  121. package/dist/rpc.d.ts +20 -0
  122. package/dist/rpc.js +3 -0
  123. package/dist/tools/task-ctl.d.ts +2 -0
  124. package/dist/tools/task-ctl.js +7 -2
  125. package/dist/tools/task.d.ts +11 -0
  126. package/dist/tools/task.js +20 -2
  127. package/dist/tools/types.d.ts +6 -0
  128. package/dist/tui/components/editor.d.ts +2 -0
  129. package/dist/tui/components/editor.js +4 -0
  130. package/dist/tui/components/loader.d.ts +5 -1
  131. package/dist/tui/components/loader.js +18 -5
  132. package/dist/tui/keybindings.d.ts +15 -3
  133. package/dist/tui/keybindings.js +15 -3
  134. package/docs/agents.md +52 -28
  135. package/docs/en/host-api.md +5 -1
  136. package/docs/en/providers.md +1 -1
  137. package/docs/en/rpc.md +28 -14
  138. package/docs/en/sessions.md +3 -1
  139. package/docs/en/tui.md +106 -70
  140. package/docs/host-api.md +5 -1
  141. package/docs/providers.md +4 -2
  142. package/docs/rpc.md +28 -14
  143. package/docs/session-format.md +1 -1
  144. package/docs/sessions.md +2 -1
  145. package/docs/tui-design.md +42 -29
  146. package/docs/tui.md +106 -70
  147. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
  * `one-at-a-time`:每个投递点只取一条;`all`:一次取空。abort 不清队列。
5
5
  */
6
6
  import type { QueueMode } from "./types.js";
7
+ import type { ImageBlock } from "../ai/types.js";
7
8
  import type { AgentMessage } from "../session/types.js";
8
9
  export declare class PendingMessageQueue {
9
10
  private items;
@@ -23,3 +24,11 @@ export declare class PendingMessageQueue {
23
24
  }
24
25
  /** 队列消息的显示文本(queue_update 事件用)。 */
25
26
  export declare function queuedText(message: AgentMessage): string;
27
+ /**
28
+ * 打断并立即发送的新回合输入:排队的 steer(按入队顺序)在前、本条在后,文字以空行拼接,图片依次保留。
29
+ * 全部为空时 undefined(不打断)。
30
+ */
31
+ export declare function mergeForInterrupt(queued: readonly AgentMessage[], text: string, images?: readonly ImageBlock[]): {
32
+ text: string;
33
+ images: ImageBlock[];
34
+ } | undefined;
@@ -53,3 +53,30 @@ export function queuedText(message) {
53
53
  return content;
54
54
  return content.map((block) => (block.type === "text" ? block.text : "[image]")).join("");
55
55
  }
56
+ /**
57
+ * 打断并立即发送的新回合输入:排队的 steer(按入队顺序)在前、本条在后,文字以空行拼接,图片依次保留。
58
+ * 全部为空时 undefined(不打断)。
59
+ */
60
+ export function mergeForInterrupt(queued, text, images = []) {
61
+ const texts = [];
62
+ const merged = [];
63
+ for (const message of queued) {
64
+ if (message.role !== "user")
65
+ continue;
66
+ const { content } = message;
67
+ if (typeof content === "string")
68
+ texts.push(content);
69
+ else {
70
+ texts.push(content.map((block) => (block.type === "text" ? block.text : "")).join(""));
71
+ for (const block of content)
72
+ if (block.type === "image")
73
+ merged.push(block);
74
+ }
75
+ }
76
+ texts.push(text);
77
+ merged.push(...images);
78
+ const parts = texts.filter((part) => part.trim() !== "");
79
+ if (parts.length === 0 && merged.length === 0)
80
+ return undefined;
81
+ return { text: parts.join("\n\n"), images: merged };
82
+ }
@@ -54,6 +54,7 @@ export interface ChildSession {
54
54
  readonly messages: readonly AgentMessage[];
55
55
  prompt(text: string, options?: {
56
56
  origin?: MessageOrigin;
57
+ interrupt?: boolean;
57
58
  }): Promise<unknown>;
58
59
  /** [W6-A] 视图里直接发的消息(运行中排到回合结束)。 */
59
60
  followUp?(text: string, options?: {
@@ -186,6 +186,8 @@ async function startAmaChild(parent, spec, createChild, run) {
186
186
  run.signal.addEventListener("abort", onAbort, { once: true });
187
187
  let billed = { cacheRead: 0, prompt: 0, reBilled: 0 };
188
188
  let active = false;
189
+ /** 视图里「打断并发送」开的回合(runOnce 等它们跑完再收尾)。 */
190
+ const forced = [];
189
191
  const runOnce = async (prompt, origin) => {
190
192
  const startTurns = turns;
191
193
  let error;
@@ -196,6 +198,8 @@ async function startAmaChild(parent, spec, createChild, run) {
196
198
  await child.prompt(prompt, origin === undefined ? {} : { origin });
197
199
  // [W6-A] 收尾阶段才入队的 followUp(只可能来自视图)留在队列里:接着再跑一轮
198
200
  for (;;) {
201
+ while (forced.length > 0)
202
+ await forced.shift();
199
203
  const left = child.clearQueue?.().followUp ?? [];
200
204
  if (left.length === 0 || run.signal.aborted)
201
205
  break;
@@ -245,6 +249,11 @@ async function startAmaChild(parent, spec, createChild, run) {
245
249
  }
246
250
  if (!active || child.followUp === undefined)
247
251
  throw new AmaError("task_idle", "the sub-agent is not running");
252
+ if (when === "interrupt") {
253
+ // 同步登记:被中止的那一轮一结束,runOnce 就接着等这一轮
254
+ forced.push(child.prompt(text, { origin: "direct", interrupt: true }));
255
+ return;
256
+ }
248
257
  await child.followUp(text, { origin: "direct" });
249
258
  },
250
259
  wait: () => current,
@@ -19,6 +19,7 @@ import { Agent } from "./agent.js";
19
19
  import type { StreamFn } from "./loop.js";
20
20
  import type { AgentSessionOptions, SessionCore } from "./session-core.js";
21
21
  import { SessionCacheController } from "./session-cache.js";
22
+ import { type BackgroundReason } from "./subagent-registry.js";
22
23
  import { type StaticSystemInput } from "./session-settings.js";
23
24
  import { type RewindDraft, type SummarizeFromResult } from "./session-rewind.js";
24
25
  import type * as CP from "../checkpoints/types.js";
@@ -76,8 +77,18 @@ export declare class AgentSessionImpl implements AgentSession, SessionCore {
76
77
  private enqueueOrRun;
77
78
  steer(text: string, options?: EnqueueOptions): Promise<"queued" | "handled">;
78
79
  followUp(text: string, options?: EnqueueOptions): Promise<"queued" | "handled">;
80
+ /**
81
+ * 打断并立即发送:取走排队的 steer、中止当前周期(工具按 abort 收尾),以「steer… + 本条」开新回合;
82
+ * followUp 留在队列。没有可发的内容 → invalid_arguments(不打断)。
83
+ */
84
+ private interruptWith;
79
85
  abort(): Promise<void>;
80
86
  waitForIdle(): Promise<void>;
87
+ /**
88
+ * [W7-B1] 把阻塞中的前台子 Agent 任务转后台(不给 taskId = 全部):工具调用立即返回,任务继续,完成后
89
+ * 照常 `<task-notification>`。返回被转后台(或被打断等待)的 taskId。
90
+ */
91
+ backgroundTask(taskId?: string, reason?: BackgroundReason): string[];
81
92
  clearQueue(): {
82
93
  steering: string[];
83
94
  followUp: string[];
@@ -13,13 +13,14 @@ import { AmaError } from "../errors.js";
13
13
  import { createSessionClassifier } from "./session-classifier.js";
14
14
  import { buildProjection } from "../session/projection.js";
15
15
  import { Agent } from "./agent.js";
16
- import { queuedText } from "./queue.js";
16
+ import { mergeForInterrupt, queuedText } from "./queue.js";
17
17
  import { resolveRetrySettings } from "./retry.js";
18
18
  import { SessionCacheController, resolveCacheSettings } from "./session-cache.js";
19
19
  import { CompactionController } from "./session-compaction.js";
20
20
  import { makeUserMessage, normalizeOrigin, runPrompt } from "./session-run.js";
21
21
  import { buildSessionState, computeStats, lastAssistantText } from "./session-state.js";
22
22
  import { DEFAULT_SUBAGENT_CONCURRENCY, SubagentPool, runSubagent } from "./session-subagent.js";
23
+ import { registryOf } from "./subagent-registry.js";
23
24
  import { persistMessage, runHookWithEvents, syncSystemMessage } from "./session-sync.js";
24
25
  import { SessionExtensions } from "./session-extensions.js";
25
26
  import { SessionSettings, providerStream } from "./session-settings.js";
@@ -277,6 +278,8 @@ export class AgentSessionImpl {
277
278
  // -------------------------------------------------------------------------
278
279
  async prompt(text, options = {}) {
279
280
  this.assertUsable();
281
+ if (options.interrupt === true && this.cycle !== undefined)
282
+ return this.interruptWith(text, options.images, options.origin);
280
283
  if (this.cycle !== undefined) {
281
284
  const behavior = options.streamingBehavior;
282
285
  if (behavior === undefined) {
@@ -298,7 +301,9 @@ export class AgentSessionImpl {
298
301
  }
299
302
  async enqueueOrRun(text, queue, options) {
300
303
  this.assertUsable();
301
- const origin = normalizeOrigin(options.origin ?? queue);
304
+ if (options.interrupt === true && this.cycle !== undefined)
305
+ return this.interruptWith(text, undefined, options.origin).then(() => "handled");
306
+ const origin = normalizeOrigin(options.origin ?? (options.interrupt === true ? "user" : queue));
302
307
  if (this.cycle === undefined) {
303
308
  await this.startCycle((signal) => runPrompt(this.runDeps(), text, undefined, origin, signal));
304
309
  return "handled";
@@ -312,6 +317,21 @@ export class AgentSessionImpl {
312
317
  followUp(text, options = {}) {
313
318
  return this.enqueueOrRun(text, "followUp", options);
314
319
  }
320
+ /**
321
+ * 打断并立即发送:取走排队的 steer、中止当前周期(工具按 abort 收尾),以「steer… + 本条」开新回合;
322
+ * followUp 留在队列。没有可发的内容 → invalid_arguments(不打断)。
323
+ */
324
+ async interruptWith(text, images, origin) {
325
+ const merged = mergeForInterrupt(this.agent.steeringQueue.snapshot(), text, images);
326
+ if (merged === undefined)
327
+ throw new AmaError("invalid_arguments", "nothing to send");
328
+ this.agent.steeringQueue.clear();
329
+ this.emitQueue();
330
+ while (this.cycle !== undefined)
331
+ await this.abort();
332
+ const kind = normalizeOrigin(origin ?? "interrupt");
333
+ return this.startCycle((s) => runPrompt(this.runDeps(), merged.text, merged.images, kind, s));
334
+ }
315
335
  async abort() {
316
336
  const cycle = this.cycle;
317
337
  if (cycle === undefined)
@@ -323,6 +343,13 @@ export class AgentSessionImpl {
323
343
  waitForIdle() {
324
344
  return this.cycle?.promise ?? Promise.resolve();
325
345
  }
346
+ /**
347
+ * [W7-B1] 把阻塞中的前台子 Agent 任务转后台(不给 taskId = 全部):工具调用立即返回,任务继续,完成后
348
+ * 照常 `<task-notification>`。返回被转后台(或被打断等待)的 taskId。
349
+ */
350
+ backgroundTask(taskId, reason = "host") {
351
+ return registryOf(this.manager.id)?.background(taskId, reason) ?? [];
352
+ }
326
353
  clearQueue() {
327
354
  const cleared = {
328
355
  steering: this.agent.steeringQueue.clear().map(queuedText),
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 子 Agent 的后台化(docs/agents-concurrency-plan.md §2.3、§2.5–§2.6)。[W7-B1]
3
+ *
4
+ * 从 subagent-registry.ts 拆出(保持 ≤ 600 行):
5
+ * - `resolveTaskBackground`:`subagents.background`(auto / always / never)→ 本会话 `task` 的缺省;
6
+ * - 工具结果:`startedResult`(直接后台)、`backgroundedResult`(前台转后台;固定英文文案在
7
+ * agents/result.ts);
8
+ * - `foregroundWaiter`:前台任务的等待者——转后台时以固定文案先行 resolve 工具调用,并解绑父 signal;
9
+ * - `WaitDetach`:`task_ctl wait` 的打断信号(`background()` 触发,按 taskId 或全部);
10
+ * - `TaskNotifier`:完成通知的串行投递——父会话空闲后以 followUp(origin task)开一轮;用户的 steer /
11
+ * followUp 总在通知之前(通知只在空闲时入场),收尾阶段入队的由 W5-H2 的同周期续投接住;每个通知
12
+ * 各开一轮,不合并。
13
+ */
14
+ import type { TaskRecord } from "../agents/task-record.js";
15
+ import type { SubagentResult } from "../tools/types.js";
16
+ import type { SubagentBackgroundEvent } from "./types-w5.js";
17
+ export type BackgroundReason = "user" | "timeout" | "host";
18
+ export type BackgroundSetting = "auto" | "always" | "never";
19
+ /** `auto`:交互 / RPC / ACP 缺省后台,`-p`(无人值守)缺省前台。 */
20
+ export declare function resolveTaskBackground(setting: BackgroundSetting | undefined, unattended: boolean): boolean;
21
+ export declare function startedResult(record: TaskRecord, outputFile?: string): SubagentResult;
22
+ export declare function backgroundedResult(record: TaskRecord, reason: BackgroundReason, afterMs: number, outputFile?: string): SubagentResult;
23
+ /** 前台任务的等待者:`resolve` 让工具调用先返回,`detachParent` 解绑父 signal 与进度回调。 */
24
+ export declare function foregroundWaiter(detachParent: () => void): {
25
+ waiter: NonNullable<TaskRecord["foregroundWaiter"]>;
26
+ promise: Promise<SubagentResult>;
27
+ };
28
+ /** `task_ctl wait` 的打断信号:每个 taskId 一个,`detach` 后换新。 */
29
+ export declare class WaitDetach {
30
+ private readonly controllers;
31
+ private readonly waiting;
32
+ signal(taskId: string): AbortSignal;
33
+ /** 登记一次正在进行的 wait;返回注销函数。 */
34
+ enter(taskId: string): () => void;
35
+ /** 有 wait 在等的 taskId。 */
36
+ waited(): string[];
37
+ /** 打断 `taskId`(缺省全部)上的 wait;返回被打断的 taskId。 */
38
+ detach(taskId?: string): string[];
39
+ }
40
+ export interface NotifierHost {
41
+ followUp?(text: string, options?: {
42
+ origin?: string;
43
+ }): Promise<unknown>;
44
+ waitForIdle?(): Promise<void>;
45
+ log(level: "debug" | "info" | "warn" | "error", message: string): void;
46
+ }
47
+ /** 完成通知的串行投递(按完成顺序;父空闲时投递,每个通知各开一轮)。 */
48
+ export declare class TaskNotifier {
49
+ private readonly host;
50
+ private readonly disposed;
51
+ private delivery;
52
+ constructor(host: NotifierHost, disposed: () => boolean);
53
+ /** 当前投递链(含已入链通知的整个回合)。 */
54
+ pending(): Promise<void>;
55
+ notify(record: TaskRecord, result: SubagentResult): void;
56
+ }
57
+ /** 等任务结束、超时或 signal 触发(后两者返回 undefined);登记进 `waits` 供 `blocking()` 查看。 */
58
+ export declare function waitForTask(record: TaskRecord, timeoutMs: number, signal: AbortSignal, waits: WaitDetach): Promise<SubagentResult | undefined>;
59
+ /** 运行中的前台任务与有 `task_ctl wait` 在等的任务。 */
60
+ export declare function blockingTasks(records: Iterable<TaskRecord>, waits: WaitDetach): string[];
61
+ export interface BackgroundContext {
62
+ emit(event: SubagentBackgroundEvent): void;
63
+ persist(record: TaskRecord): void;
64
+ notify(record: TaskRecord, result: SubagentResult): void;
65
+ /** 超时转后台的阈值(文案里的秒数)。 */
66
+ afterMs: number;
67
+ outputFile(taskId: string): string | undefined;
68
+ }
69
+ /**
70
+ * 前台 → 后台(§2.5.2):只动运行中、仍在前台的任务;置后台、落快照、解绑父 signal、发事件、完成后通知、
71
+ * 让工具调用以固定文案返回。以 `info.status` 为准——已收尾的任务不动(与结束竞争时先到者为准)。
72
+ */
73
+ export declare function backgroundRecords(records: readonly TaskRecord[], reason: BackgroundReason, ctx: BackgroundContext): string[];
74
+ /** 等到没有运行中的任务、投递链也已结束(通知回合里新起的任务继续等);会话关闭立即返回。 */
75
+ export declare function settleTasks(tasks: ReadonlyMap<string, TaskRecord>, notifier: TaskNotifier, disposed: () => boolean): Promise<void>;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * 子 Agent 的后台化(docs/agents-concurrency-plan.md §2.3、§2.5–§2.6)。[W7-B1]
3
+ *
4
+ * 从 subagent-registry.ts 拆出(保持 ≤ 600 行):
5
+ * - `resolveTaskBackground`:`subagents.background`(auto / always / never)→ 本会话 `task` 的缺省;
6
+ * - 工具结果:`startedResult`(直接后台)、`backgroundedResult`(前台转后台;固定英文文案在
7
+ * agents/result.ts);
8
+ * - `foregroundWaiter`:前台任务的等待者——转后台时以固定文案先行 resolve 工具调用,并解绑父 signal;
9
+ * - `WaitDetach`:`task_ctl wait` 的打断信号(`background()` 触发,按 taskId 或全部);
10
+ * - `TaskNotifier`:完成通知的串行投递——父会话空闲后以 followUp(origin task)开一轮;用户的 steer /
11
+ * followUp 总在通知之前(通知只在空闲时入场),收尾阶段入队的由 W5-H2 的同周期续投接住;每个通知
12
+ * 各开一轮,不合并。
13
+ */
14
+ import { backgroundedText, startedText, taskNotification } from "../agents/result.js";
15
+ import { ZERO_USAGE } from "./loop.js";
16
+ /** `auto`:交互 / RPC / ACP 缺省后台,`-p`(无人值守)缺省前台。 */
17
+ export function resolveTaskBackground(setting, unattended) {
18
+ if (setting === "always")
19
+ return true;
20
+ if (setting === "never")
21
+ return false;
22
+ return !unattended;
23
+ }
24
+ function runningResult(record, text, outputFile) {
25
+ const result = {
26
+ text,
27
+ usage: { ...ZERO_USAGE },
28
+ stopReason: "stop",
29
+ isError: false,
30
+ taskId: record.info.taskId,
31
+ status: "running",
32
+ };
33
+ if (outputFile !== undefined)
34
+ result.outputFile = outputFile;
35
+ return result;
36
+ }
37
+ export function startedResult(record, outputFile) {
38
+ return runningResult(record, startedText(record.info.taskId, record.agent.name), outputFile);
39
+ }
40
+ export function backgroundedResult(record, reason, afterMs, outputFile) {
41
+ return runningResult(record, backgroundedText(reason, afterMs), outputFile);
42
+ }
43
+ /** 前台任务的等待者:`resolve` 让工具调用先返回,`detachParent` 解绑父 signal 与进度回调。 */
44
+ export function foregroundWaiter(detachParent) {
45
+ let resolve;
46
+ const promise = new Promise((r) => {
47
+ resolve = r;
48
+ });
49
+ return { waiter: { resolve, detachParent }, promise };
50
+ }
51
+ /** `task_ctl wait` 的打断信号:每个 taskId 一个,`detach` 后换新。 */
52
+ export class WaitDetach {
53
+ controllers = new Map();
54
+ waiting = new Map();
55
+ signal(taskId) {
56
+ let controller = this.controllers.get(taskId);
57
+ if (controller === undefined) {
58
+ controller = new AbortController();
59
+ this.controllers.set(taskId, controller);
60
+ }
61
+ return controller.signal;
62
+ }
63
+ /** 登记一次正在进行的 wait;返回注销函数。 */
64
+ enter(taskId) {
65
+ this.waiting.set(taskId, (this.waiting.get(taskId) ?? 0) + 1);
66
+ return () => {
67
+ const left = (this.waiting.get(taskId) ?? 1) - 1;
68
+ if (left <= 0)
69
+ this.waiting.delete(taskId);
70
+ else
71
+ this.waiting.set(taskId, left);
72
+ };
73
+ }
74
+ /** 有 wait 在等的 taskId。 */
75
+ waited() {
76
+ return [...this.waiting.keys()];
77
+ }
78
+ /** 打断 `taskId`(缺省全部)上的 wait;返回被打断的 taskId。 */
79
+ detach(taskId) {
80
+ const ids = taskId === undefined ? this.waited() : this.waiting.has(taskId) ? [taskId] : [];
81
+ for (const id of taskId === undefined ? [...this.controllers.keys()] : [taskId]) {
82
+ this.controllers.get(id)?.abort();
83
+ this.controllers.delete(id);
84
+ }
85
+ return ids;
86
+ }
87
+ }
88
+ /** 完成通知的串行投递(按完成顺序;父空闲时投递,每个通知各开一轮)。 */
89
+ export class TaskNotifier {
90
+ host;
91
+ disposed;
92
+ delivery = Promise.resolve();
93
+ constructor(host, disposed) {
94
+ this.host = host;
95
+ this.disposed = disposed;
96
+ }
97
+ /** 当前投递链(含已入链通知的整个回合)。 */
98
+ pending() {
99
+ return this.delivery;
100
+ }
101
+ notify(record, result) {
102
+ // 被停止的任务(task_ctl stop、会话关闭)不再通知:发起停止的一方已经知道
103
+ if (this.disposed() || result.status === "aborted")
104
+ return;
105
+ const info = record.info;
106
+ const text = taskNotification({
107
+ taskId: info.taskId,
108
+ agent: info.agent,
109
+ status: (result.status ?? "completed"),
110
+ ...(info.turns === undefined ? {} : { turns: info.turns }),
111
+ ...(info.usage === undefined ? {} : { usage: info.usage }),
112
+ ...(info.outputFile === undefined ? {} : { outputFile: info.outputFile }),
113
+ report: result.text,
114
+ });
115
+ const host = this.host;
116
+ const followUp = host.followUp;
117
+ if (followUp === undefined) {
118
+ host.log("warn", `task ${info.taskId} finished but the session cannot be notified`);
119
+ return;
120
+ }
121
+ // 父空闲时投递(开新回合);父正忙则等这一周期结束再投——周期收尾阶段入队的 followUp 由同周期续投
122
+ // 接住(W5-H2)。串行投递保证按完成顺序到达、各开一轮。
123
+ this.delivery = this.delivery
124
+ .then(async () => {
125
+ await host.waitForIdle?.();
126
+ if (!this.disposed())
127
+ await followUp.call(host, text, { origin: "task" });
128
+ })
129
+ .catch((error) => host.log("warn", `task notification failed: ${String(error)}`));
130
+ }
131
+ }
132
+ /** 等任务结束、超时或 signal 触发(后两者返回 undefined);登记进 `waits` 供 `blocking()` 查看。 */
133
+ export async function waitForTask(record, timeoutMs, signal, waits) {
134
+ const running = record.running;
135
+ if (running === undefined)
136
+ return record.last;
137
+ if (signal.aborted)
138
+ return undefined;
139
+ const leave = waits.enter(record.info.taskId);
140
+ let timer;
141
+ let onAbort;
142
+ const stop = new Promise((resolve) => {
143
+ timer = setTimeout(() => resolve(undefined), timeoutMs);
144
+ onAbort = () => resolve(undefined);
145
+ signal.addEventListener("abort", onAbort, { once: true });
146
+ });
147
+ try {
148
+ return await Promise.race([running, stop]);
149
+ }
150
+ finally {
151
+ clearTimeout(timer);
152
+ if (onAbort !== undefined)
153
+ signal.removeEventListener("abort", onAbort);
154
+ leave();
155
+ }
156
+ }
157
+ /** 运行中的前台任务与有 `task_ctl wait` 在等的任务。 */
158
+ export function blockingTasks(records, waits) {
159
+ const ids = [];
160
+ for (const r of records)
161
+ if (r.foregroundWaiter !== undefined && r.running !== undefined)
162
+ ids.push(r.info.taskId);
163
+ return [...new Set([...ids, ...waits.waited()])];
164
+ }
165
+ /**
166
+ * 前台 → 后台(§2.5.2):只动运行中、仍在前台的任务;置后台、落快照、解绑父 signal、发事件、完成后通知、
167
+ * 让工具调用以固定文案返回。以 `info.status` 为准——已收尾的任务不动(与结束竞争时先到者为准)。
168
+ */
169
+ export function backgroundRecords(records, reason, ctx) {
170
+ const moved = [];
171
+ for (const record of records) {
172
+ const waiter = record.foregroundWaiter;
173
+ const running = record.running;
174
+ if (waiter === undefined || running === undefined || record.info.status !== "running")
175
+ continue;
176
+ delete record.foregroundWaiter;
177
+ record.info.background = true;
178
+ waiter.detachParent();
179
+ ctx.persist(record);
180
+ const taskId = record.info.taskId;
181
+ ctx.emit({
182
+ type: "subagent_background",
183
+ taskId,
184
+ parentToolCallId: record.parentToolCallId,
185
+ reason,
186
+ });
187
+ void running.then((result) => ctx.notify(record, result));
188
+ waiter.resolve(backgroundedResult(record, reason, ctx.afterMs, ctx.outputFile(taskId)));
189
+ moved.push(taskId);
190
+ }
191
+ return moved;
192
+ }
193
+ /** 等到没有运行中的任务、投递链也已结束(通知回合里新起的任务继续等);会话关闭立即返回。 */
194
+ export async function settleTasks(tasks, notifier, disposed) {
195
+ const running = () => [...tasks.values()].flatMap((r) => (r.running === undefined ? [] : [r.running]));
196
+ for (;;) {
197
+ if (disposed())
198
+ return;
199
+ const now = running();
200
+ if (now.length > 0) {
201
+ await Promise.allSettled(now);
202
+ continue;
203
+ }
204
+ const delivery = notifier.pending();
205
+ await delivery;
206
+ if (delivery === notifier.pending() && running().length === 0)
207
+ return;
208
+ }
209
+ }
@@ -7,14 +7,17 @@
7
7
  * 外部 Agent 运行中 / 还在排队 → 等本次运行结束后续聊(`queued`);已结束 → 同 `task_ctl send` 的后台续聊
8
8
  * (`resumed`,完成后父会话照常收 `<task-notification>`)。子会话 user 消息记 `origin: "direct"`;
9
9
  * - `drainDirect`:运行结束时把排着的消息合成一条续聊;任务被停止时丢弃(停止的一方不想它再跑)。
10
+ * - 打断并发送(`interrupt`):ama 运行中 → 子会话 `prompt{interrupt}`(中止本轮、立即开新回合,`interrupted`);
11
+ * 外部 Agent 运行中且驱动能中断回合 → 连同排着的消息一起 `handle.interrupt`(`interrupted`);驱动不能中断
12
+ * 或任务还在并发池排队 → 照常排到运行结束(`queuedNoInterrupt`,视图提示);已结束 → 同上的后台续聊。
10
13
  */
11
14
  import type { TaskLive, TaskRecord } from "../agents/task-record.js";
12
15
  import type { SubagentRequest, SubagentResult } from "../tools/types.js";
13
- export type DirectReply = "steered" | "queued" | "resumed";
16
+ export type DirectReply = "steered" | "queued" | "resumed" | "interrupted" | "queuedNoInterrupt";
14
17
  export interface DirectHost {
15
18
  spawnSubagent?(request: SubagentRequest): Promise<SubagentResult>;
16
19
  log(level: "debug" | "info" | "warn" | "error", message: string): void;
17
20
  }
18
21
  export declare function liveOf(record: TaskRecord): TaskLive;
19
- export declare function directMessage(record: TaskRecord, text: string, host: DirectHost): Promise<DirectReply>;
22
+ export declare function directMessage(record: TaskRecord, text: string, host: DirectHost, interrupt?: boolean): Promise<DirectReply>;
20
23
  export declare function drainDirect(record: TaskRecord, host: DirectHost): void;
@@ -7,6 +7,9 @@
7
7
  * 外部 Agent 运行中 / 还在排队 → 等本次运行结束后续聊(`queued`);已结束 → 同 `task_ctl send` 的后台续聊
8
8
  * (`resumed`,完成后父会话照常收 `<task-notification>`)。子会话 user 消息记 `origin: "direct"`;
9
9
  * - `drainDirect`:运行结束时把排着的消息合成一条续聊;任务被停止时丢弃(停止的一方不想它再跑)。
10
+ * - 打断并发送(`interrupt`):ama 运行中 → 子会话 `prompt{interrupt}`(中止本轮、立即开新回合,`interrupted`);
11
+ * 外部 Agent 运行中且驱动能中断回合 → 连同排着的消息一起 `handle.interrupt`(`interrupted`);驱动不能中断
12
+ * 或任务还在并发池排队 → 照常排到运行结束(`queuedNoInterrupt`,视图提示);已结束 → 同上的后台续聊。
10
13
  */
11
14
  import { AmaError } from "../errors.js";
12
15
  export function liveOf(record) {
@@ -25,11 +28,17 @@ export function liveOf(record) {
25
28
  live.entries = () => entries.call(handle);
26
29
  return live;
27
30
  }
28
- export async function directMessage(record, text, host) {
31
+ export async function directMessage(record, text, host, interrupt = false) {
29
32
  if (text.trim() === "")
30
33
  throw new AmaError("invalid_arguments", "empty message");
31
34
  if (record.running !== undefined) {
32
35
  const handle = record.handle;
36
+ if (interrupt && record.queued !== true && (await interruptRun(record, text)))
37
+ return "interrupted";
38
+ if (interrupt) {
39
+ (record.direct ??= []).push(text);
40
+ return "queuedNoInterrupt";
41
+ }
33
42
  if (handle?.message !== undefined && record.queued !== true) {
34
43
  try {
35
44
  await handle.message(text, "followUp");
@@ -47,6 +56,30 @@ export async function directMessage(record, text, host) {
47
56
  await resumeDirect(record, text, host);
48
57
  return "resumed";
49
58
  }
59
+ /** 打断运行中的任务并立即发送;做不到返回 false(不改动排队)。 */
60
+ async function interruptRun(record, text) {
61
+ const handle = record.handle;
62
+ if (handle?.message !== undefined) {
63
+ try {
64
+ await handle.message(text, "interrupt");
65
+ return true;
66
+ }
67
+ catch (error) {
68
+ if (error instanceof AmaError && error.code === "task_idle")
69
+ return false;
70
+ throw error;
71
+ }
72
+ }
73
+ if (handle?.interrupt === undefined)
74
+ return false;
75
+ // 外部 Agent:排着的消息(运行中在视图里发的)在前,一起作为新回合
76
+ const queued = record.direct ?? [];
77
+ const merged = [...queued, text].join("\n\n");
78
+ if (!(await handle.interrupt(merged)))
79
+ return false;
80
+ queued.splice(0);
81
+ return true;
82
+ }
50
83
  async function resumeDirect(record, text, host) {
51
84
  const spawn = host.spawnSubagent;
52
85
  if (spawn === undefined)
@@ -13,6 +13,7 @@
13
13
  * - 事件 `subagent_start / update / end`;父会话 `custom{ama.task}` 记任务快照(带 `status`),
14
14
  * resume 时据此重建(未完成标 `interrupted`)。
15
15
  * - [W6-A] 子 Agent 视图的 `live()` / `message()`(实现在 subagent-direct.ts)。
16
+ * - [W7-B1] 缺省后台、前台转后台 `background()`、通知投递与 `settled()`(subagent-background.ts)。
16
17
  */
17
18
  import type { SubagentRequest, SubagentResult, SubagentRunner, TaskInfo, TaskRegistryView } from "../tools/types.js";
18
19
  import { AgentCatalog, type AgentModelConfig } from "../agents/catalog.js";
@@ -21,10 +22,13 @@ import { type TaskControl } from "../agents/task-control.js";
21
22
  import type { AgentDefinition, AgentInfo } from "../agents/types.js";
22
23
  import type { SessionCore } from "./session-core.js";
23
24
  import type { SessionTaskStats } from "./types.js";
25
+ import { type DirectReply } from "./subagent-direct.js";
26
+ import { type BackgroundReason } from "./subagent-background.js";
24
27
  export declare const DEFAULT_SUBAGENT_CONCURRENCY = 4;
25
28
  export declare const DEFAULT_MAX_PENDING = 16;
26
29
  export declare const DEFAULT_RETAINED = 16;
27
30
  export { SubagentPool, TASK_CUSTOM_TYPE, type AmaRunnerSpec, type TaskHandle };
31
+ export type { BackgroundReason };
28
32
  export type AmaRunnerFactory = (spec: AmaRunnerSpec) => SubagentRunner;
29
33
  export interface SubagentEnvironment {
30
34
  catalog: AgentCatalog;
@@ -35,6 +39,10 @@ export interface SubagentEnvironment {
35
39
  /** 外部 / 宿主 runner(W5-E `ProcessRunner`、HostApi.runners);没有返回 undefined。 */
36
40
  runners?(agent: AgentDefinition): SubagentRunner | undefined;
37
41
  now?(): number;
42
+ /** [W7-B1] `task` 未指定 background 且类型也没指定时的缺省(`subagents.background` 解析后);缺省前台。 */
43
+ background?: boolean;
44
+ /** [W7-B1] 前台任务运行超过该毫秒数自动转后台(`subagents.autoBackgroundAfterMs`);0 / 不设关闭。 */
45
+ autoBackgroundAfterMs?: number;
38
46
  }
39
47
  /** 注册表需要的父会话能力(`SessionCore`;followUp 为 AgentSessionImpl 的公开方法)。 */
40
48
  export type RegistryHost = Pick<SessionCore, "manager" | "cwd" | "emit" | "appendEntry" | "outputDir" | "log" | "options"> & {
@@ -61,7 +69,8 @@ export declare class SubagentRegistry implements TaskControl {
61
69
  private readonly retained;
62
70
  private seq;
63
71
  private disposed;
64
- private delivery;
72
+ private readonly notifier;
73
+ private readonly waits;
65
74
  constructor(host: RegistryHost, env: SubagentEnvironment);
66
75
  private now;
67
76
  list(): readonly TaskInfo[];
@@ -69,23 +78,34 @@ export declare class SubagentRegistry implements TaskControl {
69
78
  stats(): SessionTaskStats | undefined;
70
79
  /** 运行中任务的已有输出(`task_ctl output`);结束后为最终文本。 */
71
80
  output(taskId: string): string | undefined;
72
- /** 等任务结束或超时;超时返回 undefined。 */
73
- wait(taskId: string, timeoutMs: number): Promise<SubagentResult | undefined>;
81
+ /** 等结束;超时或 signal(缺省本任务的转后台信号,`background()` 触发)返回 undefined。[W7-B1] */
82
+ wait(taskId: string, timeoutMs: number, options?: {
83
+ signal?: AbortSignal;
84
+ }): Promise<SubagentResult | undefined>;
85
+ /** [W7-B1] `task_ctl wait` 用的打断信号(`background()` 触发后换新)。 */
86
+ detachSignal(taskId: string): AbortSignal;
87
+ /** [W7-B1] 正在阻塞父回合的任务:运行中的前台任务与有 `task_ctl wait` 在等的任务。 */
88
+ blocking(): string[];
89
+ /**
90
+ * [W7-B1] 前台任务转后台(§2.5,subagent-background.ts),并打断等它的 `task_ctl wait`;不给 taskId =
91
+ * 全部。返回被转后台或被打断等待的 taskId(不在运行时为空)。
92
+ */
93
+ background(taskId?: string, reason?: BackgroundReason): string[];
94
+ /** [W7-B1] `-p` 收尾:等到没有运行中的任务、通知回合也已跑完;会话关闭立即返回。 */
95
+ settled(): Promise<void>;
74
96
  /** [W6-A] 子 Agent 视图读的实时数据;未知任务 undefined。 */
75
97
  live(taskId: string): TaskLive | undefined;
76
98
  /** [W6-A] 人在子 Agent 视图里发的消息(subagent-direct.ts)。 */
77
- message(taskId: string, text: string): Promise<"steered" | "queued" | "resumed">;
99
+ message(taskId: string, text: string, interrupt?: boolean): Promise<DirectReply>;
78
100
  stop(taskId: string): Promise<SubagentResult | undefined>;
79
101
  run(request: SubagentRequest, ama: AmaRunnerFactory): Promise<SubagentResult>;
80
102
  private continueTask;
81
103
  private launch;
82
- private startedResult;
83
104
  private execute;
84
105
  private handleFor;
85
106
  private emitStart;
86
107
  private sink;
87
108
  private finish;
88
- private notify;
89
109
  private outputDirectory;
90
110
  private outputName;
91
111
  private outputFileFor;