@a-t-h-i/bot-lobby 0.6.39 → 0.6.41
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 +42 -6
- package/package.json +1 -1
- package/prompts/global.md +12 -0
- package/prompts/master.md +8 -1
- package/prompts/quickfix.md +5 -3
- package/prompts/scout.md +8 -4
- package/prompts/worker.md +4 -3
- package/src/classifier/chat.ts +122 -0
- package/src/classifier/classifier.ts +46 -11
- package/src/classifier/files.ts +91 -23
- package/src/classifier/instance.ts +35 -2
- package/src/classifier/models.ts +115 -0
- package/src/execution/git.ts +45 -2
- package/src/index.ts +3 -0
- package/src/knowledge/file-notes.ts +104 -0
- package/src/knowledge/repo-map.ts +165 -0
- package/src/lobby/host.ts +2 -0
- package/src/lobby/service.ts +2 -1
- package/src/master/master.ts +39 -3
- package/src/master/scout-cache.ts +70 -0
- package/src/master/synthesis.ts +10 -2
- package/src/pi/model-settings.ts +5 -3
- package/src/pi/tool-budget.ts +66 -0
- package/src/roles/markdown.ts +12 -4
- package/src/roles/scout.ts +2 -1
- package/src/schemas/configuration.ts +21 -2
- package/src/schemas/findings.ts +2 -0
- package/src/webui/api/settings.ts +2 -1
- package/src/webui/dev/fake-service.ts +1 -0
- package/src/webui/protocol.ts +8 -0
- package/webui/dist/assets/{index-D8TuTAMK.js → index-DXDrQbgq.js} +10 -10
- package/webui/dist/assets/index-Glz3FAPp.css +2 -0
- package/webui/dist/build.json +10 -6
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-uTndyUfm.css +0 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scout findings kept for reuse. A scout asked the same thing (same domain,
|
|
3
|
+
* same instruction, same task) on a work tree that has not changed since it
|
|
4
|
+
* last answered gives the same answer, so the engine hands the oracle the
|
|
5
|
+
* earlier findings instead of running it again: after a crash and resume, or
|
|
6
|
+
* when the oracle repeats a question. Any change to the tree (a commit, an
|
|
7
|
+
* edit, a new file) makes every cached answer stale.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { Domain } from "../schemas/agent.ts";
|
|
13
|
+
import type { ScoutOutcome } from "./master.ts";
|
|
14
|
+
|
|
15
|
+
interface CachedScout {
|
|
16
|
+
version: 1;
|
|
17
|
+
tree: string;
|
|
18
|
+
at: string;
|
|
19
|
+
outcome: ScoutOutcome;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Cached answers older than this are dropped when the next one is written. */
|
|
23
|
+
export const SCOUT_CACHE_DAYS = 7;
|
|
24
|
+
|
|
25
|
+
export function scoutCacheDir(dataDir: string): string {
|
|
26
|
+
return join(dataDir, "cache", "scouts");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The question a scout is asked, as a file name. */
|
|
30
|
+
export function scoutKey(domain: Domain, instruction: string, taskText: string): string {
|
|
31
|
+
return createHash("sha256").update(JSON.stringify([domain, instruction.trim(), taskText.trim()])).digest("hex").slice(0, 32);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The earlier answer to this question on this tree, marked with when it was taken; undefined when there is none or the tree changed. */
|
|
35
|
+
export function readCachedScout(dataDir: string, key: string, tree: string): ScoutOutcome | undefined {
|
|
36
|
+
try {
|
|
37
|
+
const cached = JSON.parse(readFileSync(join(scoutCacheDir(dataDir), `${key}.json`), "utf8")) as Partial<CachedScout>;
|
|
38
|
+
if (cached.version !== 1 || cached.tree !== tree || !cached.outcome?.usable || !cached.at) return undefined;
|
|
39
|
+
return { ...cached.outcome, reused: cached.at };
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pruneScoutCache(dir: string, now: number): void {
|
|
46
|
+
for (const name of readdirSync(dir)) {
|
|
47
|
+
const path = join(dir, name);
|
|
48
|
+
try {
|
|
49
|
+
if (now - statSync(path).mtimeMs > SCOUT_CACHE_DAYS * 86_400_000) rmSync(path, { force: true });
|
|
50
|
+
} catch {
|
|
51
|
+
// Gone already.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Keep a usable answer for this question on this tree; failures are ignored (the cache only saves time). */
|
|
57
|
+
export function writeCachedScout(dataDir: string, key: string, tree: string, outcome: ScoutOutcome, now = new Date()): void {
|
|
58
|
+
if (!outcome.usable) return;
|
|
59
|
+
const dir = scoutCacheDir(dataDir);
|
|
60
|
+
try {
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
pruneScoutCache(dir, now.getTime());
|
|
63
|
+
const path = join(dir, `${key}.json`);
|
|
64
|
+
const { reused: _reused, ...fresh } = outcome;
|
|
65
|
+
writeFileSync(`${path}.tmp`, JSON.stringify({ version: 1, tree, at: now.toISOString(), outcome: fresh } satisfies CachedScout));
|
|
66
|
+
renameSync(`${path}.tmp`, path);
|
|
67
|
+
} catch {
|
|
68
|
+
// Without the cache the scout just runs next time.
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/master/synthesis.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Domain } from "../schemas/agent.ts";
|
|
2
|
+
import type { RelevantFile } from "../schemas/findings.ts";
|
|
2
3
|
import { truncate } from "../text.ts";
|
|
3
4
|
import type { ScoutOutcome } from "./master.ts";
|
|
4
5
|
|
|
@@ -34,16 +35,23 @@ export function detectSharedFiles(outcomes: ScoutOutcome[]): { path: string; dom
|
|
|
34
35
|
.map(([path, domains]) => ({ path, domains: [...domains] }));
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** A relevant file as `path:lines — why`, the way a brief should point at it. */
|
|
39
|
+
export function fileAnchor(file: RelevantFile, reasonChars = 140): string {
|
|
40
|
+
const anchor = file.lines ? `${file.path}:${file.lines}` : file.path;
|
|
41
|
+
return file.reason ? `${anchor} — ${truncate(file.reason, reasonChars)}` : anchor;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
function describeOutcome(outcome: ScoutOutcome, rawLimit: number): string {
|
|
38
45
|
const { result } = outcome;
|
|
46
|
+
const reused = outcome.reused ? `, reused from ${outcome.reused.slice(0, 16).replace("T", " ")} UTC: same question, tree unchanged` : "";
|
|
39
47
|
const lines = [
|
|
40
|
-
`### ${result.domain} (${outcome.run.status}${outcome.usable ? "" : ", unusable"}) — confidence: ${result.confidence}`,
|
|
48
|
+
`### ${result.domain} (${outcome.run.status}${outcome.usable ? "" : ", unusable"}${reused}) — confidence: ${result.confidence}`,
|
|
41
49
|
result.scope ? `Scope: ${result.scope}` : "",
|
|
42
50
|
...result.findings.slice(0, 12).map((finding) => `- ${finding}`),
|
|
43
51
|
...result.risks.slice(0, 5).map((risk) => `- RISK: ${risk}`),
|
|
44
52
|
...result.recommendations.slice(0, 5).map((rec) => `- REC: ${rec}`),
|
|
45
53
|
result.relevantFiles.length > 0
|
|
46
|
-
? `Files
|
|
54
|
+
? `Files:\n${result.relevantFiles.slice(0, 10).map((file) => `- ${fileAnchor(file)}`).join("\n")}`
|
|
47
55
|
: "",
|
|
48
56
|
outcome.issues.length > 0 ? `Issues: ${outcome.issues.join("; ")}` : "",
|
|
49
57
|
result.raw.trim() ? `Raw (truncated): ${truncate(result.raw.trim(), rawLimit)}` : "",
|
package/src/pi/model-settings.ts
CHANGED
|
@@ -47,7 +47,7 @@ interface EntryPatch {
|
|
|
47
47
|
export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: EntryPatch): BotLobbyConfig {
|
|
48
48
|
const next: BotLobbyConfig = { ...config, agents: { ...config.agents } };
|
|
49
49
|
if (kind === "master") next.master = { ...config.master, ...patch } as AgentModelConfig;
|
|
50
|
-
else if (kind === "scout") next.scout = { model: patch.model ?? config.scout.model, timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs };
|
|
50
|
+
else if (kind === "scout") next.scout = { ...config.scout, model: patch.model ?? config.scout.model, timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs };
|
|
51
51
|
else if (kind === "researcher") next.researcher = { ...config.researcher, ...patch } as AgentModelConfig;
|
|
52
52
|
else if (kind === "quickfix") next.quickFix = { ...config.quickFix, ...patch } as AgentModelConfig;
|
|
53
53
|
else if (kind === "planner") next.planner = { ...config.planner, ...patch } as AgentModelConfig;
|
|
@@ -83,10 +83,12 @@ export function prefillModels(config: BotLobbyConfig, sessionModel: string | und
|
|
|
83
83
|
return { config: next, filled };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** `on · OpenCode Zen · jev-1.13-free · key stored in pi
|
|
86
|
+
/** `on · OpenCode Zen · jev-1.13-free · key stored in pi` (or `on · openai/gpt-5-nano · your pi login`), for the settings menu and `/bot-lobby config`. */
|
|
87
87
|
export function classifierSummary(config: BotLobbyConfig, status: StatusSource | undefined): string {
|
|
88
|
+
const state = config.classifier.enabled ? "on" : "off";
|
|
89
|
+
if (config.classifier.engine) return [state, config.classifier.engine, "your pi login"].join(" · ");
|
|
88
90
|
const { host, model } = jevEndpoint(config.classifier, status);
|
|
89
91
|
const via = config.classifier.provider === "auto" ? `${host.label} (auto)` : host.label;
|
|
90
|
-
return [
|
|
92
|
+
return [state, via, model, `key ${describeKey(host, status)}`].join(" · ");
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hard limit on a subagent's tool calls, enforced inside its own process so
|
|
3
|
+
* it holds on every provider and model. The engine sets the budget through
|
|
4
|
+
* `BOT_LOBBY_TOOL_BUDGET` (scouts get `scout.maxToolCalls`). At 60% the next
|
|
5
|
+
* tool result tells the agent to wrap up; past the limit every call is
|
|
6
|
+
* refused with the reason, so the agent writes its report with what it has.
|
|
7
|
+
* An agent that keeps calling anyway is stopped after a few refusals.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export const TOOL_BUDGET_ENV = "BOT_LOBBY_TOOL_BUDGET";
|
|
12
|
+
|
|
13
|
+
export interface ToolBudget {
|
|
14
|
+
/** The call after which the agent is told to wrap up. */
|
|
15
|
+
warnAt: number;
|
|
16
|
+
/** Calls past this are refused. */
|
|
17
|
+
max: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Refusals in a row after which the run is stopped rather than refused again. */
|
|
21
|
+
export const MAX_REFUSALS = 5;
|
|
22
|
+
|
|
23
|
+
/** The env that gives a subagent `max` tool calls; none for 0 (no limit). */
|
|
24
|
+
export function toolBudgetEnv(max: number): Record<string, string> {
|
|
25
|
+
return max > 0 ? { [TOOL_BUDGET_ENV]: String(Math.round(max)) } : {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseToolBudget(value: string | undefined): ToolBudget | undefined {
|
|
29
|
+
const max = Number(value);
|
|
30
|
+
if (!value || !Number.isFinite(max) || max < 1) return undefined;
|
|
31
|
+
return { warnAt: Math.max(1, Math.round(max * 0.6)), max: Math.round(max) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What the agent is told up front, in its workflow context. */
|
|
35
|
+
export function toolBudgetNote(max: number): string {
|
|
36
|
+
const budget = parseToolBudget(String(max));
|
|
37
|
+
return budget ? `Tool budget: at most ${budget.max} tool calls. After ${budget.warnAt} you are told to wrap up; past ${budget.max} every call is refused and you report with what you have.` : "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function budgetWarning(used: number, budget: ToolBudget): string {
|
|
41
|
+
return `bot-lobby: ${used} of your ${budget.max} tool calls are used. Stop exploring: check only what your answer still needs, then write your report.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function budgetRefusal(budget: ToolBudget): string {
|
|
45
|
+
return `bot-lobby: your ${budget.max} tool calls are spent, so this call was not run. Write your report now in the required format with what you have, and mark what you did not verify as an assumption.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Count the agent's own tool calls (not those a tool made for it), warn once, then refuse. */
|
|
49
|
+
export function registerToolBudget(pi: ExtensionAPI, budget = parseToolBudget(process.env[TOOL_BUDGET_ENV])): void {
|
|
50
|
+
if (!budget) return;
|
|
51
|
+
let used = 0;
|
|
52
|
+
let refused = 0;
|
|
53
|
+
let warned = false;
|
|
54
|
+
pi.on("tool_call", (event) => {
|
|
55
|
+
if (event.parentToolCallId) return undefined;
|
|
56
|
+
used += 1;
|
|
57
|
+
if (used <= budget.max) return undefined;
|
|
58
|
+
refused += 1;
|
|
59
|
+
return { block: true, reason: budgetRefusal(budget), ...(refused >= MAX_REFUSALS ? { terminate: true } : {}) };
|
|
60
|
+
});
|
|
61
|
+
pi.on("tool_result", (event) => {
|
|
62
|
+
if (event.parentToolCallId || warned || used < budget.warnAt || used > budget.max) return undefined;
|
|
63
|
+
warned = true;
|
|
64
|
+
return { content: [...event.content, { type: "text" as const, text: budgetWarning(used, budget) }] };
|
|
65
|
+
});
|
|
66
|
+
}
|
package/src/roles/markdown.ts
CHANGED
|
@@ -56,9 +56,17 @@ export function bullets(text: string | undefined): string[] {
|
|
|
56
56
|
.filter((line) => line.length > 0);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/** Split a `path:12-40` (or `path#L12-L40`) anchor into the path and its lines. */
|
|
60
|
+
function splitAnchor(anchor: string): { path: string; lines?: string } {
|
|
61
|
+
const match = /^(.+?)(?::|#L)(\d+)(?::\d+)?(?:\s*[-–]\s*L?(\d+)(?::\d+)?)?$/.exec(anchor);
|
|
62
|
+
if (!match) return { path: anchor };
|
|
63
|
+
return { path: match[1]!, lines: match[3] && match[3] !== match[2] ? `${match[2]}-${match[3]}` : match[2]! };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse a `path — reason` bullet (the path may carry lines: `path:12-40`) into its parts. */
|
|
67
|
+
export function parseFileBullet(text: string): { path: string; lines?: string; reason: string } {
|
|
61
68
|
const match = /^`?([^`\s]+)`?\s*(?:[—–]|--|-)\s*(.*)$/.exec(text);
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
const [anchor, reason] = match ? [match[1]!, match[2]!.trim()] : [text.trim().replace(/^`|`$/g, ""), ""];
|
|
70
|
+
const { path, lines } = splitAnchor(anchor);
|
|
71
|
+
return lines ? { path, lines, reason } : { path, reason };
|
|
64
72
|
}
|
package/src/roles/scout.ts
CHANGED
|
@@ -12,7 +12,8 @@ export const scoutSpec: RoleSpec = {
|
|
|
12
12
|
"Respond with exactly these sections and nothing else:",
|
|
13
13
|
"`## Scope`, `## Findings`, `## Relevant Files`, `## Existing Patterns`,",
|
|
14
14
|
"`## Risks`, `## Recommendations`, `## Confidence`.",
|
|
15
|
-
"Use `- ` bullets. `## Relevant Files` entries are `- \\`path\\` — reason
|
|
15
|
+
"Use `- ` bullets. `## Relevant Files` entries are `- \\`path:start-end\\` — reason`, with the lines of the code that matters",
|
|
16
|
+
"(`path:42` for one line, the bare `path` only when the whole file does), so the worker opens the exact place.",
|
|
16
17
|
"`## Confidence` is exactly one of `High`, `Medium`, `Low`.",
|
|
17
18
|
"Keep the whole response under 400 words. Report uncertainty; do not implement.",
|
|
18
19
|
"An optional `## Pushback` (`**Request:**`, `**Reason:**`, optional `**Alternative:**`) flags a change you",
|
|
@@ -38,6 +38,8 @@ export interface ScoutConfig {
|
|
|
38
38
|
mcpTools?: string[];
|
|
39
39
|
model: ModelRef;
|
|
40
40
|
timeoutMs: number;
|
|
41
|
+
/** Tool calls a scout may make: it is told to wrap up at 60% and refused more past this, so it reports with what it has; 0 = no limit. */
|
|
42
|
+
maxToolCalls: number;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/** Subagent kinds with their own settings entry. */
|
|
@@ -91,6 +93,10 @@ export interface WorkflowConfig {
|
|
|
91
93
|
routeQuickFixes: boolean;
|
|
92
94
|
/** A new task gets a git branch or worktree of its own, named after it; `--branch` and `--worktree` decide for one task. */
|
|
93
95
|
gitIsolation: GitIsolation;
|
|
96
|
+
/** Scouts, workers, quick fixes and the planner get the repository map: the layout and the files matching their step, with each file's purpose, names and unchanged scout notes. */
|
|
97
|
+
repoMap: boolean;
|
|
98
|
+
/** A scout asked the same thing again for the same task, on a tree that has not changed, reuses its findings instead of running. */
|
|
99
|
+
reuseScouts: boolean;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
/**
|
|
@@ -206,9 +212,15 @@ export interface ClassifierThresholds {
|
|
|
206
212
|
quickFixLargeAt: number;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
|
-
/** The
|
|
215
|
+
/** The classifier: a fast model for obvious decisions, so large models spend fewer tokens on them. */
|
|
210
216
|
export interface ClassifierConfig {
|
|
211
217
|
enabled: boolean;
|
|
218
|
+
/**
|
|
219
|
+
* The model that answers, as `provider/id`: any classifier model pi lists
|
|
220
|
+
* (Jev, Cloudflare's clef, Liquid's d1, …) or any chat model you are signed
|
|
221
|
+
* in to (OpenAI, Anthropic, Google, …). Empty calls Jev on `provider`.
|
|
222
|
+
*/
|
|
223
|
+
engine: string;
|
|
212
224
|
/** Where Jev is called; the key comes from pi's own key store for that host. */
|
|
213
225
|
provider: JevHostName;
|
|
214
226
|
/** A pinned Jev model; empty uses the host's default. */
|
|
@@ -250,7 +262,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
250
262
|
backend: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: "", timeoutMs: 15 * 60 * 1000 },
|
|
251
263
|
qa: { model: INHERIT_MODEL, thinking: DEFAULT_THINKING, instructions: "", timeoutMs: 15 * 60 * 1000 },
|
|
252
264
|
},
|
|
253
|
-
scout: { model: INHERIT_MODEL, timeoutMs: 8 * 60 * 1000 },
|
|
265
|
+
scout: { model: INHERIT_MODEL, timeoutMs: 8 * 60 * 1000, maxToolCalls: 30 },
|
|
254
266
|
researcher: { model: INHERIT_MODEL, thinking: "low", instructions: "", timeoutMs: 10 * 60 * 1000 },
|
|
255
267
|
quickFix: { model: INHERIT_MODEL, thinking: "low", instructions: "", timeoutMs: 10 * 60 * 1000 },
|
|
256
268
|
planner: { model: INHERIT_MODEL, thinking: "high", instructions: "", timeoutMs: 5 * 60 * 1000 },
|
|
@@ -272,6 +284,8 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
272
284
|
briefCheck: true,
|
|
273
285
|
routeQuickFixes: true,
|
|
274
286
|
gitIsolation: "off",
|
|
287
|
+
repoMap: true,
|
|
288
|
+
reuseScouts: true,
|
|
275
289
|
},
|
|
276
290
|
lint: { mode: "advise", command: "", extensions: [], timeoutMs: 2 * 60 * 1000 },
|
|
277
291
|
knowledge: {
|
|
@@ -292,6 +306,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
292
306
|
},
|
|
293
307
|
classifier: {
|
|
294
308
|
enabled: false,
|
|
309
|
+
engine: "",
|
|
295
310
|
provider: "auto",
|
|
296
311
|
model: "",
|
|
297
312
|
baseUrl: "",
|
|
@@ -346,6 +361,7 @@ function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig
|
|
|
346
361
|
return {
|
|
347
362
|
model: typeof override?.model === "string" && override.model.trim() ? override.model : base.model,
|
|
348
363
|
timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
|
|
364
|
+
maxToolCalls: typeof override?.maxToolCalls === "number" && Number.isFinite(override.maxToolCalls) && override.maxToolCalls >= 0 ? Math.round(override.maxToolCalls) : base.maxToolCalls,
|
|
349
365
|
...(mcpTools.length ? { mcpTools } : {}),
|
|
350
366
|
};
|
|
351
367
|
}
|
|
@@ -432,6 +448,7 @@ function normalizeClassifier(value: unknown): ClassifierConfig {
|
|
|
432
448
|
const provider = typeof source.provider === "string" && (JEV_HOSTS as readonly string[]).includes(source.provider) ? source.provider as JevHostName : defaults.provider;
|
|
433
449
|
return {
|
|
434
450
|
enabled: flag(source.enabled, defaults.enabled),
|
|
451
|
+
engine: text(source.engine, defaults.engine).trim(),
|
|
435
452
|
provider,
|
|
436
453
|
model: text(source.model, defaults.model),
|
|
437
454
|
baseUrl: text(source.baseUrl, defaults.baseUrl),
|
|
@@ -456,6 +473,8 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
456
473
|
workflow.fastTrack = workflow.fastTrack !== false;
|
|
457
474
|
workflow.briefCheck = workflow.briefCheck !== false;
|
|
458
475
|
workflow.routeQuickFixes = workflow.routeQuickFixes !== false;
|
|
476
|
+
workflow.repoMap = workflow.repoMap !== false;
|
|
477
|
+
workflow.reuseScouts = workflow.reuseScouts !== false;
|
|
459
478
|
workflow.gitIsolation = isGitIsolation(workflow.gitIsolation) ? workflow.gitIsolation : DEFAULT_CONFIG.workflow.gitIsolation;
|
|
460
479
|
workflow.taskBudgetMinutes = typeof workflow.taskBudgetMinutes === "number" && workflow.taskBudgetMinutes > 0 ? Math.min(24 * 60, Math.round(workflow.taskBudgetMinutes)) : 0;
|
|
461
480
|
const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
|
package/src/schemas/findings.ts
CHANGED
|
@@ -90,7 +90,8 @@ export function settingsGet(ctx: ApiContext): SettingsInfo {
|
|
|
90
90
|
config = prefilled.config;
|
|
91
91
|
pushNotice(`${prefilled.filled.map(kindLabel).join(", ")} now run on the session model; change them here any time.`);
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
const classifiers = ctx.service.classifierModels?.() ?? [];
|
|
94
|
+
return { config: stripSecrets(config), models: ctx.service.models?.() ?? [], ...(classifiers.length > 0 ? { classifierModels: classifiers } : {}) };
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/** The linters the project configures (its top and a few folders below), and whether each is installed. */
|
|
@@ -455,6 +455,7 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
455
455
|
},
|
|
456
456
|
configChanged: () => {},
|
|
457
457
|
models: () => models.map((model) => ({ id: model.id, label: model.label, thinkingLevels: [...model.thinkingLevels] })),
|
|
458
|
+
classifierModels: () => [{ id: "opencode/jev-1.13-free", label: "Jev 1.13 (free)" }, { id: "openrouter/liquid/d1", label: "Liquid D1" }],
|
|
458
459
|
issuesEnabled: () => fixture.status.issuesEnabled,
|
|
459
460
|
workspace: () => ({ ...fixture.status.workspace }),
|
|
460
461
|
zen: () => ({ ...(fixture.zen.task ? { task: { ...fixture.zen.task } } : {}), runs: [...fixture.zen.runs] }),
|
package/src/webui/protocol.ts
CHANGED
|
@@ -200,6 +200,14 @@ export interface SettingsLinter {
|
|
|
200
200
|
export interface SettingsInfo {
|
|
201
201
|
config: BotLobbyConfig;
|
|
202
202
|
models: SettingsModelInfo[];
|
|
203
|
+
/** Classifier models pi can call with the logins it holds (Jev on each host, Cloudflare's clef, Liquid's d1, …). */
|
|
204
|
+
classifierModels?: SettingsClassifierModel[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A classifier model for the classifier's picker: `provider/id` and its name. */
|
|
208
|
+
export interface SettingsClassifierModel {
|
|
209
|
+
id: string;
|
|
210
|
+
label: string;
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
/** One message on the event stream (`GET /api/events`, server-sent events). */
|