@mystilleef/pi-subagent 0.10.2 → 0.11.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.
@@ -5,38 +5,24 @@
5
5
  */
6
6
 
7
7
  import { type ChildProcess, spawn } from "node:child_process";
8
- import * as fs from "node:fs";
9
8
  import readline from "node:readline";
10
- import {
11
- clampThinkingLevel,
12
- getModel,
13
- getSupportedThinkingLevels,
14
- type Message,
15
- type ModelThinkingLevel,
16
- } from "@earendil-works/pi-ai";
9
+ import type { Message } from "@earendil-works/pi-ai";
17
10
  import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
18
11
  import { getFinalOutput } from "../output/ui.js";
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";
12
+ import { serializeSamplingParams } from "../shared/sampling.js";
22
13
  import {
23
14
  type OnUpdateCallback,
24
15
  type SingleResult,
25
- type StreamingProgress,
26
16
  type SubagentDetails,
27
17
  TOOL_RESULT_FAILED_MESSAGE,
28
- type ToolActivity,
29
18
  } from "../shared/types.js";
30
19
  import {
31
20
  detectMessageError,
32
- findLastAssistantTextMessage,
33
21
  getPiInvocation,
34
22
  getSubagentDepth,
35
23
  getSubagentRuntimeLimits,
36
24
  resolveAgentSkillArgs,
37
25
  subagentDepthEnv,
38
- truncateOutput,
39
- writePromptToTempFile,
40
26
  } from "../shared/utils.js";
41
27
  import {
42
28
  type ChildEventParseResult,
@@ -44,7 +30,34 @@ import {
44
30
  parseChildEventLine,
45
31
  TOOL_EXECUTION_UPDATE_EVENT,
46
32
  } from "./child-events.js";
33
+ import { getLatestOutcomeFromMessages } from "./complete-outcome.js";
34
+ import {
35
+ buildModelDisplay,
36
+ type ChildModelSettings,
37
+ resolveEffectiveChildModelSettings,
38
+ resolveThinkingLevel,
39
+ } from "./model-resolution.js";
40
+ import {
41
+ appendWithByteLimit,
42
+ resolveCompleteExtensionPath,
43
+ resolveSamplingExtensionPath,
44
+ } from "./process-utils.js";
47
45
  import { appendSubagentResultContract } from "./prompt-contract.js";
46
+ import {
47
+ beginPromptSetup,
48
+ cleanupPromptSetupResult,
49
+ cleanupTempPrompt,
50
+ } from "./prompt-setup.js";
51
+ import {
52
+ addMessageToResult,
53
+ createErrorResult,
54
+ errorForDepthLimit,
55
+ errorForUnknownAgent,
56
+ initRuntimeResult,
57
+ type RuntimeResult,
58
+ rebuildResultFromMessages,
59
+ } from "./result-builder.js";
60
+ import { type EmitUpdateFn, makeEmitUpdate } from "./streaming-progress.js";
48
61
  import {
49
62
  acquireChildSleepInhibitor,
50
63
  getProcessTreeSpawnOptions,
@@ -54,35 +67,13 @@ import {
54
67
  terminateChildProcess,
55
68
  } from "./termination.js";
56
69
 
57
- export function resolveThinkingLevel(
58
- requested: ThinkingLevel,
59
- provider: string,
60
- modelId: string,
61
- ): { level: ThinkingLevel; warning?: string } {
62
- const model = getModel(provider as never, modelId as never);
63
- if (!model) return { level: requested };
64
- const mkWarning = (effective: ThinkingLevel) =>
65
- `Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
66
- if (model.reasoning === false) {
67
- return { level: "off", warning: mkWarning("off") };
68
- }
69
- if (!model.thinkingLevelMap) return { level: requested };
70
- const supported = getSupportedThinkingLevels(model);
71
- if (supported.length === 0) return { level: requested };
72
- const clamped = clampThinkingLevel(
73
- model,
74
- requested as ModelThinkingLevel,
75
- ) as ThinkingLevel;
76
- if (clamped === requested) return { level: requested };
77
- return { level: clamped, warning: mkWarning(clamped) };
78
- }
70
+ const COMPLETE_EXTENSION_PATH = resolveCompleteExtensionPath();
71
+ const SAMPLING_EXTENSION_PATH = resolveSamplingExtensionPath();
72
+
73
+ export { resolveThinkingLevel } from "./model-resolution.js";
74
+ export { makeEmitUpdate } from "./streaming-progress.js";
79
75
 
80
76
  type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
81
- type RuntimeResult = SingleResult & { messages: Message[] };
82
- type ChildModelSettings = {
83
- provider?: string | undefined;
84
- id?: string | undefined;
85
- };
86
77
  type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
87
78
 
88
79
  type RunSingleAgentOptions = {
@@ -90,18 +81,9 @@ type RunSingleAgentOptions = {
90
81
  getOrchestratorPid?: () => unknown;
91
82
  };
92
83
 
93
- export class SubagentAbortError extends Error {
94
- readonly result: SingleResult;
95
- constructor(result: SingleResult) {
96
- super("Subagent was aborted");
97
- this.name = "SubagentAbortError";
98
- this.result = result;
99
- }
100
- }
101
-
102
- type TempPrompt = { dir: string; filePath: string };
103
-
104
- type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
84
+ export type RunSingleAgentResult =
85
+ | { kind: "completed"; result: SingleResult }
86
+ | { kind: "aborted"; result: SingleResult };
105
87
 
106
88
  interface SubagentState {
107
89
  result: RuntimeResult;
@@ -112,53 +94,6 @@ interface SubagentState {
112
94
  terminationPromise?: Promise<unknown>;
113
95
  }
114
96
 
115
- function appendWithByteLimit(
116
- current: string,
117
- data: string | Buffer,
118
- max: number,
119
- ): string {
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 "";
138
- }
139
-
140
- /**
141
- * Rationale: Subagent usage reporting needs context window awareness to provide
142
- * meaningful "context full" indicators to the parent.
143
- */
144
- function resolveContextWindowTokens(msg: Message): number | undefined {
145
- const m = msg as unknown as Record<string, unknown>;
146
- if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
147
- return;
148
- try {
149
- const contextWindow = getModel(
150
- m["provider"] as never,
151
- m["model"] as never,
152
- )?.contextWindow;
153
- return Number.isFinite(contextWindow) && contextWindow > 0
154
- ? contextWindow
155
- : undefined;
156
- } catch {
157
- /* model lookup failures return undefined to skip context window tracking */
158
- return;
159
- }
160
- }
161
-
162
97
  function getAbortReason(signal: AbortSignal): string {
163
98
  const { reason } = signal;
164
99
  if (reason instanceof Error && reason.message) return reason.message;
@@ -216,15 +151,13 @@ function startSleepInhibitorRelease(
216
151
  return acquisitionPromise.then(releaseSleepInhibitor, () => {});
217
152
  }
218
153
 
219
- function hasCompletedAgentOutput(result: RuntimeResult): boolean {
154
+ export function hasCompletedAgentOutput(
155
+ result: RuntimeResult,
156
+ outcome?: string,
157
+ ): boolean {
220
158
  if (result.finalOutput.trim()) return true;
221
- return result.messages.some(
222
- (msg) =>
223
- msg.role === "assistant" &&
224
- msg.content.some(
225
- (part) => part.type === "text" && Boolean(part.text?.trim()),
226
- ),
227
- );
159
+ if (outcome?.trim()) return true;
160
+ return getFinalOutput(result.messages).trim().length > 0;
228
161
  }
229
162
 
230
163
  /**
@@ -232,15 +165,16 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
232
165
  * we force-kill them after a grace period and treat it as success (0) if they
233
166
  * actually produced output.
234
167
  */
235
- function getAgentEndTimeoutExitCode(
168
+ export function getAgentEndTimeoutExitCode(
236
169
  result: RuntimeResult,
237
170
  spawnError: Error | undefined,
171
+ outcome: string | undefined,
238
172
  ): number | undefined {
239
173
  if (result.termination?.cancelReason !== "agent_end_timeout") return;
240
174
  if (spawnError) return;
241
175
  if (result.stopReason === "error" || result.stopReason === "aborted") return;
242
176
  if (result.errorMessage?.trim()) return;
243
- return hasCompletedAgentOutput(result) ? 0 : 1;
177
+ return hasCompletedAgentOutput(result, outcome) ? 0 : 1;
244
178
  }
245
179
 
246
180
  /**
@@ -303,287 +237,6 @@ async function waitForSubagentProcess(
303
237
  });
304
238
  }
305
239
 
306
- function buildModelDisplay(
307
- effectiveModel: ChildModelSettings,
308
- thinking: ThinkingLevel,
309
- ): string | undefined {
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);
319
- }
320
- return parts.length > 0 ? parts.join(" ・ ") : undefined;
321
- }
322
-
323
- const EMPTY_USAGE = {
324
- input: 0,
325
- output: 0,
326
- cacheRead: 0,
327
- cacheWrite: 0,
328
- cost: 0,
329
- contextTokens: 0,
330
- turns: 0,
331
- };
332
-
333
- function initRuntimeResult(
334
- agentName: string,
335
- source: "user" | "project" | "unknown",
336
- task: string,
337
- modelDisplay: string | undefined,
338
- ): RuntimeResult {
339
- return {
340
- agent: agentName,
341
- agentSource: source,
342
- task,
343
- exitCode: 0,
344
- finalOutput: "",
345
- messages: [],
346
- stderr: "",
347
- usage: { ...EMPTY_USAGE },
348
- model: modelDisplay,
349
- };
350
- }
351
-
352
- function accumulateUsage(result: RuntimeResult, msg: Message): void {
353
- if (msg.role !== "assistant") return;
354
- result.usage.turns++;
355
- const { usage } = msg;
356
- if (!usage) return;
357
- result.usage.input += usage.input || 0;
358
- result.usage.output += usage.output || 0;
359
- result.usage.cacheRead += usage.cacheRead || 0;
360
- result.usage.cacheWrite += usage.cacheWrite || 0;
361
- result.usage.cost += usage.cost?.total || 0;
362
- result.usage.contextTokens = usage.totalTokens || 0;
363
- const ctxWindowTokens = resolveContextWindowTokens(msg);
364
- if (ctxWindowTokens !== undefined)
365
- result.usage.contextWindowTokens = ctxWindowTokens;
366
- }
367
-
368
- function addMessageToResult(result: RuntimeResult, msg: Message): void {
369
- result.messages.push(msg);
370
- result.finalOutput = truncateOutput(getFinalOutput(result.messages));
371
- if (msg.role === "toolResult" && msg.isError) {
372
- result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
373
- } else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
374
- delete result.errorMessage;
375
- }
376
- if (msg.role === "assistant") {
377
- accumulateUsage(result, msg);
378
- if (!result.model && msg.model) result.model = msg.model;
379
- if (msg.stopReason) result.stopReason = msg.stopReason;
380
- if (msg.errorMessage) result.errorMessage = msg.errorMessage;
381
- }
382
- }
383
-
384
- function createErrorResult(
385
- agent: string,
386
- source: "user" | "project" | "unknown",
387
- task: string,
388
- error: string,
389
- model?: string,
390
- ): SingleResult {
391
- return {
392
- agent,
393
- agentSource: source,
394
- task,
395
- exitCode: 1,
396
- finalOutput: "",
397
- stderr: error,
398
- usage: { ...EMPTY_USAGE },
399
- model,
400
- };
401
- }
402
-
403
- function errorForUnknownAgent(
404
- agentName: string,
405
- agents: AgentConfig[],
406
- task: string,
407
- ): SingleResult {
408
- const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
409
- return createErrorResult(
410
- agentName,
411
- "unknown",
412
- task,
413
- `Unknown agent: "${agentName}". Available agents: ${available}.`,
414
- );
415
- }
416
-
417
- function errorForDepthLimit(
418
- agentName: string,
419
- source: "user" | "project" | "unknown",
420
- task: string,
421
- depth: number,
422
- maxDepth: number,
423
- model?: string,
424
- ): SingleResult {
425
- return createErrorResult(
426
- agentName,
427
- source,
428
- task,
429
- `Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
430
- model,
431
- );
432
- }
433
-
434
- async function cleanupTempPrompt(tmpPrompt: TempPrompt): Promise<void> {
435
- try {
436
- await fs.promises.unlink(tmpPrompt.filePath);
437
- await fs.promises.rmdir(tmpPrompt.dir);
438
- } catch {
439
- /* temp file cleanup failures are non-fatal; OS will clean up eventually */
440
- }
441
- }
442
-
443
- function beginPromptSetup(agent: AgentConfig): Promise<PromptSetupResult> {
444
- if (!agent.systemPrompt.trim()) return Promise.resolve({ tmpPrompt: null });
445
- return writePromptToTempFile(agent.name, agent.systemPrompt).then(
446
- (tmpPrompt) => ({ tmpPrompt }),
447
- (error: unknown) => ({ error }),
448
- );
449
- }
450
-
451
- async function cleanupPromptSetupResult(
452
- setup: PromptSetupResult,
453
- ): Promise<void> {
454
- if ("tmpPrompt" in setup && setup.tmpPrompt) {
455
- await cleanupTempPrompt(setup.tmpPrompt);
456
- }
457
- }
458
-
459
- function findRecentMessagesAnchor(messages: Message[]): number {
460
- return findLastAssistantTextMessage(messages);
461
- }
462
-
463
- function deriveStreamingProgress(messages: Message[]): StreamingProgress {
464
- const toolCalls: { id: string; preview: string }[] = [];
465
- let lastToolPreview: string | undefined;
466
- let activeToolActivity: ToolActivity | undefined;
467
- for (const msg of messages) {
468
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
469
- for (const part of msg.content) {
470
- if (!isToolCallPart(part)) continue;
471
- const preview = sanitizeProgressPreview(
472
- makeToolPreview(part.name, part.arguments),
473
- part.name,
474
- );
475
- toolCalls.push({ id: part.id, preview });
476
- lastToolPreview = preview;
477
- activeToolActivity = { toolName: part.name, inputSummary: preview };
478
- }
479
- }
480
- const activityText = renderToolActivity(activeToolActivity);
481
- return {
482
- activeToolActivity,
483
- activityText,
484
- toolCalls,
485
- lastToolPreview,
486
- };
487
- }
488
-
489
- function sanitizeProgressPreview(preview: string, toolName: string): string {
490
- return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
491
- }
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
-
550
- export function makeEmitUpdate(
551
- result: RuntimeResult,
552
- onUpdate: OnUpdateCallback | undefined,
553
- makeDetails: (
554
- results: RuntimeResult[],
555
- options?: { includeMessages?: boolean; recentMessages?: Message[] },
556
- ) => SubagentDetails,
557
- ): (options?: {
558
- toolActivity?: ToolActivity;
559
- toolResultCompleted?: boolean;
560
- }) => void {
561
- return (options) => {
562
- const msgs = result.messages;
563
- const anchorIdx = findRecentMessagesAnchor(msgs);
564
- const recentMessages =
565
- anchorIdx >= 0 ? msgs.slice(anchorIdx) : msgs.slice(-5);
566
- const progress = deriveStreamingProgress(msgs);
567
- if (options) {
568
- applyActivityUpdates(
569
- progress,
570
- options,
571
- result.progress?.activeToolActivity,
572
- );
573
- }
574
- result.progress = progress;
575
- onUpdate?.({
576
- content: [
577
- { type: "text", text: progress.activityText ?? "(running...)" },
578
- ],
579
- details: makeDetails([result], {
580
- includeMessages: true,
581
- recentMessages,
582
- }),
583
- });
584
- };
585
- }
586
-
587
240
  function makeRequestTerminator(
588
241
  proc: ChildProcess,
589
242
  terminateOptions: {
@@ -613,10 +266,7 @@ function clearGraceTimer(state: SubagentState): void {
613
266
  function handleMessageEvent(
614
267
  event: ChildKnownEvent,
615
268
  state: SubagentState,
616
- emitUpdate: (options?: {
617
- toolActivity?: ToolActivity;
618
- toolResultCompleted?: boolean;
619
- }) => void,
269
+ emitUpdate: EmitUpdateFn,
620
270
  ): void {
621
271
  if (event.type !== "message_end" && event.type !== "tool_result_end") return;
622
272
  if (event.message) {
@@ -628,10 +278,7 @@ function handleMessageEvent(
628
278
 
629
279
  function handleToolExecutionUpdateEvent(
630
280
  event: ChildKnownEvent,
631
- emitUpdate: (options?: {
632
- toolActivity?: ToolActivity;
633
- toolResultCompleted?: boolean;
634
- }) => void,
281
+ emitUpdate: EmitUpdateFn,
635
282
  ): void {
636
283
  if (event.type !== TOOL_EXECUTION_UPDATE_EVENT) return;
637
284
  emitUpdate({ toolActivity: event.toolActivity });
@@ -640,17 +287,12 @@ function handleToolExecutionUpdateEvent(
640
287
  function handleAgentEndEvent(
641
288
  event: ChildKnownEvent,
642
289
  state: SubagentState,
643
- emitUpdate: (options?: {
644
- toolActivity?: ToolActivity;
645
- toolResultCompleted?: boolean;
646
- }) => void,
290
+ emitUpdate: EmitUpdateFn,
647
291
  requestTermination: (reason: string) => Promise<unknown>,
648
292
  ): void {
649
293
  if (event.type !== "agent_end") return;
650
- if (state.result.messages.length === 0 && Array.isArray(event.messages)) {
651
- for (const msg of event.messages as Message[]) {
652
- addMessageToResult(state.result, msg);
653
- }
294
+ if (Array.isArray(event.messages) && event.messages.length > 0) {
295
+ rebuildResultFromMessages(state.result, event.messages as Message[]);
654
296
  emitUpdate();
655
297
  }
656
298
  if (state.agentEndGraceTimer || state.terminationPromise) return;
@@ -677,10 +319,7 @@ function formatUnknownEventDiagnostic(
677
319
  function processEventLine(
678
320
  line: string,
679
321
  state: SubagentState,
680
- emitUpdate: (options?: {
681
- toolActivity?: ToolActivity;
682
- toolResultCompleted?: boolean;
683
- }) => void,
322
+ emitUpdate: EmitUpdateFn,
684
323
  requestTermination: (reason: string) => Promise<unknown>,
685
324
  debugEventDiagnostics: boolean,
686
325
  ): void {
@@ -719,16 +358,11 @@ function setupAbortHandler(
719
358
  return onAbort;
720
359
  }
721
360
 
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
- };
361
+ function buildSamplingEnv(agent: AgentConfig): string | undefined {
362
+ return serializeSamplingParams({
363
+ temperature: agent.temperature,
364
+ topP: agent.topP,
365
+ });
732
366
  }
733
367
 
734
368
  function buildPiArgs(
@@ -744,11 +378,16 @@ function buildPiArgs(
744
378
  args.push("--provider", effectiveModel.provider);
745
379
  if (effectiveModel.id) args.push("--model", effectiveModel.id);
746
380
  args.push("--thinking", thinking);
747
- if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
381
+ if (agent.tools) {
382
+ const tools = new Set(agent.tools);
383
+ tools.add("complete");
384
+ args.push("--tools", [...tools].join(","));
385
+ }
748
386
  if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
749
387
  if (tmpPrompt) {
750
388
  args.push("--append-system-prompt", tmpPrompt.filePath);
751
389
  }
390
+ args.push("--extension", COMPLETE_EXTENSION_PATH);
752
391
  const taskPrompt = task
753
392
  ? `Task: ${task}`
754
393
  : "Run according to your system prompt. If no explicit task was provided, use the default context described there.";
@@ -756,13 +395,19 @@ function buildPiArgs(
756
395
  return args;
757
396
  }
758
397
 
398
+ function buildChildEnv(samplingEnv: string | undefined): NodeJS.ProcessEnv {
399
+ const { PI_SAMPLING_PARAMS: _, ...parentEnv } = process.env;
400
+ return {
401
+ ...parentEnv,
402
+ ...subagentDepthEnv(),
403
+ ...(samplingEnv ? { PI_SAMPLING_PARAMS: samplingEnv } : {}),
404
+ };
405
+ }
406
+
759
407
  function setupChildProcess(
760
408
  proc: ChildProcess,
761
409
  state: SubagentState,
762
- emitUpdate: (options?: {
763
- toolActivity?: ToolActivity;
764
- toolResultCompleted?: boolean;
765
- }) => void,
410
+ emitUpdate: EmitUpdateFn,
766
411
  requestTermination: (reason: string) => Promise<unknown>,
767
412
  debugEventDiagnostics: boolean,
768
413
  ): void {
@@ -801,17 +446,16 @@ function setupChildProcess(
801
446
  async function finalizeResult(
802
447
  state: SubagentState,
803
448
  startedAt: number,
804
- ): Promise<SingleResult> {
449
+ ): Promise<RunSingleAgentResult> {
805
450
  state.result.durationMs = Date.now() - startedAt;
806
451
  clearGraceTimer(state);
807
452
  if (state.terminationPromise) await state.terminationPromise;
808
453
  if (state.spawnError) state.result.exitCode = 1;
809
- if (detectMessageError(state.result.messages)) {
810
- state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
811
- }
454
+ const outcome = getLatestOutcomeFromMessages(state.result.messages);
812
455
  const agentEndTimeoutExitCode = getAgentEndTimeoutExitCode(
813
456
  state.result,
814
457
  state.spawnError,
458
+ outcome,
815
459
  );
816
460
  if (agentEndTimeoutExitCode !== undefined) {
817
461
  state.result.exitCode = agentEndTimeoutExitCode;
@@ -819,11 +463,25 @@ async function finalizeResult(
819
463
  if (state.result.termination?.cancelReason === "agent_end_timeout") {
820
464
  state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
821
465
  }
466
+ // Skip unresolved-error detection when complete succeeded: intermediate tool
467
+ // errors (e.g. a linter exiting non-zero) are expected and already addressed.
468
+ if (!outcome && detectMessageError(state.result.messages)) {
469
+ state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
470
+ }
822
471
  if (state.wasAborted) {
823
472
  state.result.stderr = "";
824
- throw new SubagentAbortError(state.result);
473
+ return { kind: "aborted", result: state.result };
474
+ }
475
+ const isSemanticallySuccessful =
476
+ !state.spawnError &&
477
+ state.result.exitCode === 0 &&
478
+ (Boolean(outcome) ||
479
+ (!state.result.errorMessage?.trim() &&
480
+ !detectMessageError(state.result.messages)));
481
+ if (isSemanticallySuccessful && outcome) {
482
+ state.result.outcome = outcome;
825
483
  }
826
- return state.result;
484
+ return { kind: "completed", result: state.result };
827
485
  }
828
486
 
829
487
  /**
@@ -854,19 +512,26 @@ export async function runSingleAgent(
854
512
  parentThinking: ThinkingLevel,
855
513
  debugEventDiagnostics = false,
856
514
  options: RunSingleAgentOptions = {},
857
- ): Promise<SingleResult> {
515
+ ): Promise<RunSingleAgentResult> {
858
516
  const agent = agents.find((a) => a.name === agentName);
859
- if (!agent) return errorForUnknownAgent(agentName, agents, task);
517
+ if (!agent)
518
+ return {
519
+ kind: "completed",
520
+ result: errorForUnknownAgent(agentName, agents, task),
521
+ };
860
522
  const runtimeLimits = getSubagentRuntimeLimits();
861
523
  const depth = getSubagentDepth();
862
524
  if (depth >= runtimeLimits.maxDepth) {
863
- return errorForDepthLimit(
864
- agentName,
865
- agent.source,
866
- task,
867
- depth,
868
- runtimeLimits.maxDepth,
869
- );
525
+ return {
526
+ kind: "completed",
527
+ result: errorForDepthLimit(
528
+ agentName,
529
+ agent.source,
530
+ task,
531
+ depth,
532
+ runtimeLimits.maxDepth,
533
+ ),
534
+ };
870
535
  }
871
536
  const requestedThinking = agent.thinking ?? parentThinking;
872
537
  const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
@@ -888,13 +553,16 @@ export async function runSingleAgent(
888
553
  if ("error" in resolvedSkills) {
889
554
  const promptSetup = await promptSetupPromise;
890
555
  await cleanupPromptSetupResult(promptSetup);
891
- return createErrorResult(
892
- agentName,
893
- agent.source,
894
- task,
895
- resolvedSkills.error,
896
- modelDisplay,
897
- );
556
+ return {
557
+ kind: "completed",
558
+ result: createErrorResult(
559
+ agentName,
560
+ agent.source,
561
+ task,
562
+ resolvedSkills.error,
563
+ modelDisplay,
564
+ ),
565
+ };
898
566
  }
899
567
  const promptSetup = await promptSetupPromise;
900
568
  if ("error" in promptSetup) throw promptSetup.error;
@@ -906,6 +574,7 @@ export async function runSingleAgent(
906
574
  };
907
575
  if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
908
576
  const tmpPrompt = promptSetup.tmpPrompt;
577
+ const samplingEnv = buildSamplingEnv(agent);
909
578
  try {
910
579
  const args = buildPiArgs(
911
580
  agent,
@@ -915,17 +584,21 @@ export async function runSingleAgent(
915
584
  resolvedSkills,
916
585
  tmpPrompt,
917
586
  );
587
+ if (samplingEnv) {
588
+ args.push("--extension", SAMPLING_EXTENSION_PATH);
589
+ }
918
590
  const invocation = getPiInvocation(args);
919
591
  const terminateOptions = {
920
592
  tree: true,
921
593
  platform: process.platform,
922
594
  processTreeDetached: process.platform !== "win32",
923
595
  };
596
+ const childEnv = buildChildEnv(samplingEnv);
924
597
  const proc = spawn(invocation.command, invocation.args, {
925
598
  cwd: defaultCwd,
926
599
  shell: invocation.command === "pi" && process.platform === "win32",
927
600
  stdio: ["ignore", "pipe", "pipe"],
928
- env: { ...process.env, ...subagentDepthEnv() },
601
+ env: childEnv,
929
602
  ...getProcessTreeSpawnOptions(terminateOptions.tree),
930
603
  });
931
604
  const processDone = waitForSubagentProcess(proc);