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,117 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ConfigError } from "./domain/errors.ts";
3
+ import {
4
+ CORE_TOOL_TIERS,
5
+ parseToolTiers,
6
+ toolsForTier,
7
+ toolTiers,
8
+ } from "./tool-tiers.ts";
9
+
10
+ describe("default tiers", () => {
11
+ const tiers = toolTiers();
12
+
13
+ test("members may look, read, and message agents", () => {
14
+ for (const tool of [
15
+ "agent_list",
16
+ "agent_get",
17
+ "message_agent",
18
+ "channel_read",
19
+ "ask_user",
20
+ "web_search",
21
+ ])
22
+ expect(tiers.minTier(tool)).toBe("member");
23
+ });
24
+
25
+ test("every speaker may keep a memory of their own", () => {
26
+ for (const tool of ["memory_add", "memory_search", "memory_remove"])
27
+ expect(tiers.minTier(tool)).toBe("member");
28
+ });
29
+
30
+ test("creating, editing, archiving, and arranging agents and groups need an admin", () => {
31
+ for (const tool of [
32
+ "agent_create",
33
+ "agent_update",
34
+ "agent_avatar",
35
+ "group_create",
36
+ "group_update",
37
+ "archive",
38
+ "channel_arrange",
39
+ "agent_skills",
40
+ "schedule_create",
41
+ "delegate_task",
42
+ ])
43
+ expect(tiers.minTier(tool)).toBe("admin");
44
+ });
45
+
46
+ test("the shell, notifying the owner, Discord administration, and any tool nobody named stay with the owner", () => {
47
+ for (const tool of [
48
+ "bash",
49
+ "write",
50
+ "notify_owner",
51
+ "discord_ban",
52
+ "some_plugin_tool",
53
+ ])
54
+ expect(tiers.minTier(tool)).toBe("owner");
55
+ });
56
+
57
+ test("every tool an agent carries from the core is named on purpose", () => {
58
+ expect(Object.keys(CORE_TOOL_TIERS)).toEqual(
59
+ expect.arrayContaining(["agent_create", "skill_create", "schedule_list"]),
60
+ );
61
+ });
62
+ });
63
+
64
+ describe("toolsForTier", () => {
65
+ const tiers = toolTiers();
66
+ const all = ["agent_list", "agent_create", "bash", "some_plugin_tool"];
67
+
68
+ test("each tier gets the tools at or below it, in order", () => {
69
+ expect(toolsForTier(all, "member", tiers)).toEqual(["agent_list"]);
70
+ expect(toolsForTier(all, "admin", tiers)).toEqual([
71
+ "agent_list",
72
+ "agent_create",
73
+ ]);
74
+ expect(toolsForTier(all, "owner", tiers)).toEqual(all);
75
+ });
76
+
77
+ test("an operator's map lowers or raises a tool", () => {
78
+ const custom = toolTiers({ bash: "admin", agent_list: "owner" });
79
+ expect(toolsForTier(all, "member", custom)).toEqual([]);
80
+ expect(toolsForTier(all, "admin", custom)).toEqual([
81
+ "agent_create",
82
+ "bash",
83
+ ]);
84
+ });
85
+
86
+ test("a plugin's declared tier is read at use time; the operator's still wins", () => {
87
+ const table = toolTiers({ plugin_b: "owner" });
88
+ expect(table.minTier("plugin_a")).toBe("owner");
89
+ table.declare("a", { plugin_a: "member" });
90
+ table.declare("b", { plugin_b: "member" });
91
+ expect(table.minTier("plugin_a")).toBe("member");
92
+ expect(table.minTier("plugin_b")).toBe("owner");
93
+ });
94
+
95
+ test("a tool two plugins declare names both plugins and the fix", () => {
96
+ const table = toolTiers();
97
+ table.declare("a", { shared_tool: "member" });
98
+ expect(() => table.declare("b", { shared_tool: "member" })).toThrow(
99
+ "plugin b: tool shared_tool is already defined by plugin a. Rename one of the two tools.",
100
+ );
101
+ });
102
+ });
103
+
104
+ describe("parseToolTiers", () => {
105
+ test("reads tool=tier pairs", () => {
106
+ expect(parseToolTiers("bash=admin, web_search=member", "X")).toEqual({
107
+ bash: "admin",
108
+ web_search: "member",
109
+ });
110
+ expect(parseToolTiers(undefined, "X")).toEqual({});
111
+ });
112
+
113
+ test("refuses a pair that is not a tier", () => {
114
+ expect(() => parseToolTiers("bash=root", "X")).toThrow(ConfigError);
115
+ expect(() => parseToolTiers("bash", "X")).toThrow(ConfigError);
116
+ });
117
+ });
@@ -0,0 +1,127 @@
1
+ import { AGENT_TOOLS } from "./agents/agent-tools.ts";
2
+ import { ConfigError } from "./domain/errors.ts";
3
+ import { PluginError } from "./errors.ts";
4
+ import { SKILL_LIST_TOOL, SKILL_TOOLS } from "./modules/skills/skill-tools.ts";
5
+ import { ASK_USER_TOOL } from "./runtime/extensions/ask-user.ts";
6
+ import { COMPACT_TOOL } from "./runtime/extensions/self-compact-guard.ts";
7
+ import { DELEGATE_TOOL } from "./shared/delegate-tool.ts";
8
+ import { SCHEDULE_TOOLS } from "./shared/schedule-tools.ts";
9
+ import { TIERS, type Tier, tierAtLeast } from "./speakers.ts";
10
+
11
+ /** The lowest tier that may use each tool; a tool that is not named needs the owner. */
12
+ export interface ToolTiers {
13
+ minTier(tool: string): Tier;
14
+ /** Whether a speaker of `tier` may use the tool. */
15
+ allows(tier: Tier, tool: string): boolean;
16
+ }
17
+
18
+ const set = (tier: Tier, tools: readonly string[]) =>
19
+ Object.fromEntries(tools.map((tool) => [tool, tier]));
20
+
21
+ /**
22
+ * The core's default tiers. Members talk, read, look things up, and message agents; admins
23
+ * also create and edit agents, groups, skills, schedules, and delegated work; everything else,
24
+ * such as the shell, the owner's notes, Discord administration, and every plugin's tools,
25
+ * stays with the owner until an operator lowers it.
26
+ */
27
+ export const CORE_TOOL_TIERS: Readonly<Record<string, Tier>> = {
28
+ ...set("admin", [
29
+ ...AGENT_TOOLS,
30
+ ...SKILL_TOOLS,
31
+ ...SCHEDULE_TOOLS,
32
+ DELEGATE_TOOL,
33
+ ]),
34
+ ...set("member", [
35
+ "agent_list",
36
+ "agent_get",
37
+ "message_agent",
38
+ "channel_read",
39
+ "schedule_list",
40
+ SKILL_LIST_TOOL,
41
+ "read_attachment",
42
+ // Each speaker keeps their own; the owner's reaches only the owner's turns.
43
+ "memory_add",
44
+ "memory_search",
45
+ "memory_remove",
46
+ "web_search",
47
+ "fetch_content",
48
+ "get_search_content",
49
+ ]),
50
+ // The core's own tools serve any turn that can ask or compact.
51
+ ...set("member", [ASK_USER_TOOL, COMPACT_TOOL]),
52
+ };
53
+
54
+ /**
55
+ * What each tool needs: the operator's setting first, then the tier its plugin declared, then
56
+ * the core's default, and the owner for a tool nobody named. Plugins declare theirs when the
57
+ * host links them, so a reader asks at use time and sees the final table.
58
+ */
59
+ export class ToolTierTable implements ToolTiers {
60
+ readonly #operator: ReadonlyMap<string, Tier>;
61
+ readonly #declared = new Map<string, { plugin: string; tier: Tier }>();
62
+ readonly #core = new Map(Object.entries(CORE_TOOL_TIERS));
63
+
64
+ constructor(operator: Readonly<Record<string, Tier>> = {}) {
65
+ this.#operator = new Map(Object.entries(operator));
66
+ }
67
+
68
+ /** Records the tiers a plugin's tools need; a tool two plugins declare is a PluginError. */
69
+ declare(plugin: string, tiers: Readonly<Record<string, Tier>>): void {
70
+ for (const [tool, tier] of Object.entries(tiers)) {
71
+ const other = this.#declared.get(tool);
72
+ if (other)
73
+ throw new PluginError(
74
+ `plugin ${plugin}: tool ${tool} is already defined by plugin ${other.plugin}. Rename one of the two tools.`,
75
+ );
76
+ this.#declared.set(tool, { plugin, tier });
77
+ }
78
+ }
79
+
80
+ minTier(tool: string): Tier {
81
+ return (
82
+ this.#operator.get(tool) ??
83
+ this.#declared.get(tool)?.tier ??
84
+ this.#core.get(tool) ??
85
+ "owner"
86
+ );
87
+ }
88
+
89
+ allows(tier: Tier, tool: string): boolean {
90
+ return tierAtLeast(tier, this.minTier(tool));
91
+ }
92
+ }
93
+
94
+ /** The core's tiers with the operator's on top; plugins add theirs through `declare`. */
95
+ export function toolTiers(
96
+ operator: Readonly<Record<string, Tier>> = {},
97
+ ): ToolTierTable {
98
+ return new ToolTierTable(operator);
99
+ }
100
+
101
+ /** The tools a tier may use, in order; a turn nobody spoke in has the owner's tier. */
102
+ export function toolsForTier(
103
+ tools: readonly string[],
104
+ tier: Tier,
105
+ tiers: ToolTiers,
106
+ ): string[] {
107
+ return tools.filter((tool) => tiers.allows(tier, tool));
108
+ }
109
+
110
+ /** Reads `tool=tier` pairs, comma-separated; unset means no changes. */
111
+ export function parseToolTiers(
112
+ text: string | undefined,
113
+ name: string,
114
+ ): Record<string, Tier> {
115
+ const result: Record<string, Tier> = {};
116
+ for (const item of (text ?? "").split(",")) {
117
+ const pair = item.trim();
118
+ if (pair === "") continue;
119
+ const [tool, tier] = pair.split("=").map((part) => part.trim());
120
+ if (!tool || !tier || !(TIERS as readonly string[]).includes(tier))
121
+ throw new ConfigError(
122
+ `${name} must list tool=owner|admin|member pairs, got ${pair}`,
123
+ );
124
+ result[tool] = tier as Tier;
125
+ }
126
+ return result;
127
+ }
@@ -0,0 +1,112 @@
1
+ import { expect, test } from "bun:test";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import * as main from "./index.ts";
5
+ import * as testing from "./testing.ts";
6
+
7
+ // Types disappear at runtime. Update this list AND the changelog for every entry change.
8
+ export const MAIN_EXPORTS = [
9
+ "NotLinkedError",
10
+ "PluginError",
11
+ "Roundtable",
12
+ "ToolRefusal",
13
+ "definePlugin",
14
+ "defineRoundtable",
15
+ "defineTool",
16
+ ];
17
+ export const MAIN_TYPE_EXPORTS = [
18
+ "AgentSeed",
19
+ "ChannelClaim",
20
+ "Contribution",
21
+ "DefinedRoundtable",
22
+ "EventHandlers",
23
+ "HoldRule",
24
+ "InteractionContribution",
25
+ "Migration",
26
+ "PluginContext",
27
+ "PromptSection",
28
+ "PromptTurn",
29
+ "RoundtableConfig",
30
+ "RoundtableOptions",
31
+ "RoundtablePlugin",
32
+ "Service",
33
+ "SessionTool",
34
+ "Speaker",
35
+ "Tier",
36
+ "ToolContribution",
37
+ "ToolSpec",
38
+ "ToolTurn",
39
+ "TurnEndEvent",
40
+ "TurnEvent",
41
+ ];
42
+ export const TESTING_EXPORTS = ["testPlugin"];
43
+ export const TESTING_TYPE_EXPORTS = [
44
+ "RecordedEvent",
45
+ "TestPluginOptions",
46
+ "TestPluginResult",
47
+ ];
48
+
49
+ /** Source-level names include type-only exports, which Object.keys cannot observe. */
50
+ function entryNames(path: string): string[] {
51
+ const source = readFileSync(path, "utf8");
52
+ if (path.endsWith("index.ts"))
53
+ return [...source.matchAll(/export(?: type)?\s*\{([^}]+)\}/gs)]
54
+ .flatMap((match) =>
55
+ (match[1] ?? "")
56
+ .split(",")
57
+ .map((name) => name.trim())
58
+ .filter(Boolean),
59
+ )
60
+ .sort();
61
+ return [
62
+ ...source.matchAll(
63
+ /export (?:async )?(?:interface|function|type|class|const) (\w+)/g,
64
+ ),
65
+ ]
66
+ .map((match) => match[1] ?? "")
67
+ .sort();
68
+ }
69
+
70
+ test("the public entries have exactly the snapshotted runtime exports", () => {
71
+ expect(Object.keys(main).sort()).toEqual(MAIN_EXPORTS);
72
+ expect(Object.keys(testing).sort()).toEqual(TESTING_EXPORTS);
73
+ });
74
+
75
+ test("type-only names are snapshotted and every public name is in the changelog", () => {
76
+ expect(entryNames(resolve(import.meta.dir, "index.ts"))).toEqual(
77
+ [...MAIN_EXPORTS, ...MAIN_TYPE_EXPORTS].sort(),
78
+ );
79
+ expect(entryNames(resolve(import.meta.dir, "testing.ts"))).toEqual(
80
+ [...TESTING_EXPORTS, ...TESTING_TYPE_EXPORTS].sort(),
81
+ );
82
+ const local = resolve(import.meta.dir, "../packaging/CHANGELOG.md");
83
+ const changelog = readFileSync(
84
+ existsSync(local) ? local : resolve(import.meta.dir, "../CHANGELOG.md"),
85
+ "utf8",
86
+ );
87
+ for (const name of [
88
+ ...MAIN_EXPORTS,
89
+ ...MAIN_TYPE_EXPORTS,
90
+ ...TESTING_EXPORTS,
91
+ ...TESTING_TYPE_EXPORTS,
92
+ ])
93
+ expect(changelog).toContain(`\`${name}\``);
94
+ });
95
+
96
+ test("the package export map names exactly the existing entries", () => {
97
+ const template = resolve(import.meta.dir, "../packaging/package.json.tmpl");
98
+ const packageFile = existsSync(template)
99
+ ? template
100
+ : resolve(import.meta.dir, "../package.json");
101
+ const manifest = JSON.parse(readFileSync(packageFile, "utf8")) as {
102
+ exports: Record<string, string>;
103
+ };
104
+ expect(manifest.exports).toEqual({
105
+ ".": "./src/index.ts",
106
+ "./testing": "./src/testing.ts",
107
+ });
108
+ for (const target of Object.values(manifest.exports))
109
+ expect(
110
+ Bun.file(resolve(import.meta.dir, "..", target)).size,
111
+ ).toBeGreaterThan(0);
112
+ });
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // The plugin author's stable entry; internals remain behind package exports.
2
+
3
+ export type { AgentSeed } from "./core/agents/agent-store.ts";
4
+ export type { RoundtableConfig } from "./core/config/config.ts";
5
+ export type { ChannelClaim } from "./core/contract/channels.ts";
6
+ export type { InteractionContribution } from "./core/contract/discord.ts";
7
+ export type { Migration } from "./core/db/migrations.ts";
8
+ export type { ToolContribution, ToolSpec, ToolTurn } from "./core/define.ts";
9
+ export { definePlugin, defineTool, ToolRefusal } from "./core/define.ts";
10
+ export type { DefinedRoundtable } from "./core/define-roundtable.ts";
11
+ export { defineRoundtable } from "./core/define-roundtable.ts";
12
+ export { NotLinkedError, PluginError } from "./core/errors.ts";
13
+ export type { HoldRule } from "./core/holds.ts";
14
+ export type { RoundtableOptions } from "./core/host.ts";
15
+ export { Roundtable } from "./core/host.ts";
16
+ export type {
17
+ Contribution,
18
+ EventHandlers,
19
+ PluginContext,
20
+ PromptSection,
21
+ PromptTurn,
22
+ RoundtablePlugin,
23
+ Service,
24
+ TurnEndEvent,
25
+ TurnEvent,
26
+ } from "./core/plugin.ts";
27
+ export type { SessionTool } from "./core/sessions.ts";
28
+ export type { Speaker, Tier } from "./core/speakers.ts";
@@ -0,0 +1,156 @@
1
+ import { expect, test } from "bun:test";
2
+ import { Type } from "typebox";
3
+ import { definePlugin, defineTool, ToolRefusal } from "./index.ts";
4
+ import { testPlugin } from "./testing.ts";
5
+
6
+ const note = defineTool({
7
+ name: "note_add",
8
+ description: "Write a note",
9
+ parameters: Type.Object({ text: Type.String() }),
10
+ minTier: "member",
11
+ hold: (args) => (args.text === "danger" ? "Confirm note" : undefined),
12
+ run: (args, turn) => {
13
+ if (!args.text) throw new ToolRefusal("empty note");
14
+ return `${turn.speaker?.name ?? "anonymous"}: ${args.text}`;
15
+ },
16
+ });
17
+
18
+ test("the harness registers tools through their session factories and records tiers and holds", async () => {
19
+ const harness = await testPlugin(
20
+ definePlugin({ name: "notes", setup: () => ({ tools: [note] }) }),
21
+ );
22
+ expect(harness.tools).toEqual(["note_add"]);
23
+ expect(harness.tiers.minTier("note_add")).toBe("member");
24
+ expect(
25
+ harness.contribution.holdRules?.[0]?.describe(
26
+ "note_add",
27
+ { text: "danger" },
28
+ {},
29
+ ),
30
+ ).toBe("Confirm note");
31
+ expect(
32
+ await harness.runTool(
33
+ "note_add",
34
+ { text: "hello" },
35
+ { speaker: { id: "1", name: "Alice", tier: "owner" } },
36
+ ),
37
+ ).toBe("Alice: hello");
38
+ expect(await harness.runTool("note_add", { text: "" })).toBe("empty note");
39
+ await harness.stop();
40
+ });
41
+
42
+ test("ordinary tool errors reject the call rather than becoming model-visible refusals", async () => {
43
+ const broken = defineTool({
44
+ name: "broken_tool",
45
+ description: "Fail",
46
+ parameters: Type.Object({}),
47
+ minTier: "owner",
48
+ run: () => {
49
+ throw new Error("disk unavailable");
50
+ },
51
+ });
52
+ const harness = await testPlugin(
53
+ definePlugin({ name: "broken", setup: () => ({ tools: [broken] }) }),
54
+ );
55
+ await expect(harness.runTool("broken_tool", {})).rejects.toThrow(
56
+ "disk unavailable",
57
+ );
58
+ await harness.stop();
59
+ });
60
+
61
+ test("database and built-in services fail with the host's errors until provided", async () => {
62
+ let databaseError = "";
63
+ let coreError = "";
64
+ const harness = await testPlugin(
65
+ definePlugin({
66
+ name: "ports",
67
+ setup: (context) => {
68
+ try {
69
+ context.database();
70
+ } catch (error) {
71
+ databaseError = String(error);
72
+ }
73
+ try {
74
+ context.core.stores;
75
+ } catch (error) {
76
+ coreError = String(error);
77
+ }
78
+ return { events: {} };
79
+ },
80
+ }),
81
+ );
82
+ expect(databaseError).toBe("PluginError: no database is configured");
83
+ expect(coreError).toContain("core service stores is not provided yet");
84
+ await harness.stop();
85
+ });
86
+
87
+ test("events reach the plugin and are recorded", async () => {
88
+ const received: string[] = [];
89
+ const harness = await testPlugin(
90
+ definePlugin({
91
+ name: "listener",
92
+ setup: (context) => {
93
+ context.events.turnStarted({
94
+ agent: "helper",
95
+ channel: "test:1",
96
+ speaker: undefined,
97
+ });
98
+ return {
99
+ events: {
100
+ shutdown: () => {
101
+ received.push("shutdown");
102
+ },
103
+ },
104
+ };
105
+ },
106
+ }),
107
+ );
108
+ expect(harness.events).toEqual([
109
+ {
110
+ name: "turnStarted",
111
+ turn: { agent: "helper", channel: "test:1", speaker: undefined },
112
+ },
113
+ ]);
114
+ await harness.stop();
115
+ expect(received).toEqual(["shutdown"]);
116
+ });
117
+
118
+ test("a plugin that adds nothing gets the host's exact refusal", async () => {
119
+ await expect(
120
+ testPlugin(definePlugin({ name: "empty", setup: () => ({}) })),
121
+ ).rejects.toThrow(
122
+ "plugin empty adds nothing. Give it a part (tools, services, channels, and so on), a migration, or a provider, or remove it.",
123
+ );
124
+ });
125
+
126
+ test("sessions read during setup carry the host's NotLinkedError text", async () => {
127
+ await expect(
128
+ testPlugin(
129
+ definePlugin({
130
+ name: "early",
131
+ setup: (context) => {
132
+ context.sessions();
133
+ return { events: {} };
134
+ },
135
+ }),
136
+ ),
137
+ ).rejects.toThrow(
138
+ "session parts are linked once every plugin is set up. Call sessions() from a service's start or from a handler, not during setup.",
139
+ );
140
+ });
141
+
142
+ test("the harness returns the agent selection a plugin contributes", async () => {
143
+ const harness = await testPlugin(
144
+ definePlugin({
145
+ name: "selecting",
146
+ setup: () => ({
147
+ agentSelection: () => ({ tools: ["note_add"], groups: [] }),
148
+ }),
149
+ }),
150
+ );
151
+ expect(harness.contribution.agentSelection?.()).toEqual({
152
+ tools: ["note_add"],
153
+ groups: [],
154
+ });
155
+ await harness.stop();
156
+ });