@a-t-h-i/bot-lobby 0.6.6 → 0.6.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 +56 -1
- package/package.json +1 -1
- package/prompts/master.md +6 -0
- package/src/ask/dialog.ts +4 -2
- package/src/ask/state.ts +10 -1
- package/src/ask/tool.ts +6 -1
- package/src/ask/view.ts +2 -1
- package/src/execution/agent-runner.ts +23 -1
- package/src/execution/fallback.ts +75 -0
- package/src/lobby/feed.ts +7 -0
- package/src/lobby/layout.ts +22 -1
- package/src/lobby/mini.ts +159 -0
- package/src/lobby/planner.ts +10 -3
- package/src/lobby/quickfix.ts +17 -3
- package/src/lobby/runtime.ts +27 -4
- package/src/lobby/view.ts +22 -7
- package/src/master/master.ts +1 -1
- package/src/master/research.ts +1 -0
- package/src/pi/events.ts +2 -0
- package/src/pi/master-fallback.ts +61 -0
- package/src/pi/model-support.ts +18 -3
- package/src/pi/settings-ui.ts +63 -10
- package/src/schemas/configuration.ts +43 -9
- package/src/schemas/findings.ts +2 -0
- package/src/workflow/workflow.ts +12 -2
package/src/lobby/quickfix.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and the lobby feed, and every finished job lands in the metrics log.
|
|
7
7
|
*/
|
|
8
8
|
import { loadPrompt } from "../prompts/loader.ts";
|
|
9
|
+
import { withFallback } from "../execution/fallback.ts";
|
|
9
10
|
import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
10
11
|
import { describeToolCall } from "../pi/activity.ts";
|
|
11
12
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
@@ -74,6 +75,8 @@ export interface QuickFixProfile {
|
|
|
74
75
|
thinking: string;
|
|
75
76
|
timeoutMs: number;
|
|
76
77
|
instructions?: string;
|
|
78
|
+
/** Where the job goes when its model runs out of usage or is unavailable. */
|
|
79
|
+
fallback?: { model: string; thinking: string };
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export interface QuickFixDeps {
|
|
@@ -260,6 +263,17 @@ export class QuickFixQueue {
|
|
|
260
263
|
},
|
|
261
264
|
this.deps.runProcess ?? spawnPiProcess,
|
|
262
265
|
);
|
|
266
|
+
// A model that is out of usage hands the job to the fallback the settings name.
|
|
267
|
+
const attemptOrFallback = async (model: string | undefined, thinking: string) => {
|
|
268
|
+
const { result: outcome, switchedFrom } = await withFallback(model, thinking, profile.fallback, attempt);
|
|
269
|
+
if (switchedFrom && profile.fallback) {
|
|
270
|
+
job.model = profile.fallback.model;
|
|
271
|
+
job.thinking = profile.fallback.thinking;
|
|
272
|
+
this.addStep(job, `${switchedFrom} is out of usage or unavailable; ran on ${profile.fallback.model} (${profile.fallback.thinking})`, Date.now());
|
|
273
|
+
this.changed();
|
|
274
|
+
}
|
|
275
|
+
return outcome;
|
|
276
|
+
};
|
|
263
277
|
let result;
|
|
264
278
|
if (route) {
|
|
265
279
|
job.route = routeLabel(route);
|
|
@@ -267,7 +281,7 @@ export class QuickFixQueue {
|
|
|
267
281
|
job.thinking = route.thinking;
|
|
268
282
|
if (route.model) job.model = route.model;
|
|
269
283
|
this.changed();
|
|
270
|
-
result = await
|
|
284
|
+
result = await attemptOrFallback(route.model, route.thinking);
|
|
271
285
|
if (fellShort(result) && !controller.signal.aborted) {
|
|
272
286
|
// The routed attempt counts in the metrics on its own; the job re-runs on the configured profile.
|
|
273
287
|
appendMetrics(this.deps.root, this.deps.configDir, [{ ...quickFixMetric({ ...job, status: result.status, finishedAt: Date.now(), usage: result.usage, ...(result.model ? { model: result.model } : {}) }), id: `${job.id}-routed-${job.startedAt}` }]);
|
|
@@ -276,10 +290,10 @@ export class QuickFixQueue {
|
|
|
276
290
|
delete job.routedFrom;
|
|
277
291
|
job.thinking = profile.thinking;
|
|
278
292
|
if (profile.model) job.model = profile.model;
|
|
279
|
-
result = await
|
|
293
|
+
result = await attemptOrFallback(profile.model, profile.thinking);
|
|
280
294
|
}
|
|
281
295
|
} else {
|
|
282
|
-
result = await
|
|
296
|
+
result = await attemptOrFallback(profile.model, profile.thinking);
|
|
283
297
|
}
|
|
284
298
|
job.status = result.status;
|
|
285
299
|
job.report = result.output.trim() || undefined;
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -35,7 +35,8 @@ import { checkThinking } from "../pi/model-support.ts";
|
|
|
35
35
|
import { launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
36
36
|
import { SessionChats } from "./session-files.ts";
|
|
37
37
|
import { answerMessage, askUser, questionnaires, type Asker } from "./ask.ts";
|
|
38
|
-
import { QuickFixQueue } from "./quickfix.ts";
|
|
38
|
+
import { jobTitle, QuickFixQueue } from "./quickfix.ts";
|
|
39
|
+
import { miniLine, type MiniInput } from "./mini.ts";
|
|
39
40
|
import { PlanningSession, type PlannerSeed } from "./planner.ts";
|
|
40
41
|
import { execCommand, IssuesState } from "./issues.ts";
|
|
41
42
|
import { LobbyView, type LiveSession, type LobbyHost, type SwitchTarget, type TabId } from "./view.ts";
|
|
@@ -94,18 +95,39 @@ let frameTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
94
95
|
* through pi.
|
|
95
96
|
*/
|
|
96
97
|
function rerender(): void {
|
|
97
|
-
|
|
98
|
+
// Hidden, only the one-line status under the editor changes.
|
|
99
|
+
if (!runtime || (!runtime.visible && !miniShown(runtime)) || frameTimer) return;
|
|
98
100
|
const wait = lastFrame + FRAME_MS - Date.now();
|
|
99
101
|
const paint = () => {
|
|
100
102
|
frameTimer = undefined;
|
|
101
103
|
lastFrame = Date.now();
|
|
102
|
-
if (runtime?.visible) runtime.tui?.requestRender();
|
|
104
|
+
if (runtime?.visible || (runtime && miniShown(runtime))) runtime.tui?.requestRender();
|
|
103
105
|
};
|
|
104
106
|
if (wait <= 0) return paint();
|
|
105
107
|
frameTimer = setTimeout(paint, wait);
|
|
106
108
|
frameTimer.unref?.();
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/** The status line shows under the editor while the lobby is hidden (not while bot-lobby is minimized), unless settings turn it off. */
|
|
112
|
+
function miniShown(state: Runtime): boolean {
|
|
113
|
+
return !state.visible && !isMinimized() && loadConfig().lobby.miniLine;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** What the status line shows: the task, the planning round, the quick fix in hand. */
|
|
117
|
+
function miniInput(state: Runtime): MiniInput {
|
|
118
|
+
const snapshot = taskSnapshot();
|
|
119
|
+
const planner = state.planner;
|
|
120
|
+
const job = state.quickfix.running ?? state.quickfix.jobs.find((entry) => entry.status === "queued");
|
|
121
|
+
const queued = state.quickfix.jobs.filter((entry) => entry.status === "queued" && entry !== job).length;
|
|
122
|
+
return {
|
|
123
|
+
...(snapshot.task ? { task: snapshot.task } : {}),
|
|
124
|
+
runs: snapshot.runs,
|
|
125
|
+
...(planner ? { planning: { busy: planner.busy, round: planner.turns, limit: loadConfig().lobby.maxPlanningRounds, questions: planner.awaitingAnswers ? planner.questions.length : 0, ready: planner.reply?.status === "ready", saved: Boolean(planner.saved) } } : {}),
|
|
126
|
+
...(job ? { quickfix: { title: jobTitle(job), running: job.status === "running", queued } } : {}),
|
|
127
|
+
key: "Alt+L",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
function sessionModel(ctx: ExtensionContext): string | undefined {
|
|
110
132
|
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
111
133
|
}
|
|
@@ -769,7 +791,8 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
769
791
|
// Capture pi's TUI through a zero-line widget below the editor.
|
|
770
792
|
ctx.ui.setWidget(ANCHOR_KEY, (tui) => {
|
|
771
793
|
state.tui = tui;
|
|
772
|
-
|
|
794
|
+
// Zero lines while the lobby is up (or bot-lobby is minimized); one status line when it is hidden.
|
|
795
|
+
return { render: (width: number) => (miniShown(state) ? [miniLine(miniInput(state), width, lobbyTheme(state.ctx.ui.theme))] : []), invalidate: () => {} };
|
|
773
796
|
}, { placement: "belowEditor" });
|
|
774
797
|
// The owner's clock (pi/owner.ts) delivers comments and messages and drives auto mode; the lobby logs what it did.
|
|
775
798
|
onOwnerEvent((event) => {
|
package/src/lobby/view.ts
CHANGED
|
@@ -22,8 +22,9 @@ import type { BackgroundSession } from "./sessions.ts";
|
|
|
22
22
|
import type { QuickFixQueue } from "./quickfix.ts";
|
|
23
23
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
24
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
25
|
+
import { agentsIndicator, workingAgents } from "./mini.ts";
|
|
25
26
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
26
|
-
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneBox, type PaneLayout, type PaneMark } from "./layout.ts";
|
|
27
|
+
import { beside, bold, box, fit, highlight, pagerButton, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneBox, type PaneLayout, type PaneMark } from "./layout.ts";
|
|
27
28
|
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
28
29
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
29
30
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
@@ -816,7 +817,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
816
817
|
* end. Lists move their selection instead; the draft moves its cursor (the
|
|
817
818
|
* wheel scrolls it and the cursor follows only when it would leave the screen).
|
|
818
819
|
*/
|
|
819
|
-
private scrollPane(pane: string, delta: number, wheel = false): void {
|
|
820
|
+
private scrollPane(pane: string, delta: number, wheel = false, page = false): void {
|
|
820
821
|
if (delta === 0) return;
|
|
821
822
|
switch (pane) {
|
|
822
823
|
case "conversation":
|
|
@@ -828,13 +829,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
828
829
|
this.planOffset = Math.max(0, this.planOffset - delta);
|
|
829
830
|
break;
|
|
830
831
|
case "draft":
|
|
831
|
-
if (wheel) this.wheelDraft(delta);
|
|
832
|
+
if (wheel || page) this.wheelDraft(delta);
|
|
832
833
|
else this.moveCursor(delta);
|
|
833
834
|
break;
|
|
834
835
|
case "list":
|
|
835
|
-
if (this.tab === "tasks") this.selectTask(this.tasksSelected + Math.sign(delta));
|
|
836
|
+
if (this.tab === "tasks") this.selectTask(this.tasksSelected + (page ? delta : Math.sign(delta)));
|
|
836
837
|
else if (this.tab === "quickfix") {
|
|
837
|
-
this.fixSelected = Math.max(0, Math.min(this.fixJobs().length - 1, this.fixSelected + Math.sign(delta)));
|
|
838
|
+
this.fixSelected = Math.max(0, Math.min(this.fixJobs().length - 1, this.fixSelected + (page ? delta : Math.sign(delta))));
|
|
838
839
|
this.fixDetailOffset = 0;
|
|
839
840
|
}
|
|
840
841
|
break;
|
|
@@ -843,7 +844,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
843
844
|
else if (this.tab === "quickfix") this.fixDetailOffset = Math.max(0, this.fixDetailOffset + delta);
|
|
844
845
|
break;
|
|
845
846
|
case "table":
|
|
846
|
-
this.metricsSelected = Math.max(0, this.metricsSelected + (wheel ? Math.sign(delta) : delta));
|
|
847
|
+
this.metricsSelected = Math.max(0, this.metricsSelected + (wheel && !page ? Math.sign(delta) : delta));
|
|
847
848
|
break;
|
|
848
849
|
}
|
|
849
850
|
this.clampScroll();
|
|
@@ -1640,6 +1641,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
1640
1641
|
if (this.tab === "lobby" && pane) this.homeFocus = pane as HomePane;
|
|
1641
1642
|
if (this.tab === "tasks" && (pane === "list" || pane === "detail")) this.tasksFocus = pane;
|
|
1642
1643
|
if (this.tab === "quickfix" && (pane === "list" || pane === "detail")) this.fixFocus = pane;
|
|
1644
|
+
// The page buttons in a scrolling pane's bottom border scroll it a page or two.
|
|
1645
|
+
const paneBox = pane ? this.panes.get(pane) : undefined;
|
|
1646
|
+
if (pane && paneBox && paneBox.total > paneBox.rows && row === paneBox.top + paneBox.height - 1) {
|
|
1647
|
+
const button = pagerButton(paneBox.width, x - paneBox.left);
|
|
1648
|
+
if (button) {
|
|
1649
|
+
this.scrollPane(pane, button * Math.max(1, paneBox.rows - 1), false, true);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1643
1653
|
if (this.tab !== "plan" || row < 0 || !this.host.planner()) return;
|
|
1644
1654
|
const layout = this.planLayout;
|
|
1645
1655
|
// The draft pane: its border sits one row above its first line and two columns left of its text.
|
|
@@ -1978,7 +1988,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
1978
1988
|
if (this.help) return fit(`${badge("KEYS", "accent")} ${this.chips([["any key", "closes"]], theme)}`, width);
|
|
1979
1989
|
if (this.searching) return fit(`${badge("SEARCH", "warning")} ${this.chips([["enter", "keep"], ["esc", "clear"], ["↑↓", "results"], [k("nextTab"), "next tab"]], theme)}`, width);
|
|
1980
1990
|
const mode = this.mode === "type" ? badge("TYPE", "accent") : badge("BROWSE", "muted");
|
|
1981
|
-
|
|
1991
|
+
const keys = `${mode} ${this.chips(this.hintChips(), theme)}`;
|
|
1992
|
+
// The subagents at work sit at the right end of the line, when the keys leave room.
|
|
1993
|
+
const working = this.viewing.kind === "here" ? agentsIndicator(workingAgents(this.host.zen().runs, this.host.quickfix.running), this.now(), spinner(this.tick), width - textWidth(keys) - 3, theme) : "";
|
|
1994
|
+
const gap = width - textWidth(keys) - textWidth(working) - 1;
|
|
1995
|
+
return working && gap >= 2 ? `${keys}${" ".repeat(gap)}${working} ` : fit(keys, width);
|
|
1982
1996
|
}
|
|
1983
1997
|
|
|
1984
1998
|
/**
|
|
@@ -2054,6 +2068,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2054
2068
|
...row("Ctrl+C", "clear the prompt, or hide the lobby", inner),
|
|
2055
2069
|
...row("click", "a tab to open it, a pane to give it the keys, a draft plan line to comment on it", inner),
|
|
2056
2070
|
...row("wheel", "scroll the pane under the pointer", inner),
|
|
2071
|
+
...row("▲ ▼", "click the buttons on a pane's bottom edge: a page up or down (▲▲ ▼▼ two pages)", inner),
|
|
2057
2072
|
];
|
|
2058
2073
|
const modes = (inner: number) => [
|
|
2059
2074
|
section("Typing"),
|
package/src/master/master.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface ScoutRequest {
|
|
|
49
49
|
/** Model, thinking and time limit for one run, from settings. */
|
|
50
50
|
function profileFields(config: BotLobbyConfig, resolver: ProfileResolver | undefined, domain: Domain, role: AgentRequest["role"]) {
|
|
51
51
|
const profile = profileFor(config, resolver, domain, role);
|
|
52
|
-
return { model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs };
|
|
52
|
+
return { model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs, ...(profile.fallback ? { fallback: profile.fallback } : {}) };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
package/src/master/research.ts
CHANGED
|
@@ -88,6 +88,7 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
88
88
|
model: profile.model,
|
|
89
89
|
thinking: profile.thinking,
|
|
90
90
|
timeoutMs: profile.timeoutMs,
|
|
91
|
+
...(profile.fallback ? { fallback: profile.fallback } : {}),
|
|
91
92
|
...(request.time ? { time: request.time } : {}),
|
|
92
93
|
cwd: request.cwd,
|
|
93
94
|
signal: request.signal,
|
package/src/pi/events.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { selectKnowledge } from "../knowledge/selector.ts";
|
|
|
7
7
|
import { cancelAllRuns } from "../execution/agent-runner.ts";
|
|
8
8
|
import { describeTask } from "../workflow/workflow.ts";
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
|
+
import { registerMasterFallback } from "./master-fallback.ts";
|
|
10
11
|
import { applyStatus, clearStatus, isMinimized, setMinimized } from "./ui.ts";
|
|
11
12
|
import { isSubagentProcess, webToolsFor } from "./quiet.ts";
|
|
12
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
@@ -64,6 +65,7 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
64
65
|
* section rather than replacing the whole prompt.
|
|
65
66
|
*/
|
|
66
67
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
68
|
+
registerMasterFallback(pi);
|
|
67
69
|
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
68
70
|
let hiddenWeb: string[] = [];
|
|
69
71
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The master runs in your own Pi session, on the session's model. When that
|
|
3
|
+
* model runs out of usage mid-task (a subscription limit, no credit) the turn
|
|
4
|
+
* ends with an error and the task would stall. With a fallback model set for
|
|
5
|
+
* the master, the session switches to it and the oracle carries on.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { looksUnavailable, markUnavailable } from "../execution/fallback.ts";
|
|
9
|
+
import { fallbackOf } from "../schemas/configuration.ts";
|
|
10
|
+
import { loadConfig } from "../state/project.ts";
|
|
11
|
+
import { modelRef } from "./model-support.ts";
|
|
12
|
+
import { modelLookup } from "./tools.ts";
|
|
13
|
+
import { isSubagentProcess } from "./quiet.ts";
|
|
14
|
+
|
|
15
|
+
/** The provider's error when the last thing in the turn was a failed assistant message that says the model is out of usage or unavailable. */
|
|
16
|
+
export function usageFailure(messages: readonly unknown[]): string | undefined {
|
|
17
|
+
const last = messages.at(-1) as { role?: string; stopReason?: string; errorMessage?: string } | undefined;
|
|
18
|
+
if (last?.role !== "assistant" || last.stopReason !== "error") return undefined;
|
|
19
|
+
return looksUnavailable(last.errorMessage) ? last.errorMessage : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What the oracle is told after the switch. */
|
|
23
|
+
export function switchedMessage(from: string, to: string): string {
|
|
24
|
+
return `bot-lobby: your last turn failed because ${from} is out of usage or unavailable, so this session switched to ${to}. Carry on with the task from where you left off; do not repeat work that is already done.`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerMasterFallback(pi: ExtensionAPI): void {
|
|
28
|
+
if (isSubagentProcess()) return;
|
|
29
|
+
let failure: string | undefined;
|
|
30
|
+
pi.on("agent_end", (event) => {
|
|
31
|
+
failure = usageFailure(event.messages);
|
|
32
|
+
});
|
|
33
|
+
pi.on("agent_settled", async (_event, ctx: ExtensionContext) => {
|
|
34
|
+
if (!failure) return;
|
|
35
|
+
failure = undefined;
|
|
36
|
+
await switchMaster(pi, ctx);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function switchMaster(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
41
|
+
const fallback = fallbackOf(loadConfig().master);
|
|
42
|
+
if (!fallback) return;
|
|
43
|
+
const current = ctx.model ? modelRef(ctx.model) : "the session model";
|
|
44
|
+
if (current === fallback.model) {
|
|
45
|
+
ctx.ui.notify(`bot-lobby: ${current} (the master's fallback) is out of usage too — pick another model or wait.`, "warning");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const model = modelLookup(ctx)(fallback.model);
|
|
49
|
+
if (!model) {
|
|
50
|
+
ctx.ui.notify(`bot-lobby: the master's fallback model "${fallback.model}" is not known to Pi.`, "warning");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!(await pi.setModel(model))) {
|
|
54
|
+
ctx.ui.notify(`bot-lobby: no auth for the master's fallback model ${fallback.model}.`, "warning");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
pi.setThinkingLevel(fallback.thinking as Parameters<ExtensionAPI["setThinkingLevel"]>[0]);
|
|
58
|
+
if (ctx.model) markUnavailable(current);
|
|
59
|
+
ctx.ui.notify(`bot-lobby: ${current} ran out of usage; the master now runs on ${fallback.model} (${fallback.thinking}). Switch back with /model when it is available again.`, "warning");
|
|
60
|
+
await pi.sendUserMessage(switchedMessage(current, fallback.model));
|
|
61
|
+
}
|
package/src/pi/model-support.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type PanelMember,
|
|
9
9
|
THINKING_LEVELS,
|
|
10
10
|
type BotLobbyConfig,
|
|
11
|
+
type FallbackProfile,
|
|
11
12
|
type ProfileResolver,
|
|
12
13
|
type SubagentKind,
|
|
13
14
|
type ThinkingLevelName,
|
|
@@ -86,11 +87,25 @@ export function createProfileResolver(config: BotLobbyConfig, options: ResolverO
|
|
|
86
87
|
options.warn?.(message);
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
|
-
return { ...profile, model, thinking: check.level };
|
|
90
|
+
return { ...profile, model, thinking: check.level, ...clampedFallback(profile.fallback, kindLabel(profile.kind as SubagentKind), options) };
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
/** The fallback with its thinking level clamped to what the fallback model supports. */
|
|
95
|
+
function clampedFallback(fallback: FallbackProfile | undefined, label: string, options: ResolverOptions): { fallback?: FallbackProfile } {
|
|
96
|
+
if (!fallback) return {};
|
|
97
|
+
const check = checkThinking(options.lookup(fallback.model), fallback.thinking);
|
|
98
|
+
if (check.warning) {
|
|
99
|
+
const message = `bot-lobby: ${label} fallback — ${check.warning}. Change it in /bot-lobby settings.`;
|
|
100
|
+
if (!warned.has(message)) {
|
|
101
|
+
warned.add(message);
|
|
102
|
+
options.warn?.(message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { fallback: { model: fallback.model, thinking: check.level } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string; fallback?: FallbackProfile };
|
|
94
109
|
|
|
95
110
|
/** Unset models run on the session's; thinking is clamped to the model, with a one-time warning. */
|
|
96
111
|
function resolveRunProfile(profile: RunProfile, label: string, options: ResolverOptions): RunProfile {
|
|
@@ -103,7 +118,7 @@ function resolveRunProfile(profile: RunProfile, label: string, options: Resolver
|
|
|
103
118
|
options.warn?.(message);
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
|
-
return { ...profile, model, thinking: check.level };
|
|
121
|
+
return { ...profile, model, thinking: check.level, ...clampedFallback(profile.fallback, label, options) };
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
/** A lobby agent's model, thinking and time limit. */
|
package/src/pi/settings-ui.ts
CHANGED
|
@@ -40,11 +40,14 @@ interface EntryView {
|
|
|
40
40
|
thinking?: string;
|
|
41
41
|
instructions?: string;
|
|
42
42
|
timeoutMs?: number;
|
|
43
|
+
/** The model this agent switches to when its own runs out of usage; unset = none. */
|
|
44
|
+
fallbackModel?: string;
|
|
45
|
+
fallbackThinking?: string;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView {
|
|
46
49
|
if (kind === "master") return config.master;
|
|
47
|
-
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs };
|
|
50
|
+
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs, ...(config.scout.fallbackModel ? { fallbackModel: config.scout.fallbackModel } : {}) };
|
|
48
51
|
if (kind === "researcher") return config.researcher;
|
|
49
52
|
if (kind === "quickfix") return config.quickFix;
|
|
50
53
|
if (kind === "planner") return config.planner;
|
|
@@ -56,21 +59,33 @@ interface EntryPatch {
|
|
|
56
59
|
thinking?: string;
|
|
57
60
|
instructions?: string;
|
|
58
61
|
timeoutMs?: number;
|
|
62
|
+
/** `inherit` clears the fallback (its thinking level with it). */
|
|
63
|
+
fallbackModel?: string;
|
|
64
|
+
fallbackThinking?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A settings entry with the fallback patch applied: `inherit` removes the fallback model and its thinking. */
|
|
68
|
+
function withFallbackPatch<T extends { fallbackModel?: string; fallbackThinking?: string }>(entry: T): T {
|
|
69
|
+
if (entry.fallbackModel !== INHERIT_MODEL) return entry;
|
|
70
|
+
const { fallbackModel: _model, fallbackThinking: _thinking, ...rest } = entry;
|
|
71
|
+
return rest as T;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/** Apply a patch to one entry in a config copy; scouts ignore thinking and instructions. */
|
|
62
75
|
export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: EntryPatch): BotLobbyConfig {
|
|
63
76
|
const next: BotLobbyConfig = { ...config, agents: { ...config.agents } };
|
|
64
|
-
if (kind === "master") next.master = { ...config.master, ...patch } as AgentModelConfig;
|
|
77
|
+
if (kind === "master") next.master = withFallbackPatch({ ...config.master, ...patch } as AgentModelConfig);
|
|
65
78
|
else if (kind === "scout") {
|
|
66
|
-
|
|
79
|
+
const fallbackModel = patch.fallbackModel ?? config.scout.fallbackModel;
|
|
80
|
+
next.scout = withFallbackPatch({
|
|
67
81
|
model: patch.model ?? config.scout.model,
|
|
68
82
|
timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
else if (kind === "
|
|
72
|
-
else if (kind === "
|
|
73
|
-
else next.
|
|
83
|
+
...(fallbackModel ? { fallbackModel } : {}),
|
|
84
|
+
});
|
|
85
|
+
} else if (kind === "researcher") next.researcher = withFallbackPatch({ ...config.researcher, ...patch } as AgentModelConfig);
|
|
86
|
+
else if (kind === "quickfix") next.quickFix = withFallbackPatch({ ...config.quickFix, ...patch } as AgentModelConfig);
|
|
87
|
+
else if (kind === "planner") next.planner = withFallbackPatch({ ...config.planner, ...patch } as AgentModelConfig);
|
|
88
|
+
else next.agents[kind] = withFallbackPatch({ ...config.agents[kind], ...patch } as AgentModelConfig);
|
|
74
89
|
return next;
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -275,6 +290,37 @@ async function editThinking(pi: ExtensionAPI, ctx: ExtensionContext, kind: Setti
|
|
|
275
290
|
await commit(pi, ctx, kind, { thinking: level }, `thinking → ${level}`);
|
|
276
291
|
}
|
|
277
292
|
|
|
293
|
+
/** The model an entry falls back to when its own runs out of usage; `none` removes it. */
|
|
294
|
+
async function editFallbackModel(pi: ExtensionAPI, ctx: ExtensionContext, kind: SettingsKind): Promise<void> {
|
|
295
|
+
const view = entryView(loadConfig(), kind);
|
|
296
|
+
const current = view.fallbackModel ?? INHERIT_MODEL;
|
|
297
|
+
const none: SelectItem = { value: INHERIT_MODEL, label: current === INHERIT_MODEL ? "none ✓" : "none", description: "No fallback: when its model runs out of usage the run fails" };
|
|
298
|
+
const choice = await pick(ctx, `Fallback model — ${kindLabel(kind)}`, [none, ...modelItems(ctx, current, false)], { search: true });
|
|
299
|
+
if (choice === undefined) return;
|
|
300
|
+
const typed = choice === CUSTOM_MODEL ? (await ctx.ui.input("Model id", "provider/model"))?.trim() : choice;
|
|
301
|
+
if (!typed) return;
|
|
302
|
+
await commit(pi, ctx, kind, { fallbackModel: typed }, typed === INHERIT_MODEL ? "fallback removed" : `fallback model → ${typed}`);
|
|
303
|
+
if (typed === INHERIT_MODEL || kind === "scout") return;
|
|
304
|
+
// Keep the fallback's thinking level one its model can run.
|
|
305
|
+
const level = entryView(loadConfig(), kind).fallbackThinking;
|
|
306
|
+
const check = level ? checkThinking(modelLookup(ctx)(typed), level) : undefined;
|
|
307
|
+
if (check?.warning) {
|
|
308
|
+
updateEntry(kind, { fallbackThinking: check.level });
|
|
309
|
+
ctx.ui.notify(`bot-lobby: ${kindLabel(kind)} fallback — ${check.warning}.`, "warning");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The thinking level on the fallback model, limited to what that model supports. */
|
|
314
|
+
async function editFallbackThinking(pi: ExtensionAPI, ctx: ExtensionContext, kind: SettingsKind): Promise<void> {
|
|
315
|
+
const view = entryView(loadConfig(), kind);
|
|
316
|
+
if (!view.fallbackModel) return;
|
|
317
|
+
const model = modelLookup(ctx)(view.fallbackModel);
|
|
318
|
+
const title = `Fallback thinking — ${kindLabel(kind)} (${view.fallbackModel})`;
|
|
319
|
+
const level = await pick(ctx, title, thinkingItems(supportedThinking(model), view.fallbackThinking ?? view.thinking));
|
|
320
|
+
if (!level || !isThinkingLevel(level)) return;
|
|
321
|
+
await commit(pi, ctx, kind, { fallbackThinking: level }, `fallback thinking → ${level}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
278
324
|
async function editTimeout(pi: ExtensionAPI, ctx: ExtensionContext, kind: SettingsKind): Promise<void> {
|
|
279
325
|
const current = entryView(loadConfig(), kind).timeoutMs ?? loadConfig().workflow.agentTimeoutMs;
|
|
280
326
|
const typed = (await ctx.ui.input(`Time limit (minutes) — ${kindLabel(kind)}`, String(Math.round(current / 60_000))))?.trim();
|
|
@@ -303,6 +349,8 @@ export function entryItems(kind: SettingsKind, view: EntryView): SelectItem[] {
|
|
|
303
349
|
const items: SelectItem[] = [{ value: "model", label: "Model", description: view.model }];
|
|
304
350
|
if (kind === "scout") items.push({ value: "fixed", label: "Thinking", description: `${SCOUT_THINKING} (fixed for scouts)` });
|
|
305
351
|
else items.push({ value: "thinking", label: "Thinking", description: view.thinking ?? "" });
|
|
352
|
+
items.push({ value: "fallback", label: "Fallback model", description: view.fallbackModel ?? "none · used when this model runs out of usage" });
|
|
353
|
+
if (kind !== "scout" && view.fallbackModel) items.push({ value: "fallbackThinking", label: "Fallback thinking", description: view.fallbackThinking ?? view.thinking ?? "" });
|
|
306
354
|
if (kind !== "master") items.push({ value: "timeout", label: "Time limit", description: minutes(view.timeoutMs) });
|
|
307
355
|
if (kind !== "scout" && kind !== "researcher") {
|
|
308
356
|
items.push({ value: "instructions", label: "Instructions", description: view.instructions ? `${view.instructions.length} chars` : "(none)" });
|
|
@@ -317,6 +365,8 @@ async function editEntry(pi: ExtensionAPI, ctx: ExtensionContext, kind: Settings
|
|
|
317
365
|
if (!action || action === "back") return;
|
|
318
366
|
if (action === "model") await editModel(pi, ctx, kind);
|
|
319
367
|
else if (action === "thinking") await editThinking(pi, ctx, kind);
|
|
368
|
+
else if (action === "fallback") await editFallbackModel(pi, ctx, kind);
|
|
369
|
+
else if (action === "fallbackThinking") await editFallbackThinking(pi, ctx, kind);
|
|
320
370
|
else if (action === "timeout") await editTimeout(pi, ctx, kind);
|
|
321
371
|
else if (action === "instructions") await editInstructions(pi, ctx, kind);
|
|
322
372
|
}
|
|
@@ -352,11 +402,12 @@ function entryDescription(kind: SettingsKind, view: EntryView): string {
|
|
|
352
402
|
const thinking = kind === "scout" ? SCOUT_THINKING : view.thinking;
|
|
353
403
|
const custom = view.instructions ? " · custom" : "";
|
|
354
404
|
const limit = kind === "master" ? "" : ` · ${minutes(view.timeoutMs)}`;
|
|
355
|
-
|
|
405
|
+
const fallback = view.fallbackModel ? ` · fallback ${view.fallbackModel}` : "";
|
|
406
|
+
return `${view.model} · ${thinking}${limit}${custom}${fallback}`;
|
|
356
407
|
}
|
|
357
408
|
|
|
358
409
|
/** The lobby's on/off settings as the settings menu lists them; `panel:*` are the Lobby tab's panes. */
|
|
359
|
-
export type LobbySwitch = "autoOpen" | "autoAsk" | "mouse" | "issues" | `panel:${LobbyPanel}`;
|
|
410
|
+
export type LobbySwitch = "autoOpen" | "autoAsk" | "mouse" | "miniLine" | "issues" | `panel:${LobbyPanel}`;
|
|
360
411
|
|
|
361
412
|
const PANEL_SWITCH_LABELS: Record<LobbyPanel, string> = { conversation: "Conversation pane", activity: "Activity log pane", thinking: "Thinking pane" };
|
|
362
413
|
|
|
@@ -364,6 +415,7 @@ export const LOBBY_SWITCHES: ReadonlyArray<{ id: LobbySwitch; label: string; hel
|
|
|
364
415
|
{ id: "autoOpen", label: "Open with a task", help: "open the lobby when this session starts or resumes a task" },
|
|
365
416
|
{ id: "autoAsk", label: "Ask at once", help: "put the panel's questions to you as soon as a round ends, while the Plan tab is open" },
|
|
366
417
|
{ id: "mouse", label: "Mouse", help: "click tabs and draft lines, scroll with the wheel (shift+drag still selects text)" },
|
|
418
|
+
{ id: "miniLine", label: "Status line when hidden", help: "one line under the editor while the lobby is hidden: task steps, the planning round, a quick fix, or idle" },
|
|
367
419
|
{ id: "issues", label: "Issues tab", help: "the GitHub Issues tab" },
|
|
368
420
|
...LOBBY_PANELS.map((panel) => ({
|
|
369
421
|
id: `panel:${panel}` as const,
|
|
@@ -419,6 +471,7 @@ function lobbySummary(config: BotLobbyConfig): string {
|
|
|
419
471
|
config.lobby.autoOpen ? "opens with a task" : "opens on alt+l",
|
|
420
472
|
config.lobby.autoAsk ? "asks at once" : "asks on enter",
|
|
421
473
|
config.lobby.mouse ? "mouse" : "no mouse",
|
|
474
|
+
...(config.lobby.miniLine ? [] : ["no status line"]),
|
|
422
475
|
`planning: ${roundLimitLabel(config.lobby.maxPlanningRounds)}`,
|
|
423
476
|
...(config.lobby.issues ? ["issues tab"] : []),
|
|
424
477
|
...(hidden.length > 0 ? [`hidden: ${hidden.join(", ")}`] : []),
|
|
@@ -31,12 +31,18 @@ export interface AgentModelConfig {
|
|
|
31
31
|
instructions?: string;
|
|
32
32
|
/** Time limit per run; falls back to `workflow.agentTimeoutMs`. */
|
|
33
33
|
timeoutMs?: number;
|
|
34
|
+
/** Model this agent switches to when its own model runs out of usage or is unavailable; unset or `inherit` = none. */
|
|
35
|
+
fallbackModel?: ModelRef;
|
|
36
|
+
/** Thinking level on the fallback model; unset uses this agent's own `thinking`. */
|
|
37
|
+
fallbackThinking?: string;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** Scouts pick a model and a time limit only; their thinking is fixed at `SCOUT_THINKING`. */
|
|
37
41
|
export interface ScoutConfig {
|
|
38
42
|
model: ModelRef;
|
|
39
43
|
timeoutMs: number;
|
|
44
|
+
/** Model scouts switch to when theirs runs out of usage or is unavailable (they think at `SCOUT_THINKING` on it too). */
|
|
45
|
+
fallbackModel?: ModelRef;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** Subagent kinds with their own settings entry. */
|
|
@@ -114,6 +120,8 @@ export interface LobbyConfig {
|
|
|
114
120
|
panels: Record<LobbyPanel, boolean>;
|
|
115
121
|
/** Key overrides by action name, e.g. `{ "toggleThinking": "alt+t" }`. */
|
|
116
122
|
keys: Record<string, string>;
|
|
123
|
+
/** A one-line status (task steps, planning round, quick fix, or idle) under the editor while the lobby is hidden. */
|
|
124
|
+
miniLine: boolean;
|
|
117
125
|
/** Clicks and the wheel work in the lobby (click a draft line to comment on it); shift+drag still selects text. */
|
|
118
126
|
mouse: boolean;
|
|
119
127
|
/**
|
|
@@ -231,6 +239,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
231
239
|
panels: { conversation: true, activity: true, thinking: true },
|
|
232
240
|
keys: {},
|
|
233
241
|
mouse: true,
|
|
242
|
+
miniLine: true,
|
|
234
243
|
maxPlanningRounds: 5,
|
|
235
244
|
},
|
|
236
245
|
classifier: {
|
|
@@ -266,18 +275,22 @@ function positive(value: unknown): number | undefined {
|
|
|
266
275
|
|
|
267
276
|
/** Merge one agent's override over its default; legacy `inherit` or unknown thinking falls back to the default level. */
|
|
268
277
|
function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConfig> | undefined): AgentModelConfig {
|
|
269
|
-
const merged = { ...base, ...(override ?? {}) };
|
|
278
|
+
const { fallbackModel: rawModel, fallbackThinking: rawThinking, ...merged } = { ...base, ...(override ?? {}) };
|
|
270
279
|
const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
|
|
271
280
|
const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
|
|
272
|
-
|
|
281
|
+
const fallbackModel = typeof rawModel === "string" && rawModel.trim() && rawModel !== INHERIT_MODEL ? rawModel.trim() : undefined;
|
|
282
|
+
const fallbackThinking = typeof rawThinking === "string" && isThinkingLevel(rawThinking) ? rawThinking : undefined;
|
|
283
|
+
return { ...merged, thinking, ...(timeoutMs ? { timeoutMs } : {}), ...(fallbackModel ? { fallbackModel, ...(fallbackThinking ? { fallbackThinking } : {}) } : {}) };
|
|
273
284
|
}
|
|
274
285
|
|
|
275
286
|
/** Scouts keep a model and a time limit; any thinking value in the file is dropped. */
|
|
276
287
|
function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig {
|
|
277
288
|
const base = DEFAULT_CONFIG.scout;
|
|
289
|
+
const fallback = override?.fallbackModel;
|
|
278
290
|
return {
|
|
279
291
|
model: typeof override?.model === "string" && override.model.trim() ? override.model : base.model,
|
|
280
292
|
timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
|
|
293
|
+
...(typeof fallback === "string" && fallback.trim() && fallback !== INHERIT_MODEL ? { fallbackModel: fallback.trim() } : {}),
|
|
281
294
|
};
|
|
282
295
|
}
|
|
283
296
|
|
|
@@ -286,7 +299,7 @@ function flag(value: unknown, fallback: boolean): boolean {
|
|
|
286
299
|
}
|
|
287
300
|
|
|
288
301
|
function normalizeLobby(value: unknown): LobbyConfig {
|
|
289
|
-
const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown; maxPlanningRounds?: unknown } | undefined;
|
|
302
|
+
const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown; miniLine?: unknown; maxPlanningRounds?: unknown } | undefined;
|
|
290
303
|
const defaults = DEFAULT_CONFIG.lobby;
|
|
291
304
|
const panel = Array.isArray(source?.planningPanel)
|
|
292
305
|
? [...new Set(source.planningPanel.filter((entry): entry is PanelMember => typeof entry === "string" && isPanelMember(entry)))]
|
|
@@ -301,6 +314,7 @@ function normalizeLobby(value: unknown): LobbyConfig {
|
|
|
301
314
|
panels: Object.fromEntries(LOBBY_PANELS.map((name) => [name, flag(panels[name], defaults.panels[name])])) as Record<LobbyPanel, boolean>,
|
|
302
315
|
keys: Object.fromEntries(Object.entries(keys).filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1].trim().length > 0)),
|
|
303
316
|
mouse: flag(source?.mouse, defaults.mouse),
|
|
317
|
+
miniLine: flag(source?.miniLine, defaults.miniLine),
|
|
304
318
|
maxPlanningRounds: roundLimit(source?.maxPlanningRounds, defaults.maxPlanningRounds),
|
|
305
319
|
};
|
|
306
320
|
}
|
|
@@ -390,6 +404,22 @@ export interface AgentProfile {
|
|
|
390
404
|
thinking: string;
|
|
391
405
|
timeoutMs: number;
|
|
392
406
|
instructions?: string;
|
|
407
|
+
/** Where the run goes when its model runs out of usage or is unavailable; absent = it just fails. */
|
|
408
|
+
fallback?: FallbackProfile;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The fallback model and the thinking level to run it at. */
|
|
412
|
+
export interface FallbackProfile {
|
|
413
|
+
model: string;
|
|
414
|
+
thinking: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The fallback of a settings entry: its model and thinking (the entry's own level when none is set). */
|
|
418
|
+
export function fallbackOf(entry: { fallbackModel?: string; fallbackThinking?: string; thinking?: string }): FallbackProfile | undefined {
|
|
419
|
+
const model = modelOf(entry.fallbackModel ?? INHERIT_MODEL);
|
|
420
|
+
if (!model) return undefined;
|
|
421
|
+
const own = entry.thinking && isThinkingLevel(entry.thinking) ? entry.thinking : DEFAULT_THINKING;
|
|
422
|
+
return { model, thinking: entry.fallbackThinking && isThinkingLevel(entry.fallbackThinking) ? entry.fallbackThinking : own };
|
|
393
423
|
}
|
|
394
424
|
|
|
395
425
|
/** The settings entry a domain/role run draws from. */
|
|
@@ -414,10 +444,12 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
|
|
|
414
444
|
const instructions = config.agents[domain].instructions;
|
|
415
445
|
const fallback = config.workflow.agentTimeoutMs;
|
|
416
446
|
if (kind === "scout") {
|
|
417
|
-
|
|
447
|
+
const scoutFallback = fallbackOf({ fallbackModel: config.scout.fallbackModel, thinking: SCOUT_THINKING });
|
|
448
|
+
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(scoutFallback ? { fallback: scoutFallback } : {}) };
|
|
418
449
|
}
|
|
419
450
|
const entry = kind === "researcher" ? config.researcher : config.agents[kind];
|
|
420
|
-
|
|
451
|
+
const entryFallback = fallbackOf(entry);
|
|
452
|
+
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entryFallback ? { fallback: entryFallback } : {}) };
|
|
421
453
|
}
|
|
422
454
|
|
|
423
455
|
/** The settings entry of a lobby agent (quick fix or planner). */
|
|
@@ -426,19 +458,21 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
|
|
|
426
458
|
}
|
|
427
459
|
|
|
428
460
|
/** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
|
|
429
|
-
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
461
|
+
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string; fallback?: FallbackProfile } {
|
|
430
462
|
const entry = lobbyAgentConfig(config, kind);
|
|
431
|
-
|
|
463
|
+
const fallback = fallbackOf(entry);
|
|
464
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(fallback ? { fallback } : {}) };
|
|
432
465
|
}
|
|
433
466
|
|
|
434
467
|
/**
|
|
435
468
|
* A planning panel seat's profile: the domain's (or the researcher's) model,
|
|
436
469
|
* thinking and custom instructions, bounded by the planner's per-turn limit.
|
|
437
470
|
*/
|
|
438
|
-
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
471
|
+
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string; fallback?: FallbackProfile } {
|
|
439
472
|
const entry = member === "researcher" ? config.researcher : config.agents[member];
|
|
440
473
|
const timeoutMs = config.planner.timeoutMs ?? config.workflow.agentTimeoutMs;
|
|
441
|
-
|
|
474
|
+
const fallback = fallbackOf(entry);
|
|
475
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(fallback ? { fallback } : {}) };
|
|
442
476
|
}
|
|
443
477
|
|
|
444
478
|
/** Resolves the model, thinking and time limit one subagent run uses. */
|
package/src/schemas/findings.ts
CHANGED
|
@@ -139,6 +139,8 @@ export interface AgentRun {
|
|
|
139
139
|
wrappedUp?: boolean;
|
|
140
140
|
/** File this worker is queued for at the file desk. */
|
|
141
141
|
waitingFor?: string;
|
|
142
|
+
/** The run switched to its fallback model because this one ran out of usage or was unavailable. */
|
|
143
|
+
fellBackFrom?: string;
|
|
142
144
|
/** The classifier routed this run down: the configured profile it came from (`p/big · medium`). */
|
|
143
145
|
routedFrom?: string;
|
|
144
146
|
/** Why and where it was routed (`trivial 0.88: p/big · medium → p/cheap · low`). */
|