@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/package.json
CHANGED
package/src/catalog/subagents.ts
CHANGED
|
@@ -20,7 +20,8 @@ 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.
|
|
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.
|
|
24
25
|
|
|
25
26
|
Never edit, write, or apply patches. Findings are message text only.
|
|
26
27
|
|
package/src/chat-parts.ts
CHANGED
|
@@ -40,11 +40,16 @@ export function assembleParts(input: {
|
|
|
40
40
|
? String(trace.args.prompt ?? "")
|
|
41
41
|
: trace.name === "spawn"
|
|
42
42
|
? String(
|
|
43
|
-
trace.args.
|
|
43
|
+
trace.args.title ||
|
|
44
|
+
trace.args.description ||
|
|
45
|
+
trace.args.profile ||
|
|
44
46
|
trace.args.name ||
|
|
47
|
+
trace.args.task ||
|
|
45
48
|
trace.args.prompt ||
|
|
46
49
|
"",
|
|
47
50
|
)
|
|
51
|
+
: trace.name === "read_spawn"
|
|
52
|
+
? String(trace.args.agent_id || trace.args.id || "")
|
|
48
53
|
: String(trace.args.path ?? ""),
|
|
49
54
|
output: trace.text,
|
|
50
55
|
isError: trace.isError,
|
package/src/compact.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChatPart, ModelRef } from "@guild/protocol";
|
|
2
2
|
import { llmComplete } from "./llm.ts";
|
|
3
|
+
import type { ToolProgress } from "./tools.ts";
|
|
3
4
|
|
|
4
5
|
/** Cheap char/4 estimate, same ballpark Codex uses before a real tokenizer. */
|
|
5
6
|
export const CHARS_PER_TOKEN = 4;
|
|
@@ -251,6 +252,8 @@ async function summarizeOld(input: {
|
|
|
251
252
|
dataDir: string;
|
|
252
253
|
env?: NodeJS.ProcessEnv;
|
|
253
254
|
prefer?: ModelRef | null;
|
|
255
|
+
onProgress?: (update: ToolProgress) => void;
|
|
256
|
+
signal?: AbortSignal;
|
|
254
257
|
}): Promise<string> {
|
|
255
258
|
const transcript = input.old
|
|
256
259
|
.map((item) => {
|
|
@@ -275,6 +278,14 @@ async function summarizeOld(input: {
|
|
|
275
278
|
prefer: input.prefer,
|
|
276
279
|
tools: false,
|
|
277
280
|
temperature: 0.1,
|
|
281
|
+
toolCtx: {
|
|
282
|
+
dataDir: input.dataDir,
|
|
283
|
+
env: input.env,
|
|
284
|
+
spawnDepth: 0,
|
|
285
|
+
allowWrite: false,
|
|
286
|
+
onProgress: input.onProgress,
|
|
287
|
+
signal: input.signal,
|
|
288
|
+
},
|
|
278
289
|
system:
|
|
279
290
|
"You compact a conversation so work can continue. Output only the summary.",
|
|
280
291
|
messages: [
|
|
@@ -305,6 +316,8 @@ export async function packHistory(input: {
|
|
|
305
316
|
prefer?: ModelRef | null;
|
|
306
317
|
checkpoint?: CompactCheckpoint | null;
|
|
307
318
|
tokenLimit?: number;
|
|
319
|
+
onProgress?: (update: ToolProgress) => void;
|
|
320
|
+
signal?: AbortSignal;
|
|
308
321
|
}): Promise<PackedHistory> {
|
|
309
322
|
const user = { role: "user" as const, content: input.userMessage };
|
|
310
323
|
const history = input.history.map(clipHistoryItem);
|
|
@@ -328,12 +341,26 @@ export async function packHistory(input: {
|
|
|
328
341
|
summary = input.checkpoint!.summary;
|
|
329
342
|
checkpoint = input.checkpoint!;
|
|
330
343
|
} else {
|
|
344
|
+
input.onProgress?.({
|
|
345
|
+
thinking: "整理上文…",
|
|
346
|
+
traces: [
|
|
347
|
+
{
|
|
348
|
+
name: "context",
|
|
349
|
+
args: {},
|
|
350
|
+
text: "",
|
|
351
|
+
isError: false,
|
|
352
|
+
running: true,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
});
|
|
331
356
|
summary = await summarizeOld({
|
|
332
357
|
old: plan.old,
|
|
333
358
|
previous: input.checkpoint?.summary,
|
|
334
359
|
dataDir: input.dataDir,
|
|
335
360
|
env: input.env,
|
|
336
361
|
prefer: input.prefer,
|
|
362
|
+
onProgress: input.onProgress,
|
|
363
|
+
signal: input.signal,
|
|
337
364
|
});
|
|
338
365
|
checkpoint = {
|
|
339
366
|
throughId: lastId(plan.old),
|
package/src/db.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
closeSync,
|
|
2
4
|
existsSync,
|
|
3
5
|
mkdirSync,
|
|
6
|
+
openSync,
|
|
4
7
|
readdirSync,
|
|
5
8
|
readFileSync,
|
|
9
|
+
readSync,
|
|
6
10
|
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync,
|
|
7
13
|
} from "node:fs";
|
|
8
14
|
import { dirname, join, resolve } from "node:path";
|
|
9
15
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -17,7 +23,10 @@ import type {
|
|
|
17
23
|
import type { TrajectoryDraft, TrajectoryEvent } from "./trajectory.ts";
|
|
18
24
|
|
|
19
25
|
export const GUILD_DB_FILE = "guild.sqlite";
|
|
20
|
-
|
|
26
|
+
/** Per-room hot window in SQLite. Older rows spill to rooms/<id>/trajectory.jsonl. */
|
|
27
|
+
export const TRAJECTORY_HOT_CAP = 1000;
|
|
28
|
+
const SCHEMA_VERSION = "2";
|
|
29
|
+
const WAREHOUSE_TAIL = 1024 * 1024;
|
|
21
30
|
|
|
22
31
|
const SCHEMA = `
|
|
23
32
|
PRAGMA journal_mode = WAL;
|
|
@@ -34,7 +43,9 @@ CREATE TABLE IF NOT EXISTS rooms (
|
|
|
34
43
|
kind TEXT NOT NULL CHECK (kind IN ('channel', 'dm')),
|
|
35
44
|
name TEXT NOT NULL,
|
|
36
45
|
member_ids TEXT NOT NULL DEFAULT '[]',
|
|
37
|
-
created_at TEXT NOT NULL
|
|
46
|
+
created_at TEXT NOT NULL,
|
|
47
|
+
parent_id TEXT,
|
|
48
|
+
branch_from_id TEXT
|
|
38
49
|
);
|
|
39
50
|
|
|
40
51
|
CREATE TABLE IF NOT EXISTS messages (
|
|
@@ -51,6 +62,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
51
62
|
finished_at TEXT,
|
|
52
63
|
steer INTEGER NOT NULL DEFAULT 0,
|
|
53
64
|
steer_bot_id TEXT,
|
|
65
|
+
mentions TEXT,
|
|
54
66
|
UNIQUE (room_id, seq)
|
|
55
67
|
);
|
|
56
68
|
|
|
@@ -138,7 +150,11 @@ function parseJsonlTrajectory(raw: string): TrajectoryEvent[] {
|
|
|
138
150
|
if (!trimmed) continue;
|
|
139
151
|
try {
|
|
140
152
|
const parsed = JSON.parse(trimmed) as TrajectoryEvent;
|
|
141
|
-
if (parsed
|
|
153
|
+
if (!parsed || typeof parsed.kind !== "string") continue;
|
|
154
|
+
if (typeof parsed.seq !== "number" || !Number.isFinite(parsed.seq)) {
|
|
155
|
+
parsed.seq = out.length;
|
|
156
|
+
}
|
|
157
|
+
out.push(parsed);
|
|
142
158
|
} catch {
|
|
143
159
|
/* skip */
|
|
144
160
|
}
|
|
@@ -146,6 +162,51 @@ function parseJsonlTrajectory(raw: string): TrajectoryEvent[] {
|
|
|
146
162
|
return out;
|
|
147
163
|
}
|
|
148
164
|
|
|
165
|
+
function lastJsonlSeq(path: string): number {
|
|
166
|
+
if (!existsSync(path)) return -1;
|
|
167
|
+
const stat = statSync(path);
|
|
168
|
+
if (stat.size === 0) return -1;
|
|
169
|
+
const fd = openSync(path, "r");
|
|
170
|
+
try {
|
|
171
|
+
const size = Math.min(stat.size, WAREHOUSE_TAIL);
|
|
172
|
+
const buf = Buffer.alloc(size);
|
|
173
|
+
readSync(fd, buf, 0, size, stat.size - size);
|
|
174
|
+
const lines = buf.toString("utf8").split("\n");
|
|
175
|
+
const start = stat.size > size ? 1 : 0;
|
|
176
|
+
for (let i = lines.length - 1; i >= start; i--) {
|
|
177
|
+
const trimmed = lines[i]!.trim();
|
|
178
|
+
if (!trimmed) continue;
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(trimmed) as { seq?: unknown };
|
|
181
|
+
if (typeof parsed.seq === "number" && Number.isFinite(parsed.seq)) {
|
|
182
|
+
return parsed.seq;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
/* skip */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return -1;
|
|
189
|
+
} finally {
|
|
190
|
+
closeSync(fd);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function trajectoryValues(roomId: string, event: TrajectoryEvent) {
|
|
195
|
+
return [
|
|
196
|
+
roomId,
|
|
197
|
+
event.seq,
|
|
198
|
+
event.ts,
|
|
199
|
+
event.turnId,
|
|
200
|
+
event.botId ?? null,
|
|
201
|
+
event.kind,
|
|
202
|
+
event.summary,
|
|
203
|
+
event.payload === undefined ? null : JSON.stringify(event.payload),
|
|
204
|
+
event.result ?? null,
|
|
205
|
+
event.durationMs ?? null,
|
|
206
|
+
event.isError ? 1 : 0,
|
|
207
|
+
] as const;
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
function messageFromRow(row: Record<string, unknown>): ChatMessage {
|
|
150
211
|
const parts = parseJson<ChatPart[] | null>(row.parts, null);
|
|
151
212
|
const attachments = parseJson<ChatAttachment[] | null>(row.attachments, null);
|
|
@@ -167,12 +228,18 @@ function messageFromRow(row: Record<string, unknown>): ChatMessage {
|
|
|
167
228
|
if (asNumber(row.steer) === 1) message.steer = true;
|
|
168
229
|
const steerBotId = asString(row.steer_bot_id);
|
|
169
230
|
if (steerBotId) message.steerBotId = steerBotId;
|
|
231
|
+
if (typeof row.mentions === "string" && row.mentions) {
|
|
232
|
+
const mentions = parseJson<string[]>(row.mentions, []);
|
|
233
|
+
if (Array.isArray(mentions)) {
|
|
234
|
+
message.mentions = mentions.filter((id) => typeof id === "string");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
170
237
|
return message;
|
|
171
238
|
}
|
|
172
239
|
|
|
173
240
|
function roomFromRow(row: Record<string, unknown>): Room {
|
|
174
241
|
const memberIds = parseJson<string[]>(row.member_ids, []);
|
|
175
|
-
|
|
242
|
+
const room: Room = {
|
|
176
243
|
id: asString(row.id),
|
|
177
244
|
kind: asString(row.kind) === "dm" ? "dm" : "channel",
|
|
178
245
|
name: asString(row.name),
|
|
@@ -181,6 +248,11 @@ function roomFromRow(row: Record<string, unknown>): Room {
|
|
|
181
248
|
: [],
|
|
182
249
|
createdAt: asString(row.created_at),
|
|
183
250
|
};
|
|
251
|
+
const parentId = asString(row.parent_id);
|
|
252
|
+
if (parentId) room.parentId = parentId;
|
|
253
|
+
const branchFromId = asString(row.branch_from_id);
|
|
254
|
+
if (branchFromId) room.branchFromId = branchFromId;
|
|
255
|
+
return room;
|
|
184
256
|
}
|
|
185
257
|
|
|
186
258
|
function trajectoryFromRow(row: Record<string, unknown>): TrajectoryEvent {
|
|
@@ -215,8 +287,23 @@ export class GuildDb {
|
|
|
215
287
|
} catch {
|
|
216
288
|
/* column already exists on fresh schema */
|
|
217
289
|
}
|
|
290
|
+
try {
|
|
291
|
+
this.sqlite.exec("ALTER TABLE messages ADD COLUMN mentions TEXT");
|
|
292
|
+
} catch {
|
|
293
|
+
/* column already exists on fresh schema */
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
this.sqlite.exec("ALTER TABLE rooms ADD COLUMN parent_id TEXT");
|
|
297
|
+
} catch {
|
|
298
|
+
/* column already exists on fresh schema */
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
this.sqlite.exec("ALTER TABLE rooms ADD COLUMN branch_from_id TEXT");
|
|
302
|
+
} catch {
|
|
303
|
+
/* column already exists on fresh schema */
|
|
304
|
+
}
|
|
218
305
|
this.sqlite.prepare(
|
|
219
|
-
"INSERT
|
|
306
|
+
"INSERT INTO meta (key, value) VALUES ('schema', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
220
307
|
).run(SCHEMA_VERSION);
|
|
221
308
|
}
|
|
222
309
|
|
|
@@ -226,23 +313,27 @@ export class GuildDb {
|
|
|
226
313
|
|
|
227
314
|
importLegacyFiles(dataDir: string): void {
|
|
228
315
|
const root = join(dataDir, "rooms");
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
316
|
+
if (existsSync(root)) {
|
|
317
|
+
const ids = readdirSync(root, { withFileTypes: true })
|
|
318
|
+
.filter((entry) => entry.isDirectory())
|
|
319
|
+
.map((entry) => entry.name);
|
|
320
|
+
for (const id of ids) this.importRoomDir(join(root, id), id);
|
|
321
|
+
}
|
|
322
|
+
for (const room of this.listRooms()) this.spillColdTrajectory(room.id);
|
|
234
323
|
}
|
|
235
324
|
|
|
236
325
|
upsertRoom(room: Room): void {
|
|
237
326
|
this.sqlite
|
|
238
327
|
.prepare(
|
|
239
|
-
`INSERT INTO rooms (id, kind, name, member_ids, created_at)
|
|
240
|
-
VALUES (?, ?, ?, ?, ?)
|
|
328
|
+
`INSERT INTO rooms (id, kind, name, member_ids, created_at, parent_id, branch_from_id)
|
|
329
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
241
330
|
ON CONFLICT(id) DO UPDATE SET
|
|
242
331
|
kind = excluded.kind,
|
|
243
332
|
name = excluded.name,
|
|
244
333
|
member_ids = excluded.member_ids,
|
|
245
|
-
created_at = excluded.created_at
|
|
334
|
+
created_at = excluded.created_at,
|
|
335
|
+
parent_id = excluded.parent_id,
|
|
336
|
+
branch_from_id = excluded.branch_from_id`,
|
|
246
337
|
)
|
|
247
338
|
.run(
|
|
248
339
|
room.id,
|
|
@@ -250,6 +341,8 @@ export class GuildDb {
|
|
|
250
341
|
room.name,
|
|
251
342
|
JSON.stringify(room.memberIds),
|
|
252
343
|
room.createdAt,
|
|
344
|
+
room.parentId ?? null,
|
|
345
|
+
room.branchFromId ?? null,
|
|
253
346
|
);
|
|
254
347
|
}
|
|
255
348
|
|
|
@@ -304,10 +397,22 @@ export class GuildDb {
|
|
|
304
397
|
}
|
|
305
398
|
}
|
|
306
399
|
|
|
307
|
-
updateMessageBody(
|
|
400
|
+
updateMessageBody(
|
|
401
|
+
roomId: string,
|
|
402
|
+
messageId: string,
|
|
403
|
+
body: string,
|
|
404
|
+
mentions?: string[],
|
|
405
|
+
): ChatMessage | null {
|
|
308
406
|
const result = this.sqlite
|
|
309
|
-
.prepare(
|
|
310
|
-
|
|
407
|
+
.prepare(
|
|
408
|
+
"UPDATE messages SET body = ?, mentions = ? WHERE room_id = ? AND id = ?",
|
|
409
|
+
)
|
|
410
|
+
.run(
|
|
411
|
+
body,
|
|
412
|
+
mentions ? JSON.stringify(mentions) : null,
|
|
413
|
+
roomId,
|
|
414
|
+
messageId,
|
|
415
|
+
);
|
|
311
416
|
if (!result.changes) return null;
|
|
312
417
|
return this.getMessage(roomId, messageId);
|
|
313
418
|
}
|
|
@@ -321,12 +426,13 @@ export class GuildDb {
|
|
|
321
426
|
usage?: ChatUsage;
|
|
322
427
|
createdAt: string;
|
|
323
428
|
finishedAt: string;
|
|
429
|
+
mentions?: string[];
|
|
324
430
|
},
|
|
325
431
|
): ChatMessage | null {
|
|
326
432
|
const result = this.sqlite
|
|
327
433
|
.prepare(
|
|
328
434
|
`UPDATE messages
|
|
329
|
-
SET body = ?, parts = ?, usage = ?, created_at = ?, finished_at = ?
|
|
435
|
+
SET body = ?, parts = ?, usage = ?, created_at = ?, finished_at = ?, mentions = ?
|
|
330
436
|
WHERE room_id = ? AND id = ?`,
|
|
331
437
|
)
|
|
332
438
|
.run(
|
|
@@ -335,6 +441,7 @@ export class GuildDb {
|
|
|
335
441
|
patch.usage ? JSON.stringify(patch.usage) : null,
|
|
336
442
|
patch.createdAt,
|
|
337
443
|
patch.finishedAt,
|
|
444
|
+
patch.mentions ? JSON.stringify(patch.mentions) : null,
|
|
338
445
|
roomId,
|
|
339
446
|
messageId,
|
|
340
447
|
);
|
|
@@ -348,6 +455,7 @@ export class GuildDb {
|
|
|
348
455
|
this.sqlite
|
|
349
456
|
.prepare("DELETE FROM messages WHERE room_id = ? AND id = ?")
|
|
350
457
|
.run(roomId, messageId);
|
|
458
|
+
/* Hot window only. Warehouse jsonl stays append-only. */
|
|
351
459
|
this.sqlite
|
|
352
460
|
.prepare("DELETE FROM trajectory WHERE room_id = ? AND turn_id = ?")
|
|
353
461
|
.run(roomId, messageId);
|
|
@@ -385,19 +493,7 @@ export class GuildDb {
|
|
|
385
493
|
for (const draft of drafts) {
|
|
386
494
|
seq += 1;
|
|
387
495
|
const event: TrajectoryEvent = { ...draft, seq };
|
|
388
|
-
insert.run(
|
|
389
|
-
roomId,
|
|
390
|
-
event.seq,
|
|
391
|
-
event.ts,
|
|
392
|
-
event.turnId,
|
|
393
|
-
event.botId ?? null,
|
|
394
|
-
event.kind,
|
|
395
|
-
event.summary,
|
|
396
|
-
event.payload === undefined ? null : JSON.stringify(event.payload),
|
|
397
|
-
event.result ?? null,
|
|
398
|
-
event.durationMs ?? null,
|
|
399
|
-
event.isError ? 1 : 0,
|
|
400
|
-
);
|
|
496
|
+
insert.run(...trajectoryValues(roomId, event));
|
|
401
497
|
written.push(event);
|
|
402
498
|
}
|
|
403
499
|
this.sqlite.exec("COMMIT");
|
|
@@ -462,7 +558,39 @@ export class GuildDb {
|
|
|
462
558
|
const row = this.sqlite
|
|
463
559
|
.prepare("SELECT COALESCE(MAX(seq), -1) AS seq FROM trajectory WHERE room_id = ?")
|
|
464
560
|
.get(roomId) as { seq?: number } | undefined;
|
|
465
|
-
return asNumber(row?.seq, -1);
|
|
561
|
+
return Math.max(asNumber(row?.seq, -1), lastJsonlSeq(this.warehousePath(roomId)));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private warehousePath(roomId: string): string {
|
|
565
|
+
return join(dirname(this.path), "rooms", roomId, "trajectory.jsonl");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
spillColdTrajectory(roomId: string): void {
|
|
569
|
+
const extra = this.trajectoryCount(roomId) - TRAJECTORY_HOT_CAP;
|
|
570
|
+
if (extra <= 0) return;
|
|
571
|
+
const rows = this.sqlite
|
|
572
|
+
.prepare(
|
|
573
|
+
"SELECT * FROM trajectory WHERE room_id = ? ORDER BY seq ASC LIMIT ?",
|
|
574
|
+
)
|
|
575
|
+
.all(roomId, extra) as Record<string, unknown>[];
|
|
576
|
+
if (!rows.length) return;
|
|
577
|
+
const events = rows.map(trajectoryFromRow);
|
|
578
|
+
const path = this.warehousePath(roomId);
|
|
579
|
+
const lastArchived = lastJsonlSeq(path);
|
|
580
|
+
const fresh = events.filter((event) => event.seq > lastArchived);
|
|
581
|
+
if (fresh.length) {
|
|
582
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
583
|
+
appendFileSync(
|
|
584
|
+
path,
|
|
585
|
+
fresh.map((event) => `${JSON.stringify(event)}\n`).join(""),
|
|
586
|
+
"utf8",
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const lastSeq = events[events.length - 1]?.seq;
|
|
590
|
+
if (typeof lastSeq !== "number") return;
|
|
591
|
+
this.sqlite
|
|
592
|
+
.prepare("DELETE FROM trajectory WHERE room_id = ? AND seq <= ?")
|
|
593
|
+
.run(roomId, lastSeq);
|
|
466
594
|
}
|
|
467
595
|
|
|
468
596
|
private insertMessage(message: ChatMessage, seq: number): void {
|
|
@@ -470,8 +598,8 @@ export class GuildDb {
|
|
|
470
598
|
.prepare(
|
|
471
599
|
`INSERT INTO messages (
|
|
472
600
|
id, room_id, seq, author, body, parts, reply_to, attachments, usage,
|
|
473
|
-
created_at, finished_at, steer, steer_bot_id
|
|
474
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
601
|
+
created_at, finished_at, steer, steer_bot_id, mentions
|
|
602
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
475
603
|
)
|
|
476
604
|
.run(
|
|
477
605
|
message.id,
|
|
@@ -487,6 +615,7 @@ export class GuildDb {
|
|
|
487
615
|
message.finishedAt ?? null,
|
|
488
616
|
message.steer ? 1 : 0,
|
|
489
617
|
message.steerBotId ?? null,
|
|
618
|
+
message.mentions ? JSON.stringify(message.mentions) : null,
|
|
490
619
|
);
|
|
491
620
|
}
|
|
492
621
|
|
|
@@ -507,6 +636,12 @@ export class GuildDb {
|
|
|
507
636
|
typeof parsed.createdAt === "string"
|
|
508
637
|
? parsed.createdAt
|
|
509
638
|
: "2026-01-01T00:00:00.000Z",
|
|
639
|
+
...(typeof parsed.parentId === "string" && parsed.parentId
|
|
640
|
+
? { parentId: parsed.parentId }
|
|
641
|
+
: {}),
|
|
642
|
+
...(typeof parsed.branchFromId === "string" && parsed.branchFromId
|
|
643
|
+
? { branchFromId: parsed.branchFromId }
|
|
644
|
+
: {}),
|
|
510
645
|
});
|
|
511
646
|
}
|
|
512
647
|
} catch {
|
|
@@ -536,34 +671,20 @@ export class GuildDb {
|
|
|
536
671
|
const path = join(dir, "trajectory.jsonl");
|
|
537
672
|
if (existsSync(path)) {
|
|
538
673
|
const events = parseJsonlTrajectory(readFileSync(path, "utf8"));
|
|
539
|
-
if (events.length) {
|
|
540
|
-
this.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
event.botId ?? null,
|
|
554
|
-
event.kind,
|
|
555
|
-
event.summary,
|
|
556
|
-
event.payload === undefined ? null : JSON.stringify(event.payload),
|
|
557
|
-
event.result ?? null,
|
|
558
|
-
event.durationMs ?? null,
|
|
559
|
-
event.isError ? 1 : 0,
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
this.sqlite.exec("COMMIT");
|
|
563
|
-
} catch (error) {
|
|
564
|
-
this.sqlite.exec("ROLLBACK");
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
674
|
+
if (events.length <= TRAJECTORY_HOT_CAP) {
|
|
675
|
+
if (events.length) this.insertTrajectoryRows(id, events);
|
|
676
|
+
rmIfExists(path);
|
|
677
|
+
} else {
|
|
678
|
+
const cut = events.length - TRAJECTORY_HOT_CAP;
|
|
679
|
+
this.insertTrajectoryRows(id, events.slice(cut));
|
|
680
|
+
writeFileSync(
|
|
681
|
+
path,
|
|
682
|
+
events
|
|
683
|
+
.slice(0, cut)
|
|
684
|
+
.map((event) => `${JSON.stringify(event)}\n`)
|
|
685
|
+
.join(""),
|
|
686
|
+
"utf8",
|
|
687
|
+
);
|
|
567
688
|
}
|
|
568
689
|
}
|
|
569
690
|
}
|
|
@@ -597,12 +718,26 @@ export class GuildDb {
|
|
|
597
718
|
rmIfExists(join(dir, "messages.json"));
|
|
598
719
|
rmIfExists(join(dir, "messages.jsonl"));
|
|
599
720
|
}
|
|
600
|
-
if (this.trajectoryCount(id) > 0) {
|
|
601
|
-
rmIfExists(join(dir, "trajectory.jsonl"));
|
|
602
|
-
}
|
|
603
721
|
if (this.readCompact(id)) rmIfExists(join(dir, "compact.json"));
|
|
604
722
|
}
|
|
605
723
|
|
|
724
|
+
private insertTrajectoryRows(roomId: string, events: TrajectoryEvent[]): void {
|
|
725
|
+
if (!events.length) return;
|
|
726
|
+
this.sqlite.exec("BEGIN");
|
|
727
|
+
try {
|
|
728
|
+
const insert = this.sqlite.prepare(
|
|
729
|
+
`INSERT INTO trajectory (
|
|
730
|
+
room_id, seq, ts, turn_id, bot_id, kind, summary, payload, result, duration_ms, is_error
|
|
731
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
732
|
+
);
|
|
733
|
+
for (const event of events) insert.run(...trajectoryValues(roomId, event));
|
|
734
|
+
this.sqlite.exec("COMMIT");
|
|
735
|
+
} catch (error) {
|
|
736
|
+
this.sqlite.exec("ROLLBACK");
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
606
741
|
private messageCount(roomId: string): number {
|
|
607
742
|
const row = this.sqlite
|
|
608
743
|
.prepare("SELECT COUNT(*) AS n FROM messages WHERE room_id = ?")
|
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. A markdown numbered list that names a teammate (1. @design) also starts them, even if the handle is wrapped in backticks. Mentions that are only commentary in 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
|
+
You may @handle any staffed teammate whose job is the next step, even if the human only named you this turn. That is how the hall continues. Do not dump the same work on every seat. If two seats must run in order, only @ the seat that can start now — a numbered list that names later seats starts them this turn too. Do not write a plan and stop.
|
|
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: you coordinate this seat. Spawn first when the work is a repo survey (\`explorer\` / luna-explore), a critique (\`reviewer\`), or a bounded isolated patch (\`worker\` / luna-general); then verify the child's evidence and decide. Independent surveys: spawn background=true, keep working, then read_spawn before you answer. Sequential: background=false and wait. Do not spawn for one known file, a one-line change, or a question that needs no repo. Do not let children commit, push, or make the architecture call. Do not skip spawn just because you can do the work yourself. 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,29 +397,57 @@ 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
|
-
|
|
400
|
+
const spawnCatalog: Array<{
|
|
401
|
+
slug?: string;
|
|
402
|
+
name: string;
|
|
403
|
+
description?: string;
|
|
404
|
+
readOnly?: boolean;
|
|
405
|
+
}> = subagents.length
|
|
406
|
+
? subagents
|
|
407
|
+
: [
|
|
408
|
+
{
|
|
409
|
+
slug: "explorer",
|
|
410
|
+
name: "explorer",
|
|
411
|
+
description:
|
|
412
|
+
"Read-only codebase search. Returns absolute paths and a direct answer.",
|
|
413
|
+
readOnly: true,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
slug: "reviewer",
|
|
417
|
+
name: "reviewer",
|
|
418
|
+
description: "Read-only review of correctness, risk, and missing tests.",
|
|
419
|
+
readOnly: true,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
slug: "worker",
|
|
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");
|
|
423
451
|
const channel = (input.channelMd ?? "").trim();
|
|
424
452
|
const channelBlock = channel
|
|
425
453
|
? `# 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)}`
|
|
@@ -515,7 +543,7 @@ export function localChatReply(
|
|
|
515
543
|
userMessage: string,
|
|
516
544
|
): string {
|
|
517
545
|
const clip = userMessage.trim().slice(0, 120);
|
|
518
|
-
return `【${botName} @${handle}】收到。「${clip}」\n\n
|
|
546
|
+
return `【${botName} @${handle}】收到。「${clip}」\n\n沒有可用模型,本機工具還沒辦法跑。到模型頁(/settings)連接訂閱或填 API key,套用主模型後再問。`;
|
|
519
547
|
}
|
|
520
548
|
|
|
521
549
|
async function tryChatLlm(
|
|
@@ -569,6 +597,8 @@ async function tryChatLlm(
|
|
|
569
597
|
env,
|
|
570
598
|
prefer,
|
|
571
599
|
checkpoint: input.compact,
|
|
600
|
+
onProgress: input.onProgress,
|
|
601
|
+
signal: input.signal,
|
|
572
602
|
});
|
|
573
603
|
if (packed.compacted && packed.checkpoint && input.onCompact) {
|
|
574
604
|
input.onCompact(packed.checkpoint);
|
|
@@ -594,6 +624,7 @@ async function tryChatLlm(
|
|
|
594
624
|
pullSteers: input.pullSteers,
|
|
595
625
|
signal: input.signal,
|
|
596
626
|
mcpTools: input.mcpTools ?? (await listMcpToolRefs(dataDir)),
|
|
627
|
+
spawnHandles: new Map(),
|
|
597
628
|
...policyFor(env, {
|
|
598
629
|
sandbox: input.sandbox,
|
|
599
630
|
workspace: input.workspace,
|