@leing2021/super-pi 0.21.0 → 0.22.1

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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -357
  3. package/THIRD-PARTY-NOTICES.md +35 -0
  4. package/extensions/subagent/__tests__/async-job-tracker.test.ts +21 -0
  5. package/extensions/subagent/__tests__/execution-activity.test.ts +15 -0
  6. package/extensions/subagent/__tests__/parallel-render-stress.test.ts +82 -0
  7. package/extensions/subagent/__tests__/render-dedup.test.ts +98 -0
  8. package/extensions/subagent/__tests__/render-widget.test.ts +28 -0
  9. package/extensions/subagent/__tests__/throttle.test.ts +90 -0
  10. package/extensions/subagent/agent-management.ts +596 -0
  11. package/extensions/subagent/agent-manager-chain-detail.ts +163 -0
  12. package/extensions/subagent/agent-manager-detail.ts +232 -0
  13. package/extensions/subagent/agent-manager-edit.ts +391 -0
  14. package/extensions/subagent/agent-manager-list.ts +279 -0
  15. package/extensions/subagent/agent-manager-parallel.ts +305 -0
  16. package/extensions/subagent/agent-manager.ts +706 -0
  17. package/extensions/subagent/agent-scope.ts +9 -0
  18. package/extensions/subagent/agent-selection.ts +26 -0
  19. package/extensions/subagent/agent-serializer.ts +124 -0
  20. package/extensions/subagent/agent-templates.ts +63 -0
  21. package/extensions/subagent/agents/context-builder.md +37 -0
  22. package/extensions/subagent/agents/delegate.md +9 -0
  23. package/extensions/subagent/agents/oracle.md +73 -0
  24. package/extensions/subagent/agents/planner.md +52 -0
  25. package/extensions/subagent/agents/researcher.md +50 -0
  26. package/extensions/subagent/agents/reviewer.md +38 -0
  27. package/extensions/subagent/agents/scout.md +48 -0
  28. package/extensions/subagent/agents/worker.md +52 -0
  29. package/extensions/subagent/agents.ts +762 -0
  30. package/extensions/subagent/artifacts.ts +101 -0
  31. package/extensions/subagent/async-execution.ts +521 -0
  32. package/extensions/subagent/async-job-tracker.ts +240 -0
  33. package/extensions/subagent/async-status.ts +242 -0
  34. package/extensions/subagent/chain-clarify.ts +1365 -0
  35. package/extensions/subagent/chain-execution.ts +854 -0
  36. package/extensions/subagent/chain-serializer.ts +127 -0
  37. package/extensions/subagent/completion-dedupe.ts +66 -0
  38. package/extensions/subagent/doctor.ts +201 -0
  39. package/extensions/subagent/execution.ts +748 -0
  40. package/extensions/subagent/file-coalescer.ts +43 -0
  41. package/extensions/subagent/fork-context.ts +64 -0
  42. package/extensions/subagent/formatters.ts +123 -0
  43. package/extensions/subagent/frontmatter.ts +32 -0
  44. package/extensions/subagent/index.ts +586 -0
  45. package/extensions/subagent/intercom-bridge.ts +241 -0
  46. package/extensions/subagent/jsonl-writer.ts +84 -0
  47. package/extensions/subagent/model-fallback.ts +109 -0
  48. package/extensions/subagent/notify.ts +111 -0
  49. package/extensions/subagent/parallel-utils.ts +109 -0
  50. package/extensions/subagent/pi-args.ts +139 -0
  51. package/extensions/subagent/pi-spawn.ts +101 -0
  52. package/extensions/subagent/post-exit-stdio-guard.ts +88 -0
  53. package/extensions/subagent/prompt-template-bridge.ts +400 -0
  54. package/extensions/subagent/prompts/gather-context-and-clarify.md +13 -0
  55. package/extensions/subagent/prompts/parallel-cleanup.md +42 -0
  56. package/extensions/subagent/prompts/parallel-research.md +50 -0
  57. package/extensions/subagent/prompts/parallel-review.md +40 -0
  58. package/extensions/subagent/render-helpers.ts +83 -0
  59. package/extensions/subagent/render.ts +908 -0
  60. package/extensions/subagent/result-intercom.ts +238 -0
  61. package/extensions/subagent/result-watcher.ts +172 -0
  62. package/extensions/subagent/run-history.ts +58 -0
  63. package/extensions/subagent/run-status.ts +137 -0
  64. package/extensions/subagent/schemas.ts +165 -0
  65. package/extensions/subagent/session-tokens.ts +51 -0
  66. package/extensions/subagent/settings.ts +368 -0
  67. package/extensions/subagent/single-output.ts +98 -0
  68. package/extensions/subagent/skills.ts +627 -0
  69. package/extensions/subagent/slash-bridge.ts +177 -0
  70. package/extensions/subagent/slash-commands.ts +304 -0
  71. package/extensions/subagent/slash-live-state.ts +295 -0
  72. package/extensions/subagent/subagent-control.ts +151 -0
  73. package/extensions/subagent/subagent-executor.ts +1915 -0
  74. package/extensions/subagent/subagent-prompt-runtime.ts +76 -0
  75. package/extensions/subagent/subagent-runner.ts +1471 -0
  76. package/extensions/subagent/subagents-status.ts +473 -0
  77. package/extensions/subagent/text-editor.ts +273 -0
  78. package/extensions/subagent/throttle.ts +77 -0
  79. package/extensions/subagent/top-level-async.ts +16 -0
  80. package/extensions/subagent/types.ts +624 -0
  81. package/extensions/subagent/utils.ts +457 -0
  82. package/extensions/subagent/worktree.ts +580 -0
  83. package/extensions/super-pi-extension/index.ts +2 -55
  84. package/package.json +14 -6
  85. package/skills/pi-subagents/SKILL.md +566 -0
@@ -0,0 +1,748 @@
1
+ // SPDX-FileCopyrightText: 2025 Nico Bailon
2
+ // SPDX-License-Identifier: MIT
3
+ // Source: https://github.com/nicobailon/pi-subagents
4
+ /**
5
+ * Core execution logic for running subagents
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import type { Message } from "@mariozechner/pi-ai";
11
+ import type { AgentConfig } from "./agents.ts";
12
+ import {
13
+ ensureArtifactsDir,
14
+ getArtifactPaths,
15
+ writeArtifact,
16
+ writeMetadata,
17
+ } from "./artifacts.ts";
18
+ import {
19
+ type AgentProgress,
20
+ type ArtifactPaths,
21
+ type ControlEvent,
22
+ type ModelAttempt,
23
+ type RunSyncOptions,
24
+ type SingleResult,
25
+ type Usage,
26
+ DEFAULT_MAX_OUTPUT,
27
+ INTERCOM_DETACH_REQUEST_EVENT,
28
+ INTERCOM_DETACH_RESPONSE_EVENT,
29
+ truncateOutput,
30
+ getSubagentDepthEnv,
31
+ } from "./types.ts";
32
+ import {
33
+ DEFAULT_CONTROL_CONFIG,
34
+ buildControlEvent,
35
+ claimControlNotification,
36
+ deriveActivityState,
37
+ shouldEmitControlEvent,
38
+ shouldNotifyControlEvent,
39
+ } from "./subagent-control.ts";
40
+ import {
41
+ getFinalOutput,
42
+ findLatestSessionFile,
43
+ detectSubagentError,
44
+ extractToolArgsPreview,
45
+ extractTextFromContent,
46
+ } from "./utils.ts";
47
+ import { buildSkillInjection, resolveSkillsWithFallback } from "./skills.ts";
48
+ import { getPiSpawnCommand } from "./pi-spawn.ts";
49
+ import { createJsonlWriter } from "./jsonl-writer.ts";
50
+ import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
51
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.ts";
52
+ import { captureSingleOutputSnapshot, resolveSingleOutput, type SingleOutputSnapshot } from "./single-output.ts";
53
+ import {
54
+ buildModelCandidates,
55
+ formatModelAttemptNote,
56
+ isRetryableModelFailure,
57
+ } from "./model-fallback.ts";
58
+
59
+ function emptyUsage(): Usage {
60
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
61
+ }
62
+
63
+ function sumUsage(target: Usage, source: Usage): void {
64
+ target.input += source.input;
65
+ target.output += source.output;
66
+ target.cacheRead += source.cacheRead;
67
+ target.cacheWrite += source.cacheWrite;
68
+ target.cost += source.cost;
69
+ target.turns += source.turns;
70
+ }
71
+
72
+ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
73
+ if (lines.length === 0) return;
74
+ progress.recentOutput.push(...lines.filter((line) => line.trim()));
75
+ if (progress.recentOutput.length > 50) {
76
+ progress.recentOutput.splice(0, progress.recentOutput.length - 50);
77
+ }
78
+ }
79
+
80
+ function snapshotProgress(progress: AgentProgress): AgentProgress {
81
+ return {
82
+ ...progress,
83
+ skills: progress.skills ? [...progress.skills] : undefined,
84
+ recentTools: progress.recentTools.map((tool) => ({ ...tool })),
85
+ recentOutput: [...progress.recentOutput],
86
+ };
87
+ }
88
+
89
+ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleResult {
90
+ return {
91
+ ...result,
92
+ messages: result.messages ? [...result.messages] : undefined,
93
+ usage: { ...result.usage },
94
+ skills: result.skills ? [...result.skills] : undefined,
95
+ attemptedModels: result.attemptedModels ? [...result.attemptedModels] : undefined,
96
+ modelAttempts: result.modelAttempts
97
+ ? result.modelAttempts.map((attempt) => ({
98
+ ...attempt,
99
+ usage: attempt.usage ? { ...attempt.usage } : undefined,
100
+ }))
101
+ : undefined,
102
+ controlEvents: result.controlEvents ? result.controlEvents.map((event) => ({ ...event })) : undefined,
103
+ progress,
104
+ progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
105
+ artifactPaths: result.artifactPaths ? { ...result.artifactPaths } : undefined,
106
+ truncation: result.truncation ? { ...result.truncation } : undefined,
107
+ };
108
+ }
109
+
110
+ async function runSingleAttempt(
111
+ runtimeCwd: string,
112
+ agent: AgentConfig,
113
+ task: string,
114
+ model: string | undefined,
115
+ options: RunSyncOptions,
116
+ shared: {
117
+ sessionEnabled: boolean;
118
+ systemPrompt: string;
119
+ resolvedSkillNames?: string[];
120
+ skillsWarning?: string;
121
+ jsonlPath?: string;
122
+ artifactPaths?: ArtifactPaths;
123
+ attemptNotes: string[];
124
+ outputSnapshot?: SingleOutputSnapshot;
125
+ },
126
+ ): Promise<SingleResult> {
127
+ const modelArg = applyThinkingSuffix(model, agent.thinking);
128
+ const { args, env: sharedEnv, tempDir } = buildPiArgs({
129
+ baseArgs: ["--mode", "json", "-p"],
130
+ task,
131
+ sessionEnabled: shared.sessionEnabled,
132
+ sessionDir: options.sessionDir,
133
+ sessionFile: options.sessionFile,
134
+ model,
135
+ thinking: agent.thinking,
136
+ systemPromptMode: agent.systemPromptMode,
137
+ inheritProjectContext: agent.inheritProjectContext,
138
+ inheritSkills: agent.inheritSkills,
139
+ tools: agent.tools,
140
+ extensions: agent.extensions,
141
+ systemPrompt: shared.systemPrompt,
142
+ mcpDirectTools: agent.mcpDirectTools,
143
+ promptFileStem: agent.name,
144
+ intercomSessionName: options.intercomSessionName,
145
+ });
146
+
147
+ const result: SingleResult = {
148
+ agent: agent.name,
149
+ task,
150
+ exitCode: 0,
151
+ messages: [],
152
+ usage: emptyUsage(),
153
+ model: modelArg,
154
+ artifactPaths: shared.artifactPaths,
155
+ skills: shared.resolvedSkillNames,
156
+ skillsWarning: shared.skillsWarning,
157
+ };
158
+ const startTime = Date.now();
159
+ const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
160
+ let interruptedByControl = false;
161
+ const allControlEvents: ControlEvent[] = [];
162
+ let pendingControlEvents: ControlEvent[] = [];
163
+
164
+ const progress: AgentProgress = {
165
+ index: options.index ?? 0,
166
+ agent: agent.name,
167
+ status: "running",
168
+ task,
169
+ skills: shared.resolvedSkillNames,
170
+ recentTools: [],
171
+ recentOutput: [...shared.attemptNotes],
172
+ toolCount: 0,
173
+ tokens: 0,
174
+ durationMs: 0,
175
+ lastActivityAt: startTime,
176
+ };
177
+ result.progress = progress;
178
+ const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
179
+
180
+ const exitCode = await new Promise<number>((resolve) => {
181
+ const spawnSpec = getPiSpawnCommand(args);
182
+ const proc = spawn(spawnSpec.command, spawnSpec.args, {
183
+ cwd: options.cwd ?? runtimeCwd,
184
+ env: spawnEnv,
185
+ stdio: ["ignore", "pipe", "pipe"],
186
+ });
187
+ const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
188
+ let buf = "";
189
+ let processClosed = false;
190
+ let settled = false;
191
+ let detached = false;
192
+ let intercomStarted = false;
193
+ let removeAbortListener: (() => void) | undefined;
194
+ let removeInterruptListener: (() => void) | undefined;
195
+ let activityTimer: NodeJS.Timeout | undefined;
196
+
197
+ const detachForIntercom = () => {
198
+ detached = true;
199
+ processClosed = true;
200
+ result.detached = true;
201
+ result.detachedReason = "intercom coordination";
202
+ progress.status = "detached";
203
+ progress.durationMs = Date.now() - startTime;
204
+ result.progressSummary = {
205
+ toolCount: progress.toolCount,
206
+ tokens: progress.tokens,
207
+ durationMs: progress.durationMs,
208
+ };
209
+ finish(-2);
210
+ };
211
+
212
+ // If the child emits its final assistant message but never exits,
213
+ // start a bounded drain window and force termination if needed.
214
+ const FINAL_DRAIN_MS = 5000;
215
+ const HARD_KILL_MS = 3000;
216
+ let childExited = false;
217
+ let forcedTerminationSignal = false;
218
+ let finalDrainTimer: NodeJS.Timeout | undefined;
219
+ let finalHardKillTimer: NodeJS.Timeout | undefined;
220
+ const clearFinalDrainTimers = () => {
221
+ if (finalDrainTimer) {
222
+ clearTimeout(finalDrainTimer);
223
+ finalDrainTimer = undefined;
224
+ }
225
+ if (finalHardKillTimer) {
226
+ clearTimeout(finalHardKillTimer);
227
+ finalHardKillTimer = undefined;
228
+ }
229
+ };
230
+ const startFinalDrain = () => {
231
+ if (childExited || finalDrainTimer || settled || processClosed || detached) return;
232
+ finalDrainTimer = setTimeout(() => {
233
+ if (settled || processClosed || detached) return;
234
+ const termSent = trySignalChild(proc, "SIGTERM");
235
+ if (!termSent) return;
236
+ forcedTerminationSignal = true;
237
+ result.error = result.error
238
+ ?? `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
239
+ finalHardKillTimer = setTimeout(() => {
240
+ if (settled || processClosed || detached) return;
241
+ forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
242
+ }, HARD_KILL_MS);
243
+ finalHardKillTimer.unref?.();
244
+ }, FINAL_DRAIN_MS);
245
+ finalDrainTimer.unref?.();
246
+ };
247
+
248
+ const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
249
+ if (!options.allowIntercomDetach || detached || processClosed || !intercomStarted) return;
250
+ if (!payload || typeof payload !== "object") return;
251
+ const requestId = (payload as { requestId?: unknown }).requestId;
252
+ if (typeof requestId !== "string" || requestId.length === 0) return;
253
+ options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
254
+ detachForIntercom();
255
+ });
256
+
257
+ const finish = (code: number) => {
258
+ if (settled) return;
259
+ settled = true;
260
+ clearFinalDrainTimers();
261
+ clearStdioGuard();
262
+ if (activityTimer) {
263
+ clearInterval(activityTimer);
264
+ activityTimer = undefined;
265
+ }
266
+ unsubscribeIntercomDetach?.();
267
+ removeAbortListener?.();
268
+ removeInterruptListener?.();
269
+ resolve(code);
270
+ };
271
+
272
+ const drainPendingControlEvents = (): ControlEvent[] | undefined => {
273
+ if (pendingControlEvents.length === 0) return undefined;
274
+ const events = pendingControlEvents;
275
+ pendingControlEvents = [];
276
+ return events;
277
+ };
278
+
279
+ const emittedControlEventKeys = new Set<string>();
280
+ const emitControlEvent = (event: ControlEvent) => {
281
+ if (shouldNotifyControlEvent(controlConfig, event) && !claimControlNotification(controlConfig, event, emittedControlEventKeys)) return;
282
+ allControlEvents.push(event);
283
+ pendingControlEvents.push(event);
284
+ options.onControlEvent?.(event);
285
+ };
286
+
287
+ const updateActivityState = (now: number): boolean => {
288
+ const next = deriveActivityState({
289
+ config: controlConfig,
290
+ startedAt: startTime,
291
+ lastActivityAt: progress.lastActivityAt,
292
+ now,
293
+ });
294
+ if (next === progress.activityState) return false;
295
+ const previous = progress.activityState;
296
+ progress.activityState = next;
297
+ if (shouldEmitControlEvent(controlConfig, previous, next)) {
298
+ emitControlEvent(buildControlEvent({
299
+ from: previous,
300
+ to: next,
301
+ runId: options.runId,
302
+ agent: agent.name,
303
+ index: options.index,
304
+ ts: now,
305
+ lastActivityAt: progress.lastActivityAt,
306
+ }));
307
+ }
308
+ return true;
309
+ };
310
+
311
+
312
+ const emitUpdateSnapshot = (text: string) => {
313
+ if (!options.onUpdate || processClosed) return;
314
+ const progressSnapshot = snapshotProgress(progress);
315
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
316
+ const controlEvents = drainPendingControlEvents();
317
+ options.onUpdate({
318
+ content: [{ type: "text", text }],
319
+ details: {
320
+ mode: "single",
321
+ results: [resultSnapshot],
322
+ progress: [progressSnapshot],
323
+ controlEvents,
324
+ },
325
+ });
326
+ };
327
+
328
+ const fireUpdate = () => {
329
+ if (!options.onUpdate || processClosed) return;
330
+ progress.durationMs = Date.now() - startTime;
331
+ emitUpdateSnapshot(getFinalOutput(result.messages) || "(running...)");
332
+ };
333
+
334
+ const processLine = (line: string) => {
335
+ if (!line.trim()) return;
336
+ jsonlWriter.writeLine(line);
337
+ let evt: { type?: string; message?: Message; toolName?: string; args?: unknown };
338
+ try {
339
+ evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
340
+ } catch {
341
+ // Non-JSON stdout lines are expected; only structured events are parsed.
342
+ return;
343
+ }
344
+
345
+ const now = Date.now();
346
+ progress.durationMs = now - startTime;
347
+ progress.lastActivityAt = now;
348
+ updateActivityState(now);
349
+
350
+ if (evt.type === "tool_execution_start") {
351
+ if (options.allowIntercomDetach && evt.toolName === "intercom") {
352
+ intercomStarted = true;
353
+ }
354
+ progress.toolCount++;
355
+ progress.currentTool = evt.toolName;
356
+ progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
357
+ progress.currentToolStartedAt = now;
358
+ fireUpdate();
359
+ }
360
+
361
+ if (evt.type === "tool_execution_end") {
362
+ if (progress.currentTool) {
363
+ progress.recentTools.push({
364
+ tool: progress.currentTool,
365
+ args: progress.currentToolArgs || "",
366
+ endMs: now,
367
+ });
368
+ }
369
+ progress.currentTool = undefined;
370
+ progress.currentToolArgs = undefined;
371
+ progress.currentToolStartedAt = undefined;
372
+ fireUpdate();
373
+ }
374
+
375
+ if (evt.type === "message_end" && evt.message) {
376
+ result.messages.push(evt.message);
377
+ if (evt.message.role === "assistant") {
378
+ result.usage.turns++;
379
+ const u = evt.message.usage;
380
+ if (u) {
381
+ result.usage.input += u.input || 0;
382
+ result.usage.output += u.output || 0;
383
+ result.usage.cacheRead += u.cacheRead || 0;
384
+ result.usage.cacheWrite += u.cacheWrite || 0;
385
+ result.usage.cost += u.cost?.total || 0;
386
+ progress.tokens = result.usage.input + result.usage.output;
387
+ }
388
+ if (!result.model && evt.message.model) result.model = evt.message.model;
389
+ if (evt.message.errorMessage) result.error = evt.message.errorMessage;
390
+ appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
391
+ // Final assistant message: start the exit drain window.
392
+ const stopReason = (evt.message as { stopReason?: string }).stopReason;
393
+ const hasToolCall = Array.isArray(evt.message.content)
394
+ && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
395
+ if (stopReason === "stop" && !hasToolCall) {
396
+ startFinalDrain();
397
+ }
398
+ }
399
+ fireUpdate();
400
+ }
401
+
402
+ if (evt.type === "tool_result_end" && evt.message) {
403
+ result.messages.push(evt.message);
404
+ appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
405
+ fireUpdate();
406
+ }
407
+ };
408
+
409
+ if (controlConfig.enabled) {
410
+ const activityIntervalMs = resolveActivityTimerInterval();
411
+ activityTimer = setInterval(() => {
412
+ if (processClosed || settled || detached) return;
413
+ const now = Date.now();
414
+ if (updateActivityState(now)) {
415
+ progress.durationMs = now - startTime;
416
+ fireUpdate();
417
+ }
418
+ }, activityIntervalMs);
419
+ activityTimer.unref?.();
420
+ }
421
+
422
+ let stderrBuf = "";
423
+
424
+ const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
425
+ proc.stdout.on("data", (d) => {
426
+ buf += d.toString();
427
+ const lines = buf.split("\n");
428
+ buf = lines.pop() || "";
429
+ lines.forEach(processLine);
430
+ });
431
+ proc.stderr.on("data", (d) => {
432
+ stderrBuf += d.toString();
433
+ });
434
+ proc.on("exit", () => {
435
+ childExited = true;
436
+ clearFinalDrainTimers();
437
+ });
438
+ proc.on("close", (code, signal) => {
439
+ clearFinalDrainTimers();
440
+ clearStdioGuard();
441
+ void jsonlWriter.close().catch(() => {
442
+ // JSONL artifact flush is best effort.
443
+ });
444
+ cleanupTempDir(tempDir);
445
+ if (detached) {
446
+ finish(-2);
447
+ return;
448
+ }
449
+ processClosed = true;
450
+ if (buf.trim()) processLine(buf);
451
+ if (code !== 0 && stderrBuf.trim() && !result.error) {
452
+ result.error = stderrBuf.trim();
453
+ }
454
+ const finalCode = forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
455
+ finish(finalCode);
456
+ });
457
+ proc.on("error", (error) => {
458
+ clearFinalDrainTimers();
459
+ clearStdioGuard();
460
+ void jsonlWriter.close().catch(() => {
461
+ // JSONL artifact flush is best effort.
462
+ });
463
+ cleanupTempDir(tempDir);
464
+ if (!result.error) {
465
+ result.error = error instanceof Error ? error.message : String(error);
466
+ }
467
+ finish(1);
468
+ });
469
+
470
+ if (options.signal) {
471
+ const kill = () => {
472
+ if (processClosed || detached) return;
473
+ if (options.allowIntercomDetach && intercomStarted && !detached) {
474
+ detachForIntercom();
475
+ return;
476
+ }
477
+ proc.kill("SIGTERM");
478
+ setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
479
+ };
480
+ if (options.signal.aborted) kill();
481
+ else {
482
+ options.signal.addEventListener("abort", kill, { once: true });
483
+ removeAbortListener = () => options.signal?.removeEventListener("abort", kill);
484
+ }
485
+ }
486
+
487
+ if (options.interruptSignal) {
488
+ const interrupt = () => {
489
+ if (processClosed || detached || settled) return;
490
+ interruptedByControl = true;
491
+ progress.status = "running";
492
+ progress.durationMs = Date.now() - startTime;
493
+ result.interrupted = true;
494
+ result.finalOutput = "Interrupted. Waiting for explicit next action.";
495
+ progress.activityState = undefined;
496
+ fireUpdate();
497
+ trySignalChild(proc, "SIGINT");
498
+ setTimeout(() => {
499
+ if (settled || processClosed || detached) return;
500
+ trySignalChild(proc, "SIGTERM");
501
+ }, 1000).unref?.();
502
+ };
503
+ if (options.interruptSignal.aborted) interrupt();
504
+ else {
505
+ options.interruptSignal.addEventListener("abort", interrupt, { once: true });
506
+ removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
507
+ }
508
+ }
509
+ });
510
+ result.exitCode = exitCode;
511
+ if (interruptedByControl) {
512
+ result.exitCode = 0;
513
+ result.interrupted = true;
514
+ result.error = undefined;
515
+ result.finalOutput = result.finalOutput || "Interrupted. Waiting for explicit next action.";
516
+ result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
517
+ progress.activityState = undefined;
518
+ progress.durationMs = Date.now() - startTime;
519
+ result.progressSummary = {
520
+ toolCount: progress.toolCount,
521
+ tokens: progress.tokens,
522
+ durationMs: progress.durationMs,
523
+ };
524
+ return result;
525
+ }
526
+ if (result.detached) {
527
+ result.exitCode = 0;
528
+ result.finalOutput = "Detached for intercom coordination.";
529
+ return result;
530
+ }
531
+
532
+ if (exitCode === 0 && !result.error) {
533
+ const errInfo = detectSubagentError(result.messages);
534
+ if (errInfo.hasError) {
535
+ result.exitCode = errInfo.exitCode ?? 1;
536
+ result.error = errInfo.details
537
+ ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
538
+ : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
539
+ }
540
+ }
541
+
542
+ progress.status = result.exitCode === 0 ? "completed" : "failed";
543
+ progress.durationMs = Date.now() - startTime;
544
+ if (result.error) {
545
+ progress.error = result.error;
546
+ if (progress.currentTool) {
547
+ progress.failedTool = progress.currentTool;
548
+ }
549
+ }
550
+
551
+ result.progressSummary = {
552
+ toolCount: progress.toolCount,
553
+ tokens: progress.tokens,
554
+ durationMs: progress.durationMs,
555
+ };
556
+
557
+ let fullOutput = getFinalOutput(result.messages);
558
+ if (options.outputPath && result.exitCode === 0) {
559
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
560
+ fullOutput = resolvedOutput.fullOutput;
561
+ result.savedOutputPath = resolvedOutput.savedPath;
562
+ result.outputSaveError = resolvedOutput.saveError;
563
+ }
564
+ result.finalOutput = fullOutput;
565
+ if (options.onUpdate) {
566
+ const finalText = result.finalOutput || result.error || "(no output)";
567
+ const progressSnapshot = snapshotProgress(progress);
568
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
569
+ options.onUpdate({
570
+ content: [{ type: "text", text: finalText }],
571
+ details: { mode: "single", results: [resultSnapshot], progress: [progressSnapshot] },
572
+ });
573
+ }
574
+ return result;
575
+ }
576
+
577
+ /**
578
+ * Run a subagent synchronously (blocking until complete)
579
+ */
580
+ /**
581
+ * Resolve activity timer interval based on concurrency level.
582
+ * Higher concurrency → longer interval to reduce render pressure.
583
+ */
584
+ export function resolveActivityTimerInterval(concurrentCount = 1): number {
585
+ return concurrentCount <= 1 ? 2000 : 3000;
586
+ }
587
+
588
+ export async function runSync(
589
+ runtimeCwd: string,
590
+ agents: AgentConfig[],
591
+ agentName: string,
592
+ task: string,
593
+ options: RunSyncOptions,
594
+ ): Promise<SingleResult> {
595
+ const agent = agents.find((a) => a.name === agentName);
596
+ if (!agent) {
597
+ return {
598
+ agent: agentName,
599
+ task,
600
+ exitCode: 1,
601
+ messages: [],
602
+ usage: emptyUsage(),
603
+ error: `Unknown agent: ${agentName}`,
604
+ };
605
+ }
606
+
607
+ const shareEnabled = options.share === true;
608
+ const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
609
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
610
+ const skillNames = options.skills ?? agent.skills ?? [];
611
+ const skillCwd = options.cwd ?? runtimeCwd;
612
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
613
+ let systemPrompt = agent.systemPrompt?.trim() || "";
614
+ if (resolvedSkills.length > 0) {
615
+ const skillInjection = buildSkillInjection(resolvedSkills);
616
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
617
+ }
618
+
619
+ const candidates = buildModelCandidates(
620
+ options.modelOverride ?? agent.model,
621
+ agent.fallbackModels,
622
+ options.availableModels,
623
+ options.preferredModelProvider,
624
+ );
625
+ const attemptedModels: string[] = [];
626
+ const modelAttempts: ModelAttempt[] = [];
627
+ const aggregateUsage = emptyUsage();
628
+ const attemptNotes: string[] = [];
629
+ let totalToolCount = 0;
630
+ let totalDurationMs = 0;
631
+
632
+ let artifactPathsResult: ArtifactPaths | undefined;
633
+ let jsonlPath: string | undefined;
634
+ if (options.artifactsDir && options.artifactConfig?.enabled !== false) {
635
+ artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
636
+ ensureArtifactsDir(options.artifactsDir);
637
+ if (options.artifactConfig?.includeInput !== false) {
638
+ writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
639
+ }
640
+ if (options.artifactConfig?.includeJsonl !== false) {
641
+ jsonlPath = artifactPathsResult.jsonlPath;
642
+ }
643
+ }
644
+
645
+ let lastResult: SingleResult | undefined;
646
+ const modelsToTry = candidates.length > 0 ? candidates : [undefined];
647
+ for (let i = 0; i < modelsToTry.length; i++) {
648
+ const candidate = modelsToTry[i];
649
+ if (candidate) attemptedModels.push(candidate);
650
+ const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
651
+ sessionEnabled,
652
+ systemPrompt,
653
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
654
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
655
+ jsonlPath,
656
+ artifactPaths: artifactPathsResult,
657
+ attemptNotes,
658
+ outputSnapshot,
659
+ });
660
+ lastResult = result;
661
+ sumUsage(aggregateUsage, result.usage);
662
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
663
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
664
+ const attempt: ModelAttempt = {
665
+ model: candidate ?? result.model ?? agent.model ?? "default",
666
+ success: result.exitCode === 0,
667
+ exitCode: result.exitCode,
668
+ error: result.error,
669
+ usage: { ...result.usage },
670
+ };
671
+ modelAttempts.push(attempt);
672
+ if (result.exitCode === 0) {
673
+ break;
674
+ }
675
+ if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
676
+ break;
677
+ }
678
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
679
+ }
680
+
681
+ const result = lastResult ?? {
682
+ agent: agentName,
683
+ task,
684
+ exitCode: 1,
685
+ messages: [],
686
+ usage: emptyUsage(),
687
+ error: "Subagent did not produce a result.",
688
+ } satisfies SingleResult;
689
+
690
+ result.usage = aggregateUsage;
691
+ result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
692
+ result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
693
+ result.progressSummary = {
694
+ toolCount: totalToolCount,
695
+ tokens: aggregateUsage.input + aggregateUsage.output,
696
+ durationMs: totalDurationMs,
697
+ };
698
+ if (attemptNotes.length > 0 && result.progress) {
699
+ result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
700
+ if (result.progress.recentOutput.length > 50) {
701
+ result.progress.recentOutput.splice(50);
702
+ }
703
+ }
704
+
705
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
706
+ result.artifactPaths = artifactPathsResult;
707
+ if (options.artifactConfig?.includeOutput !== false) {
708
+ writeArtifact(artifactPathsResult.outputPath, result.finalOutput ?? "");
709
+ }
710
+ if (options.artifactConfig?.includeMetadata !== false) {
711
+ writeMetadata(artifactPathsResult.metadataPath, {
712
+ runId: options.runId,
713
+ agent: agentName,
714
+ task,
715
+ exitCode: result.exitCode,
716
+ usage: result.usage,
717
+ model: result.model,
718
+ attemptedModels: result.attemptedModels,
719
+ modelAttempts: result.modelAttempts,
720
+ durationMs: result.progressSummary?.durationMs,
721
+ toolCount: result.progressSummary?.toolCount,
722
+ error: result.error,
723
+ skills: result.skills,
724
+ skillsWarning: result.skillsWarning,
725
+ timestamp: Date.now(),
726
+ });
727
+ }
728
+
729
+ if (options.maxOutput) {
730
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
731
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
732
+ if (truncationResult.truncated) result.truncation = truncationResult;
733
+ }
734
+ } else if (options.maxOutput) {
735
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
736
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config);
737
+ if (truncationResult.truncated) result.truncation = truncationResult;
738
+ }
739
+
740
+ if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
741
+ result.sessionFile = options.sessionFile;
742
+ } else if (shareEnabled && options.sessionDir) {
743
+ const sessionFile = findLatestSessionFile(options.sessionDir);
744
+ if (sessionFile) result.sessionFile = sessionFile;
745
+ }
746
+
747
+ return result;
748
+ }