@maplezzk/pi-interactive-subagents 3.11.1 → 3.13.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/README.md +29 -55
- package/README.zh-CN.md +16 -8
- package/SKILL.md +1 -1
- package/config.json.example +2 -0
- package/package.json +2 -2
- package/pi-extension/subagents/index.ts +136 -146
- package/pi-extension/subagents/mux-config.ts +35 -0
- package/pi-extension/subagents/session.ts +3 -31
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Subagent panes are created without stealing keyboard focus (cmux, tmux). Launch
|
|
|
92
92
|
|
|
93
93
|
### Extensions
|
|
94
94
|
|
|
95
|
-
**Subagents** — 4 main-session tools +
|
|
95
|
+
**Subagents** — 4 main-session tools + 2 commands, plus 1 subagent-only tool:
|
|
96
96
|
|
|
97
97
|
| Tool | Description |
|
|
98
98
|
| -------------------- | ------------------------------------------------------------------------------------------- |
|
|
@@ -104,7 +104,6 @@ Subagent panes are created without stealing keyboard focus (cmux, tmux). Launch
|
|
|
104
104
|
| Command | Description |
|
|
105
105
|
| -------------------------- | ------------------------------------ |
|
|
106
106
|
| `/plan` | Start a full planning workflow |
|
|
107
|
-
| `/iterate` | Fork into a subagent for quick fixes |
|
|
108
107
|
| `/subagent <agent> <task>` | Spawn a named agent directly |
|
|
109
108
|
|
|
110
109
|
### Bundled Agents
|
|
@@ -155,26 +154,24 @@ The widget tracks each Pi-backed sub-agent from a child-written runtime snapshot
|
|
|
155
154
|
|
|
156
155
|
These labels are no longer derived from session-file growth. Session JSONL is still used for transcript, resume, lineage, and result extraction, but Pi-backed liveness now comes from a small activity snapshot written by the child extension. A fixed internal watchdog marks a run as `stalled` when valid snapshots never appear, stop being readable, or stop matching the current child; valid long-running `active` or `waiting` states do not become `stalled` just because time passes. When a run enters `stalled` or recovers from it, the parent agent receives a steer message so it can react. All other status transitions stay in the widget only.
|
|
157
156
|
|
|
158
|
-
**Interactive subagents stay silent.** Long-running user-driven subagents (e.g. `planner
|
|
157
|
+
**Interactive subagents stay silent.** Long-running user-driven subagents (e.g. `planner`) do not wake the parent session on `stalled`/`recovered` transitions — the user is working directly in the subagent's pane, and a steer message there would just burn an orchestrator turn on a no-op "still waiting" ping. The widget still updates normally, and child snapshots are still recorded/classified regardless of the `interactive` setting. By default, agents with `auto-exit: true` are treated as autonomous and get stall pings; agents without it are treated as interactive and stay quiet. Override per-agent with `interactive: true|false` in frontmatter, or per-spawn with `interactive: true|false` on the tool call.
|
|
159
158
|
|
|
160
159
|
#### Configuration
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
cp config.json.example config.json
|
|
166
|
-
```
|
|
161
|
+
The persisted Herdr mode, child-spawning policy, and explicit child-extension list are read from the user extension config at `~/.pi/agent/extensions/pi-interactive-subagents/config.json` (respecting `PI_CODING_AGENT_DIR`). The status panel's `status.enabled` is read from the installed package's `config.json`, falling back to `config.json.example`. The package's `config.json.example` documents the available fields; add `allowSubagentSpawning` and `subagentExtensions` to the user config without overwriting its existing mux settings:
|
|
167
162
|
|
|
168
163
|
```json
|
|
169
164
|
{
|
|
170
165
|
"herdrMode": "split",
|
|
166
|
+
"allowSubagentSpawning": false,
|
|
167
|
+
"subagentExtensions": [],
|
|
171
168
|
"status": {
|
|
172
169
|
"enabled": true
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
172
|
```
|
|
176
173
|
|
|
177
|
-
`herdrMode` accepts `split` (default, backward-compatible pane layout) or `tab` (one background tab per subagent). The `/config:subagent herdr split|tab` command updates this field.
|
|
174
|
+
`herdrMode` accepts `split` (default, backward-compatible pane layout) or `tab` (one background tab per subagent). The `/config:subagent herdr split|tab` command updates this field. `allowSubagentSpawning` is a global switch for whether child subagents may create or manage other subagents; it defaults to `false`. Set it to `true` to enable the lifecycle tools in child sessions when the corresponding extension is selected. `subagentExtensions` is an optional list of extension paths to load explicitly in child sessions; automatic project/global extension discovery is disabled. Paths may be absolute, start with `~/`, or be relative to `PI_CODING_AGENT_DIR`. `subagent-done.ts` is always loaded. Explicit `deny-tools` restrictions still apply.
|
|
178
175
|
|
|
179
176
|
`config.json` is gitignored so local overrides don't get committed.
|
|
180
177
|
|
|
@@ -186,10 +183,7 @@ cp config.json.example config.json
|
|
|
186
183
|
// Named agent with defaults from agent definition
|
|
187
184
|
subagent({ name: "Scout", agent: "scout", task: "Analyze the codebase..." });
|
|
188
185
|
|
|
189
|
-
//
|
|
190
|
-
subagent({ name: "Iterate", fork: true, task: "Fix the bug where..." });
|
|
191
|
-
|
|
192
|
-
// Agent defaults can choose a different session-mode via frontmatter
|
|
186
|
+
// Child sessions start fresh; use session-mode: lineage-only when lineage metadata is useful
|
|
193
187
|
subagent({ name: "Planner", agent: "planner", task: "Work through the design with me" });
|
|
194
188
|
|
|
195
189
|
// Custom working directory
|
|
@@ -203,7 +197,6 @@ subagent({ name: "Designer", agent: "game-designer", cwd: "agents/game-designer"
|
|
|
203
197
|
| `name` | string | required | Display name (shown in widget and pane title) |
|
|
204
198
|
| `task` | string | required | Task prompt for the sub-agent |
|
|
205
199
|
| `agent` | string | — | Load defaults from agent definition |
|
|
206
|
-
| `fork` | boolean | `false` | Force the full-context fork mode for this spawn, overriding any agent `session-mode` frontmatter |
|
|
207
200
|
| `interactive` | boolean | derived | Mark this spawn as interactive (don't wake the parent on stall/recovery). Defaults to the agent's `interactive` frontmatter, otherwise the inverse of `auto-exit`. |
|
|
208
201
|
| `model` | string | — | Override agent's default model |
|
|
209
202
|
| `systemPrompt` | string | — | Append to system prompt |
|
|
@@ -290,18 +283,6 @@ Tab/window titles update to show current phase:
|
|
|
290
283
|
|
|
291
284
|
---
|
|
292
285
|
|
|
293
|
-
## The `/iterate` Workflow
|
|
294
|
-
|
|
295
|
-
For quick, focused work without polluting the main session's context.
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
/iterate Fix the off-by-one error in the pagination logic
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
This always forks the current session into a subagent with full conversation context. It does not inherit an agent default `session-mode`. Make the fix, verify it, and exit to return. The main session gets a summary of what was done.
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
286
|
## Custom Agents
|
|
306
287
|
|
|
307
288
|
Place a `.md` file in `.pi/agents/` (project) or `~/.pi/agent/agents/` (global):
|
|
@@ -314,7 +295,6 @@ model: anthropic/claude-sonnet-4-6
|
|
|
314
295
|
thinking: minimal
|
|
315
296
|
tools: read, bash, edit, write
|
|
316
297
|
session-mode: lineage-only
|
|
317
|
-
spawning: false
|
|
318
298
|
---
|
|
319
299
|
|
|
320
300
|
# My Agent
|
|
@@ -332,8 +312,8 @@ You are a specialized agent that does X...
|
|
|
332
312
|
| `thinking` | string | Thinking level: `minimal`, `medium`, `high` |
|
|
333
313
|
| `tools` | string | Comma-separated **native pi tools only**: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` |
|
|
334
314
|
| `skills` | string | Comma-separated skill names to auto-load |
|
|
335
|
-
| `session-mode` | string | Default child-session mode: `standalone
|
|
336
|
-
| `spawning` | boolean |
|
|
315
|
+
| `session-mode` | string | Default child-session mode: `standalone` or `lineage-only` |
|
|
316
|
+
| `spawning` | boolean | Legacy field retained for compatibility. Use the global `allowSubagentSpawning` setting to control child-session subagent lifecycle tools. |
|
|
337
317
|
| `deny-tools` | string | Comma-separated extension tool names to deny |
|
|
338
318
|
| `auto-exit` | boolean | Auto-shutdown when the agent finishes its turn — no `subagent_done` call needed. If the user sends any input, auto-exit is permanently disabled and the user takes over the session. Recommended for autonomous agents (scout, worker); not for interactive ones (planner). Also determines the default value of `interactive` (see below). |
|
|
339
319
|
| `interactive` | boolean | derived | Override whether stall/recovery transitions wake the parent session. Defaults to the inverse of `auto-exit`: autonomous agents (`auto-exit: true`) are non-interactive and get stall pings; agents without `auto-exit` are interactive and stay quiet. Explicit values take precedence. |
|
|
@@ -346,15 +326,12 @@ Discovery still resolves precedence before visibility filtering. If a project-lo
|
|
|
346
326
|
|
|
347
327
|
### `session-mode`
|
|
348
328
|
|
|
349
|
-
Choose how a
|
|
329
|
+
Choose how a child session starts:
|
|
350
330
|
|
|
351
331
|
- `standalone` — default fresh session with no lineage link to the caller
|
|
352
332
|
- `lineage-only` — fresh blank child session with `parentSession` linkage, but no copied turns from the caller
|
|
353
|
-
- `fork` — linked child session seeded with the caller's prior conversation context
|
|
354
333
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
`fork: true` on the tool call always forces the `fork` mode for that specific spawn. `/iterate` uses this explicit override on purpose.
|
|
334
|
+
All child tasks are delivered through an artifact-backed initial message. There is no full-context fork mode.
|
|
358
335
|
|
|
359
336
|
```yaml
|
|
360
337
|
---
|
|
@@ -376,7 +353,7 @@ When set to `true`, the agent session shuts down automatically as soon as the ag
|
|
|
376
353
|
**When to use:**
|
|
377
354
|
|
|
378
355
|
- ✅ Autonomous agents (scout, worker, reviewer) that run to completion
|
|
379
|
-
- ❌ Interactive agents (planner
|
|
356
|
+
- ❌ Interactive agents (planner) where the user drives the session
|
|
380
357
|
|
|
381
358
|
```yaml
|
|
382
359
|
---
|
|
@@ -389,7 +366,7 @@ auto-exit: true
|
|
|
389
366
|
|
|
390
367
|
Controls whether status transitions (`stalled`, `recovered`) wake the parent session with a steer message.
|
|
391
368
|
|
|
392
|
-
**Default:** the inverse of `auto-exit`. Autonomous agents (`auto-exit: true`) are non-interactive and ping the parent on stall/recovery; agents without `auto-exit` are interactive and stay quiet. Bare spawns with no agent defs
|
|
369
|
+
**Default:** the inverse of `auto-exit`. Autonomous agents (`auto-exit: true`) are non-interactive and ping the parent on stall/recovery; agents without `auto-exit` are interactive and stay quiet. Bare spawns with no agent defs are treated as interactive.
|
|
393
370
|
|
|
394
371
|
**Why it exists:** Interactive agents can run for minutes or hours while the user thinks, types, and reads in the subagent's pane. Child snapshots still update the widget, but stalled/recovered supervision messages rarely need to wake the parent for user-driven sessions. Skipping the steer keeps the parent quiet until the child actually finishes.
|
|
395
372
|
|
|
@@ -415,18 +392,11 @@ subagent({ name: "Scout", agent: "scout", interactive: true, task: "..." });
|
|
|
415
392
|
|
|
416
393
|
## Tool Access Control
|
|
417
394
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
### `spawning: false`
|
|
395
|
+
Child subagent sessions cannot create or manage other subagents unless the global `allowSubagentSpawning` setting is `true`. When it is `false` (the default), the lifecycle tools `subagent`, `subagent_interrupt`, `subagents_list`, and `subagent_resume` are not registered in child sessions. The child-only `subagent_done` tool remains available; `caller_ping` remains available for requesting help from the parent.
|
|
421
396
|
|
|
422
|
-
|
|
397
|
+
### `spawning` (legacy)
|
|
423
398
|
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
name: worker
|
|
427
|
-
spawning: false
|
|
428
|
-
---
|
|
429
|
-
```
|
|
399
|
+
The global `allowSubagentSpawning` setting is authoritative for child sessions. Existing `spawning: false` fields remain accepted for compatibility but do not override the global setting.
|
|
430
400
|
|
|
431
401
|
### `deny-tools`
|
|
432
402
|
|
|
@@ -439,15 +409,9 @@ deny-tools: subagent
|
|
|
439
409
|
---
|
|
440
410
|
```
|
|
441
411
|
|
|
442
|
-
###
|
|
412
|
+
### Global setting
|
|
443
413
|
|
|
444
|
-
|
|
445
|
-
| ---------- | ----------- | -------------------------------------------- |
|
|
446
|
-
| planner | _(default)_ | Legitimately spawns scouts for investigation |
|
|
447
|
-
| worker | `false` | Should implement tasks, not delegate |
|
|
448
|
-
| researcher | `false` | Should research, not spawn |
|
|
449
|
-
| reviewer | `false` | Should review, not spawn |
|
|
450
|
-
| scout | `false` | Should gather context, not spawn |
|
|
414
|
+
The global switch applies uniformly to planner, worker, reviewer, scout, and custom child sessions. Leave `allowSubagentSpawning` as `false` unless a child agent is explicitly expected to create or manage another subagent.
|
|
451
415
|
|
|
452
416
|
---
|
|
453
417
|
|
|
@@ -478,7 +442,6 @@ Set a default `cwd` in agent frontmatter:
|
|
|
478
442
|
---
|
|
479
443
|
name: game-designer
|
|
480
444
|
cwd: ./agents/game-designer
|
|
481
|
-
spawning: false
|
|
482
445
|
---
|
|
483
446
|
```
|
|
484
447
|
|
|
@@ -542,3 +505,14 @@ The sub-agent status supervision and turn-only interruption features were inspir
|
|
|
542
505
|
## License
|
|
543
506
|
|
|
544
507
|
MIT
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
## Naming composition
|
|
511
|
+
|
|
512
|
+
Subagents do not depend on `pi-naming`. Terminal creation supplies an initial surface label where supported; this extension does not schedule delayed title overwrites or rename the parent's terminal when `/plan` runs. Agent identity and activity remain visible in the subagent widget.
|
|
513
|
+
|
|
514
|
+
Each launch (Pi or Claude) and Pi resume writes fresh `PI_TERMINAL_RENAME_CONTEXT` ownership data through `pi-terminal-mux`, replacing inherited values. This allows a cooperating naming extension to update only the child's explicitly owned terminal target, never the shared workspace. Shared or unverified window/tab targets remain unchanged.
|
|
515
|
+
|
|
516
|
+
To use automatic titles or `/rename` in Pi children, explicitly add the installed `pi-naming` entrypoint to `subagentExtensions`. Merely installing both packages in the parent does not enable extensions in isolated child sessions. Naming uses the same terminal-mux protocol and does not import this package.
|
|
517
|
+
|
|
518
|
+
Publication requires a terminal-mux dependency version providing the ownership API.
|
package/README.zh-CN.md
CHANGED
|
@@ -39,10 +39,9 @@ zellij --session pi # 然后运行 pi
|
|
|
39
39
|
|
|
40
40
|
## 主要能力
|
|
41
41
|
|
|
42
|
-
- **4 个主会话工具 +
|
|
42
|
+
- **4 个主会话工具 + 2 个命令**:`subagent`、`subagent_interrupt`、`subagents_list`、`subagent_resume`;命令 `/plan`、`/subagent`
|
|
43
43
|
- **内置 agent**:planner、scout、worker、reviewer、visual-tester
|
|
44
44
|
- **`/plan` 工作流**:调研 → 规划 → 确认 → 执行 → 审查 的完整流水线
|
|
45
|
-
- **`/iterate` 工作流**:fork 当前会话到子 agent 做快速修改,不污染主上下文
|
|
46
45
|
- **caller_ping**:子 agent 向父 agent 求助的机制
|
|
47
46
|
- **自定义 agent**:在 `.pi/agents/` 或 `~/.pi/agent/agents/` 放置 `.md` 定义文件
|
|
48
47
|
|
|
@@ -50,22 +49,20 @@ zellij --session pi # 然后运行 pi
|
|
|
50
49
|
|
|
51
50
|
## 配置
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
cp config.json.example config.json
|
|
57
|
-
```
|
|
52
|
+
持久化的 Herdr 模式、子 agent 创建策略和子 agent 扩展列表读取自用户扩展配置 `~/.pi/agent/extensions/pi-interactive-subagents/config.json`(遵循 `PI_CODING_AGENT_DIR`)。状态面板的 `status.enabled` 读取已安装包目录的 `config.json`,不存在时回退到 `config.json.example`。`config.json.example` 仅用于说明字段;向用户配置添加 `allowSubagentSpawning` 和 `subagentExtensions` 时不要覆盖已有的 mux 设置:
|
|
58
53
|
|
|
59
54
|
```json
|
|
60
55
|
{
|
|
61
56
|
"herdrMode": "split",
|
|
57
|
+
"allowSubagentSpawning": false,
|
|
58
|
+
"subagentExtensions": [],
|
|
62
59
|
"status": {
|
|
63
60
|
"enabled": true
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
```
|
|
67
64
|
|
|
68
|
-
`herdrMode` 支持 `split`(默认,兼容原有 pane 布局)和 `tab`(每个 subagent 独立后台 Tab
|
|
65
|
+
`herdrMode` 支持 `split`(默认,兼容原有 pane 布局)和 `tab`(每个 subagent 独立后台 Tab)。`allowSubagentSpawning` 是全局开关,控制子 agent 是否可以创建或管理其他 subagent,默认值为 `false`;设置为 `true` 后,子 agent 才会获得这些生命周期工具。`subagentExtensions` 是可选扩展路径列表;子 agent 不再自动发现项目级和全局扩展,只加载列表中的扩展,`subagent-done.ts` 始终加载。路径可以是绝对路径、`~/` 路径,或相对于 `PI_CODING_AGENT_DIR` 的路径。`/config:subagent herdr split|tab` 会更新 Herdr 字段。
|
|
69
66
|
|
|
70
67
|
## 致谢
|
|
71
68
|
|
|
@@ -75,3 +72,14 @@ cp config.json.example config.json
|
|
|
75
72
|
## 许可证
|
|
76
73
|
|
|
77
74
|
MIT
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## 与命名插件组合
|
|
78
|
+
|
|
79
|
+
子代理插件不依赖 `pi-naming`。终端创建时在支持的范围内设置初始 surface 名称;不安排延迟覆盖标题,也不因 `/plan` 修改父终端名称。子代理身份与活动状态仍在 widget 中显示。
|
|
80
|
+
|
|
81
|
+
每次启动(Pi 或 Claude)以及 Pi 恢复时,通过 `pi-terminal-mux` 写入新的 `PI_TERMINAL_RENAME_CONTEXT` 归属信息,覆盖继承值。配合命名插件时,只允许更新子代理明确拥有的终端目标,不修改共享 workspace。共享或无法确认独占的 window/tab 保持不变。
|
|
82
|
+
|
|
83
|
+
如需在 Pi 子代理中使用自动标题或 `/rename`,将已安装的 `pi-naming` 入口显式加入 `subagentExtensions`。仅在父会话安装两个包不会开启隔离子会话的扩展。naming 通过相同的 mux 协议协作,不导入本包。
|
|
84
|
+
|
|
85
|
+
发布时 terminal-mux 依赖版本必须包含归属 API。
|
package/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ description: "配置与排查 interactive subagents 的终端复用器、Herdr s
|
|
|
7
7
|
|
|
8
8
|
## 诊断
|
|
9
9
|
|
|
10
|
-
读取实际 Pi agent 目录下的 `extensions/pi-interactive-subagents/config.json`:`mux` 保存后端偏好,`herdrMode` 保存 `split|tab
|
|
10
|
+
读取实际 Pi agent 目录下的 `extensions/pi-interactive-subagents/config.json`:`mux` 保存后端偏好,`herdrMode` 保存 `split|tab`,`allowSubagentSpawning` 控制子 agent 是否可以创建/管理其他 subagent,`subagentExtensions` 指定子 agent 显式加载的扩展路径列表,默认不自动发现其他扩展。默认 `allowSubagentSpawning` 为 `false`,`subagent-done.ts` 始终加载。状态面板的 `status.enabled` 由安装包根目录的 `config.json` 读取;不存在时读取同目录 `config.json.example`。状态行数当前固定为 4,不是配置项。
|
|
11
11
|
|
|
12
12
|
环境变量优先级:`PI_TERMINAL_MUX` > `PI_SUBAGENT_MUX` > 持久化 `mux` > `auto`;`PI_SUBAGENT_HERDR_MODE` > 持久化 `herdrMode` > `split`。
|
|
13
13
|
|
package/config.json.example
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maplezzk/pi-interactive-subagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "Interactive async subagents for pi — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. Fork of HazAT/pi-interactive-subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"ajv": "^8.20.0",
|
|
65
65
|
"pi-extensions-i18n": "^0.4.0",
|
|
66
|
-
"pi-terminal-mux": "^0.
|
|
66
|
+
"pi-terminal-mux": "^0.5.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@earendil-works/pi-coding-agent": ">=0.80.0 <0.81.0",
|
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type, type Static } from "@sinclair/typebox";
|
|
4
4
|
import { Box, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { createTranslator, loadCatalog } from "pi-extensions-i18n";
|
|
8
8
|
import {
|
|
@@ -25,9 +25,8 @@ import {
|
|
|
25
25
|
isMuxAvailable,
|
|
26
26
|
sendEscape,
|
|
27
27
|
shellEscape,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
renameAgent,
|
|
28
|
+
createSurfaceRenameContext,
|
|
29
|
+
TERMINAL_RENAME_CONTEXT_ENV,
|
|
31
30
|
readScreen,
|
|
32
31
|
getLastSplitSource,
|
|
33
32
|
clearLastSplitSource,
|
|
@@ -48,6 +47,8 @@ import {
|
|
|
48
47
|
HERDR_SURFACE_MODES,
|
|
49
48
|
loadHerdrModeConfig,
|
|
50
49
|
loadMuxConfig,
|
|
50
|
+
loadSubagentExtensionsConfig,
|
|
51
|
+
loadSubagentSpawningConfig,
|
|
51
52
|
saveHerdrMode,
|
|
52
53
|
saveMuxPreference,
|
|
53
54
|
SUBAGENT_MUX_BACKENDS,
|
|
@@ -56,6 +57,8 @@ import {
|
|
|
56
57
|
} from "./mux-config.ts";
|
|
57
58
|
|
|
58
59
|
import {
|
|
60
|
+
SUBAGENT_SESSION_MODE_LINEAGE_ONLY,
|
|
61
|
+
SUBAGENT_SESSION_MODE_STANDALONE,
|
|
59
62
|
findLastAssistantMessage,
|
|
60
63
|
getNewEntries,
|
|
61
64
|
seedSubagentSessionFile,
|
|
@@ -168,12 +171,6 @@ const SubagentParams = Type.Object({
|
|
|
168
171
|
"Working directory for the sub-agent. The agent starts in this folder and picks up its local .pi/ config, CLAUDE.md, skills, and extensions. Use for role-specific subfolders.",
|
|
169
172
|
}),
|
|
170
173
|
),
|
|
171
|
-
fork: Type.Optional(
|
|
172
|
-
Type.Boolean({
|
|
173
|
-
description:
|
|
174
|
-
"Force the full-context fork mode for this spawn. The sub-agent inherits the current session conversation, overriding any agent frontmatter session-mode.",
|
|
175
|
-
}),
|
|
176
|
-
),
|
|
177
174
|
interactive: Type.Optional(
|
|
178
175
|
Type.Boolean({
|
|
179
176
|
description:
|
|
@@ -201,7 +198,10 @@ const SubagentParams = Type.Object({
|
|
|
201
198
|
),
|
|
202
199
|
});
|
|
203
200
|
|
|
204
|
-
type SubagentSessionMode =
|
|
201
|
+
type SubagentSessionMode =
|
|
202
|
+
| typeof SUBAGENT_SESSION_MODE_STANDALONE
|
|
203
|
+
| typeof SUBAGENT_SESSION_MODE_LINEAGE_ONLY;
|
|
204
|
+
const DEFAULT_SUBAGENT_SESSION_MODE: SubagentSessionMode = SUBAGENT_SESSION_MODE_STANDALONE;
|
|
205
205
|
|
|
206
206
|
interface AgentDefaults {
|
|
207
207
|
model?: string;
|
|
@@ -232,7 +232,7 @@ interface ListedAgentDefinition extends AgentDefinition {
|
|
|
232
232
|
source: AgentSource;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
/** Tools that are
|
|
235
|
+
/** Tools that are unavailable inside child subagent sessions. */
|
|
236
236
|
const SPAWNING_TOOLS = new Set([
|
|
237
237
|
"subagent",
|
|
238
238
|
"subagent_interrupt",
|
|
@@ -241,21 +241,19 @@ const SPAWNING_TOOLS = new Set([
|
|
|
241
241
|
]);
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
|
-
* Resolve the effective set of denied tool names
|
|
245
|
-
*
|
|
246
|
-
* `
|
|
244
|
+
* Resolve the effective set of denied tool names for a child subagent.
|
|
245
|
+
* The global switch controls whether every child session may create or manage
|
|
246
|
+
* other subagents; the legacy `spawning` field does not override it.
|
|
247
|
+
* `deny-tools` can add individual restrictions on top.
|
|
247
248
|
*/
|
|
248
|
-
function resolveDenyTools(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (agentDefs.spawning === false) {
|
|
254
|
-
for (const t of SPAWNING_TOOLS) denied.add(t);
|
|
255
|
-
}
|
|
249
|
+
function resolveDenyTools(
|
|
250
|
+
agentDefs: AgentDefaults | null,
|
|
251
|
+
allowSubagentSpawning = false,
|
|
252
|
+
): Set<string> {
|
|
253
|
+
const denied = allowSubagentSpawning ? new Set<string>() : new Set(SPAWNING_TOOLS);
|
|
256
254
|
|
|
257
255
|
// deny-tools: explicit list
|
|
258
|
-
if (agentDefs
|
|
256
|
+
if (agentDefs?.denyTools) {
|
|
259
257
|
for (const t of agentDefs.denyTools
|
|
260
258
|
.split(",")
|
|
261
259
|
.map((s) => s.trim())
|
|
@@ -276,6 +274,30 @@ function getBundledAgentsDir(): string {
|
|
|
276
274
|
return join(SUBAGENTS_DIR, "../../agents");
|
|
277
275
|
}
|
|
278
276
|
|
|
277
|
+
const HOME_PREFIX = "~/";
|
|
278
|
+
|
|
279
|
+
/** Resolve an extension path from the user config against the Pi agent directory. */
|
|
280
|
+
function resolveSubagentExtensionPath(extensionPath: string): string {
|
|
281
|
+
if (extensionPath === "~") return homedir();
|
|
282
|
+
if (extensionPath.startsWith(HOME_PREFIX)) return join(homedir(), extensionPath.slice(HOME_PREFIX.length));
|
|
283
|
+
return isAbsolute(extensionPath) ? extensionPath : join(getAgentConfigDir(), extensionPath);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Load and resolve explicitly configured child-session extension paths. */
|
|
287
|
+
function getConfiguredSubagentExtensions(): string[] {
|
|
288
|
+
return loadSubagentExtensionsConfig().extensions.map(resolveSubagentExtensionPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Build the no-discovery flag and explicit extensions for a child Pi command. */
|
|
292
|
+
function buildChildExtensionArgs(extensionPaths = getConfiguredSubagentExtensions()): string[] {
|
|
293
|
+
return [
|
|
294
|
+
NO_EXTENSIONS_FLAG,
|
|
295
|
+
EXTENSION_FLAG,
|
|
296
|
+
shellEscape(join(SUBAGENTS_DIR, SUBAGENT_DONE_EXTENSION_FILE)),
|
|
297
|
+
...extensionPaths.flatMap((extensionPath) => [EXTENSION_FLAG, shellEscape(extensionPath)]),
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
|
|
279
301
|
function getFrontmatterValue(frontmatter: string, key: string): string | undefined {
|
|
280
302
|
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
281
303
|
return match ? match[1].trim() : undefined;
|
|
@@ -285,8 +307,9 @@ function parseOptionalBoolean(value: string | undefined): boolean | undefined {
|
|
|
285
307
|
return value != null ? value === "true" : undefined;
|
|
286
308
|
}
|
|
287
309
|
|
|
310
|
+
/** Parse a supported session mode; invalid values return undefined. */
|
|
288
311
|
function parseSessionMode(value: string | undefined): SubagentSessionMode | undefined {
|
|
289
|
-
if (value ===
|
|
312
|
+
if (value === SUBAGENT_SESSION_MODE_STANDALONE || value === SUBAGENT_SESSION_MODE_LINEAGE_ONLY) {
|
|
290
313
|
return value;
|
|
291
314
|
}
|
|
292
315
|
return undefined;
|
|
@@ -376,30 +399,20 @@ function getDefaultSessionDirFor(cwd: string, _agentDir: string): string {
|
|
|
376
399
|
return sessionDir;
|
|
377
400
|
}
|
|
378
401
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
agentDefs
|
|
382
|
-
): SubagentSessionMode {
|
|
383
|
-
if (params.fork) return "fork";
|
|
384
|
-
return agentDefs?.sessionMode ?? "standalone";
|
|
402
|
+
/** Resolve the agent-configured session mode, defaulting to standalone. */
|
|
403
|
+
function resolveEffectiveSessionMode(agentDefs: AgentDefaults | null): SubagentSessionMode {
|
|
404
|
+
return agentDefs?.sessionMode ?? DEFAULT_SUBAGENT_SESSION_MODE;
|
|
385
405
|
}
|
|
386
406
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
agentDefs: AgentDefaults | null,
|
|
390
|
-
): {
|
|
407
|
+
/** Resolve launch behavior for standalone and lineage-only child sessions. */
|
|
408
|
+
function resolveLaunchBehavior(agentDefs: AgentDefaults | null): {
|
|
391
409
|
sessionMode: SubagentSessionMode;
|
|
392
|
-
seededSessionMode:
|
|
393
|
-
inheritsConversationContext: boolean;
|
|
394
|
-
taskDelivery: "direct" | "artifact";
|
|
410
|
+
seededSessionMode: typeof SUBAGENT_SESSION_MODE_LINEAGE_ONLY | null;
|
|
395
411
|
} {
|
|
396
|
-
const sessionMode = resolveEffectiveSessionMode(
|
|
397
|
-
const inheritsConversationContext = sessionMode === "fork";
|
|
412
|
+
const sessionMode = resolveEffectiveSessionMode(agentDefs);
|
|
398
413
|
return {
|
|
399
414
|
sessionMode,
|
|
400
|
-
seededSessionMode: sessionMode ===
|
|
401
|
-
inheritsConversationContext,
|
|
402
|
-
taskDelivery: inheritsConversationContext ? "direct" : "artifact",
|
|
415
|
+
seededSessionMode: sessionMode === DEFAULT_SUBAGENT_SESSION_MODE ? null : SUBAGENT_SESSION_MODE_LINEAGE_ONLY,
|
|
403
416
|
};
|
|
404
417
|
}
|
|
405
418
|
|
|
@@ -412,12 +425,11 @@ function resolveLaunchBehavior(
|
|
|
412
425
|
* 3. Default: the inverse of `auto-exit`. Agents that auto-exit are
|
|
413
426
|
* autonomous (scout, worker, reviewer) and the parent session should be
|
|
414
427
|
* woken on stall/recovery transitions. Agents that don't auto-exit are
|
|
415
|
-
* driven by the user in their own pane (planner
|
|
416
|
-
*
|
|
428
|
+
* driven by the user in their own pane (planner) and stall pings are
|
|
429
|
+
* noise.
|
|
417
430
|
*
|
|
418
|
-
* When no agent defs exist at all (bare `subagent({ name, task })` call,
|
|
419
|
-
*
|
|
420
|
-
* subagent is treated as interactive — matching the intent of iterate.
|
|
431
|
+
* When no agent defs exist at all (bare `subagent({ name, task })` call),
|
|
432
|
+
* `autoExit` is undefined and the subagent is treated as interactive.
|
|
421
433
|
*/
|
|
422
434
|
function resolveEffectiveInteractive(
|
|
423
435
|
params: Static<typeof SubagentParams>,
|
|
@@ -706,6 +718,10 @@ function updateWidget() {
|
|
|
706
718
|
* as standalone prompts in the child session.
|
|
707
719
|
*/
|
|
708
720
|
const SUBAGENT_CONTROL_TOOLS = ["caller_ping", "subagent_done"] as const;
|
|
721
|
+
const FILE_TIMESTAMP_LENGTH = 19;
|
|
722
|
+
const NO_EXTENSIONS_FLAG = "--no-extensions";
|
|
723
|
+
const EXTENSION_FLAG = "--extension";
|
|
724
|
+
const SUBAGENT_DONE_EXTENSION_FILE = "subagent-done.ts";
|
|
709
725
|
|
|
710
726
|
/**
|
|
711
727
|
* Build the child --tools allowlist.
|
|
@@ -731,21 +747,19 @@ function buildSubagentToolAllowlist(effectiveTools?: string): string | null {
|
|
|
731
747
|
return [...allow].join(",");
|
|
732
748
|
}
|
|
733
749
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}): string[] {
|
|
750
|
+
/**
|
|
751
|
+
* Build positional prompt args for a child launch.
|
|
752
|
+
* Skills must be separate prompts so Pi expands each `/skill:` directive.
|
|
753
|
+
*/
|
|
754
|
+
function buildPiPromptArgs(params: { effectiveSkills?: string; taskArg: string }): string[] {
|
|
739
755
|
const skillPrompts = (params.effectiveSkills ?? "")
|
|
740
756
|
.split(",")
|
|
741
757
|
.map((s) => s.trim())
|
|
742
758
|
.filter(Boolean)
|
|
743
759
|
.map((skill) => `/skill:${skill}`);
|
|
744
760
|
|
|
745
|
-
const needsSeparator = params.taskDelivery === "artifact" && skillPrompts.length > 0;
|
|
746
|
-
|
|
747
761
|
return [
|
|
748
|
-
...(
|
|
762
|
+
...(skillPrompts.length > 0 ? [""] : []),
|
|
749
763
|
...skillPrompts,
|
|
750
764
|
params.taskArg,
|
|
751
765
|
];
|
|
@@ -882,10 +896,25 @@ function handleSubagentInterrupt(
|
|
|
882
896
|
running.statusState = forceStatusAfterInterrupt(running.statusState, now);
|
|
883
897
|
updateWidget();
|
|
884
898
|
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
899
|
+
// Interrupting from the parent is terminal: Escape stops the child's active
|
|
900
|
+
// turn, while the `.exit` sidecar tells the watcher to close the surface and
|
|
901
|
+
// remove the child from the running set. Without the sidecar, the child Pi
|
|
902
|
+
// returns to its prompt and the watcher waits forever.
|
|
903
|
+
if (running.sessionFile) {
|
|
904
|
+
const exitFile = `${running.sessionFile}.exit`;
|
|
905
|
+
try {
|
|
906
|
+
writeFileSync(exitFile, JSON.stringify({ type: "done" }));
|
|
907
|
+
} catch (writeErr: unknown) {
|
|
908
|
+
const errorMessage = writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
909
|
+
const error =
|
|
910
|
+
`Failed to signal subagent "${running.name}" termination via ${exitFile}: ` +
|
|
911
|
+
errorMessage;
|
|
912
|
+
return {
|
|
913
|
+
content: [{ type: "text" as const, text: error }],
|
|
914
|
+
details: { error, id: running.id, name: running.name },
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
889
918
|
|
|
890
919
|
return {
|
|
891
920
|
content: [{ type: "text" as const, text: `Interrupt requested for subagent "${running.name}".` }],
|
|
@@ -1104,7 +1133,14 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
|
1104
1133
|
}
|
|
1105
1134
|
}
|
|
1106
1135
|
|
|
1136
|
+
/** 每次启动或恢复都用新 surface 重建归属,不能继承父进程的改名范围。 */
|
|
1137
|
+
function buildTerminalRenameEnvironment(surface: string, backend = getMuxBackend()): string {
|
|
1138
|
+
const context = createSurfaceRenameContext(surface, backend);
|
|
1139
|
+
return `${TERMINAL_RENAME_CONTEXT_ENV}=${shellEscape(JSON.stringify(context))}`;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1107
1142
|
export const __test__ = {
|
|
1143
|
+
buildTerminalRenameEnvironment,
|
|
1108
1144
|
borderLine,
|
|
1109
1145
|
parseMuxConfigRequest,
|
|
1110
1146
|
getShellReadyDelayMs,
|
|
@@ -1115,6 +1151,7 @@ export const __test__ = {
|
|
|
1115
1151
|
resolveLaunchBehavior,
|
|
1116
1152
|
resolveEffectiveInteractive,
|
|
1117
1153
|
buildSubagentToolAllowlist,
|
|
1154
|
+
buildChildExtensionArgs,
|
|
1118
1155
|
buildPiPromptArgs,
|
|
1119
1156
|
formatWidgetRightLabel,
|
|
1120
1157
|
observeRunningSubagent,
|
|
@@ -1182,17 +1219,17 @@ async function launchSubagent(
|
|
|
1182
1219
|
// For new surfaces, pause briefly so the shell is ready before sending the command.
|
|
1183
1220
|
const surfacePreCreated = !!options?.surface;
|
|
1184
1221
|
const surface = options?.surface ?? createSurface(params.name);
|
|
1222
|
+
const renameEnvironment = buildTerminalRenameEnvironment(surface);
|
|
1185
1223
|
const splitFrom = surfacePreCreated ? undefined : (getLastSplitSource() ?? undefined);
|
|
1186
1224
|
if (!surfacePreCreated) clearLastSplitSource();
|
|
1187
1225
|
if (!surfacePreCreated) {
|
|
1188
1226
|
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
|
1189
1227
|
}
|
|
1190
1228
|
|
|
1191
|
-
const launchBehavior = resolveLaunchBehavior(
|
|
1229
|
+
const launchBehavior = resolveLaunchBehavior(agentDefs);
|
|
1192
1230
|
|
|
1193
1231
|
if (launchBehavior.seededSessionMode) {
|
|
1194
1232
|
seedSubagentSessionFile({
|
|
1195
|
-
mode: launchBehavior.seededSessionMode,
|
|
1196
1233
|
parentSessionFile: sessionFile,
|
|
1197
1234
|
childSessionFile: subagentSessionFile,
|
|
1198
1235
|
childCwd: targetCwdForSession,
|
|
@@ -1201,31 +1238,27 @@ async function launchSubagent(
|
|
|
1201
1238
|
|
|
1202
1239
|
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
1203
1240
|
mkdirSync(dirname(activityFile), { recursive: true });
|
|
1204
|
-
const { inheritsConversationContext } = launchBehavior;
|
|
1205
1241
|
|
|
1206
|
-
// Build the task message
|
|
1207
|
-
// Only full-context fork mode inherits prior conversation state.
|
|
1208
|
-
// Blank-session modes need the wrapper instructions and artifact-backed handoff.
|
|
1242
|
+
// Build the task message. All child sessions use artifact-backed delivery.
|
|
1209
1243
|
const modeHint = agentDefs?.autoExit
|
|
1210
1244
|
? "Complete your task autonomously."
|
|
1211
1245
|
: "Complete your task. When finished, call the subagent_done tool. The user can interact with you at any time.";
|
|
1212
1246
|
const summaryInstruction = agentDefs?.autoExit
|
|
1213
1247
|
? "Your FINAL assistant message should summarize what you accomplished."
|
|
1214
1248
|
: "Your FINAL assistant message (before calling subagent_done or before the user exits) should summarize what you accomplished.";
|
|
1215
|
-
const
|
|
1249
|
+
const { allowSubagentSpawning } = loadSubagentSpawningConfig();
|
|
1250
|
+
const denySet = resolveDenyTools(agentDefs, allowSubagentSpawning);
|
|
1216
1251
|
const identity = agentDefs?.body ?? params.systemPrompt ?? null;
|
|
1217
1252
|
const systemPromptMode = agentDefs?.systemPromptMode;
|
|
1218
1253
|
const identityInSystemPrompt = systemPromptMode && identity;
|
|
1219
1254
|
const roleBlock = identity && !identityInSystemPrompt ? `\n\n${identity}` : "";
|
|
1220
|
-
const fullTask =
|
|
1221
|
-
? params.task
|
|
1222
|
-
: `${roleBlock}\n\n${modeHint}\n\n${params.task}\n\n${summaryInstruction}`;
|
|
1255
|
+
const fullTask = `${roleBlock}\n\n${modeHint}\n\n${params.task}\n\n${summaryInstruction}`;
|
|
1223
1256
|
// ── Claude Code CLI path ──
|
|
1224
1257
|
if (agentDefs?.cli === "claude") {
|
|
1225
1258
|
const sentinelFile = `/tmp/pi-claude-${id}-done`;
|
|
1226
1259
|
const pluginDir = join(SUBAGENTS_DIR, "plugin");
|
|
1227
1260
|
|
|
1228
|
-
const cmdParts: string[] = [];
|
|
1261
|
+
const cmdParts: string[] = [renameEnvironment];
|
|
1229
1262
|
cmdParts.push(`PI_CLAUDE_SENTINEL=${shellEscape(sentinelFile)}`);
|
|
1230
1263
|
cmdParts.push("claude");
|
|
1231
1264
|
cmdParts.push("--dangerously-skip-permissions");
|
|
@@ -1297,12 +1330,9 @@ async function launchSubagent(
|
|
|
1297
1330
|
// ── Pi CLI path ──
|
|
1298
1331
|
|
|
1299
1332
|
// Build pi command
|
|
1300
|
-
const parts: string[] = ["pi"];
|
|
1333
|
+
const parts: string[] = ["pi", ...buildChildExtensionArgs()];
|
|
1301
1334
|
parts.push("--session", shellEscape(subagentSessionFile));
|
|
1302
1335
|
|
|
1303
|
-
const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
|
|
1304
|
-
parts.push("-e", shellEscape(subagentDonePath));
|
|
1305
|
-
|
|
1306
1336
|
if (effectiveModel) {
|
|
1307
1337
|
const model = effectiveThinking ? `${effectiveModel}:${effectiveThinking}` : effectiveModel;
|
|
1308
1338
|
parts.push("--model", shellEscape(model));
|
|
@@ -1313,7 +1343,7 @@ async function launchSubagent(
|
|
|
1313
1343
|
// auto-detect file paths and read their contents.
|
|
1314
1344
|
if (identityInSystemPrompt && identity) {
|
|
1315
1345
|
const flag = systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt";
|
|
1316
|
-
const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0,
|
|
1346
|
+
const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
1317
1347
|
const spSafeName = params.name
|
|
1318
1348
|
.toLowerCase()
|
|
1319
1349
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
@@ -1332,7 +1362,7 @@ async function launchSubagent(
|
|
|
1332
1362
|
}
|
|
1333
1363
|
|
|
1334
1364
|
// Build env prefix: denied tools + subagent identity + config dir propagation
|
|
1335
|
-
const envParts: string[] = [];
|
|
1365
|
+
const envParts: string[] = [renameEnvironment];
|
|
1336
1366
|
|
|
1337
1367
|
// If the target cwd has its own .pi/agent/, use that as the config root.
|
|
1338
1368
|
// Otherwise propagate the current/global agent dir.
|
|
@@ -1363,33 +1393,22 @@ async function launchSubagent(
|
|
|
1363
1393
|
}
|
|
1364
1394
|
const envPrefix = envParts.join(" ") + " ";
|
|
1365
1395
|
|
|
1366
|
-
// Pass task and skill prompts to the
|
|
1367
|
-
//
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const artifactPath = join(artifactDir, artifactName);
|
|
1383
|
-
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
1384
|
-
writeFileSync(artifactPath, fullTask, "utf8");
|
|
1385
|
-
taskArg = `@${artifactPath}`;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
for (const promptArg of buildPiPromptArgs({
|
|
1389
|
-
effectiveSkills,
|
|
1390
|
-
taskDelivery: launchBehavior.taskDelivery,
|
|
1391
|
-
taskArg,
|
|
1392
|
-
})) {
|
|
1396
|
+
// Pass task and skill prompts to the child. The task is written to an
|
|
1397
|
+
// artifact so wrapper instructions arrive as the initial user message.
|
|
1398
|
+
const artifactTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
1399
|
+
const safeName = params.name
|
|
1400
|
+
.toLowerCase()
|
|
1401
|
+
.replace(/[^a-z0-9\s-]/g, "") // strip everything except alphanumeric, spaces, hyphens
|
|
1402
|
+
.replace(/\s+/g, "-") // spaces to hyphens
|
|
1403
|
+
.replace(/-+/g, "-") // collapse multiple hyphens
|
|
1404
|
+
.replace(/^-|-$/g, ""); // trim leading/trailing hyphens
|
|
1405
|
+
const artifactName = `context/${safeName || "subagent"}-${artifactTimestamp}.md`;
|
|
1406
|
+
const artifactPath = join(artifactDir, artifactName);
|
|
1407
|
+
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
1408
|
+
writeFileSync(artifactPath, fullTask, "utf8");
|
|
1409
|
+
const taskArg = `@${artifactPath}`;
|
|
1410
|
+
|
|
1411
|
+
for (const promptArg of buildPiPromptArgs({ effectiveSkills, taskArg })) {
|
|
1393
1412
|
parts.push(shellEscape(promptArg));
|
|
1394
1413
|
}
|
|
1395
1414
|
|
|
@@ -1416,12 +1435,6 @@ async function launchSubagent(
|
|
|
1416
1435
|
].join("\n"),
|
|
1417
1436
|
});
|
|
1418
1437
|
|
|
1419
|
-
// 延迟重命名 agent 标题(左侧侧栏),需要等 pi 启动被 herdr 检测到
|
|
1420
|
-
const agentName = params.name;
|
|
1421
|
-
const agentSurface = surface;
|
|
1422
|
-
setTimeout(() => renameAgent(agentSurface, agentName), 3000);
|
|
1423
|
-
setTimeout(() => renameAgent(agentSurface, agentName), 5000);
|
|
1424
|
-
|
|
1425
1438
|
const running: RunningSubagent = {
|
|
1426
1439
|
id,
|
|
1427
1440
|
name: params.name,
|
|
@@ -1852,13 +1865,11 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
1852
1865
|
name: "subagent_interrupt",
|
|
1853
1866
|
label: "Interrupt Subagent",
|
|
1854
1867
|
description:
|
|
1855
|
-
"Send Escape to the active turn of a currently running Pi-backed subagent. " +
|
|
1856
|
-
"The
|
|
1857
|
-
"and does not emit a subagent_result solely because of this request.",
|
|
1868
|
+
"Send Escape to stop the active turn of a currently running Pi-backed subagent, then terminate the child Pi process. " +
|
|
1869
|
+
"The parent watcher consumes the completion signal, closes the child pane, and removes the subagent from the running set.",
|
|
1858
1870
|
promptSnippet:
|
|
1859
|
-
"Send Escape to the active turn of a currently running Pi-backed subagent. " +
|
|
1860
|
-
"The
|
|
1861
|
-
"and does not emit a subagent_result solely because of this request.",
|
|
1871
|
+
"Send Escape to stop the active turn of a currently running Pi-backed subagent, then terminate the child Pi process. " +
|
|
1872
|
+
"The parent watcher consumes the completion signal, closes the child pane, and removes the subagent from the running set.",
|
|
1862
1873
|
parameters: Type.Object({
|
|
1863
1874
|
id: Type.Optional(Type.String({ description: "Exact running subagent id" })),
|
|
1864
1875
|
name: Type.Optional(Type.String({ description: "Exact running subagent display name" })),
|
|
@@ -2039,14 +2050,11 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2039
2050
|
const entryCountBefore = getNewEntries(params.sessionPath, 0).length;
|
|
2040
2051
|
|
|
2041
2052
|
const surface = createSurface(name);
|
|
2053
|
+
const renameEnvironment = buildTerminalRenameEnvironment(surface);
|
|
2042
2054
|
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
|
2043
2055
|
|
|
2044
2056
|
// Build pi resume command
|
|
2045
|
-
const parts = ["pi", "--session", shellEscape(params.sessionPath)];
|
|
2046
|
-
|
|
2047
|
-
// Load subagent-done extension so the agent can self-terminate if needed
|
|
2048
|
-
const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
|
|
2049
|
-
parts.push("-e", shellEscape(subagentDonePath));
|
|
2057
|
+
const parts = ["pi", ...buildChildExtensionArgs(), "--session", shellEscape(params.sessionPath)];
|
|
2050
2058
|
|
|
2051
2059
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
2052
2060
|
const artifactDir = getArtifactDir(ctx.sessionManager.getSessionDir(), sessionId);
|
|
@@ -2055,7 +2063,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2055
2063
|
|
|
2056
2064
|
let resumeMsgFile: string | undefined;
|
|
2057
2065
|
if (params.message) {
|
|
2058
|
-
const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0,
|
|
2066
|
+
const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
2059
2067
|
resumeMsgFile = join(
|
|
2060
2068
|
artifactDir,
|
|
2061
2069
|
"subagent-resume",
|
|
@@ -2072,14 +2080,19 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2072
2080
|
}
|
|
2073
2081
|
|
|
2074
2082
|
// Build env prefix — propagate PI_CODING_AGENT_DIR for config isolation
|
|
2075
|
-
const resumeEnvParts: string[] = [];
|
|
2083
|
+
const resumeEnvParts: string[] = [renameEnvironment];
|
|
2084
|
+
const { allowSubagentSpawning } = loadSubagentSpawningConfig();
|
|
2076
2085
|
if (process.env.PI_CODING_AGENT_DIR) {
|
|
2077
2086
|
resumeEnvParts.push(`PI_CODING_AGENT_DIR=${shellEscape(process.env.PI_CODING_AGENT_DIR)}`);
|
|
2078
2087
|
}
|
|
2079
2088
|
resumeEnvParts.push(`PI_SUBAGENT_NAME=${shellEscape(name)}`);
|
|
2080
2089
|
resumeEnvParts.push(`PI_SUBAGENT_SESSION=${shellEscape(params.sessionPath)}`);
|
|
2081
2090
|
resumeEnvParts.push(`PI_SUBAGENT_ID=${shellEscape(id)}`);
|
|
2091
|
+
resumeEnvParts.push(`PI_SUBAGENT_SURFACE=${shellEscape(surface)}`);
|
|
2082
2092
|
resumeEnvParts.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellEscape(activityFile)}`);
|
|
2093
|
+
resumeEnvParts.push(
|
|
2094
|
+
`PI_DENY_TOOLS=${shellEscape([...resolveDenyTools(null, allowSubagentSpawning)].join(","))}`,
|
|
2095
|
+
);
|
|
2083
2096
|
if (autoExit) {
|
|
2084
2097
|
resumeEnvParts.push(`PI_SUBAGENT_AUTO_EXIT=1`);
|
|
2085
2098
|
}
|
|
@@ -2205,18 +2218,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2205
2218
|
},
|
|
2206
2219
|
});
|
|
2207
2220
|
|
|
2208
|
-
// /iterate command — fork the session into a subagent
|
|
2209
|
-
pi.registerCommand("iterate", {
|
|
2210
|
-
description: "Fork session into a subagent for focused work (bugfixes, iteration)",
|
|
2211
|
-
handler: async (args, _ctx) => {
|
|
2212
|
-
const task = args.trim() || "";
|
|
2213
|
-
const toolCall = task
|
|
2214
|
-
? `Use subagent to fork a session. fork: true, name: "Iterate", task: ${JSON.stringify(task)}`
|
|
2215
|
-
: `Use subagent to fork a session. fork: true, name: "Iterate", task: "The user wants to do some hands-on work. Help them with whatever they need."`;
|
|
2216
|
-
pi.sendUserMessage(toolCall);
|
|
2217
|
-
},
|
|
2218
|
-
});
|
|
2219
|
-
|
|
2220
2221
|
// /subagent command — spawn a subagent by name
|
|
2221
2222
|
pi.registerCommand("subagent", {
|
|
2222
2223
|
description: "Spawn a subagent: /subagent <agent> <task>",
|
|
@@ -2395,17 +2396,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2395
2396
|
return;
|
|
2396
2397
|
}
|
|
2397
2398
|
|
|
2398
|
-
// Rename workspace and tab to show this is a planning session
|
|
2399
|
-
if (isMuxAvailable()) {
|
|
2400
|
-
try {
|
|
2401
|
-
const label = task.length > 40 ? task.slice(0, 40) + "..." : task;
|
|
2402
|
-
renameWorkspace(`🎯 ${label}`);
|
|
2403
|
-
renameCurrentTab(`🎯 Plan: ${label}`);
|
|
2404
|
-
} catch {
|
|
2405
|
-
// non-critical -- do not block the plan
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
2399
|
// Load the plan skill from the subagents extension directory
|
|
2410
2400
|
const planSkillPath = join(SUBAGENTS_DIR, "plan-skill.md");
|
|
2411
2401
|
let content = readFileSync(planSkillPath, "utf8");
|
|
@@ -20,10 +20,22 @@ export interface SubagentHerdrModeConfig {
|
|
|
20
20
|
source: SubagentMuxConfigSource;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface SubagentSpawningConfig {
|
|
24
|
+
allowSubagentSpawning: boolean;
|
|
25
|
+
source: SubagentMuxConfigSource;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SubagentExtensionsConfig {
|
|
29
|
+
extensions: string[];
|
|
30
|
+
source: SubagentMuxConfigSource;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
const BACKENDS: readonly MuxBackend[] = ["muxy", "cmux", "tmux", "zellij", "wezterm", "herdr", "otty", "orca"];
|
|
24
34
|
const CONFIG_FILE = "config.json";
|
|
25
35
|
const HERDR_MODE_ENV = "PI_SUBAGENT_HERDR_MODE";
|
|
26
36
|
const DEFAULT_HERDR_MODE: HerdrSurfaceMode = HERDR_SURFACE_MODE_SPLIT;
|
|
37
|
+
const DEFAULT_ALLOW_SUBAGENT_SPAWNING = false;
|
|
38
|
+
const DEFAULT_SUBAGENT_EXTENSIONS: readonly string[] = [];
|
|
27
39
|
|
|
28
40
|
function isMuxBackend(value: unknown): value is MuxBackend {
|
|
29
41
|
return typeof value === "string" && (BACKENDS as readonly string[]).includes(value);
|
|
@@ -111,6 +123,29 @@ export function loadHerdrModeConfig(path = muxConfigPath()): SubagentHerdrModeCo
|
|
|
111
123
|
return { herdrMode: DEFAULT_HERDR_MODE, source: "default" };
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
/** Read the global child-subagent spawning switch; disabled by default. */
|
|
127
|
+
export function loadSubagentSpawningConfig(path = muxConfigPath()): SubagentSpawningConfig {
|
|
128
|
+
const stored = readConfigObject(path).allowSubagentSpawning;
|
|
129
|
+
if (typeof stored === "boolean") {
|
|
130
|
+
return { allowSubagentSpawning: stored, source: "file" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { allowSubagentSpawning: DEFAULT_ALLOW_SUBAGENT_SPAWNING, source: "default" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Read the explicit extension list for child sessions; empty by default. */
|
|
137
|
+
export function loadSubagentExtensionsConfig(path = muxConfigPath()): SubagentExtensionsConfig {
|
|
138
|
+
const stored = readConfigObject(path).subagentExtensions;
|
|
139
|
+
if (Array.isArray(stored) && stored.every((value) => typeof value === "string" && value.trim())) {
|
|
140
|
+
return {
|
|
141
|
+
extensions: stored.map((value) => value.trim()),
|
|
142
|
+
source: "file",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { extensions: [...DEFAULT_SUBAGENT_EXTENSIONS], source: "default" };
|
|
147
|
+
}
|
|
148
|
+
|
|
114
149
|
/** Apply persisted mux and Herdr mode settings when no explicit environment override exists. */
|
|
115
150
|
export function applyPersistedMuxPreference(path = muxConfigPath()): void {
|
|
116
151
|
if (!environmentPreference()) {
|
|
@@ -17,36 +17,10 @@ export interface MessageEntry extends SessionEntry {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
function getForkContentLines(parentSessionFile: string): string[] {
|
|
23
|
-
const raw = readFileSync(parentSessionFile, "utf8");
|
|
24
|
-
const lines = raw.split("\n").filter((line) => line.trim());
|
|
25
|
-
|
|
26
|
-
let truncateAt = lines.length;
|
|
27
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
28
|
-
try {
|
|
29
|
-
const entry = JSON.parse(lines[i]);
|
|
30
|
-
if (entry.type === "message" && entry.message?.role === "user") {
|
|
31
|
-
truncateAt = i;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// ignore malformed lines
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return lines.slice(0, truncateAt).filter((line) => {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(line).type !== "session";
|
|
42
|
-
} catch {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
20
|
+
export const SUBAGENT_SESSION_MODE_STANDALONE = "standalone" as const;
|
|
21
|
+
export const SUBAGENT_SESSION_MODE_LINEAGE_ONLY = "lineage-only" as const;
|
|
47
22
|
|
|
48
23
|
export function seedSubagentSessionFile(params: {
|
|
49
|
-
mode: SeededSubagentSessionMode;
|
|
50
24
|
parentSessionFile: string;
|
|
51
25
|
childSessionFile: string;
|
|
52
26
|
childCwd: string;
|
|
@@ -59,9 +33,7 @@ export function seedSubagentSessionFile(params: {
|
|
|
59
33
|
cwd: params.childCwd,
|
|
60
34
|
parentSession: params.parentSessionFile,
|
|
61
35
|
};
|
|
62
|
-
const
|
|
63
|
-
params.mode === "fork" ? getForkContentLines(params.parentSessionFile) : [];
|
|
64
|
-
const lines = [JSON.stringify(header), ...contentLines];
|
|
36
|
+
const lines = [JSON.stringify(header)];
|
|
65
37
|
|
|
66
38
|
mkdirSync(dirname(params.childSessionFile), { recursive: true });
|
|
67
39
|
writeFileSync(params.childSessionFile, lines.join("\n") + "\n", "utf8");
|