pi-subagents 0.23.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 +17 -0
- package/README.md +4 -3
- package/agents/reviewer.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +18 -1
- package/src/extension/index.ts +12 -6
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +4 -1
- package/src/intercom/result-intercom.ts +8 -3
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-resume.ts +57 -31
- 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 +21 -7
- package/src/runs/foreground/chain-execution.ts +55 -21
- package/src/runs/foreground/execution.ts +6 -3
- package/src/runs/foreground/subagent-executor.ts +152 -20
- package/src/runs/shared/single-output.ts +21 -6
- package/src/shared/settings.ts +19 -0
- package/src/shared/types.ts +18 -0
- package/src/slash/slash-commands.ts +1 -0
- package/src/tui/render.ts +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
|
|
5
22
|
## [0.23.0] - 2026-05-02
|
|
6
23
|
|
|
7
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -780,11 +780,12 @@ Status and control actions:
|
|
|
780
780
|
subagent({ action: "status" })
|
|
781
781
|
subagent({ action: "status", id: "<run-id>" })
|
|
782
782
|
subagent({ action: "interrupt", id: "<run-id>" })
|
|
783
|
-
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" })
|
|
784
785
|
subagent({ action: "doctor" })
|
|
785
786
|
```
|
|
786
787
|
|
|
787
|
-
`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.
|
|
788
789
|
|
|
789
790
|
## Worktree isolation
|
|
790
791
|
|
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
|
|
|
@@ -290,6 +290,21 @@ const run = subagent({
|
|
|
290
290
|
|
|
291
291
|
Inspect async runs with `subagent({ action: "status", id: "..." })`, `subagent({ action: "status" })` for active runs, or the `/subagents-status` slash command.
|
|
292
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
|
+
|
|
293
308
|
Use diagnostics when setup or child startup looks wrong:
|
|
294
309
|
|
|
295
310
|
```typescript
|
|
@@ -409,6 +424,8 @@ Use `contact_supervisor` with `reason: "need_decision"` when:
|
|
|
409
424
|
- a child needs clarification instead of guessing
|
|
410
425
|
- an approval, product, API, or scope choice is required before continuing safely
|
|
411
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
|
+
|
|
412
429
|
Use `contact_supervisor` with `reason: "progress_update"` when:
|
|
413
430
|
- a child is explicitly asked for progress
|
|
414
431
|
- a meaningful discovery changes the plan
|
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"
|
|
@@ -31,6 +31,7 @@ const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only
|
|
|
31
31
|
Use contact_supervisor first. It resolves the supervisor session "{orchestratorTarget}" and run metadata automatically.
|
|
32
32
|
- Need a decision, blocked, approval, or product/API/scope ambiguity: contact_supervisor({ reason: "need_decision", message: "<question>" })
|
|
33
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.
|
|
34
35
|
- Meaningful progress or unexpected discoveries that change the plan: contact_supervisor({ reason: "progress_update", message: "UPDATE: <summary>" })
|
|
35
36
|
- Generic intercom is lower-level plumbing/fallback only: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
36
37
|
|
|
@@ -116,7 +117,9 @@ function intercomConfigStatus(configPath: string): { enabled: boolean; error?: u
|
|
|
116
117
|
function readJsonBestEffort(filePath: string): unknown {
|
|
117
118
|
try {
|
|
118
119
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
119
|
-
} catch {
|
|
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);
|
|
120
123
|
return null;
|
|
121
124
|
}
|
|
122
125
|
}
|
|
@@ -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: {
|
|
@@ -12,7 +12,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
12
12
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
13
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
14
14
|
import { injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
|
-
import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
15
|
+
import { buildChainInstructions, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
@@ -65,6 +65,7 @@ interface AsyncExecutionContext {
|
|
|
65
65
|
|
|
66
66
|
interface AsyncChainParams {
|
|
67
67
|
chain: ChainStep[];
|
|
68
|
+
task?: string;
|
|
68
69
|
resultMode?: Exclude<SubagentRunMode, "single">;
|
|
69
70
|
agents: AgentConfig[];
|
|
70
71
|
ctx: AsyncExecutionContext;
|
|
@@ -213,6 +214,10 @@ export function executeAsyncChain(
|
|
|
213
214
|
const chainSkills = params.chainSkills ?? [];
|
|
214
215
|
const availableModels = params.availableModels;
|
|
215
216
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
217
|
+
const firstStep = chain[0];
|
|
218
|
+
const originalTask = params.task ?? (firstStep
|
|
219
|
+
? (isParallelStep(firstStep) ? firstStep.parallel[0]?.task : (firstStep as SequentialStep).task)
|
|
220
|
+
: undefined);
|
|
216
221
|
|
|
217
222
|
for (const s of chain) {
|
|
218
223
|
const stepAgents = isParallelStep(s)
|
|
@@ -257,7 +262,7 @@ export function executeAsyncChain(
|
|
|
257
262
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
258
263
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
259
264
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
260
|
-
const behavior = resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills);
|
|
265
|
+
const behavior = suppressProgressForReadOnlyTask(resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
|
|
261
266
|
const skillNames = behavior.skills === false ? [] : behavior.skills;
|
|
262
267
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
|
|
263
268
|
if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
|
|
@@ -314,7 +319,7 @@ export function executeAsyncChain(
|
|
|
314
319
|
if (isParallelStep(s)) {
|
|
315
320
|
const parallelBehaviors = s.parallel.map((task) => {
|
|
316
321
|
const agent = agents.find((candidate) => candidate.name === task.agent)!;
|
|
317
|
-
return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
|
|
322
|
+
return suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(task), chainSkills), task.task, originalTask);
|
|
318
323
|
});
|
|
319
324
|
const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
|
|
320
325
|
if (progressPrecreated) {
|
|
@@ -436,7 +441,7 @@ export function executeAsyncChain(
|
|
|
436
441
|
|
|
437
442
|
return {
|
|
438
443
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
439
|
-
details: { mode: resultMode, results: [], asyncId: id, asyncDir },
|
|
444
|
+
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir },
|
|
440
445
|
};
|
|
441
446
|
}
|
|
442
447
|
|
|
@@ -568,6 +573,6 @@ export function executeAsyncSingle(
|
|
|
568
573
|
|
|
569
574
|
return {
|
|
570
575
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`) }],
|
|
571
|
-
details: { mode: "single", results: [], asyncId: id, asyncDir },
|
|
576
|
+
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir },
|
|
572
577
|
};
|
|
573
578
|
}
|
|
@@ -39,7 +39,7 @@ interface AsyncResultFile {
|
|
|
39
39
|
success?: boolean;
|
|
40
40
|
cwd?: string;
|
|
41
41
|
sessionFile?: string;
|
|
42
|
-
results?: Array<{ agent?: string; success?: boolean; intercomTarget?: string }>;
|
|
42
|
+
results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string }>;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface AsyncRunLocation {
|
|
@@ -59,10 +59,10 @@ function ensureObject(value: unknown, source: string): Record<string, unknown> {
|
|
|
59
59
|
return value as Record<string, unknown>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function validateOptionalString(value: Record<string, unknown>, field: string, source: string): string | undefined {
|
|
62
|
+
function validateOptionalString(value: Record<string, unknown>, field: string, source: string, displayField = field): string | undefined {
|
|
63
63
|
const fieldValue = value[field];
|
|
64
64
|
if (fieldValue === undefined) return undefined;
|
|
65
|
-
if (typeof fieldValue !== "string") throw new Error(`Invalid async result file '${source}': ${
|
|
65
|
+
if (typeof fieldValue !== "string") throw new Error(`Invalid async result file '${source}': ${displayField} must be a string.`);
|
|
66
66
|
return fieldValue;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -74,11 +74,12 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
74
74
|
if (!Array.isArray(resultsValue)) throw new Error(`Invalid async result file '${resultPath}': results must be an array.`);
|
|
75
75
|
results = resultsValue.map((entry, index) => {
|
|
76
76
|
const child = ensureObject(entry, `${resultPath} results[${index}]`);
|
|
77
|
-
const agent = validateOptionalString(child, "agent",
|
|
78
|
-
const
|
|
77
|
+
const agent = validateOptionalString(child, "agent", resultPath, `results[${index}].agent`);
|
|
78
|
+
const sessionFile = validateOptionalString(child, "sessionFile", resultPath, `results[${index}].sessionFile`);
|
|
79
|
+
const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
|
|
79
80
|
const success = child.success;
|
|
80
81
|
if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
|
|
81
|
-
return { agent, intercomTarget, ...(typeof success === "boolean" ? { success } : {}) };
|
|
82
|
+
return { agent, sessionFile, intercomTarget, ...(typeof success === "boolean" ? { success } : {}) };
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
const success = data.success;
|
|
@@ -210,6 +211,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
210
211
|
status.steps.forEach((step, index) => {
|
|
211
212
|
if (!step || typeof step !== "object" || Array.isArray(step)) throw new Error(`Invalid async status '${source}': steps[${index}] must be an object.`);
|
|
212
213
|
if (typeof step.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
|
|
214
|
+
if (step.sessionFile !== undefined && typeof step.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
|
|
213
215
|
});
|
|
214
216
|
}
|
|
215
217
|
}
|
|
@@ -243,38 +245,62 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
243
245
|
const resultSteps = result?.results ?? [];
|
|
244
246
|
const stepCount = statusSteps.length || resultSteps.length || (result?.agent ? 1 : 0);
|
|
245
247
|
const requestedIndex = params.index;
|
|
248
|
+
if (requestedIndex !== undefined && !Number.isInteger(requestedIndex)) throw new Error(`Async run '${runId}' index must be an integer.`);
|
|
249
|
+
const terminalStepStatuses = new Set(["complete", "completed", "failed", "paused"]);
|
|
246
250
|
|
|
247
251
|
if (state === "running") {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
if (requestedIndex !== undefined) {
|
|
253
|
+
if (requestedIndex < 0 || requestedIndex >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${requestedIndex} is out of range.`);
|
|
254
|
+
const selectedStep = statusSteps[requestedIndex];
|
|
255
|
+
if (selectedStep?.status === "running") {
|
|
256
|
+
return {
|
|
257
|
+
kind: "live",
|
|
258
|
+
runId,
|
|
259
|
+
asyncDir: location.asyncDir ?? undefined,
|
|
260
|
+
state,
|
|
261
|
+
agent: selectedStep.agent,
|
|
262
|
+
index: requestedIndex,
|
|
263
|
+
intercomTarget: resolveSubagentIntercomTarget(runId, selectedStep.agent, requestedIndex),
|
|
264
|
+
cwd: status?.cwd ?? result?.cwd,
|
|
265
|
+
sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (selectedStep?.status === "pending") throw new Error(`Async run '${runId}' child ${requestedIndex} is pending and has not started yet. Wait for it to run or complete before resuming.`);
|
|
269
|
+
if (selectedStep && !terminalStepStatuses.has(selectedStep.status)) throw new Error(`Async run '${runId}' child ${requestedIndex} is ${selectedStep.status} and cannot be revived yet.`);
|
|
270
|
+
} else {
|
|
271
|
+
const running = statusSteps
|
|
272
|
+
.map((step, index) => ({ step, index }))
|
|
273
|
+
.filter(({ step }) => step.status === "running");
|
|
274
|
+
const selected = running.length === 1 ? running[0] : undefined;
|
|
275
|
+
if (!selected) {
|
|
276
|
+
throw new Error(`Async run '${runId}' has ${running.length} running children. Provide index to choose one.`);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
kind: "live",
|
|
280
|
+
runId,
|
|
281
|
+
asyncDir: location.asyncDir ?? undefined,
|
|
282
|
+
state,
|
|
283
|
+
agent: selected.step.agent,
|
|
284
|
+
index: selected.index,
|
|
285
|
+
intercomTarget: resolveSubagentIntercomTarget(runId, selected.step.agent, selected.index),
|
|
286
|
+
cwd: status?.cwd ?? result?.cwd,
|
|
287
|
+
sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
288
|
+
};
|
|
256
289
|
}
|
|
257
|
-
return {
|
|
258
|
-
kind: "live",
|
|
259
|
-
runId,
|
|
260
|
-
asyncDir: location.asyncDir ?? undefined,
|
|
261
|
-
state,
|
|
262
|
-
agent: selected.step.agent,
|
|
263
|
-
index: selected.index,
|
|
264
|
-
intercomTarget: resolveSubagentIntercomTarget(runId, selected.step.agent, selected.index),
|
|
265
|
-
cwd: status?.cwd ?? result?.cwd,
|
|
266
|
-
sessionFile: status?.sessionFile ?? result?.sessionFile,
|
|
267
|
-
};
|
|
268
290
|
}
|
|
269
291
|
|
|
270
|
-
if (stepCount
|
|
271
|
-
throw new Error(`Async run '${runId}' has ${stepCount} children.
|
|
292
|
+
if (stepCount > 1 && requestedIndex === undefined) {
|
|
293
|
+
throw new Error(`Async run '${runId}' has ${stepCount} children. Provide index to choose one.`);
|
|
272
294
|
}
|
|
273
295
|
const index = requestedIndex ?? 0;
|
|
296
|
+
if (!Number.isInteger(index)) throw new Error(`Async run '${runId}' index must be an integer.`);
|
|
297
|
+
if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
|
|
274
298
|
const agent = statusSteps[index]?.agent ?? resultSteps[index]?.agent ?? result?.agent;
|
|
275
299
|
if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
|
|
276
|
-
const sessionFile =
|
|
277
|
-
|
|
300
|
+
const sessionFile = statusSteps[index]?.sessionFile
|
|
301
|
+
?? resultSteps[index]?.sessionFile
|
|
302
|
+
?? (stepCount === 1 ? status?.sessionFile ?? result?.sessionFile : undefined);
|
|
303
|
+
if (!sessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
|
|
278
304
|
const resolvedSessionFile = validateResumeSessionFile(runId, sessionFile);
|
|
279
305
|
|
|
280
306
|
return {
|
|
@@ -292,9 +318,9 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
292
318
|
|
|
293
319
|
export function buildRevivedAsyncTask(target: AsyncResumeTarget, message: string): string {
|
|
294
320
|
return [
|
|
295
|
-
"You are reviving a
|
|
321
|
+
"You are reviving a previous subagent conversation.",
|
|
296
322
|
"",
|
|
297
|
-
`Original
|
|
323
|
+
`Original run: ${target.runId}`,
|
|
298
324
|
`Original agent: ${target.agent}`,
|
|
299
325
|
target.sessionFile ? `Original session file: ${target.sessionFile}` : undefined,
|
|
300
326
|
"",
|
|
@@ -76,6 +76,7 @@ export function createResultWatcher(
|
|
|
76
76
|
output?: string;
|
|
77
77
|
error?: string;
|
|
78
78
|
success?: boolean;
|
|
79
|
+
sessionFile?: string;
|
|
79
80
|
artifactPaths?: { outputPath?: string };
|
|
80
81
|
intercomTarget?: string;
|
|
81
82
|
}>;
|
|
@@ -120,6 +121,7 @@ export function createResultWatcher(
|
|
|
120
121
|
const summary = result.success === false && result.error
|
|
121
122
|
? `${result.error}${hasRealOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
|
|
122
123
|
: output;
|
|
124
|
+
const sessionPath = result.sessionFile ?? (childResults.length === 1 ? data.sessionFile : undefined);
|
|
123
125
|
return {
|
|
124
126
|
agent: result.agent ?? data.agent ?? `step-${index + 1}`,
|
|
125
127
|
status: resolveSubagentResultStatus({
|
|
@@ -129,7 +131,7 @@ export function createResultWatcher(
|
|
|
129
131
|
summary,
|
|
130
132
|
index,
|
|
131
133
|
artifactPath: result.artifactPaths?.outputPath,
|
|
132
|
-
sessionPath:
|
|
134
|
+
...(typeof sessionPath === "string" && fsApi.existsSync(sessionPath) ? { sessionPath } : {}),
|
|
133
135
|
intercomTarget: result.intercomTarget,
|
|
134
136
|
};
|
|
135
137
|
}),
|
|
@@ -28,8 +28,24 @@ function activityText(activityState: unknown, lastActivityAt: unknown): string |
|
|
|
28
28
|
return activityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function
|
|
32
|
-
return
|
|
31
|
+
function hasExistingSessionFile(value: unknown): value is string {
|
|
32
|
+
return typeof value === "string" && fs.existsSync(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatResumeGuidance(runId: string | undefined, children: Array<{ agent?: unknown; sessionFile?: unknown }>, fallbackSessionFile?: unknown): string {
|
|
36
|
+
const knownChildren = children
|
|
37
|
+
.map((child, index) => ({ child, index }))
|
|
38
|
+
.filter(({ child }) => typeof child.agent === "string");
|
|
39
|
+
if (!runId || knownChildren.length === 0) return "Resume: unavailable; no child session file was persisted.";
|
|
40
|
+
const singleSessionFile = knownChildren[0]?.child.sessionFile ?? fallbackSessionFile;
|
|
41
|
+
if (children.length === 1 && knownChildren.length === 1 && hasExistingSessionFile(singleSessionFile)) {
|
|
42
|
+
return `Revive: subagent({ action: "resume", id: "${runId}", message: "..." })`;
|
|
43
|
+
}
|
|
44
|
+
const childWithSession = knownChildren.find(({ child }) => hasExistingSessionFile(child.sessionFile));
|
|
45
|
+
if (childWithSession) {
|
|
46
|
+
return `Revive child: subagent({ action: "resume", id: "${runId}", index: ${childWithSession.index}, message: "..." })`;
|
|
47
|
+
}
|
|
48
|
+
return "Resume: unavailable; no child session file was persisted.";
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
@@ -137,14 +153,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
137
153
|
}
|
|
138
154
|
if (status.sessionFile) lines.push(`Session: ${status.sessionFile}`);
|
|
139
155
|
if (status.state !== "running") {
|
|
140
|
-
|
|
141
|
-
if (canShowRevive(stepCount, status.sessionFile)) {
|
|
142
|
-
lines.push(`Revive: subagent({ action: "resume", id: "${status.runId}", message: "..." })`);
|
|
143
|
-
} else if (stepCount > 1) {
|
|
144
|
-
lines.push("Resume: unsupported for multi-child async runs until per-child session files are persisted.");
|
|
145
|
-
} else {
|
|
146
|
-
lines.push("Resume: unavailable; no single child session file was persisted.");
|
|
147
|
-
}
|
|
156
|
+
lines.push(formatResumeGuidance(status.runId, status.steps ?? [], status.sessionFile));
|
|
148
157
|
}
|
|
149
158
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
150
159
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
@@ -156,18 +165,12 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
156
165
|
if (resultPath) {
|
|
157
166
|
try {
|
|
158
167
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
159
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; exitCode?: number; state?: string; sessionFile?: string; results?: Array<{ agent?: string }> };
|
|
168
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; exitCode?: number; state?: string; sessionFile?: string; results?: Array<{ agent?: string; sessionFile?: string }> };
|
|
160
169
|
const status = data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
161
170
|
const runId = data.runId ?? data.id ?? resolvedId;
|
|
162
171
|
const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
lines.push(`Revive: subagent({ action: "resume", id: "${runId}", message: "..." })`);
|
|
166
|
-
} else if (stepCount > 1) {
|
|
167
|
-
lines.push("Resume: unsupported for multi-child async runs until per-child session files are persisted.");
|
|
168
|
-
} else {
|
|
169
|
-
lines.push("Resume: unavailable; no single child session file was persisted.");
|
|
170
|
-
}
|
|
172
|
+
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
173
|
+
lines.push(formatResumeGuidance(runId, children, data.sessionFile));
|
|
171
174
|
if (data.summary) lines.push("", data.summary);
|
|
172
175
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
173
176
|
} catch (error) {
|
|
@@ -76,6 +76,7 @@ interface ResultChildOutcome {
|
|
|
76
76
|
agent?: string;
|
|
77
77
|
success?: boolean;
|
|
78
78
|
error?: string;
|
|
79
|
+
sessionFile?: string;
|
|
79
80
|
model?: string;
|
|
80
81
|
attemptedModels?: string[];
|
|
81
82
|
modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
|
|
@@ -119,6 +120,7 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
119
120
|
durationMs: step.startedAt !== undefined && step.durationMs === undefined ? Math.max(0, now - step.startedAt) : step.durationMs,
|
|
120
121
|
exitCode: step.exitCode ?? (state === "complete" || state === "paused" ? 0 : 1),
|
|
121
122
|
error: state === "failed" ? step.error ?? child?.error : step.error,
|
|
123
|
+
sessionFile: step.sessionFile ?? child?.sessionFile,
|
|
122
124
|
model: step.model ?? child?.model,
|
|
123
125
|
attemptedModels: step.attemptedModels ?? child?.attemptedModels,
|
|
124
126
|
modelAttempts: step.modelAttempts ?? child?.modelAttempts,
|
|
@@ -204,6 +206,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
204
206
|
model: step.model,
|
|
205
207
|
attemptedModels: step.attemptedModels,
|
|
206
208
|
modelAttempts: step.modelAttempts,
|
|
209
|
+
sessionFile: step.sessionFile,
|
|
207
210
|
})),
|
|
208
211
|
exitCode: 1,
|
|
209
212
|
timestamp: now,
|