@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.
package/README.md CHANGED
@@ -73,18 +73,22 @@ You: /skill:03-work docs/plans/plan.md
73
73
  |-------|-------------|-----------|
74
74
  | **01-brainstorm** | Structured multi-round discovery, domain vocabulary persistence | `brainstorm_dialog` |
75
75
  | **02-plan** | TDD-gated implementation units, optional CEO Review | `plan_diff` |
76
- | **03-work** | Inline-first execution, bounded subagents, checkpoint resume, strict TDD, stop-the-line | `ce_subagent`, `ce_parallel_subagent` |
76
+ | **03-work** | Inline-first execution, checkpoint resume, strict TDD, stop-the-line | `session_checkpoint`, `task_splitter` |
77
77
  | **04-review** | Auto-assigned reviewers, five-axis findings, autofix loop | `review_router` |
78
78
  | **05-learn** | Pattern extraction → searchable solution artifacts | `pattern_extractor` |
79
79
  | **06-next** | Next-step recommendation + workflow status | `workflow_state` |
80
80
  | **07-worktree** | Isolated git worktree development | `worktree_manager` |
81
81
  | **08-help** | Phase 1 skill explainer | — |
82
82
 
83
- ### Subagent tool namespace
83
+ ### Optional: External subagent delegation
84
84
 
85
- Super Pi ships CE-specific tools named `ce_subagent` and `ce_parallel_subagent`. They are intentionally namespaced so they can coexist with the compatible third-party `pi-subagents` extension without tool-name collisions.
85
+ For generic child agent delegation (background jobs, parallel audits, scout/worker/oracle agents), install the community extension `pi-subagents` separately. Super Pi does not require it.
86
86
 
87
- These tools are helpers, not the core workflow. Use inline execution by default. Use CE subagents only for bounded, non-interactive, easily verifiable leaf tasks. Do not use them to invoke pipeline-stage skills (`01-brainstorm` through `05-learn`); run those stages directly with `/skill:<stage>`.
87
+ ```bash
88
+ pi install npm:pi-subagents
89
+ ```
90
+
91
+ Super Pi focuses on inline workflow execution. Use subagents only when you need true process isolation or background runs.
88
92
 
89
93
  ### Model & Thinking Routing
90
94
 
@@ -1,14 +1,12 @@
1
1
  import { readFile } from "node:fs/promises"
2
2
  import path from "node:path"
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
4
4
  import { Type } from "typebox"
5
5
  import { createArtifactHelperTool, type ArtifactType } from "./tools/artifact-helper"
6
6
  import { createAskUserQuestionTool } from "./tools/ask-user-question"
7
- import { createSubagentTool } from "./tools/subagent"
8
7
  import { createWorkflowStateTool } from "./tools/workflow-state"
9
8
  import { createWorktreeManagerTool } from "./tools/worktree-manager"
10
9
  import { createReviewRouterTool } from "./tools/review-router"
11
- import { createParallelSubagentTool } from "./tools/parallel-subagent"
12
10
  import { createSessionCheckpointTool } from "./tools/session-checkpoint"
13
11
  import { createTaskSplitterTool } from "./tools/task-splitter"
14
12
  import { createBrainstormDialogTool } from "./tools/brainstorm-dialog"
@@ -19,11 +17,6 @@ import { createContextHandoffTool } from "./tools/context-handoff"
19
17
  import { filterBashOutput } from "./tools/bash-output-filter"
20
18
  import { filterReadOutput } from "./tools/read-output-filter"
21
19
  import { COMPACTION_FOCUS_INSTRUCTIONS } from "./tools/compaction-optimizer"
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"
27
20
 
28
21
  const PIPELINE_STAGE_KEYS = new Set([
29
22
  "01-brainstorm",
@@ -116,34 +109,6 @@ function parseModelRef(
116
109
  }
117
110
  }
118
111
 
119
- /**
120
- * Create a spawn-based subagent runner that uses pi --mode json
121
- * with per-process env (no global process.env mutation).
122
- */
123
- function createSubagentRunner(
124
- pi: ExtensionAPI,
125
- signal?: AbortSignal,
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 as any).cwd ?? process.cwd(),
134
- extraFlags: options?.extraFlags,
135
- extraEnv: options?.extraEnv,
136
- signal,
137
- onUpdate: options?.onUpdate,
138
- }
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}`)
142
- }
143
- return result
144
- }
145
- }
146
-
147
112
  const artifactHelperParams = Type.Object({
148
113
  repoRoot: Type.String({ description: "Repository root where workflow artifacts should be created" }),
149
114
  artifactType: Type.Union([
@@ -166,18 +131,6 @@ const askUserQuestionParams = Type.Object({
166
131
  allowCustom: Type.Optional(Type.Boolean({ description: "Allow a custom answer when options are present" })),
167
132
  })
168
133
 
169
- const subagentTaskSchema = Type.Object({
170
- agent: Type.String({ description: "Skill name to invoke via /skill:<name>" }),
171
- task: Type.String({ description: "Task text passed to the subagent" }),
172
- })
173
-
174
- const subagentParams = Type.Object({
175
- agent: Type.Optional(Type.String({ description: "Single subagent skill name" })),
176
- task: Type.Optional(Type.String({ description: "Single subagent task" })),
177
- chain: Type.Optional(Type.Array(subagentTaskSchema, { description: "Serial subagent chain with optional {previous} placeholder" })),
178
- inheritSkills: Type.Optional(Type.Boolean({ description: "Whether the subagent should inherit skills. Default: true" })),
179
- })
180
-
181
134
  const workflowStateParams = Type.Object({
182
135
  repoRoot: Type.String({ description: "Repository root to scan for workflow artifacts" }),
183
136
  })
@@ -200,19 +153,6 @@ const reviewRouterParams = Type.Object({
200
153
  deletions: Type.Number({ description: "Number of lines removed" }),
201
154
  })
202
155
 
203
- const parallelSubagentTaskSchema = Type.Object({
204
- agent: Type.String({ description: "Skill name to invoke via /skill:<name>" }),
205
- task: Type.String({ description: "Task text passed to the subagent" }),
206
- })
207
-
208
- const parallelSubagentParams = Type.Object({
209
- tasks: Type.Union([
210
- Type.Array(parallelSubagentTaskSchema),
211
- Type.String({ description: "JSON stringified array of tasks" }),
212
- ], { description: "Array of independent tasks to run concurrently (can be a JSON string)" }),
213
- inheritSkills: Type.Optional(Type.Boolean({ description: "Whether subagents should inherit skills. Default: false" })),
214
- })
215
-
216
156
  const sessionCheckpointParams = Type.Object({
217
157
  operation: Type.Union([
218
158
  Type.Literal("save"),
@@ -346,9 +286,18 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
346
286
  return { action: "continue" as const }
347
287
  }
348
288
 
289
+ // Skip model/thinking switching during streaming steers — these are
290
+ // mid-stream interrupts, not new pipeline invocations.
291
+ if (event.streamingBehavior === "steer") {
292
+ return { action: "continue" as const }
293
+ }
294
+
349
295
  const settings = await readSettings(ctx.cwd)
350
296
  const modelStrategy = settings?.modelStrategy
351
297
  const thinkingStrategy = settings?.thinkingStrategy
298
+ // Notification guard: only notify in interactive (TUI) or RPC modes.
299
+ // For interactive input capability checks (askUserQuestion), use ctx.hasUI directly.
300
+ const shouldNotify = ctx.mode === "tui" || ctx.mode === "rpc"
352
301
 
353
302
  // Model switching
354
303
  if (modelStrategy) {
@@ -362,19 +311,19 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
362
311
  if (model) {
363
312
  const switched = await pi.setModel(model)
364
313
  if (switched) {
365
- if (ctx.hasUI) {
314
+ if (shouldNotify) {
366
315
  ctx.ui.notify(`Switched model for ${stageKey}: ${model.provider}/${model.id}`, "info")
367
316
  }
368
317
  } else {
369
- if (ctx.hasUI) {
318
+ if (shouldNotify) {
370
319
  ctx.ui.notify(`No API key for ${stageKey}: ${model.provider}/${model.id}`, "warning")
371
320
  }
372
321
  }
373
- } else if (ctx.hasUI) {
322
+ } else if (shouldNotify) {
374
323
  ctx.ui.notify(`Model not found for ${stageKey}: ${targetModelRef}`, "warning")
375
324
  }
376
325
  }
377
- } else if (ctx.hasUI) {
326
+ } else if (shouldNotify) {
378
327
  ctx.ui.notify(`Invalid modelStrategy for ${stageKey}: ${targetModelRef}`, "warning")
379
328
  }
380
329
  }
@@ -399,7 +348,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
399
348
  const currentLevel = pi.getThinkingLevel()
400
349
  if (currentLevel !== normalized) {
401
350
  pi.setThinkingLevel(normalized)
402
- if (ctx.hasUI) {
351
+ if (shouldNotify) {
403
352
  ctx.ui.notify(`Switched thinking level for ${stageKey}: ${normalized}`, "info")
404
353
  }
405
354
  }
@@ -411,11 +360,9 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
411
360
 
412
361
  const artifactHelper = createArtifactHelperTool()
413
362
  const askUserQuestion = createAskUserQuestionTool()
414
- const subagent = createSubagentTool()
415
363
  const workflowState = createWorkflowStateTool()
416
364
  const worktreeManager = createWorktreeManagerTool()
417
365
  const reviewRouter = createReviewRouterTool()
418
- const parallelSubagent = createParallelSubagentTool()
419
366
  const sessionCheckpoint = createSessionCheckpointTool()
420
367
  const taskSplitter = createTaskSplitterTool()
421
368
  const brainstormDialog = createBrainstormDialogTool()
@@ -489,45 +436,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
489
436
  },
490
437
  })
491
438
 
492
- pi.registerTool({
493
- name: subagent.name,
494
- label: "CE Subagent",
495
- description: "Run a single CE skill-based subagent or a serial chain in an isolated Pi process.",
496
- parameters: subagentParams,
497
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
498
- const result = await subagent.execute(
499
- {
500
- agent: params.agent,
501
- task: params.task,
502
- chain: params.chain,
503
- inheritSkills: params.inheritSkills,
504
- },
505
- createSubagentRunner(pi, signal),
506
- { onUpdate: onUpdate ? (details) => onUpdate({ content: [], details }) : undefined },
507
- )
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
-
514
- return {
515
- content: [{ type: "text", text: finalText || "(no output)" }],
516
- details: result,
517
- isError: isError || undefined,
518
- }
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
- },
529
- })
530
-
531
439
  pi.registerTool({
532
440
  name: workflowState.name,
533
441
  label: "Workflow State",
@@ -599,73 +507,6 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
599
507
  },
600
508
  })
601
509
 
602
- pi.registerTool({
603
- name: parallelSubagent.name,
604
- label: "CE Parallel Subagent",
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.",
606
- parameters: parallelSubagentParams,
607
- async execute(_toolCallId, params, signal, onUpdate) {
608
- let tasks: any[]
609
- if (typeof params.tasks === "string") {
610
- try {
611
- const cleaned = params.tasks.replace(/^```json\s*|```$/g, "").trim()
612
- tasks = JSON.parse(cleaned)
613
- } catch (e) {
614
- throw new Error(`Failed to parse tasks string as JSON: ${e instanceof Error ? e.message : String(e)}`)
615
- }
616
- } else {
617
- tasks = params.tasks
618
- }
619
-
620
- const result = await parallelSubagent.execute(
621
- {
622
- tasks,
623
- inheritSkills: params.inheritSkills,
624
- },
625
- createSubagentRunner(pi, signal),
626
- { onUpdate: onUpdate ? (details) => onUpdate({ content: [], details }) : undefined },
627
- )
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
-
648
- return {
649
- content: [{
650
- type: "text",
651
- text: `Parallel: ${successCount}/${result.results.length} succeeded${failCount > 0 ? `, ${failCount} failed` : ""}\n${summaries.join("\n")}`,
652
- }],
653
- details: result,
654
- }
655
- },
656
- renderCall(args, theme, _context) {
657
- return renderSubagentCall({
658
- tasks: typeof args.tasks === "string" ? [] : args.tasks,
659
- }, theme)
660
- },
661
- renderResult(result, renderContext, theme, _context) {
662
- // Handle partial updates where data is at top level (from onUpdate)
663
- // rather than nested in .details (from final result return value)
664
- const data = (result.details || result) as ParallelSubagentLiveDetails
665
- return renderSubagentResult(data, renderContext, theme)
666
- },
667
- })
668
-
669
510
  pi.registerTool({
670
511
  name: sessionCheckpoint.name,
671
512
  label: "Session Checkpoint",
@@ -909,31 +750,9 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
909
750
 
910
751
  export { createArtifactHelperTool } from "./tools/artifact-helper"
911
752
  export { createAskUserQuestionTool } from "./tools/ask-user-question"
912
- export { createSubagentTool } from "./tools/subagent"
913
- export { checkSubagentDepth, getChildDepthEnv, DEFAULT_MAX_SUBAGENT_DEPTH } from "./tools/subagent-depth-guard"
914
- export { AsyncMutex } from "./tools/async-mutex"
915
- export { createJsonRunner } from "./tools/subagent-json-runner"
916
- export {
917
- parseJsonEvent,
918
- applyEventToResult,
919
- type SingleResult as SingleResultType,
920
- type UsageStats,
921
- type ParsedEvent,
922
- isFailedResult as isFailedSingleResult,
923
- formatUsageStats,
924
- makeInitialResult,
925
- getFinalOutput as getFinalOutputFromMessages,
926
- getDisplayItems,
927
- } from "./tools/subagent-events"
928
- export {
929
- formatToolCall,
930
- renderSubagentCall,
931
- renderSubagentResult,
932
- } from "./tools/subagent-renderer"
933
753
  export { createWorkflowStateTool } from "./tools/workflow-state"
934
754
  export { createWorktreeManagerTool } from "./tools/worktree-manager"
935
755
  export { createReviewRouterTool } from "./tools/review-router"
936
- export { createParallelSubagentTool } from "./tools/parallel-subagent"
937
756
  export { createSessionCheckpointTool } from "./tools/session-checkpoint"
938
757
  export { createTaskSplitterTool } from "./tools/task-splitter"
939
758
  export { createBrainstormDialogTool } from "./tools/brainstorm-dialog"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leing2021/super-pi",
3
- "version": "0.23.12",
3
+ "version": "0.24.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Pi-native Compound Engineering package for iterative development workflows",
@@ -35,11 +35,11 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "peerDependencies": {
38
- "@mariozechner/pi-coding-agent": ">=0.69.0",
38
+ "@earendil-works/pi-coding-agent": ">=0.74.0",
39
39
  "typebox": ">=1.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@mariozechner/pi-coding-agent": "0.69.0",
42
+ "@earendil-works/pi-coding-agent": "^0.76.0",
43
43
  "bun-types": "^1.3.12"
44
44
  },
45
45
  "pi": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: 03-work
3
- description: "Execute plan units with parallel subagents, TDD enforcement, and checkpoint resume. Use when a plan path is ready for implementation."
3
+ description: "Execute plan units with TDD enforcement and checkpoint resume. Use when a plan path is ready for implementation."
4
4
  ---
5
5
 
6
6
  # Work
@@ -19,16 +19,14 @@ See [shared pipeline instructions](../references/pipeline-config.md) for model r
19
19
  2. **Priority:** project-level `{repo-root}/rules/` overrides package defaults
20
20
  3. **Distinguish input:** plan path vs bare prompt
21
21
  4. Derive tasks from plan **implementation units**
22
- 5. **Execution mode:**
23
- - **Inline mode** is the default execution path
24
- - **`ce_parallel_subagent`** only for bounded, non-interactive, easily verifiable leaf tasks
25
- - **`ce_subagent`** only for rare dependent serial chains of leaf tasks
26
- - Never use either tool to invoke pipeline-stage skills (`01-brainstorm through 05-learn`). Run those stages directly with `/skill:<stage>`.
22
+ 5. **Execution mode:** **inline mode** — all plan units execute inline in the current session. No built-in subagent tools.
27
23
  6. Use **`session_checkpoint`** to track progress and enable resume
28
24
  7. Use **`task_splitter`** to analyze dependencies before execution
29
25
  8. If in **worktree** (via `07-worktree`), execute inside it
30
26
  9. End by recommending `04-review`
31
27
 
28
+ > **Advanced:** If you need external child agent delegation (background runs, parallel audits), install `pi-subagents` separately. Super Pi does not require it.
29
+
32
30
  ## Hard gates — TDD enforcement
33
31
 
34
32
  Every step follows **RED → GREEN → REFACTOR**:
@@ -76,7 +74,7 @@ If the same tool, command, or implementation unit fails 3 consecutive times, sto
76
74
  3. Read implementation units if plan path
77
75
  4. Load `session_checkpoint` to skip completed units
78
76
  5. Use `task_splitter` for dependency analysis
79
- 6. Execute: **inline mode** by default; use subagents only for bounded, non-interactive, easily verifiable leaf tasks
77
+ 6. Execute: **inline mode** all units run in the current session
80
78
  7. Follow TDD per unit: RED → minimal code → GREEN → refactor → unit-level **verification**
81
79
  8. **Source-driven gate:** Before implementing framework/library-specific code, verify the API or pattern against official documentation. Flag unverified patterns as UNVERIFIED in output.
82
80
  9. Record progress via `references/progress-update-format.md`
@@ -41,10 +41,10 @@ Use `07-worktree` for isolated feature development on a separate branch.
41
41
  ### 03-work
42
42
  **Use when:** plan is ready or task is tightly scoped.
43
43
 
44
- **Execution modes:**
45
- - Inline execution for small units
46
- - `ce_parallel_subagent` for independent CE skill units
47
- - `ce_subagent` only for dependent serial chains
44
+ **Execution mode:**
45
+ - Inline execution for all units in the current session
46
+
47
+ **Advanced:** For external child agent delegation, install `pi-subagents` separately.
48
48
 
49
49
  **Key outputs:**
50
50
  - Completion report with verification evidence
@@ -1,28 +0,0 @@
1
- /**
2
- * Async mutex for serializing critical sections that mutate shared state
3
- * (e.g. process.env) across concurrent async operations.
4
- */
5
- export class AsyncMutex {
6
- private queue: (() => void)[] = []
7
- private locked = false
8
-
9
- async acquire(): Promise<() => void> {
10
- return new Promise<() => void>((resolve) => {
11
- const tryAcquire = () => {
12
- if (!this.locked) {
13
- this.locked = true
14
- resolve(() => this.release())
15
- } else {
16
- this.queue.push(tryAcquire)
17
- }
18
- }
19
- tryAcquire()
20
- })
21
- }
22
-
23
- private release(): void {
24
- this.locked = false
25
- const next = this.queue.shift()
26
- if (next) next()
27
- }
28
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * CE parallel subagent tool — runs multiple CE skill-based subagent tasks concurrently.
3
- *
4
- * Features:
5
- * - Recursion depth guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_MAX_DEPTH)
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
10
- */
11
-
12
- import {
13
- checkSubagentDepth,
14
- getChildDepthEnv,
15
- } from "./subagent-depth-guard"
16
- import {
17
- type SingleResult,
18
- makeInitialResult,
19
- isFailedResult,
20
- getFinalOutput,
21
- makeFailedResult,
22
- invokeRunner,
23
- type AnyRunner,
24
- } from "./subagent-events"
25
- import {
26
- assertNonPipelineStageSkill,
27
- type SubagentLiveRunner,
28
- type SubagentLiveExecOptions,
29
- type SubagentRunner,
30
- } from "./subagent"
31
-
32
- // ---------------------------------------------------------------------------
33
- // Constants (from pi official example)
34
- // ---------------------------------------------------------------------------
35
-
36
- const MAX_PARALLEL_TASKS = 8
37
- const MAX_CONCURRENCY = 4
38
-
39
- // ---------------------------------------------------------------------------
40
- // mapWithConcurrencyLimit (directly from pi official subagent example)
41
- // ---------------------------------------------------------------------------
42
-
43
- async function mapWithConcurrencyLimit<TIn, TOut>(
44
- items: TIn[],
45
- concurrency: number,
46
- fn: (item: TIn, index: number) => Promise<TOut>,
47
- ): Promise<TOut[]> {
48
- if (items.length === 0) return []
49
- const limit = Math.max(1, Math.min(concurrency, items.length))
50
- const results: TOut[] = new Array(items.length)
51
- let nextIndex = 0
52
- const workers = new Array(limit).fill(null).map(async () => {
53
- while (true) {
54
- const current = nextIndex++
55
- if (current >= items.length) return
56
- results[current] = await fn(items[current], current)
57
- }
58
- })
59
- await Promise.all(workers)
60
- return results
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // Public types
65
- // ---------------------------------------------------------------------------
66
-
67
- export interface ParallelSubagentTask {
68
- agent: string
69
- task: string
70
- }
71
-
72
- export interface ParallelSubagentInput {
73
- tasks: ParallelSubagentTask[]
74
- /** Whether subagents should inherit skills. Default: false for parallel */
75
- inheritSkills?: boolean
76
- }
77
-
78
- export interface ParallelSubagentLiveDetails {
79
- mode: "parallel"
80
- results: SingleResult[]
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Tool factory
85
- // ---------------------------------------------------------------------------
86
-
87
- export function createParallelSubagentTool() {
88
- return {
89
- name: "ce_parallel_subagent" as const,
90
-
91
- async execute(
92
- input: ParallelSubagentInput,
93
- runner: SubagentLiveRunner | SubagentRunner,
94
- toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
95
- ): Promise<ParallelSubagentLiveDetails> {
96
- // Recursion depth guard
97
- const depthCheck = checkSubagentDepth()
98
- if (!depthCheck.allowed) {
99
- throw new Error(depthCheck.reason!)
100
- }
101
-
102
- if (!input.tasks || input.tasks.length === 0) {
103
- throw new Error("ce_parallel_subagent requires at least one task")
104
- }
105
-
106
- if (input.tasks.length > MAX_PARALLEL_TASKS) {
107
- throw new Error(
108
- `Too many parallel tasks (${input.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
109
- )
110
- }
111
-
112
- for (const task of input.tasks) {
113
- assertNonPipelineStageSkill(task.agent, "ce_parallel_subagent")
114
- }
115
-
116
- const execOptions = buildParallelExecOptions(input.inheritSkills)
117
-
118
- // Initialize all results as running placeholders
119
- const allResults: SingleResult[] = input.tasks.map(({ agent, task }) =>
120
- makeInitialResult(agent, task),
121
- )
122
-
123
- // Emit initial state (all running)
124
- emitParallelUpdate(allResults, toolCtx)
125
-
126
- // Run tasks with concurrency limit
127
- await mapWithConcurrencyLimit(input.tasks, MAX_CONCURRENCY, async (t, index) => {
128
- const liveOptions: SubagentLiveExecOptions = {
129
- ...execOptions,
130
- onUpdate: (partial: SingleResult) => {
131
- allResults[index] = partial
132
- emitParallelUpdate(allResults, toolCtx)
133
- },
134
- }
135
-
136
- try {
137
- const result = await invokeRunner(runner, `/skill:${t.agent} ${t.task}`, liveOptions)
138
- allResults[index] = result
139
- } catch (e) {
140
- // Preserve allSettled semantics: one failure does not cancel others
141
- allResults[index] = makeFailedResult(t.agent, t.task, e instanceof Error ? e.message : String(e))
142
- }
143
- emitParallelUpdate(allResults, toolCtx)
144
- return allResults[index]
145
- })
146
-
147
- return { mode: "parallel", results: allResults }
148
- },
149
- }
150
- }
151
-
152
- // ---------------------------------------------------------------------------
153
- // Helpers
154
- // ---------------------------------------------------------------------------
155
-
156
- function emitParallelUpdate(
157
- results: SingleResult[],
158
- toolCtx?: { onUpdate?: (details: ParallelSubagentLiveDetails) => void },
159
- ) {
160
- if (toolCtx?.onUpdate) {
161
- toolCtx.onUpdate({ mode: "parallel", results: [...results] })
162
- }
163
- }
164
-
165
- function buildParallelExecOptions(inheritSkills?: boolean): { extraFlags?: string[]; extraEnv: Record<string, string> } {
166
- const flags: string[] = []
167
- const env: Record<string, string> = {}
168
-
169
- if (inheritSkills !== true) {
170
- flags.push("--no-skills")
171
- }
172
-
173
- Object.assign(env, getChildDepthEnv())
174
-
175
- return {
176
- extraFlags: flags.length > 0 ? flags : undefined,
177
- extraEnv: env,
178
- }
179
- }
180
-
181
- // ---------------------------------------------------------------------------
182
- // Legacy backward-compat types and exports
183
- // ---------------------------------------------------------------------------
184
-
185
- /** @deprecated Use ParallelSubagentLiveDetails instead */
186
- export interface ParallelResultItem {
187
- status: "fulfilled" | "rejected"
188
- value?: string
189
- reason?: string
190
- }
191
-
192
- /** @deprecated Use ParallelSubagentLiveDetails instead */
193
- export interface ParallelSubagentResult {
194
- mode: "parallel"
195
- outputs: ParallelResultItem[]
196
- }