@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.
@@ -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 after = Number(c.req.query("after") ?? 0);
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"), after, limit);
472
- return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ?? after, hasMore: messages.length === limit });
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 randomInt2, randomUUID as randomUUID7, scryptSync, timingSafeEqual } from "node:crypto";
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 '${DEFAULT_BOT_ID}',name,'',system_prompt,'',0 FROM bot_settings WHERE id=1;
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='${DEFAULT_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 > 18) throw new Error(`Unsupported database version: ${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, after = 0, limit = 1e3) {
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, after, limit);
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[randomInt2(BOT_COLORS.length)]);
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 same2 = all.filter((row) => row.model.id.toLowerCase() === q || strip(row.model.id) === strip(q));
2788
- return same2.length ? same2 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
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, after, limit);
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, after = 0, limit = 1e3) {
3839
- if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
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, after, limit);
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 Type4 } from "typebox";
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 = Type4.Object({
5384
- action: Type4.Union([Type4.Literal("list"), Type4.Literal("describe"), Type4.Literal("call")]),
5385
- tool: Type4.Optional(Type4.String()),
5386
- args: Type4.Optional(Type4.Record(Type4.String(), Type4.Unknown()))
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(definitions).map(([name3, tool]) => ({ name: name3, description: tool.description }));
5657
+ return Object.entries(this.catalog).map(([name3, tool]) => ({ name: name3, description: tool.description }));
5409
5658
  }
5410
- if (!raw.tool || !Object.hasOwn(definitions, raw.tool)) throw new ChatError(400, "Unknown chat tool; use list first");
5411
- const name2 = raw.tool, definition = definitions[name2];
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
- const sent = await this.send(parsed.chatId, { content: parsed.question, question, toolCallId, signal });
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
- 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>` : "");
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).length > 0;
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({ botId: id, chats, files, memory, workspace, asking: (on) => runtime.waitingForUser(on), roster: () => this.roster() }),
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
- async chatDeleted(chatId) {
6325
- for (const bot of this.bots.values()) {
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));