@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.
@@ -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 { AsyncMutex } from "./tools/async-mutex"
23
- import type { SubagentExecOptions, SubagentRunner } from "./tools/subagent"
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 handles env injection with mutex protection
119
- * for concurrency safety when multiple parallel subagents share the process.
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
- ): SubagentRunner {
125
- return async (prompt: string, options?: SubagentExecOptions) => {
126
- const args = ["--no-session", ...(options?.extraFlags ?? []), "-p", prompt]
127
- const release = await _subagentEnvMutex.acquire()
128
- const savedEnv: Record<string, string | undefined> = {}
129
- const extraEnv = options?.extraEnv ?? {}
130
- for (const [key, value] of Object.entries(extraEnv)) {
131
- savedEnv[key] = process.env[key]
132
- process.env[key] = value
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
- try {
135
- const execResult = await pi.exec("pi", args, {
136
- signal,
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, _onUpdate, ctx) {
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: result.outputs[result.outputs.length - 1] ?? "" }],
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: [{ type: "text", text: JSON.stringify(result, null, 2) }],
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 { assertNonPipelineStageSkill, type SubagentExecOptions, type SubagentRunner } from "./subagent"
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 ParallelResultItem {
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
- outputs: ParallelResultItem[]
78
+ results: SingleResult[]
35
79
  }
36
80
 
37
- function buildPrompt(agent: string, task: string): string {
38
- return `/skill:${agent} ${task}`
39
- }
81
+ // ---------------------------------------------------------------------------
82
+ // Tool factory
83
+ // ---------------------------------------------------------------------------
40
84
 
41
85
  export function createParallelSubagentTool() {
42
86
  return {
43
- name: "ce_parallel_subagent",
44
- async execute(input: ParallelSubagentInput, runner: SubagentRunner): Promise<ParallelSubagentResult> {
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
- // Default: parallel workers get a slim context (no inherited skills)
60
- const inheritSkills = input.inheritSkills ?? false
61
- const execOptions = buildParallelExecOptions(inheritSkills)
114
+ const execOptions = buildParallelExecOptions(input.inheritSkills)
62
115
 
63
- const promises = input.tasks.map(({ agent, task }) =>
64
- runner(buildPrompt(agent, task), execOptions),
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
- const settled = await Promise.allSettled(promises)
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
- const outputs: ParallelResultItem[] = settled.map((result) => {
70
- if (result.status === "fulfilled") {
71
- return { status: "fulfilled", value: result.value }
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
- return { status: "rejected", reason: result.reason?.message || String(result.reason) }
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
- * Build exec options for parallel subagent workers.
86
- * Defaults to slim context (no inherited skills).
87
- */
88
- function buildParallelExecOptions(inheritSkills: boolean): SubagentExecOptions {
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
- // Context slimming
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
+ }