pi-subagents 0.60.0 → 0.61.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/docs/agents.md +2 -2
  3. package/docs/configuration.md +9 -5
  4. package/docs/extension-api.md +14 -7
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +12 -3
  8. package/docs/workflows.md +14 -13
  9. package/install.mjs +2 -1
  10. package/package.json +1 -1
  11. package/skills/pi-subagents/SKILL.md +6 -4
  12. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +42 -11
  14. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  15. package/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  16. package/skills/pi-subagents/references/review-and-validation.md +1 -1
  17. package/src/agents/agent-management.ts +102 -63
  18. package/src/agents/agents.ts +527 -221
  19. package/src/api/background-work.ts +7 -2
  20. package/src/api/external-runs.ts +67 -4
  21. package/src/api/preflight.ts +13 -8
  22. package/src/api/shared-types.ts +1 -0
  23. package/src/extension/index.ts +7 -4
  24. package/src/extension/public-execution.ts +47 -4
  25. package/src/extension/rpc.ts +62 -4
  26. package/src/extension/schemas.ts +11 -7
  27. package/src/extension/tool-description.ts +14 -18
  28. package/src/runs/background/async-execution.ts +51 -35
  29. package/src/runs/background/async-job-tracker.ts +62 -3
  30. package/src/runs/background/async-resume.ts +3 -1
  31. package/src/runs/background/async-status.ts +59 -9
  32. package/src/runs/background/auto-drain.ts +1 -1
  33. package/src/runs/background/fleet-view.ts +1 -1
  34. package/src/runs/background/result-watcher.ts +1 -1
  35. package/src/runs/background/resume-guidance.ts +1 -1
  36. package/src/runs/background/run-status.ts +2 -2
  37. package/src/runs/background/subagent-runner.ts +1 -2
  38. package/src/runs/background/subagent-wait.ts +20 -21
  39. package/src/runs/background/wait-completions.ts +1 -1
  40. package/src/runs/background/wait-tool.ts +24 -18
  41. package/src/runs/foreground/execution.ts +61 -2
  42. package/src/runs/foreground/subagent-executor.ts +178 -58
  43. package/src/runs/shared/acceptance.ts +43 -18
  44. package/src/runs/shared/host-step-status.ts +1 -0
  45. package/src/runs/shared/model-fallback.ts +61 -17
  46. package/src/runs/shared/permissions.ts +1 -1
  47. package/src/runs/shared/tool-timeout.ts +1 -1
  48. package/src/runs/shared/workflow-graph.ts +3 -2
  49. package/src/shared/types.ts +40 -3
  50. package/src/shared/workflow-child-permit.ts +91 -0
  51. package/src/slash/prompt-template-bridge.ts +37 -1
  52. package/src/slash/slash-commands.ts +18 -26
  53. package/src/tui/render.ts +95 -52
  54. package/src/workflows/scripted-workflow.ts +153 -4
  55. package/src/workflows/workflow-child-summary.ts +1 -1
  56. package/src/workflows/workflow-receipt.ts +41 -4
  57. package/src/workflows/workflow-resources.ts +150 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `subagent_wait` tool: block the current turn until outstanding async runs
2
+ * `bg_wait` tool: block the current turn until outstanding async runs
3
3
  * or a named remembered detached foreground run finishes.
4
4
  *
5
5
  * Background subagent runs are detached. In an interactive session the parent
@@ -8,33 +8,33 @@
8
8
  * cannot work at all non-interactively (`pi -p ...`), where the run is a single
9
9
  * turn: once the turn ends there is nothing left to receive the notification.
10
10
  *
11
- * `subagent_wait` closes that gap. It keeps the turn alive until a tracked async
11
+ * `bg_wait` closes that gap. It keeps the turn alive until a tracked async
12
12
  * run for this session reaches a terminal state (complete / failed / paused),
13
13
  * the caller-supplied timeout elapses, or the turn is aborted. Because it awaits
14
14
  * inside the turn, the completion the model was told to wait for is actually
15
15
  * observed before the tool returns.
16
16
  *
17
- * By default `subagent_wait` returns as soon as ONE run finishes, so a fleet
17
+ * By default `bg_wait` returns as soon as ONE run finishes, so a fleet
18
18
  * manager can use it in a rolling-replacement loop: launch N workers, wait for
19
- * the next one to finish, spawn its replacement, then call `subagent_wait`
19
+ * the next one to finish, spawn its replacement, then call `bg_wait`
20
20
  * again — keeping N in flight instead of draining to zero between batches.
21
21
  * Pass `all: true` to block until every tracked async run is terminal, or `id`
22
22
  * to block on one specific async or remembered detached foreground run.
23
23
  *
24
- * `subagent_wait` also returns when a run needs attention — not just on
24
+ * `bg_wait` also returns when a run needs attention — not just on
25
25
  * completion. A child that goes idle or blocks for a decision surfaces
26
26
  * `needs_attention` (the same signal Pi shows as a control notice and,
27
- * interactively, wakes the parent with). Since `subagent_wait` is used exactly
27
+ * interactively, wakes the parent with). Since `bg_wait` is used exactly
28
28
  * where there is no next turn to receive that notice, it must break on it too,
29
29
  * or a stuck child would stall the loop until the timeout. Attention runs are
30
30
  * reported so the caller can inspect / nudge / resume / interrupt them.
31
31
  *
32
- * Wake mechanism: when given Pi's event bus (`deps.events`), `subagent_wait`
32
+ * Wake mechanism: when given Pi's event bus (`deps.events`), `bg_wait`
33
33
  * subscribes to the subagent completion/control channels and wakes the instant
34
34
  * any fires, rather than waiting out a fixed poll interval. A poll still runs
35
35
  * on the interval as a reconciliation fallback (crashed runners, missed
36
36
  * events), and the poll is the source of truth for what actually changed — the
37
- * event only ends the sleep early. With no bus, `subagent_wait` degrades to pure
37
+ * event only ends the sleep early. With no bus, `bg_wait` degrades to pure
38
38
  * polling.
39
39
  */
40
40
 
@@ -82,9 +82,8 @@ export interface SubagentWaitParams {
82
82
  nonBlocking?: boolean;
83
83
  /**
84
84
  * When true, block until EVERY active run in this session (or matching `id`)
85
- * is terminal. Default false: return as soon as the first run finishes, so a
86
- * fleet manager can spawn a replacement and wait again. Ignored when `id`
87
- * targets a single run.
85
+ * is terminal. Default false: return when the first tracked run or provider
86
+ * item finishes or needs attention. Ignored when `id` targets a single run.
88
87
  */
89
88
  all?: boolean;
90
89
  /** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
@@ -241,7 +240,7 @@ function foregroundChildrenNeedingAttention(run: ForegroundResumeRun, indices: S
241
240
  function formatForegroundAttention(run: ForegroundResumeRun, children: ReturnType<typeof foregroundChildrenNeedingAttention>, elapsedMs: number): AgentToolResult<Details> {
242
241
  const childList = children.map((child) => `${child.agent}${child.index !== undefined ? `#${child.index}` : ""}`).join(", ");
243
242
  return result(
244
- `Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call subagent_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
243
+ `Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call bg_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
245
244
  );
246
245
  }
247
246
 
@@ -262,7 +261,7 @@ function backgroundWorkIdentity(item: RegisteredBackgroundWorkItem): string {
262
261
 
263
262
  function backgroundWorkForSession(deps: SubagentWaitDeps, nowMs: number): BackgroundWorkSnapshot {
264
263
  const sessionId = deps.state.currentSessionId;
265
- if (!sessionId) throw new Error("subagent_wait requires an active session identity to scope background work safely.");
264
+ if (!sessionId) throw new Error("bg_wait requires an active session identity to scope background work safely.");
266
265
  return deps.backgroundWork?.snapshot(sessionId, nowMs) ?? snapshotBackgroundWork(sessionId, nowMs);
267
266
  }
268
267
 
@@ -376,7 +375,7 @@ function windowElapsedResult(
376
375
  };
377
376
  }
378
377
 
379
- /** Build the live status shown while async work keeps subagent_wait blocked. */
378
+ /** Build the live status shown while async work keeps bg_wait blocked. */
380
379
  function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
381
380
  const activity = runs.flatMap((run) => {
382
381
  const activeSteps = run.steps.filter((step) => step.status === "pending" || step.status === "running");
@@ -524,7 +523,7 @@ async function waitForDetachedForegroundRun(
524
523
  }
525
524
  if (now() - startedAt >= timeoutMs) {
526
525
  return windowElapsedResult(
527
- `Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call subagent_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
526
+ `Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call bg_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
528
527
  [run.runId],
529
528
  );
530
529
  }
@@ -543,10 +542,10 @@ export async function waitForSubagents(
543
542
  deps: SubagentWaitDeps,
544
543
  ): Promise<AgentToolResult<Details>> {
545
544
  if (deps.enabled === false) {
546
- return result("subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED; returning immediately without blocking background work. Active work keeps going, and you can inspect subagents with subagent({ action: \"status\" }) or rely on completion notifications.");
545
+ return result("bg_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED; returning immediately without blocking background work. Active work keeps going, and you can inspect subagents with subagent({ action: \"status\" }) or rely on completion notifications.");
547
546
  }
548
547
  if (!deps.state.currentSessionId) {
549
- return result("subagent_wait requires an active session identity to scope background work safely.", true);
548
+ return result("bg_wait requires an active session identity to scope background work safely.", true);
550
549
  }
551
550
 
552
551
  const now = deps.now ?? Date.now;
@@ -587,7 +586,7 @@ export async function waitForSubagents(
587
586
  const selected = matches[0];
588
587
  if (selected && params.nonBlocking) {
589
588
  if (!deps.subscribe) {
590
- return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking subagent_wait calls.", true);
589
+ return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking bg_wait calls.", true);
591
590
  }
592
591
  try {
593
592
  const registration = deps.subscribe({ targetKind: selected.kind, runId: selected.id, requestedId: params.id, timeoutMs });
@@ -641,7 +640,7 @@ export async function waitForSubagents(
641
640
  }
642
641
  if (now() - startedAt >= timeoutMs) {
643
642
  return windowElapsedResult(
644
- `Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call subagent_wait again or inspect subagent status.`,
643
+ `Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call bg_wait again or inspect subagent status.`,
645
644
  activeInitialRuns.map((run) => run.id),
646
645
  activeInitialProviderItems,
647
646
  );
@@ -653,7 +652,7 @@ export async function waitForSubagents(
653
652
  providerSnapshot = params.id ? providerSnapshot : backgroundWorkForSession(deps, now());
654
653
  for (const provider of initialProviderNames) {
655
654
  if (!providerSnapshot.providers.includes(provider)) {
656
- return result(`Background-work provider '${provider}' disappeared while subagent_wait was tracking its active work; completion cannot be confirmed.`, true);
655
+ return result(`Background-work provider '${provider}' disappeared while bg_wait was tracking its active work; completion cannot be confirmed.`, true);
657
656
  }
658
657
  }
659
658
  providerActive = providerSnapshot.items;
@@ -711,7 +710,7 @@ export async function waitForSubagents(
711
710
  const finishedCount = finishedAsyncCount + providerFinishedCount;
712
711
  const subject = initialProviderIds.size === 0 ? "run(s)" : "item(s)";
713
712
  const remainder = stillRunning > 0
714
- ? ` ${stillRunning} ${subject} still in flight — call subagent_wait again to catch the next one.`
713
+ ? ` ${stillRunning} ${subject} still in flight — call bg_wait again to catch the next one.`
715
714
  : relevantAttention.length > 0
716
715
  ? " No other work is waitable until attention is handled."
717
716
  : initialProviderIds.size === 0 ? " No runs remain in flight." : " No work remains in flight.";
@@ -99,7 +99,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
99
99
  }
100
100
 
101
101
  /**
102
- * Record a consumed terminal payload for later surfacing by subagent_wait, pruning
102
+ * Record a consumed terminal payload for later surfacing by bg_wait, pruning
103
103
  * stale entries with the same TTL that dedupes completion notifications. The result
104
104
  * file is deleted after delivery, so this record is the only in-process source once
105
105
  * the watcher has consumed it.
@@ -12,32 +12,38 @@ export function registerWaitTool(
12
12
  subscriptions?: Pick<WaitSubscriptionManager, "arm">,
13
13
  defaultTimeoutMs?: number,
14
14
  ): void {
15
- const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
16
- name: "subagent_wait",
17
- label: "Subagent Wait",
18
- description: `Block until background work owned by this session changes, then return.
15
+ const description = `Wait for background, provider, or detached work that has no native completion notification, then return.
19
16
 
20
- In an interactive chat, do not call this merely to wait: return control to the user and let Pi wake the session on completion. Override that default and call it when the current request is run-to-completion — for example, the user asked you to report results back before continuing or a skill cannot return before its work finishes. Headless runs auto-drain current-session work at agent_end; call this when the current turn must receive results before it ends.
17
+ Ordinary async subagent runs already notify this session natively when they complete or need attention. In an interactive chat, return control instead of calling this merely to wait. Use this tool for provider jobs, remembered detached foreground runs, or other background work without a native notification path. Headless runs auto-drain current-session subagent work at agent_end; use this tool only when the current turn must receive non-notifying background work results.
21
18
 
22
19
  • { } — return when the first initially active async run or registered provider item finishes, or when a subagent needs attention.
23
20
  • { all: true } — wait for every async run and provider item that was active when the call began.
24
21
  • { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
25
- • { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. The originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
22
+ • { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. Use this for detached work without native completion delivery; the originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
26
23
  • { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
27
24
  • { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running. Omitted values use waitTool.defaultTimeoutMs, then 30 minutes. Window expiry returns a non-error window_elapsed result with active work identities.
28
25
 
29
- Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
26
+ Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`bg_wait\` (or the deprecated \`subagent_wait\` compatibility alias) in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: bg_wait and its subagent_wait compatibility alias are disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and return immediately without blocking."}`;
27
+ const execute: ToolDefinition<typeof SubagentWaitParams, Details>["execute"] = async (_id, params, signal, onUpdate, ctx) => finalizeToolResult(await waitForSubagents(params, signal, {
28
+ state,
29
+ events: pi.events,
30
+ enabled,
31
+ ...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
32
+ onUpdate,
33
+ ...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
34
+ }));
35
+ const primaryTool: ToolDefinition<typeof SubagentWaitParams, Details> = {
36
+ name: "bg_wait",
37
+ label: "Background Wait",
38
+ description,
30
39
  parameters: SubagentWaitParams,
31
- async execute(_id, params, signal, onUpdate, ctx) {
32
- return finalizeToolResult(await waitForSubagents(params, signal, {
33
- state,
34
- events: pi.events,
35
- enabled,
36
- ...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
37
- onUpdate,
38
- ...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
39
- }));
40
- },
40
+ execute,
41
41
  };
42
- pi.registerTool(tool);
42
+ pi.registerTool(primaryTool);
43
+ pi.registerTool({
44
+ ...primaryTool,
45
+ name: "subagent_wait",
46
+ label: "Subagent Wait (deprecated)",
47
+ description: "Deprecated compatibility alias for `bg_wait`. Use `bg_wait` for background, provider, or detached work without a native completion notification. It has the same parameters and behavior.",
48
+ });
43
49
  }
@@ -49,6 +49,7 @@ import {
49
49
  formatEmptyTerminalAssistantResponseError,
50
50
  extractToolArgsPreview,
51
51
  extractTextFromContent,
52
+ MAX_STREAMED_RECENT_TOOLS,
52
53
  boundStreamedRecentTools,
53
54
  boundStreamedRecentOutput,
54
55
  boundStreamedToolCalls,
@@ -297,6 +298,55 @@ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): Si
297
298
  return snapshot;
298
299
  }
299
300
 
301
+ interface StructuredDelegationProgressState {
302
+ currentTool?: string;
303
+ currentToolArgs?: string;
304
+ recentOutput: string[];
305
+ recentTools: Array<{ tool: string; args: string }>;
306
+ activityState?: AgentProgress["activityState"];
307
+ model?: string;
308
+ toolCount: number;
309
+ tokens: number;
310
+ }
311
+
312
+ function captureStructuredDelegationProgressState(progress: AgentProgress, result: SingleResult): StructuredDelegationProgressState {
313
+ return {
314
+ currentTool: progress.currentTool,
315
+ currentToolArgs: progress.currentToolArgs,
316
+ recentOutput: [...progress.recentOutput],
317
+ recentTools: progress.recentTools.slice(-MAX_STREAMED_RECENT_TOOLS).map(({ tool, args }) => ({ tool, args })),
318
+ activityState: progress.activityState,
319
+ model: progress.model ?? result.model,
320
+ toolCount: progress.toolCount,
321
+ tokens: progress.tokens,
322
+ };
323
+ }
324
+
325
+ function structuredDelegationProgressChanged(
326
+ previous: StructuredDelegationProgressState,
327
+ progress: AgentProgress,
328
+ result: SingleResult,
329
+ ): boolean {
330
+ if (previous.currentTool !== progress.currentTool
331
+ || previous.currentToolArgs !== progress.currentToolArgs
332
+ || previous.activityState !== progress.activityState
333
+ || previous.model !== (progress.model ?? result.model)
334
+ || previous.toolCount !== progress.toolCount
335
+ || previous.tokens !== progress.tokens
336
+ || previous.recentOutput.length !== progress.recentOutput.length) return true;
337
+ for (let index = 0; index < progress.recentOutput.length; index++) {
338
+ if (previous.recentOutput[index] !== progress.recentOutput[index]) return true;
339
+ }
340
+ const recentToolsStart = Math.max(0, progress.recentTools.length - MAX_STREAMED_RECENT_TOOLS);
341
+ if (previous.recentTools.length !== progress.recentTools.length - recentToolsStart) return true;
342
+ for (let index = recentToolsStart; index < progress.recentTools.length; index++) {
343
+ const previousTool = previous.recentTools[index - recentToolsStart];
344
+ const currentTool = progress.recentTools[index];
345
+ if (previousTool?.tool !== currentTool?.tool || previousTool?.args !== currentTool?.args) return true;
346
+ }
347
+ return false;
348
+ }
349
+
300
350
  const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
301
351
  type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
302
352
 
@@ -936,6 +986,7 @@ async function runSingleAttempt(
936
986
  };
937
987
 
938
988
 
989
+ let lastStructuredDelegationProgressState: StructuredDelegationProgressState | undefined;
939
990
  const emitUpdateSnapshot = (text: string) => {
940
991
  if (!options.onUpdate || processClosed) return;
941
992
  const progressSnapshot = snapshotProgress(progress);
@@ -955,6 +1006,10 @@ async function runSingleAttempt(
955
1006
  const fireUpdate = () => {
956
1007
  if (!options.onUpdate || processClosed) return;
957
1008
  progress.durationMs = Date.now() - startTime;
1009
+ if (options.suppressUnchangedDelegationUpdates) {
1010
+ if (lastStructuredDelegationProgressState && !structuredDelegationProgressChanged(lastStructuredDelegationProgressState, progress, result)) return;
1011
+ lastStructuredDelegationProgressState = captureStructuredDelegationProgressState(progress, result);
1012
+ }
958
1013
  const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
959
1014
  emitUpdateSnapshot(output || "(running...)");
960
1015
  };
@@ -1816,7 +1871,11 @@ async function runSyncCompletionInner(
1816
1871
  agent.fallbackModels,
1817
1872
  options.availableModels,
1818
1873
  agent.modelProvider ?? options.preferredModelProvider,
1819
- { scope: options.modelScope, primaryModelFromParent: options.modelOverrideFromParent },
1874
+ {
1875
+ scope: options.modelScope,
1876
+ primaryModelFromParent: options.modelOverrideFromParent,
1877
+ origin: options.modelOrigin ?? (options.modelOverrideFromParent ? "inherited" : "configured"),
1878
+ },
1820
1879
  );
1821
1880
  if (options.workflowChildPermitLaunch && candidates.length > 1) {
1822
1881
  const error = "Workflow child permit does not support model fallback.";
@@ -2159,7 +2218,7 @@ async function runSyncCompletionInner(
2159
2218
  report: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReport,
2160
2219
  reportError: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError,
2161
2220
  fileOutput: childWrittenOutput !== undefined && options.outputPath
2162
- ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
2221
+ ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only", durable: result.savedOutputPath !== undefined }
2163
2222
  : undefined,
2164
2223
  cwd: options.cwd ?? runtimeCwd,
2165
2224
  reportOptional: isAgentContractV1(options.agentContract),