pi-subagents 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +13 -10
- package/agents/reviewer.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +24 -6
- package/src/agents/agent-management.ts +3 -1
- package/src/agents/agents.ts +22 -4
- package/src/extension/doctor.ts +1 -0
- package/src/extension/index.ts +12 -6
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +140 -11
- package/src/intercom/result-intercom.ts +8 -3
- package/src/manager-ui/agent-manager.ts +6 -5
- package/src/runs/background/async-execution.ts +22 -7
- package/src/runs/background/async-job-tracker.ts +2 -2
- package/src/runs/background/async-resume.ts +57 -31
- package/src/runs/background/async-status.ts +7 -1
- package/src/runs/background/result-watcher.ts +3 -1
- package/src/runs/background/run-status.ts +22 -19
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +52 -7
- package/src/runs/foreground/chain-clarify.ts +2 -3
- package/src/runs/foreground/chain-execution.ts +55 -21
- package/src/runs/foreground/execution.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +157 -23
- package/src/runs/shared/single-output.ts +21 -6
- package/src/shared/settings.ts +19 -0
- package/src/shared/types.ts +26 -1
- package/src/slash/slash-commands.ts +1 -0
- package/src/tui/render.ts +202 -16
- package/src/tui/subagents-status.ts +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.23.1] - 2026-05-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Persist async per-child session metadata and remember recent foreground child session metadata so `resume` can revive multi-child async runs and foreground children by index.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Keep foreground children alive when they call `contact_supervisor` for a blocking decision by treating it as intercom coordination during parent detach, matching the generic `intercom` handoff path.
|
|
12
|
+
- Pause foreground parallel and chain flows when a child detaches for intercom coordination instead of counting the child as a successful completed result and continuing the workflow, and suppress grouped completion receipts for detached chains.
|
|
13
|
+
- Tighten resume/revive safety by rejecting pending async children, detached foreground children that may still be live, ambiguous foreground/async id prefixes, and exact invalid resume matches that would otherwise be masked by a prefix match in the other namespace.
|
|
14
|
+
- Preserve child session metadata in stale-run repaired results and avoid advertising revive from top-level-only or missing child session files.
|
|
15
|
+
- Stop builtin `reviewer` runs from writing progress by default, clarify that review-only/no-edit instructions win over progress-writing or artifact-writing instructions, and suppress automatic progress injection for explicit no-edit tasks even when chain templates use `{task}`.
|
|
16
|
+
- Treat parsed provider errors as failed foreground and async subagent attempts even when the child process exits successfully, and baseline saved output files per fallback attempt.
|
|
17
|
+
- Preserve output-file read and inspect errors instead of silently overwriting or falling back when a changed saved-output path cannot be read.
|
|
18
|
+
- Show each active async widget row's lifecycle status (`running`, `complete`, `failed`, or `paused`) alongside activity and usage stats.
|
|
19
|
+
- Start new direct, slash, prompt-template, foreground, and async subagent launches in compact view while keeping `Ctrl+O` available for live detail.
|
|
20
|
+
- Label top-level async parallel completion notifications as parallel runs instead of leaking the internal chain-shaped runner plan.
|
|
21
|
+
|
|
22
|
+
## [0.23.0] - 2026-05-02
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Detect `pi-intercom` when installed through the documented `pi install npm:pi-intercom` package flow, instead of only checking the legacy local extension path.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Store and discover saved chain workflows from dedicated chain directories: user chains in `~/.pi/agent/chains/**/*.chain.md` and project chains in `.pi/chains/**/*.chain.md`.
|
|
29
|
+
- Retry foreground subagent fallback models when Pi reports a retryable provider error, such as 429/quota, even if the child process exits successfully.
|
|
30
|
+
- Align single-run async subagent widgets and `/subagents-status` rendering with foreground subagent result styling for parallel, chain, and grouped chain runs, including inline live detail when tool output expansion is enabled, while keeping multi-job async widgets compact.
|
|
31
|
+
- Render async subagent widgets through an adaptive component so active parallel agent rows fit without Pi's fixed string-widget truncation marker.
|
|
32
|
+
- Tell parent agents that async runs are detached and they should end the turn instead of running sleep/poll loops when no independent work remains.
|
|
33
|
+
|
|
5
34
|
## [0.22.0] - 2026-05-02
|
|
6
35
|
|
|
7
36
|
### Added
|
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ The package includes reusable prompt templates for common workflows. You do not
|
|
|
211
211
|
pi install npm:pi-intercom
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
Most users do not call `intercom` directly. After `pi-intercom` is installed, `pi-subagents` can automatically give child agents a private coordination channel back to the parent session.
|
|
214
|
+
Most users do not call `intercom` directly. After `pi-intercom` is installed, `pi-subagents` can automatically give child agents a private coordination channel back to the parent session. The bridge recognizes the normal `pi install npm:pi-intercom` package install as well as legacy local extension checkouts.
|
|
215
215
|
|
|
216
216
|
Use it for work where the child might need a decision instead of guessing:
|
|
217
217
|
|
|
@@ -225,7 +225,7 @@ Ask oracle to review this plan. If it sees a decision I need to make, have it as
|
|
|
225
225
|
|
|
226
226
|
The child can use one dedicated coordination tool:
|
|
227
227
|
|
|
228
|
-
- `contact_supervisor`: the child contacts the parent/supervisor session that delegated the task. Use `reason: "need_decision"` for blocking decisions or clarification, and `reason: "progress_update"` for short non-blocking updates when a discovery changes the plan.
|
|
228
|
+
- `contact_supervisor`: the child contacts the parent/supervisor session that delegated the task. Use `reason: "need_decision"` for blocking decisions or clarification, and `reason: "progress_update"` for short non-blocking updates when a discovery changes the plan. Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions; no-edit wins.
|
|
229
229
|
|
|
230
230
|
Child-side routine completion handoffs are still not expected. With the intercom bridge active, parent-side `pi-subagents` sends grouped completion results through `pi-intercom`: one grouped message per foreground parent `subagent` run and one per completed async result file. Acknowledged foreground delivery returns a compact receipt with artifact/session paths; if unacknowledged, the normal full output is preserved. Grouped messages include child intercom targets and full child summaries.
|
|
231
231
|
|
|
@@ -332,6 +332,8 @@ You can combine them in either order:
|
|
|
332
332
|
/run reviewer "review this diff" --bg --fork
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
Background runs are detached. If the parent agent has other independent work, it should keep working. If it has nothing useful to do until the background result arrives, it should end the turn instead of running sleep or status-polling loops. Pi will deliver the completion when the run finishes.
|
|
336
|
+
|
|
335
337
|
The `oracle` and `worker` builtins are designed for an explicit decision loop. A typical pattern is to ask `oracle` for diagnosis and a recommended execution prompt, then only run `worker` after the main agent approves that direction.
|
|
336
338
|
|
|
337
339
|
## Clarify and launch UI
|
|
@@ -401,7 +403,7 @@ Agent locations, lowest to highest priority:
|
|
|
401
403
|
| User | `~/.pi/agent/agents/**/*.md` |
|
|
402
404
|
| Project | `.pi/agents/**/*.md` |
|
|
403
405
|
|
|
404
|
-
Project discovery also reads legacy `.agents/**/*.md` files. Nested subdirectories are discovered recursively. `.chain.md` files
|
|
406
|
+
Project discovery also reads legacy `.agents/**/*.md` files. Nested subdirectories are discovered recursively. `.chain.md` files do not define agents. If both `.agents/` and `.pi/agents/` define the same parsed runtime agent name, `.pi/agents/` wins. Use `agentScope: "user" | "project" | "both"` to control discovery; `both` is the default and project definitions win runtime-name collisions.
|
|
405
407
|
|
|
406
408
|
Builtin agents load at the lowest priority, so a user or project agent with the same name overrides them. They do not pin a provider model; they inherit your current Pi default model unless you set `subagents.agentOverrides.<name>.model`. `oracle` is an advisory reviewer that critiques direction and proposes an execution prompt without editing files. `worker` is the implementation agent for normal tasks and approved oracle handoffs.
|
|
407
409
|
|
|
@@ -531,14 +533,14 @@ When `extensions` is present, it takes precedence over extension paths implied b
|
|
|
531
533
|
|
|
532
534
|
## Chain files
|
|
533
535
|
|
|
534
|
-
Chains are reusable `.chain.md` workflows stored
|
|
536
|
+
Chains are reusable `.chain.md` workflows stored separately from agent files.
|
|
535
537
|
|
|
536
538
|
| Scope | Path |
|
|
537
539
|
|-------|------|
|
|
538
|
-
| User | `~/.pi/agent/
|
|
539
|
-
| Project | `.pi/
|
|
540
|
+
| User | `~/.pi/agent/chains/**/*.chain.md` |
|
|
541
|
+
| Project | `.pi/chains/**/*.chain.md` |
|
|
540
542
|
|
|
541
|
-
|
|
543
|
+
Nested subdirectories are discovered recursively. If user and project scopes define the same parsed runtime chain name, the project chain wins. Chains support the same optional `package` frontmatter as agents; `name: review-flow` plus `package: code-analysis` runs as `code-analysis.review-flow`.
|
|
542
544
|
|
|
543
545
|
Example:
|
|
544
546
|
|
|
@@ -778,11 +780,12 @@ Status and control actions:
|
|
|
778
780
|
subagent({ action: "status" })
|
|
779
781
|
subagent({ action: "status", id: "<run-id>" })
|
|
780
782
|
subagent({ action: "interrupt", id: "<run-id>" })
|
|
781
|
-
subagent({ action: "resume", id: "<
|
|
783
|
+
subagent({ action: "resume", id: "<run-id>", message: "follow-up question" })
|
|
784
|
+
subagent({ action: "resume", id: "<run-id>", index: 1, message: "follow-up for child 2" })
|
|
782
785
|
subagent({ action: "doctor" })
|
|
783
786
|
```
|
|
784
787
|
|
|
785
|
-
`resume` sends the follow-up directly when
|
|
788
|
+
`resume` sends the follow-up directly when an async child is still reachable over intercom. After completion, it revives the child by starting a new async child from the stored child session file. Multi-child async runs and remembered foreground single, parallel, or chain runs can be revived by passing `index` to choose the child. Revive starts a new child process from the old session context; it does not restart the same OS process, and it requires the chosen child to have a persisted `.jsonl` session file.
|
|
786
789
|
|
|
787
790
|
## Worktree isolation
|
|
788
791
|
|
|
@@ -893,7 +896,7 @@ Fields:
|
|
|
893
896
|
- `mode`: default `always`; use `fork-only` to inject only for forked runs, or `off` to disable the bridge.
|
|
894
897
|
- `instructionFile`: optional Markdown template replacing the default bridge instructions. `{orchestratorTarget}` is interpolated. Relative paths resolve from `~/.pi/agent/extensions/subagent/`.
|
|
895
898
|
|
|
896
|
-
Bridge activation also requires `pi-intercom` to be installed and enabled, a targetable current session name or fallback alias, and `pi-intercom` in any explicit agent `extensions` allowlist.
|
|
899
|
+
Bridge activation also requires `pi-intercom` to be installed and enabled through `pi install npm:pi-intercom` or a legacy local extension checkout, a targetable current session name or fallback alias, and `pi-intercom` in any explicit agent `extensions` allowlist.
|
|
897
900
|
|
|
898
901
|
The default injected guidance tells children to use `contact_supervisor` with `reason: "need_decision"` when blocked or needing a decision, `reason: "progress_update"` only for meaningful blocked/progress updates, generic `intercom` as fallback plumbing, and avoid routine completion handoffs.
|
|
899
902
|
|
package/agents/reviewer.md
CHANGED
|
@@ -7,7 +7,6 @@ systemPromptMode: replace
|
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
|
9
9
|
defaultReads: plan.md, progress.md
|
|
10
|
-
defaultProgress: true
|
|
11
10
|
---
|
|
12
11
|
|
|
13
12
|
You are a disciplined review subagent. Your job is to inspect, evaluate, and report findings with evidence. You do not guess; you verify from the code, tests, docs, or requirements.
|
|
@@ -59,9 +58,10 @@ Review a PR or issue by understanding the context, then verifying:
|
|
|
59
58
|
- Prefer small corrective edits over broad rewrites.
|
|
60
59
|
- If everything looks good, say so plainly.
|
|
61
60
|
- If you are asked to maintain progress, record what you checked and what you found.
|
|
61
|
+
- If review-only or no-edit instructions conflict with progress-writing instructions, review-only/no-edit wins. Do not write `progress.md`; mention the conflict in your final review only if it matters.
|
|
62
62
|
|
|
63
63
|
## Supervisor coordination
|
|
64
|
-
If runtime bridge instructions identify a safe supervisor target and you are blocked or need a decision, use `contact_supervisor` with `reason: "need_decision"` and wait for the reply. Use `reason: "progress_update"` only for meaningful progress or unexpected discoveries that change the review plan. Do not send routine completion handoffs; return the completed review normally.
|
|
64
|
+
If runtime bridge instructions identify a safe supervisor target and you are blocked or need a decision, use `contact_supervisor` with `reason: "need_decision"` and wait for the reply. Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing; no-edit wins. Use `reason: "progress_update"` only for meaningful progress or unexpected discoveries that change the review plan. Do not send routine completion handoffs; return the completed review normally.
|
|
65
65
|
|
|
66
66
|
Fall back to generic `intercom` only if `contact_supervisor` is unavailable and the runtime bridge instructions identify a safe target. If no safe target is discoverable, do not guess.
|
|
67
67
|
|
package/package.json
CHANGED
|
@@ -105,7 +105,7 @@ Use this at the start of non-trivial work. Launch `scout` for local context and
|
|
|
105
105
|
|
|
106
106
|
### Parallel cleanup technique
|
|
107
107
|
|
|
108
|
-
Use this after implementation when the user wants cleanup review or when a final pass would reduce AI-slop. Launch two fresh-context `reviewer` tasks with `output: false`: one deslop pass and one verbosity pass. If the `deslop` or `verbosity-cleaner` skills are available, pass the relevant skill to that reviewer; otherwise inline the criteria. Both reviewers are review-only and should flag concrete issues with severity, file/line references, and smallest safe fixes. The parent decides what to apply and asks before making changes unless cleanup was already authorized.
|
|
108
|
+
Use this after implementation when the user wants cleanup review or when a final pass would reduce AI-slop. Launch two fresh-context `reviewer` tasks with `output: false` and `progress: false`: one deslop pass and one verbosity pass. If the `deslop` or `verbosity-cleaner` skills are available, pass the relevant skill to that reviewer; otherwise inline the criteria. Both reviewers are review-only and should flag concrete issues with severity, file/line references, and smallest safe fixes. Review-only/no-edit beats progress-writing or artifact-writing instructions. The parent decides what to apply and asks before making changes unless cleanup was already authorized.
|
|
109
109
|
|
|
110
110
|
## Builtin Agents
|
|
111
111
|
|
|
@@ -183,11 +183,10 @@ Agent files can live in:
|
|
|
183
183
|
- legacy `.agents/**/*.md` — still read for compatibility, but `.pi/agents/` wins on conflicts
|
|
184
184
|
|
|
185
185
|
Chains live in:
|
|
186
|
-
- `~/.pi/agent/
|
|
187
|
-
- `.pi/
|
|
188
|
-
- legacy `.agents/**/*.chain.md`
|
|
186
|
+
- `~/.pi/agent/chains/**/*.chain.md` — user scope
|
|
187
|
+
- `.pi/chains/**/*.chain.md` — project scope
|
|
189
188
|
|
|
190
|
-
Discovery is recursive. `.chain.md` files
|
|
189
|
+
Discovery is recursive. `.chain.md` files do not define agents. Agents and chains can set optional frontmatter `package: code-analysis`; `name: scout` plus `package: code-analysis` registers as runtime name `code-analysis.scout` while serialization keeps `name` and `package` separate.
|
|
191
190
|
|
|
192
191
|
Precedence is by parsed runtime name:
|
|
193
192
|
1. project scope
|
|
@@ -263,7 +262,9 @@ without forcing each step to rediscover everything.
|
|
|
263
262
|
|
|
264
263
|
### Async/background
|
|
265
264
|
|
|
266
|
-
Use async mode whenever the parent agent should keep working while a child runs. A normal foreground `subagent(...)` call blocks the parent until the child completes; it is appropriate when the next parent step depends on the child result. If you say you will "ask a reviewer while I continue auditing" or otherwise run local work in parallel with a child, launch with `async: true`.
|
|
265
|
+
Use async mode whenever the parent agent should keep working while a child runs. A normal foreground `subagent(...)` call blocks the parent until the child completes; it is appropriate when the next parent step depends on the child result. If you say you will "ask a reviewer while I continue auditing" or otherwise run local work in parallel with a child, launch with `async: true`.
|
|
266
|
+
|
|
267
|
+
Do not end your turn immediately after launching an async child if you promised to keep working. Continue the local inspection or other independent work, then check the async run when its result is needed. If there is no independent work left and you would only be running `sleep` or status polling commands to wait, end your turn instead. Pi will deliver the async completion when it arrives.
|
|
267
268
|
|
|
268
269
|
```typescript
|
|
269
270
|
subagent({
|
|
@@ -289,6 +290,21 @@ const run = subagent({
|
|
|
289
290
|
|
|
290
291
|
Inspect async runs with `subagent({ action: "status", id: "..." })`, `subagent({ action: "status" })` for active runs, or the `/subagents-status` slash command.
|
|
291
292
|
|
|
293
|
+
Use `resume` for follow-up work after a delegated run:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
subagent({ action: "resume", id: "run-id", message: "Follow up on this point." })
|
|
297
|
+
subagent({ action: "resume", id: "run-id", index: 1, message: "Continue reviewer 2." })
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Resume behavior:
|
|
301
|
+
- If an async child is still running and reachable, `resume` sends the follow-up to that live child over intercom.
|
|
302
|
+
- If an async child has completed, `resume` revives it by starting a new async child from the persisted child session file.
|
|
303
|
+
- Multi-child async runs require `index` unless only one running child is selectable.
|
|
304
|
+
- Completed foreground single, parallel, and chain runs can also be revived by `index` while their run metadata remains in extension state.
|
|
305
|
+
- Revive starts a new child process from the old session context; it does not restart the same OS process.
|
|
306
|
+
- If the chosen child has no persisted `.jsonl` session file, resume fails and reports that directly.
|
|
307
|
+
|
|
292
308
|
Use diagnostics when setup or child startup looks wrong:
|
|
293
309
|
|
|
294
310
|
```typescript
|
|
@@ -408,6 +424,8 @@ Use `contact_supervisor` with `reason: "need_decision"` when:
|
|
|
408
424
|
- a child needs clarification instead of guessing
|
|
409
425
|
- an approval, product, API, or scope choice is required before continuing safely
|
|
410
426
|
|
|
427
|
+
Do not use `contact_supervisor` just to resolve review-only/no-edit versus progress-writing or artifact-writing instructions. No-edit wins, and the child should return review findings without touching files.
|
|
428
|
+
|
|
411
429
|
Use `contact_supervisor` with `reason: "progress_update"` when:
|
|
412
430
|
- a child is explicitly asked for progress
|
|
413
431
|
- a meaningful discovery changes the plan
|
|
@@ -459,7 +459,9 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
459
459
|
const scope = scopeRaw as ManagementScope;
|
|
460
460
|
const isChain = hasKey(cfg, "steps");
|
|
461
461
|
const d = discoverAgentsAll(ctx.cwd);
|
|
462
|
-
const targetDir =
|
|
462
|
+
const targetDir = isChain
|
|
463
|
+
? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(ctx.cwd, ".pi", "chains")
|
|
464
|
+
: scope === "user" ? d.userDir : d.projectDir ?? path.join(ctx.cwd, ".pi", "agents");
|
|
463
465
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
464
466
|
if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
|
|
465
467
|
const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
|
package/src/agents/agents.ts
CHANGED
|
@@ -130,6 +130,10 @@ interface AgentDiscoveryResult {
|
|
|
130
130
|
projectAgentsDir: string | null;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
export function getUserChainDir(): string {
|
|
134
|
+
return path.join(os.homedir(), ".pi", "agent", "chains");
|
|
135
|
+
}
|
|
136
|
+
|
|
133
137
|
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
134
138
|
const mcpDirectTools: string[] = [];
|
|
135
139
|
const tools: string[] = [];
|
|
@@ -705,6 +709,17 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
|
|
|
705
709
|
preferredDir,
|
|
706
710
|
};
|
|
707
711
|
}
|
|
712
|
+
|
|
713
|
+
function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
714
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
715
|
+
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
716
|
+
|
|
717
|
+
const preferredDir = path.join(projectRoot, ".pi", "chains");
|
|
718
|
+
return {
|
|
719
|
+
readDirs: isDirectory(preferredDir) ? [preferredDir] : [],
|
|
720
|
+
preferredDir,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
708
723
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
709
724
|
|
|
710
725
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
@@ -742,12 +757,16 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
742
757
|
chains: ChainConfig[];
|
|
743
758
|
userDir: string;
|
|
744
759
|
projectDir: string | null;
|
|
760
|
+
userChainDir: string;
|
|
761
|
+
projectChainDir: string | null;
|
|
745
762
|
userSettingsPath: string;
|
|
746
763
|
projectSettingsPath: string | null;
|
|
747
764
|
} {
|
|
748
765
|
const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
749
766
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
767
|
+
const userChainDir = getUserChainDir();
|
|
750
768
|
const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
|
|
769
|
+
const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(cwd);
|
|
751
770
|
const userSettingsPath = getUserAgentSettingsPath();
|
|
752
771
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
753
772
|
const userSettings = readSubagentSettings(userSettingsPath);
|
|
@@ -773,18 +792,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
773
792
|
const project = Array.from(projectMap.values());
|
|
774
793
|
|
|
775
794
|
const chainMap = new Map<string, ChainConfig>();
|
|
776
|
-
for (const dir of
|
|
795
|
+
for (const dir of projectChainDirs) {
|
|
777
796
|
for (const chain of loadChainsFromDir(dir, "project")) {
|
|
778
797
|
chainMap.set(chain.name, chain);
|
|
779
798
|
}
|
|
780
799
|
}
|
|
781
800
|
const chains = [
|
|
782
|
-
...loadChainsFromDir(
|
|
783
|
-
...loadChainsFromDir(userDirNew, "user"),
|
|
801
|
+
...loadChainsFromDir(userChainDir, "user"),
|
|
784
802
|
...Array.from(chainMap.values()),
|
|
785
803
|
];
|
|
786
804
|
|
|
787
805
|
const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
788
806
|
|
|
789
|
-
return { builtin, user, project, chains, userDir, projectDir, userSettingsPath, projectSettingsPath };
|
|
807
|
+
return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
790
808
|
}
|
package/src/extension/doctor.ts
CHANGED
|
@@ -192,6 +192,7 @@ export function buildDoctorReport(input: DoctorReportInput): string {
|
|
|
192
192
|
config: input.config.intercomBridge,
|
|
193
193
|
context: input.context,
|
|
194
194
|
orchestratorTarget: input.orchestratorTarget,
|
|
195
|
+
cwd: input.cwd,
|
|
195
196
|
}), input.context).join("\n")).split("\n"),
|
|
196
197
|
];
|
|
197
198
|
return lines.join("\n");
|
package/src/extension/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
|
24
24
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
25
25
|
import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, syncResultAnimation } from "../tui/render.ts";
|
|
26
26
|
import { SubagentParams } from "./schemas.ts";
|
|
27
|
-
import { createSubagentExecutor } from "../runs/foreground/subagent-executor.ts";
|
|
27
|
+
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
28
28
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
29
29
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
30
30
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
@@ -245,6 +245,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
245
245
|
baseCwd: process.cwd(),
|
|
246
246
|
currentSessionId: null,
|
|
247
247
|
asyncJobs: new Map(),
|
|
248
|
+
foregroundRuns: new Map(),
|
|
248
249
|
foregroundControls: new Map(),
|
|
249
250
|
lastForegroundControlId: null,
|
|
250
251
|
pendingForegroundControlNotices: new Map(),
|
|
@@ -336,11 +337,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
336
337
|
return new SubagentControlNoticeComponent({ ...details, noticeText: formatSubagentControlNotice(details, content) }, theme);
|
|
337
338
|
});
|
|
338
339
|
|
|
340
|
+
const executeSubagentCollapsed = (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((result: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => {
|
|
341
|
+
if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
|
|
342
|
+
return executor.execute(id, params, signal, onUpdate, ctx);
|
|
343
|
+
};
|
|
344
|
+
|
|
339
345
|
const slashBridge = registerSlashSubagentBridge({
|
|
340
346
|
events: pi.events,
|
|
341
347
|
getContext: () => state.lastUiContext,
|
|
342
348
|
execute: (id, params, signal, onUpdate, ctx) =>
|
|
343
|
-
|
|
349
|
+
executeSubagentCollapsed(id, params, signal, onUpdate, ctx),
|
|
344
350
|
});
|
|
345
351
|
|
|
346
352
|
const promptTemplateBridge = registerPromptTemplateDelegationBridge({
|
|
@@ -348,7 +354,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
348
354
|
getContext: () => state.lastUiContext,
|
|
349
355
|
execute: async (requestId, request, signal, ctx, onUpdate) => {
|
|
350
356
|
if (request.tasks && request.tasks.length > 0) {
|
|
351
|
-
return
|
|
357
|
+
return executeSubagentCollapsed(
|
|
352
358
|
requestId,
|
|
353
359
|
{
|
|
354
360
|
tasks: request.tasks,
|
|
@@ -363,7 +369,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
363
369
|
ctx,
|
|
364
370
|
);
|
|
365
371
|
}
|
|
366
|
-
return
|
|
372
|
+
return executeSubagentCollapsed(
|
|
367
373
|
requestId,
|
|
368
374
|
{
|
|
369
375
|
agent: request.agent,
|
|
@@ -419,14 +425,14 @@ MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
|
419
425
|
CONTROL:
|
|
420
426
|
• { action: "status", id: "..." } - inspect an async/background run by id or prefix
|
|
421
427
|
• { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
|
|
422
|
-
• { action: "resume", id: "...", message: "..." } - follow up with a live async child or revive a completed
|
|
428
|
+
• { action: "resume", id: "...", message: "...", index?: 0 } - follow up with a live async child or revive a completed async/foreground child from its session
|
|
423
429
|
|
|
424
430
|
DIAGNOSTICS:
|
|
425
431
|
• { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom`,
|
|
426
432
|
parameters: SubagentParams,
|
|
427
433
|
|
|
428
434
|
execute(id, params, signal, onUpdate, ctx) {
|
|
429
|
-
return
|
|
435
|
+
return executeSubagentCollapsed(id, params, signal, onUpdate, ctx);
|
|
430
436
|
},
|
|
431
437
|
|
|
432
438
|
renderCall(args, theme) {
|
package/src/extension/schemas.ts
CHANGED
|
@@ -116,7 +116,7 @@ export const SubagentParams = Type.Object({
|
|
|
116
116
|
description: "Async run directory for action='status' or action='resume'."
|
|
117
117
|
})),
|
|
118
118
|
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child." })),
|
|
119
|
-
message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'." })),
|
|
119
|
+
message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'. Use index to choose a child from multi-child runs." })),
|
|
120
120
|
// Chain identifier for management (can't reuse 'chain' — that's the execution array)
|
|
121
121
|
chainName: Type.Optional(Type.String({
|
|
122
122
|
description: "Chain name for get/update/delete management actions"
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
5
6
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
|
|
9
|
+
const CONFIG_DIR = ".pi";
|
|
10
|
+
|
|
11
|
+
function defaultAgentDir(): string {
|
|
12
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
|
|
16
|
+
return path.join(agentDir, "extensions", PI_INTERCOM_PACKAGE_NAME);
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
function defaultIntercomConfigPath(): string {
|
|
12
|
-
return path.join(
|
|
19
|
+
function defaultIntercomConfigPath(agentDir = defaultAgentDir()): string {
|
|
20
|
+
return path.join(agentDir, "intercom", "config.json");
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
function defaultSubagentConfigDir(): string {
|
|
16
|
-
return path.join(
|
|
23
|
+
function defaultSubagentConfigDir(agentDir = defaultAgentDir()): string {
|
|
24
|
+
return path.join(agentDir, "extensions", "subagent");
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
@@ -23,6 +31,7 @@ const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only
|
|
|
23
31
|
Use contact_supervisor first. It resolves the supervisor session "{orchestratorTarget}" and run metadata automatically.
|
|
24
32
|
- Need a decision, blocked, approval, or product/API/scope ambiguity: contact_supervisor({ reason: "need_decision", message: "<question>" })
|
|
25
33
|
- After contact_supervisor with reason "need_decision", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
|
|
34
|
+
- Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions. Review-only/no-edit wins; leave files unchanged and mention the conflict in your final result only if it matters.
|
|
26
35
|
- Meaningful progress or unexpected discoveries that change the plan: contact_supervisor({ reason: "progress_update", message: "UPDATE: <summary>" })
|
|
27
36
|
- Generic intercom is lower-level plumbing/fallback only: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
28
37
|
|
|
@@ -56,6 +65,9 @@ interface ResolveIntercomBridgeInput {
|
|
|
56
65
|
extensionDir?: string;
|
|
57
66
|
configPath?: string;
|
|
58
67
|
settingsDir?: string;
|
|
68
|
+
cwd?: string;
|
|
69
|
+
agentDir?: string;
|
|
70
|
+
globalNpmRoot?: string | null;
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
|
|
@@ -102,6 +114,121 @@ function intercomConfigStatus(configPath: string): { enabled: boolean; error?: u
|
|
|
102
114
|
}
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
function readJsonBestEffort(filePath: string): unknown {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const code = error && typeof error === "object" && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
|
|
122
|
+
if (code !== "ENOENT") console.warn(`Failed to read JSON from '${filePath}'.`, error);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function packageHasPiExtension(packageRoot: string): boolean {
|
|
128
|
+
if (!fs.existsSync(packageRoot)) return false;
|
|
129
|
+
const pkg = readJsonBestEffort(path.join(packageRoot, "package.json"));
|
|
130
|
+
if (pkg && typeof pkg === "object" && !Array.isArray(pkg)) {
|
|
131
|
+
const pi = (pkg as { pi?: unknown }).pi;
|
|
132
|
+
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
133
|
+
const extensions = (pi as { extensions?: unknown }).extensions;
|
|
134
|
+
return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return fs.existsSync(path.join(packageRoot, "extensions"));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isSafePackagePath(value: string): boolean {
|
|
141
|
+
return value.length > 0
|
|
142
|
+
&& !path.isAbsolute(value)
|
|
143
|
+
&& value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseNpmPackageName(source: string): string | undefined {
|
|
147
|
+
const spec = source.slice(4).trim();
|
|
148
|
+
if (!spec) return undefined;
|
|
149
|
+
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
|
|
150
|
+
const packageName = match?.[1] ?? spec;
|
|
151
|
+
return isSafePackagePath(packageName) ? packageName : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function packageEntrySource(entry: unknown): string | undefined {
|
|
155
|
+
if (typeof entry === "string") return entry;
|
|
156
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry) && typeof (entry as { source?: unknown }).source === "string") {
|
|
157
|
+
return (entry as { source: string }).source;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function packageEntryAllowsExtensions(entry: unknown): boolean {
|
|
163
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return true;
|
|
164
|
+
const extensions = (entry as { extensions?: unknown }).extensions;
|
|
165
|
+
return !Array.isArray(extensions) || extensions.length > 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function findNearestProjectConfigDir(cwd: string): string | undefined {
|
|
169
|
+
let current = path.resolve(cwd);
|
|
170
|
+
while (true) {
|
|
171
|
+
const configDir = path.join(current, CONFIG_DIR);
|
|
172
|
+
if (fs.existsSync(path.join(configDir, "settings.json"))) return configDir;
|
|
173
|
+
const parent = path.dirname(current);
|
|
174
|
+
if (parent === current) return undefined;
|
|
175
|
+
current = parent;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let cachedGlobalNpmRoot: string | null | undefined;
|
|
180
|
+
|
|
181
|
+
function getGlobalNpmRoot(): string | null {
|
|
182
|
+
if (cachedGlobalNpmRoot !== undefined) return cachedGlobalNpmRoot;
|
|
183
|
+
try {
|
|
184
|
+
cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
185
|
+
return cachedGlobalNpmRoot;
|
|
186
|
+
} catch {
|
|
187
|
+
cachedGlobalNpmRoot = null;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function configuredPiIntercomPackageDir(input: ResolveIntercomBridgeInput, agentDir: string): string | undefined {
|
|
193
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
194
|
+
const projectConfigDir = findNearestProjectConfigDir(cwd);
|
|
195
|
+
const settingsFiles = [
|
|
196
|
+
...(projectConfigDir ? [{ file: path.join(projectConfigDir, "settings.json"), configDir: projectConfigDir, scope: "project" as const }] : []),
|
|
197
|
+
{ file: path.join(agentDir, "settings.json"), configDir: agentDir, scope: "user" as const },
|
|
198
|
+
];
|
|
199
|
+
const globalNpmRoot = input.globalNpmRoot === undefined ? getGlobalNpmRoot() : input.globalNpmRoot;
|
|
200
|
+
|
|
201
|
+
for (const { file, configDir, scope } of settingsFiles) {
|
|
202
|
+
const settings = readJsonBestEffort(file);
|
|
203
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue;
|
|
204
|
+
const packages = (settings as { packages?: unknown }).packages;
|
|
205
|
+
if (!Array.isArray(packages)) continue;
|
|
206
|
+
|
|
207
|
+
for (const entry of packages) {
|
|
208
|
+
if (!packageEntryAllowsExtensions(entry)) continue;
|
|
209
|
+
const source = packageEntrySource(entry)?.trim();
|
|
210
|
+
if (!source?.startsWith("npm:")) continue;
|
|
211
|
+
const packageName = parseNpmPackageName(source);
|
|
212
|
+
if (packageName !== PI_INTERCOM_PACKAGE_NAME) continue;
|
|
213
|
+
const candidates = scope === "project"
|
|
214
|
+
? [path.join(configDir, "npm", "node_modules", packageName)]
|
|
215
|
+
: [
|
|
216
|
+
...(globalNpmRoot ? [path.join(globalNpmRoot, packageName)] : []),
|
|
217
|
+
path.join(agentDir, "npm", "node_modules", packageName),
|
|
218
|
+
];
|
|
219
|
+
const packageRoot = candidates.find(packageHasPiExtension);
|
|
220
|
+
if (packageRoot) return path.resolve(packageRoot);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveIntercomExtensionDir(input: ResolveIntercomBridgeInput, agentDir: string): string {
|
|
227
|
+
const legacyDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir(agentDir));
|
|
228
|
+
if (fs.existsSync(legacyDir)) return legacyDir;
|
|
229
|
+
return configuredPiIntercomPackageDir(input, agentDir) ?? legacyDir;
|
|
230
|
+
}
|
|
231
|
+
|
|
105
232
|
function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extensionDir: string): boolean {
|
|
106
233
|
if (extensions === undefined) return true;
|
|
107
234
|
|
|
@@ -145,9 +272,10 @@ ${instruction}`;
|
|
|
145
272
|
export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeDiagnostic {
|
|
146
273
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
147
274
|
const mode = config.mode;
|
|
148
|
-
const
|
|
275
|
+
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
276
|
+
const extensionDir = resolveIntercomExtensionDir(input, agentDir);
|
|
149
277
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
150
|
-
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
|
|
278
|
+
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath(agentDir));
|
|
151
279
|
const wantsIntercom = mode !== "off" && !(mode === "fork-only" && input.context !== "fork");
|
|
152
280
|
const piIntercomAvailable = fs.existsSync(extensionDir);
|
|
153
281
|
let configStatus: ReturnType<typeof intercomConfigStatus> | undefined;
|
|
@@ -183,9 +311,10 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
|
|
|
183
311
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
184
312
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
185
313
|
const mode = config.mode;
|
|
186
|
-
const
|
|
314
|
+
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
315
|
+
const extensionDir = resolveIntercomExtensionDir(input, agentDir);
|
|
187
316
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
188
|
-
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir());
|
|
317
|
+
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir(agentDir));
|
|
189
318
|
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
190
319
|
orchestratorTarget || "{orchestratorTarget}",
|
|
191
320
|
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
@@ -204,7 +333,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
204
333
|
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
205
334
|
}
|
|
206
335
|
|
|
207
|
-
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
|
|
336
|
+
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath(agentDir));
|
|
208
337
|
const intercomStatus = intercomConfigStatus(configPath);
|
|
209
338
|
if (intercomStatus.error) console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, intercomStatus.error);
|
|
210
339
|
if (!intercomStatus.enabled) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import {
|
|
3
4
|
type Details,
|
|
4
5
|
type IntercomEventBus,
|
|
@@ -76,11 +77,15 @@ function asyncResumeGuidance(input: {
|
|
|
76
77
|
asyncId?: string;
|
|
77
78
|
}): string | undefined {
|
|
78
79
|
if (input.source !== "async" || !input.asyncId) return undefined;
|
|
79
|
-
|
|
80
|
+
const resumable = input.children.filter((child) => typeof child.sessionPath === "string" && fs.existsSync(child.sessionPath));
|
|
81
|
+
if (input.children.length === 1 && resumable.length === 1) {
|
|
80
82
|
return `Revive: subagent({ action: "resume", id: "${input.asyncId}", message: "..." })`;
|
|
81
83
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
+
if (resumable.length > 0) {
|
|
85
|
+
const firstIndex = resumable[0]?.index ?? input.children.indexOf(resumable[0]!);
|
|
86
|
+
return `Revive child: subagent({ action: "resume", id: "${input.asyncId}", index: ${firstIndex}, message: "..." })`;
|
|
87
|
+
}
|
|
88
|
+
return "Resume: unavailable; no child session file was persisted.";
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
function formatSubagentResultIntercomMessage(input: {
|