@a-t-h-i/bot-lobby 0.6.17 → 0.6.19
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 +53 -697
- package/package.json +1 -1
- package/src/ask/relay.ts +6 -4
- package/src/ask/tool.ts +18 -3
- package/src/ask/types.ts +12 -0
- package/src/ask/web.ts +2 -1
- package/src/delivery/checks.ts +37 -0
- package/src/delivery/lock.ts +78 -0
- package/src/delivery/merge.ts +44 -0
- package/src/delivery/operations.ts +134 -0
- package/src/delivery/pr.ts +40 -0
- package/src/delivery/review.ts +90 -0
- package/src/delivery/rules.ts +46 -0
- package/src/delivery/source.ts +39 -0
- package/src/delivery/transport.ts +36 -0
- package/src/delivery/types.ts +60 -0
- package/src/execution/agent-runner.ts +1 -23
- package/src/index.ts +5 -2
- package/src/lobby/feed.ts +0 -6
- package/src/lobby/host.ts +6 -0
- package/src/lobby/planner.ts +26 -7
- package/src/lobby/pr-review.ts +1 -7
- package/src/lobby/prompt-hub.ts +7 -1
- package/src/lobby/pulls.ts +44 -1
- package/src/lobby/quickfix.ts +3 -16
- package/src/lobby/service.ts +104 -6
- package/src/lobby/sessions.ts +12 -0
- package/src/lobby/task-rows.ts +5 -0
- package/src/master/master.ts +1 -1
- package/src/master/research.ts +0 -1
- package/src/pi/events.ts +0 -2
- package/src/pi/model-settings.ts +7 -26
- package/src/pi/model-support.ts +4 -19
- package/src/schemas/configuration.ts +8 -38
- package/src/schemas/findings.ts +0 -2
- package/src/schemas/task.ts +14 -0
- package/src/state/attachments.ts +20 -20
- package/src/state/blocking-requests.ts +23 -0
- package/src/state/comments.ts +25 -3
- package/src/state/persistence.ts +12 -1
- package/src/state/phase-timing.ts +53 -0
- package/src/state/previews.ts +15 -0
- package/src/state/task-state.ts +2 -0
- package/src/state/timing-merge.ts +25 -0
- package/src/webui/api/index.ts +26 -2
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/planner.ts +15 -2
- package/src/webui/api/prompts.ts +4 -4
- package/src/webui/api/quickfix.ts +1 -1
- package/src/webui/api/sessions.ts +2 -2
- package/src/webui/api/tasks.ts +72 -2
- package/src/webui/dev/fake-service.ts +19 -1
- package/src/webui/files.ts +15 -17
- package/src/webui/gateway.ts +64 -0
- package/src/webui/projects.ts +111 -0
- package/src/webui/protocol.ts +12 -0
- package/src/webui/server.ts +35 -6
- package/src/webui/uploads.ts +19 -9
- package/src/workflow/approvals.ts +3 -0
- package/src/workflow/workflow.ts +18 -3
- package/webui/dist/assets/index-BJI9lE5R.js +98 -0
- package/webui/dist/assets/index-Dg0tvkeY.css +2 -0
- package/webui/dist/build.json +131 -79
- package/webui/dist/index.html +3 -3
- package/webui/dist/sw.js +0 -1
- package/src/execution/fallback.ts +0 -75
- package/src/pi/master-fallback.ts +0 -61
- package/webui/dist/assets/index-BjUR1j07.css +0 -2
- package/webui/dist/assets/index-uDDAhAAQ.js +0 -98
- package/webui/dist/vite.svg +0 -1
package/src/lobby/service.ts
CHANGED
|
@@ -5,10 +5,18 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { SessionManager, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { updateBlockingRequest } from "../state/blocking-requests.ts";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { realpathSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { loadTask, ownedTask, peekTasks, saveTask } from "../state/persistence.ts";
|
|
13
|
+
import { refreshReview, deferReview } from "../delivery/review.ts";
|
|
14
|
+
import { deliver, type DeliveryRequest, type DeliveryStore } from "../delivery/operations.ts";
|
|
15
|
+
import type { LockIdentity } from "../delivery/lock.ts";
|
|
16
|
+
import { execCommand } from "./issues.ts";
|
|
9
17
|
import { releaseAttachments } from "../state/attachments.ts";
|
|
10
18
|
import { loadConfig, saveConfig as writeConfig } from "../state/project.ts";
|
|
11
|
-
import { addPlanComment, readPlanComments } from "../state/comments.ts";
|
|
19
|
+
import { addPlanComment, editComment, readPlanComments, type PlanComment } from "../state/comments.ts";
|
|
12
20
|
import { isAutoMode } from "../state/auto.ts";
|
|
13
21
|
import { sendToInbox, sendToSession as leaveForSession } from "../state/inbox.ts";
|
|
14
22
|
import { livePresence } from "../state/presence.ts";
|
|
@@ -40,6 +48,34 @@ import type { LiveSession, LobbyService, SwitchTarget } from "./host.ts";
|
|
|
40
48
|
import { pushNotice, type NoticeLevel } from "../webui/notices.ts";
|
|
41
49
|
import type { Runtime } from "./runtime.ts";
|
|
42
50
|
|
|
51
|
+
function deliveryTask(state: Runtime, taskId: string): Task {
|
|
52
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/.test(taskId) || taskId === "." || taskId === "..") throw new Error("Invalid task ID.");
|
|
53
|
+
const task = loadTask(state.root, state.configDir, taskId);
|
|
54
|
+
if (!task || task.state !== "completed" || !task.delivery) throw new Error("Completed task delivery review is unavailable in this project.");
|
|
55
|
+
return task;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function recoveryDeliveryStore(state: Runtime, identity: LockIdentity): DeliveryStore {
|
|
59
|
+
if (identity.configDir !== resolve(state.configDir) || realpathSync(identity.projectRoot) !== identity.projectRoot) throw new Error("Interrupted owner has an untrusted project/config identity.");
|
|
60
|
+
return deliveryStore({ ...state, root: identity.projectRoot }, identity.taskId);
|
|
61
|
+
}
|
|
62
|
+
function deliveryStore(state: Runtime, taskId: string): DeliveryStore {
|
|
63
|
+
let baseline: string | undefined;
|
|
64
|
+
return {
|
|
65
|
+
identity: { projectRoot: realpathSync(state.root), configDir: resolve(state.configDir) },
|
|
66
|
+
recoveryStore: (identity) => recoveryDeliveryStore(state, identity),
|
|
67
|
+
load: () => { const task = deliveryTask(state, taskId); baseline = JSON.stringify(task.delivery); return task; },
|
|
68
|
+
save: (task) => {
|
|
69
|
+
const latest = deliveryTask(state, taskId);
|
|
70
|
+
if (JSON.stringify(latest.delivery) !== baseline) throw new Error("Delivery changed during operation; reconcile before retrying.");
|
|
71
|
+
latest.delivery = structuredClone(task.delivery);
|
|
72
|
+
saveTask(state.root, state.configDir, latest);
|
|
73
|
+
baseline = JSON.stringify(latest.delivery);
|
|
74
|
+
lobbyTopics.bump("tasks");
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
/** The state background callbacks report to; set while the lobby service runs. */
|
|
44
80
|
let serviceState: Runtime | undefined;
|
|
45
81
|
|
|
@@ -82,6 +118,12 @@ function addComment(state: Runtime, taskId: string, text: string): string {
|
|
|
82
118
|
}
|
|
83
119
|
lobbyFeed.log("LOBBY", `comment on ${taskId}'s plan saved`, "info");
|
|
84
120
|
lobbyTopics.bump("plans");
|
|
121
|
+
lobbyTopics.bump("tasks");
|
|
122
|
+
return deliverComment(task, sessionId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function deliverComment(task: Task, sessionId: string): string {
|
|
126
|
+
const taskId = task.id;
|
|
85
127
|
if (task.ownerSessionId === sessionId) {
|
|
86
128
|
if (deliverComments() === 0) return `comment saved — it reaches the oracle once ${taskId} is resumed or restored`;
|
|
87
129
|
return `comment sent to the oracle — it will ${task.plan ? "amend the plan" : "revise the proposal"}`;
|
|
@@ -90,6 +132,26 @@ function addComment(state: Runtime, taskId: string, text: string): string {
|
|
|
90
132
|
return `comment saved — the session driving ${taskId} passes it to its oracle`;
|
|
91
133
|
}
|
|
92
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Correct a comment this session sent. Ownership is checked again here, not
|
|
137
|
+
* only at the API boundary: the session that wrote the comment is the only one
|
|
138
|
+
* that may change its words.
|
|
139
|
+
*/
|
|
140
|
+
function editOwnComment(state: Runtime, taskId: string, commentId: string, text: string): PlanComment {
|
|
141
|
+
const task = loadTask(state.root, state.configDir, taskId);
|
|
142
|
+
if (!task) throw new Error(`no task ${taskId}`);
|
|
143
|
+
const sessionId = state.ctx.sessionManager.getSessionId();
|
|
144
|
+
const comment = readPlanComments(state.root, state.configDir, taskId).find((entry) => entry.id === commentId);
|
|
145
|
+
if (!comment) throw new Error(`no comment ${commentId} on ${taskId}`);
|
|
146
|
+
if (!comment.by || comment.by !== sessionId) throw new Error(`only the session that wrote ${commentId} can edit it`);
|
|
147
|
+
const edited = editComment(state.root, state.configDir, taskId, commentId, text);
|
|
148
|
+
lobbyFeed.log("LOBBY", `comment ${commentId} on ${taskId}'s plan edited`, "info");
|
|
149
|
+
lobbyTopics.bump("plans");
|
|
150
|
+
lobbyTopics.bump("tasks");
|
|
151
|
+
deliverComment(task, sessionId);
|
|
152
|
+
return edited;
|
|
153
|
+
}
|
|
154
|
+
|
|
93
155
|
function failed(state: Runtime, what: string, error: Error): void {
|
|
94
156
|
lobbyFeed.log("LOBBY", `${what} — ${error.message}`, "error");
|
|
95
157
|
lobbyNotify(state, `bot-lobby: ${what} — ${error.message}`, "error");
|
|
@@ -184,6 +246,20 @@ function sessionsChanged(): void {
|
|
|
184
246
|
}
|
|
185
247
|
}
|
|
186
248
|
|
|
249
|
+
function dialogTracker(state: Runtime): (id: string, pending: boolean, sessionId?: string) => void {
|
|
250
|
+
const requests = new Map<string, { taskId: string; sessionId: string; requestId: string }>();
|
|
251
|
+
const prefix = `dialog:${randomUUID()}`;
|
|
252
|
+
return (id, pending, sessionId) => {
|
|
253
|
+
if (!sessionId) return;
|
|
254
|
+
const task = pending ? ownedTask(state.root, state.configDir, sessionId) : undefined;
|
|
255
|
+
if (task && !requests.has(id)) requests.set(id, { taskId: task.id, sessionId, requestId: `${prefix}:${id}` });
|
|
256
|
+
const request = requests.get(id);
|
|
257
|
+
if (!request) return;
|
|
258
|
+
updateBlockingRequest({ root: state.root, configDir: state.configDir, ...request }, request.requestId, pending);
|
|
259
|
+
if (!pending) requests.delete(id);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
187
263
|
/** Start a task in its own new pi session, named after the task; the session, or why not. */
|
|
188
264
|
function startSession(state: Runtime, start: { request?: string; plan?: PlannedTask; auto?: boolean }): BackgroundSession | string {
|
|
189
265
|
const request = start.request?.trim();
|
|
@@ -198,7 +274,7 @@ function startSession(state: Runtime, start: { request?: string; plan?: PlannedT
|
|
|
198
274
|
// A request may lead with flags (`--worktree add login`); the session is named after what follows them.
|
|
199
275
|
const name = taskName(plan?.title ?? (stripStartFlags(request!) || request!));
|
|
200
276
|
try {
|
|
201
|
-
const session = sessionRegistry().start(state.ctx.cwd, { name, ...(plan ? { planId: plan.id } : { request: request! }), ...(start.auto ? { auto: true } : {}) }, sessionModel(state.ctx));
|
|
277
|
+
const session = sessionRegistry().start(state.ctx.cwd, { name, projectRoot: state.root, onDialog: dialogTracker(state), ...(plan ? { planId: plan.id } : { request: request! }), ...(start.auto ? { auto: true } : {}) }, sessionModel(state.ctx));
|
|
202
278
|
lobbyFeed.log("LOBBY", `started "${name}" in a new session`, "success");
|
|
203
279
|
return session;
|
|
204
280
|
} catch (error) {
|
|
@@ -318,7 +394,7 @@ function archiveTask(state: Runtime, taskId: string): string {
|
|
|
318
394
|
if (busy) return busy;
|
|
319
395
|
try {
|
|
320
396
|
archiveTaskOnDisk(state.root, state.configDir, taskId);
|
|
321
|
-
releaseAttachments(taskId);
|
|
397
|
+
releaseAttachments(taskId, state.root);
|
|
322
398
|
} catch (error) {
|
|
323
399
|
return (error as Error).message;
|
|
324
400
|
}
|
|
@@ -347,7 +423,7 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
347
423
|
}
|
|
348
424
|
try {
|
|
349
425
|
deleteTaskOnDisk(state.root, state.configDir, taskId, where);
|
|
350
|
-
releaseAttachments(taskId);
|
|
426
|
+
releaseAttachments(taskId, state.root);
|
|
351
427
|
} catch (error) {
|
|
352
428
|
return (error as Error).message;
|
|
353
429
|
}
|
|
@@ -485,6 +561,7 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
485
561
|
};
|
|
486
562
|
let appliedMaster = masterKey();
|
|
487
563
|
return {
|
|
564
|
+
projectRoot: () => state.root,
|
|
488
565
|
sessionId: () => state.ctx.sessionManager.getSessionId(),
|
|
489
566
|
zen: () => {
|
|
490
567
|
const snapshot = taskSnapshot();
|
|
@@ -493,12 +570,33 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
493
570
|
feed: lobbyFeed,
|
|
494
571
|
masterBusy: () => !state.ctx.isIdle(),
|
|
495
572
|
tasks: () => peekTasks(state.root, state.configDir),
|
|
573
|
+
deliveryDeliver: (taskId, request: DeliveryRequest) => deliver(request, { cwd: state.root, exec: execCommand }, deliveryStore(state, taskId)),
|
|
574
|
+
deliveryReview: async (taskId) => {
|
|
575
|
+
const task = deliveryTask(state, taskId);
|
|
576
|
+
const baseline = JSON.stringify(task.delivery);
|
|
577
|
+
const delivery = await refreshReview(task, { cwd: state.root, exec: execCommand });
|
|
578
|
+
// An async lookup must not overwrite intervening deferrals or operations.
|
|
579
|
+
const latest = deliveryTask(state, taskId);
|
|
580
|
+
if (JSON.stringify(latest.delivery) !== baseline) throw new Error("Review changed during lookup; refresh again.");
|
|
581
|
+
latest.delivery = delivery;
|
|
582
|
+
saveTask(state.root, state.configDir, latest);
|
|
583
|
+
lobbyTopics.bump("tasks");
|
|
584
|
+
return delivery;
|
|
585
|
+
},
|
|
586
|
+
deliveryDefer: (taskId, reviewId) => {
|
|
587
|
+
const task = deliveryTask(state, taskId);
|
|
588
|
+
const delivery = deferReview(task, reviewId);
|
|
589
|
+
saveTask(state.root, state.configDir, task);
|
|
590
|
+
lobbyTopics.bump("tasks");
|
|
591
|
+
return delivery;
|
|
592
|
+
},
|
|
496
593
|
plans: () => listPlannedTasks(state.root, state.configDir),
|
|
497
594
|
comments: (taskId) => readPlanComments(state.root, state.configDir, taskId),
|
|
498
595
|
metrics: () => readMetrics(state.root, state.configDir),
|
|
499
596
|
classifierMetrics: () => readClassifierMetrics(state.root, state.configDir),
|
|
500
597
|
toOracle: (text) => toOracle(state, text),
|
|
501
598
|
comment: (taskId, text) => addComment(state, taskId, text),
|
|
599
|
+
editComment: (taskId, commentId, text) => editOwnComment(state, taskId, commentId, text),
|
|
502
600
|
startPlanned: (plan) => startPlanned(state, plan),
|
|
503
601
|
discardPlan: (id) => {
|
|
504
602
|
discardPlannedTask(state.root, state.configDir, id);
|
|
@@ -556,7 +654,7 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
556
654
|
workspace: () => state.workspace,
|
|
557
655
|
refreshWorkspace: () => refreshWorkspace(state),
|
|
558
656
|
sessionName: () => state.pi.getSessionName(),
|
|
559
|
-
sessions: () => backgroundSessions(),
|
|
657
|
+
sessions: () => backgroundSessions().filter((session) => session.projectRoot === state.root),
|
|
560
658
|
startSession: (start) => startSession(state, start),
|
|
561
659
|
isAuto: (taskId) => autoFor(state, taskId),
|
|
562
660
|
setAuto: (taskId, on) => switchAuto(state, taskId, on),
|
package/src/lobby/sessions.ts
CHANGED
|
@@ -39,6 +39,9 @@ export interface SessionProcess {
|
|
|
39
39
|
export type SessionStatus = "starting" | "idle" | "working" | "exited";
|
|
40
40
|
|
|
41
41
|
export interface SessionStart {
|
|
42
|
+
/** Server-owned task association; never supplied by a browser payload. */
|
|
43
|
+
onDialog?: (id: string, pending: boolean, sessionId?: string) => void;
|
|
44
|
+
projectRoot?: string;
|
|
42
45
|
/** The display name: the task's title. */
|
|
43
46
|
name: string;
|
|
44
47
|
/** What to send once it is up: the request, or a planned task to start. */
|
|
@@ -54,6 +57,7 @@ export const STOP_GRACE_MS = 5000;
|
|
|
54
57
|
|
|
55
58
|
export class BackgroundSession {
|
|
56
59
|
readonly key: string;
|
|
60
|
+
readonly projectRoot?: string;
|
|
57
61
|
readonly name: string;
|
|
58
62
|
/** The planned task it was started from. */
|
|
59
63
|
readonly planId?: string;
|
|
@@ -67,6 +71,7 @@ export class BackgroundSession {
|
|
|
67
71
|
exitCode?: number | null;
|
|
68
72
|
/** Dialogs the session waits on, oldest first. */
|
|
69
73
|
readonly dialogs: SessionDialog[] = [];
|
|
74
|
+
private readonly onDialog: NonNullable<SessionStart["onDialog"]>;
|
|
70
75
|
private readonly proc: SessionProcess;
|
|
71
76
|
private buffer = "";
|
|
72
77
|
private stderr = "";
|
|
@@ -79,7 +84,9 @@ export class BackgroundSession {
|
|
|
79
84
|
counter += 1;
|
|
80
85
|
this.key = `S${counter}`;
|
|
81
86
|
this.name = start.name;
|
|
87
|
+
this.projectRoot = start.projectRoot;
|
|
82
88
|
if (start.planId) this.planId = start.planId;
|
|
89
|
+
this.onDialog = start.onDialog ?? (() => {});
|
|
83
90
|
this.proc = proc;
|
|
84
91
|
this.onChange = onChange;
|
|
85
92
|
proc.stdout?.on("data", (chunk) => this.receive(String(chunk)));
|
|
@@ -102,6 +109,7 @@ export class BackgroundSession {
|
|
|
102
109
|
if (this.status === "exited") return;
|
|
103
110
|
this.status = "exited";
|
|
104
111
|
this.exitCode = code;
|
|
112
|
+
for (const dialog of this.dialogs) this.onDialog(dialog.id, false, this.sessionId);
|
|
105
113
|
this.dialogs.length = 0;
|
|
106
114
|
if (this.killTimer) clearTimeout(this.killTimer);
|
|
107
115
|
this.feed.say("note", reason);
|
|
@@ -148,6 +156,7 @@ export class BackgroundSession {
|
|
|
148
156
|
|
|
149
157
|
/** Stop the oracle's running turn (the session stays up). */
|
|
150
158
|
abort(): void {
|
|
159
|
+
for (const dialog of [...this.dialogs]) this.answer(dialog.id, { cancelled: true });
|
|
151
160
|
if (this.busy) this.write({ type: "abort" });
|
|
152
161
|
}
|
|
153
162
|
|
|
@@ -156,6 +165,7 @@ export class BackgroundSession {
|
|
|
156
165
|
const index = this.dialogs.findIndex((dialog) => dialog.id === id);
|
|
157
166
|
if (index < 0) return;
|
|
158
167
|
this.dialogs.splice(index, 1);
|
|
168
|
+
this.onDialog(id, false, this.sessionId);
|
|
159
169
|
if (this.alive && this.proc.stdin) this.proc.stdin.write(`${JSON.stringify({ type: "extension_ui_response", id, ...answer })}\n`);
|
|
160
170
|
this.onChange();
|
|
161
171
|
}
|
|
@@ -213,6 +223,7 @@ export class BackgroundSession {
|
|
|
213
223
|
if (event.command === "get_state" && data) {
|
|
214
224
|
this.sessionId = data.sessionId;
|
|
215
225
|
this.sessionFile = data.sessionFile;
|
|
226
|
+
for (const dialog of this.dialogs) this.onDialog(dialog.id, true, this.sessionId);
|
|
216
227
|
if (this.status === "starting") this.status = "idle";
|
|
217
228
|
} else if (event.success === false && typeof event.error === "string") {
|
|
218
229
|
this.feed.log("LOBBY", `the session refused a request: ${event.error}`, "warning");
|
|
@@ -240,6 +251,7 @@ export class BackgroundSession {
|
|
|
240
251
|
...(typeof event.placeholder === "string" ? { placeholder: event.placeholder } : {}),
|
|
241
252
|
...(typeof event.prefill === "string" ? { prefill: event.prefill } : {}),
|
|
242
253
|
});
|
|
254
|
+
this.onDialog(id, true, this.sessionId);
|
|
243
255
|
this.feed.log("LOBBY", `waiting for you: ${String(event.title ?? "a question").split("\n")[0]}`, "warning");
|
|
244
256
|
} else if (method === "notify") {
|
|
245
257
|
const level = event.notifyType === "error" ? "error" : event.notifyType === "warning" ? "warning" : "info";
|
package/src/lobby/task-rows.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/** The shapes of the Tasks tab's rows, shared by the web API and the page's words. */
|
|
2
2
|
|
|
3
|
+
import type { TimingProjection } from "../state/phase-timing.ts";
|
|
4
|
+
import type { Delivery } from "../delivery/types.ts";
|
|
5
|
+
|
|
3
6
|
export type TaskSection = "mine" | "others" | "pending" | "recent" | "archived";
|
|
4
7
|
|
|
5
8
|
/** How a row is ticked off: still to do (a plan, or a task under way), completed, or abandoned. */
|
|
6
9
|
export type CheckState = "open" | "done" | "dropped";
|
|
7
10
|
|
|
8
11
|
export interface TaskRow {
|
|
12
|
+
timing?: TimingProjection;
|
|
13
|
+
delivery?: Pick<Delivery, "status" | "reviewId">;
|
|
9
14
|
/** A task on the list, a saved plan, or a task in the archive. */
|
|
10
15
|
kind: "task" | "plan" | "archived";
|
|
11
16
|
id: string;
|
package/src/master/master.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface ScoutRequest {
|
|
|
52
52
|
/** Model, thinking and time limit for one run, from settings. */
|
|
53
53
|
function profileFields(config: BotLobbyConfig, resolver: ProfileResolver | undefined, domain: Domain, role: AgentRequest["role"]) {
|
|
54
54
|
const profile = profileFor(config, resolver, domain, role);
|
|
55
|
-
return { model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs
|
|
55
|
+
return { model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
package/src/master/research.ts
CHANGED
|
@@ -88,7 +88,6 @@ 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 } : {}),
|
|
92
91
|
...(request.time ? { time: request.time } : {}),
|
|
93
92
|
cwd: request.cwd,
|
|
94
93
|
signal: request.signal,
|
package/src/pi/events.ts
CHANGED
|
@@ -7,7 +7,6 @@ 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";
|
|
11
10
|
import { applyStatus, clearStatus, isMinimized, setMinimized } from "./ui.ts";
|
|
12
11
|
import { isSubagentProcess, webToolsFor } from "./quiet.ts";
|
|
13
12
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
@@ -65,7 +64,6 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
65
64
|
* section rather than replacing the whole prompt.
|
|
66
65
|
*/
|
|
67
66
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
68
|
-
registerMasterFallback(pi);
|
|
69
67
|
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
70
68
|
let hiddenWeb: string[] = [];
|
|
71
69
|
pi.on("session_start", (_event, ctx) => {
|
package/src/pi/model-settings.ts
CHANGED
|
@@ -26,14 +26,11 @@ export interface EntryView {
|
|
|
26
26
|
thinking?: string;
|
|
27
27
|
instructions?: string;
|
|
28
28
|
timeoutMs?: number;
|
|
29
|
-
/** The model this agent switches to when its own runs out of usage; unset = none. */
|
|
30
|
-
fallbackModel?: string;
|
|
31
|
-
fallbackThinking?: string;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
export function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView {
|
|
35
32
|
if (kind === "master") return config.master;
|
|
36
|
-
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs
|
|
33
|
+
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs };
|
|
37
34
|
if (kind === "researcher") return config.researcher;
|
|
38
35
|
if (kind === "quickfix") return config.quickFix;
|
|
39
36
|
if (kind === "planner") return config.planner;
|
|
@@ -45,33 +42,17 @@ interface EntryPatch {
|
|
|
45
42
|
thinking?: string;
|
|
46
43
|
instructions?: string;
|
|
47
44
|
timeoutMs?: number;
|
|
48
|
-
/** `inherit` clears the fallback (its thinking level with it). */
|
|
49
|
-
fallbackModel?: string;
|
|
50
|
-
fallbackThinking?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** A settings entry with the fallback patch applied: `inherit` removes the fallback model and its thinking. */
|
|
54
|
-
function withFallbackPatch<T extends { fallbackModel?: string; fallbackThinking?: string }>(entry: T): T {
|
|
55
|
-
if (entry.fallbackModel !== INHERIT_MODEL) return entry;
|
|
56
|
-
const { fallbackModel: _model, fallbackThinking: _thinking, ...rest } = entry;
|
|
57
|
-
return rest as T;
|
|
58
45
|
}
|
|
59
46
|
|
|
60
47
|
/** Apply a patch to one entry in a config copy; scouts ignore thinking and instructions. */
|
|
61
48
|
export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: EntryPatch): BotLobbyConfig {
|
|
62
49
|
const next: BotLobbyConfig = { ...config, agents: { ...config.agents } };
|
|
63
|
-
if (kind === "master") next.master =
|
|
64
|
-
else if (kind === "scout") {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...(fallbackModel ? { fallbackModel } : {}),
|
|
70
|
-
});
|
|
71
|
-
} else if (kind === "researcher") next.researcher = withFallbackPatch({ ...config.researcher, ...patch } as AgentModelConfig);
|
|
72
|
-
else if (kind === "quickfix") next.quickFix = withFallbackPatch({ ...config.quickFix, ...patch } as AgentModelConfig);
|
|
73
|
-
else if (kind === "planner") next.planner = withFallbackPatch({ ...config.planner, ...patch } as AgentModelConfig);
|
|
74
|
-
else next.agents[kind] = withFallbackPatch({ ...config.agents[kind], ...patch } as AgentModelConfig);
|
|
50
|
+
if (kind === "master") next.master = { ...config.master, ...patch } as AgentModelConfig;
|
|
51
|
+
else if (kind === "scout") next.scout = { model: patch.model ?? config.scout.model, timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs };
|
|
52
|
+
else if (kind === "researcher") next.researcher = { ...config.researcher, ...patch } as AgentModelConfig;
|
|
53
|
+
else if (kind === "quickfix") next.quickFix = { ...config.quickFix, ...patch } as AgentModelConfig;
|
|
54
|
+
else if (kind === "planner") next.planner = { ...config.planner, ...patch } as AgentModelConfig;
|
|
55
|
+
else next.agents[kind] = { ...config.agents[kind], ...patch } as AgentModelConfig;
|
|
75
56
|
return next;
|
|
76
57
|
}
|
|
77
58
|
|
package/src/pi/model-support.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type PanelMember,
|
|
9
9
|
THINKING_LEVELS,
|
|
10
10
|
type BotLobbyConfig,
|
|
11
|
-
type FallbackProfile,
|
|
12
11
|
type ProfileResolver,
|
|
13
12
|
type SubagentKind,
|
|
14
13
|
type ThinkingLevelName,
|
|
@@ -87,25 +86,11 @@ export function createProfileResolver(config: BotLobbyConfig, options: ResolverO
|
|
|
87
86
|
options.warn?.(message);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
|
-
return { ...profile, model, thinking: check.level
|
|
89
|
+
return { ...profile, model, thinking: check.level };
|
|
91
90
|
};
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
|
|
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 };
|
|
93
|
+
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string };
|
|
109
94
|
|
|
110
95
|
/** Unset models run on the session's; thinking is clamped to the model, with a one-time warning. */
|
|
111
96
|
function resolveRunProfile(profile: RunProfile, label: string, options: ResolverOptions): RunProfile {
|
|
@@ -118,7 +103,7 @@ function resolveRunProfile(profile: RunProfile, label: string, options: Resolver
|
|
|
118
103
|
options.warn?.(message);
|
|
119
104
|
}
|
|
120
105
|
}
|
|
121
|
-
return { ...profile, model, thinking: check.level
|
|
106
|
+
return { ...profile, model, thinking: check.level };
|
|
122
107
|
}
|
|
123
108
|
|
|
124
109
|
/** A lobby agent's model, thinking and time limit. */
|
|
@@ -137,7 +122,7 @@ export function resolvePanelProfile(config: BotLobbyConfig, member: PanelMember,
|
|
|
137
122
|
*/
|
|
138
123
|
export function resolveReviewProfile(config: BotLobbyConfig, options: ResolverOptions): RunProfile {
|
|
139
124
|
const profile = agentProfile(config, "qa", "reviewer");
|
|
140
|
-
return resolveRunProfile({ model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs, ...(profile.instructions ? { instructions: profile.instructions } : {})
|
|
125
|
+
return resolveRunProfile({ model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs, ...(profile.instructions ? { instructions: profile.instructions } : {}) }, kindLabel("qa"), options);
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
/** Every configured subagent whose thinking level its model does not support, for `/bot-lobby config`. */
|
|
@@ -31,18 +31,12 @@ 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;
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/** Scouts pick a model and a time limit only; their thinking is fixed at `SCOUT_THINKING`. */
|
|
41
37
|
export interface ScoutConfig {
|
|
42
38
|
model: ModelRef;
|
|
43
39
|
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;
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
/** Subagent kinds with their own settings entry. */
|
|
@@ -301,22 +295,18 @@ function positive(value: unknown): number | undefined {
|
|
|
301
295
|
|
|
302
296
|
/** Merge one agent's override over its default; legacy `inherit` or unknown thinking falls back to the default level. */
|
|
303
297
|
function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConfig> | undefined): AgentModelConfig {
|
|
304
|
-
const
|
|
298
|
+
const merged = { ...base, ...(override ?? {}) };
|
|
305
299
|
const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
|
|
306
300
|
const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
|
|
307
|
-
|
|
308
|
-
const fallbackThinking = typeof rawThinking === "string" && isThinkingLevel(rawThinking) ? rawThinking : undefined;
|
|
309
|
-
return { ...merged, thinking, ...(timeoutMs ? { timeoutMs } : {}), ...(fallbackModel ? { fallbackModel, ...(fallbackThinking ? { fallbackThinking } : {}) } : {}) };
|
|
301
|
+
return { model: merged.model, thinking, ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}) };
|
|
310
302
|
}
|
|
311
303
|
|
|
312
304
|
/** Scouts keep a model and a time limit; any thinking value in the file is dropped. */
|
|
313
305
|
function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig {
|
|
314
306
|
const base = DEFAULT_CONFIG.scout;
|
|
315
|
-
const fallback = override?.fallbackModel;
|
|
316
307
|
return {
|
|
317
308
|
model: typeof override?.model === "string" && override.model.trim() ? override.model : base.model,
|
|
318
309
|
timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
|
|
319
|
-
...(typeof fallback === "string" && fallback.trim() && fallback !== INHERIT_MODEL ? { fallbackModel: fallback.trim() } : {}),
|
|
320
310
|
};
|
|
321
311
|
}
|
|
322
312
|
|
|
@@ -441,22 +431,6 @@ export interface AgentProfile {
|
|
|
441
431
|
thinking: string;
|
|
442
432
|
timeoutMs: number;
|
|
443
433
|
instructions?: string;
|
|
444
|
-
/** Where the run goes when its model runs out of usage or is unavailable; absent = it just fails. */
|
|
445
|
-
fallback?: FallbackProfile;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** The fallback model and the thinking level to run it at. */
|
|
449
|
-
export interface FallbackProfile {
|
|
450
|
-
model: string;
|
|
451
|
-
thinking: string;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/** The fallback of a settings entry: its model and thinking (the entry's own level when none is set). */
|
|
455
|
-
export function fallbackOf(entry: { fallbackModel?: string; fallbackThinking?: string; thinking?: string }): FallbackProfile | undefined {
|
|
456
|
-
const model = modelOf(entry.fallbackModel ?? INHERIT_MODEL);
|
|
457
|
-
if (!model) return undefined;
|
|
458
|
-
const own = entry.thinking && isThinkingLevel(entry.thinking) ? entry.thinking : DEFAULT_THINKING;
|
|
459
|
-
return { model, thinking: entry.fallbackThinking && isThinkingLevel(entry.fallbackThinking) ? entry.fallbackThinking : own };
|
|
460
434
|
}
|
|
461
435
|
|
|
462
436
|
/** The settings entry a domain/role run draws from. */
|
|
@@ -481,12 +455,10 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
|
|
|
481
455
|
const instructions = config.agents[domain].instructions;
|
|
482
456
|
const fallback = config.workflow.agentTimeoutMs;
|
|
483
457
|
if (kind === "scout") {
|
|
484
|
-
|
|
485
|
-
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(scoutFallback ? { fallback: scoutFallback } : {}) };
|
|
458
|
+
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions };
|
|
486
459
|
}
|
|
487
460
|
const entry = kind === "researcher" ? config.researcher : config.agents[kind];
|
|
488
|
-
|
|
489
|
-
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entryFallback ? { fallback: entryFallback } : {}) };
|
|
461
|
+
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions };
|
|
490
462
|
}
|
|
491
463
|
|
|
492
464
|
/** The settings entry of a lobby agent (quick fix or planner). */
|
|
@@ -495,21 +467,19 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
|
|
|
495
467
|
}
|
|
496
468
|
|
|
497
469
|
/** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
|
|
498
|
-
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string
|
|
470
|
+
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
499
471
|
const entry = lobbyAgentConfig(config, kind);
|
|
500
|
-
|
|
501
|
-
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(fallback ? { fallback } : {}) };
|
|
472
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions };
|
|
502
473
|
}
|
|
503
474
|
|
|
504
475
|
/**
|
|
505
476
|
* A planning panel seat's profile: the domain's (or the researcher's) model,
|
|
506
477
|
* thinking and custom instructions, bounded by the planner's per-turn limit.
|
|
507
478
|
*/
|
|
508
|
-
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string
|
|
479
|
+
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
509
480
|
const entry = member === "researcher" ? config.researcher : config.agents[member];
|
|
510
481
|
const timeoutMs = config.planner.timeoutMs ?? config.workflow.agentTimeoutMs;
|
|
511
|
-
|
|
512
|
-
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(fallback ? { fallback } : {}) };
|
|
482
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions };
|
|
513
483
|
}
|
|
514
484
|
|
|
515
485
|
/** Resolves the model, thinking and time limit one subagent run uses. */
|
package/src/schemas/findings.ts
CHANGED
|
@@ -139,8 +139,6 @@ 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;
|
|
144
142
|
/** The classifier routed this run down: the configured profile it came from (`p/big · medium`). */
|
|
145
143
|
routedFrom?: string;
|
|
146
144
|
/** Why and where it was routed (`trivial 0.88: p/big · medium → p/cheap · low`). */
|
package/src/schemas/task.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Domain, Role } from "./agent.ts";
|
|
2
|
+
import type { Delivery } from "../delivery/types.ts";
|
|
2
3
|
|
|
3
4
|
export const TASK_STATES = [
|
|
4
5
|
"created",
|
|
@@ -171,7 +172,18 @@ export interface TaskGit {
|
|
|
171
172
|
path?: string;
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
export interface PhaseTiming {
|
|
176
|
+
phase: TaskState;
|
|
177
|
+
elapsedMs: number;
|
|
178
|
+
runningSince?: string;
|
|
179
|
+
blockingRequestIds: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
export interface Task {
|
|
183
|
+
/** Canonical waits, including legacy tasks without a reliable clock. */
|
|
184
|
+
blockingRequestIds?: string[];
|
|
185
|
+
phaseTiming?: PhaseTiming;
|
|
186
|
+
delivery?: Delivery;
|
|
175
187
|
id: string;
|
|
176
188
|
title: string;
|
|
177
189
|
/** The user's original request, kept in full while `title` stays a short label. */
|
|
@@ -226,6 +238,8 @@ export function createTask(
|
|
|
226
238
|
title,
|
|
227
239
|
request,
|
|
228
240
|
state: "created",
|
|
241
|
+
blockingRequestIds: [],
|
|
242
|
+
phaseTiming: { phase: "created", elapsedMs: 0, runningSince: now, blockingRequestIds: [] },
|
|
229
243
|
domains: [],
|
|
230
244
|
amendments: [],
|
|
231
245
|
paused: false,
|