pi-subagents 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +34 -7
- package/chain-clarify.ts +631 -31
- package/chain-execution.ts +93 -16
- package/execution.ts +5 -2
- package/index.ts +143 -21
- package/package.json +2 -1
- package/render.ts +4 -2
- package/schemas.ts +13 -11
- package/settings.ts +24 -20
- package/types.ts +2 -0
package/chain-execution.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
8
8
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import type { AgentConfig } from "./agents.js";
|
|
10
|
-
import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride } from "./chain-clarify.js";
|
|
10
|
+
import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride, type ModelInfo } from "./chain-clarify.js";
|
|
11
11
|
import {
|
|
12
12
|
resolveChainTemplates,
|
|
13
13
|
createChainDir,
|
|
@@ -36,6 +36,28 @@ import {
|
|
|
36
36
|
MAX_CONCURRENCY,
|
|
37
37
|
} from "./types.js";
|
|
38
38
|
|
|
39
|
+
/** Resolve a model name to its full provider/model format */
|
|
40
|
+
function resolveModelFullId(modelName: string | undefined, availableModels: ModelInfo[]): string | undefined {
|
|
41
|
+
if (!modelName) return undefined;
|
|
42
|
+
// If already in provider/model format, return as-is
|
|
43
|
+
if (modelName.includes("/")) return modelName;
|
|
44
|
+
|
|
45
|
+
// Handle thinking level suffixes (e.g., "claude-sonnet-4-5:high")
|
|
46
|
+
// Strip the suffix for lookup, then add it back
|
|
47
|
+
const colonIdx = modelName.lastIndexOf(":");
|
|
48
|
+
const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
|
|
49
|
+
const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
|
|
50
|
+
|
|
51
|
+
// Look up base model in available models to find provider
|
|
52
|
+
const match = availableModels.find(m => m.id === baseModel);
|
|
53
|
+
if (match) {
|
|
54
|
+
return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fallback: return as-is
|
|
58
|
+
return modelName;
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
export interface ChainExecutionParams {
|
|
40
62
|
chain: ChainStep[];
|
|
41
63
|
agents: AgentConfig[];
|
|
@@ -110,6 +132,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
110
132
|
// Behavior overrides from TUI (set if TUI is shown, undefined otherwise)
|
|
111
133
|
let tuiBehaviorOverrides: (BehaviorOverride | undefined)[] | undefined;
|
|
112
134
|
|
|
135
|
+
// Get available models for model resolution (used in TUI and execution)
|
|
136
|
+
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
137
|
+
provider: m.provider,
|
|
138
|
+
id: m.id,
|
|
139
|
+
fullId: `${m.provider}/${m.id}`,
|
|
140
|
+
}));
|
|
141
|
+
|
|
113
142
|
if (shouldClarify) {
|
|
114
143
|
// Sequential-only chain: use existing TUI
|
|
115
144
|
const seqSteps = chainSteps as SequentialStep[];
|
|
@@ -154,6 +183,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
154
183
|
originalTask,
|
|
155
184
|
chainDir,
|
|
156
185
|
resolvedBehaviors,
|
|
186
|
+
availableModels,
|
|
157
187
|
done,
|
|
158
188
|
),
|
|
159
189
|
{
|
|
@@ -227,18 +257,31 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
227
257
|
} as SingleResult;
|
|
228
258
|
}
|
|
229
259
|
|
|
230
|
-
//
|
|
260
|
+
// Resolve behavior for this parallel task
|
|
261
|
+
const behavior = parallelBehaviors[taskIndex]!;
|
|
262
|
+
|
|
263
|
+
// Build chain instructions (prefix goes BEFORE task, suffix goes AFTER)
|
|
231
264
|
const taskTemplate = parallelTemplates[taskIndex] ?? "{previous}";
|
|
232
265
|
const templateHasPrevious = taskTemplate.includes("{previous}");
|
|
266
|
+
const { prefix, suffix } = buildChainInstructions(
|
|
267
|
+
behavior,
|
|
268
|
+
chainDir,
|
|
269
|
+
false, // parallel tasks don't create progress (pre-created above)
|
|
270
|
+
templateHasPrevious ? undefined : prev
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Build task string with variable substitution
|
|
233
274
|
let taskStr = taskTemplate;
|
|
234
275
|
taskStr = taskStr.replace(/\{task\}/g, originalTask);
|
|
235
276
|
taskStr = taskStr.replace(/\{previous\}/g, prev);
|
|
236
277
|
taskStr = taskStr.replace(/\{chain_dir\}/g, chainDir);
|
|
237
278
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
279
|
+
// Assemble final task: prefix (READ/WRITE instructions) + task + suffix
|
|
280
|
+
taskStr = prefix + taskStr + suffix;
|
|
281
|
+
|
|
282
|
+
// Resolve model to full provider/model format for consistent display
|
|
283
|
+
const taskAgentConfig = agents.find((a) => a.name === task.agent);
|
|
284
|
+
const effectiveModel = resolveModelFullId(taskAgentConfig?.model, availableModels);
|
|
242
285
|
|
|
243
286
|
const r = await runSync(ctx.cwd, agents, task.agent, taskStr, {
|
|
244
287
|
cwd: task.cwd ?? cwd,
|
|
@@ -249,6 +292,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
249
292
|
share: shareEnabled,
|
|
250
293
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
251
294
|
artifactConfig,
|
|
295
|
+
modelOverride: effectiveModel,
|
|
252
296
|
onUpdate: onUpdate
|
|
253
297
|
? (p) => {
|
|
254
298
|
// Use concat instead of spread for better performance
|
|
@@ -340,14 +384,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
340
384
|
};
|
|
341
385
|
}
|
|
342
386
|
|
|
343
|
-
//
|
|
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)
|
|
387
|
+
// Resolve behavior first (TUI overrides take precedence over step config)
|
|
351
388
|
const tuiOverride = tuiBehaviorOverrides?.[stepIndex];
|
|
352
389
|
const stepOverride: StepOverrides = {
|
|
353
390
|
output: tuiOverride?.output !== undefined ? tuiOverride.output : seqStep.output,
|
|
@@ -362,8 +399,26 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
362
399
|
progressCreated = true;
|
|
363
400
|
}
|
|
364
401
|
|
|
365
|
-
//
|
|
366
|
-
|
|
402
|
+
// Build chain instructions (prefix goes BEFORE task, suffix goes AFTER)
|
|
403
|
+
const templateHasPrevious = stepTemplate.includes("{previous}");
|
|
404
|
+
const { prefix, suffix } = buildChainInstructions(
|
|
405
|
+
behavior,
|
|
406
|
+
chainDir,
|
|
407
|
+
isFirstProgress,
|
|
408
|
+
templateHasPrevious ? undefined : prev
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Build task string with variable substitution
|
|
412
|
+
let stepTask = stepTemplate;
|
|
413
|
+
stepTask = stepTask.replace(/\{task\}/g, originalTask);
|
|
414
|
+
stepTask = stepTask.replace(/\{previous\}/g, prev);
|
|
415
|
+
stepTask = stepTask.replace(/\{chain_dir\}/g, chainDir);
|
|
416
|
+
|
|
417
|
+
// Assemble final task: prefix (READ/WRITE instructions) + task + suffix (progress, previous summary)
|
|
418
|
+
stepTask = prefix + stepTask + suffix;
|
|
419
|
+
|
|
420
|
+
// Resolve model: TUI override (already full format) or agent's model resolved to full format
|
|
421
|
+
const effectiveModel = tuiOverride?.model ?? resolveModelFullId(agentConfig.model, availableModels);
|
|
367
422
|
|
|
368
423
|
// Run step
|
|
369
424
|
const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
@@ -375,6 +430,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
375
430
|
share: shareEnabled,
|
|
376
431
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
377
432
|
artifactConfig,
|
|
433
|
+
modelOverride: effectiveModel,
|
|
378
434
|
onUpdate: onUpdate
|
|
379
435
|
? (p) => {
|
|
380
436
|
// Use concat instead of spread for better performance
|
|
@@ -400,6 +456,27 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
400
456
|
if (r.progress) allProgress.push(r.progress);
|
|
401
457
|
if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
|
|
402
458
|
|
|
459
|
+
// Validate expected output file was created
|
|
460
|
+
if (behavior.output && r.exitCode === 0) {
|
|
461
|
+
try {
|
|
462
|
+
const expectedPath = behavior.output.startsWith("/")
|
|
463
|
+
? behavior.output
|
|
464
|
+
: path.join(chainDir, behavior.output);
|
|
465
|
+
if (!fs.existsSync(expectedPath)) {
|
|
466
|
+
// Look for similar files that might have been created instead
|
|
467
|
+
const dirFiles = fs.readdirSync(chainDir);
|
|
468
|
+
const mdFiles = dirFiles.filter(f => f.endsWith(".md") && f !== "progress.md");
|
|
469
|
+
const warning = mdFiles.length > 0
|
|
470
|
+
? `Agent wrote to different file(s): ${mdFiles.join(", ")} instead of ${behavior.output}`
|
|
471
|
+
: `Agent did not create expected output file: ${behavior.output}`;
|
|
472
|
+
// Add warning to result but don't fail
|
|
473
|
+
r.error = r.error ? `${r.error}\n⚠️ ${warning}` : `⚠️ ${warning}`;
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
// Ignore validation errors - this is just a diagnostic
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
403
480
|
// On failure, leave chain_dir for debugging
|
|
404
481
|
if (r.exitCode !== 0) {
|
|
405
482
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", {
|
package/execution.ts
CHANGED
|
@@ -43,7 +43,7 @@ export async function runSync(
|
|
|
43
43
|
task: string,
|
|
44
44
|
options: RunSyncOptions,
|
|
45
45
|
): Promise<SingleResult> {
|
|
46
|
-
const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index } = options;
|
|
46
|
+
const { cwd, signal, onUpdate, maxOutput, artifactsDir, artifactConfig, runId, index, modelOverride } = options;
|
|
47
47
|
const agent = agents.find((a) => a.name === agentName);
|
|
48
48
|
if (!agent) {
|
|
49
49
|
return {
|
|
@@ -68,7 +68,9 @@ export async function runSync(
|
|
|
68
68
|
} catch {}
|
|
69
69
|
args.push("--session-dir", options.sessionDir);
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
// Use model override if provided, otherwise use agent's default model
|
|
72
|
+
const effectiveModel = modelOverride ?? agent.model;
|
|
73
|
+
if (effectiveModel) args.push("--model", effectiveModel);
|
|
72
74
|
if (agent.tools?.length) {
|
|
73
75
|
const builtinTools: string[] = [];
|
|
74
76
|
const extensionPaths: string[] = [];
|
|
@@ -101,6 +103,7 @@ export async function runSync(
|
|
|
101
103
|
exitCode: 0,
|
|
102
104
|
messages: [],
|
|
103
105
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
106
|
+
model: effectiveModel, // Initialize with the model we're using
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
const progress: AgentProgress = {
|
package/index.ts
CHANGED
|
@@ -19,7 +19,8 @@ import * as path from "node:path";
|
|
|
19
19
|
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
20
20
|
import { Text } from "@mariozechner/pi-tui";
|
|
21
21
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
22
|
-
import { cleanupOldChainDirs, getStepAgents, isParallelStep, type ChainStep, type SequentialStep } from "./settings.js";
|
|
22
|
+
import { cleanupOldChainDirs, getStepAgents, isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep } from "./settings.js";
|
|
23
|
+
import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.js";
|
|
23
24
|
import { cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
|
|
24
25
|
import {
|
|
25
26
|
type AgentProgress,
|
|
@@ -144,9 +145,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
144
145
|
label: "Subagent",
|
|
145
146
|
description: `Delegate to subagents. Use exactly ONE mode:
|
|
146
147
|
• SINGLE: { agent, task } - one task
|
|
147
|
-
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential
|
|
148
|
-
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
|
|
149
|
-
|
|
148
|
+
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential pipeline
|
|
149
|
+
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent execution
|
|
150
|
+
|
|
151
|
+
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
152
|
+
• {task} - The original task/request from the user
|
|
153
|
+
• {previous} - Text response from the previous step (empty for first step)
|
|
154
|
+
• {chain_dir} - Shared directory for chain files (e.g., /tmp/pi-chain-runs/abc123/)
|
|
155
|
+
|
|
156
|
+
CHAIN DATA FLOW:
|
|
157
|
+
1. Each step's text response automatically becomes {previous} for the next step
|
|
158
|
+
2. Steps can also write files to {chain_dir} (via agent's "output" config)
|
|
159
|
+
3. Later steps can read those files (via agent's "reads" config)
|
|
160
|
+
|
|
161
|
+
Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }`,
|
|
150
162
|
parameters: SubagentParams,
|
|
151
163
|
|
|
152
164
|
async execute(_id, params, onUpdate, ctx, signal) {
|
|
@@ -177,8 +189,13 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
177
189
|
const requestedAsync = params.async ?? asyncByDefault;
|
|
178
190
|
const parallelDowngraded = hasTasks && requestedAsync;
|
|
179
191
|
// clarify implies sync mode (TUI is blocking)
|
|
180
|
-
//
|
|
181
|
-
|
|
192
|
+
// - Chains default to TUI (clarify: true), so async requires explicit clarify: false
|
|
193
|
+
// - Single defaults to no TUI, so async is allowed unless clarify: true is passed
|
|
194
|
+
const effectiveAsync = requestedAsync && !hasTasks && (
|
|
195
|
+
hasChain
|
|
196
|
+
? params.clarify === false // chains: only async if TUI explicitly disabled
|
|
197
|
+
: params.clarify !== true // single: async unless TUI explicitly enabled
|
|
198
|
+
);
|
|
182
199
|
|
|
183
200
|
const artifactConfig: ArtifactConfig = {
|
|
184
201
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -325,14 +342,74 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
325
342
|
}
|
|
326
343
|
|
|
327
344
|
if (hasTasks && params.tasks) {
|
|
345
|
+
// MAX_PARALLEL check first (fail fast before TUI)
|
|
328
346
|
if (params.tasks.length > MAX_PARALLEL)
|
|
329
347
|
return {
|
|
330
348
|
content: [{ type: "text", text: `Max ${MAX_PARALLEL} tasks` }],
|
|
331
349
|
isError: true,
|
|
332
|
-
details: { mode: "
|
|
350
|
+
details: { mode: "parallel" as const, results: [] },
|
|
333
351
|
};
|
|
352
|
+
|
|
353
|
+
// Validate all agents exist
|
|
354
|
+
const agentConfigs: AgentConfig[] = [];
|
|
355
|
+
for (const t of params.tasks) {
|
|
356
|
+
const config = agents.find(a => a.name === t.agent);
|
|
357
|
+
if (!config) {
|
|
358
|
+
return {
|
|
359
|
+
content: [{ type: "text", text: `Unknown agent: ${t.agent}` }],
|
|
360
|
+
isError: true,
|
|
361
|
+
details: { mode: "parallel" as const, results: [] },
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
agentConfigs.push(config);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Mutable copies for TUI modifications
|
|
368
|
+
let tasks = params.tasks.map(t => t.task);
|
|
369
|
+
const modelOverrides: (string | undefined)[] = new Array(params.tasks.length).fill(undefined);
|
|
370
|
+
|
|
371
|
+
// Show clarify TUI if requested
|
|
372
|
+
if (params.clarify === true && ctx.hasUI) {
|
|
373
|
+
// Get available models (same pattern as chain-execution.ts)
|
|
374
|
+
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
375
|
+
provider: m.provider,
|
|
376
|
+
id: m.id,
|
|
377
|
+
fullId: `${m.provider}/${m.id}`,
|
|
378
|
+
}));
|
|
379
|
+
|
|
380
|
+
const behaviors = agentConfigs.map(c => resolveStepBehavior(c, {}));
|
|
381
|
+
|
|
382
|
+
const result = await ctx.ui.custom<ChainClarifyResult>(
|
|
383
|
+
(tui, theme, _kb, done) =>
|
|
384
|
+
new ChainClarifyComponent(
|
|
385
|
+
tui, theme,
|
|
386
|
+
agentConfigs,
|
|
387
|
+
tasks,
|
|
388
|
+
'', // no originalTask for parallel (each task is independent)
|
|
389
|
+
undefined, // no chainDir for parallel
|
|
390
|
+
behaviors,
|
|
391
|
+
availableModels,
|
|
392
|
+
done,
|
|
393
|
+
'parallel', // mode
|
|
394
|
+
),
|
|
395
|
+
{ overlay: true, overlayOptions: { anchor: 'center', width: 84, maxHeight: '80%' } },
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (!result || !result.confirmed) {
|
|
399
|
+
return { content: [{ type: 'text', text: 'Cancelled' }], details: { mode: 'parallel', results: [] } };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Apply TUI overrides
|
|
403
|
+
tasks = result.templates;
|
|
404
|
+
for (let i = 0; i < result.behaviorOverrides.length; i++) {
|
|
405
|
+
const override = result.behaviorOverrides[i];
|
|
406
|
+
if (override?.model) modelOverrides[i] = override.model;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Execute with overrides (tasks array has same length as params.tasks)
|
|
334
411
|
const results = await mapConcurrent(params.tasks, MAX_CONCURRENCY, async (t, i) =>
|
|
335
|
-
runSync(ctx.cwd, agents, t.agent,
|
|
412
|
+
runSync(ctx.cwd, agents, t.agent, tasks[i]!, {
|
|
336
413
|
cwd: t.cwd ?? params.cwd,
|
|
337
414
|
signal,
|
|
338
415
|
runId,
|
|
@@ -342,6 +419,7 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
342
419
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
343
420
|
artifactConfig,
|
|
344
421
|
maxOutput: params.maxOutput,
|
|
422
|
+
modelOverride: modelOverrides[i],
|
|
345
423
|
}),
|
|
346
424
|
);
|
|
347
425
|
|
|
@@ -366,24 +444,67 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
366
444
|
if (hasSingle) {
|
|
367
445
|
// Look up agent config for output handling
|
|
368
446
|
const agentConfig = agents.find((a) => a.name === params.agent);
|
|
369
|
-
|
|
447
|
+
if (!agentConfig) {
|
|
448
|
+
return {
|
|
449
|
+
content: [{ type: 'text', text: `Unknown agent: ${params.agent}` }],
|
|
450
|
+
isError: true,
|
|
451
|
+
details: { mode: 'single', results: [] },
|
|
452
|
+
};
|
|
453
|
+
}
|
|
370
454
|
|
|
371
455
|
let task = params.task!;
|
|
372
|
-
let
|
|
456
|
+
let modelOverride: string | undefined;
|
|
457
|
+
// Normalize output: true means "use default" (same as undefined), false means disable
|
|
458
|
+
const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
|
|
459
|
+
let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : rawOutput;
|
|
460
|
+
|
|
461
|
+
// Show clarify TUI if requested
|
|
462
|
+
if (params.clarify === true && ctx.hasUI) {
|
|
463
|
+
// Get available models (same pattern as chain-execution.ts)
|
|
464
|
+
const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
465
|
+
provider: m.provider,
|
|
466
|
+
id: m.id,
|
|
467
|
+
fullId: `${m.provider}/${m.id}`,
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput });
|
|
471
|
+
|
|
472
|
+
const result = await ctx.ui.custom<ChainClarifyResult>(
|
|
473
|
+
(tui, theme, _kb, done) =>
|
|
474
|
+
new ChainClarifyComponent(
|
|
475
|
+
tui, theme,
|
|
476
|
+
[agentConfig],
|
|
477
|
+
[task],
|
|
478
|
+
task,
|
|
479
|
+
undefined, // no chainDir for single
|
|
480
|
+
[behavior],
|
|
481
|
+
availableModels,
|
|
482
|
+
done,
|
|
483
|
+
'single', // mode
|
|
484
|
+
),
|
|
485
|
+
{ overlay: true, overlayOptions: { anchor: 'center', width: 84, maxHeight: '80%' } },
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (!result || !result.confirmed) {
|
|
489
|
+
return { content: [{ type: 'text', text: 'Cancelled' }], details: { mode: 'single', results: [] } };
|
|
490
|
+
}
|
|
373
491
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
492
|
+
// Apply TUI overrides
|
|
493
|
+
task = result.templates[0]!;
|
|
494
|
+
const override = result.behaviorOverrides[0];
|
|
495
|
+
if (override?.model) modelOverride = override.model;
|
|
496
|
+
if (override?.output !== undefined) effectiveOutput = override.output;
|
|
497
|
+
}
|
|
378
498
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
499
|
+
// Compute output path at runtime (uses effectiveOutput which may be TUI-modified)
|
|
500
|
+
let outputPath: string | undefined;
|
|
501
|
+
if (effectiveOutput && effectiveOutput !== false) {
|
|
502
|
+
const outputDir = `/tmp/pi-${agentConfig.name}-${runId}`;
|
|
503
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
504
|
+
outputPath = `${outputDir}/${effectiveOutput}`;
|
|
383
505
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
506
|
+
// Inject output instruction into task
|
|
507
|
+
task += `\n\n---\n**Output:** Write your findings to: ${outputPath}`;
|
|
387
508
|
}
|
|
388
509
|
|
|
389
510
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
@@ -396,6 +517,7 @@ For "scout → planner" or multi-step flows, use chain (not multiple single call
|
|
|
396
517
|
artifactConfig,
|
|
397
518
|
maxOutput: params.maxOutput,
|
|
398
519
|
onUpdate,
|
|
520
|
+
modelOverride,
|
|
399
521
|
});
|
|
400
522
|
|
|
401
523
|
if (r.progress) allProgress.push(r.progress);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/nicobailon/pi-subagents/issues"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
|
+
"pi-package",
|
|
16
17
|
"pi",
|
|
17
18
|
"pi-coding-agent",
|
|
18
19
|
"subagents",
|
package/render.ts
CHANGED
|
@@ -260,9 +260,11 @@ export function renderSubagentResult(
|
|
|
260
260
|
? theme.fg("success", "✓")
|
|
261
261
|
: theme.fg("error", "✗");
|
|
262
262
|
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
263
|
+
// Show model if available (full provider/model format)
|
|
264
|
+
const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
|
|
263
265
|
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
|
+
? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
267
|
+
: `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
|
|
266
268
|
c.addChild(new Text(stepHeader, 0, 0));
|
|
267
269
|
|
|
268
270
|
// Task (truncated)
|
package/schemas.ts
CHANGED
|
@@ -13,34 +13,36 @@ export const TaskItem = Type.Object({
|
|
|
13
13
|
// Sequential chain step (single agent)
|
|
14
14
|
export const SequentialStepSchema = Type.Object({
|
|
15
15
|
agent: Type.String(),
|
|
16
|
-
task: Type.Optional(Type.String({
|
|
16
|
+
task: Type.Optional(Type.String({
|
|
17
|
+
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."
|
|
18
|
+
})),
|
|
17
19
|
cwd: Type.Optional(Type.String()),
|
|
18
20
|
// Chain behavior overrides
|
|
19
21
|
output: Type.Optional(Type.Union([
|
|
20
22
|
Type.String(),
|
|
21
23
|
Type.Boolean(),
|
|
22
|
-
], { description: "
|
|
24
|
+
], { description: "Output filename to write in {chain_dir} (string), or false to disable file output" })),
|
|
23
25
|
reads: Type.Optional(Type.Union([
|
|
24
26
|
Type.Array(Type.String()),
|
|
25
27
|
Type.Boolean(),
|
|
26
|
-
], { description: "
|
|
27
|
-
progress: Type.Optional(Type.Boolean({ description: "
|
|
28
|
+
], { description: "Files to read from {chain_dir} before running (array of filenames), or false to disable" })),
|
|
29
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
// Parallel task item (within a parallel step)
|
|
31
33
|
export const ParallelTaskSchema = Type.Object({
|
|
32
34
|
agent: Type.String(),
|
|
33
|
-
task: Type.Optional(Type.String({ description: "Task template. Defaults to {previous}." })),
|
|
35
|
+
task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
|
|
34
36
|
cwd: Type.Optional(Type.String()),
|
|
35
37
|
output: Type.Optional(Type.Union([
|
|
36
38
|
Type.String(),
|
|
37
39
|
Type.Boolean(),
|
|
38
|
-
], { description: "
|
|
40
|
+
], { description: "Output filename to write in {chain_dir} (string), or false to disable file output" })),
|
|
39
41
|
reads: Type.Optional(Type.Union([
|
|
40
42
|
Type.Array(Type.String()),
|
|
41
43
|
Type.Boolean(),
|
|
42
|
-
], { description: "
|
|
43
|
-
progress: Type.Optional(Type.Boolean({ description: "
|
|
44
|
+
], { description: "Files to read from {chain_dir} before running (array of filenames), or false to disable" })),
|
|
45
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
// Parallel chain step (multiple agents running concurrently)
|
|
@@ -64,7 +66,7 @@ export const SubagentParams = Type.Object({
|
|
|
64
66
|
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
|
|
65
67
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
|
|
66
68
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
|
|
67
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode:
|
|
69
|
+
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
|
|
68
70
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
69
71
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
|
|
70
72
|
cwd: Type.Optional(Type.String()),
|
|
@@ -75,8 +77,8 @@ export const SubagentParams = Type.Object({
|
|
|
75
77
|
sessionDir: Type.Optional(
|
|
76
78
|
Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
|
|
77
79
|
),
|
|
78
|
-
//
|
|
79
|
-
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to
|
|
80
|
+
// Clarification TUI
|
|
81
|
+
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution (default: true for chains, false for single/parallel). Implies sync mode." })),
|
|
80
82
|
// Solo agent output override
|
|
81
83
|
output: Type.Optional(Type.Union([
|
|
82
84
|
Type.String(),
|
package/settings.ts
CHANGED
|
@@ -198,42 +198,46 @@ export function buildChainInstructions(
|
|
|
198
198
|
chainDir: string,
|
|
199
199
|
isFirstProgressAgent: boolean,
|
|
200
200
|
previousSummary?: string,
|
|
201
|
-
): string {
|
|
202
|
-
const
|
|
201
|
+
): { prefix: string; suffix: string } {
|
|
202
|
+
const prefixParts: string[] = [];
|
|
203
|
+
const suffixParts: string[] = [];
|
|
203
204
|
|
|
204
|
-
//
|
|
205
|
-
if (previousSummary && previousSummary.trim()) {
|
|
206
|
-
instructions.push(`Previous step summary:\n\n${previousSummary.trim()}`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Reads (supports both absolute and relative paths)
|
|
205
|
+
// READS - prepend to override any hardcoded filenames in task text
|
|
210
206
|
if (behavior.reads && behavior.reads.length > 0) {
|
|
211
|
-
const files = behavior.reads.map((f) => resolveChainPath(f, chainDir))
|
|
212
|
-
|
|
207
|
+
const files = behavior.reads.map((f) => resolveChainPath(f, chainDir));
|
|
208
|
+
prefixParts.push(`[Read from: ${files.join(", ")}]`);
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
//
|
|
211
|
+
// OUTPUT - prepend so agent knows where to write
|
|
216
212
|
if (behavior.output) {
|
|
217
213
|
const outputPath = resolveChainPath(behavior.output, chainDir);
|
|
218
|
-
|
|
214
|
+
prefixParts.push(`[Write to: ${outputPath}]`);
|
|
219
215
|
}
|
|
220
216
|
|
|
221
|
-
// Progress
|
|
217
|
+
// Progress instructions in suffix (less critical)
|
|
222
218
|
if (behavior.progress) {
|
|
223
219
|
const progressPath = `${chainDir}/progress.md`;
|
|
224
220
|
if (isFirstProgressAgent) {
|
|
225
|
-
|
|
226
|
-
instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
|
|
221
|
+
suffixParts.push(`Create and maintain progress at: ${progressPath}`);
|
|
227
222
|
} else {
|
|
228
|
-
|
|
223
|
+
suffixParts.push(`Update progress at: ${progressPath}`);
|
|
229
224
|
}
|
|
230
225
|
}
|
|
231
226
|
|
|
232
|
-
|
|
227
|
+
// Include previous step's summary in suffix if available
|
|
228
|
+
if (previousSummary && previousSummary.trim()) {
|
|
229
|
+
suffixParts.push(`Previous step output:\n${previousSummary.trim()}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prefix = prefixParts.length > 0
|
|
233
|
+
? prefixParts.join("\n") + "\n\n"
|
|
234
|
+
: "";
|
|
235
|
+
|
|
236
|
+
const suffix = suffixParts.length > 0
|
|
237
|
+
? "\n\n---\n" + suffixParts.join("\n")
|
|
238
|
+
: "";
|
|
233
239
|
|
|
234
|
-
return
|
|
235
|
-
"\n\n---\n**Chain Instructions:**\n" + instructions.map((i) => `- ${i}`).join("\n")
|
|
236
|
-
);
|
|
240
|
+
return { prefix, suffix };
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// =============================================================================
|
package/types.ts
CHANGED
|
@@ -193,6 +193,8 @@ export interface RunSyncOptions {
|
|
|
193
193
|
index?: number;
|
|
194
194
|
sessionDir?: string;
|
|
195
195
|
share?: boolean;
|
|
196
|
+
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
197
|
+
modelOverride?: string;
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
export interface ExtensionConfig {
|