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,144 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { definePlugin, defineTool, ToolRefusal } from "./define.ts";
5
+ import { PluginError } from "./errors.ts";
6
+ import type { SessionContext } from "./sessions.ts";
7
+ import type { Speaker } from "./speakers.ts";
8
+
9
+ const speaker: Speaker = { id: "1", name: "Ann", tier: "admin" };
10
+
11
+ const note = {
12
+ name: "note_add",
13
+ description: "Save a note.",
14
+ parameters: Type.Object({ text: Type.String() }),
15
+ minTier: "member" as const,
16
+ run: (args: { text: string }) => `saved ${args.text}`,
17
+ };
18
+
19
+ type Registered = {
20
+ name: string;
21
+ execute(
22
+ id: string,
23
+ params: unknown,
24
+ signal?: AbortSignal,
25
+ ): Promise<{ content: { text: string }[]; isError?: boolean }>;
26
+ };
27
+
28
+ /** Registers the tool in a fake Pi session the way the runtime does, and returns what it registered. */
29
+ function register(tool: ReturnType<typeof defineTool>): Registered {
30
+ const registered: Registered[] = [];
31
+ const context = {
32
+ speaker: () => speaker,
33
+ turnChannel: "discord:2",
34
+ agent: { name: "helper", session: "discord:2", home: "discord:2" },
35
+ } as unknown as SessionContext;
36
+ const factory = tool.session.snapshot().factory(context);
37
+ factory?.({
38
+ registerTool: (def: Registered) => registered.push(def),
39
+ } as unknown as ExtensionAPI);
40
+ const [one] = registered;
41
+ if (!one) throw new Error("nothing was registered");
42
+ return one;
43
+ }
44
+
45
+ describe("defineTool", () => {
46
+ test("registers the tool in a session and runs it with the turn's speaker", async () => {
47
+ let seen: unknown;
48
+ const tool = defineTool({
49
+ ...note,
50
+ run: (args, turn) => {
51
+ seen = { args, speaker: turn.speaker, channel: turn.channel };
52
+ return "ok";
53
+ },
54
+ });
55
+ const result = await register(tool).execute("call", { text: "milk" });
56
+ expect(result.content[0]?.text).toBe("ok");
57
+ expect(seen).toEqual({
58
+ args: { text: "milk" },
59
+ speaker,
60
+ channel: "discord:2",
61
+ });
62
+ expect(tool.session.snapshot().requiredTools).toEqual(["note_add"]);
63
+ expect(tool.minTier).toBe("member");
64
+ expect(tool.agent).toBe(true);
65
+ });
66
+
67
+ test("a ToolRefusal becomes an error result the model reads; any other error fails the call", async () => {
68
+ const refuse = defineTool({
69
+ ...note,
70
+ run: () => {
71
+ throw new ToolRefusal("no empty notes");
72
+ },
73
+ });
74
+ const refused = await register(refuse).execute("c", { text: "" });
75
+ expect(refused.isError).toBe(true);
76
+ expect(refused.content[0]?.text).toBe("no empty notes");
77
+ const fail = defineTool({
78
+ ...note,
79
+ run: () => {
80
+ throw new Error("disk gone");
81
+ },
82
+ });
83
+ await expect(register(fail).execute("c", { text: "x" })).rejects.toThrow(
84
+ "disk gone",
85
+ );
86
+ });
87
+
88
+ test("its hold rule describes the tool's own calls and no other", () => {
89
+ const tool = defineTool({
90
+ ...note,
91
+ hold: (args) => (args.text === "rm" ? "delete everything" : undefined),
92
+ });
93
+ const rule = tool.hold;
94
+ expect(rule?.name).toBe("tool:note_add");
95
+ expect(rule?.describe("note_add", { text: "rm" }, {})).toBe(
96
+ "delete everything",
97
+ );
98
+ expect(rule?.describe("note_add", { text: "hi" }, {})).toBeUndefined();
99
+ expect(rule?.describe("other", { text: "rm" }, {})).toBeUndefined();
100
+ });
101
+
102
+ test("each mistake is refused with the tool's name and the fix", () => {
103
+ const refused = (change: Record<string, unknown>) => () =>
104
+ defineTool({ ...note, ...change } as typeof note);
105
+ expect(refused({ name: "NoteAdd" })).toThrow(PluginError);
106
+ expect(refused({ name: "NoteAdd" })).toThrow(
107
+ 'tool "NoteAdd": the name must be lowercase words joined by underscores, such as note_add. Rename the tool.',
108
+ );
109
+ expect(refused({ name: "bash" })).toThrow(
110
+ "tool bash: the agents already have a tool of this name. Rename the tool.",
111
+ );
112
+ expect(refused({ minTier: undefined })).toThrow(
113
+ "tool note_add: minTier must be one of member, admin, owner; got undefined. Set the lowest tier that may use it.",
114
+ );
115
+ expect(refused({ description: " " })).toThrow(
116
+ "tool note_add: the description is empty. Tell the model when to call the tool.",
117
+ );
118
+ expect(refused({ run: undefined })).toThrow(
119
+ "tool note_add: run is missing. Give the function the tool runs.",
120
+ );
121
+ });
122
+ });
123
+
124
+ describe("definePlugin", () => {
125
+ const setup = () => ({});
126
+
127
+ test("returns the plugin it was given", () => {
128
+ const plugin = { name: "my-notes", setup };
129
+ expect(definePlugin(plugin)).toBe(plugin);
130
+ });
131
+
132
+ test("refuses a name that is not lowercase words joined by dashes, and a missing setup", () => {
133
+ expect(() => definePlugin({ name: "My Notes", setup })).toThrow(
134
+ 'plugin "My Notes": the name must be lowercase words joined by dashes, such as my-notes. Rename the plugin.',
135
+ );
136
+ expect(() =>
137
+ definePlugin({ name: "notes" } as unknown as Parameters<
138
+ typeof definePlugin
139
+ >[0]),
140
+ ).toThrow(
141
+ "plugin notes: setup is missing. Give the function that returns what the plugin adds.",
142
+ );
143
+ });
144
+ });
@@ -0,0 +1,154 @@
1
+ import type { Static, TObject } from "typebox";
2
+ import { PluginError } from "./errors.ts";
3
+ import type { HoldRule } from "./holds.ts";
4
+ import type { RoundtablePlugin } from "./plugin.ts";
5
+ import type { AgentTurnScope, ChannelKey, SessionTool } from "./sessions.ts";
6
+ import { toolError, toolText } from "./shared/tool-result.ts";
7
+ import { type Speaker, TIERS, type Tier } from "./speakers.ts";
8
+
9
+ /** Tool names are lowercase words joined by underscores, like every tool the core ships. */
10
+ const TOOL_NAME = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
11
+ /** Plugin names are lowercase words joined by dashes. */
12
+ const PLUGIN_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
13
+ /** Tools the agents and the shell already have; a plugin's tool may not take their names. */
14
+ const RESERVED_TOOLS = ["bash", "read", "edit", "write"];
15
+
16
+ /** What a tool knows about the turn that calls it. */
17
+ export interface ToolTurn {
18
+ /** Who the turn is for; undefined for a turn nobody spoke in. */
19
+ speaker: Speaker | undefined;
20
+ /** The channel the turn runs in. */
21
+ channel: ChannelKey;
22
+ /** The agent whose turn it is. */
23
+ agent: AgentTurnScope | undefined;
24
+ signal: AbortSignal | undefined;
25
+ }
26
+
27
+ /**
28
+ * Thrown by a tool's `run` for a call the model should correct: the message becomes the tool's
29
+ * error result, which the model reads. Any other error fails the call.
30
+ */
31
+ export class ToolRefusal extends Error {
32
+ override name = "ToolRefusal";
33
+ }
34
+
35
+ export interface ToolSpec<Schema extends TObject> {
36
+ /** Lowercase words joined by underscores; unique across every plugin. */
37
+ name: string;
38
+ /** What the model reads to decide when to call the tool. */
39
+ description: string;
40
+ /** The arguments, as a Typebox object; `run` receives them typed. */
41
+ parameters: Schema;
42
+ /** The lowest tier of speaker whose turns may call the tool; the operator's settings may change it. */
43
+ minTier: Tier;
44
+ /** Whether agents carry the tool in their turns; default true. */
45
+ agent?: boolean;
46
+ /** A short description of what the call would do when it must wait for approval; undefined lets it run. */
47
+ hold?: (args: Static<Schema>) => string | undefined;
48
+ run(args: Static<Schema>, turn: ToolTurn): Promise<string> | string;
49
+ }
50
+
51
+ /** A tool a plugin adds, as `defineTool` builds it. */
52
+ export interface ToolContribution {
53
+ readonly name: string;
54
+ readonly minTier: Tier;
55
+ readonly agent: boolean;
56
+ /** The extension that registers the tool in a session. */
57
+ readonly session: SessionTool;
58
+ /** The hold rule for the tool's `hold`, when it has one. */
59
+ readonly hold?: HoldRule;
60
+ }
61
+
62
+ /**
63
+ * Defines one tool: its name, what the model reads about it, its arguments, the tier that may
64
+ * use it, and what it does. A missing or unknown tier, or a name that is not a tool name, is
65
+ * refused here with the tool's name, before anything starts.
66
+ */
67
+ export function defineTool<Schema extends TObject>(
68
+ spec: ToolSpec<Schema>,
69
+ ): ToolContribution {
70
+ const { name } = spec;
71
+ if (typeof name !== "string" || !TOOL_NAME.test(name))
72
+ throw new PluginError(
73
+ `tool ${JSON.stringify(name)}: the name must be lowercase words joined by underscores, such as note_add. Rename the tool.`,
74
+ );
75
+ if (RESERVED_TOOLS.includes(name))
76
+ throw new PluginError(
77
+ `tool ${name}: the agents already have a tool of this name. Rename the tool.`,
78
+ );
79
+ if (!TIERS.includes(spec.minTier))
80
+ throw new PluginError(
81
+ `tool ${name}: minTier must be one of ${TIERS.join(", ")}; got ${JSON.stringify(spec.minTier)}. Set the lowest tier that may use it.`,
82
+ );
83
+ if (typeof spec.description !== "string" || !spec.description.trim())
84
+ throw new PluginError(
85
+ `tool ${name}: the description is empty. Tell the model when to call the tool.`,
86
+ );
87
+ if (typeof spec.run !== "function")
88
+ throw new PluginError(
89
+ `tool ${name}: run is missing. Give the function the tool runs.`,
90
+ );
91
+ const session: SessionTool = {
92
+ name: `tool:${name}`,
93
+ phase: "tools",
94
+ snapshot: () => ({
95
+ revision: 0,
96
+ requiredTools: [name],
97
+ factory: (context) => (pi) => {
98
+ pi.registerTool({
99
+ name,
100
+ label: name,
101
+ description: spec.description,
102
+ parameters: spec.parameters,
103
+ execute: async (_toolCallId, params, signal) => {
104
+ try {
105
+ return toolText(
106
+ await spec.run(params as Static<Schema>, {
107
+ speaker: context.speaker(),
108
+ channel: context.turnChannel,
109
+ agent: context.agent,
110
+ signal,
111
+ }),
112
+ );
113
+ } catch (error) {
114
+ if (error instanceof ToolRefusal) return toolError(error.message);
115
+ throw error;
116
+ }
117
+ },
118
+ });
119
+ },
120
+ }),
121
+ };
122
+ const hold = spec.hold;
123
+ return {
124
+ name,
125
+ minTier: spec.minTier,
126
+ agent: spec.agent ?? true,
127
+ session,
128
+ ...(hold
129
+ ? {
130
+ hold: {
131
+ name: `tool:${name}`,
132
+ describe: (tool, input) =>
133
+ tool === name ? hold(input as Static<Schema>) : undefined,
134
+ },
135
+ }
136
+ : {}),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Defines a plugin. It returns the plugin it was given, typed, after checking its name, so a
142
+ * mistake shows where the plugin is written.
143
+ */
144
+ export function definePlugin(plugin: RoundtablePlugin): RoundtablePlugin {
145
+ if (typeof plugin.name !== "string" || !PLUGIN_NAME.test(plugin.name))
146
+ throw new PluginError(
147
+ `plugin ${JSON.stringify(plugin.name)}: the name must be lowercase words joined by dashes, such as my-notes. Rename the plugin.`,
148
+ );
149
+ if (typeof plugin.setup !== "function")
150
+ throw new PluginError(
151
+ `plugin ${plugin.name}: setup is missing. Give the function that returns what the plugin adds.`,
152
+ );
153
+ return plugin;
154
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { messages } from "../i18n/index.ts";
3
+ import { formModels, skillsText } from "./agent-commands.ts";
4
+
5
+ describe("formModels", () => {
6
+ const usable = [
7
+ "claude-bridge/claude-opus-5-5",
8
+ "claude-bridge/claude-haiku-4-5",
9
+ "claude-bridge/claude-sonnet-5-5",
10
+ "openai-codex/gpt-6-luna",
11
+ "openai-codex/gpt-6-sol",
12
+ "openai-codex/gpt-6-astra",
13
+ ];
14
+
15
+ test("offers the five form models in order, leaving out other usable ones", () => {
16
+ expect(formModels(usable)).toEqual([
17
+ "openai-codex/gpt-6-astra",
18
+ "openai-codex/gpt-6-sol",
19
+ "openai-codex/gpt-6-luna",
20
+ "claude-bridge/claude-sonnet-5-5",
21
+ "claude-bridge/claude-opus-5-5",
22
+ ]);
23
+ });
24
+
25
+ test("leaves out form models the host cannot run", () => {
26
+ expect(
27
+ formModels(["claude-bridge/claude-opus-5-5", "openai-codex/gpt-6-sol"]),
28
+ ).toEqual(["openai-codex/gpt-6-sol", "claude-bridge/claude-opus-5-5"]);
29
+ });
30
+
31
+ test("puts the agent's current model first when the form does not offer it", () => {
32
+ expect(formModels(usable, "claude-bridge/claude-haiku-4-5")[0]).toBe(
33
+ "claude-bridge/claude-haiku-4-5",
34
+ );
35
+ expect(formModels(usable, "openai-codex/gpt-6-sol")).toEqual(
36
+ formModels(usable),
37
+ );
38
+ });
39
+ });
40
+
41
+ describe("skillsText", () => {
42
+ test("lists carried skills with the built-in one marked, and missing ones with their reason", () => {
43
+ const skill = (name: string) => ({
44
+ name,
45
+ description: "Use when x.",
46
+ file: `/s/${name}/SKILL.md`,
47
+ });
48
+ const text = messages();
49
+ expect(
50
+ skillsText({
51
+ skills: [skill("writing-skills"), skill("example-skill")],
52
+ missing: [{ name: "gone", reason: "file not found" }],
53
+ }),
54
+ ).toBe(
55
+ `${text.agentSkills([text.agentSkillBuiltin("writing-skills"), "`example-skill`"])}\n${text.agentSkillMissing("gone", "file not found")}`,
56
+ );
57
+ });
58
+ });