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,213 @@
1
+ import type { SQL } from "bun";
2
+ import type { Migration } from "../../db/migrations.ts";
3
+ import type { ChannelKey } from "../../domain/conversation.ts";
4
+ import type { Tier } from "../../speakers.ts";
5
+ import type { Recurrence } from "./recurrence.ts";
6
+
7
+ /** Owner schedules run the owner agent; party schedules run the channel's party role. */
8
+ export type ScheduleMode = "owner" | "party";
9
+
10
+ export interface Schedule {
11
+ id: number;
12
+ channel: ChannelKey;
13
+ mode: ScheduleMode;
14
+ title: string;
15
+ prompt: string;
16
+ recurrence: Recurrence;
17
+ nextRun: Date;
18
+ createdById: string;
19
+ createdByName: string;
20
+ /** The creator's tier when it was set; the schedule runs at it and lower tiers may not change it. */
21
+ createdTier: Tier;
22
+ createdAt: Date;
23
+ lastRun?: Date;
24
+ lastStatus?: string;
25
+ }
26
+
27
+ export interface NewSchedule {
28
+ channel: ChannelKey;
29
+ mode: ScheduleMode;
30
+ title: string;
31
+ prompt: string;
32
+ recurrence: Recurrence;
33
+ nextRun: Date;
34
+ createdById: string;
35
+ createdByName: string;
36
+ createdTier: Tier;
37
+ }
38
+
39
+ export interface ScheduleChange {
40
+ title?: string;
41
+ prompt?: string;
42
+ recurrence?: Recurrence;
43
+ nextRun?: Date;
44
+ }
45
+
46
+ interface Row {
47
+ id: string | number;
48
+ channel_key: ChannelKey;
49
+ mode: ScheduleMode;
50
+ title: string;
51
+ prompt: string;
52
+ recurrence: string;
53
+ next_run: Date;
54
+ created_by_id: string;
55
+ created_by_name: string;
56
+ created_tier: Tier;
57
+ created_at: Date;
58
+ last_run: Date | null;
59
+ last_status: string | null;
60
+ }
61
+
62
+ function toSchedule(row: Row): Schedule {
63
+ return {
64
+ id: Number(row.id),
65
+ channel: row.channel_key,
66
+ mode: row.mode,
67
+ title: row.title,
68
+ prompt: row.prompt,
69
+ // pi-lens-ignore: unchecked-throwing-call — this store wrote the JSON; a corrupt row should fail loudly
70
+ recurrence: JSON.parse(row.recurrence) as Recurrence,
71
+ nextRun: row.next_run,
72
+ createdById: row.created_by_id,
73
+ createdByName: row.created_by_name,
74
+ createdTier: row.created_tier,
75
+ createdAt: row.created_at,
76
+ ...(row.last_run ? { lastRun: row.last_run } : {}),
77
+ ...(row.last_status ? { lastStatus: row.last_status } : {}),
78
+ };
79
+ }
80
+
81
+ /** Scheduled turns, in the database. A one-time schedule's row is deleted once it fires. */
82
+ export class ScheduleStore {
83
+ readonly #sql: SQL;
84
+
85
+ private constructor(sql: SQL) {
86
+ this.#sql = sql;
87
+ }
88
+
89
+ /** The store's tables; the host runs this before any store attaches. */
90
+ static readonly migration: Migration = {
91
+ name: "schedules",
92
+ up: async (sql) => {
93
+ await sql`
94
+ CREATE TABLE IF NOT EXISTS schedules (
95
+ id bigserial PRIMARY KEY,
96
+ channel_key text NOT NULL,
97
+ mode text NOT NULL,
98
+ title text NOT NULL,
99
+ prompt text NOT NULL,
100
+ recurrence text NOT NULL,
101
+ next_run timestamptz NOT NULL,
102
+ created_by_id text NOT NULL,
103
+ created_by_name text NOT NULL,
104
+ created_at timestamptz NOT NULL DEFAULT now(),
105
+ last_run timestamptz,
106
+ last_status text
107
+ )`;
108
+ // Schedules made before tiers existed were the owner's.
109
+ await sql`ALTER TABLE schedules ADD COLUMN IF NOT EXISTS created_tier text NOT NULL DEFAULT 'owner'`;
110
+ await sql`CREATE INDEX IF NOT EXISTS schedules_next_run ON schedules (next_run)`;
111
+ await sql`CREATE INDEX IF NOT EXISTS schedules_channel ON schedules (channel_key)`;
112
+ },
113
+ };
114
+
115
+ /** The store over the host's migrated pool. */
116
+ static async attach(sql: SQL): Promise<ScheduleStore> {
117
+ return new ScheduleStore(sql);
118
+ }
119
+
120
+ async create(schedule: NewSchedule): Promise<Schedule> {
121
+ const rows: Row[] = await this.#sql`
122
+ INSERT INTO schedules (channel_key, mode, title, prompt, recurrence, next_run,
123
+ created_by_id, created_by_name, created_tier)
124
+ VALUES (${schedule.channel}, ${schedule.mode}, ${schedule.title}, ${schedule.prompt},
125
+ ${JSON.stringify(schedule.recurrence)}, ${schedule.nextRun},
126
+ ${schedule.createdById}, ${schedule.createdByName}, ${schedule.createdTier})
127
+ RETURNING *`;
128
+ const [row] = rows;
129
+ if (!row) throw new Error("the schedule insert returned no row");
130
+ return toSchedule(row);
131
+ }
132
+
133
+ async get(id: number): Promise<Schedule | undefined> {
134
+ const rows: Row[] = await this
135
+ .#sql`SELECT * FROM schedules WHERE id = ${id}`;
136
+ return rows[0] ? toSchedule(rows[0]) : undefined;
137
+ }
138
+
139
+ async forChannel(channel: ChannelKey): Promise<Schedule[]> {
140
+ const rows: Row[] = await this.#sql`
141
+ SELECT * FROM schedules WHERE channel_key = ${channel} ORDER BY next_run`;
142
+ return rows.map(toSchedule);
143
+ }
144
+
145
+ async all(): Promise<Schedule[]> {
146
+ const rows: Row[] = await this
147
+ .#sql`SELECT * FROM schedules ORDER BY next_run`;
148
+ return rows.map(toSchedule);
149
+ }
150
+
151
+ /** Applies a change to one of the channel's schedules; undefined when it has no such schedule. */
152
+ async update(
153
+ channel: ChannelKey,
154
+ id: number,
155
+ change: ScheduleChange,
156
+ ): Promise<Schedule | undefined> {
157
+ const current = await this.get(id);
158
+ if (!current || current.channel !== channel) return undefined;
159
+ const rows: Row[] = await this.#sql`
160
+ UPDATE schedules SET
161
+ title = ${change.title ?? current.title},
162
+ prompt = ${change.prompt ?? current.prompt},
163
+ recurrence = ${JSON.stringify(change.recurrence ?? current.recurrence)},
164
+ next_run = ${change.nextRun ?? current.nextRun}
165
+ WHERE id = ${id}
166
+ RETURNING *`;
167
+ return rows[0] ? toSchedule(rows[0]) : undefined;
168
+ }
169
+
170
+ /** Deletes a schedule, only from the given channel when one is given. */
171
+ async remove(
172
+ id: number,
173
+ channel?: ChannelKey,
174
+ ): Promise<Schedule | undefined> {
175
+ const rows: Row[] = channel
176
+ ? await this.#sql`
177
+ DELETE FROM schedules WHERE id = ${id} AND channel_key = ${channel} RETURNING *`
178
+ : await this.#sql`DELETE FROM schedules WHERE id = ${id} RETURNING *`;
179
+ return rows[0] ? toSchedule(rows[0]) : undefined;
180
+ }
181
+
182
+ async due(now: Date): Promise<Schedule[]> {
183
+ const rows: Row[] = await this.#sql`
184
+ SELECT * FROM schedules WHERE next_run <= ${now} ORDER BY next_run`;
185
+ return rows.map(toSchedule);
186
+ }
187
+
188
+ /**
189
+ * Takes a due schedule before it runs: moves it to its next run, or deletes it when there
190
+ * is none. The next_run guard makes a second claim of the same run a no-op.
191
+ */
192
+ async claim(
193
+ schedule: Schedule,
194
+ next: Date | undefined,
195
+ now: Date,
196
+ ): Promise<boolean> {
197
+ const rows: unknown[] = next
198
+ ? await this.#sql`
199
+ UPDATE schedules SET next_run = ${next}, last_run = ${now}, last_status = 'running'
200
+ WHERE id = ${schedule.id} AND next_run = ${schedule.nextRun}
201
+ RETURNING id`
202
+ : await this.#sql`
203
+ DELETE FROM schedules WHERE id = ${schedule.id} AND next_run = ${schedule.nextRun}
204
+ RETURNING id`;
205
+ return rows.length > 0;
206
+ }
207
+
208
+ /** Records how a run ended; a deleted one-time schedule has no row left to update. */
209
+ async recordStatus(id: number, status: string): Promise<void> {
210
+ await this
211
+ .#sql`UPDATE schedules SET last_status = ${status} WHERE id = ${id}`;
212
+ }
213
+ }
@@ -0,0 +1,196 @@
1
+ import type { ChannelKey } from "../../domain/conversation.ts";
2
+ import { ScheduleError } from "../../domain/errors.ts";
3
+ import { messages } from "../../i18n/index.ts";
4
+ import type { ScheduleToolName } from "../../shared/schedule-tools.ts";
5
+ import { type Tier, tierAtLeast } from "../../speakers.ts";
6
+ import { timeZone, zonedStamp } from "../../time.ts";
7
+ import {
8
+ describeRecurrence,
9
+ nextRun,
10
+ parseRecurrence,
11
+ type Recurrence,
12
+ type RecurrenceInput,
13
+ } from "./recurrence.ts";
14
+ import type {
15
+ Schedule,
16
+ ScheduleMode,
17
+ ScheduleStore,
18
+ } from "./schedule-store.ts";
19
+
20
+ /** Party channels are open to anyone, so their roles get tighter limits than the owner's agent. */
21
+ export const SCHEDULE_LIMITS: Readonly<
22
+ Record<
23
+ ScheduleMode,
24
+ { perChannel: number; promptChars: number; aheadDays: number }
25
+ >
26
+ > = {
27
+ owner: { perChannel: 20, promptChars: 8_000, aheadDays: 366 },
28
+ party: { perChannel: 5, promptChars: 2_000, aheadDays: 90 },
29
+ };
30
+ const TITLE_CHARS = 80;
31
+ const LIST_PROMPT_PREVIEW = 200;
32
+ const DAY_MS = 86_400_000;
33
+
34
+ export interface ScheduleToolContext {
35
+ store: Pick<
36
+ ScheduleStore,
37
+ "create" | "get" | "forChannel" | "update" | "remove"
38
+ >;
39
+ channel: ChannelKey;
40
+ mode: ScheduleMode;
41
+ /** Who asked, recorded on created schedules; a scheduled run speaks for its creator. */
42
+ author: { id: string; name: string; tier?: Tier };
43
+ now: Date;
44
+ }
45
+
46
+ type Input = Record<string, unknown> & RecurrenceInput;
47
+
48
+ function text(input: Input, name: string, max: number): string {
49
+ const value = input[name];
50
+ if (typeof value !== "string" || !value.trim())
51
+ throw new ScheduleError(`${name} is required`);
52
+ if (value.length > max)
53
+ throw new ScheduleError(
54
+ `${name} is ${value.length} characters; keep it within ${max}`,
55
+ );
56
+ return value.trim();
57
+ }
58
+
59
+ function id(input: Input): number {
60
+ const value = input.id;
61
+ if (typeof value !== "number" || !Number.isInteger(value))
62
+ throw new ScheduleError("id is required; schedule_list shows the ids");
63
+ return value;
64
+ }
65
+
66
+ function hasTiming(input: Input): boolean {
67
+ return [
68
+ "in_minutes",
69
+ "at",
70
+ "time",
71
+ "every_days",
72
+ "start_date",
73
+ "weekdays",
74
+ ].some((name) => input[name] !== undefined);
75
+ }
76
+
77
+ /** The recurrence and its first run, refusing a past or too distant one-time run. */
78
+ function timing(ctx: ScheduleToolContext, input: Input): [Recurrence, Date] {
79
+ const recurrence = parseRecurrence(input, ctx.now);
80
+ const next = nextRun(recurrence, ctx.now);
81
+ if (!next)
82
+ throw new ScheduleError(
83
+ `that time has already passed; it is ${zonedStamp(ctx.now)} in ${messages().zoneName(timeZone())} now`,
84
+ );
85
+ const { aheadDays } = SCHEDULE_LIMITS[ctx.mode];
86
+ if (next.getTime() - ctx.now.getTime() > aheadDays * DAY_MS)
87
+ throw new ScheduleError(`the first run must be within ${aheadDays} days`);
88
+ return [recurrence, next];
89
+ }
90
+
91
+ function line(schedule: Schedule): string {
92
+ const prompt =
93
+ schedule.prompt.length > LIST_PROMPT_PREVIEW
94
+ ? `${schedule.prompt.slice(0, LIST_PROMPT_PREVIEW)}…`
95
+ : schedule.prompt;
96
+ const last = schedule.lastRun
97
+ ? `; last run ${zonedStamp(schedule.lastRun)} (${schedule.lastStatus ?? "?"})`
98
+ : "";
99
+ return `- #${schedule.id} ${schedule.title}: ${describeRecurrence(schedule.recurrence)}, next ${zonedStamp(schedule.nextRun)}; set by ${schedule.createdByName}${last}\n ${prompt.replace(/\n/g, " ")}`;
100
+ }
101
+
102
+ async function own(ctx: ScheduleToolContext, input: Input): Promise<Schedule> {
103
+ const schedule = await ctx.store.get(id(input));
104
+ if (!schedule || schedule.channel !== ctx.channel)
105
+ throw new ScheduleError(
106
+ `this channel has no schedule #${String(input.id)}`,
107
+ );
108
+ return schedule;
109
+ }
110
+
111
+ /** A schedule runs at its creator's tier, so someone of a lower tier may not rewrite it. */
112
+ function changeable(ctx: ScheduleToolContext, schedule: Schedule): Schedule {
113
+ if (ctx.author.tier && !tierAtLeast(ctx.author.tier, schedule.createdTier))
114
+ throw new ScheduleError(
115
+ `schedule #${schedule.id} was set by a higher tier; only that tier or above can change it`,
116
+ );
117
+ return schedule;
118
+ }
119
+
120
+ /** Runs one schedule tool against the channel's schedules and returns the answer for the model. */
121
+ export async function callScheduleTool(
122
+ ctx: ScheduleToolContext,
123
+ name: ScheduleToolName,
124
+ input: Input,
125
+ ): Promise<string> {
126
+ const limits = SCHEDULE_LIMITS[ctx.mode];
127
+ switch (name) {
128
+ case "schedule_create": {
129
+ const title = text(input, "title", TITLE_CHARS);
130
+ const prompt = text(input, "prompt", limits.promptChars);
131
+ const [recurrence, next] = timing(ctx, input);
132
+ const existing = await ctx.store.forChannel(ctx.channel);
133
+ if (existing.length >= limits.perChannel)
134
+ throw new ScheduleError(
135
+ `this channel already has ${existing.length} schedules, the most it may have; cancel one first`,
136
+ );
137
+ const created = await ctx.store.create({
138
+ channel: ctx.channel,
139
+ mode: ctx.mode,
140
+ title,
141
+ prompt,
142
+ recurrence,
143
+ nextRun: next,
144
+ createdById: ctx.author.id,
145
+ createdByName: ctx.author.name,
146
+ createdTier: ctx.author.tier ?? "owner",
147
+ });
148
+ return `Scheduled #${created.id} "${title}": ${describeRecurrence(recurrence)}, first run ${zonedStamp(next)} ${messages().zoneTime(timeZone())}.`;
149
+ }
150
+ case "schedule_list": {
151
+ if (input.id !== undefined) {
152
+ const schedule = await own(ctx, input);
153
+ return `${line(schedule).split("\n")[0]}\n\nPrompt:\n${schedule.prompt}`;
154
+ }
155
+ const all = await ctx.store.forChannel(ctx.channel);
156
+ return all.length === 0
157
+ ? "This channel has no schedules."
158
+ : `It is ${zonedStamp(ctx.now)} in ${messages().zoneName(timeZone())}.\n${all.map(line).join("\n")}`;
159
+ }
160
+ case "schedule_update": {
161
+ const schedule = changeable(ctx, await own(ctx, input));
162
+ const change: Parameters<ScheduleToolContext["store"]["update"]>[2] = {};
163
+ if (input.title !== undefined)
164
+ change.title = text(input, "title", TITLE_CHARS);
165
+ if (input.prompt !== undefined)
166
+ change.prompt = text(input, "prompt", limits.promptChars);
167
+ if (hasTiming(input)) {
168
+ const [recurrence, next] = timing(ctx, input);
169
+ change.recurrence = recurrence;
170
+ change.nextRun = next;
171
+ }
172
+ if (Object.keys(change).length === 0)
173
+ throw new ScheduleError("give a title, prompt, or timing to change");
174
+ const updated = await ctx.store.update(ctx.channel, schedule.id, change);
175
+ if (!updated) throw new ScheduleError(`schedule #${schedule.id} is gone`);
176
+ return `Updated #${updated.id} "${updated.title}": ${describeRecurrence(updated.recurrence)}, next run ${zonedStamp(updated.nextRun)}.`;
177
+ }
178
+ case "schedule_cancel": {
179
+ const schedule = changeable(ctx, await own(ctx, input));
180
+ await ctx.store.remove(schedule.id, ctx.channel);
181
+ return `Cancelled #${schedule.id} "${schedule.title}".`;
182
+ }
183
+ default:
184
+ throw new ScheduleError("unknown schedule tool");
185
+ }
186
+ }
187
+
188
+ /** What a scheduled run receives as its message. */
189
+ export function scheduledTurnText(schedule: Schedule, firedAt: Date): string {
190
+ return [
191
+ `## Scheduled task #${schedule.id}: ${schedule.title}`,
192
+ `${schedule.createdByName} set this schedule (${describeRecurrence(schedule.recurrence)}). It is due now, ${zonedStamp(firedAt)} ${messages().zoneTime(timeZone())}. Nobody wrote a new message: carry out the task below and write what you would post in this channel. If the task keeps a record for later runs, update it with schedule_update on #${schedule.id}.`,
193
+ "",
194
+ schedule.prompt,
195
+ ].join("\n");
196
+ }