@leing2021/super-pi 0.23.13 → 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 +8 -4
- package/extensions/ce-core/index.ts +15 -196
- package/package.json +3 -3
- package/skills/03-work/SKILL.md +5 -7
- package/skills/08-help/references/workflow-sequence.md +4 -4
- package/extensions/ce-core/tools/async-mutex.ts +0 -28
- package/extensions/ce-core/tools/parallel-subagent.ts +0 -196
- package/extensions/ce-core/tools/subagent-depth-guard.ts +0 -86
- package/extensions/ce-core/tools/subagent-events.ts +0 -287
- package/extensions/ce-core/tools/subagent-json-runner.ts +0 -175
- package/extensions/ce-core/tools/subagent-renderer.ts +0 -478
- package/extensions/ce-core/tools/subagent.ts +0 -209
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subagent recursion depth guard.
|
|
3
|
-
*
|
|
4
|
-
* Prevents uncontrolled recursive subagent spawning by tracking depth
|
|
5
|
-
* via environment variables. Inspired by pi-subagents' depth control
|
|
6
|
-
* but implemented as a minimal, standalone module.
|
|
7
|
-
*
|
|
8
|
-
* Environment variables:
|
|
9
|
-
* PI_SUBAGENT_DEPTH — current nesting depth (default: 0)
|
|
10
|
-
* PI_SUBAGENT_MAX_DEPTH — maximum allowed depth (default: 2)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Default maximum subagent nesting depth */
|
|
14
|
-
export const DEFAULT_MAX_SUBAGENT_DEPTH = 2
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Read the current subagent depth from environment.
|
|
18
|
-
* Returns 0 if unset or invalid.
|
|
19
|
-
*/
|
|
20
|
-
export function getCurrentDepth(): number {
|
|
21
|
-
const raw = process.env.PI_SUBAGENT_DEPTH
|
|
22
|
-
if (!raw) return 0
|
|
23
|
-
const depth = parseInt(raw, 10)
|
|
24
|
-
return Number.isNaN(depth) ? 0 : depth
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Read the maximum allowed depth from environment.
|
|
29
|
-
* Falls back to DEFAULT_MAX_SUBAGENT_DEPTH if unset or invalid.
|
|
30
|
-
*/
|
|
31
|
-
export function getMaxDepth(): number {
|
|
32
|
-
const raw = process.env.PI_SUBAGENT_MAX_DEPTH
|
|
33
|
-
if (!raw) return DEFAULT_MAX_SUBAGENT_DEPTH
|
|
34
|
-
const max = parseInt(raw, 10)
|
|
35
|
-
return Number.isNaN(max) || max < 0 ? DEFAULT_MAX_SUBAGENT_DEPTH : max
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Check whether subagent execution is allowed at the current depth.
|
|
40
|
-
* Returns an object with:
|
|
41
|
-
* allowed — whether execution can proceed
|
|
42
|
-
* depth — the current depth
|
|
43
|
-
* max — the maximum depth
|
|
44
|
-
* reason — human-readable explanation if blocked
|
|
45
|
-
*/
|
|
46
|
-
export function checkSubagentDepth(): {
|
|
47
|
-
allowed: boolean
|
|
48
|
-
depth: number
|
|
49
|
-
max: number
|
|
50
|
-
reason?: string
|
|
51
|
-
} {
|
|
52
|
-
const depth = getCurrentDepth()
|
|
53
|
-
const max = getMaxDepth()
|
|
54
|
-
|
|
55
|
-
if (depth >= max) {
|
|
56
|
-
return {
|
|
57
|
-
allowed: false,
|
|
58
|
-
depth,
|
|
59
|
-
max,
|
|
60
|
-
reason: `Subagent recursion depth limit reached (depth=${depth}, max=${max}). ` +
|
|
61
|
-
`Set PI_SUBAGENT_MAX_DEPTH to increase the limit.`,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return { allowed: true, depth, max }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Build environment variables to pass to a child subagent process.
|
|
70
|
-
* Increments the depth counter so the child can guard its own spawns.
|
|
71
|
-
*
|
|
72
|
-
* @param overrides — optional overrides for max depth per-agent
|
|
73
|
-
*/
|
|
74
|
-
export function getChildDepthEnv(overrides?: {
|
|
75
|
-
maxDepth?: number
|
|
76
|
-
}): Record<string, string> {
|
|
77
|
-
const currentDepth = getCurrentDepth()
|
|
78
|
-
const childDepth = currentDepth + 1
|
|
79
|
-
let maxDepth = overrides?.maxDepth ?? getMaxDepth()
|
|
80
|
-
if (maxDepth < 0) maxDepth = DEFAULT_MAX_SUBAGENT_DEPTH
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
PI_SUBAGENT_DEPTH: String(childDepth),
|
|
84
|
-
PI_SUBAGENT_MAX_DEPTH: String(maxDepth),
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,287 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
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
|
-
}
|