@a-t-h-i/bot-lobby 0.6.5 → 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 +127 -6
- package/package.json +1 -1
- package/prompts/global.md +18 -0
- package/prompts/master.md +77 -2
- package/prompts/worker.md +7 -2
- package/src/ask/dialog.ts +15 -3
- 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/index.ts +1 -1
- package/src/lobby/feed.ts +8 -1
- package/src/lobby/keys.ts +0 -1
- 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 +29 -26
- package/src/lobby/tabs/home.ts +6 -42
- package/src/lobby/tabs/tasks.ts +1 -1
- package/src/lobby/view.ts +31 -30
- package/src/master/master.ts +1 -1
- package/src/master/research.ts +1 -0
- package/src/pi/activity.ts +1 -33
- package/src/pi/events.ts +3 -16
- package/src/pi/master-fallback.ts +61 -0
- package/src/pi/model-support.ts +18 -3
- package/src/pi/plan-checklist.ts +305 -0
- package/src/pi/run-summary.ts +12 -3
- package/src/pi/settings-ui.ts +65 -12
- package/src/pi/tools.ts +5 -4
- package/src/pi/ui.ts +18 -284
- package/src/roles/worker.ts +1 -1
- package/src/schemas/configuration.ts +51 -15
- package/src/schemas/findings.ts +2 -0
- package/src/workflow/brief.ts +59 -0
- package/src/workflow/workflow.ts +15 -2
- package/src/pi/expressions.ts +0 -169
- package/src/pi/kaomoji.ts +0 -227
- package/src/pi/mascot-art.ts +0 -359
- package/src/pi/zen-large.ts +0 -699
- package/src/pi/zen-metrics.ts +0 -130
- package/src/pi/zen.ts +0 -659
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";
|
|
@@ -54,8 +55,8 @@ const PROMPT_FIRST: ReadonlySet<TabId> = new Set(["lobby", "plan", "quickfix"]);
|
|
|
54
55
|
export type LobbyMode = "type" | "browse";
|
|
55
56
|
|
|
56
57
|
/** What each pane toggle is called in notices and help. */
|
|
57
|
-
const PANEL_NAMES: Record<LobbyPanel, string> = {
|
|
58
|
-
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = {
|
|
58
|
+
const PANEL_NAMES: Record<LobbyPanel, string> = { conversation: "conversation", activity: "activity log", thinking: "thinking" };
|
|
59
|
+
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = { conversation: "toggleConversation", activity: "toggleActivity", thinking: "toggleThinking" };
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Which session the Lobby tab shows and talks to: this window, one it started
|
|
@@ -125,12 +126,8 @@ export interface LobbyHost {
|
|
|
125
126
|
rows(): number;
|
|
126
127
|
theme(): LobbyTheme;
|
|
127
128
|
sessionId(): string | undefined;
|
|
128
|
-
/** This session's active task and its runs, as the
|
|
129
|
+
/** This session's active task and its runs, as the lobby reads them. */
|
|
129
130
|
zen(): { task?: Task; runs: readonly AgentRun[] };
|
|
130
|
-
/** Draw the zen scene into at most `height` lines: animated, or only the task's status when `animated` is false. */
|
|
131
|
-
scene(width: number, height: number, animated: boolean): string[];
|
|
132
|
-
/** Advance the scene's clock; returns the delay it wants until the next step. */
|
|
133
|
-
advanceScene(now: number): number;
|
|
134
131
|
feed: LobbyFeed;
|
|
135
132
|
masterBusy(): boolean;
|
|
136
133
|
tasks(): Task[];
|
|
@@ -199,14 +196,12 @@ export interface LobbyHost {
|
|
|
199
196
|
hasOlderChat(sessionId: string): boolean;
|
|
200
197
|
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
201
198
|
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
202
|
-
/** A task's status without animations, for a session other than this window's. */
|
|
203
|
-
taskScene(task: Task, width: number, height: number): string[];
|
|
204
199
|
profileLabel(kind: LobbyAgentKind): string;
|
|
205
200
|
requestRender(): void;
|
|
206
201
|
now?(): number;
|
|
207
202
|
}
|
|
208
203
|
|
|
209
|
-
/** Live work speeds the clock up so spinners
|
|
204
|
+
/** Live work speeds the clock up so spinners move. */
|
|
210
205
|
export const LIVE_MS = 250;
|
|
211
206
|
export const IDLE_MS = 1000;
|
|
212
207
|
/** How often task and metrics data is reread from disk while the lobby is open. */
|
|
@@ -424,16 +419,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
424
419
|
|| Boolean(zen.task && !zen.task.paused && !TERMINAL_STATES.includes(zen.task.state));
|
|
425
420
|
}
|
|
426
421
|
|
|
427
|
-
/** One clock step:
|
|
422
|
+
/** One clock step: reread data when due, repaint. */
|
|
428
423
|
private step(): void {
|
|
429
424
|
if (!this.running) return;
|
|
430
|
-
const now = this.now();
|
|
431
425
|
this.tick += 1;
|
|
432
|
-
let delay = this.isLive() ? LIVE_MS : IDLE_MS;
|
|
433
|
-
if (this.tab === "lobby" && this.panels.animations && this.host.zen().task) delay = Math.min(delay, this.host.advanceScene(now));
|
|
434
426
|
this.refreshData(false);
|
|
435
427
|
this.host.requestRender();
|
|
436
|
-
this.schedule(
|
|
428
|
+
this.schedule(this.isLive() ? LIVE_MS : IDLE_MS);
|
|
437
429
|
}
|
|
438
430
|
|
|
439
431
|
/** Reread tasks, plans, comments and metrics from disk (throttled unless forced). */
|
|
@@ -675,8 +667,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
675
667
|
return this.cycleTab(1);
|
|
676
668
|
case "prevTab":
|
|
677
669
|
return this.cycleTab(-1);
|
|
678
|
-
case "toggleScene":
|
|
679
|
-
return this.togglePanel("animations");
|
|
680
670
|
case "toggleConversation":
|
|
681
671
|
return this.togglePanel("conversation");
|
|
682
672
|
case "toggleActivity":
|
|
@@ -827,7 +817,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
827
817
|
* end. Lists move their selection instead; the draft moves its cursor (the
|
|
828
818
|
* wheel scrolls it and the cursor follows only when it would leave the screen).
|
|
829
819
|
*/
|
|
830
|
-
private scrollPane(pane: string, delta: number, wheel = false): void {
|
|
820
|
+
private scrollPane(pane: string, delta: number, wheel = false, page = false): void {
|
|
831
821
|
if (delta === 0) return;
|
|
832
822
|
switch (pane) {
|
|
833
823
|
case "conversation":
|
|
@@ -839,13 +829,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
839
829
|
this.planOffset = Math.max(0, this.planOffset - delta);
|
|
840
830
|
break;
|
|
841
831
|
case "draft":
|
|
842
|
-
if (wheel) this.wheelDraft(delta);
|
|
832
|
+
if (wheel || page) this.wheelDraft(delta);
|
|
843
833
|
else this.moveCursor(delta);
|
|
844
834
|
break;
|
|
845
835
|
case "list":
|
|
846
|
-
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)));
|
|
847
837
|
else if (this.tab === "quickfix") {
|
|
848
|
-
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))));
|
|
849
839
|
this.fixDetailOffset = 0;
|
|
850
840
|
}
|
|
851
841
|
break;
|
|
@@ -854,7 +844,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
854
844
|
else if (this.tab === "quickfix") this.fixDetailOffset = Math.max(0, this.fixDetailOffset + delta);
|
|
855
845
|
break;
|
|
856
846
|
case "table":
|
|
857
|
-
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));
|
|
858
848
|
break;
|
|
859
849
|
}
|
|
860
850
|
this.clampScroll();
|
|
@@ -1651,6 +1641,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
1651
1641
|
if (this.tab === "lobby" && pane) this.homeFocus = pane as HomePane;
|
|
1652
1642
|
if (this.tab === "tasks" && (pane === "list" || pane === "detail")) this.tasksFocus = pane;
|
|
1653
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
|
+
}
|
|
1654
1653
|
if (this.tab !== "plan" || row < 0 || !this.host.planner()) return;
|
|
1655
1654
|
const layout = this.planLayout;
|
|
1656
1655
|
// The draft pane: its border sits one row above its first line and two columns left of its text.
|
|
@@ -1989,7 +1988,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
1989
1988
|
if (this.help) return fit(`${badge("KEYS", "accent")} ${this.chips([["any key", "closes"]], theme)}`, width);
|
|
1990
1989
|
if (this.searching) return fit(`${badge("SEARCH", "warning")} ${this.chips([["enter", "keep"], ["esc", "clear"], ["↑↓", "results"], [k("nextTab"), "next tab"]], theme)}`, width);
|
|
1991
1990
|
const mode = this.mode === "type" ? badge("TYPE", "accent") : badge("BROWSE", "muted");
|
|
1992
|
-
|
|
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);
|
|
1993
1996
|
}
|
|
1994
1997
|
|
|
1995
1998
|
/**
|
|
@@ -2065,6 +2068,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2065
2068
|
...row("Ctrl+C", "clear the prompt, or hide the lobby", inner),
|
|
2066
2069
|
...row("click", "a tab to open it, a pane to give it the keys, a draft plan line to comment on it", inner),
|
|
2067
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),
|
|
2068
2072
|
];
|
|
2069
2073
|
const modes = (inner: number) => [
|
|
2070
2074
|
section("Typing"),
|
|
@@ -2295,7 +2299,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2295
2299
|
if (entry.view.kind !== "here") return this.otherSessionBody(entry, width, height, theme, now);
|
|
2296
2300
|
const talk = this.conversationFor(entry);
|
|
2297
2301
|
return renderHome({
|
|
2298
|
-
...(zen.task ? { task: zen.task
|
|
2302
|
+
...(zen.task ? { task: zen.task } : {}),
|
|
2299
2303
|
chat: talk.chat,
|
|
2300
2304
|
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2301
2305
|
...(feed.reply ? { liveReply: feed.reply } : {}),
|
|
@@ -2313,7 +2317,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
2313
2317
|
panels: this.panels,
|
|
2314
2318
|
...(query ? { query } : {}),
|
|
2315
2319
|
keys: {
|
|
2316
|
-
animations: keyLabel(this.keys.toggleScene),
|
|
2317
2320
|
conversation: keyLabel(this.keys.toggleConversation),
|
|
2318
2321
|
activity: keyLabel(this.keys.toggleActivity),
|
|
2319
2322
|
thinking: keyLabel(this.keys.toggleThinking),
|
|
@@ -2323,8 +2326,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2323
2326
|
|
|
2324
2327
|
/**
|
|
2325
2328
|
* Another session on the Lobby tab: a background session's live feed, or an
|
|
2326
|
-
* other terminal's conversation read from its session file
|
|
2327
|
-
* status sits on top without animations.
|
|
2329
|
+
* other terminal's conversation read from its session file.
|
|
2328
2330
|
*/
|
|
2329
2331
|
private otherSessionBody(entry: SessionEntry, width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
2330
2332
|
const session = this.viewedSession();
|
|
@@ -2332,7 +2334,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2332
2334
|
const task = entry.task;
|
|
2333
2335
|
const query = this.query();
|
|
2334
2336
|
const common = {
|
|
2335
|
-
...(task ? { task
|
|
2337
|
+
...(task ? { task } : {}),
|
|
2336
2338
|
others: 0,
|
|
2337
2339
|
pending: 0,
|
|
2338
2340
|
offsets: this.homeOffsets,
|
|
@@ -2345,7 +2347,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
2345
2347
|
...(query ? { query } : {}),
|
|
2346
2348
|
title: entry.name,
|
|
2347
2349
|
keys: {
|
|
2348
|
-
animations: keyLabel(this.keys.toggleScene),
|
|
2349
2350
|
conversation: keyLabel(this.keys.toggleConversation),
|
|
2350
2351
|
activity: keyLabel(this.keys.toggleActivity),
|
|
2351
2352
|
thinking: keyLabel(this.keys.toggleThinking),
|
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/activity.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One-word descriptions of the tool an agent is running: subagents show it as
|
|
3
|
-
* `[spinner] word - elapsed
|
|
4
|
-
* action. Pure lookup:
|
|
5
|
-
* no clock, environment or I/O reads.
|
|
3
|
+
* `[spinner] word - elapsed`. Pure lookup: no clock, environment or I/O reads.
|
|
6
4
|
*/
|
|
7
5
|
const ACTIVITY_WORDS: Record<string, string> = {
|
|
8
6
|
read: "reading",
|
|
@@ -22,46 +20,16 @@ const ACTIVITY_WORDS: Record<string, string> = {
|
|
|
22
20
|
|
|
23
21
|
const FALLBACK_WORD = "working";
|
|
24
22
|
|
|
25
|
-
/** What the oracle says between its own tool calls while a master turn runs. */
|
|
26
|
-
export const ORACLE_THINKING = "thinking";
|
|
27
|
-
|
|
28
23
|
export function activityWord(toolName: string): string {
|
|
29
24
|
return ACTIVITY_WORDS[toolName.trim().toLowerCase()] ?? FALLBACK_WORD;
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
/** Master `orchestrate` actions -> the word the oracle shows instead of the generic "orchestrating". */
|
|
33
|
-
const ORACLE_ACTION_WORDS: Record<string, string> = {
|
|
34
|
-
clarify: "asking",
|
|
35
|
-
scout: "scouting",
|
|
36
|
-
research: "researching",
|
|
37
|
-
propose: "proposing",
|
|
38
|
-
plan: "planning",
|
|
39
|
-
implement: "delegating",
|
|
40
|
-
qa: "reviewing",
|
|
41
|
-
knowledge: "recording",
|
|
42
|
-
compact: "recording",
|
|
43
|
-
decide: "deciding",
|
|
44
|
-
resolve_approval: "deciding",
|
|
45
|
-
complete: "wrapping up",
|
|
46
|
-
status: "checking",
|
|
47
|
-
block: "blocking",
|
|
48
|
-
resume: "resuming",
|
|
49
|
-
cancel: "cancelling",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
27
|
function orchestrateAction(args: unknown): string | undefined {
|
|
53
28
|
if (!args || typeof args !== "object" || !("action" in args)) return undefined;
|
|
54
29
|
const action = (args as { action?: unknown }).action;
|
|
55
30
|
return typeof action === "string" ? action.trim().toLowerCase() : undefined;
|
|
56
31
|
}
|
|
57
32
|
|
|
58
|
-
/** One word for what the oracle (master) is doing; `orchestrate` maps by its action. */
|
|
59
|
-
export function oracleActivityWord(toolName: string, args?: unknown): string {
|
|
60
|
-
if (toolName.trim().toLowerCase() !== "orchestrate") return activityWord(toolName);
|
|
61
|
-
const action = orchestrateAction(args);
|
|
62
|
-
return (action && ORACLE_ACTION_WORDS[action]) || FALLBACK_WORD;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
33
|
const DETAIL_CHARS = 28;
|
|
66
34
|
|
|
67
35
|
function clip(text: string): string {
|
package/src/pi/events.ts
CHANGED
|
@@ -7,8 +7,8 @@ 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 {
|
|
11
|
-
import {
|
|
10
|
+
import { registerMasterFallback } from "./master-fallback.ts";
|
|
11
|
+
import { applyStatus, clearStatus, isMinimized, setMinimized } from "./ui.ts";
|
|
12
12
|
import { isSubagentProcess, webToolsFor } from "./quiet.ts";
|
|
13
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
14
14
|
import { taskRequest, type Task, type TaskState } from "../schemas/task.ts";
|
|
@@ -65,6 +65,7 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
65
65
|
* section rather than replacing the whole prompt.
|
|
66
66
|
*/
|
|
67
67
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
68
|
+
registerMasterFallback(pi);
|
|
68
69
|
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
69
70
|
let hiddenWeb: string[] = [];
|
|
70
71
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -76,26 +77,16 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
// The oracle's speech bubble mirrors the master's own turn: thinking between
|
|
80
|
-
// tool calls, the tool's word during one, and silent (your turn) once it ends.
|
|
81
|
-
// Subagents report into runs instead.
|
|
82
|
-
// Parallel tool calls: the newest still-running call keeps the word.
|
|
83
|
-
const inFlight = new Map<string, string>();
|
|
84
|
-
const showOracle = () => setOracleActivity([...inFlight.values()].at(-1) ?? ORACLE_THINKING);
|
|
85
80
|
// A task's time budget counts while the oracle works on it, not while it waits on the user.
|
|
86
81
|
const asking = new Set<string>();
|
|
87
82
|
pi.on("agent_start", (_event, ctx) => {
|
|
88
83
|
if (isSubagentProcess()) return;
|
|
89
|
-
inFlight.clear();
|
|
90
|
-
showOracle();
|
|
91
84
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
92
85
|
const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
|
|
93
86
|
if (task) startClock(root, configDir, task.id);
|
|
94
87
|
});
|
|
95
88
|
pi.on("tool_execution_start", (event) => {
|
|
96
89
|
if (isSubagentProcess()) return;
|
|
97
|
-
inFlight.set(event.toolCallId, oracleActivityWord(event.toolName, event.args));
|
|
98
|
-
showOracle();
|
|
99
90
|
if (ASKING_TOOLS.has(event.toolName) && !asking.has(event.toolCallId)) {
|
|
100
91
|
asking.add(event.toolCallId);
|
|
101
92
|
pauseClocks();
|
|
@@ -103,14 +94,10 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
103
94
|
});
|
|
104
95
|
pi.on("tool_execution_end", (event) => {
|
|
105
96
|
if (isSubagentProcess()) return;
|
|
106
|
-
inFlight.delete(event.toolCallId);
|
|
107
|
-
showOracle();
|
|
108
97
|
if (asking.delete(event.toolCallId)) resumeClocks();
|
|
109
98
|
});
|
|
110
99
|
pi.on("agent_end", () => {
|
|
111
100
|
if (isSubagentProcess()) return;
|
|
112
|
-
inFlight.clear();
|
|
113
|
-
setOracleActivity(undefined);
|
|
114
101
|
for (const _call of asking) resumeClocks();
|
|
115
102
|
asking.clear();
|
|
116
103
|
stopClocks();
|
|
@@ -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. */
|