pi-roundtable 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -0
- package/README.md +10 -6
- package/README.zh-TW.md +144 -0
- package/docs/plugins.md +1448 -66
- package/examples/echo-runtime.test.ts +122 -0
- package/examples/echo-runtime.ts +107 -0
- package/examples/events.test.ts +1 -0
- package/examples/events.ts +2 -2
- package/examples/fake-surface.test.ts +180 -0
- package/examples/fake-surface.ts +109 -0
- package/examples/interactions.test.ts +12 -3
- package/examples/interactions.ts +19 -19
- package/examples/shared-services.test.ts +74 -0
- package/examples/shared-services.ts +60 -0
- package/examples/study-room.test.ts +69 -0
- package/examples/study-room.ts +52 -0
- package/examples/support-desk.test.ts +34 -0
- package/examples/support-desk.ts +40 -0
- package/package.json +11 -3
- package/src/cli/checks/database.ts +11 -30
- package/src/cli/checks/images.ts +19 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/main.ts +2 -1
- package/src/core/agents/agent-claim.ts +42 -16
- package/src/core/agents/agent-messages.ts +4 -4
- package/src/core/agents/agent-ports.ts +8 -21
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-store.ts +6 -4
- package/src/core/agents/agent-team-fixture.ts +389 -0
- package/src/core/agents/agent-team.ts +26 -25
- package/src/core/agents/agent-tools.ts +34 -15
- package/src/core/agents/avatar-studio.ts +40 -6
- package/src/core/agents/fallback-avatar.ts +85 -0
- package/src/core/agents/team-editing.ts +49 -10
- package/src/core/agents/team-keys.ts +22 -9
- package/src/core/agents/team-layout.ts +4 -3
- package/src/core/agents/team-lifecycle.ts +28 -2
- package/src/core/agents/team-options.ts +9 -5
- package/src/core/agents/team-status.ts +4 -4
- package/src/core/agents/team-text.ts +8 -6
- package/src/core/agents/team-turn-types.ts +4 -4
- package/src/core/agents/team-turns.ts +6 -5
- package/src/core/builtin/agent-server.ts +187 -121
- package/src/core/builtin/discord-admin.ts +41 -0
- package/src/core/builtin/discord.ts +65 -40
- package/src/core/builtin/modules.ts +67 -58
- package/src/core/builtin/session-tool.ts +24 -0
- package/src/core/builtin/skills.ts +72 -0
- package/src/core/builtin/stores.ts +55 -31
- package/src/core/config/config.ts +45 -4
- package/src/core/config/schema.ts +6 -0
- package/src/core/contract/channels.ts +54 -14
- package/src/core/contract/providers.ts +17 -0
- package/src/core/contract/runtime.ts +138 -0
- package/src/core/contract/services.ts +45 -0
- package/src/core/contract/surface.ts +88 -0
- package/src/core/db/migrations.ts +118 -3
- package/src/core/define-roundtable.ts +70 -46
- package/src/core/define.ts +2 -1
- package/src/core/discord/agent-commands.ts +39 -14
- package/src/core/discord/agent-panel.ts +66 -0
- package/src/core/discord/channel-executor.ts +6 -3
- package/src/core/discord/channel-operations.ts +14 -11
- package/src/core/discord/command-collection.ts +46 -0
- package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
- package/src/core/discord/connection.ts +35 -0
- package/src/core/discord/discord-surface.ts +32 -94
- package/src/core/discord/dispatch-threads.ts +2 -2
- package/src/core/discord/inbound-message.ts +99 -0
- package/src/core/discord/interaction-module.ts +57 -1
- package/src/core/discord/owner-cards.ts +4 -4
- package/src/core/discord/owner-command.ts +44 -11
- package/src/core/discord/owner-discord.ts +2 -2
- package/src/core/discord/schedule-commands.ts +21 -18
- package/src/core/discord/stop-button.ts +44 -1
- package/src/core/domain/attachment.ts +12 -8
- package/src/core/domain/conversation.ts +3 -4
- package/src/core/domain/owner-prompts.ts +5 -5
- package/src/core/domain/ports.ts +9 -53
- package/src/core/freeze.ts +9 -0
- package/src/core/host.ts +255 -82
- package/src/core/http/listeners.ts +55 -20
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/index.ts +12 -2
- package/src/core/i18n/owner.ts +7 -1
- package/src/core/i18n/schedules.ts +12 -6
- package/src/core/identity.ts +1 -1
- package/src/core/judging/effort-judge.ts +25 -3
- package/src/core/log.ts +21 -3
- package/src/core/models.ts +2 -2
- package/src/core/modules/background/background-turns.ts +6 -4
- package/src/core/modules/delegation/delegate.ts +3 -2
- package/src/core/modules/delegation/delegator.ts +15 -13
- package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
- package/src/core/modules/discord-admin/discord-admin.ts +6 -6
- package/src/core/modules/host-shell/shell-policy.ts +8 -3
- package/src/core/modules/memory/owner-memory-store.ts +27 -25
- package/src/core/modules/memory/owner-memory.ts +9 -13
- package/src/core/modules/schedules/recurrence.ts +5 -3
- package/src/core/modules/schedules/schedule-store.ts +11 -11
- package/src/core/modules/schedules/schedule-tools.ts +22 -20
- package/src/core/modules/schedules/scheduler.ts +7 -2
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +2 -15
- package/src/core/modules/skills/skill-tools.ts +10 -4
- package/src/core/ops/error-reporter.ts +1 -1
- package/src/core/plugin.ts +232 -28
- package/src/core/registry/contributions.ts +173 -14
- package/src/core/registry/providers.ts +35 -5
- package/src/core/registry/services.ts +229 -0
- package/src/core/routing/channel-queue.ts +5 -0
- package/src/core/routing/channel-router.ts +21 -7
- package/src/core/routing/conversation-turns.ts +139 -0
- package/src/core/routing/message-text.ts +9 -2
- package/src/core/routing/surface-port.ts +39 -0
- package/src/core/runtime/conversation-sessions.ts +4 -3
- package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
- package/src/core/runtime/mcp.ts +1 -1
- package/src/core/runtime/pending-confirmation-store.ts +10 -12
- package/src/core/runtime/pi-agent-runtime.ts +9 -6
- package/src/core/runtime/prompt-slot.ts +8 -3
- package/src/core/runtime/runtime-types.ts +9 -35
- package/src/core/runtime/session-factory.ts +21 -5
- package/src/core/runtime/text-tools.ts +1 -3
- package/src/core/services.ts +238 -90
- package/src/core/sessions.ts +3 -2
- package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
- package/src/core/shared/delegate-tool.ts +4 -3
- package/src/core/shared/schedule-tools.ts +22 -13
- package/src/core/shared/session-messages.ts +1 -1
- package/src/core/speakers.ts +6 -38
- package/src/core/testing/database.ts +1 -7
- package/src/core/testing/eager-catalog.ts +30 -0
- package/src/core/testing/locale.ts +23 -5
- package/src/core/testing/modules.ts +105 -41
- package/src/core/testing/test-host.ts +279 -0
- package/src/core/testing/tool-set.ts +64 -0
- package/src/core/time.ts +6 -1
- package/src/core/tool-set.snapshot.json +288 -0
- package/src/core/tool-tiers.ts +17 -57
- package/src/discord/index.ts +64 -0
- package/src/index.ts +181 -10
- package/src/kit/channels.ts +14 -0
- package/src/kit/domain.ts +18 -0
- package/src/kit/holds.ts +4 -0
- package/src/kit/index.ts +129 -0
- package/src/kit/judging.ts +10 -0
- package/src/kit/memory.ts +3 -0
- package/src/kit/mirror.ts +19 -0
- package/src/kit/presentation.ts +7 -0
- package/src/kit/shell.ts +7 -0
- package/src/kit/skills.ts +8 -0
- package/src/kit/support.ts +21 -0
- package/src/kit/threads.ts +7 -0
- package/src/kit/tools.ts +14 -0
- package/src/kit/worker.ts +16 -0
- package/src/testing.ts +394 -34
- package/examples/guide.test.ts +0 -52
- package/src/cli/add-plugin.test.ts +0 -107
- package/src/cli/checks/basic.test.ts +0 -276
- package/src/cli/checks/database.test.ts +0 -117
- package/src/cli/checks/discord.test.ts +0 -200
- package/src/cli/cli.test.ts +0 -122
- package/src/cli/config-edit.test.ts +0 -130
- package/src/cli/doctor.test.ts +0 -184
- package/src/cli/init.test.ts +0 -127
- package/src/cli/size.test.ts +0 -9
- package/src/cli/templates.test.ts +0 -129
- package/src/cli/testing/fixtures.ts +0 -106
- package/src/core/agents/agent-claim.test.ts +0 -142
- package/src/core/agents/agent-dashboard.test.ts +0 -120
- package/src/core/agents/agent-guild.test.ts +0 -124
- package/src/core/agents/agent-store.test.ts +0 -205
- package/src/core/agents/avatar-studio.test.ts +0 -88
- package/src/core/agents/group-round.test.ts +0 -143
- package/src/core/agents/owner-identity.test.ts +0 -172
- package/src/core/agents/team-turns.test.ts +0 -278
- package/src/core/attachments/attachments.test.ts +0 -85
- package/src/core/boundary.test.ts +0 -45
- package/src/core/builtin/modules.test.ts +0 -127
- package/src/core/config/config.test.ts +0 -135
- package/src/core/contract/discord.ts +0 -33
- package/src/core/db/migrations.test.ts +0 -237
- package/src/core/define-roundtable.test.ts +0 -134
- package/src/core/define.test.ts +0 -144
- package/src/core/discord/agent-commands.test.ts +0 -58
- package/src/core/discord/agent-discord.test.ts +0 -70
- package/src/core/discord/dispatch-thread-host.test.ts +0 -114
- package/src/core/discord/dispatch-threads.test.ts +0 -94
- package/src/core/discord/owner-cards.test.ts +0 -435
- package/src/core/discord/owner-discord.test.ts +0 -325
- package/src/core/domain/expression.ts +0 -21
- package/src/core/domain/profile.ts +0 -32
- package/src/core/drain.test.ts +0 -46
- package/src/core/events.test.ts +0 -81
- package/src/core/holds.test.ts +0 -54
- package/src/core/host.test.ts +0 -536
- package/src/core/http/listeners.test.ts +0 -164
- package/src/core/i18n/i18n.test.ts +0 -139
- package/src/core/identity.test.ts +0 -18
- package/src/core/judging/effort-judge.test.ts +0 -112
- package/src/core/judging/model-judge.test.ts +0 -126
- package/src/core/modules/delegation/delegator.test.ts +0 -170
- package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
- package/src/core/modules/schedules/schedule.test.ts +0 -358
- package/src/core/modules/skills/skill-kind.test.ts +0 -55
- package/src/core/modules/skills/skill-registry.test.ts +0 -376
- package/src/core/ops/error-reporter.test.ts +0 -283
- package/src/core/presentation/card-cadence.ts +0 -41
- package/src/core/presentation/presentation.test.ts +0 -166
- package/src/core/public-entry.test.ts +0 -59
- package/src/core/registry/contributions.test.ts +0 -215
- package/src/core/registry/interactions.test.ts +0 -87
- package/src/core/registry/providers.test.ts +0 -103
- package/src/core/routing/channel-queue.test.ts +0 -31
- package/src/core/routing/channel-router.test.ts +0 -326
- package/src/core/routing/conversation-kind.ts +0 -14
- package/src/core/routing/settle-turn.test.ts +0 -21
- package/src/core/runtime/compaction-tiers.test.ts +0 -227
- package/src/core/runtime/extensions/ask-user.test.ts +0 -114
- package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
- package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
- package/src/core/runtime/session-archive.test.ts +0 -20
- package/src/core/runtime/steerable-run.test.ts +0 -264
- package/src/core/runtime/text-tools.test.ts +0 -110
- package/src/core/runtime/turn-answer.test.ts +0 -72
- package/src/core/runtime/worker-task.test.ts +0 -82
- package/src/core/services.test.ts +0 -28
- package/src/core/sessions.test.ts +0 -85
- package/src/core/shared/unix-server.ts +0 -18
- package/src/core/size.test.ts +0 -9
- package/src/core/speakers.test.ts +0 -78
- package/src/core/testing/file-size.ts +0 -34
- package/src/core/time.test.ts +0 -166
- package/src/core/tool-tiers.test.ts +0 -117
- package/src/entries.test.ts +0 -112
- package/src/testing.test.ts +0 -156
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentSeed } from "../agents/agent-store.ts";
|
|
2
|
-
import type { ChannelClaim } from "../contract/channels.ts";
|
|
3
|
-
import type {
|
|
2
|
+
import type { BackgroundTarget, ChannelClaim } from "../contract/channels.ts";
|
|
3
|
+
import type { ChatSurface } from "../contract/surface.ts";
|
|
4
4
|
import type { ToolContribution } from "../define.ts";
|
|
5
5
|
import { PluginError } from "../errors.ts";
|
|
6
6
|
import { type HoldRule, holdChain } from "../holds.ts";
|
|
@@ -10,9 +10,14 @@ import {
|
|
|
10
10
|
type Contribution,
|
|
11
11
|
type EventHandlers,
|
|
12
12
|
type LinkedSessions,
|
|
13
|
+
type Persona,
|
|
13
14
|
type PluginContext,
|
|
14
15
|
type PromptSection,
|
|
16
|
+
pluginContext,
|
|
15
17
|
type RoundtablePlugin,
|
|
18
|
+
refuseRemovedEvents,
|
|
19
|
+
refuseRemovedParts,
|
|
20
|
+
refuseRemovedSurfaceMethods,
|
|
16
21
|
type Service,
|
|
17
22
|
} from "../plugin.ts";
|
|
18
23
|
import {
|
|
@@ -20,39 +25,51 @@ import {
|
|
|
20
25
|
type SessionTool,
|
|
21
26
|
type ToolSelection,
|
|
22
27
|
} from "../sessions.ts";
|
|
28
|
+
import { TIERS } from "../speakers.ts";
|
|
23
29
|
import type { ToolTierTable } from "../tool-tiers.ts";
|
|
30
|
+
import { ServiceRegistry } from "./services.ts";
|
|
24
31
|
|
|
25
32
|
/** Everything the plugins contributed, in contribution order. */
|
|
26
33
|
export interface Registry {
|
|
27
34
|
services: Service[];
|
|
35
|
+
/** The plugin that contributed each service. */
|
|
36
|
+
servicePlugins: Map<Service, string>;
|
|
28
37
|
handlers: { plugin: string; events: EventHandlers }[];
|
|
29
|
-
interactions: InteractionContribution[];
|
|
30
38
|
routes: HttpRoute[];
|
|
31
39
|
holdRules: HoldRule[];
|
|
32
40
|
piPackages: string[];
|
|
33
41
|
sessionTools: SessionTool[];
|
|
34
42
|
channels: ChannelClaim[];
|
|
43
|
+
/** The contributed chat surfaces, each also in `services` as `surface:<prefix>`. */
|
|
44
|
+
surfaces: ChatSurface[];
|
|
45
|
+
personas: Persona[];
|
|
46
|
+
backgroundTargets: BackgroundTarget[];
|
|
35
47
|
dashboard: string[];
|
|
36
48
|
tools: ToolContribution[];
|
|
37
49
|
seeds: AgentSeed[];
|
|
38
50
|
prompt: PromptSection[];
|
|
39
51
|
agentSelections: (() => ToolSelection)[];
|
|
52
|
+
requiredTools: string[];
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
export const emptyRegistry = (): Registry => ({
|
|
43
56
|
services: [],
|
|
57
|
+
servicePlugins: new Map(),
|
|
44
58
|
handlers: [],
|
|
45
|
-
interactions: [],
|
|
46
59
|
routes: [],
|
|
47
60
|
holdRules: [],
|
|
48
61
|
piPackages: [],
|
|
49
62
|
sessionTools: [],
|
|
50
63
|
channels: [],
|
|
64
|
+
surfaces: [],
|
|
65
|
+
personas: [],
|
|
66
|
+
backgroundTargets: [],
|
|
51
67
|
dashboard: [],
|
|
52
68
|
tools: [],
|
|
53
69
|
seeds: [],
|
|
54
70
|
prompt: [],
|
|
55
71
|
agentSelections: [],
|
|
72
|
+
requiredTools: [],
|
|
56
73
|
});
|
|
57
74
|
|
|
58
75
|
/** The edit distance between two words, for naming the key a typo meant. */
|
|
@@ -129,6 +146,99 @@ class Names {
|
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
|
|
149
|
+
/** The kind of a conversation the agent server runs itself, which no plugin's persona may take. */
|
|
150
|
+
const RESERVED_KIND = "agent";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Refuses a persona without a kind, for the reserved agent kind, or for a kind another plugin's
|
|
154
|
+
* persona already has, so a conversation's prompt is never a matter of plugin order.
|
|
155
|
+
*/
|
|
156
|
+
function claimPersona(
|
|
157
|
+
kinds: Map<string, string>,
|
|
158
|
+
plugin: string,
|
|
159
|
+
persona: Persona,
|
|
160
|
+
): void {
|
|
161
|
+
const { kind } = persona;
|
|
162
|
+
if (typeof kind !== "string" || kind === "")
|
|
163
|
+
throw new PluginError(
|
|
164
|
+
`plugin ${plugin}: a persona needs a kind, a non-empty string such as "study", and got ${JSON.stringify(kind)}.`,
|
|
165
|
+
);
|
|
166
|
+
if (kind === RESERVED_KIND)
|
|
167
|
+
throw new PluginError(
|
|
168
|
+
`plugin ${plugin}: the persona kind "${RESERVED_KIND}" is reserved for the agent server's agents. Give the persona the kind of your own conversations.`,
|
|
169
|
+
);
|
|
170
|
+
if (typeof persona.prompt !== "function")
|
|
171
|
+
throw new PluginError(
|
|
172
|
+
`plugin ${plugin}: the persona of kind "${kind}" needs a prompt() that returns its text.`,
|
|
173
|
+
);
|
|
174
|
+
const other = kinds.get(kind);
|
|
175
|
+
if (other !== undefined)
|
|
176
|
+
throw new PluginError(
|
|
177
|
+
`plugin ${plugin}: persona kind "${kind}" is already registered by plugin ${other}. Keep one persona per kind.`,
|
|
178
|
+
);
|
|
179
|
+
kinds.set(kind, plugin);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Refuses a background target without a name or a label, or whose name another plugin's target
|
|
184
|
+
* already has, so a stored schedule's target never depends on plugin order.
|
|
185
|
+
*/
|
|
186
|
+
function claimTarget(
|
|
187
|
+
names: Map<string, string>,
|
|
188
|
+
plugin: string,
|
|
189
|
+
target: BackgroundTarget,
|
|
190
|
+
): void {
|
|
191
|
+
const { name } = target;
|
|
192
|
+
if (typeof name !== "string" || name === "")
|
|
193
|
+
throw new PluginError(
|
|
194
|
+
`plugin ${plugin}: a background target needs a name, a non-empty string such as "support", and got ${JSON.stringify(name)}.`,
|
|
195
|
+
);
|
|
196
|
+
if (typeof target.label !== "function")
|
|
197
|
+
throw new PluginError(
|
|
198
|
+
`plugin ${plugin}: the background target "${name}" needs a label(locale) that returns its name for lists.`,
|
|
199
|
+
);
|
|
200
|
+
const other = names.get(name);
|
|
201
|
+
if (other !== undefined)
|
|
202
|
+
throw new PluginError(
|
|
203
|
+
`plugin ${plugin}: background target "${name}" is already registered by plugin ${other}. Keep one target per name.`,
|
|
204
|
+
);
|
|
205
|
+
names.set(name, plugin);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The surface's own words for what a prefix must be, checked before its service is named. */
|
|
209
|
+
function checkSurfacePrefix(plugin: string, prefix: unknown): void {
|
|
210
|
+
if (typeof prefix !== "string" || prefix === "" || /[:\s]/.test(prefix))
|
|
211
|
+
throw new PluginError(
|
|
212
|
+
`plugin ${plugin}: a surface is named by the prefix of its channel keys, a non-empty word without a colon or space, such as discord; got ${JSON.stringify(prefix)}.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The surface as a service: it starts with the router's `handle` as its delivery, which drops
|
|
218
|
+
* and logs a message that belongs to another prefix, and stops with the surface.
|
|
219
|
+
*/
|
|
220
|
+
function surfaceService(
|
|
221
|
+
surface: ChatSurface,
|
|
222
|
+
context: Pick<PluginContext, "logger" | "conversations">,
|
|
223
|
+
): Service {
|
|
224
|
+
const { logger, conversations } = context;
|
|
225
|
+
return {
|
|
226
|
+
name: `surface:${surface.surface}`,
|
|
227
|
+
start: () =>
|
|
228
|
+
surface.start((message) => {
|
|
229
|
+
if (!message.channel.startsWith(`${surface.surface}:`)) {
|
|
230
|
+
logger.error(
|
|
231
|
+
{ surface: surface.surface, channel: message.channel },
|
|
232
|
+
"a surface delivered a message of another prefix; dropped",
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
void conversations.handle(message);
|
|
237
|
+
}),
|
|
238
|
+
...(surface.stop ? { stop: () => surface.stop?.() } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
132
242
|
/**
|
|
133
243
|
* Sets up every plugin in order and collects what each adds. A plugin that adds nothing, that
|
|
134
244
|
* reuses a name, or whose setup throws or returns a part the contract does not have is refused
|
|
@@ -136,17 +246,25 @@ class Names {
|
|
|
136
246
|
*/
|
|
137
247
|
export async function collectContributions(
|
|
138
248
|
plugins: readonly RoundtablePlugin[],
|
|
139
|
-
context: PluginContext,
|
|
249
|
+
context: Omit<PluginContext, "services">,
|
|
140
250
|
tiers: ToolTierTable,
|
|
251
|
+
services: ServiceRegistry = new ServiceRegistry(plugins),
|
|
141
252
|
): Promise<Registry> {
|
|
142
253
|
const registry = emptyRegistry();
|
|
143
|
-
const
|
|
254
|
+
const serviceNames = new Names("service");
|
|
144
255
|
const sessionTools = new Names("session tool");
|
|
145
256
|
const holds = new Names("hold rule");
|
|
257
|
+
const surfaces = new Names("surface");
|
|
258
|
+
const personaKinds = new Map<string, string>();
|
|
259
|
+
const targetNames = new Map<string, string>();
|
|
146
260
|
for (const plugin of plugins) {
|
|
147
261
|
let contribution: Contribution;
|
|
148
262
|
try {
|
|
149
|
-
contribution = await
|
|
263
|
+
contribution = await services.setUp(plugin, () =>
|
|
264
|
+
plugin.setup(
|
|
265
|
+
pluginContext(plugin, context, services.forPlugin(plugin)),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
150
268
|
} catch (error) {
|
|
151
269
|
if (error instanceof PluginError) throw error;
|
|
152
270
|
// Some messages end in a period already, such as a NotLinkedError's.
|
|
@@ -162,53 +280,87 @@ export async function collectContributions(
|
|
|
162
280
|
throw new PluginError(
|
|
163
281
|
`plugin ${plugin.name}: setup must return an object of the parts it adds; return {} to add none.`,
|
|
164
282
|
);
|
|
283
|
+
refuseRemovedParts(plugin.name, contribution);
|
|
165
284
|
checkKeys(plugin.name, contribution);
|
|
285
|
+
if (contribution.events)
|
|
286
|
+
refuseRemovedEvents(plugin.name, contribution.events);
|
|
166
287
|
const {
|
|
167
288
|
events,
|
|
168
|
-
interactions = [],
|
|
169
289
|
http = [],
|
|
170
290
|
holdRules = [],
|
|
171
291
|
piPackages = [],
|
|
172
292
|
channels = [],
|
|
293
|
+
surfaces: contributedSurfaces = [],
|
|
294
|
+
personas = [],
|
|
295
|
+
backgroundTargets = [],
|
|
173
296
|
dashboard = [],
|
|
174
297
|
tools = [],
|
|
175
298
|
seeds = [],
|
|
176
299
|
prompt = [],
|
|
300
|
+
requiredTools = [],
|
|
177
301
|
} = contribution;
|
|
178
302
|
const parts = [
|
|
179
303
|
contribution.services ?? [],
|
|
180
|
-
interactions,
|
|
181
304
|
http,
|
|
182
305
|
holdRules,
|
|
183
306
|
piPackages,
|
|
184
307
|
contribution.sessionTools ?? [],
|
|
185
308
|
channels,
|
|
309
|
+
contributedSurfaces,
|
|
310
|
+
personas,
|
|
311
|
+
backgroundTargets,
|
|
186
312
|
dashboard,
|
|
187
313
|
tools,
|
|
188
314
|
seeds,
|
|
189
315
|
prompt,
|
|
316
|
+
requiredTools,
|
|
190
317
|
contribution.agentSelection ? [contribution.agentSelection] : [],
|
|
318
|
+
Object.keys(contribution.toolTiers ?? {}),
|
|
191
319
|
];
|
|
192
320
|
const declares =
|
|
193
321
|
(plugin.migrations?.length ?? 0) > 0 ||
|
|
322
|
+
(plugin.provides?.length ?? 0) > 0 ||
|
|
194
323
|
Object.keys(plugin.providers ?? {}).length > 0 ||
|
|
195
324
|
plugin.preflight !== undefined ||
|
|
196
|
-
plugin
|
|
197
|
-
plugin
|
|
198
|
-
plugin.stopTurn !== undefined;
|
|
325
|
+
// It works through a service it read, such as the Discord plugin's command registrar.
|
|
326
|
+
services.reads(plugin);
|
|
199
327
|
if (!declares && !events && parts.every((part) => part.length === 0))
|
|
200
328
|
throw new PluginError(
|
|
201
329
|
`plugin ${plugin.name} adds nothing. Give it a part (tools, services, channels, and so on), a migration, or a provider, or remove it.`,
|
|
202
330
|
);
|
|
331
|
+
// Its surfaces start before its own services, which may rely on the connection.
|
|
332
|
+
for (const surface of contributedSurfaces) {
|
|
333
|
+
refuseRemovedSurfaceMethods(plugin.name, surface);
|
|
334
|
+
checkSurfacePrefix(plugin.name, surface.surface);
|
|
335
|
+
surfaces.claim(plugin.name, surface.surface);
|
|
336
|
+
const service = surfaceService(surface, context);
|
|
337
|
+
serviceNames.claim(plugin.name, service.name);
|
|
338
|
+
registry.surfaces.push(surface);
|
|
339
|
+
registry.services.push(service);
|
|
340
|
+
registry.servicePlugins.set(service, plugin.name);
|
|
341
|
+
}
|
|
203
342
|
for (const service of contribution.services ?? []) {
|
|
204
|
-
|
|
343
|
+
serviceNames.claim(plugin.name, service.name);
|
|
205
344
|
registry.services.push(service);
|
|
345
|
+
registry.servicePlugins.set(service, plugin.name);
|
|
206
346
|
}
|
|
347
|
+
for (const persona of personas)
|
|
348
|
+
claimPersona(personaKinds, plugin.name, persona);
|
|
349
|
+
for (const target of backgroundTargets)
|
|
350
|
+
claimTarget(targetNames, plugin.name, target);
|
|
207
351
|
for (const rule of holdRules) holds.claim(plugin.name, rule.name);
|
|
208
352
|
for (const tool of contribution.sessionTools ?? []) {
|
|
209
353
|
sessionTools.claim(plugin.name, tool.name);
|
|
210
354
|
registry.sessionTools.push(tool);
|
|
211
355
|
}
|
|
356
|
+
if (contribution.toolTiers) {
|
|
357
|
+
for (const [tool, tier] of Object.entries(contribution.toolTiers))
|
|
358
|
+
if (!(TIERS as readonly string[]).includes(tier))
|
|
359
|
+
throw new PluginError(
|
|
360
|
+
`plugin ${plugin.name}: toolTiers names tool ${tool} at tier ${JSON.stringify(tier)}; use one of ${TIERS.join(", ")}.`,
|
|
361
|
+
);
|
|
362
|
+
tiers.declare(plugin.name, contribution.toolTiers);
|
|
363
|
+
}
|
|
212
364
|
for (const tool of tools) {
|
|
213
365
|
tiers.declare(plugin.name, { [tool.name]: tool.minTier });
|
|
214
366
|
sessionTools.claim(plugin.name, tool.session.name);
|
|
@@ -222,20 +374,25 @@ export async function collectContributions(
|
|
|
222
374
|
if (contribution.agentSelection)
|
|
223
375
|
registry.agentSelections.push(contribution.agentSelection);
|
|
224
376
|
if (events) registry.handlers.push({ plugin: plugin.name, events });
|
|
225
|
-
registry.interactions.push(...interactions);
|
|
226
377
|
registry.routes.push(...http);
|
|
227
378
|
registry.holdRules.push(...holdRules);
|
|
228
379
|
registry.piPackages.push(...piPackages);
|
|
229
380
|
registry.channels.push(...channels);
|
|
381
|
+
registry.personas.push(...personas);
|
|
382
|
+
registry.backgroundTargets.push(...backgroundTargets);
|
|
230
383
|
registry.dashboard.push(...dashboard);
|
|
231
384
|
registry.seeds.push(...seeds);
|
|
232
385
|
registry.prompt.push(...prompt);
|
|
386
|
+
registry.requiredTools.push(...requiredTools);
|
|
233
387
|
}
|
|
234
388
|
return registry;
|
|
235
389
|
}
|
|
236
390
|
|
|
237
391
|
/** Links the collected session parts; throws PluginError on a clash. */
|
|
238
392
|
export function linkSessions(registry: Registry): LinkedSessions {
|
|
393
|
+
const personas = new Map(
|
|
394
|
+
registry.personas.map((persona) => [persona.kind, persona] as const),
|
|
395
|
+
);
|
|
239
396
|
return {
|
|
240
397
|
holds: holdChain(registry.holdRules),
|
|
241
398
|
// A package two plugins ask for loads once, where it was first asked for.
|
|
@@ -243,6 +400,8 @@ export function linkSessions(registry: Registry): LinkedSessions {
|
|
|
243
400
|
plan: compileSessionPlan(registry.sessionTools),
|
|
244
401
|
seeds: registry.seeds,
|
|
245
402
|
prompt: registry.prompt,
|
|
403
|
+
persona: (kind) => personas.get(kind)?.prompt(),
|
|
404
|
+
requiredTools: [...new Set(registry.requiredTools)],
|
|
246
405
|
agentTools: registry.tools.filter((t) => t.agent).map((t) => t.name),
|
|
247
406
|
agentSelection: () => mergeSelections(registry.agentSelections),
|
|
248
407
|
};
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Judge,
|
|
3
|
+
Providers,
|
|
4
|
+
ResolvedProviders,
|
|
5
|
+
} from "../contract/providers.ts";
|
|
2
6
|
import { PluginError, ProviderError } from "../errors.ts";
|
|
3
7
|
import { type JudgeModel, ModelJudge } from "../judging/model-judge.ts";
|
|
4
8
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
9
|
+
import { nearest } from "./contributions.ts";
|
|
5
10
|
|
|
6
11
|
const SLOTS = [
|
|
7
12
|
"judge",
|
|
8
13
|
"images",
|
|
14
|
+
"runtime",
|
|
9
15
|
] as const satisfies readonly (keyof Providers)[];
|
|
10
16
|
|
|
11
17
|
/** A judge with no model: every question throws, so each caller keeps its own default. */
|
|
@@ -24,20 +30,39 @@ function defaults(judgeModel: JudgeModel | undefined): Providers {
|
|
|
24
30
|
judge: judgeModel ? new ModelJudge(judgeModel) : unconfiguredJudge(),
|
|
25
31
|
images: async () => {
|
|
26
32
|
throw new ProviderError(
|
|
27
|
-
"no image provider is configured; agents
|
|
33
|
+
"no image provider is configured; agents use generated avatars",
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
runtime: () => {
|
|
37
|
+
throw new ProviderError(
|
|
38
|
+
"no runtime provider is configured: the agent server builds the Pi runtime when no plugin fills the runtime slot",
|
|
28
39
|
);
|
|
29
40
|
},
|
|
30
41
|
};
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
/** Refuses a provider slot the contract does not have, naming the nearest one and the valid ones. */
|
|
45
|
+
function checkSlots(plugin: RoundtablePlugin): void {
|
|
46
|
+
for (const slot of Object.keys(plugin.providers ?? {})) {
|
|
47
|
+
if ((SLOTS as readonly string[]).includes(slot)) continue;
|
|
48
|
+
const meant = nearest(slot, SLOTS);
|
|
49
|
+
throw new PluginError(
|
|
50
|
+
`plugin ${plugin.name}: unknown provider slot "${slot}".${
|
|
51
|
+
meant ? ` Did you mean "${meant}"?` : ""
|
|
52
|
+
} The slots are ${SLOTS.join(", ")}.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/** Each slot from the one plugin that fills it, else the core's default; two plugins filling one slot clash. */
|
|
34
58
|
export function resolveProviders(
|
|
35
59
|
plugins: readonly RoundtablePlugin[],
|
|
36
60
|
judgeModel?: JudgeModel,
|
|
37
|
-
):
|
|
38
|
-
const resolved = defaults(judgeModel);
|
|
61
|
+
): ResolvedProviders {
|
|
62
|
+
const resolved: Providers = defaults(judgeModel);
|
|
39
63
|
const filledBy = new Map<keyof Providers, string>();
|
|
40
64
|
for (const plugin of plugins) {
|
|
65
|
+
checkSlots(plugin);
|
|
41
66
|
for (const slot of SLOTS) {
|
|
42
67
|
const provided = plugin.providers?.[slot];
|
|
43
68
|
if (!provided) continue;
|
|
@@ -51,5 +76,10 @@ export function resolveProviders(
|
|
|
51
76
|
Object.assign(resolved, { [slot]: provided });
|
|
52
77
|
}
|
|
53
78
|
}
|
|
54
|
-
return resolved;
|
|
79
|
+
return { ...resolved, filled: new Set(filledBy.keys()) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether a plugin fills the `images` slot, so agents can be offered drawing before any host runs. */
|
|
83
|
+
export function fillsImages(plugins: readonly RoundtablePlugin[]): boolean {
|
|
84
|
+
return plugins.some((plugin) => Boolean(plugin.providers?.images));
|
|
55
85
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ServiceKey, Services } from "../contract/services.ts";
|
|
2
|
+
import { PluginError } from "../errors.ts";
|
|
3
|
+
import type { RoundtablePlugin } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
/** The host's words for reading a service no plugin declares; `testPlugin` says how to give one instead. */
|
|
6
|
+
const HOST_ADVICE =
|
|
7
|
+
"Register a plugin that provides it, before the plugin that reads it.";
|
|
8
|
+
|
|
9
|
+
/** The services a plugin declares, checked before any setup: each a key with an id, once per plugin. */
|
|
10
|
+
function declaredBy(plugin: RoundtablePlugin): readonly ServiceKey<unknown>[] {
|
|
11
|
+
const { provides } = plugin;
|
|
12
|
+
if (provides === undefined) return [];
|
|
13
|
+
if (!Array.isArray(provides))
|
|
14
|
+
throw new PluginError(
|
|
15
|
+
`plugin ${plugin.name}: provides must be a list of service keys made with serviceKey().`,
|
|
16
|
+
);
|
|
17
|
+
const seen = new Set<string>();
|
|
18
|
+
for (const key of provides) {
|
|
19
|
+
if (typeof key?.id !== "string" || key.id === "")
|
|
20
|
+
throw new PluginError(
|
|
21
|
+
`plugin ${plugin.name}: provides has an entry that is not a service key; make each with serviceKey("<id>").`,
|
|
22
|
+
);
|
|
23
|
+
if (seen.has(key.id))
|
|
24
|
+
throw new PluginError(
|
|
25
|
+
`plugin ${plugin.name}: provides lists service ${key.id} twice.`,
|
|
26
|
+
);
|
|
27
|
+
seen.add(key.id);
|
|
28
|
+
}
|
|
29
|
+
return provides;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The services a plugin replaces, checked to be a list of keys; empty when it replaces none. */
|
|
33
|
+
function replacedBy(plugin: RoundtablePlugin): readonly ServiceKey<unknown>[] {
|
|
34
|
+
const { replaces } = plugin;
|
|
35
|
+
if (replaces === undefined) return [];
|
|
36
|
+
if (!Array.isArray(replaces))
|
|
37
|
+
throw new PluginError(
|
|
38
|
+
`plugin ${plugin.name}: replaces must be a list of service keys made with serviceKey().`,
|
|
39
|
+
);
|
|
40
|
+
for (const key of replaces)
|
|
41
|
+
if (typeof key?.id !== "string" || key.id === "")
|
|
42
|
+
throw new PluginError(
|
|
43
|
+
`plugin ${plugin.name}: replaces has an entry that is not a service key; make each with serviceKey("<id>").`,
|
|
44
|
+
);
|
|
45
|
+
return replaces;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Applies every plugin's `replaces`: the plugin that provides a replaced service is dropped and
|
|
50
|
+
* the replacement is set up where it stood. Refuses a key nobody provides, a key two plugins
|
|
51
|
+
* replace, a replacement that does not declare what it replaces, and a partial replacement, where
|
|
52
|
+
* the dropped plugin provides a service the replacement does not.
|
|
53
|
+
*/
|
|
54
|
+
export function replaceServices(
|
|
55
|
+
plugins: readonly RoundtablePlugin[],
|
|
56
|
+
): RoundtablePlugin[] {
|
|
57
|
+
const replacers = plugins.filter((plugin) => replacedBy(plugin).length > 0);
|
|
58
|
+
if (replacers.length === 0) return [...plugins];
|
|
59
|
+
// Only a plugin that does not replace can be the one replaced.
|
|
60
|
+
const providerOf = new Map<string, RoundtablePlugin>();
|
|
61
|
+
for (const plugin of plugins)
|
|
62
|
+
if (!replacers.includes(plugin))
|
|
63
|
+
for (const key of declaredBy(plugin))
|
|
64
|
+
if (!providerOf.has(key.id)) providerOf.set(key.id, plugin);
|
|
65
|
+
const claimed = new Map<string, RoundtablePlugin>();
|
|
66
|
+
const dropped = new Map<RoundtablePlugin, RoundtablePlugin>();
|
|
67
|
+
for (const plugin of replacers) {
|
|
68
|
+
const own = new Set(declaredBy(plugin).map((key) => key.id));
|
|
69
|
+
for (const key of replacedBy(plugin)) {
|
|
70
|
+
const other = claimed.get(key.id);
|
|
71
|
+
if (other)
|
|
72
|
+
throw new PluginError(
|
|
73
|
+
`plugin ${plugin.name}: service ${key.id} is also replaced by plugin ${other.name}. Keep one replacement per service.`,
|
|
74
|
+
);
|
|
75
|
+
claimed.set(key.id, plugin);
|
|
76
|
+
if (!own.has(key.id))
|
|
77
|
+
throw new PluginError(
|
|
78
|
+
`plugin ${plugin.name}: replaces service ${key.id} but does not list it in provides. Provide what you replace.`,
|
|
79
|
+
);
|
|
80
|
+
const provider = providerOf.get(key.id);
|
|
81
|
+
if (!provider)
|
|
82
|
+
throw new PluginError(
|
|
83
|
+
`plugin ${plugin.name}: replaces service ${key.id}, which no other registered plugin provides. Remove it from replaces, or register the plugin that provides it.`,
|
|
84
|
+
);
|
|
85
|
+
dropped.set(provider, plugin);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [provider, replacement] of dropped) {
|
|
89
|
+
const replaced = new Set(replacedBy(replacement).map((key) => key.id));
|
|
90
|
+
const missing = declaredBy(provider).filter((key) => !replaced.has(key.id));
|
|
91
|
+
if (missing.length > 0)
|
|
92
|
+
throw new PluginError(
|
|
93
|
+
`plugin ${replacement.name}: replacing plugin ${provider.name} would drop ${missing.map((key) => key.id).join(", ")} too, which it also provides. Replace every service of ${provider.name}, or none.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
// The replacement takes the place of the first plugin it drops, not the place it was listed in.
|
|
97
|
+
const result: RoundtablePlugin[] = [];
|
|
98
|
+
for (const plugin of plugins) {
|
|
99
|
+
if (replacers.includes(plugin)) continue;
|
|
100
|
+
const replacement = dropped.get(plugin);
|
|
101
|
+
if (!replacement) result.push(plugin);
|
|
102
|
+
else if (!result.includes(replacement)) result.push(replacement);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** What to do about a service whose provider has not set up yet: the order the plugins are registered in. */
|
|
108
|
+
function orderHint(
|
|
109
|
+
provider: RoundtablePlugin,
|
|
110
|
+
reader: RoundtablePlugin | undefined,
|
|
111
|
+
): string {
|
|
112
|
+
if (reader === undefined) return "";
|
|
113
|
+
if (reader === provider)
|
|
114
|
+
return ` Plugin ${provider.name} reads it before its own setup has provided it.`;
|
|
115
|
+
return ` Register plugin ${provider.name} before plugin ${reader.name}.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The services the plugins provide and read. It knows from `provides` which plugin declares each
|
|
120
|
+
* key before any setup, so a read can tell a service switched off from one that is not set up yet.
|
|
121
|
+
*/
|
|
122
|
+
export class ServiceRegistry {
|
|
123
|
+
readonly #declaredBy = new Map<string, RoundtablePlugin>();
|
|
124
|
+
readonly #values = new Map<string, unknown>();
|
|
125
|
+
readonly #advice: string;
|
|
126
|
+
/** The plugin whose setup is running, the only one that may provide. */
|
|
127
|
+
#setting: RoundtablePlugin | undefined;
|
|
128
|
+
/** The plugins that have read a service, such as one that adds its commands through it. */
|
|
129
|
+
readonly #readers = new Set<RoundtablePlugin>();
|
|
130
|
+
|
|
131
|
+
/** `advice` says how to get a service no plugin declares; the host's is to register one that does. */
|
|
132
|
+
constructor(plugins: readonly RoundtablePlugin[], advice = HOST_ADVICE) {
|
|
133
|
+
this.#advice = advice;
|
|
134
|
+
for (const plugin of plugins)
|
|
135
|
+
for (const key of declaredBy(plugin)) {
|
|
136
|
+
const other = this.#declaredBy.get(key.id);
|
|
137
|
+
if (other)
|
|
138
|
+
throw new PluginError(
|
|
139
|
+
`plugin ${plugin.name}: service ${key.id} is also provided by plugin ${other.name}. Keep one provider, or have one plugin replace the other with replaces.`,
|
|
140
|
+
);
|
|
141
|
+
this.#declaredBy.set(key.id, plugin);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Provides a service the host or a test holds itself, without a plugin's setup. */
|
|
146
|
+
preset<T>(key: ServiceKey<T>, value: T): void {
|
|
147
|
+
this.#values.set(key.id, value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The service, for the host's own reads; throws like a plugin's `get`. */
|
|
151
|
+
get<T>(key: ServiceKey<T>): T {
|
|
152
|
+
return this.#read(key, undefined, true) as T;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
find<T>(key: ServiceKey<T>): T | undefined {
|
|
156
|
+
return this.#read(key, undefined, false);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#read<T>(
|
|
160
|
+
key: ServiceKey<T>,
|
|
161
|
+
reader: RoundtablePlugin | undefined,
|
|
162
|
+
required: boolean,
|
|
163
|
+
): T | undefined {
|
|
164
|
+
if (this.#values.has(key.id)) return this.#values.get(key.id) as T;
|
|
165
|
+
const provider = this.#declaredBy.get(key.id);
|
|
166
|
+
if (provider) {
|
|
167
|
+
throw new PluginError(
|
|
168
|
+
`service ${key.id} is not provided yet; plugin ${provider.name} provides it.${orderHint(provider, reader)}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (!required) return undefined;
|
|
172
|
+
throw new PluginError(
|
|
173
|
+
`service ${key.id} is not provided. ${key.absent ?? this.#advice}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The view one plugin's setup gets: its reads name it, and only it may provide what it declares. */
|
|
178
|
+
forPlugin(plugin: RoundtablePlugin): Services {
|
|
179
|
+
return {
|
|
180
|
+
get: <T>(key: ServiceKey<T>) => {
|
|
181
|
+
this.#readers.add(plugin);
|
|
182
|
+
return this.#read(key, plugin, true) as T;
|
|
183
|
+
},
|
|
184
|
+
find: <T>(key: ServiceKey<T>) => {
|
|
185
|
+
this.#readers.add(plugin);
|
|
186
|
+
return this.#read(key, plugin, false);
|
|
187
|
+
},
|
|
188
|
+
provide: (key, value) => {
|
|
189
|
+
if (this.#setting !== plugin)
|
|
190
|
+
throw new PluginError(
|
|
191
|
+
`plugin ${plugin.name}: services are provided from setup only; provide ${key.id} while setup runs.`,
|
|
192
|
+
);
|
|
193
|
+
if (this.#declaredBy.get(key.id) !== plugin)
|
|
194
|
+
throw new PluginError(
|
|
195
|
+
`plugin ${plugin.name}: cannot provide service ${key.id}, which it does not declare. Add it to the plugin's provides.`,
|
|
196
|
+
);
|
|
197
|
+
if (this.#values.has(key.id))
|
|
198
|
+
throw new PluginError(
|
|
199
|
+
`plugin ${plugin.name}: service ${key.id} is provided twice.`,
|
|
200
|
+
);
|
|
201
|
+
this.#values.set(key.id, value);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Whether the plugin read a service: it then works through that service, even when it returns no part. */
|
|
207
|
+
reads(plugin: RoundtablePlugin): boolean {
|
|
208
|
+
return this.#readers.has(plugin);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Runs a plugin's setup with `provide` open to it, and refuses it when it declared a service it did not provide. */
|
|
212
|
+
async setUp<T>(
|
|
213
|
+
plugin: RoundtablePlugin,
|
|
214
|
+
setup: () => Promise<T> | T,
|
|
215
|
+
): Promise<T> {
|
|
216
|
+
this.#setting = plugin;
|
|
217
|
+
try {
|
|
218
|
+
const result = await setup();
|
|
219
|
+
for (const key of declaredBy(plugin))
|
|
220
|
+
if (!this.#values.has(key.id))
|
|
221
|
+
throw new PluginError(
|
|
222
|
+
`plugin ${plugin.name}: declares that it provides service ${key.id}, but setup did not provide it. Call services.provide(...) in setup, or remove it from provides.`,
|
|
223
|
+
);
|
|
224
|
+
return result;
|
|
225
|
+
} finally {
|
|
226
|
+
this.#setting = undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -42,3 +42,8 @@ export class ChannelQueue {
|
|
|
42
42
|
return [...this.#sizes.keys()];
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** A queue of its own, apart from the host's; for work that must not wait behind a running turn. */
|
|
47
|
+
export function channelQueue(): ChannelQueue {
|
|
48
|
+
return new ChannelQueue();
|
|
49
|
+
}
|