@wolido/async-subagent-isolation 1.1.0 → 1.2.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 +6 -23
- package/ADVANCED.md +6 -23
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/examples/README.en.md +2 -0
- package/examples/README.md +2 -0
- package/examples/pi/agent/master.md +1 -2
- package/package.json +1 -1
- package/src/index.ts +44 -33
package/ADVANCED.en.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
This document covers low-level invocation, configuration fields, and environment variables for `async-subagent-isolation`. Most users can follow the natural-language Quick Start in the main README; refer to this file only when you need to construct `subagent` calls manually, reuse an isolated session, or tune runtime parameters.
|
|
6
6
|
|
|
7
|
+
> **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.
|
|
8
|
+
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## Agent definition format
|
|
@@ -114,7 +116,7 @@ Key points:
|
|
|
114
116
|
- **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
117
|
- **The receipt is not the result.** Do not fabricate results.
|
|
116
118
|
- **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;
|
|
119
|
+
- **Do not poll.** Results arrive automatically as `[subagent-result]` notifications; in-flight task information is provided directly by the notification envelope's in-flight block. `action="status"` was removed as a cleanup in v1.2.0.
|
|
118
120
|
|
|
119
121
|
### [subagent-result] envelope format
|
|
120
122
|
|
|
@@ -144,7 +146,7 @@ Status enumeration: **成功** (success, exit=0) / **失败** (failure, exit≠0
|
|
|
144
146
|
|
|
145
147
|
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
148
|
|
|
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
|
|
149
|
+
**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** (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
150
|
|
|
149
151
|
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
152
|
|
|
@@ -166,25 +168,6 @@ While subagents run, a progress widget appears above the TUI editor, listing all
|
|
|
166
168
|
|
|
167
169
|
The taskId in the widget row can be copied for `/subagent-result` (view full result) or `/subagent-cancel` (cancel the task).
|
|
168
170
|
|
|
169
|
-
### `action="status"` (in-flight query)
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
```
|
|
174
|
-
在途任务: 2
|
|
175
|
-
- 01912345-6789-7abc-8def-0123456789ab (coder): 将认证中间件重构为使用 async/await。
|
|
176
|
-
- 01912345-aaaa-7bbb-8ccc-0123456789ab (writer): 更新 README。
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
With no in-flight tasks it returns `当前无在途任务。`. Each line carries the taskId, agent name, and task description, with **no elapsed time**.
|
|
180
|
-
|
|
181
|
-
Use cases:
|
|
182
|
-
|
|
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 `action="cancel"`.
|
|
185
|
-
|
|
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
|
-
|
|
188
171
|
### Cancelling background tasks
|
|
189
172
|
|
|
190
173
|
Cancelling a running background subagent task has two paths, both sharing the same underlying cancel flow (SIGTERM → 5s → SIGKILL cascade, followed by a `[subagent-result]` notification).
|
|
@@ -209,7 +192,7 @@ Takes no arguments. Unlike `/subagent-cancel`, which cancels a single task by ta
|
|
|
209
192
|
|
|
210
193
|
**Path 2: Main agent `subagent` tool with `action="cancel"`**
|
|
211
194
|
|
|
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 `
|
|
195
|
+
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.
|
|
213
196
|
|
|
214
197
|
**Usage discipline:** The main agent should only use `action="cancel"` when:
|
|
215
198
|
- The task is clearly wrong (wrong agent, incorrect task description, etc.).
|
|
@@ -292,7 +275,7 @@ These variables are propagated into every subagent process automatically:
|
|
|
292
275
|
|
|
293
276
|
| Variable | Default | Description |
|
|
294
277
|
|----------|---------|-------------|
|
|
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="
|
|
278
|
+
| `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="cancel"`). |
|
|
296
279
|
| `PI_CURRENT_AGENT_NAME` | — | Name of the current agent, injected into every subagent process. |
|
|
297
280
|
| `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
281
|
| `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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# async-subagent-isolation 进阶参考
|
|
4
4
|
|
|
5
|
+
> **v1.2.0 提示**:`subagent` 工具的 `action="status"` 已作为 cleanup 移除。在途任务信息改由 `[subagent-result]` 通知信封的“在途任务”块提供,不再提供主动查询入口。
|
|
6
|
+
|
|
5
7
|
这里收录 `async-subagent-isolation` 的底层调用方式、配置字段和环境变量。普通用户按照主 README 的 Quick Start 用自然语言即可;只有当你需要手动构造 `subagent` 调用、复用隔离会话或调整运行参数时才需要查看本文档。
|
|
6
8
|
|
|
7
9
|
---
|
|
@@ -114,7 +116,7 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
|
|
|
114
116
|
- **回执为单行。** 异步语义引导(不臆造结果、不轮询、结果以 `[subagent-result]` 通知到达)已内嵌于 `subagent` 工具的 `description` / `promptGuidelines`,回执本身保持单行。
|
|
115
117
|
- **回执 ≠ 结果。** 不要臆造结果。
|
|
116
118
|
- **taskId = sessionId。** 回执中的 `taskId` 就是 session ID,可直接复用。
|
|
117
|
-
- **不要轮询。** 结果自动以 `[subagent-result]`
|
|
119
|
+
- **不要轮询。** 结果自动以 `[subagent-result]` 通知到达;在途任务信息由通知信封的“在途任务”块直接提供,`action="status"` 已在 v1.2.0 清理移除。
|
|
118
120
|
|
|
119
121
|
### [subagent-result] 信封格式
|
|
120
122
|
|
|
@@ -144,7 +146,7 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
|
|
|
144
146
|
|
|
145
147
|
主 agent 收到状态为"已取消"的通知时,应区分来源:用户主动取消**不得自动重试**,必须先询问用户;agent 取消是自身决策,不应在无新信息时重新派发;会话关闭终止可在会话恢复后视情况重新派发。
|
|
146
148
|
|
|
147
|
-
**在途任务块**:信封元信息区的"在途任务"
|
|
149
|
+
**在途任务块**:信封元信息区的"在途任务"列表列出**其余**仍在运行的后台任务(本任务在构建信封前已从注册表移除,故不包含自身),格式为 `在途任务: N` 加每行 `- taskId (agent名): 任务描述`,无在途任务时为"当前无在途任务。"列表**不含耗时**(回答"还有什么在跑",而非"跑了多久")。主 agent 据此知道还有几个任务没回来:剩余不为 0 时,不要向用户汇报"全部完成"。
|
|
148
150
|
|
|
149
151
|
结果全量进入 LLM 上下文(不截断)。`details` 携带结构化数据(taskId、agent、status、exitCode、stopReason、usage、sessionId、完整输出),不参与 LLM 上下文,供程序消费。
|
|
150
152
|
|
|
@@ -166,25 +168,6 @@ TUI 模式下 `subagent` 立即返回如下回执(不是结果!):
|
|
|
166
168
|
|
|
167
169
|
widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果或 `/subagent-cancel` 取消任务。
|
|
168
170
|
|
|
169
|
-
### `action="status"`(在途任务查询)
|
|
170
|
-
|
|
171
|
-
主 agent 可主动查询仍在运行的后台任务,调用 `subagent` 工具(`action="status"`,无需 agent/task 参数),返回:
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
在途任务: 2
|
|
175
|
-
- 01912345-6789-7abc-8def-0123456789ab (coder): 将认证中间件重构为使用 async/await。
|
|
176
|
-
- 01912345-aaaa-7bbb-8ccc-0123456789ab (writer): 更新 README。
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
无在途任务时返回 `当前无在途任务。`。每行含 taskId、agent 名和任务描述,**不含耗时**。
|
|
180
|
-
|
|
181
|
-
用途场景:
|
|
182
|
-
|
|
183
|
-
- `/tree` 回退后回执丢失,确认还有哪些任务在途。
|
|
184
|
-
- 不确定剩余任务时快速核对,或选取 taskId 用于 `action="cancel"`。
|
|
185
|
-
|
|
186
|
-
**不要用它轮询完成状态**:结果会自动以 `[subagent-result]` 通知到达,本工具只用于确认"还有什么在跑",不应频繁调用。
|
|
187
|
-
|
|
188
171
|
### 取消后台任务
|
|
189
172
|
|
|
190
173
|
取消运行中的后台子 agent 任务有两条路径,底层共享同一套取消流程(SIGTERM → 5s → SIGKILL 级联,最终推送 `[subagent-result]` 通知)。
|
|
@@ -209,7 +192,7 @@ widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果
|
|
|
209
192
|
|
|
210
193
|
**路径二:主 agent `subagent` 工具(`action="cancel"`)**
|
|
211
194
|
|
|
212
|
-
主 agent 可调用 `subagent` 工具(`action="cancel"`,参数 `taskId`)取消已派出的后台任务。取消来源标记为 `cancelledBy: "agent"
|
|
195
|
+
主 agent 可调用 `subagent` 工具(`action="cancel"`,参数 `taskId`)取消已派出的后台任务。取消来源标记为 `cancelledBy: "agent"`。取消成功后返回剩余在途任务列表(格式与信封的“在途任务”块一致),被取消任务的最终结果稍后以 `[subagent-result]` 通知返回。
|
|
213
196
|
|
|
214
197
|
**使用纪律:** 主 agent 仅在以下情况使用 `action="cancel"`:
|
|
215
198
|
- 任务明显错误(委派了错误的 agent、任务描述有误等)。
|
|
@@ -292,7 +275,7 @@ widget 行中的 taskId 可直接复制,用于 `/subagent-result` 查看结果
|
|
|
292
275
|
|
|
293
276
|
| 变量 | 默认值 | 说明 |
|
|
294
277
|
|------|--------|------|
|
|
295
|
-
| `PI_SUBAGENT_DEPTH` | `0` | 当前递归深度。每次嵌套调用自动递增。**深度限制为 1**——子 agent(depth ≥ 1)不可调用任何 `subagent` action(含 `action="
|
|
278
|
+
| `PI_SUBAGENT_DEPTH` | `0` | 当前递归深度。每次嵌套调用自动递增。**深度限制为 1**——子 agent(depth ≥ 1)不可调用任何 `subagent` action(含 `action="cancel"`)。 |
|
|
296
279
|
| `PI_CURRENT_AGENT_NAME` | — | 当前 agent 名称,注入每个子 agent 进程。 |
|
|
297
280
|
| `PI_SUBAGENT_ACTIVITY_TIMEOUT_MS` | `600000`(10 分钟) | stdout 和 stderr 均无输出(无活动)时的最大允许时间。 |
|
|
298
281
|
| `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
|
|
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 the progress widget, 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
|
|
|
@@ -71,6 +71,18 @@ Plain subagents split work. async-subagent-isolation splits everything.
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
+
## Uniqueness and significance
|
|
75
|
+
|
|
76
|
+
This project is built on two design decisions that support each other, and its significance comes from the two together.
|
|
77
|
+
|
|
78
|
+
**Async by default.** Dispatching is not handing over control — it's delivering a task: the call returns a receipt immediately, the task runs in an independent process in the background, and the result is pushed back as a `[subagent-result]` system notification. Async is the default semantics, not an optional switch — the main agent never blocks, can dispatch in parallel and keep planning, and the user always faces the dispatcher alone.
|
|
79
|
+
|
|
80
|
+
**Exclusive skill isolation.** A subagent's skills load from a whitelist: everything is off by default, and only individually listed skills can enter its context. Isolation happens at the process level, not the prompt level — each subagent is its own `pi` process, and none of the main agent's skills can get in. Isolation is therefore not an instruction but a structural fact: a subagent knows only what it is allowed to know, and its domain of focus is precisely controllable.
|
|
81
|
+
|
|
82
|
+
**Why it matters: context partitioning.** The main agent keeps only "what to do" and "what came back"; the subagent's long execution trail stays in its own process and session, never flowing back to the main agent. Context is cut into small slices, each handled by its own agent — the main agent stays clear-headed over the long run, and planning and review are never drowned in detail. Reliable division of labor is thus structure, not discipline: async and isolation are both defaults.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
74
86
|
## Async workflow
|
|
75
87
|
|
|
76
88
|
This is the biggest difference from the sync version, and the primary way to use it (TUI mode).
|
|
@@ -104,7 +116,7 @@ When the subagent finishes, its result is pushed as a **`[subagent-result]` syst
|
|
|
104
116
|
- If the main agent is **idle**, the notification triggers a new turn immediately.
|
|
105
117
|
- If the main agent is **busy**, it is queued and triggers a turn after the current one finishes.
|
|
106
118
|
|
|
107
|
-
Results arrive automatically — **no polling**.
|
|
119
|
+
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.
|
|
108
120
|
|
|
109
121
|
### 5. Read the full result (`/subagent-result`)
|
|
110
122
|
|
|
@@ -134,9 +146,10 @@ User runs /subagent-result <taskId> to read the full output
|
|
|
134
146
|
| Tool | Purpose | Key constraint |
|
|
135
147
|
|------|---------|----------------|
|
|
136
148
|
| `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
149
|
| `subagent` `action="cancel"` | Main agent cancels one in-flight task | Only when clearly wrong or no longer needed; never for being slow |
|
|
139
150
|
|
|
151
|
+
> **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
|
+
|
|
140
153
|
### Commands (for the user)
|
|
141
154
|
|
|
142
155
|
| Command | Purpose |
|
|
@@ -181,10 +194,10 @@ See [ADVANCED.en.md](ADVANCED.en.md) for the complete envelope format, status se
|
|
|
181
194
|
Async mode introduces a few rules, baked into the tool prompts and implementation, that the main agent follows automatically:
|
|
182
195
|
|
|
183
196
|
- **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
|
|
197
|
+
- **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.
|
|
185
198
|
- **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
199
|
- **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
|
-
- **Subagents cannot call the subagent tool**: a subagent (depth ≥ 1) can never call any `subagent` action (including `action="
|
|
200
|
+
- **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.
|
|
188
201
|
- **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
202
|
|
|
190
203
|
---
|
|
@@ -273,7 +286,7 @@ Put it in `~/.pi/agent/subagent-isolation.json` (user-level) or `.pi/subagent-is
|
|
|
273
286
|
|
|
274
287
|
## Advanced usage
|
|
275
288
|
|
|
276
|
-
Manual `subagent` calls, `sessionId` reuse, envelope and in-flight block details, `action="
|
|
289
|
+
Manual `subagent` calls, `sessionId` reuse, envelope and in-flight block details, `action="cancel"` cancellation, and environment variables are covered in [ADVANCED.en.md](ADVANCED.en.md).
|
|
277
290
|
|
|
278
291
|
---
|
|
279
292
|
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
更关键的是**派发之后的自由**。任务在后台跑的时候,你可以继续和主 agent 聊天:细化需求、调整规划、商量下一步,或提出新任务。主 agent 不必干等,可以继续规划,甚至并行派发更多任务。前台对话与后台工作并行推进。
|
|
49
49
|
|
|
50
|
-
最后是**结果回来再验收**。子 agent 完成,通知到达,主 agent
|
|
50
|
+
最后是**结果回来再验收**。子 agent 完成,通知到达,主 agent 处理并向你汇报。等待期间你可以随时查看进度 widget,但不必盯着。
|
|
51
51
|
|
|
52
52
|
一句话:同步版让你陷在"集群执行"的阻塞感里;异步版让你只面对调度者,后台工作与你自己的节奏并行。
|
|
53
53
|
|
|
@@ -71,6 +71,18 @@ async-subagent-isolation 做的是强制且完全的隔离:
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
+
## 独特性与意义
|
|
75
|
+
|
|
76
|
+
本项目由两条互为支撑的设计决策构成,意义也由它们共同兑现。
|
|
77
|
+
|
|
78
|
+
**默认异步。** 派发不是转交控制权,而是投递任务:调用即返回回执,任务在独立进程后台运行,结果由系统通知推回。异步是默认语义而非可选开关——主 agent 不被阻塞,可并行派发、继续规划,用户面对的始终只有调度者一个。
|
|
79
|
+
|
|
80
|
+
**排他性 skill 隔离。** 子 agent 的 skill 按白名单加载:全局一律关闭,只有逐条指定的 skill 能进入它的上下文。隔离发生在进程层而非提示词层——每个子 agent 是独立 pi 进程,主 agent 的 skill 一个都进不来。隔离因此不是叮嘱,而是构造事实:子 agent 只有被允许的知识,专注域精确可控。
|
|
81
|
+
|
|
82
|
+
**意义:上下文切割。** 主 agent 只保留"要做什么"与"结果是什么",子 agent 冗长的执行痕迹留在自己的进程与 session 里,不回流主 agent。上下文被切成小块、各管一段,主 agent 得以长期保持清醒,规划与验收不被细节淹没。可靠的分工由此是结构而非纪律:异步与隔离都是默认的。
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
74
86
|
## 异步工作流
|
|
75
87
|
|
|
76
88
|
这是与同步版最大的不同,也是核心使用方式(TUI 模式)。
|
|
@@ -104,7 +116,7 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
104
116
|
- 主 agent **空闲**时,通知直接触发新的对话回合,立即处理。
|
|
105
117
|
- 主 agent **忙碌**时,通知进入队列,当前回合结束后再触发。
|
|
106
118
|
|
|
107
|
-
|
|
119
|
+
结果自动到达,**无需轮询**。在途任务信息由 `[subagent-result]` 通知信封的“在途任务”块直接提供;`action="status"` 已在 v1.2.0 清理移除。
|
|
108
120
|
|
|
109
121
|
### 5. 查看全文(`/subagent-result`)
|
|
110
122
|
|
|
@@ -134,9 +146,10 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
134
146
|
| 工具 | 作用 | 关键约束 |
|
|
135
147
|
|------|------|----------|
|
|
136
148
|
| `subagent` | 单入口工具(`action` 参数);`action="dispatch"`(默认)异步派发任务(TUI 模式),非 TUI 自动降级同步 | 回执≠结果;结果以通知到达,勿轮询 |
|
|
137
|
-
| `subagent` `action="status"` | 查询在途任务(taskId、agent、任务描述,**无耗时**) | 仅确认"还有什么在跑",勿用它轮询完成 |
|
|
138
149
|
| `subagent` `action="cancel"` | 主 agent 取消单个在途任务 | 仅当任务明显错误或不再需要,勿因耗时久而取消 |
|
|
139
150
|
|
|
151
|
+
> **v1.2.0 提示**:`subagent` 工具的 `action="status"` 已作为 cleanup 移除。在途任务信息改由 `[subagent-result]` 通知信封的“在途任务”块提供,不再提供主动查询入口。
|
|
152
|
+
|
|
140
153
|
### 命令(用户使用)
|
|
141
154
|
|
|
142
155
|
| 命令 | 作用 |
|
|
@@ -181,10 +194,10 @@ TUI 模式下 `subagent` **立即返回派发回执**,不阻塞:
|
|
|
181
194
|
异步模式引入的几条纪律,内嵌在工具提示词和实现中,主 agent 自动遵守:
|
|
182
195
|
|
|
183
196
|
- **取消来源区分**:`已取消` 有用户(`/subagent-cancel`)、主 agent(`subagent` 工具 `action="cancel"`)、会话关闭(`session_shutdown`)三种来源;用户取消**不得自动重试**,须先询问。
|
|
184
|
-
-
|
|
197
|
+
- **防轮询**:结果以通知自动到达;在途任务信息由 `[subagent-result]` 通知信封直接提供,不要主动查询。
|
|
185
198
|
- **防滥用取消**:`action="cancel"` 内嵌提示词——仅当任务明显错误或不再需要时取消,勿因耗时长而取消(后台任务本就预期长时间运行)。
|
|
186
199
|
- **资源冲突纪律**:并行派发多个任务前,考虑它们是否会改同一批文件或代码区域;冲突时串行派发或先问用户。
|
|
187
|
-
- **子 agent 不可调用 subagent 工具**:子 agent(深度 ≥ 1)不可调用任何 `subagent` action(含 `action="
|
|
200
|
+
- **子 agent 不可调用 subagent 工具**:子 agent(深度 ≥ 1)不可调用任何 `subagent` action(含 `action="cancel"`),深度限制为 1。
|
|
188
201
|
- **TUI 异步 / 非 TUI 同步降级**:只在 TUI 模式走异步路径;print/json 等非 TUI 模式降级为同步阻塞。
|
|
189
202
|
|
|
190
203
|
---
|
|
@@ -273,7 +286,7 @@ alias pp='pi --tools read,grep,find,ls,subagent --no-skills --append-system-prom
|
|
|
273
286
|
|
|
274
287
|
## 进阶用法
|
|
275
288
|
|
|
276
|
-
手写 `subagent` 调用、复用 `sessionId`、信封与在途任务块细节、`action="
|
|
289
|
+
手写 `subagent` 调用、复用 `sessionId`、信封与在途任务块细节、`action="cancel"` 取消任务、环境变量等见 [ADVANCED.md](ADVANCED.md)。
|
|
277
290
|
|
|
278
291
|
---
|
|
279
292
|
|
package/examples/README.en.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
`examples/pi/` mirrors `~/.pi/agent/`. Copy it to your user directory so `pi` can discover these agents and skills.
|
|
6
6
|
|
|
7
7
|
> Install the extension first with `pi install npm:@wolido/async-subagent-isolation`.
|
|
8
|
+
>
|
|
9
|
+
> **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.
|
|
8
10
|
|
|
9
11
|
## Directory structure
|
|
10
12
|
|
package/examples/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
`examples/pi/` 是 `~/.pi/agent/` 的镜像。把它复制到用户目录后,`pi` 就能识别这些 agent 和 skill。
|
|
6
6
|
|
|
7
7
|
> 使用前先用 `pi install npm:@wolido/async-subagent-isolation` 安装扩展。
|
|
8
|
+
>
|
|
9
|
+
> **v1.2.0 提示**:`subagent` 工具的 `action="status"` 已作为 cleanup 移除。在途任务信息改由 `[subagent-result]` 通知信封的“在途任务”块提供。
|
|
8
10
|
|
|
9
11
|
## 目录结构
|
|
10
12
|
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
- `grep` / `find` / `ls` — 搜索代码、浏览项目结构
|
|
19
19
|
- `subagent` — 单入口子 agent 工具,通过 `action` 参数区分操作:
|
|
20
20
|
- `action="dispatch"`(默认)— 委派任务给子 agent
|
|
21
|
-
- `action="status"` — 查询仍在运行的后台任务(在途任务)
|
|
22
21
|
- `action="cancel"` — 取消错误或不再需要的后台任务
|
|
23
22
|
- 规划与决策 — 理解需求、拆分任务步骤、整合结果
|
|
24
23
|
|
|
@@ -30,7 +29,7 @@
|
|
|
30
29
|
2. **依赖驱动派发** — 无依赖的任务可并行派出。有依赖的必须等对应 `[subagent-result]` 通知到达后再派。
|
|
31
30
|
3. **派出后继续工作** — `subagent` 返回的只是派发回执(含 `taskId`),不是结果。派出后继续做不依赖该结果的工作,或结束回合。严禁轮询、严禁臆造结果。
|
|
32
31
|
4. **识别系统通知** — 以 `[subagent-result]` 开头的消息是系统通知(子 agent 结果),不是用户请求。收到后关联到当初派发的任务进行处理。
|
|
33
|
-
5. **通知先看在途任务块** — 每条 `[subagent-result]` 通知的元信息区带"在途任务"列表(剩余仍在运行的任务,不含本任务)。收到后先看剩余在途数:**不为 0 时还有任务未返回,不要向用户汇报"全部完成"
|
|
32
|
+
5. **通知先看在途任务块** — 每条 `[subagent-result]` 通知的元信息区带"在途任务"列表(剩余仍在运行的任务,不含本任务)。收到后先看剩余在途数:**不为 0 时还有任务未返回,不要向用户汇报"全部完成"**。主 agent 不主动查询后台;若上下文里该任务的 [subagent-result] 通知未到达,向用户报告该 taskId 并建议用户用 /subagent-cancel 或 /subagent-result 命令查看。
|
|
34
33
|
6. **已取消通知的处理** — 收到状态为"已取消"的 `[subagent-result]` 通知时,根据来源区分处理:
|
|
35
34
|
- 正文注明用户通过 `/subagent-cancel` 取消 → 用户主动操作,**不得自动重新派发**。如需重新派发,先询问用户。
|
|
36
35
|
- 正文注明主 agent 通过 `subagent` 工具(`action="cancel"`)取消 → 自身决策,不应在无新信息时重新派发。
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -94,7 +94,7 @@ function parseListField(value: unknown): string[] | undefined {
|
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
97
|
+
export function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
98
98
|
const agents: AgentConfig[] = [];
|
|
99
99
|
if (!fs.existsSync(dir)) return agents;
|
|
100
100
|
let entries: fs.Dirent[];
|
|
@@ -113,8 +113,23 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
113
113
|
} catch {
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
let frontmatter: Record<string, unknown>;
|
|
117
|
+
let body: string;
|
|
118
|
+
try {
|
|
119
|
+
({ frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122
|
+
console.warn(`[async-subagent-isolation] failed to parse frontmatter in ${entry.name}: ${msg.slice(0, 200)}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (typeof frontmatter.name !== "string" || frontmatter.name.trim() === "") {
|
|
126
|
+
console.warn(`[async-subagent-isolation] ${entry.name}: name must be a non-empty string, skipping.`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (typeof frontmatter.description !== "string" || frontmatter.description.trim() === "") {
|
|
130
|
+
console.warn(`[async-subagent-isolation] ${entry.name}: description must be a string (watch YAML flow objects like {foo:bar} or null/~), skipping.`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
118
133
|
const tools = parseListField(frontmatter.tools);
|
|
119
134
|
const hasSkills = "skills" in frontmatter;
|
|
120
135
|
const skills = hasSkills ? parseListField(frontmatter.skills) ?? [] : undefined;
|
|
@@ -242,7 +257,7 @@ export function loadModelOverrides(cwd: string): Record<string, ModelOverride> {
|
|
|
242
257
|
}
|
|
243
258
|
}
|
|
244
259
|
|
|
245
|
-
function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
260
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
246
261
|
const userDir = path.join(getAgentDir(), "agents");
|
|
247
262
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
248
263
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
@@ -877,13 +892,16 @@ function parseEnvInt(raw: string | undefined, fallback: number): number {
|
|
|
877
892
|
return Number.isNaN(parsed) ? fallback : parsed;
|
|
878
893
|
}
|
|
879
894
|
|
|
895
|
+
const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
896
|
+
|
|
880
897
|
/** Validate an explicit sessionId. Returns an error message, or null when valid. */
|
|
881
|
-
function validateSessionId(sessionId:
|
|
898
|
+
function validateSessionId(sessionId: unknown): string | null {
|
|
899
|
+
if (typeof sessionId !== "string") return `Invalid sessionId: must be a string, got ${typeof sessionId}`;
|
|
882
900
|
const trimmed = sessionId.trim();
|
|
883
901
|
if (trimmed === "") return "Invalid sessionId: must not be empty";
|
|
884
902
|
if (trimmed === "." || trimmed === "..") return `Invalid sessionId: "${trimmed}" is not allowed`;
|
|
885
|
-
if (
|
|
886
|
-
|
|
903
|
+
if (!UUID_V7_PATTERN.test(trimmed))
|
|
904
|
+
return "Invalid sessionId: expected a lowercase UUID v7 from a previous receipt. Only pass sessionId to resume (复用) an earlier taskId; omit it to generate a new one.";
|
|
887
905
|
return null;
|
|
888
906
|
}
|
|
889
907
|
|
|
@@ -1489,10 +1507,10 @@ export function truncateTaskDescription(task: string, maxLen = 200): string {
|
|
|
1489
1507
|
}
|
|
1490
1508
|
|
|
1491
1509
|
/**
|
|
1492
|
-
* Format the in-flight task list (status === "running") shared by the
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1510
|
+
* Format the in-flight task list (status === "running") shared by the result
|
|
1511
|
+
* envelope's 在途 block and the action="cancel" receipt. Deliberately carries
|
|
1512
|
+
* no elapsed time: the list answers "what is still running", not "how long has
|
|
1513
|
+
* it run".
|
|
1496
1514
|
*/
|
|
1497
1515
|
export function formatActiveTasks(): string {
|
|
1498
1516
|
const running = [...taskRegistry.values()].filter((t) => t.status === "running");
|
|
@@ -1671,10 +1689,10 @@ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
|
1671
1689
|
// Union-of-literals (anyOf + const) rather than StringEnum so the emitted
|
|
1672
1690
|
// JSON Schema enumerates each action as its own const branch.
|
|
1673
1691
|
const SubagentActionSchema = Type.Union(
|
|
1674
|
-
[Type.Literal("dispatch"), Type.Literal("
|
|
1692
|
+
[Type.Literal("dispatch"), Type.Literal("cancel")],
|
|
1675
1693
|
{
|
|
1676
1694
|
description:
|
|
1677
|
-
'Action to perform. "dispatch" (default): delegate the task to a subagent. "
|
|
1695
|
+
'Action to perform. "dispatch" (default): delegate the task to a subagent. "cancel": cancel a running background task by taskId.',
|
|
1678
1696
|
default: "dispatch",
|
|
1679
1697
|
},
|
|
1680
1698
|
);
|
|
@@ -1687,8 +1705,8 @@ const SubagentParams = Type.Object({
|
|
|
1687
1705
|
description: "taskId of the running background subagent task to cancel (required for action=cancel; from the dispatch receipt).",
|
|
1688
1706
|
})),
|
|
1689
1707
|
sessionId: Type.Optional(Type.String({
|
|
1690
|
-
pattern: "^[
|
|
1691
|
-
description: "
|
|
1708
|
+
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
|
|
1709
|
+
description: "仅用于复用此前 dispatch 回执返回的 UUID v7;省略则自动生成",
|
|
1692
1710
|
})),
|
|
1693
1711
|
agentScope: Type.Optional(AgentScopeSchema),
|
|
1694
1712
|
confirmProjectAgents: Type.Optional(
|
|
@@ -1706,15 +1724,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1706
1724
|
"",
|
|
1707
1725
|
"ACTIONS (action parameter, default \"dispatch\"):",
|
|
1708
1726
|
"- dispatch: delegate the task (async in TUI mode, blocking otherwise).",
|
|
1709
|
-
"- status: list in-flight background tasks (taskId, agent, task description).",
|
|
1710
1727
|
"- cancel: cancel a running background task by taskId.",
|
|
1728
|
+
"- 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.",
|
|
1711
1729
|
"",
|
|
1712
1730
|
"ASYNC (TUI mode): returns immediately with a dispatch receipt (taskId + session id).",
|
|
1713
1731
|
"The result arrives later as a system notification message prefixed with",
|
|
1714
1732
|
"[subagent-result] — that is a system notification, NOT a user request.",
|
|
1715
1733
|
"- Do NOT treat the receipt as the result. Do NOT fabricate results.",
|
|
1716
|
-
"- Do NOT poll for results; they arrive automatically.
|
|
1717
|
-
" tasks are still in flight (e.g. after a /tree rewind), use action=\"status\".",
|
|
1734
|
+
"- Do NOT poll for results; they arrive automatically.",
|
|
1718
1735
|
"- Continue with independent work, or end the turn. Process the result when",
|
|
1719
1736
|
" the [subagent-result] notification arrives. Reuse the session id from the",
|
|
1720
1737
|
" receipt to continue the same task later.",
|
|
@@ -1733,9 +1750,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1733
1750
|
promptSnippet:
|
|
1734
1751
|
"Delegate a task to a specialized subagent in an isolated process (async dispatch in TUI mode, blocking otherwise).",
|
|
1735
1752
|
promptGuidelines: [
|
|
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
|
|
1753
|
+
"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.",
|
|
1737
1754
|
"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.",
|
|
1738
1755
|
"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.",
|
|
1756
|
+
"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.",
|
|
1739
1757
|
"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.",
|
|
1740
1758
|
"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.",
|
|
1741
1759
|
],
|
|
@@ -1747,15 +1765,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1747
1765
|
|
|
1748
1766
|
// Depth gate runs BEFORE any action dispatch: a subagent (depth >= 1)
|
|
1749
1767
|
// is blocked from every action — dispatch spawns a nested subagent, and
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1768
|
+
// cancel would let it kill the parent's in-flight tasks. The tool surface
|
|
1769
|
+
// simply does not exist inside a subagent.
|
|
1752
1770
|
const currentDepth = parseEnvInt(process.env.PI_SUBAGENT_DEPTH, 0);
|
|
1753
1771
|
if (currentDepth >= MAX_SUBAGENT_DEPTH) {
|
|
1754
1772
|
const agentName = process.env.PI_CURRENT_AGENT_NAME || "current agent";
|
|
1755
1773
|
return {
|
|
1756
1774
|
content: [{
|
|
1757
1775
|
type: "text",
|
|
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/
|
|
1776
|
+
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/cancel).`,
|
|
1759
1777
|
}],
|
|
1760
1778
|
details: {
|
|
1761
1779
|
mode: "single",
|
|
@@ -1767,13 +1785,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1767
1785
|
};
|
|
1768
1786
|
}
|
|
1769
1787
|
|
|
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
1788
|
if (action === "cancel") {
|
|
1778
1789
|
const taskId = typeof params.taskId === "string" ? params.taskId.trim() : "";
|
|
1779
1790
|
if (!taskId) {
|
|
@@ -1800,7 +1811,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1800
1811
|
|
|
1801
1812
|
if (action !== "dispatch") {
|
|
1802
1813
|
return {
|
|
1803
|
-
content: [{ type: "text", text: `Invalid action: "${action}". Must be one of "dispatch" (default), "
|
|
1814
|
+
content: [{ type: "text", text: `Invalid action: "${action}". Must be one of "dispatch" (default), "cancel".` }],
|
|
1804
1815
|
details: {
|
|
1805
1816
|
mode: "single",
|
|
1806
1817
|
agentScope: (params.agentScope ?? "both") as AgentScope,
|
|
@@ -2031,9 +2042,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2031
2042
|
|
|
2032
2043
|
renderResult(result, { expanded }, theme, context) {
|
|
2033
2044
|
const details = result.details as SubagentDetails | undefined;
|
|
2034
|
-
//
|
|
2035
|
-
//
|
|
2036
|
-
//
|
|
2045
|
+
// cancel receipts carry no `results` array (taskId+cancelled instead)
|
|
2046
|
+
// — fall back to the plain-text content instead of throwing on
|
|
2047
|
+
// details.results.length.
|
|
2037
2048
|
if (!details || !Array.isArray(details.results) || details.results.length === 0) {
|
|
2038
2049
|
return new Text(result.content?.[0]?.type === "text" ? result.content[0].text : "(no output)", 0, 0);
|
|
2039
2050
|
}
|