@goodfish/jarvy 0.4.0 → 0.5.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 +19 -0
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/server/cli.js +450 -47
- package/dist/web/assets/figure-DQGUDG-4.js +1 -0
- package/dist/web/assets/{index-DL8kubrV.js → index-DoVuF8GU.js} +105 -105
- package/dist/web/assets/index-Dsjq2IH9.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/figure-sTOSJn2I.js +0 -1
- package/dist/web/assets/index-Dpj-oCnv.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -466,10 +466,10 @@ function chatRoutes({ chats, bots }) {
|
|
|
466
466
|
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
467
|
return c.json({ messages: page.messages.map(messageView), hasOlder: page.hasOlder });
|
|
468
468
|
}
|
|
469
|
-
const
|
|
469
|
+
const after2 = Number(c.req.query("after") ?? 0);
|
|
470
470
|
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 ??
|
|
471
|
+
const messages = chats.read(c.get("actorId"), c.req.param("id"), after2, limit);
|
|
472
|
+
return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ?? after2, hasMore: messages.length === limit });
|
|
473
473
|
});
|
|
474
474
|
routes.post("/:id/messages", jsonBody, async (c) => {
|
|
475
475
|
const message = await chats.send(c.get("actorId"), c.req.param("id"), await readJson(c));
|
|
@@ -607,6 +607,7 @@ data: ${JSON.stringify(data)}
|
|
|
607
607
|
if (change.kind === "sections" && change.ownerId === actorId) send("sections", {});
|
|
608
608
|
if (change.kind === "inbox" && change.memberId === actorId) send("inbox", {});
|
|
609
609
|
if (change.kind === "bot" && bots.owns(change.id)) send("bot", { botId: change.id, status: botStatusView(change.status) });
|
|
610
|
+
if (change.kind === "bots") send("bots", {});
|
|
610
611
|
}, () => disconnect());
|
|
611
612
|
const heartbeat = setInterval(() => {
|
|
612
613
|
if (!account.authenticated(token)) disconnect();
|
|
@@ -809,9 +810,10 @@ function createJarvyServer(services, webRoot) {
|
|
|
809
810
|
|
|
810
811
|
// src/server/shared/models.ts
|
|
811
812
|
var BOT_COLORS = ["gray", "blue", "green", "amber", "rose", "violet"];
|
|
813
|
+
var AGENT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
812
814
|
|
|
813
815
|
// src/server/infra/storage/store.ts
|
|
814
|
-
import { createHash, randomBytes, randomInt as
|
|
816
|
+
import { createHash, randomBytes, randomInt as randomInt3, randomUUID as randomUUID7, scryptSync, timingSafeEqual } from "node:crypto";
|
|
815
817
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
816
818
|
import { homedir } from "node:os";
|
|
817
819
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -1083,12 +1085,49 @@ function migrateTitles(db) {
|
|
|
1083
1085
|
}
|
|
1084
1086
|
}
|
|
1085
1087
|
|
|
1088
|
+
// src/server/infra/storage/migrations/agent-schema.ts
|
|
1089
|
+
function migrateAgent(db) {
|
|
1090
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1091
|
+
if (version >= 19) return;
|
|
1092
|
+
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
1093
|
+
try {
|
|
1094
|
+
const sequence = db.prepare("SELECT seq FROM sqlite_sequence WHERE name='inbox'").get()?.seq ?? 0;
|
|
1095
|
+
db.exec(`
|
|
1096
|
+
CREATE TABLE settings(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
1097
|
+
CREATE TABLE inbox_next(id INTEGER PRIMARY KEY AUTOINCREMENT, member_id TEXT NOT NULL REFERENCES members(id),
|
|
1098
|
+
kind TEXT NOT NULL CHECK(kind IN ('message','reaction','note','onboard','change')), reason TEXT NOT NULL,
|
|
1099
|
+
urgency TEXT NOT NULL CHECK(urgency IN ('interrupt','queue','digest')),
|
|
1100
|
+
chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE, message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
1101
|
+
actor_id TEXT REFERENCES members(id), detail TEXT,
|
|
1102
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','claimed','told','done','failed','interrupted','unknown','cancelled')),
|
|
1103
|
+
run_id TEXT REFERENCES bot_runs(id), created_at INTEGER NOT NULL, told_at INTEGER);
|
|
1104
|
+
INSERT INTO inbox_next(id,member_id,kind,reason,urgency,chat_id,message_id,actor_id,detail,state,run_id,created_at,told_at)
|
|
1105
|
+
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;
|
|
1106
|
+
DROP TABLE inbox;
|
|
1107
|
+
ALTER TABLE inbox_next RENAME TO inbox;
|
|
1108
|
+
CREATE UNIQUE INDEX inbox_one_message ON inbox(member_id, message_id) WHERE kind='message';
|
|
1109
|
+
CREATE INDEX inbox_open ON inbox(member_id, state, id);
|
|
1110
|
+
CREATE INDEX inbox_run ON inbox(run_id) WHERE run_id IS NOT NULL;
|
|
1111
|
+
`);
|
|
1112
|
+
db.prepare("UPDATE sqlite_sequence SET seq=MAX(seq,?) WHERE name='inbox'").run(sequence);
|
|
1113
|
+
db.prepare(`INSERT INTO settings(key,value) SELECT 'agent_onboarded',? FROM bots
|
|
1114
|
+
WHERE id=? AND deleted_at IS NULL AND connection_id IS NOT NULL`).run(String(Date.now()), AGENT_BOT_ID);
|
|
1115
|
+
db.exec("PRAGMA user_version=19");
|
|
1116
|
+
if (db.prepare("PRAGMA foreign_key_check").all().length) throw new Error("Migration has invalid references");
|
|
1117
|
+
db.exec("COMMIT");
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
db.exec("ROLLBACK");
|
|
1120
|
+
throw error;
|
|
1121
|
+
} finally {
|
|
1122
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1086
1126
|
// src/server/infra/storage/migrations/schema.ts
|
|
1087
1127
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1088
1128
|
|
|
1089
1129
|
// src/server/infra/storage/migrations/prototype-schema.ts
|
|
1090
1130
|
import { randomUUID } from "node:crypto";
|
|
1091
|
-
var DEFAULT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
1092
1131
|
function migratePrototype(db) {
|
|
1093
1132
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1094
1133
|
if (version >= 4) return;
|
|
@@ -1121,9 +1160,9 @@ function migratePrototype(db) {
|
|
|
1121
1160
|
system_prompt TEXT NOT NULL, model_id TEXT NOT NULL, avatar_mime TEXT, avatar_data BLOB,
|
|
1122
1161
|
avatar_updated_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL);
|
|
1123
1162
|
INSERT INTO bots(id,name,description,system_prompt,model_id,created_at)
|
|
1124
|
-
SELECT '${
|
|
1163
|
+
SELECT '${AGENT_BOT_ID}',name,'',system_prompt,'',0 FROM bot_settings WHERE id=1;
|
|
1125
1164
|
ALTER TABLE chats ADD COLUMN bot_id TEXT REFERENCES bots(id);
|
|
1126
|
-
UPDATE chats SET bot_id='${
|
|
1165
|
+
UPDATE chats SET bot_id='${AGENT_BOT_ID}';
|
|
1127
1166
|
CREATE INDEX chat_by_bot ON chats(bot_id,updated_at DESC);
|
|
1128
1167
|
CREATE TABLE attachments (id TEXT PRIMARY KEY, chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
1129
1168
|
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE, name TEXT NOT NULL,
|
|
@@ -1206,7 +1245,7 @@ function migrateRuntime(db) {
|
|
|
1206
1245
|
// src/server/infra/storage/migrations/schema.ts
|
|
1207
1246
|
function migrateDatabase(db) {
|
|
1208
1247
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1209
|
-
if (version >
|
|
1248
|
+
if (version > 19) throw new Error(`Unsupported database version: ${version}`);
|
|
1210
1249
|
migratePrototype(db);
|
|
1211
1250
|
if (version >= 5) {
|
|
1212
1251
|
migrateRuntime(db);
|
|
@@ -1222,6 +1261,7 @@ function migrateDatabase(db) {
|
|
|
1222
1261
|
migrateInbox(db);
|
|
1223
1262
|
migrateQuotes(db);
|
|
1224
1263
|
migrateTitles(db);
|
|
1264
|
+
migrateAgent(db);
|
|
1225
1265
|
return;
|
|
1226
1266
|
}
|
|
1227
1267
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1295,6 +1335,7 @@ function migrateDatabase(db) {
|
|
|
1295
1335
|
migrateInbox(db);
|
|
1296
1336
|
migrateQuotes(db);
|
|
1297
1337
|
migrateTitles(db);
|
|
1338
|
+
migrateAgent(db);
|
|
1298
1339
|
}
|
|
1299
1340
|
|
|
1300
1341
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1618,7 +1659,7 @@ var InteractionRepository = class {
|
|
|
1618
1659
|
}
|
|
1619
1660
|
/** Notices the Bot has not been told, oldest first, in chats it can still read. */
|
|
1620
1661
|
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
|
|
1662
|
+
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
1663
|
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
1664
|
return rows2.filter((row) => this.chats.canAccess(botId, row.chatId)).map(({ actorId, ...row }) => ({ ...row, actor: this.chats.member(actorId) }));
|
|
1624
1665
|
}
|
|
@@ -1848,9 +1889,9 @@ var ChatRepository = class {
|
|
|
1848
1889
|
AND (instr(lower(c.title),lower(?))>0 OR instr(lower(c.topic),lower(?))>0 OR instr(lower(COALESCE(n.text,'')),lower(?))>0)
|
|
1849
1890
|
ORDER BY c.updated_at DESC LIMIT ?`).all(actorId, query, query, query, limit).map((row) => row.id);
|
|
1850
1891
|
}
|
|
1851
|
-
messages(actorId, chatId,
|
|
1892
|
+
messages(actorId, chatId, after2 = 0, limit = 1e3) {
|
|
1852
1893
|
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,
|
|
1894
|
+
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE chat_id=? AND sequence>? ORDER BY sequence LIMIT ?`).all(chatId, after2, limit);
|
|
1854
1895
|
return rows2.map((row) => this.messageFromRow(row));
|
|
1855
1896
|
}
|
|
1856
1897
|
message(actorId, id) {
|
|
@@ -2191,6 +2232,47 @@ var CredentialRepository = class {
|
|
|
2191
2232
|
}
|
|
2192
2233
|
};
|
|
2193
2234
|
|
|
2235
|
+
// src/server/infra/storage/agent-repository.ts
|
|
2236
|
+
import { randomInt as randomInt2 } from "node:crypto";
|
|
2237
|
+
var AgentRepository = class {
|
|
2238
|
+
constructor(db, ownerId) {
|
|
2239
|
+
this.db = db;
|
|
2240
|
+
this.ownerId = ownerId;
|
|
2241
|
+
}
|
|
2242
|
+
/** Brings the agent Bot back when it is missing (deleted before it became undeletable): the same id, the first free
|
|
2243
|
+
* name of Jarvy, Jarvy 2, …, no persona and no model, so the owner picks one and is welcomed again. */
|
|
2244
|
+
ensure() {
|
|
2245
|
+
const row = this.db.prepare("SELECT deleted_at AS deletedAt FROM bots WHERE id=?").get(AGENT_BOT_ID);
|
|
2246
|
+
if (row && row.deletedAt === null) return;
|
|
2247
|
+
const taken = this.db.prepare(`SELECT 1 FROM account WHERE lower(username)=lower(:name)
|
|
2248
|
+
UNION ALL SELECT 1 FROM bots WHERE lower(name)=lower(:name) AND deleted_at IS NULL LIMIT 1`);
|
|
2249
|
+
let name2 = "Jarvy";
|
|
2250
|
+
for (let n = 2; taken.get({ name: name2 }); n++) name2 = `Jarvy ${n}`;
|
|
2251
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2252
|
+
try {
|
|
2253
|
+
if (row) this.db.prepare(`UPDATE bots SET deleted_at=NULL,name=?,description='',system_prompt='',connection_id=NULL,model_id='',thinking_level='auto'
|
|
2254
|
+
WHERE id=?`).run(name2, AGENT_BOT_ID);
|
|
2255
|
+
else {
|
|
2256
|
+
this.db.prepare("INSERT OR IGNORE INTO members VALUES(?,'bot')").run(AGENT_BOT_ID);
|
|
2257
|
+
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)]);
|
|
2258
|
+
}
|
|
2259
|
+
this.db.prepare("DELETE FROM settings WHERE key='agent_onboarded'").run();
|
|
2260
|
+
this.db.exec("COMMIT");
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
this.db.exec("ROLLBACK");
|
|
2263
|
+
throw error;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
/** Records that the owner is being welcomed; false when that already happened, so only one caller asks. */
|
|
2267
|
+
markOnboarded() {
|
|
2268
|
+
return Number(this.db.prepare("INSERT OR IGNORE INTO settings(key,value) VALUES('agent_onboarded',?)").run(String(Date.now())).changes) > 0;
|
|
2269
|
+
}
|
|
2270
|
+
/** One delivery about no message, for one Bot; the caller wakes it. */
|
|
2271
|
+
notify(input) {
|
|
2272
|
+
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());
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2194
2276
|
// src/server/infra/storage/store.ts
|
|
2195
2277
|
var defaultDataDir = () => process.env.JARVY_DATA_DIR || join(homedir(), ".jarvy");
|
|
2196
2278
|
var DATABASE_FILENAME = "jarvy.db";
|
|
@@ -2260,6 +2342,7 @@ var Store = class {
|
|
|
2260
2342
|
chats;
|
|
2261
2343
|
runtime;
|
|
2262
2344
|
credentials;
|
|
2345
|
+
agent;
|
|
2263
2346
|
constructor(dataDir = defaultDataDir()) {
|
|
2264
2347
|
const root = resolve(dataDir);
|
|
2265
2348
|
mkdirSync(root, { recursive: true, mode: 448 });
|
|
@@ -2276,6 +2359,8 @@ var Store = class {
|
|
|
2276
2359
|
this.chats = new ChatRepository(this.db);
|
|
2277
2360
|
this.runtime = new RuntimeRepository(this.db);
|
|
2278
2361
|
this.credentials = new CredentialRepository(this.db);
|
|
2362
|
+
this.agent = new AgentRepository(this.db, () => this.userMemberId());
|
|
2363
|
+
this.agent.ensure();
|
|
2279
2364
|
}
|
|
2280
2365
|
close() {
|
|
2281
2366
|
this.db.close();
|
|
@@ -2380,7 +2465,7 @@ var Store = class {
|
|
|
2380
2465
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2381
2466
|
try {
|
|
2382
2467
|
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[
|
|
2468
|
+
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
2469
|
this.db.exec("COMMIT");
|
|
2385
2470
|
} catch (error) {
|
|
2386
2471
|
this.db.exec("ROLLBACK");
|
|
@@ -2784,8 +2869,8 @@ function candidates(query) {
|
|
|
2784
2869
|
const q = query.trim().toLowerCase();
|
|
2785
2870
|
if (!q) return [];
|
|
2786
2871
|
const all = catalog();
|
|
2787
|
-
const
|
|
2788
|
-
return
|
|
2872
|
+
const same3 = all.filter((row) => row.model.id.toLowerCase() === q || strip(row.model.id) === strip(q));
|
|
2873
|
+
return same3.length ? same3 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
|
|
2789
2874
|
}
|
|
2790
2875
|
function similar(query) {
|
|
2791
2876
|
const q = strip(query);
|
|
@@ -3200,7 +3285,11 @@ var POLICY = {
|
|
|
3200
3285
|
// Marks on your messages are queued: they wake you but never interrupt (A39).
|
|
3201
3286
|
reaction: { author: "queue" },
|
|
3202
3287
|
// A new version of a notice or summary is queued like a mark.
|
|
3203
|
-
note: { member: "queue", follower: "queue" }
|
|
3288
|
+
note: { member: "queue", follower: "queue" },
|
|
3289
|
+
// The owner just gave the agent Bot its first model: it should welcome them now (A48).
|
|
3290
|
+
onboard: { setup: "interrupt" },
|
|
3291
|
+
// What the agent Bot changed about a Bot rides along with that Bot's next batch, without waking it.
|
|
3292
|
+
change: { agent: "digest" }
|
|
3204
3293
|
};
|
|
3205
3294
|
function urgencyOf(kind, reason) {
|
|
3206
3295
|
return POLICY[kind][reason];
|
|
@@ -3364,8 +3453,8 @@ var ChatMessages = class {
|
|
|
3364
3453
|
const message = this.repository.message(this.actor, id);
|
|
3365
3454
|
return message?.chatId === this.id ? message : null;
|
|
3366
3455
|
}
|
|
3367
|
-
async list({ after = 0, limit }) {
|
|
3368
|
-
return this.repository.messages(this.actor, this.id,
|
|
3456
|
+
async list({ after: after2 = 0, limit }) {
|
|
3457
|
+
return this.repository.messages(this.actor, this.id, after2, limit);
|
|
3369
3458
|
}
|
|
3370
3459
|
async search(query, { limit }) {
|
|
3371
3460
|
return this.repository.search(this.actor, this.id, query, limit);
|
|
@@ -3835,10 +3924,10 @@ var ChatService = class {
|
|
|
3835
3924
|
const messages = this.repository.messagesBefore(actorId, chatId, before ?? Number.MAX_SAFE_INTEGER, limit + 1);
|
|
3836
3925
|
return { messages: messages.slice(-limit), hasOlder: messages.length > limit };
|
|
3837
3926
|
}
|
|
3838
|
-
read(actorId, chatId,
|
|
3839
|
-
if (!Number.isSafeInteger(
|
|
3927
|
+
read(actorId, chatId, after2 = 0, limit = 1e3) {
|
|
3928
|
+
if (!Number.isSafeInteger(after2) || after2 < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
|
|
3840
3929
|
throw new ChatError(400, "Invalid message cursor or limit");
|
|
3841
|
-
return this.repository.messages(actorId, chatId,
|
|
3930
|
+
return this.repository.messages(actorId, chatId, after2, limit);
|
|
3842
3931
|
}
|
|
3843
3932
|
instance(actorId, chatId) {
|
|
3844
3933
|
this.repository.assertAccess(actorId, chatId);
|
|
@@ -4831,7 +4920,7 @@ var Identity = class {
|
|
|
4831
4920
|
}
|
|
4832
4921
|
snapshot() {
|
|
4833
4922
|
const bot = this.store.getBot(this.botId);
|
|
4834
|
-
return { name: bot.name, description: bot.description, persona: bot.systemPrompt };
|
|
4923
|
+
return { name: bot.name, description: bot.description, persona: bot.systemPrompt, agent: this.botId === AGENT_BOT_ID };
|
|
4835
4924
|
}
|
|
4836
4925
|
};
|
|
4837
4926
|
|
|
@@ -4916,7 +5005,7 @@ var BotState = class {
|
|
|
4916
5005
|
};
|
|
4917
5006
|
|
|
4918
5007
|
// src/server/service/bot/tools.ts
|
|
4919
|
-
import { Type as
|
|
5008
|
+
import { Type as Type5 } from "typebox";
|
|
4920
5009
|
import { Value } from "typebox/value";
|
|
4921
5010
|
|
|
4922
5011
|
// src/server/service/bot/history-read.ts
|
|
@@ -5114,6 +5203,7 @@ var ChatTools = class {
|
|
|
5114
5203
|
if (missing.length) throw new ChatError(404, `No member named: ${missing.join(", ")} (see members.list)`);
|
|
5115
5204
|
return found.map((item) => item.member);
|
|
5116
5205
|
}
|
|
5206
|
+
/** A channel group by ID or name. */
|
|
5117
5207
|
section(value2) {
|
|
5118
5208
|
const sections = this.chats.channels.sections(this.bot);
|
|
5119
5209
|
const matches = sections.filter((section) => section.id === value2 || same(value2, section.name));
|
|
@@ -5261,7 +5351,7 @@ var QuestionOption = Type3.Object({
|
|
|
5261
5351
|
var definitions = {
|
|
5262
5352
|
"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
5353
|
"members.list": { description: "List every member, people and Bots: name, kind (user for a person, or bot) and description; self=true is you. Use these names to @-mention someone, add them to a channel or DM them.", schema: None },
|
|
5264
|
-
"bots.list": { description: "List every Bot and its run state. Read-only; it changes nothing. People and Bots can all see every Bot's name and state; this is not private. For each Bot: name; self=true is you; status (stop: not running, idle, or running). While it runs, activity is what it is doing (reasoning, texting: writing text, tool_calling, or asking: waiting for a person's answer), chats are the chats it is handling this turn (only those you can read), and otherChats is how many others there are. If its last turn failed, error is the error code. You come first, then the rest by name.", schema: None },
|
|
5354
|
+
"bots.list": { description: "List every Bot and its run state. Read-only; it changes nothing. People and Bots can all see every Bot's name and state; this is not private. For each Bot: name; self=true is you; 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. You come first, then the rest by name.", schema: None },
|
|
5265
5355
|
"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
5356
|
"chats.info": {
|
|
5267
5357
|
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 +5379,7 @@ var definitions = {
|
|
|
5289
5379
|
}, { additionalProperties: false })
|
|
5290
5380
|
},
|
|
5291
5381
|
"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.",
|
|
5382
|
+
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
5383
|
schema: Type3.Object({ chatId: Type3.String(), members: Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }) }, { additionalProperties: false })
|
|
5294
5384
|
},
|
|
5295
5385
|
"chats.subscribe": {
|
|
@@ -5368,6 +5458,7 @@ function botRows(chats, viewerId, entries) {
|
|
|
5368
5458
|
return { self: entry.id === viewerId, row: {
|
|
5369
5459
|
name: entry.name,
|
|
5370
5460
|
...entry.id === viewerId ? { self: true } : {},
|
|
5461
|
+
...entry.agent ? { agent: true } : {},
|
|
5371
5462
|
status: entry.status.status,
|
|
5372
5463
|
...running && entry.status.activity ? { activity: entry.status.activity } : {},
|
|
5373
5464
|
...visible.length ? { chats: visible } : {},
|
|
@@ -5379,19 +5470,177 @@ function botRows(chats, viewerId, entries) {
|
|
|
5379
5470
|
return rows2.map((item) => item.row);
|
|
5380
5471
|
}
|
|
5381
5472
|
|
|
5473
|
+
// src/server/service/bot/agent-tools.ts
|
|
5474
|
+
var AgentTools = class {
|
|
5475
|
+
constructor(scope) {
|
|
5476
|
+
this.scope = scope;
|
|
5477
|
+
}
|
|
5478
|
+
get ops() {
|
|
5479
|
+
return this.scope.ops;
|
|
5480
|
+
}
|
|
5481
|
+
async confirmed(input, ask) {
|
|
5482
|
+
const chat = this.scope.tools.chat(input.askIn);
|
|
5483
|
+
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");
|
|
5484
|
+
const answer = await ask(chat.id, {
|
|
5485
|
+
question: input.question,
|
|
5486
|
+
multiple: false,
|
|
5487
|
+
allowCustom: false,
|
|
5488
|
+
options: [{ label: input.confirm, value: "confirm", style: "danger" }, { label: input.cancel, value: "cancel" }]
|
|
5489
|
+
});
|
|
5490
|
+
if (answer.status === "dismissed") return { deleted: false, answer: "dismissed" };
|
|
5491
|
+
return answer.selected[0]?.value === "confirm" ? null : { deleted: false, answer: "cancelled" };
|
|
5492
|
+
}
|
|
5493
|
+
channel(chatId) {
|
|
5494
|
+
const chat = this.scope.tools.chat(chatId);
|
|
5495
|
+
if (chat.type !== "channel") throw new ChatError(400, "Not a channel");
|
|
5496
|
+
return chat;
|
|
5497
|
+
}
|
|
5498
|
+
async call(name2, args, ask) {
|
|
5499
|
+
switch (name2) {
|
|
5500
|
+
case "models.list":
|
|
5501
|
+
return this.ops.models();
|
|
5502
|
+
case "bots.create": {
|
|
5503
|
+
const bot = await this.ops.create(args);
|
|
5504
|
+
return { name: bot.name, model: bot.connectionId ? this.ops.modelLabel(bot) : null, thinking: bot.thinkingLevel };
|
|
5505
|
+
}
|
|
5506
|
+
case "bots.update": {
|
|
5507
|
+
const { bot, ...settings } = args;
|
|
5508
|
+
const result = await this.ops.update(this.ops.bot(bot), settings);
|
|
5509
|
+
return { name: result.bot.name, changed: result.changed };
|
|
5510
|
+
}
|
|
5511
|
+
case "bots.start":
|
|
5512
|
+
case "bots.stop": {
|
|
5513
|
+
const bot = this.ops.bot(args.bot);
|
|
5514
|
+
return { name: bot.name, status: await this.ops.run(bot, name2 === "bots.start" ? "start" : "stop") };
|
|
5515
|
+
}
|
|
5516
|
+
case "bots.usage": {
|
|
5517
|
+
const input = args, bot = this.ops.bot(input.bot);
|
|
5518
|
+
return { name: bot.name, ...this.ops.usage(bot, input.days ?? 7) };
|
|
5519
|
+
}
|
|
5520
|
+
case "bots.delete": {
|
|
5521
|
+
const input = args, bot = this.ops.bot(input.bot);
|
|
5522
|
+
if (bot.id === this.ops.agentId) throw new ChatError(400, "You can't delete yourself");
|
|
5523
|
+
const declined = await this.confirmed(input, ask);
|
|
5524
|
+
if (declined) return declined;
|
|
5525
|
+
await this.ops.deleteBot(this.ops.bot(input.bot));
|
|
5526
|
+
return { deleted: true, name: bot.name };
|
|
5527
|
+
}
|
|
5528
|
+
case "chats.delete": {
|
|
5529
|
+
const input = args, chat = this.channel(input.chatId);
|
|
5530
|
+
const declined = await this.confirmed(input, ask);
|
|
5531
|
+
if (declined) return declined;
|
|
5532
|
+
await this.ops.deleteChannel(chat.id);
|
|
5533
|
+
return { deleted: true, name: chat.title };
|
|
5534
|
+
}
|
|
5535
|
+
case "chats.removeMembers": {
|
|
5536
|
+
const input = args, chat = this.channel(input.chatId);
|
|
5537
|
+
const members = this.scope.tools.members(input.members).filter((member) => chat.memberIds.includes(member.id));
|
|
5538
|
+
if (!members.length) throw new ChatError(400, "None of them is in this channel");
|
|
5539
|
+
const declined = await this.confirmed(input, ask);
|
|
5540
|
+
if (declined) return { removed: [], answer: declined.answer };
|
|
5541
|
+
for (const member of members) this.ops.removeMember(chat.id, member.id);
|
|
5542
|
+
return { removed: members.map((member) => member.name) };
|
|
5543
|
+
}
|
|
5544
|
+
case "sections.delete": {
|
|
5545
|
+
const input = args, section = this.scope.tools.section(input.section);
|
|
5546
|
+
const declined = await this.confirmed(input, ask);
|
|
5547
|
+
if (declined) return declined;
|
|
5548
|
+
this.ops.deleteSection(section);
|
|
5549
|
+
return { deleted: true };
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
};
|
|
5554
|
+
|
|
5555
|
+
// src/server/service/bot/agent-schemas.ts
|
|
5556
|
+
import { Type as Type4 } from "typebox";
|
|
5557
|
+
var None2 = Type4.Object({}, { additionalProperties: false });
|
|
5558
|
+
var Name2 = Type4.String({ minLength: 1, maxLength: 80, description: "Unique among all members, people and Bots" });
|
|
5559
|
+
var BotName = Type4.String({ minLength: 1, maxLength: 80, description: "The Bot's name, as bots.list shows it" });
|
|
5560
|
+
var Model2 = Type4.Object({
|
|
5561
|
+
connection: Type4.String({ minLength: 1, maxLength: 200, description: "A connection's name or ID from models.list" }),
|
|
5562
|
+
model: Type4.String({ minLength: 1, maxLength: 200, description: "A model's ID or name in that connection" })
|
|
5563
|
+
}, { additionalProperties: false });
|
|
5564
|
+
var Thinking = Type4.Union(
|
|
5565
|
+
["auto", "off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => Type4.Literal(level)),
|
|
5566
|
+
{ description: "How much the model thinks; auto lets Jarvy choose. models.list gives the levels each model has" }
|
|
5567
|
+
);
|
|
5568
|
+
var Settings = {
|
|
5569
|
+
description: Type4.Optional(Type4.String({ maxLength: 1e3, description: "One line that other members see in members.list" })),
|
|
5570
|
+
persona: Type4.Optional(Type4.String({ maxLength: 16e3, description: "Its standing instructions: who it is and how it works (the Persona section of its prompt)" })),
|
|
5571
|
+
model: Type4.Optional(Model2),
|
|
5572
|
+
thinking: Type4.Optional(Thinking)
|
|
5573
|
+
};
|
|
5574
|
+
var Confirm = {
|
|
5575
|
+
askIn: Type4.String({ description: "ID of a chat the owner is in, where the confirmation card goes; usually the chat you were asked in" }),
|
|
5576
|
+
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" }),
|
|
5577
|
+
confirm: Type4.String({ minLength: 1, maxLength: 40, description: "Label of the button that deletes, in the owner's language" }),
|
|
5578
|
+
cancel: Type4.String({ minLength: 1, maxLength: 40, description: "Label of the button that keeps everything, in the owner's language" })
|
|
5579
|
+
};
|
|
5580
|
+
var after = "The owner first gets a card in askIn, and nothing is deleted unless they confirm; the result says what they chose.";
|
|
5581
|
+
var agentDefinitions = {
|
|
5582
|
+
"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 },
|
|
5583
|
+
"bots.create": {
|
|
5584
|
+
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.",
|
|
5585
|
+
schema: Type4.Object({ name: Name2, ...Settings }, { additionalProperties: false })
|
|
5586
|
+
},
|
|
5587
|
+
"bots.update": {
|
|
5588
|
+
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.",
|
|
5589
|
+
schema: Type4.Object({ bot: BotName, name: Type4.Optional(Name2), ...Settings }, { additionalProperties: false })
|
|
5590
|
+
},
|
|
5591
|
+
"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 }) },
|
|
5592
|
+
"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 }) },
|
|
5593
|
+
"bots.usage": {
|
|
5594
|
+
description: "A Bot's token use and cost over the last days (7 by default), per model.",
|
|
5595
|
+
schema: Type4.Object({ bot: BotName, days: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 365 })) }, { additionalProperties: false })
|
|
5596
|
+
},
|
|
5597
|
+
"bots.delete": {
|
|
5598
|
+
description: `Delete a Bot (not yourself). ${after} Its DMs and channel memberships go; its messages in channels and its workspace stay.`,
|
|
5599
|
+
schema: Type4.Object({ bot: BotName, ...Confirm }, { additionalProperties: false })
|
|
5600
|
+
},
|
|
5601
|
+
"chats.delete": {
|
|
5602
|
+
description: `Delete a channel you are in, with its threads, messages and files. ${after}`,
|
|
5603
|
+
schema: Type4.Object({ chatId: Type4.String({ description: "Channel ID" }), ...Confirm }, { additionalProperties: false })
|
|
5604
|
+
},
|
|
5605
|
+
"chats.removeMembers": {
|
|
5606
|
+
description: `Remove members from a channel you are in, by name (see members.list). ${after}`,
|
|
5607
|
+
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 })
|
|
5608
|
+
},
|
|
5609
|
+
"sections.delete": {
|
|
5610
|
+
description: `Delete a channel group; its channels stay, outside any group. ${after}`,
|
|
5611
|
+
schema: Type4.Object({ section: Type4.String({ minLength: 1, maxLength: 80, description: "The group's name or ID (see sections.list)" }), ...Confirm }, { additionalProperties: false })
|
|
5612
|
+
}
|
|
5613
|
+
};
|
|
5614
|
+
|
|
5382
5615
|
// src/server/service/bot/tools.ts
|
|
5383
|
-
var Gateway =
|
|
5384
|
-
action:
|
|
5385
|
-
tool:
|
|
5386
|
-
args:
|
|
5616
|
+
var Gateway = Type5.Object({
|
|
5617
|
+
action: Type5.Union([Type5.Literal("list"), Type5.Literal("describe"), Type5.Literal("call")]),
|
|
5618
|
+
tool: Type5.Optional(Type5.String()),
|
|
5619
|
+
args: Type5.Optional(Type5.Record(Type5.String(), Type5.Unknown()))
|
|
5387
5620
|
}, { additionalProperties: false });
|
|
5388
5621
|
var Tools = class {
|
|
5389
5622
|
constructor(scope) {
|
|
5390
5623
|
this.scope = scope;
|
|
5391
5624
|
this.tools = new ChatTools(scope);
|
|
5625
|
+
if (scope.agent) this.agent = new AgentTools({ ops: scope.agent, tools: this.tools });
|
|
5392
5626
|
}
|
|
5393
5627
|
send;
|
|
5394
5628
|
tools;
|
|
5629
|
+
/** The agent Bot's own tools (A48); other Bots have none. */
|
|
5630
|
+
agent;
|
|
5631
|
+
get catalog() {
|
|
5632
|
+
return this.agent ? { ...definitions, ...agentDefinitions } : definitions;
|
|
5633
|
+
}
|
|
5634
|
+
/** Posts a card and waits for the person's answer (messages.ask, and the agent's confirmations). */
|
|
5635
|
+
async ask(chatId, question, toolCallId, signal) {
|
|
5636
|
+
const sent = await this.send(chatId, { content: question.question, question, toolCallId, signal });
|
|
5637
|
+
this.scope.asking?.(true);
|
|
5638
|
+
try {
|
|
5639
|
+
return { messageId: sent.id, ...await this.scope.chats.questions.wait(this.scope.chats.message(this.scope.botId, sent.id), signal) };
|
|
5640
|
+
} finally {
|
|
5641
|
+
if (!signal?.aborted) this.scope.asking?.(false);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5395
5644
|
bindSend(send) {
|
|
5396
5645
|
this.send = send;
|
|
5397
5646
|
}
|
|
@@ -5405,16 +5654,18 @@ var Tools = class {
|
|
|
5405
5654
|
signal?.throwIfAborted();
|
|
5406
5655
|
if (raw.action === "list") {
|
|
5407
5656
|
if (raw.tool !== void 0 || raw.args !== void 0) throw new ChatError(400, "list does not accept tool or args");
|
|
5408
|
-
return Object.entries(
|
|
5657
|
+
return Object.entries(this.catalog).map(([name3, tool]) => ({ name: name3, description: tool.description }));
|
|
5409
5658
|
}
|
|
5410
|
-
if (!raw.tool || !Object.hasOwn(
|
|
5411
|
-
const name2 = raw.tool, definition =
|
|
5659
|
+
if (!raw.tool || !Object.hasOwn(this.catalog, raw.tool)) throw new ChatError(400, "Unknown chat tool; use list first");
|
|
5660
|
+
const name2 = raw.tool, definition = this.catalog[name2];
|
|
5412
5661
|
if (raw.action === "describe") {
|
|
5413
5662
|
if (raw.args !== void 0) throw new ChatError(400, "describe does not accept args");
|
|
5414
5663
|
return { name: name2, description: definition.description, parameters: definition.schema };
|
|
5415
5664
|
}
|
|
5416
5665
|
const args = raw.args;
|
|
5417
5666
|
if (!args || !Value.Check(definition.schema, args)) throw new ChatError(400, "Invalid tool arguments; use describe for the parameter schema");
|
|
5667
|
+
if (this.agent && Object.hasOwn(agentDefinitions, name2))
|
|
5668
|
+
return this.agent.call(name2, args, (chatId, question) => this.ask(chatId, question, toolCallId, signal));
|
|
5418
5669
|
switch (name2) {
|
|
5419
5670
|
case "history.read":
|
|
5420
5671
|
return readHistory(this.scope.memory, this.scope.workspace, args, signal);
|
|
@@ -5504,13 +5755,7 @@ var Tools = class {
|
|
|
5504
5755
|
multiple: parsed.multiple === true,
|
|
5505
5756
|
allowCustom: parsed.allowCustom === true
|
|
5506
5757
|
};
|
|
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
|
-
}
|
|
5758
|
+
return this.ask(parsed.chatId, question, toolCallId, signal);
|
|
5514
5759
|
}
|
|
5515
5760
|
}
|
|
5516
5761
|
}
|
|
@@ -5523,6 +5768,12 @@ import { join as join5, dirname as dirname4 } from "node:path";
|
|
|
5523
5768
|
|
|
5524
5769
|
// src/server/service/chat/envelope.ts
|
|
5525
5770
|
import { isAbsolute } from "node:path";
|
|
5771
|
+
|
|
5772
|
+
// src/server/shared/agent.ts
|
|
5773
|
+
var WELCOME_LANGUAGES = { en: "English", zh: "Chinese" };
|
|
5774
|
+
var welcomeLanguage = (value2) => typeof value2 === "string" && Object.hasOwn(WELCOME_LANGUAGES, value2) ? value2 : "en";
|
|
5775
|
+
|
|
5776
|
+
// src/server/service/chat/envelope.ts
|
|
5526
5777
|
var INVALID_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/u;
|
|
5527
5778
|
function escape(value2) {
|
|
5528
5779
|
if (INVALID_XML.test(value2)) throw new Error("Invalid XML character");
|
|
@@ -5538,6 +5789,7 @@ function remember(map, id, definition) {
|
|
|
5538
5789
|
map.set(id, definition);
|
|
5539
5790
|
}
|
|
5540
5791
|
var iso2 = (time) => new Date(time).toISOString().replace(".000Z", "Z");
|
|
5792
|
+
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.`;
|
|
5541
5793
|
function encodeNoticeBatch(botId, notices) {
|
|
5542
5794
|
if (!notices.length) throw new Error("Empty notice batch");
|
|
5543
5795
|
const chats = /* @__PURE__ */ new Map(), members = /* @__PURE__ */ new Map();
|
|
@@ -5546,14 +5798,16 @@ function encodeNoticeBatch(botId, notices) {
|
|
|
5546
5798
|
}
|
|
5547
5799
|
function noticeTail(notices, chats, members) {
|
|
5548
5800
|
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)}/>`);
|
|
5801
|
+
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
5802
|
remember(members, notice.actor.id, `<member${attribute("id", notice.actor.id)}${attribute("name", notice.actor.name)}${attribute("kind", notice.actor.kind)}/>`);
|
|
5551
5803
|
}
|
|
5552
5804
|
const latest = /* @__PURE__ */ new Map();
|
|
5553
5805
|
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
5806
|
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
5807
|
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
|
-
|
|
5808
|
+
const changes = notices.flatMap((notice) => notice.kind !== "change" ? [] : [`<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>`}`]);
|
|
5809
|
+
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);
|
|
5810
|
+
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("")}<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></changes>` : "") + onboard.join("");
|
|
5557
5811
|
}
|
|
5558
5812
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5559
5813
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5589,7 +5843,8 @@ var BotInbox = class {
|
|
|
5589
5843
|
this.chats = chats;
|
|
5590
5844
|
this.files = files;
|
|
5591
5845
|
}
|
|
5592
|
-
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run.
|
|
5846
|
+
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run. A digest delivery
|
|
5847
|
+
* (a change the agent Bot made, A48) is not work of its own: it rides along with the next batch. */
|
|
5593
5848
|
pending() {
|
|
5594
5849
|
return this.repository.hasPending(this.botId) || this.hasNotices();
|
|
5595
5850
|
}
|
|
@@ -5597,7 +5852,7 @@ var BotInbox = class {
|
|
|
5597
5852
|
return this.repository.claim(this.botId, selection, this.hasNotices());
|
|
5598
5853
|
}
|
|
5599
5854
|
hasNotices() {
|
|
5600
|
-
return this.chats.notices(this.botId).
|
|
5855
|
+
return this.chats.notices(this.botId).some((notice) => notice.urgency !== "digest");
|
|
5601
5856
|
}
|
|
5602
5857
|
reference(workspace) {
|
|
5603
5858
|
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
@@ -5621,6 +5876,11 @@ var BotInbox = class {
|
|
|
5621
5876
|
return notices.map((notice) => {
|
|
5622
5877
|
const chat = { id: notice.chatId, type: notice.chatType, title: notice.chatTitle };
|
|
5623
5878
|
if (notice.kind === "note") return { kind: "note", id: notice.id, chat, actor: notice.actor, version: Number(notice.detail), createdAt: notice.createdAt };
|
|
5879
|
+
if (notice.kind === "onboard") return { kind: "onboard", id: notice.id, chat, actor: notice.actor, language: welcomeLanguage(notice.detail), createdAt: notice.createdAt };
|
|
5880
|
+
if (notice.kind === "change") {
|
|
5881
|
+
const change = JSON.parse(notice.detail);
|
|
5882
|
+
return { kind: "change", id: notice.id, chat, actor: notice.actor, what: change.what, value: change.value, createdAt: notice.createdAt };
|
|
5883
|
+
}
|
|
5624
5884
|
const marked = notice.messageId ? this.chats.message(this.botId, notice.messageId) : null;
|
|
5625
5885
|
const text3 = marked ? previewQuotes(marked.content, marked.quotes).slice(0, 120) : "";
|
|
5626
5886
|
return { kind: "reaction", id: notice.id, chat, message: { id: notice.messageId ?? "", text: text3 }, actor: notice.actor, emoji: notice.detail, createdAt: notice.createdAt };
|
|
@@ -5868,6 +6128,14 @@ var AgentRuntime = class {
|
|
|
5868
6128
|
};
|
|
5869
6129
|
|
|
5870
6130
|
// src/server/service/bot/prompt.ts
|
|
6131
|
+
var AGENT_ROLE = `
|
|
6132
|
+
## Your role
|
|
6133
|
+
You are the owner's agent Bot. Besides working like any Bot, you help them set up and run Jarvy itself.
|
|
6134
|
+
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.
|
|
6135
|
+
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.
|
|
6136
|
+
Other Bots may ask you to do these things for them. Do the ordinary ones; deleting always waits for the owner's confirmation.
|
|
6137
|
+
You can't add model connections, keys or logins. The owner does that in Settings, so point them there when a model is missing.
|
|
6138
|
+
`;
|
|
5871
6139
|
function botPrompt(setup) {
|
|
5872
6140
|
const bot = { ...setup.identity(), workspacePath: setup.workspace.cwd };
|
|
5873
6141
|
return `# Persona
|
|
@@ -5878,7 +6146,7 @@ ${bot.persona}
|
|
|
5878
6146
|
## Environment
|
|
5879
6147
|
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
6148
|
Here your name is ${bot.name}, and your description is: ${bot.description}
|
|
5881
|
-
|
|
6149
|
+
${bot.agent ? AGENT_ROLE : ""}
|
|
5882
6150
|
## Language
|
|
5883
6151
|
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
6152
|
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.
|
|
@@ -6158,15 +6426,134 @@ var LiveTurns = class {
|
|
|
6158
6426
|
}
|
|
6159
6427
|
};
|
|
6160
6428
|
|
|
6429
|
+
// src/server/service/bot/agent.ts
|
|
6430
|
+
var same2 = (a, b) => a.trim().replace(/^@/, "").toLocaleLowerCase() === b.trim().toLocaleLowerCase();
|
|
6431
|
+
var AgentOps = class {
|
|
6432
|
+
constructor(bots, store, chats) {
|
|
6433
|
+
this.bots = bots;
|
|
6434
|
+
this.store = store;
|
|
6435
|
+
this.chats = chats;
|
|
6436
|
+
}
|
|
6437
|
+
get owner() {
|
|
6438
|
+
return this.store.userMemberId();
|
|
6439
|
+
}
|
|
6440
|
+
agentId = AGENT_BOT_ID;
|
|
6441
|
+
/** A Bot by name, ignoring case and a leading @. */
|
|
6442
|
+
bot(name2) {
|
|
6443
|
+
const bot = this.store.listBots().find((record2) => same2(name2, record2.name));
|
|
6444
|
+
if (!bot) throw new ChatError(404, `No Bot named ${name2} (see bots.list)`);
|
|
6445
|
+
return bot;
|
|
6446
|
+
}
|
|
6447
|
+
/** Connected models and their thinking levels; never credentials. */
|
|
6448
|
+
models() {
|
|
6449
|
+
return this.store.listModelConnections().filter((connection) => connection.models.length).map((connection) => ({
|
|
6450
|
+
connection: connection.displayName,
|
|
6451
|
+
connectionId: connection.id,
|
|
6452
|
+
models: connection.models.map((model) => ({ id: model.id, name: model.name, thinking: ["auto", ...thinkingOptions(model).levels] }))
|
|
6453
|
+
}));
|
|
6454
|
+
}
|
|
6455
|
+
selection(bot, input) {
|
|
6456
|
+
if (!input.model && input.thinking === void 0) return null;
|
|
6457
|
+
let connectionId = bot?.connectionId ?? null, modelId = bot?.modelId ?? "";
|
|
6458
|
+
if (input.model) {
|
|
6459
|
+
const { connection, model } = input.model;
|
|
6460
|
+
const found = this.store.listModelConnections().find((item) => item.id === connection || same2(connection, item.displayName));
|
|
6461
|
+
if (!found) throw new ChatError(404, `No connection named ${connection} (see models.list)`);
|
|
6462
|
+
const picked = found.models.find((item) => item.id === model || same2(model, item.name));
|
|
6463
|
+
if (!picked) throw new ChatError(404, `No model ${model} in ${found.displayName} (see models.list)`);
|
|
6464
|
+
connectionId = found.id;
|
|
6465
|
+
modelId = picked.id;
|
|
6466
|
+
}
|
|
6467
|
+
if (!connectionId) throw new ChatError(400, "This Bot has no model yet; give model too");
|
|
6468
|
+
return { connectionId, modelId, ...input.thinking === void 0 ? {} : { thinkingLevel: input.thinking } };
|
|
6469
|
+
}
|
|
6470
|
+
/** "model (connection)", as the agent and the changed Bot read it. */
|
|
6471
|
+
modelLabel(bot) {
|
|
6472
|
+
const connection = bot.connectionId ? this.store.getModelConnection(bot.connectionId) : null;
|
|
6473
|
+
const model = connection?.models.find((item) => item.id === bot.modelId);
|
|
6474
|
+
return model ? `${model.name} (${connection.displayName})` : bot.modelId;
|
|
6475
|
+
}
|
|
6476
|
+
async create(input) {
|
|
6477
|
+
const selection = this.selection(null, input);
|
|
6478
|
+
const created = this.bots.create(input.name);
|
|
6479
|
+
if (input.description !== void 0 || input.persona !== void 0)
|
|
6480
|
+
this.bots.get(created.id).identity.update({ name: created.name, description: input.description ?? "", persona: input.persona ?? "" });
|
|
6481
|
+
if (selection) await this.bots.selectModel(created.id, selection);
|
|
6482
|
+
this.bots.listChanged();
|
|
6483
|
+
return this.store.getBot(created.id);
|
|
6484
|
+
}
|
|
6485
|
+
async update(bot, input) {
|
|
6486
|
+
const selection = this.selection(bot, input);
|
|
6487
|
+
if (input.name !== void 0 || input.description !== void 0 || input.persona !== void 0)
|
|
6488
|
+
this.bots.get(bot.id).identity.update({ name: input.name ?? bot.name, description: input.description ?? bot.description, persona: input.persona ?? bot.systemPrompt });
|
|
6489
|
+
if (selection) await this.bots.selectModel(bot.id, selection);
|
|
6490
|
+
const after2 = this.store.getBot(bot.id), changes = [];
|
|
6491
|
+
if (after2.name !== bot.name) changes.push({ what: "name", value: after2.name });
|
|
6492
|
+
if (after2.description !== bot.description) changes.push({ what: "description", value: after2.description });
|
|
6493
|
+
if (after2.systemPrompt !== bot.systemPrompt) changes.push({ what: "persona", value: null });
|
|
6494
|
+
if (after2.connectionId !== bot.connectionId || after2.modelId !== bot.modelId) changes.push({ what: "model", value: this.modelLabel(after2) });
|
|
6495
|
+
if (after2.thinkingLevel !== bot.thinkingLevel) changes.push({ what: "thinking", value: after2.thinkingLevel });
|
|
6496
|
+
this.tell(bot.id, changes);
|
|
6497
|
+
this.bots.listChanged();
|
|
6498
|
+
return { bot: after2, changed: changes.map((change) => change.what) };
|
|
6499
|
+
}
|
|
6500
|
+
async run(bot, action) {
|
|
6501
|
+
if (action === "stop" && bot.id === AGENT_BOT_ID) throw new ChatError(400, "You can't stop yourself");
|
|
6502
|
+
const runtime = this.bots.get(bot.id);
|
|
6503
|
+
await (action === "start" ? runtime.start() : runtime.stop());
|
|
6504
|
+
this.tell(bot.id, [{ what: "run", value: action }]);
|
|
6505
|
+
return this.bots.status(bot.id).status;
|
|
6506
|
+
}
|
|
6507
|
+
usage(bot, days) {
|
|
6508
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
6509
|
+
return { since: new Date(since).toISOString(), models: this.bots.memory(bot.id).usage(since) };
|
|
6510
|
+
}
|
|
6511
|
+
/** Deletions run as the owner, who has just confirmed them on a card. */
|
|
6512
|
+
async deleteBot(bot) {
|
|
6513
|
+
if (bot.id === AGENT_BOT_ID) throw new ChatError(400, "You can't delete yourself");
|
|
6514
|
+
await this.bots.delete(bot.id, true);
|
|
6515
|
+
this.bots.listChanged();
|
|
6516
|
+
}
|
|
6517
|
+
async deleteChannel(chatId) {
|
|
6518
|
+
if (!await this.chats.delete(this.owner, chatId)) throw new ChatError(404, "Channel not found");
|
|
6519
|
+
await this.bots.chatDeleted(chatId, AGENT_BOT_ID);
|
|
6520
|
+
}
|
|
6521
|
+
removeMember(chatId, memberId) {
|
|
6522
|
+
this.chats.channels.removeMember(this.owner, chatId, memberId);
|
|
6523
|
+
}
|
|
6524
|
+
deleteSection(sectionId) {
|
|
6525
|
+
if (!this.chats.channels.deleteSection(this.owner, sectionId)) throw new ChatError(404, "Section not found");
|
|
6526
|
+
}
|
|
6527
|
+
/** The changed Bot is told with its next batch (digest: it is not woken for it), in its DM with the agent. */
|
|
6528
|
+
tell(botId, changes) {
|
|
6529
|
+
if (botId === AGENT_BOT_ID || !changes.length) return;
|
|
6530
|
+
const chatId = this.chats.openDirect(AGENT_BOT_ID, botId).id;
|
|
6531
|
+
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) });
|
|
6532
|
+
}
|
|
6533
|
+
/** The agent's first model: ask it, once, to welcome the owner in their DM, in their interface language. */
|
|
6534
|
+
onboard(language) {
|
|
6535
|
+
if (!this.store.agent.markOnboarded()) return;
|
|
6536
|
+
const chatId = this.chats.openDirect(this.owner, AGENT_BOT_ID).id;
|
|
6537
|
+
this.store.agent.notify({ botId: AGENT_BOT_ID, kind: "onboard", reason: "setup", urgency: "interrupt", chatId, actorId: this.owner, detail: language });
|
|
6538
|
+
this.bots.deliver(AGENT_BOT_ID, "interrupt");
|
|
6539
|
+
}
|
|
6540
|
+
};
|
|
6541
|
+
|
|
6161
6542
|
// src/server/service/bot/service.ts
|
|
6162
6543
|
var BotService = class {
|
|
6163
6544
|
constructor(deps) {
|
|
6164
6545
|
this.deps = deps;
|
|
6546
|
+
this.agent = new AgentOps(this, deps.store, deps.chats);
|
|
6165
6547
|
}
|
|
6166
6548
|
bots = /* @__PURE__ */ new Map();
|
|
6167
6549
|
lives = /* @__PURE__ */ new Map();
|
|
6168
6550
|
closed = false;
|
|
6169
6551
|
started = false;
|
|
6552
|
+
/** What only the agent Bot does (A48). */
|
|
6553
|
+
agent;
|
|
6554
|
+
listChanged() {
|
|
6555
|
+
this.deps.listChanged?.();
|
|
6556
|
+
}
|
|
6170
6557
|
get(id) {
|
|
6171
6558
|
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
6172
6559
|
const record2 = this.deps.store.getBot(id);
|
|
@@ -6193,7 +6580,16 @@ var BotService = class {
|
|
|
6193
6580
|
model: new Model(id, store, resolveThinking),
|
|
6194
6581
|
workspace,
|
|
6195
6582
|
memory,
|
|
6196
|
-
tools: new Tools({
|
|
6583
|
+
tools: new Tools({
|
|
6584
|
+
botId: id,
|
|
6585
|
+
chats,
|
|
6586
|
+
files,
|
|
6587
|
+
memory,
|
|
6588
|
+
workspace,
|
|
6589
|
+
asking: (on) => runtime.waitingForUser(on),
|
|
6590
|
+
roster: () => this.roster(),
|
|
6591
|
+
...id === AGENT_BOT_ID ? { agent: this.agent } : {}
|
|
6592
|
+
}),
|
|
6197
6593
|
agentRuntime: runtime,
|
|
6198
6594
|
state: new BotState(() => this.deps.changed(id))
|
|
6199
6595
|
}, { deliver: async (message) => {
|
|
@@ -6215,7 +6611,7 @@ var BotService = class {
|
|
|
6215
6611
|
}
|
|
6216
6612
|
details(record2) {
|
|
6217
6613
|
const model = record2.connectionId ? this.deps.store.getModelConnection(record2.connectionId)?.models.find((model2) => model2.id === record2.modelId) : void 0;
|
|
6218
|
-
return { ...record2, effectiveThinkingLevel: model ? resolveThinking(model, record2.thinkingLevel) : null };
|
|
6614
|
+
return { ...record2, effectiveThinkingLevel: model ? resolveThinking(model, record2.thinkingLevel) : null, agent: record2.id === AGENT_BOT_ID };
|
|
6219
6615
|
}
|
|
6220
6616
|
owns(id) {
|
|
6221
6617
|
return this.deps.store.getBot(id) !== null;
|
|
@@ -6300,6 +6696,7 @@ var BotService = class {
|
|
|
6300
6696
|
return {
|
|
6301
6697
|
id: record2.id,
|
|
6302
6698
|
name: record2.name,
|
|
6699
|
+
agent: record2.id === AGENT_BOT_ID,
|
|
6303
6700
|
status,
|
|
6304
6701
|
runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
|
|
6305
6702
|
errorCode: store.runtime.latestErrorCode(record2.id)
|
|
@@ -6311,18 +6708,22 @@ var BotService = class {
|
|
|
6311
6708
|
const bot = this.bots.get(id);
|
|
6312
6709
|
return { ...bot?.status() ?? { status: "stop", activity: null, context: null, error: null }, botId: id, runId: bot?.agentRuntime.activeRunId ?? null };
|
|
6313
6710
|
}
|
|
6711
|
+
/** `language` is the browser's interface language: the agent Bot's first model asks it to welcome the owner in it (A48). */
|
|
6314
6712
|
async selectModel(id, input) {
|
|
6315
6713
|
const selection = this.deps.store.validateSelection(id, input);
|
|
6316
6714
|
if (!selection) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6317
6715
|
await this.get(id).changeModel(selection);
|
|
6318
6716
|
this.resume(id);
|
|
6717
|
+
if (id === AGENT_BOT_ID) this.agent.onboard(welcomeLanguage(input.language));
|
|
6319
6718
|
return this.deps.store.getBot(id);
|
|
6320
6719
|
}
|
|
6321
6720
|
connectionChanged(connectionId) {
|
|
6322
6721
|
for (const bot of this.deps.store.listBots()) if (bot.connectionId === connectionId) this.resume(bot.id);
|
|
6323
6722
|
}
|
|
6324
|
-
|
|
6325
|
-
|
|
6723
|
+
/** A deleted chat ends the turns handling it; `except` is the agent Bot that deleted it from inside its turn. */
|
|
6724
|
+
async chatDeleted(chatId, except) {
|
|
6725
|
+
for (const [id, bot] of this.bots) {
|
|
6726
|
+
if (id === except) continue;
|
|
6326
6727
|
const runId = bot.agentRuntime.activeRunId;
|
|
6327
6728
|
if (runId && this.deps.store.runtime.inputs(runId).some((input) => input.chatId === chatId)) await bot.abort();
|
|
6328
6729
|
}
|
|
@@ -6330,6 +6731,7 @@ var BotService = class {
|
|
|
6330
6731
|
/** Deletes a Bot: stop it, drop its data, keep its messages. `keepFiles` leaves the workspace (and
|
|
6331
6732
|
* the History database) on disk, since it may hold work worth keeping. */
|
|
6332
6733
|
async delete(id, keepFiles) {
|
|
6734
|
+
if (id === AGENT_BOT_ID) throw new RuntimeFault("agent_undeletable", "The agent Bot can't be deleted", 409);
|
|
6333
6735
|
const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
|
|
6334
6736
|
await bot.stop();
|
|
6335
6737
|
this.bots.delete(id);
|
|
@@ -6374,6 +6776,7 @@ var Application = class _Application {
|
|
|
6374
6776
|
files: this.files,
|
|
6375
6777
|
credentials: this.credentials,
|
|
6376
6778
|
changed: (id) => this.events.publish({ kind: "bot", id, status: this.bots.status(id) }),
|
|
6779
|
+
listChanged: () => this.events.publish({ kind: "bots" }),
|
|
6377
6780
|
factory
|
|
6378
6781
|
});
|
|
6379
6782
|
this.auth = new CredentialLogins(store, this.credentials, (id) => this.bots.connectionChanged(id));
|