@armadra/agent 0.6.1 → 0.6.3

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 (152) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/CHANGELOG.zh-CN.md +56 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/session-settings.d.ts +4 -1
  6. package/dist/agent/session-settings.js +7 -2
  7. package/dist/agent/session.d.ts +6 -0
  8. package/dist/agent/session.js +8 -0
  9. package/dist/agent/subagent-background.d.ts +75 -0
  10. package/dist/agent/subagent-background.js +209 -0
  11. package/dist/agent/subagent-registry.d.ts +24 -5
  12. package/dist/agent/subagent-registry.js +64 -92
  13. package/dist/agent/types-w5.d.ts +11 -1
  14. package/dist/agent/types.d.ts +5 -0
  15. package/dist/agents/builtin.js +0 -1
  16. package/dist/agents/external.js +0 -1
  17. package/dist/agents/parse.js +4 -3
  18. package/dist/agents/result.d.ts +7 -1
  19. package/dist/agents/result.js +20 -1
  20. package/dist/agents/task-control.d.ts +13 -2
  21. package/dist/agents/task-record.d.ts +13 -1
  22. package/dist/agents/task-record.js +32 -0
  23. package/dist/agents/types.d.ts +5 -1
  24. package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
  25. package/dist/ai/apis/chatgpt-backend.js +12 -4
  26. package/dist/ai/providers/discovered-cache.d.ts +29 -12
  27. package/dist/ai/providers/discovered-cache.js +72 -12
  28. package/dist/ai/providers/registry.js +7 -6
  29. package/dist/ai/types.d.ts +5 -0
  30. package/dist/auth/chatgpt/backend-client.d.ts +14 -4
  31. package/dist/auth/chatgpt/backend-client.js +41 -8
  32. package/dist/auth/chatgpt/cli.d.ts +2 -1
  33. package/dist/auth/chatgpt/cli.js +14 -7
  34. package/dist/auth/chatgpt/follow.d.ts +16 -0
  35. package/dist/auth/chatgpt/follow.js +51 -0
  36. package/dist/auth/chatgpt/presets.d.ts +8 -0
  37. package/dist/auth/chatgpt/presets.js +12 -0
  38. package/dist/auth/testing/fake-oauth.d.ts +4 -1
  39. package/dist/auth/testing/fake-oauth.js +24 -1
  40. package/dist/bundle/ama.cjs +2016 -664
  41. package/dist/cli/compose-agents.d.ts +2 -1
  42. package/dist/cli/compose-agents.js +11 -2
  43. package/dist/cli/compose-providers.d.ts +2 -1
  44. package/dist/cli/compose-providers.js +6 -3
  45. package/dist/cli/startup-steps.js +4 -1
  46. package/dist/cli/subcommands/models-discover.d.ts +5 -2
  47. package/dist/cli/subcommands/models-discover.js +52 -10
  48. package/dist/config/json-schema.js +10 -2
  49. package/dist/config/key-docs.js +9 -3
  50. package/dist/config/merge.d.ts +1 -1
  51. package/dist/config/merge.js +20 -3
  52. package/dist/config/schema-w5.js +4 -2
  53. package/dist/config/schema-w6.d.ts +1 -1
  54. package/dist/config/schema-w6.js +2 -1
  55. package/dist/config/schema.js +2 -0
  56. package/dist/config/settings-registry.js +4 -0
  57. package/dist/config/types-w5.d.ts +10 -0
  58. package/dist/config/types-w5.js +2 -0
  59. package/dist/config/types-w6.d.ts +5 -0
  60. package/dist/config/types.d.ts +3 -1
  61. package/dist/git/info.d.ts +19 -0
  62. package/dist/git/info.js +64 -8
  63. package/dist/i18n/catalog.d.ts +52 -8
  64. package/dist/i18n/messages/agents.d.ts +56 -0
  65. package/dist/i18n/messages/agents.js +58 -2
  66. package/dist/i18n/messages/auth.d.ts +12 -0
  67. package/dist/i18n/messages/auth.js +20 -0
  68. package/dist/i18n/messages/config-keys.d.ts +8 -0
  69. package/dist/i18n/messages/config-keys.js +18 -10
  70. package/dist/i18n/messages/config.d.ts +8 -0
  71. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  72. package/dist/i18n/messages/interactive-startup.js +0 -16
  73. package/dist/i18n/messages/interactive.d.ts +23 -16
  74. package/dist/i18n/messages/interactive.js +25 -0
  75. package/dist/i18n/messages/print.d.ts +4 -0
  76. package/dist/i18n/messages/print.js +4 -0
  77. package/dist/i18n/messages/report.js +4 -4
  78. package/dist/i18n/messages/settings.d.ts +6 -0
  79. package/dist/i18n/messages/settings.js +6 -0
  80. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  81. package/dist/i18n/messages/subcommands-config.js +4 -0
  82. package/dist/i18n/messages/subcommands.d.ts +4 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/modes/commands-core.js +8 -4
  85. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  86. package/dist/modes/interactive/agent-bar.js +12 -5
  87. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  88. package/dist/modes/interactive/agent-ui.js +81 -12
  89. package/dist/modes/interactive/agent-view.d.ts +4 -0
  90. package/dist/modes/interactive/agent-view.js +14 -1
  91. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  92. package/dist/modes/interactive/approval-dock.js +112 -0
  93. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  94. package/dist/modes/interactive/approval-ui.js +64 -0
  95. package/dist/modes/interactive/commands.js +4 -1
  96. package/dist/modes/interactive/event-notices.d.ts +6 -1
  97. package/dist/modes/interactive/event-notices.js +7 -1
  98. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  99. package/dist/modes/interactive/interactive-mode.js +33 -38
  100. package/dist/modes/interactive/key-dispatch.d.ts +21 -4
  101. package/dist/modes/interactive/key-dispatch.js +60 -8
  102. package/dist/modes/interactive/line/line-mode.js +5 -0
  103. package/dist/modes/interactive/run-indicator.d.ts +15 -0
  104. package/dist/modes/interactive/run-indicator.js +37 -5
  105. package/dist/modes/interactive/session-events.js +5 -1
  106. package/dist/modes/interactive/startup-header.d.ts +29 -14
  107. package/dist/modes/interactive/startup-header.js +93 -59
  108. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  109. package/dist/modes/interactive/startup-logo.js +183 -0
  110. package/dist/modes/interactive/status-area.d.ts +18 -0
  111. package/dist/modes/interactive/status-area.js +67 -1
  112. package/dist/modes/interactive/status-bar.d.ts +9 -1
  113. package/dist/modes/interactive/status-bar.js +43 -11
  114. package/dist/modes/interactive/status-line.d.ts +2 -0
  115. package/dist/modes/interactive/status-line.js +11 -6
  116. package/dist/modes/interactive/status-quota.d.ts +44 -0
  117. package/dist/modes/interactive/status-quota.js +135 -0
  118. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  119. package/dist/modes/interactive/subagent-view.js +8 -0
  120. package/dist/modes/interactive/task-background.d.ts +31 -0
  121. package/dist/modes/interactive/task-background.js +68 -0
  122. package/dist/modes/interactive/tool-view.d.ts +7 -1
  123. package/dist/modes/interactive/tool-view.js +24 -1
  124. package/dist/modes/print/print-mode.d.ts +11 -0
  125. package/dist/modes/print/print-mode.js +36 -1
  126. package/dist/modes/rpc/commands.d.ts +2 -1
  127. package/dist/modes/rpc/commands.js +9 -1
  128. package/dist/rpc.d.ts +13 -0
  129. package/dist/rpc.js +3 -0
  130. package/dist/tools/task-ctl.d.ts +2 -0
  131. package/dist/tools/task-ctl.js +7 -2
  132. package/dist/tools/task.d.ts +11 -0
  133. package/dist/tools/task.js +20 -2
  134. package/dist/tui/components/editor.d.ts +2 -0
  135. package/dist/tui/components/editor.js +4 -0
  136. package/dist/tui/components/loader.d.ts +5 -1
  137. package/dist/tui/components/loader.js +18 -5
  138. package/dist/tui/keybindings.d.ts +8 -3
  139. package/dist/tui/keybindings.js +8 -3
  140. package/docs/agents.md +52 -28
  141. package/docs/en/host-api.md +5 -1
  142. package/docs/en/providers.md +8 -4
  143. package/docs/en/rpc.md +19 -7
  144. package/docs/en/sessions.md +3 -1
  145. package/docs/en/tui.md +84 -66
  146. package/docs/host-api.md +5 -1
  147. package/docs/providers.md +29 -12
  148. package/docs/rpc.md +19 -7
  149. package/docs/sessions.md +2 -1
  150. package/docs/tui-design.md +42 -29
  151. package/docs/tui.md +67 -49
  152. package/package.json +1 -1
@@ -13,19 +13,21 @@
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 { tmpdir } from "node:os";
18
19
  import { join } from "node:path";
19
20
  import { AmaError } from "../errors.js";
20
21
  import { DEFAULT_AGENT } from "../agents/builtin.js";
21
22
  import { AgentCatalog, resolveAgentModel } from "../agents/catalog.js";
22
- import { MAX_TURNS_NOTE, capTaskText, taskNotification, writeTaskOutput, } from "../agents/result.js";
23
- import { SubagentPool, TASK_CUSTOM_TYPE, applyRunnerEvent, flushText, newRecord, rebuildRecords, recordData, } from "../agents/task-record.js";
23
+ import { MAX_TURNS_NOTE, capTaskText, writeTaskOutput } from "../agents/result.js";
24
+ import { SubagentPool, TASK_CUSTOM_TYPE, applyRunnerEvent, flushText, newRecord, rebuildRecords, recordData, startEvent, taskStats, } from "../agents/task-record.js";
24
25
  import { registerTaskControl, unregisterTaskControl, } from "../agents/task-control.js";
25
26
  import { ZERO_USAGE } from "./loop.js";
26
27
  import { createWorktree, finishWorktree } from "./worktree.js";
27
28
  import { appendTraceEntry } from "./session-trace-writer.js";
28
29
  import { directMessage, drainDirect, liveOf } from "./subagent-direct.js";
30
+ import { TaskNotifier, WaitDetach, backgroundRecords, blockingTasks, foregroundWaiter, settleTasks, startedResult, waitForTask, } from "./subagent-background.js";
29
31
  export const DEFAULT_SUBAGENT_CONCURRENCY = 4;
30
32
  export const DEFAULT_MAX_PENDING = 16;
31
33
  export const DEFAULT_RETAINED = 16;
@@ -71,7 +73,8 @@ export class SubagentRegistry {
71
73
  retained = [];
72
74
  seq = 0;
73
75
  disposed = false;
74
- delivery = Promise.resolve();
76
+ notifier;
77
+ waits = new WaitDetach();
75
78
  constructor(host, env) {
76
79
  this.host = host;
77
80
  this.env = env;
@@ -80,6 +83,7 @@ export class SubagentRegistry {
80
83
  const rebuilt = rebuildRecords(host.manager.branch(), this.catalog, host.cwd);
81
84
  this.tasks = rebuilt.records;
82
85
  this.seq = rebuilt.seq;
86
+ this.notifier = new TaskNotifier(host, () => this.disposed);
83
87
  }
84
88
  now() {
85
89
  return this.env.now?.() ?? Date.now();
@@ -95,39 +99,48 @@ export class SubagentRegistry {
95
99
  return record === undefined ? undefined : { ...record.info };
96
100
  }
97
101
  stats() {
98
- if (this.tasks.size === 0)
99
- return undefined;
100
- const byStatus = {};
101
- let running = 0;
102
- for (const { info } of this.tasks.values()) {
103
- if (info.status === "running")
104
- running++;
105
- else
106
- byStatus[info.status] = (byStatus[info.status] ?? 0) + 1;
107
- }
108
- return { total: this.tasks.size, running, byStatus };
102
+ return taskStats(this.tasks.values());
109
103
  }
110
104
  /** 运行中任务的已有输出(`task_ctl output`);结束后为最终文本。 */
111
105
  output(taskId) {
112
106
  return this.tasks.get(taskId)?.text;
113
107
  }
114
- /** 等任务结束或超时;超时返回 undefined。 */
115
- async wait(taskId, timeoutMs) {
108
+ /** 等结束;超时或 signal(缺省本任务的转后台信号,`background()` 触发)返回 undefined。[W7-B1] */
109
+ async wait(taskId, timeoutMs, options = {}) {
116
110
  const record = this.tasks.get(taskId);
117
111
  if (record === undefined)
118
112
  throw new AmaError("task_not_found", `unknown task ${taskId}`);
119
- if (record.running === undefined)
120
- return record.last;
121
- let timer;
122
- const timeout = new Promise((resolve) => {
123
- timer = setTimeout(() => resolve(undefined), timeoutMs);
113
+ return waitForTask(record, timeoutMs, options.signal ?? this.waits.signal(taskId), this.waits);
114
+ }
115
+ /** [W7-B1] `task_ctl wait` 用的打断信号(`background()` 触发后换新)。 */
116
+ detachSignal(taskId) {
117
+ return this.waits.signal(taskId);
118
+ }
119
+ /** [W7-B1] 正在阻塞父回合的任务:运行中的前台任务与有 `task_ctl wait` 在等的任务。 */
120
+ blocking() {
121
+ return blockingTasks(this.tasks.values(), this.waits);
122
+ }
123
+ /**
124
+ * [W7-B1] 前台任务转后台(§2.5,subagent-background.ts),并打断等它的 `task_ctl wait`;不给 taskId =
125
+ * 全部。返回被转后台或被打断等待的 taskId(不在运行时为空)。
126
+ */
127
+ background(taskId, reason = "user") {
128
+ if (this.disposed)
129
+ return [];
130
+ const one = taskId === undefined ? undefined : this.tasks.get(taskId);
131
+ const records = taskId === undefined ? [...this.tasks.values()] : one === undefined ? [] : [one];
132
+ const moved = backgroundRecords(records, reason, {
133
+ emit: (event) => this.host.emit(event),
134
+ persist: (record) => this.persist(record),
135
+ notify: (record, result) => this.notifier.notify(record, result),
136
+ afterMs: this.env.autoBackgroundAfterMs ?? 0,
137
+ outputFile: (id) => this.outputFileFor(id),
124
138
  });
125
- try {
126
- return await Promise.race([record.running, timeout]);
127
- }
128
- finally {
129
- clearTimeout(timer);
130
- }
139
+ return [...new Set([...moved, ...this.waits.detach(taskId)])];
140
+ }
141
+ /** [W7-B1] `-p` 收尾:等到没有运行中的任务、通知回合也已跑完;会话关闭立即返回。 */
142
+ settled() {
143
+ return settleTasks(this.tasks, this.notifier, () => this.disposed);
131
144
  }
132
145
  /** [W6-A] 子 Agent 视图读的实时数据;未知任务 undefined。 */
133
146
  live(taskId) {
@@ -170,7 +183,7 @@ export class SubagentRegistry {
170
183
  const model = resolveAgentModel(agent, request.model, this.env.modelConfig ?? {});
171
184
  if (model.warning !== undefined)
172
185
  this.host.log("warn", `agent ${name}: ${model.warning}`);
173
- const background = request.background ?? agent.background;
186
+ const background = request.background ?? agent.background ?? this.env.background ?? false;
174
187
  const taskId = `t${++this.seq}`;
175
188
  const record = newRecord({
176
189
  taskId,
@@ -197,7 +210,8 @@ export class SubagentRegistry {
197
210
  const resumable = record.agent.runner === "ama" ? ref?.sessionFile !== undefined : ref !== undefined;
198
211
  if (record.handle === undefined && !resumable)
199
212
  return Promise.resolve(failed(`Task ${taskId} cannot be continued (its session was not saved).`));
200
- return this.launch(record, request, ama, request.background ?? false);
213
+ const background = request.background ?? record.agent.background ?? this.env.background;
214
+ return this.launch(record, request, ama, background ?? false);
201
215
  }
202
216
  launch(record, request, ama, background) {
203
217
  const controller = new AbortController();
@@ -217,33 +231,35 @@ export class SubagentRegistry {
217
231
  else
218
232
  parentSignal?.addEventListener("abort", onParentAbort, { once: true });
219
233
  this.persist(record);
234
+ let timer;
220
235
  const running = this.execute(record, request, ama, controller.signal).finally(() => {
236
+ clearTimeout(timer);
221
237
  parentSignal?.removeEventListener("abort", onParentAbort);
238
+ delete record.foregroundWaiter;
222
239
  delete record.running;
223
240
  delete record.controller;
224
241
  if (!this.disposed)
225
242
  drainDirect(record, this.host);
226
243
  });
227
244
  record.running = running;
228
- if (!background)
229
- return running;
230
- void running.then((result) => this.notify(record, result));
231
- return Promise.resolve(this.startedResult(record));
232
- }
233
- startedResult(record) {
234
- const result = {
235
- text: `Started background task ${record.info.taskId} (agent ${record.agent.name}). ` +
236
- "A <task-notification> arrives when it finishes; use task_ctl to wait, stop or read output.",
237
- usage: { ...ZERO_USAGE },
238
- stopReason: "stop",
239
- isError: false,
240
- taskId: record.info.taskId,
241
- status: "running",
242
- };
243
245
  const file = this.outputFileFor(record.info.taskId);
244
- if (file !== undefined)
245
- result.outputFile = file;
246
- return result;
246
+ if (background) {
247
+ void running.then((result) => this.notifier.notify(record, result));
248
+ return Promise.resolve(startedResult(record, file));
249
+ }
250
+ // [W7-B1] 前台:转后台(background())时等待者先行 resolve,工具调用立即返回
251
+ const { waiter, promise } = foregroundWaiter(() => {
252
+ clearTimeout(timer);
253
+ parentSignal?.removeEventListener("abort", onParentAbort);
254
+ delete record.onUpdate;
255
+ });
256
+ record.foregroundWaiter = waiter;
257
+ const after = this.env.autoBackgroundAfterMs ?? 0;
258
+ if (after > 0) {
259
+ timer = setTimeout(() => this.background(record.info.taskId, "timeout"), after);
260
+ timer.unref?.();
261
+ }
262
+ return Promise.race([running, promise]);
247
263
  }
248
264
  async execute(record, request, ama, signal) {
249
265
  record.queued = true;
@@ -332,21 +348,7 @@ export class SubagentRegistry {
332
348
  return handle;
333
349
  }
334
350
  emitStart(record, handle) {
335
- const event = {
336
- type: "subagent_start",
337
- taskId: record.info.taskId,
338
- parentToolCallId: record.parentToolCallId,
339
- agent: record.agent.name,
340
- runner: record.agent.runner,
341
- description: record.info.description,
342
- background: record.info.background,
343
- cwd: record.cwd,
344
- };
345
- if (handle.model !== undefined)
346
- event.model = handle.model;
347
- if (handle.sessionFile !== undefined)
348
- event.sessionFile = handle.sessionFile;
349
- this.host.emit(event);
351
+ this.host.emit(startEvent(record, handle));
350
352
  }
351
353
  sink() {
352
354
  return {
@@ -425,36 +427,6 @@ export class SubagentRegistry {
425
427
  this.retain(info.taskId);
426
428
  return final;
427
429
  }
428
- notify(record, result) {
429
- // 被停止的任务(task_ctl stop、会话关闭)不再通知:发起停止的一方已经知道
430
- if (this.disposed || result.status === "aborted")
431
- return;
432
- const info = record.info;
433
- const text = taskNotification({
434
- taskId: info.taskId,
435
- agent: info.agent,
436
- status: (result.status ?? "completed"),
437
- ...(info.turns === undefined ? {} : { turns: info.turns }),
438
- ...(info.usage === undefined ? {} : { usage: info.usage }),
439
- ...(info.outputFile === undefined ? {} : { outputFile: info.outputFile }),
440
- report: result.text,
441
- });
442
- const host = this.host;
443
- const followUp = host.followUp;
444
- if (followUp === undefined) {
445
- host.log("warn", `task ${info.taskId} finished but the session cannot be notified`);
446
- return;
447
- }
448
- // 父空闲时投递(开新回合);父正忙则等这一周期结束再投——周期收尾阶段入队的 followUp 会滞留到
449
- // 下一次提示。串行投递保证按完成顺序到达。
450
- this.delivery = this.delivery
451
- .then(async () => {
452
- await host.waitForIdle?.();
453
- if (!this.disposed)
454
- await followUp.call(host, text, { origin: "task" });
455
- })
456
- .catch((error) => host.log("warn", `task notification failed: ${String(error)}`));
457
- }
458
430
  // -------------------------------------------------------------------------
459
431
  // 落盘、保留与重建
460
432
  // -------------------------------------------------------------------------
@@ -80,6 +80,16 @@ export interface SubagentEndEvent {
80
80
  changed: boolean;
81
81
  };
82
82
  }
83
+ /**
84
+ * [W7-B1] 前台任务转后台(docs/agents-concurrency-plan.md §2.5):`user` = 人(TUI `Ctrl+B` 等),
85
+ * `timeout` = `subagents.autoBackgroundAfterMs` 到时,`host` = 宿主 / SDK(`session.backgroundTask`)。
86
+ */
87
+ export interface SubagentBackgroundEvent {
88
+ type: "subagent_background";
89
+ taskId: string;
90
+ parentToolCallId: string;
91
+ reason: "user" | "timeout" | "host";
92
+ }
83
93
  export interface PlanStep {
84
94
  /** `S1`、`S2` …;编号列表时按序号生成。 */
85
95
  id: string;
@@ -161,7 +171,7 @@ export type SessionEventW5 =
161
171
  /** 流式中 ≤ 2 Hz(`ui.animation: false` 时不发);数据从 `getStats().telemetry` 取。 */
162
172
  {
163
173
  type: "telemetry_tick";
164
- } | SubagentStartEvent | SubagentUpdateEvent | SubagentEndEvent | PlanProposedEvent | PlanResolvedEvent | TodoUpdatedEvent | LimitReachedEvent | ModelFallbackEvent | BackgroundJobEvent;
174
+ } | SubagentStartEvent | SubagentUpdateEvent | SubagentEndEvent | SubagentBackgroundEvent | PlanProposedEvent | PlanResolvedEvent | TodoUpdatedEvent | LimitReachedEvent | ModelFallbackEvent | BackgroundJobEvent;
165
175
  /** 外部 Agent 记账(`custom{ama.agent-usage}` 汇总,§5.4)。 */
166
176
  export interface ExternalAgentStats {
167
177
  byAgent: Record<string, {
@@ -397,6 +397,11 @@ export interface AgentSession {
397
397
  steering: string[];
398
398
  followUp: string[];
399
399
  };
400
+ /**
401
+ * [W7-B1] 阻塞中的前台子 Agent 任务转后台(不给 taskId = 全部;docs/agents-concurrency-plan.md §2.5)。
402
+ * `reason` 缺省 `"host"`(宿主 / SDK);TUI 的人工操作传 `"user"`。返回被转后台或被打断等待的 taskId。
403
+ */
404
+ backgroundTask(taskId?: string, reason?: "user" | "timeout" | "host"): string[];
400
405
  subscribe(listener: (event: SessionEvent) => void): () => void;
401
406
  compact(instructions?: string): Promise<CompactionResult>;
402
407
  fork(entryId: string): Promise<AgentSession>;
@@ -17,7 +17,6 @@ function builtin(name, description, prompt, permissionMode) {
17
17
  model: "inherit",
18
18
  maxTurns: 30,
19
19
  isolation: "none",
20
- background: false,
21
20
  runner: "ama",
22
21
  prompt,
23
22
  source: "builtin",
@@ -48,7 +48,6 @@ function definition(name, runner, description, source) {
48
48
  model: "inherit",
49
49
  maxTurns: 30,
50
50
  isolation: "none",
51
- background: false,
52
51
  // 驱动表 id(gemini 等)与宿主 id 不在 AgentRunnerSpec 的字面量里,线上照样是字符串
53
52
  runner: runner,
54
53
  prompt: "",
@@ -85,8 +85,8 @@ export function parseAgentDefinition(text, filePath, source) {
85
85
  const isolation = data["isolation"] ?? "none";
86
86
  if (isolation !== "none" && isolation !== "worktree")
87
87
  return fail("isolation must be none or worktree");
88
- const background = data["background"] ?? false;
89
- if (typeof background !== "boolean")
88
+ const background = data["background"];
89
+ if (background !== undefined && typeof background !== "boolean")
90
90
  return fail("background must be true or false");
91
91
  if (runner !== "ama") {
92
92
  const ignored = ["tools", "disallowed-tools", "permission-mode"].filter((key) => data[key] !== undefined);
@@ -100,12 +100,13 @@ export function parseAgentDefinition(text, filePath, source) {
100
100
  model: model.trim(),
101
101
  maxTurns,
102
102
  isolation,
103
- background,
104
103
  runner,
105
104
  prompt: fm.body.trim(),
106
105
  source,
107
106
  filePath,
108
107
  };
108
+ if (background !== undefined)
109
+ agent.background = background;
109
110
  if (tools !== undefined)
110
111
  agent.tools = tools;
111
112
  if (disallowedTools !== undefined)
@@ -28,5 +28,11 @@ export interface NotificationInput {
28
28
  report: string;
29
29
  }
30
30
  export declare function taskNotification(input: NotificationInput): string;
31
+ export declare const NOTIFY_HINT = "A <task-notification> arrives when it finishes; use task_ctl to wait, stop or read output.";
32
+ export declare function startedText(taskId: string, agent: string): string;
33
+ /** 前台转后台的工具结果(§2.5.3):固定英文;task 工具在前面加 `[task tN] `。 */
34
+ export declare function backgroundedText(reason: "user" | "timeout" | "host", afterMs: number): string;
35
+ /** `task_ctl wait` 被转后台打断时的文案(§2.5.4)。 */
36
+ export declare function waitDetachedText(taskId: string): string;
31
37
  /** 系统提示 rules 节的一句(task 工具的 promptGuidelines;会话开始即固定)。 */
32
- export declare const TASK_NOTIFICATION_RULE = "A <task-notification> message is a background task's report, not the user speaking.";
38
+ export declare const TASK_NOTIFICATION_RULE: string;
@@ -63,5 +63,24 @@ export function taskNotification(input) {
63
63
  attrs.push(`outputFile="${attr(input.outputFile)}"`);
64
64
  return `<task-notification ${attrs.join(" ")}>\n${input.report}\n</task-notification>`;
65
65
  }
66
+ // [W7-B1] 后台任务的固定英文文案(docs/agents-concurrency-plan.md §2.5;工具结果不进缓存前缀,但要
67
+ // 稳定,便于测试与模型学习)。
68
+ export const NOTIFY_HINT = "A <task-notification> arrives when it finishes; use task_ctl to wait, stop or read output.";
69
+ export function startedText(taskId, agent) {
70
+ return `Started background task ${taskId} (agent ${agent}). ${NOTIFY_HINT}`;
71
+ }
72
+ /** 前台转后台的工具结果(§2.5.3):固定英文;task 工具在前面加 `[task tN] `。 */
73
+ export function backgroundedText(reason, afterMs) {
74
+ const head = reason === "timeout"
75
+ ? `Still running after ${Math.round(afterMs / 1000)}s and moved to the background`
76
+ : "Moved to the background by the user";
77
+ return `${head}; it was not interrupted. ${NOTIFY_HINT} Do not wait for it unless the user asks.`;
78
+ }
79
+ /** `task_ctl wait` 被转后台打断时的文案(§2.5.4)。 */
80
+ export function waitDetachedText(taskId) {
81
+ return (`Task ${taskId} is still running and was moved to the background by the user; do not wait ` +
82
+ "again — a <task-notification> arrives when it finishes.");
83
+ }
66
84
  /** 系统提示 rules 节的一句(task 工具的 promptGuidelines;会话开始即固定)。 */
67
- export const TASK_NOTIFICATION_RULE = "A <task-notification> message is a background task's report, not the user speaking.";
85
+ export const TASK_NOTIFICATION_RULE = "A <task-notification> is a background task's report, not the user speaking; never sleep or " +
86
+ "poll for it.";
@@ -8,9 +8,20 @@ import type { SubagentResult, TaskRegistryView } from "../tools/types.js";
8
8
  export interface TaskControl extends TaskRegistryView {
9
9
  /** 运行中是已有输出,结束后是最终文本。 */
10
10
  output(taskId: string): string | undefined;
11
- /** 等结束或超时(超时返回 undefined);未知 taskId 抛 `task_not_found`。 */
12
- wait(taskId: string, timeoutMs: number): Promise<SubagentResult | undefined>;
11
+ /**
12
+ * 等结束、超时或 `signal` 触发(后两者返回 undefined);未知 taskId 抛 `task_not_found`。
13
+ * [W7-B1] 不给 signal 时由 `background()` 打断(见 `detachSignal`)。
14
+ */
15
+ wait(taskId: string, timeoutMs: number, options?: {
16
+ signal?: AbortSignal;
17
+ }): Promise<SubagentResult | undefined>;
13
18
  stop(taskId: string): Promise<SubagentResult | undefined>;
19
+ /** [W7-B1] 前台任务转后台(不给 taskId = 全部);返回被转后台或被打断等待的 taskId。 */
20
+ background(taskId?: string): string[];
21
+ /** [W7-B1] 该任务的转后台信号:`background()` 触发后 abort(下次取到新的)。 */
22
+ detachSignal(taskId: string): AbortSignal;
23
+ /** [W7-B1] 正在阻塞父回合的任务(前台运行中、或有 `task_ctl wait` 在等)。 */
24
+ blocking(): string[];
14
25
  }
15
26
  export declare function registerTaskControl(sessionId: string, control: TaskControl): void;
16
27
  export declare function unregisterTaskControl(sessionId: string, control: TaskControl): void;
@@ -3,7 +3,7 @@
3
3
  * `custom{ama.task}` 快照的写入形状与 resume 重建(docs/wave5-plan.md §7.4–§7.5)。[W5-G]
4
4
  * 编排在 agent/subagent-registry.ts。
5
5
  */
6
- import type { SubagentUpdateEvent } from "../agent/types-w5.js";
6
+ import type { SessionTaskStats, SubagentStartEvent, SubagentUpdateEvent } from "../agent/types-w5.js";
7
7
  import type { MessageOrigin } from "../ai/types.js";
8
8
  import type { SessionEvent } from "../agent/types.js";
9
9
  import type { TraceExternalTurnData } from "../trace/types.js";
@@ -130,6 +130,14 @@ export interface TaskRecord {
130
130
  direct?: string[];
131
131
  /** [W6-A] 下一次续聊是人在视图里发的(子会话 user 消息记 `origin: "direct"`)。 */
132
132
  directNext?: boolean;
133
+ /**
134
+ * [W7-B1] 前台等待者:launch 时建,转后台时 resolve(工具调用先返回);结束时删掉。
135
+ * `detachParent` 解绑父 signal 与进度回调(父回合 Esc 不再连带停止它)。
136
+ */
137
+ foregroundWaiter?: {
138
+ resolve(result: SubagentResult): void;
139
+ detachParent(): void;
140
+ };
133
141
  }
134
142
  export declare function newRecord(info: TaskInfo, agent: AgentDefinition, parentToolCallId: string, cwd: string, isolation?: "none" | "worktree"): TaskRecord;
135
143
  export interface ProgressSink {
@@ -142,6 +150,10 @@ export interface ProgressSink {
142
150
  export declare function flushText(record: TaskRecord, sink: ProgressSink): void;
143
151
  /** runner 进度 → 记录与 `subagent_update`(文本 ≥ 250 ms 合并,轮次与结束时冲刷)。 */
144
152
  export declare function applyRunnerEvent(record: TaskRecord, event: SubagentEvent, sink: ProgressSink): void;
153
+ /** `subagent_start` 事件(新开或续聊都发)。 */
154
+ export declare function startEvent(record: TaskRecord, handle: TaskHandle): SubagentStartEvent;
155
+ /** `getStats().tasks`:没有任务时 undefined。 */
156
+ export declare function taskStats(records: Iterable<TaskRecord>): SessionTaskStats | undefined;
145
157
  /** 父会话 `custom{ama.task}` 的 data:TaskInfo + 父 toolCallId + 运行目录(带 `status`)。 */
146
158
  export declare function recordData(record: TaskRecord): Record<string, unknown>;
147
159
  /** 从父会话分支重建(同一 taskId 取最后一条;未结束的标 interrupted);返回最大序号。 */
@@ -209,6 +209,38 @@ function recordDisplay(record, event, now) {
209
209
  return;
210
210
  }
211
211
  }
212
+ /** `subagent_start` 事件(新开或续聊都发)。 */
213
+ export function startEvent(record, handle) {
214
+ const event = {
215
+ type: "subagent_start",
216
+ taskId: record.info.taskId,
217
+ parentToolCallId: record.parentToolCallId,
218
+ agent: record.agent.name,
219
+ runner: record.agent.runner,
220
+ description: record.info.description,
221
+ background: record.info.background,
222
+ cwd: record.cwd,
223
+ };
224
+ if (handle.model !== undefined)
225
+ event.model = handle.model;
226
+ if (handle.sessionFile !== undefined)
227
+ event.sessionFile = handle.sessionFile;
228
+ return event;
229
+ }
230
+ /** `getStats().tasks`:没有任务时 undefined。 */
231
+ export function taskStats(records) {
232
+ const byStatus = {};
233
+ let total = 0;
234
+ let running = 0;
235
+ for (const { info } of records) {
236
+ total++;
237
+ if (info.status === "running")
238
+ running++;
239
+ else
240
+ byStatus[info.status] = (byStatus[info.status] ?? 0) + 1;
241
+ }
242
+ return total === 0 ? undefined : { total, running, byStatus };
243
+ }
212
244
  /** 父会话 `custom{ama.task}` 的 data:TaskInfo + 父 toolCallId + 运行目录(带 `status`)。 */
213
245
  export function recordData(record) {
214
246
  return { ...record.info, parentToolCallId: record.parentToolCallId, cwd: record.cwd };
@@ -25,7 +25,11 @@ export interface AgentDefinition {
25
25
  /** 缺省 30。 */
26
26
  maxTurns: number;
27
27
  isolation: "none" | "worktree";
28
- background: boolean;
28
+ /**
29
+ * 显式覆盖缺省前台 / 后台;不设由 `subagents.background` 决定([W7-B1],优先级:调用参数 > 类型
30
+ * 定义 > 配置)。
31
+ */
32
+ background?: boolean;
29
33
  runner: AgentRunnerSpec;
30
34
  /** 正文:追加到子会话系统提示末尾的角色说明。 */
31
35
  prompt: string;
@@ -10,7 +10,9 @@
10
10
  * - 出错恢复(每种至多一次):401 → 强制刷新重试;SIWC 400 `subscription_sharing_unsupported_capability` → 删去
11
11
  * `error.param` 重试;codex 400 `Instructions are not valid` → 本会话切 developer-message 重试;
12
12
  * - 错误映射(文案带码前缀,宿主按码判断):429 配额 → `quota_exceeded`(不重试);失效 → `auth_expired`;
13
- * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`;503 留给会话层现有重试;
13
+ * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);503 留给会话层现有重试;
14
+ * - 渠道跟随登录方式由会话层做(auth/chatgpt/follow.ts);到这里仍不符说明用户显式写了 `@渠道`,报
15
+ * `chatgpt_flavor_mismatch`(i18n);
14
16
  * - 用量:`cost = 0`、`billing: "subscription"`。
15
17
  */
16
18
  import { type LiveToken } from "../../auth/oauth/live.js";
@@ -25,7 +27,10 @@ export declare function sessionKeyOf(model: Model, options: StreamOptions): stri
25
27
  export declare function resetChatGptState(): void;
26
28
  export declare function instructionsModeOf(model: Model, compat: OpenAIResponsesCompat, options: StreamOptions): "native" | "developer-message";
27
29
  export declare function transformChatGptBody(body: Json, model: Model, compat: OpenAIResponsesCompat, options: StreamOptions): Json;
28
- /** 发请求前:needsLogin 直接报 auth_expired;登录 flavor 与渠道不符报错。 */
30
+ /**
31
+ * 发请求前:needsLogin 直接报 auth_expired;登录 flavor 与渠道不符报错(没写 `@渠道` 的模型会话层已改到登录的
32
+ * 渠道,走到这里的是显式指定了另一渠道)。
33
+ */
29
34
  export declare function chatgptPrecheck(model: Model, compat: OpenAIResponsesCompat, apiKey?: string): void;
30
35
  export declare function chatgptHeaders(compat: OpenAIResponsesCompat, options: StreamOptions, apiKey: string | undefined): Record<string, string>;
31
36
  /** 包一层 onResponse:解析响应头里的配额。 */
@@ -10,11 +10,14 @@
10
10
  * - 出错恢复(每种至多一次):401 → 强制刷新重试;SIWC 400 `subscription_sharing_unsupported_capability` → 删去
11
11
  * `error.param` 重试;codex 400 `Instructions are not valid` → 本会话切 developer-message 重试;
12
12
  * - 错误映射(文案带码前缀,宿主按码判断):429 配额 → `quota_exceeded`(不重试);失效 → `auth_expired`;
13
- * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`;503 留给会话层现有重试;
13
+ * SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);503 留给会话层现有重试;
14
+ * - 渠道跟随登录方式由会话层做(auth/chatgpt/follow.ts);到这里仍不符说明用户显式写了 `@渠道`,报
15
+ * `chatgpt_flavor_mismatch`(i18n);
14
16
  * - 用量:`cost = 0`、`billing: "subscription"`。
15
17
  */
16
18
  import { liveToken } from "../../auth/oauth/live.js";
17
19
  import { authExpiredError } from "../../auth/oauth/refresh.js";
20
+ import { msg } from "../../i18n/index.js";
18
21
  import { parseQuotaHeaders, parseRateLimitEvent, quotaFromLimitError, } from "./chatgpt-rate-limits.js";
19
22
  export const SIWC_FORBIDDEN_FIELDS = [
20
23
  "background",
@@ -81,7 +84,10 @@ export function transformChatGptBody(body, model, compat, options) {
81
84
  out["input"] = input;
82
85
  return out;
83
86
  }
84
- /** 发请求前:needsLogin 直接报 auth_expired;登录 flavor 与渠道不符报错。 */
87
+ /**
88
+ * 发请求前:needsLogin 直接报 auth_expired;登录 flavor 与渠道不符报错(没写 `@渠道` 的模型会话层已改到登录的
89
+ * 渠道,走到这里的是显式指定了另一渠道)。
90
+ */
85
91
  export function chatgptPrecheck(model, compat, apiKey) {
86
92
  const live = liveToken(apiKey);
87
93
  if (live === undefined)
@@ -89,7 +95,9 @@ export function chatgptPrecheck(model, compat, apiKey) {
89
95
  if (live.needsLogin === true)
90
96
  throw authExpiredError(live.provider);
91
97
  if (live.flavor !== compat.chatgptBackend) {
92
- throw new Error(`chatgpt_flavor_mismatch: logged in with ${live.flavor} but ${model.provider}/${model.id} uses the ${String(compat.chatgptBackend)} channel; run \`ama auth login ${live.provider} --flavor ${String(compat.chatgptBackend)}\` or switch channel`);
98
+ const pinned = String(compat.chatgptBackend);
99
+ const ref = `${model.provider}/${model.id}@${pinned}`;
100
+ throw new Error(`chatgpt_flavor_mismatch: ${msg().auth.request.flavorMismatch(live.flavor, ref, pinned)}`);
93
101
  }
94
102
  }
95
103
  export function chatgptHeaders(compat, options, apiKey) {
@@ -196,7 +204,7 @@ export function recoverChatGptError(error, model, compat, options, apiKey, tried
196
204
  code === "subscription_sharing_user_not_eligible")
197
205
  return {
198
206
  kind: "fail",
199
- error: new Error("not_eligible: this ChatGPT account cannot share plan usage with ama (403 subscription_sharing_user_not_eligible)"),
207
+ error: new Error(`not_eligible: ${msg().auth.request.notEligible}`),
200
208
  };
201
209
  if (backend === "siwc" &&
202
210
  error.status === 400 &&
@@ -3,10 +3,17 @@
3
3
  *
4
4
  * 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
5
5
  * `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
6
- * (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
7
- * `ama auth logout chatgpt` 删掉它。只存 slug、显示名、flavor 与时间戳——没有 token、账户 id。
6
+ * (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商。`ama auth logout chatgpt` 删掉它。只存 slug、显示名、
7
+ * 后端给的元数据(上下文窗口、输入模态、推理强度)、flavor 与时间戳——没有 token、账户 id。
8
+ *
9
+ * 元数据优先级:后端明确给的 > models.dev(快照 ⊕ 用户刷新)> 自定义缺省。上下文窗口尤其如此:订阅后端的生效
10
+ * 窗口(如 codex 的 272k)可能远小于 models.dev 记的 API 版窗口(1.1M),按后者算压缩阈值会让请求超窗被拒。
11
+ * 旧版本写的缓存(siwc 不含上下文窗口)照旧用 models.dev;`chatgpt` 两边都没有时用保守缺省
12
+ * `SUBSCRIPTION_FALLBACK_CONTEXT_WINDOW`,不让自动压缩关闭。
13
+ *
14
+ * ChatGPT 的缓存带登录 flavor;组装时与当前登录的 flavor 不符视为过期、不并入(选择器提示重新发现)。
8
15
  */
9
- import type { Model, ProviderData } from "../types.js";
16
+ import type { Model, ModelThinkingLevel, ProviderData } from "../types.js";
10
17
  import type { ModelsDevIndex } from "./models-dev.js";
11
18
  export declare const DISCOVERED_CACHE_VERSION = 1;
12
19
  export interface DiscoveredCacheFile {
@@ -16,10 +23,15 @@ export interface DiscoveredCacheFile {
16
23
  fetchedAt: string;
17
24
  /** ChatGPT 登录的 flavor(siwc / codex);其它供应商不写。 */
18
25
  flavor?: string;
19
- models: {
20
- id: string;
21
- name?: string;
22
- }[];
26
+ models: DiscoveredCacheModel[];
27
+ }
28
+ /** 缓存里的一个模型:slug、显示名与后端给的元数据(都可缺)。 */
29
+ export interface DiscoveredCacheModel {
30
+ id: string;
31
+ name?: string;
32
+ contextWindow?: number;
33
+ input?: ("text" | "image")[];
34
+ reasoningLevels?: string[];
23
35
  }
24
36
  /** 缓存文件位置。 */
25
37
  export declare function discoveredCachePath(dataDir: string, provider: string): string;
@@ -27,15 +39,18 @@ export declare function discoveredCachePath(dataDir: string, provider: string):
27
39
  export declare function readDiscoveredCache(dataDir: string, provider: string): DiscoveredCacheFile | undefined;
28
40
  /** 写缓存(先写临时文件再改名);返回路径。 */
29
41
  export declare function writeDiscoveredCache(dataDir: string, provider: string, input: {
30
- models: readonly {
31
- id: string;
42
+ models: readonly (Omit<DiscoveredCacheModel, "name"> & {
32
43
  name?: string | undefined;
33
- }[];
44
+ })[];
34
45
  flavor?: string | undefined;
35
46
  }, now?: number): string;
36
47
  /** 删缓存;删了返回 true。 */
37
48
  export declare function clearDiscoveredCache(dataDir: string, provider: string): boolean;
38
- /** 缓存里的模型 → 注册表模型(models.dev 补元数据,补不到用自定义缺省)。 */
49
+ /** 后端给的推理强度 → 思考级别映射(`none` 对应 off;没列出的级别不支持)。 */
50
+ export declare function thinkingMapOf(levels: readonly string[]): Partial<Record<ModelThinkingLevel, string | null>>;
51
+ /** 订阅后端:models.dev 与后端都没给上下文窗口时的保守缺省。 */
52
+ export declare const SUBSCRIPTION_FALLBACK_CONTEXT_WINDOW = 128000;
53
+ /** 缓存里的模型 → 注册表模型(后端给的元数据优先,其次 models.dev,再次自定义缺省)。 */
39
54
  export declare function discoveredModels(file: DiscoveredCacheFile, provider: ProviderData, modelsDev: () => ModelsDevIndex | undefined): Model[];
40
55
  /** 注册表里能追加模型的最小接口(`ProviderRegistry.addModels`)。 */
41
56
  export interface DiscoveredTarget {
@@ -45,4 +60,6 @@ export interface DiscoveredTarget {
45
60
  /**
46
61
  * 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
47
62
  */
48
- export declare function mergeDiscoveredModels(registry: DiscoveredTarget, dataDir: string, modelsDev: () => ModelsDevIndex | undefined): string[];
63
+ export declare function mergeDiscoveredModels(registry: DiscoveredTarget, dataDir: string, modelsDev: () => ModelsDevIndex | undefined,
64
+ /** 当前登录的 flavor(只有 OAuth 供应商有):缓存的 flavor 与之不符视为过期,不并入。 */
65
+ loginFlavor?: (providerId: string) => string | undefined): string[];