@a-t-h-i/bot-lobby 0.6.1 → 0.6.3
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 +187 -834
- package/package.json +1 -1
- package/prompts/master.md +17 -0
- package/prompts/panel.md +3 -1
- package/prompts/planner.md +14 -0
- package/prompts/quickfix.md +3 -1
- package/prompts/scout.md +3 -0
- package/prompts/worker.md +3 -0
- package/src/classifier/answers.ts +110 -0
- package/src/classifier/classifier.ts +171 -0
- package/src/classifier/client.ts +239 -0
- package/src/classifier/effort.ts +143 -0
- package/src/classifier/files.ts +427 -0
- package/src/classifier/hosts.ts +157 -0
- package/src/classifier/instance.ts +98 -0
- package/src/classifier/limits.ts +37 -0
- package/src/classifier/seats.ts +102 -0
- package/src/classifier/tools.ts +58 -0
- package/src/classifier/triage.ts +203 -0
- package/src/execution/agent-runner.ts +5 -0
- package/src/index.ts +9 -0
- package/src/lobby/feed.ts +76 -8
- package/src/lobby/layout.ts +14 -13
- package/src/lobby/markdown.ts +61 -18
- package/src/lobby/planner.ts +305 -27
- package/src/lobby/quickfix.ts +120 -8
- package/src/lobby/runtime.ts +57 -10
- package/src/lobby/session-files.ts +162 -25
- package/src/lobby/sessions.ts +21 -7
- package/src/lobby/tabs/home.ts +219 -61
- package/src/lobby/tabs/metrics.ts +28 -1
- package/src/lobby/tabs/plan.ts +27 -4
- package/src/lobby/tabs/quickfix.ts +5 -1
- package/src/lobby/view.ts +140 -22
- package/src/master/master.ts +95 -29
- package/src/pi/commands.ts +4 -2
- package/src/pi/events.ts +18 -4
- package/src/pi/fresh-context.ts +134 -0
- package/src/pi/owner.ts +19 -10
- package/src/pi/run-summary.ts +3 -1
- package/src/pi/settings-ui.ts +138 -2
- package/src/pi/start-task.ts +7 -0
- package/src/pi/tools.ts +7 -2
- package/src/pi/ui.ts +3 -2
- package/src/pi/zen-large.ts +10 -10
- package/src/pi/zen.ts +6 -6
- package/src/schemas/configuration.ts +129 -1
- package/src/schemas/findings.ts +4 -0
- package/src/schemas/task.ts +22 -0
- package/src/state/archive.ts +12 -3
- package/src/state/backlog.ts +13 -3
- package/src/state/file-cache.ts +62 -0
- package/src/state/metrics.ts +137 -14
- package/src/state/persistence.ts +36 -2
- package/src/width.ts +102 -0
- package/src/workflow/workflow.ts +49 -1
package/src/lobby/view.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* pane toggles, paging) work in both. Everything bot-lobby-specific arrives
|
|
10
10
|
* through `LobbyHost`, so the view renders and reacts the same in tests.
|
|
11
11
|
*/
|
|
12
|
-
import { decodeKittyPrintable, Editor, Input, Key, matchesKey,
|
|
12
|
+
import { decodeKittyPrintable, Editor, Input, Key, matchesKey, type Component, type EditorTheme, type Focusable, type TUI, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
13
|
+
import { textWidth } from "../width.ts";
|
|
13
14
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
14
15
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
15
16
|
import type { PlannedTask } from "../state/backlog.ts";
|
|
16
17
|
import type { PlanComment } from "../state/comments.ts";
|
|
17
|
-
import { aggregateMetrics, collectMetrics, SORT_KEYS, sortGroups, taskStats, taskTimesByModel, type GroupBy, type MetricRecord, type SortKey } from "../state/metrics.ts";
|
|
18
|
+
import { aggregateMetrics, collectMetrics, SORT_KEYS, sortGroups, summarizeClassifier, taskStats, taskTimesByModel, type GroupBy, type MetricRecord, type SortKey } from "../state/metrics.ts";
|
|
18
19
|
import { PANEL_MEMBERS, type LobbyAgentKind, type LobbyPanel, type PanelMember } from "../schemas/configuration.ts";
|
|
19
20
|
import type { ChatEntry, LobbyFeed } from "./feed.ts";
|
|
20
21
|
import type { BackgroundSession } from "./sessions.ts";
|
|
@@ -23,7 +24,7 @@ import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } f
|
|
|
23
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
24
25
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
25
26
|
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
26
|
-
import {
|
|
27
|
+
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
27
28
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
28
29
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
29
30
|
import { filterJobs, newestFirst, renderQuickFix } from "./tabs/quickfix.ts";
|
|
@@ -113,6 +114,9 @@ function sameView(a: SessionView, b: SessionView): boolean {
|
|
|
113
114
|
return true;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** Tops a conversation whose earlier messages are not in memory. */
|
|
118
|
+
export const OLDER_NOTE = "↑ earlier messages load as you scroll up";
|
|
119
|
+
|
|
116
120
|
const SECTION_OF: Record<SessionWhere, string> = { "this window": "THIS WINDOW", background: "BACKGROUND", "other terminal": "OTHER TERMINALS", "not running": "NOT RUNNING" };
|
|
117
121
|
const WHERE_MARKS: Record<SessionWhere, string> = { "this window": "●", background: "◆", "other terminal": "◇", "not running": "○" };
|
|
118
122
|
|
|
@@ -133,6 +137,8 @@ export interface LobbyHost {
|
|
|
133
137
|
plans(): PlannedTask[];
|
|
134
138
|
comments(taskId: string): PlanComment[];
|
|
135
139
|
metrics(): MetricRecord[];
|
|
140
|
+
/** Classifier calls (kind `classifier`), kept out of the agent tables. */
|
|
141
|
+
classifierMetrics?(): MetricRecord[];
|
|
136
142
|
/** Send text to the oracle, or start a task when none is active; returns a notice. */
|
|
137
143
|
toOracle(text: string): string | undefined;
|
|
138
144
|
comment(taskId: string, text: string): string;
|
|
@@ -148,6 +154,8 @@ export interface LobbyHost {
|
|
|
148
154
|
answerPanel(): Promise<string>;
|
|
149
155
|
/** The seats a new session starts with, from settings. */
|
|
150
156
|
defaultPanel(): readonly PanelMember[];
|
|
157
|
+
/** Planning rounds before the oracle finalizes alone (`lobby.maxPlanningRounds`); 0 = unlimited. */
|
|
158
|
+
planningRounds?(): number;
|
|
151
159
|
/** `model · thinking` a panel seat runs on. */
|
|
152
160
|
seatLabel(member: PanelMember): string;
|
|
153
161
|
issues: IssuesState;
|
|
@@ -185,8 +193,12 @@ export interface LobbyHost {
|
|
|
185
193
|
archiveTask(taskId: string): string;
|
|
186
194
|
restoreTask(taskId: string): string;
|
|
187
195
|
deleteTask(taskId: string, where: "list" | "archive"): string;
|
|
188
|
-
/** Another session's conversation, read from its saved session file. */
|
|
196
|
+
/** Another session's conversation, read from its saved session file (its newest messages). */
|
|
189
197
|
sessionChat(sessionId: string): ChatEntry[];
|
|
198
|
+
/** Whether another session has messages earlier than `sessionChat` returns. */
|
|
199
|
+
hasOlderChat(sessionId: string): boolean;
|
|
200
|
+
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
201
|
+
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
190
202
|
/** A task's status without animations, for a session other than this window's. */
|
|
191
203
|
taskScene(task: Task, width: number, height: number): string[];
|
|
192
204
|
profileLabel(kind: LobbyAgentKind): string;
|
|
@@ -212,7 +224,7 @@ class LobbyEditor extends Editor {
|
|
|
212
224
|
if (!this.label) return super.renderTopBorder(width, hiddenLineCount);
|
|
213
225
|
const label = ` ${this.label} `;
|
|
214
226
|
const more = hiddenLineCount > 0 ? ` ↑${hiddenLineCount} ` : "";
|
|
215
|
-
const rest = width - 2 -
|
|
227
|
+
const rest = width - 2 - textWidth(label) - textWidth(more);
|
|
216
228
|
if (rest < 1) return super.renderTopBorder(width, hiddenLineCount);
|
|
217
229
|
return `${this.borderColor("──")}${this.paintLabel(label)}${this.borderColor("─".repeat(rest))}${more ? this.borderColor(more) : ""}`;
|
|
218
230
|
}
|
|
@@ -336,12 +348,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
336
348
|
|
|
337
349
|
/** The Tasks tab lists archived tasks too (v). */
|
|
338
350
|
showArchived = false;
|
|
339
|
-
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; at: number } = {
|
|
351
|
+
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; classifierMetrics: MetricRecord[]; at: number } = {
|
|
340
352
|
tasks: [],
|
|
341
353
|
plans: [],
|
|
342
354
|
archived: [],
|
|
343
355
|
comments: new Map(),
|
|
344
356
|
metrics: [],
|
|
357
|
+
classifierMetrics: [],
|
|
345
358
|
at: 0,
|
|
346
359
|
};
|
|
347
360
|
|
|
@@ -425,12 +438,14 @@ export class LobbyView implements Component, Focusable {
|
|
|
425
438
|
refreshData(force: boolean): void {
|
|
426
439
|
const now = this.now();
|
|
427
440
|
if (!force && now - this.data.at < DATA_REFRESH_MS) return;
|
|
441
|
+
this.dataVersion += 1;
|
|
428
442
|
this.data = {
|
|
429
443
|
tasks: this.host.tasks(),
|
|
430
444
|
plans: this.host.plans(),
|
|
431
445
|
archived: this.showArchived ? this.host.archivedTasks() : [],
|
|
432
446
|
comments: new Map(),
|
|
433
447
|
metrics: this.tab === "metrics" ? this.host.metrics() : this.data.metrics,
|
|
448
|
+
classifierMetrics: this.tab === "metrics" ? this.host.classifierMetrics?.() ?? [] : this.data.classifierMetrics,
|
|
434
449
|
at: now,
|
|
435
450
|
};
|
|
436
451
|
if (this.tab === "tasks") {
|
|
@@ -832,6 +847,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
832
847
|
break;
|
|
833
848
|
}
|
|
834
849
|
this.clampScroll();
|
|
850
|
+
if (pane === "conversation") this.followHistory();
|
|
835
851
|
}
|
|
836
852
|
|
|
837
853
|
/** Home and End: the oldest or first line of the focused pane, or back to its newest or last. */
|
|
@@ -855,6 +871,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
855
871
|
return;
|
|
856
872
|
}
|
|
857
873
|
this.scrollPane(pane, far);
|
|
874
|
+
// Home loads earlier messages at the top; go on to the very first of them.
|
|
875
|
+
if (start && pane === "conversation" && this.history) this.homeOffsets.conversation = Number.MAX_SAFE_INTEGER;
|
|
858
876
|
}
|
|
859
877
|
|
|
860
878
|
/** The wheel over the draft scrolls it; the cursor follows only when it would leave the screen. */
|
|
@@ -948,7 +966,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
948
966
|
const rows = this.taskRowList();
|
|
949
967
|
this.tasksSelected = Math.max(0, Math.min(rows.length - 1, index));
|
|
950
968
|
this.tasksDetailOffset = 0;
|
|
951
|
-
|
|
969
|
+
// Only the newly selected task's comments: moving through the list must not reread every task from disk.
|
|
970
|
+
const row = rows[this.tasksSelected];
|
|
971
|
+
if (row?.kind === "task" && !this.data.comments.has(row.id)) this.data.comments.set(row.id, this.host.comments(row.id));
|
|
952
972
|
}
|
|
953
973
|
|
|
954
974
|
private async loadIssueDetail(): Promise<void> {
|
|
@@ -998,6 +1018,17 @@ export class LobbyView implements Component, Focusable {
|
|
|
998
1018
|
return true;
|
|
999
1019
|
}
|
|
1000
1020
|
if (data === "m") return this.openSettings("quickfix"), true;
|
|
1021
|
+
if (data === "r" || data === "t") {
|
|
1022
|
+
const job = this.fixJobs()[this.fixSelected];
|
|
1023
|
+
// Only a held job takes r and t; otherwise the key resumes typing like any other.
|
|
1024
|
+
if (!job || job.status !== "held") return false;
|
|
1025
|
+
if (data === "r" && this.host.quickfix.runAnyway(job.id)) this.say(`running ${job.id} anyway`);
|
|
1026
|
+
if (data === "t") {
|
|
1027
|
+
this.startInNewSession({ request: job.prompt });
|
|
1028
|
+
this.host.quickfix.movedToTask(job.id);
|
|
1029
|
+
}
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1001
1032
|
return false;
|
|
1002
1033
|
case "issues":
|
|
1003
1034
|
return this.issuesCommand(data, enter, escape);
|
|
@@ -1188,6 +1219,50 @@ export class LobbyView implements Component, Focusable {
|
|
|
1188
1219
|
void this.host.answerDialog(session).then(() => this.host.requestRender());
|
|
1189
1220
|
}
|
|
1190
1221
|
|
|
1222
|
+
/* -------------------------------------------------------------- history */
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* The whole conversation of the session in view, loaded when the
|
|
1226
|
+
* conversation pane is scrolled back past the messages kept in memory, and
|
|
1227
|
+
* let go once it is back at the newest.
|
|
1228
|
+
*/
|
|
1229
|
+
private history?: { view: SessionView; entries: readonly ChatEntry[] };
|
|
1230
|
+
|
|
1231
|
+
/** The conversation to draw for the session in view, and whether earlier messages wait to be loaded. */
|
|
1232
|
+
private conversationFor(entry: SessionEntry): { chat: readonly ChatEntry[]; older: boolean } {
|
|
1233
|
+
if (this.history && sameView(this.history.view, entry.view)) return { chat: this.history.entries, older: false };
|
|
1234
|
+
if (entry.view.kind === "here") return { chat: this.host.feed.chat, older: this.host.feed.chatOlder };
|
|
1235
|
+
if (entry.view.kind === "background") {
|
|
1236
|
+
const session = this.viewedSessionFor(entry.view.key);
|
|
1237
|
+
return { chat: session?.feed.chat ?? [], older: Boolean(session?.feed.chatOlder && session.sessionId) };
|
|
1238
|
+
}
|
|
1239
|
+
return entry.sessionId ? { chat: this.host.sessionChat(entry.sessionId), older: this.host.hasOlderChat(entry.sessionId) } : { chat: [], older: false };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* After the conversation pane scrolls: at its top, with earlier messages
|
|
1244
|
+
* waiting, load the whole conversation; back at its newest, let it go.
|
|
1245
|
+
* True when history was loaded now.
|
|
1246
|
+
*/
|
|
1247
|
+
private followHistory(): boolean {
|
|
1248
|
+
if (this.tab !== "lobby") return false;
|
|
1249
|
+
if (this.homeOffsets.conversation === 0) {
|
|
1250
|
+
this.history = undefined;
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
if (this.history && sameView(this.history.view, this.viewing)) return false;
|
|
1254
|
+
const pane = this.panes.get("conversation");
|
|
1255
|
+
if (!pane || this.homeOffsets.conversation < pane.total - pane.rows) return false;
|
|
1256
|
+
const entry = this.viewedEntry();
|
|
1257
|
+
if (!this.conversationFor(entry).older) return false;
|
|
1258
|
+
const sessionId = entry.view.kind === "here" ? undefined : entry.view.kind === "background" ? this.viewedSessionFor(entry.view.key)?.sessionId : entry.sessionId;
|
|
1259
|
+
if (entry.view.kind !== "here" && !sessionId) return false;
|
|
1260
|
+
const entries = this.host.chatHistory(sessionId);
|
|
1261
|
+
if (entries.length === 0) return false;
|
|
1262
|
+
this.history = { view: entry.view, entries };
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1191
1266
|
/* ------------------------------------------------------------ sessions */
|
|
1192
1267
|
|
|
1193
1268
|
/** Every session the lobby can show: this window, the background sessions it started, then tasks other terminals drive. */
|
|
@@ -1253,6 +1328,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1253
1328
|
this.viewing = target;
|
|
1254
1329
|
this.picking = false;
|
|
1255
1330
|
this.newSession = false;
|
|
1331
|
+
this.history = undefined;
|
|
1256
1332
|
for (const pane of HOME_PANES) this.homeOffsets[pane] = 0;
|
|
1257
1333
|
if (this.tab !== "lobby") this.setTab("lobby");
|
|
1258
1334
|
this.setMode("type");
|
|
@@ -1433,7 +1509,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1433
1509
|
seated = !seats.delete(seat);
|
|
1434
1510
|
if (seated) seats.add(seat);
|
|
1435
1511
|
}
|
|
1436
|
-
this.say(`${MEMBER_LABELS[seat]} ${seated ? "joins" : "leaves
|
|
1512
|
+
this.say(`${MEMBER_LABELS[seat]} ${seated ? "joins the panel from the next round and sits every round" : "leaves the panel from the next round"}`);
|
|
1437
1513
|
return true;
|
|
1438
1514
|
}
|
|
1439
1515
|
if (data === "x" || (escape && session?.busy)) {
|
|
@@ -1640,8 +1716,41 @@ export class LobbyView implements Component, Focusable {
|
|
|
1640
1716
|
|
|
1641
1717
|
/* -------------------------------------------------------------- render */
|
|
1642
1718
|
|
|
1719
|
+
/** Bumped each time the data is reread. */
|
|
1720
|
+
private dataVersion = 0;
|
|
1721
|
+
/** Task rows as last built: several parts of a frame ask, and they only change with the data, the search or the clock. */
|
|
1722
|
+
private rowsMemo?: { key: string; rows: TaskRow[] };
|
|
1723
|
+
|
|
1724
|
+
/** The Metrics tab's figures as last computed: they change only with the data, the grouping, the order or the search. */
|
|
1725
|
+
private metricsMemo?: { key: string; figures: ReturnType<LobbyView["computeMetrics"]> };
|
|
1726
|
+
|
|
1727
|
+
private metricsFigures(query: string | undefined) {
|
|
1728
|
+
const key = `${this.dataVersion}|${this.metricsBy}|${this.metricsSort}|${query ?? ""}`;
|
|
1729
|
+
if (this.metricsMemo?.key !== key) this.metricsMemo = { key, figures: this.computeMetrics(query) };
|
|
1730
|
+
return this.metricsMemo.figures;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
private computeMetrics(query: string | undefined) {
|
|
1734
|
+
const records = filterRecords(collectMetrics(this.data.metrics, this.data.tasks), query);
|
|
1735
|
+
return {
|
|
1736
|
+
records,
|
|
1737
|
+
groups: sortGroups(aggregateMetrics(records, this.metricsBy), this.metricsSort),
|
|
1738
|
+
taskTimes: taskTimesByModel(this.data.tasks, records),
|
|
1739
|
+
stats: taskStats(this.data.tasks),
|
|
1740
|
+
jev: summarizeClassifier(this.data.classifierMetrics, records),
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1643
1744
|
/** Task rows, narrowed by the Tasks tab's search. */
|
|
1644
1745
|
private taskRowList(): TaskRow[] {
|
|
1746
|
+
const key = `${this.dataVersion}|${this.tick}|${this.showArchived}|${this.queries.tasks ?? ""}`;
|
|
1747
|
+
if (this.rowsMemo?.key === key) return this.rowsMemo.rows;
|
|
1748
|
+
const rows = this.buildTaskRows();
|
|
1749
|
+
this.rowsMemo = { key, rows };
|
|
1750
|
+
return rows;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private buildTaskRows(): TaskRow[] {
|
|
1645
1754
|
const names = new Map<string, string>();
|
|
1646
1755
|
for (const session of this.host.sessions()) if (session.sessionId && session.alive) names.set(session.sessionId, session.name);
|
|
1647
1756
|
const auto = new Set(this.data.tasks.filter((task) => !TERMINAL_STATES.includes(task.state) && this.host.isAuto(task.id)).map((task) => task.id));
|
|
@@ -1674,12 +1783,12 @@ export class LobbyView implements Component, Focusable {
|
|
|
1674
1783
|
const badge = badges[tab] ? ` ${paint(theme, tab === "plan" && session?.awaitingAnswers ? "warning" : "accent", badges[tab]!)}` : "";
|
|
1675
1784
|
let cell = ` ${number} ${name}${badge} `;
|
|
1676
1785
|
if (tab === this.tab) cell = theme.bg ? theme.bg("selectedBg", cell) : `[${cell}]`;
|
|
1677
|
-
const from =
|
|
1786
|
+
const from = textWidth(left);
|
|
1678
1787
|
left += cell;
|
|
1679
|
-
spans.push({ tab, from, to:
|
|
1788
|
+
spans.push({ tab, from, to: textWidth(left) });
|
|
1680
1789
|
}
|
|
1681
1790
|
this.tabSpans = spans;
|
|
1682
|
-
const room = width -
|
|
1791
|
+
const room = width - textWidth(left) - 1;
|
|
1683
1792
|
const entry = this.viewedEntry();
|
|
1684
1793
|
const help = paint(theme, "dim", `${keyLabel(this.keys.help)} keys `);
|
|
1685
1794
|
const auto = entry.auto ? ` ${bold(theme, paint(theme, "success", "⟳ AUTO"))}` : "";
|
|
@@ -1700,8 +1809,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1700
1809
|
? [`${dot} ${zen.task.id} ${state}${auto}${asking} ${help}`, `${dot} ${zen.task.id} ${state}${auto}${asking} `, `${dot} ${state}${auto} `, `${dot}${auto} `]
|
|
1701
1810
|
: [`${paint(theme, "dim", "no task in this session")}${asking} ${help}`, `${paint(theme, "dim", "no task in this session")}${asking} `, help, ""];
|
|
1702
1811
|
}
|
|
1703
|
-
const status = choices.find((choice) =>
|
|
1704
|
-
const gap = width -
|
|
1812
|
+
const status = choices.find((choice) => textWidth(choice) <= room) ?? "";
|
|
1813
|
+
const gap = width - textWidth(left) - textWidth(status);
|
|
1705
1814
|
return gap >= 1 ? `${left}${" ".repeat(gap)}${status}` : fit(left, width);
|
|
1706
1815
|
}
|
|
1707
1816
|
|
|
@@ -1763,7 +1872,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1763
1872
|
const tail = this.searching
|
|
1764
1873
|
? paint(theme, "dim", " enter keep · esc clear · ↑↓ browse results ")
|
|
1765
1874
|
: paint(theme, "dim", ` ${keyLabel(this.keys.search)} or / edits · esc clears `);
|
|
1766
|
-
const room = Math.max(4, width -
|
|
1875
|
+
const room = Math.max(4, width - textWidth(lead) - textWidth(tail));
|
|
1767
1876
|
const field = this.searching ? this.search.render(room)[0] ?? "" : bold(theme, fit(query ?? "", room));
|
|
1768
1877
|
return [fit(`${lead}${fit(field, room)}${tail}`, width)];
|
|
1769
1878
|
}
|
|
@@ -1901,6 +2010,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1901
2010
|
break;
|
|
1902
2011
|
case "quickfix":
|
|
1903
2012
|
if (this.host.quickfix.jobs.length > 0) keys.push(["↑↓", "select"], ["x", "cancel"]);
|
|
2013
|
+
if (this.fixJobs()[this.fixSelected]?.status === "held") keys.push(["r", "run anyway"], ["t", "as a task"]);
|
|
1904
2014
|
keys.push(["m", "model"]);
|
|
1905
2015
|
break;
|
|
1906
2016
|
case "issues":
|
|
@@ -1971,6 +2081,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1971
2081
|
...(session ? { session: this.planView(session) } : {}),
|
|
1972
2082
|
profile: this.host.profileLabel("planner"),
|
|
1973
2083
|
seats: this.seatViews(undefined),
|
|
2084
|
+
limit: this.host.planningRounds?.() ?? 0,
|
|
1974
2085
|
offset: this.planOffset,
|
|
1975
2086
|
focus: this.planFocus,
|
|
1976
2087
|
draftOffset: this.planDraftOffset,
|
|
@@ -1999,11 +2110,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
1999
2110
|
}, width, height, theme);
|
|
2000
2111
|
}
|
|
2001
2112
|
case "metrics": {
|
|
2002
|
-
const records
|
|
2003
|
-
const groups = sortGroups(aggregateMetrics(records, this.metricsBy), this.metricsSort);
|
|
2113
|
+
const { records, groups, taskTimes, stats, jev } = this.metricsFigures(query);
|
|
2004
2114
|
this.metricsSelected = Math.min(this.metricsSelected, Math.max(0, groups.length - 1));
|
|
2005
|
-
|
|
2006
|
-
return renderMetrics({ groups, taskTimes, records, stats: taskStats(this.data.tasks), by: this.metricsBy, sort: this.metricsSort, selected: this.metricsSelected, ...(query ? { query } : {}), panes: this.panes }, width, height, theme);
|
|
2115
|
+
return renderMetrics({ groups, taskTimes, records, stats, by: this.metricsBy, sort: this.metricsSort, selected: this.metricsSelected, ...(query ? { query } : {}), panes: this.panes, ...(jev ? { classifier: jev } : {}) }, width, height, theme);
|
|
2007
2116
|
}
|
|
2008
2117
|
}
|
|
2009
2118
|
}
|
|
@@ -2034,6 +2143,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2034
2143
|
questions: session?.questions.filter((question) => question.from === MEMBER_LABELS[member]).length ?? 0,
|
|
2035
2144
|
ready: state?.reply?.status === "ready",
|
|
2036
2145
|
profile: this.host.seatLabel(member),
|
|
2146
|
+
...(!state && session?.satOut.has(member) ? { satOut: session.satOut.get(member)! } : {}),
|
|
2147
|
+
...(session?.pins.has(member) ? { pinned: true } : {}),
|
|
2037
2148
|
};
|
|
2038
2149
|
});
|
|
2039
2150
|
return [oracle, ...members];
|
|
@@ -2050,6 +2161,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2050
2161
|
...(session.step ? { step: session.step } : {}),
|
|
2051
2162
|
...(session.error ? { error: session.error } : {}),
|
|
2052
2163
|
turns: session.turns,
|
|
2164
|
+
limit: session.limit,
|
|
2165
|
+
nextMode: session.nextMode,
|
|
2053
2166
|
...(session.seed ? { seed: session.seed } : {}),
|
|
2054
2167
|
...(session.saved ? { saved: session.saved } : {}),
|
|
2055
2168
|
...(session.title ? { title: session.title } : {}),
|
|
@@ -2126,7 +2239,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2126
2239
|
const room = height - lines.length - 2;
|
|
2127
2240
|
if (room > 2) {
|
|
2128
2241
|
lines.push("", rule(width, "Conversation", theme));
|
|
2129
|
-
const talk = chat.length > 0 ?
|
|
2242
|
+
const talk = chat.length > 0 ? chatTail(chat, width, theme, room - 1).lines : [paint(theme, "dim", "Nothing said yet.")];
|
|
2130
2243
|
lines.push(...talk.slice(-(room - 1)));
|
|
2131
2244
|
}
|
|
2132
2245
|
return lines;
|
|
@@ -2151,9 +2264,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
2151
2264
|
const query = this.query();
|
|
2152
2265
|
const entry = this.viewedEntry();
|
|
2153
2266
|
if (entry.view.kind !== "here") return this.otherSessionBody(entry, width, height, theme, now);
|
|
2267
|
+
const talk = this.conversationFor(entry);
|
|
2154
2268
|
return renderHome({
|
|
2155
2269
|
...(zen.task ? { task: zen.task, scene: (w: number, h: number, animated: boolean) => this.host.scene(w, h, animated) } : {}),
|
|
2156
|
-
chat:
|
|
2270
|
+
chat: talk.chat,
|
|
2271
|
+
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2157
2272
|
...(feed.reply ? { liveReply: feed.reply } : {}),
|
|
2158
2273
|
activity: feed.activity,
|
|
2159
2274
|
thoughts: feed.thoughts,
|
|
@@ -2183,6 +2298,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2183
2298
|
*/
|
|
2184
2299
|
private otherSessionBody(entry: SessionEntry, width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
2185
2300
|
const session = this.viewedSession();
|
|
2301
|
+
const talk = this.conversationFor(entry);
|
|
2186
2302
|
const task = entry.task;
|
|
2187
2303
|
const query = this.query();
|
|
2188
2304
|
const common = {
|
|
@@ -2208,7 +2324,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2208
2324
|
const emptyNote = session.status === "starting" ? `starting ${session.name}…` : !session.alive ? `${session.name} has ended.` : `${session.name} has not said anything yet.`;
|
|
2209
2325
|
return renderHome({
|
|
2210
2326
|
...common,
|
|
2211
|
-
chat:
|
|
2327
|
+
chat: talk.chat,
|
|
2328
|
+
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2212
2329
|
...(session.feed.reply ? { liveReply: session.feed.reply } : {}),
|
|
2213
2330
|
activity: session.feed.activity,
|
|
2214
2331
|
thoughts: session.feed.thoughts,
|
|
@@ -2219,7 +2336,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2219
2336
|
const running = entry.where === "other terminal";
|
|
2220
2337
|
return renderHome({
|
|
2221
2338
|
...common,
|
|
2222
|
-
chat:
|
|
2339
|
+
chat: talk.chat,
|
|
2340
|
+
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2223
2341
|
activity: [],
|
|
2224
2342
|
thoughts: [],
|
|
2225
2343
|
busy: false,
|
package/src/master/master.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { parseWorkerResult, validateWorkerResult } from "../roles/worker.ts";
|
|
|
13
13
|
import { parseReviewResult, validateReviewResult } from "../roles/reviewer.ts";
|
|
14
14
|
import { truncate } from "../text.ts";
|
|
15
15
|
import { isScoutResultUsable, parseScoutResult, validateScoutResult } from "../roles/scout.ts";
|
|
16
|
+
import type { FileHinter } from "../classifier/files.ts";
|
|
17
|
+
import { fellShort, profileLabel, routeLabel, type EffortRoute, type EffortRouter } from "../classifier/effort.ts";
|
|
16
18
|
|
|
17
19
|
export interface ScoutOutcome {
|
|
18
20
|
result: ScoutResult;
|
|
@@ -36,6 +38,10 @@ export interface ScoutRequest {
|
|
|
36
38
|
profile?: ProfileResolver;
|
|
37
39
|
signal?: AbortSignal;
|
|
38
40
|
onUpdate?: (run: AgentRun) => void;
|
|
41
|
+
/** Likely files for each scout's context, and the lookup tool, while the classifier's file hints are on. */
|
|
42
|
+
hints?: FileHinter;
|
|
43
|
+
/** Moves a trivial scout to the cheaper model, while the classifier's effort routing is on. */
|
|
44
|
+
effort?: EffortRouter;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/** Model, thinking and time limit for one run, from settings. */
|
|
@@ -53,16 +59,41 @@ function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
|
|
53
59
|
].join("\n");
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
function scoutContext(request: ScoutRequest, domain: Domain): AgentRequest["context"] {
|
|
62
|
+
function scoutContext(request: ScoutRequest, domain: Domain, likely = ""): AgentRequest["context"] {
|
|
57
63
|
const slices = readAgentKnowledge(request.dataRoots, domain);
|
|
58
64
|
return {
|
|
59
65
|
task: request.taskText,
|
|
60
66
|
...selectKnowledge(`${request.taskText} ${domainSpec(domain).scoutFocus}`, slices),
|
|
61
67
|
instructions: request.config.agents[domain].instructions,
|
|
62
|
-
workflowContext: `Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`,
|
|
68
|
+
workflowContext: [`Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`, likely].filter(Boolean).join("\n\n"),
|
|
63
69
|
};
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
/** Where the classifier routes a run, or undefined (no router, no route, a failure). */
|
|
73
|
+
async function routeFor(effort: EffortRouter | undefined, instruction: string, profile: { model?: string; thinking: string }, options: { thinkingFixed?: boolean; context?: string; signal?: AbortSignal }): Promise<EffortRoute | undefined> {
|
|
74
|
+
if (!effort) return undefined;
|
|
75
|
+
try {
|
|
76
|
+
return await effort.route(instruction, profile, options);
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The run fields a route changes: model, thinking, no in-place retry (the fallback is the retry), and its label. */
|
|
83
|
+
function routedFields(route: EffortRoute): Pick<AgentRequest, "model" | "thinking" | "retries" | "routedFrom" | "route"> {
|
|
84
|
+
return { model: route.model, thinking: route.thinking, retries: 0, routedFrom: profileLabel(route.from), route: routeLabel(route) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The Likely files block for one agent's step, or "" without hints. */
|
|
88
|
+
async function likelyFor(hints: FileHinter | undefined, query: string, signal: AbortSignal | undefined, context?: string): Promise<string> {
|
|
89
|
+
if (!hints) return "";
|
|
90
|
+
try {
|
|
91
|
+
return await hints.block(query, signal, context);
|
|
92
|
+
} catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
function toOutcome(run: AgentRun, domain: Domain): ScoutOutcome {
|
|
67
98
|
if (run.status !== "success") {
|
|
68
99
|
return {
|
|
@@ -78,20 +109,36 @@ function toOutcome(run: AgentRun, domain: Domain): ScoutOutcome {
|
|
|
78
109
|
|
|
79
110
|
/** Run the selected domain scouts concurrently and persist their findings. */
|
|
80
111
|
export async function runScouts(request: ScoutRequest, run: ProcessRunner = spawnPiProcess): Promise<ScoutOutcome[]> {
|
|
81
|
-
|
|
112
|
+
// Each scout gets the files most likely to answer its own instruction and focus.
|
|
113
|
+
const likely = await Promise.all(request.domains.map((domain) => likelyFor(request.hints, `${request.instruction}\n${domainSpec(domain).scoutFocus}`, request.signal, request.taskText)));
|
|
114
|
+
const extraTools = request.hints?.tools() ?? [];
|
|
115
|
+
const requests: AgentRequest[] = request.domains.map((domain, index) => ({
|
|
82
116
|
taskId: request.taskId,
|
|
83
117
|
domain,
|
|
84
118
|
role: "scout",
|
|
85
119
|
instruction: scoutInstruction(request, domain),
|
|
86
|
-
context: scoutContext(request, domain),
|
|
120
|
+
context: scoutContext(request, domain, likely[index]),
|
|
121
|
+
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
87
122
|
...profileFields(request.config, request.profile, domain, "scout"),
|
|
88
123
|
cwd: request.cwd,
|
|
89
124
|
signal: request.signal,
|
|
90
125
|
onUpdate: request.onUpdate,
|
|
91
126
|
...watchdogOptions(request.config.workflow),
|
|
92
127
|
}));
|
|
93
|
-
|
|
94
|
-
const
|
|
128
|
+
// Scouts think at a fixed level; a trivial one may move to the cheaper model.
|
|
129
|
+
const routes = await Promise.all(requests.map((entry) => routeFor(request.effort, entry.instruction, { ...(entry.model ? { model: entry.model } : {}), thinking: entry.thinking ?? "low" }, { thinkingFixed: true, context: request.taskText, ...(request.signal ? { signal: request.signal } : {}) })));
|
|
130
|
+
const first = requests.map((entry, index) => (routes[index] ? { ...entry, ...routedFields(routes[index]!) } : entry));
|
|
131
|
+
const runs = await runParallel(first, request.config.workflow.maxParallelScouts, run);
|
|
132
|
+
let outcomes = runs.map((agentRun) => toOutcome(agentRun, agentRun.domain));
|
|
133
|
+
// A routed scout that came back unusable runs again on its configured model.
|
|
134
|
+
const again = outcomes.map((outcome, index) => (routes[index] && !outcome.usable && outcome.run.status !== "cancelled" && !request.signal?.aborted ? index : -1)).filter((index) => index >= 0);
|
|
135
|
+
if (again.length > 0) {
|
|
136
|
+
const reruns = await runParallel(again.map((index) => requests[index]!), request.config.workflow.maxParallelScouts, run);
|
|
137
|
+
outcomes = outcomes.map((outcome, index) => {
|
|
138
|
+
const position = again.indexOf(index);
|
|
139
|
+
return position >= 0 ? toOutcome(reruns[position]!, reruns[position]!.domain) : outcome;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
95
142
|
saveScoutResults(request.taskDir, outcomes);
|
|
96
143
|
return outcomes;
|
|
97
144
|
}
|
|
@@ -152,9 +199,13 @@ export interface WorkerRequest {
|
|
|
152
199
|
profile?: ProfileResolver;
|
|
153
200
|
signal?: AbortSignal;
|
|
154
201
|
onUpdate?: (run: AgentRun) => void;
|
|
202
|
+
/** Likely files for the worker's step, and the lookup tool, while the classifier's file hints are on. */
|
|
203
|
+
hints?: FileHinter;
|
|
204
|
+
/** Lowers thinking (or the model) for a step the classifier judges simple or trivial. */
|
|
205
|
+
effort?: EffortRouter;
|
|
155
206
|
}
|
|
156
207
|
|
|
157
|
-
function workerWorkflowContext(request: WorkerRequest): string {
|
|
208
|
+
function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
158
209
|
const spec = domainSpec(request.domain);
|
|
159
210
|
const own = request.scoutOutcomes.filter(
|
|
160
211
|
(outcome) => outcome.result.domain === request.domain && outcome.usable,
|
|
@@ -166,7 +217,8 @@ function workerWorkflowContext(request: WorkerRequest): string {
|
|
|
166
217
|
own.length > 0
|
|
167
218
|
? `Scout findings for your domain:\n${summarizeOutcomes(own, 1500)}`
|
|
168
219
|
: "No scout findings were collected for your domain; verify the repository yourself.",
|
|
169
|
-
|
|
220
|
+
likely,
|
|
221
|
+
].filter(Boolean).join("\n\n");
|
|
170
222
|
}
|
|
171
223
|
|
|
172
224
|
/** Delegate one implementation step to a domain worker. */
|
|
@@ -176,28 +228,42 @@ export async function runWorker(
|
|
|
176
228
|
): Promise<WorkerOutcome> {
|
|
177
229
|
const slices = readAgentKnowledge(request.dataRoots, request.domain);
|
|
178
230
|
const selected = selectKnowledge(`${request.taskText} ${request.instruction}`, slices);
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
...request.agent,
|
|
231
|
+
const configured = profileFields(request.config, request.profile, request.domain, "worker");
|
|
232
|
+
const [likely, route] = await Promise.all([
|
|
233
|
+
likelyFor(request.hints, request.instruction, request.signal, request.taskText),
|
|
234
|
+
routeFor(request.effort, request.instruction, { ...(configured.model ? { model: configured.model } : {}), thinking: configured.thinking }, { context: request.taskText, ...(request.signal ? { signal: request.signal } : {}) }),
|
|
235
|
+
]);
|
|
236
|
+
const hintTools = request.hints?.tools() ?? [];
|
|
237
|
+
const extraTools = [...(request.agent?.extraTools ?? []), ...hintTools];
|
|
238
|
+
const base: AgentRequest = {
|
|
239
|
+
taskId: request.taskId,
|
|
240
|
+
domain: request.domain,
|
|
241
|
+
role: "worker",
|
|
242
|
+
instruction: request.instruction,
|
|
243
|
+
context: {
|
|
244
|
+
task: request.taskText,
|
|
245
|
+
...selected,
|
|
246
|
+
instructions: request.config.agents[request.domain].instructions,
|
|
247
|
+
workflowContext: workerWorkflowContext(request, likely),
|
|
197
248
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
249
|
+
...configured,
|
|
250
|
+
cwd: request.cwd,
|
|
251
|
+
signal: request.signal,
|
|
252
|
+
onUpdate: request.onUpdate,
|
|
253
|
+
...watchdogOptions(request.config.workflow),
|
|
254
|
+
...request.agent,
|
|
255
|
+
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
256
|
+
};
|
|
257
|
+
let outcome = workerOutcome(request.domain, await runAgent(route ? { ...base, ...routedFields(route) } : base, run));
|
|
258
|
+
// A routed step that fell short runs again at the configured model and thinking.
|
|
259
|
+
if (route && fellShort(outcome.run, outcome.issues) && !request.signal?.aborted) {
|
|
260
|
+
outcome = workerOutcome(request.domain, await runAgent(base, run));
|
|
261
|
+
}
|
|
262
|
+
return outcome;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function workerOutcome(domain: Domain, agentRun: AgentRun): WorkerOutcome {
|
|
266
|
+
const result = parseWorkerResult(domain, agentRun.output);
|
|
201
267
|
const issues =
|
|
202
268
|
agentRun.status === "success"
|
|
203
269
|
? validateWorkerResult(result)
|
package/src/pi/commands.ts
CHANGED
|
@@ -19,7 +19,8 @@ import { readFirstExisting } from "../knowledge/store.ts";
|
|
|
19
19
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
20
20
|
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
21
21
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
22
|
-
import { openSettings } from "./settings-ui.ts";
|
|
22
|
+
import { classifierSummary, openSettings } from "./settings-ui.ts";
|
|
23
|
+
import { keyStatus } from "../classifier/instance.ts";
|
|
23
24
|
import { kickoff, startPlannedTask, startTask } from "./start-task.ts";
|
|
24
25
|
import { setAuto, toggleOwnAuto } from "./owner.ts";
|
|
25
26
|
import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
@@ -197,7 +198,8 @@ function showConfig(ctx: ExtensionCommandContext): void {
|
|
|
197
198
|
const warnings = thinkingMismatches(config, modelLookup(ctx), ctx.model ? modelRef(ctx.model) : undefined);
|
|
198
199
|
if (hasScoutThinking(readRawConfig())) warnings.push(`Scout: thinking is fixed at "${SCOUT_THINKING}"; the scout.thinking value in the file is ignored.`);
|
|
199
200
|
const notes = warnings.length > 0 ? `\n\nWarnings:\n${warnings.map((line) => `- ${line}`).join("\n")}` : "";
|
|
200
|
-
|
|
201
|
+
const jev = `Classifier: ${classifierSummary(config, keyStatus)}`;
|
|
202
|
+
ctx.ui.notify(`${globalConfigPath()}\n${jev}\n${JSON.stringify(config, null, 2)}${notes}`, warnings.length > 0 ? "warning" : "info");
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
/**
|
package/src/pi/events.ts
CHANGED
|
@@ -11,9 +11,19 @@ import { applyStatus, clearStatus, isMinimized, setMinimized, setOracleActivity
|
|
|
11
11
|
import { ORACLE_THINKING, oracleActivityWord } from "./activity.ts";
|
|
12
12
|
import { isSubagentProcess, visibleTools } from "./quiet.ts";
|
|
13
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
14
|
-
import { taskRequest, type Task } from "../schemas/task.ts";
|
|
14
|
+
import { taskRequest, type Task, type TaskState } from "../schemas/task.ts";
|
|
15
15
|
import { pendingComments, readPlanComments, type PlanComment } from "../state/comments.ts";
|
|
16
16
|
import { isAutoMode } from "../state/auto.ts";
|
|
17
|
+
import { triageContext } from "../classifier/triage.ts";
|
|
18
|
+
import { previousTaskNote } from "./fresh-context.ts";
|
|
19
|
+
|
|
20
|
+
/** States in which the triage still helps the Master shape the task; once it is planned, the hints are noise. */
|
|
21
|
+
const SHAPING_STATES: ReadonlySet<TaskState> = new Set(["created", "clarifying", "scouting", "synthesizing", "awaiting_approval"]);
|
|
22
|
+
|
|
23
|
+
/** The Master's workflow context: the task's state, plus the classifier's triage while the task is being shaped. */
|
|
24
|
+
export function masterWorkflowContext(task: Task): string {
|
|
25
|
+
return [describeTask(task), SHAPING_STATES.has(task.state) ? triageContext(task.triage) : ""].filter(Boolean).join("\n\n");
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
export function masterTaskContext(task: Task, comments: readonly PlanComment[] = [], auto = false): string {
|
|
19
29
|
const open = pendingComments(comments);
|
|
@@ -83,9 +93,13 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
83
93
|
pi.on("before_agent_start", (event, ctx) => {
|
|
84
94
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
85
95
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
86
|
-
const
|
|
96
|
+
const hidden = isSubagentProcess() || isMinimized();
|
|
97
|
+
const task = hidden ? undefined : activeTask(root, configDir, sessionId);
|
|
87
98
|
if (!task) {
|
|
88
|
-
|
|
99
|
+
// After a task ended and cleared the context, a line on where its record is, in case the user refers back to it.
|
|
100
|
+
const previous = hidden ? undefined : previousTaskNote(ctx, configDir);
|
|
101
|
+
if (previous) event.systemPromptOptions.sections["bot-lobby"] = previous;
|
|
102
|
+
else delete event.systemPromptOptions.sections["bot-lobby"];
|
|
89
103
|
return;
|
|
90
104
|
}
|
|
91
105
|
const slices = readAgentKnowledge(readDataRoots(root, configDir), "master");
|
|
@@ -96,7 +110,7 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
96
110
|
standards: selected.standards,
|
|
97
111
|
knowledge: selected.knowledge,
|
|
98
112
|
decisions: selected.decisions,
|
|
99
|
-
workflowContext:
|
|
113
|
+
workflowContext: masterWorkflowContext(task),
|
|
100
114
|
instructions: loadConfig().master.instructions,
|
|
101
115
|
});
|
|
102
116
|
});
|