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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"cli"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
|
-
"pi-subagents": "
|
|
24
|
+
"pi-subagents": "install.mjs"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"*.ts",
|
package/render.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering functions for subagent results
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
6
|
+
import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { Container, Markdown, Spacer, Text, type Widget } from "@mariozechner/pi-tui";
|
|
8
|
+
import {
|
|
9
|
+
type AsyncJobState,
|
|
10
|
+
type Details,
|
|
11
|
+
MAX_WIDGET_JOBS,
|
|
12
|
+
WIDGET_KEY,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.js";
|
|
15
|
+
import { getFinalOutput, getDisplayItems, getOutputTail, getLastActivity } from "./utils.js";
|
|
16
|
+
|
|
17
|
+
type Theme = ExtensionContext["ui"]["theme"];
|
|
18
|
+
|
|
19
|
+
// Track last rendered widget state to avoid no-op re-renders
|
|
20
|
+
let lastWidgetHash = "";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compute a simple hash of job states for change detection
|
|
24
|
+
*/
|
|
25
|
+
function computeWidgetHash(jobs: AsyncJobState[]): string {
|
|
26
|
+
return jobs.slice(0, MAX_WIDGET_JOBS).map(job =>
|
|
27
|
+
`${job.asyncId}:${job.status}:${job.currentStep}:${job.updatedAt}:${job.totalTokens?.total ?? 0}`
|
|
28
|
+
).join("|");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render the async jobs widget
|
|
33
|
+
*/
|
|
34
|
+
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
35
|
+
if (!ctx.hasUI) return;
|
|
36
|
+
if (jobs.length === 0) {
|
|
37
|
+
if (lastWidgetHash !== "") {
|
|
38
|
+
lastWidgetHash = "";
|
|
39
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if anything changed since last render
|
|
45
|
+
// Always re-render if any displayed job is running (output tail updates constantly)
|
|
46
|
+
const displayedJobs = jobs.slice(0, MAX_WIDGET_JOBS);
|
|
47
|
+
const hasRunningJobs = displayedJobs.some(job => job.status === "running");
|
|
48
|
+
const newHash = computeWidgetHash(jobs);
|
|
49
|
+
if (!hasRunningJobs && newHash === lastWidgetHash) {
|
|
50
|
+
return; // Skip re-render, nothing changed
|
|
51
|
+
}
|
|
52
|
+
lastWidgetHash = newHash;
|
|
53
|
+
|
|
54
|
+
const theme = ctx.ui.theme;
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
lines.push(theme.fg("accent", "Async subagents"));
|
|
57
|
+
|
|
58
|
+
for (const job of displayedJobs) {
|
|
59
|
+
const id = job.asyncId.slice(0, 6);
|
|
60
|
+
const status =
|
|
61
|
+
job.status === "complete"
|
|
62
|
+
? theme.fg("success", "complete")
|
|
63
|
+
: job.status === "failed"
|
|
64
|
+
? theme.fg("error", "failed")
|
|
65
|
+
: theme.fg("warning", "running");
|
|
66
|
+
|
|
67
|
+
const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
|
|
68
|
+
const stepIndex = job.currentStep !== undefined ? job.currentStep + 1 : undefined;
|
|
69
|
+
const stepText = stepIndex !== undefined ? `step ${stepIndex}/${stepsTotal}` : `steps ${stepsTotal}`;
|
|
70
|
+
const endTime = (job.status === "complete" || job.status === "failed") ? (job.updatedAt ?? Date.now()) : Date.now();
|
|
71
|
+
const elapsed = job.startedAt ? formatDuration(endTime - job.startedAt) : "";
|
|
72
|
+
const agentLabel = job.agents ? job.agents.join(" -> ") : (job.mode ?? "single");
|
|
73
|
+
|
|
74
|
+
const tokenText = job.totalTokens ? ` | ${formatTokens(job.totalTokens.total)} tok` : "";
|
|
75
|
+
const activityText = job.status === "running" ? getLastActivity(job.outputFile) : "";
|
|
76
|
+
const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
|
|
77
|
+
|
|
78
|
+
lines.push(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`);
|
|
79
|
+
|
|
80
|
+
if (job.status === "running" && job.outputFile) {
|
|
81
|
+
const tail = getOutputTail(job.outputFile, 3);
|
|
82
|
+
for (const line of tail) {
|
|
83
|
+
lines.push(theme.fg("dim", ` > ${line}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ctx.ui.setWidget(WIDGET_KEY, lines);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render a subagent result
|
|
93
|
+
*/
|
|
94
|
+
export function renderSubagentResult(
|
|
95
|
+
result: AgentToolResult<Details>,
|
|
96
|
+
_options: { expanded: boolean },
|
|
97
|
+
theme: Theme,
|
|
98
|
+
): Widget {
|
|
99
|
+
const d = result.details;
|
|
100
|
+
if (!d || !d.results.length) {
|
|
101
|
+
const t = result.content[0];
|
|
102
|
+
return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const mdTheme = getMarkdownTheme();
|
|
106
|
+
|
|
107
|
+
if (d.mode === "single" && d.results.length === 1) {
|
|
108
|
+
const r = d.results[0];
|
|
109
|
+
const isRunning = r.progress?.status === "running";
|
|
110
|
+
const icon = isRunning
|
|
111
|
+
? theme.fg("warning", "...")
|
|
112
|
+
: r.exitCode === 0
|
|
113
|
+
? theme.fg("success", "ok")
|
|
114
|
+
: theme.fg("error", "X");
|
|
115
|
+
const output = r.truncation?.text || getFinalOutput(r.messages);
|
|
116
|
+
|
|
117
|
+
const progressInfo = isRunning && r.progress
|
|
118
|
+
? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
|
|
119
|
+
: r.progressSummary
|
|
120
|
+
? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
|
|
121
|
+
: "";
|
|
122
|
+
|
|
123
|
+
const c = new Container();
|
|
124
|
+
c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, 0, 0));
|
|
125
|
+
c.addChild(new Spacer(1));
|
|
126
|
+
c.addChild(
|
|
127
|
+
new Text(theme.fg("dim", `Task: ${r.task.slice(0, 150)}${r.task.length > 150 ? "..." : ""}`), 0, 0),
|
|
128
|
+
);
|
|
129
|
+
c.addChild(new Spacer(1));
|
|
130
|
+
|
|
131
|
+
const items = getDisplayItems(r.messages);
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
if (item.type === "tool")
|
|
134
|
+
c.addChild(new Text(theme.fg("muted", formatToolCall(item.name, item.args)), 0, 0));
|
|
135
|
+
}
|
|
136
|
+
if (items.length) c.addChild(new Spacer(1));
|
|
137
|
+
|
|
138
|
+
if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
|
|
139
|
+
c.addChild(new Spacer(1));
|
|
140
|
+
c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
|
|
141
|
+
if (r.sessionFile) {
|
|
142
|
+
c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (r.artifactPaths) {
|
|
146
|
+
c.addChild(new Spacer(1));
|
|
147
|
+
c.addChild(new Text(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), 0, 0));
|
|
148
|
+
}
|
|
149
|
+
return c;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
153
|
+
|| d.results.some((r) => r.progress?.status === "running");
|
|
154
|
+
const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
|
|
155
|
+
const icon = hasRunning
|
|
156
|
+
? theme.fg("warning", "...")
|
|
157
|
+
: ok === d.results.length
|
|
158
|
+
? theme.fg("success", "ok")
|
|
159
|
+
: theme.fg("error", "X");
|
|
160
|
+
|
|
161
|
+
const totalSummary =
|
|
162
|
+
d.progressSummary ||
|
|
163
|
+
d.results.reduce(
|
|
164
|
+
(acc, r) => {
|
|
165
|
+
const prog = r.progress || r.progressSummary;
|
|
166
|
+
if (prog) {
|
|
167
|
+
acc.toolCount += prog.toolCount;
|
|
168
|
+
acc.tokens += prog.tokens;
|
|
169
|
+
acc.durationMs =
|
|
170
|
+
d.mode === "chain"
|
|
171
|
+
? acc.durationMs + prog.durationMs
|
|
172
|
+
: Math.max(acc.durationMs, prog.durationMs);
|
|
173
|
+
}
|
|
174
|
+
return acc;
|
|
175
|
+
},
|
|
176
|
+
{ toolCount: 0, tokens: 0, durationMs: 0 },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const summaryStr =
|
|
180
|
+
totalSummary.toolCount || totalSummary.tokens
|
|
181
|
+
? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
|
|
182
|
+
: "";
|
|
183
|
+
|
|
184
|
+
const modeLabel = d.mode === "parallel" ? "parallel (no live progress)" : d.mode;
|
|
185
|
+
// For parallel-in-chain, show task count (results) for consistency with step display
|
|
186
|
+
// For sequential chains, show logical step count
|
|
187
|
+
const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
|
|
188
|
+
const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
|
|
189
|
+
const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
|
|
190
|
+
const stepInfo = hasRunning ? ` ${currentStep}/${totalCount}` : ` ${ok}/${totalCount}`;
|
|
191
|
+
|
|
192
|
+
// Build chain visualization: "scout → planner" with status icons
|
|
193
|
+
// Note: Only works correctly for sequential chains. Chains with parallel steps
|
|
194
|
+
// (indicated by "[agent1+agent2]" format) have multiple results per step,
|
|
195
|
+
// breaking the 1:1 mapping between chainAgents and results.
|
|
196
|
+
const chainVis = d.chainAgents?.length && !hasParallelInChain
|
|
197
|
+
? d.chainAgents
|
|
198
|
+
.map((agent, i) => {
|
|
199
|
+
const result = d.results[i];
|
|
200
|
+
const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
|
|
201
|
+
const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
|
|
202
|
+
const isCurrent = i === (d.currentStepIndex ?? d.results.length);
|
|
203
|
+
const icon = isFailed
|
|
204
|
+
? theme.fg("error", "✗")
|
|
205
|
+
: isComplete
|
|
206
|
+
? theme.fg("success", "✓")
|
|
207
|
+
: isCurrent && hasRunning
|
|
208
|
+
? theme.fg("warning", "●")
|
|
209
|
+
: theme.fg("dim", "○");
|
|
210
|
+
return `${icon} ${agent}`;
|
|
211
|
+
})
|
|
212
|
+
.join(theme.fg("dim", " → "))
|
|
213
|
+
: null;
|
|
214
|
+
|
|
215
|
+
const c = new Container();
|
|
216
|
+
c.addChild(
|
|
217
|
+
new Text(
|
|
218
|
+
`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`,
|
|
219
|
+
0,
|
|
220
|
+
0,
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
// Show chain visualization
|
|
224
|
+
if (chainVis) {
|
|
225
|
+
c.addChild(new Text(` ${chainVis}`, 0, 0));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// === STATIC STEP LAYOUT (like clarification UI) ===
|
|
229
|
+
// Each step gets a fixed section with task/output/status
|
|
230
|
+
// Note: For chains with parallel steps, chainAgents indices don't map 1:1 to results
|
|
231
|
+
// (parallel steps produce multiple results). Fall back to result-based iteration.
|
|
232
|
+
const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
|
|
233
|
+
const stepsToShow = useResultsDirectly ? d.results.length : d.chainAgents!.length;
|
|
234
|
+
|
|
235
|
+
c.addChild(new Spacer(1));
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < stepsToShow; i++) {
|
|
238
|
+
const r = d.results[i];
|
|
239
|
+
const agentName = useResultsDirectly
|
|
240
|
+
? (r?.agent || `step-${i + 1}`)
|
|
241
|
+
: (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
|
|
242
|
+
|
|
243
|
+
if (!r) {
|
|
244
|
+
// Pending step
|
|
245
|
+
c.addChild(new Text(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), 0, 0));
|
|
246
|
+
c.addChild(new Text(theme.fg("dim", ` status: ○ pending`), 0, 0));
|
|
247
|
+
c.addChild(new Spacer(1));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const progressFromArray = d.progress?.find((p) => p.index === i)
|
|
252
|
+
|| d.progress?.find((p) => p.agent === r.agent && p.status === "running");
|
|
253
|
+
const rProg = r.progress || progressFromArray || r.progressSummary;
|
|
254
|
+
const rRunning = rProg?.status === "running";
|
|
255
|
+
|
|
256
|
+
// Step header with status
|
|
257
|
+
const statusIcon = rRunning
|
|
258
|
+
? theme.fg("warning", "●")
|
|
259
|
+
: r.exitCode === 0
|
|
260
|
+
? theme.fg("success", "✓")
|
|
261
|
+
: theme.fg("error", "✗");
|
|
262
|
+
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
263
|
+
const stepHeader = rRunning
|
|
264
|
+
? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${stats}`
|
|
265
|
+
: `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${stats}`;
|
|
266
|
+
c.addChild(new Text(stepHeader, 0, 0));
|
|
267
|
+
|
|
268
|
+
// Task (truncated)
|
|
269
|
+
const taskPreview = r.task.slice(0, 120) + (r.task.length > 120 ? "..." : "");
|
|
270
|
+
c.addChild(new Text(theme.fg("dim", ` task: ${taskPreview}`), 0, 0));
|
|
271
|
+
|
|
272
|
+
// Output target (extract from task)
|
|
273
|
+
const outputMatch = r.task.match(/[Oo]utput(?:\s+to)?\s+([^\s]+\.(?:md|txt|json))/);
|
|
274
|
+
if (outputMatch) {
|
|
275
|
+
c.addChild(new Text(theme.fg("dim", ` output: ${outputMatch[1]}`), 0, 0));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (rRunning && rProg) {
|
|
279
|
+
// Current tool for running step
|
|
280
|
+
if (rProg.currentTool) {
|
|
281
|
+
const toolLine = rProg.currentToolArgs
|
|
282
|
+
? `${rProg.currentTool}: ${rProg.currentToolArgs.slice(0, 100)}${rProg.currentToolArgs.length > 100 ? "..." : ""}`
|
|
283
|
+
: rProg.currentTool;
|
|
284
|
+
c.addChild(new Text(theme.fg("warning", ` > ${toolLine}`), 0, 0));
|
|
285
|
+
}
|
|
286
|
+
// Recent tools
|
|
287
|
+
if (rProg.recentTools?.length) {
|
|
288
|
+
for (const t of rProg.recentTools.slice(0, 3)) {
|
|
289
|
+
const args = t.args.slice(0, 90) + (t.args.length > 90 ? "..." : "");
|
|
290
|
+
c.addChild(new Text(theme.fg("dim", ` ${t.tool}: ${args}`), 0, 0));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Recent output (limited)
|
|
294
|
+
const recentLines = (rProg.recentOutput ?? []).slice(-5);
|
|
295
|
+
for (const line of recentLines) {
|
|
296
|
+
c.addChild(new Text(theme.fg("dim", ` ${line.slice(0, 100)}${line.length > 100 ? "..." : ""}`), 0, 0));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
c.addChild(new Spacer(1));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (d.artifacts) {
|
|
304
|
+
c.addChild(new Spacer(1));
|
|
305
|
+
c.addChild(new Text(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), 0, 0));
|
|
306
|
+
}
|
|
307
|
+
return c;
|
|
308
|
+
}
|
package/schemas.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeBox schemas for subagent tool parameters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
|
|
7
|
+
export const TaskItem = Type.Object({
|
|
8
|
+
agent: Type.String(),
|
|
9
|
+
task: Type.String(),
|
|
10
|
+
cwd: Type.Optional(Type.String())
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Sequential chain step (single agent)
|
|
14
|
+
export const SequentialStepSchema = Type.Object({
|
|
15
|
+
agent: Type.String(),
|
|
16
|
+
task: Type.Optional(Type.String({ description: "Task template. Use {task}, {previous}, {chain_dir}. Required for first step." })),
|
|
17
|
+
cwd: Type.Optional(Type.String()),
|
|
18
|
+
// Chain behavior overrides
|
|
19
|
+
output: Type.Optional(Type.Union([
|
|
20
|
+
Type.String(),
|
|
21
|
+
Type.Boolean(),
|
|
22
|
+
], { description: "Override output filename (string), or false for text-only" })),
|
|
23
|
+
reads: Type.Optional(Type.Union([
|
|
24
|
+
Type.Array(Type.String()),
|
|
25
|
+
Type.Boolean(),
|
|
26
|
+
], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
|
|
27
|
+
progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Parallel task item (within a parallel step)
|
|
31
|
+
export const ParallelTaskSchema = Type.Object({
|
|
32
|
+
agent: Type.String(),
|
|
33
|
+
task: Type.Optional(Type.String({ description: "Task template. Defaults to {previous}." })),
|
|
34
|
+
cwd: Type.Optional(Type.String()),
|
|
35
|
+
output: Type.Optional(Type.Union([
|
|
36
|
+
Type.String(),
|
|
37
|
+
Type.Boolean(),
|
|
38
|
+
], { description: "Override output filename (string), or false for text-only" })),
|
|
39
|
+
reads: Type.Optional(Type.Union([
|
|
40
|
+
Type.Array(Type.String()),
|
|
41
|
+
Type.Boolean(),
|
|
42
|
+
], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
|
|
43
|
+
progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Parallel chain step (multiple agents running concurrently)
|
|
47
|
+
export const ParallelStepSchema = Type.Object({
|
|
48
|
+
parallel: Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
|
|
49
|
+
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
50
|
+
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Chain item can be either sequential or parallel
|
|
54
|
+
export const ChainItem = Type.Union([SequentialStepSchema, ParallelStepSchema]);
|
|
55
|
+
|
|
56
|
+
export const MaxOutputSchema = Type.Optional(
|
|
57
|
+
Type.Object({
|
|
58
|
+
bytes: Type.Optional(Type.Number({ description: "Max bytes (default: 204800)" })),
|
|
59
|
+
lines: Type.Optional(Type.Number({ description: "Max lines (default: 5000)" })),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export const SubagentParams = Type.Object({
|
|
64
|
+
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
|
|
65
|
+
task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
|
|
66
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
|
|
67
|
+
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: [{agent}, {agent, task:'{previous}'}] - sequential pipeline" })),
|
|
68
|
+
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
69
|
+
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
|
|
70
|
+
cwd: Type.Optional(Type.String()),
|
|
71
|
+
maxOutput: MaxOutputSchema,
|
|
72
|
+
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
73
|
+
includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
|
|
74
|
+
share: Type.Optional(Type.Boolean({ description: "Create shareable session log (default: true)", default: true })),
|
|
75
|
+
sessionDir: Type.Optional(
|
|
76
|
+
Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
|
|
77
|
+
),
|
|
78
|
+
// Chain clarification TUI
|
|
79
|
+
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to clarify chain templates (default: true for chains). Implies sync mode." })),
|
|
80
|
+
// Solo agent output override
|
|
81
|
+
output: Type.Optional(Type.Union([
|
|
82
|
+
Type.String(),
|
|
83
|
+
Type.Boolean(),
|
|
84
|
+
], { description: "Override output file for single agent (string), or false to disable (uses agent default if omitted)" })),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const StatusParams = Type.Object({
|
|
88
|
+
id: Type.Optional(Type.String({ description: "Async run id or prefix" })),
|
|
89
|
+
dir: Type.Optional(Type.String({ description: "Async run directory (overrides id search)" })),
|
|
90
|
+
});
|
package/settings.ts
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Chain behavior, template resolution, and directory management
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
-
import * as os from "node:os";
|
|
7
6
|
import * as path from "node:path";
|
|
8
7
|
import type { AgentConfig } from "./agents.js";
|
|
9
8
|
|
|
10
|
-
const SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
11
9
|
const CHAIN_RUNS_DIR = "/tmp/pi-chain-runs";
|
|
12
10
|
const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
13
11
|
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// Settings Types
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
18
|
-
export interface ChainTemplates {
|
|
19
|
-
[chainKey: string]: {
|
|
20
|
-
[agentName: string]: string;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface SubagentSettings {
|
|
25
|
-
chains?: ChainTemplates;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
12
|
// =============================================================================
|
|
29
13
|
// Behavior Resolution Types
|
|
30
14
|
// =============================================================================
|
|
@@ -83,10 +67,6 @@ export function isParallelStep(step: ChainStep): step is ParallelStep {
|
|
|
83
67
|
return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
|
|
84
68
|
}
|
|
85
69
|
|
|
86
|
-
export function isSequentialStep(step: ChainStep): step is SequentialStep {
|
|
87
|
-
return "agent" in step && !("parallel" in step);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
70
|
/** Get all agent names in a step (single for sequential, multiple for parallel) */
|
|
91
71
|
export function getStepAgents(step: ChainStep): string[] {
|
|
92
72
|
if (isParallelStep(step)) {
|
|
@@ -95,51 +75,6 @@ export function getStepAgents(step: ChainStep): string[] {
|
|
|
95
75
|
return [step.agent];
|
|
96
76
|
}
|
|
97
77
|
|
|
98
|
-
/** Get total task count in a step */
|
|
99
|
-
export function getStepTaskCount(step: ChainStep): number {
|
|
100
|
-
if (isParallelStep(step)) {
|
|
101
|
-
return step.parallel.length;
|
|
102
|
-
}
|
|
103
|
-
return 1;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// =============================================================================
|
|
107
|
-
// Settings Management
|
|
108
|
-
// =============================================================================
|
|
109
|
-
|
|
110
|
-
export function loadSubagentSettings(): SubagentSettings {
|
|
111
|
-
try {
|
|
112
|
-
const data = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
113
|
-
return (data.subagent as SubagentSettings) ?? {};
|
|
114
|
-
} catch {
|
|
115
|
-
return {};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function saveChainTemplate(chainKey: string, templates: Record<string, string>): void {
|
|
120
|
-
let settings: Record<string, unknown> = {};
|
|
121
|
-
try {
|
|
122
|
-
settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
123
|
-
} catch {}
|
|
124
|
-
|
|
125
|
-
if (!settings.subagent) settings.subagent = {};
|
|
126
|
-
const subagent = settings.subagent as Record<string, unknown>;
|
|
127
|
-
if (!subagent.chains) subagent.chains = {};
|
|
128
|
-
const chains = subagent.chains as Record<string, unknown>;
|
|
129
|
-
|
|
130
|
-
chains[chainKey] = templates;
|
|
131
|
-
|
|
132
|
-
const dir = path.dirname(SETTINGS_PATH);
|
|
133
|
-
if (!fs.existsSync(dir)) {
|
|
134
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
-
}
|
|
136
|
-
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function getChainKey(agents: string[]): string {
|
|
140
|
-
return agents.join("->");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
78
|
// =============================================================================
|
|
144
79
|
// Chain Directory Management
|
|
145
80
|
// =============================================================================
|
|
@@ -183,36 +118,6 @@ export function cleanupOldChainDirs(): void {
|
|
|
183
118
|
// Template Resolution
|
|
184
119
|
// =============================================================================
|
|
185
120
|
|
|
186
|
-
/**
|
|
187
|
-
* Resolve templates for each step in a chain.
|
|
188
|
-
* Priority: inline task > saved template > default
|
|
189
|
-
* Default for step 0: "{task}", for others: "{previous}"
|
|
190
|
-
*/
|
|
191
|
-
export function resolveChainTemplates(
|
|
192
|
-
agentNames: string[],
|
|
193
|
-
inlineTasks: (string | undefined)[],
|
|
194
|
-
settings: SubagentSettings,
|
|
195
|
-
): string[] {
|
|
196
|
-
const chainKey = getChainKey(agentNames);
|
|
197
|
-
const savedTemplates = settings.chains?.[chainKey] ?? {};
|
|
198
|
-
|
|
199
|
-
return agentNames.map((agent, i) => {
|
|
200
|
-
// Priority: inline > saved > default
|
|
201
|
-
const inline = inlineTasks[i];
|
|
202
|
-
if (inline) return inline;
|
|
203
|
-
|
|
204
|
-
const saved = savedTemplates[agent];
|
|
205
|
-
if (saved) return saved;
|
|
206
|
-
|
|
207
|
-
// Default: first step uses {task}, others use {previous}
|
|
208
|
-
return i === 0 ? "{task}" : "{previous}";
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// =============================================================================
|
|
213
|
-
// Parallel-Aware Template Resolution
|
|
214
|
-
// =============================================================================
|
|
215
|
-
|
|
216
121
|
/** Resolved templates for a chain - string for sequential, string[] for parallel */
|
|
217
122
|
export type ResolvedTemplates = (string | string[])[];
|
|
218
123
|
|
|
@@ -220,9 +125,8 @@ export type ResolvedTemplates = (string | string[])[];
|
|
|
220
125
|
* Resolve templates for a chain with parallel step support.
|
|
221
126
|
* Returns string for sequential steps, string[] for parallel steps.
|
|
222
127
|
*/
|
|
223
|
-
export function
|
|
128
|
+
export function resolveChainTemplates(
|
|
224
129
|
steps: ChainStep[],
|
|
225
|
-
settings: SubagentSettings,
|
|
226
130
|
): ResolvedTemplates {
|
|
227
131
|
return steps.map((step, i) => {
|
|
228
132
|
if (isParallelStep(step)) {
|
|
@@ -241,43 +145,6 @@ export function resolveChainTemplatesV2(
|
|
|
241
145
|
});
|
|
242
146
|
}
|
|
243
147
|
|
|
244
|
-
/**
|
|
245
|
-
* Flatten templates for display (TUI navigation needs flat list)
|
|
246
|
-
*/
|
|
247
|
-
export function flattenTemplates(templates: ResolvedTemplates): string[] {
|
|
248
|
-
const result: string[] = [];
|
|
249
|
-
for (const t of templates) {
|
|
250
|
-
if (Array.isArray(t)) {
|
|
251
|
-
result.push(...t);
|
|
252
|
-
} else {
|
|
253
|
-
result.push(t);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return result;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Unflatten templates back to structured form
|
|
261
|
-
*/
|
|
262
|
-
export function unflattenTemplates(
|
|
263
|
-
flat: string[],
|
|
264
|
-
steps: ChainStep[],
|
|
265
|
-
): ResolvedTemplates {
|
|
266
|
-
const result: ResolvedTemplates = [];
|
|
267
|
-
let idx = 0;
|
|
268
|
-
for (const step of steps) {
|
|
269
|
-
if (isParallelStep(step)) {
|
|
270
|
-
const count = step.parallel.length;
|
|
271
|
-
result.push(flat.slice(idx, idx + count));
|
|
272
|
-
idx += count;
|
|
273
|
-
} else {
|
|
274
|
-
result.push(flat[idx]!);
|
|
275
|
-
idx++;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
148
|
// =============================================================================
|
|
282
149
|
// Behavior Resolution
|
|
283
150
|
// =============================================================================
|
|
@@ -311,20 +178,6 @@ export function resolveStepBehavior(
|
|
|
311
178
|
return { output, reads, progress };
|
|
312
179
|
}
|
|
313
180
|
|
|
314
|
-
/**
|
|
315
|
-
* Find index of first agent in chain that has progress enabled
|
|
316
|
-
*/
|
|
317
|
-
export function findFirstProgressAgentIndex(
|
|
318
|
-
agentConfigs: AgentConfig[],
|
|
319
|
-
stepOverrides: StepOverrides[],
|
|
320
|
-
): number {
|
|
321
|
-
return agentConfigs.findIndex((config, i) => {
|
|
322
|
-
const override = stepOverrides[i];
|
|
323
|
-
if (override?.progress !== undefined) return override.progress;
|
|
324
|
-
return config.defaultProgress ?? false;
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
181
|
// =============================================================================
|
|
329
182
|
// Chain Instruction Injection
|
|
330
183
|
// =============================================================================
|
|
@@ -472,21 +325,4 @@ export function aggregateParallelOutputs(results: ParallelTaskResult[]): string
|
|
|
472
325
|
.join("\n\n");
|
|
473
326
|
}
|
|
474
327
|
|
|
475
|
-
/**
|
|
476
|
-
* Check if any parallel task failed
|
|
477
|
-
*/
|
|
478
|
-
export function hasParallelFailures(results: ParallelTaskResult[]): boolean {
|
|
479
|
-
return results.some((r) => r.exitCode !== 0);
|
|
480
|
-
}
|
|
481
328
|
|
|
482
|
-
/**
|
|
483
|
-
* Get failure summary for parallel step
|
|
484
|
-
*/
|
|
485
|
-
export function getParallelFailureSummary(results: ParallelTaskResult[]): string {
|
|
486
|
-
const failures = results.filter((r) => r.exitCode !== 0);
|
|
487
|
-
if (failures.length === 0) return "";
|
|
488
|
-
|
|
489
|
-
return failures
|
|
490
|
-
.map((f) => `- Task ${f.taskIndex + 1} (${f.agent}): ${f.error || "failed"}`)
|
|
491
|
-
.join("\n");
|
|
492
|
-
}
|