@leing2021/super-pi 0.22.1 → 0.23.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 (117) hide show
  1. package/README.md +66 -293
  2. package/extensions/ce-core/index.ts +276 -48
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -20
  6. package/rules/common/naming.md +154 -0
  7. package/skills/01-brainstorm/SKILL.md +42 -79
  8. package/skills/01-brainstorm/references/ce-brainstorm-mode.md +47 -0
  9. package/skills/02-plan/SKILL.md +43 -61
  10. package/skills/02-plan/references/solution-search.md +46 -0
  11. package/skills/03-work/SKILL.md +34 -52
  12. package/skills/03-work/references/completion-report.md +51 -0
  13. package/skills/04-review/SKILL.md +44 -51
  14. package/skills/04-review/references/solution-search.md +46 -0
  15. package/skills/06-next/SKILL.md +35 -20
  16. package/skills/06-next/references/recommendation-logic.md +47 -2
  17. package/skills/07-worktree/SKILL.md +21 -20
  18. package/skills/08-help/SKILL.md +46 -0
  19. package/skills/08-help/references/workflow-sequence.md +135 -0
  20. package/skills/references/language-detection.md +44 -0
  21. package/LICENSE +0 -21
  22. package/THIRD-PARTY-NOTICES.md +0 -35
  23. package/extensions/subagent/__tests__/async-job-tracker.test.ts +0 -21
  24. package/extensions/subagent/__tests__/execution-activity.test.ts +0 -15
  25. package/extensions/subagent/__tests__/parallel-render-stress.test.ts +0 -82
  26. package/extensions/subagent/__tests__/render-dedup.test.ts +0 -98
  27. package/extensions/subagent/__tests__/render-widget.test.ts +0 -28
  28. package/extensions/subagent/__tests__/throttle.test.ts +0 -90
  29. package/extensions/subagent/agent-management.ts +0 -596
  30. package/extensions/subagent/agent-manager-chain-detail.ts +0 -163
  31. package/extensions/subagent/agent-manager-detail.ts +0 -232
  32. package/extensions/subagent/agent-manager-edit.ts +0 -391
  33. package/extensions/subagent/agent-manager-list.ts +0 -279
  34. package/extensions/subagent/agent-manager-parallel.ts +0 -305
  35. package/extensions/subagent/agent-manager.ts +0 -706
  36. package/extensions/subagent/agent-scope.ts +0 -9
  37. package/extensions/subagent/agent-selection.ts +0 -26
  38. package/extensions/subagent/agent-serializer.ts +0 -124
  39. package/extensions/subagent/agent-templates.ts +0 -63
  40. package/extensions/subagent/agents/context-builder.md +0 -37
  41. package/extensions/subagent/agents/delegate.md +0 -9
  42. package/extensions/subagent/agents/oracle.md +0 -73
  43. package/extensions/subagent/agents/planner.md +0 -52
  44. package/extensions/subagent/agents/researcher.md +0 -50
  45. package/extensions/subagent/agents/reviewer.md +0 -38
  46. package/extensions/subagent/agents/scout.md +0 -48
  47. package/extensions/subagent/agents/worker.md +0 -52
  48. package/extensions/subagent/agents.ts +0 -762
  49. package/extensions/subagent/artifacts.ts +0 -101
  50. package/extensions/subagent/async-execution.ts +0 -521
  51. package/extensions/subagent/async-job-tracker.ts +0 -240
  52. package/extensions/subagent/async-status.ts +0 -242
  53. package/extensions/subagent/chain-clarify.ts +0 -1365
  54. package/extensions/subagent/chain-execution.ts +0 -854
  55. package/extensions/subagent/chain-serializer.ts +0 -127
  56. package/extensions/subagent/completion-dedupe.ts +0 -66
  57. package/extensions/subagent/doctor.ts +0 -201
  58. package/extensions/subagent/execution.ts +0 -748
  59. package/extensions/subagent/file-coalescer.ts +0 -43
  60. package/extensions/subagent/fork-context.ts +0 -64
  61. package/extensions/subagent/formatters.ts +0 -123
  62. package/extensions/subagent/frontmatter.ts +0 -32
  63. package/extensions/subagent/index.ts +0 -586
  64. package/extensions/subagent/intercom-bridge.ts +0 -241
  65. package/extensions/subagent/jsonl-writer.ts +0 -84
  66. package/extensions/subagent/model-fallback.ts +0 -109
  67. package/extensions/subagent/notify.ts +0 -111
  68. package/extensions/subagent/parallel-utils.ts +0 -109
  69. package/extensions/subagent/pi-args.ts +0 -139
  70. package/extensions/subagent/pi-spawn.ts +0 -101
  71. package/extensions/subagent/post-exit-stdio-guard.ts +0 -88
  72. package/extensions/subagent/prompt-template-bridge.ts +0 -400
  73. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  74. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  75. package/extensions/subagent/prompts/parallel-research.md +0 -50
  76. package/extensions/subagent/prompts/parallel-review.md +0 -40
  77. package/extensions/subagent/render-helpers.ts +0 -83
  78. package/extensions/subagent/render.ts +0 -908
  79. package/extensions/subagent/result-intercom.ts +0 -238
  80. package/extensions/subagent/result-watcher.ts +0 -172
  81. package/extensions/subagent/run-history.ts +0 -58
  82. package/extensions/subagent/run-status.ts +0 -137
  83. package/extensions/subagent/schemas.ts +0 -165
  84. package/extensions/subagent/session-tokens.ts +0 -51
  85. package/extensions/subagent/settings.ts +0 -368
  86. package/extensions/subagent/single-output.ts +0 -98
  87. package/extensions/subagent/skills.ts +0 -627
  88. package/extensions/subagent/slash-bridge.ts +0 -177
  89. package/extensions/subagent/slash-commands.ts +0 -304
  90. package/extensions/subagent/slash-live-state.ts +0 -295
  91. package/extensions/subagent/subagent-control.ts +0 -151
  92. package/extensions/subagent/subagent-executor.ts +0 -1915
  93. package/extensions/subagent/subagent-prompt-runtime.ts +0 -76
  94. package/extensions/subagent/subagent-runner.ts +0 -1471
  95. package/extensions/subagent/subagents-status.ts +0 -473
  96. package/extensions/subagent/text-editor.ts +0 -273
  97. package/extensions/subagent/throttle.ts +0 -77
  98. package/extensions/subagent/top-level-async.ts +0 -16
  99. package/extensions/subagent/types.ts +0 -624
  100. package/extensions/subagent/utils.ts +0 -457
  101. package/extensions/subagent/worktree.ts +0 -580
  102. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  103. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  104. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  105. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  106. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  107. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  108. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  109. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  110. package/extensions/super-pi-extension/index.ts +0 -129
  111. package/extensions/super-pi-extension/model-sync.ts +0 -204
  112. package/skills/08-status/SKILL.md +0 -41
  113. package/skills/08-status/references/artifact-locations.md +0 -10
  114. package/skills/09-help/SKILL.md +0 -37
  115. package/skills/09-help/references/workflow-sequence.md +0 -9
  116. package/skills/10-rules/SKILL.md +0 -76
  117. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,748 +0,0 @@
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
- }