pi-roundtable 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +10 -6
  3. package/README.zh-TW.md +144 -0
  4. package/docs/plugins.md +1448 -66
  5. package/examples/echo-runtime.test.ts +122 -0
  6. package/examples/echo-runtime.ts +107 -0
  7. package/examples/events.test.ts +1 -0
  8. package/examples/events.ts +2 -2
  9. package/examples/fake-surface.test.ts +180 -0
  10. package/examples/fake-surface.ts +109 -0
  11. package/examples/interactions.test.ts +12 -3
  12. package/examples/interactions.ts +19 -19
  13. package/examples/shared-services.test.ts +74 -0
  14. package/examples/shared-services.ts +60 -0
  15. package/examples/study-room.test.ts +69 -0
  16. package/examples/study-room.ts +52 -0
  17. package/examples/support-desk.test.ts +34 -0
  18. package/examples/support-desk.ts +40 -0
  19. package/package.json +11 -3
  20. package/src/cli/checks/database.ts +11 -30
  21. package/src/cli/checks/images.ts +19 -0
  22. package/src/cli/doctor.ts +6 -0
  23. package/src/cli/main.ts +2 -1
  24. package/src/core/agents/agent-claim.ts +42 -16
  25. package/src/core/agents/agent-messages.ts +4 -4
  26. package/src/core/agents/agent-ports.ts +8 -21
  27. package/src/core/agents/agent-prompt.ts +3 -1
  28. package/src/core/agents/agent-store.ts +6 -4
  29. package/src/core/agents/agent-team-fixture.ts +389 -0
  30. package/src/core/agents/agent-team.ts +26 -25
  31. package/src/core/agents/agent-tools.ts +34 -15
  32. package/src/core/agents/avatar-studio.ts +40 -6
  33. package/src/core/agents/fallback-avatar.ts +85 -0
  34. package/src/core/agents/team-editing.ts +49 -10
  35. package/src/core/agents/team-keys.ts +22 -9
  36. package/src/core/agents/team-layout.ts +4 -3
  37. package/src/core/agents/team-lifecycle.ts +28 -2
  38. package/src/core/agents/team-options.ts +9 -5
  39. package/src/core/agents/team-status.ts +4 -4
  40. package/src/core/agents/team-text.ts +8 -6
  41. package/src/core/agents/team-turn-types.ts +4 -4
  42. package/src/core/agents/team-turns.ts +6 -5
  43. package/src/core/builtin/agent-server.ts +187 -121
  44. package/src/core/builtin/discord-admin.ts +41 -0
  45. package/src/core/builtin/discord.ts +65 -40
  46. package/src/core/builtin/modules.ts +67 -58
  47. package/src/core/builtin/session-tool.ts +24 -0
  48. package/src/core/builtin/skills.ts +72 -0
  49. package/src/core/builtin/stores.ts +55 -31
  50. package/src/core/config/config.ts +45 -4
  51. package/src/core/config/schema.ts +6 -0
  52. package/src/core/contract/channels.ts +54 -14
  53. package/src/core/contract/providers.ts +17 -0
  54. package/src/core/contract/runtime.ts +138 -0
  55. package/src/core/contract/services.ts +45 -0
  56. package/src/core/contract/surface.ts +88 -0
  57. package/src/core/db/migrations.ts +118 -3
  58. package/src/core/define-roundtable.ts +70 -46
  59. package/src/core/define.ts +2 -1
  60. package/src/core/discord/agent-commands.ts +39 -14
  61. package/src/core/discord/agent-panel.ts +66 -0
  62. package/src/core/discord/channel-executor.ts +6 -3
  63. package/src/core/discord/channel-operations.ts +14 -11
  64. package/src/core/discord/command-collection.ts +46 -0
  65. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  66. package/src/core/discord/connection.ts +35 -0
  67. package/src/core/discord/discord-surface.ts +32 -94
  68. package/src/core/discord/dispatch-threads.ts +2 -2
  69. package/src/core/discord/inbound-message.ts +99 -0
  70. package/src/core/discord/interaction-module.ts +57 -1
  71. package/src/core/discord/owner-cards.ts +4 -4
  72. package/src/core/discord/owner-command.ts +44 -11
  73. package/src/core/discord/owner-discord.ts +2 -2
  74. package/src/core/discord/schedule-commands.ts +21 -18
  75. package/src/core/discord/stop-button.ts +44 -1
  76. package/src/core/domain/attachment.ts +12 -8
  77. package/src/core/domain/conversation.ts +3 -4
  78. package/src/core/domain/owner-prompts.ts +5 -5
  79. package/src/core/domain/ports.ts +9 -53
  80. package/src/core/freeze.ts +9 -0
  81. package/src/core/host.ts +255 -82
  82. package/src/core/http/listeners.ts +55 -20
  83. package/src/core/i18n/agent-panel.ts +4 -0
  84. package/src/core/i18n/index.ts +12 -2
  85. package/src/core/i18n/owner.ts +7 -1
  86. package/src/core/i18n/schedules.ts +12 -6
  87. package/src/core/identity.ts +1 -1
  88. package/src/core/judging/effort-judge.ts +25 -3
  89. package/src/core/log.ts +21 -3
  90. package/src/core/models.ts +2 -2
  91. package/src/core/modules/background/background-turns.ts +6 -4
  92. package/src/core/modules/delegation/delegate.ts +3 -2
  93. package/src/core/modules/delegation/delegator.ts +15 -13
  94. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  95. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  96. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  97. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  98. package/src/core/modules/memory/owner-memory.ts +9 -13
  99. package/src/core/modules/schedules/recurrence.ts +5 -3
  100. package/src/core/modules/schedules/schedule-store.ts +11 -11
  101. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  102. package/src/core/modules/schedules/scheduler.ts +7 -2
  103. package/src/core/modules/schedules/schedules.ts +9 -3
  104. package/src/core/modules/skills/skill-registry.ts +3 -2
  105. package/src/core/modules/skills/skill-store.ts +2 -15
  106. package/src/core/modules/skills/skill-tools.ts +10 -4
  107. package/src/core/ops/error-reporter.ts +1 -1
  108. package/src/core/plugin.ts +232 -28
  109. package/src/core/registry/contributions.ts +173 -14
  110. package/src/core/registry/providers.ts +35 -5
  111. package/src/core/registry/services.ts +229 -0
  112. package/src/core/routing/channel-queue.ts +5 -0
  113. package/src/core/routing/channel-router.ts +21 -7
  114. package/src/core/routing/conversation-turns.ts +139 -0
  115. package/src/core/routing/message-text.ts +9 -2
  116. package/src/core/routing/surface-port.ts +39 -0
  117. package/src/core/runtime/conversation-sessions.ts +4 -3
  118. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  119. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  120. package/src/core/runtime/mcp.ts +1 -1
  121. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  122. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  123. package/src/core/runtime/prompt-slot.ts +8 -3
  124. package/src/core/runtime/runtime-types.ts +9 -35
  125. package/src/core/runtime/session-factory.ts +21 -5
  126. package/src/core/runtime/text-tools.ts +1 -3
  127. package/src/core/services.ts +238 -90
  128. package/src/core/sessions.ts +3 -2
  129. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  130. package/src/core/shared/delegate-tool.ts +4 -3
  131. package/src/core/shared/schedule-tools.ts +22 -13
  132. package/src/core/shared/session-messages.ts +1 -1
  133. package/src/core/speakers.ts +6 -38
  134. package/src/core/testing/database.ts +1 -7
  135. package/src/core/testing/eager-catalog.ts +30 -0
  136. package/src/core/testing/locale.ts +23 -5
  137. package/src/core/testing/modules.ts +105 -41
  138. package/src/core/testing/test-host.ts +279 -0
  139. package/src/core/testing/tool-set.ts +64 -0
  140. package/src/core/time.ts +6 -1
  141. package/src/core/tool-set.snapshot.json +288 -0
  142. package/src/core/tool-tiers.ts +17 -57
  143. package/src/discord/index.ts +64 -0
  144. package/src/index.ts +181 -10
  145. package/src/kit/channels.ts +14 -0
  146. package/src/kit/domain.ts +18 -0
  147. package/src/kit/holds.ts +4 -0
  148. package/src/kit/index.ts +129 -0
  149. package/src/kit/judging.ts +10 -0
  150. package/src/kit/memory.ts +3 -0
  151. package/src/kit/mirror.ts +19 -0
  152. package/src/kit/presentation.ts +7 -0
  153. package/src/kit/shell.ts +7 -0
  154. package/src/kit/skills.ts +8 -0
  155. package/src/kit/support.ts +21 -0
  156. package/src/kit/threads.ts +7 -0
  157. package/src/kit/tools.ts +14 -0
  158. package/src/kit/worker.ts +16 -0
  159. package/src/testing.ts +394 -34
  160. package/examples/guide.test.ts +0 -52
  161. package/src/cli/add-plugin.test.ts +0 -107
  162. package/src/cli/checks/basic.test.ts +0 -276
  163. package/src/cli/checks/database.test.ts +0 -117
  164. package/src/cli/checks/discord.test.ts +0 -200
  165. package/src/cli/cli.test.ts +0 -122
  166. package/src/cli/config-edit.test.ts +0 -130
  167. package/src/cli/doctor.test.ts +0 -184
  168. package/src/cli/init.test.ts +0 -127
  169. package/src/cli/size.test.ts +0 -9
  170. package/src/cli/templates.test.ts +0 -129
  171. package/src/cli/testing/fixtures.ts +0 -106
  172. package/src/core/agents/agent-claim.test.ts +0 -142
  173. package/src/core/agents/agent-dashboard.test.ts +0 -120
  174. package/src/core/agents/agent-guild.test.ts +0 -124
  175. package/src/core/agents/agent-store.test.ts +0 -205
  176. package/src/core/agents/avatar-studio.test.ts +0 -88
  177. package/src/core/agents/group-round.test.ts +0 -143
  178. package/src/core/agents/owner-identity.test.ts +0 -172
  179. package/src/core/agents/team-turns.test.ts +0 -278
  180. package/src/core/attachments/attachments.test.ts +0 -85
  181. package/src/core/boundary.test.ts +0 -45
  182. package/src/core/builtin/modules.test.ts +0 -127
  183. package/src/core/config/config.test.ts +0 -135
  184. package/src/core/contract/discord.ts +0 -33
  185. package/src/core/db/migrations.test.ts +0 -237
  186. package/src/core/define-roundtable.test.ts +0 -134
  187. package/src/core/define.test.ts +0 -144
  188. package/src/core/discord/agent-commands.test.ts +0 -58
  189. package/src/core/discord/agent-discord.test.ts +0 -70
  190. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  191. package/src/core/discord/dispatch-threads.test.ts +0 -94
  192. package/src/core/discord/owner-cards.test.ts +0 -435
  193. package/src/core/discord/owner-discord.test.ts +0 -325
  194. package/src/core/domain/expression.ts +0 -21
  195. package/src/core/domain/profile.ts +0 -32
  196. package/src/core/drain.test.ts +0 -46
  197. package/src/core/events.test.ts +0 -81
  198. package/src/core/holds.test.ts +0 -54
  199. package/src/core/host.test.ts +0 -536
  200. package/src/core/http/listeners.test.ts +0 -164
  201. package/src/core/i18n/i18n.test.ts +0 -139
  202. package/src/core/identity.test.ts +0 -18
  203. package/src/core/judging/effort-judge.test.ts +0 -112
  204. package/src/core/judging/model-judge.test.ts +0 -126
  205. package/src/core/modules/delegation/delegator.test.ts +0 -170
  206. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  207. package/src/core/modules/schedules/schedule.test.ts +0 -358
  208. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  209. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  210. package/src/core/ops/error-reporter.test.ts +0 -283
  211. package/src/core/presentation/card-cadence.ts +0 -41
  212. package/src/core/presentation/presentation.test.ts +0 -166
  213. package/src/core/public-entry.test.ts +0 -59
  214. package/src/core/registry/contributions.test.ts +0 -215
  215. package/src/core/registry/interactions.test.ts +0 -87
  216. package/src/core/registry/providers.test.ts +0 -103
  217. package/src/core/routing/channel-queue.test.ts +0 -31
  218. package/src/core/routing/channel-router.test.ts +0 -326
  219. package/src/core/routing/conversation-kind.ts +0 -14
  220. package/src/core/routing/settle-turn.test.ts +0 -21
  221. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  222. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  223. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  224. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  225. package/src/core/runtime/session-archive.test.ts +0 -20
  226. package/src/core/runtime/steerable-run.test.ts +0 -264
  227. package/src/core/runtime/text-tools.test.ts +0 -110
  228. package/src/core/runtime/turn-answer.test.ts +0 -72
  229. package/src/core/runtime/worker-task.test.ts +0 -82
  230. package/src/core/services.test.ts +0 -28
  231. package/src/core/sessions.test.ts +0 -85
  232. package/src/core/shared/unix-server.ts +0 -18
  233. package/src/core/size.test.ts +0 -9
  234. package/src/core/speakers.test.ts +0 -78
  235. package/src/core/testing/file-size.ts +0 -34
  236. package/src/core/time.test.ts +0 -166
  237. package/src/core/tool-tiers.test.ts +0 -117
  238. package/src/entries.test.ts +0 -112
  239. package/src/testing.test.ts +0 -156
@@ -1,31 +1,33 @@
1
1
  import { join } from "node:path";
2
2
  import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+ import type { SurfacePort } from "../contract/surface.ts";
3
4
  import { scheduleCommands } from "../discord/schedule-commands.ts";
4
5
  import type { ChannelKey } from "../domain/conversation.ts";
5
6
  import type { OwnerIdentity } from "../identity.ts";
6
7
  import type { ModelRef, ThinkingLevel } from "../models.ts";
7
- import { BackgroundTurns } from "../modules/background/background-turns.ts";
8
+ import { ConversationBackgroundTurns } from "../modules/background/background-turns.ts";
8
9
  import { delegateExtension } from "../modules/delegation/delegate.ts";
9
10
  import {
11
+ DefaultDelegator,
10
12
  type DelegationWorker,
11
- Delegator,
12
13
  } from "../modules/delegation/delegator.ts";
13
- import { SolWorker } from "../modules/delegation/sol-worker.ts";
14
- import { discordAdminExtension } from "../modules/discord-admin/discord-admin.ts";
15
- import { ownerMemoryExtension } from "../modules/memory/owner-memory.ts";
14
+ import { WebResearchWorker } from "../modules/delegation/web-research-worker.ts";
16
15
  import { notifyExtension } from "../modules/notify/notify.ts";
17
16
  import { Scheduler } from "../modules/schedules/scheduler.ts";
18
- import {
19
- type AgentChannelLookup,
20
- schedulesExtension,
21
- } from "../modules/schedules/schedules.ts";
17
+ import { schedulesExtension } from "../modules/schedules/schedules.ts";
22
18
  import type { RoundtablePlugin } from "../plugin.ts";
23
- import type {
24
- SessionContext,
25
- SessionTool,
26
- SessionToolSnapshot,
27
- } from "../sessions.ts";
28
- import { THE_SPEAKER } from "../speakers.ts";
19
+ import {
20
+ AGENTS,
21
+ BACKGROUND_TURNS,
22
+ DELEGATION,
23
+ SCHEDULES,
24
+ } from "../services.ts";
25
+ import type { SessionContext } from "../sessions.ts";
26
+ import { DELEGATE_TOOL } from "../shared/delegate-tool.ts";
27
+ import { SCHEDULE_TOOLS } from "../shared/schedule-tools.ts";
28
+ import { THE_SPEAKER, type Tier } from "../speakers.ts";
29
+ import { DISCORD } from "./discord.ts";
30
+ import { fixed } from "./session-tool.ts";
29
31
 
30
32
  export interface ModulesOptions {
31
33
  /** Who the tools serve in the owner's own sessions. */
@@ -42,58 +44,68 @@ export interface ModulesOptions {
42
44
  thinking: ThinkingLevel;
43
45
  worker?: DelegationWorker;
44
46
  };
45
- /** Another agent's channel, for reading its schedules; throws ScheduleError when unknown. */
46
- agentChannelOf?: AgentChannelLookup;
47
47
  }
48
48
 
49
- /** A session tool whose extension never changes. */
50
- function fixed(
51
- name: string,
52
- factory: SessionToolSnapshot["factory"],
53
- ): SessionTool {
54
- return { name, phase: "tools", snapshot: () => ({ revision: 0, factory }) };
55
- }
49
+ /**
50
+ * Admins schedule and delegate; every speaker may list schedules and use the web tools the
51
+ * delegated worker and pi-web-access give. Notifying the owner stays with the owner.
52
+ */
53
+ const MODULE_TIERS: Readonly<Record<string, Tier>> = {
54
+ ...Object.fromEntries(
55
+ [...SCHEDULE_TOOLS, DELEGATE_TOOL].map((tool) => [tool, "admin" as const]),
56
+ ),
57
+ schedule_list: "member",
58
+ web_search: "member",
59
+ fetch_content: "member",
60
+ get_search_content: "member",
61
+ };
56
62
 
57
63
  /**
58
- * The owner's modules: memory, notifications, schedules, delegated tasks, and Discord reading
59
- * and management, each a session tool, and the turns nobody wrote. The delegator's running jobs
60
- * join the shutdown drain.
64
+ * The owner's modules: notifications, schedules, and delegated tasks, each a session tool, and
65
+ * the turns nobody wrote. The delegator's running jobs join the shutdown drain.
61
66
  */
62
67
  export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
63
- const { owner, agentChannelOf } = options;
68
+ const { owner } = options;
64
69
  // Outside the agent server a conversation has no channel of its own to post a run in.
65
70
  const channelFor = async (
66
71
  channel: ChannelKey,
67
- surfaceOwnerChannel: () => Promise<ChannelKey>,
68
- ) => (channel.startsWith("discord:") ? channel : surfaceOwnerChannel());
72
+ surfaces: SurfacePort,
73
+ ownerChannel: () => Promise<ChannelKey>,
74
+ ) => (surfaces.of(channel) ? channel : ownerChannel());
69
75
  return {
70
76
  name: "modules",
71
- setup: ({ conversations, core, logger }) => {
72
- const { stores, discord } = core;
73
- const { surface } = discord;
77
+ provides: [BACKGROUND_TURNS, DELEGATION],
78
+ setup: ({ conversations, services, logger, surfaces }) => {
79
+ const schedules = services.get(SCHEDULES);
80
+ const discord = services.get(DISCORD);
81
+ const { connection } = discord;
82
+ // Another agent's channel, for schedule_list; asked when a tool runs, after the agent server set up.
83
+ const agentChannelOf = (agent: string) =>
84
+ services.get(AGENTS).team.channelOf(agent);
74
85
  const ownerChannelFor = (channel: ChannelKey) =>
75
- channelFor(channel, () => surface.ownerChannel());
76
- const background = new BackgroundTurns({
86
+ channelFor(channel, surfaces, () => connection.ownerChannel());
87
+ const background = new ConversationBackgroundTurns({
77
88
  conversations,
78
89
  system: { id: "assistant", name: options.assistant },
79
90
  logger,
80
91
  });
81
- const delegator = new Delegator({
92
+ const delegator = new DefaultDelegator({
82
93
  worker:
83
94
  options.delegation.worker ??
84
- new SolWorker({
95
+ new WebResearchWorker({
85
96
  modelRuntime: options.modelRuntime,
86
97
  agentDir: options.agentDir,
87
98
  workDir: join(options.dataDir, "delegate"),
88
99
  model: options.delegation.model,
89
100
  thinking: options.delegation.thinking,
90
101
  }),
102
+ targets: (name) => conversations.target(name),
91
103
  deliver: (job, outcome) => background.runDelegated(job, outcome),
92
104
  threads: discord.threads,
93
105
  logger,
94
106
  });
95
- core.provide("background", background);
96
- core.provide("delegator", delegator);
107
+ services.provide(BACKGROUND_TURNS, background);
108
+ services.provide(DELEGATION, delegator);
97
109
  // An agent session serves every speaker, so its tools name none.
98
110
  const served = (session: SessionContext) =>
99
111
  session.agent ? THE_SPEAKER : owner;
@@ -102,19 +114,11 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
102
114
  { name: "delegator", busy: () => delegator.runningChannels() },
103
115
  ],
104
116
  sessionTools: [
105
- fixed("owner-memory", (session) =>
106
- ownerMemoryExtension(
107
- stores.memory,
108
- served(session),
109
- // The owner's own chats have one speaker; the agent server's have several.
110
- session.agent ? session.speaker : undefined,
111
- ),
112
- ),
113
- fixed("notify", () => notifyExtension(surface, owner)),
117
+ fixed("notify", () => notifyExtension(connection, owner)),
114
118
  fixed("schedules", (session) =>
115
119
  schedulesExtension(
116
120
  {
117
- store: stores.schedules,
121
+ store: schedules,
118
122
  owner: { id: owner.id, name: owner.name },
119
123
  channelFor: ownerChannelFor,
120
124
  },
@@ -137,10 +141,8 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
137
141
  session.speaker,
138
142
  ),
139
143
  ),
140
- fixed("discord-admin", () =>
141
- discordAdminExtension(surface.ownerDiscord(), owner),
142
- ),
143
144
  ],
145
+ toolTiers: MODULE_TIERS,
144
146
  };
145
147
  },
146
148
  };
@@ -153,12 +155,22 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
153
155
  export function schedulerPlugin(): RoundtablePlugin {
154
156
  return {
155
157
  name: "schedules",
156
- setup: ({ core, logger }) => {
158
+ setup: ({ conversations, services, env, logger }) => {
159
+ const schedules = services.get(SCHEDULES);
157
160
  const scheduler = new Scheduler({
158
- store: core.stores.schedules,
159
- runner: core.background,
161
+ store: schedules,
162
+ runner: services.get(BACKGROUND_TURNS),
160
163
  logger,
161
164
  });
165
+ // Without Discord there are no schedule commands; the scheduler still fires.
166
+ const discord = services.find(DISCORD);
167
+ discord?.commands.add(
168
+ scheduleCommands(
169
+ discord.guard,
170
+ schedules,
171
+ (target) => conversations.target(target)?.label(env.locale) ?? target,
172
+ ),
173
+ );
162
174
  return {
163
175
  services: [
164
176
  {
@@ -167,9 +179,6 @@ export function schedulerPlugin(): RoundtablePlugin {
167
179
  stop: () => scheduler.stop(),
168
180
  },
169
181
  ],
170
- interactions: [
171
- scheduleCommands(core.discord.guard, core.stores.schedules),
172
- ],
173
182
  };
174
183
  },
175
184
  };
@@ -0,0 +1,24 @@
1
+ import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ AgentTurnScope,
4
+ SessionTool,
5
+ SessionToolSnapshot,
6
+ } from "../sessions.ts";
7
+
8
+ /** A session tool whose extension never changes. */
9
+ export function fixed(
10
+ name: string,
11
+ factory: SessionToolSnapshot["factory"],
12
+ ): SessionTool {
13
+ return { name, phase: "tools", snapshot: () => ({ revision: 0, factory }) };
14
+ }
15
+
16
+ /** A session tool whose extension never changes, for agent sessions only. */
17
+ export function agentOnly(
18
+ name: string,
19
+ factory: (scope: AgentTurnScope) => ExtensionFactory,
20
+ ): SessionTool {
21
+ return fixed(name, (session) =>
22
+ session.agent ? factory(session.agent) : null,
23
+ );
24
+ }
@@ -0,0 +1,72 @@
1
+ import type { SQL } from "bun";
2
+ import { StoredSkillRegistry } from "../modules/skills/skill-registry.ts";
3
+ import { SkillStore } from "../modules/skills/skill-store.ts";
4
+ import {
5
+ SKILL_LIST_TOOL,
6
+ SKILL_TOOLS,
7
+ skillToolsExtension,
8
+ } from "../modules/skills/skill-tools.ts";
9
+ import type { RoundtablePlugin } from "../plugin.ts";
10
+ import { AGENTS, SKILLS } from "../services.ts";
11
+ import type { Tier } from "../speakers.ts";
12
+ import { agentOnly } from "./session-tool.ts";
13
+
14
+ export interface SkillsOptions {
15
+ /** The guild whose agents' skills the store keeps. */
16
+ guildId: string;
17
+ /** Where the registry reads and writes skills. */
18
+ reposDir: string;
19
+ writtenDir: string;
20
+ builtinDir: string;
21
+ }
22
+
23
+ /** Admins manage skills; every speaker may look one up. */
24
+ const SKILL_TIERS: Readonly<Record<string, Tier>> = {
25
+ ...Object.fromEntries(SKILL_TOOLS.map((tool) => [tool, "admin" as const])),
26
+ [SKILL_LIST_TOOL]: "member",
27
+ };
28
+
29
+ /**
30
+ * The skills agents carry: the tables, the registry provided as `SKILLS`, and the skill tools of
31
+ * agent sessions. An addon; `skills: false` leaves it out, and agents carry none.
32
+ */
33
+ export function skillsPlugin(
34
+ options: SkillsOptions,
35
+ /** Opens the store; a test hands a stand-in, so the plugin can be set up without a database. */
36
+ openStore: (sql: SQL, guildId: string) => Promise<SkillStore> = (
37
+ sql,
38
+ guildId,
39
+ ) => SkillStore.attach(sql, guildId),
40
+ ): RoundtablePlugin {
41
+ let registry: StoredSkillRegistry | undefined;
42
+ return {
43
+ name: "skills",
44
+ migrations: SkillStore.migrations(options.guildId),
45
+ provides: [SKILLS],
46
+ // The skills are read once every plugin is set up, so a plugin may still place some in setup.
47
+ preflight: () => registry?.init(),
48
+ setup: async ({ database, services, logger }) => {
49
+ const built = new StoredSkillRegistry({
50
+ store: await openStore(database(), options.guildId),
51
+ reposDir: options.reposDir,
52
+ writtenDir: options.writtenDir,
53
+ builtinDir: options.builtinDir,
54
+ logger,
55
+ });
56
+ registry = built;
57
+ services.provide(SKILLS, built);
58
+ return {
59
+ sessionTools: [
60
+ agentOnly("skill-tools", () =>
61
+ skillToolsExtension(built, (name) => {
62
+ // The agent server is set up after this plugin and read when a session is built.
63
+ services.get(AGENTS).directory.activeAgent(name);
64
+ }),
65
+ ),
66
+ ],
67
+ agentSelection: () => ({ tools: SKILL_TOOLS, groups: [] }),
68
+ toolTiers: SKILL_TIERS,
69
+ };
70
+ },
71
+ };
72
+ }
@@ -1,41 +1,65 @@
1
- import { AgentStore } from "../agents/agent-store.ts";
2
- import { OwnerMemoryStore } from "../modules/memory/owner-memory-store.ts";
3
- import { ScheduleStore } from "../modules/schedules/schedule-store.ts";
4
- import { SkillStore } from "../modules/skills/skill-store.ts";
1
+ import type { OwnerIdentity } from "../identity.ts";
2
+ import { ownerMemoryExtension } from "../modules/memory/owner-memory.ts";
3
+ import { PgMemoryStore } from "../modules/memory/owner-memory-store.ts";
4
+ import { PgScheduleStore } from "../modules/schedules/schedule-store.ts";
5
5
  import type { RoundtablePlugin } from "../plugin.ts";
6
- import { PendingConfirmationStore } from "../runtime/pending-confirmation-store.ts";
6
+ import { MEMORY, SCHEDULES } from "../services.ts";
7
+ import { THE_SPEAKER, type Tier } from "../speakers.ts";
8
+ import { fixed } from "./session-tool.ts";
7
9
 
8
- export interface StoresOptions {
9
- /** The owner's Discord id: the speaker whose memory the first rows belong to. */
10
- ownerId: string;
11
- /** The agent server's guild; every agent-server row carries it. */
12
- guildId: string;
10
+ export interface MemoryOptions {
11
+ /** The owner: whose memory the first rows belong to, and whom the tools serve in the owner's own sessions. */
12
+ owner: OwnerIdentity & { id: string };
13
13
  }
14
14
 
15
+ /** Each speaker keeps a memory of their own, so every tier may use the tools; the owner's reaches only the owner's turns. */
16
+ const MEMORY_TIERS: Readonly<Record<string, Tier>> = {
17
+ memory_add: "member",
18
+ memory_search: "member",
19
+ memory_remove: "member",
20
+ };
21
+
15
22
  /**
16
- * The core's own tables and the stores over them. The host migrates them over its one pool
17
- * before any setup, in this order, and closes the pool after every service has stopped.
23
+ * The remembered facts of each speaker: the table, the store over it provided as `MEMORY`, and
24
+ * the memory tools and prompt block of every conversation. An addon; `memory: false` leaves it
25
+ * out. The host migrates the table over its one pool before any setup.
18
26
  */
19
- export function storesPlugin(options: StoresOptions): RoundtablePlugin {
20
- const { ownerId, guildId } = options;
27
+ export function memoryPlugin(options: MemoryOptions): RoundtablePlugin {
28
+ const { owner } = options;
29
+ return {
30
+ name: "memory",
31
+ migrations: PgMemoryStore.migrations(owner.id),
32
+ provides: [MEMORY],
33
+ setup: ({ database, services }) => {
34
+ const memory = PgMemoryStore.attach(database(), owner.id);
35
+ services.provide(MEMORY, memory);
36
+ return {
37
+ sessionTools: [
38
+ fixed("owner-memory", (session) =>
39
+ ownerMemoryExtension(
40
+ memory,
41
+ owner.id,
42
+ // An agent session serves every speaker, so its tools name none.
43
+ session.agent ? THE_SPEAKER : owner,
44
+ // The owner's own chats have one speaker; the agent server's have several.
45
+ session.agent ? session.speaker : undefined,
46
+ ),
47
+ ),
48
+ ],
49
+ toolTiers: MEMORY_TIERS,
50
+ };
51
+ },
52
+ };
53
+ }
54
+
55
+ /** The scheduled turns' table, and the store over it provided as `SCHEDULES`. */
56
+ export function scheduleStorePlugin(): RoundtablePlugin {
21
57
  return {
22
- name: "stores",
23
- migrations: [
24
- ...OwnerMemoryStore.migrations(ownerId),
25
- ScheduleStore.migration,
26
- PendingConfirmationStore.migration,
27
- ...AgentStore.migrations(guildId),
28
- ...SkillStore.migrations(guildId),
29
- ],
30
- setup: async ({ database, core }) => {
31
- const sql = database();
32
- core.provide("stores", {
33
- memory: OwnerMemoryStore.attach(sql, ownerId),
34
- schedules: await ScheduleStore.attach(sql),
35
- confirmations: await PendingConfirmationStore.attach(sql),
36
- agents: await AgentStore.attach(sql, guildId),
37
- skills: await SkillStore.attach(sql, guildId),
38
- });
58
+ name: "schedule-store",
59
+ migrations: [PgScheduleStore.migration],
60
+ provides: [SCHEDULES],
61
+ setup: async ({ database, services }) => {
62
+ services.provide(SCHEDULES, await PgScheduleStore.attach(database()));
39
63
  return {};
40
64
  },
41
65
  };
@@ -18,6 +18,7 @@ import {
18
18
  number,
19
19
  oneOf,
20
20
  optional,
21
+ orOff,
21
22
  record,
22
23
  shape,
23
24
  text,
@@ -48,6 +49,17 @@ export interface RoundtableConfig {
48
49
  entryChannel: string;
49
50
  /** The root slash command, without the slash; default the lowercase assistant name. */
50
51
  rootCommand?: string;
52
+ /**
53
+ * Whether the owner's conversations have Discord administration tools; default true. `false`
54
+ * leaves the `discord-admin` addon out.
55
+ */
56
+ admin?: boolean;
57
+ /**
58
+ * Text appended as it is to the refusal a non-owner gets from the root command, such as a
59
+ * pointer to the commands anyone may use; include the space or punctuation your language
60
+ * needs before it. Default none.
61
+ */
62
+ refusalHint?: string;
51
63
  };
52
64
  database: { url: string };
53
65
  /** Where the process keeps its files, such as pictures, attachments, and skills. */
@@ -74,19 +86,31 @@ export interface RoundtableConfig {
74
86
  agents?: AgentSeed[];
75
87
  /**
76
88
  * Where the agents' avatars are served: the public address that reaches it, and the TCP port
77
- * (default 3000) or unix socket the process listens on.
89
+ * (default 3000) or unix socket the process listens on. `socketMode` is the socket file's
90
+ * permission bits (default `0o660`); widen it only for a proxy that runs as another user.
78
91
  */
79
92
  http: {
80
93
  publicUrl: string;
81
94
  port?: number;
82
95
  hostname?: string;
83
96
  socketPath?: string;
97
+ socketMode?: number;
84
98
  };
85
99
  /** A picture of your own for agents without one, and the style reference for drawing them; default a plain bot. */
86
100
  avatar?: string;
87
101
  /** The agents' shared working directory; default `<dataDir>/work`. */
88
102
  workDir?: string;
89
- skills?: { builtinDir?: string; reposDir?: string };
103
+ /**
104
+ * Where the skill registry reads built-in skills and keeps linked repositories. `false` leaves
105
+ * the `skills` addon out: agents carry no skills, and no skill tools exist. Stored skills stay
106
+ * in their tables.
107
+ */
108
+ skills?: false | { builtinDir?: string; reposDir?: string };
109
+ /**
110
+ * Whether each speaker's memory is kept and offered as tools; default true. `false` leaves the
111
+ * `memory` addon out: no memory tools, no memory prompt block, and the table stays as it is.
112
+ */
113
+ memory?: boolean;
90
114
  /** The agent that investigates the process's own errors, by name. */
91
115
  ops?: { agent: string };
92
116
  plugins?: RoundtablePlugin[];
@@ -116,6 +140,8 @@ const schema = shape({
116
140
  guild: text,
117
141
  entryChannel: text,
118
142
  rootCommand: optional(text),
143
+ admin: optional(bool),
144
+ refusalHint: optional(text),
119
145
  }),
120
146
  database: shape({ url: text }),
121
147
  dataDir: text,
@@ -158,12 +184,14 @@ const schema = shape({
158
184
  port: optional(integer(1, 65535)),
159
185
  hostname: optional(text),
160
186
  socketPath: optional(text),
187
+ socketMode: optional(integer(0, 0o777)),
161
188
  }),
162
189
  avatar: optional(text),
163
190
  workDir: optional(text),
164
191
  skills: optional(
165
- shape({ builtinDir: optional(text), reposDir: optional(text) }),
192
+ orOff(shape({ builtinDir: optional(text), reposDir: optional(text) })),
166
193
  ),
194
+ memory: optional(bool),
167
195
  ops: optional(shape({ agent: text })),
168
196
  plugins: optional(
169
197
  list(
@@ -187,6 +215,8 @@ export interface ResolvedConfig {
187
215
  guild: string;
188
216
  entryChannel: string;
189
217
  rootCommand: string;
218
+ admin: boolean;
219
+ refusalHint?: string;
190
220
  };
191
221
  databaseUrl: string;
192
222
  dataDir: string;
@@ -206,10 +236,13 @@ export interface ResolvedConfig {
206
236
  port: number;
207
237
  hostname?: string;
208
238
  socketPath?: string;
239
+ socketMode?: number;
209
240
  };
210
241
  avatar?: string;
211
242
  workDir: string;
212
- skills: { builtinDir?: string; reposDir?: string };
243
+ /** `false` when the skills addon is off. */
244
+ skills: false | { builtinDir?: string; reposDir?: string };
245
+ memory: boolean;
213
246
  ops?: { agent: string };
214
247
  plugins: RoundtablePlugin[];
215
248
  }
@@ -243,6 +276,10 @@ export function resolveConfig(input: unknown): ResolvedConfig {
243
276
  rootCommand:
244
277
  config.discord.rootCommand ??
245
278
  name.toLowerCase().replace(/[^a-z0-9-]+/g, "-"),
279
+ admin: config.discord.admin ?? true,
280
+ ...(config.discord.refusalHint === undefined
281
+ ? {}
282
+ : { refusalHint: config.discord.refusalHint }),
246
283
  },
247
284
  databaseUrl: config.database.url,
248
285
  dataDir: config.dataDir,
@@ -275,10 +312,14 @@ export function resolveConfig(input: unknown): ResolvedConfig {
275
312
  port: config.http.port ?? 3000,
276
313
  ...(config.http.hostname ? { hostname: config.http.hostname } : {}),
277
314
  ...(config.http.socketPath ? { socketPath: config.http.socketPath } : {}),
315
+ ...(config.http.socketMode === undefined
316
+ ? {}
317
+ : { socketMode: config.http.socketMode }),
278
318
  },
279
319
  ...(config.avatar ? { avatar: config.avatar } : {}),
280
320
  workDir: config.workDir ?? `${config.dataDir}/work`,
281
321
  skills: config.skills ?? {},
322
+ memory: config.memory ?? true,
282
323
  ...(config.ops ? { ops: config.ops } : {}),
283
324
  plugins: config.plugins ?? [],
284
325
  };
@@ -115,6 +115,12 @@ export const guarded = <T>(
115
115
  },
116
116
  });
117
117
 
118
+ /** `false`, or whatever `item` accepts: a feature that is switched off, or configured. */
119
+ export const orOff = <T>(item: Field<T>): Field<T | false> => ({
120
+ describe: `false, or ${item.describe}`,
121
+ check: (value, path) => (value === false ? false : item.check(value, path)),
122
+ });
123
+
118
124
  /** A field that may be left out. */
119
125
  export type Optional<T> = { optional: Field<T> };
120
126