pi-subagents 0.83.0 → 0.84.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 (73) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/bundle/agents.js +4 -4
  3. package/bundle/capability-ceiling.js +1 -1
  4. package/bundle/child-tool-plan.js +3 -3
  5. package/bundle/{chunk-HJJAVHFB.js → chunk-235AWJUQ.js} +35 -32
  6. package/bundle/{chunk-P2NCCTZP.js → chunk-2D52JFLF.js} +1 -1
  7. package/bundle/{chunk-WG2OTTAK.js → chunk-2DHZTCOL.js} +2 -2
  8. package/bundle/{chunk-MBDOYZ2V.js → chunk-2S4K53MV.js} +1 -1
  9. package/bundle/{chunk-527E3D2J.js → chunk-4E7BKDTX.js} +5 -5
  10. package/bundle/{chunk-CZTMUBUI.js → chunk-7BWGNDNK.js} +4 -4
  11. package/bundle/{chunk-HXVKGS33.js → chunk-AKOIVLA5.js} +4 -4
  12. package/bundle/{chunk-MHX22USZ.js → chunk-C5P5CHYK.js} +1 -1
  13. package/bundle/{chunk-5ESXK62X.js → chunk-CGN2TVE7.js} +417 -389
  14. package/bundle/{chunk-VTMNBPCQ.js → chunk-D3JAFGMO.js} +1 -1
  15. package/bundle/{chunk-CDI3GMNV.js → chunk-DZDETGRJ.js} +18 -15
  16. package/bundle/{chunk-BKFWQ642.js → chunk-F4ASL6OV.js} +1 -1
  17. package/bundle/{chunk-JHZ6DZ5M.js → chunk-G2Z6ZH26.js} +1 -1
  18. package/bundle/{chunk-XXKG3P2W.js → chunk-J5YL7LYR.js} +4 -4
  19. package/bundle/{chunk-LDMXJIA6.js → chunk-JG4QKI5M.js} +58 -67
  20. package/bundle/{chunk-OOFW75PV.js → chunk-L3WFKBEI.js} +15 -11
  21. package/bundle/{chunk-Y3I6H6DV.js → chunk-LFDBD4PW.js} +2 -2
  22. package/bundle/{chunk-7F5SK2U3.js → chunk-LNRJCP2Y.js} +17 -14
  23. package/bundle/{chunk-4BW55BTL.js → chunk-NENDSQUY.js} +4 -4
  24. package/bundle/{chunk-6B4ITO2W.js → chunk-NKQ6JNAS.js} +1 -1
  25. package/bundle/{chunk-RZJN63GU.js → chunk-PALJGGYC.js} +2 -2
  26. package/bundle/{chunk-TJQJ3MEA.js → chunk-PV63UFUR.js} +10 -10
  27. package/bundle/{chunk-Z7KVHQHL.js → chunk-QXASAHIA.js} +2 -2
  28. package/bundle/{chunk-HLE22FSH.js → chunk-SSG7IEP5.js} +2 -2
  29. package/bundle/{chunk-CSSZRTKY.js → chunk-V2BZYLEP.js} +2 -2
  30. package/bundle/{chunk-CNLDYEH4.js → chunk-VQVVHSY6.js} +11 -11
  31. package/bundle/{chunk-CGXWSNPA.js → chunk-XDQORLZ5.js} +4 -4
  32. package/bundle/control-channel.js +3 -3
  33. package/bundle/index.js +1 -1
  34. package/bundle/intercom-bridge.js +3 -3
  35. package/bundle/preflight.js +9 -9
  36. package/bundle/project-panes.js +4 -4
  37. package/docs/configuration.md +12 -1
  38. package/docs/extension-api.md +2 -0
  39. package/docs/missions.md +2 -0
  40. package/docs/tool-reference.md +1 -1
  41. package/package.json +1 -1
  42. package/src/agents/agent-management.d.ts +1 -1
  43. package/src/agents/agent-management.js +2 -2
  44. package/src/agents/agents.js +1 -2
  45. package/src/agents/frontmatter.js +22 -18
  46. package/src/extension/index.js +2 -2
  47. package/src/missions/actions.js +7 -2
  48. package/src/missions/lifecycle.d.ts +11 -0
  49. package/src/missions/lifecycle.js +37 -6
  50. package/src/missions/store.js +1 -0
  51. package/src/missions/types.d.ts +2 -0
  52. package/src/runs/background/async-execution.d.ts +4 -1
  53. package/src/runs/background/async-execution.js +18 -3
  54. package/src/runs/background/async-resume.js +3 -2
  55. package/src/runs/background/async-retention.d.ts +1 -0
  56. package/src/runs/background/async-retention.js +16 -10
  57. package/src/runs/background/retained-children.js +1 -1
  58. package/src/runs/background/run-child-session.d.ts +2 -0
  59. package/src/runs/background/run-child-session.js +9 -1
  60. package/src/runs/background/subagent-runner.js +1 -0
  61. package/src/runs/foreground/subagent-executor.js +29 -51
  62. package/src/runs/shared/capability-ceiling.d.ts +2 -1
  63. package/src/runs/shared/capability-ceiling.js +3 -2
  64. package/src/runs/shared/child-session.d.ts +2 -0
  65. package/src/runs/shared/child-session.js +1 -0
  66. package/src/runs/shared/model-resolution-diagnostic.d.ts +11 -0
  67. package/src/runs/shared/model-resolution-diagnostic.js +25 -0
  68. package/src/runs/shared/parallel-handoff.d.ts +1 -1
  69. package/src/runs/shared/parallel-handoff.js +10 -6
  70. package/src/runs/shared/parallel-utils.d.ts +2 -0
  71. package/src/shared/types.d.ts +1 -1
  72. package/src/workflows/scripted-workflow.js +6 -1
  73. package/workflow-script-worker.cjs +450 -175
@@ -87,7 +87,7 @@ import { formatHerdrMachineRunnerUnsupported, resolveMachinePlacement } from "..
87
87
  import { lookupRunnerLauncher, resolveAgentRunnerLauncher } from "../shared/runner-launcher.js";
88
88
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.js";
89
89
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.js";
90
- import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding } from "../../missions/lifecycle.js";
90
+ import { attachMissionToLaunchResult, missionAcceptanceStatus, prepareMissionLaunch, writeMissionAsyncBinding } from "../../missions/lifecycle.js";
91
91
  import { MissionNotFoundError, updateMission } from "../../missions/store.js";
92
92
  import { createMissionWorkflowState } from "../../missions/workflow-state.js";
93
93
  import { resolveAuthorityDecision } from "../../policy/authority.js";
@@ -678,9 +678,6 @@ function resolveForegroundResumeTarget(params, state, options = {}) {
678
678
  ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
679
679
  };
680
680
  }
681
- function isAsyncRunNotFound(error) {
682
- return error instanceof Error && error.message.startsWith("Async run not found.");
683
- }
684
681
  function isMissingExactAsyncStatusFile(error, requested) {
685
682
  return error instanceof Error && error.message === `Status file not found for async run '${requested}'.`;
686
683
  }
@@ -750,38 +747,12 @@ function resolveResumeTarget(params, state, options = {}) {
750
747
  throw foregroundError;
751
748
  return asyncTarget;
752
749
  }
753
- if (foregroundError && !isAsyncRunNotFound(asyncError))
754
- throw foregroundError;
755
750
  if (foregroundError)
756
751
  throw foregroundError;
757
752
  if (asyncError)
758
753
  throw asyncError;
759
754
  throw new Error("Run not found. Provide id or runId.");
760
755
  }
761
- function getAsyncInterruptTarget(state, runId, location, options = {}) {
762
- if (location?.asyncDir) {
763
- return {
764
- asyncId: location.resolvedId ?? runId ?? path.basename(location.asyncDir),
765
- asyncDir: location.asyncDir,
766
- };
767
- }
768
- if (runId) {
769
- const direct = state.asyncJobs.get(runId);
770
- if (direct)
771
- return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
772
- if (options.fallbackToNewest === false)
773
- return undefined;
774
- }
775
- let newest;
776
- for (const job of state.asyncJobs.values()) {
777
- if (job.status !== "running")
778
- continue;
779
- if (!newest || (job.updatedAt ?? 0) > newest.updatedAt) {
780
- newest = { asyncId: job.asyncId, asyncDir: job.asyncDir, updatedAt: job.updatedAt ?? 0 };
781
- }
782
- }
783
- return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
784
- }
785
756
  function isStaleExtensionContextError(error) {
786
757
  if (!(error instanceof Error))
787
758
  return false;
@@ -853,13 +824,13 @@ function emitControlNotification(input) {
853
824
  }
854
825
  }
855
826
  function interruptAsyncRun(state, runId, kill, location) {
856
- const target = getAsyncInterruptTarget(state, runId, location, { fallbackToNewest: runId === undefined });
827
+ const target = location?.asyncDir ? { asyncId: location.resolvedId ?? runId, asyncDir: location.asyncDir } : state.asyncJobs.get(runId);
857
828
  if (!target)
858
829
  return null;
859
830
  const status = reconcileAsyncRun(target.asyncDir, omitUndefinedProperties({ kill })).status;
860
831
  if (!status || status.state !== "running" || typeof status.pid !== "number") {
861
832
  return {
862
- content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
833
+ content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId}'.` }],
863
834
  isError: true,
864
835
  details: { mode: "management", results: [] },
865
836
  };
@@ -1047,6 +1018,7 @@ function appendStepToAsyncChain(input) {
1047
1018
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1048
1019
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1049
1020
  currentModelProvider: parentModel?.provider,
1021
+ parentProviderRegistry: input.ctx.modelRegistry,
1050
1022
  currentModel: parentModel,
1051
1023
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1052
1024
  modelScope: discoveredForAppend.modelScope,
@@ -1086,7 +1058,7 @@ function appendStepToAsyncChain(input) {
1086
1058
  worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
1087
1059
  asyncDir: resolved.location.asyncDir,
1088
1060
  validateOutputBindings: false,
1089
- capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
1061
+ capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.parentSessionId)),
1090
1062
  requiredExtensions: retainedRequiredExtensions,
1091
1063
  }));
1092
1064
  if ("error" in built) {
@@ -1534,6 +1506,7 @@ async function resumeExternalJobFollowUp(input) {
1534
1506
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1535
1507
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1536
1508
  currentModelProvider: parentModel?.provider,
1509
+ parentProviderRegistry: input.ctx.modelRegistry,
1537
1510
  currentModel: parentModel,
1538
1511
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1539
1512
  modelScope: input.modelScope,
@@ -1562,7 +1535,7 @@ async function resumeExternalJobFollowUp(input) {
1562
1535
  availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo),
1563
1536
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1564
1537
  ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1565
- capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(currentSessionId),
1538
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(input.ctx.sessionManager.getSessionId() ?? undefined),
1566
1539
  runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1567
1540
  activeAsyncCapacity,
1568
1541
  externalJobFollowUp: { sourceRunId: input.target.runId, sourceStepIndex: input.target.index, parentProviderJobId: externalJob.providerJobId, requestId, requestDigest },
@@ -1822,6 +1795,7 @@ async function resumeAsyncRun(input) {
1822
1795
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1823
1796
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1824
1797
  currentModelProvider: parentModel?.provider,
1798
+ parentProviderRegistry: input.ctx.modelRegistry,
1825
1799
  currentModel: parentModel,
1826
1800
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1827
1801
  modelScope,
@@ -1858,7 +1832,7 @@ async function resumeAsyncRun(input) {
1858
1832
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1859
1833
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1860
1834
  runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1861
- capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1835
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.ctx.sessionManager.getSessionId() ?? undefined)),
1862
1836
  thinkingCeiling: target.thinkingCeiling,
1863
1837
  activeAsyncCapacity,
1864
1838
  }));
@@ -1963,6 +1937,7 @@ async function resumeAsyncRun(input) {
1963
1937
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1964
1938
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1965
1939
  currentModelProvider: parentModel?.provider,
1940
+ parentProviderRegistry: input.ctx.modelRegistry,
1966
1941
  currentModel: parentModel,
1967
1942
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1968
1943
  modelScope,
@@ -2028,7 +2003,7 @@ async function resumeAsyncRun(input) {
2028
2003
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
2029
2004
  // Recovery descriptors, remembered foreground runs, and current workflow roots
2030
2005
  // preserve parent authority before the selected agent's descendant ceiling.
2031
- capabilityCeiling: intersectSubagentCapabilityCeilings(recoveryDescriptor?.capabilityCeiling ?? (target.source === "foreground" || (target.source === "async" && target.mode === "workflow") ? target.capabilityCeiling : undefined), resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
2006
+ capabilityCeiling: intersectSubagentCapabilityCeilings(recoveryDescriptor?.capabilityCeiling ?? (target.source === "foreground" || (target.source === "async" && target.mode === "workflow") ? target.capabilityCeiling : undefined), resolveCurrentSubagentCapabilityCeiling(input.ctx.sessionManager.getSessionId() ?? undefined)),
2032
2007
  runFanoutBudget: input.params.runFanoutBudget ?? recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
2033
2008
  parentWorkflowRunId: input.params.workflowParentRunId,
2034
2009
  workflowKey: input.params.workflowKey,
@@ -3134,6 +3109,7 @@ async function runAsyncPath(data, deps) {
3134
3109
  parentSessionId: data.parentPiSessionId,
3135
3110
  parentSessionFile: ctx.sessionManager.getSessionFile() ?? undefined,
3136
3111
  currentModelProvider: parentModel?.provider,
3112
+ parentProviderRegistry: ctx.modelRegistry,
3137
3113
  currentModel: parentModel,
3138
3114
  scopedModelIds: data.scopedModelIds,
3139
3115
  modelScope: data.modelScope,
@@ -5093,7 +5069,7 @@ export function createSubagentExecutor(deps) {
5093
5069
  const eventsPath = path.join(asyncDir, "events.jsonl");
5094
5070
  const startedAt = Date.now();
5095
5071
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5096
- const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(currentSessionId));
5072
+ const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(ctx.sessionManager.getSessionId() ?? undefined));
5097
5073
  const completionOwnerId = deps.state.completionOwnerId ?? currentCompletionOwnerId();
5098
5074
  deps.state.completionOwnerId = completionOwnerId;
5099
5075
  const workflowScriptSourceDigest = workflowScriptDigest(requestParams.workflowScript);
@@ -5971,6 +5947,7 @@ export function createSubagentExecutor(deps) {
5971
5947
  ...(result.details.results[0]?.sessionFile ? { sessionPath: result.details.results[0].sessionFile } : {}),
5972
5948
  artifactPaths: child.artifactPaths,
5973
5949
  ...(["completed", "failed"].includes(childStatus) ? { completedAt: new Date().toISOString() } : {}),
5950
+ acceptanceStatus: missionAcceptanceStatus(result.details.results),
5974
5951
  heartbeat: { status: childStatus, ...(childPhase ? { phase: childPhase } : {}) },
5975
5952
  });
5976
5953
  if (result.details.asyncId) {
@@ -6137,7 +6114,7 @@ export function createSubagentExecutor(deps) {
6137
6114
  let lastProgressAt = 0;
6138
6115
  let workflowProgressClosed = false;
6139
6116
  const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
6140
- const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager)));
6117
+ const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(ctx.sessionManager.getSessionId() ?? undefined));
6141
6118
  const sendWorkflowProgress = () => {
6142
6119
  if (workflowProgressClosed)
6143
6120
  return;
@@ -6294,6 +6271,7 @@ export function createSubagentExecutor(deps) {
6294
6271
  ...(result.details.results[0]?.sessionFile ? { sessionPath: result.details.results[0].sessionFile } : {}),
6295
6272
  artifactPaths: child.artifactPaths,
6296
6273
  ...(["completed", "failed"].includes(childStatus) ? { completedAt: new Date().toISOString() } : {}),
6274
+ acceptanceStatus: missionAcceptanceStatus(result.details.results),
6297
6275
  heartbeat: { status: childStatus, ...(childPhase ? { phase: childPhase } : {}) },
6298
6276
  });
6299
6277
  return child;
@@ -7061,22 +7039,22 @@ export function createSubagentExecutor(deps) {
7061
7039
  if (stopResult)
7062
7040
  return stopResult;
7063
7041
  return {
7064
- content: [{ type: "text", text: "No stoppable async run found in this session." }],
7042
+ content: [{ type: "text", text: `No stoppable async run found for '${targetRunId}'.` }],
7065
7043
  isError: true,
7066
7044
  details: { mode: "management", results: [] },
7067
7045
  };
7068
7046
  }
7069
7047
  if (action === "interrupt") {
7070
7048
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
7049
+ if (!targetRunId)
7050
+ return { content: [{ type: "text", text: "action='interrupt' requires id." }], isError: true, details: { mode: "management", results: [] } };
7071
7051
  let resolved;
7072
- if (targetRunId) {
7073
- try {
7074
- resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
7075
- }
7076
- catch (error) {
7077
- const message = error instanceof Error ? error.message : String(error);
7078
- return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
7079
- }
7052
+ try {
7053
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
7054
+ }
7055
+ catch (error) {
7056
+ const message = error instanceof Error ? error.message : String(error);
7057
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
7080
7058
  }
7081
7059
  if (resolved?.kind === "nested")
7082
7060
  return interruptNestedRun(resolved);
@@ -7101,7 +7079,7 @@ export function createSubagentExecutor(deps) {
7101
7079
  if (asyncInterruptResult)
7102
7080
  return asyncInterruptResult;
7103
7081
  return {
7104
- content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
7082
+ content: [{ type: "text", text: `No interrupt-capable run found for '${targetRunId}'.` }],
7105
7083
  isError: true,
7106
7084
  details: { mode: "management", results: [] },
7107
7085
  };
@@ -7117,7 +7095,7 @@ export function createSubagentExecutor(deps) {
7117
7095
  ...ctx,
7118
7096
  cwd: requestCwd,
7119
7097
  config: deps.config,
7120
- currentSessionId: deps.state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined,
7098
+ piSessionId: ctx.sessionManager.getSessionId() ?? undefined,
7121
7099
  discoverAgentsAll: deps.discoverAgentsAll,
7122
7100
  runtimeAgentOwner: deps.pi,
7123
7101
  onAgentsChanged: deps.onAgentsChanged,
@@ -7218,7 +7196,7 @@ export function createSubagentExecutor(deps) {
7218
7196
  let prepareForkSessionForIndex = async () => { };
7219
7197
  // Check before pruner/session preparation, then recheck immediately before each fork
7220
7198
  // because the session capability ceiling can tighten while preparation awaits.
7221
- const launchCapabilityCeiling = () => intersectSubagentCapabilityCeilings(intersectSubagentCapabilityCeilings(effectiveParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(requestSessionId)), deps.childRuntime?.capabilityCeiling);
7199
+ const launchCapabilityCeiling = () => intersectSubagentCapabilityCeilings(intersectSubagentCapabilityCeilings(effectiveParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(requestPiSessionId)), deps.childRuntime?.capabilityCeiling);
7222
7200
  const assertLaunchableAgentsAllowedBeforeFork = () => {
7223
7201
  if (!contextPolicy.usesFork)
7224
7202
  return;
@@ -7499,7 +7477,7 @@ export function createSubagentExecutor(deps) {
7499
7477
  scopedModelIds: scopedModelIdsFromContext(ctx),
7500
7478
  parentSessionId: requestSessionId,
7501
7479
  parentPiSessionId: requestPiSessionId,
7502
- capabilityCeiling: intersectSubagentCapabilityCeilings(effectiveParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(requestSessionId)),
7480
+ capabilityCeiling: intersectSubagentCapabilityCeilings(effectiveParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(requestPiSessionId)),
7503
7481
  runFanoutBudget,
7504
7482
  topLevelAsyncCapacityEligible,
7505
7483
  activeAsyncCapacity,
@@ -48,7 +48,8 @@ export declare function parseSubagentCapabilityCeiling(value: unknown, field?: s
48
48
  export declare function registerSubagentCapabilityCeiling(options: RegisterSubagentCapabilityCeilingOptions): SubagentCapabilityCeilingHandle;
49
49
  export declare function intersectSubagentCapabilityCeilings(...ceilings: Array<ResolvedSubagentCapabilityCeiling | undefined>): ResolvedSubagentCapabilityCeiling | undefined;
50
50
  export declare function resolveSubagentCapabilityCeiling(sessionId: string | undefined, inherited?: ResolvedSubagentCapabilityCeiling): ResolvedSubagentCapabilityCeiling | undefined;
51
- export declare function resolveCurrentSubagentCapabilityCeiling(sessionId: string | undefined): ResolvedSubagentCapabilityCeiling | undefined;
51
+ /** Ceilings are registered under `ctx.sessionManager.getSessionId()`; pass that id, never the session file path. */
52
+ export declare function resolveCurrentSubagentCapabilityCeiling(piSessionId: string | undefined): ResolvedSubagentCapabilityCeiling | undefined;
52
53
  export declare function isAgentAllowedByCapabilityCeiling(agentName: string, ceiling: ResolvedSubagentCapabilityCeiling | undefined): boolean;
53
54
  export declare function capabilityCeilingAgentRestrictionMessage(agentName: string, ceiling: ResolvedSubagentCapabilityCeiling | undefined): string | undefined;
54
55
  export declare function assertAgentAllowedByCapabilityCeiling(agentName: string, ceiling: ResolvedSubagentCapabilityCeiling | undefined): void;
@@ -135,8 +135,9 @@ export function resolveSubagentCapabilityCeiling(sessionId, inherited) {
135
135
  }
136
136
  return intersectSubagentCapabilityCeilings(inherited, ...active);
137
137
  }
138
- export function resolveCurrentSubagentCapabilityCeiling(sessionId) {
139
- return resolveSubagentCapabilityCeiling(sessionId, undefined);
138
+ /** Ceilings are registered under `ctx.sessionManager.getSessionId()`; pass that id, never the session file path. */
139
+ export function resolveCurrentSubagentCapabilityCeiling(piSessionId) {
140
+ return resolveSubagentCapabilityCeiling(piSessionId, undefined);
140
141
  }
141
142
  export function isAgentAllowedByCapabilityCeiling(agentName, ceiling) {
142
143
  return ceiling?.allowedAgents === undefined || ceiling.allowedAgents.includes(agentName);
@@ -109,6 +109,8 @@ export interface ChildSession {
109
109
  readonly modelId: string | undefined;
110
110
  /** Provider id of the selected model when this child inherited that provider from the parent's registry instead of loading its extension. */
111
111
  readonly inheritedProvider?: string;
112
+ /** Provider id of the selected model when an extension in this child's model runtime registered that provider, at load or later (e.g. on `session_start`). */
113
+ readonly claimedProvider?: string;
112
114
  /** Live provider/id of the selected model when Pi marks it virtual (`api === "pi-virtual"`); assistant messages then name the dispatched physical model. */
113
115
  readonly virtualModelId?: string;
114
116
  readonly contextWindow?: number;
@@ -633,6 +633,7 @@ export function createDefaultChildSessionFactory(options = {}) {
633
633
  get sessionId() { return session.sessionId; },
634
634
  get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
635
635
  get inheritedProvider() { return session.model && inheritedProviders.has(session.model.provider) ? session.model.provider : undefined; },
636
+ get claimedProvider() { return session.model && !inheritedProviders.has(session.model.provider) && modelRuntime.getRegisteredProviderIds().includes(session.model.provider) ? session.model.provider : undefined; },
636
637
  get virtualModelId() { return session.model?.api === "pi-virtual" ? `${session.model.provider}/${session.model.id}` : undefined; },
637
638
  get contextWindow() { return session.model?.contextWindow; },
638
639
  };
@@ -31,3 +31,14 @@ export declare function formatChildModelResolutionDiagnostic(diagnostic: ChildMo
31
31
  export declare function formatInheritedProviderDiagnostic(diagnostic: ChildModelResolutionDiagnostic & {
32
32
  provider: string;
33
33
  }): string;
34
+ /**
35
+ * Explain a background child that failed before any successful response on a
36
+ * provider a parent extension registered, when that extension did not load in
37
+ * the child. A parent extension can register a provider under a built-in id,
38
+ * so the model still resolves in the child, through the built-in provider
39
+ * rather than the parent's. Whether that caused this failure is unknown here,
40
+ * hence the conditional wording.
41
+ */
42
+ export declare function formatMissingParentProviderDiagnostic(diagnostic: ChildModelResolutionDiagnostic & {
43
+ provider: string;
44
+ }): string;
@@ -89,3 +89,28 @@ export function formatInheritedProviderDiagnostic(diagnostic) {
89
89
  `If ${provider} depends on those hooks, run this agent with \`async: true\` (a background child loads the ambient extensions unless the agent sets \`extensions\`), or load the extension for this agent with \`subagentOnlyExtensions\` or \`extensions\` in the agent frontmatter.`,
90
90
  ].join("\n");
91
91
  }
92
+ /**
93
+ * Explain a background child that failed before any successful response on a
94
+ * provider a parent extension registered, when that extension did not load in
95
+ * the child. A parent extension can register a provider under a built-in id,
96
+ * so the model still resolves in the child, through the built-in provider
97
+ * rather than the parent's. Whether that caused this failure is unknown here,
98
+ * hence the conditional wording.
99
+ */
100
+ export function formatMissingParentProviderDiagnostic(diagnostic) {
101
+ const subject = diagnostic.agent ? `Agent '${diagnostic.agent}'` : "Subagent";
102
+ const model = diagnostic.model ? `'${diagnostic.model}'` : "this model";
103
+ const provider = `'${diagnostic.provider}'`;
104
+ const facts = `${subject} ran as a background child without the ambient extensions. In the parent, provider ${provider} for ${model} is registered by an extension that did not load in this child, so the child sent its requests without that extension's provider, through the built-in provider of the same id if there is one.`;
105
+ if (capabilityCeilingDeniesExtensions(diagnostic)) {
106
+ return [
107
+ facts,
108
+ `${denyExtensionsClause(diagnostic)}, so that extension cannot load for this child: listed \`subagentOnlyExtensions\` or \`extensions\` entries are suppressed, and leaving \`extensions\` unset does not enable ambient loading.`,
109
+ `If ${provider} depends on that extension, relax the capability ceiling to allow extensions, or use a model whose provider does not.`,
110
+ ].join("\n");
111
+ }
112
+ return [
113
+ facts,
114
+ `If ${provider} depends on that extension, list it in \`subagentOnlyExtensions\` or \`extensions\` in the agent frontmatter, leave \`extensions\` unset so the child loads the ambient extensions, or register it from the host with \`registerRequiredChildExtensions\` so it loads for every launch.`,
115
+ ].join("\n");
116
+ }
@@ -23,7 +23,7 @@ export declare function resolveParallelHandoffChild(input: {
23
23
  child: ParallelHandoffGroup["children"][number];
24
24
  } | undefined;
25
25
  export declare function protectRetainedWorktreeForResume(manifestPath: string, runId: string, childIndex: number, signal?: AbortSignal): Promise<string>;
26
- export declare function resolveRetainedWorktreeCwd(manifestPath: string, runId: string, childIndex: number): string | undefined;
26
+ export declare function resolveRetainedWorktreeCwd(manifestPath: string, runId: string, childIndex: number, recordedWorktreePath?: string): string | undefined;
27
27
  export declare function isTerminalParallelHandoffChildStatus(status: unknown): boolean;
28
28
  export declare function formatStoredParallelHandoffCleanup(manifestPath: string, manifest?: ParallelHandoffManifest): string;
29
29
  export interface ParallelHandoffEvidenceResult {
@@ -165,17 +165,18 @@ export async function protectRetainedWorktreeForResume(manifestPath, runId, chil
165
165
  return cwd;
166
166
  }), { signal }));
167
167
  }
168
- export function resolveRetainedWorktreeCwd(manifestPath, runId, childIndex) {
168
+ export function resolveRetainedWorktreeCwd(manifestPath, runId, childIndex, recordedWorktreePath) {
169
169
  const manifest = readParallelHandoffManifest(manifestPath);
170
- if (!manifest)
171
- return undefined;
172
- if (manifest.runId !== runId)
170
+ if (manifest && manifest.runId !== runId)
173
171
  throw new Error(`Managed worktree handoff belongs to run '${manifest.runId}', not '${runId}'.`);
174
- const match = manifest.groups
172
+ const match = manifest?.groups
175
173
  .flatMap((group) => group.children.map((child) => ({ group, child })))
176
174
  .find(({ child }) => child.index === childIndex);
177
- if (!match)
175
+ if (!manifest || !match) {
176
+ if (recordedWorktreePath)
177
+ throw new Error(`Async run '${runId}' child ${childIndex} ran in managed worktree ${recordedWorktreePath}, but its handoff has no resumable record yet. Retry after the run settles; if it already ended, inspect ${manifestPath}.`);
178
178
  return undefined;
179
+ }
179
180
  const cleanup = match.group.cleanup.tasks.find((task) => task.index === match.child.taskIndex);
180
181
  if (!cleanup)
181
182
  throw new Error(`Async run '${runId}' child ${childIndex} has no managed worktree cleanup record.`);
@@ -185,6 +186,9 @@ export function resolveRetainedWorktreeCwd(manifestPath, runId, childIndex) {
185
186
  const requiredCwd = path.join(cleanup.path, relativeCwd);
186
187
  if (cleanup.worktreeRemoved || cleanup.branchRemoved)
187
188
  throw new Error(`Async run '${runId}' required managed worktree was removed: ${requiredCwd}`);
189
+ // The runner removes the worktree right after this marker, without the locks that guard resume admission.
190
+ if (cleanup.reason?.includes("pending durable handoff capture"))
191
+ throw new Error(`Async run '${runId}' child ${childIndex} is still finalizing its managed worktree: ${requiredCwd}. Retry after the run settles; if it already ended, inspect ${manifestPath}.`);
188
192
  let worktreeRoot = "";
189
193
  let resolvedRequiredCwd = "";
190
194
  try {
@@ -55,6 +55,8 @@ export interface RunnerSubagentStep {
55
55
  }>;
56
56
  modelResponseAliases?: Record<string, string[]>;
57
57
  tools?: string[];
58
+ /** Provider of `model` that a parent extension registers; set only when this child loads no ambient extensions. */
59
+ parentExtensionProvider?: string;
58
60
  excludeTools?: string[];
59
61
  allowNestedSubagents?: boolean;
60
62
  /** Resolved selected-agent policy for launches made by this child. */
@@ -1936,7 +1936,7 @@ export interface AsyncStatus {
1936
1936
  label?: string;
1937
1937
  workflowKey?: string;
1938
1938
  lane?: WorkflowLaneMetadata;
1939
- /** Display-only worktree path copied at launch; handoff remains authoritative. */
1939
+ /** Worktree path copied at setup; the handoff stays authoritative for the cwd. */
1940
1940
  worktreePath?: string;
1941
1941
  /** Display-only branch copied at launch; handoff remains authoritative. */
1942
1942
  branch?: string;
@@ -774,17 +774,22 @@ export function validateWorkflowScript(script, options = {}) {
774
774
  : undefined;
775
775
  const workflowBody = wrapper && astNode(wrapper.body) ? wrapper.body : root;
776
776
  // A rebound `runs` may not be the workflow API, so its calls prove nothing about child launches.
777
- const agentNameError = options.agentNameError && !invalidatesRunsBinding(workflowBody) ? options.agentNameError : undefined;
777
+ const runsRebound = invalidatesRunsBinding(workflowBody);
778
+ const agentNameError = options.agentNameError && !runsRebound ? options.agentNameError : undefined;
778
779
  walkAst(workflowBody, (node) => {
779
780
  if (node !== wrapper && node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) {
780
781
  errors.push({ message: "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains.", ...nodeLocation(node) });
781
782
  }
783
+ if (node.type === "ImportExpression")
784
+ errors.push({ message: "workflowScript cannot load modules with import().", ...nodeLocation(node) });
782
785
  if (directRunsCall(node, "run")) {
783
786
  const args = Array.isArray(node.arguments) ? node.arguments : [];
784
787
  const keyNode = astNode(args[0]) ? args[0] : undefined;
785
788
  const key = literalString(keyNode);
786
789
  if (keyNode && key !== undefined && !KEY_PATTERN.test(key))
787
790
  errors.push({ message: "runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
791
+ if (keyNode?.type === "ObjectExpression" && !runsRebound)
792
+ errors.push({ message: `runs.run expects (key, params); got an object as the first argument. Use runs.run("plan", { agent, task }) or runs.all([{ key, agent, task }]).`, ...nodeLocation(keyNode) });
788
793
  if (astNode(args[1])) {
789
794
  errors.push(...validateStaticBaseRef(args[1], "runs.run"));
790
795
  const message = definitelyNonJson(args[1]);