@mystilleef/pi-subagent 0.9.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,19 +17,19 @@ 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
35
  getSubagentRuntimeLimits,
@@ -46,7 +46,11 @@ import {
46
46
  } from "./child-events.js";
47
47
  import { appendSubagentResultContract } from "./prompt-contract.js";
48
48
  import {
49
+ acquireChildSleepInhibitor,
49
50
  getProcessTreeSpawnOptions,
51
+ isFinitePid,
52
+ makeHostSleepInhibitorAdapter,
53
+ type SleepInhibitorHandle,
50
54
  terminateChildProcess,
51
55
  } from "./termination.js";
52
56
 
@@ -73,10 +77,18 @@ export function resolveThinkingLevel(
73
77
  return { level: clamped, warning: mkWarning(clamped) };
74
78
  }
75
79
 
76
- export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
77
-
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
94
  readonly result: SingleResult;
@@ -142,6 +154,7 @@ function resolveContextWindowTokens(msg: Message): number | undefined {
142
154
  ? contextWindow
143
155
  : undefined;
144
156
  } catch {
157
+ /* model lookup failures return undefined to skip context window tracking */
145
158
  return;
146
159
  }
147
160
  }
@@ -153,6 +166,56 @@ function getAbortReason(signal: AbortSignal): string {
153
166
  return "abort";
154
167
  }
155
168
 
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
+
156
219
  function hasCompletedAgentOutput(result: RuntimeResult): boolean {
157
220
  if (result.finalOutput.trim()) return true;
158
221
  return result.messages.some(
@@ -241,13 +304,20 @@ async function waitForSubagentProcess(
241
304
  }
242
305
 
243
306
  function buildModelDisplay(
244
- parentModel: { provider: string; id: string } | undefined,
307
+ effectiveModel: ChildModelSettings,
245
308
  thinking: ThinkingLevel,
246
309
  ): string | undefined {
247
- if (parentModel) {
248
- 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);
249
319
  }
250
- return thinking ? `thinking:${thinking}` : undefined;
320
+ return parts.length > 0 ? parts.join(" ・ ") : undefined;
251
321
  }
252
322
 
253
323
  const EMPTY_USAGE = {
@@ -366,7 +436,7 @@ async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
366
436
  await fs.promises.unlink(tmpPrompt.filePath);
367
437
  await fs.promises.rmdir(tmpPrompt.dir);
368
438
  } catch {
369
- /* ignore */
439
+ /* temp file cleanup failures are non-fatal; OS will clean up eventually */
370
440
  }
371
441
  }
372
442
 
@@ -387,18 +457,7 @@ async function cleanupPromptSetupResult(
387
457
  }
388
458
 
389
459
  function findRecentMessagesAnchor(messages: Message[]): number {
390
- for (let i = messages.length - 1; i >= 0; i--) {
391
- const msg = messages[i];
392
- if (
393
- msg?.role === "assistant" &&
394
- msg.content.some(
395
- (c) => c.type === "text" && (c as { text?: string }).text?.trim(),
396
- )
397
- ) {
398
- return i;
399
- }
400
- }
401
- return -1;
460
+ return findLastAssistantTextMessage(messages);
402
461
  }
403
462
 
404
463
  function deriveStreamingProgress(messages: Message[]): StreamingProgress {
@@ -431,6 +490,63 @@ function sanitizeProgressPreview(preview: string, toolName: string): string {
431
490
  return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
432
491
  }
433
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
+
434
550
  export function makeEmitUpdate(
435
551
  result: RuntimeResult,
436
552
  onUpdate: OnUpdateCallback | undefined,
@@ -448,46 +564,12 @@ export function makeEmitUpdate(
448
564
  const recentMessages =
449
565
  anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
450
566
  const progress = deriveStreamingProgress(msgs);
451
- // Preserve stored activity tree for tool-result completion signals
452
- // so the parent retains nested context until newer activity arrives
453
- if (options?.toolResultCompleted && result.progress?.activeToolActivity) {
454
- progress.activeToolActivity = result.progress.activeToolActivity;
455
- const renderedText = renderToolActivity(progress.activeToolActivity);
456
- if (renderedText !== undefined) progress.activityText = renderedText;
457
- else delete progress.activityText;
458
- }
459
- // Handle parsed tool activity from child events
460
- // Merge with parent activity if this is a nested update
461
- if (options?.toolActivity) {
462
- if (
463
- progress.activeToolActivity &&
464
- progress.activeToolActivity.toolName === options.toolActivity.toolName
465
- ) {
466
- // Merge: prefer parser inputSummary when non-empty and richer than bare toolName fallback
467
- const incomingSummary = options.toolActivity.inputSummary;
468
- const preferIncoming =
469
- incomingSummary && incomingSummary !== options.toolActivity.toolName;
470
- progress.activeToolActivity = {
471
- ...progress.activeToolActivity,
472
- inputSummary: preferIncoming
473
- ? incomingSummary
474
- : progress.activeToolActivity.inputSummary,
475
- instanceName:
476
- options.toolActivity.instanceName ??
477
- progress.activeToolActivity.instanceName,
478
- child:
479
- options.toolActivity.child ?? progress.activeToolActivity.child,
480
- };
481
- } else {
482
- progress.activeToolActivity = options.toolActivity;
483
- }
484
- const renderedActivity = renderToolActivity(progress.activeToolActivity);
485
- if (renderedActivity !== undefined)
486
- progress.activityText = renderedActivity;
487
- else delete progress.activityText;
488
- }
489
- if (options?.toolResultCompleted) {
490
- progress.toolResultCompleted = true;
567
+ if (options) {
568
+ applyActivityUpdates(
569
+ progress,
570
+ options,
571
+ result.progress?.activeToolActivity,
572
+ );
491
573
  }
492
574
  result.progress = progress;
493
575
  onUpdate?.({
@@ -637,18 +719,30 @@ function setupAbortHandler(
637
719
  return onAbort;
638
720
  }
639
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
+
640
734
  function buildPiArgs(
641
735
  agent: AgentConfig,
642
736
  task: string,
643
- parentModel: { provider: string; id: string } | undefined,
737
+ effectiveModel: ChildModelSettings,
644
738
  thinking: ThinkingLevel,
645
739
  resolvedSkills: { args: string[] },
646
740
  tmpPrompt: { filePath: string } | null,
647
741
  ): string[] {
648
742
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
649
- if (parentModel) {
650
- args.push("--provider", parentModel.provider, "--model", parentModel.id);
651
- }
743
+ if (effectiveModel.provider && effectiveModel.id)
744
+ args.push("--provider", effectiveModel.provider);
745
+ if (effectiveModel.id) args.push("--model", effectiveModel.id);
652
746
  args.push("--thinking", thinking);
653
747
  if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
654
748
  if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
@@ -722,7 +816,13 @@ async function finalizeResult(
722
816
  if (agentEndTimeoutExitCode !== undefined) {
723
817
  state.result.exitCode = agentEndTimeoutExitCode;
724
818
  }
725
- 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
+ }
726
826
  return state.result;
727
827
  }
728
828
 
@@ -750,9 +850,10 @@ export async function runSingleAgent(
750
850
  results: RuntimeResult[],
751
851
  options?: { includeMessages?: boolean; recentMessages?: Message[] },
752
852
  ) => SubagentDetails,
753
- parentModel: { provider: string; id: string } | undefined,
853
+ parentModel: ChildModelSettings | undefined,
754
854
  parentThinking: ThinkingLevel,
755
855
  debugEventDiagnostics = false,
856
+ options: RunSingleAgentOptions = {},
756
857
  ): Promise<SingleResult> {
757
858
  const agent = agents.find((a) => a.name === agentName);
758
859
  if (!agent) return errorForUnknownAgent(agentName, agents, task);
@@ -768,14 +869,16 @@ export async function runSingleAgent(
768
869
  );
769
870
  }
770
871
  const requestedThinking = agent.thinking ?? parentThinking;
771
- const { level: thinking, warning: thinkingWarning } = parentModel
772
- ? resolveThinkingLevel(
773
- requestedThinking,
774
- parentModel.provider,
775
- parentModel.id,
776
- )
777
- : { level: requestedThinking };
778
- 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);
779
882
  const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
780
883
  agent.skills
781
884
  ? resolveAgentSkillArgs(defaultCwd, agent.skills)
@@ -807,7 +910,7 @@ export async function runSingleAgent(
807
910
  const args = buildPiArgs(
808
911
  agent,
809
912
  task,
810
- parentModel,
913
+ effectiveModel,
811
914
  thinking,
812
915
  resolvedSkills,
813
916
  tmpPrompt,
@@ -845,12 +948,21 @@ export async function runSingleAgent(
845
948
  () => clearGraceTimer(state),
846
949
  requestTermination,
847
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);
848
959
  try {
849
960
  state.result.exitCode = (await processDone) ?? 0;
850
961
  return await finalizeResult(state, startedAt);
851
962
  } finally {
852
963
  clearGraceTimer(state);
853
964
  if (signal && onAbort) signal.removeEventListener("abort", onAbort);
965
+ void startSleepInhibitorRelease(sleepInhibitorPromise);
854
966
  }
855
967
  } finally {
856
968
  if (tmpPrompt) await cleanupTempPrompt(tmpPrompt);