pi-roundtable 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +6 -4
  3. package/docs/plugins.md +1448 -66
  4. package/examples/echo-runtime.test.ts +122 -0
  5. package/examples/echo-runtime.ts +107 -0
  6. package/examples/events.test.ts +1 -0
  7. package/examples/events.ts +2 -2
  8. package/examples/fake-surface.test.ts +180 -0
  9. package/examples/fake-surface.ts +109 -0
  10. package/examples/interactions.test.ts +12 -3
  11. package/examples/interactions.ts +19 -19
  12. package/examples/shared-services.test.ts +74 -0
  13. package/examples/shared-services.ts +60 -0
  14. package/examples/study-room.test.ts +69 -0
  15. package/examples/study-room.ts +52 -0
  16. package/examples/support-desk.test.ts +34 -0
  17. package/examples/support-desk.ts +40 -0
  18. package/package.json +9 -2
  19. package/src/cli/checks/database.ts +11 -30
  20. package/src/cli/checks/images.ts +19 -0
  21. package/src/cli/doctor.ts +6 -0
  22. package/src/cli/main.ts +2 -1
  23. package/src/core/agents/agent-claim.ts +42 -16
  24. package/src/core/agents/agent-messages.ts +4 -4
  25. package/src/core/agents/agent-ports.ts +8 -21
  26. package/src/core/agents/agent-prompt.ts +3 -1
  27. package/src/core/agents/agent-store.ts +6 -4
  28. package/src/core/agents/agent-team-fixture.ts +389 -0
  29. package/src/core/agents/agent-team.ts +26 -25
  30. package/src/core/agents/agent-tools.ts +34 -15
  31. package/src/core/agents/avatar-studio.ts +40 -6
  32. package/src/core/agents/fallback-avatar.ts +85 -0
  33. package/src/core/agents/team-editing.ts +49 -10
  34. package/src/core/agents/team-keys.ts +22 -9
  35. package/src/core/agents/team-layout.ts +4 -3
  36. package/src/core/agents/team-lifecycle.ts +28 -2
  37. package/src/core/agents/team-options.ts +9 -5
  38. package/src/core/agents/team-status.ts +4 -4
  39. package/src/core/agents/team-text.ts +8 -6
  40. package/src/core/agents/team-turn-types.ts +4 -4
  41. package/src/core/agents/team-turns.ts +6 -5
  42. package/src/core/builtin/agent-server.ts +187 -121
  43. package/src/core/builtin/discord-admin.ts +41 -0
  44. package/src/core/builtin/discord.ts +65 -40
  45. package/src/core/builtin/modules.ts +67 -58
  46. package/src/core/builtin/session-tool.ts +24 -0
  47. package/src/core/builtin/skills.ts +72 -0
  48. package/src/core/builtin/stores.ts +55 -31
  49. package/src/core/config/config.ts +45 -4
  50. package/src/core/config/schema.ts +6 -0
  51. package/src/core/contract/channels.ts +54 -14
  52. package/src/core/contract/providers.ts +17 -0
  53. package/src/core/contract/runtime.ts +138 -0
  54. package/src/core/contract/services.ts +45 -0
  55. package/src/core/contract/surface.ts +88 -0
  56. package/src/core/db/migrations.ts +118 -3
  57. package/src/core/define-roundtable.ts +70 -46
  58. package/src/core/define.ts +2 -1
  59. package/src/core/discord/agent-commands.ts +39 -14
  60. package/src/core/discord/agent-panel.ts +66 -0
  61. package/src/core/discord/channel-executor.ts +6 -3
  62. package/src/core/discord/channel-operations.ts +14 -11
  63. package/src/core/discord/command-collection.ts +46 -0
  64. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  65. package/src/core/discord/connection.ts +35 -0
  66. package/src/core/discord/discord-surface.ts +32 -94
  67. package/src/core/discord/dispatch-threads.ts +2 -2
  68. package/src/core/discord/inbound-message.ts +99 -0
  69. package/src/core/discord/interaction-module.ts +57 -1
  70. package/src/core/discord/owner-cards.ts +4 -4
  71. package/src/core/discord/owner-command.ts +44 -11
  72. package/src/core/discord/owner-discord.ts +2 -2
  73. package/src/core/discord/schedule-commands.ts +21 -18
  74. package/src/core/discord/stop-button.ts +44 -1
  75. package/src/core/domain/attachment.ts +12 -8
  76. package/src/core/domain/conversation.ts +3 -4
  77. package/src/core/domain/owner-prompts.ts +5 -5
  78. package/src/core/domain/ports.ts +9 -53
  79. package/src/core/freeze.ts +9 -0
  80. package/src/core/host.ts +255 -82
  81. package/src/core/http/listeners.ts +55 -20
  82. package/src/core/i18n/agent-panel.ts +4 -0
  83. package/src/core/i18n/index.ts +12 -2
  84. package/src/core/i18n/owner.ts +7 -1
  85. package/src/core/i18n/schedules.ts +12 -6
  86. package/src/core/identity.ts +1 -1
  87. package/src/core/judging/effort-judge.ts +25 -3
  88. package/src/core/log.ts +21 -3
  89. package/src/core/models.ts +2 -2
  90. package/src/core/modules/background/background-turns.ts +6 -4
  91. package/src/core/modules/delegation/delegate.ts +3 -2
  92. package/src/core/modules/delegation/delegator.ts +15 -13
  93. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  94. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  95. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  96. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  97. package/src/core/modules/memory/owner-memory.ts +9 -13
  98. package/src/core/modules/schedules/recurrence.ts +5 -3
  99. package/src/core/modules/schedules/schedule-store.ts +11 -11
  100. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  101. package/src/core/modules/schedules/scheduler.ts +7 -2
  102. package/src/core/modules/schedules/schedules.ts +9 -3
  103. package/src/core/modules/skills/skill-registry.ts +3 -2
  104. package/src/core/modules/skills/skill-store.ts +2 -15
  105. package/src/core/modules/skills/skill-tools.ts +10 -4
  106. package/src/core/ops/error-reporter.ts +1 -1
  107. package/src/core/plugin.ts +232 -28
  108. package/src/core/registry/contributions.ts +173 -14
  109. package/src/core/registry/providers.ts +35 -5
  110. package/src/core/registry/services.ts +229 -0
  111. package/src/core/routing/channel-queue.ts +5 -0
  112. package/src/core/routing/channel-router.ts +21 -7
  113. package/src/core/routing/conversation-turns.ts +139 -0
  114. package/src/core/routing/message-text.ts +9 -2
  115. package/src/core/routing/surface-port.ts +39 -0
  116. package/src/core/runtime/conversation-sessions.ts +4 -3
  117. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  118. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  119. package/src/core/runtime/mcp.ts +1 -1
  120. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  121. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  122. package/src/core/runtime/prompt-slot.ts +8 -3
  123. package/src/core/runtime/runtime-types.ts +9 -35
  124. package/src/core/runtime/session-factory.ts +21 -5
  125. package/src/core/runtime/text-tools.ts +1 -3
  126. package/src/core/services.ts +238 -90
  127. package/src/core/sessions.ts +3 -2
  128. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  129. package/src/core/shared/delegate-tool.ts +4 -3
  130. package/src/core/shared/schedule-tools.ts +22 -13
  131. package/src/core/shared/session-messages.ts +1 -1
  132. package/src/core/speakers.ts +6 -38
  133. package/src/core/testing/database.ts +1 -7
  134. package/src/core/testing/eager-catalog.ts +30 -0
  135. package/src/core/testing/locale.ts +23 -5
  136. package/src/core/testing/modules.ts +105 -41
  137. package/src/core/testing/test-host.ts +279 -0
  138. package/src/core/testing/tool-set.ts +64 -0
  139. package/src/core/time.ts +6 -1
  140. package/src/core/tool-set.snapshot.json +288 -0
  141. package/src/core/tool-tiers.ts +17 -57
  142. package/src/discord/index.ts +64 -0
  143. package/src/index.ts +181 -10
  144. package/src/kit/channels.ts +14 -0
  145. package/src/kit/domain.ts +18 -0
  146. package/src/kit/holds.ts +4 -0
  147. package/src/kit/index.ts +129 -0
  148. package/src/kit/judging.ts +10 -0
  149. package/src/kit/memory.ts +3 -0
  150. package/src/kit/mirror.ts +19 -0
  151. package/src/kit/presentation.ts +7 -0
  152. package/src/kit/shell.ts +7 -0
  153. package/src/kit/skills.ts +8 -0
  154. package/src/kit/support.ts +21 -0
  155. package/src/kit/threads.ts +7 -0
  156. package/src/kit/tools.ts +14 -0
  157. package/src/kit/worker.ts +16 -0
  158. package/src/testing.ts +394 -34
  159. package/examples/guide.test.ts +0 -52
  160. package/src/cli/add-plugin.test.ts +0 -107
  161. package/src/cli/checks/basic.test.ts +0 -276
  162. package/src/cli/checks/database.test.ts +0 -117
  163. package/src/cli/checks/discord.test.ts +0 -200
  164. package/src/cli/cli.test.ts +0 -122
  165. package/src/cli/config-edit.test.ts +0 -130
  166. package/src/cli/doctor.test.ts +0 -184
  167. package/src/cli/init.test.ts +0 -127
  168. package/src/cli/size.test.ts +0 -9
  169. package/src/cli/templates.test.ts +0 -129
  170. package/src/cli/testing/fixtures.ts +0 -106
  171. package/src/core/agents/agent-claim.test.ts +0 -142
  172. package/src/core/agents/agent-dashboard.test.ts +0 -120
  173. package/src/core/agents/agent-guild.test.ts +0 -124
  174. package/src/core/agents/agent-store.test.ts +0 -205
  175. package/src/core/agents/avatar-studio.test.ts +0 -88
  176. package/src/core/agents/group-round.test.ts +0 -143
  177. package/src/core/agents/owner-identity.test.ts +0 -172
  178. package/src/core/agents/team-turns.test.ts +0 -278
  179. package/src/core/attachments/attachments.test.ts +0 -85
  180. package/src/core/boundary.test.ts +0 -45
  181. package/src/core/builtin/modules.test.ts +0 -127
  182. package/src/core/config/config.test.ts +0 -135
  183. package/src/core/contract/discord.ts +0 -33
  184. package/src/core/db/migrations.test.ts +0 -237
  185. package/src/core/define-roundtable.test.ts +0 -134
  186. package/src/core/define.test.ts +0 -144
  187. package/src/core/discord/agent-commands.test.ts +0 -58
  188. package/src/core/discord/agent-discord.test.ts +0 -70
  189. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  190. package/src/core/discord/dispatch-threads.test.ts +0 -94
  191. package/src/core/discord/owner-cards.test.ts +0 -435
  192. package/src/core/discord/owner-discord.test.ts +0 -325
  193. package/src/core/domain/expression.ts +0 -21
  194. package/src/core/domain/profile.ts +0 -32
  195. package/src/core/drain.test.ts +0 -46
  196. package/src/core/events.test.ts +0 -81
  197. package/src/core/holds.test.ts +0 -54
  198. package/src/core/host.test.ts +0 -536
  199. package/src/core/http/listeners.test.ts +0 -164
  200. package/src/core/i18n/i18n.test.ts +0 -139
  201. package/src/core/identity.test.ts +0 -18
  202. package/src/core/judging/effort-judge.test.ts +0 -112
  203. package/src/core/judging/model-judge.test.ts +0 -126
  204. package/src/core/modules/delegation/delegator.test.ts +0 -170
  205. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  206. package/src/core/modules/schedules/schedule.test.ts +0 -358
  207. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  208. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  209. package/src/core/ops/error-reporter.test.ts +0 -283
  210. package/src/core/presentation/card-cadence.ts +0 -41
  211. package/src/core/presentation/presentation.test.ts +0 -166
  212. package/src/core/public-entry.test.ts +0 -59
  213. package/src/core/registry/contributions.test.ts +0 -215
  214. package/src/core/registry/interactions.test.ts +0 -87
  215. package/src/core/registry/providers.test.ts +0 -103
  216. package/src/core/routing/channel-queue.test.ts +0 -31
  217. package/src/core/routing/channel-router.test.ts +0 -326
  218. package/src/core/routing/conversation-kind.ts +0 -14
  219. package/src/core/routing/settle-turn.test.ts +0 -21
  220. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  221. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  222. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  223. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  224. package/src/core/runtime/session-archive.test.ts +0 -20
  225. package/src/core/runtime/steerable-run.test.ts +0 -264
  226. package/src/core/runtime/text-tools.test.ts +0 -110
  227. package/src/core/runtime/turn-answer.test.ts +0 -72
  228. package/src/core/runtime/worker-task.test.ts +0 -82
  229. package/src/core/services.test.ts +0 -28
  230. package/src/core/sessions.test.ts +0 -85
  231. package/src/core/shared/unix-server.ts +0 -18
  232. package/src/core/size.test.ts +0 -9
  233. package/src/core/speakers.test.ts +0 -78
  234. package/src/core/testing/file-size.ts +0 -34
  235. package/src/core/time.test.ts +0 -166
  236. package/src/core/tool-tiers.test.ts +0 -117
  237. package/src/entries.test.ts +0 -112
  238. package/src/testing.test.ts +0 -156
@@ -19,8 +19,8 @@ import {
19
19
  type ToolSelection,
20
20
  type TransientTask,
21
21
  } from "../sessions.ts";
22
+ import { activeToolsExtension } from "../shared/active-tools.ts";
22
23
  import { packageDir } from "../shared/package-dir.ts";
23
- import { profileToolsExtension } from "../shared/profile-tools.ts";
24
24
  import { readAttachmentExtension } from "../shared/read-attachment-tool.ts";
25
25
  import { addressee, type Speaker, THE_SPEAKER } from "../speakers.ts";
26
26
  import {
@@ -83,6 +83,19 @@ export class SessionFactory {
83
83
  return this.#linked;
84
84
  }
85
85
 
86
+ /**
87
+ * The system prompt of a non-agent conversation of the kind: a plugin's persona, else for
88
+ * "owner" none. A kind nobody wrote a persona for is refused, never given the owner's.
89
+ */
90
+ personaOf(kind: string): string {
91
+ const contributed = this.link().persona(kind);
92
+ if (contributed !== undefined) return contributed;
93
+ if (kind === "owner") return "";
94
+ throw new ConfigError(
95
+ `no persona is registered for the conversation kind "${kind}". A plugin adds one with \`personas: [{ kind: "${kind}", prompt() { ... } }]\`, or its claim must start conversations of a kind that has one.`,
96
+ );
97
+ }
98
+
86
99
  get plan(): SessionPlan {
87
100
  return this.link().plan;
88
101
  }
@@ -196,8 +209,11 @@ export class SessionFactory {
196
209
  slot: PromptSlot,
197
210
  attachmentDir: string,
198
211
  agent: AgentTurnScope | undefined,
212
+ kind: string,
199
213
  ): Promise<ChannelSession> {
200
- const { agentDir, model, thinking, persona, logger } = this.#options;
214
+ const { agentDir, model, thinking, logger } = this.#options;
215
+ // An agent's prompt is set before each run; the other kinds are refused here, before a session is built.
216
+ const persona = agent ? undefined : this.personaOf(kind);
201
217
  const skills = this.skillsOf(agent);
202
218
  const state = {
203
219
  tools: [] as readonly string[],
@@ -218,7 +234,7 @@ export class SessionFactory {
218
234
  this.plan.compaction?.engine,
219
235
  );
220
236
  const context: SessionContext = {
221
- kind: agent ? "agent" : "owner",
237
+ kind: agent ? "agent" : kind,
222
238
  homeChannel: channel,
223
239
  turnChannel: agent && agents ? agents.turnChannel(agent) : channel,
224
240
  compaction: {
@@ -253,10 +269,10 @@ export class SessionFactory {
253
269
  () => addressee(this.#deps.speaker(channel), this.#options.owner),
254
270
  ),
255
271
  selfCompactGuard: selfCompactGuardExtension(),
256
- profileTools: profileToolsExtension(() => state.tools),
272
+ activeTools: activeToolsExtension(() => state.tools),
257
273
  }),
258
274
  // An agent's prompt is set before each run by the agent-prompt extension.
259
- appendSystemPrompt: agent ? [] : [persona],
275
+ appendSystemPrompt: persona === undefined ? [] : [persona],
260
276
  });
261
277
  await resourceLoader.reload();
262
278
 
@@ -14,15 +14,13 @@ export interface TextToolDef<Name extends string = string> {
14
14
  run(input: ToolInput, signal?: AbortSignal): Promise<string> | string;
15
15
  }
16
16
 
17
- type ErrorClass = abstract new (...args: never[]) => Error;
18
-
19
17
  /**
20
18
  * Registers text tools. A thrown `refusal` becomes an error result the model reads; any other
21
19
  * error fails the call.
22
20
  */
23
21
  export function textToolsExtension(
24
22
  defs: readonly TextToolDef[],
25
- refusal: ErrorClass,
23
+ refusal: abstract new (...args: never[]) => Error,
26
24
  ): ExtensionFactory {
27
25
  return (pi) => {
28
26
  for (const def of defs) {
@@ -1,107 +1,255 @@
1
- import type { AgentStore } from "./agents/agent-store.ts";
2
- import type { AgentTeam } from "./agents/agent-team.ts";
3
- import type { AvatarStudio } from "./agents/avatar-studio.ts";
4
- import type { DiscordSurface } from "./discord/discord-surface.ts";
5
- import type { DispatchThreads } from "./discord/dispatch-threads.ts";
6
- import type { OwnerCards } from "./discord/owner-cards.ts";
7
- import type { OwnerGuard } from "./discord/owner-command.ts";
8
- import { PluginError } from "./errors.ts";
9
- import type { ConfirmationJudge } from "./judging/confirmation-judge.ts";
10
- import type { BackgroundTurns } from "./modules/background/background-turns.ts";
11
- import type { Delegator } from "./modules/delegation/delegator.ts";
12
- import type { OwnerMemoryStore } from "./modules/memory/owner-memory-store.ts";
13
- import type { ScheduleStore } from "./modules/schedules/schedule-store.ts";
14
- import type { SkillRegistry } from "./modules/skills/skill-registry.ts";
15
- import type { SkillStore } from "./modules/skills/skill-store.ts";
16
- import type { PendingConfirmationStore } from "./runtime/pending-confirmation-store.ts";
17
- import type { PiAgentRuntime } from "./runtime/pi-agent-runtime.ts";
1
+ import type { Agent, AgentGroup } from "./agents/agent-rules.ts";
2
+ import type { AvatarMode } from "./agents/agent-tools.ts";
3
+ import type { TeamStatus } from "./agents/team-status.ts";
4
+ import type { ScheduledOutcome } from "./contract/channels.ts";
5
+ import type { AgentRuntime } from "./contract/runtime.ts";
6
+ import { type ServiceKey, serviceKey } from "./contract/services.ts";
7
+ import type { PendingConfirmation } from "./domain/conversation.ts";
8
+ import type { HttpRoute } from "./http/listeners.ts";
9
+ import type { ThinkingSetting } from "./models.ts";
10
+ import type {
11
+ DelegationJob,
12
+ DelegationOutcome,
13
+ } from "./modules/delegation/delegator.ts";
14
+ import type {
15
+ Memory,
16
+ MemoryKind,
17
+ PromptMemory,
18
+ } from "./modules/memory/owner-memory-store.ts";
19
+ import type {
20
+ NewSchedule,
21
+ Schedule,
22
+ ScheduleChange,
23
+ } from "./modules/schedules/schedule-store.ts";
24
+ import type {
25
+ SkillCatalogEntry,
26
+ SkillSet,
27
+ } from "./modules/skills/skill-rules.ts";
28
+ import type { AgentTurnScope, ChannelKey } from "./sessions.ts";
18
29
 
19
- /** The stores every process has, attached to the host's one pool. */
20
- export interface CoreStores {
21
- /** The owner's memory. */
22
- memory: OwnerMemoryStore;
23
- schedules: ScheduleStore;
24
- /** Held actions, kept across a restart. */
25
- confirmations: PendingConfirmationStore;
26
- /** The agent server's agents and groups. */
27
- agents: AgentStore;
28
- skills: SkillStore;
29
- }
30
+ // The services the built-in plugins provide, and the ports they keep to. Each port is an
31
+ // interface, so an object with the same methods satisfies it without being one of the built-in
32
+ // classes; a plugin replaces a built-in by providing its own under the same key.
30
33
 
31
- /** The Discord connection and what stands on it. */
32
- export interface CoreDiscord {
33
- surface: DiscordSurface;
34
- /** The owner's cards, which also answer the interactions of held actions. */
35
- cards: OwnerCards;
36
- guard: OwnerGuard;
37
- studio: AvatarStudio;
38
- /** The threads that carry background reports. */
39
- threads: DispatchThreads;
40
- }
34
+ /** The agent server: its team, its directory, the runtime that runs every agent turn. Provided by the agent-server plugin. */
35
+ export const AGENTS: ServiceKey<AgentServer> =
36
+ serviceKey<AgentServer>("roundtable.agents");
37
+ /** Scheduled turns, stored. Provided by the `schedule-store` plugin. */
38
+ export const SCHEDULES: ServiceKey<ScheduleStore> = serviceKey<ScheduleStore>(
39
+ "roundtable.schedules",
40
+ );
41
+ /** Turns nobody wrote: a due schedule's, a delegated task's report, a logged error's. Provided by the modules plugin. */
42
+ export const BACKGROUND_TURNS: ServiceKey<BackgroundTurns> =
43
+ serviceKey<BackgroundTurns>("roundtable.background-turns");
44
+ /** Tasks run in the background, each reporting back in its channel. Provided by the modules plugin. */
45
+ export const DELEGATION: ServiceKey<Delegator> = serviceKey<Delegator>(
46
+ "roundtable.delegation",
47
+ );
48
+ /**
49
+ * The remembered facts of each speaker. Provided by the `memory` plugin, an addon: read it with
50
+ * `find` when your plugin works without memory, and with `get` when it cannot.
51
+ */
52
+ export const MEMORY: ServiceKey<MemoryStore> = serviceKey<MemoryStore>(
53
+ "roundtable.memory",
54
+ {
55
+ absent:
56
+ "The memory addon is switched off (config memory: false). Switch it on, or provide the service from a plugin of your own.",
57
+ },
58
+ );
59
+ /**
60
+ * The skills agents carry. Provided by the `skills` plugin, an addon: read it with `find` when
61
+ * your plugin works without skills, and with `get` when it cannot.
62
+ */
63
+ export const SKILLS: ServiceKey<SkillRegistry> = serviceKey<SkillRegistry>(
64
+ "roundtable.skills",
65
+ {
66
+ absent:
67
+ "The skills addon is switched off (config skills: false). Switch it on, or provide the service from a plugin of your own.",
68
+ },
69
+ );
41
70
 
42
- /** What the agent server builds. */
43
- export interface CoreAgents {
71
+ /** What the agent server builds, for the plugins set up after it. */
72
+ export interface AgentServer {
44
73
  team: AgentTeam;
45
- runtime: PiAgentRuntime;
46
- skills: SkillRegistry;
47
- /** Decides whether the owner's reply approves a held action. */
48
- confirmations: ConfirmationJudge;
74
+ /** Read-only lookups of agents and groups; edits go through the team. */
75
+ directory: AgentDirectory;
76
+ /** The runtime every agent-server turn runs on: the `runtime` provider's, or Pi's when no plugin fills the slot. */
77
+ runtime: AgentRuntime;
78
+ /** Decides whether the owner's reply approves the actions the assistant held for them. */
79
+ approvals: {
80
+ approves(pending: PendingConfirmation, reply: string): Promise<boolean>;
81
+ };
82
+ /** Agents' pictures. */
83
+ avatars: AvatarStudio;
49
84
  }
50
85
 
51
- /** What the built-in plugins build for the plugins registered after them. */
52
- export interface CoreServices {
53
- stores: CoreStores;
54
- discord: CoreDiscord;
55
- /** Turns nobody wrote: a due schedule's, a delegated task's report, a logged error's. */
56
- background: BackgroundTurns;
57
- delegator: Delegator;
58
- /** The agent server: its team, the runtime that runs every turn, and the skill registry. */
59
- agents: CoreAgents;
86
+ /** Agent pictures: drawn by an image provider, or made from the agent's name, kept and served by content hash. */
87
+ export interface AvatarStudio {
88
+ /** Whether an image provider is configured; without one, only `fallback` makes pictures. */
89
+ readonly canDraw: boolean;
90
+ /** The public URL of a picture, or of the default when there is none. */
91
+ url(hash: string | undefined): string;
92
+ /** Draws a new picture from an avatar prompt; returns its hash. */
93
+ draw(avatarPrompt: string): Promise<string>;
94
+ /** Edits a stored picture by an instruction; returns the new picture's hash. */
95
+ edit(hash: string | undefined, instruction: string): Promise<string>;
96
+ /** A picture made from an agent's display name and name, for a host without an image provider; returns its hash. */
97
+ fallback(displayName: string, name: string): Promise<string>;
98
+ /** The pictures, public on the named listener. */
99
+ route(listener: string): HttpRoute;
100
+ /** Serves `/avatars/<hash>.png`; undefined for any other path. */
101
+ serve(pathname: string): Response | undefined;
60
102
  }
61
103
 
62
- /** Reads the core's services; reading one before its built-in plugin has set up throws a PluginError. */
63
- export type CoreAccess = {
64
- readonly [K in keyof CoreServices]: CoreServices[K];
65
- };
104
+ /** The read half of the agent store: every inbound message asks, and only this process changes them. */
105
+ export interface AgentDirectory {
106
+ agents(): Agent[];
107
+ agent(name: string): Agent | undefined;
108
+ /** The agent, or throws AgentError when it is unknown or archived. */
109
+ activeAgent(name: string): Agent;
110
+ agentByChannel(channelId: string): Agent | undefined;
111
+ groups(): AgentGroup[];
112
+ group(name: string): AgentGroup | undefined;
113
+ groupByChannel(channelId: string): AgentGroup | undefined;
114
+ }
66
115
 
67
- /** Holds what the built-in plugins provide, for the plugins set up after them. */
68
- export class CoreRegistry implements CoreAccess {
69
- readonly #values = new Map<keyof CoreServices, unknown>();
116
+ /** What an agent's settings may change, as `update` takes it. */
117
+ export interface AgentChange {
118
+ displayName?: string;
119
+ prompt?: string;
120
+ model?: string;
121
+ thinking?: string;
122
+ }
70
123
 
71
- /** Stores a service; a built-in plugin calls this once in its setup. */
72
- provide<K extends keyof CoreServices>(key: K, value: CoreServices[K]): void {
73
- if (this.#values.has(key))
74
- throw new PluginError(
75
- `core service ${key} is provided twice. Register only one plugin that provides it.`,
76
- );
77
- this.#values.set(key, value);
78
- }
124
+ /** The agent team as plugins use it: who is in it, what model they run, and their channels. */
125
+ export interface AgentTeam {
126
+ readonly guildId: string;
127
+ /** Called when a turn starts or ends and when an agent or group changes. */
128
+ onChange(listener: () => void): void;
129
+ /** Every active agent and group, with what each is doing now. */
130
+ status(): Promise<TeamStatus>;
131
+ /** The agent or group that owns a channel of the agent server. */
132
+ owns(channel: ChannelKey): "agent" | "group" | undefined;
133
+ /** The model and thinking setting of an agent's next run: its own, or the assistant's. */
134
+ modelOf(name: string): { model: string; thinking: ThinkingSetting };
135
+ /** The assistant's model and thinking setting, which agents without their own follow. */
136
+ defaultModel(): { model: string; thinking: ThinkingSetting };
137
+ /** Models an agent can be set to: every one the host runs, `current` first when given. */
138
+ usableModels(current?: string): Promise<string[]>;
139
+ /** An active agent's channel; throws ScheduleError for an unknown agent, as the schedule tools catch it. */
140
+ channelOf(name: string): ChannelKey;
141
+ /** The channel the scope's turns run in: the group's in a group round, otherwise its own. */
142
+ turnChannel(scope: AgentTurnScope): ChannelKey;
143
+ /** Posts text under the caller's name in the channel of its turn. */
144
+ postAs(caller: AgentTurnScope, text: string): Promise<void>;
145
+ /** The assistant's own notice, posted in the coordinator's channel under its name. */
146
+ announce(text: string): Promise<void>;
147
+ /** Changes an agent's display name, prompt, model, or thinking level; a model must be one the host runs. */
148
+ update(name: string, change: AgentChange): Promise<string>;
149
+ /** Draws and stores a new picture for an agent. */
150
+ redrawAvatar(name: string, mode: AvatarMode, text?: string): Promise<Agent>;
151
+ }
79
152
 
80
- #need<K extends keyof CoreServices>(key: K): CoreServices[K] {
81
- if (!this.#values.has(key))
82
- throw new PluginError(
83
- `core service ${key} is not provided yet. Register the built-in plugin that provides it before the plugin that reads it.`,
84
- );
85
- return this.#values.get(key) as CoreServices[K];
86
- }
153
+ /** Scheduled turns, stored. A one-time schedule is deleted once it fires. */
154
+ export interface ScheduleStore {
155
+ create(schedule: NewSchedule): Promise<Schedule>;
156
+ get(id: number): Promise<Schedule | undefined>;
157
+ forChannel(channel: ChannelKey): Promise<Schedule[]>;
158
+ all(): Promise<Schedule[]>;
159
+ /** Applies a change to one of the channel's schedules; undefined when it has no such schedule. */
160
+ update(
161
+ channel: ChannelKey,
162
+ id: number,
163
+ change: ScheduleChange,
164
+ ): Promise<Schedule | undefined>;
165
+ /** Deletes a schedule, only from the given channel when one is given. */
166
+ remove(id: number, channel?: ChannelKey): Promise<Schedule | undefined>;
167
+ due(now: Date): Promise<Schedule[]>;
168
+ /** Takes a due schedule before it runs: moves it to its next run, or deletes it when there is none; false when another claim won. */
169
+ claim(
170
+ schedule: Schedule,
171
+ next: Date | undefined,
172
+ now: Date,
173
+ ): Promise<boolean>;
174
+ /** Records how a run ended. */
175
+ recordStatus(id: number, status: string): Promise<void>;
176
+ }
87
177
 
88
- get stores(): CoreStores {
89
- return this.#need("stores");
90
- }
178
+ /** Turns nobody wrote, each answered in its channel by the claim that owns it. */
179
+ export interface BackgroundTurns {
180
+ /** A due schedule's turn, run as its creator's. */
181
+ runScheduled(schedule: Schedule, firedAt: Date): Promise<ScheduledOutcome>;
182
+ /** A delegated task's report, answered in its channel under the same rules as a schedule. */
183
+ runDelegated(job: DelegationJob, result: DelegationOutcome): Promise<void>;
184
+ /** The process's own logged error, reported to an agent in its channel as a report turn. */
185
+ runErrorReport(channel: ChannelKey, text: string): Promise<ScheduledOutcome>;
186
+ }
91
187
 
92
- get discord(): CoreDiscord {
93
- return this.#need("discord");
94
- }
188
+ /** What a delegated task is asked with: its job, before the delegator numbers and times it. */
189
+ export type DelegationRequest = Omit<
190
+ DelegationJob,
191
+ "id" | "startedAt" | "thread"
192
+ >;
95
193
 
96
- get background(): BackgroundTurns {
97
- return this.#need("background");
98
- }
194
+ /** Tasks run in the background, each handing its result back to its channel. Jobs live in memory. */
195
+ export interface Delegator {
196
+ /** Starts a job and returns at once; throws DelegationError on bad input or a full channel. */
197
+ start(request: DelegationRequest): DelegationJob;
198
+ /** The channel of each job still running, one entry per job. */
199
+ runningChannels(): ChannelKey[];
200
+ /** Resolves once every started job has reported. */
201
+ idle(): Promise<void>;
202
+ }
99
203
 
100
- get delegator(): Delegator {
101
- return this.#need("delegator");
102
- }
204
+ /** Remembered facts, each speaker's own; every agent shares the memory of whoever is speaking. */
205
+ export interface MemoryStore {
206
+ /** One speaker's memory; it never reads or changes another's. */
207
+ forSpeaker(speakerId: string): SpeakerMemory;
208
+ }
209
+
210
+ /** One speaker's remembered facts. */
211
+ export interface SpeakerMemory {
212
+ list(): Promise<Memory[]>;
213
+ /** What every turn carries: all core facts, and events that have not passed yet. */
214
+ forPrompt(today: string): Promise<PromptMemory>;
215
+ add(fact: string, kind?: MemoryKind, eventDate?: string): Promise<Memory>;
216
+ /** Memories containing any query term, most terms matched first, then newest first. */
217
+ search(query: string, limit?: number): Promise<Memory[]>;
218
+ /** Replaces one memory's text, kind, and date; undefined when the id is unknown. */
219
+ update(
220
+ id: number,
221
+ change: { fact: string; kind: MemoryKind; eventDate?: string },
222
+ ): Promise<Memory | undefined>;
223
+ /** Deletes one memory; false when the id is unknown. */
224
+ removeById(id: number): Promise<boolean>;
225
+ /** Deletes every memory containing the text; returns the removed facts. */
226
+ remove(text: string): Promise<string[]>;
227
+ }
103
228
 
104
- get agents(): CoreAgents {
105
- return this.#need("agents");
106
- }
229
+ /** The skills agents carry: built in, linked from repositories, or written by an agent. */
230
+ export interface SkillRegistry {
231
+ /** The built-in skills plus the named registered ones, resolved to their files. */
232
+ resolve(names: readonly string[]): SkillSet;
233
+ /** What an agent carries, built-in skills included. */
234
+ carried(agent: string): SkillSet;
235
+ /** The registered skills an agent carries, as the dashboard lists them. */
236
+ carriedNames(agent: string): string[];
237
+ /** An agent's skills as `agent_get` shows them. */
238
+ describeCarried(agent: string): string;
239
+ /** Every skill, built-in ones first. */
240
+ catalog(): SkillCatalogEntry[];
241
+ /** Every skill, or those of a group or matching a query, as the `skill_list` tool shows them. */
242
+ list(filter?: { group?: string; query?: string }): string;
243
+ /** The skills linked from a managed repository. */
244
+ linkedFrom(repo: string): string[];
245
+ /** Throws AgentError when a name is not registered; built-in names are carried already. */
246
+ checkRegistered(names: readonly string[]): void;
247
+ /** Links the skill at a repository path, or every child folder with a SKILL.md. */
248
+ link(repo: string, path: string): Promise<string>;
249
+ /** Adds and removes an agent's registered skills together, or changes nothing; returns what it carries after. */
250
+ attach(
251
+ agent: string,
252
+ add: readonly string[],
253
+ remove: readonly string[],
254
+ ): Promise<string[]>;
107
255
  }
@@ -43,7 +43,8 @@ export interface TransientTask {
43
43
 
44
44
  /** What one Pi session is for, as a session tool's factory sees it. */
45
45
  export interface SessionContext {
46
- kind: "owner" | "agent";
46
+ /** "agent" for an agent's session; otherwise the kind of conversation, which the claim that owns the channel names. */
47
+ kind: string;
47
48
  /** Where the session's schedules and delegated reports belong: the agent's own channel, or the owner's. */
48
49
  homeChannel: ChannelKey;
49
50
  /** Where the session's turns run: a group's channel for an agent's seat in it, else the home channel. */
@@ -94,7 +95,7 @@ const CORE_EXTENSIONS = [
94
95
  "confirmation-gate",
95
96
  "ask-user",
96
97
  "self-compact-guard",
97
- "profile-tools",
98
+ "active-tools",
98
99
  ] as const;
99
100
 
100
101
  /** Session tools grouped by phase, each group in registration order. */
@@ -1,11 +1,11 @@
1
1
  import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  /**
4
- * Pins each run's active tools to the current profile. Inline factories load after path
4
+ * Pins each run's active tools to the turn's selection. Inline factories load after path
5
5
  * extensions, so this handler runs last and undoes additions such as pi-web-access
6
6
  * re-adding web_enable or claude-bridge's AskClaude.
7
7
  */
8
- export function profileToolsExtension(
8
+ export function activeToolsExtension(
9
9
  currentTools: () => readonly string[],
10
10
  ): ExtensionFactory {
11
11
  return (pi) => {
@@ -1,9 +1,10 @@
1
1
  import { Type } from "typebox";
2
+ import { freeze } from "../freeze.ts";
2
3
 
3
- /** Hands web research to the cheaper worker model; the same for the owner agent and party roles. */
4
+ /** Hands web research to the cheaper worker model; shared by conversation sessions. */
4
5
  export const DELEGATE_TOOL = "delegate_task" as const;
5
6
 
6
- export const DELEGATE_TOOL_SPEC = {
7
+ export const DELEGATE_TOOL_SPEC = freeze({
7
8
  name: DELEGATE_TOOL,
8
9
  label: "Delegate task",
9
10
  description:
@@ -12,4 +13,4 @@ export const DELEGATE_TOOL_SPEC = {
12
13
  title: Type.String({ description: "A short name for the task." }),
13
14
  task: Type.String({ description: "The self-contained task." }),
14
15
  }),
15
- };
16
+ });
@@ -1,24 +1,23 @@
1
1
  import { type TSchema, Type } from "typebox";
2
- import { messages } from "../i18n/index.ts";
3
- import { timeZone } from "../time.ts";
2
+ import { catalogFor, type Locale } from "../i18n/index.ts";
4
3
 
5
4
  /**
6
- * Schedule tools, the same for the owner agent and party roles. Each acts on the schedules of
5
+ * Schedule tools, shared by conversation sessions. Each acts on the schedules of
7
6
  * the channel it runs in; the host decides the channel and speaker, never the model.
8
7
  */
9
- export const SCHEDULE_TOOLS = [
8
+ export const SCHEDULE_TOOLS = Object.freeze([
10
9
  "schedule_create",
11
10
  "schedule_list",
12
11
  "schedule_update",
13
12
  "schedule_cancel",
14
- ] as const;
13
+ ] as const);
15
14
  export type ScheduleToolName = (typeof SCHEDULE_TOOLS)[number];
16
15
 
17
16
  export function isScheduleTool(value: string): value is ScheduleToolName {
18
17
  return (SCHEDULE_TOOLS as readonly string[]).includes(value);
19
18
  }
20
19
 
21
- const timing = () => ({
20
+ const timing = (zone: string) => ({
22
21
  in_minutes: Type.Optional(
23
22
  Type.Integer({
24
23
  minimum: 1,
@@ -28,12 +27,12 @@ const timing = () => ({
28
27
  ),
29
28
  at: Type.Optional(
30
29
  Type.String({
31
- description: `Run once at this ${messages().zoneTime(timeZone())}, "YYYY-MM-DD HH:MM". Give one of in_minutes, at, or time.`,
30
+ description: `Run once at this ${zone}, "YYYY-MM-DD HH:MM". Give one of in_minutes, at, or time.`,
32
31
  }),
33
32
  ),
34
33
  time: Type.Optional(
35
34
  Type.String({
36
- description: `Repeat at this ${messages().zoneTime(timeZone())} of day, "HH:MM".`,
35
+ description: `Repeat at this ${zone} of day, "HH:MM".`,
37
36
  }),
38
37
  ),
39
38
  every_days: Type.Optional(
@@ -70,24 +69,34 @@ export interface ScheduleToolSpec {
70
69
  parameters: TSchema;
71
70
  }
72
71
 
73
- export function scheduleToolSpecs(): readonly ScheduleToolSpec[] {
72
+ /** The wording of the tools' descriptions: a locale's catalog, and the zone the times are in. */
73
+ export interface ScheduleToolWording {
74
+ locale: Locale;
75
+ /** An IANA time zone, such as `Asia/Taipei`. */
76
+ timeZone: string;
77
+ }
78
+
79
+ export function scheduleToolSpecs(
80
+ wording: ScheduleToolWording,
81
+ ): readonly ScheduleToolSpec[] {
82
+ const zone = catalogFor(wording.locale).zoneTime(wording.timeZone);
74
83
  return [
75
84
  {
76
85
  name: "schedule_create",
77
86
  label: "Create schedule",
78
- description: `Schedule a task for yourself in this channel: at the set ${messages().zoneTime(timeZone())} you are woken with the prompt and your answer is posted here. Use it for reminders, follow-ups, and recurring checks someone asks for. Write the prompt as a complete instruction to your future self, including who it is for and what to report; it runs without the current conversation in view.`,
87
+ description: `Schedule a task for yourself in this channel: at the set ${zone} you are woken with the prompt and your answer is posted here. Use it for reminders, follow-ups, and recurring checks someone asks for. Write the prompt as a complete instruction to your future self, including who it is for and what to report; it runs without the current conversation in view.`,
79
88
  parameters: Type.Object({
80
89
  title: Type.String({ description: "A short name for the schedule." }),
81
90
  prompt: Type.String({
82
91
  description: "What to do when it runs, self-contained.",
83
92
  }),
84
- ...timing(),
93
+ ...timing(zone),
85
94
  }),
86
95
  },
87
96
  {
88
97
  name: "schedule_list",
89
98
  label: "List schedules",
90
- description: `List this channel's schedules with their next run, and the current ${messages().zoneTime(timeZone())}. Give id to read one schedule's full prompt.`,
99
+ description: `List this channel's schedules with their next run, and the current ${zone}. Give id to read one schedule's full prompt.`,
91
100
  parameters: Type.Object({
92
101
  id: Type.Optional(Type.Integer({ description: "Schedule id." })),
93
102
  }),
@@ -103,7 +112,7 @@ export function scheduleToolSpecs(): readonly ScheduleToolSpec[] {
103
112
  prompt: Type.Optional(
104
113
  Type.String({ description: "The whole new prompt." }),
105
114
  ),
106
- ...timing(),
115
+ ...timing(zone),
107
116
  }),
108
117
  },
109
118
  {
@@ -1,5 +1,5 @@
1
1
  /** An assistant message of a Pi session, as far as its final answer needs it. */
2
- interface AssistantLike {
2
+ export interface AssistantLike {
3
3
  role: "assistant";
4
4
  content: unknown;
5
5
  stopReason?: string;
@@ -1,8 +1,8 @@
1
- import { ConfigError } from "./domain/errors.ts";
1
+ import { freeze } from "./freeze.ts";
2
2
  import type { OwnerIdentity } from "./identity.ts";
3
3
 
4
4
  /** How much a speaker may do: `owner` above `admin` above `member`. */
5
- export const TIERS = ["member", "admin", "owner"] as const;
5
+ export const TIERS = Object.freeze(["member", "admin", "owner"] as const);
6
6
  export type Tier = (typeof TIERS)[number];
7
7
 
8
8
  /** Whether `tier` is `least` or above. */
@@ -42,14 +42,16 @@ export function addressee(
42
42
  * the description is fixed when the session opens, so it names no one, and the prompt says who
43
43
  * the speaker is.
44
44
  */
45
- export const THE_SPEAKER: OwnerIdentity = {
45
+ export const THE_SPEAKER: Readonly<Omit<OwnerIdentity, "pronouns">> & {
46
+ readonly pronouns: Readonly<OwnerIdentity["pronouns"]>;
47
+ } = freeze({
46
48
  name: "the speaker",
47
49
  pronouns: {
48
50
  subject: "the speaker",
49
51
  object: "the speaker",
50
52
  possessive: "the speaker's",
51
53
  },
52
- };
54
+ });
53
55
 
54
56
  /** The turn's text under the speaker's name, so a conversation several speakers share says who wrote what. */
55
57
  export function attributed(speaker: Speaker, text: string): string {
@@ -107,37 +109,3 @@ export function speakerPolicy(map: SpeakerMap): SpeakerPolicy {
107
109
  },
108
110
  };
109
111
  }
110
-
111
- /**
112
- * Reads a tier's members from text: comma-separated `user:<id>`, `role:<id>`, or `@everyone`;
113
- * a bare id is a user. `allowEveryone` is false for tiers that cannot be everyone.
114
- */
115
- export function parseTierMembers(
116
- text: string | undefined,
117
- name: string,
118
- allowEveryone: boolean,
119
- ): TierMembers | undefined {
120
- const items = (text ?? "")
121
- .split(",")
122
- .map((item) => item.trim())
123
- .filter((item) => item !== "");
124
- if (items.length === 0) return undefined;
125
- const users: string[] = [];
126
- const roles: string[] = [];
127
- let everyone = false;
128
- for (const item of items) {
129
- if (item === "@everyone") {
130
- if (!allowEveryone)
131
- throw new ConfigError(`${name} cannot include @everyone`);
132
- everyone = true;
133
- continue;
134
- }
135
- const [kind, id] = item.includes(":") ? item.split(":", 2) : ["user", item];
136
- if ((kind !== "user" && kind !== "role") || !/^\d{17,20}$/.test(id ?? ""))
137
- throw new ConfigError(
138
- `${name} must list user:<id>, role:<id>, or @everyone, got ${item}`,
139
- );
140
- (kind === "role" ? roles : users).push(id as string);
141
- }
142
- return { users, roles, everyone };
143
- }