@kevin5251984/guild 0.2.19 → 0.2.21

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/store.ts CHANGED
@@ -26,7 +26,11 @@ import { DEFAULT_BOTS } from "./catalog/default-bots.ts";
26
26
  import { CATALOG_SKILLS } from "./catalog/skills.ts";
27
27
  import { CATALOG_SUBAGENTS } from "./catalog/subagents.ts";
28
28
  import { parseAgentFile } from "./agent-file.ts";
29
- import { parseMentionIds, sanitizeMentionIds } from "./mention.ts";
29
+ import {
30
+ parseMentionIds,
31
+ sanitizeMentionIds,
32
+ withoutDeferredIds,
33
+ } from "./mention.ts";
30
34
 
31
35
  const MARKDOWN: Record<LibraryKind, string> = {
32
36
  souls: "SOUL.md",
@@ -97,6 +101,9 @@ export type LiveTurn = {
97
101
  startedAt?: string;
98
102
  /** Full-ish tool history for Trajectory. Stripped from GET /live. */
99
103
  traces?: LiveTrace[];
104
+ /** Seat assignment text, kept so Continue can resume after Pause. */
105
+ asked?: string;
106
+ paused?: boolean;
100
107
  };
101
108
 
102
109
  export class GuildStore {
@@ -362,7 +369,6 @@ export class GuildStore {
362
369
  beginTurn(roomId: string, botIds: string[] = [""]): AbortSignal {
363
370
  const controller = new AbortController();
364
371
  const ids = botIds.length ? botIds : [""];
365
- for (const botId of ids) this.bindBotAbort(roomId, botId, controller);
366
372
  this.turnGroups.set(controller.signal, {
367
373
  roomId,
368
374
  botIds: new Set(ids),
@@ -371,43 +377,54 @@ export class GuildStore {
371
377
  return controller.signal;
372
378
  }
373
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
+
374
397
  adoptTurn(roomId: string, botId: string, signal: AbortSignal): void {
375
398
  const group = this.turnGroups.get(signal);
376
399
  if (!group || group.roomId !== roomId) return;
377
- this.bindBotAbort(roomId, botId, group.controller);
378
400
  group.botIds.add(botId);
379
401
  }
380
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
+
381
421
  abortTurn(roomId: string, botId?: string): boolean {
382
422
  if (botId) {
383
- const room = this.botAborts.get(roomId);
384
- const controller = room?.get(botId);
385
- if (!controller) {
386
- const live = this.liveTurns.get(roomId);
387
- const steers = this.pendingSteers.get(roomId);
388
- const hadLive = Boolean(live?.delete(botId));
389
- steers?.delete(botId);
390
- if (live && live.size === 0) this.liveTurns.delete(roomId);
391
- if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
392
- this.spillTrajectoryIfIdle(roomId);
393
- return hadLive;
394
- }
395
- const group = this.turnGroups.get(controller.signal);
396
- const ids = group ? [...group.botIds] : [botId];
397
- this.turnGroups.delete(controller.signal);
398
- const live = this.liveTurns.get(roomId);
399
- const steers = this.pendingSteers.get(roomId);
400
- for (const id of ids) {
401
- room?.delete(id);
402
- live?.delete(id);
403
- steers?.delete(id);
404
- }
405
- if (room && room.size === 0) this.botAborts.delete(roomId);
406
- if (live && live.size === 0) this.liveTurns.delete(roomId);
407
- if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
408
- 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();
409
426
  this.spillTrajectoryIfIdle(roomId);
410
- return true;
427
+ return hadLive || Boolean(controller);
411
428
  }
412
429
  const room = this.botAborts.get(roomId);
413
430
  this.botAborts.delete(roomId);
@@ -415,10 +432,18 @@ export class GuildStore {
415
432
  this.pendingSteers.delete(roomId);
416
433
  let aborted = false;
417
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
+ }
418
444
  for (const controller of room?.values() ?? []) {
419
445
  if (seen.has(controller)) continue;
420
446
  seen.add(controller);
421
- this.turnGroups.delete(controller.signal);
422
447
  if (!controller.signal.aborted) {
423
448
  controller.abort();
424
449
  aborted = true;
@@ -428,6 +453,37 @@ export class GuildStore {
428
453
  return aborted || Boolean(room);
429
454
  }
430
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
+
431
487
  endTurn(roomId: string, signal?: AbortSignal): void {
432
488
  const group = signal ? this.turnGroups.get(signal) : undefined;
433
489
  if (group) {
@@ -436,7 +492,11 @@ export class GuildStore {
436
492
  const live = this.liveTurns.get(roomId);
437
493
  const steers = this.pendingSteers.get(roomId);
438
494
  for (const botId of group.botIds) {
439
- 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);
440
500
  live?.delete(botId);
441
501
  steers?.delete(botId);
442
502
  }
@@ -445,8 +505,14 @@ export class GuildStore {
445
505
  if (steers && steers.size === 0) this.pendingSteers.delete(roomId);
446
506
  if (!group.controller.signal.aborted) group.controller.abort();
447
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
+ }
448
513
  this.botAborts.delete(roomId);
449
- this.clearLiveTurn(roomId);
514
+ if (kept.size) this.liveTurns.set(roomId, kept);
515
+ else this.clearLiveTurn(roomId);
450
516
  }
451
517
  this.spillTrajectoryIfIdle(roomId);
452
518
  }
@@ -989,11 +1055,14 @@ export class GuildStore {
989
1055
  const now = new Date().toISOString();
990
1056
  const startedAt = author !== "you" ? usage?.startedAt : undefined;
991
1057
  const bots = this.listBots();
992
- const mentionIds = (
993
- mentions !== undefined
1058
+ const mentionIds = withoutDeferredIds(
1059
+ (mentions !== undefined
994
1060
  ? sanitizeMentionIds(mentions, bots)
995
1061
  : parseMentionIds(text, bots, author === "you" ? "user" : "bot")
996
- ).filter((id) => id !== author);
1062
+ ).filter((id) => id !== author),
1063
+ text,
1064
+ bots,
1065
+ );
997
1066
  const message: ChatMessage = {
998
1067
  id: randomUUID(),
999
1068
  roomId,
@@ -1022,10 +1091,12 @@ export class GuildStore {
1022
1091
  const text = body.trim();
1023
1092
  if (!text) throw new StoreError(400, "message is required");
1024
1093
  const bots = this.listBots();
1025
- const mentionIds = (
1094
+ const mentionIds = withoutDeferredIds(
1026
1095
  mentions !== undefined
1027
1096
  ? sanitizeMentionIds(mentions, bots)
1028
- : parseMentionIds(text, bots, "user")
1097
+ : parseMentionIds(text, bots, "user"),
1098
+ text,
1099
+ bots,
1029
1100
  );
1030
1101
  const next = this.db.updateMessageBody(roomId, messageId, text, mentionIds);
1031
1102
  if (!next) throw new StoreError(404, "message not found");
package/src/subagent.ts CHANGED
@@ -73,16 +73,21 @@ function agentKey(value: string): string {
73
73
  return value.trim().replace(/^\/+/, "").toLowerCase();
74
74
  }
75
75
 
76
- /** Parent read_only cannot escalate via spawn. Explorer from full_access keeps run. */
76
+ /**
77
+ * A child never outruns its parent, and a read-only agent never gets `run`
78
+ * back: a readOnly child is pinned to read_only even under a full_access
79
+ * parent (that used to be an escalation hole — CHILD_TOOLS_RO advertised run
80
+ * and gateTool let a full_access child through it).
81
+ */
77
82
  export function childSpawnPolicy(
78
83
  parentSandbox: ToolContext["sandbox"],
79
84
  agentReadOnly: boolean,
80
85
  ): { sandbox: Sandbox; allowWrite: boolean } {
81
86
  const parent = parseSandbox(parentSandbox);
82
- if (parent === "read_only") {
87
+ if (parent === "read_only" || agentReadOnly) {
83
88
  return { sandbox: "read_only", allowWrite: false };
84
89
  }
85
- return { sandbox: parent, allowWrite: !agentReadOnly };
90
+ return { sandbox: parent, allowWrite: true };
86
91
  }
87
92
 
88
93
  export function resolveSubagent(
@@ -108,8 +113,8 @@ Never say you cannot access this machine. Check [exit code: N] on every run.
108
113
  Independent searches: emit multiple tool calls in one round; they run in parallel.`;
109
114
 
110
115
  const CHILD_TOOLS_RO = `You ARE already running on the user's local computer (Guild).
111
- Tools: run, read, list, skill. You cannot write files and cannot spawn subagents.
112
- Read-only. Never edit, patch, or create files. Check [exit code: N] on every run.
116
+ Tools: read, list, skill. You cannot run shell commands, cannot write files, and cannot spawn subagents.
117
+ Read-only. Never edit, patch, or create files.
113
118
  Independent searches: emit multiple tool calls in one round; they run in parallel.`;
114
119
 
115
120
  export const SPAWN_MAX_PARALLEL = 8;
package/src/tools.ts CHANGED
@@ -7,6 +7,7 @@ import { Type, type Tool } from "@earendil-works/pi-ai";
7
7
  import { listHostSkills } from "./host-skills.ts";
8
8
  import type { McpToolRef } from "./mcp.ts";
9
9
  import {
10
+ defaultWorkspace,
10
11
  gateTool,
11
12
  parseSandbox,
12
13
  resolveToolPath,
@@ -186,7 +187,9 @@ export function guildTools(
186
187
  (tool) => tool.name === "read" || tool.name === "list",
187
188
  );
188
189
  } else if (sandbox === "workspace_write") {
189
- tools = tools.filter((tool) => tool.name !== "image_gen");
190
+ tools = tools.filter(
191
+ (tool) => tool.name !== "image_gen" && tool.name !== "browser",
192
+ );
190
193
  }
191
194
  tools.push({
192
195
  name: "skill",
@@ -465,9 +468,11 @@ export async function builtinExecute(
465
468
  ctx: ToolContext = {},
466
469
  ): Promise<ToolOutcome> {
467
470
  try {
471
+ // workspace_write resolves relative paths from the workspace root, which
472
+ // defaults to the guild checkout (same root gateTool checks against).
468
473
  const pathBase =
469
- parseSandbox(ctx.sandbox) === "workspace_write" && ctx.workspace
470
- ? resolveToolPath(ctx.workspace)
474
+ parseSandbox(ctx.sandbox) === "workspace_write"
475
+ ? resolveToolPath(ctx.workspace?.trim() || defaultWorkspace())
471
476
  : HOME;
472
477
  if (name === "run") {
473
478
  return await runCommand(
package/src/version.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ /**
4
+ * Outbound User-Agent. npm ships this package's source, so the manifest always
5
+ * sits one level above `src/` — same lookup as `--version` in cli.ts.
6
+ */
7
+ export function guildUserAgent(): string {
8
+ return `Guild/${guildVersion()}`;
9
+ }
10
+
11
+ let cached: string | null = null;
12
+
13
+ export function guildVersion(): string {
14
+ if (cached !== null) return cached;
15
+ try {
16
+ const pkg = JSON.parse(
17
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
18
+ ) as { version?: unknown };
19
+ const version = typeof pkg.version === "string" ? pkg.version.trim() : "";
20
+ cached = version || "0";
21
+ } catch {
22
+ cached = "0";
23
+ }
24
+ return cached;
25
+ }
@@ -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"
@@ -133,9 +138,20 @@ export type LlmApi =
133
138
  | "anthropic-messages"
134
139
  | "openai-responses";
135
140
 
141
+ export type ModelReasoning = {
142
+ /** Effort strings this model accepts, catalog order. Missing = no effort picker. */
143
+ supportedEfforts?: string[];
144
+ defaultEffort?: string;
145
+ /** When true, do not send `none` — reasoning cannot be turned off. */
146
+ mandatory?: boolean;
147
+ defaultEnabled?: boolean;
148
+ supportsMaxTokens?: boolean;
149
+ };
150
+
136
151
  export type ModelEntry = {
137
152
  id: string;
138
153
  name?: string;
154
+ reasoning?: ModelReasoning;
139
155
  };
140
156
 
141
157
  export type ProviderEntry = {
@@ -158,7 +174,8 @@ export type AuxRole =
158
174
 
159
175
  export type ModelsFile = {
160
176
  default?: ModelRef | null;
161
- reasoning?: "minimal" | "low" | "medium" | "high";
177
+ /** Guild-default effort when a seat has no `model.reasoning`. */
178
+ reasoning?: string;
162
179
  fast?: boolean;
163
180
  aux?: Partial<Record<AuxRole, ModelRef | null>>;
164
181
  recent?: ModelRef[];