@wolido/async-subagent-isolation 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ADVANCED.en.md CHANGED
@@ -114,7 +114,7 @@ Key points:
114
114
  - **The receipt is a single line.** The async-semantics guidance (don't fabricate results, don't poll, results arrive as a `[subagent-result]` notification) is embedded in the `subagent` tool's `description` / `promptGuidelines`; the receipt itself stays a single line.
115
115
  - **The receipt is not the result.** Do not fabricate results.
116
116
  - **taskId = sessionId.** The `taskId` in the receipt is the session ID; reuse it directly.
117
- - **Do not poll.** Results arrive automatically as `[subagent-result]` notifications; to confirm which tasks are still in flight (e.g. after a `/tree` rewind), use the `subagent_status` tool.
117
+ - **Do not poll.** Results arrive automatically as `[subagent-result]` notifications; to confirm which tasks are still in flight (e.g. after a `/tree` rewind), use the `subagent` tool with `action="status"`.
118
118
 
119
119
  ### [subagent-result] envelope format
120
120
 
@@ -139,12 +139,12 @@ Status enumeration: **成功** (success, exit=0) / **失败** (failure, exit≠0
139
139
 
140
140
  "Cancelled" has three sub-cases with different envelope bodies:
141
141
  - 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.
142
- - Main agent cancelled via `subagent_cancel` tool (cancelledBy: agent) → body states the task was cancelled by the main agent via the subagent_cancel tool.
142
+ - 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).
143
143
  - Session shutdown killed the task (cancelledBy: none) → body states the task was terminated by session_shutdown.
144
144
 
145
145
  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.
146
146
 
147
- **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 matches the `subagent_status` tool — `在途任务: N` followed by one `- taskId (agent): task description` line per task, or `当前无在途任务。` when none remain. It deliberately carries **no elapsed time** (it answers "what is still running", not "how long has it run"). 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.
147
+ **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 matches the `subagent` tool's `action="status"` — `在途任务: N` followed by one `- taskId (agent): task description` line per task, or `当前无在途任务。` when none remain. It deliberately carries **no elapsed time** (it answers "what is still running", not "how long has it run"). 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.
148
148
 
149
149
  The full output enters the LLM context (not truncated). The `details` carries structured data (taskId, agent, status, exitCode, stopReason, usage, sessionId, full output) for programmatic consumption; it does not enter the LLM context.
150
150
 
@@ -166,9 +166,9 @@ While subagents run, a progress widget appears above the TUI editor, listing all
166
166
 
167
167
  The taskId in the widget row can be copied for `/subagent-result` (view full result) or `/subagent-cancel` (cancel the task).
168
168
 
169
- ### subagent_status (in-flight query)
169
+ ### `action="status"` (in-flight query)
170
170
 
171
- The main agent can actively query still-running background tasks via the `subagent_status` tool (no parameters), which returns:
171
+ The main agent can actively query still-running background tasks via the `subagent` tool with `action="status"` (no agent/task parameters needed), which returns:
172
172
 
173
173
  ```
174
174
  在途任务: 2
@@ -181,7 +181,7 @@ With no in-flight tasks it returns `当前无在途任务。`. Each line carries
181
181
  Use cases:
182
182
 
183
183
  - After a `/tree` rewind loses the receipts, confirm which tasks are still in flight.
184
- - When unsure how many tasks remain, or to pick a taskId for `subagent_cancel`.
184
+ - When unsure how many tasks remain, or to pick a taskId for `action="cancel"`.
185
185
 
186
186
  **Do not use it to poll for completion**: results arrive automatically as `[subagent-result]` notifications. This tool only confirms "what is still running" — it should not be called frequently.
187
187
 
@@ -207,11 +207,11 @@ To cancel all running tasks at once:
207
207
 
208
208
  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"`.
209
209
 
210
- **Path 2: Main agent `subagent_cancel` tool**
210
+ **Path 2: Main agent `subagent` tool with `action="cancel"`**
211
211
 
212
- The main agent can call the `subagent_cancel` tool (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 `subagent_status`); the cancelled task's final result arrives later as a `[subagent-result]` notification.
212
+ 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 `action="status"`); the cancelled task's final result arrives later as a `[subagent-result]` notification.
213
213
 
214
- **Usage discipline:** The main agent should only use `subagent_cancel` when:
214
+ **Usage discipline:** The main agent should only use `action="cancel"` when:
215
215
  - The task is clearly wrong (wrong agent, incorrect task description, etc.).
216
216
  - The task is no longer needed (requirement change, later discovery that this step is unnecessary).
217
217
 
@@ -292,7 +292,7 @@ These variables are propagated into every subagent process automatically:
292
292
 
293
293
  | Variable | Default | Description |
294
294
  |----------|---------|-------------|
295
- | `PI_SUBAGENT_DEPTH` | `0` | Current recursion depth. Auto-incremented per nested call. **Depth limit is 1** — a subagent (depth ≥ 1) cannot dispatch `subagent`; recursive delegation is blocked entirely. |
295
+ | `PI_SUBAGENT_DEPTH` | `0` | Current recursion depth. Auto-incremented per nested call. **Depth limit is 1** — a subagent (depth ≥ 1) cannot call any `subagent` action (including `action="status"` / `action="cancel"`). |
296
296
  | `PI_CURRENT_AGENT_NAME` | — | Name of the current agent, injected into every subagent process. |
297
297
  | `PI_SUBAGENT_ACTIVITY_TIMEOUT_MS` | `600000` (10 min) | Max idle time with no output on either stdout or stderr before the subagent is killed. |
298
298
  | `PI_SUBAGENT_HARD_TIMEOUT_MS` | `0` (disabled) | Absolute maximum runtime for a single call. Set a positive value (ms) to enable. |
package/ADVANCED.md CHANGED
@@ -114,7 +114,7 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
114
114
  - **回执为单行。** 异步语义引导(不臆造结果、不轮询、结果以 `[subagent-result]` 通知到达)已内嵌于 `subagent` 工具的 `description` / `promptGuidelines`,回执本身保持单行。
115
115
  - **回执 ≠ 结果。** 不要臆造结果。
116
116
  - **taskId = sessionId。** 回执中的 `taskId` 就是 session ID,可直接复用。
117
- - **不要轮询。** 结果自动以 `[subagent-result]` 通知到达;如需确认还有哪些任务在途(如 `/tree` 回退后),用 `subagent_status` 工具查询。
117
+ - **不要轮询。** 结果自动以 `[subagent-result]` 通知到达;如需确认还有哪些任务在途(如 `/tree` 回退后),用 `subagent` 工具(`action="status"`)查询。
118
118
 
119
119
  ### [subagent-result] 信封格式
120
120
 
@@ -139,12 +139,12 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
139
139
 
140
140
  "已取消"分三种情况,信封正文不同:
141
141
  - 用户通过 `/subagent-cancel` 取消(cancelledBy: user)→ 正文注明"属用户主动操作。请勿自动重新派发;如需重新派发,先询问用户。"
142
- - 主 agent 通过 `subagent_cancel` 工具取消(cancelledBy: agent)→ 正文注明"该任务已由主 agent 通过 subagent_cancel 工具取消。"
142
+ - 主 agent 通过 `subagent` 工具(`action="cancel"`)取消(cancelledBy: agent)→ 正文注明"该任务已由主 agent 通过 subagent 工具(action=cancel)取消。"
143
143
  - 会话关闭(session_shutdown)终止(cancelledBy: 无)→ 正文注明"任务因会话关闭被终止(session_shutdown)。"
144
144
 
145
145
  主 agent 收到状态为"已取消"的通知时,应区分来源:用户主动取消**不得自动重试**,必须先询问用户;agent 取消是自身决策,不应在无新信息时重新派发;会话关闭终止可在会话恢复后视情况重新派发。
146
146
 
147
- **在途任务块**:信封元信息区的"在途任务"列表列出**其余**仍在运行的后台任务(本任务在构建信封前已从注册表移除,故不包含自身),格式与 `subagent_status` 工具一致——`在途任务: N` 加每行 `- taskId (agent名): 任务描述`,无在途任务时为"当前无在途任务。"。列表**不含耗时**(回答"还有什么在跑",而非"跑了多久")。主 agent 据此知道还有几个任务没回来:剩余不为 0 时,不要向用户汇报"全部完成"。
147
+ **在途任务块**:信封元信息区的"在途任务"列表列出**其余**仍在运行的后台任务(本任务在构建信封前已从注册表移除,故不包含自身),格式与 `subagent` 工具 `action="status"` 一致——`在途任务: N` 加每行 `- taskId (agent名): 任务描述`,无在途任务时为"当前无在途任务。"。列表**不含耗时**(回答"还有什么在跑",而非"跑了多久")。主 agent 据此知道还有几个任务没回来:剩余不为 0 时,不要向用户汇报"全部完成"。
148
148
 
149
149
  结果全量进入 LLM 上下文(不截断)。`details` 携带结构化数据(taskId、agent、status、exitCode、stopReason、usage、sessionId、完整输出),不参与 LLM 上下文,供程序消费。
150
150
 
@@ -166,9 +166,9 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
166
166
 
167
167
  widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果或 `/subagent-cancel` 取消任务。
168
168
 
169
- ### subagent_status(在途任务查询)
169
+ ### `action="status"`(在途任务查询)
170
170
 
171
- 主 agent 可主动查询仍在运行的后台任务,调用 `subagent_status` 工具(无参数),返回:
171
+ 主 agent 可主动查询仍在运行的后台任务,调用 `subagent` 工具(`action="status"`,无需 agent/task 参数),返回:
172
172
 
173
173
  ```
174
174
  在途任务: 2
@@ -181,7 +181,7 @@ widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果
181
181
  用途场景:
182
182
 
183
183
  - `/tree` 回退后回执丢失,确认还有哪些任务在途。
184
- - 不确定剩余任务时快速核对,或选取 taskId 用于 `subagent_cancel`。
184
+ - 不确定剩余任务时快速核对,或选取 taskId 用于 `action="cancel"`。
185
185
 
186
186
  **不要用它轮询完成状态**:结果会自动以 `[subagent-result]` 通知到达,本工具只用于确认"还有什么在跑",不应频繁调用。
187
187
 
@@ -207,11 +207,11 @@ widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果
207
207
 
208
208
  无参数。与 `/subagent-cancel` 按 taskId 取消单个任务不同,`/subagent-cancel-all` 取消全部运行中的任务。每个被取消任务照常推送各自的"已取消" `[subagent-result]` 通知(主 agent 会收到 N 个已取消信封)。成功时提示"已取消全部 N 个运行中任务",无运行中任务时提示"无运行中任务可取消"。取消来源同样标记为 `cancelledBy: "user"`。
209
209
 
210
- **路径二:主 agent `subagent_cancel` 工具**
210
+ **路径二:主 agent `subagent` 工具(`action="cancel"`)**
211
211
 
212
- 主 agent 可调用 `subagent_cancel` 工具(参数 `taskId`)取消已派出的后台任务。取消来源标记为 `cancelledBy: "agent"`。取消成功后返回剩余在途任务列表(格式与 `subagent_status` 一致),被取消任务的最终结果稍后以 `[subagent-result]` 通知返回。
212
+ 主 agent 可调用 `subagent` 工具(`action="cancel"`,参数 `taskId`)取消已派出的后台任务。取消来源标记为 `cancelledBy: "agent"`。取消成功后返回剩余在途任务列表(格式与 `action="status"` 一致),被取消任务的最终结果稍后以 `[subagent-result]` 通知返回。
213
213
 
214
- **使用纪律:** 主 agent 仅在以下情况使用 `subagent_cancel`:
214
+ **使用纪律:** 主 agent 仅在以下情况使用 `action="cancel"`:
215
215
  - 任务明显错误(委派了错误的 agent、任务描述有误等)。
216
216
  - 任务不再需要(用户需求变更、后续发现无需此步骤)。
217
217
 
@@ -292,7 +292,7 @@ widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果
292
292
 
293
293
  | 变量 | 默认值 | 说明 |
294
294
  |------|--------|------|
295
- | `PI_SUBAGENT_DEPTH` | `0` | 当前递归深度。每次嵌套调用自动递增。**深度限制为 1**——子 agent(depth ≥ 1)不可再派发 `subagent`,递归委派已被完全禁止。 |
295
+ | `PI_SUBAGENT_DEPTH` | `0` | 当前递归深度。每次嵌套调用自动递增。**深度限制为 1**——子 agent(depth ≥ 1)不可调用任何 `subagent` action(含 `action="status"` / `action="cancel"`)。 |
296
296
  | `PI_CURRENT_AGENT_NAME` | — | 当前 agent 名称,注入每个子 agent 进程。 |
297
297
  | `PI_SUBAGENT_ACTIVITY_TIMEOUT_MS` | `600000`(10 分钟) | stdout 和 stderr 均无输出(无活动)时的最大允许时间。 |
298
298
  | `PI_SUBAGENT_HARD_TIMEOUT_MS` | `0`(禁用) | 单次调用的绝对最大运行时长。设为正数(毫秒)启用。 |
package/README.en.md CHANGED
@@ -47,7 +47,7 @@ The main agent is the dispatcher: it understands the request, splits it into tas
47
47
 
48
48
  More important is **the freedom after dispatch**. While a task runs in the background, you keep talking to the main agent — refine the requirements, adjust the plan, discuss next steps, or raise a new task. The main agent doesn't wait idle; it can keep planning and even dispatch more tasks in parallel. Foreground conversation and background work move forward together.
49
49
 
50
- Finally, **review when the result returns**. The subagent finishes, the notification arrives, and the main agent processes it and reports back. While you wait, you can check in-flight status any time (the progress widget or `subagent_status`), but you never have to watch.
50
+ Finally, **review when the result returns**. The subagent finishes, the notification arrives, and the main agent processes it and reports back. While you wait, you can check in-flight status any time (the progress widget or the `subagent` tool with `action="status"`), but you never have to watch.
51
51
 
52
52
  In one line: sync traps you in the "swarm execution" block; async keeps you facing a single dispatcher while background work runs alongside your own pace.
53
53
 
@@ -104,7 +104,7 @@ When the subagent finishes, its result is pushed as a **`[subagent-result]` syst
104
104
  - If the main agent is **idle**, the notification triggers a new turn immediately.
105
105
  - If the main agent is **busy**, it is queued and triggers a turn after the current one finishes.
106
106
 
107
- Results arrive automatically — **no polling**. To confirm which tasks are still in flight (e.g. after a `/tree` rewind loses the receipts), use the `subagent_status` tool.
107
+ Results arrive automatically — **no polling**. To confirm which tasks are still in flight (e.g. after a `/tree` rewind loses the receipts), use the `subagent` tool with `action="status"`.
108
108
 
109
109
  ### 5. Read the full result (`/subagent-result`)
110
110
 
@@ -133,9 +133,9 @@ User runs /subagent-result <taskId> to read the full output
133
133
 
134
134
  | Tool | Purpose | Key constraint |
135
135
  |------|---------|----------------|
136
- | `subagent` | Async dispatch (TUI mode); falls back to sync in non-TUI | Receipt ≠ result; results arrive as notifications, don't poll |
137
- | `subagent_status` | List in-flight tasks (taskId, agent, description, **no elapsed time**) | Only confirm "what's still running"; never poll with it |
138
- | `subagent_cancel` | Main agent cancels one in-flight task | Only when clearly wrong or no longer needed; never for being slow |
136
+ | `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 |
137
+ | `subagent` `action="status"` | List in-flight tasks (taskId, agent, description, **no elapsed time**) | Only confirm "what's still running"; never poll with it |
138
+ | `subagent` `action="cancel"` | Main agent cancels one in-flight task | Only when clearly wrong or no longer needed; never for being slow |
139
139
 
140
140
  ### Commands (for the user)
141
141
 
@@ -180,11 +180,11 @@ See [ADVANCED.en.md](ADVANCED.en.md) for the complete envelope format, status se
180
180
 
181
181
  Async mode introduces a few rules, baked into the tool prompts and implementation, that the main agent follows automatically:
182
182
 
183
- - **Cancel-origin distinction**: `已取消` (cancelled) has three origins — user (`/subagent-cancel`), main agent (`subagent_cancel` tool), and session shutdown (`session_shutdown`). A user-initiated cancel must **never be auto-retried**; ask the user first.
184
- - **No polling**: results arrive automatically as notifications. `subagent_status` only confirms what's in flight (e.g. after a `/tree` rewind), carries no elapsed time, and is not meant to be called frequently.
185
- - **Anti-abuse cancellation**: `subagent_cancel` ships 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).
183
+ - **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.
184
+ - **No polling**: results arrive automatically as notifications. `action="status"` only confirms what's in flight (e.g. after a `/tree` rewind), carries no elapsed time, and is not meant to be called frequently.
185
+ - **Anti-abuse cancellation**: `action="cancel"` ships 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).
186
186
  - **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.
187
- - **Recursion blocked entirely**: a subagent (depth ≥ 1) can never dispatch `subagent`; delegation depth is capped at 1.
187
+ - **Subagents cannot call the subagent tool**: a subagent (depth ≥ 1) can never call any `subagent` action (including `action="status"` / `action="cancel"`); delegation depth is capped at 1.
188
188
  - **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.
189
189
 
190
190
  ---
@@ -273,7 +273,7 @@ Put it in `~/.pi/agent/subagent-isolation.json` (user-level) or `.pi/subagent-is
273
273
 
274
274
  ## Advanced usage
275
275
 
276
- Manual `subagent` calls, `sessionId` reuse, envelope and in-flight block details, `subagent_status` queries, cancellation, and environment variables are covered in [ADVANCED.en.md](ADVANCED.en.md).
276
+ Manual `subagent` calls, `sessionId` reuse, envelope and in-flight block details, `action="status"` queries, `action="cancel"` cancellation, and environment variables are covered in [ADVANCED.en.md](ADVANCED.en.md).
277
277
 
278
278
  ---
279
279
 
package/README.md CHANGED
@@ -47,7 +47,7 @@
47
47
 
48
48
  更关键的是**派发之后的自由**。任务在后台跑的时候,你可以继续和主 agent 聊天:细化需求、调整规划、商量下一步,或提出新任务。主 agent 不必干等,可以继续规划,甚至并行派发更多任务。前台对话与后台工作并行推进。
49
49
 
50
- 最后是**结果回来再验收**。子 agent 完成,通知到达,主 agent 处理并向你汇报。等待期间你可以随时查看在途状态(进度 widget 或 `subagent_status`),但不必盯着。
50
+ 最后是**结果回来再验收**。子 agent 完成,通知到达,主 agent 处理并向你汇报。等待期间你可以随时查看在途状态(进度 widget 或 `subagent` 工具 `action="status"`),但不必盯着。
51
51
 
52
52
  一句话:同步版让你陷在"集群执行"的阻塞感里;异步版让你只面对调度者,后台工作与你自己的节奏并行。
53
53
 
@@ -104,7 +104,7 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
104
104
  - 主 agent **空闲**时,通知直接触发新的对话回合,立即处理。
105
105
  - 主 agent **忙碌**时,通知进入队列,当前回合结束后再触发。
106
106
 
107
- 结果自动到达,**无需轮询**。如需确认还有哪些任务在途(例如 `/tree` 回退后回执丢失),用 `subagent_status` 工具查询。
107
+ 结果自动到达,**无需轮询**。如需确认还有哪些任务在途(例如 `/tree` 回退后回执丢失),用 `subagent` 工具(`action="status"`)查询。
108
108
 
109
109
  ### 5. 查看全文(`/subagent-result`)
110
110
 
@@ -133,9 +133,9 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
133
133
 
134
134
  | 工具 | 作用 | 关键约束 |
135
135
  |------|------|----------|
136
- | `subagent` | 异步派发任务(TUI 模式);非 TUI 自动降级同步 | 回执≠结果;结果以通知到达,勿轮询 |
137
- | `subagent_status` | 查询在途任务(taskId、agent、任务描述,**无耗时**) | 仅确认"还有什么在跑",勿用它轮询完成 |
138
- | `subagent_cancel` | 主 agent 取消单个在途任务 | 仅当任务明显错误或不再需要,勿因耗时久而取消 |
136
+ | `subagent` | 单入口工具(`action` 参数);`action="dispatch"`(默认)异步派发任务(TUI 模式),非 TUI 自动降级同步 | 回执≠结果;结果以通知到达,勿轮询 |
137
+ | `subagent` `action="status"` | 查询在途任务(taskId、agent、任务描述,**无耗时**) | 仅确认"还有什么在跑",勿用它轮询完成 |
138
+ | `subagent` `action="cancel"` | 主 agent 取消单个在途任务 | 仅当任务明显错误或不再需要,勿因耗时久而取消 |
139
139
 
140
140
  ### 命令(用户使用)
141
141
 
@@ -180,11 +180,11 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
180
180
 
181
181
  异步模式引入的几条纪律,内嵌在工具提示词和实现中,主 agent 自动遵守:
182
182
 
183
- - **取消来源区分**:`已取消` 有用户(`/subagent-cancel`)、主 agent(`subagent_cancel`)、会话关闭(`session_shutdown`)三种来源;用户取消**不得自动重试**,须先询问。
184
- - **防轮询**:结果以通知自动到达;`subagent_status` 只用于确认在途(如 `/tree` 回退后),不带耗时、不鼓励频繁调用。
185
- - **防滥用取消**:`subagent_cancel` 内嵌提示词——仅当任务明显错误或不再需要时取消,勿因耗时长而取消(后台任务本就预期长时间运行)。
183
+ - **取消来源区分**:`已取消` 有用户(`/subagent-cancel`)、主 agent(`subagent` 工具 `action="cancel"`)、会话关闭(`session_shutdown`)三种来源;用户取消**不得自动重试**,须先询问。
184
+ - **防轮询**:结果以通知自动到达;`action="status"` 只用于确认在途(如 `/tree` 回退后),不带耗时、不鼓励频繁调用。
185
+ - **防滥用取消**:`action="cancel"` 内嵌提示词——仅当任务明显错误或不再需要时取消,勿因耗时长而取消(后台任务本就预期长时间运行)。
186
186
  - **资源冲突纪律**:并行派发多个任务前,考虑它们是否会改同一批文件或代码区域;冲突时串行派发或先问用户。
187
- - **递归委派完全禁止**:子 agent(深度 ≥ 1)不可再派发 `subagent`,深度限制为 1。
187
+ - **子 agent 不可调用 subagent 工具**:子 agent(深度 ≥ 1)不可调用任何 `subagent` action(含 `action="status"` / `action="cancel"`),深度限制为 1。
188
188
  - **TUI 异步 / 非 TUI 同步降级**:只在 TUI 模式走异步路径;print/json 等非 TUI 模式降级为同步阻塞。
189
189
 
190
190
  ---
@@ -273,7 +273,7 @@ alias pp='pi --tools read,grep,find,ls,subagent --no-skills --append-system-prom
273
273
 
274
274
  ## 进阶用法
275
275
 
276
- 手写 `subagent` 调用、复用 `sessionId`、信封与在途任务块细节、`subagent_status` 查询、取消任务、环境变量等见 [ADVANCED.md](ADVANCED.md)。
276
+ 手写 `subagent` 调用、复用 `sessionId`、信封与在途任务块细节、`action="status"` 查询、`action="cancel"` 取消任务、环境变量等见 [ADVANCED.md](ADVANCED.md)。
277
277
 
278
278
  ---
279
279
 
@@ -16,9 +16,10 @@
16
16
 
17
17
  - `read` — 读文件
18
18
  - `grep` / `find` / `ls` — 搜索代码、浏览项目结构
19
- - `subagent` — 委派任务给子 agent
20
- - `subagent_status` 查询仍在运行的后台任务(在途任务)
21
- - `subagent_cancel` — 取消错误或不再需要的后台任务
19
+ - `subagent` — 单入口子 agent 工具,通过 `action` 参数区分操作:
20
+ - `action="dispatch"`(默认)— 委派任务给子 agent
21
+ - `action="status"` — 查询仍在运行的后台任务(在途任务)
22
+ - `action="cancel"` — 取消错误或不再需要的后台任务
22
23
  - 规划与决策 — 理解需求、拆分任务步骤、整合结果
23
24
 
24
25
  仅此而已。你无权执行写入或命令类操作。
@@ -29,21 +30,21 @@
29
30
  2. **依赖驱动派发** — 无依赖的任务可并行派出。有依赖的必须等对应 `[subagent-result]` 通知到达后再派。
30
31
  3. **派出后继续工作** — `subagent` 返回的只是派发回执(含 `taskId`),不是结果。派出后继续做不依赖该结果的工作,或结束回合。严禁轮询、严禁臆造结果。
31
32
  4. **识别系统通知** — 以 `[subagent-result]` 开头的消息是系统通知(子 agent 结果),不是用户请求。收到后关联到当初派发的任务进行处理。
32
- 5. **通知先看在途任务块** — 每条 `[subagent-result]` 通知的元信息区带"在途任务"列表(剩余仍在运行的任务,不含本任务)。收到后先看剩余在途数:**不为 0 时还有任务未返回,不要向用户汇报"全部完成"**。需要确认在途状态(如 `/tree` 回退后不确定还剩哪些任务)时,用 `subagent_status` 工具查询。
33
+ 5. **通知先看在途任务块** — 每条 `[subagent-result]` 通知的元信息区带"在途任务"列表(剩余仍在运行的任务,不含本任务)。收到后先看剩余在途数:**不为 0 时还有任务未返回,不要向用户汇报"全部完成"**。需要确认在途状态(如 `/tree` 回退后不确定还剩哪些任务)时,用 `subagent` 工具(`action="status"`)查询。
33
34
  6. **已取消通知的处理** — 收到状态为"已取消"的 `[subagent-result]` 通知时,根据来源区分处理:
34
- - 正文注明用户通过 `/subagent-cancel` 取消 → 用户主动操作,**不得自动重新派发**。如需重新派发,先询问用户。
35
- - 正文注明主 agent 通过 `subagent_cancel` 取消 → 自身决策,不应在无新信息时重新派发。
36
- - 正文注明会话关闭(session_shutdown)终止 → 可在会话恢复后视情况重新派发。
35
+ - 正文注明用户通过 `/subagent-cancel` 取消 → 用户主动操作,**不得自动重新派发**。如需重新派发,先询问用户。
36
+ - 正文注明主 agent 通过 `subagent` 工具(`action="cancel"`)取消 → 自身决策,不应在无新信息时重新派发。
37
+ - 正文注明会话关闭(session_shutdown)终止 → 可在会话恢复后视情况重新派发。
37
38
 
38
- 7. **`subagent_cancel` 使用纪律** — 你可以使用 `subagent_cancel` 工具纠正错误委派(如委派了错误的 agent、任务描述有误)或取消不再需要的任务。**不要因等待时间长而取消**——后台子 agent 本就预期长时间运行。取消的判据是"这个任务不该继续",不是"等太久了"。
39
+ 7. **`action="cancel"` 使用纪律** — 你可以使用 `subagent` 工具(`action="cancel"`,参数 `taskId`)纠正错误委派(如委派了错误的 agent、任务描述有误)或取消不再需要的任务。**不要因等待时间长而取消**——后台子 agent 本就预期长时间运行。取消的判据是"这个任务不该继续",不是"等太久了"。
39
40
 
40
41
  8. **用标准任务格式** — 每次委派必须包含以下结构:
41
42
 
42
- - **背景** — 任务来源、已完成的上下文
43
- - **输入** — 相关文件路径、数据
44
- - **要求** — 明确的任务清单,可逐项检查
45
- - **输出格式** — 期望的返回结构
46
- - **验收标准** — 如何判断完成(必须含验证命令及输出)
43
+ - **背景** — 任务来源、已完成的上下文
44
+ - **输入** — 相关文件路径、数据
45
+ - **要求** — 明确的任务清单,可逐项检查
46
+ - **输出格式** — 期望的返回结构
47
+ - **验收标准** — 如何判断完成(必须含验证命令及输出)
47
48
 
48
49
  ### 调用示例
49
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolido/async-subagent-isolation",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A pi extension that asynchronously delegates tasks to specialized subagents running in isolated pi processes.",
5
5
  "license": "MIT",
6
6
  "author": "Wolido",
package/src/index.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  getAgentDir,
28
28
  parseFrontmatter,
29
29
  } from "@earendil-works/pi-coding-agent";
30
- import { Box, Container, Key, Markdown, matchesKey, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
30
+ import { Box, Container, Key, Markdown, matchesKey, Spacer, Text, truncateToWidth, visibleWidth, sliceByColumn } from "@earendil-works/pi-tui";
31
31
  import { Type } from "typebox";
32
32
 
33
33
  // ===== UUID v7 helper =====
@@ -545,6 +545,10 @@ interface AgentProgress {
545
545
  const PROGRESS_WIDGET_KEY = "async-subagent-isolation-progress";
546
546
  const MAX_WIDGET_LINES = 20;
547
547
  const MAX_RECENT_TOOLS = 3;
548
+ // Cap for the dynamic name column: wide enough for typical agent names
549
+ // (existing tests require a 20-char name to render in full), small enough
550
+ // to leave room for the phase/elapsed columns on narrow terminals.
551
+ const MAX_NAME_WIDTH = 30;
548
552
 
549
553
  /** Plain-text one-line summary of a tool call for the progress widget (no theme colors). */
550
554
  function summarizeToolCall(toolName: string, args: Record<string, any>): string {
@@ -561,6 +565,22 @@ function getRecentToolSummaries(messages: Message[]): string[] {
561
565
  .map((item) => summarizeToolCall(item.name, item.args));
562
566
  }
563
567
 
568
+ /**
569
+ * Fit an agent name into a fixed-width column: pad short names with spaces,
570
+ * truncate over-long names with a "..." suffix. Unlike truncateToWidth this
571
+ * never injects ANSI reset codes around the ellipsis (agent names carry no
572
+ * styling), so every row keeps the phase/elapsed columns at the same raw
573
+ * string offset as well as the same display column. Callers must pass
574
+ * width >= 4 so the ellipsis fits.
575
+ */
576
+ function fitNameColumn(name: string, width: number): string {
577
+ const nameWidth = visibleWidth(name);
578
+ if (nameWidth <= width) return name + " ".repeat(width - nameWidth);
579
+ // strict: never keep a wide char that would straddle the cut boundary
580
+ const cut = sliceByColumn(name, 0, width - 3, true);
581
+ return cut + "..." + " ".repeat(Math.max(0, width - 3 - visibleWidth(cut)));
582
+ }
583
+
564
584
  export function formatElapsed(startedAt: number): string {
565
585
  const totalSec = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
566
586
  const mm = String(Math.floor(totalSec / 60)).padStart(2, "0");
@@ -622,6 +642,9 @@ export class SubagentProgressManager {
622
642
  const maxAgentRows = Math.max(1, Math.min(process.stdout.rows || MAX_WIDGET_LINES, MAX_WIDGET_LINES));
623
643
  const visible = sorted.slice(-maxAgentRows);
624
644
  const total = sorted.length;
645
+ // Name column width adapts to the longest visible agent name (capped),
646
+ // so the phase and elapsed columns start at the same offset on every row.
647
+ const nameColWidth = Math.min(MAX_NAME_WIDTH, Math.max(...visible.map((a) => visibleWidth(a.name))));
625
648
  // Note: the factory closes over `theme`; between a theme change and the next
626
649
  // refresh (≤1s) the widget may briefly use the stale theme. The 1Hz timer
627
650
  // replaces the factory on every tick, so this self-heals.
@@ -640,7 +663,7 @@ export class SubagentProgressManager {
640
663
  return borderColor("─") + theme.fg("accent", label) + borderColor("─".repeat(width - labelWidth - 1));
641
664
  };
642
665
  const renderRow = (a: AgentProgress, width: number): string => {
643
- const nameCol = a.name;
666
+ const nameCol = fitNameColumn(a.name, nameColWidth);
644
667
  const phaseCol = truncateToWidth(formatPhase(a.phase), 20, "...", true);
645
668
  const toolHint = a.recentTools.length > 0 ? ` → ${a.recentTools[a.recentTools.length - 1]}` : "";
646
669
  const line =
@@ -1424,7 +1447,7 @@ export interface AsyncSubagentTask {
1424
1447
  status: "running" | "cancelled" | "killed_on_shutdown";
1425
1448
  /**
1426
1449
  * Who cancelled the task: "user" via /subagent-cancel, "agent" via the
1427
- * subagent_cancel tool. Set by cancelTask so the result envelope can name
1450
+ * subagent tool's action="cancel". Set by cancelTask so the result envelope can name
1428
1451
  * the cancel's origin; undefined for non-cancelled endings.
1429
1452
  */
1430
1453
  cancelledBy?: "user" | "agent";
@@ -1447,7 +1470,7 @@ export const taskRegistry = new Map<string, AsyncSubagentTask>();
1447
1470
  * Cancel a running async subagent task: mark it cancelled, record who
1448
1471
  * cancelled it (for the envelope), and fire its abort controller — reusing
1449
1472
  * the SIGTERM -> 5s -> SIGKILL cascade in runSingleAgent. Shared by the
1450
- * /subagent-cancel command and the subagent_cancel tool. Returns false when
1473
+ * /subagent-cancel command and the subagent tool's action="cancel". Returns false when
1451
1474
  * no running task with that id exists.
1452
1475
  */
1453
1476
  function cancelTask(taskId: string, cancelledBy: "user" | "agent"): boolean {
@@ -1467,8 +1490,8 @@ export function truncateTaskDescription(task: string, maxLen = 200): string {
1467
1490
 
1468
1491
  /**
1469
1492
  * Format the in-flight task list (status === "running") shared by the
1470
- * subagent_status tool, the result envelope's 在途 block, and the
1471
- * subagent_cancel receipt. Deliberately carries no elapsed time: the list
1493
+ * action="status" branch, the result envelope's 在途 block, and the
1494
+ * action="cancel" receipt. Deliberately carries no elapsed time: the list
1472
1495
  * answers "what is still running", not "how long has it run".
1473
1496
  */
1474
1497
  export function formatActiveTasks(): string {
@@ -1520,7 +1543,7 @@ const DETAILS_OUTPUT_MAX_CHARS = 16 * 1024;
1520
1543
  */
1521
1544
  function abortedFallbackBody(stopReason?: string, cancelledBy?: "user" | "agent"): string {
1522
1545
  if (stopReason === "killed_on_shutdown") return "任务因会话关闭被终止(session_shutdown)。";
1523
- if (cancelledBy === "agent") return "该任务已由主 agent 通过 subagent_cancel 工具取消。";
1546
+ if (cancelledBy === "agent") return "该任务已由主 agent 通过 subagent 工具(action=cancel)取消。";
1524
1547
  return "该任务已由用户通过 /subagent-cancel 取消,属用户主动操作。请勿自动重新派发;如需重新派发,先询问用户。";
1525
1548
  }
1526
1549
 
@@ -1645,9 +1668,24 @@ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
1645
1668
  default: "both",
1646
1669
  });
1647
1670
 
1671
+ // Union-of-literals (anyOf + const) rather than StringEnum so the emitted
1672
+ // JSON Schema enumerates each action as its own const branch.
1673
+ const SubagentActionSchema = Type.Union(
1674
+ [Type.Literal("dispatch"), Type.Literal("status"), Type.Literal("cancel")],
1675
+ {
1676
+ description:
1677
+ 'Action to perform. "dispatch" (default): delegate the task to a subagent. "status": list in-flight background tasks. "cancel": cancel a running background task by taskId.',
1678
+ default: "dispatch",
1679
+ },
1680
+ );
1681
+
1648
1682
  const SubagentParams = Type.Object({
1649
- agent: Type.String({ description: "Name of the agent to invoke" }),
1650
- task: Type.String({ description: "Task to delegate. Must be non-empty and include background, input, requirements, output format, and acceptance criteria." }),
1683
+ action: Type.Optional(SubagentActionSchema),
1684
+ agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (required for action=dispatch)" })),
1685
+ task: Type.Optional(Type.String({ description: "Task to delegate (required for action=dispatch). Must be non-empty and include background, input, requirements, output format, and acceptance criteria." })),
1686
+ taskId: Type.Optional(Type.String({
1687
+ description: "taskId of the running background subagent task to cancel (required for action=cancel; from the dispatch receipt).",
1688
+ })),
1651
1689
  sessionId: Type.Optional(Type.String({
1652
1690
  pattern: "^[A-Za-z0-9_.-]+$",
1653
1691
  description: "Optional session ID to reuse; a new UUID v7 is generated if omitted. Allowed characters: letters, digits, underscore, dot, and hyphen.",
@@ -1666,17 +1704,26 @@ export default function (pi: ExtensionAPI) {
1666
1704
  description: [
1667
1705
  "Delegate a task to a specialized subagent with isolated context.",
1668
1706
  "",
1707
+ "ACTIONS (action parameter, default \"dispatch\"):",
1708
+ "- dispatch: delegate the task (async in TUI mode, blocking otherwise).",
1709
+ "- status: list in-flight background tasks (taskId, agent, task description).",
1710
+ "- cancel: cancel a running background task by taskId.",
1711
+ "",
1669
1712
  "ASYNC (TUI mode): returns immediately with a dispatch receipt (taskId + session id).",
1670
1713
  "The result arrives later as a system notification message prefixed with",
1671
1714
  "[subagent-result] — that is a system notification, NOT a user request.",
1672
1715
  "- Do NOT treat the receipt as the result. Do NOT fabricate results.",
1673
1716
  "- Do NOT poll for results; they arrive automatically. To confirm which",
1674
- " tasks are still in flight (e.g. after a /tree rewind), use the",
1675
- " subagent_status tool.",
1717
+ " tasks are still in flight (e.g. after a /tree rewind), use action=\"status\".",
1676
1718
  "- Continue with independent work, or end the turn. Process the result when",
1677
1719
  " the [subagent-result] notification arrives. Reuse the session id from the",
1678
1720
  " receipt to continue the same task later.",
1679
1721
  "",
1722
+ "CANCEL DISCIPLINE: cancel a task (action=\"cancel\") only when it is clearly",
1723
+ "wrong (错误) or no longer needed (不再需要). Do NOT cancel just because it is",
1724
+ "taking a long time — background subagents are expected to run long; be patient",
1725
+ "(耐心等待) and let the [subagent-result] notification arrive.",
1726
+ "",
1680
1727
  "SYNC (non-TUI modes): waits for the subagent to finish and returns the full",
1681
1728
  "result directly (no notification follows).",
1682
1729
  "",
@@ -1689,26 +1736,74 @@ export default function (pi: ExtensionAPI) {
1689
1736
  "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 for status.",
1690
1737
  "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.",
1691
1738
  "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.",
1692
- "subagent: A [subagent-result] notification with status 已取消 (cancelled) can come from the user (/subagent-cancel) or from you (subagent_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.",
1739
+ "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.",
1693
1740
  "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.",
1694
1741
  ],
1695
1742
  parameters: SubagentParams,
1696
1743
 
1697
1744
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1698
- const currentDepth = parseEnvInt(process.env.PI_SUBAGENT_DEPTH, 0);
1745
+ // Single-entry action dispatch (default "dispatch").
1746
+ const action = (params.action as string | undefined) ?? "dispatch";
1699
1747
 
1700
- // Recursive delegation is blocked entirely: a subagent (depth >= 1)
1701
- // can never spawn another subagent.
1748
+ // Depth gate runs BEFORE any action dispatch: a subagent (depth >= 1)
1749
+ // is blocked from every action — dispatch spawns a nested subagent, and
1750
+ // status/cancel would let it observe or kill the parent's in-flight
1751
+ // tasks. The tool surface simply does not exist inside a subagent.
1752
+ const currentDepth = parseEnvInt(process.env.PI_SUBAGENT_DEPTH, 0);
1702
1753
  if (currentDepth >= MAX_SUBAGENT_DEPTH) {
1703
1754
  const agentName = process.env.PI_CURRENT_AGENT_NAME || "current agent";
1704
1755
  return {
1705
1756
  content: [{
1706
1757
  type: "text",
1707
- text: `Subagent delegation is blocked: depth limit reached (depth: ${currentDepth}, max: ${MAX_SUBAGENT_DEPTH}). Agent \`${agentName}\` runs inside a subagent and recursive delegation is not allowed.`,
1758
+ text: `Subagent tool is blocked: depth limit reached (depth: ${currentDepth}, max: ${MAX_SUBAGENT_DEPTH}). Agent \`${agentName}\` runs inside a subagent and cannot invoke subagent actions (dispatch/status/cancel).`,
1708
1759
  }],
1709
1760
  details: {
1710
1761
  mode: "single",
1711
- agentScope: params.agentScope ?? "both",
1762
+ agentScope: (params.agentScope ?? "both") as AgentScope,
1763
+ projectAgentsDir: null,
1764
+ results: [],
1765
+ } as SubagentDetails,
1766
+ isError: true,
1767
+ };
1768
+ }
1769
+
1770
+ if (action === "status") {
1771
+ return {
1772
+ content: [{ type: "text", text: formatActiveTasks() }],
1773
+ details: { activeTasks: [...taskRegistry.values()].filter((t) => t.status === "running").map((t) => t.taskId) },
1774
+ };
1775
+ }
1776
+
1777
+ if (action === "cancel") {
1778
+ const taskId = typeof params.taskId === "string" ? params.taskId.trim() : "";
1779
+ if (!taskId) {
1780
+ return {
1781
+ content: [{ type: "text", text: 'Missing or empty required parameter: "taskId" (taskId 必填,不能为空).' }],
1782
+ details: { taskId: "", cancelled: false },
1783
+ isError: true,
1784
+ };
1785
+ }
1786
+ // Only registry (async/TUI) tasks are cancellable; sync-mode tasks are
1787
+ // awaited inline and never enter the registry.
1788
+ if (!cancelTask(taskId, "agent")) {
1789
+ return {
1790
+ content: [{ type: "text", text: `无此运行中任务: ${taskId} (no running subagent task with this id).` }],
1791
+ details: { taskId, cancelled: false },
1792
+ isError: true,
1793
+ };
1794
+ }
1795
+ return {
1796
+ content: [{ type: "text", text: `已发送取消请求: ${taskId} (cancel request sent); 结果稍后以 [subagent-result] 通知返回。\n${formatActiveTasks()}` }],
1797
+ details: { taskId, cancelled: true },
1798
+ };
1799
+ }
1800
+
1801
+ if (action !== "dispatch") {
1802
+ return {
1803
+ content: [{ type: "text", text: `Invalid action: "${action}". Must be one of "dispatch" (default), "status", "cancel".` }],
1804
+ details: {
1805
+ mode: "single",
1806
+ agentScope: (params.agentScope ?? "both") as AgentScope,
1712
1807
  projectAgentsDir: null,
1713
1808
  results: [],
1714
1809
  } as SubagentDetails,
@@ -1826,7 +1921,7 @@ export default function (pi: ExtensionAPI) {
1826
1921
  };
1827
1922
  }
1828
1923
 
1829
- progressManager.register(ctx, effectiveSessionId, params.agent);
1924
+ progressManager.register(ctx, effectiveSessionId, agentName);
1830
1925
 
1831
1926
  // TUI mode: dispatch asynchronously. execute() returns a receipt
1832
1927
  // immediately; the finished result is pushed later as a
@@ -1835,7 +1930,7 @@ export default function (pi: ExtensionAPI) {
1835
1930
  if (ctx.mode === "tui") {
1836
1931
  const taskRecord: AsyncSubagentTask = {
1837
1932
  taskId: effectiveSessionId,
1838
- agentName: params.agent,
1933
+ agentName,
1839
1934
  task,
1840
1935
  startedAt: Date.now(),
1841
1936
  // Per-task controller: the turn-level `signal` fires when the
@@ -1848,7 +1943,7 @@ export default function (pi: ExtensionAPI) {
1848
1943
  runSingleAgent(
1849
1944
  ctx.cwd,
1850
1945
  agents,
1851
- params.agent,
1946
+ agentName,
1852
1947
  task,
1853
1948
  params.cwd,
1854
1949
  undefined,
@@ -1878,7 +1973,7 @@ export default function (pi: ExtensionAPI) {
1878
1973
  (err) => completeAsyncTask(pi, taskRecord, null, err),
1879
1974
  );
1880
1975
  return {
1881
- content: [{ type: "text", text: buildDispatchReceipt(params.agent, effectiveSessionId) }],
1976
+ content: [{ type: "text", text: buildDispatchReceipt(agentName, effectiveSessionId) }],
1882
1977
  details: makeDetails([]),
1883
1978
  };
1884
1979
  }
@@ -1887,7 +1982,7 @@ export default function (pi: ExtensionAPI) {
1887
1982
  const result = await runSingleAgent(
1888
1983
  ctx.cwd,
1889
1984
  agents,
1890
- params.agent,
1985
+ agentName,
1891
1986
  task,
1892
1987
  params.cwd,
1893
1988
  undefined,
@@ -1936,8 +2031,11 @@ export default function (pi: ExtensionAPI) {
1936
2031
 
1937
2032
  renderResult(result, { expanded }, theme, context) {
1938
2033
  const details = result.details as SubagentDetails | undefined;
1939
- if (!details || details.results.length === 0) {
1940
- return new Text(result.content[0]?.type === "text" ? result.content[0].text : "(no output)", 0, 0);
2034
+ // status/cancel receipts carry no `results` array (activeTasks /
2035
+ // taskId+cancelled instead) fall back to the plain-text content
2036
+ // instead of throwing on details.results.length.
2037
+ if (!details || !Array.isArray(details.results) || details.results.length === 0) {
2038
+ return new Text(result.content?.[0]?.type === "text" ? result.content[0].text : "(no output)", 0, 0);
1941
2039
  }
1942
2040
 
1943
2041
  const mdTheme = getMarkdownTheme();
@@ -2034,80 +2132,6 @@ export default function (pi: ExtensionAPI) {
2034
2132
  }
2035
2133
  });
2036
2134
 
2037
- // Read-only in-flight query. Results still arrive automatically as
2038
- // [subagent-result] notifications; this tool exists only to confirm which
2039
- // tasks are still running (e.g. after a /tree rewind wiped the receipts).
2040
- pi.registerTool({
2041
- name: "subagent_status",
2042
- label: "Subagent Status",
2043
- description: [
2044
- "List currently running background subagent tasks (在途任务: taskId、agent、任务描述).",
2045
- "Results arrive automatically as [subagent-result] notifications — do NOT use",
2046
- "this tool to poll for completion. Use it only to confirm which tasks are still",
2047
- "in flight (e.g. after a /tree rewind), or to pick a taskId for subagent_cancel.",
2048
- ].join("\n"),
2049
- promptSnippet:
2050
- "List in-flight background subagent tasks (not for polling — results arrive as [subagent-result] notifications).",
2051
- parameters: Type.Object({}),
2052
-
2053
- async execute() {
2054
- return {
2055
- content: [{ type: "text", text: formatActiveTasks() }],
2056
- details: { activeTasks: [...taskRegistry.values()].filter((t) => t.status === "running").map((t) => t.taskId) },
2057
- };
2058
- },
2059
- });
2060
-
2061
- // Cancellation has two paths sharing cancelTask (which reuses the SIGTERM
2062
- // -> 5s -> SIGKILL abort cascade and records who cancelled on the task so
2063
- // the result envelope can name the origin): the main agent's subagent_cancel
2064
- // tool below, and the user's /subagent-cancel command.
2065
- pi.registerTool({
2066
- name: "subagent_cancel",
2067
- label: "Subagent Cancel",
2068
- description: [
2069
- "Cancel a running background subagent task (取消一个仍在运行的后台 subagent 任务) by taskId.",
2070
- "Use only when the task is clearly wrong (错误) or no longer needed (不再需要).",
2071
- "Do NOT cancel just because it is taking a long time — background subagents are",
2072
- "expected to run long; be patient (耐心等待) and let the [subagent-result]",
2073
- "notification arrive.",
2074
- ].join("\n"),
2075
- promptSnippet:
2076
- "Cancel a running background subagent task by taskId (only when it is clearly wrong or no longer needed).",
2077
- promptGuidelines: [
2078
- "subagent_cancel: Cancel a background subagent task only when it is clearly wrong or no longer needed — never cancel merely because it is taking long; the cancellation arrives later as a [subagent-result] notification with status 已取消 (cancelled).",
2079
- ],
2080
- parameters: Type.Object({
2081
- taskId: Type.String({
2082
- description: "taskId of the running background subagent task to cancel (from the subagent dispatch receipt).",
2083
- }),
2084
- }),
2085
-
2086
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
2087
- const taskId = typeof params.taskId === "string" ? params.taskId.trim() : "";
2088
- if (!taskId) {
2089
- return {
2090
- content: [{ type: "text", text: 'Missing or empty required parameter: "taskId" (taskId 必填,不能为空).' }],
2091
- details: { taskId: "", cancelled: false },
2092
- isError: true,
2093
- };
2094
- }
2095
- // Only registry (async/TUI) tasks are cancellable; sync-mode tasks are
2096
- // awaited inline and never enter the registry.
2097
- if (!cancelTask(taskId, "agent")) {
2098
- return {
2099
- content: [{ type: "text", text: `无此运行中任务: ${taskId} (no running subagent task with this id).` }],
2100
- details: { taskId, cancelled: false },
2101
- isError: true,
2102
- };
2103
- }
2104
- return {
2105
- content: [{ type: "text", text: `已发送取消请求: ${taskId} (cancel request sent); 结果稍后以 [subagent-result] 通知返回。\n${formatActiveTasks()}` }],
2106
- details: { taskId, cancelled: true },
2107
- };
2108
- },
2109
- });
2110
-
2111
2135
  // /subagent-cancel <taskId> is the user's cancel path.
2112
2136
  // (Optional-call guards keep minimal mock `pi` objects in tests working.)
2113
2137
  pi.registerCommand?.("subagent-cancel", {