@mystilleef/pi-subagent 0.10.2 → 0.12.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,25 @@
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,
24
+ resolveAgentExtensionPaths,
36
25
  resolveAgentSkillArgs,
37
26
  subagentDepthEnv,
38
- truncateOutput,
39
- writePromptToTempFile,
40
27
  } from "../shared/utils.js";
41
28
  import {
42
29
  type ChildEventParseResult,
@@ -44,7 +31,35 @@ import {
44
31
  parseChildEventLine,
45
32
  TOOL_EXECUTION_UPDATE_EVENT,
46
33
  } from "./child-events.js";
47
- import { appendSubagentResultContract } from "./prompt-contract.js";
34
+ import { getLatestOutcomeFromMessages } from "./complete-outcome.js";
35
+ import {
36
+ buildModelDisplay,
37
+ type ChildModelSettings,
38
+ resolveEffectiveChildModelSettings,
39
+ resolveThinkingLevel,
40
+ } from "./model-resolution.js";
41
+ import {
42
+ appendWithByteLimit,
43
+ resolveCompleteExtensionPath,
44
+ resolvePackageExtensionPath,
45
+ resolveSamplingExtensionPath,
46
+ } from "./process-utils.js";
47
+ import { SUBAGENT_RESULT_CONTRACT } from "./prompt-contract.js";
48
+ import {
49
+ beginPromptSetup,
50
+ cleanupPromptSetupResult,
51
+ cleanupTempPrompt,
52
+ } from "./prompt-setup.js";
53
+ import {
54
+ addMessageToResult,
55
+ createErrorResult,
56
+ errorForDepthLimit,
57
+ errorForUnknownAgent,
58
+ initRuntimeResult,
59
+ type RuntimeResult,
60
+ rebuildResultFromMessages,
61
+ } from "./result-builder.js";
62
+ import { type EmitUpdateFn, makeEmitUpdate } from "./streaming-progress.js";
48
63
  import {
49
64
  acquireChildSleepInhibitor,
50
65
  getProcessTreeSpawnOptions,
@@ -54,35 +69,14 @@ import {
54
69
  terminateChildProcess,
55
70
  } from "./termination.js";
56
71
 
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
- }
72
+ const COMPLETE_EXTENSION_PATH = resolveCompleteExtensionPath();
73
+ const SAMPLING_EXTENSION_PATH = resolveSamplingExtensionPath();
74
+ const PACKAGE_EXTENSION_PATH = resolvePackageExtensionPath();
75
+
76
+ export { resolveThinkingLevel } from "./model-resolution.js";
77
+ export { makeEmitUpdate } from "./streaming-progress.js";
79
78
 
80
79
  type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
81
- type RuntimeResult = SingleResult & { messages: Message[] };
82
- type ChildModelSettings = {
83
- provider?: string | undefined;
84
- id?: string | undefined;
85
- };
86
80
  type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
87
81
 
88
82
  type RunSingleAgentOptions = {
@@ -90,18 +84,9 @@ type RunSingleAgentOptions = {
90
84
  getOrchestratorPid?: () => unknown;
91
85
  };
92
86
 
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 };
87
+ export type RunSingleAgentResult =
88
+ | { kind: "completed"; result: SingleResult }
89
+ | { kind: "aborted"; result: SingleResult };
105
90
 
106
91
  interface SubagentState {
107
92
  result: RuntimeResult;
@@ -112,53 +97,6 @@ interface SubagentState {
112
97
  terminationPromise?: Promise<unknown>;
113
98
  }
114
99
 
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
100
  function getAbortReason(signal: AbortSignal): string {
163
101
  const { reason } = signal;
164
102
  if (reason instanceof Error && reason.message) return reason.message;
@@ -216,15 +154,13 @@ function startSleepInhibitorRelease(
216
154
  return acquisitionPromise.then(releaseSleepInhibitor, () => {});
217
155
  }
218
156
 
219
- function hasCompletedAgentOutput(result: RuntimeResult): boolean {
157
+ export function hasCompletedAgentOutput(
158
+ result: RuntimeResult,
159
+ outcome?: string,
160
+ ): boolean {
220
161
  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
- );
162
+ if (outcome?.trim()) return true;
163
+ return getFinalOutput(result.messages).trim().length > 0;
228
164
  }
229
165
 
230
166
  /**
@@ -232,15 +168,16 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
232
168
  * we force-kill them after a grace period and treat it as success (0) if they
233
169
  * actually produced output.
234
170
  */
235
- function getAgentEndTimeoutExitCode(
171
+ export function getAgentEndTimeoutExitCode(
236
172
  result: RuntimeResult,
237
173
  spawnError: Error | undefined,
174
+ outcome: string | undefined,
238
175
  ): number | undefined {
239
176
  if (result.termination?.cancelReason !== "agent_end_timeout") return;
240
177
  if (spawnError) return;
241
178
  if (result.stopReason === "error" || result.stopReason === "aborted") return;
242
179
  if (result.errorMessage?.trim()) return;
243
- return hasCompletedAgentOutput(result) ? 0 : 1;
180
+ return hasCompletedAgentOutput(result, outcome) ? 0 : 1;
244
181
  }
245
182
 
246
183
  /**
@@ -303,287 +240,6 @@ async function waitForSubagentProcess(
303
240
  });
304
241
  }
305
242
 
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
243
  function makeRequestTerminator(
588
244
  proc: ChildProcess,
589
245
  terminateOptions: {
@@ -613,10 +269,7 @@ function clearGraceTimer(state: SubagentState): void {
613
269
  function handleMessageEvent(
614
270
  event: ChildKnownEvent,
615
271
  state: SubagentState,
616
- emitUpdate: (options?: {
617
- toolActivity?: ToolActivity;
618
- toolResultCompleted?: boolean;
619
- }) => void,
272
+ emitUpdate: EmitUpdateFn,
620
273
  ): void {
621
274
  if (event.type !== "message_end" && event.type !== "tool_result_end") return;
622
275
  if (event.message) {
@@ -628,10 +281,7 @@ function handleMessageEvent(
628
281
 
629
282
  function handleToolExecutionUpdateEvent(
630
283
  event: ChildKnownEvent,
631
- emitUpdate: (options?: {
632
- toolActivity?: ToolActivity;
633
- toolResultCompleted?: boolean;
634
- }) => void,
284
+ emitUpdate: EmitUpdateFn,
635
285
  ): void {
636
286
  if (event.type !== TOOL_EXECUTION_UPDATE_EVENT) return;
637
287
  emitUpdate({ toolActivity: event.toolActivity });
@@ -640,17 +290,12 @@ function handleToolExecutionUpdateEvent(
640
290
  function handleAgentEndEvent(
641
291
  event: ChildKnownEvent,
642
292
  state: SubagentState,
643
- emitUpdate: (options?: {
644
- toolActivity?: ToolActivity;
645
- toolResultCompleted?: boolean;
646
- }) => void,
293
+ emitUpdate: EmitUpdateFn,
647
294
  requestTermination: (reason: string) => Promise<unknown>,
648
295
  ): void {
649
296
  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
- }
297
+ if (Array.isArray(event.messages) && event.messages.length > 0) {
298
+ rebuildResultFromMessages(state.result, event.messages as Message[]);
654
299
  emitUpdate();
655
300
  }
656
301
  if (state.agentEndGraceTimer || state.terminationPromise) return;
@@ -677,10 +322,7 @@ function formatUnknownEventDiagnostic(
677
322
  function processEventLine(
678
323
  line: string,
679
324
  state: SubagentState,
680
- emitUpdate: (options?: {
681
- toolActivity?: ToolActivity;
682
- toolResultCompleted?: boolean;
683
- }) => void,
325
+ emitUpdate: EmitUpdateFn,
684
326
  requestTermination: (reason: string) => Promise<unknown>,
685
327
  debugEventDiagnostics: boolean,
686
328
  ): void {
@@ -719,50 +361,101 @@ function setupAbortHandler(
719
361
  return onAbort;
720
362
  }
721
363
 
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
- };
364
+ function buildSamplingEnv(agent: AgentConfig): string | undefined {
365
+ return serializeSamplingParams({
366
+ temperature: agent.temperature,
367
+ topP: agent.topP,
368
+ });
732
369
  }
733
370
 
734
- function buildPiArgs(
735
- agent: AgentConfig,
736
- task: string,
737
- effectiveModel: ChildModelSettings,
738
- thinking: ThinkingLevel,
739
- resolvedSkills: { args: string[] },
740
- tmpPrompt: { filePath: string } | null,
741
- ): string[] {
742
- const args: string[] = ["--mode", "json", "-p", "--no-session", "--approve"];
371
+ export interface BuildPiArgsConfig {
372
+ agent: AgentConfig;
373
+ task: string;
374
+ effectiveModel: ChildModelSettings;
375
+ thinking: ThinkingLevel;
376
+ resolvedSkills: { args: string[] };
377
+ tmpPrompt: { filePath: string } | null;
378
+ resolvedExtensionPaths?: string[] | undefined;
379
+ samplingEnv?: string | undefined;
380
+ }
381
+
382
+ export function buildPiArgs(config: BuildPiArgsConfig): string[] {
383
+ const {
384
+ agent,
385
+ task,
386
+ effectiveModel,
387
+ thinking,
388
+ resolvedSkills,
389
+ tmpPrompt,
390
+ resolvedExtensionPaths,
391
+ samplingEnv,
392
+ } = config;
393
+ const args: string[] = [
394
+ "--mode",
395
+ "json",
396
+ "-p",
397
+ "--no-session",
398
+ "--approve",
399
+ "--no-themes",
400
+ "--no-prompt-templates",
401
+ ];
402
+ if (agent.extensions !== undefined) {
403
+ args.push("--no-extensions");
404
+ }
743
405
  if (effectiveModel.provider && effectiveModel.id)
744
406
  args.push("--provider", effectiveModel.provider);
745
407
  if (effectiveModel.id) args.push("--model", effectiveModel.id);
746
408
  args.push("--thinking", thinking);
747
- if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
748
- if (agent.skills) args.push("--no-skills", ...resolvedSkills.args);
409
+ if (agent.tools) {
410
+ const tools = new Set(agent.tools);
411
+ tools.add("complete");
412
+ args.push("--tools", [...tools].join(","));
413
+ }
414
+ if (agent.skills !== undefined)
415
+ args.push("--no-skills", ...resolvedSkills.args);
416
+ if (agent.context === false) args.push("--no-context-files");
749
417
  if (tmpPrompt) {
750
- args.push("--append-system-prompt", tmpPrompt.filePath);
418
+ if (agent.replacePrompt) {
419
+ args.push("--system-prompt", tmpPrompt.filePath);
420
+ } else {
421
+ args.push("--append-system-prompt", tmpPrompt.filePath);
422
+ }
751
423
  }
424
+ if (agent.extensions !== undefined) {
425
+ args.push("--extension", PACKAGE_EXTENSION_PATH);
426
+ if (resolvedExtensionPaths && resolvedExtensionPaths.length > 0) {
427
+ for (const rp of resolvedExtensionPaths) {
428
+ if (rp !== PACKAGE_EXTENSION_PATH) {
429
+ args.push("--extension", rp);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ args.push("--extension", COMPLETE_EXTENSION_PATH);
435
+ if (samplingEnv) {
436
+ args.push("--extension", SAMPLING_EXTENSION_PATH);
437
+ }
438
+ args.push("--append-system-prompt", SUBAGENT_RESULT_CONTRACT);
752
439
  const taskPrompt = task
753
440
  ? `Task: ${task}`
754
441
  : "Run according to your system prompt. If no explicit task was provided, use the default context described there.";
755
- args.push(appendSubagentResultContract(taskPrompt));
442
+ args.push(taskPrompt);
756
443
  return args;
757
444
  }
758
445
 
446
+ function buildChildEnv(samplingEnv: string | undefined): NodeJS.ProcessEnv {
447
+ const { PI_SAMPLING_PARAMS: _, ...parentEnv } = process.env;
448
+ return {
449
+ ...parentEnv,
450
+ ...subagentDepthEnv(),
451
+ ...(samplingEnv ? { PI_SAMPLING_PARAMS: samplingEnv } : {}),
452
+ };
453
+ }
454
+
759
455
  function setupChildProcess(
760
456
  proc: ChildProcess,
761
457
  state: SubagentState,
762
- emitUpdate: (options?: {
763
- toolActivity?: ToolActivity;
764
- toolResultCompleted?: boolean;
765
- }) => void,
458
+ emitUpdate: EmitUpdateFn,
766
459
  requestTermination: (reason: string) => Promise<unknown>,
767
460
  debugEventDiagnostics: boolean,
768
461
  ): void {
@@ -801,17 +494,16 @@ function setupChildProcess(
801
494
  async function finalizeResult(
802
495
  state: SubagentState,
803
496
  startedAt: number,
804
- ): Promise<SingleResult> {
497
+ ): Promise<RunSingleAgentResult> {
805
498
  state.result.durationMs = Date.now() - startedAt;
806
499
  clearGraceTimer(state);
807
500
  if (state.terminationPromise) await state.terminationPromise;
808
501
  if (state.spawnError) state.result.exitCode = 1;
809
- if (detectMessageError(state.result.messages)) {
810
- state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
811
- }
502
+ const outcome = getLatestOutcomeFromMessages(state.result.messages);
812
503
  const agentEndTimeoutExitCode = getAgentEndTimeoutExitCode(
813
504
  state.result,
814
505
  state.spawnError,
506
+ outcome,
815
507
  );
816
508
  if (agentEndTimeoutExitCode !== undefined) {
817
509
  state.result.exitCode = agentEndTimeoutExitCode;
@@ -819,11 +511,25 @@ async function finalizeResult(
819
511
  if (state.result.termination?.cancelReason === "agent_end_timeout") {
820
512
  state.result.stderr = state.result.stderr.replace(/^Terminated\r?\n/gm, "");
821
513
  }
514
+ // Skip unresolved-error detection when complete succeeded: intermediate tool
515
+ // errors (e.g. a linter exiting non-zero) are expected and already addressed.
516
+ if (!outcome && detectMessageError(state.result.messages)) {
517
+ state.result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
518
+ }
822
519
  if (state.wasAborted) {
823
520
  state.result.stderr = "";
824
- throw new SubagentAbortError(state.result);
521
+ return { kind: "aborted", result: state.result };
825
522
  }
826
- return state.result;
523
+ const isSemanticallySuccessful =
524
+ !state.spawnError &&
525
+ state.result.exitCode === 0 &&
526
+ (Boolean(outcome) ||
527
+ (!state.result.errorMessage?.trim() &&
528
+ !detectMessageError(state.result.messages)));
529
+ if (isSemanticallySuccessful && outcome) {
530
+ state.result.outcome = outcome;
531
+ }
532
+ return { kind: "completed", result: state.result };
827
533
  }
828
534
 
829
535
  /**
@@ -854,19 +560,26 @@ export async function runSingleAgent(
854
560
  parentThinking: ThinkingLevel,
855
561
  debugEventDiagnostics = false,
856
562
  options: RunSingleAgentOptions = {},
857
- ): Promise<SingleResult> {
563
+ ): Promise<RunSingleAgentResult> {
858
564
  const agent = agents.find((a) => a.name === agentName);
859
- if (!agent) return errorForUnknownAgent(agentName, agents, task);
565
+ if (!agent)
566
+ return {
567
+ kind: "completed",
568
+ result: errorForUnknownAgent(agentName, agents, task),
569
+ };
860
570
  const runtimeLimits = getSubagentRuntimeLimits();
861
571
  const depth = getSubagentDepth();
862
572
  if (depth >= runtimeLimits.maxDepth) {
863
- return errorForDepthLimit(
864
- agentName,
865
- agent.source,
866
- task,
867
- depth,
868
- runtimeLimits.maxDepth,
869
- );
573
+ return {
574
+ kind: "completed",
575
+ result: errorForDepthLimit(
576
+ agentName,
577
+ agent.source,
578
+ task,
579
+ depth,
580
+ runtimeLimits.maxDepth,
581
+ ),
582
+ };
870
583
  }
871
584
  const requestedThinking = agent.thinking ?? parentThinking;
872
585
  const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
@@ -883,21 +596,55 @@ export async function runSingleAgent(
883
596
  agent.skills
884
597
  ? resolveAgentSkillArgs(defaultCwd, agent.skills)
885
598
  : Promise.resolve({ args: [] });
599
+ const extensionNames =
600
+ agent.extensions && agent.extensions.length > 0
601
+ ? agent.extensions
602
+ : undefined;
603
+ const resolvedExtensionsPromise: Promise<
604
+ { resolvedPaths: string[] } | { error: string }
605
+ > = extensionNames
606
+ ? resolveAgentExtensionPaths(defaultCwd, extensionNames)
607
+ : Promise.resolve({ resolvedPaths: [] });
886
608
  const promptSetupPromise = beginPromptSetup(agent);
887
- const resolvedSkills = await resolvedSkillsPromise;
888
- if ("error" in resolvedSkills) {
889
- const promptSetup = await promptSetupPromise;
609
+ const [resolvedSkills, resolvedExtensions, promptSetup] = await Promise.all([
610
+ resolvedSkillsPromise,
611
+ resolvedExtensionsPromise,
612
+ promptSetupPromise,
613
+ ]);
614
+ const abortWithError = async (
615
+ error: string,
616
+ ): Promise<RunSingleAgentResult> => {
890
617
  await cleanupPromptSetupResult(promptSetup);
891
- return createErrorResult(
892
- agentName,
893
- agent.source,
894
- task,
895
- resolvedSkills.error,
896
- modelDisplay,
897
- );
618
+ return {
619
+ kind: "completed",
620
+ result: createErrorResult(
621
+ agentName,
622
+ agent.source,
623
+ task,
624
+ error,
625
+ modelDisplay,
626
+ ),
627
+ };
628
+ };
629
+ if ("error" in resolvedSkills) return abortWithError(resolvedSkills.error);
630
+ if ("error" in resolvedExtensions)
631
+ return abortWithError(resolvedExtensions.error);
632
+ if ("error" in promptSetup) {
633
+ return {
634
+ kind: "completed",
635
+ result: createErrorResult(
636
+ agentName,
637
+ agent.source,
638
+ task,
639
+ `Failed to write prompt: ${
640
+ promptSetup.error instanceof Error
641
+ ? promptSetup.error.message
642
+ : String(promptSetup.error)
643
+ }`,
644
+ modelDisplay,
645
+ ),
646
+ };
898
647
  }
899
- const promptSetup = await promptSetupPromise;
900
- if ("error" in promptSetup) throw promptSetup.error;
901
648
  const startedAt = Date.now();
902
649
  const state: SubagentState = {
903
650
  result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
@@ -906,26 +653,33 @@ export async function runSingleAgent(
906
653
  };
907
654
  if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
908
655
  const tmpPrompt = promptSetup.tmpPrompt;
656
+ const samplingEnv = buildSamplingEnv(agent);
909
657
  try {
910
- const args = buildPiArgs(
658
+ const args = buildPiArgs({
911
659
  agent,
912
660
  task,
913
661
  effectiveModel,
914
662
  thinking,
915
663
  resolvedSkills,
916
664
  tmpPrompt,
917
- );
665
+ resolvedExtensionPaths:
666
+ agent.extensions !== undefined
667
+ ? resolvedExtensions.resolvedPaths
668
+ : undefined,
669
+ samplingEnv,
670
+ });
918
671
  const invocation = getPiInvocation(args);
919
672
  const terminateOptions = {
920
673
  tree: true,
921
674
  platform: process.platform,
922
675
  processTreeDetached: process.platform !== "win32",
923
676
  };
677
+ const childEnv = buildChildEnv(samplingEnv);
924
678
  const proc = spawn(invocation.command, invocation.args, {
925
679
  cwd: defaultCwd,
926
680
  shell: invocation.command === "pi" && process.platform === "win32",
927
681
  stdio: ["ignore", "pipe", "pipe"],
928
- env: { ...process.env, ...subagentDepthEnv() },
682
+ env: childEnv,
929
683
  ...getProcessTreeSpawnOptions(terminateOptions.tree),
930
684
  });
931
685
  const processDone = waitForSubagentProcess(proc);