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,205 +0,0 @@
1
- import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
- import { SQL } from "bun";
3
- import { AgentError } from "../domain/errors.ts";
4
- import {
5
- describeDb,
6
- openTestStore,
7
- TEST_GUILD,
8
- type TestStore,
9
- testDatabaseUrl,
10
- } from "../testing/database.ts";
11
- import { AgentStore } from "./agent-store.ts";
12
-
13
- // Runs against a real PostgreSQL, only when ROUNDTABLE_TEST_DATABASE_URL is set.
14
- describeDb("PostgreSQL", () => {
15
- let store: TestStore<AgentStore>;
16
-
17
- const seed = (name: string, channelId?: string) => ({
18
- name,
19
- displayName: name.toUpperCase(),
20
- prompt: `You are ${name}.`,
21
- avatarPrompt: `A portrait of ${name}.`,
22
- ...(channelId ? { channelId } : {}),
23
- });
24
-
25
- beforeEach(async () => {
26
- await store?.close();
27
- const admin = new SQL(testDatabaseUrl);
28
- for (const table of [
29
- "agents",
30
- "agent_groups",
31
- "agent_group_messages",
32
- "agent_group_cursors",
33
- ])
34
- await admin.unsafe(`DROP TABLE IF EXISTS ${table}`);
35
- await admin.close();
36
- store = await openTestStore(AgentStore, TEST_GUILD);
37
- await store.seed([
38
- seed("coordinator", "100"),
39
- seed("infra"),
40
- seed("doctor"),
41
- ]);
42
- });
43
-
44
- afterAll(async () => {
45
- await store.close();
46
- });
47
-
48
- describe("agents", () => {
49
- test("seeding never overwrites an existing agent", async () => {
50
- await store.updateAgent("infra", { prompt: "Edited." });
51
- const added = await store.seed([seed("infra"), seed("new-one")]);
52
- expect(added).toEqual(["new-one"]);
53
- expect(store.agent("infra")?.prompt).toBe("Edited.");
54
- const reopened = await openTestStore(AgentStore, TEST_GUILD);
55
- expect(reopened.agent("infra")?.prompt).toBe("Edited.");
56
- await reopened.close();
57
- });
58
-
59
- test("finds an active agent by its channel", async () => {
60
- expect(store.agentByChannel("100")?.name).toBe("coordinator");
61
- await store.archiveAgent("coordinator");
62
- expect(store.agentByChannel("100")).toBeUndefined();
63
- });
64
-
65
- test("display names change; names are checked and never reused", async () => {
66
- const agent = await store.updateAgent("infra", {
67
- displayName: " Ops ",
68
- });
69
- expect(agent.displayName).toBe("Ops");
70
- expect(agent.name).toBe("infra");
71
- await expect(
72
- store.updateAgent("infra", { displayName: "discord bot" }),
73
- ).rejects.toBeInstanceOf(AgentError);
74
- expect(() => store.checkNewName("Bad Name")).toThrow(AgentError);
75
- await store.archiveAgent("doctor");
76
- expect(() => store.checkNewName("doctor")).toThrow(/taken/);
77
- expect(() => store.activeAgent("doctor")).toThrow(/archived/);
78
- });
79
-
80
- test("prompts are limited to 4,000 characters", async () => {
81
- await expect(
82
- store.updateAgent("infra", { prompt: "x".repeat(4001) }),
83
- ).rejects.toThrow(/4000/);
84
- });
85
- });
86
-
87
- describe("model settings", () => {
88
- test("model and thinking are kept, survive a reopen, and reset with null", async () => {
89
- await store.updateAgent("infra", {
90
- model: "openai-codex/gpt-6-sol",
91
- thinking: "high",
92
- });
93
- const reopened = await openTestStore(AgentStore, TEST_GUILD);
94
- expect(reopened.agent("infra")).toMatchObject({
95
- model: "openai-codex/gpt-6-sol",
96
- thinking: "high",
97
- });
98
- await reopened.close();
99
- await store.updateAgent("infra", { prompt: "Other." });
100
- expect(store.agent("infra")?.model).toBe("openai-codex/gpt-6-sol");
101
- const reset = await store.updateAgent("infra", {
102
- model: null,
103
- thinking: null,
104
- });
105
- expect(reset.model).toBeUndefined();
106
- expect(reset.thinking).toBeUndefined();
107
- });
108
-
109
- test("a malformed model or unknown thinking level is refused", async () => {
110
- await expect(
111
- store.updateAgent("infra", { model: "gpt-6-sol" }),
112
- ).rejects.toThrow(AgentError);
113
- await expect(
114
- // @ts-expect-error: an unknown level from outside the type system
115
- store.updateAgent("infra", { thinking: "extreme" }),
116
- ).rejects.toThrow(/thinking level/);
117
- });
118
- });
119
-
120
- describe("groups", () => {
121
- const makeGroup = () =>
122
- store.createGroup({
123
- name: "ops",
124
- displayName: "Ops",
125
- channelId: "200",
126
- members: ["coordinator", "infra", "doctor"],
127
- host: "coordinator",
128
- });
129
-
130
- test("a group needs 2 to 6 distinct active members and a member host", async () => {
131
- expect(() => store.checkNewGroup("g", ["infra"], "infra")).toThrow(
132
- AgentError,
133
- );
134
- expect(() =>
135
- store.checkNewGroup("g", ["infra", "doctor"], "coordinator"),
136
- ).toThrow(/host/);
137
- expect(() =>
138
- store.checkNewGroup("infra", ["infra", "doctor"], "infra"),
139
- ).toThrow(/taken/);
140
- const group = await makeGroup();
141
- expect(store.groupByChannel("200")?.members).toEqual(group.members);
142
- });
143
-
144
- test("an archived agent leaves its groups, which hand over hosting or archive", async () => {
145
- await makeGroup();
146
- await store.archiveAgent("coordinator");
147
- const group = store.group("ops");
148
- expect(group?.members).toEqual(["infra", "doctor"]);
149
- expect(group?.host).toBe("infra");
150
- await store.archiveAgent("doctor");
151
- expect(store.group("ops")?.status).toBe("archived");
152
- });
153
-
154
- test("each message reaches each member once, its own excluded", async () => {
155
- await makeGroup();
156
- const owner = await store.appendGroupMessage("ops", {
157
- author: "owner",
158
- authorName: "Riley",
159
- text: "hi",
160
- });
161
- await store.appendGroupMessage("ops", {
162
- author: "infra",
163
- authorName: "INFRA",
164
- text: "disk ok",
165
- });
166
- const first = await store.backlog("ops", "doctor", 40);
167
- expect(first.messages.map((m) => m.text)).toEqual(["hi", "disk ok"]);
168
- await store.advanceCursor("ops", "doctor", first.lastId ?? 0);
169
- expect((await store.backlog("ops", "doctor", 40)).messages).toEqual([]);
170
-
171
- const infra = await store.backlog("ops", "infra", 40);
172
- expect(infra.messages.map((m) => m.id)).toEqual([owner]);
173
- await store.advanceCursor("ops", "infra", infra.lastId ?? 0);
174
-
175
- // A cursor never moves back, so a message is never delivered twice.
176
- await store.advanceCursor("ops", "infra", owner - 1);
177
- expect((await store.backlog("ops", "infra", 40)).messages).toEqual([]);
178
- });
179
-
180
- test("a long backlog carries the newest messages and counts the rest", async () => {
181
- await makeGroup();
182
- for (let i = 0; i < 5; i++)
183
- await store.appendGroupMessage("ops", {
184
- author: "owner",
185
- authorName: "Riley",
186
- text: `m${i}`,
187
- });
188
- const backlog = await store.backlog("ops", "doctor", 2);
189
- expect(backlog.messages.map((m) => m.text)).toEqual(["m3", "m4"]);
190
- expect(backlog.skipped).toBe(3);
191
- });
192
-
193
- test("catching up marks everything as received", async () => {
194
- const group = await makeGroup();
195
- await store.appendGroupMessage("ops", {
196
- author: "owner",
197
- authorName: "Riley",
198
- text: "old",
199
- });
200
- await store.catchUp(group);
201
- for (const member of group.members)
202
- expect((await store.backlog("ops", member, 40)).messages).toEqual([]);
203
- });
204
- });
205
- });
@@ -1,88 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdtempSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { createCanvas, loadImage } from "canvas";
6
- import { AvatarStudio, type ImageDrawer } from "./avatar-studio.ts";
7
-
8
- const REFERENCE = join(import.meta.dir, "..", "assets", "neutral.png");
9
-
10
- function picture(width: number, height: number, color: string): Uint8Array {
11
- const canvas = createCanvas(width, height);
12
- const ctx = canvas.getContext("2d");
13
- ctx.fillStyle = color;
14
- ctx.fillRect(0, 0, width, height);
15
- return canvas.toBuffer("image/png");
16
- }
17
-
18
- async function studio(draw: ImageDrawer) {
19
- const s = new AvatarStudio({
20
- dir: mkdtempSync(join(tmpdir(), "roundtable-avatars-")),
21
- publicUrl: "https://example.com",
22
- referencePath: REFERENCE,
23
- draw,
24
- });
25
- await s.init();
26
- return s;
27
- }
28
-
29
- describe("AvatarStudio", () => {
30
- test("an agent without a picture gets the assistant's neutral avatar", async () => {
31
- const s = await studio(async () => picture(10, 10, "red"));
32
- const url = s.url(undefined);
33
- expect(url).toMatch(/^https:\/\/example\.com\/avatars\/[0-9a-f]{64}\.png$/);
34
- const served = s.serve(new URL(url).pathname);
35
- expect(served?.status).toBe(200);
36
- expect(served?.headers.get("Content-Type")).toBe("image/png");
37
- });
38
-
39
- test("draws with the reference and stores a square of at most 512 px", async () => {
40
- const prompts: string[] = [];
41
- const s = await studio(async (prompt, refs) => {
42
- prompts.push(prompt);
43
- expect(refs).toHaveLength(1);
44
- return picture(1024, 768, "blue");
45
- });
46
- const hash = await s.draw("a friendly engineer");
47
- expect(prompts[0]).toContain("a friendly engineer");
48
- expect(prompts[0]).toContain("anime");
49
- const served = s.serve(`/avatars/${hash}.png`);
50
- if (!served) throw new Error("not served");
51
- const image = await loadImage(Buffer.from(await served.arrayBuffer()));
52
- expect([image.width, image.height]).toEqual([512, 512]);
53
- });
54
-
55
- test("an edit starts from the current picture and yields a new hash", async () => {
56
- let references = 0;
57
- const s = await studio(async (_prompt, refs) => {
58
- references += refs.length;
59
- return picture(64, 64, references > 1 ? "green" : "blue");
60
- });
61
- const first = await s.draw("x");
62
- const second = await s.edit(first, "add glasses");
63
- expect(second).not.toBe(first);
64
- });
65
-
66
- test("serves nothing but avatar pictures", async () => {
67
- const s = await studio(async () => picture(4, 4, "red"));
68
- expect(s.serve("/mcp/personal")).toBeUndefined();
69
- expect(s.serve("/avatars/../secrets.env")).toBeUndefined();
70
- expect(s.serve(`/avatars/${"0".repeat(64)}.png`)?.status).toBe(404);
71
- });
72
-
73
- test("its public route answers GET for pictures and 404 for any other avatar path", async () => {
74
- const s = await studio(async () => picture(4, 4, "red"));
75
- const hash = await s.draw("x");
76
- const route = s.route("public");
77
- expect(route).toMatchObject({
78
- listener: "public",
79
- path: { prefix: "/avatars/" },
80
- methods: ["GET", "HEAD"],
81
- });
82
- const get = (path: string) =>
83
- route.handle(new Request(`https://example.com${path}`));
84
- expect((await get(`/avatars/${hash}.png`)).status).toBe(200);
85
- const other = await get("/avatars/../secrets.env");
86
- expect([other.status, await other.text()]).toEqual([404, "Not found"]);
87
- });
88
- });
@@ -1,143 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import pino from "pino";
3
- import type { YesNoQuestion } from "../domain/ports.ts";
4
- import { TEST_OWNER as OWNER } from "../testing/owner.ts";
5
- import type { Agent } from "./agent-store.ts";
6
- import { mentionedMembers, planRound, RelevanceScorer } from "./group-round.ts";
7
-
8
- const agent = (name: string, displayName: string): Agent => ({
9
- name,
10
- displayName,
11
- prompt: `You are ${displayName}.`,
12
- avatarPrompt: "",
13
- status: "active",
14
- });
15
- const members = [
16
- agent("coordinator", "Координатор"),
17
- agent("infra", "Infra"),
18
- agent("doctor", "Bot Доктор"),
19
- ];
20
- const logger = pino({ level: "silent" });
21
-
22
- describe("mentionedMembers", () => {
23
- test("matches display names and names, ignoring case", () => {
24
- expect(mentionedMembers("@Bot Доктор what do you think", members)).toEqual([
25
- "doctor",
26
- ]);
27
- expect(
28
- mentionedMembers("@infra and @Координатор take a look", members),
29
- ).toEqual(["coordinator", "infra"]);
30
- });
31
-
32
- test("does not match a longer word", () => {
33
- expect(mentionedMembers("@infrastructure", members)).toEqual([]);
34
- expect(mentionedMembers("infra hello", members)).toEqual([]);
35
- });
36
- });
37
-
38
- describe("planRound", () => {
39
- const names = members.map((m) => m.name);
40
-
41
- test("members above the threshold answer, most relevant last", () => {
42
- expect(
43
- planRound({
44
- members: names,
45
- host: "coordinator",
46
- scores: { coordinator: 0.6, infra: 0.95, doctor: 0.2 },
47
- forced: new Set(),
48
- }),
49
- ).toEqual(["coordinator", "infra"]);
50
- });
51
-
52
- test("a forced member answers whatever its score, in score order", () => {
53
- expect(
54
- planRound({
55
- members: names,
56
- host: "coordinator",
57
- scores: { coordinator: 0.1, infra: 0.9, doctor: 0.05 },
58
- forced: new Set(["doctor"]),
59
- }),
60
- ).toEqual(["doctor", "infra"]);
61
- });
62
-
63
- test("ties keep group order", () => {
64
- expect(
65
- planRound({
66
- members: names,
67
- host: "coordinator",
68
- scores: { coordinator: 0.7, infra: 0.7, doctor: 0.7 },
69
- forced: new Set(),
70
- }),
71
- ).toEqual(names);
72
- });
73
-
74
- test("with nobody qualifying the host answers alone", () => {
75
- expect(
76
- planRound({
77
- members: names,
78
- host: "infra",
79
- scores: { coordinator: 0.1, infra: 0.2, doctor: 0.3 },
80
- forced: new Set(),
81
- }),
82
- ).toEqual(["infra"]);
83
- });
84
-
85
- test("without scores only forced members answer, or the host", () => {
86
- expect(
87
- planRound({
88
- members: names,
89
- host: "coordinator",
90
- scores: undefined,
91
- forced: new Set(["doctor"]),
92
- }),
93
- ).toEqual(["doctor"]);
94
- expect(
95
- planRound({
96
- members: names,
97
- host: "coordinator",
98
- scores: undefined,
99
- forced: new Set(),
100
- }),
101
- ).toEqual(["coordinator"]);
102
- });
103
- });
104
-
105
- describe("RelevanceScorer", () => {
106
- test("asks one question per member in one request and maps the answers back", async () => {
107
- const asked: Record<string, YesNoQuestion>[] = [];
108
- const scorer = new RelevanceScorer(
109
- {
110
- askYesNo: async (_state, questions) => {
111
- asked.push(questions);
112
- return { m_coordinator: 0.3, m_infra: 0.9, m_doctor: 0.1 };
113
- },
114
- },
115
- logger,
116
- OWNER,
117
- );
118
- expect(await scorer.score(members, [], "the disk is almost full")).toEqual({
119
- coordinator: 0.3,
120
- infra: 0.9,
121
- doctor: 0.1,
122
- });
123
- expect(asked).toHaveLength(1);
124
- expect(Object.keys(asked[0] ?? {})).toEqual([
125
- "m_coordinator",
126
- "m_infra",
127
- "m_doctor",
128
- ]);
129
- });
130
-
131
- test("a failed request yields no scores", async () => {
132
- const scorer = new RelevanceScorer(
133
- {
134
- askYesNo: async () => {
135
- throw new Error("down");
136
- },
137
- },
138
- logger,
139
- OWNER,
140
- );
141
- expect(await scorer.score(members, [], "hi")).toBeUndefined();
142
- });
143
- });
@@ -1,172 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import type { InboundMessage } from "../domain/conversation.ts";
4
- import type { OwnerIdentity } from "../identity.ts";
5
- import { withReference } from "../routing/message-text.ts";
6
- import {
7
- answerText,
8
- askUserExtension,
9
- } from "../runtime/extensions/ask-user.ts";
10
- import {
11
- ConfirmationGate,
12
- confirmedTurnText,
13
- } from "../runtime/extensions/confirmation-gate.ts";
14
- import { PromptSlot } from "../runtime/prompt-slot.ts";
15
- import { addressee, type Speaker, THE_SPEAKER } from "../speakers.ts";
16
- import {
17
- agentFailureText,
18
- agentMessageText,
19
- agentReplyText,
20
- agentSystemPrompt,
21
- chainLimitReason,
22
- openingTaskText,
23
- } from "./agent-prompt.ts";
24
- import type { Agent } from "./agent-store.ts";
25
-
26
- const ALICE: OwnerIdentity = {
27
- name: "Alice",
28
- pronouns: { subject: "she", object: "her", possessive: "her" },
29
- };
30
-
31
- const agent: Agent = {
32
- name: "scout",
33
- displayName: "Scout",
34
- prompt: "Scout things.",
35
- avatarPrompt: "",
36
- channelId: "5",
37
- status: "active",
38
- };
39
- const coordinator: Agent = {
40
- ...agent,
41
- name: "coordinator",
42
- displayName: "Coordinator",
43
- channelId: "1",
44
- };
45
-
46
- /** Every text these modules show a model, built for an owner who is not the test default. */
47
- function texts(): string[] {
48
- const gate = new ConfirmationGate(() => "delete the task", ALICE);
49
- return [
50
- agentSystemPrompt({
51
- agent,
52
- coordinator,
53
- shared: "",
54
- workDir: "/w",
55
- owner: ALICE,
56
- shellUser: "alice-bot",
57
- }),
58
- agentSystemPrompt({
59
- agent: coordinator,
60
- coordinator,
61
- shared: "",
62
- workDir: "/w",
63
- owner: ALICE,
64
- shellUser: "alice-bot",
65
- }),
66
- agentMessageText(agent, "x", ALICE),
67
- agentReplyText(ALICE, agent, "x"),
68
- agentFailureText(ALICE, agent, "r"),
69
- openingTaskText(undefined, "t", ALICE),
70
- chainLimitReason(ALICE),
71
- answerText(undefined, ALICE),
72
- answerText({ choices: ["A"], text: "b" }, ALICE),
73
- gate.hold("any", {}) ?? "",
74
- confirmedTurnText(
75
- { profile: "general", heldAt: new Date(0), calls: [] },
76
- "ok",
77
- ALICE,
78
- ),
79
- withReference(
80
- {
81
- text: "look",
82
- forwarded: { channelMention: "<#2>", url: "https://x", text: "f" },
83
- } as InboundMessage,
84
- ALICE,
85
- ),
86
- ];
87
- }
88
-
89
- test("prompts name the configured owner and shell user, never the default's", () => {
90
- const all = texts().join("\n");
91
- expect(all).not.toContain("Riley");
92
- expect(all).not.toContain("mcops");
93
- expect(all).not.toMatch(/\b(he|him|his|He|His)\b/);
94
- expect(all).toContain("Alice");
95
- expect(all).toContain("alice-bot");
96
- });
97
-
98
- const BOB: Speaker = { id: "100000000000000007", name: "Bob", tier: "member" };
99
- const promptFor = (speaker?: Speaker) =>
100
- agentSystemPrompt({
101
- agent,
102
- coordinator,
103
- shared: "",
104
- workDir: "/w",
105
- owner: ALICE,
106
- shellUser: "alice-bot",
107
- ...(speaker ? { speaker } : {}),
108
- });
109
-
110
- test("an owner-tier speaker gets the owner's prompts unchanged", () => {
111
- const owner: Speaker = { id: "1", name: "Alice", tier: "owner" };
112
- expect(promptFor(owner)).toBe(promptFor());
113
- expect(openingTaskText(undefined, "t", addressee(owner, ALICE))).toBe(
114
- openingTaskText(undefined, "t", ALICE),
115
- );
116
- });
117
-
118
- test("a member's turn addresses the member and says whose memory and approvals apply", () => {
119
- const prompt = promptFor(BOB);
120
- expect(prompt).toContain("only Bob approves held actions");
121
- expect(prompt).toContain("held for Bob's confirmation");
122
- expect(prompt).toContain(
123
- "You are talking with Bob (Discord user 100000000000000007), who speaks at the member tier",
124
- );
125
- expect(prompt).toContain("Your memory is Bob's own, not Alice's");
126
- expect(prompt).not.toContain("only Alice approves");
127
- const bob = addressee(BOB, ALICE);
128
- expect(chainLimitReason(bob)).toContain("without Bob in between");
129
- expect(agentMessageText(agent, "x", bob)).toContain("not from Bob");
130
- expect(gateHold(bob)).toContain("Held for Bob's confirmation");
131
- expect(answerText(undefined, bob)).toContain("Bob did not answer");
132
- });
133
-
134
- function gateHold(who: OwnerIdentity): string {
135
- const gate = new ConfirmationGate(() => "delete the task", ALICE);
136
- gate.beginTurn("general", false, who);
137
- return gate.hold("any", {}) ?? "";
138
- }
139
-
140
- test("a tool description in a shared session names the speaker, not the owner", () => {
141
- let description = "";
142
- askUserExtension(
143
- new PromptSlot(),
144
- THE_SPEAKER,
145
- )({
146
- registerTool: (tool: { description: string }) => {
147
- description = tool.description;
148
- },
149
- } as unknown as ExtensionAPI);
150
- expect(description).toContain("Ask the speaker a question");
151
- expect(description).toContain("wait for the speaker's answer");
152
- expect(description).toContain("The speaker has 30 minutes");
153
- expect(description).not.toContain("Alice");
154
- });
155
-
156
- test("a speaker who is not the owner gets the guest's shared prompt, the owner the owner's", () => {
157
- const prompt = (speaker?: Speaker) =>
158
- agentSystemPrompt({
159
- agent,
160
- coordinator,
161
- shared: "For the owner.",
162
- guestShared: "For guests.",
163
- workDir: "/w",
164
- owner: ALICE,
165
- shellUser: "alice-bot",
166
- ...(speaker ? { speaker } : {}),
167
- });
168
- expect(prompt().startsWith("For the owner.")).toBe(true);
169
- expect(prompt({ id: "1", name: "Alice", tier: "owner" })).toBe(prompt());
170
- expect(prompt(BOB).startsWith("For guests.")).toBe(true);
171
- expect(prompt(BOB)).not.toContain("For the owner.");
172
- });