@kevin5251984/guild 0.2.12 → 0.2.14
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 +10 -8
- package/src/catalog/subagents.ts +1 -2
- package/src/chat-parts.ts +1 -6
- package/src/generate.ts +27 -55
- package/src/handlers.ts +6 -183
- package/src/harness.ts +8 -21
- package/src/image-gen.ts +1 -2
- package/src/llm.ts +7 -14
- package/src/mention.ts +12 -63
- package/src/oauth.ts +27 -131
- package/src/public/chat.css +191 -143
- package/src/public/chat.html +49 -191
- package/src/public/i18n.js +6 -18
- package/src/public/index.html +46 -1
- package/src/public/settings.html +1 -2
- package/src/public/skills-add.html +0 -8
- package/src/public/studio.html +117 -171
- package/src/public/style.css +150 -260
- package/src/public/subagents-add.html +0 -8
- package/src/router.ts +0 -11
- package/src/store.ts +0 -14
- package/src/subagent.ts +6 -203
- package/src/tools.ts +18 -117
- package/src/trajectory.ts +5 -51
- package/src/usage.ts +0 -10
- package/vendor/protocol/src/index.ts +1 -4
- package/src/public/buddy.js +0 -432
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevin5251984/guild",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A local guild of adventurers. npx @kevin5251984/guild web",
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "tsx src/cli.ts",
|
|
33
|
+
"start": "node ./bin/guildd.mjs",
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"test": "tsx --test test/*.test.ts",
|
|
36
|
+
"prepack": "node ./scripts/vendor-protocol.mjs prepack",
|
|
37
|
+
"postpack": "node ./scripts/vendor-protocol.mjs postpack"
|
|
38
|
+
},
|
|
31
39
|
"dependencies": {
|
|
32
40
|
"@cordisjs/plugin-include": "1.0.4",
|
|
33
41
|
"@cordisjs/plugin-loader": "1.0.0-rc.5",
|
|
@@ -42,11 +50,5 @@
|
|
|
42
50
|
"devDependencies": {
|
|
43
51
|
"@types/node": "22.19.19",
|
|
44
52
|
"typescript": "5.9.3"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"dev": "tsx src/cli.ts",
|
|
48
|
-
"start": "node ./bin/guildd.mjs",
|
|
49
|
-
"build": "tsc -p tsconfig.json",
|
|
50
|
-
"test": "tsx --test test/*.test.ts"
|
|
51
53
|
}
|
|
52
|
-
}
|
|
54
|
+
}
|
package/src/catalog/subagents.ts
CHANGED
|
@@ -20,8 +20,7 @@ sandbox_mode = "read-only"
|
|
|
20
20
|
developer_instructions = """
|
|
21
21
|
Role: codebase search specialist. Find files and code. Read-only.
|
|
22
22
|
|
|
23
|
-
Answer "where is X / which files do Y" with every relevant absolute path and the actual need behind the request.
|
|
24
|
-
Fire 3+ independent tool calls in the first round (they run in parallel). Stop after two waves add nothing new.
|
|
23
|
+
Answer "where is X / which files do Y" with every relevant absolute path and the actual need behind the request. Prefer parallel searches. Stop after two waves add nothing new.
|
|
25
24
|
|
|
26
25
|
Never edit, write, or apply patches. Findings are message text only.
|
|
27
26
|
|
package/src/chat-parts.ts
CHANGED
|
@@ -40,16 +40,11 @@ export function assembleParts(input: {
|
|
|
40
40
|
? String(trace.args.prompt ?? "")
|
|
41
41
|
: trace.name === "spawn"
|
|
42
42
|
? String(
|
|
43
|
-
trace.args.
|
|
44
|
-
trace.args.description ||
|
|
45
|
-
trace.args.profile ||
|
|
43
|
+
trace.args.description ||
|
|
46
44
|
trace.args.name ||
|
|
47
|
-
trace.args.task ||
|
|
48
45
|
trace.args.prompt ||
|
|
49
46
|
"",
|
|
50
47
|
)
|
|
51
|
-
: trace.name === "read_spawn"
|
|
52
|
-
? String(trace.args.agent_id || trace.args.id || "")
|
|
53
48
|
: String(trace.args.path ?? ""),
|
|
54
49
|
output: trace.text,
|
|
55
50
|
isError: trace.isError,
|
package/src/generate.ts
CHANGED
|
@@ -350,16 +350,16 @@ When work belongs to someone else, put @handle at the start of a line with a wri
|
|
|
350
350
|
- Done when
|
|
351
351
|
- Constraints / out of scope
|
|
352
352
|
- Files or evidence
|
|
353
|
-
Each line-start @handle on this quest starts that seat.
|
|
353
|
+
Each line-start @handle on this quest starts that seat. Mentions in the middle of a sentence do not dispatch.
|
|
354
354
|
Do not @all unless the human did. Do not recruit extra people; the human staffs the roster (max ${CHANNEL_ROSTER_CAP} on a quest).
|
|
355
|
-
|
|
355
|
+
Only line-start @handle a seat the human already named this turn, or that they asked you to split the work to. Do not invent a third seat. If B must wait for A, do not @ B this turn.
|
|
356
356
|
Stay quiet: no status theater, no "I'll start now." Speak when you finish, block, or need a decision. Money, sends, and destructive actions wait for the human.
|
|
357
357
|
|
|
358
358
|
Harness this turn (Memory → Plan → Skills → Act):
|
|
359
359
|
- Memory: Channel.md is the task. MEMORY.md is standing notes. The compact log is working memory — do not recap the whole thread.
|
|
360
360
|
- Plan: one local directive (goal + done when) before tools. Revise it when evidence changes.
|
|
361
361
|
- Skills: the catalog is availability, not a todo. Call \`skill\` only when this directive matches. Do not load every skill.
|
|
362
|
-
- Act:
|
|
362
|
+
- Act: inspect, smallest change, verify, stop. Spawn is a specialist for a bounded slice of THIS seat's job (explore / review / implement) with a fresh context. Do not spawn to do another staffed bot's job — @handle them instead.`;
|
|
363
363
|
|
|
364
364
|
export function buildChatSystem(input: {
|
|
365
365
|
botName: string;
|
|
@@ -397,57 +397,29 @@ export function buildChatSystem(input: {
|
|
|
397
397
|
"</system-reminder>",
|
|
398
398
|
].join("\n")
|
|
399
399
|
: "";
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
name: "worker",
|
|
424
|
-
description:
|
|
425
|
-
"Implementation executor. Smallest correct change, then verify.",
|
|
426
|
-
readOnly: false,
|
|
427
|
-
},
|
|
428
|
-
];
|
|
429
|
-
const spawnLine = [
|
|
430
|
-
"<available_subagents>",
|
|
431
|
-
...spawnCatalog.slice(0, 40).map((item) => {
|
|
432
|
-
const key = item.slug || item.name;
|
|
433
|
-
const desc = (item.description || item.name)
|
|
434
|
-
.replace(/\s+/g, " ")
|
|
435
|
-
.trim()
|
|
436
|
-
.slice(0, 220);
|
|
437
|
-
const mode = item.readOnly ? "read-only" : "read-write";
|
|
438
|
-
return `- \`${key}\` (${mode}): ${desc}`;
|
|
439
|
-
}),
|
|
440
|
-
"</available_subagents>",
|
|
441
|
-
"Call spawn with the exact name (or slug) and a self-contained prompt (Pi: agent+task). Default: explorer to orient across unknown files, reviewer to critique a change, worker for an isolated patch. Independent slices: several spawn calls in this round, or tasks: [{name, prompt}]. You stay this seat's coordinator. Skipping spawn and reading the whole tree yourself is the wrong default.",
|
|
442
|
-
"If the user writes /name matching a subagent, spawn that one. The child has a fresh context and returns a summary.",
|
|
443
|
-
wantSpawn.length
|
|
444
|
-
? `This turn the user invoked ${wantSpawn
|
|
445
|
-
.map((item) => "`/" + (item.slug || item.name) + "`")
|
|
446
|
-
.join(", ")}. Call spawn with that exact name first, with a self-contained prompt covering their request. Do not skip this and do the work yourself.`
|
|
447
|
-
: "",
|
|
448
|
-
]
|
|
449
|
-
.filter(Boolean)
|
|
450
|
-
.join("\n");
|
|
400
|
+
const spawnLine = subagents.length
|
|
401
|
+
? [
|
|
402
|
+
"<available_subagents>",
|
|
403
|
+
...subagents.slice(0, 40).map((item) => {
|
|
404
|
+
const key = item.slug || item.name;
|
|
405
|
+
const desc = (item.description || item.name)
|
|
406
|
+
.replace(/\s+/g, " ")
|
|
407
|
+
.trim()
|
|
408
|
+
.slice(0, 220);
|
|
409
|
+
const mode = item.readOnly ? "read-only" : "read-write";
|
|
410
|
+
return `- \`${key}\` (${mode}): ${desc}`;
|
|
411
|
+
}),
|
|
412
|
+
"</available_subagents>",
|
|
413
|
+
"Call spawn with the exact name (or slug) and a self-contained prompt. If the user writes /name matching a subagent, spawn that one. The child has a fresh context and returns a summary.",
|
|
414
|
+
wantSpawn.length
|
|
415
|
+
? `This turn the user invoked ${wantSpawn
|
|
416
|
+
.map((item) => "`/" + (item.slug || item.name) + "`")
|
|
417
|
+
.join(", ")}. Call spawn with that exact name first, with a self-contained prompt covering their request. Do not skip this and do the work yourself.`
|
|
418
|
+
: "",
|
|
419
|
+
]
|
|
420
|
+
.filter(Boolean)
|
|
421
|
+
.join("\n")
|
|
422
|
+
: "";
|
|
451
423
|
const channel = (input.channelMd ?? "").trim();
|
|
452
424
|
const channelBlock = channel
|
|
453
425
|
? `# Channel.md\nThis channel's operating notes written by the user. Follow them for this room. They outrank MEMORY.md.\n\n${channel.slice(0, 4000)}`
|
|
@@ -543,7 +515,7 @@ export function localChatReply(
|
|
|
543
515
|
userMessage: string,
|
|
544
516
|
): string {
|
|
545
517
|
const clip = userMessage.trim().slice(0, 120);
|
|
546
|
-
return `【${botName} @${handle}】收到。「${clip}」\n\n
|
|
518
|
+
return `【${botName} @${handle}】收到。「${clip}」\n\n沒有可用模型,本機工具還沒辦法跑。到私訊幫我選一個模型後再問。`;
|
|
547
519
|
}
|
|
548
520
|
|
|
549
521
|
async function tryChatLlm(
|
package/src/handlers.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
} from "./generate.ts";
|
|
19
19
|
import {
|
|
20
20
|
liveTrajectoryEvents,
|
|
21
|
-
promoteSpawnEvent,
|
|
22
21
|
synthesizeTrajectory,
|
|
23
22
|
turnTrajectoryEvents,
|
|
24
23
|
userTrajectoryEvent,
|
|
@@ -26,13 +25,7 @@ import {
|
|
|
26
25
|
import { importFromGithub, importFromUrl } from "./skill-import.ts";
|
|
27
26
|
import { harvestBotMemory, harvestChannelMemory } from "./memory.ts";
|
|
28
27
|
import { listHostSkills, type HostSkill } from "./host-skills.ts";
|
|
29
|
-
import {
|
|
30
|
-
CHANNEL_ROSTER_CAP,
|
|
31
|
-
GuildStore,
|
|
32
|
-
StoreError,
|
|
33
|
-
type LiveStep,
|
|
34
|
-
type LiveTurn,
|
|
35
|
-
} from "./store.ts";
|
|
28
|
+
import { GuildStore, StoreError, type LiveStep, type LiveTurn } from "./store.ts";
|
|
36
29
|
import { listSpawnRefs } from "./subagent.ts";
|
|
37
30
|
import {
|
|
38
31
|
importHostMcp,
|
|
@@ -44,7 +37,6 @@ import {
|
|
|
44
37
|
} from "./mcp.ts";
|
|
45
38
|
import {
|
|
46
39
|
assignmentFor,
|
|
47
|
-
handoffHandles,
|
|
48
40
|
isBroadcastMention,
|
|
49
41
|
summonedHandles,
|
|
50
42
|
} from "./mention.ts";
|
|
@@ -250,7 +242,6 @@ export function updateBot(
|
|
|
250
242
|
name?: string;
|
|
251
243
|
handle?: string;
|
|
252
244
|
oneLiner?: string;
|
|
253
|
-
portrait?: string | null;
|
|
254
245
|
skillIds?: string[];
|
|
255
246
|
soul?: MarkdownDraft;
|
|
256
247
|
agent?: MarkdownDraft;
|
|
@@ -265,162 +256,6 @@ export function updateBot(
|
|
|
265
256
|
return store.updateBot(id, { ...input, skillIds });
|
|
266
257
|
}
|
|
267
258
|
|
|
268
|
-
const LOOK_HAIR = [
|
|
269
|
-
"jet-black short crop",
|
|
270
|
-
"jet-black long straight hair",
|
|
271
|
-
"copper-red bob with blunt bangs",
|
|
272
|
-
"copper-red messy spikes",
|
|
273
|
-
"indigo long waves",
|
|
274
|
-
"indigo pixie cut",
|
|
275
|
-
"honey-blonde high ponytail",
|
|
276
|
-
"honey-blonde bowl cut",
|
|
277
|
-
"hot-pink messy spikes",
|
|
278
|
-
"hot-pink bob",
|
|
279
|
-
"ash-white curly volume",
|
|
280
|
-
"ash-white long hair",
|
|
281
|
-
"teal-tinted undercut",
|
|
282
|
-
"teal high ponytail",
|
|
283
|
-
"deep-burgundy twin braids",
|
|
284
|
-
"deep-burgundy shag",
|
|
285
|
-
];
|
|
286
|
-
const LOOK_CLOTH = [
|
|
287
|
-
"sunflower-yellow collared shirt",
|
|
288
|
-
"cobalt hooded jacket",
|
|
289
|
-
"rose cardigan over a cream tee",
|
|
290
|
-
"forest-green knit turtleneck",
|
|
291
|
-
"ivory blouse with a coral scarf",
|
|
292
|
-
"charcoal work vest over a rust tee",
|
|
293
|
-
"lilac haori",
|
|
294
|
-
"orange windbreaker",
|
|
295
|
-
"white lab coat over a black tee",
|
|
296
|
-
"crimson bomber jacket",
|
|
297
|
-
"mint sailor collar",
|
|
298
|
-
"navy peacoat",
|
|
299
|
-
"gold-trimmed teal capelet",
|
|
300
|
-
"checkered red-and-black shirt",
|
|
301
|
-
"pale-blue denim jacket",
|
|
302
|
-
"magenta track jacket",
|
|
303
|
-
];
|
|
304
|
-
const LOOK_EXTRA = [
|
|
305
|
-
"round wire glasses",
|
|
306
|
-
"small gold hoop earrings",
|
|
307
|
-
"over-ear headphones around the neck",
|
|
308
|
-
"a paintbrush tucked behind one ear",
|
|
309
|
-
"a red hair clip",
|
|
310
|
-
"a thin black choker",
|
|
311
|
-
"a knitted ear warmer",
|
|
312
|
-
"no extra accessories",
|
|
313
|
-
];
|
|
314
|
-
const LOOK_SKIN = [
|
|
315
|
-
"fair peach human skin",
|
|
316
|
-
"warm tan human skin",
|
|
317
|
-
"light brown human skin",
|
|
318
|
-
"deep brown human skin",
|
|
319
|
-
"golden beige human skin",
|
|
320
|
-
];
|
|
321
|
-
const LOOK_FACE = [
|
|
322
|
-
"round cheerful face with wide-set eyes",
|
|
323
|
-
"sharp jaw and narrow eyes",
|
|
324
|
-
"soft oval face with thick brows",
|
|
325
|
-
"heart-shaped face and a small nose",
|
|
326
|
-
"square face with a bright closed-mouth smile",
|
|
327
|
-
];
|
|
328
|
-
const LOOK_RACE = [
|
|
329
|
-
{
|
|
330
|
-
id: "human" as const,
|
|
331
|
-
label: "human",
|
|
332
|
-
prompt:
|
|
333
|
-
"human. Ordinary rounded human ears, fully human anatomy, no fantasy ears.",
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
id: "dwarf" as const,
|
|
337
|
-
label: "dwarf",
|
|
338
|
-
prompt:
|
|
339
|
-
"young dwarf. Stout neck, broader cheekbones, a slightly larger nose, thick brows, rounded ears, youthful (not elderly, not bald).",
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
id: "elf" as const,
|
|
343
|
-
label: "elf",
|
|
344
|
-
prompt:
|
|
345
|
-
"young elf. Long pointed ears clearly visible, fine features, almond eyes, still youthful.",
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
id: "demihuman" as const,
|
|
349
|
-
label: "demihuman",
|
|
350
|
-
prompt:
|
|
351
|
-
"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.",
|
|
352
|
-
},
|
|
353
|
-
];
|
|
354
|
-
|
|
355
|
-
function lookSeed(key: string): number {
|
|
356
|
-
let n = 2166136261;
|
|
357
|
-
for (const ch of key) {
|
|
358
|
-
n ^= ch.charCodeAt(0);
|
|
359
|
-
n = Math.imul(n, 16777619);
|
|
360
|
-
}
|
|
361
|
-
return n >>> 0;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function lookTraits(bot: { name: string; handle: string }): {
|
|
365
|
-
hair: string;
|
|
366
|
-
cloth: string;
|
|
367
|
-
extra: string;
|
|
368
|
-
skin: string;
|
|
369
|
-
face: string;
|
|
370
|
-
race: (typeof LOOK_RACE)[number];
|
|
371
|
-
} {
|
|
372
|
-
const n = lookSeed(bot.handle || bot.name || "bot");
|
|
373
|
-
return {
|
|
374
|
-
hair: LOOK_HAIR[n % LOOK_HAIR.length],
|
|
375
|
-
cloth: LOOK_CLOTH[(n >>> 4) % LOOK_CLOTH.length],
|
|
376
|
-
extra: LOOK_EXTRA[(n >>> 8) % LOOK_EXTRA.length],
|
|
377
|
-
skin: LOOK_SKIN[(n >>> 12) % LOOK_SKIN.length],
|
|
378
|
-
face: LOOK_FACE[(n >>> 16) % LOOK_FACE.length],
|
|
379
|
-
race: LOOK_RACE[(n >>> 20) % LOOK_RACE.length],
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export function lookPrompt(bot: {
|
|
384
|
-
name: string;
|
|
385
|
-
handle: string;
|
|
386
|
-
oneLiner?: string;
|
|
387
|
-
}): string {
|
|
388
|
-
const role = bot.oneLiner?.trim() || "keeps a seat in the guild tavern";
|
|
389
|
-
const look = lookTraits(bot);
|
|
390
|
-
return [
|
|
391
|
-
`SNES 16-bit pixel-art bust portrait of ${bot.name} (@${bot.handle}), a unique ${look.race.label} guild adventurer.`,
|
|
392
|
-
`Race (mandatory): ${look.race.prompt}`,
|
|
393
|
-
`Mandatory look: ${look.skin}, ${look.face}, ${look.hair}, wearing a ${look.cloth}, ${look.extra}.`,
|
|
394
|
-
`They ${role}.`,
|
|
395
|
-
"Hair color, outfit, and race are locked; do not default to brown hair or a beige coat.",
|
|
396
|
-
"Natural skin tones only, never green, gray, or monster skin.",
|
|
397
|
-
"Head and shoulders only, facing the camera, chunky 16-bit pixels, limited tavern palette, cream pixel outline.",
|
|
398
|
-
"Animal Crossing crossed with Earthbound, youthful SNES NPC.",
|
|
399
|
-
"Opaque cream or tavern-wood background, no black void, no white photo studio, no checkerboard, no transparency.",
|
|
400
|
-
"Close-up character headshot, no full body, no legs, no floor, no scenery, no photorealism, no 3D render, no text, no watermark.",
|
|
401
|
-
].join(" ");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export async function generateBotLook(
|
|
405
|
-
store: GuildStore,
|
|
406
|
-
id: string,
|
|
407
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
408
|
-
) {
|
|
409
|
-
const bot = store.getBot(id);
|
|
410
|
-
if (!bot) throw new StoreError(404, "bot not found");
|
|
411
|
-
const { generateImage } = await import("./image-gen.ts");
|
|
412
|
-
const result = await generateImage({
|
|
413
|
-
prompt: lookPrompt(bot),
|
|
414
|
-
aspectRatio: "1:1",
|
|
415
|
-
dataDir: store.dataDir,
|
|
416
|
-
env,
|
|
417
|
-
});
|
|
418
|
-
if (result.isError || !result.publicPath) {
|
|
419
|
-
throw new StoreError(502, result.text);
|
|
420
|
-
}
|
|
421
|
-
return store.updateBot(id, { portrait: result.publicPath });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
259
|
export async function importSkills(
|
|
425
260
|
store: GuildStore,
|
|
426
261
|
input: { source: string; url?: string; repo?: string },
|
|
@@ -585,7 +420,7 @@ function handoffTargets(
|
|
|
585
420
|
const queued = new Set<string>();
|
|
586
421
|
for (const reply of replies) {
|
|
587
422
|
if (isBroadcastMention(reply.body)) continue;
|
|
588
|
-
const names =
|
|
423
|
+
const names = summonedHandles(reply.body, handles);
|
|
589
424
|
if (!names.length) continue;
|
|
590
425
|
const from = bots.find((bot) => bot.id === reply.author);
|
|
591
426
|
const fromHandle = from?.handle || reply.author;
|
|
@@ -899,18 +734,7 @@ function liveDetail(trace: ToolTrace): string {
|
|
|
899
734
|
if (trace.name === "skill") return String(args.name || "");
|
|
900
735
|
if (trace.name === "image_gen") return String(args.prompt || "");
|
|
901
736
|
if (trace.name === "spawn") {
|
|
902
|
-
return String(
|
|
903
|
-
args.title ||
|
|
904
|
-
args.description ||
|
|
905
|
-
args.profile ||
|
|
906
|
-
args.name ||
|
|
907
|
-
args.task ||
|
|
908
|
-
args.prompt ||
|
|
909
|
-
"",
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
if (trace.name === "read_spawn") {
|
|
913
|
-
return String(args.agent_id || args.id || "");
|
|
737
|
+
return String(args.description || args.name || args.prompt || "");
|
|
914
738
|
}
|
|
915
739
|
if (trace.name.startsWith("mcp__")) {
|
|
916
740
|
return JSON.stringify(args).slice(0, 120);
|
|
@@ -1221,9 +1045,8 @@ async function generateReplies(
|
|
|
1221
1045
|
return speak(botId, turnAsked, history);
|
|
1222
1046
|
}),
|
|
1223
1047
|
);
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (!hops.length) break;
|
|
1048
|
+
const hops = handoffTargets(store, memberIds, replies, asked, history);
|
|
1049
|
+
if (hops.length && !signal.aborted) {
|
|
1227
1050
|
const hopAt = new Date().toISOString();
|
|
1228
1051
|
for (const hop of hops) {
|
|
1229
1052
|
store.adoptTurn(roomId, hop.botId, signal);
|
|
@@ -1310,7 +1133,7 @@ export function listRoomTrajectory(store: GuildStore, roomId: string) {
|
|
|
1310
1133
|
});
|
|
1311
1134
|
return {
|
|
1312
1135
|
...base,
|
|
1313
|
-
events: base.events.
|
|
1136
|
+
events: base.events.concat(extra),
|
|
1314
1137
|
live: extra.length > 0 || open,
|
|
1315
1138
|
};
|
|
1316
1139
|
}
|
package/src/harness.ts
CHANGED
|
@@ -156,15 +156,7 @@ export function gateTool(
|
|
|
156
156
|
if (sandbox === "full_access") return null;
|
|
157
157
|
|
|
158
158
|
if (sandbox === "read_only") {
|
|
159
|
-
if (
|
|
160
|
-
name === "read" ||
|
|
161
|
-
name === "list" ||
|
|
162
|
-
name === "skill" ||
|
|
163
|
-
name === "spawn" ||
|
|
164
|
-
name === "read_spawn"
|
|
165
|
-
) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
159
|
+
if (name === "read" || name === "list" || name === "skill") return null;
|
|
168
160
|
return {
|
|
169
161
|
text: `sandbox=read_only refused ${name}`,
|
|
170
162
|
isError: true,
|
|
@@ -182,13 +174,7 @@ export function gateTool(
|
|
|
182
174
|
};
|
|
183
175
|
}
|
|
184
176
|
|
|
185
|
-
if (
|
|
186
|
-
name === "read" ||
|
|
187
|
-
name === "list" ||
|
|
188
|
-
name === "skill" ||
|
|
189
|
-
name === "spawn" ||
|
|
190
|
-
name === "read_spawn"
|
|
191
|
-
) {
|
|
177
|
+
if (name === "read" || name === "list" || name === "skill" || name === "spawn") {
|
|
192
178
|
return null;
|
|
193
179
|
}
|
|
194
180
|
|
|
@@ -310,17 +296,18 @@ export async function runAgentLoop(input: {
|
|
|
310
296
|
if (traces.length) return { text: emptyAfterTools, traces, thinking };
|
|
311
297
|
return input.nullIfNoTraces ? null : { text: emptyAfterTools, traces, thinking };
|
|
312
298
|
}
|
|
313
|
-
const outcomes =
|
|
314
|
-
|
|
315
|
-
|
|
299
|
+
const outcomes: ToolOutcome[] = [];
|
|
300
|
+
for (const call of asked.calls) {
|
|
301
|
+
outcomes.push(
|
|
302
|
+
await executeToolTraced(
|
|
316
303
|
call.name,
|
|
317
304
|
call.args,
|
|
318
305
|
input.toolCtx,
|
|
319
306
|
traces,
|
|
320
307
|
thinking,
|
|
321
308
|
),
|
|
322
|
-
)
|
|
323
|
-
|
|
309
|
+
);
|
|
310
|
+
}
|
|
324
311
|
input.onTools?.(asked.calls, outcomes);
|
|
325
312
|
}
|
|
326
313
|
}
|
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 }> {
|
|
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,7 +252,6 @@ export async function generateImage(input: {
|
|
|
252
252
|
`markdown: `,
|
|
253
253
|
].join("\n"),
|
|
254
254
|
isError: false,
|
|
255
|
-
publicPath: saved.publicPath,
|
|
256
255
|
};
|
|
257
256
|
}
|
|
258
257
|
errors.push(`${route.model} @ ${route.url}: ${hit.error}`);
|
package/src/llm.ts
CHANGED
|
@@ -39,11 +39,13 @@ import {
|
|
|
39
39
|
export const AUX_ROLES: { id: AuxRole; name: string; hint: string }[] = [
|
|
40
40
|
{ id: "vision", name: "Vision", hint: "Image analysis" },
|
|
41
41
|
{ id: "web", name: "Web extract", hint: "Page summarization" },
|
|
42
|
-
{ id: "
|
|
42
|
+
{ id: "compression", name: "Compression", hint: "Context compaction" },
|
|
43
|
+
{ id: "skills", name: "Skills hub", hint: "Skill search" },
|
|
44
|
+
{ id: "approval", name: "Approval", hint: "Command risk scoring" },
|
|
45
|
+
{ id: "title", name: "Title", hint: "Session titles" },
|
|
46
|
+
{ id: "generate", name: "Generate", hint: "Soul / Agent / Skill markdown" },
|
|
43
47
|
];
|
|
44
48
|
|
|
45
|
-
const CONFIGURABLE_AUX = new Set(AUX_ROLES.map((role) => role.id));
|
|
46
|
-
|
|
47
49
|
export const DEFAULT_MODELS: ModelsFile = {
|
|
48
50
|
default: null,
|
|
49
51
|
reasoning: "medium",
|
|
@@ -290,9 +292,7 @@ export function resolveLlm(
|
|
|
290
292
|
const file = readModelsFile(dataDir);
|
|
291
293
|
const ref: ModelRef | null | undefined =
|
|
292
294
|
prefer ??
|
|
293
|
-
(role &&
|
|
294
|
-
? file.aux?.[role as AuxRole]
|
|
295
|
-
: file.default);
|
|
295
|
+
(role && role !== "chat" ? file.aux?.[role] : file.default);
|
|
296
296
|
const tryProvider = (id: string, modelId?: string): LlmTarget | null => {
|
|
297
297
|
const oauth = oauthTarget(dataDir, id, modelId);
|
|
298
298
|
if (oauth) return oauth;
|
|
@@ -541,8 +541,6 @@ async function completeOpenAiTools(
|
|
|
541
541
|
prompt_tokens?: number;
|
|
542
542
|
completion_tokens?: number;
|
|
543
543
|
total_tokens?: number;
|
|
544
|
-
prompt_tokens_details?: { cached_tokens?: number };
|
|
545
|
-
input_tokens_details?: { cached_tokens?: number };
|
|
546
544
|
};
|
|
547
545
|
};
|
|
548
546
|
const message = data.choices?.[0]?.message;
|
|
@@ -650,12 +648,7 @@ async function completeAnthropicTools(
|
|
|
650
648
|
const data = (await response.json()) as {
|
|
651
649
|
stop_reason?: string;
|
|
652
650
|
content?: Part[];
|
|
653
|
-
usage?: {
|
|
654
|
-
input_tokens?: number;
|
|
655
|
-
output_tokens?: number;
|
|
656
|
-
cache_read_input_tokens?: number;
|
|
657
|
-
cache_creation_input_tokens?: number;
|
|
658
|
-
};
|
|
651
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
659
652
|
};
|
|
660
653
|
const parts = data.content ?? [];
|
|
661
654
|
lastParts = parts;
|
package/src/mention.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/** @channel / @quest / @here / @all still mean the whole quest. */
|
|
2
2
|
const BROADCAST = /(^|\s)@(channel|quest|here|all)\b/i;
|
|
3
3
|
const HANDLE = /@([A-Za-z0-9_-]+)/g;
|
|
4
|
-
/** Consecutive @handles at the start of a line
|
|
5
|
-
const LINE_LEAD = /^[ \t]*((
|
|
6
|
-
/** `1. @design` / `- @infra`. */
|
|
7
|
-
const LIST_MARK = /^[ \t]*(?:\d+[.)、]\s*|[-*•]\s+)/;
|
|
8
|
-
/** Assignee right after the marker, or after 通過後/最後/再叫… — not the first @ anywhere on the line. */
|
|
9
|
-
const LIST_ASSIGN =
|
|
10
|
-
/^(?:`?@([A-Za-z0-9_-]+)`?|(?:通過後|最後|再叫|交給|交棒(?:給)?|指派|(?:call|ask)\s+)\s*`?@([A-Za-z0-9_-]+)`?)/i;
|
|
4
|
+
/** Consecutive @handles at the start of a line. */
|
|
5
|
+
const LINE_LEAD = /^[ \t]*((?:@[A-Za-z0-9_-]+[\s,、]*)+)/;
|
|
11
6
|
|
|
12
7
|
/** Drop fenced / inline code so `@pm` in a snippet does not dispatch. */
|
|
13
8
|
export function stripMentionNoise(text: string): string {
|
|
@@ -37,24 +32,13 @@ export type MentionBlock = {
|
|
|
37
32
|
start: number;
|
|
38
33
|
end: number;
|
|
39
34
|
handles: string[];
|
|
40
|
-
kind: "lead" | "list";
|
|
41
35
|
};
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
const names: string[] = [];
|
|
45
|
-
for (const row of chunk.matchAll(HANDLE)) {
|
|
46
|
-
const key = row[1].toLowerCase();
|
|
47
|
-
if (!known.has(key) || names.includes(key)) continue;
|
|
48
|
-
names.push(key);
|
|
49
|
-
}
|
|
50
|
-
return names;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Line-start @handle groups, plus markdown list items that name a seat. */
|
|
37
|
+
/** Line-start @handle groups. Prose `@pm` in the middle of a line is not a block. */
|
|
54
38
|
export function lineStartMentions(text: string, handles: string[]): MentionBlock[] {
|
|
55
39
|
const raw = String(text ?? "");
|
|
56
40
|
const known = new Set(handles.map((handle) => handle.toLowerCase()));
|
|
57
|
-
const starts: { start: number; handles: string[]
|
|
41
|
+
const starts: { start: number; handles: string[] }[] = [];
|
|
58
42
|
let fence = false;
|
|
59
43
|
let offset = 0;
|
|
60
44
|
for (const part of raw.split(/(\r?\n)/)) {
|
|
@@ -69,22 +53,15 @@ export function lineStartMentions(text: string, handles: string[]): MentionBlock
|
|
|
69
53
|
continue;
|
|
70
54
|
}
|
|
71
55
|
if (!fence) {
|
|
72
|
-
const listed = LIST_MARK.test(part);
|
|
73
56
|
const lead = part.match(LINE_LEAD);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (names.length) {
|
|
83
|
-
starts.push({
|
|
84
|
-
start: offset,
|
|
85
|
-
handles: names,
|
|
86
|
-
kind: listed ? "list" : "lead",
|
|
87
|
-
});
|
|
57
|
+
if (lead) {
|
|
58
|
+
const names: string[] = [];
|
|
59
|
+
for (const row of lead[1].matchAll(HANDLE)) {
|
|
60
|
+
const key = row[1].toLowerCase();
|
|
61
|
+
if (!known.has(key) || names.includes(key)) continue;
|
|
62
|
+
names.push(key);
|
|
63
|
+
}
|
|
64
|
+
if (names.length) starts.push({ start: offset, handles: names });
|
|
88
65
|
}
|
|
89
66
|
}
|
|
90
67
|
offset += part.length;
|
|
@@ -93,7 +70,6 @@ export function lineStartMentions(text: string, handles: string[]): MentionBlock
|
|
|
93
70
|
start: row.start,
|
|
94
71
|
end: i + 1 < starts.length ? starts[i + 1].start : raw.length,
|
|
95
72
|
handles: row.handles,
|
|
96
|
-
kind: row.kind,
|
|
97
73
|
}));
|
|
98
74
|
}
|
|
99
75
|
|
|
@@ -128,31 +104,6 @@ export function summonedHandles(text: string, handles: string[]): string[] {
|
|
|
128
104
|
return [];
|
|
129
105
|
}
|
|
130
106
|
|
|
131
|
-
/** Fenced samples only. Keep inline `code` so 「再叫 `@infra`」 still parses. */
|
|
132
|
-
function stripFences(text: string): string {
|
|
133
|
-
return String(text ?? "").replace(/```[\s\S]*?```/g, " ");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Bot replies: line-start specs, else first prose @handle, plus assignment verbs
|
|
138
|
-
* even when the handle is wrapped in backticks (models quote @infra a lot).
|
|
139
|
-
* Fixture chatter like `(@pm / @rd)` has no verb, so it does not hop.
|
|
140
|
-
*/
|
|
141
|
-
const HANDOFF_ASK =
|
|
142
|
-
/(?:(?:再叫|交給|交棒(?:給)?|指派)\s*|(?:call|ask|ping|notify|handoff(?:\s+to)?)\s+)`?@([A-Za-z0-9_-]+)`?/gi;
|
|
143
|
-
|
|
144
|
-
export function handoffHandles(text: string, handles: string[]): string[] {
|
|
145
|
-
const known = new Set(handles.map((handle) => handle.toLowerCase()));
|
|
146
|
-
const out = summonedHandles(text, handles);
|
|
147
|
-
const ask = new RegExp(HANDOFF_ASK.source, "gi");
|
|
148
|
-
for (const row of stripFences(text).matchAll(ask)) {
|
|
149
|
-
const key = row[1].toLowerCase();
|
|
150
|
-
if (!known.has(key) || out.includes(key)) continue;
|
|
151
|
-
out.push(key);
|
|
152
|
-
}
|
|
153
|
-
return out;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
107
|
/**
|
|
157
108
|
* Spec for one seat: shared preamble plus that @handle's line-start block.
|
|
158
109
|
* Full text when the handle was only summoned in prose.
|
|
@@ -168,8 +119,6 @@ export function assignmentFor(
|
|
|
168
119
|
if (!key || !blocks.length) return raw;
|
|
169
120
|
const mine = blocks.filter((block) => block.handles.includes(key));
|
|
170
121
|
if (!mine.length) return raw;
|
|
171
|
-
// Numbered plans share constraints ("don't ship until PM signs off"). Slice only classic specs.
|
|
172
|
-
if (!blocks.some((block) => block.kind === "lead")) return raw;
|
|
173
122
|
const preamble = raw.slice(0, blocks[0].start).trim();
|
|
174
123
|
const chunks = mine.map((block) => raw.slice(block.start, block.end).trim());
|
|
175
124
|
return [preamble, ...chunks].filter(Boolean).join("\n\n");
|