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,176 @@
1
+ import { join } from "node:path";
2
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+ import { scheduleCommands } from "../discord/schedule-commands.ts";
4
+ import type { ChannelKey } from "../domain/conversation.ts";
5
+ import type { OwnerIdentity } from "../identity.ts";
6
+ import type { ModelRef, ThinkingLevel } from "../models.ts";
7
+ import { BackgroundTurns } from "../modules/background/background-turns.ts";
8
+ import { delegateExtension } from "../modules/delegation/delegate.ts";
9
+ import {
10
+ type DelegationWorker,
11
+ Delegator,
12
+ } from "../modules/delegation/delegator.ts";
13
+ import { SolWorker } from "../modules/delegation/sol-worker.ts";
14
+ import { discordAdminExtension } from "../modules/discord-admin/discord-admin.ts";
15
+ import { ownerMemoryExtension } from "../modules/memory/owner-memory.ts";
16
+ import { notifyExtension } from "../modules/notify/notify.ts";
17
+ import { Scheduler } from "../modules/schedules/scheduler.ts";
18
+ import {
19
+ type AgentChannelLookup,
20
+ schedulesExtension,
21
+ } from "../modules/schedules/schedules.ts";
22
+ import type { RoundtablePlugin } from "../plugin.ts";
23
+ import type {
24
+ SessionContext,
25
+ SessionTool,
26
+ SessionToolSnapshot,
27
+ } from "../sessions.ts";
28
+ import { THE_SPEAKER } from "../speakers.ts";
29
+
30
+ export interface ModulesOptions {
31
+ /** Who the tools serve in the owner's own sessions. */
32
+ owner: OwnerIdentity & { id: string };
33
+ /** The name a turn the process itself starts is written by. */
34
+ assistant: string;
35
+ /** Shared with the rest of the host, so logins refresh in one place. */
36
+ modelRuntime: ModelRuntime;
37
+ agentDir: string;
38
+ dataDir: string;
39
+ /** The model that runs delegated tasks, or a worker of your own that replaces it. */
40
+ delegation: {
41
+ model: ModelRef;
42
+ thinking: ThinkingLevel;
43
+ worker?: DelegationWorker;
44
+ };
45
+ /** Another agent's channel, for reading its schedules; throws ScheduleError when unknown. */
46
+ agentChannelOf?: AgentChannelLookup;
47
+ }
48
+
49
+ /** A session tool whose extension never changes. */
50
+ function fixed(
51
+ name: string,
52
+ factory: SessionToolSnapshot["factory"],
53
+ ): SessionTool {
54
+ return { name, phase: "tools", snapshot: () => ({ revision: 0, factory }) };
55
+ }
56
+
57
+ /**
58
+ * The owner's modules: memory, notifications, schedules, delegated tasks, and Discord reading
59
+ * and management, each a session tool, and the turns nobody wrote. The delegator's running jobs
60
+ * join the shutdown drain.
61
+ */
62
+ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
63
+ const { owner, agentChannelOf } = options;
64
+ // Outside the agent server a conversation has no channel of its own to post a run in.
65
+ const channelFor = async (
66
+ channel: ChannelKey,
67
+ surfaceOwnerChannel: () => Promise<ChannelKey>,
68
+ ) => (channel.startsWith("discord:") ? channel : surfaceOwnerChannel());
69
+ return {
70
+ name: "modules",
71
+ setup: ({ conversations, core, logger }) => {
72
+ const { stores, discord } = core;
73
+ const { surface } = discord;
74
+ const ownerChannelFor = (channel: ChannelKey) =>
75
+ channelFor(channel, () => surface.ownerChannel());
76
+ const background = new BackgroundTurns({
77
+ conversations,
78
+ system: { id: "assistant", name: options.assistant },
79
+ logger,
80
+ });
81
+ const delegator = new Delegator({
82
+ worker:
83
+ options.delegation.worker ??
84
+ new SolWorker({
85
+ modelRuntime: options.modelRuntime,
86
+ agentDir: options.agentDir,
87
+ workDir: join(options.dataDir, "delegate"),
88
+ model: options.delegation.model,
89
+ thinking: options.delegation.thinking,
90
+ }),
91
+ deliver: (job, outcome) => background.runDelegated(job, outcome),
92
+ threads: discord.threads,
93
+ logger,
94
+ });
95
+ core.provide("background", background);
96
+ core.provide("delegator", delegator);
97
+ // An agent session serves every speaker, so its tools name none.
98
+ const served = (session: SessionContext) =>
99
+ session.agent ? THE_SPEAKER : owner;
100
+ return {
101
+ services: [
102
+ { name: "delegator", busy: () => delegator.runningChannels() },
103
+ ],
104
+ sessionTools: [
105
+ fixed("owner-memory", (session) =>
106
+ ownerMemoryExtension(
107
+ stores.memory,
108
+ served(session),
109
+ // The owner's own chats have one speaker; the agent server's have several.
110
+ session.agent ? session.speaker : undefined,
111
+ ),
112
+ ),
113
+ fixed("notify", () => notifyExtension(surface, owner)),
114
+ fixed("schedules", (session) =>
115
+ schedulesExtension(
116
+ {
117
+ store: stores.schedules,
118
+ owner: { id: owner.id, name: owner.name },
119
+ channelFor: ownerChannelFor,
120
+ },
121
+ session.homeChannel,
122
+ served(session),
123
+ session.agent ? agentChannelOf : undefined,
124
+ session.speaker,
125
+ ),
126
+ ),
127
+ fixed("delegate", (session) =>
128
+ delegateExtension(
129
+ {
130
+ delegator,
131
+ owner: { id: owner.id, name: owner.name },
132
+ channelFor: ownerChannelFor,
133
+ },
134
+ session.homeChannel,
135
+ served(session),
136
+ session.turnChannel,
137
+ session.speaker,
138
+ ),
139
+ ),
140
+ fixed("discord-admin", () =>
141
+ discordAdminExtension(surface.ownerDiscord(), owner),
142
+ ),
143
+ ],
144
+ };
145
+ },
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Fires the schedules that fall due, and adds their slash commands. Registered apart from the
151
+ * modules so a plugin list can start the scheduler after everything a fired schedule reaches.
152
+ */
153
+ export function schedulerPlugin(): RoundtablePlugin {
154
+ return {
155
+ name: "schedules",
156
+ setup: ({ core, logger }) => {
157
+ const scheduler = new Scheduler({
158
+ store: core.stores.schedules,
159
+ runner: core.background,
160
+ logger,
161
+ });
162
+ return {
163
+ services: [
164
+ {
165
+ name: "scheduler",
166
+ start: () => scheduler.start(),
167
+ stop: () => scheduler.stop(),
168
+ },
169
+ ],
170
+ interactions: [
171
+ scheduleCommands(core.discord.guard, core.stores.schedules),
172
+ ],
173
+ };
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,10 @@
1
+ import type { AgentSeed } from "../agents/agent-rules.ts";
2
+ import type { RoundtablePlugin } from "../plugin.ts";
3
+
4
+ /** The configured first team, created on the first start; an agent already stored is never overwritten. */
5
+ export function seedsPlugin(agents: readonly AgentSeed[]): RoundtablePlugin {
6
+ return {
7
+ name: "seeds",
8
+ setup: () => ({ seeds: agents }),
9
+ };
10
+ }
@@ -0,0 +1,42 @@
1
+ import { AgentStore } from "../agents/agent-store.ts";
2
+ import { OwnerMemoryStore } from "../modules/memory/owner-memory-store.ts";
3
+ import { ScheduleStore } from "../modules/schedules/schedule-store.ts";
4
+ import { SkillStore } from "../modules/skills/skill-store.ts";
5
+ import type { RoundtablePlugin } from "../plugin.ts";
6
+ import { PendingConfirmationStore } from "../runtime/pending-confirmation-store.ts";
7
+
8
+ export interface StoresOptions {
9
+ /** The owner's Discord id: the speaker whose memory the first rows belong to. */
10
+ ownerId: string;
11
+ /** The agent server's guild; every agent-server row carries it. */
12
+ guildId: string;
13
+ }
14
+
15
+ /**
16
+ * The core's own tables and the stores over them. The host migrates them over its one pool
17
+ * before any setup, in this order, and closes the pool after every service has stopped.
18
+ */
19
+ export function storesPlugin(options: StoresOptions): RoundtablePlugin {
20
+ const { ownerId, guildId } = options;
21
+ return {
22
+ name: "stores",
23
+ migrations: [
24
+ ...OwnerMemoryStore.migrations(ownerId),
25
+ ScheduleStore.migration,
26
+ PendingConfirmationStore.migration,
27
+ ...AgentStore.migrations(guildId),
28
+ ...SkillStore.migrations(guildId),
29
+ ],
30
+ setup: async ({ database, core }) => {
31
+ const sql = database();
32
+ core.provide("stores", {
33
+ memory: OwnerMemoryStore.attach(sql, ownerId),
34
+ schedules: await ScheduleStore.attach(sql),
35
+ confirmations: await PendingConfirmationStore.attach(sql),
36
+ agents: await AgentStore.attach(sql, guildId),
37
+ skills: await SkillStore.attach(sql, guildId),
38
+ });
39
+ return {};
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ConfigError } from "../domain/errors.ts";
3
+ import { type RoundtableConfig, resolveConfig } from "./config.ts";
4
+
5
+ const minimal: RoundtableConfig = {
6
+ owner: { id: "100000000000000001", name: "Ada" },
7
+ discord: {
8
+ token: "token",
9
+ guild: "900000000000000001",
10
+ entryChannel: "900000000000000002",
11
+ },
12
+ database: { url: "postgres://localhost/roundtable" },
13
+ dataDir: "/data",
14
+ model: "anthropic/claude-sonnet-5-5",
15
+ http: { publicUrl: "https://bot.example.com" },
16
+ };
17
+
18
+ const refused = (input: unknown): string => {
19
+ try {
20
+ resolveConfig(input);
21
+ } catch (error) {
22
+ if (error instanceof ConfigError) return error.message;
23
+ throw error;
24
+ }
25
+ return "";
26
+ };
27
+
28
+ describe("resolveConfig", () => {
29
+ test("fills every default from a minimal configuration", () => {
30
+ const config = resolveConfig(minimal);
31
+ expect(config.name).toBe("Roundtable");
32
+ expect(config.owner).toEqual({
33
+ id: "100000000000000001",
34
+ name: "Ada",
35
+ pronouns: { subject: "they", object: "them", possessive: "their" },
36
+ });
37
+ expect(config.discord.rootCommand).toBe("roundtable");
38
+ expect(config.agentDir).toBe("/data/pi");
39
+ expect(config.model).toEqual({
40
+ provider: "anthropic",
41
+ id: "claude-sonnet-5-5",
42
+ });
43
+ expect(config.thinking).toBe("medium");
44
+ expect(config.judge).toEqual({ model: config.model, threshold: 0.6 });
45
+ expect(config.delegation).toEqual({
46
+ model: config.model,
47
+ thinking: "medium",
48
+ });
49
+ expect(config.locale).toBe("en");
50
+ expect(config.timeZone).toBe("UTC");
51
+ expect(config.http.port).toBe(3000);
52
+ expect(config.agents).toEqual([]);
53
+ expect(config.plugins).toEqual([]);
54
+ });
55
+
56
+ test("the root command follows the assistant's name unless one is given", () => {
57
+ expect(
58
+ resolveConfig({ ...minimal, name: "Robin Hood" }).discord.rootCommand,
59
+ ).toBe("robin-hood");
60
+ expect(
61
+ resolveConfig({
62
+ ...minimal,
63
+ discord: { ...minimal.discord, rootCommand: "rh" },
64
+ }).discord.rootCommand,
65
+ ).toBe("rh");
66
+ });
67
+
68
+ test("pronouns become the words prompts use", () => {
69
+ const owner = { ...minimal.owner, pronouns: "she" as const };
70
+ expect(resolveConfig({ ...minimal, owner }).owner.pronouns).toEqual({
71
+ subject: "she",
72
+ object: "her",
73
+ possessive: "her",
74
+ });
75
+ });
76
+
77
+ test("an unknown key names the nearest known key, at any depth", () => {
78
+ expect(refused({ ...minimal, discrod: {} })).toStartWith(
79
+ 'config discrod: unknown key. Did you mean "discord"? The keys here are name, owner, discord,',
80
+ );
81
+ expect(
82
+ refused({ ...minimal, discord: { ...minimal.discord, tokn: "x" } }),
83
+ ).toContain('config discord.tokn: unknown key. Did you mean "token"?');
84
+ });
85
+
86
+ test("a missing required key names it and the fix", () => {
87
+ const { model: _model, ...without } = minimal;
88
+ expect(refused(without)).toBe(
89
+ "config model: required, expected a non-empty string. Add it to roundtable.config.ts.",
90
+ );
91
+ expect(refused({ ...minimal, owner: { name: "Ada" } })).toBe(
92
+ "config owner.id: required, expected a non-empty string. Add it to roundtable.config.ts.",
93
+ );
94
+ });
95
+
96
+ test("a wrong value says what was expected and what it got", () => {
97
+ expect(refused({ ...minimal, thinking: "loud" })).toBe(
98
+ 'config thinking: expected one of off, minimal, low, medium, high, xhigh, got "loud". Fix the value in roundtable.config.ts.',
99
+ );
100
+ expect(
101
+ refused({ ...minimal, http: { publicUrl: "https://x", port: 70000 } }),
102
+ ).toContain(
103
+ "config http.port: expected an integer from 1 to 65535, got 70000.",
104
+ );
105
+ expect(refused({ ...minimal, model: "sonnet" })).toBe(
106
+ 'config model: expected <provider>/<id>, got "sonnet". Write it like anthropic/claude-sonnet-5-5.',
107
+ );
108
+ expect(refused({ ...minimal, locale: "fr" })).toContain(
109
+ "config locale: expected a locale, en or zh-TW",
110
+ );
111
+ expect(refused({ ...minimal, toolTiers: { shell: "root" } })).toContain(
112
+ "config toolTiers.shell: expected one of owner, admin, member",
113
+ );
114
+ expect(refused({ ...minimal, plugins: [{ name: "x" }] })).toContain(
115
+ "config plugins[0]: expected a plugin, an object with a name and a setup function",
116
+ );
117
+ expect(refused("nope")).toContain("expected an object");
118
+ });
119
+
120
+ test("agents are seeds with their four texts", () => {
121
+ const seed = {
122
+ name: "coordinator",
123
+ displayName: "Coordinator",
124
+ prompt: "You coordinate.",
125
+ avatarPrompt: "A calm coordinator.",
126
+ channelId: "900000000000000002",
127
+ };
128
+ expect(resolveConfig({ ...minimal, agents: [seed] }).agents).toEqual([
129
+ seed,
130
+ ]);
131
+ expect(refused({ ...minimal, agents: [{ name: "x" }] })).toContain(
132
+ "config agents[0].displayName: required",
133
+ );
134
+ });
135
+ });
@@ -0,0 +1,285 @@
1
+ import type { AgentSeed } from "../agents/agent-rules.ts";
2
+ import { ConfigError } from "../domain/errors.ts";
3
+ import { isLocale, type Locale } from "../i18n/index.ts";
4
+ import type { OwnerIdentity } from "../identity.ts";
5
+ import {
6
+ type ModelRef,
7
+ parseModelRef,
8
+ THINKING_LEVELS,
9
+ type ThinkingLevel,
10
+ } from "../models.ts";
11
+ import type { RoundtablePlugin } from "../plugin.ts";
12
+ import type { Tier, TierMembers } from "../speakers.ts";
13
+ import {
14
+ bool,
15
+ guarded,
16
+ integer,
17
+ list,
18
+ number,
19
+ oneOf,
20
+ optional,
21
+ record,
22
+ shape,
23
+ text,
24
+ } from "./schema.ts";
25
+
26
+ /** How prompts refer back to the owner. */
27
+ export type Pronouns = "he" | "she" | "they";
28
+
29
+ /** Who holds a tier besides the owner: user ids, role ids, and for members everyone. */
30
+ export interface TierConfig {
31
+ users?: readonly string[];
32
+ roles?: readonly string[];
33
+ everyone?: boolean;
34
+ }
35
+
36
+ /** What `roundtable.config.ts` gives `defineRoundtable`. */
37
+ export interface RoundtableConfig {
38
+ /** The assistant's display name; default "Roundtable". */
39
+ name?: string;
40
+ /** The one owner: the only person who can change everything. */
41
+ owner: { id: string; name: string; pronouns?: Pronouns };
42
+ discord: {
43
+ /** The bot token; keep it in `.env`, not in this file. */
44
+ token: string;
45
+ /** The agent server's guild id. */
46
+ guild: string;
47
+ /** The channel the coordinator lives in. */
48
+ entryChannel: string;
49
+ /** The root slash command, without the slash; default the lowercase assistant name. */
50
+ rootCommand?: string;
51
+ };
52
+ database: { url: string };
53
+ /** Where the process keeps its files, such as pictures, attachments, and skills. */
54
+ dataDir: string;
55
+ /** Pi's agent directory, holding the model logins; default `<dataDir>/pi`. */
56
+ agentDir?: string;
57
+ /** The model of the agents, written `<provider>/<id>`. */
58
+ model: string;
59
+ /** The level a turn thinks at when its judge cannot pick; default "medium". */
60
+ thinking?: ThinkingLevel;
61
+ /** The model that judges small questions and the delegated tasks' model; default the agents' model. */
62
+ judge?: { model?: string; threshold?: number };
63
+ delegation?: { model?: string; thinking?: ThinkingLevel };
64
+ /** The language of what the bot shows in Discord; default "en". */
65
+ locale?: Locale;
66
+ /** The IANA time zone schedules and stamps use; default "UTC". */
67
+ timeZone?: string;
68
+ /** Who may talk to the agents besides the owner, and what each tier may use. */
69
+ speakers?: { admins?: TierConfig; members?: TierConfig };
70
+ toolTiers?: Record<string, Tier>;
71
+ /** Prompt files, read once at start; `shared` starts every agent's prompt. */
72
+ prompts?: { shared: string; guest?: string };
73
+ /** The first team, created once; an agent already stored is never overwritten. */
74
+ agents?: AgentSeed[];
75
+ /**
76
+ * Where the agents' avatars are served: the public address that reaches it, and the TCP port
77
+ * (default 3000) or unix socket the process listens on.
78
+ */
79
+ http: {
80
+ publicUrl: string;
81
+ port?: number;
82
+ hostname?: string;
83
+ socketPath?: string;
84
+ };
85
+ /** A picture of your own for agents without one, and the style reference for drawing them; default a plain bot. */
86
+ avatar?: string;
87
+ /** The agents' shared working directory; default `<dataDir>/work`. */
88
+ workDir?: string;
89
+ skills?: { builtinDir?: string; reposDir?: string };
90
+ /** The agent that investigates the process's own errors, by name. */
91
+ ops?: { agent: string };
92
+ plugins?: RoundtablePlugin[];
93
+ }
94
+
95
+ const tierMembers = shape({
96
+ users: optional(list(text)),
97
+ roles: optional(list(text)),
98
+ everyone: optional(bool),
99
+ });
100
+
101
+ const isPlugin = (value: unknown): value is RoundtablePlugin =>
102
+ typeof value === "object" &&
103
+ value !== null &&
104
+ typeof (value as { name?: unknown }).name === "string" &&
105
+ typeof (value as { setup?: unknown }).setup === "function";
106
+
107
+ const schema = shape({
108
+ name: optional(text),
109
+ owner: shape({
110
+ id: text,
111
+ name: text,
112
+ pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
113
+ }),
114
+ discord: shape({
115
+ token: text,
116
+ guild: text,
117
+ entryChannel: text,
118
+ rootCommand: optional(text),
119
+ }),
120
+ database: shape({ url: text }),
121
+ dataDir: text,
122
+ agentDir: optional(text),
123
+ model: text,
124
+ thinking: optional(oneOf(...THINKING_LEVELS)),
125
+ judge: optional(
126
+ shape({ model: optional(text), threshold: optional(number(0, 1)) }),
127
+ ),
128
+ delegation: optional(
129
+ shape({
130
+ model: optional(text),
131
+ thinking: optional(oneOf(...THINKING_LEVELS)),
132
+ }),
133
+ ),
134
+ locale: optional(
135
+ guarded("a locale, en or zh-TW", (value): value is Locale =>
136
+ isLocale(value),
137
+ ),
138
+ ),
139
+ timeZone: optional(text),
140
+ speakers: optional(
141
+ shape({ admins: optional(tierMembers), members: optional(tierMembers) }),
142
+ ),
143
+ toolTiers: optional(record(oneOf<Tier>("owner", "admin", "member"))),
144
+ prompts: optional(shape({ shared: text, guest: optional(text) })),
145
+ agents: optional(
146
+ list(
147
+ shape({
148
+ name: text,
149
+ displayName: text,
150
+ prompt: text,
151
+ avatarPrompt: text,
152
+ channelId: optional(text),
153
+ }),
154
+ ),
155
+ ),
156
+ http: shape({
157
+ publicUrl: text,
158
+ port: optional(integer(1, 65535)),
159
+ hostname: optional(text),
160
+ socketPath: optional(text),
161
+ }),
162
+ avatar: optional(text),
163
+ workDir: optional(text),
164
+ skills: optional(
165
+ shape({ builtinDir: optional(text), reposDir: optional(text) }),
166
+ ),
167
+ ops: optional(shape({ agent: text })),
168
+ plugins: optional(
169
+ list(
170
+ guarded("a plugin, an object with a name and a setup function", isPlugin),
171
+ ),
172
+ ),
173
+ });
174
+
175
+ const PRONOUNS: Record<Pronouns, OwnerIdentity["pronouns"]> = {
176
+ he: { subject: "he", object: "him", possessive: "his" },
177
+ she: { subject: "she", object: "her", possessive: "her" },
178
+ they: { subject: "they", object: "them", possessive: "their" },
179
+ };
180
+
181
+ /** The configuration with every default filled in. */
182
+ export interface ResolvedConfig {
183
+ name: string;
184
+ owner: OwnerIdentity & { id: string };
185
+ discord: {
186
+ token: string;
187
+ guild: string;
188
+ entryChannel: string;
189
+ rootCommand: string;
190
+ };
191
+ databaseUrl: string;
192
+ dataDir: string;
193
+ agentDir: string;
194
+ model: ModelRef;
195
+ thinking: ThinkingLevel;
196
+ judge: { model: ModelRef; threshold: number };
197
+ delegation: { model: ModelRef; thinking: ThinkingLevel };
198
+ locale: Locale;
199
+ timeZone: string;
200
+ speakers: { admins?: TierMembers; members?: TierMembers };
201
+ toolTiers: Record<string, Tier>;
202
+ prompts: { shared?: string; guest?: string };
203
+ agents: AgentSeed[];
204
+ http: {
205
+ publicUrl: string;
206
+ port: number;
207
+ hostname?: string;
208
+ socketPath?: string;
209
+ };
210
+ avatar?: string;
211
+ workDir: string;
212
+ skills: { builtinDir?: string; reposDir?: string };
213
+ ops?: { agent: string };
214
+ plugins: RoundtablePlugin[];
215
+ }
216
+
217
+ function modelOf(value: string, path: string): ModelRef {
218
+ const parsed = parseModelRef(value);
219
+ if (!parsed)
220
+ throw new ConfigError(
221
+ `config ${path}: expected <provider>/<id>, got ${JSON.stringify(value)}. Write it like anthropic/claude-sonnet-5-5.`,
222
+ );
223
+ return parsed;
224
+ }
225
+
226
+ /** The configuration checked against its schema, defaults filled; throws a ConfigError naming the key and the fix. */
227
+ export function resolveConfig(input: unknown): ResolvedConfig {
228
+ const config = schema.check(input, "") as RoundtableConfig;
229
+ const name = config.name ?? "Roundtable";
230
+ const model = modelOf(config.model, "model");
231
+ const thinking = config.thinking ?? "medium";
232
+ return {
233
+ name,
234
+ owner: {
235
+ id: config.owner.id,
236
+ name: config.owner.name,
237
+ pronouns: PRONOUNS[config.owner.pronouns ?? "they"],
238
+ },
239
+ discord: {
240
+ token: config.discord.token,
241
+ guild: config.discord.guild,
242
+ entryChannel: config.discord.entryChannel,
243
+ rootCommand:
244
+ config.discord.rootCommand ??
245
+ name.toLowerCase().replace(/[^a-z0-9-]+/g, "-"),
246
+ },
247
+ databaseUrl: config.database.url,
248
+ dataDir: config.dataDir,
249
+ agentDir: config.agentDir ?? `${config.dataDir}/pi`,
250
+ model,
251
+ thinking,
252
+ judge: {
253
+ model: config.judge?.model
254
+ ? modelOf(config.judge.model, "judge.model")
255
+ : model,
256
+ threshold: config.judge?.threshold ?? 0.6,
257
+ },
258
+ delegation: {
259
+ model: config.delegation?.model
260
+ ? modelOf(config.delegation.model, "delegation.model")
261
+ : model,
262
+ thinking: config.delegation?.thinking ?? "medium",
263
+ },
264
+ locale: config.locale ?? "en",
265
+ timeZone: config.timeZone ?? "UTC",
266
+ speakers: {
267
+ ...(config.speakers?.admins ? { admins: config.speakers.admins } : {}),
268
+ ...(config.speakers?.members ? { members: config.speakers.members } : {}),
269
+ },
270
+ toolTiers: config.toolTiers ?? {},
271
+ prompts: config.prompts ?? {},
272
+ agents: config.agents ?? [],
273
+ http: {
274
+ publicUrl: config.http.publicUrl,
275
+ port: config.http.port ?? 3000,
276
+ ...(config.http.hostname ? { hostname: config.http.hostname } : {}),
277
+ ...(config.http.socketPath ? { socketPath: config.http.socketPath } : {}),
278
+ },
279
+ ...(config.avatar ? { avatar: config.avatar } : {}),
280
+ workDir: config.workDir ?? `${config.dataDir}/work`,
281
+ skills: config.skills ?? {},
282
+ ...(config.ops ? { ops: config.ops } : {}),
283
+ plugins: config.plugins ?? [],
284
+ };
285
+ }