pi-roundtable 0.1.0 → 0.2.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 (238) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +6 -4
  3. package/docs/plugins.md +1448 -66
  4. package/examples/echo-runtime.test.ts +122 -0
  5. package/examples/echo-runtime.ts +107 -0
  6. package/examples/events.test.ts +1 -0
  7. package/examples/events.ts +2 -2
  8. package/examples/fake-surface.test.ts +180 -0
  9. package/examples/fake-surface.ts +109 -0
  10. package/examples/interactions.test.ts +12 -3
  11. package/examples/interactions.ts +19 -19
  12. package/examples/shared-services.test.ts +74 -0
  13. package/examples/shared-services.ts +60 -0
  14. package/examples/study-room.test.ts +69 -0
  15. package/examples/study-room.ts +52 -0
  16. package/examples/support-desk.test.ts +34 -0
  17. package/examples/support-desk.ts +40 -0
  18. package/package.json +9 -2
  19. package/src/cli/checks/database.ts +11 -30
  20. package/src/cli/checks/images.ts +19 -0
  21. package/src/cli/doctor.ts +6 -0
  22. package/src/cli/main.ts +2 -1
  23. package/src/core/agents/agent-claim.ts +42 -16
  24. package/src/core/agents/agent-messages.ts +4 -4
  25. package/src/core/agents/agent-ports.ts +8 -21
  26. package/src/core/agents/agent-prompt.ts +3 -1
  27. package/src/core/agents/agent-store.ts +6 -4
  28. package/src/core/agents/agent-team-fixture.ts +389 -0
  29. package/src/core/agents/agent-team.ts +26 -25
  30. package/src/core/agents/agent-tools.ts +34 -15
  31. package/src/core/agents/avatar-studio.ts +40 -6
  32. package/src/core/agents/fallback-avatar.ts +85 -0
  33. package/src/core/agents/team-editing.ts +49 -10
  34. package/src/core/agents/team-keys.ts +22 -9
  35. package/src/core/agents/team-layout.ts +4 -3
  36. package/src/core/agents/team-lifecycle.ts +28 -2
  37. package/src/core/agents/team-options.ts +9 -5
  38. package/src/core/agents/team-status.ts +4 -4
  39. package/src/core/agents/team-text.ts +8 -6
  40. package/src/core/agents/team-turn-types.ts +4 -4
  41. package/src/core/agents/team-turns.ts +6 -5
  42. package/src/core/builtin/agent-server.ts +187 -121
  43. package/src/core/builtin/discord-admin.ts +41 -0
  44. package/src/core/builtin/discord.ts +65 -40
  45. package/src/core/builtin/modules.ts +67 -58
  46. package/src/core/builtin/session-tool.ts +24 -0
  47. package/src/core/builtin/skills.ts +72 -0
  48. package/src/core/builtin/stores.ts +55 -31
  49. package/src/core/config/config.ts +45 -4
  50. package/src/core/config/schema.ts +6 -0
  51. package/src/core/contract/channels.ts +54 -14
  52. package/src/core/contract/providers.ts +17 -0
  53. package/src/core/contract/runtime.ts +138 -0
  54. package/src/core/contract/services.ts +45 -0
  55. package/src/core/contract/surface.ts +88 -0
  56. package/src/core/db/migrations.ts +118 -3
  57. package/src/core/define-roundtable.ts +70 -46
  58. package/src/core/define.ts +2 -1
  59. package/src/core/discord/agent-commands.ts +39 -14
  60. package/src/core/discord/agent-panel.ts +66 -0
  61. package/src/core/discord/channel-executor.ts +6 -3
  62. package/src/core/discord/channel-operations.ts +14 -11
  63. package/src/core/discord/command-collection.ts +46 -0
  64. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  65. package/src/core/discord/connection.ts +35 -0
  66. package/src/core/discord/discord-surface.ts +32 -94
  67. package/src/core/discord/dispatch-threads.ts +2 -2
  68. package/src/core/discord/inbound-message.ts +99 -0
  69. package/src/core/discord/interaction-module.ts +57 -1
  70. package/src/core/discord/owner-cards.ts +4 -4
  71. package/src/core/discord/owner-command.ts +44 -11
  72. package/src/core/discord/owner-discord.ts +2 -2
  73. package/src/core/discord/schedule-commands.ts +21 -18
  74. package/src/core/discord/stop-button.ts +44 -1
  75. package/src/core/domain/attachment.ts +12 -8
  76. package/src/core/domain/conversation.ts +3 -4
  77. package/src/core/domain/owner-prompts.ts +5 -5
  78. package/src/core/domain/ports.ts +9 -53
  79. package/src/core/freeze.ts +9 -0
  80. package/src/core/host.ts +255 -82
  81. package/src/core/http/listeners.ts +55 -20
  82. package/src/core/i18n/agent-panel.ts +4 -0
  83. package/src/core/i18n/index.ts +12 -2
  84. package/src/core/i18n/owner.ts +7 -1
  85. package/src/core/i18n/schedules.ts +12 -6
  86. package/src/core/identity.ts +1 -1
  87. package/src/core/judging/effort-judge.ts +25 -3
  88. package/src/core/log.ts +21 -3
  89. package/src/core/models.ts +2 -2
  90. package/src/core/modules/background/background-turns.ts +6 -4
  91. package/src/core/modules/delegation/delegate.ts +3 -2
  92. package/src/core/modules/delegation/delegator.ts +15 -13
  93. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  94. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  95. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  96. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  97. package/src/core/modules/memory/owner-memory.ts +9 -13
  98. package/src/core/modules/schedules/recurrence.ts +5 -3
  99. package/src/core/modules/schedules/schedule-store.ts +11 -11
  100. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  101. package/src/core/modules/schedules/scheduler.ts +7 -2
  102. package/src/core/modules/schedules/schedules.ts +9 -3
  103. package/src/core/modules/skills/skill-registry.ts +3 -2
  104. package/src/core/modules/skills/skill-store.ts +2 -15
  105. package/src/core/modules/skills/skill-tools.ts +10 -4
  106. package/src/core/ops/error-reporter.ts +1 -1
  107. package/src/core/plugin.ts +232 -28
  108. package/src/core/registry/contributions.ts +173 -14
  109. package/src/core/registry/providers.ts +35 -5
  110. package/src/core/registry/services.ts +229 -0
  111. package/src/core/routing/channel-queue.ts +5 -0
  112. package/src/core/routing/channel-router.ts +21 -7
  113. package/src/core/routing/conversation-turns.ts +139 -0
  114. package/src/core/routing/message-text.ts +9 -2
  115. package/src/core/routing/surface-port.ts +39 -0
  116. package/src/core/runtime/conversation-sessions.ts +4 -3
  117. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  118. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  119. package/src/core/runtime/mcp.ts +1 -1
  120. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  121. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  122. package/src/core/runtime/prompt-slot.ts +8 -3
  123. package/src/core/runtime/runtime-types.ts +9 -35
  124. package/src/core/runtime/session-factory.ts +21 -5
  125. package/src/core/runtime/text-tools.ts +1 -3
  126. package/src/core/services.ts +238 -90
  127. package/src/core/sessions.ts +3 -2
  128. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  129. package/src/core/shared/delegate-tool.ts +4 -3
  130. package/src/core/shared/schedule-tools.ts +22 -13
  131. package/src/core/shared/session-messages.ts +1 -1
  132. package/src/core/speakers.ts +6 -38
  133. package/src/core/testing/database.ts +1 -7
  134. package/src/core/testing/eager-catalog.ts +30 -0
  135. package/src/core/testing/locale.ts +23 -5
  136. package/src/core/testing/modules.ts +105 -41
  137. package/src/core/testing/test-host.ts +279 -0
  138. package/src/core/testing/tool-set.ts +64 -0
  139. package/src/core/time.ts +6 -1
  140. package/src/core/tool-set.snapshot.json +288 -0
  141. package/src/core/tool-tiers.ts +17 -57
  142. package/src/discord/index.ts +64 -0
  143. package/src/index.ts +181 -10
  144. package/src/kit/channels.ts +14 -0
  145. package/src/kit/domain.ts +18 -0
  146. package/src/kit/holds.ts +4 -0
  147. package/src/kit/index.ts +129 -0
  148. package/src/kit/judging.ts +10 -0
  149. package/src/kit/memory.ts +3 -0
  150. package/src/kit/mirror.ts +19 -0
  151. package/src/kit/presentation.ts +7 -0
  152. package/src/kit/shell.ts +7 -0
  153. package/src/kit/skills.ts +8 -0
  154. package/src/kit/support.ts +21 -0
  155. package/src/kit/threads.ts +7 -0
  156. package/src/kit/tools.ts +14 -0
  157. package/src/kit/worker.ts +16 -0
  158. package/src/testing.ts +394 -34
  159. package/examples/guide.test.ts +0 -52
  160. package/src/cli/add-plugin.test.ts +0 -107
  161. package/src/cli/checks/basic.test.ts +0 -276
  162. package/src/cli/checks/database.test.ts +0 -117
  163. package/src/cli/checks/discord.test.ts +0 -200
  164. package/src/cli/cli.test.ts +0 -122
  165. package/src/cli/config-edit.test.ts +0 -130
  166. package/src/cli/doctor.test.ts +0 -184
  167. package/src/cli/init.test.ts +0 -127
  168. package/src/cli/size.test.ts +0 -9
  169. package/src/cli/templates.test.ts +0 -129
  170. package/src/cli/testing/fixtures.ts +0 -106
  171. package/src/core/agents/agent-claim.test.ts +0 -142
  172. package/src/core/agents/agent-dashboard.test.ts +0 -120
  173. package/src/core/agents/agent-guild.test.ts +0 -124
  174. package/src/core/agents/agent-store.test.ts +0 -205
  175. package/src/core/agents/avatar-studio.test.ts +0 -88
  176. package/src/core/agents/group-round.test.ts +0 -143
  177. package/src/core/agents/owner-identity.test.ts +0 -172
  178. package/src/core/agents/team-turns.test.ts +0 -278
  179. package/src/core/attachments/attachments.test.ts +0 -85
  180. package/src/core/boundary.test.ts +0 -45
  181. package/src/core/builtin/modules.test.ts +0 -127
  182. package/src/core/config/config.test.ts +0 -135
  183. package/src/core/contract/discord.ts +0 -33
  184. package/src/core/db/migrations.test.ts +0 -237
  185. package/src/core/define-roundtable.test.ts +0 -134
  186. package/src/core/define.test.ts +0 -144
  187. package/src/core/discord/agent-commands.test.ts +0 -58
  188. package/src/core/discord/agent-discord.test.ts +0 -70
  189. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  190. package/src/core/discord/dispatch-threads.test.ts +0 -94
  191. package/src/core/discord/owner-cards.test.ts +0 -435
  192. package/src/core/discord/owner-discord.test.ts +0 -325
  193. package/src/core/domain/expression.ts +0 -21
  194. package/src/core/domain/profile.ts +0 -32
  195. package/src/core/drain.test.ts +0 -46
  196. package/src/core/events.test.ts +0 -81
  197. package/src/core/holds.test.ts +0 -54
  198. package/src/core/host.test.ts +0 -536
  199. package/src/core/http/listeners.test.ts +0 -164
  200. package/src/core/i18n/i18n.test.ts +0 -139
  201. package/src/core/identity.test.ts +0 -18
  202. package/src/core/judging/effort-judge.test.ts +0 -112
  203. package/src/core/judging/model-judge.test.ts +0 -126
  204. package/src/core/modules/delegation/delegator.test.ts +0 -170
  205. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  206. package/src/core/modules/schedules/schedule.test.ts +0 -358
  207. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  208. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  209. package/src/core/ops/error-reporter.test.ts +0 -283
  210. package/src/core/presentation/card-cadence.ts +0 -41
  211. package/src/core/presentation/presentation.test.ts +0 -166
  212. package/src/core/public-entry.test.ts +0 -59
  213. package/src/core/registry/contributions.test.ts +0 -215
  214. package/src/core/registry/interactions.test.ts +0 -87
  215. package/src/core/registry/providers.test.ts +0 -103
  216. package/src/core/routing/channel-queue.test.ts +0 -31
  217. package/src/core/routing/channel-router.test.ts +0 -326
  218. package/src/core/routing/conversation-kind.ts +0 -14
  219. package/src/core/routing/settle-turn.test.ts +0 -21
  220. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  221. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  222. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  223. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  224. package/src/core/runtime/session-archive.test.ts +0 -20
  225. package/src/core/runtime/steerable-run.test.ts +0 -264
  226. package/src/core/runtime/text-tools.test.ts +0 -110
  227. package/src/core/runtime/turn-answer.test.ts +0 -72
  228. package/src/core/runtime/worker-task.test.ts +0 -82
  229. package/src/core/services.test.ts +0 -28
  230. package/src/core/sessions.test.ts +0 -85
  231. package/src/core/shared/unix-server.ts +0 -18
  232. package/src/core/size.test.ts +0 -9
  233. package/src/core/speakers.test.ts +0 -78
  234. package/src/core/testing/file-size.ts +0 -34
  235. package/src/core/time.test.ts +0 -166
  236. package/src/core/tool-tiers.test.ts +0 -117
  237. package/src/entries.test.ts +0 -112
  238. package/src/testing.test.ts +0 -156
@@ -1,33 +0,0 @@
1
- import type {
2
- APIApplicationCommandSubcommandGroupOption,
3
- APIApplicationCommandSubcommandOption,
4
- Interaction,
5
- InteractionContextType,
6
- RESTPostAPIChatInputApplicationCommandsJSONBody,
7
- } from "discord.js";
8
-
9
- /** A group of slash commands and the components they post, handled together. */
10
- export interface InteractionModule {
11
- /** Top-level commands of its own; never the configured root command. */
12
- commands(): RESTPostAPIChatInputApplicationCommandsJSONBody[];
13
- /** Resolves true when this module handled the interaction. */
14
- handle(interaction: Interaction): Promise<boolean>;
15
- }
16
-
17
- /** A subcommand or subcommand group under the root command. */
18
- export type RootOption =
19
- | APIApplicationCommandSubcommandOption
20
- | APIApplicationCommandSubcommandGroupOption;
21
-
22
- /** A plugin's interactions: its module, and the subcommands it adds under the root command. */
23
- export interface InteractionContribution {
24
- module: InteractionModule;
25
- rootOptions?: readonly RootOption[];
26
- }
27
-
28
- /** The one root command every plugin's subcommands go under, such as `/roundtable`. */
29
- export interface CommandRoot {
30
- name: string;
31
- description: string;
32
- contexts: readonly InteractionContextType[];
33
- }
@@ -1,237 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { SQL } from "bun";
3
- import { MigrationError, PluginError } from "../errors.ts";
4
- import { Roundtable } from "../host.ts";
5
- import { silentLogger } from "../log.ts";
6
- import type { RoundtablePlugin } from "../plugin.ts";
7
- import { type Migration, migrate, openPool } from "./migrations.ts";
8
-
9
- // The core cannot import a consumer's test helpers, so it reads the test database itself.
10
- const url = process.env.ROUNDTABLE_TEST_DATABASE_URL;
11
- const describeDb = url ? describe : describe.skip;
12
-
13
- /** What the promise rejected with; undefined when it resolved. */
14
- async function rejection(
15
- promise: Promise<unknown> | undefined,
16
- ): Promise<unknown> {
17
- try {
18
- await promise;
19
- } catch (error) {
20
- return error;
21
- }
22
- return undefined;
23
- }
24
-
25
- function recording(name: string, log: string[], fails = false): Migration {
26
- return {
27
- name,
28
- up: async () => {
29
- log.push(name);
30
- if (fails) throw new Error("boom");
31
- },
32
- };
33
- }
34
-
35
- function roundtable(
36
- plugins: RoundtablePlugin[],
37
- database?: { url: string },
38
- ): { host: Roundtable; exits: number[] } {
39
- const exits: number[] = [];
40
- const host = new Roundtable(
41
- {
42
- logger: silentLogger(),
43
- drain: { intervalMs: 1, limitMs: 50 },
44
- exit: (code) => exits.push(code),
45
- ...(database ? { database } : {}),
46
- },
47
- plugins,
48
- );
49
- return { host, exits };
50
- }
51
-
52
- describe("migrate", () => {
53
- // A pool connects on its first query, so these never reach a server.
54
- const unreachable = () => openPool("postgres://nobody@127.0.0.1:1/none");
55
-
56
- test("runs the migrations in order", async () => {
57
- const log: string[] = [];
58
- const sql = unreachable();
59
- await migrate(sql, [recording("a", log), recording("b", log)]);
60
- await sql.close();
61
- expect(log).toEqual(["a", "b"]);
62
- });
63
-
64
- test("refuses a name declared twice before running any", async () => {
65
- const log: string[] = [];
66
- const sql = unreachable();
67
- const error = await rejection(
68
- migrate(sql, [recording("a", log), recording("a", log)]),
69
- );
70
- await sql.close();
71
- expect(error).toBeInstanceOf(PluginError);
72
- expect(log).toEqual([]);
73
- });
74
-
75
- test("stops at a failing migration and names it", async () => {
76
- const log: string[] = [];
77
- const sql = unreachable();
78
- const error = await rejection(
79
- migrate(sql, [recording("a", log, true), recording("b", log)]),
80
- );
81
- await sql.close();
82
- expect(error).toBeInstanceOf(MigrationError);
83
- expect(String(error)).toContain("migration a failed");
84
- expect(log).toEqual(["a"]);
85
- });
86
- });
87
-
88
- describe("Roundtable migrations", () => {
89
- test("refuses migrations without a configured database, before any setup", async () => {
90
- const log: string[] = [];
91
- const { host } = roundtable([
92
- {
93
- name: "stores",
94
- migrations: [recording("a", log)],
95
- setup: () => {
96
- log.push("setup");
97
- return {};
98
- },
99
- },
100
- ]);
101
- expect(await rejection(host.run())).toBeInstanceOf(PluginError);
102
- expect(log).toEqual([]);
103
- });
104
-
105
- test("without a database, reading it during setup is a plugin error", async () => {
106
- let error: unknown;
107
- const { host } = roundtable([
108
- {
109
- name: "reader",
110
- setup: ({ database }) => {
111
- try {
112
- database();
113
- } catch (e) {
114
- error = e;
115
- }
116
- return { events: {} };
117
- },
118
- },
119
- ]);
120
- await host.run();
121
- expect(error).toBeInstanceOf(PluginError);
122
- });
123
- });
124
-
125
- describeDb("PostgreSQL", () => {
126
- const table = "roundtable_migration_test";
127
- const tableMigration: Migration = {
128
- name: "test-table",
129
- up: async (sql) => {
130
- await sql.unsafe(
131
- `CREATE TABLE IF NOT EXISTS ${table} (id int PRIMARY KEY, note text NOT NULL)`,
132
- );
133
- await sql.unsafe(
134
- `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS added text NOT NULL DEFAULT 'x'`,
135
- );
136
- },
137
- };
138
-
139
- async function reset(): Promise<void> {
140
- const admin = openPool(url ?? "");
141
- await admin.unsafe(`DROP TABLE IF EXISTS ${table}`);
142
- await admin.close();
143
- }
144
-
145
- test("migrates every plugin in order before any setup, then closes the pool last", async () => {
146
- await reset();
147
- const log: string[] = [];
148
- let pool: SQL | undefined;
149
- const { host, exits } = roundtable(
150
- [
151
- {
152
- name: "first",
153
- migrations: [recording("first", log), tableMigration],
154
- setup: ({ database }) => {
155
- log.push("setup first");
156
- pool = database();
157
- return {
158
- services: [{ name: "uses", stop: () => void log.push("stop") }],
159
- };
160
- },
161
- },
162
- {
163
- name: "second",
164
- migrations: [recording("second", log)],
165
- setup: () => {
166
- log.push("setup second");
167
- return {};
168
- },
169
- },
170
- ],
171
- { url: url ?? "" },
172
- );
173
- await host.run();
174
- expect(log).toEqual(["first", "second", "setup first", "setup second"]);
175
- await pool?.unsafe(`INSERT INTO ${table} (id, note) VALUES (1, 'kept')`);
176
- await host.shutdown("SIGTERM");
177
- expect(log.at(-1)).toBe("stop");
178
- expect(exits).toEqual([0]);
179
- // The pool closed after the services stopped.
180
- expect(await rejection(pool?.unsafe("SELECT 1"))).toBeInstanceOf(Error);
181
- });
182
-
183
- test("a second boot over the existing schema keeps its rows", async () => {
184
- const read: { id: number; note: string; added: string }[][] = [];
185
- for (let boot = 0; boot < 2; boot++) {
186
- const { host } = roundtable(
187
- [
188
- {
189
- name: "stores",
190
- migrations: [tableMigration],
191
- setup: async ({ database }) => {
192
- read.push(
193
- await database().unsafe(
194
- `SELECT id, note, added FROM ${table} ORDER BY id`,
195
- ),
196
- );
197
- return {};
198
- },
199
- },
200
- ],
201
- { url: url ?? "" },
202
- );
203
- await host.run();
204
- await host.shutdown("SIGTERM");
205
- }
206
- expect(read).toEqual([
207
- [{ id: 1, note: "kept", added: "x" }],
208
- [{ id: 1, note: "kept", added: "x" }],
209
- ]);
210
- await reset();
211
- });
212
-
213
- test("a failing migration stops the boot before any setup", async () => {
214
- const log: string[] = [];
215
- const { host } = roundtable(
216
- [
217
- {
218
- name: "broken",
219
- migrations: [recording("broken", log, true)],
220
- setup: () => {
221
- log.push("setup");
222
- return {};
223
- },
224
- },
225
- ],
226
- { url: url ?? "" },
227
- );
228
- const error = await rejection(host.run());
229
- expect(error).toBeInstanceOf(PluginError);
230
- expect(String(error)).toContain("plugin broken: migration broken failed: ");
231
- expect(String(error)).toContain(
232
- "Fix the migration or restore the database, then start again.",
233
- );
234
- expect((error as Error).cause).toBeInstanceOf(MigrationError);
235
- expect(log).toEqual(["broken"]);
236
- });
237
- });
@@ -1,134 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { RoundtableConfig } from "./config/config.ts";
6
- import { defineRoundtable } from "./define-roundtable.ts";
7
- import { ConfigError } from "./domain/errors.ts";
8
- import { messages } from "./i18n/index.ts";
9
- import { createZhTW } from "./i18n/zh-tw.ts";
10
- import type { RoundtablePlugin } from "./plugin.ts";
11
- import { useTestLocale } from "./testing/locale.ts";
12
-
13
- const dataDir = mkdtempSync(join(tmpdir(), "roundtable-define-"));
14
-
15
- const config: RoundtableConfig = {
16
- owner: { id: "100000000000000001", name: "Ada" },
17
- discord: {
18
- token: "token",
19
- guild: "900000000000000001",
20
- entryChannel: "900000000000000002",
21
- },
22
- database: { url: "postgres://localhost/roundtable" },
23
- dataDir,
24
- model: "anthropic/claude-sonnet-5-5",
25
- http: { publicUrl: "https://bot.example.com", port: 3456 },
26
- };
27
-
28
- const mine: RoundtablePlugin = {
29
- name: "mine",
30
- setup: () => ({ dashboard: ["a line"] }),
31
- };
32
-
33
- afterEach(() => {
34
- // Other suites read the text through the catalog; leave it as the tests expect.
35
- useTestLocale();
36
- });
37
-
38
- describe("defineRoundtable", () => {
39
- test("returns the built-in plugins in their fixed order, the operator's, then the scheduler", async () => {
40
- const { plugins } = await defineRoundtable({ ...config, plugins: [mine] });
41
- expect(plugins.map((plugin) => plugin.name)).toEqual([
42
- "stores",
43
- "discord",
44
- "modules",
45
- "agent-server",
46
- "seeds",
47
- "mine",
48
- "schedules",
49
- ]);
50
- });
51
-
52
- test("migrates the core's tables first, in a fixed order", async () => {
53
- const { plugins } = await defineRoundtable(config);
54
- expect(
55
- plugins.flatMap((plugin) => (plugin.migrations ?? []).map((m) => m.name)),
56
- ).toEqual([
57
- "owner-memory",
58
- "owner-memory-speaker",
59
- "schedules",
60
- "pending-confirmations",
61
- "agents",
62
- "agents-guild",
63
- "skills",
64
- "skills-guild",
65
- "skills-written-kind",
66
- ]);
67
- });
68
-
69
- test("the host options carry the database, the root command, the tool tiers, and one listener", async () => {
70
- const { options } = await defineRoundtable({
71
- ...config,
72
- name: "Robin",
73
- toolTiers: { shell: "admin" },
74
- });
75
- expect(options.database).toEqual({ url: config.database.url });
76
- expect(options.commands?.root.name).toBe("robin");
77
- expect(options.listeners).toEqual([{ id: "public", port: 3456 }]);
78
- expect(options.toolTiers?.minTier("shell")).toBe("admin");
79
- });
80
-
81
- test("the locale and the assistant's name reach the text the bot shows", async () => {
82
- await defineRoundtable({ ...config, name: "Robin", locale: "zh-TW" });
83
- expect(messages().ownerRootDescription).toContain("Robin");
84
- expect(messages().stopLabel).toBe(
85
- createZhTW({ assistant: "Robin", root: "robin" }).stopLabel,
86
- );
87
- });
88
-
89
- test("a mistake in the configuration stops before anything is built, naming the key", async () => {
90
- await expect(
91
- defineRoundtable({
92
- ...config,
93
- discord: { ...config.discord, guilld: "1" },
94
- } as RoundtableConfig),
95
- ).rejects.toThrow(
96
- 'config discord.guilld: unknown key. Did you mean "guild"?',
97
- );
98
- await expect(
99
- defineRoundtable({ ...config, model: "sonnet" }),
100
- ).rejects.toBeInstanceOf(ConfigError);
101
- });
102
-
103
- test("reads the prompt files named in the configuration, and refuses a missing or empty one", async () => {
104
- const dir = join(dataDir, "prompts");
105
- mkdirSync(dir, { recursive: true });
106
- writeFileSync(join(dir, "shared.md"), "You are Robin.\n");
107
- writeFileSync(join(dir, "empty.md"), " \n");
108
- await defineRoundtable({
109
- ...config,
110
- prompts: { shared: join(dir, "shared.md") },
111
- });
112
- await expect(
113
- defineRoundtable({
114
- ...config,
115
- prompts: { shared: join(dir, "missing.md") },
116
- }),
117
- ).rejects.toThrow("config prompts.shared: cannot read");
118
- await expect(
119
- defineRoundtable({
120
- ...config,
121
- prompts: { shared: join(dir, "empty.md") },
122
- }),
123
- ).rejects.toThrow(
124
- `config prompts.shared: ${join(dir, "empty.md")} is empty`,
125
- );
126
- });
127
-
128
- test("the judge answers with a clear error until the model is available", async () => {
129
- const { options } = await defineRoundtable(config);
130
- await expect(options.judgeModel?.("system", "question")).rejects.toThrow(
131
- "the judge's model anthropic/claude-sonnet-5-5 is not available",
132
- );
133
- });
134
- });
@@ -1,144 +0,0 @@
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
- });
@@ -1,58 +0,0 @@
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
- });
@@ -1,70 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { ChannelType, type Client } from "discord.js";
3
- import { silentLogger } from "../log.ts";
4
- import { DiscordDashboard } from "./agent-discord.ts";
5
-
6
- const BOT = "900";
7
-
8
- /** A dashboard channel in a fake server whose one bot message may already be pinned. */
9
- function fixture(options: { pinned: boolean; pinFails?: boolean }) {
10
- const pins: string[] = [];
11
- const message = {
12
- id: "m1",
13
- pinned: options.pinned,
14
- author: { id: BOT },
15
- channel: { messages: { fetch: async () => message } },
16
- edit: async () => message,
17
- pin: async () => {
18
- if (options.pinFails) throw new Error("Missing Permissions");
19
- pins.push("m1");
20
- },
21
- };
22
- const channel = {
23
- type: ChannelType.GuildText,
24
- name: "dashboard",
25
- parentId: null,
26
- topic: null,
27
- setTopic: async () => undefined,
28
- messages: {
29
- fetch: async () => ({
30
- find: (match: (m: typeof message) => boolean) => [message].find(match),
31
- }),
32
- },
33
- };
34
- channel.topic = null;
35
- const client = {
36
- user: { id: BOT },
37
- guilds: {
38
- fetch: async () => ({
39
- channels: {
40
- fetch: async () => ({
41
- find: (match: (c: typeof channel) => boolean) =>
42
- [channel].find(match),
43
- }),
44
- },
45
- }),
46
- },
47
- } as unknown as Client;
48
- return { pins, dashboard: new DiscordDashboard(client, "1", silentLogger()) };
49
- }
50
-
51
- describe("DiscordDashboard pinning", () => {
52
- test("an existing unpinned message is pinned once, however often it is updated", async () => {
53
- const { dashboard, pins } = fixture({ pinned: false });
54
- await dashboard.show(["a"]);
55
- await dashboard.show(["b"]);
56
- expect(pins).toEqual(["m1"]);
57
- });
58
-
59
- test("an already pinned message is left alone", async () => {
60
- const { dashboard, pins } = fixture({ pinned: true });
61
- await dashboard.show(["a"]);
62
- expect(pins).toEqual([]);
63
- });
64
-
65
- test("a refused pin does not stop the update", async () => {
66
- const { dashboard } = fixture({ pinned: false, pinFails: true });
67
- await dashboard.show(["a"]);
68
- await dashboard.show(["b"]);
69
- });
70
- });