pi-roundtable 0.1.0 → 0.2.1

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 (239) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +10 -6
  3. package/README.zh-TW.md +144 -0
  4. package/docs/plugins.md +1448 -66
  5. package/examples/echo-runtime.test.ts +122 -0
  6. package/examples/echo-runtime.ts +107 -0
  7. package/examples/events.test.ts +1 -0
  8. package/examples/events.ts +2 -2
  9. package/examples/fake-surface.test.ts +180 -0
  10. package/examples/fake-surface.ts +109 -0
  11. package/examples/interactions.test.ts +12 -3
  12. package/examples/interactions.ts +19 -19
  13. package/examples/shared-services.test.ts +74 -0
  14. package/examples/shared-services.ts +60 -0
  15. package/examples/study-room.test.ts +69 -0
  16. package/examples/study-room.ts +52 -0
  17. package/examples/support-desk.test.ts +34 -0
  18. package/examples/support-desk.ts +40 -0
  19. package/package.json +11 -3
  20. package/src/cli/checks/database.ts +11 -30
  21. package/src/cli/checks/images.ts +19 -0
  22. package/src/cli/doctor.ts +6 -0
  23. package/src/cli/main.ts +2 -1
  24. package/src/core/agents/agent-claim.ts +42 -16
  25. package/src/core/agents/agent-messages.ts +4 -4
  26. package/src/core/agents/agent-ports.ts +8 -21
  27. package/src/core/agents/agent-prompt.ts +3 -1
  28. package/src/core/agents/agent-store.ts +6 -4
  29. package/src/core/agents/agent-team-fixture.ts +389 -0
  30. package/src/core/agents/agent-team.ts +26 -25
  31. package/src/core/agents/agent-tools.ts +34 -15
  32. package/src/core/agents/avatar-studio.ts +40 -6
  33. package/src/core/agents/fallback-avatar.ts +85 -0
  34. package/src/core/agents/team-editing.ts +49 -10
  35. package/src/core/agents/team-keys.ts +22 -9
  36. package/src/core/agents/team-layout.ts +4 -3
  37. package/src/core/agents/team-lifecycle.ts +28 -2
  38. package/src/core/agents/team-options.ts +9 -5
  39. package/src/core/agents/team-status.ts +4 -4
  40. package/src/core/agents/team-text.ts +8 -6
  41. package/src/core/agents/team-turn-types.ts +4 -4
  42. package/src/core/agents/team-turns.ts +6 -5
  43. package/src/core/builtin/agent-server.ts +187 -121
  44. package/src/core/builtin/discord-admin.ts +41 -0
  45. package/src/core/builtin/discord.ts +65 -40
  46. package/src/core/builtin/modules.ts +67 -58
  47. package/src/core/builtin/session-tool.ts +24 -0
  48. package/src/core/builtin/skills.ts +72 -0
  49. package/src/core/builtin/stores.ts +55 -31
  50. package/src/core/config/config.ts +45 -4
  51. package/src/core/config/schema.ts +6 -0
  52. package/src/core/contract/channels.ts +54 -14
  53. package/src/core/contract/providers.ts +17 -0
  54. package/src/core/contract/runtime.ts +138 -0
  55. package/src/core/contract/services.ts +45 -0
  56. package/src/core/contract/surface.ts +88 -0
  57. package/src/core/db/migrations.ts +118 -3
  58. package/src/core/define-roundtable.ts +70 -46
  59. package/src/core/define.ts +2 -1
  60. package/src/core/discord/agent-commands.ts +39 -14
  61. package/src/core/discord/agent-panel.ts +66 -0
  62. package/src/core/discord/channel-executor.ts +6 -3
  63. package/src/core/discord/channel-operations.ts +14 -11
  64. package/src/core/discord/command-collection.ts +46 -0
  65. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  66. package/src/core/discord/connection.ts +35 -0
  67. package/src/core/discord/discord-surface.ts +32 -94
  68. package/src/core/discord/dispatch-threads.ts +2 -2
  69. package/src/core/discord/inbound-message.ts +99 -0
  70. package/src/core/discord/interaction-module.ts +57 -1
  71. package/src/core/discord/owner-cards.ts +4 -4
  72. package/src/core/discord/owner-command.ts +44 -11
  73. package/src/core/discord/owner-discord.ts +2 -2
  74. package/src/core/discord/schedule-commands.ts +21 -18
  75. package/src/core/discord/stop-button.ts +44 -1
  76. package/src/core/domain/attachment.ts +12 -8
  77. package/src/core/domain/conversation.ts +3 -4
  78. package/src/core/domain/owner-prompts.ts +5 -5
  79. package/src/core/domain/ports.ts +9 -53
  80. package/src/core/freeze.ts +9 -0
  81. package/src/core/host.ts +255 -82
  82. package/src/core/http/listeners.ts +55 -20
  83. package/src/core/i18n/agent-panel.ts +4 -0
  84. package/src/core/i18n/index.ts +12 -2
  85. package/src/core/i18n/owner.ts +7 -1
  86. package/src/core/i18n/schedules.ts +12 -6
  87. package/src/core/identity.ts +1 -1
  88. package/src/core/judging/effort-judge.ts +25 -3
  89. package/src/core/log.ts +21 -3
  90. package/src/core/models.ts +2 -2
  91. package/src/core/modules/background/background-turns.ts +6 -4
  92. package/src/core/modules/delegation/delegate.ts +3 -2
  93. package/src/core/modules/delegation/delegator.ts +15 -13
  94. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  95. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  96. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  97. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  98. package/src/core/modules/memory/owner-memory.ts +9 -13
  99. package/src/core/modules/schedules/recurrence.ts +5 -3
  100. package/src/core/modules/schedules/schedule-store.ts +11 -11
  101. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  102. package/src/core/modules/schedules/scheduler.ts +7 -2
  103. package/src/core/modules/schedules/schedules.ts +9 -3
  104. package/src/core/modules/skills/skill-registry.ts +3 -2
  105. package/src/core/modules/skills/skill-store.ts +2 -15
  106. package/src/core/modules/skills/skill-tools.ts +10 -4
  107. package/src/core/ops/error-reporter.ts +1 -1
  108. package/src/core/plugin.ts +232 -28
  109. package/src/core/registry/contributions.ts +173 -14
  110. package/src/core/registry/providers.ts +35 -5
  111. package/src/core/registry/services.ts +229 -0
  112. package/src/core/routing/channel-queue.ts +5 -0
  113. package/src/core/routing/channel-router.ts +21 -7
  114. package/src/core/routing/conversation-turns.ts +139 -0
  115. package/src/core/routing/message-text.ts +9 -2
  116. package/src/core/routing/surface-port.ts +39 -0
  117. package/src/core/runtime/conversation-sessions.ts +4 -3
  118. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  119. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  120. package/src/core/runtime/mcp.ts +1 -1
  121. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  122. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  123. package/src/core/runtime/prompt-slot.ts +8 -3
  124. package/src/core/runtime/runtime-types.ts +9 -35
  125. package/src/core/runtime/session-factory.ts +21 -5
  126. package/src/core/runtime/text-tools.ts +1 -3
  127. package/src/core/services.ts +238 -90
  128. package/src/core/sessions.ts +3 -2
  129. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  130. package/src/core/shared/delegate-tool.ts +4 -3
  131. package/src/core/shared/schedule-tools.ts +22 -13
  132. package/src/core/shared/session-messages.ts +1 -1
  133. package/src/core/speakers.ts +6 -38
  134. package/src/core/testing/database.ts +1 -7
  135. package/src/core/testing/eager-catalog.ts +30 -0
  136. package/src/core/testing/locale.ts +23 -5
  137. package/src/core/testing/modules.ts +105 -41
  138. package/src/core/testing/test-host.ts +279 -0
  139. package/src/core/testing/tool-set.ts +64 -0
  140. package/src/core/time.ts +6 -1
  141. package/src/core/tool-set.snapshot.json +288 -0
  142. package/src/core/tool-tiers.ts +17 -57
  143. package/src/discord/index.ts +64 -0
  144. package/src/index.ts +181 -10
  145. package/src/kit/channels.ts +14 -0
  146. package/src/kit/domain.ts +18 -0
  147. package/src/kit/holds.ts +4 -0
  148. package/src/kit/index.ts +129 -0
  149. package/src/kit/judging.ts +10 -0
  150. package/src/kit/memory.ts +3 -0
  151. package/src/kit/mirror.ts +19 -0
  152. package/src/kit/presentation.ts +7 -0
  153. package/src/kit/shell.ts +7 -0
  154. package/src/kit/skills.ts +8 -0
  155. package/src/kit/support.ts +21 -0
  156. package/src/kit/threads.ts +7 -0
  157. package/src/kit/tools.ts +14 -0
  158. package/src/kit/worker.ts +16 -0
  159. package/src/testing.ts +394 -34
  160. package/examples/guide.test.ts +0 -52
  161. package/src/cli/add-plugin.test.ts +0 -107
  162. package/src/cli/checks/basic.test.ts +0 -276
  163. package/src/cli/checks/database.test.ts +0 -117
  164. package/src/cli/checks/discord.test.ts +0 -200
  165. package/src/cli/cli.test.ts +0 -122
  166. package/src/cli/config-edit.test.ts +0 -130
  167. package/src/cli/doctor.test.ts +0 -184
  168. package/src/cli/init.test.ts +0 -127
  169. package/src/cli/size.test.ts +0 -9
  170. package/src/cli/templates.test.ts +0 -129
  171. package/src/cli/testing/fixtures.ts +0 -106
  172. package/src/core/agents/agent-claim.test.ts +0 -142
  173. package/src/core/agents/agent-dashboard.test.ts +0 -120
  174. package/src/core/agents/agent-guild.test.ts +0 -124
  175. package/src/core/agents/agent-store.test.ts +0 -205
  176. package/src/core/agents/avatar-studio.test.ts +0 -88
  177. package/src/core/agents/group-round.test.ts +0 -143
  178. package/src/core/agents/owner-identity.test.ts +0 -172
  179. package/src/core/agents/team-turns.test.ts +0 -278
  180. package/src/core/attachments/attachments.test.ts +0 -85
  181. package/src/core/boundary.test.ts +0 -45
  182. package/src/core/builtin/modules.test.ts +0 -127
  183. package/src/core/config/config.test.ts +0 -135
  184. package/src/core/contract/discord.ts +0 -33
  185. package/src/core/db/migrations.test.ts +0 -237
  186. package/src/core/define-roundtable.test.ts +0 -134
  187. package/src/core/define.test.ts +0 -144
  188. package/src/core/discord/agent-commands.test.ts +0 -58
  189. package/src/core/discord/agent-discord.test.ts +0 -70
  190. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  191. package/src/core/discord/dispatch-threads.test.ts +0 -94
  192. package/src/core/discord/owner-cards.test.ts +0 -435
  193. package/src/core/discord/owner-discord.test.ts +0 -325
  194. package/src/core/domain/expression.ts +0 -21
  195. package/src/core/domain/profile.ts +0 -32
  196. package/src/core/drain.test.ts +0 -46
  197. package/src/core/events.test.ts +0 -81
  198. package/src/core/holds.test.ts +0 -54
  199. package/src/core/host.test.ts +0 -536
  200. package/src/core/http/listeners.test.ts +0 -164
  201. package/src/core/i18n/i18n.test.ts +0 -139
  202. package/src/core/identity.test.ts +0 -18
  203. package/src/core/judging/effort-judge.test.ts +0 -112
  204. package/src/core/judging/model-judge.test.ts +0 -126
  205. package/src/core/modules/delegation/delegator.test.ts +0 -170
  206. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  207. package/src/core/modules/schedules/schedule.test.ts +0 -358
  208. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  209. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  210. package/src/core/ops/error-reporter.test.ts +0 -283
  211. package/src/core/presentation/card-cadence.ts +0 -41
  212. package/src/core/presentation/presentation.test.ts +0 -166
  213. package/src/core/public-entry.test.ts +0 -59
  214. package/src/core/registry/contributions.test.ts +0 -215
  215. package/src/core/registry/interactions.test.ts +0 -87
  216. package/src/core/registry/providers.test.ts +0 -103
  217. package/src/core/routing/channel-queue.test.ts +0 -31
  218. package/src/core/routing/channel-router.test.ts +0 -326
  219. package/src/core/routing/conversation-kind.ts +0 -14
  220. package/src/core/routing/settle-turn.test.ts +0 -21
  221. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  222. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  223. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  224. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  225. package/src/core/runtime/session-archive.test.ts +0 -20
  226. package/src/core/runtime/steerable-run.test.ts +0 -264
  227. package/src/core/runtime/text-tools.test.ts +0 -110
  228. package/src/core/runtime/turn-answer.test.ts +0 -72
  229. package/src/core/runtime/worker-task.test.ts +0 -82
  230. package/src/core/services.test.ts +0 -28
  231. package/src/core/sessions.test.ts +0 -85
  232. package/src/core/shared/unix-server.ts +0 -18
  233. package/src/core/size.test.ts +0 -9
  234. package/src/core/speakers.test.ts +0 -78
  235. package/src/core/testing/file-size.ts +0 -34
  236. package/src/core/time.test.ts +0 -166
  237. package/src/core/tool-tiers.test.ts +0 -117
  238. package/src/entries.test.ts +0 -112
  239. package/src/testing.test.ts +0 -156
@@ -1,166 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { CardCadence, CONVERSATION_GAP_MS } from "./card-cadence.ts";
3
- import { headline } from "./headline.ts";
4
- import { quietLinks } from "./quiet-links.ts";
5
- import { DISCORD_MESSAGE_LIMIT, splitReply } from "./reply-splitter.ts";
6
- import { THINKING_LIMIT, thinkingLine } from "./thinking-line.ts";
7
-
8
- describe("headline", () => {
9
- test("takes the first sentence", () => {
10
- expect(headline("Today is a nice day。Let us go for a walk!")).toBe(
11
- "Today is a nice day。",
12
- );
13
- expect(headline("Bun 1.4.2 is out. It fixes a lot.")).toBe(
14
- "Bun 1.4.2 is out.",
15
- );
16
- });
17
-
18
- test("does not stop at a decimal point", () => {
19
- expect(headline("Version 1.4.2 ships today")).toBe(
20
- "Version 1.4.2 ships today",
21
- );
22
- });
23
-
24
- test("strips markdown and skips code", () => {
25
- expect(
26
- headline(
27
- "```ts\nconst x = 1;\n```\n## **Key point**:see [this post](https://x.y)",
28
- ),
29
- ).toBe("Key point:see this post");
30
- expect(headline("- `bun` is fast")).toBe("bun is fast");
31
- });
32
-
33
- test("truncates to 80 characters", () => {
34
- const result = headline("é".repeat(120));
35
- expect(Array.from(result)).toHaveLength(80);
36
- expect(result.endsWith("…")).toBe(true);
37
- });
38
-
39
- test("emoji are left out, since the card font cannot draw them", () => {
40
- expect(headline("Schedule test OK,get some water 💧")).toBe(
41
- "Schedule test OK,get some water",
42
- );
43
- expect(headline("🎉 Congrats 👍🏽 done 🇹🇼!")).toBe("Congrats done !");
44
- expect(headline("👨‍👩‍👧 family❤️ all good")).toBe("family all good");
45
- expect(headline("💧")).toBe("");
46
- });
47
-
48
- test("empty text gives an empty headline", () => {
49
- expect(headline("\n\n")).toBe("");
50
- });
51
- });
52
-
53
- describe("splitReply", () => {
54
- test("short text stays one message", () => {
55
- expect(splitReply("hello\n\nworld")).toEqual(["hello\n\nworld"]);
56
- });
57
-
58
- test("splits on paragraph boundaries within the limit", () => {
59
- const [a, b, c] = ["a".repeat(900), "b".repeat(900), "c".repeat(900)];
60
- expect(splitReply(`${a}\n\n${b}\n\n${c}`)).toEqual([`${a}\n\n${b}`, c]);
61
- });
62
-
63
- test("keeps a code block with blank lines intact", () => {
64
- const code = "```ts\nconst a = 1;\n\nconst b = 2;\n```";
65
- const text = `${"x".repeat(1980)}\n\n${code}`;
66
- expect(splitReply(text)).toEqual(["x".repeat(1980), code]);
67
- });
68
-
69
- test("a long code block becomes several complete code blocks", () => {
70
- const body = Array.from(
71
- { length: 200 },
72
- (_, i) => `line ${i} ${"-".repeat(20)}`,
73
- ).join("\n");
74
- const chunks = splitReply(`\`\`\`ts\n${body}\n\`\`\``);
75
- expect(chunks.length).toBeGreaterThan(1);
76
- for (const chunk of chunks) {
77
- expect(chunk.length).toBeLessThanOrEqual(2000);
78
- expect(chunk.startsWith("```ts\n")).toBe(true);
79
- expect(chunk.endsWith("\n```")).toBe(true);
80
- }
81
- const rejoined = chunks.map((c) => c.slice(6, -4)).join("\n");
82
- expect(rejoined).toBe(body);
83
- });
84
-
85
- test("one overlong line is split by characters", () => {
86
- const chunks = splitReply("é".repeat(4500));
87
- expect(chunks.map((c) => c.length)).toEqual([2000, 2000, 500]);
88
- });
89
-
90
- test("every chunk respects the limit", () => {
91
- const text = Array.from(
92
- { length: 50 },
93
- (_, i) => `Paragraph ${i} ${"é".repeat(i * 10)}`,
94
- ).join("\n\n");
95
- const chunks = splitReply(text);
96
- expect(chunks.every((c) => c.length <= 2000)).toBe(true);
97
- expect(chunks.join("\n\n")).toBe(text);
98
- });
99
- });
100
-
101
- describe("quietLinks", () => {
102
- test("wraps cited links and keeps sentence punctuation outside", () => {
103
- expect(
104
- quietLinks("Source:https://example.com/a?b=1。Also https://x.org/p."),
105
- ).toBe("Source:<https://example.com/a?b=1>。Also <https://x.org/p>.");
106
- });
107
-
108
- test("GIF and video links keep their preview", () => {
109
- const text =
110
- "https://giphy.com/gifs/clap\nhttps://www.youtube.com/watch?v=abc";
111
- expect(quietLinks(text)).toBe(text);
112
- });
113
-
114
- test("markdown links and already wrapped links", () => {
115
- expect(
116
- quietLinks("[review](https://example.com/r) and <https://a.io>"),
117
- ).toBe("[review](<https://example.com/r>) and <https://a.io>");
118
- });
119
- });
120
-
121
- describe("thinkingLine", () => {
122
- test("each line becomes small grey text and blank lines are dropped", () => {
123
- expect(thinkingLine(" check the date\n\nthen answer ")).toBe(
124
- "-# check the date\n-# then answer",
125
- );
126
- expect(thinkingLine(" ")).toBeUndefined();
127
- });
128
-
129
- test("long thinking is cut and stays within one message", () => {
130
- const line = thinkingLine("é".repeat(THINKING_LIMIT + 50));
131
- expect(line?.endsWith("…")).toBe(true);
132
- expect(line?.length).toBeLessThanOrEqual(DISCORD_MESSAGE_LIMIT);
133
- });
134
- });
135
-
136
- describe("card cadence", () => {
137
- const at = (minutes: number) => minutes * 60_000;
138
-
139
- test("a conversation's first reply shows a card; the same tone does not repeat it", () => {
140
- const cadence = new CardCadence();
141
- expect(cadence.shows("discord:1", "neutral", false, at(0))).toBe(true);
142
- expect(cadence.shows("discord:1", "neutral", false, at(1))).toBe(false);
143
- expect(cadence.shows("discord:1", "happy", false, at(2))).toBe(true);
144
- expect(cadence.shows("discord:1", "happy", false, at(3))).toBe(false);
145
- expect(cadence.shows("discord:1", "neutral", false, at(4))).toBe(false);
146
- expect(cadence.shows("discord:1", "thinking", false, at(5))).toBe(true);
147
- });
148
-
149
- test("a quiet channel opens a new conversation", () => {
150
- const cadence = new CardCadence();
151
- cadence.shows("discord:1", "neutral", false, 0);
152
- expect(
153
- cadence.shows("discord:1", "neutral", false, CONVERSATION_GAP_MS - 1),
154
- ).toBe(false);
155
- expect(
156
- cadence.shows("discord:1", "neutral", false, 2 * CONVERSATION_GAP_MS - 1),
157
- ).toBe(true);
158
- });
159
-
160
- test("a failure always shows its card; channels are independent", () => {
161
- const cadence = new CardCadence();
162
- cadence.shows("discord:1", "apologetic", true, at(0));
163
- expect(cadence.shows("discord:1", "apologetic", true, at(1))).toBe(true);
164
- expect(cadence.shows("discord:2", "neutral", false, at(1))).toBe(true);
165
- });
166
- });
@@ -1,59 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { readdirSync, readFileSync } from "node:fs";
3
- import { join, relative, resolve } from "node:path";
4
-
5
- const ROOT = resolve(import.meta.dir, "../..");
6
- const CORE = join(ROOT, "src/core");
7
-
8
- /** The core's modules, tests and test helpers aside. */
9
- function modules(dir: string): string[] {
10
- return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
11
- const path = join(dir, entry.name);
12
- if (entry.isDirectory())
13
- return entry.name === "testing" ? [] : modules(path);
14
- return entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")
15
- ? [path]
16
- : [];
17
- });
18
- }
19
-
20
- /** The packages a module imports, by their bare names. */
21
- function packages(file: string): string[] {
22
- const source = readFileSync(file, "utf8");
23
- return [
24
- ...source.matchAll(/(?:from|import)\s*\(?\s*"([^."][^"]*)"/g),
25
- ].flatMap((match) => (match[1] ? [match[1]] : []));
26
- }
27
-
28
- /** Services only the first consumer's deployment runs; the core must work without them. */
29
- const PRIVATE = new RegExp(
30
- // Spelled in pieces so this test does not itself name the private services.
31
- ["j" + "ev", "phoe" + "nix", "context" + "forge", "type" + "safe"].join("|"),
32
- "i",
33
- );
34
-
35
- describe("the public core", () => {
36
- test("imports no package of a private service", () => {
37
- const found = modules(CORE).flatMap((file) =>
38
- packages(file)
39
- .filter((name) => PRIVATE.test(name))
40
- .map((name) => `${relative(ROOT, file)} -> ${name}`),
41
- );
42
- expect(found).toEqual([]);
43
- });
44
-
45
- test("every module loads with no consumer configuration", () => {
46
- const imports = modules(CORE)
47
- .map((file) => `await import(${JSON.stringify(file)});`)
48
- .join("\n");
49
- const run = Bun.spawnSync({
50
- cmd: [process.execPath, "-e", imports],
51
- cwd: ROOT,
52
- // Only what a process needs to start; none of a consumer's variables.
53
- env: { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "" },
54
- stderr: "pipe",
55
- });
56
- expect(run.stderr.toString()).toBe("");
57
- expect(run.exitCode).toBe(0);
58
- });
59
- });
@@ -1,215 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { Type } from "typebox";
3
- import { defineTool } from "../define.ts";
4
- import { NotLinkedError, PluginError } from "../errors.ts";
5
- import { silentLogger } from "../log.ts";
6
- import type {
7
- Contribution,
8
- PluginContext,
9
- RoundtablePlugin,
10
- } from "../plugin.ts";
11
- import { toolTiers } from "../tool-tiers.ts";
12
- import {
13
- collectContributions,
14
- linkSessions,
15
- nearest,
16
- } from "./contributions.ts";
17
-
18
- /** A context the plugins under test never read. */
19
- const context = { logger: silentLogger() } as unknown as PluginContext;
20
-
21
- const plugin = (
22
- name: string,
23
- contribution: Contribution | (() => Contribution),
24
- ): RoundtablePlugin => ({
25
- name,
26
- setup: () =>
27
- typeof contribution === "function" ? contribution() : contribution,
28
- });
29
-
30
- const tool = (name: string, minTier: "member" | "admin" | "owner" = "member") =>
31
- defineTool({
32
- name,
33
- description: "A tool.",
34
- parameters: Type.Object({}),
35
- minTier,
36
- run: () => "ok",
37
- });
38
-
39
- const collect = (plugins: RoundtablePlugin[], tiers = toolTiers()) =>
40
- collectContributions(plugins, context, tiers);
41
-
42
- describe("collectContributions", () => {
43
- test("names the part a typo meant, or lists the parts", async () => {
44
- await expect(
45
- collect([plugin("p", { serivces: [] } as unknown as Contribution)]),
46
- ).rejects.toThrow(
47
- 'plugin p: setup returned an unknown part "serivces". Did you mean "services"? The parts are services,',
48
- );
49
- await expect(
50
- collect([plugin("p", { zzzzzz: [] } as unknown as Contribution)]),
51
- ).rejects.toThrow(
52
- 'plugin p: setup returned an unknown part "zzzzzz". The parts are services,',
53
- );
54
- });
55
-
56
- test("refuses a plugin that adds nothing, and says what to give it", async () => {
57
- await expect(collect([plugin("empty", {})])).rejects.toThrow(
58
- "plugin empty adds nothing. Give it a part (tools, services, channels, and so on), a migration, or a provider, or remove it.",
59
- );
60
- });
61
-
62
- test("a setup that throws or returns no object is named and keeps its cause", async () => {
63
- const boom = new Error("boom");
64
- const failing = collect([
65
- plugin("p", () => {
66
- throw boom;
67
- }),
68
- ]);
69
- await expect(failing).rejects.toThrow(
70
- "plugin p: setup failed: boom. Fix the error, or remove the plugin.",
71
- );
72
- await expect(failing).rejects.toHaveProperty("cause", boom);
73
- await expect(
74
- collect([
75
- plugin("early", () => {
76
- throw new NotLinkedError("parts are linked later.");
77
- }),
78
- ]),
79
- ).rejects.toThrow(
80
- "plugin early: setup failed: parts are linked later. Fix the error, or remove the plugin.",
81
- );
82
- await expect(
83
- collect([
84
- { name: "q", setup: () => undefined as unknown as Contribution },
85
- ]),
86
- ).rejects.toThrow(
87
- "plugin q: setup must return an object of the parts it adds; return {} to add none.",
88
- );
89
- });
90
-
91
- test("a service, a session tool, or a hold rule two plugins register names both plugins", async () => {
92
- const service = { name: "sweeper" };
93
- await expect(
94
- collect([
95
- plugin("a", { services: [service] }),
96
- plugin("b", { services: [service] }),
97
- ]),
98
- ).rejects.toThrow(
99
- "plugin b: service sweeper is already registered by plugin a. Rename one of the two.",
100
- );
101
- const rule = { name: "shell", describe: () => undefined };
102
- await expect(
103
- collect([
104
- plugin("a", { holdRules: [rule] }),
105
- plugin("b", { holdRules: [rule] }),
106
- ]),
107
- ).rejects.toThrow(
108
- "plugin b: hold rule shell is already registered by plugin a. Rename one of the two.",
109
- );
110
- });
111
-
112
- test("a tool two plugins define names both plugins", async () => {
113
- await expect(
114
- collect([
115
- plugin("a", { tools: [tool("note_add")] }),
116
- plugin("b", { tools: [tool("note_add")] }),
117
- ]),
118
- ).rejects.toThrow(
119
- "plugin b: tool note_add is already defined by plugin a. Rename one of the two tools.",
120
- );
121
- });
122
-
123
- test("a plugin's tools carry their tier, their hold rule, and their place among the agent's tools", async () => {
124
- const tiers = toolTiers({ note_list: "admin" });
125
- const held = defineTool({
126
- name: "note_clear",
127
- description: "Clear the notes.",
128
- parameters: Type.Object({}),
129
- minTier: "admin",
130
- agent: false,
131
- hold: () => "clear every note",
132
- run: () => "cleared",
133
- });
134
- const registry = await collect(
135
- [
136
- plugin("notes", {
137
- tools: [tool("note_add"), tool("note_list", "member"), held],
138
- }),
139
- ],
140
- tiers,
141
- );
142
- expect(tiers.minTier("note_add")).toBe("member");
143
- // The operator's setting wins over the plugin's.
144
- expect(tiers.minTier("note_list")).toBe("admin");
145
- expect(tiers.minTier("note_clear")).toBe("admin");
146
- const linked = linkSessions(registry);
147
- expect(linked.agentTools).toEqual(["note_add", "note_list"]);
148
- expect(linked.holds("note_clear", {}, {})).toBe("clear every note");
149
- expect(linked.holds("note_add", {}, {})).toBeUndefined();
150
- });
151
-
152
- test("seeds and prompt sections are collected in contribution order", async () => {
153
- const seed = (name: string) => ({
154
- name,
155
- displayName: name,
156
- prompt: "p",
157
- avatarPrompt: "a",
158
- });
159
- const section = (name: string) => ({ name, build: () => name });
160
- const registry = await collect([
161
- plugin("a", { seeds: [seed("one")], prompt: [section("x")] }),
162
- plugin("b", { seeds: [seed("two")], prompt: [section("y")] }),
163
- ]);
164
- const linked = linkSessions(registry);
165
- expect(linked.seeds.map((s) => s.name)).toEqual(["one", "two"]);
166
- expect(linked.prompt.map((s) => s.name)).toEqual(["x", "y"]);
167
- });
168
-
169
- test("every plugin's agent selection is merged when read, each name once", async () => {
170
- let groups = ["mail"];
171
- const registry = await collect([
172
- plugin("a", {
173
- agentSelection: () => ({ tools: ["web_search"], groups }),
174
- }),
175
- plugin("b", {
176
- agentSelection: () => ({
177
- tools: ["web_search", "notes"],
178
- groups: ["mail", "calendar"],
179
- }),
180
- }),
181
- ]);
182
- const linked = linkSessions(registry);
183
- expect(linked.agentSelection()).toEqual({
184
- tools: ["web_search", "notes"],
185
- groups: ["mail", "calendar"],
186
- });
187
- // Read again at each turn, so a set that changes while the process runs stays current.
188
- groups = ["docs"];
189
- expect(linked.agentSelection().groups).toEqual([
190
- "docs",
191
- "mail",
192
- "calendar",
193
- ]);
194
- expect(
195
- linkSessions(
196
- await collect([plugin("c", { dashboard: ["a line"] })]),
197
- ).agentSelection(),
198
- ).toEqual({ tools: [], groups: [] });
199
- });
200
-
201
- test("every refusal is a PluginError", async () => {
202
- await expect(collect([plugin("empty", {})])).rejects.toBeInstanceOf(
203
- PluginError,
204
- );
205
- });
206
- });
207
-
208
- describe("nearest", () => {
209
- test("finds a close word and nothing for a far one", () => {
210
- const known = ["services", "events", "interactions"];
211
- expect(nearest("serivces", known)).toBe("services");
212
- expect(nearest("event", known)).toBe("events");
213
- expect(nearest("banana", known)).toBeUndefined();
214
- });
215
- });
@@ -1,87 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { InteractionContextType } from "discord.js";
3
- import type { InteractionModule, RootOption } from "../contract/discord.ts";
4
- import { PluginError } from "../errors.ts";
5
- import { composeInteractions } from "./interactions.ts";
6
-
7
- const ROOT = {
8
- name: "roundtable",
9
- description: "control",
10
- contexts: [InteractionContextType.Guild, InteractionContextType.BotDM],
11
- };
12
-
13
- function module(...names: string[]): InteractionModule {
14
- return {
15
- commands: () =>
16
- names.map((name) => ({ name, description: name, type: 1 as const })),
17
- handle: async () => false,
18
- };
19
- }
20
-
21
- const sub = (name: string): RootOption => ({
22
- type: 1,
23
- name,
24
- description: name,
25
- });
26
-
27
- describe("composeInteractions", () => {
28
- test("puts every contribution's subcommands under one root, after the modules' own commands", () => {
29
- const a = module("roll");
30
- const b = module();
31
- const composed = composeInteractions(ROOT, [
32
- { module: b, rootOptions: [sub("help"), sub("status")] },
33
- { module: a },
34
- { module: module(), rootOptions: [sub("party")] },
35
- ]);
36
- expect(composed.commands.map((c) => c.name)).toEqual([
37
- "roll",
38
- "roundtable",
39
- ]);
40
- const root = composed.commands[1];
41
- expect(root?.options?.map((o) => o.name)).toEqual([
42
- "help",
43
- "status",
44
- "party",
45
- ]);
46
- expect(root?.contexts).toEqual(ROOT.contexts);
47
- expect(composed.modules[0]).toBe(b);
48
- expect(composed.modules[1]).toBe(a);
49
- });
50
-
51
- test("registers no root without subcommands", () => {
52
- const composed = composeInteractions(ROOT, [{ module: module("dice") }]);
53
- expect(composed.commands.map((c) => c.name)).toEqual(["dice"]);
54
- });
55
-
56
- test("refuses a subcommand added twice, a command registered twice, and a module registering the root", () => {
57
- const refused = (build: () => unknown) => {
58
- try {
59
- build();
60
- return undefined;
61
- } catch (error) {
62
- return error;
63
- }
64
- };
65
- expect(
66
- refused(() =>
67
- composeInteractions(ROOT, [
68
- { module: module(), rootOptions: [sub("mcp")] },
69
- { module: module(), rootOptions: [sub("mcp")] },
70
- ]),
71
- ),
72
- ).toBeInstanceOf(PluginError);
73
- expect(
74
- refused(() =>
75
- composeInteractions(ROOT, [
76
- { module: module("roll") },
77
- { module: module("roll") },
78
- ]),
79
- ),
80
- ).toBeInstanceOf(PluginError);
81
- expect(
82
- refused(() =>
83
- composeInteractions(ROOT, [{ module: module("roundtable") }]),
84
- ),
85
- ).toBeInstanceOf(PluginError);
86
- });
87
- });
@@ -1,103 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Judge, Providers } from "../contract/providers.ts";
3
- import { PluginError, ProviderError } from "../errors.ts";
4
- import { Roundtable } from "../host.ts";
5
- import { ModelJudge } from "../judging/model-judge.ts";
6
- import { silentLogger } from "../log.ts";
7
- import type { RoundtablePlugin } from "../plugin.ts";
8
- import { resolveProviders } from "./providers.ts";
9
-
10
- /** What the promise rejected with; undefined when it resolved. */
11
- async function rejection(promise: Promise<unknown>): Promise<unknown> {
12
- try {
13
- await promise;
14
- } catch (error) {
15
- return error;
16
- }
17
- return undefined;
18
- }
19
-
20
- const fixedJudge: Judge = {
21
- askYesNo: async () => ({ approves: 1 }),
22
- askChoice: async () => ({ choice: "a", confidence: 1 }),
23
- askScore: async () => [1],
24
- };
25
-
26
- const providing = (
27
- name: string,
28
- providers: Partial<Providers>,
29
- ): RoundtablePlugin => ({ name, providers, setup: () => ({}) });
30
-
31
- describe("resolveProviders", () => {
32
- test("without plugins, the judge asks the given model and images refuse", async () => {
33
- const resolved = resolveProviders([], async () => '{"approves": 0.7}');
34
- expect(resolved.judge).toBeInstanceOf(ModelJudge);
35
- expect(
36
- await resolved.judge.askYesNo(
37
- {},
38
- {
39
- approves: { type: "yesno", instructions: "Approve?" },
40
- },
41
- ),
42
- ).toEqual({ approves: 0.7 });
43
- expect(await rejection(resolved.images("a cat", []))).toBeInstanceOf(
44
- ProviderError,
45
- );
46
- });
47
-
48
- test("without a judge model, every judgment refuses so callers keep their defaults", async () => {
49
- const { judge } = resolveProviders([]);
50
- expect(
51
- await rejection(
52
- judge.askScore({}, "effort", {
53
- type: "score",
54
- instructions: "",
55
- criteria: [],
56
- }),
57
- ),
58
- ).toBeInstanceOf(ProviderError);
59
- });
60
-
61
- test("a plugin's provider replaces the default for its slot only", () => {
62
- const images = async () => new Uint8Array([1]);
63
- const resolved = resolveProviders([
64
- providing("acme-judge", { judge: fixedJudge }),
65
- providing("codex", { images }),
66
- ]);
67
- expect(resolved.judge).toBe(fixedJudge);
68
- expect(resolved.images).toBe(images);
69
- });
70
-
71
- test("two plugins filling one slot clash", () => {
72
- expect(() =>
73
- resolveProviders([
74
- providing("acme-judge", { judge: fixedJudge }),
75
- providing("other", { judge: fixedJudge }),
76
- ]),
77
- ).toThrow(PluginError);
78
- });
79
- });
80
-
81
- describe("Roundtable providers", () => {
82
- test("a provider-only plugin is valid, and setups read the resolved providers", async () => {
83
- let seen: Judge | undefined;
84
- const roundtable = new Roundtable(
85
- {
86
- logger: silentLogger(),
87
- exit: () => {},
88
- },
89
- [
90
- providing("acme-judge", { judge: fixedJudge }),
91
- {
92
- name: "reader",
93
- setup: ({ providers }) => {
94
- seen = providers.judge;
95
- return { events: {} };
96
- },
97
- },
98
- ],
99
- );
100
- await roundtable.run();
101
- expect(seen).toBe(fixedJudge);
102
- });
103
- });
@@ -1,31 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { ChannelQueue } from "./channel-queue.ts";
3
-
4
- describe("ChannelQueue", () => {
5
- test("reports every change of a channel's count", async () => {
6
- const queue = new ChannelQueue();
7
- const seen: [string, number][] = [];
8
- queue.onChange((channel) => seen.push([channel, queue.size(channel)]));
9
- let release = () => {};
10
- const first = queue.run(
11
- "discord:1",
12
- () => new Promise<void>((resolve) => (release = resolve)),
13
- );
14
- const second = queue.run("discord:1", async () => {});
15
- expect(seen).toEqual([
16
- ["discord:1", 1],
17
- ["discord:1", 2],
18
- ]);
19
- // The first task starts on a later tick.
20
- await Bun.sleep(0);
21
- release();
22
- await Promise.all([first, second]);
23
- await Bun.sleep(0);
24
- expect(seen).toEqual([
25
- ["discord:1", 1],
26
- ["discord:1", 2],
27
- ["discord:1", 1],
28
- ["discord:1", 0],
29
- ]);
30
- });
31
- });