pi-subagents 0.83.1 → 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 (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bundle/{chunk-5YIITCPD.js → chunk-235AWJUQ.js} +24 -21
  3. package/bundle/{chunk-YXFJVCOL.js → chunk-4E7BKDTX.js} +3 -3
  4. package/bundle/{chunk-UFL7IYCW.js → chunk-7BWGNDNK.js} +2 -2
  5. package/bundle/{chunk-ZVOAMTUQ.js → chunk-AKOIVLA5.js} +3 -3
  6. package/bundle/{chunk-RNIZLSEA.js → chunk-CGN2TVE7.js} +1 -0
  7. package/bundle/{chunk-F5DBXJ2G.js → chunk-DZDETGRJ.js} +8 -5
  8. package/bundle/{chunk-C7JMPU5F.js → chunk-J5YL7LYR.js} +1 -1
  9. package/bundle/{chunk-UUP372DR.js → chunk-JG4QKI5M.js} +22 -8
  10. package/bundle/{chunk-UYK5MZWE.js → chunk-L3WFKBEI.js} +1 -1
  11. package/bundle/{chunk-VLFZIN6D.js → chunk-NENDSQUY.js} +2 -2
  12. package/bundle/{chunk-MABL3FUM.js → chunk-NKQ6JNAS.js} +1 -1
  13. package/bundle/{chunk-QP5ZWNIS.js → chunk-PALJGGYC.js} +1 -1
  14. package/bundle/{chunk-IPGZMQXV.js → chunk-PV63UFUR.js} +5 -5
  15. package/bundle/{chunk-AHIFYGR3.js → chunk-VQVVHSY6.js} +5 -5
  16. package/bundle/{chunk-NPIYWVOD.js → chunk-XDQORLZ5.js} +3 -3
  17. package/bundle/index.js +1 -1
  18. package/bundle/preflight.js +2 -2
  19. package/package.json +1 -1
  20. package/src/runs/background/async-execution.d.ts +3 -0
  21. package/src/runs/background/async-execution.js +15 -0
  22. package/src/runs/background/async-retention.d.ts +1 -0
  23. package/src/runs/background/async-retention.js +16 -10
  24. package/src/runs/background/run-child-session.d.ts +2 -0
  25. package/src/runs/background/run-child-session.js +9 -1
  26. package/src/runs/background/subagent-runner.js +1 -0
  27. package/src/runs/foreground/subagent-executor.js +5 -0
  28. package/src/runs/shared/child-session.d.ts +2 -0
  29. package/src/runs/shared/child-session.js +1 -0
  30. package/src/runs/shared/model-resolution-diagnostic.d.ts +11 -0
  31. package/src/runs/shared/model-resolution-diagnostic.js +25 -0
  32. package/src/runs/shared/parallel-utils.d.ts +2 -0
  33. package/src/workflows/scripted-workflow.js +2 -0
  34. package/workflow-script-worker.cjs +449 -175
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "pi-subagents",
4
- "version": "0.83.1",
4
+ "version": "0.84.0",
5
5
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
6
6
  "author": "Nico Bailon",
7
7
  "license": "MIT",
@@ -2,6 +2,7 @@
2
2
  * Async execution logic for subagent tool
3
3
  */
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import type { ParentProviderRegistry } from "../shared/child-session.ts";
5
6
  import { type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
6
7
  import { type ClaudeCodeOverride } from "../shared/claude-code-adapter.ts";
7
8
  import { type ChainStep } from "../../shared/settings.ts";
@@ -44,6 +45,8 @@ interface AsyncExecutionContext {
44
45
  childRuntime?: ChildRuntimeConfig;
45
46
  /** The launching session's project trust; undefined when the host has no trust concept. */
46
47
  projectTrusted?: boolean;
48
+ /** The parent's model registry, read at launch to name a provider an extension registered there. */
49
+ parentProviderRegistry?: Pick<ParentProviderRegistry, "getRegisteredProviderIds">;
47
50
  }
48
51
  export declare const DEFAULT_ASYNC_TIMEOUT_MS: number;
49
52
  interface AsyncChainParams {
@@ -135,6 +135,17 @@ function supportsNativeRunner(nodeExecutable) {
135
135
  && nodeExecutable === process.execPath
136
136
  && !sourceUnderNodeModules();
137
137
  }
138
+ /**
139
+ * A runner child without the ambient extensions does not load the parent extension that
140
+ * registered its model's provider, and still resolves the model when a built-in provider
141
+ * has the same id. Name that provider so a failed run can say so.
142
+ */
143
+ function parentExtensionProviderForChild(ctx, disableAmbientExtensions, model, availableModels, preferredProvider) {
144
+ if (!disableAmbientExtensions || !ctx.parentProviderRegistry)
145
+ return undefined;
146
+ const provider = findModelInfo(model, availableModels, preferredProvider)?.provider;
147
+ return provider && ctx.parentProviderRegistry.getRegisteredProviderIds().includes(provider) ? provider : undefined;
148
+ }
138
149
  export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
139
150
  export function formatAsyncStartedMessage(headline, interactive) {
140
151
  const guidance = interactive
@@ -997,6 +1008,7 @@ export function buildAsyncRunnerSteps(id, params) {
997
1008
  runtimeSnapshotHost: ctx.pi,
998
1009
  });
999
1010
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1011
+ const parentExtensionProvider = externalRunner || machine ? undefined : parentExtensionProviderForChild(ctx, toolPlan.disableAmbientExtensions, selectedModel, availableModels, a.modelProvider ?? ctx.currentModelProvider);
1000
1012
  if (externalRunner && permissionRules) {
1001
1013
  throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
1002
1014
  }
@@ -1026,6 +1038,7 @@ export function buildAsyncRunnerSteps(id, params) {
1026
1038
  thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
1027
1039
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1028
1040
  launchResolvedExtensions,
1041
+ ...(parentExtensionProvider ? { parentExtensionProvider } : {}),
1029
1042
  ...(requestedModel ? { requestedModel } : {}),
1030
1043
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
1031
1044
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
@@ -1811,6 +1824,7 @@ export function executeAsyncSingle(id, params) {
1811
1824
  runtimeSnapshotHost: ctx.pi,
1812
1825
  });
1813
1826
  const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
1827
+ const parentExtensionProvider = externalRunner || machine ? undefined : parentExtensionProviderForChild(ctx, toolPlan.disableAmbientExtensions, selectedModel, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider);
1814
1828
  const fast = params.fast ?? agentConfig.fast;
1815
1829
  const launchThinking = resolveEffectiveThinking(selectedModel, effectiveThinking);
1816
1830
  const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
@@ -1933,6 +1947,7 @@ export function executeAsyncSingle(id, params) {
1933
1947
  ...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
1934
1948
  thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
1935
1949
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1950
+ ...(parentExtensionProvider ? { parentExtensionProvider } : {}),
1936
1951
  ...(requestedModel ? { requestedModel } : {}),
1937
1952
  ...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
1938
1953
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
@@ -3,6 +3,7 @@ export declare const ASYNC_RETENTION_DAYS = 30;
3
3
  export declare const ASYNC_RETENTION_BATCH_SIZE = 100;
4
4
  export declare const ASYNC_RETENTION_DELAY_MS = 60000;
5
5
  export declare const ASYNC_RETENTION_TOMBSTONE_GRACE_MS: number;
6
+ export declare const RESUMABLE_RETENTION_DAYS = 90;
6
7
  export interface AsyncRetentionOptions {
7
8
  asyncDirRoot: string;
8
9
  resultsDir: string;
@@ -15,7 +15,11 @@ export const ASYNC_RETENTION_DAYS = 30;
15
15
  export const ASYNC_RETENTION_BATCH_SIZE = 100;
16
16
  export const ASYNC_RETENTION_DELAY_MS = 60_000;
17
17
  export const ASYNC_RETENTION_TOMBSTONE_GRACE_MS = 24 * 60 * 60 * 1000;
18
+ // A run or result whose session file still exists can be resumed, so it is kept this long
19
+ // instead of ASYNC_RETENTION_DAYS. Session files outlive run dirs, so without a ceiling it is kept forever.
20
+ export const RESUMABLE_RETENTION_DAYS = 90;
18
21
  const RETENTION_MS = ASYNC_RETENTION_DAYS * 24 * 60 * 60 * 1000;
22
+ const RESUMABLE_RETENTION_MS = RESUMABLE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
19
23
  const LOCK_NAME = ".async-retention.lock";
20
24
  const CURSOR_NAME = ".async-retention-cursor.json";
21
25
  const LOG_NAME = "async-retention-maintenance.jsonl";
@@ -239,9 +243,10 @@ function runSkipReason(input) {
239
243
  return "mission-reference";
240
244
  if (hasUnresolvedRunHandoff(runDir, status))
241
245
  return "handoff-reference";
242
- if (hasResumableContract(runDir, status))
243
- return "resumable";
244
246
  const timestamp = statusTimestamp(status, runDir);
247
+ // Unknown ages stay protected.
248
+ if (hasResumableContract(runDir, status) && (timestamp === undefined || timestamp > input.resumableCutoff))
249
+ return "resumable";
245
250
  if (timestamp === undefined)
246
251
  return "unknown-age";
247
252
  if (timestamp > input.cutoff)
@@ -464,13 +469,13 @@ function resultSkipReason(input) {
464
469
  return "workflow-reference";
465
470
  if (hasUnresolvedResultHandoff(input.data))
466
471
  return "handoff-reference";
467
- if (resultHasResumableSession(input.data))
472
+ const timestamp = resultTimestamp(input.data, input.candidate.path);
473
+ if (resultHasResumableSession(input.data) && (timestamp === undefined || timestamp > input.resumableCutoff))
468
474
  return "resumable";
469
475
  if (input.candidate.kind === "archive" && fs.existsSync(path.join(input.resultsDir, "completion-replay", `${encodeURIComponent(runId)}.json`)))
470
476
  return "replay-reference";
471
477
  if (!terminalResult(input.data) && input.candidate.kind !== "replay" && input.candidate.kind !== "archive" && input.candidate.kind !== "tombstone")
472
478
  return "non-terminal-result";
473
- const timestamp = resultTimestamp(input.data, input.candidate.path);
474
479
  if (timestamp === undefined)
475
480
  return "unknown-age";
476
481
  if (timestamp > input.cutoff)
@@ -690,6 +695,7 @@ export async function cleanupAsyncRetention(options) {
690
695
  return finish();
691
696
  const protectedRunIds = new Set(options.protectedRunIds ?? []);
692
697
  const cutoff = currentTime - retentionMs;
698
+ const resumableCutoff = currentTime - RESUMABLE_RETENTION_MS;
693
699
  const cursor = readCursor(maintenanceRoot);
694
700
  const runBudget = Math.ceil(batchSize / 2);
695
701
  const resultBudget = batchSize - runBudget;
@@ -765,7 +771,7 @@ export async function cleanupAsyncRetention(options) {
765
771
  if (reconciliation.repaired)
766
772
  result.repairedRuns += 1;
767
773
  }
768
- const initialReason = runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
774
+ const initialReason = runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, resumableCutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
769
775
  if (initialReason) {
770
776
  increment(result.skipped, initialReason);
771
777
  continue;
@@ -780,7 +786,7 @@ export async function cleanupAsyncRetention(options) {
780
786
  continue;
781
787
  }
782
788
  const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
783
- const finalReason = freshWaitReferences.safe ? runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
789
+ const finalReason = freshWaitReferences.safe ? runSkipReason({ runDir, status, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
784
790
  if (finalReason) {
785
791
  increment(result.skipped, `recheck-${finalReason}`);
786
792
  continue;
@@ -805,7 +811,7 @@ export async function cleanupAsyncRetention(options) {
805
811
  }
806
812
  const recheckStatus = readStatus(tombstone);
807
813
  const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
808
- const recheckReason = freshWaitReferences.safe ? runSkipReason({ runDir: tombstone, status: recheckStatus, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
814
+ const recheckReason = freshWaitReferences.safe ? runSkipReason({ runDir: tombstone, status: recheckStatus, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
809
815
  if (recheckReason) {
810
816
  if (!fs.existsSync(runDir)) {
811
817
  fs.renameSync(tombstone, runDir);
@@ -842,7 +848,7 @@ export async function cleanupAsyncRetention(options) {
842
848
  continue;
843
849
  }
844
850
  const data = readJson(candidate.path);
845
- const reason = resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
851
+ const reason = resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, resumableCutoff, protectedRunIds, waitRunIds: waitReferences.runIds });
846
852
  if (reason) {
847
853
  increment(result.skipped, reason);
848
854
  continue;
@@ -853,7 +859,7 @@ export async function cleanupAsyncRetention(options) {
853
859
  continue;
854
860
  }
855
861
  const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
856
- const finalReason = freshWaitReferences.safe ? resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
862
+ const finalReason = freshWaitReferences.safe ? resultSkipReason({ candidate, data, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
857
863
  if (finalReason) {
858
864
  increment(result.skipped, `recheck-${finalReason}`);
859
865
  continue;
@@ -870,7 +876,7 @@ export async function cleanupAsyncRetention(options) {
870
876
  const tombstoneCandidate = { ...candidate, path: tombstone, kind: "tombstone" };
871
877
  const recheckData = readJson(tombstone);
872
878
  const freshWaitReferences = parseWaitRunIds(waitSubscriptionsDir);
873
- const recheckReason = freshWaitReferences.safe ? resultSkipReason({ candidate: tombstoneCandidate, data: recheckData, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
879
+ const recheckReason = freshWaitReferences.safe ? resultSkipReason({ candidate: tombstoneCandidate, data: recheckData, asyncDirRoot: options.asyncDirRoot, resultsDir: options.resultsDir, maintenanceRoot, cutoff, resumableCutoff, protectedRunIds, waitRunIds: freshWaitReferences.runIds }) : "wait-references-unknown";
874
880
  if (recheckReason) {
875
881
  if (!fs.existsSync(candidate.path)) {
876
882
  fs.renameSync(tombstone, candidate.path);
@@ -83,6 +83,8 @@ export interface RunChildSessionInput {
83
83
  }>;
84
84
  modelResponseAliases?: Record<string, string[]>;
85
85
  mutationTools?: readonly string[];
86
+ /** Provider of the launch model that a parent extension registered and this child does not load. */
87
+ parentExtensionProvider?: string;
86
88
  }
87
89
  export interface RunChildSessionResult {
88
90
  exitCode: number;
@@ -3,7 +3,7 @@ import { qualifyModelWithProvider } from "../../shared/model-info.js";
3
3
  import { acceptChildWatchdogEvent, applyChildWatchdogMessage, childWatchdogIsActive, isChildWatchdogStatusEvent, } from "../../watchdog/child-status.js";
4
4
  import { projectChildLifecycle } from "../shared/child-lifecycle.js";
5
5
  import { formatSubagentModelVerificationError } from "../shared/model-resolution.js";
6
- import { formatChildModelResolutionDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.js";
6
+ import { formatChildModelResolutionDiagnostic, formatMissingParentProviderDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.js";
7
7
  import { isMutatingTool, resolveCurrentPath } from "../shared/long-running-guard.js";
8
8
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.js";
9
9
  import { createReportedChildSessionInput } from "../shared/child-launch.js";
@@ -520,6 +520,14 @@ export function runChildSession(input) {
520
520
  && !input.launch.session.ambientExtensions) {
521
521
  finalError = `${promptErrorMessage}\n\n${formatChildModelResolutionDiagnostic({ agent: input.launch.config.agent, model: input.launch.session.model, host: "runner", capabilityCeiling: input.launch.toolPlan.capabilityCeiling })}`;
522
522
  }
523
+ else if (input.parentExtensionProvider
524
+ && session?.claimedProvider !== input.parentExtensionProvider
525
+ && finalError !== undefined
526
+ && (finalError === promptErrorMessage || finalError === assistantError)
527
+ && toolCount === 0
528
+ && messages.every((message) => message.role !== "assistant" || message.stopReason === "error")) {
529
+ finalError = `${finalError}\n\n${formatMissingParentProviderDiagnostic({ agent: input.launch.config.agent, model: input.launch.session.model, provider: input.parentExtensionProvider, capabilityCeiling: input.launch.toolPlan.capabilityCeiling })}`;
530
+ }
523
531
  const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
524
532
  const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
525
533
  if (!finalError && forced && !forcedDrainAfterFinalSuccess && !interrupted && !timedOut && !stopped) {
@@ -938,6 +938,7 @@ export async function runSingleStepInner(step, ctx) {
938
938
  modelVerificationRegistry: step.modelVerificationRegistry,
939
939
  modelResponseAliases: step.modelResponseAliases,
940
940
  mutationTools: step.mutationTools,
941
+ parentExtensionProvider: step.parentExtensionProvider,
941
942
  }));
942
943
  launched = true;
943
944
  aggregateUsage.input += run.usage.input;
@@ -1018,6 +1018,7 @@ function appendStepToAsyncChain(input) {
1018
1018
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1019
1019
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1020
1020
  currentModelProvider: parentModel?.provider,
1021
+ parentProviderRegistry: input.ctx.modelRegistry,
1021
1022
  currentModel: parentModel,
1022
1023
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1023
1024
  modelScope: discoveredForAppend.modelScope,
@@ -1505,6 +1506,7 @@ async function resumeExternalJobFollowUp(input) {
1505
1506
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1506
1507
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1507
1508
  currentModelProvider: parentModel?.provider,
1509
+ parentProviderRegistry: input.ctx.modelRegistry,
1508
1510
  currentModel: parentModel,
1509
1511
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1510
1512
  modelScope: input.modelScope,
@@ -1793,6 +1795,7 @@ async function resumeAsyncRun(input) {
1793
1795
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1794
1796
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1795
1797
  currentModelProvider: parentModel?.provider,
1798
+ parentProviderRegistry: input.ctx.modelRegistry,
1796
1799
  currentModel: parentModel,
1797
1800
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1798
1801
  modelScope,
@@ -1934,6 +1937,7 @@ async function resumeAsyncRun(input) {
1934
1937
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1935
1938
  parentSessionFile: input.ctx.sessionManager.getSessionFile() ?? undefined,
1936
1939
  currentModelProvider: parentModel?.provider,
1940
+ parentProviderRegistry: input.ctx.modelRegistry,
1937
1941
  currentModel: parentModel,
1938
1942
  scopedModelIds: scopedModelIdsFromContext(input.ctx),
1939
1943
  modelScope,
@@ -3105,6 +3109,7 @@ async function runAsyncPath(data, deps) {
3105
3109
  parentSessionId: data.parentPiSessionId,
3106
3110
  parentSessionFile: ctx.sessionManager.getSessionFile() ?? undefined,
3107
3111
  currentModelProvider: parentModel?.provider,
3112
+ parentProviderRegistry: ctx.modelRegistry,
3108
3113
  currentModel: parentModel,
3109
3114
  scopedModelIds: data.scopedModelIds,
3110
3115
  modelScope: data.modelScope,
@@ -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
+ }
@@ -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. */
@@ -780,6 +780,8 @@ export function validateWorkflowScript(script, options = {}) {
780
780
  if (node !== wrapper && node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) {
781
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) });
782
782
  }
783
+ if (node.type === "ImportExpression")
784
+ errors.push({ message: "workflowScript cannot load modules with import().", ...nodeLocation(node) });
783
785
  if (directRunsCall(node, "run")) {
784
786
  const args = Array.isArray(node.arguments) ? node.arguments : [];
785
787
  const keyNode = astNode(args[0]) ? args[0] : undefined;