@mystilleef/pi-subagent 0.8.0 → 0.10.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.
@@ -17,39 +17,43 @@ import {
17
17
  import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
18
18
  import { getFinalOutput } from "../output/ui.js";
19
19
  import { makeToolPreview, renderToolActivity } from "../progress/progress.js";
20
+ import { SENSITIVE_PATTERN } from "../progress/progress-format.js";
21
+ import { isToolCallPart } from "../progress/progress-state.js";
20
22
  import {
21
- isToolCallPart,
22
- SENSITIVE_PATTERN,
23
- } from "../progress/progress-state.js";
24
- import type {
25
- OnUpdateCallback,
26
- SingleResult,
27
- StreamingProgress,
28
- SubagentDetails,
29
- ToolActivity,
23
+ type OnUpdateCallback,
24
+ type SingleResult,
25
+ type StreamingProgress,
26
+ type SubagentDetails,
27
+ TOOL_RESULT_FAILED_MESSAGE,
28
+ type ToolActivity,
30
29
  } from "../shared/types.js";
31
30
  import {
32
31
  detectMessageError,
32
+ findLastAssistantTextMessage,
33
33
  getPiInvocation,
34
34
  getSubagentDepth,
35
+ getSubagentRuntimeLimits,
35
36
  resolveAgentSkillArgs,
36
37
  subagentDepthEnv,
37
38
  truncateOutput,
38
39
  writePromptToTempFile,
39
40
  } from "../shared/utils.js";
40
41
  import {
42
+ type ChildEventParseResult,
41
43
  type ChildKnownEvent,
42
44
  parseChildEventLine,
43
45
  TOOL_EXECUTION_UPDATE_EVENT,
44
46
  } from "./child-events.js";
45
47
  import { appendSubagentResultContract } from "./prompt-contract.js";
46
48
  import {
49
+ acquireChildSleepInhibitor,
47
50
  getProcessTreeSpawnOptions,
51
+ isFinitePid,
52
+ makeHostSleepInhibitorAdapter,
53
+ type SleepInhibitorHandle,
48
54
  terminateChildProcess,
49
55
  } from "./termination.js";
50
56
 
51
- const MAX_STDERR_BYTES = 10_000;
52
- const AGENT_END_GRACE_MS = 250;
53
57
  export function resolveThinkingLevel(
54
58
  requested: ThinkingLevel,
55
59
  provider: string,
@@ -73,15 +77,25 @@ export function resolveThinkingLevel(
73
77
  return { level: clamped, warning: mkWarning(clamped) };
74
78
  }
75
79
 
76
- const MAX_SUBAGENT_DEPTH = 3;
77
- export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
78
-
80
+ type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
79
81
  type RuntimeResult = SingleResult & { messages: Message[] };
82
+ type ChildModelSettings = {
83
+ provider?: string | undefined;
84
+ id?: string | undefined;
85
+ };
86
+ type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
87
+
88
+ type RunSingleAgentOptions = {
89
+ acquireSleepInhibitor?: SleepInhibitorAcquirer;
90
+ getOrchestratorPid?: () => unknown;
91
+ };
80
92
 
81
93
  export class SubagentAbortError extends Error {
82
- constructor(public readonly result: SingleResult) {
94
+ readonly result: SingleResult;
95
+ constructor(result: SingleResult) {
83
96
  super("Subagent was aborted");
84
97
  this.name = "SubagentAbortError";
98
+ this.result = result;
85
99
  }
86
100
  }
87
101
 
@@ -91,6 +105,7 @@ type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
91
105
 
92
106
  interface SubagentState {
93
107
  result: RuntimeResult;
108
+ runtimeLimits: RuntimeLimits;
94
109
  spawnError?: Error;
95
110
  wasAborted: boolean;
96
111
  agentEndGraceTimer?: ReturnType<typeof setTimeout>;
@@ -99,30 +114,47 @@ interface SubagentState {
99
114
 
100
115
  function appendWithByteLimit(
101
116
  current: string,
102
- data: string,
117
+ data: string | Buffer,
103
118
  max: number,
104
119
  ): string {
105
- if (current.length >= max) return current;
106
- return current + data.slice(0, max - current.length);
120
+ const currentBytes = Buffer.from(current, "utf-8");
121
+ if (currentBytes.length >= max) return current;
122
+ const incomingBytes = Buffer.isBuffer(data)
123
+ ? data
124
+ : Buffer.from(data, "utf-8");
125
+ const combined = Buffer.concat([currentBytes, incomingBytes]);
126
+ if (combined.length <= max) return combined.toString("utf-8");
127
+ return truncateValidUtf8(combined, max);
128
+ }
129
+
130
+ function truncateValidUtf8(buffer: Buffer, max: number): string {
131
+ let end = Math.min(max, buffer.length);
132
+ while (end > 0) {
133
+ const candidate = buffer.subarray(0, end).toString("utf-8");
134
+ if (!candidate.endsWith("�")) return candidate;
135
+ end -= 1;
136
+ }
137
+ return "";
107
138
  }
108
139
 
109
140
  /**
110
- * Attempts to resolve the context window token limit for a given message's model.
111
141
  * Rationale: Subagent usage reporting needs context window awareness to provide
112
142
  * meaningful "context full" indicators to the parent.
113
143
  */
114
144
  function resolveContextWindowTokens(msg: Message): number | undefined {
115
145
  const m = msg as unknown as Record<string, unknown>;
116
- if (typeof m.provider !== "string" || typeof m.model !== "string") return;
146
+ if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
147
+ return;
117
148
  try {
118
149
  const contextWindow = getModel(
119
- m.provider as never,
120
- m.model as never,
150
+ m["provider"] as never,
151
+ m["model"] as never,
121
152
  )?.contextWindow;
122
153
  return Number.isFinite(contextWindow) && contextWindow > 0
123
154
  ? contextWindow
124
155
  : undefined;
125
156
  } catch {
157
+ /* model lookup failures return undefined to skip context window tracking */
126
158
  return;
127
159
  }
128
160
  }
@@ -134,11 +166,56 @@ function getAbortReason(signal: AbortSignal): string {
134
166
  return "abort";
135
167
  }
136
168
 
137
- /**
138
- * Verifies if the agent produced any textual output or final response.
139
- * Precondition: Called after process exit to distinguish between clean completion
140
- * and silent failures where the process exited 0 but did nothing.
141
- */
169
+ const hostSleepInhibitorAdapter = makeHostSleepInhibitorAdapter();
170
+
171
+ async function acquireDefaultSleepInhibitor(
172
+ pid: number,
173
+ ): Promise<SleepInhibitorHandle> {
174
+ return acquireChildSleepInhibitor(pid, hostSleepInhibitorAdapter);
175
+ }
176
+
177
+ async function acquireSubagentSleepInhibitor(
178
+ pid: number,
179
+ acquireSleepInhibitor: SleepInhibitorAcquirer,
180
+ ): Promise<SleepInhibitorHandle | undefined> {
181
+ try {
182
+ return await acquireSleepInhibitor(pid);
183
+ } catch {
184
+ /* acquisition failures degrade gracefully to no inhibitor */
185
+ return undefined;
186
+ }
187
+ }
188
+
189
+ function getValidatedOrchestratorPid(
190
+ options: RunSingleAgentOptions,
191
+ ): number | undefined {
192
+ try {
193
+ const pid = options.getOrchestratorPid
194
+ ? options.getOrchestratorPid()
195
+ : process.pid;
196
+ return isFinitePid(pid) ? pid : undefined;
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ }
201
+
202
+ async function releaseSleepInhibitor(
203
+ handle: SleepInhibitorHandle | undefined,
204
+ ): Promise<void> {
205
+ if (!handle) return;
206
+ try {
207
+ await handle.release();
208
+ } catch {
209
+ /* release failures are non-fatal */
210
+ }
211
+ }
212
+
213
+ function startSleepInhibitorRelease(
214
+ acquisitionPromise: Promise<SleepInhibitorHandle | undefined>,
215
+ ): Promise<void> {
216
+ return acquisitionPromise.then(releaseSleepInhibitor, () => {});
217
+ }
218
+
142
219
  function hasCompletedAgentOutput(result: RuntimeResult): boolean {
143
220
  if (result.finalOutput.trim()) return true;
144
221
  return result.messages.some(
@@ -151,7 +228,6 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
151
228
  }
152
229
 
153
230
  /**
154
- * Determines the exit code for processes terminated via the agent_end timeout.
155
231
  * Rationale: `pi` processes in JSON mode might hang after finishing their task;
156
232
  * we force-kill them after a grace period and treat it as success (0) if they
157
233
  * actually produced output.
@@ -168,7 +244,6 @@ function getAgentEndTimeoutExitCode(
168
244
  }
169
245
 
170
246
  /**
171
- * Orchestrates the cleanup and exit code capture of a child process.
172
247
  * Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
173
248
  * are destroyed and promises settled even if the process or its pipes hang.
174
249
  */
@@ -229,13 +304,20 @@ async function waitForSubagentProcess(
229
304
  }
230
305
 
231
306
  function buildModelDisplay(
232
- parentModel: { provider: string; id: string } | undefined,
307
+ effectiveModel: ChildModelSettings,
233
308
  thinking: ThinkingLevel,
234
309
  ): string | undefined {
235
- if (parentModel) {
236
- return `${parentModel.provider}/${parentModel.id}${thinking ? `:${thinking}` : ""}`;
310
+ const parts: string[] = [];
311
+ if (effectiveModel.provider) {
312
+ parts.push(effectiveModel.provider);
313
+ }
314
+ if (effectiveModel.id) {
315
+ parts.push(effectiveModel.id);
316
+ }
317
+ if (thinking) {
318
+ parts.push(thinking);
237
319
  }
238
- return thinking ? `thinking:${thinking}` : undefined;
320
+ return parts.length > 0 ? parts.join(" ・ ") : undefined;
239
321
  }
240
322
 
241
323
  const EMPTY_USAGE = {
@@ -278,8 +360,9 @@ function accumulateUsage(result: RuntimeResult, msg: Message): void {
278
360
  result.usage.cacheWrite += usage.cacheWrite || 0;
279
361
  result.usage.cost += usage.cost?.total || 0;
280
362
  result.usage.contextTokens = usage.totalTokens || 0;
281
- result.usage.contextWindowTokens =
282
- resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
363
+ const ctxWindowTokens = resolveContextWindowTokens(msg);
364
+ if (ctxWindowTokens !== undefined)
365
+ result.usage.contextWindowTokens = ctxWindowTokens;
283
366
  }
284
367
 
285
368
  function addMessageToResult(result: RuntimeResult, msg: Message): void {
@@ -288,7 +371,7 @@ function addMessageToResult(result: RuntimeResult, msg: Message): void {
288
371
  if (msg.role === "toolResult" && msg.isError) {
289
372
  result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
290
373
  } else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
291
- result.errorMessage = undefined;
374
+ delete result.errorMessage;
292
375
  }
293
376
  if (msg.role === "assistant") {
294
377
  accumulateUsage(result, msg);
@@ -336,13 +419,14 @@ function errorForDepthLimit(
336
419
  source: "user" | "project" | "unknown",
337
420
  task: string,
338
421
  depth: number,
422
+ maxDepth: number,
339
423
  model?: string,
340
424
  ): SingleResult {
341
425
  return createErrorResult(
342
426
  agentName,
343
427
  source,
344
428
  task,
345
- `Subagent nesting limit reached (depth ${depth}/${MAX_SUBAGENT_DEPTH}).`,
429
+ `Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
346
430
  model,
347
431
  );
348
432
  }
@@ -352,7 +436,7 @@ async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
352
436
  await fs.promises.unlink(tmpPrompt.filePath);
353
437
  await fs.promises.rmdir(tmpPrompt.dir);
354
438
  } catch {
355
- /* ignore */
439
+ /* temp file cleanup failures are non-fatal; OS will clean up eventually */
356
440
  }
357
441
  }
358
442
 
@@ -373,26 +457,9 @@ async function cleanupPromptSetupResult(
373
457
  }
374
458
 
375
459
  function findRecentMessagesAnchor(messages: Message[]): number {
376
- for (let i = messages.length - 1; i >= 0; i--) {
377
- const msg = messages[i];
378
- if (
379
- msg?.role === "assistant" &&
380
- msg.content.some(
381
- (c) => c.type === "text" && (c as { text?: string }).text?.trim(),
382
- )
383
- ) {
384
- return i;
385
- }
386
- }
387
- return -1;
460
+ return findLastAssistantTextMessage(messages);
388
461
  }
389
462
 
390
- /**
391
- * Derives current execution progress from accumulated messages.
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.
395
- */
396
463
  function deriveStreamingProgress(messages: Message[]): StreamingProgress {
397
464
  const toolCalls: { id: string; preview: string }[] = [];
398
465
  let lastToolPreview: string | undefined;
@@ -410,22 +477,76 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
410
477
  activeToolActivity = { toolName: part.name, inputSummary: preview };
411
478
  }
412
479
  }
480
+ const activityText = renderToolActivity(activeToolActivity);
413
481
  return {
414
482
  activeToolActivity,
415
- activityText: renderToolActivity(activeToolActivity),
483
+ activityText,
416
484
  toolCalls,
417
485
  lastToolPreview,
418
486
  };
419
487
  }
420
488
 
421
- /**
422
- * Prevents leaking secrets in the CLI progress display.
423
- * Redacts values if the preview contains sensitive keywords.
424
- */
425
489
  function sanitizeProgressPreview(preview: string, toolName: string): string {
426
490
  return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
427
491
  }
428
492
 
493
+ /**
494
+ * Merge incoming tool activity with existing progress activity.
495
+ * When tool names match, prefer richer inputSummary from incoming.
496
+ * Otherwise, replace entirely with incoming activity.
497
+ */
498
+ function mergeToolActivity(
499
+ existing: ToolActivity | undefined,
500
+ incoming: ToolActivity,
501
+ ): ToolActivity {
502
+ if (existing && existing.toolName === incoming.toolName) {
503
+ const incomingSummary = incoming.inputSummary;
504
+ const preferIncoming =
505
+ incomingSummary && incomingSummary !== incoming.toolName;
506
+ return {
507
+ ...existing,
508
+ inputSummary: preferIncoming ? incomingSummary : existing.inputSummary,
509
+ instanceName: incoming.instanceName ?? existing.instanceName,
510
+ child: incoming.child ?? existing.child,
511
+ };
512
+ }
513
+ return incoming;
514
+ }
515
+
516
+ /**
517
+ * Apply tool activity and result completion updates to progress state.
518
+ * Handles merging of child events with parent activity tree.
519
+ */
520
+ function applyActivityUpdates(
521
+ progress: StreamingProgress,
522
+ options: { toolActivity?: ToolActivity; toolResultCompleted?: boolean },
523
+ previousActivity?: ToolActivity,
524
+ ): void {
525
+ // Preserve stored activity tree for tool-result completion signals
526
+ // so the parent retains nested context until newer activity arrives
527
+ if (options.toolResultCompleted && previousActivity) {
528
+ progress.activeToolActivity = previousActivity;
529
+ const renderedText = renderToolActivity(previousActivity);
530
+ if (renderedText !== undefined) progress.activityText = renderedText;
531
+ else delete progress.activityText;
532
+ }
533
+ // Handle parsed tool activity from child events
534
+ // Merge with parent activity if this is a nested update
535
+ if (options.toolActivity) {
536
+ progress.activeToolActivity = mergeToolActivity(
537
+ progress.activeToolActivity,
538
+ options.toolActivity,
539
+ );
540
+ const renderedActivity = renderToolActivity(progress.activeToolActivity);
541
+ if (renderedActivity !== undefined)
542
+ progress.activityText = renderedActivity;
543
+ else delete progress.activityText;
544
+ }
545
+ if (options.toolResultCompleted) {
546
+ progress.toolResultCompleted = true;
547
+ }
548
+ }
549
+
429
550
  export function makeEmitUpdate(
430
551
  result: RuntimeResult,
431
552
  onUpdate: OnUpdateCallback | undefined,
@@ -443,41 +564,12 @@ export function makeEmitUpdate(
443
564
  const recentMessages =
444
565
  anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
445
566
  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;
567
+ if (options) {
568
+ applyActivityUpdates(
569
+ progress,
570
+ options,
571
+ result.progress?.activeToolActivity,
572
+ );
481
573
  }
482
574
  result.progress = progress;
483
575
  onUpdate?.({
@@ -515,7 +607,7 @@ function makeRequestTerminator(
515
607
  function clearGraceTimer(state: SubagentState): void {
516
608
  if (!state.agentEndGraceTimer) return;
517
609
  clearTimeout(state.agentEndGraceTimer);
518
- state.agentEndGraceTimer = undefined;
610
+ delete state.agentEndGraceTimer;
519
611
  }
520
612
 
521
613
  function handleMessageEvent(
@@ -563,12 +655,25 @@ function handleAgentEndEvent(
563
655
  }
564
656
  if (state.agentEndGraceTimer || state.terminationPromise) return;
565
657
  state.agentEndGraceTimer = setTimeout(() => {
566
- state.agentEndGraceTimer = undefined;
658
+ delete state.agentEndGraceTimer;
567
659
  void requestTermination("agent_end_timeout");
568
- }, AGENT_END_GRACE_MS);
660
+ }, state.runtimeLimits.agentEndGraceMs);
569
661
  state.agentEndGraceTimer.unref?.();
570
662
  }
571
663
 
664
+ function formatUnknownEventDiagnostic(
665
+ line: string,
666
+ parseResult: Exclude<ChildEventParseResult, { kind: "known" }>,
667
+ ): string {
668
+ if (parseResult.kind === "invalid" && !line.trim()) {
669
+ return "[pi-subagent:unknown-event] blank";
670
+ }
671
+ if (parseResult.kind === "invalid") {
672
+ return `[pi-subagent:unknown-event] malformed: ${line}`;
673
+ }
674
+ return `[pi-subagent:unknown-event] unknown: ${JSON.stringify(parseResult.event)}`;
675
+ }
676
+
572
677
  function processEventLine(
573
678
  line: string,
574
679
  state: SubagentState,
@@ -577,9 +682,17 @@ function processEventLine(
577
682
  toolResultCompleted?: boolean;
578
683
  }) => void,
579
684
  requestTermination: (reason: string) => Promise<unknown>,
685
+ debugEventDiagnostics: boolean,
580
686
  ): void {
581
687
  const parseResult = parseChildEventLine(line);
582
- if (parseResult.kind !== "known") return;
688
+ if (parseResult.kind !== "known") {
689
+ if (debugEventDiagnostics) {
690
+ process.stderr.write(
691
+ `${formatUnknownEventDiagnostic(line, parseResult)}\n`,
692
+ );
693
+ }
694
+ return;
695
+ }
583
696
  const { event } = parseResult;
584
697
  handleMessageEvent(event, state, emitUpdate);
585
698
  handleToolExecutionUpdateEvent(event, emitUpdate);
@@ -606,18 +719,30 @@ function setupAbortHandler(
606
719
  return onAbort;
607
720
  }
608
721
 
722
+ function resolveEffectiveChildModelSettings(
723
+ agent: AgentConfig,
724
+ parentModel: ChildModelSettings | undefined,
725
+ ): ChildModelSettings {
726
+ return {
727
+ provider: agent.provider ?? parentModel?.provider,
728
+ id:
729
+ agent.model ??
730
+ (agent.provider === undefined ? parentModel?.id : undefined),
731
+ };
732
+ }
733
+
609
734
  function buildPiArgs(
610
735
  agent: AgentConfig,
611
736
  task: string,
612
- parentModel: { provider: string; id: string } | undefined,
737
+ effectiveModel: ChildModelSettings,
613
738
  thinking: ThinkingLevel,
614
739
  resolvedSkills: { args: string[] },
615
740
  tmpPrompt: { filePath: string } | null,
616
741
  ): string[] {
617
742
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
618
- if (parentModel) {
619
- args.push("--provider", parentModel.provider, "--model", parentModel.id);
620
- }
743
+ if (effectiveModel.provider && effectiveModel.id)
744
+ args.push("--provider", effectiveModel.provider);
745
+ if (effectiveModel.id) args.push("--model", effectiveModel.id);
621
746
  args.push("--thinking", thinking);
622
747
  if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
623
748
  if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
@@ -639,28 +764,35 @@ function setupChildProcess(
639
764
  toolResultCompleted?: boolean;
640
765
  }) => void,
641
766
  requestTermination: (reason: string) => Promise<unknown>,
767
+ debugEventDiagnostics: boolean,
642
768
  ): void {
643
769
  proc.once("error", (error) => {
644
770
  state.spawnError = error;
645
771
  state.result.stderr = appendWithByteLimit(
646
772
  state.result.stderr,
647
773
  error.message,
648
- MAX_STDERR_BYTES,
774
+ state.runtimeLimits.maxStderrBytes,
649
775
  );
650
776
  });
651
777
  if (proc.stdout) {
652
778
  readline
653
779
  .createInterface({ input: proc.stdout })
654
780
  .on("line", (line) =>
655
- processEventLine(line, state, emitUpdate, requestTermination),
781
+ processEventLine(
782
+ line,
783
+ state,
784
+ emitUpdate,
785
+ requestTermination,
786
+ debugEventDiagnostics,
787
+ ),
656
788
  );
657
789
  }
658
790
  if (proc.stderr) {
659
- proc.stderr.on("data", (data) => {
791
+ proc.stderr.on("data", (data: Buffer) => {
660
792
  state.result.stderr = appendWithByteLimit(
661
793
  state.result.stderr,
662
- data.toString(),
663
- MAX_STDERR_BYTES,
794
+ data,
795
+ state.runtimeLimits.maxStderrBytes,
664
796
  );
665
797
  });
666
798
  }
@@ -684,13 +816,17 @@ async function finalizeResult(
684
816
  if (agentEndTimeoutExitCode !== undefined) {
685
817
  state.result.exitCode = agentEndTimeoutExitCode;
686
818
  }
687
- if (state.wasAborted) throw new SubagentAbortError(state.result);
819
+ if (state.result.termination?.cancelReason === "agent_end_timeout") {
820
+ state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
821
+ }
822
+ if (state.wasAborted) {
823
+ state.result.stderr = "";
824
+ throw new SubagentAbortError(state.result);
825
+ }
688
826
  return state.result;
689
827
  }
690
828
 
691
829
  /**
692
- * Executes a single subagent task.
693
- *
694
830
  * Rationale: Subagents run in isolated child processes to protect the parent's
695
831
  * context window and allow specialized system prompts/tools without polluting
696
832
  * the main conversation.
@@ -714,24 +850,35 @@ export async function runSingleAgent(
714
850
  results: RuntimeResult[],
715
851
  options?: { includeMessages?: boolean; recentMessages?: Message[] },
716
852
  ) => SubagentDetails,
717
- parentModel: { provider: string; id: string } | undefined,
853
+ parentModel: ChildModelSettings | undefined,
718
854
  parentThinking: ThinkingLevel,
855
+ debugEventDiagnostics = false,
856
+ options: RunSingleAgentOptions = {},
719
857
  ): Promise<SingleResult> {
720
858
  const agent = agents.find((a) => a.name === agentName);
721
859
  if (!agent) return errorForUnknownAgent(agentName, agents, task);
860
+ const runtimeLimits = getSubagentRuntimeLimits();
722
861
  const depth = getSubagentDepth();
723
- if (depth >= MAX_SUBAGENT_DEPTH) {
724
- return errorForDepthLimit(agentName, agent.source, task, depth);
862
+ if (depth >= runtimeLimits.maxDepth) {
863
+ return errorForDepthLimit(
864
+ agentName,
865
+ agent.source,
866
+ task,
867
+ depth,
868
+ runtimeLimits.maxDepth,
869
+ );
725
870
  }
726
871
  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 };
734
- const modelDisplay = buildModelDisplay(parentModel, thinking);
872
+ const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
873
+ const { level: thinking, warning: thinkingWarning } =
874
+ effectiveModel.provider && effectiveModel.id
875
+ ? resolveThinkingLevel(
876
+ requestedThinking,
877
+ effectiveModel.provider,
878
+ effectiveModel.id,
879
+ )
880
+ : { level: requestedThinking };
881
+ const modelDisplay = buildModelDisplay(effectiveModel, thinking);
735
882
  const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
736
883
  agent.skills
737
884
  ? resolveAgentSkillArgs(defaultCwd, agent.skills)
@@ -754,6 +901,7 @@ export async function runSingleAgent(
754
901
  const startedAt = Date.now();
755
902
  const state: SubagentState = {
756
903
  result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
904
+ runtimeLimits,
757
905
  wasAborted: false,
758
906
  };
759
907
  if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
@@ -762,7 +910,7 @@ export async function runSingleAgent(
762
910
  const args = buildPiArgs(
763
911
  agent,
764
912
  task,
765
- parentModel,
913
+ effectiveModel,
766
914
  thinking,
767
915
  resolvedSkills,
768
916
  tmpPrompt,
@@ -787,19 +935,34 @@ export async function runSingleAgent(
787
935
  terminateOptions,
788
936
  state,
789
937
  );
790
- setupChildProcess(proc, state, emitUpdate, requestTermination);
938
+ setupChildProcess(
939
+ proc,
940
+ state,
941
+ emitUpdate,
942
+ requestTermination,
943
+ debugEventDiagnostics,
944
+ );
791
945
  const onAbort = setupAbortHandler(
792
946
  signal,
793
947
  state,
794
948
  () => clearGraceTimer(state),
795
949
  requestTermination,
796
950
  );
951
+ const orchestratorPid = getValidatedOrchestratorPid(options);
952
+ const sleepInhibitorPromise =
953
+ orchestratorPid !== undefined
954
+ ? acquireSubagentSleepInhibitor(
955
+ orchestratorPid,
956
+ options.acquireSleepInhibitor ?? acquireDefaultSleepInhibitor,
957
+ )
958
+ : Promise.resolve(undefined);
797
959
  try {
798
960
  state.result.exitCode = (await processDone) ?? 0;
799
961
  return await finalizeResult(state, startedAt);
800
962
  } finally {
801
963
  clearGraceTimer(state);
802
964
  if (signal && onAbort) signal.removeEventListener("abort", onAbort);
965
+ void startSleepInhibitorRelease(sleepInhibitorPromise);
803
966
  }
804
967
  } finally {
805
968
  if (tmpPrompt) await cleanupTempPrompt(tmpPrompt);