@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
@@ -21,6 +21,10 @@
21
21
  * - [W5-H2] plan 模式产出计划、`plan.unattended: stop`(缺省)没人审批:stderr 一行(计划文件与审批办法)、
22
22
  * json 带 `planPending{planId, version, filePath}`、退出码 9。
23
23
  * - [W5-H2] `--image` / `@图片` 超限时按 config `images.resize` 缩放(缺省 auto)。
24
+ * - [W7-B2] 后台子 Agent(显式 `background:true` 或 `subagents.background: always`):主回合结束后若还有任务
25
+ * 在跑或通知待投递,stderr 一行提示并等它们结束、跑完通知回合再输出(最终文本 = 最后一条助手回复);
26
+ * 受 `--max-turns` / `--max-cost` / `limits.*` 约束(到限即停止等待,退出码 8),SIGINT / SIGTERM 照常中止
27
+ * (未结束的任务随会话关闭被停止);json 结果带 `tasks`(同 `getStats().tasks`)。
24
28
  * - 退出码:最终助手消息 `error / aborted` 或提示被拒 → 1;有工具调用被拒 → 7;到达预算 → 8;
25
29
  * 计划待审批 → 9;SIGINT 130、SIGTERM 143(先 abort)。
26
30
  */
@@ -29,6 +33,7 @@ import { ExitCode } from "../../cli/exit-codes.js";
29
33
  import { errorText, lastAssistant, onStdoutClosed, onTerminationSignals } from "../shared.js";
30
34
  import { toJsonLine, toWireEvent } from "./json-event.js";
31
35
  import { formatUsd } from "../../agent/limits.js";
36
+ import { registryOf } from "../../agent/subagent-registry.js";
32
37
  import { promptImages, sessionModel } from "../image-input.js";
33
38
  import { msg } from "../../i18n/index.js";
34
39
  export function joinPrompt(argument, piped) {
@@ -118,9 +123,15 @@ export async function runPrintMode(runtime, context) {
118
123
  const denied = [];
119
124
  let limit;
120
125
  let plan;
126
+ let stopWaiting = () => undefined;
127
+ const stopped = new Promise((resolve) => {
128
+ stopWaiting = resolve;
129
+ });
121
130
  const unsubscribe = session.subscribe((event) => {
122
- if (event.type === "limit_reached")
131
+ if (event.type === "limit_reached") {
123
132
  limit ??= event;
133
+ stopWaiting();
134
+ }
124
135
  else if (event.type === "plan_proposed")
125
136
  plan = event;
126
137
  else if (event.type === "plan_resolved" && event.planId === plan?.planId)
@@ -139,16 +150,25 @@ export async function runPrintMode(runtime, context) {
139
150
  let signalled;
140
151
  const offSignals = onTerminationSignals((code) => {
141
152
  signalled ??= code;
153
+ stopWaiting();
142
154
  void session.abort();
143
155
  });
144
156
  let stdoutClosed = false;
145
157
  const offEpipe = onStdoutClosed(() => {
146
158
  stdoutClosed = true;
159
+ stopWaiting();
147
160
  void session.abort();
148
161
  });
149
162
  let failure;
150
163
  try {
151
164
  await session.prompt(prompt, images.length > 0 ? { images } : {});
165
+ const ended = lastAssistant(session)?.stopReason;
166
+ if (signalled === undefined &&
167
+ !stdoutClosed &&
168
+ limit === undefined &&
169
+ ended !== "error" &&
170
+ ended !== "aborted")
171
+ await waitBackgroundTasks(session, stopped, io);
152
172
  }
153
173
  catch (error) {
154
174
  failure = errorText(error);
@@ -183,6 +203,7 @@ export async function runPrintMode(runtime, context) {
183
203
  cost: stats.cost,
184
204
  cacheHitRate: stats.cacheHitRate,
185
205
  ...(stats.cache !== undefined ? { cache: stats.cache } : {}),
206
+ ...(stats.tasks !== undefined ? { tasks: stats.tasks } : {}),
186
207
  ...(denied.length > 0 ? { deniedTools: denied } : {}),
187
208
  ...(plan !== undefined
188
209
  ? {
@@ -230,6 +251,20 @@ export async function runPrintMode(runtime, context) {
230
251
  return ExitCode.ToolDenied;
231
252
  return ExitCode.Ok;
232
253
  }
254
+ /**
255
+ * [W7-B2] 主回合结束后等后台任务与它们的通知回合(docs/agents-concurrency-plan.md §2.6、§6 Q5)。
256
+ * `settled()`(subagent-background.ts)等到没有运行中任务且通知投递链(含通知回合)结束;`stopped`:SIGINT /
257
+ * SIGTERM、stdout 关闭或预算到限时 resolve,立即停止等待。没有子 Agent 注册表时直接返回。
258
+ */
259
+ export async function waitBackgroundTasks(session, stopped, io) {
260
+ const registry = registryOf(session.state.sessionId);
261
+ if (registry === undefined)
262
+ return;
263
+ const running = registry.list().filter((task) => task.status === "running").length;
264
+ if (running > 0)
265
+ io.stderr(msg().print.print.waitingTasks(running));
266
+ await Promise.race([registry.settled(), stopped]);
267
+ }
233
268
  /** stderr 一行:计划已落盘待审批(-p 不替人批准)。 */
234
269
  export function describePlanPending(plan) {
235
270
  const where = plan.filePath ?? plan.planId;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RPC 命令分派表(设计 §13.2、src/rpc.ts 的 33 条命令)。[B6]
2
+ * RPC 命令分派表(设计 §13.2、src/rpc.ts 的命令表,现 44 条)。[B6]
3
3
  *
4
4
  * - 提示类(prompt / steer / follow_up)不等运行结束:会话开始运行(`before_agent_start` /
5
5
  * `agent_start`)、入队或被处理后立刻应答 `{ disposition }`;运行中的进展走事件。
@@ -11,6 +11,9 @@
11
11
  * - [W5-F] 计划:声明 `plans` 能力后计划审批交客户端(`plan_proposed` → `plan_response`),未声明按
12
12
  * `plan.unattended`;`approve_fresh` 在这里新建会话并以计划全文开新回合。`get_tasks / get_agents`
13
13
  * 读 `RpcContext.tasks / agents`(W5-G 的注册表与发现结果;未装配时回空表)。
14
+ * - [W7-B2] `background_task { taskId? }` → `{ backgrounded }`(`session.backgroundTask`,B1)。
15
+ * - 打断并发送:`prompt / steer` 的 `interrupt: true` → `session.prompt(message, { interrupt })`(中止当前回合、
16
+ * 立刻以「排队的 steer + 本条」开新回合),应答 `started`。
14
17
  */
15
18
  import type { AgentInfo } from "../../agents/types.js";
16
19
  import type { TaskRegistryView } from "../../tools/types.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RPC 命令分派表(设计 §13.2、src/rpc.ts 的 33 条命令)。[B6]
2
+ * RPC 命令分派表(设计 §13.2、src/rpc.ts 的命令表,现 44 条)。[B6]
3
3
  *
4
4
  * - 提示类(prompt / steer / follow_up)不等运行结束:会话开始运行(`before_agent_start` /
5
5
  * `agent_start`)、入队或被处理后立刻应答 `{ disposition }`;运行中的进展走事件。
@@ -11,6 +11,9 @@
11
11
  * - [W5-F] 计划:声明 `plans` 能力后计划审批交客户端(`plan_proposed` → `plan_response`),未声明按
12
12
  * `plan.unattended`;`approve_fresh` 在这里新建会话并以计划全文开新回合。`get_tasks / get_agents`
13
13
  * 读 `RpcContext.tasks / agents`(W5-G 的注册表与发现结果;未装配时回空表)。
14
+ * - [W7-B2] `background_task { taskId? }` → `{ backgrounded }`(`session.backgroundTask`,B1)。
15
+ * - 打断并发送:`prompt / steer` 的 `interrupt: true` → `session.prompt(message, { interrupt })`(中止当前回合、
16
+ * 立刻以「排队的 steer + 本条」开新回合),应答 `started`。
14
17
  */
15
18
  import { formatModelRef } from "../../ai/providers/channels.js";
16
19
  import { AgentSessionImpl } from "../../agent/session.js";
@@ -81,6 +84,12 @@ function applyPlanAttendance(ctx) {
81
84
  planController(ctx.session())?.setAttendance(ctx.capabilities.has("plans") ? "client" : "unattended");
82
85
  }
83
86
  /** 开始一次运行:开始 / 入队 / 被处理后即返回,不等运行结束。 */
87
+ /** `prompt / steer` 的 `interrupt`:可选布尔。 */
88
+ function interruptFlag(value) {
89
+ if (value !== undefined && typeof value !== "boolean")
90
+ throw new AmaError("invalid_arguments", "interrupt must be a boolean");
91
+ return value === true;
92
+ }
84
93
  async function startRun(ctx, run) {
85
94
  const session = ctx.session();
86
95
  let settled = false;
@@ -154,8 +163,14 @@ export const handlers = {
154
163
  prompt: (p, ctx) => startRun(ctx, () => ctx.session().prompt(p.message, {
155
164
  ...(p.images !== undefined ? { images: p.images } : {}),
156
165
  ...(p.streamingBehavior !== undefined ? { streamingBehavior: p.streamingBehavior } : {}),
166
+ ...(interruptFlag(p.interrupt) ? { interrupt: true } : {}),
157
167
  })),
158
- steer: (p, ctx) => startRun(ctx, () => ctx.session().steer(p.message)),
168
+ steer: (p, ctx) => startRun(ctx, () => interruptFlag(p.interrupt)
169
+ ? ctx.session().prompt(p.message, {
170
+ interrupt: true,
171
+ ...(p.images !== undefined ? { images: p.images } : {}),
172
+ })
173
+ : ctx.session().steer(p.message)),
159
174
  follow_up: (p, ctx) => startRun(ctx, () => ctx.session().followUp(p.message)),
160
175
  abort: async (_p, ctx) => {
161
176
  await ctx.session().abort();
@@ -312,5 +327,12 @@ export const handlers = {
312
327
  get_agents: async (_p, ctx) => ({ agents: [...(ctx.agents?.() ?? [])] }),
313
328
  // [W6-T2] 轨迹(docs/wave6-plan.md §2.6;分页 / 增量 / 脱敏见 trace/query.ts)
314
329
  get_trace: async (p, ctx) => sessionTrace(ctx.session(), p ?? {}),
330
+ // [W7-B2] 前台任务转后台:工具调用立即返回、任务继续,完成后照常通知
331
+ background_task: async (p, ctx) => {
332
+ const taskId = p?.taskId;
333
+ if (taskId !== undefined && typeof taskId !== "string")
334
+ throw new AmaError("invalid_arguments", msg().print.rpc.badTaskId);
335
+ return { backgrounded: ctx.session().backgroundTask(taskId) };
336
+ },
315
337
  };
316
338
  export const RPC_COMMAND_TYPES = Object.keys(handlers);
package/dist/rpc.d.ts CHANGED
@@ -19,6 +19,9 @@
19
19
  * - [W6-C0] 第六波(docs/wave6-plan.md §2.6、§4.4):命令 `get_trace`(参数 `RpcGetTraceParams`、返回
20
20
  * `RpcW6Results`,W6-T2 实现,之前回 `not_implemented`);事件 `quota_update`;`permission_request.context`
21
21
  * 可带 `toolCallId`;`keySource` 可为 `oauth`。`RPC_PROTOCOL_VERSION` 不变。
22
+ * - [W7-B2] 后台子 Agent(docs/agents-concurrency-plan.md §2.5):命令 `background_task { taskId? }` →
23
+ * `RpcW7Results`(无 taskId = 全部前台运行中任务;对已结束 / 已在后台的任务回空表);事件
24
+ * `subagent_background` 由 `SessionEvent` 派生。`RPC_PROTOCOL_VERSION` 不变。
22
25
  */
23
26
  import type { AssistantEvent, ImageBlock, ModelThinkingLevel, Usage } from "./ai/types.js";
24
27
  import type { PlanData, PlanDecisionKind, QueueMode, SessionEvent, TodoItemView } from "./agent/types.js";
@@ -42,14 +45,21 @@ export interface RpcHello {
42
45
  /** 无参数命令。 */
43
46
  export type NoParams = Record<never, never>;
44
47
  export interface RpcCommandMap {
48
+ /**
49
+ * `interrupt: true`:运行中中止当前回合(工具按 abort 收尾),立刻以「排队的 steer + 本条」开新回合
50
+ * (user 消息 origin `interrupt`),应答 `started`;空闲时等同不填。优先于 `streamingBehavior`。
51
+ */
45
52
  prompt: {
46
53
  message: string;
47
54
  images?: ImageBlock[];
48
55
  streamingBehavior?: "steer" | "followUp";
56
+ interrupt?: boolean;
49
57
  };
58
+ /** `interrupt: true` 同 `prompt`。 */
50
59
  steer: {
51
60
  message: string;
52
61
  images?: ImageBlock[];
62
+ interrupt?: boolean;
53
63
  };
54
64
  follow_up: {
55
65
  message: string;
@@ -139,6 +149,16 @@ export interface RpcCommandMap {
139
149
  get_tasks: NoParams;
140
150
  get_agents: NoParams;
141
151
  get_trace: RpcGetTraceParams;
152
+ background_task: {
153
+ taskId?: string;
154
+ };
155
+ }
156
+ /** [W7-B2] 第七波命令成功时的 `data` 形状。 */
157
+ export interface RpcW7Results {
158
+ /** 实际转了后台的 taskId(含被打断的 `task_ctl wait`);没有可转的为空表。 */
159
+ background_task: {
160
+ backgrounded: string[];
161
+ };
142
162
  }
143
163
  /** [W6-C0] `get_trace` 的参数。 */
144
164
  export interface RpcGetTraceParams {
package/dist/rpc.js CHANGED
@@ -19,5 +19,8 @@
19
19
  * - [W6-C0] 第六波(docs/wave6-plan.md §2.6、§4.4):命令 `get_trace`(参数 `RpcGetTraceParams`、返回
20
20
  * `RpcW6Results`,W6-T2 实现,之前回 `not_implemented`);事件 `quota_update`;`permission_request.context`
21
21
  * 可带 `toolCallId`;`keySource` 可为 `oauth`。`RPC_PROTOCOL_VERSION` 不变。
22
+ * - [W7-B2] 后台子 Agent(docs/agents-concurrency-plan.md §2.5):命令 `background_task { taskId? }` →
23
+ * `RpcW7Results`(无 taskId = 全部前台运行中任务;对已结束 / 已在后台的任务回空表);事件
24
+ * `subagent_background` 由 `SessionEvent` 派生。`RPC_PROTOCOL_VERSION` 不变。
22
25
  */
23
26
  export const RPC_PROTOCOL_VERSION = 1;
@@ -7,6 +7,8 @@
7
7
  * 与父一致,运行时拒绝;
8
8
  * - `wait` / `output` 是轮询类调用(`annotations.pollable`,重复调用检测豁免);
9
9
  * - `send` = 续聊放后台:等价于 `task{taskId, prompt, background:true}`,完成后同样收到通知。
10
+ * - [W7-B1] `wait` 被转后台(`background()`,TUI `Ctrl+B` / RPC `background_task`)打断时立即返回固定
11
+ * 文案,要求模型不要再等。
10
12
  */
11
13
  import type { ToolDefinition } from "./types.js";
12
14
  export declare const TASK_CTL_TOOL = "task_ctl";
@@ -7,8 +7,10 @@
7
7
  * 与父一致,运行时拒绝;
8
8
  * - `wait` / `output` 是轮询类调用(`annotations.pollable`,重复调用检测豁免);
9
9
  * - `send` = 续聊放后台:等价于 `task{taskId, prompt, background:true}`,完成后同样收到通知。
10
+ * - [W7-B1] `wait` 被转后台(`background()`,TUI `Ctrl+B` / RPC `background_task`)打断时立即返回固定
11
+ * 文案,要求模型不要再等。
10
12
  */
11
- import { capTaskText, formatTokens } from "../agents/result.js";
13
+ import { capTaskText, formatTokens, waitDetachedText } from "../agents/result.js";
12
14
  import { taskControl } from "../agents/task-control.js";
13
15
  export const TASK_CTL_TOOL = "task_ctl";
14
16
  export const DEFAULT_WAIT_MS = 30_000;
@@ -96,7 +98,10 @@ export function createTaskCtlTool() {
96
98
  }
97
99
  case "wait": {
98
100
  const timeout = Math.min(Math.max(input.timeoutMs ?? DEFAULT_WAIT_MS, 0), MAX_WAIT_MS);
99
- const result = await control.wait(taskId, timeout);
101
+ const signal = control.detachSignal(taskId);
102
+ const result = await control.wait(taskId, timeout, { signal });
103
+ if (result === undefined && signal.aborted)
104
+ return { content: waitDetachedText(taskId) };
100
105
  if (result === undefined) {
101
106
  const turns = control.get(taskId)?.turns ?? 0;
102
107
  return {
@@ -5,6 +5,8 @@
5
5
  * - `agent`:子 Agent 类型(内置 general / explore / plan、`.ama/agents/*.md`)或外部 Agent;缺省
6
6
  * general。描述里列出可用类型(catalog 由 compose-agents.ts 经 `bindTaskAgents` 绑定;没绑定时
7
7
  * 只列内置类型)。
8
+ * - [W7-B1] 缺省前台 / 后台由 `subagents.background` 决定(交互 / RPC / ACP 后台,`-p` 前台),描述随之
9
+ * 二选一(`bindTaskBackground`);前台任务可被 `background()` 转后台,工具立即以固定文案返回。
8
10
  * - `background`:立即返回 taskId,完成后父会话收到 `<task-notification>`;`taskId`:向已有子会话
9
11
  * 追加消息(续聊,忽略 agent / tools / model);`isolation: "worktree"`:在独立 git worktree 里跑。
10
12
  * - **parallel**:同一回复里的多个 task 真并行(会话注册表的池限流,缺省 4);与 sequential 工具
@@ -33,9 +35,18 @@ export interface TaskInput {
33
35
  /** 预留(并发上限在会话的任务注册表,见 agent/subagent-registry.ts)。 */
34
36
  export interface TaskToolOptions {
35
37
  }
38
+ /** 缺省前台(`-p`、`subagents.background: never`)时的描述。 */
39
+ export declare const FOREGROUND_DESCRIPTION: string;
40
+ /** [W7-B1] 缺省后台(交互 / RPC / ACP,docs/agents-concurrency-plan.md §2.6)时的描述。 */
41
+ export declare const BACKGROUND_DESCRIPTION: string;
36
42
  /** 描述里类型清单的标题(清单本身另有 400 token 预算,见 agents/catalog.ts)。 */
37
43
  export declare const TASK_AGENTS_HEADING = "\nAgents:\n";
38
44
  /** 组装根把会话的类型目录绑到 task 工具上(描述里的类型清单)。 */
39
45
  export declare function bindTaskAgents(tool: ToolDefinition | undefined, catalog: AgentCatalog): void;
46
+ /**
47
+ * [W7-B1] 组装根把本进程的缺省前台 / 后台(`subagents.background` 按运行模式解析后)绑到 task 工具:
48
+ * 决定描述用哪一版。会话内不变(缓存前缀稳定);没绑定按缺省前台。
49
+ */
50
+ export declare function bindTaskBackground(tool: ToolDefinition | undefined, background: boolean): void;
40
51
  export declare function buildSubagentRequest(input: TaskInput, ctx: ToolContext): SubagentRequest | string;
41
52
  export declare function createTaskTool(_options?: TaskToolOptions): ToolDefinition<TaskInput>;
@@ -5,6 +5,8 @@
5
5
  * - `agent`:子 Agent 类型(内置 general / explore / plan、`.ama/agents/*.md`)或外部 Agent;缺省
6
6
  * general。描述里列出可用类型(catalog 由 compose-agents.ts 经 `bindTaskAgents` 绑定;没绑定时
7
7
  * 只列内置类型)。
8
+ * - [W7-B1] 缺省前台 / 后台由 `subagents.background` 决定(交互 / RPC / ACP 后台,`-p` 前台),描述随之
9
+ * 二选一(`bindTaskBackground`);前台任务可被 `background()` 转后台,工具立即以固定文案返回。
8
10
  * - `background`:立即返回 taskId,完成后父会话收到 `<task-notification>`;`taskId`:向已有子会话
9
11
  * 追加消息(续聊,忽略 agent / tools / model);`isolation: "worktree"`:在独立 git worktree 里跑。
10
12
  * - **parallel**:同一回复里的多个 task 真并行(会话注册表的池限流,缺省 4);与 sequential 工具
@@ -24,17 +26,32 @@ const THINKING = [
24
26
  "high",
25
27
  "xhigh",
26
28
  ];
27
- const BASE_DESCRIPTION = "Delegate to a sub-agent (fresh context; give full instructions). Returns its final report. " +
29
+ /** 缺省前台(`-p`、`subagents.background: never`)时的描述。 */
30
+ export const FOREGROUND_DESCRIPTION = "Delegate to a sub-agent (fresh context; give full instructions). Returns its final report. " +
28
31
  "Tasks in one reply run in parallel; writers should use isolation worktree. background: " +
29
32
  "returns a taskId now (see task_ctl); taskId: continue that task.";
33
+ /** [W7-B1] 缺省后台(交互 / RPC / ACP,docs/agents-concurrency-plan.md §2.6)时的描述。 */
34
+ export const BACKGROUND_DESCRIPTION = "Delegate to a sub-agent (fresh context; give full instructions). Runs in the background by " +
35
+ "default: returns a taskId; a <task-notification> follows when done, so keep working. " +
36
+ "background:false if your next step needs the result. taskId: continue a task. Tasks in one " +
37
+ "reply run in parallel; writers use isolation worktree.";
30
38
  /** 描述里类型清单的标题(清单本身另有 400 token 预算,见 agents/catalog.ts)。 */
31
39
  export const TASK_AGENTS_HEADING = "\nAgents:\n";
32
40
  const catalogs = new WeakMap();
41
+ const backgrounds = new WeakMap();
33
42
  /** 组装根把会话的类型目录绑到 task 工具上(描述里的类型清单)。 */
34
43
  export function bindTaskAgents(tool, catalog) {
35
44
  if (tool !== undefined)
36
45
  catalogs.set(tool, catalog);
37
46
  }
47
+ /**
48
+ * [W7-B1] 组装根把本进程的缺省前台 / 后台(`subagents.background` 按运行模式解析后)绑到 task 工具:
49
+ * 决定描述用哪一版。会话内不变(缓存前缀稳定);没绑定按缺省前台。
50
+ */
51
+ export function bindTaskBackground(tool, background) {
52
+ if (tool !== undefined)
53
+ backgrounds.set(tool, background);
54
+ }
38
55
  function fail(message) {
39
56
  return { content: message, isError: true };
40
57
  }
@@ -87,7 +104,8 @@ export function createTaskTool(_options = {}) {
87
104
  label: "Task",
88
105
  get description() {
89
106
  const catalog = catalogs.get(tool) ?? new AgentCatalog();
90
- return `${BASE_DESCRIPTION}${TASK_AGENTS_HEADING}${catalog.describe()}`;
107
+ const base = backgrounds.get(tool) === true ? BACKGROUND_DESCRIPTION : FOREGROUND_DESCRIPTION;
108
+ return `${base}${TASK_AGENTS_HEADING}${catalog.describe()}`;
91
109
  },
92
110
  parameters: {
93
111
  type: "object",
@@ -181,6 +181,12 @@ export interface RunnerHandle {
181
181
  send(text: string): Promise<void>;
182
182
  wait(): Promise<SubagentResult>;
183
183
  stop(): Promise<void>;
184
+ /**
185
+ * 打断当前回合并立即以 `text` 开下一回合(子 Agent 视图的「打断并发送」);`wait()` 跟到这一回合结束。
186
+ * 驱动能中断回合(ACP `session/cancel`、Claude stream-json interrupt、Codex `turn/interrupt`)且正在
187
+ * 运行时返回 true;否则 false(调用方退回排队)。
188
+ */
189
+ interrupt?(text: string): Promise<boolean>;
184
190
  }
185
191
  /** `ama`(AmaRunner)| `claude` | `codex` | `acp:<program>`(ProcessRunner,W5-E)| 宿主 id。 */
186
192
  export interface SubagentRunner {
@@ -88,6 +88,8 @@ export declare class Editor implements Component, Focusable {
88
88
  col: number;
89
89
  };
90
90
  get isCompletionOpen(): boolean;
91
+ /** 正在用 ↑↓ 浏览输入历史(编辑后即退出)。 */
92
+ get isBrowsingHistory(): boolean;
91
93
  getHistory(): readonly string[];
92
94
  setHistory(entries: readonly string[]): void;
93
95
  addToHistory(text: string): void;
@@ -88,6 +88,10 @@ export class Editor {
88
88
  get isCompletionOpen() {
89
89
  return this.completion !== null;
90
90
  }
91
+ /** 正在用 ↑↓ 浏览输入历史(编辑后即退出)。 */
92
+ get isBrowsingHistory() {
93
+ return this.historyIndex !== -1;
94
+ }
91
95
  getHistory() {
92
96
  return this.history;
93
97
  }
@@ -2,7 +2,8 @@
2
2
  * 运行指示(设计 §12.1、§12.6;终端界面视觉设计 v1 §3.7)。[B4]
3
3
  *
4
4
  * 一行 `⠋ 动词 · 已用时 · 附加项…`:spinner `accent`、动词正文色、其余 `dim`,以 ` · ` 连接。
5
- * - `setVerb(verb, extras, { elapsed })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
5
+ * - `setVerb(verb, extras, { elapsed, optional })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
6
+ * `optional` 是排在附加项之后的可丢弃项(按键提示),整行放不下时从后往前丢,而不是被截断;
6
7
  * `setMessage(text)` 是只换动词的旧接口。
7
8
  * - 帧取 `theme.glyphs.spinner`(Unicode 10 帧 80 ms、ASCII 4 帧 250 ms);`frame` 暴露当前帧字形,
8
9
  * `onFrame` 让别的组件(运行中的工具摘要行)与 Loader 同帧换字,保证一帧只多改一行。
@@ -27,6 +28,8 @@ export interface LoaderOptions {
27
28
  export interface LoaderVerbOptions {
28
29
  /** 本动词是否显示已用时(缺省跟随 `showElapsed`)。 */
29
30
  elapsed?: boolean;
31
+ /** 可丢弃的附加项:宽度不够时从后往前整项丢掉(缺省没有)。 */
32
+ optional?: readonly string[];
30
33
  }
31
34
  export declare function formatElapsed(ms: number): string;
32
35
  export declare class Loader implements Component {
@@ -37,6 +40,7 @@ export declare class Loader implements Component {
37
40
  private startedAt;
38
41
  private verb;
39
42
  private extras;
43
+ private optional;
40
44
  private verbElapsed;
41
45
  private lastElapsed;
42
46
  private readonly listeners;
@@ -2,17 +2,21 @@
2
2
  * 运行指示(设计 §12.1、§12.6;终端界面视觉设计 v1 §3.7)。[B4]
3
3
  *
4
4
  * 一行 `⠋ 动词 · 已用时 · 附加项…`:spinner `accent`、动词正文色、其余 `dim`,以 ` · ` 连接。
5
- * - `setVerb(verb, extras, { elapsed })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
5
+ * - `setVerb(verb, extras, { elapsed, optional })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
6
+ * `optional` 是排在附加项之后的可丢弃项(按键提示),整行放不下时从后往前丢,而不是被截断;
6
7
  * `setMessage(text)` 是只换动词的旧接口。
7
8
  * - 帧取 `theme.glyphs.spinner`(Unicode 10 帧 80 ms、ASCII 4 帧 250 ms);`frame` 暴露当前帧字形,
8
9
  * `onFrame` 让别的组件(运行中的工具摘要行)与 Loader 同帧换字,保证一帧只多改一行。
9
10
  * - `animation: false`:spinner 固定为 `glyphs.spinnerStatic`,定时器每秒一次,已用时变了才请求重绘。
10
11
  * - `stop()` 后不再触发渲染。时钟可注入便于测试。
11
12
  */
12
- import { truncateToWidth } from "../ansi.js";
13
+ import { truncateToWidth, visibleWidth } from "../ansi.js";
13
14
  import { UNICODE_GLYPHS } from "../glyphs.js";
14
15
  /** Unicode 帧表(兼容旧导出;实际帧取 `theme.glyphs.spinner`)。 */
15
16
  export const LOADER_FRAMES = UNICODE_GLYPHS.spinner;
17
+ function sameList(a, b) {
18
+ return a.length === b.length && a.every((e, i) => e === b[i]);
19
+ }
16
20
  export function formatElapsed(ms) {
17
21
  const total = Math.max(0, Math.floor(ms / 1000));
18
22
  if (total < 60)
@@ -29,6 +33,7 @@ export class Loader {
29
33
  startedAt;
30
34
  verb;
31
35
  extras = [];
36
+ optional = [];
32
37
  verbElapsed;
33
38
  lastElapsed = "";
34
39
  listeners = new Set();
@@ -65,12 +70,13 @@ export class Loader {
65
70
  setVerb(verb, extras = [], options = {}) {
66
71
  const same = verb === this.verb &&
67
72
  options.elapsed === this.verbElapsed &&
68
- extras.length === this.extras.length &&
69
- extras.every((e, i) => e === this.extras[i]);
73
+ sameList(extras, this.extras) &&
74
+ sameList(options.optional ?? [], this.optional);
70
75
  if (same)
71
76
  return;
72
77
  this.verb = verb;
73
78
  this.extras = [...extras];
79
+ this.optional = [...(options.optional ?? [])];
74
80
  this.verbElapsed = options.elapsed;
75
81
  this.requestRender();
76
82
  }
@@ -142,7 +148,14 @@ export class Loader {
142
148
  for (const extra of this.extras)
143
149
  parts.push(dim(extra));
144
150
  const spinner = theme ? theme.fg("accent", this.frame) : this.frame;
145
- return [truncateToWidth(`${spinner} ${parts.join(dim(" · "))}`, width)];
151
+ const line = (items) => `${spinner} ${items.join(dim(" · "))}`;
152
+ for (const extra of this.optional) {
153
+ const next = [...parts, dim(extra)];
154
+ if (visibleWidth(line(next)) > width)
155
+ break;
156
+ parts.push(dim(extra));
157
+ }
158
+ return [truncateToWidth(line(parts), width)];
146
159
  }
147
160
  invalidate() { }
148
161
  }
@@ -36,6 +36,13 @@ export declare const DEFAULT_KEYBINDINGS: {
36
36
  readonly "app.exit": readonly ["ctrl+d"];
37
37
  readonly "app.message.followUp": readonly ["alt+enter"];
38
38
  readonly "app.message.dequeue": readonly ["alt+up"];
39
+ /**
40
+ * 运行中打断当前回合并立即发送输入框的文字(连同排队的插话);`ui.enterWhileRunning: "interrupt"` 时与
41
+ * Enter 互换(它变成排队)。缺省 `Ctrl+X`:各终端(macOS Terminal、iTerm2、tmux、Windows Terminal)都原样
42
+ * 送达 0x18,且没有别的绑定;`Ctrl+Enter` 不开 kitty 键盘协议时与 Enter 无法区分,`Ctrl+S` 可能被 XOFF
43
+ * 流控吃掉,`Alt+Enter` 已是 followUp,`Alt+<字母>` 在 macOS 缺省不送 Meta,`Ctrl+]` 在非美式键盘难按。
44
+ */
45
+ readonly "app.message.interrupt": readonly ["ctrl+x"];
39
46
  readonly "app.permission.cycle": readonly ["shift+tab", "tab"];
40
47
  readonly "app.tools.expand": readonly ["ctrl+o"];
41
48
  readonly "app.model.select": readonly ["ctrl+l"];
@@ -45,10 +52,15 @@ export declare const DEFAULT_KEYBINDINGS: {
45
52
  /** [W5-U] 粘贴剪贴板图片(写进数据目录,输入框插入 `@路径`)。 */
46
53
  readonly "app.paste.image": readonly ["ctrl+v"];
47
54
  /**
48
- * [W6-A] 进入 Agent 栏:只在输入为空时生效(有字时 `Ctrl+B` 仍是光标左移、`↓` 仍是下移 / 历史);
49
- * `↓` 另要求栏可见。tmux 缺省前缀吃掉 `Ctrl+B` 时用 `↓`。
55
+ * [W6-A / W7-A] 进入 Agent 栏:输入为空、补全未开、有子 Agent 任务即可(不要求栏可见);有字时 `↓` 仍是
56
+ * 下移 / 历史,并给一次提示。`Ctrl+B` 不再进栏(tmux 缺省前缀会吃掉它),留给 `app.tasks.background`。
57
+ */
58
+ readonly "app.agents.focus": readonly ["down"];
59
+ /**
60
+ * [W7-C] 有阻塞中的前台子 Agent 任务(或 `task_ctl wait`)时全部转后台,不看输入框;没有时落回编辑器
61
+ * (`tui.editor.cursorLeft` 的 `ctrl+b`)。tmux 里按 `C-b C-b` 透传。
50
62
  */
51
- readonly "app.agents.focus": readonly ["ctrl+b", "down"];
63
+ readonly "app.tasks.background": readonly ["ctrl+b"];
52
64
  };
53
65
  export type ActionId = keyof typeof DEFAULT_KEYBINDINGS;
54
66
  export type KeybindingOverrides = Partial<Record<ActionId, readonly string[]>>;
@@ -39,6 +39,13 @@ export const DEFAULT_KEYBINDINGS = {
39
39
  "app.exit": ["ctrl+d"],
40
40
  "app.message.followUp": ["alt+enter"],
41
41
  "app.message.dequeue": ["alt+up"],
42
+ /**
43
+ * 运行中打断当前回合并立即发送输入框的文字(连同排队的插话);`ui.enterWhileRunning: "interrupt"` 时与
44
+ * Enter 互换(它变成排队)。缺省 `Ctrl+X`:各终端(macOS Terminal、iTerm2、tmux、Windows Terminal)都原样
45
+ * 送达 0x18,且没有别的绑定;`Ctrl+Enter` 不开 kitty 键盘协议时与 Enter 无法区分,`Ctrl+S` 可能被 XOFF
46
+ * 流控吃掉,`Alt+Enter` 已是 followUp,`Alt+<字母>` 在 macOS 缺省不送 Meta,`Ctrl+]` 在非美式键盘难按。
47
+ */
48
+ "app.message.interrupt": ["ctrl+x"],
42
49
  "app.permission.cycle": ["shift+tab", "tab"],
43
50
  "app.tools.expand": ["ctrl+o"],
44
51
  "app.model.select": ["ctrl+l"],
@@ -48,10 +55,15 @@ export const DEFAULT_KEYBINDINGS = {
48
55
  /** [W5-U] 粘贴剪贴板图片(写进数据目录,输入框插入 `@路径`)。 */
49
56
  "app.paste.image": ["ctrl+v"],
50
57
  /**
51
- * [W6-A] 进入 Agent 栏:只在输入为空时生效(有字时 `Ctrl+B` 仍是光标左移、`↓` 仍是下移 / 历史);
52
- * `↓` 另要求栏可见。tmux 缺省前缀吃掉 `Ctrl+B` 时用 `↓`。
58
+ * [W6-A / W7-A] 进入 Agent 栏:输入为空、补全未开、有子 Agent 任务即可(不要求栏可见);有字时 `↓` 仍是
59
+ * 下移 / 历史,并给一次提示。`Ctrl+B` 不再进栏(tmux 缺省前缀会吃掉它),留给 `app.tasks.background`。
60
+ */
61
+ "app.agents.focus": ["down"],
62
+ /**
63
+ * [W7-C] 有阻塞中的前台子 Agent 任务(或 `task_ctl wait`)时全部转后台,不看输入框;没有时落回编辑器
64
+ * (`tui.editor.cursorLeft` 的 `ctrl+b`)。tmux 里按 `C-b C-b` 透传。
53
65
  */
54
- "app.agents.focus": ["ctrl+b", "down"],
66
+ "app.tasks.background": ["ctrl+b"],
55
67
  };
56
68
  export function isActionId(id) {
57
69
  return Object.prototype.hasOwnProperty.call(DEFAULT_KEYBINDINGS, id);