pi-roundtable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/docs/plugins.md +924 -0
  5. package/examples/channels.test.ts +13 -0
  6. package/examples/channels.ts +27 -0
  7. package/examples/dashboard.test.ts +11 -0
  8. package/examples/dashboard.ts +9 -0
  9. package/examples/events.test.ts +26 -0
  10. package/examples/events.ts +25 -0
  11. package/examples/guide.test.ts +52 -0
  12. package/examples/holds.test.ts +16 -0
  13. package/examples/holds.ts +30 -0
  14. package/examples/http.test.ts +12 -0
  15. package/examples/http.ts +17 -0
  16. package/examples/interactions.test.ts +25 -0
  17. package/examples/interactions.ts +28 -0
  18. package/examples/migrations.test.ts +31 -0
  19. package/examples/migrations.ts +42 -0
  20. package/examples/packages.test.ts +9 -0
  21. package/examples/packages.ts +9 -0
  22. package/examples/preflight.test.ts +11 -0
  23. package/examples/preflight.ts +19 -0
  24. package/examples/prompt.test.ts +25 -0
  25. package/examples/prompt.ts +20 -0
  26. package/examples/providers.test.ts +10 -0
  27. package/examples/providers.ts +17 -0
  28. package/examples/seeds.test.ts +12 -0
  29. package/examples/seeds.ts +18 -0
  30. package/examples/selection.test.ts +13 -0
  31. package/examples/selection.ts +14 -0
  32. package/examples/services.test.ts +20 -0
  33. package/examples/services.ts +30 -0
  34. package/examples/session-tools.test.ts +24 -0
  35. package/examples/session-tools.ts +34 -0
  36. package/examples/tools.test.ts +18 -0
  37. package/examples/tools.ts +28 -0
  38. package/package.json +55 -0
  39. package/src/cli/add-plugin.test.ts +107 -0
  40. package/src/cli/add-plugin.ts +75 -0
  41. package/src/cli/checks/basic.test.ts +276 -0
  42. package/src/cli/checks/bun.ts +26 -0
  43. package/src/cli/checks/configuration.ts +28 -0
  44. package/src/cli/checks/database.test.ts +117 -0
  45. package/src/cli/checks/database.ts +100 -0
  46. package/src/cli/checks/discord.test.ts +200 -0
  47. package/src/cli/checks/discord.ts +218 -0
  48. package/src/cli/checks/environment.ts +27 -0
  49. package/src/cli/checks/model.ts +28 -0
  50. package/src/cli/checks/public-url.ts +40 -0
  51. package/src/cli/cli.test.ts +122 -0
  52. package/src/cli/cli.ts +166 -0
  53. package/src/cli/config-edit.test.ts +130 -0
  54. package/src/cli/config-edit.ts +199 -0
  55. package/src/cli/discord-api.ts +96 -0
  56. package/src/cli/doctor.test.ts +184 -0
  57. package/src/cli/doctor.ts +92 -0
  58. package/src/cli/http.ts +23 -0
  59. package/src/cli/init.test.ts +127 -0
  60. package/src/cli/init.ts +82 -0
  61. package/src/cli/main.ts +13 -0
  62. package/src/cli/project.ts +123 -0
  63. package/src/cli/report.ts +68 -0
  64. package/src/cli/roundtable.mjs +22 -0
  65. package/src/cli/runtime.ts +53 -0
  66. package/src/cli/size.test.ts +9 -0
  67. package/src/cli/start.ts +33 -0
  68. package/src/cli/templates.test.ts +129 -0
  69. package/src/cli/templates.ts +119 -0
  70. package/src/cli/testing/fixtures.ts +106 -0
  71. package/src/core/agents/agent-claim.test.ts +142 -0
  72. package/src/core/agents/agent-claim.ts +170 -0
  73. package/src/core/agents/agent-dashboard.test.ts +120 -0
  74. package/src/core/agents/agent-dashboard.ts +185 -0
  75. package/src/core/agents/agent-guild.test.ts +124 -0
  76. package/src/core/agents/agent-messages.ts +189 -0
  77. package/src/core/agents/agent-ports.ts +103 -0
  78. package/src/core/agents/agent-prompt.ts +163 -0
  79. package/src/core/agents/agent-rules.ts +106 -0
  80. package/src/core/agents/agent-schema.ts +115 -0
  81. package/src/core/agents/agent-settings.ts +49 -0
  82. package/src/core/agents/agent-store.test.ts +205 -0
  83. package/src/core/agents/agent-store.ts +384 -0
  84. package/src/core/agents/agent-team.ts +445 -0
  85. package/src/core/agents/agent-tools.ts +386 -0
  86. package/src/core/agents/avatar-studio.test.ts +88 -0
  87. package/src/core/agents/avatar-studio.ts +147 -0
  88. package/src/core/agents/group-messages.ts +99 -0
  89. package/src/core/agents/group-round.test.ts +143 -0
  90. package/src/core/agents/group-round.ts +117 -0
  91. package/src/core/agents/group-turns.ts +132 -0
  92. package/src/core/agents/owner-identity.test.ts +172 -0
  93. package/src/core/agents/team-editing.ts +205 -0
  94. package/src/core/agents/team-keys.ts +40 -0
  95. package/src/core/agents/team-layout.ts +135 -0
  96. package/src/core/agents/team-lifecycle.ts +188 -0
  97. package/src/core/agents/team-options.ts +52 -0
  98. package/src/core/agents/team-status.ts +80 -0
  99. package/src/core/agents/team-text.ts +60 -0
  100. package/src/core/agents/team-turn-types.ts +107 -0
  101. package/src/core/agents/team-turns.test.ts +278 -0
  102. package/src/core/agents/team-turns.ts +323 -0
  103. package/src/core/assets/neutral.png +0 -0
  104. package/src/core/assets/prompts/shared-guest.md +11 -0
  105. package/src/core/assets/prompts/shared.md +11 -0
  106. package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
  107. package/src/core/attachments/attachment-dir.ts +15 -0
  108. package/src/core/attachments/attachment-fetcher.ts +83 -0
  109. package/src/core/attachments/attachments.test.ts +85 -0
  110. package/src/core/attachments/image-prep.ts +50 -0
  111. package/src/core/attachments/prompt-block.ts +37 -0
  112. package/src/core/attachments/turn-attachments.ts +79 -0
  113. package/src/core/boundary.test.ts +45 -0
  114. package/src/core/builtin/agent-server.ts +275 -0
  115. package/src/core/builtin/discord.ts +95 -0
  116. package/src/core/builtin/modules.test.ts +127 -0
  117. package/src/core/builtin/modules.ts +176 -0
  118. package/src/core/builtin/seeds.ts +10 -0
  119. package/src/core/builtin/stores.ts +42 -0
  120. package/src/core/config/config.test.ts +135 -0
  121. package/src/core/config/config.ts +285 -0
  122. package/src/core/config/schema.ts +181 -0
  123. package/src/core/contract/channels.ts +152 -0
  124. package/src/core/contract/discord.ts +33 -0
  125. package/src/core/contract/providers.ts +67 -0
  126. package/src/core/db/guild-scope.ts +53 -0
  127. package/src/core/db/migrations.test.ts +237 -0
  128. package/src/core/db/migrations.ts +37 -0
  129. package/src/core/define-roundtable.test.ts +134 -0
  130. package/src/core/define-roundtable.ts +195 -0
  131. package/src/core/define.test.ts +144 -0
  132. package/src/core/define.ts +154 -0
  133. package/src/core/discord/agent-commands.test.ts +58 -0
  134. package/src/core/discord/agent-commands.ts +385 -0
  135. package/src/core/discord/agent-discord.test.ts +70 -0
  136. package/src/core/discord/agent-discord.ts +367 -0
  137. package/src/core/discord/channel-executor.ts +404 -0
  138. package/src/core/discord/channel-operations.ts +329 -0
  139. package/src/core/discord/discord-surface.ts +431 -0
  140. package/src/core/discord/dispatch-thread-host.test.ts +114 -0
  141. package/src/core/discord/dispatch-thread-host.ts +72 -0
  142. package/src/core/discord/dispatch-threads.test.ts +94 -0
  143. package/src/core/discord/dispatch-threads.ts +180 -0
  144. package/src/core/discord/interaction-module.ts +1 -0
  145. package/src/core/discord/owner-cards.test.ts +435 -0
  146. package/src/core/discord/owner-cards.ts +463 -0
  147. package/src/core/discord/owner-command.ts +126 -0
  148. package/src/core/discord/owner-discord-access.ts +183 -0
  149. package/src/core/discord/owner-discord-threads.ts +160 -0
  150. package/src/core/discord/owner-discord.test.ts +325 -0
  151. package/src/core/discord/owner-discord.ts +282 -0
  152. package/src/core/discord/owner-panel.ts +103 -0
  153. package/src/core/discord/schedule-commands.ts +149 -0
  154. package/src/core/discord/stop-button.ts +20 -0
  155. package/src/core/domain/attachment.ts +39 -0
  156. package/src/core/domain/conversation.ts +42 -0
  157. package/src/core/domain/errors.ts +31 -0
  158. package/src/core/domain/expression.ts +21 -0
  159. package/src/core/domain/owner-prompts.ts +45 -0
  160. package/src/core/domain/ports.ts +91 -0
  161. package/src/core/domain/profile.ts +32 -0
  162. package/src/core/drain.test.ts +46 -0
  163. package/src/core/drain.ts +33 -0
  164. package/src/core/errors.ts +29 -0
  165. package/src/core/events.test.ts +81 -0
  166. package/src/core/events.ts +55 -0
  167. package/src/core/holds.test.ts +54 -0
  168. package/src/core/holds.ts +46 -0
  169. package/src/core/host.test.ts +536 -0
  170. package/src/core/host.ts +297 -0
  171. package/src/core/http/listeners.test.ts +164 -0
  172. package/src/core/http/listeners.ts +151 -0
  173. package/src/core/i18n/agent-panel.ts +96 -0
  174. package/src/core/i18n/cards.ts +64 -0
  175. package/src/core/i18n/channels.ts +28 -0
  176. package/src/core/i18n/conversation.ts +49 -0
  177. package/src/core/i18n/dashboard.ts +72 -0
  178. package/src/core/i18n/discord.ts +23 -0
  179. package/src/core/i18n/en.ts +27 -0
  180. package/src/core/i18n/i18n.test.ts +139 -0
  181. package/src/core/i18n/index.ts +58 -0
  182. package/src/core/i18n/owner.ts +35 -0
  183. package/src/core/i18n/schedules.ts +92 -0
  184. package/src/core/i18n/time-zones.ts +27 -0
  185. package/src/core/i18n/types.ts +7 -0
  186. package/src/core/i18n/zh-tw.ts +26 -0
  187. package/src/core/identity.test.ts +18 -0
  188. package/src/core/identity.ts +31 -0
  189. package/src/core/judging/confirmation-judge.ts +68 -0
  190. package/src/core/judging/effort-judge.test.ts +112 -0
  191. package/src/core/judging/effort-judge.ts +158 -0
  192. package/src/core/judging/model-judge.test.ts +126 -0
  193. package/src/core/judging/model-judge.ts +193 -0
  194. package/src/core/log.ts +45 -0
  195. package/src/core/models.ts +52 -0
  196. package/src/core/modules/background/background-turns.ts +78 -0
  197. package/src/core/modules/delegation/delegate.ts +54 -0
  198. package/src/core/modules/delegation/delegator.test.ts +170 -0
  199. package/src/core/modules/delegation/delegator.ts +191 -0
  200. package/src/core/modules/delegation/sol-worker.ts +78 -0
  201. package/src/core/modules/discord-admin/discord-admin.ts +288 -0
  202. package/src/core/modules/host-shell/shell-policy.ts +386 -0
  203. package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
  204. package/src/core/modules/memory/owner-memory-store.ts +228 -0
  205. package/src/core/modules/memory/owner-memory.ts +153 -0
  206. package/src/core/modules/notify/notify.ts +26 -0
  207. package/src/core/modules/schedules/recurrence.ts +206 -0
  208. package/src/core/modules/schedules/schedule-store.ts +213 -0
  209. package/src/core/modules/schedules/schedule-tools.ts +196 -0
  210. package/src/core/modules/schedules/schedule.test.ts +358 -0
  211. package/src/core/modules/schedules/scheduler.ts +121 -0
  212. package/src/core/modules/schedules/schedules.ts +87 -0
  213. package/src/core/modules/skills/repo-name.ts +11 -0
  214. package/src/core/modules/skills/skill-kind.test.ts +55 -0
  215. package/src/core/modules/skills/skill-link.ts +69 -0
  216. package/src/core/modules/skills/skill-listing.ts +128 -0
  217. package/src/core/modules/skills/skill-registry.test.ts +376 -0
  218. package/src/core/modules/skills/skill-registry.ts +323 -0
  219. package/src/core/modules/skills/skill-rules.ts +120 -0
  220. package/src/core/modules/skills/skill-store.ts +285 -0
  221. package/src/core/modules/skills/skill-tools.ts +207 -0
  222. package/src/core/ops/error-reporter.test.ts +283 -0
  223. package/src/core/ops/error-reporter.ts +271 -0
  224. package/src/core/plugin.ts +200 -0
  225. package/src/core/presentation/card-cadence.ts +41 -0
  226. package/src/core/presentation/headline.ts +34 -0
  227. package/src/core/presentation/presentation.test.ts +166 -0
  228. package/src/core/presentation/quiet-links.ts +36 -0
  229. package/src/core/presentation/reply-splitter.ts +110 -0
  230. package/src/core/presentation/thinking-line.ts +24 -0
  231. package/src/core/public-entry.test.ts +59 -0
  232. package/src/core/registry/contributions.test.ts +215 -0
  233. package/src/core/registry/contributions.ts +249 -0
  234. package/src/core/registry/interactions.test.ts +87 -0
  235. package/src/core/registry/interactions.ts +55 -0
  236. package/src/core/registry/providers.test.ts +103 -0
  237. package/src/core/registry/providers.ts +55 -0
  238. package/src/core/routing/channel-queue.test.ts +31 -0
  239. package/src/core/routing/channel-queue.ts +44 -0
  240. package/src/core/routing/channel-router.test.ts +326 -0
  241. package/src/core/routing/channel-router.ts +157 -0
  242. package/src/core/routing/conversation-kind.ts +14 -0
  243. package/src/core/routing/forward-join.ts +65 -0
  244. package/src/core/routing/message-text.ts +16 -0
  245. package/src/core/routing/settle-turn.test.ts +21 -0
  246. package/src/core/routing/settle-turn.ts +26 -0
  247. package/src/core/runtime/compaction-tiers.test.ts +227 -0
  248. package/src/core/runtime/compaction-tiers.ts +203 -0
  249. package/src/core/runtime/conversation-sessions.ts +162 -0
  250. package/src/core/runtime/extensions/agent-prompt.ts +16 -0
  251. package/src/core/runtime/extensions/ask-user.test.ts +114 -0
  252. package/src/core/runtime/extensions/ask-user.ts +98 -0
  253. package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
  254. package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
  255. package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
  256. package/src/core/runtime/mcp.ts +23 -0
  257. package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
  258. package/src/core/runtime/pending-confirmation-store.ts +75 -0
  259. package/src/core/runtime/pi-agent-runtime.ts +477 -0
  260. package/src/core/runtime/prompt-slot.ts +81 -0
  261. package/src/core/runtime/runtime-types.ts +150 -0
  262. package/src/core/runtime/session-archive.test.ts +20 -0
  263. package/src/core/runtime/session-archive.ts +17 -0
  264. package/src/core/runtime/session-factory.ts +291 -0
  265. package/src/core/runtime/steerable-run.test.ts +264 -0
  266. package/src/core/runtime/steerable-run.ts +109 -0
  267. package/src/core/runtime/text-tools.test.ts +110 -0
  268. package/src/core/runtime/text-tools.ts +66 -0
  269. package/src/core/runtime/turn-answer.test.ts +72 -0
  270. package/src/core/runtime/turn-answer.ts +79 -0
  271. package/src/core/runtime/worker-task.test.ts +82 -0
  272. package/src/core/runtime/worker-task.ts +65 -0
  273. package/src/core/services.test.ts +28 -0
  274. package/src/core/services.ts +107 -0
  275. package/src/core/sessions.test.ts +85 -0
  276. package/src/core/sessions.ts +149 -0
  277. package/src/core/shared/attachment-reader.ts +91 -0
  278. package/src/core/shared/delegate-tool.ts +15 -0
  279. package/src/core/shared/mcp-adapter.ts +58 -0
  280. package/src/core/shared/package-dir.ts +9 -0
  281. package/src/core/shared/profile-tools.ts +16 -0
  282. package/src/core/shared/read-attachment-tool.ts +43 -0
  283. package/src/core/shared/schedule-tools.ts +118 -0
  284. package/src/core/shared/session-messages.ts +50 -0
  285. package/src/core/shared/tool-result.ts +9 -0
  286. package/src/core/shared/unix-server.ts +18 -0
  287. package/src/core/size.test.ts +9 -0
  288. package/src/core/speakers.test.ts +78 -0
  289. package/src/core/speakers.ts +143 -0
  290. package/src/core/testing/database.ts +52 -0
  291. package/src/core/testing/file-size.ts +34 -0
  292. package/src/core/testing/locale.ts +8 -0
  293. package/src/core/testing/modules.ts +97 -0
  294. package/src/core/testing/owner.ts +15 -0
  295. package/src/core/testing/thread-host.ts +60 -0
  296. package/src/core/time.test.ts +166 -0
  297. package/src/core/time.ts +67 -0
  298. package/src/core/tool-tiers.test.ts +117 -0
  299. package/src/core/tool-tiers.ts +127 -0
  300. package/src/entries.test.ts +112 -0
  301. package/src/index.ts +28 -0
  302. package/src/testing.test.ts +156 -0
  303. package/src/testing.ts +182 -0
  304. package/templates/.env.example +22 -0
  305. package/templates/README.md +11 -0
  306. package/templates/_gitignore +3 -0
  307. package/templates/agents.ts +12 -0
  308. package/templates/biome.json.tmpl +7 -0
  309. package/templates/docker-compose.yml +16 -0
  310. package/templates/package.json.tmpl +24 -0
  311. package/templates/persona/shared.md +2 -0
  312. package/templates/plugin/plugin.test.ts.tmpl +13 -0
  313. package/templates/plugin/plugin.ts +18 -0
  314. package/templates/roundtable.config.ts +24 -0
  315. package/templates/tsconfig.json.tmpl +16 -0
@@ -0,0 +1,92 @@
1
+ import type { CatalogContext } from "./types.ts";
2
+
3
+ const WEEKDAYS_ZH: Record<string, string> = {
4
+ sun: "日",
5
+ mon: "一",
6
+ tue: "二",
7
+ wed: "三",
8
+ thu: "四",
9
+ fri: "五",
10
+ sat: "六",
11
+ };
12
+
13
+ const WEEKDAYS_EN: Record<string, string> = {
14
+ sun: "Sun",
15
+ mon: "Mon",
16
+ tue: "Tue",
17
+ wed: "Wed",
18
+ thu: "Thu",
19
+ fri: "Fri",
20
+ sat: "Sat",
21
+ };
22
+
23
+ /** The schedule panel, the schedule slash commands, and how a recurrence reads. */
24
+ export function schedulesEn(ctx: CatalogContext) {
25
+ return {
26
+ scheduleWeekday: (day: string) => WEEKDAYS_EN[day] ?? day,
27
+ scheduleOnce: (date: string, time: string) => `Once on ${date} at ${time}`,
28
+ scheduleDaily: (time: string) => `Daily at ${time}`,
29
+ scheduleEveryDays: (days: number, time: string, startDate: string) =>
30
+ `Every ${days} days at ${time} (counting from ${startDate})`,
31
+ scheduleWeekly: (days: readonly string[], time: string) =>
32
+ `Weekly on ${days.join(", ")} at ${time}`,
33
+ scheduleHead: (id: number, title: string, channel: string, mode: string) =>
34
+ `**#${id} ${title}** · ${channel} · ${mode}`,
35
+ scheduleModeOwner: ctx.assistant,
36
+ scheduleModeParty: "party role",
37
+ scheduleFooter: `Schedules are created and edited by the bot in conversation; here you can review and cancel them. \`/${ctx.root} schedule cancel\` cancels one.`,
38
+ scheduleGroupDescription: "Schedules",
39
+ scheduleListDescription: "List the schedules of every channel",
40
+ scheduleCancelDescription: "Cancel a schedule",
41
+ scheduleIdDescription: "The schedule to cancel",
42
+ scheduleLastRun: (unix: number, status: string) =>
43
+ `; last run <t:${unix}:R> (${status})`,
44
+ scheduleNextRun: (recurrence: string, unix: number) =>
45
+ `${recurrence}; next run <t:${unix}:f>`,
46
+ scheduleSetBy: (name: string, lastRun: string) =>
47
+ `Set by ${name}${lastRun}`,
48
+ scheduleChoice: (id: number, title: string, recurrence: string) =>
49
+ `#${id} ${title} (${recurrence})`,
50
+ scheduleTitle: "Schedules",
51
+ scheduleNone: "No schedules right now.",
52
+ schedulePickOne: "Pick a schedule from the list.",
53
+ scheduleUnknown: (id: string) => `There is no schedule #${id}.`,
54
+ scheduleCancelled: "Schedule cancelled",
55
+ };
56
+ }
57
+
58
+ export function schedulesZhTW(
59
+ ctx: CatalogContext,
60
+ ): ReturnType<typeof schedulesEn> {
61
+ return {
62
+ scheduleWeekday: (day: string) => WEEKDAYS_ZH[day] ?? day,
63
+ scheduleOnce: (date: string, time: string) => `單次 ${date} ${time}`,
64
+ scheduleDaily: (time: string) => `每天 ${time}`,
65
+ scheduleEveryDays: (days: number, time: string, startDate: string) =>
66
+ `每 ${days} 天 ${time}(從 ${startDate} 起算)`,
67
+ scheduleWeekly: (days: readonly string[], time: string) =>
68
+ `每週${days.join("、")} ${time}`,
69
+ scheduleHead: (id: number, title: string, channel: string, mode: string) =>
70
+ `**#${id} ${title}** ${channel} ${mode}`,
71
+ scheduleModeOwner: ctx.assistant,
72
+ scheduleModeParty: "party 角色",
73
+ scheduleFooter: `排程由 bot 在對話中建立與修改;這裡可以總覽與取消。\`/${ctx.root} schedule cancel\` 取消。`,
74
+ scheduleGroupDescription: "排程",
75
+ scheduleListDescription: "列出所有頻道的排程",
76
+ scheduleCancelDescription: "取消一個排程",
77
+ scheduleIdDescription: "要取消的排程",
78
+ scheduleLastRun: (unix: number, status: string) =>
79
+ `;上次 <t:${unix}:R>(${status})`,
80
+ scheduleNextRun: (recurrence: string, unix: number) =>
81
+ `${recurrence};下次 <t:${unix}:f>`,
82
+ scheduleSetBy: (name: string, lastRun: string) =>
83
+ `由 ${name} 設定${lastRun}`,
84
+ scheduleChoice: (id: number, title: string, recurrence: string) =>
85
+ `#${id} ${title}(${recurrence})`,
86
+ scheduleTitle: "排程",
87
+ scheduleNone: "目前沒有排程。",
88
+ schedulePickOne: "請從清單選一個排程。",
89
+ scheduleUnknown: (id: string) => `沒有 #${id} 這個排程。`,
90
+ scheduleCancelled: "排程已取消",
91
+ };
92
+ }
@@ -0,0 +1,27 @@
1
+ /** A zone's place name: the last part of its IANA id, underscores as spaces ("America/New_York" is "New York"). */
2
+ function zoneCity(zone: string): string {
3
+ return (zone.split("/").at(-1) ?? zone).replaceAll("_", " ");
4
+ }
5
+
6
+ /** The zone wording of one catalog, from how it names a zone. */
7
+ function zoneWording(place: (zone: string) => string) {
8
+ const zoneTime = (zone: string) => `${place(zone)} time`;
9
+ return {
10
+ zoneName: place,
11
+ zoneTime,
12
+ atTimeError: (zone: string) =>
13
+ `at must be ${zoneTime(zone)} as "YYYY-MM-DD HH:MM"`,
14
+ repeatingTimeError: (zone: string) =>
15
+ `give at ("YYYY-MM-DD HH:MM") for one run, or time ("HH:MM", ${place(zone)}) to repeat`,
16
+ };
17
+ }
18
+
19
+ /** Time-zone wording shared by prompts and schedule tools; the English catalog names a zone by its IANA id. */
20
+ export function timeZonesEn() {
21
+ return zoneWording((zone) => zone);
22
+ }
23
+
24
+ /** The Traditional Chinese catalog names a zone by its city, so no zone has a wording of its own. */
25
+ export function timeZonesZhTW(): ReturnType<typeof timeZonesEn> {
26
+ return zoneWording(zoneCity);
27
+ }
@@ -0,0 +1,7 @@
1
+ /** What every catalog factory knows about the deployment it speaks for. */
2
+ export interface CatalogContext {
3
+ /** The assistant's display name, such as "Roundtable". */
4
+ assistant: string;
5
+ /** The name of the root slash command, without the slash. */
6
+ root: string;
7
+ }
@@ -0,0 +1,26 @@
1
+ import { agentPanelZhTW } from "./agent-panel.ts";
2
+ import { cardsZhTW } from "./cards.ts";
3
+ import { channelsZhTW } from "./channels.ts";
4
+ import { conversationZhTW } from "./conversation.ts";
5
+ import { dashboardZhTW } from "./dashboard.ts";
6
+ import { discordZhTW } from "./discord.ts";
7
+ import type { Messages } from "./en.ts";
8
+ import { ownerZhTW } from "./owner.ts";
9
+ import { schedulesZhTW } from "./schedules.ts";
10
+ import { timeZonesZhTW } from "./time-zones.ts";
11
+ import type { CatalogContext } from "./types.ts";
12
+
13
+ /** The Traditional Chinese catalog. */
14
+ export function createZhTW(ctx: CatalogContext): Messages {
15
+ return {
16
+ ...cardsZhTW(ctx),
17
+ ...ownerZhTW(ctx),
18
+ ...schedulesZhTW(ctx),
19
+ ...timeZonesZhTW(),
20
+ ...agentPanelZhTW(ctx),
21
+ ...conversationZhTW(ctx),
22
+ ...dashboardZhTW(ctx),
23
+ ...channelsZhTW(ctx),
24
+ ...discordZhTW(ctx),
25
+ };
26
+ }
@@ -0,0 +1,18 @@
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
+ });
@@ -0,0 +1,31 @@
1
+ /** Who the agents work for, as prompts and tool descriptions name them. */
2
+ export interface OwnerIdentity {
3
+ name: string;
4
+ /** How prompts refer back to the owner, such as he, him, his. */
5
+ pronouns: { subject: string; object: string; possessive: string };
6
+ }
7
+
8
+ const capital = (word: string) => word.charAt(0).toUpperCase() + word.slice(1);
9
+
10
+ /** The owner's name and pronouns as prompt text uses them, capitalized for a sentence's start. */
11
+ export function ownerWords(owner: OwnerIdentity): OwnerWords {
12
+ const { subject, object, possessive } = owner.pronouns;
13
+ return {
14
+ name: owner.name,
15
+ he: subject,
16
+ He: capital(subject),
17
+ him: object,
18
+ his: possessive,
19
+ His: capital(possessive),
20
+ };
21
+ }
22
+
23
+ /** The owner's words as `ownerWords` gives them. */
24
+ export interface OwnerWords {
25
+ name: string;
26
+ he: string;
27
+ He: string;
28
+ him: string;
29
+ his: string;
30
+ His: string;
31
+ }
@@ -0,0 +1,68 @@
1
+ import type { Judge } from "../contract/providers.ts";
2
+ import type { PendingConfirmation } from "../domain/conversation.ts";
3
+ import type { Logger } from "../log.ts";
4
+
5
+ export interface ConfirmationJudgeOptions {
6
+ judge: Judge;
7
+ threshold: number;
8
+ /** How the judge's instructions name the assistant that held the actions. */
9
+ assistant?: string;
10
+ logger: Logger;
11
+ }
12
+
13
+ /** Decides with the judge whether the owner's reply confirms the actions the assistant held for them. */
14
+ export class ConfirmationJudge {
15
+ readonly #options: ConfirmationJudgeOptions;
16
+
17
+ constructor(options: ConfirmationJudgeOptions) {
18
+ this.#options = options;
19
+ }
20
+
21
+ /** True only for a confident approval; a failed request counts as no approval. */
22
+ async approves(
23
+ pending: PendingConfirmation,
24
+ reply: string,
25
+ ): Promise<boolean> {
26
+ const {
27
+ judge,
28
+ threshold,
29
+ logger,
30
+ assistant = "The assistant",
31
+ } = this.#options;
32
+ try {
33
+ const answer = await judge.askChoice(
34
+ {
35
+ held_actions: pending.calls.map((call) => call.action),
36
+ owner_reply: reply,
37
+ },
38
+ "confirmation",
39
+ {
40
+ type: "choice",
41
+ instructions: `${assistant} asked the owner to confirm the held_actions before running them. Does owner_reply give the go-ahead for them as described?`,
42
+ criteria: {
43
+ approve: `The owner clearly agrees that ${assistant} should go ahead with the held actions as described.`,
44
+ decline:
45
+ "The owner refuses, cancels, postpones, or asks to change the actions first.",
46
+ other:
47
+ "The reply is about something else or does not clearly answer.",
48
+ },
49
+ },
50
+ );
51
+ const approved =
52
+ answer.choice === "approve" && answer.confidence >= threshold;
53
+ logger.info(
54
+ {
55
+ choice: answer.choice,
56
+ confidence: answer.confidence,
57
+ approved,
58
+ actions: pending.calls.map((call) => call.action),
59
+ },
60
+ "confirmation decided",
61
+ );
62
+ return approved;
63
+ } catch (error) {
64
+ logger.warn({ err: error }, "confirmation request failed; not approving");
65
+ return false;
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,112 @@
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
+ });
@@ -0,0 +1,158 @@
1
+ import type { Judge } from "../contract/providers.ts";
2
+ import type { Logger } from "../log.ts";
3
+ import type { ThinkingLevel } from "../models.ts";
4
+
5
+ /** The levels the judge chooses between, lowest first; criterion i of a brief asks for level i. */
6
+ export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh"] as const;
7
+ export type EffortLevel = (typeof EFFORT_LEVELS)[number];
8
+
9
+ /** What the judge is told about the conversation, and one criterion per effort level. */
10
+ export interface EffortBrief {
11
+ instructions: string;
12
+ criteria: readonly [string, string, string, string];
13
+ }
14
+
15
+ export const JUDGE_WORK =
16
+ "How much step-by-step reasoning does handling the message well require? Judge the work " +
17
+ "the message asks for, not its length or tone.";
18
+
19
+ /** The default brief: an assistant agent that works through tools for its owner. */
20
+ export const AGENT_BRIEF: EffortBrief = {
21
+ instructions:
22
+ "The owner sent this message to a personal assistant agent that works through tools: " +
23
+ "notes, schedules, research, coding, and infrastructure. previous_reply, when present, is " +
24
+ `the assistant's last answer in this conversation. ${JUDGE_WORK} A short reply that ` +
25
+ "continues earlier work asks for as much as the work it continues.",
26
+ criteria: [
27
+ "Chat or a quick lookup: a greeting, thanks, a short factual question, reading one note " +
28
+ "or calendar entry, setting a simple reminder, confirming or declining held actions.",
29
+ "Routine work: a few tool calls in a row, a small edit or a message draft, summarizing a " +
30
+ "page or a few notes, a research question with a clear answer.",
31
+ "Substantial work: debugging, planning or changing code or infrastructure, research that " +
32
+ "compares sources, a decision with real tradeoffs, work spanning several tools or systems.",
33
+ "An exceptional problem that sustained reasoning would still get wrong: architecture or " +
34
+ "migration across systems, a subtle correctness or security problem, synthesizing a " +
35
+ "large body of material into a design, or the owner explicitly asks for the deepest " +
36
+ "thinking available.",
37
+ ],
38
+ };
39
+
40
+ /**
41
+ * How much of the distribution must reach each level before a first turn runs at it. The
42
+ * top level is slow and costly, so it takes a near-certain answer; below it, half is enough.
43
+ */
44
+ const REQUIRED_MASS = [0, 0.5, 0.5, 0.8] as const;
45
+
46
+ /** The judge reads this much of the previous reply; its gist is at the start and the end. */
47
+ const PREVIOUS_REPLY_CHARS = 1_500;
48
+
49
+ export interface EffortJudgeOptions<Fallback extends ThinkingLevel> {
50
+ judge: Judge;
51
+ brief: EffortBrief;
52
+ /** The level of a first turn the judge could not judge. */
53
+ fallback: Fallback;
54
+ /** The share of the judge's answer a turn needs to move off the previous turn's level. */
55
+ threshold: number;
56
+ logger: Logger;
57
+ }
58
+
59
+ /** What the conversation's previous turn left: its answer and the level it ran at. */
60
+ export interface PreviousTurn<Level extends ThinkingLevel> {
61
+ reply?: string;
62
+ level?: Level;
63
+ }
64
+
65
+ /** Picks the thinking level of one turn from the judge's reading of its message. */
66
+ export class EffortJudge<Fallback extends ThinkingLevel = ThinkingLevel> {
67
+ readonly #options: EffortJudgeOptions<Fallback>;
68
+
69
+ constructor(options: EffortJudgeOptions<Fallback>) {
70
+ this.#options = options;
71
+ }
72
+
73
+ /** Keeps the previous turn's level when the judge is unsure or fails. */
74
+ async judge(
75
+ message: string,
76
+ previous: PreviousTurn<EffortLevel | Fallback> = {},
77
+ ): Promise<EffortLevel | Fallback> {
78
+ const { judge, brief, fallback, threshold, logger } = this.#options;
79
+ try {
80
+ const probabilities = await judge.askScore(
81
+ {
82
+ message,
83
+ ...(previous.reply
84
+ ? { previous_reply: excerpt(previous.reply) }
85
+ : {}),
86
+ },
87
+ "effort",
88
+ {
89
+ type: "score",
90
+ instructions: brief.instructions,
91
+ criteria: brief.criteria,
92
+ },
93
+ );
94
+ const from = EFFORT_LEVELS.indexOf(previous.level as EffortLevel);
95
+ const index =
96
+ from === -1
97
+ ? levelIndex(probabilities)
98
+ : moveIndex(from, probabilities, threshold);
99
+ const level =
100
+ index === from && previous.level
101
+ ? previous.level
102
+ : (EFFORT_LEVELS[index] ?? "low");
103
+ logger.info(
104
+ { probabilities, previous: previous.level, level },
105
+ "effort decided",
106
+ );
107
+ return level;
108
+ } catch (error) {
109
+ const level = previous.level ?? fallback;
110
+ logger.warn({ err: error, level }, "effort request failed");
111
+ return level;
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * A first turn's level: the highest level whose share of the distribution at or above it
118
+ * meets REQUIRED_MASS. Below the top level that is the median of an ordinal answer, which
119
+ * unlike the mean never lands on a middle level nothing voted for.
120
+ */
121
+ export function levelIndex(probabilities: readonly number[]): number {
122
+ let reached = 0;
123
+ for (let index = probabilities.length - 1; index > 0; index--) {
124
+ reached += probabilities[index] ?? 0;
125
+ if (reached >= (REQUIRED_MASS[index] ?? 0.5)) return index;
126
+ }
127
+ return 0;
128
+ }
129
+
130
+ /**
131
+ * A later turn's level, starting from the previous one: up to the highest level that
132
+ * `threshold` of the answer reaches, and never less than a first turn needs, else down to
133
+ * the lowest level that `threshold` of it stays at or under, else where it was. Upgrades
134
+ * are checked first, because thinking too much costs less than answering wrong.
135
+ */
136
+ export function moveIndex(
137
+ from: number,
138
+ probabilities: readonly number[],
139
+ threshold: number,
140
+ ): number {
141
+ const sum = (values: readonly number[]) =>
142
+ values.reduce((total, value) => total + value, 0);
143
+ for (let index = probabilities.length - 1; index > from; index--)
144
+ if (
145
+ sum(probabilities.slice(index)) >=
146
+ Math.max(threshold, REQUIRED_MASS[index] ?? 0)
147
+ )
148
+ return index;
149
+ for (let index = 0; index < from; index++)
150
+ if (sum(probabilities.slice(0, index + 1)) >= threshold) return index;
151
+ return from;
152
+ }
153
+
154
+ function excerpt(text: string): string {
155
+ if (text.length <= PREVIOUS_REPLY_CHARS) return text;
156
+ const half = PREVIOUS_REPLY_CHARS / 2;
157
+ return `${text.slice(0, half)}\n…\n${text.slice(-half)}`;
158
+ }
@@ -0,0 +1,126 @@
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
+ });