@goodfish/jarvy 0.4.0 → 0.6.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 +45 -0
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- package/THIRD_PARTY_NOTICE.md +1 -1
- package/dist/server/cli.js +1877 -355
- package/dist/web/assets/figure-DQGUDG-4.js +1 -0
- package/dist/web/assets/index-Ds2S6THj.js +536 -0
- package/dist/web/assets/index-TrYYVs4D.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -3
- package/dist/web/assets/figure-sTOSJn2I.js +0 -1
- package/dist/web/assets/index-DL8kubrV.js +0 -534
- package/dist/web/assets/index-Dpj-oCnv.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/server/cli.ts
|
|
4
4
|
import { existsSync as existsSync2 } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { dirname as
|
|
6
|
+
import { dirname as dirname8, join as join11 } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// src/server/api/server.ts
|
|
@@ -12,7 +12,7 @@ import { getRequestListener } from "@hono/node-server";
|
|
|
12
12
|
|
|
13
13
|
// src/server/api/app.ts
|
|
14
14
|
import { Hono as Hono10 } from "hono";
|
|
15
|
-
import { HTTPException as
|
|
15
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
16
16
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
17
17
|
|
|
18
18
|
// src/server/api/middleware/security.ts
|
|
@@ -184,6 +184,7 @@ function installAuthentication(api, account) {
|
|
|
184
184
|
|
|
185
185
|
// src/server/api/routes/bots.ts
|
|
186
186
|
import { Hono as Hono4 } from "hono";
|
|
187
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
187
188
|
|
|
188
189
|
// src/server/api/views/bot-status.ts
|
|
189
190
|
function botStatusView(status) {
|
|
@@ -379,7 +380,18 @@ function botRoutes(bots) {
|
|
|
379
380
|
routes.get("/", (c) => c.json({ bots: bots.list() }));
|
|
380
381
|
routes.post("/", jsonBody, async (c) => {
|
|
381
382
|
const input = await readJson(c);
|
|
382
|
-
return c.json(bots.create(textField(input, "name")), 201);
|
|
383
|
+
return c.json(bots.create(textField(input, "name"), input.environment === void 0 ? void 0 : environmentKind(input.environment)), 201);
|
|
384
|
+
});
|
|
385
|
+
routes.put("/:id/environment", jsonBody, async (c) => {
|
|
386
|
+
const input = await readJson(c), id = c.req.param("id");
|
|
387
|
+
if (input.hostGranted === void 0) return c.json(bots.setHome(id, environmentKind(input.home)));
|
|
388
|
+
if (typeof input.hostGranted !== "boolean") throw new HTTPException7(400, { message: "hostGranted must be true or false" });
|
|
389
|
+
return c.json(bots.grantHost(id, input.hostGranted));
|
|
390
|
+
});
|
|
391
|
+
routes.post("/:id/computer/:action", async (c) => {
|
|
392
|
+
const action = c.req.param("action");
|
|
393
|
+
if (action !== "start" && action !== "stop" && action !== "reset") throw new HTTPException7(404, { message: "Not found" });
|
|
394
|
+
return c.json(await bots.computer(c.req.param("id"), action));
|
|
383
395
|
});
|
|
384
396
|
routes.get("/:id", (c) => c.json(bots.describe(c.req.param("id"))));
|
|
385
397
|
routes.delete("/:id", async (c) => {
|
|
@@ -428,10 +440,14 @@ function botRoutes(bots) {
|
|
|
428
440
|
routes.route("/", activityRoutes(bots));
|
|
429
441
|
return routes;
|
|
430
442
|
}
|
|
443
|
+
function environmentKind(value2) {
|
|
444
|
+
if (value2 !== "computer" && value2 !== "host") throw new HTTPException7(400, { message: "environment must be computer or host" });
|
|
445
|
+
return value2;
|
|
446
|
+
}
|
|
431
447
|
|
|
432
448
|
// src/server/api/routes/chats.ts
|
|
433
449
|
import { Hono as Hono5 } from "hono";
|
|
434
|
-
import { HTTPException as
|
|
450
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
435
451
|
|
|
436
452
|
// src/server/api/views/download.ts
|
|
437
453
|
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"]);
|
|
@@ -466,10 +482,10 @@ function chatRoutes({ chats, bots }) {
|
|
|
466
482
|
const page = chats.readBefore(c.get("actorId"), c.req.param("id"), before === void 0 ? null : Number(before), Number(tail ?? c.req.query("limit") ?? 50));
|
|
467
483
|
return c.json({ messages: page.messages.map(messageView), hasOlder: page.hasOlder });
|
|
468
484
|
}
|
|
469
|
-
const
|
|
485
|
+
const after2 = Number(c.req.query("after") ?? 0);
|
|
470
486
|
const limit = Number(c.req.query("limit") ?? 1e3);
|
|
471
|
-
const messages = chats.read(c.get("actorId"), c.req.param("id"),
|
|
472
|
-
return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ??
|
|
487
|
+
const messages = chats.read(c.get("actorId"), c.req.param("id"), after2, limit);
|
|
488
|
+
return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ?? after2, hasMore: messages.length === limit });
|
|
473
489
|
});
|
|
474
490
|
routes.post("/:id/messages", jsonBody, async (c) => {
|
|
475
491
|
const message = await chats.send(c.get("actorId"), c.req.param("id"), await readJson(c));
|
|
@@ -484,16 +500,16 @@ function chatRoutes({ chats, bots }) {
|
|
|
484
500
|
try {
|
|
485
501
|
name2 = decodeURIComponent(c.req.header("x-file-name") ?? "");
|
|
486
502
|
} catch {
|
|
487
|
-
throw new
|
|
503
|
+
throw new HTTPException8(400, { message: "Invalid filename" });
|
|
488
504
|
}
|
|
489
505
|
const data = Buffer.from(await c.req.arrayBuffer());
|
|
490
|
-
if (!data.length) throw new
|
|
506
|
+
if (!data.length) throw new HTTPException8(400, { message: "Empty attachment" });
|
|
491
507
|
const mime = (c.req.header("content-type") ?? "application/octet-stream").slice(0, 100);
|
|
492
508
|
return c.json(chats.upload(c.get("actorId"), c.req.param("id"), name2, mime, data), 201);
|
|
493
509
|
});
|
|
494
510
|
routes.delete("/:id", async (c) => {
|
|
495
511
|
const id = c.req.param("id");
|
|
496
|
-
if (!await chats.delete(c.get("actorId"), id)) throw new
|
|
512
|
+
if (!await chats.delete(c.get("actorId"), id)) throw new HTTPException8(404, { message: "Chat not found" });
|
|
497
513
|
await bots.chatDeleted(id);
|
|
498
514
|
return c.json({ ok: true });
|
|
499
515
|
});
|
|
@@ -503,7 +519,7 @@ function attachmentRoutes(chats) {
|
|
|
503
519
|
const routes = new Hono5();
|
|
504
520
|
routes.get("/:id", (c) => {
|
|
505
521
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
506
|
-
if (!attachment) throw new
|
|
522
|
+
if (!attachment) throw new HTTPException8(404, { message: "Attachment not found" });
|
|
507
523
|
const size = attachment.data.length, range = byteRange(c.req.header("Range"), size);
|
|
508
524
|
const headers2 = {
|
|
509
525
|
"Content-Type": downloadType(attachment.file.mimeType),
|
|
@@ -520,14 +536,14 @@ function attachmentRoutes(chats) {
|
|
|
520
536
|
});
|
|
521
537
|
routes.get("/:id/run", (c) => {
|
|
522
538
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
523
|
-
if (!attachment) throw new
|
|
539
|
+
if (!attachment) throw new HTTPException8(404, { message: "Attachment not found" });
|
|
524
540
|
const { name: name2, mimeType } = attachment.file;
|
|
525
|
-
if (!/\.html?$/i.test(name2) && mimeType !== "text/html") throw new
|
|
541
|
+
if (!/\.html?$/i.test(name2) && mimeType !== "text/html") throw new HTTPException8(415, { message: "Only HTML attachments can run" });
|
|
526
542
|
c.set("sandboxedDocument", true);
|
|
527
543
|
return c.body(new Uint8Array(attachment.data), 200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": String(attachment.data.length) });
|
|
528
544
|
});
|
|
529
545
|
routes.delete("/:id", (c) => {
|
|
530
|
-
if (!chats.removePendingAttachment(c.get("actorId"), c.req.param("id"))) throw new
|
|
546
|
+
if (!chats.removePendingAttachment(c.get("actorId"), c.req.param("id"))) throw new HTTPException8(404, { message: "Pending attachment not found" });
|
|
531
547
|
return c.json({ ok: true });
|
|
532
548
|
});
|
|
533
549
|
return routes;
|
|
@@ -607,6 +623,7 @@ data: ${JSON.stringify(data)}
|
|
|
607
623
|
if (change.kind === "sections" && change.ownerId === actorId) send("sections", {});
|
|
608
624
|
if (change.kind === "inbox" && change.memberId === actorId) send("inbox", {});
|
|
609
625
|
if (change.kind === "bot" && bots.owns(change.id)) send("bot", { botId: change.id, status: botStatusView(change.status) });
|
|
626
|
+
if (change.kind === "bots") send("bots", {});
|
|
610
627
|
}, () => disconnect());
|
|
611
628
|
const heartbeat = setInterval(() => {
|
|
612
629
|
if (!account.authenticated(token)) disconnect();
|
|
@@ -637,13 +654,13 @@ data: ${JSON.stringify(data)}
|
|
|
637
654
|
|
|
638
655
|
// src/server/api/routes/channels.ts
|
|
639
656
|
import { Hono as Hono8 } from "hono";
|
|
640
|
-
import { HTTPException as
|
|
657
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
641
658
|
function channelRoutes({ chats }) {
|
|
642
659
|
const routes = new Hono8();
|
|
643
660
|
const channels = chats.channels;
|
|
644
661
|
const chat = (actorId, id) => {
|
|
645
662
|
const found = chats.get(actorId, id);
|
|
646
|
-
if (!found) throw new
|
|
663
|
+
if (!found) throw new HTTPException9(404, { message: "Chat not found" });
|
|
647
664
|
return chatView(found, (member) => chats.member(member));
|
|
648
665
|
};
|
|
649
666
|
routes.get("/members", (c) => c.json({ members: channels.everyone(c.get("actorId")) }));
|
|
@@ -664,7 +681,7 @@ function channelRoutes({ chats }) {
|
|
|
664
681
|
routes.post("/sections", jsonBody, async (c) => c.json(channels.createSection(c.get("actorId"), await readJson(c)), 201));
|
|
665
682
|
routes.patch("/sections/:id", jsonBody, async (c) => c.json(channels.updateSection(c.get("actorId"), c.req.param("id"), await readJson(c))));
|
|
666
683
|
routes.delete("/sections/:id", (c) => {
|
|
667
|
-
if (!channels.deleteSection(c.get("actorId"), c.req.param("id"))) throw new
|
|
684
|
+
if (!channels.deleteSection(c.get("actorId"), c.req.param("id"))) throw new HTTPException9(404, { message: "Section not found" });
|
|
668
685
|
return c.json({ ok: true });
|
|
669
686
|
});
|
|
670
687
|
routes.post("/channels", jsonBody, async (c) => {
|
|
@@ -680,7 +697,7 @@ function channelRoutes({ chats }) {
|
|
|
680
697
|
routes.get("/chats/:id", (c) => c.json(chat(c.get("actorId"), c.req.param("id"))));
|
|
681
698
|
routes.get("/messages/:id", (c) => {
|
|
682
699
|
const found = chats.message(c.get("actorId"), c.req.param("id"));
|
|
683
|
-
if (!found) throw new
|
|
700
|
+
if (!found) throw new HTTPException9(404, { message: "Message not found" });
|
|
684
701
|
return c.json(messageView(found));
|
|
685
702
|
});
|
|
686
703
|
routes.post("/messages/:id/thread", (c) => {
|
|
@@ -785,7 +802,7 @@ function createHttpApp(services, webRoot) {
|
|
|
785
802
|
api.route("/", eventRoutes(services));
|
|
786
803
|
api.route("/", liveRoutes(services));
|
|
787
804
|
api.all("*", () => {
|
|
788
|
-
throw new
|
|
805
|
+
throw new HTTPException10(404, { message: "API route not found" });
|
|
789
806
|
});
|
|
790
807
|
app.route("/api", api);
|
|
791
808
|
app.get("*", serveStatic({ root: webRoot, onFound: (path, c) => {
|
|
@@ -795,8 +812,8 @@ function createHttpApp(services, webRoot) {
|
|
|
795
812
|
c.header("Cache-Control", "no-store");
|
|
796
813
|
} }));
|
|
797
814
|
app.all("*", (c) => {
|
|
798
|
-
if (c.req.method !== "GET" && c.req.method !== "HEAD") throw new
|
|
799
|
-
throw new
|
|
815
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") throw new HTTPException10(405, { message: "Method not allowed" });
|
|
816
|
+
throw new HTTPException10(404, { message: "Web assets missing; run npm run build" });
|
|
800
817
|
});
|
|
801
818
|
return app;
|
|
802
819
|
}
|
|
@@ -809,12 +826,13 @@ function createJarvyServer(services, webRoot) {
|
|
|
809
826
|
|
|
810
827
|
// src/server/shared/models.ts
|
|
811
828
|
var BOT_COLORS = ["gray", "blue", "green", "amber", "rose", "violet"];
|
|
829
|
+
var AGENT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
812
830
|
|
|
813
831
|
// src/server/infra/storage/store.ts
|
|
814
|
-
import { createHash, randomBytes, randomInt as
|
|
832
|
+
import { createHash, randomBytes, randomInt as randomInt3, randomUUID as randomUUID8, scryptSync, timingSafeEqual } from "node:crypto";
|
|
815
833
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
816
834
|
import { homedir } from "node:os";
|
|
817
|
-
import { dirname, join, resolve } from "node:path";
|
|
835
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
818
836
|
import { DatabaseSync } from "node:sqlite";
|
|
819
837
|
|
|
820
838
|
// src/server/infra/storage/migrations/composition-schema.ts
|
|
@@ -936,13 +954,13 @@ function migrateChannels(db) {
|
|
|
936
954
|
SELECT cm.chat_id, cm.member_id, COALESCE((SELECT MAX(sequence) FROM messages m WHERE m.chat_id=cm.chat_id), 0) FROM chat_members cm;
|
|
937
955
|
`);
|
|
938
956
|
const taken = new Set(db.prepare("SELECT username FROM account").all().map((row) => row.username.toLowerCase()));
|
|
939
|
-
const
|
|
957
|
+
const rename3 = db.prepare("UPDATE bots SET name=? WHERE id=?");
|
|
940
958
|
for (const bot of db.prepare("SELECT id,name FROM bots ORDER BY created_at,id").all()) {
|
|
941
959
|
const base = bot.name.replace(/@/g, "").trim() || "Bot";
|
|
942
960
|
let name2 = base;
|
|
943
961
|
for (let suffix = 2; taken.has(name2.toLowerCase()); suffix++) name2 = `${base} ${suffix}`;
|
|
944
962
|
taken.add(name2.toLowerCase());
|
|
945
|
-
if (name2 !== bot.name)
|
|
963
|
+
if (name2 !== bot.name) rename3.run(name2, bot.id);
|
|
946
964
|
}
|
|
947
965
|
db.exec("PRAGMA user_version=12; COMMIT;");
|
|
948
966
|
} catch (error) {
|
|
@@ -1083,12 +1101,76 @@ function migrateTitles(db) {
|
|
|
1083
1101
|
}
|
|
1084
1102
|
}
|
|
1085
1103
|
|
|
1104
|
+
// src/server/infra/storage/migrations/agent-schema.ts
|
|
1105
|
+
function migrateAgent(db) {
|
|
1106
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1107
|
+
if (version >= 19) return;
|
|
1108
|
+
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
1109
|
+
try {
|
|
1110
|
+
const sequence = db.prepare("SELECT seq FROM sqlite_sequence WHERE name='inbox'").get()?.seq ?? 0;
|
|
1111
|
+
db.exec(`
|
|
1112
|
+
CREATE TABLE settings(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
1113
|
+
CREATE TABLE inbox_next(id INTEGER PRIMARY KEY AUTOINCREMENT, member_id TEXT NOT NULL REFERENCES members(id),
|
|
1114
|
+
kind TEXT NOT NULL CHECK(kind IN ('message','reaction','note','onboard','change')), reason TEXT NOT NULL,
|
|
1115
|
+
urgency TEXT NOT NULL CHECK(urgency IN ('interrupt','queue','digest')),
|
|
1116
|
+
chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE, message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
1117
|
+
actor_id TEXT REFERENCES members(id), detail TEXT,
|
|
1118
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','claimed','told','done','failed','interrupted','unknown','cancelled')),
|
|
1119
|
+
run_id TEXT REFERENCES bot_runs(id), created_at INTEGER NOT NULL, told_at INTEGER);
|
|
1120
|
+
INSERT INTO inbox_next(id,member_id,kind,reason,urgency,chat_id,message_id,actor_id,detail,state,run_id,created_at,told_at)
|
|
1121
|
+
SELECT id,member_id,kind,reason,urgency,chat_id,message_id,actor_id,detail,state,run_id,created_at,told_at FROM inbox ORDER BY id;
|
|
1122
|
+
DROP TABLE inbox;
|
|
1123
|
+
ALTER TABLE inbox_next RENAME TO inbox;
|
|
1124
|
+
CREATE UNIQUE INDEX inbox_one_message ON inbox(member_id, message_id) WHERE kind='message';
|
|
1125
|
+
CREATE INDEX inbox_open ON inbox(member_id, state, id);
|
|
1126
|
+
CREATE INDEX inbox_run ON inbox(run_id) WHERE run_id IS NOT NULL;
|
|
1127
|
+
`);
|
|
1128
|
+
db.prepare("UPDATE sqlite_sequence SET seq=MAX(seq,?) WHERE name='inbox'").run(sequence);
|
|
1129
|
+
db.prepare(`INSERT INTO settings(key,value) SELECT 'agent_onboarded',? FROM bots
|
|
1130
|
+
WHERE id=? AND deleted_at IS NULL AND connection_id IS NOT NULL`).run(String(Date.now()), AGENT_BOT_ID);
|
|
1131
|
+
db.exec("PRAGMA user_version=19");
|
|
1132
|
+
if (db.prepare("PRAGMA foreign_key_check").all().length) throw new Error("Migration has invalid references");
|
|
1133
|
+
db.exec("COMMIT");
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
db.exec("ROLLBACK");
|
|
1136
|
+
throw error;
|
|
1137
|
+
} finally {
|
|
1138
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/server/infra/storage/migrations/environment-schema.ts
|
|
1143
|
+
function migrateEnvironments(db) {
|
|
1144
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1145
|
+
if (version >= 20) return;
|
|
1146
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1147
|
+
try {
|
|
1148
|
+
db.exec(`
|
|
1149
|
+
CREATE TABLE environments(id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES members(id),
|
|
1150
|
+
kind TEXT NOT NULL CHECK(kind IN ('host','computer')), name TEXT NOT NULL DEFAULT '',
|
|
1151
|
+
connection TEXT NOT NULL DEFAULT '{}', directory TEXT, bot_id TEXT REFERENCES bots(id),
|
|
1152
|
+
created_at INTEGER NOT NULL, deleted_at INTEGER);
|
|
1153
|
+
CREATE UNIQUE INDEX environments_host ON environments(owner_id) WHERE kind='host' AND deleted_at IS NULL;
|
|
1154
|
+
CREATE UNIQUE INDEX environments_computer ON environments(bot_id) WHERE kind='computer' AND deleted_at IS NULL;
|
|
1155
|
+
CREATE TABLE environment_grants(environment_id TEXT NOT NULL REFERENCES environments(id),
|
|
1156
|
+
bot_id TEXT NOT NULL REFERENCES bots(id), granted_by TEXT NOT NULL REFERENCES members(id),
|
|
1157
|
+
granted_at INTEGER NOT NULL, PRIMARY KEY(environment_id, bot_id));
|
|
1158
|
+
ALTER TABLE bots ADD COLUMN home_environment_id TEXT REFERENCES environments(id);
|
|
1159
|
+
ALTER TABLE bots ADD COLUMN current_environment_id TEXT REFERENCES environments(id);
|
|
1160
|
+
PRAGMA user_version=20;
|
|
1161
|
+
`);
|
|
1162
|
+
db.exec("COMMIT");
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
db.exec("ROLLBACK");
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1086
1169
|
// src/server/infra/storage/migrations/schema.ts
|
|
1087
1170
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1088
1171
|
|
|
1089
1172
|
// src/server/infra/storage/migrations/prototype-schema.ts
|
|
1090
1173
|
import { randomUUID } from "node:crypto";
|
|
1091
|
-
var DEFAULT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
1092
1174
|
function migratePrototype(db) {
|
|
1093
1175
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1094
1176
|
if (version >= 4) return;
|
|
@@ -1121,9 +1203,9 @@ function migratePrototype(db) {
|
|
|
1121
1203
|
system_prompt TEXT NOT NULL, model_id TEXT NOT NULL, avatar_mime TEXT, avatar_data BLOB,
|
|
1122
1204
|
avatar_updated_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL);
|
|
1123
1205
|
INSERT INTO bots(id,name,description,system_prompt,model_id,created_at)
|
|
1124
|
-
SELECT '${
|
|
1206
|
+
SELECT '${AGENT_BOT_ID}',name,'',system_prompt,'',0 FROM bot_settings WHERE id=1;
|
|
1125
1207
|
ALTER TABLE chats ADD COLUMN bot_id TEXT REFERENCES bots(id);
|
|
1126
|
-
UPDATE chats SET bot_id='${
|
|
1208
|
+
UPDATE chats SET bot_id='${AGENT_BOT_ID}';
|
|
1127
1209
|
CREATE INDEX chat_by_bot ON chats(bot_id,updated_at DESC);
|
|
1128
1210
|
CREATE TABLE attachments (id TEXT PRIMARY KEY, chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
1129
1211
|
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE, name TEXT NOT NULL,
|
|
@@ -1206,7 +1288,7 @@ function migrateRuntime(db) {
|
|
|
1206
1288
|
// src/server/infra/storage/migrations/schema.ts
|
|
1207
1289
|
function migrateDatabase(db) {
|
|
1208
1290
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1209
|
-
if (version >
|
|
1291
|
+
if (version > 20) throw new Error(`Unsupported database version: ${version}`);
|
|
1210
1292
|
migratePrototype(db);
|
|
1211
1293
|
if (version >= 5) {
|
|
1212
1294
|
migrateRuntime(db);
|
|
@@ -1222,6 +1304,8 @@ function migrateDatabase(db) {
|
|
|
1222
1304
|
migrateInbox(db);
|
|
1223
1305
|
migrateQuotes(db);
|
|
1224
1306
|
migrateTitles(db);
|
|
1307
|
+
migrateAgent(db);
|
|
1308
|
+
migrateEnvironments(db);
|
|
1225
1309
|
return;
|
|
1226
1310
|
}
|
|
1227
1311
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1295,6 +1379,8 @@ function migrateDatabase(db) {
|
|
|
1295
1379
|
migrateInbox(db);
|
|
1296
1380
|
migrateQuotes(db);
|
|
1297
1381
|
migrateTitles(db);
|
|
1382
|
+
migrateAgent(db);
|
|
1383
|
+
migrateEnvironments(db);
|
|
1298
1384
|
}
|
|
1299
1385
|
|
|
1300
1386
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1618,7 +1704,7 @@ var InteractionRepository = class {
|
|
|
1618
1704
|
}
|
|
1619
1705
|
/** Notices the Bot has not been told, oldest first, in chats it can still read. */
|
|
1620
1706
|
notices(botId) {
|
|
1621
|
-
const rows2 = this.db.prepare(`SELECT n.id,n.kind,n.chat_id AS chatId,c.title AS chatTitle,c.type AS chatType,n.message_id AS messageId,n.actor_id AS actorId,n.detail,n.created_at AS createdAt
|
|
1707
|
+
const rows2 = this.db.prepare(`SELECT n.id,n.kind,n.urgency,n.chat_id AS chatId,c.title AS chatTitle,c.type AS chatType,n.message_id AS messageId,n.actor_id AS actorId,n.detail,n.created_at AS createdAt
|
|
1622
1708
|
FROM inbox n JOIN chats c ON c.id=n.chat_id WHERE n.member_id=? AND n.kind<>'message' AND n.state='pending' ORDER BY n.id`).all(botId);
|
|
1623
1709
|
return rows2.filter((row) => this.chats.canAccess(botId, row.chatId)).map(({ actorId, ...row }) => ({ ...row, actor: this.chats.member(actorId) }));
|
|
1624
1710
|
}
|
|
@@ -1848,9 +1934,9 @@ var ChatRepository = class {
|
|
|
1848
1934
|
AND (instr(lower(c.title),lower(?))>0 OR instr(lower(c.topic),lower(?))>0 OR instr(lower(COALESCE(n.text,'')),lower(?))>0)
|
|
1849
1935
|
ORDER BY c.updated_at DESC LIMIT ?`).all(actorId, query, query, query, limit).map((row) => row.id);
|
|
1850
1936
|
}
|
|
1851
|
-
messages(actorId, chatId,
|
|
1937
|
+
messages(actorId, chatId, after2 = 0, limit = 1e3) {
|
|
1852
1938
|
this.assertAccess(actorId, chatId);
|
|
1853
|
-
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE chat_id=? AND sequence>? ORDER BY sequence LIMIT ?`).all(chatId,
|
|
1939
|
+
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE chat_id=? AND sequence>? ORDER BY sequence LIMIT ?`).all(chatId, after2, limit);
|
|
1854
1940
|
return rows2.map((row) => this.messageFromRow(row));
|
|
1855
1941
|
}
|
|
1856
1942
|
message(actorId, id) {
|
|
@@ -1874,7 +1960,7 @@ var ChatRepository = class {
|
|
|
1874
1960
|
return rows2.map(({ historyId, toolCallId, ...row }) => ({ ...row, source: historyId === null ? null : { historyId, toolCallId } }));
|
|
1875
1961
|
}
|
|
1876
1962
|
messageFromRow(row) {
|
|
1877
|
-
const attachments = this.db.prepare("SELECT id,name,mime_type AS mimeType,size FROM attachments WHERE message_id=? ORDER BY created_at,
|
|
1963
|
+
const attachments = this.db.prepare("SELECT id,name,mime_type AS mimeType,size FROM attachments WHERE message_id=? ORDER BY created_at,rowid").all(row.id);
|
|
1878
1964
|
return {
|
|
1879
1965
|
id: row.id,
|
|
1880
1966
|
chatId: row.chatId,
|
|
@@ -2135,9 +2221,112 @@ var RuntimeRepository = class {
|
|
|
2135
2221
|
};
|
|
2136
2222
|
|
|
2137
2223
|
// src/server/infra/pi/catalog.ts
|
|
2138
|
-
import {
|
|
2139
|
-
|
|
2140
|
-
|
|
2224
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2225
|
+
import { dirname, join } from "node:path";
|
|
2226
|
+
import { builtinProviders, getBuiltinModelDataGeneratedAt } from "@earendil-works/pi-ai/providers/all";
|
|
2227
|
+
var CATALOG_URL = "https://pi.dev/api/models";
|
|
2228
|
+
var REFRESH_MS = 4 * 60 * 60 * 1e3;
|
|
2229
|
+
var TIMEOUT_MS = 15e3;
|
|
2230
|
+
var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getModels().map((model) => model.api)));
|
|
2231
|
+
var ModelCatalog = class {
|
|
2232
|
+
path;
|
|
2233
|
+
kept;
|
|
2234
|
+
loaded = false;
|
|
2235
|
+
overlaid;
|
|
2236
|
+
loading;
|
|
2237
|
+
constructor(dataDir) {
|
|
2238
|
+
this.path = join(dataDir, "pi", "models.json");
|
|
2239
|
+
}
|
|
2240
|
+
providers() {
|
|
2241
|
+
this.refresh();
|
|
2242
|
+
return this.overlaid ?? builtinProviders();
|
|
2243
|
+
}
|
|
2244
|
+
/** Newer entries for one provider's models (a connection's catalog comes from Pi with its credentials). */
|
|
2245
|
+
overlay(providerId, models) {
|
|
2246
|
+
return merge(models, this.entries(providerId));
|
|
2247
|
+
}
|
|
2248
|
+
/** Loads the kept catalog, then checks pi.dev when the last check is 4 hours old; never waits. */
|
|
2249
|
+
refresh() {
|
|
2250
|
+
const due = process.env.PI_OFFLINE === void 0 && Date.now() - (this.kept?.checkedAt ?? 0) >= REFRESH_MS;
|
|
2251
|
+
if (this.loading || this.loaded && !due) return;
|
|
2252
|
+
this.loading = (async () => {
|
|
2253
|
+
if (!this.loaded) {
|
|
2254
|
+
this.kept = await this.read();
|
|
2255
|
+
this.loaded = true;
|
|
2256
|
+
this.apply();
|
|
2257
|
+
}
|
|
2258
|
+
if (!due) return;
|
|
2259
|
+
await this.fetch();
|
|
2260
|
+
this.apply();
|
|
2261
|
+
})().catch((error) => console.warn("[models] catalog refresh failed", { error: error instanceof Error ? error.message : String(error) })).finally(() => {
|
|
2262
|
+
this.loading = void 0;
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
async fetch() {
|
|
2266
|
+
const previous = this.kept;
|
|
2267
|
+
const checkedAt = Date.now();
|
|
2268
|
+
try {
|
|
2269
|
+
const response = await fetch(CATALOG_URL, {
|
|
2270
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
2271
|
+
headers: { accept: "application/json", "user-agent": "Jarvy", ...previous?.etag ? { "if-none-match": previous.etag } : {} }
|
|
2272
|
+
});
|
|
2273
|
+
if (response.status === 304 && previous) {
|
|
2274
|
+
this.kept = { ...previous, checkedAt };
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2278
|
+
const body = await response.json();
|
|
2279
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("not a catalog");
|
|
2280
|
+
const lastModified = Date.parse(response.headers.get("last-modified") ?? "");
|
|
2281
|
+
this.kept = { models: body, etag: response.headers.get("etag") ?? void 0, lastModified: Number.isNaN(lastModified) ? 0 : lastModified, checkedAt };
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
this.kept = { ...previous ?? { models: {}, lastModified: 0 }, checkedAt };
|
|
2284
|
+
throw error;
|
|
2285
|
+
} finally {
|
|
2286
|
+
await this.write().catch((error) => console.warn("[models] catalog not saved", { error: error instanceof Error ? error.message : String(error) }));
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
apply() {
|
|
2290
|
+
this.overlaid = builtinProviders().map((provider) => {
|
|
2291
|
+
const entries = this.entries(provider.id);
|
|
2292
|
+
if (!entries.length) return provider;
|
|
2293
|
+
const models = merge(provider.getModels(), entries);
|
|
2294
|
+
return { ...provider, getModels: () => models };
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
entries(providerId) {
|
|
2298
|
+
const kept = this.kept, generatedAt = getBuiltinModelDataGeneratedAt();
|
|
2299
|
+
if (!kept || generatedAt !== void 0 && kept.lastModified <= generatedAt) return [];
|
|
2300
|
+
const models = kept.models[providerId];
|
|
2301
|
+
if (!models || typeof models !== "object") return [];
|
|
2302
|
+
return Object.values(models).filter((model) => typeof model?.id === "string" && typeof model.api === "string" && modelApis.has(model.api) && (model.type === void 0 || model.type === "chat")).map((model) => ({ ...model, provider: providerId }));
|
|
2303
|
+
}
|
|
2304
|
+
async read() {
|
|
2305
|
+
try {
|
|
2306
|
+
const kept = JSON.parse(await readFile(this.path, "utf8"));
|
|
2307
|
+
return kept && typeof kept.models === "object" ? kept : void 0;
|
|
2308
|
+
} catch {
|
|
2309
|
+
return void 0;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
async write() {
|
|
2313
|
+
await mkdir(dirname(this.path), { recursive: true, mode: 448 });
|
|
2314
|
+
const next = `${this.path}.tmp`;
|
|
2315
|
+
await writeFile(next, JSON.stringify(this.kept), { mode: 384 });
|
|
2316
|
+
await rename(next, this.path);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
function merge(models, newer) {
|
|
2320
|
+
const merged = [...models];
|
|
2321
|
+
for (const model of newer) {
|
|
2322
|
+
const index = merged.findIndex((entry) => entry.id === model.id);
|
|
2323
|
+
if (index >= 0) merged[index] = model;
|
|
2324
|
+
else merged.push(model);
|
|
2325
|
+
}
|
|
2326
|
+
return merged;
|
|
2327
|
+
}
|
|
2328
|
+
function providerCatalog(providers) {
|
|
2329
|
+
return providers.flatMap((provider) => {
|
|
2141
2330
|
const models = provider.getModels();
|
|
2142
2331
|
if (!models.length && provider.getAllModels?.().length) return [];
|
|
2143
2332
|
const common = { id: provider.id, api: models[0]?.api ?? "openai-completions", modelCount: models.length };
|
|
@@ -2147,7 +2336,6 @@ function providerCatalog() {
|
|
|
2147
2336
|
];
|
|
2148
2337
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2149
2338
|
}
|
|
2150
|
-
var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getModels().map((model) => model.api)));
|
|
2151
2339
|
|
|
2152
2340
|
// src/server/infra/storage/credential-repository.ts
|
|
2153
2341
|
function credentialValue(value2) {
|
|
@@ -2191,8 +2379,133 @@ var CredentialRepository = class {
|
|
|
2191
2379
|
}
|
|
2192
2380
|
};
|
|
2193
2381
|
|
|
2382
|
+
// src/server/infra/storage/agent-repository.ts
|
|
2383
|
+
import { randomInt as randomInt2 } from "node:crypto";
|
|
2384
|
+
var AgentRepository = class {
|
|
2385
|
+
constructor(db, ownerId) {
|
|
2386
|
+
this.db = db;
|
|
2387
|
+
this.ownerId = ownerId;
|
|
2388
|
+
}
|
|
2389
|
+
/** Brings the agent Bot back when it is missing (deleted before it became undeletable): the same id, the first free
|
|
2390
|
+
* name of Jarvy, Jarvy 2, …, no persona and no model, so the owner picks one and is welcomed again. */
|
|
2391
|
+
ensure() {
|
|
2392
|
+
const row = this.db.prepare("SELECT deleted_at AS deletedAt FROM bots WHERE id=?").get(AGENT_BOT_ID);
|
|
2393
|
+
if (row && row.deletedAt === null) return;
|
|
2394
|
+
const taken = this.db.prepare(`SELECT 1 FROM account WHERE lower(username)=lower(:name)
|
|
2395
|
+
UNION ALL SELECT 1 FROM bots WHERE lower(name)=lower(:name) AND deleted_at IS NULL LIMIT 1`);
|
|
2396
|
+
let name2 = "Jarvy";
|
|
2397
|
+
for (let n = 2; taken.get({ name: name2 }); n++) name2 = `Jarvy ${n}`;
|
|
2398
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2399
|
+
try {
|
|
2400
|
+
if (row) this.db.prepare(`UPDATE bots SET deleted_at=NULL,name=?,description='',system_prompt='',connection_id=NULL,model_id='',thinking_level='auto'
|
|
2401
|
+
WHERE id=?`).run(name2, AGENT_BOT_ID);
|
|
2402
|
+
else {
|
|
2403
|
+
this.db.prepare("INSERT OR IGNORE INTO members VALUES(?,'bot')").run(AGENT_BOT_ID);
|
|
2404
|
+
this.db.prepare("INSERT INTO bots(id,name,description,system_prompt,model_id,created_at,owner_id,thinking_level,color) VALUES(?,?,'','','',?,?,'auto',?)").run(AGENT_BOT_ID, name2, Date.now(), this.ownerId(), BOT_COLORS[randomInt2(BOT_COLORS.length)]);
|
|
2405
|
+
}
|
|
2406
|
+
this.db.prepare("DELETE FROM settings WHERE key='agent_onboarded'").run();
|
|
2407
|
+
this.db.exec("COMMIT");
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
this.db.exec("ROLLBACK");
|
|
2410
|
+
throw error;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
/** Records that the owner is being welcomed; false when that already happened, so only one caller asks. */
|
|
2414
|
+
markOnboarded() {
|
|
2415
|
+
return Number(this.db.prepare("INSERT OR IGNORE INTO settings(key,value) VALUES('agent_onboarded',?)").run(String(Date.now())).changes) > 0;
|
|
2416
|
+
}
|
|
2417
|
+
/** One delivery about no message, for one Bot; the caller wakes it. */
|
|
2418
|
+
notify(input) {
|
|
2419
|
+
this.db.prepare("INSERT INTO inbox(member_id,kind,reason,urgency,chat_id,actor_id,detail,created_at) VALUES(?,?,?,?,?,?,?,?)").run(input.botId, input.kind, input.reason, input.urgency, input.chatId, input.actorId, input.detail, Date.now());
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
|
|
2423
|
+
// src/server/infra/storage/environment-repository.ts
|
|
2424
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2425
|
+
var COLUMNS = "id, owner_id AS ownerId, kind, bot_id AS botId, connection";
|
|
2426
|
+
var record = (row) => ({ id: row.id, ownerId: row.ownerId, kind: row.kind, botId: row.botId, spec: JSON.parse(row.connection) });
|
|
2427
|
+
var EnvironmentRepository = class {
|
|
2428
|
+
constructor(db, ownerId) {
|
|
2429
|
+
this.db = db;
|
|
2430
|
+
this.ownerId = ownerId;
|
|
2431
|
+
}
|
|
2432
|
+
get(id) {
|
|
2433
|
+
const row = this.db.prepare(`SELECT ${COLUMNS} FROM environments WHERE id=? AND deleted_at IS NULL`).get(id);
|
|
2434
|
+
return row ? record(row) : null;
|
|
2435
|
+
}
|
|
2436
|
+
/** The owner's computer. */
|
|
2437
|
+
host() {
|
|
2438
|
+
const owner = this.ownerId();
|
|
2439
|
+
const find = () => this.db.prepare(`SELECT ${COLUMNS} FROM environments WHERE owner_id=? AND kind='host' AND deleted_at IS NULL`).get(owner);
|
|
2440
|
+
const found = find();
|
|
2441
|
+
if (found) return record(found);
|
|
2442
|
+
this.db.prepare("INSERT OR IGNORE INTO environments(id,owner_id,kind,created_at) VALUES(?,?,'host',?)").run(randomUUID7(), owner, Date.now());
|
|
2443
|
+
return record(find());
|
|
2444
|
+
}
|
|
2445
|
+
computerOf(botId) {
|
|
2446
|
+
const row = this.db.prepare(`SELECT ${COLUMNS} FROM environments WHERE bot_id=? AND kind='computer' AND deleted_at IS NULL`).get(botId);
|
|
2447
|
+
return row ? record(row) : null;
|
|
2448
|
+
}
|
|
2449
|
+
/** Every live Bot's computer. */
|
|
2450
|
+
computers() {
|
|
2451
|
+
return this.db.prepare(`SELECT e.id, e.owner_id AS ownerId, e.kind, e.bot_id AS botId, e.connection FROM environments e JOIN bots b ON b.id=e.bot_id
|
|
2452
|
+
WHERE e.kind='computer' AND e.deleted_at IS NULL AND b.deleted_at IS NULL`).all().map(record);
|
|
2453
|
+
}
|
|
2454
|
+
/** Gives the Bot a computer, reached by the container's name. */
|
|
2455
|
+
addComputer(botId, container) {
|
|
2456
|
+
const existing = this.computerOf(botId);
|
|
2457
|
+
if (existing) return existing;
|
|
2458
|
+
const id = randomUUID7();
|
|
2459
|
+
this.db.prepare("INSERT INTO environments(id,owner_id,kind,connection,directory,bot_id,created_at) VALUES(?,?,'computer',?,'/workspace',?,?)").run(id, this.ownerId(), JSON.stringify({ container }), botId, Date.now());
|
|
2460
|
+
return this.get(id);
|
|
2461
|
+
}
|
|
2462
|
+
/** The Bot's computer is gone: whoever was placed on it is back on the owner's computer. */
|
|
2463
|
+
dropComputer(botId) {
|
|
2464
|
+
const computer = this.computerOf(botId);
|
|
2465
|
+
if (!computer) return;
|
|
2466
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2467
|
+
try {
|
|
2468
|
+
this.db.prepare("UPDATE bots SET home_environment_id=NULL WHERE home_environment_id=?").run(computer.id);
|
|
2469
|
+
this.db.prepare("UPDATE bots SET current_environment_id=NULL WHERE current_environment_id=?").run(computer.id);
|
|
2470
|
+
this.db.prepare("DELETE FROM environment_grants WHERE environment_id=?").run(computer.id);
|
|
2471
|
+
this.db.prepare("UPDATE environments SET deleted_at=? WHERE id=?").run(Date.now(), computer.id);
|
|
2472
|
+
this.db.exec("COMMIT");
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
this.db.exec("ROLLBACK");
|
|
2475
|
+
throw error;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
/** The Bot's default environment and the one it works in now. */
|
|
2479
|
+
placement(botId) {
|
|
2480
|
+
const row = this.db.prepare("SELECT home_environment_id AS home, current_environment_id AS current FROM bots WHERE id=?").get(botId);
|
|
2481
|
+
const home = row?.home && this.get(row.home) || this.host();
|
|
2482
|
+
const current = row?.current && this.get(row.current) || home;
|
|
2483
|
+
return { home, current };
|
|
2484
|
+
}
|
|
2485
|
+
/** Makes an environment the Bot's default and moves the Bot there. */
|
|
2486
|
+
setHome(botId, environmentId) {
|
|
2487
|
+
this.db.prepare("UPDATE bots SET home_environment_id=?, current_environment_id=? WHERE id=?").run(environmentId, environmentId, botId);
|
|
2488
|
+
}
|
|
2489
|
+
/** Moves the Bot without changing its default. */
|
|
2490
|
+
setCurrent(botId, environmentId) {
|
|
2491
|
+
this.db.prepare("UPDATE bots SET current_environment_id=? WHERE id=?").run(environmentId, botId);
|
|
2492
|
+
}
|
|
2493
|
+
/** The environments the owner granted the Bot, oldest grant first. */
|
|
2494
|
+
grants(botId) {
|
|
2495
|
+
return this.db.prepare(`SELECT e.id, e.owner_id AS ownerId, e.kind, e.bot_id AS botId, e.connection FROM environment_grants g JOIN environments e ON e.id=g.environment_id
|
|
2496
|
+
WHERE g.bot_id=? AND e.deleted_at IS NULL ORDER BY g.granted_at, e.id`).all(botId).map(record);
|
|
2497
|
+
}
|
|
2498
|
+
/** A grant lasts until it is revoked (A50); false when nothing changed. */
|
|
2499
|
+
grant(environmentId, botId, by) {
|
|
2500
|
+
return Number(this.db.prepare("INSERT OR IGNORE INTO environment_grants(environment_id,bot_id,granted_by,granted_at) VALUES(?,?,?,?)").run(environmentId, botId, by, Date.now()).changes) > 0;
|
|
2501
|
+
}
|
|
2502
|
+
revoke(environmentId, botId) {
|
|
2503
|
+
return Number(this.db.prepare("DELETE FROM environment_grants WHERE environment_id=? AND bot_id=?").run(environmentId, botId).changes) > 0;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2194
2507
|
// src/server/infra/storage/store.ts
|
|
2195
|
-
var defaultDataDir = () => process.env.JARVY_DATA_DIR ||
|
|
2508
|
+
var defaultDataDir = () => process.env.JARVY_DATA_DIR || join2(homedir(), ".jarvy");
|
|
2196
2509
|
var DATABASE_FILENAME = "jarvy.db";
|
|
2197
2510
|
var SESSION_LIFETIME_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2198
2511
|
function passwordHash(password, salt) {
|
|
@@ -2260,10 +2573,12 @@ var Store = class {
|
|
|
2260
2573
|
chats;
|
|
2261
2574
|
runtime;
|
|
2262
2575
|
credentials;
|
|
2576
|
+
agent;
|
|
2577
|
+
environments;
|
|
2263
2578
|
constructor(dataDir = defaultDataDir()) {
|
|
2264
2579
|
const root = resolve(dataDir);
|
|
2265
2580
|
mkdirSync(root, { recursive: true, mode: 448 });
|
|
2266
|
-
this.path =
|
|
2581
|
+
this.path = join2(root, DATABASE_FILENAME);
|
|
2267
2582
|
this.db = new DatabaseSync(this.path);
|
|
2268
2583
|
chmodSync(this.path, 384);
|
|
2269
2584
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;");
|
|
@@ -2276,6 +2591,9 @@ var Store = class {
|
|
|
2276
2591
|
this.chats = new ChatRepository(this.db);
|
|
2277
2592
|
this.runtime = new RuntimeRepository(this.db);
|
|
2278
2593
|
this.credentials = new CredentialRepository(this.db);
|
|
2594
|
+
this.agent = new AgentRepository(this.db, () => this.userMemberId());
|
|
2595
|
+
this.environments = new EnvironmentRepository(this.db, () => this.userMemberId());
|
|
2596
|
+
this.agent.ensure();
|
|
2279
2597
|
}
|
|
2280
2598
|
close() {
|
|
2281
2599
|
this.db.close();
|
|
@@ -2284,7 +2602,7 @@ var Store = class {
|
|
|
2284
2602
|
installationDeviceId() {
|
|
2285
2603
|
const row = this.db.prepare("SELECT device_id FROM installation WHERE id=1").get();
|
|
2286
2604
|
if (row) return row.device_id;
|
|
2287
|
-
const id =
|
|
2605
|
+
const id = randomUUID8();
|
|
2288
2606
|
this.db.prepare("INSERT INTO installation(id,device_id) VALUES(1,?)").run(id);
|
|
2289
2607
|
return id;
|
|
2290
2608
|
}
|
|
@@ -2332,8 +2650,8 @@ var Store = class {
|
|
|
2332
2650
|
if (token) this.db.prepare("DELETE FROM auth_sessions WHERE token_hash=?").run(tokenHash(token));
|
|
2333
2651
|
}
|
|
2334
2652
|
botFromRow(row) {
|
|
2335
|
-
const workspacePath =
|
|
2336
|
-
mkdirSync(
|
|
2653
|
+
const workspacePath = join2(dirname2(this.path), "bots", row.id, "workspace");
|
|
2654
|
+
mkdirSync(join2(workspacePath, "temp"), { recursive: true, mode: 448 });
|
|
2337
2655
|
return { ...row, workspacePath };
|
|
2338
2656
|
}
|
|
2339
2657
|
listBots() {
|
|
@@ -2360,6 +2678,7 @@ var Store = class {
|
|
|
2360
2678
|
this.db.prepare("DELETE FROM chats WHERE type='dm' AND id IN (SELECT chat_id FROM chat_members WHERE member_id=?)").run(id);
|
|
2361
2679
|
for (const table of ["chat_members", "chat_states", "inbox"]) this.db.prepare(`DELETE FROM ${table} WHERE member_id=?`).run(id);
|
|
2362
2680
|
this.db.prepare("DELETE FROM bot_sessions WHERE bot_id=?").run(id);
|
|
2681
|
+
this.db.prepare("DELETE FROM environment_grants WHERE bot_id=?").run(id);
|
|
2363
2682
|
this.db.exec("COMMIT");
|
|
2364
2683
|
} catch (error) {
|
|
2365
2684
|
this.db.exec("ROLLBACK");
|
|
@@ -2376,11 +2695,11 @@ var Store = class {
|
|
|
2376
2695
|
/** The caller names the Bot (A45: the interface picks a free default in its own language). */
|
|
2377
2696
|
createBot(name2) {
|
|
2378
2697
|
const clean = this.botName(name2);
|
|
2379
|
-
const id =
|
|
2698
|
+
const id = randomUUID8(), createdAt = Date.now();
|
|
2380
2699
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2381
2700
|
try {
|
|
2382
2701
|
this.db.prepare("INSERT INTO members VALUES(?,'bot')").run(id);
|
|
2383
|
-
this.db.prepare("INSERT INTO bots(id,name,description,system_prompt,model_id,created_at,owner_id,thinking_level,color) VALUES(?,?,?,?,?,?,?,'auto',?)").run(id, clean, "", "", "", createdAt, this.userMemberId(), BOT_COLORS[
|
|
2702
|
+
this.db.prepare("INSERT INTO bots(id,name,description,system_prompt,model_id,created_at,owner_id,thinking_level,color) VALUES(?,?,?,?,?,?,?,'auto',?)").run(id, clean, "", "", "", createdAt, this.userMemberId(), BOT_COLORS[randomInt3(BOT_COLORS.length)]);
|
|
2384
2703
|
this.db.exec("COMMIT");
|
|
2385
2704
|
} catch (error) {
|
|
2386
2705
|
this.db.exec("ROLLBACK");
|
|
@@ -2448,7 +2767,7 @@ var Store = class {
|
|
|
2448
2767
|
}
|
|
2449
2768
|
const key = input.apiKey === void 0 ? null : requireText(input.apiKey, "API Key", 8192);
|
|
2450
2769
|
if (key && authType !== "api_key") throw new ValidationError("Invalid credential type");
|
|
2451
|
-
const id =
|
|
2770
|
+
const id = randomUUID8(), now = Date.now();
|
|
2452
2771
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2453
2772
|
try {
|
|
2454
2773
|
this.db.prepare("INSERT INTO provider_connections(id,provider_name,display_name,kind,auth_type,base_url,api,created_at,updated_at,owner_id) VALUES(?,?,?,?,?,?,?,?,?,?)").run(id, providerName, displayName, kind, authType, baseUrl, api, now, now, this.userMemberId());
|
|
@@ -2582,7 +2901,6 @@ var AccountService = class {
|
|
|
2582
2901
|
};
|
|
2583
2902
|
|
|
2584
2903
|
// src/server/infra/pi/model.ts
|
|
2585
|
-
import { builtinProviders as builtinProviders2 } from "@earendil-works/pi-ai/providers/all";
|
|
2586
2904
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2587
2905
|
var FORMATS = /* @__PURE__ */ new Set(["openai", "openrouter", "deepseek", "together", "baseten", "zai", "qwen", "chat-template", "qwen-chat-template", "string-thinking", "ant-ling"]);
|
|
2588
2906
|
function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
@@ -2617,12 +2935,12 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2617
2935
|
model.headers = { ...model.headers, ...headers2 };
|
|
2618
2936
|
return model;
|
|
2619
2937
|
}
|
|
2620
|
-
async function modelCatalog(credentials, providerName) {
|
|
2938
|
+
async function modelCatalog(credentials, providerName, catalog) {
|
|
2621
2939
|
const runtime = await ModelRuntime.create({ credentials, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
2622
|
-
return runtime.getModels(providerName).map(publicModel);
|
|
2940
|
+
return catalog.overlay(providerName, runtime.getModels(providerName)).map(publicModel);
|
|
2623
2941
|
}
|
|
2624
|
-
function
|
|
2625
|
-
return (
|
|
2942
|
+
function providerModels(providers, providerName) {
|
|
2943
|
+
return (providers.find((provider) => provider.id === providerName)?.getModels() ?? []).map(publicModel);
|
|
2626
2944
|
}
|
|
2627
2945
|
function catalogCompat(model) {
|
|
2628
2946
|
const { thinkingFormat: _format, ...compat } = model.compat ?? {};
|
|
@@ -2664,12 +2982,12 @@ var CredentialCoordinator = class {
|
|
|
2664
2982
|
if (this.tails.get(connectionId) === tail) this.tails.delete(connectionId);
|
|
2665
2983
|
});
|
|
2666
2984
|
if (!signal) return operation;
|
|
2667
|
-
return new Promise((
|
|
2985
|
+
return new Promise((resolve5, reject) => {
|
|
2668
2986
|
const aborted = () => reject(signal.reason);
|
|
2669
2987
|
signal.addEventListener("abort", aborted, { once: true });
|
|
2670
2988
|
operation.then((value2) => {
|
|
2671
2989
|
signal.removeEventListener("abort", aborted);
|
|
2672
|
-
|
|
2990
|
+
resolve5(value2);
|
|
2673
2991
|
}, (error) => {
|
|
2674
2992
|
signal.removeEventListener("abort", aborted);
|
|
2675
2993
|
reject(error);
|
|
@@ -2752,7 +3070,6 @@ function resolveThinking(model, choice) {
|
|
|
2752
3070
|
}
|
|
2753
3071
|
|
|
2754
3072
|
// src/server/infra/pi/autoconfig.ts
|
|
2755
|
-
import { builtinProviders as builtinProviders3 } from "@earendil-works/pi-ai/providers/all";
|
|
2756
3073
|
var VENDORS = [
|
|
2757
3074
|
[/^glm/i, ["zai"]],
|
|
2758
3075
|
[/^deepseek/i, ["deepseek"]],
|
|
@@ -2773,23 +3090,20 @@ function officialScore(provider, id) {
|
|
|
2773
3090
|
return (id.includes("/") ? 0 : 1) + (words.some((word) => provider.includes(word)) ? 1 : 0);
|
|
2774
3091
|
}
|
|
2775
3092
|
var strip = (id) => id.toLowerCase().replace(/^.*\//, "").replace(/-(\d{6,8})$/, "");
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
rows ??= builtinProviders3().flatMap((provider) => provider.getModels().map((model) => ({ provider: provider.id, model, official: officialScore(provider.id, model.id) })));
|
|
2779
|
-
return rows;
|
|
3093
|
+
function rows(providers) {
|
|
3094
|
+
return providers.flatMap((provider) => provider.getModels().map((model) => ({ provider: provider.id, model, official: officialScore(provider.id, model.id) })));
|
|
2780
3095
|
}
|
|
2781
3096
|
var source = (row) => ({ provider: row.provider, modelId: row.model.id, name: row.model.name, api: row.model.api, official: row.official === 2 });
|
|
2782
3097
|
var completeness = (row) => Object.values(row.model.thinkingLevelMap ?? {}).filter((value2) => value2 !== void 0).length + Object.keys(row.model.compat ?? {}).length + (row.model.cost.input > 0 ? 1 : 0);
|
|
2783
|
-
function candidates(query) {
|
|
3098
|
+
function candidates(all, query) {
|
|
2784
3099
|
const q = query.trim().toLowerCase();
|
|
2785
3100
|
if (!q) return [];
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
return same2.length ? same2 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
|
|
3101
|
+
const same3 = all.filter((row) => row.model.id.toLowerCase() === q || strip(row.model.id) === strip(q));
|
|
3102
|
+
return same3.length ? same3 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
|
|
2789
3103
|
}
|
|
2790
|
-
function similar(query) {
|
|
3104
|
+
function similar(all, query) {
|
|
2791
3105
|
const q = strip(query);
|
|
2792
|
-
return
|
|
3106
|
+
return all.map((row) => ({ row, ...similarity(strip(row.model.id), q) })).filter((item) => item.prefix >= 3).sort((a, b) => b.score - a.score || b.row.official - a.row.official).slice(0, 5).map((item) => item.row);
|
|
2793
3107
|
}
|
|
2794
3108
|
function similarity(a, b) {
|
|
2795
3109
|
let prefix = 0;
|
|
@@ -2826,14 +3140,14 @@ function matched(row, api, alternatives, reseller) {
|
|
|
2826
3140
|
alternatives: alternatives.filter((item) => item !== row).slice(0, 8).map(source)
|
|
2827
3141
|
};
|
|
2828
3142
|
}
|
|
2829
|
-
function autoconfigure(modelId, api, choice) {
|
|
2830
|
-
const hits = candidates(modelId);
|
|
3143
|
+
function autoconfigure(providers, modelId, api, choice) {
|
|
3144
|
+
const all = rows(providers), hits = candidates(all, modelId);
|
|
2831
3145
|
if (choice) {
|
|
2832
|
-
const row =
|
|
3146
|
+
const row = all.find((item) => item.provider === choice.provider && item.model.id === choice.modelId);
|
|
2833
3147
|
if (row) return matched(row, api, hits, row.official < 2);
|
|
2834
3148
|
}
|
|
2835
3149
|
if (!hits.length) {
|
|
2836
|
-
const options = similar(modelId);
|
|
3150
|
+
const options = similar(all, modelId);
|
|
2837
3151
|
return options.length ? { status: "choose", reason: "similar", options: options.map(source) } : { status: "none" };
|
|
2838
3152
|
}
|
|
2839
3153
|
const official = hits.filter((row) => row.official === 2);
|
|
@@ -2917,11 +3231,12 @@ async function discoverModels(input, signal) {
|
|
|
2917
3231
|
|
|
2918
3232
|
// src/server/service/models/service.ts
|
|
2919
3233
|
var ModelService = class {
|
|
2920
|
-
constructor(store, credentials, logins, changed) {
|
|
3234
|
+
constructor(store, credentials, logins, changed, models) {
|
|
2921
3235
|
this.store = store;
|
|
2922
3236
|
this.credentials = credentials;
|
|
2923
3237
|
this.logins = logins;
|
|
2924
3238
|
this.changed = changed;
|
|
3239
|
+
this.models = models;
|
|
2925
3240
|
}
|
|
2926
3241
|
required(value2) {
|
|
2927
3242
|
if (!value2) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
@@ -2956,15 +3271,19 @@ var ModelService = class {
|
|
|
2956
3271
|
return this.required(this.store.deleteConnectionModel(id, modelId));
|
|
2957
3272
|
}
|
|
2958
3273
|
providers() {
|
|
2959
|
-
return providerCatalog();
|
|
3274
|
+
return providerCatalog(this.models.providers());
|
|
3275
|
+
}
|
|
3276
|
+
/** Brings pi.dev's newer catalog entries in the background (A49). */
|
|
3277
|
+
refreshCatalog() {
|
|
3278
|
+
this.models.refresh();
|
|
2960
3279
|
}
|
|
2961
3280
|
builtinModels(provider) {
|
|
2962
|
-
return
|
|
3281
|
+
return providerModels(this.models.providers(), provider);
|
|
2963
3282
|
}
|
|
2964
3283
|
async catalog(id) {
|
|
2965
3284
|
const connection = this.required(this.store.getModelConnection(id));
|
|
2966
3285
|
const credentials = new ConnectionCredentials(this.store.credentials, this.credentials, this.store.userMemberId(), id, connection.providerName);
|
|
2967
|
-
return modelCatalog(credentials, connection.providerName);
|
|
3286
|
+
return modelCatalog(credentials, connection.providerName, this.models);
|
|
2968
3287
|
}
|
|
2969
3288
|
discover(input, signal) {
|
|
2970
3289
|
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
@@ -2977,7 +3296,7 @@ var ModelService = class {
|
|
|
2977
3296
|
if (!key) throw new RuntimeFault("discovery_key", "Enter the API key.", 400);
|
|
2978
3297
|
return discoverModels({ api: String(input.api ?? "openai-completions"), baseUrl: input.baseUrl, key }, signal);
|
|
2979
3298
|
}
|
|
2980
|
-
/** Suggests model fields from
|
|
3299
|
+
/** Suggests model fields from Pi's catalog (with pi.dev's newer entries, A49); the caller decides whether to save them. */
|
|
2981
3300
|
autoconfigure(input) {
|
|
2982
3301
|
const text3 = (value2) => typeof value2 === "string" && value2.trim() && value2.length <= 200 ? value2.trim() : null;
|
|
2983
3302
|
const modelId = text3(input.modelId), api = text3(input.api);
|
|
@@ -2989,7 +3308,7 @@ var ModelService = class {
|
|
|
2989
3308
|
if (!provider || !chosen) throw new RuntimeFault("autoconfig_source", "The setup source isn't valid.", 400);
|
|
2990
3309
|
choice = { provider, modelId: chosen };
|
|
2991
3310
|
}
|
|
2992
|
-
return autoconfigure(modelId, api, choice);
|
|
3311
|
+
return autoconfigure(this.models.providers(), modelId, api, choice);
|
|
2993
3312
|
}
|
|
2994
3313
|
options() {
|
|
2995
3314
|
return this.list().filter((connection) => connection.models.length).map((connection) => ({
|
|
@@ -3016,7 +3335,7 @@ var ModelService = class {
|
|
|
3016
3335
|
};
|
|
3017
3336
|
|
|
3018
3337
|
// src/server/infra/pi/auth.ts
|
|
3019
|
-
import { randomUUID as
|
|
3338
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
3020
3339
|
import { ModelRuntime as ModelRuntime2 } from "@earendil-works/pi-coding-agent";
|
|
3021
3340
|
var CredentialLogins = class {
|
|
3022
3341
|
constructor(store, credentials, changed) {
|
|
@@ -3064,7 +3383,7 @@ var CredentialLogins = class {
|
|
|
3064
3383
|
prompt(flow, prompt) {
|
|
3065
3384
|
const signal = prompt.signal ? AbortSignal.any([flow.controller.signal, prompt.signal]) : flow.controller.signal;
|
|
3066
3385
|
signal.throwIfAborted();
|
|
3067
|
-
const id =
|
|
3386
|
+
const id = randomUUID9();
|
|
3068
3387
|
flow.state.prompt = {
|
|
3069
3388
|
id,
|
|
3070
3389
|
type: prompt.type,
|
|
@@ -3072,7 +3391,7 @@ var CredentialLogins = class {
|
|
|
3072
3391
|
..."placeholder" in prompt ? { placeholder: prompt.placeholder } : {},
|
|
3073
3392
|
...prompt.type === "select" ? { options: prompt.options } : {}
|
|
3074
3393
|
};
|
|
3075
|
-
return new Promise((
|
|
3394
|
+
return new Promise((resolve5, reject) => {
|
|
3076
3395
|
const clear = () => {
|
|
3077
3396
|
signal.removeEventListener("abort", aborted);
|
|
3078
3397
|
flow.answer = void 0;
|
|
@@ -3084,7 +3403,7 @@ var CredentialLogins = class {
|
|
|
3084
3403
|
};
|
|
3085
3404
|
flow.answer = (value2) => {
|
|
3086
3405
|
clear();
|
|
3087
|
-
|
|
3406
|
+
resolve5(value2);
|
|
3088
3407
|
};
|
|
3089
3408
|
signal.addEventListener("abort", aborted, { once: true });
|
|
3090
3409
|
if (signal.aborted) aborted();
|
|
@@ -3149,9 +3468,9 @@ var User = class extends Member {
|
|
|
3149
3468
|
};
|
|
3150
3469
|
|
|
3151
3470
|
// src/server/application.ts
|
|
3152
|
-
import { createHash as
|
|
3153
|
-
import { mkdir as
|
|
3154
|
-
import { join as
|
|
3471
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3472
|
+
import { mkdir as mkdir5, realpath } from "node:fs/promises";
|
|
3473
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
3155
3474
|
import lockfile from "proper-lockfile";
|
|
3156
3475
|
|
|
3157
3476
|
// src/server/service/chat/mentions.ts
|
|
@@ -3159,12 +3478,12 @@ var WORD = /[\p{Script=Latin}\p{Nd}_]/u;
|
|
|
3159
3478
|
function findMentions(source2, candidates2) {
|
|
3160
3479
|
const text3 = source2.replace(/```[\s\S]*?```|`[^`\n]*`/g, (code) => " ".repeat(code.length));
|
|
3161
3480
|
const lower = text3.toLowerCase(), sorted = [...candidates2].sort((a, b) => b.name.length - a.name.length), found = [];
|
|
3162
|
-
for (let
|
|
3163
|
-
if (
|
|
3481
|
+
for (let at2 = lower.indexOf("@"); at2 >= 0; at2 = lower.indexOf("@", at2 + 1)) {
|
|
3482
|
+
if (at2 > 0 && WORD.test(text3[at2 - 1])) continue;
|
|
3164
3483
|
const match = sorted.find((candidate) => {
|
|
3165
3484
|
const name2 = candidate.name.toLowerCase();
|
|
3166
|
-
if (!name2 || !lower.startsWith(name2,
|
|
3167
|
-
const next = text3[
|
|
3485
|
+
if (!name2 || !lower.startsWith(name2, at2 + 1)) return false;
|
|
3486
|
+
const next = text3[at2 + 1 + name2.length];
|
|
3168
3487
|
return !(next && WORD.test(next) && WORD.test(name2.at(-1)));
|
|
3169
3488
|
});
|
|
3170
3489
|
if (match && !found.includes(match)) found.push(match);
|
|
@@ -3200,7 +3519,13 @@ var POLICY = {
|
|
|
3200
3519
|
// Marks on your messages are queued: they wake you but never interrupt (A39).
|
|
3201
3520
|
reaction: { author: "queue" },
|
|
3202
3521
|
// A new version of a notice or summary is queued like a mark.
|
|
3203
|
-
note: { member: "queue", follower: "queue" }
|
|
3522
|
+
note: { member: "queue", follower: "queue" },
|
|
3523
|
+
// The owner just gave the agent Bot its first model: it should welcome them now (A48).
|
|
3524
|
+
onboard: { setup: "interrupt" },
|
|
3525
|
+
// What the agent Bot changed about a Bot rides along with that Bot's next batch, without waking it. The owner moving
|
|
3526
|
+
// a Bot to another work environment stops its running turn like an @, so it doesn't go on in the old place (A50); a
|
|
3527
|
+
// Bot not running, and a change of access alone, hear of it with their next batch instead (fish m1012-m1015).
|
|
3528
|
+
change: { agent: "digest", owner: "interrupt" }
|
|
3204
3529
|
};
|
|
3205
3530
|
function urgencyOf(kind, reason) {
|
|
3206
3531
|
return POLICY[kind][reason];
|
|
@@ -3364,8 +3689,8 @@ var ChatMessages = class {
|
|
|
3364
3689
|
const message = this.repository.message(this.actor, id);
|
|
3365
3690
|
return message?.chatId === this.id ? message : null;
|
|
3366
3691
|
}
|
|
3367
|
-
async list({ after = 0, limit }) {
|
|
3368
|
-
return this.repository.messages(this.actor, this.id,
|
|
3692
|
+
async list({ after: after2 = 0, limit }) {
|
|
3693
|
+
return this.repository.messages(this.actor, this.id, after2, limit);
|
|
3369
3694
|
}
|
|
3370
3695
|
async search(query, { limit }) {
|
|
3371
3696
|
return this.repository.search(this.actor, this.id, query, limit);
|
|
@@ -3424,7 +3749,7 @@ var ChatQuestions = class {
|
|
|
3424
3749
|
if (question.state === "answered") return Promise.resolve(resultOf(question, question.answer));
|
|
3425
3750
|
if (question.state === "dismissed") return Promise.resolve({ status: "dismissed" });
|
|
3426
3751
|
if (question.state === "ignored") return Promise.reject(new ChatError(409, "The question has expired", "question_expired"));
|
|
3427
|
-
return new Promise((
|
|
3752
|
+
return new Promise((resolve5, reject) => {
|
|
3428
3753
|
const stop = () => {
|
|
3429
3754
|
this.waiting.delete(message.id);
|
|
3430
3755
|
if (this.repository.settleQuestion(message.id, "ignored", null)) this.changed(message.chatId);
|
|
@@ -3437,7 +3762,7 @@ var ChatQuestions = class {
|
|
|
3437
3762
|
signal?.addEventListener("abort", stop, { once: true });
|
|
3438
3763
|
this.waiting.set(message.id, (result) => {
|
|
3439
3764
|
signal?.removeEventListener("abort", stop);
|
|
3440
|
-
|
|
3765
|
+
resolve5(result);
|
|
3441
3766
|
});
|
|
3442
3767
|
});
|
|
3443
3768
|
}
|
|
@@ -3795,13 +4120,13 @@ var ChatService = class {
|
|
|
3795
4120
|
return this.repository.getChat(actorId, chatId);
|
|
3796
4121
|
}
|
|
3797
4122
|
/** The message with up to `context` messages before and after it (read position unchanged). */
|
|
3798
|
-
around(actorId, messageId,
|
|
4123
|
+
around(actorId, messageId, context2) {
|
|
3799
4124
|
const message = this.repository.message(actorId, messageId);
|
|
3800
4125
|
if (!message) throw new ChatError(404, "Message not found");
|
|
3801
4126
|
return [
|
|
3802
|
-
...this.repository.messagesBefore(actorId, message.chatId, message.sequence,
|
|
4127
|
+
...this.repository.messagesBefore(actorId, message.chatId, message.sequence, context2),
|
|
3803
4128
|
message,
|
|
3804
|
-
...this.repository.messages(actorId, message.chatId, message.sequence,
|
|
4129
|
+
...this.repository.messages(actorId, message.chatId, message.sequence, context2)
|
|
3805
4130
|
];
|
|
3806
4131
|
}
|
|
3807
4132
|
/** Exactly the unread messages of a chat, oldest first; the caller moves the read position. */
|
|
@@ -3818,10 +4143,10 @@ var ChatService = class {
|
|
|
3818
4143
|
find(actorId, query, { within, limit = 50 } = {}) {
|
|
3819
4144
|
const messages = this.searchAll(actorId, query, limit, within);
|
|
3820
4145
|
const chats = within ? [] : this.repository.searchChatIds(actorId, query.trim(), 30).map((id) => this.repository.getChat(actorId, id)).filter((chat) => chat !== null);
|
|
3821
|
-
const
|
|
4146
|
+
const where2 = /* @__PURE__ */ new Map();
|
|
3822
4147
|
const chatOf = (id) => {
|
|
3823
|
-
if (!
|
|
3824
|
-
return
|
|
4148
|
+
if (!where2.has(id)) where2.set(id, this.repository.getChat(actorId, id));
|
|
4149
|
+
return where2.get(id);
|
|
3825
4150
|
};
|
|
3826
4151
|
return { chats, messages: messages.flatMap((message) => {
|
|
3827
4152
|
const chat = chatOf(message.chatId);
|
|
@@ -3835,10 +4160,10 @@ var ChatService = class {
|
|
|
3835
4160
|
const messages = this.repository.messagesBefore(actorId, chatId, before ?? Number.MAX_SAFE_INTEGER, limit + 1);
|
|
3836
4161
|
return { messages: messages.slice(-limit), hasOlder: messages.length > limit };
|
|
3837
4162
|
}
|
|
3838
|
-
read(actorId, chatId,
|
|
3839
|
-
if (!Number.isSafeInteger(
|
|
4163
|
+
read(actorId, chatId, after2 = 0, limit = 1e3) {
|
|
4164
|
+
if (!Number.isSafeInteger(after2) || after2 < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
|
|
3840
4165
|
throw new ChatError(400, "Invalid message cursor or limit");
|
|
3841
|
-
return this.repository.messages(actorId, chatId,
|
|
4166
|
+
return this.repository.messages(actorId, chatId, after2, limit);
|
|
3842
4167
|
}
|
|
3843
4168
|
instance(actorId, chatId) {
|
|
3844
4169
|
this.repository.assertAccess(actorId, chatId);
|
|
@@ -3913,70 +4238,11 @@ var ChatService = class {
|
|
|
3913
4238
|
}
|
|
3914
4239
|
};
|
|
3915
4240
|
|
|
3916
|
-
// src/server/infra/files/attachments.ts
|
|
3917
|
-
import { constants } from "node:fs";
|
|
3918
|
-
import { mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
3919
|
-
import { join as join2, resolve as resolve2 } from "node:path";
|
|
3920
|
-
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
3921
|
-
var MAX_FILE = 10 * 1024 * 1024;
|
|
3922
|
-
var AttachmentFiles = class {
|
|
3923
|
-
constructor(dataDir) {
|
|
3924
|
-
this.dataDir = dataDir;
|
|
3925
|
-
}
|
|
3926
|
-
async materialize(file, signal) {
|
|
3927
|
-
const dir = join2(this.dataDir, "attachments", file.id);
|
|
3928
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
3929
|
-
if (await realpath(dir) !== resolve2(dir)) throw new ChatError(400, "Invalid attachment directory");
|
|
3930
|
-
const path = join2(dir, file.name), temporary = join2(dir, `.${randomUUID9()}.tmp`);
|
|
3931
|
-
try {
|
|
3932
|
-
const handle = await open(temporary, "wx", 384);
|
|
3933
|
-
try {
|
|
3934
|
-
await handle.writeFile(file.data, { signal });
|
|
3935
|
-
} finally {
|
|
3936
|
-
await handle.close();
|
|
3937
|
-
}
|
|
3938
|
-
signal?.throwIfAborted();
|
|
3939
|
-
await rename(temporary, path);
|
|
3940
|
-
} finally {
|
|
3941
|
-
await rm(temporary, { force: true });
|
|
3942
|
-
}
|
|
3943
|
-
return { path, name: file.name };
|
|
3944
|
-
}
|
|
3945
|
-
async readOutgoing(workspace, items, signal) {
|
|
3946
|
-
if (items.length > 5) throw new ChatError(400, "Too many attachments");
|
|
3947
|
-
const result = [];
|
|
3948
|
-
for (const item of items) {
|
|
3949
|
-
signal?.throwIfAborted();
|
|
3950
|
-
const path = await realpath(resolve2(workspace, item.path));
|
|
3951
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
3952
|
-
try {
|
|
3953
|
-
const stat = await handle.stat();
|
|
3954
|
-
if (!stat.isFile() || !stat.size || stat.size > MAX_FILE) throw new ChatError(400, "Invalid attachment size or type");
|
|
3955
|
-
const data = Buffer.alloc(Math.min(MAX_FILE + 1, stat.size + 1));
|
|
3956
|
-
let bytes = 0;
|
|
3957
|
-
while (bytes < data.length) {
|
|
3958
|
-
signal?.throwIfAborted();
|
|
3959
|
-
const read = await handle.read(data, bytes, data.length - bytes, bytes);
|
|
3960
|
-
if (!read.bytesRead) break;
|
|
3961
|
-
bytes += read.bytesRead;
|
|
3962
|
-
}
|
|
3963
|
-
if (bytes !== stat.size) throw new ChatError(409, "Attachment changed while reading");
|
|
3964
|
-
result.push({ name: item.name, mimeType: "application/octet-stream", data: data.subarray(0, bytes) });
|
|
3965
|
-
} finally {
|
|
3966
|
-
await handle.close();
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3969
|
-
return result;
|
|
3970
|
-
}
|
|
3971
|
-
};
|
|
3972
|
-
|
|
3973
4241
|
// src/server/service/chat/files.ts
|
|
3974
4242
|
var ChatFiles = class {
|
|
3975
|
-
constructor(
|
|
4243
|
+
constructor(chats) {
|
|
3976
4244
|
this.chats = chats;
|
|
3977
|
-
this.files = new AttachmentFiles(dataDir);
|
|
3978
4245
|
}
|
|
3979
|
-
files;
|
|
3980
4246
|
async envelope(botId, messageIds, signal) {
|
|
3981
4247
|
const output = [];
|
|
3982
4248
|
for (const id of messageIds) {
|
|
@@ -4000,7 +4266,7 @@ var ChatFiles = class {
|
|
|
4000
4266
|
sender: message.sender,
|
|
4001
4267
|
content: expandQuotes(message.content, message.quotes, (memberId) => this.chats.member(memberId)?.name ?? memberId),
|
|
4002
4268
|
createdAt: message.createdAt,
|
|
4003
|
-
attachments:
|
|
4269
|
+
attachments: listing(message),
|
|
4004
4270
|
...this.reply(message)
|
|
4005
4271
|
});
|
|
4006
4272
|
}
|
|
@@ -4011,20 +4277,21 @@ var ChatFiles = class {
|
|
|
4011
4277
|
const sender = message.replyTo ? this.chats.member(message.replyTo.senderId) : null;
|
|
4012
4278
|
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: replyExcerpt(message.replyTo) } } : {};
|
|
4013
4279
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
if (!attachment) throw new ChatError(404, "Attachment not found");
|
|
4020
|
-
paths.push(await this.files.materialize({ ...attachment, id: item.id, name: item.name }, signal));
|
|
4021
|
-
}
|
|
4022
|
-
return paths;
|
|
4023
|
-
}
|
|
4024
|
-
readOutgoing(workspace, items, signal) {
|
|
4025
|
-
return this.files.readOutgoing(workspace, items, signal);
|
|
4280
|
+
/** A file of a message the actor can read, with its contents. */
|
|
4281
|
+
open(actorId, attachmentId) {
|
|
4282
|
+
const found = this.chats.download(actorId, attachmentId);
|
|
4283
|
+
if (!found) throw new ChatError(404, "File not found, or in a message you can't read");
|
|
4284
|
+
return found;
|
|
4026
4285
|
}
|
|
4027
4286
|
};
|
|
4287
|
+
function listing(message) {
|
|
4288
|
+
return message.attachments.map((file) => ({ id: file.id, name: file.name, type: file.mimeType || "application/octet-stream", size: byteSize(file.size) }));
|
|
4289
|
+
}
|
|
4290
|
+
function byteSize(bytes) {
|
|
4291
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4292
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
4293
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
4294
|
+
}
|
|
4028
4295
|
|
|
4029
4296
|
// src/server/service/events.ts
|
|
4030
4297
|
var Changes = class {
|
|
@@ -4049,8 +4316,8 @@ var Changes = class {
|
|
|
4049
4316
|
// src/server/service/bot/activity.ts
|
|
4050
4317
|
function activityPage(messages, history, limit) {
|
|
4051
4318
|
const candidates2 = [
|
|
4052
|
-
...messages.map((
|
|
4053
|
-
...history.map((
|
|
4319
|
+
...messages.map((record3) => ({ kind: "chat", record: record3 })),
|
|
4320
|
+
...history.map((record3) => ({ kind: "history", record: record3 }))
|
|
4054
4321
|
];
|
|
4055
4322
|
candidates2.sort((a, b) => {
|
|
4056
4323
|
if (a.record.timestamp !== b.record.timestamp) return b.record.timestamp - a.record.timestamp;
|
|
@@ -4063,8 +4330,8 @@ function activityPage(messages, history, limit) {
|
|
|
4063
4330
|
}
|
|
4064
4331
|
|
|
4065
4332
|
// src/server/service/bot/service.ts
|
|
4066
|
-
import { dirname as
|
|
4067
|
-
import { rm as
|
|
4333
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
4334
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
4068
4335
|
|
|
4069
4336
|
// src/server/infra/pi/history.ts
|
|
4070
4337
|
var value = (input) => JSON.parse(JSON.stringify(input));
|
|
@@ -4181,7 +4448,7 @@ var PiHistoryAdapter = class {
|
|
|
4181
4448
|
// src/server/infra/pi/session.ts
|
|
4182
4449
|
import { existsSync, readFileSync } from "node:fs";
|
|
4183
4450
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
4184
|
-
import { dirname as
|
|
4451
|
+
import { dirname as dirname3, join as join3, resolve as resolve2, sep } from "node:path";
|
|
4185
4452
|
import { createAgentSession, createCodemodeExtension, DefaultResourceLoader, ModelRuntime as ModelRuntime3, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4186
4453
|
|
|
4187
4454
|
// src/server/infra/pi/http.ts
|
|
@@ -4207,6 +4474,9 @@ function configureHttp(timeoutMs) {
|
|
|
4207
4474
|
// src/server/infra/pi/session.ts
|
|
4208
4475
|
import { Type } from "typebox";
|
|
4209
4476
|
|
|
4477
|
+
// src/server/shared/tools.ts
|
|
4478
|
+
var isToolMedia = (value2) => !!value2 && typeof value2 === "object" && value2.kind === "media" && Array.isArray(value2.images);
|
|
4479
|
+
|
|
4210
4480
|
// src/server/infra/pi/live.ts
|
|
4211
4481
|
var LiveTranslator = class {
|
|
4212
4482
|
message = 0;
|
|
@@ -4269,6 +4539,130 @@ function resultText(result) {
|
|
|
4269
4539
|
}
|
|
4270
4540
|
}
|
|
4271
4541
|
|
|
4542
|
+
// src/server/infra/pi/environment-tools.ts
|
|
4543
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4544
|
+
import { readFile as readLocal, rm } from "node:fs/promises";
|
|
4545
|
+
import { basename } from "node:path";
|
|
4546
|
+
import { createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createWriteToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4547
|
+
|
|
4548
|
+
// src/server/infra/pi/image-kind.ts
|
|
4549
|
+
var IMAGE_SNIFF_BYTES = 4100;
|
|
4550
|
+
function imageKind(bytes) {
|
|
4551
|
+
if (startsWith(bytes, [255, 216, 255])) return bytes[3] === 247 ? null : "image/jpeg";
|
|
4552
|
+
if (startsWith(bytes, PNG)) return isPng(bytes) && !isAnimatedPng(bytes) ? "image/png" : null;
|
|
4553
|
+
if (ascii(bytes, 0, "GIF87a") || ascii(bytes, 0, "GIF89a")) return "image/gif";
|
|
4554
|
+
if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WEBP")) return "image/webp";
|
|
4555
|
+
if (ascii(bytes, 0, "BM") && isBmp(bytes)) return "image/bmp";
|
|
4556
|
+
return null;
|
|
4557
|
+
}
|
|
4558
|
+
var PNG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
4559
|
+
var isPng = (bytes) => bytes.length >= 16 && u32be(bytes, PNG.length) === 13 && ascii(bytes, 12, "IHDR");
|
|
4560
|
+
function isAnimatedPng(bytes) {
|
|
4561
|
+
let offset = PNG.length;
|
|
4562
|
+
while (offset + 8 <= bytes.length) {
|
|
4563
|
+
const length = u32be(bytes, offset), type = offset + 4;
|
|
4564
|
+
if (ascii(bytes, type, "acTL")) return true;
|
|
4565
|
+
if (ascii(bytes, type, "IDAT")) return false;
|
|
4566
|
+
const next = offset + 8 + length + 4;
|
|
4567
|
+
if (next <= offset || next > bytes.length) return false;
|
|
4568
|
+
offset = next;
|
|
4569
|
+
}
|
|
4570
|
+
return false;
|
|
4571
|
+
}
|
|
4572
|
+
function isBmp(bytes) {
|
|
4573
|
+
if (bytes.length < 26) return false;
|
|
4574
|
+
const size = u32le(bytes, 2), pixels = u32le(bytes, 10), header = u32le(bytes, 14);
|
|
4575
|
+
if (size !== 0 && size < 26) return false;
|
|
4576
|
+
if (pixels < 14 + header) return false;
|
|
4577
|
+
if (size !== 0 && pixels >= size) return false;
|
|
4578
|
+
let planes, bits;
|
|
4579
|
+
if (header === 12) {
|
|
4580
|
+
planes = u16le(bytes, 22);
|
|
4581
|
+
bits = u16le(bytes, 24);
|
|
4582
|
+
} else if (header >= 40 && header <= 124) {
|
|
4583
|
+
if (bytes.length < 30) return false;
|
|
4584
|
+
planes = u16le(bytes, 26);
|
|
4585
|
+
bits = u16le(bytes, 28);
|
|
4586
|
+
} else return false;
|
|
4587
|
+
return planes === 1 && [1, 4, 8, 16, 24, 32].includes(bits);
|
|
4588
|
+
}
|
|
4589
|
+
var at = (bytes, offset) => bytes[offset] ?? 0;
|
|
4590
|
+
var u16le = (bytes, offset) => at(bytes, offset) + (at(bytes, offset + 1) << 8);
|
|
4591
|
+
var u32be = (bytes, offset) => at(bytes, offset) * 16777216 + (at(bytes, offset + 1) << 16) + (at(bytes, offset + 2) << 8) + at(bytes, offset + 3);
|
|
4592
|
+
var u32le = (bytes, offset) => at(bytes, offset) + (at(bytes, offset + 1) << 8) + (at(bytes, offset + 2) << 16) + at(bytes, offset + 3) * 16777216;
|
|
4593
|
+
var startsWith = (bytes, prefix) => bytes.length >= prefix.length && prefix.every((byte, index) => bytes[index] === byte);
|
|
4594
|
+
function ascii(bytes, offset, text3) {
|
|
4595
|
+
if (bytes.length < offset + text3.length) return false;
|
|
4596
|
+
for (let index = 0; index < text3.length; index++) if (bytes[offset + index] !== text3.charCodeAt(index)) return false;
|
|
4597
|
+
return true;
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
// src/server/infra/pi/environment-tools.ts
|
|
4601
|
+
function environmentTools(lookup) {
|
|
4602
|
+
const pinned = new AsyncLocalStorage();
|
|
4603
|
+
const current = () => pinned.getStore() ?? lookup();
|
|
4604
|
+
const access = async (path) => {
|
|
4605
|
+
if (!await current().exists(path)) throw Object.assign(new Error(`ENOENT: no such file or directory, access '${path}'`), { code: "ENOENT" });
|
|
4606
|
+
};
|
|
4607
|
+
const readFile3 = (path) => current().readFile(path), writeFile4 = (path, content2) => current().writeFile(path, content2);
|
|
4608
|
+
const cwd = current().cwd;
|
|
4609
|
+
const definitions2 = [
|
|
4610
|
+
relocated(createBashToolDefinition(cwd, { operations: {
|
|
4611
|
+
exec: (command, dir, { onData, signal, timeout, env }) => current().exec(command, {
|
|
4612
|
+
cwd: dir,
|
|
4613
|
+
onData,
|
|
4614
|
+
...signal ? { signal } : {},
|
|
4615
|
+
...timeout ? { timeout } : {},
|
|
4616
|
+
...env ? { env: defined(env) } : {}
|
|
4617
|
+
})
|
|
4618
|
+
} }), current),
|
|
4619
|
+
// Pictures go to the model as pictures; the kind comes from a file's first bytes, as Pi's local read does.
|
|
4620
|
+
createReadToolDefinition(cwd, { operations: { readFile: readFile3, access, detectImageMimeType: async (path) => imageKind(await current().head(path, IMAGE_SNIFF_BYTES)) } }),
|
|
4621
|
+
createWriteToolDefinition(cwd, { operations: { writeFile: writeFile4, mkdir: (path) => current().mkdir(path) } }),
|
|
4622
|
+
createEditToolDefinition(cwd, { operations: { readFile: readFile3, writeFile: writeFile4, access } })
|
|
4623
|
+
];
|
|
4624
|
+
return definitions2.map((definition) => inside(definition, lookup, pinned));
|
|
4625
|
+
}
|
|
4626
|
+
function inside(definition, lookup, pinned) {
|
|
4627
|
+
const execute = definition.execute.bind(definition);
|
|
4628
|
+
return { ...definition, execute: ((...args) => {
|
|
4629
|
+
const env = lookup(), ctx = args[4];
|
|
4630
|
+
if (ctx && typeof ctx === "object") args[4] = new Proxy(ctx, { get: (target2, key) => key === "cwd" ? env.cwd : Reflect.get(target2, key, target2) });
|
|
4631
|
+
return pinned.run(env, () => execute(...args));
|
|
4632
|
+
}) };
|
|
4633
|
+
}
|
|
4634
|
+
function relocated(definition, current) {
|
|
4635
|
+
const execute = definition.execute.bind(definition);
|
|
4636
|
+
return { ...definition, execute: (async (...args) => {
|
|
4637
|
+
const env = current();
|
|
4638
|
+
if (env.kind === "host") return execute(...args);
|
|
4639
|
+
try {
|
|
4640
|
+
const result = await execute(...args), path = result.details?.fullOutputPath;
|
|
4641
|
+
if (typeof path !== "string") return result;
|
|
4642
|
+
const target2 = await moveOut(path, env), swap = (text3) => text3?.replaceAll(path, target2);
|
|
4643
|
+
return {
|
|
4644
|
+
...result,
|
|
4645
|
+
content: result.content.map((part) => part.type === "text" ? { ...part, text: swap(part.text) } : part),
|
|
4646
|
+
details: { ...result.details, fullOutputPath: target2 },
|
|
4647
|
+
...result.structuredContent?.full_output_path ? { structuredContent: { ...result.structuredContent, full_output_path: target2 } } : {}
|
|
4648
|
+
};
|
|
4649
|
+
} catch (error) {
|
|
4650
|
+
const path = error instanceof Error ? /Full output: (\S+?)\]/.exec(error.message)?.[1] : void 0;
|
|
4651
|
+
if (!path) throw error;
|
|
4652
|
+
throw new Error(error.message.replaceAll(path, await moveOut(path, env)));
|
|
4653
|
+
}
|
|
4654
|
+
}) };
|
|
4655
|
+
}
|
|
4656
|
+
async function moveOut(path, env) {
|
|
4657
|
+
const target2 = `${env.cwd}/temp/${basename(path)}`;
|
|
4658
|
+
await env.writeFile(target2, await readLocal(path));
|
|
4659
|
+
await rm(path, { force: true });
|
|
4660
|
+
return target2;
|
|
4661
|
+
}
|
|
4662
|
+
function defined(vars) {
|
|
4663
|
+
return Object.fromEntries(Object.entries(vars).filter((entry) => entry[1] !== void 0));
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4272
4666
|
// src/server/infra/pi/session.ts
|
|
4273
4667
|
var HTTP_IDLE_TIMEOUT_MS = 9e5;
|
|
4274
4668
|
function compactionReserve(contextWindow) {
|
|
@@ -4309,10 +4703,10 @@ var PiSession = class _PiSession {
|
|
|
4309
4703
|
}
|
|
4310
4704
|
static async create(options) {
|
|
4311
4705
|
const { bot, store, reference, selection } = options;
|
|
4312
|
-
const agentDir = join3(
|
|
4706
|
+
const agentDir = join3(dirname3(bot.workspacePath), "agent", "pi"), sessionsDir = join3(agentDir, "sessions");
|
|
4313
4707
|
await mkdir2(sessionsDir, { recursive: true, mode: 448 });
|
|
4314
|
-
const expected =
|
|
4315
|
-
if (!
|
|
4708
|
+
const expected = resolve2(sessionsDir) + sep;
|
|
4709
|
+
if (!resolve2(reference.file).startsWith(expected) || dirname3(resolve2(reference.file)) !== resolve2(sessionsDir))
|
|
4316
4710
|
throw new RuntimeFault("session_path", "The session file path isn't valid.");
|
|
4317
4711
|
const exists = existsSync(reference.file);
|
|
4318
4712
|
if (!exists && (reference.persisted || store.runtime.runs(bot.id).some((run) => run.state === "unknown")))
|
|
@@ -4363,6 +4757,10 @@ var PiSession = class _PiSession {
|
|
|
4363
4757
|
executionMode: "sequential",
|
|
4364
4758
|
execute: async (toolCallId, args, signal) => {
|
|
4365
4759
|
const output = await tool.execute(args, { toolCallId, signal });
|
|
4760
|
+
if (isToolMedia(output)) return { content: [
|
|
4761
|
+
{ type: "text", text: output.text },
|
|
4762
|
+
...output.images.map((image) => ({ type: "image", data: image.data, mimeType: image.mimeType }))
|
|
4763
|
+
], details: {} };
|
|
4366
4764
|
return { content: [{ type: "text", text: typeof output === "string" ? output : JSON.stringify(output) }], details: {} };
|
|
4367
4765
|
}
|
|
4368
4766
|
});
|
|
@@ -4379,7 +4777,8 @@ var PiSession = class _PiSession {
|
|
|
4379
4777
|
modelRuntime: models,
|
|
4380
4778
|
model,
|
|
4381
4779
|
thinkingLevel: resolveThinking(model, selection.thinkingLevel),
|
|
4382
|
-
tools
|
|
4780
|
+
tools,
|
|
4781
|
+
customTools: environmentTools(options.environment)
|
|
4383
4782
|
});
|
|
4384
4783
|
adapter.session = result.session;
|
|
4385
4784
|
adapter.settings = settingsManager;
|
|
@@ -4396,8 +4795,8 @@ var PiSession = class _PiSession {
|
|
|
4396
4795
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
4397
4796
|
const entry = entries[index];
|
|
4398
4797
|
if (entry.type !== "message" || entry.message !== event.message) continue;
|
|
4399
|
-
const
|
|
4400
|
-
if (
|
|
4798
|
+
const record3 = history.convert(entry);
|
|
4799
|
+
if (record3) await options.onHistory(record3);
|
|
4401
4800
|
break;
|
|
4402
4801
|
}
|
|
4403
4802
|
});
|
|
@@ -4444,7 +4843,7 @@ var PiSession = class _PiSession {
|
|
|
4444
4843
|
}
|
|
4445
4844
|
/** The last assistant message decides the prompt (a later retry success replaces an earlier error).
|
|
4446
4845
|
* pi-ai providers and Agent.handleRunFailure report a failure as "aborted" when the run's signal was
|
|
4447
|
-
* cancelled, but Pi
|
|
4846
|
+
* cancelled, but Pi 1.2.0's lazyStream reports request-setup failures as "error" regardless — e.g. the
|
|
4448
4847
|
* request that follows a tool cancelled mid-run. Apply the same signal rule to those. */
|
|
4449
4848
|
classify(message) {
|
|
4450
4849
|
if (message.stopReason === "aborted" || message.stopReason === "error" && this.session.agent.signal?.aborted) return { outcome: "interrupted" };
|
|
@@ -4480,8 +4879,8 @@ var PiSession = class _PiSession {
|
|
|
4480
4879
|
this.wantsAbort = false;
|
|
4481
4880
|
this.abortFailure = void 0;
|
|
4482
4881
|
this.result = { outcome: "completed" };
|
|
4483
|
-
const settled = new Promise((
|
|
4484
|
-
this.settledResolve =
|
|
4882
|
+
const settled = new Promise((resolve5) => {
|
|
4883
|
+
this.settledResolve = resolve5;
|
|
4485
4884
|
});
|
|
4486
4885
|
this.options.live?.({ type: "start" });
|
|
4487
4886
|
let outcome = "failed";
|
|
@@ -4536,10 +4935,617 @@ var PiSession = class _PiSession {
|
|
|
4536
4935
|
}
|
|
4537
4936
|
};
|
|
4538
4937
|
|
|
4938
|
+
// src/server/infra/env/docker.ts
|
|
4939
|
+
import { execFile, spawn } from "node:child_process";
|
|
4940
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4941
|
+
import { createWriteStream } from "node:fs";
|
|
4942
|
+
import { mkdir as mkdir3, mkdtemp, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
4943
|
+
import { tmpdir } from "node:os";
|
|
4944
|
+
import { join as join4 } from "node:path";
|
|
4945
|
+
import { Connection, RemoteExecutionEnv, packagedDaemon } from "@earendil-works/pi-env";
|
|
4946
|
+
|
|
4947
|
+
// src/server/infra/env/computer-image.ts
|
|
4948
|
+
var COMPUTER_DOCKERFILE = `FROM mcr.microsoft.com/playwright:v1.61.1-noble
|
|
4949
|
+
ARG UID=1000
|
|
4950
|
+
ARG GID=1000
|
|
4951
|
+
RUN apt-get update \\
|
|
4952
|
+
&& apt-get install -y --no-install-recommends build-essential ca-certificates curl fonts-noto-cjk git python3 python3-dev python3-pip python3-venv sudo \\
|
|
4953
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
4954
|
+
&& npm install -g playwright@1.61.1 && npm cache clean --force \\
|
|
4955
|
+
&& taken="$(getent passwd "$UID" | cut -d: -f1)"; if [ -n "$taken" ]; then userdel -r "$taken" 2>/dev/null || userdel "$taken"; fi \\
|
|
4956
|
+
&& (getent group "$GID" > /dev/null || groupadd -g "$GID" bot) \\
|
|
4957
|
+
&& useradd -m -d /workspace -u "$UID" -g "$GID" -s /bin/bash bot \\
|
|
4958
|
+
&& install -d -o "$UID" -g "$GID" /home/bot/.npm-global /home/bot/.npm-global/bin /home/bot/.npm-global/lib \\
|
|
4959
|
+
&& echo "bot ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/bot && chmod 0440 /etc/sudoers.d/bot \\
|
|
4960
|
+
&& mkdir -p /workspace && chown "$UID:$GID" /workspace
|
|
4961
|
+
ENV NPM_CONFIG_PREFIX=/home/bot/.npm-global PATH=/home/bot/.npm-global/bin:$PATH \\
|
|
4962
|
+
NODE_PATH=/home/bot/.npm-global/lib/node_modules:/usr/lib/node_modules
|
|
4963
|
+
USER bot
|
|
4964
|
+
WORKDIR /workspace
|
|
4965
|
+
`;
|
|
4966
|
+
|
|
4967
|
+
// src/server/infra/env/durable.ts
|
|
4968
|
+
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
4969
|
+
function overDurable(id, kind, env, options) {
|
|
4970
|
+
let closed = false;
|
|
4971
|
+
return {
|
|
4972
|
+
id,
|
|
4973
|
+
kind,
|
|
4974
|
+
cwd: env.cwd,
|
|
4975
|
+
async exec(command, exec) {
|
|
4976
|
+
const result = await env.exec(command, {
|
|
4977
|
+
cwd: exec.cwd,
|
|
4978
|
+
...options.passEnv && exec.env ? { env: exec.env, inheritEnv: false } : { inheritEnv: true },
|
|
4979
|
+
...exec.timeout ? { timeout: exec.timeout } : {},
|
|
4980
|
+
onOutput: (text3) => exec.onData(Buffer.from(text3))
|
|
4981
|
+
}, context(exec.signal));
|
|
4982
|
+
if (result.ok) return { exitCode: result.value.exitCode };
|
|
4983
|
+
if (result.error.code === "aborted") throw new Error("aborted");
|
|
4984
|
+
if (result.error.code === "timeout") throw new Error(`timeout:${exec.timeout}`);
|
|
4985
|
+
throw fault(result.error);
|
|
4986
|
+
},
|
|
4987
|
+
async readFile(path) {
|
|
4988
|
+
return Buffer.from(unwrap(await env.readBinaryFile(path, BACKGROUND_CONTEXT)));
|
|
4989
|
+
},
|
|
4990
|
+
async head(path, bytes) {
|
|
4991
|
+
const reader = unwrap(await env.openBinaryReader(path, void 0, BACKGROUND_CONTEXT));
|
|
4992
|
+
try {
|
|
4993
|
+
return Buffer.from(unwrap(await reader.read(0, bytes, BACKGROUND_CONTEXT)));
|
|
4994
|
+
} finally {
|
|
4995
|
+
await reader.close(BACKGROUND_CONTEXT);
|
|
4996
|
+
}
|
|
4997
|
+
},
|
|
4998
|
+
async writeFile(path, content2) {
|
|
4999
|
+
unwrap(await env.writeFile(path, content2, BACKGROUND_CONTEXT));
|
|
5000
|
+
},
|
|
5001
|
+
async mkdir(path) {
|
|
5002
|
+
unwrap(await env.createDir(path, { recursive: true }, BACKGROUND_CONTEXT));
|
|
5003
|
+
},
|
|
5004
|
+
async exists(path) {
|
|
5005
|
+
return unwrap(await env.exists(path, BACKGROUND_CONTEXT));
|
|
5006
|
+
},
|
|
5007
|
+
async fileSize(path) {
|
|
5008
|
+
const real = await env.canonicalPath(path, BACKGROUND_CONTEXT);
|
|
5009
|
+
if (!real.ok) {
|
|
5010
|
+
if (real.error.code === "not_found") return null;
|
|
5011
|
+
throw fault(real.error);
|
|
5012
|
+
}
|
|
5013
|
+
const info = unwrap(await env.fileInfo(real.value, BACKGROUND_CONTEXT));
|
|
5014
|
+
return info.kind === "file" ? info.size : null;
|
|
5015
|
+
},
|
|
5016
|
+
async close() {
|
|
5017
|
+
if (closed) return;
|
|
5018
|
+
closed = true;
|
|
5019
|
+
await options.close?.();
|
|
5020
|
+
}
|
|
5021
|
+
};
|
|
5022
|
+
}
|
|
5023
|
+
var context = (signal) => signal ? withAbortSignal(signal, BACKGROUND_CONTEXT) : BACKGROUND_CONTEXT;
|
|
5024
|
+
function unwrap(result) {
|
|
5025
|
+
if (result.ok) return result.value;
|
|
5026
|
+
throw fault(result.error);
|
|
5027
|
+
}
|
|
5028
|
+
function fault(error) {
|
|
5029
|
+
return Object.assign(new Error(error.message), { code: error.code });
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
// src/server/infra/env/docker.ts
|
|
5033
|
+
var COMPUTER_CWD = "/workspace";
|
|
5034
|
+
var DAEMON_DIR = "/opt/jarvy";
|
|
5035
|
+
function docker(args, timeoutMs = 6e4) {
|
|
5036
|
+
return new Promise((resolve5, reject) => execFile("docker", args, { timeout: timeoutMs, maxBuffer: 8 << 20 }, (error, stdout, stderr) => {
|
|
5037
|
+
if (error) reject(Object.assign(new Error((stderr || error.message).trim().split("\n").at(-1) ?? "docker failed"), { code: "docker_failed" }));
|
|
5038
|
+
else resolve5(stdout.trim());
|
|
5039
|
+
}));
|
|
5040
|
+
}
|
|
5041
|
+
async function dockerAvailable() {
|
|
5042
|
+
try {
|
|
5043
|
+
await docker(["version", "--format", "{{.Server.Version}}"], 1e4);
|
|
5044
|
+
return true;
|
|
5045
|
+
} catch {
|
|
5046
|
+
return false;
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
function computerImage(uid, gid) {
|
|
5050
|
+
return `jarvy-computer:${createHash3("sha256").update(`${COMPUTER_DOCKERFILE}
|
|
5051
|
+
${uid}:${gid}`).digest("hex").slice(0, 12)}`;
|
|
5052
|
+
}
|
|
5053
|
+
async function pruneImages(keep) {
|
|
5054
|
+
const tags = (await docker(["images", "--filter", "label=jarvy.computer=1", "--format", "{{.Repository}}:{{.Tag}}"])).split("\n").filter(Boolean);
|
|
5055
|
+
const removed = [];
|
|
5056
|
+
for (const tag of tags) if (tag !== keep) {
|
|
5057
|
+
try {
|
|
5058
|
+
await docker(["image", "rm", tag], 12e4);
|
|
5059
|
+
removed.push(tag);
|
|
5060
|
+
} catch {
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
return removed;
|
|
5064
|
+
}
|
|
5065
|
+
async function imageExists(tag) {
|
|
5066
|
+
try {
|
|
5067
|
+
await docker(["image", "inspect", "--format", "{{.Id}}", tag]);
|
|
5068
|
+
return true;
|
|
5069
|
+
} catch {
|
|
5070
|
+
return false;
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
async function buildImage(tag, uid, gid, logFile) {
|
|
5074
|
+
const dir = await mkdtemp(join4(tmpdir(), "jarvy-computer-"));
|
|
5075
|
+
try {
|
|
5076
|
+
await writeFile2(join4(dir, "Dockerfile"), COMPUTER_DOCKERFILE);
|
|
5077
|
+
const log = createWriteStream(logFile, { flags: "w", mode: 384 });
|
|
5078
|
+
const code = await new Promise((resolve5, reject) => {
|
|
5079
|
+
const child = spawn(
|
|
5080
|
+
"docker",
|
|
5081
|
+
["build", "-t", tag, "--label", "jarvy.computer=1", "--build-arg", `UID=${uid}`, "--build-arg", `GID=${gid}`, dir],
|
|
5082
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
5083
|
+
);
|
|
5084
|
+
child.stdout.pipe(log, { end: false });
|
|
5085
|
+
child.stderr.pipe(log, { end: false });
|
|
5086
|
+
child.on("error", reject);
|
|
5087
|
+
child.on("close", resolve5);
|
|
5088
|
+
}).finally(() => log.end());
|
|
5089
|
+
if (code !== 0) throw Object.assign(new Error(`docker build exited with ${code}`), { code: "docker_failed" });
|
|
5090
|
+
} finally {
|
|
5091
|
+
await rm2(dir, { recursive: true, force: true });
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
async function containerStates(label) {
|
|
5095
|
+
const out = await docker(["ps", "-a", "--filter", `label=${label}`, "--format", "{{.Names}} {{.State}}"]);
|
|
5096
|
+
const states = /* @__PURE__ */ new Map();
|
|
5097
|
+
for (const line of out.split("\n").filter(Boolean)) {
|
|
5098
|
+
const [name2, state] = line.split(" ");
|
|
5099
|
+
if (name2) states.set(name2, state === "running" ? "running" : "stopped");
|
|
5100
|
+
}
|
|
5101
|
+
return states;
|
|
5102
|
+
}
|
|
5103
|
+
async function containerState(name2) {
|
|
5104
|
+
try {
|
|
5105
|
+
return await docker(["inspect", "--format", "{{.State.Running}}", name2]) === "true" ? "running" : "stopped";
|
|
5106
|
+
} catch (error) {
|
|
5107
|
+
if (/no such (object|container)/i.test(error.message)) return "missing";
|
|
5108
|
+
throw error;
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
async function runContainer(spec) {
|
|
5112
|
+
await docker([
|
|
5113
|
+
"run",
|
|
5114
|
+
"-d",
|
|
5115
|
+
"--name",
|
|
5116
|
+
spec.name,
|
|
5117
|
+
"--init",
|
|
5118
|
+
"--user",
|
|
5119
|
+
`${spec.uid}:${spec.gid}`,
|
|
5120
|
+
"--shm-size",
|
|
5121
|
+
"1g",
|
|
5122
|
+
...Object.entries(spec.labels).flatMap(([key, value2]) => ["--label", `${key}=${value2}`]),
|
|
5123
|
+
"-v",
|
|
5124
|
+
`${spec.workspace}:${COMPUTER_CWD}`,
|
|
5125
|
+
"-v",
|
|
5126
|
+
`${spec.daemonDir}:${DAEMON_DIR}:ro`,
|
|
5127
|
+
"-w",
|
|
5128
|
+
COMPUTER_CWD,
|
|
5129
|
+
spec.image,
|
|
5130
|
+
"sleep",
|
|
5131
|
+
"infinity"
|
|
5132
|
+
], 12e4);
|
|
5133
|
+
}
|
|
5134
|
+
async function startContainer(name2) {
|
|
5135
|
+
await docker(["start", name2], 6e4);
|
|
5136
|
+
}
|
|
5137
|
+
async function homeAtWorkspace(name2) {
|
|
5138
|
+
const home = await docker(["exec", name2, "sh", "-c", "getent passwd bot | cut -d: -f6"], 3e4);
|
|
5139
|
+
if (home !== COMPUTER_CWD) await docker(["exec", "-u", "root", name2, "usermod", "-d", COMPUTER_CWD, "bot"], 3e4);
|
|
5140
|
+
}
|
|
5141
|
+
async function stopContainer(name2) {
|
|
5142
|
+
await docker(["stop", "--time", "5", name2], 6e4);
|
|
5143
|
+
}
|
|
5144
|
+
async function removeContainer(name2) {
|
|
5145
|
+
try {
|
|
5146
|
+
await docker(["rm", "--force", name2], 6e4);
|
|
5147
|
+
} catch (error) {
|
|
5148
|
+
if (!/no such (object|container)/i.test(error.message)) throw error;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
async function syncDaemon(dir) {
|
|
5152
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
5153
|
+
const wanted = await readFile2(packagedDaemon({ platform: "linux", arch }));
|
|
5154
|
+
const target2 = join4(dir, "pi-env");
|
|
5155
|
+
const current = await readFile2(target2).catch(() => null);
|
|
5156
|
+
if (current?.equals(wanted)) return;
|
|
5157
|
+
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
5158
|
+
const temp = `${target2}.${process.pid}.tmp`;
|
|
5159
|
+
await writeFile2(temp, wanted, { mode: 493 });
|
|
5160
|
+
await rename2(temp, target2);
|
|
5161
|
+
}
|
|
5162
|
+
function containerEnvironment(id, container, log) {
|
|
5163
|
+
const connection = new Connection({ command: ["docker", "exec", "-i", container, `${DAEMON_DIR}/pi-env`], onLog: (text3) => log(text3.trim()) });
|
|
5164
|
+
return overDurable(id, "computer", new RemoteExecutionEnv({ connection, id, cwd: COMPUTER_CWD }), { passEnv: false, close: () => connection.close() });
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
// src/server/infra/env/host.ts
|
|
5168
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
5169
|
+
function hostEnvironment(cwd) {
|
|
5170
|
+
return overDurable("host", "host", new NodeExecutionEnv({ cwd }), { passEnv: true });
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
// src/server/service/bot/environments.ts
|
|
5174
|
+
var ABOUT = {
|
|
5175
|
+
computer: "A Linux container only you use, with Node, Python, git, build tools, and Playwright with its browsers. You can install software there with sudo; it stays until the owner resets your sandbox, and /workspace, your home there, is kept when it is reset.",
|
|
5176
|
+
host: "The machine running Jarvy. Your tools run there as your owner and can reach their files: work in your directory, and leave other members' workspaces, live data directories (such as ~/.jarvy), main repository checkouts and system directories alone unless the task needs it; say in your reply what you changed there."
|
|
5177
|
+
};
|
|
5178
|
+
var BotEnvironments = class {
|
|
5179
|
+
constructor(store, computers) {
|
|
5180
|
+
this.store = store;
|
|
5181
|
+
this.computers = computers;
|
|
5182
|
+
}
|
|
5183
|
+
open = /* @__PURE__ */ new Map();
|
|
5184
|
+
current(botId, workspacePath) {
|
|
5185
|
+
const target2 = this.store.environments.placement(botId).current;
|
|
5186
|
+
const open = this.open.get(botId);
|
|
5187
|
+
if (open?.id === target2.id) return open.view;
|
|
5188
|
+
if (open) this.retire(open);
|
|
5189
|
+
const opened = this.track(target2.id, target2.kind === "computer" ? this.computers.environment(target2.botId ?? botId, target2) : hostEnvironment(workspacePath));
|
|
5190
|
+
this.open.set(botId, opened);
|
|
5191
|
+
return opened.view;
|
|
5192
|
+
}
|
|
5193
|
+
placement(botId) {
|
|
5194
|
+
const environments = this.store.environments, { home, current } = environments.placement(botId);
|
|
5195
|
+
const hostId = environments.host().id;
|
|
5196
|
+
return {
|
|
5197
|
+
home: home.kind,
|
|
5198
|
+
current: current.kind,
|
|
5199
|
+
computer: this.computers.status(botId),
|
|
5200
|
+
docker: this.computers.available,
|
|
5201
|
+
hostGranted: environments.grants(botId).some((grant) => grant.id === hostId)
|
|
5202
|
+
};
|
|
5203
|
+
}
|
|
5204
|
+
/** The environments the Bot may be in: its sandbox, its default, and the ones its owner granted it. */
|
|
5205
|
+
usable(botId) {
|
|
5206
|
+
const environments = this.store.environments;
|
|
5207
|
+
const all = [environments.computerOf(botId), environments.placement(botId).home, ...environments.grants(botId)];
|
|
5208
|
+
return [...new Map(all.filter((record3) => !!record3).map((record3) => [record3.id, record3])).values()];
|
|
5209
|
+
}
|
|
5210
|
+
place(botId, record3, workspacePath) {
|
|
5211
|
+
if (record3.kind === "host") return { id: record3.id, kind: "host", name: "your owner's computer", directory: workspacePath };
|
|
5212
|
+
return { id: record3.id, kind: "sandbox", name: record3.botId === botId ? "your sandbox" : "another Bot's sandbox", directory: COMPUTER_CWD };
|
|
5213
|
+
}
|
|
5214
|
+
/** A Bot's environments as other Bots see them in bots.list. */
|
|
5215
|
+
summary(botId) {
|
|
5216
|
+
const name2 = (record3) => record3.kind === "host" ? "owner's computer" : record3.botId === botId ? "own sandbox" : "another Bot's sandbox";
|
|
5217
|
+
const { home, current } = this.store.environments.placement(botId);
|
|
5218
|
+
return { default: name2(home), current: name2(current), usable: this.usable(botId).map(name2) };
|
|
5219
|
+
}
|
|
5220
|
+
/** The Bot's default environment, which its instructions describe. */
|
|
5221
|
+
home(botId, workspacePath) {
|
|
5222
|
+
return this.place(botId, this.store.environments.placement(botId).home, workspacePath);
|
|
5223
|
+
}
|
|
5224
|
+
/** Where the Bot works now, for the end of each batch. */
|
|
5225
|
+
here(botId, workspacePath) {
|
|
5226
|
+
return this.place(botId, this.store.environments.placement(botId).current, workspacePath);
|
|
5227
|
+
}
|
|
5228
|
+
/** environments.list: where the Bot is and where else it may go. */
|
|
5229
|
+
list(botId, workspacePath) {
|
|
5230
|
+
const { home, current } = this.store.environments.placement(botId);
|
|
5231
|
+
return { current: current.id, environments: this.usable(botId).map((record3) => ({
|
|
5232
|
+
...this.place(botId, record3, workspacePath),
|
|
5233
|
+
...record3.id === home.id ? { default: true } : {},
|
|
5234
|
+
...record3.kind === "computer" ? { state: this.computers.status(record3.botId ?? botId)?.state ?? "stopped" } : {}
|
|
5235
|
+
})) };
|
|
5236
|
+
}
|
|
5237
|
+
/** environments.switch: the Bot moves itself, like cd, until it switches back. A sandbox is started first; if it
|
|
5238
|
+
* can't start, the Bot stays where it was. */
|
|
5239
|
+
async switch(botId, environmentId, workspacePath) {
|
|
5240
|
+
const target2 = this.usable(botId).find((record3) => record3.id === environmentId);
|
|
5241
|
+
if (!target2) throw new ChatError(404, "No work environment with this id that you may use; see environments.list");
|
|
5242
|
+
const already = this.store.environments.placement(botId).current.id === target2.id;
|
|
5243
|
+
if (target2.kind === "computer") await this.computers.ensure(target2.botId ?? botId);
|
|
5244
|
+
this.store.environments.setCurrent(botId, target2.id);
|
|
5245
|
+
return { ...this.place(botId, target2, workspacePath), about: ABOUT[target2.kind], ...already ? { note: "You were already here." } : {} };
|
|
5246
|
+
}
|
|
5247
|
+
/** The owner makes the Bot's sandbox (given it on first use) or their computer its default, and moves it there. */
|
|
5248
|
+
setHome(botId, kind, workspacePath) {
|
|
5249
|
+
const environments = this.store.environments, from = this.here(botId, workspacePath);
|
|
5250
|
+
if (kind === "host") environments.setHome(botId, environments.host().id);
|
|
5251
|
+
else {
|
|
5252
|
+
const computer = environments.computerOf(botId);
|
|
5253
|
+
if (computer) {
|
|
5254
|
+
environments.setHome(botId, computer.id);
|
|
5255
|
+
void this.computers.ensure(botId).catch(() => {
|
|
5256
|
+
});
|
|
5257
|
+
} else this.computers.create(botId);
|
|
5258
|
+
}
|
|
5259
|
+
const to = this.here(botId, workspacePath);
|
|
5260
|
+
return from.id === to.id ? null : { from, to };
|
|
5261
|
+
}
|
|
5262
|
+
/** The owner grants or takes back their computer (A50); taking it back while the Bot works there sends it home. */
|
|
5263
|
+
grantHost(botId, granted, by, workspacePath) {
|
|
5264
|
+
const environments = this.store.environments, host = environments.host();
|
|
5265
|
+
if (granted) return { changed: environments.grant(host.id, botId, by), moved: null };
|
|
5266
|
+
const changed = environments.revoke(host.id, botId), { home, current } = environments.placement(botId);
|
|
5267
|
+
if (current.id !== host.id || home.id === host.id) return { changed, moved: null };
|
|
5268
|
+
const from = this.here(botId, workspacePath);
|
|
5269
|
+
environments.setCurrent(botId, home.id);
|
|
5270
|
+
return { changed, moved: { from, to: this.here(botId, workspacePath) } };
|
|
5271
|
+
}
|
|
5272
|
+
async release(botId) {
|
|
5273
|
+
const open = this.open.get(botId);
|
|
5274
|
+
this.open.delete(botId);
|
|
5275
|
+
await open?.env.close();
|
|
5276
|
+
await this.computers.remove(botId);
|
|
5277
|
+
}
|
|
5278
|
+
async close() {
|
|
5279
|
+
const all = [...this.open.values()];
|
|
5280
|
+
this.open.clear();
|
|
5281
|
+
await Promise.all(all.map((open) => open.env.close()));
|
|
5282
|
+
await this.computers.close();
|
|
5283
|
+
}
|
|
5284
|
+
retire(open) {
|
|
5285
|
+
open.retired = true;
|
|
5286
|
+
if (!open.busy) void open.env.close();
|
|
5287
|
+
}
|
|
5288
|
+
/** Counts the operations running on an environment, so one the Bot has left closes only when they are done. */
|
|
5289
|
+
track(id, env) {
|
|
5290
|
+
const opened = { id, env, view: env, busy: 0, retired: false };
|
|
5291
|
+
const run = async (work) => {
|
|
5292
|
+
opened.busy++;
|
|
5293
|
+
try {
|
|
5294
|
+
return await work();
|
|
5295
|
+
} finally {
|
|
5296
|
+
if (--opened.busy === 0 && opened.retired) void env.close();
|
|
5297
|
+
}
|
|
5298
|
+
};
|
|
5299
|
+
opened.view = {
|
|
5300
|
+
id: env.id,
|
|
5301
|
+
kind: env.kind,
|
|
5302
|
+
cwd: env.cwd,
|
|
5303
|
+
exec: (command, options) => run(() => env.exec(command, options)),
|
|
5304
|
+
readFile: (path) => run(() => env.readFile(path)),
|
|
5305
|
+
head: (path, bytes) => run(() => env.head(path, bytes)),
|
|
5306
|
+
writeFile: (path, content2) => run(() => env.writeFile(path, content2)),
|
|
5307
|
+
mkdir: (path) => run(() => env.mkdir(path)),
|
|
5308
|
+
exists: (path) => run(() => env.exists(path)),
|
|
5309
|
+
fileSize: (path) => run(() => env.fileSize(path)),
|
|
5310
|
+
close: () => env.close()
|
|
5311
|
+
};
|
|
5312
|
+
return opened;
|
|
5313
|
+
}
|
|
5314
|
+
};
|
|
5315
|
+
|
|
5316
|
+
// src/server/service/bot/computers.ts
|
|
5317
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
5318
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5319
|
+
var SWEEP_MS = 6e4;
|
|
5320
|
+
var Computers = class {
|
|
5321
|
+
constructor(deps) {
|
|
5322
|
+
this.deps = deps;
|
|
5323
|
+
this.dataDir = dirname4(deps.store.path);
|
|
5324
|
+
this.tag = createHash4("sha256").update(this.dataDir).digest("hex").slice(0, 12);
|
|
5325
|
+
}
|
|
5326
|
+
docker = null;
|
|
5327
|
+
probing = null;
|
|
5328
|
+
image = null;
|
|
5329
|
+
timer = null;
|
|
5330
|
+
states = /* @__PURE__ */ new Map();
|
|
5331
|
+
preparing = /* @__PURE__ */ new Map();
|
|
5332
|
+
connections = /* @__PURE__ */ new Map();
|
|
5333
|
+
/** Containers whose home was checked since Jarvy started (`homeAtWorkspace`). */
|
|
5334
|
+
homes = /* @__PURE__ */ new Map();
|
|
5335
|
+
dataDir;
|
|
5336
|
+
/** Tells this Jarvy's containers from those of other data directories on the same Docker. */
|
|
5337
|
+
tag;
|
|
5338
|
+
uid = process.getuid?.() ?? 1e3;
|
|
5339
|
+
gid = process.getgid?.() ?? 1e3;
|
|
5340
|
+
/** Looks for Docker and learns the state of every computer, once; every computer waits for it. */
|
|
5341
|
+
start() {
|
|
5342
|
+
this.probing ??= this.probe();
|
|
5343
|
+
return this.probing;
|
|
5344
|
+
}
|
|
5345
|
+
async probe() {
|
|
5346
|
+
this.docker = await dockerAvailable();
|
|
5347
|
+
if (!this.docker) return;
|
|
5348
|
+
try {
|
|
5349
|
+
await syncDaemon(join5(this.dataDir, "computers", "bin"));
|
|
5350
|
+
} catch (error) {
|
|
5351
|
+
this.docker = false;
|
|
5352
|
+
this.log(`computers disabled: ${error.message}`);
|
|
5353
|
+
return;
|
|
5354
|
+
}
|
|
5355
|
+
await this.sweep().catch((error) => this.log(`computer sweep failed: ${error.message}`));
|
|
5356
|
+
this.timer = setInterval(() => void this.sweep().catch((error) => this.log(`computer sweep failed: ${error.message}`)), SWEEP_MS);
|
|
5357
|
+
this.timer.unref();
|
|
5358
|
+
}
|
|
5359
|
+
/** Whether new computers can be made here; false until `start` found Docker. */
|
|
5360
|
+
get available() {
|
|
5361
|
+
return this.docker === true;
|
|
5362
|
+
}
|
|
5363
|
+
status(botId) {
|
|
5364
|
+
if (!this.deps.store.environments.computerOf(botId)) return null;
|
|
5365
|
+
if (!this.available) return { state: "unavailable" };
|
|
5366
|
+
return this.states.get(botId) ?? { state: "stopped" };
|
|
5367
|
+
}
|
|
5368
|
+
/** Gives a Bot its computer and makes it the Bot's default environment; the container is made in the background. */
|
|
5369
|
+
create(botId) {
|
|
5370
|
+
if (!this.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 409);
|
|
5371
|
+
const computer = this.deps.store.environments.addComputer(botId, `jarvy-${this.tag.slice(0, 6)}-${botId.slice(0, 8)}`);
|
|
5372
|
+
this.deps.store.environments.setHome(botId, computer.id);
|
|
5373
|
+
void this.ensure(botId).catch(() => {
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
/** What the Bot's tools use while it works on its computer: every call first makes sure the container runs. */
|
|
5377
|
+
environment(botId, record3) {
|
|
5378
|
+
const run = async (op) => {
|
|
5379
|
+
try {
|
|
5380
|
+
return await op(await this.connect(botId, record3));
|
|
5381
|
+
} catch (error) {
|
|
5382
|
+
if (error instanceof RuntimeFault || await containerState(record3.spec.container).catch(() => "missing") === "running") throw error;
|
|
5383
|
+
await this.disconnect(botId);
|
|
5384
|
+
this.set(botId, { state: "stopped" });
|
|
5385
|
+
return op(await this.connect(botId, record3));
|
|
5386
|
+
}
|
|
5387
|
+
};
|
|
5388
|
+
return {
|
|
5389
|
+
id: record3.id,
|
|
5390
|
+
kind: "computer",
|
|
5391
|
+
cwd: COMPUTER_CWD,
|
|
5392
|
+
exec: (command, options) => run((env) => env.exec(command, options)),
|
|
5393
|
+
readFile: (path) => run((env) => env.readFile(path)),
|
|
5394
|
+
head: (path, bytes) => run((env) => env.head(path, bytes)),
|
|
5395
|
+
writeFile: (path, content2) => run((env) => env.writeFile(path, content2)),
|
|
5396
|
+
mkdir: (path) => run((env) => env.mkdir(path)),
|
|
5397
|
+
exists: (path) => run((env) => env.exists(path)),
|
|
5398
|
+
fileSize: (path) => run((env) => env.fileSize(path)),
|
|
5399
|
+
close: () => this.disconnect(botId)
|
|
5400
|
+
};
|
|
5401
|
+
}
|
|
5402
|
+
async connect(botId, record3) {
|
|
5403
|
+
const container = await this.ensure(botId);
|
|
5404
|
+
let home = this.homes.get(container);
|
|
5405
|
+
if (!home) {
|
|
5406
|
+
home = homeAtWorkspace(container).catch((error) => this.log(`home of ${botId} not moved: ${error.message}`));
|
|
5407
|
+
this.homes.set(container, home);
|
|
5408
|
+
}
|
|
5409
|
+
await home;
|
|
5410
|
+
let env = this.connections.get(botId);
|
|
5411
|
+
if (!env) {
|
|
5412
|
+
env = containerEnvironment(record3.id, container, (text3) => this.log(`[${botId.slice(0, 8)}] ${text3}`));
|
|
5413
|
+
this.connections.set(botId, env);
|
|
5414
|
+
}
|
|
5415
|
+
return env;
|
|
5416
|
+
}
|
|
5417
|
+
/** Makes sure the container exists and runs, building the image first when needed; returns its name. */
|
|
5418
|
+
ensure(botId) {
|
|
5419
|
+
const pending = this.preparing.get(botId);
|
|
5420
|
+
if (pending) return pending;
|
|
5421
|
+
const computer = this.deps.store.environments.computerOf(botId);
|
|
5422
|
+
if (!computer) return Promise.reject(new RuntimeFault("computer_missing", "This Bot has no sandbox.", 404));
|
|
5423
|
+
const container = computer.spec.container;
|
|
5424
|
+
if (this.states.get(botId)?.state === "running") return Promise.resolve(container);
|
|
5425
|
+
if (this.docker !== false) this.set(botId, { state: "preparing" });
|
|
5426
|
+
const task = (async () => {
|
|
5427
|
+
try {
|
|
5428
|
+
await this.start();
|
|
5429
|
+
if (!this.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 503);
|
|
5430
|
+
const image = await this.imageReady();
|
|
5431
|
+
const state = await containerState(container);
|
|
5432
|
+
if (state === "missing") await runContainer({
|
|
5433
|
+
name: container,
|
|
5434
|
+
image,
|
|
5435
|
+
workspace: join5(this.dataDir, "bots", botId, "workspace"),
|
|
5436
|
+
daemonDir: join5(this.dataDir, "computers", "bin"),
|
|
5437
|
+
uid: this.uid,
|
|
5438
|
+
gid: this.gid,
|
|
5439
|
+
labels: { "jarvy.data": this.tag, "jarvy.bot": botId }
|
|
5440
|
+
});
|
|
5441
|
+
else if (state === "stopped") await startContainer(container);
|
|
5442
|
+
this.set(botId, { state: "running" });
|
|
5443
|
+
return container;
|
|
5444
|
+
} catch (error) {
|
|
5445
|
+
this.set(botId, { state: "failed", error: error instanceof RuntimeFault ? error.code : "computer_failed" });
|
|
5446
|
+
this.log(`computer of ${botId} failed: ${error.message}`);
|
|
5447
|
+
if (error instanceof RuntimeFault) throw error;
|
|
5448
|
+
throw new RuntimeFault("computer_failed", `Your sandbox could not start: ${error.message}`, 503);
|
|
5449
|
+
}
|
|
5450
|
+
})().finally(() => this.preparing.delete(botId));
|
|
5451
|
+
this.preparing.set(botId, task);
|
|
5452
|
+
return task;
|
|
5453
|
+
}
|
|
5454
|
+
async stop(botId) {
|
|
5455
|
+
const computer = this.need(botId);
|
|
5456
|
+
await this.disconnect(botId);
|
|
5457
|
+
await stopContainer(computer.spec.container);
|
|
5458
|
+
this.set(botId, { state: "stopped" });
|
|
5459
|
+
}
|
|
5460
|
+
/** Removes the container, so the next start makes a fresh one from the image: what was installed goes, the
|
|
5461
|
+
* workspace stays. */
|
|
5462
|
+
async reset(botId) {
|
|
5463
|
+
const computer = this.need(botId);
|
|
5464
|
+
await this.disconnect(botId);
|
|
5465
|
+
await removeContainer(computer.spec.container);
|
|
5466
|
+
this.homes.delete(computer.spec.container);
|
|
5467
|
+
this.set(botId, { state: "stopped" });
|
|
5468
|
+
}
|
|
5469
|
+
/** The Bot is being deleted: its container goes with it. */
|
|
5470
|
+
async remove(botId) {
|
|
5471
|
+
const computer = this.deps.store.environments.computerOf(botId);
|
|
5472
|
+
if (!computer) return;
|
|
5473
|
+
await this.disconnect(botId);
|
|
5474
|
+
if (this.available) await removeContainer(computer.spec.container);
|
|
5475
|
+
this.deps.store.environments.dropComputer(botId);
|
|
5476
|
+
this.states.delete(botId);
|
|
5477
|
+
}
|
|
5478
|
+
/** Closes the connections; containers keep running. */
|
|
5479
|
+
async close() {
|
|
5480
|
+
if (this.timer) clearInterval(this.timer);
|
|
5481
|
+
this.timer = null;
|
|
5482
|
+
await Promise.all([...this.connections.keys()].map((botId) => this.disconnect(botId)));
|
|
5483
|
+
}
|
|
5484
|
+
need(botId) {
|
|
5485
|
+
const computer = this.deps.store.environments.computerOf(botId);
|
|
5486
|
+
if (!computer) throw new RuntimeFault("computer_missing", "This Bot has no sandbox.", 404);
|
|
5487
|
+
if (!this.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 409);
|
|
5488
|
+
return computer;
|
|
5489
|
+
}
|
|
5490
|
+
imageReady() {
|
|
5491
|
+
this.image ??= (async () => {
|
|
5492
|
+
const tag = computerImage(this.uid, this.gid);
|
|
5493
|
+
if (!await imageExists(tag)) {
|
|
5494
|
+
this.log(`building ${tag}`);
|
|
5495
|
+
await buildImage(tag, this.uid, this.gid, join5(this.dataDir, "computers", "build.log"));
|
|
5496
|
+
void pruneImages(tag).then((removed) => {
|
|
5497
|
+
if (removed.length) this.log(`removed ${removed.join(", ")}`);
|
|
5498
|
+
}).catch(() => {
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
return tag;
|
|
5502
|
+
})().catch((error) => {
|
|
5503
|
+
this.image = null;
|
|
5504
|
+
throw error;
|
|
5505
|
+
});
|
|
5506
|
+
return this.image;
|
|
5507
|
+
}
|
|
5508
|
+
async disconnect(botId) {
|
|
5509
|
+
const env = this.connections.get(botId);
|
|
5510
|
+
this.connections.delete(botId);
|
|
5511
|
+
await env?.close();
|
|
5512
|
+
}
|
|
5513
|
+
set(botId, status) {
|
|
5514
|
+
const before = this.states.get(botId);
|
|
5515
|
+
this.states.set(botId, status);
|
|
5516
|
+
if (before?.state !== status.state || before?.error !== status.error) this.deps.changed();
|
|
5517
|
+
}
|
|
5518
|
+
/** Learns what happened to containers outside Jarvy, and removes containers of this Jarvy that no Bot has any more
|
|
5519
|
+
* (a Bot deleted while Docker was out of reach). */
|
|
5520
|
+
async sweep() {
|
|
5521
|
+
const states = await containerStates(`jarvy.data=${this.tag}`);
|
|
5522
|
+
const computers = this.deps.store.environments.computers();
|
|
5523
|
+
const owned = new Set(computers.map((computer) => computer.spec.container));
|
|
5524
|
+
for (const name2 of states.keys()) if (!owned.has(name2)) {
|
|
5525
|
+
this.log(`removing ${name2}, which no Bot has`);
|
|
5526
|
+
await removeContainer(name2).catch((error) => this.log(`removing ${name2} failed: ${error.message}`));
|
|
5527
|
+
}
|
|
5528
|
+
for (const computer of computers) {
|
|
5529
|
+
const botId = computer.botId;
|
|
5530
|
+
if (this.preparing.has(botId)) continue;
|
|
5531
|
+
const running = states.get(computer.spec.container) === "running";
|
|
5532
|
+
if (!running) {
|
|
5533
|
+
if (this.states.get(botId)?.state !== "failed") this.set(botId, { state: "stopped" });
|
|
5534
|
+
await this.disconnect(botId);
|
|
5535
|
+
continue;
|
|
5536
|
+
}
|
|
5537
|
+
this.set(botId, { state: "running" });
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
log(text3) {
|
|
5541
|
+
(this.deps.log ?? console.log)(`[computers] ${text3}`);
|
|
5542
|
+
}
|
|
5543
|
+
};
|
|
5544
|
+
|
|
4539
5545
|
// src/server/infra/storage/history-repository.ts
|
|
4540
5546
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
4541
5547
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
4542
|
-
import { dirname as
|
|
5548
|
+
import { dirname as dirname5 } from "node:path";
|
|
4543
5549
|
var json = (value2) => value2 == null ? null : JSON.stringify(value2);
|
|
4544
5550
|
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')=?)";
|
|
4545
5551
|
var BLOCKS = "FROM json_each(history.message,'$.content') b WHERE json_extract(b.value,'$.type')";
|
|
@@ -4551,7 +5557,7 @@ var SUMMARY = `SELECT id,timestamp,role,model,
|
|
|
4551
5557
|
var HistoryRepository = class {
|
|
4552
5558
|
db;
|
|
4553
5559
|
constructor(path) {
|
|
4554
|
-
mkdirSync2(
|
|
5560
|
+
mkdirSync2(dirname5(path), { recursive: true, mode: 448 });
|
|
4555
5561
|
this.db = new DatabaseSync2(path);
|
|
4556
5562
|
chmodSync2(path, 384);
|
|
4557
5563
|
const version = this.db.prepare("PRAGMA user_version").get().user_version;
|
|
@@ -4607,17 +5613,17 @@ var HistoryRepository = class {
|
|
|
4607
5613
|
const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
|
|
4608
5614
|
return row ? this.decode(row) : null;
|
|
4609
5615
|
}
|
|
4610
|
-
links(
|
|
5616
|
+
links(record3) {
|
|
4611
5617
|
const links = [];
|
|
4612
|
-
for (const block of
|
|
5618
|
+
for (const block of record3.message.content) {
|
|
4613
5619
|
if (block.type !== "tool_call" && block.type !== "tool_result") continue;
|
|
4614
5620
|
const callId = block.tool_call_id;
|
|
4615
5621
|
if (block.type === "tool_result") {
|
|
4616
|
-
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(
|
|
5622
|
+
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(record3.id, "tool_call", callId);
|
|
4617
5623
|
if (call) links.push({ ...call, tool_call_id: callId });
|
|
4618
5624
|
} else {
|
|
4619
|
-
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(
|
|
4620
|
-
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(
|
|
5625
|
+
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(record3.id, "tool_call", callId);
|
|
5626
|
+
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record3.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
|
|
4621
5627
|
links.push(...results.map((result) => ({ ...result, tool_call_id: callId })));
|
|
4622
5628
|
}
|
|
4623
5629
|
}
|
|
@@ -4661,14 +5667,14 @@ var Memory = class {
|
|
|
4661
5667
|
return this.history().usage(since);
|
|
4662
5668
|
}
|
|
4663
5669
|
read(id) {
|
|
4664
|
-
const
|
|
4665
|
-
return
|
|
5670
|
+
const record3 = this.history().read(id);
|
|
5671
|
+
return record3 ? { record: record3, links: this.history().links(record3) } : null;
|
|
4666
5672
|
}
|
|
4667
5673
|
readMany(ids, mode) {
|
|
4668
5674
|
const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
|
|
4669
5675
|
const result = { mode, records: [], missingIds: [] };
|
|
4670
|
-
found.forEach((
|
|
4671
|
-
if (
|
|
5676
|
+
found.forEach((record3, index) => {
|
|
5677
|
+
if (record3) result.records.push(record3);
|
|
4672
5678
|
else result.missingIds.push(ids[index]);
|
|
4673
5679
|
});
|
|
4674
5680
|
return result;
|
|
@@ -4680,7 +5686,7 @@ var Memory = class {
|
|
|
4680
5686
|
};
|
|
4681
5687
|
|
|
4682
5688
|
// src/server/service/bot/bot.ts
|
|
4683
|
-
import { createHash as
|
|
5689
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
4684
5690
|
var Bot = class extends Member {
|
|
4685
5691
|
kind = "bot";
|
|
4686
5692
|
identity;
|
|
@@ -4771,7 +5777,7 @@ var Bot = class extends Member {
|
|
|
4771
5777
|
...message.question ? { question: message.question } : {},
|
|
4772
5778
|
...message.alsoToChannel ? { alsoToChannel: true } : {},
|
|
4773
5779
|
...message.replyTo ? { replyTo: message.replyTo } : {},
|
|
4774
|
-
clientMessageId: source2 ?
|
|
5780
|
+
clientMessageId: source2 ? createHash5("sha256").update(JSON.stringify([this.id, source2.historyId, source2.toolCallId])).digest("hex") : void 0
|
|
4775
5781
|
});
|
|
4776
5782
|
}
|
|
4777
5783
|
/** 由 Bot 协调配置与运行实例;配置在应用成功后提交。执行中切换从下一次模型请求生效(A28)。 */
|
|
@@ -4794,19 +5800,20 @@ var Bot = class extends Member {
|
|
|
4794
5800
|
identity: () => this.identity.snapshot(),
|
|
4795
5801
|
model: () => this.model.current(),
|
|
4796
5802
|
workspace: this.workspace.location(),
|
|
5803
|
+
home: () => this.workspace.home(),
|
|
4797
5804
|
tools: this.tools.definitions(),
|
|
4798
5805
|
onStatus: (status) => this.updateState(status),
|
|
4799
|
-
onHistory: (
|
|
5806
|
+
onHistory: (record3) => this.recordHistory(record3)
|
|
4800
5807
|
};
|
|
4801
5808
|
}
|
|
4802
5809
|
updateState(status) {
|
|
4803
5810
|
this.state.update(status);
|
|
4804
5811
|
}
|
|
4805
|
-
async recordHistory(
|
|
4806
|
-
const historyId = await this.memory.append(
|
|
4807
|
-
if (
|
|
5812
|
+
async recordHistory(record3) {
|
|
5813
|
+
const historyId = await this.memory.append(record3);
|
|
5814
|
+
if (record3.role !== "assistant") return;
|
|
4808
5815
|
this.callHistory.clear();
|
|
4809
|
-
for (const block of
|
|
5816
|
+
for (const block of record3.message.content) {
|
|
4810
5817
|
if (block.type === "tool_call") this.callHistory.set(block.tool_call_id, historyId);
|
|
4811
5818
|
}
|
|
4812
5819
|
}
|
|
@@ -4831,16 +5838,16 @@ var Identity = class {
|
|
|
4831
5838
|
}
|
|
4832
5839
|
snapshot() {
|
|
4833
5840
|
const bot = this.store.getBot(this.botId);
|
|
4834
|
-
return { name: bot.name, description: bot.description, persona: bot.systemPrompt };
|
|
5841
|
+
return { name: bot.name, description: bot.description, persona: bot.systemPrompt, agent: this.botId === AGENT_BOT_ID };
|
|
4835
5842
|
}
|
|
4836
5843
|
};
|
|
4837
5844
|
|
|
4838
5845
|
// src/server/service/bot/model.ts
|
|
4839
5846
|
var Model = class {
|
|
4840
|
-
constructor(botId, store,
|
|
5847
|
+
constructor(botId, store, resolve5) {
|
|
4841
5848
|
this.botId = botId;
|
|
4842
5849
|
this.store = store;
|
|
4843
|
-
this.resolve =
|
|
5850
|
+
this.resolve = resolve5;
|
|
4844
5851
|
}
|
|
4845
5852
|
current() {
|
|
4846
5853
|
const bot = this.store.getBot(this.botId);
|
|
@@ -4861,43 +5868,86 @@ var Model = class {
|
|
|
4861
5868
|
}
|
|
4862
5869
|
};
|
|
4863
5870
|
|
|
5871
|
+
// src/server/service/bot/workspace.ts
|
|
5872
|
+
import { posix } from "node:path";
|
|
5873
|
+
|
|
4864
5874
|
// src/server/infra/files/workspace.ts
|
|
4865
|
-
import { mkdir as
|
|
4866
|
-
import { join as
|
|
5875
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "node:fs/promises";
|
|
5876
|
+
import { join as join6 } from "node:path";
|
|
4867
5877
|
var LocalWorkspace = class {
|
|
4868
5878
|
constructor(cwd) {
|
|
4869
5879
|
this.cwd = cwd;
|
|
4870
5880
|
}
|
|
4871
5881
|
get tempDir() {
|
|
4872
|
-
return
|
|
5882
|
+
return join6(this.cwd, "temp");
|
|
4873
5883
|
}
|
|
4874
5884
|
async prepare() {
|
|
4875
|
-
await
|
|
5885
|
+
await mkdir4(this.tempDir, { recursive: true, mode: 448 });
|
|
4876
5886
|
}
|
|
4877
5887
|
async writeTempFile(name2, content2, signal) {
|
|
4878
5888
|
await this.prepare();
|
|
4879
|
-
const path =
|
|
4880
|
-
await
|
|
5889
|
+
const path = join6(this.tempDir, name2);
|
|
5890
|
+
await writeFile3(path, content2, { encoding: "utf8", flag: "wx", mode: 384, signal });
|
|
4881
5891
|
return path;
|
|
4882
5892
|
}
|
|
4883
5893
|
};
|
|
4884
5894
|
|
|
4885
5895
|
// src/server/service/bot/workspace.ts
|
|
5896
|
+
var MAX_FILE = 10 * 1024 * 1024;
|
|
4886
5897
|
var Workspace = class {
|
|
4887
|
-
|
|
4888
|
-
|
|
5898
|
+
constructor(cwd, current, home) {
|
|
5899
|
+
this.current = current;
|
|
5900
|
+
this.home = home;
|
|
4889
5901
|
this.local = new LocalWorkspace(cwd);
|
|
4890
5902
|
}
|
|
5903
|
+
local;
|
|
4891
5904
|
prepare() {
|
|
4892
5905
|
return this.local.prepare();
|
|
4893
5906
|
}
|
|
4894
|
-
|
|
4895
|
-
|
|
5907
|
+
/** A file in the temporary directory of the environment the Bot works in, by the path it sees there. */
|
|
5908
|
+
async writeTempFile(name2, content2, signal) {
|
|
5909
|
+
const env = this.current();
|
|
5910
|
+
if (env.kind === "host") return this.local.writeTempFile(name2, content2, signal);
|
|
5911
|
+
const path = `${env.cwd}/temp/${name2}`;
|
|
5912
|
+
await env.writeFile(path, content2);
|
|
5913
|
+
return path;
|
|
5914
|
+
}
|
|
5915
|
+
/** Puts a chat file into the Bot's environment: at `path`, or in temp/files/ under its own name (with part of its
|
|
5916
|
+
* id when another file has that name). Returns where it went. */
|
|
5917
|
+
async put(name2, id, data, path) {
|
|
5918
|
+
const env = this.current();
|
|
5919
|
+
let target2 = path ? posix.resolve(env.cwd, path) : `${env.cwd}/temp/files/${plain(name2)}`;
|
|
5920
|
+
if (!path && await env.exists(target2)) {
|
|
5921
|
+
const dot = plain(name2).lastIndexOf("."), stem = dot > 0 ? plain(name2).slice(0, dot) : plain(name2), extension = dot > 0 ? plain(name2).slice(dot) : "";
|
|
5922
|
+
target2 = `${env.cwd}/temp/files/${stem}-${id.slice(0, 8)}${extension}`;
|
|
5923
|
+
}
|
|
5924
|
+
await env.mkdir(posix.dirname(target2));
|
|
5925
|
+
await env.writeFile(target2, data);
|
|
5926
|
+
return target2;
|
|
5927
|
+
}
|
|
5928
|
+
/** The files a Bot attaches to a message, read from its environment: paths there, relative to its directory. */
|
|
5929
|
+
async readOutgoing(items, signal) {
|
|
5930
|
+
if (items.length > 5) throw new ChatError(400, "Too many attachments");
|
|
5931
|
+
const env = this.current(), files = [];
|
|
5932
|
+
for (const item of items) {
|
|
5933
|
+
signal?.throwIfAborted();
|
|
5934
|
+
const path = posix.resolve(env.cwd, item.path), size = await env.fileSize(path);
|
|
5935
|
+
if (size === null) throw new ChatError(400, `No file at ${path} in your environment`);
|
|
5936
|
+
if (!size || size > MAX_FILE) throw new ChatError(400, "Invalid attachment size or type");
|
|
5937
|
+
const data = await env.readFile(path);
|
|
5938
|
+
if (data.length !== size) throw new ChatError(409, "Attachment changed while reading");
|
|
5939
|
+
files.push({ name: item.name, mimeType: "application/octet-stream", data });
|
|
5940
|
+
}
|
|
5941
|
+
return files;
|
|
4896
5942
|
}
|
|
4897
5943
|
location() {
|
|
4898
5944
|
return { type: "local", cwd: this.local.cwd, tempDir: this.local.tempDir };
|
|
4899
5945
|
}
|
|
4900
5946
|
};
|
|
5947
|
+
function plain(name2) {
|
|
5948
|
+
const cleaned = name2.replace(/[/\\\0]/g, "_").trim();
|
|
5949
|
+
return cleaned && cleaned !== "." && cleaned !== ".." ? cleaned : "file";
|
|
5950
|
+
}
|
|
4901
5951
|
|
|
4902
5952
|
// src/server/service/bot/state.ts
|
|
4903
5953
|
var BotState = class {
|
|
@@ -4916,7 +5966,7 @@ var BotState = class {
|
|
|
4916
5966
|
};
|
|
4917
5967
|
|
|
4918
5968
|
// src/server/service/bot/tools.ts
|
|
4919
|
-
import { Type as
|
|
5969
|
+
import { Type as Type5 } from "typebox";
|
|
4920
5970
|
import { Value } from "typebox/value";
|
|
4921
5971
|
|
|
4922
5972
|
// src/server/service/bot/history-read.ts
|
|
@@ -4965,20 +6015,20 @@ ${content(block.result.content, heading + "#")}`;
|
|
|
4965
6015
|
function formatHistory(result, format) {
|
|
4966
6016
|
if (format === "json") return JSON.stringify(result, null, 2);
|
|
4967
6017
|
const sections = [`# History (${result.mode})`];
|
|
4968
|
-
for (const
|
|
4969
|
-
sections.push(`## #${
|
|
4970
|
-
if (
|
|
6018
|
+
for (const record3 of result.records) {
|
|
6019
|
+
sections.push(`## #${record3.id} \xB7 ${record3.role}`, `Time: ${new Date(record3.timestamp).toISOString()}`);
|
|
6020
|
+
if (record3.model) sections.push(`Model:
|
|
4971
6021
|
|
|
4972
|
-
${jsonBlock(
|
|
4973
|
-
if ("preview" in
|
|
4974
|
-
sections.push(fence(
|
|
6022
|
+
${jsonBlock(record3.model)}`);
|
|
6023
|
+
if ("preview" in record3) {
|
|
6024
|
+
sections.push(fence(record3.preview));
|
|
4975
6025
|
continue;
|
|
4976
6026
|
}
|
|
4977
|
-
sections.push(content(
|
|
4978
|
-
if (
|
|
4979
|
-
if (
|
|
4980
|
-
if (
|
|
4981
|
-
if (
|
|
6027
|
+
sections.push(content(record3.message.content));
|
|
6028
|
+
if (record3.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record3.message.tools));
|
|
6029
|
+
if (record3.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record3.message.tools_removed));
|
|
6030
|
+
if (record3.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record3.message.stop_reason));
|
|
6031
|
+
if (record3.message.error !== void 0) sections.push("### Error", fence(record3.message.error));
|
|
4982
6032
|
}
|
|
4983
6033
|
if (result.missingIds.length) sections.push(`## Missing IDs
|
|
4984
6034
|
|
|
@@ -5004,6 +6054,38 @@ async function readHistory(memory, workspace, input, signal) {
|
|
|
5004
6054
|
return workspace.writeTempFile(name2, rendered, signal);
|
|
5005
6055
|
}
|
|
5006
6056
|
|
|
6057
|
+
// src/server/service/bot/file-view.ts
|
|
6058
|
+
var TEXT_LIMIT = 64 * 1024;
|
|
6059
|
+
var PICTURE_LIMIT = 4 * 1024 * 1024;
|
|
6060
|
+
function viewFile(file, data) {
|
|
6061
|
+
const head = `${file.name} (${file.mimeType || "application/octet-stream"}, ${byteSize(file.size)})`;
|
|
6062
|
+
const picture = pictureType(data);
|
|
6063
|
+
if (picture) {
|
|
6064
|
+
if (data.length > PICTURE_LIMIT) throw new ChatError(400, "This picture is over 4 MB; use files.get and look at a smaller copy");
|
|
6065
|
+
return { kind: "media", text: head, images: [{ data: data.toString("base64"), mimeType: picture }] };
|
|
6066
|
+
}
|
|
6067
|
+
const text3 = data.length <= TEXT_LIMIT ? asText(data) : null;
|
|
6068
|
+
if (text3 !== null) return `${head}
|
|
6069
|
+
|
|
6070
|
+
${text3}`;
|
|
6071
|
+
throw new ChatError(400, "files.view shows pictures and text files up to 64 KB; use files.get to work on this file with your tools");
|
|
6072
|
+
}
|
|
6073
|
+
function pictureType(data) {
|
|
6074
|
+
if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
|
|
6075
|
+
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
6076
|
+
if (data.length >= 6 && /^GIF8[79]a$/.test(data.subarray(0, 6).toString("latin1"))) return "image/gif";
|
|
6077
|
+
if (data.length >= 12 && data.subarray(0, 4).toString("latin1") === "RIFF" && data.subarray(8, 12).toString("latin1") === "WEBP") return "image/webp";
|
|
6078
|
+
return null;
|
|
6079
|
+
}
|
|
6080
|
+
function asText(data) {
|
|
6081
|
+
if (data.includes(0)) return null;
|
|
6082
|
+
try {
|
|
6083
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
6084
|
+
} catch {
|
|
6085
|
+
return null;
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
|
|
5007
6089
|
// src/server/service/bot/chat-views.ts
|
|
5008
6090
|
var iso = (time) => new Date(time).toISOString();
|
|
5009
6091
|
var name = (chats) => (memberId) => chats.member(memberId)?.name ?? memberId;
|
|
@@ -5114,6 +6196,7 @@ var ChatTools = class {
|
|
|
5114
6196
|
if (missing.length) throw new ChatError(404, `No member named: ${missing.join(", ")} (see members.list)`);
|
|
5115
6197
|
return found.map((item) => item.member);
|
|
5116
6198
|
}
|
|
6199
|
+
/** A channel group by ID or name. */
|
|
5117
6200
|
section(value2) {
|
|
5118
6201
|
const sections = this.chats.channels.sections(this.bot);
|
|
5119
6202
|
const matches = sections.filter((section) => section.id === value2 || same(value2, section.name));
|
|
@@ -5228,7 +6311,8 @@ var ChatTools = class {
|
|
|
5228
6311
|
return { messages: await this.rows(page, signal), ...messages.length > limit ? { hasMore: true } : {} };
|
|
5229
6312
|
}
|
|
5230
6313
|
async rows(messages, signal) {
|
|
5231
|
-
|
|
6314
|
+
signal?.throwIfAborted();
|
|
6315
|
+
return messages.map((message) => messageRow(this.chats, this.bot, message, listing(message)));
|
|
5232
6316
|
}
|
|
5233
6317
|
search(input) {
|
|
5234
6318
|
const found = input.chatId === void 0 ? this.chats.searchAll(this.bot, input.query, input.limit ?? 30) : this.chats.search(this.bot, this.chat(input.chatId).id, input.query, input.limit ?? 30);
|
|
@@ -5237,13 +6321,7 @@ var ChatTools = class {
|
|
|
5237
6321
|
if (!chats.has(id)) chats.set(id, this.chats.get(this.bot, id));
|
|
5238
6322
|
return chats.get(id);
|
|
5239
6323
|
};
|
|
5240
|
-
return found.map((message) => messageRow(
|
|
5241
|
-
this.chats,
|
|
5242
|
-
this.bot,
|
|
5243
|
-
message,
|
|
5244
|
-
message.attachments.map((file) => ({ name: file.name })),
|
|
5245
|
-
input.chatId === void 0 ? chatOf(message.chatId) : null
|
|
5246
|
-
));
|
|
6324
|
+
return found.map((message) => messageRow(this.chats, this.bot, message, listing(message), input.chatId === void 0 ? chatOf(message.chatId) : null));
|
|
5247
6325
|
}
|
|
5248
6326
|
};
|
|
5249
6327
|
|
|
@@ -5252,6 +6330,7 @@ import { Type as Type3 } from "typebox";
|
|
|
5252
6330
|
var None = Type3.Object({}, { additionalProperties: false });
|
|
5253
6331
|
var Name = (description) => Type3.String({ minLength: 1, maxLength: 80, description });
|
|
5254
6332
|
var MessageId = Type3.String({ pattern: "^m\\d+$" });
|
|
6333
|
+
var FileId = Type3.String({ minLength: 1, maxLength: 80, description: "The file's id: from <file id=\u2026/> in a message, or the attachments of messages.read and messages.search" });
|
|
5255
6334
|
var QuestionOption = Type3.Object({
|
|
5256
6335
|
label: Type3.String({ minLength: 1, maxLength: 80, description: "Text shown on the option" }),
|
|
5257
6336
|
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" })),
|
|
@@ -5261,7 +6340,10 @@ var QuestionOption = Type3.Object({
|
|
|
5261
6340
|
var definitions = {
|
|
5262
6341
|
"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
6342
|
"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": {
|
|
6343
|
+
"bots.list": {
|
|
6344
|
+
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; agent=true is the agent Bot, the owner's helper for running Jarvy: ask it to create or change a Bot, give a Bot a model, see a Bot's usage, or delete a Bot, channel or channel group; 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. With environments=true, each Bot also has environment: its default work environment, the one it is in now (current) and the ones it may use. You come first, then the rest by name.",
|
|
6345
|
+
schema: Type3.Object({ environments: Type3.Optional(Type3.Boolean({ description: "Also give each Bot's work environments" })) }, { additionalProperties: false })
|
|
6346
|
+
},
|
|
5265
6347
|
"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 },
|
|
5266
6348
|
"chats.info": {
|
|
5267
6349
|
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.",
|
|
@@ -5289,7 +6371,7 @@ var definitions = {
|
|
|
5289
6371
|
}, { additionalProperties: false })
|
|
5290
6372
|
},
|
|
5291
6373
|
"chats.addMembers": {
|
|
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.",
|
|
6374
|
+
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, and the agent Bot (see bots.list) once the owner confirms.",
|
|
5293
6375
|
schema: Type3.Object({ chatId: Type3.String(), members: Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }) }, { additionalProperties: false })
|
|
5294
6376
|
},
|
|
5295
6377
|
"chats.subscribe": {
|
|
@@ -5311,7 +6393,7 @@ var definitions = {
|
|
|
5311
6393
|
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("New name") }, { additionalProperties: false })
|
|
5312
6394
|
},
|
|
5313
6395
|
"messages.read": {
|
|
5314
|
-
description: "Read messages
|
|
6396
|
+
description: "Read messages; their files are listed by id, name, type and size, for files.get and files.view. 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.",
|
|
5315
6397
|
schema: Type3.Object({
|
|
5316
6398
|
chatId: Type3.Optional(Type3.String()),
|
|
5317
6399
|
after: Type3.Optional(Type3.Integer({ minimum: 0 })),
|
|
@@ -5322,9 +6404,22 @@ var definitions = {
|
|
|
5322
6404
|
}, { additionalProperties: false })
|
|
5323
6405
|
},
|
|
5324
6406
|
"messages.search": {
|
|
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.
|
|
6407
|
+
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. Results list their files like messages.read.",
|
|
5326
6408
|
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 })
|
|
5327
6409
|
},
|
|
6410
|
+
"files.get": {
|
|
6411
|
+
description: "Put a file from a message into your environment, to work on it with your tools; the result has its path there. It goes to temp/files/ under its own name unless you give a path.",
|
|
6412
|
+
schema: Type3.Object({ id: FileId, path: Type3.Optional(Type3.String({ minLength: 1, maxLength: 1e3, description: "Where to put it instead: absolute, or relative to your directory" })) }, { additionalProperties: false })
|
|
6413
|
+
},
|
|
6414
|
+
"environments.list": { description: "List the work environments you may use: your sandbox, your default and the ones your owner granted you. current is the id of the one you are in. Each has its id, name, the directory your file and command tools start in there, default=true for your default, and state for a sandbox (it starts when you switch to it).", schema: None },
|
|
6415
|
+
"environments.switch": {
|
|
6416
|
+
description: "Move your file and command tools (bash, read, write, edit) to another work environment from environments.list, like cd one level up: you stay there, across turns, until you switch back. The result has your directory there and what you can do there. Commands already running finish where they started.",
|
|
6417
|
+
schema: Type3.Object({ id: Type3.String({ minLength: 1, maxLength: 80, description: "The environment's id from environments.list" }) }, { additionalProperties: false })
|
|
6418
|
+
},
|
|
6419
|
+
"files.view": {
|
|
6420
|
+
description: "Look at a file from a message without putting it anywhere: a picture (PNG, JPEG, GIF or WebP, up to 4 MB) is shown to you, and a text file up to 64 KB comes back as text. Anything else needs files.get.",
|
|
6421
|
+
schema: Type3.Object({ id: FileId }, { additionalProperties: false })
|
|
6422
|
+
},
|
|
5328
6423
|
"messages.react": {
|
|
5329
6424
|
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
6425
|
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 })
|
|
@@ -5341,7 +6436,7 @@ var definitions = {
|
|
|
5341
6436
|
}, { additionalProperties: false })
|
|
5342
6437
|
},
|
|
5343
6438
|
"messages.send": {
|
|
5344
|
-
description: "Send a message, with files from
|
|
6439
|
+
description: "Send a message, with files from your environment if you like (path there, absolute or relative to your directory, and the name people see). 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.",
|
|
5345
6440
|
schema: Type3.Object({
|
|
5346
6441
|
chatId: Type3.Optional(Type3.String()),
|
|
5347
6442
|
to: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
@@ -5355,7 +6450,7 @@ var definitions = {
|
|
|
5355
6450
|
};
|
|
5356
6451
|
|
|
5357
6452
|
// src/server/service/bot/roster.ts
|
|
5358
|
-
function botRows(chats, viewerId, entries) {
|
|
6453
|
+
function botRows(chats, viewerId, entries, environment) {
|
|
5359
6454
|
const rows2 = entries.map((entry) => {
|
|
5360
6455
|
const running = entry.status.status === "running";
|
|
5361
6456
|
const visible = [];
|
|
@@ -5368,30 +6463,194 @@ function botRows(chats, viewerId, entries) {
|
|
|
5368
6463
|
return { self: entry.id === viewerId, row: {
|
|
5369
6464
|
name: entry.name,
|
|
5370
6465
|
...entry.id === viewerId ? { self: true } : {},
|
|
6466
|
+
...entry.agent ? { agent: true } : {},
|
|
5371
6467
|
status: entry.status.status,
|
|
5372
6468
|
...running && entry.status.activity ? { activity: entry.status.activity } : {},
|
|
5373
6469
|
...visible.length ? { chats: visible } : {},
|
|
5374
6470
|
...hidden ? { otherChats: hidden } : {},
|
|
5375
|
-
...entry.errorCode ? { error: entry.errorCode } : {}
|
|
6471
|
+
...entry.errorCode ? { error: entry.errorCode } : {},
|
|
6472
|
+
...environment ? { environment: environment(entry.id) } : {}
|
|
5376
6473
|
} };
|
|
5377
6474
|
});
|
|
5378
6475
|
rows2.sort((a, b) => Number(b.self) - Number(a.self) || a.row.name.localeCompare(b.row.name));
|
|
5379
6476
|
return rows2.map((item) => item.row);
|
|
5380
6477
|
}
|
|
5381
6478
|
|
|
6479
|
+
// src/server/service/bot/agent-tools.ts
|
|
6480
|
+
var AgentTools = class {
|
|
6481
|
+
constructor(scope) {
|
|
6482
|
+
this.scope = scope;
|
|
6483
|
+
}
|
|
6484
|
+
get ops() {
|
|
6485
|
+
return this.scope.ops;
|
|
6486
|
+
}
|
|
6487
|
+
async confirmed(input, ask) {
|
|
6488
|
+
const chat = this.scope.tools.chat(input.askIn);
|
|
6489
|
+
if (!chat.memberIds.includes(this.ops.owner)) throw new ChatError(400, "The owner is not in askIn; use a chat they are in, such as your DM with them");
|
|
6490
|
+
const answer = await ask(chat.id, {
|
|
6491
|
+
question: input.question,
|
|
6492
|
+
multiple: false,
|
|
6493
|
+
allowCustom: false,
|
|
6494
|
+
options: [{ label: input.confirm, value: "confirm", style: "danger" }, { label: input.cancel, value: "cancel" }]
|
|
6495
|
+
});
|
|
6496
|
+
if (answer.status === "dismissed") return { deleted: false, answer: "dismissed" };
|
|
6497
|
+
return answer.selected[0]?.value === "confirm" ? null : { deleted: false, answer: "cancelled" };
|
|
6498
|
+
}
|
|
6499
|
+
channel(chatId) {
|
|
6500
|
+
const chat = this.scope.tools.chat(chatId);
|
|
6501
|
+
if (chat.type !== "channel") throw new ChatError(400, "Not a channel");
|
|
6502
|
+
return chat;
|
|
6503
|
+
}
|
|
6504
|
+
async call(name2, args, ask) {
|
|
6505
|
+
switch (name2) {
|
|
6506
|
+
case "models.list":
|
|
6507
|
+
return this.ops.models();
|
|
6508
|
+
case "bots.create": {
|
|
6509
|
+
const { environment, ...settings } = args;
|
|
6510
|
+
const bot = await this.ops.create({ ...settings, ...environment ? { environment: environment === "sandbox" ? "computer" : "host" } : {} });
|
|
6511
|
+
return { name: bot.name, model: bot.connectionId ? this.ops.modelLabel(bot) : null, thinking: bot.thinkingLevel, environment: this.ops.environment(bot.id) === "computer" ? "sandbox" : "host" };
|
|
6512
|
+
}
|
|
6513
|
+
case "bots.update": {
|
|
6514
|
+
const { bot, ...settings } = args;
|
|
6515
|
+
const result = await this.ops.update(this.ops.bot(bot), settings);
|
|
6516
|
+
return { name: result.bot.name, changed: result.changed };
|
|
6517
|
+
}
|
|
6518
|
+
case "bots.start":
|
|
6519
|
+
case "bots.stop": {
|
|
6520
|
+
const bot = this.ops.bot(args.bot);
|
|
6521
|
+
return { name: bot.name, status: await this.ops.run(bot, name2 === "bots.start" ? "start" : "stop") };
|
|
6522
|
+
}
|
|
6523
|
+
case "bots.usage": {
|
|
6524
|
+
const input = args, bot = this.ops.bot(input.bot);
|
|
6525
|
+
return { name: bot.name, ...this.ops.usage(bot, input.days ?? 7) };
|
|
6526
|
+
}
|
|
6527
|
+
case "bots.delete": {
|
|
6528
|
+
const input = args, bot = this.ops.bot(input.bot);
|
|
6529
|
+
if (bot.id === this.ops.agentId) throw new ChatError(400, "You can't delete yourself");
|
|
6530
|
+
const declined = await this.confirmed(input, ask);
|
|
6531
|
+
if (declined) return declined;
|
|
6532
|
+
await this.ops.deleteBot(this.ops.bot(input.bot));
|
|
6533
|
+
return { deleted: true, name: bot.name };
|
|
6534
|
+
}
|
|
6535
|
+
case "chats.delete": {
|
|
6536
|
+
const input = args, chat = this.channel(input.chatId);
|
|
6537
|
+
const declined = await this.confirmed(input, ask);
|
|
6538
|
+
if (declined) return declined;
|
|
6539
|
+
await this.ops.deleteChannel(chat.id);
|
|
6540
|
+
return { deleted: true, name: chat.title };
|
|
6541
|
+
}
|
|
6542
|
+
case "chats.removeMembers": {
|
|
6543
|
+
const input = args, chat = this.channel(input.chatId);
|
|
6544
|
+
const members = this.scope.tools.members(input.members).filter((member) => chat.memberIds.includes(member.id));
|
|
6545
|
+
if (!members.length) throw new ChatError(400, "None of them is in this channel");
|
|
6546
|
+
const declined = await this.confirmed(input, ask);
|
|
6547
|
+
if (declined) return { removed: [], answer: declined.answer };
|
|
6548
|
+
for (const member of members) this.ops.removeMember(chat.id, member.id);
|
|
6549
|
+
return { removed: members.map((member) => member.name) };
|
|
6550
|
+
}
|
|
6551
|
+
case "sections.delete": {
|
|
6552
|
+
const input = args, section = this.scope.tools.section(input.section);
|
|
6553
|
+
const declined = await this.confirmed(input, ask);
|
|
6554
|
+
if (declined) return declined;
|
|
6555
|
+
this.ops.deleteSection(section);
|
|
6556
|
+
return { deleted: true };
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
};
|
|
6561
|
+
|
|
6562
|
+
// src/server/service/bot/agent-schemas.ts
|
|
6563
|
+
import { Type as Type4 } from "typebox";
|
|
6564
|
+
var None2 = Type4.Object({}, { additionalProperties: false });
|
|
6565
|
+
var Name2 = Type4.String({ minLength: 1, maxLength: 80, description: "Unique among all members, people and Bots" });
|
|
6566
|
+
var BotName = Type4.String({ minLength: 1, maxLength: 80, description: "The Bot's name, as bots.list shows it" });
|
|
6567
|
+
var Model2 = Type4.Object({
|
|
6568
|
+
connection: Type4.String({ minLength: 1, maxLength: 200, description: "A connection's name or ID from models.list" }),
|
|
6569
|
+
model: Type4.String({ minLength: 1, maxLength: 200, description: "A model's ID or name in that connection" })
|
|
6570
|
+
}, { additionalProperties: false });
|
|
6571
|
+
var Thinking = Type4.Union(
|
|
6572
|
+
["auto", "off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => Type4.Literal(level)),
|
|
6573
|
+
{ description: "How much the model thinks; auto lets Jarvy choose. models.list gives the levels each model has" }
|
|
6574
|
+
);
|
|
6575
|
+
var Settings = {
|
|
6576
|
+
description: Type4.Optional(Type4.String({ maxLength: 1e3, description: "One line that other members see in members.list" })),
|
|
6577
|
+
persona: Type4.Optional(Type4.String({ maxLength: 16e3, description: "Its standing instructions: who it is and how it works (the Persona section of its prompt)" })),
|
|
6578
|
+
model: Type4.Optional(Model2),
|
|
6579
|
+
thinking: Type4.Optional(Thinking)
|
|
6580
|
+
};
|
|
6581
|
+
var Environment = Type4.Union([Type4.Literal("sandbox"), Type4.Literal("host")], {
|
|
6582
|
+
description: "Where its file and command tools run by default: sandbox, a Linux container that Jarvy makes and only it uses (the default when Docker is available), or host, the owner's computer, where you work too"
|
|
6583
|
+
});
|
|
6584
|
+
var Confirm = {
|
|
6585
|
+
askIn: Type4.String({ description: "ID of a chat the owner is in, where the confirmation card goes; usually the chat you were asked in" }),
|
|
6586
|
+
question: Type4.String({ minLength: 1, maxLength: 500, description: "The card's question, in the owner's language: what will be deleted and what goes with it" }),
|
|
6587
|
+
confirm: Type4.String({ minLength: 1, maxLength: 40, description: "Label of the button that deletes, in the owner's language" }),
|
|
6588
|
+
cancel: Type4.String({ minLength: 1, maxLength: 40, description: "Label of the button that keeps everything, in the owner's language" })
|
|
6589
|
+
};
|
|
6590
|
+
var after = "The owner first gets a card in askIn, and nothing is deleted unless they confirm; the result says what they chose.";
|
|
6591
|
+
var agentDefinitions = {
|
|
6592
|
+
"models.list": { description: "List the models you can give a Bot: each connection with its models and their thinking levels. Only the owner adds connections, keys and logins, in Settings.", schema: None2 },
|
|
6593
|
+
"bots.create": {
|
|
6594
|
+
description: "Create a Bot. Give it a model so it can work; without one it waits until it gets one. It starts when a message first arrives for it. bots.update changes everything later.",
|
|
6595
|
+
schema: Type4.Object({ name: Name2, ...Settings, environment: Type4.Optional(Environment) }, { additionalProperties: false })
|
|
6596
|
+
},
|
|
6597
|
+
"bots.update": {
|
|
6598
|
+
description: "Change a Bot's name, description, persona, model or thinking; give only what changes. Works for any Bot, yourself included. The Bot is told what changed with its next batch, in its DM with you.",
|
|
6599
|
+
schema: Type4.Object({ bot: BotName, name: Type4.Optional(Name2), ...Settings }, { additionalProperties: false })
|
|
6600
|
+
},
|
|
6601
|
+
"bots.start": { description: "Start a stopped Bot. A stopped Bot also starts by itself when a message arrives for it.", schema: Type4.Object({ bot: BotName }, { additionalProperties: false }) },
|
|
6602
|
+
"bots.stop": { description: "Stop a Bot (not yourself): its current turn ends, and it stays stopped until it is started or a message arrives for it.", schema: Type4.Object({ bot: BotName }, { additionalProperties: false }) },
|
|
6603
|
+
"bots.usage": {
|
|
6604
|
+
description: "A Bot's token use and cost over the last days (7 by default), per model.",
|
|
6605
|
+
schema: Type4.Object({ bot: BotName, days: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 365 })) }, { additionalProperties: false })
|
|
6606
|
+
},
|
|
6607
|
+
"bots.delete": {
|
|
6608
|
+
description: `Delete a Bot (not yourself). ${after} Its DMs and channel memberships go; its messages in channels and its workspace stay.`,
|
|
6609
|
+
schema: Type4.Object({ bot: BotName, ...Confirm }, { additionalProperties: false })
|
|
6610
|
+
},
|
|
6611
|
+
"chats.delete": {
|
|
6612
|
+
description: `Delete a channel you are in, with its threads, messages and files. ${after}`,
|
|
6613
|
+
schema: Type4.Object({ chatId: Type4.String({ description: "Channel ID" }), ...Confirm }, { additionalProperties: false })
|
|
6614
|
+
},
|
|
6615
|
+
"chats.removeMembers": {
|
|
6616
|
+
description: `Remove members from a channel you are in, by name (see members.list). ${after}`,
|
|
6617
|
+
schema: Type4.Object({ chatId: Type4.String({ description: "Channel ID" }), members: Type4.Array(Type4.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }), ...Confirm }, { additionalProperties: false })
|
|
6618
|
+
},
|
|
6619
|
+
"sections.delete": {
|
|
6620
|
+
description: `Delete a channel group; its channels stay, outside any group. ${after}`,
|
|
6621
|
+
schema: Type4.Object({ section: Type4.String({ minLength: 1, maxLength: 80, description: "The group's name or ID (see sections.list)" }), ...Confirm }, { additionalProperties: false })
|
|
6622
|
+
}
|
|
6623
|
+
};
|
|
6624
|
+
|
|
5382
6625
|
// src/server/service/bot/tools.ts
|
|
5383
|
-
var Gateway =
|
|
5384
|
-
action:
|
|
5385
|
-
tool:
|
|
5386
|
-
args:
|
|
6626
|
+
var Gateway = Type5.Object({
|
|
6627
|
+
action: Type5.Union([Type5.Literal("list"), Type5.Literal("describe"), Type5.Literal("call")]),
|
|
6628
|
+
tool: Type5.Optional(Type5.String()),
|
|
6629
|
+
args: Type5.Optional(Type5.Record(Type5.String(), Type5.Unknown()))
|
|
5387
6630
|
}, { additionalProperties: false });
|
|
5388
6631
|
var Tools = class {
|
|
5389
6632
|
constructor(scope) {
|
|
5390
6633
|
this.scope = scope;
|
|
5391
6634
|
this.tools = new ChatTools(scope);
|
|
6635
|
+
if (scope.agent) this.agent = new AgentTools({ ops: scope.agent, tools: this.tools });
|
|
5392
6636
|
}
|
|
5393
6637
|
send;
|
|
5394
6638
|
tools;
|
|
6639
|
+
/** The agent Bot's own tools (A48); other Bots have none. */
|
|
6640
|
+
agent;
|
|
6641
|
+
get catalog() {
|
|
6642
|
+
return this.agent ? { ...definitions, ...agentDefinitions } : definitions;
|
|
6643
|
+
}
|
|
6644
|
+
/** Posts a card and waits for the person's answer (messages.ask, and the agent's confirmations). */
|
|
6645
|
+
async ask(chatId, question, toolCallId, signal) {
|
|
6646
|
+
const sent = await this.send(chatId, { content: question.question, question, toolCallId, signal });
|
|
6647
|
+
this.scope.asking?.(true);
|
|
6648
|
+
try {
|
|
6649
|
+
return { messageId: sent.id, ...await this.scope.chats.questions.wait(this.scope.chats.message(this.scope.botId, sent.id), signal) };
|
|
6650
|
+
} finally {
|
|
6651
|
+
if (!signal?.aborted) this.scope.asking?.(false);
|
|
6652
|
+
}
|
|
6653
|
+
}
|
|
5395
6654
|
bindSend(send) {
|
|
5396
6655
|
this.send = send;
|
|
5397
6656
|
}
|
|
@@ -5405,16 +6664,18 @@ var Tools = class {
|
|
|
5405
6664
|
signal?.throwIfAborted();
|
|
5406
6665
|
if (raw.action === "list") {
|
|
5407
6666
|
if (raw.tool !== void 0 || raw.args !== void 0) throw new ChatError(400, "list does not accept tool or args");
|
|
5408
|
-
return Object.entries(
|
|
6667
|
+
return Object.entries(this.catalog).map(([name3, tool]) => ({ name: name3, description: tool.description }));
|
|
5409
6668
|
}
|
|
5410
|
-
if (!raw.tool || !Object.hasOwn(
|
|
5411
|
-
const name2 = raw.tool, definition =
|
|
6669
|
+
if (!raw.tool || !Object.hasOwn(this.catalog, raw.tool)) throw new ChatError(400, "Unknown chat tool; use list first");
|
|
6670
|
+
const name2 = raw.tool, definition = this.catalog[name2];
|
|
5412
6671
|
if (raw.action === "describe") {
|
|
5413
6672
|
if (raw.args !== void 0) throw new ChatError(400, "describe does not accept args");
|
|
5414
6673
|
return { name: name2, description: definition.description, parameters: definition.schema };
|
|
5415
6674
|
}
|
|
5416
6675
|
const args = raw.args;
|
|
5417
6676
|
if (!args || !Value.Check(definition.schema, args)) throw new ChatError(400, "Invalid tool arguments; use describe for the parameter schema");
|
|
6677
|
+
if (this.agent && Object.hasOwn(agentDefinitions, name2))
|
|
6678
|
+
return this.agent.call(name2, args, (chatId, question) => this.ask(chatId, question, toolCallId, signal));
|
|
5418
6679
|
switch (name2) {
|
|
5419
6680
|
case "history.read":
|
|
5420
6681
|
return readHistory(this.scope.memory, this.scope.workspace, args, signal);
|
|
@@ -5425,7 +6686,16 @@ var Tools = class {
|
|
|
5425
6686
|
case "members.list":
|
|
5426
6687
|
return this.tools.membersList();
|
|
5427
6688
|
case "bots.list":
|
|
5428
|
-
return botRows(
|
|
6689
|
+
return botRows(
|
|
6690
|
+
this.scope.chats,
|
|
6691
|
+
this.scope.botId,
|
|
6692
|
+
this.scope.roster(),
|
|
6693
|
+
args.environments ? (id) => this.scope.places.summary(id) : void 0
|
|
6694
|
+
);
|
|
6695
|
+
case "environments.list":
|
|
6696
|
+
return this.scope.places.list();
|
|
6697
|
+
case "environments.switch":
|
|
6698
|
+
return this.scope.places.switch(args.id);
|
|
5429
6699
|
case "chats.create":
|
|
5430
6700
|
return this.tools.create(args);
|
|
5431
6701
|
case "chats.update":
|
|
@@ -5459,6 +6729,16 @@ var Tools = class {
|
|
|
5459
6729
|
}
|
|
5460
6730
|
case "messages.read":
|
|
5461
6731
|
return this.tools.read(args, signal);
|
|
6732
|
+
case "files.get": {
|
|
6733
|
+
const { id, path } = args;
|
|
6734
|
+
const { file, data } = this.scope.files.open(this.scope.botId, id);
|
|
6735
|
+
signal?.throwIfAborted();
|
|
6736
|
+
return { path: await this.scope.workspace.put(file.name, id, data, path), name: file.name, type: file.mimeType, size: byteSize(file.size) };
|
|
6737
|
+
}
|
|
6738
|
+
case "files.view": {
|
|
6739
|
+
const { file, data } = this.scope.files.open(this.scope.botId, args.id);
|
|
6740
|
+
return viewFile(file, data);
|
|
6741
|
+
}
|
|
5462
6742
|
case "messages.search":
|
|
5463
6743
|
return this.tools.search(args);
|
|
5464
6744
|
case "messages.send": {
|
|
@@ -5504,13 +6784,7 @@ var Tools = class {
|
|
|
5504
6784
|
multiple: parsed.multiple === true,
|
|
5505
6785
|
allowCustom: parsed.allowCustom === true
|
|
5506
6786
|
};
|
|
5507
|
-
|
|
5508
|
-
this.scope.asking?.(true);
|
|
5509
|
-
try {
|
|
5510
|
-
return { messageId: sent.id, ...await this.scope.chats.questions.wait(this.scope.chats.message(this.scope.botId, sent.id), signal) };
|
|
5511
|
-
} finally {
|
|
5512
|
-
if (!signal?.aborted) this.scope.asking?.(false);
|
|
5513
|
-
}
|
|
6787
|
+
return this.ask(parsed.chatId, question, toolCallId, signal);
|
|
5514
6788
|
}
|
|
5515
6789
|
}
|
|
5516
6790
|
}
|
|
@@ -5519,10 +6793,13 @@ var Tools = class {
|
|
|
5519
6793
|
};
|
|
5520
6794
|
|
|
5521
6795
|
// src/server/service/bot/inbox.ts
|
|
5522
|
-
import { join as
|
|
6796
|
+
import { join as join7, dirname as dirname6 } from "node:path";
|
|
6797
|
+
|
|
6798
|
+
// src/server/shared/agent.ts
|
|
6799
|
+
var WELCOME_LANGUAGES = { en: "English", zh: "Chinese" };
|
|
6800
|
+
var welcomeLanguage = (value2) => typeof value2 === "string" && Object.hasOwn(WELCOME_LANGUAGES, value2) ? value2 : "en";
|
|
5523
6801
|
|
|
5524
6802
|
// src/server/service/chat/envelope.ts
|
|
5525
|
-
import { isAbsolute } from "node:path";
|
|
5526
6803
|
var INVALID_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/u;
|
|
5527
6804
|
function escape(value2) {
|
|
5528
6805
|
if (INVALID_XML.test(value2)) throw new Error("Invalid XML character");
|
|
@@ -5537,23 +6814,29 @@ function remember(map, id, definition) {
|
|
|
5537
6814
|
if (old !== void 0 && old !== definition) throw new Error("Conflicting source metadata");
|
|
5538
6815
|
map.set(id, definition);
|
|
5539
6816
|
}
|
|
6817
|
+
var environmentLine = (place) => place ? `<environment${attribute("name", place.name)}${attribute("directory", place.directory)}/>` : "";
|
|
5540
6818
|
var iso2 = (time) => new Date(time).toISOString().replace(".000Z", "Z");
|
|
5541
|
-
|
|
6819
|
+
var welcome = (owner, language) => `${owner} has just set up Jarvy and picked your model. You are their agent Bot. In this DM, welcome them in ${WELCOME_LANGUAGES[language]}, the language of their interface: say in a few sentences what Jarvy is and how you can help, such as building a team of Bots for their work and running Jarvy for them. Then ask what they want to get done: a software project, writing, research or something else.`;
|
|
6820
|
+
function encodeNoticeBatch(botId, notices, environment) {
|
|
5542
6821
|
if (!notices.length) throw new Error("Empty notice batch");
|
|
5543
6822
|
const chats = /* @__PURE__ */ new Map(), members = /* @__PURE__ */ new Map();
|
|
5544
6823
|
const list = noticeTail(notices, chats, members);
|
|
5545
|
-
return `<notice_batch${attribute("bot", botId)}>${[...chats.values()].join("")}${[...members.values()].join("")}${list}</notice_batch>`;
|
|
6824
|
+
return `<notice_batch${attribute("bot", botId)}>${[...chats.values()].join("")}${[...members.values()].join("")}${list}${environmentLine(environment)}</notice_batch>`;
|
|
5546
6825
|
}
|
|
5547
6826
|
function noticeTail(notices, chats, members) {
|
|
5548
6827
|
for (const notice of notices) {
|
|
5549
|
-
if (!chats.has(notice.chat.id)) chats.set(notice.chat.id, `<chat${attribute("id", notice.chat.id)}${attribute("type", notice.chat.type)}${attribute("title", notice.chat.title)}/>`);
|
|
6828
|
+
if (!chats.has(notice.chat.id)) chats.set(notice.chat.id, `<chat${attribute("id", notice.chat.id)}${attribute("type", notice.chat.type)}${notice.chat.title ? attribute("title", notice.chat.title) : ""}/>`);
|
|
5550
6829
|
remember(members, notice.actor.id, `<member${attribute("id", notice.actor.id)}${attribute("name", notice.actor.name)}${attribute("kind", notice.actor.kind)}/>`);
|
|
5551
6830
|
}
|
|
5552
6831
|
const latest = /* @__PURE__ */ new Map();
|
|
5553
6832
|
for (const notice of notices) if (notice.kind === "note" && (latest.get(notice.chat.id)?.version ?? 0) < notice.version) latest.set(notice.chat.id, notice);
|
|
5554
6833
|
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))}/>`);
|
|
5555
6834
|
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>`]);
|
|
5556
|
-
|
|
6835
|
+
const changed = notices.filter((notice) => notice.kind === "change");
|
|
6836
|
+
const changes = changed.map((notice) => `<change${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("what", notice.what)}${attribute("at", iso2(notice.createdAt))}${notice.value === null ? "/>" : `>${escape(notice.value)}</change>`}`);
|
|
6837
|
+
const byAgent = changed.some((notice) => notice.actor.kind === "bot"), byOwner = changed.some((notice) => notice.actor.kind === "user");
|
|
6838
|
+
const onboard = notices.flatMap((notice) => notice.kind !== "onboard" ? [] : [`<onboard${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("at", iso2(notice.createdAt))}><note>${escape(welcome(notice.actor.name, notice.language))}</note></onboard>`]).slice(-1);
|
|
6839
|
+
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>` : "") + (changes.length ? `<changes>${changes.join("")}` + (byAgent ? "<note>The agent Bot changed your settings, and the changes already apply. A persona change is already in your instructions. If something is unclear, ask it in that DM.</note>" : "") + (byOwner ? "<note>Your owner changed where you work, and it already applies: environment is where your file and command tools run now, and access is what environments.switch can take you to. If you were in the middle of something, carry on where you are now.</note>" : "") + "</changes>" : "") + onboard.join("");
|
|
5557
6840
|
}
|
|
5558
6841
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5559
6842
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5564,32 +6847,33 @@ function encodeChatBatch(botId, messages, digest, notices) {
|
|
|
5564
6847
|
const { chat } = message;
|
|
5565
6848
|
remember(chats, chat.id, `<chat${attribute("id", chat.id)}${attribute("type", chat.type)}${chat.title ? attribute("title", chat.title) : ""}${chat.section ? attribute("section", chat.section) : ""}${chat.parent ? attribute("parent", chat.parent) : ""}${chat.root ? attribute("root", chat.root) : ""}${chat.status ? attribute("status", chat.status) : ""}/>`);
|
|
5566
6849
|
remember(members, message.sender.id, `<member${attribute("id", message.sender.id)}${attribute("name", message.sender.name)}${attribute("kind", message.sender.kind)}/>`);
|
|
5567
|
-
const
|
|
5568
|
-
const attributes = attribute("id", message.id) + attribute("chat", message.chat.id) + attribute("from", message.sender.id) + attribute("at",
|
|
6850
|
+
const at2 = new Date(message.createdAt).toISOString().replace(".000Z", "Z");
|
|
6851
|
+
const attributes = attribute("id", message.id) + attribute("chat", message.chat.id) + attribute("from", message.sender.id) + attribute("at", at2);
|
|
5569
6852
|
const reply = message.replyTo;
|
|
5570
6853
|
if (reply) remember(members, reply.sender.id, `<member${attribute("id", reply.sender.id)}${attribute("name", reply.sender.name)}${attribute("kind", reply.sender.kind)}/>`);
|
|
5571
6854
|
const quote = reply ? `<reply_to${attribute("message", reply.id)}${attribute("from", reply.sender.id)}>${escape(reply.text)}</reply_to>` : "";
|
|
5572
|
-
const files = message.attachments.map((file) => {
|
|
5573
|
-
if (!isAbsolute(file.path)) throw new Error("Attachment path must be absolute");
|
|
5574
|
-
return `<file${attribute("path", file.path)}${attribute("name", file.name)}/>`;
|
|
5575
|
-
}).join("");
|
|
6855
|
+
const files = message.attachments.map((file) => `<file${attribute("id", file.id)}${attribute("name", file.name)}${attribute("type", file.type)}${attribute("size", file.size)}/>`).join("");
|
|
5576
6856
|
return `<message${attributes}>${quote}${message.content ? `<text>${escape(message.content)}</text>` : ""}${files}</message>`;
|
|
5577
6857
|
});
|
|
5578
6858
|
const ref = (key, item) => attribute(key, item.chatId) + (chats.has(item.chatId) ? "" : attribute("title", item.title) + attribute("type", item.type));
|
|
5579
6859
|
let tail = notices?.length ? noticeTail(notices, chats, members) : "";
|
|
5580
6860
|
if (digest?.unread.length) tail += `<unread${attribute("total", String(digest.unread.reduce((sum, item) => sum + item.count, 0)))}>${digest.unread.map((item) => `<chat${ref("id", item)}${attribute("count", String(item.count))}/>`).join("")}</unread>`;
|
|
6861
|
+
tail += environmentLine(digest?.environment);
|
|
5581
6862
|
return `<chat_batch${attribute("bot", botId)}>${[...chats.values()].join("")}${[...members.values()].join("")}${encoded.join("")}${tail}</chat_batch>`;
|
|
5582
6863
|
}
|
|
5583
6864
|
|
|
5584
6865
|
// src/server/service/bot/inbox.ts
|
|
5585
6866
|
var BotInbox = class {
|
|
5586
|
-
|
|
6867
|
+
/** `where` is the work environment the Bot is in now, named at the end of each batch (A50). */
|
|
6868
|
+
constructor(botId, repository, chats, files, where2) {
|
|
5587
6869
|
this.botId = botId;
|
|
5588
6870
|
this.repository = repository;
|
|
5589
6871
|
this.chats = chats;
|
|
5590
6872
|
this.files = files;
|
|
6873
|
+
this.where = where2;
|
|
5591
6874
|
}
|
|
5592
|
-
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run.
|
|
6875
|
+
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run. A digest delivery
|
|
6876
|
+
* (a change the agent Bot made, A48) is not work of its own: it rides along with the next batch. */
|
|
5593
6877
|
pending() {
|
|
5594
6878
|
return this.repository.hasPending(this.botId) || this.hasNotices();
|
|
5595
6879
|
}
|
|
@@ -5597,11 +6881,11 @@ var BotInbox = class {
|
|
|
5597
6881
|
return this.repository.claim(this.botId, selection, this.hasNotices());
|
|
5598
6882
|
}
|
|
5599
6883
|
hasNotices() {
|
|
5600
|
-
return this.chats.notices(this.botId).
|
|
6884
|
+
return this.chats.notices(this.botId).some((notice) => notice.urgency !== "digest");
|
|
5601
6885
|
}
|
|
5602
6886
|
reference(workspace) {
|
|
5603
6887
|
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
5604
|
-
return this.repository.ensureSession(this.botId,
|
|
6888
|
+
return this.repository.ensureSession(this.botId, join7(dirname6(workspace), "agent", "pi", "sessions", `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`));
|
|
5605
6889
|
}
|
|
5606
6890
|
/** XML for the claimed messages plus the tail (other deliveries not told yet, unread); `notices` are told once
|
|
5607
6891
|
* the batch is submitted. */
|
|
@@ -5612,7 +6896,7 @@ var BotInbox = class {
|
|
|
5612
6896
|
}
|
|
5613
6897
|
const inputs = await this.files.envelope(this.botId, batch.inputs.map((input) => input.messageId));
|
|
5614
6898
|
for (const input of batch.inputs) if (!this.chats.get(this.botId, input.chatId)) throw new RuntimeFault("chat_access_changed", "Chat access has changed.");
|
|
5615
|
-
const digest = this.chats.channels.digest(this.botId);
|
|
6899
|
+
const digest = { ...this.chats.channels.digest(this.botId), ...this.where ? { environment: this.where() } : {} };
|
|
5616
6900
|
const pending = this.envelope(this.chats.notices(this.botId));
|
|
5617
6901
|
return { xml: encodeChatBatch(this.botId, inputs, digest, pending.length ? pending : void 0), notices: pending.map((notice) => notice.id) };
|
|
5618
6902
|
}
|
|
@@ -5621,6 +6905,11 @@ var BotInbox = class {
|
|
|
5621
6905
|
return notices.map((notice) => {
|
|
5622
6906
|
const chat = { id: notice.chatId, type: notice.chatType, title: notice.chatTitle };
|
|
5623
6907
|
if (notice.kind === "note") return { kind: "note", id: notice.id, chat, actor: notice.actor, version: Number(notice.detail), createdAt: notice.createdAt };
|
|
6908
|
+
if (notice.kind === "onboard") return { kind: "onboard", id: notice.id, chat, actor: notice.actor, language: welcomeLanguage(notice.detail), createdAt: notice.createdAt };
|
|
6909
|
+
if (notice.kind === "change") {
|
|
6910
|
+
const change = JSON.parse(notice.detail);
|
|
6911
|
+
return { kind: "change", id: notice.id, chat, actor: notice.actor, what: change.what, value: change.value, createdAt: notice.createdAt };
|
|
6912
|
+
}
|
|
5624
6913
|
const marked = notice.messageId ? this.chats.message(this.botId, notice.messageId) : null;
|
|
5625
6914
|
const text3 = marked ? previewQuotes(marked.content, marked.quotes).slice(0, 120) : "";
|
|
5626
6915
|
return { kind: "reaction", id: notice.id, chat, message: { id: notice.messageId ?? "", text: text3 }, actor: notice.actor, emoji: notice.detail, createdAt: notice.createdAt };
|
|
@@ -5634,7 +6923,7 @@ var BotInbox = class {
|
|
|
5634
6923
|
notices() {
|
|
5635
6924
|
const pending = this.chats.notices(this.botId);
|
|
5636
6925
|
if (!pending.length) return null;
|
|
5637
|
-
return { xml: encodeNoticeBatch(this.botId, this.envelope(pending)), ids: pending.map((notice) => notice.id) };
|
|
6926
|
+
return { xml: encodeNoticeBatch(this.botId, this.envelope(pending), this.where?.()), ids: pending.map((notice) => notice.id) };
|
|
5638
6927
|
}
|
|
5639
6928
|
/** These notices reached the model. */
|
|
5640
6929
|
tell(ids) {
|
|
@@ -5667,8 +6956,8 @@ var AgentRuntime = class {
|
|
|
5667
6956
|
return this.runId;
|
|
5668
6957
|
}
|
|
5669
6958
|
update(patch) {
|
|
5670
|
-
const
|
|
5671
|
-
this.status = { ...this.status, ...patch, context:
|
|
6959
|
+
const context2 = this.session?.context;
|
|
6960
|
+
this.status = { ...this.status, ...patch, context: context2 ? { tokens: context2.tokens, limit: context2.contextWindow } : null };
|
|
5672
6961
|
this.setup?.onStatus(this.status);
|
|
5673
6962
|
}
|
|
5674
6963
|
activity(phase) {
|
|
@@ -5679,9 +6968,9 @@ var AgentRuntime = class {
|
|
|
5679
6968
|
if (this.prompting) this.update({ status: "running", activity: on ? "asking" : "tool_calling" });
|
|
5680
6969
|
}
|
|
5681
6970
|
fail(error) {
|
|
5682
|
-
const
|
|
5683
|
-
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code:
|
|
5684
|
-
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code:
|
|
6971
|
+
const fault2 = publicFault(error);
|
|
6972
|
+
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault2.code, message: fault2.message, ...fault2.params ? { params: fault2.params } : {} } });
|
|
6973
|
+
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code: fault2.code, ...error instanceof RuntimeFault ? {} : { error } });
|
|
5685
6974
|
}
|
|
5686
6975
|
async start(setup) {
|
|
5687
6976
|
await this.operation(async () => {
|
|
@@ -5805,7 +7094,7 @@ var AgentRuntime = class {
|
|
|
5805
7094
|
return task;
|
|
5806
7095
|
}
|
|
5807
7096
|
// Model/thinking changes queue behind start/stop and each other but not behind the running turn:
|
|
5808
|
-
// Pi
|
|
7097
|
+
// Pi 1.2.0 re-reads agent.state.model/thinkingLevel before every next request (AgentSession's prepareNextTurnWithContext),
|
|
5809
7098
|
// and each request resolves its API key when it starts, so in-flight requests are unaffected.
|
|
5810
7099
|
reconfigure(operation) {
|
|
5811
7100
|
const task = this.operations.catch(() => {
|
|
@@ -5868,6 +7157,14 @@ var AgentRuntime = class {
|
|
|
5868
7157
|
};
|
|
5869
7158
|
|
|
5870
7159
|
// src/server/service/bot/prompt.ts
|
|
7160
|
+
var AGENT_ROLE = `
|
|
7161
|
+
## Your role
|
|
7162
|
+
You are the owner's agent Bot. Besides working like any Bot, you help them set up and run Jarvy itself.
|
|
7163
|
+
Help a new owner get started: find out what they want to get done, suggest a team of Bots for it, then create and set up those Bots.
|
|
7164
|
+
The chat gateway lists tools only you have: models.list, bots.create, bots.update, bots.start, bots.stop and bots.usage. bots.delete, chats.delete, chats.removeMembers and sections.delete first ask the owner on a card, and delete nothing unless they confirm.
|
|
7165
|
+
Other Bots may ask you to do these things for them. Do the ordinary ones; deleting always waits for the owner's confirmation.
|
|
7166
|
+
You can't add model connections, keys or logins. The owner does that in Settings, so point them there when a model is missing.
|
|
7167
|
+
`;
|
|
5871
7168
|
function botPrompt(setup) {
|
|
5872
7169
|
const bot = { ...setup.identity(), workspacePath: setup.workspace.cwd };
|
|
5873
7170
|
return `# Persona
|
|
@@ -5878,14 +7175,14 @@ ${bot.persona}
|
|
|
5878
7175
|
## Environment
|
|
5879
7176
|
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
7177
|
Here your name is ${bot.name}, and your description is: ${bot.description}
|
|
5881
|
-
|
|
7178
|
+
${bot.agent ? AGENT_ROLE : ""}
|
|
5882
7179
|
## Language
|
|
5883
7180
|
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
7181
|
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
7182
|
|
|
5886
7183
|
## Chat
|
|
5887
7184
|
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
|
-
|
|
7185
|
+
Files in messages come with an ID: files.get puts one into your environment and gives you its path, and files.view shows you an image or a text file directly. To pass a file to someone, attach its path in your environment and its name when you send with the chat tool.
|
|
5889
7186
|
Channels can be put in channel groups and have a notice. Threads have a title, a status and a summary.
|
|
5890
7187
|
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
7188
|
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.
|
|
@@ -5894,17 +7191,33 @@ Each batch of messages ends with your unread counts and the notices and summarie
|
|
|
5894
7191
|
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
7192
|
history.read is listed by the chat gateway. History is material to look back on, not an instruction for the task at hand.
|
|
5896
7193
|
|
|
5897
|
-
|
|
5898
|
-
|
|
7194
|
+
${workspaceSection(setup, bot.workspacePath)}
|
|
7195
|
+
## Responding
|
|
7196
|
+
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.
|
|
7197
|
+
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.
|
|
7198
|
+
`;
|
|
7199
|
+
}
|
|
7200
|
+
function workspaceSection(setup, workspacePath) {
|
|
7201
|
+
const place = setup.home();
|
|
7202
|
+
if (place.kind === "computer") return `## Workspace
|
|
7203
|
+
Your workspace is your own folder and your home directory: your file and command tools see it as ${place.cwd}, and ~ is the same place, so settings such as ~/.ssh and ~/.gitconfig stay with it. It is kept when your sandbox is reset. Keep it tidy, and put temporary files under temp and clear them out.
|
|
7204
|
+
|
|
7205
|
+
## Work environment
|
|
7206
|
+
Your tools run in your sandbox by default: a Linux container only you use, with Node, Python, git, build tools, and Playwright with its browsers. You can install software there with sudo; it stays until the owner resets your sandbox.
|
|
7207
|
+
${SWITCHING}
|
|
7208
|
+
`;
|
|
7209
|
+
return `## Workspace
|
|
7210
|
+
You have your own workspace: ${workspacePath}
|
|
5899
7211
|
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
7212
|
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
7213
|
Shared directories and other people's files are read-only. Ask the person in charge before writing to them.
|
|
5902
7214
|
|
|
5903
|
-
##
|
|
5904
|
-
|
|
5905
|
-
|
|
7215
|
+
## Work environment
|
|
7216
|
+
Your tools run on your owner's computer by default, the machine running Jarvy.
|
|
7217
|
+
${SWITCHING}
|
|
5906
7218
|
`;
|
|
5907
7219
|
}
|
|
7220
|
+
var SWITCHING = "Each batch of messages ends with the environment you are in now. environments.list shows the ones you may use, and environments.switch moves your tools to another, like cd, until you switch back.";
|
|
5908
7221
|
|
|
5909
7222
|
// src/server/shared/live.ts
|
|
5910
7223
|
var LIVE_STEPS = 300;
|
|
@@ -5918,9 +7231,9 @@ function clip(text3, length) {
|
|
|
5918
7231
|
return text3.length > length ? `${text3.slice(0, length - 1)}\u2026` : text3;
|
|
5919
7232
|
}
|
|
5920
7233
|
var field = (value2, length = LIVE_INPUT) => typeof value2 === "string" ? clip(value2, length) : void 0;
|
|
5921
|
-
var
|
|
7234
|
+
var record2 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
|
|
5922
7235
|
function liveInput(tool, args, chat) {
|
|
5923
|
-
const values =
|
|
7236
|
+
const values = record2(args);
|
|
5924
7237
|
switch (tool) {
|
|
5925
7238
|
case "bash":
|
|
5926
7239
|
return { command: field(values.command) };
|
|
@@ -5944,15 +7257,15 @@ function liveInput(tool, args, chat) {
|
|
|
5944
7257
|
}
|
|
5945
7258
|
function gateway(values, chat) {
|
|
5946
7259
|
const action = values.action === "call" && typeof values.tool === "string" ? values.tool : typeof values.action === "string" ? values.action : "";
|
|
5947
|
-
const inner =
|
|
5948
|
-
if (action === "messages.send") return { action, chat:
|
|
7260
|
+
const inner = record2(values.args), where2 = target(inner, chat);
|
|
7261
|
+
if (action === "messages.send") return { action, chat: where2, text: field(inner.message, LIVE_MESSAGE) };
|
|
5949
7262
|
if (action === "messages.ask") return {
|
|
5950
7263
|
action,
|
|
5951
|
-
chat:
|
|
7264
|
+
chat: where2,
|
|
5952
7265
|
text: field(inner.question, LIVE_MESSAGE),
|
|
5953
|
-
options: Array.isArray(inner.options) ? inner.options.map((option2) => field(
|
|
7266
|
+
options: Array.isArray(inner.options) ? inner.options.map((option2) => field(record2(option2).label) ?? "").filter(Boolean) : void 0
|
|
5954
7267
|
};
|
|
5955
|
-
return
|
|
7268
|
+
return where2 ? { action, chat: where2 } : { action };
|
|
5956
7269
|
}
|
|
5957
7270
|
function target(inner, chat) {
|
|
5958
7271
|
if (typeof inner.to === "string") return { id: null, type: "dm", title: clip(inner.to, 80) };
|
|
@@ -6158,25 +7471,156 @@ var LiveTurns = class {
|
|
|
6158
7471
|
}
|
|
6159
7472
|
};
|
|
6160
7473
|
|
|
7474
|
+
// src/server/service/bot/agent.ts
|
|
7475
|
+
var same2 = (a, b) => a.trim().replace(/^@/, "").toLocaleLowerCase() === b.trim().toLocaleLowerCase();
|
|
7476
|
+
var AgentOps = class {
|
|
7477
|
+
constructor(bots, store, chats) {
|
|
7478
|
+
this.bots = bots;
|
|
7479
|
+
this.store = store;
|
|
7480
|
+
this.chats = chats;
|
|
7481
|
+
}
|
|
7482
|
+
get owner() {
|
|
7483
|
+
return this.store.userMemberId();
|
|
7484
|
+
}
|
|
7485
|
+
agentId = AGENT_BOT_ID;
|
|
7486
|
+
/** A Bot by name, ignoring case and a leading @. */
|
|
7487
|
+
bot(name2) {
|
|
7488
|
+
const bot = this.store.listBots().find((record3) => same2(name2, record3.name));
|
|
7489
|
+
if (!bot) throw new ChatError(404, `No Bot named ${name2} (see bots.list)`);
|
|
7490
|
+
return bot;
|
|
7491
|
+
}
|
|
7492
|
+
/** Connected models and their thinking levels; never credentials. */
|
|
7493
|
+
models() {
|
|
7494
|
+
return this.store.listModelConnections().filter((connection) => connection.models.length).map((connection) => ({
|
|
7495
|
+
connection: connection.displayName,
|
|
7496
|
+
connectionId: connection.id,
|
|
7497
|
+
models: connection.models.map((model) => ({ id: model.id, name: model.name, thinking: ["auto", ...thinkingOptions(model).levels] }))
|
|
7498
|
+
}));
|
|
7499
|
+
}
|
|
7500
|
+
selection(bot, input) {
|
|
7501
|
+
if (!input.model && input.thinking === void 0) return null;
|
|
7502
|
+
let connectionId = bot?.connectionId ?? null, modelId = bot?.modelId ?? "";
|
|
7503
|
+
if (input.model) {
|
|
7504
|
+
const { connection, model } = input.model;
|
|
7505
|
+
const found = this.store.listModelConnections().find((item) => item.id === connection || same2(connection, item.displayName));
|
|
7506
|
+
if (!found) throw new ChatError(404, `No connection named ${connection} (see models.list)`);
|
|
7507
|
+
const picked = found.models.find((item) => item.id === model || same2(model, item.name));
|
|
7508
|
+
if (!picked) throw new ChatError(404, `No model ${model} in ${found.displayName} (see models.list)`);
|
|
7509
|
+
connectionId = found.id;
|
|
7510
|
+
modelId = picked.id;
|
|
7511
|
+
}
|
|
7512
|
+
if (!connectionId) throw new ChatError(400, "This Bot has no model yet; give model too");
|
|
7513
|
+
return { connectionId, modelId, ...input.thinking === void 0 ? {} : { thinkingLevel: input.thinking } };
|
|
7514
|
+
}
|
|
7515
|
+
/** Where a Bot works now: computer or host (A50). */
|
|
7516
|
+
environment(botId) {
|
|
7517
|
+
return this.bots.describe(botId).environment.home;
|
|
7518
|
+
}
|
|
7519
|
+
/** "model (connection)", as the agent and the changed Bot read it. */
|
|
7520
|
+
modelLabel(bot) {
|
|
7521
|
+
const connection = bot.connectionId ? this.store.getModelConnection(bot.connectionId) : null;
|
|
7522
|
+
const model = connection?.models.find((item) => item.id === bot.modelId);
|
|
7523
|
+
return model ? `${model.name} (${connection.displayName})` : bot.modelId;
|
|
7524
|
+
}
|
|
7525
|
+
async create(input) {
|
|
7526
|
+
const selection = this.selection(null, input);
|
|
7527
|
+
const created = this.bots.create(input.name, input.environment);
|
|
7528
|
+
if (input.description !== void 0 || input.persona !== void 0)
|
|
7529
|
+
this.bots.get(created.id).identity.update({ name: created.name, description: input.description ?? "", persona: input.persona ?? "" });
|
|
7530
|
+
if (selection) await this.bots.selectModel(created.id, selection);
|
|
7531
|
+
this.bots.listChanged();
|
|
7532
|
+
return this.store.getBot(created.id);
|
|
7533
|
+
}
|
|
7534
|
+
async update(bot, input) {
|
|
7535
|
+
const selection = this.selection(bot, input);
|
|
7536
|
+
if (input.name !== void 0 || input.description !== void 0 || input.persona !== void 0)
|
|
7537
|
+
this.bots.get(bot.id).identity.update({ name: input.name ?? bot.name, description: input.description ?? bot.description, persona: input.persona ?? bot.systemPrompt });
|
|
7538
|
+
if (selection) await this.bots.selectModel(bot.id, selection);
|
|
7539
|
+
const after2 = this.store.getBot(bot.id), changes = [];
|
|
7540
|
+
if (after2.name !== bot.name) changes.push({ what: "name", value: after2.name });
|
|
7541
|
+
if (after2.description !== bot.description) changes.push({ what: "description", value: after2.description });
|
|
7542
|
+
if (after2.systemPrompt !== bot.systemPrompt) changes.push({ what: "persona", value: null });
|
|
7543
|
+
if (after2.connectionId !== bot.connectionId || after2.modelId !== bot.modelId) changes.push({ what: "model", value: this.modelLabel(after2) });
|
|
7544
|
+
if (after2.thinkingLevel !== bot.thinkingLevel) changes.push({ what: "thinking", value: after2.thinkingLevel });
|
|
7545
|
+
this.tell(bot.id, changes);
|
|
7546
|
+
this.bots.listChanged();
|
|
7547
|
+
return { bot: after2, changed: changes.map((change) => change.what) };
|
|
7548
|
+
}
|
|
7549
|
+
async run(bot, action) {
|
|
7550
|
+
if (action === "stop" && bot.id === AGENT_BOT_ID) throw new ChatError(400, "You can't stop yourself");
|
|
7551
|
+
const runtime = this.bots.get(bot.id);
|
|
7552
|
+
await (action === "start" ? runtime.start() : runtime.stop());
|
|
7553
|
+
this.tell(bot.id, [{ what: "run", value: action }]);
|
|
7554
|
+
return this.bots.status(bot.id).status;
|
|
7555
|
+
}
|
|
7556
|
+
usage(bot, days) {
|
|
7557
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
7558
|
+
return { since: new Date(since).toISOString(), models: this.bots.memory(bot.id).usage(since) };
|
|
7559
|
+
}
|
|
7560
|
+
/** Deletions run as the owner, who has just confirmed them on a card. */
|
|
7561
|
+
async deleteBot(bot) {
|
|
7562
|
+
if (bot.id === AGENT_BOT_ID) throw new ChatError(400, "You can't delete yourself");
|
|
7563
|
+
await this.bots.delete(bot.id, true);
|
|
7564
|
+
this.bots.listChanged();
|
|
7565
|
+
}
|
|
7566
|
+
async deleteChannel(chatId) {
|
|
7567
|
+
if (!await this.chats.delete(this.owner, chatId)) throw new ChatError(404, "Channel not found");
|
|
7568
|
+
await this.bots.chatDeleted(chatId, AGENT_BOT_ID);
|
|
7569
|
+
}
|
|
7570
|
+
removeMember(chatId, memberId) {
|
|
7571
|
+
this.chats.channels.removeMember(this.owner, chatId, memberId);
|
|
7572
|
+
}
|
|
7573
|
+
deleteSection(sectionId) {
|
|
7574
|
+
if (!this.chats.channels.deleteSection(this.owner, sectionId)) throw new ChatError(404, "Section not found");
|
|
7575
|
+
}
|
|
7576
|
+
/** The changed Bot is told with its next batch (digest: it is not woken for it), in its DM with the agent. */
|
|
7577
|
+
tell(botId, changes) {
|
|
7578
|
+
if (botId === AGENT_BOT_ID || !changes.length) return;
|
|
7579
|
+
const chatId = this.chats.openDirect(AGENT_BOT_ID, botId).id;
|
|
7580
|
+
for (const change of changes) this.store.agent.notify({ botId, kind: "change", reason: "agent", urgency: "digest", chatId, actorId: AGENT_BOT_ID, detail: JSON.stringify(change) });
|
|
7581
|
+
}
|
|
7582
|
+
/** The agent's first model: ask it, once, to welcome the owner in their DM, in their interface language. */
|
|
7583
|
+
onboard(language) {
|
|
7584
|
+
if (!this.store.agent.markOnboarded()) return;
|
|
7585
|
+
const chatId = this.chats.openDirect(this.owner, AGENT_BOT_ID).id;
|
|
7586
|
+
this.store.agent.notify({ botId: AGENT_BOT_ID, kind: "onboard", reason: "setup", urgency: "interrupt", chatId, actorId: this.owner, detail: language });
|
|
7587
|
+
this.bots.deliver(AGENT_BOT_ID, "interrupt");
|
|
7588
|
+
}
|
|
7589
|
+
};
|
|
7590
|
+
|
|
6161
7591
|
// src/server/service/bot/service.ts
|
|
6162
7592
|
var BotService = class {
|
|
6163
7593
|
constructor(deps) {
|
|
6164
7594
|
this.deps = deps;
|
|
7595
|
+
this.agent = new AgentOps(this, deps.store, deps.chats);
|
|
7596
|
+
this.computers = new Computers({ store: deps.store, changed: () => this.listChanged() });
|
|
7597
|
+
this.environments = new BotEnvironments(deps.store, this.computers);
|
|
6165
7598
|
}
|
|
6166
7599
|
bots = /* @__PURE__ */ new Map();
|
|
6167
7600
|
lives = /* @__PURE__ */ new Map();
|
|
7601
|
+
computers;
|
|
7602
|
+
environments;
|
|
6168
7603
|
closed = false;
|
|
6169
7604
|
started = false;
|
|
7605
|
+
/** What only the agent Bot does (A48). */
|
|
7606
|
+
agent;
|
|
7607
|
+
listChanged() {
|
|
7608
|
+
this.deps.listChanged?.();
|
|
7609
|
+
}
|
|
6170
7610
|
get(id) {
|
|
6171
7611
|
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
6172
|
-
const
|
|
6173
|
-
if (!
|
|
7612
|
+
const record3 = this.deps.store.getBot(id);
|
|
7613
|
+
if (!record3) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6174
7614
|
let bot = this.bots.get(id);
|
|
6175
7615
|
if (bot) return bot;
|
|
6176
7616
|
const { store, chats, files, credentials } = this.deps, live = this.live(id);
|
|
6177
|
-
const
|
|
7617
|
+
const here = () => {
|
|
7618
|
+
const place = this.environments.here(id, record3.workspacePath);
|
|
7619
|
+
return { name: place.name, directory: place.directory };
|
|
7620
|
+
};
|
|
7621
|
+
const runtime = new AgentRuntime(new BotInbox(id, store.runtime, chats, files, here), (setup, model, reference, activity) => (this.deps.factory ?? PiSession.create)({
|
|
6178
7622
|
store,
|
|
6179
|
-
bot:
|
|
7623
|
+
bot: record3,
|
|
6180
7624
|
selection: model.selection,
|
|
6181
7625
|
reference,
|
|
6182
7626
|
credentials,
|
|
@@ -6184,20 +7628,38 @@ var BotService = class {
|
|
|
6184
7628
|
systemPrompt: () => botPrompt(setup),
|
|
6185
7629
|
onHistory: setup.onHistory,
|
|
6186
7630
|
activity,
|
|
6187
|
-
live: (event) => live.event(event)
|
|
7631
|
+
live: (event) => live.event(event),
|
|
7632
|
+
environment: () => this.environments.current(id, record3.workspacePath)
|
|
6188
7633
|
}));
|
|
6189
|
-
const workspace = new Workspace(
|
|
6190
|
-
|
|
7634
|
+
const workspace = new Workspace(record3.workspacePath, () => this.environments.current(id, record3.workspacePath), () => {
|
|
7635
|
+
const place = this.environments.home(id, record3.workspacePath);
|
|
7636
|
+
return { kind: place.kind === "sandbox" ? "computer" : "host", cwd: place.directory };
|
|
7637
|
+
});
|
|
7638
|
+
const memory = new Memory(join8(dirname7(record3.workspacePath), "history.db"));
|
|
6191
7639
|
bot = new Bot(id, {
|
|
6192
7640
|
identity: new Identity(id, store),
|
|
6193
7641
|
model: new Model(id, store, resolveThinking),
|
|
6194
7642
|
workspace,
|
|
6195
7643
|
memory,
|
|
6196
|
-
tools: new Tools({
|
|
7644
|
+
tools: new Tools({
|
|
7645
|
+
botId: id,
|
|
7646
|
+
chats,
|
|
7647
|
+
files,
|
|
7648
|
+
memory,
|
|
7649
|
+
workspace,
|
|
7650
|
+
asking: (on) => runtime.waitingForUser(on),
|
|
7651
|
+
roster: () => this.roster(),
|
|
7652
|
+
places: {
|
|
7653
|
+
list: () => this.environments.list(id, record3.workspacePath),
|
|
7654
|
+
switch: (environmentId) => this.environments.switch(id, environmentId, record3.workspacePath),
|
|
7655
|
+
summary: (botId) => this.environments.summary(botId)
|
|
7656
|
+
},
|
|
7657
|
+
...id === AGENT_BOT_ID ? { agent: this.agent } : {}
|
|
7658
|
+
}),
|
|
6197
7659
|
agentRuntime: runtime,
|
|
6198
7660
|
state: new BotState(() => this.deps.changed(id))
|
|
6199
7661
|
}, { deliver: async (message) => {
|
|
6200
|
-
const outgoing = await
|
|
7662
|
+
const outgoing = await workspace.readOutgoing(message.attachments ?? [], message.signal);
|
|
6201
7663
|
message.signal?.throwIfAborted();
|
|
6202
7664
|
return chats.deliver(id, message.chatId, {
|
|
6203
7665
|
content: message.content,
|
|
@@ -6213,9 +7675,14 @@ var BotService = class {
|
|
|
6213
7675
|
this.bots.set(id, bot);
|
|
6214
7676
|
return bot;
|
|
6215
7677
|
}
|
|
6216
|
-
details(
|
|
6217
|
-
const model =
|
|
6218
|
-
return {
|
|
7678
|
+
details(record3) {
|
|
7679
|
+
const model = record3.connectionId ? this.deps.store.getModelConnection(record3.connectionId)?.models.find((model2) => model2.id === record3.modelId) : void 0;
|
|
7680
|
+
return {
|
|
7681
|
+
...record3,
|
|
7682
|
+
effectiveThinkingLevel: model ? resolveThinking(model, record3.thinkingLevel) : null,
|
|
7683
|
+
agent: record3.id === AGENT_BOT_ID,
|
|
7684
|
+
environment: this.environments.placement(record3.id)
|
|
7685
|
+
};
|
|
6219
7686
|
}
|
|
6220
7687
|
owns(id) {
|
|
6221
7688
|
return this.deps.store.getBot(id) !== null;
|
|
@@ -6238,15 +7705,58 @@ var BotService = class {
|
|
|
6238
7705
|
return { id: chat.id, type: chat.type, title: other ? this.deps.chats.member(other)?.name ?? chat.title : chat.title };
|
|
6239
7706
|
}
|
|
6240
7707
|
list() {
|
|
6241
|
-
return this.deps.store.listBots().map((
|
|
6242
|
-
}
|
|
6243
|
-
|
|
6244
|
-
|
|
7708
|
+
return this.deps.store.listBots().map((record3) => this.details(record3));
|
|
7709
|
+
}
|
|
7710
|
+
/** A new Bot works in its sandbox when this machine has Docker, unless asked for the owner's computer (A50). */
|
|
7711
|
+
create(name2, environment) {
|
|
7712
|
+
const kind = environment ?? (this.computers.available ? "computer" : "host");
|
|
7713
|
+
if (kind === "computer" && !this.computers.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 409);
|
|
7714
|
+
const record3 = this.deps.store.createBot(name2);
|
|
7715
|
+
this.environments.setHome(record3.id, kind, record3.workspacePath);
|
|
7716
|
+
return this.details(record3);
|
|
7717
|
+
}
|
|
7718
|
+
/** The owner moves a Bot to its sandbox or to their computer, as its default; the next tool call runs there. */
|
|
7719
|
+
setHome(id, kind) {
|
|
7720
|
+
const moved = this.environments.setHome(id, kind, this.record(id).workspacePath);
|
|
7721
|
+
if (moved) this.tellOwner(id, [{ what: "environment", value: where(moved) }], true);
|
|
7722
|
+
return this.describe(id);
|
|
7723
|
+
}
|
|
7724
|
+
/** The owner lets a Bot use their computer, or takes it back; taking it back while the Bot is there sends it home. */
|
|
7725
|
+
grantHost(id, granted) {
|
|
7726
|
+
const { changed, moved } = this.environments.grantHost(id, granted, this.deps.store.userMemberId(), this.record(id).workspacePath);
|
|
7727
|
+
const changes = changed ? [{ what: "access", value: `your owner's computer: ${granted ? "you may switch to it" : "taken back"}` }] : [];
|
|
7728
|
+
if (moved) changes.push({ what: "environment", value: where(moved) });
|
|
7729
|
+
if (changes.length) this.tellOwner(id, changes, !!moved);
|
|
7730
|
+
return this.describe(id);
|
|
7731
|
+
}
|
|
7732
|
+
/** Tells a Bot what its owner changed about where it works (A41, A50). A move interrupts a running turn, as an
|
|
7733
|
+
* @-mention does, so the Bot doesn't go on in the old place; a Bot not running, or a change of access alone, is
|
|
7734
|
+
* told with its next batch (fish m1012–m1015). */
|
|
7735
|
+
tellOwner(id, changes, move) {
|
|
7736
|
+
if (!this.record(id).connectionId) return;
|
|
7737
|
+
const owner = this.deps.store.userMemberId(), chatId = this.deps.chats.openDirect(owner, id).id;
|
|
7738
|
+
const urgency = move && this.bots.get(id)?.status().status === "running" ? urgencyOf("change", "owner") : "digest";
|
|
7739
|
+
for (const change of changes) this.deps.store.agent.notify({ botId: id, kind: "change", reason: "owner", urgency, chatId, actorId: owner, detail: JSON.stringify(change) });
|
|
7740
|
+
if (urgency === "interrupt") this.deliver(id, urgency);
|
|
7741
|
+
}
|
|
7742
|
+
record(id) {
|
|
7743
|
+
const record3 = this.deps.store.getBot(id);
|
|
7744
|
+
if (!record3) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
7745
|
+
return record3;
|
|
7746
|
+
}
|
|
7747
|
+
/** Starting (or resetting, which starts afresh) returns at once: the Bot's state follows as it changes. */
|
|
7748
|
+
async computer(id, action) {
|
|
7749
|
+
this.describe(id);
|
|
7750
|
+
if (action === "stop") await this.computers.stop(id);
|
|
7751
|
+
else {
|
|
7752
|
+
if (action === "reset") await this.computers.reset(id);
|
|
7753
|
+
void this.computers.ensure(id).catch(() => {
|
|
7754
|
+
});
|
|
7755
|
+
}
|
|
7756
|
+
return this.describe(id);
|
|
6245
7757
|
}
|
|
6246
7758
|
describe(id) {
|
|
6247
|
-
|
|
6248
|
-
if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6249
|
-
return this.details(record2);
|
|
7759
|
+
return this.details(this.record(id));
|
|
6250
7760
|
}
|
|
6251
7761
|
runs(id) {
|
|
6252
7762
|
this.describe(id);
|
|
@@ -6273,9 +7783,9 @@ var BotService = class {
|
|
|
6273
7783
|
if (this.closed || !this.deps.store.getBot(id)) return;
|
|
6274
7784
|
const bot = this.get(id);
|
|
6275
7785
|
void bot.deliver(urgency).catch((error) => {
|
|
6276
|
-
const
|
|
6277
|
-
bot.state.update({ ...bot.status(), error: { code:
|
|
6278
|
-
console.error("[bot] delivery failed", { botId: id, code:
|
|
7786
|
+
const fault2 = publicFault(error);
|
|
7787
|
+
bot.state.update({ ...bot.status(), error: { code: fault2.code, message: fault2.message, ...fault2.params ? { params: fault2.params } : {} } });
|
|
7788
|
+
console.error("[bot] delivery failed", { botId: id, code: fault2.code });
|
|
6279
7789
|
});
|
|
6280
7790
|
}
|
|
6281
7791
|
/** Picks up what is still waiting for a Bot (after a failure, a new model, or a restart), if anything. */
|
|
@@ -6289,20 +7799,22 @@ var BotService = class {
|
|
|
6289
7799
|
start() {
|
|
6290
7800
|
if (this.started) return;
|
|
6291
7801
|
this.started = true;
|
|
7802
|
+
void this.computers.start();
|
|
6292
7803
|
for (const { botId, urgency } of this.deps.store.runtime.waitingBots(this.deps.store.userMemberId())) this.deliver(botId, urgency);
|
|
6293
7804
|
}
|
|
6294
7805
|
/** Every Bot's run state, for `bots.list` (A43). A Bot not loaded in this process is stopped. */
|
|
6295
7806
|
roster() {
|
|
6296
7807
|
const { store } = this.deps;
|
|
6297
|
-
return store.listBots().map((
|
|
6298
|
-
const bot = this.bots.get(
|
|
7808
|
+
return store.listBots().map((record3) => {
|
|
7809
|
+
const bot = this.bots.get(record3.id), runId = bot?.agentRuntime.activeRunId ?? null;
|
|
6299
7810
|
const status = bot?.status() ?? { status: "stop", activity: null, context: null, error: null };
|
|
6300
7811
|
return {
|
|
6301
|
-
id:
|
|
6302
|
-
name:
|
|
7812
|
+
id: record3.id,
|
|
7813
|
+
name: record3.name,
|
|
7814
|
+
agent: record3.id === AGENT_BOT_ID,
|
|
6303
7815
|
status,
|
|
6304
7816
|
runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
|
|
6305
|
-
errorCode: store.runtime.latestErrorCode(
|
|
7817
|
+
errorCode: store.runtime.latestErrorCode(record3.id)
|
|
6306
7818
|
};
|
|
6307
7819
|
});
|
|
6308
7820
|
}
|
|
@@ -6311,18 +7823,22 @@ var BotService = class {
|
|
|
6311
7823
|
const bot = this.bots.get(id);
|
|
6312
7824
|
return { ...bot?.status() ?? { status: "stop", activity: null, context: null, error: null }, botId: id, runId: bot?.agentRuntime.activeRunId ?? null };
|
|
6313
7825
|
}
|
|
7826
|
+
/** `language` is the browser's interface language: the agent Bot's first model asks it to welcome the owner in it (A48). */
|
|
6314
7827
|
async selectModel(id, input) {
|
|
6315
7828
|
const selection = this.deps.store.validateSelection(id, input);
|
|
6316
7829
|
if (!selection) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6317
7830
|
await this.get(id).changeModel(selection);
|
|
6318
7831
|
this.resume(id);
|
|
7832
|
+
if (id === AGENT_BOT_ID) this.agent.onboard(welcomeLanguage(input.language));
|
|
6319
7833
|
return this.deps.store.getBot(id);
|
|
6320
7834
|
}
|
|
6321
7835
|
connectionChanged(connectionId) {
|
|
6322
7836
|
for (const bot of this.deps.store.listBots()) if (bot.connectionId === connectionId) this.resume(bot.id);
|
|
6323
7837
|
}
|
|
6324
|
-
|
|
6325
|
-
|
|
7838
|
+
/** A deleted chat ends the turns handling it; `except` is the agent Bot that deleted it from inside its turn. */
|
|
7839
|
+
async chatDeleted(chatId, except) {
|
|
7840
|
+
for (const [id, bot] of this.bots) {
|
|
7841
|
+
if (id === except) continue;
|
|
6326
7842
|
const runId = bot.agentRuntime.activeRunId;
|
|
6327
7843
|
if (runId && this.deps.store.runtime.inputs(runId).some((input) => input.chatId === chatId)) await bot.abort();
|
|
6328
7844
|
}
|
|
@@ -6330,21 +7846,25 @@ var BotService = class {
|
|
|
6330
7846
|
/** Deletes a Bot: stop it, drop its data, keep its messages. `keepFiles` leaves the workspace (and
|
|
6331
7847
|
* the History database) on disk, since it may hold work worth keeping. */
|
|
6332
7848
|
async delete(id, keepFiles) {
|
|
7849
|
+
if (id === AGENT_BOT_ID) throw new RuntimeFault("agent_undeletable", "The agent Bot can't be deleted", 409);
|
|
6333
7850
|
const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
|
|
6334
7851
|
await bot.stop();
|
|
6335
7852
|
this.bots.delete(id);
|
|
6336
7853
|
this.lives.get(id)?.close();
|
|
6337
7854
|
this.lives.delete(id);
|
|
7855
|
+
await this.environments.release(id);
|
|
6338
7856
|
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6339
|
-
if (!keepFiles) await
|
|
7857
|
+
if (!keepFiles) await rm3(dirname7(workspacePath), { recursive: true, force: true });
|
|
6340
7858
|
}
|
|
6341
7859
|
async close() {
|
|
6342
7860
|
if (this.closed) return;
|
|
6343
7861
|
this.closed = true;
|
|
6344
7862
|
await Promise.all([...this.bots.values()].map((bot) => bot.stop()));
|
|
6345
7863
|
for (const live of this.lives.values()) live.close();
|
|
7864
|
+
await this.environments.close();
|
|
6346
7865
|
}
|
|
6347
7866
|
};
|
|
7867
|
+
var where = (moved) => `${moved.to.name}, in ${moved.to.directory}`;
|
|
6348
7868
|
|
|
6349
7869
|
// src/server/application.ts
|
|
6350
7870
|
var Application = class _Application {
|
|
@@ -6366,7 +7886,7 @@ var Application = class _Application {
|
|
|
6366
7886
|
(id) => this.events.publish({ kind: "chat", id }),
|
|
6367
7887
|
() => this.events.publish({ kind: "inbox", memberId: this.user.id })
|
|
6368
7888
|
);
|
|
6369
|
-
this.files = new ChatFiles(
|
|
7889
|
+
this.files = new ChatFiles(this.chats);
|
|
6370
7890
|
store.runtime.recover();
|
|
6371
7891
|
this.bots = new BotService({
|
|
6372
7892
|
store,
|
|
@@ -6374,11 +7894,12 @@ var Application = class _Application {
|
|
|
6374
7894
|
files: this.files,
|
|
6375
7895
|
credentials: this.credentials,
|
|
6376
7896
|
changed: (id) => this.events.publish({ kind: "bot", id, status: this.bots.status(id) }),
|
|
7897
|
+
listChanged: () => this.events.publish({ kind: "bots" }),
|
|
6377
7898
|
factory
|
|
6378
7899
|
});
|
|
6379
7900
|
this.auth = new CredentialLogins(store, this.credentials, (id) => this.bots.connectionChanged(id));
|
|
6380
|
-
this.account = new AccountService(store,
|
|
6381
|
-
this.models = new ModelService(store, this.credentials, this.auth, (id) => this.bots.connectionChanged(id));
|
|
7901
|
+
this.account = new AccountService(store, createHash6("sha256").update(dataDir).digest("hex").slice(0, 10));
|
|
7902
|
+
this.models = new ModelService(store, this.credentials, this.auth, (id) => this.bots.connectionChanged(id), new ModelCatalog(dataDir));
|
|
6382
7903
|
}
|
|
6383
7904
|
events = new Changes();
|
|
6384
7905
|
credentials = new CredentialCoordinator();
|
|
@@ -6391,8 +7912,8 @@ var Application = class _Application {
|
|
|
6391
7912
|
models;
|
|
6392
7913
|
closing;
|
|
6393
7914
|
static async open(dataDir, options = {}) {
|
|
6394
|
-
await
|
|
6395
|
-
const root = await
|
|
7915
|
+
await mkdir5(resolve3(dataDir), { recursive: true, mode: 448 });
|
|
7916
|
+
const root = await realpath(resolve3(dataDir));
|
|
6396
7917
|
let app;
|
|
6397
7918
|
let release;
|
|
6398
7919
|
try {
|
|
@@ -6400,7 +7921,7 @@ var Application = class _Application {
|
|
|
6400
7921
|
retries: 0,
|
|
6401
7922
|
stale: 3e4,
|
|
6402
7923
|
update: 1e4,
|
|
6403
|
-
lockfilePath:
|
|
7924
|
+
lockfilePath: join9(root, ".runtime.lock"),
|
|
6404
7925
|
onCompromised: () => {
|
|
6405
7926
|
console.error("[runtime] data lock compromised");
|
|
6406
7927
|
options.onCompromised?.();
|
|
@@ -6423,6 +7944,7 @@ var Application = class _Application {
|
|
|
6423
7944
|
}
|
|
6424
7945
|
start() {
|
|
6425
7946
|
this.bots.start();
|
|
7947
|
+
this.models.refreshCatalog();
|
|
6426
7948
|
}
|
|
6427
7949
|
close() {
|
|
6428
7950
|
if (!this.closing) this.closing = (async () => {
|
|
@@ -6438,14 +7960,14 @@ var Application = class _Application {
|
|
|
6438
7960
|
};
|
|
6439
7961
|
|
|
6440
7962
|
// src/server/daemon.ts
|
|
6441
|
-
import { spawn } from "node:child_process";
|
|
7963
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
6442
7964
|
import { closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
6443
|
-
import { join as
|
|
6444
|
-
var pidPath = (dir) =>
|
|
6445
|
-
var logPath = (dir) =>
|
|
7965
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
7966
|
+
var pidPath = (dir) => join10(resolve4(dir), "jarvy.pid");
|
|
7967
|
+
var logPath = (dir) => join10(resolve4(dir), "jarvy.log");
|
|
6446
7968
|
function writePidFile(dir, url) {
|
|
6447
|
-
const
|
|
6448
|
-
writeFileSync(pidPath(dir), JSON.stringify(
|
|
7969
|
+
const record3 = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
|
|
7970
|
+
writeFileSync(pidPath(dir), JSON.stringify(record3), { mode: 384 });
|
|
6449
7971
|
}
|
|
6450
7972
|
function removePidFile(dir) {
|
|
6451
7973
|
if (readRecord(dir)?.pid === process.pid) rmSync(pidPath(dir), { force: true });
|
|
@@ -6473,19 +7995,19 @@ function alive(pid) {
|
|
|
6473
7995
|
}
|
|
6474
7996
|
}
|
|
6475
7997
|
function runningServer(dir) {
|
|
6476
|
-
const
|
|
6477
|
-
if (!
|
|
6478
|
-
const token = alive(
|
|
6479
|
-
if (token === null || token &&
|
|
7998
|
+
const record3 = readRecord(dir);
|
|
7999
|
+
if (!record3) return null;
|
|
8000
|
+
const token = alive(record3.pid) ? startToken(record3.pid) : null;
|
|
8001
|
+
if (token === null || token && record3.startToken && token !== record3.startToken) {
|
|
6480
8002
|
rmSync(pidPath(dir), { force: true });
|
|
6481
8003
|
return null;
|
|
6482
8004
|
}
|
|
6483
|
-
return
|
|
8005
|
+
return record3;
|
|
6484
8006
|
}
|
|
6485
8007
|
function startBackground(script, runArgs, dir) {
|
|
6486
|
-
mkdirSync3(
|
|
8008
|
+
mkdirSync3(resolve4(dir), { recursive: true, mode: 448 });
|
|
6487
8009
|
const log = openSync(logPath(dir), "a", 384);
|
|
6488
|
-
const child =
|
|
8010
|
+
const child = spawn2(process.execPath, [...process.execArgv, script, "run", ...runArgs], { detached: true, stdio: ["ignore", log, log, "ipc"] });
|
|
6489
8011
|
closeSync(log);
|
|
6490
8012
|
return new Promise((resolvePromise, reject) => {
|
|
6491
8013
|
child.once("error", reject);
|
|
@@ -6513,11 +8035,11 @@ function signalReady(url) {
|
|
|
6513
8035
|
process.disconnect();
|
|
6514
8036
|
}
|
|
6515
8037
|
async function stopServer(dir, waitMs = 3e4) {
|
|
6516
|
-
const
|
|
6517
|
-
if (!
|
|
6518
|
-
process.kill(
|
|
8038
|
+
const record3 = runningServer(dir);
|
|
8039
|
+
if (!record3) return "not_running";
|
|
8040
|
+
process.kill(record3.pid, "SIGTERM");
|
|
6519
8041
|
for (const deadline = Date.now() + waitMs; Date.now() < deadline; ) {
|
|
6520
|
-
if (!alive(
|
|
8042
|
+
if (!alive(record3.pid)) {
|
|
6521
8043
|
rmSync(pidPath(dir), { force: true });
|
|
6522
8044
|
return "stopped";
|
|
6523
8045
|
}
|
|
@@ -6536,7 +8058,7 @@ function option(args, flag, fallback) {
|
|
|
6536
8058
|
function secretPrompt(label) {
|
|
6537
8059
|
if (!process.stdin.isTTY || !process.stdin.setRawMode) throw new Error("Account setup requires an interactive terminal");
|
|
6538
8060
|
process.stdout.write(label);
|
|
6539
|
-
return new Promise((
|
|
8061
|
+
return new Promise((resolve5, reject) => {
|
|
6540
8062
|
let value2 = "";
|
|
6541
8063
|
const done = (result, error) => {
|
|
6542
8064
|
process.stdin.off("data", onData);
|
|
@@ -6544,7 +8066,7 @@ function secretPrompt(label) {
|
|
|
6544
8066
|
process.stdin.pause();
|
|
6545
8067
|
process.stdout.write("\n");
|
|
6546
8068
|
if (error) reject(error);
|
|
6547
|
-
else
|
|
8069
|
+
else resolve5(result ?? "");
|
|
6548
8070
|
};
|
|
6549
8071
|
const onData = (buffer) => {
|
|
6550
8072
|
for (const char of buffer.toString("utf8")) {
|
|
@@ -6661,18 +8183,18 @@ async function main() {
|
|
|
6661
8183
|
try {
|
|
6662
8184
|
if (!store.isInitialized()) await initialize(store);
|
|
6663
8185
|
const { host, port, url } = listenOptions(args);
|
|
6664
|
-
const webRoot =
|
|
6665
|
-
if (!existsSync2(
|
|
8186
|
+
const webRoot = join11(dirname8(fileURLToPath(import.meta.url)), "../web");
|
|
8187
|
+
if (!existsSync2(join11(webRoot, "index.html"))) throw new Error("Web build missing. Run npm run build first");
|
|
6666
8188
|
const server = createJarvyServer(app, webRoot);
|
|
6667
8189
|
closeHttp = () => {
|
|
6668
8190
|
server.close();
|
|
6669
8191
|
server.closeAllConnections();
|
|
6670
8192
|
};
|
|
6671
|
-
await new Promise((
|
|
8193
|
+
await new Promise((resolve5, reject) => {
|
|
6672
8194
|
server.once("error", reject);
|
|
6673
8195
|
server.listen(port, host, () => {
|
|
6674
8196
|
server.off("error", reject);
|
|
6675
|
-
|
|
8197
|
+
resolve5();
|
|
6676
8198
|
});
|
|
6677
8199
|
});
|
|
6678
8200
|
app.start();
|