oira666_pi-subagent 0.3.4 → 0.3.6

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.
package/README.md CHANGED
@@ -138,6 +138,8 @@ While a `subagents` tool call is running, mid-stream steering input can be broad
138
138
 
139
139
  ## Subagent Session Resume
140
140
 
141
+ > Requires Pi **0.81.0 or newer**. Crash recovery uses Pi's public full Provider SDK and session-replacement lifecycle.
142
+
141
143
  Subagent subprocesses save sessions in `sessions-subagents`. When a main Pi session is resumed and its latest branch contains an unfinished `subagents` tool call (aborted, errored, or closed by Pi's synthetic unfinished-tool error), the extension can resume that delegation from the saved subagent sessions.
142
144
 
143
145
  The same detection also runs after navigating the session tree in the TUI (Esc navigation): if you jump back to a point whose branch ends in an unfinished `subagents` call, the extension offers to resume those subagents from their saved sessions.
@@ -146,6 +148,8 @@ The same detection also runs after navigating the session tree in the TUI (Esc n
146
148
  - Non-UI modes (`pi -p`, JSON/RPC) resume automatically.
147
149
  - Already-finished subagents are reused as completed; unfinished ones continue from their own saved sessions.
148
150
  - Nested subagents use the same mechanism recursively.
151
+ - Provider fallback goes through the selected model's effective Pi provider, so custom providers, custom APIs, auth-derived endpoints, headers, and provider-scoped environment are preserved.
152
+ - Pending resume state and delayed callbacks are discarded on `/resume`, `/new`, `/fork`, and `/reload`, preventing stale work from an old runtime from leaking into the replacement session.
149
153
 
150
154
  | Env Var | Default | Description |
151
155
  | --- | --- | --- |
package/index.ts CHANGED
@@ -10,11 +10,13 @@
10
10
 
11
11
  import * as fs from "node:fs";
12
12
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
- import { createFauxCore, fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai";
14
- // The synthetic resume provider forwards to the real fallback model through a
15
- // small dispatcher built on stable pi-ai exports. This deliberately avoids the
16
- // deprecated/temporary `@mariozechner/pi-ai/compat` global `streamSimple`.
17
- import { streamSimpleForModel as streamModelSimple } from "./resumeStream.js";
13
+ import {
14
+ createFauxCore,
15
+ createProvider,
16
+ fauxAssistantMessage,
17
+ fauxToolCall,
18
+ lazyStream,
19
+ } from "@mariozechner/pi-ai";
18
20
  import { Type } from "@sinclair/typebox";
19
21
  import { type AgentConfig, discoverAgents } from "./agents.js";
20
22
  import {
@@ -562,7 +564,6 @@ function isStreamingSteerInput(event: any, ctx: { isIdle: () => boolean }): bool
562
564
  return !ctx.isIdle();
563
565
  }
564
566
 
565
- const RESUME_STATE_KEY = "__piSubagentResumeState";
566
567
  const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
567
568
  const RESUME_INTERACTIVE_DELAY_MS = 50;
568
569
 
@@ -572,21 +573,6 @@ type SyntheticResumeState = {
572
573
  trigger: "resumePrompt" | "nextRequest";
573
574
  };
574
575
 
575
- function clearSyntheticResumeState(): void {
576
- const state = getSyntheticResumeState();
577
- state.plans = [];
578
- state.phase = "tool";
579
- state.trigger = "resumePrompt";
580
- }
581
-
582
- function getSyntheticResumeState(): SyntheticResumeState {
583
- const g = globalThis as any;
584
- if (!g[RESUME_STATE_KEY]) {
585
- g[RESUME_STATE_KEY] = { plans: [], phase: "tool", trigger: "resumePrompt" } satisfies SyntheticResumeState;
586
- }
587
- return g[RESUME_STATE_KEY] as SyntheticResumeState;
588
- }
589
-
590
576
  // Model definition for the synthetic subagent-resume provider. Shared between
591
577
  // the faux core (which produces the canned assistant turn) and the provider
592
578
  // registration below.
@@ -663,23 +649,101 @@ export default function (pi: ExtensionAPI) {
663
649
  let lastRestorableModel: any | undefined;
664
650
  let latestSessionCtx: any | undefined;
665
651
  let pendingInteractiveResumePrompt: string | null = null;
652
+ let lifecycleGeneration = 0;
653
+ let sessionActive = false;
654
+ const scheduledTasks = new Set<ReturnType<typeof setTimeout>>();
655
+ const resumeState: SyntheticResumeState = {
656
+ plans: [],
657
+ phase: "tool",
658
+ trigger: "resumePrompt",
659
+ };
660
+
661
+ function clearSyntheticResumeState(): void {
662
+ resumeState.plans = [];
663
+ resumeState.phase = "tool";
664
+ resumeState.trigger = "resumePrompt";
665
+ }
666
+
667
+ function scheduleSessionTask(callback: () => void, delayMs: number): void {
668
+ const expectedGeneration = lifecycleGeneration;
669
+ const timer = setTimeout(() => {
670
+ scheduledTasks.delete(timer);
671
+ if (!sessionActive || expectedGeneration !== lifecycleGeneration) return;
672
+ callback();
673
+ }, delayMs);
674
+ scheduledTasks.add(timer);
675
+ }
666
676
 
667
- async function streamWithRealModelFallback(context: any, options: any, fallback: any) {
668
- if (!fallback) return null;
669
- const auth = resumeModelRegistry
670
- ? await resumeModelRegistry.getApiKeyAndHeaders(fallback)
671
- : { ok: true, apiKey: undefined, headers: undefined };
672
- if (!auth.ok) {
673
- throw new Error(auth.error);
677
+ function mergeProviderHeaders(
678
+ base: Record<string, string | null> | undefined,
679
+ override: Record<string, string | null> | undefined,
680
+ ): Record<string, string | null> | undefined {
681
+ const merged = new Map<string, [string, string | null]>();
682
+ for (const headers of [base, override]) {
683
+ for (const [name, value] of Object.entries(headers ?? {})) {
684
+ const key = name.toLowerCase();
685
+ if (value === null) merged.delete(key);
686
+ else merged.set(key, [name, value]);
687
+ }
674
688
  }
675
- return streamModelSimple(fallback, context, {
676
- ...options,
677
- apiKey: auth.apiKey,
678
- headers: auth.headers,
689
+ return merged.size > 0 ? Object.fromEntries(merged.values()) : undefined;
690
+ }
691
+
692
+ function streamWithRealModelFallback(
693
+ context: any,
694
+ options: any,
695
+ fallback: any,
696
+ expectedGeneration = lifecycleGeneration,
697
+ ) {
698
+ if (!fallback || !resumeModelRegistry) return null;
699
+
700
+ // Use pi 0.81's effective Provider instead of dispatching on model.api.
701
+ // This preserves custom provider streams, provider composition, dynamic
702
+ // auth base URLs, provider-scoped env, and future/custom API identifiers.
703
+ return lazyStream(fallback, async () => {
704
+ if (!sessionActive || expectedGeneration !== lifecycleGeneration) {
705
+ throw new Error("Subagent resume fallback was cancelled by session replacement.");
706
+ }
707
+ const provider = resumeModelRegistry.getProvider?.(fallback.provider);
708
+ if (!provider || provider.id === RESUME_PROVIDER) {
709
+ throw new Error(`Subagent resume fallback provider is unavailable: ${fallback.provider}.`);
710
+ }
711
+ const [providerResolution, modelResolution] = await Promise.all([
712
+ resumeModelRegistry.getProviderAuth?.(fallback.provider),
713
+ resumeModelRegistry.getApiKeyAndHeaders?.(fallback),
714
+ ]);
715
+ if (!sessionActive || expectedGeneration !== lifecycleGeneration) {
716
+ throw new Error("Subagent resume fallback was cancelled by session replacement.");
717
+ }
718
+ if (!providerResolution || !modelResolution?.ok) {
719
+ throw new Error(
720
+ modelResolution?.error ?? `Provider is not configured: ${fallback.provider}`,
721
+ );
722
+ }
723
+ const providerAuth = providerResolution.auth ?? {};
724
+ const requestModel = providerAuth.baseUrl
725
+ ? { ...fallback, baseUrl: providerAuth.baseUrl }
726
+ : fallback;
727
+ const requestOptions = {
728
+ ...options,
729
+ // Model-aware resolution includes configured/model headers. Never
730
+ // forward the synthetic provider's no-op credential.
731
+ apiKey: modelResolution.apiKey,
732
+ headers: mergeProviderHeaders(modelResolution.headers, options?.headers),
733
+ env: {
734
+ ...(providerResolution.env ?? {}),
735
+ ...(modelResolution.env ?? {}),
736
+ ...(options?.env ?? {}),
737
+ },
738
+ };
739
+ return provider.streamSimple(requestModel, context, requestOptions);
679
740
  });
680
741
  }
681
742
 
682
- async function restoreVisibleModelForResume(): Promise<any | undefined> {
743
+ async function restoreVisibleModelForResume(
744
+ expectedGeneration = lifecycleGeneration,
745
+ ): Promise<any | undefined> {
746
+ if (!sessionActive || expectedGeneration !== lifecycleGeneration) return undefined;
683
747
  const restore = modelToRestoreAfterResume ?? lastRestorableModel;
684
748
  if (!restore) return undefined;
685
749
  lastRestorableModel = restore;
@@ -687,7 +751,9 @@ export default function (pi: ExtensionAPI) {
687
751
  try {
688
752
  await pi.setModel(restore);
689
753
  } catch (err) {
690
- console.error("[pi-subagent] Failed to restore real model during resume:", err);
754
+ if (sessionActive && expectedGeneration === lifecycleGeneration) {
755
+ console.error("[pi-subagent] Failed to restore real model during resume:", err);
756
+ }
691
757
  }
692
758
  }
693
759
  return restore;
@@ -712,12 +778,26 @@ export default function (pi: ExtensionAPI) {
712
778
  models: [RESUME_MODEL_DEF],
713
779
  });
714
780
 
715
- pi.registerProvider(RESUME_PROVIDER, {
716
- baseUrl: "http://127.0.0.1/pi-subagent-resume",
717
- api: "openai-responses",
718
- apiKey: "pi-subagent-resume-noop-key",
719
- streamSimple: async (model, context, options) => {
720
- const state = getSyntheticResumeState();
781
+ const resumeProvider = createProvider({
782
+ id: RESUME_PROVIDER,
783
+ name: "Pi Subagent Resume",
784
+ auth: {
785
+ apiKey: {
786
+ name: "Internal synthetic resume provider",
787
+ async resolve() {
788
+ return {
789
+ auth: { apiKey: "pi-subagent-resume-noop-key" },
790
+ source: "internal synthetic provider",
791
+ };
792
+ },
793
+ },
794
+ },
795
+ models: resumeCore.models,
796
+ api: {
797
+ stream: resumeCore.stream,
798
+ streamSimple: (model, context, options) => {
799
+ const state = resumeState;
800
+ const expectedGeneration = lifecycleGeneration;
721
801
  const discoveredPlans = state.plans.length > 0
722
802
  ? state.plans
723
803
  : pendingResumePlans.length > 0
@@ -759,7 +839,7 @@ export default function (pi: ExtensionAPI) {
759
839
  .result()
760
840
  .catch(() => {})
761
841
  .finally(() => {
762
- void restoreVisibleModelForResume();
842
+ void restoreVisibleModelForResume(expectedGeneration);
763
843
  });
764
844
  return stream;
765
845
  }
@@ -768,14 +848,28 @@ export default function (pi: ExtensionAPI) {
768
848
  // injection turn (e.g. a request raced ahead of the model restore).
769
849
  // Forward the request to the real fallback model instead.
770
850
  if (phase === "final") {
771
- const restore = await restoreVisibleModelForResume();
772
- const delegated = await streamWithRealModelFallback(context, options, restore);
773
- if (delegated) return delegated;
851
+ const delegated = streamWithRealModelFallback(
852
+ context,
853
+ options,
854
+ modelToRestoreAfterResume ?? lastRestorableModel,
855
+ expectedGeneration,
856
+ );
857
+ if (delegated) {
858
+ void restoreVisibleModelForResume(expectedGeneration);
859
+ return delegated;
860
+ }
774
861
  }
775
862
 
776
- const restore = await restoreVisibleModelForResume();
777
- const fallback = await streamWithRealModelFallback(context, options, restore ?? lastRestorableModel);
778
- if (fallback) return fallback;
863
+ const fallback = streamWithRealModelFallback(
864
+ context,
865
+ options,
866
+ modelToRestoreAfterResume ?? lastRestorableModel,
867
+ expectedGeneration,
868
+ );
869
+ if (fallback) {
870
+ void restoreVisibleModelForResume(expectedGeneration);
871
+ return fallback;
872
+ }
779
873
 
780
874
  // No real model to fall back to: surface a clear error turn.
781
875
  if (!(plans.length > 0 && phase === "tool")) {
@@ -788,9 +882,10 @@ export default function (pi: ExtensionAPI) {
788
882
  () => fauxAssistantMessage([], { stopReason: "error", errorMessage: errorText }),
789
883
  ]);
790
884
  return resumeCore.streamSimple(model, context, options);
885
+ },
791
886
  },
792
- models: [{ ...RESUME_MODEL_DEF, api: "openai-responses" }],
793
887
  });
888
+ pi.registerProvider(resumeProvider);
794
889
 
795
890
  const depthConfig = resolveDelegationDepthConfig(pi);
796
891
  const { currentDepth, maxDepth, canDelegate, ancestorAgentStack, preventCycles } =
@@ -1178,6 +1273,7 @@ export default function (pi: ExtensionAPI) {
1178
1273
  }
1179
1274
 
1180
1275
  async function restoreModelAfterResumeFailure(ctx?: { ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }) {
1276
+ if (!sessionActive) return;
1181
1277
  const restore = modelToRestoreAfterResume;
1182
1278
  modelToRestoreAfterResume = undefined;
1183
1279
  pendingResumePlans = [];
@@ -1221,7 +1317,14 @@ export default function (pi: ExtensionAPI) {
1221
1317
 
1222
1318
  // Auto-discover agents on session start
1223
1319
  pi.on("session_start", async (event, ctx) => {
1320
+ lifecycleGeneration += 1;
1321
+ sessionActive = true;
1224
1322
  latestSessionCtx = ctx;
1323
+ resumeModelRegistry = ctx.modelRegistry;
1324
+ clearSyntheticResumeState();
1325
+ pendingResumePlans = [];
1326
+ pendingInteractiveResumePrompt = null;
1327
+ modelToRestoreAfterResume = undefined;
1225
1328
  updateCombinedUsageStatus(ctx);
1226
1329
  try {
1227
1330
  // Always repair sessions left on the synthetic resume model, even in
@@ -1317,6 +1420,24 @@ export default function (pi: ExtensionAPI) {
1317
1420
  }
1318
1421
  });
1319
1422
 
1423
+ // Pi 0.81 replaces and rebinds the entire extension runtime on /resume,
1424
+ // /new, /fork, and /reload. Invalidate every detached callback so it cannot
1425
+ // use stale pi/context objects after the old runtime has been torn down.
1426
+ pi.on("session_shutdown", () => {
1427
+ sessionActive = false;
1428
+ lifecycleGeneration += 1;
1429
+ for (const timer of scheduledTasks) clearTimeout(timer);
1430
+ scheduledTasks.clear();
1431
+ clearSyntheticResumeState();
1432
+ pendingResumePlans = [];
1433
+ pendingInteractiveResumePrompt = null;
1434
+ modelToRestoreAfterResume = undefined;
1435
+ latestSessionCtx = undefined;
1436
+ resumeModelRegistry = undefined;
1437
+ activeSubagentUsageSummaries.clear();
1438
+ activeSubagents.clear();
1439
+ });
1440
+
1320
1441
  /**
1321
1442
  * Detect unfinished subagent calls at the current branch leaf and offer to
1322
1443
  * resume them. Shared between session_start (startup/resume) and
@@ -1373,7 +1494,6 @@ export default function (pi: ExtensionAPI) {
1373
1494
  }
1374
1495
 
1375
1496
  pendingResumePlans = [...plans];
1376
- const resumeState = getSyntheticResumeState();
1377
1497
  resumeState.plans = [...plans];
1378
1498
  resumeState.phase = "tool";
1379
1499
  // Headless subprocess/RPC subagents cannot answer a visible resume
@@ -1407,7 +1527,7 @@ export default function (pi: ExtensionAPI) {
1407
1527
  // which is the last extension hook before the initial chat render.
1408
1528
  pendingInteractiveResumePrompt = `Resuming ${totalTaskCount} subagents...`;
1409
1529
  } else {
1410
- setTimeout(() => {
1530
+ scheduleSessionTask(() => {
1411
1531
  try {
1412
1532
  pi.sendUserMessage(`Resuming ${totalTaskCount} subagents...`);
1413
1533
  } catch (err) {
@@ -1452,7 +1572,7 @@ export default function (pi: ExtensionAPI) {
1452
1572
  pi.on("message_end", (_event, ctx) => {
1453
1573
  latestSessionCtx = ctx;
1454
1574
  updateCombinedUsageStatus(ctx);
1455
- setTimeout(() => updateCombinedUsageStatus(ctx), 0);
1575
+ scheduleSessionTask(() => updateCombinedUsageStatus(ctx), 0);
1456
1576
  });
1457
1577
 
1458
1578
  pi.on("tool_execution_end", (event, ctx) => {
@@ -1460,7 +1580,7 @@ export default function (pi: ExtensionAPI) {
1460
1580
  if (isSubagentToolName(event.toolName)) {
1461
1581
  activeSubagentUsageSummaries.delete(event.toolCallId);
1462
1582
  updateCombinedUsageStatus(ctx);
1463
- setTimeout(() => updateCombinedUsageStatus(ctx), 0);
1583
+ scheduleSessionTask(() => updateCombinedUsageStatus(ctx), 0);
1464
1584
  }
1465
1585
  });
1466
1586
 
@@ -1468,7 +1588,7 @@ export default function (pi: ExtensionAPI) {
1468
1588
  const prompt = pendingInteractiveResumePrompt;
1469
1589
  if (!prompt) return;
1470
1590
  pendingInteractiveResumePrompt = null;
1471
- setTimeout(() => {
1591
+ scheduleSessionTask(() => {
1472
1592
  try {
1473
1593
  pi.sendUserMessage(prompt);
1474
1594
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -9,7 +9,6 @@
9
9
  "agents.ts",
10
10
  "runner.ts",
11
11
  "resume.ts",
12
- "resumeStream.ts",
13
12
  "names.ts",
14
13
  "shared.ts",
15
14
  "render.ts",
@@ -53,10 +52,10 @@
53
52
  "typescript": "^5.9.3"
54
53
  },
55
54
  "peerDependencies": {
56
- "@mariozechner/pi-agent-core": ">=0.37.0",
57
- "@mariozechner/pi-ai": ">=0.37.0",
58
- "@mariozechner/pi-coding-agent": ">=0.37.0",
59
- "@mariozechner/pi-tui": ">=0.37.0"
55
+ "@mariozechner/pi-agent-core": ">=0.81.0",
56
+ "@mariozechner/pi-ai": ">=0.81.0",
57
+ "@mariozechner/pi-coding-agent": ">=0.81.0",
58
+ "@mariozechner/pi-tui": ">=0.81.0"
60
59
  },
61
60
  "peerDependenciesMeta": {
62
61
  "@mariozechner/pi-agent-core": {
package/shims.d.ts CHANGED
@@ -12,6 +12,7 @@ declare module "@mariozechner/pi-ai" {
12
12
  export type Message = any;
13
13
 
14
14
  export function createAssistantMessageEventStream(): any;
15
+ export function createProvider(options: any): any;
15
16
  export function lazyStream(model: any, setup: () => Promise<any>): any;
16
17
  export type AssistantMessageEventStream = any;
17
18
  export type ProviderStreams = { stream: (...args: any[]) => any; streamSimple: (...args: any[]) => any };
@@ -90,6 +91,7 @@ declare module "@mariozechner/pi-coding-agent" {
90
91
  export interface ExtensionAPI {
91
92
  registerFlag(name: string, config: any): void;
92
93
  getFlag(name: string): unknown;
94
+ registerProvider(provider: any): void;
93
95
  registerProvider(name: string, provider: any): void;
94
96
  registerTool(tool: any): void;
95
97
  addBeforeAgentStart(hook: (ctx: ExtensionContext) => unknown): void;
package/resumeStream.ts DELETED
@@ -1,104 +0,0 @@
1
- /**
2
- * Durable model-streaming helper for the synthetic subagent-resume provider.
3
- *
4
- * The synthetic resume provider only ever synthesizes an assistant turn that
5
- * carries the `subagent` tool call(s). But pi may still invoke the provider's
6
- * `streamSimple` handler in edge/race situations where the real model has not
7
- * been restored yet (e.g. a request arrives before `pi.setModel(realModel)`
8
- * has propagated). In that case the handler must forward the request to the
9
- * real fallback model and return a valid assistant stream.
10
- *
11
- * Historically this used the global `streamSimple` dispatcher exported from the
12
- * `@mariozechner/pi-ai` package root. Pi's provider/model rework moved that
13
- * dispatcher into the explicitly temporary `@mariozechner/pi-ai/compat`
14
- * entrypoint ("deleted with the coding-agent ModelManager migration").
15
- *
16
- * This module reimplements the same behavior using only stable pi-ai exports:
17
- * - the root `lazyStream` helper (returns a stream synchronously while async
18
- * setup runs behind it), and
19
- * - the per-API `ProviderStreams` factories published under the stable
20
- * `@mariozechner/pi-ai/api/*` subpaths.
21
- *
22
- * `model.api` selects the concrete API implementation, mirroring exactly what
23
- * pi core does when it dispatches a stream to the provider that owns a model.
24
- */
25
- import { lazyStream } from "@mariozechner/pi-ai";
26
- import type { AssistantMessageEventStream, ProviderStreams } from "@mariozechner/pi-ai";
27
-
28
- type ProviderStreamsFactory = () => ProviderStreams;
29
-
30
- /**
31
- * Lazily import the `ProviderStreams` factory for a given `model.api`.
32
- *
33
- * Each entry maps a `KnownApi` id to its stable `/api/*` subpath module and the
34
- * factory export that module provides. Dynamic `import()` keeps the API modules
35
- * out of the hot path until a fallback stream is actually needed, and the
36
- * host's module cache deduplicates repeated loads.
37
- */
38
- const API_LOADERS: Record<string, () => Promise<ProviderStreamsFactory>> = {
39
- "openai-responses": async () =>
40
- (await import("@mariozechner/pi-ai/api/openai-responses.lazy")).openAIResponsesApi,
41
- "openai-completions": async () =>
42
- (await import("@mariozechner/pi-ai/api/openai-completions.lazy")).openAICompletionsApi,
43
- "azure-openai-responses": async () =>
44
- (await import("@mariozechner/pi-ai/api/azure-openai-responses.lazy")).azureOpenAIResponsesApi,
45
- "openai-codex-responses": async () =>
46
- (await import("@mariozechner/pi-ai/api/openai-codex-responses.lazy")).openAICodexResponsesApi,
47
- "anthropic-messages": async () =>
48
- (await import("@mariozechner/pi-ai/api/anthropic-messages.lazy")).anthropicMessagesApi,
49
- "bedrock-converse-stream": async () =>
50
- (await import("@mariozechner/pi-ai/api/bedrock-converse-stream.lazy")).bedrockConverseStreamApi,
51
- "google-generative-ai": async () =>
52
- (await import("@mariozechner/pi-ai/api/google-generative-ai.lazy")).googleGenerativeAIApi,
53
- "google-vertex": async () =>
54
- (await import("@mariozechner/pi-ai/api/google-vertex.lazy")).googleVertexApi,
55
- "mistral-conversations": async () =>
56
- (await import("@mariozechner/pi-ai/api/mistral-conversations.lazy")).mistralConversationsApi,
57
- "pi-messages": async () =>
58
- (await import("@mariozechner/pi-ai/api/pi-messages.lazy")).piMessagesApi,
59
- };
60
-
61
- const providerStreamsCache = new Map<string, ProviderStreams>();
62
-
63
- /** API ids for which the resume fallback can forward to a real model. */
64
- export function getSupportedResumeFallbackApis(): string[] {
65
- return Object.keys(API_LOADERS);
66
- }
67
-
68
- /** Load (and cache) the `ProviderStreams` implementation for a `model.api`. */
69
- export async function resolveProviderStreams(api: string): Promise<ProviderStreams> {
70
- const cached = providerStreamsCache.get(api);
71
- if (cached) return cached;
72
-
73
- const loader = API_LOADERS[api];
74
- if (!loader) {
75
- throw new Error(
76
- `Subagent resume cannot forward to fallback model: unsupported model API "${api}". ` +
77
- `Supported APIs: ${Object.keys(API_LOADERS).join(", ")}.`,
78
- );
79
- }
80
-
81
- const factory = await loader();
82
- const providerStreams = factory();
83
- providerStreamsCache.set(api, providerStreams);
84
- return providerStreams;
85
- }
86
-
87
- /**
88
- * Stream a real fallback model through its owning API implementation.
89
- *
90
- * Returns synchronously via `lazyStream`; the async API-module load and the
91
- * underlying provider request run behind the returned stream. `options` is
92
- * expected to already carry the resolved `apiKey`/`headers` (the extension
93
- * resolves those through `ctx.modelRegistry.getApiKeyAndHeaders`).
94
- */
95
- export function streamSimpleForModel(
96
- model: any,
97
- context: any,
98
- options: any,
99
- ): AssistantMessageEventStream {
100
- return lazyStream(model, async () => {
101
- const providerStreams = await resolveProviderStreams(model.api);
102
- return providerStreams.streamSimple(model, context, options);
103
- });
104
- }