pi-subagents 0.24.2 → 0.24.4

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.
@@ -66,6 +66,7 @@ import {
66
66
  formatWorktreeTaskCwdConflict,
67
67
  type WorktreeSetup,
68
68
  } from "../shared/worktree.ts";
69
+ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
69
70
  import { writeInitialProgressFile } from "../../shared/settings.ts";
70
71
 
71
72
  interface SubagentRunConfig {
@@ -221,7 +222,12 @@ function runPiStreaming(
221
222
  ...(piPackageRoot ? { piPackageRoot } : {}),
222
223
  ...(piArgv1 ? { argv1: piArgv1 } : {}),
223
224
  });
224
- const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
225
+ const child = spawn(spawnSpec.command, spawnSpec.args, {
226
+ cwd,
227
+ stdio: ["ignore", "pipe", "pipe"],
228
+ env: spawnEnv,
229
+ windowsHide: true,
230
+ });
225
231
  let stderr = "";
226
232
  let stdoutBuf = "";
227
233
  let stderrBuf = "";
@@ -229,6 +235,7 @@ function runPiStreaming(
229
235
  const usage = emptyUsage();
230
236
  let model: string | undefined;
231
237
  let error: string | undefined;
238
+ let assistantError: string | undefined;
232
239
  let interrupted = false;
233
240
  let observedMutationAttempt = false;
234
241
  const rawStdoutLines: string[] = [];
@@ -289,7 +296,7 @@ function runPiStreaming(
289
296
 
290
297
  if (event.type !== "message_end" || event.message.role !== "assistant") return;
291
298
  if (event.message.model) model = event.message.model;
292
- if (event.message.errorMessage) error = event.message.errorMessage;
299
+ if (event.message.errorMessage) assistantError = event.message.errorMessage;
293
300
  const eventUsage = event.message.usage;
294
301
  if (eventUsage) {
295
302
  usage.turns++;
@@ -303,6 +310,7 @@ function runPiStreaming(
303
310
  const hasToolCall = Array.isArray(event.message.content)
304
311
  && event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
305
312
  if (stopReason === "stop" && !hasToolCall) {
313
+ if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
306
314
  cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
307
315
  startFinalDrain();
308
316
  }
@@ -370,7 +378,7 @@ function runPiStreaming(
370
378
  const termSent = trySignalChild(child, "SIGTERM");
371
379
  if (!termSent) return;
372
380
  forcedTerminationSignal = true;
373
- if (!cleanTerminalAssistantStopReceived && !error) {
381
+ if (!cleanTerminalAssistantStopReceived && !error && !assistantError) {
374
382
  error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
375
383
  }
376
384
  finalHardKillTimer = setTimeout(() => {
@@ -394,14 +402,15 @@ function runPiStreaming(
394
402
  if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
395
403
  outputStream.end();
396
404
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
397
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !error;
405
+ const finalError = error ?? assistantError;
406
+ const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
398
407
  resolve({
399
408
  stderr,
400
409
  exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
401
410
  messages,
402
411
  usage,
403
412
  model,
404
- error: interrupted || forcedDrainAfterFinalSuccess ? undefined : error,
413
+ error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
405
414
  finalOutput,
406
415
  interrupted,
407
416
  observedMutationAttempt,
@@ -416,7 +425,7 @@ function runPiStreaming(
416
425
  outputStream.end();
417
426
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
418
427
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
419
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
428
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
420
429
  });
421
430
  });
422
431
  }
@@ -545,6 +554,7 @@ interface SingleStepContext {
545
554
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
546
555
  childIntercomTarget?: string;
547
556
  orchestratorIntercomTarget?: string;
557
+ onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
548
558
  onChildEvent?: (event: ChildEvent) => void;
549
559
  }
550
560
 
@@ -596,6 +606,7 @@ async function runSingleStep(
596
606
 
597
607
  for (let index = 0; index < candidates.length; index++) {
598
608
  const candidate = candidates[index];
609
+ ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
599
610
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
600
611
  const { args, env, tempDir } = buildPiArgs({
601
612
  baseArgs: ["--mode", "json", "-p"],
@@ -611,6 +622,7 @@ async function runSingleStep(
611
622
  systemPrompt: step.systemPrompt,
612
623
  systemPromptMode: step.systemPromptMode,
613
624
  mcpDirectTools: step.mcpDirectTools,
625
+ cwd: step.cwd ?? ctx.cwd,
614
626
  promptFileStem: step.agent,
615
627
  intercomSessionName: ctx.childIntercomTarget,
616
628
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
@@ -633,11 +645,13 @@ async function runSingleStep(
633
645
  cleanupTempDir(tempDir);
634
646
 
635
647
  const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
636
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError
648
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
637
649
  ? evaluateCompletionMutationGuard({
638
650
  agent: step.agent,
639
651
  task,
640
652
  messages: run.messages,
653
+ tools: step.tools,
654
+ mcpDirectTools: step.mcpDirectTools,
641
655
  })
642
656
  : undefined;
643
657
  const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
@@ -912,6 +926,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
912
926
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
913
927
  skills: step.skills,
914
928
  model: step.model,
929
+ thinking: step.thinking,
915
930
  attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
916
931
  recentTools: [],
917
932
  recentOutput: [],
@@ -1007,6 +1022,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1007
1022
  appendControlEvent(event);
1008
1023
  return true;
1009
1024
  };
1025
+ const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
1026
+ const step = statusPayload.steps[flatIndex];
1027
+ if (!step) return;
1028
+ step.model = model;
1029
+ step.thinking = thinking;
1030
+ statusPayload.lastUpdate = now;
1031
+ writeAtomicJson(statusPath, statusPayload);
1032
+ };
1010
1033
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
1011
1034
  const step = statusPayload.steps[flatIndex];
1012
1035
  if (!step) return;
@@ -1332,6 +1355,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1332
1355
  registerInterrupt: (interrupt) => {
1333
1356
  activeChildInterrupt = interrupt;
1334
1357
  },
1358
+ onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1335
1359
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
1336
1360
  });
1337
1361
  if (task.sessionFile) {
@@ -1346,6 +1370,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1346
1370
  statusPayload.steps[fi].durationMs = taskDuration;
1347
1371
  statusPayload.steps[fi].exitCode = singleResult.exitCode;
1348
1372
  statusPayload.steps[fi].model = singleResult.model;
1373
+ statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1349
1374
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1350
1375
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1351
1376
  statusPayload.steps[fi].error = singleResult.error;
@@ -1475,6 +1500,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1475
1500
  registerInterrupt: (interrupt) => {
1476
1501
  activeChildInterrupt = interrupt;
1477
1502
  },
1503
+ onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
1478
1504
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
1479
1505
  });
1480
1506
  if (seqStep.sessionFile) {
@@ -1522,6 +1548,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1522
1548
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
1523
1549
  statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
1524
1550
  statusPayload.steps[flatIndex].model = singleResult.model;
1551
+ statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
1525
1552
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
1526
1553
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
1527
1554
  statusPayload.steps[flatIndex].error = singleResult.error;
@@ -151,6 +151,7 @@ async function runSingleAttempt(
151
151
  extensions: agent.extensions,
152
152
  systemPrompt: shared.systemPrompt,
153
153
  mcpDirectTools: agent.mcpDirectTools,
154
+ cwd: options.cwd ?? runtimeCwd,
154
155
  promptFileStem: agent.name,
155
156
  intercomSessionName: options.intercomSessionName,
156
157
  orchestratorIntercomTarget: options.orchestratorIntercomTarget,
@@ -207,6 +208,7 @@ async function runSingleAttempt(
207
208
  cwd: options.cwd ?? runtimeCwd,
208
209
  env: spawnEnv,
209
210
  stdio: ["ignore", "pipe", "pipe"],
211
+ windowsHide: true,
210
212
  });
211
213
  const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
212
214
  let buf = "";
@@ -214,6 +216,7 @@ async function runSingleAttempt(
214
216
  let settled = false;
215
217
  let detached = false;
216
218
  let intercomStarted = false;
219
+ let assistantError: string | undefined;
217
220
  let removeAbortListener: (() => void) | undefined;
218
221
  let removeInterruptListener: (() => void) | undefined;
219
222
  let activityTimer: NodeJS.Timeout | undefined;
@@ -259,7 +262,7 @@ async function runSingleAttempt(
259
262
  const termSent = trySignalChild(proc, "SIGTERM");
260
263
  if (!termSent) return;
261
264
  forcedTerminationSignal = true;
262
- if (!cleanTerminalAssistantStopReceived) {
265
+ if (!cleanTerminalAssistantStopReceived && !assistantError) {
263
266
  result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
264
267
  }
265
268
  finalHardKillTimer = setTimeout(() => {
@@ -465,13 +468,15 @@ async function runSingleAttempt(
465
468
  progress.tokens = result.usage.input + result.usage.output;
466
469
  }
467
470
  if (!result.model && evt.message.model) result.model = evt.message.model;
468
- if (evt.message.errorMessage) result.error = evt.message.errorMessage;
469
- appendRecentOutput(progress, extractTextFromContent(evt.message.content).split("\n").slice(-10));
471
+ if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
472
+ const assistantText = extractTextFromContent(evt.message.content);
473
+ appendRecentOutput(progress, assistantText.split("\n").slice(-10));
470
474
  // Final assistant message: start the exit drain window.
471
475
  const stopReason = (evt.message as { stopReason?: string }).stopReason;
472
476
  const hasToolCall = Array.isArray(evt.message.content)
473
477
  && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
474
478
  if (stopReason === "stop" && !hasToolCall) {
479
+ if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
475
480
  cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
476
481
  startFinalDrain();
477
482
  }
@@ -551,6 +556,7 @@ async function runSingleAttempt(
551
556
  }
552
557
  processClosed = true;
553
558
  if (buf.trim()) processLine(buf);
559
+ if (!result.error && assistantError) result.error = assistantError;
554
560
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !result.error;
555
561
  if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
556
562
  result.error = stderrBuf.trim();
@@ -662,8 +668,14 @@ async function runSingleAttempt(
662
668
  };
663
669
 
664
670
  let fullOutput = getFinalOutput(result.messages);
665
- const completionGuard = result.exitCode === 0 && !result.error
666
- ? evaluateCompletionMutationGuard({ agent: agent.name, task, messages: result.messages })
671
+ const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
672
+ ? evaluateCompletionMutationGuard({
673
+ agent: agent.name,
674
+ task,
675
+ messages: result.messages,
676
+ tools: agent.tools,
677
+ mcpDirectTools: agent.mcpDirectTools,
678
+ })
667
679
  : undefined;
668
680
  if (completionGuard?.triggered && !observedMutationAttempt) {
669
681
  result.exitCode = 1;
@@ -35,7 +35,7 @@ import { createForkContextResolver } from "../../shared/fork-context.ts";
35
35
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
36
36
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
37
37
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
38
- import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
38
+ import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
39
39
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
40
40
  import {
41
41
  buildSubagentResultIntercomPayload,
@@ -979,7 +979,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
979
979
  };
980
980
  }
981
981
  const rawOutput = params.output !== undefined ? params.output : a.output;
982
- const effectiveOutput: string | false | undefined = rawOutput === true ? a.output : (rawOutput as string | false | undefined);
982
+ const effectiveOutput = normalizeSingleOutputOverride(rawOutput, a.output);
983
983
  const effectiveOutputMode = params.outputMode ?? "inline";
984
984
  const normalizedSkills = normalizeSkillInput(params.skill);
985
985
  const skills = normalizedSkills === false ? [] : normalizedSkills;
@@ -1716,7 +1716,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1716
1716
  );
1717
1717
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
1718
1718
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
1719
- let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : (rawOutput as string | false | undefined);
1719
+ let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
1720
1720
  const effectiveOutputMode = params.outputMode ?? "inline";
1721
1721
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1722
1722
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
@@ -1750,7 +1750,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1750
1750
  task = result.templates[0]!;
1751
1751
  const override = result.behaviorOverrides[0];
1752
1752
  if (override?.model) modelOverride = override.model;
1753
- if (override?.output !== undefined) effectiveOutput = override.output;
1753
+ if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
1754
1754
  if (override?.skills !== undefined) skillOverride = override.skills;
1755
1755
 
1756
1756
  if (result.runInBackground) {
@@ -2127,9 +2127,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2127
2127
  return toExecutionErrorResult(effectiveParams, error);
2128
2128
  }
2129
2129
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2130
- const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && effectiveParams.clarify === true;
2131
- const effectiveAsync = requestedAsync
2132
- && (hasChain ? effectiveParams.clarify === false : effectiveParams.clarify !== true);
2130
+ const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2131
+ const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
2133
2132
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2134
2133
 
2135
2134
  const artifactConfig: ArtifactConfig = {
@@ -54,11 +54,24 @@ const GENERAL_IMPLEMENTATION_PATTERNS = [
54
54
  /\b(?:update|add|remove|replace|delete|create)\s+(?:the\s+)?(?:file|files|code|source|implementation|test|tests|component|function|module|class|method|logic|import|imports|readme|docs?|changelog|package\.json|config|manifest|extension|prompt|command)\b/i,
55
55
  ];
56
56
 
57
+ const READ_ONLY_BUILTIN_TOOLS = new Set([
58
+ "read",
59
+ "grep",
60
+ "find",
61
+ "ls",
62
+ "web_search",
63
+ "fetch_content",
64
+ "get_search_content",
65
+ "intercom",
66
+ "contact_supervisor",
67
+ ]);
57
68
 
58
69
  interface CompletionMutationGuardInput {
59
70
  agent: string;
60
71
  task: string;
61
72
  messages: Message[];
73
+ tools?: string[];
74
+ mcpDirectTools?: string[];
62
75
  }
63
76
 
64
77
  interface CompletionMutationGuardResult {
@@ -83,6 +96,13 @@ function stripScopedNoEditConstraints(task: string): string {
83
96
  return stripped;
84
97
  }
85
98
 
99
+ function declaresOnlyReadOnlyTools(tools: string[] | undefined, mcpDirectTools: string[] | undefined): boolean {
100
+ return tools !== undefined
101
+ && tools.length > 0
102
+ && (mcpDirectTools?.length ?? 0) === 0
103
+ && tools.every((tool) => READ_ONLY_BUILTIN_TOOLS.has(tool));
104
+ }
105
+
86
106
  export function expectsImplementationMutation(agent: string, task: string): boolean {
87
107
  const taskText = stripFrameworkInstructions(task);
88
108
  const taskTextWithoutScopedConstraints = stripScopedNoEditConstraints(taskText);
@@ -115,7 +135,9 @@ export function hasMutationToolCall(messages: Message[]): boolean {
115
135
  }
116
136
 
117
137
  export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
118
- const expectedMutation = expectsImplementationMutation(input.agent, input.task);
138
+ const expectedMutation = declaresOnlyReadOnlyTools(input.tools, input.mcpDirectTools)
139
+ ? false
140
+ : expectsImplementationMutation(input.agent, input.task);
119
141
  const attemptedMutation = hasMutationToolCall(input.messages);
120
142
  return {
121
143
  expectedMutation,