pi-subagents 0.3.2 → 0.3.3
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 +55 -0
- package/README.md +15 -2
- package/chain-clarify.ts +480 -19
- package/chain-execution.ts +93 -16
- package/execution.ts +5 -2
- package/index.ts +14 -3
- package/package.json +1 -1
- package/render.ts +4 -2
- package/schemas.ts +11 -9
- package/settings.ts +24 -20
- package/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.3] - 2026-01-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Thinking level selector in chain TUI** - Press `[t]` to set thinking level for any step
|
|
7
|
+
- Options: off, minimal, low, medium, high, xhigh (ultrathink)
|
|
8
|
+
- Appends to model as suffix (e.g., `anthropic/claude-sonnet-4-5:high`)
|
|
9
|
+
- Pre-selects current thinking level if already set
|
|
10
|
+
- **Model selector in chain TUI** - Press `[m]` to select a different model for any step
|
|
11
|
+
- Fuzzy search through all available models
|
|
12
|
+
- Shows current model with ✓ indicator
|
|
13
|
+
- Provider/model format (e.g., `anthropic/claude-haiku-4-5`)
|
|
14
|
+
- Override indicator (✎) when model differs from agent default
|
|
15
|
+
- **Model visibility in chain execution** - Shows which model each step is using
|
|
16
|
+
- Display format: `Step 1: scout (claude-haiku-4-5) | 3 tools, 16.8s`
|
|
17
|
+
- Model shown in both running and completed steps
|
|
18
|
+
- **Auto-propagate output changes to reads** - When you change a step's output filename,
|
|
19
|
+
downstream steps that read from it are automatically updated to use the new filename
|
|
20
|
+
- Maintains chain dependencies without manual updates
|
|
21
|
+
- Example: Change scout's output from `context.md` to `summary.md`, planner's reads updates automatically
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Progress is now chain-level** - `[p]` toggles progress for ALL steps at once
|
|
25
|
+
- Progress setting shown at chain level (not per-step)
|
|
26
|
+
- Chains share a single progress.md, so chain-wide toggle is more intuitive
|
|
27
|
+
- **Clearer output/writes labeling** - Renamed `output:` to `writes:` to clarify it's a file
|
|
28
|
+
- Hotkey changed from `[o]` to `[w]` for consistency
|
|
29
|
+
- **{previous} data flow indicator** - Shows on the PRODUCING step (not receiving):
|
|
30
|
+
- `↳ response → {previous}` appears after scout's reads line
|
|
31
|
+
- Only shows when next step's template uses `{previous}`
|
|
32
|
+
- Clearer mental model: output flows DOWN the chain
|
|
33
|
+
- Chain TUI footer updated: `[e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress`
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **Chain READ/WRITE instructions now prepended** - Instructions restructured:
|
|
37
|
+
- `[Read from: /path/file.md]` and `[Write to: /path/file.md]` prepended BEFORE task
|
|
38
|
+
- Overrides any hardcoded filenames in task text from parent agent
|
|
39
|
+
- Previously: instructions were appended at end and could be overlooked
|
|
40
|
+
- **Output file validation** - After each step, validates expected file was created:
|
|
41
|
+
- If missing, warns: "Agent wrote to different file(s): X instead of Y"
|
|
42
|
+
- Helps diagnose when agents don't create expected outputs
|
|
43
|
+
- **Root cause: agents need `write` tool** - Agents without `write` in their tools list
|
|
44
|
+
cannot create output files (they tried MCP workarounds which failed)
|
|
45
|
+
- **Thinking level suffixes now preserved** - Models with thinking levels (e.g., `claude-sonnet-4-5:high`)
|
|
46
|
+
now correctly resolve to `anthropic/claude-sonnet-4-5:high` instead of losing the provider prefix
|
|
47
|
+
|
|
48
|
+
### Improved
|
|
49
|
+
- **Per-step progress indicators** - When progress is enabled, each step shows its role:
|
|
50
|
+
- Step 1: `● creates & updates progress.md`
|
|
51
|
+
- Step 2+: `↔ reads & updates progress.md`
|
|
52
|
+
- Clear visualization of progress.md data flow through the chain
|
|
53
|
+
- **Comprehensive tool descriptions** - Better documentation of chain variables:
|
|
54
|
+
- Tool description now explains `{task}`, `{previous}`, `{chain_dir}` in detail
|
|
55
|
+
- Schema descriptions clarify what each variable means and when to use them
|
|
56
|
+
- Helps agents construct proper chain queries for any use case
|
|
57
|
+
|
|
3
58
|
## [0.3.2] - 2026-01-25
|
|
4
59
|
|
|
5
60
|
### Performance
|
package/README.md
CHANGED
|
@@ -50,9 +50,22 @@ npx pi-subagents --remove
|
|
|
50
50
|
- `Esc` - Cancel
|
|
51
51
|
- `↑↓` - Navigate between steps
|
|
52
52
|
- `e` - Edit task/template
|
|
53
|
-
- `
|
|
53
|
+
- `m` - Select model (fuzzy search with all available models)
|
|
54
|
+
- `t` - Select thinking level (off, minimal, low, medium, high, xhigh)
|
|
55
|
+
- `w` - Edit writes (output file path)
|
|
54
56
|
- `r` - Edit reads list
|
|
55
|
-
- `p` - Toggle progress tracking
|
|
57
|
+
- `p` - Toggle progress tracking for ALL steps (chains share one progress.md)
|
|
58
|
+
|
|
59
|
+
*Model selector mode:*
|
|
60
|
+
- `↑↓` - Navigate model list
|
|
61
|
+
- `Enter` - Select model
|
|
62
|
+
- `Esc` - Cancel (keep current model)
|
|
63
|
+
- Type to filter (fuzzy search by model name or provider)
|
|
64
|
+
|
|
65
|
+
*Thinking level selector mode:*
|
|
66
|
+
- `↑↓` - Navigate level list
|
|
67
|
+
- `Enter` - Select level
|
|
68
|
+
- `Esc` - Cancel (keep current level)
|
|
56
69
|
|
|
57
70
|
*Edit mode (full-screen editor with word wrapping):*
|
|
58
71
|
- `Esc` - Save changes and exit
|
package/chain-clarify.ts
CHANGED
|
@@ -11,11 +11,19 @@ import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui"
|
|
|
11
11
|
import type { AgentConfig } from "./agents.js";
|
|
12
12
|
import type { ResolvedStepBehavior } from "./settings.js";
|
|
13
13
|
|
|
14
|
+
/** Model info for display */
|
|
15
|
+
export interface ModelInfo {
|
|
16
|
+
provider: string;
|
|
17
|
+
id: string;
|
|
18
|
+
fullId: string; // "provider/id"
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/** Modified behavior overrides from TUI editing */
|
|
15
22
|
export interface BehaviorOverride {
|
|
16
23
|
output?: string | false;
|
|
17
24
|
reads?: string[] | false;
|
|
18
25
|
progress?: boolean;
|
|
26
|
+
model?: string; // Override agent's default model (format: "provider/id")
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export interface ChainClarifyResult {
|
|
@@ -25,7 +33,11 @@ export interface ChainClarifyResult {
|
|
|
25
33
|
behaviorOverrides: (BehaviorOverride | undefined)[];
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
type EditMode = "template" | "output" | "reads";
|
|
36
|
+
type EditMode = "template" | "output" | "reads" | "model" | "thinking";
|
|
37
|
+
|
|
38
|
+
/** Valid thinking levels */
|
|
39
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
40
|
+
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
29
41
|
|
|
30
42
|
/**
|
|
31
43
|
* TUI component for chain clarification.
|
|
@@ -47,6 +59,17 @@ export class ChainClarifyComponent implements Component {
|
|
|
47
59
|
/** Track user modifications to behaviors (sparse - only stores changes) */
|
|
48
60
|
private behaviorOverrides: Map<number, BehaviorOverride> = new Map();
|
|
49
61
|
|
|
62
|
+
/** Model selector state */
|
|
63
|
+
private modelSearchQuery: string = "";
|
|
64
|
+
private modelSelectedIndex: number = 0;
|
|
65
|
+
private filteredModels: ModelInfo[] = [];
|
|
66
|
+
|
|
67
|
+
/** Max models visible in selector */
|
|
68
|
+
private readonly MODEL_SELECTOR_HEIGHT = 10;
|
|
69
|
+
|
|
70
|
+
/** Thinking level selector state */
|
|
71
|
+
private thinkingSelectedIndex: number = 0;
|
|
72
|
+
|
|
50
73
|
constructor(
|
|
51
74
|
private tui: TUI,
|
|
52
75
|
private theme: Theme,
|
|
@@ -55,8 +78,12 @@ export class ChainClarifyComponent implements Component {
|
|
|
55
78
|
private originalTask: string,
|
|
56
79
|
private chainDir: string,
|
|
57
80
|
private resolvedBehaviors: ResolvedStepBehavior[],
|
|
81
|
+
private availableModels: ModelInfo[],
|
|
58
82
|
private done: (result: ChainClarifyResult) => void,
|
|
59
|
-
) {
|
|
83
|
+
) {
|
|
84
|
+
// Initialize filtered models
|
|
85
|
+
this.filteredModels = [...availableModels];
|
|
86
|
+
}
|
|
60
87
|
|
|
61
88
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
89
|
// Helper methods for rendering
|
|
@@ -244,7 +271,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
244
271
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
272
|
|
|
246
273
|
/** Get effective behavior for a step (with user overrides applied) */
|
|
247
|
-
private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior {
|
|
274
|
+
private getEffectiveBehavior(stepIndex: number): ResolvedStepBehavior & { model?: string } {
|
|
248
275
|
const base = this.resolvedBehaviors[stepIndex]!;
|
|
249
276
|
const override = this.behaviorOverrides.get(stepIndex);
|
|
250
277
|
if (!override) return base;
|
|
@@ -253,9 +280,44 @@ export class ChainClarifyComponent implements Component {
|
|
|
253
280
|
output: override.output !== undefined ? override.output : base.output,
|
|
254
281
|
reads: override.reads !== undefined ? override.reads : base.reads,
|
|
255
282
|
progress: override.progress !== undefined ? override.progress : base.progress,
|
|
283
|
+
model: override.model,
|
|
256
284
|
};
|
|
257
285
|
}
|
|
258
286
|
|
|
287
|
+
/** Get the effective model for a step (override or agent default) */
|
|
288
|
+
private getEffectiveModel(stepIndex: number): string {
|
|
289
|
+
const override = this.behaviorOverrides.get(stepIndex);
|
|
290
|
+
if (override?.model) return override.model; // Override is already in provider/model format
|
|
291
|
+
|
|
292
|
+
// Use agent's configured model or "default"
|
|
293
|
+
const agentModel = this.agentConfigs[stepIndex]?.model;
|
|
294
|
+
if (!agentModel) return "default";
|
|
295
|
+
|
|
296
|
+
// Resolve model name to full provider/model format
|
|
297
|
+
return this.resolveModelFullId(agentModel);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Resolve a model name to its full provider/model format */
|
|
301
|
+
private resolveModelFullId(modelName: string): string {
|
|
302
|
+
// If already in provider/model format, return as-is
|
|
303
|
+
if (modelName.includes("/")) return modelName;
|
|
304
|
+
|
|
305
|
+
// Handle thinking level suffixes (e.g., "claude-sonnet-4-5:high")
|
|
306
|
+
// Strip the suffix for lookup, then add it back
|
|
307
|
+
const colonIdx = modelName.lastIndexOf(":");
|
|
308
|
+
const baseModel = colonIdx !== -1 ? modelName.substring(0, colonIdx) : modelName;
|
|
309
|
+
const thinkingSuffix = colonIdx !== -1 ? modelName.substring(colonIdx) : "";
|
|
310
|
+
|
|
311
|
+
// Look up base model in available models to find provider
|
|
312
|
+
const match = this.availableModels.find(m => m.id === baseModel);
|
|
313
|
+
if (match) {
|
|
314
|
+
return thinkingSuffix ? `${match.fullId}${thinkingSuffix}` : match.fullId;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Fallback to just the model name if not found
|
|
318
|
+
return modelName;
|
|
319
|
+
}
|
|
320
|
+
|
|
259
321
|
/** Update a behavior override for a step */
|
|
260
322
|
private updateBehavior(stepIndex: number, field: keyof BehaviorOverride, value: string | boolean | string[] | false): void {
|
|
261
323
|
const existing = this.behaviorOverrides.get(stepIndex) ?? {};
|
|
@@ -264,7 +326,13 @@ export class ChainClarifyComponent implements Component {
|
|
|
264
326
|
|
|
265
327
|
handleInput(data: string): void {
|
|
266
328
|
if (this.editingStep !== null) {
|
|
267
|
-
this.
|
|
329
|
+
if (this.editMode === "model") {
|
|
330
|
+
this.handleModelSelectorInput(data);
|
|
331
|
+
} else if (this.editMode === "thinking") {
|
|
332
|
+
this.handleThinkingSelectorInput(data);
|
|
333
|
+
} else {
|
|
334
|
+
this.handleEditInput(data);
|
|
335
|
+
}
|
|
268
336
|
return;
|
|
269
337
|
}
|
|
270
338
|
|
|
@@ -303,8 +371,8 @@ export class ChainClarifyComponent implements Component {
|
|
|
303
371
|
return;
|
|
304
372
|
}
|
|
305
373
|
|
|
306
|
-
// '
|
|
307
|
-
if (data === "
|
|
374
|
+
// 'w' to edit writes (output file)
|
|
375
|
+
if (data === "w") {
|
|
308
376
|
this.enterEditMode("output");
|
|
309
377
|
return;
|
|
310
378
|
}
|
|
@@ -315,13 +383,30 @@ export class ChainClarifyComponent implements Component {
|
|
|
315
383
|
return;
|
|
316
384
|
}
|
|
317
385
|
|
|
318
|
-
// 'p' to toggle progress
|
|
386
|
+
// 'p' to toggle progress for ALL steps (chains share a single progress.md)
|
|
319
387
|
if (data === "p") {
|
|
320
|
-
|
|
321
|
-
this.
|
|
388
|
+
// Check if any step has progress enabled
|
|
389
|
+
const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
|
|
390
|
+
// Toggle all steps to the opposite state
|
|
391
|
+
const newState = !anyEnabled;
|
|
392
|
+
for (let i = 0; i < this.agentConfigs.length; i++) {
|
|
393
|
+
this.updateBehavior(i, "progress", newState);
|
|
394
|
+
}
|
|
322
395
|
this.tui.requestRender();
|
|
323
396
|
return;
|
|
324
397
|
}
|
|
398
|
+
|
|
399
|
+
// 'm' to select model
|
|
400
|
+
if (data === "m") {
|
|
401
|
+
this.enterModelSelector();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 't' to select thinking level
|
|
406
|
+
if (data === "t") {
|
|
407
|
+
this.enterThinkingSelector();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
325
410
|
}
|
|
326
411
|
|
|
327
412
|
private enterEditMode(mode: EditMode): void {
|
|
@@ -345,6 +430,173 @@ export class ChainClarifyComponent implements Component {
|
|
|
345
430
|
this.tui.requestRender();
|
|
346
431
|
}
|
|
347
432
|
|
|
433
|
+
/** Enter model selector mode */
|
|
434
|
+
private enterModelSelector(): void {
|
|
435
|
+
this.editingStep = this.selectedStep;
|
|
436
|
+
this.editMode = "model";
|
|
437
|
+
this.modelSearchQuery = "";
|
|
438
|
+
this.modelSelectedIndex = 0;
|
|
439
|
+
this.filteredModels = [...this.availableModels];
|
|
440
|
+
|
|
441
|
+
// Pre-select current model if it exists in the list
|
|
442
|
+
const currentModel = this.getEffectiveModel(this.selectedStep);
|
|
443
|
+
const currentIndex = this.filteredModels.findIndex(m => m.fullId === currentModel || m.id === currentModel);
|
|
444
|
+
if (currentIndex >= 0) {
|
|
445
|
+
this.modelSelectedIndex = currentIndex;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.tui.requestRender();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Filter models based on search query (fuzzy match) */
|
|
452
|
+
private filterModels(): void {
|
|
453
|
+
const query = this.modelSearchQuery.toLowerCase();
|
|
454
|
+
if (!query) {
|
|
455
|
+
this.filteredModels = [...this.availableModels];
|
|
456
|
+
} else {
|
|
457
|
+
this.filteredModels = this.availableModels.filter(m =>
|
|
458
|
+
m.fullId.toLowerCase().includes(query) ||
|
|
459
|
+
m.id.toLowerCase().includes(query) ||
|
|
460
|
+
m.provider.toLowerCase().includes(query)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
// Clamp selected index
|
|
464
|
+
this.modelSelectedIndex = Math.min(this.modelSelectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Handle input in model selector mode */
|
|
468
|
+
private handleModelSelectorInput(data: string): void {
|
|
469
|
+
// Escape or Ctrl+C - cancel and exit
|
|
470
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
471
|
+
this.exitEditMode();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Enter - select current model
|
|
476
|
+
if (matchesKey(data, "return")) {
|
|
477
|
+
const selected = this.filteredModels[this.modelSelectedIndex];
|
|
478
|
+
if (selected) {
|
|
479
|
+
this.updateBehavior(this.editingStep!, "model", selected.fullId);
|
|
480
|
+
}
|
|
481
|
+
this.exitEditMode();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Up arrow - move selection up
|
|
486
|
+
if (matchesKey(data, "up")) {
|
|
487
|
+
if (this.filteredModels.length > 0) {
|
|
488
|
+
this.modelSelectedIndex = this.modelSelectedIndex === 0
|
|
489
|
+
? this.filteredModels.length - 1
|
|
490
|
+
: this.modelSelectedIndex - 1;
|
|
491
|
+
}
|
|
492
|
+
this.tui.requestRender();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Down arrow - move selection down
|
|
497
|
+
if (matchesKey(data, "down")) {
|
|
498
|
+
if (this.filteredModels.length > 0) {
|
|
499
|
+
this.modelSelectedIndex = this.modelSelectedIndex === this.filteredModels.length - 1
|
|
500
|
+
? 0
|
|
501
|
+
: this.modelSelectedIndex + 1;
|
|
502
|
+
}
|
|
503
|
+
this.tui.requestRender();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Backspace - delete last character from search
|
|
508
|
+
if (matchesKey(data, "backspace")) {
|
|
509
|
+
if (this.modelSearchQuery.length > 0) {
|
|
510
|
+
this.modelSearchQuery = this.modelSearchQuery.slice(0, -1);
|
|
511
|
+
this.filterModels();
|
|
512
|
+
}
|
|
513
|
+
this.tui.requestRender();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Printable character - add to search query
|
|
518
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
519
|
+
this.modelSearchQuery += data;
|
|
520
|
+
this.filterModels();
|
|
521
|
+
this.tui.requestRender();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Enter thinking level selector mode */
|
|
527
|
+
private enterThinkingSelector(): void {
|
|
528
|
+
this.editingStep = this.selectedStep;
|
|
529
|
+
this.editMode = "thinking";
|
|
530
|
+
|
|
531
|
+
// Pre-select current thinking level if set
|
|
532
|
+
const currentModel = this.getEffectiveModel(this.selectedStep);
|
|
533
|
+
const colonIdx = currentModel.lastIndexOf(":");
|
|
534
|
+
if (colonIdx !== -1) {
|
|
535
|
+
const suffix = currentModel.substring(colonIdx + 1);
|
|
536
|
+
const levelIdx = THINKING_LEVELS.indexOf(suffix as ThinkingLevel);
|
|
537
|
+
this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : 0;
|
|
538
|
+
} else {
|
|
539
|
+
this.thinkingSelectedIndex = 0; // Default to "off"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.tui.requestRender();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Handle input in thinking level selector mode */
|
|
546
|
+
private handleThinkingSelectorInput(data: string): void {
|
|
547
|
+
// Escape or Ctrl+C - cancel and exit
|
|
548
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
549
|
+
this.exitEditMode();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Enter - select current thinking level
|
|
554
|
+
if (matchesKey(data, "return")) {
|
|
555
|
+
const selectedLevel = THINKING_LEVELS[this.thinkingSelectedIndex];
|
|
556
|
+
this.applyThinkingLevel(selectedLevel);
|
|
557
|
+
this.exitEditMode();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Up arrow - move selection up
|
|
562
|
+
if (matchesKey(data, "up")) {
|
|
563
|
+
this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
|
|
564
|
+
? THINKING_LEVELS.length - 1
|
|
565
|
+
: this.thinkingSelectedIndex - 1;
|
|
566
|
+
this.tui.requestRender();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Down arrow - move selection down
|
|
571
|
+
if (matchesKey(data, "down")) {
|
|
572
|
+
this.thinkingSelectedIndex = this.thinkingSelectedIndex === THINKING_LEVELS.length - 1
|
|
573
|
+
? 0
|
|
574
|
+
: this.thinkingSelectedIndex + 1;
|
|
575
|
+
this.tui.requestRender();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Apply thinking level to the current step's model */
|
|
581
|
+
private applyThinkingLevel(level: ThinkingLevel): void {
|
|
582
|
+
const stepIndex = this.editingStep!;
|
|
583
|
+
const currentModel = this.getEffectiveModel(stepIndex);
|
|
584
|
+
|
|
585
|
+
// Strip any existing thinking level suffix
|
|
586
|
+
const colonIdx = currentModel.lastIndexOf(":");
|
|
587
|
+
let baseModel = currentModel;
|
|
588
|
+
if (colonIdx !== -1) {
|
|
589
|
+
const suffix = currentModel.substring(colonIdx + 1);
|
|
590
|
+
if (THINKING_LEVELS.includes(suffix as ThinkingLevel)) {
|
|
591
|
+
baseModel = currentModel.substring(0, colonIdx);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Apply new thinking level (don't add suffix for "off")
|
|
596
|
+
const newModel = level === "off" ? baseModel : `${baseModel}:${level}`;
|
|
597
|
+
this.updateBehavior(stepIndex, "model", newModel);
|
|
598
|
+
}
|
|
599
|
+
|
|
348
600
|
private handleEditInput(data: string): void {
|
|
349
601
|
const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
|
|
350
602
|
const { lines: wrapped, starts } = this.wrapText(this.editBuffer, textWidth);
|
|
@@ -495,9 +747,19 @@ export class ChainClarifyComponent implements Component {
|
|
|
495
747
|
originalLines[0] = this.editBuffer;
|
|
496
748
|
this.templates[stepIndex] = originalLines.join("\n");
|
|
497
749
|
} else if (this.editMode === "output") {
|
|
750
|
+
// Capture OLD output before updating (for downstream propagation)
|
|
751
|
+
const oldBehavior = this.getEffectiveBehavior(stepIndex);
|
|
752
|
+
const oldOutput = typeof oldBehavior.output === "string" ? oldBehavior.output : null;
|
|
753
|
+
|
|
498
754
|
// Empty string or whitespace means disable output
|
|
499
755
|
const trimmed = this.editBuffer.trim();
|
|
500
|
-
|
|
756
|
+
const newOutput = trimmed === "" ? false : trimmed;
|
|
757
|
+
this.updateBehavior(stepIndex, "output", newOutput);
|
|
758
|
+
|
|
759
|
+
// Propagate output filename change to downstream steps' reads
|
|
760
|
+
if (oldOutput && typeof newOutput === "string" && oldOutput !== newOutput) {
|
|
761
|
+
this.propagateOutputChange(stepIndex, oldOutput, newOutput);
|
|
762
|
+
}
|
|
501
763
|
} else if (this.editMode === "reads") {
|
|
502
764
|
// Parse comma-separated list, empty means disable reads
|
|
503
765
|
const trimmed = this.editBuffer.trim();
|
|
@@ -510,13 +772,181 @@ export class ChainClarifyComponent implements Component {
|
|
|
510
772
|
}
|
|
511
773
|
}
|
|
512
774
|
|
|
775
|
+
/**
|
|
776
|
+
* When a step's output filename changes, update downstream steps that read from it.
|
|
777
|
+
* This maintains the chain dependency automatically.
|
|
778
|
+
*/
|
|
779
|
+
private propagateOutputChange(changedStepIndex: number, oldOutput: string, newOutput: string): void {
|
|
780
|
+
// Check all downstream steps (steps that come after the changed step)
|
|
781
|
+
for (let i = changedStepIndex + 1; i < this.agentConfigs.length; i++) {
|
|
782
|
+
const behavior = this.getEffectiveBehavior(i);
|
|
783
|
+
|
|
784
|
+
// Skip if reads is disabled or empty
|
|
785
|
+
if (behavior.reads === false || !behavior.reads || behavior.reads.length === 0) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Check if this step reads the old output file
|
|
790
|
+
const readsArray = behavior.reads;
|
|
791
|
+
const oldIndex = readsArray.indexOf(oldOutput);
|
|
792
|
+
|
|
793
|
+
if (oldIndex !== -1) {
|
|
794
|
+
// Replace old filename with new filename in reads
|
|
795
|
+
const newReads = [...readsArray];
|
|
796
|
+
newReads[oldIndex] = newOutput;
|
|
797
|
+
this.updateBehavior(i, "reads", newReads);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
513
802
|
render(_width: number): string[] {
|
|
514
803
|
if (this.editingStep !== null) {
|
|
804
|
+
if (this.editMode === "model") {
|
|
805
|
+
return this.renderModelSelector();
|
|
806
|
+
}
|
|
807
|
+
if (this.editMode === "thinking") {
|
|
808
|
+
return this.renderThinkingSelector();
|
|
809
|
+
}
|
|
515
810
|
return this.renderFullEditMode();
|
|
516
811
|
}
|
|
517
812
|
return this.renderNavigationMode();
|
|
518
813
|
}
|
|
519
814
|
|
|
815
|
+
/** Render the model selector view */
|
|
816
|
+
private renderModelSelector(): string[] {
|
|
817
|
+
const innerW = this.width - 2;
|
|
818
|
+
const th = this.theme;
|
|
819
|
+
const lines: string[] = [];
|
|
820
|
+
|
|
821
|
+
// Header
|
|
822
|
+
const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
|
|
823
|
+
const headerText = ` Select Model (Step ${this.editingStep! + 1}: ${agentName}) `;
|
|
824
|
+
lines.push(this.renderHeader(headerText));
|
|
825
|
+
lines.push(this.row(""));
|
|
826
|
+
|
|
827
|
+
// Search input
|
|
828
|
+
const searchPrefix = th.fg("dim", "Search: ");
|
|
829
|
+
const cursor = "\x1b[7m \x1b[27m"; // Reverse video space for cursor
|
|
830
|
+
const searchDisplay = this.modelSearchQuery + cursor;
|
|
831
|
+
lines.push(this.row(` ${searchPrefix}${searchDisplay}`));
|
|
832
|
+
lines.push(this.row(""));
|
|
833
|
+
|
|
834
|
+
// Current model info
|
|
835
|
+
const currentModel = this.getEffectiveModel(this.editingStep!);
|
|
836
|
+
const currentLabel = th.fg("dim", "Current: ");
|
|
837
|
+
lines.push(this.row(` ${currentLabel}${th.fg("warning", currentModel)}`));
|
|
838
|
+
lines.push(this.row(""));
|
|
839
|
+
|
|
840
|
+
// Model list with scroll
|
|
841
|
+
if (this.filteredModels.length === 0) {
|
|
842
|
+
lines.push(this.row(` ${th.fg("dim", "No matching models")}`));
|
|
843
|
+
} else {
|
|
844
|
+
// Calculate visible range (scroll to keep selection visible)
|
|
845
|
+
const maxVisible = this.MODEL_SELECTOR_HEIGHT;
|
|
846
|
+
let startIdx = 0;
|
|
847
|
+
|
|
848
|
+
// Keep selection centered if possible
|
|
849
|
+
if (this.filteredModels.length > maxVisible) {
|
|
850
|
+
startIdx = Math.max(0, this.modelSelectedIndex - Math.floor(maxVisible / 2));
|
|
851
|
+
startIdx = Math.min(startIdx, this.filteredModels.length - maxVisible);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const endIdx = Math.min(startIdx + maxVisible, this.filteredModels.length);
|
|
855
|
+
|
|
856
|
+
// Show scroll indicator if needed
|
|
857
|
+
if (startIdx > 0) {
|
|
858
|
+
lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
862
|
+
const model = this.filteredModels[i]!;
|
|
863
|
+
const isSelected = i === this.modelSelectedIndex;
|
|
864
|
+
const isCurrent = model.fullId === currentModel || model.id === currentModel;
|
|
865
|
+
|
|
866
|
+
const prefix = isSelected ? th.fg("accent", "→ ") : " ";
|
|
867
|
+
const modelText = isSelected ? th.fg("accent", model.id) : model.id;
|
|
868
|
+
const providerBadge = th.fg("dim", ` [${model.provider}]`);
|
|
869
|
+
const currentBadge = isCurrent ? th.fg("success", " ✓") : "";
|
|
870
|
+
|
|
871
|
+
lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Show scroll indicator if needed
|
|
875
|
+
const remaining = this.filteredModels.length - endIdx;
|
|
876
|
+
if (remaining > 0) {
|
|
877
|
+
lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Pad to consistent height
|
|
882
|
+
const contentLines = lines.length;
|
|
883
|
+
const targetHeight = 18; // Consistent height
|
|
884
|
+
for (let i = contentLines; i < targetHeight; i++) {
|
|
885
|
+
lines.push(this.row(""));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Footer
|
|
889
|
+
const footerText = " [Enter] Select • [Esc] Cancel • Type to search ";
|
|
890
|
+
lines.push(this.renderFooter(footerText));
|
|
891
|
+
|
|
892
|
+
return lines;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Render the thinking level selector view */
|
|
896
|
+
private renderThinkingSelector(): string[] {
|
|
897
|
+
const innerW = this.width - 2;
|
|
898
|
+
const th = this.theme;
|
|
899
|
+
const lines: string[] = [];
|
|
900
|
+
|
|
901
|
+
// Header
|
|
902
|
+
const agentName = this.agentConfigs[this.editingStep!]?.name ?? "unknown";
|
|
903
|
+
const headerText = ` Thinking Level (Step ${this.editingStep! + 1}: ${agentName}) `;
|
|
904
|
+
lines.push(this.renderHeader(headerText));
|
|
905
|
+
lines.push(this.row(""));
|
|
906
|
+
|
|
907
|
+
// Current model info
|
|
908
|
+
const currentModel = this.getEffectiveModel(this.editingStep!);
|
|
909
|
+
const currentLabel = th.fg("dim", "Model: ");
|
|
910
|
+
lines.push(this.row(` ${currentLabel}${th.fg("accent", currentModel)}`));
|
|
911
|
+
lines.push(this.row(""));
|
|
912
|
+
|
|
913
|
+
// Description
|
|
914
|
+
lines.push(this.row(` ${th.fg("dim", "Select thinking level (extended thinking budget):")}`));
|
|
915
|
+
lines.push(this.row(""));
|
|
916
|
+
|
|
917
|
+
// Thinking level options
|
|
918
|
+
const levelDescriptions: Record<ThinkingLevel, string> = {
|
|
919
|
+
"off": "No extended thinking",
|
|
920
|
+
"minimal": "Brief reasoning",
|
|
921
|
+
"low": "Light reasoning",
|
|
922
|
+
"medium": "Moderate reasoning",
|
|
923
|
+
"high": "Deep reasoning",
|
|
924
|
+
"xhigh": "Maximum reasoning (ultrathink)",
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
for (let i = 0; i < THINKING_LEVELS.length; i++) {
|
|
928
|
+
const level = THINKING_LEVELS[i];
|
|
929
|
+
const isSelected = i === this.thinkingSelectedIndex;
|
|
930
|
+
const prefix = isSelected ? th.fg("accent", "→ ") : " ";
|
|
931
|
+
const levelText = isSelected ? th.fg("accent", level) : level;
|
|
932
|
+
const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
|
|
933
|
+
lines.push(this.row(` ${prefix}${levelText}${desc}`));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Pad to consistent height
|
|
937
|
+
const contentLines = lines.length;
|
|
938
|
+
const targetHeight = 16;
|
|
939
|
+
for (let i = contentLines; i < targetHeight; i++) {
|
|
940
|
+
lines.push(this.row(""));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Footer
|
|
944
|
+
const footerText = " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
|
|
945
|
+
lines.push(this.renderFooter(footerText));
|
|
946
|
+
|
|
947
|
+
return lines;
|
|
948
|
+
}
|
|
949
|
+
|
|
520
950
|
/** Render navigation mode (step selection, preview) */
|
|
521
951
|
private renderNavigationMode(): string[] {
|
|
522
952
|
const innerW = this.width - 2;
|
|
@@ -536,6 +966,11 @@ export class ChainClarifyComponent implements Component {
|
|
|
536
966
|
lines.push(this.row(` Original Task: ${taskPreview}`));
|
|
537
967
|
const chainDirPreview = truncateToWidth(this.chainDir, innerW - 12);
|
|
538
968
|
lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
|
|
969
|
+
|
|
970
|
+
// Chain-wide progress setting
|
|
971
|
+
const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
|
|
972
|
+
const progressValue = progressEnabled ? th.fg("success", "✓ enabled") : th.fg("dim", "✗ disabled");
|
|
973
|
+
lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
|
|
539
974
|
lines.push(this.row(""));
|
|
540
975
|
|
|
541
976
|
// Each step
|
|
@@ -567,12 +1002,22 @@ export class ChainClarifyComponent implements Component {
|
|
|
567
1002
|
const templateLabel = th.fg("dim", "task: ");
|
|
568
1003
|
lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
|
|
569
1004
|
|
|
570
|
-
//
|
|
571
|
-
const
|
|
1005
|
+
// Model line (show override indicator if modified)
|
|
1006
|
+
const effectiveModel = this.getEffectiveModel(i);
|
|
1007
|
+
const override = this.behaviorOverrides.get(i);
|
|
1008
|
+
const isOverridden = override?.model !== undefined;
|
|
1009
|
+
const modelValue = isOverridden
|
|
1010
|
+
? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
|
|
1011
|
+
: effectiveModel;
|
|
1012
|
+
const modelLabel = th.fg("dim", "model: ");
|
|
1013
|
+
lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
|
|
1014
|
+
|
|
1015
|
+
// Writes line (output file) - renamed from "output" for clarity
|
|
1016
|
+
const writesValue = behavior.output === false
|
|
572
1017
|
? th.fg("dim", "(disabled)")
|
|
573
1018
|
: (behavior.output || th.fg("dim", "(none)"));
|
|
574
|
-
const
|
|
575
|
-
lines.push(this.row(` ${
|
|
1019
|
+
const writesLabel = th.fg("dim", "writes: ");
|
|
1020
|
+
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
576
1021
|
|
|
577
1022
|
// Reads line
|
|
578
1023
|
const readsValue = behavior.reads === false
|
|
@@ -583,16 +1028,32 @@ export class ChainClarifyComponent implements Component {
|
|
|
583
1028
|
const readsLabel = th.fg("dim", "reads: ");
|
|
584
1029
|
lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
|
|
585
1030
|
|
|
586
|
-
// Progress line
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1031
|
+
// Progress line - show when chain-wide progress is enabled
|
|
1032
|
+
// First step creates & updates, subsequent steps read & update
|
|
1033
|
+
if (progressEnabled) {
|
|
1034
|
+
const isFirstStep = i === 0;
|
|
1035
|
+
const progressAction = isFirstStep
|
|
1036
|
+
? th.fg("success", "●") + th.fg("dim", " creates & updates progress.md")
|
|
1037
|
+
: th.fg("accent", "↔") + th.fg("dim", " reads & updates progress.md");
|
|
1038
|
+
const progressLabel = th.fg("dim", "progress: ");
|
|
1039
|
+
lines.push(this.row(` ${progressLabel}${progressAction}`));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Show {previous} indicator for all steps except the last
|
|
1043
|
+
// This shows that this step's text response becomes {previous} for the next step
|
|
1044
|
+
if (i < this.agentConfigs.length - 1) {
|
|
1045
|
+
const nextStepUsePrevious = (this.templates[i + 1] ?? "").includes("{previous}");
|
|
1046
|
+
if (nextStepUsePrevious) {
|
|
1047
|
+
const indicator = th.fg("dim", " ↳ response → ") + th.fg("warning", "{previous}");
|
|
1048
|
+
lines.push(this.row(indicator));
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
590
1051
|
|
|
591
1052
|
lines.push(this.row(""));
|
|
592
1053
|
}
|
|
593
1054
|
|
|
594
1055
|
// Footer with keybindings
|
|
595
|
-
const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [
|
|
1056
|
+
const footerText = " [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hinking [w]rites [r]eads [p]rogress ";
|
|
596
1057
|
lines.push(this.renderFooter(footerText));
|
|
597
1058
|
|
|
598
1059
|
return lines;
|
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
|
@@ -144,9 +144,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
144
144
|
label: "Subagent",
|
|
145
145
|
description: `Delegate to subagents. Use exactly ONE mode:
|
|
146
146
|
• SINGLE: { agent, task } - one task
|
|
147
|
-
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential
|
|
148
|
-
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent
|
|
149
|
-
|
|
147
|
+
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential pipeline
|
|
148
|
+
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent execution
|
|
149
|
+
|
|
150
|
+
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
151
|
+
• {task} - The original task/request from the user
|
|
152
|
+
• {previous} - Text response from the previous step (empty for first step)
|
|
153
|
+
• {chain_dir} - Shared directory for chain files (e.g., /tmp/pi-chain-runs/abc123/)
|
|
154
|
+
|
|
155
|
+
CHAIN DATA FLOW:
|
|
156
|
+
1. Each step's text response automatically becomes {previous} for the next step
|
|
157
|
+
2. Steps can also write files to {chain_dir} (via agent's "output" config)
|
|
158
|
+
3. Later steps can read those files (via agent's "reads" config)
|
|
159
|
+
|
|
160
|
+
Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }`,
|
|
150
161
|
parameters: SubagentParams,
|
|
151
162
|
|
|
152
163
|
async execute(_id, params, onUpdate, ctx, signal) {
|
package/package.json
CHANGED
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()),
|
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 {
|