@ferris1225/pi-subagents 0.31.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -74
- package/package.json +2 -2
- package/src/agents.ts +8 -13
- package/src/announcements.ts +59 -0
- package/src/background.ts +59 -6
- package/src/config.ts +14 -2
- package/src/dispatch.ts +1833 -845
- package/src/fixloop.ts +1 -1
- package/src/format.ts +30 -4
- package/src/index.ts +9 -10
- package/src/models.ts +184 -54
- package/src/monitor.ts +141 -93
- package/src/prompt.ts +3 -2
- package/src/recovery.ts +145 -0
- package/src/rpc-run.ts +991 -0
- package/src/runtime.ts +284 -145
- package/src/session-fork.ts +84 -0
- package/src/setup.ts +271 -184
- package/src/spawn.ts +557 -977
- package/src/tools.ts +730 -409
- package/src/trajectory.ts +312 -0
- package/src/ui.ts +32 -16
- package/src/worktree.ts +687 -0
- package/src/widget.ts +0 -182
package/src/fixloop.ts
CHANGED
|
@@ -87,7 +87,7 @@ export function chainKeyFragments(result: SingleResult): string[] {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Compact one-line outcome for a finished chain run,
|
|
90
|
+
* Compact one-line outcome for a finished chain run, retained so
|
|
91
91
|
* each round reads as what it did: a reviewer reports its verdict plus the
|
|
92
92
|
* key fragments of what it found ("fail · src/index.ts · render()"), a worker
|
|
93
93
|
* the fragments of what it changed. Failed runs and runs with nothing
|
package/src/format.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function formatCompletionBlock(result: SingleResult, maxResultLines: numb
|
|
|
90
90
|
const output = getResultOutput(result);
|
|
91
91
|
const { text, truncated } = truncateResultOutput(output, maxResultLines);
|
|
92
92
|
const fallbackNote = result.modelFallbackFrom
|
|
93
|
-
? ` (model
|
|
93
|
+
? ` (model pool fallback: primary ${result.modelFallbackFrom} → final ${result.model ?? "dynamic default"})`
|
|
94
94
|
: "";
|
|
95
95
|
const startupRetryNote = result.startupRetries
|
|
96
96
|
? ` (recovered after ${result.startupRetries} startup retr${result.startupRetries === 1 ? "y" : "ies"} — concurrent pi startup race)`
|
|
@@ -98,7 +98,33 @@ export function formatCompletionBlock(result: SingleResult, maxResultLines: numb
|
|
|
98
98
|
const modelRetryNote = result.modelRetries
|
|
99
99
|
? ` (recovered after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on a transient provider error)`
|
|
100
100
|
: "";
|
|
101
|
-
const
|
|
101
|
+
const relations = [
|
|
102
|
+
result.forkedFromRunId !== undefined ? `forked from #${result.forkedFromRunId}` : undefined,
|
|
103
|
+
(result.forkChildRunIds?.length ?? 0) > 0 ? `fork children ${result.forkChildRunIds!.map((id) => `#${id}`).join(", ")}` : undefined,
|
|
104
|
+
].filter((value): value is string => Boolean(value));
|
|
105
|
+
const relationNote = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
106
|
+
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
107
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${modelRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
|
|
108
|
+
if (result.isolation === "worktree") {
|
|
109
|
+
const isolation =
|
|
110
|
+
result.integrationStatus === "integrated"
|
|
111
|
+
? "worktree · changes integrated into the original working tree"
|
|
112
|
+
: result.integrationStatus === "no_changes"
|
|
113
|
+
? "worktree · no changes; temporary worktree removed"
|
|
114
|
+
: result.integrationStatus === "retained"
|
|
115
|
+
? result.integrationApplied
|
|
116
|
+
? "worktree · changes applied, but cleanup failed; recovery artifacts retained"
|
|
117
|
+
: "worktree · integration failed; recovery artifacts retained"
|
|
118
|
+
: "worktree · isolated";
|
|
119
|
+
lines.push(`Isolation: ${isolation}${relationNote}`);
|
|
120
|
+
if (result.integrationWorktreePath) lines.push(`Retained worktree: ${result.integrationWorktreePath}`);
|
|
121
|
+
if (result.integrationPatchPath) lines.push(`Retained patch: ${result.integrationPatchPath}`);
|
|
122
|
+
if (result.integrationError) lines.push(`Integration error: ${result.integrationError}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
} else if (relations.length > 0) {
|
|
125
|
+
lines.push(`Relation: ${relations.join(" · ")}`, "");
|
|
126
|
+
}
|
|
127
|
+
lines.push(text);
|
|
102
128
|
// A run can exit cleanly while its last tools failed (e.g. a build that broke):
|
|
103
129
|
// the final text alone may claim more than the tools achieved, so surface the
|
|
104
130
|
// failures explicitly and tell the main agent to verify before relying on it.
|
|
@@ -130,10 +156,10 @@ export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: nu
|
|
|
130
156
|
const sameModel = result.modelRetries
|
|
131
157
|
? `, after ${result.modelRetries} same-model retr${result.modelRetries === 1 ? "y" : "ies"} on transient errors`
|
|
132
158
|
: "";
|
|
133
|
-
const retry = result.modelFallbackFrom ? ", and the
|
|
159
|
+
const retry = result.modelFallbackFrom ? ", and the configured backup chain also failed" : "";
|
|
134
160
|
const sessionPreserved = Boolean(result.sessionDir && result.sessionId) && opts?.runId !== undefined;
|
|
135
161
|
const recovery = sessionPreserved
|
|
136
|
-
? ` The sub-agent's earlier work in this run is preserved. Once a model is available again, call
|
|
162
|
+
? ` The sub-agent's earlier work in this run is preserved. Once a model is available again, call subagent_control with { action: "resume", id: ${opts!.runId} } to CONTINUE it in-context (it keeps the same run id and does not re-scan), or execute the task in the main window with your own tools.`
|
|
137
163
|
: ` Please execute this task in the main window with your own tools; do not re-dispatch it as a sub-agent.`;
|
|
138
164
|
return `The sub-agent could not complete this task: its model was unavailable or failed (or the run stalled)${sameModel}${retry}.${recovery}`;
|
|
139
165
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Assembly point: builds the shared runtime and registers everything.
|
|
5
5
|
* The heavy lifting lives in focused modules:
|
|
6
6
|
* - dispatch.ts — the `subagent` tool (spawn, auto-fix chain, vision model)
|
|
7
|
-
* - tools.ts — subagent_wait /
|
|
8
|
-
* -
|
|
9
|
-
* - runtime.ts
|
|
7
|
+
* - tools.ts — subagent_control / subagent_wait / status / stop
|
|
8
|
+
* - announcements.ts — session-start recovery and feature notices
|
|
9
|
+
* - runtime.ts — shared per-session state
|
|
10
10
|
*
|
|
11
11
|
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
12
12
|
* that injects a delegation directive into the parent system prompt so the main
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Text } from "@earendil-works/pi-tui";
|
|
21
21
|
import { discoverAgents } from "./agents.ts";
|
|
22
|
+
import { registerAnnouncements } from "./announcements.ts";
|
|
22
23
|
import { getConfigPath, loadConfig } from "./config.ts";
|
|
23
24
|
import { registerSubagentTool } from "./dispatch.ts";
|
|
24
25
|
import { matchRunIds } from "./format.ts";
|
|
@@ -27,7 +28,6 @@ import { createRuntime } from "./runtime.ts";
|
|
|
27
28
|
import { runSetup } from "./setup.ts";
|
|
28
29
|
import { currentSubagentDepth } from "./spawn.ts";
|
|
29
30
|
import { registerLookupTools } from "./tools.ts";
|
|
30
|
-
import { registerWidget } from "./widget.ts";
|
|
31
31
|
|
|
32
32
|
export { matchRunIds };
|
|
33
33
|
|
|
@@ -57,23 +57,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
57
57
|
),
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
pi.on("session_shutdown", () => {
|
|
61
|
-
runtime.shutdown();
|
|
60
|
+
pi.on("session_shutdown", async () => {
|
|
61
|
+
await runtime.shutdown();
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
registerSubagentTool(pi, runtime);
|
|
65
65
|
registerLookupTools(pi, runtime);
|
|
66
66
|
|
|
67
67
|
pi.registerCommand("subagents-setup", {
|
|
68
|
-
description: "Configure pi-subagents:
|
|
68
|
+
description: "Configure pi-subagents: enabled agents, primary/backup model pools, and runtime settings",
|
|
69
69
|
handler: async (_args, ctx) => {
|
|
70
70
|
await runSetup(ctx, configPath);
|
|
71
71
|
},
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
// one-time feature announcements after updates.
|
|
76
|
-
registerWidget(pi, runtime);
|
|
74
|
+
registerAnnouncements(pi, runtime);
|
|
77
75
|
|
|
78
76
|
// Proactive dispatch: inject the delegation directive into the parent system prompt.
|
|
79
77
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -82,6 +80,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
82
80
|
const { agents } = discoverAgents(ctx.cwd, {
|
|
83
81
|
scope: config.agentScope,
|
|
84
82
|
enabledNames: config.enabledAgents,
|
|
83
|
+
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
85
84
|
});
|
|
86
85
|
const directive = buildDelegationDirective(agents);
|
|
87
86
|
if (!directive) return undefined;
|
package/src/models.ts
CHANGED
|
@@ -1,82 +1,212 @@
|
|
|
1
|
-
|
|
2
|
-
* Model
|
|
1
|
+
/*
|
|
2
|
+
* Model-pool resolution and setup-picker helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Runtime pools deliberately do not filter configured references by current
|
|
5
|
+
* availability: a stale primary/backup is attempted and normal provider/model
|
|
6
|
+
* failure handling advances to the next candidate. Setup uses the same catalog
|
|
7
|
+
* only for honest availability labels; it never rewrites persisted choices.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
10
11
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
|
|
12
|
-
export type ModelContext = Pick<ExtensionContext, "model" | "
|
|
13
|
+
export type ModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> &
|
|
14
|
+
Partial<Pick<ExtensionContext, "scopedModels">>;
|
|
15
|
+
|
|
16
|
+
export const CURRENT_MAIN_MODEL = "__current_main_model__";
|
|
17
|
+
|
|
18
|
+
export type ModelPoolSlot = "primary" | "backup";
|
|
19
|
+
export type ModelPickerSlot = ModelPoolSlot | "vision";
|
|
20
|
+
|
|
21
|
+
export interface ModelPickerItem {
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Visible for diagnosis/search, but cannot be selected. */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ModelListEntry = Pick<
|
|
30
|
+
Model<Api>,
|
|
31
|
+
"provider" | "id" | "name" | "input" | "reasoning"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export interface ResolvedAgentModelPool {
|
|
35
|
+
/** Effective first candidate. Undefined means let pi use its normal default. */
|
|
36
|
+
primaryRef?: string;
|
|
37
|
+
/** Ordered candidates after the primary, already deduplicated. */
|
|
38
|
+
fallbackModelRefs: string[];
|
|
39
|
+
/** All known references in runtime order, useful for tests/inspection. */
|
|
40
|
+
candidateRefs: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AgentModelPoolInput {
|
|
44
|
+
primaryRef?: string;
|
|
45
|
+
backupRef?: string;
|
|
46
|
+
mainRef?: string;
|
|
47
|
+
declaredDefaultRef?: string;
|
|
48
|
+
}
|
|
13
49
|
|
|
14
|
-
export interface
|
|
50
|
+
export interface AgentModelPoolMaps {
|
|
15
51
|
agentModels: Record<string, string>;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
52
|
+
agentBackupModels: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AgentModelPoolRow {
|
|
56
|
+
name: string;
|
|
57
|
+
primary: string;
|
|
58
|
+
backup: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function cleanModelRef(ref: string | undefined): string | undefined {
|
|
62
|
+
const trimmed = ref?.trim();
|
|
63
|
+
return trimmed || undefined;
|
|
20
64
|
}
|
|
21
65
|
|
|
22
66
|
export function modelRef(model: { provider: string; id: string }): string {
|
|
23
67
|
return `${model.provider}/${model.id}`;
|
|
24
68
|
}
|
|
25
69
|
|
|
70
|
+
export function currentModelRef(ctx: Pick<ModelContext, "model">): string | undefined {
|
|
71
|
+
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
75
|
+
* Current authenticated registry models narrowed by the session scope. Scope
|
|
76
|
+
* entries are a session snapshot, so they act only as a whitelist; the live
|
|
77
|
+
* registry remains the source of truth for availability and model metadata.
|
|
29
78
|
*/
|
|
79
|
+
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
80
|
+
const models = ctx.modelRegistry.getAvailable();
|
|
81
|
+
// scopedModels was added after the declared Pi 0.80.6 minimum. Treat a
|
|
82
|
+
// missing field exactly like an empty scope and use the full live registry.
|
|
83
|
+
const scopedModels = ctx.scopedModels ?? [];
|
|
84
|
+
if (scopedModels.length === 0) return models;
|
|
85
|
+
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
86
|
+
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Model refs usable by setup, with an available current main model first. */
|
|
30
90
|
export function availableModelRefs(ctx: ModelContext): string[] {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const currentRef = ctx.model ? modelRef(ctx.model) : undefined;
|
|
35
|
-
if (!currentRef) return refs;
|
|
91
|
+
const refs = [...new Set(availableModelsInScope(ctx).map(modelRef))];
|
|
92
|
+
const currentRef = currentModelRef(ctx);
|
|
93
|
+
if (!currentRef || !refs.includes(currentRef)) return refs;
|
|
36
94
|
return [currentRef, ...refs.filter((ref) => ref !== currentRef)];
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
/**
|
|
40
|
-
* Resolve
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
98
|
+
* Resolve one agent's ordered runtime pool:
|
|
99
|
+
*
|
|
100
|
+
* configured primary -> configured backup -> current main-window model
|
|
101
|
+
*
|
|
102
|
+
* Without a primary override, the current main model remains the primary; an
|
|
103
|
+
* agent-declared default is used only when no main model exists. Equal refs are
|
|
104
|
+
* removed without consulting availability, so stale refs stay in the chain and
|
|
105
|
+
* fail normally at runtime instead of being silently repaired.
|
|
45
106
|
*/
|
|
46
|
-
export function
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
107
|
+
export function resolveAgentModelPool(input: AgentModelPoolInput): ResolvedAgentModelPool {
|
|
108
|
+
const mainRef = cleanModelRef(input.mainRef);
|
|
109
|
+
const primaryRef = cleanModelRef(input.primaryRef) ?? mainRef ?? cleanModelRef(input.declaredDefaultRef);
|
|
110
|
+
const ordered = [primaryRef, cleanModelRef(input.backupRef), mainRef];
|
|
111
|
+
const seen = new Set<string>();
|
|
112
|
+
const candidateRefs: string[] = [];
|
|
113
|
+
for (const ref of ordered) {
|
|
114
|
+
if (!ref || seen.has(ref)) continue;
|
|
115
|
+
seen.add(ref);
|
|
116
|
+
candidateRefs.push(ref);
|
|
117
|
+
}
|
|
118
|
+
const fallbackModelRefs = candidateRefs.filter((ref) => ref !== primaryRef);
|
|
119
|
+
return { primaryRef, fallbackModelRefs, candidateRefs };
|
|
50
120
|
}
|
|
51
121
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
function modelCapabilities(model: ModelListEntry): string {
|
|
123
|
+
const capabilities = [model.input.includes("image") ? "vision" : "text-only"];
|
|
124
|
+
if (model.reasoning) capabilities.push("reasoning");
|
|
125
|
+
return capabilities.join(" + ");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Build the single searchable model list shared by primary/backup/vision picks.
|
|
129
|
+
* Only refs Pi currently reports as available are shown, which means providers
|
|
130
|
+
* without a configured API key/OAuth session never flood the setup picker. */
|
|
131
|
+
export function buildModelPickerItems(options: {
|
|
132
|
+
models: readonly ModelListEntry[];
|
|
133
|
+
availableRefs: readonly string[];
|
|
134
|
+
slot: ModelPickerSlot;
|
|
135
|
+
configuredRef?: string;
|
|
136
|
+
mainRef?: string;
|
|
137
|
+
}): ModelPickerItem[] {
|
|
138
|
+
const configuredRef = cleanModelRef(options.configuredRef);
|
|
139
|
+
const mainRef = cleanModelRef(options.mainRef);
|
|
140
|
+
const available = new Set(options.availableRefs.map((ref) => ref.trim()));
|
|
141
|
+
const byRef = new Map<string, ModelListEntry>();
|
|
142
|
+
for (const model of options.models) {
|
|
143
|
+
const ref = modelRef(model);
|
|
144
|
+
if (available.has(ref) && !byRef.has(ref)) byRef.set(ref, model);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const refs = [...byRef.keys()]
|
|
148
|
+
.filter((ref) => options.slot !== "vision" || byRef.get(ref)?.input.includes("image") === true)
|
|
149
|
+
.sort((left, right) => {
|
|
150
|
+
const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
|
|
151
|
+
const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
|
|
152
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
153
|
+
});
|
|
71
154
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
removed++;
|
|
155
|
+
const dynamic = options.slot === "backup"
|
|
156
|
+
? {
|
|
157
|
+
value: CURRENT_MAIN_MODEL,
|
|
158
|
+
label: "Current main model (default)",
|
|
159
|
+
description: "Clear configured backup; use the main-window model dynamically",
|
|
78
160
|
}
|
|
161
|
+
: {
|
|
162
|
+
value: CURRENT_MAIN_MODEL,
|
|
163
|
+
label: "Current main model (dynamic)",
|
|
164
|
+
description: options.slot === "vision"
|
|
165
|
+
? "Clear vision override; use the main-window model for vision tasks"
|
|
166
|
+
: "Clear primary override; use the main-window model dynamically",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const items: ModelPickerItem[] = [dynamic];
|
|
170
|
+
for (const ref of refs) {
|
|
171
|
+
const model = byRef.get(ref)!;
|
|
172
|
+
const tags = ["available"];
|
|
173
|
+
if (ref === configuredRef) tags.push("configured");
|
|
174
|
+
if (ref === mainRef) tags.push("current main");
|
|
175
|
+
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
176
|
+
items.push({
|
|
177
|
+
value: ref,
|
|
178
|
+
label: ref,
|
|
179
|
+
description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
|
|
180
|
+
});
|
|
79
181
|
}
|
|
182
|
+
return items;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Pure pool update: the dynamic/default choice removes the persisted override. */
|
|
186
|
+
export function applyModelPoolChoice(
|
|
187
|
+
current: AgentModelPoolMaps,
|
|
188
|
+
agentName: string,
|
|
189
|
+
slot: ModelPoolSlot,
|
|
190
|
+
choice: string,
|
|
191
|
+
): AgentModelPoolMaps {
|
|
192
|
+
const next: AgentModelPoolMaps = {
|
|
193
|
+
agentModels: { ...current.agentModels },
|
|
194
|
+
agentBackupModels: { ...current.agentBackupModels },
|
|
195
|
+
};
|
|
196
|
+
const target = slot === "primary" ? next.agentModels : next.agentBackupModels;
|
|
197
|
+
if (choice === CURRENT_MAIN_MODEL) delete target[agentName];
|
|
198
|
+
else target[agentName] = choice.trim();
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
80
201
|
|
|
81
|
-
|
|
202
|
+
/** Pure rows used by the overview component and focused helper tests. */
|
|
203
|
+
export function buildAgentModelPoolRows(
|
|
204
|
+
agentNames: readonly string[],
|
|
205
|
+
pools: AgentModelPoolMaps,
|
|
206
|
+
): AgentModelPoolRow[] {
|
|
207
|
+
return agentNames.map((name) => ({
|
|
208
|
+
name,
|
|
209
|
+
primary: pools.agentModels[name] ?? "Main (dynamic)",
|
|
210
|
+
backup: pools.agentBackupModels[name] ?? "Main (default)",
|
|
211
|
+
}));
|
|
82
212
|
}
|