@ferris1225/pi-subagents 4.2.7 → 4.2.12
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/LICENSE +23 -23
- package/README.md +48 -3
- package/agents/executor.md +4 -3
- package/agents/explorer.md +37 -37
- package/package.json +9 -1
- package/src/agents.ts +237 -237
- package/src/announcements.ts +81 -78
- package/src/background.ts +205 -205
- package/src/completion.ts +165 -165
- package/src/config.ts +308 -308
- package/src/dispatch.ts +83 -14
- package/src/format.ts +183 -165
- package/src/index.ts +102 -100
- package/src/models.ts +203 -203
- package/src/monitor.ts +3 -2
- package/src/prompt.ts +2 -1
- package/src/recovery.ts +163 -163
- package/src/runtime.ts +33 -0
- package/src/session-fork.ts +86 -86
- package/src/setup.ts +341 -341
- package/src/spawn.ts +663 -658
- package/src/status.ts +67 -0
- package/src/temp-hygiene.ts +230 -230
- package/src/tools.ts +1 -1
- package/src/ui.ts +248 -248
- package/src/widget.ts +268 -266
- package/src/worktree.ts +974 -943
package/src/index.ts
CHANGED
|
@@ -1,100 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
|
-
*
|
|
4
|
-
* Assembly point: builds the shared runtime and registers everything.
|
|
5
|
-
* The heavy lifting lives in focused modules:
|
|
6
|
-
* - dispatch.ts — tool contract, managed role policy, internal steps
|
|
7
|
-
* - thread-lifecycle.ts — stable generations, controls, final integration/delivery
|
|
8
|
-
* - tools.ts — subagent_control / subagent_stop
|
|
9
|
-
* - announcements.ts — session-start recovery, notices, and widget install
|
|
10
|
-
* - widget.ts — active-only TUI run status
|
|
11
|
-
* - runtime.ts — shared per-session state
|
|
12
|
-
*
|
|
13
|
-
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
14
|
-
* that injects a delegation directive into the parent system prompt so the main
|
|
15
|
-
* model uses the tool proactively.
|
|
16
|
-
*
|
|
17
|
-
* The tool is not registered inside child sub-agent processes, which prevents
|
|
18
|
-
* runaway recursion and keeps child context windows clean.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
23
|
-
import { discoverAgents } from "./agents.ts";
|
|
24
|
-
import { registerAnnouncements } from "./announcements.ts";
|
|
25
|
-
import { getConfigPath, loadConfig } from "./config.ts";
|
|
26
|
-
import { registerSubagentTool } from "./dispatch.ts";
|
|
27
|
-
import { matchRunIds } from "./format.ts";
|
|
28
|
-
import { buildDelegationDirective } from "./prompt.ts";
|
|
29
|
-
import { createRuntime } from "./runtime.ts";
|
|
30
|
-
import { runSetup } from "./setup.ts";
|
|
31
|
-
import { currentSubagentDepth } from "./spawn.ts";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
0,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
pi
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
|
+
*
|
|
4
|
+
* Assembly point: builds the shared runtime and registers everything.
|
|
5
|
+
* The heavy lifting lives in focused modules:
|
|
6
|
+
* - dispatch.ts — tool contract, managed role policy, internal steps
|
|
7
|
+
* - thread-lifecycle.ts — stable generations, controls, final integration/delivery
|
|
8
|
+
* - tools.ts — subagent_control / subagent_stop
|
|
9
|
+
* - announcements.ts — session-start recovery, notices, and widget install
|
|
10
|
+
* - widget.ts — active-only TUI run status
|
|
11
|
+
* - runtime.ts — shared per-session state
|
|
12
|
+
*
|
|
13
|
+
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
14
|
+
* that injects a delegation directive into the parent system prompt so the main
|
|
15
|
+
* model uses the tool proactively.
|
|
16
|
+
*
|
|
17
|
+
* The tool is not registered inside child sub-agent processes, which prevents
|
|
18
|
+
* runaway recursion and keeps child context windows clean.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
23
|
+
import { discoverAgents } from "./agents.ts";
|
|
24
|
+
import { registerAnnouncements } from "./announcements.ts";
|
|
25
|
+
import { getConfigPath, loadConfig } from "./config.ts";
|
|
26
|
+
import { registerSubagentTool } from "./dispatch.ts";
|
|
27
|
+
import { matchRunIds } from "./format.ts";
|
|
28
|
+
import { buildDelegationDirective } from "./prompt.ts";
|
|
29
|
+
import { createRuntime } from "./runtime.ts";
|
|
30
|
+
import { runSetup } from "./setup.ts";
|
|
31
|
+
import { currentSubagentDepth } from "./spawn.ts";
|
|
32
|
+
import { clearActiveRunsStatus } from "./status.ts";
|
|
33
|
+
import { bootstrapDurableState } from "./thread-lifecycle.ts";
|
|
34
|
+
import { registerLookupTools } from "./tools.ts";
|
|
35
|
+
import { clearActiveRunsWidget } from "./widget.ts";
|
|
36
|
+
|
|
37
|
+
export { matchRunIds };
|
|
38
|
+
|
|
39
|
+
export default function (pi: ExtensionAPI): void {
|
|
40
|
+
const configPath = getConfigPath(getAgentDir());
|
|
41
|
+
const runtime = createRuntime(pi, configPath);
|
|
42
|
+
|
|
43
|
+
// Recursion guard: sub-agent children are leaf processes. The `subagent` tool is
|
|
44
|
+
// excluded from their toolset at spawn (--exclude-tools); this check is defense
|
|
45
|
+
// in depth so a child can never expose the tool back to its model, even if
|
|
46
|
+
// another extension ignores the depth marker.
|
|
47
|
+
if (currentSubagentDepth() >= 1) {
|
|
48
|
+
pi.registerCommand("subagents-setup", {
|
|
49
|
+
description: "Configure pi-subagents (unavailable in nested sub-agent processes)",
|
|
50
|
+
handler: async (_args, ctx) => {
|
|
51
|
+
ctx.ui.notify("pi-subagents setup is unavailable in nested sub-agent processes.", "warning");
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pi.registerMessageRenderer("subagent-result", (message, _options, theme) =>
|
|
58
|
+
new Text(
|
|
59
|
+
`${theme.fg("toolTitle", theme.bold("subagent result"))}\n${message.content}`,
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
66
|
+
clearActiveRunsStatus(ctx);
|
|
67
|
+
clearActiveRunsWidget(ctx);
|
|
68
|
+
await runtime.shutdown();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
registerSubagentTool(pi, runtime);
|
|
72
|
+
registerLookupTools(pi, runtime);
|
|
73
|
+
|
|
74
|
+
pi.registerCommand("subagents-setup", {
|
|
75
|
+
description: "Configure pi-subagents: agents, selected models, capability-aware thinking, and runtime settings",
|
|
76
|
+
handler: async (_args, ctx) => {
|
|
77
|
+
await runSetup(ctx, configPath);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
registerAnnouncements(pi, runtime);
|
|
82
|
+
|
|
83
|
+
// Durable bootstrap: restore parked threads from the manifest so a reload or
|
|
84
|
+
// restart keeps status and resume working, then age out old records and sweep
|
|
85
|
+
// leaked temp/state directories. Registration never blocks on it and every
|
|
86
|
+
// stage is best-effort; the restore pass is published as runtime.durableRestore
|
|
87
|
+
// so the tools and the session-start notice wait for it instead of racing it.
|
|
88
|
+
void bootstrapDurableState(runtime);
|
|
89
|
+
|
|
90
|
+
// Proactive dispatch: inject the delegation directive into the parent system prompt.
|
|
91
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
92
|
+
const config = await loadConfig(configPath);
|
|
93
|
+
const { agents } = discoverAgents(ctx.cwd, {
|
|
94
|
+
scope: config.agentScope,
|
|
95
|
+
enabledNames: config.enabledAgents,
|
|
96
|
+
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
97
|
+
});
|
|
98
|
+
const directive = buildDelegationDirective(agents);
|
|
99
|
+
if (!directive) return undefined;
|
|
100
|
+
return { systemPrompt: `${event.systemPrompt}\n${directive}` };
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Model routing, capability-aware thinking, and setup-picker helpers.
|
|
3
|
-
*
|
|
4
|
-
* Runtime has one explicit fallback only: a configured agent model hands
|
|
5
|
-
* off directly to the current main-window model. Setup lists only currently
|
|
6
|
-
* available models and derives thinking choices from Pi's model metadata.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
clampThinkingLevel,
|
|
11
|
-
getSupportedThinkingLevels,
|
|
12
|
-
type Api,
|
|
13
|
-
type Model,
|
|
14
|
-
} from "@earendil-works/pi-ai";
|
|
15
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
17
|
-
|
|
18
|
-
export type ModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> &
|
|
19
|
-
Partial<Pick<ExtensionContext, "scopedModels">>;
|
|
20
|
-
|
|
21
|
-
export const CURRENT_MAIN_MODEL = "__current_main_model__";
|
|
22
|
-
|
|
23
|
-
export interface ModelPickerItem {
|
|
24
|
-
value: string;
|
|
25
|
-
label: string;
|
|
26
|
-
description?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type ModelListEntry = Pick<
|
|
30
|
-
Model<Api>,
|
|
31
|
-
"provider" | "id" | "name" | "input" | "reasoning" | "thinkingLevelMap"
|
|
32
|
-
>;
|
|
33
|
-
|
|
34
|
-
export interface ResolvedAgentModelRoute {
|
|
35
|
-
/** Effective first candidate. Undefined means let Pi use its normal default. */
|
|
36
|
-
primaryRef?: string;
|
|
37
|
-
/** Current main-window model when it differs from the selection. */
|
|
38
|
-
mainFallbackRef?: string;
|
|
39
|
-
/** Runtime order, useful for status/tests. */
|
|
40
|
-
candidateRefs: string[];
|
|
41
|
-
/** Configured ref skipped because Pi does not currently report it available. */
|
|
42
|
-
unavailableSelectedRef?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface AgentModelRouteInput {
|
|
46
|
-
selectedRef?: string;
|
|
47
|
-
mainRef?: string;
|
|
48
|
-
/** When supplied, a configured selection outside this live set is skipped. */
|
|
49
|
-
availableRefs?: readonly string[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function cleanModelRef(ref: string | undefined): string | undefined {
|
|
53
|
-
const trimmed = ref?.trim();
|
|
54
|
-
return trimmed || undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function modelRef(model: { provider: string; id: string }): string {
|
|
58
|
-
return `${model.provider}/${model.id}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function currentModelRef(ctx: Pick<ModelContext, "model">): string | undefined {
|
|
62
|
-
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Current authenticated registry models narrowed by the session scope. Scope
|
|
67
|
-
* entries are a session snapshot, so they act only as a whitelist; the live
|
|
68
|
-
* registry remains the source of truth for availability and model metadata.
|
|
69
|
-
*/
|
|
70
|
-
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
71
|
-
const models = ctx.modelRegistry.getAvailable();
|
|
72
|
-
// scopedModels was added after the original Pi minimum. Treat a missing field
|
|
73
|
-
// exactly like an empty scope and use the full live registry.
|
|
74
|
-
const scopedModels = ctx.scopedModels ?? [];
|
|
75
|
-
if (scopedModels.length === 0) return models;
|
|
76
|
-
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
77
|
-
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function findModelByRef(
|
|
81
|
-
models: readonly Model<Api>[],
|
|
82
|
-
ref: string | undefined,
|
|
83
|
-
): Model<Api> | undefined {
|
|
84
|
-
const normalized = cleanModelRef(ref);
|
|
85
|
-
return normalized ? models.find((model) => modelRef(model) === normalized) : undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Split persisted agent model overrides into the ones Pi still reports as
|
|
89
|
-
* available and the stale ones. Stale refs are dropped at session start (with
|
|
90
|
-
* a user notice) so the config never carries models that can no longer run. */
|
|
91
|
-
export function filterUnavailableModelOverrides(
|
|
92
|
-
agentModels: Record<string, string>,
|
|
93
|
-
models: readonly Model<Api>[],
|
|
94
|
-
): { kept: Record<string, string>; dropped: Array<{ agent: string; ref: string }> } {
|
|
95
|
-
const kept: Record<string, string> = {};
|
|
96
|
-
const dropped: Array<{ agent: string; ref: string }> = [];
|
|
97
|
-
for (const [agent, ref] of Object.entries(agentModels)) {
|
|
98
|
-
if (findModelByRef(models, ref)) kept[agent] = ref;
|
|
99
|
-
else dropped.push({ agent, ref });
|
|
100
|
-
}
|
|
101
|
-
return { kept, dropped };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Resolve one agent's runtime route:
|
|
106
|
-
*
|
|
107
|
-
* configured selection -> current main-window model
|
|
108
|
-
*
|
|
109
|
-
* Without an override the current main model is primary, so an agent never
|
|
110
|
-
* pins its own model. A configured selection that Pi no longer reports as
|
|
111
|
-
* available is skipped immediately instead of spawning a doomed child.
|
|
112
|
-
*/
|
|
113
|
-
export function resolveAgentModelRoute(input: AgentModelRouteInput): ResolvedAgentModelRoute {
|
|
114
|
-
const selectedRef = cleanModelRef(input.selectedRef);
|
|
115
|
-
const mainRef = cleanModelRef(input.mainRef);
|
|
116
|
-
const available = input.availableRefs
|
|
117
|
-
? new Set(input.availableRefs.map((ref) => ref.trim()).filter(Boolean))
|
|
118
|
-
: undefined;
|
|
119
|
-
const selectedAvailable = !selectedRef || !available || available.has(selectedRef);
|
|
120
|
-
const usableSelectedRef = selectedAvailable ? selectedRef : undefined;
|
|
121
|
-
const primaryRef = usableSelectedRef ?? mainRef;
|
|
122
|
-
const ordered = [primaryRef, usableSelectedRef && usableSelectedRef !== mainRef ? mainRef : undefined];
|
|
123
|
-
const candidateRefs = [...new Set(ordered.filter((ref): ref is string => Boolean(ref)))];
|
|
124
|
-
return {
|
|
125
|
-
primaryRef,
|
|
126
|
-
...(candidateRefs[1] ? { mainFallbackRef: candidateRefs[1] } : {}),
|
|
127
|
-
candidateRefs,
|
|
128
|
-
...(!selectedAvailable && selectedRef ? { unavailableSelectedRef: selectedRef } : {}),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The exact levels Pi exposes for this model, including `off` when supported. */
|
|
133
|
-
export function supportedThinkingLevels(model: Model<Api> | undefined): ThinkingLevel[] {
|
|
134
|
-
return model ? (getSupportedThinkingLevels(model) as ThinkingLevel[]) : [];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Clamp an agent preference to the effective model's actual capability map. */
|
|
138
|
-
export function resolveThinkingLevel(
|
|
139
|
-
model: Model<Api> | undefined,
|
|
140
|
-
preferred: ThinkingLevel = DEFAULT_THINKING_LEVEL,
|
|
141
|
-
): ThinkingLevel {
|
|
142
|
-
return model ? (clampThinkingLevel(model, preferred) as ThinkingLevel) : preferred;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function modelCapabilities(model: ModelListEntry): string {
|
|
146
|
-
const input = model.input.includes("image") ? "vision" : "text-only";
|
|
147
|
-
const thinking = getSupportedThinkingLevels(model as Model<Api>).join("/");
|
|
148
|
-
return `${input} · thinking: ${thinking}`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Build one searchable list for agent model selection. Only models Pi
|
|
152
|
-
* currently reports as available are supplied by setup. */
|
|
153
|
-
export function buildModelPickerItems(options: {
|
|
154
|
-
models: readonly ModelListEntry[];
|
|
155
|
-
configuredRef?: string;
|
|
156
|
-
mainRef?: string;
|
|
157
|
-
}): ModelPickerItem[] {
|
|
158
|
-
const configuredRef = cleanModelRef(options.configuredRef);
|
|
159
|
-
const mainRef = cleanModelRef(options.mainRef);
|
|
160
|
-
const byRef = new Map<string, ModelListEntry>();
|
|
161
|
-
for (const model of options.models) {
|
|
162
|
-
const ref = modelRef(model);
|
|
163
|
-
if (!byRef.has(ref)) byRef.set(ref, model);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const refs = [...byRef.keys()]
|
|
167
|
-
.sort((left, right) => {
|
|
168
|
-
const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
|
|
169
|
-
const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
|
|
170
|
-
return leftRank - rightRank || left.localeCompare(right);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const dynamic: ModelPickerItem = {
|
|
174
|
-
value: CURRENT_MAIN_MODEL,
|
|
175
|
-
label: "Current main model (dynamic)",
|
|
176
|
-
description: "Clear agent override; use the current main model dynamically",
|
|
177
|
-
};
|
|
178
|
-
const items: ModelPickerItem[] = [dynamic];
|
|
179
|
-
for (const ref of refs) {
|
|
180
|
-
const model = byRef.get(ref)!;
|
|
181
|
-
const tags = [ref === configuredRef ? "configured" : "", ref === mainRef ? "current main" : ""]
|
|
182
|
-
.filter(Boolean);
|
|
183
|
-
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
184
|
-
items.push({
|
|
185
|
-
value: ref,
|
|
186
|
-
label: ref,
|
|
187
|
-
description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return items;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** The dynamic choice removes the persisted per-agent override. */
|
|
194
|
-
export function applyAgentModelChoice(
|
|
195
|
-
current: Record<string, string>,
|
|
196
|
-
agentName: string,
|
|
197
|
-
choice: string,
|
|
198
|
-
): Record<string, string> {
|
|
199
|
-
const next = { ...current };
|
|
200
|
-
if (choice === CURRENT_MAIN_MODEL) delete next[agentName];
|
|
201
|
-
else next[agentName] = choice.trim();
|
|
202
|
-
return next;
|
|
203
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Model routing, capability-aware thinking, and setup-picker helpers.
|
|
3
|
+
*
|
|
4
|
+
* Runtime has one explicit fallback only: a configured agent model hands
|
|
5
|
+
* off directly to the current main-window model. Setup lists only currently
|
|
6
|
+
* available models and derives thinking choices from Pi's model metadata.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
clampThinkingLevel,
|
|
11
|
+
getSupportedThinkingLevels,
|
|
12
|
+
type Api,
|
|
13
|
+
type Model,
|
|
14
|
+
} from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
17
|
+
|
|
18
|
+
export type ModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> &
|
|
19
|
+
Partial<Pick<ExtensionContext, "scopedModels">>;
|
|
20
|
+
|
|
21
|
+
export const CURRENT_MAIN_MODEL = "__current_main_model__";
|
|
22
|
+
|
|
23
|
+
export interface ModelPickerItem {
|
|
24
|
+
value: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ModelListEntry = Pick<
|
|
30
|
+
Model<Api>,
|
|
31
|
+
"provider" | "id" | "name" | "input" | "reasoning" | "thinkingLevelMap"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export interface ResolvedAgentModelRoute {
|
|
35
|
+
/** Effective first candidate. Undefined means let Pi use its normal default. */
|
|
36
|
+
primaryRef?: string;
|
|
37
|
+
/** Current main-window model when it differs from the selection. */
|
|
38
|
+
mainFallbackRef?: string;
|
|
39
|
+
/** Runtime order, useful for status/tests. */
|
|
40
|
+
candidateRefs: string[];
|
|
41
|
+
/** Configured ref skipped because Pi does not currently report it available. */
|
|
42
|
+
unavailableSelectedRef?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AgentModelRouteInput {
|
|
46
|
+
selectedRef?: string;
|
|
47
|
+
mainRef?: string;
|
|
48
|
+
/** When supplied, a configured selection outside this live set is skipped. */
|
|
49
|
+
availableRefs?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cleanModelRef(ref: string | undefined): string | undefined {
|
|
53
|
+
const trimmed = ref?.trim();
|
|
54
|
+
return trimmed || undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function modelRef(model: { provider: string; id: string }): string {
|
|
58
|
+
return `${model.provider}/${model.id}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function currentModelRef(ctx: Pick<ModelContext, "model">): string | undefined {
|
|
62
|
+
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Current authenticated registry models narrowed by the session scope. Scope
|
|
67
|
+
* entries are a session snapshot, so they act only as a whitelist; the live
|
|
68
|
+
* registry remains the source of truth for availability and model metadata.
|
|
69
|
+
*/
|
|
70
|
+
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
71
|
+
const models = ctx.modelRegistry.getAvailable();
|
|
72
|
+
// scopedModels was added after the original Pi minimum. Treat a missing field
|
|
73
|
+
// exactly like an empty scope and use the full live registry.
|
|
74
|
+
const scopedModels = ctx.scopedModels ?? [];
|
|
75
|
+
if (scopedModels.length === 0) return models;
|
|
76
|
+
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
77
|
+
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function findModelByRef(
|
|
81
|
+
models: readonly Model<Api>[],
|
|
82
|
+
ref: string | undefined,
|
|
83
|
+
): Model<Api> | undefined {
|
|
84
|
+
const normalized = cleanModelRef(ref);
|
|
85
|
+
return normalized ? models.find((model) => modelRef(model) === normalized) : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Split persisted agent model overrides into the ones Pi still reports as
|
|
89
|
+
* available and the stale ones. Stale refs are dropped at session start (with
|
|
90
|
+
* a user notice) so the config never carries models that can no longer run. */
|
|
91
|
+
export function filterUnavailableModelOverrides(
|
|
92
|
+
agentModels: Record<string, string>,
|
|
93
|
+
models: readonly Model<Api>[],
|
|
94
|
+
): { kept: Record<string, string>; dropped: Array<{ agent: string; ref: string }> } {
|
|
95
|
+
const kept: Record<string, string> = {};
|
|
96
|
+
const dropped: Array<{ agent: string; ref: string }> = [];
|
|
97
|
+
for (const [agent, ref] of Object.entries(agentModels)) {
|
|
98
|
+
if (findModelByRef(models, ref)) kept[agent] = ref;
|
|
99
|
+
else dropped.push({ agent, ref });
|
|
100
|
+
}
|
|
101
|
+
return { kept, dropped };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve one agent's runtime route:
|
|
106
|
+
*
|
|
107
|
+
* configured selection -> current main-window model
|
|
108
|
+
*
|
|
109
|
+
* Without an override the current main model is primary, so an agent never
|
|
110
|
+
* pins its own model. A configured selection that Pi no longer reports as
|
|
111
|
+
* available is skipped immediately instead of spawning a doomed child.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveAgentModelRoute(input: AgentModelRouteInput): ResolvedAgentModelRoute {
|
|
114
|
+
const selectedRef = cleanModelRef(input.selectedRef);
|
|
115
|
+
const mainRef = cleanModelRef(input.mainRef);
|
|
116
|
+
const available = input.availableRefs
|
|
117
|
+
? new Set(input.availableRefs.map((ref) => ref.trim()).filter(Boolean))
|
|
118
|
+
: undefined;
|
|
119
|
+
const selectedAvailable = !selectedRef || !available || available.has(selectedRef);
|
|
120
|
+
const usableSelectedRef = selectedAvailable ? selectedRef : undefined;
|
|
121
|
+
const primaryRef = usableSelectedRef ?? mainRef;
|
|
122
|
+
const ordered = [primaryRef, usableSelectedRef && usableSelectedRef !== mainRef ? mainRef : undefined];
|
|
123
|
+
const candidateRefs = [...new Set(ordered.filter((ref): ref is string => Boolean(ref)))];
|
|
124
|
+
return {
|
|
125
|
+
primaryRef,
|
|
126
|
+
...(candidateRefs[1] ? { mainFallbackRef: candidateRefs[1] } : {}),
|
|
127
|
+
candidateRefs,
|
|
128
|
+
...(!selectedAvailable && selectedRef ? { unavailableSelectedRef: selectedRef } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The exact levels Pi exposes for this model, including `off` when supported. */
|
|
133
|
+
export function supportedThinkingLevels(model: Model<Api> | undefined): ThinkingLevel[] {
|
|
134
|
+
return model ? (getSupportedThinkingLevels(model) as ThinkingLevel[]) : [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Clamp an agent preference to the effective model's actual capability map. */
|
|
138
|
+
export function resolveThinkingLevel(
|
|
139
|
+
model: Model<Api> | undefined,
|
|
140
|
+
preferred: ThinkingLevel = DEFAULT_THINKING_LEVEL,
|
|
141
|
+
): ThinkingLevel {
|
|
142
|
+
return model ? (clampThinkingLevel(model, preferred) as ThinkingLevel) : preferred;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function modelCapabilities(model: ModelListEntry): string {
|
|
146
|
+
const input = model.input.includes("image") ? "vision" : "text-only";
|
|
147
|
+
const thinking = getSupportedThinkingLevels(model as Model<Api>).join("/");
|
|
148
|
+
return `${input} · thinking: ${thinking}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Build one searchable list for agent model selection. Only models Pi
|
|
152
|
+
* currently reports as available are supplied by setup. */
|
|
153
|
+
export function buildModelPickerItems(options: {
|
|
154
|
+
models: readonly ModelListEntry[];
|
|
155
|
+
configuredRef?: string;
|
|
156
|
+
mainRef?: string;
|
|
157
|
+
}): ModelPickerItem[] {
|
|
158
|
+
const configuredRef = cleanModelRef(options.configuredRef);
|
|
159
|
+
const mainRef = cleanModelRef(options.mainRef);
|
|
160
|
+
const byRef = new Map<string, ModelListEntry>();
|
|
161
|
+
for (const model of options.models) {
|
|
162
|
+
const ref = modelRef(model);
|
|
163
|
+
if (!byRef.has(ref)) byRef.set(ref, model);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const refs = [...byRef.keys()]
|
|
167
|
+
.sort((left, right) => {
|
|
168
|
+
const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
|
|
169
|
+
const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
|
|
170
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const dynamic: ModelPickerItem = {
|
|
174
|
+
value: CURRENT_MAIN_MODEL,
|
|
175
|
+
label: "Current main model (dynamic)",
|
|
176
|
+
description: "Clear agent override; use the current main model dynamically",
|
|
177
|
+
};
|
|
178
|
+
const items: ModelPickerItem[] = [dynamic];
|
|
179
|
+
for (const ref of refs) {
|
|
180
|
+
const model = byRef.get(ref)!;
|
|
181
|
+
const tags = [ref === configuredRef ? "configured" : "", ref === mainRef ? "current main" : ""]
|
|
182
|
+
.filter(Boolean);
|
|
183
|
+
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
184
|
+
items.push({
|
|
185
|
+
value: ref,
|
|
186
|
+
label: ref,
|
|
187
|
+
description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return items;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The dynamic choice removes the persisted per-agent override. */
|
|
194
|
+
export function applyAgentModelChoice(
|
|
195
|
+
current: Record<string, string>,
|
|
196
|
+
agentName: string,
|
|
197
|
+
choice: string,
|
|
198
|
+
): Record<string, string> {
|
|
199
|
+
const next = { ...current };
|
|
200
|
+
if (choice === CURRENT_MAIN_MODEL) delete next[agentName];
|
|
201
|
+
else next[agentName] = choice.trim();
|
|
202
|
+
return next;
|
|
203
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The store notifies wait/status consumers on every mutation. Each run carries
|
|
6
6
|
* timing information plus a concise activity string ("thinking",
|
|
7
|
-
* "read src/index.ts", ...).
|
|
8
|
-
*
|
|
7
|
+
* "read src/index.ts", ...). Settled rows stay until the next beginTurn so the
|
|
8
|
+
* footer can count them beside live siblings; the widget ignores them. Tool
|
|
9
|
+
* results are the durable user-facing record.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { stripVTControlCharacters } from "node:util";
|
package/src/prompt.ts
CHANGED
|
@@ -35,12 +35,13 @@ export function buildDelegationDirective(
|
|
|
35
35
|
"`executor`: brief it as the edit authorization. For cleanup, name the scope (uncommitted diff, Git range, directory) — every safe proven cut applies without per-item approval; finding no safe cut is a valid result. After a wide fan-out, pass the result-artifact paths to one executor and read its merged brief instead of every result yourself.",
|
|
36
36
|
]
|
|
37
37
|
: []),
|
|
38
|
+
"A discovered defect is not a change: re-read the current code and confirm it is not a false positive before you edit or brief a writer to edit.",
|
|
38
39
|
"Parallelize by default: map the todo list onto ONE `tasks` dispatch. One child owns one deliverable and its files; only genuinely dependent work waits for its prerequisite.",
|
|
39
40
|
"Brief each child completely — goal, exact paths, constraints, expected output; it has no conversation memory and cannot delegate. Resume parked threads with `subagent_control resume`.",
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
const handoffRules = [
|
|
43
|
-
"Dispatch never blocks or ends your turn — keep working
|
|
44
|
+
"Dispatch never blocks or ends your turn — keep working, but only on what the children are not: never re-read a scope you just delegated. Each completion resumes you automatically; never sleep or poll for it.",
|
|
44
45
|
"Results are already shown; add only your conclusion or next action, never a restatement.",
|
|
45
46
|
"Never declare the overall task done while a dispatched run is still active.",
|
|
46
47
|
];
|