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,205 @@
1
+ import { AgentError } from "../domain/errors.ts";
2
+ import type { AgentTurnScope } from "../domain/ports.ts";
3
+ import { assistantName } from "../i18n/index.ts";
4
+ import { addressee } from "../speakers.ts";
5
+ import { openingTaskText } from "./agent-prompt.ts";
6
+ import { modelSetting, thinkingSetting } from "./agent-settings.ts";
7
+ import type { Agent } from "./agent-store.ts";
8
+ import type { AvatarMode } from "./agent-tools.ts";
9
+ import { channelKey } from "./team-keys.ts";
10
+ import { groupTopic, teamCategory } from "./team-layout.ts";
11
+ import type { TeamContext } from "./team-options.ts";
12
+
13
+ /** Creating and changing agents, their avatars, and groups. */
14
+ export class TeamEditing {
15
+ readonly #ctx: TeamContext;
16
+
17
+ constructor(ctx: TeamContext) {
18
+ this.#ctx = ctx;
19
+ }
20
+
21
+ /**
22
+ * Changes an agent's display name, prompt, model, or thinking level; `default` returns the
23
+ * model or thinking level to the assistant's. A model must be one the host runs.
24
+ */
25
+ async update(
26
+ name: string,
27
+ change: {
28
+ displayName?: string;
29
+ prompt?: string;
30
+ model?: string;
31
+ thinking?: string;
32
+ },
33
+ ): Promise<string> {
34
+ const { store, logger } = this.#ctx.options;
35
+ if (Object.values(change).every((value) => value === undefined))
36
+ throw new AgentError(
37
+ "Give display_name, prompt, model, or thinking to change.",
38
+ );
39
+ const model = await modelSetting(change.model, () =>
40
+ this.#ctx.options.models.usable(),
41
+ );
42
+ const thinking = thinkingSetting(change.thinking);
43
+ const agent = await store.updateAgent(name, {
44
+ ...(change.displayName !== undefined
45
+ ? { displayName: change.displayName }
46
+ : {}),
47
+ ...(change.prompt !== undefined ? { prompt: change.prompt } : {}),
48
+ ...(model !== undefined ? { model } : {}),
49
+ ...(thinking !== undefined ? { thinking } : {}),
50
+ });
51
+ logger.info(
52
+ {
53
+ agent: name,
54
+ fields: Object.keys(change).filter(
55
+ (key) => change[key as keyof typeof change] !== undefined,
56
+ ),
57
+ ...(model !== undefined ? { model } : {}),
58
+ ...(thinking !== undefined ? { thinking } : {}),
59
+ },
60
+ "agent updated",
61
+ );
62
+ if (change.displayName !== undefined)
63
+ for (const group of store.groups())
64
+ if (group.status === "active" && group.members.includes(name))
65
+ this.#ctx.retitle(group);
66
+ this.#ctx.changed();
67
+ const { model: runs, thinking: level } = this.#ctx.modelOf(name);
68
+ return `Updated "${agent.displayName}" (${agent.name}); it applies from its next turn. It runs ${runs} with thinking ${level}.`;
69
+ }
70
+
71
+ async create(
72
+ caller: AgentTurnScope,
73
+ input: {
74
+ name: string;
75
+ displayName: string;
76
+ prompt: string;
77
+ avatarPrompt: string;
78
+ task: string;
79
+ category?: string;
80
+ skills?: string[];
81
+ },
82
+ ): Promise<string> {
83
+ const speaker = this.#ctx.turns.speakerOf(caller);
84
+ if (!speaker)
85
+ throw new AgentError(
86
+ "agent_create can only be used during an agent turn.",
87
+ );
88
+ const { store, channels, queue, skills, logger } = this.#ctx.options;
89
+ const { category, skills: carried = [], ...fields } = input;
90
+ const parent = teamCategory(category, "Agents");
91
+ store.checkNewName(input.name);
92
+ skills.checkRegistered(carried);
93
+ const channelId = await channels.createChannel(
94
+ input.name,
95
+ input.displayName,
96
+ parent,
97
+ );
98
+ const agent = await store.createAgent({ ...fields, channelId });
99
+ if (carried.length > 0) await skills.attach(agent.name, carried, []);
100
+ logger.info({ agent: agent.name, by: caller.name }, "agent created");
101
+ this.#ctx.changed();
102
+ const avatar = await this.#redraw(agent.name, "redraw").then(
103
+ () => "its avatar is drawn",
104
+ (error: unknown) =>
105
+ `its avatar could not be drawn (${error instanceof Error ? error.message : String(error)}), so it uses ${assistantName()}'s for now`,
106
+ );
107
+ const home = channelKey(channelId);
108
+ const creator = store.agent(caller.name);
109
+ void queue
110
+ .run(home, () =>
111
+ this.#ctx.turns.answerBackground(
112
+ home,
113
+ speaker,
114
+ openingTaskText(
115
+ creator,
116
+ input.task,
117
+ addressee(speaker, this.#ctx.options.owner),
118
+ ),
119
+ ),
120
+ )
121
+ .catch((error: unknown) =>
122
+ logger.error({ agent: agent.name, err: error }, "opening turn failed"),
123
+ );
124
+ return `Created "${agent.displayName}" (${agent.name}) in <#${channelId}>; ${avatar}. It is starting on its opening task now.`;
125
+ }
126
+
127
+ async avatar(name: string, mode: AvatarMode, text?: string): Promise<string> {
128
+ await this.#redraw(name, mode, text);
129
+ return `The new avatar of "${this.#ctx.options.store.agent(name)?.displayName ?? name}" shows from its next message.`;
130
+ }
131
+
132
+ /** Draws and stores a new picture; the owner's panel and the agent tool both use it. */
133
+ async redrawAvatar(
134
+ name: string,
135
+ mode: AvatarMode,
136
+ text?: string,
137
+ ): Promise<Agent> {
138
+ return this.#redraw(name, mode, text);
139
+ }
140
+
141
+ async #redraw(name: string, mode: AvatarMode, text?: string): Promise<Agent> {
142
+ const { store, studio, logger } = this.#ctx.options;
143
+ const agent = store.activeAgent(name);
144
+ if (mode !== "redraw" && !text?.trim())
145
+ throw new AgentError(
146
+ mode === "edit"
147
+ ? "Give the edit instruction as text."
148
+ : "Give the new avatar prompt as text.",
149
+ );
150
+ const avatarPrompt =
151
+ mode === "new_prompt" ? (text as string) : agent.avatarPrompt;
152
+ if (mode === "new_prompt") await store.updateAgent(name, { avatarPrompt });
153
+ let hash: string;
154
+ try {
155
+ hash =
156
+ mode === "edit"
157
+ ? await studio.edit(agent.avatarHash, text as string)
158
+ : await studio.draw(avatarPrompt);
159
+ } catch (error) {
160
+ logger.warn({ agent: name, mode, err: error }, "avatar drawing failed");
161
+ throw new AgentError(
162
+ `The avatar could not be drawn: ${error instanceof Error ? error.message : String(error)}. The previous picture stays.`,
163
+ );
164
+ }
165
+ logger.info({ agent: name, mode }, "avatar redrawn");
166
+ return store.updateAgent(name, { avatarHash: hash });
167
+ }
168
+
169
+ async createGroup(input: {
170
+ name: string;
171
+ displayName: string;
172
+ members: string[];
173
+ host?: string;
174
+ category?: string;
175
+ }): Promise<string> {
176
+ const { store, channels, logger } = this.#ctx.options;
177
+ const { category, ...fields } = input;
178
+ const parent = teamCategory(category, "Groups");
179
+ const host =
180
+ input.host ??
181
+ (input.members.includes("coordinator")
182
+ ? "coordinator"
183
+ : (input.members[0] ?? ""));
184
+ store.checkNewGroup(input.name, input.members, host);
185
+ const topic = groupTopic(
186
+ { ...fields, host, channelId: "", status: "active" },
187
+ store,
188
+ );
189
+ const channelId = await channels.createChannel(input.name, topic, parent);
190
+ const group = await store.createGroup({ ...fields, host, channelId });
191
+ logger.info({ group: group.name, members: group.members }, "group created");
192
+ this.#ctx.changed();
193
+ return `Created the group "${group.displayName}" in <#${channelId}> with ${group.members.join(", ")}; host ${group.host}.`;
194
+ }
195
+
196
+ async updateGroup(
197
+ name: string,
198
+ change: { displayName?: string; members?: string[]; host?: string },
199
+ ): Promise<string> {
200
+ const group = await this.#ctx.options.store.updateGroup(name, change);
201
+ this.#ctx.retitle(group);
202
+ this.#ctx.changed();
203
+ return `Updated the group "${group.displayName}": members ${group.members.join(", ")}; host ${group.host}.`;
204
+ }
205
+ }
@@ -0,0 +1,40 @@
1
+ import type { ChannelKey } from "../domain/conversation.ts";
2
+ import type { AgentTurnScope } from "../domain/ports.ts";
3
+ import type { Agent, AgentGroup, AgentStore } from "./agent-store.ts";
4
+
5
+ export const channelKey = (channelId: string): ChannelKey =>
6
+ `discord:${channelId}`;
7
+
8
+ export const channelIdOf = (channel: ChannelKey) =>
9
+ channel.slice("discord:".length);
10
+
11
+ /** An agent's conversation inside a group, apart from its own. */
12
+ export const groupSessionKey = (group: AgentGroup, agent: string): ChannelKey =>
13
+ `agentgroup:${group.channelId}.${agent}`;
14
+
15
+ /** An agent's own conversation. */
16
+ export function homeScope(agent: Agent): AgentTurnScope {
17
+ const home = channelKey(agent.channelId ?? "");
18
+ return { name: agent.name, session: home, home };
19
+ }
20
+
21
+ /** An agent's conversation inside a group. */
22
+ export function groupScope(agent: Agent, group: AgentGroup): AgentTurnScope {
23
+ return {
24
+ name: agent.name,
25
+ session: groupSessionKey(group, agent.name),
26
+ home: channelKey(agent.channelId ?? ""),
27
+ group: group.name,
28
+ };
29
+ }
30
+
31
+ /** The agent or group that owns a channel of the agent server. */
32
+ export function channelOwner(
33
+ store: Pick<AgentStore, "agentByChannel" | "groupByChannel">,
34
+ channel: ChannelKey,
35
+ ): "agent" | "group" | undefined {
36
+ const id = channelIdOf(channel);
37
+ if (store.agentByChannel(id)) return "agent";
38
+ if (store.groupByChannel(id)) return "group";
39
+ return undefined;
40
+ }
@@ -0,0 +1,135 @@
1
+ import { AgentError } from "../domain/errors.ts";
2
+ import { messages } from "../i18n/index.ts";
3
+ import type { CategoryLayout } from "./agent-ports.ts";
4
+ import type { AgentGroup, AgentStore } from "./agent-store.ts";
5
+
6
+ type TeamStore = Pick<
7
+ AgentStore,
8
+ "agent" | "group" | "agentByChannel" | "groupByChannel"
9
+ >;
10
+
11
+ /** The two kinds of team category (spec behavior 56). */
12
+ export type CategoryKind = "Agents" | "Groups";
13
+ const TEAM_CATEGORY = /^(Agents|Groups)(?:-[^\r\n]{1,32})?$/u;
14
+
15
+ /** A team category's kind, or undefined for any other category. */
16
+ export function categoryKind(name: string): CategoryKind | undefined {
17
+ return TEAM_CATEGORY.exec(name)?.[1] as CategoryKind | undefined;
18
+ }
19
+
20
+ /** The category a tool names for a channel of this kind, the plain one when absent. */
21
+ export function teamCategory(
22
+ name: string | undefined,
23
+ kind: CategoryKind,
24
+ ): string {
25
+ if (name === undefined) return kind;
26
+ const found = categoryKind(name);
27
+ if (!found)
28
+ throw new AgentError(
29
+ `"${name}" is not a team category: use ${kind} or ${kind}-<suffix>, the suffix 1 to 32 characters on one line.`,
30
+ );
31
+ if (found !== kind)
32
+ throw new AgentError(
33
+ `"${name}" holds ${found === "Agents" ? "agents" : "groups"}; ${kind === "Agents" ? "an agent" : "a group"} goes under ${kind} or ${kind}-<suffix>.`,
34
+ );
35
+ return name;
36
+ }
37
+
38
+ /** A group channel's topic: who is in it and who hosts. */
39
+ export function groupTopic(
40
+ group: AgentGroup,
41
+ store: Pick<AgentStore, "agent">,
42
+ ): string {
43
+ const shown = (name: string) => store.agent(name)?.displayName ?? name;
44
+ return messages().groupTopic(
45
+ group.displayName,
46
+ group.members.map(shown),
47
+ shown(group.host),
48
+ );
49
+ }
50
+
51
+ /** The team categories in server order, each with its channels by agent or group name. */
52
+ export function layoutText(
53
+ layout: readonly CategoryLayout[],
54
+ store: TeamStore,
55
+ ): string {
56
+ const lines = layout
57
+ .filter((c) => categoryKind(c.name))
58
+ .map((c) => {
59
+ const shown = c.channelIds.map(
60
+ (id) =>
61
+ store.agentByChannel(id)?.name ??
62
+ store.groupByChannel(id)?.name ??
63
+ `<#${id}>`,
64
+ );
65
+ return `- ${c.name}: ${shown.join(", ") || "empty"}`;
66
+ });
67
+ return `Categories, in server order:\n${lines.join("\n") || "none"}`;
68
+ }
69
+
70
+ /** An arrange request's categories with the channels of the named agents and groups. */
71
+ export function resolveArrangement(
72
+ entries: readonly { category: string; names: string[] }[],
73
+ store: TeamStore,
74
+ ): { category: string; channelIds: string[] }[] {
75
+ if (entries.length === 0) throw new AgentError("List at least one category.");
76
+ const categories = new Set<string>();
77
+ const names = new Set<string>();
78
+ return entries.map(({ category, names: members }) => {
79
+ if (categories.has(category))
80
+ throw new AgentError(`The category "${category}" is listed twice.`);
81
+ categories.add(category);
82
+ if (members.length === 0)
83
+ throw new AgentError(
84
+ `List at least one agent or group under "${category}".`,
85
+ );
86
+ const channelIds = members.map((name) => {
87
+ if (names.has(name)) throw new AgentError(`"${name}" is listed twice.`);
88
+ names.add(name);
89
+ const agent = store.agent(name);
90
+ const target = agent ?? store.group(name);
91
+ if (!target)
92
+ throw new AgentError(
93
+ `There is no agent or group "${name}"; see agent_list.`,
94
+ );
95
+ if (target.status !== "active")
96
+ throw new AgentError(`"${name}" is archived.`);
97
+ teamCategory(category, agent ? "Agents" : "Groups");
98
+ if (!target.channelId) throw new AgentError(`${name} has no channel.`);
99
+ return target.channelId;
100
+ });
101
+ return { category, channelIds };
102
+ });
103
+ }
104
+
105
+ /**
106
+ * The server layout after an arrangement: the listed categories and channels first, everything
107
+ * else after in its current order; and the team categories left empty, to delete.
108
+ */
109
+ export function planArrangement(
110
+ listed: readonly { category: string; channelIds: string[] }[],
111
+ current: readonly CategoryLayout[],
112
+ ): { layout: CategoryLayout[]; remove: string[] } {
113
+ const moved = new Set(listed.flatMap((l) => l.channelIds));
114
+ const stay = (c: CategoryLayout) =>
115
+ c.channelIds.filter((id) => !moved.has(id));
116
+ const ahead: CategoryLayout[] = listed.map(({ category, channelIds }) => {
117
+ const existing = current.find((c) => c.name === category);
118
+ return {
119
+ ...(existing?.id ? { id: existing.id } : {}),
120
+ name: category,
121
+ channelIds: [...channelIds, ...(existing ? stay(existing) : [])],
122
+ };
123
+ });
124
+ const rest = current
125
+ .filter((c) => !ahead.some((a) => a.id === c.id))
126
+ .map((c) => ({ ...c, channelIds: stay(c) }));
127
+ const layout = [...ahead, ...rest];
128
+ const remove = layout.flatMap((c) =>
129
+ c.id && c.channelIds.length === 0 && categoryKind(c.name) ? [c.id] : [],
130
+ );
131
+ return {
132
+ layout: layout.filter((c) => !c.id || !remove.includes(c.id)),
133
+ remove,
134
+ };
135
+ }
@@ -0,0 +1,188 @@
1
+ import type { ChannelKey } from "../domain/conversation.ts";
2
+ import { AgentError } from "../domain/errors.ts";
3
+ import type { AgentTurnScope } from "../domain/ports.ts";
4
+ import type { AgentGroup } from "./agent-store.ts";
5
+ import {
6
+ channelIdOf,
7
+ channelKey,
8
+ groupSessionKey,
9
+ homeScope,
10
+ } from "./team-keys.ts";
11
+ import { categoryKind, groupTopic } from "./team-layout.ts";
12
+ import type { TeamContext } from "./team-options.ts";
13
+
14
+ /** The team's startup, fresh starts, and archiving of agents and groups. */
15
+ export class TeamLifecycle {
16
+ readonly #ctx: TeamContext;
17
+
18
+ constructor(ctx: TeamContext) {
19
+ this.#ctx = ctx;
20
+ }
21
+
22
+ // ── Startup ────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Inserts the seed agents, gives every active agent a channel, archives agents and groups
26
+ * whose channels were deleted while the assistant was down, and draws missing avatars in the
27
+ * background.
28
+ */
29
+ async start(): Promise<void> {
30
+ const { store, channels, seeds, logger } = this.#ctx.options;
31
+ const added = await store.seed(seeds());
32
+ if (added.length > 0) logger.info({ added }, "seed agents added");
33
+ for (const agent of store.agents()) {
34
+ if (agent.status !== "active") continue;
35
+ if (!agent.channelId) {
36
+ const id = await channels.createChannel(
37
+ agent.name,
38
+ agent.displayName,
39
+ "Agents",
40
+ );
41
+ await store.updateAgent(agent.name, { channelId: id });
42
+ logger.info(
43
+ { agent: agent.name, channel: id },
44
+ "agent channel created",
45
+ );
46
+ } else if (!(await channels.exists(agent.channelId))) {
47
+ await this.channelDeleted(agent.channelId);
48
+ }
49
+ }
50
+ const layout = await channels.layout();
51
+ for (const group of store.groups()) {
52
+ if (group.status !== "active") continue;
53
+ if (!(await channels.exists(group.channelId))) {
54
+ await this.channelDeleted(group.channelId);
55
+ continue;
56
+ }
57
+ this.retitle(group);
58
+ const home = layout.find((c) => c.channelIds.includes(group.channelId));
59
+ if (home && categoryKind(home.name) === "Groups") continue;
60
+ if (await channels.placeIn(group.channelId, "Groups"))
61
+ logger.info({ group: group.name }, "group channel moved to Groups");
62
+ }
63
+ this.#ctx.changed();
64
+ void this.#drawMissingAvatars();
65
+ }
66
+
67
+ async #drawMissingAvatars(): Promise<void> {
68
+ const { store, studio, logger } = this.#ctx.options;
69
+ for (const agent of store.agents()) {
70
+ if (agent.status !== "active" || agent.avatarHash) continue;
71
+ try {
72
+ const hash = await studio.draw(agent.avatarPrompt);
73
+ await store.updateAgent(agent.name, { avatarHash: hash });
74
+ logger.info({ agent: agent.name }, "avatar drawn");
75
+ } catch (error) {
76
+ logger.warn({ agent: agent.name, err: error }, "avatar not drawn");
77
+ }
78
+ }
79
+ }
80
+
81
+ /** Starts an agent's or every group member's conversation over; call inside the channel's queue. */
82
+ async startFresh(channel: ChannelKey): Promise<void> {
83
+ const { store } = this.#ctx.options;
84
+ const runtime = this.#ctx.options.runtime();
85
+ const id = channelIdOf(channel);
86
+ const agent = store.agentByChannel(id);
87
+ if (agent) {
88
+ await runtime.startFresh(homeScope(agent).session);
89
+ this.#ctx.changed();
90
+ return;
91
+ }
92
+ const group = store.groupByChannel(id);
93
+ if (!group) return;
94
+ for (const member of group.members)
95
+ await runtime.startFresh(groupSessionKey(group, member));
96
+ await store.catchUp(group);
97
+ }
98
+
99
+ // ── Archive ────────────────────────────────────────────────────────────
100
+
101
+ /** A deleted channel archives the agent or group that owned it. */
102
+ channelDeleted(channelId: string): Promise<void> {
103
+ return this.#archive(channelId);
104
+ }
105
+
106
+ /**
107
+ * Archives an agent or group like a deleted channel, then keeps its channel under Archive,
108
+ * where no one owns it and so nothing answers. Runs at once, not in the target's queue: the
109
+ * caller may be a member of the group it archives, and the round stops at its next turn.
110
+ */
111
+ async archive(caller: AgentTurnScope, name: string): Promise<string> {
112
+ const { store, entryChannelId, channels, logger } = this.#ctx.options;
113
+ const agent = store.agent(name);
114
+ const group = agent ? undefined : store.group(name);
115
+ const target = agent ?? group;
116
+ if (!target)
117
+ throw new AgentError(
118
+ `There is no agent or group "${name}"; see agent_list.`,
119
+ );
120
+ if (target.status !== "active")
121
+ throw new AgentError(`"${name}" is already archived.`);
122
+ if (agent && agent.channelId === entryChannelId)
123
+ throw new AgentError("The coordinator cannot be archived.");
124
+ if (agent && name === caller.name)
125
+ throw new AgentError("You cannot archive yourself; ask another agent.");
126
+ const channelId = target.channelId;
127
+ if (!channelId) throw new AgentError(`${name} has no channel.`);
128
+ await this.#archive(channelId);
129
+ try {
130
+ await channels.removeWebhook(channelId);
131
+ } catch (error) {
132
+ logger.warn(
133
+ { name, err: error },
134
+ "archived channel's webhook not removed",
135
+ );
136
+ }
137
+ try {
138
+ await channels.placeIn(channelId, "Archive");
139
+ } catch (error) {
140
+ logger.error({ name, err: error }, "archived channel not moved");
141
+ return `Archived "${target.displayName}", but its channel <#${channelId}> could not be moved to Archive (${error instanceof Error ? error.message : String(error)}); it stays silent where it is.`;
142
+ }
143
+ return `Archived "${target.displayName}"; its channel <#${channelId}> is kept under Archive.`;
144
+ }
145
+
146
+ async #archive(channelId: string): Promise<void> {
147
+ const { store, schedules, logger } = this.#ctx.options;
148
+ const runtime = this.#ctx.options.runtime();
149
+ const agent = store.agentByChannel(channelId);
150
+ if (agent) {
151
+ const left = await store.archiveAgent(agent.name);
152
+ const home = channelKey(channelId);
153
+ for (const schedule of await schedules.forChannel(home))
154
+ await schedules.remove(schedule.id, home);
155
+ await runtime.startFresh(home);
156
+ for (const group of left) {
157
+ await runtime.startFresh(groupSessionKey(group, agent.name));
158
+ if (group.status === "active") this.retitle(group);
159
+ }
160
+ logger.info(
161
+ { agent: agent.name, groups: left.map((g) => g.name) },
162
+ "agent archived",
163
+ );
164
+ this.#ctx.changed();
165
+ return;
166
+ }
167
+ const group = store.groupByChannel(channelId);
168
+ if (!group) return;
169
+ await store.archiveGroup(group.name);
170
+ for (const member of group.members)
171
+ await runtime.startFresh(groupSessionKey(group, member));
172
+ logger.info({ group: group.name }, "group archived");
173
+ this.#ctx.changed();
174
+ }
175
+
176
+ /** Writes a group's members and host into its channel topic, without waiting on Discord. */
177
+ retitle(group: AgentGroup): void {
178
+ const { channels, store, logger } = this.#ctx.options;
179
+ channels
180
+ .setTopic(group.channelId, groupTopic(group, store))
181
+ .catch((error: unknown) =>
182
+ logger.warn(
183
+ { group: group.name, err: error },
184
+ "group topic not updated",
185
+ ),
186
+ );
187
+ }
188
+ }
@@ -0,0 +1,52 @@
1
+ import type { ChannelKey } from "../domain/conversation.ts";
2
+ import type { ThinkingSetting } from "../models.ts";
3
+ import type { SkillRegistry } from "../modules/skills/skill-registry.ts";
4
+ import type { PromptSection } from "../plugin.ts";
5
+ import type { ToolSelection } from "../sessions.ts";
6
+ import type { AgentChannels, AgentModels } from "./agent-ports.ts";
7
+ import type { AgentGroup, AgentSeed } from "./agent-store.ts";
8
+ import type { AvatarStudio } from "./avatar-studio.ts";
9
+ import type { TeamTurns, TeamTurnsOptions } from "./team-turns.ts";
10
+
11
+ export interface AgentTeamOptions
12
+ extends Omit<TeamTurnsOptions, "selection" | "changed"> {
13
+ /** The agent server; its entry channel is where the coordinator lives. */
14
+ guildId: string;
15
+ /** The first team, read and inserted once on start; an agent already stored is never overwritten. */
16
+ seeds: () => readonly AgentSeed[];
17
+ /** Sections the plugins add to every agent turn's system prompt, read before each run. */
18
+ promptSections?: () => readonly PromptSection[];
19
+ /** The tools plugins defined for agents, read before each agent turn. */
20
+ pluginTools?: () => readonly string[];
21
+ channels: AgentChannels;
22
+ studio: Pick<AvatarStudio, "url" | "draw" | "edit">;
23
+ /** The tools and groups the plugins give every agent, read before each agent turn. */
24
+ pluginSelection: () => ToolSelection;
25
+ models: AgentModels;
26
+ schedules: {
27
+ forChannel(channel: ChannelKey): Promise<{ id: number }[]>;
28
+ remove(id: number, channel?: ChannelKey): Promise<unknown>;
29
+ };
30
+ /** The shell's shared working directory. */
31
+ workDir: string;
32
+ /** The host account the agents' shell and file tools run as. */
33
+ shellUser: string;
34
+ /** The prompt every persona shares, the assistant's included; each agent's starts with it. */
35
+ sharedPrompt: string;
36
+ /** The shared prompt for speakers other than the owner, when `sharedPrompt` speaks to the owner. */
37
+ guestPrompt?: string;
38
+ /** The skill registry: what each agent carries and the skill tools. */
39
+ skills: SkillRegistry;
40
+ }
41
+
42
+ /** What the team's collaborators share: its options, its turns, and its change signal. */
43
+ export interface TeamContext {
44
+ readonly options: AgentTeamOptions;
45
+ readonly turns: TeamTurns;
46
+ /** Called when a turn starts or ends and when an agent or group changes. */
47
+ changed(): void;
48
+ /** The model and thinking setting of an agent's next run. */
49
+ modelOf(name: string): { model: string; thinking: ThinkingSetting };
50
+ /** Writes a group's members and host into its channel topic, without waiting on Discord. */
51
+ retitle(group: AgentGroup): void;
52
+ }