@a-t-h-i/bot-lobby 0.6.47 → 0.6.49

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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A background session's questions, asked wherever the user is. A session
3
+ * started "in a new session" runs headless, so every question it asks (its
4
+ * oracle's questionnaire, a pick, a line of text, a yes/no) reaches this
5
+ * window as an RPC dialog. Each one is put in this window's question queue,
6
+ * the same pop-up the user's own session asks in, named after the session it
7
+ * comes from, so nobody has to switch sessions to answer. The answer goes
8
+ * back to the session; a dialog answered some other way (or a session that
9
+ * ends) takes its question back out of the queue.
10
+ */
11
+ import { readRelayRequest } from "../ask/relay.ts";
12
+ import { readResult } from "../ask/web.ts";
13
+ import { promptHub, type PromptHub } from "./prompt-hub.ts";
14
+ import type { DialogAnswer, SessionDialog } from "./sessions.ts";
15
+
16
+ export interface SessionQuestion {
17
+ /** The session's name, shown as who asks. */
18
+ from: string;
19
+ dialog: SessionDialog;
20
+ signal: AbortSignal;
21
+ }
22
+
23
+ /** Put a session's dialog to the user in the question pop-up; undefined when it was taken back first. */
24
+ export async function askForSession({ from, dialog, signal }: SessionQuestion, hub: PromptHub = promptHub): Promise<DialogAnswer | undefined> {
25
+ const options = { signal };
26
+ const relayed = dialog.method === "editor" ? readRelayRequest(dialog.title, dialog.prefill) : undefined;
27
+ if (relayed) {
28
+ const outcome = await hub.ask("questionnaire", from, { questions: relayed }, options);
29
+ if (outcome.how === "cancelled") return undefined;
30
+ const result = outcome.how === "answered" ? readResult(outcome.value) : undefined;
31
+ return { value: JSON.stringify(result ?? { answers: [], cancelled: true }) };
32
+ }
33
+ if (dialog.method === "select") {
34
+ const outcome = await hub.ask("choose", from, { title: dialog.title, options: dialog.options ?? [] }, options);
35
+ return settled(outcome, (value) => (typeof value === "string" ? { value } : undefined));
36
+ }
37
+ if (dialog.method === "confirm") {
38
+ const outcome = await hub.ask("confirm", from, { question: dialog.title, ...(dialog.message ? { detail: dialog.message } : {}) }, options);
39
+ return settled(outcome, (value) => (typeof value === "boolean" ? { confirmed: value } : undefined));
40
+ }
41
+ const outcome = await hub.ask("text", from, { question: dialog.title, ...(dialog.placeholder ? { placeholder: dialog.placeholder } : {}) }, options);
42
+ return settled(outcome, (value) => (typeof value === "string" ? { value } : undefined));
43
+ }
44
+
45
+ function settled(outcome: { how: "answered" | "dismissed" | "cancelled"; value: unknown }, read: (value: unknown) => DialogAnswer | undefined): DialogAnswer | undefined {
46
+ if (outcome.how === "cancelled") return undefined;
47
+ if (outcome.how === "dismissed") return { cancelled: true };
48
+ return read(outcome.value) ?? { cancelled: true };
49
+ }
@@ -54,6 +54,11 @@ export interface SessionStart {
54
54
  message?: string;
55
55
  /** The task it was started to carry on (resumed from the Tasks screen). */
56
56
  taskId?: string;
57
+ /**
58
+ * Put a dialog to the user (this window's question pop-up); resolves with the answer, or undefined when the
59
+ * dialog was settled some other way first (`signal` aborts then).
60
+ */
61
+ ask?: (question: { from: string; dialog: SessionDialog; signal: AbortSignal }) => Promise<DialogAnswer | undefined>;
57
62
  }
58
63
 
59
64
  let counter = 0;
@@ -80,6 +85,9 @@ export class BackgroundSession {
80
85
  /** Dialogs the session waits on, oldest first. */
81
86
  readonly dialogs: SessionDialog[] = [];
82
87
  private readonly onDialog: NonNullable<SessionStart["onDialog"]>;
88
+ private readonly ask?: SessionStart["ask"];
89
+ /** The dialogs being asked in the question pop-up, to take back when they are settled another way. */
90
+ private readonly asking = new Map<string, AbortController>();
83
91
  private readonly proc: SessionProcess;
84
92
  private buffer = "";
85
93
  private stderr = "";
@@ -96,6 +104,7 @@ export class BackgroundSession {
96
104
  if (start.planId) this.planId = start.planId;
97
105
  if (start.taskId) this.taskId = start.taskId;
98
106
  this.onDialog = start.onDialog ?? (() => {});
107
+ if (start.ask) this.ask = start.ask;
99
108
  this.proc = proc;
100
109
  this.onChange = onChange;
101
110
  proc.stdout?.on("data", (chunk) => this.receive(String(chunk)));
@@ -121,6 +130,8 @@ export class BackgroundSession {
121
130
  this.exitCode = code;
122
131
  for (const dialog of this.dialogs) this.onDialog(dialog.id, false, this.sessionId);
123
132
  this.dialogs.length = 0;
133
+ for (const controller of this.asking.values()) controller.abort();
134
+ this.asking.clear();
124
135
  if (this.killTimer) clearTimeout(this.killTimer);
125
136
  this.feed.say("note", reason);
126
137
  for (const resolve of this.exitWaiters.splice(0)) resolve();
@@ -175,6 +186,8 @@ export class BackgroundSession {
175
186
  const index = this.dialogs.findIndex((dialog) => dialog.id === id);
176
187
  if (index < 0) return;
177
188
  this.dialogs.splice(index, 1);
189
+ this.asking.get(id)?.abort();
190
+ this.asking.delete(id);
178
191
  this.onDialog(id, false, this.sessionId);
179
192
  if (this.alive && this.proc.stdin) this.proc.stdin.write(`${JSON.stringify({ type: "extension_ui_response", id, ...answer })}\n`);
180
193
  this.onChange();
@@ -248,6 +261,21 @@ export class BackgroundSession {
248
261
  this.onChange();
249
262
  }
250
263
 
264
+ /** Ask the dialog in the question pop-up, wherever the user is; its answer goes back to the session. */
265
+ private putToUser(dialog: SessionDialog): void {
266
+ if (!this.ask) return;
267
+ const controller = new AbortController();
268
+ this.asking.set(dialog.id, controller);
269
+ this.ask({ from: this.name, dialog, signal: controller.signal }).then(
270
+ (answer) => {
271
+ if (this.asking.get(dialog.id) !== controller) return;
272
+ this.asking.delete(dialog.id);
273
+ if (answer) this.answer(dialog.id, answer);
274
+ },
275
+ () => this.asking.delete(dialog.id),
276
+ );
277
+ }
278
+
251
279
  private uiRequest(event: Record<string, unknown>): void {
252
280
  const id = String(event.id ?? "");
253
281
  const method = String(event.method ?? "");
@@ -262,6 +290,7 @@ export class BackgroundSession {
262
290
  ...(typeof event.prefill === "string" ? { prefill: event.prefill } : {}),
263
291
  });
264
292
  this.onDialog(id, true, this.sessionId);
293
+ this.putToUser(this.dialogs.at(-1)!);
265
294
  this.feed.log("LOBBY", `waiting for you: ${String(event.title ?? "a question").split("\n")[0]}`, "warning");
266
295
  } else if (method === "notify") {
267
296
  const level = event.notifyType === "error" ? "error" : event.notifyType === "warning" ? "warning" : "info";
@@ -299,6 +328,8 @@ export const launchPi = (args: string[], cwd: string, overrides: NodeJS.ProcessE
299
328
  const env: NodeJS.ProcessEnv = { ...process.env, PI_SKIP_VERSION_CHECK: "1" };
300
329
  delete env.BOT_LOBBY_SUBAGENT;
301
330
  delete env.BOT_LOBBY_WEB_PROJECT;
331
+ // It has no page of its own: its questions come to this window (see ask/web.ts).
332
+ env.BOT_LOBBY_BACKGROUND = "1";
302
333
  Object.assign(env, overrides);
303
334
  const proc: ChildProcess = spawn(invocation.command, invocation.args, { cwd, env, shell: false, detached: POSIX, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
304
335
  return proc as unknown as SessionProcess;
@@ -60,7 +60,7 @@ export interface ScoutRequest {
60
60
  /** Model, thinking and time limit for one run, from settings. */
61
61
  function profileFields(config: BotLobbyConfig, resolver: ProfileResolver | undefined, domain: Domain, role: AgentRequest["role"]) {
62
62
  const profile = profileFor(config, resolver, domain, role);
63
- return { model: profile.model, mcpTools: profile.mcpTools, thinking: profile.thinking, timeoutMs: profile.timeoutMs, time: boundedAgentTime(profile.timeoutMs) };
63
+ return { model: profile.model, mcpTools: profile.mcpTools, ...(profile.autoCompact === false ? { autoCompact: false } : {}), thinking: profile.thinking, timeoutMs: profile.timeoutMs, time: boundedAgentTime(profile.timeoutMs) };
64
64
  }
65
65
 
66
66
  function scoutInstruction(request: ScoutRequest, domain: Domain): string {
@@ -71,6 +71,7 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
71
71
  context: researchContext(request),
72
72
  model: profile.model,
73
73
  mcpTools: profile.mcpTools,
74
+ ...(profile.autoCompact === false ? { autoCompact: false } : {}),
74
75
  thinking: profile.thinking,
75
76
  timeoutMs: profile.timeoutMs,
76
77
  ...(request.time ? { time: request.time } : {}),
@@ -1,6 +1,31 @@
1
- import { compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, type ExtensionAPI, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
1
+ import { compact, CONFIG_DIR_NAME, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, type ExtensionAPI, type ExtensionContext, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
2
2
  import { contextMarks, freshContextOn } from "./fresh-context.ts";
3
- import { isSubagentProcess } from "./quiet.ts";
3
+ import { AUTO_COMPACT_ENV, isSubagentProcess } from "./quiet.ts";
4
+ import { dataRoot, detectProjectRoot, loadConfig } from "../state/project.ts";
5
+ import { describeShared, DIGEST_INSTRUCTION, parseDigest, shareDigest, type DigestItem } from "../knowledge/oracle-digest.ts";
6
+
7
+ const CHECKPOINT = "Write a concise checkpoint with: Requirements and constraints; Decisions; Files changed; Verification and evidence; Failed approaches (do not repeat); Unfinished work and next action; Outstanding children and their assignments. Preserve concrete file references and distinguish verified facts from guesses. The parent remains responsible for integration and verification.";
8
+
9
+ export interface AutoCompactionOptions {
10
+ /** Whether this agent compacts on its own: a subagent by its environment, the oracle by its settings. */
11
+ enabled?: () => boolean;
12
+ /** Where the oracle's shared knowledge goes; by default the project's knowledge files. */
13
+ share?: (items: DigestItem[], ctx: ExtensionContext) => string;
14
+ }
15
+
16
+ function enabledBySettings(): boolean {
17
+ if (isSubagentProcess()) return process.env[AUTO_COMPACT_ENV] !== "0";
18
+ try {
19
+ return loadConfig().master.autoCompact !== false;
20
+ } catch {
21
+ return true;
22
+ }
23
+ }
24
+
25
+ function shareWithAgents(items: DigestItem[], ctx: ExtensionContext): string {
26
+ const root = detectProjectRoot(ctx.cwd, CONFIG_DIR_NAME);
27
+ return describeShared(shareDigest(dataRoot(root, CONFIG_DIR_NAME), items));
28
+ }
4
29
 
5
30
  export const CONTEXT_TARGET = 64_000;
6
31
 
@@ -8,13 +33,20 @@ export function compactionThreshold(window: number, reserve = DEFAULT_COMPACTION
8
33
  return Math.max(1, Math.min(CONTEXT_TARGET, Math.floor(window * 0.6), window - Math.min(reserve, Math.floor(window / 2))));
9
34
  }
10
35
 
11
- /** Compact at a settled tool boundary; ctx.compact() would abort the turn we need to continue. */
12
- export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compact = compact): void {
36
+ /**
37
+ * Compact at a settled tool boundary; ctx.compact() would abort the turn we need to continue. Each agent's settings
38
+ * can turn it off. The oracle's checkpoint also lists what it learned that is worth keeping, tagged by agent, and
39
+ * that goes into the knowledge base: its own, and the designer's, backend's and QA's where it concerns them.
40
+ */
41
+ export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compact = compact, options: AutoCompactionOptions = {}): void {
42
+ const enabled = options.enabled ?? enabledBySettings;
43
+ const share = options.share ?? shareWithAgents;
44
+ const oracle = !isSubagentProcess();
13
45
  let busy = false;
14
46
  let lastLeaf: string | null | undefined;
15
47
  pi.on("session_start", () => { busy = false; lastLeaf = undefined; });
16
48
  pi.on("turn_end", async (event: TurnEndEvent, ctx) => {
17
- if (busy || !ctx.model) return;
49
+ if (busy || !ctx.model || !enabled()) return;
18
50
  const leaf = ctx.sessionManager.getLeafId();
19
51
  if (leaf === lastLeaf) return;
20
52
  const configured = pi.getSettings().compaction;
@@ -61,9 +93,18 @@ export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compa
61
93
  const headers = auth.headers ? Object.fromEntries(Object.entries(auth.headers).filter((entry): entry is [string, string] => entry[1] !== null)) : undefined;
62
94
  const retry = pi.getSettings().retry;
63
95
  const retryPolicy = { enabled: retry?.enabled ?? true, maxRetries: retry?.maxRetries ?? 3, baseDelayMs: retry?.baseDelayMs ?? 2_000, maxAgentDelayMs: retry?.maxAgentDelayMs };
64
- const result = await summarize({ firstKeptEntryId, messagesToSummarize: messages, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: estimate, previousSummary: previous?.type === "compaction" ? previous.summary : undefined, fileOps, settings: { enabled: true, reserveTokens: reserve, keepRecentTokens } }, auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model, auth.apiKey, headers, "Write a concise checkpoint with: Requirements and constraints; Decisions; Files changed; Verification and evidence; Failed approaches (do not repeat); Unfinished work and next action; Outstanding children and their assignments. Preserve concrete file references and distinguish verified facts from guesses. The parent remains responsible for integration and verification.", ctx.signal, undefined, undefined, auth.env, retryPolicy);
96
+ const result = await summarize({ firstKeptEntryId, messagesToSummarize: messages, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: estimate, previousSummary: previous?.type === "compaction" ? previous.summary : undefined, fileOps, settings: { enabled: true, reserveTokens: reserve, keepRecentTokens } }, auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model, auth.apiKey, headers, oracle ? `${CHECKPOINT} ${DIGEST_INSTRUCTION}` : CHECKPOINT, ctx.signal, undefined, undefined, auth.env, retryPolicy);
65
97
  ctx.signal?.throwIfAborted();
66
98
  ctx.ui.notify("Agent context compacted", "info");
99
+ if (oracle) {
100
+ try {
101
+ const items = parseDigest(result.summary);
102
+ const shared = items.length ? share(items, ctx) : "";
103
+ if (shared) ctx.ui.notify(`Knowledge updated: ${shared}`, "info");
104
+ } catch (error) {
105
+ ctx.ui.notify(`Could not update knowledge: ${(error as Error).message}`, "warning");
106
+ }
107
+ }
67
108
  return { entries: [...event.entries, { type: "compaction" as const, summary: result.summary, firstKeptEntryId, usage: result.usage, details: result.details }] };
68
109
  } catch (error) {
69
110
  ctx.ui.notify(`Context compaction failed: ${(error as Error).message}`, "warning");
package/src/pi/quiet.ts CHANGED
@@ -58,6 +58,9 @@ export function webToolsFor(active: string[], taskActive: boolean, hidden: reado
58
58
  return { active: [...active, ...back], hidden: [] };
59
59
  }
60
60
 
61
+ /** Set to "0" in a subagent's environment when its settings turn auto-compaction off. */
62
+ export const AUTO_COMPACT_ENV = "BOT_LOBBY_AUTO_COMPACT";
63
+
61
64
  /** True inside a bot-lobby subagent process (marked in `spawnPiProcess`). */
62
65
  export function isSubagentProcess(): boolean {
63
66
  return process.env.BOT_LOBBY_SUBAGENT === "1";
package/src/pi/tools.ts CHANGED
@@ -132,8 +132,8 @@ export function workflowDeps(
132
132
  effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
133
133
  // Time spent waiting on the user is not the task's: its budget clock waits too.
134
134
  // The questions go to the web page, where a free-text answer may run to several lines.
135
- ask: async (question) => (canAsk() ? whileAsking(() => askText(question)) : undefined),
136
- choose: async (title, options) => (canAsk() ? whileAsking(() => askChoice(title, options)) : undefined),
135
+ ask: async (question) => (canAsk() ? whileAsking(() => askText(question, undefined, ctx)) : undefined),
136
+ choose: async (title, options) => (canAsk() ? whileAsking(() => askChoice(title, options, undefined, ctx)) : undefined),
137
137
  // An agent's own questions (the designer's), relayed as the questionnaire.
138
138
  ...(canAsk() ? { askQuestions: (questions: AskQuestion[], from: string, askSignal?: AbortSignal) => whileAsking(() => askUser(questions, ctx, askSignal, from)) } : {}),
139
139
  notify: (message, level = "info") => {
@@ -31,6 +31,8 @@ export interface AgentModelConfig {
31
31
  mcpTools?: string[];
32
32
  /** Time limit per run; falls back to `workflow.agentTimeoutMs`. */
33
33
  timeoutMs?: number;
34
+ /** Compact this agent's context on its own as it fills (on unless false); the oracle's also updates the knowledge base. */
35
+ autoCompact?: boolean;
34
36
  }
35
37
 
36
38
  /** Scouts pick a model and a time limit only; their thinking is fixed at `SCOUT_THINKING`. */
@@ -40,6 +42,8 @@ export interface ScoutConfig {
40
42
  timeoutMs: number;
41
43
  /** 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
44
  maxToolCalls: number;
45
+ /** Compact a scout's context on its own as it fills (on unless false). */
46
+ autoCompact?: boolean;
43
47
  }
44
48
 
45
49
  /** Subagent kinds with their own settings entry. */
@@ -351,7 +355,7 @@ function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConf
351
355
  const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
352
356
  const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
353
357
  const mcpTools = normalizeMcpTools(merged.mcpTools);
354
- return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}) };
358
+ return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}), ...(merged.autoCompact === false ? { autoCompact: false } : {}) };
355
359
  }
356
360
 
357
361
  /** Scouts keep a model and a time limit; any thinking value in the file is dropped. */
@@ -363,6 +367,7 @@ function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig
363
367
  timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
364
368
  maxToolCalls: typeof override?.maxToolCalls === "number" && Number.isFinite(override.maxToolCalls) && override.maxToolCalls >= 0 ? Math.round(override.maxToolCalls) : base.maxToolCalls,
365
369
  ...(mcpTools.length ? { mcpTools } : {}),
370
+ ...(override?.autoCompact === false ? { autoCompact: false } : {}),
366
371
  };
367
372
  }
368
373
 
@@ -513,6 +518,13 @@ export interface AgentProfile {
513
518
  instructions?: string;
514
519
  /** Exact native MCP tool names this profile may call; empty grants none. */
515
520
  mcpTools?: string[];
521
+ /** False when its settings turn auto-compaction off. */
522
+ autoCompact?: boolean;
523
+ }
524
+
525
+ /** `{ autoCompact: false }` when an entry turns auto-compaction off, else nothing. */
526
+ function compactField(entry: { autoCompact?: boolean }): { autoCompact?: false } {
527
+ return entry.autoCompact === false ? { autoCompact: false } : {};
516
528
  }
517
529
 
518
530
  /** The settings entry a domain/role run draws from. */
@@ -537,10 +549,10 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
537
549
  const instructions = config.agents[domain].instructions;
538
550
  const fallback = config.workflow.agentTimeoutMs;
539
551
  if (kind === "scout") {
540
- return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(config.scout.mcpTools?.length ? { mcpTools: config.scout.mcpTools } : {}) };
552
+ return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(config.scout.mcpTools?.length ? { mcpTools: config.scout.mcpTools } : {}), ...compactField(config.scout) };
541
553
  }
542
554
  const entry = kind === "researcher" ? config.researcher : config.agents[kind];
543
- return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
555
+ return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...compactField(entry) };
544
556
  }
545
557
 
546
558
  /** The settings entry of a lobby agent (quick fix or planner). */
@@ -549,19 +561,19 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
549
561
  }
550
562
 
551
563
  /** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
552
- export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] } {
564
+ export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[]; autoCompact?: boolean } {
553
565
  const entry = lobbyAgentConfig(config, kind);
554
- return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
566
+ return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...compactField(entry) };
555
567
  }
556
568
 
557
569
  /**
558
570
  * A planning panel seat's profile: the domain's (or the researcher's) model,
559
571
  * thinking and custom instructions, bounded by the planner's per-turn limit.
560
572
  */
561
- export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] } {
573
+ export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[]; autoCompact?: boolean } {
562
574
  const entry = member === "researcher" ? config.researcher : config.agents[member];
563
575
  const timeoutMs = config.planner.timeoutMs ?? config.workflow.agentTimeoutMs;
564
- return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
576
+ return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...compactField(entry) };
565
577
  }
566
578
 
567
579
  /** Resolves the model, thinking and time limit one subagent run uses. */
@@ -178,7 +178,8 @@ export const DEFAULT_ID = "mist"
178
178
  * T3 Tools Inc., packages/shared/src/themePalettes.ts), their roles mapped onto
179
179
  * the shadcn variables: canvas is the background, surface the card, accent the
180
180
  * primary, accent surface the accent, error the destructive colour. Slate,
181
- * Sand, Rosewood, Aurora and Midnight are original complementary palettes.
181
+ * Sand, Rosewood, Aurora and Midnight, and Graphite, Lagoon, Dune, Plum, Moss and Crimson, are original
182
+ * complementary palettes.
182
183
  */
183
184
  export const PRESETS: readonly Palette[] = [
184
185
  { id: DEFAULT_ID, name: "Default" },
@@ -642,6 +643,282 @@ export const PRESETS: readonly Palette[] = [
642
643
  "chart-1": "#acbbed",
643
644
  },
644
645
  },
646
+ {
647
+ id: "graphite",
648
+ name: "Graphite",
649
+ light: {
650
+ "background": "#fafafa",
651
+ "foreground": "#222222",
652
+ "card": "#fefefe",
653
+ "card-foreground": "#222222",
654
+ "popover": "#fefefe",
655
+ "popover-foreground": "#222222",
656
+ "primary": "#333333",
657
+ "primary-foreground": "#fcfcfc",
658
+ "secondary": "#ebebeb",
659
+ "secondary-foreground": "#222222",
660
+ "muted": "#f0f0f0",
661
+ "muted-foreground": "#5d5d5d",
662
+ "accent": "#e8e8e8",
663
+ "accent-foreground": "#222222",
664
+ "destructive": "#d02b31",
665
+ "border": "#dedede",
666
+ "input": "#cecece",
667
+ "ring": "#333333",
668
+ "chart-1": "#333333",
669
+ },
670
+ dark: {
671
+ "background": "#121212",
672
+ "foreground": "#eeeeee",
673
+ "card": "#191919",
674
+ "card-foreground": "#eeeeee",
675
+ "popover": "#202020",
676
+ "popover-foreground": "#eeeeee",
677
+ "primary": "#e4e4e4",
678
+ "primary-foreground": "#121212",
679
+ "secondary": "#292929",
680
+ "secondary-foreground": "#eeeeee",
681
+ "muted": "#242424",
682
+ "muted-foreground": "#ababab",
683
+ "accent": "#333333",
684
+ "accent-foreground": "#eeeeee",
685
+ "destructive": "#f66c6d",
686
+ "border": "#333333",
687
+ "input": "#424242",
688
+ "ring": "#e4e4e4",
689
+ "chart-1": "#e4e4e4",
690
+ },
691
+ },
692
+ {
693
+ id: "lagoon",
694
+ name: "Lagoon",
695
+ light: {
696
+ "background": "#f3fcfd",
697
+ "foreground": "#0e2627",
698
+ "card": "#fbffff",
699
+ "card-foreground": "#0e2627",
700
+ "popover": "#fbffff",
701
+ "popover-foreground": "#0e2627",
702
+ "primary": "#007780",
703
+ "primary-foreground": "#fcfcfc",
704
+ "secondary": "#d5f2f3",
705
+ "secondary-foreground": "#0e2627",
706
+ "muted": "#e3f4f5",
707
+ "muted-foreground": "#456466",
708
+ "accent": "#c3f2f4",
709
+ "accent-foreground": "#0e2627",
710
+ "destructive": "#d02b31",
711
+ "border": "#cde3e4",
712
+ "input": "#b8d4d5",
713
+ "ring": "#007780",
714
+ "chart-1": "#007780",
715
+ },
716
+ dark: {
717
+ "background": "#041516",
718
+ "foreground": "#e8f1f1",
719
+ "card": "#091d1e",
720
+ "card-foreground": "#e8f1f1",
721
+ "popover": "#102425",
722
+ "popover-foreground": "#e8f1f1",
723
+ "primary": "#53c5cb",
724
+ "primary-foreground": "#041516",
725
+ "secondary": "#162d2f",
726
+ "secondary-foreground": "#e8f1f1",
727
+ "muted": "#152829",
728
+ "muted-foreground": "#9cafb0",
729
+ "accent": "#0c3a3c",
730
+ "accent-foreground": "#e8f1f1",
731
+ "destructive": "#f66c6d",
732
+ "border": "#203839",
733
+ "input": "#2f4749",
734
+ "ring": "#53c5cb",
735
+ "chart-1": "#53c5cb",
736
+ },
737
+ },
738
+ {
739
+ id: "dune",
740
+ name: "Dune",
741
+ light: {
742
+ "background": "#fef9f3",
743
+ "foreground": "#2b1f11",
744
+ "card": "#fffefb",
745
+ "card-foreground": "#2b1f11",
746
+ "popover": "#fffefb",
747
+ "popover-foreground": "#2b1f11",
748
+ "primary": "#9d6300",
749
+ "primary-foreground": "#fcfcfc",
750
+ "secondary": "#f9e8d6",
751
+ "secondary-foreground": "#2b1f11",
752
+ "muted": "#f8eee4",
753
+ "muted-foreground": "#6b5a48",
754
+ "accent": "#fde3c6",
755
+ "accent-foreground": "#2b1f11",
756
+ "destructive": "#d02b31",
757
+ "border": "#e9dccd",
758
+ "input": "#dbcbb9",
759
+ "ring": "#9d6300",
760
+ "chart-1": "#9d6300",
761
+ },
762
+ dark: {
763
+ "background": "#181006",
764
+ "foreground": "#f3ede8",
765
+ "card": "#21170c",
766
+ "card-foreground": "#f3ede8",
767
+ "popover": "#291e12",
768
+ "popover-foreground": "#f3ede8",
769
+ "primary": "#dba666",
770
+ "primary-foreground": "#181006",
771
+ "secondary": "#322618",
772
+ "secondary-foreground": "#f3ede8",
773
+ "muted": "#2c2217",
774
+ "muted-foreground": "#b4a99d",
775
+ "accent": "#422e16",
776
+ "accent-foreground": "#f3ede8",
777
+ "destructive": "#f66c6d",
778
+ "border": "#3d3022",
779
+ "input": "#4d4031",
780
+ "ring": "#dba666",
781
+ "chart-1": "#dba666",
782
+ },
783
+ },
784
+ {
785
+ id: "plum",
786
+ name: "Plum",
787
+ light: {
788
+ "background": "#fef8ff",
789
+ "foreground": "#291c2c",
790
+ "card": "#fffdff",
791
+ "card-foreground": "#291c2c",
792
+ "popover": "#fffdff",
793
+ "popover-foreground": "#291c2c",
794
+ "primary": "#9553a4",
795
+ "primary-foreground": "#fcfcfc",
796
+ "secondary": "#f6e4fa",
797
+ "secondary-foreground": "#291c2c",
798
+ "muted": "#f7ecf9",
799
+ "muted-foreground": "#68566d",
800
+ "accent": "#f9dcff",
801
+ "accent-foreground": "#291c2c",
802
+ "destructive": "#d02b31",
803
+ "border": "#e7d8ea",
804
+ "input": "#d8c7dc",
805
+ "ring": "#9553a4",
806
+ "chart-1": "#9553a4",
807
+ },
808
+ dark: {
809
+ "background": "#170d19",
810
+ "foreground": "#f2ecf3",
811
+ "card": "#201522",
812
+ "card-foreground": "#f2ecf3",
813
+ "popover": "#271c2a",
814
+ "popover-foreground": "#f2ecf3",
815
+ "primary": "#d398e0",
816
+ "primary-foreground": "#170d19",
817
+ "secondary": "#312334",
818
+ "secondary-foreground": "#f2ecf3",
819
+ "muted": "#2a202d",
820
+ "muted-foreground": "#b2a6b5",
821
+ "accent": "#3f2945",
822
+ "accent-foreground": "#f2ecf3",
823
+ "destructive": "#f66c6d",
824
+ "border": "#3b2d3e",
825
+ "input": "#4b3c4e",
826
+ "ring": "#d398e0",
827
+ "chart-1": "#d398e0",
828
+ },
829
+ },
830
+ {
831
+ id: "moss",
832
+ name: "Moss",
833
+ light: {
834
+ "background": "#f8fcf6",
835
+ "foreground": "#1c2517",
836
+ "card": "#fdfffc",
837
+ "card-foreground": "#1c2517",
838
+ "popover": "#fdfffc",
839
+ "popover-foreground": "#1c2517",
840
+ "primary": "#4a7333",
841
+ "primary-foreground": "#fcfcfc",
842
+ "secondary": "#e3f0dd",
843
+ "secondary-foreground": "#1c2517",
844
+ "muted": "#ebf3e8",
845
+ "muted-foreground": "#56624f",
846
+ "accent": "#dcefd2",
847
+ "accent-foreground": "#1c2517",
848
+ "destructive": "#d02b31",
849
+ "border": "#d8e2d3",
850
+ "input": "#c6d2c0",
851
+ "ring": "#4a7333",
852
+ "chart-1": "#4a7333",
853
+ },
854
+ dark: {
855
+ "background": "#0d140a",
856
+ "foreground": "#ecf0ea",
857
+ "card": "#151c11",
858
+ "card-foreground": "#ecf0ea",
859
+ "popover": "#1b2317",
860
+ "popover-foreground": "#ecf0ea",
861
+ "primary": "#97bf82",
862
+ "primary-foreground": "#0d140a",
863
+ "secondary": "#232c1e",
864
+ "secondary-foreground": "#ecf0ea",
865
+ "muted": "#1f271c",
866
+ "muted-foreground": "#a6aea1",
867
+ "accent": "#293821",
868
+ "accent-foreground": "#ecf0ea",
869
+ "destructive": "#f66c6d",
870
+ "border": "#2d3628",
871
+ "input": "#3c4637",
872
+ "ring": "#97bf82",
873
+ "chart-1": "#97bf82",
874
+ },
875
+ },
876
+ {
877
+ id: "crimson",
878
+ name: "Crimson",
879
+ light: {
880
+ "background": "#fff7f6",
881
+ "foreground": "#331919",
882
+ "card": "#fffcfc",
883
+ "card-foreground": "#331919",
884
+ "popover": "#fffcfc",
885
+ "popover-foreground": "#331919",
886
+ "primary": "#c13a46",
887
+ "primary-foreground": "#fcfcfc",
888
+ "secondary": "#ffe1df",
889
+ "secondary-foreground": "#331919",
890
+ "muted": "#ffeae9",
891
+ "muted-foreground": "#785252",
892
+ "accent": "#ffd7d5",
893
+ "accent-foreground": "#331919",
894
+ "destructive": "#d02b31",
895
+ "border": "#f4d6d5",
896
+ "input": "#e9c3c2",
897
+ "ring": "#c13a46",
898
+ "chart-1": "#c13a46",
899
+ },
900
+ dark: {
901
+ "background": "#1e0b0b",
902
+ "foreground": "#f8ebeb",
903
+ "card": "#281212",
904
+ "card-foreground": "#f8ebeb",
905
+ "popover": "#301919",
906
+ "popover-foreground": "#f8ebeb",
907
+ "primary": "#ff898b",
908
+ "primary-foreground": "#1e0b0b",
909
+ "secondary": "#3b2020",
910
+ "secondary-foreground": "#f8ebeb",
911
+ "muted": "#331d1d",
912
+ "muted-foreground": "#bea4a3",
913
+ "accent": "#502324",
914
+ "accent-foreground": "#f8ebeb",
915
+ "destructive": "#f66c6d",
916
+ "border": "#462a2a",
917
+ "input": "#573939",
918
+ "ring": "#ff898b",
919
+ "chart-1": "#ff898b",
920
+ },
921
+ },
645
922
  ]
646
923
 
647
924
  /** Colours for a preview swatch, per mode: the default's own values by hand, the rest from the theme. */