pi-roundtable 0.7.15 → 0.7.17
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 +36 -0
- package/docs/plugins.md +94 -4
- package/package.json +2 -1
- package/src/core/agents/agent-ports.ts +9 -0
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-team.ts +2 -0
- package/src/core/agents/team-options.ts +2 -0
- package/src/core/agents/team-turn-types.ts +1 -1
- package/src/core/agents/team-turns.ts +24 -0
- package/src/core/builtin/agent-server.ts +37 -0
- package/src/core/config/config.ts +30 -3
- package/src/core/contract/runtime.ts +5 -0
- package/src/core/contract/surface.ts +8 -0
- package/src/core/define-roundtable.ts +3 -0
- package/src/core/discord/agent-discord.ts +33 -0
- package/src/core/discord/discord-surface.ts +20 -0
- package/src/core/domain/interim.ts +16 -0
- package/src/core/domain/ports.ts +6 -0
- package/src/core/holds.ts +2 -0
- package/src/core/modules/host-shell/git-push.ts +112 -0
- package/src/core/modules/host-shell/shell-paths.ts +160 -0
- package/src/core/modules/host-shell/shell-policy.ts +111 -48
- package/src/core/routing/conversation-turns.ts +3 -0
- package/src/core/routing/surface-port.ts +1 -0
- package/src/core/runtime/conversation-sessions.ts +7 -2
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +6 -7
- package/src/core/runtime/interim-text.ts +207 -0
- package/src/core/runtime/pi-agent-runtime.ts +13 -2
- package/src/core/runtime/prompt-slot.ts +13 -2
- package/src/core/runtime/runtime-types.ts +8 -0
- package/src/core/runtime/session-factory.ts +16 -0
- package/src/index.ts +5 -0
- package/src/kit/index.ts +20 -0
- package/src/kit/jev.ts +357 -0
- package/src/kit/shell.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.17] - 2026-10-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A turn posts the text it writes before its final answer as it goes, so a proposal written before `ask_user` shows above its card instead of never reaching the channel. Primary text (400 characters or more, or with a Markdown heading, list, table or code fence) of a tool-calling assistant message is posted as ordinary messages when the message ends; shorter narration and the tools called (`-# bash ×3 · read`, names only) share one small-text progress message per run of tool calls, edited at most every 1.5 s, bounded to 2000 characters by dropping its oldest lines. Before any card the pending text is posted first. The final reply, its thinking line, and steered runs are unchanged; a failed interim post is logged and never fails the turn. See [interim text](docs/plugins.md#interim-text-what-a-turn-writes-before-its-final-answer).
|
|
13
|
+
- Config `interimText: "on" | "off"` (default `"on"`) and `interimPrimaryChars` (default 400), also on `PiAgentRuntimeOptions` and the agent server's options.
|
|
14
|
+
- `TurnRequest.interim`, `ChatSurface.interim(channel)` and `SurfacePort.interim(channel)` (Discord's surface implements it), optional `AgentChannels.interim(channelId, as)` (the agents' webhooks), and `PromptSlot.bind`'s optional `beforeCard`.
|
|
15
|
+
- Types: `InterimMessage`, `InterimPosts`, `InterimTextMode`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `ask_user`'s description asks for a question that reads on its own, with the context it needs.
|
|
20
|
+
|
|
21
|
+
## [0.7.16] - 2026-10-04
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `pi-roundtable/kit` owns a Jev compaction engine on pi-jev-compaction 1.0.0 (now a dependency, pinned exactly): `jevCompact(input, options)` and two adapters, `jevCompactionExtension({ logger })` for the `compaction` session tool (placed through `session.compaction.wrap`, engine `JEV_COMPACTION_ENGINE`, `pi-jev-compaction`) and `jevCompactor({ logger })` for pi-roundtable-sandbox's `compaction` option. Both log each fallback to Pi's summary with its reason and `tokensBefore`, except a missing API key, which they log once (`Jev is not configured, so compaction uses Pi's summary`) and then compact through Pi's summary quietly: the key is optional.
|
|
26
|
+
- Values: `jevCompact`, `jevCompactionExtension`, `jevCompactor`, `isRuleLoad`, `JEV_COMPACTION_ENGINE`, `JEV_GOAL`, `JEV_PREVIOUS_SUMMARY_LIMIT_TOKENS`; types: `JevCompactInput`, `JevCompactOptions`, `JevCompactOutcome`, `JevCompactor`, `JevCompactRequest`, `JevExtensionOptions`, `JevSkipReason`.
|
|
27
|
+
- Unlike pi-jev-compaction's own `compactPiSession`, a summary carries the previous summary once, not twice, so a chain of compactions no longer doubles (one session's grew from 8k to 303k characters over five compactions).
|
|
28
|
+
- A previous summary over 60,000 tokens (`previousSummaryLimitTokens`) skips Jev, so Pi's summary condenses the chain.
|
|
29
|
+
- Jev judges with a goal (`JEV_GOAL`, option `goal`) that keeps the tool results that set rules still in force, which it dropped without one, and the summary ends with the rule loads it summarized (skills, notebook system prompts, reads of SKILL.md, AGENTS.md and `.agents/skills/`; option `ruleLoad`, default `isRuleLoad`) for the agent to load again.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The shell hold rule lets more of the agents' own work run.
|
|
34
|
+
- The agents get a scratch dir, the config's new `scratchDir` (default `<os temp dir>/<discord.rootCommand>-scratch`, created with mode 0700 at startup; a symlink or another user's dir is refused), and their `bash` runs with `TMPDIR` pointing to it. Redirects, `tee`, and `write` and `edit` inside it run as inside the workspace; the rest of `/tmp` stays held. `AgentSessions.scratchDir` and `HoldContext.scratchDir` carry it, and the agents' prompt names it.
|
|
35
|
+
- `rm` is no longer always held: it runs when every operand resolves, after the line's literal variable assignments, `cd`, `..` and symlinks, inside the workspace or the scratch dir without being one of them. A command substitution, an unknown variable, a root itself, `/`, or no operand keeps it held.
|
|
36
|
+
- `git push` stays held by `shellHoldRule`; the new `shellHoldRuleFor({ ownPushOwners, heldPushRepos })` (type `PushPolicy`) lets a plain push to a GitHub repository of a listed owner run, unless it forces, deletes, pushes tags or a mirror, or the repository is in `heldPushRepos` or its remote cannot be read.
|
|
37
|
+
- A relative write target or `rm` operand resolves from the line's last `cd`, not always from the workspace.
|
|
38
|
+
- Value: `shellHoldRuleFor`; type: `PushPolicy`.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- pi-roundtable-coding frees a finished job's repository before delivering its report, so the turn that receives the report can ship it or start the next worker there instead of being refused with "still using".
|
|
43
|
+
|
|
8
44
|
## [0.7.15] - 2026-10-03
|
|
9
45
|
|
|
10
46
|
### Changed
|
package/docs/plugins.md
CHANGED
|
@@ -63,11 +63,19 @@ Use the kit's building blocks for a plugin that runs Pi itself, such as a coding
|
|
|
63
63
|
- Work: `promptSlot` (how a run asks the owner while it works), `workTimeout` (a time limit that does not count the time spent waiting on the owner), `runWorkerTask`, `archiveSessions`, and `approvalCard` and `canonicalJson` for the cards of held actions.
|
|
64
64
|
- Diagnostics: `scrubDiagnostic(text, max = 600)` masks credentials (URL userinfo, token shapes, secret-named assignments and JSON fields, `Authorization`/`Cookie`/`x-api-key` headers, JWTs, PEM blocks), turns control characters other than tab and newline into spaces, and cuts the result at `max` characters.
|
|
65
65
|
It scans only the first `max * 4` characters (at least 4,096), in linear time, so pass it git, gh or provider error text before showing that text to a user.
|
|
66
|
-
- Shell: `SHELL_TOOLS` and `
|
|
66
|
+
- Shell: `SHELL_TOOLS`, and `shellHoldRule` and `shellHoldRuleFor(policy)`, the hold rule that keeps risky host-shell commands behind the owner's approval; see [the shell rule](#the-shell-rule).
|
|
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
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`.
|
|
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
|
+
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
|
+
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/`).
|
|
74
|
+
`config` passes pi-jev-compaction's settings (`apiKey`, `model`, thresholds) over its config file and environment; `asker` replaces Jev's service, for tests.
|
|
75
|
+
The API key is optional: without one, `jevCompact` skips with `no_key` without calling Jev, and compaction is Pi's own summary.
|
|
76
|
+
Two adapters log each skip as `Jev leaves the compaction to Pi's summary` with `reason`, `detail` and `tokensBefore` through the `logger` they take: `jevCompactionExtension({ logger, ...options })`, the `compaction` session tool's extension, placed as `session.compaction.wrap(jevCompactionExtension({ logger }))` under the engine `JEV_COMPACTION_ENGINE` (`pi-jev-compaction`), and `jevCompactor({ logger, ...options })`, a `JevCompactor` for pi-roundtable-sandbox's `compaction` option that returns the compaction or `undefined` and logs with the `channel`.
|
|
77
|
+
A `no_key` skip is logged once per adapter, as `Jev is not configured, so compaction uses Pi's summary`, and then not again; every other skip is logged each time.
|
|
78
|
+
The types are `JevCompactInput`, `JevCompactOptions`, `JevCompactOutcome`, `JevCompactor`, `JevCompactRequest`, `JevExtensionOptions` and `JevSkipReason`.
|
|
71
79
|
- Effort: `effortJudge` picks a turn's thinking level from a message with your own brief (`EffortBrief`, `JUDGE_WORK`).
|
|
72
80
|
- Presentation and small helpers: `thinkingLine`, `zonedStamp(date, timeZone)`, `channelQueue()` (a queue of your own, so work does not wait behind a running turn), `checkRepoName` and `SKILL_LIST_TOOL` with `skillListExtension` for repositories and skills, and `searchTerms` for memory search.
|
|
73
81
|
|
|
@@ -505,6 +513,36 @@ A rule whose verdict depends on the input, such as one that holds only a `delete
|
|
|
505
513
|
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.
|
|
506
514
|
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.
|
|
507
515
|
|
|
516
|
+
#### The shell rule
|
|
517
|
+
|
|
518
|
+
`shellHoldRule` in `pi-roundtable/kit` judges the agents' `bash`, `write` and `edit` calls; the agent server links it, and a session without a workspace has no shell.
|
|
519
|
+
Its scratch roots are the shared workspace (`HoldContext.workspace`) and the scratch dir (`HoldContext.scratchDir`).
|
|
520
|
+
The scratch dir is the config's `scratchDir`, by default `<os temp dir>/<discord.rootCommand>-scratch` (such as `/tmp/roundtable-scratch`); the agent server creates it with mode 0700 at startup and refuses one that is a symlink or another user's, and the agents' `bash` runs with `TMPDIR` pointing to it, so `mktemp` and tools write there.
|
|
521
|
+
`AgentSessions.scratchDir` carries it to a runtime, and the agents' prompt tells them to write temporary files there.
|
|
522
|
+
|
|
523
|
+
- A `>`, `>>` or `tee` target, and a `write` or `edit` path, inside a scratch root run; anything else, the rest of `/tmp` included, is held.
|
|
524
|
+
- An `rm` runs when every operand resolves inside a scratch root and none is a root itself or `/`. Operands resolve after the variables assigned earlier in the same command line (`NAME=value` and `export NAME=value` with literal values; `$TMPDIR` is the scratch dir and `$HOME` the service user's home), from the directory of the last `cd`, through `..` and, for paths that exist, symlinks, so a link out of a root is held. A glob is judged by its directory part. A command substitution, an unknown variable, `~user`, an `rm` without operands, and `xargs rm` are held.
|
|
525
|
+
- `sudo`, `kill`, `dd`, `mkfs` and the other programs held whatever their arguments stay held, as do service, container, firewall and package changes, `git reset --hard`, and `gh` writes.
|
|
526
|
+
- `git push` is held, `force-pushes` for a force push and otherwise as a push to GitHub.
|
|
527
|
+
|
|
528
|
+
`shellHoldRuleFor({ ownPushOwners?, heldPushRepos? })` (its options are the type `PushPolicy`) is the same rule with plain pushes to the owner's own repositories let through; `shellHoldRule` is `shellHoldRuleFor()`, which holds every push.
|
|
529
|
+
A push runs without a hold when all of these hold:
|
|
530
|
+
|
|
531
|
+
- it does not force (`-f`, `--force*`, a `+` refspec), delete (`--delete`, `-d`, a `:ref` refspec), or push tags or more than its refs (`--tags`, `--follow-tags`, `--mirror`, `--all`, `--prune`), takes no other option than `-u`, `-q`, `-v`, `-n`, `--no-verify`, `--atomic`, `--porcelain`, `--progress` and `-o`, and names no `refs/tags/…` ref or local tag;
|
|
532
|
+
- the repository directory is known: `git -C <dir>`, else the last `cd` in the command line, else the workspace; a line with a subshell or `||` does not follow its `cd`;
|
|
533
|
+
- every push URL of the remote (the named one, default `origin`, read with `git remote get-url --push --all` with a short timeout and no shell; or a URL given in its place) is a GitHub repository, https or ssh, whose owner is in `ownPushOwners` and whose `owner/repo` is not in `heldPushRepos`.
|
|
534
|
+
|
|
535
|
+
Git's `-c`, `--git-dir` and `--work-tree`, a `GIT_*` variable before `git`, and any failure to read the remote keep the push held.
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
import { shellHoldRuleFor } from "pi-roundtable/kit";
|
|
539
|
+
|
|
540
|
+
const shell = shellHoldRuleFor({
|
|
541
|
+
ownPushOwners: ["octocat"],
|
|
542
|
+
heldPushRepos: ["octocat/deployed-app"],
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
508
546
|
### `prompt`: text added to every agent turn
|
|
509
547
|
|
|
510
548
|
Each section's `build` gets the agent, the speaker (undefined between turns), and the turn's scope.
|
|
@@ -1001,7 +1039,7 @@ The slot is a `RuntimeFactory`: `(deps: RuntimeDeps) => AgentRuntime`, called on
|
|
|
1001
1039
|
`deps` provides `logger`, `env`, `owner`, `toolTiers`, and the host's `judge`.
|
|
1002
1040
|
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.
|
|
1003
1041
|
`prompts(conversation, speaker)` gives you the owner's approval and question cards on the conversation's surface, or `undefined`.
|
|
1004
|
-
`agents` holds the agent server's per-agent settings: `workDir`, `skills(name)`, `modelOf(name)`, and `turnChannel(scope)`.
|
|
1042
|
+
`agents` holds the agent server's per-agent settings: `workDir`, `scratchDir` (the agents' shell's `TMPDIR`), `skills(name)`, `modelOf(name)`, and `turnChannel(scope)`.
|
|
1005
1043
|
`confirmations` stores held actions across restarts.
|
|
1006
1044
|
|
|
1007
1045
|
An `AgentRuntime` has these methods:
|
|
@@ -1018,7 +1056,7 @@ An `AgentRuntime` has these methods:
|
|
|
1018
1056
|
| `preflight?()` | Runs in the host's preflight, before anything starts; a throw stops the boot |
|
|
1019
1057
|
| `dispose?()` | Runs when the host stops the agent server's runtime service |
|
|
1020
1058
|
|
|
1021
|
-
A request has the turn's `channel`, `selection` (its tools), `text`, `attachments`, `speaker`,
|
|
1059
|
+
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)).
|
|
1022
1060
|
An agent's turn also has `agent`, the agent's scope, whose `session` is the conversation's key.
|
|
1023
1061
|
Other turns use `kind` to name the conversation's persona, defaulting to `"owner"` when absent.
|
|
1024
1062
|
|
|
@@ -1489,6 +1527,22 @@ Use it for tools that `defineTool` cannot express, such as a set that changes wh
|
|
|
1489
1527
|
Each extension needs a unique name; the core reserves `read-attachment`, `confirmation-gate`, `ask-user`, `self-compact-guard`, and `active-tools`.
|
|
1490
1528
|
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.
|
|
1491
1529
|
At most one plugin may add a `compaction` extension, and it must name the `engine` its compactions record.
|
|
1530
|
+
The core's Jev compactor is one such extension:
|
|
1531
|
+
|
|
1532
|
+
```ts
|
|
1533
|
+
sessionTools: [
|
|
1534
|
+
{
|
|
1535
|
+
name: "jev-compaction",
|
|
1536
|
+
phase: "compaction",
|
|
1537
|
+
engine: JEV_COMPACTION_ENGINE,
|
|
1538
|
+
snapshot: () => ({
|
|
1539
|
+
revision: 0,
|
|
1540
|
+
factory: (session) =>
|
|
1541
|
+
session.compaction.wrap(jevCompactionExtension({ logger })),
|
|
1542
|
+
}),
|
|
1543
|
+
},
|
|
1544
|
+
],
|
|
1545
|
+
```
|
|
1492
1546
|
|
|
1493
1547
|
<!-- example: examples/session-tools.ts -->
|
|
1494
1548
|
```ts
|
|
@@ -1828,6 +1882,7 @@ The agent server claims only `discord:` keys, so claims on your surface's channe
|
|
|
1828
1882
|
| `showStop(channel)` | Shows the owner a stop control until the returned function is called; using it calls `conversations.stop(channel)` | none is shown |
|
|
1829
1883
|
| `react`, `unreact` | Adds or removes the bot's reaction on a message | no marks on queued or steered messages |
|
|
1830
1884
|
| `prompts(channel, speaker?)` | The owner's way to approve a held action or answer `ask_user` inside a running turn, as `OwnerPrompts`: `confirm` and `ask` | the action is held until the owner's next message |
|
|
1885
|
+
| `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 |
|
|
1831
1886
|
|
|
1832
1887
|
The host starts each surface as `surface:<prefix>` at the contributing plugin's place in the order, before that plugin's own services.
|
|
1833
1888
|
It stops surfaces in reverse order, like other services.
|
|
@@ -1839,6 +1894,22 @@ If `prompts` is unavailable, it returns `undefined` and held actions wait for th
|
|
|
1839
1894
|
`of(channel)` returns the surface, or `undefined`.
|
|
1840
1895
|
The agent server asks the owner for approvals through `context.surfaces.prompts`, so a surface that gives `prompts` gets them in its own channels.
|
|
1841
1896
|
|
|
1897
|
+
#### Interim text: what a turn writes before its final answer
|
|
1898
|
+
|
|
1899
|
+
A model often writes text in an assistant message that then calls tools, such as a proposal before it asks `ask_user` "go with this version?".
|
|
1900
|
+
On a surface that gives `interim`, and in the agent server's channels through the agents' webhooks, the host posts that text as the turn goes, sorted in two:
|
|
1901
|
+
|
|
1902
|
+
- **Primary** text is posted as ordinary messages as soon as its assistant message ends: text of 400 characters or more, or written with a Markdown heading, list, table or code fence.
|
|
1903
|
+
- **Secondary** text, short narration between tool calls, goes to one progress message per run of tool-calling messages, in Discord's small text (`-# ` per line): each text as a line, then the tools called in the run, such as `-# bash ×3 · read · web_search` (names only). It is edited in place at most every 1.5 seconds, its last state always lands, and it stays inside 2000 characters by dropping its oldest lines behind `-# …`. A primary post or a card starts a new progress message.
|
|
1904
|
+
|
|
1905
|
+
Before any card, an `ask_user` question or an approval, the host posts the pending text and brings the progress message up to date, so what the model wrote before the card shows above it.
|
|
1906
|
+
The final reply is posted at the end as before, with its thinking line; an intermediate message is never the final one, so nothing is posted twice, and a steered run keeps its final text.
|
|
1907
|
+
A failed interim post or edit is logged and never fails the turn.
|
|
1908
|
+
Turns with nowhere to post, such as transient tasks, coding workers, and turns of a claim that passes its own `reply` to `context.turns.run`, post only their final reply.
|
|
1909
|
+
A runtime that fills [the `runtime` slot](#the-runtime-slot-replace-pi) receives the place to post as `TurnRequest.interim` and may use it or not.
|
|
1910
|
+
|
|
1911
|
+
The config's `interimText: "off"` posts only the final reply (default `"on"`), and `interimPrimaryChars` sets the length of primary text (default 400).
|
|
1912
|
+
|
|
1842
1913
|
The Discord plugin collects [slash commands](#slash-commands-commandsadd); the host doesn't compose them or pass them to surfaces on other networks.
|
|
1843
1914
|
|
|
1844
1915
|
This in-memory chat surface records everything the host asks of it.
|
|
@@ -2075,7 +2146,7 @@ It returns:
|
|
|
2075
2146
|
|---|---|
|
|
2076
2147
|
| `contribution` | What the plugin added, as the host would collect it: `tools`, `prompt`, `seeds`, `events`, `services`, `http`, and the rest |
|
|
2077
2148
|
| `tools`, `tiers` | The tool names, and the table that says what tier each needs |
|
|
2078
|
-
| `holds` | The plugin's `holdRules` chained as the host links them (`holdChain`): `holds(tool, input, { workspace? })` returns the description of a call that must be approved first, or `undefined` |
|
|
2149
|
+
| `holds` | The plugin's `holdRules` chained as the host links them (`holdChain`): `holds(tool, input, { workspace?, scratchDir? })` returns the description of a call that must be approved first, or `undefined` |
|
|
2079
2150
|
| `runTool(name, args, { speaker, channel }?)` | Runs a tool the way an agent's turn would, in the channel (default `test:1`) for the speaker, and returns the text the model reads |
|
|
2080
2151
|
| `files` | Accepted files from `runTool`, recorded as `{ channel, file: ReplyFile }`; inject a surface with `supportsFiles: true` and pass its channel to test attachment tools |
|
|
2081
2152
|
| `events` | The events the plugin itself reported through `context.events`, and those of `context.turns` |
|
|
@@ -2471,6 +2542,9 @@ Import from the entries listed below; source area files are internal.
|
|
|
2471
2542
|
| `HttpRoute` | `pi-roundtable` | type |
|
|
2472
2543
|
| `ImageDrawer` | `pi-roundtable` | type |
|
|
2473
2544
|
| `InboundMessage` | `pi-roundtable` | type |
|
|
2545
|
+
| `InterimMessage` | `pi-roundtable` | type |
|
|
2546
|
+
| `InterimPosts` | `pi-roundtable` | type |
|
|
2547
|
+
| `InterimTextMode` | `pi-roundtable` | type |
|
|
2474
2548
|
| `Judge` | `pi-roundtable` | type |
|
|
2475
2549
|
| `JudgeError` | `pi-roundtable` | value |
|
|
2476
2550
|
| `JudgeModel` | `pi-roundtable` | type |
|
|
@@ -2666,6 +2740,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2666
2740
|
| `OwnerNotifier` | `pi-roundtable/kit` | type |
|
|
2667
2741
|
| `PreviousTurn` | `pi-roundtable/kit` | type |
|
|
2668
2742
|
| `PromptSlot` | `pi-roundtable/kit` | type |
|
|
2743
|
+
| `PushPolicy` | `pi-roundtable/kit` | type |
|
|
2669
2744
|
| `SCHEDULE_TOOLS` | `pi-roundtable/kit` | value |
|
|
2670
2745
|
| `SHELL_TOOLS` | `pi-roundtable/kit` | value |
|
|
2671
2746
|
| `SKILL_LIST_TOOL` | `pi-roundtable/kit` | value |
|
|
@@ -2707,6 +2782,20 @@ Import from the entries listed below; source area files are internal.
|
|
|
2707
2782
|
| `CompactionEngine` | `pi-roundtable/kit` | type |
|
|
2708
2783
|
| `CompactionHistory` | `pi-roundtable/kit` | type |
|
|
2709
2784
|
| `LatestCompaction` | `pi-roundtable/kit` | type |
|
|
2785
|
+
| `isRuleLoad` | `pi-roundtable/kit` | value |
|
|
2786
|
+
| `JEV_COMPACTION_ENGINE` | `pi-roundtable/kit` | value |
|
|
2787
|
+
| `JEV_GOAL` | `pi-roundtable/kit` | value |
|
|
2788
|
+
| `JEV_PREVIOUS_SUMMARY_LIMIT_TOKENS` | `pi-roundtable/kit` | value |
|
|
2789
|
+
| `jevCompact` | `pi-roundtable/kit` | value |
|
|
2790
|
+
| `jevCompactionExtension` | `pi-roundtable/kit` | value |
|
|
2791
|
+
| `jevCompactor` | `pi-roundtable/kit` | value |
|
|
2792
|
+
| `JevCompactInput` | `pi-roundtable/kit` | type |
|
|
2793
|
+
| `JevCompactOptions` | `pi-roundtable/kit` | type |
|
|
2794
|
+
| `JevCompactOutcome` | `pi-roundtable/kit` | type |
|
|
2795
|
+
| `JevCompactor` | `pi-roundtable/kit` | type |
|
|
2796
|
+
| `JevCompactRequest` | `pi-roundtable/kit` | type |
|
|
2797
|
+
| `JevExtensionOptions` | `pi-roundtable/kit` | type |
|
|
2798
|
+
| `JevSkipReason` | `pi-roundtable/kit` | type |
|
|
2710
2799
|
| `isScheduleTool` | `pi-roundtable/kit` | value |
|
|
2711
2800
|
| `lastAssistant` | `pi-roundtable/kit` | value |
|
|
2712
2801
|
| `mcpAdapterExtension` | `pi-roundtable/kit` | value |
|
|
@@ -2723,6 +2812,7 @@ Import from the entries listed below; source area files are internal.
|
|
|
2723
2812
|
| `searchTerms` | `pi-roundtable/kit` | value |
|
|
2724
2813
|
| `settleTurn` | `pi-roundtable/kit` | value |
|
|
2725
2814
|
| `shellHoldRule` | `pi-roundtable/kit` | value |
|
|
2815
|
+
| `shellHoldRuleFor` | `pi-roundtable/kit` | value |
|
|
2726
2816
|
| `skillListExtension` | `pi-roundtable/kit` | value |
|
|
2727
2817
|
| `splitReply` | `pi-roundtable/kit` | value |
|
|
2728
2818
|
| `stringList` | `pi-roundtable/kit` | value |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "A plugin-driven Pi agent server for Discord",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"@earendil-works/pi-coding-agent": ">=0.99.2 <2",
|
|
62
62
|
"canvas": "3.2.3",
|
|
63
63
|
"discord.js": "14.27.0",
|
|
64
|
+
"pi-jev-compaction": "1.0.0",
|
|
64
65
|
"pi-mcp-adapter": "5.0.0",
|
|
65
66
|
"pino": "10.3.1",
|
|
66
67
|
"typebox": "1.3.34",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentRuntime, ContextUse } from "../contract/runtime.ts";
|
|
2
|
+
import type { InterimPosts } from "../domain/interim.ts";
|
|
2
3
|
import type { ThinkingSetting } from "../models.ts";
|
|
3
4
|
|
|
4
5
|
export type { ContextUse };
|
|
@@ -47,6 +48,14 @@ export interface ChannelMessage {
|
|
|
47
48
|
/** The agent server's channels as the agent team needs them. */
|
|
48
49
|
export interface AgentChannels {
|
|
49
50
|
post(channelId: string, post: AgentPost): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Where a running turn posts the text it writes before its final answer, under the same name
|
|
53
|
+
* and avatar as `post`; absent = only the final reply is posted.
|
|
54
|
+
*/
|
|
55
|
+
interim?(
|
|
56
|
+
channelId: string,
|
|
57
|
+
as: Omit<AgentPost, "thinking" | "chunks" | "files">,
|
|
58
|
+
): InterimPosts;
|
|
50
59
|
/** Creates a text channel under the category, made when missing; returns its id. */
|
|
51
60
|
createChannel(
|
|
52
61
|
name: string,
|
|
@@ -18,6 +18,8 @@ export function agentSystemPrompt(input: {
|
|
|
18
18
|
owner: OwnerIdentity;
|
|
19
19
|
/** The host account the shell and file tools run as. */
|
|
20
20
|
shellUser: string;
|
|
21
|
+
/** The scratch dir the shell's TMPDIR points to, for temporary files. */
|
|
22
|
+
scratchDir?: string;
|
|
21
23
|
group?: { group: AgentGroup; members: readonly Agent[] };
|
|
22
24
|
/** Who the turn is for; without one, or the owner, the prompt speaks to the owner. */
|
|
23
25
|
speaker?: Speaker;
|
|
@@ -35,7 +37,7 @@ export function agentSystemPrompt(input: {
|
|
|
35
37
|
`You are "${agent.displayName}" (agent name \`${agent.name}\`), one of ${o.name}'s agents in ${o.his} Discord agent server. Each agent owns one channel and one conversation; you all share ${o.his} tools and ${o.his} memory.`,
|
|
36
38
|
roleText(agent, coordinator, who),
|
|
37
39
|
`The team: agent_list shows every agent and group; agent_get and agent_update read and improve any agent's prompt, display name, model, and thinking level, yours included; agent_create adds an agent with its own channel; ${input.avatars === false ? "" : "agent_avatar redraws an avatar; "}schedule_list with agent reads another agent's schedules. A message marked as coming from another agent is that agent speaking, not ${w.name}; only ${w.name} approves held actions.`,
|
|
38
|
-
`Your shell and file tools run on ${o.his} VPS as the user ${shellUser}, in the shared workspace ${workDir}. Commands that are destructive or change the system, and writes outside the workspace, are held for ${w.his} confirmation: tell ${w.him} exactly what will run and ask ${w.him} to confirm.`,
|
|
40
|
+
`Your shell and file tools run on ${o.his} VPS as the user ${shellUser}, in the shared workspace ${workDir}.${input.scratchDir ? ` Write temporary files under the scratch dir ${input.scratchDir} ($TMPDIR), not elsewhere in /tmp.` : ""} Commands that are destructive or change the system, and writes outside the workspace${input.scratchDir ? " and the scratch dir" : ""}, are held for ${w.his} confirmation: tell ${w.him} exactly what will run and ask ${w.him} to confirm.`,
|
|
39
41
|
];
|
|
40
42
|
if (group) {
|
|
41
43
|
const others = group.members
|
|
@@ -131,6 +131,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
131
131
|
entryChannelId,
|
|
132
132
|
owner,
|
|
133
133
|
shellUser,
|
|
134
|
+
scratchDir,
|
|
134
135
|
} = this.#options;
|
|
135
136
|
const agent = store.agent(scope.name);
|
|
136
137
|
if (!agent) throw new AgentError(`no agent ${scope.name}`);
|
|
@@ -146,6 +147,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
146
147
|
workDir,
|
|
147
148
|
owner,
|
|
148
149
|
shellUser,
|
|
150
|
+
...(scratchDir ? { scratchDir } : {}),
|
|
149
151
|
avatars: this.#avatars(),
|
|
150
152
|
...(group
|
|
151
153
|
? {
|
|
@@ -35,6 +35,8 @@ export interface AgentTeamOptions
|
|
|
35
35
|
workDir: string;
|
|
36
36
|
/** The host account the agents' shell and file tools run as. */
|
|
37
37
|
shellUser: string;
|
|
38
|
+
/** The scratch dir the agents' shell writes temporary files to. */
|
|
39
|
+
scratchDir?: string;
|
|
38
40
|
/** The prompt every persona shares, the assistant's included; each agent's starts with it. */
|
|
39
41
|
sharedPrompt: string;
|
|
40
42
|
/** The shared prompt for speakers other than the owner, when `sharedPrompt` speaks to the owner. */
|
|
@@ -44,7 +44,7 @@ export interface TeamTurnsOptions {
|
|
|
44
44
|
/** Who the agents work for, as their prompts and group history name them. */
|
|
45
45
|
owner: OwnerIdentity;
|
|
46
46
|
store: PgAgentStore;
|
|
47
|
-
channels: Pick<AgentChannels, "post">;
|
|
47
|
+
channels: Pick<AgentChannels, "post" | "interim">;
|
|
48
48
|
studio: Pick<AvatarStudio, "url">;
|
|
49
49
|
/** Set once the runtime exists, which itself needs the team's tools. */
|
|
50
50
|
runtime: () => AgentTurnRunner;
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
TurnResult,
|
|
7
7
|
} from "../domain/conversation.ts";
|
|
8
8
|
import { AgentError } from "../domain/errors.ts";
|
|
9
|
+
import type { InterimPosts } from "../domain/interim.ts";
|
|
9
10
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
10
11
|
import { messages } from "../i18n/index.ts";
|
|
11
12
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
@@ -185,6 +186,7 @@ export class TeamTurns {
|
|
|
185
186
|
...(scope.group ? { group: scope.group } : {}),
|
|
186
187
|
};
|
|
187
188
|
this.#options.events?.turnStarted(turn);
|
|
189
|
+
const interim = this.#interim(postTo, agent);
|
|
188
190
|
let result: TurnResult;
|
|
189
191
|
try {
|
|
190
192
|
result = await settleTurn(
|
|
@@ -200,6 +202,7 @@ export class TeamTurns {
|
|
|
200
202
|
...(extra.interactive ? { interactive: true } : {}),
|
|
201
203
|
agent: scope,
|
|
202
204
|
speaker: chain.speaker,
|
|
205
|
+
...(interim ? { interim } : {}),
|
|
203
206
|
}),
|
|
204
207
|
),
|
|
205
208
|
"agent turn",
|
|
@@ -250,6 +253,27 @@ export class TeamTurns {
|
|
|
250
253
|
return this.#options.store.agent(agent.name) ?? agent;
|
|
251
254
|
}
|
|
252
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The turn's interim posts under the agent's name, read as each goes out so a name changed
|
|
258
|
+
* during the turn shows; none in a channel no agent or group owns any more.
|
|
259
|
+
*/
|
|
260
|
+
#interim(channel: ChannelKey, agent: Agent): InterimPosts | undefined {
|
|
261
|
+
const { store, channels, studio } = this.#options;
|
|
262
|
+
if (!channels.interim) return undefined;
|
|
263
|
+
const interim = channels.interim.bind(channels);
|
|
264
|
+
return {
|
|
265
|
+
post: async (text) => {
|
|
266
|
+
if (!channelOwner(store, channel))
|
|
267
|
+
throw new AgentError(`${channel} is archived`);
|
|
268
|
+
const as = this.#current(agent);
|
|
269
|
+
return interim(channelIdOf(channel), {
|
|
270
|
+
name: as.displayName,
|
|
271
|
+
avatarUrl: studio.url(as.avatarHash),
|
|
272
|
+
}).post(text);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
253
277
|
/**
|
|
254
278
|
* Posts under the agent's name. A channel no agent or group owns any more, archived while a
|
|
255
279
|
* turn ran, gets nothing, so its webhook is not made again.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import type { SQL } from "bun";
|
|
@@ -12,6 +13,8 @@ import { discordKey } from "../agents/team-keys.ts";
|
|
|
12
13
|
import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
|
|
13
14
|
import type { AgentRuntime, AgentSessions } from "../contract/runtime.ts";
|
|
14
15
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
16
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
17
|
+
import type { InterimTextMode } from "../domain/interim.ts";
|
|
15
18
|
import type { OwnerIdentity } from "../identity.ts";
|
|
16
19
|
import { ConfirmationJudge } from "../judging/confirmation-judge.ts";
|
|
17
20
|
import { AGENT_BRIEF, EffortJudge } from "../judging/effort-judge.ts";
|
|
@@ -53,6 +56,11 @@ export interface AgentServerOptions {
|
|
|
53
56
|
judgeThreshold: number;
|
|
54
57
|
/** The shell's shared working directory. */
|
|
55
58
|
workDir: string;
|
|
59
|
+
/**
|
|
60
|
+
* The agents' scratch dir, created with mode 0700 when the agent server starts: their shell's
|
|
61
|
+
* TMPDIR, where writes and removals run without a hold.
|
|
62
|
+
*/
|
|
63
|
+
scratchDir?: string;
|
|
56
64
|
/** The host account the agents' shell and file tools run as. */
|
|
57
65
|
shellUser: string;
|
|
58
66
|
/** The prompt every agent starts with, and the one for speakers other than the owner. */
|
|
@@ -65,6 +73,10 @@ export interface AgentServerOptions {
|
|
|
65
73
|
avatarReference: string;
|
|
66
74
|
/** Reports the process's own errors to an agent; without one nothing is reported. */
|
|
67
75
|
errorReporter?: ErrorReporter;
|
|
76
|
+
/** Whether turns post the text they write before their final answer as they go; default "on". */
|
|
77
|
+
interimText?: InterimTextMode;
|
|
78
|
+
/** An intermediate text this long or longer is posted as an ordinary message; default 400. */
|
|
79
|
+
interimPrimaryChars?: number;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
/** The name of the agent server's plugin, as `serviceStarted` events name it. */
|
|
@@ -178,6 +190,7 @@ export function agentServerPlugin(
|
|
|
178
190
|
owner: options.owner,
|
|
179
191
|
toolTiers: context.toolTiers,
|
|
180
192
|
shellUser: options.shellUser,
|
|
193
|
+
...(options.scratchDir ? { scratchDir: options.scratchDir } : {}),
|
|
181
194
|
store,
|
|
182
195
|
channels: connection.agentChannels(options.guildId),
|
|
183
196
|
studio,
|
|
@@ -227,6 +240,9 @@ export function agentServerPlugin(
|
|
|
227
240
|
});
|
|
228
241
|
const agentSessions: AgentSessions = {
|
|
229
242
|
workDir: options.workDir,
|
|
243
|
+
...(options.scratchDir
|
|
244
|
+
? { scratchDir: ensureScratchDir(options.scratchDir) }
|
|
245
|
+
: {}),
|
|
230
246
|
modelOf: (name) => built.modelOf(name),
|
|
231
247
|
skills: (name) => built.skillsOf(name),
|
|
232
248
|
turnChannel: (scope) => built.turnChannel(scope),
|
|
@@ -267,6 +283,12 @@ export function agentServerPlugin(
|
|
|
267
283
|
agents: agentSessions,
|
|
268
284
|
prompts,
|
|
269
285
|
logger,
|
|
286
|
+
...(options.interimText
|
|
287
|
+
? { interimText: options.interimText }
|
|
288
|
+
: {}),
|
|
289
|
+
...(options.interimPrimaryChars
|
|
290
|
+
? { interimPrimaryChars: options.interimPrimaryChars }
|
|
291
|
+
: {}),
|
|
270
292
|
});
|
|
271
293
|
runtime = running;
|
|
272
294
|
context.services.provide(AGENTS, {
|
|
@@ -339,3 +361,18 @@ export function agentServerPlugin(
|
|
|
339
361
|
},
|
|
340
362
|
};
|
|
341
363
|
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Creates the scratch dir for the service user alone. A shared temp dir lets anyone create the
|
|
367
|
+
* path first, so a symlink or a dir the service user does not own is refused.
|
|
368
|
+
*/
|
|
369
|
+
export function ensureScratchDir(dir: string): string {
|
|
370
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
371
|
+
const stat = lstatSync(dir);
|
|
372
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid?.())
|
|
373
|
+
throw new ConfigError(
|
|
374
|
+
`the scratch dir ${dir} is not a directory of this service's user. Remove it or set scratchDir to another path.`,
|
|
375
|
+
);
|
|
376
|
+
chmodSync(dir, 0o700);
|
|
377
|
+
return dir;
|
|
378
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import type { AgentSeed } from "../agents/agent-rules.ts";
|
|
2
4
|
import { ConfigError } from "../domain/errors.ts";
|
|
5
|
+
import type { InterimTextMode } from "../domain/interim.ts";
|
|
3
6
|
import { isLocale, type Locale } from "../i18n/index.ts";
|
|
4
7
|
import type { OwnerIdentity } from "../identity.ts";
|
|
5
8
|
import {
|
|
@@ -9,6 +12,7 @@ import {
|
|
|
9
12
|
type ThinkingLevel,
|
|
10
13
|
} from "../models.ts";
|
|
11
14
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
15
|
+
import { PRIMARY_CHARS } from "../runtime/interim-text.ts";
|
|
12
16
|
import type { Tier, TierMembers } from "../speakers.ts";
|
|
13
17
|
import {
|
|
14
18
|
bool,
|
|
@@ -103,6 +107,11 @@ export interface RoundtableConfig {
|
|
|
103
107
|
avatar?: string;
|
|
104
108
|
/** The agents' shared working directory; default `<dataDir>/work`. */
|
|
105
109
|
workDir?: string;
|
|
110
|
+
/**
|
|
111
|
+
* The agents' scratch dir: their shell runs with TMPDIR pointing to it, and writes and removals
|
|
112
|
+
* inside it run without a hold. Default `<os temp dir>/<discord.rootCommand>-scratch`.
|
|
113
|
+
*/
|
|
114
|
+
scratchDir?: string;
|
|
106
115
|
/**
|
|
107
116
|
* Where the skill registry reads built-in skills and keeps linked repositories. `false` leaves
|
|
108
117
|
* the `skills` addon out: agents carry no skills, and no skill tools exist. Stored skills stay
|
|
@@ -116,6 +125,14 @@ export interface RoundtableConfig {
|
|
|
116
125
|
memory?: boolean;
|
|
117
126
|
/** The agent that investigates the process's own errors, by name. */
|
|
118
127
|
ops?: { agent: string };
|
|
128
|
+
/**
|
|
129
|
+
* Whether a turn posts the text it writes before its final answer as it goes: long or
|
|
130
|
+
* structured text as ordinary messages, short narration and the tools called in one small
|
|
131
|
+
* progress message. Default "on"; "off" posts only the final reply.
|
|
132
|
+
*/
|
|
133
|
+
interimText?: InterimTextMode;
|
|
134
|
+
/** An intermediate text this long or longer is posted as an ordinary message; default 400. */
|
|
135
|
+
interimPrimaryChars?: number;
|
|
119
136
|
plugins?: RoundtablePlugin[];
|
|
120
137
|
}
|
|
121
138
|
|
|
@@ -191,11 +208,14 @@ const schema = shape({
|
|
|
191
208
|
}),
|
|
192
209
|
avatar: optional(text),
|
|
193
210
|
workDir: optional(text),
|
|
211
|
+
scratchDir: optional(text),
|
|
194
212
|
skills: optional(
|
|
195
213
|
orOff(shape({ builtinDir: optional(text), reposDir: optional(text) })),
|
|
196
214
|
),
|
|
197
215
|
memory: optional(bool),
|
|
198
216
|
ops: optional(shape({ agent: text })),
|
|
217
|
+
interimText: optional(oneOf<InterimTextMode>("on", "off")),
|
|
218
|
+
interimPrimaryChars: optional(integer(1, 100_000)),
|
|
199
219
|
plugins: optional(
|
|
200
220
|
list(
|
|
201
221
|
guarded("a plugin, an object with a name and a setup function", isPlugin),
|
|
@@ -243,10 +263,13 @@ export interface ResolvedConfig {
|
|
|
243
263
|
};
|
|
244
264
|
avatar?: string;
|
|
245
265
|
workDir: string;
|
|
266
|
+
scratchDir: string;
|
|
246
267
|
/** `false` when the skills addon is off. */
|
|
247
268
|
skills: false | { builtinDir?: string; reposDir?: string };
|
|
248
269
|
memory: boolean;
|
|
249
270
|
ops?: { agent: string };
|
|
271
|
+
interimText: InterimTextMode;
|
|
272
|
+
interimPrimaryChars: number;
|
|
250
273
|
plugins: RoundtablePlugin[];
|
|
251
274
|
}
|
|
252
275
|
|
|
@@ -265,6 +288,9 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
265
288
|
const name = config.name ?? "Roundtable";
|
|
266
289
|
const model = modelOf(config.model, "model");
|
|
267
290
|
const thinking = config.thinking ?? "medium";
|
|
291
|
+
const rootCommand =
|
|
292
|
+
config.discord.rootCommand ??
|
|
293
|
+
name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
268
294
|
return {
|
|
269
295
|
name,
|
|
270
296
|
owner: {
|
|
@@ -276,9 +302,7 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
276
302
|
token: config.discord.token,
|
|
277
303
|
guild: config.discord.guild,
|
|
278
304
|
entryChannel: config.discord.entryChannel,
|
|
279
|
-
rootCommand
|
|
280
|
-
config.discord.rootCommand ??
|
|
281
|
-
name.toLowerCase().replace(/[^a-z0-9-]+/g, "-"),
|
|
305
|
+
rootCommand,
|
|
282
306
|
admin: config.discord.admin ?? true,
|
|
283
307
|
...(config.discord.refusalHint === undefined
|
|
284
308
|
? {}
|
|
@@ -321,9 +345,12 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
321
345
|
},
|
|
322
346
|
...(config.avatar ? { avatar: config.avatar } : {}),
|
|
323
347
|
workDir: config.workDir ?? `${config.dataDir}/work`,
|
|
348
|
+
scratchDir: config.scratchDir ?? join(tmpdir(), `${rootCommand}-scratch`),
|
|
324
349
|
skills: config.skills ?? {},
|
|
325
350
|
memory: config.memory ?? true,
|
|
326
351
|
...(config.ops ? { ops: config.ops } : {}),
|
|
352
|
+
interimText: config.interimText ?? "on",
|
|
353
|
+
interimPrimaryChars: config.interimPrimaryChars ?? PRIMARY_CHARS,
|
|
327
354
|
plugins: config.plugins ?? [],
|
|
328
355
|
};
|
|
329
356
|
}
|
|
@@ -82,6 +82,11 @@ export interface LoadedSkill {
|
|
|
82
82
|
export interface AgentSessions {
|
|
83
83
|
/** The shell's working directory, shared by every agent; writes outside it are held. */
|
|
84
84
|
workDir: string;
|
|
85
|
+
/**
|
|
86
|
+
* The host's scratch dir: the agents' shell runs with TMPDIR pointing to it, and writes and
|
|
87
|
+
* removals inside it run without a hold, like the workspace's.
|
|
88
|
+
*/
|
|
89
|
+
scratchDir?: string;
|
|
85
90
|
/**
|
|
86
91
|
* The skills the agent carries, read at the start of every run; a change rebuilds its
|
|
87
92
|
* sessions, keeping their history.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OutboundReply } from "../domain/conversation.ts";
|
|
2
|
+
import type { InterimPosts } from "../domain/interim.ts";
|
|
2
3
|
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
3
4
|
import { PluginError } from "../errors.ts";
|
|
4
5
|
import type { ChannelKey } from "../sessions.ts";
|
|
@@ -68,6 +69,11 @@ export interface ChatSurface {
|
|
|
68
69
|
* absent, = the action is held until the owner's next message.
|
|
69
70
|
*/
|
|
70
71
|
prompts?(channel: ChannelKey, speaker?: Speaker): OwnerPrompts | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Where a running turn posts the text it writes before its final answer: ordinary messages it
|
|
74
|
+
* may edit in place. Absent, or undefined, = only the final reply is posted.
|
|
75
|
+
*/
|
|
76
|
+
interim?(channel: ChannelKey): InterimPosts | undefined;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
/**
|
|
@@ -87,4 +93,6 @@ export interface SurfacePort {
|
|
|
87
93
|
unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
88
94
|
/** The owner's prompts in the channel; undefined when its surface has none. */
|
|
89
95
|
prompts(channel: ChannelKey, speaker?: Speaker): OwnerPrompts | undefined;
|
|
96
|
+
/** The channel's interim posts; undefined when its surface has none. */
|
|
97
|
+
interim(channel: ChannelKey): InterimPosts | undefined;
|
|
90
98
|
}
|
|
@@ -211,11 +211,14 @@ export async function defineRoundtable(
|
|
|
211
211
|
thinking: config.thinking,
|
|
212
212
|
judgeThreshold: config.judge.threshold,
|
|
213
213
|
workDir: config.workDir,
|
|
214
|
+
scratchDir: config.scratchDir,
|
|
214
215
|
shellUser: userInfo().username,
|
|
215
216
|
prompts: { shared, guest },
|
|
216
217
|
avatarListener: "public",
|
|
217
218
|
avatarUrl: config.http.publicUrl,
|
|
218
219
|
avatarReference: config.avatar ?? join(ASSETS, "neutral.png"),
|
|
220
|
+
interimText: config.interimText,
|
|
221
|
+
interimPrimaryChars: config.interimPrimaryChars,
|
|
219
222
|
...(errorReporter ? { errorReporter } : {}),
|
|
220
223
|
}),
|
|
221
224
|
seedsPlugin(config.agents),
|