@dreki-gg/pi-subagent 0.2.0 → 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 +29 -0
- package/README.md +66 -4
- package/agents/docs-scout.md +36 -0
- package/agents/planner.md +38 -0
- package/agents/reviewer.md +36 -0
- package/agents/scout.md +51 -0
- package/agents/ux-designer.md +101 -0
- package/agents/worker.md +25 -0
- package/extensions/subagent/agents.ts +11 -0
- package/extensions/subagent/delegate-executor.ts +200 -0
- package/extensions/subagent/delegate-types.ts +62 -0
- package/extensions/subagent/delegate-ui.ts +132 -0
- package/extensions/subagent/index.ts +294 -1
- package/extensions/subagent/synthesis.ts +78 -0
- package/extensions/subagent/workflows.ts +150 -0
- package/package.json +8 -2
- package/prompts/implement-and-review.md +10 -0
- package/prompts/implement.md +10 -0
- package/prompts/scout-and-plan.md +9 -0
- package/skills/subagent-workflows/SKILL.md +52 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Message } from '@mariozechner/pi-ai';
|
|
2
|
+
|
|
3
|
+
export interface UsageStats {
|
|
4
|
+
input: number;
|
|
5
|
+
output: number;
|
|
6
|
+
cacheRead: number;
|
|
7
|
+
cacheWrite: number;
|
|
8
|
+
cost: number;
|
|
9
|
+
contextTokens: number;
|
|
10
|
+
turns: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentResult {
|
|
14
|
+
agent: string;
|
|
15
|
+
task: string;
|
|
16
|
+
exitCode: number;
|
|
17
|
+
messages: Message[];
|
|
18
|
+
stderr: string;
|
|
19
|
+
usage: UsageStats;
|
|
20
|
+
model?: string;
|
|
21
|
+
stopReason?: string;
|
|
22
|
+
errorMessage?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type WorkflowId =
|
|
26
|
+
| 'scout-only'
|
|
27
|
+
| 'scout-and-plan'
|
|
28
|
+
| 'implement'
|
|
29
|
+
| 'implement-and-review'
|
|
30
|
+
| 'quick-fix'
|
|
31
|
+
| 'review';
|
|
32
|
+
|
|
33
|
+
export interface WorkflowDefinition {
|
|
34
|
+
id: WorkflowId;
|
|
35
|
+
label: string;
|
|
36
|
+
description: string;
|
|
37
|
+
phases: PhaseDefinition[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PhaseKind = 'parallel' | 'single';
|
|
41
|
+
|
|
42
|
+
export interface PhaseDefinition {
|
|
43
|
+
kind: PhaseKind;
|
|
44
|
+
name: string;
|
|
45
|
+
agents: string[];
|
|
46
|
+
requiresConfirmation?: boolean;
|
|
47
|
+
taskTemplate: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PhaseResult {
|
|
51
|
+
name: string;
|
|
52
|
+
kind: PhaseKind;
|
|
53
|
+
agents: AgentResult[];
|
|
54
|
+
skipped?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DelegateState {
|
|
58
|
+
synthesis: string;
|
|
59
|
+
workflow: WorkflowDefinition;
|
|
60
|
+
phases: PhaseResult[];
|
|
61
|
+
totalUsage: UsageStats;
|
|
62
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type {
|
|
3
|
+
AgentResult,
|
|
4
|
+
DelegateState,
|
|
5
|
+
PhaseResult,
|
|
6
|
+
UsageStats,
|
|
7
|
+
WorkflowDefinition,
|
|
8
|
+
} from './delegate-types';
|
|
9
|
+
import { WORKFLOWS, suggestWorkflow } from './workflows';
|
|
10
|
+
|
|
11
|
+
function formatTokens(count: number): string {
|
|
12
|
+
if (count < 1000) return count.toString();
|
|
13
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
14
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
15
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatUsage(usage: UsageStats): string {
|
|
19
|
+
const parts: string[] = [];
|
|
20
|
+
if (usage.turns) parts.push(`${usage.turns} turns`);
|
|
21
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
22
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
23
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
24
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
25
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
26
|
+
return parts.join(' ');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFinalText(result: AgentResult): string {
|
|
30
|
+
for (let i = result.messages.length - 1; i >= 0; i--) {
|
|
31
|
+
const msg = result.messages[i];
|
|
32
|
+
if (msg.role === 'assistant') {
|
|
33
|
+
for (const part of msg.content) {
|
|
34
|
+
if (part.type === 'text') return part.text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return '(no output)';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function confirmSynthesis(
|
|
42
|
+
ctx: ExtensionCommandContext,
|
|
43
|
+
synthesis: string,
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
return ctx.ui.confirm('Delegate — Task Synthesis', `${synthesis}\n\nProceed with this task?`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function pickWorkflow(
|
|
49
|
+
ctx: ExtensionCommandContext,
|
|
50
|
+
synthesis: string,
|
|
51
|
+
): Promise<WorkflowDefinition | null> {
|
|
52
|
+
const suggested = suggestWorkflow(synthesis);
|
|
53
|
+
const options = WORKFLOWS.map((wf) => {
|
|
54
|
+
const marker = wf.id === suggested ? ' ← suggested' : '';
|
|
55
|
+
return `${wf.label}${marker} — ${wf.description}`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const choice = await ctx.ui.select('Delegate — Choose Workflow', options);
|
|
59
|
+
if (choice === undefined || choice === null) return null;
|
|
60
|
+
|
|
61
|
+
const selectedIndex = options.indexOf(choice as string);
|
|
62
|
+
if (selectedIndex < 0) return null;
|
|
63
|
+
|
|
64
|
+
return WORKFLOWS[selectedIndex];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function confirmPlan(
|
|
68
|
+
ctx: ExtensionCommandContext,
|
|
69
|
+
planText: string,
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
return ctx.ui.confirm('Delegate — Review Plan', `${planText}\n\nContinue with implementation?`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatPhaseHeader(
|
|
75
|
+
phaseName: string,
|
|
76
|
+
status: 'running' | 'done' | 'failed' | 'skipped',
|
|
77
|
+
): string {
|
|
78
|
+
const icons = { running: '⏳', done: '✓', failed: '✗', skipped: '⊘' };
|
|
79
|
+
return `${icons[status]} ${phaseName}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatAgentResult(result: AgentResult): string {
|
|
83
|
+
const icon = result.exitCode === 0 ? '✓' : '✗';
|
|
84
|
+
const output = getFinalText(result);
|
|
85
|
+
const preview = output.length > 200 ? `${output.slice(0, 200)}...` : output;
|
|
86
|
+
const usage = formatUsage(result.usage);
|
|
87
|
+
const model = result.model ? ` [${result.model}]` : '';
|
|
88
|
+
|
|
89
|
+
return `${icon} ${result.agent}${model}\n${preview}\n${usage}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatPhaseSummary(phase: PhaseResult): string {
|
|
93
|
+
if (phase.skipped) return `⊘ ${phase.name} — skipped`;
|
|
94
|
+
|
|
95
|
+
const header = `── ${phase.name} (${phase.kind}) ──`;
|
|
96
|
+
const agentSummaries = phase.agents.map(formatAgentResult).join('\n\n');
|
|
97
|
+
return `${header}\n${agentSummaries}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatFullSummary(state: DelegateState): string {
|
|
101
|
+
const sections = [
|
|
102
|
+
`── Synthesis ──\n${state.synthesis}`,
|
|
103
|
+
`── Workflow: ${state.workflow.label} ──`,
|
|
104
|
+
...state.phases.map(formatPhaseSummary),
|
|
105
|
+
`── Usage ──\nTotal: ${formatUsage(state.totalUsage)}`,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
return sections.join('\n\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function aggregateUsage(results: AgentResult[]): UsageStats {
|
|
112
|
+
const total: UsageStats = {
|
|
113
|
+
input: 0,
|
|
114
|
+
output: 0,
|
|
115
|
+
cacheRead: 0,
|
|
116
|
+
cacheWrite: 0,
|
|
117
|
+
cost: 0,
|
|
118
|
+
contextTokens: 0,
|
|
119
|
+
turns: 0,
|
|
120
|
+
};
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
total.input += r.usage.input;
|
|
123
|
+
total.output += r.usage.output;
|
|
124
|
+
total.cacheRead += r.usage.cacheRead;
|
|
125
|
+
total.cacheWrite += r.usage.cacheWrite;
|
|
126
|
+
total.cost += r.usage.cost;
|
|
127
|
+
total.turns += r.usage.turns;
|
|
128
|
+
}
|
|
129
|
+
return total;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { getFinalText };
|
|
@@ -13,20 +13,43 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
16
17
|
import * as fs from 'node:fs';
|
|
18
|
+
import { copyFile, mkdir, readdir, unlink } from 'node:fs/promises';
|
|
17
19
|
import * as os from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
18
21
|
import * as path from 'node:path';
|
|
19
22
|
import type { AgentToolResult } from '@mariozechner/pi-agent-core';
|
|
20
23
|
import type { Message } from '@mariozechner/pi-ai';
|
|
21
24
|
import { StringEnum } from '@mariozechner/pi-ai';
|
|
22
25
|
import {
|
|
23
26
|
type ExtensionAPI,
|
|
27
|
+
type ExtensionCommandContext,
|
|
28
|
+
getAgentDir,
|
|
24
29
|
getMarkdownTheme,
|
|
25
30
|
withFileMutationQueue,
|
|
26
31
|
} from '@mariozechner/pi-coding-agent';
|
|
27
32
|
import { Container, Markdown, Spacer, Text } from '@mariozechner/pi-tui';
|
|
28
33
|
import { Type } from '@sinclair/typebox';
|
|
29
|
-
import { type AgentConfig, type AgentScope, discoverAgents } from './agents.js';
|
|
34
|
+
import { type AgentConfig, type AgentScope, bundledAgentsDir, discoverAgents } from './agents.js';
|
|
35
|
+
import { runAgent, runParallel } from './delegate-executor.js';
|
|
36
|
+
import { buildSynthesisPrompt, extractRecentConversation } from './synthesis.js';
|
|
37
|
+
import type {
|
|
38
|
+
AgentResult,
|
|
39
|
+
DelegateState,
|
|
40
|
+
PhaseDefinition,
|
|
41
|
+
PhaseResult,
|
|
42
|
+
UsageStats as DelegateUsageStats,
|
|
43
|
+
} from './delegate-types.js';
|
|
44
|
+
import {
|
|
45
|
+
aggregateUsage,
|
|
46
|
+
confirmPlan,
|
|
47
|
+
confirmSynthesis,
|
|
48
|
+
formatFullSummary,
|
|
49
|
+
formatPhaseHeader,
|
|
50
|
+
getFinalText,
|
|
51
|
+
pickWorkflow,
|
|
52
|
+
} from './delegate-ui.js';
|
|
30
53
|
|
|
31
54
|
const MAX_PARALLEL_TASKS = 8;
|
|
32
55
|
const MAX_CONCURRENCY = 4;
|
|
@@ -1083,4 +1106,274 @@ export default function (pi: ExtensionAPI) {
|
|
|
1083
1106
|
return new Text(text?.type === 'text' ? text.text : '(no output)', 0, 0);
|
|
1084
1107
|
},
|
|
1085
1108
|
});
|
|
1109
|
+
|
|
1110
|
+
// ── Delegate commands ──────────────────────────────────────────────────────
|
|
1111
|
+
|
|
1112
|
+
function emptyDelegateUsage(): DelegateUsageStats {
|
|
1113
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function combineParallelOutputs(results: AgentResult[]): string {
|
|
1117
|
+
return results
|
|
1118
|
+
.map((r) => {
|
|
1119
|
+
const output = getFinalText(r);
|
|
1120
|
+
return `## ${r.agent}\n${output}`;
|
|
1121
|
+
})
|
|
1122
|
+
.join('\n\n');
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function executePhase(
|
|
1126
|
+
cwd: string,
|
|
1127
|
+
phase: PhaseDefinition,
|
|
1128
|
+
synthesis: string,
|
|
1129
|
+
previousOutput: string,
|
|
1130
|
+
ctx: ExtensionCommandContext,
|
|
1131
|
+
): Promise<{ results: AgentResult[]; output: string; aborted: boolean }> {
|
|
1132
|
+
const task = phase.taskTemplate
|
|
1133
|
+
.replace(/\{synthesis\}/g, synthesis)
|
|
1134
|
+
.replace(/\{previous\}/g, previousOutput);
|
|
1135
|
+
|
|
1136
|
+
ctx.ui.notify(
|
|
1137
|
+
`${formatPhaseHeader(phase.name, 'running')} ${phase.kind === 'parallel' ? `(${phase.agents.length} agents)` : phase.agents[0]}`,
|
|
1138
|
+
'info',
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
let results: AgentResult[];
|
|
1142
|
+
if (phase.kind === 'parallel') {
|
|
1143
|
+
results = await runParallel(
|
|
1144
|
+
cwd,
|
|
1145
|
+
phase.agents,
|
|
1146
|
+
task,
|
|
1147
|
+
(_phaseName, _agentName, _result) => {
|
|
1148
|
+
// Streaming updates happen per-message
|
|
1149
|
+
},
|
|
1150
|
+
phase.name,
|
|
1151
|
+
);
|
|
1152
|
+
} else {
|
|
1153
|
+
const result = await runAgent(cwd, phase.agents[0], task, undefined, phase.name);
|
|
1154
|
+
results = [result];
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const hasError = results.some((r) => r.exitCode !== 0 || r.stopReason === 'error');
|
|
1158
|
+
if (hasError) {
|
|
1159
|
+
const failedAgents = results
|
|
1160
|
+
.filter((r) => r.exitCode !== 0 || r.stopReason === 'error')
|
|
1161
|
+
.map((r) => `${r.agent}: ${r.errorMessage || r.stderr || '(unknown error)'}`)
|
|
1162
|
+
.join('\n');
|
|
1163
|
+
|
|
1164
|
+
ctx.ui.notify(`${formatPhaseHeader(phase.name, 'failed')}\n${failedAgents}`, 'error');
|
|
1165
|
+
return { results, output: combineParallelOutputs(results), aborted: true };
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const output = combineParallelOutputs(results);
|
|
1169
|
+
ctx.ui.notify(formatPhaseHeader(phase.name, 'done'), 'info');
|
|
1170
|
+
|
|
1171
|
+
if (phase.requiresConfirmation) {
|
|
1172
|
+
const planText = output;
|
|
1173
|
+
const proceed = await confirmPlan(ctx, planText);
|
|
1174
|
+
if (!proceed) {
|
|
1175
|
+
ctx.ui.notify('Delegate aborted by user after plan review.', 'warning');
|
|
1176
|
+
return { results, output, aborted: true };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return { results, output, aborted: false };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function listMdFiles(dir: string): Promise<Set<string>> {
|
|
1184
|
+
if (!existsSync(dir)) return new Set();
|
|
1185
|
+
try {
|
|
1186
|
+
const entries = await readdir(dir);
|
|
1187
|
+
return new Set(entries.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')));
|
|
1188
|
+
} catch {
|
|
1189
|
+
return new Set();
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
pi.registerCommand('delegate-agents', {
|
|
1194
|
+
description: 'List, customize, or reset delegate agents',
|
|
1195
|
+
handler: async (args, ctx) => {
|
|
1196
|
+
const userDir = join(getAgentDir(), 'agents');
|
|
1197
|
+
const parts = args?.trim().split(/\s+/) || [];
|
|
1198
|
+
const action = parts[0] || 'list';
|
|
1199
|
+
|
|
1200
|
+
if (action === 'list') {
|
|
1201
|
+
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1202
|
+
const user = await listMdFiles(userDir);
|
|
1203
|
+
const allNames = new Set([...bundled, ...user]);
|
|
1204
|
+
|
|
1205
|
+
const lines: string[] = ['## Delegate Agents\n'];
|
|
1206
|
+
for (const name of [...allNames].sort()) {
|
|
1207
|
+
const isBundled = bundled.has(name);
|
|
1208
|
+
const isUser = user.has(name);
|
|
1209
|
+
if (isUser && isBundled) {
|
|
1210
|
+
lines.push(`- **${name}** — user override (bundled version available, \`/delegate-agents reset ${name}\` to restore)`);
|
|
1211
|
+
} else if (isUser) {
|
|
1212
|
+
lines.push(`- **${name}** — user-only`);
|
|
1213
|
+
} else {
|
|
1214
|
+
lines.push(`- **${name}** — bundled`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
pi.sendMessage({ customType: 'delegate-agents-list', content: lines.join('\n'), display: true });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (action === 'reset') {
|
|
1222
|
+
const name = parts[1];
|
|
1223
|
+
if (!name) {
|
|
1224
|
+
ctx.ui.notify('Usage: /delegate-agents reset <name|--all>', 'warning');
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (name === '--all') {
|
|
1229
|
+
const user = await listMdFiles(userDir);
|
|
1230
|
+
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1231
|
+
let count = 0;
|
|
1232
|
+
for (const n of user) {
|
|
1233
|
+
if (bundled.has(n)) {
|
|
1234
|
+
await unlink(join(userDir, `${n}.md`));
|
|
1235
|
+
count++;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
ctx.ui.notify(count > 0 ? `Reset ${count} agent(s) to bundled versions.` : 'No user overrides to reset.', 'info');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const userFile = join(userDir, `${name}.md`);
|
|
1243
|
+
const bundledFile = join(bundledAgentsDir, `${name}.md`);
|
|
1244
|
+
|
|
1245
|
+
if (!existsSync(userFile)) {
|
|
1246
|
+
ctx.ui.notify(`No user override for "${name}" — already using bundled version.`, 'info');
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (!existsSync(bundledFile)) {
|
|
1250
|
+
ctx.ui.notify(`"${name}" is user-only (no bundled version). Delete manually if needed.`, 'warning');
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
await unlink(userFile);
|
|
1255
|
+
ctx.ui.notify(`Reset "${name}" — now using bundled version.`, 'info');
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (action === 'edit') {
|
|
1260
|
+
const name = parts[1];
|
|
1261
|
+
if (!name) {
|
|
1262
|
+
ctx.ui.notify('Usage: /delegate-agents edit <name>', 'warning');
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const bundledFile = join(bundledAgentsDir, `${name}.md`);
|
|
1267
|
+
const userFile = join(userDir, `${name}.md`);
|
|
1268
|
+
|
|
1269
|
+
if (existsSync(userFile)) {
|
|
1270
|
+
ctx.ui.notify(`User override already exists: ${userFile}`, 'info');
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (!existsSync(bundledFile)) {
|
|
1274
|
+
ctx.ui.notify(`No bundled agent named "${name}".`, 'warning');
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
await mkdir(userDir, { recursive: true });
|
|
1279
|
+
await copyFile(bundledFile, userFile);
|
|
1280
|
+
ctx.ui.notify(`Copied "${name}" to ${userFile} — edit it there to customize.`, 'info');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
ctx.ui.notify('Unknown action. Usage: /delegate-agents [list|reset <name|--all>|edit <name>]', 'warning');
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
pi.registerCommand('delegate', {
|
|
1289
|
+
description: 'Orchestrate subagent workflows — scouts, planners, workers, reviewers',
|
|
1290
|
+
handler: async (args, ctx) => {
|
|
1291
|
+
const explicitTask = args?.trim() || undefined;
|
|
1292
|
+
|
|
1293
|
+
// Step 1: Synthesize
|
|
1294
|
+
const conversation = extractRecentConversation(ctx);
|
|
1295
|
+
if (!conversation.trim() && !explicitTask) {
|
|
1296
|
+
ctx.ui.notify(
|
|
1297
|
+
'No conversation context and no task specified. Usage: /delegate [task]',
|
|
1298
|
+
'warning',
|
|
1299
|
+
);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const prompt = buildSynthesisPrompt(conversation, explicitTask);
|
|
1304
|
+
ctx.ui.notify('⏳ Synthesizing task from conversation...', 'info');
|
|
1305
|
+
|
|
1306
|
+
const synthResult = await runAgent(ctx.cwd, 'planner', prompt);
|
|
1307
|
+
const synthesis = getFinalText(synthResult);
|
|
1308
|
+
|
|
1309
|
+
if (!synthesis.trim()) {
|
|
1310
|
+
ctx.ui.notify('Failed to synthesize task from conversation.', 'error');
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Step 2: Confirm synthesis
|
|
1315
|
+
const synthOk = await confirmSynthesis(ctx, synthesis);
|
|
1316
|
+
if (!synthOk) {
|
|
1317
|
+
ctx.ui.notify('Delegate cancelled.', 'info');
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Step 3: Pick workflow
|
|
1322
|
+
const workflow = await pickWorkflow(ctx, synthesis);
|
|
1323
|
+
if (!workflow) {
|
|
1324
|
+
ctx.ui.notify('Delegate cancelled — no workflow selected.', 'info');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Step 4: Execute phases
|
|
1329
|
+
const state: DelegateState = {
|
|
1330
|
+
synthesis,
|
|
1331
|
+
workflow,
|
|
1332
|
+
phases: [],
|
|
1333
|
+
totalUsage: emptyDelegateUsage(),
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
let previousOutput = '';
|
|
1337
|
+
|
|
1338
|
+
ctx.ui.notify(`Starting workflow: ${workflow.label}`, 'info');
|
|
1339
|
+
|
|
1340
|
+
for (const phaseDef of workflow.phases) {
|
|
1341
|
+
const phaseResult = await executePhase(ctx.cwd, phaseDef, synthesis, previousOutput, ctx);
|
|
1342
|
+
|
|
1343
|
+
const phase: PhaseResult = {
|
|
1344
|
+
name: phaseDef.name,
|
|
1345
|
+
kind: phaseDef.kind,
|
|
1346
|
+
agents: phaseResult.results,
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
state.phases.push(phase);
|
|
1350
|
+
const phaseUsage = aggregateUsage(phaseResult.results);
|
|
1351
|
+
state.totalUsage.input += phaseUsage.input;
|
|
1352
|
+
state.totalUsage.output += phaseUsage.output;
|
|
1353
|
+
state.totalUsage.cacheRead += phaseUsage.cacheRead;
|
|
1354
|
+
state.totalUsage.cacheWrite += phaseUsage.cacheWrite;
|
|
1355
|
+
state.totalUsage.cost += phaseUsage.cost;
|
|
1356
|
+
state.totalUsage.turns += phaseUsage.turns;
|
|
1357
|
+
|
|
1358
|
+
if (phaseResult.aborted) break;
|
|
1359
|
+
|
|
1360
|
+
previousOutput = phaseResult.output;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Step 5: Show full summary
|
|
1364
|
+
const summary = formatFullSummary(state);
|
|
1365
|
+
ctx.ui.notify(`Delegate complete — ${workflow.label}`, 'info');
|
|
1366
|
+
|
|
1367
|
+
pi.sendMessage({
|
|
1368
|
+
customType: 'delegate-summary',
|
|
1369
|
+
content: summary,
|
|
1370
|
+
display: true,
|
|
1371
|
+
details: {
|
|
1372
|
+
workflow: workflow.id,
|
|
1373
|
+
phaseCount: state.phases.length,
|
|
1374
|
+
totalUsage: state.totalUsage,
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1086
1379
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
const MAX_MESSAGES = 40;
|
|
4
|
+
|
|
5
|
+
const SYNTHESIS_INSTRUCTION = `Synthesize the current conversation into a clear task specification for a team of subagents that will execute it.
|
|
6
|
+
|
|
7
|
+
Your synthesis MUST capture:
|
|
8
|
+
|
|
9
|
+
1. **Goal** — What are we building, changing, or investigating?
|
|
10
|
+
2. **Decisions made** — Every locked choice, preference, or constraint the user confirmed.
|
|
11
|
+
3. **Constraints** — What was explicitly rejected and why.
|
|
12
|
+
4. **Architecture** — The agreed structure, file layout, data flow, or integration shape.
|
|
13
|
+
5. **Open questions** — Anything flagged but not resolved yet.
|
|
14
|
+
6. **Intent** — The user's real motivation and the nuance behind the decisions.
|
|
15
|
+
|
|
16
|
+
Be specific and actionable. Do NOT summarize vaguely. Include concrete names, paths, tools, APIs, and patterns that were discussed.
|
|
17
|
+
|
|
18
|
+
The output should be usable by agents who have NOT seen this conversation.
|
|
19
|
+
|
|
20
|
+
Format:
|
|
21
|
+
|
|
22
|
+
## Goal
|
|
23
|
+
<one paragraph>
|
|
24
|
+
|
|
25
|
+
## Decisions
|
|
26
|
+
<bulleted list of every locked decision>
|
|
27
|
+
|
|
28
|
+
## Constraints
|
|
29
|
+
<bulleted list of what was rejected and why>
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
<concrete structure, files, data flow>
|
|
33
|
+
|
|
34
|
+
## Open Questions
|
|
35
|
+
<anything unresolved>
|
|
36
|
+
|
|
37
|
+
## Intent
|
|
38
|
+
<the user's real motivation>`;
|
|
39
|
+
|
|
40
|
+
export function extractRecentConversation(ctx: ExtensionCommandContext): string {
|
|
41
|
+
const entries = ctx.sessionManager.getBranch();
|
|
42
|
+
const messages: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (entry.type !== 'message') continue;
|
|
46
|
+
const msg = entry.message;
|
|
47
|
+
if (!msg) continue;
|
|
48
|
+
|
|
49
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
50
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
51
|
+
const textParts = content
|
|
52
|
+
.filter((part: { type: string }) => part.type === 'text')
|
|
53
|
+
.map((part: { type: string; text?: string }) => (part as { text: string }).text)
|
|
54
|
+
.join('\n');
|
|
55
|
+
|
|
56
|
+
if (textParts.trim()) {
|
|
57
|
+
messages.push(`[${msg.role}]\n${textParts.trim()}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return messages.slice(-MAX_MESSAGES).join('\n\n---\n\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildSynthesisPrompt(conversation: string, explicitTask?: string): string {
|
|
66
|
+
const parts = [SYNTHESIS_INSTRUCTION];
|
|
67
|
+
|
|
68
|
+
if (explicitTask) {
|
|
69
|
+
parts.push(`\nThe user explicitly specified the task as:\n${explicitTask}`);
|
|
70
|
+
parts.push(
|
|
71
|
+
'\nUse this as the primary goal, but enrich it with relevant context from the conversation.',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
parts.push(`\n\n## Conversation\n\n${conversation}`);
|
|
76
|
+
|
|
77
|
+
return parts.join('\n');
|
|
78
|
+
}
|