@mystilleef/pi-subagent 0.5.0 → 0.7.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.
@@ -7,23 +7,27 @@
7
7
  import { type ChildProcess, spawn } from "node:child_process";
8
8
  import * as fs from "node:fs";
9
9
  import readline from "node:readline";
10
- import { getModel, type Message } from "@earendil-works/pi-ai";
11
- import type { AgentConfig, ThinkingLevel } from "./agents.js";
12
- import { parseChildEventLine } from "./child-events.js";
13
- import { makeToolPreview } from "./progress.js";
14
- import { isToolCallPart } from "./progress-state.js";
15
- import { appendSubagentResultContract } from "./prompt-contract.js";
16
10
  import {
17
- getProcessTreeSpawnOptions,
18
- terminateChildProcess,
19
- } from "./termination.js";
11
+ clampThinkingLevel,
12
+ getModel,
13
+ getSupportedThinkingLevels,
14
+ type Message,
15
+ type ModelThinkingLevel,
16
+ } from "@earendil-works/pi-ai";
17
+ import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
18
+ import { getFinalOutput } from "../output/ui.js";
19
+ import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
20
+ import {
21
+ isToolCallPart,
22
+ SENSITIVE_PATTERN,
23
+ } from "../progress/progress-state.js";
20
24
  import type {
21
25
  OnUpdateCallback,
22
26
  SingleResult,
23
27
  StreamingProgress,
24
28
  SubagentDetails,
25
- } from "./types.js";
26
- import { getFinalOutput } from "./ui.js";
29
+ ToolActivity,
30
+ } from "../shared/types.js";
27
31
  import {
28
32
  detectMessageError,
29
33
  getPiInvocation,
@@ -32,16 +36,59 @@ import {
32
36
  subagentDepthEnv,
33
37
  truncateOutput,
34
38
  writePromptToTempFile,
35
- } from "./utils.js";
39
+ } from "../shared/utils.js";
40
+ import {
41
+ type ChildKnownEvent,
42
+ parseChildEventLine,
43
+ TOOL_EXECUTION_UPDATE_EVENT,
44
+ } from "./child-events.js";
45
+ import { appendSubagentResultContract } from "./prompt-contract.js";
46
+ import {
47
+ getProcessTreeSpawnOptions,
48
+ terminateChildProcess,
49
+ } from "./termination.js";
36
50
 
37
51
  const MAX_STDERR_BYTES = 10_000;
38
52
  const AGENT_END_GRACE_MS = 250;
53
+ export function resolveThinkingLevel(
54
+ requested: ThinkingLevel,
55
+ provider: string,
56
+ modelId: string,
57
+ ): { level: ThinkingLevel; warning?: string } {
58
+ const model = getModel(provider as never, modelId as never);
59
+ if (!model) return { level: requested };
60
+ const mkWarning = (effective: ThinkingLevel) =>
61
+ `Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
62
+ if (model.reasoning === false) {
63
+ return { level: "off", warning: mkWarning("off") };
64
+ }
65
+ if (!model.thinkingLevelMap) return { level: requested };
66
+ const supported = getSupportedThinkingLevels(model);
67
+ if (supported.length === 0) return { level: requested };
68
+ const clamped = clampThinkingLevel(
69
+ model,
70
+ requested as ModelThinkingLevel,
71
+ ) as ThinkingLevel;
72
+ if (clamped === requested) return { level: requested };
73
+ return { level: clamped, warning: mkWarning(clamped) };
74
+ }
39
75
 
40
- const MAX_SUBAGENT_DEPTH = 1;
76
+ const MAX_SUBAGENT_DEPTH = 2;
41
77
  export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
42
78
 
43
79
  type RuntimeResult = SingleResult & { messages: Message[] };
44
80
 
81
+ export class SubagentAbortError extends Error {
82
+ constructor(public readonly result: SingleResult) {
83
+ super("Subagent was aborted");
84
+ this.name = "SubagentAbortError";
85
+ }
86
+ }
87
+
88
+ type TempPrompt = { dir: string; filePath: string };
89
+
90
+ type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
91
+
45
92
  interface SubagentState {
46
93
  result: RuntimeResult;
47
94
  spawnError?: Error;
@@ -50,10 +97,6 @@ interface SubagentState {
50
97
  terminationPromise?: Promise<unknown>;
51
98
  }
52
99
 
53
- /**
54
- * Appends data to a string while ensuring the result does not exceed a maximum byte limit.
55
- * Used to prevent memory exhaustion when capturing child process stderr.
56
- */
57
100
  function appendWithByteLimit(
58
101
  current: string,
59
102
  data: string,
@@ -72,7 +115,10 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
72
115
  const m = msg as unknown as Record<string, unknown>;
73
116
  if (typeof m.provider !== "string" || typeof m.model !== "string") return;
74
117
  try {
75
- const { contextWindow } = getModel(m.provider as never, m.model as never);
118
+ const contextWindow = getModel(
119
+ m.provider as never,
120
+ m.model as never,
121
+ )?.contextWindow;
76
122
  return Number.isFinite(contextWindow) && contextWindow > 0
77
123
  ? contextWindow
78
124
  : undefined;
@@ -81,9 +127,6 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
81
127
  }
82
128
  }
83
129
 
84
- /**
85
- * Normalizes AbortSignal reasons into human-readable strings.
86
- */
87
130
  function getAbortReason(signal: AbortSignal): string {
88
131
  const { reason } = signal;
89
132
  if (reason instanceof Error && reason.message) return reason.message;
@@ -129,6 +172,25 @@ function getAgentEndTimeoutExitCode(
129
172
  * Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
130
173
  * are destroyed and promises settled even if the process or its pipes hang.
131
174
  */
175
+ function createProcessCleanup(proc: ChildProcess, idleMs: number) {
176
+ let idleTimer: NodeJS.Timeout | undefined;
177
+ const destroyStreams = () => {
178
+ proc.stdout?.destroy();
179
+ proc.stderr?.destroy();
180
+ };
181
+ return {
182
+ armIdleTimer: () => {
183
+ if (idleTimer) clearTimeout(idleTimer);
184
+ idleTimer = setTimeout(destroyStreams, idleMs);
185
+ idleTimer.unref?.();
186
+ },
187
+ clearIdleTimer: () => {
188
+ if (idleTimer) clearTimeout(idleTimer);
189
+ },
190
+ destroyStreams,
191
+ };
192
+ }
193
+
132
194
  async function waitForSubagentProcess(
133
195
  proc: ChildProcess,
134
196
  idleMs = 100,
@@ -138,27 +200,13 @@ async function waitForSubagentProcess(
138
200
  let exitCode: number | null = null;
139
201
  let exited = false;
140
202
  let settled = false;
141
- let idleTimer: NodeJS.Timeout | undefined;
142
-
203
+ const cleanup = createProcessCleanup(proc, idleMs);
143
204
  const done = () => {
144
205
  if (settled) return;
145
206
  settled = true;
146
- if (idleTimer) clearTimeout(idleTimer);
207
+ cleanup.clearIdleTimer();
147
208
  resolve(exitCode);
148
209
  };
149
-
150
- const destroyStreams = () => {
151
- proc.stdout?.destroy();
152
- proc.stderr?.destroy();
153
- };
154
-
155
- const armIdleTimer = () => {
156
- if (!exited) return;
157
- if (idleTimer) clearTimeout(idleTimer);
158
- idleTimer = setTimeout(destroyStreams, idleMs);
159
- idleTimer.unref?.();
160
- };
161
-
162
210
  proc.on("close", done);
163
211
  proc.on("error", () => {
164
212
  exitCode = 1;
@@ -168,12 +216,15 @@ async function waitForSubagentProcess(
168
216
  proc.on("exit", (code) => {
169
217
  exitCode = code;
170
218
  exited = true;
171
- armIdleTimer();
172
- const hardTimer = setTimeout(destroyStreams, hardMs);
219
+ cleanup.armIdleTimer();
220
+ const hardTimer = setTimeout(cleanup.destroyStreams, hardMs);
173
221
  hardTimer.unref?.();
174
222
  });
175
- proc.stdout?.on("data", armIdleTimer);
176
- proc.stderr?.on("data", armIdleTimer);
223
+ const onStreamData = () => {
224
+ if (exited) cleanup.armIdleTimer();
225
+ };
226
+ proc.stdout?.on("data", onStreamData);
227
+ proc.stderr?.on("data", onStreamData);
177
228
  });
178
229
  }
179
230
 
@@ -187,6 +238,16 @@ function buildModelDisplay(
187
238
  return thinking ? `thinking:${thinking}` : undefined;
188
239
  }
189
240
 
241
+ const EMPTY_USAGE = {
242
+ input: 0,
243
+ output: 0,
244
+ cacheRead: 0,
245
+ cacheWrite: 0,
246
+ cost: 0,
247
+ contextTokens: 0,
248
+ turns: 0,
249
+ };
250
+
190
251
  function initRuntimeResult(
191
252
  agentName: string,
192
253
  source: "user" | "project" | "unknown",
@@ -201,52 +262,42 @@ function initRuntimeResult(
201
262
  finalOutput: "",
202
263
  messages: [],
203
264
  stderr: "",
204
- usage: {
205
- input: 0,
206
- output: 0,
207
- cacheRead: 0,
208
- cacheWrite: 0,
209
- cost: 0,
210
- contextTokens: 0,
211
- turns: 0,
212
- },
265
+ usage: { ...EMPTY_USAGE },
213
266
  model: modelDisplay,
214
267
  };
215
268
  }
216
269
 
270
+ function accumulateUsage(result: RuntimeResult, msg: Message): void {
271
+ if (msg.role !== "assistant") return;
272
+ result.usage.turns++;
273
+ const { usage } = msg;
274
+ if (!usage) return;
275
+ result.usage.input += usage.input || 0;
276
+ result.usage.output += usage.output || 0;
277
+ result.usage.cacheRead += usage.cacheRead || 0;
278
+ result.usage.cacheWrite += usage.cacheWrite || 0;
279
+ result.usage.cost += usage.cost?.total || 0;
280
+ result.usage.contextTokens = usage.totalTokens || 0;
281
+ result.usage.contextWindowTokens =
282
+ resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
283
+ }
284
+
217
285
  function addMessageToResult(result: RuntimeResult, msg: Message): void {
218
286
  result.messages.push(msg);
219
287
  result.finalOutput = truncateOutput(getFinalOutput(result.messages));
220
-
221
288
  if (msg.role === "toolResult" && msg.isError) {
222
289
  result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
223
290
  } else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
224
291
  result.errorMessage = undefined;
225
292
  }
226
-
227
- if (msg.role !== "assistant") return;
228
- result.usage.turns++;
229
-
230
- const { usage } = msg;
231
- if (usage) {
232
- result.usage.input += usage.input || 0;
233
- result.usage.output += usage.output || 0;
234
- result.usage.cacheRead += usage.cacheRead || 0;
235
- result.usage.cacheWrite += usage.cacheWrite || 0;
236
- result.usage.cost += usage.cost?.total || 0;
237
- result.usage.contextTokens = usage.totalTokens || 0;
238
- result.usage.contextWindowTokens =
239
- resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
293
+ if (msg.role === "assistant") {
294
+ accumulateUsage(result, msg);
295
+ if (!result.model && msg.model) result.model = msg.model;
296
+ if (msg.stopReason) result.stopReason = msg.stopReason;
297
+ if (msg.errorMessage) result.errorMessage = msg.errorMessage;
240
298
  }
241
-
242
- if (!result.model && msg.model) result.model = msg.model;
243
- if (msg.stopReason) result.stopReason = msg.stopReason;
244
- if (msg.errorMessage) result.errorMessage = msg.errorMessage;
245
299
  }
246
300
 
247
- /**
248
- * Standardized error result generator.
249
- */
250
301
  function createErrorResult(
251
302
  agent: string,
252
303
  source: "user" | "project" | "unknown",
@@ -261,15 +312,7 @@ function createErrorResult(
261
312
  exitCode: 1,
262
313
  finalOutput: "",
263
314
  stderr: error,
264
- usage: {
265
- input: 0,
266
- output: 0,
267
- cacheRead: 0,
268
- cacheWrite: 0,
269
- cost: 0,
270
- contextTokens: 0,
271
- turns: 0,
272
- },
315
+ usage: { ...EMPTY_USAGE },
273
316
  model,
274
317
  };
275
318
  }
@@ -304,10 +347,7 @@ function errorForDepthLimit(
304
347
  );
305
348
  }
306
349
 
307
- async function cleanupTempPrompt(tmpPrompt: {
308
- dir: string;
309
- filePath: string;
310
- }): Promise<void> {
350
+ async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
311
351
  try {
312
352
  await fs.promises.unlink(tmpPrompt.filePath);
313
353
  await fs.promises.rmdir(tmpPrompt.dir);
@@ -316,6 +356,22 @@ async function cleanupTempPrompt(tmpPrompt: {
316
356
  }
317
357
  }
318
358
 
359
+ function beginPromptSetup(agent: AgentConfig): Promise<PromptSetupResult> {
360
+ if (!agent.systemPrompt.trim()) return Promise.resolve({ tmpPrompt: null });
361
+ return writePromptToTempFile(agent.name, agent.systemPrompt).then(
362
+ (tmpPrompt) => ({ tmpPrompt }),
363
+ (error: unknown) => ({ error }),
364
+ );
365
+ }
366
+
367
+ async function cleanupPromptSetupResult(
368
+ setup: PromptSetupResult,
369
+ ): Promise<void> {
370
+ if ("tmpPrompt" in setup && setup.tmpPrompt) {
371
+ await cleanupTempPrompt(setup.tmpPrompt);
372
+ }
373
+ }
374
+
319
375
  function findRecentMessagesAnchor(messages: Message[]): number {
320
376
  for (let i = messages.length - 1; i >= 0; i--) {
321
377
  const msg = messages[i];
@@ -334,10 +390,13 @@ function findRecentMessagesAnchor(messages: Message[]): number {
334
390
  /**
335
391
  * Derives current execution progress from accumulated messages.
336
392
  * Maps tool calls to UI-safe previews for real-time feedback.
393
+ * Builds activeToolActivity from the most recent tool call, providing
394
+ * a compact parent summary for subagent tools before nested child data arrives.
337
395
  */
338
396
  function deriveStreamingProgress(messages: Message[]): StreamingProgress {
339
397
  const toolCalls: { id: string; preview: string }[] = [];
340
398
  let lastToolPreview: string | undefined;
399
+ let activeToolActivity: ToolActivity | undefined;
341
400
  for (const msg of messages) {
342
401
  if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
343
402
  for (const part of msg.content) {
@@ -348,9 +407,15 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
348
407
  );
349
408
  toolCalls.push({ id: part.id, preview });
350
409
  lastToolPreview = preview;
410
+ activeToolActivity = { toolName: part.name, inputSummary: preview };
351
411
  }
352
412
  }
353
- return { activityText: lastToolPreview, toolCalls, lastToolPreview };
413
+ return {
414
+ activeToolActivity,
415
+ activityText: renderToolActivity(activeToolActivity),
416
+ toolCalls,
417
+ lastToolPreview,
418
+ };
354
419
  }
355
420
 
356
421
  /**
@@ -358,23 +423,62 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
358
423
  * Redacts values if the preview contains sensitive keywords.
359
424
  */
360
425
  function sanitizeProgressPreview(preview: string, toolName: string): string {
361
- return /secret|token|password/i.test(preview) ? toolName : preview;
426
+ return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
362
427
  }
363
428
 
364
- function makeEmitUpdate(
429
+ export function makeEmitUpdate(
365
430
  result: RuntimeResult,
366
431
  onUpdate: OnUpdateCallback | undefined,
367
432
  makeDetails: (
368
433
  results: RuntimeResult[],
369
434
  options?: { includeMessages?: boolean; recentMessages?: Message[] },
370
435
  ) => SubagentDetails,
371
- ): () => void {
372
- return () => {
436
+ ): (options?: {
437
+ toolActivity?: ToolActivity;
438
+ toolResultCompleted?: boolean;
439
+ }) => void {
440
+ return (options) => {
373
441
  const msgs = result.messages;
374
442
  const anchorIdx = findRecentMessagesAnchor(msgs);
375
443
  const recentMessages =
376
444
  anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
377
445
  const progress = deriveStreamingProgress(msgs);
446
+ // Preserve stored activity tree for tool-result completion signals
447
+ // so the parent retains nested context until newer activity arrives
448
+ if (options?.toolResultCompleted && result.progress?.activeToolActivity) {
449
+ progress.activeToolActivity = result.progress.activeToolActivity;
450
+ progress.activityText = renderToolActivity(progress.activeToolActivity);
451
+ }
452
+ // Handle parsed tool activity from child events
453
+ // Merge with parent activity if this is a nested update
454
+ if (options?.toolActivity) {
455
+ if (
456
+ progress.activeToolActivity &&
457
+ progress.activeToolActivity.toolName === options.toolActivity.toolName
458
+ ) {
459
+ // Merge: prefer parser inputSummary when non-empty and richer than bare toolName fallback
460
+ const incomingSummary = options.toolActivity.inputSummary;
461
+ const preferIncoming =
462
+ incomingSummary && incomingSummary !== options.toolActivity.toolName;
463
+ progress.activeToolActivity = {
464
+ ...progress.activeToolActivity,
465
+ inputSummary: preferIncoming
466
+ ? incomingSummary
467
+ : progress.activeToolActivity.inputSummary,
468
+ instanceName:
469
+ options.toolActivity.instanceName ??
470
+ progress.activeToolActivity.instanceName,
471
+ child:
472
+ options.toolActivity.child ?? progress.activeToolActivity.child,
473
+ };
474
+ } else {
475
+ progress.activeToolActivity = options.toolActivity;
476
+ }
477
+ progress.activityText = renderToolActivity(progress.activeToolActivity);
478
+ }
479
+ if (options?.toolResultCompleted) {
480
+ progress.toolResultCompleted = true;
481
+ }
378
482
  result.progress = progress;
379
483
  onUpdate?.({
380
484
  content: [
@@ -414,33 +518,49 @@ function clearGraceTimer(state: SubagentState): void {
414
518
  state.agentEndGraceTimer = undefined;
415
519
  }
416
520
 
417
- function processEventLine(
418
- line: string,
521
+ function handleMessageEvent(
522
+ event: ChildKnownEvent,
419
523
  state: SubagentState,
420
- emitUpdate: () => void,
421
- requestTermination: (reason: string) => Promise<unknown>,
524
+ emitUpdate: (options?: {
525
+ toolActivity?: ToolActivity;
526
+ toolResultCompleted?: boolean;
527
+ }) => void,
422
528
  ): void {
423
- const parseResult = parseChildEventLine(line);
424
- if (parseResult.kind !== "known") return;
425
- const { event } = parseResult;
426
-
427
- if (
428
- (event.type === "message_end" || event.type === "tool_result_end") &&
429
- event.message
430
- ) {
529
+ if (event.type !== "message_end" && event.type !== "tool_result_end") return;
530
+ if (event.message) {
431
531
  addMessageToResult(state.result, event.message as Message);
432
- emitUpdate();
532
+ const toolResultCompleted = event.type === "tool_result_end";
533
+ emitUpdate({ toolResultCompleted });
433
534
  }
535
+ }
434
536
 
435
- if (event.type !== "agent_end") return;
537
+ function handleToolExecutionUpdateEvent(
538
+ event: ChildKnownEvent,
539
+ emitUpdate: (options?: {
540
+ toolActivity?: ToolActivity;
541
+ toolResultCompleted?: boolean;
542
+ }) => void,
543
+ ): void {
544
+ if (event.type !== TOOL_EXECUTION_UPDATE_EVENT) return;
545
+ emitUpdate({ toolActivity: event.toolActivity });
546
+ }
436
547
 
548
+ function handleAgentEndEvent(
549
+ event: ChildKnownEvent,
550
+ state: SubagentState,
551
+ emitUpdate: (options?: {
552
+ toolActivity?: ToolActivity;
553
+ toolResultCompleted?: boolean;
554
+ }) => void,
555
+ requestTermination: (reason: string) => Promise<unknown>,
556
+ ): void {
557
+ if (event.type !== "agent_end") return;
437
558
  if (state.result.messages.length === 0 && Array.isArray(event.messages)) {
438
559
  for (const msg of event.messages as Message[]) {
439
560
  addMessageToResult(state.result, msg);
440
561
  }
441
562
  emitUpdate();
442
563
  }
443
-
444
564
  if (state.agentEndGraceTimer || state.terminationPromise) return;
445
565
  state.agentEndGraceTimer = setTimeout(() => {
446
566
  state.agentEndGraceTimer = undefined;
@@ -449,6 +569,23 @@ function processEventLine(
449
569
  state.agentEndGraceTimer.unref?.();
450
570
  }
451
571
 
572
+ function processEventLine(
573
+ line: string,
574
+ state: SubagentState,
575
+ emitUpdate: (options?: {
576
+ toolActivity?: ToolActivity;
577
+ toolResultCompleted?: boolean;
578
+ }) => void,
579
+ requestTermination: (reason: string) => Promise<unknown>,
580
+ ): void {
581
+ const parseResult = parseChildEventLine(line);
582
+ if (parseResult.kind !== "known") return;
583
+ const { event } = parseResult;
584
+ handleMessageEvent(event, state, emitUpdate);
585
+ handleToolExecutionUpdateEvent(event, emitUpdate);
586
+ handleAgentEndEvent(event, state, emitUpdate, requestTermination);
587
+ }
588
+
452
589
  function setupAbortHandler(
453
590
  signal: AbortSignal | undefined,
454
591
  state: SubagentState,
@@ -497,7 +634,10 @@ function buildPiArgs(
497
634
  function setupChildProcess(
498
635
  proc: ChildProcess,
499
636
  state: SubagentState,
500
- emitUpdate: () => void,
637
+ emitUpdate: (options?: {
638
+ toolActivity?: ToolActivity;
639
+ toolResultCompleted?: boolean;
640
+ }) => void,
501
641
  requestTermination: (reason: string) => Promise<unknown>,
502
642
  ): void {
503
643
  proc.once("error", (error) => {
@@ -544,7 +684,7 @@ async function finalizeResult(
544
684
  if (agentEndTimeoutExitCode !== undefined) {
545
685
  state.result.exitCode = agentEndTimeoutExitCode;
546
686
  }
547
- if (state.wasAborted) throw new Error("Subagent was aborted");
687
+ if (state.wasAborted) throw new SubagentAbortError(state.result);
548
688
  return state.result;
549
689
  }
550
690
 
@@ -556,7 +696,7 @@ async function finalizeResult(
556
696
  * the main conversation.
557
697
  *
558
698
  * Safety:
559
- * - Enforces a strict recursion limit (depth 1) via environment variables.
699
+ * - Enforces a strict recursion limit (depth 2) via environment variables.
560
700
  * - Uses temporary prompt files to pass large system prompts without shell limits.
561
701
  * - Streams JSON events from the child to provide real-time UI updates to the parent.
562
702
  * - Implements aggressive process tree termination to prevent orphan processes.
@@ -579,18 +719,28 @@ export async function runSingleAgent(
579
719
  ): Promise<SingleResult> {
580
720
  const agent = agents.find((a) => a.name === agentName);
581
721
  if (!agent) return errorForUnknownAgent(agentName, agents, task);
582
-
583
722
  const depth = getSubagentDepth();
584
723
  if (depth >= MAX_SUBAGENT_DEPTH) {
585
724
  return errorForDepthLimit(agentName, agent.source, task, depth);
586
725
  }
587
-
588
- const thinking = agent.thinking ?? parentThinking;
726
+ const requestedThinking = agent.thinking ?? parentThinking;
727
+ const { level: thinking, warning: thinkingWarning } = parentModel
728
+ ? resolveThinkingLevel(
729
+ requestedThinking,
730
+ parentModel.provider,
731
+ parentModel.id,
732
+ )
733
+ : { level: requestedThinking };
589
734
  const modelDisplay = buildModelDisplay(parentModel, thinking);
590
- const resolvedSkills = agent.skills
591
- ? await resolveAgentSkillArgs(defaultCwd, agent.skills)
592
- : { args: [] };
735
+ const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
736
+ agent.skills
737
+ ? resolveAgentSkillArgs(defaultCwd, agent.skills)
738
+ : Promise.resolve({ args: [] });
739
+ const promptSetupPromise = beginPromptSetup(agent);
740
+ const resolvedSkills = await resolvedSkillsPromise;
593
741
  if ("error" in resolvedSkills) {
742
+ const promptSetup = await promptSetupPromise;
743
+ await cleanupPromptSetupResult(promptSetup);
594
744
  return createErrorResult(
595
745
  agentName,
596
746
  agent.source,
@@ -599,18 +749,16 @@ export async function runSingleAgent(
599
749
  modelDisplay,
600
750
  );
601
751
  }
602
-
752
+ const promptSetup = await promptSetupPromise;
753
+ if ("error" in promptSetup) throw promptSetup.error;
603
754
  const startedAt = Date.now();
604
755
  const state: SubagentState = {
605
756
  result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
606
757
  wasAborted: false,
607
758
  };
608
-
609
- let tmpPrompt: { dir: string; filePath: string } | null = null;
759
+ if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
760
+ const tmpPrompt = promptSetup.tmpPrompt;
610
761
  try {
611
- tmpPrompt = agent.systemPrompt.trim()
612
- ? await writePromptToTempFile(agent.name, agent.systemPrompt)
613
- : null;
614
762
  const args = buildPiArgs(
615
763
  agent,
616
764
  task,
@@ -632,7 +780,6 @@ export async function runSingleAgent(
632
780
  env: { ...process.env, ...subagentDepthEnv() },
633
781
  ...getProcessTreeSpawnOptions(terminateOptions.tree),
634
782
  });
635
-
636
783
  const processDone = waitForSubagentProcess(proc);
637
784
  const emitUpdate = makeEmitUpdate(state.result, onUpdate, makeDetails);
638
785
  const requestTermination = makeRequestTerminator(
@@ -641,7 +788,6 @@ export async function runSingleAgent(
641
788
  state,
642
789
  );
643
790
  setupChildProcess(proc, state, emitUpdate, requestTermination);
644
-
645
791
  const onAbort = setupAbortHandler(
646
792
  signal,
647
793
  state,
@@ -3,6 +3,8 @@ export const SUBAGENT_RESULT_CONTRACT = `
3
3
  - End your final response with exactly one line:
4
4
  - Outcome: <short, single, compact lower-case sentence>.
5
5
  - Outcome summarizes the result of your task in a single sentence.
6
+ - The outcome line is for internal use by the agent.
7
+ - Don't present outcome line in the main agent's response.
6
8
  `;
7
9
 
8
10
  export function appendSubagentResultContract(prompt: string): string {