@goodfish/jarvy 0.2.0 → 0.4.0
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/CHANGELOG.md +44 -0
- package/README.md +5 -3
- package/README.zh-CN.md +6 -2
- package/dist/server/cli.js +803 -290
- package/dist/web/assets/figure-sTOSJn2I.js +1 -0
- package/dist/web/assets/index-DL8kubrV.js +534 -0
- package/dist/web/assets/index-Dpj-oCnv.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/web/assets/figure-BOkXKeYO.js +0 -1
- package/dist/web/assets/index-BYOxf981.js +0 -420
- package/dist/web/assets/index-CFv3-Mdq.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createServer } from "node:http";
|
|
|
11
11
|
import { getRequestListener } from "@hono/node-server";
|
|
12
12
|
|
|
13
13
|
// src/server/api/app.ts
|
|
14
|
-
import { Hono as
|
|
14
|
+
import { Hono as Hono10 } from "hono";
|
|
15
15
|
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
16
16
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
17
17
|
|
|
@@ -72,28 +72,41 @@ import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
|
72
72
|
|
|
73
73
|
// src/server/shared/runtime.ts
|
|
74
74
|
var RuntimeFault = class extends Error {
|
|
75
|
-
constructor(code, message, status = 409) {
|
|
75
|
+
constructor(code, message, status = 409, params) {
|
|
76
76
|
super(message);
|
|
77
77
|
this.code = code;
|
|
78
78
|
this.status = status;
|
|
79
|
+
this.params = params;
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
82
|
function publicFault(error) {
|
|
82
|
-
return error instanceof RuntimeFault ? error : new RuntimeFault("runtime_failed", "
|
|
83
|
+
return error instanceof RuntimeFault ? error : new RuntimeFault("runtime_failed", "The run failed. Check the model setup or the run records.", 500);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// src/server/shared/chat.ts
|
|
86
87
|
var ChatError = class extends Error {
|
|
87
|
-
constructor(status, message) {
|
|
88
|
+
constructor(status, message, code, params) {
|
|
88
89
|
super(message);
|
|
89
90
|
this.status = status;
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.params = params;
|
|
90
93
|
}
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
// src/server/shared/errors.ts
|
|
94
97
|
var ValidationError = class extends Error {
|
|
98
|
+
constructor(message, code, params) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.code = code;
|
|
101
|
+
this.params = params;
|
|
102
|
+
}
|
|
95
103
|
};
|
|
96
104
|
var ConflictError = class extends Error {
|
|
105
|
+
constructor(message, code, params) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.code = code;
|
|
108
|
+
this.params = params;
|
|
109
|
+
}
|
|
97
110
|
};
|
|
98
111
|
|
|
99
112
|
// src/server/api/middleware/errors.ts
|
|
@@ -103,7 +116,9 @@ var handleError = (error, c) => {
|
|
|
103
116
|
else if (error instanceof ValidationError || error instanceof TypeError && "code" in error && error.code === "ERR_INVALID_URL") status = 400;
|
|
104
117
|
else if (error instanceof ConflictError) status = 409;
|
|
105
118
|
if (status === 500) console.error("[api] request failed", { path: c.req.path, code: error instanceof RuntimeFault ? error.code : "internal" });
|
|
106
|
-
return c.json({ error:
|
|
119
|
+
if (status === 500) return c.json({ error: "Internal server error" }, 500);
|
|
120
|
+
const coded = error instanceof RuntimeFault || error instanceof ChatError || error instanceof ValidationError || error instanceof ConflictError ? error : null;
|
|
121
|
+
return c.json({ error: error.message, ...coded?.code ? { code: coded.code } : {}, ...coded?.params ? { params: coded.params } : {} }, status);
|
|
107
122
|
};
|
|
108
123
|
|
|
109
124
|
// src/server/api/routes/account.ts
|
|
@@ -181,6 +196,7 @@ function botStatusView(status) {
|
|
|
181
196
|
runId: status.runId,
|
|
182
197
|
errorCode: status.error?.code ?? null,
|
|
183
198
|
error: status.error?.message ?? null,
|
|
199
|
+
...status.error?.params ? { errorParams: status.error.params } : {},
|
|
184
200
|
...status.context ? { context: { tokens: status.context.tokens, contextWindow: status.context.limit, percent: status.context.tokens === null ? null : status.context.tokens / status.context.limit * 100 } } : {}
|
|
185
201
|
};
|
|
186
202
|
}
|
|
@@ -189,6 +205,43 @@ function botStatusView(status) {
|
|
|
189
205
|
import { Hono as Hono2 } from "hono";
|
|
190
206
|
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
191
207
|
|
|
208
|
+
// src/server/shared/quotes.ts
|
|
209
|
+
var MARK = /\[\[q(\d+)\]\]/g;
|
|
210
|
+
function quoteMarks(content2) {
|
|
211
|
+
return [...content2.matchAll(MARK)].map((match) => Number(match[1]));
|
|
212
|
+
}
|
|
213
|
+
function clipQuote(text3) {
|
|
214
|
+
const chars = [...text3];
|
|
215
|
+
return chars.length > 200 ? `${chars.slice(0, 120).join("")}\u2026\u2026${chars.slice(-60).join("")}` : text3;
|
|
216
|
+
}
|
|
217
|
+
function previewQuotes(content2, quotes) {
|
|
218
|
+
if (!quotes.length) return content2;
|
|
219
|
+
return content2.replace(MARK, (mark, n) => {
|
|
220
|
+
const quote = quotes[Number(n) - 1];
|
|
221
|
+
return quote ? `"${clipQuote(quote.text).replace(/\s+/g, " ").trim()}"` : mark;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function clipMarked(content2, length) {
|
|
225
|
+
return content2.length <= length ? content2 : content2.slice(0, length).replace(/\[(?:\[(?:q\d*\]?)?)?$/, "");
|
|
226
|
+
}
|
|
227
|
+
function replyExcerpt(reply) {
|
|
228
|
+
return reply.content ? previewQuotes(reply.content, reply.quotes).slice(0, 200) : "(attachment)";
|
|
229
|
+
}
|
|
230
|
+
function expandQuotes(content2, quotes, name2) {
|
|
231
|
+
if (!quotes.length) return content2;
|
|
232
|
+
return content2.replace(/[ \t]*\[\[q(\d+)\]\][ \t]*/g, (mark, n, offset, whole) => {
|
|
233
|
+
const quote = quotes[Number(n) - 1];
|
|
234
|
+
if (!quote) return mark;
|
|
235
|
+
const head = `${name2(quote.senderId)}${quote.messageId ? ` (${quote.messageId})` : ""}: `;
|
|
236
|
+
const block = `${head}${clipQuote(quote.text).trim()}`.split("\n").map((line) => `> ${line}`).join("\n");
|
|
237
|
+
const end = offset + mark.length;
|
|
238
|
+
return `${offset > 0 && whole[offset - 1] !== "\n" ? "\n" : ""}${block}${end < whole.length && whole[end] !== "\n" ? "\n" : ""}`;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function quoteComparable(text3) {
|
|
242
|
+
return text3.replace(MARK, "").replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<(https?:\/\/[^>\s]+)>/g, "$1").replace(/^[ \t|:*_=-]*$/gm, (line) => /[-*_=]{3}/.test(line) ? "" : line).replace(/^[ \t]*(?:>[ \t>]*|#{1,6}[ \t]+|(?:[-*+]|\d+[.)])[ \t]+|```.*|~~~.*)/gm, "").replace(/\\([\\`*_{}[\]()#+\-.!|~>])/g, "$1").replace(/[*_~`|]/g, "").replace(/\s+/g, "");
|
|
243
|
+
}
|
|
244
|
+
|
|
192
245
|
// src/server/api/views/chat.ts
|
|
193
246
|
function chatView(chat, member) {
|
|
194
247
|
return {
|
|
@@ -224,7 +277,7 @@ function threadView(entry, member) {
|
|
|
224
277
|
...chatView(entry.chat, member),
|
|
225
278
|
replies: entry.info.replies,
|
|
226
279
|
lastReplyAt: entry.info.lastReplyAt,
|
|
227
|
-
root: entry.root ? { senderId: entry.root.senderId, content: entry.root.content
|
|
280
|
+
root: entry.root ? { senderId: entry.root.senderId, content: clipMarked(entry.root.content, 300), quotes: entry.root.quotes } : null
|
|
228
281
|
};
|
|
229
282
|
}
|
|
230
283
|
function messageView(message) {
|
|
@@ -255,19 +308,19 @@ function cursor(raw) {
|
|
|
255
308
|
try {
|
|
256
309
|
value2 = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
257
310
|
} catch {
|
|
258
|
-
throw new HTTPException5(400, { message: "
|
|
311
|
+
throw new HTTPException5(400, { message: "Invalid page position" });
|
|
259
312
|
}
|
|
260
313
|
if (value2 && typeof value2 === "object" && "timestamp" in value2 && Number.isSafeInteger(value2.timestamp) && Number(value2.timestamp) >= 0 && "kind" in value2 && "id" in value2) {
|
|
261
314
|
if (value2.kind === "chat" && typeof value2.id === "string" && value2.id.length) return value2;
|
|
262
315
|
if (value2.kind === "history" && Number.isSafeInteger(value2.id) && Number(value2.id) > 0) return value2;
|
|
263
316
|
}
|
|
264
|
-
throw new HTTPException5(400, { message: "
|
|
317
|
+
throw new HTTPException5(400, { message: "Invalid page position" });
|
|
265
318
|
}
|
|
266
319
|
function activityRoutes(bots) {
|
|
267
320
|
const routes = new Hono2();
|
|
268
321
|
routes.get("/:id/activity", (c) => {
|
|
269
322
|
const limit = Number(c.req.query("limit") ?? 40);
|
|
270
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new HTTPException5(400, { message: "
|
|
323
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new HTTPException5(400, { message: "Invalid page size" });
|
|
271
324
|
const page = bots.activity(c.req.param("id"), cursor(c.req.query("before")), limit);
|
|
272
325
|
return c.json({ items: page.items, nextBefore: page.nextBefore ? Buffer.from(JSON.stringify(page.nextBefore)).toString("base64url") : null });
|
|
273
326
|
});
|
|
@@ -276,10 +329,10 @@ function activityRoutes(bots) {
|
|
|
276
329
|
return c.json({ chat: detail.chat, message: messageView(detail.message) });
|
|
277
330
|
});
|
|
278
331
|
routes.all("/:id/activity", () => {
|
|
279
|
-
throw new HTTPException5(405, { message: "
|
|
332
|
+
throw new HTTPException5(405, { message: "Activity is read-only" });
|
|
280
333
|
});
|
|
281
334
|
routes.all("/:id/activity/messages/:messageId", () => {
|
|
282
|
-
throw new HTTPException5(405, { message: "
|
|
335
|
+
throw new HTTPException5(405, { message: "Activity is read-only" });
|
|
283
336
|
});
|
|
284
337
|
return routes;
|
|
285
338
|
}
|
|
@@ -289,27 +342,33 @@ import { Hono as Hono3 } from "hono";
|
|
|
289
342
|
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
290
343
|
function integer(raw, label) {
|
|
291
344
|
const n = Number(raw);
|
|
292
|
-
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) throw new HTTPException6(400, { message:
|
|
345
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) throw new HTTPException6(400, { message: `Invalid ${label}` });
|
|
293
346
|
return n;
|
|
294
347
|
}
|
|
295
348
|
function historyRoutes(bots) {
|
|
296
349
|
const routes = new Hono3();
|
|
297
350
|
routes.get("/:id/history", (c) => {
|
|
298
351
|
const memory = bots.memory(c.req.param("id"));
|
|
299
|
-
const before = c.req.query("before"), limit = integer(c.req.query("limit") ?? "50", "
|
|
300
|
-
if (limit > 100) throw new HTTPException6(400, { message: "
|
|
301
|
-
return c.json(memory.list(before === void 0 ? void 0 : integer(before, "
|
|
352
|
+
const before = c.req.query("before"), limit = integer(c.req.query("limit") ?? "50", "page size");
|
|
353
|
+
if (limit > 100) throw new HTTPException6(400, { message: "The page size can't be over 100" });
|
|
354
|
+
return c.json(memory.list(before === void 0 ? void 0 : integer(before, "page position"), limit));
|
|
302
355
|
});
|
|
303
356
|
routes.get("/:id/history/:recordId{[0-9]+}", (c) => {
|
|
304
|
-
const detail = bots.memory(c.req.param("id")).read(integer(c.req.param("recordId"), "
|
|
305
|
-
if (!detail) throw new
|
|
357
|
+
const detail = bots.memory(c.req.param("id")).read(integer(c.req.param("recordId"), "record ID"));
|
|
358
|
+
if (!detail) throw new RuntimeFault("history_missing", "History record not found", 404);
|
|
306
359
|
return c.json(detail);
|
|
307
360
|
});
|
|
361
|
+
routes.get("/:id/usage", (c) => {
|
|
362
|
+
const raw = c.req.query("since") ?? "";
|
|
363
|
+
const since = Number(raw);
|
|
364
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(since)) throw new HTTPException6(400, { message: "Invalid start time" });
|
|
365
|
+
return c.json({ since, models: bots.memory(c.req.param("id")).usage(since) });
|
|
366
|
+
});
|
|
308
367
|
routes.all("/:id/history", () => {
|
|
309
|
-
throw new HTTPException6(405, { message: "History
|
|
368
|
+
throw new HTTPException6(405, { message: "History is read-only" });
|
|
310
369
|
});
|
|
311
370
|
routes.all("/:id/history/:recordId{[0-9]+}", () => {
|
|
312
|
-
throw new HTTPException6(405, { message: "History
|
|
371
|
+
throw new HTTPException6(405, { message: "History is read-only" });
|
|
313
372
|
});
|
|
314
373
|
return routes;
|
|
315
374
|
}
|
|
@@ -320,7 +379,7 @@ function botRoutes(bots) {
|
|
|
320
379
|
routes.get("/", (c) => c.json({ bots: bots.list() }));
|
|
321
380
|
routes.post("/", jsonBody, async (c) => {
|
|
322
381
|
const input = await readJson(c);
|
|
323
|
-
return c.json(bots.create(
|
|
382
|
+
return c.json(bots.create(textField(input, "name")), 201);
|
|
324
383
|
});
|
|
325
384
|
routes.get("/:id", (c) => c.json(bots.describe(c.req.param("id"))));
|
|
326
385
|
routes.delete("/:id", async (c) => {
|
|
@@ -373,6 +432,26 @@ function botRoutes(bots) {
|
|
|
373
432
|
// src/server/api/routes/chats.ts
|
|
374
433
|
import { Hono as Hono5 } from "hono";
|
|
375
434
|
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
435
|
+
|
|
436
|
+
// src/server/api/views/download.ts
|
|
437
|
+
var MEDIA_TYPES = /* @__PURE__ */ new Set(["video/webm", "video/mp4", "video/quicktime", "audio/mpeg", "audio/wav", "audio/x-wav", "audio/ogg", "audio/webm", "audio/mp4"]);
|
|
438
|
+
function downloadType(mimeType) {
|
|
439
|
+
const type = mimeType.split(";")[0].trim().toLowerCase();
|
|
440
|
+
return MEDIA_TYPES.has(type) ? type : "application/octet-stream";
|
|
441
|
+
}
|
|
442
|
+
function byteRange(header, size) {
|
|
443
|
+
const match = header?.trim().match(/^bytes=(\d*)-(\d*)$/);
|
|
444
|
+
if (!match || !match[1] && !match[2]) return null;
|
|
445
|
+
if (!match[1]) {
|
|
446
|
+
const suffix = Number(match[2]);
|
|
447
|
+
return suffix === 0 ? "unsatisfiable" : { start: Math.max(0, size - suffix), end: size - 1 };
|
|
448
|
+
}
|
|
449
|
+
const start = Number(match[1]), end = match[2] ? Number(match[2]) : Infinity;
|
|
450
|
+
if (end < start) return null;
|
|
451
|
+
return start >= size ? "unsatisfiable" : { start, end: Math.min(end, size - 1) };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/server/api/routes/chats.ts
|
|
376
455
|
function chatRoutes({ chats, bots }) {
|
|
377
456
|
const routes = new Hono5();
|
|
378
457
|
const view = (chat) => chatView(chat, (id) => chats.member(id));
|
|
@@ -425,11 +504,19 @@ function attachmentRoutes(chats) {
|
|
|
425
504
|
routes.get("/:id", (c) => {
|
|
426
505
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
427
506
|
if (!attachment) throw new HTTPException7(404, { message: "Attachment not found" });
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"Content-
|
|
507
|
+
const size = attachment.data.length, range = byteRange(c.req.header("Range"), size);
|
|
508
|
+
const headers2 = {
|
|
509
|
+
"Content-Type": downloadType(attachment.file.mimeType),
|
|
510
|
+
"Accept-Ranges": "bytes",
|
|
431
511
|
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(attachment.file.name)}`
|
|
432
|
-
}
|
|
512
|
+
};
|
|
513
|
+
if (range === "unsatisfiable") return c.body(null, 416, { ...headers2, "Content-Range": `bytes */${size}` });
|
|
514
|
+
if (!range) return c.body(new Uint8Array(attachment.data), 200, { ...headers2, "Content-Length": String(size) });
|
|
515
|
+
return c.body(
|
|
516
|
+
new Uint8Array(attachment.data.subarray(range.start, range.end + 1)),
|
|
517
|
+
206,
|
|
518
|
+
{ ...headers2, "Content-Length": String(range.end - range.start + 1), "Content-Range": `bytes ${range.start}-${range.end}/${size}` }
|
|
519
|
+
);
|
|
433
520
|
});
|
|
434
521
|
routes.get("/:id/run", (c) => {
|
|
435
522
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
@@ -626,12 +713,68 @@ function channelRoutes({ chats }) {
|
|
|
626
713
|
return routes;
|
|
627
714
|
}
|
|
628
715
|
|
|
716
|
+
// src/server/api/routes/live.ts
|
|
717
|
+
import { Hono as Hono9 } from "hono";
|
|
718
|
+
function liveRoutes({ account, bots }) {
|
|
719
|
+
const routes = new Hono9();
|
|
720
|
+
routes.get("/bots/:id/live", (c) => {
|
|
721
|
+
const token = c.get("sessionToken"), signal = c.req.raw.signal, live = bots.live(c.req.param("id"));
|
|
722
|
+
let disconnect;
|
|
723
|
+
const body = new ReadableStream({
|
|
724
|
+
start(controller) {
|
|
725
|
+
let ended = false;
|
|
726
|
+
const encoder = new TextEncoder();
|
|
727
|
+
const send = (event, data) => {
|
|
728
|
+
if (ended) return;
|
|
729
|
+
if ((controller.desiredSize ?? 0) <= 0) {
|
|
730
|
+
disconnect();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
controller.enqueue(encoder.encode(`event: ${event}
|
|
734
|
+
data: ${JSON.stringify(data)}
|
|
735
|
+
|
|
736
|
+
`));
|
|
737
|
+
};
|
|
738
|
+
const { turn, unsubscribe } = live.subscribe((update) => {
|
|
739
|
+
if (!account.authenticated(token)) {
|
|
740
|
+
disconnect();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
send(update.type, update);
|
|
744
|
+
}, () => disconnect());
|
|
745
|
+
const heartbeat = setInterval(() => {
|
|
746
|
+
if (!account.authenticated(token)) disconnect();
|
|
747
|
+
else send("heartbeat", {});
|
|
748
|
+
}, 15e3);
|
|
749
|
+
heartbeat.unref();
|
|
750
|
+
const aborted = () => disconnect();
|
|
751
|
+
disconnect = (close = true) => {
|
|
752
|
+
if (ended) return;
|
|
753
|
+
ended = true;
|
|
754
|
+
clearInterval(heartbeat);
|
|
755
|
+
unsubscribe();
|
|
756
|
+
signal.removeEventListener("abort", aborted);
|
|
757
|
+
if (close) controller.close();
|
|
758
|
+
};
|
|
759
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
760
|
+
if (signal.aborted) disconnect();
|
|
761
|
+
else send("snapshot", { turn, now: Date.now() });
|
|
762
|
+
},
|
|
763
|
+
cancel() {
|
|
764
|
+
disconnect(false);
|
|
765
|
+
}
|
|
766
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: c.env.outgoing.writableHighWaterMark }));
|
|
767
|
+
return c.body(body, 200, { "Content-Type": "text/event-stream", Connection: "keep-alive", "X-Accel-Buffering": "no" });
|
|
768
|
+
});
|
|
769
|
+
return routes;
|
|
770
|
+
}
|
|
771
|
+
|
|
629
772
|
// src/server/api/app.ts
|
|
630
773
|
function createHttpApp(services, webRoot) {
|
|
631
|
-
const app = new
|
|
774
|
+
const app = new Hono10();
|
|
632
775
|
app.use("*", headers);
|
|
633
776
|
app.onError(handleError);
|
|
634
|
-
const api = new
|
|
777
|
+
const api = new Hono10();
|
|
635
778
|
api.use("*", apiSecurity);
|
|
636
779
|
installAuthentication(api, services.account);
|
|
637
780
|
api.route("/bots", botRoutes(services.bots));
|
|
@@ -640,6 +783,7 @@ function createHttpApp(services, webRoot) {
|
|
|
640
783
|
api.route("/attachments", attachmentRoutes(services.chats));
|
|
641
784
|
api.route("/", modelRoutes(services.models));
|
|
642
785
|
api.route("/", eventRoutes(services));
|
|
786
|
+
api.route("/", liveRoutes(services));
|
|
643
787
|
api.all("*", () => {
|
|
644
788
|
throw new HTTPException9(404, { message: "API route not found" });
|
|
645
789
|
});
|
|
@@ -924,6 +1068,21 @@ function migrateQuotes(db) {
|
|
|
924
1068
|
}
|
|
925
1069
|
}
|
|
926
1070
|
|
|
1071
|
+
// src/server/infra/storage/migrations/title-schema.ts
|
|
1072
|
+
function migrateTitles(db) {
|
|
1073
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1074
|
+
if (version >= 18) return;
|
|
1075
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1076
|
+
try {
|
|
1077
|
+
db.exec(`UPDATE chats SET title='' WHERE type='dm' AND title='\u65B0\u5BF9\u8BDD' AND NOT EXISTS(SELECT 1 FROM messages WHERE chat_id=chats.id);
|
|
1078
|
+
PRAGMA user_version=18;`);
|
|
1079
|
+
db.exec("COMMIT");
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
db.exec("ROLLBACK");
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
927
1086
|
// src/server/infra/storage/migrations/schema.ts
|
|
928
1087
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
929
1088
|
|
|
@@ -1047,7 +1206,7 @@ function migrateRuntime(db) {
|
|
|
1047
1206
|
// src/server/infra/storage/migrations/schema.ts
|
|
1048
1207
|
function migrateDatabase(db) {
|
|
1049
1208
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1050
|
-
if (version >
|
|
1209
|
+
if (version > 18) throw new Error(`Unsupported database version: ${version}`);
|
|
1051
1210
|
migratePrototype(db);
|
|
1052
1211
|
if (version >= 5) {
|
|
1053
1212
|
migrateRuntime(db);
|
|
@@ -1062,6 +1221,7 @@ function migrateDatabase(db) {
|
|
|
1062
1221
|
migrateInteractions(db);
|
|
1063
1222
|
migrateInbox(db);
|
|
1064
1223
|
migrateQuotes(db);
|
|
1224
|
+
migrateTitles(db);
|
|
1065
1225
|
return;
|
|
1066
1226
|
}
|
|
1067
1227
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1134,6 +1294,7 @@ function migrateDatabase(db) {
|
|
|
1134
1294
|
migrateInteractions(db);
|
|
1135
1295
|
migrateInbox(db);
|
|
1136
1296
|
migrateQuotes(db);
|
|
1297
|
+
migrateTitles(db);
|
|
1137
1298
|
}
|
|
1138
1299
|
|
|
1139
1300
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1323,40 +1484,7 @@ var ChannelRepository = class {
|
|
|
1323
1484
|
|
|
1324
1485
|
// src/server/infra/storage/thread-repository.ts
|
|
1325
1486
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1326
|
-
|
|
1327
|
-
// src/server/shared/quotes.ts
|
|
1328
|
-
var MARK = /\[\[q(\d+)\]\]/g;
|
|
1329
|
-
function quoteMarks(content2) {
|
|
1330
|
-
return [...content2.matchAll(MARK)].map((match) => Number(match[1]));
|
|
1331
|
-
}
|
|
1332
|
-
function clipQuote(text3) {
|
|
1333
|
-
const chars = [...text3];
|
|
1334
|
-
return chars.length > 200 ? `${chars.slice(0, 120).join("")}\u2026\u2026${chars.slice(-60).join("")}` : text3;
|
|
1335
|
-
}
|
|
1336
|
-
function previewQuotes(content2, quotes) {
|
|
1337
|
-
if (!quotes.length) return content2;
|
|
1338
|
-
return content2.replace(MARK, (mark, n) => {
|
|
1339
|
-
const quote = quotes[Number(n) - 1];
|
|
1340
|
-
return quote ? `\u300C${clipQuote(quote.text).replace(/\s+/g, " ").trim()}\u300D` : mark;
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
function expandQuotes(content2, quotes, name2) {
|
|
1344
|
-
if (!quotes.length) return content2;
|
|
1345
|
-
return content2.replace(/[ \t]*\[\[q(\d+)\]\][ \t]*/g, (mark, n, offset, whole) => {
|
|
1346
|
-
const quote = quotes[Number(n) - 1];
|
|
1347
|
-
if (!quote) return mark;
|
|
1348
|
-
const head = `${name2(quote.senderId)}${quote.messageId ? `\uFF08${quote.messageId}\uFF09` : ""}\uFF1A`;
|
|
1349
|
-
const block = `${head}${clipQuote(quote.text).trim()}`.split("\n").map((line) => `> ${line}`).join("\n");
|
|
1350
|
-
const end = offset + mark.length;
|
|
1351
|
-
return `${offset > 0 && whole[offset - 1] !== "\n" ? "\n" : ""}${block}${end < whole.length && whole[end] !== "\n" ? "\n" : ""}`;
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
function quoteComparable(text3) {
|
|
1355
|
-
return text3.replace(MARK, "").replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<(https?:\/\/[^>\s]+)>/g, "$1").replace(/^[ \t|:*_=-]*$/gm, (line) => /[-*_=]{3}/.test(line) ? "" : line).replace(/^[ \t]*(?:>[ \t>]*|#{1,6}[ \t]+|(?:[-*+]|\d+[.)])[ \t]+|```.*|~~~.*)/gm, "").replace(/\\([\\`*_{}[\]()#+\-.!|~>])/g, "$1").replace(/[*_~`|]/g, "").replace(/\s+/g, "");
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// src/server/infra/storage/thread-repository.ts
|
|
1359
|
-
function threadTitle(content2, names) {
|
|
1487
|
+
function threadTitle(content2, names, file = "") {
|
|
1360
1488
|
const line = (content2.split("\n").map((item) => item.trim()).find(Boolean) ?? "").replace(/[*`~]/g, "").replace(/^[#>\s-]+/, "");
|
|
1361
1489
|
const sorted = [...names].sort((a, b) => b.length - a.length);
|
|
1362
1490
|
let rest = line;
|
|
@@ -1366,18 +1494,18 @@ function threadTitle(content2, names) {
|
|
|
1366
1494
|
if (!name2) break;
|
|
1367
1495
|
rest = rest.slice(name2.length + 1);
|
|
1368
1496
|
}
|
|
1369
|
-
return (rest.trim() || line ||
|
|
1497
|
+
return (rest.trim() || line || file).slice(0, 60);
|
|
1370
1498
|
}
|
|
1371
1499
|
var ThreadRepository = class {
|
|
1372
1500
|
constructor(db, chats) {
|
|
1373
1501
|
this.db = db;
|
|
1374
1502
|
this.chats = chats;
|
|
1375
1503
|
}
|
|
1376
|
-
/** The thread of a channel message, created on first use; whoever opens it
|
|
1504
|
+
/** The thread of a channel message, created on first use; whoever opens it and the root's author follow it. */
|
|
1377
1505
|
open(actorId, rootMessageId, names) {
|
|
1378
1506
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1379
1507
|
try {
|
|
1380
|
-
const root = this.db.prepare(`SELECT m.chat_id AS chatId,m.content,c.type,c.owner_id AS ownerId FROM messages m JOIN chats c ON c.id=m.chat_id
|
|
1508
|
+
const root = this.db.prepare(`SELECT m.chat_id AS chatId,m.content,m.sender_id AS senderId,c.type,c.owner_id AS ownerId FROM messages m JOIN chats c ON c.id=m.chat_id
|
|
1381
1509
|
WHERE m.id=?`).get(rootMessageId);
|
|
1382
1510
|
if (!root || !this.chats.canAccess(actorId, root.chatId, true)) throw new ChatError(404, "Message not found");
|
|
1383
1511
|
if (root.type !== "channel") throw new ChatError(400, "Threads start from channel messages");
|
|
@@ -1385,9 +1513,11 @@ var ThreadRepository = class {
|
|
|
1385
1513
|
if (!id) {
|
|
1386
1514
|
id = randomUUID4();
|
|
1387
1515
|
const now = Date.now();
|
|
1516
|
+
const firstFile = this.db.prepare("SELECT name FROM attachments WHERE message_id=? ORDER BY created_at,rowid LIMIT 1").get(rootMessageId)?.name;
|
|
1388
1517
|
this.db.prepare(`INSERT INTO chats(id,type,title,owner_id,created_at,updated_at,parent_id,root_message_id,status)
|
|
1389
|
-
VALUES(?,'thread',?,?,?,?,?,?,'open')`).run(id, threadTitle(previewQuotes(root.content, this.chats.quotes.of(rootMessageId)), names), root.ownerId, now, now, root.chatId, rootMessageId);
|
|
1518
|
+
VALUES(?,'thread',?,?,?,?,?,?,'open')`).run(id, threadTitle(previewQuotes(root.content, this.chats.quotes.of(rootMessageId)), names, firstFile), root.ownerId, now, now, root.chatId, rootMessageId);
|
|
1390
1519
|
this.follow(actorId, id);
|
|
1520
|
+
if (root.senderId !== actorId && this.chats.canAccess(root.senderId, root.chatId)) this.follow(root.senderId, id);
|
|
1391
1521
|
}
|
|
1392
1522
|
this.db.exec("COMMIT");
|
|
1393
1523
|
return id;
|
|
@@ -1551,7 +1681,7 @@ var QuoteRepository = class {
|
|
|
1551
1681
|
var MESSAGE_COLUMNS = "id,chat_id AS chatId,sequence,sender_id AS senderId,content,created_at AS createdAt,history_id AS historyId,tool_call_id AS toolCallId,question,echo_of AS echoOf,reply_to AS replyTo";
|
|
1552
1682
|
var CHAT_COLUMNS = `c.id,c.type,c.title,c.owner_id AS ownerId,c.created_at AS createdAt,c.updated_at AS updatedAt,
|
|
1553
1683
|
c.section_id AS sectionId,c.topic,c.parent_id AS parentId,c.root_message_id AS rootMessageId,c.status,
|
|
1554
|
-
(SELECT
|
|
1684
|
+
(SELECT content FROM messages WHERE chat_id=c.id ORDER BY sequence DESC LIMIT 1) AS lastMessage,
|
|
1555
1685
|
(SELECT id FROM messages WHERE chat_id=c.id ORDER BY sequence DESC LIMIT 1) AS lastMessageId,
|
|
1556
1686
|
(SELECT COALESCE(a.username,b.name) FROM messages m LEFT JOIN account a ON a.member_id=m.sender_id LEFT JOIN bots b ON b.id=m.sender_id
|
|
1557
1687
|
WHERE m.chat_id=c.id ORDER BY m.sequence DESC LIMIT 1) AS lastSender`;
|
|
@@ -1610,8 +1740,8 @@ var ChatRepository = class {
|
|
|
1610
1740
|
if (!this.canAccess(actorId, id)) return null;
|
|
1611
1741
|
const found = this.db.prepare(`SELECT ${CHAT_COLUMNS} FROM chats c WHERE c.id=?`).get(id);
|
|
1612
1742
|
if (!found) return null;
|
|
1613
|
-
const { lastMessageId, ...
|
|
1614
|
-
|
|
1743
|
+
const { lastMessageId, lastMessage: content2, ...rest } = found;
|
|
1744
|
+
const row = { ...rest, lastMessage: content2 !== null && lastMessageId ? { content: clipMarked(content2, 300), quotes: this.quotes.of(lastMessageId) } : null };
|
|
1615
1745
|
const members = this.db.prepare("SELECT member_id AS id FROM chat_members WHERE chat_id=? ORDER BY member_id").all(row.parentId ?? id);
|
|
1616
1746
|
const state = this.db.prepare("SELECT following,subscribed,pinned,muted FROM chat_states WHERE chat_id=? AND member_id=?").get(id, actorId);
|
|
1617
1747
|
return {
|
|
@@ -1763,10 +1893,10 @@ var ChatRepository = class {
|
|
|
1763
1893
|
quotes: this.quotes.of(row.id)
|
|
1764
1894
|
};
|
|
1765
1895
|
}
|
|
1766
|
-
/** What a reply shows of the message it answers: who wrote it and the start of its
|
|
1896
|
+
/** What a reply shows of the message it answers: who wrote it and the start of its content, with its quotes ("" is attachments only). */
|
|
1767
1897
|
quote(id) {
|
|
1768
1898
|
const row = this.db.prepare("SELECT id,sender_id AS senderId,content FROM messages WHERE id=?").get(id);
|
|
1769
|
-
return row ? { id: row.id, senderId: row.senderId, content:
|
|
1899
|
+
return row ? { id: row.id, senderId: row.senderId, content: clipMarked(row.content, 200), quotes: this.quotes.of(row.id) } : null;
|
|
1770
1900
|
}
|
|
1771
1901
|
append(actorId, chatId, draft, requestHash, files = [], deliveries = { message: [], echo: [] }, mentions = []) {
|
|
1772
1902
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1793,7 +1923,7 @@ var ChatRepository = class {
|
|
|
1793
1923
|
if (draft.replyTo && this.db.prepare("SELECT chat_id AS chatId FROM messages WHERE id=?").get(draft.replyTo)?.chatId !== chatId)
|
|
1794
1924
|
throw new ChatError(400, "Reply target is not in this chat");
|
|
1795
1925
|
this.db.prepare("INSERT INTO messages(position,id,chat_id,sequence,sender_id,content,created_at,client_key,request_hash,history_id,tool_call_id,question,reply_to) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)").run(position, id, chatId, sequence, actorId, draft.content, createdAt, draft.clientMessageId ?? null, requestHash, draft.source?.historyId ?? null, draft.source?.toolCallId ?? null, question, draft.replyTo ?? null);
|
|
1796
|
-
for (const
|
|
1926
|
+
for (const file2 of files) this.db.prepare("INSERT INTO attachments(id,chat_id,message_id,uploaded_by,name,mime_type,size,data,created_at) VALUES(?,?,?,?,?,?,?,?,?)").run(randomUUID5(), chatId, id, actorId, file2.name, file2.mimeType, file2.data.length, file2.data, createdAt);
|
|
1797
1927
|
this.quotes.insert(id, draft.quotes ?? []);
|
|
1798
1928
|
for (const attachmentId of draft.attachmentIds) this.db.prepare("UPDATE attachments SET message_id=? WHERE id=?").run(id, attachmentId);
|
|
1799
1929
|
this.inbox(deliveries.message, chatId, id, actorId, createdAt);
|
|
@@ -1826,8 +1956,9 @@ var ChatRepository = class {
|
|
|
1826
1956
|
});
|
|
1827
1957
|
}
|
|
1828
1958
|
}
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1959
|
+
const file = this.db.prepare("SELECT name FROM attachments WHERE message_id=? ORDER BY created_at,rowid LIMIT 1").get(id);
|
|
1960
|
+
const title = draft.content ? previewQuotes(draft.content, draft.quotes ?? []).slice(0, 46).replace(/\s+/g, " ") : file?.name.slice(0, 46) ?? "";
|
|
1961
|
+
this.db.prepare("UPDATE chats SET title=CASE WHEN title='' AND ?=1 THEN ? ELSE title END,updated_at=? WHERE id=?").run(sequence, title, createdAt, chatId);
|
|
1831
1962
|
const message = this.messageFromRow({ id, chatId, sequence, senderId: actorId, content: draft.content, createdAt, historyId: draft.source?.historyId ?? null, toolCallId: draft.source?.toolCallId ?? null, question, echoOf: null, replyTo: draft.replyTo ?? null });
|
|
1832
1963
|
this.db.exec("COMMIT");
|
|
1833
1964
|
return { message, duplicate: false, ...echo ? { echo } : {} };
|
|
@@ -1927,7 +2058,7 @@ var RuntimeRepository = class {
|
|
|
1927
2058
|
}
|
|
1928
2059
|
submitted(runId) {
|
|
1929
2060
|
if (!this.db.prepare("UPDATE bot_runs SET state='submitted' WHERE id=? AND state='claimed'").run(runId).changes)
|
|
1930
|
-
throw new RuntimeFault("invalid_run", "
|
|
2061
|
+
throw new RuntimeFault("invalid_run", "The run state has changed.");
|
|
1931
2062
|
}
|
|
1932
2063
|
finish(runId, state, errorCode = null) {
|
|
1933
2064
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1944,7 +2075,7 @@ var RuntimeRepository = class {
|
|
|
1944
2075
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1945
2076
|
try {
|
|
1946
2077
|
if (!this.db.prepare("UPDATE bot_runs SET state='cancelled',ended_at=? WHERE id=? AND state='claimed'").run(Date.now(), runId).changes)
|
|
1947
|
-
throw new RuntimeFault("unsafe_replay", "
|
|
2078
|
+
throw new RuntimeFault("unsafe_replay", "A submitted run can't be replayed automatically.");
|
|
1948
2079
|
this.db.prepare("UPDATE inbox SET state='pending',run_id=NULL WHERE run_id=? AND state='claimed'").run(runId);
|
|
1949
2080
|
this.db.exec("COMMIT");
|
|
1950
2081
|
} catch (error) {
|
|
@@ -2011,8 +2142,8 @@ function providerCatalog() {
|
|
|
2011
2142
|
if (!models.length && provider.getAllModels?.().length) return [];
|
|
2012
2143
|
const common = { id: provider.id, api: models[0]?.api ?? "openai-completions", modelCount: models.length };
|
|
2013
2144
|
return [
|
|
2014
|
-
...provider.auth.oauth ? [{ ...common, name: provider.auth.oauth.name,
|
|
2015
|
-
...provider.auth.apiKey?.login ? [{ ...common, name: provider.name,
|
|
2145
|
+
...provider.auth.oauth ? [{ ...common, name: provider.auth.oauth.name, authType: "oauth" }] : [],
|
|
2146
|
+
...provider.auth.apiKey?.login ? [{ ...common, name: provider.name, authType: "api_key" }] : []
|
|
2016
2147
|
];
|
|
2017
2148
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2018
2149
|
}
|
|
@@ -2020,15 +2151,15 @@ var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getMod
|
|
|
2020
2151
|
|
|
2021
2152
|
// src/server/infra/storage/credential-repository.ts
|
|
2022
2153
|
function credentialValue(value2) {
|
|
2023
|
-
if (!value2 || typeof value2 !== "object") throw new RuntimeFault("invalid_credential", "
|
|
2154
|
+
if (!value2 || typeof value2 !== "object") throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
2024
2155
|
const v = value2;
|
|
2025
2156
|
if (v.type === "api_key" && (typeof v.key === "string" || v.env && typeof v.env === "object")) {
|
|
2026
|
-
if (v.env && (Array.isArray(v.env) || Object.values(v.env).some((item) => typeof item !== "string"))) throw new RuntimeFault("invalid_credential", "
|
|
2157
|
+
if (v.env && (Array.isArray(v.env) || Object.values(v.env).some((item) => typeof item !== "string"))) throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
2027
2158
|
return structuredClone(v);
|
|
2028
2159
|
}
|
|
2029
2160
|
if (v.type === "oauth" && typeof v.access === "string" && typeof v.refresh === "string" && typeof v.expires === "number" && Number.isFinite(v.expires))
|
|
2030
2161
|
return structuredClone(v);
|
|
2031
|
-
throw new RuntimeFault("invalid_credential", "
|
|
2162
|
+
throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
2032
2163
|
}
|
|
2033
2164
|
var CredentialRepository = class {
|
|
2034
2165
|
constructor(db) {
|
|
@@ -2044,9 +2175,9 @@ var CredentialRepository = class {
|
|
|
2044
2175
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2045
2176
|
try {
|
|
2046
2177
|
if (!this.db.prepare("SELECT 1 FROM provider_connections WHERE id=? AND owner_id=?").get(connectionId, ownerId))
|
|
2047
|
-
throw new RuntimeFault("connection_missing", "
|
|
2178
|
+
throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
2048
2179
|
const current = this.read(ownerId, connectionId);
|
|
2049
|
-
if ((current?.revision ?? null) !== prior) throw new RuntimeFault("credential_changed", "
|
|
2180
|
+
if ((current?.revision ?? null) !== prior) throw new RuntimeFault("credential_changed", "The credential changed. Please try again.");
|
|
2050
2181
|
this.db.prepare(`INSERT INTO provider_credentials(connection_id,type,credential_json,updated_at) VALUES(?,?,?,?)
|
|
2051
2182
|
ON CONFLICT(connection_id) DO UPDATE SET type=excluded.type,credential_json=excluded.credential_json,updated_at=excluded.updated_at`).run(connectionId, value2.type, next, Date.now());
|
|
2052
2183
|
this.db.exec("COMMIT");
|
|
@@ -2239,13 +2370,12 @@ var Store = class {
|
|
|
2239
2370
|
botName(value2, exceptBotId) {
|
|
2240
2371
|
const name2 = requireText(value2, "Bot name", 80);
|
|
2241
2372
|
if (name2.includes("@") || /[\n\r]/.test(name2)) throw new ValidationError("Bot name cannot contain @ or line breaks");
|
|
2242
|
-
if (this.nameTaken(name2, exceptBotId)) throw new ConflictError("
|
|
2373
|
+
if (this.nameTaken(name2, exceptBotId)) throw new ConflictError("Another member already has this name", "name_taken");
|
|
2243
2374
|
return name2;
|
|
2244
2375
|
}
|
|
2376
|
+
/** The caller names the Bot (A45: the interface picks a free default in its own language). */
|
|
2245
2377
|
createBot(name2) {
|
|
2246
|
-
|
|
2247
|
-
if (name2 === void 0) for (let suffix = 2; this.nameTaken(clean); suffix++) clean = `\u65B0 Bot ${suffix}`;
|
|
2248
|
-
else clean = this.botName(name2);
|
|
2378
|
+
const clean = this.botName(name2);
|
|
2249
2379
|
const id = randomUUID7(), createdAt = Date.now();
|
|
2250
2380
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2251
2381
|
try {
|
|
@@ -2331,7 +2461,7 @@ var Store = class {
|
|
|
2331
2461
|
return this.getModelConnection(id);
|
|
2332
2462
|
}
|
|
2333
2463
|
checkCustomName(name2, except = "") {
|
|
2334
|
-
if (this.db.prepare("SELECT 1 FROM provider_connections WHERE owner_id=? AND kind='custom' AND provider_name=? AND id<>?").get(this.userMemberId(), name2, except)) throw new ValidationError("
|
|
2464
|
+
if (this.db.prepare("SELECT 1 FROM provider_connections WHERE owner_id=? AND kind='custom' AND provider_name=? AND id<>?").get(this.userMemberId(), name2, except)) throw new ValidationError("A provider with this name already exists", "provider_name_taken");
|
|
2335
2465
|
}
|
|
2336
2466
|
updateModelConnection(id, input) {
|
|
2337
2467
|
const current = this.getModelConnection(id);
|
|
@@ -2460,8 +2590,8 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2460
2590
|
const contextWindow = configured2.contextWindow ?? builtin?.contextWindow;
|
|
2461
2591
|
const maxTokens = configured2.maxTokens ?? builtin?.maxTokens;
|
|
2462
2592
|
const baseUrl = connection.baseUrl || builtin?.baseUrl || runtime.getModels(connection.providerName)[0]?.baseUrl;
|
|
2463
|
-
if (!contextWindow || !maxTokens) throw new RuntimeFault("model_incomplete", "
|
|
2464
|
-
if (configured2.thinkingFormat && !FORMATS.has(configured2.thinkingFormat)) throw new RuntimeFault("
|
|
2593
|
+
if (!contextWindow || !maxTokens) throw new RuntimeFault("model_incomplete", "Fill in the model's context window and max output.", 422);
|
|
2594
|
+
if (configured2.thinkingFormat && !FORMATS.has(configured2.thinkingFormat)) throw new RuntimeFault("thinking_format", "This thinking format isn't supported.", 422);
|
|
2465
2595
|
const compat = { ...builtin?.compat, ...configured2.compat, ...configured2.thinkingFormat ? { thinkingFormat: configured2.thinkingFormat } : {} };
|
|
2466
2596
|
runtime.registerProvider(connection.providerName, {
|
|
2467
2597
|
baseUrl,
|
|
@@ -2483,7 +2613,7 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2483
2613
|
}]
|
|
2484
2614
|
});
|
|
2485
2615
|
const model = runtime.getModel(connection.providerName, configured2.id);
|
|
2486
|
-
if (!model) throw new RuntimeFault("model_missing", "
|
|
2616
|
+
if (!model) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
2487
2617
|
model.headers = { ...model.headers, ...headers2 };
|
|
2488
2618
|
return model;
|
|
2489
2619
|
}
|
|
@@ -2519,7 +2649,7 @@ var CredentialCoordinator = class {
|
|
|
2519
2649
|
tails = /* @__PURE__ */ new Map();
|
|
2520
2650
|
closing = false;
|
|
2521
2651
|
async exclusive(connectionId, action, signal) {
|
|
2522
|
-
if (this.closing) throw new RuntimeFault("stopping", "
|
|
2652
|
+
if (this.closing) throw new RuntimeFault("stopping", "Shutting down.");
|
|
2523
2653
|
signal?.throwIfAborted();
|
|
2524
2654
|
const prior = this.tails.get(connectionId) ?? Promise.resolve();
|
|
2525
2655
|
const operation = prior.then(() => {
|
|
@@ -2566,7 +2696,7 @@ var ConnectionCredentials = class {
|
|
|
2566
2696
|
options?.signal?.throwIfAborted();
|
|
2567
2697
|
if (providerId !== this.providerName) return void 0;
|
|
2568
2698
|
const row = this.repository.read(this.ownerId, this.connectionId);
|
|
2569
|
-
if (!row) throw new RuntimeFault("credentials_missing", "
|
|
2699
|
+
if (!row) throw new RuntimeFault("credentials_missing", "Set up this connection's credential first.", 422);
|
|
2570
2700
|
return row.value;
|
|
2571
2701
|
}
|
|
2572
2702
|
async list(options) {
|
|
@@ -2575,7 +2705,7 @@ var ConnectionCredentials = class {
|
|
|
2575
2705
|
return row ? [{ providerId: this.providerName, type: row.value.type }] : [];
|
|
2576
2706
|
}
|
|
2577
2707
|
async modify(providerId, fn, options) {
|
|
2578
|
-
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "
|
|
2708
|
+
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "The credential belongs to another connection.");
|
|
2579
2709
|
return this.coordinator.exclusive(this.connectionId, async () => {
|
|
2580
2710
|
const prior = this.repository.read(this.ownerId, this.connectionId);
|
|
2581
2711
|
const next = await fn(prior?.value);
|
|
@@ -2584,7 +2714,7 @@ var ConnectionCredentials = class {
|
|
|
2584
2714
|
}, options?.signal);
|
|
2585
2715
|
}
|
|
2586
2716
|
async delete(providerId, options) {
|
|
2587
|
-
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "
|
|
2717
|
+
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "The credential belongs to another connection.");
|
|
2588
2718
|
await this.coordinator.exclusive(this.connectionId, async () => {
|
|
2589
2719
|
this.repository.delete(this.ownerId, this.connectionId);
|
|
2590
2720
|
}, options?.signal);
|
|
@@ -2723,10 +2853,10 @@ function autoconfigure(modelId, api, choice) {
|
|
|
2723
2853
|
// src/server/infra/models/discovery.ts
|
|
2724
2854
|
async function discoverModels(input, signal) {
|
|
2725
2855
|
const api = String(input.api ?? "openai-completions");
|
|
2726
|
-
if (!["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"].includes(api)) throw new RuntimeFault("discovery_protocol", "
|
|
2727
|
-
if (typeof input.baseUrl !== "string") throw new RuntimeFault("
|
|
2856
|
+
if (!["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"].includes(api)) throw new RuntimeFault("discovery_protocol", "This protocol can't list models yet.", 400);
|
|
2857
|
+
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
2728
2858
|
const url = new URL(input.baseUrl);
|
|
2729
|
-
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new RuntimeFault("discovery_url", "API URL
|
|
2859
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new RuntimeFault("discovery_url", "The API URL isn't valid.", 400);
|
|
2730
2860
|
const key = input.key;
|
|
2731
2861
|
let path = url.pathname.replace(/\/+$/, "");
|
|
2732
2862
|
if (!path.endsWith("/models")) {
|
|
@@ -2745,11 +2875,14 @@ async function discoverModels(input, signal) {
|
|
|
2745
2875
|
const models = /* @__PURE__ */ new Map();
|
|
2746
2876
|
let more = true;
|
|
2747
2877
|
while (more) {
|
|
2748
|
-
const response = await fetch(url, { headers: headers2, signal })
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
if (!
|
|
2878
|
+
const response = await fetch(url, { headers: headers2, signal }).catch((error) => {
|
|
2879
|
+
if (signal.aborted) throw error;
|
|
2880
|
+
throw new RuntimeFault("discovery_unreachable", `Couldn't reach ${url.host}.`, 502, { host: url.host });
|
|
2881
|
+
});
|
|
2882
|
+
if (!response.ok) throw new RuntimeFault("discovery_http", `Couldn't fetch the models: HTTP ${response.status}.`, 502, { status: response.status });
|
|
2883
|
+
const body = await response.json().catch(() => null);
|
|
2884
|
+
const items = api === "google-generative-ai" ? body?.models : body?.data;
|
|
2885
|
+
if (!body || !Array.isArray(items)) throw new RuntimeFault("discovery_format", "The response isn't a model list for this protocol.", 502);
|
|
2753
2886
|
for (const raw of items) {
|
|
2754
2887
|
if (!raw || typeof raw !== "object") continue;
|
|
2755
2888
|
const item = raw;
|
|
@@ -2834,26 +2967,26 @@ var ModelService = class {
|
|
|
2834
2967
|
return modelCatalog(credentials, connection.providerName);
|
|
2835
2968
|
}
|
|
2836
2969
|
discover(input, signal) {
|
|
2837
|
-
if (typeof input.baseUrl !== "string") throw new RuntimeFault("
|
|
2970
|
+
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
2838
2971
|
let key = typeof input.apiKey === "string" ? input.apiKey.trim() : "";
|
|
2839
2972
|
if (!key && typeof input.connectionId === "string") {
|
|
2840
2973
|
this.required(this.store.getModelConnection(input.connectionId));
|
|
2841
2974
|
const credential = this.store.credentials.read(this.store.userMemberId(), input.connectionId)?.value;
|
|
2842
2975
|
if (credential?.type === "api_key") key = credential.key ?? "";
|
|
2843
2976
|
}
|
|
2844
|
-
if (!key) throw new RuntimeFault("discovery_key", "
|
|
2977
|
+
if (!key) throw new RuntimeFault("discovery_key", "Enter the API key.", 400);
|
|
2845
2978
|
return discoverModels({ api: String(input.api ?? "openai-completions"), baseUrl: input.baseUrl, key }, signal);
|
|
2846
2979
|
}
|
|
2847
2980
|
/** Suggests model fields from the local Pi catalog; the caller decides whether to save them. */
|
|
2848
2981
|
autoconfigure(input) {
|
|
2849
2982
|
const text3 = (value2) => typeof value2 === "string" && value2.trim() && value2.length <= 200 ? value2.trim() : null;
|
|
2850
2983
|
const modelId = text3(input.modelId), api = text3(input.api);
|
|
2851
|
-
if (!modelId || !api) throw new RuntimeFault("
|
|
2984
|
+
if (!modelId || !api) throw new RuntimeFault("autoconfig_model", "Enter the model ID.", 400);
|
|
2852
2985
|
let choice;
|
|
2853
2986
|
if (input.choice !== void 0) {
|
|
2854
2987
|
const raw = input.choice;
|
|
2855
2988
|
const provider = text3(raw?.provider), chosen = text3(raw?.modelId);
|
|
2856
|
-
if (!provider || !chosen) throw new RuntimeFault("
|
|
2989
|
+
if (!provider || !chosen) throw new RuntimeFault("autoconfig_source", "The setup source isn't valid.", 400);
|
|
2857
2990
|
choice = { provider, modelId: chosen };
|
|
2858
2991
|
}
|
|
2859
2992
|
return autoconfigure(modelId, api, choice);
|
|
@@ -2894,7 +3027,7 @@ var CredentialLogins = class {
|
|
|
2894
3027
|
flows = /* @__PURE__ */ new Map();
|
|
2895
3028
|
connection(id) {
|
|
2896
3029
|
const connection = this.store.getModelConnection(id);
|
|
2897
|
-
if (!connection) throw new RuntimeFault("connection_missing", "
|
|
3030
|
+
if (!connection) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
2898
3031
|
return connection;
|
|
2899
3032
|
}
|
|
2900
3033
|
status(id) {
|
|
@@ -2920,7 +3053,7 @@ var CredentialLogins = class {
|
|
|
2920
3053
|
flow.state = { phase: "completed" };
|
|
2921
3054
|
this.changed(id);
|
|
2922
3055
|
} catch (error) {
|
|
2923
|
-
flow.state = flow.controller.signal.aborted ? { phase: "cancelled" } : { phase: "error"
|
|
3056
|
+
flow.state = flow.controller.signal.aborted ? { phase: "cancelled" } : { phase: "error" };
|
|
2924
3057
|
if (!flow.controller.signal.aborted) console.error("[oauth] login failed", { connectionId: id, provider: connection.providerName, type: error instanceof Error ? error.constructor.name : "Error" });
|
|
2925
3058
|
} finally {
|
|
2926
3059
|
flow.answer = void 0;
|
|
@@ -2960,9 +3093,9 @@ var CredentialLogins = class {
|
|
|
2960
3093
|
input(id, body) {
|
|
2961
3094
|
this.connection(id);
|
|
2962
3095
|
const flow = this.flows.get(id), prompt = flow?.state.prompt;
|
|
2963
|
-
if (!flow?.answer || !prompt || body.promptId !== prompt.id) throw new RuntimeFault("login_step", "
|
|
2964
|
-
if (typeof body.value !== "string" || !body.value.trim()) throw new RuntimeFault("login_input", "
|
|
2965
|
-
if (prompt.options && !prompt.options.some((option2) => option2.id === body.value)) throw new RuntimeFault("
|
|
3096
|
+
if (!flow?.answer || !prompt || body.promptId !== prompt.id) throw new RuntimeFault("login_step", "The sign-in step has changed. Please start again.", 409);
|
|
3097
|
+
if (typeof body.value !== "string" || !body.value.trim()) throw new RuntimeFault("login_input", "Enter the sign-in details.", 400);
|
|
3098
|
+
if (prompt.options && !prompt.options.some((option2) => option2.id === body.value)) throw new RuntimeFault("login_option", "Pick a sign-in method.", 400);
|
|
2966
3099
|
flow.answer(body.value);
|
|
2967
3100
|
return this.status(id);
|
|
2968
3101
|
}
|
|
@@ -3044,14 +3177,14 @@ function messageTargets({ dm, sender, members, mentioned, replied, subscribers }
|
|
|
3044
3177
|
if (dm) return members.filter((member) => member.id !== sender.id).map((member) => ({ member, reason: "dm" }));
|
|
3045
3178
|
const targets = mentioned.filter((member) => member.id !== sender.id).map((member) => ({ member, reason: "mention" }));
|
|
3046
3179
|
const add = (member, reason) => {
|
|
3047
|
-
if (member.id !== sender.id && !targets.some((
|
|
3180
|
+
if (member.id !== sender.id && !targets.some((target2) => target2.member.id === member.id)) targets.push({ member, reason });
|
|
3048
3181
|
};
|
|
3049
3182
|
if (replied && members.some((member) => member.id === replied.id)) add(replied, "reply");
|
|
3050
3183
|
for (const member of members) if (subscribers.includes(member.id)) add(member, "subscription");
|
|
3051
3184
|
return targets;
|
|
3052
3185
|
}
|
|
3053
3186
|
function echoTargets({ sender, members, subscribers, reached }) {
|
|
3054
|
-
return members.filter((member) => member.id !== sender.id && subscribers.includes(member.id) && !reached.some((
|
|
3187
|
+
return members.filter((member) => member.id !== sender.id && subscribers.includes(member.id) && !reached.some((target2) => target2.member.id === member.id)).map((member) => ({ member, reason: "subscription" }));
|
|
3055
3188
|
}
|
|
3056
3189
|
function reactionTargets({ actorId, author }) {
|
|
3057
3190
|
return author && author.id !== actorId ? [{ member: author, reason: "author" }] : [];
|
|
@@ -3078,9 +3211,9 @@ function mutedUrgency(reason, urgency) {
|
|
|
3078
3211
|
|
|
3079
3212
|
// src/server/service/delivery/delivery.ts
|
|
3080
3213
|
function plan(kind, targets, muted = []) {
|
|
3081
|
-
return targets.flatMap((
|
|
3082
|
-
const urgency = urgencyOf(kind,
|
|
3083
|
-
return urgency ? [{ ...
|
|
3214
|
+
return targets.flatMap((target2) => {
|
|
3215
|
+
const urgency = urgencyOf(kind, target2.reason);
|
|
3216
|
+
return urgency ? [{ ...target2, kind, urgency: muted.includes(target2.member.id) ? mutedUrgency(target2.reason, urgency) : urgency }] : [];
|
|
3084
3217
|
});
|
|
3085
3218
|
}
|
|
3086
3219
|
var Dispatcher = class {
|
|
@@ -3290,7 +3423,7 @@ var ChatQuestions = class {
|
|
|
3290
3423
|
if (!question) return Promise.reject(new ChatError(409, "Not a question"));
|
|
3291
3424
|
if (question.state === "answered") return Promise.resolve(resultOf(question, question.answer));
|
|
3292
3425
|
if (question.state === "dismissed") return Promise.resolve({ status: "dismissed" });
|
|
3293
|
-
if (question.state === "ignored") return Promise.reject(new ChatError(409, "
|
|
3426
|
+
if (question.state === "ignored") return Promise.reject(new ChatError(409, "The question has expired", "question_expired"));
|
|
3294
3427
|
return new Promise((resolve6, reject) => {
|
|
3295
3428
|
const stop = () => {
|
|
3296
3429
|
this.waiting.delete(message.id);
|
|
@@ -3314,15 +3447,15 @@ var ChatQuestions = class {
|
|
|
3314
3447
|
this.repository.assertAccess(actorId, chatId, true);
|
|
3315
3448
|
if (message.sender.id === actorId) throw new ChatError(403, "A question is answered by the other members");
|
|
3316
3449
|
const question = message.question, waiter = this.waiting.get(messageId);
|
|
3317
|
-
if (question.state !== "pending") throw new ChatError(409, "
|
|
3450
|
+
if (question.state !== "pending") throw new ChatError(409, "The question is already settled", "question_closed");
|
|
3318
3451
|
if (!waiter) {
|
|
3319
3452
|
if (this.repository.settleQuestion(messageId, "ignored", null)) this.changed(chatId);
|
|
3320
|
-
throw new ChatError(409, "
|
|
3453
|
+
throw new ChatError(409, "The question has expired", "question_expired");
|
|
3321
3454
|
}
|
|
3322
3455
|
const dismissed = input.action === "dismiss";
|
|
3323
3456
|
if (!dismissed && input.action !== "answer") throw new ChatError(400, "Invalid question action");
|
|
3324
3457
|
const answer = dismissed ? null : answerOf(question, input);
|
|
3325
|
-
if (!this.repository.settleQuestion(messageId, dismissed ? "dismissed" : "answered", answer)) throw new ChatError(409, "
|
|
3458
|
+
if (!this.repository.settleQuestion(messageId, dismissed ? "dismissed" : "answered", answer)) throw new ChatError(409, "The question is already settled", "question_closed");
|
|
3326
3459
|
this.waiting.delete(messageId);
|
|
3327
3460
|
waiter(answer ? resultOf(question, answer) : { status: "dismissed" });
|
|
3328
3461
|
this.changed(chatId);
|
|
@@ -3489,7 +3622,7 @@ var ChatThreads = class {
|
|
|
3489
3622
|
const chat = this.repository.getChat(actorId, threadId);
|
|
3490
3623
|
if (!chat || chat.type !== "thread") throw new ChatError(404, "Thread not found");
|
|
3491
3624
|
const root = chat.rootMessageId ? this.repository.message(actorId, chat.rootMessageId) : null;
|
|
3492
|
-
return { chat, info: this.store.info(chat.rootMessageId), root: root ? { senderId: root.sender.id, content:
|
|
3625
|
+
return { chat, info: this.store.info(chat.rootMessageId), root: root ? { senderId: root.sender.id, content: root.content, quotes: root.quotes } : null };
|
|
3493
3626
|
}
|
|
3494
3627
|
list(actorId, channelId) {
|
|
3495
3628
|
return this.store.list(actorId, channelId).map((id) => this.entry(actorId, id));
|
|
@@ -3540,7 +3673,7 @@ function resolveQuotes(repository, actorId, chatId, content2, drafts) {
|
|
|
3540
3673
|
|
|
3541
3674
|
// src/server/service/chat/service.ts
|
|
3542
3675
|
function titleValue(value2) {
|
|
3543
|
-
if (value2 === void 0) return "
|
|
3676
|
+
if (value2 === void 0) return "";
|
|
3544
3677
|
if (typeof value2 !== "string" || !value2.trim() || value2.length > 100 || value2.includes("\0")) throw new ChatError(400, "Invalid Chat title");
|
|
3545
3678
|
return value2.trim();
|
|
3546
3679
|
}
|
|
@@ -3602,9 +3735,9 @@ var ChatService = class {
|
|
|
3602
3735
|
/** A DM with another member of the actor's space (a Bot DMs its owner or another Bot); one per pair. */
|
|
3603
3736
|
openDirect(actorId, targetId) {
|
|
3604
3737
|
const owner = this.repository.channels.ownerOf(actorId), directory = this.repository.channels.directory(owner);
|
|
3605
|
-
const actor = directory.find((member) => member.id === actorId),
|
|
3606
|
-
if (!actor || !
|
|
3607
|
-
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name}\
|
|
3738
|
+
const actor = directory.find((member) => member.id === actorId), target2 = directory.find((member) => member.id === targetId);
|
|
3739
|
+
if (!actor || !target2) throw new ChatError(404, "Member not found");
|
|
3740
|
+
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target2.name}`);
|
|
3608
3741
|
this.notifySaved(chat.id);
|
|
3609
3742
|
return chat;
|
|
3610
3743
|
}
|
|
@@ -3876,7 +4009,7 @@ var ChatFiles = class {
|
|
|
3876
4009
|
/** The quoted message as the envelope shows it: who wrote it and its first 200 characters. */
|
|
3877
4010
|
reply(message) {
|
|
3878
4011
|
const sender = message.replyTo ? this.chats.member(message.replyTo.senderId) : null;
|
|
3879
|
-
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: message.replyTo
|
|
4012
|
+
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: replyExcerpt(message.replyTo) } } : {};
|
|
3880
4013
|
}
|
|
3881
4014
|
async paths(actorId, message, signal) {
|
|
3882
4015
|
const paths = [];
|
|
@@ -3916,8 +4049,8 @@ var Changes = class {
|
|
|
3916
4049
|
// src/server/service/bot/activity.ts
|
|
3917
4050
|
function activityPage(messages, history, limit) {
|
|
3918
4051
|
const candidates2 = [
|
|
3919
|
-
...messages.map((
|
|
3920
|
-
...history.map((
|
|
4052
|
+
...messages.map((record2) => ({ kind: "chat", record: record2 })),
|
|
4053
|
+
...history.map((record2) => ({ kind: "history", record: record2 }))
|
|
3921
4054
|
];
|
|
3922
4055
|
candidates2.sort((a, b) => {
|
|
3923
4056
|
if (a.record.timestamp !== b.record.timestamp) return b.record.timestamp - a.record.timestamp;
|
|
@@ -4073,7 +4206,74 @@ function configureHttp(timeoutMs) {
|
|
|
4073
4206
|
|
|
4074
4207
|
// src/server/infra/pi/session.ts
|
|
4075
4208
|
import { Type } from "typebox";
|
|
4209
|
+
|
|
4210
|
+
// src/server/infra/pi/live.ts
|
|
4211
|
+
var LiveTranslator = class {
|
|
4212
|
+
message = 0;
|
|
4213
|
+
open = /* @__PURE__ */ new Set();
|
|
4214
|
+
translate(event) {
|
|
4215
|
+
switch (event.type) {
|
|
4216
|
+
case "message_start":
|
|
4217
|
+
if (event.message.role === "assistant") this.message++;
|
|
4218
|
+
return [];
|
|
4219
|
+
case "message_update": {
|
|
4220
|
+
const update = event.assistantMessageEvent;
|
|
4221
|
+
if (update.type !== "thinking_start" && update.type !== "thinking_delta" && update.type !== "thinking_end" && update.type !== "text_start" && update.type !== "text_delta" && update.type !== "text_end") return [];
|
|
4222
|
+
const key = `${this.message}:${update.contentIndex}`, kind = update.type.startsWith("thinking") ? "thinking" : "text";
|
|
4223
|
+
const events = [];
|
|
4224
|
+
if (!this.open.has(key) && !update.type.endsWith("_end")) {
|
|
4225
|
+
this.open.add(key);
|
|
4226
|
+
events.push({ type: "block_start", key, kind });
|
|
4227
|
+
}
|
|
4228
|
+
if (update.type.endsWith("_delta") && "delta" in update && update.delta) events.push({ type: "block_delta", key, kind, delta: update.delta });
|
|
4229
|
+
if (update.type.endsWith("_end") && this.open.delete(key)) events.push({ type: "block_end", key });
|
|
4230
|
+
return events;
|
|
4231
|
+
}
|
|
4232
|
+
case "message_end": {
|
|
4233
|
+
if (event.message.role !== "assistant") return [];
|
|
4234
|
+
const prefix = `${this.message}:`, closing = [...this.open].filter((key) => key.startsWith(prefix));
|
|
4235
|
+
for (const key of closing) this.open.delete(key);
|
|
4236
|
+
return closing.map((key) => ({ type: "block_end", key }));
|
|
4237
|
+
}
|
|
4238
|
+
case "tool_execution_start":
|
|
4239
|
+
return [{ type: "tool_start", id: event.toolCallId, parentId: event.parentToolCallId ?? null, name: event.toolName, args: event.args }];
|
|
4240
|
+
case "tool_execution_end":
|
|
4241
|
+
return [{ type: "tool_end", id: event.toolCallId, output: resultText(event.result), failed: event.isError }];
|
|
4242
|
+
case "compaction_start":
|
|
4243
|
+
return [{ type: "compaction_start" }];
|
|
4244
|
+
case "compaction_end":
|
|
4245
|
+
return [{
|
|
4246
|
+
type: "compaction_end",
|
|
4247
|
+
failed: !!event.errorMessage || event.aborted,
|
|
4248
|
+
error: event.errorMessage ?? null,
|
|
4249
|
+
tokensBefore: event.result?.tokensBefore ?? null,
|
|
4250
|
+
tokensAfter: event.result?.estimatedTokensAfter ?? null
|
|
4251
|
+
}];
|
|
4252
|
+
case "auto_retry_start":
|
|
4253
|
+
return [{ type: "retry_start", attempt: event.attempt, maxAttempts: event.maxAttempts, error: event.errorMessage }];
|
|
4254
|
+
case "auto_retry_end":
|
|
4255
|
+
return [{ type: "retry_end", failed: !event.success, error: event.finalError ?? null }];
|
|
4256
|
+
default:
|
|
4257
|
+
return [];
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
};
|
|
4261
|
+
function resultText(result) {
|
|
4262
|
+
const content2 = result?.content;
|
|
4263
|
+
if (Array.isArray(content2)) return content2.map((part) => part?.type === "text" ? String(part.text ?? "") : part?.type === "image" ? "[image]" : "").filter(Boolean).join("\n");
|
|
4264
|
+
if (typeof result === "string") return result;
|
|
4265
|
+
try {
|
|
4266
|
+
return JSON.stringify(result) ?? "";
|
|
4267
|
+
} catch {
|
|
4268
|
+
return String(result);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
// src/server/infra/pi/session.ts
|
|
4076
4273
|
var HTTP_IDLE_TIMEOUT_MS = 9e5;
|
|
4274
|
+
function compactionReserve(contextWindow) {
|
|
4275
|
+
return Math.min(65536, Math.max(16384, Math.min(32768, Math.floor(contextWindow / 4)), Math.floor(contextWindow * 0.13)));
|
|
4276
|
+
}
|
|
4077
4277
|
var PiSession = class _PiSession {
|
|
4078
4278
|
constructor(options, models, credentials) {
|
|
4079
4279
|
this.options = options;
|
|
@@ -4081,6 +4281,7 @@ var PiSession = class _PiSession {
|
|
|
4081
4281
|
this.credentials = credentials;
|
|
4082
4282
|
}
|
|
4083
4283
|
session;
|
|
4284
|
+
settings;
|
|
4084
4285
|
unsubscribe;
|
|
4085
4286
|
unsubscribeHistory;
|
|
4086
4287
|
promptActive = false;
|
|
@@ -4092,6 +4293,7 @@ var PiSession = class _PiSession {
|
|
|
4092
4293
|
abortFailure;
|
|
4093
4294
|
result = { outcome: "completed" };
|
|
4094
4295
|
signature = "";
|
|
4296
|
+
translator = new LiveTranslator();
|
|
4095
4297
|
get context() {
|
|
4096
4298
|
const usage = this.session.getContextUsage();
|
|
4097
4299
|
return usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : void 0;
|
|
@@ -4111,33 +4313,36 @@ var PiSession = class _PiSession {
|
|
|
4111
4313
|
await mkdir2(sessionsDir, { recursive: true, mode: 448 });
|
|
4112
4314
|
const expected = resolve3(sessionsDir) + sep;
|
|
4113
4315
|
if (!resolve3(reference.file).startsWith(expected) || dirname2(resolve3(reference.file)) !== resolve3(sessionsDir))
|
|
4114
|
-
throw new RuntimeFault("session_path", "
|
|
4316
|
+
throw new RuntimeFault("session_path", "The session file path isn't valid.");
|
|
4115
4317
|
const exists = existsSync(reference.file);
|
|
4116
4318
|
if (!exists && (reference.persisted || store.runtime.runs(bot.id).some((run) => run.state === "unknown")))
|
|
4117
|
-
throw new RuntimeFault("session_missing", "
|
|
4319
|
+
throw new RuntimeFault("session_missing", "The session file is missing; check the run records before recovering.");
|
|
4118
4320
|
if (exists) {
|
|
4119
4321
|
let header;
|
|
4120
4322
|
try {
|
|
4121
4323
|
header = JSON.parse(readFileSync(reference.file, "utf8").split("\n")[0]);
|
|
4122
4324
|
} catch {
|
|
4123
|
-
throw new RuntimeFault("session_corrupt", "
|
|
4325
|
+
throw new RuntimeFault("session_corrupt", "The session file can't be read.");
|
|
4124
4326
|
}
|
|
4125
4327
|
if (header.type !== "session" || !header.id || reference.sessionId && header.id !== reference.sessionId)
|
|
4126
|
-
throw new RuntimeFault("session_mismatch", "
|
|
4328
|
+
throw new RuntimeFault("session_mismatch", "The session file doesn't match this Bot.");
|
|
4127
4329
|
}
|
|
4128
4330
|
const connection = store.getModelConnection(selection.connectionId);
|
|
4129
|
-
if (!connection) throw new RuntimeFault("connection_missing", "
|
|
4331
|
+
if (!connection) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
4130
4332
|
const scoped = new ConnectionCredentials(store.credentials, options.credentials, store.userMemberId(), connection.id, connection.providerName);
|
|
4131
4333
|
await scoped.read(connection.providerName);
|
|
4132
4334
|
const credentials = new CurrentCredentials(scoped);
|
|
4133
4335
|
const models = await ModelRuntime3.create({ credentials, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
4134
4336
|
const configured2 = connection.models.find((model2) => model2.id === selection.modelId);
|
|
4135
|
-
if (!configured2) throw new RuntimeFault("model_missing", "
|
|
4337
|
+
if (!configured2) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
4136
4338
|
const model = installModel(models, connection, configured2, store.modelHeaders(connection.id, configured2.id));
|
|
4137
4339
|
const sessionManager = SessionManager.open(reference.file, sessionsDir, bot.workspacePath);
|
|
4138
4340
|
const adapter = new _PiSession(options, models, credentials);
|
|
4139
4341
|
configureHttp(HTTP_IDLE_TIMEOUT_MS);
|
|
4140
|
-
const settingsManager = SettingsManager.inMemory({
|
|
4342
|
+
const settingsManager = SettingsManager.inMemory({
|
|
4343
|
+
httpIdleTimeoutMs: HTTP_IDLE_TIMEOUT_MS,
|
|
4344
|
+
compaction: { reserveTokens: compactionReserve(model.contextWindow) }
|
|
4345
|
+
});
|
|
4141
4346
|
const tools = ["bash", "read", "edit", "write", "codemode", ...options.tools.map((tool) => tool.name)];
|
|
4142
4347
|
const resourceLoader = new DefaultResourceLoader({
|
|
4143
4348
|
cwd: bot.workspacePath,
|
|
@@ -4177,9 +4382,10 @@ var PiSession = class _PiSession {
|
|
|
4177
4382
|
tools
|
|
4178
4383
|
});
|
|
4179
4384
|
adapter.session = result.session;
|
|
4385
|
+
adapter.settings = settingsManager;
|
|
4180
4386
|
if (result.extensionsResult.errors.length || !tools.every((name2) => adapter.session.getActiveToolNames().includes(name2))) {
|
|
4181
4387
|
adapter.session.dispose();
|
|
4182
|
-
throw new RuntimeFault("tools_missing", "
|
|
4388
|
+
throw new RuntimeFault("tools_missing", "Couldn't load the run's tools.");
|
|
4183
4389
|
}
|
|
4184
4390
|
adapter.unsubscribe = adapter.session.subscribe((event) => adapter.onEvent(event));
|
|
4185
4391
|
if (options.onHistory) {
|
|
@@ -4190,8 +4396,8 @@ var PiSession = class _PiSession {
|
|
|
4190
4396
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
4191
4397
|
const entry = entries[index];
|
|
4192
4398
|
if (entry.type !== "message" || entry.message !== event.message) continue;
|
|
4193
|
-
const
|
|
4194
|
-
if (
|
|
4399
|
+
const record2 = history.convert(entry);
|
|
4400
|
+
if (record2) await options.onHistory(record2);
|
|
4195
4401
|
break;
|
|
4196
4402
|
}
|
|
4197
4403
|
});
|
|
@@ -4233,6 +4439,7 @@ var PiSession = class _PiSession {
|
|
|
4233
4439
|
}
|
|
4234
4440
|
);
|
|
4235
4441
|
if (event.type === "message_end" && event.message.role === "assistant") this.result = this.classify(event.message);
|
|
4442
|
+
if (this.options.live) for (const live of this.translator.translate(event)) this.options.live(live);
|
|
4236
4443
|
this.maybeAbort();
|
|
4237
4444
|
}
|
|
4238
4445
|
/** The last assistant message decides the prompt (a later retry success replaces an earlier error).
|
|
@@ -4241,7 +4448,7 @@ var PiSession = class _PiSession {
|
|
|
4241
4448
|
* request that follows a tool cancelled mid-run. Apply the same signal rule to those. */
|
|
4242
4449
|
classify(message) {
|
|
4243
4450
|
if (message.stopReason === "aborted" || message.stopReason === "error" && this.session.agent.signal?.aborted) return { outcome: "interrupted" };
|
|
4244
|
-
if (message.stopReason === "error") return { outcome: "failed", error: message.errorMessage?.trim() || "
|
|
4451
|
+
if (message.stopReason === "error") return { outcome: "failed", error: message.errorMessage?.trim() || "Unknown error" };
|
|
4245
4452
|
return { outcome: "completed" };
|
|
4246
4453
|
}
|
|
4247
4454
|
maybeAbort() {
|
|
@@ -4276,21 +4483,28 @@ var PiSession = class _PiSession {
|
|
|
4276
4483
|
const settled = new Promise((resolve6) => {
|
|
4277
4484
|
this.settledResolve = resolve6;
|
|
4278
4485
|
});
|
|
4486
|
+
this.options.live?.({ type: "start" });
|
|
4487
|
+
let outcome = "failed";
|
|
4279
4488
|
try {
|
|
4280
|
-
|
|
4489
|
+
try {
|
|
4490
|
+
await this.session.prompt(xml, { expandPromptTemplates: false });
|
|
4491
|
+
} finally {
|
|
4492
|
+
await this.session.waitForIdle();
|
|
4493
|
+
if (this.started && !this.settled) await settled;
|
|
4494
|
+
this.promptActive = false;
|
|
4495
|
+
await this.abortTask;
|
|
4496
|
+
this.options.store.runtime.recordSession(this.options.bot.id, this.sessionId, existsSync(this.sessionFile));
|
|
4497
|
+
}
|
|
4498
|
+
if (this.abortFailure) throw this.abortFailure;
|
|
4499
|
+
outcome = this.result.outcome;
|
|
4500
|
+
return this.result;
|
|
4281
4501
|
} finally {
|
|
4282
|
-
|
|
4283
|
-
if (this.started && !this.settled) await settled;
|
|
4284
|
-
this.promptActive = false;
|
|
4285
|
-
await this.abortTask;
|
|
4286
|
-
this.options.store.runtime.recordSession(this.options.bot.id, this.sessionId, existsSync(this.sessionFile));
|
|
4502
|
+
this.options.live?.({ type: "end", outcome });
|
|
4287
4503
|
}
|
|
4288
|
-
if (this.abortFailure) throw this.abortFailure;
|
|
4289
|
-
return this.result;
|
|
4290
4504
|
}
|
|
4291
4505
|
async configure(selection) {
|
|
4292
4506
|
const connection = this.options.store.getModelConnection(selection.connectionId), configured2 = connection?.models.find((item) => item.id === selection.modelId);
|
|
4293
|
-
if (!connection || !configured2) throw new RuntimeFault("model_missing", "
|
|
4507
|
+
if (!connection || !configured2) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
4294
4508
|
const signature = JSON.stringify([{ connectionId: selection.connectionId, modelId: selection.modelId }, connection.providerName, connection.api, connection.baseUrl, configured2, this.options.store.modelHeaders(connection.id, configured2.id)]);
|
|
4295
4509
|
if (signature === this.signature) {
|
|
4296
4510
|
this.session.setThinkingLevel(resolveThinking(configured2, selection.thinkingLevel), { persist: false });
|
|
@@ -4303,12 +4517,13 @@ var PiSession = class _PiSession {
|
|
|
4303
4517
|
this.credentials.current = next;
|
|
4304
4518
|
try {
|
|
4305
4519
|
await this.session.setModel(model, { persist: false });
|
|
4520
|
+
this.settings.applyOverrides({ compaction: { reserveTokens: compactionReserve(model.contextWindow) } });
|
|
4306
4521
|
this.session.setThinkingLevel(resolveThinking(model, selection.thinkingLevel), { persist: false });
|
|
4307
4522
|
this.signature = signature;
|
|
4308
4523
|
return this.session.thinkingLevel;
|
|
4309
4524
|
} catch {
|
|
4310
4525
|
this.credentials.current = previous;
|
|
4311
|
-
throw new RuntimeFault("model_switch_uncertain", "
|
|
4526
|
+
throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.", 409);
|
|
4312
4527
|
}
|
|
4313
4528
|
}
|
|
4314
4529
|
async dispose() {
|
|
@@ -4327,14 +4542,12 @@ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
|
4327
4542
|
import { dirname as dirname3 } from "node:path";
|
|
4328
4543
|
var json = (value2) => value2 == null ? null : JSON.stringify(value2);
|
|
4329
4544
|
var CALL = "EXISTS (SELECT 1 FROM json_each(history.message, '$.content') b WHERE json_extract(b.value, '$.type')=? AND json_extract(b.value, '$.tool_call_id')=?)";
|
|
4330
|
-
var
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
WHEN 'image' THEN '\u56FE\u7247' END, ' '),1,160)
|
|
4337
|
-
FROM json_each(history.message,'$.content') b), '') AS preview FROM history`;
|
|
4545
|
+
var BLOCKS = "FROM json_each(history.message,'$.content') b WHERE json_extract(b.value,'$.type')";
|
|
4546
|
+
var SUMMARY = `SELECT id,timestamp,role,model,
|
|
4547
|
+
COALESCE((SELECT substr(group_concat(substr(json_extract(b.value,'$.text'),1,160),' '),1,160) ${BLOCKS} IN ('text','reasoning')),'') AS preview,
|
|
4548
|
+
(SELECT json_group_array(json_object('kind',CASE json_extract(b.value,'$.type') WHEN 'tool_call' THEN 'call' ELSE 'result' END,
|
|
4549
|
+
'name',json_extract(b.value,'$.name'),'error',json_extract(b.value,'$.is_error'))) ${BLOCKS} IN ('tool_call','tool_result')) AS tools,
|
|
4550
|
+
(SELECT count(*) ${BLOCKS}='image') AS images FROM history`;
|
|
4338
4551
|
var HistoryRepository = class {
|
|
4339
4552
|
db;
|
|
4340
4553
|
constructor(path) {
|
|
@@ -4376,24 +4589,35 @@ var HistoryRepository = class {
|
|
|
4376
4589
|
return row ? { ...row, model: row.model === null ? null : JSON.parse(row.model), message: JSON.parse(row.message) } : null;
|
|
4377
4590
|
});
|
|
4378
4591
|
}
|
|
4592
|
+
/** Model calls since a time, per provider and model: every assistant record with usage is one call. */
|
|
4593
|
+
usage(since) {
|
|
4594
|
+
const rows2 = this.db.prepare(`SELECT json_extract(model,'$.provider') AS provider,json_extract(model,'$.id') AS model,count(*) AS calls,
|
|
4595
|
+
COALESCE(SUM(json_extract(usage,'$.input_tokens')),0) AS input,COALESCE(SUM(json_extract(usage,'$.output_tokens')),0) AS output,
|
|
4596
|
+
COALESCE(SUM(json_extract(usage,'$.cache_read_tokens')),0) AS cacheRead,COALESCE(SUM(json_extract(usage,'$.cache_write_tokens')),0) AS cacheWrite,
|
|
4597
|
+
COALESCE(SUM(json_extract(usage,'$.cost.total')),0) AS cost,COALESCE(MAX(json_extract(usage,'$.cost.total')),0)>0 AS priced
|
|
4598
|
+
FROM history WHERE role='assistant' AND usage IS NOT NULL AND model IS NOT NULL AND timestamp>=?
|
|
4599
|
+
GROUP BY 1,2 ORDER BY cost DESC,calls DESC`).all(since);
|
|
4600
|
+
return rows2.map(({ priced, cost, ...row }) => ({ ...row, cost: priced ? cost : null }));
|
|
4601
|
+
}
|
|
4379
4602
|
decodeSummary(row) {
|
|
4380
|
-
|
|
4603
|
+
const tools = JSON.parse(row.tools).map(({ kind, name: name2, error }) => error ? { kind, name: name2, error: true } : { kind, name: name2 });
|
|
4604
|
+
return { ...row, model: row.model === null ? null : JSON.parse(row.model), preview: row.preview.replace(/\s+/g, " ").trim(), tools };
|
|
4381
4605
|
}
|
|
4382
4606
|
read(id) {
|
|
4383
4607
|
const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
|
|
4384
4608
|
return row ? this.decode(row) : null;
|
|
4385
4609
|
}
|
|
4386
|
-
links(
|
|
4610
|
+
links(record2) {
|
|
4387
4611
|
const links = [];
|
|
4388
|
-
for (const block of
|
|
4612
|
+
for (const block of record2.message.content) {
|
|
4389
4613
|
if (block.type !== "tool_call" && block.type !== "tool_result") continue;
|
|
4390
4614
|
const callId = block.tool_call_id;
|
|
4391
4615
|
if (block.type === "tool_result") {
|
|
4392
|
-
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(
|
|
4616
|
+
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(record2.id, "tool_call", callId);
|
|
4393
4617
|
if (call) links.push({ ...call, tool_call_id: callId });
|
|
4394
4618
|
} else {
|
|
4395
|
-
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(
|
|
4396
|
-
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(
|
|
4619
|
+
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(record2.id, "tool_call", callId);
|
|
4620
|
+
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record2.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
|
|
4397
4621
|
links.push(...results.map((result) => ({ ...result, tool_call_id: callId })));
|
|
4398
4622
|
}
|
|
4399
4623
|
}
|
|
@@ -4408,6 +4632,11 @@ var HistoryRepository = class {
|
|
|
4408
4632
|
};
|
|
4409
4633
|
|
|
4410
4634
|
// src/server/service/bot/memory.ts
|
|
4635
|
+
function summaryLine({ tools, images, ...summary }) {
|
|
4636
|
+
const steps = tools.map((tool) => tool.kind === "call" ? `Called ${tool.name}` : `${tool.name} \xB7 ${tool.error ? "failed" : "result"}`);
|
|
4637
|
+
const line = [summary.preview, ...steps, ...Array.from({ length: images }, () => "image")].filter(Boolean).join(" ");
|
|
4638
|
+
return { ...summary, preview: line.slice(0, 160).replace(/\s+/g, " ").trim() };
|
|
4639
|
+
}
|
|
4411
4640
|
var Memory = class {
|
|
4412
4641
|
constructor(historyPath) {
|
|
4413
4642
|
this.historyPath = historyPath;
|
|
@@ -4428,15 +4657,18 @@ var Memory = class {
|
|
|
4428
4657
|
timeline(before, limit) {
|
|
4429
4658
|
return this.history().timeline(before, limit);
|
|
4430
4659
|
}
|
|
4660
|
+
usage(since) {
|
|
4661
|
+
return this.history().usage(since);
|
|
4662
|
+
}
|
|
4431
4663
|
read(id) {
|
|
4432
|
-
const
|
|
4433
|
-
return
|
|
4664
|
+
const record2 = this.history().read(id);
|
|
4665
|
+
return record2 ? { record: record2, links: this.history().links(record2) } : null;
|
|
4434
4666
|
}
|
|
4435
4667
|
readMany(ids, mode) {
|
|
4436
|
-
const found = mode === "summary" ? this.history().summaries(ids) : this.history().details(ids);
|
|
4668
|
+
const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
|
|
4437
4669
|
const result = { mode, records: [], missingIds: [] };
|
|
4438
|
-
found.forEach((
|
|
4439
|
-
if (
|
|
4670
|
+
found.forEach((record2, index) => {
|
|
4671
|
+
if (record2) result.records.push(record2);
|
|
4440
4672
|
else result.missingIds.push(ids[index]);
|
|
4441
4673
|
});
|
|
4442
4674
|
return result;
|
|
@@ -4564,17 +4796,17 @@ var Bot = class extends Member {
|
|
|
4564
4796
|
workspace: this.workspace.location(),
|
|
4565
4797
|
tools: this.tools.definitions(),
|
|
4566
4798
|
onStatus: (status) => this.updateState(status),
|
|
4567
|
-
onHistory: (
|
|
4799
|
+
onHistory: (record2) => this.recordHistory(record2)
|
|
4568
4800
|
};
|
|
4569
4801
|
}
|
|
4570
4802
|
updateState(status) {
|
|
4571
4803
|
this.state.update(status);
|
|
4572
4804
|
}
|
|
4573
|
-
async recordHistory(
|
|
4574
|
-
const historyId = await this.memory.append(
|
|
4575
|
-
if (
|
|
4805
|
+
async recordHistory(record2) {
|
|
4806
|
+
const historyId = await this.memory.append(record2);
|
|
4807
|
+
if (record2.role !== "assistant") return;
|
|
4576
4808
|
this.callHistory.clear();
|
|
4577
|
-
for (const block of
|
|
4809
|
+
for (const block of record2.message.content) {
|
|
4578
4810
|
if (block.type === "tool_call") this.callHistory.set(block.tool_call_id, historyId);
|
|
4579
4811
|
}
|
|
4580
4812
|
}
|
|
@@ -4612,7 +4844,7 @@ var Model = class {
|
|
|
4612
4844
|
}
|
|
4613
4845
|
current() {
|
|
4614
4846
|
const bot = this.store.getBot(this.botId);
|
|
4615
|
-
if (!bot.connectionId || !bot.modelId) throw new RuntimeFault("model_required", "
|
|
4847
|
+
if (!bot.connectionId || !bot.modelId) throw new RuntimeFault("model_required", "Pick a model for the Bot first.", 422);
|
|
4616
4848
|
return { selection: { connectionId: bot.connectionId, modelId: bot.modelId, thinkingLevel: bot.thinkingLevel }, credentialRef: bot.connectionId };
|
|
4617
4849
|
}
|
|
4618
4850
|
async prepare(input) {
|
|
@@ -4733,20 +4965,20 @@ ${content(block.result.content, heading + "#")}`;
|
|
|
4733
4965
|
function formatHistory(result, format) {
|
|
4734
4966
|
if (format === "json") return JSON.stringify(result, null, 2);
|
|
4735
4967
|
const sections = [`# History (${result.mode})`];
|
|
4736
|
-
for (const
|
|
4737
|
-
sections.push(`## #${
|
|
4738
|
-
if (
|
|
4968
|
+
for (const record2 of result.records) {
|
|
4969
|
+
sections.push(`## #${record2.id} \xB7 ${record2.role}`, `Time: ${new Date(record2.timestamp).toISOString()}`);
|
|
4970
|
+
if (record2.model) sections.push(`Model:
|
|
4739
4971
|
|
|
4740
|
-
${jsonBlock(
|
|
4741
|
-
if ("preview" in
|
|
4742
|
-
sections.push(fence(
|
|
4972
|
+
${jsonBlock(record2.model)}`);
|
|
4973
|
+
if ("preview" in record2) {
|
|
4974
|
+
sections.push(fence(record2.preview));
|
|
4743
4975
|
continue;
|
|
4744
4976
|
}
|
|
4745
|
-
sections.push(content(
|
|
4746
|
-
if (
|
|
4747
|
-
if (
|
|
4748
|
-
if (
|
|
4749
|
-
if (
|
|
4977
|
+
sections.push(content(record2.message.content));
|
|
4978
|
+
if (record2.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record2.message.tools));
|
|
4979
|
+
if (record2.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record2.message.tools_removed));
|
|
4980
|
+
if (record2.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record2.message.stop_reason));
|
|
4981
|
+
if (record2.message.error !== void 0) sections.push("### Error", fence(record2.message.error));
|
|
4750
4982
|
}
|
|
4751
4983
|
if (result.missingIds.length) sections.push(`## Missing IDs
|
|
4752
4984
|
|
|
@@ -4756,10 +4988,10 @@ ${result.missingIds.join(", ")}`);
|
|
|
4756
4988
|
|
|
4757
4989
|
// src/server/service/bot/history-read.ts
|
|
4758
4990
|
var historyReadSchema = Type2.Object({
|
|
4759
|
-
ids: Type2.Array(Type2.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }), { description: "
|
|
4760
|
-
mode: Type2.Optional(Type2.Union([Type2.Literal("summary"), Type2.Literal("detail")], { default: "summary", description: "summary
|
|
4991
|
+
ids: Type2.Array(Type2.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }), { description: "IDs in your own History, read in the order given." }),
|
|
4992
|
+
mode: Type2.Optional(Type2.Union([Type2.Literal("summary"), Type2.Literal("detail")], { default: "summary", description: "summary returns an excerpt of each record; detail returns the full message content, without usage or adapter extras." })),
|
|
4761
4993
|
format: Type2.Optional(Type2.Union([Type2.Literal("json"), Type2.Literal("markdown")], { default: "json" })),
|
|
4762
|
-
to_file: Type2.Optional(Type2.Boolean({ default: false, description: "
|
|
4994
|
+
to_file: Type2.Optional(Type2.Boolean({ default: false, description: "Write the result to your temporary directory and return only its path; false returns the content directly." }))
|
|
4763
4995
|
}, { additionalProperties: false });
|
|
4764
4996
|
async function readHistory(memory, workspace, input, signal) {
|
|
4765
4997
|
signal?.throwIfAborted();
|
|
@@ -4824,7 +5056,7 @@ function threadRow(chats, entry) {
|
|
|
4824
5056
|
status: entry.chat.status,
|
|
4825
5057
|
replies: entry.info.replies,
|
|
4826
5058
|
...entry.info.lastReplyAt ? { lastReplyAt: iso(entry.info.lastReplyAt) } : {},
|
|
4827
|
-
root: entry.root ? { id: entry.chat.rootMessageId, from: chats.member(entry.root.senderId)?.name ?? entry.root.senderId, preview: entry.root.content.slice(0, 200) } : null,
|
|
5059
|
+
root: entry.root ? { id: entry.chat.rootMessageId, from: chats.member(entry.root.senderId)?.name ?? entry.root.senderId, preview: previewQuotes(entry.root.content, entry.root.quotes).slice(0, 200) } : null,
|
|
4828
5060
|
following: entry.chat.following,
|
|
4829
5061
|
subscribed: entry.chat.subscribed,
|
|
4830
5062
|
unread: entry.chat.unread
|
|
@@ -4843,7 +5075,7 @@ function messageRow(chats, botId, message, attachments, chat) {
|
|
|
4843
5075
|
...message.mentions.length ? { mentions: message.mentions.map((id) => chats.member(id)?.name ?? id) } : {},
|
|
4844
5076
|
...message.thread ? { thread: { id: message.thread.id, title: message.thread.title, status: message.thread.status, replies: message.thread.replies } } : {},
|
|
4845
5077
|
...message.echoOf ? { fromThread: message.echoOf } : {},
|
|
4846
|
-
...message.replyTo ? { replyTo: { id: message.replyTo.id, from: chats.member(message.replyTo.senderId)?.name ?? message.replyTo.senderId, text: message.replyTo
|
|
5078
|
+
...message.replyTo ? { replyTo: { id: message.replyTo.id, from: chats.member(message.replyTo.senderId)?.name ?? message.replyTo.senderId, text: replyExcerpt(message.replyTo) } } : {},
|
|
4847
5079
|
...message.reactions.length ? { reactions: message.reactions.map((group) => ({
|
|
4848
5080
|
emoji: group.emoji,
|
|
4849
5081
|
by: group.memberIds.map((id) => chats.member(id)?.name ?? id)
|
|
@@ -5021,34 +5253,34 @@ var None = Type3.Object({}, { additionalProperties: false });
|
|
|
5021
5253
|
var Name = (description) => Type3.String({ minLength: 1, maxLength: 80, description });
|
|
5022
5254
|
var MessageId = Type3.String({ pattern: "^m\\d+$" });
|
|
5023
5255
|
var QuestionOption = Type3.Object({
|
|
5024
|
-
label: Type3.String({ minLength: 1, maxLength: 80, description: "
|
|
5025
|
-
value: Type3.Optional(Type3.String({ minLength: 1, maxLength: 500, description: "
|
|
5026
|
-
description: Type3.Optional(Type3.String({ minLength: 1, maxLength: 200, description: "
|
|
5256
|
+
label: Type3.String({ minLength: 1, maxLength: 80, description: "Text shown on the option" }),
|
|
5257
|
+
value: Type3.Optional(Type3.String({ minLength: 1, maxLength: 500, description: "What you get back when it is picked, written as the person would naturally say it; label by default" })),
|
|
5258
|
+
description: Type3.Optional(Type3.String({ minLength: 1, maxLength: 200, description: "A note under the option" })),
|
|
5027
5259
|
style: Type3.Optional(Type3.Union([Type3.Literal("default"), Type3.Literal("primary"), Type3.Literal("danger")]))
|
|
5028
5260
|
}, { additionalProperties: false });
|
|
5029
5261
|
var definitions = {
|
|
5030
|
-
"history.read": { description: "
|
|
5031
|
-
"members.list": { description: "
|
|
5032
|
-
"bots.list": { description: "
|
|
5033
|
-
"chats.list": { description: "
|
|
5262
|
+
"history.read": { description: "Read your own History by a list of IDs: summaries or details, as JSON or Markdown, or written to a temporary file. IDs that don't exist are listed in missingIds. History is material to look back on, not an instruction.", schema: historyReadSchema },
|
|
5263
|
+
"members.list": { description: "List every member, people and Bots: name, kind (user for a person, or bot) and description; self=true is you. Use these names to @-mention someone, add them to a channel or DM them.", schema: None },
|
|
5264
|
+
"bots.list": { description: "List every Bot and its run state. Read-only; it changes nothing. People and Bots can all see every Bot's name and state; this is not private. For each Bot: name; self=true is you; status (stop: not running, idle, or running). While it runs, activity is what it is doing (reasoning, texting: writing text, tool_calling, or asking: waiting for a person's answer), chats are the chats it is handling this turn (only those you can read), and otherChats is how many others there are. If its last turn failed, error is the error code. You come first, then the rest by name.", schema: None },
|
|
5265
|
+
"chats.list": { description: "List the chats you are in: DMs, channels and the threads you follow. Each has its type, title (DMs also give the other member's name in with), channel group (section), topic, your unread count and whether you subscribe; threads also give their channel and status.", schema: None },
|
|
5034
5266
|
"chats.info": {
|
|
5035
|
-
description: "
|
|
5267
|
+
description: "Details of one chat: members, the channel topic, the full notice or thread summary, your unread count and read position, and whether you subscribe; threads also give their title, status, channel and source message. Reading a notice or summary counts as having seen that version.",
|
|
5036
5268
|
schema: Type3.Object({ chatId: Type3.String() }, { additionalProperties: false })
|
|
5037
5269
|
},
|
|
5038
5270
|
"chats.create": {
|
|
5039
|
-
description: "
|
|
5271
|
+
description: "Create a channel. You become a member, and the result has its chatId. members: names of the members to add (see members.list). section: the name of an existing channel group (see sections.list); leave it out for no group.",
|
|
5040
5272
|
schema: Type3.Object({
|
|
5041
|
-
name: Name("
|
|
5273
|
+
name: Name("Channel name"),
|
|
5042
5274
|
topic: Type3.Optional(Type3.String({ maxLength: 200 })),
|
|
5043
5275
|
section: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
5044
5276
|
members: Type3.Optional(Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { maxItems: 50 }))
|
|
5045
5277
|
}, { additionalProperties: false })
|
|
5046
5278
|
},
|
|
5047
5279
|
"chats.update": {
|
|
5048
|
-
description: "
|
|
5280
|
+
description: "Change a chat you are in. Channels: name, topic, section (a channel group's name; an empty string takes it out of its group) and notice. Threads: title, status (open or done) and notice (the summary). notice is Markdown and replaces the whole text.",
|
|
5049
5281
|
schema: Type3.Object({
|
|
5050
5282
|
chatId: Type3.String(),
|
|
5051
|
-
name: Type3.Optional(Name("
|
|
5283
|
+
name: Type3.Optional(Name("Channel name")),
|
|
5052
5284
|
topic: Type3.Optional(Type3.String({ maxLength: 200 })),
|
|
5053
5285
|
section: Type3.Optional(Type3.String({ maxLength: 80 })),
|
|
5054
5286
|
notice: Type3.Optional(Type3.String({ minLength: 1, maxLength: 2e4 })),
|
|
@@ -5057,29 +5289,29 @@ var definitions = {
|
|
|
5057
5289
|
}, { additionalProperties: false })
|
|
5058
5290
|
},
|
|
5059
5291
|
"chats.addMembers": {
|
|
5060
|
-
description: "
|
|
5292
|
+
description: "Add members to a channel you are in, by name (see members.list). Only people can remove members from a channel or delete channels and channel groups.",
|
|
5061
5293
|
schema: Type3.Object({ chatId: Type3.String(), members: Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }) }, { additionalProperties: false })
|
|
5062
5294
|
},
|
|
5063
5295
|
"chats.subscribe": {
|
|
5064
|
-
description: "
|
|
5296
|
+
description: "Subscribe to a channel or thread, or stop. Normally you only get new messages in channels and threads when you are @-mentioned. While you subscribe, every new message there (with or without an @, whoever it mentions, from a person or a Bot) is sent to you at once and interrupts what you are doing. Subscribe only while you need to follow something live, and stop when you are done; you don't have to reply to every message you get this way. Subscribing to a channel doesn't include its threads; subscribing to a thread also follows it.",
|
|
5065
5297
|
schema: Type3.Object({ chatId: Type3.String(), subscribed: Type3.Boolean() }, { additionalProperties: false })
|
|
5066
5298
|
},
|
|
5067
5299
|
"threads.list": {
|
|
5068
|
-
description: "
|
|
5069
|
-
schema: Type3.Object({ chatId: Type3.String({ description: "
|
|
5300
|
+
description: "List a channel's threads: title, status, reply count, time of the last reply, source message, and whether you follow or subscribe. status is open by default; done and all also work.",
|
|
5301
|
+
schema: Type3.Object({ chatId: Type3.String({ description: "Channel ID" }), status: Type3.Optional(Type3.Union([Type3.Literal("open"), Type3.Literal("done"), Type3.Literal("all")])) }, { additionalProperties: false })
|
|
5070
5302
|
},
|
|
5071
5303
|
"threads.follow": {
|
|
5072
|
-
description: "
|
|
5073
|
-
schema: Type3.Object({ chatId: Type3.String({ description: "
|
|
5304
|
+
description: "Follow or unfollow a thread. Followed threads appear in chats.list, and their new replies count as unread for you without interrupting you (use chats.subscribe to get them live). Posting in a thread or being @-mentioned in it follows it for you.",
|
|
5305
|
+
schema: Type3.Object({ chatId: Type3.String({ description: "Thread ID" }), following: Type3.Boolean() }, { additionalProperties: false })
|
|
5074
5306
|
},
|
|
5075
|
-
"sections.list": { description: "
|
|
5076
|
-
"sections.create": { description: "
|
|
5307
|
+
"sections.list": { description: "List the channel groups (sections) with their IDs and names.", schema: None },
|
|
5308
|
+
"sections.create": { description: "Create a channel group.", schema: Type3.Object({ name: Name("Group name") }, { additionalProperties: false }) },
|
|
5077
5309
|
"sections.update": {
|
|
5078
|
-
description: "
|
|
5079
|
-
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("
|
|
5310
|
+
description: "Rename a channel group. section: its current name or ID.",
|
|
5311
|
+
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("New name") }, { additionalProperties: false })
|
|
5080
5312
|
},
|
|
5081
5313
|
"messages.read": {
|
|
5082
|
-
description: "\
|
|
5314
|
+
description: "Read messages and the paths of the attachments you can read. Three ways: \u2460 chatId + after: read on from that sequence number (from the start by default); \u2461 around with a message ID: that message and context messages on each side (5 by default), to see the context; this doesn't move your read position; \u2462 unread=true: your unread messages, in one chat with chatId, or in every chat (grouped by chat) without it. \u2460 and \u2462 move your read position to the last message read; hasMore=true means there is more, so call again.",
|
|
5083
5315
|
schema: Type3.Object({
|
|
5084
5316
|
chatId: Type3.Optional(Type3.String()),
|
|
5085
5317
|
after: Type3.Optional(Type3.Integer({ minimum: 0 })),
|
|
@@ -5090,15 +5322,15 @@ var definitions = {
|
|
|
5090
5322
|
}, { additionalProperties: false })
|
|
5091
5323
|
},
|
|
5092
5324
|
"messages.search": {
|
|
5093
|
-
description: "
|
|
5325
|
+
description: "Search message text. With chatId, only that chat; without it, every chat you can see, and each result says which chat it is in. Use messages.read to read attachments.",
|
|
5094
5326
|
schema: Type3.Object({ chatId: Type3.Optional(Type3.String()), query: Type3.String({ minLength: 1, maxLength: 500 }), limit: Type3.Optional(Type3.Integer({ minimum: 1, maximum: 100 })) }, { additionalProperties: false })
|
|
5095
5327
|
},
|
|
5096
5328
|
"messages.react": {
|
|
5097
|
-
description:
|
|
5098
|
-
schema: Type3.Object({ messageId: MessageId, emoji: Type3.String({ minLength: 1, maxLength: 32, description: "
|
|
5329
|
+
description: `Add or remove an emoji reaction on a message (people and Bots can both react). on=false removes it. Giving an emoji the message already has from you removes it instead (and the other way round); marked or unmarked and note in the result say which happened. Use this toggle to signal states such as "got it", "on it" and "done". Only the message's author is told: if they are running, it joins their current turn without interrupting; if they are idle, it starts a turn.`,
|
|
5330
|
+
schema: Type3.Object({ messageId: MessageId, emoji: Type3.String({ minLength: 1, maxLength: 32, description: "A single emoji, such as \u{1F44D}" }), on: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false })
|
|
5099
5331
|
},
|
|
5100
5332
|
"messages.ask": {
|
|
5101
|
-
description: '
|
|
5333
|
+
description: `Post a multiple-choice card in a chat you are in and wait for the person's answer. It returns {status:"answered", selected:[{label,value}], custom}, or {status:"dismissed"} if they closed the question without answering. Use it when someone needs to choose or confirm (a plan, a preference, whether to go on with something risky); ask open questions with messages.send. options: 1\u20136; multiple allows several choices; allowCustom lets them write their own answer (custom). If, while you wait, the person sends a new message, cancels this turn or stops the Bot, the call is interrupted and the new message arrives in the next batch. Only people can answer, so it can't be used in chats without one (such as a DM between Bots). Never use it to ask for passwords or keys.`,
|
|
5102
5334
|
schema: Type3.Object({
|
|
5103
5335
|
chatId: Type3.String(),
|
|
5104
5336
|
question: Type3.String({ minLength: 1, maxLength: 500 }),
|
|
@@ -5109,7 +5341,7 @@ var definitions = {
|
|
|
5109
5341
|
}, { additionalProperties: false })
|
|
5110
5342
|
},
|
|
5111
5343
|
"messages.send": {
|
|
5112
|
-
description: "
|
|
5344
|
+
description: "Send a message, with files from the tool host if you like (path and name). Give chatId or to, not both: to is a member's name and sends them a DM (a person or a Bot; the first time creates the DM). Only messages sent successfully are visible in the chat. In channels and threads, only the members whose @name is in the text and the members subscribed there receive it; DMs need no @. The result lists who was @-mentioned (notified), who got it through a subscription (subscribers), and the names that aren't in the channel and weren't told. thread: the ID of a message in the channel, to reply in that message's thread (created if it has none; chatId is the channel). replyTo: the ID of a message in the same chat, to reply to it as a quote; this tells its author, like an @. When replying in a thread, alsoToChannel=true also posts a copy to the channel.",
|
|
5113
5345
|
schema: Type3.Object({
|
|
5114
5346
|
chatId: Type3.Optional(Type3.String()),
|
|
5115
5347
|
to: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
@@ -5166,7 +5398,7 @@ var Tools = class {
|
|
|
5166
5398
|
definitions() {
|
|
5167
5399
|
return [{
|
|
5168
5400
|
name: "chat",
|
|
5169
|
-
description: "
|
|
5401
|
+
description: "Gateway to the chat and to your own History. Use list to see the tools, describe to check a tool's parameters, then call to run it. Only what messages.send or messages.ask sends successfully is visible in the chat.",
|
|
5170
5402
|
parameters: Gateway,
|
|
5171
5403
|
execute: async (raw, { toolCallId, signal }) => {
|
|
5172
5404
|
if (!Value.Check(Gateway, raw)) throw new ChatError(400, "Invalid chat tool request");
|
|
@@ -5221,7 +5453,7 @@ var Tools = class {
|
|
|
5221
5453
|
messageId: message.id,
|
|
5222
5454
|
emoji: parsed.emoji,
|
|
5223
5455
|
[applied ? "marked" : "unmarked"]: true,
|
|
5224
|
-
note: applied ? "
|
|
5456
|
+
note: applied ? "Reaction added" : "You already had this reaction, so this removed it",
|
|
5225
5457
|
reactions: message.reactions.map((group) => ({ emoji: group.emoji, count: group.memberIds.length }))
|
|
5226
5458
|
};
|
|
5227
5459
|
}
|
|
@@ -5321,7 +5553,7 @@ function noticeTail(notices, chats, members) {
|
|
|
5321
5553
|
for (const notice of notices) if (notice.kind === "note" && (latest.get(notice.chat.id)?.version ?? 0) < notice.version) latest.set(notice.chat.id, notice);
|
|
5322
5554
|
const updates = [...latest.values()].map((note) => `<${note.chat.type === "thread" ? "summary" : "notice"}${attribute("chat", note.chat.id)}${attribute("version", String(note.version))}${attribute("by", note.actor.id)}${attribute("at", iso2(note.createdAt))}/>`);
|
|
5323
5555
|
const marks = notices.flatMap((notice) => notice.kind !== "reaction" ? [] : [`<reaction${attribute("message", notice.message.id)}${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("emoji", notice.emoji)}${attribute("at", iso2(notice.createdAt))}>${escape(notice.message.text)}</reaction>`]);
|
|
5324
|
-
return (updates.length ? `<updates>${updates.join("")}</updates>` : "") + (marks.length ? `<notices>${marks.join("")}<note
|
|
5556
|
+
return (updates.length ? `<updates>${updates.join("")}</updates>` : "") + (marks.length ? `<notices>${marks.join("")}<note>These are reactions to your messages. Act on them if you need to; no reply is required.</note></notices>` : "");
|
|
5325
5557
|
}
|
|
5326
5558
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5327
5559
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5368,7 +5600,7 @@ var BotInbox = class {
|
|
|
5368
5600
|
return this.chats.notices(this.botId).length > 0;
|
|
5369
5601
|
}
|
|
5370
5602
|
reference(workspace) {
|
|
5371
|
-
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "
|
|
5603
|
+
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
5372
5604
|
return this.repository.ensureSession(this.botId, join5(dirname4(workspace), "agent", "pi", "sessions", `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`));
|
|
5373
5605
|
}
|
|
5374
5606
|
/** XML for the claimed messages plus the tail (other deliveries not told yet, unread); `notices` are told once
|
|
@@ -5379,7 +5611,7 @@ var BotInbox = class {
|
|
|
5379
5611
|
return pending2 && { xml: pending2.xml, notices: pending2.ids };
|
|
5380
5612
|
}
|
|
5381
5613
|
const inputs = await this.files.envelope(this.botId, batch.inputs.map((input) => input.messageId));
|
|
5382
|
-
for (const input of batch.inputs) if (!this.chats.get(this.botId, input.chatId)) throw new RuntimeFault("chat_access_changed", "
|
|
5614
|
+
for (const input of batch.inputs) if (!this.chats.get(this.botId, input.chatId)) throw new RuntimeFault("chat_access_changed", "Chat access has changed.");
|
|
5383
5615
|
const digest = this.chats.channels.digest(this.botId);
|
|
5384
5616
|
const pending = this.envelope(this.chats.notices(this.botId));
|
|
5385
5617
|
return { xml: encodeChatBatch(this.botId, inputs, digest, pending.length ? pending : void 0), notices: pending.map((notice) => notice.id) };
|
|
@@ -5448,7 +5680,7 @@ var AgentRuntime = class {
|
|
|
5448
5680
|
}
|
|
5449
5681
|
fail(error) {
|
|
5450
5682
|
const fault = publicFault(error);
|
|
5451
|
-
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault.code, message: fault.message } });
|
|
5683
|
+
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault.code, message: fault.message, ...fault.params ? { params: fault.params } : {} } });
|
|
5452
5684
|
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code: fault.code, ...error instanceof RuntimeFault ? {} : { error } });
|
|
5453
5685
|
}
|
|
5454
5686
|
async start(setup) {
|
|
@@ -5549,7 +5781,10 @@ var AgentRuntime = class {
|
|
|
5549
5781
|
this.promptTask = session.prompt(xml);
|
|
5550
5782
|
const result = await this.promptTask;
|
|
5551
5783
|
repository.finish(batch.run.id, result.outcome, result.outcome === "failed" ? "model_request_failed" : null);
|
|
5552
|
-
if (result.outcome === "failed")
|
|
5784
|
+
if (result.outcome === "failed") {
|
|
5785
|
+
const detail = result.error.replace(/\s+/g, " ").slice(0, 300);
|
|
5786
|
+
throw new RuntimeFault("model_request_failed", `Model request failed: ${detail}`, 502, { detail });
|
|
5787
|
+
}
|
|
5553
5788
|
} catch (error) {
|
|
5554
5789
|
const run = repository.run(batch.run.id);
|
|
5555
5790
|
if (run?.state === "claimed" || run?.state === "submitted") repository.finish(batch.run.id, submitted ? "unknown" : "failed", publicFault(error).code);
|
|
@@ -5575,7 +5810,7 @@ var AgentRuntime = class {
|
|
|
5575
5810
|
reconfigure(operation) {
|
|
5576
5811
|
const task = this.operations.catch(() => {
|
|
5577
5812
|
}).then(async () => {
|
|
5578
|
-
if (this.stopping) throw new RuntimeFault("stopping", "
|
|
5813
|
+
if (this.stopping) throw new RuntimeFault("stopping", "Shutting down.");
|
|
5579
5814
|
await operation();
|
|
5580
5815
|
});
|
|
5581
5816
|
this.operations = task;
|
|
@@ -5586,7 +5821,7 @@ var AgentRuntime = class {
|
|
|
5586
5821
|
this.pauses++;
|
|
5587
5822
|
const task = this.operations.catch(() => {
|
|
5588
5823
|
}).then(async () => {
|
|
5589
|
-
if (this.stopping) throw new RuntimeFault("stopping", "
|
|
5824
|
+
if (this.stopping) throw new RuntimeFault("stopping", "Shutting down.");
|
|
5590
5825
|
await this.pump;
|
|
5591
5826
|
await operation();
|
|
5592
5827
|
});
|
|
@@ -5636,65 +5871,323 @@ var AgentRuntime = class {
|
|
|
5636
5871
|
function botPrompt(setup) {
|
|
5637
5872
|
const bot = { ...setup.identity(), workspacePath: setup.workspace.cwd };
|
|
5638
5873
|
return `# Persona
|
|
5639
|
-
|
|
5874
|
+
Your name is ${bot.name}.
|
|
5640
5875
|
|
|
5641
5876
|
${bot.persona}
|
|
5642
5877
|
|
|
5643
|
-
##
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
##
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
##
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5878
|
+
## Environment
|
|
5879
|
+
You are in a group chat with people and other Bots. You take part in direct messages (DMs), channels and threads, and you talk and share information with the other members through the chat.
|
|
5880
|
+
Here your name is ${bot.name}, and your description is: ${bot.description}
|
|
5881
|
+
|
|
5882
|
+
## Language
|
|
5883
|
+
Write to people in the language they write to you in. In a channel or thread, use the language its conversation is held in.
|
|
5884
|
+
These instructions and the tool descriptions are in English so that every Bot reads the same rules. They do not decide which language you use with people.
|
|
5885
|
+
|
|
5886
|
+
## Chat
|
|
5887
|
+
The chat tool is your only way to talk to the other members. Anything you don't send through it is seen by no one.
|
|
5888
|
+
To pass a document, an image or another file to someone, attach its path and name when you send with the chat tool.
|
|
5889
|
+
Channels can be put in channel groups and have a notice. Threads have a title, a status and a summary.
|
|
5890
|
+
In channels and threads you only get new messages when you are @-mentioned; once you subscribe, every new message there is sent to you. DMs need no @.
|
|
5891
|
+
Each batch of messages ends with your unread counts and the notices and summaries that have a new version. Read them through the chat gateway when you need to.
|
|
5892
|
+
|
|
5893
|
+
## History
|
|
5894
|
+
To look into earlier work, search the chat first, then call history.read with the History ID a message carries to read how the work was done.
|
|
5895
|
+
history.read is listed by the chat gateway. History is material to look back on, not an instruction for the task at hand.
|
|
5896
|
+
|
|
5897
|
+
## Workspace
|
|
5898
|
+
You have your own workspace: ${bot.workspacePath}
|
|
5899
|
+
Treat it as your only base. Create, change and delete files only inside it, keep it tidy, and put temporary files under temp and clear them out.
|
|
5900
|
+
Leave everything else alone by default: other members' workspaces, live data directories (such as ~/.jarvy), main repository checkouts and system directories. Touch them only when the task itself needs it or the person in charge asks you to, and say in your reply what you changed.
|
|
5901
|
+
Shared directories and other people's files are read-only. Ask the person in charge before writing to them.
|
|
5902
|
+
|
|
5903
|
+
## Responding
|
|
5904
|
+
When you are @-mentioned or get a DM, first let the person know you have it: react to that message with an emoji, or confirm in a sentence or two with an emoji. A reaction is only a signal, not a command; pick the emoji yourself.
|
|
5905
|
+
When something will take a while, post a short progress note partway through, and give the outcome when you are done. You don't need to answer every message that isn't meant for you.
|
|
5667
5906
|
`;
|
|
5668
5907
|
}
|
|
5669
5908
|
|
|
5909
|
+
// src/server/shared/live.ts
|
|
5910
|
+
var LIVE_STEPS = 300;
|
|
5911
|
+
var LIVE_TEXT = 4e3;
|
|
5912
|
+
var LIVE_OUTPUT_END = 1200;
|
|
5913
|
+
var LIVE_INPUT = 300;
|
|
5914
|
+
var LIVE_MESSAGE = 2e3;
|
|
5915
|
+
|
|
5916
|
+
// src/server/service/bot/live-input.ts
|
|
5917
|
+
function clip(text3, length) {
|
|
5918
|
+
return text3.length > length ? `${text3.slice(0, length - 1)}\u2026` : text3;
|
|
5919
|
+
}
|
|
5920
|
+
var field = (value2, length = LIVE_INPUT) => typeof value2 === "string" ? clip(value2, length) : void 0;
|
|
5921
|
+
var record = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
|
|
5922
|
+
function liveInput(tool, args, chat) {
|
|
5923
|
+
const values = record(args);
|
|
5924
|
+
switch (tool) {
|
|
5925
|
+
case "bash":
|
|
5926
|
+
return { command: field(values.command) };
|
|
5927
|
+
case "read":
|
|
5928
|
+
case "edit":
|
|
5929
|
+
case "write":
|
|
5930
|
+
return { path: field(values.path) };
|
|
5931
|
+
case "codemode":
|
|
5932
|
+
return { command: field(values.code) };
|
|
5933
|
+
case "chat":
|
|
5934
|
+
return gateway(values, chat);
|
|
5935
|
+
default: {
|
|
5936
|
+
let json2 = "";
|
|
5937
|
+
try {
|
|
5938
|
+
json2 = JSON.stringify(args) ?? "";
|
|
5939
|
+
} catch {
|
|
5940
|
+
}
|
|
5941
|
+
return json2 ? { args: clip(json2, LIVE_INPUT) } : {};
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
function gateway(values, chat) {
|
|
5946
|
+
const action = values.action === "call" && typeof values.tool === "string" ? values.tool : typeof values.action === "string" ? values.action : "";
|
|
5947
|
+
const inner = record(values.args), where = target(inner, chat);
|
|
5948
|
+
if (action === "messages.send") return { action, chat: where, text: field(inner.message, LIVE_MESSAGE) };
|
|
5949
|
+
if (action === "messages.ask") return {
|
|
5950
|
+
action,
|
|
5951
|
+
chat: where,
|
|
5952
|
+
text: field(inner.question, LIVE_MESSAGE),
|
|
5953
|
+
options: Array.isArray(inner.options) ? inner.options.map((option2) => field(record(option2).label) ?? "").filter(Boolean) : void 0
|
|
5954
|
+
};
|
|
5955
|
+
return where ? { action, chat: where } : { action };
|
|
5956
|
+
}
|
|
5957
|
+
function target(inner, chat) {
|
|
5958
|
+
if (typeof inner.to === "string") return { id: null, type: "dm", title: clip(inner.to, 80) };
|
|
5959
|
+
return typeof inner.chatId === "string" ? chat(inner.chatId) ?? void 0 : void 0;
|
|
5960
|
+
}
|
|
5961
|
+
function sentChat(output, chat) {
|
|
5962
|
+
try {
|
|
5963
|
+
const chatId = JSON.parse(output).chatId;
|
|
5964
|
+
return typeof chatId === "string" ? chat(chatId) ?? void 0 : void 0;
|
|
5965
|
+
} catch {
|
|
5966
|
+
return void 0;
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
|
|
5970
|
+
// src/server/service/bot/live.ts
|
|
5971
|
+
var FLUSH_MS = 100;
|
|
5972
|
+
function liveOutput(text3) {
|
|
5973
|
+
if (!text3) return null;
|
|
5974
|
+
if (text3.length <= 2 * LIVE_OUTPUT_END) return { head: text3, tail: "", omitted: 0 };
|
|
5975
|
+
return { head: text3.slice(0, LIVE_OUTPUT_END), tail: text3.slice(-LIVE_OUTPUT_END), omitted: text3.length - 2 * LIVE_OUTPUT_END };
|
|
5976
|
+
}
|
|
5977
|
+
var LiveTurns = class {
|
|
5978
|
+
constructor(chat) {
|
|
5979
|
+
this.chat = chat;
|
|
5980
|
+
}
|
|
5981
|
+
turn = null;
|
|
5982
|
+
steps = /* @__PURE__ */ new Map();
|
|
5983
|
+
blocks = /* @__PURE__ */ new Map();
|
|
5984
|
+
compaction = null;
|
|
5985
|
+
retry = null;
|
|
5986
|
+
counter = 0;
|
|
5987
|
+
pending = [];
|
|
5988
|
+
timer = null;
|
|
5989
|
+
listeners = /* @__PURE__ */ new Map();
|
|
5990
|
+
/** Listens from now on and returns the turn so far (null when the Bot is not running one). Pending changes go to
|
|
5991
|
+
* the earlier listeners first, so the snapshot and the next patch never overlap. */
|
|
5992
|
+
subscribe(listener, close) {
|
|
5993
|
+
this.flush();
|
|
5994
|
+
this.listeners.set(listener, close);
|
|
5995
|
+
return { turn: this.turn ? structuredClone(this.turn) : null, unsubscribe: () => {
|
|
5996
|
+
this.listeners.delete(listener);
|
|
5997
|
+
} };
|
|
5998
|
+
}
|
|
5999
|
+
/** Ends every listener's stream (the Bot was deleted, or the server is stopping). */
|
|
6000
|
+
close() {
|
|
6001
|
+
for (const close of [...this.listeners.values()]) close();
|
|
6002
|
+
this.listeners.clear();
|
|
6003
|
+
}
|
|
6004
|
+
event(event) {
|
|
6005
|
+
if (event.type === "start") {
|
|
6006
|
+
this.flush();
|
|
6007
|
+
this.reset();
|
|
6008
|
+
this.turn = { startedAt: Date.now(), steps: [], dropped: 0 };
|
|
6009
|
+
this.emit({ type: "start", turn: structuredClone(this.turn) });
|
|
6010
|
+
return;
|
|
6011
|
+
}
|
|
6012
|
+
if (!this.turn) return;
|
|
6013
|
+
switch (event.type) {
|
|
6014
|
+
case "end": {
|
|
6015
|
+
for (const step of this.turn.steps) if (step.state === "running") this.finish(step, event.outcome !== "completed");
|
|
6016
|
+
this.flush();
|
|
6017
|
+
this.emit({ type: "end", outcome: event.outcome, endedAt: Date.now() });
|
|
6018
|
+
this.reset();
|
|
6019
|
+
return;
|
|
6020
|
+
}
|
|
6021
|
+
case "block_start":
|
|
6022
|
+
this.blocks.set(event.key, this.add({ kind: event.kind, text: "", omitted: 0 }).id);
|
|
6023
|
+
return;
|
|
6024
|
+
case "block_delta": {
|
|
6025
|
+
let id = this.blocks.get(event.key);
|
|
6026
|
+
if (!id) {
|
|
6027
|
+
id = this.add({ kind: event.kind, text: "", omitted: 0 }).id;
|
|
6028
|
+
this.blocks.set(event.key, id);
|
|
6029
|
+
}
|
|
6030
|
+
const step = this.steps.get(id);
|
|
6031
|
+
if (!step) return;
|
|
6032
|
+
const text3 = (step.text ?? "") + event.delta, over = Math.max(0, text3.length - LIVE_TEXT);
|
|
6033
|
+
step.text = text3.slice(over);
|
|
6034
|
+
step.omitted = (step.omitted ?? 0) + over;
|
|
6035
|
+
this.queue({ op: "text", id, append: event.delta });
|
|
6036
|
+
return;
|
|
6037
|
+
}
|
|
6038
|
+
case "block_end": {
|
|
6039
|
+
const step = this.steps.get(this.blocks.get(event.key) ?? "");
|
|
6040
|
+
if (step) this.finish(step, false);
|
|
6041
|
+
this.blocks.delete(event.key);
|
|
6042
|
+
return;
|
|
6043
|
+
}
|
|
6044
|
+
case "tool_start":
|
|
6045
|
+
this.add({ id: event.id, kind: "tool", parentId: event.parentId, tool: event.name, input: liveInput(event.name, event.args, this.chat) });
|
|
6046
|
+
return;
|
|
6047
|
+
case "tool_end": {
|
|
6048
|
+
const step = this.steps.get(event.id);
|
|
6049
|
+
if (!step) return;
|
|
6050
|
+
step.output = liveOutput(event.output);
|
|
6051
|
+
if (step.input?.action === "messages.send" && !event.failed) step.input.chat = sentChat(event.output, this.chat) ?? step.input.chat;
|
|
6052
|
+
this.finish(step, event.failed);
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
case "compaction_start":
|
|
6056
|
+
this.compaction = this.add({ kind: "compaction" }).id;
|
|
6057
|
+
return;
|
|
6058
|
+
case "compaction_end": {
|
|
6059
|
+
const step = this.steps.get(this.compaction ?? "");
|
|
6060
|
+
this.compaction = null;
|
|
6061
|
+
if (!step) return;
|
|
6062
|
+
step.tokensBefore = event.tokensBefore;
|
|
6063
|
+
step.tokensAfter = event.tokensAfter;
|
|
6064
|
+
step.output = event.error ? liveOutput(event.error) : null;
|
|
6065
|
+
this.finish(step, event.failed);
|
|
6066
|
+
return;
|
|
6067
|
+
}
|
|
6068
|
+
case "retry_start": {
|
|
6069
|
+
const previous = this.steps.get(this.retry ?? "");
|
|
6070
|
+
if (previous?.state === "running") this.finish(previous, true);
|
|
6071
|
+
this.retry = this.add({ kind: "retry", attempt: event.attempt, maxAttempts: event.maxAttempts, output: liveOutput(event.error) }).id;
|
|
6072
|
+
return;
|
|
6073
|
+
}
|
|
6074
|
+
case "retry_end": {
|
|
6075
|
+
const step = this.steps.get(this.retry ?? "");
|
|
6076
|
+
this.retry = null;
|
|
6077
|
+
if (step) {
|
|
6078
|
+
if (event.error) step.output = liveOutput(event.error);
|
|
6079
|
+
this.finish(step, event.failed);
|
|
6080
|
+
}
|
|
6081
|
+
return;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
add(fields) {
|
|
6086
|
+
const turn = this.turn;
|
|
6087
|
+
if (turn.steps.length >= LIVE_STEPS) {
|
|
6088
|
+
const index = Math.max(0, turn.steps.findIndex((step2) => step2.state !== "running")), [gone] = turn.steps.splice(index, 1);
|
|
6089
|
+
this.steps.delete(gone.id);
|
|
6090
|
+
turn.dropped++;
|
|
6091
|
+
this.queue({ op: "drop", id: gone.id });
|
|
6092
|
+
}
|
|
6093
|
+
const step = { id: `s${++this.counter}`, state: "running", startedAt: Date.now(), endedAt: null, parentId: null, ...fields };
|
|
6094
|
+
turn.steps.push(step);
|
|
6095
|
+
this.steps.set(step.id, step);
|
|
6096
|
+
this.queue({ op: "step", step: structuredClone(step) });
|
|
6097
|
+
return step;
|
|
6098
|
+
}
|
|
6099
|
+
finish(step, failed) {
|
|
6100
|
+
step.state = failed ? "failed" : "ok";
|
|
6101
|
+
step.endedAt = Date.now();
|
|
6102
|
+
this.queue({ op: "step", step: structuredClone(step) });
|
|
6103
|
+
}
|
|
6104
|
+
queue(patch) {
|
|
6105
|
+
this.pending.push(patch);
|
|
6106
|
+
this.timer ??= setTimeout(() => this.flush(), FLUSH_MS);
|
|
6107
|
+
}
|
|
6108
|
+
/** Sends the pending changes as one patch: text streamed into the same step is joined, and a step sent whole
|
|
6109
|
+
* replaces what came before it for that step. A step is copied when it is queued, so text streamed after that
|
|
6110
|
+
* follows it as its own patch. */
|
|
6111
|
+
flush() {
|
|
6112
|
+
if (this.timer) {
|
|
6113
|
+
clearTimeout(this.timer);
|
|
6114
|
+
this.timer = null;
|
|
6115
|
+
}
|
|
6116
|
+
if (!this.pending.length) return;
|
|
6117
|
+
const patches = [];
|
|
6118
|
+
for (const patch of this.pending) {
|
|
6119
|
+
const last = patches.at(-1);
|
|
6120
|
+
if (patch.op === "text" && last?.op === "text" && last.id === patch.id) {
|
|
6121
|
+
last.append += patch.append;
|
|
6122
|
+
continue;
|
|
6123
|
+
}
|
|
6124
|
+
if (patch.op === "step") {
|
|
6125
|
+
for (let index = patches.length - 1; index >= 0; index--) {
|
|
6126
|
+
const item = patches[index];
|
|
6127
|
+
if ((item.op === "step" ? item.step.id : item.id) === patch.step.id) patches.splice(index, 1);
|
|
6128
|
+
}
|
|
6129
|
+
patches.push(patch);
|
|
6130
|
+
continue;
|
|
6131
|
+
}
|
|
6132
|
+
patches.push({ ...patch });
|
|
6133
|
+
}
|
|
6134
|
+
this.pending = [];
|
|
6135
|
+
this.emit({ type: "patch", patches });
|
|
6136
|
+
}
|
|
6137
|
+
emit(update) {
|
|
6138
|
+
for (const listener of [...this.listeners.keys()]) {
|
|
6139
|
+
try {
|
|
6140
|
+
listener(update);
|
|
6141
|
+
} catch (error) {
|
|
6142
|
+
console.error("[live] listener failed", { error });
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
reset() {
|
|
6147
|
+
this.turn = null;
|
|
6148
|
+
this.steps.clear();
|
|
6149
|
+
this.blocks.clear();
|
|
6150
|
+
this.compaction = null;
|
|
6151
|
+
this.retry = null;
|
|
6152
|
+
this.counter = 0;
|
|
6153
|
+
this.pending = [];
|
|
6154
|
+
if (this.timer) {
|
|
6155
|
+
clearTimeout(this.timer);
|
|
6156
|
+
this.timer = null;
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
};
|
|
6160
|
+
|
|
5670
6161
|
// src/server/service/bot/service.ts
|
|
5671
6162
|
var BotService = class {
|
|
5672
6163
|
constructor(deps) {
|
|
5673
6164
|
this.deps = deps;
|
|
5674
6165
|
}
|
|
5675
6166
|
bots = /* @__PURE__ */ new Map();
|
|
6167
|
+
lives = /* @__PURE__ */ new Map();
|
|
5676
6168
|
closed = false;
|
|
5677
6169
|
started = false;
|
|
5678
6170
|
get(id) {
|
|
5679
|
-
if (this.closed) throw new RuntimeFault("stopping", "
|
|
5680
|
-
const
|
|
5681
|
-
if (!
|
|
6171
|
+
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
6172
|
+
const record2 = this.deps.store.getBot(id);
|
|
6173
|
+
if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5682
6174
|
let bot = this.bots.get(id);
|
|
5683
6175
|
if (bot) return bot;
|
|
5684
|
-
const { store, chats, files, credentials } = this.deps;
|
|
6176
|
+
const { store, chats, files, credentials } = this.deps, live = this.live(id);
|
|
5685
6177
|
const runtime = new AgentRuntime(new BotInbox(id, store.runtime, chats, files), (setup, model, reference, activity) => (this.deps.factory ?? PiSession.create)({
|
|
5686
6178
|
store,
|
|
5687
|
-
bot:
|
|
6179
|
+
bot: record2,
|
|
5688
6180
|
selection: model.selection,
|
|
5689
6181
|
reference,
|
|
5690
6182
|
credentials,
|
|
5691
6183
|
tools: setup.tools,
|
|
5692
6184
|
systemPrompt: () => botPrompt(setup),
|
|
5693
6185
|
onHistory: setup.onHistory,
|
|
5694
|
-
activity
|
|
6186
|
+
activity,
|
|
6187
|
+
live: (event) => live.event(event)
|
|
5695
6188
|
}));
|
|
5696
|
-
const workspace = new Workspace(
|
|
5697
|
-
const memory = new Memory(join6(dirname5(
|
|
6189
|
+
const workspace = new Workspace(record2.workspacePath);
|
|
6190
|
+
const memory = new Memory(join6(dirname5(record2.workspacePath), "history.db"));
|
|
5698
6191
|
bot = new Bot(id, {
|
|
5699
6192
|
identity: new Identity(id, store),
|
|
5700
6193
|
model: new Model(id, store, resolveThinking),
|
|
@@ -5704,7 +6197,7 @@ var BotService = class {
|
|
|
5704
6197
|
agentRuntime: runtime,
|
|
5705
6198
|
state: new BotState(() => this.deps.changed(id))
|
|
5706
6199
|
}, { deliver: async (message) => {
|
|
5707
|
-
const outgoing = await files.readOutgoing(
|
|
6200
|
+
const outgoing = await files.readOutgoing(record2.workspacePath, message.attachments ?? [], message.signal);
|
|
5708
6201
|
message.signal?.throwIfAborted();
|
|
5709
6202
|
return chats.deliver(id, message.chatId, {
|
|
5710
6203
|
content: message.content,
|
|
@@ -5720,23 +6213,40 @@ var BotService = class {
|
|
|
5720
6213
|
this.bots.set(id, bot);
|
|
5721
6214
|
return bot;
|
|
5722
6215
|
}
|
|
5723
|
-
details(
|
|
5724
|
-
const model =
|
|
5725
|
-
return { ...
|
|
6216
|
+
details(record2) {
|
|
6217
|
+
const model = record2.connectionId ? this.deps.store.getModelConnection(record2.connectionId)?.models.find((model2) => model2.id === record2.modelId) : void 0;
|
|
6218
|
+
return { ...record2, effectiveThinkingLevel: model ? resolveThinking(model, record2.thinkingLevel) : null };
|
|
5726
6219
|
}
|
|
5727
6220
|
owns(id) {
|
|
5728
6221
|
return this.deps.store.getBot(id) !== null;
|
|
5729
6222
|
}
|
|
6223
|
+
/** The turn a Bot is running, live (A47); kept for a Bot whether or not it has started. */
|
|
6224
|
+
live(id) {
|
|
6225
|
+
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6226
|
+
let live = this.lives.get(id);
|
|
6227
|
+
if (!live) {
|
|
6228
|
+
live = new LiveTurns((chatId) => this.liveChat(id, chatId));
|
|
6229
|
+
this.lives.set(id, live);
|
|
6230
|
+
}
|
|
6231
|
+
return live;
|
|
6232
|
+
}
|
|
6233
|
+
/** A chat as the Bot sees it, for the live turn: a DM is named after the other member. */
|
|
6234
|
+
liveChat(botId, chatId) {
|
|
6235
|
+
const chat = this.deps.chats.get(botId, chatId);
|
|
6236
|
+
if (!chat) return null;
|
|
6237
|
+
const other = chat.type === "dm" ? chat.memberIds.find((member) => member !== botId) : void 0;
|
|
6238
|
+
return { id: chat.id, type: chat.type, title: other ? this.deps.chats.member(other)?.name ?? chat.title : chat.title };
|
|
6239
|
+
}
|
|
5730
6240
|
list() {
|
|
5731
|
-
return this.deps.store.listBots().map((
|
|
6241
|
+
return this.deps.store.listBots().map((record2) => this.details(record2));
|
|
5732
6242
|
}
|
|
5733
6243
|
create(name2) {
|
|
5734
6244
|
return this.details(this.deps.store.createBot(name2));
|
|
5735
6245
|
}
|
|
5736
6246
|
describe(id) {
|
|
5737
|
-
const
|
|
5738
|
-
if (!
|
|
5739
|
-
return this.details(
|
|
6247
|
+
const record2 = this.deps.store.getBot(id);
|
|
6248
|
+
if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6249
|
+
return this.details(record2);
|
|
5740
6250
|
}
|
|
5741
6251
|
runs(id) {
|
|
5742
6252
|
this.describe(id);
|
|
@@ -5751,7 +6261,7 @@ var BotService = class {
|
|
|
5751
6261
|
this.describe(id);
|
|
5752
6262
|
const actor = this.deps.store.userMemberId();
|
|
5753
6263
|
const message = this.deps.chats.message(actor, messageId);
|
|
5754
|
-
if (!message || message.sender.id !== id) throw new RuntimeFault("message_missing", "
|
|
6264
|
+
if (!message || message.sender.id !== id) throw new RuntimeFault("message_missing", "Message not found", 404);
|
|
5755
6265
|
const chat = this.deps.chats.get(actor, message.chatId);
|
|
5756
6266
|
return { message, chat: { id: chat.id, title: chat.title } };
|
|
5757
6267
|
}
|
|
@@ -5764,7 +6274,7 @@ var BotService = class {
|
|
|
5764
6274
|
const bot = this.get(id);
|
|
5765
6275
|
void bot.deliver(urgency).catch((error) => {
|
|
5766
6276
|
const fault = publicFault(error);
|
|
5767
|
-
bot.state.update({ ...bot.status(), error: { code: fault.code, message: fault.message } });
|
|
6277
|
+
bot.state.update({ ...bot.status(), error: { code: fault.code, message: fault.message, ...fault.params ? { params: fault.params } : {} } });
|
|
5768
6278
|
console.error("[bot] delivery failed", { botId: id, code: fault.code });
|
|
5769
6279
|
});
|
|
5770
6280
|
}
|
|
@@ -5784,26 +6294,26 @@ var BotService = class {
|
|
|
5784
6294
|
/** Every Bot's run state, for `bots.list` (A43). A Bot not loaded in this process is stopped. */
|
|
5785
6295
|
roster() {
|
|
5786
6296
|
const { store } = this.deps;
|
|
5787
|
-
return store.listBots().map((
|
|
5788
|
-
const bot = this.bots.get(
|
|
6297
|
+
return store.listBots().map((record2) => {
|
|
6298
|
+
const bot = this.bots.get(record2.id), runId = bot?.agentRuntime.activeRunId ?? null;
|
|
5789
6299
|
const status = bot?.status() ?? { status: "stop", activity: null, context: null, error: null };
|
|
5790
6300
|
return {
|
|
5791
|
-
id:
|
|
5792
|
-
name:
|
|
6301
|
+
id: record2.id,
|
|
6302
|
+
name: record2.name,
|
|
5793
6303
|
status,
|
|
5794
6304
|
runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
|
|
5795
|
-
errorCode: store.runtime.latestErrorCode(
|
|
6305
|
+
errorCode: store.runtime.latestErrorCode(record2.id)
|
|
5796
6306
|
};
|
|
5797
6307
|
});
|
|
5798
6308
|
}
|
|
5799
6309
|
status(id) {
|
|
5800
|
-
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot
|
|
6310
|
+
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5801
6311
|
const bot = this.bots.get(id);
|
|
5802
6312
|
return { ...bot?.status() ?? { status: "stop", activity: null, context: null, error: null }, botId: id, runId: bot?.agentRuntime.activeRunId ?? null };
|
|
5803
6313
|
}
|
|
5804
6314
|
async selectModel(id, input) {
|
|
5805
6315
|
const selection = this.deps.store.validateSelection(id, input);
|
|
5806
|
-
if (!selection) throw new RuntimeFault("bot_missing", "Bot
|
|
6316
|
+
if (!selection) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5807
6317
|
await this.get(id).changeModel(selection);
|
|
5808
6318
|
this.resume(id);
|
|
5809
6319
|
return this.deps.store.getBot(id);
|
|
@@ -5823,13 +6333,16 @@ var BotService = class {
|
|
|
5823
6333
|
const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
|
|
5824
6334
|
await bot.stop();
|
|
5825
6335
|
this.bots.delete(id);
|
|
5826
|
-
|
|
6336
|
+
this.lives.get(id)?.close();
|
|
6337
|
+
this.lives.delete(id);
|
|
6338
|
+
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5827
6339
|
if (!keepFiles) await rm2(dirname5(workspacePath), { recursive: true, force: true });
|
|
5828
6340
|
}
|
|
5829
6341
|
async close() {
|
|
5830
6342
|
if (this.closed) return;
|
|
5831
6343
|
this.closed = true;
|
|
5832
6344
|
await Promise.all([...this.bots.values()].map((bot) => bot.stop()));
|
|
6345
|
+
for (const live of this.lives.values()) live.close();
|
|
5833
6346
|
}
|
|
5834
6347
|
};
|
|
5835
6348
|
|
|
@@ -5895,7 +6408,7 @@ var Application = class _Application {
|
|
|
5895
6408
|
}
|
|
5896
6409
|
});
|
|
5897
6410
|
} catch {
|
|
5898
|
-
throw new RuntimeFault("data_locked", "
|
|
6411
|
+
throw new RuntimeFault("data_locked", "The data directory is in use, or its run lock can't be taken.", 409);
|
|
5899
6412
|
}
|
|
5900
6413
|
let store;
|
|
5901
6414
|
try {
|
|
@@ -5931,8 +6444,8 @@ import { join as join8, resolve as resolve5 } from "node:path";
|
|
|
5931
6444
|
var pidPath = (dir) => join8(resolve5(dir), "jarvy.pid");
|
|
5932
6445
|
var logPath = (dir) => join8(resolve5(dir), "jarvy.log");
|
|
5933
6446
|
function writePidFile(dir, url) {
|
|
5934
|
-
const
|
|
5935
|
-
writeFileSync(pidPath(dir), JSON.stringify(
|
|
6447
|
+
const record2 = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
|
|
6448
|
+
writeFileSync(pidPath(dir), JSON.stringify(record2), { mode: 384 });
|
|
5936
6449
|
}
|
|
5937
6450
|
function removePidFile(dir) {
|
|
5938
6451
|
if (readRecord(dir)?.pid === process.pid) rmSync(pidPath(dir), { force: true });
|
|
@@ -5960,14 +6473,14 @@ function alive(pid) {
|
|
|
5960
6473
|
}
|
|
5961
6474
|
}
|
|
5962
6475
|
function runningServer(dir) {
|
|
5963
|
-
const
|
|
5964
|
-
if (!
|
|
5965
|
-
const token = alive(
|
|
5966
|
-
if (token === null || token &&
|
|
6476
|
+
const record2 = readRecord(dir);
|
|
6477
|
+
if (!record2) return null;
|
|
6478
|
+
const token = alive(record2.pid) ? startToken(record2.pid) : null;
|
|
6479
|
+
if (token === null || token && record2.startToken && token !== record2.startToken) {
|
|
5967
6480
|
rmSync(pidPath(dir), { force: true });
|
|
5968
6481
|
return null;
|
|
5969
6482
|
}
|
|
5970
|
-
return
|
|
6483
|
+
return record2;
|
|
5971
6484
|
}
|
|
5972
6485
|
function startBackground(script, runArgs, dir) {
|
|
5973
6486
|
mkdirSync3(resolve5(dir), { recursive: true, mode: 448 });
|
|
@@ -6000,11 +6513,11 @@ function signalReady(url) {
|
|
|
6000
6513
|
process.disconnect();
|
|
6001
6514
|
}
|
|
6002
6515
|
async function stopServer(dir, waitMs = 3e4) {
|
|
6003
|
-
const
|
|
6004
|
-
if (!
|
|
6005
|
-
process.kill(
|
|
6516
|
+
const record2 = runningServer(dir);
|
|
6517
|
+
if (!record2) return "not_running";
|
|
6518
|
+
process.kill(record2.pid, "SIGTERM");
|
|
6006
6519
|
for (const deadline = Date.now() + waitMs; Date.now() < deadline; ) {
|
|
6007
|
-
if (!alive(
|
|
6520
|
+
if (!alive(record2.pid)) {
|
|
6008
6521
|
rmSync(pidPath(dir), { force: true });
|
|
6009
6522
|
return "stopped";
|
|
6010
6523
|
}
|