pi-subagents 0.24.4 → 0.25.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/CHANGELOG.md +8 -0
- package/README.md +16 -10
- package/package.json +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +6 -2
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +101 -4
- package/src/runs/background/async-job-tracker.ts +41 -6
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +60 -30
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +79 -3
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +48 -11
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +310 -14
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/pi-args.ts +62 -5
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/types.ts +95 -0
- package/src/tui/render.ts +107 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.25.0] - 2026-05-21
|
|
6
|
+
|
|
5
7
|
### Added
|
|
8
|
+
- Allow child agents whose resolved builtin tools explicitly include `subagent` to run child-safe nested fanout, with parent-visible nested status trees and nested `status`/`interrupt`/`resume` by id.
|
|
6
9
|
|
|
7
10
|
### Fixed
|
|
11
|
+
- Preserve compact nested child summaries in grouped result/intercom payloads and async completion metadata before ordinary result files are processed and deleted.
|
|
12
|
+
- Keep async result files retryable when nested registry enrichment temporarily fails, instead of marking them seen before a successful delivery pass.
|
|
13
|
+
- Require an explicit id for child-safe nested `status` when no local foreground run is active, preventing fanout children from listing unrelated top-level async runs.
|
|
14
|
+
- Keep fanout child control inbox polling alive across transient filesystem errors, and retain control requests for retry when control-result writes fail.
|
|
15
|
+
- Share nested path/env sanitization between child launch arguments and nested event projection.
|
|
8
16
|
|
|
9
17
|
## [0.24.4] - 2026-05-20
|
|
10
18
|
|
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Foreground runs stream progress in the conversation while they run.
|
|
|
149
149
|
|
|
150
150
|
Background runs keep working after control returns to you. Inspect active runs with `subagent({ action: "status" })`, or a specific run with `subagent({ action: "status", id: "..." })`.
|
|
151
151
|
|
|
152
|
-
They also show a compact async widget and send completion notifications. Parallel background runs show per-agent progress instead of fake chain steps. Chains with parallel groups keep their grouped shape in progress and results, so failed or paused agents stay visible next to completed ones.
|
|
152
|
+
They also show a compact async widget and send completion notifications. Parallel background runs show per-agent progress instead of fake chain steps. Chains with parallel groups keep their grouped shape in progress and results, so failed or paused agents stay visible next to completed ones. When a child is explicitly allowed to fan out with `tools: subagent`, its nested runs appear under that parent child in the main status tree instead of being hidden inside the child process.
|
|
153
153
|
|
|
154
154
|
You can also ask naturally:
|
|
155
155
|
|
|
@@ -181,7 +181,7 @@ Use the optional prompt shortcuts below when you want the pattern to be repeatab
|
|
|
181
181
|
|
|
182
182
|
Packaged `planner`, `worker`, and `oracle` default to forked context when a launch omits `context`; pass `context: "fresh"` when you intentionally want a fresh child run.
|
|
183
183
|
|
|
184
|
-
Child-safety boundaries are enforced at runtime. Spawned child sessions do not
|
|
184
|
+
Child-safety boundaries are enforced at runtime. Spawned child sessions do not receive the bundled `pi-subagents` skill, and forked child context filtering removes parent-only subagent artifacts (including old hidden orchestration-instruction messages, slash/status/control messages, and prior parent `subagent` tool-call/tool-result history) while preserving ordinary prose and unrelated tool calls/results. By default, children do not register the `subagent` tool and receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents. The explicit exception is an agent whose resolved builtin `tools` includes `subagent`; that child gets a child-safe `subagent` tool for the fanout work the parent assigned, still bounded by `maxSubagentDepth`.
|
|
185
185
|
|
|
186
186
|
## Optional shortcuts
|
|
187
187
|
|
|
@@ -223,7 +223,7 @@ The child can use one dedicated coordination tool:
|
|
|
223
223
|
|
|
224
224
|
- `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.
|
|
225
225
|
|
|
226
|
-
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
|
|
226
|
+
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, full child summaries, and compact nested child summaries under the parent child that launched them.
|
|
227
227
|
|
|
228
228
|
If a child appears stalled, needs-attention notices can show up in the parent session with useful next actions, such as checking `subagent({ action: "status" })`, interrupting the run, or nudging the child.
|
|
229
229
|
|
|
@@ -472,8 +472,9 @@ Examples:
|
|
|
472
472
|
- `tools` omitted and `extensions` omitted: normal builtins and normal extensions.
|
|
473
473
|
- `tools: mcp:chrome-devtools`: normal builtins plus direct Chrome DevTools MCP tools.
|
|
474
474
|
- `tools: read, bash, mcp:chrome-devtools`: only `read` and `bash` as builtins, plus direct Chrome DevTools MCP tools.
|
|
475
|
+
- `tools: subagent, read`: a child-safe `subagent` tool is available inside that child so it can run explicitly assigned nested fanout.
|
|
475
476
|
|
|
476
|
-
Direct MCP tools require [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter). Subagents only receive direct MCP tools when `mcp:` entries are listed in their frontmatter; global `directTools: true` in `mcp.json` is not enough by itself. The generic `mcp` proxy tool can still be used for discovery when available. The adapter caches tool metadata at startup, so after connecting a new MCP server for the first time, restart Pi before relying on direct tools.
|
|
477
|
+
Direct MCP tools require [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter). Subagents only receive direct MCP tools when `mcp:` entries are listed in their frontmatter; global `directTools: true` in `mcp.json` is not enough by itself. The generic `mcp` proxy tool can still be used for discovery when available. The adapter caches tool metadata at startup, so after connecting a new MCP server for the first time, restart Pi before relying on direct tools. An `mcp:` entry named `subagent` does not authorize nested fanout; only the builtin `subagent` tool name does.
|
|
477
478
|
|
|
478
479
|
`extensions` controls child extension loading:
|
|
479
480
|
|
|
@@ -593,7 +594,7 @@ What the bundled skill covers:
|
|
|
593
594
|
- **Delegation patterns**: when to launch which agent, whether to use single, parallel, chain, or async mode, and whether to use fresh or forked context
|
|
594
595
|
- **Prompt workflow recipes**: how to apply the packaged techniques directly with `subagent(...)` when the user describes the workflow in natural language instead of invoking a slash command. This includes parallel review, review-loop, parallel research, parallel context-build, parallel handoff-plan, gather-context-and-clarify, and parallel cleanup
|
|
595
596
|
- **Role-agent prompting guidance**: compact contract prompts instead of long scripts, what to include in role-specific meta prompts, and retrieval budgets for researchers
|
|
596
|
-
- **Safety boundaries**: child agents must not run subagents
|
|
597
|
+
- **Safety boundaries**: child agents must not run subagents unless their resolved builtin tools explicitly include `subagent`, must not invent intercom targets, and must escalate unapproved decisions
|
|
597
598
|
- **Intercom conventions**: when to ask vs send, and how parent-side result delivery works with `pi-intercom`
|
|
598
599
|
- **Control and diagnostics**: attention signals, soft interrupts, status, and the `doctor` action
|
|
599
600
|
|
|
@@ -737,13 +738,18 @@ Status and control actions:
|
|
|
737
738
|
```ts
|
|
738
739
|
subagent({ action: "status" })
|
|
739
740
|
subagent({ action: "status", id: "<run-id>" })
|
|
741
|
+
subagent({ action: "status", id: "<nested-run-id>" })
|
|
740
742
|
subagent({ action: "interrupt", id: "<run-id>" })
|
|
743
|
+
subagent({ action: "interrupt", id: "<nested-run-id>" })
|
|
741
744
|
subagent({ action: "resume", id: "<run-id>", message: "follow-up question" })
|
|
742
745
|
subagent({ action: "resume", id: "<run-id>", index: 1, message: "follow-up for child 2" })
|
|
746
|
+
subagent({ action: "resume", id: "<nested-run-id>", message: "follow-up for a nested child" })
|
|
743
747
|
subagent({ action: "doctor" })
|
|
744
748
|
```
|
|
745
749
|
|
|
746
|
-
`
|
|
750
|
+
`status` resolves exact foreground ids, top-level async ids, and nested run ids before falling back to prefix matching. Nested status shows the root/parent path, nested children, session/artifact paths when known, and nested control commands. Inside child-safe fanout mode, bare `status` requires an id when no local foreground run is active, so children cannot enumerate unrelated top-level async runs. Bare `interrupt` still targets only the visible top-level run; interrupting a nested run requires its explicit nested id.
|
|
751
|
+
|
|
752
|
+
`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. Nested runs can be resumed by nested id when their live route or persisted session metadata is available. 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.
|
|
747
753
|
|
|
748
754
|
## Worktree isolation
|
|
749
755
|
|
|
@@ -822,7 +828,7 @@ Session directory precedence is: `params.sessionDir`, then `config.defaultSessio
|
|
|
822
828
|
{ "maxSubagentDepth": 1 }
|
|
823
829
|
```
|
|
824
830
|
|
|
825
|
-
Controls nested delegation when no inherited `PI_SUBAGENT_MAX_DEPTH` is already in effect. Per-agent `maxSubagentDepth` can tighten the limit for that agent’s child runs, but cannot relax an inherited stricter limit.
|
|
831
|
+
Controls nested delegation when no inherited `PI_SUBAGENT_MAX_DEPTH` is already in effect. Per-agent `maxSubagentDepth` can tighten the limit for that agent’s child runs, but cannot relax an inherited stricter limit. This applies even to children that explicitly declare `tools: subagent`; at the cap, execution fanout is blocked instead of silently hiding nested work.
|
|
826
832
|
|
|
827
833
|
### `intercomBridge`
|
|
828
834
|
|
|
@@ -898,7 +904,7 @@ Async runs write:
|
|
|
898
904
|
subagent-log-<id>.md
|
|
899
905
|
```
|
|
900
906
|
|
|
901
|
-
`status.json` powers the widget and `subagent({ action: "status" })` output. `events.jsonl` contains wrapper events plus child Pi JSON events annotated with run and step metadata. `output-<n>.log` is a live human-readable tail. Fallback information is persisted so background runs are debuggable after completion.
|
|
907
|
+
`status.json` powers the widget and `subagent({ action: "status" })` output. `events.jsonl` contains wrapper events plus child Pi JSON events annotated with run and step metadata. Nested fanout status is stored as compact sidecar event/registry metadata and merged into parent status views and result/intercom payloads; full recursive status snapshots are not embedded in parent result files. `output-<n>.log` is a live human-readable tail. Fallback information is persisted so background runs are debuggable after completion.
|
|
902
908
|
|
|
903
909
|
## Live progress
|
|
904
910
|
|
|
@@ -920,9 +926,9 @@ This is disabled by default. Session data may contain source code, paths, enviro
|
|
|
920
926
|
|
|
921
927
|
## Recursion guard
|
|
922
928
|
|
|
923
|
-
Subagents can call `subagent
|
|
929
|
+
Subagents can call `subagent` only when their resolved builtin tools explicitly include `subagent`. That is meant for delegated fanout agents, not ordinary worker/reviewer children. A depth guard prevents unbounded nesting.
|
|
924
930
|
|
|
925
|
-
By default, nesting is limited to two levels: main session → subagent → sub-subagent. Deeper calls are blocked with guidance to complete the current task directly.
|
|
931
|
+
By default, nesting is limited to two levels: main session → subagent → sub-subagent. Deeper calls are blocked with guidance to complete the current task directly. Nested runs appear in the parent status widget and `status` output as a tree, and `status`, `interrupt`, and `resume` can target a nested run by its id.
|
|
926
932
|
|
|
927
933
|
Configure the limit with:
|
|
928
934
|
|
package/package.json
CHANGED
package/prompts/review-loop.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Review/fix loop until clean
|
|
|
4
4
|
|
|
5
5
|
Run a parent-orchestrated review loop for the requested work.
|
|
6
6
|
|
|
7
|
-
Use the `subagent` tool. Keep the parent session as the loop controller and final decision-maker. Child subagents must receive concrete role-specific tasks; they must not run subagents or manage the loop themselves.
|
|
7
|
+
Use the `subagent` tool. Keep the parent session as the loop controller and final decision-maker. Child subagents must receive concrete role-specific tasks; they must not run subagents or manage the loop themselves unless the parent intentionally selected an explicit fanout agent whose builtin `tools` includes `subagent` for that assigned fanout.
|
|
8
8
|
|
|
9
9
|
Default to a maximum of 3 review rounds unless I specify a different cap. Count a review round each time fresh-context reviewers inspect the current diff after a worker pass. Stop early when reviewers find no blockers or fixes worth doing now.
|
|
10
10
|
|
|
@@ -10,7 +10,7 @@ description: |
|
|
|
10
10
|
|
|
11
11
|
# Pi Subagents
|
|
12
12
|
|
|
13
|
-
This skill is for the main parent orchestrator only. Do not inject or follow it inside spawned child subagents. The parent session owns delegation, orchestration, review fanout, and final fix-worker launches; child subagents should receive concrete role-specific tasks
|
|
13
|
+
This skill is for the main parent orchestrator only. Do not inject or follow it inside spawned child subagents. The parent session owns delegation, orchestration, review fanout, and final fix-worker launches; child subagents should receive concrete role-specific tasks. Ordinary children should not run their own subagent workflows; the explicit exception is a delegated fanout child whose resolved builtin `tools` includes `subagent`, and that child may use `subagent` only for the fanout work the parent assigned.
|
|
14
14
|
|
|
15
15
|
Use this skill when the parent orchestrator needs to launch a specialized subagent, compose multiple agents into a workflow, or create/edit agents and chains on demand.
|
|
16
16
|
|
|
@@ -108,7 +108,38 @@ Use this at the start of non-trivial work. Launch `scout` for local context and
|
|
|
108
108
|
|
|
109
109
|
### Parallel cleanup technique
|
|
110
110
|
|
|
111
|
-
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.
|
|
111
|
+
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. Phrase the constraint as “Do not modify project/source files; returning findings through the configured output artifact is allowed” when you use `output` or `outputMode: "file-only"`. The parent decides what to apply and asks before making changes unless cleanup was already authorized.
|
|
112
|
+
|
|
113
|
+
### Staged fix orchestration technique
|
|
114
|
+
|
|
115
|
+
Use this when a broad diff has known reviewer findings across several items and the user wants the parent to “orchestrate subagents like a boss.” Keep the active worktree safe with a three-stage chain:
|
|
116
|
+
|
|
117
|
+
1. A parallel read-only planning fanout, one planner/reviewer per issue cluster. Each child inspects the real diff and returns exact files, line refs, proposed fixes, and focused validation. They must not edit.
|
|
118
|
+
2. One writer worker. It receives the planner summaries through `{previous}`, the parent’s accepted scope, stop rules, and verification contract. It is the only child allowed to edit the active worktree.
|
|
119
|
+
3. A parallel read-only validation fanout. Validators inspect the worker diff from fresh context with distinct angles, report pass/fail, remaining blockers, and missing verification.
|
|
120
|
+
|
|
121
|
+
Prefer `async: true`, `context: "fresh"` for planners/validators, `outputMode: "file-only"` for large summaries, and per-stage output names that will not collide. Use this pattern instead of launching several writer workers into a dirty worktree. Include non-blocking suggestions in the writer prompt only when they are small, safe, and do not expand product scope; otherwise record them as deferred.
|
|
122
|
+
|
|
123
|
+
Example shape:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
subagent({
|
|
127
|
+
async: true,
|
|
128
|
+
context: "fresh",
|
|
129
|
+
chain: [
|
|
130
|
+
{ parallel: [
|
|
131
|
+
{ agent: "reviewer", task: "Plan fixes for deploy docs/workflow. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/deploy.md", outputMode: "file-only" },
|
|
132
|
+
{ agent: "reviewer", task: "Plan fixes for scheduler contract. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/scheduler.md", outputMode: "file-only" },
|
|
133
|
+
{ agent: "reviewer", task: "Plan fixes for sandbox/security. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/sandbox.md", outputMode: "file-only" }
|
|
134
|
+
], concurrency: 3 },
|
|
135
|
+
{ agent: "worker", task: "Apply only the accepted fixes from these planning summaries. You are the sole writer for the active worktree. Run focused validation and report changed files, commands, failures, and remaining issues.\n\nPlanning summaries:\n{previous}", output: "worker/fixes.md", outputMode: "file-only", progress: true },
|
|
136
|
+
{ parallel: [
|
|
137
|
+
{ agent: "reviewer", task: "Validate the post-worker diff for deploy and scheduler fixes. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "validation/deploy-scheduler.md", outputMode: "file-only" },
|
|
138
|
+
{ agent: "reviewer", task: "Validate the post-worker diff for sandbox/security fixes. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "validation/sandbox.md", outputMode: "file-only" }
|
|
139
|
+
], concurrency: 2 }
|
|
140
|
+
]
|
|
141
|
+
})
|
|
142
|
+
```
|
|
112
143
|
|
|
113
144
|
## Builtin Agents
|
|
114
145
|
|
|
@@ -144,7 +175,7 @@ A strong subagent prompt usually includes:
|
|
|
144
175
|
- **Goal**: the concrete outcome the child should produce.
|
|
145
176
|
- **Context/evidence**: relevant plan paths, files, diffs, decisions, or user constraints already approved.
|
|
146
177
|
- **Success criteria**: what must be true before the child can finish.
|
|
147
|
-
- **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents, or escalation for unapproved decisions.
|
|
178
|
+
- **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents unless it is an explicitly assigned `tools: subagent` fanout child, or escalation for unapproved decisions.
|
|
148
179
|
- **Validation**: targeted checks to run, or the next-best check when validation is impossible.
|
|
149
180
|
- **Output**: the expected summary shape, artifact path, or finding format.
|
|
150
181
|
- **Stop rules**: when to ask via `intercom`, when to stop after enough evidence, and when not to keep searching.
|
|
@@ -245,7 +276,7 @@ subagent({
|
|
|
245
276
|
})
|
|
246
277
|
```
|
|
247
278
|
|
|
248
|
-
Avoid duplicate output paths in parallel tasks. Concurrent children should not write to the same file. For large saved outputs, set `outputMode: "file-only"` together with an `output` path. The parent result then contains only a compact reference like `Output saved to: /abs/report.md (48.2 KB, 2847 lines). Read this file if needed.` instead of the full saved content. Do not use `output: false` for this; `output: false` means no file output. Failed runs and save errors still return inline details for debugging.
|
|
279
|
+
Avoid duplicate output paths in parallel tasks. Concurrent children should not write to the same file. For large saved outputs, set `outputMode: "file-only"` together with an `output` path. The parent result then contains only a compact reference like `Output saved to: /abs/report.md (48.2 KB, 2847 lines). Read this file if needed.` instead of the full saved content. Do not use `output: false` for this; `output: false` means no file output. When a task is review-only, say “do not modify project/source files” rather than “do not write files” if you also configured `output`; otherwise the child may treat the output artifact as forbidden. Failed runs and save errors still return inline details for debugging.
|
|
249
280
|
|
|
250
281
|
### Chain execution
|
|
251
282
|
|
|
@@ -293,13 +324,14 @@ const run = subagent({
|
|
|
293
324
|
// Continue local inspection, then later call status with the returned id.
|
|
294
325
|
```
|
|
295
326
|
|
|
296
|
-
Inspect async runs with `subagent({ action: "status", id: "..." })` or `subagent({ action: "status" })` for active runs.
|
|
327
|
+
Inspect async runs with `subagent({ action: "status", id: "..." })` or `subagent({ action: "status" })` for active runs. If a delegated fanout child launches nested runs, the parent status view shows them as a tree and you can target a nested run directly with its nested id.
|
|
297
328
|
|
|
298
329
|
Use `resume` for follow-up work after a delegated run:
|
|
299
330
|
|
|
300
331
|
```typescript
|
|
301
332
|
subagent({ action: "resume", id: "run-id", message: "Follow up on this point." })
|
|
302
333
|
subagent({ action: "resume", id: "run-id", index: 1, message: "Continue reviewer 2." })
|
|
334
|
+
subagent({ action: "resume", id: "nested-run-id", message: "Continue this nested reviewer." })
|
|
303
335
|
```
|
|
304
336
|
|
|
305
337
|
Resume behavior:
|
|
@@ -307,6 +339,7 @@ Resume behavior:
|
|
|
307
339
|
- If an async child has completed, `resume` revives it by starting a new async child from the persisted child session file.
|
|
308
340
|
- Multi-child async runs require `index` unless only one running child is selectable.
|
|
309
341
|
- Completed foreground single, parallel, and chain runs can also be revived by `index` while their run metadata remains in extension state.
|
|
342
|
+
- Nested runs can be resumed by nested id when a live route or persisted nested session metadata is available.
|
|
310
343
|
- Revive starts a new child process from the old session context; it does not restart the same OS process.
|
|
311
344
|
- If the chosen child has no persisted `.jsonl` session file, resume fails and reports that directly.
|
|
312
345
|
|
|
@@ -330,13 +363,14 @@ Use soft interrupt when a child is clearly blocked or drifting and the parent ne
|
|
|
330
363
|
subagent({ action: "interrupt" })
|
|
331
364
|
```
|
|
332
365
|
|
|
333
|
-
Pass `id` when targeting a specific controllable run:
|
|
366
|
+
Pass `id` when targeting a specific controllable run, including a nested run shown in the parent status tree:
|
|
334
367
|
|
|
335
368
|
```typescript
|
|
336
369
|
subagent({ action: "interrupt", id: "abc123" })
|
|
370
|
+
subagent({ action: "interrupt", id: "nested-run-id" })
|
|
337
371
|
```
|
|
338
372
|
|
|
339
|
-
A soft interrupt cancels the current child turn and leaves the run paused. It does not mean the delegated task succeeded or failed. After an interrupt, decide the next explicit action: resume with clearer instructions, replace the task, ask the user, or stop the workflow.
|
|
373
|
+
A soft interrupt cancels the current child turn and leaves the run paused. It does not mean the delegated task succeeded or failed. Bare `interrupt` does not target hidden nested descendants; use the explicit nested id. After an interrupt, decide the next explicit action: resume with clearer instructions, replace the task, ask the user, or stop the workflow.
|
|
340
374
|
|
|
341
375
|
Per-run control thresholds can be overridden when a task legitimately runs without observable output for longer than usual:
|
|
342
376
|
|
|
@@ -430,7 +464,7 @@ Use `contact_supervisor` with `reason: "need_decision"` when:
|
|
|
430
464
|
- a child needs clarification instead of guessing
|
|
431
465
|
- an approval, product, API, or scope choice is required before continuing safely
|
|
432
466
|
|
|
433
|
-
Do not use `contact_supervisor` just to resolve review-only/no-edit versus progress-writing or artifact
|
|
467
|
+
Do not use `contact_supervisor` just to resolve review-only/no-project-edit versus progress-writing or output-artifact instructions. The child must not modify project/source files, but returning findings through its normal response or configured output artifact is allowed unless the parent explicitly set `output: false`.
|
|
434
468
|
|
|
435
469
|
Use `contact_supervisor` with `reason: "progress_update"` when:
|
|
436
470
|
- a child is explicitly asked for progress
|
|
@@ -440,7 +474,7 @@ Use `contact_supervisor` with `reason: "progress_update"` when:
|
|
|
440
474
|
Message conventions:
|
|
441
475
|
- `reason: "need_decision"` waits for the parent reply and returns it to the child.
|
|
442
476
|
- `reason: "progress_update"` is non-blocking and should stay concise.
|
|
443
|
-
- Child-side routine completion handoffs are not expected. With the intercom bridge active, parent-side `pi-subagents` sends grouped completion results through `pi-intercom`: one grouped message per foreground parent 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
|
|
477
|
+
- Child-side routine completion handoffs are not expected. With the intercom bridge active, parent-side `pi-subagents` sends grouped completion results through `pi-intercom`: one grouped message per foreground parent 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, full child summaries, and compact nested summaries under the parent child that launched them.
|
|
444
478
|
|
|
445
479
|
If bridge instructions provide the child-facing tool, a child can ask:
|
|
446
480
|
|
|
@@ -656,9 +690,11 @@ The first `worker` implements the approved plan. The parent continues with indep
|
|
|
656
690
|
|
|
657
691
|
For complex work, risky changes, broad refactors, or many changed lines, increase review and validation fanout rather than trusting one reviewer. Use distinct angles such as correctness/regressions, tests/validation, simplicity/maintainability, security/privacy, performance, docs/API contracts, and user-flow behavior. When reviewers find non-trivial issues or the fix worker touches many lines, run another focused review round before final validation.
|
|
658
692
|
|
|
693
|
+
When review has already produced concrete findings across several independent areas, use staged fix orchestration: parallel read-only planners for each issue cluster, one sole writer worker for the active worktree, then parallel fresh-context validators. This is the safest way to handle a dirty worktree with many prior changes because it parallelizes judgment without parallelizing writes. Non-blocking suggestions may go into the writer prompt only if they are small, safe, and inside the approved scope; otherwise defer them explicitly.
|
|
694
|
+
|
|
659
695
|
For very large work, split into serial milestones instead of launching a swarm of writers. Each milestone gets one writer, a validation contract, fresh-context review/validation, a fix pass, and parent acceptance before the next milestone starts. Use parallel subagents inside a milestone for read-only context, research, review, and validation only.
|
|
660
696
|
|
|
661
|
-
Keep orchestration authority in the parent session. Child subagents should not launch more subagents, read this skill, or run their own orchestration loops
|
|
697
|
+
Keep orchestration authority in the parent session. Child subagents should not launch more subagents, read this skill, or run their own orchestration loops unless the parent intentionally selected a fanout agent whose builtin `tools` includes `subagent`. Spawned subagents do not receive the `pi-subagents` skill, parent-only status/control/slash messages, or prior parent `subagent` tool-call/tool-result artifacts. Ordinary children also do not receive the `subagent` extension tool. Child context filtering strips old hidden orchestration-instruction messages when they appear in inherited history. Every child receives a boundary instruction: ordinary children are told the parent owns orchestration and they must not propose or run subagents; explicit fanout children are told to use `subagent` only for the assigned fanout work, with `maxSubagentDepth` still enforced. Implementation children must call real edit/write tools instead of printing pseudo tool calls. Pass children concrete role-specific work instead.
|
|
662
698
|
|
|
663
699
|
1. Clarify first. This is mandatory. Gather code context with `scout` or `context-builder`, add `researcher` only when external evidence matters, then ask the user clarifying questions with `interview` until scope, acceptance criteria, constraints, and non-goals are clear.
|
|
664
700
|
2. Define the validation contract. State what done means before implementation: expected behavior, checks to run, user flows to exercise, and evidence required in the worker handoff. For UI, CLI, integration, or workflow changes, include at least one validator angle that uses the product the way a user would rather than only reading code.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { discoverAgents } from "../agents/agents.ts";
|
|
6
|
+
import { getArtifactsDir } from "../shared/artifacts.ts";
|
|
7
|
+
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
8
|
+
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
9
|
+
import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
10
|
+
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
11
|
+
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
12
|
+
import { SubagentParams } from "./schemas.ts";
|
|
13
|
+
import { loadConfig } from "./config.ts";
|
|
14
|
+
import { type Details, type SubagentState } from "../shared/types.ts";
|
|
15
|
+
|
|
16
|
+
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
17
|
+
if (parentSessionFile) {
|
|
18
|
+
const baseName = path.basename(parentSessionFile, ".jsonl");
|
|
19
|
+
const sessionsDir = path.dirname(parentSessionFile);
|
|
20
|
+
return path.join(sessionsDir, baseName);
|
|
21
|
+
}
|
|
22
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function expandTilde(p: string): string {
|
|
26
|
+
return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createChildSafeState(): SubagentState {
|
|
30
|
+
return {
|
|
31
|
+
baseCwd: "",
|
|
32
|
+
currentSessionId: null,
|
|
33
|
+
asyncJobs: new Map(),
|
|
34
|
+
foregroundRuns: new Map(),
|
|
35
|
+
foregroundControls: new Map(),
|
|
36
|
+
lastForegroundControlId: null,
|
|
37
|
+
pendingForegroundControlNotices: new Map(),
|
|
38
|
+
cleanupTimers: new Map(),
|
|
39
|
+
lastUiContext: null,
|
|
40
|
+
poller: null,
|
|
41
|
+
completionSeen: new Map(),
|
|
42
|
+
watcher: null,
|
|
43
|
+
watcherRestartTimer: null,
|
|
44
|
+
resultFileCoalescer: {
|
|
45
|
+
schedule: () => false,
|
|
46
|
+
clear: () => {},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState): NodeJS.Timeout | undefined {
|
|
52
|
+
let route;
|
|
53
|
+
try {
|
|
54
|
+
route = resolveNestedRouteFromEnv();
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!route) return undefined;
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const inFlight = new Set<string>();
|
|
61
|
+
const pendingResults = new Map<string, Parameters<typeof writeNestedControlResult>[1]>();
|
|
62
|
+
const timer = setInterval(() => {
|
|
63
|
+
try {
|
|
64
|
+
for (const request of readNestedControlRequests(route)) {
|
|
65
|
+
if (seen.has(request.requestId) || inFlight.has(request.requestId)) continue;
|
|
66
|
+
inFlight.add(request.requestId);
|
|
67
|
+
void (async () => {
|
|
68
|
+
try {
|
|
69
|
+
let result = pendingResults.get(request.requestId);
|
|
70
|
+
if (!result) {
|
|
71
|
+
let ok = false;
|
|
72
|
+
let message = "Control request failed.";
|
|
73
|
+
try {
|
|
74
|
+
const control = state.foregroundControls.get(request.targetRunId);
|
|
75
|
+
if (!control) {
|
|
76
|
+
message = `Nested run ${request.targetRunId} is not active in this fanout child.`;
|
|
77
|
+
} else if (request.action === "interrupt") {
|
|
78
|
+
ok = control.interrupt?.() === true;
|
|
79
|
+
message = ok
|
|
80
|
+
? `Interrupt requested for nested run ${request.targetRunId}.`
|
|
81
|
+
: `Nested run ${request.targetRunId} has no active child step to interrupt.`;
|
|
82
|
+
} else if (!request.message?.trim()) {
|
|
83
|
+
message = "Nested resume requires message.";
|
|
84
|
+
} else if (!control.currentAgent) {
|
|
85
|
+
message = `Nested run ${request.targetRunId} has no active child message route.`;
|
|
86
|
+
} else {
|
|
87
|
+
const index = control.currentIndex ?? 0;
|
|
88
|
+
const target = resolveSubagentIntercomTarget(request.targetRunId, control.currentAgent, index);
|
|
89
|
+
ok = await deliverSubagentIntercomMessageEvent(
|
|
90
|
+
pi.events,
|
|
91
|
+
target,
|
|
92
|
+
`Follow-up for nested run ${request.targetRunId} (${control.currentAgent}):\n\n${request.message.trim()}`,
|
|
93
|
+
500,
|
|
94
|
+
{ source: "nested-resume", runId: request.targetRunId, agent: control.currentAgent, index },
|
|
95
|
+
);
|
|
96
|
+
message = ok
|
|
97
|
+
? `Delivered follow-up to live nested run ${request.targetRunId}.`
|
|
98
|
+
: `Nested child intercom target is not registered: ${target}`;
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
}
|
|
103
|
+
result = { ts: Date.now(), requestId: request.requestId, targetRunId: request.targetRunId, ok, message };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
writeNestedControlResult(route, result);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
pendingResults.set(request.requestId, result);
|
|
109
|
+
console.error(`Failed to write nested control result for request '${request.requestId}' targeting '${request.targetRunId}' via inbox '${route.controlInbox}'; keeping request for retry:`, error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
pendingResults.delete(request.requestId);
|
|
113
|
+
seen.add(request.requestId);
|
|
114
|
+
try { fs.unlinkSync(request.filePath); } catch {}
|
|
115
|
+
} finally {
|
|
116
|
+
inFlight.delete(request.requestId);
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`Failed to poll nested control inbox '${route.controlInbox}' for root '${route.rootRunId}':`, error);
|
|
122
|
+
}
|
|
123
|
+
}, 200);
|
|
124
|
+
timer.unref?.();
|
|
125
|
+
return timer;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {
|
|
129
|
+
if (process.env[SUBAGENT_CHILD_ENV] !== "1" || process.env[SUBAGENT_FANOUT_CHILD_ENV] !== "1") return;
|
|
130
|
+
|
|
131
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
132
|
+
const registeredKey = "__piSubagentFanoutChildRegisteredApis";
|
|
133
|
+
const registeredApis = globalStore[registeredKey] instanceof WeakSet
|
|
134
|
+
? globalStore[registeredKey] as WeakSet<ExtensionAPI>
|
|
135
|
+
: new WeakSet<ExtensionAPI>();
|
|
136
|
+
globalStore[registeredKey] = registeredApis;
|
|
137
|
+
if (registeredApis.has(pi)) return;
|
|
138
|
+
registeredApis.add(pi);
|
|
139
|
+
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
const state = createChildSafeState();
|
|
142
|
+
const executor = createSubagentExecutor({
|
|
143
|
+
pi,
|
|
144
|
+
state,
|
|
145
|
+
config,
|
|
146
|
+
asyncByDefault: config.asyncByDefault === true,
|
|
147
|
+
tempArtifactsDir: getArtifactsDir(null),
|
|
148
|
+
getSubagentSessionRoot,
|
|
149
|
+
expandTilde,
|
|
150
|
+
discoverAgents,
|
|
151
|
+
allowMutatingManagementActions: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
155
|
+
name: "subagent",
|
|
156
|
+
label: "Subagent",
|
|
157
|
+
description: [
|
|
158
|
+
"Delegate to subagents from child-safe fanout mode.",
|
|
159
|
+
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
160
|
+
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
161
|
+
].join("\n"),
|
|
162
|
+
parameters: SubagentParams,
|
|
163
|
+
execute(id, params, signal, onUpdate, ctx) {
|
|
164
|
+
return executor.execute(id, params as SubagentParamsLike, signal, onUpdate, ctx);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
pi.registerTool(tool);
|
|
169
|
+
startNestedControlInboxListener(pi, state);
|
|
170
|
+
}
|
package/src/extension/index.ts
CHANGED
|
@@ -33,7 +33,8 @@ import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
|
33
33
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
34
34
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
35
35
|
import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
36
|
-
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
36
|
+
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
37
|
+
import registerFanoutChildSubagentExtension from "./fanout-child.ts";
|
|
37
38
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
38
39
|
import { loadConfig } from "./config.ts";
|
|
39
40
|
import {
|
|
@@ -207,7 +208,10 @@ class SubagentControlNoticeComponent implements Component {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
210
|
-
if (process.env[SUBAGENT_CHILD_ENV] === "1")
|
|
211
|
+
if (process.env[SUBAGENT_CHILD_ENV] === "1") {
|
|
212
|
+
if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") registerFanoutChildSubagentExtension(pi);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
211
215
|
const globalStore = globalThis as Record<string, unknown>;
|
|
212
216
|
const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
|
|
213
217
|
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
@@ -3,6 +3,8 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import {
|
|
4
4
|
type Details,
|
|
5
5
|
type IntercomEventBus,
|
|
6
|
+
type NestedRunSummary,
|
|
7
|
+
type PublicNestedRunSummary,
|
|
6
8
|
type SingleResult,
|
|
7
9
|
type SubagentResultIntercomChild,
|
|
8
10
|
type SubagentResultIntercomPayload,
|
|
@@ -60,6 +62,110 @@ function resolveGroupedStatus(children: SubagentResultIntercomChild[]): Subagent
|
|
|
60
62
|
return "failed";
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
function compactNestedRun(run: NestedRunSummary | PublicNestedRunSummary, depth = 0): PublicNestedRunSummary {
|
|
66
|
+
return {
|
|
67
|
+
id: run.id,
|
|
68
|
+
parentRunId: run.parentRunId,
|
|
69
|
+
...(run.parentStepIndex !== undefined ? { parentStepIndex: run.parentStepIndex } : {}),
|
|
70
|
+
...(run.parentAgent ? { parentAgent: run.parentAgent } : {}),
|
|
71
|
+
depth: run.depth,
|
|
72
|
+
path: run.path.slice(0, 4).map((part) => ({
|
|
73
|
+
runId: part.runId,
|
|
74
|
+
...(part.stepIndex !== undefined ? { stepIndex: part.stepIndex } : {}),
|
|
75
|
+
...(part.agent ? { agent: part.agent } : {}),
|
|
76
|
+
})),
|
|
77
|
+
...(run.asyncDir ? { asyncDir: run.asyncDir } : {}),
|
|
78
|
+
...(run.sessionId ? { sessionId: run.sessionId } : {}),
|
|
79
|
+
...(run.sessionFile ? { sessionFile: run.sessionFile } : {}),
|
|
80
|
+
...(run.intercomTarget ? { intercomTarget: run.intercomTarget } : {}),
|
|
81
|
+
...(run.ownerIntercomTarget ? { ownerIntercomTarget: run.ownerIntercomTarget } : {}),
|
|
82
|
+
...(run.leafIntercomTarget ? { leafIntercomTarget: run.leafIntercomTarget } : {}),
|
|
83
|
+
...(run.ownerState ? { ownerState: run.ownerState } : {}),
|
|
84
|
+
...(run.mode ? { mode: run.mode } : {}),
|
|
85
|
+
state: run.state,
|
|
86
|
+
...(run.agent ? { agent: run.agent } : {}),
|
|
87
|
+
...(run.agents?.length ? { agents: run.agents.slice(0, 12) } : {}),
|
|
88
|
+
...(run.currentStep !== undefined ? { currentStep: run.currentStep } : {}),
|
|
89
|
+
...(run.chainStepCount !== undefined ? { chainStepCount: run.chainStepCount } : {}),
|
|
90
|
+
...(run.parallelGroups?.length ? { parallelGroups: run.parallelGroups.slice(0, 8) } : {}),
|
|
91
|
+
...(run.activityState ? { activityState: run.activityState } : {}),
|
|
92
|
+
...(run.lastActivityAt !== undefined ? { lastActivityAt: run.lastActivityAt } : {}),
|
|
93
|
+
...(run.currentTool ? { currentTool: run.currentTool } : {}),
|
|
94
|
+
...(run.currentToolStartedAt !== undefined ? { currentToolStartedAt: run.currentToolStartedAt } : {}),
|
|
95
|
+
...(run.currentPath ? { currentPath: run.currentPath } : {}),
|
|
96
|
+
...(run.turnCount !== undefined ? { turnCount: run.turnCount } : {}),
|
|
97
|
+
...(run.toolCount !== undefined ? { toolCount: run.toolCount } : {}),
|
|
98
|
+
...(run.totalTokens ? { totalTokens: run.totalTokens } : {}),
|
|
99
|
+
...(run.startedAt !== undefined ? { startedAt: run.startedAt } : {}),
|
|
100
|
+
...(run.endedAt !== undefined ? { endedAt: run.endedAt } : {}),
|
|
101
|
+
...(run.lastUpdate !== undefined ? { lastUpdate: run.lastUpdate } : {}),
|
|
102
|
+
...(run.error ? { error: run.error } : {}),
|
|
103
|
+
...(run.steps?.length ? { steps: run.steps.slice(0, 12).map((step) => ({
|
|
104
|
+
agent: step.agent,
|
|
105
|
+
status: step.status,
|
|
106
|
+
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
107
|
+
...(step.activityState ? { activityState: step.activityState } : {}),
|
|
108
|
+
...(step.lastActivityAt !== undefined ? { lastActivityAt: step.lastActivityAt } : {}),
|
|
109
|
+
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
110
|
+
...(step.currentToolStartedAt !== undefined ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
111
|
+
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
112
|
+
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
113
|
+
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
114
|
+
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
115
|
+
...(step.endedAt !== undefined ? { endedAt: step.endedAt } : {}),
|
|
116
|
+
...(step.error ? { error: step.error } : {}),
|
|
117
|
+
...(depth < 2 && step.children?.length ? { children: step.children.slice(0, 8).map((child) => compactNestedRun(child, depth + 1)) } : {}),
|
|
118
|
+
})) } : {}),
|
|
119
|
+
...(depth < 2 && run.children?.length ? { children: run.children.slice(0, 8).map((child) => compactNestedRun(child, depth + 1)) } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function compactNestedResultChildren(children: Array<NestedRunSummary | PublicNestedRunSummary> | undefined): PublicNestedRunSummary[] | undefined {
|
|
124
|
+
if (!children?.length) return undefined;
|
|
125
|
+
return children.slice(0, 16).map((child) => compactNestedRun(child));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function attachNestedChildrenToResultChildren(
|
|
129
|
+
runId: string,
|
|
130
|
+
children: SubagentResultIntercomChild[],
|
|
131
|
+
nestedChildren: NestedRunSummary[] | undefined,
|
|
132
|
+
): SubagentResultIntercomChild[] {
|
|
133
|
+
const compact = compactNestedResultChildren(nestedChildren);
|
|
134
|
+
if (!compact?.length) return children.map((child) => ({ ...child, children: compactNestedResultChildren(child.children) }));
|
|
135
|
+
return children.map((child, index) => {
|
|
136
|
+
const childIndex = child.index ?? index;
|
|
137
|
+
const alreadyAttachedIds = new Set(child.children?.map((nested) => nested.id) ?? []);
|
|
138
|
+
const attached = compact.filter((nested) => nested.parentRunId === runId && nested.parentStepIndex === childIndex && !alreadyAttachedIds.has(nested.id));
|
|
139
|
+
const fallbackAttached = children.length === 1
|
|
140
|
+
? compact.filter((nested) => nested.parentRunId === runId && nested.parentStepIndex === undefined && !alreadyAttachedIds.has(nested.id))
|
|
141
|
+
: [];
|
|
142
|
+
const merged = compactNestedResultChildren([...(child.children ?? []), ...attached, ...fallbackAttached]);
|
|
143
|
+
return merged?.length ? { ...child, children: merged } : { ...child, children: undefined };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatNestedResultLines(children: PublicNestedRunSummary[] | undefined): string[] {
|
|
148
|
+
if (!children?.length) return [];
|
|
149
|
+
const lines = ["Nested subagents:"];
|
|
150
|
+
let remaining = 10;
|
|
151
|
+
const append = (runs: PublicNestedRunSummary[] | undefined, indent: string): void => {
|
|
152
|
+
for (const run of runs ?? []) {
|
|
153
|
+
if (remaining <= 0) {
|
|
154
|
+
lines.push(`${indent}↳ +more nested runs; inspect status for full tree`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
remaining--;
|
|
158
|
+
const label = run.agent ?? run.agents?.join("+") ?? run.id;
|
|
159
|
+
lines.push(`${indent}↳ ${label} — ${run.state} [${run.id}]`);
|
|
160
|
+
if (run.sessionFile) lines.push(`${indent} Session: ${run.sessionFile}`);
|
|
161
|
+
append(run.children, `${indent} `);
|
|
162
|
+
for (const step of run.steps ?? []) append(step.children, `${indent} `);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
append(children, "");
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
63
169
|
interface GroupedResultIntercomMessageInput {
|
|
64
170
|
to: string;
|
|
65
171
|
runId: string;
|
|
@@ -128,6 +234,7 @@ function formatSubagentResultIntercomMessage(input: {
|
|
|
128
234
|
if (child.intercomTarget) lines.push(`${input.source === "async" ? "Previous intercom target" : "Run intercom target"}: ${child.intercomTarget}`);
|
|
129
235
|
if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
|
|
130
236
|
if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
|
|
237
|
+
lines.push(...formatNestedResultLines(child.children));
|
|
131
238
|
lines.push("Summary:");
|
|
132
239
|
lines.push(child.summary);
|
|
133
240
|
}
|
|
@@ -139,6 +246,7 @@ export function buildSubagentResultIntercomPayload(input: GroupedResultIntercomM
|
|
|
139
246
|
const children = input.children.map((child) => ({
|
|
140
247
|
...child,
|
|
141
248
|
summary: child.summary.trim() || "(no output)",
|
|
249
|
+
children: compactNestedResultChildren(child.children),
|
|
142
250
|
}));
|
|
143
251
|
const status = resolveGroupedStatus(children);
|
|
144
252
|
const summary = formatStatusCounts(countStatuses(children));
|