pi-subagents 0.25.0 → 0.28.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 +34 -0
- package/README.md +175 -19
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/skills/pi-subagents/SKILL.md +60 -17
- package/src/agents/agent-management.ts +71 -15
- package/src/agents/agent-serializer.ts +13 -2
- package/src/agents/agents.ts +88 -17
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +2 -0
- package/src/extension/index.ts +5 -2
- package/src/extension/schemas.ts +132 -6
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +88 -6
- package/src/runs/background/async-status.ts +11 -1
- package/src/runs/background/run-status.ts +10 -1
- package/src/runs/background/subagent-runner.ts +665 -39
- package/src/runs/foreground/chain-execution.ts +369 -118
- package/src/runs/foreground/execution.ts +392 -19
- package/src/runs/foreground/subagent-executor.ts +126 -3
- package/src/runs/shared/acceptance-contract.ts +318 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +173 -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/parallel-utils.ts +33 -1
- package/src/runs/shared/pi-args.ts +11 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
- package/src/runs/shared/workflow-graph.ts +210 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +265 -1
- package/src/shared/utils.ts +7 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +178 -45
package/src/extension/schemas.ts
CHANGED
|
@@ -35,9 +35,73 @@ 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
|
+
description: "Optional acceptance contract. Use this for goal-style requests and for implementation handoffs from plans, PRDs, specs, issues, or broad fixes. Put implementation instructions and plan paths in task; put the definition of done in criteria, proof in evidence/verify, constraints in stopRules, and the bounded loop budget in maxFinalizationTurns. Runtime validation still requires at least one of criteria, evidence, verify, review, or stopRules. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
100
|
+
});
|
|
101
|
+
|
|
38
102
|
const TaskItem = Type.Object({
|
|
39
|
-
agent: Type.String(),
|
|
40
|
-
task: Type.String(),
|
|
103
|
+
agent: Type.String(),
|
|
104
|
+
task: Type.String(),
|
|
41
105
|
cwd: Type.Optional(Type.String()),
|
|
42
106
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
43
107
|
output: Type.Optional(OutputOverride),
|
|
@@ -46,12 +110,17 @@ const TaskItem = Type.Object({
|
|
|
46
110
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
47
111
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
48
112
|
skill: Type.Optional(SkillOverride),
|
|
113
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
49
114
|
});
|
|
50
115
|
|
|
51
116
|
// Parallel task item (within a parallel step)
|
|
52
117
|
const ParallelTaskSchema = Type.Object({
|
|
53
118
|
agent: Type.String(),
|
|
54
119
|
task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
|
|
120
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
121
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })),
|
|
122
|
+
as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
|
|
123
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
55
124
|
cwd: Type.Optional(Type.String()),
|
|
56
125
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
57
126
|
output: Type.Optional(OutputOverride),
|
|
@@ -60,14 +129,51 @@ const ParallelTaskSchema = Type.Object({
|
|
|
60
129
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
61
130
|
skill: Type.Optional(SkillOverride),
|
|
62
131
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
132
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
63
133
|
});
|
|
64
134
|
|
|
135
|
+
const DynamicExpandSchema = Type.Object({
|
|
136
|
+
from: Type.Object({
|
|
137
|
+
output: Type.String({ description: "Prior named structured output to expand from." }),
|
|
138
|
+
path: Type.String({ description: "JSON Pointer into the structured output, e.g. /items." }),
|
|
139
|
+
}, { additionalProperties: false }),
|
|
140
|
+
item: Type.Optional(Type.String({ description: "Template variable name for each item. Defaults to item." })),
|
|
141
|
+
key: Type.Optional(Type.String({ description: "JSON Pointer relative to each item for stable child ids." })),
|
|
142
|
+
maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required fanout bound unless configured globally." })),
|
|
143
|
+
onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Empty input behavior. Defaults to skip." })),
|
|
144
|
+
}, { additionalProperties: false });
|
|
145
|
+
|
|
146
|
+
const DynamicParallelTemplateSchema = Type.Object({
|
|
147
|
+
agent: Type.String(),
|
|
148
|
+
task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })),
|
|
149
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
150
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })),
|
|
151
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
152
|
+
cwd: Type.Optional(Type.String()),
|
|
153
|
+
output: Type.Optional(OutputOverride),
|
|
154
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
155
|
+
reads: Type.Optional(ReadsOverride),
|
|
156
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
157
|
+
skill: Type.Optional(SkillOverride),
|
|
158
|
+
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
159
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
160
|
+
}, { additionalProperties: false });
|
|
161
|
+
|
|
162
|
+
const DynamicCollectSchema = Type.Object({
|
|
163
|
+
as: Type.String({ description: "Safe output name for the ordered collected result array." }),
|
|
164
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
165
|
+
}, { additionalProperties: false });
|
|
166
|
+
|
|
65
167
|
// Flattened so chain steps do not need an object-shape anyOf/oneOf union.
|
|
66
168
|
const ChainItem = Type.Object({
|
|
67
169
|
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
68
170
|
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."
|
|
171
|
+
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
172
|
})),
|
|
173
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
174
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })),
|
|
175
|
+
as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
|
|
176
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
71
177
|
cwd: Type.Optional(Type.String()),
|
|
72
178
|
output: Type.Optional(OutputOverride),
|
|
73
179
|
outputMode: Type.Optional(OutputModeOverride),
|
|
@@ -75,13 +181,30 @@ const ChainItem = Type.Object({
|
|
|
75
181
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
76
182
|
skill: Type.Optional(SkillOverride),
|
|
77
183
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
78
|
-
|
|
184
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
185
|
+
parallel: Type.Optional(Type.Unsafe({
|
|
186
|
+
anyOf: [
|
|
187
|
+
Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
|
|
188
|
+
DynamicParallelTemplateSchema,
|
|
189
|
+
],
|
|
190
|
+
description: "Static parallel tasks array, or a single dynamic fanout child template when expand/collect are present.",
|
|
191
|
+
})),
|
|
192
|
+
expand: Type.Optional(DynamicExpandSchema),
|
|
193
|
+
collect: Type.Optional(DynamicCollectSchema),
|
|
79
194
|
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
80
195
|
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
81
196
|
worktree: Type.Optional(Type.Boolean({
|
|
82
197
|
description: "Create isolated git worktrees for each parallel task."
|
|
83
198
|
})),
|
|
84
|
-
}, {
|
|
199
|
+
}, {
|
|
200
|
+
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
|
|
201
|
+
additionalProperties: false,
|
|
202
|
+
allOf: [
|
|
203
|
+
{ if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
|
|
204
|
+
{ if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
|
|
205
|
+
{ not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
|
|
206
|
+
],
|
|
207
|
+
});
|
|
85
208
|
|
|
86
209
|
const ControlOverrides = Type.Object({
|
|
87
210
|
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
|
|
@@ -127,10 +250,12 @@ export const SubagentParams = Type.Object({
|
|
|
127
250
|
{ type: "object", additionalProperties: true },
|
|
128
251
|
{ type: "string" },
|
|
129
252
|
],
|
|
130
|
-
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
253
|
+
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth, maxExecutionTimeMs, maxTokens. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
131
254
|
})),
|
|
132
255
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
|
|
133
256
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
257
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Foreground execution wall-clock timeout in milliseconds. When it expires, running children are soft-interrupted and timed-out results are returned. Foreground only; async/background runs ignore this field." })),
|
|
258
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias for timeoutMs. Use only one unless both values are identical." })),
|
|
134
259
|
worktree: Type.Optional(Type.Boolean({
|
|
135
260
|
description: "Create isolated git worktrees for each parallel task. " +
|
|
136
261
|
"Prevents filesystem conflicts. Requires clean git state. " +
|
|
@@ -165,4 +290,5 @@ export const SubagentParams = Type.Object({
|
|
|
165
290
|
outputMode: Type.Optional(OutputModeOverride),
|
|
166
291
|
skill: Type.Optional(SkillOverride),
|
|
167
292
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
293
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
168
294
|
});
|
|
@@ -20,8 +20,10 @@ export function resolveSubagentResultStatus(input: {
|
|
|
20
20
|
state?: string;
|
|
21
21
|
interrupted?: boolean;
|
|
22
22
|
detached?: boolean;
|
|
23
|
+
timedOut?: boolean;
|
|
23
24
|
}): SubagentResultStatus {
|
|
24
25
|
if (input.detached) return "detached";
|
|
26
|
+
if (input.timedOut || input.state === "timed-out") return "timed-out";
|
|
25
27
|
if (input.interrupted || input.state === "paused") return "paused";
|
|
26
28
|
if (typeof input.success === "boolean") return input.success ? "completed" : "failed";
|
|
27
29
|
if (input.state === "complete") return "completed";
|
|
@@ -36,6 +38,7 @@ function countStatuses(children: SubagentResultIntercomChild[]): Record<Subagent
|
|
|
36
38
|
failed: 0,
|
|
37
39
|
paused: 0,
|
|
38
40
|
detached: 0,
|
|
41
|
+
"timed-out": 0,
|
|
39
42
|
};
|
|
40
43
|
for (const child of children) {
|
|
41
44
|
counts[child.status] += 1;
|
|
@@ -49,6 +52,7 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
|
|
|
49
52
|
counts.failed ? `${counts.failed} failed` : undefined,
|
|
50
53
|
counts.paused ? `${counts.paused} paused` : undefined,
|
|
51
54
|
counts.detached ? `${counts.detached} detached` : undefined,
|
|
55
|
+
counts["timed-out"] ? `${counts["timed-out"]} timed out` : undefined,
|
|
52
56
|
].filter((part): part is string => Boolean(part));
|
|
53
57
|
return parts.length ? parts.join(", ") : "0 results";
|
|
54
58
|
}
|
|
@@ -56,6 +60,7 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
|
|
|
56
60
|
function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
|
|
57
61
|
const counts = countStatuses(children);
|
|
58
62
|
if (counts.failed > 0) return "failed";
|
|
63
|
+
if (counts["timed-out"] > 0) return "timed-out";
|
|
59
64
|
if (counts.paused > 0) return "paused";
|
|
60
65
|
if (counts.completed > 0) return "completed";
|
|
61
66
|
if (counts.detached > 0) return "detached";
|
|
@@ -12,7 +12,7 @@ import type { ExtensionAPI } from "@earendil-works/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, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
|
-
import { buildChainInstructions, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
15
|
+
import { buildChainInstructions, isDynamicParallelStep, 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";
|
|
@@ -20,7 +20,12 @@ import { resolveChildCwd } from "../../shared/utils.ts";
|
|
|
20
20
|
import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
|
|
21
21
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
22
22
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
23
|
+
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
24
|
+
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
25
|
+
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
26
|
+
import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
|
|
23
27
|
import {
|
|
28
|
+
type AcceptanceInput,
|
|
24
29
|
type ArtifactConfig,
|
|
25
30
|
type Details,
|
|
26
31
|
type MaxOutputConfig,
|
|
@@ -107,6 +112,7 @@ interface AsyncChainParams {
|
|
|
107
112
|
sessionRoot?: string;
|
|
108
113
|
chainSkills?: string[];
|
|
109
114
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
115
|
+
dynamicFanoutMaxItems?: number;
|
|
110
116
|
maxSubagentDepth: number;
|
|
111
117
|
worktreeSetupHook?: string;
|
|
112
118
|
worktreeSetupHookTimeoutMs?: number;
|
|
@@ -114,6 +120,7 @@ interface AsyncChainParams {
|
|
|
114
120
|
controlIntercomTarget?: string;
|
|
115
121
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
116
122
|
nestedRoute?: NestedRouteInfo;
|
|
123
|
+
acceptance?: AcceptanceInput;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
interface AsyncSingleParams {
|
|
@@ -140,6 +147,7 @@ interface AsyncSingleParams {
|
|
|
140
147
|
controlIntercomTarget?: string;
|
|
141
148
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
142
149
|
nestedRoute?: NestedRouteInfo;
|
|
150
|
+
acceptance?: AcceptanceInput;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
interface AsyncExecutionResult {
|
|
@@ -248,12 +256,25 @@ export function executeAsyncChain(
|
|
|
248
256
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
249
257
|
const firstStep = chain[0];
|
|
250
258
|
const originalTask = params.task ?? (firstStep
|
|
251
|
-
? (isParallelStep(firstStep)
|
|
259
|
+
? (isParallelStep(firstStep)
|
|
260
|
+
? firstStep.parallel[0]?.task
|
|
261
|
+
: isDynamicParallelStep(firstStep)
|
|
262
|
+
? firstStep.parallel.task
|
|
263
|
+
: (firstStep as SequentialStep).task)
|
|
252
264
|
: undefined);
|
|
265
|
+
try {
|
|
266
|
+
validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof ChainOutputValidationError) return formatAsyncStartError(resultMode, error.message);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: chain });
|
|
253
272
|
|
|
254
273
|
for (const s of chain) {
|
|
255
274
|
const stepAgents = isParallelStep(s)
|
|
256
275
|
? s.parallel.map((t) => t.agent)
|
|
276
|
+
: isDynamicParallelStep(s)
|
|
277
|
+
? [s.parallel.agent]
|
|
257
278
|
: [(s as SequentialStep).agent];
|
|
258
279
|
for (const agentName of stepAgents) {
|
|
259
280
|
if (!agents.find((x) => x.name === agentName)) {
|
|
@@ -316,13 +337,20 @@ export function executeAsyncChain(
|
|
|
316
337
|
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
317
338
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
318
339
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
319
|
-
|
|
340
|
+
let taskTemplate = s.task ?? "{previous}";
|
|
341
|
+
taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
|
|
342
|
+
taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
|
|
343
|
+
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
|
|
320
344
|
|
|
321
345
|
const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
|
|
322
346
|
const model = applyThinkingSuffix(primaryModel, a.thinking);
|
|
323
347
|
return {
|
|
324
348
|
agent: s.agent,
|
|
325
349
|
task,
|
|
350
|
+
phase: s.phase,
|
|
351
|
+
label: s.label,
|
|
352
|
+
outputName: s.as,
|
|
353
|
+
structured: Boolean(s.outputSchema),
|
|
326
354
|
cwd: stepCwd,
|
|
327
355
|
model,
|
|
328
356
|
thinking: resolveEffectiveThinking(model, a.thinking),
|
|
@@ -342,6 +370,18 @@ export function executeAsyncChain(
|
|
|
342
370
|
outputMode: behavior.outputMode,
|
|
343
371
|
sessionFile,
|
|
344
372
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
373
|
+
maxExecutionTimeMs: a.maxExecutionTimeMs,
|
|
374
|
+
maxTokens: a.maxTokens,
|
|
375
|
+
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
376
|
+
explicit: s.acceptance,
|
|
377
|
+
agentName: s.agent,
|
|
378
|
+
task: s.task,
|
|
379
|
+
mode: resultMode,
|
|
380
|
+
async: true,
|
|
381
|
+
dynamic: false,
|
|
382
|
+
}),
|
|
383
|
+
...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
|
|
384
|
+
...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
|
|
345
385
|
};
|
|
346
386
|
};
|
|
347
387
|
|
|
@@ -382,6 +422,24 @@ export function executeAsyncChain(
|
|
|
382
422
|
worktree: s.worktree,
|
|
383
423
|
};
|
|
384
424
|
}
|
|
425
|
+
if (isDynamicParallelStep(s)) {
|
|
426
|
+
const agent = agents.find((candidate) => candidate.name === s.parallel.agent)!;
|
|
427
|
+
const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(s.parallel), chainSkills), s.parallel.task, originalTask);
|
|
428
|
+
const progressPrecreated = behavior.progress;
|
|
429
|
+
if (progressPrecreated) {
|
|
430
|
+
writeInitialProgressFile(runnerCwd);
|
|
431
|
+
progressInstructionCreated = true;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
expand: s.expand,
|
|
435
|
+
parallel: buildSeqStep(s.parallel as SequentialStep, undefined, undefined, progressPrecreated, behavior),
|
|
436
|
+
collect: s.collect,
|
|
437
|
+
concurrency: s.concurrency,
|
|
438
|
+
failFast: s.failFast,
|
|
439
|
+
phase: s.phase,
|
|
440
|
+
label: s.label,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
385
443
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
386
444
|
});
|
|
387
445
|
} catch (error) {
|
|
@@ -391,6 +449,10 @@ export function executeAsyncChain(
|
|
|
391
449
|
let childTargetIndex = 0;
|
|
392
450
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
393
451
|
if ("parallel" in step) {
|
|
452
|
+
if (!Array.isArray(step.parallel)) {
|
|
453
|
+
childTargetIndex++;
|
|
454
|
+
return [undefined];
|
|
455
|
+
}
|
|
394
456
|
return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
|
|
395
457
|
}
|
|
396
458
|
return [childIntercomTarget(step.agent, childTargetIndex++)];
|
|
@@ -420,6 +482,8 @@ export function executeAsyncChain(
|
|
|
420
482
|
controlIntercomTarget,
|
|
421
483
|
childIntercomTargets,
|
|
422
484
|
resultMode,
|
|
485
|
+
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
486
|
+
workflowGraph,
|
|
423
487
|
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
424
488
|
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
425
489
|
parentRunId: nestedAddress.parentRunId,
|
|
@@ -444,6 +508,8 @@ export function executeAsyncChain(
|
|
|
444
508
|
const firstStep = chain[0];
|
|
445
509
|
const firstAgents = isParallelStep(firstStep)
|
|
446
510
|
? firstStep.parallel.map((t) => t.agent)
|
|
511
|
+
: isDynamicParallelStep(firstStep)
|
|
512
|
+
? [firstStep.parallel.agent]
|
|
447
513
|
: [(firstStep as SequentialStep).agent];
|
|
448
514
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
449
515
|
const flatAgents: string[] = [];
|
|
@@ -454,6 +520,10 @@ export function executeAsyncChain(
|
|
|
454
520
|
parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
|
|
455
521
|
flatAgents.push(...step.parallel.map((task) => task.agent));
|
|
456
522
|
flatStepStart += step.parallel.length;
|
|
523
|
+
} else if (isDynamicParallelStep(step)) {
|
|
524
|
+
parallelGroups.push({ start: flatStepStart, count: 1, stepIndex });
|
|
525
|
+
flatAgents.push(step.parallel.agent);
|
|
526
|
+
flatStepStart++;
|
|
457
527
|
} else {
|
|
458
528
|
flatAgents.push((step as SequentialStep).agent);
|
|
459
529
|
flatStepStart++;
|
|
@@ -502,12 +572,15 @@ export function executeAsyncChain(
|
|
|
502
572
|
agents: flatAgents,
|
|
503
573
|
task: isParallelStep(firstStep)
|
|
504
574
|
? firstStep.parallel[0]?.task?.slice(0, 50)
|
|
575
|
+
: isDynamicParallelStep(firstStep)
|
|
576
|
+
? firstStep.parallel.task?.slice(0, 50)
|
|
505
577
|
: (firstStep as SequentialStep).task?.slice(0, 50),
|
|
506
578
|
chain: chain.map((s) =>
|
|
507
|
-
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
|
|
579
|
+
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
508
580
|
),
|
|
509
581
|
chainStepCount: chain.length,
|
|
510
582
|
parallelGroups,
|
|
583
|
+
workflowGraph,
|
|
511
584
|
cwd: runnerCwd,
|
|
512
585
|
asyncDir,
|
|
513
586
|
nestedRoute,
|
|
@@ -516,13 +589,13 @@ export function executeAsyncChain(
|
|
|
516
589
|
|
|
517
590
|
const chainDesc = chain
|
|
518
591
|
.map((s) =>
|
|
519
|
-
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
|
|
592
|
+
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
520
593
|
)
|
|
521
594
|
.join(" -> ");
|
|
522
595
|
|
|
523
596
|
return {
|
|
524
597
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
525
|
-
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir },
|
|
598
|
+
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph },
|
|
526
599
|
};
|
|
527
600
|
}
|
|
528
601
|
|
|
@@ -618,6 +691,15 @@ export function executeAsyncSingle(
|
|
|
618
691
|
outputMode,
|
|
619
692
|
sessionFile,
|
|
620
693
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
694
|
+
maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
|
|
695
|
+
maxTokens: agentConfig.maxTokens,
|
|
696
|
+
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
697
|
+
explicit: params.acceptance,
|
|
698
|
+
agentName: agent,
|
|
699
|
+
task,
|
|
700
|
+
mode: "single",
|
|
701
|
+
async: true,
|
|
702
|
+
}),
|
|
621
703
|
},
|
|
622
704
|
],
|
|
623
705
|
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
@@ -12,6 +12,10 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
|
|
|
12
12
|
interface AsyncRunStepSummary {
|
|
13
13
|
index: number;
|
|
14
14
|
agent: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
phase?: string;
|
|
17
|
+
outputName?: string;
|
|
18
|
+
structured?: boolean;
|
|
15
19
|
status: AsyncJobStep["status"];
|
|
16
20
|
activityState?: ActivityState;
|
|
17
21
|
lastActivityAt?: number;
|
|
@@ -139,6 +143,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
139
143
|
return {
|
|
140
144
|
index,
|
|
141
145
|
agent: step.agent,
|
|
146
|
+
...(step.label ? { label: step.label } : {}),
|
|
147
|
+
...(step.phase ? { phase: step.phase } : {}),
|
|
148
|
+
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
149
|
+
...(step.structured ? { structured: step.structured } : {}),
|
|
142
150
|
status: step.status,
|
|
143
151
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
144
152
|
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
@@ -259,7 +267,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
|
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
262
|
-
const
|
|
270
|
+
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
271
|
+
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
272
|
+
const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
|
|
263
273
|
const activity = formatActivityFacts(step);
|
|
264
274
|
if (activity) parts.push(activity);
|
|
265
275
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -49,6 +49,11 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
|
|
|
49
49
|
return "Resume: unavailable; no child session file was persisted.";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
|
|
53
|
+
if (!finalization) return "";
|
|
54
|
+
return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
53
58
|
const steps = status.steps ?? [];
|
|
54
59
|
if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
|
|
@@ -217,7 +222,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
217
222
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
218
223
|
const modelText = modelThinking ? ` (${modelThinking})` : "";
|
|
219
224
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
220
|
-
|
|
225
|
+
const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
|
|
226
|
+
const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
|
|
227
|
+
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
228
|
+
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
229
|
+
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
|
|
221
230
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
222
231
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
223
232
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|