@leing2021/super-pi 0.23.9 → 0.23.10
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 +77 -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 +453 -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,26 @@ 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
|
+
return renderSubagentResult(result.details as SubagentLiveDetails, renderContext, theme)
|
|
525
|
+
},
|
|
524
526
|
})
|
|
525
527
|
|
|
526
528
|
pi.registerTool({
|
|
@@ -599,7 +601,7 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
599
601
|
label: "CE Parallel Subagent",
|
|
600
602
|
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
603
|
parameters: parallelSubagentParams,
|
|
602
|
-
async execute(_toolCallId, params, signal) {
|
|
604
|
+
async execute(_toolCallId, params, signal, onUpdate) {
|
|
603
605
|
let tasks: any[]
|
|
604
606
|
if (typeof params.tasks === "string") {
|
|
605
607
|
try {
|
|
@@ -618,13 +620,32 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
618
620
|
inheritSkills: params.inheritSkills,
|
|
619
621
|
},
|
|
620
622
|
createSubagentRunner(pi, signal),
|
|
623
|
+
{ onUpdate },
|
|
621
624
|
)
|
|
622
625
|
|
|
626
|
+
// Build summary content
|
|
627
|
+
const successCount = result.results.filter(r => r.exitCode === 0).length
|
|
628
|
+
const failCount = result.results.filter(r => isFailedResult(r)).length
|
|
629
|
+
const summaries = result.results.map(r => {
|
|
630
|
+
const output = getFinalOutput(r.messages) || r.errorMessage || r.stderr || "(no output)"
|
|
631
|
+
const status = isFailedResult(r) ? "failed" : "completed"
|
|
632
|
+
return `### [${r.agent}] ${status}\n\n${output}`
|
|
633
|
+
})
|
|
634
|
+
|
|
623
635
|
return {
|
|
624
|
-
content: [{
|
|
636
|
+
content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `Parallel: ${successCount}/${result.results.length} succeeded${failCount > 0 ? `, ${failCount} failed` : ""}\n\n${summaries.join("\n\n---\n\n")}`,
|
|
639
|
+
}],
|
|
625
640
|
details: result,
|
|
626
641
|
}
|
|
627
642
|
},
|
|
643
|
+
renderCall(args, theme, _context) {
|
|
644
|
+
return renderSubagentCall(args, theme)
|
|
645
|
+
},
|
|
646
|
+
renderResult(result, renderContext, theme, _context) {
|
|
647
|
+
return renderSubagentResult(result.details as SubagentLiveDetails, renderContext, theme)
|
|
648
|
+
},
|
|
628
649
|
})
|
|
629
650
|
|
|
630
651
|
pi.registerTool({
|
|
@@ -873,6 +894,24 @@ export { createAskUserQuestionTool } from "./tools/ask-user-question"
|
|
|
873
894
|
export { createSubagentTool } from "./tools/subagent"
|
|
874
895
|
export { checkSubagentDepth, getChildDepthEnv, DEFAULT_MAX_SUBAGENT_DEPTH } from "./tools/subagent-depth-guard"
|
|
875
896
|
export { AsyncMutex } from "./tools/async-mutex"
|
|
897
|
+
export { createJsonRunner } from "./tools/subagent-json-runner"
|
|
898
|
+
export {
|
|
899
|
+
parseJsonEvent,
|
|
900
|
+
applyEventToResult,
|
|
901
|
+
type SingleResult as SingleResultType,
|
|
902
|
+
type UsageStats,
|
|
903
|
+
type ParsedEvent,
|
|
904
|
+
isFailedResult as isFailedSingleResult,
|
|
905
|
+
formatUsageStats,
|
|
906
|
+
makeInitialResult,
|
|
907
|
+
getFinalOutput as getFinalOutputFromMessages,
|
|
908
|
+
getDisplayItems,
|
|
909
|
+
} from "./tools/subagent-events"
|
|
910
|
+
export {
|
|
911
|
+
formatToolCall,
|
|
912
|
+
renderSubagentCall,
|
|
913
|
+
renderSubagentResult,
|
|
914
|
+
} from "./tools/subagent-renderer"
|
|
876
915
|
export { createWorkflowStateTool } from "./tools/workflow-state"
|
|
877
916
|
export { createWorktreeManagerTool } from "./tools/worktree-manager"
|
|
878
917
|
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
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent event model and JSON event parser.
|
|
3
|
+
*
|
|
4
|
+
* Provides the data types and parsing logic for pi --mode json event streams,
|
|
5
|
+
* used by both ce_subagent and ce_parallel_subagent for live TUI updates.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from pi official subagent example patterns.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface UsageStats {
|
|
15
|
+
input: number
|
|
16
|
+
output: number
|
|
17
|
+
cacheRead: number
|
|
18
|
+
cacheWrite: number
|
|
19
|
+
cost: number
|
|
20
|
+
contextTokens: number
|
|
21
|
+
turns: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SingleResult {
|
|
25
|
+
agent: string
|
|
26
|
+
task: string
|
|
27
|
+
exitCode: number
|
|
28
|
+
messages: unknown[]
|
|
29
|
+
stderr: string
|
|
30
|
+
usage: UsageStats
|
|
31
|
+
model?: string
|
|
32
|
+
stopReason?: string
|
|
33
|
+
errorMessage?: string
|
|
34
|
+
step?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type DisplayItem =
|
|
38
|
+
| { type: "text"; text: string }
|
|
39
|
+
| { type: "toolCall"; name: string; args: Record<string, unknown> }
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Parsed event types
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export interface MessageEndEvent {
|
|
46
|
+
type: "message_end"
|
|
47
|
+
message: unknown
|
|
48
|
+
usage?: {
|
|
49
|
+
input?: number
|
|
50
|
+
output?: number
|
|
51
|
+
cacheRead?: number
|
|
52
|
+
cacheWrite?: number
|
|
53
|
+
cost?: { total?: number }
|
|
54
|
+
totalTokens?: number
|
|
55
|
+
}
|
|
56
|
+
stopReason?: string
|
|
57
|
+
model?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ToolResultEndEvent {
|
|
61
|
+
type: "tool_result_end"
|
|
62
|
+
message: unknown
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ParsedEvent = MessageEndEvent | ToolResultEndEvent
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Result helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
export function makeInitialResult(agent: string, task: string, step?: number): SingleResult {
|
|
72
|
+
return {
|
|
73
|
+
agent,
|
|
74
|
+
task,
|
|
75
|
+
exitCode: -1,
|
|
76
|
+
messages: [],
|
|
77
|
+
stderr: "",
|
|
78
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
79
|
+
step,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isFailedResult(result: SingleResult): boolean {
|
|
84
|
+
return (
|
|
85
|
+
(result.exitCode !== -1 && result.exitCode !== 0) ||
|
|
86
|
+
result.stopReason === "error" ||
|
|
87
|
+
result.stopReason === "aborted"
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getFinalOutput(messages: unknown[]): string {
|
|
92
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
93
|
+
const msg = messages[i] as { role?: string; content?: unknown[] } | undefined
|
|
94
|
+
if (!msg || msg.role !== "assistant") continue
|
|
95
|
+
const parts = msg.content
|
|
96
|
+
if (!Array.isArray(parts)) continue
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
const p = part as { type?: string; text?: string } | undefined
|
|
99
|
+
if (p?.type === "text" && typeof p.text === "string") return p.text
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return ""
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getDisplayItems(messages: unknown[]): DisplayItem[] {
|
|
106
|
+
const items: DisplayItem[] = []
|
|
107
|
+
for (const msg of messages) {
|
|
108
|
+
const m = msg as { role?: string; content?: unknown[] } | undefined
|
|
109
|
+
if (!m || m.role !== "assistant") continue
|
|
110
|
+
const parts = m.content
|
|
111
|
+
if (!Array.isArray(parts)) continue
|
|
112
|
+
for (const part of parts) {
|
|
113
|
+
const p = part as { type?: string; text?: string; name?: string; arguments?: unknown } | undefined
|
|
114
|
+
if (!p) continue
|
|
115
|
+
if (p.type === "text" && typeof p.text === "string") {
|
|
116
|
+
items.push({ type: "text", text: p.text })
|
|
117
|
+
} else if (p.type === "toolCall" && typeof p.name === "string") {
|
|
118
|
+
items.push({ type: "toolCall", name: p.name, args: (p.arguments ?? {}) as Record<string, unknown> })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return items
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// JSON event parser
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export function parseJsonEvent(line: string): ParsedEvent | null {
|
|
130
|
+
const trimmed = line.trim()
|
|
131
|
+
if (!trimmed) return null
|
|
132
|
+
|
|
133
|
+
let raw: unknown
|
|
134
|
+
try {
|
|
135
|
+
raw = JSON.parse(trimmed)
|
|
136
|
+
} catch {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof raw !== "object" || raw === null) return null
|
|
141
|
+
const obj = raw as Record<string, unknown>
|
|
142
|
+
|
|
143
|
+
if (obj.type === "message_end" && obj.message) {
|
|
144
|
+
// pi JSON mode puts usage/stopReason/model inside message object
|
|
145
|
+
const msgObj = typeof obj.message === "object" && obj.message !== null
|
|
146
|
+
? obj.message as Record<string, unknown>
|
|
147
|
+
: undefined
|
|
148
|
+
const rawUsage = obj.usage ?? msgObj?.usage
|
|
149
|
+
const rawStopReason = typeof obj.stopReason === "string"
|
|
150
|
+
? obj.stopReason
|
|
151
|
+
: typeof msgObj?.stopReason === "string" ? (msgObj.stopReason as string) : undefined
|
|
152
|
+
const rawModel = typeof obj.model === "string"
|
|
153
|
+
? obj.model
|
|
154
|
+
: typeof msgObj?.model === "string" ? (msgObj.model as string) : undefined
|
|
155
|
+
return {
|
|
156
|
+
type: "message_end",
|
|
157
|
+
message: obj.message,
|
|
158
|
+
usage: typeof rawUsage === "object" && rawUsage !== null
|
|
159
|
+
? rawUsage as MessageEndEvent["usage"]
|
|
160
|
+
: undefined,
|
|
161
|
+
stopReason: rawStopReason,
|
|
162
|
+
model: rawModel,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (obj.type === "tool_result_end" && obj.message) {
|
|
167
|
+
return {
|
|
168
|
+
type: "tool_result_end",
|
|
169
|
+
message: obj.message,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Event application
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
export function applyEventToResult(result: SingleResult, event: ParsedEvent): void {
|
|
181
|
+
if (event.type === "message_end") {
|
|
182
|
+
result.messages.push(event.message)
|
|
183
|
+
const msg = event.message as { role?: string; errorMessage?: string } | undefined
|
|
184
|
+
if (msg?.role === "assistant") {
|
|
185
|
+
result.usage.turns++
|
|
186
|
+
if (event.usage) {
|
|
187
|
+
result.usage.input += event.usage.input ?? 0
|
|
188
|
+
result.usage.output += event.usage.output ?? 0
|
|
189
|
+
result.usage.cacheRead += event.usage.cacheRead ?? 0
|
|
190
|
+
result.usage.cacheWrite += event.usage.cacheWrite ?? 0
|
|
191
|
+
result.usage.cost += event.usage.cost?.total ?? 0
|
|
192
|
+
result.usage.contextTokens = event.usage.totalTokens ?? result.usage.contextTokens
|
|
193
|
+
}
|
|
194
|
+
if (event.stopReason) result.stopReason = event.stopReason
|
|
195
|
+
if (event.model && !result.model) result.model = event.model
|
|
196
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage
|
|
197
|
+
}
|
|
198
|
+
} else if (event.type === "tool_result_end") {
|
|
199
|
+
result.messages.push(event.message)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Formatting helpers
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function formatTokens(count: number): string {
|
|
208
|
+
if (count < 1000) return count.toString()
|
|
209
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`
|
|
210
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`
|
|
211
|
+
return `${(count / 1000000).toFixed(1)}M`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function formatUsageStats(
|
|
215
|
+
usage: UsageStats,
|
|
216
|
+
model?: string,
|
|
217
|
+
): string {
|
|
218
|
+
const parts: string[] = []
|
|
219
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`)
|
|
220
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`)
|
|
221
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`)
|
|
222
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`)
|
|
223
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`)
|
|
224
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`)
|
|
225
|
+
if (usage.contextTokens && usage.contextTokens > 0) {
|
|
226
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`)
|
|
227
|
+
}
|
|
228
|
+
if (model) parts.push(model)
|
|
229
|
+
return parts.join(" ")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Shared result helpers (used by subagent.ts and parallel-subagent.ts)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
export function isSingleResult(val: unknown): val is SingleResult {
|
|
237
|
+
return (
|
|
238
|
+
typeof val === "object" &&
|
|
239
|
+
val !== null &&
|
|
240
|
+
"agent" in val &&
|
|
241
|
+
"exitCode" in val &&
|
|
242
|
+
"messages" in val &&
|
|
243
|
+
"usage" in val
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function makeFailedResult(agent: string, task: string, error: string): SingleResult {
|
|
248
|
+
return {
|
|
249
|
+
agent,
|
|
250
|
+
task,
|
|
251
|
+
exitCode: 1,
|
|
252
|
+
messages: [],
|
|
253
|
+
stderr: error,
|
|
254
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
255
|
+
errorMessage: error,
|
|
256
|
+
stopReason: "error",
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type AnyRunner =
|
|
261
|
+
| ((prompt: string, options?: any) => Promise<SingleResult>)
|
|
262
|
+
| ((prompt: string, options?: any) => Promise<string>)
|
|
263
|
+
|
|
264
|
+
export async function invokeRunner(
|
|
265
|
+
runner: AnyRunner,
|
|
266
|
+
prompt: string,
|
|
267
|
+
options: any,
|
|
268
|
+
): Promise<SingleResult> {
|
|
269
|
+
const result = await runner(prompt, options)
|
|
270
|
+
|
|
271
|
+
if (typeof result === "string") {
|
|
272
|
+
return {
|
|
273
|
+
agent: "",
|
|
274
|
+
task: "",
|
|
275
|
+
exitCode: 0,
|
|
276
|
+
messages: [{ role: "assistant", content: [{ type: "text", text: result }] }],
|
|
277
|
+
stderr: "",
|
|
278
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (isSingleResult(result)) {
|
|
283
|
+
return result
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result as unknown as SingleResult
|
|
287
|
+
}
|