pi-subagents 0.13.4 → 0.14.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.
@@ -10,6 +10,7 @@ import { executeChain } from "./chain-execution.ts";
10
10
  import { resolveExecutionAgentScope } from "./agent-scope.ts";
11
11
  import { handleManagementAction } from "./agent-management.ts";
12
12
  import { runSync } from "./execution.ts";
13
+ import { resolveModelCandidate } from "./model-fallback.ts";
13
14
  import { aggregateParallelOutputs } from "./parallel-utils.ts";
14
15
  import { recordRun } from "./run-history.ts";
15
16
  import {
@@ -24,7 +25,7 @@ import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async
24
25
  import { createForkContextResolver } from "./fork-context.ts";
25
26
  import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
26
27
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
27
- import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
28
+ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent } from "./utils.ts";
28
29
  import {
29
30
  cleanupWorktrees,
30
31
  createWorktrees,
@@ -364,7 +365,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
364
365
  };
365
366
  }
366
367
  const id = randomUUID();
367
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
368
+ const asyncCtx = {
369
+ pi: deps.pi,
370
+ cwd: ctx.cwd,
371
+ currentSessionId: deps.state.currentSessionId!,
372
+ currentModelProvider: ctx.model?.provider,
373
+ };
368
374
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
369
375
  provider: m.provider,
370
376
  id: m.id,
@@ -409,6 +415,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
409
415
  const normalizedSkills = normalizeSkillInput(params.skill);
410
416
  const skills = normalizedSkills === false ? [] : normalizedSkills;
411
417
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
418
+ const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, ctx.model?.provider);
412
419
  return executeAsyncSingle(id, {
413
420
  agent: params.agent!,
414
421
  task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
@@ -424,7 +431,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
424
431
  sessionFile: sessionFileForIndex(0),
425
432
  skills,
426
433
  output: effectiveOutput,
427
- modelOverride: params.model as string | undefined,
434
+ modelOverride,
428
435
  maxSubagentDepth,
429
436
  worktreeSetupHook: deps.config.worktreeSetupHook,
430
437
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -485,7 +492,12 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
485
492
  };
486
493
  }
487
494
  const id = randomUUID();
488
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
495
+ const asyncCtx = {
496
+ pi: deps.pi,
497
+ cwd: ctx.cwd,
498
+ currentSessionId: deps.state.currentSessionId!,
499
+ currentModelProvider: ctx.model?.provider,
500
+ };
489
501
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
490
502
  return executeAsyncChain(id, {
491
503
  chain: asyncChain,
@@ -632,6 +644,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
632
644
  maxSubagentDepth: input.maxSubagentDepths[index],
633
645
  modelOverride: input.modelOverrides[index],
634
646
  availableModels: input.availableModels,
647
+ preferredModelProvider: input.ctx.model?.provider,
635
648
  skills: effectiveSkills === false ? [] : effectiveSkills,
636
649
  onUpdate: input.onUpdate
637
650
  ? (progressUpdate) => {
@@ -707,13 +720,16 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
707
720
  if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
708
721
  }
709
722
 
723
+ const currentProvider = ctx.model?.provider;
710
724
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
711
725
  provider: m.provider,
712
726
  id: m.id,
713
727
  fullId: `${m.provider}/${m.id}`,
714
728
  }));
715
729
  let taskTexts = tasks.map((t) => t.task);
716
- const modelOverrides: (string | undefined)[] = tasks.map((t) => t.model);
730
+ const modelOverrides: (string | undefined)[] = tasks.map((t, i) =>
731
+ resolveModelCandidate(t.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
732
+ );
717
733
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
718
734
  normalizeSkillInput(t.skill),
719
735
  );
@@ -722,7 +738,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
722
738
  const behaviors = agentConfigs.map((c, i) =>
723
739
  resolveStepBehavior(c, { skills: skillOverrides[i] }),
724
740
  );
725
- const availableSkills = discoverAvailableSkills(ctx.cwd);
741
+ const availableSkills = discoverAvailableSkills(params.cwd ?? ctx.cwd);
726
742
 
727
743
  const result = await ctx.ui.custom<ChainClarifyResult>(
728
744
  (tui, theme, _kb, done) =>
@@ -734,6 +750,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
734
750
  undefined,
735
751
  behaviors,
736
752
  availableModels,
753
+ currentProvider,
737
754
  availableSkills,
738
755
  done,
739
756
  "parallel",
@@ -761,7 +778,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
761
778
  };
762
779
  }
763
780
  const id = randomUUID();
764
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
781
+ const asyncCtx = {
782
+ pi: deps.pi,
783
+ cwd: ctx.cwd,
784
+ currentSessionId: deps.state.currentSessionId!,
785
+ currentModelProvider: ctx.model?.provider,
786
+ };
765
787
  const parallelTasks = tasks.map((t, i) => ({
766
788
  agent: t.agent,
767
789
  task: params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!,
@@ -863,12 +885,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
863
885
 
864
886
  return {
865
887
  content: [{ type: "text", text: fullContent }],
866
- details: {
888
+ details: compactForegroundDetails({
867
889
  mode: "parallel",
868
890
  results,
869
891
  progress: params.includeProgress ? allProgress : undefined,
870
892
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
871
- },
893
+ }),
872
894
  };
873
895
  } finally {
874
896
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
@@ -901,13 +923,18 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
901
923
  };
902
924
  }
903
925
 
926
+ const currentProvider = ctx.model?.provider;
904
927
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
905
928
  provider: m.provider,
906
929
  id: m.id,
907
930
  fullId: `${m.provider}/${m.id}`,
908
931
  }));
909
932
  let task = params.task!;
910
- let modelOverride: string | undefined = params.model as string | undefined;
933
+ let modelOverride: string | undefined = resolveModelCandidate(
934
+ (params.model as string | undefined) ?? agentConfig.model,
935
+ availableModels,
936
+ currentProvider,
937
+ );
911
938
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
912
939
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
913
940
  let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : (rawOutput as string | false | undefined);
@@ -916,7 +943,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
916
943
 
917
944
  if (params.clarify === true && ctx.hasUI) {
918
945
  const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput, skills: skillOverride });
919
- const availableSkills = discoverAvailableSkills(ctx.cwd);
946
+ const availableSkills = discoverAvailableSkills(params.cwd ?? ctx.cwd);
920
947
 
921
948
  const result = await ctx.ui.custom<ChainClarifyResult>(
922
949
  (tui, theme, _kb, done) =>
@@ -928,6 +955,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
928
955
  undefined,
929
956
  [behavior],
930
957
  availableModels,
958
+ currentProvider,
931
959
  availableSkills,
932
960
  done,
933
961
  "single",
@@ -954,7 +982,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
954
982
  };
955
983
  }
956
984
  const id = randomUUID();
957
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
985
+ const asyncCtx = {
986
+ pi: deps.pi,
987
+ cwd: ctx.cwd,
988
+ currentSessionId: deps.state.currentSessionId!,
989
+ currentModelProvider: ctx.model?.provider,
990
+ };
958
991
  return executeAsyncSingle(id, {
959
992
  agent: params.agent!,
960
993
  task: params.context === "fork" ? wrapForkTask(task) : task,
@@ -1009,6 +1042,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1009
1042
  onUpdate,
1010
1043
  modelOverride,
1011
1044
  availableModels,
1045
+ preferredModelProvider: currentProvider,
1012
1046
  skills: effectiveSkills,
1013
1047
  });
1014
1048
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
@@ -1029,37 +1063,37 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1029
1063
  if (r.detached) {
1030
1064
  return {
1031
1065
  content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}` }],
1032
- details: {
1066
+ details: compactForegroundDetails({
1033
1067
  mode: "single",
1034
1068
  results: [r],
1035
1069
  progress: params.includeProgress ? allProgress : undefined,
1036
1070
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1037
1071
  truncation: r.truncation,
1038
- },
1072
+ }),
1039
1073
  };
1040
1074
  }
1041
1075
 
1042
1076
  if (r.exitCode !== 0)
1043
1077
  return {
1044
1078
  content: [{ type: "text", text: r.error || "Failed" }],
1045
- details: {
1079
+ details: compactForegroundDetails({
1046
1080
  mode: "single",
1047
1081
  results: [r],
1048
1082
  progress: params.includeProgress ? allProgress : undefined,
1049
1083
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1050
1084
  truncation: r.truncation,
1051
- },
1085
+ }),
1052
1086
  isError: true,
1053
1087
  };
1054
1088
  return {
1055
1089
  content: [{ type: "text", text: finalizedOutput.displayOutput || "(no output)" }],
1056
- details: {
1090
+ details: compactForegroundDetails({
1057
1091
  mode: "single",
1058
1092
  results: [r],
1059
1093
  progress: params.includeProgress ? allProgress : undefined,
1060
1094
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1061
1095
  truncation: r.truncation,
1062
- },
1096
+ }),
1063
1097
  };
1064
1098
  }
1065
1099
 
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import type { Message } from "@mariozechner/pi-ai";
6
7
  import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
7
8
  import { getPiSpawnCommand } from "./pi-spawn.ts";
8
9
  import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
@@ -27,6 +28,7 @@ import {
27
28
  } from "./parallel-utils.ts";
28
29
  import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
29
30
  import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
31
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
30
32
  import {
31
33
  cleanupWorktrees,
32
34
  createWorktrees,
@@ -123,32 +125,44 @@ function emptyUsage(): Usage {
123
125
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
124
126
  }
125
127
 
126
- function parseRunOutput(output: string): { usage: Usage; model?: string; error?: string } {
127
- const usage = emptyUsage();
128
- let model: string | undefined;
129
- let error: string | undefined;
130
- for (const line of output.split("\n")) {
131
- if (!line.trim()) continue;
132
- try {
133
- const evt = JSON.parse(line) as { type?: string; message?: { role?: string; model?: string; errorMessage?: string; usage?: any } };
134
- if (evt.type !== "message_end" || evt.message?.role !== "assistant") continue;
135
- const msg = evt.message;
136
- if (msg.model) model = msg.model;
137
- if (msg.errorMessage) error = msg.errorMessage;
138
- const u = msg.usage;
139
- if (u) {
140
- usage.turns++;
141
- usage.input += u.input ?? u.inputTokens ?? 0;
142
- usage.output += u.output ?? u.outputTokens ?? 0;
143
- usage.cacheRead += u.cacheRead ?? 0;
144
- usage.cacheWrite += u.cacheWrite ?? 0;
145
- usage.cost += u.cost?.total ?? 0;
146
- }
147
- } catch {
148
- // Ignore malformed stdout lines.
149
- }
150
- }
151
- return { usage, model, error };
128
+ interface ChildEventContext {
129
+ eventsPath: string;
130
+ runId: string;
131
+ stepIndex: number;
132
+ agent: string;
133
+ }
134
+
135
+ interface ChildUsage {
136
+ input?: number;
137
+ inputTokens?: number;
138
+ output?: number;
139
+ outputTokens?: number;
140
+ cacheRead?: number;
141
+ cacheWrite?: number;
142
+ cost?: { total?: number };
143
+ }
144
+
145
+ type ChildMessage = Message & {
146
+ model?: string;
147
+ errorMessage?: string;
148
+ usage?: ChildUsage;
149
+ };
150
+
151
+ interface ChildEvent {
152
+ type?: string;
153
+ message?: ChildMessage;
154
+ toolName?: string;
155
+ args?: Record<string, unknown>;
156
+ }
157
+
158
+ interface RunPiStreamingResult {
159
+ stderr: string;
160
+ exitCode: number | null;
161
+ messages: Message[];
162
+ usage: Usage;
163
+ model?: string;
164
+ error?: string;
165
+ finalOutput: string;
152
166
  }
153
167
 
154
168
  function runPiStreaming(
@@ -159,7 +173,8 @@ function runPiStreaming(
159
173
  piPackageRoot?: string,
160
174
  piArgv1?: string,
161
175
  maxSubagentDepth?: number,
162
- ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
176
+ childEventContext?: ChildEventContext,
177
+ ): Promise<RunPiStreamingResult> {
163
178
  return new Promise((resolve) => {
164
179
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
165
180
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
@@ -168,29 +183,119 @@ function runPiStreaming(
168
183
  ...(piArgv1 ? { argv1: piArgv1 } : {}),
169
184
  });
170
185
  const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
171
- let stdout = "";
172
186
  let stderr = "";
187
+ let stdoutBuf = "";
188
+ let stderrBuf = "";
189
+ const messages: Message[] = [];
190
+ const usage = emptyUsage();
191
+ let model: string | undefined;
192
+ let error: string | undefined;
193
+ const rawStdoutLines: string[] = [];
194
+
195
+ const writeOutputLine = (line: string) => {
196
+ if (!line.trim()) return;
197
+ outputStream.write(`${line}\n`);
198
+ };
199
+
200
+ const writeOutputText = (text: string) => {
201
+ for (const line of text.split("\n")) {
202
+ writeOutputLine(line);
203
+ }
204
+ };
205
+
206
+ const appendChildEvent = (event: Record<string, unknown>) => {
207
+ if (!childEventContext) return;
208
+ appendJsonl(childEventContext.eventsPath, JSON.stringify({
209
+ ...event,
210
+ subagentSource: "child",
211
+ subagentRunId: childEventContext.runId,
212
+ subagentStepIndex: childEventContext.stepIndex,
213
+ subagentAgent: childEventContext.agent,
214
+ observedAt: Date.now(),
215
+ }));
216
+ };
217
+
218
+ const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
219
+ appendChildEvent({ type, line });
220
+ };
221
+
222
+ const processStdoutLine = (line: string) => {
223
+ if (!line.trim()) return;
224
+ let event: ChildEvent;
225
+ try {
226
+ event = JSON.parse(line) as ChildEvent;
227
+ } catch {
228
+ rawStdoutLines.push(line);
229
+ writeOutputLine(line);
230
+ appendChildLine("subagent.child.stdout", line);
231
+ return;
232
+ }
233
+
234
+ appendChildEvent(event);
235
+
236
+ if (event.type === "tool_execution_start" && event.toolName) {
237
+ const toolArgs = extractToolArgsPreview(event.args ?? {});
238
+ writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
239
+ return;
240
+ }
241
+
242
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
243
+ messages.push(event.message);
244
+ const text = extractTextFromContent(event.message.content);
245
+ if (text) writeOutputText(text);
246
+
247
+ if (event.type !== "message_end" || event.message.role !== "assistant") return;
248
+ if (event.message.model) model = event.message.model;
249
+ if (event.message.errorMessage) error = event.message.errorMessage;
250
+ const eventUsage = event.message.usage;
251
+ if (!eventUsage) return;
252
+ usage.turns++;
253
+ usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
254
+ usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
255
+ usage.cacheRead += eventUsage.cacheRead ?? 0;
256
+ usage.cacheWrite += eventUsage.cacheWrite ?? 0;
257
+ usage.cost += eventUsage.cost?.total ?? 0;
258
+ }
259
+ };
260
+
261
+ const processStderrText = (text: string) => {
262
+ stderr += text;
263
+ stderrBuf += text;
264
+ outputStream.write(text);
265
+ if (!childEventContext) return;
266
+ const lines = stderrBuf.split("\n");
267
+ stderrBuf = lines.pop() || "";
268
+ for (const line of lines) {
269
+ if (!line.trim()) continue;
270
+ appendChildLine("subagent.child.stderr", line);
271
+ }
272
+ };
173
273
 
174
274
  child.stdout.on("data", (chunk: Buffer) => {
175
275
  const text = chunk.toString();
176
- stdout += text;
177
- outputStream.write(text);
276
+ stdoutBuf += text;
277
+ const lines = stdoutBuf.split("\n");
278
+ stdoutBuf = lines.pop() || "";
279
+ for (const line of lines) processStdoutLine(line);
178
280
  });
179
281
 
180
282
  child.stderr.on("data", (chunk: Buffer) => {
181
- const text = chunk.toString();
182
- stderr += text;
183
- outputStream.write(text);
283
+ processStderrText(chunk.toString());
184
284
  });
185
285
 
186
286
  child.on("close", (exitCode) => {
287
+ if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
288
+ if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
187
289
  outputStream.end();
188
- resolve({ stdout, stderr, exitCode });
290
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
291
+ resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
189
292
  });
190
293
 
191
- child.on("error", () => {
294
+ child.on("error", (spawnError) => {
192
295
  outputStream.end();
193
- resolve({ stdout, stderr, exitCode: 1 });
296
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
297
+ const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
298
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
194
299
  });
195
300
  });
196
301
  }
@@ -386,14 +491,13 @@ async function runSingleStep(
386
491
  const attemptedModels: string[] = [];
387
492
  const modelAttempts: ModelAttempt[] = [];
388
493
  const attemptNotes: string[] = [];
389
- let finalResult:
390
- | { stdout: string; stderr: string; exitCode: number | null; usage: Usage; model?: string; error?: string }
391
- | undefined;
494
+ const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
495
+ let finalResult: RunPiStreamingResult | undefined;
392
496
 
393
497
  for (let index = 0; index < candidates.length; index++) {
394
498
  const candidate = candidates[index];
395
499
  const { args, env, tempDir } = buildPiArgs({
396
- baseArgs: ["-p"],
500
+ baseArgs: ["--mode", "json", "-p"],
397
501
  task,
398
502
  sessionEnabled,
399
503
  sessionDir,
@@ -406,28 +510,41 @@ async function runSingleStep(
406
510
  mcpDirectTools: step.mcpDirectTools,
407
511
  promptFileStem: step.agent,
408
512
  });
409
- const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
410
- const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, ctx.piArgv1, step.maxSubagentDepth);
513
+ const run = await runPiStreaming(
514
+ args,
515
+ step.cwd ?? ctx.cwd,
516
+ ctx.outputFile,
517
+ env,
518
+ ctx.piPackageRoot,
519
+ ctx.piArgv1,
520
+ step.maxSubagentDepth,
521
+ { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
522
+ );
411
523
  cleanupTempDir(tempDir);
412
524
 
413
- const parsed = parseRunOutput(run.stdout);
414
- const error = parsed.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
525
+ const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
526
+ const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
527
+ const error = hiddenError?.hasError
528
+ ? hiddenError.details
529
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
530
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
531
+ : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
415
532
  const attempt: ModelAttempt = {
416
- model: candidate ?? parsed.model ?? step.model ?? "default",
417
- success: run.exitCode === 0 && !error,
418
- exitCode: run.exitCode,
533
+ model: candidate ?? run.model ?? step.model ?? "default",
534
+ success: effectiveExitCode === 0 && !error,
535
+ exitCode: effectiveExitCode,
419
536
  error,
420
- usage: parsed.usage,
537
+ usage: run.usage,
421
538
  };
422
539
  modelAttempts.push(attempt);
423
540
  if (candidate) attemptedModels.push(candidate);
424
- finalResult = { ...run, usage: parsed.usage, model: candidate ?? parsed.model, error };
541
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
425
542
  if (attempt.success) break;
426
543
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
427
544
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
428
545
  }
429
546
 
430
- const rawOutput = (finalResult?.stdout || "").trim();
547
+ const rawOutput = finalResult?.finalOutput ?? "";
431
548
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
432
549
  ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
433
550
  : { fullOutput: rawOutput };
@@ -438,12 +555,12 @@ async function runSingleStep(
438
555
  }
439
556
  if (resolvedOutput.savedPath) {
440
557
  outputForSummary = outputForSummary
441
- ? `${outputForSummary}\n\nšŸ“„ Output saved to: ${resolvedOutput.savedPath}`
442
- : `šŸ“„ Output saved to: ${resolvedOutput.savedPath}`;
558
+ ? `${outputForSummary}\n\nOutput saved to: ${resolvedOutput.savedPath}`
559
+ : `Output saved to: ${resolvedOutput.savedPath}`;
443
560
  } else if (resolvedOutput.saveError && step.outputPath && finalResult?.exitCode === 0) {
444
561
  outputForSummary = outputForSummary
445
- ? `${outputForSummary}\n\nāš ļø Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
446
- : `āš ļø Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`;
562
+ ? `${outputForSummary}\n\nFailed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`
563
+ : `Failed to save output to: ${step.outputPath}\n${resolvedOutput.saveError}`;
447
564
  }
448
565
 
449
566
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
package/types.ts CHANGED
@@ -40,17 +40,6 @@ export interface TokenUsage {
40
40
  total: number;
41
41
  }
42
42
 
43
- // ============================================================================
44
- // Skills
45
- // ============================================================================
46
-
47
- export interface ResolvedSkill {
48
- name: string;
49
- path: string;
50
- content: string;
51
- source: "project" | "user";
52
- }
53
-
54
43
  // ============================================================================
55
44
  // Progress Tracking
56
45
  // ============================================================================
@@ -96,7 +85,7 @@ export interface SingleResult {
96
85
  exitCode: number;
97
86
  detached?: boolean;
98
87
  detachedReason?: string;
99
- messages: Message[];
88
+ messages?: Message[];
100
89
  usage: Usage;
101
90
  model?: string;
102
91
  attemptedModels?: string[];
@@ -271,6 +260,8 @@ export interface RunSyncOptions {
271
260
  modelOverride?: string;
272
261
  /** Registry models available for heuristic bare-model resolution */
273
262
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
263
+ /** Current parent-session provider to prefer for ambiguous bare model ids */
264
+ preferredModelProvider?: string;
274
265
  /** Skills to inject (overrides agent default if provided) */
275
266
  skills?: string[];
276
267
  }
@@ -309,10 +300,66 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
309
300
  cleanupDays: 7,
310
301
  };
311
302
 
303
+ function sanitizeTempScopeSegment(value: string): string {
304
+ const sanitized = value
305
+ .trim()
306
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
307
+ .replace(/^-+|-+$/g, "");
308
+ return sanitized || "unknown";
309
+ }
310
+
311
+ export function resolveTempScopeId(options?: {
312
+ env?: NodeJS.ProcessEnv;
313
+ getuid?: (() => number) | undefined;
314
+ userInfo?: (() => { username?: string | null }) | undefined;
315
+ homedir?: (() => string) | undefined;
316
+ }): string {
317
+ const env = options?.env ?? process.env;
318
+ const getuid = options && Object.hasOwn(options, "getuid")
319
+ ? options.getuid
320
+ : process.getuid?.bind(process);
321
+ if (typeof getuid === "function") {
322
+ return `uid-${getuid()}`;
323
+ }
324
+
325
+ for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
326
+ const value = env[key];
327
+ if (value) return `user-${sanitizeTempScopeSegment(value)}`;
328
+ }
329
+
330
+ const userInfo = options && Object.hasOwn(options, "userInfo")
331
+ ? options.userInfo
332
+ : os.userInfo;
333
+ try {
334
+ const username = userInfo?.().username;
335
+ if (username) return `user-${sanitizeTempScopeSegment(username)}`;
336
+ } catch {
337
+ // Fall through to home-directory-based scoping.
338
+ }
339
+
340
+ const homedir = env.USERPROFILE ?? env.HOME;
341
+ if (homedir) return `home-${sanitizeTempScopeSegment(homedir)}`;
342
+
343
+ const resolveHomedir = options && Object.hasOwn(options, "homedir")
344
+ ? options.homedir
345
+ : os.homedir;
346
+ try {
347
+ const fallbackHomedir = resolveHomedir?.();
348
+ if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
349
+ } catch {
350
+ // Fall through to the last-resort shared scope.
351
+ }
352
+
353
+ return "shared";
354
+ }
355
+
312
356
  export const MAX_PARALLEL = 8;
313
357
  export const MAX_CONCURRENCY = 4;
314
- export const RESULTS_DIR = path.join(os.tmpdir(), "pi-async-subagent-results");
315
- export const ASYNC_DIR = path.join(os.tmpdir(), "pi-async-subagent-runs");
358
+ export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
359
+ export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
360
+ export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
361
+ export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
362
+ export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
316
363
  export const WIDGET_KEY = "subagent-async";
317
364
  export const SLASH_RESULT_TYPE = "subagent-slash-result";
318
365
  export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
@@ -329,6 +376,10 @@ export const DEFAULT_FORK_PREAMBLE =
329
376
  "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
330
377
  "— focus exclusively on completing this task using your tools.";
331
378
 
379
+ export function getAsyncConfigPath(suffix: string): string {
380
+ return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
381
+ }
382
+
332
383
  export function wrapForkTask(task: string, preamble?: string | false): string {
333
384
  if (preamble === false) return task;
334
385
  const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;