@kevin5251984/guild 0.2.20 → 0.2.22

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/src/router.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import type { IncomingMessage, ServerResponse } from "node:http";
3
+ import { extname } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import type { LibraryKind, ModelRef, ModelsFile } from "@guild/protocol";
5
6
  import {
@@ -24,6 +25,8 @@ import {
24
25
  getBotDetail,
25
26
  getLiveTurn,
26
27
  abortLiveTurn,
28
+ pauseLiveTurn,
29
+ continueLiveTurn,
27
30
  healthPayload,
28
31
  importSkills,
29
32
  mergeModelsFile,
@@ -64,6 +67,30 @@ import { hostGit, hostList, hostRead, hostTree } from "./host-browse.ts";
64
67
  import { listHostSkills } from "./host-skills.ts";
65
68
  import { listHostAgents } from "./host-agents.ts";
66
69
  import { generatedDir, isSafeGeneratedName } from "./image-gen.ts";
70
+ import {
71
+ createCronJob,
72
+ fireCronJob,
73
+ pauseCronJob,
74
+ publicCronJob,
75
+ removeCronJob,
76
+ resumeCronJob,
77
+ } from "./cron.ts";
78
+ import {
79
+ allowedWorkspaceWritePath,
80
+ resolveToolPath,
81
+ workspaceFromEnv,
82
+ } from "./harness.ts";
83
+
84
+ const LOCAL_IMAGE_CAP = 12 * 1024 * 1024;
85
+
86
+ function localImageType(path: string): string | null {
87
+ const ext = extname(path).toLowerCase();
88
+ if (ext === ".png") return "image/png";
89
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
90
+ if (ext === ".gif") return "image/gif";
91
+ if (ext === ".webp") return "image/webp";
92
+ return null;
93
+ }
67
94
 
68
95
  const PUBLIC = fileURLToPath(new URL("./public/", import.meta.url));
69
96
 
@@ -271,7 +298,8 @@ function modelRefFrom(value: unknown): ModelRef | null {
271
298
  const provider = str(rec, "provider").trim();
272
299
  const model = str(rec, "model").trim();
273
300
  if (!provider || !model) return null;
274
- return { provider, model };
301
+ const reasoning = str(rec, "reasoning").trim();
302
+ return reasoning ? { provider, model, reasoning } : { provider, model };
275
303
  }
276
304
 
277
305
  function strList(record: Record<string, unknown>, key: string): string[] {
@@ -388,6 +416,38 @@ export async function handleRequest(
388
416
  return;
389
417
  }
390
418
 
419
+ if (method === "GET" && path === "/local") {
420
+ const raw = requestUrl(req).searchParams.get("p") || "";
421
+ const type = localImageType(raw);
422
+ if (!raw.startsWith("/") || raw.includes("\0") || raw.includes("..") || !type) {
423
+ json(res, 404, { error: "not_found", path });
424
+ return;
425
+ }
426
+ const target = resolveToolPath(raw);
427
+ if (!allowedWorkspaceWritePath(target, workspaceFromEnv(env), store.dataDir)) {
428
+ json(res, 404, { error: "not_found", path });
429
+ return;
430
+ }
431
+ if (!existsSync(target)) {
432
+ json(res, 404, { error: "not_found", path });
433
+ return;
434
+ }
435
+ const st = statSync(target);
436
+ if (!st.isFile() || st.size > LOCAL_IMAGE_CAP) {
437
+ json(res, 404, { error: "not_found", path });
438
+ return;
439
+ }
440
+ const bytes = readFileSync(target);
441
+ res.writeHead(200, {
442
+ "content-type": type,
443
+ "content-length": bytes.length,
444
+ "cache-control": "private, max-age=60",
445
+ "x-content-type-options": "nosniff",
446
+ });
447
+ res.end(bytes);
448
+ return;
449
+ }
450
+
391
451
  if (method === "GET" && path.startsWith("/generated/")) {
392
452
  const name = decodeURIComponent(path.slice("/generated/".length));
393
453
  if (!isSafeGeneratedName(name)) {
@@ -441,6 +501,45 @@ export async function handleRequest(
441
501
  return;
442
502
  }
443
503
 
504
+ if (path === "/cron" && method === "GET") {
505
+ const room = requestUrl(req).searchParams.get("room") || "";
506
+ json(res, 200, {
507
+ jobs: store.listCronJobs(room || undefined).map(publicCronJob),
508
+ });
509
+ return;
510
+ }
511
+ if (path === "/cron" && method === "POST") {
512
+ const body = asRecord(await readJson(req));
513
+ const job = createCronJob(store, {
514
+ roomId: str(body, "roomId") || str(body, "room_id"),
515
+ botId: str(body, "botId") || str(body, "bot_id"),
516
+ prompt: str(body, "prompt"),
517
+ schedule: str(body, "schedule"),
518
+ name: str(body, "name"),
519
+ });
520
+ json(res, 201, publicCronJob(job));
521
+ return;
522
+ }
523
+ const cronAct = path.match(/^\/cron\/([^/]+)\/(pause|resume|run)$/);
524
+ if (cronAct && method === "POST") {
525
+ const id = decodeURIComponent(cronAct[1]);
526
+ if (cronAct[2] === "pause") {
527
+ json(res, 200, publicCronJob(pauseCronJob(store, id)));
528
+ return;
529
+ }
530
+ if (cronAct[2] === "resume") {
531
+ json(res, 200, publicCronJob(resumeCronJob(store, id)));
532
+ return;
533
+ }
534
+ json(res, 200, await fireCronJob(store, id, env));
535
+ return;
536
+ }
537
+ const cronOne = path.match(/^\/cron\/([^/]+)$/);
538
+ if (cronOne && method === "DELETE") {
539
+ json(res, 200, removeCronJob(store, decodeURIComponent(cronOne[1])));
540
+ return;
541
+ }
542
+
444
543
  if (method === "GET" && (path === "/bots" || path === "/bench")) {
445
544
  json(res, 200, listBench(store));
446
545
  return;
@@ -709,6 +808,66 @@ export async function handleRequest(
709
808
  return;
710
809
  }
711
810
 
811
+ const channelPause = path.match(/^\/channels\/([^/]+)\/pause$/);
812
+ if (channelPause && method === "POST") {
813
+ const body = asRecord(await readJson(req));
814
+ json(
815
+ res,
816
+ 200,
817
+ pauseLiveTurn(
818
+ store,
819
+ decodeURIComponent(channelPause[1]),
820
+ str(body, "botId") || undefined,
821
+ ),
822
+ );
823
+ return;
824
+ }
825
+ const dmPause = path.match(/^\/dms\/([^/]+)\/pause$/);
826
+ if (dmPause && method === "POST") {
827
+ const room = openDm(store, decodeURIComponent(dmPause[1]));
828
+ const body = asRecord(await readJson(req));
829
+ json(
830
+ res,
831
+ 200,
832
+ pauseLiveTurn(store, room.id, str(body, "botId") || undefined),
833
+ );
834
+ return;
835
+ }
836
+
837
+ const channelContinue = path.match(/^\/channels\/([^/]+)\/continue$/);
838
+ if (channelContinue && method === "POST") {
839
+ const body = asRecord(await readJson(req));
840
+ json(
841
+ res,
842
+ 200,
843
+ await continueLiveTurn(
844
+ store,
845
+ decodeURIComponent(channelContinue[1]),
846
+ str(body, "botId"),
847
+ env,
848
+ extras,
849
+ ),
850
+ );
851
+ return;
852
+ }
853
+ const dmContinue = path.match(/^\/dms\/([^/]+)\/continue$/);
854
+ if (dmContinue && method === "POST") {
855
+ const room = openDm(store, decodeURIComponent(dmContinue[1]));
856
+ const body = asRecord(await readJson(req));
857
+ json(
858
+ res,
859
+ 200,
860
+ await continueLiveTurn(
861
+ store,
862
+ room.id,
863
+ str(body, "botId"),
864
+ env,
865
+ extras,
866
+ ),
867
+ );
868
+ return;
869
+ }
870
+
712
871
  const channelSteer = path.match(/^\/channels\/([^/]+)\/steer$/);
713
872
  if (channelSteer && method === "POST") {
714
873
  const body = asRecord(await readJson(req));
package/src/store.ts CHANGED
@@ -10,7 +10,7 @@ import type { TrajectoryDraft, TrajectoryEvent } from "./trajectory.ts";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { randomUUID } from "node:crypto";
13
- import { closeGuildDb, openGuildDb, type GuildDb } from "./db.ts";
13
+ import { closeGuildDb, openGuildDb, type CronJobRow, type GuildDb } from "./db.ts";
14
14
  import type {
15
15
  Bot,
16
16
  ChatAttachment,
@@ -101,6 +101,9 @@ export type LiveTurn = {
101
101
  startedAt?: string;
102
102
  /** Full-ish tool history for Trajectory. Stripped from GET /live. */
103
103
  traces?: LiveTrace[];
104
+ /** Seat assignment text, kept so Continue can resume after Pause. */
105
+ asked?: string;
106
+ paused?: boolean;
104
107
  };
105
108
 
106
109
  export class GuildStore {
@@ -366,7 +369,6 @@ export class GuildStore {
366
369
  beginTurn(roomId: string, botIds: string[] = [""]): AbortSignal {
367
370
  const controller = new AbortController();
368
371
  const ids = botIds.length ? botIds : [""];
369
- for (const botId of ids) this.bindBotAbort(roomId, botId, controller);
370
372
  this.turnGroups.set(controller.signal, {
371
373
  roomId,
372
374
  botIds: new Set(ids),
@@ -375,43 +377,54 @@ export class GuildStore {
375
377
  return controller.signal;
376
378
  }
377
379
 
380
+ /**
381
+ * Per-seat AbortController, child of the turn group. Pause/Stop one bot
382
+ * without taking the rest of the wave down.
383
+ */
384
+ armBotTurn(roomId: string, botId: string, parent: AbortSignal): AbortSignal {
385
+ const controller = new AbortController();
386
+ const onParent = () => {
387
+ if (!controller.signal.aborted) controller.abort();
388
+ };
389
+ if (parent.aborted) onParent();
390
+ else parent.addEventListener("abort", onParent, { once: true });
391
+ this.bindBotAbort(roomId, botId, controller);
392
+ const group = this.turnGroups.get(parent);
393
+ if (group && group.roomId === roomId) group.botIds.add(botId);
394
+ return controller.signal;
395
+ }
396
+
378
397
  adoptTurn(roomId: string, botId: string, signal: AbortSignal): void {
379
398
  const group = this.turnGroups.get(signal);
380
399
  if (!group || group.roomId !== roomId) return;
381
- this.bindBotAbort(roomId, botId, group.controller);
382
400
  group.botIds.add(botId);
383
401
  }
384
402
 
403
+ private dropBotLive(roomId: string, botId: string): boolean {
404
+ const live = this.liveTurns.get(roomId);
405
+ const steers = this.pendingSteers.get(roomId);
406
+ const room = this.botAborts.get(roomId);
407
+ const hadLive = Boolean(live?.delete(botId));
408
+ steers?.delete(botId);
409
+ room?.delete(botId);
410
+ if (live && live.size === 0) this.liveTurns.delete(roomId);
411
+ if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
412
+ if (room && room.size === 0) this.botAborts.delete(roomId);
413
+ for (const [sig, group] of this.turnGroups) {
414
+ if (group.roomId !== roomId || !group.botIds.has(botId)) continue;
415
+ group.botIds.delete(botId);
416
+ if (group.botIds.size === 0) this.turnGroups.delete(sig);
417
+ }
418
+ return hadLive;
419
+ }
420
+
385
421
  abortTurn(roomId: string, botId?: string): boolean {
386
422
  if (botId) {
387
- const room = this.botAborts.get(roomId);
388
- const controller = room?.get(botId);
389
- if (!controller) {
390
- const live = this.liveTurns.get(roomId);
391
- const steers = this.pendingSteers.get(roomId);
392
- const hadLive = Boolean(live?.delete(botId));
393
- steers?.delete(botId);
394
- if (live && live.size === 0) this.liveTurns.delete(roomId);
395
- if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
396
- this.spillTrajectoryIfIdle(roomId);
397
- return hadLive;
398
- }
399
- const group = this.turnGroups.get(controller.signal);
400
- const ids = group ? [...group.botIds] : [botId];
401
- this.turnGroups.delete(controller.signal);
402
- const live = this.liveTurns.get(roomId);
403
- const steers = this.pendingSteers.get(roomId);
404
- for (const id of ids) {
405
- room?.delete(id);
406
- live?.delete(id);
407
- steers?.delete(id);
408
- }
409
- if (room && room.size === 0) this.botAborts.delete(roomId);
410
- if (live && live.size === 0) this.liveTurns.delete(roomId);
411
- if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
412
- if (!controller.signal.aborted) controller.abort();
423
+ const controller = this.botAborts.get(roomId)?.get(botId);
424
+ const hadLive = this.dropBotLive(roomId, botId);
425
+ if (controller && !controller.signal.aborted) controller.abort();
413
426
  this.spillTrajectoryIfIdle(roomId);
414
- return true;
427
+ return hadLive || Boolean(controller);
415
428
  }
416
429
  const room = this.botAborts.get(roomId);
417
430
  this.botAborts.delete(roomId);
@@ -419,10 +432,18 @@ export class GuildStore {
419
432
  this.pendingSteers.delete(roomId);
420
433
  let aborted = false;
421
434
  const seen = new Set<AbortController>();
435
+ for (const [sig, group] of [...this.turnGroups]) {
436
+ if (group.roomId !== roomId) continue;
437
+ this.turnGroups.delete(sig);
438
+ if (!group.controller.signal.aborted) {
439
+ group.controller.abort();
440
+ aborted = true;
441
+ }
442
+ seen.add(group.controller);
443
+ }
422
444
  for (const controller of room?.values() ?? []) {
423
445
  if (seen.has(controller)) continue;
424
446
  seen.add(controller);
425
- this.turnGroups.delete(controller.signal);
426
447
  if (!controller.signal.aborted) {
427
448
  controller.abort();
428
449
  aborted = true;
@@ -432,6 +453,37 @@ export class GuildStore {
432
453
  return aborted || Boolean(room);
433
454
  }
434
455
 
456
+ pauseTurn(roomId: string, botId?: string): boolean {
457
+ const ids = botId
458
+ ? [botId]
459
+ : [...(this.liveTurns.get(roomId)?.keys() ?? [])];
460
+ let any = false;
461
+ for (const id of ids) {
462
+ if (!id) continue;
463
+ const live = this.getLiveBotTurn(roomId, id);
464
+ if (!live) continue;
465
+ const traces = (live.traces || []).map((tr) =>
466
+ tr.running ? { ...tr, running: false, text: tr.text || "paused" } : tr,
467
+ );
468
+ const steps = (live.steps || []).map((step) =>
469
+ step.running ? { ...step, running: false } : step,
470
+ );
471
+ this.setLiveTurn(roomId, {
472
+ ...live,
473
+ traces,
474
+ steps,
475
+ paused: true,
476
+ });
477
+ const controller = this.botAborts.get(roomId)?.get(id);
478
+ this.botAborts.get(roomId)?.delete(id);
479
+ const room = this.botAborts.get(roomId);
480
+ if (room && room.size === 0) this.botAborts.delete(roomId);
481
+ if (controller && !controller.signal.aborted) controller.abort();
482
+ any = true;
483
+ }
484
+ return any;
485
+ }
486
+
435
487
  endTurn(roomId: string, signal?: AbortSignal): void {
436
488
  const group = signal ? this.turnGroups.get(signal) : undefined;
437
489
  if (group) {
@@ -440,7 +492,11 @@ export class GuildStore {
440
492
  const live = this.liveTurns.get(roomId);
441
493
  const steers = this.pendingSteers.get(roomId);
442
494
  for (const botId of group.botIds) {
443
- if (room?.get(botId) === group.controller) room.delete(botId);
495
+ if (live?.get(botId)?.paused) {
496
+ room?.delete(botId);
497
+ continue;
498
+ }
499
+ room?.delete(botId);
444
500
  live?.delete(botId);
445
501
  steers?.delete(botId);
446
502
  }
@@ -449,8 +505,14 @@ export class GuildStore {
449
505
  if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
450
506
  if (!group.controller.signal.aborted) group.controller.abort();
451
507
  } else {
508
+ const live = this.liveTurns.get(roomId);
509
+ const kept = new Map<string, LiveTurn>();
510
+ for (const [id, turn] of live ?? []) {
511
+ if (turn.paused) kept.set(id, turn);
512
+ }
452
513
  this.botAborts.delete(roomId);
453
- this.clearLiveTurn(roomId);
514
+ if (kept.size) this.liveTurns.set(roomId, kept);
515
+ else this.clearLiveTurn(roomId);
454
516
  }
455
517
  this.spillTrajectoryIfIdle(roomId);
456
518
  }
@@ -1267,6 +1329,27 @@ export class GuildStore {
1267
1329
  if (!this.getRoom(roomId)) throw new StoreError(404, "room not found");
1268
1330
  this.db.writeCompact(roomId, compact);
1269
1331
  }
1332
+
1333
+ listCronJobs(roomId?: string) {
1334
+ if (roomId && !this.getRoom(roomId)) throw new StoreError(404, "room not found");
1335
+ return this.db.listCronJobs(roomId);
1336
+ }
1337
+
1338
+ getCronJob(id: string) {
1339
+ const job = this.db.getCronJob(id);
1340
+ if (!job) throw new StoreError(404, "cron job not found");
1341
+ return job;
1342
+ }
1343
+
1344
+ writeCronJob(job: CronJobRow): void {
1345
+ if (!this.getRoom(job.roomId)) throw new StoreError(404, "room not found");
1346
+ if (!this.getBot(job.botId)) throw new StoreError(400, "bot not found");
1347
+ this.db.upsertCronJob(job);
1348
+ }
1349
+
1350
+ deleteCronJob(id: string): boolean {
1351
+ return this.db.deleteCronJob(id);
1352
+ }
1270
1353
  }
1271
1354
 
1272
1355
  function normalizePortrait(raw: string | null | undefined): string | undefined {
package/src/subagent.ts CHANGED
@@ -373,6 +373,9 @@ export async function spawnSubagent(input: {
373
373
  allowWrite: child.allowWrite,
374
374
  sandbox: child.sandbox,
375
375
  workspace: input.ctx.workspace,
376
+ roomId: input.ctx.roomId,
377
+ botId: input.ctx.botId,
378
+ cronRun: input.ctx.cronRun,
376
379
  dispatch: input.ctx.dispatch,
377
380
  signal: input.ctx.signal,
378
381
  },
package/src/tools.ts CHANGED
@@ -78,6 +78,12 @@ export type ToolContext = {
78
78
  ) => Promise<ToolOutcome>;
79
79
  /** Devin-style background spawn handles for this turn. Same Map across dispatch clones. */
80
80
  spawnHandles?: Map<string, SpawnHandle>;
81
+ /** Channel or DM this turn is in. Cron jobs default here. */
82
+ roomId?: string;
83
+ /** Seat running this turn. cronjob create defaults here. */
84
+ botId?: string;
85
+ /** Hermes: cron child sessions cannot manage cron. */
86
+ cronRun?: boolean;
81
87
  };
82
88
 
83
89
  export type SpawnHandle = {
@@ -191,6 +197,33 @@ export function guildTools(
191
197
  (tool) => tool.name !== "image_gen" && tool.name !== "browser",
192
198
  );
193
199
  }
200
+ if (!ctx.cronRun) {
201
+ tools.push({
202
+ name: "cronjob",
203
+ description:
204
+ "Schedule a later hall turn (Hermes cronjob). Fresh @handle turn with a self-contained prompt. Actions: create, list, pause, resume, run, remove. schedule may be natural language (每10分鐘, 10分鐘後, 每天9點, in 30 minutes, every 2h, 0 9 * * *, ISO). Split when vs task: schedule is the time phrase, prompt is the work. bot_id defaults to this seat. Do not create cron jobs from a cron run.",
205
+ parameters: Type.Object({
206
+ action: Type.String({
207
+ description: "create | list | pause | resume | run | remove",
208
+ }),
209
+ schedule: Type.Optional(
210
+ Type.String({
211
+ description:
212
+ "Natural language or Hermes form: 每10分鐘, in 30m, every 2h, 每天9點, 0 9 * * *, ISO",
213
+ }),
214
+ ),
215
+ prompt: Type.Optional(
216
+ Type.String({ description: "Self-contained task for the seat" }),
217
+ ),
218
+ name: Type.Optional(Type.String({ description: "Short job name" })),
219
+ job_id: Type.Optional(Type.String({ description: "Job id or name" })),
220
+ bot_id: Type.Optional(Type.String({ description: "Seat to run" })),
221
+ room_id: Type.Optional(
222
+ Type.String({ description: "Room id. Defaults to this hall." }),
223
+ ),
224
+ }),
225
+ });
226
+ }
194
227
  tools.push({
195
228
  name: "skill",
196
229
  description: `Load a staffed skill's full instructions by name.${available}`,
@@ -387,6 +420,21 @@ function openaiParameters(name: string): {
387
420
  required: ["prompt"],
388
421
  };
389
422
  }
423
+ if (name === "cronjob") {
424
+ return {
425
+ type: "object",
426
+ properties: {
427
+ action: { type: "string", description: "create | list | pause | resume | run | remove" },
428
+ schedule: { type: "string" },
429
+ prompt: { type: "string" },
430
+ name: { type: "string" },
431
+ job_id: { type: "string" },
432
+ bot_id: { type: "string" },
433
+ room_id: { type: "string" },
434
+ },
435
+ required: ["action"],
436
+ };
437
+ }
390
438
  if (name === "browser") {
391
439
  return {
392
440
  type: "object",
@@ -437,6 +485,7 @@ export const BUILTIN_TOOL_NAMES = [
437
485
  "read_spawn",
438
486
  "image_gen",
439
487
  "browser",
488
+ "cronjob",
440
489
  ] as const;
441
490
 
442
491
  export async function executeTool(
@@ -512,6 +561,12 @@ export async function builtinExecute(
512
561
  env: ctx.env,
513
562
  });
514
563
  }
564
+ if (name === "cronjob") {
565
+ return {
566
+ text: "cronjob needs guildd (the cron plugin)",
567
+ isError: true,
568
+ };
569
+ }
515
570
  if (name === "browser") {
516
571
  const { runBrowser } = await import("./browser.ts");
517
572
  return runBrowser(args, {
@@ -817,7 +872,7 @@ export function nextToolRound(round: number): ToolRoundPhase {
817
872
  }
818
873
 
819
874
  export const TOOL_SYSTEM = `You ARE already running on the user's local computer (Guild, same design as Pi / DeepSeek Harness).
820
- Tools: run, read, write, list, skill, spawn, image_gen, browser, plus any connected MCP tools (names start with mcp__).
875
+ Tools: run, read, write, list, skill, spawn, image_gen, browser, cronjob, plus any connected MCP tools (names start with mcp__).
821
876
  You can inspect RAM, disk, CPU, processes, files, and run shell commands.
822
877
  Never say you cannot access this machine. Never tell the user to run the command themselves.
823
878
  When the question is about this computer, call tools first, then answer with evidence from the output.
@@ -827,4 +882,5 @@ You stay coordinator. Spawn is the specialist, not a last resort (Devin run_suba
827
882
  Independent tool calls in one round also run in parallel — fire several reads/searches together.
828
883
  Check the [exit code: N] marker on every run result; investigate failures before moving on. Prefer the workdir argument over cd.
829
884
  To follow a staffed skill, call skill with its exact name (or slug) before applying it. Relative paths in a skill resolve against that skill's base directory.
885
+ When the user asks to 排程 / schedule a later hall turn — including natural-language times like 每10分鐘, 10分鐘後, tomorrow 9am — call cronjob create. schedule is the time phrase; prompt is the self-contained task (the job will not see this live turn). bot_id defaults to you. Also accepts in 30m, every 2h, 0 9 * * *, ISO. A cron run cannot create more cron jobs.
830
886
  Prefer small commands. macOS RAM: sysctl hw.memsize ; memory_pressure. Disk: df -h.`;
@@ -6,7 +6,12 @@ export type HealthResponse = {
6
6
 
7
7
  export type BotStatus = "bench" | "staffed" | "running" | "retired";
8
8
 
9
- export type ModelRef = { provider: string; model: string };
9
+ export type ModelRef = {
10
+ provider: string;
11
+ model: string;
12
+ /** Seat-specific effort. Missing → that model's catalog default. */
13
+ reasoning?: string;
14
+ };
10
15
 
11
16
  export type LibraryKind =
12
17
  | "souls"
@@ -169,7 +174,7 @@ export type AuxRole =
169
174
 
170
175
  export type ModelsFile = {
171
176
  default?: ModelRef | null;
172
- /** Last chosen effort string (catalog-defined: low, high, xhigh, …). */
177
+ /** Guild-default effort when a seat has no `model.reasoning`. */
173
178
  reasoning?: string;
174
179
  fast?: boolean;
175
180
  aux?: Partial<Record<AuxRole, ModelRef | null>>;