@kevin5251984/guild 0.2.18 → 0.2.19
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 +1 -1
- package/src/catalog/subagents.ts +2 -1
- package/src/chat-parts.ts +6 -1
- package/src/compact.ts +27 -0
- package/src/db.ts +199 -64
- package/src/generate.ts +58 -27
- package/src/handlers.ts +341 -54
- package/src/harness.ts +21 -8
- package/src/image-gen.ts +2 -1
- package/src/llm.ts +400 -58
- package/src/memory.ts +69 -0
- package/src/mention.ts +119 -12
- package/src/oauth.ts +202 -25
- package/src/opencode-free.ts +247 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +174 -72
- package/src/public/chat.html +212 -43
- package/src/public/i18n.js +41 -6
- package/src/public/index.html +1 -46
- package/src/public/mobile.css +491 -0
- package/src/public/mobile.html +784 -0
- package/src/public/settings.html +188 -27
- 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/router.ts +76 -5
- package/src/store.ts +161 -5
- package/src/subagent.ts +233 -6
- package/src/tools.ts +124 -18
- package/src/trajectory.ts +72 -9
- package/src/usage.ts +10 -0
- package/vendor/protocol/src/index.ts +14 -1
package/src/handlers.ts
CHANGED
|
@@ -18,14 +18,25 @@ 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,
|
|
@@ -38,7 +49,9 @@ import {
|
|
|
38
49
|
import {
|
|
39
50
|
assignmentFor,
|
|
40
51
|
isBroadcastMention,
|
|
41
|
-
|
|
52
|
+
messageMentionIds,
|
|
53
|
+
parseMentionIds,
|
|
54
|
+
sanitizeMentionIds,
|
|
42
55
|
} from "./mention.ts";
|
|
43
56
|
import { slashNames } from "./slash.ts";
|
|
44
57
|
import { toHistoryItem, type HistoryItem } from "./compact.ts";
|
|
@@ -59,6 +72,8 @@ export type HandlerExtras = {
|
|
|
59
72
|
mcpTools?: McpToolRef[] | Promise<McpToolRef[]>;
|
|
60
73
|
onTurnComplete?: (turn: TurnComplete) => void;
|
|
61
74
|
turn?: (input: Parameters<typeof chatReply>[0]) => Promise<ChatReply>;
|
|
75
|
+
/** Bot ids the client already resolved from @mentions. */
|
|
76
|
+
mentions?: string[];
|
|
62
77
|
};
|
|
63
78
|
|
|
64
79
|
export function healthPayload(): HealthResponse {
|
|
@@ -242,6 +257,7 @@ export function updateBot(
|
|
|
242
257
|
name?: string;
|
|
243
258
|
handle?: string;
|
|
244
259
|
oneLiner?: string;
|
|
260
|
+
portrait?: string | null;
|
|
245
261
|
skillIds?: string[];
|
|
246
262
|
soul?: MarkdownDraft;
|
|
247
263
|
agent?: MarkdownDraft;
|
|
@@ -256,6 +272,162 @@ export function updateBot(
|
|
|
256
272
|
return store.updateBot(id, { ...input, skillIds });
|
|
257
273
|
}
|
|
258
274
|
|
|
275
|
+
const LOOK_HAIR = [
|
|
276
|
+
"jet-black short crop",
|
|
277
|
+
"jet-black long straight hair",
|
|
278
|
+
"copper-red bob with blunt bangs",
|
|
279
|
+
"copper-red messy spikes",
|
|
280
|
+
"indigo long waves",
|
|
281
|
+
"indigo pixie cut",
|
|
282
|
+
"honey-blonde high ponytail",
|
|
283
|
+
"honey-blonde bowl cut",
|
|
284
|
+
"hot-pink messy spikes",
|
|
285
|
+
"hot-pink bob",
|
|
286
|
+
"ash-white curly volume",
|
|
287
|
+
"ash-white long hair",
|
|
288
|
+
"teal-tinted undercut",
|
|
289
|
+
"teal high ponytail",
|
|
290
|
+
"deep-burgundy twin braids",
|
|
291
|
+
"deep-burgundy shag",
|
|
292
|
+
];
|
|
293
|
+
const LOOK_CLOTH = [
|
|
294
|
+
"sunflower-yellow collared shirt",
|
|
295
|
+
"cobalt hooded jacket",
|
|
296
|
+
"rose cardigan over a cream tee",
|
|
297
|
+
"forest-green knit turtleneck",
|
|
298
|
+
"ivory blouse with a coral scarf",
|
|
299
|
+
"charcoal work vest over a rust tee",
|
|
300
|
+
"lilac haori",
|
|
301
|
+
"orange windbreaker",
|
|
302
|
+
"white lab coat over a black tee",
|
|
303
|
+
"crimson bomber jacket",
|
|
304
|
+
"mint sailor collar",
|
|
305
|
+
"navy peacoat",
|
|
306
|
+
"gold-trimmed teal capelet",
|
|
307
|
+
"checkered red-and-black shirt",
|
|
308
|
+
"pale-blue denim jacket",
|
|
309
|
+
"magenta track jacket",
|
|
310
|
+
];
|
|
311
|
+
const LOOK_EXTRA = [
|
|
312
|
+
"round wire glasses",
|
|
313
|
+
"small gold hoop earrings",
|
|
314
|
+
"over-ear headphones around the neck",
|
|
315
|
+
"a paintbrush tucked behind one ear",
|
|
316
|
+
"a red hair clip",
|
|
317
|
+
"a thin black choker",
|
|
318
|
+
"a knitted ear warmer",
|
|
319
|
+
"no extra accessories",
|
|
320
|
+
];
|
|
321
|
+
const LOOK_SKIN = [
|
|
322
|
+
"fair peach human skin",
|
|
323
|
+
"warm tan human skin",
|
|
324
|
+
"light brown human skin",
|
|
325
|
+
"deep brown human skin",
|
|
326
|
+
"golden beige human skin",
|
|
327
|
+
];
|
|
328
|
+
const LOOK_FACE = [
|
|
329
|
+
"round cheerful face with wide-set eyes",
|
|
330
|
+
"sharp jaw and narrow eyes",
|
|
331
|
+
"soft oval face with thick brows",
|
|
332
|
+
"heart-shaped face and a small nose",
|
|
333
|
+
"square face with a bright closed-mouth smile",
|
|
334
|
+
];
|
|
335
|
+
const LOOK_RACE = [
|
|
336
|
+
{
|
|
337
|
+
id: "human" as const,
|
|
338
|
+
label: "human",
|
|
339
|
+
prompt:
|
|
340
|
+
"human. Ordinary rounded human ears, fully human anatomy, no fantasy ears.",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "dwarf" as const,
|
|
344
|
+
label: "dwarf",
|
|
345
|
+
prompt:
|
|
346
|
+
"young dwarf. Stout neck, broader cheekbones, a slightly larger nose, thick brows, rounded ears, youthful (not elderly, not bald).",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: "elf" as const,
|
|
350
|
+
label: "elf",
|
|
351
|
+
prompt:
|
|
352
|
+
"young elf. Long pointed ears clearly visible, fine features, almond eyes, still youthful.",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "demihuman" as const,
|
|
356
|
+
label: "demihuman",
|
|
357
|
+
prompt:
|
|
358
|
+
"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.",
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
function lookSeed(key: string): number {
|
|
363
|
+
let n = 2166136261;
|
|
364
|
+
for (const ch of key) {
|
|
365
|
+
n ^= ch.charCodeAt(0);
|
|
366
|
+
n = Math.imul(n, 16777619);
|
|
367
|
+
}
|
|
368
|
+
return n >>> 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function lookTraits(bot: { name: string; handle: string }): {
|
|
372
|
+
hair: string;
|
|
373
|
+
cloth: string;
|
|
374
|
+
extra: string;
|
|
375
|
+
skin: string;
|
|
376
|
+
face: string;
|
|
377
|
+
race: (typeof LOOK_RACE)[number];
|
|
378
|
+
} {
|
|
379
|
+
const n = lookSeed(bot.handle || bot.name || "bot");
|
|
380
|
+
return {
|
|
381
|
+
hair: LOOK_HAIR[n % LOOK_HAIR.length],
|
|
382
|
+
cloth: LOOK_CLOTH[(n >>> 4) % LOOK_CLOTH.length],
|
|
383
|
+
extra: LOOK_EXTRA[(n >>> 8) % LOOK_EXTRA.length],
|
|
384
|
+
skin: LOOK_SKIN[(n >>> 12) % LOOK_SKIN.length],
|
|
385
|
+
face: LOOK_FACE[(n >>> 16) % LOOK_FACE.length],
|
|
386
|
+
race: LOOK_RACE[(n >>> 20) % LOOK_RACE.length],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function lookPrompt(bot: {
|
|
391
|
+
name: string;
|
|
392
|
+
handle: string;
|
|
393
|
+
oneLiner?: string;
|
|
394
|
+
}): string {
|
|
395
|
+
const role = bot.oneLiner?.trim() || "keeps a seat in the guild tavern";
|
|
396
|
+
const look = lookTraits(bot);
|
|
397
|
+
return [
|
|
398
|
+
`SNES 16-bit pixel-art bust portrait of ${bot.name} (@${bot.handle}), a unique ${look.race.label} guild adventurer.`,
|
|
399
|
+
`Race (mandatory): ${look.race.prompt}`,
|
|
400
|
+
`Mandatory look: ${look.skin}, ${look.face}, ${look.hair}, wearing a ${look.cloth}, ${look.extra}.`,
|
|
401
|
+
`They ${role}.`,
|
|
402
|
+
"Hair color, outfit, and race are locked; do not default to brown hair or a beige coat.",
|
|
403
|
+
"Natural skin tones only, never green, gray, or monster skin.",
|
|
404
|
+
"Head and shoulders only, facing the camera, chunky 16-bit pixels, limited tavern palette, cream pixel outline.",
|
|
405
|
+
"Animal Crossing crossed with Earthbound, youthful SNES NPC.",
|
|
406
|
+
"Opaque cream or tavern-wood background, no black void, no white photo studio, no checkerboard, no transparency.",
|
|
407
|
+
"Close-up character headshot, no full body, no legs, no floor, no scenery, no photorealism, no 3D render, no text, no watermark.",
|
|
408
|
+
].join(" ");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function generateBotLook(
|
|
412
|
+
store: GuildStore,
|
|
413
|
+
id: string,
|
|
414
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
415
|
+
) {
|
|
416
|
+
const bot = store.getBot(id);
|
|
417
|
+
if (!bot) throw new StoreError(404, "bot not found");
|
|
418
|
+
const { generateImage } = await import("./image-gen.ts");
|
|
419
|
+
const result = await generateImage({
|
|
420
|
+
prompt: lookPrompt(bot),
|
|
421
|
+
aspectRatio: "1:1",
|
|
422
|
+
dataDir: store.dataDir,
|
|
423
|
+
env,
|
|
424
|
+
});
|
|
425
|
+
if (result.isError || !result.publicPath) {
|
|
426
|
+
throw new StoreError(502, result.text);
|
|
427
|
+
}
|
|
428
|
+
return store.updateBot(id, { portrait: result.publicPath });
|
|
429
|
+
}
|
|
430
|
+
|
|
259
431
|
export async function importSkills(
|
|
260
432
|
store: GuildStore,
|
|
261
433
|
input: { source: string; url?: string; repo?: string },
|
|
@@ -356,10 +528,53 @@ export function createChannel(store: GuildStore, name: string) {
|
|
|
356
528
|
return store.createChannel(name);
|
|
357
529
|
}
|
|
358
530
|
|
|
531
|
+
export function createBranch(
|
|
532
|
+
store: GuildStore,
|
|
533
|
+
parentId: string,
|
|
534
|
+
messageId: string,
|
|
535
|
+
name?: string,
|
|
536
|
+
) {
|
|
537
|
+
return store.createBranch(parentId, messageId, name);
|
|
538
|
+
}
|
|
539
|
+
|
|
359
540
|
export function deleteChannel(store: GuildStore, id: string) {
|
|
541
|
+
const room = store.getRoom(id);
|
|
542
|
+
if (room?.parentId) {
|
|
543
|
+
throw new StoreError(400, "close the branch instead");
|
|
544
|
+
}
|
|
360
545
|
return store.deleteChannel(id);
|
|
361
546
|
}
|
|
362
547
|
|
|
548
|
+
export async function closeBranch(
|
|
549
|
+
store: GuildStore,
|
|
550
|
+
id: string,
|
|
551
|
+
merge = false,
|
|
552
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
553
|
+
) {
|
|
554
|
+
const room = store.getRoom(id);
|
|
555
|
+
if (!room) throw new StoreError(404, "channel not found");
|
|
556
|
+
if (room.kind !== "channel") throw new StoreError(400, "not a channel");
|
|
557
|
+
if (!room.parentId) throw new StoreError(400, "not a branch");
|
|
558
|
+
const parent = store.getRoom(room.parentId);
|
|
559
|
+
if (!parent) throw new StoreError(404, "parent not found");
|
|
560
|
+
if (merge) {
|
|
561
|
+
for (const child of store
|
|
562
|
+
.listChannels()
|
|
563
|
+
.filter((item) => item.parentId === id)) {
|
|
564
|
+
await closeBranch(store, child.id, true, env);
|
|
565
|
+
}
|
|
566
|
+
await mergeQuestMemory({
|
|
567
|
+
store,
|
|
568
|
+
parentId: parent.id,
|
|
569
|
+
childId: id,
|
|
570
|
+
questName: room.name,
|
|
571
|
+
env,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
store.deleteChannel(id);
|
|
575
|
+
return { ok: true as const, id, parentId: parent.id, merged: Boolean(merge) };
|
|
576
|
+
}
|
|
577
|
+
|
|
363
578
|
export function renameChannel(store: GuildStore, id: string, name: string) {
|
|
364
579
|
return store.renameChannel(id, name);
|
|
365
580
|
}
|
|
@@ -401,16 +616,23 @@ export function openDm(store: GuildStore, botId: string) {
|
|
|
401
616
|
return store.openDm(botId);
|
|
402
617
|
}
|
|
403
618
|
|
|
619
|
+
/** A seat may speak twice in one turn so report-back can resume the assigner. */
|
|
620
|
+
const HANDOFF_SEAT_CAP = 2;
|
|
621
|
+
|
|
404
622
|
function handoffTargets(
|
|
405
623
|
store: GuildStore,
|
|
406
624
|
memberIds: string[],
|
|
407
625
|
replies: ChatMessage[],
|
|
408
626
|
asked: string,
|
|
409
627
|
history: HistoryItem[],
|
|
628
|
+
fresh: ChatMessage[],
|
|
410
629
|
): { botId: string; fromHandle: string; asked: string; history: HistoryItem[] }[] {
|
|
411
630
|
const bots = store.listBots();
|
|
412
631
|
const handles = bots.map((bot) => bot.handle);
|
|
413
|
-
const spoke = new
|
|
632
|
+
const spoke = new Map<string, number>();
|
|
633
|
+
for (const row of replies) {
|
|
634
|
+
spoke.set(row.author, (spoke.get(row.author) || 0) + 1);
|
|
635
|
+
}
|
|
414
636
|
const hops: {
|
|
415
637
|
botId: string;
|
|
416
638
|
fromHandle: string;
|
|
@@ -418,10 +640,10 @@ function handoffTargets(
|
|
|
418
640
|
history: HistoryItem[];
|
|
419
641
|
}[] = [];
|
|
420
642
|
const queued = new Set<string>();
|
|
421
|
-
for (const reply of
|
|
643
|
+
for (const reply of fresh) {
|
|
422
644
|
if (isBroadcastMention(reply.body)) continue;
|
|
423
|
-
const
|
|
424
|
-
if (!
|
|
645
|
+
const ids = messageMentionIds(reply, bots);
|
|
646
|
+
if (!ids.length) continue;
|
|
425
647
|
const from = bots.find((bot) => bot.id === reply.author);
|
|
426
648
|
const fromHandle = from?.handle || reply.author;
|
|
427
649
|
const hopHistory = history.concat(
|
|
@@ -429,10 +651,10 @@ function handoffTargets(
|
|
|
429
651
|
{ author: reply.author, body: reply.body },
|
|
430
652
|
);
|
|
431
653
|
for (const bot of bots) {
|
|
432
|
-
if (!
|
|
654
|
+
if (!ids.includes(bot.id)) continue;
|
|
433
655
|
if (!memberIds.includes(bot.id)) continue;
|
|
434
656
|
if (bot.id === reply.author) continue;
|
|
435
|
-
if (spoke.
|
|
657
|
+
if ((spoke.get(bot.id) || 0) >= HANDOFF_SEAT_CAP || queued.has(bot.id)) continue;
|
|
436
658
|
queued.add(bot.id);
|
|
437
659
|
const spec = assignmentFor(reply.body, bot.handle, handles);
|
|
438
660
|
hops.push({
|
|
@@ -449,19 +671,16 @@ function handoffTargets(
|
|
|
449
671
|
function replyBots(
|
|
450
672
|
store: GuildStore,
|
|
451
673
|
memberIds: string[],
|
|
452
|
-
|
|
674
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
453
675
|
extraBotId?: string,
|
|
454
676
|
): string[] {
|
|
455
|
-
if (isBroadcastMention(
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
677
|
+
if (isBroadcastMention(userMessage.body)) return memberIds;
|
|
678
|
+
const mentioned = new Set(
|
|
679
|
+
messageMentionIds(
|
|
680
|
+
{ author: userMessage.author || "you", body: userMessage.body, mentions: userMessage.mentions },
|
|
681
|
+
store.listBots(),
|
|
682
|
+
),
|
|
460
683
|
);
|
|
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
684
|
if (mentioned.size > 0) {
|
|
466
685
|
return memberIds.filter((id) => mentioned.has(id));
|
|
467
686
|
}
|
|
@@ -480,7 +699,11 @@ function turnUserMessage(
|
|
|
480
699
|
store.listBots().find((bot) => bot.id === parent.author)?.handle ||
|
|
481
700
|
parent.author;
|
|
482
701
|
const preview = parent.body.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
483
|
-
return
|
|
702
|
+
return [
|
|
703
|
+
`(回覆 @${handle}。只做下一句;引言裡的交棒已經發生過,不要再叫別人。)`,
|
|
704
|
+
`> ${preview}`,
|
|
705
|
+
body,
|
|
706
|
+
].join("\n");
|
|
484
707
|
}
|
|
485
708
|
|
|
486
709
|
const ATTACH_TOKEN = /^\[[A-Za-z]+ #\d+\]$/;
|
|
@@ -534,22 +757,26 @@ function askedText(
|
|
|
534
757
|
export function inviteMentionedBots(
|
|
535
758
|
store: GuildStore,
|
|
536
759
|
roomId: string,
|
|
537
|
-
|
|
760
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
538
761
|
): string[] {
|
|
539
762
|
const room = store.getRoom(roomId);
|
|
540
763
|
if (!room) return [];
|
|
541
764
|
if (room.kind !== "channel") return room.memberIds;
|
|
542
|
-
if (isBroadcastMention(
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
765
|
+
if (isBroadcastMention(userMessage.body)) return room.memberIds;
|
|
766
|
+
const ids = messageMentionIds(
|
|
767
|
+
{
|
|
768
|
+
author: userMessage.author || "you",
|
|
769
|
+
body: userMessage.body,
|
|
770
|
+
mentions: userMessage.mentions,
|
|
771
|
+
},
|
|
772
|
+
store.listBots(),
|
|
546
773
|
);
|
|
547
774
|
let memberIds = room.memberIds;
|
|
548
|
-
for (const
|
|
549
|
-
if (
|
|
550
|
-
if (
|
|
551
|
-
store.addMember(roomId,
|
|
552
|
-
memberIds = [...memberIds,
|
|
775
|
+
for (const id of ids) {
|
|
776
|
+
if (memberIds.includes(id)) continue;
|
|
777
|
+
if (!store.getBot(id)) continue;
|
|
778
|
+
store.addMember(roomId, id);
|
|
779
|
+
memberIds = [...memberIds, id];
|
|
553
780
|
}
|
|
554
781
|
return memberIds;
|
|
555
782
|
}
|
|
@@ -734,7 +961,18 @@ function liveDetail(trace: ToolTrace): string {
|
|
|
734
961
|
if (trace.name === "skill") return String(args.name || "");
|
|
735
962
|
if (trace.name === "image_gen") return String(args.prompt || "");
|
|
736
963
|
if (trace.name === "spawn") {
|
|
737
|
-
return String(
|
|
964
|
+
return String(
|
|
965
|
+
args.title ||
|
|
966
|
+
args.description ||
|
|
967
|
+
args.profile ||
|
|
968
|
+
args.name ||
|
|
969
|
+
args.task ||
|
|
970
|
+
args.prompt ||
|
|
971
|
+
"",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
if (trace.name === "read_spawn") {
|
|
975
|
+
return String(args.agent_id || args.id || "");
|
|
738
976
|
}
|
|
739
977
|
if (trace.name.startsWith("mcp__")) {
|
|
740
978
|
return JSON.stringify(args).slice(0, 120);
|
|
@@ -918,20 +1156,20 @@ async function generateReplies(
|
|
|
918
1156
|
store: GuildStore,
|
|
919
1157
|
roomId: string,
|
|
920
1158
|
memberIds: string[],
|
|
921
|
-
userMessage: { body: string; attachments?: ChatAttachment[] },
|
|
1159
|
+
userMessage: { author?: string; body: string; attachments?: ChatAttachment[]; mentions?: string[] },
|
|
922
1160
|
history: HistoryItem[],
|
|
923
1161
|
onlyBotId?: string,
|
|
924
1162
|
env: NodeJS.ProcessEnv = process.env,
|
|
925
1163
|
parent?: ChatMessage,
|
|
926
1164
|
extras: HandlerExtras = {},
|
|
927
1165
|
) {
|
|
928
|
-
const extraBotId = hasExplicitSummon(store, userMessage
|
|
1166
|
+
const extraBotId = hasExplicitSummon(store, userMessage)
|
|
929
1167
|
? undefined
|
|
930
1168
|
: followBotId(store, history, parent);
|
|
931
1169
|
const asked = askedText(store, parent, userMessage);
|
|
932
1170
|
const targets = onlyBotId
|
|
933
1171
|
? [onlyBotId]
|
|
934
|
-
: replyBots(store, memberIds, userMessage
|
|
1172
|
+
: replyBots(store, memberIds, userMessage, extraBotId);
|
|
935
1173
|
const replies: ChatMessage[] = [];
|
|
936
1174
|
const harvested: { handle: string; author: string; body: string }[] = [];
|
|
937
1175
|
const signal = store.beginTurn(roomId, targets);
|
|
@@ -996,6 +1234,11 @@ async function generateReplies(
|
|
|
996
1234
|
throw err;
|
|
997
1235
|
}
|
|
998
1236
|
const usage = { ...(generated.usage || {}), startedAt };
|
|
1237
|
+
const hopMentions = parseMentionIds(
|
|
1238
|
+
generated.body,
|
|
1239
|
+
store.listBots(),
|
|
1240
|
+
"bot",
|
|
1241
|
+
).filter((id) => id !== botId);
|
|
999
1242
|
const reply = store.appendMessage(
|
|
1000
1243
|
roomId,
|
|
1001
1244
|
botId,
|
|
@@ -1004,6 +1247,9 @@ async function generateReplies(
|
|
|
1004
1247
|
undefined,
|
|
1005
1248
|
undefined,
|
|
1006
1249
|
usage,
|
|
1250
|
+
undefined,
|
|
1251
|
+
undefined,
|
|
1252
|
+
hopMentions,
|
|
1007
1253
|
);
|
|
1008
1254
|
store.dropLiveBotTurn(roomId, botId);
|
|
1009
1255
|
recordTurn(store, roomId, botId, generated, reply);
|
|
@@ -1045,8 +1291,12 @@ async function generateReplies(
|
|
|
1045
1291
|
return speak(botId, turnAsked, history);
|
|
1046
1292
|
}),
|
|
1047
1293
|
);
|
|
1048
|
-
|
|
1049
|
-
|
|
1294
|
+
let seen = 0;
|
|
1295
|
+
for (let wave = 0; wave < CHANNEL_ROSTER_CAP && !signal.aborted; wave++) {
|
|
1296
|
+
const fresh = replies.slice(seen);
|
|
1297
|
+
seen = replies.length;
|
|
1298
|
+
const hops = handoffTargets(store, memberIds, replies, asked, history, fresh);
|
|
1299
|
+
if (!hops.length) break;
|
|
1050
1300
|
const hopAt = new Date().toISOString();
|
|
1051
1301
|
for (const hop of hops) {
|
|
1052
1302
|
store.adoptTurn(roomId, hop.botId, signal);
|
|
@@ -1133,7 +1383,7 @@ export function listRoomTrajectory(store: GuildStore, roomId: string) {
|
|
|
1133
1383
|
});
|
|
1134
1384
|
return {
|
|
1135
1385
|
...base,
|
|
1136
|
-
events: base.events.concat(extra),
|
|
1386
|
+
events: base.events.map(promoteSpawnEvent).concat(extra),
|
|
1137
1387
|
live: extra.length > 0 || open,
|
|
1138
1388
|
};
|
|
1139
1389
|
}
|
|
@@ -1200,12 +1450,33 @@ function followBotId(
|
|
|
1200
1450
|
return lastBotSpeaker(store, messages);
|
|
1201
1451
|
}
|
|
1202
1452
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1453
|
+
/** Reply-to-a-bot locks that seat. Client assigneeId cannot steal it unless the new text @mentions someone. */
|
|
1454
|
+
function exclusiveReplyBot(
|
|
1455
|
+
store: GuildStore,
|
|
1456
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
1457
|
+
parent: ChatMessage | undefined,
|
|
1458
|
+
assignee?: string,
|
|
1459
|
+
): string | undefined {
|
|
1460
|
+
if (hasExplicitSummon(store, userMessage)) return assignee || undefined;
|
|
1461
|
+
if (parent && parent.author !== "you" && store.getBot(parent.author)) {
|
|
1462
|
+
return parent.author;
|
|
1463
|
+
}
|
|
1464
|
+
return assignee || undefined;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function hasExplicitSummon(
|
|
1468
|
+
store: GuildStore,
|
|
1469
|
+
userMessage: { author?: string; body: string; mentions?: string[] },
|
|
1470
|
+
): boolean {
|
|
1471
|
+
if (isBroadcastMention(userMessage.body)) return true;
|
|
1205
1472
|
return (
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1473
|
+
messageMentionIds(
|
|
1474
|
+
{
|
|
1475
|
+
author: userMessage.author || "you",
|
|
1476
|
+
body: userMessage.body,
|
|
1477
|
+
mentions: userMessage.mentions,
|
|
1478
|
+
},
|
|
1479
|
+
store.listBots(),
|
|
1209
1480
|
).length > 0
|
|
1210
1481
|
);
|
|
1211
1482
|
}
|
|
@@ -1258,6 +1529,11 @@ export async function postUserMessage(
|
|
|
1258
1529
|
const packed = parseAttachments(attachments);
|
|
1259
1530
|
const tokens = packed?.map((att) => att.token).join(" ") || "";
|
|
1260
1531
|
const text = body.trim() || tokens;
|
|
1532
|
+
const bots = store.listBots();
|
|
1533
|
+
const mentions =
|
|
1534
|
+
extras.mentions !== undefined
|
|
1535
|
+
? sanitizeMentionIds(extras.mentions, bots)
|
|
1536
|
+
: parseMentionIds(text, bots, "user");
|
|
1261
1537
|
const message = store.appendMessage(
|
|
1262
1538
|
roomId,
|
|
1263
1539
|
"you",
|
|
@@ -1265,6 +1541,10 @@ export async function postUserMessage(
|
|
|
1265
1541
|
undefined,
|
|
1266
1542
|
parent ? parent.id : undefined,
|
|
1267
1543
|
packed,
|
|
1544
|
+
undefined,
|
|
1545
|
+
undefined,
|
|
1546
|
+
undefined,
|
|
1547
|
+
mentions,
|
|
1268
1548
|
);
|
|
1269
1549
|
try {
|
|
1270
1550
|
store.appendTrajectory(roomId, [
|
|
@@ -1275,10 +1555,11 @@ export async function postUserMessage(
|
|
|
1275
1555
|
}
|
|
1276
1556
|
const history = previous.map(toHistoryItem);
|
|
1277
1557
|
const assignee = assigneeId?.trim();
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1558
|
+
const only = exclusiveReplyBot(store, message, parent, assignee);
|
|
1559
|
+
let memberIds = only
|
|
1560
|
+
? inviteAssignee(store, roomId, only)
|
|
1561
|
+
: inviteMentionedBots(store, roomId, message);
|
|
1562
|
+
if (!only && !hasExplicitSummon(store, message)) {
|
|
1282
1563
|
memberIds = includeFollowBot(
|
|
1283
1564
|
store,
|
|
1284
1565
|
roomId,
|
|
@@ -1292,7 +1573,7 @@ export async function postUserMessage(
|
|
|
1292
1573
|
memberIds,
|
|
1293
1574
|
message,
|
|
1294
1575
|
history,
|
|
1295
|
-
|
|
1576
|
+
only || undefined,
|
|
1296
1577
|
env,
|
|
1297
1578
|
parent,
|
|
1298
1579
|
extras,
|
|
@@ -1317,19 +1598,25 @@ export async function retryMessage(
|
|
|
1317
1598
|
const current = messages[index];
|
|
1318
1599
|
|
|
1319
1600
|
if (current.author === "you") {
|
|
1601
|
+
const bots = store.listBots();
|
|
1602
|
+
const mentions =
|
|
1603
|
+
extras.mentions !== undefined
|
|
1604
|
+
? sanitizeMentionIds(extras.mentions, bots)
|
|
1605
|
+
: undefined;
|
|
1320
1606
|
const message =
|
|
1321
1607
|
typeof body === "string" && body.trim()
|
|
1322
|
-
? store.updateMessage(roomId, messageId, body)
|
|
1608
|
+
? store.updateMessage(roomId, messageId, body, mentions)
|
|
1323
1609
|
: current;
|
|
1324
1610
|
store.truncateAfter(roomId, messageId);
|
|
1325
1611
|
const kept = store.listMessages(roomId);
|
|
1326
1612
|
const history = kept.slice(0, -1).map(toHistoryItem);
|
|
1327
1613
|
const parent = parentMessage(kept.slice(0, -1), message.replyTo);
|
|
1328
1614
|
const assignee = assigneeId?.trim();
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1615
|
+
const only = exclusiveReplyBot(store, message, parent, assignee);
|
|
1616
|
+
let memberIds = only
|
|
1617
|
+
? inviteAssignee(store, roomId, only)
|
|
1618
|
+
: inviteMentionedBots(store, roomId, message);
|
|
1619
|
+
if (!only && !hasExplicitSummon(store, message)) {
|
|
1333
1620
|
memberIds = includeFollowBot(
|
|
1334
1621
|
store,
|
|
1335
1622
|
roomId,
|
|
@@ -1343,7 +1630,7 @@ export async function retryMessage(
|
|
|
1343
1630
|
memberIds,
|
|
1344
1631
|
message,
|
|
1345
1632
|
history,
|
|
1346
|
-
|
|
1633
|
+
only || undefined,
|
|
1347
1634
|
env,
|
|
1348
1635
|
parent,
|
|
1349
1636
|
extras,
|
|
@@ -1443,4 +1730,4 @@ export async function retryMessage(
|
|
|
1443
1730
|
|
|
1444
1731
|
export { StoreError, localGenerate };
|
|
1445
1732
|
|
|
1446
|
-
export { publicModels, mergeModelsFile } from "./llm.ts";
|
|
1733
|
+
export { publicModels, mergeModelsFile, refreshOpenCodeFreeCatalog } from "./llm.ts";
|
package/src/harness.ts
CHANGED
|
@@ -156,7 +156,15 @@ export function gateTool(
|
|
|
156
156
|
if (sandbox === "full_access") return null;
|
|
157
157
|
|
|
158
158
|
if (sandbox === "read_only") {
|
|
159
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
name === "read" ||
|
|
161
|
+
name === "list" ||
|
|
162
|
+
name === "skill" ||
|
|
163
|
+
name === "spawn" ||
|
|
164
|
+
name === "read_spawn"
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
160
168
|
return {
|
|
161
169
|
text: `sandbox=read_only refused ${name}`,
|
|
162
170
|
isError: true,
|
|
@@ -174,7 +182,13 @@ export function gateTool(
|
|
|
174
182
|
};
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
if (
|
|
185
|
+
if (
|
|
186
|
+
name === "read" ||
|
|
187
|
+
name === "list" ||
|
|
188
|
+
name === "skill" ||
|
|
189
|
+
name === "spawn" ||
|
|
190
|
+
name === "read_spawn"
|
|
191
|
+
) {
|
|
178
192
|
return null;
|
|
179
193
|
}
|
|
180
194
|
|
|
@@ -296,18 +310,17 @@ export async function runAgentLoop(input: {
|
|
|
296
310
|
if (traces.length) return { text: emptyAfterTools, traces, thinking };
|
|
297
311
|
return input.nullIfNoTraces ? null : { text: emptyAfterTools, traces, thinking };
|
|
298
312
|
}
|
|
299
|
-
const outcomes
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await executeToolTraced(
|
|
313
|
+
const outcomes = await Promise.all(
|
|
314
|
+
asked.calls.map((call) =>
|
|
315
|
+
executeToolTraced(
|
|
303
316
|
call.name,
|
|
304
317
|
call.args,
|
|
305
318
|
input.toolCtx,
|
|
306
319
|
traces,
|
|
307
320
|
thinking,
|
|
308
321
|
),
|
|
309
|
-
)
|
|
310
|
-
|
|
322
|
+
),
|
|
323
|
+
);
|
|
311
324
|
input.onTools?.(asked.calls, outcomes);
|
|
312
325
|
}
|
|
313
326
|
}
|
package/src/image-gen.ts
CHANGED
|
@@ -218,7 +218,7 @@ export async function generateImage(input: {
|
|
|
218
218
|
aspectRatio?: string;
|
|
219
219
|
dataDir?: string;
|
|
220
220
|
env?: NodeJS.ProcessEnv;
|
|
221
|
-
}): Promise<{ text: string; isError: boolean }> {
|
|
221
|
+
}): Promise<{ text: string; isError: boolean; publicPath?: string }> {
|
|
222
222
|
const prompt = String(input.prompt || "").trim();
|
|
223
223
|
if (!prompt) return { text: "prompt is required", isError: true };
|
|
224
224
|
const aspect = String(input.aspectRatio || "").trim();
|
|
@@ -252,6 +252,7 @@ export async function generateImage(input: {
|
|
|
252
252
|
`markdown: `,
|
|
253
253
|
].join("\n"),
|
|
254
254
|
isError: false,
|
|
255
|
+
publicPath: saved.publicPath,
|
|
255
256
|
};
|
|
256
257
|
}
|
|
257
258
|
errors.push(`${route.model} @ ${route.url}: ${hit.error}`);
|