pi-subagents-j0k3r 1.0.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/.releaserc.json +14 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/index.ts +531 -0
- package/package.json +73 -0
- package/scripts/verify-package-files.mjs +41 -0
- package/skills/subagents-configuration/SKILL.md +182 -0
- package/src/config.ts +262 -0
- package/src/debug.ts +38 -0
- package/src/history.ts +254 -0
- package/src/interaction-channel.ts +197 -0
- package/src/manager.ts +533 -0
- package/src/model-profiles-ui.ts +609 -0
- package/src/profile-resolver.ts +60 -0
- package/src/runner.ts +688 -0
- package/src/thread-view.ts +477 -0
- package/src/tools.ts +492 -0
- package/src/types.ts +234 -0
- package/src/ui.ts +399 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
|
+
import { readSubagentsConfig } from './config.js';
|
|
3
|
+
import type { SubagentManager } from './manager.js';
|
|
4
|
+
import type { SubagentTask } from './types.js';
|
|
5
|
+
|
|
6
|
+
function ok(text: string, details: Record<string, unknown> = {}) { return { content: [{ type: 'text', text }], details }; }
|
|
7
|
+
function fail(error: unknown) { const msg = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: msg }], details: { error: msg }, isError: true }; }
|
|
8
|
+
function clip(text: string | undefined, limit = 240): string {
|
|
9
|
+
if (!text) return '';
|
|
10
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
11
|
+
return normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized;
|
|
12
|
+
}
|
|
13
|
+
function formatTokens(count: number): string {
|
|
14
|
+
if (count < 1000) return count.toString();
|
|
15
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
16
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
17
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
18
|
+
}
|
|
19
|
+
function formatUsage(task: SubagentTask): string {
|
|
20
|
+
const usage = task.usage;
|
|
21
|
+
if (!usage) return '';
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? 's' : ''}`);
|
|
24
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
25
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
26
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
27
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
28
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
29
|
+
if (usage.contextTokens) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
30
|
+
return parts.join(' ');
|
|
31
|
+
}
|
|
32
|
+
function modelEffortLine(task: SubagentTask): string {
|
|
33
|
+
return [`model: ${task.model ?? 'default/current'}`, `effort: ${task.effort ?? 'default/current'}`].join(' · ');
|
|
34
|
+
}
|
|
35
|
+
function formatTask(task: SubagentTask): string {
|
|
36
|
+
const when = task.last_activity_at ?? task.started_at ?? task.created_at;
|
|
37
|
+
const usage = formatUsage(task);
|
|
38
|
+
const lines = [
|
|
39
|
+
`agent: ${task.agent} · status: ${task.status} · id: ${task.id}`,
|
|
40
|
+
modelEffortLine(task),
|
|
41
|
+
usage ? `usage: ${usage}` : undefined,
|
|
42
|
+
`last: ${task.last_activity ?? 'n/a'}${when ? ` at ${when}` : ''}`,
|
|
43
|
+
].filter(Boolean) as string[];
|
|
44
|
+
const preview = clip(task.output_preview ?? task.result ?? task.error);
|
|
45
|
+
if (preview) lines.push(`preview: ${preview}`);
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatTaskListItem(task: SubagentTask): string {
|
|
50
|
+
const when = task.last_activity_at ?? task.started_at ?? task.created_at;
|
|
51
|
+
const usage = formatUsage(task);
|
|
52
|
+
const lines = [
|
|
53
|
+
`agent: ${task.agent} · status: ${task.status} · id: ${task.id}`,
|
|
54
|
+
modelEffortLine(task),
|
|
55
|
+
usage ? `usage: ${usage}` : undefined,
|
|
56
|
+
`last: ${task.last_activity ?? 'n/a'}${when ? ` at ${when}` : ''}`,
|
|
57
|
+
(task.result || task.error || task.output_preview) ? `preview: collapsed · use subagent_result ${task.id}` : undefined,
|
|
58
|
+
].filter(Boolean) as string[];
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatTaskListRow(task: SubagentTask): string {
|
|
63
|
+
const usage = formatUsage(task);
|
|
64
|
+
return [
|
|
65
|
+
`agent: ${task.agent} · status: ${task.status} · id: ${task.id}`,
|
|
66
|
+
usage ? `usage: ${usage}` : undefined,
|
|
67
|
+
].filter(Boolean).join(' · ');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatTaskListSummary(tasks: SubagentTask[]): string {
|
|
71
|
+
if (!tasks.length) return 'Listed 0 subagent task(s).';
|
|
72
|
+
const mostRecent = tasks[0]!;
|
|
73
|
+
return [
|
|
74
|
+
`Listed ${tasks.length} subagent task(s).`,
|
|
75
|
+
`Most recent: ${mostRecent.agent} · ${mostRecent.status} · ${mostRecent.id}${mostRecent.task ? ` · task: ${clip(mostRecent.task, 80)}` : ''}`,
|
|
76
|
+
'List view: collapsed · ctrl+o to expand',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatTaskListRender(tasks: SubagentTask[], expanded: boolean): string {
|
|
81
|
+
if (!tasks.length) return 'Listed 0 subagent task(s).';
|
|
82
|
+
if (expanded) return `Listed ${tasks.length} subagent task(s):\n\n${tasks.map(formatTaskListItem).join('\n\n')}`;
|
|
83
|
+
const visible = tasks.slice(0, 5);
|
|
84
|
+
const hidden = tasks.length - visible.length;
|
|
85
|
+
return [
|
|
86
|
+
`Listed ${tasks.length} subagent task(s).`,
|
|
87
|
+
'List view: collapsed · ctrl+o to expand',
|
|
88
|
+
'',
|
|
89
|
+
...visible.map(formatTaskListRow),
|
|
90
|
+
hidden > 0 ? `… ${hidden} more task(s) hidden` : undefined,
|
|
91
|
+
].filter(Boolean).join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function progressText(tasks: SubagentTask[], frame = 0, options: { backgroundable?: boolean; backgroundShortcut?: string } = {}): string {
|
|
95
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'][frame % 10];
|
|
96
|
+
const active = tasks.find((task) => task.status === 'running') ?? tasks[0];
|
|
97
|
+
if (!active) return `${spinner} Starting subagent…`;
|
|
98
|
+
const usage = formatUsage(active);
|
|
99
|
+
return [
|
|
100
|
+
`${spinner} agent: ${active.agent} · status: ${active.status} · effort: ${active.effort ?? 'default/current'}`,
|
|
101
|
+
`↳ model: ${active.model ?? 'starting'}${usage ? ` · usage: ${usage}` : ''}`,
|
|
102
|
+
`↳ ${clip(active.last_activity ?? active.task ?? active.id, 160)}`,
|
|
103
|
+
options.backgroundable ? `↳ ${options.backgroundShortcut ?? 'ctrl+h'} to send to background` : undefined,
|
|
104
|
+
].filter(Boolean).join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function installDoubleEscapeCancel(ctx: any, manager: SubagentManager, onCancel: () => void): () => void {
|
|
108
|
+
let lastEscapeAt = 0;
|
|
109
|
+
const unsubscribe = ctx?.ui?.onTerminalInput?.((data: string) => {
|
|
110
|
+
if (data !== '\u001b') return undefined;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const isDoubleEscape = now - lastEscapeAt <= 600;
|
|
113
|
+
lastEscapeAt = now;
|
|
114
|
+
if (!isDoubleEscape) return { consume: true };
|
|
115
|
+
onCancel();
|
|
116
|
+
const cancelled = manager.cancelRunning('cancelled by double escape');
|
|
117
|
+
ctx?.abort?.();
|
|
118
|
+
ctx?.ui?.notify?.(
|
|
119
|
+
cancelled.length ? `Cancelled ${cancelled.length} subagent task(s).` : 'Requested subagent/main cancellation.',
|
|
120
|
+
'warning',
|
|
121
|
+
);
|
|
122
|
+
lastEscapeAt = 0;
|
|
123
|
+
return { consume: true };
|
|
124
|
+
});
|
|
125
|
+
return typeof unsubscribe === 'function' ? unsubscribe : () => {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const activeClaudeBackgroundHandoffs = new Set<() => SubagentTask[]>();
|
|
129
|
+
|
|
130
|
+
function sendTasksToBackground(
|
|
131
|
+
ctx: any,
|
|
132
|
+
manager: SubagentManager,
|
|
133
|
+
getTaskIds: () => string[],
|
|
134
|
+
onBackground: (tasks: SubagentTask[]) => void,
|
|
135
|
+
): SubagentTask[] {
|
|
136
|
+
const backgrounded = manager.sendToBackground(getTaskIds());
|
|
137
|
+
if (!backgrounded.length) return [];
|
|
138
|
+
ctx?.ui?.notify?.(
|
|
139
|
+
backgrounded.length === 1 ? `Sent subagent to background: ${backgrounded[0]!.id}` : `Sent ${backgrounded.length} subagent task(s) to background.`,
|
|
140
|
+
'info',
|
|
141
|
+
);
|
|
142
|
+
onBackground(backgrounded);
|
|
143
|
+
return backgrounded;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function triggerClaudeBackgroundHandoff(): boolean {
|
|
147
|
+
for (const handoff of [...activeClaudeBackgroundHandoffs]) {
|
|
148
|
+
if (handoff().length) return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ctrlShortcutToTerminalInput(shortcut: string): string | undefined {
|
|
154
|
+
const match = shortcut.trim().toLowerCase().match(/^ctrl\+([a-z])$/);
|
|
155
|
+
if (!match) return undefined;
|
|
156
|
+
const code = match[1]!.charCodeAt(0) - 96;
|
|
157
|
+
return code >= 1 && code <= 26 ? String.fromCharCode(code) : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function installBackgroundHandoffShortcut(
|
|
161
|
+
ctx: any,
|
|
162
|
+
manager: SubagentManager,
|
|
163
|
+
getTaskIds: () => string[],
|
|
164
|
+
onBackground: (tasks: SubagentTask[]) => void,
|
|
165
|
+
): () => void {
|
|
166
|
+
const shortcut = readSubagentsConfig(ctx?.cwd ?? process.cwd()).background_handoff_shortcut ?? 'ctrl+h';
|
|
167
|
+
const terminalInput = ctrlShortcutToTerminalInput(shortcut);
|
|
168
|
+
const handoff = () => sendTasksToBackground(ctx, manager, getTaskIds, onBackground);
|
|
169
|
+
activeClaudeBackgroundHandoffs.add(handoff);
|
|
170
|
+
const unsubscribe = terminalInput ? ctx?.ui?.onTerminalInput?.((data: string) => {
|
|
171
|
+
if (data !== terminalInput) return undefined;
|
|
172
|
+
return handoff().length ? { consume: true } : undefined;
|
|
173
|
+
}) : undefined;
|
|
174
|
+
return () => {
|
|
175
|
+
activeClaudeBackgroundHandoffs.delete(handoff);
|
|
176
|
+
if (typeof unsubscribe === 'function') unsubscribe();
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const TERMINAL_ESCAPE_RE = /\u001b\][^\u001b\u0007]*(?:\u001b\\|\u0007)|\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
181
|
+
const TERMINAL_ESCAPE_AT_START_RE = /^(?:\u001b\][^\u001b\u0007]*(?:\u001b\\|\u0007)|\u001b\[[0-?]*[ -/]*[@-~])/;
|
|
182
|
+
|
|
183
|
+
function visibleTextWidth(text: string): number {
|
|
184
|
+
return [...text.replace(TERMINAL_ESCAPE_RE, '')].length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function truncateStyledLine(text: string, width: number): string {
|
|
188
|
+
if (width <= 0) return '';
|
|
189
|
+
if (visibleTextWidth(text) <= width) return text;
|
|
190
|
+
const maxTextWidth = Math.max(0, width - 1);
|
|
191
|
+
let out = '';
|
|
192
|
+
let used = 0;
|
|
193
|
+
let index = 0;
|
|
194
|
+
while (index < text.length && used < maxTextWidth) {
|
|
195
|
+
const rest = text.slice(index);
|
|
196
|
+
const escape = rest.match(TERMINAL_ESCAPE_AT_START_RE)?.[0];
|
|
197
|
+
if (escape) {
|
|
198
|
+
out += escape;
|
|
199
|
+
index += escape.length;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const char = [...rest][0];
|
|
203
|
+
if (!char) break;
|
|
204
|
+
out += char;
|
|
205
|
+
index += char.length;
|
|
206
|
+
used++;
|
|
207
|
+
}
|
|
208
|
+
return `${out}…\u001b[0m`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function textComponent(text: string) {
|
|
212
|
+
return {
|
|
213
|
+
invalidate() {},
|
|
214
|
+
render(width: number) {
|
|
215
|
+
return text.split('\n').map((line) => truncateStyledLine(line, width));
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function taskFromDetails(result: any): SubagentTask | undefined {
|
|
221
|
+
return result?.details?.tasks?.[0] ?? result?.details?.results?.[0] ?? result?.details?.task;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sessionIdFromToolContext(ctx: any): string | undefined {
|
|
225
|
+
const direct = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId;
|
|
226
|
+
if (typeof direct === 'string' && direct.length > 0) return direct;
|
|
227
|
+
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
228
|
+
return typeof file === 'string' && file.length > 0 ? file : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function compactTaskForToolResult(task: SubagentTask): SubagentTask {
|
|
232
|
+
const { thread_snapshot: _threadSnapshot, ...compact } = task;
|
|
233
|
+
return compact;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function compactTaskWithoutFinalText(task: SubagentTask): SubagentTask {
|
|
237
|
+
const { thread_snapshot: _threadSnapshot, transcript: _transcript, result: _result, error: _error, ...compact } = task;
|
|
238
|
+
return compact;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function compactResultDetails<T extends Record<string, any>>(details: T): T {
|
|
242
|
+
return {
|
|
243
|
+
...details,
|
|
244
|
+
task: details.task ? compactTaskForToolResult(details.task) : details.task,
|
|
245
|
+
tasks: Array.isArray(details.tasks) ? details.tasks.map(compactTaskForToolResult) : details.tasks,
|
|
246
|
+
results: Array.isArray(details.results) ? details.results.map(compactTaskForToolResult) : details.results,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function collapsedResultHint(task: SubagentTask | undefined, failed: boolean): string {
|
|
251
|
+
if (!task) return failed ? 'result: collapsed · ctrl+o to expand' : 'response: collapsed · ctrl+o to expand';
|
|
252
|
+
const label = failed ? 'error' : 'response';
|
|
253
|
+
return `${label}: collapsed · ctrl+o to expand · /subagents or subagent_result ${task.id}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function taskFinalText(task: SubagentTask | undefined, result?: any): string {
|
|
257
|
+
if (typeof result?.details?.full_result === 'string') return result.details.full_result;
|
|
258
|
+
return task?.result ?? task?.error ?? task?.output_preview ?? '';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatTaskModeContent(tasks: SubagentTask[]): string {
|
|
262
|
+
return [
|
|
263
|
+
`Completed ${tasks.length} subagent task(s):`,
|
|
264
|
+
...tasks.map((task) => {
|
|
265
|
+
const finalText = taskFinalText(task);
|
|
266
|
+
return [
|
|
267
|
+
formatTask(task),
|
|
268
|
+
finalText ? `\n# response from ${task.agent} (${task.id})\n${finalText}` : undefined,
|
|
269
|
+
].filter(Boolean).join('\n');
|
|
270
|
+
}),
|
|
271
|
+
].join('\n\n');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function backgroundLaunchContent(taskIds: string[], verb = 'Sent'): string {
|
|
275
|
+
return [
|
|
276
|
+
`${verb} ${taskIds.length} subagent task(s) to background:`,
|
|
277
|
+
taskIds.join('\n'),
|
|
278
|
+
'',
|
|
279
|
+
'Background behavior:',
|
|
280
|
+
'- Do not call subagent_status or subagent_result just to wait.',
|
|
281
|
+
'- The subagent will notify this chat automatically when it finishes.',
|
|
282
|
+
'- Keep the chat available so the user can continue asking questions while it runs.',
|
|
283
|
+
].join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function subagentResultSummary(task: SubagentTask): string {
|
|
287
|
+
const failed = task.status === 'failed' || task.status === 'cancelled';
|
|
288
|
+
const usage = formatUsage(task);
|
|
289
|
+
return [
|
|
290
|
+
`Subagent result: ${task.agent} · status: ${task.status} · id: ${task.id}`,
|
|
291
|
+
modelEffortLine(task),
|
|
292
|
+
usage ? `usage: ${usage}` : undefined,
|
|
293
|
+
collapsedResultHint(task, failed),
|
|
294
|
+
].filter(Boolean).join('\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function registerSubagentTools(pi: any, manager: SubagentManager): void {
|
|
298
|
+
pi.registerTool({
|
|
299
|
+
name: 'subagent_list_agents',
|
|
300
|
+
label: 'Subagent List Agents',
|
|
301
|
+
description: 'List available markdown-defined subagents for delegation.',
|
|
302
|
+
promptSnippet: 'List available subagents loaded from .pi/subagents/*.md.',
|
|
303
|
+
parameters: Type.Object({}),
|
|
304
|
+
async execute(_id: string, _params: any, _signal: any, _onUpdate: any, ctx: any) {
|
|
305
|
+
try { const agents = manager.listAgents(ctx?.cwd ?? process.cwd()); return ok(`Found ${agents.length} subagent(s).`, { agents }); } catch (e) { return fail(e); }
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
pi.registerTool({
|
|
310
|
+
name: 'subagent_run',
|
|
311
|
+
label: 'Subagent Run',
|
|
312
|
+
description: 'Delegate a task to one or more markdown-defined subagents. Use mode=task to wait; use mode=background to free the chat and wait for the automatic completion notification.',
|
|
313
|
+
promptSnippet: 'Delegate analysis/review/test/design tasks to subagents. Supports one or many agents, task or background mode.',
|
|
314
|
+
parameters: Type.Object({
|
|
315
|
+
agent: Type.Optional(Type.String()),
|
|
316
|
+
agents: Type.Optional(Type.Array(Type.String())),
|
|
317
|
+
task: Type.String(),
|
|
318
|
+
context: Type.Optional(Type.String()),
|
|
319
|
+
mode: Type.Optional(Type.Union([Type.Literal('task'), Type.Literal('background')])),
|
|
320
|
+
}),
|
|
321
|
+
async execute(_id: string, params: any, _signal: any, onUpdate: any, ctx: any) {
|
|
322
|
+
let cancelledByDoubleEscape = false;
|
|
323
|
+
let frame = 0;
|
|
324
|
+
let active = true;
|
|
325
|
+
let latestTasks: SubagentTask[] = [];
|
|
326
|
+
const isBackground = params.mode === 'background';
|
|
327
|
+
const subagentsConfig = readSubagentsConfig(ctx?.cwd ?? process.cwd());
|
|
328
|
+
const canBackgroundInClaude = !isBackground && subagentsConfig.mode === 'claude';
|
|
329
|
+
const backgroundShortcut = subagentsConfig.background_handoff_shortcut ?? 'ctrl+h';
|
|
330
|
+
let resolveBackground: ((value: { mode: 'background'; task_ids: string[] }) => void) | undefined;
|
|
331
|
+
const backgroundPromise = canBackgroundInClaude
|
|
332
|
+
? new Promise<{ mode: 'background'; task_ids: string[] }>((resolve) => { resolveBackground = resolve; })
|
|
333
|
+
: undefined;
|
|
334
|
+
const emit = () => {
|
|
335
|
+
if (!active || isBackground) return;
|
|
336
|
+
try {
|
|
337
|
+
onUpdate?.({
|
|
338
|
+
content: [{ type: 'text', text: progressText(latestTasks, frame, { backgroundable: canBackgroundInClaude, backgroundShortcut }) }],
|
|
339
|
+
details: { tasks: latestTasks.map(compactTaskForToolResult), frame: frame++, backgroundable: canBackgroundInClaude, backgroundShortcut },
|
|
340
|
+
});
|
|
341
|
+
} catch {
|
|
342
|
+
active = false;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const interval = isBackground ? undefined : setInterval(emit, 500);
|
|
346
|
+
const uninstallCancel = isBackground ? () => {} : installDoubleEscapeCancel(ctx, manager, () => { cancelledByDoubleEscape = true; });
|
|
347
|
+
const uninstallBackground = canBackgroundInClaude
|
|
348
|
+
? installBackgroundHandoffShortcut(ctx, manager, () => latestTasks.map((task) => task.id), (tasks) => {
|
|
349
|
+
active = false;
|
|
350
|
+
resolveBackground?.({ mode: 'background', task_ids: tasks.map((task) => task.id) });
|
|
351
|
+
})
|
|
352
|
+
: () => {};
|
|
353
|
+
try {
|
|
354
|
+
emit();
|
|
355
|
+
const runPromise = manager.run(params, { ...ctx, pi }, _signal, isBackground ? undefined : (tasks) => { latestTasks = tasks; emit(); });
|
|
356
|
+
const result = backgroundPromise ? await Promise.race([runPromise, backgroundPromise]) : await runPromise;
|
|
357
|
+
if (cancelledByDoubleEscape) throw new Error('Subagent run cancelled by double escape');
|
|
358
|
+
if (!('results' in result)) {
|
|
359
|
+
const details = compactResultDetails(result as any);
|
|
360
|
+
return ok(backgroundLaunchContent(result.task_ids, 'Sent'), details);
|
|
361
|
+
}
|
|
362
|
+
const failedTasks = (result.results ?? []).filter((task) => task.status === 'failed' || task.status === 'cancelled');
|
|
363
|
+
const text = result.mode === 'background'
|
|
364
|
+
? backgroundLaunchContent(result.task_ids, 'Started')
|
|
365
|
+
: formatTaskModeContent(result.results ?? []);
|
|
366
|
+
const details = compactResultDetails(result as any);
|
|
367
|
+
return failedTasks.length ? { ...fail(`${failedTasks.length} subagent task(s) failed or were cancelled.\n\n${failedTasks.map(formatTask).join('\n\n')}`), details } : ok(text, details);
|
|
368
|
+
} catch (e) { return fail(e); }
|
|
369
|
+
finally {
|
|
370
|
+
active = false;
|
|
371
|
+
if (interval) clearInterval(interval);
|
|
372
|
+
uninstallCancel();
|
|
373
|
+
uninstallBackground();
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
renderCall(args: any, theme: any) {
|
|
377
|
+
const agents = args.agents?.length ? args.agents.join(', ') : args.agent ?? 'subagent';
|
|
378
|
+
const mode = args.mode ?? 'task';
|
|
379
|
+
const uiMode = readSubagentsConfig(process.cwd()).mode;
|
|
380
|
+
const detailsHint = uiMode === 'claude' ? '(/subagents for details)' : '(ctrl+, or /subagents for details)';
|
|
381
|
+
const text = `${theme.fg?.('toolTitle', theme.bold?.('subagent ') ?? 'subagent ') ?? 'subagent '}${theme.fg?.('accent', agents) ?? agents}${theme.fg?.('dim', ` (${mode})`) ?? ` (${mode})`} ${theme.fg?.('dim', detailsHint) ?? detailsHint}`;
|
|
382
|
+
return textComponent(text);
|
|
383
|
+
},
|
|
384
|
+
renderResult(result: any, { expanded, isPartial }: any, theme: any) {
|
|
385
|
+
const task = taskFromDetails(result);
|
|
386
|
+
if (isPartial) {
|
|
387
|
+
const frame = result?.details?.frame ?? 0;
|
|
388
|
+
const raw = task
|
|
389
|
+
? progressText([task], frame, { backgroundable: Boolean(result?.details?.backgroundable), backgroundShortcut: result?.details?.backgroundShortcut })
|
|
390
|
+
: progressText([], frame, { backgroundable: Boolean(result?.details?.backgroundable), backgroundShortcut: result?.details?.backgroundShortcut });
|
|
391
|
+
const lines = raw.split('\n');
|
|
392
|
+
const styled = lines.map((line: string, index: number) => (
|
|
393
|
+
index === 0
|
|
394
|
+
? (theme.fg?.('warning', line) ?? line)
|
|
395
|
+
: (theme.fg?.('dim', line) ?? line)
|
|
396
|
+
)).filter(Boolean).join('\n');
|
|
397
|
+
return textComponent(styled);
|
|
398
|
+
}
|
|
399
|
+
const failed = result?.isError || task?.status === 'failed' || task?.status === 'cancelled';
|
|
400
|
+
const status = failed ? (theme.fg?.('error', task?.status ?? 'failed') ?? (task?.status ?? 'failed')) : (theme.fg?.('success', task?.status ?? 'done') ?? (task?.status ?? 'done'));
|
|
401
|
+
const usage = task ? formatUsage(task) : '';
|
|
402
|
+
const summary = task
|
|
403
|
+
? [
|
|
404
|
+
`agent: ${theme.fg?.('accent', task.agent) ?? task.agent} · status: ${status} · effort: ${theme.fg?.('accent', task.effort ?? 'default/current') ?? (task.effort ?? 'default/current')}`,
|
|
405
|
+
`${theme.fg?.('dim', `model: ${task.model ?? 'default/current'} · id: ${task.id}`) ?? `model: ${task.model ?? 'default/current'} · id: ${task.id}`}${usage ? `\n${theme.fg?.('dim', `usage: ${usage}`) ?? `usage: ${usage}`}` : ''}`,
|
|
406
|
+
].join('\n')
|
|
407
|
+
: status;
|
|
408
|
+
const hint = collapsedResultHint(task, failed);
|
|
409
|
+
const finalText = taskFinalText(task, result);
|
|
410
|
+
const body = expanded && finalText
|
|
411
|
+
? `${summary}\n${theme.fg?.('toolTitle', 'Subagent response') ?? 'Subagent response'}\n${finalText}`
|
|
412
|
+
: `${summary}\n${theme.fg?.('dim', hint) ?? hint}`;
|
|
413
|
+
return textComponent(body);
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
pi.registerTool({
|
|
418
|
+
name: 'subagent_status',
|
|
419
|
+
label: 'Subagent Status',
|
|
420
|
+
description: 'Get status for a delegated subagent task.',
|
|
421
|
+
parameters: Type.Object({ task_id: Type.String() }),
|
|
422
|
+
async execute(_id: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
|
423
|
+
try {
|
|
424
|
+
const task = manager.getTask(params.task_id, ctx?.cwd ?? process.cwd());
|
|
425
|
+
if (!task) throw new Error('Subagent task not found');
|
|
426
|
+
return ok(formatTask(task), { task: compactTaskForToolResult(task) });
|
|
427
|
+
} catch (e) { return fail(e); }
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
pi.registerTool({
|
|
432
|
+
name: 'subagent_result',
|
|
433
|
+
label: 'Subagent Result',
|
|
434
|
+
description: 'Read result for a delegated subagent task.',
|
|
435
|
+
parameters: Type.Object({ task_id: Type.String() }),
|
|
436
|
+
async execute(_id: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
|
437
|
+
try {
|
|
438
|
+
const task = manager.getTask(params.task_id, ctx?.cwd ?? process.cwd());
|
|
439
|
+
if (!task) throw new Error('Subagent task not found');
|
|
440
|
+
const fullResult = task.result ?? task.error ?? task.output_preview ?? formatTask(task);
|
|
441
|
+
return ok(fullResult, { task: compactTaskForToolResult(task), full_result: fullResult });
|
|
442
|
+
} catch (e) { return fail(e); }
|
|
443
|
+
},
|
|
444
|
+
renderResult(result: any, { expanded }: any, theme: any) {
|
|
445
|
+
const task = taskFromDetails(result);
|
|
446
|
+
const failed = result?.isError || task?.status === 'failed' || task?.status === 'cancelled';
|
|
447
|
+
if (!task) return textComponent(theme.fg?.(failed ? 'error' : 'dim', result?.content?.[0]?.text ?? '') ?? (result?.content?.[0]?.text ?? ''));
|
|
448
|
+
const status = failed ? (theme.fg?.('error', task.status) ?? task.status) : (theme.fg?.('success', task.status) ?? task.status);
|
|
449
|
+
const usage = formatUsage(task);
|
|
450
|
+
const summary = [
|
|
451
|
+
`Subagent result: ${theme.fg?.('accent', task.agent) ?? task.agent} · status: ${status} · id: ${task.id}`,
|
|
452
|
+
theme.fg?.('dim', modelEffortLine(task)) ?? modelEffortLine(task),
|
|
453
|
+
usage ? (theme.fg?.('dim', `usage: ${usage}`) ?? `usage: ${usage}`) : undefined,
|
|
454
|
+
].filter(Boolean).join('\n');
|
|
455
|
+
const finalText = taskFinalText(task, result);
|
|
456
|
+
const body = expanded && finalText
|
|
457
|
+
? `${summary}\n${theme.fg?.('toolTitle', 'Subagent response') ?? 'Subagent response'}\n${finalText}`
|
|
458
|
+
: `${summary}\n${theme.fg?.('dim', collapsedResultHint(task, failed)) ?? collapsedResultHint(task, failed)}`;
|
|
459
|
+
return textComponent(body);
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
pi.registerTool({
|
|
464
|
+
name: 'subagent_list_tasks',
|
|
465
|
+
label: 'Subagent List Tasks',
|
|
466
|
+
description: 'List delegated subagent tasks.',
|
|
467
|
+
parameters: Type.Object({}),
|
|
468
|
+
async execute(_id: string, _params: any, _signal: any, _onUpdate: any, ctx: any) {
|
|
469
|
+
try {
|
|
470
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
471
|
+
const tasks = manager.listSessionTasks(cwd, sessionIdFromToolContext(ctx));
|
|
472
|
+
const compactTasks = tasks.map(compactTaskWithoutFinalText);
|
|
473
|
+
return ok(formatTaskListSummary(compactTasks), { tasks: compactTasks });
|
|
474
|
+
} catch (e) { return fail(e); }
|
|
475
|
+
},
|
|
476
|
+
renderResult(result: any, { expanded }: any, theme: any) {
|
|
477
|
+
const tasks = Array.isArray(result?.details?.tasks) ? result.details.tasks : [];
|
|
478
|
+
const text = formatTaskListRender(tasks, Boolean(expanded));
|
|
479
|
+
return textComponent(expanded ? text : (theme.fg?.('dim', text) ?? text));
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
pi.registerTool({
|
|
484
|
+
name: 'subagent_cancel',
|
|
485
|
+
label: 'Subagent Cancel',
|
|
486
|
+
description: 'Cancel a running delegated subagent task.',
|
|
487
|
+
parameters: Type.Object({ task_id: Type.String() }),
|
|
488
|
+
async execute(_id: string, params: any) {
|
|
489
|
+
try { const task = manager.cancel(params.task_id); return ok(formatTask(task), { task: compactTaskForToolResult(task) }); } catch (e) { return fail(e); }
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|