@wolido/async-subagent-isolation 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADVANCED.en.md +12 -8
- package/ADVANCED.md +12 -8
- package/README.en.md +10 -5
- package/README.md +10 -5
- package/examples/pi/agent/master.md +4 -3
- package/package.json +1 -1
- package/src/index.ts +186 -22
package/ADVANCED.en.md
CHANGED
|
@@ -125,40 +125,44 @@ Once the subagent finishes, its result is pushed into the conversation:
|
|
|
125
125
|
```
|
|
126
126
|
## [subagent-result] coder 成功 (taskId: 01912345-6789-7abc-8def-0123456789ab)
|
|
127
127
|
|
|
128
|
+
> [subagent-result] 任务完成通知,非用户新指令。处理前先锚定你当前正在执行的主线任务与进度;对照派发记录消化本通知,勿让通知覆盖或改写你的主线计划。
|
|
129
|
+
|
|
128
130
|
- 状态: 成功
|
|
129
131
|
- 任务: 将认证中间件重构为使用 async/await。
|
|
130
132
|
- 耗时: 02:34 · 用量: 5 turns/↑12.5k/↓3.2k/$0.0042
|
|
131
133
|
- 会话: 01912345-6789-7abc-8def-0123456789ab
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
本任务结束时,其他在途任务: 1
|
|
134
136
|
- 01912345-aaaa-7bbb-8ccc-0123456789ab (writer): 更新 README。
|
|
135
137
|
|
|
136
138
|
---
|
|
137
139
|
<full subagent output>
|
|
138
140
|
```
|
|
139
141
|
|
|
142
|
+
**Trigger line**: between the title line and the metadata block sits a fixed blockquote line (`>` prefix), verbatim-identical in every envelope. It is a meta-instruction addressed to the main agent and does three jobs: identity correction (this is a completion notification, not a new user instruction), mainline retention (anchor the mainline task and progress currently in flight before processing), and a fixed processing order (anchor the mainline first, then digest the notification against dispatch records). The wording is deliberately unconditional, leaving no "the result is important, so interrupting the mainline is fine" loophole; since steer delivery inserts notifications mid-turn, the line restates mainline awareness verbatim at delivery. It enters only the LLM context and does not affect the summary card shown to the user in the TUI.
|
|
143
|
+
|
|
140
144
|
Status enumeration: **成功** (success, exit=0) / **失败** (failure, exit≠0 or stopReason=error) / **超时** (timeout, activity_timeout or hard_timeout) / **已取消** (cancelled, aborted or killed_on_shutdown).
|
|
141
145
|
|
|
142
146
|
**Duration**: the `- 耗时:` line shows the subagent's real run time. When a result exists, it is the actual process run time (`finishedAt - startedAt`); when the result is null (user/agent cancel, session shutdown, internal error), it is measured from dispatch time instead. The format is `MM:SS`, or `H:MM:SS` at one hour and beyond (hours not zero-padded). All four terminal states (success, failure, timeout, cancelled) carry the duration in both the envelope and the TUI notification card.
|
|
143
147
|
|
|
144
148
|
"Cancelled" has three sub-cases with different envelope bodies:
|
|
145
149
|
- User cancelled via `/subagent-cancel` (cancelledBy: user) → body states this is a deliberate user action; the main agent must NOT auto-retry and must ask the user before re-dispatching.
|
|
146
|
-
- Main agent cancelled via the `subagent` tool with `action="cancel"` (cancelledBy: agent) → body states the task was cancelled by the main agent via the subagent tool (action=cancel).
|
|
150
|
+
- Main agent cancelled via the `subagent` tool with `action="cancel"` (cancelledBy: agent) → body states the task was cancelled by the main agent via the subagent tool (action=cancel), followed by "取消理由: ..." (the reason given at the confirmation step).
|
|
147
151
|
- Session shutdown killed the task (cancelledBy: none) → body states the task was terminated by session_shutdown.
|
|
148
152
|
|
|
149
153
|
When the main agent receives a "已取消" notification, it should distinguish the origin: a user cancel must never be auto-retried (ask the user first); an agent cancel is its own decision — do not re-dispatch without new information; a session-shutdown cancel can be re-dispatched after the session resumes, at the agent's discretion.
|
|
150
154
|
|
|
151
|
-
**In-flight block**: the "在途任务" list in the envelope's metadata section lists the **other** background tasks still running (this task is removed from the registry before the envelope is built, so it never appears in its own list). Its format is
|
|
155
|
+
**In-flight block**: the "在途任务" list in the envelope's metadata section lists the **other** background tasks still running (this task is removed from the registry before the envelope is built, so it never appears in its own list). Its format is `本任务结束时,其他在途任务: N` followed by one `- taskId (agent): task description` line per task, or `本任务结束时无其他在途任务。` when none remain. It deliberately carries **no elapsed time and no clock time** (it answers "what else was running when this task ended", not "how long has it run" or "what time is it"). The block is a **build-time snapshot** whose wording is anchored to this task's end event rather than an absolute "now" — between envelope construction and delivery the main agent may have dispatched new tasks, making the snapshot stale; on conflict with dispatch records the main agent issued itself this turn, the dispatch records prevail. The main agent uses it to know how many tasks are still outstanding — while the count is non-zero, do not report "all done" to the user.
|
|
152
156
|
|
|
153
157
|
The full output enters the LLM context (not truncated). The `details` carries structured data (taskId, agent, status, exitCode, stopReason, durationMs (required, run time in milliseconds), usage, sessionId, full output) for programmatic consumption; it does not enter the LLM context.
|
|
154
158
|
|
|
155
159
|
### Notification delivery
|
|
156
160
|
|
|
157
|
-
Notifications are sent via `pi.sendMessage` with `deliverAs: "
|
|
161
|
+
Notifications are sent via `pi.sendMessage` with `deliverAs: "steer"` + `triggerTurn: true`:
|
|
158
162
|
- When the main agent is idle, it triggers a new conversation turn immediately.
|
|
159
|
-
- When the main agent is busy, the notification is queued and
|
|
163
|
+
- When the main agent is busy, the notification is queued and delivered after the current assistant turn's tool calls finish, before the next LLM call (steer semantics) — it is not held back until the whole turn ends, so it cannot lag behind tasks dispatched later in the same turn.
|
|
160
164
|
|
|
161
|
-
The main agent is trained (via `promptGuidelines`) to recognize the `[subagent-result]` prefix as a system notification, not a user request.
|
|
165
|
+
The main agent is trained (via `promptGuidelines`) to recognize the `[subagent-result]` prefix as a system notification, not a user request; a "notification digestion" entry in the tool description further fixes the digestion order: anchor the current mainline task and progress first, then digest the notification against dispatch records, decide the next step autonomously from the result, and defer when it conflicts with the mainline rather than letting the notification rewrite the mainline plan. The fixed trigger line under the envelope title (see the envelope format above) restates this order verbatim at delivery, mitigating steer delivery's interruption of turn-plan continuity.
|
|
162
166
|
|
|
163
167
|
### Progress widget
|
|
164
168
|
|
|
@@ -194,9 +198,9 @@ To cancel all running tasks at once:
|
|
|
194
198
|
|
|
195
199
|
Takes no arguments. Unlike `/subagent-cancel`, which cancels a single task by taskId, this cancels every running task. Each cancelled task still emits its own "已取消" `[subagent-result]` notification (the main agent receives N cancelled envelopes). On success it notifies "已取消全部 N 个运行中任务"; with no running tasks it notifies "无运行中任务可取消". The cancel source is likewise recorded as `cancelledBy: "user"`.
|
|
196
200
|
|
|
197
|
-
**Path 2: Main agent `subagent` tool with `action="cancel"
|
|
201
|
+
**Path 2: Main agent `subagent` tool with `action="cancel"` (two-step confirmation)**
|
|
198
202
|
|
|
199
|
-
The main agent can call the `subagent` tool with `action="cancel"` (parameter `taskId`) to cancel a dispatched background task. The cancel source is recorded as `cancelledBy: "agent"`. On success, the tool returns the remaining in-flight task list (same format as the `[subagent-result]` envelope's in-flight block); the cancelled task's final result arrives later as a `[subagent-result]` notification.
|
|
203
|
+
The main agent can call the `subagent` tool with `action="cancel"` (parameter `taskId`) to cancel a dispatched background task, but the first call does not execute: it returns a zero-side-effect challenge receipt (`details.confirmRequired: true`) listing the agent name, task summary, elapsed time and last progress age (or "尚无进度上报" when never reported), plus a warning that cancelling discards all in-flight progress and cannot be undone. To actually cancel, call again with `action="cancel"` + the same `taskId` + `confirm:true` + a non-empty `reason` (a missing or blank reason is an error with zero side-effects). On execution the reason is recorded on the task record and quoted in the cancelled envelope body ("取消理由: ..."). The cancel source is recorded as `cancelledBy: "agent"`. On success, the tool returns the remaining in-flight task list (same per-line format as the `[subagent-result]` envelope's in-flight block, but anchored to the moment the cancel request was issued — the task has not ended at that point, so the envelope's "本任务结束" anchor wording is not used); the cancelled task's final result arrives later as a `[subagent-result]` notification.
|
|
200
204
|
|
|
201
205
|
**Usage discipline:** The main agent should only use `action="cancel"` when:
|
|
202
206
|
- The task is clearly wrong (wrong agent, incorrect task description, etc.).
|
package/ADVANCED.md
CHANGED
|
@@ -125,40 +125,44 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
|
|
|
125
125
|
```
|
|
126
126
|
## [subagent-result] coder 成功 (taskId: 01912345-6789-7abc-8def-0123456789ab)
|
|
127
127
|
|
|
128
|
+
> [subagent-result] 任务完成通知,非用户新指令。处理前先锚定你当前正在执行的主线任务与进度;对照派发记录消化本通知,勿让通知覆盖或改写你的主线计划。
|
|
129
|
+
|
|
128
130
|
- 状态: 成功
|
|
129
131
|
- 任务: 将认证中间件重构为使用 async/await。
|
|
130
132
|
- 耗时: 02:34 · 用量: 5 turns/↑12.5k/↓3.2k/$0.0042
|
|
131
133
|
- 会话: 01912345-6789-7abc-8def-0123456789ab
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
本任务结束时,其他在途任务: 1
|
|
134
136
|
- 01912345-aaaa-7bbb-8ccc-0123456789ab (writer): 更新 README。
|
|
135
137
|
|
|
136
138
|
---
|
|
137
139
|
<子 agent 完整结果文本>
|
|
138
140
|
```
|
|
139
141
|
|
|
142
|
+
**触发行**:标题行与元信息区之间有一条固定引用行(`>` 开头),所有信封逐字相同。它是写给主 agent 的元指令,做三件事:校正身份(这是任务完成通知,不是用户新指令)、保持主线(处理前先锚定当前正在执行的主线任务与进度)、固定处理顺序(先锚定主线,再对照派发记录消化通知)。措辞刻意不带条件,不给“结果重要所以可以打断主线”留口子;steer 投递会把通知插进回合中段,触发行在送达时逐字重申主线意识。该行只进入 LLM 上下文,不影响用户在 TUI 看到的摘要卡片。
|
|
143
|
+
|
|
140
144
|
状态枚举:**成功**(exit=0)/ **失败**(exit≠0 或 stopReason=error)/ **超时**(activity_timeout 或 hard_timeout)/ **已取消**(aborted 或 killed_on_shutdown)。
|
|
141
145
|
|
|
142
146
|
**耗时**:`- 耗时:` 行是子 agent 的真实运行时长。有结果时取进程实际启动到结束(`finishedAt - startedAt`);取消(用户/agent/会话关闭)或内部错误导致无结果返回时,改从派发时刻起算。格式为 `MM:SS`,≥1 小时为 `H:MM:SS`(小时不补零)。四种状态(成功/失败/超时/已取消)的信封与 TUI 通知卡片都带耗时。
|
|
143
147
|
|
|
144
148
|
"已取消"分三种情况,信封正文不同:
|
|
145
149
|
- 用户通过 `/subagent-cancel` 取消(cancelledBy: user)→ 正文注明"属用户主动操作。请勿自动重新派发;如需重新派发,先询问用户。"
|
|
146
|
-
- 主 agent 通过 `subagent` 工具(`action="cancel"`)取消(cancelledBy: agent)→ 正文注明"该任务已由主 agent 通过 subagent 工具(action=cancel)取消。"
|
|
150
|
+
- 主 agent 通过 `subagent` 工具(`action="cancel"`)取消(cancelledBy: agent)→ 正文注明"该任务已由主 agent 通过 subagent 工具(action=cancel)取消。",并附"取消理由: ..."(两步确认时填写的 reason)
|
|
147
151
|
- 会话关闭(session_shutdown)终止(cancelledBy: 无)→ 正文注明"任务因会话关闭被终止(session_shutdown)。"
|
|
148
152
|
|
|
149
153
|
主 agent 收到状态为"已取消"的通知时,应区分来源:用户主动取消**不得自动重试**,必须先询问用户;agent 取消是自身决策,不应在无新信息时重新派发;会话关闭终止可在会话恢复后视情况重新派发。
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
**在途任务块**:信封元信息区的“在途任务”列表列出**其余**仍在运行的后台任务(本任务在构建信封前已从注册表移除,故不包含自身),格式为 `本任务结束时,其他在途任务: N` 加每行 `- taskId (agent名): 任务描述`,无在途任务时为“本任务结束时无其他在途任务。”列表**不含耗时或时钟时间**(回答“本任务结束时还有什么在跑”,而非“跑了多久”或“几点了”)。该列表是**构建时刻快照**,措辞锚定本任务结束事件而非绝对“此刻”——信封构建与送达之间主 agent 可能已派发新任务,快照随之滞后;与主 agent 本回合亲手发出的派发记录冲突时,以派发记录为准。主 agent 据此知道还有几个任务没回来:剩余不为 0 时,不要向用户汇报“全部完成”。
|
|
152
156
|
|
|
153
157
|
结果全量进入 LLM 上下文(不截断)。`details` 携带结构化数据(taskId、agent、status、exitCode、stopReason、durationMs(耗时毫秒数,必填)、usage、sessionId、完整输出),不参与 LLM 上下文,供程序消费。
|
|
154
158
|
|
|
155
159
|
### 通知投递
|
|
156
160
|
|
|
157
|
-
通知通过 `pi.sendMessage` 发送,`deliverAs: "
|
|
161
|
+
通知通过 `pi.sendMessage` 发送,`deliverAs: "steer"` + `triggerTurn: true`:
|
|
158
162
|
- 主 agent 空闲时直接触发新的对话回合。
|
|
159
|
-
- 主 agent
|
|
163
|
+
- 主 agent 忙碌时进入消息队列,在当前 assistant turn 的工具调用执行完后、下一次 LLM 调用前送达(steer 语义),不憋到整个回合结束——避免通知滞后于回合内新派发的任务。
|
|
160
164
|
|
|
161
|
-
主 agent 通过 promptGuidelines 被训练识别 `[subagent-result]`
|
|
165
|
+
主 agent 通过 promptGuidelines 被训练识别 `[subagent-result]` 前缀为系统通知(非用户请求);工具描述中的“通知消化流程”条目进一步规定消化顺序:先锚定当前主线任务与进度,再对照派发记录消化通知,基于结果自主决定下一步,与主线冲突时暂缓优先,勿让通知改写主线计划。信封标题行下的固定触发行(见上文信封格式)在通知送达时逐字重申这一顺序,缓解 steer 投递对回合计划连续性的打断。
|
|
162
166
|
|
|
163
167
|
### 进度 widget
|
|
164
168
|
|
|
@@ -194,9 +198,9 @@ widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果
|
|
|
194
198
|
|
|
195
199
|
无参数。与 `/subagent-cancel` 按 taskId 取消单个任务不同,`/subagent-cancel-all` 取消全部运行中的任务。每个被取消任务照常推送各自的"已取消" `[subagent-result]` 通知(主 agent 会收到 N 个已取消信封)。成功时提示"已取消全部 N 个运行中任务",无运行中任务时提示"无运行中任务可取消"。取消来源同样标记为 `cancelledBy: "user"`。
|
|
196
200
|
|
|
197
|
-
**路径二:主 agent `subagent` 工具(`action="cancel"
|
|
201
|
+
**路径二:主 agent `subagent` 工具(`action="cancel"`,两步确认)**
|
|
198
202
|
|
|
199
|
-
主 agent 可调用 `subagent`
|
|
203
|
+
主 agent 可调用 `subagent` 工具(`action="cancel"`,参数 `taskId`)取消已派出的后台任务,但首次调用不会直接执行:它返回零副作用的质询回执(`details.confirmRequired: true`),列出 agent 名、任务摘要、已运行时长、最近进度距今(从未上报则明示"尚无进度上报"),并警告取消将丢弃全部在途进度且不可撤销。确认取消需再次调用:`action="cancel"` + 同一 `taskId` + `confirm:true` + 非空 `reason`(缺失或空白报错,零副作用)。执行后 `reason` 记录在任务记录上,并随取消信封正文返回("取消理由: ...")。取消来源标记为 `cancelledBy: "agent"`。取消成功后返回其余在途任务列表(列表行格式与信封的“在途任务”块一致,但措辞锚定取消请求发出时刻——此时该任务并未结束,不用信封的“本任务结束”锚定语),被取消任务的最终结果稍后以 `[subagent-result]` 通知返回。
|
|
200
204
|
|
|
201
205
|
**使用纪律:** 主 agent 仅在以下情况使用 `action="cancel"`:
|
|
202
206
|
- 任务明显错误(委派了错误的 agent、任务描述有误等)。
|
package/README.en.md
CHANGED
|
@@ -114,7 +114,8 @@ The subagent runs in a background process. A progress widget appears above the T
|
|
|
114
114
|
When the subagent finishes, its result is pushed as a **`[subagent-result]` system notification** (a system message, not a user request):
|
|
115
115
|
|
|
116
116
|
- If the main agent is **idle**, the notification triggers a new turn immediately.
|
|
117
|
-
- If the main agent is **busy**, it is queued and
|
|
117
|
+
- If the main agent is **busy**, it is queued and delivered with steer semantics — after the current assistant turn's tool calls finish, before the next LLM call — without waiting for the whole turn to end.
|
|
118
|
+
- Either way, the envelope carries a fixed **trigger line** right under the title, reminding the main agent that this is a completion notification rather than a new user instruction, and to anchor its current mainline task and progress before digesting it (see "Notification envelope and card" for the format).
|
|
118
119
|
|
|
119
120
|
Results arrive automatically — **no polling**. In-flight task information is provided directly by the `[subagent-result]` notification envelope; `action="status"` was removed as a cleanup in v1.2.0.
|
|
120
121
|
|
|
@@ -146,7 +147,7 @@ User runs /subagent-result <taskId> to read the full output
|
|
|
146
147
|
| Tool | Purpose | Key constraint |
|
|
147
148
|
|------|---------|----------------|
|
|
148
149
|
| `subagent` | Single-entry tool (`action` parameter); `action="dispatch"` (default) dispatches asynchronously (TUI mode), falls back to sync in non-TUI | Receipt ≠ result; results arrive as notifications, don't poll |
|
|
149
|
-
| `subagent` `action="cancel"` | Main agent cancels one in-flight task | Only when clearly wrong or no longer needed; never for being slow |
|
|
150
|
+
| `subagent` `action="cancel"` | Main agent cancels one in-flight task (two-step confirmation: first call returns a challenge; `confirm:true` + a non-empty `reason` executes) | Only when clearly wrong or no longer needed; never for being slow |
|
|
150
151
|
|
|
151
152
|
> **v1.2.0 note**: the `subagent` tool's `action="status"` has been removed as a cleanup. In-flight task information is now provided by the `[subagent-result]` notification envelope's in-flight block, with no active-query entry point.
|
|
152
153
|
|
|
@@ -167,21 +168,24 @@ The `[subagent-result]` notification is **self-contained** — it carries everyt
|
|
|
167
168
|
```
|
|
168
169
|
## [subagent-result] coder 成功 (taskId: 01912345-6789-7abc-8def-0123456789ab)
|
|
169
170
|
|
|
171
|
+
> [subagent-result] 任务完成通知,非用户新指令。处理前先锚定你当前正在执行的主线任务与进度;对照派发记录消化本通知,勿让通知覆盖或改写你的主线计划。
|
|
172
|
+
|
|
170
173
|
- 状态: 成功
|
|
171
174
|
- 任务: 将认证中间件重构为使用 async/await。
|
|
172
175
|
- 耗时: 02:34 · 用量: 5 turns/↑12.5k/↓3.2k/$0.0042
|
|
173
176
|
- 会话: 01912345-6789-7abc-8def-0123456789ab
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
本任务结束时,其他在途任务: 1
|
|
176
179
|
- 01912345-aaaa-7bbb-8ccc-0123456789ab (writer): 更新 README。
|
|
177
180
|
|
|
178
181
|
---
|
|
179
182
|
<full subagent output>
|
|
180
183
|
```
|
|
181
184
|
|
|
185
|
+
- **Trigger line**: a fixed blockquote line, verbatim-identical in every envelope, placed right after the title line and before the metadata and in-flight blocks. It is a meta-instruction addressed to the main agent: identity correction (this is a completion notification, not a new user instruction), mainline anchoring (anchor the mainline task and progress currently in flight before digesting the notification), and a fixed processing order (anchor the mainline first, then digest it against dispatch records). The wording is deliberately unconditional, leaving no "the result is important, so interrupting the mainline is fine" loophole; because steer delivery can land a notification mid-turn, the line keeps the main agent from letting a notification override or rewrite its mainline plan.
|
|
182
186
|
- **Status**: `成功` (success) / `失败` (failure) / `超时` (timeout) / `已取消` (cancelled).
|
|
183
187
|
- **Duration**: the subagent's real run time (process start to finish; `MM:SS`, or `H:MM:SS` at 1h+), shown for all four states. For cancellations or internal errors with no result, it is measured from dispatch time.
|
|
184
|
-
- **In-flight block**:
|
|
188
|
+
- **In-flight block**: a build-time snapshot anchored to this task's end event (excluding itself), listing the other background tasks still running when this task ended; it may be stale by delivery time — when it conflicts with dispatch records issued this turn, the dispatch records prevail. The main agent learns how many are outstanding — while the count is non-zero, do not report "all done" to the user.
|
|
185
189
|
- **Full result**: the body enters the LLM context in full, untruncated.
|
|
186
190
|
|
|
187
191
|
In the TUI, the user sees a **tinted summary card**, not the full result: success green (✓), failure red (✗), timeout/cancelled yellow. The card shows the agent, status, taskId, duration, and usage summary (duration included for all four states), plus the hint `查看全文: /subagent-result <taskId>`; the full text lives in the task's session file.
|
|
@@ -196,7 +200,8 @@ Async mode introduces a few rules, baked into the tool prompts and implementatio
|
|
|
196
200
|
|
|
197
201
|
- **Cancel-origin distinction**: `已取消` (cancelled) has three origins — user (`/subagent-cancel`), main agent (`subagent` tool with `action="cancel"`), and session shutdown (`session_shutdown`). A user-initiated cancel must **never be auto-retried**; ask the user first.
|
|
198
202
|
- **No polling**: results arrive automatically as notifications; in-flight task information is provided directly by the `[subagent-result]` notification envelope, with no active-query entry point.
|
|
199
|
-
- **
|
|
203
|
+
- **Notification digestion**: a `[subagent-result]` is a completion notification, not a new user instruction; the main agent anchors its current mainline task and progress before handling it, digests it against its own dispatch records, and decides the next step autonomously from the result. When a notification conflicts with the mainline, it defers rather than letting the notification rewrite the plan. The discipline is baked in twice: the envelope trigger line plus a "notification digestion" entry in the tool description.
|
|
204
|
+
- **Anti-abuse cancellation**: `action="cancel"` is a two-step confirmation (the first call only returns a zero-side-effect challenge with elapsed time and last progress; `confirm:true` + a non-empty `reason` executes, and the reason is recorded on the task and quoted in the cancelled envelope body), with prompt guidance — cancel only when the task is clearly wrong or no longer needed, never just because it's slow (background subagents are expected to run long). Waiting means making no tool call at all and ending the turn; there is deliberately no query, nag or status action for in-flight tasks.
|
|
200
205
|
- **Resource-conflict discipline**: before dispatching multiple tasks in parallel, consider whether they touch the same files or code areas; when in doubt, dispatch sequentially or ask the user.
|
|
201
206
|
- **Subagents cannot call the subagent tool**: a subagent (depth ≥ 1) can never call any `subagent` action (including `action="cancel"`); delegation depth is capped at 1.
|
|
202
207
|
- **TUI async / non-TUI sync fallback**: only TUI mode takes the async path; print/json and other non-TUI modes fall back to synchronous blocking.
|
package/README.md
CHANGED
|
@@ -114,7 +114,8 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
114
114
|
子 agent 完成后,结果以 **`[subagent-result]` 系统通知**推送到对话(系统消息,不是用户请求):
|
|
115
115
|
|
|
116
116
|
- 主 agent **空闲**时,通知直接触发新的对话回合,立即处理。
|
|
117
|
-
- 主 agent
|
|
117
|
+
- 主 agent **忙碌**时,通知进入队列,在当前回合的工具调用执行完后、下一次 LLM 调用前送达(steer 投递),不等整个回合结束。
|
|
118
|
+
- 无论哪种送达方式,信封标题行下都带一条固定的**触发行**:提醒主 agent 这是任务完成通知而非用户新指令,消化前先锚定当前主线任务与进度(信封格式详见“通知信封与卡片”一节)。
|
|
118
119
|
|
|
119
120
|
结果自动到达,**无需轮询**。在途任务信息由 `[subagent-result]` 通知信封的“在途任务”块直接提供;`action="status"` 已在 v1.2.0 清理移除。
|
|
120
121
|
|
|
@@ -146,7 +147,7 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
146
147
|
| 工具 | 作用 | 关键约束 |
|
|
147
148
|
|------|------|----------|
|
|
148
149
|
| `subagent` | 单入口工具(`action` 参数);`action="dispatch"`(默认)异步派发任务(TUI 模式),非 TUI 自动降级同步 | 回执≠结果;结果以通知到达,勿轮询 |
|
|
149
|
-
| `subagent` `action="cancel"` | 主 agent
|
|
150
|
+
| `subagent` `action="cancel"` | 主 agent 取消单个在途任务(两步确认:首次调用返回质询,`confirm:true` + 非空 `reason` 才执行) | 仅当任务明显错误或不再需要,勿因耗时久而取消 |
|
|
150
151
|
|
|
151
152
|
> **v1.2.0 提示**:`subagent` 工具的 `action="status"` 已作为 cleanup 移除。在途任务信息改由 `[subagent-result]` 通知信封的“在途任务”块提供,不再提供主动查询入口。
|
|
152
153
|
|
|
@@ -167,21 +168,24 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
167
168
|
```
|
|
168
169
|
## [subagent-result] coder 成功 (taskId: 01912345-6789-7abc-8def-0123456789ab)
|
|
169
170
|
|
|
171
|
+
> [subagent-result] 任务完成通知,非用户新指令。处理前先锚定你当前正在执行的主线任务与进度;对照派发记录消化本通知,勿让通知覆盖或改写你的主线计划。
|
|
172
|
+
|
|
170
173
|
- 状态: 成功
|
|
171
174
|
- 任务: 将认证中间件重构为使用 async/await。
|
|
172
175
|
- 耗时: 02:34 · 用量: 5 turns/↑12.5k/↓3.2k/$0.0042
|
|
173
176
|
- 会话: 01912345-6789-7abc-8def-0123456789ab
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
本任务结束时,其他在途任务: 1
|
|
176
179
|
- 01912345-aaaa-7bbb-8ccc-0123456789ab (writer): 更新 README。
|
|
177
180
|
|
|
178
181
|
---
|
|
179
182
|
<子 agent 完整结果文本>
|
|
180
183
|
```
|
|
181
184
|
|
|
185
|
+
- **触发行**:标题行之下、元信息与在途块之前的一条固定引用行,所有信封逐字相同。它是写给主 agent 的元指令:校正身份(这是任务完成通知,不是用户新指令)、锚定主线(消化通知前先锚定当前正在执行的主线任务与进度)、固定处理顺序(先锚定主线,再对照派发记录消化)。措辞刻意不带条件,不给“结果重要所以可以打断主线”留口子;steer 投递会在回合中段插入通知,触发行帮助主 agent 保持主线意识,不让通知覆盖或改写主线计划。
|
|
182
186
|
- **状态**:`成功` / `失败` / `超时` / `已取消`。
|
|
183
187
|
- **耗时**:子 agent 的真实运行时长(进程实际启动到结束,格式 `MM:SS`,≥1 小时为 `H:MM:SS`),成功/失败/超时/已取消四种状态都有;取消或内部错误(无结果返回)时从派发时刻起算。
|
|
184
|
-
-
|
|
188
|
+
- **在途任务块**:锚定“本任务结束”事件的构建时刻快照(不含自身),列出本任务结束时其余仍在运行的后台任务;快照在送达时可能滞后,与本回合亲手发出的派发记录冲突时以派发记录为准。让主 agent 知道还有几个任务没回来——剩余不为 0 时,不要向用户汇报“全部完成”。
|
|
185
189
|
- **完整结果**:正文全量进入 LLM 上下文,不截断。
|
|
186
190
|
|
|
187
191
|
用户在 TUI 中看到的是**带底色的摘要卡片**,不是全文:成功绿色(✓)、失败红色(✗)、超时/已取消黄色。卡片显示 agent、状态、taskId、耗时和用量摘要(四种状态均带耗时),并提示 `查看全文: /subagent-result <taskId>`;完整结果保存在任务会话文件中。
|
|
@@ -196,7 +200,8 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
196
200
|
|
|
197
201
|
- **取消来源区分**:`已取消` 有用户(`/subagent-cancel`)、主 agent(`subagent` 工具 `action="cancel"`)、会话关闭(`session_shutdown`)三种来源;用户取消**不得自动重试**,须先询问。
|
|
198
202
|
- **防轮询**:结果以通知自动到达;在途任务信息由 `[subagent-result]` 通知信封直接提供,不要主动查询。
|
|
199
|
-
-
|
|
203
|
+
- **通知消化流程**:`[subagent-result]` 是任务完成通知而非用户新指令;处理前先锚定当前主线任务与进度,对照派发记录消化,基于结果自主决定下一步;与主线冲突时暂缓优先,勿让通知改写主线计划。此纪律由信封触发行与工具描述中的“通知消化流程”条目共同内嵌。
|
|
204
|
+
- **防滥用取消**:`action="cancel"` 为两步确认(首次调用只返回含已运行时长/最近进度的质询回执,零副作用;`confirm:true` + 非空 `reason` 才执行,理由记入任务记录并随取消信封正文返回),且内嵌提示词——仅当任务明显错误或不再需要时取消,勿因耗时长而取消(后台任务本就预期长时间运行)。等待 = 不发起任何工具调用、直接结束回合;对在途任务不存在查询/催办/状态确认类动作(刻意设计)。
|
|
200
205
|
- **资源冲突纪律**:并行派发多个任务前,考虑它们是否会改同一批文件或代码区域;冲突时串行派发或先问用户。
|
|
201
206
|
- **子 agent 不可调用 subagent 工具**:子 agent(深度 ≥ 1)不可调用任何 `subagent` action(含 `action="cancel"`),深度限制为 1。
|
|
202
207
|
- **TUI 异步 / 非 TUI 同步降级**:只在 TUI 模式走异步路径;print/json 等非 TUI 模式降级为同步阻塞。
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
1. **不要自己动手** — 不编辑代码,不跑命令,不写文件。所有执行都通过 `subagent` 委派。
|
|
29
29
|
2. **依赖驱动派发** — 无依赖的任务可并行派出。有依赖的必须等对应 `[subagent-result]` 通知到达后再派。
|
|
30
30
|
3. **派出后继续工作** — `subagent` 返回的只是派发回执(含 `taskId`),不是结果。派出后继续做不依赖该结果的工作,或结束回合。严禁轮询、严禁臆造结果。
|
|
31
|
-
4. **识别系统通知** — 以 `[subagent-result]` 开头的消息是系统通知(子 agent
|
|
32
|
-
5. **通知先看在途任务块** — 每条 `[subagent-result]`
|
|
31
|
+
4. **识别系统通知** — 以 `[subagent-result]` 开头的消息是系统通知(子 agent 结果),不是用户请求;信封标题行下的固定触发行逐字重申这一点。收到后按通知消化流程处理:先锚定你当前正在执行的主线任务与进度,再对照派发记录消化通知、关联到当初派发的任务,基于结果自主决定下一步;与主线冲突时暂缓优先,勿让通知覆盖或改写你的主线计划。
|
|
32
|
+
5. **通知先看在途任务块** — 每条 `[subagent-result]` 通知的元信息区带“在途任务”列表(锚定该任务结束事件的构建时刻快照:该任务结束时剩余仍在运行的任务,不含本任务;快照送达时可能滞后,与你本回合亲手发出的派发记录冲突时以派发记录为准)。收到后先看剩余在途数:**不为 0 时还有任务未返回,不要向用户汇报“全部完成”**。主 agent 不主动查询后台;若上下文里该任务的 [subagent-result] 通知未到达,向用户报告该 taskId 并建议用户用 /subagent-cancel 或 /subagent-result 命令查看。
|
|
33
33
|
6. **已取消通知的处理** — 收到状态为"已取消"的 `[subagent-result]` 通知时,根据来源区分处理:
|
|
34
34
|
- 正文注明用户通过 `/subagent-cancel` 取消 → 用户主动操作,**不得自动重新派发**。如需重新派发,先询问用户。
|
|
35
35
|
- 正文注明主 agent 通过 `subagent` 工具(`action="cancel"`)取消 → 自身决策,不应在无新信息时重新派发。
|
|
36
36
|
- 正文注明会话关闭(session_shutdown)终止 → 可在会话恢复后视情况重新派发。
|
|
37
37
|
|
|
38
|
-
7. **`action="cancel"` 使用纪律** — 你可以使用 `subagent` 工具(`action="cancel"`,参数 `taskId`)纠正错误委派(如委派了错误的 agent
|
|
38
|
+
7. **`action="cancel"` 使用纪律** — 你可以使用 `subagent` 工具(`action="cancel"`,参数 `taskId`)纠正错误委派(如委派了错误的 agent、任务描述有误)或取消不再需要的任务。取消是两步确认:首次调用只返回质询回执(含已运行时长与最近进度,零副作用);确认需再次调用并带同一 `taskId` + `confirm:true` + 非空 `reason`(理由会记入任务记录并随取消信封返回)。**不要因等待时间长而取消**——后台子 agent 本就预期长时间运行。取消的判据是"这个任务不该继续",不是"等太久了"。等待 = 不发起任何工具调用、直接结束回合;对在途任务不存在查询/催办/状态确认类动作(刻意设计)。
|
|
39
39
|
|
|
40
40
|
8. **用标准任务格式** — 每次委派必须包含以下结构:
|
|
41
41
|
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
|
|
95
95
|
子 agent 的结果以 `[subagent-result]` 通知分散、不定序到达。整合时以用户目标为锚,不被通知到达的顺序带着走:
|
|
96
96
|
|
|
97
|
+
- **先锚定再消化**:通知可能在回合中段送达(steer 投递),打断正在推进的回合计划。处理每条通知前先锚定当前主线任务与进度,消化完毕回到主线继续,不让通知覆盖或改写主线计划。
|
|
97
98
|
- **同组不齐不交付**:同一目标下的多个子任务,等该组全部返回后统一整合汇报;组内未齐,不提前交付。
|
|
98
99
|
- **先归类再确认**:每条通知到达时,先判断它属于哪个目标组,再据信封"在途任务"块确认该组剩余在途数量,最后归位汇总。
|
|
99
100
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -630,6 +630,10 @@ export function formatDuration(ms: number): string {
|
|
|
630
630
|
*/
|
|
631
631
|
export class SubagentProgressManager {
|
|
632
632
|
private agents = new Map<string, AgentProgress>();
|
|
633
|
+
// Last progress timestamp per session, fed by update(); backs the cancel
|
|
634
|
+
// challenge's "最近进度距今" line. Kept separate from AgentProgress so the
|
|
635
|
+
// challenge can read it without touching widget render state.
|
|
636
|
+
private lastActivityAt = new Map<string, number>();
|
|
633
637
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
634
638
|
private ctx: ExtensionContext | null = null;
|
|
635
639
|
private widgetSet = false;
|
|
@@ -653,9 +657,19 @@ export class SubagentProgressManager {
|
|
|
653
657
|
if (update.phase !== undefined) agent.phase = update.phase;
|
|
654
658
|
if (update.currentTool !== undefined) agent.currentTool = update.currentTool;
|
|
655
659
|
if (update.recentTools !== undefined) agent.recentTools = update.recentTools;
|
|
660
|
+
this.lastActivityAt.set(sessionId, Date.now());
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Last update() time for a session, or undefined when the task has never
|
|
665
|
+
* reported progress (the cancel challenge renders that as "尚无进度上报").
|
|
666
|
+
*/
|
|
667
|
+
getLastActivityAt(sessionId: string): number | undefined {
|
|
668
|
+
return this.lastActivityAt.get(sessionId);
|
|
656
669
|
}
|
|
657
670
|
|
|
658
671
|
unregister(sessionId: string): void {
|
|
672
|
+
this.lastActivityAt.delete(sessionId);
|
|
659
673
|
if (!this.agents.has(sessionId)) return;
|
|
660
674
|
this.agents.delete(sessionId);
|
|
661
675
|
if (this.agents.size === 0) {
|
|
@@ -668,6 +682,18 @@ export class SubagentProgressManager {
|
|
|
668
682
|
}
|
|
669
683
|
}
|
|
670
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Drop every registered task (widget state and last-activity timestamps)
|
|
687
|
+
* and stop the refresh timer. Test-isolation hook only: the module-level
|
|
688
|
+
* singleton outlives individual tests, so a case that dispatches without
|
|
689
|
+
* finishing its tasks would otherwise leak state into later cases.
|
|
690
|
+
* Production teardown always goes through per-task unregister.
|
|
691
|
+
*/
|
|
692
|
+
resetForTests(): void {
|
|
693
|
+
for (const id of [...this.agents.keys()]) this.unregister(id);
|
|
694
|
+
this.lastActivityAt.clear();
|
|
695
|
+
}
|
|
696
|
+
|
|
671
697
|
refresh(): void {
|
|
672
698
|
if (!this.ctx?.hasUI) return;
|
|
673
699
|
const sorted = [...this.agents.values()].sort((a, b) => a.startedAt - b.startedAt);
|
|
@@ -738,6 +764,14 @@ export class SubagentProgressManager {
|
|
|
738
764
|
|
|
739
765
|
const progressManager = new SubagentProgressManager();
|
|
740
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Test-isolation hook for the module-level progress singleton (see
|
|
769
|
+
* SubagentProgressManager.resetForTests). Production code never calls this.
|
|
770
|
+
*/
|
|
771
|
+
export function resetProgressManagerForTests(): void {
|
|
772
|
+
progressManager.resetForTests();
|
|
773
|
+
}
|
|
774
|
+
|
|
741
775
|
/**
|
|
742
776
|
* Build the isolated session directory for a subagent.
|
|
743
777
|
* All subagent sessions live under a dedicated root, independent of the main
|
|
@@ -1496,6 +1530,14 @@ export interface AsyncSubagentTask {
|
|
|
1496
1530
|
* the cancel's origin; undefined for non-cancelled endings.
|
|
1497
1531
|
*/
|
|
1498
1532
|
cancelledBy?: "user" | "agent";
|
|
1533
|
+
/**
|
|
1534
|
+
* Why the task was cancelled, supplied by the confirming cancel call
|
|
1535
|
+
* (action="cancel" + confirm:true requires a non-empty reason). Recorded
|
|
1536
|
+
* so the cancellation stays auditable: the [subagent-result] envelope body
|
|
1537
|
+
* quotes it (single-lined and capped at 200 chars). Undefined for user
|
|
1538
|
+
* cancels and non-cancelled endings.
|
|
1539
|
+
*/
|
|
1540
|
+
cancelReason?: string;
|
|
1499
1541
|
/**
|
|
1500
1542
|
* Child process handle, set once runSingleAgent has spawned. Lets the
|
|
1501
1543
|
* session_shutdown handler SIGKILL directly: on "quit" the main process
|
|
@@ -1518,11 +1560,12 @@ export const taskRegistry = new Map<string, AsyncSubagentTask>();
|
|
|
1518
1560
|
* /subagent-cancel command and the subagent tool's action="cancel". Returns false when
|
|
1519
1561
|
* no running task with that id exists.
|
|
1520
1562
|
*/
|
|
1521
|
-
function cancelTask(taskId: string, cancelledBy: "user" | "agent"): boolean {
|
|
1563
|
+
function cancelTask(taskId: string, cancelledBy: "user" | "agent", reason?: string): boolean {
|
|
1522
1564
|
const task = taskRegistry.get(taskId);
|
|
1523
1565
|
if (!task || task.status !== "running") return false;
|
|
1524
1566
|
task.status = "cancelled";
|
|
1525
1567
|
task.cancelledBy = cancelledBy;
|
|
1568
|
+
if (reason) task.cancelReason = reason;
|
|
1526
1569
|
task.abortController.abort();
|
|
1527
1570
|
return true;
|
|
1528
1571
|
}
|
|
@@ -1534,16 +1577,35 @@ export function truncateTaskDescription(task: string, maxLen = 200): string {
|
|
|
1534
1577
|
}
|
|
1535
1578
|
|
|
1536
1579
|
/**
|
|
1537
|
-
* Format the in-flight task list (status === "running")
|
|
1538
|
-
* envelope's 在途 block
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1580
|
+
* Format the in-flight task list (status === "running") for the result
|
|
1581
|
+
* envelope's 在途 block. The list is a build-time snapshot; since the
|
|
1582
|
+
* notification may be delivered after the main agent has dispatched new
|
|
1583
|
+
* tasks, the wording is anchored to this envelope's task-end event (an
|
|
1584
|
+
* event the main agent can order against its own dispatch records) instead
|
|
1585
|
+
* of an absolute "right now" claim. Deliberately carries no elapsed time
|
|
1586
|
+
* or clock time: the list answers "what was still running when this task
|
|
1587
|
+
* ended", not "how long has it run" or "what time is it".
|
|
1541
1588
|
*/
|
|
1542
1589
|
export function formatActiveTasks(): string {
|
|
1543
1590
|
const running = [...taskRegistry.values()].filter((t) => t.status === "running");
|
|
1544
|
-
if (running.length === 0) return "
|
|
1591
|
+
if (running.length === 0) return "本任务结束时无其他在途任务。";
|
|
1592
|
+
const lines = running.map((t) => `- ${t.taskId} (${t.agentName}): ${truncateTaskDescription(t.task)}`);
|
|
1593
|
+
return `本任务结束时,其他在途任务: ${running.length}\n${lines.join("\n")}`;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Remaining in-flight list for the action="cancel" confirmation receipt.
|
|
1598
|
+
* NOT shared with the envelope block (formatActiveTasks): at this point no
|
|
1599
|
+
* task has ended — the cancel was merely requested and the cancelled task's
|
|
1600
|
+
* result arrives later — so a "本任务结束" anchor would be wrong here. The
|
|
1601
|
+
* receipt is returned synchronously in the same turn, so anchoring the
|
|
1602
|
+
* snapshot to the cancel request itself is accurate.
|
|
1603
|
+
*/
|
|
1604
|
+
function formatRemainingTasksAfterCancelRequest(): string {
|
|
1605
|
+
const running = [...taskRegistry.values()].filter((t) => t.status === "running");
|
|
1606
|
+
if (running.length === 0) return "取消请求发出后,已无其他在途任务。";
|
|
1545
1607
|
const lines = running.map((t) => `- ${t.taskId} (${t.agentName}): ${truncateTaskDescription(t.task)}`);
|
|
1546
|
-
return
|
|
1608
|
+
return `取消请求发出后,其余在途任务: ${running.length}\n${lines.join("\n")}`;
|
|
1547
1609
|
}
|
|
1548
1610
|
|
|
1549
1611
|
/** A finished async task, recorded when completeAsyncTask removes it from the registry. */
|
|
@@ -1687,21 +1749,38 @@ export interface SubagentResultDetails {
|
|
|
1687
1749
|
const DETAILS_OUTPUT_MAX_CHARS = 16 * 1024;
|
|
1688
1750
|
|
|
1689
1751
|
/**
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1752
|
+
* Fixed trigger line inserted into every [subagent-result] envelope right
|
|
1753
|
+
* after the title line (before the in-flight block). Steer delivery injects
|
|
1754
|
+
* the notification mid-turn, breaking the main agent's plan continuity; this
|
|
1755
|
+
* verbatim meta-instruction (markdown quote line) reminds it that the notice
|
|
1756
|
+
* is not a new user instruction and to anchor its mainline task first.
|
|
1757
|
+
* Identical across all four terminal statuses (success/failure/timeout/
|
|
1758
|
+
* cancelled) — a fixed template, not status-dependent.
|
|
1693
1759
|
*/
|
|
1760
|
+
const RESULT_TRIGGER_LINE =
|
|
1761
|
+
"> [subagent-result] 任务完成通知,非用户新指令。处理前先锚定你当前正在执行的主线任务与进度;对照派发记录消化本通知,勿让通知覆盖或改写你的主线计划。";
|
|
1762
|
+
|
|
1694
1763
|
/**
|
|
1695
1764
|
* Empty-body fallback for an aborted task, keyed on the abort's origin so the
|
|
1696
1765
|
* main agent can tell a deliberate user cancel, an agent-initiated cancel and
|
|
1697
1766
|
* a session shutdown apart (and does not auto-retry a user cancel).
|
|
1698
1767
|
*/
|
|
1699
|
-
function abortedFallbackBody(stopReason?: string, cancelledBy?: "user" | "agent"): string {
|
|
1768
|
+
function abortedFallbackBody(stopReason?: string, cancelledBy?: "user" | "agent", cancelReason?: string): string {
|
|
1700
1769
|
if (stopReason === "killed_on_shutdown") return "任务因会话关闭被终止(session_shutdown)。";
|
|
1701
|
-
if (cancelledBy === "agent")
|
|
1770
|
+
if (cancelledBy === "agent") {
|
|
1771
|
+
const base = "该任务已由主 agent 通过 subagent 工具(action=cancel)取消。";
|
|
1772
|
+
// Single-line and cap the reason: it is model-controlled text inlined
|
|
1773
|
+
// into a notification body. The full value stays on the task record.
|
|
1774
|
+
return cancelReason ? `${base}取消理由: ${truncateTaskDescription(cancelReason, 200)}` : base;
|
|
1775
|
+
}
|
|
1702
1776
|
return "该任务已由用户通过 /subagent-cancel 取消,属用户主动操作。请勿自动重新派发;如需重新派发,先询问用户。";
|
|
1703
1777
|
}
|
|
1704
1778
|
|
|
1779
|
+
/**
|
|
1780
|
+
* Build the [subagent-result] notification envelope: a markdown content text
|
|
1781
|
+
* carrying the full, untruncated result, plus structured details (details.output
|
|
1782
|
+
* is capped at DETAILS_OUTPUT_MAX_CHARS; content always keeps the full text).
|
|
1783
|
+
*/
|
|
1705
1784
|
export function buildResultEnvelope(
|
|
1706
1785
|
task: AsyncSubagentTask,
|
|
1707
1786
|
result: SingleResult | null,
|
|
@@ -1725,10 +1804,12 @@ export function buildResultEnvelope(
|
|
|
1725
1804
|
// Only genuine failures are labelled "内部错误"; a user cancel or session
|
|
1726
1805
|
// shutdown rejection is an expected abort, so it gets a note carrying the
|
|
1727
1806
|
// abort's origin (user cancel vs session shutdown).
|
|
1728
|
-
if (!body && errorMessage) body = status === "failure" ? `内部错误: ${errorMessage}` : abortedFallbackBody(stopReason, task.cancelledBy);
|
|
1807
|
+
if (!body && errorMessage) body = status === "failure" ? `内部错误: ${errorMessage}` : abortedFallbackBody(stopReason, task.cancelledBy, task.cancelReason);
|
|
1729
1808
|
const lines = [
|
|
1730
1809
|
`## [subagent-result] ${task.agentName} ${statusWord} (taskId: ${task.taskId})`,
|
|
1731
1810
|
"",
|
|
1811
|
+
RESULT_TRIGGER_LINE,
|
|
1812
|
+
"",
|
|
1732
1813
|
`- 状态: ${statusWord}`,
|
|
1733
1814
|
`- 任务: ${truncateTaskDescription(task.task)}`,
|
|
1734
1815
|
`- 耗时: ${formatDuration(durationMs)} · 用量: ${formatUsageStats(usage, result?.model) || "-"}`,
|
|
@@ -1739,7 +1820,7 @@ export function buildResultEnvelope(
|
|
|
1739
1820
|
formatActiveTasks(),
|
|
1740
1821
|
"",
|
|
1741
1822
|
"---",
|
|
1742
|
-
body || (status === "cancelled" ? abortedFallbackBody(stopReason, task.cancelledBy) : "(no output)"),
|
|
1823
|
+
body || (status === "cancelled" ? abortedFallbackBody(stopReason, task.cancelledBy, task.cancelReason) : "(no output)"),
|
|
1743
1824
|
];
|
|
1744
1825
|
return {
|
|
1745
1826
|
content: lines.join("\n"),
|
|
@@ -1769,6 +1850,42 @@ function buildDispatchReceipt(agentName: string, taskId: string): string {
|
|
|
1769
1850
|
return `已派出 ${agentName}. taskId: ${taskId}`;
|
|
1770
1851
|
}
|
|
1771
1852
|
|
|
1853
|
+
/**
|
|
1854
|
+
* Build the two-step-confirmation challenge for action="cancel" (first call,
|
|
1855
|
+
* confirm !== true): a zero-side-effect receipt spelling out what a cancel
|
|
1856
|
+
* would destroy — agent, task summary, elapsed time, last progress — plus the
|
|
1857
|
+
* exact second-call shape. The main agent must confirm deliberately instead
|
|
1858
|
+
* of reflexively cancelling a healthy in-flight task.
|
|
1859
|
+
*/
|
|
1860
|
+
function buildCancelChallenge(task: AsyncSubagentTask): string {
|
|
1861
|
+
const lastActivityAt = progressManager.getLastActivityAt(task.taskId);
|
|
1862
|
+
let progressLine: string;
|
|
1863
|
+
if (lastActivityAt === undefined) {
|
|
1864
|
+
progressLine = "- 最近进度: 尚无进度上报(no progress reported yet)。";
|
|
1865
|
+
} else {
|
|
1866
|
+
// Read the clock once and derive both language phrases from that single
|
|
1867
|
+
// value — two Date.now() reads could straddle a second boundary and
|
|
1868
|
+
// disagree ("5 秒前 (6s ago)").
|
|
1869
|
+
const ageSec = Math.max(0, Math.floor((Date.now() - lastActivityAt) / 1000));
|
|
1870
|
+
// Under an hour, plain seconds read best; past that, fold into
|
|
1871
|
+
// formatDuration (H:MM:SS) instead of a huge second count.
|
|
1872
|
+
progressLine =
|
|
1873
|
+
ageSec < 3600
|
|
1874
|
+
? `- 最近进度更新: ${ageSec} 秒前 (last activity ${ageSec}s ago)。`
|
|
1875
|
+
: `- 最近进度更新: ${formatDuration(ageSec * 1000)} 前 (last activity ${formatDuration(ageSec * 1000)} ago)。`;
|
|
1876
|
+
}
|
|
1877
|
+
return [
|
|
1878
|
+
`取消确认请求 (cancel confirmation required): 任务 ${task.taskId} 仍在运行;本次调用未取消任何东西。`,
|
|
1879
|
+
`- agent: ${task.agentName}`,
|
|
1880
|
+
`- 任务: ${truncateTaskDescription(task.task)}`,
|
|
1881
|
+
`- 已运行: ${formatDuration(Date.now() - task.startedAt)} (elapsed since dispatch)`,
|
|
1882
|
+
progressLine,
|
|
1883
|
+
"",
|
|
1884
|
+
"⚠️ 取消将丢弃该任务的全部在途进度,且不可撤销(cancelling discards all in-flight progress and cannot be undone)。",
|
|
1885
|
+
`如确认取消,再次调用 subagent 工具: action="cancel" + taskId="${task.taskId}" + confirm:true + reason(reason 必填,说明取消理由)。`,
|
|
1886
|
+
].join("\n");
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1772
1889
|
/**
|
|
1773
1890
|
* Finalize an async task: unregister progress, drop it from the registry, and
|
|
1774
1891
|
* push the [subagent-result] notification. Called exactly once per task, on
|
|
@@ -1820,7 +1937,7 @@ function completeAsyncTask(pi: ExtensionAPI, task: AsyncSubagentTask, result: Si
|
|
|
1820
1937
|
const envelope = buildResultEnvelope(task, result, status, stopReason, errorMessage);
|
|
1821
1938
|
pi.sendMessage(
|
|
1822
1939
|
{ customType: "subagent-result", content: envelope.content, display: true, details: envelope.details },
|
|
1823
|
-
{ deliverAs: "
|
|
1940
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
1824
1941
|
);
|
|
1825
1942
|
} catch {
|
|
1826
1943
|
// The session may already be gone (e.g. after session_shutdown); the
|
|
@@ -1851,6 +1968,15 @@ const SubagentParams = Type.Object({
|
|
|
1851
1968
|
taskId: Type.Optional(Type.String({
|
|
1852
1969
|
description: "taskId of the running background subagent task to cancel (required for action=cancel; from the dispatch receipt).",
|
|
1853
1970
|
})),
|
|
1971
|
+
confirm: Type.Optional(Type.Boolean({
|
|
1972
|
+
description:
|
|
1973
|
+
'Set true to actually execute an action=cancel after reviewing the challenge returned by the first call. Default: false — the first action=cancel call only returns a challenge (confirmRequired) and cancels nothing.',
|
|
1974
|
+
default: false,
|
|
1975
|
+
})),
|
|
1976
|
+
reason: Type.Optional(Type.String({
|
|
1977
|
+
description:
|
|
1978
|
+
"Why the task is being cancelled (required and must be non-empty when confirm=true). Recorded on the task and quoted in the [subagent-result] envelope.",
|
|
1979
|
+
})),
|
|
1854
1980
|
sessionId: Type.Optional(Type.String({
|
|
1855
1981
|
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
|
|
1856
1982
|
description: "仅用于复用此前 dispatch 回执返回的 UUID v7;省略则自动生成",
|
|
@@ -1871,7 +1997,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1871
1997
|
"",
|
|
1872
1998
|
"ACTIONS (action parameter, default \"dispatch\"):",
|
|
1873
1999
|
"- dispatch: delegate the task (async in TUI mode, blocking otherwise).",
|
|
1874
|
-
"- cancel:
|
|
2000
|
+
"- cancel: request cancellation of a running background task by taskId (two-step: the first call returns a challenge; confirm:true + reason executes).",
|
|
1875
2001
|
"- sessionId: only set when resuming (复用) a previously dispatched task. Must be the UUID v7 from a previous dispatch receipt. Omit otherwise; a new UUID v7 is generated automatically.",
|
|
1876
2002
|
"",
|
|
1877
2003
|
"ASYNC (TUI mode): returns immediately with a dispatch receipt (taskId + session id).",
|
|
@@ -1884,9 +2010,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1884
2010
|
" receipt to continue the same task later.",
|
|
1885
2011
|
"",
|
|
1886
2012
|
"CANCEL DISCIPLINE: cancel a task (action=\"cancel\") only when it is clearly",
|
|
1887
|
-
"wrong (错误) or no longer needed (不再需要).
|
|
1888
|
-
"
|
|
1889
|
-
"(
|
|
2013
|
+
"wrong (错误) or no longer needed (不再需要). Agent-initiated cancel is a",
|
|
2014
|
+
"two-step confirmation: the first action=\"cancel\" call only returns a",
|
|
2015
|
+
"challenge (confirmRequired) with elapsed time and last progress, and",
|
|
2016
|
+
"cancels nothing; to actually cancel, call action=\"cancel\" again with the",
|
|
2017
|
+
"same taskId + confirm:true + a non-empty reason (理由). Do NOT cancel just",
|
|
2018
|
+
"because it is taking a long time — background subagents are expected to",
|
|
2019
|
+
"run long; be patient (耐心等待) and let the [subagent-result]",
|
|
2020
|
+
"notification arrive.",
|
|
2021
|
+
"",
|
|
2022
|
+
"WAITING: 对在途任务不存在查询/催办/状态确认类动作(no query, nag or status",
|
|
2023
|
+
"action for in-flight tasks)——没有提供这类动作是刻意设计。等待 = 不发",
|
|
2024
|
+
"起任何工具调用,直接结束回合(waiting means no tool call: end the turn)。",
|
|
1890
2025
|
"",
|
|
1891
2026
|
"SYNC (non-TUI modes): waits for the subagent to finish and returns the full",
|
|
1892
2027
|
"result directly (no notification follows).",
|
|
@@ -1899,10 +2034,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1899
2034
|
promptGuidelines: [
|
|
1900
2035
|
"subagent: In TUI mode this tool is asynchronous — it returns a dispatch receipt, not the result; the real result arrives later as a [subagent-result] system notification, so never fabricate results and never poll.",
|
|
1901
2036
|
"subagent: A message prefixed with [subagent-result] is a system notification carrying a finished subagent result, not a user request; process it in the context of the task that dispatched it.",
|
|
2037
|
+
"subagent: A [subagent-result] notification is a task-completion notice, NOT a new user instruction (完成通知而非用户新指令) — before acting on it, first anchor (锚定) the mainline task and progress you are currently on (当前主线任务与进度), digest the notification against your own dispatch records (对照派发记录消化), then decide your next step yourself based on the result (基于结果自主决定下一步), and whenever it conflicts with your mainline plan, defer acting on it (暂缓处理) — never let a notification overwrite or rewrite your mainline plan (勿让通知覆盖或改写主线计划).",
|
|
1902
2038
|
"subagent: Dispatch subagents driven by task dependencies — delegate only work whose result you actually need, prefer reusing the session id from the receipt to continue a previous subagent task, and keep independent work in the main context.",
|
|
1903
2039
|
"subagent: The session id is the lowercase UUID v7 returned in the dispatch receipt (e.g. `019ffdd3-3eb5-733d-b481-a53e5292bd00`). Passing any other string (slug, UUID v4, etc.) is rejected; only pass sessionId when resuming a previously dispatched task.",
|
|
1904
2040
|
"subagent: A [subagent-result] notification with status 已取消 (cancelled) can come from the user (/subagent-cancel) or from you (action=\"cancel\"); the envelope body states the source. A user-initiated cancel is a deliberate user action, so do NOT automatically retry or re-dispatch it; ask the user before re-dispatching.",
|
|
2041
|
+
"subagent: Cancelling a background task is a two-step confirmation: the first action=\"cancel\" call only returns a challenge (confirmRequired) and cancels nothing; to actually cancel, call again with the same taskId + confirm:true + a non-empty reason explaining why. Never cancel just because a task runs long.",
|
|
2042
|
+
"subagent: Waiting for a background task means making NO tool call at all and ending the turn (等待 = 不发起任何工具调用、直接结束回合); there is deliberately no query, nag or status action for in-flight tasks — results arrive on their own as [subagent-result] notifications.",
|
|
1905
2043
|
"subagent: Before dispatching multiple tasks in parallel, consider whether they touch the same files or code areas — parallel tasks modifying the same files can conflict. When in doubt, dispatch sequentially or ask the user.",
|
|
2044
|
+
"subagent: The in-flight block in a [subagent-result] envelope is a build-time snapshot (构建时刻快照) anchored to that task's end event and may be stale (可能滞后) by the time you process the notification; if it conflicts with dispatch records you issued yourself this turn, trust your dispatch records (冲突时以派发记录为准).",
|
|
1906
2045
|
],
|
|
1907
2046
|
parameters: SubagentParams,
|
|
1908
2047
|
|
|
@@ -1942,16 +2081,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
1942
2081
|
};
|
|
1943
2082
|
}
|
|
1944
2083
|
// Only registry (async/TUI) tasks are cancellable; sync-mode tasks are
|
|
1945
|
-
// awaited inline and never enter the registry.
|
|
1946
|
-
|
|
2084
|
+
// awaited inline and never enter the registry. Existence is checked
|
|
2085
|
+
// BEFORE the confirm/reason gates so a wrong id always fails the same
|
|
2086
|
+
// way regardless of confirmation state.
|
|
2087
|
+
const task = taskRegistry.get(taskId);
|
|
2088
|
+
if (!task || task.status !== "running") {
|
|
1947
2089
|
return {
|
|
1948
2090
|
content: [{ type: "text", text: `无此运行中任务: ${taskId} (no running subagent task with this id).` }],
|
|
1949
2091
|
details: { taskId, cancelled: false },
|
|
1950
2092
|
isError: true,
|
|
1951
2093
|
};
|
|
1952
2094
|
}
|
|
2095
|
+
// Two-step confirmation: the first call (confirm !== true) only
|
|
2096
|
+
// returns a challenge spelling out what would be destroyed — zero
|
|
2097
|
+
// side-effects (no status change, no abort, no notification). This
|
|
2098
|
+
// structural friction exists because the main agent used to fire
|
|
2099
|
+
// reflexive cancels at healthy in-flight tasks.
|
|
2100
|
+
if (params.confirm !== true) {
|
|
2101
|
+
return {
|
|
2102
|
+
content: [{ type: "text", text: buildCancelChallenge(task) }],
|
|
2103
|
+
details: { taskId, cancelled: false, confirmRequired: true },
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
// A confirmed cancel must justify itself: the reason is recorded on
|
|
2107
|
+
// the task record and quoted in the [subagent-result] envelope body.
|
|
2108
|
+
const reason = typeof params.reason === "string" ? params.reason.trim() : "";
|
|
2109
|
+
if (!reason) {
|
|
2110
|
+
return {
|
|
2111
|
+
content: [{ type: "text", text: 'Missing or empty required parameter: "reason" (confirm:true 时 reason 必填,不能为空).' }],
|
|
2112
|
+
details: { taskId, cancelled: false },
|
|
2113
|
+
isError: true,
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
cancelTask(taskId, "agent", reason);
|
|
1953
2117
|
return {
|
|
1954
|
-
content: [{ type: "text", text: `已发送取消请求: ${taskId} (cancel request sent); 结果稍后以 [subagent-result] 通知返回。\n${
|
|
2118
|
+
content: [{ type: "text", text: `已发送取消请求: ${taskId} (cancel request sent); 结果稍后以 [subagent-result] 通知返回。\n${formatRemainingTasksAfterCancelRequest()}` }],
|
|
1955
2119
|
details: { taskId, cancelled: true },
|
|
1956
2120
|
};
|
|
1957
2121
|
}
|