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,115 @@
1
+ import { scopeToGuild } from "../db/guild-scope.ts";
2
+ import type { Migration } from "../db/migrations.ts";
3
+ import type { ThinkingLevel } from "../models.ts";
4
+ import type { Agent, AgentGroup, AgentStatus } from "./agent-rules.ts";
5
+
6
+ export interface AgentRow {
7
+ name: string;
8
+ display_name: string;
9
+ prompt: string;
10
+ avatar_prompt: string;
11
+ avatar_hash: string | null;
12
+ channel_id: string | null;
13
+ model: string | null;
14
+ thinking: ThinkingLevel | null;
15
+ status: AgentStatus;
16
+ }
17
+
18
+ export interface GroupRow {
19
+ name: string;
20
+ display_name: string;
21
+ channel_id: string;
22
+ members: string;
23
+ host: string;
24
+ status: AgentStatus;
25
+ }
26
+
27
+ export const toAgent = (row: AgentRow): Agent => ({
28
+ name: row.name,
29
+ displayName: row.display_name,
30
+ prompt: row.prompt,
31
+ avatarPrompt: row.avatar_prompt,
32
+ ...(row.avatar_hash ? { avatarHash: row.avatar_hash } : {}),
33
+ ...(row.channel_id ? { channelId: row.channel_id } : {}),
34
+ ...(row.model ? { model: row.model } : {}),
35
+ ...(row.thinking ? { thinking: row.thinking } : {}),
36
+ status: row.status,
37
+ });
38
+
39
+ export const toGroup = (row: GroupRow): AgentGroup => ({
40
+ name: row.name,
41
+ displayName: row.display_name,
42
+ channelId: row.channel_id,
43
+ members: row.members.split(",").filter(Boolean),
44
+ host: row.host,
45
+ status: row.status,
46
+ });
47
+
48
+ /** The store's tables; the host runs this before any store attaches. */
49
+ export const agentsMigration: Migration = {
50
+ name: "agents",
51
+ up: async (sql) => {
52
+ await sql`
53
+ CREATE TABLE IF NOT EXISTS agents (
54
+ name text PRIMARY KEY,
55
+ display_name text NOT NULL,
56
+ prompt text NOT NULL,
57
+ avatar_prompt text NOT NULL,
58
+ avatar_hash text,
59
+ channel_id text UNIQUE,
60
+ status text NOT NULL DEFAULT 'active',
61
+ created_at timestamptz NOT NULL DEFAULT now(),
62
+ updated_at timestamptz NOT NULL DEFAULT now()
63
+ )`;
64
+ await sql`ALTER TABLE agents ADD COLUMN IF NOT EXISTS model text`;
65
+ await sql`ALTER TABLE agents ADD COLUMN IF NOT EXISTS thinking text`;
66
+ await sql`
67
+ CREATE TABLE IF NOT EXISTS agent_groups (
68
+ name text PRIMARY KEY,
69
+ display_name text NOT NULL,
70
+ channel_id text NOT NULL UNIQUE,
71
+ -- Comma-separated agent names, which never contain a comma.
72
+ members text NOT NULL,
73
+ host text NOT NULL,
74
+ status text NOT NULL DEFAULT 'active',
75
+ created_at timestamptz NOT NULL DEFAULT now()
76
+ )`;
77
+ await sql`
78
+ CREATE TABLE IF NOT EXISTS agent_group_messages (
79
+ id bigserial PRIMARY KEY,
80
+ group_name text NOT NULL,
81
+ author text NOT NULL,
82
+ author_name text NOT NULL,
83
+ text text NOT NULL,
84
+ created_at timestamptz NOT NULL DEFAULT now()
85
+ )`;
86
+ await sql`CREATE INDEX IF NOT EXISTS agent_group_messages_group ON agent_group_messages (group_name, id)`;
87
+ await sql`
88
+ CREATE TABLE IF NOT EXISTS agent_group_cursors (
89
+ group_name text NOT NULL,
90
+ agent_name text NOT NULL,
91
+ last_id bigint NOT NULL,
92
+ PRIMARY KEY (group_name, agent_name)
93
+ )`;
94
+ },
95
+ };
96
+
97
+ /**
98
+ * The migrations of the store's tables for the guild the process serves: the tables, then
99
+ * each row's guild, with existing rows given `guildId`.
100
+ */
101
+ export function agentMigrations(guildId: string): Migration[] {
102
+ return [
103
+ agentsMigration,
104
+ {
105
+ name: "agents-guild",
106
+ up: (sql) =>
107
+ scopeToGuild(sql, guildId, [
108
+ { table: "agents", key: ["name"] },
109
+ { table: "agent_groups", key: ["name"] },
110
+ { table: "agent_group_messages", key: ["id"] },
111
+ { table: "agent_group_cursors", key: ["group_name", "agent_name"] },
112
+ ]),
113
+ },
114
+ ];
115
+ }
@@ -0,0 +1,49 @@
1
+ import { AgentError } from "../domain/errors.ts";
2
+ import { assistantName } from "../i18n/index.ts";
3
+ import {
4
+ AUTO_THINKING,
5
+ type ThinkingLevel,
6
+ type ThinkingSetting,
7
+ } from "../models.ts";
8
+ import { checkModelRef, checkThinking } from "./agent-store.ts";
9
+
10
+ /** Resets a model or thinking setting to the assistant's. */
11
+ const DEFAULT_SETTING = "default";
12
+
13
+ /** A thinking setting in words, for agents reading one. */
14
+ export function describeThinking(setting: ThinkingSetting): string {
15
+ return setting === AUTO_THINKING
16
+ ? "auto, the judge picks each turn's level"
17
+ : setting;
18
+ }
19
+
20
+ /**
21
+ * The model an update sets: unchanged when absent, null for the assistant's, otherwise one the host
22
+ * runs.
23
+ */
24
+ export async function modelSetting(
25
+ model: string | undefined,
26
+ usable: () => Promise<string[]>,
27
+ ): Promise<string | null | undefined> {
28
+ if (model === undefined) return undefined;
29
+ const value = model.trim();
30
+ if (value === DEFAULT_SETTING) return null;
31
+ checkModelRef(value);
32
+ const models = await usable();
33
+ if (!models.includes(value))
34
+ throw new AgentError(
35
+ `${assistantName()} cannot run ${value}. Usable models: ${models.join(", ")}; or "default" for ${assistantName()}'s.`,
36
+ );
37
+ return value;
38
+ }
39
+
40
+ /** The thinking level an update sets: unchanged when absent, null for the assistant's. */
41
+ export function thinkingSetting(
42
+ level: string | undefined,
43
+ ): ThinkingLevel | null | undefined {
44
+ if (level === undefined) return undefined;
45
+ const value = level.trim();
46
+ if (value === DEFAULT_SETTING) return null;
47
+ checkThinking(value);
48
+ return value;
49
+ }
@@ -0,0 +1,205 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { SQL } from "bun";
3
+ import { AgentError } from "../domain/errors.ts";
4
+ import {
5
+ describeDb,
6
+ openTestStore,
7
+ TEST_GUILD,
8
+ type TestStore,
9
+ testDatabaseUrl,
10
+ } from "../testing/database.ts";
11
+ import { AgentStore } from "./agent-store.ts";
12
+
13
+ // Runs against a real PostgreSQL, only when ROUNDTABLE_TEST_DATABASE_URL is set.
14
+ describeDb("PostgreSQL", () => {
15
+ let store: TestStore<AgentStore>;
16
+
17
+ const seed = (name: string, channelId?: string) => ({
18
+ name,
19
+ displayName: name.toUpperCase(),
20
+ prompt: `You are ${name}.`,
21
+ avatarPrompt: `A portrait of ${name}.`,
22
+ ...(channelId ? { channelId } : {}),
23
+ });
24
+
25
+ beforeEach(async () => {
26
+ await store?.close();
27
+ const admin = new SQL(testDatabaseUrl);
28
+ for (const table of [
29
+ "agents",
30
+ "agent_groups",
31
+ "agent_group_messages",
32
+ "agent_group_cursors",
33
+ ])
34
+ await admin.unsafe(`DROP TABLE IF EXISTS ${table}`);
35
+ await admin.close();
36
+ store = await openTestStore(AgentStore, TEST_GUILD);
37
+ await store.seed([
38
+ seed("coordinator", "100"),
39
+ seed("infra"),
40
+ seed("doctor"),
41
+ ]);
42
+ });
43
+
44
+ afterAll(async () => {
45
+ await store.close();
46
+ });
47
+
48
+ describe("agents", () => {
49
+ test("seeding never overwrites an existing agent", async () => {
50
+ await store.updateAgent("infra", { prompt: "Edited." });
51
+ const added = await store.seed([seed("infra"), seed("new-one")]);
52
+ expect(added).toEqual(["new-one"]);
53
+ expect(store.agent("infra")?.prompt).toBe("Edited.");
54
+ const reopened = await openTestStore(AgentStore, TEST_GUILD);
55
+ expect(reopened.agent("infra")?.prompt).toBe("Edited.");
56
+ await reopened.close();
57
+ });
58
+
59
+ test("finds an active agent by its channel", async () => {
60
+ expect(store.agentByChannel("100")?.name).toBe("coordinator");
61
+ await store.archiveAgent("coordinator");
62
+ expect(store.agentByChannel("100")).toBeUndefined();
63
+ });
64
+
65
+ test("display names change; names are checked and never reused", async () => {
66
+ const agent = await store.updateAgent("infra", {
67
+ displayName: " Ops ",
68
+ });
69
+ expect(agent.displayName).toBe("Ops");
70
+ expect(agent.name).toBe("infra");
71
+ await expect(
72
+ store.updateAgent("infra", { displayName: "discord bot" }),
73
+ ).rejects.toBeInstanceOf(AgentError);
74
+ expect(() => store.checkNewName("Bad Name")).toThrow(AgentError);
75
+ await store.archiveAgent("doctor");
76
+ expect(() => store.checkNewName("doctor")).toThrow(/taken/);
77
+ expect(() => store.activeAgent("doctor")).toThrow(/archived/);
78
+ });
79
+
80
+ test("prompts are limited to 4,000 characters", async () => {
81
+ await expect(
82
+ store.updateAgent("infra", { prompt: "x".repeat(4001) }),
83
+ ).rejects.toThrow(/4000/);
84
+ });
85
+ });
86
+
87
+ describe("model settings", () => {
88
+ test("model and thinking are kept, survive a reopen, and reset with null", async () => {
89
+ await store.updateAgent("infra", {
90
+ model: "openai-codex/gpt-6-sol",
91
+ thinking: "high",
92
+ });
93
+ const reopened = await openTestStore(AgentStore, TEST_GUILD);
94
+ expect(reopened.agent("infra")).toMatchObject({
95
+ model: "openai-codex/gpt-6-sol",
96
+ thinking: "high",
97
+ });
98
+ await reopened.close();
99
+ await store.updateAgent("infra", { prompt: "Other." });
100
+ expect(store.agent("infra")?.model).toBe("openai-codex/gpt-6-sol");
101
+ const reset = await store.updateAgent("infra", {
102
+ model: null,
103
+ thinking: null,
104
+ });
105
+ expect(reset.model).toBeUndefined();
106
+ expect(reset.thinking).toBeUndefined();
107
+ });
108
+
109
+ test("a malformed model or unknown thinking level is refused", async () => {
110
+ await expect(
111
+ store.updateAgent("infra", { model: "gpt-6-sol" }),
112
+ ).rejects.toThrow(AgentError);
113
+ await expect(
114
+ // @ts-expect-error: an unknown level from outside the type system
115
+ store.updateAgent("infra", { thinking: "extreme" }),
116
+ ).rejects.toThrow(/thinking level/);
117
+ });
118
+ });
119
+
120
+ describe("groups", () => {
121
+ const makeGroup = () =>
122
+ store.createGroup({
123
+ name: "ops",
124
+ displayName: "Ops",
125
+ channelId: "200",
126
+ members: ["coordinator", "infra", "doctor"],
127
+ host: "coordinator",
128
+ });
129
+
130
+ test("a group needs 2 to 6 distinct active members and a member host", async () => {
131
+ expect(() => store.checkNewGroup("g", ["infra"], "infra")).toThrow(
132
+ AgentError,
133
+ );
134
+ expect(() =>
135
+ store.checkNewGroup("g", ["infra", "doctor"], "coordinator"),
136
+ ).toThrow(/host/);
137
+ expect(() =>
138
+ store.checkNewGroup("infra", ["infra", "doctor"], "infra"),
139
+ ).toThrow(/taken/);
140
+ const group = await makeGroup();
141
+ expect(store.groupByChannel("200")?.members).toEqual(group.members);
142
+ });
143
+
144
+ test("an archived agent leaves its groups, which hand over hosting or archive", async () => {
145
+ await makeGroup();
146
+ await store.archiveAgent("coordinator");
147
+ const group = store.group("ops");
148
+ expect(group?.members).toEqual(["infra", "doctor"]);
149
+ expect(group?.host).toBe("infra");
150
+ await store.archiveAgent("doctor");
151
+ expect(store.group("ops")?.status).toBe("archived");
152
+ });
153
+
154
+ test("each message reaches each member once, its own excluded", async () => {
155
+ await makeGroup();
156
+ const owner = await store.appendGroupMessage("ops", {
157
+ author: "owner",
158
+ authorName: "Riley",
159
+ text: "hi",
160
+ });
161
+ await store.appendGroupMessage("ops", {
162
+ author: "infra",
163
+ authorName: "INFRA",
164
+ text: "disk ok",
165
+ });
166
+ const first = await store.backlog("ops", "doctor", 40);
167
+ expect(first.messages.map((m) => m.text)).toEqual(["hi", "disk ok"]);
168
+ await store.advanceCursor("ops", "doctor", first.lastId ?? 0);
169
+ expect((await store.backlog("ops", "doctor", 40)).messages).toEqual([]);
170
+
171
+ const infra = await store.backlog("ops", "infra", 40);
172
+ expect(infra.messages.map((m) => m.id)).toEqual([owner]);
173
+ await store.advanceCursor("ops", "infra", infra.lastId ?? 0);
174
+
175
+ // A cursor never moves back, so a message is never delivered twice.
176
+ await store.advanceCursor("ops", "infra", owner - 1);
177
+ expect((await store.backlog("ops", "infra", 40)).messages).toEqual([]);
178
+ });
179
+
180
+ test("a long backlog carries the newest messages and counts the rest", async () => {
181
+ await makeGroup();
182
+ for (let i = 0; i < 5; i++)
183
+ await store.appendGroupMessage("ops", {
184
+ author: "owner",
185
+ authorName: "Riley",
186
+ text: `m${i}`,
187
+ });
188
+ const backlog = await store.backlog("ops", "doctor", 2);
189
+ expect(backlog.messages.map((m) => m.text)).toEqual(["m3", "m4"]);
190
+ expect(backlog.skipped).toBe(3);
191
+ });
192
+
193
+ test("catching up marks everything as received", async () => {
194
+ const group = await makeGroup();
195
+ await store.appendGroupMessage("ops", {
196
+ author: "owner",
197
+ authorName: "Riley",
198
+ text: "old",
199
+ });
200
+ await store.catchUp(group);
201
+ for (const member of group.members)
202
+ expect((await store.backlog("ops", member, 40)).messages).toEqual([]);
203
+ });
204
+ });
205
+ });