pi-roundtable 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/docs/plugins.md +54 -5
  3. package/package.json +1 -1
  4. package/src/core/agents/team-turns.ts +2 -7
  5. package/src/core/builtin/skills.ts +11 -0
  6. package/src/core/builtin/worker-sessions.ts +44 -0
  7. package/src/core/config/config.ts +4 -6
  8. package/src/core/config/skills-config.ts +23 -0
  9. package/src/core/define-roundtable.ts +8 -0
  10. package/src/core/discord/owner-card-rows.ts +7 -3
  11. package/src/core/discord/owner-card-types.ts +4 -0
  12. package/src/core/discord/owner-cards.ts +10 -3
  13. package/src/core/domain/turn-timeout.ts +38 -0
  14. package/src/core/i18n/en.ts +2 -0
  15. package/src/core/i18n/turn-timeout.ts +131 -0
  16. package/src/core/i18n/zh-tw.ts +2 -0
  17. package/src/core/interactions/prompts.ts +17 -0
  18. package/src/core/modules/delegation/web-research-worker.ts +10 -38
  19. package/src/core/modules/delegation/worker-session.ts +111 -0
  20. package/src/core/modules/skills/package-skills-fixture.ts +33 -0
  21. package/src/core/modules/skills/package-skills.ts +121 -0
  22. package/src/core/modules/skills/skill-listing.ts +11 -0
  23. package/src/core/modules/skills/skill-registry.ts +47 -3
  24. package/src/core/modules/skills/skill-rules.ts +13 -1
  25. package/src/core/presentation/failure-notice.ts +13 -0
  26. package/src/core/routing/conversation-turns.ts +2 -7
  27. package/src/core/runtime/pi-agent-runtime.ts +10 -10
  28. package/src/core/runtime/prompt-slot.ts +8 -0
  29. package/src/core/runtime/runtime-types.ts +11 -3
  30. package/src/core/runtime/turn-activity.ts +42 -0
  31. package/src/core/runtime/turn-budget.ts +163 -19
  32. package/src/core/runtime/turn-limits.ts +61 -0
  33. package/src/core/services.ts +44 -1
  34. package/src/index.ts +11 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.22.1] - 2026-10-21
9
+
10
+ ### Changed
11
+
12
+ - A turn that reaches its time limit asks before it ends (`pi-roundtable`). When the built-in runtime's 10-minute budget or 30-minute cap fires on a turn that can show the owner a card, the turn keeps working and a card names the limit, the work time, what the turn is doing (the running tool and for how long, waiting on the model, or compacting) and the tools so far, with **Continue** (another full budget from the answer; at the cap the cap moves too) and **Cancel**. No answer within 5 minutes aborts the turn; a turn that finishes first closes the card and replies as usual. A turn that cannot show a card is aborted as before. An aborted turn now fails with `TurnTimeoutError` (an `AgentRunError`, with the reason as `TurnTimeout`: its `TurnLimit`, work time, running tools, tools so far and `TurnTimeoutEnd`), its log warning carries the same fields, and the posted failure says why instead of the generic notice. `ApprovalOptions` gains `wording` (`ApprovalWording`: the buttons' labels and the footers) for a card that is not an approval; the Discord cards honour it. `PiAgentRuntimeOptions` gains `turnTimeoutCardMs`, the card's grace period; the runtime plugin still passes none of the limits, so they stay fixed. See [the turn limits](docs/plugins.md#what-happens-when-the-bot-starts-and-stops).
13
+
14
+ ## [0.22.0] - 2026-10-21
15
+
16
+ ### Added
17
+
18
+ - Worker sessions for plugins (`pi-roundtable`). A new built-in plugin, `worker-sessions` (after `modules`), provides `WORKER_SESSIONS`, a `WorkerSessions`: `run(request)` runs a `WorkerSessionRequest` (`task`, `signal`, `prompt`, `extensions`, `toolNames`, and optionally `model`, `thinking` and `aborted`) in a fresh, unsaved Pi session on the host's model login and resolves to the worker's final answer, rejecting with an `AgentRunError` when it ends without one and with the signal's reason when it is aborted; the session is disposed either way. The session has no built-in tool, skill, context file, memory or agent tool, and only the tools its `extensions` register and `toolNames` names are active, even if an extension activates another. `model` and `thinking` default to the host's `delegation` settings, shown as `WorkerSessions.delegation`. The delegation worker for web research now runs on the same code. It is a separate plugin, not part of `modules`, so a host that replaces `BACKGROUND_TURNS` and `DELEGATION` still replaces that plugin whole. The built-in plugin names gain `worker-sessions`, so a plugin of that name is now refused. See [Workers of your own](docs/plugins.md#workers-of-your-own).
19
+ - `pi-roundtable-browser`, a new official workspace package published in lockstep (not yet on npm; see `.github/PACKAGE-RELEASES.md`): `browserAgent(options)` adds the owner-tier tool `browser_task({ task, url? })`, which runs a fresh worker session that drives a real browser through a `BrowserDriver` and returns its report, attaching the screenshots it kept to the agent's reply. The default driver runs agent-browser and Chromium in a Docker container started per task and removed after it, with no network but a unix socket to a host-side proxy that refuses every destination that is not a public address. See the package README and `site/src/content/docs/plugins/browser-package.mdx`.
20
+ - Skills from an npm package (`pi-roundtable`, `pi-roundtable-web`). `skills: { packages: ["pi-workflow-skills"] }` (the new type `SkillsConfig`) names packages the host installed; at startup the registry reads each package's `pi.skills` folders and offers every skill found as a fourth kind, `package` (`SkillSource` gains `{ kind: "package"; package }`; `SkillCatalogEntry.source` and the web console's skill list show the package). A package skill is read-only, named by its frontmatter and resolved to the installed file on every read, so upgrading the package changes the skill with no database write. Agents carry package skills and groups include them exactly like linked or written ones (`agent_skills`, `agent_create`, `skill_group_set`, `repo_task`); `skill_unlink`, `skill_update` and `skill_delete` refuse a package skill with a message saying it comes from the package, and `skill_link` and `skill_create` refuse a name a package skill holds. A package that is not installed, a package without `pi.skills`, two packages offering one name, or a package skill named like a built-in, linked or written skill stops startup with a `PluginError` naming the fix. A carried package skill that an upgrade removed is reported missing and skipped, as a vanished linked skill is.
21
+
8
22
  ## [0.21.0] - 2026-10-20
9
23
 
10
24
  ### Added
package/docs/plugins.md CHANGED
@@ -50,6 +50,29 @@ Their exported names are grouped by area in the source, with one entry for each;
50
50
  The [changelog](../CHANGELOG.md) lists every exported name, including type-only contracts.
51
51
  Import fixtures and fake threads from `pi-roundtable/testing` for tests.
52
52
 
53
+ #### Workers of your own
54
+
55
+ A plugin that gives a model its own small set of tools, such as a worker that drives a browser, asks the host for a worker session instead of building one from Pi's parts:
56
+
57
+ ```ts
58
+ const sessions = context.services.get(WORKER_SESSIONS);
59
+ const report = await sessions.run({
60
+ task: "What the worker is asked to do.",
61
+ signal: turn.signal,
62
+ prompt: "You are a ... worker. Report in the task's language.",
63
+ extensions: [registersTheWorkersTools],
64
+ toolNames: ["my_tool"],
65
+ });
66
+ ```
67
+
68
+ `run` starts a fresh session that is never saved, on the host's model login (the same credentials and refreshes the agents use), with the `model` and `thinking` of the host's `delegation` settings unless the request names its own (`model` is `<provider>/<id>`; `sessions.delegation` shows the defaults).
69
+ The session has no built-in tool, no skills, no context files, no memory and none of the agents' tools: only the tools the `extensions` register with `pi.registerTool` and `toolNames` lists are active, and an extension that activates another tool is undone.
70
+ `prompt` is added to its system prompt, so the standing rules and any instructions the worker should follow, such as a tool's usage guide, go there.
71
+ `run` resolves to the worker's final answer, rejects with an `AgentRunError` when it ends without one, and rejects when `signal` aborts (`aborted` is the message of that rejection, default "the worker ran out of time"); the session is disposed either way.
72
+ The tools are the plugin's to bound: the worker has no tier, no hold and no conversation, so a tool checks what the worker asks for, limits its work, and cleans up in a `finally` around `run`.
73
+ A file a tool produces reaches the person only through the tool that called `run`: collect it and call `turn.attachFile` after `run` returns, as the [browser package source][browser-source] does with screenshots.
74
+ The `worker-sessions` plugin provides the service on every host.
75
+
53
76
  #### Helpers for a Pi session of your own
54
77
 
55
78
  Use the kit's building blocks for a plugin that runs Pi itself, such as a coding worker:
@@ -218,6 +241,7 @@ The built-in plugins provide these, from the main entry:
218
241
  | `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` |
219
242
  | `BACKGROUND_TURNS` | `BackgroundTurns` | `modules` | Turns nobody wrote: `runScheduled`, `runDelegated`, `runErrorReport` |
220
243
  | `DELEGATION` | `Delegator` | `modules` | `start(request)` a background task, `runningChannels()`, `idle()` |
244
+ | `WORKER_SESSIONS` | `WorkerSessions` | `worker-sessions` | Fresh Pi sessions for a plugin's own workers, on the host's model login: `delegation` is the host's delegation `model` and `thinking`, and `run({ task, signal, prompt, extensions, toolNames, model?, thinking?, aborted? })` runs the task in a session that has only the tools `extensions` register and `toolNames` names (no built-in tool, skill, context file, memory or agent tool) and returns the worker's final answer; see [Workers of your own](#workers-of-your-own) |
221
245
 
222
246
  The data types the ports use (`Schedule`, `NewSchedule`, `Agent`, `AgentGroup`, `TeamStatus`, `Memory`, `SkillSet`, `DelegationJob`, and so on) are in the main entry too.
223
247
  Your plugins run after the built-ins, so they can read every key above.
@@ -366,7 +390,16 @@ You can switch them off in the configuration.
366
390
  | 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 |
367
391
 
368
392
  Switching an addon off leaves its tables and rows unchanged, ready for when you turn it on again.
369
- `skills` also takes the two directories (`skills: { builtinDir, reposDir }`) when it is on.
393
+ `skills` also takes the two directories and the skill packages (`skills: { builtinDir, reposDir, packages }`) when it is on.
394
+
395
+ `skills.packages` lists npm packages the host installed, for example `["pi-workflow-skills"]`.
396
+ At startup the registry reads each package's `pi.skills` folders (Pi's package manifest) and offers every skill it finds there as a fourth kind, `package`, beside the built-in, linked and written ones.
397
+ A package skill is named by its frontmatter and resolved to the installed file on every read, so upgrading the package changes the skill and nothing is written to the database.
398
+ Agents carry package skills and groups include them exactly like the others: `agent_skills`, `agent_create`'s `skills` field, `skill_group_set` and `repo_task` accept their names.
399
+ `skill_unlink`, `skill_update` and `skill_delete` refuse a package skill, naming the package, and `skill_link` and `skill_create` refuse a name a package skill already holds.
400
+ `SkillCatalogEntry.source` is `{ kind: "package", package }` for them, and the web console's skill list shows the package.
401
+ A package that is not installed, a package with no `pi.skills` (or none that holds a `SKILL.md`), two packages offering one name, a package skill with the name of a built-in, linked or written skill: each stops startup with a `PluginError` naming the fix.
402
+ A carried package skill that an upgrade removed is reported missing and skipped, as a linked skill whose file vanished is.
370
403
 
371
404
  Use `find` for an optional addon service; it returns `undefined` while the addon is off.
372
405
  Use `get` if your plugin needs the addon; it fails with a message naming the switch:
@@ -3162,7 +3195,7 @@ The [pi-roundtable-mcp][mcp-package] workspace, published separately on npm, add
3162
3195
  The first lets the owner add MCP servers such as Notion or a calendar from Discord and gives your code the list; the second lets an agent outside Discord reach your agent.
3163
3196
  Install it with `bun add pi-roundtable-mcp`.
3164
3197
 
3165
- Six official packages live in this repository as Bun workspaces and publish as separate npm packages, versioned in lockstep with the core.
3198
+ Seven official packages live in this repository as Bun workspaces and publish as separate npm packages, versioned in lockstep with the core.
3166
3199
  The [site guide source files][package-guides] describe installation, requirements, configuration, and security considerations:
3167
3200
 
3168
3201
  - [pi-roundtable-drawing][drawing-package]: [site guide source][drawing-guide] · local relationship maps, magic circles, sigils, sacred geometry, and card spreads.
@@ -3170,6 +3203,7 @@ The [site guide source files][package-guides] describe installation, requirement
3170
3203
  - [pi-roundtable-web][web-package]: [site guide source][web-guide] · an owner-only console for conversations, transcripts, and memory notes with live updates.
3171
3204
  - [pi-roundtable-sandbox][sandbox-package]: [site guide source][sandbox-guide] · sealed guest channels on native Linux Docker with a host-only credential broker and allow-listed tools.
3172
3205
  - [pi-roundtable-mcp][mcp-package]: [site guide source][mcp-guide] · MCP connectors through a gateway and remote MCP endpoints for agent turns and owner-granted Discord channel tools.
3206
+ - [pi-roundtable-browser][browser-package]: [site guide source][browser-guide] · `browser_task`, a worker that drives agent-browser in a Docker container with no route to the host's private networks, and a `BrowserDriver` for another browser.
3173
3207
  - [pi-roundtable-webchat][webchat-package]: [site guide source][webchat-guide] · a WebSocket chat for people an OpenID Connect provider signs in, with private conversations, live progress, and approval cards; see [web chat](#web-chat-pi-roundtable-webchat).
3174
3208
 
3175
3209
  [drawing-package]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/drawing/README.md
@@ -3177,17 +3211,20 @@ The [site guide source files][package-guides] describe installation, requirement
3177
3211
  [web-package]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/web/README.md
3178
3212
  [sandbox-package]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/sandbox/README.md
3179
3213
  [mcp-package]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/mcp/README.md
3214
+ [browser-package]: https://github.com/wayne930242/pi-roundtable/blob/master/packages/browser/README.md
3180
3215
  [package-guides]: https://github.com/wayne930242/pi-roundtable/tree/master/site/src/content/docs/plugins/
3181
3216
  [drawing-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/drawing-package.mdx
3182
3217
  [coding-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/coding-package.mdx
3183
3218
  [web-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/web-package.mdx
3184
3219
  [sandbox-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/sandbox-package.mdx
3185
3220
  [mcp-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/mcp-package.mdx
3221
+ [browser-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/browser-package.mdx
3186
3222
  [webchat-guide]: https://github.com/wayne930242/pi-roundtable/blob/master/site/src/content/docs/plugins/webchat-package.mdx
3187
3223
  [coding-source]: https://github.com/wayne930242/pi-roundtable/tree/master/packages/coding
3188
3224
  [sandbox-source]: https://github.com/wayne930242/pi-roundtable/tree/master/packages/sandbox
3189
3225
  [web-source]: https://github.com/wayne930242/pi-roundtable/tree/master/packages/web
3190
3226
  [drawing-source]: https://github.com/wayne930242/pi-roundtable/tree/master/packages/drawing
3227
+ [browser-source]: https://github.com/wayne930242/pi-roundtable/tree/master/packages/browser
3191
3228
  [mcp-source]: https://github.com/wayne930242/pi-roundtable/tree/master/packages/mcp
3192
3229
  [codex-images-template]: https://github.com/wayne930242/pi-roundtable/blob/master/templates/official/codex-images/plugin.ts
3193
3230
  [dice-template]: https://github.com/wayne930242/pi-roundtable/blob/master/templates/official/dice/plugin.ts
@@ -3455,7 +3492,7 @@ The host's `run()` handles startup:
3455
3492
  The runtime plugin builds the runtime later, in its own setup: from the `runtime` slot when a plugin fills it, else the Pi runtime.
3456
3493
  2. The database is opened and every plugin's migrations run, in plugin order.
3457
3494
  3. Every plugin's `setup` runs, in plugin order.
3458
- The order is the built-ins (`memory`, `schedule-store`, `prechecks`, `discord`, `modules`, `discord-admin`, `skills`, `conversations`, `runtime`, `agent-server`, `seeds`; an addon that is switched off is not there, and without Discord neither are `discord`, `discord-admin`, `skills`, `agent-server`, or `seeds`), then yours in the order of `plugins` in `roundtable.config.ts`, then the built-in `schedules`, so a due schedule fires only once everything it can reach is running.
3495
+ The order is the built-ins (`memory`, `schedule-store`, `prechecks`, `discord`, `modules`, `worker-sessions`, `discord-admin`, `skills`, `conversations`, `runtime`, `agent-server`, `seeds`; an addon that is switched off is not there, and without Discord neither are `discord`, `discord-admin`, `skills`, `agent-server`, or `seeds`), then yours in the order of `plugins` in `roundtable.config.ts`, then the built-in `schedules`, so a due schedule fires only once everything it can reach is running.
3459
3496
  4. The contributions are linked: tool tiers, hold rules, the session plan, the channel router, and the events.
3460
3497
  From here `sessions()`, `conversations`, `surfaces`, `turns`, and `dashboard()` work.
3461
3498
  5. Every plugin's `preflight` runs, in plugin order.
@@ -3489,7 +3526,10 @@ On `SIGTERM` or `SIGINT` the bot takes no new work at once, finishes what runs,
3489
3526
  `shutdown()` returns exit code `0`, or `1` if a listener, service, or pool failed to stop.
3490
3527
  Every call shares the same shutdown.
3491
3528
  Give the service manager a stop timeout longer than the drain limit plus about a minute for the steps after it, such as systemd's `TimeoutStopSec=5min` with the default three-minute drain; a shorter one has the manager kill the process in the middle of the drain.
3492
- The built-in runtime aborts a turn that works 10 minutes, not counting time spent waiting on the owner's cards. Each message steered into the running turn gives it the full 10 minutes again from that moment, up to a cap of 30 minutes of work for the whole turn; once the cap is reached the turn takes no more steering, so a message waits for the next turn, and the turn is aborted as on its timeout. Both limits are fixed: the runtime plugin passes no option for them, so a host cannot change them, and a limit that waiting on an owner's card pushed back can fire up to a second late. The log says `steer renewed the turn's time budget` with `deadlineMs`, the work time left, and the timeout warning names the limit that fired (`limit: "turn"` or `"turnMax"`). Task workers (`task.timeoutMs`), coding workers (`workTimeout`) and sandbox guests (their own `turnTimeoutMs`) keep their own limits, which steering does not renew.
3529
+ The built-in runtime limits a turn to 10 minutes of work, not counting time spent waiting on the owner's cards. Each message steered into the running turn gives it the full 10 minutes again from that moment, up to a cap of 30 minutes of work for the whole turn; once the cap is reached the turn takes no more steering, so a message waits for the next turn.
3530
+ When a limit fires on a turn that can show a card (an owner's turn or a report turn on a surface with cards, the same turns that can ask with `ask_user` or an approval card), the turn is not aborted: it keeps working and a card asks the owner whether to go on. The card names the limit, how long the turn has worked, what it is doing now (the tool running and for how long, or waiting on the model, or compacting the conversation) and the tools it called so far, by name and count, never their inputs. **Continue** gives the turn another full 10 minutes from the answer; at the cap it moves the cap too, to 10 minutes from the answer, so the owner can keep going in steps. **Cancel** aborts the turn. With no answer within 5 minutes of wall time the turn is aborted. The time the card is open counts as work, unlike an `ask_user` or approval card, because the turn keeps working; while it is open the turn takes no steering. If the turn finishes first, the card closes saying so and the reply is posted as usual; a press after that gets the usual "no longer valid" reply. The card's buttons come from `ApprovalOptions.wording` on `Prompts.confirm`, with `supersede` closing it, so a surface that shows approvals shows it; one that ignores `wording` shows its own approve and decline labels.
3531
+ A turn that cannot show a card (a schedule's turn, a member's turn, a turn without a surface) is aborted at once, as before. Either way an aborted turn fails with a `TurnTimeoutError` (an `AgentRunError`) whose `timeout` (`TurnTimeout`) holds the limit, the work time, the running tools, whether it was compacting, the tools so far, and how it ended (`end`: `"timeout"`, `"cancelled"` by the owner, or `"unanswered"`); its message says the same in English, and the failure the conversation posts says it in the host's language instead of the generic failure notice.
3532
+ The limits are fixed: the runtime plugin passes no option for them, nor for the card's grace period (`turnTimeoutMs`, `turnMaxMs` and `turnTimeoutCardMs` of `PiAgentRuntimeOptions`, for a host that builds the runtime itself), and a limit that waiting on an owner's card pushed back can fire up to a second late. The log says `steer renewed the turn's time budget` with `deadlineMs`, the work time left; `turn reached its time limit; asking the owner` when the card is posted; `owner continued the turn past its time limit` on Continue; and the warning `turn timed out; aborting` names the limit that fired (`limit: "turn"` or `"turnMax"`) with `workedMs`, `running`, `compacting`, `tools` and `end`. Task workers (`task.timeoutMs`), coding workers (`workTimeout`) and sandbox guests (their own `turnTimeoutMs`) keep their own limits, which steering does not renew and which post no card.
3493
3533
  A turn whose model ignores its abort (a subprocess that does not exit) is given up on too: a turn that outlives its timeout, or is stopped, gets `turnAbortGraceMs` (15 seconds by default) to end, and then its session is disposed of and the turn is reported failed, so it cannot hold the queue.
3494
3534
  `listen()`, called by the command line and a host's own entry point, exits the process with that code.
3495
3535
 
@@ -3543,7 +3583,7 @@ These are the messages as the code writes them, with `<...>` where your names go
3543
3583
  | `no runtime provider is configured: the runtime plugin builds the Pi runtime when no plugin fills the runtime slot; read the running one from services.get(RUNTIME)` | Only a plugin that calls `providers.runtime` without `providers.filled.has("runtime")` sees it; read `services.get(RUNTIME)` instead |
3544
3584
  | `session tool <name> takes a core extension name. Rename it.` | Pick a name other than the core's |
3545
3585
  | `plugin <name>: setup failed: the database was queried during setup…` (from `Roundtable.check()`, `roundtable doctor` or `start`) | A plugin's `setup` ran a query. Take `database()` in setup and query from a service's `start` or from a handler |
3546
- | `two plugins are named <name>.` (from `roundtable doctor`) | Rename yours; the built-in plugins are `memory`, `schedule-store`, `prechecks`, `discord`, `modules`, `discord-admin`, `skills`, `conversations`, `runtime`, `agent-server`, `seeds`, and `schedules` |
3586
+ | `two plugins are named <name>.` (from `roundtable doctor`) | Rename yours; the built-in plugins are `memory`, `schedule-store`, `prechecks`, `discord`, `modules`, `worker-sessions`, `discord-admin`, `skills`, `conversations`, `runtime`, `agent-server`, `seeds`, and `schedules` |
3547
3587
  | `migration <plugin>/<name> is declared twice` | A plugin declares two migrations with one name: rename one (the same name in two plugins is fine) |
3548
3588
  | `/<root> <name> is added twice`, `/<name> is registered twice` | Give each slash command and subcommand its own name |
3549
3589
  | `route <name> is registered twice`, `routes <a> and <b> overlap on listener <id>` | Give each route its own name and a path no other route can take |
@@ -3700,6 +3740,7 @@ Import from the entries listed below; source area files are internal.
3700
3740
  | `ApprovalDetails` | `pi-roundtable` | type |
3701
3741
  | `ApprovalFile` | `pi-roundtable` | type |
3702
3742
  | `ApprovalOptions` | `pi-roundtable` | type |
3743
+ | `ApprovalWording` | `pi-roundtable` | type |
3703
3744
  | `ApprovedBy` | `pi-roundtable` | type |
3704
3745
  | `AskOption` | `pi-roundtable` | type |
3705
3746
  | `AskSupport` | `pi-roundtable` | type |
@@ -3885,8 +3926,12 @@ Import from the entries listed below; source area files are internal.
3885
3926
  | `SessionToolSnapshot` | `pi-roundtable` | type |
3886
3927
  | `SkillCatalogEntry` | `pi-roundtable` | type |
3887
3928
  | `SkillRegistry` | `pi-roundtable` | type |
3929
+ | `WorkerSessions` | `pi-roundtable` | type |
3930
+ | `WorkerSessionRequest` | `pi-roundtable` | type |
3931
+ | `WORKER_SESSIONS` | `pi-roundtable` | value |
3888
3932
  | `SkillSet` | `pi-roundtable` | type |
3889
3933
  | `SkillSource` | `pi-roundtable` | type |
3934
+ | `SkillsConfig` | `pi-roundtable` | type |
3890
3935
  | `Speaker` | `pi-roundtable` | type |
3891
3936
  | `SpeakerMemory` | `pi-roundtable` | type |
3892
3937
  | `StoredAttachment` | `pi-roundtable` | type |
@@ -3922,6 +3967,10 @@ Import from the entries listed below; source area files are internal.
3922
3967
  | `TurnRequest` | `pi-roundtable` | type |
3923
3968
  | `TurnResult` | `pi-roundtable` | type |
3924
3969
  | `TurnSelection` | `pi-roundtable` | type |
3970
+ | `TurnLimit` | `pi-roundtable` | type |
3971
+ | `TurnTimeout` | `pi-roundtable` | type |
3972
+ | `TurnTimeoutEnd` | `pi-roundtable` | type |
3973
+ | `TurnTimeoutError` | `pi-roundtable` | value |
3925
3974
  | `WebSocketAccept` | `pi-roundtable` | type |
3926
3975
  | `WebSocketRoute` | `pi-roundtable` | type |
3927
3976
  | `WebSocketSendResult` | `pi-roundtable` | type |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "A plugin-driven Pi agent server for one owner, reached through Discord or the web",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@ import type {
8
8
  import { AgentError } from "../domain/errors.ts";
9
9
  import type { InterimPosts } from "../domain/interim.ts";
10
10
  import type { AgentTurnScope } from "../domain/ports.ts";
11
- import { messages } from "../i18n/index.ts";
11
+ import { failureNotice } from "../presentation/failure-notice.ts";
12
12
  import { splitReply } from "../presentation/reply-splitter.ts";
13
13
  import { thinkingLine } from "../presentation/thinking-line.ts";
14
14
  import { withReplyFiles } from "../reply-files.ts";
@@ -35,11 +35,6 @@ import {
35
35
 
36
36
  export { MAX_CHAIN_MESSAGES, type TeamTurnsOptions };
37
37
 
38
- /** What a failed turn posts: the stopped notice for a stop, the failure notice otherwise. */
39
- function noticeOf(result: { stopped?: true }): string {
40
- return result.stopped ? messages().stoppedNotice : messages().failureNotice;
41
- }
42
-
43
38
  /**
44
39
  * The agent team's turns: owner messages, background turns, group rounds, and messages between
45
40
  * agents, each posted under the answering agent's name.
@@ -246,7 +241,7 @@ export class TeamTurns {
246
241
  : undefined;
247
242
  await this.#post(postTo, this.#current(agent), {
248
243
  ...(thinking ? { thinking } : {}),
249
- chunks: result.ok ? splitReply(result.text) : [noticeOf(result)],
244
+ chunks: result.ok ? splitReply(result.text) : [failureNotice(result)],
250
245
  ...(result.ok && result.files?.length ? { files: result.files } : {}),
251
246
  });
252
247
  } catch (error) {
@@ -1,4 +1,5 @@
1
1
  import type { SQL } from "bun";
2
+ import { discoverPackageSkills } from "../modules/skills/package-skills.ts";
2
3
  import { StoredSkillRegistry } from "../modules/skills/skill-registry.ts";
3
4
  import { SkillStore } from "../modules/skills/skill-store.ts";
4
5
  import {
@@ -19,6 +20,10 @@ export interface SkillsOptions {
19
20
  reposDir: string;
20
21
  writtenDir: string;
21
22
  builtinDir: string;
23
+ /** npm packages whose `pi.skills` folders offer read-only skills; the host installed them. */
24
+ packages?: readonly string[];
25
+ /** Where a package is found; default the installed package's folder. */
26
+ packageDir?: (name: string) => string;
22
27
  }
23
28
 
24
29
  /** Admins manage skills; every speaker may look one up. */
@@ -47,11 +52,17 @@ export function skillsPlugin(
47
52
  // The skills are read once every plugin is set up, so a plugin may still place some in setup.
48
53
  preflight: () => registry?.init(),
49
54
  setup: async ({ database, services, logger }) => {
55
+ // Read before the store opens, so a missing package stops startup at once.
56
+ const packageSkills = discoverPackageSkills(
57
+ options.packages ?? [],
58
+ options.packageDir,
59
+ );
50
60
  const built = new StoredSkillRegistry({
51
61
  store: await openStore(database(), options.guildId),
52
62
  reposDir: options.reposDir,
53
63
  writtenDir: options.writtenDir,
54
64
  builtinDir: options.builtinDir,
65
+ packageSkills,
55
66
  logger,
56
67
  });
57
68
  registry = built;
@@ -0,0 +1,44 @@
1
+ import { join } from "node:path";
2
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ formatModelRef,
5
+ type ModelRef,
6
+ type ThinkingLevel,
7
+ } from "../models.ts";
8
+ import { workerSessions } from "../modules/delegation/worker-session.ts";
9
+ import type { RoundtablePlugin } from "../plugin.ts";
10
+ import { WORKER_SESSIONS } from "../services.ts";
11
+
12
+ export interface WorkerSessionsPluginOptions {
13
+ /** Shared with the rest of the host, so logins refresh in one place. */
14
+ modelRuntime: ModelRuntime;
15
+ agentDir: string;
16
+ dataDir: string;
17
+ /** The model and thinking level a worker session uses unless its request names its own. */
18
+ delegation: { model: ModelRef; thinking: ThinkingLevel };
19
+ }
20
+
21
+ /** Provides `WORKER_SESSIONS`: worker sessions on the host's model login, with its delegation settings as the defaults. */
22
+ export function workerSessionsPlugin(
23
+ options: WorkerSessionsPluginOptions,
24
+ ): RoundtablePlugin {
25
+ return {
26
+ name: "worker-sessions",
27
+ provides: [WORKER_SESSIONS],
28
+ setup: ({ services }) => {
29
+ services.provide(
30
+ WORKER_SESSIONS,
31
+ workerSessions({
32
+ modelRuntime: options.modelRuntime,
33
+ agentDir: options.agentDir,
34
+ workDir: join(options.dataDir, "workers"),
35
+ delegation: {
36
+ model: formatModelRef(options.delegation.model),
37
+ thinking: options.delegation.thinking,
38
+ },
39
+ }),
40
+ );
41
+ return {};
42
+ },
43
+ };
44
+ }
@@ -36,11 +36,11 @@ import {
36
36
  number,
37
37
  oneOf,
38
38
  optional,
39
- orOff,
40
39
  record,
41
40
  shape,
42
41
  text,
43
42
  } from "./schema.ts";
43
+ import { type SkillsConfig, skillsField } from "./skills-config.ts";
44
44
 
45
45
  export type { DiscordConfig };
46
46
 
@@ -168,7 +168,7 @@ export interface RoundtableConfig {
168
168
  * the `skills` addon out: agents carry no skills, and no skill tools exist. Stored skills stay
169
169
  * in their tables. Without `discord` there are no agents, so the addon is off.
170
170
  */
171
- skills?: false | { builtinDir?: string; reposDir?: string };
171
+ skills?: false | SkillsConfig;
172
172
  /**
173
173
  * Whether each speaker's memory is kept and offered as tools; default true. `false` leaves the
174
174
  * `memory` addon out: no memory tools, no memory prompt block, and the table stays as it is.
@@ -284,9 +284,7 @@ const schema = shape({
284
284
  avatar: optional(text),
285
285
  workDir: optional(text),
286
286
  scratchDir: optional(text),
287
- skills: optional(
288
- orOff(shape({ builtinDir: optional(text), reposDir: optional(text) })),
289
- ),
287
+ skills: optional(skillsField),
290
288
  memory: optional(bool),
291
289
  ops: optional(shape({ agent: optional(text), conversation: optional(text) })),
292
290
  drainSeconds: optional(integer(1, 3_600)),
@@ -352,7 +350,7 @@ export interface ResolvedConfig {
352
350
  workDir: string;
353
351
  scratchDir: string;
354
352
  /** `false` when the skills addon is off. */
355
- skills: false | { builtinDir?: string; reposDir?: string };
353
+ skills: false | SkillsConfig;
356
354
  memory: boolean;
357
355
  ops?: { agent: string } | { conversation: ChannelKey };
358
356
  drainMs: number;
@@ -0,0 +1,23 @@
1
+ import { list, optional, orOff, shape, text } from "./schema.ts";
2
+
3
+ /** The `skills` setting when the addon is on. */
4
+ export interface SkillsConfig {
5
+ /** Where the registry reads the release's built-in skills. */
6
+ builtinDir?: string;
7
+ /** Where it keeps linked repositories; default `<dataDir>/repos`. */
8
+ reposDir?: string;
9
+ /**
10
+ * npm packages the host installed. Every skill in their `pi.skills` folders is offered
11
+ * read-only, and carried and grouped like any other. Startup fails, naming the fix, when a
12
+ * package is not installed, ships no skills, or holds a skill whose name another skill has.
13
+ */
14
+ packages?: string[];
15
+ }
16
+
17
+ export const skillsField = orOff(
18
+ shape({
19
+ builtinDir: optional(text),
20
+ reposDir: optional(text),
21
+ packages: optional(list(text)),
22
+ }),
23
+ );
@@ -13,6 +13,7 @@ import {
13
13
  precheckPlugin,
14
14
  scheduleStorePlugin,
15
15
  } from "./builtin/stores.ts";
16
+ import { workerSessionsPlugin } from "./builtin/worker-sessions.ts";
16
17
  import { warnDeprecations } from "./config/access.ts";
17
18
  import {
18
19
  type ResolvedConfig,
@@ -187,6 +188,7 @@ function discordAssembly(
187
188
  reposDir: config.skills.reposDir ?? join(config.dataDir, "repos"),
188
189
  writtenDir: join(config.dataDir, "skills"),
189
190
  builtinDir: config.skills.builtinDir ?? join(ASSETS, "skills"),
191
+ packages: config.skills.packages ?? [],
190
192
  }),
191
193
  ]
192
194
  : [],
@@ -321,6 +323,12 @@ export async function defineRoundtable(
321
323
  ? { errorReporter }
322
324
  : {}),
323
325
  }),
326
+ workerSessionsPlugin({
327
+ modelRuntime,
328
+ agentDir: config.agentDir,
329
+ dataDir: config.dataDir,
330
+ delegation: config.delegation,
331
+ }),
324
332
  ...(assembly?.admin ?? []),
325
333
  ...(assembly?.skills ?? []),
326
334
  conversationsPlugin(),
@@ -123,23 +123,27 @@ export function answeredLine(answer: OwnerAnswer): string {
123
123
  return messages().cardAnswered(parts);
124
124
  }
125
125
 
126
- /** An approval's controls: run, cancel, and, when the card offers a copy, a copy button that stays usable once it ended. */
126
+ /**
127
+ * An approval's controls: run, cancel, or the labels its wording gives, and, when the card offers
128
+ * a copy, a copy button that stays usable once it ended.
129
+ */
127
130
  export function approvalRows(
128
131
  id: string,
129
132
  disabled: boolean,
130
133
  copy: { label: string } | undefined,
134
+ labels?: { approve: string; decline: string },
131
135
  ): Rows {
132
136
  return [
133
137
  new ActionRowBuilder<ButtonBuilder>().addComponents(
134
138
  new ButtonBuilder()
135
139
  .setCustomId(`${CARD_PREFIX}${id}:yes`)
136
- .setLabel(messages().cardRunLabel)
140
+ .setLabel(labels?.approve ?? messages().cardRunLabel)
137
141
  .setEmoji("✅")
138
142
  .setStyle(ButtonStyle.Success)
139
143
  .setDisabled(disabled),
140
144
  new ButtonBuilder()
141
145
  .setCustomId(`${CARD_PREFIX}${id}:no`)
142
- .setLabel(messages().cardCancelLabel)
146
+ .setLabel(labels?.decline ?? messages().cardCancelLabel)
143
147
  .setEmoji("❌")
144
148
  .setStyle(ButtonStyle.Secondary)
145
149
  .setDisabled(disabled),
@@ -83,6 +83,8 @@ export interface OpenCard {
83
83
  mention?: boolean;
84
84
  /** Who may answer, and what to tell someone else who presses it. */
85
85
  audience: Audience;
86
+ /** An approval card's footers once approved or declined, where its wording gives them. */
87
+ outcomes?: { approved: string; declined: string };
86
88
  }
87
89
 
88
90
  /** What `#post` needs to post one card; `T` is what the card resolves with. */
@@ -101,6 +103,8 @@ export interface CardSpec<T> {
101
103
  build?(id: string, footer: string, disabled: boolean): CardPayload;
102
104
  rows?(id: string, disabled: boolean): Rows;
103
105
  footer: string;
106
+ /** An approval card's footers once approved or declined, where its wording gives them. */
107
+ outcomes?: { approved: string; declined: string };
104
108
  wait?: {
105
109
  pending: T;
106
110
  late(
@@ -175,8 +175,10 @@ export class OwnerCards implements InteractionModule {
175
175
  sections: [message],
176
176
  ...(options?.supersede ? { supersede: options.supersede } : {}),
177
177
  ...(copy ? { copy: copy.text } : {}),
178
- rows: (id, disabled) => approvalRows(id, disabled, copy),
179
- footer: messages().cardApprovalFooter,
178
+ rows: (id, disabled) =>
179
+ approvalRows(id, disabled, copy, options?.wording),
180
+ footer: options?.wording?.footer ?? messages().cardApprovalFooter,
181
+ ...(options?.wording ? { outcomes: options.wording } : {}),
180
182
  },
181
183
  );
182
184
  },
@@ -296,6 +298,7 @@ export class OwnerCards implements InteractionModule {
296
298
  ...(card.question ? { question: card.question } : {}),
297
299
  ...(card.form ? { form: card.form } : {}),
298
300
  ...(card.previews ? { previews: card.previews } : {}),
301
+ ...(card.outcomes ? { outcomes: card.outcomes } : {}),
299
302
  };
300
303
  this.#open.set(id, open);
301
304
  this.#options
@@ -417,7 +420,11 @@ export class OwnerCards implements InteractionModule {
417
420
  return true;
418
421
  }
419
422
  await interaction.update(
420
- card.view(approved ? messages().cardApproved : messages().cardDeclined),
423
+ card.view(
424
+ approved
425
+ ? (card.outcomes?.approved ?? messages().cardApproved)
426
+ : (card.outcomes?.declined ?? messages().cardDeclined),
427
+ ),
421
428
  );
422
429
  return true;
423
430
  }
@@ -0,0 +1,38 @@
1
+ import { AgentRunError } from "./errors.ts";
2
+
3
+ /** Which limit a turn reached: its budget since the start or the last steer, or its cap. */
4
+ export type TurnLimit = "turn" | "turnMax";
5
+
6
+ /**
7
+ * How a timed-out turn ended: at its limit with no card to ask on (`timeout`), cancelled on the
8
+ * timeout card (`cancelled`), or with the card unanswered in its grace period (`unanswered`).
9
+ */
10
+ export type TurnTimeoutEnd = "timeout" | "cancelled" | "unanswered";
11
+
12
+ /** Why a turn hit a time limit, as data: which limit, how long it worked, and what it was doing. */
13
+ export interface TurnTimeout {
14
+ limit: TurnLimit;
15
+ /** The limit's length: the budget, or the cap as the owner's Continues raised it. */
16
+ limitMs: number;
17
+ /** The turn's work so far, not counting time spent waiting on the owner's other cards. */
18
+ workedMs: number;
19
+ /** The tools running, the longest-running first, each with how long it has run. */
20
+ running: { tool: string; ms: number }[];
21
+ compacting: boolean;
22
+ /** Every tool called so far, by name, with how often. */
23
+ tools: [string, number][];
24
+ /** How long the timeout card waits for an answer. */
25
+ graceMs: number;
26
+ end: TurnTimeoutEnd;
27
+ }
28
+
29
+ /** A turn aborted at a time limit; `timeout` says why, and the message says it in English. */
30
+ export class TurnTimeoutError extends AgentRunError {
31
+ override name = "TurnTimeoutError";
32
+ constructor(
33
+ readonly timeout: TurnTimeout,
34
+ message: string,
35
+ ) {
36
+ super(message);
37
+ }
38
+ }
@@ -7,6 +7,7 @@ import { discordEn } from "./discord.ts";
7
7
  import { ownerEn } from "./owner.ts";
8
8
  import { schedulesEn } from "./schedules.ts";
9
9
  import { timeZonesEn } from "./time-zones.ts";
10
+ import { turnTimeoutEn } from "./turn-timeout.ts";
10
11
  import type { CatalogContext } from "./types.ts";
11
12
 
12
13
  export type Messages = ReturnType<typeof createEn>;
@@ -23,5 +24,6 @@ export function createEn(ctx: CatalogContext) {
23
24
  ...dashboardEn(ctx),
24
25
  ...channelsEn(ctx),
25
26
  ...discordEn(ctx),
27
+ ...turnTimeoutEn(),
26
28
  };
27
29
  }