pi-roundtable 0.24.1 → 0.25.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/docs/plugins.md +45 -1
  3. package/package.json +1 -1
  4. package/src/core/agents/agent-book.ts +155 -0
  5. package/src/core/agents/agent-ports.ts +5 -1
  6. package/src/core/agents/agent-schema.ts +3 -0
  7. package/src/core/agents/agent-statements.ts +179 -0
  8. package/src/core/agents/agent-store.ts +184 -191
  9. package/src/core/agents/agent-team-fixture.ts +21 -2
  10. package/src/core/agents/agent-team.ts +42 -36
  11. package/src/core/agents/agent-tools.ts +12 -6
  12. package/src/core/agents/team-editing.ts +150 -32
  13. package/src/core/agents/team-keys.ts +17 -0
  14. package/src/core/agents/team-lifecycle.ts +96 -23
  15. package/src/core/agents/team-options.ts +6 -3
  16. package/src/core/agents/team-selection.ts +28 -0
  17. package/src/core/builtin/agent-server.ts +29 -5
  18. package/src/core/builtin/prompts.ts +40 -0
  19. package/src/core/builtin/skills.ts +9 -5
  20. package/src/core/define-roundtable.ts +3 -0
  21. package/src/core/i18n/agent-panel.ts +4 -0
  22. package/src/core/i18n/cards.ts +1 -1
  23. package/src/core/modules/prompts/prompt-documents.ts +425 -0
  24. package/src/core/modules/prompts/prompt-history.ts +103 -0
  25. package/src/core/modules/skills/skill-contract.ts +52 -0
  26. package/src/core/modules/skills/skill-document-files.ts +66 -0
  27. package/src/core/modules/skills/skill-registry.ts +136 -8
  28. package/src/core/modules/skills/skill-rules.ts +94 -0
  29. package/src/core/modules/skills/skill-store.ts +88 -17
  30. package/src/core/modules/skills/skill-tools.ts +18 -10
  31. package/src/core/runtime/extensions/agent-prompt.ts +20 -5
  32. package/src/core/runtime/extensions/persona-prompt.ts +39 -0
  33. package/src/core/runtime/extensions/prompt-read.ts +22 -0
  34. package/src/core/runtime/session-factory.ts +51 -35
  35. package/src/core/services.ts +88 -33
  36. package/src/index.ts +13 -0
@@ -3,7 +3,12 @@ import type { CharacterSpec } from "../../characters/index.ts";
3
3
  import type { Migration } from "../db/migrations.ts";
4
4
  import { AgentError } from "../domain/errors.ts";
5
5
  import type { ThinkingLevel } from "../models.ts";
6
+ import {
7
+ PromptHistory,
8
+ type PromptVersion,
9
+ } from "../modules/prompts/prompt-history.ts";
6
10
  import type { AgentDirectory } from "../services.ts";
11
+ import { AgentBook } from "./agent-book.ts";
7
12
  import {
8
13
  type Agent,
9
14
  type AgentGroup,
@@ -17,7 +22,6 @@ import {
17
22
  checkPrompt,
18
23
  checkThinking,
19
24
  type GroupMessage,
20
- MAX_GROUP_MEMBERS,
21
25
  MIN_GROUP_MEMBERS,
22
26
  } from "./agent-rules.ts";
23
27
  import {
@@ -25,9 +29,9 @@ import {
25
29
  agentMigrations,
26
30
  agentsMigration,
27
31
  type GroupRow,
28
- toAgent,
29
32
  toGroup,
30
33
  } from "./agent-schema.ts";
34
+ import { AgentStatements } from "./agent-statements.ts";
31
35
  import { GroupMessages } from "./group-messages.ts";
32
36
 
33
37
  export {
@@ -55,22 +59,23 @@ import { isCheckDatabase } from "../db/check-database.ts";
55
59
  * Agents, groups, and group messages, in the database. Agents and groups are also held
56
60
  * in memory, because every inbound message asks and only this process changes them.
57
61
  */
58
- export class PgAgentStore implements AgentDirectory {
62
+ export class PgAgentStore extends AgentBook implements AgentDirectory {
59
63
  readonly #sql: SQL;
60
- readonly #agents = new Map<string, Agent>();
61
- /** Agents whose character column holds a value, readable or not. */
62
- readonly #dressed = new Set<string>();
63
- readonly #groups = new Map<string, AgentGroup>();
64
64
  /** The tail of the agent and group writes, which run one at a time so none reads a map another is about to change. */
65
65
  #queue: Promise<unknown> = Promise.resolve();
66
66
 
67
67
  readonly #guild: string;
68
+ readonly #statements: AgentStatements;
68
69
  readonly #messages: GroupMessages;
70
+ readonly #prompts: PromptHistory;
69
71
 
70
72
  private constructor(sql: SQL, guildId: string) {
73
+ super();
71
74
  this.#sql = sql;
72
75
  this.#guild = guildId;
76
+ this.#statements = new AgentStatements(sql, guildId);
73
77
  this.#messages = new GroupMessages(sql, guildId);
78
+ this.#prompts = new PromptHistory(sql, guildId);
74
79
  }
75
80
 
76
81
  /** The store's tables; the host runs this before any store attaches. */
@@ -94,11 +99,11 @@ export class PgAgentStore implements AgentDirectory {
94
99
  .#sql`SELECT * FROM agents WHERE guild_id = ${this.#guild}`;
95
100
  const groups: GroupRow[] = await this
96
101
  .#sql`SELECT * FROM agent_groups WHERE guild_id = ${this.#guild}`;
97
- this.#agents.clear();
98
- this.#groups.clear();
99
- this.#dressed.clear();
100
- for (const row of agents) this.#keepAgentRow(row);
101
- for (const row of groups) this.#groups.set(row.name, toGroup(row));
102
+ this.agentMap.clear();
103
+ this.groupMap.clear();
104
+ this.dressed.clear();
105
+ for (const row of agents) this.keepAgentRow(row);
106
+ for (const row of groups) this.groupMap.set(row.name, toGroup(row));
102
107
  }
103
108
 
104
109
  /** Inserts seed agents that do not exist yet; existing rows, edited or not, are left alone. */
@@ -106,15 +111,14 @@ export class PgAgentStore implements AgentDirectory {
106
111
  return this.#serial(async () => {
107
112
  const added: string[] = [];
108
113
  for (const seed of seeds) {
109
- const rows: AgentRow[] = await this.#sql`
110
- INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id)
111
- VALUES (${this.#guild}, ${seed.name}, ${seed.displayName}, ${seed.prompt}, ${seed.avatarPrompt ?? ""},
112
- ${seed.channelId ?? null})
113
- ON CONFLICT DO NOTHING
114
- RETURNING *`;
114
+ const rows = await this.#statements.insertSeed({
115
+ ...seed,
116
+ avatarPrompt: seed.avatarPrompt ?? "",
117
+ channelId: seed.channelId ?? null,
118
+ });
115
119
  // Only the inserted rows join the map: reloading it whole would drop a write that
116
120
  // committed after its SELECT.
117
- if (rows[0]) added.push(this.#keepAgent(rows[0]).name);
121
+ if (rows[0]) added.push(this.keepAgent(rows[0]).name);
118
122
  }
119
123
  return added;
120
124
  });
@@ -126,66 +130,6 @@ export class PgAgentStore implements AgentDirectory {
126
130
  return run;
127
131
  }
128
132
 
129
- agents(): Agent[] {
130
- return [...this.#agents.values()];
131
- }
132
-
133
- agent(name: string): Agent | undefined {
134
- return this.#agents.get(name);
135
- }
136
-
137
- /** The active agent with that name, or an error the caller can show. */
138
- activeAgent(name: string): Agent {
139
- const agent = this.#agents.get(name);
140
- if (!agent)
141
- throw new AgentError(
142
- `There is no agent "${name}". Active agents: ${this.#activeNames()}.`,
143
- );
144
- if (agent.status !== "active")
145
- throw new AgentError(
146
- `Agent "${name}" is archived. Active agents: ${this.#activeNames()}.`,
147
- );
148
- return agent;
149
- }
150
-
151
- agentByChannel(channelId: string): Agent | undefined {
152
- for (const agent of this.#agents.values())
153
- if (agent.channelId === channelId && agent.status === "active")
154
- return agent;
155
- return undefined;
156
- }
157
-
158
- groups(): AgentGroup[] {
159
- return [...this.#groups.values()];
160
- }
161
-
162
- group(name: string): AgentGroup | undefined {
163
- return this.#groups.get(name);
164
- }
165
-
166
- groupByChannel(channelId: string): AgentGroup | undefined {
167
- for (const group of this.#groups.values())
168
- if (group.channelId === channelId && group.status === "active")
169
- return group;
170
- return undefined;
171
- }
172
-
173
- #activeNames(): string {
174
- return (
175
- this.agents()
176
- .flatMap((a) => (a.status === "active" ? [a.name] : []))
177
- .join(", ") || "none"
178
- );
179
- }
180
-
181
- /** Names are shared between agents and groups, archived ones included. */
182
- #checkFree(name: string): void {
183
- if (this.#agents.has(name) || this.#groups.has(name))
184
- throw new AgentError(
185
- `The name "${name}" is taken, archived agents and groups included.`,
186
- );
187
- }
188
-
189
133
  async createAgent(agent: {
190
134
  name: string;
191
135
  displayName: string;
@@ -203,23 +147,20 @@ export class PgAgentStore implements AgentDirectory {
203
147
  checkPrompt(agent.prompt);
204
148
  const avatarPrompt = agent.avatarPrompt ?? "";
205
149
  if (avatarPrompt !== "") checkAvatarPrompt(avatarPrompt);
206
- this.#checkFree(agent.name);
207
- const rows: AgentRow[] = await this.#sql`
208
- INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id, avatar_hash, character)
209
- VALUES (${this.#guild}, ${agent.name}, ${agent.displayName.trim()}, ${agent.prompt}, ${avatarPrompt},
210
- ${agent.channelId ?? null}, ${agent.avatarHash ?? null},
211
- ${agent.character ?? null}::jsonb)
212
- RETURNING *`;
213
- return this.#keepAgent(rows[0]);
150
+ this.checkFree(agent.name);
151
+ const rows = await this.#statements.insertAgent({
152
+ name: agent.name,
153
+ displayName: agent.displayName.trim(),
154
+ prompt: agent.prompt,
155
+ avatarPrompt,
156
+ channelId: agent.channelId ?? null,
157
+ avatarHash: agent.avatarHash ?? null,
158
+ character: agent.character ?? null,
159
+ });
160
+ return this.keepAgent(rows[0]);
214
161
  });
215
162
  }
216
163
 
217
- /** A name that createAgent would accept, checked before its channel is made. */
218
- checkNewName(name: string): void {
219
- checkAgentName(name);
220
- this.#checkFree(name);
221
- }
222
-
223
164
  async updateAgent(
224
165
  name: string,
225
166
  change: {
@@ -233,6 +174,8 @@ export class PgAgentStore implements AgentDirectory {
233
174
  model?: string | null;
234
175
  thinking?: ThinkingLevel | null;
235
176
  },
177
+ /** Who made the change; a changed prompt is kept in the agent's prompt history under this name. */
178
+ by = "unknown",
236
179
  ): Promise<Agent> {
237
180
  return this.#serial(async () => {
238
181
  const current = this.activeAgent(name);
@@ -249,65 +192,112 @@ export class PgAgentStore implements AgentDirectory {
249
192
  change.thinking === undefined
250
193
  ? (current.thinking ?? null)
251
194
  : change.thinking;
252
- const rows: AgentRow[] = await this.#sql`
253
- UPDATE agents SET
254
- display_name = ${change.displayName?.trim() ?? current.displayName},
255
- prompt = ${change.prompt ?? current.prompt},
256
- avatar_prompt = ${change.avatarPrompt ?? current.avatarPrompt},
257
- avatar_hash = ${change.avatarHash ?? current.avatarHash ?? null},
258
- character = CASE WHEN ${change.character !== undefined} THEN ${change.character ?? null}::jsonb ELSE character END,
259
- channel_id = ${change.channelId ?? current.channelId ?? null},
260
- model = ${model},
261
- thinking = ${thinking},
262
- updated_at = now()
263
- WHERE guild_id = ${this.#guild} AND name = ${name}
264
- RETURNING *`;
265
- return this.#keepAgent(rows[0]);
195
+ const row = {
196
+ displayName: change.displayName?.trim() ?? current.displayName,
197
+ prompt: change.prompt ?? current.prompt,
198
+ avatarPrompt: change.avatarPrompt ?? current.avatarPrompt,
199
+ avatarHash: change.avatarHash ?? current.avatarHash ?? null,
200
+ characterSet: change.character !== undefined,
201
+ character: change.character ?? null,
202
+ channelId: change.channelId ?? current.channelId ?? null,
203
+ model,
204
+ thinking,
205
+ };
206
+ const prompt = change.prompt;
207
+ // A new prompt and its history row commit together, so no change goes unrecorded.
208
+ const rows =
209
+ prompt !== undefined && prompt !== current.prompt
210
+ ? await this.#sql.begin(async (tx) => {
211
+ const written = await new AgentStatements(
212
+ tx,
213
+ this.#guild,
214
+ ).updateAgent(name, row);
215
+ await this.#prompts.record(
216
+ "agent",
217
+ name,
218
+ { action: "set", text: prompt, previous: current.prompt, by },
219
+ tx,
220
+ );
221
+ return written;
222
+ })
223
+ : await this.#statements.updateAgent(name, row);
224
+ return this.keepAgent(rows[0]);
266
225
  });
267
226
  }
268
227
 
269
- /** Archives an agent and takes it out of every group; says which groups it left. */
228
+ /** The changes of an agent's prompt, newest first. */
229
+ promptHistory(name: string, limit?: number): Promise<PromptVersion[]> {
230
+ return this.#prompts.list("agent", name, limit);
231
+ }
232
+
233
+ /**
234
+ * Archives an agent and takes it out of every group; says which groups it left. A group this
235
+ * would leave with too few members is archived whole, its members kept, so it can be restored
236
+ * once they are all active again.
237
+ */
270
238
  async archiveAgent(name: string): Promise<AgentGroup[]> {
271
239
  return this.#serial(async () => {
272
- const rows: AgentRow[] = await this.#sql`
273
- UPDATE agents SET status = 'archived', updated_at = now()
274
- WHERE guild_id = ${this.#guild} AND name = ${name} RETURNING *`;
275
- this.#keepAgent(rows[0]);
240
+ this.keepAgent((await this.#statements.archiveAgent(name))[0]);
276
241
  const left: AgentGroup[] = [];
277
242
  for (const group of this.groups()) {
278
243
  if (group.status !== "active" || !group.members.includes(name))
279
244
  continue;
280
245
  const members = group.members.filter((m) => m !== name);
246
+ if (members.length < MIN_GROUP_MEMBERS) {
247
+ left.push(await this.#writeGroup(group.name, { status: "archived" }));
248
+ continue;
249
+ }
281
250
  const host = group.host === name ? (members[0] ?? name) : group.host;
282
- left.push(
283
- await this.#writeGroup(group.name, {
284
- members,
285
- host,
286
- status: members.length < MIN_GROUP_MEMBERS ? "archived" : "active",
287
- }),
288
- );
251
+ left.push(await this.#writeGroup(group.name, { members, host }));
289
252
  }
290
253
  return left;
291
254
  });
292
255
  }
293
256
 
294
- #keepAgent(row: AgentRow | undefined): Agent {
295
- if (!row) throw new AgentError("the agent was not found");
296
- return this.#keepAgentRow(row);
297
- }
298
-
299
- #keepAgentRow(row: AgentRow): Agent {
300
- const agent = toAgent(row);
301
- this.#agents.set(agent.name, agent);
302
- if (row.character !== null && row.character !== undefined)
303
- this.#dressed.add(agent.name);
304
- else this.#dressed.delete(agent.name);
305
- return agent;
257
+ /**
258
+ * Returns an archived agent to the active ones, on a new channel when its old one is gone. It
259
+ * does not rejoin the groups it left.
260
+ */
261
+ async restoreAgent(name: string, channelId?: string): Promise<Agent> {
262
+ return this.#serial(async () => {
263
+ const current = this.agentMap.get(name);
264
+ if (!current) throw new AgentError(`There is no agent "${name}".`);
265
+ if (current.status === "active")
266
+ throw new AgentError(`"${name}" is not archived.`);
267
+ const rows = await this.#statements.restoreAgent(
268
+ name,
269
+ channelId ?? current.channelId ?? null,
270
+ );
271
+ return this.keepAgent(rows[0]);
272
+ });
306
273
  }
307
274
 
308
- /** Whether the stored column holds a character, even one this version cannot read. */
309
- hasStoredCharacter(name: string): boolean {
310
- return this.#dressed.has(name);
275
+ /**
276
+ * Deletes an archived agent, its group cursors and its prompt history, and takes it out of the
277
+ * archived groups that still list it; its name is free again, and a new agent of that name
278
+ * starts with an empty history. An active agent is never deleted: archive it first.
279
+ */
280
+ async deleteAgent(name: string): Promise<void> {
281
+ return this.#serial(async () => {
282
+ const current = this.agentMap.get(name);
283
+ if (!current) throw new AgentError(`There is no agent "${name}".`);
284
+ if (current.status === "active")
285
+ throw new AgentError(
286
+ `"${name}" is active; archive it before deleting it.`,
287
+ );
288
+ await this.#statements.deleteAgent(name);
289
+ this.agentMap.delete(name);
290
+ this.dressed.delete(name);
291
+ // An archived group still lists the agent; a new agent of this name must not join it unasked.
292
+ for (const group of this.groups()) {
293
+ if (!group.members.includes(name)) continue;
294
+ const members = group.members.filter((m) => m !== name);
295
+ await this.#writeGroup(group.name, {
296
+ members,
297
+ host: group.host === name ? (members[0] ?? "") : group.host,
298
+ });
299
+ }
300
+ });
311
301
  }
312
302
 
313
303
  /**
@@ -320,12 +310,13 @@ export class PgAgentStore implements AgentDirectory {
320
310
  avatarHash: string,
321
311
  ): Promise<boolean> {
322
312
  return this.#serial(async () => {
323
- const rows: AgentRow[] = await this.#sql`
324
- UPDATE agents SET character = ${character}::jsonb, avatar_hash = ${avatarHash}, updated_at = now()
325
- WHERE guild_id = ${this.#guild} AND name = ${name} AND character IS NULL
326
- RETURNING *`;
313
+ const rows = await this.#statements.dressIfUndressed(
314
+ name,
315
+ character,
316
+ avatarHash,
317
+ );
327
318
  if (!rows[0]) return false;
328
- this.#keepAgentRow(rows[0]);
319
+ this.keepAgentRow(rows[0]);
329
320
  return true;
330
321
  });
331
322
  }
@@ -341,38 +332,17 @@ export class PgAgentStore implements AgentDirectory {
341
332
  avatarHash: string,
342
333
  ): Promise<boolean> {
343
334
  return this.#serial(async () => {
344
- const rows: AgentRow[] = await this.#sql`
345
- UPDATE agents SET avatar_hash = ${avatarHash}, updated_at = now()
346
- WHERE guild_id = ${this.#guild} AND name = ${name} AND status = 'active'
347
- AND character = ${character}::jsonb
348
- RETURNING *`;
335
+ const rows = await this.#statements.setAvatarIfLook(
336
+ name,
337
+ character,
338
+ avatarHash,
339
+ );
349
340
  if (!rows[0]) return false;
350
- this.#keepAgentRow(rows[0]);
341
+ this.keepAgentRow(rows[0]);
351
342
  return true;
352
343
  });
353
344
  }
354
345
 
355
- #checkMembers(members: readonly string[], host: string): void {
356
- if (
357
- new Set(members).size !== members.length ||
358
- members.length < MIN_GROUP_MEMBERS ||
359
- members.length > MAX_GROUP_MEMBERS
360
- )
361
- throw new AgentError(
362
- `A group has ${MIN_GROUP_MEMBERS} to ${MAX_GROUP_MEMBERS} different members.`,
363
- );
364
- for (const member of members) this.activeAgent(member);
365
- if (!members.includes(host))
366
- throw new AgentError(`The host "${host}" must be one of the members.`);
367
- }
368
-
369
- /** Checks a new group before its channel is made. */
370
- checkNewGroup(name: string, members: readonly string[], host: string): void {
371
- checkAgentName(name);
372
- this.#checkFree(name);
373
- this.#checkMembers(members, host);
374
- }
375
-
376
346
  async createGroup(group: {
377
347
  name: string;
378
348
  displayName: string;
@@ -384,12 +354,12 @@ export class PgAgentStore implements AgentDirectory {
384
354
  return this.#serial(async () => {
385
355
  this.checkNewGroup(group.name, group.members, group.host);
386
356
  checkDisplayName(group.displayName);
387
- const rows: GroupRow[] = await this.#sql`
388
- INSERT INTO agent_groups (guild_id, name, display_name, channel_id, members, host)
389
- VALUES (${this.#guild}, ${group.name}, ${group.displayName.trim()}, ${group.channelId ?? null},
390
- ${group.members.join(",")}, ${group.host})
391
- RETURNING *`;
392
- return this.#keepGroup(rows[0]);
357
+ const rows = await this.#statements.insertGroup({
358
+ ...group,
359
+ displayName: group.displayName.trim(),
360
+ channelId: group.channelId ?? null,
361
+ });
362
+ return this.keepGroup(rows[0]);
393
363
  });
394
364
  }
395
365
 
@@ -398,7 +368,7 @@ export class PgAgentStore implements AgentDirectory {
398
368
  change: { displayName?: string; members?: string[]; host?: string },
399
369
  ): Promise<AgentGroup> {
400
370
  return this.#serial(async () => {
401
- const current = this.#groups.get(name);
371
+ const current = this.groupMap.get(name);
402
372
  if (current?.status !== "active")
403
373
  throw new AgentError(`There is no active group "${name}".`);
404
374
  if (change.displayName !== undefined)
@@ -407,7 +377,7 @@ export class PgAgentStore implements AgentDirectory {
407
377
  const host =
408
378
  change.host ??
409
379
  (members.includes(current.host) ? current.host : (members[0] ?? ""));
410
- this.#checkMembers(members, host);
380
+ this.checkMembers(members, host);
411
381
  return this.#writeGroup(name, {
412
382
  displayName: change.displayName?.trim() ?? current.displayName,
413
383
  members,
@@ -421,6 +391,38 @@ export class PgAgentStore implements AgentDirectory {
421
391
  return this.#serial(() => this.#writeGroup(name, { status: "archived" }));
422
392
  }
423
393
 
394
+ /**
395
+ * Returns an archived group to the active ones, on a new channel when its old one is gone. Its
396
+ * members must all be active still and number enough; otherwise it refuses and stays archived.
397
+ */
398
+ async restoreGroup(name: string, channelId?: string): Promise<AgentGroup> {
399
+ return this.#serial(async () => {
400
+ const current = this.checkRestorableGroup(name);
401
+ const rows = await this.#statements.restoreGroup(
402
+ name,
403
+ channelId ?? current.channelId ?? null,
404
+ );
405
+ return this.keepGroup(rows[0]);
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Deletes an archived group with its messages and cursors, in the Discord channel's log and in
411
+ * each room's (`<group>@<surface>`); its name is free again. An active group is never deleted.
412
+ */
413
+ async deleteGroup(name: string): Promise<void> {
414
+ return this.#serial(async () => {
415
+ const current = this.groupMap.get(name);
416
+ if (!current) throw new AgentError(`There is no group "${name}".`);
417
+ if (current.status === "active")
418
+ throw new AgentError(
419
+ `"${name}" is active; archive it before deleting it.`,
420
+ );
421
+ await this.#statements.deleteGroup(name);
422
+ this.groupMap.delete(name);
423
+ });
424
+ }
425
+
424
426
  async #writeGroup(
425
427
  name: string,
426
428
  change: {
@@ -430,24 +432,15 @@ export class PgAgentStore implements AgentDirectory {
430
432
  status?: AgentStatus;
431
433
  },
432
434
  ): Promise<AgentGroup> {
433
- const current = this.#groups.get(name);
435
+ const current = this.groupMap.get(name);
434
436
  if (!current) throw new AgentError(`There is no group "${name}".`);
435
- const rows: GroupRow[] = await this.#sql`
436
- UPDATE agent_groups SET
437
- display_name = ${change.displayName ?? current.displayName},
438
- members = ${(change.members ?? current.members).join(",")},
439
- host = ${change.host ?? current.host},
440
- status = ${change.status ?? current.status}
441
- WHERE guild_id = ${this.#guild} AND name = ${name}
442
- RETURNING *`;
443
- return this.#keepGroup(rows[0]);
444
- }
445
-
446
- #keepGroup(row: GroupRow | undefined): AgentGroup {
447
- if (!row) throw new AgentError("the group was not found");
448
- const group = toGroup(row);
449
- this.#groups.set(group.name, group);
450
- return group;
437
+ const rows = await this.#statements.updateGroup(name, {
438
+ displayName: change.displayName ?? current.displayName,
439
+ members: change.members ?? current.members,
440
+ host: change.host ?? current.host,
441
+ status: change.status ?? current.status,
442
+ });
443
+ return this.keepGroup(rows[0]);
451
444
  }
452
445
 
453
446
  appendGroupMessage(
@@ -203,6 +203,10 @@ export class FakeRuntime implements AgentTurnRunner {
203
203
  async startFresh(session: ChannelKey) {
204
204
  this.fresh.push(session);
205
205
  }
206
+ deleted: ChannelKey[] = [];
207
+ async deleteConversation(session: ChannelKey) {
208
+ this.deleted.push(session);
209
+ }
206
210
  usage = new Map<ChannelKey, ContextUse>();
207
211
  contextUsage(session: ChannelKey) {
208
212
  return this.usage.get(session);
@@ -244,7 +248,17 @@ export const script: {
244
248
  prompt: PromptSection[];
245
249
  /** The tools plugins defined for agents. */
246
250
  tools: string[];
247
- } = { scores: undefined, prompt: [], tools: [] };
251
+ /** The prompt every agent's starts with; read before each turn, so a test may change it. */
252
+ shared: string;
253
+ /** The one a speaker other than the owner gets instead; empty falls back to `shared`. */
254
+ guest: string;
255
+ } = {
256
+ scores: undefined,
257
+ prompt: [],
258
+ tools: [],
259
+ shared: "Shared rules.",
260
+ guest: "",
261
+ };
248
262
  let queue: ChannelQueue;
249
263
  let team: AgentTeam;
250
264
  let skillStore: TestStore<SkillStore>;
@@ -323,7 +337,9 @@ export function useTeamFixture(
323
337
  "agent_groups",
324
338
  "agent_group_messages",
325
339
  "agent_group_cursors",
340
+ "prompt_history",
326
341
  "skills",
342
+ "skill_versions",
327
343
  "skill_groups",
328
344
  "agent_skills",
329
345
  ])
@@ -361,6 +377,8 @@ export function useTeamFixture(
361
377
  script.scores = undefined;
362
378
  script.prompt = [];
363
379
  script.tools = [];
380
+ script.shared = "Shared rules.";
381
+ script.guest = "";
364
382
  queue = channelQueue();
365
383
  removedSchedules.length = 0;
366
384
  stops.length = 0;
@@ -420,7 +438,8 @@ export function useTeamFixture(
420
438
  return () => stops.push(`hide ${channel}`);
421
439
  },
422
440
  workDir: "/tmp/agents/work",
423
- sharedPrompt: "Shared rules.",
441
+ sharedPrompt: () => script.shared,
442
+ guestPrompt: () => script.guest,
424
443
  skills,
425
444
  logger: silentLogger(),
426
445
  });