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.
@@ -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
- // Build task string
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
- // 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);
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
- // 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)
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
- // Add chain instructions (include previous summary only if not already in template)
366
- stepTask += buildChainInstructions(behavior, chainDir, isFirstProgress, templateHasPrevious ? undefined : prev);
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
- if (agent.model) args.push("--model", agent.model);
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, {previous} passes output
148
- • PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
149
- For "scout → planner" or multi-step flows, use chain (not multiple single calls).`,
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
- // If user requested async without explicit clarify: false, downgrade to sync for chains
181
- const effectiveAsync = requestedAsync && !hasTasks && (hasChain ? params.clarify === false : true);
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: "single" as const, results: [] },
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, t.task, {
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
- // Note: runSync already handles unknown agent, but we need config for output
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 outputPath: string | undefined;
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
- // Check if agent has output and it's not disabled
375
- if (agentConfig) {
376
- const effectiveOutput =
377
- params.output !== undefined ? params.output : agentConfig.output;
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
- if (effectiveOutput && effectiveOutput !== false) {
380
- const outputDir = `/tmp/pi-${agentConfig.name}-${runId}`;
381
- fs.mkdirSync(outputDir, { recursive: true });
382
- outputPath = `${outputDir}/${effectiveOutput}`;
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
- // Inject output instruction into task
385
- task += `\n\n---\n**Output:** Write your findings to: ${outputPath}`;
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.2",
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({ description: "Task template. Use {task}, {previous}, {chain_dir}. Required for first step." })),
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: "Override output filename (string), or false for text-only" })),
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: "Override files to read from {chain_dir} (array), or false to disable" })),
27
- progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
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: "Override output filename (string), or false for text-only" })),
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: "Override files to read from {chain_dir} (array), or false to disable" })),
43
- progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
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: [{agent}, {agent, task:'{previous}'}] - sequential pipeline" })),
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
- // Chain clarification TUI
79
- clarify: Type.Optional(Type.Boolean({ description: "Show TUI to clarify chain templates (default: true for chains). Implies sync mode." })),
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 instructions: string[] = [];
201
+ ): { prefix: string; suffix: string } {
202
+ const prefixParts: string[] = [];
203
+ const suffixParts: string[] = [];
203
204
 
204
- // Include previous step's summary if available (prose output from prior agent)
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)).join(", ");
212
- instructions.push(`Read these files: ${files}`);
207
+ const files = behavior.reads.map((f) => resolveChainPath(f, chainDir));
208
+ prefixParts.push(`[Read from: ${files.join(", ")}]`);
213
209
  }
214
210
 
215
- // Output (supports both absolute and relative paths)
211
+ // OUTPUT - prepend so agent knows where to write
216
212
  if (behavior.output) {
217
213
  const outputPath = resolveChainPath(behavior.output, chainDir);
218
- instructions.push(`Write your output to: ${outputPath}`);
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
- instructions.push(`Create and maintain: ${progressPath}`);
226
- instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
221
+ suffixParts.push(`Create and maintain progress at: ${progressPath}`);
227
222
  } else {
228
- instructions.push(`Read and update: ${progressPath}`);
223
+ suffixParts.push(`Update progress at: ${progressPath}`);
229
224
  }
230
225
  }
231
226
 
232
- if (instructions.length === 0) return "";
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 {