pi-roundtable 0.7.19 → 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 +140 -0
- package/README.md +72 -15
- package/README.zh-TW.md +65 -13
- package/docs/migrating-0.8.md +132 -0
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +853 -81
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/discord-adapter.test.ts +42 -0
- package/examples/discord-adapter.ts +26 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/echo-runtime.ts +1 -1
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.test.ts +72 -0
- package/examples/headless.ts +35 -0
- package/examples/http.test.ts +31 -1
- package/examples/http.ts +39 -0
- 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 +7 -3
- 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 +130 -5
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +28 -5
- package/src/cli/init.ts +9 -1
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +33 -2
- package/src/cli/report.ts +12 -3
- package/src/cli/templates.ts +46 -14
- 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/agent-team-fixture.ts +6 -0
- package/src/core/agents/team-turns.ts +10 -1
- package/src/core/builtin/agent-server.ts +38 -101
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +197 -43
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +120 -0
- package/src/core/config/config.ts +228 -94
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +81 -9
- package/src/core/contract/providers.ts +3 -2
- package/src/core/contract/runtime.ts +31 -11
- package/src/core/contract/surface.ts +35 -6
- package/src/core/conversations/conversation-registry.ts +56 -0
- package/src/core/conversations/conversation-store.ts +126 -0
- package/src/core/conversations/conversations-plugin.ts +29 -0
- package/src/core/define-roundtable.ts +157 -57
- 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-adapter.ts +9 -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 -86
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +10 -0
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +33 -2
- package/src/core/domain/progress.ts +10 -0
- package/src/core/events.ts +1 -0
- package/src/core/host.ts +32 -10
- package/src/core/http/listeners.ts +174 -21
- package/src/core/http/websocket.ts +316 -0
- package/src/core/i18n/cards.ts +11 -9
- 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/ops/error-reporter.ts +24 -12
- package/src/core/plugin.ts +62 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/channel-router.ts +226 -17
- package/src/core/routing/conversation-turns.ts +85 -1
- package/src/core/routing/surface-port.ts +35 -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 +32 -10
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
- package/src/core/runtime/pending-confirmation-store.ts +67 -4
- package/src/core/runtime/pi-agent-runtime.ts +143 -135
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +6 -6
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +205 -0
- 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/runtime/turn-progress.ts +116 -0
- package/src/core/services.ts +42 -2
- package/src/core/sessions.ts +30 -0
- package/src/core/shared/unix-server.ts +35 -5
- 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 +149 -38
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +460 -0
- package/src/core/testing/test-host.ts +100 -19
- 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 +10 -0
- package/src/index.ts +65 -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 +96 -21
- package/templates/.env.example +1 -1
- package/templates/adapters/web/.env.example +24 -0
- package/templates/adapters/web/README.md +22 -0
- package/templates/adapters/web/package.json.tmpl +26 -0
- package/templates/adapters/web/persona/assistant.md +2 -0
- package/templates/adapters/web/roundtable.config.ts +59 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/plugins/self-compact.ts +11 -0
- package/templates/roundtable.config.ts +14 -2
- package/src/core/domain/owner-prompts.ts +0 -45
|
@@ -1,38 +1,58 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
ExtensionFactory,
|
|
4
|
+
ModelRuntime,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
6
|
import { scheduleCommands } from "../discord/schedule-commands.ts";
|
|
5
7
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
8
|
+
import {
|
|
9
|
+
ConfigError,
|
|
10
|
+
DelegationError,
|
|
11
|
+
ScheduleError,
|
|
12
|
+
} from "../domain/errors.ts";
|
|
6
13
|
import { messages } from "../i18n/index.ts";
|
|
14
|
+
import { legacyPrincipalsOf } from "../identity/identity-view.ts";
|
|
7
15
|
import type { OwnerIdentity } from "../identity.ts";
|
|
8
16
|
import type { ModelRef, ThinkingLevel } from "../models.ts";
|
|
9
17
|
import { ConversationBackgroundTurns } from "../modules/background/background-turns.ts";
|
|
18
|
+
import {
|
|
19
|
+
type PerPrincipalLimits,
|
|
20
|
+
personalTarget,
|
|
21
|
+
} from "../modules/background/personal-target.ts";
|
|
10
22
|
import { delegateExtension } from "../modules/delegation/delegate.ts";
|
|
11
23
|
import {
|
|
12
24
|
DefaultDelegator,
|
|
13
25
|
type DelegationWorker,
|
|
14
26
|
} from "../modules/delegation/delegator.ts";
|
|
15
27
|
import { WebResearchWorker } from "../modules/delegation/web-research-worker.ts";
|
|
16
|
-
import {
|
|
28
|
+
import { sessionNotify } from "../modules/notify/session-notify.ts";
|
|
17
29
|
import { precheckScriptHoldRule } from "../modules/schedules/precheck-tools.ts";
|
|
18
30
|
import { Scheduler } from "../modules/schedules/scheduler.ts";
|
|
19
31
|
import { schedulesExtension } from "../modules/schedules/schedules.ts";
|
|
32
|
+
import type { ErrorReporter } from "../ops/error-reporter.ts";
|
|
20
33
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
34
|
+
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
21
35
|
import {
|
|
22
36
|
AGENTS,
|
|
23
37
|
BACKGROUND_TURNS,
|
|
24
38
|
DELEGATION,
|
|
39
|
+
IDENTITY,
|
|
25
40
|
PRECHECKS,
|
|
26
41
|
SCHEDULES,
|
|
27
42
|
} from "../services.ts";
|
|
28
43
|
import type { SessionContext } from "../sessions.ts";
|
|
29
44
|
import { DELEGATE_TOOL } from "../shared/delegate-tool.ts";
|
|
30
45
|
import { SCHEDULE_TOOLS } from "../shared/schedule-tools.ts";
|
|
31
|
-
import {
|
|
46
|
+
import type { Tier } from "../speakers.ts";
|
|
32
47
|
import { DISCORD } from "./discord.ts";
|
|
33
48
|
import { fixed } from "./session-tool.ts";
|
|
34
49
|
|
|
35
50
|
export interface ModulesOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Reports the process's own errors to a conversation, connected when the modules start; one
|
|
53
|
+
* that reports to an agent is the agent server's to connect.
|
|
54
|
+
*/
|
|
55
|
+
errorReporter?: ErrorReporter;
|
|
36
56
|
/** Who the tools serve in the owner's own sessions. */
|
|
37
57
|
owner: OwnerIdentity & { id: string };
|
|
38
58
|
/** The name a turn the process itself starts is written by. */
|
|
@@ -41,6 +61,11 @@ export interface ModulesOptions {
|
|
|
41
61
|
modelRuntime: ModelRuntime;
|
|
42
62
|
agentDir: string;
|
|
43
63
|
dataDir: string;
|
|
64
|
+
/**
|
|
65
|
+
* Each person's limits across their conversations, which the personal target holds them to;
|
|
66
|
+
* unset, only each conversation's, as in 0.8.
|
|
67
|
+
*/
|
|
68
|
+
perPrincipal?: PerPrincipalLimits;
|
|
44
69
|
/** The model that runs delegated tasks, or a worker of your own that replaces it. */
|
|
45
70
|
delegation: {
|
|
46
71
|
model: ModelRef;
|
|
@@ -64,20 +89,24 @@ const MODULE_TIERS: Readonly<Record<string, Tier>> = {
|
|
|
64
89
|
};
|
|
65
90
|
|
|
66
91
|
/**
|
|
67
|
-
* The
|
|
68
|
-
*
|
|
92
|
+
* The modules: notifications, schedules, and delegated tasks, each a session tool, and the turns
|
|
93
|
+
* nobody wrote. The delegator's running jobs join the shutdown drain. `notify` sends through the
|
|
94
|
+
* plugins' direct channels, so with none there is no notify. A conversation no chat surface carries
|
|
95
|
+
* schedules and delegates only when recorded private, into its creator's reachable,
|
|
96
|
+
* background-capable direct channel. It
|
|
97
|
+
* contributes `PERSONAL_TARGET`, with the per-person limits given, whose turns the schedules and
|
|
98
|
+
* delegated reports are, and a session whose conversation's claim takes no background turns gets
|
|
99
|
+
* neither tool.
|
|
69
100
|
*/
|
|
70
101
|
export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
71
102
|
const { owner } = options;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
surfaces: SurfacePort,
|
|
76
|
-
ownerChannel: () => Promise<ChannelKey>,
|
|
77
|
-
) => (surfaces.of(channel) ? channel : ownerChannel());
|
|
103
|
+
const personal = personalTarget(options.perPrincipal);
|
|
104
|
+
// Set up once the modules set up; the preflight reads it after every plugin linked.
|
|
105
|
+
let reportsReach: (() => void) | undefined;
|
|
78
106
|
return {
|
|
79
107
|
name: "modules",
|
|
80
108
|
provides: [BACKGROUND_TURNS, DELEGATION],
|
|
109
|
+
preflight: () => reportsReach?.(),
|
|
81
110
|
setup: ({
|
|
82
111
|
conversations,
|
|
83
112
|
services,
|
|
@@ -85,20 +114,44 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
85
114
|
surfaces,
|
|
86
115
|
sessions,
|
|
87
116
|
toolTiers,
|
|
117
|
+
directChannels,
|
|
88
118
|
}) => {
|
|
89
119
|
const schedules = services.get(SCHEDULES);
|
|
90
120
|
// Absent when a plugin list leaves the prechecks plugin out: then none can be attached.
|
|
91
121
|
const prechecks = services.find(PRECHECKS);
|
|
92
|
-
|
|
93
|
-
const
|
|
122
|
+
// Absent on a host without Discord: then there are no threads.
|
|
123
|
+
const discord = services.find(DISCORD);
|
|
94
124
|
// Another agent's channel, for schedule_list; asked when a tool runs, after the agent server set up.
|
|
95
125
|
const agentChannelOf = (agent: string) =>
|
|
96
126
|
services.get(AGENTS).team.channelOf(agent);
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
// A conversation no chat surface carries keeps its runs in the creator's direct messages, where
|
|
128
|
+
// a claim must take them; anyone's elsewhere would run in a conversation that is not theirs.
|
|
129
|
+
const channelFor = async (
|
|
130
|
+
channel: ChannelKey,
|
|
131
|
+
principalId: string,
|
|
132
|
+
refused: (message: string) => Error,
|
|
133
|
+
) => {
|
|
134
|
+
if (surfaces.of(channel)) return channel;
|
|
135
|
+
const reached = await directChannels.reach(principalId);
|
|
136
|
+
if (!reached)
|
|
137
|
+
throw refused(
|
|
138
|
+
"this conversation has no chat surface to post a run in, and you have no direct channel on this host to post it in instead",
|
|
139
|
+
);
|
|
140
|
+
if (!conversations.takesBackground(reached.channel))
|
|
141
|
+
throw refused(
|
|
142
|
+
`this conversation has no chat surface to post a run in, and nothing here takes background turns in your ${reached.provider.name} direct messages, where it would go instead`,
|
|
143
|
+
);
|
|
144
|
+
return reached.channel;
|
|
145
|
+
};
|
|
146
|
+
const scheduleChannelFor = (channel: ChannelKey, principalId: string) =>
|
|
147
|
+
channelFor(channel, principalId, (m) => new ScheduleError(m));
|
|
148
|
+
const delegateChannelFor = (channel: ChannelKey, principalId: string) =>
|
|
149
|
+
channelFor(channel, principalId, (m) => new DelegationError(m));
|
|
150
|
+
const identity = services.find(IDENTITY);
|
|
99
151
|
const background = new ConversationBackgroundTurns({
|
|
100
152
|
conversations,
|
|
101
153
|
system: { id: "assistant", name: options.assistant },
|
|
154
|
+
...(identity ? { principalOf: legacyPrincipalsOf(identity) } : {}),
|
|
102
155
|
logger,
|
|
103
156
|
});
|
|
104
157
|
const delegator = new DefaultDelegator({
|
|
@@ -113,17 +166,106 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
113
166
|
}),
|
|
114
167
|
targets: (name) => conversations.target(name),
|
|
115
168
|
deliver: (job, outcome) => background.runDelegated(job, outcome),
|
|
116
|
-
threads: discord.threads,
|
|
169
|
+
...(discord ? { threads: discord.threads } : {}),
|
|
117
170
|
logger,
|
|
118
171
|
});
|
|
119
172
|
services.provide(BACKGROUND_TURNS, background);
|
|
120
173
|
services.provide(DELEGATION, delegator);
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
174
|
+
// Schedules and delegated reports run as background turns in the conversation, or, for one
|
|
175
|
+
// no chat surface carries, in the creator's direct messages; where no claim would take them,
|
|
176
|
+
// none could ever start, so the session gets no tools. Read when a session is made, after
|
|
177
|
+
// every plugin set up: a conversation without a chat surface has them only when it is
|
|
178
|
+
// recorded private to someone a direct channel knows. Creation checks the speaker's own
|
|
179
|
+
// destination again.
|
|
180
|
+
const offered = (
|
|
181
|
+
session: SessionContext,
|
|
182
|
+
extension: () => ExtensionFactory,
|
|
183
|
+
): ExtensionFactory | null => {
|
|
184
|
+
const home = session.homeChannel;
|
|
185
|
+
if (surfaces.of(home))
|
|
186
|
+
return conversations.takesBackground(home) ? extension() : null;
|
|
187
|
+
if (directChannels.providers().length === 0) return null;
|
|
188
|
+
// Only a conversation recorded private has a person to run its work for: a session
|
|
189
|
+
// outlives the turn it is built in, so whoever speaks then decides nothing.
|
|
190
|
+
const { conversation } = session;
|
|
191
|
+
if (session.agent || conversation.visibility !== "private") return null;
|
|
192
|
+
const own = conversation.principalId;
|
|
193
|
+
return async (pi) => {
|
|
194
|
+
// Only whether a direct channel knows them, without the network; the conversation there,
|
|
195
|
+
// and whether a claim takes its turns, are checked when a tool runs.
|
|
196
|
+
try {
|
|
197
|
+
if (!(await directChannels.known(own))) return;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.warn(
|
|
200
|
+
{ channel: home, err: error },
|
|
201
|
+
"could not tell whether the conversation's person has a direct channel",
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await extension()(pi);
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
// Whose a conversation is, as its session was made: an agent's serves everyone; another's
|
|
209
|
+
// schedules are its person's in a private one, and the speaker's own in one no chat surface
|
|
210
|
+
// carries, where its schedules are kept in a conversation it is not.
|
|
211
|
+
const visibility =
|
|
212
|
+
(session: SessionContext) =>
|
|
213
|
+
async (): Promise<"private" | "shared"> => {
|
|
214
|
+
if (session.agent) return "shared";
|
|
215
|
+
if (session.conversation.visibility === "private") return "private";
|
|
216
|
+
return surfaces.of(session.homeChannel) ? "shared" : "private";
|
|
217
|
+
};
|
|
218
|
+
const principalOf = identity ? legacyPrincipalsOf(identity) : undefined;
|
|
219
|
+
// A reporter for an ops agent is the agent server's to connect.
|
|
220
|
+
const reporter =
|
|
221
|
+
options.errorReporter &&
|
|
222
|
+
"conversation" in options.errorReporter.destination
|
|
223
|
+
? options.errorReporter
|
|
224
|
+
: undefined;
|
|
225
|
+
if (reporter && "conversation" in reporter.destination) {
|
|
226
|
+
const channel = reporter.destination.conversation;
|
|
227
|
+
// A report that no surface can post, or that no conversation owns, would only be logged;
|
|
228
|
+
// its report turn is the owner's background turn, which runs only where a claim takes one.
|
|
229
|
+
reportsReach = () => {
|
|
230
|
+
if (!surfaces.of(channel))
|
|
231
|
+
throw new ConfigError(
|
|
232
|
+
`config ops.conversation: no chat surface serves ${JSON.stringify(channel)}, so its error reports could never be posted. Name a conversation of a surface a plugin brings, or leave ops out.`,
|
|
233
|
+
);
|
|
234
|
+
if (!conversations.owns(channel))
|
|
235
|
+
throw new ConfigError(
|
|
236
|
+
`config ops.conversation: no plugin's conversations own ${JSON.stringify(channel)}, so nothing answers its error reports. Name a conversation a plugin's claim owns, or leave ops out.`,
|
|
237
|
+
);
|
|
238
|
+
if (!conversations.takesBackground(channel))
|
|
239
|
+
throw new ConfigError(
|
|
240
|
+
`config ops.conversation: the claim that owns ${JSON.stringify(channel)} takes no background turns, so nothing answers its error reports. Name a conversation whose claim takes background turns, or leave ops out.`,
|
|
241
|
+
);
|
|
242
|
+
// A report turn is the system principal's, which a conversation private to its person refuses.
|
|
243
|
+
if (!conversations.takesSystemReports(channel))
|
|
244
|
+
throw new ConfigError(
|
|
245
|
+
`config ops.conversation: the claim that owns ${JSON.stringify(channel)} keeps its conversations private to their people, so it takes no system error reports and each would only be logged. Name a conversation of a surface whose conversations are shared, such as a Discord channel, or report to an agent with ops.agent.`,
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
}
|
|
124
249
|
return {
|
|
125
250
|
services: [
|
|
126
251
|
{ name: "delegator", busy: () => delegator.runningChannels() },
|
|
252
|
+
...(reporter
|
|
253
|
+
? [
|
|
254
|
+
{
|
|
255
|
+
name: "error-reports",
|
|
256
|
+
start: () =>
|
|
257
|
+
reporter.connect({
|
|
258
|
+
post: (channel, text) =>
|
|
259
|
+
surfaces.sendReply(channel, {
|
|
260
|
+
chunks: splitReply(text),
|
|
261
|
+
}),
|
|
262
|
+
turn: (channel, text) =>
|
|
263
|
+
background.runErrorReport(channel, text),
|
|
264
|
+
logger,
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
: []),
|
|
127
269
|
],
|
|
128
270
|
// Saving a precheck script that calls a held tool waits for the owner, as the call would.
|
|
129
271
|
holdRules: [
|
|
@@ -134,37 +276,49 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
134
276
|
}),
|
|
135
277
|
],
|
|
136
278
|
sessionTools: [
|
|
137
|
-
fixed(
|
|
279
|
+
fixed(
|
|
280
|
+
"notify",
|
|
281
|
+
sessionNotify({
|
|
282
|
+
directChannels,
|
|
283
|
+
owner,
|
|
284
|
+
logger,
|
|
285
|
+
onlyOwnerNotified: async () =>
|
|
286
|
+
toolTiers.minTier("notify") === "owner" &&
|
|
287
|
+
(identity ? (await identity.owners()).length <= 1 : true),
|
|
288
|
+
}),
|
|
289
|
+
),
|
|
138
290
|
fixed("schedules", (session) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
291
|
+
offered(session, () =>
|
|
292
|
+
schedulesExtension(
|
|
293
|
+
{
|
|
294
|
+
store: schedules,
|
|
295
|
+
channelFor: scheduleChannelFor,
|
|
296
|
+
...(prechecks ? { prechecks } : {}),
|
|
297
|
+
holds: () => sessions().holds,
|
|
298
|
+
visibility: visibility(session),
|
|
299
|
+
...(principalOf ? { principalOf } : {}),
|
|
300
|
+
target: personal,
|
|
301
|
+
},
|
|
302
|
+
session.homeChannel,
|
|
303
|
+
session.agent ? agentChannelOf : undefined,
|
|
304
|
+
session.speaker,
|
|
305
|
+
),
|
|
151
306
|
),
|
|
152
307
|
),
|
|
153
308
|
fixed("delegate", (session) =>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
delegator,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
served(session),
|
|
162
|
-
session.turnChannel,
|
|
163
|
-
session.speaker,
|
|
309
|
+
offered(session, () =>
|
|
310
|
+
delegateExtension(
|
|
311
|
+
{ delegator, channelFor: delegateChannelFor },
|
|
312
|
+
session.homeChannel,
|
|
313
|
+
session.turnChannel,
|
|
314
|
+
session.speaker,
|
|
315
|
+
),
|
|
164
316
|
),
|
|
165
317
|
),
|
|
166
318
|
],
|
|
167
319
|
toolTiers: MODULE_TIERS,
|
|
320
|
+
// Every person's conversations where a claim takes background turns are its own.
|
|
321
|
+
backgroundTargets: [personal],
|
|
168
322
|
};
|
|
169
323
|
},
|
|
170
324
|
};
|
|
@@ -4,9 +4,10 @@ import { PgMemoryStore } from "../modules/memory/owner-memory-store.ts";
|
|
|
4
4
|
import { memoryPrecheckRegistry } from "../modules/schedules/prechecks.ts";
|
|
5
5
|
import { PgScheduleStore } from "../modules/schedules/schedule-store.ts";
|
|
6
6
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
7
|
+
import { memoryReader } from "../runtime/session-conversation.ts";
|
|
7
8
|
import { MEMORY, type MemoryStore, PRECHECKS, SCHEDULES } from "../services.ts";
|
|
8
|
-
import type { SessionTool } from "../sessions.ts";
|
|
9
|
-
import {
|
|
9
|
+
import type { SessionContext, SessionTool } from "../sessions.ts";
|
|
10
|
+
import type { Tier } from "../speakers.ts";
|
|
10
11
|
import { fixed } from "./session-tool.ts";
|
|
11
12
|
|
|
12
13
|
export interface MemoryOptions {
|
|
@@ -21,29 +22,47 @@ export const MEMORY_TIERS: Readonly<Record<string, Tier>> = {
|
|
|
21
22
|
memory_remove: "member",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Whose memory a session's running turn reads: a private conversation's person's, whoever speaks
|
|
27
|
+
* in it, and in a shared one each turn's speaker's; never by tier, so a second owner reads their
|
|
28
|
+
* own. The host's own turns read no one's in a shared conversation.
|
|
29
|
+
*/
|
|
30
|
+
function memoryOwner(
|
|
31
|
+
session: SessionContext,
|
|
32
|
+
): { principalId: string; name: string } | undefined {
|
|
33
|
+
if (session.memory === "none") return undefined;
|
|
34
|
+
const speaker = session.speaker();
|
|
35
|
+
const { conversation } = session;
|
|
36
|
+
const principalId = memoryReader(conversation, speaker);
|
|
37
|
+
if (principalId === undefined) return undefined;
|
|
38
|
+
// A principal 0.8 left is named by their id until someone names them; their speaker has a name.
|
|
39
|
+
const principal =
|
|
40
|
+
conversation.visibility === "private" ? conversation.principal : undefined;
|
|
41
|
+
const named =
|
|
42
|
+
principal && principal.displayName !== principal.id
|
|
43
|
+
? principal.displayName
|
|
44
|
+
: undefined;
|
|
45
|
+
const name =
|
|
46
|
+
named ??
|
|
47
|
+
(speaker?.principalId === principalId ? speaker.name : principalId);
|
|
48
|
+
return { principalId, name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The memory tools and prompt block of every conversation, over the given store; none for a persona without memory. */
|
|
25
52
|
export function memorySessionTool(
|
|
26
53
|
memory: MemoryStore,
|
|
27
54
|
owner: MemoryOptions["owner"],
|
|
28
55
|
): SessionTool {
|
|
29
|
-
return fixed("owner-memory", (session) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// memory; an owner-tier speaker, such as remote MCP, speaks as the owner.
|
|
40
|
-
const speaker = () => {
|
|
41
|
-
const current = session.speaker();
|
|
42
|
-
return current?.tier === "owner" ? undefined : current;
|
|
43
|
-
};
|
|
44
|
-
// Its tools keep the owner's words, so the owner's own chats read as before.
|
|
45
|
-
return ownerMemoryExtension(memory, owner.id, owner, speaker, false);
|
|
46
|
-
});
|
|
56
|
+
return fixed("owner-memory", (session) =>
|
|
57
|
+
session.memory === "none"
|
|
58
|
+
? null
|
|
59
|
+
: ownerMemoryExtension(memory, {
|
|
60
|
+
ownerId: owner.id,
|
|
61
|
+
addressee: session.addressee,
|
|
62
|
+
describesSpeakers: session.conversation.visibility === "shared",
|
|
63
|
+
whose: () => memoryOwner(session),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
/**
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
+
import {
|
|
3
|
+
type AccessRules,
|
|
4
|
+
type AccessTier,
|
|
5
|
+
checkAccessRules,
|
|
6
|
+
rulesOfSpeakerMap,
|
|
7
|
+
} from "../identity/access-policy.ts";
|
|
8
|
+
import { parseIdentity } from "../identity/actor-facts.ts";
|
|
9
|
+
import { PRONOUNS } from "../identity.ts";
|
|
10
|
+
import type { Logger } from "../log.ts";
|
|
11
|
+
import type { TierMembers } from "../speakers.ts";
|
|
12
|
+
import type { Pronouns, ResolvedConfig, RoundtableConfig } from "./config.ts";
|
|
13
|
+
import {
|
|
14
|
+
type Field,
|
|
15
|
+
guarded,
|
|
16
|
+
integer,
|
|
17
|
+
list,
|
|
18
|
+
oneOf,
|
|
19
|
+
optional,
|
|
20
|
+
shape,
|
|
21
|
+
text,
|
|
22
|
+
} from "./schema.ts";
|
|
23
|
+
|
|
24
|
+
/** Who holds a tier besides the owners: identities, roles written `<surface>:role:<name>`, or everyone, on every surface or on those listed. */
|
|
25
|
+
export interface AccessTierConfig {
|
|
26
|
+
identities?: readonly string[];
|
|
27
|
+
roles?: readonly string[];
|
|
28
|
+
everyone?: boolean | readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One owner: their name and pronouns, their principal, and the identities that are theirs. */
|
|
32
|
+
export interface AccessOwnerConfig {
|
|
33
|
+
name: string;
|
|
34
|
+
pronouns?: Pronouns;
|
|
35
|
+
/**
|
|
36
|
+
* Their principal's id. Required for the first owner, the primary one: give the old
|
|
37
|
+
* `owner.id`, which `roundtable upgrade` writes, or an id of your choosing, such as their
|
|
38
|
+
* Discord user id. Another owner without one gets a new `p_` principal at the first boot.
|
|
39
|
+
*/
|
|
40
|
+
principal?: string;
|
|
41
|
+
/** `<provider>:<subject>` strings, such as `discord:<user id>` or `token:remote-mcp`; `roundtable principal list` prints them. */
|
|
42
|
+
identities?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Who the host serves, and at which tier: the owners, then admins and members by identity, role, or everyone. */
|
|
46
|
+
export interface AccessConfig {
|
|
47
|
+
/** The owners, the first of them the primary owner, whose Discord identity the Discord parts act for until 0.10. */
|
|
48
|
+
owners: readonly AccessOwnerConfig[];
|
|
49
|
+
admins?: AccessTierConfig;
|
|
50
|
+
members?: AccessTierConfig;
|
|
51
|
+
/** `admitted` (default): someone the rules admit gets a principal at first contact, and a 0.8 speaker claims theirs; `linked`: only identities linked with the CLI, or the owners' configured ones, are served, and no one claims a 0.8 principal. */
|
|
52
|
+
provisioning?: "admitted" | "linked";
|
|
53
|
+
/** How many days someone whose tier comes only from the rules may go unseen before their background turns are refused; default 30. */
|
|
54
|
+
backgroundStaleDays?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const everyone: Field<boolean | string[]> = guarded(
|
|
58
|
+
"true, false, or a list of surfaces",
|
|
59
|
+
(value): value is boolean | string[] =>
|
|
60
|
+
typeof value === "boolean" ||
|
|
61
|
+
(Array.isArray(value) &&
|
|
62
|
+
value.every((item) => typeof item === "string" && item.trim() !== "")),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const tierShape = shape({
|
|
66
|
+
identities: optional(list(text)),
|
|
67
|
+
roles: optional(list(text)),
|
|
68
|
+
everyone: optional(everyone),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** The `access` key's shape. */
|
|
72
|
+
export const accessShape = shape({
|
|
73
|
+
owners: list(
|
|
74
|
+
shape({
|
|
75
|
+
name: text,
|
|
76
|
+
pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
|
|
77
|
+
principal: optional(text),
|
|
78
|
+
identities: optional(list(text)),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
admins: optional(tierShape),
|
|
82
|
+
members: optional(tierShape),
|
|
83
|
+
provisioning: optional(oneOf<"admitted" | "linked">("admitted", "linked")),
|
|
84
|
+
backgroundStaleDays: optional(integer(1, 36_500)),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/** The days an unseen principal's background turns keep their last tier, by default. */
|
|
88
|
+
export const BACKGROUND_STALE_DAYS = 30;
|
|
89
|
+
|
|
90
|
+
/** The deprecation the 0.8 `owner` and `speakers` print, once per host logger. */
|
|
91
|
+
export const LEGACY_ACCESS =
|
|
92
|
+
"config owner and speakers are deprecated and go away in 0.10: write access: { owners, admins, members } instead; roundtable upgrade rewrites them";
|
|
93
|
+
|
|
94
|
+
function tierOf(config: AccessTierConfig | undefined): AccessTier | undefined {
|
|
95
|
+
if (!config) return undefined;
|
|
96
|
+
return {
|
|
97
|
+
...(config.identities ? { identities: config.identities } : {}),
|
|
98
|
+
...(config.roles ? { roles: config.roles } : {}),
|
|
99
|
+
...(config.everyone === undefined ? {} : { everyone: config.everyone }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The rules an `access` configuration means, checked. */
|
|
104
|
+
export function rulesOfAccess(config: AccessConfig): AccessRules {
|
|
105
|
+
if (config.owners.length === 0)
|
|
106
|
+
throw new ConfigError(
|
|
107
|
+
"config access.owners: expected at least one owner. Name the person who runs this host.",
|
|
108
|
+
);
|
|
109
|
+
const [primary] = config.owners;
|
|
110
|
+
if (primary?.principal === undefined)
|
|
111
|
+
throw new ConfigError(
|
|
112
|
+
"config access.owners[0].principal: required for the primary owner in 0.9, expected their principal id. Give the old owner.id (roundtable upgrade writes it), or an id of your choosing, such as their Discord user id.",
|
|
113
|
+
);
|
|
114
|
+
const admins = tierOf(config.admins);
|
|
115
|
+
const members = tierOf(config.members);
|
|
116
|
+
return checkAccessRules({
|
|
117
|
+
owners: config.owners.map((owner) => ({
|
|
118
|
+
name: owner.name,
|
|
119
|
+
...(owner.pronouns ? { pronouns: owner.pronouns } : {}),
|
|
120
|
+
...(owner.principal === undefined ? {} : { principal: owner.principal }),
|
|
121
|
+
identities: owner.identities ?? [],
|
|
122
|
+
})),
|
|
123
|
+
...(admins ? { admins } : {}),
|
|
124
|
+
...(members ? { members } : {}),
|
|
125
|
+
provisioning: config.provisioning ?? "admitted",
|
|
126
|
+
backgroundStaleDays: config.backgroundStaleDays ?? BACKGROUND_STALE_DAYS,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The rules the 0.8 `owner` and `speakers` mean: the owner as the primary owner, their principal
|
|
132
|
+
* the old `owner.id`, which is also their Discord identity on a host with Discord, and the tiers
|
|
133
|
+
* by Discord user and role ids, with `everyone` meaning everyone on Discord, the one surface it
|
|
134
|
+
* reached in 0.8.
|
|
135
|
+
*/
|
|
136
|
+
export function rulesOfLegacy(
|
|
137
|
+
owner: { id: string; name: string; pronouns?: Pronouns },
|
|
138
|
+
speakers: { admins?: TierMembers; members?: TierMembers } = {},
|
|
139
|
+
withDiscord = true,
|
|
140
|
+
): AccessRules {
|
|
141
|
+
const map = rulesOfSpeakerMap({
|
|
142
|
+
owners: [owner.id],
|
|
143
|
+
...(speakers.admins ? { admins: speakers.admins } : {}),
|
|
144
|
+
...(speakers.members ? { members: speakers.members } : {}),
|
|
145
|
+
});
|
|
146
|
+
return checkAccessRules({
|
|
147
|
+
...map,
|
|
148
|
+
owners: [
|
|
149
|
+
{
|
|
150
|
+
name: owner.name,
|
|
151
|
+
...(owner.pronouns ? { pronouns: owner.pronouns } : {}),
|
|
152
|
+
principal: owner.id,
|
|
153
|
+
identities: withDiscord ? [`discord:${owner.id}`] : [],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
provisioning: "admitted",
|
|
157
|
+
backgroundStaleDays: BACKGROUND_STALE_DAYS,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The Discord user id among an owner's identities, if they list one. */
|
|
162
|
+
export function discordIdOf(identities: readonly string[]): string | undefined {
|
|
163
|
+
for (const identity of identities) {
|
|
164
|
+
const ref = parseIdentity(identity);
|
|
165
|
+
if (ref?.provider === "discord") return ref.subject;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const warned = new WeakMap<Logger, Set<string>>();
|
|
171
|
+
|
|
172
|
+
/** Logs each deprecation once per logger, so a host warns once however often it is defined. */
|
|
173
|
+
export function warnDeprecations(
|
|
174
|
+
logger: Logger,
|
|
175
|
+
deprecations: readonly string[],
|
|
176
|
+
): void {
|
|
177
|
+
let seen = warned.get(logger);
|
|
178
|
+
if (!seen) {
|
|
179
|
+
seen = new Set();
|
|
180
|
+
warned.set(logger, seen);
|
|
181
|
+
}
|
|
182
|
+
for (const message of deprecations) {
|
|
183
|
+
if (seen.has(message)) continue;
|
|
184
|
+
seen.add(message);
|
|
185
|
+
logger.warn(`deprecated: ${message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The access rules and the primary owner, from `access` or the deprecated `owner` and `speakers`. */
|
|
190
|
+
export function accessOf(
|
|
191
|
+
config: Pick<RoundtableConfig, "owner" | "speakers" | "access">,
|
|
192
|
+
withDiscord: boolean,
|
|
193
|
+
): Pick<ResolvedConfig, "access" | "primaryOwner" | "deprecations"> {
|
|
194
|
+
const { owner, speakers, access } = config;
|
|
195
|
+
if (access && (owner || speakers))
|
|
196
|
+
throw new ConfigError(
|
|
197
|
+
"config access: write the owners in access or in owner and speakers, not both. Remove owner and speakers; roundtable upgrade rewrites them as access.",
|
|
198
|
+
);
|
|
199
|
+
if (!access && !owner)
|
|
200
|
+
throw new ConfigError(
|
|
201
|
+
'config access: required, expected who runs this host, such as access: { owners: [{ name: "Ada", principal: "<id>", identities: ["discord:<user id>"] }] }. Add it to roundtable.config.ts.',
|
|
202
|
+
);
|
|
203
|
+
const rules = access
|
|
204
|
+
? rulesOfAccess(access)
|
|
205
|
+
: rulesOfLegacy(
|
|
206
|
+
// The schema checked that one of the two is here.
|
|
207
|
+
owner as NonNullable<typeof owner>,
|
|
208
|
+
speakers,
|
|
209
|
+
withDiscord,
|
|
210
|
+
);
|
|
211
|
+
const [primary] = rules.owners;
|
|
212
|
+
if (primary?.principal === undefined)
|
|
213
|
+
throw new ConfigError("config access.owners[0].principal: required");
|
|
214
|
+
const discordId = discordIdOf(primary.identities);
|
|
215
|
+
if (withDiscord && discordId === undefined)
|
|
216
|
+
throw new ConfigError(
|
|
217
|
+
'config access.owners[0].identities: the primary owner needs a discord:<user id> identity while Discord is configured; the Discord parts act for them until 0.10. Add it, such as identities: ["discord:<your user id>"].',
|
|
218
|
+
);
|
|
219
|
+
return {
|
|
220
|
+
access: rules,
|
|
221
|
+
primaryOwner: {
|
|
222
|
+
id: primary.principal,
|
|
223
|
+
name: primary.name,
|
|
224
|
+
pronouns: PRONOUNS[primary.pronouns ?? "they"],
|
|
225
|
+
...(discordId === undefined ? {} : { discordId }),
|
|
226
|
+
},
|
|
227
|
+
deprecations: access ? [] : [LEGACY_ACCESS],
|
|
228
|
+
};
|
|
229
|
+
}
|