@kevin5251984/guild 0.2.18 → 0.2.20
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/catalog/subagents.ts +2 -1
- package/src/chat-parts.ts +6 -1
- package/src/cli.ts +2 -5
- package/src/compact.ts +27 -0
- package/src/db.ts +236 -68
- package/src/generate.ts +59 -28
- package/src/handlers.ts +355 -84
- package/src/harness.ts +39 -10
- package/src/host-browse.ts +149 -4
- package/src/image-gen.ts +2 -1
- package/src/llm.ts +435 -71
- package/src/mcp.ts +32 -5
- package/src/memory.ts +69 -0
- package/src/mention.ts +189 -14
- package/src/oauth.ts +204 -27
- package/src/opencode-free.ts +248 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +174 -72
- package/src/public/chat.html +354 -69
- package/src/public/i18n.js +44 -6
- package/src/public/index.html +1 -46
- package/src/public/mobile.css +597 -0
- package/src/public/mobile.html +954 -0
- package/src/public/settings.html +239 -35
- package/src/public/skills-add.html +8 -0
- package/src/public/studio.html +171 -117
- package/src/public/style.css +220 -26
- package/src/public/subagents-add.html +8 -0
- package/src/reasoning-catalog.ts +346 -0
- package/src/router.ts +184 -13
- package/src/store.ts +170 -5
- package/src/subagent.ts +239 -7
- package/src/tools.ts +132 -21
- package/src/trajectory.ts +72 -9
- package/src/usage.ts +10 -0
- package/src/version.ts +25 -0
- package/vendor/protocol/src/index.ts +27 -2
package/src/handlers.ts
CHANGED
|
@@ -18,27 +18,41 @@ import {
|
|
|
18
18
|
} from "./generate.ts";
|
|
19
19
|
import {
|
|
20
20
|
liveTrajectoryEvents,
|
|
21
|
+
promoteSpawnEvent,
|
|
21
22
|
synthesizeTrajectory,
|
|
22
23
|
turnTrajectoryEvents,
|
|
23
24
|
userTrajectoryEvent,
|
|
24
25
|
} from "./trajectory.ts";
|
|
25
26
|
import { importFromGithub, importFromUrl } from "./skill-import.ts";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
harvestBotMemory,
|
|
29
|
+
harvestChannelMemory,
|
|
30
|
+
mergeQuestMemory,
|
|
31
|
+
} from "./memory.ts";
|
|
27
32
|
import { listHostSkills, type HostSkill } from "./host-skills.ts";
|
|
28
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
CHANNEL_ROSTER_CAP,
|
|
35
|
+
GuildStore,
|
|
36
|
+
StoreError,
|
|
37
|
+
type LiveStep,
|
|
38
|
+
type LiveTurn,
|
|
39
|
+
} from "./store.ts";
|
|
29
40
|
import { listSpawnRefs } from "./subagent.ts";
|
|
30
41
|
import {
|
|
31
42
|
importHostMcp,
|
|
32
43
|
listGuildMcp,
|
|
33
44
|
listHostMcp,
|
|
34
45
|
listMcpToolRefs,
|
|
46
|
+
publicMcpServer,
|
|
35
47
|
removeGuildMcp,
|
|
36
48
|
upsertGuildMcp,
|
|
37
49
|
} from "./mcp.ts";
|
|
38
50
|
import {
|
|
39
51
|
assignmentFor,
|
|
40
52
|
isBroadcastMention,
|
|
41
|
-
|
|
53
|
+
messageMentionIds,
|
|
54
|
+
parseMentionIds,
|
|
55
|
+
sanitizeMentionIds,
|
|
42
56
|
} from "./mention.ts";
|
|
43
57
|
import { slashNames } from "./slash.ts";
|
|
44
58
|
import { toHistoryItem, type HistoryItem } from "./compact.ts";
|
|
@@ -59,6 +73,8 @@ export type HandlerExtras = {
|
|
|
59
73
|
mcpTools?: McpToolRef[] | Promise<McpToolRef[]>;
|
|
60
74
|
onTurnComplete?: (turn: TurnComplete) => void;
|
|
61
75
|
turn?: (input: Parameters<typeof chatReply>[0]) => Promise<ChatReply>;
|
|
76
|
+
/** Bot ids the client already resolved from @mentions. */
|
|
77
|
+
mentions?: string[];
|
|
62
78
|
};
|
|
63
79
|
|
|
64
80
|
export function healthPayload(): HealthResponse {
|
|
@@ -77,12 +93,13 @@ export function listLibrary(store: GuildStore, kind: LibraryKind) {
|
|
|
77
93
|
return store.listLibrary(kind);
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
/** HTTP surfaces never carry `launch.env` values; see `publicMcpServer`. */
|
|
80
97
|
export function listMcpServers(store: GuildStore) {
|
|
81
|
-
return listGuildMcp(store.dataDir);
|
|
98
|
+
return listGuildMcp(store.dataDir).map(publicMcpServer);
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
export function listHostMcpServers() {
|
|
85
|
-
return listHostMcp();
|
|
102
|
+
return listHostMcp().map(publicMcpServer);
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
export function createMcpServer(
|
|
@@ -97,13 +114,15 @@ export function createMcpServer(
|
|
|
97
114
|
},
|
|
98
115
|
) {
|
|
99
116
|
try {
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
return publicMcpServer(
|
|
118
|
+
upsertGuildMcp(store.dataDir, input.name, {
|
|
119
|
+
command: input.command || "",
|
|
120
|
+
args: input.args || [],
|
|
121
|
+
env: input.env,
|
|
122
|
+
cwd: input.cwd,
|
|
123
|
+
url: input.url,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
107
126
|
} catch (error) {
|
|
108
127
|
throw new StoreError(
|
|
109
128
|
400,
|
|
@@ -114,7 +133,7 @@ export function createMcpServer(
|
|
|
114
133
|
|
|
115
134
|
export function importMcpServer(store: GuildStore, hostId: string) {
|
|
116
135
|
try {
|
|
117
|
-
return importHostMcp(store.dataDir, hostId);
|
|
136
|
+
return publicMcpServer(importHostMcp(store.dataDir, hostId));
|
|
118
137
|
} catch (error) {
|
|
119
138
|
throw new StoreError(
|
|
120
139
|
404,
|
|
@@ -242,6 +261,7 @@ export function updateBot(
|
|
|
242
261
|
name?: string;
|
|
243
262
|
handle?: string;
|
|
244
263
|
oneLiner?: string;
|
|
264
|
+
portrait?: string | null;
|
|
245
265
|
skillIds?: string[];
|
|
246
266
|
soul?: MarkdownDraft;
|
|
247
267
|
agent?: MarkdownDraft;
|
|
@@ -256,6 +276,162 @@ export function updateBot(
|
|
|
256
276
|
return store.updateBot(id, { ...input, skillIds });
|
|
257
277
|
}
|
|
258
278
|
|
|
279
|
+
const LOOK_HAIR = [
|
|
280
|
+
"jet-black short crop",
|
|
281
|
+
"jet-black long straight hair",
|
|
282
|
+
"copper-red bob with blunt bangs",
|
|
283
|
+
"copper-red messy spikes",
|
|
284
|
+
"indigo long waves",
|
|
285
|
+
"indigo pixie cut",
|
|
286
|
+
"honey-blonde high ponytail",
|
|
287
|
+
"honey-blonde bowl cut",
|
|
288
|
+
"hot-pink messy spikes",
|
|
289
|
+
"hot-pink bob",
|
|
290
|
+
"ash-white curly volume",
|
|
291
|
+
"ash-white long hair",
|
|
292
|
+
"teal-tinted undercut",
|
|
293
|
+
"teal high ponytail",
|
|
294
|
+
"deep-burgundy twin braids",
|
|
295
|
+
"deep-burgundy shag",
|
|
296
|
+
];
|
|
297
|
+
const LOOK_CLOTH = [
|
|
298
|
+
"sunflower-yellow collared shirt",
|
|
299
|
+
"cobalt hooded jacket",
|
|
300
|
+
"rose cardigan over a cream tee",
|
|
301
|
+
"forest-green knit turtleneck",
|
|
302
|
+
"ivory blouse with a coral scarf",
|
|
303
|
+
"charcoal work vest over a rust tee",
|
|
304
|
+
"lilac haori",
|
|
305
|
+
"orange windbreaker",
|
|
306
|
+
"white lab coat over a black tee",
|
|
307
|
+
"crimson bomber jacket",
|
|
308
|
+
"mint sailor collar",
|
|
309
|
+
"navy peacoat",
|
|
310
|
+
"gold-trimmed teal capelet",
|
|
311
|
+
"checkered red-and-black shirt",
|
|
312
|
+
"pale-blue denim jacket",
|
|
313
|
+
"magenta track jacket",
|
|
314
|
+
];
|
|
315
|
+
const LOOK_EXTRA = [
|
|
316
|
+
"round wire glasses",
|
|
317
|
+
"small gold hoop earrings",
|
|
318
|
+
"over-ear headphones around the neck",
|
|
319
|
+
"a paintbrush tucked behind one ear",
|
|
320
|
+
"a red hair clip",
|
|
321
|
+
"a thin black choker",
|
|
322
|
+
"a knitted ear warmer",
|
|
323
|
+
"no extra accessories",
|
|
324
|
+
];
|
|
325
|
+
const LOOK_SKIN = [
|
|
326
|
+
"fair peach human skin",
|
|
327
|
+
"warm tan human skin",
|
|
328
|
+
"light brown human skin",
|
|
329
|
+
"deep brown human skin",
|
|
330
|
+
"golden beige human skin",
|
|
331
|
+
];
|
|
332
|
+
const LOOK_FACE = [
|
|
333
|
+
"round cheerful face with wide-set eyes",
|
|
334
|
+
"sharp jaw and narrow eyes",
|
|
335
|
+
"soft oval face with thick brows",
|
|
336
|
+
"heart-shaped face and a small nose",
|
|
337
|
+
"square face with a bright closed-mouth smile",
|
|
338
|
+
];
|
|
339
|
+
const LOOK_RACE = [
|
|
340
|
+
{
|
|
341
|
+
id: "human" as const,
|
|
342
|
+
label: "human",
|
|
343
|
+
prompt:
|
|
344
|
+
"human. Ordinary rounded human ears, fully human anatomy, no fantasy ears.",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "dwarf" as const,
|
|
348
|
+
label: "dwarf",
|
|
349
|
+
prompt:
|
|
350
|
+
"young dwarf. Stout neck, broader cheekbones, a slightly larger nose, thick brows, rounded ears, youthful (not elderly, not bald).",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: "elf" as const,
|
|
354
|
+
label: "elf",
|
|
355
|
+
prompt:
|
|
356
|
+
"young elf. Long pointed ears clearly visible, fine features, almond eyes, still youthful.",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "demihuman" as const,
|
|
360
|
+
label: "demihuman",
|
|
361
|
+
prompt:
|
|
362
|
+
"demihuman who is 90% human: a human face and bust with only one subtle tell (tiny pointed ear tips, faint whisker marks, or slightly elongated canines). Not a full animal-person, not a mascot, not extra limbs.",
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
function lookSeed(key: string): number {
|
|
367
|
+
let n = 2166136261;
|
|
368
|
+
for (const ch of key) {
|
|
369
|
+
n ^= ch.charCodeAt(0);
|
|
370
|
+
n = Math.imul(n, 16777619);
|
|
371
|
+
}
|
|
372
|
+
return n >>> 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function lookTraits(bot: { name: string; handle: string }): {
|
|
376
|
+
hair: string;
|
|
377
|
+
cloth: string;
|
|
378
|
+
extra: string;
|
|
379
|
+
skin: string;
|
|
380
|
+
face: string;
|
|
381
|
+
race: (typeof LOOK_RACE)[number];
|
|
382
|
+
} {
|
|
383
|
+
const n = lookSeed(bot.handle || bot.name || "bot");
|
|
384
|
+
return {
|
|
385
|
+
hair: LOOK_HAIR[n % LOOK_HAIR.length],
|
|
386
|
+
cloth: LOOK_CLOTH[(n >>> 4) % LOOK_CLOTH.length],
|
|
387
|
+
extra: LOOK_EXTRA[(n >>> 8) % LOOK_EXTRA.length],
|
|
388
|
+
skin: LOOK_SKIN[(n >>> 12) % LOOK_SKIN.length],
|
|
389
|
+
face: LOOK_FACE[(n >>> 16) % LOOK_FACE.length],
|
|
390
|
+
race: LOOK_RACE[(n >>> 20) % LOOK_RACE.length],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function lookPrompt(bot: {
|
|
395
|
+
name: string;
|
|
396
|
+
handle: string;
|
|
397
|
+
oneLiner?: string;
|
|
398
|
+
}): string {
|
|
399
|
+
const role = bot.oneLiner?.trim() || "keeps a seat in the guild tavern";
|
|
400
|
+
const look = lookTraits(bot);
|
|
401
|
+
return [
|
|
402
|
+
`SNES 16-bit pixel-art bust portrait of ${bot.name} (@${bot.handle}), a unique ${look.race.label} guild adventurer.`,
|
|
403
|
+
`Race (mandatory): ${look.race.prompt}`,
|
|
404
|
+
`Mandatory look: ${look.skin}, ${look.face}, ${look.hair}, wearing a ${look.cloth}, ${look.extra}.`,
|
|
405
|
+
`They ${role}.`,
|
|
406
|
+
"Hair color, outfit, and race are locked; do not default to brown hair or a beige coat.",
|
|
407
|
+
"Natural skin tones only, never green, gray, or monster skin.",
|
|
408
|
+
"Head and shoulders only, facing the camera, chunky 16-bit pixels, limited tavern palette, cream pixel outline.",
|
|
409
|
+
"Animal Crossing crossed with Earthbound, youthful SNES NPC.",
|
|
410
|
+
"Opaque cream or tavern-wood background, no black void, no white photo studio, no checkerboard, no transparency.",
|
|
411
|
+
"Close-up character headshot, no full body, no legs, no floor, no scenery, no photorealism, no 3D render, no text, no watermark.",
|
|
412
|
+
].join(" ");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function generateBotLook(
|
|
416
|
+
store: GuildStore,
|
|
417
|
+
id: string,
|
|
418
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
419
|
+
) {
|
|
420
|
+
const bot = store.getBot(id);
|
|
421
|
+
if (!bot) throw new StoreError(404, "bot not found");
|
|
422
|
+
const { generateImage } = await import("./image-gen.ts");
|
|
423
|
+
const result = await generateImage({
|
|
424
|
+
prompt: lookPrompt(bot),
|
|
425
|
+
aspectRatio: "1:1",
|
|
426
|
+
dataDir: store.dataDir,
|
|
427
|
+
env,
|
|
428
|
+
});
|
|
429
|
+
if (result.isError || !result.publicPath) {
|
|
430
|
+
throw new StoreError(502, result.text);
|
|
431
|
+
}
|
|
432
|
+
return store.updateBot(id, { portrait: result.publicPath });
|
|
433
|
+
}
|
|
434
|
+
|
|
259
435
|
export async function importSkills(
|
|
260
436
|
store: GuildStore,
|
|
261
437
|
input: { source: string; url?: string; repo?: string },
|
|
@@ -356,10 +532,53 @@ export function createChannel(store: GuildStore, name: string) {
|
|
|
356
532
|
return store.createChannel(name);
|
|
357
533
|
}
|
|
358
534
|
|
|
535
|
+
export function createBranch(
|
|
536
|
+
store: GuildStore,
|
|
537
|
+
parentId: string,
|
|
538
|
+
messageId: string,
|
|
539
|
+
name?: string,
|
|
540
|
+
) {
|
|
541
|
+
return store.createBranch(parentId, messageId, name);
|
|
542
|
+
}
|
|
543
|
+
|
|
359
544
|
export function deleteChannel(store: GuildStore, id: string) {
|
|
545
|
+
const room = store.getRoom(id);
|
|
546
|
+
if (room?.parentId) {
|
|
547
|
+
throw new StoreError(400, "close the branch instead");
|
|
548
|
+
}
|
|
360
549
|
return store.deleteChannel(id);
|
|
361
550
|
}
|
|
362
551
|
|
|
552
|
+
export async function closeBranch(
|
|
553
|
+
store: GuildStore,
|
|
554
|
+
id: string,
|
|
555
|
+
merge = false,
|
|
556
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
557
|
+
) {
|
|
558
|
+
const room = store.getRoom(id);
|
|
559
|
+
if (!room) throw new StoreError(404, "channel not found");
|
|
560
|
+
if (room.kind !== "channel") throw new StoreError(400, "not a channel");
|
|
561
|
+
if (!room.parentId) throw new StoreError(400, "not a branch");
|
|
562
|
+
const parent = store.getRoom(room.parentId);
|
|
563
|
+
if (!parent) throw new StoreError(404, "parent not found");
|
|
564
|
+
if (merge) {
|
|
565
|
+
for (const child of store
|
|
566
|
+
.listChannels()
|
|
567
|
+
.filter((item) => item.parentId === id)) {
|
|
568
|
+
await closeBranch(store, child.id, true, env);
|
|
569
|
+
}
|
|
570
|
+
await mergeQuestMemory({
|
|
571
|
+
store,
|
|
572
|
+
parentId: parent.id,
|
|
573
|
+
childId: id,
|
|
574
|
+
questName: room.name,
|
|
575
|
+
env,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
store.deleteChannel(id);
|
|
579
|
+
return { ok: true as const, id, parentId: parent.id, merged: Boolean(merge) };
|
|
580
|
+
}
|
|
581
|
+
|
|
363
582
|
export function renameChannel(store: GuildStore, id: string, name: string) {
|
|
364
583
|
return store.renameChannel(id, name);
|
|
365
584
|
}
|
|
@@ -401,16 +620,23 @@ export function openDm(store: GuildStore, botId: string) {
|
|
|
401
620
|
return store.openDm(botId);
|
|
402
621
|
}
|
|
403
622
|
|
|
623
|
+
/** A seat may speak twice in one turn so report-back can resume the assigner. */
|
|
624
|
+
const HANDOFF_SEAT_CAP = 2;
|
|
625
|
+
|
|
404
626
|
function handoffTargets(
|
|
405
627
|
store: GuildStore,
|
|
406
628
|
memberIds: string[],
|
|
407
629
|
replies: ChatMessage[],
|
|
408
630
|
asked: string,
|
|
409
631
|
history: HistoryItem[],
|
|
632
|
+
fresh: ChatMessage[],
|
|
410
633
|
): { botId: string; fromHandle: string; asked: string; history: HistoryItem[] }[] {
|
|
411
634
|
const bots = store.listBots();
|
|
412
635
|
const handles = bots.map((bot) => bot.handle);
|
|
413
|
-
const spoke = new
|
|
636
|
+
const spoke = new Map<string, number>();
|
|
637
|
+
for (const row of replies) {
|
|
638
|
+
spoke.set(row.author, (spoke.get(row.author) || 0) + 1);
|
|
639
|
+
}
|
|
414
640
|
const hops: {
|
|
415
641
|
botId: string;
|
|
416
642
|
fromHandle: string;
|
|
@@ -418,10 +644,10 @@ function handoffTargets(
|
|
|
418
644
|
history: HistoryItem[];
|
|
419
645
|
}[] = [];
|
|
420
646
|
const queued = new Set<string>();
|
|
421
|
-
for (const reply of
|
|
647
|
+
for (const reply of fresh) {
|
|
422
648
|
if (isBroadcastMention(reply.body)) continue;
|
|
423
|
-
const
|
|
424
|
-
if (!
|
|
649
|
+
const ids = messageMentionIds(reply, bots);
|
|
650
|
+
if (!ids.length) continue;
|
|
425
651
|
const from = bots.find((bot) => bot.id === reply.author);
|
|
426
652
|
const fromHandle = from?.handle || reply.author;
|
|
427
653
|
const hopHistory = history.concat(
|
|
@@ -429,10 +655,10 @@ function handoffTargets(
|
|
|
429
655
|
{ author: reply.author, body: reply.body },
|
|
430
656
|
);
|
|
431
657
|
for (const bot of bots) {
|
|
432
|
-
if (!
|
|
658
|
+
if (!ids.includes(bot.id)) continue;
|
|
433
659
|
if (!memberIds.includes(bot.id)) continue;
|
|
434
660
|
if (bot.id === reply.author) continue;
|
|
435
|
-
if (spoke.
|
|
661
|
+
if ((spoke.get(bot.id) || 0) >= HANDOFF_SEAT_CAP || queued.has(bot.id)) continue;
|
|
436
662
|
queued.add(bot.id);
|
|
437
663
|
const spec = assignmentFor(reply.body, bot.handle, handles);
|
|
438
664
|
hops.push({
|
|
@@ -449,19 +675,16 @@ function handoffTargets(
|
|
|
449
675
|
function replyBots(
|
|
450
676
|
store: GuildStore,
|
|
451
677
|
memberIds: string[],
|
|
452
|
-
|
|
678
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
453
679
|
extraBotId?: string,
|
|
454
680
|
): string[] {
|
|
455
|
-
if (isBroadcastMention(
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
681
|
+
if (isBroadcastMention(userMessage.body)) return memberIds;
|
|
682
|
+
const mentioned = new Set(
|
|
683
|
+
messageMentionIds(
|
|
684
|
+
{ author: userMessage.author || "you", body: userMessage.body, mentions: userMessage.mentions },
|
|
685
|
+
store.listBots(),
|
|
686
|
+
),
|
|
460
687
|
);
|
|
461
|
-
const mentioned = new Set<string>();
|
|
462
|
-
for (const bot of bots) {
|
|
463
|
-
if (names.includes(bot.handle.toLowerCase())) mentioned.add(bot.id);
|
|
464
|
-
}
|
|
465
688
|
if (mentioned.size > 0) {
|
|
466
689
|
return memberIds.filter((id) => mentioned.has(id));
|
|
467
690
|
}
|
|
@@ -480,7 +703,11 @@ function turnUserMessage(
|
|
|
480
703
|
store.listBots().find((bot) => bot.id === parent.author)?.handle ||
|
|
481
704
|
parent.author;
|
|
482
705
|
const preview = parent.body.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
483
|
-
return
|
|
706
|
+
return [
|
|
707
|
+
`(回覆 @${handle}。只做下一句;引言裡的交棒已經發生過,不要再叫別人。)`,
|
|
708
|
+
`> ${preview}`,
|
|
709
|
+
body,
|
|
710
|
+
].join("\n");
|
|
484
711
|
}
|
|
485
712
|
|
|
486
713
|
const ATTACH_TOKEN = /^\[[A-Za-z]+ #\d+\]$/;
|
|
@@ -530,28 +757,14 @@ function askedText(
|
|
|
530
757
|
return `附件:\n${legend}\n\n${asked}`;
|
|
531
758
|
}
|
|
532
759
|
|
|
533
|
-
/**
|
|
760
|
+
/** Roster is staffed in the members panel. @handle never pulls a new seat. */
|
|
534
761
|
export function inviteMentionedBots(
|
|
535
762
|
store: GuildStore,
|
|
536
763
|
roomId: string,
|
|
537
|
-
|
|
764
|
+
_userMessage?: { author?: string; body: string; mentions?: string[] },
|
|
538
765
|
): string[] {
|
|
539
766
|
const room = store.getRoom(roomId);
|
|
540
|
-
|
|
541
|
-
if (room.kind !== "channel") return room.memberIds;
|
|
542
|
-
if (isBroadcastMention(userText)) return room.memberIds;
|
|
543
|
-
const names = summonedHandles(
|
|
544
|
-
userText,
|
|
545
|
-
store.listBots().map((bot) => bot.handle),
|
|
546
|
-
);
|
|
547
|
-
let memberIds = room.memberIds;
|
|
548
|
-
for (const bot of store.listBots()) {
|
|
549
|
-
if (!names.includes(bot.handle.toLowerCase())) continue;
|
|
550
|
-
if (memberIds.includes(bot.id)) continue;
|
|
551
|
-
store.addMember(roomId, bot.id);
|
|
552
|
-
memberIds = [...memberIds, bot.id];
|
|
553
|
-
}
|
|
554
|
-
return memberIds;
|
|
767
|
+
return room?.memberIds ?? [];
|
|
555
768
|
}
|
|
556
769
|
|
|
557
770
|
export function channelMarkdownForRoom(
|
|
@@ -734,7 +947,18 @@ function liveDetail(trace: ToolTrace): string {
|
|
|
734
947
|
if (trace.name === "skill") return String(args.name || "");
|
|
735
948
|
if (trace.name === "image_gen") return String(args.prompt || "");
|
|
736
949
|
if (trace.name === "spawn") {
|
|
737
|
-
return String(
|
|
950
|
+
return String(
|
|
951
|
+
args.title ||
|
|
952
|
+
args.description ||
|
|
953
|
+
args.profile ||
|
|
954
|
+
args.name ||
|
|
955
|
+
args.task ||
|
|
956
|
+
args.prompt ||
|
|
957
|
+
"",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (trace.name === "read_spawn") {
|
|
961
|
+
return String(args.agent_id || args.id || "");
|
|
738
962
|
}
|
|
739
963
|
if (trace.name.startsWith("mcp__")) {
|
|
740
964
|
return JSON.stringify(args).slice(0, 120);
|
|
@@ -918,20 +1142,20 @@ async function generateReplies(
|
|
|
918
1142
|
store: GuildStore,
|
|
919
1143
|
roomId: string,
|
|
920
1144
|
memberIds: string[],
|
|
921
|
-
userMessage: { body: string; attachments?: ChatAttachment[] },
|
|
1145
|
+
userMessage: { author?: string; body: string; attachments?: ChatAttachment[]; mentions?: string[] },
|
|
922
1146
|
history: HistoryItem[],
|
|
923
1147
|
onlyBotId?: string,
|
|
924
1148
|
env: NodeJS.ProcessEnv = process.env,
|
|
925
1149
|
parent?: ChatMessage,
|
|
926
1150
|
extras: HandlerExtras = {},
|
|
927
1151
|
) {
|
|
928
|
-
const extraBotId = hasExplicitSummon(store, userMessage
|
|
1152
|
+
const extraBotId = hasExplicitSummon(store, userMessage)
|
|
929
1153
|
? undefined
|
|
930
1154
|
: followBotId(store, history, parent);
|
|
931
1155
|
const asked = askedText(store, parent, userMessage);
|
|
932
1156
|
const targets = onlyBotId
|
|
933
1157
|
? [onlyBotId]
|
|
934
|
-
: replyBots(store, memberIds, userMessage
|
|
1158
|
+
: replyBots(store, memberIds, userMessage, extraBotId);
|
|
935
1159
|
const replies: ChatMessage[] = [];
|
|
936
1160
|
const harvested: { handle: string; author: string; body: string }[] = [];
|
|
937
1161
|
const signal = store.beginTurn(roomId, targets);
|
|
@@ -942,7 +1166,7 @@ async function generateReplies(
|
|
|
942
1166
|
turnAsked: string,
|
|
943
1167
|
turnHistory: HistoryItem[],
|
|
944
1168
|
) => {
|
|
945
|
-
if (!memberIds.includes(botId)
|
|
1169
|
+
if (!memberIds.includes(botId)) return;
|
|
946
1170
|
if (signal.aborted) return;
|
|
947
1171
|
const prev = store.getLiveBotTurn(roomId, botId);
|
|
948
1172
|
const startedAt = prev?.startedAt || new Date().toISOString();
|
|
@@ -996,6 +1220,11 @@ async function generateReplies(
|
|
|
996
1220
|
throw err;
|
|
997
1221
|
}
|
|
998
1222
|
const usage = { ...(generated.usage || {}), startedAt };
|
|
1223
|
+
const hopMentions = parseMentionIds(
|
|
1224
|
+
generated.body,
|
|
1225
|
+
store.listBots(),
|
|
1226
|
+
"bot",
|
|
1227
|
+
).filter((id) => id !== botId);
|
|
999
1228
|
const reply = store.appendMessage(
|
|
1000
1229
|
roomId,
|
|
1001
1230
|
botId,
|
|
@@ -1004,6 +1233,9 @@ async function generateReplies(
|
|
|
1004
1233
|
undefined,
|
|
1005
1234
|
undefined,
|
|
1006
1235
|
usage,
|
|
1236
|
+
undefined,
|
|
1237
|
+
undefined,
|
|
1238
|
+
hopMentions,
|
|
1007
1239
|
);
|
|
1008
1240
|
store.dropLiveBotTurn(roomId, botId);
|
|
1009
1241
|
recordTurn(store, roomId, botId, generated, reply);
|
|
@@ -1045,8 +1277,12 @@ async function generateReplies(
|
|
|
1045
1277
|
return speak(botId, turnAsked, history);
|
|
1046
1278
|
}),
|
|
1047
1279
|
);
|
|
1048
|
-
|
|
1049
|
-
|
|
1280
|
+
let seen = 0;
|
|
1281
|
+
for (let wave = 0; wave < CHANNEL_ROSTER_CAP && !signal.aborted; wave++) {
|
|
1282
|
+
const fresh = replies.slice(seen);
|
|
1283
|
+
seen = replies.length;
|
|
1284
|
+
const hops = handoffTargets(store, memberIds, replies, asked, history, fresh);
|
|
1285
|
+
if (!hops.length) break;
|
|
1050
1286
|
const hopAt = new Date().toISOString();
|
|
1051
1287
|
for (const hop of hops) {
|
|
1052
1288
|
store.adoptTurn(roomId, hop.botId, signal);
|
|
@@ -1133,7 +1369,7 @@ export function listRoomTrajectory(store: GuildStore, roomId: string) {
|
|
|
1133
1369
|
});
|
|
1134
1370
|
return {
|
|
1135
1371
|
...base,
|
|
1136
|
-
events: base.events.concat(extra),
|
|
1372
|
+
events: base.events.map(promoteSpawnEvent).concat(extra),
|
|
1137
1373
|
live: extra.length > 0 || open,
|
|
1138
1374
|
};
|
|
1139
1375
|
}
|
|
@@ -1168,7 +1404,7 @@ function plantLiveTurns(
|
|
|
1168
1404
|
const startedAt = new Date().toISOString();
|
|
1169
1405
|
for (const botId of botIds) {
|
|
1170
1406
|
if (!botId) continue;
|
|
1171
|
-
if (!memberIds.includes(botId)
|
|
1407
|
+
if (!memberIds.includes(botId)) continue;
|
|
1172
1408
|
store.dropLastFailedReply(roomId, botId);
|
|
1173
1409
|
store.setLiveTurn(roomId, {
|
|
1174
1410
|
botId,
|
|
@@ -1200,29 +1436,45 @@ function followBotId(
|
|
|
1200
1436
|
return lastBotSpeaker(store, messages);
|
|
1201
1437
|
}
|
|
1202
1438
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1439
|
+
/** Reply-to-a-bot locks that seat. Client assigneeId cannot steal it unless the new text @mentions someone. */
|
|
1440
|
+
function exclusiveReplyBot(
|
|
1441
|
+
store: GuildStore,
|
|
1442
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
1443
|
+
parent: ChatMessage | undefined,
|
|
1444
|
+
assignee?: string,
|
|
1445
|
+
): string | undefined {
|
|
1446
|
+
if (hasExplicitSummon(store, userMessage)) return assignee || undefined;
|
|
1447
|
+
if (parent && parent.author !== "you" && store.getBot(parent.author)) {
|
|
1448
|
+
return parent.author;
|
|
1449
|
+
}
|
|
1450
|
+
return assignee || undefined;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function hasExplicitSummon(
|
|
1454
|
+
store: GuildStore,
|
|
1455
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
1456
|
+
): boolean {
|
|
1457
|
+
if (isBroadcastMention(userMessage.body)) return true;
|
|
1205
1458
|
return (
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1459
|
+
messageMentionIds(
|
|
1460
|
+
{
|
|
1461
|
+
author: userMessage.author || "you",
|
|
1462
|
+
body: userMessage.body,
|
|
1463
|
+
mentions: userMessage.mentions,
|
|
1464
|
+
},
|
|
1465
|
+
store.listBots(),
|
|
1209
1466
|
).length > 0
|
|
1210
1467
|
);
|
|
1211
1468
|
}
|
|
1212
1469
|
|
|
1213
1470
|
function includeFollowBot(
|
|
1214
|
-
|
|
1215
|
-
|
|
1471
|
+
_store: GuildStore,
|
|
1472
|
+
_roomId: string,
|
|
1216
1473
|
memberIds: string[],
|
|
1217
1474
|
follow?: string,
|
|
1218
1475
|
): string[] {
|
|
1219
1476
|
if (!follow || memberIds.includes(follow)) return memberIds;
|
|
1220
|
-
|
|
1221
|
-
store.addMember(roomId, follow);
|
|
1222
|
-
return [...memberIds, follow];
|
|
1223
|
-
} catch {
|
|
1224
|
-
return memberIds;
|
|
1225
|
-
}
|
|
1477
|
+
return memberIds;
|
|
1226
1478
|
}
|
|
1227
1479
|
|
|
1228
1480
|
function inviteAssignee(
|
|
@@ -1233,12 +1485,10 @@ function inviteAssignee(
|
|
|
1233
1485
|
const room = store.getRoom(roomId);
|
|
1234
1486
|
if (!room) return [];
|
|
1235
1487
|
if (room.kind !== "channel") return room.memberIds;
|
|
1236
|
-
if (room.memberIds.includes(assigneeId)) return room.memberIds;
|
|
1237
1488
|
if (!store.getBot(assigneeId)) {
|
|
1238
1489
|
throw new StoreError(400, "assignee does not exist");
|
|
1239
1490
|
}
|
|
1240
|
-
|
|
1241
|
-
return [...room.memberIds, assigneeId];
|
|
1491
|
+
return room.memberIds;
|
|
1242
1492
|
}
|
|
1243
1493
|
|
|
1244
1494
|
export async function postUserMessage(
|
|
@@ -1258,6 +1508,11 @@ export async function postUserMessage(
|
|
|
1258
1508
|
const packed = parseAttachments(attachments);
|
|
1259
1509
|
const tokens = packed?.map((att) => att.token).join(" ") || "";
|
|
1260
1510
|
const text = body.trim() || tokens;
|
|
1511
|
+
const bots = store.listBots();
|
|
1512
|
+
const mentions =
|
|
1513
|
+
extras.mentions !== undefined
|
|
1514
|
+
? sanitizeMentionIds(extras.mentions, bots)
|
|
1515
|
+
: parseMentionIds(text, bots, "user");
|
|
1261
1516
|
const message = store.appendMessage(
|
|
1262
1517
|
roomId,
|
|
1263
1518
|
"you",
|
|
@@ -1265,6 +1520,10 @@ export async function postUserMessage(
|
|
|
1265
1520
|
undefined,
|
|
1266
1521
|
parent ? parent.id : undefined,
|
|
1267
1522
|
packed,
|
|
1523
|
+
undefined,
|
|
1524
|
+
undefined,
|
|
1525
|
+
undefined,
|
|
1526
|
+
mentions,
|
|
1268
1527
|
);
|
|
1269
1528
|
try {
|
|
1270
1529
|
store.appendTrajectory(roomId, [
|
|
@@ -1275,10 +1534,11 @@ export async function postUserMessage(
|
|
|
1275
1534
|
}
|
|
1276
1535
|
const history = previous.map(toHistoryItem);
|
|
1277
1536
|
const assignee = assigneeId?.trim();
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1537
|
+
const only = exclusiveReplyBot(store, message, parent, assignee);
|
|
1538
|
+
let memberIds = only
|
|
1539
|
+
? inviteAssignee(store, roomId, only)
|
|
1540
|
+
: inviteMentionedBots(store, roomId, message);
|
|
1541
|
+
if (!only && !hasExplicitSummon(store, message)) {
|
|
1282
1542
|
memberIds = includeFollowBot(
|
|
1283
1543
|
store,
|
|
1284
1544
|
roomId,
|
|
@@ -1292,7 +1552,7 @@ export async function postUserMessage(
|
|
|
1292
1552
|
memberIds,
|
|
1293
1553
|
message,
|
|
1294
1554
|
history,
|
|
1295
|
-
|
|
1555
|
+
only || undefined,
|
|
1296
1556
|
env,
|
|
1297
1557
|
parent,
|
|
1298
1558
|
extras,
|
|
@@ -1317,19 +1577,25 @@ export async function retryMessage(
|
|
|
1317
1577
|
const current = messages[index];
|
|
1318
1578
|
|
|
1319
1579
|
if (current.author === "you") {
|
|
1580
|
+
const bots = store.listBots();
|
|
1581
|
+
const mentions =
|
|
1582
|
+
extras.mentions !== undefined
|
|
1583
|
+
? sanitizeMentionIds(extras.mentions, bots)
|
|
1584
|
+
: undefined;
|
|
1320
1585
|
const message =
|
|
1321
1586
|
typeof body === "string" && body.trim()
|
|
1322
|
-
? store.updateMessage(roomId, messageId, body)
|
|
1587
|
+
? store.updateMessage(roomId, messageId, body, mentions)
|
|
1323
1588
|
: current;
|
|
1324
1589
|
store.truncateAfter(roomId, messageId);
|
|
1325
1590
|
const kept = store.listMessages(roomId);
|
|
1326
1591
|
const history = kept.slice(0, -1).map(toHistoryItem);
|
|
1327
1592
|
const parent = parentMessage(kept.slice(0, -1), message.replyTo);
|
|
1328
1593
|
const assignee = assigneeId?.trim();
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1594
|
+
const only = exclusiveReplyBot(store, message, parent, assignee);
|
|
1595
|
+
let memberIds = only
|
|
1596
|
+
? inviteAssignee(store, roomId, only)
|
|
1597
|
+
: inviteMentionedBots(store, roomId, message);
|
|
1598
|
+
if (!only && !hasExplicitSummon(store, message)) {
|
|
1333
1599
|
memberIds = includeFollowBot(
|
|
1334
1600
|
store,
|
|
1335
1601
|
roomId,
|
|
@@ -1343,7 +1609,7 @@ export async function retryMessage(
|
|
|
1343
1609
|
memberIds,
|
|
1344
1610
|
message,
|
|
1345
1611
|
history,
|
|
1346
|
-
|
|
1612
|
+
only || undefined,
|
|
1347
1613
|
env,
|
|
1348
1614
|
parent,
|
|
1349
1615
|
extras,
|
|
@@ -1443,4 +1709,9 @@ export async function retryMessage(
|
|
|
1443
1709
|
|
|
1444
1710
|
export { StoreError, localGenerate };
|
|
1445
1711
|
|
|
1446
|
-
export {
|
|
1712
|
+
export {
|
|
1713
|
+
publicModels,
|
|
1714
|
+
mergeModelsFile,
|
|
1715
|
+
refreshOpenCodeFreeCatalog,
|
|
1716
|
+
refreshReasoningCatalog,
|
|
1717
|
+
} from "./llm.ts";
|