pi-roundtable 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/docs/plugins.md +76 -5
- package/examples/shared-services.test.ts +28 -0
- package/examples/shared-services.ts +31 -0
- package/package.json +6 -3
- package/src/core/contract/services.ts +7 -0
- package/src/core/host.ts +1 -0
- package/src/core/http/listeners.ts +5 -25
- package/src/core/modules/delegation/web-research-worker.ts +14 -1
- package/src/core/plugin.ts +8 -0
- package/src/core/registry/contributions.ts +1 -0
- package/src/core/registry/services.ts +73 -1
- package/src/core/runtime/session-factory.ts +1 -1
- package/src/core/shared/package-dir.ts +8 -5
- package/src/core/shared/unix-server.ts +26 -0
- package/src/core/testing/partial.ts +21 -0
- package/src/core/testing/recording-logger.ts +44 -0
- package/src/discord/index.ts +1 -1
- package/src/kit/channels.ts +1 -1
- package/src/kit/domain.ts +1 -1
- package/src/kit/holds.ts +1 -1
- package/src/kit/index.ts +2 -1
- package/src/kit/judging.ts +1 -1
- package/src/kit/memory.ts +1 -1
- package/src/kit/mirror.ts +1 -1
- package/src/kit/presentation.ts +1 -1
- package/src/kit/process.ts +5 -0
- package/src/kit/shell.ts +1 -1
- package/src/kit/skills.ts +1 -1
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/kit/tools.ts +1 -1
- package/src/kit/worker.ts +1 -1
- package/src/testing.ts +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
Found by running the first consuming host on the published 0.2.1; each item answers one finding of its friction log.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `RoundtablePlugin.requires`: the services a plugin reads with `get` in its setup. The host checks the list before any migration or setup and refuses a key no registered plugin provides, one a plugin registered after it provides, and one the plugin provides itself, naming the plugins and the fix.
|
|
15
|
+
- `Services.lazy(KEY)`: a function that returns the service once every plugin is set up, for a plugin that needs a service of one registered after it. Calling it during setup throws a `NotLinkedError`; the host refuses to boot, naming the plugin, when no registered plugin provides the key. A plugin no longer has to keep a shared object that the later plugin fills in. `testPlugin` checks `requires` against the `services` option and answers `lazy` readers after setup.
|
|
16
|
+
- `pi-roundtable/kit`: `packageDir`, the installed package's folder as Pi's `additionalExtensionPaths` takes it (`packageDir(name, import.meta.url)`; the second argument is the file the package is looked up from, so a host finds its own dependencies even when `pi-roundtable` is linked apart from them), and `serveUnix`, an HTTP server on a unix socket without Bun's idle timeout (`serveUnix(socketPath, fetch, { error? })`). They are the core's own, so a worker process of a host no longer copies them.
|
|
17
|
+
- `pi-roundtable/testing`: `partial`, a typed stand-in (`partial<Port>({ ... })`) with only the members the test gives, which names any other member it is asked for; `recordingLogger` with `RecordingLogger` and `RecordedLog`, a logger whose `lines` keep each call's level, fields, and message. Neither needs an `as unknown as` cast.
|
|
18
|
+
- A guide section on developing the core and a host together with `bun link`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `pi-web-access` is a peer dependency (`>=0.35.0 <0.36.0`) instead of a dependency, and a development dependency at 0.35.0. `bun add pi-roundtable` still installs it; a host that depends on its own build of it now has one copy, with no `overrides` entry. A project without it fails at the `modules` plugin's setup with a `PluginError` that names the command to run.
|
|
23
|
+
- `pi-roundtable/kit` and `pi-roundtable/discord` are versioned like the main entry: before 1.0, a breaking change to an exported name comes in a minor release and is listed in the changelog. The signature report already guarded every name; the "unstable, not covered by semver" label is gone.
|
|
24
|
+
- `Services` has a new member, `lazy`; a hand-written `Services` (only the core's own and tests') needs it.
|
|
25
|
+
- The guide's mention of the first consumer by name is gone.
|
|
26
|
+
|
|
8
27
|
## [0.2.1] - 2026-10-01
|
|
9
28
|
|
|
10
29
|
### Added
|
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ test("hello greets", async () => {
|
|
|
111
111
|
|
|
112
112
|
The [plugin guide](docs/plugins.md) explains every part a plugin can add (tools, prompt sections, agents, events, services, migrations, providers, slash commands, HTTP routes, and more), the order things start and stop in, and every startup error with its fix.
|
|
113
113
|
Its examples live in [`examples/`](examples), and the test suite runs each of them.
|
|
114
|
-
`pi-roundtable/kit` supplies claim, tool and presentation helpers and type-only names for the context’s existing services, and `pi-roundtable/discord` supplies the slash-command registrar, owner-command and panel helpers, and the agent panel, and is the entry that names discord.js types (`pi-roundtable/testing` names a few, through `testHost`'s composed commands); both are
|
|
114
|
+
`pi-roundtable/kit` supplies claim, tool and presentation helpers and type-only names for the context’s existing services, and `pi-roundtable/discord` supplies the slash-command registrar, owner-command and panel helpers, and the agent panel, and is the entry that names discord.js types (`pi-roundtable/testing` names a few, through `testHost`'s composed commands); both are versioned like the main entry: before 1.0 a breaking change comes in a minor release and is listed in the changelog.
|
|
115
115
|
|
|
116
116
|
## Settings
|
|
117
117
|
|
package/README.zh-TW.md
CHANGED
|
@@ -111,7 +111,7 @@ test("hello greets", async () => {
|
|
|
111
111
|
|
|
112
112
|
[外掛指南](docs/plugins.md)(英文)說明外掛能新增的每個部分(工具、提示詞區段、智慧體、事件、服務、migration、provider、斜線指令、HTTP 路由等等)、啟動與停止的順序,以及每一種啟動錯誤和它的修正方式。
|
|
113
113
|
指南裡的範例放在 [`examples/`](examples),測試套件會執行每一個範例。
|
|
114
|
-
`pi-roundtable/kit` 提供頻道認領(claim)、工具與呈現用的輔助函式,以及 context 現有服務的純型別名稱;`pi-roundtable/discord` 提供斜線指令註冊器、擁有者指令與面板的輔助函式,以及智慧體面板,是會用到 discord.js 型別的入口(`pi-roundtable/testing` 也透過 `testHost`
|
|
114
|
+
`pi-roundtable/kit` 提供頻道認領(claim)、工具與呈現用的輔助函式,以及 context 現有服務的純型別名稱;`pi-roundtable/discord` 提供斜線指令註冊器、擁有者指令與面板的輔助函式,以及智慧體面板,是會用到 discord.js 型別的入口(`pi-roundtable/testing` 也透過 `testHost` 組合出的指令用到少數幾個)。這兩個入口的版本規則與主入口相同:1.0 之前,不相容的變更會放在次版本(minor)發佈,並列在變更記錄中。
|
|
115
115
|
|
|
116
116
|
## 設定
|
|
117
117
|
|
package/docs/plugins.md
CHANGED
|
@@ -33,13 +33,13 @@ Everything a plugin author needs comes from four entries, and nothing else can b
|
|
|
33
33
|
|---|---|
|
|
34
34
|
| `pi-roundtable` | `definePlugin`, `defineTool`, `defineRoundtable`, `ToolRefusal`, `PluginError`, `NotLinkedError`, `Roundtable`, and the types (`Tier`, `Speaker`, `Contribution`, `PluginContext`, and so on) |
|
|
35
35
|
| `pi-roundtable/testing` | Fixtures for testing plugins, without Discord or a database unless the test explicitly opens one; `testHost` names a few discord.js types (`ComposedCommands`, `CommandGuard`, `InteractionModule`, `RootOption`) so a test can drive the composed slash commands |
|
|
36
|
-
| `pi-roundtable/kit` |
|
|
37
|
-
| `pi-roundtable/discord` |
|
|
36
|
+
| `pi-roundtable/kit` | Helpers for channel claims, tools, presentation, worker processes, and naming existing core parts |
|
|
37
|
+
| `pi-roundtable/discord` | The entry built on discord.js types: the `DISCORD` service (slash commands, the owner guard), owner-command and panel helpers, the agent panel, and the channel-operation tables |
|
|
38
38
|
|
|
39
39
|
### Advanced building blocks
|
|
40
40
|
|
|
41
41
|
Start with the main entry and the context's built-in services.
|
|
42
|
-
The kit
|
|
42
|
+
The kit and Discord entries are versioned like the main entry: before 1.0 a breaking change to any exported name comes in a minor release and is listed in the changelog, and a test compares every exported signature with a recorded report.
|
|
43
43
|
Use `pi-roundtable/kit` for channel claims, tool and presentation helpers, and the types of the core's existing parts.
|
|
44
44
|
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
45
|
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.
|
|
@@ -127,10 +127,16 @@ A plugin lists the keys it provides in `provides` and provides each from `setup`
|
|
|
127
127
|
|---|---|
|
|
128
128
|
| `services.get(KEY)` | The service, or a `PluginError` that names the key and the plugin to register first when it is not provided yet |
|
|
129
129
|
| `services.find(KEY)` | The service, or `undefined` when no registered plugin declares it, such as an addon that is off; it throws like `get` when a plugin declares it but has not set up yet, because that is order, not absence |
|
|
130
|
+
| `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 |
|
|
130
131
|
| `services.provide(KEY, value)` | Only from setup, only for a key the plugin declares in `provides`, once per key |
|
|
131
132
|
|
|
132
133
|
A plugin that declares a key and does not provide it is refused when its `setup` returns, and two plugins that declare one key are refused before any setup.
|
|
133
134
|
|
|
135
|
+
A plugin that reads a service with `get` during its setup lists the key in `requires`, so a wrong order is refused before any migration or setup, with both plugins named, and the plugin states what it needs in one place (`noteCounter` below).
|
|
136
|
+
A key in `requires` must be provided by a plugin registered before this one.
|
|
137
|
+
A service you read with `find` (an addon that may be off) stays out of `requires`.
|
|
138
|
+
When the other plugin has to come after yours, because it reads what yours provides, read its service with `services.lazy` instead of keeping the object in a variable that the later plugin fills in (`earlyNoteReader` below).
|
|
139
|
+
|
|
134
140
|
The built-in plugins provide these, from the main entry:
|
|
135
141
|
|
|
136
142
|
| Key | Port | Provided by | What it is |
|
|
@@ -147,7 +153,7 @@ Your plugins run after the built-ins, so they can read every key above.
|
|
|
147
153
|
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)).
|
|
148
154
|
|
|
149
155
|
A plugin of yours shares a service the same way: the key is a constant you export, the port is an interface you export, and plugins registered after yours read it.
|
|
150
|
-
A plugin registered before yours cannot, and says so: `service <id> is not provided yet; plugin <yours> provides it. Register plugin <yours> before plugin <reader>.`
|
|
156
|
+
A plugin registered before yours cannot read it during its setup, and says so (it reads the service with `services.lazy` instead, from a callback that runs after startup): `service <id> is not provided yet; plugin <yours> provides it. Register plugin <yours> before plugin <reader>.`
|
|
151
157
|
|
|
152
158
|
<!-- example: examples/shared-services.ts -->
|
|
153
159
|
```ts
|
|
@@ -195,6 +201,37 @@ export function noteReader(
|
|
|
195
201
|
});
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
/** A plugin that reads the service in `setup` lists it in `requires`: a wrong order stops the start, naming both plugins. */
|
|
205
|
+
export function noteCounter(onCount: (count: number) => void) {
|
|
206
|
+
return definePlugin({
|
|
207
|
+
name: "note-counter",
|
|
208
|
+
requires: [NOTE_INDEX],
|
|
209
|
+
setup: ({ services }) => {
|
|
210
|
+
const index = services.get(NOTE_INDEX);
|
|
211
|
+
return {
|
|
212
|
+
services: [
|
|
213
|
+
{ name: "note-counter", start: () => onCount(index.all().length) },
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A plugin registered before the notes reads them with `lazy`, from a callback that runs after startup. */
|
|
221
|
+
export function earlyNoteReader(onNotes: (notes: readonly string[]) => void) {
|
|
222
|
+
return definePlugin({
|
|
223
|
+
name: "early-note-reader",
|
|
224
|
+
setup: ({ services }) => {
|
|
225
|
+
const index = services.lazy(NOTE_INDEX);
|
|
226
|
+
return {
|
|
227
|
+
services: [
|
|
228
|
+
{ name: "early-note-reader", start: () => onNotes(index().all()) },
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
/** A plugin that provides the same key and lists it in `replaces` takes the place of the one before it. */
|
|
199
236
|
export function shoutingNotes() {
|
|
200
237
|
const stored: string[] = [];
|
|
@@ -1184,6 +1221,9 @@ export function alwaysOn(tools: () => string[]) {
|
|
|
1184
1221
|
Names of npm packages, installed in your project (`bun add pi-web-access`), whose Pi extensions every conversation session loads.
|
|
1185
1222
|
Two plugins that name the same package load it once.
|
|
1186
1223
|
|
|
1224
|
+
`pi-web-access` is also what the built-in delegation worker loads to search and read the web, so `pi-roundtable` lists it as a peer dependency (`>=0.35.0 <0.36.0`): `bun add pi-roundtable` installs it for you, and a project that depends on its own build of it, such as a fork, gets that one copy for both the worker and `piPackages`, with no `overrides` entry.
|
|
1225
|
+
A project that has none installed stops at the `modules` plugin's setup with a `PluginError` that names the command to run.
|
|
1226
|
+
|
|
1187
1227
|
<!-- example: examples/packages.ts -->
|
|
1188
1228
|
```ts
|
|
1189
1229
|
import { definePlugin } from "pi-roundtable";
|
|
@@ -1724,6 +1764,9 @@ The harness supplies what the host would, so a claim or a background turn behave
|
|
|
1724
1764
|
- Once `AGENTS` is given, its `approvals` is the real confirmation judge over `providers.judge` when you pass a judge, so a held action is approved or declined as the agent server decides.
|
|
1725
1765
|
- 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.
|
|
1726
1766
|
|
|
1767
|
+
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.
|
|
1768
|
+
A `services.lazy` reader answers once `testPlugin` returns, from the services you gave.
|
|
1769
|
+
|
|
1727
1770
|
A plugin that fills the `runtime` slot needs none of these for its own runtime: the harness builds it from the slot (with a silent logger, a one-owner identity, in-memory held actions, and one stand-in agent setting) and puts it under `AGENTS`'s `runtime`.
|
|
1728
1771
|
|
|
1729
1772
|
A test for the tools example:
|
|
@@ -1763,6 +1806,8 @@ Use it only with your own store class in a gated database test and always close
|
|
|
1763
1806
|
`useTestLocale()` resets the process-wide locale to English and time zone to UTC; call it after a test that changes either, not from a running plugin.
|
|
1764
1807
|
`testPlugin`'s options take `env` (a partial `HostEnv`) for the `context.env` the plugin sees; it defaults to `en` and `UTC`.
|
|
1765
1808
|
`OWNER_SPEAKER`, `fakeThreads`, and `silentLogger` supply neutral stand-ins for owner turns, dispatch threads, and logging.
|
|
1809
|
+
`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.
|
|
1810
|
+
`partial<Port>({ ... })` is a stand-in for a port that your code takes as an argument rather than reads from a service: it has the members you give and nothing else, and reading another member throws an error that names it, so the test needs no `as unknown as Port` cast.
|
|
1766
1811
|
`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()`.
|
|
1767
1812
|
Only `commands` and `guard` are given; a plugin that reads another member of `DISCORD` in a test gives its own with `servicePair(DISCORD, { ... })`.
|
|
1768
1813
|
|
|
@@ -1827,7 +1872,7 @@ On `SIGTERM` or `SIGINT` the bot stops serving new work last:
|
|
|
1827
1872
|
4. Services stop in the reverse of the order they started.
|
|
1828
1873
|
5. The database pool closes.
|
|
1829
1874
|
|
|
1830
|
-
`shutdown()` returns the exit code, `0` or `1` when a listener, a service, or the pool failed to stop, and every call shares the one shutdown; only `listen()`, which the command line and
|
|
1875
|
+
`shutdown()` returns the exit code, `0` or `1` when a listener, a service, or the pool failed to stop, and every call shares the one shutdown; only `listen()`, which the command line and a host's own entry point call, exits the process with it.
|
|
1831
1876
|
|
|
1832
1877
|
## Errors and their fixes
|
|
1833
1878
|
|
|
@@ -1901,6 +1946,10 @@ Your plugins always run after the built-ins, so the built-in keys show this only
|
|
|
1901
1946
|
Pass what you need in the harness's `services` option, or test that part elsewhere.
|
|
1902
1947
|
`find(KEY)` is `undefined` for a service nobody declares, in the host and in the harness.
|
|
1903
1948
|
|
|
1949
|
+
A key in `requires` is checked before any migration or setup, and a wrong order stops the start with `plugin <yours>: requires service <id>, which plugin <name> provides after it. Register plugin <name> before plugin <yours>, or read the service with services.lazy(KEY) from a callback that runs after startup.`
|
|
1950
|
+
Calling the function `services.lazy(KEY)` returned during setup throws `service <id> is read through lazy() once every plugin is set up. Call it from a service's start or from a handler, not during setup.`
|
|
1951
|
+
A `lazy` key that no registered plugin provides stops the start once every plugin is set up: `plugin <yours>: services.lazy reads service <id>, which no registered plugin provides.`
|
|
1952
|
+
|
|
1904
1953
|
### A setup or a migration that throws
|
|
1905
1954
|
|
|
1906
1955
|
```text
|
|
@@ -1970,6 +2019,22 @@ These are existing compatibility limits, not flags the package overrides in your
|
|
|
1970
2019
|
With `skipLibCheck: false`, the example consumer instead reports 97 dependency-declaration errors, so keep it enabled for this configuration.
|
|
1971
2020
|
Discord-facing signatures, which are in `pi-roundtable/discord` and, for the composed slash commands, in `pi-roundtable/testing`, use the package's pinned `discord.js` types; use those compatible types for panel rows and interaction handlers. `discord.js` is a regular dependency of the package, so a project that imports either entry installs it with `pi-roundtable`.
|
|
1972
2021
|
|
|
2022
|
+
## Developing the core and a host together
|
|
2023
|
+
|
|
2024
|
+
A host pins an exact `pi-roundtable` version, so a change to the core reaches it only after a release.
|
|
2025
|
+
To try a core change in a host first, link the checkout:
|
|
2026
|
+
|
|
2027
|
+
```sh
|
|
2028
|
+
# in the pi-roundtable checkout
|
|
2029
|
+
bun link
|
|
2030
|
+
# in the host
|
|
2031
|
+
bun link pi-roundtable
|
|
2032
|
+
```
|
|
2033
|
+
|
|
2034
|
+
The host then imports the checkout's source, so the core's edits show at once and the host's `bun run typecheck` and `bun test` run against them.
|
|
2035
|
+
A linked checkout resolves its own dependencies from its own `node_modules`, so a host's `overrides` do not reach it: a host that depends on its own build of `pi-web-access` has two copies while linked.
|
|
2036
|
+
When the change is done, 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; the host's checks run once more against what was published.
|
|
2037
|
+
|
|
1973
2038
|
## Name-to-entry index
|
|
1974
2039
|
|
|
1975
2040
|
Type-only exports require `import type` when `verbatimModuleSyntax` is enabled.
|
|
@@ -2160,6 +2225,10 @@ The source area files are not package subpaths.
|
|
|
2160
2225
|
| `fakeThreads` | `pi-roundtable/testing` | value |
|
|
2161
2226
|
| `openTestStore` | `pi-roundtable/testing` | value |
|
|
2162
2227
|
| `servicePair` | `pi-roundtable/testing` | value |
|
|
2228
|
+
| `partial` | `pi-roundtable/testing` | value |
|
|
2229
|
+
| `recordingLogger` | `pi-roundtable/testing` | value |
|
|
2230
|
+
| `RecordingLogger` | `pi-roundtable/testing` | type |
|
|
2231
|
+
| `RecordedLog` | `pi-roundtable/testing` | type |
|
|
2163
2232
|
| `silentLogger` | `pi-roundtable/testing` | value |
|
|
2164
2233
|
| `testDatabaseUrl` | `pi-roundtable/testing` | value |
|
|
2165
2234
|
| `testHost` | `pi-roundtable/testing` | value |
|
|
@@ -2204,6 +2273,8 @@ The source area files are not package subpaths.
|
|
|
2204
2273
|
| `SCHEDULE_TOOLS` | `pi-roundtable/kit` | value |
|
|
2205
2274
|
| `SHELL_TOOLS` | `pi-roundtable/kit` | value |
|
|
2206
2275
|
| `SKILL_LIST_TOOL` | `pi-roundtable/kit` | value |
|
|
2276
|
+
| `packageDir` | `pi-roundtable/kit` | value |
|
|
2277
|
+
| `serveUnix` | `pi-roundtable/kit` | value |
|
|
2207
2278
|
| `ScheduleToolContext` | `pi-roundtable/kit` | type |
|
|
2208
2279
|
| `ScheduleToolName` | `pi-roundtable/kit` | type |
|
|
2209
2280
|
| `ScheduleToolSpec` | `pi-roundtable/kit` | type |
|
|
@@ -2,8 +2,10 @@ import { expect, test } from "bun:test";
|
|
|
2
2
|
import { definePlugin, PluginError, Roundtable } from "pi-roundtable";
|
|
3
3
|
import { servicePair, silentLogger, testPlugin } from "pi-roundtable/testing";
|
|
4
4
|
import {
|
|
5
|
+
earlyNoteReader,
|
|
5
6
|
NOTE_INDEX,
|
|
6
7
|
type NoteIndex,
|
|
8
|
+
noteCounter,
|
|
7
9
|
noteReader,
|
|
8
10
|
notes,
|
|
9
11
|
shoutingNotes,
|
|
@@ -72,3 +74,29 @@ test("a provider that does not provide what it lists is refused after setup", as
|
|
|
72
74
|
}),
|
|
73
75
|
).rejects.toBeInstanceOf(PluginError);
|
|
74
76
|
});
|
|
77
|
+
|
|
78
|
+
test("a plugin that requires the service is refused when the notes come after it, before any setup", async () => {
|
|
79
|
+
const roundtable = new Roundtable({ logger: silentLogger() }, [
|
|
80
|
+
noteCounter(() => undefined),
|
|
81
|
+
notes(),
|
|
82
|
+
]);
|
|
83
|
+
await expect(roundtable.run()).rejects.toThrow(
|
|
84
|
+
"plugin note-counter: requires service my-notes.index, which plugin my-notes provides after it.",
|
|
85
|
+
);
|
|
86
|
+
await roundtable.shutdown("test");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a plugin that requires the service counts the notes written before startup", async () => {
|
|
90
|
+
const counts: number[] = [];
|
|
91
|
+
await withHost(
|
|
92
|
+
[notes(), writer, noteCounter((count) => counts.push(count))],
|
|
93
|
+
() => expect(counts).toEqual([1]),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a plugin registered before the notes reads them lazily, once the host started", async () => {
|
|
98
|
+
const seen: (readonly string[])[] = [];
|
|
99
|
+
await withHost([earlyNoteReader((list) => seen.push(list)), notes()], () =>
|
|
100
|
+
expect(seen).toEqual([[]]),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
@@ -42,6 +42,37 @@ export function noteReader(
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** A plugin that reads the service in `setup` lists it in `requires`: a wrong order stops the start, naming both plugins. */
|
|
46
|
+
export function noteCounter(onCount: (count: number) => void) {
|
|
47
|
+
return definePlugin({
|
|
48
|
+
name: "note-counter",
|
|
49
|
+
requires: [NOTE_INDEX],
|
|
50
|
+
setup: ({ services }) => {
|
|
51
|
+
const index = services.get(NOTE_INDEX);
|
|
52
|
+
return {
|
|
53
|
+
services: [
|
|
54
|
+
{ name: "note-counter", start: () => onCount(index.all().length) },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A plugin registered before the notes reads them with `lazy`, from a callback that runs after startup. */
|
|
62
|
+
export function earlyNoteReader(onNotes: (notes: readonly string[]) => void) {
|
|
63
|
+
return definePlugin({
|
|
64
|
+
name: "early-note-reader",
|
|
65
|
+
setup: ({ services }) => {
|
|
66
|
+
const index = services.lazy(NOTE_INDEX);
|
|
67
|
+
return {
|
|
68
|
+
services: [
|
|
69
|
+
{ name: "early-note-reader", start: () => onNotes(index().all()) },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
/** A plugin that provides the same key and lists it in `replaces` takes the place of the one before it. */
|
|
46
77
|
export function shoutingNotes() {
|
|
47
78
|
const stored: string[] = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A plugin-driven Pi agent server for Discord",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,14 +50,17 @@
|
|
|
50
50
|
"canvas": "3.2.3",
|
|
51
51
|
"discord.js": "14.27.0",
|
|
52
52
|
"pi-mcp-adapter": "2.37.0",
|
|
53
|
-
"pi-web-access": "0.35.0",
|
|
54
53
|
"pino": "10.3.1",
|
|
55
54
|
"typebox": "1.3.34",
|
|
56
55
|
"unpdf": "1.8.1"
|
|
57
56
|
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"pi-web-access": ">=0.35.0 <0.36.0"
|
|
59
|
+
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"typescript": "7.0.2",
|
|
60
62
|
"@biomejs/biome": "2.5.14",
|
|
61
|
-
"@types/bun": "1.4.2"
|
|
63
|
+
"@types/bun": "1.4.2",
|
|
64
|
+
"pi-web-access": "0.35.0"
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -37,6 +37,13 @@ export interface Services {
|
|
|
37
37
|
* Throws like `get` when a plugin declares it but has not been set up yet: that is order, not absence.
|
|
38
38
|
*/
|
|
39
39
|
find<T>(key: ServiceKey<T>): T | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* A reader for a service whose plugin may be set up after this one: call it from a service's
|
|
42
|
+
* `start`, a handler, or any callback that runs after startup, not during setup, where it throws
|
|
43
|
+
* a NotLinkedError. The host checks before startup that a registered plugin provides the key and
|
|
44
|
+
* refuses to boot with a PluginError naming this plugin when none does.
|
|
45
|
+
*/
|
|
46
|
+
lazy<T>(key: ServiceKey<T>): () => T;
|
|
40
47
|
/**
|
|
41
48
|
* Provides a service the plugin declares in `provides`; only while the plugin's setup runs, and
|
|
42
49
|
* once per key.
|
package/src/core/host.ts
CHANGED
|
@@ -278,6 +278,7 @@ export class Roundtable {
|
|
|
278
278
|
// A plugin that replaces a service takes the place of the one that provided it.
|
|
279
279
|
this.#active = replaceServices(this.#plugins);
|
|
280
280
|
this.#services = new ServiceRegistry(this.#active);
|
|
281
|
+
this.#services.checkRequires();
|
|
281
282
|
const providers = resolveProviders(this.#active, this.#options.judgeModel);
|
|
282
283
|
await this.#migrate();
|
|
283
284
|
this.#registry = await collectContributions(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { chmodSync
|
|
1
|
+
import { chmodSync } from "node:fs";
|
|
2
2
|
import type { Server } from "bun";
|
|
3
3
|
import { PluginError } from "../errors.ts";
|
|
4
4
|
import type { Logger } from "../log.ts";
|
|
5
|
+
import { serveUnix } from "../shared/unix-server.ts";
|
|
5
6
|
|
|
6
7
|
/** A handler a plugin attaches to one of the host's configured listeners. */
|
|
7
8
|
export interface HttpRoute {
|
|
@@ -96,29 +97,6 @@ export async function routeRequest(
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
/**
|
|
100
|
-
* Serves HTTP on a unix socket without Bun's 10-second idle timeout, which would cut long
|
|
101
|
-
* turns and quiet model streams. Bun 1.4.2 honors `idleTimeout` on unix sockets, but its
|
|
102
|
-
* types reject the option there, hence the cast. A stale socket file is removed first.
|
|
103
|
-
* (`shared/unix-server.ts` keeps its own copy for standalone worker images.)
|
|
104
|
-
*/
|
|
105
|
-
function serveUnix(
|
|
106
|
-
socketPath: string,
|
|
107
|
-
fetch: (request: Request) => Response | Promise<Response>,
|
|
108
|
-
): Server<undefined> {
|
|
109
|
-
rmSync(socketPath, { force: true });
|
|
110
|
-
const options = {
|
|
111
|
-
unix: socketPath,
|
|
112
|
-
idleTimeout: 0,
|
|
113
|
-
fetch,
|
|
114
|
-
error: serverError,
|
|
115
|
-
};
|
|
116
|
-
// SAFETY: these are Bun's unix-socket options; only `idleTimeout` is missing from its types.
|
|
117
|
-
return Bun.serve(
|
|
118
|
-
options as unknown as Parameters<typeof Bun.serve>[0],
|
|
119
|
-
) as Server<undefined>;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
100
|
/** Serves HTTP on a TCP port with the same idle setting as the unix sockets. */
|
|
123
101
|
function serveTcp(
|
|
124
102
|
port: number,
|
|
@@ -164,7 +142,9 @@ export class HttpListeners {
|
|
|
164
142
|
const handle = (request: Request) =>
|
|
165
143
|
routeRequest(routes, request, listener.id, this.#logger);
|
|
166
144
|
if ("socketPath" in listener) {
|
|
167
|
-
this.#servers.push(
|
|
145
|
+
this.#servers.push(
|
|
146
|
+
serveUnix(listener.socketPath, handle, { error: serverError }),
|
|
147
|
+
);
|
|
168
148
|
chmodSync(listener.socketPath, listener.mode ?? 0o660);
|
|
169
149
|
} else {
|
|
170
150
|
this.#servers.push(
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SessionManager,
|
|
7
7
|
SettingsManager,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { PluginError } from "../../errors.ts";
|
|
9
10
|
import {
|
|
10
11
|
formatModelRef,
|
|
11
12
|
type ModelRef,
|
|
@@ -20,6 +21,18 @@ const WEB_TOOLS = ["web_search", "fetch_content", "get_search_content"];
|
|
|
20
21
|
const WORKER_PROMPT =
|
|
21
22
|
"You are a research worker. Another agent handed you the task below and will pass your report on. Do it with web search and page reading, preferring primary sources. Report in the task's language, self-contained, with a source link for each claim, and say what you could not verify.";
|
|
22
23
|
|
|
24
|
+
/** The folder of `pi-web-access`, a peer dependency the host installs, or a PluginError that says how. */
|
|
25
|
+
function webAccessDir(): string {
|
|
26
|
+
try {
|
|
27
|
+
return packageDir("pi-web-access");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new PluginError(
|
|
30
|
+
"the delegation worker loads pi-web-access, which this project does not have installed. It is a peer dependency of pi-roundtable: run `bun add pi-web-access@0.35.0` (or your own build of it, which the worker then uses too).",
|
|
31
|
+
{ cause: error },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
export interface WebResearchWorkerOptions {
|
|
24
37
|
/** Shared with the rest of the host, so the Codex login refreshes in one place. */
|
|
25
38
|
modelRuntime: ModelRuntime;
|
|
@@ -36,7 +49,7 @@ export class WebResearchWorker {
|
|
|
36
49
|
|
|
37
50
|
constructor(options: WebResearchWorkerOptions) {
|
|
38
51
|
this.#options = options;
|
|
39
|
-
this.#webAccessPath =
|
|
52
|
+
this.#webAccessPath = webAccessDir();
|
|
40
53
|
mkdirSync(options.workDir, { recursive: true });
|
|
41
54
|
}
|
|
42
55
|
|
package/src/core/plugin.ts
CHANGED
|
@@ -281,6 +281,14 @@ export interface RoundtablePlugin {
|
|
|
281
281
|
* list before any setup, and refuses the plugin when setup returns without providing one.
|
|
282
282
|
*/
|
|
283
283
|
provides?: readonly ServiceKey<unknown>[];
|
|
284
|
+
/**
|
|
285
|
+
* The services this plugin's setup reads with `get`, so the host can check them before any
|
|
286
|
+
* setup and any migration: a key no registered plugin provides, or one provided by a plugin
|
|
287
|
+
* registered after this one, is a PluginError that names both plugins and the fix. A service
|
|
288
|
+
* read only after startup, from a callback, is `services.lazy` instead, which does not depend
|
|
289
|
+
* on order. Leave out a service read with `find`, since that one may be absent.
|
|
290
|
+
*/
|
|
291
|
+
requires?: readonly ServiceKey<unknown>[];
|
|
284
292
|
/**
|
|
285
293
|
* Services this plugin replaces. The host drops the plugin that provides them and sets this one
|
|
286
294
|
* up where it stood, so it may read what the plugins before that place provide and nothing
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServiceKey, Services } from "../contract/services.ts";
|
|
2
|
-
import { PluginError } from "../errors.ts";
|
|
2
|
+
import { NotLinkedError, PluginError } from "../errors.ts";
|
|
3
3
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
4
4
|
|
|
5
5
|
/** The host's words for reading a service no plugin declares; `testPlugin` says how to give one instead. */
|
|
@@ -29,6 +29,22 @@ function declaredBy(plugin: RoundtablePlugin): readonly ServiceKey<unknown>[] {
|
|
|
29
29
|
return provides;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** The services a plugin requires, checked to be a list of keys; empty when it requires none. */
|
|
33
|
+
function requiredBy(plugin: RoundtablePlugin): readonly ServiceKey<unknown>[] {
|
|
34
|
+
const { requires } = plugin;
|
|
35
|
+
if (requires === undefined) return [];
|
|
36
|
+
if (!Array.isArray(requires))
|
|
37
|
+
throw new PluginError(
|
|
38
|
+
`plugin ${plugin.name}: requires must be a list of service keys made with serviceKey().`,
|
|
39
|
+
);
|
|
40
|
+
for (const key of requires)
|
|
41
|
+
if (typeof key?.id !== "string" || key.id === "")
|
|
42
|
+
throw new PluginError(
|
|
43
|
+
`plugin ${plugin.name}: requires has an entry that is not a service key; make each with serviceKey("<id>").`,
|
|
44
|
+
);
|
|
45
|
+
return requires;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/** The services a plugin replaces, checked to be a list of keys; empty when it replaces none. */
|
|
33
49
|
function replacedBy(plugin: RoundtablePlugin): readonly ServiceKey<unknown>[] {
|
|
34
50
|
const { replaces } = plugin;
|
|
@@ -127,10 +143,16 @@ export class ServiceRegistry {
|
|
|
127
143
|
#setting: RoundtablePlugin | undefined;
|
|
128
144
|
/** The plugins that have read a service, such as one that adds its commands through it. */
|
|
129
145
|
readonly #readers = new Set<RoundtablePlugin>();
|
|
146
|
+
readonly #plugins: readonly RoundtablePlugin[];
|
|
147
|
+
/** What each plugin asked `lazy` for, to refuse a key nobody provides once every plugin is set up. */
|
|
148
|
+
readonly #lazy: { plugin: RoundtablePlugin; key: ServiceKey<unknown> }[] = [];
|
|
149
|
+
/** Whether every plugin is set up, from which a `lazy` reader returns its service. */
|
|
150
|
+
#settled = false;
|
|
130
151
|
|
|
131
152
|
/** `advice` says how to get a service no plugin declares; the host's is to register one that does. */
|
|
132
153
|
constructor(plugins: readonly RoundtablePlugin[], advice = HOST_ADVICE) {
|
|
133
154
|
this.#advice = advice;
|
|
155
|
+
this.#plugins = plugins;
|
|
134
156
|
for (const plugin of plugins)
|
|
135
157
|
for (const key of declaredBy(plugin)) {
|
|
136
158
|
const other = this.#declaredBy.get(key.id);
|
|
@@ -147,6 +169,45 @@ export class ServiceRegistry {
|
|
|
147
169
|
this.#values.set(key.id, value);
|
|
148
170
|
}
|
|
149
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Refuses a plugin whose `requires` names a service no registered plugin or test provides, or
|
|
174
|
+
* one a plugin registered after it provides. It runs before any migration or setup, so a wrong
|
|
175
|
+
* order fails at the start with both plugins named.
|
|
176
|
+
*/
|
|
177
|
+
checkRequires(): void {
|
|
178
|
+
this.#plugins.forEach((plugin, index) => {
|
|
179
|
+
for (const key of requiredBy(plugin)) {
|
|
180
|
+
if (this.#values.has(key.id)) continue;
|
|
181
|
+
const provider = this.#declaredBy.get(key.id);
|
|
182
|
+
if (!provider)
|
|
183
|
+
throw new PluginError(
|
|
184
|
+
`plugin ${plugin.name}: requires service ${key.id}, which no registered plugin provides. ${key.absent ?? this.#advice}`,
|
|
185
|
+
);
|
|
186
|
+
if (provider === plugin)
|
|
187
|
+
throw new PluginError(
|
|
188
|
+
`plugin ${plugin.name}: requires service ${key.id}, which it provides itself. A plugin cannot require what it provides.`,
|
|
189
|
+
);
|
|
190
|
+
if (this.#plugins.indexOf(provider) > index)
|
|
191
|
+
throw new PluginError(
|
|
192
|
+
`plugin ${plugin.name}: requires service ${key.id}, which plugin ${provider.name} provides after it. Register plugin ${provider.name} before plugin ${plugin.name}, or read the service with services.lazy(KEY) from a callback that runs after startup.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Called once every plugin is set up: `lazy` readers start answering, and a key a plugin asked
|
|
200
|
+
* for that nobody provides is refused with the plugin named.
|
|
201
|
+
*/
|
|
202
|
+
settle(): void {
|
|
203
|
+
for (const { plugin, key } of this.#lazy)
|
|
204
|
+
if (!this.#values.has(key.id))
|
|
205
|
+
throw new PluginError(
|
|
206
|
+
`plugin ${plugin.name}: services.lazy reads service ${key.id}, which no registered plugin provides. ${key.absent ?? this.#advice}`,
|
|
207
|
+
);
|
|
208
|
+
this.#settled = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
150
211
|
/** The service, for the host's own reads; throws like a plugin's `get`. */
|
|
151
212
|
get<T>(key: ServiceKey<T>): T {
|
|
152
213
|
return this.#read(key, undefined, true) as T;
|
|
@@ -185,6 +246,17 @@ export class ServiceRegistry {
|
|
|
185
246
|
this.#readers.add(plugin);
|
|
186
247
|
return this.#read(key, plugin, false);
|
|
187
248
|
},
|
|
249
|
+
lazy: <T>(key: ServiceKey<T>) => {
|
|
250
|
+
this.#readers.add(plugin);
|
|
251
|
+
this.#lazy.push({ plugin, key });
|
|
252
|
+
return () => {
|
|
253
|
+
if (!this.#settled)
|
|
254
|
+
throw new NotLinkedError(
|
|
255
|
+
`service ${key.id} is read through lazy() once every plugin is set up. Call it from a service's start or from a handler, not during setup.`,
|
|
256
|
+
);
|
|
257
|
+
return this.#values.get(key.id) as T;
|
|
258
|
+
};
|
|
259
|
+
},
|
|
188
260
|
provide: (key, value) => {
|
|
189
261
|
if (this.#setting !== plugin)
|
|
190
262
|
throw new PluginError(
|
|
@@ -77,7 +77,7 @@ export class SessionFactory {
|
|
|
77
77
|
const linked = this.#options.sessions();
|
|
78
78
|
this.#linked = {
|
|
79
79
|
...linked,
|
|
80
|
-
extensionPaths: linked.piPackages.map(packageDir),
|
|
80
|
+
extensionPaths: linked.piPackages.map((name) => packageDir(name)),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
return this.#linked;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The installed package's folder, as Pi's `additionalExtensionPaths` takes it. The package is
|
|
6
|
+
* looked up from `from`, which is `import.meta.url` of the module that asks, so a host finds its
|
|
7
|
+
* own dependencies even when `pi-roundtable` is linked or installed apart from them; without it
|
|
8
|
+
* the lookup starts at the core's own files.
|
|
9
|
+
*/
|
|
10
|
+
export function packageDir(name: string, from: string | URL = import.meta.url) {
|
|
11
|
+
return dirname(createRequire(from).resolve(`${name}/package.json`));
|
|
9
12
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import type { Server } from "bun";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Serves HTTP on a unix socket without Bun's 10-second idle timeout, which would cut long
|
|
6
|
+
* turns and quiet model streams. Bun 1.4.2 honors `idleTimeout` on unix sockets, but its
|
|
7
|
+
* types reject the option there, hence the cast. A stale socket file is removed first.
|
|
8
|
+
* `error` answers a failure raised outside the fetch handler; without it Bun's own page is sent.
|
|
9
|
+
*/
|
|
10
|
+
export function serveUnix(
|
|
11
|
+
socketPath: string,
|
|
12
|
+
fetch: (request: Request) => Response | Promise<Response>,
|
|
13
|
+
options: { error?: (error: Error) => Response | Promise<Response> } = {},
|
|
14
|
+
): Server<undefined> {
|
|
15
|
+
rmSync(socketPath, { force: true });
|
|
16
|
+
const serveOptions = {
|
|
17
|
+
unix: socketPath,
|
|
18
|
+
idleTimeout: 0,
|
|
19
|
+
fetch,
|
|
20
|
+
...(options.error ? { error: options.error } : {}),
|
|
21
|
+
};
|
|
22
|
+
// SAFETY: these are Bun's unix-socket options; only `idleTimeout` is missing from its types.
|
|
23
|
+
return Bun.serve(
|
|
24
|
+
serveOptions as unknown as Parameters<typeof Bun.serve>[0],
|
|
25
|
+
) as Server<undefined>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Members a probe may ask of any object: a test's stand-in is not refused for them. */
|
|
2
|
+
const PROBED = new Set(["then", "toJSON", "asymmetricMatch"]);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A stand-in for a port the code under test takes, made of only the members the test gives.
|
|
6
|
+
* Reading any other member throws an error that names it, so a test learns at once which member
|
|
7
|
+
* the code reaches for, instead of meeting `undefined is not a function` or a hidden cast.
|
|
8
|
+
* `partial<AgentTeam>({ announce })` is an `AgentTeam` to the type checker.
|
|
9
|
+
*/
|
|
10
|
+
export function partial<T extends object>(given: Partial<T>): T {
|
|
11
|
+
// SAFETY: the stand-in answers only the members given; any other read throws before it is used.
|
|
12
|
+
return new Proxy(given, {
|
|
13
|
+
get(target, member, receiver) {
|
|
14
|
+
if (member in target || typeof member === "symbol" || PROBED.has(member))
|
|
15
|
+
return Reflect.get(target, member, receiver);
|
|
16
|
+
throw new Error(
|
|
17
|
+
`partial() was given no "${member}". Give it where the stand-in is made: partial({ ${member}: ... }).`,
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
}) as T;
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Logger } from "../log.ts";
|
|
2
|
+
|
|
3
|
+
/** One line a recording logger kept. */
|
|
4
|
+
export interface RecordedLog {
|
|
5
|
+
level: "debug" | "info" | "warn" | "error" | "fatal";
|
|
6
|
+
/** The fields of the call, with those of every `child` the logger came from. */
|
|
7
|
+
fields: Record<string, unknown>;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A logger that keeps what it is asked to write, and the lines it kept. */
|
|
12
|
+
export interface RecordingLogger {
|
|
13
|
+
readonly logger: Logger;
|
|
14
|
+
/** Every line written through the logger or a child of it, in order. */
|
|
15
|
+
readonly lines: RecordedLog[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A logger for a test that checks what the code logs: it writes nowhere, and `lines` holds each
|
|
20
|
+
* call with its level, fields (a child's included), and message.
|
|
21
|
+
*/
|
|
22
|
+
export function recordingLogger(): RecordingLogger {
|
|
23
|
+
const lines: RecordedLog[] = [];
|
|
24
|
+
const make = (bound: Record<string, unknown>): Logger => {
|
|
25
|
+
const write =
|
|
26
|
+
(level: RecordedLog["level"]) =>
|
|
27
|
+
(first: object | string, message?: string): void => {
|
|
28
|
+
lines.push(
|
|
29
|
+
typeof first === "string"
|
|
30
|
+
? { level, fields: { ...bound }, message: first }
|
|
31
|
+
: { level, fields: { ...bound, ...first }, message: message ?? "" },
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
debug: write("debug"),
|
|
36
|
+
info: write("info"),
|
|
37
|
+
warn: write("warn"),
|
|
38
|
+
error: write("error"),
|
|
39
|
+
fatal: write("fatal"),
|
|
40
|
+
child: (fields) => make({ ...bound, ...fields }),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
return { logger: make({}), lines };
|
|
44
|
+
}
|
package/src/discord/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The Discord entry: everything that names a discord.js type, apart from the main and kit entries.
|
|
2
|
-
//
|
|
2
|
+
// Versioned like the main entry: a breaking change comes in a minor release before 1.0 and is listed in the changelog.
|
|
3
3
|
|
|
4
4
|
export type { DiscordServices } from "../core/builtin/discord.ts";
|
|
5
5
|
export { DISCORD } from "../core/builtin/discord.ts";
|
package/src/kit/channels.ts
CHANGED
package/src/kit/domain.ts
CHANGED
package/src/kit/holds.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Plugin helpers, versioned like the main entry; see the plugin guide.
|
|
2
2
|
// The chain a host links from every plugin's hold rules, to test a rule set as the host runs it.
|
|
3
3
|
|
|
4
4
|
export { holdChain } from "../core/holds.ts";
|
package/src/kit/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Helpers for owner commands, claims, and naming existing core parts; versioned like the main entry.
|
|
2
2
|
|
|
3
3
|
export type { ChannelQueue } from "./channels.ts";
|
|
4
4
|
export {
|
|
@@ -62,6 +62,7 @@ export {
|
|
|
62
62
|
thinkingLine,
|
|
63
63
|
zonedStamp,
|
|
64
64
|
} from "./presentation.ts";
|
|
65
|
+
export { packageDir, serveUnix } from "./process.ts";
|
|
65
66
|
export {
|
|
66
67
|
SHELL_TOOLS,
|
|
67
68
|
shellHoldRule,
|
package/src/kit/judging.ts
CHANGED
package/src/kit/memory.ts
CHANGED
package/src/kit/mirror.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Plugin helpers, versioned like the main entry; see the plugin guide.
|
|
2
2
|
// Mirror a built-in tool in a worker that cannot reach the host: the specs and the dispatcher.
|
|
3
3
|
|
|
4
4
|
export type { ScheduleToolContext } from "../core/modules/schedules/schedule-tools.ts";
|
package/src/kit/presentation.ts
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Plugin helpers, versioned like the main entry; see the plugin guide.
|
|
2
|
+
// Running a worker process of your own: where an installed package lives, and a unix-socket HTTP server.
|
|
3
|
+
|
|
4
|
+
export { packageDir } from "../core/shared/package-dir.ts";
|
|
5
|
+
export { serveUnix } from "../core/shared/unix-server.ts";
|
package/src/kit/shell.ts
CHANGED
package/src/kit/skills.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Plugin helpers, versioned like the main entry; see the plugin guide.
|
|
2
2
|
// Skills and repositories: the names a repository shelf accepts, and the tool that lists skills.
|
|
3
3
|
|
|
4
4
|
export { checkRepoName } from "../core/modules/skills/repo-name.ts";
|
package/src/kit/support.ts
CHANGED
package/src/kit/threads.ts
CHANGED
package/src/kit/tools.ts
CHANGED
package/src/kit/worker.ts
CHANGED
package/src/testing.ts
CHANGED
|
@@ -63,7 +63,6 @@ import { type ToolTierTable, toolTiers } from "./core/tool-tiers.ts";
|
|
|
63
63
|
// Fixtures for plugin tests.
|
|
64
64
|
|
|
65
65
|
export { silentLogger } from "./core/log.ts";
|
|
66
|
-
|
|
67
66
|
export type { TestStore } from "./core/testing/database.ts";
|
|
68
67
|
export {
|
|
69
68
|
describeDb,
|
|
@@ -74,8 +73,13 @@ export {
|
|
|
74
73
|
export { eagerText, useEagerCatalog } from "./core/testing/eager-catalog.ts";
|
|
75
74
|
export type { TestLocale } from "./core/testing/locale.ts";
|
|
76
75
|
export { useTestLocale } from "./core/testing/locale.ts";
|
|
77
|
-
|
|
78
76
|
export { OWNER_SPEAKER } from "./core/testing/owner.ts";
|
|
77
|
+
export { partial } from "./core/testing/partial.ts";
|
|
78
|
+
export type {
|
|
79
|
+
RecordedLog,
|
|
80
|
+
RecordingLogger,
|
|
81
|
+
} from "./core/testing/recording-logger.ts";
|
|
82
|
+
export { recordingLogger } from "./core/testing/recording-logger.ts";
|
|
79
83
|
export type { TestHost, TestHostOptions } from "./core/testing/test-host.ts";
|
|
80
84
|
export { testHost } from "./core/testing/test-host.ts";
|
|
81
85
|
export type { FakeThreadHost } from "./core/testing/thread-host.ts";
|
|
@@ -393,6 +397,7 @@ export async function testPlugin(
|
|
|
393
397
|
logger,
|
|
394
398
|
}),
|
|
395
399
|
);
|
|
400
|
+
services.checkRequires();
|
|
396
401
|
const context: Omit<PluginContext, "services"> = {
|
|
397
402
|
logger,
|
|
398
403
|
env,
|