@leing2021/super-pi 0.23.8 → 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.
@@ -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
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Spawn-based JSON runner for ce_subagent / ce_parallel_subagent.
3
+ *
4
+ * Spawns `pi --mode json --no-session ... -p <prompt>`, parses stdout
5
+ * JSON events in real-time, and triggers an onUpdate callback for
6
+ * live TUI progress.
7
+ *
8
+ * Key design decisions:
9
+ * - Per-process env via spawn options, NO global process.env mutation.
10
+ * - Injectable spawnFactory for testing.
11
+ * - AbortSignal support for Ctrl+C cancellation.
12
+ */
13
+
14
+ import { type ChildProcess, spawn } from "node:child_process"
15
+ import {
16
+ type SingleResult,
17
+ parseJsonEvent,
18
+ applyEventToResult,
19
+ makeInitialResult,
20
+ } from "./subagent-events"
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface SpawnInput {
27
+ cwd?: string
28
+ env?: Record<string, string | undefined>
29
+ stdio?: any
30
+ shell?: boolean
31
+ }
32
+
33
+ export type SpawnFactory = (
34
+ command: string,
35
+ args: string[],
36
+ options: SpawnInput,
37
+ ) => ChildProcessLike
38
+
39
+ export interface ChildProcessLike {
40
+ stdout: { on(event: string, cb: (data: Buffer) => void): void }
41
+ stderr: { on(event: string, cb: (data: Buffer) => void): void }
42
+ on(event: string, cb: (code: number) => void): void
43
+ on(event: string, cb: () => void): void
44
+ kill(signal?: string): void
45
+ readonly killed: boolean
46
+ }
47
+
48
+ export interface JsonRunnerOptions {
49
+ /** Override spawn for testing. Defaults to real child_process.spawn */
50
+ spawnFactory?: SpawnFactory
51
+ }
52
+
53
+ export interface JsonRunConfig {
54
+ prompt: string
55
+ agent: string
56
+ task: string
57
+ cwd: string
58
+ step?: number
59
+ extraFlags?: string[]
60
+ extraEnv?: Record<string, string>
61
+ signal?: AbortSignal
62
+ onUpdate?: (partial: SingleResult) => void
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Default spawn factory
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const realSpawn: SpawnFactory = (
70
+ command: string,
71
+ args: string[],
72
+ options: SpawnInput,
73
+ ): ChildProcessLike => {
74
+ return spawn(command, args, {
75
+ cwd: options.cwd,
76
+ env: options.env as Record<string, string>,
77
+ stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
78
+ shell: options.shell ?? false,
79
+ }) as unknown as ChildProcessLike
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Runner
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export function createJsonRunner(opts: JsonRunnerOptions = {}) {
87
+ const doSpawn = opts.spawnFactory ?? realSpawn
88
+
89
+ async function run(config: JsonRunConfig): Promise<SingleResult> {
90
+ const result = makeInitialResult(config.agent, config.task, config.step)
91
+
92
+ const args = buildArgs(config.prompt, config.extraFlags)
93
+ const env = buildEnv(config.extraEnv)
94
+
95
+ const emitUpdate = () => {
96
+ config.onUpdate?.(result)
97
+ }
98
+
99
+ let wasAborted = false
100
+
101
+ const exitCode = await new Promise<number>((resolve) => {
102
+ const proc = doSpawn("pi", args, {
103
+ cwd: config.cwd,
104
+ env,
105
+ })
106
+
107
+ let buffer = ""
108
+
109
+ const processLine = (line: string) => {
110
+ const event = parseJsonEvent(line)
111
+ if (!event) return
112
+ applyEventToResult(result, event)
113
+ emitUpdate()
114
+ }
115
+
116
+ proc.stdout.on("data", (data: Buffer) => {
117
+ buffer += data.toString()
118
+ const lines = buffer.split("\n")
119
+ buffer = lines.pop() || ""
120
+ for (const line of lines) processLine(line)
121
+ })
122
+
123
+ proc.stderr.on("data", (data: Buffer) => {
124
+ result.stderr += data.toString()
125
+ })
126
+
127
+ proc.on("close", (code: number) => {
128
+ if (buffer.trim()) processLine(buffer)
129
+ resolve(code ?? 0)
130
+ })
131
+
132
+ proc.on("error", () => {
133
+ resolve(1)
134
+ })
135
+
136
+ if (config.signal) {
137
+ const killProc = () => {
138
+ wasAborted = true
139
+ proc.kill("SIGTERM")
140
+ setTimeout(() => {
141
+ if (!proc.killed) proc.kill("SIGKILL")
142
+ }, 5000)
143
+ }
144
+ if (config.signal.aborted) killProc()
145
+ else config.signal.addEventListener("abort", killProc, { once: true })
146
+ }
147
+ })
148
+
149
+ result.exitCode = exitCode
150
+ if (wasAborted) throw new Error("Subagent was aborted")
151
+ return result
152
+ }
153
+
154
+ return { run }
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Helpers
159
+ // ---------------------------------------------------------------------------
160
+
161
+ function buildArgs(prompt: string, extraFlags?: string[]): string[] {
162
+ const args: string[] = ["--mode", "json", "--no-session"]
163
+ if (extraFlags && extraFlags.length > 0) {
164
+ args.push(...extraFlags)
165
+ }
166
+ args.push("-p", prompt)
167
+ return args
168
+ }
169
+
170
+ function buildEnv(extraEnv?: Record<string, string>): Record<string, string | undefined> {
171
+ return {
172
+ ...process.env,
173
+ ...(extraEnv ?? {}),
174
+ }
175
+ }