pi-roundtable 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +35 -11
- package/README.zh-TW.md +27 -10
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +606 -78
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.ts +5 -1
- package/examples/prechecks.test.ts +1 -0
- package/examples/principals.test.ts +60 -0
- package/examples/principals.ts +48 -0
- package/examples/prompt.test.ts +1 -1
- package/examples/study-room.test.ts +29 -1
- package/examples/study-room.ts +16 -19
- package/examples/support-desk.test.ts +3 -1
- package/examples/tools.test.ts +6 -1
- package/package.json +5 -2
- package/src/cli/checks/database.ts +11 -0
- package/src/cli/checks/identity.ts +225 -0
- package/src/cli/checks/model.ts +21 -3
- package/src/cli/cli.ts +115 -1
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +7 -0
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +5 -0
- package/src/cli/report.ts +12 -3
- package/src/cli/upgrade-rewrite.ts +102 -0
- package/src/cli/upgrade-source.ts +430 -0
- package/src/cli/upgrade.ts +176 -0
- package/src/core/agents/agent-claim.ts +56 -34
- package/src/core/agents/team-turns.ts +8 -4
- package/src/core/builtin/agent-server.ts +9 -7
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +140 -85
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +64 -1
- package/src/core/config/config.ts +78 -41
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +77 -9
- package/src/core/contract/runtime.ts +23 -7
- package/src/core/contract/surface.ts +22 -6
- package/src/core/conversations/conversation-registry.ts +14 -0
- package/src/core/conversations/conversation-store.ts +13 -0
- package/src/core/define-roundtable.ts +41 -11
- package/src/core/discord/agent-panel.ts +2 -2
- package/src/core/discord/card-audience.ts +148 -0
- package/src/core/discord/channel-context.ts +348 -0
- package/src/core/discord/connection.ts +15 -3
- package/src/core/discord/direct-channel.ts +54 -0
- package/src/core/discord/discord-owners.ts +133 -0
- package/src/core/discord/discord-surface.ts +69 -14
- package/src/core/discord/fresh-marker.ts +24 -0
- package/src/core/discord/inbound-message.ts +21 -7
- package/src/core/discord/interaction-module.ts +11 -1
- package/src/core/discord/owner-cards-fixture.ts +80 -0
- package/src/core/discord/owner-cards.ts +58 -97
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +7 -2
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +27 -2
- package/src/core/host.ts +27 -8
- package/src/core/i18n/cards.ts +8 -4
- package/src/core/identity/access-policy.ts +227 -0
- package/src/core/identity/actor-facts.ts +65 -0
- package/src/core/identity/caching-principal-store.ts +111 -0
- package/src/core/identity/config-owner.ts +145 -0
- package/src/core/identity/contact.ts +27 -0
- package/src/core/identity/identity-plugin.ts +56 -0
- package/src/core/identity/identity-schema.ts +282 -0
- package/src/core/identity/identity-service.ts +489 -0
- package/src/core/identity/identity-view.ts +77 -0
- package/src/core/identity/plugin-identities.ts +109 -0
- package/src/core/identity/principal-reads.ts +38 -0
- package/src/core/identity/principal-store.ts +478 -0
- package/src/core/identity/principal-tables.ts +53 -0
- package/src/core/identity/seen-throttle.ts +57 -0
- package/src/core/identity/ulid.ts +36 -0
- package/src/core/identity.ts +34 -5
- package/src/core/interactions/prompts.ts +127 -0
- package/src/core/modules/background/background-turns.ts +56 -17
- package/src/core/modules/background/personal-target.ts +50 -0
- package/src/core/modules/delegation/delegate.ts +28 -14
- package/src/core/modules/delegation/delegator.ts +23 -7
- package/src/core/modules/memory/owner-memory.ts +123 -50
- package/src/core/modules/notify/notify.ts +53 -13
- package/src/core/modules/notify/session-notify.ts +110 -0
- package/src/core/modules/schedules/prechecks.ts +8 -2
- package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
- package/src/core/modules/schedules/schedule-store.ts +51 -20
- package/src/core/modules/schedules/schedule-tools.ts +122 -12
- package/src/core/modules/schedules/scheduler.ts +43 -2
- package/src/core/modules/schedules/schedules.ts +43 -14
- package/src/core/plugin.ts +48 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/routing/channel-router.ts +218 -17
- package/src/core/routing/conversation-turns.ts +38 -8
- package/src/core/routing/surface-port.ts +32 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/bridge-guard.ts +225 -0
- package/src/core/runtime/compaction-tiers.ts +5 -1
- package/src/core/runtime/conversation-sessions.ts +124 -12
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +28 -14
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/pending-confirmation-store.ts +30 -5
- package/src/core/runtime/pi-agent-runtime.ts +135 -134
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +4 -4
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +50 -5
- package/src/core/runtime/runtime-types.ts +40 -12
- package/src/core/runtime/session-conversation.ts +143 -0
- package/src/core/runtime/session-factory.ts +115 -15
- package/src/core/runtime/session-scope.ts +78 -0
- package/src/core/runtime/steerable-run.ts +9 -0
- package/src/core/runtime/tool-call-scope.ts +43 -0
- package/src/core/runtime/turn-answer.ts +30 -1
- package/src/core/services.ts +27 -1
- package/src/core/sessions.ts +30 -0
- package/src/core/speakers.ts +21 -19
- package/src/core/testing/background.ts +31 -0
- package/src/core/testing/fixture-database.ts +55 -0
- package/src/core/testing/map-identity.ts +76 -0
- package/src/core/testing/module-sessions.ts +134 -0
- package/src/core/testing/modules.ts +108 -21
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +118 -41
- package/src/core/testing/test-host.ts +79 -16
- package/src/core/testing/tool-set.ts +32 -0
- package/src/core/tool-set.snapshot.json +4 -4
- package/src/core/tool-tiers.ts +57 -3
- package/src/discord/index.ts +8 -0
- package/src/index.ts +47 -8
- package/src/kit/compaction.ts +13 -0
- package/src/kit/index.ts +10 -0
- package/src/kit/support.ts +1 -1
- package/src/testing.ts +55 -16
- package/templates/.env.example +1 -1
- package/templates/adapters/web/README.md +7 -0
- package/templates/adapters/web/roundtable.config.ts +14 -10
- package/templates/roundtable.config.ts +12 -1
- package/src/core/domain/owner-prompts.ts +0 -45
package/docs/plugins.md
CHANGED
|
@@ -67,7 +67,7 @@ Use the kit's building blocks for a plugin that runs Pi itself, such as a coding
|
|
|
67
67
|
- Tools: `textToolsExtension`, `requiredString`, `stringList` (with `toolText` and `toolError`) for tools that return text.
|
|
68
68
|
- 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`.
|
|
69
69
|
The specs take the locale and time zone for their descriptions, so the worker needs no process-wide setting.
|
|
70
|
-
- Compaction: `CompactionTiers` gives a session the core's compaction tiers (`settings()` for Pi's `SettingsManager`, `wrapCompactor(factory, onBypass)` to hold a compaction extension back past the ceiling, `latest()`), with `SOFT_COMPACT_TOKENS` (300,000), `HARD_COMPACT_TOKENS` (500,000), `COMPACT_HEADROOM_TOKENS` (50,000), `compactionEngine(details, engine)` and the types `CompactionEngine`, `CompactionHistory` and `LatestCompaction`.
|
|
70
|
+
- Compaction: `CompactionTiers` gives a session the core's compaction tiers (`settings()` for Pi's `SettingsManager`, `wrapCompactor(factory, onBypass, prepare?)` to hold a compaction extension back past the ceiling and change what it is given first, `latest()`), with `SOFT_COMPACT_TOKENS` (300,000), `HARD_COMPACT_TOKENS` (500,000), `COMPACT_HEADROOM_TOKENS` (50,000), `compactionEngine(details, engine)` and the types `CompactionEngine`, `CompactionHistory` and `LatestCompaction`. `summaryProjection(messages, history?)` is what a shared conversation's compaction may summarize, without anyone's private memory, prompt states, or reasoning, and `privateCompaction(preparation, additionalMessages?)` sets a `session_before_compact` preparation to it. Optional surrounding history pairs calls/results split across summary, prefix and kept context. Independent shared sessions use `memoryProjection(messages, view)` on requests and `hidesPrivateExchange(messages, view)` for raw replay guards (`MemoryView`: `shared` and `reader`; legacy attribution is internal to the bridge guard, not a projection option). `recordMemoryTurn(manager, reader, privateMemory)` appends `MEMORY_TURN_ENTRY` (`roundtable-memory-turn`) before prompting and returns a marker for memory first used later in that turn; `carriesMemory(message)` recognizes built-in memory calls/results and custom `privateTo` results, including immediate failures and unanswered calls. `bridgeHistoryHidesMemory(rawBranch, reader)` checks those records and raw exchanges across compaction; SYSTEM is a reader of its own there, and the core's SYSTEM-as-primary-owner attribution and its pre-scope legacy attribution stay inside the core.
|
|
71
71
|
- Jev compaction: `jevCompact(input, options)` compacts through Jev (pi-jev-compaction 1.0.0) and returns `{ compaction }` or `{ skipped, detail? }` (a `JevSkipReason`: pi-jev-compaction's fallbacks `no_key`, `aborted`, `nothing_to_compact`, `no_candidates`, `cannot_fit`, `jev_error`, `reduction_too_small`, or `previous_summary_too_large`), for Pi's own summary to run instead.
|
|
72
72
|
Its summary carries the previous summary once, as the transcript's `[previous compaction]` message, with `estimatedTokensAfter` measured on the final text; a previous summary over `previousSummaryLimitTokens` (default `JEV_PREVIOUS_SUMMARY_LIMIT_TOKENS`, 60,000) skips without calling Jev, so Pi's summary condenses the chain.
|
|
73
73
|
Jev judges with `goal` (default `JEV_GOAL`: keep the tool results that set rules still in force, drop stale lookups, listings and finished edits), and the summary ends with a `## Rules loaded before this compaction` section naming each rule load among the summarized messages (tool name and JSON arguments cut at 200 characters) for the agent to load again; `ruleLoad(tool, args)` picks them (default `isRuleLoad`: tools ending in `invoke-skill` or `get-system-prompt`, and tools ending in `read` whose `path` is a `SKILL.md`, an `AGENTS.md` or under `.agents/skills/`).
|
|
@@ -97,6 +97,7 @@ definePlugin({
|
|
|
97
97
|
providers: {}, // optional: replaces a part the core runs on
|
|
98
98
|
provides: [], // optional: keys of the services setup provides to other plugins
|
|
99
99
|
replaces: [], // optional: keys of built-in services this plugin takes over
|
|
100
|
+
identities: [], // optional: identities the plugin's own credentials stand for
|
|
100
101
|
preflight() {}, // optional: a check that runs before anything starts
|
|
101
102
|
setup(context) { // required: returns the parts the plugin adds
|
|
102
103
|
return { /* parts */ };
|
|
@@ -109,6 +110,16 @@ Errors point to the plugin definition.
|
|
|
109
110
|
|
|
110
111
|
A plugin whose `setup` returns `{}` and has no migrations, providers, or hooks stops startup (see [Errors](#errors-and-their-fixes)).
|
|
111
112
|
|
|
113
|
+
`identities` is for a plugin that serves a credential of its own, such as the bearer token of an endpoint, and runs turns for whoever holds it.
|
|
114
|
+
Each entry is a `PluginIdentity`, `{ identity, principal? }`: the identity written `token:<subject>`, such as `token:remote-mcp`, and the id of the principal it stands for, by default the primary owner (the first of `access.owners`).
|
|
115
|
+
Only the `token` provider is accepted. A surface's identity, such as `discord:<user id>` or `oidc:…`, is a person's own account: declaring it would make whoever holds it the principal the plugin binds it to, owner included, and unlink it when the plugin stops declaring it. Link those in `access.owners` or with `roundtable principal link`.
|
|
116
|
+
At every start the `identity` plugin links each one to its principal as the plugin's (`roundtable principal list` shows `plugin <name>`), moves one whose principal changed since the last start, and unlinks one no plugin declares any more.
|
|
117
|
+
The start stops with a `ConfigError` for an identity that does not parse or is not a `token:` identity, one two plugins declare, the system principal, a principal that does not exist, and an identity linked to someone else by the configuration, the CLI, or a first contact; the message names what to change.
|
|
118
|
+
A declared identity is never admitted or claimed as someone new, and linking it leaves the principal's 0.8 claim alone: a plugin's credential is not the person's own identity.
|
|
119
|
+
Nor does its link tell who a configured owner is: an owner in `access.owners` that also lists a declared identity is known by its `principal` or its other identities, and the start stops when that is not the principal the plugin binds the identity to on that start (its `principal`, or else the primary owner), or when nothing else tells who the owner is. Whom the identity was bound to at an earlier start does not matter: once the plugin is bound to that owner in its options, the next start moves the identity to them.
|
|
120
|
+
Read whom it stands for with `IDENTITY.principalOf(identity)`, and start the turn with `IDENTITY.speakerFor(principalId)`.
|
|
121
|
+
A plugin that replaces `IDENTITY` replaces this too: the declarations are the built-in `identity` plugin's to link.
|
|
122
|
+
|
|
112
123
|
### The context
|
|
113
124
|
|
|
114
125
|
`setup` receives a `PluginContext`:
|
|
@@ -126,9 +137,10 @@ A plugin whose `setup` returns `{}` and has no migrations, providers, or hooks s
|
|
|
126
137
|
| `services` | The services plugins provide to each other, read by key: `services.get(SCHEDULES)`; see [services](#services-what-plugins-provide-to-each-other) |
|
|
127
138
|
| `surfaces` | Every contributed [chat surface](#surfaces-a-chat-network-of-your-own), chosen by the prefix of a channel key: `of`, `sendReply`, `startTyping`, `showStop`, `react`, `unreact`, `prompts` |
|
|
128
139
|
| `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) |
|
|
129
|
-
| `
|
|
140
|
+
| `directChannels` | Every plugin's [direct channels](#directchannels-reaching-a-person-on-their-own): `providers()`, `reach(principalId)`, `known(principalId)`, and `notify(principalId, text)` |
|
|
141
|
+
| `sessions()`, `conversations`, `surfaces`, `directChannels`, `turns`, `dashboard()` | Linked once every plugin has been set up; calling them during `setup` throws `NotLinkedError` |
|
|
130
142
|
|
|
131
|
-
`sessions()`, `conversations`, `surfaces`, `turns`, and `dashboard()` are available from a service's `start`, an event handler, or a claim's turn.
|
|
143
|
+
`sessions()`, `conversations`, `surfaces`, `directChannels`, `turns`, and `dashboard()` are available from a service's `start`, an event handler, a session tool, or a claim's turn.
|
|
132
144
|
|
|
133
145
|
Use `QueuePort` from the main entry for `context.queue`; the kit's `ChannelQueue` is also type-only.
|
|
134
146
|
|
|
@@ -140,9 +152,9 @@ Use `QueuePort` from the main entry for `context.queue`; the kit's `ChannelQueue
|
|
|
140
152
|
`Logger` has five levels and `child(fields)`; you can pass an existing pino logger as `DefineOverrides.logger`.
|
|
141
153
|
|
|
142
154
|
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, or, with `config.ops.conversation`, to that conversation as a visible message and a report turn its claim answers.
|
|
143
|
-
The report turn is
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
The report turn is the host's own background turn of the `owner` target: it runs as the system principal (`SYSTEM_PRINCIPAL`) at the owner tier, as in 0.8, and so does the report turn of an outside Discord webhook's post in an agent's channel, whose text is untrusted input; neither can set up a schedule or delegate a task. The host does not start, with a `ConfigError` naming `config 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 claim that owns it takes no background turns (has no `background`), or that claim says `takesSystemReports: false`, since its conversations are each private to their person and refuse the system principal's turns; a report is then never only logged. A claim like that is answered at startup, not by a warning at each report.
|
|
156
|
+
The core contributes `PERSONAL_TARGET` (`"owner"`) with or without Discord; `ops.conversation` still needs a surface and a claim that accepts system report turns.
|
|
157
|
+
Webchat accepts personal background turns but declares `takesSystemReports: false`, so `web:<id>` fails at startup; name a shared conversation of another surface, or an agent with Discord.
|
|
146
158
|
It then calls `DefineOverrides.errorSink(entry)` if you supply one; this function must not throw.
|
|
147
159
|
A logger you supply reaches the ops agent and `errorSink` only if it forwards its error lines there.
|
|
148
160
|
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.
|
|
@@ -175,13 +187,14 @@ The built-in plugins provide these, from the main entry:
|
|
|
175
187
|
|
|
176
188
|
| Key | Port | Provided by | What it is |
|
|
177
189
|
|---|---|---|---|
|
|
178
|
-
| `
|
|
190
|
+
| `IDENTITY` | `IdentityService` | `identity` | Who the host serves, read-only: `resolve(facts, { conversation }?)` gives the `Speaker` with its `principalId` behind a surface's `ActorFacts`, or undefined; `principal(id)`, `list()`, `identities(principalId)`, `roles(principalId)`, `tierOf(principalId)` (the lasting roles' tier), `speakerFor(principalId, tier?)` for a turn on someone's behalf, `owners()`, and `principalOf(identity)`, the principal an identity written `<provider>:<subject>` is linked to, such as one a plugin declares in `identities`. Principals are written only by the configuration and `roundtable principal`. `SYSTEM_PRINCIPAL` is the host's own, which no plugin speaks as |
|
|
191
|
+
| `CONVERSATIONS` | `ConversationRegistry` | `conversations` | The conversations run through `context.turns`: `register`, `get(key)`, `list({ principal }?)`, `setTitle(key, title)`, and `adopt(key, principalId)` only for migrating a conversation recorded before 0.9; each a `ConversationRecord` of `key`, `surface`, `kind`, `visibility` (`"private"` or `"shared"`), `principalId?`, `title?`, `createdAt`, `lastActiveAt` |
|
|
179
192
|
| `RUNTIME` | `AgentRuntime` | `runtime` | The runtime every conversation turn runs on, the agent server's and `context.turns`': the `runtime` slot's when a plugin fills it, Pi's otherwise |
|
|
180
193
|
| `AGENTS` | `AgentServer` | `agent-server` | The `team` (`AgentTeam`), the read-only `directory` (`AgentDirectory`), the `runtime` every agent turn runs on (the same one `RUNTIME` provides), `approvals` (whether the owner's reply approves held actions), and `avatars` (`AvatarStudio`) |
|
|
181
194
|
| `SKILLS` | `SkillRegistry` | `skills` (an addon) | What agents carry: `carried`, `carriedNames`, `describeCarried`, `catalog`, `list`, `linkedFrom`, `checkRegistered`, `link`, `attach` |
|
|
182
|
-
| `SCHEDULES` | `ScheduleStore` | `schedule-store` | The stored schedules: `create`, `get`, `forChannel`, `all`, `update`, `remove`, `due`, `claim`, `recordStatus` |
|
|
195
|
+
| `SCHEDULES` | `ScheduleStore` | `schedule-store` | The stored schedules: `create`, `createWithin`, `get`, `forChannel`, `all`, `update`, `remove`, `due`, `claim`, `recordStatus` |
|
|
183
196
|
| `PRECHECKS` | `PrecheckRegistry` | `prechecks` | The host's named [prechecks](#prechecks-wake-a-schedule-only-when-it-has-work): `register`, `get`, `list`; and the runner of agents' precheck scripts: `useScriptRunner`, `scriptRunner` |
|
|
184
|
-
| `MEMORY` | `MemoryStore` | `memory` (an addon) | `forSpeaker(
|
|
197
|
+
| `MEMORY` | `MemoryStore` | `memory` (an addon) | `forSpeaker(principalId)` gives that principal's `SpeakerMemory`: `list`, `forPrompt`, `add`, `search`, `update`, `removeById`, `remove`; `MEMORY_KINDS` is `core`, `note`, `event` |
|
|
185
198
|
| `BACKGROUND_TURNS` | `BackgroundTurns` | `modules` | Turns nobody wrote: `runScheduled`, `runDelegated`, `runErrorReport` |
|
|
186
199
|
| `DELEGATION` | `Delegator` | `modules` | `start(request)` a background task, `runningChannels()`, `idle()` |
|
|
187
200
|
|
|
@@ -327,9 +340,9 @@ You can switch them off in the configuration.
|
|
|
327
340
|
|
|
328
341
|
| Addon | Plugin | Switch in `roundtable.config.ts` | What it adds | When it is off |
|
|
329
342
|
|---|---|---|---|---|
|
|
330
|
-
| Memory | `memory` (provides `MEMORY`) | `memory: false` | The memory table, the `memory_add`, `memory_search` and `memory_remove` tools, and the memory block of every system prompt | No memory tools and no block; the table is left as it is |
|
|
343
|
+
| Memory | `memory` (provides `MEMORY`) | `memory: false` | The memory table, keyed by principal, the `memory_add`, `memory_search` and `memory_remove` tools, and the memory block of every system prompt but a `memory: "none"` persona's | No memory tools and no block; the table is left as it is |
|
|
331
344
|
| 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 |
|
|
332
|
-
| 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 |
|
|
345
|
+
| Discord administration | `discord-admin` | `discord: { admin: false }` | The `discord_*` tools that read and manage the server, for the owner; they check what the primary owner may do on the server, whichever owner's turn calls them | No `discord_*` tools; the channel executor that remote MCP uses is the connection's, so it stays |
|
|
333
346
|
|
|
334
347
|
Switching an addon off leaves its tables and rows unchanged, ready for when you turn it on again.
|
|
335
348
|
`skills` also takes the two directories (`skills: { builtinDir, reposDir }`) when it is on.
|
|
@@ -344,11 +357,166 @@ service roundtable.memory is not provided. The memory addon is switched off (con
|
|
|
344
357
|
`serviceKey(id, { absent })` lets your own keys say the same.
|
|
345
358
|
Switching an addon off and adding a plugin of yours that provides the same key is equivalent to `replaces`.
|
|
346
359
|
|
|
360
|
+
##### Memory in the agents' channels
|
|
361
|
+
|
|
362
|
+
By default every speaker an agent's channel admits reads their own memory there, as in any shared conversation.
|
|
363
|
+
`discord: { agentMemory: "owners" }` (or the same key in `discord({ ... })`; `AgentServerOptions.memory`, which the agent server hands the runtime as `AgentSessions.memory`) limits that to the owners: a turn of a speaker below the owner tier has no memory block in its prompt and no memory tools, and its worker tasks load none either, while an owner's turns in the same channel are unchanged.
|
|
364
|
+
A guest's turn then records no private memory, so it never makes a `claude-bridge` model refuse the owner afterwards (see the [bridge guard](migration-0.9.md#claude-bridge-guard)).
|
|
365
|
+
It does not make the guest's turn safe to run after the owner's: once the owner's turn has loaded memory, the guest's bridge turns are refused, as they are without the option, until the conversation starts fresh.
|
|
366
|
+
It applies to the agents' channels and groups only; other conversations keep deciding by their persona.
|
|
367
|
+
The default `"everyone"` is the 0.8 and 0.9 behavior.
|
|
368
|
+
|
|
369
|
+
## Principals and access
|
|
370
|
+
|
|
371
|
+
A principal is the person the host serves; an identity is an account or credential linked to that person.
|
|
372
|
+
`Speaker.id` identifies the external actor, while required `Speaker.principalId` keys memory, private conversations, schedule authors, notifications, and per-person limits.
|
|
373
|
+
Link Discord and web accounts with `roundtable principal link <principal> <identity>` to make them one person rather than two memories.
|
|
374
|
+
A 0.8 person's principal retains their old speaker id; new admitted people get opaque `p_<ulid>` ids.
|
|
375
|
+
|
|
376
|
+
Configure top-level `access.owners`, `admins`, and `members`.
|
|
377
|
+
Identities are `<provider>:<subject>` (OIDC uses `oidc:<base64url issuer>:<subject>`), and roles are `<surface>:role:<name>`.
|
|
378
|
+
`everyone: ["web"]` admits that surface only; `everyone: true` admits all surfaces and deserves particular care on a host with several adapters.
|
|
379
|
+
The primary owner is the first owner, with an explicit `principal` and, on Discord, a Discord identity; Discord administration still acts with that person's permissions.
|
|
380
|
+
Owner is granted only through configuration or the principal CLI, never an IdP role.
|
|
381
|
+
`provisioning: "admitted"` is the default; `"linked"` admits only already linked identities.
|
|
382
|
+
A disabled principal is refused, and CLI changes reach a running host within the identity cache's 30 seconds.
|
|
383
|
+
|
|
384
|
+
Surfaces report verified `ActorFacts`, not authority.
|
|
385
|
+
The router resolves them through `IDENTITY` before admission and overwrites a surface's own `message.speaker`.
|
|
386
|
+
A claim must ignore a message without a resolved speaker and run an admitted message as that speaker, rather than reconstructing one at an assumed tier.
|
|
387
|
+
Old author fields are a warned fallback until 1.0.
|
|
388
|
+
A plugin initiating a turn on someone's behalf uses `IDENTITY.speakerFor(principalId)` and passes the speaker explicitly; `runTurn` without one is refused.
|
|
389
|
+
A plugin's own credential bindings accept only `token:` (see [the plugin object](#the-plugin-object)), never a surface's account.
|
|
390
|
+
|
|
391
|
+
The example below reads only the current principal, so two linked actors ask about the same person and owner tier alone cannot invent a missing principal.
|
|
392
|
+
Put its `access` in the host configuration and its plugin in `plugins`.
|
|
393
|
+
|
|
394
|
+
<!-- example: examples/principals.ts -->
|
|
395
|
+
```ts
|
|
396
|
+
import {
|
|
397
|
+
type AccessConfig,
|
|
398
|
+
definePlugin,
|
|
399
|
+
defineTool,
|
|
400
|
+
IDENTITY,
|
|
401
|
+
ToolRefusal,
|
|
402
|
+
} from "pi-roundtable";
|
|
403
|
+
import { Type } from "typebox";
|
|
404
|
+
|
|
405
|
+
/** One person may have several identities; surface roles never make an owner. */
|
|
406
|
+
export const access = {
|
|
407
|
+
owners: [
|
|
408
|
+
{
|
|
409
|
+
principal: "operator",
|
|
410
|
+
name: "Ada",
|
|
411
|
+
identities: ["discord:966666600000000001", "token:remote-mcp"],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
members: { roles: ["web:role:App.User"] },
|
|
415
|
+
provisioning: "admitted",
|
|
416
|
+
} satisfies AccessConfig;
|
|
417
|
+
|
|
418
|
+
/** Ask about the current principal, not the external actor or the primary owner. */
|
|
419
|
+
export const principals = definePlugin({
|
|
420
|
+
name: "principals",
|
|
421
|
+
requires: [IDENTITY],
|
|
422
|
+
setup: ({ services }) => {
|
|
423
|
+
const identity = services.get(IDENTITY);
|
|
424
|
+
return {
|
|
425
|
+
tools: [
|
|
426
|
+
defineTool({
|
|
427
|
+
name: "principal_who",
|
|
428
|
+
description: "Show who this turn is for.",
|
|
429
|
+
parameters: Type.Object({}),
|
|
430
|
+
minTier: "member",
|
|
431
|
+
run: async (_args, turn) => {
|
|
432
|
+
if (!turn.speaker)
|
|
433
|
+
throw new ToolRefusal("No speaker for this turn.");
|
|
434
|
+
const person = await identity.principal(turn.speaker.principalId);
|
|
435
|
+
if (!person || person.disabled)
|
|
436
|
+
throw new ToolRefusal("This principal is not available.");
|
|
437
|
+
return `${person.displayName} (${person.id})`;
|
|
438
|
+
},
|
|
439
|
+
}),
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
<!-- /example -->
|
|
446
|
+
|
|
447
|
+
<!-- example: examples/principals.test.ts -->
|
|
448
|
+
```ts
|
|
449
|
+
import { expect, test } from "bun:test";
|
|
450
|
+
import { IDENTITY, type Speaker } from "pi-roundtable";
|
|
451
|
+
import { servicePair, testPlugin } from "pi-roundtable/testing";
|
|
452
|
+
import { access, principals } from "./principals.ts";
|
|
453
|
+
|
|
454
|
+
test("two actor identities of one principal ask about the same person", async () => {
|
|
455
|
+
const lookedUp: string[] = [];
|
|
456
|
+
const harness = await testPlugin(principals, {
|
|
457
|
+
services: [
|
|
458
|
+
servicePair(IDENTITY, {
|
|
459
|
+
principal: async (id) => {
|
|
460
|
+
lookedUp.push(id);
|
|
461
|
+
return { id, displayName: "Mo", disabled: false };
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
try {
|
|
467
|
+
for (const id of ["discord-actor", "web-actor"]) {
|
|
468
|
+
const speaker: Speaker = {
|
|
469
|
+
id,
|
|
470
|
+
name: "Mo",
|
|
471
|
+
principalId: "p_mo",
|
|
472
|
+
tier: "member",
|
|
473
|
+
};
|
|
474
|
+
expect(await harness.runTool("principal_who", {}, { speaker })).toBe(
|
|
475
|
+
"Mo (p_mo)",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
expect(lookedUp).toEqual(["p_mo", "p_mo"]);
|
|
479
|
+
} finally {
|
|
480
|
+
await harness.stop();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("an unavailable principal is refused even at owner tier", async () => {
|
|
485
|
+
const harness = await testPlugin(principals, {
|
|
486
|
+
services: [servicePair(IDENTITY, { principal: async () => undefined })],
|
|
487
|
+
});
|
|
488
|
+
try {
|
|
489
|
+
expect(
|
|
490
|
+
await harness.runTool(
|
|
491
|
+
"principal_who",
|
|
492
|
+
{},
|
|
493
|
+
{
|
|
494
|
+
speaker: {
|
|
495
|
+
id: "actor",
|
|
496
|
+
name: "Ada",
|
|
497
|
+
principalId: "operator",
|
|
498
|
+
tier: "owner",
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
),
|
|
502
|
+
).toContain("This principal is not available.");
|
|
503
|
+
expect(access.owners[0]?.principal).toBe("operator");
|
|
504
|
+
expect(access.members.roles).toEqual(["web:role:App.User"]);
|
|
505
|
+
} finally {
|
|
506
|
+
await harness.stop();
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
<!-- /example -->
|
|
511
|
+
|
|
512
|
+
For existing hosts and compatibility aliases, read [Migrating to 0.9](migration-0.9.md).
|
|
513
|
+
|
|
347
514
|
## Tiers: who may use what
|
|
348
515
|
|
|
349
516
|
Every turn has a speaker with a tier: `owner`, `admin`, or `member`, from most to least trusted.
|
|
350
|
-
By default, only the owner speaks.
|
|
351
|
-
The operator can open
|
|
517
|
+
By default, only the configured owner speaks.
|
|
518
|
+
The operator can open admission through `access` and is responsible for that setup.
|
|
519
|
+
A turn's `kind` chooses a persona, never a tier.
|
|
352
520
|
|
|
353
521
|
Each tool names the lowest tier that may call it.
|
|
354
522
|
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.
|
|
@@ -516,7 +684,7 @@ export const cleanup = definePlugin({
|
|
|
516
684
|
|
|
517
685
|
A rule whose verdict depends on the input, such as one that holds only a `delete` action, can also answer `mayHold(tool)`: whether it may hold some call of that tool.
|
|
518
686
|
It is asked when the input is not known yet, as for a [precheck script](#precheck-scripts-prechecks-the-agent-writes)'s call whose arguments are computed when it runs; a rule without it is judged by `describe` with an empty input.
|
|
519
|
-
A rule whose held call stands for others can answer `approvalTier(tool, input, context)`: the lowest tier that may approve it when higher than the tool's own. The held call keeps it as `minTier`, and both its card and a confirming message require it. A card is answered, and a confirming message accepted, from the speaker whose turn held the call, when their tier is at least `minTier`, and from the
|
|
687
|
+
A rule whose held call stands for others can answer `approvalTier(tool, input, context)`: the lowest tier that may approve it when higher than the tool's own. The held call keeps it as `minTier`, and both its card and a confirming message require it. A card is answered, and a confirming message accepted, from the speaker whose turn held the call, when their tier is at least `minTier`, and from the owners where the conversation is shared ([who answers a prompt](#who-answers-a-prompt-promptscope)); nobody else in the channel may approve it. The held call records that speaker as `PendingConfirmation.speakerId` and their principal as `principalId`, so a confirming message from another identity of theirs counts after a restart too; a call held without them is the owners' to approve, and one held before 0.9, without a principal, is matched by `speakerId`.
|
|
520
688
|
|
|
521
689
|
#### The shell rule
|
|
522
690
|
|
|
@@ -601,7 +769,7 @@ test("the section names the agent, and the speaker when there is one", async ()
|
|
|
601
769
|
expect(
|
|
602
770
|
section?.build({
|
|
603
771
|
agent,
|
|
604
|
-
speaker: { id: "1", name: "Ada", tier: "member" },
|
|
772
|
+
speaker: { id: "1", name: "Ada", tier: "member", principalId: "1" },
|
|
605
773
|
scope,
|
|
606
774
|
}),
|
|
607
775
|
).toContain("You are talking with Ada.");
|
|
@@ -855,8 +1023,9 @@ The core stores the script with the schedule and decides with it exactly as with
|
|
|
855
1023
|
pi-roundtable-sandbox's `precheckScriptRunner` runs each script in a sealed container whose only way out is the MCP tools the host grants for that schedule; see its README.
|
|
856
1024
|
|
|
857
1025
|
- `schedule_create` and `schedule_update` take `precheck_script`, a JavaScript module of at most `PRECHECK_SCRIPT_CHARS` (8,000) characters with a default export; it is parsed, never run, when it is set. A schedule has a `precheck` or a `precheck_script`; setting one removes the other, and `null` removes either.
|
|
858
|
-
- The runner's `run(script, context)` gets the `PrecheckScriptContext`: the schedule, `firedAt`, `signal`, the host's `timeZone`, and `today`, the date there. Its answer is checked like a named precheck's, and its finding is named `script`.
|
|
859
|
-
- `describe({ channel, target, tier })` (a `PrecheckScope`) tells the model how to write one and what it may call there; `schedule_list` shows it, waiting at most 10 seconds. `tier` is the asker's there and the
|
|
1026
|
+
- The runner's `run(script, context)` gets the `PrecheckScriptContext`: the schedule, `firedAt`, `tier`, `signal`, the host's `timeZone`, and `today`, the date there. `tier` is the tier the run is for: the schedule's, capped at what its creator holds now, as its turn would run. Its answer is checked like a named precheck's, and its finding is named `script`.
|
|
1027
|
+
- `describe({ channel, target, tier })` (a `PrecheckScope`) tells the model how to write one and what it may call there; `schedule_list` shows it, waiting at most 10 seconds. `tier` is the asker's there and the run's `tier` when it runs, so a runner may grant lower tiers less.
|
|
1028
|
+
- Before a schedule's precheck, named or a script, the scheduler checks who its turn would run as, as `ConversationPort.runsAs` does: a creator who may not run it now, such as one disabled, runs no precheck, and the run is recorded `skipped:` with the reason.
|
|
860
1029
|
- When the host stops, the scheduler aborts running scripts and waits up to 15 seconds for the runner to clean up; a precheck that ends then starts no turn.
|
|
861
1030
|
- Without a runner, the tools neither take nor mention `precheck_script`, a script is refused with the registered names, and a schedule that already has one wakes with `### Precheck failed: script`, never a silent skip.
|
|
862
1031
|
|
|
@@ -1043,8 +1212,9 @@ The slot's default factory refuses calls: read the running runtime with `service
|
|
|
1043
1212
|
The slot is a `RuntimeFactory`: `(deps: RuntimeDeps) => AgentRuntime`, called once when the runtime plugin sets up, before the agent server.
|
|
1044
1213
|
`deps` provides `logger`, `env`, `owner`, `toolTiers`, and the host's `judge`.
|
|
1045
1214
|
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.
|
|
1046
|
-
`prompts(conversation,
|
|
1047
|
-
`
|
|
1215
|
+
`prompts(conversation, scope)` gives you the approval and question prompts on the conversation's surface for a turn's `PromptScope`, made with `promptScope(speaker, visibility)` from the turn's speaker and `TurnRequest.conversation`, or `undefined`; see [who answers a prompt](#who-answers-a-prompt-promptscope).
|
|
1216
|
+
Given a `Speaker`, the 0.8 form, it reads them as the speaker of a shared conversation and the host warns once that the form goes away in 1.0.
|
|
1217
|
+
`agents` holds the agent server's per-agent settings: `workDir`, `scratchDir` (the agents' shell's `TMPDIR`), `skills(name)`, `modelOf(name)`, `turnChannel(scope)`, and `memory` (`"everyone"`, the default, or `"owners"`; see [memory in the agents' channels](#memory-in-the-agents-channels)).
|
|
1048
1218
|
The agent server sets up after the runtime is built, so read `agents` when a turn runs; it is `undefined` on a host without the agent server, where no agent turn runs.
|
|
1049
1219
|
`confirmations` stores held actions across restarts.
|
|
1050
1220
|
|
|
@@ -1056,13 +1226,13 @@ An `AgentRuntime` has these methods:
|
|
|
1056
1226
|
| `steer(conversation, text, attachments, speakerId?)` | Adds a message to the conversation's running steerable turn; `false` when the message must wait for its own turn |
|
|
1057
1227
|
| `stop(conversation)` | Aborts the conversation's running turn; `false` when none runs |
|
|
1058
1228
|
| `startFresh(conversation)`, `deleteConversation(conversation)` | Archive the conversation, or remove it for good; called between turns |
|
|
1059
|
-
| `pendingConfirmation(conversation)`, `heldActions(conversation)` | The held actions known in memory, and those restored from the store after a restart: the agent server reads `heldActions` to show an approval card again after a restart. A `PendingConfirmation` carries the `selectionId` of the `TurnSelection` whose turn held the calls, an opaque string that the caller resolves again when the owner confirms; the core stores it and never reads it |
|
|
1060
|
-
| `recentTranscript(conversation, limit)` | The latest messages, for the owner's and the dashboard's views of a conversation |
|
|
1229
|
+
| `pendingConfirmation(conversation)`, `heldActions(conversation)` | The held actions known in memory, and those restored from the store after a restart: the agent server reads `heldActions` to show an approval card again after a restart. After a restart `pendingConfirmation` knows a conversation's held actions only once `heldActions` or `recentTranscript` has restored them, so a host that decides an approval asks one of them first. A `PendingConfirmation` carries the `selectionId` of the `TurnSelection` whose turn held the calls, an opaque string that the caller resolves again when the owner confirms; the core stores it and never reads it |
|
|
1230
|
+
| `recentTranscript(conversation, limit)` | The latest messages, for the owner's and the dashboard's views of a conversation; the built-in runtime gives none of a history recorded, or a session opened, for someone else than the conversation serves now |
|
|
1061
1231
|
| `contextUsage?(conversation)` | How full the conversation's context is, `{ tokens, contextWindow }`; leave it out and the team status shows no context bar |
|
|
1062
1232
|
| `preflight?()` | Runs in the host's preflight, before anything starts; a throw stops the boot |
|
|
1063
1233
|
| `dispose?()` | Runs when the host stops the runtime plugin's `runtime` service |
|
|
1064
1234
|
|
|
1065
|
-
A request has the turn's `channel`, `selection` (its tools), `text`, `attachments`, `speaker`, flags (`steerable`, `interactive`, `confirmed`), and `interim`, where the turn may post the text it writes before its final answer ([interim text](#interim-text-what-a-turn-writes-before-its-final-answer)).
|
|
1235
|
+
A request has the turn's `channel`, `selection` (its tools), `text`, `attachments`, `speaker` (required: the person the turn is for, at whose tier it runs; the built-in runtime refuses a request without one with an `AgentRunError` before asking the model, and a plugin that starts a turn on someone's behalf gets theirs from `IDENTITY.speakerFor`), `conversation` (`TurnConversation`: `private` to a `principalId`, or `shared`, as `context.turns` records it; shared when absent), flags (`steerable`, `interactive`, `confirmed`), and `interim`, where the turn may post the text it writes before its final answer ([interim text](#interim-text-what-a-turn-writes-before-its-final-answer)).
|
|
1066
1236
|
An agent's turn also has `agent`, the agent's scope, whose `session` is the conversation's key.
|
|
1067
1237
|
Other turns use `kind` to name the conversation's persona, defaulting to `"owner"` when absent.
|
|
1068
1238
|
|
|
@@ -1243,6 +1413,7 @@ test("an agent's turn carries its scope, and its conversation is the scope's ses
|
|
|
1243
1413
|
channel: "fake:agent-room",
|
|
1244
1414
|
selection: { id: "agent", tools: [], groups: [] },
|
|
1245
1415
|
text: "status?",
|
|
1416
|
+
speaker: OWNER_SPEAKER,
|
|
1246
1417
|
agent: {
|
|
1247
1418
|
name: "infra",
|
|
1248
1419
|
session: "fake:agent-room",
|
|
@@ -1354,9 +1525,12 @@ Call `commands.add` from `setup`; after preflight it throws `commands can be add
|
|
|
1354
1525
|
Use `services.find(DISCORD)` if your plugin can work without Discord.
|
|
1355
1526
|
When the service is absent, the rest of your plugin can run without commands.
|
|
1356
1527
|
|
|
1357
|
-
Build a feature's part of the root command with `ownerCommandModule(guard, handlers)` from `pi-roundtable/discord`;
|
|
1528
|
+
Build a feature's part of the root command with `ownerCommandModule(guard, handlers)` from `pi-roundtable/discord`; only the owners may use it, it defers interactions, and supplies a failure panel.
|
|
1358
1529
|
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.
|
|
1359
|
-
`DISCORD.guard` is the `CommandGuard`;
|
|
1530
|
+
`DISCORD.guard` is the `CommandGuard`; `await guard.allows(actor)` is whether the actor is an owner now: the primary owner by user id, and every other owner principal by their Discord identity, checked through `IDENTITY` each time, so an owner granted or revoked with the CLI is seen at once and a stranger is never resolved. It accepts an interaction or anything with `user.id` (a `DiscordActor`), so a test needs no cast.
|
|
1531
|
+
`guard.isOwner(actor)` is deprecated until 1.0: it is the primary owner only, by user id, so a component handler that still asks it refuses the other owners.
|
|
1532
|
+
`commandGuard({ ownerId, identity?, root, logger, refusalHint? })` makes one; without `identity` the primary owner is the only owner.
|
|
1533
|
+
The connection's `ownerChannel()` and `notifyOwner(text)` are deprecated until 1.0: they reach the primary owner alone. Reach a person through `context.directChannels` instead, where the Discord plugin's provider finds their direct messages by a Discord identity of theirs.
|
|
1360
1534
|
`DiscordOptions.refusalHint` (`discord.refusalHint` in the configuration) is appended unchanged to the refusal a non-owner gets.
|
|
1361
1535
|
|
|
1362
1536
|
<!-- example: examples/interactions.ts -->
|
|
@@ -1583,9 +1757,18 @@ The built-in addons use it: each declares the tiers of its own tools.
|
|
|
1583
1757
|
|
|
1584
1758
|
A session tool is a Pi extension placed in every conversation session by its phase: `tools`, `compaction`, or `mcp`.
|
|
1585
1759
|
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.
|
|
1586
|
-
Each extension needs a unique name; the core reserves `read-attachment`, `confirmation-gate`, `ask-user`, `self-compact-guard`, and `active-tools`.
|
|
1760
|
+
Each extension needs a unique name; the core reserves `read-attachment`, `confirmation-gate`, `ask-user`, `self-compact-guard`, `private-memory`, and `active-tools`.
|
|
1587
1761
|
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.
|
|
1588
1762
|
At most one plugin may add a `compaction` extension, and it must name the `engine` its compactions record.
|
|
1763
|
+
In a shared conversation the core's wrapper hands it the history to summarize as `summaryProjection` gives it, without anyone's private memory, the prompt states, or the turns' reasoning, and Pi's own summary reads the same, since a summary outlives the turns it covers and every later speaker reads it. A compactor that reads the history otherwise, outside `session.compaction.wrap` or in a runtime of your own, must summarize a shared conversation's history the same way: call `privateCompaction(event.preparation, branchMessages)` from `pi-roundtable/kit` before reading it, passing the message entries of `event.branchEntries` so exchanges split across the kept boundary are paired too.
|
|
1764
|
+
|
|
1765
|
+
The factory gets the session's `SessionContext`, fixed when the session is made:
|
|
1766
|
+
- `kind`, `homeChannel`, `turnChannel`, and `agent` say what the session is for and where its turns run.
|
|
1767
|
+
- `conversation` (`SessionConversation`) says whom it serves: `{ visibility: "private", principalId, principal? }` for one person's conversation, with their name and pronouns when the host has a record of them, or `{ visibility: "shared" }`. An agent's session is always shared. Otherwise it is as the turn's `TurnRequest.conversation` names it, else as the conversation registry recorded it, else shared; it is never the owner's by default. The runtime reads this again at every turn, the registry included, falling back to whom the open session or its recorded history served when nothing names the conversation, and refuses a turn of someone else in a conversation private to its person before the session is touched. A session's history records whom it served: when the conversation serves someone else, another principal or the other visibility, the session is made anew with nothing held for it, and its earlier history is archived rather than replayed, after a restart too; a history from before 0.9, which records no one, carries on. Changed session tools or skills rebuild a session with its history.
|
|
1768
|
+
- `addressee` is whom tool descriptions should name: the primary owner exactly as configured in their own private conversation, another person by their name and pronouns (their name alone when they gave none), and `THE_SPEAKER` in a shared conversation, or a private one of someone the host knows by no name yet.
|
|
1769
|
+
- `memory` is `"none"` for a persona that declares it, and `"speaker"` otherwise; a memory tool of your own loads nothing for `"none"`. A task the session runs with `runTask` keeps its conversation's `memory`.
|
|
1770
|
+
- `speaker()` is the running turn's speaker, undefined between turns; a tool reads it when it runs, never when the session is made, since a session outlives the turn it is built in.
|
|
1771
|
+
|
|
1589
1772
|
The core's Jev compactor is one such extension:
|
|
1590
1773
|
|
|
1591
1774
|
```ts
|
|
@@ -1661,6 +1844,13 @@ It leaves those other channels silent for the owner's notes, preventing other cl
|
|
|
1661
1844
|
Your Discord claim won't receive messages from that guild at any priority below 100, including `priority: 10`.
|
|
1662
1845
|
Claims on other surfaces, such as the example's `echo:` keys, don't compete with the agent server.
|
|
1663
1846
|
|
|
1847
|
+
Before a claim's `admit` sees a message, the router resolves its author through `IDENTITY` and sets `message.speaker`: their `id` on the surface, `name`, `tier`, and `principalId`, the principal whose memory, schedules, and conversations are theirs.
|
|
1848
|
+
It is undefined when the access rules serve no one by them, and for bots and integrations, which are not resolved; a `speaker` the surface set is dropped.
|
|
1849
|
+
A claim that serves only the people the host serves answers only when `speaker` is set.
|
|
1850
|
+
The router records the author, such as linking them at their first contact, only once a claim admits the message, so messages no claim takes link or make no one.
|
|
1851
|
+
One exception: an author already linked whom the access rules now refuse is recorded as seen at no tier when the message is resolved, whether or not a claim takes it, at most once every few minutes, so their background turns stop with them.
|
|
1852
|
+
When that record finds the author someone else than the claim admitted, such as linked to another principal meanwhile or refused now, or cannot be made, the router drops the message: a turn admission's optional `dropped()` is called instead of `run`, so the claim frees what it holds for it, and a background admission's `unanswered` hears it skipped.
|
|
1853
|
+
|
|
1664
1854
|
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.
|
|
1665
1855
|
The router calls only the owning claim's `stop`, returning `false` when that method is absent.
|
|
1666
1856
|
Give `stop` to a claim whose conversations run turns that can be interrupted.
|
|
@@ -1706,7 +1896,8 @@ export const echo = definePlugin({
|
|
|
1706
1896
|
|
|
1707
1897
|
A conversation has a kind: the string its claim returns from `startFresh`, such as `"owner"` or `"study"`.
|
|
1708
1898
|
Your plugin names its claims' kinds; the host treats them as opaque strings.
|
|
1709
|
-
A `Persona` supplies the system prompt for every non-agent conversation of one kind: `{ kind, prompt() }`.
|
|
1899
|
+
A `Persona` supplies the system prompt for every non-agent conversation of one kind: `{ kind, prompt(), memory? }`.
|
|
1900
|
+
`memory: "none"` leaves memory out of its conversations: no memory tools and no memory in the prompt; `"speaker"`, the default, reads as the next list says.
|
|
1710
1901
|
The runtime reads `prompt()` when it creates the conversation's session, so message catalog text uses the host's language.
|
|
1711
1902
|
|
|
1712
1903
|
- A plugin contributes `personas` with the kinds it owns; `sessions().persona(kind)` is the linked lookup a runtime uses.
|
|
@@ -1717,7 +1908,12 @@ The runtime reads `prompt()` when it creates the conversation's session, so mess
|
|
|
1717
1908
|
The host merges every plugin's list and keeps each name once.
|
|
1718
1909
|
- A turn whose kind has no persona is refused with an error naming `personas`.
|
|
1719
1910
|
The Pi runtime refuses when it makes the session; a runtime of your own does the same, as the example's does.
|
|
1720
|
-
- With the memory addon on,
|
|
1911
|
+
- With the memory addon on, the conversation decides whose memory the system prompt carries and the memory tools change, by principal and never by tier: a private conversation's person's, whoever speaks in it; in a shared conversation, each turn's speaker's (`speaker.principalId`), so a second owner reads their own and not the primary owner's; and no one's in the host's own turns (`SYSTEM_PRINCIPAL`) in a shared conversation.
|
|
1912
|
+
The primary owner's memory keeps its 0.8 heading and rows: their principal is 0.8's owner id.
|
|
1913
|
+
- A shared conversation's history never hands one person's memory to another. Each memory tool's result records whose memory it holds (`details: { privateTo: <principalId> }`), and before every model request the Pi runtime shows a result of someone other than the turn's reader, the host's own turns included, as `(another person's private memory, hidden)`, and a memory result that does not say whose as a placeholder too, one recorded before results said whose or a failed call's, such as one whose arguments were refused (a memory tool whose store fails still records whose memory the call was for, so its person reads their own failed exchange); the call that result answers, matched by its id, keeps its id and name but reads its arguments as `{ hidden: <the same placeholder> }`, so what the model copied from that memory into the call is not sent either; the prompt states Pi recorded in the history collapse into the current prompt alone, so an earlier speaker's memory section is not sent either, and the answers of earlier turns go without their reasoning (thinking, encrypted reasoning, tool calls' thought signatures, and the thought signature Gemini puts on an answer's text; OpenAI's signature of an answer's message, which names its id and phase, is no reasoning, but the message and call item ids OpenAI's Responses paired with the removed reasoning go with it, while each call keeps the id its result answers and each message its phase), which may restate the memory their turn read; the running turn keeps its own. What people wrote, and the assistant's replies, stay shared. A tool of your own whose result holds someone's private data returns the same `details.privateTo` principal id, including an error that repeats private input: both its arguments and result are hidden, regardless of the tool name.
|
|
1914
|
+
A hidden result also drops private payload in its `details` and nested-call records, retaining only its ownership marker.
|
|
1915
|
+
A host building an independent shared Pi session applies `memoryProjection` on `context_with_system` and `privateCompaction` before summarizing; the optional surrounding-history arguments of `summaryProjection` and `privateCompaction` preserve redaction across split exchanges. In the opt-in sandbox Pi protocol, return `PiToolResponse.privateTo` and pass the admitted `author.principalId`; the worker records `details.privateTo` and applies those projections itself. The runtime does it for you when the tool's `session.runTask` worker loaded the reader's memory: that call's result, and those of the calls it runs within (the outer call of a nested call, the call whose worker's task started it), record the reader as whose it is, while a call merely running beside it keeps its result public. The runtime knows which call started a task from the call its session tools' `execute` runs within; a task started from outside every tool call, such as an event handler's, cannot say, so every call running then records the reader. The guarantee, then, covers memory exchanges: a memory tool's call and result, and a call whose worker loaded the reader's memory and its result, each pair hidden from every other speaker and from compaction summaries, and the reasoning of earlier turns. Anything else the model writes, into another tool's arguments or into its reply, is public, and whatever it repeats there of someone's memory reaches everyone in the conversation. This holds for the requests the core sends; a provider that replays history of its own bypasses it. Each turn records its actual reader and whether its request carried private prompt memory, a memory exchange, or a memory-loaded worker. Before any provider call and on agent model switches too, claude-bridge checks the raw branch, including compacted reader records, and refuses a turn whose history carried another reader's private memory or an inaccessible explicit exchange. This refuses reasoning replay across readers even without memory tool calls. A recorded turn without private memory never blocks another reader merely for speaking; malformed, truncated and unanswered memory calls belong to their recorded turn reader, so its own reader can continue. For this core check only SYSTEM counts as primary owner, including modern exchanges/reasoning; SYSTEM can read that raw owner memory and repeat it publicly, despite per-request projection loading no personal memory for SYSTEM. Pre-0.9 untagged memory results before the first scope record retain owner attribution; later unowned exchanges without reader records fail closed. Disabling future memory does not bypass recorded private history. In Pi sandbox mode, party-wide prompt blocks must explicitly set `memory.visibility: "shared"` (default private), and the worker capability handshake fails private turns closed on an old image. Admission of other principals warns at startup and in doctor rather than failing boot. Public replies, untagged custom tools and old summaries remain possible disclosure paths: see the [bridge guard](migration-0.9.md#claude-bridge-guard).
|
|
1916
|
+
- A conversation 0.8 recorded through `context.turns` without `conversation` stays shared: whether such a room was one person's cannot be told, and adopting it for someone would refuse everyone else's turns. Its memory is each speaker's, as before, and its tools name the speaker. A plugin whose 0.8 conversations were one person's adopts them with `CONVERSATIONS.adopt`, as remote-mcp does.
|
|
1721
1917
|
|
|
1722
1918
|
Call `context.turns.run(input)` inside your claim's queue task to run a turn in a conversation it owns.
|
|
1723
1919
|
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`.
|
|
@@ -1727,12 +1923,19 @@ Supply `reply(result)` to handle the reply yourself.
|
|
|
1727
1923
|
It rejects with `NotLinkedError` during `setup`, and with a `PluginError` on a host whose runtime plugin has not provided a runtime.
|
|
1728
1924
|
|
|
1729
1925
|
Before each turn runs, `context.turns.run` records its conversation in the host's registry, `CONVERSATIONS`: at the first turn its key, surface, kind, visibility, owner, and title, and at every later turn only that it was active.
|
|
1730
|
-
`conversation: { visibility: "private" }` records it as the speaker's own (`principalId` is `speaker.
|
|
1926
|
+
`conversation: { visibility: "private" }` records it as the speaker's own (its `principalId` is `speaker.principalId`); without it the conversation is `shared`.
|
|
1927
|
+
Only its person speaks in a private conversation: a turn whose speaker is another principal is refused before it starts, except the host's own turns (`SYSTEM_PRINCIPAL`), such as a report's.
|
|
1928
|
+
Neither a later turn nor another plugin changes what the first turn recorded, with one exception for data from before 0.9: `adopt(key, principalId)` makes a conversation recorded `shared` with no principal, as 0.8 recorded every turn run without `conversation`, private to that principal.
|
|
1929
|
+
It is for a plugin migrating what its own 0.8 version left, such as remote-mcp giving each session 0.8 opened to the primary owner; it never changes a private conversation or one with a principal, and repeating or racing it is safe.
|
|
1930
|
+
Adopting a conversation that 0.9 or later recorded is outside its contract: a shared conversation recorded since then is shared on purpose, and a later version may refuse to adopt it.
|
|
1731
1931
|
`conversation.title` names it at its first turn; `setTitle(key, title)` renames it later.
|
|
1932
|
+
The runtime is told who the conversation belongs to as the registry keeps it, in `TurnRequest.conversation`, so a private conversation's prompts stay its person's at every later turn; a host without the registry passes the turn's own `conversation`, if it gives one.
|
|
1732
1933
|
A turn whose conversation cannot be recorded does not run, and the call rejects.
|
|
1733
1934
|
The registry records and never refuses: who may speak in a conversation stays your claim's decision, which may read `get(key)` to check the owner.
|
|
1734
1935
|
`list({ principal })` gives one principal's conversations and `list()` every one, the most recently active first; the web console lists them too.
|
|
1735
|
-
|
|
1936
|
+
A conversation from before the registry is recorded at its next turn.
|
|
1937
|
+
Register a private conversation before the first direct runtime call, transcript reads included, rather than relying on a current speaker to define the session.
|
|
1938
|
+
Never change visibility to reuse history: when the effective private principal or visibility changes, the runtime archives history, drops held actions, and starts isolated, after a restart too.
|
|
1736
1939
|
|
|
1737
1940
|
<!-- example: examples/study-room.ts -->
|
|
1738
1941
|
```ts
|
|
@@ -1763,25 +1966,22 @@ export const studyRoom = definePlugin({
|
|
|
1763
1966
|
priority: 10,
|
|
1764
1967
|
// The id of a room starts with `study-`, on whichever surface carries it.
|
|
1765
1968
|
owns: (channel) => parseChannelKey(channel).id.startsWith("study-"),
|
|
1766
|
-
admit: (message) =>
|
|
1767
|
-
message.
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
},
|
|
1783
|
-
failure: "a study turn failed",
|
|
1784
|
-
},
|
|
1969
|
+
admit: (message) => {
|
|
1970
|
+
const speaker = message.speaker;
|
|
1971
|
+
if (message.authorIsBot || !speaker) return undefined;
|
|
1972
|
+
return {
|
|
1973
|
+
kind: "turn",
|
|
1974
|
+
run: async () => {
|
|
1975
|
+
await turns.run({
|
|
1976
|
+
channel: message.channel,
|
|
1977
|
+
kind: STUDY,
|
|
1978
|
+
text: message.text,
|
|
1979
|
+
speaker,
|
|
1980
|
+
});
|
|
1981
|
+
},
|
|
1982
|
+
failure: "a study turn failed",
|
|
1983
|
+
};
|
|
1984
|
+
},
|
|
1785
1985
|
// What the conversation was, so a host picks the right persona when it starts over.
|
|
1786
1986
|
startFresh: async () => STUDY,
|
|
1787
1987
|
},
|
|
@@ -1796,7 +1996,8 @@ The test uses the fake surface and echo runtime from [the runtime slot](#the-run
|
|
|
1796
1996
|
<!-- example: examples/study-room.test.ts -->
|
|
1797
1997
|
```ts
|
|
1798
1998
|
import { expect, test } from "bun:test";
|
|
1799
|
-
import {
|
|
1999
|
+
import { IDENTITY } from "pi-roundtable";
|
|
2000
|
+
import { servicePair, testPlugin } from "pi-roundtable/testing";
|
|
1800
2001
|
import { createEchoRuntime } from "./echo-runtime.ts";
|
|
1801
2002
|
import { FakeSurface } from "./fake-surface.ts";
|
|
1802
2003
|
import { studyRoom } from "./study-room.ts";
|
|
@@ -1807,6 +2008,16 @@ async function studying() {
|
|
|
1807
2008
|
const harness = await testPlugin(studyRoom, {
|
|
1808
2009
|
surfaces: [surface],
|
|
1809
2010
|
providers: { runtime: createEchoRuntime },
|
|
2011
|
+
services: [
|
|
2012
|
+
servicePair(IDENTITY, {
|
|
2013
|
+
resolve: async (facts) => ({
|
|
2014
|
+
id: facts.subject,
|
|
2015
|
+
name: facts.name,
|
|
2016
|
+
principalId: "student",
|
|
2017
|
+
tier: "member",
|
|
2018
|
+
}),
|
|
2019
|
+
}),
|
|
2020
|
+
],
|
|
1810
2021
|
});
|
|
1811
2022
|
return { surface, harness };
|
|
1812
2023
|
}
|
|
@@ -1836,6 +2047,7 @@ test("a message in a study room runs as a turn of the study kind, with the tutor
|
|
|
1836
2047
|
expect(started?.turn).toMatchObject({
|
|
1837
2048
|
kind: "study",
|
|
1838
2049
|
channel: "fake:study-algebra",
|
|
2050
|
+
speaker: { principalId: "student", tier: "member" },
|
|
1839
2051
|
});
|
|
1840
2052
|
expect(started?.turn?.agent).toBeUndefined();
|
|
1841
2053
|
await harness.stop();
|
|
@@ -1857,6 +2069,22 @@ test("starting a room over says it was a study conversation", async () => {
|
|
|
1857
2069
|
await harness.stop();
|
|
1858
2070
|
});
|
|
1859
2071
|
|
|
2072
|
+
test("an author with no resolved tier never starts a study turn", async () => {
|
|
2073
|
+
const surface = new FakeSurface();
|
|
2074
|
+
const harness = await testPlugin(studyRoom, {
|
|
2075
|
+
surfaces: [surface],
|
|
2076
|
+
providers: { runtime: createEchoRuntime },
|
|
2077
|
+
});
|
|
2078
|
+
try {
|
|
2079
|
+
surface.say("fake:study-algebra", "hello");
|
|
2080
|
+
await Bun.sleep(30);
|
|
2081
|
+
expect(surface.replies).toEqual([]);
|
|
2082
|
+
expect(harness.events).toEqual([]);
|
|
2083
|
+
} finally {
|
|
2084
|
+
await harness.stop();
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
|
|
1860
2088
|
test("the persona is the plugin's and belongs to the study kind only", async () => {
|
|
1861
2089
|
const { harness } = await studying();
|
|
1862
2090
|
const [persona] = harness.contribution.personas ?? [];
|
|
@@ -1867,18 +2095,136 @@ test("the persona is the plugin's and belongs to the study kind only", async ()
|
|
|
1867
2095
|
```
|
|
1868
2096
|
<!-- /example -->
|
|
1869
2097
|
|
|
2098
|
+
### Channel context: what was said around an addressed message
|
|
2099
|
+
|
|
2100
|
+
In a Discord server channel, people talk among themselves and address the assistant now and then.
|
|
2101
|
+
Channel context lets the turn for an addressed message also read what was posted there since the assistant last posted, mentions or not.
|
|
2102
|
+
The agent server reads it for every turn in an agent's channel; a group's round does not, since it already carries what was said since the group's last turn.
|
|
2103
|
+
A plugin's claim opts in by calling `DISCORD.channelContext(message, options?)` from `pi-roundtable/discord` inside its turn's `run`, and appending the result with `withChannelContext(text, context)`, as `withReference` adds the replied-to message.
|
|
2104
|
+
|
|
2105
|
+
It reads Discord, not the host's history, so it survives restarts:
|
|
2106
|
+
|
|
2107
|
+
- It fetches the `fetch` messages before the addressed one (default 50, at most 100) and keeps those after the assistant's latest post there: its bot user's, or one of its application's webhooks, such as an agent's voice. Any post of the assistant ends the window, including a notice that a conversation started over; the core posts no marker of its own, so a fresh-start reply must be a public post, not an ephemeral one, to cut the window.
|
|
2108
|
+
- It leaves out the assistant's own posts, the addressed message, empty messages, and every message it handed over before or that addressed it before in that channel, remembered in memory per channel, so a restart can repeat at most what came since the assistant's last post.
|
|
2109
|
+
- Other bots and integrations are kept and marked as bots; the host's owners (the primary owner and every other owner's Discord identities) are marked as owners.
|
|
2110
|
+
- Consecutive messages of one author at least `similarity` alike (default 0.8, by their longest common run of text) count as one, the later kept.
|
|
2111
|
+
- It keeps the newest `keep` (default 15), oldest first. Mentions read as names, and stickers and files as text with their names and links; nothing is downloaded or shown to the model as an image. A message is cut at `messageChars` (default 500), another bot's at `botMessageChars` (default 80).
|
|
2112
|
+
- It never fails the turn over Discord: a Discord error, such as a channel the bot cannot read, is logged and the turn runs without context.
|
|
2113
|
+
|
|
2114
|
+
`DISCORD.channelContext` resolves undefined in a direct message, on another surface, when nothing new was said, and when the host's `discord.channelContext` is `false`, which turns it off for the agent server and every claim.
|
|
2115
|
+
The configuration's values are the host's defaults, and a call's `options` override them for that call.
|
|
2116
|
+
Call it once per answered message: what it returns is remembered as handed over.
|
|
2117
|
+
A call's `options` are validated like the configuration (`fetch`, `keep`, `messageChars` and `botMessageChars` positive integers, `similarity` from 0 to 1); an invalid one makes the call reject, a programming error, so `keep: 0` is not "none" (skip the call for that).
|
|
2118
|
+
It remembers what it handed over and what addressed the assistant, not every message that ran a turn: a post a claim answered some other way, such as a webhook report turn or a steered message, can also come in the context of a later turn if the assistant has not posted since.
|
|
2119
|
+
|
|
2120
|
+
What it returns is public channel text, visible to everyone in the channel; it is not private memory, carries no `privateTo`, and goes into the turn's text, so it lives in the conversation's history, not in the system prompt.
|
|
2121
|
+
The turn's reader stays the person who addressed the assistant.
|
|
2122
|
+
People the access rules give no tier are included as channel text, as everyone there sees them, but they do not become speakers: they are not resolved, linked, or recorded, and the turn does not run as them.
|
|
2123
|
+
Only server channels are read, never direct messages, and only messages the bot can read.
|
|
2124
|
+
|
|
2125
|
+
The window ends at the assistant's last public post, so a conversation started over must end it too, and the owner's reply to the command is ephemeral.
|
|
2126
|
+
When any claim's conversation is started over in a server channel or thread, through `conversations.startFresh` (the agent server's channels, a plugin's claim such as a party, an owner channel), the core posts one short divider there as the bot, after the claim's `startFresh` succeeded, and the next turn reads nothing from before it.
|
|
2127
|
+
It is not posted in a direct message or on another surface, and a failed post is logged as a warning: the restart still counts and the ephemeral reply still goes out.
|
|
2128
|
+
The text is `discord.freshMarker` (default `─── new conversation ───`); `false` posts none.
|
|
2129
|
+
A surface a plugin contributes may implement the optional `ChatSurface.markFresh(channel)` for the same job.
|
|
2130
|
+
|
|
2131
|
+
The block gives each author as a name, their Discord id and a role (`owner`, `bot` or `member`) in separate attributes, and says the messages were not addressed to the assistant. A name loses the characters `"`, `'`, `=`, `<`, `>`, `(`, `)` and `,`, so it cannot imitate another author, an attribute or a role, and every `<` (also the full-width `<`) in a message's text is replaced by `‹`, so a message can neither close the block nor open a message of its own.
|
|
2132
|
+
`ChannelContext` is `{ messages, text }`: `messages` are `ChannelContextMessage`s (`id`, `authorId`, `authorName`, `bot`, `owner`, `text`, `at`), oldest first, and `text` is the block.
|
|
2133
|
+
`ChannelContextOptions` holds the options above, and `CHANNEL_CONTEXT_DEFAULTS` their defaults.
|
|
2134
|
+
In a test, `fakeDiscord({ channelContext })` answers `DISCORD.channelContext` as the test says; by default with no context.
|
|
2135
|
+
|
|
2136
|
+
<!-- example: examples/channel-context.ts -->
|
|
2137
|
+
```ts
|
|
2138
|
+
import { definePlugin, parseChannelKey } from "pi-roundtable";
|
|
2139
|
+
import { DISCORD, withChannelContext } from "pi-roundtable/discord";
|
|
2140
|
+
|
|
2141
|
+
/** The kind of the tavern's conversations. */
|
|
2142
|
+
const TAVERN = "tavern";
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* A claim that opts in to channel context: in the Discord channels it owns it answers only the
|
|
2146
|
+
* messages that mention the assistant or reply to it, and each turn also reads what the others
|
|
2147
|
+
* said there since the assistant last posted. The context is public channel text appended to the
|
|
2148
|
+
* turn's text; the people who wrote it do not become speakers.
|
|
2149
|
+
*/
|
|
2150
|
+
export function tavern(channelIds: readonly string[]) {
|
|
2151
|
+
return definePlugin({
|
|
2152
|
+
name: "tavern",
|
|
2153
|
+
setup: ({ turns, services }) => {
|
|
2154
|
+
const discord = services.get(DISCORD);
|
|
2155
|
+
return {
|
|
2156
|
+
personas: [
|
|
2157
|
+
{
|
|
2158
|
+
kind: TAVERN,
|
|
2159
|
+
prompt: () =>
|
|
2160
|
+
"You keep the tavern's table. Answer the person who addressed you.",
|
|
2161
|
+
},
|
|
2162
|
+
],
|
|
2163
|
+
channels: [
|
|
2164
|
+
{
|
|
2165
|
+
name: "tavern",
|
|
2166
|
+
priority: 10,
|
|
2167
|
+
owns: (channel) => {
|
|
2168
|
+
const { surface, id } = parseChannelKey(channel);
|
|
2169
|
+
return surface === "discord" && channelIds.includes(id);
|
|
2170
|
+
},
|
|
2171
|
+
admit: (message) => {
|
|
2172
|
+
const { speaker } = message;
|
|
2173
|
+
if (message.authorIsBot || !speaker) return undefined;
|
|
2174
|
+
if (!message.mentionsBot && !message.repliesToBot)
|
|
2175
|
+
return undefined;
|
|
2176
|
+
return {
|
|
2177
|
+
kind: "turn",
|
|
2178
|
+
run: async () => {
|
|
2179
|
+
// Once per answered message, inside `run`: what it returns is not returned again.
|
|
2180
|
+
const around = await discord.channelContext(message, {
|
|
2181
|
+
keep: 20,
|
|
2182
|
+
});
|
|
2183
|
+
await turns.run({
|
|
2184
|
+
channel: message.channel,
|
|
2185
|
+
kind: TAVERN,
|
|
2186
|
+
text: withChannelContext(message.text, around),
|
|
2187
|
+
speaker,
|
|
2188
|
+
});
|
|
2189
|
+
},
|
|
2190
|
+
failure: "a tavern turn failed",
|
|
2191
|
+
};
|
|
2192
|
+
},
|
|
2193
|
+
startFresh: async () => TAVERN,
|
|
2194
|
+
},
|
|
2195
|
+
],
|
|
2196
|
+
};
|
|
2197
|
+
},
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
```
|
|
2201
|
+
<!-- /example -->
|
|
2202
|
+
|
|
1870
2203
|
### `backgroundTargets`: whose turn a schedule or delegated task is
|
|
1871
2204
|
|
|
1872
2205
|
Schedules and delegated tasks return later as turns without a new message in the channel.
|
|
1873
2206
|
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.
|
|
1874
|
-
`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.
|
|
2207
|
+
`schedules` (`perChannel`, `promptChars`, `aheadDays`, and optionally `perPrincipal`) bounds what `schedule_create` accepts, and `delegation` (`maxRunning`, and optionally `maxRunningPerPrincipal`) bounds how many delegated tasks may run in one channel; a target without one of them may not schedule or delegate at all. `perPrincipal` and `maxRunningPerPrincipal` hold each person to that many of the target's schedules, or running tasks, across all their conversations. A schedule store counts a person's schedules for `perPrincipal` in `createWithin`, which must count and store as one step for that person, so creates at once in several conversations stay within the limit; the built-in store does it in one transaction under a lock on the creator.
|
|
1875
2208
|
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.
|
|
1876
2209
|
A channel's claim runs turns only for its own target, keeping the owner's tools out of channels open to many people.
|
|
1877
2210
|
|
|
2211
|
+
A turn is someone's: `turn.author.principalId` is the principal who set up the work, `turn.author.id` and `name` how they are shown, and `turn.tier` the tier it asks for, all required.
|
|
2212
|
+
The router checks the author as the turn starts, in its place in the channel's queue, and hands the claim `turn.speaker`, the speaker to run it as: the principal at the lower of `turn.tier` and their tier now.
|
|
2213
|
+
It skips the turn, with the reason, when the principal does not exist, is disabled, holds no tier, or holds one only from what a surface reports and was last seen more than `access.backgroundStaleDays` ago, and when the turn names no principal or tier; it never runs one as the owner's by default.
|
|
2214
|
+
A turn naming the system principal (`SYSTEM_PRINCIPAL`) runs only when the host itself started it.
|
|
2215
|
+
A schedule stores its creator's principal as `createdById` and runs as them; a delegated task's report runs as the person whose turn delegated it.
|
|
2216
|
+
In a turn nobody is named for, the schedule and delegation tools refuse.
|
|
2217
|
+
|
|
1878
2218
|
- Contribute `backgroundTargets` alongside the claim that serves them; read one with `context.conversations.target(name)` from a service or handler.
|
|
1879
2219
|
- Two targets with the same name are refused, naming both plugins.
|
|
1880
|
-
- The
|
|
1881
|
-
|
|
2220
|
+
- The core's `modules` plugin contributes `PERSONAL_TARGET` (name `"owner"`, exported from the main entry; `OWNER_TARGET` is a deprecated alias) on every host, for every person's conversations whose claim answers background turns, such as the agents' and an app's owner conversation.
|
|
2221
|
+
The agent server's claim answers that target and skips all others, so a turn for another target never runs with the owner's tools.
|
|
2222
|
+
Its limits are 0.8's, and it holds no one to a number of schedules or running tasks across their conversations unless the host's configuration says so: `background: { perPrincipal: { schedules: 20, delegations: 2 } }` sets its `perPrincipal` and `maxRunningPerPrincipal`. A host that serves many people sets both.
|
|
2223
|
+
- The built-in schedule and delegation tools make `PERSONAL_TARGET` work, and a session has them only where it can run: when the claim that owns its conversation has `background`, or, for a conversation no chat surface carries, when its creator has a direct channel, as a provider knows without the network.
|
|
2224
|
+
Such a conversation has the tools only when the registry records it private: its person is whom the work runs for. One recorded shared, or not recorded, has neither tool, whoever speaks in it, because a session outlives the turn it is built in.
|
|
2225
|
+
A plugin that calls the runtime directly records its no-surface conversation as private before the session is built.
|
|
2226
|
+
Creation reaches the speaker's own direct channel and checks that its claim takes background turns, refusing when either is absent; it never falls back to the primary owner's messages.
|
|
2227
|
+
In a private conversation, as the host's conversation registry records it, `schedule_list`, `schedule_update`, and `schedule_cancel` see only the speaker's own schedules.
|
|
1882
2228
|
- A turn for a target no plugin contributes is skipped with the reason `no plugin contributes the background target "<name>"`.
|
|
1883
2229
|
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.
|
|
1884
2230
|
A one-time schedule is spent when it fires.
|
|
@@ -1929,6 +2275,42 @@ export const supportDesk = definePlugin({
|
|
|
1929
2275
|
```
|
|
1930
2276
|
<!-- /example -->
|
|
1931
2277
|
|
|
2278
|
+
### `directChannels`: reaching a person on their own
|
|
2279
|
+
|
|
2280
|
+
A plugin whose chat network gives each person a conversation of their own, such as direct messages or a web inbox, contributes a `DirectChannelProvider` for it:
|
|
2281
|
+
|
|
2282
|
+
```ts
|
|
2283
|
+
setup: ({ services }) => ({
|
|
2284
|
+
directChannels: [
|
|
2285
|
+
{
|
|
2286
|
+
name: "inbox",
|
|
2287
|
+
label: "a notice in the web chat's inbox",
|
|
2288
|
+
reaches: async (principalId) => inboxOf(principalId),
|
|
2289
|
+
deliver: async (principalId, text) => postNotice(principalId, text),
|
|
2290
|
+
},
|
|
2291
|
+
],
|
|
2292
|
+
}),
|
|
2293
|
+
```
|
|
2294
|
+
|
|
2295
|
+
| Field | What it is |
|
|
2296
|
+
|---|---|
|
|
2297
|
+
| `name` | Unique across every plugin; a second provider of one name is a `PluginError` naming both plugins |
|
|
2298
|
+
| `label` | How the `notify` tool names a notice sent through it, completing "Send Ada …", such as `a direct message on Discord` |
|
|
2299
|
+
| `reaches(principalId)` | The person's own conversation here, as a channel key, or `undefined` when they have none here. It may reach the network and throw |
|
|
2300
|
+
| `knows(principalId)` | Optional: whether the person has a conversation here, from the host's own records such as their linked identities, without the network. A session asks it when it decides whether to offer `notify` or the schedule and delegation tools, so an outage then does not take them away; without it the host asks `reaches` |
|
|
2301
|
+
| `deliver(principalId, text)` | Optional: sends the notice itself. Without it the host posts the text in the channel `reaches` names, through the surface that serves it |
|
|
2302
|
+
|
|
2303
|
+
The host reaches a person through the first provider, in contribution order, that reaches them.
|
|
2304
|
+
`context.directChannels.reach(principalId)` resolves that provider and channel, or `undefined`; `known(principalId)` resolves the first provider that knows the person without the network; `notify(principalId, text)` sends a notice there and resolves `false` when no provider reaches the person.
|
|
2305
|
+
The Discord plugin contributes one named `discord`: it finds a principal's Discord identities through `IDENTITY` and reaches the first one's direct messages, the primary owner's configured identity first, so a single owner is reached in the same direct messages as in 0.8.
|
|
2306
|
+
|
|
2307
|
+
The `notify` tool, 0.8's `notify_owner`, sends such a notice. A session has it only when a provider is contributed, and a private conversation only when a provider knows its person; the channel itself is reached when a notice is sent.
|
|
2308
|
+
It notifies the conversation's person in a private conversation and the turn's speaker in a shared one, and refuses when no provider reaches them or when the speaker is not the private conversation's person; the host's own turns, such as a report's, notify the conversation's person in a private conversation and the primary owner elsewhere, as `notify_owner` did.
|
|
2309
|
+
Its description names the channels that can reach the person, so a host with only Discord reads `Send Ada a direct message on Discord. …` as 0.8 did.
|
|
2310
|
+
In a shared conversation it names the primary owner while only they can receive a notice (the host has no other owner, and only owners may use it), and "the speaker" otherwise; this is checked again before each turn, so an open session follows an owner granted or revoked meanwhile.
|
|
2311
|
+
Until 1.0 the name `notify_owner` still selects it in a selection or a profile and names it in a task's `exclude` and a plugin's `requiredTools`, and an operator's or a plugin's `toolTiers` entry for `notify_owner` applies to `notify` unless `notify` has its own.
|
|
2312
|
+
Using the old name logs a deprecation warning once per logger, whether in a selection or in `toolTiers`.
|
|
2313
|
+
|
|
1932
2314
|
### `surfaces`: a chat network of your own
|
|
1933
2315
|
|
|
1934
2316
|
A chat surface connects the host to one chat network: it reports what people write, and it posts what the conversations answer.
|
|
@@ -1949,19 +2331,38 @@ The agent server claims only `discord:` keys, so claims on your surface's channe
|
|
|
1949
2331
|
| `startTyping(channel)` | Shows a typing indicator until the returned function is called | none is shown |
|
|
1950
2332
|
| `showStop(channel)` | Shows the owner a stop control until the returned function is called; using it calls `conversations.stop(channel)` | none is shown |
|
|
1951
2333
|
| `react`, `unreact` | Adds or removes the bot's reaction on a message | no marks on queued or steered messages |
|
|
1952
|
-
| `prompts(channel,
|
|
2334
|
+
| `prompts(channel, scope?)` | How those the turn's `PromptScope` names approve a held action or answer `ask_user` inside a running turn, as `Prompts`: `confirm` and `ask`; without a scope, the owners | the action is held until a message approves it |
|
|
1953
2335
|
| `interim(channel)` | Where a running turn posts the text it writes before its final answer, as `InterimPosts`: `post(text)` sends one message of at most 2000 characters and resolves to an `InterimMessage` whose `edit(text)` changes it in place | only the final reply is posted |
|
|
1954
2336
|
| `progress(channel, event)` | Shows a turn run through `context.turns` as it goes, such as a live preview in a web chat. `event` is a `TurnProgress`: `{ type: "text", delta }` (the reply's text, joined over 250 ms and always sent before a tool event, never the thinking), `{ type: "tool_start", id, tool, preview? }` (a one-line preview of the arguments, at most 80 characters, never their full text), or `{ type: "tool_end", id, tool, ok }`. The final reply still comes through `sendReply`; a rejection is logged and the turn goes on | only the final reply is shown |
|
|
1955
2337
|
|
|
2338
|
+
Each message a surface delivers says who wrote it in `actor`, the `ActorFacts` the identity service resolves: `provider` and `subject`, such as `discord` and the user id, `name`, `roles` written `<surface>:role:<name>` (left out where the surface does not know them, such as a Discord DM, which is not the same as none), and `legacyId`, the speaker id 0.8 gave the person, so someone carried over keeps their principal.
|
|
2339
|
+
A surface that leaves `actor` out has it read from `authorId`, `authorName`, and `authorRoleIds` with its prefix as the provider, and the host warns once that this goes away in 1.0.
|
|
2340
|
+
|
|
1956
2341
|
The host starts each surface as `surface:<prefix>` at the contributing plugin's place in the order, before that plugin's own services.
|
|
1957
2342
|
It stops surfaces in reverse order, like other services.
|
|
1958
2343
|
The host logs and drops messages whose prefix differs from the delivering surface's, since claims can't identify which surface they belong to.
|
|
1959
2344
|
`context.surfaces` picks the surface by the key's prefix.
|
|
1960
2345
|
Its `sendReply` rejects with a `PluginError` naming the prefix when no surface serves it.
|
|
1961
2346
|
`startTyping`, `showStop`, `react`, and `unreact` do nothing when a channel has no surface or its surface omits those methods.
|
|
1962
|
-
If `prompts` is unavailable, it returns `undefined` and held actions wait for
|
|
2347
|
+
If `prompts` is unavailable, it returns `undefined` and held actions wait for a message that approves them.
|
|
1963
2348
|
`of(channel)` returns the surface, or `undefined`.
|
|
1964
|
-
The
|
|
2349
|
+
The runtime asks for approvals through `context.surfaces.prompts`, so a surface that gives `prompts` gets them in its own channels.
|
|
2350
|
+
|
|
2351
|
+
#### Who answers a prompt: `PromptScope`
|
|
2352
|
+
|
|
2353
|
+
The runtime makes each interactive turn's `PromptScope` from its speaker and its conversation: `principalId` and `speakerId` (the speaker's principal and the identity that spoke), `tier`, and `escalate`.
|
|
2354
|
+
A conversation `context.turns` records as `private` gives `escalate: "none"`; a shared one, and a turn that names no conversation, `"owners"`.
|
|
2355
|
+
A surface's prompts keep these rules:
|
|
2356
|
+
|
|
2357
|
+
- The speaker's principal answers their own question on any of their identities the surface knows, and approves a call when their tier is at least its `minTier` (the owner tier when absent) and, for an owner-tier call, the principal is an owner; a surface that can check their tier again when they answer does.
|
|
2358
|
+
- Where the scope escalates to the owners, every owner may answer too, and an approval above the speaker's tier is the owners' alone.
|
|
2359
|
+
- Where it escalates to no one, nobody else answers, and an approval above the speaker's tier resolves `expired` at once, shown to no one: the call stays held.
|
|
2360
|
+
- Anyone else is refused, even at the same tier as the speaker.
|
|
2361
|
+
- Without a scope, as for a turn with no speaker, the prompts are the owners'.
|
|
2362
|
+
|
|
2363
|
+
On Discord the owners are every owner with a Discord identity, and the primary owner always; a card in a thread mentions the identity that spoke when it is theirs and one of the principal's Discord identities, every Discord identity of the principal when the speaker is none of them, and the owners when it goes to them, so a single owner's cards read as they did in 0.8.
|
|
2364
|
+
A principal with no Discord identity, such as the reporter of a background report, answers nothing there, so their card goes to the owners; so does an owner-tier call of a turn whose tier defaulted to owner when its principal is not an owner.
|
|
2365
|
+
The web chat shows prompts to the conversation's person only: its owners are not on it, so an approval above that person's tier expires at once whatever the scope says.
|
|
1965
2366
|
|
|
1966
2367
|
#### Interim text: what a turn writes before its final answer
|
|
1967
2368
|
|
|
@@ -1987,15 +2388,26 @@ Its plugin adds a claim that answers through `context.surfaces`:
|
|
|
1987
2388
|
<!-- example: examples/fake-surface.ts -->
|
|
1988
2389
|
```ts
|
|
1989
2390
|
import {
|
|
2391
|
+
type Approval,
|
|
1990
2392
|
type ChannelKey,
|
|
1991
2393
|
type ChatSurface,
|
|
1992
2394
|
definePlugin,
|
|
1993
2395
|
type InboundMessage,
|
|
1994
2396
|
type OutboundReply,
|
|
1995
|
-
type
|
|
2397
|
+
type PromptScope,
|
|
2398
|
+
type Prompts,
|
|
1996
2399
|
parseChannelKey,
|
|
2400
|
+
TIERS,
|
|
1997
2401
|
} from "pi-roundtable";
|
|
1998
2402
|
|
|
2403
|
+
/** A prompt the surface showed, open until someone it is for answers it. */
|
|
2404
|
+
interface Asked {
|
|
2405
|
+
id: string;
|
|
2406
|
+
channel: ChannelKey;
|
|
2407
|
+
title: string;
|
|
2408
|
+
open: boolean;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
1999
2411
|
/**
|
|
2000
2412
|
* A chat surface connects the host to one chat network. This one is an in-memory chat: its
|
|
2001
2413
|
* channels are the keys that start with `fake:`, and it records what the host asks of it. A real
|
|
@@ -2007,7 +2419,12 @@ export class FakeSurface implements ChatSurface {
|
|
|
2007
2419
|
readonly replies: { channel: ChannelKey; reply: OutboundReply }[] = [];
|
|
2008
2420
|
readonly typing: string[] = [];
|
|
2009
2421
|
readonly stops: string[] = [];
|
|
2010
|
-
readonly asked:
|
|
2422
|
+
readonly asked: Asked[] = [];
|
|
2423
|
+
/** Who may answer each open prompt, and how it settles. */
|
|
2424
|
+
readonly #answering = new Map<
|
|
2425
|
+
string,
|
|
2426
|
+
{ principalId?: string; owners: boolean; settle(answer: Approval): void }
|
|
2427
|
+
>();
|
|
2011
2428
|
#deliver: ((message: InboundMessage) => void) | undefined;
|
|
2012
2429
|
|
|
2013
2430
|
/** The host hands over its router; every message the network reports goes to it. */
|
|
@@ -2049,16 +2466,66 @@ export class FakeSurface implements ChatSurface {
|
|
|
2049
2466
|
return () => void this.stops.push(`hide ${channel}`);
|
|
2050
2467
|
}
|
|
2051
2468
|
|
|
2052
|
-
/**
|
|
2053
|
-
|
|
2469
|
+
/**
|
|
2470
|
+
* How a turn asks to approve a held action or answers a question. The scope names who may
|
|
2471
|
+
* answer: its speaker when their tier holds the call, and the owners unless the conversation
|
|
2472
|
+
* is private, where a call above the speaker's tier goes to no one and expires at once.
|
|
2473
|
+
*/
|
|
2474
|
+
prompts(channel: ChannelKey, scope?: PromptScope): Prompts {
|
|
2054
2475
|
return {
|
|
2055
|
-
confirm: async (title) => {
|
|
2056
|
-
|
|
2057
|
-
|
|
2476
|
+
confirm: async (title, _message, signal, minTier = "owner") => {
|
|
2477
|
+
const theirs =
|
|
2478
|
+
scope !== undefined &&
|
|
2479
|
+
TIERS.indexOf(scope.tier) >= TIERS.indexOf(minTier);
|
|
2480
|
+
const owners = scope?.escalate !== "none";
|
|
2481
|
+
if (!theirs && !owners) return "expired";
|
|
2482
|
+
return this.#ask(channel, title, signal, {
|
|
2483
|
+
...(theirs ? { principalId: scope.principalId } : {}),
|
|
2484
|
+
owners,
|
|
2485
|
+
});
|
|
2058
2486
|
},
|
|
2059
2487
|
ask: async () => undefined,
|
|
2060
2488
|
};
|
|
2061
2489
|
}
|
|
2490
|
+
|
|
2491
|
+
/** Someone answers an open prompt; it settles only when it is theirs to answer. */
|
|
2492
|
+
answer(
|
|
2493
|
+
id: string,
|
|
2494
|
+
who: { principalId: string; owner?: boolean },
|
|
2495
|
+
approved: boolean,
|
|
2496
|
+
): void {
|
|
2497
|
+
const open = this.#answering.get(id);
|
|
2498
|
+
if (!open) return;
|
|
2499
|
+
const theirs = open.principalId === who.principalId;
|
|
2500
|
+
if (theirs || (who.owner && open.owners))
|
|
2501
|
+
open.settle(approved ? "approved" : "declined");
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
#ask(
|
|
2505
|
+
channel: ChannelKey,
|
|
2506
|
+
title: string,
|
|
2507
|
+
signal: AbortSignal | undefined,
|
|
2508
|
+
audience: { principalId?: string; owners: boolean },
|
|
2509
|
+
): Promise<Approval> {
|
|
2510
|
+
const asked = {
|
|
2511
|
+
id: `q${this.asked.length + 1}`,
|
|
2512
|
+
channel,
|
|
2513
|
+
title,
|
|
2514
|
+
open: true,
|
|
2515
|
+
};
|
|
2516
|
+
this.asked.push(asked);
|
|
2517
|
+
return new Promise((resolve) => {
|
|
2518
|
+
const settle = (answer: Approval) => {
|
|
2519
|
+
this.#answering.delete(asked.id);
|
|
2520
|
+
asked.open = false;
|
|
2521
|
+
resolve(answer);
|
|
2522
|
+
};
|
|
2523
|
+
this.#answering.set(asked.id, { ...audience, settle });
|
|
2524
|
+
signal?.addEventListener("abort", () => settle("cancelled"), {
|
|
2525
|
+
once: true,
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2062
2529
|
}
|
|
2063
2530
|
|
|
2064
2531
|
/**
|
|
@@ -2150,10 +2617,9 @@ The runtime plugin still builds the runtime, so every claim that runs turns thro
|
|
|
2150
2617
|
|
|
2151
2618
|
What needs Discord is refused or left out rather than failing later:
|
|
2152
2619
|
|
|
2153
|
-
- `agents`, `skills` (anything but `false`), and `ops.agent` are configuration errors. `ops: { conversation: "<surface>:<id>" }` needs a chat surface that serves it
|
|
2154
|
-
- `
|
|
2155
|
-
- `schedule_*` and `delegate_task` are
|
|
2156
|
-
A plugin that contributes `owner`, and a claim that takes its background turns, bring them back; they then work in a conversation a chat surface carries, posting their runs there, and refuse elsewhere.
|
|
2620
|
+
- `agents`, `skills` (anything but `false`), and `ops.agent` are configuration errors. `ops: { conversation: "<surface>:<id>" }` needs a chat surface that serves it and a claim that owns it and takes background turns, or the host does not start. Webchat declares `takesSystemReports: false` and takes no system error reports.
|
|
2621
|
+
- `notify` is not registered unless a plugin contributes a [direct channel](#directchannels-reaching-a-person-on-their-own), since there is nowhere to send a notice.
|
|
2622
|
+
- `schedule_*` and `delegate_task` are registered only in a conversation whose claim takes background turns: their runs are turns of the [background target](#backgroundtargets-whose-turn-a-schedule-or-delegated-task-is) `PERSONAL_TARGET`, named `owner`, which the core contributes on every host. A conversation of a claim without `background` has neither; a no-surface conversation needs a registered private principal known to a direct channel, as [background targets](#backgroundtargets-whose-turn-a-schedule-or-delegated-task-is) describes.
|
|
2157
2623
|
- `roundtable doctor` skips the Discord checks.
|
|
2158
2624
|
|
|
2159
2625
|
Pi's runtime still needs the `compact_session` tool in every session, from the Pi package pi-self-compact: load it from a plugin with `piPackages: ["pi-self-compact"]`, as the `plugins/self-compact.ts` of a project `roundtable init` creates does, or the preflight stops the start and says so.
|
|
@@ -2186,7 +2652,11 @@ export function studyHall(
|
|
|
2186
2652
|
): RoundtableConfig {
|
|
2187
2653
|
return {
|
|
2188
2654
|
name: "Study Hall",
|
|
2189
|
-
|
|
2655
|
+
access: {
|
|
2656
|
+
owners: [{ principal: "owner", name: "Ada" }],
|
|
2657
|
+
// Admit the fake surface's authors explicitly, not in the claim.
|
|
2658
|
+
members: { everyone: ["fake"] },
|
|
2659
|
+
},
|
|
2190
2660
|
database: { url: where.databaseUrl },
|
|
2191
2661
|
dataDir: where.dataDir,
|
|
2192
2662
|
model: "anthropic/claude-sonnet-5-5",
|
|
@@ -2202,13 +2672,13 @@ export function studyHall(
|
|
|
2202
2672
|
### Web chat: pi-roundtable-webchat
|
|
2203
2673
|
|
|
2204
2674
|
[pi-roundtable-webchat][webchat-package] is a chat network that comes as a plugin.
|
|
2205
|
-
`webChat({ verifier,
|
|
2675
|
+
`webChat({ verifier, personas, origins })` adds a chat surface whose conversations have keys `web:<conversation>`, the claim that runs each message as a turn of its conversation's persona through `context.turns`, and a REST API and a WebSocket under `/chat` on the host's `public` listener.
|
|
2206
2676
|
People an OpenID Connect provider signs in open private conversations, see each turn's text and tools as it runs, and answer its approval cards.
|
|
2207
2677
|
It needs no Discord: a host whose configuration has no `discord` and lists `webChat(...)` in `plugins` is a web-only assistant.
|
|
2208
2678
|
|
|
2209
2679
|
#### `roundtable init --adapter web`
|
|
2210
2680
|
|
|
2211
|
-
`roundtable init --adapter web` writes a project without Discord around it: no `agents.ts` and no shared persona, a `roundtable.config.ts` that lists `webChat(...)` with an `oidcJwtVerifier`, an access map whose members and admins come from the token's roles, and one persona, `assistant`, whose prompt is `persona/assistant.md`.
|
|
2681
|
+
`roundtable init --adapter web` writes a project without Discord around it: no `agents.ts` and no shared persona, a `roundtable.config.ts` that lists `webChat(...)` with an `oidcJwtVerifier`, an access map whose members and admins come from the token's roles, in top-level `access`, and one persona, `assistant`, whose prompt is `persona/assistant.md`.
|
|
2212
2682
|
The persona's `selection` names its tools (the hello plugin's), so tools a later plugin adds, such as pi-web-access's, stay out until you add them by name.
|
|
2213
2683
|
`package.json` pins pi-roundtable-webchat at the core's version and pi-self-compact, and `.env.example` asks for `OWNER_NAME`, `DATABASE_URL`, `MODEL`, `OIDC_ISSUER`, `OIDC_AUDIENCE`, `OIDC_JWKS_URL`, `CHAT_MEMBER_ROLES`, and `CHAT_ORIGINS`, with no Discord variable.
|
|
2214
2684
|
The host listens on `127.0.0.1:3000`; serve it over HTTPS through a reverse proxy.
|
|
@@ -2218,9 +2688,10 @@ The host listens on `127.0.0.1:3000`; serve it over HTTPS through a reverse prox
|
|
|
2218
2688
|
|
|
2219
2689
|
A browser asks for a one-time ticket with `POST /chat/tickets` and `Authorization: Bearer <token>`, then opens `/chat/socket` offering the subprotocols `roundtable.webchat.v1` and `ticket.<ticket>`; another client sends the bearer token on the upgrade.
|
|
2220
2690
|
The token never travels in a URL.
|
|
2221
|
-
Each WebSocket message is one JSON object with a `type`: the client sends `send`, `stop`, `approval`, `answer`, and `auth`; the server answers with `ready`, `accepted`, `typing`, `stoppable`, `progress`, `reply`, `failed`, `prompt`, `prompt_closed`, `reauth`, and `error`.
|
|
2691
|
+
Each WebSocket message is one JSON object with a `type`: the client sends `send`, `stop`, `approval`, `answer`, and `auth`; the server answers with `ready`, `accepted`, `typing`, `stoppable`, `progress`, `reply`, `failed`, `prompt`, `prompt_closed`, `notice`, `reauth`, and `error`.
|
|
2222
2692
|
`progress` carries the turn's [`TurnProgress`](#surfaces-a-chat-network-of-your-own) events, never the thinking and never a tool's full arguments; `reply` carries the answer in full markdown.
|
|
2223
|
-
The REST API lists a person's conversations, opens one,
|
|
2693
|
+
The REST API lists a person's conversations, opens one, reads its messages, and lists and acknowledges private inbox notices from `notify`.
|
|
2694
|
+
Tickets, quotas, connections, and approvals follow `principalId`; refreshing a token to another principal closes the socket with 4403.
|
|
2224
2695
|
The package README documents every frame, error code, close code, and limit.
|
|
2225
2696
|
|
|
2226
2697
|
#### Security model
|
|
@@ -2230,14 +2701,17 @@ The package README documents every frame, error code, close code, and limit.
|
|
|
2230
2701
|
- `origins` is required, and checked on every upgrade and every browser request.
|
|
2231
2702
|
- Each person has bounded sockets, tickets, conversations opened per hour, and turns running or queued at once; frames are bounded in size and rate.
|
|
2232
2703
|
- An approval card goes to the conversation's person only, at the tier the held call needs.
|
|
2233
|
-
- No token claim makes anyone the owner; owners
|
|
2704
|
+
- No token claim makes anyone the owner; list their linked identity in top-level `access.owners` or grant a lasting role through the CLI.
|
|
2705
|
+
- The old webchat `access` option is removed; prefix roles as `<surface>:role:<name>` and scope old `everyone: true` to `everyone: ["<surface>"]`.
|
|
2234
2706
|
|
|
2235
2707
|
#### Provider settings
|
|
2236
2708
|
|
|
2237
2709
|
The verifier is generic; what makes it safe is how it is pointed at your provider.
|
|
2238
2710
|
The README's [provider settings][webchat-provider-settings] explain, with Microsoft Entra ID as the example, why to name people by a claim that stays the same across app registrations (`subjectClaim`), pin the tenant in `check`, accept access tokens only, refuse app-only tokens, merge issuers of one tenant only (`speakerIssuer`), and mind the guests `everyone` admits.
|
|
2239
2711
|
|
|
2240
|
-
The web chat takes no attachments
|
|
2712
|
+
The web chat takes no attachments or system error reports (`takesSystemReports: false`), so `ops.conversation` cannot name it.
|
|
2713
|
+
It accepts `PERSONAL_TARGET` background turns only for the private conversation's principal; include `schedule_*`, `delegate_task`, and `notify` in a persona's selection only deliberately, adjusting their tiers if needed.
|
|
2714
|
+
Leave out `web_search` and `fetch_content` unless people may make the host fetch any address, internal ones included.
|
|
2241
2715
|
|
|
2242
2716
|
[webchat-package]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/webchat/README.md
|
|
2243
2717
|
[webchat-provider-settings]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/webchat/README.md#provider-settings
|
|
@@ -2352,6 +2826,8 @@ Use `testPlugin(plugin, options?)` for most tests; it sets up one plugin against
|
|
|
2352
2826
|
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.
|
|
2353
2827
|
|
|
2354
2828
|
`testPlugin` sets one plugin up against a fake context and starts its services, with no Discord and no PostgreSQL unless you pass `{ database }`.
|
|
2829
|
+
It resolves only its default owner unless you inject `servicePair(IDENTITY, { resolve })` for your test's actors; do not invent a speaker in a claim to bypass admission.
|
|
2830
|
+
The [principals example](#principals-and-access) injects only the read method it exercises, and the study-room example tests router-resolved attribution and refusal.
|
|
2355
2831
|
It returns:
|
|
2356
2832
|
|
|
2357
2833
|
| Field | What it is |
|
|
@@ -2411,7 +2887,12 @@ import { notes } from "./tools.ts";
|
|
|
2411
2887
|
|
|
2412
2888
|
test("note_add saves a note for the speaker and refuses an empty one", async () => {
|
|
2413
2889
|
const harness = await testPlugin(notes);
|
|
2414
|
-
const ada: Speaker = {
|
|
2890
|
+
const ada: Speaker = {
|
|
2891
|
+
id: "1",
|
|
2892
|
+
name: "Ada",
|
|
2893
|
+
tier: "member",
|
|
2894
|
+
principalId: "1",
|
|
2895
|
+
};
|
|
2415
2896
|
expect(harness.tools).toEqual(["note_add"]);
|
|
2416
2897
|
expect(harness.tiers.minTier("note_add")).toBe("member");
|
|
2417
2898
|
expect(
|
|
@@ -2443,7 +2924,7 @@ Use `partial<Port>({ ... })` to stand in for a port your code takes as an argume
|
|
|
2443
2924
|
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.
|
|
2444
2925
|
`fakePrecheck(name, answer, { description?, timeoutMs? })` is a precheck that answers `answer` (a `PrecheckResult`, an `Error` it throws, or a function of its context) and records each context in `calls`; `fakePrechecks(...prechecks)` is a real in-memory `PrecheckRegistry` with them registered, which a test gives a plugin as `servicePair(PRECHECKS, registry)` and reads back with `registry.get(name)`.
|
|
2445
2926
|
`fakeScriptRunner(answer, { describe?, timeoutMs? })` is a `PrecheckScriptRunner` that runs nothing and answers `answer` (a result, an `Error` it throws, or a function of the script and its context), recording each call in `calls`; give it to `registry.useScriptRunner`.
|
|
2446
|
-
`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()
|
|
2927
|
+
`fakeDiscord({ ownerId?, rootCommand?, channelContext? })` is the `DISCORD` service for a plugin that adds slash commands or reads channel context: give it as `services: [discord.service]`, read what the plugin added with `discord.added()`, and compose the tree Discord would get with `discord.compose()`; `channelContext` answers `DISCORD.channelContext`, by default with no context.
|
|
2447
2928
|
Only `commands` and `guard` are given; a plugin that reads another member of `DISCORD` in a test gives its own with `servicePair(DISCORD, { ... })`.
|
|
2448
2929
|
|
|
2449
2930
|
### `testHost`: the built-in plugins and yours, over PostgreSQL
|
|
@@ -2480,9 +2961,9 @@ It returns:
|
|
|
2480
2961
|
### `describeSurfaceContract`: what every chat surface keeps
|
|
2481
2962
|
|
|
2482
2963
|
A plugin that contributes a `ChatSurface` can test it against the contract every surface keeps.
|
|
2483
|
-
`describeSurfaceContract(name, make)` registers one test per check, each on a fresh `SurfaceContractSubject` from `make`: the `surface`, a `channel` of it, and one person on its network, who can `write(text)`, whose `observations()` list what they have seen (`SurfaceObservation`: a reply's text and file names, typing and stop controls going on and off, progress, and prompts opening and closing), and who may `answer(prompt, approved)` an approval; `speaker` is who they are to `surface.prompts`, an owner by default, and every approval the contract asks for needs exactly their tier. `stranger`, when given, is someone else on the network, not
|
|
2964
|
+
`describeSurfaceContract(name, make)` registers one test per check, each on a fresh `SurfaceContractSubject` from `make`: the `surface`, a `channel` of it, and one person on its network, who can `write(text)`, whose `observations()` list what they have seen (`SurfaceObservation`: a reply's text and file names, typing and stop controls going on and off, progress, and prompts opening and closing), and who may `answer(prompt, approved)` an approval; `speaker` is who they are to `surface.prompts`, given as `promptScope(speaker, visibility)`, an owner by default, and every approval the contract asks for needs exactly their tier. `stranger`, when given, is someone else on the network, another principal and not an owner, who knows the id of the person's open approval and may `join()` and `answer(prompt, approved)` it; `owner`, when given, is an owner on the network, another principal than the person, who does the same in the person's private conversation. Both are `SurfaceContractAnswerer`s.
|
|
2484
2965
|
The contract starts the surface, calls the subject's `join()`, and stops the surface after the subject's `close()`.
|
|
2485
|
-
It checks that the surface names a prefix and owns its channel, delivers what the person writes, and shows every chunk of a reply; and, for the parts the surface offers, that it delivers files when `supportsFiles` is set, ends typing and stop controls idempotently, shows progress, and resolves an approval as the person approves or declines it, or `cancelled` when the turn stops,
|
|
2966
|
+
It checks that the surface names a prefix and owns its channel, delivers what the person writes, and shows every chunk of a reply; and, for the parts the surface offers, that it delivers files when `supportsFiles` is set, ends typing and stop controls idempotently, shows progress, and resolves an approval as the person approves or declines it, or `cancelled` when the turn stops, with a `stranger`, that the stranger's answer leaves the person's approval open, and with an `owner`, that an owner's answer leaves it open in a private conversation; and, for a speaker below the owner tier, that a private conversation's approval above their tier resolves `expired` at once and is not shown.
|
|
2486
2967
|
A check for a part the surface does not offer passes.
|
|
2487
2968
|
`checkSurfaceContract(make)` runs the same checks and returns the ones that failed, as `SurfaceContractFailure`s.
|
|
2488
2969
|
|
|
@@ -2544,7 +3025,7 @@ These are the messages as the code writes them, with `<...>` where your names go
|
|
|
2544
3025
|
| `plugin <name>: setup is missing. Give the function that returns what the plugin adds.` | Add `setup` |
|
|
2545
3026
|
| `plugin <name> adds nothing. Give it a part (tools, services, channels, and so on), a migration, or a provider, or remove it.` | Return a part from `setup`, or remove the plugin from `roundtable.config.ts` |
|
|
2546
3027
|
| `plugin <name>: setup must return an object of the parts it adds; return {} to add none.` | Return an object, not `undefined` |
|
|
2547
|
-
| `plugin <name>: setup returned an unknown part "<key>". Did you mean "<closest>"? The parts are services, events, http, holdRules, piPackages, sessionTools, channels, surfaces, personas, backgroundTargets, dashboard, tools, seeds, prompt, agentSelection, requiredTools.` | Fix the key; `migrations`, `providers`, and `preflight` belong on the plugin, not in what `setup` returns |
|
|
3028
|
+
| `plugin <name>: setup returned an unknown part "<key>". Did you mean "<closest>"? The parts are services, events, http, holdRules, piPackages, sessionTools, channels, surfaces, personas, backgroundTargets, directChannels, dashboard, tools, seeds, prompt, agentSelection, requiredTools.` | Fix the key; `migrations`, `providers`, and `preflight` belong on the plugin, not in what `setup` returns |
|
|
2548
3029
|
|
|
2549
3030
|
### Tools
|
|
2550
3031
|
|
|
@@ -2707,6 +3188,13 @@ Import from the entries listed below; source area files are internal.
|
|
|
2707
3188
|
|
|
2708
3189
|
| Name | Entry | Kind |
|
|
2709
3190
|
|---|---|---|
|
|
3191
|
+
| `AccessConfig` | `pi-roundtable` | type |
|
|
3192
|
+
| `AccessOwner` | `pi-roundtable` | type |
|
|
3193
|
+
| `AccessOwnerConfig` | `pi-roundtable` | type |
|
|
3194
|
+
| `AccessRules` | `pi-roundtable` | type |
|
|
3195
|
+
| `AccessTier` | `pi-roundtable` | type |
|
|
3196
|
+
| `AccessTierConfig` | `pi-roundtable` | type |
|
|
3197
|
+
| `ActorFacts` | `pi-roundtable` | type |
|
|
2710
3198
|
| `AGENTS` | `pi-roundtable` | value |
|
|
2711
3199
|
| `AGENT_SERVER_PLUGIN` | `pi-roundtable` | value |
|
|
2712
3200
|
| `AGENT_SERVER_PRIORITY` | `pi-roundtable` | value |
|
|
@@ -2733,6 +3221,8 @@ Import from the entries listed below; source area files are internal.
|
|
|
2733
3221
|
| `AvatarStudio` | `pi-roundtable` | type |
|
|
2734
3222
|
| `BACKGROUND_TURNS` | `pi-roundtable` | value |
|
|
2735
3223
|
| `CONVERSATIONS` | `pi-roundtable` | value |
|
|
3224
|
+
| `BackgroundConfig` | `pi-roundtable` | type |
|
|
3225
|
+
| `BackgroundRunsAs` | `pi-roundtable` | type |
|
|
2736
3226
|
| `BackgroundTarget` | `pi-roundtable` | type |
|
|
2737
3227
|
| `BackgroundTurn` | `pi-roundtable` | type |
|
|
2738
3228
|
| `BackgroundTurns` | `pi-roundtable` | type |
|
|
@@ -2752,6 +3242,9 @@ Import from the entries listed below; source area files are internal.
|
|
|
2752
3242
|
| `ConversationTurns` | `pi-roundtable` | type |
|
|
2753
3243
|
| `DELEGATION` | `pi-roundtable` | value |
|
|
2754
3244
|
| `DefineOverrides` | `pi-roundtable` | type |
|
|
3245
|
+
| `DirectChannelProvider` | `pi-roundtable` | type |
|
|
3246
|
+
| `DirectChannels` | `pi-roundtable` | type |
|
|
3247
|
+
| `DirectReach` | `pi-roundtable` | type |
|
|
2755
3248
|
| `DefinedRoundtable` | `pi-roundtable` | type |
|
|
2756
3249
|
| `DelegationError` | `pi-roundtable` | value |
|
|
2757
3250
|
| `DelegationJob` | `pi-roundtable` | type |
|
|
@@ -2771,6 +3264,11 @@ Import from the entries listed below; source area files are internal.
|
|
|
2771
3264
|
| `HostEnv` | `pi-roundtable` | type |
|
|
2772
3265
|
| `HostEnvironment` | `pi-roundtable` | type |
|
|
2773
3266
|
| `HttpRoute` | `pi-roundtable` | type |
|
|
3267
|
+
| `IDENTITY` | `pi-roundtable` | value |
|
|
3268
|
+
| `IdentityError` | `pi-roundtable` | value |
|
|
3269
|
+
| `IdentityLink` | `pi-roundtable` | type |
|
|
3270
|
+
| `IdentityRef` | `pi-roundtable` | type |
|
|
3271
|
+
| `IdentityService` | `pi-roundtable` | type |
|
|
2774
3272
|
| `ImageDrawer` | `pi-roundtable` | type |
|
|
2775
3273
|
| `InboundMessage` | `pi-roundtable` | type |
|
|
2776
3274
|
| `InterimMessage` | `pi-roundtable` | type |
|
|
@@ -2780,6 +3278,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2780
3278
|
| `JudgeError` | `pi-roundtable` | value |
|
|
2781
3279
|
| `JudgeModel` | `pi-roundtable` | type |
|
|
2782
3280
|
| `LinkedSessions` | `pi-roundtable` | type |
|
|
3281
|
+
| `LinkSource` | `pi-roundtable` | type |
|
|
2783
3282
|
| `ListenerAddress` | `pi-roundtable` | type |
|
|
2784
3283
|
| `ListenerConfig` | `pi-roundtable` | type |
|
|
2785
3284
|
| `LoadedSkill` | `pi-roundtable` | type |
|
|
@@ -2801,17 +3300,23 @@ Import from the entries listed below; source area files are internal.
|
|
|
2801
3300
|
| `NewSchedule` | `pi-roundtable` | type |
|
|
2802
3301
|
| `NotLinkedError` | `pi-roundtable` | value |
|
|
2803
3302
|
| `OWNER_TARGET` | `pi-roundtable` | value |
|
|
3303
|
+
| `PERSONAL_TARGET` | `pi-roundtable` | value |
|
|
2804
3304
|
| `OutboundReply` | `pi-roundtable` | type |
|
|
2805
3305
|
| `OwnerAnswer` | `pi-roundtable` | type |
|
|
2806
3306
|
| `OwnerPrompts` | `pi-roundtable` | type |
|
|
2807
3307
|
| `OwnerQuestion` | `pi-roundtable` | type |
|
|
2808
3308
|
| `PendingConfirmation` | `pi-roundtable` | type |
|
|
3309
|
+
| `PerPrincipalLimits` | `pi-roundtable` | type |
|
|
2809
3310
|
| `Persona` | `pi-roundtable` | type |
|
|
2810
3311
|
| `PluginContext` | `pi-roundtable` | type |
|
|
3312
|
+
| `PluginIdentity` | `pi-roundtable` | type |
|
|
2811
3313
|
| `PluginError` | `pi-roundtable` | value |
|
|
3314
|
+
| `Principal` | `pi-roundtable` | type |
|
|
2812
3315
|
| `PromptMemory` | `pi-roundtable` | type |
|
|
3316
|
+
| `PromptScope` | `pi-roundtable` | type |
|
|
2813
3317
|
| `PromptSection` | `pi-roundtable` | type |
|
|
2814
3318
|
| `PromptTurn` | `pi-roundtable` | type |
|
|
3319
|
+
| `Prompts` | `pi-roundtable` | type |
|
|
2815
3320
|
| `Pronouns` | `pi-roundtable` | type |
|
|
2816
3321
|
| `ProviderError` | `pi-roundtable` | value |
|
|
2817
3322
|
| `Providers` | `pi-roundtable` | type |
|
|
@@ -2820,6 +3325,8 @@ Import from the entries listed below; source area files are internal.
|
|
|
2820
3325
|
| `ReferenceImage` | `pi-roundtable` | type |
|
|
2821
3326
|
| `ResolvedProviders` | `pi-roundtable` | type |
|
|
2822
3327
|
| `ResolvedSkill` | `pi-roundtable` | type |
|
|
3328
|
+
| `RoleGrant` | `pi-roundtable` | type |
|
|
3329
|
+
| `RoleSource` | `pi-roundtable` | type |
|
|
2823
3330
|
| `Roundtable` | `pi-roundtable` | value |
|
|
2824
3331
|
| `ReplyFile` | `pi-roundtable` | type |
|
|
2825
3332
|
| `ReplyFileError` | `pi-roundtable` | value |
|
|
@@ -2856,6 +3363,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2856
3363
|
| `ServiceStartedEvent` | `pi-roundtable` | type |
|
|
2857
3364
|
| `Services` | `pi-roundtable` | type |
|
|
2858
3365
|
| `SessionContext` | `pi-roundtable` | type |
|
|
3366
|
+
| `SessionConversation` | `pi-roundtable` | type |
|
|
2859
3367
|
| `SessionPlan` | `pi-roundtable` | type |
|
|
2860
3368
|
| `SessionTool` | `pi-roundtable` | type |
|
|
2861
3369
|
| `SessionToolSnapshot` | `pi-roundtable` | type |
|
|
@@ -2867,6 +3375,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2867
3375
|
| `SpeakerMemory` | `pi-roundtable` | type |
|
|
2868
3376
|
| `StoredAttachment` | `pi-roundtable` | type |
|
|
2869
3377
|
| `SurfacePort` | `pi-roundtable` | type |
|
|
3378
|
+
| `SYSTEM_PRINCIPAL` | `pi-roundtable` | value |
|
|
2870
3379
|
| `THE_SPEAKER` | `pi-roundtable` | value |
|
|
2871
3380
|
| `TIERS` | `pi-roundtable` | value |
|
|
2872
3381
|
| `TeamAgentStatus` | `pi-roundtable` | type |
|
|
@@ -2885,6 +3394,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2885
3394
|
| `TranscriptEntry` | `pi-roundtable` | type |
|
|
2886
3395
|
| `TransientTask` | `pi-roundtable` | type |
|
|
2887
3396
|
| `TurnAttachments` | `pi-roundtable` | type |
|
|
3397
|
+
| `TurnConversation` | `pi-roundtable` | type |
|
|
2888
3398
|
| `TurnEndEvent` | `pi-roundtable` | type |
|
|
2889
3399
|
| `TurnProgress` | `pi-roundtable` | type |
|
|
2890
3400
|
| `TurnProgressEvent` | `pi-roundtable` | type |
|
|
@@ -2897,6 +3407,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2897
3407
|
| `WebSocketSendResult` | `pi-roundtable` | type |
|
|
2898
3408
|
| `Weekday` | `pi-roundtable` | type |
|
|
2899
3409
|
| `attachReplyFile` | `pi-roundtable` | value |
|
|
3410
|
+
| `promptScope` | `pi-roundtable` | value |
|
|
2900
3411
|
| `withReplyFiles` | `pi-roundtable` | value |
|
|
2901
3412
|
| `prepareImageBytes` | `pi-roundtable/kit` | value |
|
|
2902
3413
|
| `ImagePreparationError` | `pi-roundtable/kit` | value |
|
|
@@ -2913,6 +3424,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2913
3424
|
| `OWNER_SPEAKER` | `pi-roundtable/testing` | value |
|
|
2914
3425
|
| `RecordedEvent` | `pi-roundtable/testing` | type |
|
|
2915
3426
|
| `ServicePair` | `pi-roundtable/testing` | type |
|
|
3427
|
+
| `SurfaceContractAnswerer` | `pi-roundtable/testing` | type |
|
|
2916
3428
|
| `TEST_GUILD` | `pi-roundtable/testing` | value |
|
|
2917
3429
|
| `TestHost` | `pi-roundtable/testing` | type |
|
|
2918
3430
|
| `TestHostOptions` | `pi-roundtable/testing` | type |
|
|
@@ -2980,6 +3492,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2980
3492
|
| `McpEndpoint` | `pi-roundtable/kit` | type |
|
|
2981
3493
|
| `ModelRef` | `pi-roundtable/kit` | type |
|
|
2982
3494
|
| `OwnerIdentity` | `pi-roundtable/kit` | type |
|
|
3495
|
+
| `Notifier` | `pi-roundtable/kit` | type |
|
|
2983
3496
|
| `OwnerNotifier` | `pi-roundtable/kit` | type |
|
|
2984
3497
|
| `PreviousTurn` | `pi-roundtable/kit` | type |
|
|
2985
3498
|
| `PromptSlot` | `pi-roundtable/kit` | type |
|
|
@@ -3022,6 +3535,15 @@ Import from the entries listed below; source area files are internal.
|
|
|
3022
3535
|
| `compactionEngine` | `pi-roundtable/kit` | value |
|
|
3023
3536
|
| `HARD_COMPACT_TOKENS` | `pi-roundtable/kit` | value |
|
|
3024
3537
|
| `SOFT_COMPACT_TOKENS` | `pi-roundtable/kit` | value |
|
|
3538
|
+
| `summaryProjection` | `pi-roundtable/kit` | value |
|
|
3539
|
+
| `privateCompaction` | `pi-roundtable/kit` | value |
|
|
3540
|
+
| `memoryProjection` | `pi-roundtable/kit` | value |
|
|
3541
|
+
| `hidesPrivateExchange` | `pi-roundtable/kit` | value |
|
|
3542
|
+
| `MemoryView` | `pi-roundtable/kit` | type |
|
|
3543
|
+
| `MEMORY_TURN_ENTRY` | `pi-roundtable/kit` | value |
|
|
3544
|
+
| `recordMemoryTurn` | `pi-roundtable/kit` | value |
|
|
3545
|
+
| `carriesMemory` | `pi-roundtable/kit` | value |
|
|
3546
|
+
| `bridgeHistoryHidesMemory` | `pi-roundtable/kit` | value |
|
|
3025
3547
|
| `CompactionEngine` | `pi-roundtable/kit` | type |
|
|
3026
3548
|
| `CompactionHistory` | `pi-roundtable/kit` | type |
|
|
3027
3549
|
| `LatestCompaction` | `pi-roundtable/kit` | type |
|
|
@@ -3072,8 +3594,12 @@ Import from the entries listed below; source area files are internal.
|
|
|
3072
3594
|
| `AgentPanel` | `pi-roundtable/discord` | type |
|
|
3073
3595
|
| `AgentPanelMessage` | `pi-roundtable/discord` | type |
|
|
3074
3596
|
| `AgentPanelOptions` | `pi-roundtable/discord` | type |
|
|
3597
|
+
| `CHANNEL_CONTEXT_DEFAULTS` | `pi-roundtable/discord` | value |
|
|
3075
3598
|
| `CHANNEL_OPERATIONS` | `pi-roundtable/discord` | value |
|
|
3076
3599
|
| `CHANNEL_TOOLS` | `pi-roundtable/discord` | value |
|
|
3600
|
+
| `ChannelContext` | `pi-roundtable/discord` | type |
|
|
3601
|
+
| `ChannelContextMessage` | `pi-roundtable/discord` | type |
|
|
3602
|
+
| `ChannelContextOptions` | `pi-roundtable/discord` | type |
|
|
3077
3603
|
| `ChannelExecutor` | `pi-roundtable/discord` | type |
|
|
3078
3604
|
| `ChannelInfo` | `pi-roundtable/discord` | type |
|
|
3079
3605
|
| `ChannelOperation` | `pi-roundtable/discord` | type |
|
|
@@ -3086,6 +3612,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
3086
3612
|
| `ComposedCommands` | `pi-roundtable/discord` | type |
|
|
3087
3613
|
| `DISCORD` | `pi-roundtable/discord` | value |
|
|
3088
3614
|
| `DISCORD_ADMIN_TOOLS` | `pi-roundtable/discord` | value |
|
|
3615
|
+
| `DiscordActor` | `pi-roundtable/discord` | type |
|
|
3089
3616
|
| `DiscordAdapterConfig` | `pi-roundtable/discord` | type |
|
|
3090
3617
|
| `DiscordConnection` | `pi-roundtable/discord` | type |
|
|
3091
3618
|
| `DiscordServices` | `pi-roundtable/discord` | type |
|
|
@@ -3114,3 +3641,4 @@ Import from the entries listed below; source area files are internal.
|
|
|
3114
3641
|
| `parseChannelTool` | `pi-roundtable/discord` | value |
|
|
3115
3642
|
| `plain` | `pi-roundtable/discord` | value |
|
|
3116
3643
|
| `replyWithPanels` | `pi-roundtable/discord` | value |
|
|
3644
|
+
| `withChannelContext` | `pi-roundtable/discord` | value |
|