pi-roundtable 0.5.0 → 0.5.2
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 +12 -0
- package/README.md +39 -11
- package/README.zh-TW.md +35 -14
- package/docs/plugins.md +258 -167
- package/examples/migrations.test.ts +1 -1
- package/examples/migrations.ts +2 -2
- package/package.json +8 -4
- package/src/cli/cli.ts +2 -0
- package/src/cli/main.ts +1 -0
- package/src/cli/project.ts +2 -0
- package/src/cli/runtime.ts +2 -2
- package/src/cli/templates.ts +10 -9
- package/src/core/agents/agent-prompt.ts +1 -2
- package/src/core/agents/agent-store.ts +1 -2
- package/src/core/agents/agent-team-fixture.ts +5 -3
- package/src/core/agents/fallback-avatar.ts +1 -0
- package/src/core/agents/group-messages.ts +1 -1
- package/src/core/agents/group-round.ts +10 -9
- package/src/core/agents/group-turns.ts +13 -8
- package/src/core/agents/team-layout.ts +13 -14
- package/src/core/agents/team-status.ts +8 -8
- package/src/core/agents/team-turns.ts +8 -9
- package/src/core/define-roundtable.ts +19 -19
- package/src/core/define.ts +10 -10
- package/src/core/discord/channel-executor.ts +20 -18
- package/src/core/discord/channel-operations.ts +7 -3
- package/src/core/discord/inbound-message.ts +13 -11
- package/src/core/discord/owner-cards.ts +3 -3
- package/src/core/discord/owner-discord-threads.ts +16 -12
- package/src/core/discord/owner-discord.ts +5 -4
- package/src/core/host.ts +1 -0
- package/src/core/log.ts +1 -0
- package/src/core/modules/schedules/scheduler.ts +3 -0
- package/src/core/modules/skills/skill-link.ts +13 -8
- package/src/core/modules/skills/skill-listing.ts +2 -4
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +3 -2
- package/src/core/registry/contributions.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -2
- package/src/core/routing/settle-turn.ts +8 -0
- package/src/core/runtime/compaction-tiers.ts +9 -10
- package/src/core/runtime/turn-answer.ts +10 -12
- package/src/core/shared/session-messages.ts +4 -4
package/docs/plugins.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Writing plugins for pi-roundtable
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Read it once, top to bottom, and you can write, test, and run a plugin.
|
|
3
|
+
After running `npx pi-roundtable init`, you can write a plugin to add something the bot doesn't do yet.
|
|
5
4
|
|
|
6
5
|
Every code block below marked with `example:` is a real file under [`examples/`](../examples), and the test suite fails when a block here differs from its file.
|
|
7
6
|
Each example has a test next to it that runs it without Discord or PostgreSQL, except the one that needs a database, which says so.
|
|
@@ -10,11 +9,13 @@ Each example has a test next to it that runs it without Discord or PostgreSQL, e
|
|
|
10
9
|
|
|
11
10
|
A plugin is an object with a name and a `setup` function.
|
|
12
11
|
`setup` returns the parts the plugin adds to the bot: tools agents can call, text added to their prompt, agents to create, handlers for events, long-lived services, slash commands, HTTP routes, and so on.
|
|
13
|
-
The bot itself is assembled from plugins too
|
|
14
|
-
The built-
|
|
12
|
+
The bot itself is assembled from plugins too.
|
|
13
|
+
The core ships built-in plugins for its memory and schedule stores, Discord connection, agent server, notifications, delegation, and schedules.
|
|
14
|
+
You can switch off three [addons](#addons-memory-skills-and-discord-administration): memory, skills, and Discord administration.
|
|
15
|
+
Your plugins are added after them and can read or replace the built-ins' [keyed services](#services-what-plugins-provide-to-each-other).
|
|
15
16
|
|
|
16
17
|
You write plugins in TypeScript, list them in `roundtable.config.ts`, and Bun loads them directly.
|
|
17
|
-
|
|
18
|
+
Importing a plugin installs it, with no build step or plugin registry.
|
|
18
19
|
|
|
19
20
|
```ts
|
|
20
21
|
// roundtable.config.ts
|
|
@@ -39,30 +40,32 @@ Everything a plugin author needs comes from four entries, and nothing else can b
|
|
|
39
40
|
### Advanced building blocks
|
|
40
41
|
|
|
41
42
|
Start with the main entry and the context's built-in services.
|
|
42
|
-
The kit and Discord entries
|
|
43
|
+
The kit and Discord entries follow the main entry's versioning: before 1.0, breaking changes to exported names come in minor releases and appear in the changelog.
|
|
44
|
+
A test compares every exported signature with a recorded report.
|
|
43
45
|
Use `pi-roundtable/kit` for channel claims, tool and presentation helpers, and the types of the core's existing parts.
|
|
44
46
|
Use `pi-roundtable/discord` for everything that touches Discord: slash commands, owner-command modules and panels, the agent panel, and the channel-operation tables.
|
|
45
47
|
The main and kit entries name no discord.js type (a test checks their declarations), so a plugin that does not talk to Discord never depends on it.
|
|
46
|
-
|
|
47
|
-
Their names are grouped by area in the source,
|
|
48
|
+
These entries expose built-in services through keys and ports, without exporting their classes; you can read a service or provide your own implementation.
|
|
49
|
+
Their exported names are grouped by area in the source, with one entry for each; directory paths and files under `src/core` are internal.
|
|
48
50
|
The [changelog](../CHANGELOG.md) lists every exported name, including type-only contracts.
|
|
49
|
-
|
|
51
|
+
Import fixtures and fake threads from `pi-roundtable/testing` for tests.
|
|
50
52
|
|
|
51
53
|
#### Helpers for a Pi session of your own
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
Use the kit's building blocks for a plugin that runs Pi itself, such as a coding worker or a container with no network:
|
|
54
56
|
|
|
55
57
|
- MCP: `mcpExtension` and `VirtualServer` expose MCP servers to a session, and `mcpAdapterExtension` and `readAttachmentExtension` do the same inside an out-of-process worker.
|
|
56
58
|
- Work: `promptSlot` (how a run asks the owner while it works), `workTimeout` (a time limit that does not count the time spent waiting on the owner), `runWorkerTask`, `archiveSessions`, and `approvalCard` and `canonicalJson` for the cards of held actions.
|
|
57
59
|
- Shell: `SHELL_TOOLS` and `shellHoldRule`, the hold rule that keeps risky host-shell commands behind the owner's approval.
|
|
58
60
|
- Tools: `textToolsExtension`, `requiredString`, `stringList` (with `toolText` and `toolError`) for tools that return text.
|
|
59
|
-
- Mirroring a built-in tool in a worker that cannot reach the host: `SCHEDULE_TOOLS`, `scheduleToolSpecs({ locale, timeZone })`, `isScheduleTool`, `callScheduleTool`, `DELEGATE_TOOL` and `DELEGATE_TOOL_SPEC`.
|
|
61
|
+
- Mirroring a built-in tool in a worker that cannot reach the host: `SCHEDULE_TOOLS`, `scheduleToolSpecs({ locale, timeZone })`, `isScheduleTool`, `callScheduleTool`, `DELEGATE_TOOL` and `DELEGATE_TOOL_SPEC`.
|
|
62
|
+
The specs take the locale and time zone for their descriptions, so the worker needs no process-wide setting.
|
|
60
63
|
- Effort: `effortJudge` picks a turn's thinking level from a message with your own brief (`EffortBrief`, `JUDGE_WORK`).
|
|
61
64
|
- Presentation and small helpers: `thinkingLine`, `zonedStamp(date, timeZone)`, `channelQueue()` (a queue of your own, so work does not wait behind a running turn), `checkRepoName` and `SKILL_LIST_TOOL` with `skillListExtension` for repositories and skills, and `searchTerms` for memory search.
|
|
62
65
|
|
|
63
66
|
`roundtable add plugin <name>` creates `plugins/<name>.ts` and its test from a small template and lists it in `roundtable.config.ts`.
|
|
64
67
|
The name is lowercase words joined by dashes, such as `my-notes`.
|
|
65
|
-
Two names are reserved for the [official plugins](#official-plugins): `codex-images` and `dice` copy a ready-made plugin into the project
|
|
68
|
+
Two names are reserved for the [official plugins](#official-plugins): `codex-images` and `dice` copy a ready-made plugin into the project.
|
|
66
69
|
|
|
67
70
|
## The plugin object
|
|
68
71
|
|
|
@@ -80,10 +83,10 @@ definePlugin({
|
|
|
80
83
|
});
|
|
81
84
|
```
|
|
82
85
|
|
|
83
|
-
`definePlugin`
|
|
86
|
+
`definePlugin` checks the name and the presence of `setup`, then returns the typed object you gave it.
|
|
87
|
+
Errors point to the plugin definition.
|
|
84
88
|
|
|
85
|
-
A plugin
|
|
86
|
-
A plugin whose `setup` returns `{}` and that has no migrations, providers, or hooks stops the start (see [Errors](#errors-and-their-fixes)).
|
|
89
|
+
A plugin whose `setup` returns `{}` and has no migrations, providers, or hooks stops startup (see [Errors](#errors-and-their-fixes)).
|
|
87
90
|
|
|
88
91
|
### The context
|
|
89
92
|
|
|
@@ -104,26 +107,29 @@ A plugin whose `setup` returns `{}` and that has no migrations, providers, or ho
|
|
|
104
107
|
| `turns` | Runs one turn of a conversation your claim owns, over the runtime and the surfaces: [`turns.run`](#personas-and-contextturns-conversations-of-a-kind-of-your-own) |
|
|
105
108
|
| `sessions()`, `conversations`, `surfaces`, `turns`, `dashboard()` | Linked once every plugin has been set up; calling them during `setup` throws `NotLinkedError` |
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
`sessions()`, `conversations`, `surfaces`, `turns`, and `dashboard()` are available from a service's `start`, an event handler, or a claim's turn.
|
|
108
111
|
|
|
109
|
-
Use
|
|
112
|
+
Use `QueuePort` from the main entry for `context.queue`; the kit's `ChannelQueue` is also type-only.
|
|
110
113
|
|
|
111
114
|
`context.core` of 0.1.0 is gone: reading it throws a `PluginError` that names `context.services`.
|
|
112
115
|
|
|
113
116
|
### Logging and error reports
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
`Logger`
|
|
118
|
+
`context.logger` is a child of the host's logger and adds `plugin: <your plugin's name>` to each line in the journal.
|
|
119
|
+
`Logger` has five levels and `child(fields)`; you can pass an existing pino logger as `DefineOverrides.logger`.
|
|
117
120
|
|
|
118
|
-
With `defineRoundtable`, the
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
With `defineRoundtable`, the host's logger sends every `error` and `fatal` line to the ops agent named by `config.ops.agent`, which reports it in its channel.
|
|
122
|
+
It then calls `DefineOverrides.errorSink(entry)` if you supply one; this function must not throw.
|
|
123
|
+
A logger you supply reaches the ops agent and `errorSink` only if it forwards its error lines there.
|
|
124
|
+
The report names the plugin next to `app` and `module`; the same error is reported at most once an hour, regardless of which plugin wrote it.
|
|
121
125
|
|
|
122
126
|
### Services: what plugins provide to each other
|
|
123
127
|
|
|
124
128
|
A service is something one plugin builds and others read, such as the schedule store or the agent team.
|
|
125
|
-
Each has a key made with `serviceKey<T>(id)`,
|
|
126
|
-
|
|
129
|
+
Each has a key made with `serviceKey<T>(id)`, where `T` is the service's interface, or port.
|
|
130
|
+
Any object with those methods can provide the service or stand in for it in a test.
|
|
131
|
+
List the keys in `provides` and provide each from `setup` with `services.provide(KEY, value)`.
|
|
132
|
+
The host reads these lists before setup to find which plugin declares each service.
|
|
127
133
|
|
|
128
134
|
| Method | What it does |
|
|
129
135
|
|---|---|
|
|
@@ -132,12 +138,14 @@ A plugin lists the keys it provides in `provides` and provides each from `setup`
|
|
|
132
138
|
| `services.lazy(KEY)` | A function that returns the service once every plugin is set up, for a service whose plugin is registered after this one; call it from a service's `start`, a handler, or another callback that runs after startup, since calling it during setup throws a `NotLinkedError`. The host refuses to boot, naming your plugin, when no registered plugin provides the key |
|
|
133
139
|
| `services.provide(KEY, value)` | Only from setup, only for a key the plugin declares in `provides`, once per key |
|
|
134
140
|
|
|
135
|
-
|
|
141
|
+
The host refuses a plugin that declares a key without providing it when `setup` returns.
|
|
142
|
+
It refuses duplicate declarations before any setup.
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
A
|
|
139
|
-
A
|
|
140
|
-
|
|
144
|
+
If your plugin reads a service with `get` during setup, list the key in `requires` (`noteCounter` below).
|
|
145
|
+
A wrong order is refused before any migration or setup, with both plugins named.
|
|
146
|
+
A key in `requires` must come from a plugin registered before yours.
|
|
147
|
+
A service you read with `find`, such as an addon that may be off, stays out of `requires`.
|
|
148
|
+
If the other plugin reads your service and has to come after yours, use `services.lazy` to read its service when setup is complete (`earlyNoteReader` below).
|
|
141
149
|
|
|
142
150
|
The built-in plugins provide these, from the main entry:
|
|
143
151
|
|
|
@@ -154,8 +162,10 @@ The data types the ports use (`Schedule`, `NewSchedule`, `Agent`, `AgentGroup`,
|
|
|
154
162
|
Your plugins run after the built-ins, so they can read every key above.
|
|
155
163
|
The Discord connection's key, `DISCORD`, is in the Discord entry, because its port names discord.js types: `DiscordServices` has `connection`, `commands`, `guard`, and `threads` (see [`commands.add`](#slash-commands-commandsadd)).
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
To share your own service, export its key as a constant and its port as an interface.
|
|
166
|
+
Plugins registered after yours can read it during setup.
|
|
167
|
+
An earlier plugin can read it with `services.lazy` from a callback that runs after startup.
|
|
168
|
+
Reading it during that earlier plugin's setup throws: `service <id> is not provided yet; plugin <yours> provides it. Register plugin <yours> before plugin <reader>.`
|
|
159
169
|
|
|
160
170
|
<!-- example: examples/shared-services.ts -->
|
|
161
171
|
```ts
|
|
@@ -255,7 +265,9 @@ export function shoutingNotes() {
|
|
|
255
265
|
|
|
256
266
|
#### Replacing a built-in service
|
|
257
267
|
|
|
258
|
-
A plugin that lists a key in both `provides` and `replaces` takes over that service
|
|
268
|
+
A plugin that lists a key in both `provides` and `replaces` takes over that service.
|
|
269
|
+
The host drops the original plugin and sets up the replacement in its place.
|
|
270
|
+
During setup, the replacement can read services from plugins before that position.
|
|
259
271
|
The dropped plugin's migrations and setup do not run.
|
|
260
272
|
To replace the schedule store, implement `ScheduleStore`, provide it under `SCHEDULES`, and replace it:
|
|
261
273
|
|
|
@@ -272,7 +284,7 @@ definePlugin({
|
|
|
272
284
|
});
|
|
273
285
|
```
|
|
274
286
|
|
|
275
|
-
The host
|
|
287
|
+
The host checks replacements before setup:
|
|
276
288
|
|
|
277
289
|
| Message | Fix |
|
|
278
290
|
|---|---|
|
|
@@ -283,8 +295,8 @@ The host refuses a replacement that cannot be made whole:
|
|
|
283
295
|
|
|
284
296
|
#### Addons: memory, skills, and Discord administration
|
|
285
297
|
|
|
286
|
-
|
|
287
|
-
|
|
298
|
+
The host adds these three built-in plugins by default.
|
|
299
|
+
You can switch them off in the configuration.
|
|
288
300
|
|
|
289
301
|
| Addon | Plugin | Switch in `roundtable.config.ts` | What it adds | When it is off |
|
|
290
302
|
|---|---|---|---|---|
|
|
@@ -292,10 +304,11 @@ They are on by default, so a bot that says nothing about them has all three.
|
|
|
292
304
|
| Skills | `skills` (provides `SKILLS`) | `skills: false` | The skill tables, the skill tools of agent sessions (`skill_list`, `skill_link`, `skill_create`, `agent_skills`, and the rest), and the skills every agent carries, `writing-skills` included | No skill tools and no skills in any session; `agent_get` has no skills line; `agent_create` leaves out its `skills` parameter and refuses a call that passes some with `Skills are off on this host`; the tables are left as they are |
|
|
293
305
|
| Discord administration | `discord-admin` | `discord: { admin: false }` | The `discord_*` tools that read and manage the server, for the owner | No `discord_*` tools; the channel executor that remote MCP uses is the connection's, so it stays |
|
|
294
306
|
|
|
295
|
-
|
|
307
|
+
Switching an addon off leaves its tables and rows unchanged, ready for when you turn it on again.
|
|
296
308
|
`skills` also takes the two directories (`skills: { builtinDir, reposDir }`) when it is on.
|
|
297
309
|
|
|
298
|
-
|
|
310
|
+
Use `find` for an optional addon service; it returns `undefined` while the addon is off.
|
|
311
|
+
Use `get` if your plugin needs the addon; it fails with a message naming the switch:
|
|
299
312
|
|
|
300
313
|
```text
|
|
301
314
|
service roundtable.memory is not provided. The memory addon is switched off (config memory: false). Switch it on, or provide the service from a plugin of your own.
|
|
@@ -306,13 +319,13 @@ Switching an addon off and adding a plugin of yours that provides the same key i
|
|
|
306
319
|
|
|
307
320
|
## Tiers: who may use what
|
|
308
321
|
|
|
309
|
-
Every turn
|
|
310
|
-
|
|
311
|
-
|
|
322
|
+
Every turn has a speaker with a tier: `owner`, `admin`, or `member`, from most to least trusted.
|
|
323
|
+
By default, only the owner speaks.
|
|
324
|
+
The operator can open the bot to others by listing them under `speakers` in `roundtable.config.ts` and is responsible for that setup.
|
|
312
325
|
|
|
313
|
-
|
|
314
|
-
The
|
|
315
|
-
|
|
326
|
+
Each tool names the lowest tier that may call it.
|
|
327
|
+
The bot offers the tool in turns whose speaker is at that tier or above; the operator's `toolTiers` setting can override the plugin's choice.
|
|
328
|
+
Tools with no tier assigned require the owner.
|
|
316
329
|
A plugin that adds raw session tools names their tiers with `toolTiers`; the core's own table names only `ask_user`, `compact_session` and `read_attachment`.
|
|
317
330
|
|
|
318
331
|
## The parts
|
|
@@ -468,7 +481,8 @@ test("the section names the agent, and the speaker when there is one", async ()
|
|
|
468
481
|
### `seeds`: agents created on the first start
|
|
469
482
|
|
|
470
483
|
A seed has a name, a display name, a prompt, and a prompt for drawing its avatar.
|
|
471
|
-
|
|
484
|
+
On the first start, the host creates agents that aren't stored yet and leaves existing agents unchanged.
|
|
485
|
+
You can edit them in Discord afterwards.
|
|
472
486
|
`agents` in `roundtable.config.ts` is the same list, for your own team.
|
|
473
487
|
|
|
474
488
|
<!-- example: examples/seeds.ts -->
|
|
@@ -504,7 +518,7 @@ export const library = definePlugin({
|
|
|
504
518
|
| `changed()` | The team changed: an agent or group was created, edited, arranged, archived, or started over |
|
|
505
519
|
| `shutdown(left)` | The shutdown drain ended, before any service stops; `left` lists the work it gave up on |
|
|
506
520
|
|
|
507
|
-
|
|
521
|
+
The host logs errors thrown by a handler and continues running the others.
|
|
508
522
|
|
|
509
523
|
<!-- example: examples/events.ts -->
|
|
510
524
|
```ts
|
|
@@ -536,12 +550,16 @@ export function turnLog(lines: string[]) {
|
|
|
536
550
|
```
|
|
537
551
|
<!-- /example -->
|
|
538
552
|
|
|
539
|
-
`serviceStarted` names the plugin and the service
|
|
540
|
-
|
|
553
|
+
`serviceStarted` names the plugin and the service.
|
|
554
|
+
When the agent server's channels and dashboard are up, it reports `AGENT_SERVER_PLUGIN` and `AGENT_TEAM_SERVICE` (`"agent-server"` and `"team"`) with `outcome === "ready"`.
|
|
555
|
+
Wait for that event to do something that needs the team running, such as posting a notice.
|
|
541
556
|
|
|
542
557
|
A turn event has a `kind`: `"agent"` for an agent's turn, else the kind the turn ran as (`"study"` in the example below).
|
|
543
558
|
`agent` is the agent's name and is absent for a turn of another kind.
|
|
544
|
-
|
|
559
|
+
In a test, call the handler with the payload the core sends.
|
|
560
|
+
A turn is `{ agent, kind, channel, speaker }`, plus `group` for a member's turn in a group.
|
|
561
|
+
`turnEnded` adds `result`, `changed` takes nothing, and `shutdown` takes the list of unfinished work.
|
|
562
|
+
`harness.stop()` delivers an empty list.
|
|
545
563
|
|
|
546
564
|
<!-- example: examples/events.test.ts -->
|
|
547
565
|
```ts
|
|
@@ -579,13 +597,15 @@ test("the handlers record a turn, a team change, and the shutdown", async () =>
|
|
|
579
597
|
|
|
580
598
|
A service has a name and optional `start`, `stop`, and `busy`.
|
|
581
599
|
Services start once everything is set up and stop in reverse order.
|
|
582
|
-
`busy` lists the work still running, one entry each; a shutdown waits until every service's list is empty, so a deploy
|
|
600
|
+
`busy` lists the work still running, one entry each; a shutdown waits until every service's list is empty, for at most an hour, so a deploy rarely cuts work short.
|
|
583
601
|
The host adds its own channel queue to that wait, so a surface that queues its turns through `context.queue` needs no `busy` for them.
|
|
584
602
|
|
|
585
603
|
A service may also have `startInBackground`.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
604
|
+
After all the `start`s finish and the HTTP listeners open, the host runs every service's background start without waiting for it.
|
|
605
|
+
Boot is already complete.
|
|
606
|
+
The host logs errors from background starts and keeps running the process and the other background starts.
|
|
607
|
+
When a background start finishes, every plugin hears `serviceStarted` with `ready` or `failed`.
|
|
608
|
+
A `ready` event means the service has finished its background setup.
|
|
589
609
|
|
|
590
610
|
```ts
|
|
591
611
|
services: [
|
|
@@ -600,8 +620,9 @@ services: [
|
|
|
600
620
|
```
|
|
601
621
|
|
|
602
622
|
Use a service for anything with a lifetime: a timer, a queue, a connection.
|
|
603
|
-
|
|
604
|
-
A
|
|
623
|
+
Agents create schedules with the built-in `schedule_create` tool, and the built-in scheduler fires them.
|
|
624
|
+
A scheduled turn is an ordinary agent turn that can call your tools, so schedules need no separate plugin part.
|
|
625
|
+
For a timer of your own, write a service like this one.
|
|
605
626
|
|
|
606
627
|
<!-- example: examples/services.ts -->
|
|
607
628
|
```ts
|
|
@@ -640,32 +661,38 @@ export function heartbeat(everyMs: number, beat: () => Promise<void> | void) {
|
|
|
640
661
|
|
|
641
662
|
### `migrations` and `context.database()`: tables of your own
|
|
642
663
|
|
|
643
|
-
`migrations`
|
|
644
|
-
|
|
664
|
+
Declare `migrations` on the plugin object.
|
|
665
|
+
At each start, the host runs every plugin's migrations in plugin order before any `setup`, so the tables are ready when setup asks for the database.
|
|
645
666
|
A migration is `{ name, runs?, up(sql) }`.
|
|
646
667
|
Table names are shared with the core and with every other plugin, so prefix them with your plugin's name; a migration's own name only has to be unique inside its plugin.
|
|
647
668
|
|
|
648
|
-
The host
|
|
649
|
-
|
|
669
|
+
The host creates `roundtable_migrations` to keep a ledger of migrations.
|
|
670
|
+
Each migration is recorded under the id `<plugin>/<name>`, such as `visit-counter/visit-counter-1-create`.
|
|
671
|
+
Renaming a plugin or migration makes its migrations run again, so keep both names once they are recorded.
|
|
650
672
|
|
|
651
673
|
- `runs: "once"` (the default) runs the migration one time over a database and records it.
|
|
652
|
-
It runs in its own transaction under an advisory lock,
|
|
674
|
+
It runs in its own transaction under an advisory lock, with the ledger row written in the same transaction.
|
|
675
|
+
A failed migration leaves nothing behind; two hosts starting together apply it once between them.
|
|
653
676
|
A `db.begin(...)` inside `up` becomes a savepoint of that transaction.
|
|
654
677
|
Write DDL that PostgreSQL can run in a transaction (no `CREATE INDEX CONCURRENTLY`).
|
|
655
678
|
- `runs: "every-boot"` runs at every start and is never recorded, so `up` must be idempotent (`CREATE TABLE IF NOT EXISTS`, `UPDATE ... WHERE` a condition that stops matching).
|
|
656
679
|
Use it for a migration that converges data another build may write since, such as a move from a table an older version still writes.
|
|
657
680
|
|
|
658
681
|
A start logs one `migrations` line with the ids applied now, the number skipped, and the number that ran every boot.
|
|
659
|
-
|
|
682
|
+
When you add a ledger to a database that already ran the migrations, each `once` migration runs one more time and is recorded.
|
|
683
|
+
This is safe because it repeats the same work that previously ran at every start.
|
|
660
684
|
`roundtable doctor` runs the same runner inside a transaction it rolls back, so it checks exactly what a start would run.
|
|
661
685
|
|
|
662
|
-
`migrateDatabase(url, plugins)`
|
|
686
|
+
`migrateDatabase(url, plugins)` from the main entry runs the plugins' migrations with the same ledger and lock, then closes its connection.
|
|
687
|
+
It returns a `MigrationReport` with three lists of ids: `applied`, `skipped`, and `everyBoot`.
|
|
663
688
|
Use it in a test or a script that needs the tables before the host runs, and pass the same plugins, with the same names, the host runs.
|
|
664
689
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
690
|
+
Run migrations before passing a database to `testPlugin`, as this example's test does.
|
|
691
|
+
The harness gives your plugin that database without running migrations.
|
|
692
|
+
This is the one example whose test needs PostgreSQL; it is skipped unless `ROUNDTABLE_TEST_DATABASE_URL` is set.
|
|
693
|
+
The test opens a Bun `SQL` on that URL, runs the migrations twice, passes the client to `testPlugin`, and drops its table in `finally`.
|
|
694
|
+
The package exports no test database client.
|
|
695
|
+
Use a disposable database you can write to:
|
|
669
696
|
|
|
670
697
|
```sh
|
|
671
698
|
ROUNDTABLE_TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/plugin_test bun test
|
|
@@ -677,8 +704,8 @@ import { definePlugin, defineTool } from "pi-roundtable";
|
|
|
677
704
|
import { Type } from "typebox";
|
|
678
705
|
|
|
679
706
|
/**
|
|
680
|
-
* Migrations create the plugin's tables before any setup runs
|
|
681
|
-
*
|
|
707
|
+
* Migrations create the plugin's tables before any setup runs. Each one runs once and is recorded
|
|
708
|
+
* in a ledger. Table names are shared with every other plugin: prefix them.
|
|
682
709
|
*/
|
|
683
710
|
export const visitCounter = definePlugin({
|
|
684
711
|
name: "visit-counter",
|
|
@@ -737,7 +764,7 @@ test.skipIf(!url)(
|
|
|
737
764
|
try {
|
|
738
765
|
for (const migration of visitCounter.migrations ?? []) {
|
|
739
766
|
await migration.up(sql);
|
|
740
|
-
await migration.up(sql); //
|
|
767
|
+
await migration.up(sql); // Written to be idempotent, so a second run changes nothing.
|
|
741
768
|
}
|
|
742
769
|
const harness = await testPlugin(visitCounter, { database: sql });
|
|
743
770
|
expect(await harness.runTool("visit_count", { place: "lab" })).toBe(
|
|
@@ -759,7 +786,8 @@ test.skipIf(!url)(
|
|
|
759
786
|
### `providers`: replace a part the core runs on
|
|
760
787
|
|
|
761
788
|
`providers` also sits on the plugin.
|
|
762
|
-
There are three slots,
|
|
789
|
+
There are three slots, each filled by at most one plugin.
|
|
790
|
+
An unknown or misspelled slot stops startup with an error listing the valid names.
|
|
763
791
|
|
|
764
792
|
| Slot | The core's default | Your replacement |
|
|
765
793
|
|---|---|---|
|
|
@@ -767,8 +795,9 @@ There are three slots, and one plugin may fill each; a slot that is not one of t
|
|
|
767
795
|
| `images` | No drawing; each agent gets an avatar generated from its display name | `async (prompt, references) => bytes` returning PNG bytes |
|
|
768
796
|
| `runtime` | None: the agent server builds the Pi runtime itself | `(deps) => runtime`, an [`AgentRuntime`](#the-runtime-slot-replace-pi) that runs every conversation |
|
|
769
797
|
|
|
770
|
-
|
|
771
|
-
|
|
798
|
+
When no `images` provider is configured, `agent_create` has no `avatar_prompt` parameter and `agent_avatar` is unavailable.
|
|
799
|
+
The owner's profile panel reports the missing provider and offers no redraw option.
|
|
800
|
+
Each agent gets a picture generated from its display name and the assistant's icon, so you can tell agents apart.
|
|
772
801
|
`bunx roundtable doctor` reports whether the slot is filled.
|
|
773
802
|
|
|
774
803
|
<!-- example: examples/providers.ts -->
|
|
@@ -795,13 +824,19 @@ export const pixelAvatars = definePlugin({
|
|
|
795
824
|
|
|
796
825
|
### The `runtime` slot: replace Pi
|
|
797
826
|
|
|
798
|
-
The runtime
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
827
|
+
The runtime handles the agent server's conversations and those of every claim that runs turns through `context.turns`.
|
|
828
|
+
It keeps one persistent conversation per channel key, with its turns, held actions, and history.
|
|
829
|
+
By default, the agent server builds the Pi runtime.
|
|
830
|
+
Filling the `runtime` slot replaces it for agent turns, the owner's conversations, steering, held actions, and transcripts; the host skips building Pi.
|
|
831
|
+
The slot's default factory refuses calls, so check `providers.filled.has("runtime")` before calling `providers.runtime`.
|
|
832
|
+
The agent server does this for you.
|
|
802
833
|
|
|
803
834
|
The slot is a `RuntimeFactory`: `(deps: RuntimeDeps) => AgentRuntime`, called once when the agent server sets up.
|
|
804
|
-
`deps`
|
|
835
|
+
`deps` provides `logger`, `env`, `owner`, `toolTiers`, and the host's `judge`.
|
|
836
|
+
Its `sessions()` gives you linked hold rules, packages, session tools, and personas from preflight onwards; call it in a turn, when those parts are available.
|
|
837
|
+
`prompts(conversation, speaker)` gives you the owner's approval and question cards on the conversation's surface, or `undefined`.
|
|
838
|
+
`agents` holds the agent server's per-agent settings: `workDir`, `skills(name)`, `modelOf(name)`, and `turnChannel(scope)`.
|
|
839
|
+
`confirmations` stores held actions across restarts.
|
|
805
840
|
|
|
806
841
|
An `AgentRuntime` has these methods:
|
|
807
842
|
|
|
@@ -817,7 +852,9 @@ An `AgentRuntime` has these methods:
|
|
|
817
852
|
| `preflight?()` | Runs in the host's preflight, before anything starts; a throw stops the boot |
|
|
818
853
|
| `dispose?()` | Runs when the host stops the agent server's runtime service |
|
|
819
854
|
|
|
820
|
-
A request has the turn's `channel`, `selection` (its tools), `text`, `attachments`, `speaker`, and flags (`steerable`, `interactive`, `confirmed`)
|
|
855
|
+
A request has the turn's `channel`, `selection` (its tools), `text`, `attachments`, `speaker`, and flags (`steerable`, `interactive`, `confirmed`).
|
|
856
|
+
An agent's turn also has `agent`, the agent's scope, whose `session` is the conversation's key.
|
|
857
|
+
Other turns use `kind` to name the conversation's persona, defaulting to `"owner"` when absent.
|
|
821
858
|
|
|
822
859
|
<!-- example: examples/echo-runtime.ts -->
|
|
823
860
|
```ts
|
|
@@ -931,7 +968,8 @@ export const echoRuntime = definePlugin({
|
|
|
931
968
|
```
|
|
932
969
|
<!-- /example -->
|
|
933
970
|
|
|
934
|
-
|
|
971
|
+
`testPlugin` builds the runtime from the slot as the agent server does and exposes it as `harness.runtime`.
|
|
972
|
+
The test runs `context.turns` over that runtime and a fake surface, without Discord or Pi:
|
|
935
973
|
|
|
936
974
|
<!-- example: examples/echo-runtime.test.ts -->
|
|
937
975
|
```ts
|
|
@@ -1092,17 +1130,24 @@ export function needsKey(
|
|
|
1092
1130
|
|
|
1093
1131
|
### Slash commands: `commands.add`
|
|
1094
1132
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
`
|
|
1133
|
+
The Discord plugin composes every plugin's slash commands under one root command, `/roundtable` by default or the name in `discord.rootCommand`.
|
|
1134
|
+
It registers them with Discord as it connects.
|
|
1135
|
+
Add commands from `setup` with `context.services.get(DISCORD).commands.add(...)`, importing `DISCORD` from `pi-roundtable/discord`.
|
|
1136
|
+
Subcommands go under the root command.
|
|
1137
|
+
The `module` answers Discord interactions and returns `true` for those it handled.
|
|
1138
|
+
Its `commands()` returns its own top-level commands, excluding the root.
|
|
1139
|
+
A plugin that only adds commands can return `{}` because reading a service in `setup` counts as a contribution.
|
|
1140
|
+
|
|
1141
|
+
The Discord plugin composes commands in its `preflight`, before any service starts.
|
|
1142
|
+
Duplicate command names or subcommands, or a module that registers the root itself, stop startup with a `PluginError` before reaching Discord.
|
|
1143
|
+
Call `commands.add` from `setup`; after preflight it throws `commands can be added only while plugins set up`.
|
|
1144
|
+
Use `services.find(DISCORD)` if your plugin can work without Discord.
|
|
1145
|
+
When the service is absent, the rest of your plugin can run without commands.
|
|
1146
|
+
|
|
1147
|
+
Build a feature's part of the root command with `ownerCommandModule(guard, handlers)` from `pi-roundtable/discord`; it is owner-only, defers interactions, and supplies a failure panel.
|
|
1148
|
+
That entry also exports `groupOption` for subcommand groups, the panel helpers (`ownerPanel`, `ownerPanels`, `ephemeralPanel`, `replyWithPanels`, `plain`, `OwnerFacingError`), and `agentPanel({ guard, agents })` for an agent's profile panel.
|
|
1149
|
+
`DISCORD.guard` is the `CommandGuard`; its `isOwner(actor)` accepts an interaction or anything with `user.id`, so a test needs no cast.
|
|
1150
|
+
`DiscordOptions.refusalHint` (`discord.refusalHint` in the configuration) is appended unchanged to the refusal a non-owner gets.
|
|
1106
1151
|
|
|
1107
1152
|
<!-- example: examples/interactions.ts -->
|
|
1108
1153
|
```ts
|
|
@@ -1141,10 +1186,11 @@ A test gives the plugin `fakeDiscord()` from `pi-roundtable/testing`: `testPlugi
|
|
|
1141
1186
|
|
|
1142
1187
|
### `http`: routes on the bot's listener
|
|
1143
1188
|
|
|
1144
|
-
The configuration's `http` block opens
|
|
1189
|
+
The configuration's `http` block opens a listener named `public`, which serves the agents' avatars.
|
|
1190
|
+
`http.publicUrl` is its internet address.
|
|
1145
1191
|
A route names the listener, a path (`{ exact }` or `{ prefix }`), optionally the methods, and a handler that gets a `Request` and returns a `Response`.
|
|
1146
1192
|
Two routes that could take the same request are refused, so a route cannot shadow the avatars.
|
|
1147
|
-
|
|
1193
|
+
This listener is reachable from the internet, so check a secret in the handler before taking action.
|
|
1148
1194
|
A handler that throws, or returns a rejected promise, answers `500 Internal Server Error` with that fixed body, and the listener keeps serving.
|
|
1149
1195
|
The host logs one error line with the route's `name` and its `listener`; it never logs the request URL, since a path may hold a secret.
|
|
1150
1196
|
|
|
@@ -1196,7 +1242,7 @@ export const links = definePlugin({
|
|
|
1196
1242
|
|
|
1197
1243
|
### `agentSelection`: tools every agent carries
|
|
1198
1244
|
|
|
1199
|
-
|
|
1245
|
+
`agentSelection` runs before each turn, keeping the selection current as the process runs.
|
|
1200
1246
|
It names tools and tool groups; a tool still has to pass the speaker's tier.
|
|
1201
1247
|
|
|
1202
1248
|
<!-- example: examples/selection.ts -->
|
|
@@ -1220,10 +1266,12 @@ export function alwaysOn(tools: () => string[]) {
|
|
|
1220
1266
|
|
|
1221
1267
|
### `piPackages`: Pi extensions every session loads
|
|
1222
1268
|
|
|
1223
|
-
|
|
1269
|
+
List npm packages whose Pi extensions every conversation session should load, and install them in your project (`bun add pi-web-access`).
|
|
1224
1270
|
Two plugins that name the same package load it once.
|
|
1225
1271
|
|
|
1226
|
-
|
|
1272
|
+
The built-in delegation worker also loads `pi-web-access` to search and read the web.
|
|
1273
|
+
`pi-roundtable` lists it as a peer dependency (`>=0.35.0 <0.36.0`), so `bun add pi-roundtable` installs it for you.
|
|
1274
|
+
If your project depends on its own build, such as a fork, both the worker and `piPackages` use that copy without an `overrides` entry.
|
|
1227
1275
|
A project that has none installed stops at the `modules` plugin's setup with a `PluginError` that names the command to run.
|
|
1228
1276
|
|
|
1229
1277
|
<!-- example: examples/packages.ts -->
|
|
@@ -1244,14 +1292,15 @@ export const webSearch = definePlugin({
|
|
|
1244
1292
|
|
|
1245
1293
|
`toolTiers` maps the name of each tool a `sessionTools` extension registers to the lowest tier that may use it: `{ toolTiers: { notes_search: "member", notes_edit: "admin" } }`.
|
|
1246
1294
|
A tool built with `defineTool` already carries its tier; this is the same for the raw form.
|
|
1247
|
-
The operator's `toolTiers` setting
|
|
1295
|
+
The operator's `toolTiers` setting wins, and tools with no tier assigned require the owner.
|
|
1296
|
+
If two plugins name the same tool, the host throws a `PluginError` naming both.
|
|
1248
1297
|
The built-in addons use it: each declares the tiers of its own tools.
|
|
1249
1298
|
|
|
1250
1299
|
### `sessionTools`: the raw form of `tools`
|
|
1251
1300
|
|
|
1252
1301
|
A session tool is a Pi extension placed in every conversation session by its phase: `tools`, `compaction`, or `mcp`.
|
|
1253
|
-
|
|
1254
|
-
|
|
1302
|
+
Use it for tools that `defineTool` cannot express, such as a set that changes while the process runs (bump `revision`) or a tool that depends on the session.
|
|
1303
|
+
Each extension needs a unique name; the core reserves `read-attachment`, `confirmation-gate`, `ask-user`, `self-compact-guard`, and `active-tools`.
|
|
1255
1304
|
A runtime of your own pins the active tools the way the core does: `activeToolsExtension(() => tools)` from `pi-roundtable/kit` is the extension the core places last, so its handler runs after every other extension's.
|
|
1256
1305
|
At most one plugin may add a `compaction` extension, and it must name the `engine` its compactions record.
|
|
1257
1306
|
|
|
@@ -1298,23 +1347,27 @@ export const clock = definePlugin({
|
|
|
1298
1347
|
|
|
1299
1348
|
A claim makes the plugin the owner of the conversations in some channels.
|
|
1300
1349
|
The router asks claims by descending `priority`, then plugin order; the first that owns a channel decides everything there, and a message its `admit` returns nothing for is dropped.
|
|
1301
|
-
|
|
1350
|
+
The built-in agent server already owns the agents' channels, so most plugins need no claim.
|
|
1302
1351
|
|
|
1303
1352
|
A channel key is `<surface>:<id>`: the surface names the chat network (`discord`), and the id is that network's own, which may contain colons.
|
|
1304
1353
|
`parseChannelKey(key)` splits a key at its first colon into `{ surface, id }` and throws for a key without a surface; `channelKey(surface, id)` builds one.
|
|
1305
|
-
A claim's `owns(channel, space)` receives
|
|
1354
|
+
A claim's `owns(channel, space)` receives the channel and the message's server or workspace id as `space`, using the surface's own ids.
|
|
1355
|
+
`space` is absent for direct messages or when the router asks about a channel alone.
|
|
1306
1356
|
The agent server uses it to own every channel of its Discord server.
|
|
1307
|
-
|
|
1357
|
+
Use `parseChannelKey` to read a key, and check the surface in `owns`: `parseChannelKey(channel).surface === "mcp"` for `mcp:` keys, or `"discord"` for Discord.
|
|
1308
1358
|
|
|
1309
|
-
The built-in agent server claims with `AGENT_SERVER_PRIORITY` (100), the highest
|
|
1310
|
-
|
|
1311
|
-
|
|
1359
|
+
The built-in agent server claims `discord:` keys with `AGENT_SERVER_PRIORITY` (100), the highest claim priority.
|
|
1360
|
+
It owns the agents' channels and every other channel in its Discord guild.
|
|
1361
|
+
It leaves those other channels silent for the owner's notes, preventing other claims from answering there.
|
|
1362
|
+
Your Discord claim won't receive messages from that guild at any priority below 100, including `priority: 10`.
|
|
1363
|
+
Claims on other surfaces, such as the example's `echo:` keys, don't compete with the agent server.
|
|
1312
1364
|
|
|
1313
1365
|
A claim may have `stop(channel)`, which stops the channel's running turn and returns whether one was running; the Stop button, `conversations.stop`, and every other stop go through it.
|
|
1314
|
-
The router
|
|
1366
|
+
The router calls only the owning claim's `stop`, returning `false` when that method is absent.
|
|
1315
1367
|
Give `stop` to a claim whose conversations run turns that can be interrupted.
|
|
1316
1368
|
|
|
1317
|
-
|
|
1369
|
+
For a claim with its own conversations, return the conversation's kind from `startFresh`.
|
|
1370
|
+
Run turns with [`context.turns`](#personas-and-contextturns-conversations-of-a-kind-of-your-own), which handles typing, the stop control, events, and replies.
|
|
1318
1371
|
|
|
1319
1372
|
<!-- example: examples/channels.ts -->
|
|
1320
1373
|
```ts
|
|
@@ -1351,18 +1404,23 @@ export const echo = definePlugin({
|
|
|
1351
1404
|
### `personas` and `context.turns`: conversations of a kind of your own
|
|
1352
1405
|
|
|
1353
1406
|
A conversation has a kind: the string its claim returns from `startFresh`, such as `"owner"` or `"study"`.
|
|
1354
|
-
|
|
1355
|
-
A `Persona`
|
|
1407
|
+
Your plugin names its claims' kinds; the host treats them as opaque strings.
|
|
1408
|
+
A `Persona` supplies the system prompt for every non-agent conversation of one kind: `{ kind, prompt() }`.
|
|
1409
|
+
The runtime reads `prompt()` when it creates the conversation's session, so message catalog text uses the host's language.
|
|
1356
1410
|
|
|
1357
1411
|
- A plugin contributes `personas` with the kinds it owns; `sessions().persona(kind)` is the linked lookup a runtime uses.
|
|
1358
1412
|
- Two personas of one kind are refused, naming both plugins, and so is the kind `"agent"`, which the agent server keeps for its agents.
|
|
1359
|
-
- The kind `"owner"` is the default
|
|
1360
|
-
|
|
1361
|
-
-
|
|
1413
|
+
- The kind `"owner"` is the default for a conversation whose claim names none.
|
|
1414
|
+
The plugin that owns the owner's conversations contributes its persona; without one, those conversations start with an empty system prompt.
|
|
1415
|
+
- List tools your plugin needs in `requiredTools`; startup builds a session and refuses to run if any listed tool is unregistered.
|
|
1416
|
+
The host merges every plugin's list and keeps each name once.
|
|
1417
|
+
- A turn whose kind has no persona is refused with an error naming `personas`.
|
|
1362
1418
|
The Pi runtime refuses when it makes the session; a runtime of your own does the same, as the example's does.
|
|
1363
1419
|
|
|
1364
|
-
`context.turns.run(input)`
|
|
1365
|
-
It shows typing and the stop control on the channel's surface, runs the turn
|
|
1420
|
+
Call `context.turns.run(input)` inside your claim's queue task to run a turn in a conversation it owns.
|
|
1421
|
+
It shows typing and the stop control on the channel's surface, runs the turn, and emits `turnStarted` and `turnEnded` with the turn's `kind`.
|
|
1422
|
+
It catches runtime errors as failed results and posts the answer, failure notice, or stopped notice through the surface.
|
|
1423
|
+
Supply `reply(result)` to handle the reply yourself.
|
|
1366
1424
|
`input` has `channel`, `kind`, `text`, `speaker`, and optionally `attachments`, `selection` (by default the plugins' `agentSelection`), `steerable`, `interactive`, `confirmed`, and `reply`.
|
|
1367
1425
|
It rejects with `NotLinkedError` during `setup`, and with a `PluginError` on a host whose agent server has not provided a runtime.
|
|
1368
1426
|
|
|
@@ -1423,7 +1481,7 @@ export const studyRoom = definePlugin({
|
|
|
1423
1481
|
```
|
|
1424
1482
|
<!-- /example -->
|
|
1425
1483
|
|
|
1426
|
-
The test
|
|
1484
|
+
The test uses the fake surface and echo runtime from [the runtime slot](#the-runtime-slot-replace-pi):
|
|
1427
1485
|
|
|
1428
1486
|
<!-- example: examples/study-room.test.ts -->
|
|
1429
1487
|
```ts
|
|
@@ -1501,16 +1559,19 @@ test("the persona is the plugin's and belongs to the study kind only", async ()
|
|
|
1501
1559
|
|
|
1502
1560
|
### `backgroundTargets`: whose turn a schedule or delegated task is
|
|
1503
1561
|
|
|
1504
|
-
|
|
1562
|
+
Schedules and delegated tasks return later as turns without a new message in the channel.
|
|
1505
1563
|
A `BackgroundTarget` says whose turn that is: a `name` that the schedule or job stores, a `label(locale)` that lists such as `/<root> schedule` show, and the limits that apply to it.
|
|
1506
1564
|
`schedules` (`perChannel`, `promptChars`, `aheadDays`) bounds what `schedule_create` accepts, and `delegation` (`maxRunning`) bounds how many delegated tasks may run in one channel; a target without one of them may not schedule or delegate at all.
|
|
1507
1565
|
The claim that answers the target serves it in its `background(turn)`, where `turn.target` is the name, and skips every target it does not serve.
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
-
|
|
1511
|
-
- Two targets
|
|
1512
|
-
- The agent server contributes `OWNER_TARGET` (name `"owner"`, exported from the main entry) for the owner's and
|
|
1513
|
-
|
|
1566
|
+
A channel's claim runs turns only for its own target, keeping the owner's tools out of channels open to many people.
|
|
1567
|
+
|
|
1568
|
+
- Contribute `backgroundTargets` alongside the claim that serves them; read one with `context.conversations.target(name)` from a service or handler.
|
|
1569
|
+
- Two targets with the same name are refused, naming both plugins.
|
|
1570
|
+
- The agent server contributes `OWNER_TARGET` (name `"owner"`, exported from the main entry) for the owner's and agents' conversations.
|
|
1571
|
+
Its claim answers that target and skips all others, so a turn for another target never runs with the owner's tools.
|
|
1572
|
+
- A turn for a target no plugin contributes is skipped with the reason `no plugin contributes the background target "<name>"`.
|
|
1573
|
+
It never falls back to the owner's target; a recurring schedule keeps the reason as its last status and runs again once a plugin contributes the target.
|
|
1574
|
+
A one-time schedule is spent when it fires.
|
|
1514
1575
|
- A schedule keeps its target's name in its row, so renaming a target orphans the schedules made for it.
|
|
1515
1576
|
|
|
1516
1577
|
<!-- example: examples/support-desk.ts -->
|
|
@@ -1562,11 +1623,12 @@ export const supportDesk = definePlugin({
|
|
|
1562
1623
|
|
|
1563
1624
|
A chat surface connects the host to one chat network: it reports what people write, and it posts what the conversations answer.
|
|
1564
1625
|
The host ships Discord's; a plugin contributes another with `surfaces`, and its claims answer on it through `context.surfaces`.
|
|
1565
|
-
|
|
1626
|
+
All surfaces share one conversation router.
|
|
1627
|
+
The surface delivers messages; the channel's owning claim decides what happens to them.
|
|
1566
1628
|
|
|
1567
1629
|
A surface serves the channels whose key starts with its `surface` prefix: `surface = "fake"` serves `fake:<id>` keys, and only those reach its methods.
|
|
1568
1630
|
The prefix is one non-empty word without a colon or a space, unique per host: two surfaces with one prefix stop the start, naming both plugins.
|
|
1569
|
-
|
|
1631
|
+
The agent server claims only `discord:` keys, so claims on your surface's channels don't compete with it.
|
|
1570
1632
|
|
|
1571
1633
|
| Method | What the surface does | Without it |
|
|
1572
1634
|
|---|---|---|
|
|
@@ -1579,18 +1641,20 @@ A surface's channels are never the agent server's, which claims `discord:` keys
|
|
|
1579
1641
|
| `react`, `unreact` | Adds or removes the bot's reaction on a message | no marks on queued or steered messages |
|
|
1580
1642
|
| `prompts(channel, speaker?)` | The owner's way to approve a held action or answer `ask_user` inside a running turn, as `OwnerPrompts`: `confirm` and `ask` | the action is held until the owner's next message |
|
|
1581
1643
|
|
|
1582
|
-
The host starts each surface as
|
|
1583
|
-
|
|
1644
|
+
The host starts each surface as `surface:<prefix>` at the contributing plugin's place in the order, before that plugin's own services.
|
|
1645
|
+
It stops surfaces in reverse order, like other services.
|
|
1646
|
+
The host logs and drops messages whose prefix differs from the delivering surface's, since claims can't identify which surface they belong to.
|
|
1584
1647
|
`context.surfaces` picks the surface by the key's prefix.
|
|
1585
1648
|
Its `sendReply` rejects with a `PluginError` naming the prefix when no surface serves it.
|
|
1586
|
-
`startTyping`, `showStop`, `react`, and `unreact` do nothing
|
|
1649
|
+
`startTyping`, `showStop`, `react`, and `unreact` do nothing when a channel has no surface or its surface omits those methods.
|
|
1650
|
+
If `prompts` is unavailable, it returns `undefined` and held actions wait for the owner's next message.
|
|
1587
1651
|
`of(channel)` returns the surface, or `undefined`.
|
|
1588
1652
|
The agent server asks the owner for approvals through `context.surfaces.prompts`, so a surface that gives `prompts` gets them in its own channels.
|
|
1589
1653
|
|
|
1590
|
-
|
|
1591
|
-
The Discord plugin collects them (see [slash commands](#slash-commands-commandsadd)), and a surface of another network has no commands.
|
|
1654
|
+
The Discord plugin collects [slash commands](#slash-commands-commandsadd); the host doesn't compose them or pass them to surfaces on other networks.
|
|
1592
1655
|
|
|
1593
|
-
|
|
1656
|
+
This in-memory chat surface records everything the host asks of it.
|
|
1657
|
+
Its plugin adds a claim that answers through `context.surfaces`:
|
|
1594
1658
|
|
|
1595
1659
|
<!-- example: examples/fake-surface.ts -->
|
|
1596
1660
|
```ts
|
|
@@ -1712,8 +1776,8 @@ A test boots a host with the plugin, writes through the surface, and reads what
|
|
|
1712
1776
|
|
|
1713
1777
|
A plugin adds slash commands through the Discord plugin's registrar and never receives the composed commands (see [slash commands](#slash-commands-commandsadd)).
|
|
1714
1778
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1779
|
+
The following fields and parts from 0.1.0 were removed because only built-in plugins used them.
|
|
1780
|
+
`definePlugin` or startup refuses plugins that use them and names the replacement:
|
|
1717
1781
|
|
|
1718
1782
|
| Removed | Use instead |
|
|
1719
1783
|
|---|---|
|
|
@@ -1727,9 +1791,13 @@ A plugin that still has one is refused where it is written (`definePlugin`) or w
|
|
|
1727
1791
|
## Official plugins
|
|
1728
1792
|
|
|
1729
1793
|
The package ships two plugins you can copy into a project and change.
|
|
1730
|
-
`roundtable add plugin codex-images` and `roundtable add plugin dice` write `plugins/<name>.ts` and `plugins/<name>.test.ts`, import the plugin in `roundtable.config.ts`, and list it in `plugins
|
|
1731
|
-
|
|
1732
|
-
Both names are reserved
|
|
1794
|
+
`roundtable add plugin codex-images` and `roundtable add plugin dice` write `plugins/<name>.ts` and `plugins/<name>.test.ts`, import the plugin in `roundtable.config.ts`, and list it in `plugins`.
|
|
1795
|
+
You can edit the copied files; `add plugin` refuses to overwrite existing ones.
|
|
1796
|
+
Both names are reserved for these copies.
|
|
1797
|
+
|
|
1798
|
+
A separate package, [pi-roundtable-mcp](https://www.npmjs.com/package/pi-roundtable-mcp), adds two more plugins, `mcpConnectors` and `remoteMcp`.
|
|
1799
|
+
The first lets the owner add MCP servers such as Notion or a calendar from Discord and gives your code the list; the second lets an agent outside Discord reach your agent.
|
|
1800
|
+
Install it with `bun add pi-roundtable-mcp`.
|
|
1733
1801
|
|
|
1734
1802
|
| Plugin | What it does | What it needs |
|
|
1735
1803
|
|---|---|---|
|
|
@@ -1737,16 +1805,17 @@ Both names are reserved, so `add plugin codex-images` never makes a template plu
|
|
|
1737
1805
|
| `dice` | Adds the `roll_dice` tool for members: `2d6+3`, `4d6k3` (keep or drop the highest or lowest dice), several groups, and fate dice `dF`, answered as text such as `2d6+3: [3, 5] + 3 = 11` | Nothing; it takes at most 100 dice in all and 1000 sides per die |
|
|
1738
1806
|
|
|
1739
1807
|
`codex-images` takes the login through [`context.apiKey("openai-codex")`](#the-context).
|
|
1740
|
-
It sends
|
|
1741
|
-
|
|
1742
|
-
|
|
1808
|
+
It sends requests to ChatGPT's Codex backend using the owner's ChatGPT subscription login.
|
|
1809
|
+
OpenAI doesn't document the backend for this use, so it can stop working without notice; the subscription's terms apply.
|
|
1810
|
+
The copied file begins with this warning.
|
|
1743
1811
|
|
|
1744
1812
|
Each copy has a test that runs offline: `codex-images` with a fake `fetch`, `dice` with a fake random source.
|
|
1745
1813
|
Both files export a `create...` function (`createCodexImages`, `createDice`) that takes the part a test replaces, and the plugin you list in the config, built from it.
|
|
1746
1814
|
|
|
1747
1815
|
## Testing a plugin
|
|
1748
1816
|
|
|
1749
|
-
|
|
1817
|
+
Use `testPlugin(plugin, options?)` for most tests; it sets up one plugin against a fake context.
|
|
1818
|
+
Use [`testHost`](#testhost-the-built-in-plugins-and-yours-over-postgresql) for tests that depend on built-in plugins or the host's setup order; it boots them alongside yours over PostgreSQL.
|
|
1750
1819
|
|
|
1751
1820
|
`testPlugin` sets one plugin up against a fake context and starts its services, with no Discord and no PostgreSQL unless you pass `{ database }`.
|
|
1752
1821
|
It returns:
|
|
@@ -1763,8 +1832,10 @@ It returns:
|
|
|
1763
1832
|
| `stop()` | Delivers `shutdown`, stops the injected surfaces, and stops the services in reverse |
|
|
1764
1833
|
|
|
1765
1834
|
The harness applies the same checks as the host (a plugin that adds nothing, an unknown part, a clash of names, a tool with no tier), so a mistake fails your test with the message the start would print.
|
|
1766
|
-
|
|
1767
|
-
|
|
1835
|
+
Run migrations yourself (see the [`migrations`](#migrations-and-contextdatabase-tables-of-your-own) test), call event handlers with their payloads (see [`events`](#events-hear-what-the-core-does)), and call `contribution.prompt`'s `build` to test prompt text (see [`prompt`](#prompt-text-added-to-every-agent-turn)).
|
|
1836
|
+
The harness leaves these calls to the test.
|
|
1837
|
+
`sessions()`, `conversations`, `surfaces`, `turns`, and `dashboard()` throw `NotLinkedError` during `setup`, as they do in the host.
|
|
1838
|
+
After setup, `conversations` routes to the plugin's claims, `surfaces` contains its surfaces, which start with its services, and `turns` runs over the runtime and surfaces.
|
|
1768
1839
|
|
|
1769
1840
|
#### Options
|
|
1770
1841
|
|
|
@@ -1783,14 +1854,16 @@ It does not run migrations (see [`migrations`](#migrations-and-contextdatabase-t
|
|
|
1783
1854
|
|
|
1784
1855
|
The harness supplies what the host would, so a claim or a background turn behaves as it does there:
|
|
1785
1856
|
|
|
1786
|
-
- `BACKGROUND_TURNS`
|
|
1787
|
-
|
|
1857
|
+
- `BACKGROUND_TURNS` runs real background turns over the harness's router, so scheduled and delegated turns reach the plugin's claims.
|
|
1858
|
+
Supply your own with `servicePair(BACKGROUND_TURNS, { ... })`.
|
|
1859
|
+
- When you give `AGENTS` and a `providers.judge`, its `approvals` uses the real confirmation judge to approve or decline held actions as the agent server does.
|
|
1788
1860
|
- Giving `AGENTS` a `team` puts the agent server's own claim in the router, for the `owner`, so the plugin's claims are tested against the agent channels as on a host; its `owner` background target comes with it.
|
|
1789
1861
|
|
|
1790
1862
|
A plugin under test that lists a key in `requires` is refused unless the `services` option gives it (or the plugin provides it), the way the host refuses a key no plugin provides.
|
|
1791
1863
|
A `services.lazy` reader answers once `testPlugin` returns, from the services you gave.
|
|
1792
1864
|
|
|
1793
|
-
|
|
1865
|
+
For a plugin that fills the `runtime` slot, the harness builds the runtime and puts it under `AGENTS`'s `runtime`.
|
|
1866
|
+
It supplies a silent logger, a one-owner identity, in-memory held actions, and one stand-in agent setting.
|
|
1794
1867
|
|
|
1795
1868
|
A test for the tools example:
|
|
1796
1869
|
|
|
@@ -1825,18 +1898,22 @@ Importing `pi-roundtable/testing` works with `CI=true` and no database URL.
|
|
|
1825
1898
|
`describeDb` is Bun's `describe` when `ROUNDTABLE_TEST_DATABASE_URL` is set, and `describe.skip` otherwise; use it to gate a database suite.
|
|
1826
1899
|
`testDatabaseUrl` is that URL or an empty string, and `TEST_GUILD` is a synthetic guild identifier.
|
|
1827
1900
|
`openTestStore(Store, ...args)` runs the store's `migrations(...args)` or its single `migration`, calls `Store.attach`, and returns a `TestStore<T>` whose `close()` closes its own SQL pool.
|
|
1828
|
-
Use it
|
|
1829
|
-
|
|
1901
|
+
Use it with your own store class in a gated database test, and close the store when the test ends.
|
|
1902
|
+
It doesn't attach another instance of the host's stores.
|
|
1903
|
+
Call `useTestLocale()` after a test changes the process-wide locale or time zone to reset them to English and UTC, not from a running plugin.
|
|
1830
1904
|
`testPlugin`'s options take `env` (a partial `HostEnv`) for the `context.env` the plugin sees; it defaults to `en` and `UTC`.
|
|
1831
1905
|
`OWNER_SPEAKER`, `fakeThreads`, and `silentLogger` supply neutral stand-ins for owner turns, dispatch threads, and logging.
|
|
1832
1906
|
`recordingLogger()` is a logger that keeps what it is asked to write: its `lines` hold each call's `level`, `fields` (those of a `child` included), and `message`, for a test of what the code logs.
|
|
1833
|
-
`partial<Port>({ ... })`
|
|
1907
|
+
Use `partial<Port>({ ... })` to stand in for a port your code takes as an argument.
|
|
1908
|
+
It provides the members you give it and throws an error naming any missing member you read, so the test needs no `as unknown as Port` cast.
|
|
1834
1909
|
`fakeDiscord({ ownerId?, rootCommand? })` is the `DISCORD` service for a plugin that adds slash commands: give it as `services: [discord.service]`, read what the plugin added with `discord.added()`, and compose the tree Discord would get with `discord.compose()`.
|
|
1835
1910
|
Only `commands` and `guard` are given; a plugin that reads another member of `DISCORD` in a test gives its own with `servicePair(DISCORD, { ... })`.
|
|
1836
1911
|
|
|
1837
1912
|
### `testHost`: the built-in plugins and yours, over PostgreSQL
|
|
1838
1913
|
|
|
1839
|
-
`testPlugin`
|
|
1914
|
+
`testPlugin` sets up your plugin alone.
|
|
1915
|
+
For tests that need the agent server, modules, stores, or the host's session-tool setup order, use `testHost(options?)`.
|
|
1916
|
+
It boots `defineRoundtable` over `ROUNDTABLE_TEST_DATABASE_URL`, with stand-ins for Discord and the runtime; gate the suite with `describeDb`:
|
|
1840
1917
|
|
|
1841
1918
|
| Option | What it gives the host |
|
|
1842
1919
|
|---|---|
|
|
@@ -1857,14 +1934,17 @@ It returns:
|
|
|
1857
1934
|
| `sessionContext(scope?)` | A `SessionContext` as the runtime builds it, with the real compaction wrapper |
|
|
1858
1935
|
| `stop()` | Stops the host; call it when the test ends |
|
|
1859
1936
|
|
|
1860
|
-
`useEagerCatalog()`
|
|
1937
|
+
`useEagerCatalog()` sets a catalog that marks every text and uses a foreign time zone.
|
|
1938
|
+
`eagerText(value)` lists the marked strings under a value, so a test can check that the host applied its own environment before building text from the catalog.
|
|
1939
|
+
`useTestLocale()` restores the neutral defaults.
|
|
1861
1940
|
|
|
1862
1941
|
`holdChain(rules)` is in `pi-roundtable/kit`: the chain the host links from every plugin's hold rules, to test a rule set without a harness.
|
|
1863
1942
|
|
|
1864
1943
|
## What happens when the bot starts and stops
|
|
1865
1944
|
|
|
1866
|
-
`roundtable start`
|
|
1867
|
-
|
|
1945
|
+
`roundtable start` checks Bun, `.env`, the configuration, the plugins, the model login, and the public URL without using the network.
|
|
1946
|
+
A failed check stops startup with the same message as `roundtable doctor`.
|
|
1947
|
+
The host's `run()` handles startup:
|
|
1868
1948
|
|
|
1869
1949
|
1. Each plugin's `replaces` is applied: the plugin that provided a replaced service is dropped, and the replacement stands where it stood.
|
|
1870
1950
|
Providers are then resolved: each slot from the plugin that fills it, or the core's default.
|
|
@@ -1880,12 +1960,16 @@ Then the host runs `run()`:
|
|
|
1880
1960
|
7. The HTTP listeners open.
|
|
1881
1961
|
8. Every service's `startInBackground` runs, at once and without holding up the boot, and every plugin hears `serviceStarted` as each ends; the agent server's `team` service starts the agents' channels and the dashboard there.
|
|
1882
1962
|
|
|
1883
|
-
|
|
1963
|
+
Steps 1 to 5 must succeed before anything reaches Discord or the listeners.
|
|
1884
1964
|
|
|
1885
|
-
`run()`
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1965
|
+
`run()` applies the host's environment to the process: the locale and time zone, the assistant's name, and `PI_CODING_AGENT_DIR` from `options.environment`.
|
|
1966
|
+
`defineRoundtable` puts these values in the options for `run()` to apply.
|
|
1967
|
+
Build command and tool descriptions from the message catalog in `setup` or later, when that environment is in effect.
|
|
1968
|
+
The Discord plugin builds the root command's description in its preflight.
|
|
1969
|
+
The message catalog and time zone are process-wide, so only one host can run in a process at a time.
|
|
1970
|
+
A second `run()` is refused while a host is running; stop that host or use a separate process.
|
|
1971
|
+
If a startup step fails, the host stops its listeners, stops services in reverse order, closes the pool, and rethrows.
|
|
1972
|
+
The process exits non-zero.
|
|
1889
1973
|
The same host may call `run()` again after that.
|
|
1890
1974
|
|
|
1891
1975
|
On `SIGTERM` or `SIGINT` the bot stops serving new work last:
|
|
@@ -1896,7 +1980,9 @@ On `SIGTERM` or `SIGINT` the bot stops serving new work last:
|
|
|
1896
1980
|
4. Services stop in the reverse of the order they started.
|
|
1897
1981
|
5. The database pool closes.
|
|
1898
1982
|
|
|
1899
|
-
`shutdown()` returns
|
|
1983
|
+
`shutdown()` returns exit code `0`, or `1` if a listener, service, or pool failed to stop.
|
|
1984
|
+
Every call shares the same shutdown.
|
|
1985
|
+
`listen()`, called by the command line and a host's own entry point, exits the process with that code.
|
|
1900
1986
|
|
|
1901
1987
|
## Errors and their fixes
|
|
1902
1988
|
|
|
@@ -1947,7 +2033,7 @@ These are the messages as the code writes them, with `<...>` where your names go
|
|
|
1947
2033
|
| `no persona is registered for the conversation kind "<kind>". A plugin adds one with personas: [...], or its claim must start conversations of a kind that has one.` (a failed turn) | Contribute a persona of that kind, or run the turn with a kind that has one |
|
|
1948
2034
|
| `no runtime provider is configured: the agent server builds the Pi runtime when no plugin fills the runtime slot` | Only a plugin that calls `providers.runtime` without `providers.filled.has("runtime")` sees it; check `filled` first |
|
|
1949
2035
|
| `session tool <name> takes a core extension name. Rename it.` | Pick a name other than the core's |
|
|
1950
|
-
| `two plugins are named <name>.` (from `roundtable doctor`) | Rename yours; the built-in plugins are `
|
|
2036
|
+
| `two plugins are named <name>.` (from `roundtable doctor`) | Rename yours; the built-in plugins are `memory`, `schedule-store`, `discord`, `modules`, `discord-admin`, `skills`, `agent-server`, `seeds`, and `schedules` |
|
|
1951
2037
|
| `migration <plugin>/<name> is declared twice` | A plugin declares two migrations with one name: rename one (the same name in two plugins is fine) |
|
|
1952
2038
|
| `/<root> <name> is added twice`, `/<name> is registered twice` | Give each slash command and subcommand its own name |
|
|
1953
2039
|
| `route <name> is registered twice`, `routes <a> and <b> overlap on listener <id>` | Give each route its own name and a path no other route can take |
|
|
@@ -1962,11 +2048,11 @@ plugin <name>: setup failed: session parts are linked once every plugin is set u
|
|
|
1962
2048
|
```
|
|
1963
2049
|
|
|
1964
2050
|
The same message exists for `conversations` (`Use them from a service's start or from a handler, not during setup.`), for `surfaces` (`chat surfaces are linked once every plugin is set up.`), for `turns` (`conversation turns are linked once every plugin is set up.`), and for `dashboard()`.
|
|
1965
|
-
|
|
2051
|
+
Move the call into a service's `start` or an event handler.
|
|
1966
2052
|
|
|
1967
2053
|
`context.services.get(KEY)` before the plugin that provides it has run throws `service <id> is not provided yet; plugin <name> provides it. Register plugin <name> before plugin <yours>.`
|
|
1968
2054
|
When no registered plugin declares the key it says `service <id> is not provided. Register a plugin that provides it, before the plugin that reads it.`
|
|
1969
|
-
Your plugins
|
|
2055
|
+
Your plugins run after the built-ins, so built-in keys show this error only in `testPlugin`, which has no built-ins: `service <id> is not provided. testPlugin has no built-in plugins: give it in the services option, ...`.
|
|
1970
2056
|
Pass what you need in the harness's `services` option, or test that part elsewhere.
|
|
1971
2057
|
`find(KEY)` is `undefined` for a service nobody declares, in the host and in the harness.
|
|
1972
2058
|
|
|
@@ -2012,7 +2098,8 @@ Your own plugins' text is yours to write in any language.
|
|
|
2012
2098
|
|
|
2013
2099
|
## Consumer TypeScript configuration
|
|
2014
2100
|
|
|
2015
|
-
The package ships `.ts`, so your compiler checks its source with your project's options
|
|
2101
|
+
The package ships `.ts`, so your compiler checks its source with your project's options.
|
|
2102
|
+
`skipLibCheck` skips dependency declarations; package source is still checked.
|
|
2016
2103
|
The following configuration was tested against an installed tarball with TypeScript 5.9.3, and the package itself is checked with TypeScript 7.0.2:
|
|
2017
2104
|
|
|
2018
2105
|
```json
|
|
@@ -2038,10 +2125,12 @@ The following configuration was tested against an installed tarball with TypeScr
|
|
|
2038
2125
|
|
|
2039
2126
|
Install TypeScript and `@types/bun` as development dependencies, as the generated project does.
|
|
2040
2127
|
Keep `exactOptionalPropertyTypes` and `noPropertyAccessFromIndexSignature` disabled: a main-only consumer produces 3 and 132 package-source errors respectively when either is enabled, even with `skipLibCheck: true`.
|
|
2041
|
-
|
|
2128
|
+
The package doesn't override these flags; they are existing compatibility limits.
|
|
2042
2129
|
`noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, and `noUncheckedSideEffectImports` were also tested enabled and pass.
|
|
2043
|
-
|
|
2044
|
-
Discord-facing signatures
|
|
2130
|
+
Keep `skipLibCheck` enabled for this configuration; setting it to `false` produces 97 dependency-declaration errors in the example consumer.
|
|
2131
|
+
Discord-facing signatures in `pi-roundtable/discord` and the composed slash commands in `pi-roundtable/testing` use the package's pinned `discord.js` types.
|
|
2132
|
+
Use compatible types for panel rows and interaction handlers.
|
|
2133
|
+
`discord.js` is a regular dependency, so it installs with `pi-roundtable` when your project imports either entry.
|
|
2045
2134
|
|
|
2046
2135
|
## Developing the core and a host together
|
|
2047
2136
|
|
|
@@ -2055,15 +2144,17 @@ bun link
|
|
|
2055
2144
|
bun link pi-roundtable
|
|
2056
2145
|
```
|
|
2057
2146
|
|
|
2058
|
-
The host
|
|
2059
|
-
|
|
2060
|
-
|
|
2147
|
+
The host imports the checkout's source, so core edits take effect at once and the host's `bun run typecheck` and `bun test` check them.
|
|
2148
|
+
The linked checkout resolves dependencies from its own `node_modules`, outside the host's `overrides`.
|
|
2149
|
+
A host that depends on its own build of `pi-web-access` has two copies while linked.
|
|
2150
|
+
When the change is ready, release the core, set the host's `pi-roundtable` to the new exact version, and run `bun install` to replace the link with the published package.
|
|
2151
|
+
Run the host's checks again against that package.
|
|
2061
2152
|
|
|
2062
2153
|
## Name-to-entry index
|
|
2063
2154
|
|
|
2064
2155
|
Type-only exports require `import type` when `verbatimModuleSyntax` is enabled.
|
|
2065
|
-
|
|
2066
|
-
|
|
2156
|
+
Each name is exported from exactly one entry; importing it from another causes a TypeScript and runtime error.
|
|
2157
|
+
Import from the entries listed below; source area files are internal.
|
|
2067
2158
|
|
|
2068
2159
|
| Name | Entry | Kind |
|
|
2069
2160
|
|---|---|---|
|