pi-roundtable 0.1.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 (315) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/docs/plugins.md +924 -0
  5. package/examples/channels.test.ts +13 -0
  6. package/examples/channels.ts +27 -0
  7. package/examples/dashboard.test.ts +11 -0
  8. package/examples/dashboard.ts +9 -0
  9. package/examples/events.test.ts +26 -0
  10. package/examples/events.ts +25 -0
  11. package/examples/guide.test.ts +52 -0
  12. package/examples/holds.test.ts +16 -0
  13. package/examples/holds.ts +30 -0
  14. package/examples/http.test.ts +12 -0
  15. package/examples/http.ts +17 -0
  16. package/examples/interactions.test.ts +25 -0
  17. package/examples/interactions.ts +28 -0
  18. package/examples/migrations.test.ts +31 -0
  19. package/examples/migrations.ts +42 -0
  20. package/examples/packages.test.ts +9 -0
  21. package/examples/packages.ts +9 -0
  22. package/examples/preflight.test.ts +11 -0
  23. package/examples/preflight.ts +19 -0
  24. package/examples/prompt.test.ts +25 -0
  25. package/examples/prompt.ts +20 -0
  26. package/examples/providers.test.ts +10 -0
  27. package/examples/providers.ts +17 -0
  28. package/examples/seeds.test.ts +12 -0
  29. package/examples/seeds.ts +18 -0
  30. package/examples/selection.test.ts +13 -0
  31. package/examples/selection.ts +14 -0
  32. package/examples/services.test.ts +20 -0
  33. package/examples/services.ts +30 -0
  34. package/examples/session-tools.test.ts +24 -0
  35. package/examples/session-tools.ts +34 -0
  36. package/examples/tools.test.ts +18 -0
  37. package/examples/tools.ts +28 -0
  38. package/package.json +55 -0
  39. package/src/cli/add-plugin.test.ts +107 -0
  40. package/src/cli/add-plugin.ts +75 -0
  41. package/src/cli/checks/basic.test.ts +276 -0
  42. package/src/cli/checks/bun.ts +26 -0
  43. package/src/cli/checks/configuration.ts +28 -0
  44. package/src/cli/checks/database.test.ts +117 -0
  45. package/src/cli/checks/database.ts +100 -0
  46. package/src/cli/checks/discord.test.ts +200 -0
  47. package/src/cli/checks/discord.ts +218 -0
  48. package/src/cli/checks/environment.ts +27 -0
  49. package/src/cli/checks/model.ts +28 -0
  50. package/src/cli/checks/public-url.ts +40 -0
  51. package/src/cli/cli.test.ts +122 -0
  52. package/src/cli/cli.ts +166 -0
  53. package/src/cli/config-edit.test.ts +130 -0
  54. package/src/cli/config-edit.ts +199 -0
  55. package/src/cli/discord-api.ts +96 -0
  56. package/src/cli/doctor.test.ts +184 -0
  57. package/src/cli/doctor.ts +92 -0
  58. package/src/cli/http.ts +23 -0
  59. package/src/cli/init.test.ts +127 -0
  60. package/src/cli/init.ts +82 -0
  61. package/src/cli/main.ts +13 -0
  62. package/src/cli/project.ts +123 -0
  63. package/src/cli/report.ts +68 -0
  64. package/src/cli/roundtable.mjs +22 -0
  65. package/src/cli/runtime.ts +53 -0
  66. package/src/cli/size.test.ts +9 -0
  67. package/src/cli/start.ts +33 -0
  68. package/src/cli/templates.test.ts +129 -0
  69. package/src/cli/templates.ts +119 -0
  70. package/src/cli/testing/fixtures.ts +106 -0
  71. package/src/core/agents/agent-claim.test.ts +142 -0
  72. package/src/core/agents/agent-claim.ts +170 -0
  73. package/src/core/agents/agent-dashboard.test.ts +120 -0
  74. package/src/core/agents/agent-dashboard.ts +185 -0
  75. package/src/core/agents/agent-guild.test.ts +124 -0
  76. package/src/core/agents/agent-messages.ts +189 -0
  77. package/src/core/agents/agent-ports.ts +103 -0
  78. package/src/core/agents/agent-prompt.ts +163 -0
  79. package/src/core/agents/agent-rules.ts +106 -0
  80. package/src/core/agents/agent-schema.ts +115 -0
  81. package/src/core/agents/agent-settings.ts +49 -0
  82. package/src/core/agents/agent-store.test.ts +205 -0
  83. package/src/core/agents/agent-store.ts +384 -0
  84. package/src/core/agents/agent-team.ts +445 -0
  85. package/src/core/agents/agent-tools.ts +386 -0
  86. package/src/core/agents/avatar-studio.test.ts +88 -0
  87. package/src/core/agents/avatar-studio.ts +147 -0
  88. package/src/core/agents/group-messages.ts +99 -0
  89. package/src/core/agents/group-round.test.ts +143 -0
  90. package/src/core/agents/group-round.ts +117 -0
  91. package/src/core/agents/group-turns.ts +132 -0
  92. package/src/core/agents/owner-identity.test.ts +172 -0
  93. package/src/core/agents/team-editing.ts +205 -0
  94. package/src/core/agents/team-keys.ts +40 -0
  95. package/src/core/agents/team-layout.ts +135 -0
  96. package/src/core/agents/team-lifecycle.ts +188 -0
  97. package/src/core/agents/team-options.ts +52 -0
  98. package/src/core/agents/team-status.ts +80 -0
  99. package/src/core/agents/team-text.ts +60 -0
  100. package/src/core/agents/team-turn-types.ts +107 -0
  101. package/src/core/agents/team-turns.test.ts +278 -0
  102. package/src/core/agents/team-turns.ts +323 -0
  103. package/src/core/assets/neutral.png +0 -0
  104. package/src/core/assets/prompts/shared-guest.md +11 -0
  105. package/src/core/assets/prompts/shared.md +11 -0
  106. package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
  107. package/src/core/attachments/attachment-dir.ts +15 -0
  108. package/src/core/attachments/attachment-fetcher.ts +83 -0
  109. package/src/core/attachments/attachments.test.ts +85 -0
  110. package/src/core/attachments/image-prep.ts +50 -0
  111. package/src/core/attachments/prompt-block.ts +37 -0
  112. package/src/core/attachments/turn-attachments.ts +79 -0
  113. package/src/core/boundary.test.ts +45 -0
  114. package/src/core/builtin/agent-server.ts +275 -0
  115. package/src/core/builtin/discord.ts +95 -0
  116. package/src/core/builtin/modules.test.ts +127 -0
  117. package/src/core/builtin/modules.ts +176 -0
  118. package/src/core/builtin/seeds.ts +10 -0
  119. package/src/core/builtin/stores.ts +42 -0
  120. package/src/core/config/config.test.ts +135 -0
  121. package/src/core/config/config.ts +285 -0
  122. package/src/core/config/schema.ts +181 -0
  123. package/src/core/contract/channels.ts +152 -0
  124. package/src/core/contract/discord.ts +33 -0
  125. package/src/core/contract/providers.ts +67 -0
  126. package/src/core/db/guild-scope.ts +53 -0
  127. package/src/core/db/migrations.test.ts +237 -0
  128. package/src/core/db/migrations.ts +37 -0
  129. package/src/core/define-roundtable.test.ts +134 -0
  130. package/src/core/define-roundtable.ts +195 -0
  131. package/src/core/define.test.ts +144 -0
  132. package/src/core/define.ts +154 -0
  133. package/src/core/discord/agent-commands.test.ts +58 -0
  134. package/src/core/discord/agent-commands.ts +385 -0
  135. package/src/core/discord/agent-discord.test.ts +70 -0
  136. package/src/core/discord/agent-discord.ts +367 -0
  137. package/src/core/discord/channel-executor.ts +404 -0
  138. package/src/core/discord/channel-operations.ts +329 -0
  139. package/src/core/discord/discord-surface.ts +431 -0
  140. package/src/core/discord/dispatch-thread-host.test.ts +114 -0
  141. package/src/core/discord/dispatch-thread-host.ts +72 -0
  142. package/src/core/discord/dispatch-threads.test.ts +94 -0
  143. package/src/core/discord/dispatch-threads.ts +180 -0
  144. package/src/core/discord/interaction-module.ts +1 -0
  145. package/src/core/discord/owner-cards.test.ts +435 -0
  146. package/src/core/discord/owner-cards.ts +463 -0
  147. package/src/core/discord/owner-command.ts +126 -0
  148. package/src/core/discord/owner-discord-access.ts +183 -0
  149. package/src/core/discord/owner-discord-threads.ts +160 -0
  150. package/src/core/discord/owner-discord.test.ts +325 -0
  151. package/src/core/discord/owner-discord.ts +282 -0
  152. package/src/core/discord/owner-panel.ts +103 -0
  153. package/src/core/discord/schedule-commands.ts +149 -0
  154. package/src/core/discord/stop-button.ts +20 -0
  155. package/src/core/domain/attachment.ts +39 -0
  156. package/src/core/domain/conversation.ts +42 -0
  157. package/src/core/domain/errors.ts +31 -0
  158. package/src/core/domain/expression.ts +21 -0
  159. package/src/core/domain/owner-prompts.ts +45 -0
  160. package/src/core/domain/ports.ts +91 -0
  161. package/src/core/domain/profile.ts +32 -0
  162. package/src/core/drain.test.ts +46 -0
  163. package/src/core/drain.ts +33 -0
  164. package/src/core/errors.ts +29 -0
  165. package/src/core/events.test.ts +81 -0
  166. package/src/core/events.ts +55 -0
  167. package/src/core/holds.test.ts +54 -0
  168. package/src/core/holds.ts +46 -0
  169. package/src/core/host.test.ts +536 -0
  170. package/src/core/host.ts +297 -0
  171. package/src/core/http/listeners.test.ts +164 -0
  172. package/src/core/http/listeners.ts +151 -0
  173. package/src/core/i18n/agent-panel.ts +96 -0
  174. package/src/core/i18n/cards.ts +64 -0
  175. package/src/core/i18n/channels.ts +28 -0
  176. package/src/core/i18n/conversation.ts +49 -0
  177. package/src/core/i18n/dashboard.ts +72 -0
  178. package/src/core/i18n/discord.ts +23 -0
  179. package/src/core/i18n/en.ts +27 -0
  180. package/src/core/i18n/i18n.test.ts +139 -0
  181. package/src/core/i18n/index.ts +58 -0
  182. package/src/core/i18n/owner.ts +35 -0
  183. package/src/core/i18n/schedules.ts +92 -0
  184. package/src/core/i18n/time-zones.ts +27 -0
  185. package/src/core/i18n/types.ts +7 -0
  186. package/src/core/i18n/zh-tw.ts +26 -0
  187. package/src/core/identity.test.ts +18 -0
  188. package/src/core/identity.ts +31 -0
  189. package/src/core/judging/confirmation-judge.ts +68 -0
  190. package/src/core/judging/effort-judge.test.ts +112 -0
  191. package/src/core/judging/effort-judge.ts +158 -0
  192. package/src/core/judging/model-judge.test.ts +126 -0
  193. package/src/core/judging/model-judge.ts +193 -0
  194. package/src/core/log.ts +45 -0
  195. package/src/core/models.ts +52 -0
  196. package/src/core/modules/background/background-turns.ts +78 -0
  197. package/src/core/modules/delegation/delegate.ts +54 -0
  198. package/src/core/modules/delegation/delegator.test.ts +170 -0
  199. package/src/core/modules/delegation/delegator.ts +191 -0
  200. package/src/core/modules/delegation/sol-worker.ts +78 -0
  201. package/src/core/modules/discord-admin/discord-admin.ts +288 -0
  202. package/src/core/modules/host-shell/shell-policy.ts +386 -0
  203. package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
  204. package/src/core/modules/memory/owner-memory-store.ts +228 -0
  205. package/src/core/modules/memory/owner-memory.ts +153 -0
  206. package/src/core/modules/notify/notify.ts +26 -0
  207. package/src/core/modules/schedules/recurrence.ts +206 -0
  208. package/src/core/modules/schedules/schedule-store.ts +213 -0
  209. package/src/core/modules/schedules/schedule-tools.ts +196 -0
  210. package/src/core/modules/schedules/schedule.test.ts +358 -0
  211. package/src/core/modules/schedules/scheduler.ts +121 -0
  212. package/src/core/modules/schedules/schedules.ts +87 -0
  213. package/src/core/modules/skills/repo-name.ts +11 -0
  214. package/src/core/modules/skills/skill-kind.test.ts +55 -0
  215. package/src/core/modules/skills/skill-link.ts +69 -0
  216. package/src/core/modules/skills/skill-listing.ts +128 -0
  217. package/src/core/modules/skills/skill-registry.test.ts +376 -0
  218. package/src/core/modules/skills/skill-registry.ts +323 -0
  219. package/src/core/modules/skills/skill-rules.ts +120 -0
  220. package/src/core/modules/skills/skill-store.ts +285 -0
  221. package/src/core/modules/skills/skill-tools.ts +207 -0
  222. package/src/core/ops/error-reporter.test.ts +283 -0
  223. package/src/core/ops/error-reporter.ts +271 -0
  224. package/src/core/plugin.ts +200 -0
  225. package/src/core/presentation/card-cadence.ts +41 -0
  226. package/src/core/presentation/headline.ts +34 -0
  227. package/src/core/presentation/presentation.test.ts +166 -0
  228. package/src/core/presentation/quiet-links.ts +36 -0
  229. package/src/core/presentation/reply-splitter.ts +110 -0
  230. package/src/core/presentation/thinking-line.ts +24 -0
  231. package/src/core/public-entry.test.ts +59 -0
  232. package/src/core/registry/contributions.test.ts +215 -0
  233. package/src/core/registry/contributions.ts +249 -0
  234. package/src/core/registry/interactions.test.ts +87 -0
  235. package/src/core/registry/interactions.ts +55 -0
  236. package/src/core/registry/providers.test.ts +103 -0
  237. package/src/core/registry/providers.ts +55 -0
  238. package/src/core/routing/channel-queue.test.ts +31 -0
  239. package/src/core/routing/channel-queue.ts +44 -0
  240. package/src/core/routing/channel-router.test.ts +326 -0
  241. package/src/core/routing/channel-router.ts +157 -0
  242. package/src/core/routing/conversation-kind.ts +14 -0
  243. package/src/core/routing/forward-join.ts +65 -0
  244. package/src/core/routing/message-text.ts +16 -0
  245. package/src/core/routing/settle-turn.test.ts +21 -0
  246. package/src/core/routing/settle-turn.ts +26 -0
  247. package/src/core/runtime/compaction-tiers.test.ts +227 -0
  248. package/src/core/runtime/compaction-tiers.ts +203 -0
  249. package/src/core/runtime/conversation-sessions.ts +162 -0
  250. package/src/core/runtime/extensions/agent-prompt.ts +16 -0
  251. package/src/core/runtime/extensions/ask-user.test.ts +114 -0
  252. package/src/core/runtime/extensions/ask-user.ts +98 -0
  253. package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
  254. package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
  255. package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
  256. package/src/core/runtime/mcp.ts +23 -0
  257. package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
  258. package/src/core/runtime/pending-confirmation-store.ts +75 -0
  259. package/src/core/runtime/pi-agent-runtime.ts +477 -0
  260. package/src/core/runtime/prompt-slot.ts +81 -0
  261. package/src/core/runtime/runtime-types.ts +150 -0
  262. package/src/core/runtime/session-archive.test.ts +20 -0
  263. package/src/core/runtime/session-archive.ts +17 -0
  264. package/src/core/runtime/session-factory.ts +291 -0
  265. package/src/core/runtime/steerable-run.test.ts +264 -0
  266. package/src/core/runtime/steerable-run.ts +109 -0
  267. package/src/core/runtime/text-tools.test.ts +110 -0
  268. package/src/core/runtime/text-tools.ts +66 -0
  269. package/src/core/runtime/turn-answer.test.ts +72 -0
  270. package/src/core/runtime/turn-answer.ts +79 -0
  271. package/src/core/runtime/worker-task.test.ts +82 -0
  272. package/src/core/runtime/worker-task.ts +65 -0
  273. package/src/core/services.test.ts +28 -0
  274. package/src/core/services.ts +107 -0
  275. package/src/core/sessions.test.ts +85 -0
  276. package/src/core/sessions.ts +149 -0
  277. package/src/core/shared/attachment-reader.ts +91 -0
  278. package/src/core/shared/delegate-tool.ts +15 -0
  279. package/src/core/shared/mcp-adapter.ts +58 -0
  280. package/src/core/shared/package-dir.ts +9 -0
  281. package/src/core/shared/profile-tools.ts +16 -0
  282. package/src/core/shared/read-attachment-tool.ts +43 -0
  283. package/src/core/shared/schedule-tools.ts +118 -0
  284. package/src/core/shared/session-messages.ts +50 -0
  285. package/src/core/shared/tool-result.ts +9 -0
  286. package/src/core/shared/unix-server.ts +18 -0
  287. package/src/core/size.test.ts +9 -0
  288. package/src/core/speakers.test.ts +78 -0
  289. package/src/core/speakers.ts +143 -0
  290. package/src/core/testing/database.ts +52 -0
  291. package/src/core/testing/file-size.ts +34 -0
  292. package/src/core/testing/locale.ts +8 -0
  293. package/src/core/testing/modules.ts +97 -0
  294. package/src/core/testing/owner.ts +15 -0
  295. package/src/core/testing/thread-host.ts +60 -0
  296. package/src/core/time.test.ts +166 -0
  297. package/src/core/time.ts +67 -0
  298. package/src/core/tool-tiers.test.ts +117 -0
  299. package/src/core/tool-tiers.ts +127 -0
  300. package/src/entries.test.ts +112 -0
  301. package/src/index.ts +28 -0
  302. package/src/testing.test.ts +156 -0
  303. package/src/testing.ts +182 -0
  304. package/templates/.env.example +22 -0
  305. package/templates/README.md +11 -0
  306. package/templates/_gitignore +3 -0
  307. package/templates/agents.ts +12 -0
  308. package/templates/biome.json.tmpl +7 -0
  309. package/templates/docker-compose.yml +16 -0
  310. package/templates/package.json.tmpl +24 -0
  311. package/templates/persona/shared.md +2 -0
  312. package/templates/plugin/plugin.test.ts.tmpl +13 -0
  313. package/templates/plugin/plugin.ts +18 -0
  314. package/templates/roundtable.config.ts +24 -0
  315. package/templates/tsconfig.json.tmpl +16 -0
@@ -0,0 +1,384 @@
1
+ import type { SQL } from "bun";
2
+ import type { Migration } from "../db/migrations.ts";
3
+ import { AgentError } from "../domain/errors.ts";
4
+ import type { ThinkingLevel } from "../models.ts";
5
+ import {
6
+ type Agent,
7
+ type AgentGroup,
8
+ type AgentSeed,
9
+ type AgentStatus,
10
+ type Backlog,
11
+ checkAgentName,
12
+ checkAvatarPrompt,
13
+ checkDisplayName,
14
+ checkModelRef,
15
+ checkPrompt,
16
+ checkThinking,
17
+ type GroupMessage,
18
+ MAX_GROUP_MEMBERS,
19
+ MIN_GROUP_MEMBERS,
20
+ } from "./agent-rules.ts";
21
+ import {
22
+ type AgentRow,
23
+ agentMigrations,
24
+ agentsMigration,
25
+ type GroupRow,
26
+ toAgent,
27
+ toGroup,
28
+ } from "./agent-schema.ts";
29
+ import { GroupMessages } from "./group-messages.ts";
30
+
31
+ export {
32
+ type Agent,
33
+ type AgentGroup,
34
+ type AgentSeed,
35
+ type AgentStatus,
36
+ type Backlog,
37
+ checkAgentName,
38
+ checkAvatarPrompt,
39
+ checkDisplayName,
40
+ checkModelRef,
41
+ checkPrompt,
42
+ checkThinking,
43
+ type GroupMessage,
44
+ MAX_AVATAR_PROMPT_CHARS,
45
+ MAX_GROUP_MEMBERS,
46
+ MAX_PROMPT_CHARS,
47
+ MIN_GROUP_MEMBERS,
48
+ } from "./agent-rules.ts";
49
+
50
+ /**
51
+ * Agents, groups, and group messages, in the database. Agents and groups are also held
52
+ * in memory, because every inbound message asks and only this process changes them.
53
+ */
54
+ export class AgentStore {
55
+ readonly #sql: SQL;
56
+ readonly #agents = new Map<string, Agent>();
57
+ readonly #groups = new Map<string, AgentGroup>();
58
+
59
+ readonly #guild: string;
60
+ readonly #messages: GroupMessages;
61
+
62
+ private constructor(sql: SQL, guildId: string) {
63
+ this.#sql = sql;
64
+ this.#guild = guildId;
65
+ this.#messages = new GroupMessages(sql, guildId);
66
+ }
67
+
68
+ /** The store's tables; the host runs this before any store attaches. */
69
+ static readonly migration: Migration = agentsMigration;
70
+
71
+ /** The migrations of the store's tables for the guild the process serves. */
72
+ static migrations(guildId: string): Migration[] {
73
+ return agentMigrations(guildId);
74
+ }
75
+
76
+ /** The store over the host's migrated pool, holding the rows of one guild. */
77
+ static async attach(sql: SQL, guildId: string): Promise<AgentStore> {
78
+ const store = new AgentStore(sql, guildId);
79
+ await store.#reload();
80
+ return store;
81
+ }
82
+
83
+ async #reload(): Promise<void> {
84
+ const agents: AgentRow[] = await this
85
+ .#sql`SELECT * FROM agents WHERE guild_id = ${this.#guild}`;
86
+ const groups: GroupRow[] = await this
87
+ .#sql`SELECT * FROM agent_groups WHERE guild_id = ${this.#guild}`;
88
+ this.#agents.clear();
89
+ this.#groups.clear();
90
+ for (const row of agents) this.#agents.set(row.name, toAgent(row));
91
+ for (const row of groups) this.#groups.set(row.name, toGroup(row));
92
+ }
93
+
94
+ /** Inserts seed agents that do not exist yet; existing rows, edited or not, are left alone. */
95
+ async seed(seeds: readonly AgentSeed[]): Promise<string[]> {
96
+ const added: string[] = [];
97
+ for (const seed of seeds) {
98
+ const rows: unknown[] = await this.#sql`
99
+ INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id)
100
+ VALUES (${this.#guild}, ${seed.name}, ${seed.displayName}, ${seed.prompt}, ${seed.avatarPrompt},
101
+ ${seed.channelId ?? null})
102
+ ON CONFLICT DO NOTHING
103
+ RETURNING name`;
104
+ if (rows.length > 0) added.push(seed.name);
105
+ }
106
+ await this.#reload();
107
+ return added;
108
+ }
109
+
110
+ agents(): Agent[] {
111
+ return [...this.#agents.values()];
112
+ }
113
+
114
+ agent(name: string): Agent | undefined {
115
+ return this.#agents.get(name);
116
+ }
117
+
118
+ /** The active agent with that name, or an error the caller can show. */
119
+ activeAgent(name: string): Agent {
120
+ const agent = this.#agents.get(name);
121
+ if (!agent)
122
+ throw new AgentError(
123
+ `There is no agent "${name}". Active agents: ${this.#activeNames()}.`,
124
+ );
125
+ if (agent.status !== "active")
126
+ throw new AgentError(
127
+ `Agent "${name}" is archived. Active agents: ${this.#activeNames()}.`,
128
+ );
129
+ return agent;
130
+ }
131
+
132
+ agentByChannel(channelId: string): Agent | undefined {
133
+ for (const agent of this.#agents.values())
134
+ if (agent.channelId === channelId && agent.status === "active")
135
+ return agent;
136
+ return undefined;
137
+ }
138
+
139
+ groups(): AgentGroup[] {
140
+ return [...this.#groups.values()];
141
+ }
142
+
143
+ group(name: string): AgentGroup | undefined {
144
+ return this.#groups.get(name);
145
+ }
146
+
147
+ groupByChannel(channelId: string): AgentGroup | undefined {
148
+ for (const group of this.#groups.values())
149
+ if (group.channelId === channelId && group.status === "active")
150
+ return group;
151
+ return undefined;
152
+ }
153
+
154
+ #activeNames(): string {
155
+ return (
156
+ this.agents()
157
+ .filter((a) => a.status === "active")
158
+ .map((a) => a.name)
159
+ .join(", ") || "none"
160
+ );
161
+ }
162
+
163
+ /** Names are shared between agents and groups, archived ones included. */
164
+ #checkFree(name: string): void {
165
+ if (this.#agents.has(name) || this.#groups.has(name))
166
+ throw new AgentError(
167
+ `The name "${name}" is taken, archived agents and groups included.`,
168
+ );
169
+ }
170
+
171
+ async createAgent(agent: {
172
+ name: string;
173
+ displayName: string;
174
+ prompt: string;
175
+ avatarPrompt: string;
176
+ channelId: string;
177
+ }): Promise<Agent> {
178
+ checkAgentName(agent.name);
179
+ checkDisplayName(agent.displayName);
180
+ checkPrompt(agent.prompt);
181
+ checkAvatarPrompt(agent.avatarPrompt);
182
+ this.#checkFree(agent.name);
183
+ const rows: AgentRow[] = await this.#sql`
184
+ INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id)
185
+ VALUES (${this.#guild}, ${agent.name}, ${agent.displayName.trim()}, ${agent.prompt}, ${agent.avatarPrompt},
186
+ ${agent.channelId})
187
+ RETURNING *`;
188
+ return this.#keepAgent(rows[0]);
189
+ }
190
+
191
+ /** A name that createAgent would accept, checked before its channel is made. */
192
+ checkNewName(name: string): void {
193
+ checkAgentName(name);
194
+ this.#checkFree(name);
195
+ }
196
+
197
+ async updateAgent(
198
+ name: string,
199
+ change: {
200
+ displayName?: string;
201
+ prompt?: string;
202
+ avatarPrompt?: string;
203
+ avatarHash?: string;
204
+ channelId?: string;
205
+ /** `null` returns it to the assistant's. */
206
+ model?: string | null;
207
+ thinking?: ThinkingLevel | null;
208
+ },
209
+ ): Promise<Agent> {
210
+ const current = this.activeAgent(name);
211
+ if (change.displayName !== undefined) checkDisplayName(change.displayName);
212
+ if (change.prompt !== undefined) checkPrompt(change.prompt);
213
+ if (change.avatarPrompt !== undefined)
214
+ checkAvatarPrompt(change.avatarPrompt);
215
+ if (change.model) checkModelRef(change.model);
216
+ if (change.thinking) checkThinking(change.thinking);
217
+ const model =
218
+ change.model === undefined ? (current.model ?? null) : change.model;
219
+ const thinking =
220
+ change.thinking === undefined
221
+ ? (current.thinking ?? null)
222
+ : change.thinking;
223
+ const rows: AgentRow[] = await this.#sql`
224
+ UPDATE agents SET
225
+ display_name = ${change.displayName?.trim() ?? current.displayName},
226
+ prompt = ${change.prompt ?? current.prompt},
227
+ avatar_prompt = ${change.avatarPrompt ?? current.avatarPrompt},
228
+ avatar_hash = ${change.avatarHash ?? current.avatarHash ?? null},
229
+ channel_id = ${change.channelId ?? current.channelId ?? null},
230
+ model = ${model},
231
+ thinking = ${thinking},
232
+ updated_at = now()
233
+ WHERE guild_id = ${this.#guild} AND name = ${name}
234
+ RETURNING *`;
235
+ return this.#keepAgent(rows[0]);
236
+ }
237
+
238
+ /** Archives an agent and takes it out of every group; says which groups it left. */
239
+ async archiveAgent(name: string): Promise<AgentGroup[]> {
240
+ const rows: AgentRow[] = await this.#sql`
241
+ UPDATE agents SET status = 'archived', updated_at = now()
242
+ WHERE guild_id = ${this.#guild} AND name = ${name} RETURNING *`;
243
+ this.#keepAgent(rows[0]);
244
+ const left: AgentGroup[] = [];
245
+ for (const group of this.groups()) {
246
+ if (group.status !== "active" || !group.members.includes(name)) continue;
247
+ const members = group.members.filter((m) => m !== name);
248
+ const host = group.host === name ? (members[0] ?? name) : group.host;
249
+ left.push(
250
+ await this.#writeGroup(group.name, {
251
+ members,
252
+ host,
253
+ status: members.length < MIN_GROUP_MEMBERS ? "archived" : "active",
254
+ }),
255
+ );
256
+ }
257
+ return left;
258
+ }
259
+
260
+ #keepAgent(row: AgentRow | undefined): Agent {
261
+ if (!row) throw new AgentError("the agent was not found");
262
+ const agent = toAgent(row);
263
+ this.#agents.set(agent.name, agent);
264
+ return agent;
265
+ }
266
+
267
+ #checkMembers(members: readonly string[], host: string): void {
268
+ if (
269
+ new Set(members).size !== members.length ||
270
+ members.length < MIN_GROUP_MEMBERS ||
271
+ members.length > MAX_GROUP_MEMBERS
272
+ )
273
+ throw new AgentError(
274
+ `A group has ${MIN_GROUP_MEMBERS} to ${MAX_GROUP_MEMBERS} different members.`,
275
+ );
276
+ for (const member of members) this.activeAgent(member);
277
+ if (!members.includes(host))
278
+ throw new AgentError(`The host "${host}" must be one of the members.`);
279
+ }
280
+
281
+ /** Checks a new group before its channel is made. */
282
+ checkNewGroup(name: string, members: readonly string[], host: string): void {
283
+ checkAgentName(name);
284
+ this.#checkFree(name);
285
+ this.#checkMembers(members, host);
286
+ }
287
+
288
+ async createGroup(group: {
289
+ name: string;
290
+ displayName: string;
291
+ channelId: string;
292
+ members: string[];
293
+ host: string;
294
+ }): Promise<AgentGroup> {
295
+ this.checkNewGroup(group.name, group.members, group.host);
296
+ checkDisplayName(group.displayName);
297
+ const rows: GroupRow[] = await this.#sql`
298
+ INSERT INTO agent_groups (guild_id, name, display_name, channel_id, members, host)
299
+ VALUES (${this.#guild}, ${group.name}, ${group.displayName.trim()}, ${group.channelId},
300
+ ${group.members.join(",")}, ${group.host})
301
+ RETURNING *`;
302
+ return this.#keepGroup(rows[0]);
303
+ }
304
+
305
+ async updateGroup(
306
+ name: string,
307
+ change: { displayName?: string; members?: string[]; host?: string },
308
+ ): Promise<AgentGroup> {
309
+ const current = this.#groups.get(name);
310
+ if (current?.status !== "active")
311
+ throw new AgentError(`There is no active group "${name}".`);
312
+ if (change.displayName !== undefined) checkDisplayName(change.displayName);
313
+ const members = change.members ?? current.members;
314
+ const host =
315
+ change.host ??
316
+ (members.includes(current.host) ? current.host : (members[0] ?? ""));
317
+ this.#checkMembers(members, host);
318
+ return this.#writeGroup(name, {
319
+ displayName: change.displayName?.trim() ?? current.displayName,
320
+ members,
321
+ host,
322
+ status: "active",
323
+ });
324
+ }
325
+
326
+ async archiveGroup(name: string): Promise<AgentGroup> {
327
+ return this.#writeGroup(name, { status: "archived" });
328
+ }
329
+
330
+ async #writeGroup(
331
+ name: string,
332
+ change: {
333
+ displayName?: string;
334
+ members?: string[];
335
+ host?: string;
336
+ status?: AgentStatus;
337
+ },
338
+ ): Promise<AgentGroup> {
339
+ const current = this.#groups.get(name);
340
+ if (!current) throw new AgentError(`There is no group "${name}".`);
341
+ const rows: GroupRow[] = await this.#sql`
342
+ UPDATE agent_groups SET
343
+ display_name = ${change.displayName ?? current.displayName},
344
+ members = ${(change.members ?? current.members).join(",")},
345
+ host = ${change.host ?? current.host},
346
+ status = ${change.status ?? current.status}
347
+ WHERE guild_id = ${this.#guild} AND name = ${name}
348
+ RETURNING *`;
349
+ return this.#keepGroup(rows[0]);
350
+ }
351
+
352
+ #keepGroup(row: GroupRow | undefined): AgentGroup {
353
+ if (!row) throw new AgentError("the group was not found");
354
+ const group = toGroup(row);
355
+ this.#groups.set(group.name, group);
356
+ return group;
357
+ }
358
+
359
+ appendGroupMessage(
360
+ group: string,
361
+ message: { author: string; authorName: string; text: string },
362
+ ): Promise<number> {
363
+ return this.#messages.append(group, message);
364
+ }
365
+
366
+ /** The group's newest messages, oldest first. */
367
+ recentGroupMessages(group: string, limit: number): Promise<GroupMessage[]> {
368
+ return this.#messages.recent(group, limit);
369
+ }
370
+
371
+ /** Group messages the member has not received yet, its own excluded; the newest `limit` are carried. */
372
+ backlog(group: string, agent: string, limit: number): Promise<Backlog> {
373
+ return this.#messages.backlog(group, agent, limit);
374
+ }
375
+
376
+ advanceCursor(group: string, agent: string, lastId: number): Promise<void> {
377
+ return this.#messages.advanceCursor(group, agent, lastId);
378
+ }
379
+
380
+ /** Marks every message so far as received by every member, for a group started over. */
381
+ catchUp(group: AgentGroup): Promise<void> {
382
+ return this.#messages.catchUp(group);
383
+ }
384
+ }