@a-t-h-i/bot-lobby 0.6.0 → 0.6.2
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 +177 -799
- 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 +6 -0
- package/src/lobby/keys.ts +1 -1
- package/src/lobby/planner.ts +305 -27
- package/src/lobby/quickfix.ts +120 -8
- package/src/lobby/runtime.ts +136 -5
- package/src/lobby/session-files.ts +12 -0
- package/src/lobby/sessions.ts +28 -2
- package/src/lobby/tabs/home.ts +54 -26
- 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/tabs/tasks.ts +30 -14
- package/src/lobby/view.ts +334 -41
- package/src/master/master.ts +95 -29
- package/src/pi/commands.ts +24 -4
- package/src/pi/events.ts +11 -2
- package/src/pi/owner.ts +57 -8
- package/src/pi/run-summary.ts +3 -1
- package/src/pi/settings-ui.ts +138 -2
- package/src/pi/start-task.ts +4 -0
- package/src/pi/tools.ts +7 -2
- package/src/schemas/configuration.ts +125 -1
- package/src/schemas/findings.ts +4 -0
- package/src/schemas/task.ts +24 -0
- package/src/state/archive.ts +99 -0
- package/src/state/inbox.ts +49 -15
- package/src/state/metrics.ts +74 -1
- package/src/state/presence.ts +99 -0
- package/src/workflow/workflow.ts +49 -1
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
@@ -18,7 +19,8 @@ import { readFirstExisting } from "../knowledge/store.ts";
|
|
|
18
19
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
19
20
|
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
20
21
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
21
|
-
import { openSettings } from "./settings-ui.ts";
|
|
22
|
+
import { classifierSummary, openSettings } from "./settings-ui.ts";
|
|
23
|
+
import { keyStatus } from "../classifier/instance.ts";
|
|
22
24
|
import { kickoff, startPlannedTask, startTask } from "./start-task.ts";
|
|
23
25
|
import { setAuto, toggleOwnAuto } from "./owner.ts";
|
|
24
26
|
import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
@@ -43,11 +45,12 @@ const HELP = [
|
|
|
43
45
|
"/bot-lobby claim <taskId> Take ownership of an orphaned task",
|
|
44
46
|
"/bot-lobby auto [on|off] Auto mode: the oracle drives this session's task without asking (alt+g)",
|
|
45
47
|
"/bot-lobby start-plan PLAN-… [auto] Start a planned task here; its agreed plan needs no approval",
|
|
48
|
+
"/bot-lobby switch <session.jsonl> Run a saved session in this window (the lobby's session browser uses it: alt+o, s)",
|
|
46
49
|
"/bot-lobby help This help",
|
|
47
50
|
].join("\n");
|
|
48
51
|
|
|
49
52
|
/** Subcommands only win when no free-form text follows (so tasks still start). */
|
|
50
|
-
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan"]);
|
|
53
|
+
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan", "switch"]);
|
|
51
54
|
|
|
52
55
|
function isTaskId(value: string | undefined): boolean {
|
|
53
56
|
return Boolean(value && /^TASK-/.test(value));
|
|
@@ -62,7 +65,8 @@ export function parseCommand(args: string): { sub: string | undefined; rest: str
|
|
|
62
65
|
if (!sub || !SUBCOMMANDS.has(sub)) return { sub: undefined, rest: [], restText: trimmed };
|
|
63
66
|
const takesArgs = sub === "amend" || sub === "claim"
|
|
64
67
|
|| (sub === "auto" && rest.length === 1 && /^(on|off)$/i.test(rest[0]!))
|
|
65
|
-
|| (sub === "start-plan" && rest.length >= 1 && rest.length <= 2 && /^PLAN-/.test(rest[0]!) && (rest.length === 1 || rest[1] === "auto"))
|
|
68
|
+
|| (sub === "start-plan" && rest.length >= 1 && rest.length <= 2 && /^PLAN-/.test(rest[0]!) && (rest.length === 1 || rest[1] === "auto"))
|
|
69
|
+
|| (sub === "switch" && rest.length >= 1 && /\.jsonl$/.test(rest.join(" ")));
|
|
66
70
|
if (!takesArgs && rest.length > 0 && !(rest.length === 1 && isTaskId(rest[0]))) {
|
|
67
71
|
return { sub: undefined, rest: [], restText: trimmed };
|
|
68
72
|
}
|
|
@@ -194,7 +198,21 @@ function showConfig(ctx: ExtensionCommandContext): void {
|
|
|
194
198
|
const warnings = thinkingMismatches(config, modelLookup(ctx), ctx.model ? modelRef(ctx.model) : undefined);
|
|
195
199
|
if (hasScoutThinking(readRawConfig())) warnings.push(`Scout: thinking is fixed at "${SCOUT_THINKING}"; the scout.thinking value in the file is ignored.`);
|
|
196
200
|
const notes = warnings.length > 0 ? `\n\nWarnings:\n${warnings.map((line) => `- ${line}`).join("\n")}` : "";
|
|
197
|
-
|
|
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");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `/bot-lobby switch <file>`: run a saved session in this window. Only a
|
|
207
|
+
* command context may replace the session, so the lobby's browser asks for a
|
|
208
|
+
* switch through this command. The context is stale once the switch starts.
|
|
209
|
+
*/
|
|
210
|
+
async function switchCommand(ctx: ExtensionCommandContext, path: string): Promise<void> {
|
|
211
|
+
if (!existsSync(path)) return ctx.ui.notify(`bot-lobby: no session file at ${path}`, "warning");
|
|
212
|
+
if (ctx.sessionManager.getSessionFile() === path) return ctx.ui.notify("bot-lobby: this window already runs that session", "info");
|
|
213
|
+
if (!ctx.isIdle()) return ctx.ui.notify("bot-lobby: the oracle is working — esc stops it, then switch", "warning");
|
|
214
|
+
const result = await ctx.switchSession(path);
|
|
215
|
+
if (result.cancelled) ctx.ui.notify("bot-lobby: the switch was cancelled", "warning");
|
|
198
216
|
}
|
|
199
217
|
|
|
200
218
|
/** `/bot-lobby auto [on|off]`: switch (or set) auto mode for this session's task. */
|
|
@@ -265,6 +283,8 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
265
283
|
return claimTaskCommand(ctx, configDir, rest[0]);
|
|
266
284
|
case "auto":
|
|
267
285
|
return autoCommand(ctx, configDir, rest[0]);
|
|
286
|
+
case "switch":
|
|
287
|
+
return switchCommand(ctx, restText);
|
|
268
288
|
case "start-plan": {
|
|
269
289
|
const started = await startPlannedTask(pi, ctx, configDir, rest[0]!, { auto: rest[1] === "auto" });
|
|
270
290
|
if (typeof started === "string") return ctx.ui.notify(`bot-lobby: ${started}`, "warning");
|
package/src/pi/events.ts
CHANGED
|
@@ -11,9 +11,18 @@ 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
|
+
|
|
19
|
+
/** States in which the triage still helps the Master shape the task; once it is planned, the hints are noise. */
|
|
20
|
+
const SHAPING_STATES: ReadonlySet<TaskState> = new Set(["created", "clarifying", "scouting", "synthesizing", "awaiting_approval"]);
|
|
21
|
+
|
|
22
|
+
/** The Master's workflow context: the task's state, plus the classifier's triage while the task is being shaped. */
|
|
23
|
+
export function masterWorkflowContext(task: Task): string {
|
|
24
|
+
return [describeTask(task), SHAPING_STATES.has(task.state) ? triageContext(task.triage) : ""].filter(Boolean).join("\n\n");
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
export function masterTaskContext(task: Task, comments: readonly PlanComment[] = [], auto = false): string {
|
|
19
28
|
const open = pendingComments(comments);
|
|
@@ -96,7 +105,7 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
96
105
|
standards: selected.standards,
|
|
97
106
|
knowledge: selected.knowledge,
|
|
98
107
|
decisions: selected.decisions,
|
|
99
|
-
workflowContext:
|
|
108
|
+
workflowContext: masterWorkflowContext(task),
|
|
100
109
|
instructions: loadConfig().master.instructions,
|
|
101
110
|
});
|
|
102
111
|
});
|
package/src/pi/owner.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a session does
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* oracle
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* What a session does on its own clock. Every few seconds a session (in the
|
|
3
|
+
* terminal or headless, started from another window's lobby) refreshes its
|
|
4
|
+
* heartbeat so other windows see it running, passes on what other sessions
|
|
5
|
+
* left for its oracle — plan comments, messages for its task or for the
|
|
6
|
+
* session itself — and in auto mode keeps the oracle going whenever its turn
|
|
7
|
+
* ends before the task is done. Subagents never own tasks or show up as
|
|
8
|
+
* sessions, so none of this runs in them.
|
|
8
9
|
*/
|
|
9
10
|
import type { KeyId } from "@earendil-works/pi-tui";
|
|
10
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -12,7 +13,8 @@ import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
|
12
13
|
import { activeTask, loadTask } from "../state/persistence.ts";
|
|
13
14
|
import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
14
15
|
import { commentMessage, markCommentsDelivered, readPlanComments, undeliveredComments } from "../state/comments.ts";
|
|
15
|
-
import { inboxMessage, markInboxDelivered, readInbox } from "../state/inbox.ts";
|
|
16
|
+
import { inboxMessage, markInboxDelivered, markSessionInboxDelivered, readInbox, readSessionInbox } from "../state/inbox.ts";
|
|
17
|
+
import { removePresence, writePresence } from "../state/presence.ts";
|
|
16
18
|
import { isAutoMode, setAutoMode } from "../state/auto.ts";
|
|
17
19
|
import { isSubagentProcess } from "./quiet.ts";
|
|
18
20
|
import { isMinimized } from "./ui.ts";
|
|
@@ -26,6 +28,7 @@ export const AUTO_KEY = "alt+g";
|
|
|
26
28
|
|
|
27
29
|
export type OwnerEvent =
|
|
28
30
|
| { kind: "comments" | "inbox"; taskId: string; count: number }
|
|
31
|
+
| { kind: "messages"; count: number }
|
|
29
32
|
| { kind: "auto"; taskId: string; on: boolean }
|
|
30
33
|
| { kind: "nudge"; taskId: string }
|
|
31
34
|
| { kind: "stalled"; taskId: string };
|
|
@@ -33,6 +36,8 @@ export type OwnerEvent =
|
|
|
33
36
|
interface Owner {
|
|
34
37
|
pi: ExtensionAPI;
|
|
35
38
|
ctx: ExtensionContext;
|
|
39
|
+
/** The session this clock runs for, kept so its heartbeat can be removed after its context goes stale. */
|
|
40
|
+
sessionId: string;
|
|
36
41
|
root: string;
|
|
37
42
|
configDir: string;
|
|
38
43
|
timer?: ReturnType<typeof setInterval>;
|
|
@@ -84,6 +89,40 @@ export function deliverInbox(): number {
|
|
|
84
89
|
return fresh.length;
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
/** Pass messages other sessions left for this session itself (it may have no task) to its Master. */
|
|
93
|
+
export function deliverSessionInbox(): number {
|
|
94
|
+
const state = owner;
|
|
95
|
+
if (!state) return 0;
|
|
96
|
+
const sessionId = state.ctx.sessionManager.getSessionId();
|
|
97
|
+
const fresh = readSessionInbox(state.root, state.configDir, sessionId).filter((message) => !message.delivered);
|
|
98
|
+
if (fresh.length === 0) return 0;
|
|
99
|
+
state.pi.sendUserMessage(inboxMessage(fresh), state.ctx.isIdle() ? undefined : { deliverAs: "steer" });
|
|
100
|
+
markSessionInboxDelivered(state.root, state.configDir, sessionId, fresh.map((message) => message.id));
|
|
101
|
+
listener?.({ kind: "messages", count: fresh.length });
|
|
102
|
+
return fresh.length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Tell other windows this session is running: its name, file, task and process. */
|
|
106
|
+
export function heartbeat(): void {
|
|
107
|
+
const state = owner;
|
|
108
|
+
if (!state) return;
|
|
109
|
+
const name = state.pi.getSessionName?.();
|
|
110
|
+
const sessionFile = state.ctx.sessionManager.getSessionFile?.();
|
|
111
|
+
const task = ownTask(state);
|
|
112
|
+
try {
|
|
113
|
+
writePresence(state.root, state.configDir, {
|
|
114
|
+
sessionId: state.ctx.sessionManager.getSessionId(),
|
|
115
|
+
pid: process.pid,
|
|
116
|
+
mode: state.ctx.mode ?? "tui",
|
|
117
|
+
...(name ? { name } : {}),
|
|
118
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
119
|
+
...(live(task) ? { taskId: task.id } : {}),
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
// A read-only project only means other windows do not see this one.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
87
126
|
/** What auto mode remembers between nudges: the task as it was, and how many nudges changed nothing. */
|
|
88
127
|
export interface AutoTrack {
|
|
89
128
|
fingerprint: string;
|
|
@@ -172,13 +211,22 @@ export function toggleOwnAuto(): string {
|
|
|
172
211
|
|
|
173
212
|
/** One tick of the owner's clock. */
|
|
174
213
|
export function ownerTick(): void {
|
|
214
|
+
heartbeat();
|
|
175
215
|
deliverComments();
|
|
176
216
|
deliverInbox();
|
|
217
|
+
deliverSessionInbox();
|
|
177
218
|
driveAuto();
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
function stop(): void {
|
|
181
222
|
if (owner?.timer) clearInterval(owner.timer);
|
|
223
|
+
if (owner) {
|
|
224
|
+
try {
|
|
225
|
+
removePresence(owner.root, owner.configDir, owner.sessionId);
|
|
226
|
+
} catch {
|
|
227
|
+
// The heartbeat goes stale on its own.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
182
230
|
owner = undefined;
|
|
183
231
|
}
|
|
184
232
|
|
|
@@ -186,7 +234,8 @@ function stop(): void {
|
|
|
186
234
|
export function startOwner(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string, pollMs = OWNER_POLL_MS): void {
|
|
187
235
|
stop();
|
|
188
236
|
if (isSubagentProcess()) return;
|
|
189
|
-
owner = { pi, ctx, root: detectProjectRoot(ctx.cwd, configDir), configDir };
|
|
237
|
+
owner = { pi, ctx, sessionId: ctx.sessionManager.getSessionId(), root: detectProjectRoot(ctx.cwd, configDir), configDir };
|
|
238
|
+
heartbeat();
|
|
190
239
|
if (pollMs > 0) {
|
|
191
240
|
owner.timer = setInterval(() => ownerTick(), pollMs);
|
|
192
241
|
owner.timer.unref?.();
|
package/src/pi/run-summary.ts
CHANGED
|
@@ -42,8 +42,9 @@ const STATUS_ICONS: Record<AgentRun["status"], string> = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
/** Why a run ended the way it did, in words, when that is not plain success. */
|
|
45
|
-
export function runFlags(run: Pick<AgentRun, "status" | "stalled" | "wrappedUp" | "attempts">): string[] {
|
|
45
|
+
export function runFlags(run: Pick<AgentRun, "status" | "stalled" | "wrappedUp" | "attempts" | "route">): string[] {
|
|
46
46
|
const flags: string[] = [];
|
|
47
|
+
if (run.route) flags.push(`routed ${run.route}`);
|
|
47
48
|
if (run.stalled) flags.push("stalled");
|
|
48
49
|
else if (run.status === "timeout") flags.push("hit its time limit");
|
|
49
50
|
else if (run.status === "failed") flags.push("failed");
|
|
@@ -88,6 +89,7 @@ export function runLogEntry(run: AgentRun): RunLogEntry {
|
|
|
88
89
|
...(run.stalled ? { stalled: true } : {}),
|
|
89
90
|
...(run.wrappedUp ? { wrappedUp: true } : {}),
|
|
90
91
|
...(run.error ? { error: run.error.split("\n")[0]!.slice(0, 200) } : {}),
|
|
92
|
+
...(run.routedFrom ? { routedFrom: run.routedFrom } : {}),
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
|
package/src/pi/settings-ui.ts
CHANGED
|
@@ -7,19 +7,25 @@ import {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Container, type Component, type Focusable, fuzzyFilter, getKeybindings, Input, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
|
9
9
|
import {
|
|
10
|
+
JEV_HOSTS,
|
|
10
11
|
INHERIT_MODEL,
|
|
11
12
|
isThinkingLevel,
|
|
12
13
|
LOBBY_PANELS,
|
|
14
|
+
PLANNING_ROUND_CHOICES,
|
|
13
15
|
SCOUT_THINKING,
|
|
14
16
|
SUBAGENT_KINDS,
|
|
15
17
|
type AgentModelConfig,
|
|
16
18
|
type BotLobbyConfig,
|
|
19
|
+
type ClassifierFeature,
|
|
20
|
+
type JevHostName,
|
|
17
21
|
type LobbyPanel,
|
|
18
22
|
type SubagentKind,
|
|
19
23
|
} from "../schemas/configuration.ts";
|
|
20
24
|
import { globalConfigPath, loadConfig, saveConfig } from "../state/project.ts";
|
|
21
25
|
import { checkThinking, kindLabel, modelRef, supportedThinking } from "./model-support.ts";
|
|
22
26
|
import { modelLookup } from "./tools.ts";
|
|
27
|
+
import { classifier } from "../classifier/instance.ts";
|
|
28
|
+
import { describeKey, hostLabel, jevEndpoint, keyHint, maskKey, resolveKey, type StatusSource } from "../classifier/hosts.ts";
|
|
23
29
|
|
|
24
30
|
const CUSTOM_MODEL = "__custom__";
|
|
25
31
|
const MAX_VISIBLE = 12;
|
|
@@ -381,15 +387,29 @@ export function toggleLobbySwitch(config: BotLobbyConfig, id: LobbySwitch): BotL
|
|
|
381
387
|
return { ...config, lobby: { ...config.lobby, [id]: value } };
|
|
382
388
|
}
|
|
383
389
|
|
|
384
|
-
/**
|
|
390
|
+
/** `5 rounds`, or `unlimited` for 0. */
|
|
391
|
+
export function roundLimitLabel(limit: number): string {
|
|
392
|
+
return limit > 0 ? `${limit} round${limit === 1 ? "" : "s"}` : "unlimited";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The next planning round limit in the menu's cycle (2, 3, 5, 8, unlimited); a hand-edited value rejoins it. */
|
|
396
|
+
export function nextRoundLimit(current: number): number {
|
|
397
|
+
const index = PLANNING_ROUND_CHOICES.indexOf(current as (typeof PLANNING_ROUND_CHOICES)[number]);
|
|
398
|
+
if (index >= 0) return PLANNING_ROUND_CHOICES[(index + 1) % PLANNING_ROUND_CHOICES.length]!;
|
|
399
|
+
return PLANNING_ROUND_CHOICES.find((choice) => choice > current) ?? 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** On/off settings for the lobby, and the planning round limit; enter flips or cycles one and saves it. Key rebinding stays in the file (lobby.keys). */
|
|
385
403
|
async function editLobby(ctx: ExtensionContext): Promise<void> {
|
|
386
404
|
for (;;) {
|
|
387
405
|
const config = loadConfig();
|
|
388
406
|
const items: SelectItem[] = LOBBY_SWITCHES.map((entry) => ({ value: entry.id, label: entry.label, description: `${lobbySwitch(config, entry.id) ? "on" : "off"} · ${entry.help}` }));
|
|
407
|
+
items.push({ value: "rounds", label: "Planning rounds", description: `${roundLimitLabel(config.lobby.maxPlanningRounds)} · enter cycles 2, 3, 5, 8, unlimited; the last round the oracle settles alone` });
|
|
389
408
|
items.push({ value: "back", label: "Back", description: `keys: lobby.keys in ${globalConfigPath()}` });
|
|
390
409
|
const choice = await pick(ctx, "bot-lobby settings · Lobby", items);
|
|
391
410
|
if (!choice || choice === "back") return;
|
|
392
|
-
saveConfig(
|
|
411
|
+
if (choice === "rounds") saveConfig({ ...config, lobby: { ...config.lobby, maxPlanningRounds: nextRoundLimit(config.lobby.maxPlanningRounds) } });
|
|
412
|
+
else saveConfig(toggleLobbySwitch(config, choice as LobbySwitch));
|
|
393
413
|
}
|
|
394
414
|
}
|
|
395
415
|
|
|
@@ -399,11 +419,125 @@ function lobbySummary(config: BotLobbyConfig): string {
|
|
|
399
419
|
config.lobby.autoOpen ? "opens with a task" : "opens on alt+l",
|
|
400
420
|
config.lobby.autoAsk ? "asks at once" : "asks on enter",
|
|
401
421
|
config.lobby.mouse ? "mouse" : "no mouse",
|
|
422
|
+
`planning: ${roundLimitLabel(config.lobby.maxPlanningRounds)}`,
|
|
402
423
|
...(config.lobby.issues ? ["issues tab"] : []),
|
|
403
424
|
...(hidden.length > 0 ? [`hidden: ${hidden.join(", ")}`] : []),
|
|
404
425
|
].join(" · ");
|
|
405
426
|
}
|
|
406
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Classifier decisions that exist so far, as the settings menu lists them.
|
|
430
|
+
* Each phase of the classifier work adds its own entry.
|
|
431
|
+
*/
|
|
432
|
+
export const CLASSIFIER_FEATURE_ITEMS: ReadonlyArray<{ id: ClassifierFeature; label: string; help: string }> = [
|
|
433
|
+
{ id: "seats", label: "Planning seats", help: "each round, only the seats your idea or latest answers touch sit; 1-4 in the Plan tab pins one" },
|
|
434
|
+
{ id: "answers", label: "Obvious answers", help: "a panel question whose recommended option the conversation already makes clearly right is answered for you (listed under Assumptions)" },
|
|
435
|
+
{ id: "triage", label: "Task triage", help: "a new task's size, domains and research need reach the Master as hints; a quick fix that is really a task is held for you" },
|
|
436
|
+
{ id: "effort", label: "Effort routing", help: "a simple step runs one thinking level lower, a trivial one on the cheaper model below; a routed run that falls short runs again on your settings" },
|
|
437
|
+
{ id: "files", label: "File hints", help: "scouts, workers, quick fixes and the planning panel start with the files most likely needed, and can look more up with find_relevant_files" },
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
/** The next Jev host in the menu's cycle. */
|
|
441
|
+
export function nextJevHost(current: JevHostName): JevHostName {
|
|
442
|
+
return JEV_HOSTS[(JEV_HOSTS.indexOf(current) + 1) % JEV_HOSTS.length]!;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** The config with one classifier decision switched on or off. */
|
|
446
|
+
export function toggleClassifierFeature(config: BotLobbyConfig, feature: ClassifierFeature): BotLobbyConfig {
|
|
447
|
+
const features = { ...config.classifier.features, [feature]: !config.classifier.features[feature] };
|
|
448
|
+
return { ...config, classifier: { ...config.classifier, features } };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** `on · OpenCode Zen · jev-1.13-free · key stored in pi`, for the settings menu and `/bot-lobby config`. */
|
|
452
|
+
export function classifierSummary(config: BotLobbyConfig, status: StatusSource | undefined): string {
|
|
453
|
+
const { host, model } = jevEndpoint(config.classifier, status);
|
|
454
|
+
const via = config.classifier.provider === "auto" ? `${host.label} (auto)` : host.label;
|
|
455
|
+
return [config.classifier.enabled ? "on" : "off", via, model, `key ${describeKey(host, status)}`].join(" · ");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** pi's auth status per provider, never throwing. */
|
|
459
|
+
function authStatus(ctx: ExtensionContext): StatusSource {
|
|
460
|
+
return (piProvider) => {
|
|
461
|
+
try {
|
|
462
|
+
return ctx.modelRegistry.getProviderAuthStatus(piProvider);
|
|
463
|
+
} catch {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Where the key lives and how to set it, with the loaded key masked. */
|
|
470
|
+
async function keyHelp(ctx: ExtensionContext, config: BotLobbyConfig): Promise<string> {
|
|
471
|
+
const status = authStatus(ctx);
|
|
472
|
+
const { host } = jevEndpoint(config.classifier, status);
|
|
473
|
+
let masked = "";
|
|
474
|
+
try {
|
|
475
|
+
const key = await resolveKey(host, (provider) => ctx.modelRegistry.getApiKeyForProvider(provider));
|
|
476
|
+
if (key) masked = ` Loaded: ${maskKey(key)}.`;
|
|
477
|
+
} catch {
|
|
478
|
+
// No key to show.
|
|
479
|
+
}
|
|
480
|
+
const where = host.name === "opencode"
|
|
481
|
+
? "it is the OpenCode key pi already uses for Zen and Go models"
|
|
482
|
+
: host.name === "typesafe" ? "it is saved in pi's auth.json with your other keys" : `it is the key pi already holds for ${host.label}`;
|
|
483
|
+
return `Jev via ${host.label}: key ${describeKey(host, status)}.${masked} To set it, ${keyHint(config.classifier)}; ${where}.`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Type a Jev model id; empty uses the host's default (OpenCode: the free jev-1.13-free). */
|
|
487
|
+
async function editJevModel(ctx: ExtensionContext): Promise<void> {
|
|
488
|
+
const config = loadConfig();
|
|
489
|
+
const { host } = jevEndpoint(config.classifier, authStatus(ctx));
|
|
490
|
+
const typed = await ctx.ui.input(`Jev model (empty: ${host.label}'s default, ${host.model})`, config.classifier.model || host.model);
|
|
491
|
+
if (typed === undefined) return;
|
|
492
|
+
const model = typed.trim() === host.model ? "" : typed.trim();
|
|
493
|
+
saveConfig({ ...config, classifier: { ...config.classifier, model } });
|
|
494
|
+
ctx.ui.notify(`bot-lobby: Jev model → ${model || `${host.model} (${host.label}'s default)`} — saved to ${globalConfigPath()}`, "info");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The model trivial steps run on; `none` keeps each agent's model and only lowers thinking. */
|
|
498
|
+
async function editCheapModel(ctx: ExtensionContext): Promise<void> {
|
|
499
|
+
const config = loadConfig();
|
|
500
|
+
const current = config.classifier.effort.cheapModel;
|
|
501
|
+
const none: SelectItem = { value: INHERIT_MODEL, label: current === INHERIT_MODEL ? "none ✓" : "none", description: "Trivial steps keep their model and run one thinking level lower" };
|
|
502
|
+
const choice = await pick(ctx, "Cheaper model for trivial steps", [none, ...modelItems(ctx, current, false)], { search: true });
|
|
503
|
+
if (choice === undefined) return;
|
|
504
|
+
const typed = choice === CUSTOM_MODEL ? (await ctx.ui.input("Model id", "provider/model"))?.trim() : choice;
|
|
505
|
+
if (!typed) return;
|
|
506
|
+
saveConfig({ ...config, classifier: { ...config.classifier, effort: { cheapModel: typed } } });
|
|
507
|
+
ctx.ui.notify(`bot-lobby: trivial steps run on ${typed === INHERIT_MODEL ? "their own model, one thinking level lower" : typed} — saved to ${globalConfigPath()}`, "info");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The classifier: on/off, where Jev is called, its key, which decisions it makes, and a connection test. */
|
|
511
|
+
async function editClassifier(ctx: ExtensionContext): Promise<void> {
|
|
512
|
+
for (;;) {
|
|
513
|
+
const config = loadConfig();
|
|
514
|
+
const settings = config.classifier;
|
|
515
|
+
const status = authStatus(ctx);
|
|
516
|
+
const { host, model } = jevEndpoint(settings, status);
|
|
517
|
+
const items: SelectItem[] = [
|
|
518
|
+
{ value: "enabled", label: "Classifier", description: `${settings.enabled ? "on" : "off"} · Jev makes the obvious decisions so large models spend fewer tokens on them` },
|
|
519
|
+
{ value: "host", label: "Host", description: `${hostLabel(settings.provider)}${settings.provider === "auto" ? ` → ${host.label}` : ""} · enter cycles ${JEV_HOSTS.map(hostLabel).join(", ")}` },
|
|
520
|
+
{ value: "key", label: "API key", description: describeKey(host, status) },
|
|
521
|
+
{ value: "model", label: "Model", description: `${model}${settings.model ? "" : " (the host's default)"} · e.g. jev-1.13 once OpenCode's free jev-1.13-free ends` },
|
|
522
|
+
...CLASSIFIER_FEATURE_ITEMS.map((entry) => ({ value: `feature:${entry.id}`, label: entry.label, description: `${settings.features[entry.id] ? "on" : "off"} · ${entry.help}` })),
|
|
523
|
+
{ value: "cheap", label: "Cheaper model", description: `${settings.effort.cheapModel === INHERIT_MODEL ? "none: trivial steps keep their model and drop a thinking level" : settings.effort.cheapModel} · what effort routing runs trivial steps on` },
|
|
524
|
+
{ value: "test", label: "Test connection", description: "one tiny call: shows the model and how long it took" },
|
|
525
|
+
{ value: "back", label: "Back", description: `thresholds and limits: classifier in ${globalConfigPath()}` },
|
|
526
|
+
];
|
|
527
|
+
const choice = await pick(ctx, "bot-lobby settings · Classifier (Jev)", items);
|
|
528
|
+
if (!choice || choice === "back") return;
|
|
529
|
+
if (choice === "enabled") saveConfig({ ...config, classifier: { ...settings, enabled: !settings.enabled } });
|
|
530
|
+
else if (choice === "host") saveConfig({ ...config, classifier: { ...settings, provider: nextJevHost(settings.provider) } });
|
|
531
|
+
else if (choice === "key") ctx.ui.notify(await keyHelp(ctx, config), "info");
|
|
532
|
+
else if (choice === "model") await editJevModel(ctx);
|
|
533
|
+
else if (choice === "cheap") await editCheapModel(ctx);
|
|
534
|
+
else if (choice === "test") {
|
|
535
|
+
const result = await classifier().test();
|
|
536
|
+
ctx.ui.notify(result.ok ? `Jev answered in ${result.ms} ms (${result.model}).` : `Jev test failed: ${result.error}`, result.ok ? "info" : "error");
|
|
537
|
+
} else if (choice.startsWith("feature:")) saveConfig(toggleClassifierFeature(config, choice.slice("feature:".length) as ClassifierFeature));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
407
541
|
/** Open the per-agent settings editor; every change is written to the global config. */
|
|
408
542
|
export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
409
543
|
if (ctx.mode !== "tui") {
|
|
@@ -419,10 +553,12 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
|
|
|
419
553
|
const config = loadConfig();
|
|
420
554
|
const items: SelectItem[] = SETTINGS_KINDS.map((kind) => ({ value: kind, label: kindLabel(kind), description: entryDescription(kind, entryView(config, kind)) }));
|
|
421
555
|
items.push({ value: "lobby", label: "Lobby", description: lobbySummary(config) });
|
|
556
|
+
items.push({ value: "classifier", label: "Classifier (Jev)", description: classifierSummary(config, authStatus(ctx)) });
|
|
422
557
|
items.push({ value: "close", label: "Close" });
|
|
423
558
|
const choice = await pick(ctx, "bot-lobby settings", items);
|
|
424
559
|
if (!choice || choice === "close") return;
|
|
425
560
|
if (choice === "lobby") await editLobby(ctx);
|
|
561
|
+
else if (choice === "classifier") await editClassifier(ctx);
|
|
426
562
|
else await editEntry(pi, ctx, choice as SettingsKind);
|
|
427
563
|
}
|
|
428
564
|
}
|
package/src/pi/start-task.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { applyStatus } from "./ui.ts";
|
|
|
14
14
|
import { applyMasterModel } from "./settings-ui.ts";
|
|
15
15
|
import { setAutoMode } from "../state/auto.ts";
|
|
16
16
|
import { loadPlannedTask, markPlannedTaskStarted, plannedTaskRequest } from "../state/backlog.ts";
|
|
17
|
+
import { triageFor } from "../classifier/instance.ts";
|
|
17
18
|
|
|
18
19
|
function uniqueTaskId(root: string, configDir: string, request: string): string {
|
|
19
20
|
const base = nextTaskId(request);
|
|
@@ -65,6 +66,9 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
65
66
|
if (options.approvedPlan) task.approvedPlan = options.approvedPlan;
|
|
66
67
|
createTaskDir(root, configDir, task);
|
|
67
68
|
transition(task, "clarifying");
|
|
69
|
+
// The classifier's read of the request (when it is on) reaches the Master's very first turn.
|
|
70
|
+
const triage = await triageFor({ cwd: ctx.cwd, root, configDir }, request);
|
|
71
|
+
if (triage) task.triage = triage;
|
|
68
72
|
saveTask(root, configDir, task);
|
|
69
73
|
if (options.auto) setAutoMode(root, configDir, task.id, true, sessionId);
|
|
70
74
|
// A session that starts a task is named after it, so /resume and the lobby list it by name.
|