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 +4 -0
- package/index.ts +173 -53
- package/package.json +5 -6
- package/shims.d.ts +2 -0
- package/resumeStream.ts +0 -104
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 {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
57
|
-
"@mariozechner/pi-ai": ">=0.
|
|
58
|
-
"@mariozechner/pi-coding-agent": ">=0.
|
|
59
|
-
"@mariozechner/pi-tui": ">=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
|
-
}
|