@leing2021/super-pi 0.23.12 → 0.24.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.
@@ -1,478 +0,0 @@
1
- /**
2
- * TUI renderers for ce_subagent and ce_parallel_subagent.
3
- *
4
- * Displays real-time status in pi tool frames:
5
- * - Collapsed: status icon + agent name + recent tool calls + usage
6
- * - Expanded (Ctrl+O): full tool calls + final output + per-task usage
7
- *
8
- * Tool call formatting (formatToolCall) adapted directly from pi official
9
- * subagent example with super-pi skill semantic adaptations.
10
- */
11
-
12
- import * as os from "node:os"
13
- import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"
14
- import {
15
- type SingleResult,
16
- getFinalOutput,
17
- getDisplayItems,
18
- isFailedResult,
19
- formatUsageStats,
20
- type DisplayItem,
21
- } from "./subagent-events"
22
- import type { SubagentLiveDetails } from "./subagent"
23
-
24
- // ---------------------------------------------------------------------------
25
- // Constants
26
- // ---------------------------------------------------------------------------
27
-
28
- const COLLAPSED_ITEM_COUNT = 10
29
-
30
- // ---------------------------------------------------------------------------
31
- // Tool call formatting (from pi official subagent example)
32
- // ---------------------------------------------------------------------------
33
-
34
- export function formatToolCall(
35
- toolName: string,
36
- args: Record<string, unknown>,
37
- themeFg: (color: any, text: string) => string,
38
- ): string {
39
- const shortenPath = (p: string) => {
40
- const home = os.homedir()
41
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p
42
- }
43
-
44
- switch (toolName) {
45
- case "bash": {
46
- const command = (args.command as string) || "..."
47
- const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command
48
- return themeFg("muted", "$ ") + themeFg("toolOutput", preview)
49
- }
50
- case "read": {
51
- const rawPath = (args.file_path || args.path || "...") as string
52
- const filePath = shortenPath(rawPath)
53
- const offset = args.offset as number | undefined
54
- const limit = args.limit as number | undefined
55
- let text = themeFg("accent", filePath)
56
- if (offset !== undefined || limit !== undefined) {
57
- const startLine = offset ?? 1
58
- const endLine = limit !== undefined ? startLine + limit - 1 : ""
59
- text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`)
60
- }
61
- return themeFg("muted", "read ") + text
62
- }
63
- case "write": {
64
- const rawPath = (args.file_path || args.path || "...") as string
65
- const filePath = shortenPath(rawPath)
66
- const content = (args.content || "") as string
67
- const lines = content.split("\n").length
68
- let text = themeFg("muted", "write ") + themeFg("accent", filePath)
69
- if (lines > 1) text += themeFg("dim", ` (${lines} lines)`)
70
- return text
71
- }
72
- case "edit": {
73
- const rawPath = (args.file_path || args.path || "...") as string
74
- return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath))
75
- }
76
- case "ls": {
77
- const rawPath = (args.path || ".") as string
78
- return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath))
79
- }
80
- case "find": {
81
- const pattern = (args.pattern || "*") as string
82
- const rawPath = (args.path || ".") as string
83
- return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`)
84
- }
85
- case "grep": {
86
- const pattern = (args.pattern || "") as string
87
- const rawPath = (args.path || ".") as string
88
- return (
89
- themeFg("muted", "grep ") +
90
- themeFg("accent", `/${pattern}/`) +
91
- themeFg("dim", ` in ${shortenPath(rawPath)}`)
92
- )
93
- }
94
- default: {
95
- const argsStr = JSON.stringify(args)
96
- const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr
97
- return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`)
98
- }
99
- }
100
- }
101
-
102
- // ---------------------------------------------------------------------------
103
- // Theme type
104
- // ---------------------------------------------------------------------------
105
-
106
- interface Theme {
107
- fg: (color: any, text: string) => string
108
- bold: (text: string) => string
109
- }
110
-
111
- // ---------------------------------------------------------------------------
112
- // renderCall
113
- // ---------------------------------------------------------------------------
114
-
115
- export function renderSubagentCall(
116
- args: {
117
- agent?: string
118
- task?: string
119
- chain?: Array<{ agent: string; task: string }>
120
- tasks?: Array<{ agent: string; task: string }>
121
- },
122
- theme: Theme,
123
- ): any {
124
- if (args.chain && args.chain.length > 0) {
125
- let text =
126
- theme.fg("toolTitle", theme.bold("ce_subagent ")) +
127
- theme.fg("accent", `chain (${args.chain.length} steps)`)
128
- for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
129
- const step = args.chain[i]
130
- const cleanTask = step.task.replace(/\{previous\}/g, "").trim()
131
- const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask
132
- text +=
133
- "\n " +
134
- theme.fg("muted", `${i + 1}.`) +
135
- " " +
136
- theme.fg("accent", step.agent) +
137
- theme.fg("dim", ` ${preview}`)
138
- }
139
- if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`
140
- return new Text(text, 0, 0)
141
- }
142
-
143
- if (args.tasks && args.tasks.length > 0) {
144
- let text =
145
- theme.fg("toolTitle", theme.bold("⬇ parallel ")) +
146
- theme.fg("accent", `${args.tasks.length} agents launching...`)
147
- for (let i = 0; i < args.tasks.length; i++) {
148
- const t = args.tasks[i]
149
- const num = theme.fg("muted", `${i + 1}.`)
150
- const preview = t.task.length > 50 ? `${t.task.slice(0, 50)}...` : t.task
151
- text += `\n ${num} ${theme.fg("accent", t.agent)} ${theme.fg("dim", preview)}`
152
- }
153
- return new Text(text, 0, 0)
154
- }
155
-
156
- // Single mode
157
- const agentName = args.agent || "..."
158
- const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "..."
159
- let text =
160
- theme.fg("toolTitle", theme.bold("ce_subagent ")) +
161
- theme.fg("accent", agentName)
162
- text += `\n ${theme.fg("dim", preview)}`
163
- return new Text(text, 0, 0)
164
- }
165
-
166
- // ---------------------------------------------------------------------------
167
- // renderResult
168
- // ---------------------------------------------------------------------------
169
-
170
- interface RenderContext {
171
- expanded: boolean
172
- }
173
-
174
- export function renderSubagentResult(
175
- details: SubagentLiveDetails,
176
- context: RenderContext,
177
- theme: Theme,
178
- ): any {
179
- if (!details || !details.results || details.results.length === 0) {
180
- return new Text("(no output)", 0, 0)
181
- }
182
-
183
- const mdTheme = getMarkdownTheme()
184
-
185
- const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
186
- const toShow = limit ? items.slice(-limit) : items
187
- const skipped = limit && items.length > limit ? items.length - limit : 0
188
- let text = ""
189
- if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`)
190
- for (const item of toShow) {
191
- if (item.type === "text") {
192
- const preview = context.expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n")
193
- text += `${theme.fg("toolOutput", preview)}\n`
194
- } else {
195
- text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`
196
- }
197
- }
198
- return text.trimEnd()
199
- }
200
-
201
- // --- Single mode ---
202
- if (details.mode === "single" && details.results.length === 1) {
203
- return renderSingleResult(details.results[0], context, theme, mdTheme, renderDisplayItems)
204
- }
205
-
206
- // --- Chain mode ---
207
- if (details.mode === "chain") {
208
- return renderChainResult(details.results, context, theme, mdTheme, renderDisplayItems)
209
- }
210
-
211
- // --- Parallel mode ---
212
- if (details.mode === "parallel") {
213
- return renderParallelResult(details.results, context, theme, mdTheme, renderDisplayItems)
214
- }
215
-
216
- return new Text("(no output)", 0, 0)
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // Single result renderer
221
- // ---------------------------------------------------------------------------
222
-
223
- function renderSingleResult(
224
- r: SingleResult,
225
- context: RenderContext,
226
- theme: Theme,
227
- mdTheme: any,
228
- renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
229
- ): any {
230
- const isError = isFailedResult(r)
231
- const icon = isError ? theme.fg("error", "✗") : r.exitCode === -1 ? theme.fg("warning", "⏳") : theme.fg("success", "✓")
232
- const displayItems = getDisplayItems(r.messages)
233
- const finalOutput = getFinalOutput(r.messages)
234
-
235
- if (context.expanded) {
236
- const container = new Container()
237
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}`
238
- if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`
239
- container.addChild(new Text(header, 0, 0))
240
- if (isError && r.errorMessage)
241
- container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0))
242
- container.addChild(new Spacer(1))
243
- container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0))
244
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0))
245
- container.addChild(new Spacer(1))
246
- container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0))
247
- if (displayItems.length === 0 && !finalOutput) {
248
- container.addChild(new Text(theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)"), 0, 0))
249
- } else {
250
- for (const item of displayItems) {
251
- if (item.type === "toolCall")
252
- container.addChild(
253
- new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0),
254
- )
255
- }
256
- if (finalOutput) {
257
- container.addChild(new Spacer(1))
258
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
259
- }
260
- }
261
- const usageStr = formatUsageStats(r.usage, r.model)
262
- if (usageStr) {
263
- container.addChild(new Spacer(1))
264
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0))
265
- }
266
- return container
267
- }
268
-
269
- // Collapsed
270
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}`
271
- if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`
272
- if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`
273
- else if (r.exitCode === -1) text += `\n${theme.fg("muted", "(running...)")}`
274
- else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`
275
- else {
276
- text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`
277
- if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
278
- }
279
- const usageStr = formatUsageStats(r.usage, r.model)
280
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`
281
- return new Text(text, 0, 0)
282
- }
283
-
284
- // ---------------------------------------------------------------------------
285
- // Chain result renderer
286
- // ---------------------------------------------------------------------------
287
-
288
- function aggregateUsage(results: SingleResult[]) {
289
- const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 }
290
- for (const r of results) {
291
- total.input += r.usage.input
292
- total.output += r.usage.output
293
- total.cacheRead += r.usage.cacheRead
294
- total.cacheWrite += r.usage.cacheWrite
295
- total.cost += r.usage.cost
296
- total.contextTokens += r.usage.contextTokens
297
- total.turns += r.usage.turns
298
- }
299
- return total
300
- }
301
-
302
- function renderChainResult(
303
- results: SingleResult[],
304
- context: RenderContext,
305
- theme: Theme,
306
- mdTheme: any,
307
- renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
308
- ): any {
309
- const successCount = results.filter(r => r.exitCode === 0).length
310
- const icon = successCount === results.length ? theme.fg("success", "✓") : theme.fg("error", "✗")
311
-
312
- if (context.expanded) {
313
- const container = new Container()
314
- container.addChild(
315
- new Text(
316
- `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${results.length} steps`)}`,
317
- 0, 0,
318
- ),
319
- )
320
- for (const r of results) {
321
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗")
322
- const displayItems = getDisplayItems(r.messages)
323
- const finalOutput = getFinalOutput(r.messages)
324
-
325
- container.addChild(new Spacer(1))
326
- container.addChild(new Text(`${theme.fg("muted", `─── Step ${r.step ?? "?"}: `)}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0))
327
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0))
328
- for (const item of displayItems) {
329
- if (item.type === "toolCall")
330
- container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
331
- }
332
- if (finalOutput) {
333
- container.addChild(new Spacer(1))
334
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
335
- }
336
- const stepUsage = formatUsageStats(r.usage, r.model)
337
- if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0))
338
- }
339
- const usageStr = formatUsageStats(aggregateUsage(results))
340
- if (usageStr) {
341
- container.addChild(new Spacer(1))
342
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0))
343
- }
344
- return container
345
- }
346
-
347
- // Collapsed
348
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${results.length} steps`)}`
349
- for (const r of results) {
350
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗")
351
- const displayItems = getDisplayItems(r.messages)
352
- text += `\n\n${theme.fg("muted", `─── Step ${r.step ?? "?"}: `)}${theme.fg("accent", r.agent)} ${rIcon}`
353
- if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`
354
- else text += `\n${renderDisplayItems(displayItems, 5)}`
355
- }
356
- const usageStr = formatUsageStats(aggregateUsage(results))
357
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`
358
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
359
- return new Text(text, 0, 0)
360
- }
361
-
362
- // ---------------------------------------------------------------------------
363
- // Parallel result renderer
364
- // ---------------------------------------------------------------------------
365
-
366
- function renderParallelResult(
367
- results: SingleResult[],
368
- context: RenderContext,
369
- theme: Theme,
370
- mdTheme: any,
371
- renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
372
- ): any {
373
- const running = results.filter(r => r.exitCode === -1).length
374
- const successCount = results.filter(r => r.exitCode !== -1 && !isFailedResult(r)).length
375
- const failCount = results.filter(r => r.exitCode !== -1 && isFailedResult(r)).length
376
- const isRunning = running > 0
377
-
378
- if (isRunning) {
379
- // --- Live progress: compact status line ---
380
- const doneCount = successCount + failCount
381
- const icon = theme.fg("warning", "⏳")
382
- const bar = renderProgressBar(doneCount, results.length, theme)
383
- const doneLabel = failCount > 0
384
- ? theme.fg("success", `${doneCount}/${results.length}✓`) + " " + theme.fg("error", `${failCount}✗`)
385
- : theme.fg("success", `${doneCount}/${results.length}✓`)
386
- const runningLabel = theme.fg("dim", `, ${running} running...`)
387
- return new Text(`${icon} ${bar} ${doneLabel}${runningLabel}`, 0, 0)
388
- }
389
-
390
- // --- Completed: summary card layout ---
391
- const allSuccess = failCount === 0
392
- const headerIcon = allSuccess ? theme.fg("success", "✓") : theme.fg("warning", "◐")
393
- const headerText = failCount > 0
394
- ? `${successCount}/${results.length} succeeded, ${failCount} failed`
395
- : `${successCount}/${results.length} succeeded`
396
-
397
- if (context.expanded) {
398
- // Expanded: header + per-task details with tool calls and output
399
- const container = new Container()
400
- container.addChild(new Text(`${headerIcon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", headerText)}`, 0, 0))
401
- container.addChild(new Spacer(1))
402
-
403
- for (const r of results) {
404
- const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓")
405
- const finalOutput = getFinalOutput(r.messages)
406
- const summaryText = isFailedResult(r)
407
- ? (r.errorMessage || r.stderr || "unknown error")
408
- : (finalOutput ? summarizeText(finalOutput, 200) : "(no output)")
409
-
410
- container.addChild(new Text(`${rIcon} ${theme.fg("accent", r.agent)} — ${summaryText}`, 0, 0))
411
-
412
- if (!isFailedResult(r) && finalOutput) {
413
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
414
- }
415
-
416
- const taskUsage = formatUsageStats(r.usage, r.model)
417
- if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0))
418
- container.addChild(new Spacer(1))
419
- }
420
-
421
- const usageStr = formatUsageStats(aggregateUsage(results))
422
- if (usageStr) {
423
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0))
424
- }
425
- return container
426
- }
427
-
428
- // Collapsed: one-line summary per task
429
- let text = `${headerIcon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", headerText)}`
430
- for (const r of results) {
431
- const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓")
432
- const finalOutput = getFinalOutput(r.messages)
433
- const summaryText = isFailedResult(r)
434
- ? (r.errorMessage || r.stderr || "unknown error")
435
- : (finalOutput ? summarizeText(finalOutput, 120) : "(no output)")
436
- text += `\n ${rIcon} ${theme.fg("accent", r.agent)} — ${summaryText}`
437
- }
438
- const usageStr = formatUsageStats(aggregateUsage(results))
439
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`
440
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
441
- return new Text(text, 0, 0)
442
- }
443
-
444
- // ---------------------------------------------------------------------------
445
- // Progress bar & text helpers
446
- // ---------------------------------------------------------------------------
447
-
448
- function renderProgressBar(done: number, total: number, theme: Theme): string {
449
- const width = Math.min(total, 20)
450
- const filled = Math.round((done / total) * width)
451
- const empty = width - filled
452
- const bar = theme.fg("success", "█".repeat(filled)) + theme.fg("dim", "░".repeat(empty))
453
- return bar
454
- }
455
-
456
- function summarizeText(text: string, maxLen: number): string {
457
- // Take first meaningful paragraph or line
458
- const firstLine = text.split("\n").find(l => l.trim().length > 0) || ""
459
- // Strip markdown headers and bold for summary
460
- const cleaned = firstLine.replace(/^#+\s*/, "").replace(/\*\*/g, "").replace(/\[.*?\]\(.*?\)/g, "").trim()
461
- if (cleaned.length <= maxLen) return cleaned
462
- return cleaned.slice(0, maxLen - 1) + "…"
463
- }
464
-
465
- // ---------------------------------------------------------------------------
466
- // Markdown theme helper
467
- // ---------------------------------------------------------------------------
468
-
469
- function getMarkdownTheme(): any {
470
- // Return a simple theme for testing / default usage
471
- // In production, pi provides getMarkdownTheme()
472
- try {
473
- const { getMarkdownTheme: gmt } = require("@mariozechner/pi-coding-agent")
474
- return gmt()
475
- } catch {
476
- return undefined
477
- }
478
- }
@@ -1,209 +0,0 @@
1
- /**
2
- * CE subagent tool — runs a single CE skill-based subagent or a serial chain.
3
- *
4
- * Features:
5
- * - Recursion depth guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_MAX_DEPTH)
6
- * - Optional context slimming via --no-skills flag
7
- * - Live TUI updates via onUpdate callback + SingleResult details
8
- */
9
-
10
- import {
11
- checkSubagentDepth,
12
- getChildDepthEnv,
13
- } from "./subagent-depth-guard"
14
- import {
15
- type SingleResult,
16
- getFinalOutput,
17
- invokeRunner,
18
- } from "./subagent-events"
19
-
20
- // ---------------------------------------------------------------------------
21
- // Public types
22
- // ---------------------------------------------------------------------------
23
-
24
- export interface SubagentTask {
25
- agent: string
26
- task: string
27
- }
28
-
29
- export interface SubagentInput {
30
- agent?: string
31
- task?: string
32
- chain?: SubagentTask[]
33
- /** Whether the subagent should inherit skills. Default: true */
34
- inheritSkills?: boolean
35
- }
36
-
37
- export interface SubagentLiveDetails {
38
- mode: "single" | "chain" | "parallel"
39
- results: SingleResult[]
40
- }
41
-
42
- /** @deprecated Use SubagentLiveRunner instead */
43
- export type SubagentRunner = (
44
- prompt: string,
45
- options?: SubagentExecOptions,
46
- ) => Promise<string>
47
-
48
- export interface SubagentExecOptions {
49
- /** Extra CLI flags to pass to the child pi process */
50
- extraFlags?: string[]
51
- /** Environment variables to inject into the child process */
52
- extraEnv?: Record<string, string>
53
- /** Callback for live TUI partial updates */
54
- onUpdate?: (partial: SingleResult) => void
55
- }
56
-
57
- /**
58
- * Live runner that returns a structured SingleResult and supports
59
- * onUpdate for partial progress.
60
- */
61
- export type SubagentLiveRunner = (
62
- prompt: string,
63
- options?: SubagentLiveExecOptions,
64
- ) => Promise<SingleResult>
65
-
66
- export type SubagentLiveExecOptions = SubagentExecOptions
67
-
68
- export interface SubagentLiveResult {
69
- mode: "single" | "chain"
70
- results: SingleResult[]
71
- /** @deprecated Backward compat: final text output per step */
72
- outputs?: string[]
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // Pipeline stage guard
77
- // ---------------------------------------------------------------------------
78
-
79
- const PIPELINE_STAGE_SKILLS = new Set([
80
- "01-brainstorm",
81
- "02-plan",
82
- "03-work",
83
- "04-review",
84
- "05-learn",
85
- ])
86
-
87
- export function assertNonPipelineStageSkill(agent: string, toolName: string): void {
88
- if (PIPELINE_STAGE_SKILLS.has(agent)) {
89
- throw new Error(
90
- `Pipeline-stage skill "${agent}" cannot run through ${toolName}. ` +
91
- `Run it directly with /skill:${agent} instead.`,
92
- )
93
- }
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Tool factory
98
- // ---------------------------------------------------------------------------
99
-
100
- export function createSubagentTool() {
101
- return {
102
- name: "ce_subagent" as const,
103
-
104
- /**
105
- * Execute the subagent tool.
106
- *
107
- * Supports two runner signatures:
108
- * - SubagentLiveRunner (preferred): returns SingleResult, supports onUpdate
109
- * - SubagentRunner (legacy): returns string, no onUpdate
110
- */
111
- async execute(
112
- input: SubagentInput,
113
- runner: SubagentLiveRunner | SubagentRunner,
114
- toolCtx?: { onUpdate?: (details: SubagentLiveDetails) => void },
115
- ): Promise<SubagentLiveResult> {
116
- // Recursion depth guard
117
- const depthCheck = checkSubagentDepth()
118
- if (!depthCheck.allowed) {
119
- throw new Error(depthCheck.reason!)
120
- }
121
-
122
- const hasSingle = Boolean(input.agent && input.task)
123
- const hasChain = Boolean(input.chain && input.chain.length > 0)
124
-
125
- if (Number(hasSingle) + Number(hasChain) !== 1) {
126
- throw new Error("Provide exactly one mode: single or chain")
127
- }
128
-
129
- const execOptions = buildExecOptions(input.inheritSkills)
130
-
131
- if (hasSingle) {
132
- assertNonPipelineStageSkill(input.agent!, "ce_subagent")
133
- const prompt = buildPrompt(input.agent!, input.task!)
134
-
135
- const liveOptions: SubagentLiveExecOptions = {
136
- ...execOptions,
137
- onUpdate: (partial: SingleResult) => {
138
- if (toolCtx?.onUpdate) {
139
- toolCtx.onUpdate({ mode: "single", results: [partial] })
140
- }
141
- },
142
- }
143
-
144
- const result = await invokeRunner(runner, prompt, liveOptions)
145
- result.agent = input.agent!
146
- result.task = input.task!
147
- return {
148
- mode: "single",
149
- results: [result],
150
- outputs: [getFinalOutput(result.messages)],
151
- }
152
- }
153
-
154
- // Chain mode
155
- const results: SingleResult[] = []
156
- let previous = ""
157
-
158
- for (const task of input.chain ?? []) {
159
- assertNonPipelineStageSkill(task.agent, "ce_subagent")
160
- const prompt = buildPrompt(task.agent, task.task.replace(/\{previous\}/g, previous))
161
-
162
- const liveOptions: SubagentLiveExecOptions = {
163
- ...execOptions,
164
- onUpdate: (partial: SingleResult) => {
165
- if (toolCtx?.onUpdate) {
166
- toolCtx.onUpdate({ mode: "chain", results: [...results, partial] })
167
- }
168
- },
169
- }
170
-
171
- const result = await invokeRunner(runner, prompt, liveOptions)
172
- result.agent = task.agent
173
- result.task = task.task
174
- results.push(result)
175
- previous = getFinalOutput(result.messages)
176
- }
177
-
178
- return {
179
- mode: "chain",
180
- results,
181
- outputs: results.map(r => getFinalOutput(r.messages)),
182
- }
183
- },
184
- }
185
- }
186
-
187
- // ---------------------------------------------------------------------------
188
- // Helpers
189
- // ---------------------------------------------------------------------------
190
-
191
- function buildExecOptions(inheritSkills?: boolean): { extraFlags?: string[]; extraEnv: Record<string, string> } {
192
- const flags: string[] = []
193
- const env: Record<string, string> = {}
194
-
195
- if (inheritSkills === false) {
196
- flags.push("--no-skills")
197
- }
198
-
199
- Object.assign(env, getChildDepthEnv())
200
-
201
- return {
202
- extraFlags: flags.length > 0 ? flags : undefined,
203
- extraEnv: env,
204
- }
205
- }
206
-
207
- function buildPrompt(agent: string, task: string): string {
208
- return `/skill:${agent} ${task}`
209
- }