pi-subagents 0.3.0
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 +94 -0
- package/README.md +300 -0
- package/agents.ts +172 -0
- package/artifacts.ts +70 -0
- package/chain-clarify.ts +612 -0
- package/index.ts +2186 -0
- package/install.mjs +93 -0
- package/notify.ts +87 -0
- package/package.json +38 -0
- package/settings.ts +492 -0
- package/subagent-runner.ts +608 -0
- package/types.ts +114 -0
package/install.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pi-subagents installer
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx pi-subagents # Install to ~/.pi/agent/extensions/subagent
|
|
8
|
+
* npx pi-subagents --remove # Remove the extension
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
|
|
16
|
+
const EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
17
|
+
const REPO_URL = "https://github.com/nicobailon/pi-subagents.git";
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const isRemove = args.includes("--remove") || args.includes("-r");
|
|
21
|
+
const isHelp = args.includes("--help") || args.includes("-h");
|
|
22
|
+
|
|
23
|
+
if (isHelp) {
|
|
24
|
+
console.log(`
|
|
25
|
+
pi-subagents - Pi extension for delegating tasks to subagents
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
npx pi-subagents Install the extension
|
|
29
|
+
npx pi-subagents --remove Remove the extension
|
|
30
|
+
npx pi-subagents --help Show this help
|
|
31
|
+
|
|
32
|
+
Installation directory: ${EXTENSION_DIR}
|
|
33
|
+
`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isRemove) {
|
|
38
|
+
if (fs.existsSync(EXTENSION_DIR)) {
|
|
39
|
+
console.log(`Removing ${EXTENSION_DIR}...`);
|
|
40
|
+
fs.rmSync(EXTENSION_DIR, { recursive: true });
|
|
41
|
+
console.log("✓ pi-subagents removed");
|
|
42
|
+
} else {
|
|
43
|
+
console.log("pi-subagents is not installed");
|
|
44
|
+
}
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Install
|
|
49
|
+
console.log("Installing pi-subagents...\n");
|
|
50
|
+
|
|
51
|
+
// Ensure parent directory exists
|
|
52
|
+
const parentDir = path.dirname(EXTENSION_DIR);
|
|
53
|
+
if (!fs.existsSync(parentDir)) {
|
|
54
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if already installed
|
|
58
|
+
if (fs.existsSync(EXTENSION_DIR)) {
|
|
59
|
+
const isGitRepo = fs.existsSync(path.join(EXTENSION_DIR, ".git"));
|
|
60
|
+
if (isGitRepo) {
|
|
61
|
+
console.log("Updating existing installation...");
|
|
62
|
+
try {
|
|
63
|
+
execSync("git pull", { cwd: EXTENSION_DIR, stdio: "inherit" });
|
|
64
|
+
console.log("\n✓ pi-subagents updated");
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error("Failed to update. Try removing and reinstalling:");
|
|
67
|
+
console.error(" npx pi-subagents --remove && npx pi-subagents");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`Directory exists but is not a git repo: ${EXTENSION_DIR}`);
|
|
72
|
+
console.log("Remove it first with: npx pi-subagents --remove");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Fresh install
|
|
77
|
+
console.log(`Cloning to ${EXTENSION_DIR}...`);
|
|
78
|
+
try {
|
|
79
|
+
execSync(`git clone ${REPO_URL} "${EXTENSION_DIR}"`, { stdio: "inherit" });
|
|
80
|
+
console.log("\n✓ pi-subagents installed");
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error("Failed to clone repository");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`
|
|
88
|
+
The extension is now available in pi. Tools added:
|
|
89
|
+
• subagent - Delegate tasks to agents (single, chain, parallel)
|
|
90
|
+
• subagent_status - Check async run status
|
|
91
|
+
|
|
92
|
+
Documentation: ${EXTENSION_DIR}/README.md
|
|
93
|
+
`);
|
package/notify.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent completion notifications (extension)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
interface ChainStepResult {
|
|
8
|
+
agent: string;
|
|
9
|
+
output: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SubagentResult {
|
|
14
|
+
id: string | null;
|
|
15
|
+
agent: string | null;
|
|
16
|
+
success: boolean;
|
|
17
|
+
summary: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
sessionFile?: string;
|
|
21
|
+
shareUrl?: string;
|
|
22
|
+
gistUrl?: string;
|
|
23
|
+
shareError?: string;
|
|
24
|
+
results?: ChainStepResult[];
|
|
25
|
+
taskIndex?: number;
|
|
26
|
+
totalTasks?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function registerSubagentNotify(pi: ExtensionAPI): void {
|
|
30
|
+
const seen = new Map<string, number>();
|
|
31
|
+
const ttlMs = 10 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
const prune = (now: number) => {
|
|
34
|
+
for (const [key, ts] of seen.entries()) {
|
|
35
|
+
if (now - ts > ttlMs) seen.delete(key);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleComplete = (data: unknown) => {
|
|
40
|
+
const result = data as SubagentResult;
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const key = `${result.id ?? "no-id"}:${result.agent ?? "unknown"}:${result.timestamp ?? now}`;
|
|
43
|
+
prune(now);
|
|
44
|
+
if (seen.has(key)) return;
|
|
45
|
+
seen.set(key, now);
|
|
46
|
+
|
|
47
|
+
const agent = result.agent ?? "unknown";
|
|
48
|
+
const status = result.success ? "completed" : "failed";
|
|
49
|
+
|
|
50
|
+
const taskInfo =
|
|
51
|
+
result.taskIndex !== undefined && result.totalTasks !== undefined
|
|
52
|
+
? ` (${result.taskIndex + 1}/${result.totalTasks})`
|
|
53
|
+
: "";
|
|
54
|
+
|
|
55
|
+
const extra: string[] = [];
|
|
56
|
+
if (result.shareUrl) {
|
|
57
|
+
extra.push(`Session: ${result.shareUrl}`);
|
|
58
|
+
} else if (result.shareError) {
|
|
59
|
+
extra.push(`Session share error: ${result.shareError}`);
|
|
60
|
+
} else if (result.sessionFile) {
|
|
61
|
+
extra.push(`Session file: ${result.sessionFile}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const content = [
|
|
65
|
+
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
66
|
+
"",
|
|
67
|
+
result.summary,
|
|
68
|
+
extra.length ? "" : undefined,
|
|
69
|
+
extra.length ? extra.join("\n") : undefined,
|
|
70
|
+
]
|
|
71
|
+
.filter((line) => line !== undefined)
|
|
72
|
+
.join("\n");
|
|
73
|
+
|
|
74
|
+
pi.sendMessage(
|
|
75
|
+
{
|
|
76
|
+
customType: "subagent-notify",
|
|
77
|
+
content,
|
|
78
|
+
display: true,
|
|
79
|
+
},
|
|
80
|
+
{ triggerTurn: true },
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
pi.events.on("subagent:complete", handleComplete);
|
|
85
|
+
pi.events.on("subagent_enhanced:complete", handleComplete);
|
|
86
|
+
pi.events.on("async_subagent:complete", handleComplete);
|
|
87
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-subagents",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
|
|
5
|
+
"author": "Nico Bailon",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nicobailon/pi-subagents.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/nicobailon/pi-subagents#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/nicobailon/pi-subagents/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi",
|
|
17
|
+
"pi-coding-agent",
|
|
18
|
+
"subagents",
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"pi-subagents": "./install.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"*.ts",
|
|
28
|
+
"*.mjs",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./index.ts",
|
|
35
|
+
"./notify.ts"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
package/settings.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent settings, chain behavior, and template management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { AgentConfig } from "./agents.js";
|
|
9
|
+
|
|
10
|
+
const SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
11
|
+
const CHAIN_RUNS_DIR = "/tmp/pi-chain-runs";
|
|
12
|
+
const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Settings Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export interface ChainTemplates {
|
|
19
|
+
[chainKey: string]: {
|
|
20
|
+
[agentName: string]: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SubagentSettings {
|
|
25
|
+
chains?: ChainTemplates;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Behavior Resolution Types
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export interface ResolvedStepBehavior {
|
|
33
|
+
output: string | false;
|
|
34
|
+
reads: string[] | false;
|
|
35
|
+
progress: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StepOverrides {
|
|
39
|
+
output?: string | false;
|
|
40
|
+
reads?: string[] | false;
|
|
41
|
+
progress?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Chain Step Types
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/** Sequential step: single agent execution */
|
|
49
|
+
export interface SequentialStep {
|
|
50
|
+
agent: string;
|
|
51
|
+
task?: string;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
output?: string | false;
|
|
54
|
+
reads?: string[] | false;
|
|
55
|
+
progress?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parallel task item within a parallel step */
|
|
59
|
+
export interface ParallelTaskItem {
|
|
60
|
+
agent: string;
|
|
61
|
+
task?: string;
|
|
62
|
+
cwd?: string;
|
|
63
|
+
output?: string | false;
|
|
64
|
+
reads?: string[] | false;
|
|
65
|
+
progress?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parallel step: multiple agents running concurrently */
|
|
69
|
+
export interface ParallelStep {
|
|
70
|
+
parallel: ParallelTaskItem[];
|
|
71
|
+
concurrency?: number;
|
|
72
|
+
failFast?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Union type for chain steps */
|
|
76
|
+
export type ChainStep = SequentialStep | ParallelStep;
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Type Guards
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
export function isParallelStep(step: ChainStep): step is ParallelStep {
|
|
83
|
+
return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isSequentialStep(step: ChainStep): step is SequentialStep {
|
|
87
|
+
return "agent" in step && !("parallel" in step);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Get all agent names in a step (single for sequential, multiple for parallel) */
|
|
91
|
+
export function getStepAgents(step: ChainStep): string[] {
|
|
92
|
+
if (isParallelStep(step)) {
|
|
93
|
+
return step.parallel.map((t) => t.agent);
|
|
94
|
+
}
|
|
95
|
+
return [step.agent];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get total task count in a step */
|
|
99
|
+
export function getStepTaskCount(step: ChainStep): number {
|
|
100
|
+
if (isParallelStep(step)) {
|
|
101
|
+
return step.parallel.length;
|
|
102
|
+
}
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Settings Management
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
export function loadSubagentSettings(): SubagentSettings {
|
|
111
|
+
try {
|
|
112
|
+
const data = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
113
|
+
return (data.subagent as SubagentSettings) ?? {};
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function saveChainTemplate(chainKey: string, templates: Record<string, string>): void {
|
|
120
|
+
let settings: Record<string, unknown> = {};
|
|
121
|
+
try {
|
|
122
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
if (!settings.subagent) settings.subagent = {};
|
|
126
|
+
const subagent = settings.subagent as Record<string, unknown>;
|
|
127
|
+
if (!subagent.chains) subagent.chains = {};
|
|
128
|
+
const chains = subagent.chains as Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
chains[chainKey] = templates;
|
|
131
|
+
|
|
132
|
+
const dir = path.dirname(SETTINGS_PATH);
|
|
133
|
+
if (!fs.existsSync(dir)) {
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getChainKey(agents: string[]): string {
|
|
140
|
+
return agents.join("->");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Chain Directory Management
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
export function createChainDir(runId: string): string {
|
|
148
|
+
const chainDir = path.join(CHAIN_RUNS_DIR, runId);
|
|
149
|
+
fs.mkdirSync(chainDir, { recursive: true });
|
|
150
|
+
return chainDir;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function removeChainDir(chainDir: string): void {
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(chainDir, { recursive: true });
|
|
156
|
+
} catch {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function cleanupOldChainDirs(): void {
|
|
160
|
+
if (!fs.existsSync(CHAIN_RUNS_DIR)) return;
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
let dirs: string[];
|
|
163
|
+
try {
|
|
164
|
+
dirs = fs.readdirSync(CHAIN_RUNS_DIR);
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const dir of dirs) {
|
|
170
|
+
try {
|
|
171
|
+
const dirPath = path.join(CHAIN_RUNS_DIR, dir);
|
|
172
|
+
const stat = fs.statSync(dirPath);
|
|
173
|
+
if (stat.isDirectory() && now - stat.mtimeMs > CHAIN_DIR_MAX_AGE_MS) {
|
|
174
|
+
fs.rmSync(dirPath, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Skip directories that can't be processed; continue with others
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Template Resolution
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve templates for each step in a chain.
|
|
188
|
+
* Priority: inline task > saved template > default
|
|
189
|
+
* Default for step 0: "{task}", for others: "{previous}"
|
|
190
|
+
*/
|
|
191
|
+
export function resolveChainTemplates(
|
|
192
|
+
agentNames: string[],
|
|
193
|
+
inlineTasks: (string | undefined)[],
|
|
194
|
+
settings: SubagentSettings,
|
|
195
|
+
): string[] {
|
|
196
|
+
const chainKey = getChainKey(agentNames);
|
|
197
|
+
const savedTemplates = settings.chains?.[chainKey] ?? {};
|
|
198
|
+
|
|
199
|
+
return agentNames.map((agent, i) => {
|
|
200
|
+
// Priority: inline > saved > default
|
|
201
|
+
const inline = inlineTasks[i];
|
|
202
|
+
if (inline) return inline;
|
|
203
|
+
|
|
204
|
+
const saved = savedTemplates[agent];
|
|
205
|
+
if (saved) return saved;
|
|
206
|
+
|
|
207
|
+
// Default: first step uses {task}, others use {previous}
|
|
208
|
+
return i === 0 ? "{task}" : "{previous}";
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =============================================================================
|
|
213
|
+
// Parallel-Aware Template Resolution
|
|
214
|
+
// =============================================================================
|
|
215
|
+
|
|
216
|
+
/** Resolved templates for a chain - string for sequential, string[] for parallel */
|
|
217
|
+
export type ResolvedTemplates = (string | string[])[];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolve templates for a chain with parallel step support.
|
|
221
|
+
* Returns string for sequential steps, string[] for parallel steps.
|
|
222
|
+
*/
|
|
223
|
+
export function resolveChainTemplatesV2(
|
|
224
|
+
steps: ChainStep[],
|
|
225
|
+
settings: SubagentSettings,
|
|
226
|
+
): ResolvedTemplates {
|
|
227
|
+
return steps.map((step, i) => {
|
|
228
|
+
if (isParallelStep(step)) {
|
|
229
|
+
// Parallel step: resolve each task's template
|
|
230
|
+
return step.parallel.map((task) => {
|
|
231
|
+
if (task.task) return task.task;
|
|
232
|
+
// Default for parallel tasks is {previous}
|
|
233
|
+
return "{previous}";
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// Sequential step: existing logic
|
|
237
|
+
const seq = step as SequentialStep;
|
|
238
|
+
if (seq.task) return seq.task;
|
|
239
|
+
// Default: first step uses {task}, others use {previous}
|
|
240
|
+
return i === 0 ? "{task}" : "{previous}";
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Flatten templates for display (TUI navigation needs flat list)
|
|
246
|
+
*/
|
|
247
|
+
export function flattenTemplates(templates: ResolvedTemplates): string[] {
|
|
248
|
+
const result: string[] = [];
|
|
249
|
+
for (const t of templates) {
|
|
250
|
+
if (Array.isArray(t)) {
|
|
251
|
+
result.push(...t);
|
|
252
|
+
} else {
|
|
253
|
+
result.push(t);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Unflatten templates back to structured form
|
|
261
|
+
*/
|
|
262
|
+
export function unflattenTemplates(
|
|
263
|
+
flat: string[],
|
|
264
|
+
steps: ChainStep[],
|
|
265
|
+
): ResolvedTemplates {
|
|
266
|
+
const result: ResolvedTemplates = [];
|
|
267
|
+
let idx = 0;
|
|
268
|
+
for (const step of steps) {
|
|
269
|
+
if (isParallelStep(step)) {
|
|
270
|
+
const count = step.parallel.length;
|
|
271
|
+
result.push(flat.slice(idx, idx + count));
|
|
272
|
+
idx += count;
|
|
273
|
+
} else {
|
|
274
|
+
result.push(flat[idx]!);
|
|
275
|
+
idx++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =============================================================================
|
|
282
|
+
// Behavior Resolution
|
|
283
|
+
// =============================================================================
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolve effective chain behavior per step.
|
|
287
|
+
* Priority: step override > agent frontmatter > false (disabled)
|
|
288
|
+
*/
|
|
289
|
+
export function resolveStepBehavior(
|
|
290
|
+
agentConfig: AgentConfig,
|
|
291
|
+
stepOverrides: StepOverrides,
|
|
292
|
+
): ResolvedStepBehavior {
|
|
293
|
+
// Output: step override > frontmatter > false (no output)
|
|
294
|
+
const output =
|
|
295
|
+
stepOverrides.output !== undefined
|
|
296
|
+
? stepOverrides.output
|
|
297
|
+
: agentConfig.output ?? false;
|
|
298
|
+
|
|
299
|
+
// Reads: step override > frontmatter defaultReads > false (no reads)
|
|
300
|
+
const reads =
|
|
301
|
+
stepOverrides.reads !== undefined
|
|
302
|
+
? stepOverrides.reads
|
|
303
|
+
: agentConfig.defaultReads ?? false;
|
|
304
|
+
|
|
305
|
+
// Progress: step override > frontmatter defaultProgress > false
|
|
306
|
+
const progress =
|
|
307
|
+
stepOverrides.progress !== undefined
|
|
308
|
+
? stepOverrides.progress
|
|
309
|
+
: agentConfig.defaultProgress ?? false;
|
|
310
|
+
|
|
311
|
+
return { output, reads, progress };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Find index of first agent in chain that has progress enabled
|
|
316
|
+
*/
|
|
317
|
+
export function findFirstProgressAgentIndex(
|
|
318
|
+
agentConfigs: AgentConfig[],
|
|
319
|
+
stepOverrides: StepOverrides[],
|
|
320
|
+
): number {
|
|
321
|
+
return agentConfigs.findIndex((config, i) => {
|
|
322
|
+
const override = stepOverrides[i];
|
|
323
|
+
if (override?.progress !== undefined) return override.progress;
|
|
324
|
+
return config.defaultProgress ?? false;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// Chain Instruction Injection
|
|
330
|
+
// =============================================================================
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Resolve a file path: absolute paths pass through, relative paths get chainDir prepended.
|
|
334
|
+
*/
|
|
335
|
+
function resolveChainPath(filePath: string, chainDir: string): string {
|
|
336
|
+
return path.isAbsolute(filePath) ? filePath : `${chainDir}/${filePath}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Build chain instructions from resolved behavior.
|
|
341
|
+
* These are appended to the task to tell the agent what to read/write.
|
|
342
|
+
*/
|
|
343
|
+
export function buildChainInstructions(
|
|
344
|
+
behavior: ResolvedStepBehavior,
|
|
345
|
+
chainDir: string,
|
|
346
|
+
isFirstProgressAgent: boolean,
|
|
347
|
+
previousSummary?: string,
|
|
348
|
+
): string {
|
|
349
|
+
const instructions: string[] = [];
|
|
350
|
+
|
|
351
|
+
// Include previous step's summary if available (prose output from prior agent)
|
|
352
|
+
if (previousSummary && previousSummary.trim()) {
|
|
353
|
+
instructions.push(`Previous step summary:\n\n${previousSummary.trim()}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Reads (supports both absolute and relative paths)
|
|
357
|
+
if (behavior.reads && behavior.reads.length > 0) {
|
|
358
|
+
const files = behavior.reads.map((f) => resolveChainPath(f, chainDir)).join(", ");
|
|
359
|
+
instructions.push(`Read these files: ${files}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Output (supports both absolute and relative paths)
|
|
363
|
+
if (behavior.output) {
|
|
364
|
+
const outputPath = resolveChainPath(behavior.output, chainDir);
|
|
365
|
+
instructions.push(`Write your output to: ${outputPath}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Progress
|
|
369
|
+
if (behavior.progress) {
|
|
370
|
+
const progressPath = `${chainDir}/progress.md`;
|
|
371
|
+
if (isFirstProgressAgent) {
|
|
372
|
+
instructions.push(`Create and maintain: ${progressPath}`);
|
|
373
|
+
instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
|
|
374
|
+
} else {
|
|
375
|
+
instructions.push(`Read and update: ${progressPath}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (instructions.length === 0) return "";
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
"\n\n---\n**Chain Instructions:**\n" + instructions.map((i) => `- ${i}`).join("\n")
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// =============================================================================
|
|
387
|
+
// Parallel Step Support
|
|
388
|
+
// =============================================================================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Resolve behaviors for all tasks in a parallel step.
|
|
392
|
+
* Creates namespaced output paths to avoid collisions.
|
|
393
|
+
*/
|
|
394
|
+
export function resolveParallelBehaviors(
|
|
395
|
+
tasks: ParallelTaskItem[],
|
|
396
|
+
agentConfigs: AgentConfig[],
|
|
397
|
+
stepIndex: number,
|
|
398
|
+
): ResolvedStepBehavior[] {
|
|
399
|
+
return tasks.map((task, taskIndex) => {
|
|
400
|
+
const config = agentConfigs.find((a) => a.name === task.agent);
|
|
401
|
+
if (!config) {
|
|
402
|
+
throw new Error(`Unknown agent: ${task.agent}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Build subdirectory path for this parallel task
|
|
406
|
+
const subdir = `parallel-${stepIndex}/${taskIndex}-${task.agent}`;
|
|
407
|
+
|
|
408
|
+
// Output: task override > agent default (namespaced) > false
|
|
409
|
+
// Absolute paths pass through unchanged; relative paths get namespaced under subdir
|
|
410
|
+
let output: string | false = false;
|
|
411
|
+
if (task.output !== undefined) {
|
|
412
|
+
if (task.output === false) {
|
|
413
|
+
output = false;
|
|
414
|
+
} else if (path.isAbsolute(task.output)) {
|
|
415
|
+
output = task.output; // Absolute path: use as-is
|
|
416
|
+
} else {
|
|
417
|
+
output = `${subdir}/${task.output}`; // Relative: namespace under subdir
|
|
418
|
+
}
|
|
419
|
+
} else if (config.output) {
|
|
420
|
+
// Agent defaults are always relative, so namespace them
|
|
421
|
+
output = `${subdir}/${config.output}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Reads: task override > agent default > false
|
|
425
|
+
const reads =
|
|
426
|
+
task.reads !== undefined ? task.reads : config.defaultReads ?? false;
|
|
427
|
+
|
|
428
|
+
// Progress: task override > agent default > false
|
|
429
|
+
const progress =
|
|
430
|
+
task.progress !== undefined
|
|
431
|
+
? task.progress
|
|
432
|
+
: config.defaultProgress ?? false;
|
|
433
|
+
|
|
434
|
+
return { output, reads, progress };
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Create subdirectories for parallel step outputs
|
|
440
|
+
*/
|
|
441
|
+
export function createParallelDirs(
|
|
442
|
+
chainDir: string,
|
|
443
|
+
stepIndex: number,
|
|
444
|
+
taskCount: number,
|
|
445
|
+
agentNames: string[],
|
|
446
|
+
): void {
|
|
447
|
+
for (let i = 0; i < taskCount; i++) {
|
|
448
|
+
const subdir = path.join(chainDir, `parallel-${stepIndex}`, `${i}-${agentNames[i]}`);
|
|
449
|
+
fs.mkdirSync(subdir, { recursive: true });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Result from a parallel task (simplified for aggregation) */
|
|
454
|
+
export interface ParallelTaskResult {
|
|
455
|
+
agent: string;
|
|
456
|
+
taskIndex: number;
|
|
457
|
+
output: string;
|
|
458
|
+
exitCode: number;
|
|
459
|
+
error?: string;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Aggregate outputs from parallel tasks into a single string for {previous}.
|
|
464
|
+
* Uses clear separators so the next agent can parse all outputs.
|
|
465
|
+
*/
|
|
466
|
+
export function aggregateParallelOutputs(results: ParallelTaskResult[]): string {
|
|
467
|
+
return results
|
|
468
|
+
.map((r, i) => {
|
|
469
|
+
const header = `=== Parallel Task ${i + 1} (${r.agent}) ===`;
|
|
470
|
+
return `${header}\n${r.output}`;
|
|
471
|
+
})
|
|
472
|
+
.join("\n\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Check if any parallel task failed
|
|
477
|
+
*/
|
|
478
|
+
export function hasParallelFailures(results: ParallelTaskResult[]): boolean {
|
|
479
|
+
return results.some((r) => r.exitCode !== 0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Get failure summary for parallel step
|
|
484
|
+
*/
|
|
485
|
+
export function getParallelFailureSummary(results: ParallelTaskResult[]): string {
|
|
486
|
+
const failures = results.filter((r) => r.exitCode !== 0);
|
|
487
|
+
if (failures.length === 0) return "";
|
|
488
|
+
|
|
489
|
+
return failures
|
|
490
|
+
.map((f) => `- Task ${f.taskIndex + 1} (${f.agent}): ${f.error || "failed"}`)
|
|
491
|
+
.join("\n");
|
|
492
|
+
}
|