pi-subagents 0.24.4 → 0.27.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 +29 -0
- package/README.md +145 -27
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +71 -20
- package/src/agents/agent-management.ts +57 -15
- package/src/agents/agent-serializer.ts +3 -2
- package/src/agents/agents.ts +47 -16
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +171 -0
- package/src/extension/index.ts +7 -2
- package/src/extension/schemas.ts +138 -5
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +185 -10
- 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 +71 -31
- 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 +89 -4
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +648 -42
- package/src/runs/foreground/chain-execution.ts +331 -118
- package/src/runs/foreground/execution.ts +226 -10
- package/src/runs/foreground/subagent-executor.ts +377 -14
- package/src/runs/shared/acceptance-contract.ts +291 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +161 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- 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/parallel-utils.ts +31 -1
- package/src/runs/shared/pi-args.ts +73 -5
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
- package/src/runs/shared/workflow-graph.ts +206 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +345 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +268 -43
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ChainConfig, ChainStepConfig } from "./agents.ts";
|
|
2
2
|
import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
3
3
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
4
|
+
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
|
+
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
|
+
import type { ChainStep } from "../shared/settings.ts";
|
|
4
7
|
|
|
5
8
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
6
9
|
const lines = sectionBody.split("\n");
|
|
@@ -20,6 +23,25 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
20
23
|
else if (rawValue) step.output = rawValue;
|
|
21
24
|
continue;
|
|
22
25
|
}
|
|
26
|
+
if (key === "phase") {
|
|
27
|
+
if (rawValue) step.phase = rawValue;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (key === "label") {
|
|
31
|
+
if (rawValue) step.label = rawValue;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (key === "as") {
|
|
35
|
+
if (rawValue) step.as = rawValue;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (key === "outputschema") {
|
|
39
|
+
if (rawValue.startsWith("{") || rawValue.startsWith("[")) {
|
|
40
|
+
throw new Error("Inline outputSchema values are not supported in .chain.md files; use a schema file path.");
|
|
41
|
+
}
|
|
42
|
+
if (rawValue) step.outputSchema = rawValue;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
23
45
|
if (key === "outputmode") {
|
|
24
46
|
if (rawValue === "inline" || rawValue === "file-only") step.outputMode = rawValue;
|
|
25
47
|
continue;
|
|
@@ -102,6 +124,100 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
102
124
|
};
|
|
103
125
|
}
|
|
104
126
|
|
|
127
|
+
export function parseJsonChain(content: string, source: "user" | "project", filePath: string): ChainConfig {
|
|
128
|
+
let parsed: unknown;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(content);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${message}`);
|
|
134
|
+
}
|
|
135
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
136
|
+
throw new Error(`JSON chain '${filePath}' must contain an object root.`);
|
|
137
|
+
}
|
|
138
|
+
const input = parsed as Record<string, unknown>;
|
|
139
|
+
if (typeof input.name !== "string" || !input.name.trim()) {
|
|
140
|
+
throw new Error(`JSON chain '${filePath}' must include string name.`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof input.description !== "string" || !input.description.trim()) {
|
|
143
|
+
throw new Error(`JSON chain '${filePath}' must include string description.`);
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(input.chain)) {
|
|
146
|
+
throw new Error(`JSON chain '${filePath}' must include array chain.`);
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < input.chain.length; i++) {
|
|
149
|
+
const step = input.chain[i];
|
|
150
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
151
|
+
throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
|
|
152
|
+
}
|
|
153
|
+
const stepRecord = step as Record<string, unknown>;
|
|
154
|
+
const parallel = stepRecord.parallel;
|
|
155
|
+
if (Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
156
|
+
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
|
|
157
|
+
}
|
|
158
|
+
if (parallel && typeof parallel === "object" && !Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
159
|
+
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on dynamic fanout groups; set acceptance on the dynamic template.`);
|
|
160
|
+
}
|
|
161
|
+
const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
|
|
162
|
+
if (acceptanceErrors.length > 0) {
|
|
163
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(parallel)) {
|
|
166
|
+
for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
|
|
167
|
+
const task = parallel[taskIndex];
|
|
168
|
+
if (!task || typeof task !== "object" || Array.isArray(task)) continue;
|
|
169
|
+
const taskErrors = validateAcceptanceInput((task as Record<string, unknown>).acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
|
|
170
|
+
if (taskErrors.length > 0) {
|
|
171
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${taskErrors.join(" ")}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else if (parallel && typeof parallel === "object") {
|
|
175
|
+
const templateErrors = validateAcceptanceInput((parallel as Record<string, unknown>).acceptance, `step ${i + 1} dynamic template acceptance`);
|
|
176
|
+
if (templateErrors.length > 0) {
|
|
177
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${templateErrors.join(" ")}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
validateChainOutputBindings(input.chain as ChainStep[], { maxItems: Number.MAX_SAFE_INTEGER });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof ChainOutputValidationError) throw new Error(`Invalid JSON chain '${filePath}': ${error.message}`);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
const parsedPackage = parsePackageName(typeof input.package === "string" ? input.package : undefined, `Chain '${input.name}' package`);
|
|
188
|
+
if (parsedPackage.error) throw new Error(parsedPackage.error);
|
|
189
|
+
const extraFields: Record<string, string> = {};
|
|
190
|
+
for (const [key, value] of Object.entries(input)) {
|
|
191
|
+
if (key === "name" || key === "package" || key === "description" || key === "chain") continue;
|
|
192
|
+
if (typeof value === "string") extraFields[key] = value;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
name: buildRuntimeName(input.name.trim(), parsedPackage.packageName),
|
|
196
|
+
localName: input.name.trim(),
|
|
197
|
+
packageName: parsedPackage.packageName,
|
|
198
|
+
description: input.description.trim(),
|
|
199
|
+
source,
|
|
200
|
+
filePath,
|
|
201
|
+
steps: input.chain as ChainStepConfig[],
|
|
202
|
+
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function serializeJsonChain(config: ChainConfig): string {
|
|
207
|
+
const root: Record<string, unknown> = {
|
|
208
|
+
name: frontmatterNameForConfig(config),
|
|
209
|
+
description: config.description,
|
|
210
|
+
chain: config.steps,
|
|
211
|
+
};
|
|
212
|
+
if (config.packageName) root.package = config.packageName;
|
|
213
|
+
if (config.extraFields) {
|
|
214
|
+
for (const [key, value] of Object.entries(config.extraFields)) {
|
|
215
|
+
if (key !== "name" && key !== "description" && key !== "package" && key !== "chain") root[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
105
221
|
export function serializeChain(config: ChainConfig): string {
|
|
106
222
|
const lines: string[] = [];
|
|
107
223
|
lines.push("---");
|
|
@@ -121,6 +237,10 @@ export function serializeChain(config: ChainConfig): string {
|
|
|
121
237
|
lines.push(`## ${step.agent}`);
|
|
122
238
|
if (step.output === false) lines.push("output: false");
|
|
123
239
|
else if (step.output) lines.push(`output: ${step.output}`);
|
|
240
|
+
if (step.phase) lines.push(`phase: ${step.phase}`);
|
|
241
|
+
if (step.label) lines.push(`label: ${step.label}`);
|
|
242
|
+
if (step.as) lines.push(`as: ${step.as}`);
|
|
243
|
+
if (step.outputSchema) lines.push(`outputSchema: ${step.outputSchema}`);
|
|
124
244
|
if (step.outputMode) lines.push(`outputMode: ${step.outputMode}`);
|
|
125
245
|
if (step.reads === false) lines.push("reads: false");
|
|
126
246
|
else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(`reads: ${step.reads.join(", ")}`);
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
"For goal-style requests such as /goal, goal, active goal, or work until evidence says done, use explicit acceptance on the delegated run: criteria for the target, evidence/verify for proof, stopRules for constraints, and maxFinalizationTurns for the bounded loop.",
|
|
160
|
+
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
161
|
+
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
162
|
+
].join("\n"),
|
|
163
|
+
parameters: SubagentParams,
|
|
164
|
+
execute(id, params, signal, onUpdate, ctx) {
|
|
165
|
+
return executor.execute(id, params as SubagentParamsLike, signal, onUpdate, ctx);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
pi.registerTool(tool);
|
|
170
|
+
startNestedControlInboxListener(pi, state);
|
|
171
|
+
}
|
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];
|
|
@@ -391,6 +395,7 @@ EXECUTION (use exactly ONE mode):
|
|
|
391
395
|
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
392
396
|
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
393
397
|
• Optional context: { context: "fresh" | "fork" } (default: if any requested agent has defaultContext: "fork", the whole invocation uses fork; otherwise "fresh"; inspect agent defaults via { action: "list" })
|
|
398
|
+
• Goal-style requests: when the user says “/goal”, “goal”, “active goal”, “work until evidence says done”, or “verify against a goal”, model that as explicit acceptance. Use acceptance.criteria for the target, acceptance.evidence/verify for proof, acceptance.stopRules for constraints, and acceptance.maxFinalizationTurns for the bounded loop.
|
|
394
399
|
|
|
395
400
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
396
401
|
• {task} - The original task/request from the user
|
package/src/extension/schemas.ts
CHANGED
|
@@ -35,9 +35,82 @@ const ReadsOverride = Type.Unsafe({
|
|
|
35
35
|
description: "Files to read before running (array of filenames), or false to disable",
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
const JsonSchemaObject = Type.Unsafe({
|
|
39
|
+
type: "object",
|
|
40
|
+
additionalProperties: true,
|
|
41
|
+
description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const AcceptanceEvidenceKind = Type.String({
|
|
45
|
+
enum: [
|
|
46
|
+
"changed-files",
|
|
47
|
+
"tests-added",
|
|
48
|
+
"commands-run",
|
|
49
|
+
"validation-output",
|
|
50
|
+
"residual-risks",
|
|
51
|
+
"no-staged-files",
|
|
52
|
+
"diff-summary",
|
|
53
|
+
"review-findings",
|
|
54
|
+
"manual-notes",
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const AcceptanceGateSchema = Type.Object({
|
|
59
|
+
id: Type.String(),
|
|
60
|
+
must: Type.String(),
|
|
61
|
+
evidence: Type.Optional(Type.Array(AcceptanceEvidenceKind)),
|
|
62
|
+
severity: Type.Optional(Type.String({ enum: ["required", "recommended"] })),
|
|
63
|
+
}, { additionalProperties: false });
|
|
64
|
+
|
|
65
|
+
const AcceptanceVerifyCommandSchema = Type.Object({
|
|
66
|
+
id: Type.String(),
|
|
67
|
+
command: Type.String(),
|
|
68
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
69
|
+
cwd: Type.Optional(Type.String()),
|
|
70
|
+
env: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: { type: "string" } })),
|
|
71
|
+
allowFailure: Type.Optional(Type.Boolean()),
|
|
72
|
+
}, { additionalProperties: false });
|
|
73
|
+
|
|
74
|
+
const AcceptanceReviewGateSchema = Type.Object({
|
|
75
|
+
agent: Type.Optional(Type.String()),
|
|
76
|
+
focus: Type.Optional(Type.String()),
|
|
77
|
+
required: Type.Optional(Type.Boolean()),
|
|
78
|
+
}, { additionalProperties: false });
|
|
79
|
+
|
|
80
|
+
const AcceptanceOverride = Type.Unsafe({
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
criteria: {
|
|
84
|
+
type: "array",
|
|
85
|
+
items: {
|
|
86
|
+
anyOf: [
|
|
87
|
+
{ type: "string" },
|
|
88
|
+
AcceptanceGateSchema,
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
evidence: { type: "array", items: AcceptanceEvidenceKind },
|
|
93
|
+
verify: { type: "array", items: AcceptanceVerifyCommandSchema },
|
|
94
|
+
review: AcceptanceReviewGateSchema,
|
|
95
|
+
stopRules: { type: "array", items: { type: "string" } },
|
|
96
|
+
maxFinalizationTurns: { type: "integer", minimum: 1, maximum: 10 },
|
|
97
|
+
},
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
allOf: [{
|
|
100
|
+
anyOf: [
|
|
101
|
+
{ required: ["criteria"] },
|
|
102
|
+
{ required: ["evidence"] },
|
|
103
|
+
{ required: ["verify"] },
|
|
104
|
+
{ required: ["review"] },
|
|
105
|
+
{ required: ["stopRules"] },
|
|
106
|
+
],
|
|
107
|
+
}],
|
|
108
|
+
description: "Optional acceptance contract. Use this for goal-style requests such as /goal, goal, active goal, or work until evidence says done: criteria define the target, evidence/verify define proof, stopRules define constraints, and maxFinalizationTurns defines the bounded loop. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
109
|
+
});
|
|
110
|
+
|
|
38
111
|
const TaskItem = Type.Object({
|
|
39
|
-
agent: Type.String(),
|
|
40
|
-
task: Type.String(),
|
|
112
|
+
agent: Type.String(),
|
|
113
|
+
task: Type.String(),
|
|
41
114
|
cwd: Type.Optional(Type.String()),
|
|
42
115
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
43
116
|
output: Type.Optional(OutputOverride),
|
|
@@ -46,12 +119,17 @@ const TaskItem = Type.Object({
|
|
|
46
119
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
47
120
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
48
121
|
skill: Type.Optional(SkillOverride),
|
|
122
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
49
123
|
});
|
|
50
124
|
|
|
51
125
|
// Parallel task item (within a parallel step)
|
|
52
126
|
const ParallelTaskSchema = Type.Object({
|
|
53
127
|
agent: Type.String(),
|
|
54
128
|
task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
|
|
129
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
130
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })),
|
|
131
|
+
as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
|
|
132
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
55
133
|
cwd: Type.Optional(Type.String()),
|
|
56
134
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
57
135
|
output: Type.Optional(OutputOverride),
|
|
@@ -60,14 +138,51 @@ const ParallelTaskSchema = Type.Object({
|
|
|
60
138
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
61
139
|
skill: Type.Optional(SkillOverride),
|
|
62
140
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
141
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
63
142
|
});
|
|
64
143
|
|
|
144
|
+
const DynamicExpandSchema = Type.Object({
|
|
145
|
+
from: Type.Object({
|
|
146
|
+
output: Type.String({ description: "Prior named structured output to expand from." }),
|
|
147
|
+
path: Type.String({ description: "JSON Pointer into the structured output, e.g. /items." }),
|
|
148
|
+
}, { additionalProperties: false }),
|
|
149
|
+
item: Type.Optional(Type.String({ description: "Template variable name for each item. Defaults to item." })),
|
|
150
|
+
key: Type.Optional(Type.String({ description: "JSON Pointer relative to each item for stable child ids." })),
|
|
151
|
+
maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required fanout bound unless configured globally." })),
|
|
152
|
+
onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Empty input behavior. Defaults to skip." })),
|
|
153
|
+
}, { additionalProperties: false });
|
|
154
|
+
|
|
155
|
+
const DynamicParallelTemplateSchema = Type.Object({
|
|
156
|
+
agent: Type.String(),
|
|
157
|
+
task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })),
|
|
158
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
159
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })),
|
|
160
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
161
|
+
cwd: Type.Optional(Type.String()),
|
|
162
|
+
output: Type.Optional(OutputOverride),
|
|
163
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
164
|
+
reads: Type.Optional(ReadsOverride),
|
|
165
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
166
|
+
skill: Type.Optional(SkillOverride),
|
|
167
|
+
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
168
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
169
|
+
}, { additionalProperties: false });
|
|
170
|
+
|
|
171
|
+
const DynamicCollectSchema = Type.Object({
|
|
172
|
+
as: Type.String({ description: "Safe output name for the ordered collected result array." }),
|
|
173
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
174
|
+
}, { additionalProperties: false });
|
|
175
|
+
|
|
65
176
|
// Flattened so chain steps do not need an object-shape anyOf/oneOf union.
|
|
66
177
|
const ChainItem = Type.Object({
|
|
67
178
|
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
68
179
|
task: Type.Optional(Type.String({
|
|
69
|
-
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
180
|
+
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
70
181
|
})),
|
|
182
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
183
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })),
|
|
184
|
+
as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
|
|
185
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
71
186
|
cwd: Type.Optional(Type.String()),
|
|
72
187
|
output: Type.Optional(OutputOverride),
|
|
73
188
|
outputMode: Type.Optional(OutputModeOverride),
|
|
@@ -75,13 +190,30 @@ const ChainItem = Type.Object({
|
|
|
75
190
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
76
191
|
skill: Type.Optional(SkillOverride),
|
|
77
192
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
78
|
-
|
|
193
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
194
|
+
parallel: Type.Optional(Type.Unsafe({
|
|
195
|
+
anyOf: [
|
|
196
|
+
Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
|
|
197
|
+
DynamicParallelTemplateSchema,
|
|
198
|
+
],
|
|
199
|
+
description: "Static parallel tasks array, or a single dynamic fanout child template when expand/collect are present.",
|
|
200
|
+
})),
|
|
201
|
+
expand: Type.Optional(DynamicExpandSchema),
|
|
202
|
+
collect: Type.Optional(DynamicCollectSchema),
|
|
79
203
|
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
80
204
|
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
81
205
|
worktree: Type.Optional(Type.Boolean({
|
|
82
206
|
description: "Create isolated git worktrees for each parallel task."
|
|
83
207
|
})),
|
|
84
|
-
}, {
|
|
208
|
+
}, {
|
|
209
|
+
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
allOf: [
|
|
212
|
+
{ if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
|
|
213
|
+
{ if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
|
|
214
|
+
{ not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
|
|
215
|
+
],
|
|
216
|
+
});
|
|
85
217
|
|
|
86
218
|
const ControlOverrides = Type.Object({
|
|
87
219
|
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
|
|
@@ -165,4 +297,5 @@ export const SubagentParams = Type.Object({
|
|
|
165
297
|
outputMode: Type.Optional(OutputModeOverride),
|
|
166
298
|
skill: Type.Optional(SkillOverride),
|
|
167
299
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
300
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
168
301
|
});
|
|
@@ -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));
|