@goodfish/jarvy 0.2.0 → 0.3.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 +27 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -0
- package/dist/server/cli.js +280 -208
- package/dist/web/assets/figure-DBBkOPNl.js +1 -0
- package/dist/web/assets/index-C1Aq7FIn.js +505 -0
- package/dist/web/assets/index-CDOMm3pH.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
|
@@ -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) => {
|
|
@@ -924,6 +983,21 @@ function migrateQuotes(db) {
|
|
|
924
983
|
}
|
|
925
984
|
}
|
|
926
985
|
|
|
986
|
+
// src/server/infra/storage/migrations/title-schema.ts
|
|
987
|
+
function migrateTitles(db) {
|
|
988
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
989
|
+
if (version >= 18) return;
|
|
990
|
+
db.exec("BEGIN IMMEDIATE");
|
|
991
|
+
try {
|
|
992
|
+
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);
|
|
993
|
+
PRAGMA user_version=18;`);
|
|
994
|
+
db.exec("COMMIT");
|
|
995
|
+
} catch (error) {
|
|
996
|
+
db.exec("ROLLBACK");
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
927
1001
|
// src/server/infra/storage/migrations/schema.ts
|
|
928
1002
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
929
1003
|
|
|
@@ -1047,7 +1121,7 @@ function migrateRuntime(db) {
|
|
|
1047
1121
|
// src/server/infra/storage/migrations/schema.ts
|
|
1048
1122
|
function migrateDatabase(db) {
|
|
1049
1123
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1050
|
-
if (version >
|
|
1124
|
+
if (version > 18) throw new Error(`Unsupported database version: ${version}`);
|
|
1051
1125
|
migratePrototype(db);
|
|
1052
1126
|
if (version >= 5) {
|
|
1053
1127
|
migrateRuntime(db);
|
|
@@ -1062,6 +1136,7 @@ function migrateDatabase(db) {
|
|
|
1062
1136
|
migrateInteractions(db);
|
|
1063
1137
|
migrateInbox(db);
|
|
1064
1138
|
migrateQuotes(db);
|
|
1139
|
+
migrateTitles(db);
|
|
1065
1140
|
return;
|
|
1066
1141
|
}
|
|
1067
1142
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1134,6 +1209,7 @@ function migrateDatabase(db) {
|
|
|
1134
1209
|
migrateInteractions(db);
|
|
1135
1210
|
migrateInbox(db);
|
|
1136
1211
|
migrateQuotes(db);
|
|
1212
|
+
migrateTitles(db);
|
|
1137
1213
|
}
|
|
1138
1214
|
|
|
1139
1215
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1323,40 +1399,7 @@ var ChannelRepository = class {
|
|
|
1323
1399
|
|
|
1324
1400
|
// src/server/infra/storage/thread-repository.ts
|
|
1325
1401
|
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) {
|
|
1402
|
+
function threadTitle(content2, names, file = "") {
|
|
1360
1403
|
const line = (content2.split("\n").map((item) => item.trim()).find(Boolean) ?? "").replace(/[*`~]/g, "").replace(/^[#>\s-]+/, "");
|
|
1361
1404
|
const sorted = [...names].sort((a, b) => b.length - a.length);
|
|
1362
1405
|
let rest = line;
|
|
@@ -1366,18 +1409,18 @@ function threadTitle(content2, names) {
|
|
|
1366
1409
|
if (!name2) break;
|
|
1367
1410
|
rest = rest.slice(name2.length + 1);
|
|
1368
1411
|
}
|
|
1369
|
-
return (rest.trim() || line ||
|
|
1412
|
+
return (rest.trim() || line || file).slice(0, 60);
|
|
1370
1413
|
}
|
|
1371
1414
|
var ThreadRepository = class {
|
|
1372
1415
|
constructor(db, chats) {
|
|
1373
1416
|
this.db = db;
|
|
1374
1417
|
this.chats = chats;
|
|
1375
1418
|
}
|
|
1376
|
-
/** The thread of a channel message, created on first use; whoever opens it
|
|
1419
|
+
/** The thread of a channel message, created on first use; whoever opens it and the root's author follow it. */
|
|
1377
1420
|
open(actorId, rootMessageId, names) {
|
|
1378
1421
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1379
1422
|
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
|
|
1423
|
+
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
1424
|
WHERE m.id=?`).get(rootMessageId);
|
|
1382
1425
|
if (!root || !this.chats.canAccess(actorId, root.chatId, true)) throw new ChatError(404, "Message not found");
|
|
1383
1426
|
if (root.type !== "channel") throw new ChatError(400, "Threads start from channel messages");
|
|
@@ -1385,9 +1428,11 @@ var ThreadRepository = class {
|
|
|
1385
1428
|
if (!id) {
|
|
1386
1429
|
id = randomUUID4();
|
|
1387
1430
|
const now = Date.now();
|
|
1431
|
+
const firstFile = this.db.prepare("SELECT name FROM attachments WHERE message_id=? ORDER BY created_at,rowid LIMIT 1").get(rootMessageId)?.name;
|
|
1388
1432
|
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);
|
|
1433
|
+
VALUES(?,'thread',?,?,?,?,?,?,'open')`).run(id, threadTitle(previewQuotes(root.content, this.chats.quotes.of(rootMessageId)), names, firstFile), root.ownerId, now, now, root.chatId, rootMessageId);
|
|
1390
1434
|
this.follow(actorId, id);
|
|
1435
|
+
if (root.senderId !== actorId && this.chats.canAccess(root.senderId, root.chatId)) this.follow(root.senderId, id);
|
|
1391
1436
|
}
|
|
1392
1437
|
this.db.exec("COMMIT");
|
|
1393
1438
|
return id;
|
|
@@ -1551,7 +1596,7 @@ var QuoteRepository = class {
|
|
|
1551
1596
|
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
1597
|
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
1598
|
c.section_id AS sectionId,c.topic,c.parent_id AS parentId,c.root_message_id AS rootMessageId,c.status,
|
|
1554
|
-
(SELECT
|
|
1599
|
+
(SELECT content FROM messages WHERE chat_id=c.id ORDER BY sequence DESC LIMIT 1) AS lastMessage,
|
|
1555
1600
|
(SELECT id FROM messages WHERE chat_id=c.id ORDER BY sequence DESC LIMIT 1) AS lastMessageId,
|
|
1556
1601
|
(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
1602
|
WHERE m.chat_id=c.id ORDER BY m.sequence DESC LIMIT 1) AS lastSender`;
|
|
@@ -1610,8 +1655,8 @@ var ChatRepository = class {
|
|
|
1610
1655
|
if (!this.canAccess(actorId, id)) return null;
|
|
1611
1656
|
const found = this.db.prepare(`SELECT ${CHAT_COLUMNS} FROM chats c WHERE c.id=?`).get(id);
|
|
1612
1657
|
if (!found) return null;
|
|
1613
|
-
const { lastMessageId, ...
|
|
1614
|
-
|
|
1658
|
+
const { lastMessageId, lastMessage: content2, ...rest } = found;
|
|
1659
|
+
const row = { ...rest, lastMessage: content2 !== null && lastMessageId ? { content: clipMarked(content2, 300), quotes: this.quotes.of(lastMessageId) } : null };
|
|
1615
1660
|
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
1661
|
const state = this.db.prepare("SELECT following,subscribed,pinned,muted FROM chat_states WHERE chat_id=? AND member_id=?").get(id, actorId);
|
|
1617
1662
|
return {
|
|
@@ -1763,10 +1808,10 @@ var ChatRepository = class {
|
|
|
1763
1808
|
quotes: this.quotes.of(row.id)
|
|
1764
1809
|
};
|
|
1765
1810
|
}
|
|
1766
|
-
/** What a reply shows of the message it answers: who wrote it and the start of its
|
|
1811
|
+
/** 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
1812
|
quote(id) {
|
|
1768
1813
|
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:
|
|
1814
|
+
return row ? { id: row.id, senderId: row.senderId, content: clipMarked(row.content, 200), quotes: this.quotes.of(row.id) } : null;
|
|
1770
1815
|
}
|
|
1771
1816
|
append(actorId, chatId, draft, requestHash, files = [], deliveries = { message: [], echo: [] }, mentions = []) {
|
|
1772
1817
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1793,7 +1838,7 @@ var ChatRepository = class {
|
|
|
1793
1838
|
if (draft.replyTo && this.db.prepare("SELECT chat_id AS chatId FROM messages WHERE id=?").get(draft.replyTo)?.chatId !== chatId)
|
|
1794
1839
|
throw new ChatError(400, "Reply target is not in this chat");
|
|
1795
1840
|
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
|
|
1841
|
+
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
1842
|
this.quotes.insert(id, draft.quotes ?? []);
|
|
1798
1843
|
for (const attachmentId of draft.attachmentIds) this.db.prepare("UPDATE attachments SET message_id=? WHERE id=?").run(id, attachmentId);
|
|
1799
1844
|
this.inbox(deliveries.message, chatId, id, actorId, createdAt);
|
|
@@ -1826,8 +1871,9 @@ var ChatRepository = class {
|
|
|
1826
1871
|
});
|
|
1827
1872
|
}
|
|
1828
1873
|
}
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1874
|
+
const file = this.db.prepare("SELECT name FROM attachments WHERE message_id=? ORDER BY created_at,rowid LIMIT 1").get(id);
|
|
1875
|
+
const title = draft.content ? previewQuotes(draft.content, draft.quotes ?? []).slice(0, 46).replace(/\s+/g, " ") : file?.name.slice(0, 46) ?? "";
|
|
1876
|
+
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
1877
|
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
1878
|
this.db.exec("COMMIT");
|
|
1833
1879
|
return { message, duplicate: false, ...echo ? { echo } : {} };
|
|
@@ -1927,7 +1973,7 @@ var RuntimeRepository = class {
|
|
|
1927
1973
|
}
|
|
1928
1974
|
submitted(runId) {
|
|
1929
1975
|
if (!this.db.prepare("UPDATE bot_runs SET state='submitted' WHERE id=? AND state='claimed'").run(runId).changes)
|
|
1930
|
-
throw new RuntimeFault("invalid_run", "
|
|
1976
|
+
throw new RuntimeFault("invalid_run", "The run state has changed.");
|
|
1931
1977
|
}
|
|
1932
1978
|
finish(runId, state, errorCode = null) {
|
|
1933
1979
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1944,7 +1990,7 @@ var RuntimeRepository = class {
|
|
|
1944
1990
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1945
1991
|
try {
|
|
1946
1992
|
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", "
|
|
1993
|
+
throw new RuntimeFault("unsafe_replay", "A submitted run can't be replayed automatically.");
|
|
1948
1994
|
this.db.prepare("UPDATE inbox SET state='pending',run_id=NULL WHERE run_id=? AND state='claimed'").run(runId);
|
|
1949
1995
|
this.db.exec("COMMIT");
|
|
1950
1996
|
} catch (error) {
|
|
@@ -2011,8 +2057,8 @@ function providerCatalog() {
|
|
|
2011
2057
|
if (!models.length && provider.getAllModels?.().length) return [];
|
|
2012
2058
|
const common = { id: provider.id, api: models[0]?.api ?? "openai-completions", modelCount: models.length };
|
|
2013
2059
|
return [
|
|
2014
|
-
...provider.auth.oauth ? [{ ...common, name: provider.auth.oauth.name,
|
|
2015
|
-
...provider.auth.apiKey?.login ? [{ ...common, name: provider.name,
|
|
2060
|
+
...provider.auth.oauth ? [{ ...common, name: provider.auth.oauth.name, authType: "oauth" }] : [],
|
|
2061
|
+
...provider.auth.apiKey?.login ? [{ ...common, name: provider.name, authType: "api_key" }] : []
|
|
2016
2062
|
];
|
|
2017
2063
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2018
2064
|
}
|
|
@@ -2020,15 +2066,15 @@ var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getMod
|
|
|
2020
2066
|
|
|
2021
2067
|
// src/server/infra/storage/credential-repository.ts
|
|
2022
2068
|
function credentialValue(value2) {
|
|
2023
|
-
if (!value2 || typeof value2 !== "object") throw new RuntimeFault("invalid_credential", "
|
|
2069
|
+
if (!value2 || typeof value2 !== "object") throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
2024
2070
|
const v = value2;
|
|
2025
2071
|
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", "
|
|
2072
|
+
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
2073
|
return structuredClone(v);
|
|
2028
2074
|
}
|
|
2029
2075
|
if (v.type === "oauth" && typeof v.access === "string" && typeof v.refresh === "string" && typeof v.expires === "number" && Number.isFinite(v.expires))
|
|
2030
2076
|
return structuredClone(v);
|
|
2031
|
-
throw new RuntimeFault("invalid_credential", "
|
|
2077
|
+
throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
2032
2078
|
}
|
|
2033
2079
|
var CredentialRepository = class {
|
|
2034
2080
|
constructor(db) {
|
|
@@ -2044,9 +2090,9 @@ var CredentialRepository = class {
|
|
|
2044
2090
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2045
2091
|
try {
|
|
2046
2092
|
if (!this.db.prepare("SELECT 1 FROM provider_connections WHERE id=? AND owner_id=?").get(connectionId, ownerId))
|
|
2047
|
-
throw new RuntimeFault("connection_missing", "
|
|
2093
|
+
throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
2048
2094
|
const current = this.read(ownerId, connectionId);
|
|
2049
|
-
if ((current?.revision ?? null) !== prior) throw new RuntimeFault("credential_changed", "
|
|
2095
|
+
if ((current?.revision ?? null) !== prior) throw new RuntimeFault("credential_changed", "The credential changed. Please try again.");
|
|
2050
2096
|
this.db.prepare(`INSERT INTO provider_credentials(connection_id,type,credential_json,updated_at) VALUES(?,?,?,?)
|
|
2051
2097
|
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
2098
|
this.db.exec("COMMIT");
|
|
@@ -2239,13 +2285,12 @@ var Store = class {
|
|
|
2239
2285
|
botName(value2, exceptBotId) {
|
|
2240
2286
|
const name2 = requireText(value2, "Bot name", 80);
|
|
2241
2287
|
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("
|
|
2288
|
+
if (this.nameTaken(name2, exceptBotId)) throw new ConflictError("Another member already has this name", "name_taken");
|
|
2243
2289
|
return name2;
|
|
2244
2290
|
}
|
|
2291
|
+
/** The caller names the Bot (A45: the interface picks a free default in its own language). */
|
|
2245
2292
|
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);
|
|
2293
|
+
const clean = this.botName(name2);
|
|
2249
2294
|
const id = randomUUID7(), createdAt = Date.now();
|
|
2250
2295
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2251
2296
|
try {
|
|
@@ -2331,7 +2376,7 @@ var Store = class {
|
|
|
2331
2376
|
return this.getModelConnection(id);
|
|
2332
2377
|
}
|
|
2333
2378
|
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("
|
|
2379
|
+
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
2380
|
}
|
|
2336
2381
|
updateModelConnection(id, input) {
|
|
2337
2382
|
const current = this.getModelConnection(id);
|
|
@@ -2460,8 +2505,8 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2460
2505
|
const contextWindow = configured2.contextWindow ?? builtin?.contextWindow;
|
|
2461
2506
|
const maxTokens = configured2.maxTokens ?? builtin?.maxTokens;
|
|
2462
2507
|
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("
|
|
2508
|
+
if (!contextWindow || !maxTokens) throw new RuntimeFault("model_incomplete", "Fill in the model's context window and max output.", 422);
|
|
2509
|
+
if (configured2.thinkingFormat && !FORMATS.has(configured2.thinkingFormat)) throw new RuntimeFault("thinking_format", "This thinking format isn't supported.", 422);
|
|
2465
2510
|
const compat = { ...builtin?.compat, ...configured2.compat, ...configured2.thinkingFormat ? { thinkingFormat: configured2.thinkingFormat } : {} };
|
|
2466
2511
|
runtime.registerProvider(connection.providerName, {
|
|
2467
2512
|
baseUrl,
|
|
@@ -2483,7 +2528,7 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2483
2528
|
}]
|
|
2484
2529
|
});
|
|
2485
2530
|
const model = runtime.getModel(connection.providerName, configured2.id);
|
|
2486
|
-
if (!model) throw new RuntimeFault("model_missing", "
|
|
2531
|
+
if (!model) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
2487
2532
|
model.headers = { ...model.headers, ...headers2 };
|
|
2488
2533
|
return model;
|
|
2489
2534
|
}
|
|
@@ -2519,7 +2564,7 @@ var CredentialCoordinator = class {
|
|
|
2519
2564
|
tails = /* @__PURE__ */ new Map();
|
|
2520
2565
|
closing = false;
|
|
2521
2566
|
async exclusive(connectionId, action, signal) {
|
|
2522
|
-
if (this.closing) throw new RuntimeFault("stopping", "
|
|
2567
|
+
if (this.closing) throw new RuntimeFault("stopping", "Shutting down.");
|
|
2523
2568
|
signal?.throwIfAborted();
|
|
2524
2569
|
const prior = this.tails.get(connectionId) ?? Promise.resolve();
|
|
2525
2570
|
const operation = prior.then(() => {
|
|
@@ -2566,7 +2611,7 @@ var ConnectionCredentials = class {
|
|
|
2566
2611
|
options?.signal?.throwIfAborted();
|
|
2567
2612
|
if (providerId !== this.providerName) return void 0;
|
|
2568
2613
|
const row = this.repository.read(this.ownerId, this.connectionId);
|
|
2569
|
-
if (!row) throw new RuntimeFault("credentials_missing", "
|
|
2614
|
+
if (!row) throw new RuntimeFault("credentials_missing", "Set up this connection's credential first.", 422);
|
|
2570
2615
|
return row.value;
|
|
2571
2616
|
}
|
|
2572
2617
|
async list(options) {
|
|
@@ -2575,7 +2620,7 @@ var ConnectionCredentials = class {
|
|
|
2575
2620
|
return row ? [{ providerId: this.providerName, type: row.value.type }] : [];
|
|
2576
2621
|
}
|
|
2577
2622
|
async modify(providerId, fn, options) {
|
|
2578
|
-
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "
|
|
2623
|
+
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "The credential belongs to another connection.");
|
|
2579
2624
|
return this.coordinator.exclusive(this.connectionId, async () => {
|
|
2580
2625
|
const prior = this.repository.read(this.ownerId, this.connectionId);
|
|
2581
2626
|
const next = await fn(prior?.value);
|
|
@@ -2584,7 +2629,7 @@ var ConnectionCredentials = class {
|
|
|
2584
2629
|
}, options?.signal);
|
|
2585
2630
|
}
|
|
2586
2631
|
async delete(providerId, options) {
|
|
2587
|
-
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "
|
|
2632
|
+
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "The credential belongs to another connection.");
|
|
2588
2633
|
await this.coordinator.exclusive(this.connectionId, async () => {
|
|
2589
2634
|
this.repository.delete(this.ownerId, this.connectionId);
|
|
2590
2635
|
}, options?.signal);
|
|
@@ -2723,10 +2768,10 @@ function autoconfigure(modelId, api, choice) {
|
|
|
2723
2768
|
// src/server/infra/models/discovery.ts
|
|
2724
2769
|
async function discoverModels(input, signal) {
|
|
2725
2770
|
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("
|
|
2771
|
+
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);
|
|
2772
|
+
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
2728
2773
|
const url = new URL(input.baseUrl);
|
|
2729
|
-
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new RuntimeFault("discovery_url", "API URL
|
|
2774
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new RuntimeFault("discovery_url", "The API URL isn't valid.", 400);
|
|
2730
2775
|
const key = input.key;
|
|
2731
2776
|
let path = url.pathname.replace(/\/+$/, "");
|
|
2732
2777
|
if (!path.endsWith("/models")) {
|
|
@@ -2745,11 +2790,14 @@ async function discoverModels(input, signal) {
|
|
|
2745
2790
|
const models = /* @__PURE__ */ new Map();
|
|
2746
2791
|
let more = true;
|
|
2747
2792
|
while (more) {
|
|
2748
|
-
const response = await fetch(url, { headers: headers2, signal })
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
if (!
|
|
2793
|
+
const response = await fetch(url, { headers: headers2, signal }).catch((error) => {
|
|
2794
|
+
if (signal.aborted) throw error;
|
|
2795
|
+
throw new RuntimeFault("discovery_unreachable", `Couldn't reach ${url.host}.`, 502, { host: url.host });
|
|
2796
|
+
});
|
|
2797
|
+
if (!response.ok) throw new RuntimeFault("discovery_http", `Couldn't fetch the models: HTTP ${response.status}.`, 502, { status: response.status });
|
|
2798
|
+
const body = await response.json().catch(() => null);
|
|
2799
|
+
const items = api === "google-generative-ai" ? body?.models : body?.data;
|
|
2800
|
+
if (!body || !Array.isArray(items)) throw new RuntimeFault("discovery_format", "The response isn't a model list for this protocol.", 502);
|
|
2753
2801
|
for (const raw of items) {
|
|
2754
2802
|
if (!raw || typeof raw !== "object") continue;
|
|
2755
2803
|
const item = raw;
|
|
@@ -2834,26 +2882,26 @@ var ModelService = class {
|
|
|
2834
2882
|
return modelCatalog(credentials, connection.providerName);
|
|
2835
2883
|
}
|
|
2836
2884
|
discover(input, signal) {
|
|
2837
|
-
if (typeof input.baseUrl !== "string") throw new RuntimeFault("
|
|
2885
|
+
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
2838
2886
|
let key = typeof input.apiKey === "string" ? input.apiKey.trim() : "";
|
|
2839
2887
|
if (!key && typeof input.connectionId === "string") {
|
|
2840
2888
|
this.required(this.store.getModelConnection(input.connectionId));
|
|
2841
2889
|
const credential = this.store.credentials.read(this.store.userMemberId(), input.connectionId)?.value;
|
|
2842
2890
|
if (credential?.type === "api_key") key = credential.key ?? "";
|
|
2843
2891
|
}
|
|
2844
|
-
if (!key) throw new RuntimeFault("discovery_key", "
|
|
2892
|
+
if (!key) throw new RuntimeFault("discovery_key", "Enter the API key.", 400);
|
|
2845
2893
|
return discoverModels({ api: String(input.api ?? "openai-completions"), baseUrl: input.baseUrl, key }, signal);
|
|
2846
2894
|
}
|
|
2847
2895
|
/** Suggests model fields from the local Pi catalog; the caller decides whether to save them. */
|
|
2848
2896
|
autoconfigure(input) {
|
|
2849
2897
|
const text3 = (value2) => typeof value2 === "string" && value2.trim() && value2.length <= 200 ? value2.trim() : null;
|
|
2850
2898
|
const modelId = text3(input.modelId), api = text3(input.api);
|
|
2851
|
-
if (!modelId || !api) throw new RuntimeFault("
|
|
2899
|
+
if (!modelId || !api) throw new RuntimeFault("autoconfig_model", "Enter the model ID.", 400);
|
|
2852
2900
|
let choice;
|
|
2853
2901
|
if (input.choice !== void 0) {
|
|
2854
2902
|
const raw = input.choice;
|
|
2855
2903
|
const provider = text3(raw?.provider), chosen = text3(raw?.modelId);
|
|
2856
|
-
if (!provider || !chosen) throw new RuntimeFault("
|
|
2904
|
+
if (!provider || !chosen) throw new RuntimeFault("autoconfig_source", "The setup source isn't valid.", 400);
|
|
2857
2905
|
choice = { provider, modelId: chosen };
|
|
2858
2906
|
}
|
|
2859
2907
|
return autoconfigure(modelId, api, choice);
|
|
@@ -2894,7 +2942,7 @@ var CredentialLogins = class {
|
|
|
2894
2942
|
flows = /* @__PURE__ */ new Map();
|
|
2895
2943
|
connection(id) {
|
|
2896
2944
|
const connection = this.store.getModelConnection(id);
|
|
2897
|
-
if (!connection) throw new RuntimeFault("connection_missing", "
|
|
2945
|
+
if (!connection) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
2898
2946
|
return connection;
|
|
2899
2947
|
}
|
|
2900
2948
|
status(id) {
|
|
@@ -2920,7 +2968,7 @@ var CredentialLogins = class {
|
|
|
2920
2968
|
flow.state = { phase: "completed" };
|
|
2921
2969
|
this.changed(id);
|
|
2922
2970
|
} catch (error) {
|
|
2923
|
-
flow.state = flow.controller.signal.aborted ? { phase: "cancelled" } : { phase: "error"
|
|
2971
|
+
flow.state = flow.controller.signal.aborted ? { phase: "cancelled" } : { phase: "error" };
|
|
2924
2972
|
if (!flow.controller.signal.aborted) console.error("[oauth] login failed", { connectionId: id, provider: connection.providerName, type: error instanceof Error ? error.constructor.name : "Error" });
|
|
2925
2973
|
} finally {
|
|
2926
2974
|
flow.answer = void 0;
|
|
@@ -2960,9 +3008,9 @@ var CredentialLogins = class {
|
|
|
2960
3008
|
input(id, body) {
|
|
2961
3009
|
this.connection(id);
|
|
2962
3010
|
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("
|
|
3011
|
+
if (!flow?.answer || !prompt || body.promptId !== prompt.id) throw new RuntimeFault("login_step", "The sign-in step has changed. Please start again.", 409);
|
|
3012
|
+
if (typeof body.value !== "string" || !body.value.trim()) throw new RuntimeFault("login_input", "Enter the sign-in details.", 400);
|
|
3013
|
+
if (prompt.options && !prompt.options.some((option2) => option2.id === body.value)) throw new RuntimeFault("login_option", "Pick a sign-in method.", 400);
|
|
2966
3014
|
flow.answer(body.value);
|
|
2967
3015
|
return this.status(id);
|
|
2968
3016
|
}
|
|
@@ -3290,7 +3338,7 @@ var ChatQuestions = class {
|
|
|
3290
3338
|
if (!question) return Promise.reject(new ChatError(409, "Not a question"));
|
|
3291
3339
|
if (question.state === "answered") return Promise.resolve(resultOf(question, question.answer));
|
|
3292
3340
|
if (question.state === "dismissed") return Promise.resolve({ status: "dismissed" });
|
|
3293
|
-
if (question.state === "ignored") return Promise.reject(new ChatError(409, "
|
|
3341
|
+
if (question.state === "ignored") return Promise.reject(new ChatError(409, "The question has expired", "question_expired"));
|
|
3294
3342
|
return new Promise((resolve6, reject) => {
|
|
3295
3343
|
const stop = () => {
|
|
3296
3344
|
this.waiting.delete(message.id);
|
|
@@ -3314,15 +3362,15 @@ var ChatQuestions = class {
|
|
|
3314
3362
|
this.repository.assertAccess(actorId, chatId, true);
|
|
3315
3363
|
if (message.sender.id === actorId) throw new ChatError(403, "A question is answered by the other members");
|
|
3316
3364
|
const question = message.question, waiter = this.waiting.get(messageId);
|
|
3317
|
-
if (question.state !== "pending") throw new ChatError(409, "
|
|
3365
|
+
if (question.state !== "pending") throw new ChatError(409, "The question is already settled", "question_closed");
|
|
3318
3366
|
if (!waiter) {
|
|
3319
3367
|
if (this.repository.settleQuestion(messageId, "ignored", null)) this.changed(chatId);
|
|
3320
|
-
throw new ChatError(409, "
|
|
3368
|
+
throw new ChatError(409, "The question has expired", "question_expired");
|
|
3321
3369
|
}
|
|
3322
3370
|
const dismissed = input.action === "dismiss";
|
|
3323
3371
|
if (!dismissed && input.action !== "answer") throw new ChatError(400, "Invalid question action");
|
|
3324
3372
|
const answer = dismissed ? null : answerOf(question, input);
|
|
3325
|
-
if (!this.repository.settleQuestion(messageId, dismissed ? "dismissed" : "answered", answer)) throw new ChatError(409, "
|
|
3373
|
+
if (!this.repository.settleQuestion(messageId, dismissed ? "dismissed" : "answered", answer)) throw new ChatError(409, "The question is already settled", "question_closed");
|
|
3326
3374
|
this.waiting.delete(messageId);
|
|
3327
3375
|
waiter(answer ? resultOf(question, answer) : { status: "dismissed" });
|
|
3328
3376
|
this.changed(chatId);
|
|
@@ -3489,7 +3537,7 @@ var ChatThreads = class {
|
|
|
3489
3537
|
const chat = this.repository.getChat(actorId, threadId);
|
|
3490
3538
|
if (!chat || chat.type !== "thread") throw new ChatError(404, "Thread not found");
|
|
3491
3539
|
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:
|
|
3540
|
+
return { chat, info: this.store.info(chat.rootMessageId), root: root ? { senderId: root.sender.id, content: root.content, quotes: root.quotes } : null };
|
|
3493
3541
|
}
|
|
3494
3542
|
list(actorId, channelId) {
|
|
3495
3543
|
return this.store.list(actorId, channelId).map((id) => this.entry(actorId, id));
|
|
@@ -3540,7 +3588,7 @@ function resolveQuotes(repository, actorId, chatId, content2, drafts) {
|
|
|
3540
3588
|
|
|
3541
3589
|
// src/server/service/chat/service.ts
|
|
3542
3590
|
function titleValue(value2) {
|
|
3543
|
-
if (value2 === void 0) return "
|
|
3591
|
+
if (value2 === void 0) return "";
|
|
3544
3592
|
if (typeof value2 !== "string" || !value2.trim() || value2.length > 100 || value2.includes("\0")) throw new ChatError(400, "Invalid Chat title");
|
|
3545
3593
|
return value2.trim();
|
|
3546
3594
|
}
|
|
@@ -3604,7 +3652,7 @@ var ChatService = class {
|
|
|
3604
3652
|
const owner = this.repository.channels.ownerOf(actorId), directory = this.repository.channels.directory(owner);
|
|
3605
3653
|
const actor = directory.find((member) => member.id === actorId), target = directory.find((member) => member.id === targetId);
|
|
3606
3654
|
if (!actor || !target) throw new ChatError(404, "Member not found");
|
|
3607
|
-
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name}\
|
|
3655
|
+
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target.name}`);
|
|
3608
3656
|
this.notifySaved(chat.id);
|
|
3609
3657
|
return chat;
|
|
3610
3658
|
}
|
|
@@ -3876,7 +3924,7 @@ var ChatFiles = class {
|
|
|
3876
3924
|
/** The quoted message as the envelope shows it: who wrote it and its first 200 characters. */
|
|
3877
3925
|
reply(message) {
|
|
3878
3926
|
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
|
|
3927
|
+
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: replyExcerpt(message.replyTo) } } : {};
|
|
3880
3928
|
}
|
|
3881
3929
|
async paths(actorId, message, signal) {
|
|
3882
3930
|
const paths = [];
|
|
@@ -4111,28 +4159,28 @@ var PiSession = class _PiSession {
|
|
|
4111
4159
|
await mkdir2(sessionsDir, { recursive: true, mode: 448 });
|
|
4112
4160
|
const expected = resolve3(sessionsDir) + sep;
|
|
4113
4161
|
if (!resolve3(reference.file).startsWith(expected) || dirname2(resolve3(reference.file)) !== resolve3(sessionsDir))
|
|
4114
|
-
throw new RuntimeFault("session_path", "
|
|
4162
|
+
throw new RuntimeFault("session_path", "The session file path isn't valid.");
|
|
4115
4163
|
const exists = existsSync(reference.file);
|
|
4116
4164
|
if (!exists && (reference.persisted || store.runtime.runs(bot.id).some((run) => run.state === "unknown")))
|
|
4117
|
-
throw new RuntimeFault("session_missing", "
|
|
4165
|
+
throw new RuntimeFault("session_missing", "The session file is missing; check the run records before recovering.");
|
|
4118
4166
|
if (exists) {
|
|
4119
4167
|
let header;
|
|
4120
4168
|
try {
|
|
4121
4169
|
header = JSON.parse(readFileSync(reference.file, "utf8").split("\n")[0]);
|
|
4122
4170
|
} catch {
|
|
4123
|
-
throw new RuntimeFault("session_corrupt", "
|
|
4171
|
+
throw new RuntimeFault("session_corrupt", "The session file can't be read.");
|
|
4124
4172
|
}
|
|
4125
4173
|
if (header.type !== "session" || !header.id || reference.sessionId && header.id !== reference.sessionId)
|
|
4126
|
-
throw new RuntimeFault("session_mismatch", "
|
|
4174
|
+
throw new RuntimeFault("session_mismatch", "The session file doesn't match this Bot.");
|
|
4127
4175
|
}
|
|
4128
4176
|
const connection = store.getModelConnection(selection.connectionId);
|
|
4129
|
-
if (!connection) throw new RuntimeFault("connection_missing", "
|
|
4177
|
+
if (!connection) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
4130
4178
|
const scoped = new ConnectionCredentials(store.credentials, options.credentials, store.userMemberId(), connection.id, connection.providerName);
|
|
4131
4179
|
await scoped.read(connection.providerName);
|
|
4132
4180
|
const credentials = new CurrentCredentials(scoped);
|
|
4133
4181
|
const models = await ModelRuntime3.create({ credentials, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
4134
4182
|
const configured2 = connection.models.find((model2) => model2.id === selection.modelId);
|
|
4135
|
-
if (!configured2) throw new RuntimeFault("model_missing", "
|
|
4183
|
+
if (!configured2) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
4136
4184
|
const model = installModel(models, connection, configured2, store.modelHeaders(connection.id, configured2.id));
|
|
4137
4185
|
const sessionManager = SessionManager.open(reference.file, sessionsDir, bot.workspacePath);
|
|
4138
4186
|
const adapter = new _PiSession(options, models, credentials);
|
|
@@ -4179,7 +4227,7 @@ var PiSession = class _PiSession {
|
|
|
4179
4227
|
adapter.session = result.session;
|
|
4180
4228
|
if (result.extensionsResult.errors.length || !tools.every((name2) => adapter.session.getActiveToolNames().includes(name2))) {
|
|
4181
4229
|
adapter.session.dispose();
|
|
4182
|
-
throw new RuntimeFault("tools_missing", "
|
|
4230
|
+
throw new RuntimeFault("tools_missing", "Couldn't load the run's tools.");
|
|
4183
4231
|
}
|
|
4184
4232
|
adapter.unsubscribe = adapter.session.subscribe((event) => adapter.onEvent(event));
|
|
4185
4233
|
if (options.onHistory) {
|
|
@@ -4241,7 +4289,7 @@ var PiSession = class _PiSession {
|
|
|
4241
4289
|
* request that follows a tool cancelled mid-run. Apply the same signal rule to those. */
|
|
4242
4290
|
classify(message) {
|
|
4243
4291
|
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() || "
|
|
4292
|
+
if (message.stopReason === "error") return { outcome: "failed", error: message.errorMessage?.trim() || "Unknown error" };
|
|
4245
4293
|
return { outcome: "completed" };
|
|
4246
4294
|
}
|
|
4247
4295
|
maybeAbort() {
|
|
@@ -4290,7 +4338,7 @@ var PiSession = class _PiSession {
|
|
|
4290
4338
|
}
|
|
4291
4339
|
async configure(selection) {
|
|
4292
4340
|
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", "
|
|
4341
|
+
if (!connection || !configured2) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
4294
4342
|
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
4343
|
if (signature === this.signature) {
|
|
4296
4344
|
this.session.setThinkingLevel(resolveThinking(configured2, selection.thinkingLevel), { persist: false });
|
|
@@ -4308,7 +4356,7 @@ var PiSession = class _PiSession {
|
|
|
4308
4356
|
return this.session.thinkingLevel;
|
|
4309
4357
|
} catch {
|
|
4310
4358
|
this.credentials.current = previous;
|
|
4311
|
-
throw new RuntimeFault("model_switch_uncertain", "
|
|
4359
|
+
throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.", 409);
|
|
4312
4360
|
}
|
|
4313
4361
|
}
|
|
4314
4362
|
async dispose() {
|
|
@@ -4327,14 +4375,12 @@ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
|
4327
4375
|
import { dirname as dirname3 } from "node:path";
|
|
4328
4376
|
var json = (value2) => value2 == null ? null : JSON.stringify(value2);
|
|
4329
4377
|
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`;
|
|
4378
|
+
var BLOCKS = "FROM json_each(history.message,'$.content') b WHERE json_extract(b.value,'$.type')";
|
|
4379
|
+
var SUMMARY = `SELECT id,timestamp,role,model,
|
|
4380
|
+
COALESCE((SELECT substr(group_concat(substr(json_extract(b.value,'$.text'),1,160),' '),1,160) ${BLOCKS} IN ('text','reasoning')),'') AS preview,
|
|
4381
|
+
(SELECT json_group_array(json_object('kind',CASE json_extract(b.value,'$.type') WHEN 'tool_call' THEN 'call' ELSE 'result' END,
|
|
4382
|
+
'name',json_extract(b.value,'$.name'),'error',json_extract(b.value,'$.is_error'))) ${BLOCKS} IN ('tool_call','tool_result')) AS tools,
|
|
4383
|
+
(SELECT count(*) ${BLOCKS}='image') AS images FROM history`;
|
|
4338
4384
|
var HistoryRepository = class {
|
|
4339
4385
|
db;
|
|
4340
4386
|
constructor(path) {
|
|
@@ -4376,8 +4422,19 @@ var HistoryRepository = class {
|
|
|
4376
4422
|
return row ? { ...row, model: row.model === null ? null : JSON.parse(row.model), message: JSON.parse(row.message) } : null;
|
|
4377
4423
|
});
|
|
4378
4424
|
}
|
|
4425
|
+
/** Model calls since a time, per provider and model: every assistant record with usage is one call. */
|
|
4426
|
+
usage(since) {
|
|
4427
|
+
const rows2 = this.db.prepare(`SELECT json_extract(model,'$.provider') AS provider,json_extract(model,'$.id') AS model,count(*) AS calls,
|
|
4428
|
+
COALESCE(SUM(json_extract(usage,'$.input_tokens')),0) AS input,COALESCE(SUM(json_extract(usage,'$.output_tokens')),0) AS output,
|
|
4429
|
+
COALESCE(SUM(json_extract(usage,'$.cache_read_tokens')),0) AS cacheRead,COALESCE(SUM(json_extract(usage,'$.cache_write_tokens')),0) AS cacheWrite,
|
|
4430
|
+
COALESCE(SUM(json_extract(usage,'$.cost.total')),0) AS cost,COALESCE(MAX(json_extract(usage,'$.cost.total')),0)>0 AS priced
|
|
4431
|
+
FROM history WHERE role='assistant' AND usage IS NOT NULL AND model IS NOT NULL AND timestamp>=?
|
|
4432
|
+
GROUP BY 1,2 ORDER BY cost DESC,calls DESC`).all(since);
|
|
4433
|
+
return rows2.map(({ priced, cost, ...row }) => ({ ...row, cost: priced ? cost : null }));
|
|
4434
|
+
}
|
|
4379
4435
|
decodeSummary(row) {
|
|
4380
|
-
|
|
4436
|
+
const tools = JSON.parse(row.tools).map(({ kind, name: name2, error }) => error ? { kind, name: name2, error: true } : { kind, name: name2 });
|
|
4437
|
+
return { ...row, model: row.model === null ? null : JSON.parse(row.model), preview: row.preview.replace(/\s+/g, " ").trim(), tools };
|
|
4381
4438
|
}
|
|
4382
4439
|
read(id) {
|
|
4383
4440
|
const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
|
|
@@ -4408,6 +4465,11 @@ var HistoryRepository = class {
|
|
|
4408
4465
|
};
|
|
4409
4466
|
|
|
4410
4467
|
// src/server/service/bot/memory.ts
|
|
4468
|
+
function summaryLine({ tools, images, ...summary }) {
|
|
4469
|
+
const steps = tools.map((tool) => tool.kind === "call" ? `Called ${tool.name}` : `${tool.name} \xB7 ${tool.error ? "failed" : "result"}`);
|
|
4470
|
+
const line = [summary.preview, ...steps, ...Array.from({ length: images }, () => "image")].filter(Boolean).join(" ");
|
|
4471
|
+
return { ...summary, preview: line.slice(0, 160).replace(/\s+/g, " ").trim() };
|
|
4472
|
+
}
|
|
4411
4473
|
var Memory = class {
|
|
4412
4474
|
constructor(historyPath) {
|
|
4413
4475
|
this.historyPath = historyPath;
|
|
@@ -4428,12 +4490,15 @@ var Memory = class {
|
|
|
4428
4490
|
timeline(before, limit) {
|
|
4429
4491
|
return this.history().timeline(before, limit);
|
|
4430
4492
|
}
|
|
4493
|
+
usage(since) {
|
|
4494
|
+
return this.history().usage(since);
|
|
4495
|
+
}
|
|
4431
4496
|
read(id) {
|
|
4432
4497
|
const record = this.history().read(id);
|
|
4433
4498
|
return record ? { record, links: this.history().links(record) } : null;
|
|
4434
4499
|
}
|
|
4435
4500
|
readMany(ids, mode) {
|
|
4436
|
-
const found = mode === "summary" ? this.history().summaries(ids) : this.history().details(ids);
|
|
4501
|
+
const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
|
|
4437
4502
|
const result = { mode, records: [], missingIds: [] };
|
|
4438
4503
|
found.forEach((record, index) => {
|
|
4439
4504
|
if (record) result.records.push(record);
|
|
@@ -4612,7 +4677,7 @@ var Model = class {
|
|
|
4612
4677
|
}
|
|
4613
4678
|
current() {
|
|
4614
4679
|
const bot = this.store.getBot(this.botId);
|
|
4615
|
-
if (!bot.connectionId || !bot.modelId) throw new RuntimeFault("model_required", "
|
|
4680
|
+
if (!bot.connectionId || !bot.modelId) throw new RuntimeFault("model_required", "Pick a model for the Bot first.", 422);
|
|
4616
4681
|
return { selection: { connectionId: bot.connectionId, modelId: bot.modelId, thinkingLevel: bot.thinkingLevel }, credentialRef: bot.connectionId };
|
|
4617
4682
|
}
|
|
4618
4683
|
async prepare(input) {
|
|
@@ -4756,10 +4821,10 @@ ${result.missingIds.join(", ")}`);
|
|
|
4756
4821
|
|
|
4757
4822
|
// src/server/service/bot/history-read.ts
|
|
4758
4823
|
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
|
|
4824
|
+
ids: Type2.Array(Type2.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }), { description: "IDs in your own History, read in the order given." }),
|
|
4825
|
+
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
4826
|
format: Type2.Optional(Type2.Union([Type2.Literal("json"), Type2.Literal("markdown")], { default: "json" })),
|
|
4762
|
-
to_file: Type2.Optional(Type2.Boolean({ default: false, description: "
|
|
4827
|
+
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
4828
|
}, { additionalProperties: false });
|
|
4764
4829
|
async function readHistory(memory, workspace, input, signal) {
|
|
4765
4830
|
signal?.throwIfAborted();
|
|
@@ -4824,7 +4889,7 @@ function threadRow(chats, entry) {
|
|
|
4824
4889
|
status: entry.chat.status,
|
|
4825
4890
|
replies: entry.info.replies,
|
|
4826
4891
|
...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,
|
|
4892
|
+
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
4893
|
following: entry.chat.following,
|
|
4829
4894
|
subscribed: entry.chat.subscribed,
|
|
4830
4895
|
unread: entry.chat.unread
|
|
@@ -4843,7 +4908,7 @@ function messageRow(chats, botId, message, attachments, chat) {
|
|
|
4843
4908
|
...message.mentions.length ? { mentions: message.mentions.map((id) => chats.member(id)?.name ?? id) } : {},
|
|
4844
4909
|
...message.thread ? { thread: { id: message.thread.id, title: message.thread.title, status: message.thread.status, replies: message.thread.replies } } : {},
|
|
4845
4910
|
...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
|
|
4911
|
+
...message.replyTo ? { replyTo: { id: message.replyTo.id, from: chats.member(message.replyTo.senderId)?.name ?? message.replyTo.senderId, text: replyExcerpt(message.replyTo) } } : {},
|
|
4847
4912
|
...message.reactions.length ? { reactions: message.reactions.map((group) => ({
|
|
4848
4913
|
emoji: group.emoji,
|
|
4849
4914
|
by: group.memberIds.map((id) => chats.member(id)?.name ?? id)
|
|
@@ -5021,34 +5086,34 @@ var None = Type3.Object({}, { additionalProperties: false });
|
|
|
5021
5086
|
var Name = (description) => Type3.String({ minLength: 1, maxLength: 80, description });
|
|
5022
5087
|
var MessageId = Type3.String({ pattern: "^m\\d+$" });
|
|
5023
5088
|
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: "
|
|
5089
|
+
label: Type3.String({ minLength: 1, maxLength: 80, description: "Text shown on the option" }),
|
|
5090
|
+
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" })),
|
|
5091
|
+
description: Type3.Optional(Type3.String({ minLength: 1, maxLength: 200, description: "A note under the option" })),
|
|
5027
5092
|
style: Type3.Optional(Type3.Union([Type3.Literal("default"), Type3.Literal("primary"), Type3.Literal("danger")]))
|
|
5028
5093
|
}, { additionalProperties: false });
|
|
5029
5094
|
var definitions = {
|
|
5030
|
-
"history.read": { description: "
|
|
5031
|
-
"members.list": { description: "
|
|
5032
|
-
"bots.list": { description: "
|
|
5033
|
-
"chats.list": { description: "
|
|
5095
|
+
"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 },
|
|
5096
|
+
"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 },
|
|
5097
|
+
"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 },
|
|
5098
|
+
"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
5099
|
"chats.info": {
|
|
5035
|
-
description: "
|
|
5100
|
+
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
5101
|
schema: Type3.Object({ chatId: Type3.String() }, { additionalProperties: false })
|
|
5037
5102
|
},
|
|
5038
5103
|
"chats.create": {
|
|
5039
|
-
description: "
|
|
5104
|
+
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
5105
|
schema: Type3.Object({
|
|
5041
|
-
name: Name("
|
|
5106
|
+
name: Name("Channel name"),
|
|
5042
5107
|
topic: Type3.Optional(Type3.String({ maxLength: 200 })),
|
|
5043
5108
|
section: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
5044
5109
|
members: Type3.Optional(Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { maxItems: 50 }))
|
|
5045
5110
|
}, { additionalProperties: false })
|
|
5046
5111
|
},
|
|
5047
5112
|
"chats.update": {
|
|
5048
|
-
description: "
|
|
5113
|
+
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
5114
|
schema: Type3.Object({
|
|
5050
5115
|
chatId: Type3.String(),
|
|
5051
|
-
name: Type3.Optional(Name("
|
|
5116
|
+
name: Type3.Optional(Name("Channel name")),
|
|
5052
5117
|
topic: Type3.Optional(Type3.String({ maxLength: 200 })),
|
|
5053
5118
|
section: Type3.Optional(Type3.String({ maxLength: 80 })),
|
|
5054
5119
|
notice: Type3.Optional(Type3.String({ minLength: 1, maxLength: 2e4 })),
|
|
@@ -5057,29 +5122,29 @@ var definitions = {
|
|
|
5057
5122
|
}, { additionalProperties: false })
|
|
5058
5123
|
},
|
|
5059
5124
|
"chats.addMembers": {
|
|
5060
|
-
description: "
|
|
5125
|
+
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
5126
|
schema: Type3.Object({ chatId: Type3.String(), members: Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }) }, { additionalProperties: false })
|
|
5062
5127
|
},
|
|
5063
5128
|
"chats.subscribe": {
|
|
5064
|
-
description: "
|
|
5129
|
+
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
5130
|
schema: Type3.Object({ chatId: Type3.String(), subscribed: Type3.Boolean() }, { additionalProperties: false })
|
|
5066
5131
|
},
|
|
5067
5132
|
"threads.list": {
|
|
5068
|
-
description: "
|
|
5069
|
-
schema: Type3.Object({ chatId: Type3.String({ description: "
|
|
5133
|
+
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.",
|
|
5134
|
+
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
5135
|
},
|
|
5071
5136
|
"threads.follow": {
|
|
5072
|
-
description: "
|
|
5073
|
-
schema: Type3.Object({ chatId: Type3.String({ description: "
|
|
5137
|
+
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.",
|
|
5138
|
+
schema: Type3.Object({ chatId: Type3.String({ description: "Thread ID" }), following: Type3.Boolean() }, { additionalProperties: false })
|
|
5074
5139
|
},
|
|
5075
|
-
"sections.list": { description: "
|
|
5076
|
-
"sections.create": { description: "
|
|
5140
|
+
"sections.list": { description: "List the channel groups (sections) with their IDs and names.", schema: None },
|
|
5141
|
+
"sections.create": { description: "Create a channel group.", schema: Type3.Object({ name: Name("Group name") }, { additionalProperties: false }) },
|
|
5077
5142
|
"sections.update": {
|
|
5078
|
-
description: "
|
|
5079
|
-
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("
|
|
5143
|
+
description: "Rename a channel group. section: its current name or ID.",
|
|
5144
|
+
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("New name") }, { additionalProperties: false })
|
|
5080
5145
|
},
|
|
5081
5146
|
"messages.read": {
|
|
5082
|
-
description: "\
|
|
5147
|
+
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
5148
|
schema: Type3.Object({
|
|
5084
5149
|
chatId: Type3.Optional(Type3.String()),
|
|
5085
5150
|
after: Type3.Optional(Type3.Integer({ minimum: 0 })),
|
|
@@ -5090,15 +5155,15 @@ var definitions = {
|
|
|
5090
5155
|
}, { additionalProperties: false })
|
|
5091
5156
|
},
|
|
5092
5157
|
"messages.search": {
|
|
5093
|
-
description: "
|
|
5158
|
+
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
5159
|
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
5160
|
},
|
|
5096
5161
|
"messages.react": {
|
|
5097
|
-
description:
|
|
5098
|
-
schema: Type3.Object({ messageId: MessageId, emoji: Type3.String({ minLength: 1, maxLength: 32, description: "
|
|
5162
|
+
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.`,
|
|
5163
|
+
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
5164
|
},
|
|
5100
5165
|
"messages.ask": {
|
|
5101
|
-
description: '
|
|
5166
|
+
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
5167
|
schema: Type3.Object({
|
|
5103
5168
|
chatId: Type3.String(),
|
|
5104
5169
|
question: Type3.String({ minLength: 1, maxLength: 500 }),
|
|
@@ -5109,7 +5174,7 @@ var definitions = {
|
|
|
5109
5174
|
}, { additionalProperties: false })
|
|
5110
5175
|
},
|
|
5111
5176
|
"messages.send": {
|
|
5112
|
-
description: "
|
|
5177
|
+
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
5178
|
schema: Type3.Object({
|
|
5114
5179
|
chatId: Type3.Optional(Type3.String()),
|
|
5115
5180
|
to: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
@@ -5166,7 +5231,7 @@ var Tools = class {
|
|
|
5166
5231
|
definitions() {
|
|
5167
5232
|
return [{
|
|
5168
5233
|
name: "chat",
|
|
5169
|
-
description: "
|
|
5234
|
+
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
5235
|
parameters: Gateway,
|
|
5171
5236
|
execute: async (raw, { toolCallId, signal }) => {
|
|
5172
5237
|
if (!Value.Check(Gateway, raw)) throw new ChatError(400, "Invalid chat tool request");
|
|
@@ -5221,7 +5286,7 @@ var Tools = class {
|
|
|
5221
5286
|
messageId: message.id,
|
|
5222
5287
|
emoji: parsed.emoji,
|
|
5223
5288
|
[applied ? "marked" : "unmarked"]: true,
|
|
5224
|
-
note: applied ? "
|
|
5289
|
+
note: applied ? "Reaction added" : "You already had this reaction, so this removed it",
|
|
5225
5290
|
reactions: message.reactions.map((group) => ({ emoji: group.emoji, count: group.memberIds.length }))
|
|
5226
5291
|
};
|
|
5227
5292
|
}
|
|
@@ -5321,7 +5386,7 @@ function noticeTail(notices, chats, members) {
|
|
|
5321
5386
|
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
5387
|
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
5388
|
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
|
|
5389
|
+
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
5390
|
}
|
|
5326
5391
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5327
5392
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5368,7 +5433,7 @@ var BotInbox = class {
|
|
|
5368
5433
|
return this.chats.notices(this.botId).length > 0;
|
|
5369
5434
|
}
|
|
5370
5435
|
reference(workspace) {
|
|
5371
|
-
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "
|
|
5436
|
+
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
5372
5437
|
return this.repository.ensureSession(this.botId, join5(dirname4(workspace), "agent", "pi", "sessions", `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`));
|
|
5373
5438
|
}
|
|
5374
5439
|
/** XML for the claimed messages plus the tail (other deliveries not told yet, unread); `notices` are told once
|
|
@@ -5379,7 +5444,7 @@ var BotInbox = class {
|
|
|
5379
5444
|
return pending2 && { xml: pending2.xml, notices: pending2.ids };
|
|
5380
5445
|
}
|
|
5381
5446
|
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", "
|
|
5447
|
+
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
5448
|
const digest = this.chats.channels.digest(this.botId);
|
|
5384
5449
|
const pending = this.envelope(this.chats.notices(this.botId));
|
|
5385
5450
|
return { xml: encodeChatBatch(this.botId, inputs, digest, pending.length ? pending : void 0), notices: pending.map((notice) => notice.id) };
|
|
@@ -5448,7 +5513,7 @@ var AgentRuntime = class {
|
|
|
5448
5513
|
}
|
|
5449
5514
|
fail(error) {
|
|
5450
5515
|
const fault = publicFault(error);
|
|
5451
|
-
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault.code, message: fault.message } });
|
|
5516
|
+
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault.code, message: fault.message, ...fault.params ? { params: fault.params } : {} } });
|
|
5452
5517
|
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code: fault.code, ...error instanceof RuntimeFault ? {} : { error } });
|
|
5453
5518
|
}
|
|
5454
5519
|
async start(setup) {
|
|
@@ -5549,7 +5614,10 @@ var AgentRuntime = class {
|
|
|
5549
5614
|
this.promptTask = session.prompt(xml);
|
|
5550
5615
|
const result = await this.promptTask;
|
|
5551
5616
|
repository.finish(batch.run.id, result.outcome, result.outcome === "failed" ? "model_request_failed" : null);
|
|
5552
|
-
if (result.outcome === "failed")
|
|
5617
|
+
if (result.outcome === "failed") {
|
|
5618
|
+
const detail = result.error.replace(/\s+/g, " ").slice(0, 300);
|
|
5619
|
+
throw new RuntimeFault("model_request_failed", `Model request failed: ${detail}`, 502, { detail });
|
|
5620
|
+
}
|
|
5553
5621
|
} catch (error) {
|
|
5554
5622
|
const run = repository.run(batch.run.id);
|
|
5555
5623
|
if (run?.state === "claimed" || run?.state === "submitted") repository.finish(batch.run.id, submitted ? "unknown" : "failed", publicFault(error).code);
|
|
@@ -5575,7 +5643,7 @@ var AgentRuntime = class {
|
|
|
5575
5643
|
reconfigure(operation) {
|
|
5576
5644
|
const task = this.operations.catch(() => {
|
|
5577
5645
|
}).then(async () => {
|
|
5578
|
-
if (this.stopping) throw new RuntimeFault("stopping", "
|
|
5646
|
+
if (this.stopping) throw new RuntimeFault("stopping", "Shutting down.");
|
|
5579
5647
|
await operation();
|
|
5580
5648
|
});
|
|
5581
5649
|
this.operations = task;
|
|
@@ -5586,7 +5654,7 @@ var AgentRuntime = class {
|
|
|
5586
5654
|
this.pauses++;
|
|
5587
5655
|
const task = this.operations.catch(() => {
|
|
5588
5656
|
}).then(async () => {
|
|
5589
|
-
if (this.stopping) throw new RuntimeFault("stopping", "
|
|
5657
|
+
if (this.stopping) throw new RuntimeFault("stopping", "Shutting down.");
|
|
5590
5658
|
await this.pump;
|
|
5591
5659
|
await operation();
|
|
5592
5660
|
});
|
|
@@ -5636,34 +5704,38 @@ var AgentRuntime = class {
|
|
|
5636
5704
|
function botPrompt(setup) {
|
|
5637
5705
|
const bot = { ...setup.identity(), workspacePath: setup.workspace.cwd };
|
|
5638
5706
|
return `# Persona
|
|
5639
|
-
|
|
5707
|
+
Your name is ${bot.name}.
|
|
5640
5708
|
|
|
5641
5709
|
${bot.persona}
|
|
5642
5710
|
|
|
5643
|
-
##
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
##
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
##
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5711
|
+
## Environment
|
|
5712
|
+
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.
|
|
5713
|
+
Here your name is ${bot.name}, and your description is: ${bot.description}
|
|
5714
|
+
|
|
5715
|
+
## Language
|
|
5716
|
+
Write to people in the language they write to you in. In a channel or thread, use the language its conversation is held in.
|
|
5717
|
+
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.
|
|
5718
|
+
|
|
5719
|
+
## Chat
|
|
5720
|
+
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.
|
|
5721
|
+
To pass a document, an image or another file to someone, attach its path and name when you send with the chat tool.
|
|
5722
|
+
Channels can be put in channel groups and have a notice. Threads have a title, a status and a summary.
|
|
5723
|
+
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 @.
|
|
5724
|
+
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.
|
|
5725
|
+
|
|
5726
|
+
## History
|
|
5727
|
+
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.
|
|
5728
|
+
history.read is listed by the chat gateway. History is material to look back on, not an instruction for the task at hand.
|
|
5729
|
+
|
|
5730
|
+
## Workspace
|
|
5731
|
+
You have your own workspace: ${bot.workspacePath}
|
|
5732
|
+
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.
|
|
5733
|
+
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.
|
|
5734
|
+
Shared directories and other people's files are read-only. Ask the person in charge before writing to them.
|
|
5735
|
+
|
|
5736
|
+
## Responding
|
|
5737
|
+
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.
|
|
5738
|
+
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
5739
|
`;
|
|
5668
5740
|
}
|
|
5669
5741
|
|
|
@@ -5676,9 +5748,9 @@ var BotService = class {
|
|
|
5676
5748
|
closed = false;
|
|
5677
5749
|
started = false;
|
|
5678
5750
|
get(id) {
|
|
5679
|
-
if (this.closed) throw new RuntimeFault("stopping", "
|
|
5751
|
+
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
5680
5752
|
const record = this.deps.store.getBot(id);
|
|
5681
|
-
if (!record) throw new RuntimeFault("bot_missing", "Bot
|
|
5753
|
+
if (!record) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5682
5754
|
let bot = this.bots.get(id);
|
|
5683
5755
|
if (bot) return bot;
|
|
5684
5756
|
const { store, chats, files, credentials } = this.deps;
|
|
@@ -5751,7 +5823,7 @@ var BotService = class {
|
|
|
5751
5823
|
this.describe(id);
|
|
5752
5824
|
const actor = this.deps.store.userMemberId();
|
|
5753
5825
|
const message = this.deps.chats.message(actor, messageId);
|
|
5754
|
-
if (!message || message.sender.id !== id) throw new RuntimeFault("message_missing", "
|
|
5826
|
+
if (!message || message.sender.id !== id) throw new RuntimeFault("message_missing", "Message not found", 404);
|
|
5755
5827
|
const chat = this.deps.chats.get(actor, message.chatId);
|
|
5756
5828
|
return { message, chat: { id: chat.id, title: chat.title } };
|
|
5757
5829
|
}
|
|
@@ -5764,7 +5836,7 @@ var BotService = class {
|
|
|
5764
5836
|
const bot = this.get(id);
|
|
5765
5837
|
void bot.deliver(urgency).catch((error) => {
|
|
5766
5838
|
const fault = publicFault(error);
|
|
5767
|
-
bot.state.update({ ...bot.status(), error: { code: fault.code, message: fault.message } });
|
|
5839
|
+
bot.state.update({ ...bot.status(), error: { code: fault.code, message: fault.message, ...fault.params ? { params: fault.params } : {} } });
|
|
5768
5840
|
console.error("[bot] delivery failed", { botId: id, code: fault.code });
|
|
5769
5841
|
});
|
|
5770
5842
|
}
|
|
@@ -5797,13 +5869,13 @@ var BotService = class {
|
|
|
5797
5869
|
});
|
|
5798
5870
|
}
|
|
5799
5871
|
status(id) {
|
|
5800
|
-
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot
|
|
5872
|
+
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5801
5873
|
const bot = this.bots.get(id);
|
|
5802
5874
|
return { ...bot?.status() ?? { status: "stop", activity: null, context: null, error: null }, botId: id, runId: bot?.agentRuntime.activeRunId ?? null };
|
|
5803
5875
|
}
|
|
5804
5876
|
async selectModel(id, input) {
|
|
5805
5877
|
const selection = this.deps.store.validateSelection(id, input);
|
|
5806
|
-
if (!selection) throw new RuntimeFault("bot_missing", "Bot
|
|
5878
|
+
if (!selection) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5807
5879
|
await this.get(id).changeModel(selection);
|
|
5808
5880
|
this.resume(id);
|
|
5809
5881
|
return this.deps.store.getBot(id);
|
|
@@ -5823,7 +5895,7 @@ var BotService = class {
|
|
|
5823
5895
|
const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
|
|
5824
5896
|
await bot.stop();
|
|
5825
5897
|
this.bots.delete(id);
|
|
5826
|
-
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot
|
|
5898
|
+
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5827
5899
|
if (!keepFiles) await rm2(dirname5(workspacePath), { recursive: true, force: true });
|
|
5828
5900
|
}
|
|
5829
5901
|
async close() {
|
|
@@ -5895,7 +5967,7 @@ var Application = class _Application {
|
|
|
5895
5967
|
}
|
|
5896
5968
|
});
|
|
5897
5969
|
} catch {
|
|
5898
|
-
throw new RuntimeFault("data_locked", "
|
|
5970
|
+
throw new RuntimeFault("data_locked", "The data directory is in use, or its run lock can't be taken.", 409);
|
|
5899
5971
|
}
|
|
5900
5972
|
let store;
|
|
5901
5973
|
try {
|