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,139 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { Glob } from "bun";
5
- import { useTestLocale } from "../testing/locale.ts";
6
- import { createEn } from "./en.ts";
7
- import { LOCALES, type Messages, messages, setLocale } from "./index.ts";
8
- import { createZhTW } from "./zh-tw.ts";
9
-
10
- const CTX = { assistant: "Roundtable", root: "roundtable" };
11
- const CJK = /[\u3400-\u9fff\u3000-\u303f\uff00-\uffef]/;
12
-
13
- type Entry = string | ((...args: never[]) => unknown);
14
-
15
- /** Calls a message function with plain arguments of one shape; the first shape it accepts wins. */
16
- function render(entry: (...values: unknown[]) => unknown): string {
17
- for (const arg of ["x", ["x"], 1, { x: "x" }]) {
18
- try {
19
- return JSON.stringify(
20
- entry(...Array.from({ length: entry.length }, () => arg)),
21
- );
22
- } catch {
23
- // Try the next shape.
24
- }
25
- }
26
- throw new Error(`no plain arguments fit ${entry.toString().slice(0, 60)}`);
27
- }
28
-
29
- /** Every entry of a catalog as text: a string as it is, a function called with plain arguments. */
30
- function rendered(catalog: Messages): [string, string][] {
31
- return Object.entries(catalog as Record<string, unknown>).map(
32
- ([key, entry]) => [
33
- key,
34
- typeof entry === "string"
35
- ? entry
36
- : typeof entry === "function"
37
- ? render(entry as (...values: unknown[]) => unknown)
38
- : JSON.stringify(entry),
39
- ],
40
- );
41
- }
42
-
43
- const kind = (entry: unknown): string =>
44
- typeof entry === "function" ? `function/${entry.length}` : typeof entry;
45
-
46
- afterEach(() => {
47
- useTestLocale();
48
- });
49
-
50
- describe("catalogs", () => {
51
- test("the English and Traditional Chinese catalogs have the same keys, and functions the same arity", () => {
52
- const en = createEn(CTX) as Record<string, Entry>;
53
- const zh = createZhTW(CTX) as Record<string, Entry>;
54
- expect(Object.keys(zh).sort()).toEqual(Object.keys(en).sort());
55
- const shapes = (catalog: Record<string, Entry>) =>
56
- Object.fromEntries(
57
- Object.entries(catalog).map(([key, entry]) => [key, kind(entry)]),
58
- );
59
- expect(shapes(zh)).toEqual(shapes(en));
60
- });
61
-
62
- test("the English catalog has no Chinese text or fullwidth punctuation", () => {
63
- const offenders = rendered(createEn(CTX))
64
- .filter(([, text]) => CJK.test(text))
65
- .map(([key]) => key);
66
- expect(offenders).toEqual([]);
67
- });
68
-
69
- test("the Traditional Chinese catalog is not the English one", () => {
70
- const en = new Map(rendered(createEn(CTX)));
71
- const same = rendered(createZhTW(CTX)).filter(
72
- ([key, text]) => en.get(key) === text,
73
- );
74
- // A few entries are the same in both languages, such as a bare command name.
75
- expect(same.length).toBeLessThan(en.size / 5);
76
- });
77
- });
78
-
79
- describe("setLocale", () => {
80
- test("names the assistant and the root command in the text", () => {
81
- setLocale("en", { assistant: "Robin", root: "robin" });
82
- const text = JSON.stringify(rendered(messages()));
83
- expect(text).toContain("Robin");
84
- expect(text).toContain("/robin");
85
- expect(text).not.toContain("Roundtable");
86
- });
87
-
88
- test("shows English by default-style settings and Chinese for zh-TW", () => {
89
- setLocale("en", CTX);
90
- expect(messages().stopLabel).toBe("Stop");
91
- setLocale("zh-TW", CTX);
92
- expect(messages().stopLabel).toBe("停止");
93
- });
94
-
95
- test("an override replaces one message and leaves the others", () => {
96
- setLocale("en", { ...CTX, overrides: { stopLabel: "Halt" } });
97
- expect(messages().stopLabel).toBe("Halt");
98
- expect(messages().ownerFailedTitle).toBe("Could not complete");
99
- });
100
-
101
- test("an unknown locale is refused", () => {
102
- expect(() => setLocale("fr" as never, CTX)).toThrow("Unknown locale: fr");
103
- expect(LOCALES).toEqual(["en", "zh-TW"]);
104
- });
105
- });
106
-
107
- describe("what the core shows", () => {
108
- const files = (root: string, pattern: string) =>
109
- [...new Glob(pattern).scanSync({ cwd: root })].map((f) => join(root, f));
110
-
111
- test("no Chinese text outside the catalogs and the tests", () => {
112
- const offenders = files("src/core", "**/*.ts")
113
- .filter((f) => !f.includes("/i18n/") && !/\.test\.ts$/.test(f))
114
- .flatMap((f) =>
115
- readFileSync(f, "utf8")
116
- .split("\n")
117
- .map((line, index) => ({ f, line, index }))
118
- // Regexes may match fullwidth punctuation; only ideographs are text.
119
- .filter(({ line }) => /[\u3400-\u9fff]/.test(line))
120
- .map(({ f: file, index }) => `${file}:${index + 1}`),
121
- );
122
- expect(offenders).toEqual([]);
123
- });
124
-
125
- test("no source in the core names its first consumer", () => {
126
- // Spelled in pieces so this test does not itself name the consumer.
127
- const consumer = new RegExp(["mer", "lin"].join(""), "i");
128
- const offenders = files("src/core", "**/*.ts")
129
- .filter((f) => !/\.test\.ts$/.test(f) && !f.includes("/testing/"))
130
- .flatMap((f) =>
131
- readFileSync(f, "utf8")
132
- .split("\n")
133
- .map((line, index) => ({ f, line, index }))
134
- .filter(({ line }) => consumer.test(line))
135
- .map(({ f: file, index }) => `${file}:${index + 1}`),
136
- );
137
- expect(offenders).toEqual([]);
138
- });
139
- });
@@ -1,18 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { ownerWords } from "./identity.ts";
3
-
4
- test("owner words capitalize the subject and possessive for a sentence's start", () => {
5
- expect(
6
- ownerWords({
7
- name: "Alice",
8
- pronouns: { subject: "she", object: "her", possessive: "her" },
9
- }),
10
- ).toEqual({
11
- name: "Alice",
12
- he: "she",
13
- He: "She",
14
- him: "her",
15
- his: "her",
16
- His: "Her",
17
- });
18
- });
@@ -1,112 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Judge, ScoreQuestion } from "../contract/providers.ts";
3
- import { JudgeError } from "../errors.ts";
4
- import { silentLogger } from "../log.ts";
5
- import {
6
- AGENT_BRIEF,
7
- EffortJudge,
8
- levelIndex,
9
- moveIndex,
10
- } from "./effort-judge.ts";
11
-
12
- class FakeJudge implements Judge {
13
- constructor(private readonly scores: number[] | Error) {}
14
- async askYesNo(): Promise<Record<string, number>> {
15
- throw new Error("not used here");
16
- }
17
- async askChoice(): Promise<never> {
18
- throw new Error("not used here");
19
- }
20
- scored: { state: Record<string, unknown>; criteria: readonly string[] }[] =
21
- [];
22
- async askScore(
23
- state: Record<string, unknown>,
24
- _name: string,
25
- question: ScoreQuestion,
26
- ): Promise<number[]> {
27
- this.scored.push({ state, criteria: question.criteria });
28
- if (this.scores instanceof Error) throw this.scores;
29
- return this.scores;
30
- }
31
- }
32
-
33
- describe("EffortJudge", () => {
34
- type Level = "low" | "medium" | "high" | "xhigh";
35
- const judge = (
36
- scores: number[] | Error,
37
- previous: { reply?: string; level?: Level } = {},
38
- fallback: Level = "low",
39
- ) => {
40
- const fakeJudge = new FakeJudge(scores);
41
- const level = new EffortJudge({
42
- judge: fakeJudge,
43
- brief: AGENT_BRIEF,
44
- fallback,
45
- threshold: 0.6,
46
- logger: silentLogger(),
47
- }).judge("sounds good, go ahead", previous);
48
- return { level, fakeJudge };
49
- };
50
-
51
- test("a first turn takes the median of the judge's distribution up to high", async () => {
52
- expect(await judge([0.9, 0.1, 0, 0]).level).toBe("low");
53
- expect(await judge([0.2, 0.6, 0.2, 0]).level).toBe("medium");
54
- expect(await judge([0, 0.1, 0.9, 0]).level).toBe("high");
55
- });
56
-
57
- test("xhigh takes a near-certain answer, otherwise it stays high", async () => {
58
- expect(await judge([0, 0, 0.1, 0.9]).level).toBe("xhigh");
59
- expect(await judge([0, 0, 0.3, 0.7]).level).toBe("high");
60
- expect(await judge([0, 0, 0.3, 0.7], { level: "high" }).level).toBe("high");
61
- });
62
-
63
- test("a split first answer lands where the mass is, not in the middle", () => {
64
- expect(levelIndex([0.6, 0, 0.4, 0])).toBe(0);
65
- expect(levelIndex([0.4, 0, 0.6, 0])).toBe(2);
66
- });
67
-
68
- test("an unsure answer keeps the previous turn's level", async () => {
69
- expect(await judge([0.4, 0.15, 0.45, 0], { level: "high" }).level).toBe(
70
- "high",
71
- );
72
- expect(await judge([0.45, 0.1, 0.45, 0], { level: "low" }).level).toBe(
73
- "low",
74
- );
75
- });
76
-
77
- test("a confident answer moves off the previous level either way", async () => {
78
- expect(await judge([0, 0.3, 0.7, 0], { level: "low" }).level).toBe("high");
79
- expect(await judge([0.55, 0.45, 0, 0], { level: "high" }).level).toBe(
80
- "medium",
81
- );
82
- expect(await judge([0.9, 0.1, 0, 0], { level: "high" }).level).toBe("low");
83
- });
84
-
85
- test("moveIndex checks upgrades before downgrades", () => {
86
- expect(moveIndex(1, [0.5, 0, 0.5, 0], 0.5)).toBe(2);
87
- });
88
-
89
- test("a judge failure keeps the previous level, or runs at the fallback", async () => {
90
- expect(await judge(new JudgeError("down")).level).toBe("low");
91
- expect(await judge(new JudgeError("down"), {}, "medium").level).toBe(
92
- "medium",
93
- );
94
- expect(
95
- await judge(new JudgeError("down"), { level: "high" }, "medium").level,
96
- ).toBe("high");
97
- });
98
-
99
- test("the judge reads the previous reply with the message and the brief's rubric", async () => {
100
- const { level, fakeJudge } = judge([1, 0, 0, 0], {
101
- reply: "Plan: move the database first, then switch DNS.",
102
- });
103
- await level;
104
- expect(fakeJudge.scored[0]).toEqual({
105
- state: {
106
- message: "sounds good, go ahead",
107
- previous_reply: "Plan: move the database first, then switch DNS.",
108
- },
109
- criteria: AGENT_BRIEF.criteria,
110
- });
111
- });
112
- });
@@ -1,126 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { JudgeError } from "../errors.ts";
3
- import { type JudgeModel, ModelJudge } from "./model-judge.ts";
4
-
5
- /** A model that answers each prompt with the next scripted text and records the prompts. */
6
- function scripted(...answers: string[]): {
7
- model: JudgeModel;
8
- prompts: string[];
9
- } {
10
- const prompts: string[] = [];
11
- return {
12
- prompts,
13
- model: async (_system, prompt) => {
14
- prompts.push(prompt);
15
- const answer = answers.shift();
16
- if (answer === undefined) throw new Error("no scripted answer left");
17
- return answer;
18
- },
19
- };
20
- }
21
-
22
- /** What the promise rejected with; undefined when it resolved. */
23
- async function rejection(promise: Promise<unknown>): Promise<unknown> {
24
- try {
25
- await promise;
26
- } catch (error) {
27
- return error;
28
- }
29
- return undefined;
30
- }
31
-
32
- const approval = {
33
- approves: {
34
- type: "yesno" as const,
35
- instructions: "Does the reply approve the held actions?",
36
- criteria: { true: "the owner agrees", false: "the owner declines" },
37
- },
38
- };
39
-
40
- describe("ModelJudge", () => {
41
- test("a yes-or-no answer gives each question's probability, and the prompt carries the state", async () => {
42
- const { model, prompts } = scripted('{"approves": 0.92}');
43
- const judge = new ModelJudge(model);
44
- expect(
45
- await judge.askYesNo({ reply: "sounds good, send it" }, approval),
46
- ).toEqual({
47
- approves: 0.92,
48
- });
49
- expect(prompts[0]).toContain('"reply": "sounds good, send it"');
50
- expect(prompts[0]).toContain("Yes means: the owner agrees.");
51
- // A bare "how sure are you" made a model rate its own certainty, so a clear no scored 0.99.
52
- expect(prompts[0]).toContain("probability that the answer is yes");
53
- });
54
-
55
- test("a code fence or words around the object are tolerated", async () => {
56
- const { model } = scripted('Sure:\n```json\n{"approves": 0.1}\n```');
57
- expect(await new ModelJudge(model).askYesNo({}, approval)).toEqual({
58
- approves: 0.1,
59
- });
60
- });
61
-
62
- test("a missing or out-of-range probability throws", async () => {
63
- const { model } = scripted('{"other": 1}', '{"approves": 1.5}', "no JSON");
64
- const judge = new ModelJudge(model);
65
- for (let i = 0; i < 3; i++)
66
- expect(await rejection(judge.askYesNo({}, approval))).toBeInstanceOf(
67
- JudgeError,
68
- );
69
- });
70
-
71
- test("a choice must name a listed option with a confidence", async () => {
72
- const question = {
73
- type: "choice" as const,
74
- instructions: "Which expression fits the reply?",
75
- criteria: { happy: "cheerful", neutral: "plain" },
76
- };
77
- const { model } = scripted(
78
- '{"choice": "happy", "confidence": 0.8}',
79
- '{"choice": "angry", "confidence": 0.8}',
80
- '{"choice": "neutral"}',
81
- );
82
- const judge = new ModelJudge(model);
83
- expect(await judge.askChoice({}, "expression", question)).toEqual({
84
- choice: "happy",
85
- confidence: 0.8,
86
- });
87
- expect(
88
- await rejection(judge.askChoice({}, "expression", question)),
89
- ).toBeInstanceOf(JudgeError);
90
- expect(
91
- await rejection(judge.askChoice({}, "expression", question)),
92
- ).toBeInstanceOf(JudgeError);
93
- });
94
-
95
- test("a score gives one probability per level, normalized to sum to one", async () => {
96
- const question = {
97
- type: "score" as const,
98
- instructions: "How much thinking does the turn need?",
99
- criteria: ["low", "medium", "high"],
100
- };
101
- const { model } = scripted(
102
- '{"probabilities": [0.2, 0.2, 0.4]}',
103
- '{"probabilities": [0.5, 0.5]}',
104
- '{"probabilities": [0, 0, 0]}',
105
- );
106
- const judge = new ModelJudge(model);
107
- expect(await judge.askScore({}, "effort", question)).toEqual([
108
- 0.25, 0.25, 0.5,
109
- ]);
110
- expect(
111
- await rejection(judge.askScore({}, "effort", question)),
112
- ).toBeInstanceOf(JudgeError);
113
- expect(
114
- await rejection(judge.askScore({}, "effort", question)),
115
- ).toBeInstanceOf(JudgeError);
116
- });
117
-
118
- test("a failing model's error reaches the caller", async () => {
119
- const judge = new ModelJudge(async () => {
120
- throw new Error("offline");
121
- });
122
- expect(String(await rejection(judge.askYesNo({}, approval)))).toContain(
123
- "offline",
124
- );
125
- });
126
- });
@@ -1,170 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ChannelKey } from "../../domain/conversation.ts";
3
- import { messages } from "../../i18n/index.ts";
4
- import { silentLogger } from "../../log.ts";
5
- import { fakeThreads } from "../../testing/thread-host.ts";
6
- import {
7
- type DelegationJob,
8
- type DelegationOutcome,
9
- Delegator,
10
- delegatedTurnText,
11
- } from "./delegator.ts";
12
-
13
- const OWNER = { id: "1", name: "Riley" };
14
-
15
- function setup(
16
- run: (task: string, signal: AbortSignal) => Promise<string>,
17
- timeoutMs?: number,
18
- ) {
19
- const reports: [DelegationJob, DelegationOutcome][] = [];
20
- const { host, threads } = fakeThreads();
21
- /** Threads already archived when each report was delivered. */
22
- const archived: string[][] = [];
23
- const delegator = new Delegator({
24
- worker: { run },
25
- deliver: async (job, outcome) => {
26
- archived.push([...host.closed]);
27
- reports.push([job, outcome]);
28
- },
29
- threads,
30
- logger: silentLogger(),
31
- ...(timeoutMs ? { timeoutMs } : {}),
32
- });
33
- return { delegator, reports, host, archived };
34
- }
35
-
36
- const request = (channel: ChannelKey = "discord:1") => ({
37
- channel,
38
- mode: "party" as const,
39
- author: OWNER,
40
- title: "t",
41
- task: "find it",
42
- });
43
-
44
- describe("Delegator", () => {
45
- test("a report comes back with its job, after start has returned", async () => {
46
- const { delegator, reports } = setup(async (task) => `report for ${task}`);
47
- const job = delegator.start(request());
48
- expect(reports).toEqual([]);
49
- await delegator.idle();
50
- expect(reports).toEqual([
51
- [job, { ok: true, report: "report for find it" }],
52
- ]);
53
- });
54
-
55
- test("a channel holds only so many running jobs; other channels are not affected", async () => {
56
- let release = () => {};
57
- const gate = new Promise<void>((resolve) => {
58
- release = resolve;
59
- });
60
- const { delegator } = setup(async () => {
61
- await gate;
62
- return "done";
63
- });
64
- delegator.start(request());
65
- delegator.start(request());
66
- expect(() => delegator.start(request())).toThrow("already has 2");
67
- expect(delegator.start(request("discord:2")).id).toBe(3);
68
- release();
69
- await delegator.idle();
70
- expect(delegator.start(request()).id).toBe(4);
71
- await delegator.idle();
72
- });
73
-
74
- test("a failing or slow worker reports the failure", async () => {
75
- const { delegator, reports } = setup(
76
- (_task, signal) =>
77
- new Promise((_resolve, reject) =>
78
- signal.addEventListener("abort", () =>
79
- reject(new Error("timed out")),
80
- ),
81
- ),
82
- 10,
83
- );
84
- delegator.start(request());
85
- await delegator.idle();
86
- expect(reports[0]?.[1]).toEqual({ ok: false, error: "timed out" });
87
- });
88
-
89
- test("an owner job runs in a thread of its origin, archived with the report before the turn", async () => {
90
- const { delegator, reports, host, archived } = setup(async () => "R");
91
- delegator.start({
92
- ...request("discord:dm"),
93
- mode: "owner",
94
- origin: "discord:10",
95
- title: "check the Bun version",
96
- });
97
- await delegator.idle();
98
- expect(host.opened).toEqual([
99
- { parentId: "10", name: "check the Bun version", id: "900" },
100
- ]);
101
- expect(host.lines.get("10")).toBe(
102
- messages().threadStarted("check the Bun version", "<#900>"),
103
- );
104
- expect(host.textsIn("900")).toEqual([
105
- messages().delegationTask("find it"),
106
- messages().delegationDone("R"),
107
- ]);
108
- expect(archived).toEqual([["900"]]);
109
- const [job, outcome] = reports[0] ?? [];
110
- expect(job?.channel).toBe("discord:dm");
111
- expect(
112
- delegatedTurnText(
113
- job as DelegationJob,
114
- outcome as DelegationOutcome,
115
- "x",
116
- ),
117
- ).toContain("in the thread <#900>");
118
- });
119
-
120
- test("a failed or timed-out job's thread gets the failure and is archived", async () => {
121
- const { delegator, host } = setup(
122
- (_task, signal) =>
123
- new Promise((_resolve, reject) =>
124
- signal.addEventListener("abort", () =>
125
- reject(new Error("timed out")),
126
- ),
127
- ),
128
- 10,
129
- );
130
- delegator.start({ ...request(), origin: "discord:10" });
131
- await delegator.idle();
132
- expect(host.textsIn("900").at(-1)).toBe(
133
- messages().delegationFailed("timed out"),
134
- );
135
- expect(host.closed).toEqual(["900"]);
136
- });
137
-
138
- test("a party job, or one whose thread cannot open, reports only in its channel", async () => {
139
- const { delegator, reports, host } = setup(async () => "R");
140
- delegator.start(request());
141
- host.failOpen = true;
142
- delegator.start({ ...request("discord:2"), origin: "discord:2" });
143
- await delegator.idle();
144
- expect(host.posts).toEqual([]);
145
- expect(reports.map(([job]) => job.thread)).toEqual([undefined, undefined]);
146
- expect(reports.map(([, outcome]) => outcome.ok)).toEqual([true, true]);
147
- });
148
-
149
- test("an empty task is refused", () => {
150
- const { delegator } = setup(async () => "x");
151
- expect(() => delegator.start({ ...request(), task: " " })).toThrow(
152
- "required",
153
- );
154
- });
155
-
156
- test("the report turn carries the task and the report", () => {
157
- const job: DelegationJob = { ...request(), id: 5, startedAt: new Date(0) };
158
- const text = delegatedTurnText(
159
- job,
160
- { ok: true, report: "R" },
161
- "2026-09-27 01:00",
162
- );
163
- expect(text).toContain("## Delegated task #5: t");
164
- expect(text).toContain("### Task\nfind it");
165
- expect(text).toContain("### Report\nR");
166
- expect(delegatedTurnText(job, { ok: false, error: "boom" }, "x")).toContain(
167
- "failed: boom",
168
- );
169
- });
170
- });