pi-subagents 0.60.0 → 0.62.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 (76) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/docs/agents.md +7 -3
  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 +13 -4
  8. package/docs/workflows.md +14 -13
  9. package/install.mjs +1 -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 +41 -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 +121 -63
  18. package/src/agents/agent-serializer.ts +3 -0
  19. package/src/agents/agents.ts +543 -223
  20. package/src/agents/runtime-agent-registry.ts +5 -1
  21. package/src/api/background-work.ts +7 -2
  22. package/src/api/external-runs.ts +67 -4
  23. package/src/api/preflight.ts +17 -8
  24. package/src/api/shared-types.ts +1 -0
  25. package/src/extension/index.ts +7 -4
  26. package/src/extension/public-execution.ts +48 -4
  27. package/src/extension/rpc.ts +62 -4
  28. package/src/extension/schemas.ts +17 -9
  29. package/src/extension/tool-description.ts +15 -19
  30. package/src/runs/background/active-async-capacity.ts +26 -7
  31. package/src/runs/background/async-execution.ts +98 -51
  32. package/src/runs/background/async-job-tracker.ts +62 -3
  33. package/src/runs/background/async-resume.ts +6 -3
  34. package/src/runs/background/async-status.ts +59 -9
  35. package/src/runs/background/auto-drain.ts +1 -1
  36. package/src/runs/background/fleet-view.ts +1 -1
  37. package/src/runs/background/process-terminal.ts +16 -0
  38. package/src/runs/background/result-watcher.ts +1 -1
  39. package/src/runs/background/resume-guidance.ts +1 -1
  40. package/src/runs/background/run-status.ts +2 -2
  41. package/src/runs/background/scheduled-runs.ts +63 -6
  42. package/src/runs/background/steering.ts +4 -1
  43. package/src/runs/background/subagent-runner.ts +15 -9
  44. package/src/runs/background/subagent-wait.ts +20 -21
  45. package/src/runs/background/wait-completions.ts +1 -1
  46. package/src/runs/background/wait-tool.ts +18 -18
  47. package/src/runs/foreground/execution.ts +75 -9
  48. package/src/runs/foreground/subagent-executor.ts +181 -60
  49. package/src/runs/shared/acceptance.ts +113 -27
  50. package/src/runs/shared/capability-ceiling.ts +1 -0
  51. package/src/runs/shared/dynamic-fanout.ts +1 -1
  52. package/src/runs/shared/host-step-status.ts +1 -0
  53. package/src/runs/shared/model-fallback.ts +61 -17
  54. package/src/runs/shared/parallel-utils.ts +2 -6
  55. package/src/runs/shared/permissions.ts +1 -1
  56. package/src/runs/shared/pi-args.ts +24 -12
  57. package/src/runs/shared/pi-spawn.ts +69 -35
  58. package/src/runs/shared/structured-output.ts +33 -6
  59. package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  60. package/src/runs/shared/task-intent.ts +17 -6
  61. package/src/runs/shared/tool-timeout.ts +1 -1
  62. package/src/runs/shared/workflow-graph.ts +3 -2
  63. package/src/shared/atomic-json.ts +3 -1
  64. package/src/shared/fork-context.ts +0 -12
  65. package/src/shared/fork-session-cwd.ts +27 -0
  66. package/src/shared/launch-contract.ts +3 -0
  67. package/src/shared/types.ts +43 -4
  68. package/src/shared/workflow-child-permit.ts +91 -0
  69. package/src/slash/prompt-template-bridge.ts +37 -1
  70. package/src/slash/slash-commands.ts +18 -26
  71. package/src/slash/subagents-admin.ts +2 -0
  72. package/src/tui/render.ts +95 -52
  73. package/src/workflows/scripted-workflow.ts +153 -4
  74. package/src/workflows/workflow-child-summary.ts +1 -1
  75. package/src/workflows/workflow-receipt.ts +41 -4
  76. package/src/workflows/workflow-resources.ts +150 -0
@@ -77,9 +77,10 @@ import {
77
77
  } from "../shared/parallel-utils.ts";
78
78
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
79
79
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
+ import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
80
81
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
81
82
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
82
- import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
83
+ import { clearStructuredOutputCaptures, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
83
84
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
84
85
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
85
86
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
@@ -130,7 +131,7 @@ import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_C
130
131
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
131
132
  import { writeInitialProgressFile } from "../../shared/settings.ts";
132
133
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
133
- import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
134
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
134
135
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
135
136
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
136
137
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
@@ -1397,7 +1398,7 @@ async function runSingleStepInner(
1397
1398
  }
1398
1399
 
1399
1400
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
1400
- ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
1401
+ ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(step.acceptanceInput) })
1401
1402
  : undefined);
1402
1403
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
1403
1404
  let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
@@ -1407,6 +1408,7 @@ async function runSingleStepInner(
1407
1408
  if (!step.runner) {
1408
1409
  resolvedTaskToolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1409
1410
  tools: step.tools,
1411
+ excludeTools: step.excludeTools,
1410
1412
  allowNestedSubagents: step.allowNestedSubagents,
1411
1413
  extensions: step.extensions,
1412
1414
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1627,6 +1629,9 @@ async function runSingleStepInner(
1627
1629
  const effectiveCwd = step.cwd ?? ctx.cwd;
1628
1630
  const cwdError = preflightLaunchCwd(step.requestedCwd ?? effectiveCwd, effectiveCwd);
1629
1631
  if (cwdError) return { agent: step.agent, output: cwdError, error: cwdError, exitCode: 1, context: step.context };
1632
+ if (step.context === "fork" && step.sessionFile && fs.existsSync(step.sessionFile)) {
1633
+ alignForkedSessionCwd(step.sessionFile, effectiveCwd);
1634
+ }
1630
1635
 
1631
1636
  const candidates = step.modelCandidates !== undefined
1632
1637
  ? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
@@ -1681,10 +1686,9 @@ async function runSingleStepInner(
1681
1686
  }));
1682
1687
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1683
1688
  if (effectiveStructuredOutput) {
1684
- try {
1685
- if (fs.existsSync(effectiveStructuredOutput.outputPath)) fs.unlinkSync(effectiveStructuredOutput.outputPath);
1686
- } catch {
1687
- // Missing/stale structured-output files are handled after the child exits.
1689
+ const cleanupError = clearStructuredOutputCaptures(effectiveStructuredOutput);
1690
+ if (cleanupError) {
1691
+ return omitUndefinedProperties({ agent: step.agent, output: cleanupError, error: cleanupError, exitCode: 1, context: step.context });
1688
1692
  }
1689
1693
  }
1690
1694
  const watchdogConfig = resolveWatchdogConfig(step.cwd ?? ctx.cwd);
@@ -1712,6 +1716,7 @@ async function runSingleStepInner(
1712
1716
  inheritSkills: step.inheritSkills,
1713
1717
  requireReadTool: Boolean(step.skills?.length),
1714
1718
  tools: step.tools,
1719
+ excludeTools: step.excludeTools,
1715
1720
  allowNestedSubagents: step.allowNestedSubagents,
1716
1721
  extensions: step.extensions,
1717
1722
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1761,6 +1766,7 @@ async function runSingleStepInner(
1761
1766
  if (step.definitionDigest) {
1762
1767
  const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
1763
1768
  tools: step.tools,
1769
+ excludeTools: step.excludeTools,
1764
1770
  allowNestedSubagents: step.allowNestedSubagents,
1765
1771
  extensions: step.extensions,
1766
1772
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1793,6 +1799,7 @@ async function runSingleStepInner(
1793
1799
  inheritSkills: step.inheritSkills,
1794
1800
  skills: step.skills,
1795
1801
  tools: toolPlan.effectiveToolAllowlist,
1802
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1796
1803
  extensions: toolPlan.extensionArgs,
1797
1804
  mcpDirectTools: toolPlan.effectiveMcpTools,
1798
1805
  ...(step.outputPath ? { outputPath: step.outputPath } : {}),
@@ -2149,7 +2156,7 @@ async function runSingleStepInner(
2149
2156
  report: structuredAcceptanceReport as import("../../shared/types.ts").AcceptanceReport | undefined,
2150
2157
  reportError: structuredAcceptanceReportError,
2151
2158
  fileOutput: childWrittenOutput !== undefined && step.outputPath
2152
- ? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only" }
2159
+ ? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only", durable: resolvedOutput.savedPath !== undefined }
2153
2160
  : undefined,
2154
2161
  cwd: step.cwd ?? ctx.cwd,
2155
2162
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
@@ -5347,7 +5354,6 @@ async function runSubagent(
5347
5354
  statusPayload.error = `Step failed: ${failedStep.agent}`;
5348
5355
  }
5349
5356
  }
5350
- writeStatusPayload();
5351
5357
  try {
5352
5358
  runPersistence.write(resultPath, {
5353
5359
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -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,32 @@ 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\` 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 is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns 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
43
  }
@@ -3,12 +3,12 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
- import { existsSync, unlinkSync } from "node:fs";
6
+ import { existsSync } from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
10
10
  import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
11
- import { alignForkedSessionCwd } from "../../shared/fork-context.ts";
11
+ import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
12
12
  import {
13
13
  ensureArtifactsDir,
14
14
  formatOutputArtifactContent,
@@ -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,
@@ -72,7 +73,7 @@ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledge
72
73
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
73
74
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
74
75
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
75
- import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
76
+ import { clearStructuredOutputCaptures, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
76
77
  import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
77
78
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
78
79
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
@@ -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
 
@@ -361,6 +411,7 @@ async function runSingleAttempt(
361
411
  inheritSkills: agent.inheritSkills,
362
412
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
363
413
  tools: agent.tools,
414
+ excludeTools: agent.excludeTools,
364
415
  allowNestedSubagents: agent.allowNestedSubagents,
365
416
  extensions: agent.extensions,
366
417
  subagentOnlyExtensions: agent.subagentOnlyExtensions,
@@ -414,6 +465,7 @@ async function runSingleAttempt(
414
465
  cwd: options.cwd ?? runtimeCwd,
415
466
  requireReadTool: Boolean(shared.resolvedSkillNames?.length),
416
467
  structuredOutput: Boolean(options.structuredOutput),
468
+ excludeTools: agent.excludeTools,
417
469
  fast: options.fast ?? agent.fast,
418
470
  model: modelArg,
419
471
  modelCandidates: shared.modelCandidates,
@@ -470,6 +522,7 @@ async function runSingleAttempt(
470
522
  inheritSkills: agent.inheritSkills,
471
523
  skills: shared.resolvedSkillNames ?? [],
472
524
  tools: toolPlan.effectiveToolAllowlist,
525
+ ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
473
526
  extensions: toolPlan.extensionArgs,
474
527
  mcpDirectTools: toolPlan.effectiveMcpTools,
475
528
  ...(options.outputPath ? { outputPath: options.outputPath } : {}),
@@ -501,10 +554,14 @@ async function runSingleAttempt(
501
554
  }, options.context);
502
555
  const startTime = Date.now();
503
556
  if (options.structuredOutput) {
504
- try {
505
- if (existsSync(options.structuredOutput.outputPath)) unlinkSync(options.structuredOutput.outputPath);
506
- } catch {
507
- // Missing/stale structured-output files are handled after the child exits.
557
+ const cleanupError = clearStructuredOutputCaptures(options.structuredOutput);
558
+ if (cleanupError) {
559
+ cleanupTempDir(tempDir);
560
+ result.exitCode = 1;
561
+ result.error = cleanupError;
562
+ result.finalOutput = cleanupError;
563
+ result.progressSummary = { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime };
564
+ return result;
508
565
  }
509
566
  }
510
567
  const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
@@ -936,6 +993,7 @@ async function runSingleAttempt(
936
993
  };
937
994
 
938
995
 
996
+ let lastStructuredDelegationProgressState: StructuredDelegationProgressState | undefined;
939
997
  const emitUpdateSnapshot = (text: string) => {
940
998
  if (!options.onUpdate || processClosed) return;
941
999
  const progressSnapshot = snapshotProgress(progress);
@@ -955,6 +1013,10 @@ async function runSingleAttempt(
955
1013
  const fireUpdate = () => {
956
1014
  if (!options.onUpdate || processClosed) return;
957
1015
  progress.durationMs = Date.now() - startTime;
1016
+ if (options.suppressUnchangedDelegationUpdates) {
1017
+ if (lastStructuredDelegationProgressState && !structuredDelegationProgressChanged(lastStructuredDelegationProgressState, progress, result)) return;
1018
+ lastStructuredDelegationProgressState = captureStructuredDelegationProgressState(progress, result);
1019
+ }
958
1020
  const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
959
1021
  emitUpdateSnapshot(output || "(running...)");
960
1022
  };
@@ -1816,7 +1878,11 @@ async function runSyncCompletionInner(
1816
1878
  agent.fallbackModels,
1817
1879
  options.availableModels,
1818
1880
  agent.modelProvider ?? options.preferredModelProvider,
1819
- { scope: options.modelScope, primaryModelFromParent: options.modelOverrideFromParent },
1881
+ {
1882
+ scope: options.modelScope,
1883
+ primaryModelFromParent: options.modelOverrideFromParent,
1884
+ origin: options.modelOrigin ?? (options.modelOverrideFromParent ? "inherited" : "configured"),
1885
+ },
1820
1886
  );
1821
1887
  if (options.workflowChildPermitLaunch && candidates.length > 1) {
1822
1888
  const error = "Workflow child permit does not support model fallback.";
@@ -2159,7 +2225,7 @@ async function runSyncCompletionInner(
2159
2225
  report: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReport,
2160
2226
  reportError: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError,
2161
2227
  fileOutput: childWrittenOutput !== undefined && options.outputPath
2162
- ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
2228
+ ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only", durable: result.savedOutputPath !== undefined }
2163
2229
  : undefined,
2164
2230
  cwd: options.cwd ?? runtimeCwd,
2165
2231
  reportOptional: isAgentContractV1(options.agentContract),