@leing2021/super-pi 0.23.9 → 0.23.11
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/extensions/ce-core/index.ts +93 -38
- package/extensions/ce-core/tools/parallel-subagent.ts +126 -36
- package/extensions/ce-core/tools/subagent-events.ts +287 -0
- package/extensions/ce-core/tools/subagent-json-runner.ts +175 -0
- package/extensions/ce-core/tools/subagent-renderer.ts +478 -0
- package/extensions/ce-core/tools/subagent.ts +97 -21
- package/package.json +1 -1
|
@@ -19,10 +19,12 @@ import { createContextHandoffTool } from "./tools/context-handoff"
|
|
|
19
19
|
import { filterBashOutput } from "./tools/bash-output-filter"
|
|
20
20
|
import { filterReadOutput } from "./tools/read-output-filter"
|
|
21
21
|
import { COMPACTION_FOCUS_INSTRUCTIONS } from "./tools/compaction-optimizer"
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
22
|
+
import { createJsonRunner, type JsonRunConfig } from "./tools/subagent-json-runner"
|
|
23
|
+
import { getFinalOutput, isFailedResult, type SingleResult } from "./tools/subagent-events"
|
|
24
|
+
import { renderSubagentCall, renderSubagentResult } from "./tools/subagent-renderer"
|
|
25
|
+
import type { SubagentLiveRunner, SubagentLiveExecOptions, SubagentLiveDetails } from "./tools/subagent"
|
|
26
|
+
import type { ParallelSubagentLiveDetails } from "./tools/parallel-subagent"
|
|
24
27
|
|
|
25
|
-
const _subagentEnvMutex = new AsyncMutex()
|
|
26
28
|
const PIPELINE_STAGE_KEYS = new Set([
|
|
27
29
|
"01-brainstorm",
|
|
28
30
|
"02-plan",
|
|
@@ -115,43 +117,30 @@ function parseModelRef(
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
|
-
* Create a subagent runner that
|
|
119
|
-
*
|
|
120
|
+
* Create a spawn-based subagent runner that uses pi --mode json
|
|
121
|
+
* with per-process env (no global process.env mutation).
|
|
120
122
|
*/
|
|
121
123
|
function createSubagentRunner(
|
|
122
124
|
pi: ExtensionAPI,
|
|
123
125
|
signal?: AbortSignal,
|
|
124
|
-
):
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
): SubagentLiveRunner {
|
|
127
|
+
const jsonRunner = createJsonRunner()
|
|
128
|
+
return async (prompt: string, options?: SubagentLiveExecOptions) => {
|
|
129
|
+
const config: JsonRunConfig = {
|
|
130
|
+
prompt,
|
|
131
|
+
agent: "",
|
|
132
|
+
task: "",
|
|
133
|
+
cwd: pi.cwd ?? process.cwd(),
|
|
134
|
+
extraFlags: options?.extraFlags,
|
|
135
|
+
extraEnv: options?.extraEnv,
|
|
136
|
+
signal,
|
|
137
|
+
onUpdate: options?.onUpdate,
|
|
133
138
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
timeout: 10 * 60 * 1000,
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
if (execResult.code !== 0) {
|
|
141
|
-
throw new Error(execResult.stderr || execResult.stdout || `Subagent failed for prompt: ${prompt}`)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return (execResult.stdout || "").trim()
|
|
145
|
-
} finally {
|
|
146
|
-
for (const [key, oldValue] of Object.entries(savedEnv)) {
|
|
147
|
-
if (oldValue === undefined) {
|
|
148
|
-
delete process.env[key]
|
|
149
|
-
} else {
|
|
150
|
-
process.env[key] = oldValue
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
release()
|
|
139
|
+
const result = await jsonRunner.run(config)
|
|
140
|
+
if (isFailedResult(result) || result.exitCode !== 0) {
|
|
141
|
+
throw new Error(result.errorMessage || result.stderr || `Subagent failed for prompt: ${prompt}`)
|
|
154
142
|
}
|
|
143
|
+
return result
|
|
155
144
|
}
|
|
156
145
|
}
|
|
157
146
|
|
|
@@ -505,7 +494,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
505
494
|
label: "CE Subagent",
|
|
506
495
|
description: "Run a single CE skill-based subagent or a serial chain in an isolated Pi process.",
|
|
507
496
|
parameters: subagentParams,
|
|
508
|
-
async execute(_toolCallId, params, signal,
|
|
497
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
509
498
|
const result = await subagent.execute(
|
|
510
499
|
{
|
|
511
500
|
agent: params.agent,
|
|
@@ -514,13 +503,29 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
514
503
|
inheritSkills: params.inheritSkills,
|
|
515
504
|
},
|
|
516
505
|
createSubagentRunner(pi, signal),
|
|
506
|
+
{ onUpdate },
|
|
517
507
|
)
|
|
518
508
|
|
|
509
|
+
// Build final content from results
|
|
510
|
+
const lastResult = result.results[result.results.length - 1]
|
|
511
|
+
const finalText = lastResult ? getFinalOutput(lastResult.messages) : ""
|
|
512
|
+
const isError = lastResult ? isFailedResult(lastResult) : false
|
|
513
|
+
|
|
519
514
|
return {
|
|
520
|
-
content: [{ type: "text", text:
|
|
515
|
+
content: [{ type: "text", text: finalText || "(no output)" }],
|
|
521
516
|
details: result,
|
|
517
|
+
isError: isError || undefined,
|
|
522
518
|
}
|
|
523
519
|
},
|
|
520
|
+
renderCall(args, theme, _context) {
|
|
521
|
+
return renderSubagentCall(args, theme)
|
|
522
|
+
},
|
|
523
|
+
renderResult(result, renderContext, theme, _context) {
|
|
524
|
+
// Handle partial updates where data is at top level (from onUpdate)
|
|
525
|
+
// rather than nested in .details (from final result return value)
|
|
526
|
+
const data = (result.details || result) as SubagentLiveDetails
|
|
527
|
+
return renderSubagentResult(data, renderContext, theme)
|
|
528
|
+
},
|
|
524
529
|
})
|
|
525
530
|
|
|
526
531
|
pi.registerTool({
|
|
@@ -599,7 +604,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
599
604
|
label: "CE Parallel Subagent",
|
|
600
605
|
description: "Run multiple CE skill-based subagent tasks concurrently. IMPORTANT: Provide 'tasks' as a clean JSON array object. If the environment forces a string, provide a valid JSON array string.",
|
|
601
606
|
parameters: parallelSubagentParams,
|
|
602
|
-
async execute(_toolCallId, params, signal) {
|
|
607
|
+
async execute(_toolCallId, params, signal, onUpdate) {
|
|
603
608
|
let tasks: any[]
|
|
604
609
|
if (typeof params.tasks === "string") {
|
|
605
610
|
try {
|
|
@@ -618,13 +623,45 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
618
623
|
inheritSkills: params.inheritSkills,
|
|
619
624
|
},
|
|
620
625
|
createSubagentRunner(pi, signal),
|
|
626
|
+
{ onUpdate },
|
|
621
627
|
)
|
|
622
628
|
|
|
629
|
+
// Build compact summary content for LLM consumption
|
|
630
|
+
const successCount = result.results.filter(r => r.exitCode === 0).length
|
|
631
|
+
const failCount = result.results.filter(r => isFailedResult(r)).length
|
|
632
|
+
const summaries = result.results.map((r, i) => {
|
|
633
|
+
const icon = isFailedResult(r) ? "✗" : "✓"
|
|
634
|
+
const output = getFinalOutput(r.messages) || r.errorMessage || r.stderr || "(no output)"
|
|
635
|
+
// Compact: first non-empty line, stripped of markdown formatting
|
|
636
|
+
const summaryLine = output.split("\n").find((l: string) => l.trim().length > 0) || ""
|
|
637
|
+
const cleaned = summaryLine.replace(/^#+\s*/, "").replace(/\*\*/g, "").trim()
|
|
638
|
+
const oneLine = cleaned.length > 200 ? cleaned.slice(0, 199) + "…" : cleaned
|
|
639
|
+
return `${i + 1}. ${icon} ${r.agent} — ${oneLine}`
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
// Full outputs available in details for expanded view
|
|
643
|
+
const fullOutputs = result.results.map(r => {
|
|
644
|
+
const output = getFinalOutput(r.messages) || ""
|
|
645
|
+
return output
|
|
646
|
+
})
|
|
647
|
+
|
|
623
648
|
return {
|
|
624
|
-
content: [{
|
|
649
|
+
content: [{
|
|
650
|
+
type: "text",
|
|
651
|
+
text: `Parallel: ${successCount}/${result.results.length} succeeded${failCount > 0 ? `, ${failCount} failed` : ""}\n${summaries.join("\n")}`,
|
|
652
|
+
}],
|
|
625
653
|
details: result,
|
|
626
654
|
}
|
|
627
655
|
},
|
|
656
|
+
renderCall(args, theme, _context) {
|
|
657
|
+
return renderSubagentCall(args, theme)
|
|
658
|
+
},
|
|
659
|
+
renderResult(result, renderContext, theme, _context) {
|
|
660
|
+
// Handle partial updates where data is at top level (from onUpdate)
|
|
661
|
+
// rather than nested in .details (from final result return value)
|
|
662
|
+
const data = (result.details || result) as SubagentLiveDetails
|
|
663
|
+
return renderSubagentResult(data, renderContext, theme)
|
|
664
|
+
},
|
|
628
665
|
})
|
|
629
666
|
|
|
630
667
|
pi.registerTool({
|
|
@@ -873,6 +910,24 @@ export { createAskUserQuestionTool } from "./tools/ask-user-question"
|
|
|
873
910
|
export { createSubagentTool } from "./tools/subagent"
|
|
874
911
|
export { checkSubagentDepth, getChildDepthEnv, DEFAULT_MAX_SUBAGENT_DEPTH } from "./tools/subagent-depth-guard"
|
|
875
912
|
export { AsyncMutex } from "./tools/async-mutex"
|
|
913
|
+
export { createJsonRunner } from "./tools/subagent-json-runner"
|
|
914
|
+
export {
|
|
915
|
+
parseJsonEvent,
|
|
916
|
+
applyEventToResult,
|
|
917
|
+
type SingleResult as SingleResultType,
|
|
918
|
+
type UsageStats,
|
|
919
|
+
type ParsedEvent,
|
|
920
|
+
isFailedResult as isFailedSingleResult,
|
|
921
|
+
formatUsageStats,
|
|
922
|
+
makeInitialResult,
|
|
923
|
+
getFinalOutput as getFinalOutputFromMessages,
|
|
924
|
+
getDisplayItems,
|
|
925
|
+
} from "./tools/subagent-events"
|
|
926
|
+
export {
|
|
927
|
+
formatToolCall,
|
|
928
|
+
renderSubagentCall,
|
|
929
|
+
renderSubagentResult,
|
|
930
|
+
} from "./tools/subagent-renderer"
|
|
876
931
|
export { createWorkflowStateTool } from "./tools/workflow-state"
|
|
877
932
|
export { createWorktreeManagerTool } from "./tools/worktree-manager"
|
|
878
933
|
export { createReviewRouterTool } from "./tools/review-router"
|
|
@@ -4,13 +4,63 @@
|
|
|
4
4
|
* Features:
|
|
5
5
|
* - Recursion depth guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_MAX_DEPTH)
|
|
6
6
|
* - Optional context slimming via --no-skills flag
|
|
7
|
+
* - Concurrency limit via mapWithConcurrencyLimit (from pi official example)
|
|
8
|
+
* - Live TUI updates with running placeholders and per-task status
|
|
9
|
+
* - Promise.allSettled semantics: one failure does NOT cancel others
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import {
|
|
10
13
|
checkSubagentDepth,
|
|
11
14
|
getChildDepthEnv,
|
|
12
15
|
} from "./subagent-depth-guard"
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type SingleResult,
|
|
18
|
+
makeInitialResult,
|
|
19
|
+
isFailedResult,
|
|
20
|
+
getFinalOutput,
|
|
21
|
+
makeFailedResult,
|
|
22
|
+
invokeRunner,
|
|
23
|
+
} from "./subagent-events"
|
|
24
|
+
import {
|
|
25
|
+
assertNonPipelineStageSkill,
|
|
26
|
+
type SubagentLiveRunner,
|
|
27
|
+
type SubagentLiveExecOptions,
|
|
28
|
+
} from "./subagent"
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Constants (from pi official example)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const MAX_PARALLEL_TASKS = 8
|
|
35
|
+
const MAX_CONCURRENCY = 4
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// mapWithConcurrencyLimit (directly from pi official subagent example)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
42
|
+
items: TIn[],
|
|
43
|
+
concurrency: number,
|
|
44
|
+
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
45
|
+
): Promise<TOut[]> {
|
|
46
|
+
if (items.length === 0) return []
|
|
47
|
+
const limit = Math.max(1, Math.min(concurrency, items.length))
|
|
48
|
+
const results: TOut[] = new Array(items.length)
|
|
49
|
+
let nextIndex = 0
|
|
50
|
+
const workers = new Array(limit).fill(null).map(async () => {
|
|
51
|
+
while (true) {
|
|
52
|
+
const current = nextIndex++
|
|
53
|
+
if (current >= items.length) return
|
|
54
|
+
results[current] = await fn(items[current], current)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
await Promise.all(workers)
|
|
58
|
+
return results
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Public types
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
14
64
|
|
|
15
65
|
export interface ParallelSubagentTask {
|
|
16
66
|
agent: string
|
|
@@ -23,25 +73,24 @@ export interface ParallelSubagentInput {
|
|
|
23
73
|
inheritSkills?: boolean
|
|
24
74
|
}
|
|
25
75
|
|
|
26
|
-
export interface
|
|
27
|
-
status: "fulfilled" | "rejected"
|
|
28
|
-
value?: string
|
|
29
|
-
reason?: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ParallelSubagentResult {
|
|
76
|
+
export interface ParallelSubagentLiveDetails {
|
|
33
77
|
mode: "parallel"
|
|
34
|
-
|
|
78
|
+
results: SingleResult[]
|
|
35
79
|
}
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Tool factory
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
40
84
|
|
|
41
85
|
export function createParallelSubagentTool() {
|
|
42
86
|
return {
|
|
43
|
-
name: "ce_parallel_subagent",
|
|
44
|
-
|
|
87
|
+
name: "ce_parallel_subagent" as const,
|
|
88
|
+
|
|
89
|
+
async execute(
|
|
90
|
+
input: ParallelSubagentInput,
|
|
91
|
+
runner: SubagentLiveRunner,
|
|
92
|
+
toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
|
|
93
|
+
): Promise<ParallelSubagentLiveDetails> {
|
|
45
94
|
// Recursion depth guard
|
|
46
95
|
const depthCheck = checkSubagentDepth()
|
|
47
96
|
if (!depthCheck.allowed) {
|
|
@@ -52,49 +101,73 @@ export function createParallelSubagentTool() {
|
|
|
52
101
|
throw new Error("ce_parallel_subagent requires at least one task")
|
|
53
102
|
}
|
|
54
103
|
|
|
104
|
+
if (input.tasks.length > MAX_PARALLEL_TASKS) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Too many parallel tasks (${input.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
55
110
|
for (const task of input.tasks) {
|
|
56
111
|
assertNonPipelineStageSkill(task.agent, "ce_parallel_subagent")
|
|
57
112
|
}
|
|
58
113
|
|
|
59
|
-
|
|
60
|
-
const inheritSkills = input.inheritSkills ?? false
|
|
61
|
-
const execOptions = buildParallelExecOptions(inheritSkills)
|
|
114
|
+
const execOptions = buildParallelExecOptions(input.inheritSkills)
|
|
62
115
|
|
|
63
|
-
|
|
64
|
-
|
|
116
|
+
// Initialize all results as running placeholders
|
|
117
|
+
const allResults: SingleResult[] = input.tasks.map(({ agent, task }) =>
|
|
118
|
+
makeInitialResult(agent, task),
|
|
65
119
|
)
|
|
66
120
|
|
|
67
|
-
|
|
121
|
+
// Emit initial state (all running)
|
|
122
|
+
emitParallelUpdate(allResults, toolCtx)
|
|
123
|
+
|
|
124
|
+
// Run tasks with concurrency limit
|
|
125
|
+
await mapWithConcurrencyLimit(input.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
126
|
+
const liveOptions: SubagentLiveExecOptions = {
|
|
127
|
+
...execOptions,
|
|
128
|
+
onUpdate: (partial: SingleResult) => {
|
|
129
|
+
allResults[index] = partial
|
|
130
|
+
emitParallelUpdate(allResults, toolCtx)
|
|
131
|
+
},
|
|
132
|
+
}
|
|
68
133
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
134
|
+
try {
|
|
135
|
+
const result = await invokeRunner(runner, `/skill:${t.agent} ${t.task}`, liveOptions)
|
|
136
|
+
allResults[index] = result
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// Preserve allSettled semantics: one failure does not cancel others
|
|
139
|
+
allResults[index] = makeFailedResult(t.agent, t.task, e instanceof Error ? e.message : String(e))
|
|
72
140
|
}
|
|
73
|
-
|
|
141
|
+
emitParallelUpdate(allResults, toolCtx)
|
|
142
|
+
return allResults[index]
|
|
74
143
|
})
|
|
75
144
|
|
|
76
|
-
return {
|
|
77
|
-
mode: "parallel",
|
|
78
|
-
outputs,
|
|
79
|
-
}
|
|
145
|
+
return { mode: "parallel", results: allResults }
|
|
80
146
|
},
|
|
81
147
|
}
|
|
82
148
|
}
|
|
83
149
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Helpers
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function emitParallelUpdate(
|
|
155
|
+
results: SingleResult[],
|
|
156
|
+
toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
|
|
157
|
+
) {
|
|
158
|
+
if (toolCtx?.onUpdate) {
|
|
159
|
+
toolCtx.onUpdate({ mode: "parallel", results: [...results] })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildParallelExecOptions(inheritSkills?: boolean): { extraFlags?: string[]; extraEnv: Record<string, string> } {
|
|
89
164
|
const flags: string[] = []
|
|
90
165
|
const env: Record<string, string> = {}
|
|
91
166
|
|
|
92
|
-
|
|
93
|
-
if (!inheritSkills) {
|
|
167
|
+
if (inheritSkills !== true) {
|
|
94
168
|
flags.push("--no-skills")
|
|
95
169
|
}
|
|
96
170
|
|
|
97
|
-
// Recursion depth
|
|
98
171
|
Object.assign(env, getChildDepthEnv())
|
|
99
172
|
|
|
100
173
|
return {
|
|
@@ -102,3 +175,20 @@ function buildParallelExecOptions(inheritSkills: boolean): SubagentExecOptions {
|
|
|
102
175
|
extraEnv: env,
|
|
103
176
|
}
|
|
104
177
|
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Legacy backward-compat types and exports
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/** @deprecated Use ParallelSubagentLiveDetails instead */
|
|
184
|
+
export interface ParallelResultItem {
|
|
185
|
+
status: "fulfilled" | "rejected"
|
|
186
|
+
value?: string
|
|
187
|
+
reason?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @deprecated Use ParallelSubagentLiveDetails instead */
|
|
191
|
+
export interface ParallelSubagentResult {
|
|
192
|
+
mode: "parallel"
|
|
193
|
+
outputs: ParallelResultItem[]
|
|
194
|
+
}
|