pi-roundtable 0.7.19 → 0.9.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 (177) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -15
  3. package/README.zh-TW.md +65 -13
  4. package/docs/migrating-0.8.md +132 -0
  5. package/docs/migration-0.9.md +247 -0
  6. package/docs/plugins.md +853 -81
  7. package/examples/channel-context.test.ts +88 -0
  8. package/examples/channel-context.ts +62 -0
  9. package/examples/discord-adapter.test.ts +42 -0
  10. package/examples/discord-adapter.ts +26 -0
  11. package/examples/echo-runtime.test.ts +1 -0
  12. package/examples/echo-runtime.ts +1 -1
  13. package/examples/fake-surface.test.ts +69 -6
  14. package/examples/fake-surface.ts +73 -7
  15. package/examples/headless.test.ts +72 -0
  16. package/examples/headless.ts +35 -0
  17. package/examples/http.test.ts +31 -1
  18. package/examples/http.ts +39 -0
  19. package/examples/prechecks.test.ts +1 -0
  20. package/examples/principals.test.ts +60 -0
  21. package/examples/principals.ts +48 -0
  22. package/examples/prompt.test.ts +1 -1
  23. package/examples/study-room.test.ts +29 -1
  24. package/examples/study-room.ts +16 -19
  25. package/examples/support-desk.test.ts +3 -1
  26. package/examples/tools.test.ts +6 -1
  27. package/package.json +7 -3
  28. package/src/cli/checks/database.ts +11 -0
  29. package/src/cli/checks/identity.ts +225 -0
  30. package/src/cli/checks/model.ts +21 -3
  31. package/src/cli/cli.ts +130 -5
  32. package/src/cli/config-edit.ts +83 -13
  33. package/src/cli/doctor.ts +28 -5
  34. package/src/cli/init.ts +9 -1
  35. package/src/cli/line-diff.ts +72 -0
  36. package/src/cli/principal.ts +412 -0
  37. package/src/cli/project.ts +33 -2
  38. package/src/cli/report.ts +12 -3
  39. package/src/cli/templates.ts +46 -14
  40. package/src/cli/upgrade-rewrite.ts +102 -0
  41. package/src/cli/upgrade-source.ts +430 -0
  42. package/src/cli/upgrade.ts +176 -0
  43. package/src/core/agents/agent-claim.ts +56 -34
  44. package/src/core/agents/agent-team-fixture.ts +6 -0
  45. package/src/core/agents/team-turns.ts +10 -1
  46. package/src/core/builtin/agent-server.ts +38 -101
  47. package/src/core/builtin/discord-admin.ts +4 -1
  48. package/src/core/builtin/discord.ts +71 -13
  49. package/src/core/builtin/modules.ts +197 -43
  50. package/src/core/builtin/stores.ts +40 -21
  51. package/src/core/config/access.ts +229 -0
  52. package/src/core/config/adapters.ts +120 -0
  53. package/src/core/config/config.ts +228 -94
  54. package/src/core/contract/channel-context.ts +53 -0
  55. package/src/core/contract/channels.ts +81 -9
  56. package/src/core/contract/providers.ts +3 -2
  57. package/src/core/contract/runtime.ts +31 -11
  58. package/src/core/contract/surface.ts +35 -6
  59. package/src/core/conversations/conversation-registry.ts +56 -0
  60. package/src/core/conversations/conversation-store.ts +126 -0
  61. package/src/core/conversations/conversations-plugin.ts +29 -0
  62. package/src/core/define-roundtable.ts +157 -57
  63. package/src/core/discord/agent-panel.ts +2 -2
  64. package/src/core/discord/card-audience.ts +148 -0
  65. package/src/core/discord/channel-context.ts +348 -0
  66. package/src/core/discord/connection.ts +15 -3
  67. package/src/core/discord/direct-channel.ts +54 -0
  68. package/src/core/discord/discord-adapter.ts +9 -0
  69. package/src/core/discord/discord-owners.ts +133 -0
  70. package/src/core/discord/discord-surface.ts +69 -14
  71. package/src/core/discord/fresh-marker.ts +24 -0
  72. package/src/core/discord/inbound-message.ts +21 -7
  73. package/src/core/discord/interaction-module.ts +11 -1
  74. package/src/core/discord/owner-cards-fixture.ts +80 -0
  75. package/src/core/discord/owner-cards.ts +58 -86
  76. package/src/core/discord/owner-command.ts +27 -13
  77. package/src/core/discord/stop-button.ts +4 -4
  78. package/src/core/domain/conversation.ts +10 -0
  79. package/src/core/domain/errors.ts +5 -0
  80. package/src/core/domain/ports.ts +33 -2
  81. package/src/core/domain/progress.ts +10 -0
  82. package/src/core/events.ts +1 -0
  83. package/src/core/host.ts +32 -10
  84. package/src/core/http/listeners.ts +174 -21
  85. package/src/core/http/websocket.ts +316 -0
  86. package/src/core/i18n/cards.ts +11 -9
  87. package/src/core/identity/access-policy.ts +227 -0
  88. package/src/core/identity/actor-facts.ts +65 -0
  89. package/src/core/identity/caching-principal-store.ts +111 -0
  90. package/src/core/identity/config-owner.ts +145 -0
  91. package/src/core/identity/contact.ts +27 -0
  92. package/src/core/identity/identity-plugin.ts +56 -0
  93. package/src/core/identity/identity-schema.ts +282 -0
  94. package/src/core/identity/identity-service.ts +489 -0
  95. package/src/core/identity/identity-view.ts +77 -0
  96. package/src/core/identity/plugin-identities.ts +109 -0
  97. package/src/core/identity/principal-reads.ts +38 -0
  98. package/src/core/identity/principal-store.ts +478 -0
  99. package/src/core/identity/principal-tables.ts +53 -0
  100. package/src/core/identity/seen-throttle.ts +57 -0
  101. package/src/core/identity/ulid.ts +36 -0
  102. package/src/core/identity.ts +34 -5
  103. package/src/core/interactions/prompts.ts +127 -0
  104. package/src/core/modules/background/background-turns.ts +56 -17
  105. package/src/core/modules/background/personal-target.ts +50 -0
  106. package/src/core/modules/delegation/delegate.ts +28 -14
  107. package/src/core/modules/delegation/delegator.ts +23 -7
  108. package/src/core/modules/memory/owner-memory.ts +123 -50
  109. package/src/core/modules/notify/notify.ts +53 -13
  110. package/src/core/modules/notify/session-notify.ts +110 -0
  111. package/src/core/modules/schedules/prechecks.ts +8 -2
  112. package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
  113. package/src/core/modules/schedules/schedule-store.ts +51 -20
  114. package/src/core/modules/schedules/schedule-tools.ts +122 -12
  115. package/src/core/modules/schedules/scheduler.ts +43 -2
  116. package/src/core/modules/schedules/schedules.ts +43 -14
  117. package/src/core/ops/error-reporter.ts +24 -12
  118. package/src/core/plugin.ts +62 -0
  119. package/src/core/presence/direct-channels.ts +104 -0
  120. package/src/core/registry/contributions.ts +63 -3
  121. package/src/core/registry/providers.ts +1 -1
  122. package/src/core/routing/channel-router.ts +226 -17
  123. package/src/core/routing/conversation-turns.ts +85 -1
  124. package/src/core/routing/surface-port.ts +35 -2
  125. package/src/core/routing/system-turns.ts +29 -0
  126. package/src/core/runtime/bridge-guard.ts +225 -0
  127. package/src/core/runtime/compaction-tiers.ts +5 -1
  128. package/src/core/runtime/conversation-sessions.ts +124 -12
  129. package/src/core/runtime/extensions/ask-user.ts +1 -1
  130. package/src/core/runtime/extensions/confirmation-gate.ts +32 -10
  131. package/src/core/runtime/extensions/private-memory.ts +447 -0
  132. package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
  133. package/src/core/runtime/pending-confirmation-store.ts +67 -4
  134. package/src/core/runtime/pi-agent-runtime.ts +143 -135
  135. package/src/core/runtime/preflight-tools.ts +47 -0
  136. package/src/core/runtime/prompt-slot.ts +6 -6
  137. package/src/core/runtime/reader-history.ts +129 -0
  138. package/src/core/runtime/runtime-plugin.ts +205 -0
  139. package/src/core/runtime/runtime-types.ts +40 -12
  140. package/src/core/runtime/session-conversation.ts +143 -0
  141. package/src/core/runtime/session-factory.ts +115 -15
  142. package/src/core/runtime/session-scope.ts +78 -0
  143. package/src/core/runtime/steerable-run.ts +9 -0
  144. package/src/core/runtime/tool-call-scope.ts +43 -0
  145. package/src/core/runtime/turn-answer.ts +30 -1
  146. package/src/core/runtime/turn-progress.ts +116 -0
  147. package/src/core/services.ts +42 -2
  148. package/src/core/sessions.ts +30 -0
  149. package/src/core/shared/unix-server.ts +35 -5
  150. package/src/core/speakers.ts +21 -19
  151. package/src/core/testing/background.ts +31 -0
  152. package/src/core/testing/fixture-database.ts +55 -0
  153. package/src/core/testing/map-identity.ts +76 -0
  154. package/src/core/testing/module-sessions.ts +134 -0
  155. package/src/core/testing/modules.ts +149 -38
  156. package/src/core/testing/owner.ts +17 -0
  157. package/src/core/testing/surface-contract.ts +460 -0
  158. package/src/core/testing/test-host.ts +100 -19
  159. package/src/core/testing/tool-set.ts +32 -0
  160. package/src/core/tool-set.snapshot.json +4 -4
  161. package/src/core/tool-tiers.ts +57 -3
  162. package/src/discord/index.ts +10 -0
  163. package/src/index.ts +65 -8
  164. package/src/kit/compaction.ts +13 -0
  165. package/src/kit/index.ts +10 -0
  166. package/src/kit/support.ts +1 -1
  167. package/src/testing.ts +96 -21
  168. package/templates/.env.example +1 -1
  169. package/templates/adapters/web/.env.example +24 -0
  170. package/templates/adapters/web/README.md +22 -0
  171. package/templates/adapters/web/package.json.tmpl +26 -0
  172. package/templates/adapters/web/persona/assistant.md +2 -0
  173. package/templates/adapters/web/roundtable.config.ts +59 -0
  174. package/templates/package.json.tmpl +1 -0
  175. package/templates/plugins/self-compact.ts +11 -0
  176. package/templates/roundtable.config.ts +14 -2
  177. package/src/core/domain/owner-prompts.ts +0 -45
@@ -1,4 +1,8 @@
1
1
  import { attachmentsOf } from "../attachments/turn-attachments.ts";
2
+ import {
3
+ type ChannelContext,
4
+ withChannelContext,
5
+ } from "../contract/channel-context.ts";
2
6
  import type {
3
7
  Admission,
4
8
  BackgroundTarget,
@@ -8,12 +12,13 @@ import type {
8
12
  import type { AgentRuntime } from "../contract/runtime.ts";
9
13
  import type { SurfacePort } from "../contract/surface.ts";
10
14
  import type { ChannelKey } from "../domain/conversation.ts";
11
- import { messages } from "../i18n/index.ts";
12
15
  import type { OwnerIdentity } from "../identity.ts";
13
16
  import type { Logger } from "../log.ts";
17
+ import { PERSONAL_TARGET } from "../modules/background/personal-target.ts";
14
18
  import { withReference } from "../routing/message-text.ts";
15
19
  import { outcome } from "../routing/settle-turn.ts";
16
- import { addressee, attributed, type SpeakerPolicy } from "../speakers.ts";
20
+ import { systemTurn } from "../routing/system-turns.ts";
21
+ import { addressee, attributed } from "../speakers.ts";
17
22
  import type { DiscordAgentTeam } from "./agent-team.ts";
18
23
  import { discordIdOf } from "./team-keys.ts";
19
24
 
@@ -27,23 +32,15 @@ import { discordIdOf } from "./team-keys.ts";
27
32
  export const AGENT_SERVER_PRIORITY = 100;
28
33
 
29
34
  /**
30
- * The host's own background target: the conversations of the owner and of the agent server. The
31
- * agent server contributes it, so schedules and delegated tasks made in those conversations carry
32
- * its name and limits. Its label is read when a list is shown, never when this module loads,
33
- * because the host applies its catalog at startup.
35
+ * The host's own background target, which the agent server answers.
36
+ * @deprecated Use `PERSONAL_TARGET`, the same target, which the core contributes on every host;
37
+ * this name goes away in 1.0.
34
38
  */
35
- export const OWNER_TARGET: BackgroundTarget = {
36
- name: "owner",
37
- label: () => messages().scheduleModeOwner,
38
- schedules: { perChannel: 20, promptChars: 8_000, aheadDays: 366 },
39
- delegation: { maxRunning: 3 },
40
- };
39
+ export const OWNER_TARGET: BackgroundTarget = PERSONAL_TARGET;
41
40
 
42
41
  export interface AgentClaimOptions {
43
42
  /** The owner, as forwarded messages name them. */
44
43
  owner: OwnerIdentity;
45
- /** Who may talk to the agents, and at which tier. */
46
- speakers: SpeakerPolicy;
47
44
  team: Pick<
48
45
  DiscordAgentTeam,
49
46
  | "guildId"
@@ -60,6 +57,13 @@ export interface AgentClaimOptions {
60
57
  logger: Logger;
61
58
  /** Replaceable in tests. */
62
59
  fetchImpl?: typeof fetch;
60
+ /**
61
+ * What was said in an agent's channel since its last post, `DISCORD.channelContext`; without it
62
+ * the turns carry none. A group's round carries what was said since its last turn already.
63
+ */
64
+ channelContext?: (
65
+ message: InboundMessage,
66
+ ) => Promise<ChannelContext | undefined>;
63
67
  }
64
68
 
65
69
  /**
@@ -68,24 +72,39 @@ export interface AgentClaimOptions {
68
72
  * failure notice, is a labelled report turn for the agent.
69
73
  */
70
74
  export function agentClaim(options: AgentClaimOptions): ChannelClaim {
71
- const { speakers, team, runtime, surface, attachmentDir, logger, fetchImpl } =
72
- options;
75
+ const { team, runtime, surface, attachmentDir, logger, fetchImpl } = options;
73
76
  const attachments = (message: InboundMessage) =>
74
77
  attachmentsOf(message, attachmentDir(message.channel), logger, fetchImpl);
78
+ /** The channel context of a message in an agent's channel; none, logged, when it cannot be read. */
79
+ const around = async (
80
+ message: InboundMessage,
81
+ ): Promise<ChannelContext | undefined> => {
82
+ try {
83
+ return await options.channelContext?.(message);
84
+ } catch (error) {
85
+ logger.warn(
86
+ { channel: message.channel, err: error },
87
+ "channel context not read; the turn runs without it",
88
+ );
89
+ return undefined;
90
+ }
91
+ };
75
92
 
76
93
  const webhookReport = (message: InboundMessage): Admission | undefined => {
77
94
  const text = message.text.trim();
78
95
  if (!text) return undefined;
79
96
  return {
80
97
  kind: "background",
81
- turn: {
98
+ // The host's own turn, at the owner tier as in 0.8; what the webhook posted is untrusted input.
99
+ turn: systemTurn({
82
100
  channel: message.channel,
83
- target: OWNER_TARGET.name,
101
+ target: PERSONAL_TARGET.name,
84
102
  author: { id: message.authorId, name: message.authorName },
103
+ tier: "owner",
85
104
  turnId: `webhook-${message.messageId}`,
86
105
  text: `Webhook "${message.authorName}" posted in your channel:\n${text}`,
87
106
  report: true,
88
- },
107
+ }),
89
108
  unanswered: (result) =>
90
109
  logger.warn(
91
110
  {
@@ -112,13 +131,9 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
112
131
  if (!owned) return undefined;
113
132
  if (owned === "agent" && message.integration && !message.integration.own)
114
133
  return webhookReport(message);
115
- if (message.authorIsBot) return undefined;
116
- const speaker = speakers.resolve({
117
- id: message.authorId,
118
- name: message.authorName,
119
- ...(message.authorRoleIds ? { roleIds: message.authorRoleIds } : {}),
120
- });
121
- if (!speaker) return undefined;
134
+ // Who may talk to the agents, and at which tier, the router resolved through the identity service.
135
+ const { speaker } = message;
136
+ if (message.authorIsBot || !speaker) return undefined;
122
137
  const { channel, messageId } = message;
123
138
  const forwarder = addressee(speaker, options.owner);
124
139
  return {
@@ -147,7 +162,10 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
147
162
  await team.answerOwner(
148
163
  channel,
149
164
  speaker,
150
- attributed(speaker, text),
165
+ attributed(
166
+ speaker,
167
+ withChannelContext(text, await around(message)),
168
+ ),
151
169
  message.text,
152
170
  saved,
153
171
  );
@@ -166,22 +184,26 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
166
184
  },
167
185
  background: async (turn) => {
168
186
  // Fail closed: a turn for any target but the owner's never reaches the owner's tools.
169
- if (turn.target !== OWNER_TARGET.name)
187
+ if (turn.target !== PERSONAL_TARGET.name)
170
188
  return {
171
189
  status: "skipped",
172
- reason: `the agent server answers only "${OWNER_TARGET.name}" background turns`,
190
+ reason: `the agent server answers only "${PERSONAL_TARGET.name}" background turns`,
173
191
  };
174
192
  const owned = team.owns(turn.channel);
175
193
  if (owned === "group")
176
194
  return { status: "skipped", reason: "groups have no schedules" };
195
+ // Who the turn runs as, checked by the router: its author's principal, at a tier they hold.
196
+ const { speaker } = turn;
197
+ if (!speaker)
198
+ return {
199
+ status: "skipped",
200
+ reason:
201
+ "a background turn reaches the agent server through the router, which says whom it runs as",
202
+ };
177
203
  return outcome(
178
204
  await team.answerBackground(
179
205
  turn.channel,
180
- {
181
- id: turn.author.id,
182
- name: turn.author.name,
183
- tier: turn.tier ?? "owner",
184
- },
206
+ speaker,
185
207
  turn.text,
186
208
  turn.report === true,
187
209
  ),
@@ -25,6 +25,7 @@ import {
25
25
  } from "../testing/database.ts";
26
26
  import { useTestLocale } from "../testing/locale.ts";
27
27
  import { OWNER_SPEAKER, TEST_OWNER } from "../testing/owner.ts";
28
+ import { toolTiers } from "../tool-tiers.ts";
28
29
  import type {
29
30
  AgentCategory,
30
31
  AgentChannels,
@@ -176,6 +177,10 @@ export interface TeamFixture {
176
177
  skills: SkillRegistry;
177
178
  }
178
179
 
180
+ /** The tool tiers the team reads; `deploy` is an admin's tool, so an admin may approve it. */
181
+ const FIXTURE_TIERS = toolTiers();
182
+ FIXTURE_TIERS.declare("test", { deploy: "admin" });
183
+
179
184
  /** The fixture of the running test; set by `useTeamFixture` before each test. */
180
185
  export const fx = {} as TeamFixture;
181
186
 
@@ -297,6 +302,7 @@ export function useTeamFixture(): void {
297
302
  eventLog.length = 0;
298
303
  team = new AgentTeam({
299
304
  owner: TEST_OWNER,
305
+ toolTiers: FIXTURE_TIERS,
300
306
  shellUser: "mcops",
301
307
  guildId: "1",
302
308
  entryChannelId: ENTRY,
@@ -143,8 +143,17 @@ export class TeamTurns {
143
143
  );
144
144
  }
145
145
 
146
- /** Whether the speaker's tier holds every tool of the held actions. */
146
+ /**
147
+ * Whether the speaker may approve the held actions by a message: an owner, as the agents'
148
+ * channels are shared, or the principal whose turn held them (by their id, for actions held
149
+ * before 0.9), when their tier now holds every tool. Nobody else in the channel may.
150
+ */
147
151
  #mayApprove(speaker: Speaker, pending: PendingConfirmation): boolean {
152
+ const theirs =
153
+ pending.principalId !== undefined
154
+ ? speaker.principalId === pending.principalId
155
+ : speaker.id === pending.speakerId;
156
+ if (speaker.tier !== "owner" && !theirs) return false;
148
157
  const tiers = this.#options.toolTiers ?? toolTiers();
149
158
  return pending.calls.every(
150
159
  (call) =>
@@ -2,7 +2,7 @@ import { chmodSync, lstatSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
4
  import type { SQL } from "bun";
5
- import { agentClaim, OWNER_TARGET } from "../agents/agent-claim.ts";
5
+ import { agentClaim } from "../agents/agent-claim.ts";
6
6
  import { AgentDashboard } from "../agents/agent-dashboard.ts";
7
7
  import { PgAgentStore } from "../agents/agent-store.ts";
8
8
  import { DiscordAgentTeam } from "../agents/agent-team.ts";
@@ -11,28 +11,26 @@ import { FileAvatarStudio } from "../agents/avatar-studio.ts";
11
11
  import { RelevanceScorer } from "../agents/group-round.ts";
12
12
  import { discordKey } from "../agents/team-keys.ts";
13
13
  import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
14
- import type { AgentRuntime, AgentSessions } from "../contract/runtime.ts";
14
+ import type { AgentSessions } from "../contract/runtime.ts";
15
15
  import type { ChannelKey } from "../domain/conversation.ts";
16
16
  import { ConfigError } from "../domain/errors.ts";
17
- import type { InterimTextMode } from "../domain/interim.ts";
18
17
  import type { OwnerIdentity } from "../identity.ts";
19
18
  import { ConfirmationJudge } from "../judging/confirmation-judge.ts";
20
- import { AGENT_BRIEF, EffortJudge } from "../judging/effort-judge.ts";
21
- import {
22
- AUTO_THINKING,
23
- formatModelRef,
24
- type ModelRef,
25
- type ThinkingLevel,
26
- } from "../models.ts";
19
+ import { AUTO_THINKING, formatModelRef, type ModelRef } from "../models.ts";
27
20
  import type { ErrorReporter } from "../ops/error-reporter.ts";
28
21
  import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
29
22
  import { splitReply } from "../presentation/reply-splitter.ts";
30
23
  import { agentPromptExtension } from "../runtime/extensions/agent-prompt.ts";
31
- import { PendingConfirmationStore } from "../runtime/pending-confirmation-store.ts";
32
- import { PiAgentRuntime } from "../runtime/pi-agent-runtime.ts";
33
- import { AGENTS, BACKGROUND_TURNS, SCHEDULES, SKILLS } from "../services.ts";
24
+ import type { AgentSessionsSlot } from "../runtime/runtime-plugin.ts";
25
+ import {
26
+ AGENTS,
27
+ BACKGROUND_TURNS,
28
+ RUNTIME,
29
+ SCHEDULES,
30
+ SKILLS,
31
+ } from "../services.ts";
34
32
  import type { SessionTool } from "../sessions.ts";
35
- import type { Speaker, SpeakerPolicy, Tier } from "../speakers.ts";
33
+ import type { Tier } from "../speakers.ts";
36
34
  import { DISCORD } from "./discord.ts";
37
35
  import { agentOnly } from "./session-tool.ts";
38
36
 
@@ -43,17 +41,15 @@ export interface AgentServerOptions {
43
41
  owner: OwnerIdentity & { id: string };
44
42
  /** The assistant's display name, as the confirmation judge names it. */
45
43
  assistant: string;
46
- /** Who may talk to the agents, and at which tier. */
47
- speakers: SpeakerPolicy;
48
- /** Shared with the rest of the host, so logins refresh in one place. */
44
+ /** Shared with the rest of the host, so logins refresh in one place; lists the models an agent may run. */
49
45
  modelRuntime: ModelRuntime;
50
- agentDir: string;
51
46
  dataDir: string;
52
- /** The model of agents and of the assistant, and the thinking level a judge falls back to. */
47
+ /** The model of agents and of the assistant. */
53
48
  model: ModelRef;
54
- thinking: ThinkingLevel;
55
- /** How sure the judges must be before their answer is used. */
49
+ /** How sure the confirmation judge must be before its answer is used. */
56
50
  judgeThreshold: number;
51
+ /** Where the agent server hands the runtime plugin its per-agent settings. */
52
+ agents?: AgentSessionsSlot;
57
53
  /** The shell's shared working directory. */
58
54
  workDir: string;
59
55
  /**
@@ -63,6 +59,11 @@ export interface AgentServerOptions {
63
59
  scratchDir?: string;
64
60
  /** The host account the agents' shell and file tools run as. */
65
61
  shellUser: string;
62
+ /**
63
+ * Whose turns in the agents' channels load memory: `"everyone"` (the default) or `"owners"`;
64
+ * see `AgentSessions.memory`.
65
+ */
66
+ memory?: "everyone" | "owners";
66
67
  /** The prompt every agent starts with, and the one for speakers other than the owner. */
67
68
  prompts: { shared: string; guest?: string };
68
69
  /** The listener the avatar pictures are served on. */
@@ -73,10 +74,6 @@ export interface AgentServerOptions {
73
74
  avatarReference: string;
74
75
  /** Reports the process's own errors to an agent; without one nothing is reported. */
75
76
  errorReporter?: ErrorReporter;
76
- /** Whether turns post the text they write before their final answer as they go; default "on". */
77
- interimText?: InterimTextMode;
78
- /** An intermediate text this long or longer is posted as an ordinary message; default 400. */
79
- interimPrimaryChars?: number;
80
77
  }
81
78
 
82
79
  /** The name of the agent server's plugin, as `serviceStarted` events name it. */
@@ -109,11 +106,9 @@ const AGENT_TIERS: Readonly<Record<string, Tier>> = {
109
106
  channel_read: "member",
110
107
  };
111
108
 
112
- /** The tables the agent server keeps: its agents and groups, and the held actions. */
109
+ /** The tables the agent server keeps: its agents and groups. */
113
110
  export interface AgentServerStores {
114
111
  agents: PgAgentStore;
115
- /** Held actions, kept across a restart. */
116
- confirmations: PendingConfirmationStore;
117
112
  }
118
113
 
119
114
  /** Attaches the agent server's stores over the host's migrated pool. */
@@ -121,16 +116,14 @@ async function attachStores(
121
116
  sql: SQL,
122
117
  guildId: string,
123
118
  ): Promise<AgentServerStores> {
124
- return {
125
- agents: await PgAgentStore.attach(sql, guildId),
126
- confirmations: await PendingConfirmationStore.attach(sql),
127
- };
119
+ return { agents: await PgAgentStore.attach(sql, guildId) };
128
120
  }
129
121
 
130
122
  /**
131
- * The agent server: the team with its channels and dashboard, and the runtime that runs every
132
- * agent turn. The skills the agents carry come from the skills addon when it is on. It claims the agent channels, starts the team in the
133
- * background, and hands what it built to the plugins after it.
123
+ * The agent server: the team with its channels and dashboard, run on the runtime plugin's runtime,
124
+ * which it hands its per-agent settings. The skills the agents carry come from the skills addon
125
+ * when it is on. It claims the agent channels, starts the team in the background, and hands what
126
+ * it built to the plugins after it.
134
127
  */
135
128
  export function agentServerPlugin(
136
129
  options: AgentServerOptions,
@@ -141,18 +134,13 @@ export function agentServerPlugin(
141
134
  ) => Promise<AgentServerStores> = (context, guildId) =>
142
135
  attachStores(context.database(), guildId),
143
136
  ): RoundtablePlugin {
144
- let runtime: AgentRuntime | undefined;
145
137
  return {
146
138
  name: AGENT_SERVER_PLUGIN,
147
- // The agent server's own tables; the host runs them before any setup, in this order.
148
- migrations: [
149
- PendingConfirmationStore.migration,
150
- ...PgAgentStore.migrations(options.guildId),
151
- ],
139
+ // The agent server's own tables; the host runs them before any setup, in this order. The
140
+ // held actions' table is the runtime plugin's.
141
+ migrations: PgAgentStore.migrations(options.guildId),
152
142
  provides: [AGENTS],
153
- preflight: async () => {
154
- await runtime?.preflight?.();
155
- },
143
+ requires: [RUNTIME],
156
144
  setup: async (context) => {
157
145
  const discord = context.services.get(DISCORD);
158
146
  const schedules = context.services.get(SCHEDULES);
@@ -168,10 +156,8 @@ export function agentServerPlugin(
168
156
  await studio.init();
169
157
  // Absent when the skills addon is off: the agents then carry none.
170
158
  const skills = context.services.find(SKILLS);
171
- const { agents: store, confirmations: heldActions } = await openStores(
172
- context,
173
- options.guildId,
174
- );
159
+ const { agents: store } = await openStores(context, options.guildId);
160
+ const running = context.services.get(RUNTIME);
175
161
  const judge = providers.judge;
176
162
  const confirmations = new ConfirmationJudge({
177
163
  judge,
@@ -194,11 +180,7 @@ export function agentServerPlugin(
194
180
  store,
195
181
  channels: connection.agentChannels(options.guildId),
196
182
  studio,
197
- // The runtime is built next, with this team's tools.
198
- runtime: () => {
199
- if (!runtime) throw new Error("the runtime is built with the team");
200
- return runtime;
201
- },
183
+ runtime: () => running,
202
184
  confirmations,
203
185
  scorer: new RelevanceScorer(judge, logger, options.owner),
204
186
  models: {
@@ -246,51 +228,9 @@ export function agentServerPlugin(
246
228
  modelOf: (name) => built.modelOf(name),
247
229
  skills: (name) => built.skillsOf(name),
248
230
  turnChannel: (scope) => built.turnChannel(scope),
231
+ ...(options.memory ? { memory: options.memory } : {}),
249
232
  };
250
- const prompts = (channel: ChannelKey, speaker?: Speaker) =>
251
- context.surfaces.prompts(channel, speaker);
252
- // A plugin that fills the runtime slot replaces the whole conversation runtime; without
253
- // one the agent server runs Pi, built from the options below.
254
- const running: AgentRuntime = providers.filled.has("runtime")
255
- ? providers.runtime({
256
- logger,
257
- env: context.env,
258
- owner: { id: options.owner.id, name: options.owner.name },
259
- sessions: context.sessions,
260
- toolTiers: context.toolTiers,
261
- prompts,
262
- agents: agentSessions,
263
- confirmations: heldActions,
264
- judge,
265
- })
266
- : new PiAgentRuntime({
267
- owner: options.owner,
268
- sessions: context.sessions,
269
- agentDir: options.agentDir,
270
- modelRuntime: options.modelRuntime,
271
- dataDir: options.dataDir,
272
- model: options.model,
273
- thinking: options.thinking,
274
- effort: new EffortJudge({
275
- judge,
276
- brief: AGENT_BRIEF,
277
- fallback: options.thinking,
278
- threshold: options.judgeThreshold,
279
- logger,
280
- }),
281
- confirmations: heldActions,
282
- toolTiers: context.toolTiers,
283
- agents: agentSessions,
284
- prompts,
285
- logger,
286
- ...(options.interimText
287
- ? { interimText: options.interimText }
288
- : {}),
289
- ...(options.interimPrimaryChars
290
- ? { interimPrimaryChars: options.interimPrimaryChars }
291
- : {}),
292
- });
293
- runtime = running;
233
+ options.agents?.bind(agentSessions);
294
234
  context.services.provide(AGENTS, {
295
235
  team: built,
296
236
  directory: store,
@@ -325,7 +265,6 @@ export function agentServerPlugin(
325
265
  });
326
266
  return {
327
267
  services: [
328
- { name: "runtime", stop: () => running.dispose?.() },
329
268
  { name: "dashboard", stop: () => dashboard.stop() },
330
269
  {
331
270
  name: AGENT_TEAM_SERVICE,
@@ -341,17 +280,15 @@ export function agentServerPlugin(
341
280
  },
342
281
  },
343
282
  ],
344
- // The schedules and delegated tasks of the owner's and the agents' conversations are its own.
345
- backgroundTargets: [OWNER_TARGET],
346
283
  channels: [
347
284
  agentClaim({
348
285
  owner: options.owner,
349
- speakers: options.speakers,
350
286
  team: built,
351
287
  runtime: running,
352
288
  surface: context.surfaces,
353
289
  attachmentDir,
354
290
  logger,
291
+ channelContext: (message) => discord.channelContext(message),
355
292
  }),
356
293
  ],
357
294
  http: [studio.route(options.avatarListener)],
@@ -8,7 +8,10 @@ import { DISCORD } from "./discord.ts";
8
8
  import { fixed } from "./session-tool.ts";
9
9
 
10
10
  export interface DiscordAdminOptions {
11
- /** Whose Discord permissions the tools check, and who they serve. */
11
+ /**
12
+ * Whose Discord permissions the tools check, and who they serve: the primary owner, by their
13
+ * Discord user id, whichever owner's turn calls them.
14
+ */
12
15
  owner: OwnerIdentity & { id: string };
13
16
  }
14
17
 
@@ -1,7 +1,14 @@
1
1
  import { join } from "node:path";
2
+ import type {
3
+ ChannelContext,
4
+ ChannelContextOptions,
5
+ } from "../contract/channel-context.ts";
2
6
  import { type ServiceKey, serviceKey } from "../contract/services.ts";
7
+ import { ChannelContextReader } from "../discord/channel-context.ts";
3
8
  import { CommandCollection } from "../discord/command-collection.ts";
4
9
  import type { DiscordConnection } from "../discord/connection.ts";
10
+ import { discordDirectChannel } from "../discord/direct-channel.ts";
11
+ import { DiscordOwners } from "../discord/discord-owners.ts";
5
12
  import { DiscordSurface } from "../discord/discord-surface.ts";
6
13
  import { DispatchThreads } from "../discord/dispatch-threads.ts";
7
14
  import type {
@@ -9,20 +16,33 @@ import type {
9
16
  CommandRegistrar,
10
17
  } from "../discord/interaction-module.ts";
11
18
  import { OwnerCards } from "../discord/owner-cards.ts";
12
- import { OwnerGuard, ownerRootCommand } from "../discord/owner-command.ts";
19
+ import { commandGuard, ownerRootCommand } from "../discord/owner-command.ts";
13
20
  import { stopButtonModule } from "../discord/stop-button.ts";
21
+ import type { InboundMessage } from "../domain/conversation.ts";
14
22
  import type { RoundtablePlugin } from "../plugin.ts";
15
- import type { SpeakerPolicy } from "../speakers.ts";
23
+ import { IDENTITY } from "../services.ts";
16
24
 
17
25
  /** The Discord connection and what stands on it, as plugins use it. Provided by the Discord plugin. */
18
26
  export interface DiscordServices {
19
27
  connection: DiscordConnection;
20
28
  /** Adds a plugin's slash commands and components; during setup only. */
21
29
  commands: CommandRegistrar;
22
- /** Who may use the owner's commands. */
30
+ /** Who may use the owner's commands: every owner, by their Discord identity. */
23
31
  guard: CommandGuard;
24
32
  /** The threads that carry background reports. */
25
33
  threads: DispatchThreads;
34
+ /**
35
+ * What was said in a server channel since the assistant last posted there, before `message`:
36
+ * for a claim that answers it, to append to its turn's text with `withChannelContext`. Call it
37
+ * once per answered message, inside the turn's `run`: the messages it returns, and `message`,
38
+ * are remembered per channel and not returned again. Undefined in a direct message, on another
39
+ * surface, when the host's `discord.channelContext` is `false`, when nothing new was said, and
40
+ * when Discord cannot be read (logged). Never rejects. `options` override the host's for this call.
41
+ */
42
+ channelContext(
43
+ message: InboundMessage,
44
+ options?: ChannelContextOptions,
45
+ ): Promise<ChannelContext | undefined>;
26
46
  }
27
47
 
28
48
  /** The Discord connection, provided by the Discord plugin. */
@@ -31,16 +51,22 @@ export const DISCORD: ServiceKey<DiscordServices> =
31
51
 
32
52
  export interface DiscordOptions {
33
53
  token: string;
54
+ /**
55
+ * The primary owner's Discord user id: an owner always, and the one the admin tools act as.
56
+ * Every other owner the identity service knows uses the owner's commands and cards too.
57
+ */
34
58
  ownerId: string;
35
- /** How the owner is named in audit-log reasons and refusals. */
59
+ /** How the primary owner is named in audit-log reasons and refusals. */
36
60
  ownerName: string;
37
- /** Who may answer a card besides the owner. */
38
- speakers: SpeakerPolicy;
39
61
  dataDir: string;
40
62
  /** The name of the root slash command, without the slash. */
41
63
  rootCommand: string;
42
64
  /** Text appended as it is to the refusal a non-owner gets, such as a pointer to the commands anyone may use. */
43
65
  refusalHint?: string;
66
+ /** The host's channel context settings; `false` turns it off. Default: on, with the defaults. */
67
+ channelContext?: ChannelContextOptions | false;
68
+ /** The divider posted in a server channel when its conversation starts over; `false` posts none. Default: a short English divider. */
69
+ freshMarker?: string | false;
44
70
  }
45
71
 
46
72
  /**
@@ -62,10 +88,12 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
62
88
  );
63
89
  },
64
90
  setup: async ({ conversations, services, logger }) => {
91
+ const identity = services.find(IDENTITY);
65
92
  // The surface exists by the time a turn posts a card.
66
93
  const cards: OwnerCards = new OwnerCards({
67
94
  ownerId: options.ownerId,
68
- speakers: options.speakers,
95
+ // Who may answer a card besides the primary owner, by the identity service.
96
+ ...(identity ? { identity } : {}),
69
97
  channel: (channelId) => connected.cardChannel(channelId),
70
98
  logger,
71
99
  });
@@ -73,16 +101,23 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
73
101
  token: options.token,
74
102
  ownerId: options.ownerId,
75
103
  ownerName: options.ownerName,
76
- prompts: (channel, speaker) => cards.prompts(channel, speaker),
104
+ prompts: (channel, scope) => cards.prompts(channel, scope),
77
105
  logger,
106
+ ...(options.freshMarker === undefined
107
+ ? {}
108
+ : { freshMarker: options.freshMarker }),
78
109
  });
79
110
  surface = connected;
80
- const guard = new OwnerGuard(
81
- options.ownerId,
111
+ // Every owner may use the owner's commands, by the identity service; the primary owner always.
112
+ const guard = commandGuard({
113
+ ownerId: options.ownerId,
114
+ ...(identity ? { identity } : {}),
115
+ root: options.rootCommand,
82
116
  logger,
83
- options.rootCommand,
84
- options.refusalHint,
85
- );
117
+ ...(options.refusalHint === undefined
118
+ ? {}
119
+ : { refusalHint: options.refusalHint }),
120
+ });
86
121
  // A channel whose claim keeps reports in place, such as an open channel, opens no thread.
87
122
  const threads = new DispatchThreads({
88
123
  host: {
@@ -101,15 +136,38 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
101
136
  collection.registrar.add({
102
137
  module: stopButtonModule({ guard, conversations }),
103
138
  });
139
+ // Marks the owners in channel context: the primary owner, and every other owner's Discord identities.
140
+ const owners = new DiscordOwners({
141
+ ownerId: options.ownerId,
142
+ ...(identity ? { identity } : {}),
143
+ logger,
144
+ });
145
+ const context = new ChannelContextReader({
146
+ read: (channelId, before, limit) =>
147
+ connected.messagesBefore(channelId, before, limit),
148
+ owners: () => owners.ids(),
149
+ settings: options.channelContext ?? {},
150
+ logger,
151
+ });
104
152
  services.provide(DISCORD, {
105
153
  connection: connected,
106
154
  commands: collection.registrar,
107
155
  guard,
108
156
  threads,
157
+ channelContext: (message, given) => context.of(message, given),
109
158
  });
110
159
  return {
111
160
  // The host starts the surface first; it registers the composed commands as it connects.
112
161
  surfaces: [connected],
162
+ // A person's Discord direct messages, through a Discord identity of theirs.
163
+ directChannels: [
164
+ discordDirectChannel({
165
+ directChannel: (userId) => connected.directChannel(userId),
166
+ sendDirect: (userId, text) => connected.sendDirect(userId, text),
167
+ ...(identity ? { identity } : {}),
168
+ ownerId: options.ownerId,
169
+ }),
170
+ ],
113
171
  services: [
114
172
  // The sweep reads threads through the connection, so it follows the surface's start.
115
173
  { name: "threads", start: () => void threads.sweep() },