oira666_pi-subagent 0.3.4 → 0.3.7
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 +12 -6
- package/agents/team-lead.md +1 -0
- package/agents.ts +35 -1
- package/index.ts +276 -78
- package/package.json +5 -6
- package/runner.ts +9 -2
- package/shims.d.ts +2 -0
- package/resumeStream.ts +0 -104
package/README.md
CHANGED
|
@@ -79,8 +79,9 @@ Built-in agents are only used as a fallback when **all three** locations are emp
|
|
|
79
79
|
---
|
|
80
80
|
name: writer
|
|
81
81
|
description: Expert technical writer
|
|
82
|
-
model: anthropic/claude-3-5-sonnet
|
|
83
82
|
thinking: low
|
|
83
|
+
first-layer: enabled
|
|
84
|
+
last-layer: disabled
|
|
84
85
|
tools: read,write
|
|
85
86
|
---
|
|
86
87
|
|
|
@@ -93,9 +94,11 @@ You are an expert technical writer focused on clarity and conciseness.
|
|
|
93
94
|
| ------------- | -------- | -------------------- | -------------------------------------------------------- |
|
|
94
95
|
| `name` | Yes | — | Agent identifier used in tool calls |
|
|
95
96
|
| `description` | Yes | — | What the agent does (shown to the main agent) |
|
|
96
|
-
| `model` | No |
|
|
97
|
+
| `model` | No | Current parent model | Legacy fallback only when live parent model context is unavailable |
|
|
97
98
|
| `thinking` | No | Pi default | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
|
|
98
99
|
| `tools` | No | `read,bash,edit,write` | Comma-separated built-in tools |
|
|
100
|
+
| `first-layer` | No | `enabled` | Set to `disabled` to hide/block this agent at depth 1 |
|
|
101
|
+
| `last-layer` | No | `enabled` | Set to `disabled` to hide/block this agent at max depth |
|
|
99
102
|
|
|
100
103
|
Available tools: `read`, `bash`, `edit`, `write`.
|
|
101
104
|
|
|
@@ -103,7 +106,7 @@ The Markdown body becomes the agent's system prompt (appended to Pi's default, n
|
|
|
103
106
|
|
|
104
107
|
## Delegation Guards
|
|
105
108
|
|
|
106
|
-
Depth and cycle guards prevent runaway recursive delegation.
|
|
109
|
+
Depth and cycle guards prevent runaway recursive delegation. Layer availability is evaluated for the child being launched: depth 1 is the first layer, and `PI_SUBAGENT_MAX_DEPTH` is the last layer. The bundled `team-lead` agent sets `last-layer: disabled` so it cannot consume the final delegation layer.
|
|
107
110
|
|
|
108
111
|
| Config | Default | Description |
|
|
109
112
|
| ------------------------------ | ------- | ------------------------------------------------ |
|
|
@@ -138,6 +141,8 @@ While a `subagents` tool call is running, mid-stream steering input can be broad
|
|
|
138
141
|
|
|
139
142
|
## Subagent Session Resume
|
|
140
143
|
|
|
144
|
+
> Requires Pi **0.81.0 or newer**. Crash recovery uses Pi's public full Provider SDK and session-replacement lifecycle.
|
|
145
|
+
|
|
141
146
|
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
147
|
|
|
143
148
|
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 +151,8 @@ The same detection also runs after navigating the session tree in the TUI (Esc n
|
|
|
146
151
|
- Non-UI modes (`pi -p`, JSON/RPC) resume automatically.
|
|
147
152
|
- Already-finished subagents are reused as completed; unfinished ones continue from their own saved sessions.
|
|
148
153
|
- Nested subagents use the same mechanism recursively.
|
|
154
|
+
- 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.
|
|
155
|
+
- 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
156
|
|
|
150
157
|
| Env Var | Default | Description |
|
|
151
158
|
| --- | --- | --- |
|
|
@@ -211,8 +218,7 @@ subagent *instance* by its unique name.
|
|
|
211
218
|
## CLI Argument Proxying
|
|
212
219
|
|
|
213
220
|
Flags passed to the parent `pi` process are forwarded to subagent child
|
|
214
|
-
processes, so they inherit the same provider, API key,
|
|
215
|
-
extension manages itself are blocked from being forwarded.
|
|
221
|
+
processes, so they inherit the same provider, API key, and other runtime settings. At every new launch, the extension explicitly passes the parent's currently active model; changing `/model` mid-conversation therefore affects all subsequently started subagents. Flags the extension manages itself are blocked from being forwarded.
|
|
216
222
|
|
|
217
223
|
**Always forwarded verbatim:**
|
|
218
224
|
|
|
@@ -233,7 +239,7 @@ extension manages itself are blocked from being forwarded.
|
|
|
233
239
|
|
|
234
240
|
| Flag | Overridden by |
|
|
235
241
|
| --- | --- |
|
|
236
|
-
| `--model` | `model:`
|
|
242
|
+
| `--model` | Replaced at launch by the parent's currently active model (`model:` is only a no-context compatibility fallback) |
|
|
237
243
|
| `--thinking` | `thinking:` in agent frontmatter |
|
|
238
244
|
| `--tools` / `--no-tools` | `tools:` in agent frontmatter |
|
|
239
245
|
|
package/agents/team-lead.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: team-lead
|
|
3
3
|
description: "A team of agents with different specializations that can take any complex task, split it into parts, and implement architecture, generation, review, or any other kind of task."
|
|
4
|
+
last-layer: disabled
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
You are an experienced team lead, focused on tasks management. You don't do any work yourself. You delegate.
|
package/agents.ts
CHANGED
|
@@ -24,6 +24,10 @@ export interface AgentConfig {
|
|
|
24
24
|
tools?: string[];
|
|
25
25
|
model?: string;
|
|
26
26
|
thinking?: string;
|
|
27
|
+
/** Whether this agent may be launched at delegation depth 1 (default: true). */
|
|
28
|
+
firstLayer?: boolean;
|
|
29
|
+
/** Whether this agent may be launched at the maximum delegation depth (default: true). */
|
|
30
|
+
lastLayer?: boolean;
|
|
27
31
|
systemPrompt: string;
|
|
28
32
|
source: AgentSource;
|
|
29
33
|
filePath: string;
|
|
@@ -63,8 +67,25 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
function parseLayerSetting(
|
|
71
|
+
value: unknown,
|
|
72
|
+
field: "first-layer" | "last-layer",
|
|
73
|
+
filePath: string,
|
|
74
|
+
): boolean {
|
|
75
|
+
if (value === undefined) return true;
|
|
76
|
+
if (typeof value === "string") {
|
|
77
|
+
const normalized = value.trim().toLowerCase();
|
|
78
|
+
if (normalized === "enabled") return true;
|
|
79
|
+
if (normalized === "disabled") return false;
|
|
80
|
+
}
|
|
81
|
+
console.warn(
|
|
82
|
+
`[pi-subagent] Ignoring invalid ${field} field in "${filePath}". Expected enabled or disabled.`,
|
|
83
|
+
);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
/** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
|
|
67
|
-
function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
|
|
88
|
+
export function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
|
|
68
89
|
let content: string;
|
|
69
90
|
try {
|
|
70
91
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -113,6 +134,8 @@ function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | nu
|
|
|
113
134
|
tools,
|
|
114
135
|
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
115
136
|
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
137
|
+
firstLayer: parseLayerSetting(frontmatter["first-layer"], "first-layer", filePath),
|
|
138
|
+
lastLayer: parseLayerSetting(frontmatter["last-layer"], "last-layer", filePath),
|
|
116
139
|
systemPrompt: body,
|
|
117
140
|
source,
|
|
118
141
|
filePath,
|
|
@@ -161,6 +184,17 @@ function dedupeAgents(
|
|
|
161
184
|
// Public API
|
|
162
185
|
// ---------------------------------------------------------------------------
|
|
163
186
|
|
|
187
|
+
/** Whether an agent is available to be launched at the requested child depth. */
|
|
188
|
+
export function isAgentEnabledAtLayer(
|
|
189
|
+
agent: AgentConfig,
|
|
190
|
+
targetDepth: number,
|
|
191
|
+
maxDepth: number,
|
|
192
|
+
): boolean {
|
|
193
|
+
if (targetDepth === 1 && agent.firstLayer === false) return false;
|
|
194
|
+
if (targetDepth === maxDepth && agent.lastLayer === false) return false;
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
164
198
|
/**
|
|
165
199
|
* Discover all available agents according to the requested scope.
|
|
166
200
|
*
|
package/index.ts
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
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
|
-
import { type AgentConfig, discoverAgents } from "./agents.js";
|
|
21
|
+
import { type AgentConfig, discoverAgents, isAgentEnabledAtLayer } from "./agents.js";
|
|
20
22
|
import {
|
|
21
23
|
allocateSubagentNames,
|
|
22
24
|
clearResumeActive,
|
|
@@ -98,6 +100,38 @@ const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
|
98
100
|
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
99
101
|
const SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV = "PI_SUBAGENT_CONFIRM_PROJECT_AGENTS";
|
|
100
102
|
|
|
103
|
+
const BASE_SUBAGENTS_TOOL_DESCRIPTION = [
|
|
104
|
+
"Delegate work to specialized subagents running as isolated pi processes.",
|
|
105
|
+
"",
|
|
106
|
+
"Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
|
|
107
|
+
" - 1 task -> single delegation",
|
|
108
|
+
" - N tasks -> all N run concurrently in one call",
|
|
109
|
+
"",
|
|
110
|
+
"For sequential work (task B depends on task A's output), make separate",
|
|
111
|
+
"tool calls one after another. Do NOT put dependent tasks in the same array.",
|
|
112
|
+
"",
|
|
113
|
+
'Single: { tasks: [{ agent: "writer", task: "Rewrite README.md" }] }',
|
|
114
|
+
'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
|
|
115
|
+
].join("\n");
|
|
116
|
+
|
|
117
|
+
const GPT_56_SUBAGENT_GUIDANCE =
|
|
118
|
+
"Be careful with subagents: use them when the user explicitly asks or when they are truly necessary, because they are expensive. Good cases: running several exploration tasks in parallel, solving several tasks in parallel, or delegating several large tasks to separate subagents. Bad cases (don't do this): creating many nested subagents with similar tasks, using sequential subagents for simple short tasks, running a subagent just to read a file or execute a bash command, or delegating work that does not need a team or parallel execution (unless the user asked you to).";
|
|
119
|
+
|
|
120
|
+
export function isGpt56Model(model: unknown): boolean {
|
|
121
|
+
if (typeof model === "string") return model.toLowerCase().includes("gpt-5.6");
|
|
122
|
+
if (!model || typeof model !== "object") return false;
|
|
123
|
+
const candidate = model as { id?: unknown; name?: unknown };
|
|
124
|
+
return [candidate.id, candidate.name].some(
|
|
125
|
+
(value) => typeof value === "string" && value.toLowerCase().includes("gpt-5.6"),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getSubagentsToolDescription(model?: unknown): string {
|
|
130
|
+
return isGpt56Model(model)
|
|
131
|
+
? `${BASE_SUBAGENTS_TOOL_DESCRIPTION}\n\n${GPT_56_SUBAGENT_GUIDANCE}`
|
|
132
|
+
: BASE_SUBAGENTS_TOOL_DESCRIPTION;
|
|
133
|
+
}
|
|
134
|
+
|
|
101
135
|
type ProjectAgentConfirmationSetting = "ask" | "never" | "session";
|
|
102
136
|
type ProjectAgentApproval = "once" | "session" | "no";
|
|
103
137
|
|
|
@@ -385,6 +419,15 @@ function makeDetailsFactory(
|
|
|
385
419
|
};
|
|
386
420
|
}
|
|
387
421
|
|
|
422
|
+
function filterAgentsForCurrentLayer(
|
|
423
|
+
agents: AgentConfig[],
|
|
424
|
+
currentDepth: number,
|
|
425
|
+
maxDepth: number,
|
|
426
|
+
): AgentConfig[] {
|
|
427
|
+
const targetDepth = currentDepth + 1;
|
|
428
|
+
return agents.filter((agent) => isAgentEnabledAtLayer(agent, targetDepth, maxDepth));
|
|
429
|
+
}
|
|
430
|
+
|
|
388
431
|
function formatAgentNames(agents: AgentConfig[]): string {
|
|
389
432
|
return agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
390
433
|
}
|
|
@@ -562,7 +605,6 @@ function isStreamingSteerInput(event: any, ctx: { isIdle: () => boolean }): bool
|
|
|
562
605
|
return !ctx.isIdle();
|
|
563
606
|
}
|
|
564
607
|
|
|
565
|
-
const RESUME_STATE_KEY = "__piSubagentResumeState";
|
|
566
608
|
const SUBAGENT_FALLBACK_MODEL_ENV = "PI_SUBAGENT_FALLBACK_MODEL";
|
|
567
609
|
const RESUME_INTERACTIVE_DELAY_MS = 50;
|
|
568
610
|
|
|
@@ -572,21 +614,6 @@ type SyntheticResumeState = {
|
|
|
572
614
|
trigger: "resumePrompt" | "nextRequest";
|
|
573
615
|
};
|
|
574
616
|
|
|
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
617
|
// Model definition for the synthetic subagent-resume provider. Shared between
|
|
591
618
|
// the faux core (which produces the canned assistant turn) and the provider
|
|
592
619
|
// registration below.
|
|
@@ -654,6 +681,25 @@ function getRestorableModel(ctx: any): any | undefined {
|
|
|
654
681
|
return findLastNonResumeModel(ctx) ?? getEnvFallbackModel(ctx);
|
|
655
682
|
}
|
|
656
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Pick the parent model inherited by a subagent launch.
|
|
686
|
+
*
|
|
687
|
+
* A normal tool call was emitted by the current model, so that model is the
|
|
688
|
+
* authoritative choice. Looking backward in the session is only appropriate
|
|
689
|
+
* while our own synthetic resume model is active (or no current model exists).
|
|
690
|
+
*/
|
|
691
|
+
export function selectParentModelForSubagent(
|
|
692
|
+
currentModel: any | undefined,
|
|
693
|
+
modelBeforeSynthetic: any | undefined,
|
|
694
|
+
historicalRealModel: any | undefined,
|
|
695
|
+
lastRestorableModel: any | undefined,
|
|
696
|
+
): any | undefined {
|
|
697
|
+
if (currentModel?.provider && currentModel.provider !== RESUME_PROVIDER) {
|
|
698
|
+
return currentModel;
|
|
699
|
+
}
|
|
700
|
+
return modelBeforeSynthetic ?? historicalRealModel ?? lastRestorableModel;
|
|
701
|
+
}
|
|
702
|
+
|
|
657
703
|
// ---------------------------------------------------------------------------
|
|
658
704
|
// Extension entry point
|
|
659
705
|
// ---------------------------------------------------------------------------
|
|
@@ -663,23 +709,116 @@ export default function (pi: ExtensionAPI) {
|
|
|
663
709
|
let lastRestorableModel: any | undefined;
|
|
664
710
|
let latestSessionCtx: any | undefined;
|
|
665
711
|
let pendingInteractiveResumePrompt: string | null = null;
|
|
712
|
+
let lifecycleGeneration = 0;
|
|
713
|
+
let sessionActive = false;
|
|
714
|
+
const scheduledTasks = new Set<ReturnType<typeof setTimeout>>();
|
|
715
|
+
const resumeState: SyntheticResumeState = {
|
|
716
|
+
plans: [],
|
|
717
|
+
phase: "tool",
|
|
718
|
+
trigger: "resumePrompt",
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
function clearSyntheticResumeState(): void {
|
|
722
|
+
resumeState.plans = [];
|
|
723
|
+
resumeState.phase = "tool";
|
|
724
|
+
resumeState.trigger = "resumePrompt";
|
|
725
|
+
}
|
|
666
726
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
throw new Error(auth.error);
|
|
727
|
+
function getParentModelForSubagent(ctx: any): any | undefined {
|
|
728
|
+
const currentModel = ctx?.model;
|
|
729
|
+
// Avoid historical lookup during normal calls: the current assistant
|
|
730
|
+
// response is the one that emitted the subagents tool call.
|
|
731
|
+
if (currentModel?.provider && currentModel.provider !== RESUME_PROVIDER) {
|
|
732
|
+
return currentModel;
|
|
674
733
|
}
|
|
675
|
-
return
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
734
|
+
return selectParentModelForSubagent(
|
|
735
|
+
currentModel,
|
|
736
|
+
modelToRestoreAfterResume,
|
|
737
|
+
findLastNonResumeModel(ctx) ?? getEnvFallbackModel(ctx),
|
|
738
|
+
lastRestorableModel,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function scheduleSessionTask(callback: () => void, delayMs: number): void {
|
|
743
|
+
const expectedGeneration = lifecycleGeneration;
|
|
744
|
+
const timer = setTimeout(() => {
|
|
745
|
+
scheduledTasks.delete(timer);
|
|
746
|
+
if (!sessionActive || expectedGeneration !== lifecycleGeneration) return;
|
|
747
|
+
callback();
|
|
748
|
+
}, delayMs);
|
|
749
|
+
scheduledTasks.add(timer);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function mergeProviderHeaders(
|
|
753
|
+
base: Record<string, string | null> | undefined,
|
|
754
|
+
override: Record<string, string | null> | undefined,
|
|
755
|
+
): Record<string, string | null> | undefined {
|
|
756
|
+
const merged = new Map<string, [string, string | null]>();
|
|
757
|
+
for (const headers of [base, override]) {
|
|
758
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
759
|
+
const key = name.toLowerCase();
|
|
760
|
+
if (value === null) merged.delete(key);
|
|
761
|
+
else merged.set(key, [name, value]);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return merged.size > 0 ? Object.fromEntries(merged.values()) : undefined;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function streamWithRealModelFallback(
|
|
768
|
+
context: any,
|
|
769
|
+
options: any,
|
|
770
|
+
fallback: any,
|
|
771
|
+
expectedGeneration = lifecycleGeneration,
|
|
772
|
+
) {
|
|
773
|
+
if (!fallback || !resumeModelRegistry) return null;
|
|
774
|
+
|
|
775
|
+
// Use pi 0.81's effective Provider instead of dispatching on model.api.
|
|
776
|
+
// This preserves custom provider streams, provider composition, dynamic
|
|
777
|
+
// auth base URLs, provider-scoped env, and future/custom API identifiers.
|
|
778
|
+
return lazyStream(fallback, async () => {
|
|
779
|
+
if (!sessionActive || expectedGeneration !== lifecycleGeneration) {
|
|
780
|
+
throw new Error("Subagent resume fallback was cancelled by session replacement.");
|
|
781
|
+
}
|
|
782
|
+
const provider = resumeModelRegistry.getProvider?.(fallback.provider);
|
|
783
|
+
if (!provider || provider.id === RESUME_PROVIDER) {
|
|
784
|
+
throw new Error(`Subagent resume fallback provider is unavailable: ${fallback.provider}.`);
|
|
785
|
+
}
|
|
786
|
+
const [providerResolution, modelResolution] = await Promise.all([
|
|
787
|
+
resumeModelRegistry.getProviderAuth?.(fallback.provider),
|
|
788
|
+
resumeModelRegistry.getApiKeyAndHeaders?.(fallback),
|
|
789
|
+
]);
|
|
790
|
+
if (!sessionActive || expectedGeneration !== lifecycleGeneration) {
|
|
791
|
+
throw new Error("Subagent resume fallback was cancelled by session replacement.");
|
|
792
|
+
}
|
|
793
|
+
if (!providerResolution || !modelResolution?.ok) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
modelResolution?.error ?? `Provider is not configured: ${fallback.provider}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const providerAuth = providerResolution.auth ?? {};
|
|
799
|
+
const requestModel = providerAuth.baseUrl
|
|
800
|
+
? { ...fallback, baseUrl: providerAuth.baseUrl }
|
|
801
|
+
: fallback;
|
|
802
|
+
const requestOptions = {
|
|
803
|
+
...options,
|
|
804
|
+
// Model-aware resolution includes configured/model headers. Never
|
|
805
|
+
// forward the synthetic provider's no-op credential.
|
|
806
|
+
apiKey: modelResolution.apiKey,
|
|
807
|
+
headers: mergeProviderHeaders(modelResolution.headers, options?.headers),
|
|
808
|
+
env: {
|
|
809
|
+
...(providerResolution.env ?? {}),
|
|
810
|
+
...(modelResolution.env ?? {}),
|
|
811
|
+
...(options?.env ?? {}),
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
return provider.streamSimple(requestModel, context, requestOptions);
|
|
679
815
|
});
|
|
680
816
|
}
|
|
681
817
|
|
|
682
|
-
async function restoreVisibleModelForResume(
|
|
818
|
+
async function restoreVisibleModelForResume(
|
|
819
|
+
expectedGeneration = lifecycleGeneration,
|
|
820
|
+
): Promise<any | undefined> {
|
|
821
|
+
if (!sessionActive || expectedGeneration !== lifecycleGeneration) return undefined;
|
|
683
822
|
const restore = modelToRestoreAfterResume ?? lastRestorableModel;
|
|
684
823
|
if (!restore) return undefined;
|
|
685
824
|
lastRestorableModel = restore;
|
|
@@ -687,7 +826,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
687
826
|
try {
|
|
688
827
|
await pi.setModel(restore);
|
|
689
828
|
} catch (err) {
|
|
690
|
-
|
|
829
|
+
if (sessionActive && expectedGeneration === lifecycleGeneration) {
|
|
830
|
+
console.error("[pi-subagent] Failed to restore real model during resume:", err);
|
|
831
|
+
}
|
|
691
832
|
}
|
|
692
833
|
}
|
|
693
834
|
return restore;
|
|
@@ -712,12 +853,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
712
853
|
models: [RESUME_MODEL_DEF],
|
|
713
854
|
});
|
|
714
855
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
856
|
+
const resumeProvider = createProvider({
|
|
857
|
+
id: RESUME_PROVIDER,
|
|
858
|
+
name: "Pi Subagent Resume",
|
|
859
|
+
auth: {
|
|
860
|
+
apiKey: {
|
|
861
|
+
name: "Internal synthetic resume provider",
|
|
862
|
+
async resolve() {
|
|
863
|
+
return {
|
|
864
|
+
auth: { apiKey: "pi-subagent-resume-noop-key" },
|
|
865
|
+
source: "internal synthetic provider",
|
|
866
|
+
};
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
models: resumeCore.models,
|
|
871
|
+
api: {
|
|
872
|
+
stream: resumeCore.stream,
|
|
873
|
+
streamSimple: (model, context, options) => {
|
|
874
|
+
const state = resumeState;
|
|
875
|
+
const expectedGeneration = lifecycleGeneration;
|
|
721
876
|
const discoveredPlans = state.plans.length > 0
|
|
722
877
|
? state.plans
|
|
723
878
|
: pendingResumePlans.length > 0
|
|
@@ -759,7 +914,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
759
914
|
.result()
|
|
760
915
|
.catch(() => {})
|
|
761
916
|
.finally(() => {
|
|
762
|
-
void restoreVisibleModelForResume();
|
|
917
|
+
void restoreVisibleModelForResume(expectedGeneration);
|
|
763
918
|
});
|
|
764
919
|
return stream;
|
|
765
920
|
}
|
|
@@ -768,14 +923,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
768
923
|
// injection turn (e.g. a request raced ahead of the model restore).
|
|
769
924
|
// Forward the request to the real fallback model instead.
|
|
770
925
|
if (phase === "final") {
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
926
|
+
const delegated = streamWithRealModelFallback(
|
|
927
|
+
context,
|
|
928
|
+
options,
|
|
929
|
+
modelToRestoreAfterResume ?? lastRestorableModel,
|
|
930
|
+
expectedGeneration,
|
|
931
|
+
);
|
|
932
|
+
if (delegated) {
|
|
933
|
+
void restoreVisibleModelForResume(expectedGeneration);
|
|
934
|
+
return delegated;
|
|
935
|
+
}
|
|
774
936
|
}
|
|
775
937
|
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
938
|
+
const fallback = streamWithRealModelFallback(
|
|
939
|
+
context,
|
|
940
|
+
options,
|
|
941
|
+
modelToRestoreAfterResume ?? lastRestorableModel,
|
|
942
|
+
expectedGeneration,
|
|
943
|
+
);
|
|
944
|
+
if (fallback) {
|
|
945
|
+
void restoreVisibleModelForResume(expectedGeneration);
|
|
946
|
+
return fallback;
|
|
947
|
+
}
|
|
779
948
|
|
|
780
949
|
// No real model to fall back to: surface a clear error turn.
|
|
781
950
|
if (!(plans.length > 0 && phase === "tool")) {
|
|
@@ -788,9 +957,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
788
957
|
() => fauxAssistantMessage([], { stopReason: "error", errorMessage: errorText }),
|
|
789
958
|
]);
|
|
790
959
|
return resumeCore.streamSimple(model, context, options);
|
|
960
|
+
},
|
|
791
961
|
},
|
|
792
|
-
models: [{ ...RESUME_MODEL_DEF, api: "openai-responses" }],
|
|
793
962
|
});
|
|
963
|
+
pi.registerProvider(resumeProvider);
|
|
794
964
|
|
|
795
965
|
const depthConfig = resolveDelegationDepthConfig(pi);
|
|
796
966
|
const { currentDepth, maxDepth, canDelegate, ancestorAgentStack, preventCycles } =
|
|
@@ -1178,6 +1348,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1178
1348
|
}
|
|
1179
1349
|
|
|
1180
1350
|
async function restoreModelAfterResumeFailure(ctx?: { ui?: { notify?: (message: string, type?: "info" | "warning" | "error") => void } }) {
|
|
1351
|
+
if (!sessionActive) return;
|
|
1181
1352
|
const restore = modelToRestoreAfterResume;
|
|
1182
1353
|
modelToRestoreAfterResume = undefined;
|
|
1183
1354
|
pendingResumePlans = [];
|
|
@@ -1221,7 +1392,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1221
1392
|
|
|
1222
1393
|
// Auto-discover agents on session start
|
|
1223
1394
|
pi.on("session_start", async (event, ctx) => {
|
|
1395
|
+
lifecycleGeneration += 1;
|
|
1396
|
+
sessionActive = true;
|
|
1224
1397
|
latestSessionCtx = ctx;
|
|
1398
|
+
resumeModelRegistry = ctx.modelRegistry;
|
|
1399
|
+
clearSyntheticResumeState();
|
|
1400
|
+
pendingResumePlans = [];
|
|
1401
|
+
pendingInteractiveResumePrompt = null;
|
|
1402
|
+
modelToRestoreAfterResume = undefined;
|
|
1225
1403
|
updateCombinedUsageStatus(ctx);
|
|
1226
1404
|
try {
|
|
1227
1405
|
// Always repair sessions left on the synthetic resume model, even in
|
|
@@ -1239,7 +1417,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1239
1417
|
if (!canDelegate) return;
|
|
1240
1418
|
|
|
1241
1419
|
const discovery = discoverAgents(ctx.cwd, "both");
|
|
1242
|
-
discoveredAgents = discovery.agents;
|
|
1420
|
+
discoveredAgents = filterAgentsForCurrentLayer(discovery.agents, currentDepth, maxDepth);
|
|
1243
1421
|
currentSessionId = ctx.sessionManager.getSessionId?.() ?? "ephemeral";
|
|
1244
1422
|
currentSubagentSessionRoot = getDefaultSubagentSessionRoot(ctx);
|
|
1245
1423
|
if (resumableSubagentsDisabled()) {
|
|
@@ -1317,6 +1495,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
1317
1495
|
}
|
|
1318
1496
|
});
|
|
1319
1497
|
|
|
1498
|
+
// Pi 0.81 replaces and rebinds the entire extension runtime on /resume,
|
|
1499
|
+
// /new, /fork, and /reload. Invalidate every detached callback so it cannot
|
|
1500
|
+
// use stale pi/context objects after the old runtime has been torn down.
|
|
1501
|
+
pi.on("session_shutdown", () => {
|
|
1502
|
+
sessionActive = false;
|
|
1503
|
+
lifecycleGeneration += 1;
|
|
1504
|
+
for (const timer of scheduledTasks) clearTimeout(timer);
|
|
1505
|
+
scheduledTasks.clear();
|
|
1506
|
+
clearSyntheticResumeState();
|
|
1507
|
+
pendingResumePlans = [];
|
|
1508
|
+
pendingInteractiveResumePrompt = null;
|
|
1509
|
+
modelToRestoreAfterResume = undefined;
|
|
1510
|
+
latestSessionCtx = undefined;
|
|
1511
|
+
resumeModelRegistry = undefined;
|
|
1512
|
+
activeSubagentUsageSummaries.clear();
|
|
1513
|
+
activeSubagents.clear();
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1320
1516
|
/**
|
|
1321
1517
|
* Detect unfinished subagent calls at the current branch leaf and offer to
|
|
1322
1518
|
* resume them. Shared between session_start (startup/resume) and
|
|
@@ -1373,7 +1569,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1373
1569
|
}
|
|
1374
1570
|
|
|
1375
1571
|
pendingResumePlans = [...plans];
|
|
1376
|
-
const resumeState = getSyntheticResumeState();
|
|
1377
1572
|
resumeState.plans = [...plans];
|
|
1378
1573
|
resumeState.phase = "tool";
|
|
1379
1574
|
// Headless subprocess/RPC subagents cannot answer a visible resume
|
|
@@ -1407,7 +1602,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1407
1602
|
// which is the last extension hook before the initial chat render.
|
|
1408
1603
|
pendingInteractiveResumePrompt = `Resuming ${totalTaskCount} subagents...`;
|
|
1409
1604
|
} else {
|
|
1410
|
-
|
|
1605
|
+
scheduleSessionTask(() => {
|
|
1411
1606
|
try {
|
|
1412
1607
|
pi.sendUserMessage(`Resuming ${totalTaskCount} subagents...`);
|
|
1413
1608
|
} catch (err) {
|
|
@@ -1452,7 +1647,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1452
1647
|
pi.on("message_end", (_event, ctx) => {
|
|
1453
1648
|
latestSessionCtx = ctx;
|
|
1454
1649
|
updateCombinedUsageStatus(ctx);
|
|
1455
|
-
|
|
1650
|
+
scheduleSessionTask(() => updateCombinedUsageStatus(ctx), 0);
|
|
1456
1651
|
});
|
|
1457
1652
|
|
|
1458
1653
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
@@ -1460,7 +1655,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1460
1655
|
if (isSubagentToolName(event.toolName)) {
|
|
1461
1656
|
activeSubagentUsageSummaries.delete(event.toolCallId);
|
|
1462
1657
|
updateCombinedUsageStatus(ctx);
|
|
1463
|
-
|
|
1658
|
+
scheduleSessionTask(() => updateCombinedUsageStatus(ctx), 0);
|
|
1464
1659
|
}
|
|
1465
1660
|
});
|
|
1466
1661
|
|
|
@@ -1468,7 +1663,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1468
1663
|
const prompt = pendingInteractiveResumePrompt;
|
|
1469
1664
|
if (!prompt) return;
|
|
1470
1665
|
pendingInteractiveResumePrompt = null;
|
|
1471
|
-
|
|
1666
|
+
scheduleSessionTask(() => {
|
|
1472
1667
|
try {
|
|
1473
1668
|
pi.sendUserMessage(prompt);
|
|
1474
1669
|
} catch (err) {
|
|
@@ -1571,22 +1766,13 @@ keeping their full previous context:
|
|
|
1571
1766
|
|
|
1572
1767
|
// Register the subagents tool
|
|
1573
1768
|
if (canDelegate) {
|
|
1574
|
-
|
|
1769
|
+
let registeredForGpt56 = false;
|
|
1770
|
+
const registerSubagentsTool = (model?: unknown) => {
|
|
1771
|
+
registeredForGpt56 = isGpt56Model(model);
|
|
1772
|
+
pi.registerTool({
|
|
1575
1773
|
name: SUBAGENT_TOOL_NAME,
|
|
1576
1774
|
label: "Subagents",
|
|
1577
|
-
description:
|
|
1578
|
-
"Delegate work to specialized subagents running as isolated pi processes.",
|
|
1579
|
-
"",
|
|
1580
|
-
"Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
|
|
1581
|
-
" - 1 task -> single delegation",
|
|
1582
|
-
" - N tasks -> all N run concurrently in one call",
|
|
1583
|
-
"",
|
|
1584
|
-
"For sequential work (task B depends on task A's output), make separate",
|
|
1585
|
-
"tool calls one after another. Do NOT put dependent tasks in the same array.",
|
|
1586
|
-
"",
|
|
1587
|
-
'Single: { tasks: [{ agent: "writer", task: "Rewrite README.md" }] }',
|
|
1588
|
-
'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
|
|
1589
|
-
].join("\n"),
|
|
1775
|
+
description: getSubagentsToolDescription(model),
|
|
1590
1776
|
parameters: SubagentParams,
|
|
1591
1777
|
|
|
1592
1778
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -1594,7 +1780,7 @@ keeping their full previous context:
|
|
|
1594
1780
|
recordToolCallStart(toolCallId);
|
|
1595
1781
|
updateLatestBroadcastTargets(undefined);
|
|
1596
1782
|
const discovery = discoverAgents(ctx.cwd, "both");
|
|
1597
|
-
const
|
|
1783
|
+
const agents = filterAgentsForCurrentLayer(discovery.agents, currentDepth, maxDepth);
|
|
1598
1784
|
|
|
1599
1785
|
const makeDetails = makeDetailsFactory(
|
|
1600
1786
|
discovery.projectAgentsDir,
|
|
@@ -1732,7 +1918,8 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1732
1918
|
return {
|
|
1733
1919
|
agent: task.agent,
|
|
1734
1920
|
task: task.task,
|
|
1735
|
-
model:
|
|
1921
|
+
model:
|
|
1922
|
+
formatModelFlag(getParentModelForSubagent(ctx)) ?? agentConfig?.model,
|
|
1736
1923
|
tools: agentConfig?.tools,
|
|
1737
1924
|
sessionDir:
|
|
1738
1925
|
resumePlan?.details?.results[index]?.sessionDir ??
|
|
@@ -1762,11 +1949,9 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1762
1949
|
resumePlan?.details?.results[0],
|
|
1763
1950
|
getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, 0),
|
|
1764
1951
|
!!resumePlan,
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1767
|
-
|
|
1768
|
-
// can change while the parent session is long-running.
|
|
1769
|
-
formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
|
|
1952
|
+
// Normal calls inherit the model that emitted this tool call;
|
|
1953
|
+
// synthetic resume calls recover the preceding real model.
|
|
1954
|
+
formatModelFlag(getParentModelForSubagent(ctx)),
|
|
1770
1955
|
topLevelBaseId,
|
|
1771
1956
|
names[0],
|
|
1772
1957
|
);
|
|
@@ -1782,7 +1967,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1782
1967
|
resumePlan?.details?.results,
|
|
1783
1968
|
(index) => getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, index),
|
|
1784
1969
|
!!resumePlan,
|
|
1785
|
-
formatModelFlag(
|
|
1970
|
+
formatModelFlag(getParentModelForSubagent(ctx)),
|
|
1786
1971
|
topLevelBaseId,
|
|
1787
1972
|
{ names },
|
|
1788
1973
|
);
|
|
@@ -1801,6 +1986,19 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1801
1986
|
renderCall: (args, theme, context) => renderCall(args, theme, context),
|
|
1802
1987
|
renderResult: (result, { expanded }, theme) =>
|
|
1803
1988
|
renderResult(result, expanded, theme),
|
|
1989
|
+
});
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
registerSubagentsTool(latestSessionCtx?.model);
|
|
1993
|
+
pi.on("model_select", (event) => {
|
|
1994
|
+
if (registeredForGpt56 !== isGpt56Model(event.model)) {
|
|
1995
|
+
registerSubagentsTool(event.model);
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
1999
|
+
if (registeredForGpt56 !== isGpt56Model(ctx.model)) {
|
|
2000
|
+
registerSubagentsTool(ctx.model);
|
|
2001
|
+
}
|
|
1804
2002
|
});
|
|
1805
2003
|
|
|
1806
2004
|
if (!resumableSubagentsDisabled()) pi.registerTool({
|
|
@@ -1982,7 +2180,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1982
2180
|
undefined,
|
|
1983
2181
|
(index) => targets[index].sessionDir,
|
|
1984
2182
|
true,
|
|
1985
|
-
formatModelFlag(
|
|
2183
|
+
formatModelFlag(getParentModelForSubagent(ctx)),
|
|
1986
2184
|
topLevelBaseId,
|
|
1987
2185
|
{ names: targets.map((target) => target.name), rawPrompts: true },
|
|
1988
2186
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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/runner.ts
CHANGED
|
@@ -542,6 +542,12 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
542
542
|
// Build pi CLI arguments
|
|
543
543
|
// ---------------------------------------------------------------------------
|
|
544
544
|
|
|
545
|
+
export function resolveSubagentModel(agentModel?: string, currentParentModel?: string): string | undefined {
|
|
546
|
+
// The active parent model is authoritative. Agent frontmatter is retained as
|
|
547
|
+
// a compatibility fallback only for callers that cannot supply live context.
|
|
548
|
+
return currentParentModel ?? agentModel ?? process.env[SUBAGENT_FALLBACK_MODEL_ENV] ?? _inheritedCliArgs.fallbackModel;
|
|
549
|
+
}
|
|
550
|
+
|
|
545
551
|
function buildPiArgs(
|
|
546
552
|
agent: AgentConfig,
|
|
547
553
|
systemPromptPath: string | null,
|
|
@@ -561,8 +567,9 @@ function buildPiArgs(
|
|
|
561
567
|
if (sessionDir) args.push("--session-dir", sessionDir);
|
|
562
568
|
if (resumeSession) args.push("--continue");
|
|
563
569
|
|
|
564
|
-
//
|
|
565
|
-
|
|
570
|
+
// Always use the model active in the parent at launch time. This matters
|
|
571
|
+
// when /model changed after the parent process originally started.
|
|
572
|
+
const model = resolveSubagentModel(agent.model, fallbackModelOverride);
|
|
566
573
|
if (model) args.push("--model", model);
|
|
567
574
|
|
|
568
575
|
const thinking = agent.thinking ?? _inheritedCliArgs.fallbackThinking;
|
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
|
-
}
|