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
@@ -0,0 +1,76 @@
1
+ import { IdentityError } from "../domain/errors.ts";
2
+ import type { ActorFacts } from "../identity/actor-facts.ts";
3
+ import type { ContactAssessor } from "../identity/contact.ts";
4
+ import type { IdentityService } from "../identity/identity-service.ts";
5
+ import type { IdentityLink, Principal } from "../identity/principal-store.ts";
6
+ import { type SpeakerMap, speakerPolicy, tierAtLeast } from "../speakers.ts";
7
+
8
+ /**
9
+ * Who a 0.8 speaker map serves, for a test without a database: each person their own principal,
10
+ * as one carried over from 0.8 is, and nothing written; a background turn runs as the person of
11
+ * its principal id at the map's tier for their id with no roles. It resolves as the router's assessor
12
+ * and as the `resolve` of `IDENTITY`; its owners are the map's, and a person has a Discord
13
+ * identity once the map names their user id or they have been resolved through it.
14
+ */
15
+ export function mapIdentity(
16
+ map: SpeakerMap,
17
+ ): ContactAssessor &
18
+ Pick<IdentityService, "resolve" | "owners" | "identities" | "speakerFor"> {
19
+ const policy = speakerPolicy(map);
20
+ const linked = new Set([
21
+ ...map.owners,
22
+ ...(map.admins?.users ?? []),
23
+ ...(map.members?.users ?? []),
24
+ ]);
25
+ const resolve = async (facts: ActorFacts) => {
26
+ const speaker = policy.resolve({
27
+ id: facts.subject,
28
+ name: facts.name,
29
+ roleIds: (facts.roles ?? []).map((role) =>
30
+ role.replace(/^discord:role:/, ""),
31
+ ),
32
+ });
33
+ if (speaker) linked.add(speaker.id);
34
+ return speaker;
35
+ };
36
+ const principal = (id: string): Principal => ({
37
+ id,
38
+ displayName: id,
39
+ disabled: false,
40
+ });
41
+ return {
42
+ resolve,
43
+ assess: async (facts) => {
44
+ const speaker = await resolve(facts);
45
+ return speaker && { speaker, take: async () => speaker };
46
+ },
47
+ owners: async () => map.owners.map(principal),
48
+ // Whom a background turn runs as: the person of the id at the map's tier for them, or lower.
49
+ speakerFor: async (principalId, tier) => {
50
+ const own = policy.resolve({
51
+ id: principalId,
52
+ name: principalId,
53
+ roleIds: [],
54
+ })?.tier;
55
+ if (!own) throw new IdentityError(`there is no principal ${principalId}`);
56
+ return {
57
+ id: principalId,
58
+ name: principalId,
59
+ tier: tier && tierAtLeast(own, tier) ? tier : own,
60
+ principalId,
61
+ };
62
+ },
63
+ identities: async (principalId): Promise<IdentityLink[]> =>
64
+ linked.has(principalId)
65
+ ? [
66
+ {
67
+ provider: "discord",
68
+ subject: principalId,
69
+ principalId,
70
+ source: "legacy",
71
+ linkedAt: new Date(0),
72
+ },
73
+ ]
74
+ : [],
75
+ };
76
+ }
@@ -0,0 +1,134 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionFactory,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import type { OwnerIdentity } from "../identity.ts";
6
+ import type {
7
+ AgentTurnScope,
8
+ ChannelKey,
9
+ SessionContext,
10
+ } from "../sessions.ts";
11
+ import { type Speaker, THE_SPEAKER } from "../speakers.ts";
12
+ import { OWNER_CHANNEL, type setUpModules } from "./modules.ts";
13
+ import { OWNER_SPEAKER } from "./owner.ts";
14
+
15
+ /** Sessions, people, and tool calls the modules' tests share. */
16
+ export const HOME: ChannelKey = "discord:scout";
17
+ export const GROUP: ChannelKey = "discord:war-room";
18
+ export const OUTSIDE: ChannelKey = "other:table-1";
19
+
20
+ export const scout: AgentTurnScope = {
21
+ name: "scout",
22
+ session: HOME,
23
+ home: HOME,
24
+ };
25
+ export const seat: AgentTurnScope = {
26
+ name: "scout",
27
+ session: GROUP,
28
+ home: HOME,
29
+ group: "war-room",
30
+ };
31
+
32
+ export function context(
33
+ agent?: AgentTurnScope,
34
+ home?: ChannelKey,
35
+ ): SessionContext {
36
+ const session: SessionContext = {
37
+ kind: agent ? "agent" : "owner",
38
+ homeChannel: home ?? agent?.home ?? OWNER_CHANNEL,
39
+ turnChannel: agent?.session ?? home ?? OWNER_CHANNEL,
40
+ compaction: { wrap: (compactor) => compactor },
41
+ // Shared, as the host makes a session it has no record of; `privateTo` makes it someone's.
42
+ conversation: { visibility: "shared" },
43
+ addressee: THE_SPEAKER,
44
+ memory: "speaker",
45
+ // A turn of the owner's runs, as every tool call is part of one.
46
+ speaker: () => OWNER_SPEAKER,
47
+ runTask: async () => "report",
48
+ };
49
+ if (agent) session.agent = agent;
50
+ return session;
51
+ }
52
+
53
+ /** A session of a conversation of the owner's kind whose turns are someone else's. */
54
+ export function contextOf(speaker: Speaker, home: ChannelKey): SessionContext {
55
+ return { ...context(undefined, home), speaker: () => speaker };
56
+ }
57
+
58
+ /** An admin who is not the primary owner, such as one a remote MCP token is bound to. */
59
+ export const ANN: Speaker = {
60
+ id: "p_ann",
61
+ name: "Ann",
62
+ tier: "admin",
63
+ principalId: "p_ann",
64
+ };
65
+
66
+ export interface Registered {
67
+ name: string;
68
+ parameters: { properties: Record<string, unknown> };
69
+ execute(
70
+ id: string,
71
+ params: unknown,
72
+ ): Promise<{ content: { text: string }[]; isError?: boolean }>;
73
+ }
74
+
75
+ /** The extension one of the modules' session tools gives a session; null when it gives none. */
76
+ export function factoryOf(
77
+ setup: Awaited<ReturnType<typeof setUpModules>>,
78
+ name: string,
79
+ session: SessionContext,
80
+ ): ExtensionFactory | null | undefined {
81
+ return setup.contribution.sessionTools
82
+ ?.find((tool) => tool.name === name)
83
+ ?.snapshot()
84
+ .factory(session);
85
+ }
86
+
87
+ /** The tools one of the modules' extensions registers in a session. */
88
+ export async function registered(
89
+ setup: Awaited<ReturnType<typeof setUpModules>>,
90
+ session: SessionContext,
91
+ extension: string,
92
+ ): Promise<Registered[]> {
93
+ const tool = setup.contribution.sessionTools?.find(
94
+ (candidate) => candidate.name === extension,
95
+ );
96
+ const factory: ExtensionFactory | null | undefined = tool
97
+ ?.snapshot()
98
+ .factory(session);
99
+ if (!factory) throw new Error(`no ${extension} extension`);
100
+ const tools: Registered[] = [];
101
+ await factory({
102
+ registerTool: (definition: Registered) => tools.push(definition),
103
+ on: () => undefined,
104
+ } as unknown as ExtensionAPI);
105
+ return tools;
106
+ }
107
+
108
+ /** The session of a conversation private to `principalId`, its tools addressing them as given. */
109
+ export function privateTo(
110
+ session: SessionContext,
111
+ principalId: string,
112
+ addressee: OwnerIdentity = THE_SPEAKER,
113
+ ): SessionContext {
114
+ return {
115
+ ...session,
116
+ conversation: { visibility: "private", principalId },
117
+ addressee,
118
+ };
119
+ }
120
+
121
+ /** Runs notify in a session; the tool's answer and whether it was an error. */
122
+ export async function notifyIn(
123
+ setup: Awaited<ReturnType<typeof setUpModules>>,
124
+ session: SessionContext,
125
+ text = "the build is green",
126
+ ) {
127
+ const [notify] = await registered(setup, session, "notify");
128
+ if (!notify) return undefined;
129
+ const answer = await notify.execute("1", { text });
130
+ return {
131
+ text: answer.content[0]?.text ?? "",
132
+ error: answer.isError === true,
133
+ };
134
+ }
@@ -1,13 +1,16 @@
1
1
  import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
2
2
  import type { SQL } from "bun";
3
- import { OWNER_TARGET } from "../agents/agent-claim.ts";
4
3
  import { DISCORD, type DiscordServices } from "../builtin/discord.ts";
5
4
  import { discordAdminPlugin } from "../builtin/discord-admin.ts";
6
- import { modulesPlugin } from "../builtin/modules.ts";
5
+ import { type ModulesOptions, modulesPlugin } from "../builtin/modules.ts";
7
6
  import { skillsPlugin } from "../builtin/skills.ts";
8
7
  import { memoryPlugin } from "../builtin/stores.ts";
8
+ import type { ConversationRegistry } from "../conversations/conversation-registry.ts";
9
9
  import type { ChannelKey } from "../domain/conversation.ts";
10
+ import type { IdentityService } from "../identity/identity-service.ts";
11
+ import type { Principal } from "../identity/principal-store.ts";
10
12
  import { silentLogger } from "../log.ts";
13
+ import { PERSONAL_TARGET } from "../modules/background/personal-target.ts";
11
14
  import type { SkillStore } from "../modules/skills/skill-store.ts";
12
15
  import {
13
16
  type Contribution,
@@ -15,10 +18,22 @@ import {
15
18
  pluginContext,
16
19
  type RoundtablePlugin,
17
20
  } from "../plugin.ts";
21
+ import {
22
+ type DirectChannelProvider,
23
+ directChannelsPort,
24
+ } from "../presence/direct-channels.ts";
18
25
  import { ServiceRegistry } from "../registry/services.ts";
19
26
  import { surfacePort } from "../routing/surface-port.ts";
20
27
  import type { AgentServer, MemoryStore, ScheduleStore } from "../services.ts";
21
- import { AGENTS, MEMORY, SCHEDULES } from "../services.ts";
28
+ import {
29
+ AGENTS,
30
+ CONVERSATIONS,
31
+ IDENTITY,
32
+ MEMORY,
33
+ SCHEDULES,
34
+ } from "../services.ts";
35
+ import type { Tier } from "../speakers.ts";
36
+ import { toolTiers } from "../tool-tiers.ts";
22
37
 
23
38
  /** What the modules' tools did, for a test to read. */
24
39
  export interface ModuleRecord {
@@ -26,16 +41,66 @@ export interface ModuleRecord {
26
41
  threadOrigins: (ChannelKey | undefined)[];
27
42
  /** The channel each delegated report came back to. */
28
43
  reportChannels: ChannelKey[];
29
- /** How often a conversation without a chat channel fell back to the owner's messages. */
44
+ /** How often anything asked the connection for the primary owner's messages, which the modules no longer read. */
30
45
  ownerChannelAsked: number;
46
+ /** Whose direct channel was asked for, in order. */
47
+ directAsked: string[];
48
+ /** The notices sent through a direct channel, by principal. */
49
+ notified: { principalId: string; text: string }[];
50
+ /** What was posted on the surface, by channel. */
51
+ posted: { channel: ChannelKey; text: string }[];
31
52
  }
32
53
 
33
54
  export const OWNER_CHANNEL: ChannelKey = "discord:owner-dm";
34
55
 
56
+ /** The primary owner the modules serve: principal "1". */
57
+ export const MODULES_OWNER = {
58
+ id: "1",
59
+ name: "Owner",
60
+ pronouns: { subject: "they", object: "them", possessive: "their" },
61
+ };
62
+
63
+ /** The direct messages the stand-in Discord provider reaches by default: the owner's, principal "1". */
64
+ const DIRECT: Readonly<Record<string, ChannelKey>> = { "1": OWNER_CHANNEL };
65
+
35
66
  /** The modules plugin set up over stand-in stores and surface; returns its contribution and the record. */
36
67
  export async function setUpModules(
37
- options: { agentChannelOf?: (name: string) => ChannelKey } = {},
68
+ options: {
69
+ agentChannelOf?: (name: string) => ChannelKey;
70
+ /** Whether Discord is there; default true. A host without it has no owner's messages. */
71
+ discord?: boolean;
72
+ /** Whether a plugin contributes the owner's background target, as the agent server does; default true. */
73
+ ownerTarget?: boolean;
74
+ /** Whether a claim owns a channel; by default the Discord channels are owned and no other. */
75
+ owns?: (channel: ChannelKey) => boolean;
76
+ /** Whether the claim owning a channel takes background turns; by default every owned channel's does. */
77
+ takesBackground?: (channel: ChannelKey) => boolean;
78
+ /** Whether the claim owning a channel takes the host's own turns; by default every owned channel's does. */
79
+ takesSystemReports?: (channel: ChannelKey) => boolean;
80
+ errorReporter?: ModulesOptions["errorReporter"];
81
+ /** The schedules the tools keep; by default a stub that answers nothing. */
82
+ schedules?: ScheduleStore;
83
+ /** The conversations the host records, which say whose a conversation is; by default none is recorded. */
84
+ conversations?: Pick<ConversationRegistry, "get">;
85
+ /**
86
+ * Each principal's direct messages on the stand-in Discord provider, the host's only direct
87
+ * channel; by default the owner's are `OWNER_CHANNEL`, and without Discord there is none.
88
+ */
89
+ direct?: Readonly<Record<string, ChannelKey>>;
90
+ /**
91
+ * Whether the stand-in provider reaches the network, read at each `reaches`, which throws
92
+ * while it does not; `knows` answers from its records either way. Default always.
93
+ */
94
+ online?: () => boolean;
95
+ /** The host's owners, as `IDENTITY` lists them; by default there is no identity service. */
96
+ owners?: readonly Principal[];
97
+ /** The operator's tool tiers; by default none, so notify stays the owners'. */
98
+ toolTiers?: Readonly<Record<string, Tier>>;
99
+ /** The modules' options besides the test's own. */
100
+ modules?: Partial<ModulesOptions>;
101
+ } = {},
38
102
  ): Promise<{
103
+ plugin: RoundtablePlugin;
39
104
  contribution: Contribution;
40
105
  record: ModuleRecord;
41
106
  services: ServiceRegistry;
@@ -44,13 +109,31 @@ export async function setUpModules(
44
109
  threadOrigins: [],
45
110
  reportChannels: [],
46
111
  ownerChannelAsked: 0,
112
+ directAsked: [],
113
+ notified: [],
114
+ posted: [],
47
115
  };
116
+ const direct =
117
+ options.direct ?? (options.discord === false ? undefined : DIRECT);
118
+ const providers: DirectChannelProvider[] = direct
119
+ ? [
120
+ {
121
+ name: "discord",
122
+ label: "a direct message on Discord",
123
+ knows: async (principalId) => direct[principalId] !== undefined,
124
+ reaches: async (principalId) => {
125
+ record.directAsked.push(principalId);
126
+ if (options.online?.() === false)
127
+ throw new Error("Discord is unreachable");
128
+ return direct[principalId];
129
+ },
130
+ deliver: async (principalId, text) =>
131
+ void record.notified.push({ principalId, text }),
132
+ },
133
+ ]
134
+ : [];
48
135
  const plugin = modulesPlugin({
49
- owner: {
50
- id: "1",
51
- name: "Owner",
52
- pronouns: { subject: "they", object: "them", possessive: "their" },
53
- },
136
+ owner: MODULES_OWNER,
54
137
  assistant: "Assistant",
55
138
  // SAFETY: a worker of the test's own replaces the one that would read the runtime.
56
139
  modelRuntime: {} as ModelRuntime,
@@ -61,58 +144,86 @@ export async function setUpModules(
61
144
  thinking: "low",
62
145
  worker: { run: async () => "found it" },
63
146
  },
147
+ ...(options.errorReporter ? { errorReporter: options.errorReporter } : {}),
148
+ ...options.modules,
64
149
  });
65
150
  const services = new ServiceRegistry([plugin]);
66
151
  // SAFETY: the tools under test read no store; each stub is asked for nothing else.
67
152
  services.preset(MEMORY, {} as MemoryStore);
68
- services.preset(SCHEDULES, {} as ScheduleStore);
69
- // SAFETY: the schedule tools ask the agent team for a channel and nothing else.
153
+ services.preset(SCHEDULES, options.schedules ?? ({} as ScheduleStore));
154
+ if (options.conversations)
155
+ // SAFETY: the schedule tools only read a conversation's record.
156
+ services.preset(
157
+ CONVERSATIONS,
158
+ options.conversations as unknown as ConversationRegistry,
159
+ );
160
+ if (options.owners) {
161
+ const owners = options.owners;
162
+ // SAFETY: the modules ask the identity service for the owners and a principal by id only.
163
+ services.preset(IDENTITY, {
164
+ owners: async () => owners,
165
+ principal: async (id: string) => owners.find((owner) => owner.id === id),
166
+ } as unknown as IdentityService);
167
+ }
70
168
  if (options.agentChannelOf)
169
+ // SAFETY: the schedule tools ask the agent team for a channel and nothing else.
71
170
  services.preset(AGENTS, {
72
171
  team: { channelOf: options.agentChannelOf },
73
172
  } as unknown as AgentServer);
74
- // SAFETY: the tools under test use the connection's owner channel and the threads' open only.
75
- services.preset(DISCORD, {
76
- connection: {
77
- ownerChannel: async () => {
78
- record.ownerChannelAsked += 1;
79
- return OWNER_CHANNEL;
173
+ if (options.discord !== false)
174
+ // SAFETY: the tools under test use the connection's owner channel and the threads' open only.
175
+ services.preset(DISCORD, {
176
+ connection: {
177
+ ownerChannel: async () => {
178
+ record.ownerChannelAsked += 1;
179
+ return OWNER_CHANNEL;
180
+ },
181
+ ownerOperations: () => ({}),
80
182
  },
81
- ownerOperations: () => ({}),
82
- },
83
- threads: {
84
- open: async (origin: ChannelKey | undefined) => {
85
- record.threadOrigins.push(origin);
86
- return undefined;
183
+ threads: {
184
+ open: async (origin: ChannelKey | undefined) => {
185
+ record.threadOrigins.push(origin);
186
+ return undefined;
187
+ },
87
188
  },
189
+ guard: {},
190
+ commands: { add: () => undefined },
191
+ } as unknown as DiscordServices);
192
+ const owns =
193
+ options.owns ?? ((channel: ChannelKey) => channel.startsWith("discord:"));
194
+ // Only Discord's channels have a surface; any other conversation has none to report in.
195
+ const surfaces = surfacePort(() => [
196
+ {
197
+ surface: "discord",
198
+ start: async () => undefined,
199
+ sendReply: async (channel, reply) =>
200
+ void record.posted.push({ channel, text: reply.chunks.join("") }),
88
201
  },
89
- guard: {},
90
- commands: { add: () => undefined },
91
- } as unknown as DiscordServices);
92
- // SAFETY: setup reads only the logger, the conversations' background and targets, the surfaces, and the services.
202
+ ]);
203
+ // SAFETY: setup and the preflight read only the logger, the conversations' background, targets and owners, the surfaces, the direct channels, and the services.
93
204
  const context = {
94
205
  logger: silentLogger(),
95
206
  conversations: {
96
207
  target: (name: string) =>
97
- name === OWNER_TARGET.name ? OWNER_TARGET : undefined,
208
+ options.ownerTarget !== false && name === PERSONAL_TARGET.name
209
+ ? PERSONAL_TARGET
210
+ : undefined,
211
+ owns,
212
+ takesBackground: options.takesBackground ?? owns,
213
+ takesSystemReports: options.takesSystemReports ?? owns,
98
214
  background: async (turn: { channel: ChannelKey }) => {
99
215
  record.reportChannels.push(turn.channel);
100
216
  return { status: "ran" };
101
217
  },
102
218
  },
103
- // Only Discord's channels have a surface; any other conversation has none to report in.
104
- surfaces: surfacePort(() => [
105
- {
106
- surface: "discord",
107
- start: async () => undefined,
108
- sendReply: async () => undefined,
109
- },
110
- ]),
219
+ surfaces,
220
+ directChannels: directChannelsPort(() => providers, surfaces),
221
+ toolTiers: toolTiers(options.toolTiers),
111
222
  } as unknown as Omit<PluginContext, "services">;
112
223
  const contribution = await services.setUp(plugin, () =>
113
224
  plugin.setup(pluginContext(plugin, context, services.forPlugin(plugin))),
114
225
  );
115
- return { contribution, record, services };
226
+ return { plugin, contribution, record, services };
116
227
  }
117
228
 
118
229
  /** What each addon plugin contributes, set up over stand-ins as far as setup reads them. */
@@ -1,3 +1,4 @@
1
+ import type { Principal } from "../identity/principal-store.ts";
1
2
  import type { OwnerIdentity } from "../identity.ts";
2
3
  import type { Speaker } from "../speakers.ts";
3
4
 
@@ -7,9 +8,25 @@ export const TEST_OWNER: OwnerIdentity = {
7
8
  pronouns: { subject: "he", object: "him", possessive: "his" },
8
9
  };
9
10
 
11
+ /** The owner as a principal of the tests, its id the owner speaker's, as for an owner carried over from 0.8. */
12
+ export const OWNER_PRINCIPAL: Principal = {
13
+ id: "1",
14
+ displayName: TEST_OWNER.name,
15
+ pronouns: "he",
16
+ disabled: false,
17
+ };
18
+
19
+ /** A member as a principal of the tests. */
20
+ export const MEMBER_PRINCIPAL: Principal = {
21
+ id: "2",
22
+ displayName: "Bo",
23
+ disabled: false,
24
+ };
25
+
10
26
  /** The owner as a speaker of the tests. */
11
27
  export const OWNER_SPEAKER: Speaker = {
12
28
  id: "1",
13
29
  name: TEST_OWNER.name,
14
30
  tier: "owner",
31
+ principalId: OWNER_PRINCIPAL.id,
15
32
  };