pi-subagents 0.3.0 → 0.3.2
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 +35 -0
- package/agents.ts +0 -10
- package/async-execution.ts +261 -0
- package/chain-execution.ts +444 -0
- package/execution.ts +383 -0
- package/formatters.ts +111 -0
- package/index.ts +92 -1615
- package/package.json +2 -2
- package/render.ts +308 -0
- package/schemas.ts +90 -0
- package/settings.ts +2 -166
- package/types.ts +166 -0
- package/utils.ts +325 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain execution logic for subagent tool
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
8
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { AgentConfig } from "./agents.js";
|
|
10
|
+
import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveChainTemplates,
|
|
13
|
+
createChainDir,
|
|
14
|
+
removeChainDir,
|
|
15
|
+
resolveStepBehavior,
|
|
16
|
+
resolveParallelBehaviors,
|
|
17
|
+
buildChainInstructions,
|
|
18
|
+
createParallelDirs,
|
|
19
|
+
aggregateParallelOutputs,
|
|
20
|
+
isParallelStep,
|
|
21
|
+
type StepOverrides,
|
|
22
|
+
type ChainStep,
|
|
23
|
+
type SequentialStep,
|
|
24
|
+
type ParallelTaskResult,
|
|
25
|
+
type ResolvedTemplates,
|
|
26
|
+
} from "./settings.js";
|
|
27
|
+
import { runSync } from "./execution.js";
|
|
28
|
+
import { buildChainSummary } from "./formatters.js";
|
|
29
|
+
import { getFinalOutput, mapConcurrent } from "./utils.js";
|
|
30
|
+
import {
|
|
31
|
+
type AgentProgress,
|
|
32
|
+
type ArtifactConfig,
|
|
33
|
+
type ArtifactPaths,
|
|
34
|
+
type Details,
|
|
35
|
+
type SingleResult,
|
|
36
|
+
MAX_CONCURRENCY,
|
|
37
|
+
} from "./types.js";
|
|
38
|
+
|
|
39
|
+
export interface ChainExecutionParams {
|
|
40
|
+
chain: ChainStep[];
|
|
41
|
+
agents: AgentConfig[];
|
|
42
|
+
ctx: ExtensionContext;
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
runId: string;
|
|
45
|
+
cwd?: string;
|
|
46
|
+
shareEnabled: boolean;
|
|
47
|
+
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
48
|
+
artifactsDir: string;
|
|
49
|
+
artifactConfig: ArtifactConfig;
|
|
50
|
+
includeProgress?: boolean;
|
|
51
|
+
clarify?: boolean;
|
|
52
|
+
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ChainExecutionResult {
|
|
56
|
+
content: Array<{ type: "text"; text: string }>;
|
|
57
|
+
details: Details;
|
|
58
|
+
isError?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute a chain of subagent steps
|
|
63
|
+
*/
|
|
64
|
+
export async function executeChain(params: ChainExecutionParams): Promise<ChainExecutionResult> {
|
|
65
|
+
const {
|
|
66
|
+
chain: chainSteps,
|
|
67
|
+
agents,
|
|
68
|
+
ctx,
|
|
69
|
+
signal,
|
|
70
|
+
runId,
|
|
71
|
+
cwd,
|
|
72
|
+
shareEnabled,
|
|
73
|
+
sessionDirForIndex,
|
|
74
|
+
artifactsDir,
|
|
75
|
+
artifactConfig,
|
|
76
|
+
includeProgress,
|
|
77
|
+
clarify,
|
|
78
|
+
onUpdate,
|
|
79
|
+
} = params;
|
|
80
|
+
|
|
81
|
+
const allProgress: AgentProgress[] = [];
|
|
82
|
+
const allArtifactPaths: ArtifactPaths[] = [];
|
|
83
|
+
|
|
84
|
+
// Compute chain metadata for observability
|
|
85
|
+
const chainAgents: string[] = chainSteps.map((step) =>
|
|
86
|
+
isParallelStep(step)
|
|
87
|
+
? `[${step.parallel.map((t) => t.agent).join("+")}]`
|
|
88
|
+
: (step as SequentialStep).agent,
|
|
89
|
+
);
|
|
90
|
+
const totalSteps = chainSteps.length;
|
|
91
|
+
|
|
92
|
+
// Get original task from first step
|
|
93
|
+
const firstStep = chainSteps[0]!;
|
|
94
|
+
const originalTask = isParallelStep(firstStep)
|
|
95
|
+
? firstStep.parallel[0]!.task!
|
|
96
|
+
: (firstStep as SequentialStep).task!;
|
|
97
|
+
|
|
98
|
+
// Create chain directory
|
|
99
|
+
const chainDir = createChainDir(runId);
|
|
100
|
+
|
|
101
|
+
// Check if chain has any parallel steps
|
|
102
|
+
const hasParallelSteps = chainSteps.some(isParallelStep);
|
|
103
|
+
|
|
104
|
+
// Resolve templates (parallel-aware)
|
|
105
|
+
let templates: ResolvedTemplates = resolveChainTemplates(chainSteps);
|
|
106
|
+
|
|
107
|
+
// For TUI: only show if no parallel steps (TUI v1 doesn't support parallel display)
|
|
108
|
+
const shouldClarify = clarify !== false && ctx.hasUI && !hasParallelSteps;
|
|
109
|
+
|
|
110
|
+
// Behavior overrides from TUI (set if TUI is shown, undefined otherwise)
|
|
111
|
+
let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
|
|
112
|
+
|
|
113
|
+
if (shouldClarify) {
|
|
114
|
+
// Sequential-only chain: use existing TUI
|
|
115
|
+
const seqSteps = chainSteps as SequentialStep[];
|
|
116
|
+
|
|
117
|
+
// Load agent configs for sequential steps
|
|
118
|
+
const agentConfigs: AgentConfig[] = [];
|
|
119
|
+
for (const step of seqSteps) {
|
|
120
|
+
const config = agents.find((a) => a.name === step.agent);
|
|
121
|
+
if (!config) {
|
|
122
|
+
removeChainDir(chainDir);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `Unknown agent: ${step.agent}` }],
|
|
125
|
+
isError: true,
|
|
126
|
+
details: { mode: "chain" as const, results: [] },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
agentConfigs.push(config);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Build step overrides
|
|
133
|
+
const stepOverrides: StepOverrides[] = seqSteps.map((step) => ({
|
|
134
|
+
output: step.output,
|
|
135
|
+
reads: step.reads,
|
|
136
|
+
progress: step.progress,
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
// Pre-resolve behaviors for TUI display
|
|
140
|
+
const resolvedBehaviors = agentConfigs.map((config, i) =>
|
|
141
|
+
resolveStepBehavior(config, stepOverrides[i]!),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Flatten templates for TUI (all strings for sequential)
|
|
145
|
+
const flatTemplates = templates as string[];
|
|
146
|
+
|
|
147
|
+
const result = await ctx.ui.custom<ChainClarifyResult>(
|
|
148
|
+
(tui, theme, _kb, done) =>
|
|
149
|
+
new ChainClarifyComponent(
|
|
150
|
+
tui,
|
|
151
|
+
theme,
|
|
152
|
+
agentConfigs,
|
|
153
|
+
flatTemplates,
|
|
154
|
+
originalTask,
|
|
155
|
+
chainDir,
|
|
156
|
+
resolvedBehaviors,
|
|
157
|
+
done,
|
|
158
|
+
),
|
|
159
|
+
{
|
|
160
|
+
overlay: true,
|
|
161
|
+
overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" },
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (!result || !result.confirmed) {
|
|
166
|
+
removeChainDir(chainDir);
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: "text", text: "Chain cancelled" }],
|
|
169
|
+
details: { mode: "chain", results: [] },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Update templates from TUI result
|
|
173
|
+
templates = result.templates;
|
|
174
|
+
// Store behavior overrides from TUI (used below in sequential step execution)
|
|
175
|
+
tuiBehaviorOverrides = result.behaviorOverrides;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Execute chain (handles both sequential and parallel steps)
|
|
179
|
+
const results: SingleResult[] = [];
|
|
180
|
+
let prev = "";
|
|
181
|
+
let globalTaskIndex = 0; // For unique artifact naming
|
|
182
|
+
let progressCreated = false; // Track if progress.md has been created
|
|
183
|
+
|
|
184
|
+
for (let stepIndex = 0; stepIndex < chainSteps.length; stepIndex++) {
|
|
185
|
+
const step = chainSteps[stepIndex]!;
|
|
186
|
+
const stepTemplates = templates[stepIndex]!;
|
|
187
|
+
|
|
188
|
+
if (isParallelStep(step)) {
|
|
189
|
+
// === PARALLEL STEP EXECUTION ===
|
|
190
|
+
const parallelTemplates = stepTemplates as string[];
|
|
191
|
+
const concurrency = step.concurrency ?? MAX_CONCURRENCY;
|
|
192
|
+
const failFast = step.failFast ?? false;
|
|
193
|
+
|
|
194
|
+
// Create subdirectories for parallel outputs
|
|
195
|
+
const agentNames = step.parallel.map((t) => t.agent);
|
|
196
|
+
createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
|
|
197
|
+
|
|
198
|
+
// Resolve behaviors for parallel tasks
|
|
199
|
+
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex);
|
|
200
|
+
|
|
201
|
+
// If any parallel task has progress enabled and progress.md hasn't been created,
|
|
202
|
+
// create it now to avoid race conditions
|
|
203
|
+
const anyNeedsProgress = parallelBehaviors.some((b) => b.progress);
|
|
204
|
+
if (anyNeedsProgress && !progressCreated) {
|
|
205
|
+
const progressPath = path.join(chainDir, "progress.md");
|
|
206
|
+
fs.writeFileSync(progressPath, "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n");
|
|
207
|
+
progressCreated = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Track if we should abort remaining tasks (for fail-fast)
|
|
211
|
+
let aborted = false;
|
|
212
|
+
|
|
213
|
+
// Execute parallel tasks
|
|
214
|
+
const parallelResults = await mapConcurrent(
|
|
215
|
+
step.parallel,
|
|
216
|
+
concurrency,
|
|
217
|
+
async (task, taskIndex) => {
|
|
218
|
+
if (aborted && failFast) {
|
|
219
|
+
// Return a placeholder for skipped tasks
|
|
220
|
+
return {
|
|
221
|
+
agent: task.agent,
|
|
222
|
+
task: "(skipped)",
|
|
223
|
+
exitCode: -1,
|
|
224
|
+
messages: [],
|
|
225
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
226
|
+
error: "Skipped due to fail-fast",
|
|
227
|
+
} as SingleResult;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Build task string
|
|
231
|
+
const taskTemplate = parallelTemplates[taskIndex] ?? "{previous}";
|
|
232
|
+
const templateHasPrevious = taskTemplate.includes("{previous}");
|
|
233
|
+
let taskStr = taskTemplate;
|
|
234
|
+
taskStr = taskStr.replace(/\{task\}/g, originalTask);
|
|
235
|
+
taskStr = taskStr.replace(/\{previous\}/g, prev);
|
|
236
|
+
taskStr = taskStr.replace(/\{chain_dir\}/g, chainDir);
|
|
237
|
+
|
|
238
|
+
// Add chain instructions (include previous summary only if not already in template)
|
|
239
|
+
const behavior = parallelBehaviors[taskIndex]!;
|
|
240
|
+
// For parallel, no single "first progress" - each manages independently
|
|
241
|
+
taskStr += buildChainInstructions(behavior, chainDir, false, templateHasPrevious ? undefined : prev);
|
|
242
|
+
|
|
243
|
+
const r = await runSync(ctx.cwd, agents, task.agent, taskStr, {
|
|
244
|
+
cwd: task.cwd ?? cwd,
|
|
245
|
+
signal,
|
|
246
|
+
runId,
|
|
247
|
+
index: globalTaskIndex + taskIndex,
|
|
248
|
+
sessionDir: sessionDirForIndex(globalTaskIndex + taskIndex),
|
|
249
|
+
share: shareEnabled,
|
|
250
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
251
|
+
artifactConfig,
|
|
252
|
+
onUpdate: onUpdate
|
|
253
|
+
? (p) => {
|
|
254
|
+
// Use concat instead of spread for better performance
|
|
255
|
+
const stepResults = p.details?.results || [];
|
|
256
|
+
const stepProgress = p.details?.progress || [];
|
|
257
|
+
onUpdate({
|
|
258
|
+
...p,
|
|
259
|
+
details: {
|
|
260
|
+
mode: "chain",
|
|
261
|
+
results: results.concat(stepResults),
|
|
262
|
+
progress: allProgress.concat(stepProgress),
|
|
263
|
+
chainAgents,
|
|
264
|
+
totalSteps,
|
|
265
|
+
currentStepIndex: stepIndex,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
: undefined,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (r.exitCode !== 0 && failFast) {
|
|
273
|
+
aborted = true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return r;
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Update global task index
|
|
281
|
+
globalTaskIndex += step.parallel.length;
|
|
282
|
+
|
|
283
|
+
// Collect results and progress
|
|
284
|
+
for (const r of parallelResults) {
|
|
285
|
+
results.push(r);
|
|
286
|
+
if (r.progress) allProgress.push(r.progress);
|
|
287
|
+
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for failures (track original task index for better error messages)
|
|
291
|
+
const failures = parallelResults
|
|
292
|
+
.map((r, originalIndex) => ({ ...r, originalIndex }))
|
|
293
|
+
.filter((r) => r.exitCode !== 0 && r.exitCode !== -1);
|
|
294
|
+
if (failures.length > 0) {
|
|
295
|
+
const failureSummary = failures
|
|
296
|
+
.map((f) => `- Task ${f.originalIndex + 1} (${f.agent}): ${f.error || "failed"}`)
|
|
297
|
+
.join("\n");
|
|
298
|
+
const errorMsg = `Parallel step ${stepIndex + 1} failed:\n${failureSummary}`;
|
|
299
|
+
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
300
|
+
index: stepIndex,
|
|
301
|
+
error: errorMsg,
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: summary }],
|
|
305
|
+
details: {
|
|
306
|
+
mode: "chain",
|
|
307
|
+
results,
|
|
308
|
+
progress: includeProgress ? allProgress : undefined,
|
|
309
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
310
|
+
chainAgents,
|
|
311
|
+
totalSteps,
|
|
312
|
+
currentStepIndex: stepIndex,
|
|
313
|
+
},
|
|
314
|
+
isError: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Aggregate outputs for {previous}
|
|
319
|
+
const taskResults: ParallelTaskResult[] = parallelResults.map((r, i) => ({
|
|
320
|
+
agent: r.agent,
|
|
321
|
+
taskIndex: i,
|
|
322
|
+
output: getFinalOutput(r.messages),
|
|
323
|
+
exitCode: r.exitCode,
|
|
324
|
+
error: r.error,
|
|
325
|
+
}));
|
|
326
|
+
prev = aggregateParallelOutputs(taskResults);
|
|
327
|
+
} else {
|
|
328
|
+
// === SEQUENTIAL STEP EXECUTION ===
|
|
329
|
+
const seqStep = step as SequentialStep;
|
|
330
|
+
const stepTemplate = stepTemplates as string;
|
|
331
|
+
|
|
332
|
+
// Get agent config
|
|
333
|
+
const agentConfig = agents.find((a) => a.name === seqStep.agent);
|
|
334
|
+
if (!agentConfig) {
|
|
335
|
+
removeChainDir(chainDir);
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: "text", text: `Unknown agent: ${seqStep.agent}` }],
|
|
338
|
+
isError: true,
|
|
339
|
+
details: { mode: "chain" as const, results: [] },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Build task string (check if template has {previous} before replacement)
|
|
344
|
+
const templateHasPrevious = stepTemplate.includes("{previous}");
|
|
345
|
+
let stepTask = stepTemplate;
|
|
346
|
+
stepTask = stepTask.replace(/\{task\}/g, originalTask);
|
|
347
|
+
stepTask = stepTask.replace(/\{previous\}/g, prev);
|
|
348
|
+
stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
|
|
349
|
+
|
|
350
|
+
// Resolve behavior (TUI overrides take precedence over step config)
|
|
351
|
+
const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
|
|
352
|
+
const stepOverride: StepOverrides = {
|
|
353
|
+
output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
|
|
354
|
+
reads: tuiOverride?.reads !== undefined ? tuiOverride.reads : seqStep.reads,
|
|
355
|
+
progress: tuiOverride?.progress !== undefined ? tuiOverride.progress : seqStep.progress,
|
|
356
|
+
};
|
|
357
|
+
const behavior = resolveStepBehavior(agentConfig, stepOverride);
|
|
358
|
+
|
|
359
|
+
// Determine if this is the first agent to create progress.md
|
|
360
|
+
const isFirstProgress = behavior.progress && !progressCreated;
|
|
361
|
+
if (isFirstProgress) {
|
|
362
|
+
progressCreated = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Add chain instructions (include previous summary only if not already in template)
|
|
366
|
+
stepTask += buildChainInstructions(behavior, chainDir, isFirstProgress, templateHasPrevious ? undefined : prev);
|
|
367
|
+
|
|
368
|
+
// Run step
|
|
369
|
+
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
370
|
+
cwd: seqStep.cwd ?? cwd,
|
|
371
|
+
signal,
|
|
372
|
+
runId,
|
|
373
|
+
index: globalTaskIndex,
|
|
374
|
+
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
375
|
+
share: shareEnabled,
|
|
376
|
+
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
377
|
+
artifactConfig,
|
|
378
|
+
onUpdate: onUpdate
|
|
379
|
+
? (p) => {
|
|
380
|
+
// Use concat instead of spread for better performance
|
|
381
|
+
const stepResults = p.details?.results || [];
|
|
382
|
+
const stepProgress = p.details?.progress || [];
|
|
383
|
+
onUpdate({
|
|
384
|
+
...p,
|
|
385
|
+
details: {
|
|
386
|
+
mode: "chain",
|
|
387
|
+
results: results.concat(stepResults),
|
|
388
|
+
progress: allProgress.concat(stepProgress),
|
|
389
|
+
chainAgents,
|
|
390
|
+
totalSteps,
|
|
391
|
+
currentStepIndex: stepIndex,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
: undefined,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
globalTaskIndex++;
|
|
399
|
+
results.push(r);
|
|
400
|
+
if (r.progress) allProgress.push(r.progress);
|
|
401
|
+
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
402
|
+
|
|
403
|
+
// On failure, leave chain_dir for debugging
|
|
404
|
+
if (r.exitCode !== 0) {
|
|
405
|
+
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
|
406
|
+
index: stepIndex,
|
|
407
|
+
error: r.error || "Chain failed",
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: "text", text: summary }],
|
|
411
|
+
details: {
|
|
412
|
+
mode: "chain",
|
|
413
|
+
results,
|
|
414
|
+
progress: includeProgress ? allProgress : undefined,
|
|
415
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
416
|
+
chainAgents,
|
|
417
|
+
totalSteps,
|
|
418
|
+
currentStepIndex: stepIndex,
|
|
419
|
+
},
|
|
420
|
+
isError: true,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
prev = getFinalOutput(r.messages);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Chain complete - return summary with paths
|
|
429
|
+
// Chain dir left for inspection (cleaned up after 24h)
|
|
430
|
+
const summary = buildChainSummary(chainSteps, results, chainDir, "completed");
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
content: [{ type: "text", text: summary }],
|
|
434
|
+
details: {
|
|
435
|
+
mode: "chain",
|
|
436
|
+
results,
|
|
437
|
+
progress: includeProgress ? allProgress : undefined,
|
|
438
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
439
|
+
chainAgents,
|
|
440
|
+
totalSteps,
|
|
441
|
+
// currentStepIndex omitted for completed chains
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|