@pi-agents/orchid 0.1.0-beta.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/agents/AGENTS-MANIFEST.md +42 -0
  5. package/agents/brain.md +42 -0
  6. package/agents/context-builder.md +46 -0
  7. package/agents/delegate.md +12 -0
  8. package/agents/dev-1.md +42 -0
  9. package/agents/oracle.md +73 -0
  10. package/agents/planner.md +55 -0
  11. package/agents/researcher.md +52 -0
  12. package/agents/reviewer.md +79 -0
  13. package/agents/scout.md +50 -0
  14. package/agents/tester.md +45 -0
  15. package/agents/worker.md +55 -0
  16. package/extensions/ralph.ts +1 -0
  17. package/extensions/reviewer-extension.ts +125 -0
  18. package/extensions/task-orchestrator.ts +28 -0
  19. package/package.json +63 -0
  20. package/prompts/gather-context-and-clarify.md +13 -0
  21. package/prompts/parallel-cleanup.md +59 -0
  22. package/prompts/parallel-context-build.md +53 -0
  23. package/prompts/parallel-handoff-plan.md +59 -0
  24. package/prompts/parallel-research.md +50 -0
  25. package/prompts/parallel-review.md +54 -0
  26. package/prompts/review-loop.md +41 -0
  27. package/skills/orchid/SKILL.md +214 -0
  28. package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
  29. package/skills/orchid/orchid-converge/SKILL.md +124 -0
  30. package/skills/orchid/orchid-decompose/SKILL.md +201 -0
  31. package/skills/orchid/orchid-doctor/SKILL.md +162 -0
  32. package/skills/orchid/orchid-investigate/SKILL.md +102 -0
  33. package/skills/orchid/orchid-launch/SKILL.md +147 -0
  34. package/skills/ralph/SKILL.md +73 -0
  35. package/skills/subagents/pi-subagents/SKILL.md +813 -0
  36. package/src/index.ts +7 -0
  37. package/src/orchestrator/abort.ts +534 -0
  38. package/src/orchestrator/agent-bridge-extension.ts +1020 -0
  39. package/src/orchestrator/agent-host.ts +954 -0
  40. package/src/orchestrator/cleanup.ts +776 -0
  41. package/src/orchestrator/config-loader.ts +1412 -0
  42. package/src/orchestrator/config-schema.ts +690 -0
  43. package/src/orchestrator/config.ts +81 -0
  44. package/src/orchestrator/context-window.ts +66 -0
  45. package/src/orchestrator/diagnostic-reports.ts +475 -0
  46. package/src/orchestrator/diagnostics.ts +394 -0
  47. package/src/orchestrator/discovery.ts +1833 -0
  48. package/src/orchestrator/engine-worker.ts +415 -0
  49. package/src/orchestrator/engine.ts +5940 -0
  50. package/src/orchestrator/execution.ts +3104 -0
  51. package/src/orchestrator/extension.ts +5934 -0
  52. package/src/orchestrator/formatting.ts +785 -0
  53. package/src/orchestrator/git.ts +88 -0
  54. package/src/orchestrator/index.ts +28 -0
  55. package/src/orchestrator/lane-runner.ts +1787 -0
  56. package/src/orchestrator/mailbox.ts +780 -0
  57. package/src/orchestrator/merge.ts +3414 -0
  58. package/src/orchestrator/messages.ts +1062 -0
  59. package/src/orchestrator/migrations.ts +278 -0
  60. package/src/orchestrator/naming.ts +117 -0
  61. package/src/orchestrator/path-resolver.ts +275 -0
  62. package/src/orchestrator/persistence.ts +2625 -0
  63. package/src/orchestrator/process-registry.ts +452 -0
  64. package/src/orchestrator/quality-gate.ts +1085 -0
  65. package/src/orchestrator/resume.ts +3488 -0
  66. package/src/orchestrator/sessions.ts +57 -0
  67. package/src/orchestrator/settings-loader.ts +136 -0
  68. package/src/orchestrator/settings-tui.ts +2208 -0
  69. package/src/orchestrator/sidecar-telemetry.ts +267 -0
  70. package/src/orchestrator/supervisor.ts +4548 -0
  71. package/src/orchestrator/task-executor-core.ts +675 -0
  72. package/src/orchestrator/tmux-compat.ts +37 -0
  73. package/src/orchestrator/tool-allowlist-constants.ts +37 -0
  74. package/src/orchestrator/types.ts +4465 -0
  75. package/src/orchestrator/verification.ts +547 -0
  76. package/src/orchestrator/waves.ts +1564 -0
  77. package/src/orchestrator/workspace.ts +707 -0
  78. package/src/orchestrator/worktree.ts +2725 -0
  79. package/src/ralph/index.ts +825 -0
  80. package/src/subagents/agents/agent-management.ts +648 -0
  81. package/src/subagents/agents/agent-scope.ts +6 -0
  82. package/src/subagents/agents/agent-selection.ts +23 -0
  83. package/src/subagents/agents/agent-serializer.ts +86 -0
  84. package/src/subagents/agents/agents.ts +832 -0
  85. package/src/subagents/agents/chain-serializer.ts +137 -0
  86. package/src/subagents/agents/frontmatter.ts +29 -0
  87. package/src/subagents/agents/identity.ts +30 -0
  88. package/src/subagents/agents/skills.ts +632 -0
  89. package/src/subagents/extension/config.ts +16 -0
  90. package/src/subagents/extension/control-notices.ts +92 -0
  91. package/src/subagents/extension/doctor.ts +199 -0
  92. package/src/subagents/extension/fanout-child.ts +170 -0
  93. package/src/subagents/extension/index.ts +573 -0
  94. package/src/subagents/extension/schemas.ts +168 -0
  95. package/src/subagents/intercom/intercom-bridge.ts +379 -0
  96. package/src/subagents/intercom/result-intercom.ts +377 -0
  97. package/src/subagents/runs/background/async-execution.ts +712 -0
  98. package/src/subagents/runs/background/async-job-tracker.ts +310 -0
  99. package/src/subagents/runs/background/async-resume.ts +345 -0
  100. package/src/subagents/runs/background/async-status.ts +325 -0
  101. package/src/subagents/runs/background/completion-dedupe.ts +63 -0
  102. package/src/subagents/runs/background/notify.ts +108 -0
  103. package/src/subagents/runs/background/parallel-groups.ts +45 -0
  104. package/src/subagents/runs/background/result-watcher.ts +307 -0
  105. package/src/subagents/runs/background/run-id-resolver.ts +83 -0
  106. package/src/subagents/runs/background/run-status.ts +269 -0
  107. package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
  108. package/src/subagents/runs/background/subagent-runner.ts +1808 -0
  109. package/src/subagents/runs/background/top-level-async.ts +13 -0
  110. package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
  111. package/src/subagents/runs/foreground/chain-execution.ts +938 -0
  112. package/src/subagents/runs/foreground/execution.ts +918 -0
  113. package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
  114. package/src/subagents/runs/shared/completion-guard.ts +147 -0
  115. package/src/subagents/runs/shared/long-running-guard.ts +175 -0
  116. package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  117. package/src/subagents/runs/shared/model-fallback.ts +103 -0
  118. package/src/subagents/runs/shared/nested-events.ts +819 -0
  119. package/src/subagents/runs/shared/nested-path.ts +52 -0
  120. package/src/subagents/runs/shared/nested-render.ts +115 -0
  121. package/src/subagents/runs/shared/parallel-utils.ts +109 -0
  122. package/src/subagents/runs/shared/pi-args.ts +220 -0
  123. package/src/subagents/runs/shared/pi-spawn.ts +115 -0
  124. package/src/subagents/runs/shared/run-history.ts +60 -0
  125. package/src/subagents/runs/shared/single-output.ts +164 -0
  126. package/src/subagents/runs/shared/subagent-control.ts +226 -0
  127. package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
  128. package/src/subagents/runs/shared/worktree.ts +577 -0
  129. package/src/subagents/shared/artifacts.ts +98 -0
  130. package/src/subagents/shared/atomic-json.ts +16 -0
  131. package/src/subagents/shared/file-coalescer.ts +40 -0
  132. package/src/subagents/shared/fork-context.ts +76 -0
  133. package/src/subagents/shared/formatters.ts +133 -0
  134. package/src/subagents/shared/jsonl-writer.ts +81 -0
  135. package/src/subagents/shared/model-info.ts +78 -0
  136. package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
  137. package/src/subagents/shared/session-identity.ts +10 -0
  138. package/src/subagents/shared/session-tokens.ts +44 -0
  139. package/src/subagents/shared/settings.ts +397 -0
  140. package/src/subagents/shared/status-format.ts +49 -0
  141. package/src/subagents/shared/types.ts +822 -0
  142. package/src/subagents/shared/utils.ts +450 -0
  143. package/src/subagents/slash/prompt-template-bridge.ts +397 -0
  144. package/src/subagents/slash/slash-bridge.ts +174 -0
  145. package/src/subagents/slash/slash-commands.ts +528 -0
  146. package/src/subagents/slash/slash-live-state.ts +292 -0
  147. package/src/subagents/tui/render-helpers.ts +80 -0
  148. package/src/subagents/tui/render.ts +1358 -0
  149. package/templates/agents/local/supervisor.md +33 -0
  150. package/templates/agents/local/task-merger.md +27 -0
  151. package/templates/agents/local/task-reviewer.md +30 -0
  152. package/templates/agents/local/task-worker.md +34 -0
  153. package/templates/agents/supervisor-routing.md +92 -0
  154. package/templates/agents/supervisor.md +229 -0
  155. package/templates/agents/task-merger.md +214 -0
  156. package/templates/agents/task-reviewer.md +260 -0
  157. package/templates/agents/task-worker-segment.md +44 -0
  158. package/templates/agents/task-worker.md +557 -0
  159. package/templates/tasks/CONTEXT.md +30 -0
  160. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
  161. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
  162. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
  163. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
@@ -0,0 +1,918 @@
1
+ /**
2
+ * Core execution logic for running subagents
3
+ */
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import type { Message } from "@earendil-works/pi-ai";
8
+ import type { AgentConfig } from "../../agents/agents.ts";
9
+ import {
10
+ ensureArtifactsDir,
11
+ getArtifactPaths,
12
+ writeArtifact,
13
+ writeMetadata,
14
+ } from "../../shared/artifacts.ts";
15
+ import {
16
+ type AgentProgress,
17
+ type ArtifactPaths,
18
+ type ControlEvent,
19
+ type ModelAttempt,
20
+ type RunSyncOptions,
21
+ type SingleResult,
22
+ type Usage,
23
+ DEFAULT_MAX_OUTPUT,
24
+ INTERCOM_DETACH_REQUEST_EVENT,
25
+ INTERCOM_DETACH_RESPONSE_EVENT,
26
+ truncateOutput,
27
+ getSubagentDepthEnv,
28
+ } from "../../shared/types.ts";
29
+ import {
30
+ DEFAULT_CONTROL_CONFIG,
31
+ buildControlEvent,
32
+ claimControlNotification,
33
+ deriveActivityState,
34
+ shouldNotifyControlEvent,
35
+ } from "../shared/subagent-control.ts";
36
+ import {
37
+ getFinalOutput,
38
+ findLatestSessionFile,
39
+ detectSubagentError,
40
+ extractToolArgsPreview,
41
+ extractTextFromContent,
42
+ } from "../../shared/utils.ts";
43
+ import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
44
+ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
45
+ import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
46
+ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
47
+ import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
48
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
49
+ import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
50
+ import {
51
+ buildModelCandidates,
52
+ formatModelAttemptNote,
53
+ isRetryableModelFailure,
54
+ } from "../shared/model-fallback.ts";
55
+ import {
56
+ createMutatingFailureState,
57
+ didMutatingToolFail,
58
+ isMutatingTool,
59
+ nextLongRunningTrigger,
60
+ recordMutatingFailure,
61
+ resetMutatingFailureState,
62
+ resolveCurrentPath,
63
+ shouldEscalateMutatingFailures,
64
+ summarizeRecentMutatingFailures,
65
+ } from "../shared/long-running-guard.ts";
66
+
67
+ const artifactOutputByResult = new WeakMap<SingleResult, string>();
68
+
69
+ function emptyUsage(): Usage {
70
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
71
+ }
72
+
73
+ function sumUsage(target: Usage, source: Usage): void {
74
+ target.input += source.input;
75
+ target.output += source.output;
76
+ target.cacheRead += source.cacheRead;
77
+ target.cacheWrite += source.cacheWrite;
78
+ target.cost += source.cost;
79
+ target.turns += source.turns;
80
+ }
81
+
82
+ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
83
+ if (lines.length === 0) return;
84
+ progress.recentOutput.push(...lines.filter((line) => line.trim()));
85
+ if (progress.recentOutput.length > 50) {
86
+ progress.recentOutput.splice(0, progress.recentOutput.length - 50);
87
+ }
88
+ }
89
+
90
+ function snapshotProgress(progress: AgentProgress): AgentProgress {
91
+ return {
92
+ ...progress,
93
+ skills: progress.skills ? [...progress.skills] : undefined,
94
+ recentTools: progress.recentTools.map((tool) => ({ ...tool })),
95
+ recentOutput: [...progress.recentOutput],
96
+ };
97
+ }
98
+
99
+ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleResult {
100
+ return {
101
+ ...result,
102
+ messages: result.outputMode === "file-only" && result.savedOutputPath ? undefined : result.messages ? [...result.messages] : undefined,
103
+ usage: { ...result.usage },
104
+ skills: result.skills ? [...result.skills] : undefined,
105
+ attemptedModels: result.attemptedModels ? [...result.attemptedModels] : undefined,
106
+ modelAttempts: result.modelAttempts
107
+ ? result.modelAttempts.map((attempt) => ({
108
+ ...attempt,
109
+ usage: attempt.usage ? { ...attempt.usage } : undefined,
110
+ }))
111
+ : undefined,
112
+ controlEvents: result.controlEvents ? result.controlEvents.map((event) => ({ ...event })) : undefined,
113
+ progress,
114
+ progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
115
+ artifactPaths: result.artifactPaths ? { ...result.artifactPaths } : undefined,
116
+ truncation: result.truncation ? { ...result.truncation } : undefined,
117
+ outputReference: result.outputReference ? { ...result.outputReference } : undefined,
118
+ };
119
+ }
120
+
121
+ async function runSingleAttempt(
122
+ runtimeCwd: string,
123
+ agent: AgentConfig,
124
+ task: string,
125
+ model: string | undefined,
126
+ options: RunSyncOptions,
127
+ shared: {
128
+ sessionEnabled: boolean;
129
+ systemPrompt: string;
130
+ resolvedSkillNames?: string[];
131
+ skillsWarning?: string;
132
+ jsonlPath?: string;
133
+ artifactPaths?: ArtifactPaths;
134
+ attemptNotes: string[];
135
+ outputSnapshot?: SingleOutputSnapshot;
136
+ },
137
+ ): Promise<SingleResult> {
138
+ const modelArg = applyThinkingSuffix(model, agent.thinking);
139
+ const { args, env: sharedEnv, tempDir } = buildPiArgs({
140
+ baseArgs: ["--mode", "json", "-p"],
141
+ task,
142
+ sessionEnabled: shared.sessionEnabled,
143
+ sessionDir: options.sessionDir,
144
+ sessionFile: options.sessionFile,
145
+ model,
146
+ thinking: agent.thinking,
147
+ systemPromptMode: agent.systemPromptMode,
148
+ inheritProjectContext: agent.inheritProjectContext,
149
+ inheritSkills: agent.inheritSkills,
150
+ tools: agent.tools,
151
+ extensions: agent.extensions,
152
+ systemPrompt: shared.systemPrompt,
153
+ mcpDirectTools: agent.mcpDirectTools,
154
+ cwd: options.cwd ?? runtimeCwd,
155
+ promptFileStem: agent.name,
156
+ intercomSessionName: options.intercomSessionName,
157
+ orchestratorIntercomTarget: options.orchestratorIntercomTarget,
158
+ runId: options.runId,
159
+ childAgentName: agent.name,
160
+ childIndex: options.index ?? 0,
161
+ parentEventSink: options.nestedRoute?.eventSink,
162
+ parentControlInbox: options.nestedRoute?.controlInbox,
163
+ parentRootRunId: options.nestedRoute?.rootRunId,
164
+ parentCapabilityToken: options.nestedRoute?.capabilityToken,
165
+ });
166
+
167
+ const result: SingleResult = {
168
+ agent: agent.name,
169
+ task,
170
+ exitCode: 0,
171
+ messages: [],
172
+ usage: emptyUsage(),
173
+ model: modelArg,
174
+ artifactPaths: shared.artifactPaths,
175
+ skills: shared.resolvedSkillNames,
176
+ skillsWarning: shared.skillsWarning,
177
+ };
178
+ const startTime = Date.now();
179
+ const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
180
+ let interruptedByControl = false;
181
+ const allControlEvents: ControlEvent[] = [];
182
+ let pendingControlEvents: ControlEvent[] = [];
183
+ const emittedControlEventKeys = new Set<string>();
184
+ const emitControlEvent = (event: ControlEvent) => {
185
+ if (!shouldNotifyControlEvent(controlConfig, event)) return;
186
+ if (!claimControlNotification(controlConfig, event, emittedControlEventKeys)) return;
187
+ allControlEvents.push(event);
188
+ pendingControlEvents.push(event);
189
+ options.onControlEvent?.(event);
190
+ };
191
+
192
+ const progress: AgentProgress = {
193
+ index: options.index ?? 0,
194
+ agent: agent.name,
195
+ status: "running",
196
+ task,
197
+ skills: shared.resolvedSkillNames,
198
+ recentTools: [],
199
+ recentOutput: [...shared.attemptNotes],
200
+ toolCount: 0,
201
+ tokens: 0,
202
+ durationMs: 0,
203
+ lastActivityAt: startTime,
204
+ };
205
+ result.progress = progress;
206
+ const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
207
+ let observedMutationAttempt = false;
208
+
209
+ const exitCode = await new Promise<number>((resolve) => {
210
+ const spawnSpec = getPiSpawnCommand(args);
211
+ const proc = spawn(spawnSpec.command, spawnSpec.args, {
212
+ cwd: options.cwd ?? runtimeCwd,
213
+ env: spawnEnv,
214
+ stdio: ["ignore", "pipe", "pipe"],
215
+ windowsHide: true,
216
+ });
217
+ const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
218
+ let buf = "";
219
+ let processClosed = false;
220
+ let settled = false;
221
+ let detached = false;
222
+ let intercomStarted = false;
223
+ let assistantError: string | undefined;
224
+ let removeAbortListener: (() => void) | undefined;
225
+ let removeInterruptListener: (() => void) | undefined;
226
+ let activityTimer: NodeJS.Timeout | undefined;
227
+
228
+ const detachForIntercom = () => {
229
+ detached = true;
230
+ processClosed = true;
231
+ result.detached = true;
232
+ result.detachedReason = "intercom coordination";
233
+ progress.status = "detached";
234
+ progress.durationMs = Date.now() - startTime;
235
+ result.progressSummary = {
236
+ toolCount: progress.toolCount,
237
+ tokens: progress.tokens,
238
+ durationMs: progress.durationMs,
239
+ };
240
+ finish(-2);
241
+ };
242
+
243
+ // If the child emits a terminal assistant stop but never exits,
244
+ // give it a short grace period to flush naturally, then clean it up.
245
+ const FINAL_STOP_GRACE_MS = 1000;
246
+ const HARD_KILL_MS = 3000;
247
+ let childExited = false;
248
+ let forcedTerminationSignal = false;
249
+ let cleanTerminalAssistantStopReceived = false;
250
+ let finalDrainTimer: NodeJS.Timeout | undefined;
251
+ let finalHardKillTimer: NodeJS.Timeout | undefined;
252
+ const clearFinalDrainTimers = () => {
253
+ if (finalDrainTimer) {
254
+ clearTimeout(finalDrainTimer);
255
+ finalDrainTimer = undefined;
256
+ }
257
+ if (finalHardKillTimer) {
258
+ clearTimeout(finalHardKillTimer);
259
+ finalHardKillTimer = undefined;
260
+ }
261
+ };
262
+ const startFinalDrain = () => {
263
+ if (childExited || finalDrainTimer || settled || processClosed || detached) return;
264
+ finalDrainTimer = setTimeout(() => {
265
+ if (settled || processClosed || detached) return;
266
+ const termSent = trySignalChild(proc, "SIGTERM");
267
+ if (!termSent) return;
268
+ forcedTerminationSignal = true;
269
+ if (!cleanTerminalAssistantStopReceived && !assistantError) {
270
+ result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
271
+ }
272
+ finalHardKillTimer = setTimeout(() => {
273
+ if (settled || processClosed || detached) return;
274
+ forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
275
+ }, HARD_KILL_MS);
276
+ finalHardKillTimer.unref?.();
277
+ }, FINAL_STOP_GRACE_MS);
278
+ finalDrainTimer.unref?.();
279
+ };
280
+
281
+ const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
282
+ if (!options.allowIntercomDetach || detached || processClosed || !intercomStarted) return;
283
+ if (!payload || typeof payload !== "object") return;
284
+ const requestId = (payload as { requestId?: unknown }).requestId;
285
+ if (typeof requestId !== "string" || requestId.length === 0) return;
286
+ options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
287
+ detachForIntercom();
288
+ });
289
+
290
+ const finish = (code: number) => {
291
+ if (settled) return;
292
+ settled = true;
293
+ clearFinalDrainTimers();
294
+ clearStdioGuard();
295
+ if (activityTimer) {
296
+ clearInterval(activityTimer);
297
+ activityTimer = undefined;
298
+ }
299
+ unsubscribeIntercomDetach?.();
300
+ removeAbortListener?.();
301
+ removeInterruptListener?.();
302
+ resolve(code);
303
+ };
304
+
305
+ const drainPendingControlEvents = (): ControlEvent[] | undefined => {
306
+ if (pendingControlEvents.length === 0) return undefined;
307
+ const events = pendingControlEvents;
308
+ pendingControlEvents = [];
309
+ return events;
310
+ };
311
+
312
+ let activeLongRunningNotified = false;
313
+ let pendingToolResult: { tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined;
314
+ const mutatingFailures = createMutatingFailureState();
315
+ const mutatingFailureWindowMs = 5 * 60_000;
316
+ const currentToolDurationMs = (now: number) => progress.currentToolStartedAt ? Math.max(0, now - progress.currentToolStartedAt) : undefined;
317
+ const emitNeedsAttention = (now: number, input: { message?: string; reason?: ControlEvent["reason"]; recentFailureSummary?: string; currentTool?: string; currentPath?: string; currentToolDurationMs?: number } = {}): boolean => {
318
+ if (!controlConfig.enabled) return false;
319
+ const previous = progress.activityState;
320
+ progress.activityState = "needs_attention";
321
+ const event = buildControlEvent({
322
+ type: "needs_attention",
323
+ from: previous,
324
+ to: "needs_attention",
325
+ runId: options.runId,
326
+ agent: agent.name,
327
+ index: options.index,
328
+ ts: now,
329
+ lastActivityAt: progress.lastActivityAt,
330
+ message: input.message,
331
+ reason: input.reason ?? "idle",
332
+ turns: result.usage.turns,
333
+ tokens: progress.tokens,
334
+ toolCount: progress.toolCount,
335
+ currentTool: input.currentTool ?? progress.currentTool,
336
+ currentToolDurationMs: input.currentToolDurationMs ?? currentToolDurationMs(now),
337
+ currentPath: input.currentPath ?? progress.currentPath,
338
+ recentFailureSummary: input.recentFailureSummary,
339
+ });
340
+ emitControlEvent(event);
341
+ return previous !== "needs_attention";
342
+ };
343
+ const emitActiveLongRunning = (now: number, reason: ControlEvent["reason"]): boolean => {
344
+ if (!controlConfig.enabled || activeLongRunningNotified || progress.activityState === "needs_attention") return false;
345
+ activeLongRunningNotified = true;
346
+ const previous = progress.activityState;
347
+ progress.activityState = "active_long_running";
348
+ emitControlEvent(buildControlEvent({
349
+ type: "active_long_running",
350
+ from: previous,
351
+ to: "active_long_running",
352
+ runId: options.runId,
353
+ agent: agent.name,
354
+ index: options.index,
355
+ ts: now,
356
+ message: `${agent.name} is still active but long-running`,
357
+ reason,
358
+ turns: result.usage.turns,
359
+ tokens: progress.tokens,
360
+ toolCount: progress.toolCount,
361
+ currentTool: progress.currentTool,
362
+ currentToolDurationMs: currentToolDurationMs(now),
363
+ currentPath: progress.currentPath,
364
+ elapsedMs: now - startTime,
365
+ }));
366
+ return true;
367
+ };
368
+ const updateActivityState = (now: number): boolean => {
369
+ if (!controlConfig.enabled) return false;
370
+ const idleState = deriveActivityState({
371
+ config: controlConfig,
372
+ startedAt: startTime,
373
+ lastActivityAt: progress.lastActivityAt,
374
+ now,
375
+ });
376
+ if (idleState === "needs_attention") {
377
+ return progress.activityState === "needs_attention" ? false : emitNeedsAttention(now);
378
+ }
379
+ const activeReason = nextLongRunningTrigger(controlConfig, {
380
+ startedAt: startTime,
381
+ now,
382
+ turns: result.usage.turns,
383
+ tokens: progress.tokens,
384
+ });
385
+ return activeReason ? emitActiveLongRunning(now, activeReason) : false;
386
+ };
387
+
388
+
389
+ const emitUpdateSnapshot = (text: string) => {
390
+ if (!options.onUpdate || processClosed) return;
391
+ const progressSnapshot = snapshotProgress(progress);
392
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
393
+ const controlEvents = drainPendingControlEvents();
394
+ options.onUpdate({
395
+ content: [{ type: "text", text }],
396
+ details: {
397
+ mode: "single",
398
+ results: [resultSnapshot],
399
+ progress: [progressSnapshot],
400
+ controlEvents,
401
+ },
402
+ });
403
+ };
404
+
405
+ const fireUpdate = () => {
406
+ if (!options.onUpdate || processClosed) return;
407
+ progress.durationMs = Date.now() - startTime;
408
+ emitUpdateSnapshot(getFinalOutput(result.messages) || "(running...)");
409
+ };
410
+
411
+ const processLine = (line: string) => {
412
+ if (!line.trim()) return;
413
+ jsonlWriter.writeLine(line);
414
+ let evt: { type?: string; message?: Message; toolName?: string; args?: unknown };
415
+ try {
416
+ evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
417
+ } catch {
418
+ // Non-JSON stdout lines are expected; only structured events are parsed.
419
+ return;
420
+ }
421
+
422
+ const now = Date.now();
423
+ progress.durationMs = now - startTime;
424
+ progress.lastActivityAt = now;
425
+ updateActivityState(now);
426
+
427
+ if (evt.type === "tool_execution_start") {
428
+ const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
429
+ ? evt.args as Record<string, unknown>
430
+ : {};
431
+ if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
432
+ intercomStarted = true;
433
+ }
434
+ progress.toolCount++;
435
+ progress.currentTool = evt.toolName;
436
+ progress.currentToolArgs = extractToolArgsPreview(toolArgs);
437
+ progress.currentToolStartedAt = now;
438
+ progress.currentPath = resolveCurrentPath(evt.toolName, toolArgs);
439
+ const mutates = isMutatingTool(evt.toolName, toolArgs);
440
+ observedMutationAttempt = observedMutationAttempt || mutates;
441
+ pendingToolResult = { tool: evt.toolName ?? "tool", path: progress.currentPath, mutates, startedAt: now };
442
+ fireUpdate();
443
+ }
444
+
445
+ if (evt.type === "tool_execution_end") {
446
+ if (progress.currentTool) {
447
+ progress.recentTools.push({
448
+ tool: progress.currentTool,
449
+ args: progress.currentToolArgs || "",
450
+ endMs: now,
451
+ });
452
+ }
453
+ progress.currentTool = undefined;
454
+ progress.currentToolArgs = undefined;
455
+ progress.currentToolStartedAt = undefined;
456
+ progress.currentPath = undefined;
457
+ fireUpdate();
458
+ }
459
+
460
+ if (evt.type === "message_end" && evt.message) {
461
+ result.messages.push(evt.message);
462
+ if (evt.message.role === "assistant") {
463
+ result.usage.turns++;
464
+ progress.turnCount = result.usage.turns;
465
+ const u = evt.message.usage;
466
+ if (u) {
467
+ result.usage.input += u.input || 0;
468
+ result.usage.output += u.output || 0;
469
+ result.usage.cacheRead += u.cacheRead || 0;
470
+ result.usage.cacheWrite += u.cacheWrite || 0;
471
+ result.usage.cost += u.cost?.total || 0;
472
+ progress.tokens = result.usage.input + result.usage.output;
473
+ }
474
+ if (!result.model && evt.message.model) result.model = evt.message.model;
475
+ if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
476
+ const assistantText = extractTextFromContent(evt.message.content);
477
+ appendRecentOutput(progress, assistantText.split("\n").slice(-10));
478
+ // Final assistant message: start the exit drain window.
479
+ const stopReason = (evt.message as { stopReason?: string }).stopReason;
480
+ const hasToolCall = Array.isArray(evt.message.content)
481
+ && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
482
+ if (stopReason === "stop" && !hasToolCall) {
483
+ if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
484
+ cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
485
+ startFinalDrain();
486
+ }
487
+ }
488
+ updateActivityState(now);
489
+ fireUpdate();
490
+ }
491
+
492
+ if (evt.type === "tool_result_end" && evt.message) {
493
+ result.messages.push(evt.message);
494
+ const resultText = extractTextFromContent(evt.message.content);
495
+ appendRecentOutput(progress, resultText.split("\n").slice(-10));
496
+ const toolSnapshot = pendingToolResult;
497
+ pendingToolResult = undefined;
498
+ if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
499
+ recordMutatingFailure(mutatingFailures, {
500
+ tool: toolSnapshot.tool,
501
+ path: toolSnapshot.path,
502
+ error: resultText.split("\n").find((line) => line.trim())?.trim().slice(0, 180) ?? "mutating tool failed",
503
+ ts: now,
504
+ }, mutatingFailureWindowMs);
505
+ if (shouldEscalateMutatingFailures(mutatingFailures, controlConfig.failedToolAttemptsBeforeAttention)) {
506
+ emitNeedsAttention(now, {
507
+ message: `${agent.name} needs attention after repeated mutating tool failures`,
508
+ reason: "tool_failures",
509
+ currentTool: toolSnapshot.tool,
510
+ currentPath: toolSnapshot.path,
511
+ currentToolDurationMs: toolSnapshot.startedAt ? Math.max(0, now - toolSnapshot.startedAt) : undefined,
512
+ recentFailureSummary: summarizeRecentMutatingFailures(mutatingFailures),
513
+ });
514
+ }
515
+ } else if (toolSnapshot?.mutates) {
516
+ resetMutatingFailureState(mutatingFailures);
517
+ }
518
+ fireUpdate();
519
+ }
520
+ };
521
+
522
+ if (controlConfig.enabled) {
523
+ activityTimer = setInterval(() => {
524
+ if (processClosed || settled || detached) return;
525
+ const now = Date.now();
526
+ if (updateActivityState(now)) {
527
+ progress.durationMs = now - startTime;
528
+ fireUpdate();
529
+ }
530
+ }, 1000);
531
+ activityTimer.unref?.();
532
+ }
533
+
534
+ let stderrBuf = "";
535
+
536
+ const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
537
+ proc.stdout.on("data", (d) => {
538
+ buf += d.toString();
539
+ const lines = buf.split("\n");
540
+ buf = lines.pop() || "";
541
+ lines.forEach(processLine);
542
+ });
543
+ proc.stderr.on("data", (d) => {
544
+ stderrBuf += d.toString();
545
+ });
546
+ proc.on("exit", () => {
547
+ childExited = true;
548
+ clearFinalDrainTimers();
549
+ });
550
+ proc.on("close", (code, signal) => {
551
+ clearFinalDrainTimers();
552
+ clearStdioGuard();
553
+ void jsonlWriter.close().catch(() => {
554
+ // JSONL artifact flush is best effort.
555
+ });
556
+ cleanupTempDir(tempDir);
557
+ if (detached) {
558
+ finish(-2);
559
+ return;
560
+ }
561
+ processClosed = true;
562
+ if (buf.trim()) processLine(buf);
563
+ if (!result.error && assistantError) result.error = assistantError;
564
+ const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !result.error;
565
+ if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
566
+ result.error = stderrBuf.trim();
567
+ }
568
+ const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
569
+ finish(finalCode);
570
+ });
571
+ proc.on("error", (error) => {
572
+ clearFinalDrainTimers();
573
+ clearStdioGuard();
574
+ void jsonlWriter.close().catch(() => {
575
+ // JSONL artifact flush is best effort.
576
+ });
577
+ cleanupTempDir(tempDir);
578
+ if (!result.error) {
579
+ result.error = error instanceof Error ? error.message : String(error);
580
+ }
581
+ finish(1);
582
+ });
583
+
584
+ if (options.signal) {
585
+ const kill = () => {
586
+ if (processClosed || detached) return;
587
+ if (options.allowIntercomDetach && intercomStarted && !detached) {
588
+ detachForIntercom();
589
+ return;
590
+ }
591
+ proc.kill("SIGTERM");
592
+ setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
593
+ };
594
+ if (options.signal.aborted) kill();
595
+ else {
596
+ options.signal.addEventListener("abort", kill, { once: true });
597
+ removeAbortListener = () => options.signal?.removeEventListener("abort", kill);
598
+ }
599
+ }
600
+
601
+ if (options.interruptSignal) {
602
+ const interrupt = () => {
603
+ if (processClosed || detached || settled) return;
604
+ interruptedByControl = true;
605
+ progress.status = "running";
606
+ progress.durationMs = Date.now() - startTime;
607
+ result.interrupted = true;
608
+ result.finalOutput = "Interrupted. Waiting for explicit next action.";
609
+ progress.activityState = undefined;
610
+ fireUpdate();
611
+ trySignalChild(proc, "SIGINT");
612
+ setTimeout(() => {
613
+ if (settled || processClosed || detached) return;
614
+ trySignalChild(proc, "SIGTERM");
615
+ }, 1000).unref?.();
616
+ };
617
+ if (options.interruptSignal.aborted) interrupt();
618
+ else {
619
+ options.interruptSignal.addEventListener("abort", interrupt, { once: true });
620
+ removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
621
+ }
622
+ }
623
+ });
624
+ result.exitCode = exitCode;
625
+ if (interruptedByControl) {
626
+ result.exitCode = 0;
627
+ result.interrupted = true;
628
+ result.error = undefined;
629
+ result.finalOutput = result.finalOutput || "Interrupted. Waiting for explicit next action.";
630
+ result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
631
+ progress.activityState = undefined;
632
+ progress.durationMs = Date.now() - startTime;
633
+ result.progressSummary = {
634
+ toolCount: progress.toolCount,
635
+ tokens: progress.tokens,
636
+ durationMs: progress.durationMs,
637
+ };
638
+ return result;
639
+ }
640
+ if (result.detached) {
641
+ result.exitCode = 0;
642
+ result.finalOutput = "Detached for intercom coordination.";
643
+ return result;
644
+ }
645
+
646
+ if (result.error && result.exitCode === 0) {
647
+ result.exitCode = 1;
648
+ }
649
+ if (result.exitCode === 0 && !result.error) {
650
+ const errInfo = detectSubagentError(result.messages);
651
+ if (errInfo.hasError) {
652
+ result.exitCode = errInfo.exitCode ?? 1;
653
+ result.error = errInfo.details
654
+ ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
655
+ : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
656
+ }
657
+ }
658
+
659
+ progress.status = result.exitCode === 0 ? "completed" : "failed";
660
+ progress.durationMs = Date.now() - startTime;
661
+ if (result.error) {
662
+ progress.error = result.error;
663
+ if (progress.currentTool) {
664
+ progress.failedTool = progress.currentTool;
665
+ }
666
+ }
667
+
668
+ result.progressSummary = {
669
+ toolCount: progress.toolCount,
670
+ tokens: progress.tokens,
671
+ durationMs: progress.durationMs,
672
+ };
673
+
674
+ let fullOutput = getFinalOutput(result.messages);
675
+ const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
676
+ ? evaluateCompletionMutationGuard({
677
+ agent: agent.name,
678
+ task,
679
+ messages: result.messages,
680
+ tools: agent.tools,
681
+ mcpDirectTools: agent.mcpDirectTools,
682
+ })
683
+ : undefined;
684
+ if (completionGuard?.triggered && !observedMutationAttempt) {
685
+ result.exitCode = 1;
686
+ result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
687
+ progress.status = "failed";
688
+ progress.error = result.error;
689
+ emitControlEvent(buildControlEvent({
690
+ from: progress.activityState,
691
+ to: "needs_attention",
692
+ runId: options.runId ?? agent.name,
693
+ agent: agent.name,
694
+ index: options.index,
695
+ ts: Date.now(),
696
+ message: `${agent.name} completed without making edits for an implementation task`,
697
+ reason: "completion_guard",
698
+ }));
699
+ }
700
+ if (options.outputPath && result.exitCode === 0) {
701
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
702
+ fullOutput = resolvedOutput.fullOutput;
703
+ result.savedOutputPath = resolvedOutput.savedPath;
704
+ result.outputSaveError = resolvedOutput.saveError;
705
+ if (resolvedOutput.savedPath) {
706
+ result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
707
+ }
708
+ }
709
+ artifactOutputByResult.set(result, fullOutput);
710
+ result.outputMode = options.outputMode ?? "inline";
711
+ result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
712
+ ? result.outputReference.message
713
+ : fullOutput;
714
+ result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
715
+ if (options.onUpdate) {
716
+ const finalText = result.finalOutput || result.error || "(no output)";
717
+ const progressSnapshot = snapshotProgress(progress);
718
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
719
+ options.onUpdate({
720
+ content: [{ type: "text", text: finalText }],
721
+ details: {
722
+ mode: "single",
723
+ results: [resultSnapshot],
724
+ progress: [progressSnapshot],
725
+ controlEvents: allControlEvents.length ? allControlEvents : undefined,
726
+ },
727
+ });
728
+ }
729
+ return result;
730
+ }
731
+
732
+ /**
733
+ * Run a subagent synchronously (blocking until complete)
734
+ */
735
+ export async function runSync(
736
+ runtimeCwd: string,
737
+ agents: AgentConfig[],
738
+ agentName: string,
739
+ task: string,
740
+ options: RunSyncOptions,
741
+ ): Promise<SingleResult> {
742
+ const agent = agents.find((a) => a.name === agentName);
743
+ if (!agent) {
744
+ return {
745
+ agent: agentName,
746
+ task,
747
+ exitCode: 1,
748
+ messages: [],
749
+ usage: emptyUsage(),
750
+ error: `Unknown agent: ${agentName}`,
751
+ };
752
+ }
753
+ const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
754
+ if (outputModeValidationError) {
755
+ return {
756
+ agent: agentName,
757
+ task,
758
+ exitCode: 1,
759
+ messages: [],
760
+ usage: emptyUsage(),
761
+ outputMode: options.outputMode,
762
+ error: outputModeValidationError,
763
+ };
764
+ }
765
+
766
+ const shareEnabled = options.share === true;
767
+ const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
768
+ const skillNames = options.skills ?? agent.skills ?? [];
769
+ const skillCwd = options.cwd ?? runtimeCwd;
770
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
771
+ if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
772
+ return {
773
+ agent: agentName,
774
+ task,
775
+ exitCode: 1,
776
+ messages: [],
777
+ usage: emptyUsage(),
778
+ error: "Skills not found: pi-subagents",
779
+ };
780
+ }
781
+ let systemPrompt = agent.systemPrompt?.trim() || "";
782
+ if (resolvedSkills.length > 0) {
783
+ const skillInjection = buildSkillInjection(resolvedSkills);
784
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
785
+ }
786
+
787
+ const candidates = buildModelCandidates(
788
+ options.modelOverride ?? agent.model,
789
+ agent.fallbackModels,
790
+ options.availableModels,
791
+ options.preferredModelProvider,
792
+ );
793
+ const attemptedModels: string[] = [];
794
+ const modelAttempts: ModelAttempt[] = [];
795
+ const aggregateUsage = emptyUsage();
796
+ const attemptNotes: string[] = [];
797
+ let totalToolCount = 0;
798
+ let totalDurationMs = 0;
799
+
800
+ let artifactPathsResult: ArtifactPaths | undefined;
801
+ let jsonlPath: string | undefined;
802
+ if (options.artifactsDir && options.artifactConfig?.enabled !== false) {
803
+ artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
804
+ ensureArtifactsDir(options.artifactsDir);
805
+ if (options.artifactConfig?.includeInput !== false) {
806
+ writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
807
+ }
808
+ if (options.artifactConfig?.includeJsonl !== false) {
809
+ jsonlPath = artifactPathsResult.jsonlPath;
810
+ }
811
+ }
812
+
813
+ let lastResult: SingleResult | undefined;
814
+ const modelsToTry = candidates.length > 0 ? candidates : [undefined];
815
+ for (let i = 0; i < modelsToTry.length; i++) {
816
+ const candidate = modelsToTry[i];
817
+ if (candidate) attemptedModels.push(candidate);
818
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
819
+ const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
820
+ sessionEnabled,
821
+ systemPrompt,
822
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
823
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
824
+ jsonlPath,
825
+ artifactPaths: artifactPathsResult,
826
+ attemptNotes,
827
+ outputSnapshot,
828
+ });
829
+ lastResult = result;
830
+ sumUsage(aggregateUsage, result.usage);
831
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
832
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
833
+ const attemptSucceeded = result.exitCode === 0 && !result.error;
834
+ const attempt: ModelAttempt = {
835
+ model: candidate ?? result.model ?? agent.model ?? "default",
836
+ success: attemptSucceeded,
837
+ exitCode: result.exitCode,
838
+ error: result.error,
839
+ usage: { ...result.usage },
840
+ };
841
+ modelAttempts.push(attempt);
842
+ if (attemptSucceeded) {
843
+ break;
844
+ }
845
+ if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
846
+ break;
847
+ }
848
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
849
+ }
850
+
851
+ const result = lastResult ?? {
852
+ agent: agentName,
853
+ task,
854
+ exitCode: 1,
855
+ messages: [],
856
+ usage: emptyUsage(),
857
+ error: "Subagent did not produce a result.",
858
+ } satisfies SingleResult;
859
+
860
+ result.usage = aggregateUsage;
861
+ result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
862
+ result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
863
+ result.progressSummary = {
864
+ toolCount: totalToolCount,
865
+ tokens: aggregateUsage.input + aggregateUsage.output,
866
+ durationMs: totalDurationMs,
867
+ };
868
+ if (attemptNotes.length > 0 && result.progress) {
869
+ result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
870
+ if (result.progress.recentOutput.length > 50) {
871
+ result.progress.recentOutput.splice(50);
872
+ }
873
+ }
874
+
875
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
876
+ result.artifactPaths = artifactPathsResult;
877
+ if (options.artifactConfig?.includeOutput !== false) {
878
+ writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
879
+ }
880
+ if (options.artifactConfig?.includeMetadata !== false) {
881
+ writeMetadata(artifactPathsResult.metadataPath, {
882
+ runId: options.runId,
883
+ agent: agentName,
884
+ task,
885
+ exitCode: result.exitCode,
886
+ usage: result.usage,
887
+ model: result.model,
888
+ attemptedModels: result.attemptedModels,
889
+ modelAttempts: result.modelAttempts,
890
+ durationMs: result.progressSummary?.durationMs,
891
+ toolCount: result.progressSummary?.toolCount,
892
+ error: result.error,
893
+ skills: result.skills,
894
+ skillsWarning: result.skillsWarning,
895
+ timestamp: Date.now(),
896
+ });
897
+ }
898
+
899
+ if (options.maxOutput) {
900
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
901
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
902
+ if (truncationResult.truncated) result.truncation = truncationResult;
903
+ }
904
+ } else if (options.maxOutput) {
905
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
906
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config);
907
+ if (truncationResult.truncated) result.truncation = truncationResult;
908
+ }
909
+
910
+ if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
911
+ result.sessionFile = options.sessionFile;
912
+ } else if (shareEnabled && options.sessionDir) {
913
+ const sessionFile = findLatestSessionFile(options.sessionDir);
914
+ if (sessionFile) result.sessionFile = sessionFile;
915
+ }
916
+
917
+ return result;
918
+ }