pi-roundtable 0.7.19 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +44 -11
  3. package/README.zh-TW.md +44 -9
  4. package/docs/migrating-0.8.md +132 -0
  5. package/docs/plugins.md +267 -23
  6. package/examples/discord-adapter.test.ts +42 -0
  7. package/examples/discord-adapter.ts +26 -0
  8. package/examples/echo-runtime.ts +1 -1
  9. package/examples/headless.test.ts +72 -0
  10. package/examples/headless.ts +31 -0
  11. package/examples/http.test.ts +31 -1
  12. package/examples/http.ts +39 -0
  13. package/package.json +3 -2
  14. package/src/cli/cli.ts +15 -4
  15. package/src/cli/doctor.ts +21 -5
  16. package/src/cli/init.ts +9 -1
  17. package/src/cli/project.ts +28 -2
  18. package/src/cli/templates.ts +46 -14
  19. package/src/core/agents/agent-team-fixture.ts +6 -0
  20. package/src/core/agents/team-turns.ts +6 -1
  21. package/src/core/builtin/agent-server.ts +30 -95
  22. package/src/core/builtin/modules.ts +133 -34
  23. package/src/core/config/adapters.ts +57 -0
  24. package/src/core/config/config.ts +174 -77
  25. package/src/core/contract/channels.ts +4 -0
  26. package/src/core/contract/providers.ts +3 -2
  27. package/src/core/contract/runtime.ts +8 -4
  28. package/src/core/contract/surface.ts +13 -0
  29. package/src/core/conversations/conversation-registry.ts +42 -0
  30. package/src/core/conversations/conversation-store.ts +113 -0
  31. package/src/core/conversations/conversations-plugin.ts +29 -0
  32. package/src/core/define-roundtable.ts +124 -54
  33. package/src/core/discord/discord-adapter.ts +9 -0
  34. package/src/core/discord/owner-cards.ts +19 -8
  35. package/src/core/domain/conversation.ts +5 -0
  36. package/src/core/domain/owner-prompts.ts +1 -1
  37. package/src/core/domain/ports.ts +6 -0
  38. package/src/core/domain/progress.ts +10 -0
  39. package/src/core/events.ts +1 -0
  40. package/src/core/host.ts +6 -3
  41. package/src/core/http/listeners.ts +174 -21
  42. package/src/core/http/websocket.ts +316 -0
  43. package/src/core/i18n/cards.ts +5 -7
  44. package/src/core/ops/error-reporter.ts +24 -12
  45. package/src/core/plugin.ts +14 -0
  46. package/src/core/registry/providers.ts +1 -1
  47. package/src/core/routing/channel-router.ts +8 -0
  48. package/src/core/routing/conversation-turns.ts +55 -1
  49. package/src/core/routing/surface-port.ts +3 -0
  50. package/src/core/runtime/extensions/confirmation-gate.ts +10 -2
  51. package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
  52. package/src/core/runtime/pending-confirmation-store.ts +42 -4
  53. package/src/core/runtime/pi-agent-runtime.ts +13 -6
  54. package/src/core/runtime/prompt-slot.ts +2 -2
  55. package/src/core/runtime/runtime-plugin.ts +160 -0
  56. package/src/core/runtime/turn-progress.ts +116 -0
  57. package/src/core/services.ts +15 -1
  58. package/src/core/shared/unix-server.ts +35 -5
  59. package/src/core/testing/modules.ts +47 -23
  60. package/src/core/testing/surface-contract.ts +383 -0
  61. package/src/core/testing/test-host.ts +27 -9
  62. package/src/discord/index.ts +2 -0
  63. package/src/index.ts +18 -0
  64. package/src/testing.ts +41 -5
  65. package/templates/adapters/web/.env.example +24 -0
  66. package/templates/adapters/web/README.md +15 -0
  67. package/templates/adapters/web/package.json.tmpl +26 -0
  68. package/templates/adapters/web/persona/assistant.md +2 -0
  69. package/templates/adapters/web/roundtable.config.ts +55 -0
  70. package/templates/package.json.tmpl +1 -0
  71. package/templates/plugins/self-compact.ts +11 -0
  72. package/templates/roundtable.config.ts +2 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.8.0] - 2026-10-07
9
+
10
+ A Discord project from 0.7 upgrades without changing its configuration; [Migrating to 0.8](docs/migrating-0.8.md) covers what to check and how to start a host without Discord.
11
+ The behavior changes are who may approve a held call (Fixed), two built-in plugins before `agent-server` and `context.turns` on `RUNTIME` (Changed), and the start of a project `roundtable init` creates (Fixed).
12
+
13
+ ### Added
14
+
15
+ - [Migrating to 0.8](docs/migrating-0.8.md), the upgrade guide. The README describes pi-roundtable as an agent server reached through Discord or the web chat, built for one owner, with a threat model; running it for several people is at the operator's risk.
16
+
17
+ - The `adapters` configuration key and `discord()` from `pi-roundtable/discord`: `adapters: [discord({ token, guild, entryChannel, ... })]` takes the same settings as the top-level `discord` and assembles the same plugins in the same order; `roundtable doctor` reads it the same way. The top-level `discord` stays the default form and what `roundtable init` writes. Configuring Discord in both places, a second Discord adapter, an unknown adapter, or an entry that is not an adapter is a configuration error naming `adapters[<n>]`; a chat network that comes as a plugin, such as `webChat()`, stays in `plugins`. Types: `AdapterConfig` and `DiscordConfig` from `pi-roundtable`, `DiscordAdapterConfig` from `pi-roundtable/discord`; value: `discord`.
18
+ - `roundtable init --adapter web` creates a project without Discord around the new pi-roundtable-webchat package: an OpenID Connect verifier, an access map from the token's roles, a persona in `persona/assistant.md` whose `selection` names its tools (the hello plugin's, and no `schedule_*`, `delegate_task`, or web tools), and a `.env.example` without Discord; `--adapter discord` is the default and unchanged.
19
+ - pi-roundtable-webchat, a new package: a WebSocket chat adapter for people an OpenID Connect provider signs in, with private conversations, live progress, approval cards, and a REST API; see its changelog.
20
+ - The chat surface contract in `pi-roundtable/testing`: `describeSurfaceContract(name, make)` registers one test per check on a `SurfaceContractSubject` (a surface, a channel, and one person who writes, sees `SurfaceObservation`s, and answers approvals), and `checkSurfaceContract(make)` returns the checks a surface broke as `SurfaceContractFailure`s. It checks the prefix and channel, delivery, every chunk of a reply, and, where offered, files, idempotent typing and stop controls, progress, and approvals that resolve as answered or `cancelled` when the turn stops. Names: `describeSurfaceContract`, `checkSurfaceContract`, `SurfaceContractSubject`, `SurfaceObservation`, `SurfaceContractFailure`.
21
+ - A host without Discord: leave `discord` out of the configuration and the host runs no Discord plugin, no agent server, no agents, and no skills, while the runtime plugin still runs every turn of `context.turns` over the plugins' own surfaces. `http` becomes optional (`http.publicUrl` stays required with Discord), and without it no listener opens. `agents`, `skills`, and `ops.agent` without Discord are configuration errors; `notify_owner` is not registered; `schedule_*` and `delegate_task` are not registered, since their runs are turns of the `owner` background target the agent server contributes; a plugin that contributes it brings them back, working in a conversation a chat surface carries and refusing elsewhere. `roundtable doctor` skips the Discord checks, and `testHost({ discord: false })` boots such a host. See [a host without Discord](docs/plugins.md#a-host-without-discord) and `examples/headless.ts`.
22
+ - `ops: { conversation: "<surface>:<id>" }` reports the process's own errors to a conversation instead of an agent. The host does not start, with a `ConfigError` naming `ops.conversation` and the reason, when no chat surface serves the key, no plugin's claim owns it, no plugin contributes the `owner` background target the report turn runs as, or the claim that owns it takes no background turns. Without Discord nothing contributes `owner` unless a plugin does. The web chat takes no error reports in 0.8: a `web:` conversation is only its signed-in person's, and its claim takes no background turns, so `web:<id>` fails at startup.
23
+ - `ConversationPort.owns(channel)`: whether a plugin's claim owns a channel, and `ConversationPort.takesBackground(channel)`: whether that claim runs background turns there. A test double that implements the whole port, rather than a `Pick` of it, adds the methods; `testPlugin`'s stand-in has them.
24
+ - The conversation registry, `CONVERSATIONS`, provided by the new built-in `conversations` plugin (table `conversations`, ledger id `conversations/conversations`). `context.turns.run` records each conversation before its turn runs: at the first turn its key, surface, kind, visibility, principal and title, and later only that it was active; a turn whose conversation cannot be recorded does not run, and `run` rejects before the turn starts, so a claim that answers its person tells them itself. `ConversationTurnInput.conversation: { visibility: "private" | "shared", title? }` records a conversation as the speaker's own (default `shared`). The registry offers `register`, `get`, `list({ principal }?)` and `setTitle`; no session file moves. Types: `ConversationRegistry`, `ConversationRecord`, `ConversationRegistration`, `ConversationVisibility`.
25
+ - Live turn progress for a turn run through `context.turns`: the `turnProgress(event)` handler and the optional `ChatSurface.progress(channel, event)` hear a `TurnProgress` as the turn goes, `{ type: "text", delta }` (text joined over 250 ms, sent before any tool event, never the thinking), `{ type: "tool_start", id, tool, preview? }` (a one-line preview of the arguments of at most 80 characters, never their full text), and `{ type: "tool_end", id, tool, ok }`. The Pi runtime reports it through the new `TurnRequest.progress`; the optional `SurfacePort.progress` and `EventSink.turnProgress` carry it (the host's surface port always has `progress`; a port of your own written before 0.8 needs no change). Types: `TurnProgress`, `TurnProgressEvent`.
26
+ - `RUNTIME`, the runtime every conversation turn runs on, provided by the new built-in `runtime` plugin that every host registers just before the agent server. It is the `runtime` slot's runtime when a plugin fills the slot, and Pi's otherwise; `AGENTS.runtime` is the same instance, and `context.turns` now reads `RUNTIME`, so turns no longer need the agent server. `testPlugin` takes `servicePair(RUNTIME, runtime)`, given whole; `servicePair(AGENTS, { runtime })` keeps working.
27
+ - WebSocket routes: an `HttpRoute` may declare `websocket` (`WebSocketRoute`) to take WebSocket upgrades on its path, while every other request still reaches `handle`. An upgrade is a `GET` with `Upgrade: websocket`, a `Sec-WebSocket-Key`, and `Sec-WebSocket-Version: 13`; a `GET` missing the key or version gets `400` and other methods reach `handle`, all without calling `accept`. It then passes the route's `origins` allowlist (a missing or other Origin gets `403`; `"any"` skips the check; an entry that is not exactly a `scheme://host[:port]` origin, special such as `https://…` or non-special such as `chrome-extension://<id>` or `tauri://localhost`, is refused at startup, as is `"null"`) and `maxConnections` (256 by default; one more gets `503`), and then `accept(request)`, which authenticates before any socket opens and returns `{ data, headers? }` or a refusing `Response` (`WebSocketAccept`). Handlers get a `RouteSocket` (`data`, `send`, `close`); `send` answers a `WebSocketSendResult`: `"sent"`, `"queued"` behind a slow reader, or `"dropped"`. `maxBufferedBytes` (1 MiB by default) is checked before each `send`: once that many bytes wait for a client that stopped reading, `send` answers `"dropped"` and the socket is cut without a close frame, so the client and the route's `close` see 1006; nothing is dropped silently. A `send` that finds fewer waiting goes out whole, so one message larger than the limit still reaches a reading client and the bytes waiting can reach the limit plus one message; an empty message is `"sent"`. Every limit (`maxMessageBytes`, `maxBufferedBytes`, `maxConnections`, `rate.messages`, `rate.perMs`) must be a positive integer, or startup fails naming the route and field. Handlers may return any value, which is ignored; a promise is awaited. A message over `maxMessageBytes` (64 KiB by default) closes the socket with 1009, one over `rate` (120 a minute by default) with 1008, and a throwing handler closes only its socket with 1011 and logs the route and listener, never the URL. A throwing `accept` answers 500 with the fixed body. On shutdown every socket is closed with 1001 and the host waits up to five seconds for the routes' `close` handlers before stopping services. A route with `websocket` whose `methods` leave out `GET` is refused at startup. `pi-roundtable/kit`'s `serveUnix` takes an optional `websocket` handler and passes the server to `fetch`, which may answer nothing only when `websocket` is given. The guide's example spends each ticket once, and the guide warns against combining `origins: "any"` with cookie authentication.
28
+
29
+ ### Changed
30
+
31
+ - Two built-in plugins join every host, just before `agent-server`: `conversations`, which provides `CONVERSATIONS`, and `runtime`, which provides `RUNTIME`. The order of the other plugins and of their migrations is unchanged; a plugin that matches built-in plugins by name, or reads `serviceStarted` events, sees the two new names.
32
+ - `context.turns` runs every turn on `RUNTIME` instead of the agent server's runtime, so it works on a host without the agent server. `AGENTS.runtime` is the same instance, and a plugin that fills the `runtime` provider slot keeps replacing it.
33
+ - `PendingConfirmation` gains `speakerId`, the speaker whose turn held the calls. A runtime with its own `HeldActionStore` keeps it across a restart; a restored held call without one is the owner's to approve.
34
+ - `HttpRoute` gains the optional `websocket`, and `pi-roundtable/kit`'s `serveUnix` passes the server to `fetch` as its second argument, with an overload that takes a `websocket` handler; see WebSocket routes under Added.
35
+ - The official packages release in lockstep at this version: pi-roundtable-webchat is new; pi-roundtable-web lists the conversations the registry records; pi-roundtable-sandbox's `apiKey` and `oauthToken` receive the turn's `{ channel, speaker }`. See their changelogs.
36
+ - The Pi runtime and the held actions' table move from the agent server to the `runtime` plugin. The `held-actions` migration is recorded once more under `runtime/held-actions`; it creates the table only when it is missing, so an existing database is unchanged.
37
+ - `RoundtableConfig.discord` and `RoundtableConfig.http` (and `http.publicUrl`) are optional in the type; code that reads them from a config object now checks for them.
38
+ - `RuntimeDeps.agents` is optional and read when a turn runs: the runtime is built before the agent server sets up, and it is `undefined` on a host without the agent server.
39
+
40
+ ### Fixed
41
+
42
+ - A project `roundtable init` creates now starts. Pi's runtime requires the `compact_session` tool in every session, which comes from the Pi package pi-self-compact, and neither template loaded it, so `roundtable start` stopped at the preflight with `required tools are not registered: compact_session`. Both templates (`--adapter discord` and `--adapter web`) now pin `pi-self-compact` 0.2.0 and list a `plugins/self-compact.ts` that loads it with `piPackages`; the preflight's message names the package and the fix. An existing project adds the same: `bun add pi-self-compact`, then a plugin with `piPackages: ["pi-self-compact"]`.
43
+ - An approval card now gets the lowest tier that may approve its call (`OwnerPrompts.confirm`'s `minTier`, the higher of the tool's tier and the hold rule's `approvalTier`). The runtime dropped it on the way to the surface, so every card was the owner's alone. On Discord, a held call of a member- or admin-tier tool may now be approved by the speaker whose turn held it, when their tier is at least `minTier` (checked again when they press), and by the owner; nobody else in the channel may approve it, whatever their tier, and in the owner's own turn the card stays the owner's. A speaker below `minTier` leaves the card to the owner. The chat surface contract gains a check that another person cannot answer the person's approval, through the optional `SurfaceContractSubject.stranger`, and its approvals now ask for exactly the speaker's tier.
44
+ - Behavior change: a held call in an agent's channel or a group room is approved by text only by the speaker whose turn held it, at a tier that holds its tools (read again from their message), or by the owner. Before, any speaker of that tier in the channel could approve another person's held call with a message such as "yes", and in a group room that also woke the seat. `PendingConfirmation.speakerId` records the speaker, and the held actions' table gains the column `speaker_id` (migration `runtime/held-actions-speaker`, a nullable column; rows held before it are the owner's to approve), with `speaker_held_at` (migration `runtime/held-actions-speaker-hold`, a nullable column), the hold the speaker belongs to: 0.7 replaces held actions without touching `speaker_id`, so after a rollback and an upgrade a speaker counts only while the row still holds what it was written with, and otherwise the owner approves.
45
+
8
46
  ## [0.7.19] - 2026-10-07
9
47
 
10
48
  ### Fixed
package/README.md CHANGED
@@ -5,13 +5,13 @@ English | [Traditional Chinese](README.zh-TW.md)
5
5
  Docs: <https://pi-roundtable.wayneh.tw>
6
6
  Source and issues: <https://github.com/wayne930242/pi-roundtable>
7
7
 
8
- A Discord agent server on [Pi](https://github.com/earendil-works/pi).
9
- Each AI agent has its own channel and conversation in your Discord server.
10
- They share tools and memory.
8
+ An agent server on [Pi](https://github.com/earendil-works/pi) that you talk to through Discord, or through a web chat.
9
+ In Discord, each AI agent has its own channel and conversation in your server, and they share tools and memory.
10
+ On the web, people your OpenID Connect provider signs in chat with an assistant in private conversations, through [pi-roundtable-webchat](packages/webchat).
11
11
  You can extend the bot with TypeScript plugins.
12
12
 
13
- - Built for one owner and one Discord server.
14
- You can allow others to talk to the agents, but that setup and its risks are the operator's.
13
+ - Built for one owner.
14
+ Discord and the web chat both work, and you can let other people talk to the assistant, but running it for several people is at the operator's risk: read the [threat model](#threat-model) first.
15
15
  - Bun only.
16
16
  The package ships its TypeScript source, so there is no build step.
17
17
  - MIT licensed.
@@ -21,9 +21,9 @@ You can extend the bot with TypeScript plugins.
21
21
  - [Bun](https://bun.sh/docs/installation) 1.3 or newer.
22
22
  - PostgreSQL.
23
23
  The project `init` creates has a `docker-compose.yml` that runs one.
24
- - A Discord bot: an application with a bot user, the Message Content intent turned on, and an invitation to your server.
25
24
  - A model login: the API key of the provider of the model you choose (`ANTHROPIC_API_KEY` for `anthropic/...`), or a login made with Pi.
26
- - An address that reaches the process from the internet, such as a tunnel, because Discord fetches the agents' avatars from it.
25
+ - For Discord: a Discord bot (an application with a bot user, the Message Content intent turned on, and an invitation to your server), and an address that reaches the process from the internet, such as a tunnel, because Discord fetches the agents' avatars from it.
26
+ - For the web chat: an OpenID Connect provider that issues access tokens for the chat's API, and a reverse proxy that serves the host over HTTPS.
27
27
 
28
28
  ## Five minutes
29
29
 
@@ -39,6 +39,8 @@ bunx roundtable start
39
39
 
40
40
  `init` writes a working project without asking for secrets.
41
41
  It refuses to write anything when Bun is missing or too old, or when a file it would create already exists.
42
+ `init --adapter web` writes a project without Discord around the web chat instead; see [web chat](#web-chat).
43
+ Both load the Pi package pi-self-compact, whose `compact_session` tool every session needs.
42
44
 
43
45
  ### `.env`
44
46
 
@@ -54,6 +56,8 @@ Bun loads `.env` by itself, and `.gitignore` keeps it out of Git.
54
56
  | `MODEL` | The agents' model, `<provider>/<id>` |
55
57
  | `PUBLIC_URL` | The address that reaches this process from the internet |
56
58
 
59
+ A web chat project asks for `OWNER_NAME`, `DATABASE_URL`, and `MODEL`, and for the OpenID Connect issuer, audience, and signing keys address, the roles that may chat, and the origins of the pages that open it.
60
+
57
61
  ### `doctor`
58
62
 
59
63
  `bunx roundtable doctor` checks, in order, and prints each check as passed or failed with how to fix it:
@@ -65,10 +69,10 @@ Bun loads `.env` by itself, and `.gitignore` keeps it out of Git.
65
69
  5. Whether a plugin fills the `images` slot.
66
70
  The check passes with or without one; agents without an image provider get avatars generated from their display names.
67
71
  6. PostgreSQL is reachable and migratable.
68
- 7. The Discord token is valid, the bot is in your server, the Message Content intent is on, and the bot has the permissions it needs in the entry channel (including Pin Messages).
72
+ 7. With Discord configured: the Discord token is valid, the bot is in your server, the Message Content intent is on, and the bot has the permissions it needs in the entry channel (including Pin Messages).
69
73
  When the bot is not in the server, the fix is an invitation link that asks for exactly those permissions.
70
74
  8. The model login exists.
71
- 9. `PUBLIC_URL` is a well-formed address; with `--reachable` it also has to answer, which is only true while the bot runs.
75
+ 9. With Discord configured: `PUBLIC_URL` is a well-formed address; with `--reachable` it also has to answer, which is only true while the bot runs.
72
76
 
73
77
  It exits non-zero on any failure and changes nothing it checked.
74
78
  A fresh project fails only on the credentials you have not entered yet, and says which.
@@ -126,6 +130,18 @@ Its examples live in [`examples/`](examples), and the test suite runs each of th
126
130
  It is the entry that names discord.js types (`pi-roundtable/testing` names a few, through `testHost`'s composed commands).
127
131
  Both follow the main entry's versioning: before 1.0, breaking changes come in minor releases and are listed in the changelog.
128
132
 
133
+ ## Web chat
134
+
135
+ The package [pi-roundtable-webchat](packages/webchat) is a chat network that comes as a plugin: a WebSocket and a REST API under `/chat` on the host's listener.
136
+ People your OpenID Connect provider signs in open private conversations with the personas you list, see each turn's text and tools as it runs, and answer approval cards for held calls.
137
+ A host whose configuration has no `discord` and lists `webChat({ ... })` in `plugins` is a web-only assistant; `roundtable init --adapter web` creates one.
138
+
139
+ ```sh
140
+ npx pi-roundtable init my-desk --adapter web
141
+ ```
142
+
143
+ Its README describes the protocol, the access map, the limits, and the [provider settings](packages/webchat/README.md#provider-settings) that keep one person one identity, such as a stable subject claim and a pinned tenant.
144
+
129
145
  ## MCP connectors
130
146
 
131
147
  The separate package [pi-roundtable-mcp](https://www.npmjs.com/package/pi-roundtable-mcp) connects the bot to the MCP ecosystem in both directions, with two plugins:
@@ -140,7 +156,7 @@ The separate package [pi-roundtable-mcp](https://www.npmjs.com/package/pi-roundt
140
156
  bun add pi-roundtable-mcp
141
157
  ```
142
158
 
143
- It works with pi-roundtable 0.4 and 0.5, and its README lists every option.
159
+ It releases in lockstep with pi-roundtable, and its README lists every option.
144
160
 
145
161
  ## Settings
146
162
 
@@ -158,11 +174,28 @@ export default {
158
174
  } satisfies RoundtableConfig;
159
175
  ```
160
176
 
177
+ ## Threat model
178
+
179
+ pi-roundtable runs one assistant for one owner.
180
+ It does not yet isolate the people it talks to from each other the way a multi-user service must; whoever lets others in takes that on.
181
+
182
+ - **Who is trusted.** The operator controls the host, its configuration, and its credentials. The owner (`owner.id`) may use every tool. Everyone else is admitted at a tier, `member` or `admin`: in Discord from user and role ids in `speakers`, on the web from the access map over the token's roles. No token claim makes anyone the owner.
183
+ - **What a turn may do.** A turn gets only the tools its speaker's tier holds, and a tool no plugin gives a tier is the owner's alone. Tools run in the host process, with its files and its network: `web_search` and `fetch_content` can reach internal addresses, so name the tools of a web persona in its `selection`. Plugins run in the same process, with the same access.
184
+ - **Approvals.** A held call is approved only by the speaker whose turn held it, at a tier that still holds the call, or by the owner, on its card or by a confirming message.
185
+ - **Conversations.** In Discord an agent's channel and a group room are shared: everyone who writes there adds to one conversation the agent reads. A web chat conversation belongs to the person who opened it: in the web chat only they may read it, write in it, or answer its prompts, but the owner can read it through the owner console ([pi-roundtable-web](packages/web)), and the operator through the database and the data directory.
186
+ - **Memory.** Each speaker's memory is their own. One person who writes from Discord and from the web has two identities, and two memories, until principals arrive in 0.9.
187
+ - **Model credentials.** Every turn, whoever speaks, runs on the host's model login and is billed to it. Per-person credentials are planned for a later release.
188
+ - **Data.** Conversations, memory, and schedules sit unencrypted in PostgreSQL and the data directory.
189
+
190
+ ## Upgrading
191
+
192
+ [Migrating to 0.8](docs/migrating-0.8.md) covers upgrading a Discord project from 0.7, which needs no configuration change, and starting a host without Discord.
193
+
161
194
  ## Commands
162
195
 
163
196
  | Command | What it does |
164
197
  | --- | --- |
165
- | `roundtable init [dir]` | Creates a project in `dir` (default: the current directory) |
198
+ | `roundtable init [dir] [--adapter discord\|web]` | Creates a project in `dir` (default: the current directory) that talks through Discord (the default) or through pi-roundtable-webchat |
166
199
  | `roundtable doctor [--reachable]` | Checks the setup and says how to fix what is wrong |
167
200
  | `roundtable start` | Runs the checks that need no network, then the bot |
168
201
  | `roundtable add plugin <name>` | Adds `plugins/<name>.ts` and its test, and lists it in the config |
package/README.zh-TW.md CHANGED
@@ -5,10 +5,12 @@
5
5
  文件:<https://pi-roundtable.wayneh.tw>
6
6
  原始碼與 issue:<https://github.com/wayne930242/pi-roundtable>
7
7
 
8
- 一個建立在 [Pi](https://github.com/earendil-works/pi) 上的 Discord 智慧體(agent)伺服器。
9
- 它在你的 Discord 伺服器裡放一組 AI 智慧體:每個有自己的頻道和對話,共用工具與記憶。想加功能,就用 TypeScript 寫外掛(plugin)。
8
+ 一個建立在 [Pi](https://github.com/earendil-works/pi) 上的智慧體(agent)伺服器,可以透過 Discord 或網頁聊天跟它對話。
9
+ 在 Discord 裡,每個 AI 智慧體在你的伺服器有自己的頻道和對話,共用工具與記憶。
10
+ 在網頁上,經你的 OpenID Connect 供應商登入的人,透過 [pi-roundtable-webchat](packages/webchat) 在各自的私人對話裡跟助理聊天。
11
+ 想加功能,就用 TypeScript 寫外掛(plugin)。
10
12
 
11
- - 為一位擁有者和一個 Discord 伺服器設計。其他人也能跟智慧體對話,風險由營運者自行承擔。
13
+ - 為一位擁有者設計。Discord 和網頁聊天都能用,也可以讓其他人跟助理對話,但開放給多人使用的風險由營運者自行承擔:請先讀[威脅模型](#威脅模型)。
12
14
  - 只支援 Bun。套件直接發佈 TypeScript 原始碼,不需要建置步驟。
13
15
  - MIT 授權。
14
16
 
@@ -16,9 +18,9 @@
16
18
 
17
19
  - [Bun](https://bun.sh/docs/installation) 1.3 以上。
18
20
  - PostgreSQL。`init` 建立的專案附有 `docker-compose.yml`,可以直接啟動一個。
19
- - 一個 Discord bot:有 bot 使用者、開了 Message Content intent,也已經邀請進你的伺服器。
20
21
  - 模型登入:你選的模型的供應商 API key(`anthropic/...` 用 `ANTHROPIC_API_KEY`),或之前用 Pi 登入過的帳號。
21
- - 一個從網際網路連得到這個行程的位址,例如通道(tunnel)。Discord 要從這個位址抓智慧體的頭像。
22
+ - 用 Discord 的話:一個 Discord bot(有 bot 使用者、開了 Message Content intent,也已經邀請進你的伺服器),以及一個從網際網路連得到這個行程的位址,例如通道(tunnel)。Discord 要從這個位址抓智慧體的頭像。
23
+ - 用網頁聊天的話:一個會為聊天 API 簽發 access token 的 OpenID Connect 供應商,以及一個用 HTTPS 對外提供服務的反向代理。
22
24
 
23
25
  ## 五分鐘上手
24
26
 
@@ -34,6 +36,8 @@ bunx roundtable start
34
36
 
35
37
  `init` 會寫出一個能跑的專案,過程不會問你任何祕密資訊。
36
38
  Bun 沒裝、版本太舊,或要建立的檔案已經存在時,它什麼都不寫。
39
+ `init --adapter web` 則寫出一個沒有 Discord、以網頁聊天為中心的專案,見[網頁聊天](#網頁聊天)。
40
+ 兩種專案都會載入 Pi package pi-self-compact,每個 session 都需要它提供的 `compact_session` 工具。
37
41
 
38
42
  ### `.env`
39
43
 
@@ -49,6 +53,8 @@ Bun 會自己讀 `.env`,`.gitignore` 也已經擋掉它。
49
53
  | `MODEL` | 智慧體使用的模型,格式為 `<provider>/<id>` |
50
54
  | `PUBLIC_URL` | 能從網際網路連到這個行程的位址 |
51
55
 
56
+ 網頁聊天專案要填的是 `OWNER_NAME`、`DATABASE_URL`、`MODEL`,以及 OpenID Connect 的 issuer、audience、簽章金鑰位址、可以聊天的角色,和會開啟聊天的網頁來源(origin)。
57
+
52
58
  ### `doctor`
53
59
 
54
60
  `bunx roundtable doctor` 依序檢查下列項目,逐項印出通過或失敗,並說明如何修正:
@@ -59,10 +65,10 @@ Bun 會自己讀 `.env`,`.gitignore` 也已經擋掉它。
59
65
  4. 每個外掛都能載入,且沒有兩個外掛同名。
60
66
  5. 有沒有外掛填了 `images` 槽位。沒有也不算失敗,智慧體會用顯示名稱產生頭像。
61
67
  6. PostgreSQL 連得上,且能執行 migration。
62
- 7. Discord token 有效、bot 已在你的伺服器裡、Message Content intent 已開啟,而且 bot 在入口頻道有它需要的權限(包含 Pin Messages)。
68
+ 7. 有設定 Discord 時:Discord token 有效、bot 已在你的伺服器裡、Message Content intent 已開啟,而且 bot 在入口頻道有它需要的權限(包含 Pin Messages)。
63
69
  bot 還不在伺服器裡的話,它會給你一個邀請連結,連結已經帶好這些權限。
64
70
  8. 模型登入存在。
65
- 9. `PUBLIC_URL` 的格式正確;加上 `--reachable` 還要求它有回應,所以 bot 得先跑起來。
71
+ 9. 有設定 Discord 時:`PUBLIC_URL` 的格式正確;加上 `--reachable` 還要求它有回應,所以 bot 得先跑起來。
66
72
 
67
73
  只要有一項失敗,就以非零狀態結束,檢查過程不會改動任何東西。
68
74
  全新的專案只會卡在還沒填的憑證,它會列出是哪幾個。
@@ -119,6 +125,18 @@ test("hello greets", async () => {
119
125
  `pi-roundtable/discord` 提供斜線指令註冊器、擁有者指令與面板的輔助函式,以及智慧體面板;它是會用到 discord.js 型別的入口(`pi-roundtable/testing` 也透過 `testHost` 組合出的指令用到少數幾個)。
120
126
  這兩個入口的版本規則與主入口相同:1.0 之前,不相容的變更會放在次版本(minor)發佈,並列在變更記錄中。
121
127
 
128
+ ## 網頁聊天
129
+
130
+ 套件 [pi-roundtable-webchat](packages/webchat) 是以外掛形式提供的聊天管道:在主機的 listener 上的 `/chat` 底下提供 WebSocket 和 REST API。
131
+ 經你的 OpenID Connect 供應商登入的人,可以用你列出的人格(persona)開私人對話,在每一輪執行時看到它寫的文字和用的工具,也能回答暫緩呼叫的核准卡。
132
+ 設定裡沒有 `discord`、`plugins` 列了 `webChat({ ... })` 的主機,就是只有網頁的助理;`roundtable init --adapter web` 會建立這種專案。
133
+
134
+ ```sh
135
+ npx pi-roundtable init my-desk --adapter web
136
+ ```
137
+
138
+ 它的 README(英文)說明協定、存取對照、各項上限,以及讓同一個人只有一個身分的[供應商設定](packages/webchat/README.md#provider-settings),例如使用穩定的 subject claim、固定租戶。
139
+
122
140
  ## MCP connector
123
141
 
124
142
  獨立套件 [pi-roundtable-mcp](https://www.npmjs.com/package/pi-roundtable-mcp) 用兩個外掛,把 bot 雙向接上 MCP:
@@ -133,7 +151,7 @@ test("hello greets", async () => {
133
151
  bun add pi-roundtable-mcp
134
152
  ```
135
153
 
136
- 它支援 pi-roundtable 0.4 和 0.5,每個選項都列在它的 README。
154
+ 它與 pi-roundtable 同步發版,每個選項都列在它的 README。
137
155
 
138
156
  ## 設定
139
157
 
@@ -151,11 +169,28 @@ export default {
151
169
  } satisfies RoundtableConfig;
152
170
  ```
153
171
 
172
+ ## 威脅模型
173
+
174
+ pi-roundtable 為一位擁有者執行一個助理。
175
+ 它還沒有像多使用者服務那樣把對話的人彼此隔開;開放給別人使用的人要自己承擔這部分。
176
+
177
+ - **信任誰。** 營運者掌控主機、設定和憑證。擁有者(`owner.id`)可以使用每一個工具。其他人以某個 tier(`member` 或 `admin`)進來:Discord 依 `speakers` 裡的使用者與角色 id,網頁依存取對照表和 token 的角色。沒有任何 token claim 能讓人成為擁有者。
178
+ - **一輪能做什麼。** 一輪只拿得到發言者的 tier 允許的工具;沒有外掛給 tier 的工具只有擁有者能用。工具在主機的行程裡執行,用的是主機的檔案和網路:`web_search` 和 `fetch_content` 連得到內部位址,所以網頁人格要在 `selection` 裡寫明它的工具。外掛也在同一個行程裡執行,權限一樣。
179
+ - **核准。** 暫緩的呼叫只能由讓它暫緩的那一輪的發言者核准,而且發言者的 tier 仍要足以執行這個呼叫;擁有者也可以核准。核准卡和確認訊息都適用這條規則。
180
+ - **對話。** Discord 裡智慧體的頻道和群組房間是共用的:在那裡發言的每個人,都會寫進智慧體讀取的同一段對話。網頁聊天的對話屬於開啟它的人:在網頁聊天裡只有開啟者本人能讀、能發言、能回答它的提問,但擁有者可以透過擁有者主控台([pi-roundtable-web](packages/web))讀取,營運者也能從資料庫和資料目錄讀到。
181
+ - **記憶。** 每位發言者的記憶各自分開。同一個人從 Discord 和網頁發言會有兩個身分、兩份記憶,要等 0.9 加入 principal 才會合併。
182
+ - **模型憑證。** 每一輪不論誰發言,都用主機的模型登入執行、由它計費。每個人自己的憑證預計在之後的版本加入。
183
+ - **資料。** 對話、記憶和排程以未加密的形式存在 PostgreSQL 和資料目錄裡。
184
+
185
+ ## 升級
186
+
187
+ [升級到 0.8](docs/migrating-0.8.md)(英文)說明怎麼把 0.7 的 Discord 專案升級(不需要改設定),以及怎麼啟動沒有 Discord 的主機。
188
+
154
189
  ## 指令
155
190
 
156
191
  | 指令 | 作用 |
157
192
  | --- | --- |
158
- | `roundtable init [dir]` | 在 `dir`(預設為目前目錄)建立專案 |
193
+ | `roundtable init [dir] [--adapter discord\|web]` | 在 `dir`(預設為目前目錄)建立專案,透過 Discord(預設)或 pi-roundtable-webchat 對話 |
159
194
  | `roundtable doctor [--reachable]` | 檢查設定,並說明如何修正有問題的地方 |
160
195
  | `roundtable start` | 先執行不需要網路的檢查,再啟動 bot |
161
196
  | `roundtable add plugin <name>` | 新增 `plugins/<name>.ts` 和它的測試,並列進設定 |
@@ -0,0 +1,132 @@
1
+ # Migrating to pi-roundtable 0.8
2
+
3
+ 0.8 makes Discord optional and adds a web chat.
4
+ A Discord project from 0.7 upgrades without changing its configuration.
5
+ A new project can leave Discord out and talk through pi-roundtable-webchat, or through a chat surface of its own.
6
+ The [changelog](../CHANGELOG.md) lists every change; this guide covers what to do about them.
7
+
8
+ ## Upgrading a Discord project from 0.7
9
+
10
+ 1. Upgrade pi-roundtable and every official package you use to 0.8 together.
11
+ The packages release in lockstep, and each one's peer range on pi-roundtable is the same minor.
12
+
13
+ ```sh
14
+ bun add pi-roundtable@0.8 pi-roundtable-mcp@0.8 # and any other pi-roundtable-* package you use
15
+ ```
16
+
17
+ 2. Make sure a plugin loads pi-self-compact.
18
+ Every session needs its `compact_session` tool, and a project that loads no package that registers it stops at startup with `required tools are not registered: compact_session`.
19
+ A running 0.7 project already loads it, since 0.7 had the same requirement; a project `roundtable init` created before 0.8 did not, and could not start.
20
+ The fix is the plugin `roundtable init` now writes, `plugins/self-compact.ts`:
21
+
22
+ ```sh
23
+ bun add pi-self-compact@0.2.0
24
+ ```
25
+
26
+ ```ts
27
+ import { definePlugin } from "pi-roundtable";
28
+
29
+ export const selfCompact = definePlugin({
30
+ name: "self-compact",
31
+ setup: () => ({ piPackages: ["pi-self-compact"] }),
32
+ });
33
+ ```
34
+
35
+ 3. Start the host.
36
+ Its migrations only add:
37
+ - the table `conversations` (ledger id `conversations/conversations`), the conversation registry;
38
+ - `runtime/held-actions`, the held actions' migration recorded under its new plugin; the table exists already, so nothing changes;
39
+ - `runtime/held-actions-speaker`, a nullable column `speaker_id` on `held_actions`;
40
+ - `runtime/held-actions-speaker-hold`, a nullable column `speaker_held_at` on `held_actions`, the hold the speaker belongs to.
41
+
42
+ Nothing else is required.
43
+ `bunx roundtable doctor` runs the same checks as before.
44
+
45
+ ### What behaves differently
46
+
47
+ - **Approvals.**
48
+ A held call is approved only by the speaker whose turn held it, at a tier that still holds the call, or by the owner.
49
+ That holds for the approval card and for a confirming message in an agent's channel or a group room.
50
+ In 0.7 any speaker whose tier held the tools could approve another person's call by message.
51
+ Calls held before the upgrade carry no speaker, so they are the owner's to approve.
52
+ A tool whose calls someone above the speaker should approve needs a higher `minTier`, or a hold rule's `approvalTier`.
53
+ - **Plugin order.**
54
+ Two built-in plugins join the list, just before `agent-server`: `conversations`, which provides `CONVERSATIONS`, and `runtime`, which builds the runtime every turn runs on and provides `RUNTIME`.
55
+ A plugin that matches built-in plugins by name, or reads `serviceStarted` events, sees the two new names; the order of everything else is unchanged.
56
+
57
+ ### What changes for plugin code
58
+
59
+ - **`RUNTIME`.**
60
+ `context.turns` now runs on `services.get(RUNTIME)`, not on the agent server, so it works on a host without one.
61
+ `AGENTS.runtime` is the same instance, and a plugin that fills the `runtime` provider slot keeps replacing it.
62
+ Read `RUNTIME` in new code.
63
+ - **Optional configuration.**
64
+ `RoundtableConfig.discord`, `RoundtableConfig.http`, and `http.publicUrl` are optional in the type, and `RuntimeDeps.agents` is optional and read when a turn runs.
65
+ Code that reads them from a configuration or from `RuntimeDeps` checks for them first.
66
+ - **Held actions.**
67
+ `PendingConfirmation.speakerId` records who held the calls.
68
+ A runtime with its own `HeldActionStore` keeps `speakerId` across a restart; without it, a restored call is the owner's to approve.
69
+ - **HTTP.**
70
+ `HttpRoute.websocket` (a `WebSocketRoute`) takes WebSocket upgrades on a route's path.
71
+ `pi-roundtable/kit`'s `serveUnix` passes the server to `fetch` as a second argument, and takes an optional `websocket` handler; with one, `fetch` may answer nothing after it upgrades the request.
72
+ - **Testing.**
73
+ `testHost({ discord: false })` boots a host without Discord, and `describeSurfaceContract` checks a chat surface of your own.
74
+
75
+ ### Optional: Discord as an adapter
76
+
77
+ The top-level `discord` stays the form `roundtable init` writes.
78
+ The same settings may come as an adapter instead, which is the form later releases build on:
79
+
80
+ ```ts
81
+ import { discord } from "pi-roundtable/discord";
82
+
83
+ export default {
84
+ // ...
85
+ adapters: [
86
+ discord({
87
+ token: env("DISCORD_TOKEN"),
88
+ guild: env("DISCORD_GUILD_ID"),
89
+ entryChannel: env("DISCORD_ENTRY_CHANNEL_ID"),
90
+ }),
91
+ ],
92
+ } satisfies RoundtableConfig;
93
+ ```
94
+
95
+ Configure Discord in one place: both is a configuration error.
96
+
97
+ ## Starting a host without Discord
98
+
99
+ ### A web chat project
100
+
101
+ ```sh
102
+ npx pi-roundtable init my-desk --adapter web
103
+ cd my-desk
104
+ bun install
105
+ docker compose up -d
106
+ cp .env.example .env # then fill it in
107
+ bunx roundtable doctor
108
+ bunx roundtable start
109
+ ```
110
+
111
+ The project lists `webChat({ ... })` from pi-roundtable-webchat in `plugins`, with an OpenID Connect verifier, an access map from the token's roles, and one persona whose `selection` names its tools.
112
+ It listens on `127.0.0.1:3000`; serve it over HTTPS through a reverse proxy, and set `CHAT_ORIGINS` to the pages that open the chat.
113
+ Before anyone signs in, read the web chat README's [provider settings](../packages/webchat/README.md#provider-settings): name people by a claim that stays the same across your app registrations, pin your tenant, and accept access tokens only.
114
+
115
+ ### Leaving Discord out of an existing configuration
116
+
117
+ Remove `discord` and the host runs without it: no Discord plugin, no agent server, no agents, and no skills.
118
+ Every claim that runs turns through `context.turns` still works, over the chat surfaces your plugins bring.
119
+
120
+ - `agents`, `skills` (anything but `false`), and `ops.agent` are configuration errors. `ops: { conversation: "<surface>:<id>" }` needs a chat surface that serves it, a claim that owns it and takes background turns, and a plugin that contributes the `owner` background target, or the host does not start; without Discord nothing contributes `owner` unless a plugin of yours does, so leave `ops` out unless you have one. The web chat cannot take error reports in 0.8, so `web:<id>` fails at startup.
121
+ - `http` is optional; without it no listener opens.
122
+ - `notify_owner` is not registered.
123
+ - `schedule_*` and `delegate_task` are not registered, since their runs are turns of the `owner` background target, which the agent server contributes; a plugin that contributes it brings them back. The web chat takes no background turns yet, so leave them out of a web persona's `selection` on a host that keeps Discord.
124
+ - `roundtable doctor` skips the Discord checks.
125
+
126
+ See [a host without Discord](plugins.md#a-host-without-discord) and [the web chat](plugins.md#web-chat-pi-roundtable-webchat) in the plugin guide, and the README's [threat model](../README.md#threat-model) before you let several people in.
127
+
128
+ ## Rolling back to 0.7
129
+
130
+ Every 0.8 migration adds a table or a nullable column, so 0.7.19 starts on a database 0.8 has used.
131
+ It ignores the `conversations` table and the `speaker_id` and `speaker_held_at` columns, and approvals follow 0.7's rules again.
132
+ Upgrading once more is safe too: 0.7 replaces a channel's held actions without touching `speaker_id`, so 0.8 counts a speaker only while the row's `held_at` still matches the `speaker_held_at` it wrote, and held actions 0.7 wrote are the owner's to approve.