@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/package.json +2 -2
- package/src/cli.ts +2 -5
- package/src/db.ts +39 -6
- package/src/generate.ts +3 -3
- package/src/handlers.ts +126 -50
- package/src/harness.ts +20 -4
- package/src/host-browse.ts +149 -4
- package/src/llm.ts +37 -15
- package/src/mcp.ts +32 -5
- package/src/mention.ts +80 -12
- package/src/oauth.ts +2 -2
- package/src/opencode-free.ts +3 -2
- package/src/public/chat.css +6 -0
- package/src/public/chat.html +260 -47
- package/src/public/i18n.js +6 -0
- package/src/public/md.js +18 -1
- package/src/public/mobile.css +224 -14
- package/src/public/mobile.html +344 -22
- package/src/public/settings.html +51 -8
- package/src/reasoning-catalog.ts +346 -0
- package/src/router.ts +172 -9
- package/src/store.ts +109 -38
- package/src/subagent.ts +10 -5
- package/src/tools.ts +8 -3
- package/src/version.ts +25 -0
- package/vendor/protocol/src/index.ts +19 -2
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 {
|
|
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
|
|
384
|
-
const
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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:
|
|
112
|
-
Read-only. Never edit, patch, or create files.
|
|
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(
|
|
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"
|
|
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 = {
|
|
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
|
-
|
|
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[];
|