pi-roundtable 0.7.18 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +44 -11
- package/README.zh-TW.md +44 -9
- package/docs/migrating-0.8.md +132 -0
- package/docs/plugins.md +268 -23
- package/examples/discord-adapter.test.ts +42 -0
- package/examples/discord-adapter.ts +26 -0
- package/examples/echo-runtime.ts +1 -1
- package/examples/headless.test.ts +72 -0
- package/examples/headless.ts +31 -0
- package/examples/http.test.ts +31 -1
- package/examples/http.ts +39 -0
- package/package.json +3 -2
- package/src/cli/cli.ts +15 -4
- package/src/cli/doctor.ts +21 -5
- package/src/cli/init.ts +9 -1
- package/src/cli/project.ts +28 -2
- package/src/cli/templates.ts +46 -14
- package/src/core/agents/agent-team-fixture.ts +6 -0
- package/src/core/agents/team-turns.ts +6 -1
- package/src/core/builtin/agent-server.ts +30 -95
- package/src/core/builtin/modules.ts +133 -34
- package/src/core/builtin/stores.ts +29 -14
- package/src/core/config/adapters.ts +57 -0
- package/src/core/config/config.ts +174 -77
- package/src/core/contract/channels.ts +4 -0
- package/src/core/contract/providers.ts +3 -2
- package/src/core/contract/runtime.ts +8 -4
- package/src/core/contract/surface.ts +13 -0
- package/src/core/conversations/conversation-registry.ts +42 -0
- package/src/core/conversations/conversation-store.ts +113 -0
- package/src/core/conversations/conversations-plugin.ts +29 -0
- package/src/core/define-roundtable.ts +124 -54
- package/src/core/discord/discord-adapter.ts +9 -0
- package/src/core/discord/owner-cards.ts +19 -8
- package/src/core/domain/conversation.ts +5 -0
- package/src/core/domain/owner-prompts.ts +1 -1
- package/src/core/domain/ports.ts +6 -0
- package/src/core/domain/progress.ts +10 -0
- package/src/core/events.ts +1 -0
- package/src/core/host.ts +6 -3
- package/src/core/http/listeners.ts +174 -21
- package/src/core/http/websocket.ts +316 -0
- package/src/core/i18n/cards.ts +5 -7
- package/src/core/modules/memory/owner-memory.ts +5 -3
- package/src/core/ops/error-reporter.ts +24 -12
- package/src/core/plugin.ts +14 -0
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/channel-router.ts +8 -0
- package/src/core/routing/conversation-turns.ts +55 -1
- package/src/core/routing/surface-port.ts +3 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +10 -2
- package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
- package/src/core/runtime/pending-confirmation-store.ts +42 -4
- package/src/core/runtime/pi-agent-runtime.ts +13 -6
- package/src/core/runtime/prompt-slot.ts +2 -2
- package/src/core/runtime/runtime-plugin.ts +160 -0
- package/src/core/runtime/turn-progress.ts +116 -0
- package/src/core/services.ts +15 -1
- package/src/core/shared/unix-server.ts +35 -5
- package/src/core/testing/modules.ts +47 -23
- package/src/core/testing/surface-contract.ts +383 -0
- package/src/core/testing/test-host.ts +27 -9
- package/src/discord/index.ts +2 -0
- package/src/index.ts +18 -0
- package/src/testing.ts +41 -5
- package/templates/adapters/web/.env.example +24 -0
- package/templates/adapters/web/README.md +15 -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 +55 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/plugins/self-compact.ts +11 -0
- package/templates/roundtable.config.ts +2 -1
|
@@ -11,28 +11,26 @@ import { FileAvatarStudio } from "../agents/avatar-studio.ts";
|
|
|
11
11
|
import { RelevanceScorer } from "../agents/group-round.ts";
|
|
12
12
|
import { discordKey } from "../agents/team-keys.ts";
|
|
13
13
|
import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
|
|
14
|
-
import type {
|
|
14
|
+
import type { AgentSessions } from "../contract/runtime.ts";
|
|
15
15
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
16
16
|
import { ConfigError } from "../domain/errors.ts";
|
|
17
|
-
import type { InterimTextMode } from "../domain/interim.ts";
|
|
18
17
|
import type { OwnerIdentity } from "../identity.ts";
|
|
19
18
|
import { ConfirmationJudge } from "../judging/confirmation-judge.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
AUTO_THINKING,
|
|
23
|
-
formatModelRef,
|
|
24
|
-
type ModelRef,
|
|
25
|
-
type ThinkingLevel,
|
|
26
|
-
} from "../models.ts";
|
|
19
|
+
import { AUTO_THINKING, formatModelRef, type ModelRef } from "../models.ts";
|
|
27
20
|
import type { ErrorReporter } from "../ops/error-reporter.ts";
|
|
28
21
|
import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
|
|
29
22
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
30
23
|
import { agentPromptExtension } from "../runtime/extensions/agent-prompt.ts";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
24
|
+
import type { AgentSessionsSlot } from "../runtime/runtime-plugin.ts";
|
|
25
|
+
import {
|
|
26
|
+
AGENTS,
|
|
27
|
+
BACKGROUND_TURNS,
|
|
28
|
+
RUNTIME,
|
|
29
|
+
SCHEDULES,
|
|
30
|
+
SKILLS,
|
|
31
|
+
} from "../services.ts";
|
|
34
32
|
import type { SessionTool } from "../sessions.ts";
|
|
35
|
-
import type {
|
|
33
|
+
import type { SpeakerPolicy, Tier } from "../speakers.ts";
|
|
36
34
|
import { DISCORD } from "./discord.ts";
|
|
37
35
|
import { agentOnly } from "./session-tool.ts";
|
|
38
36
|
|
|
@@ -45,15 +43,15 @@ export interface AgentServerOptions {
|
|
|
45
43
|
assistant: string;
|
|
46
44
|
/** Who may talk to the agents, and at which tier. */
|
|
47
45
|
speakers: SpeakerPolicy;
|
|
48
|
-
/** Shared with the rest of the host, so logins refresh in one place. */
|
|
46
|
+
/** Shared with the rest of the host, so logins refresh in one place; lists the models an agent may run. */
|
|
49
47
|
modelRuntime: ModelRuntime;
|
|
50
|
-
agentDir: string;
|
|
51
48
|
dataDir: string;
|
|
52
|
-
/** The model of agents and of the assistant
|
|
49
|
+
/** The model of agents and of the assistant. */
|
|
53
50
|
model: ModelRef;
|
|
54
|
-
|
|
55
|
-
/** How sure the judges must be before their answer is used. */
|
|
51
|
+
/** How sure the confirmation judge must be before its answer is used. */
|
|
56
52
|
judgeThreshold: number;
|
|
53
|
+
/** Where the agent server hands the runtime plugin its per-agent settings. */
|
|
54
|
+
agents?: AgentSessionsSlot;
|
|
57
55
|
/** The shell's shared working directory. */
|
|
58
56
|
workDir: string;
|
|
59
57
|
/**
|
|
@@ -73,10 +71,6 @@ export interface AgentServerOptions {
|
|
|
73
71
|
avatarReference: string;
|
|
74
72
|
/** Reports the process's own errors to an agent; without one nothing is reported. */
|
|
75
73
|
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;
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
/** The name of the agent server's plugin, as `serviceStarted` events name it. */
|
|
@@ -109,11 +103,9 @@ const AGENT_TIERS: Readonly<Record<string, Tier>> = {
|
|
|
109
103
|
channel_read: "member",
|
|
110
104
|
};
|
|
111
105
|
|
|
112
|
-
/** The tables the agent server keeps: its agents and groups
|
|
106
|
+
/** The tables the agent server keeps: its agents and groups. */
|
|
113
107
|
export interface AgentServerStores {
|
|
114
108
|
agents: PgAgentStore;
|
|
115
|
-
/** Held actions, kept across a restart. */
|
|
116
|
-
confirmations: PendingConfirmationStore;
|
|
117
109
|
}
|
|
118
110
|
|
|
119
111
|
/** Attaches the agent server's stores over the host's migrated pool. */
|
|
@@ -121,16 +113,14 @@ async function attachStores(
|
|
|
121
113
|
sql: SQL,
|
|
122
114
|
guildId: string,
|
|
123
115
|
): Promise<AgentServerStores> {
|
|
124
|
-
return {
|
|
125
|
-
agents: await PgAgentStore.attach(sql, guildId),
|
|
126
|
-
confirmations: await PendingConfirmationStore.attach(sql),
|
|
127
|
-
};
|
|
116
|
+
return { agents: await PgAgentStore.attach(sql, guildId) };
|
|
128
117
|
}
|
|
129
118
|
|
|
130
119
|
/**
|
|
131
|
-
* The agent server: the team with its channels and dashboard,
|
|
132
|
-
* agent
|
|
133
|
-
*
|
|
120
|
+
* The agent server: the team with its channels and dashboard, run on the runtime plugin's runtime,
|
|
121
|
+
* which it hands its per-agent settings. The skills the agents carry come from the skills addon
|
|
122
|
+
* when it is on. It claims the agent channels, starts the team in the background, and hands what
|
|
123
|
+
* it built to the plugins after it.
|
|
134
124
|
*/
|
|
135
125
|
export function agentServerPlugin(
|
|
136
126
|
options: AgentServerOptions,
|
|
@@ -141,18 +131,13 @@ export function agentServerPlugin(
|
|
|
141
131
|
) => Promise<AgentServerStores> = (context, guildId) =>
|
|
142
132
|
attachStores(context.database(), guildId),
|
|
143
133
|
): RoundtablePlugin {
|
|
144
|
-
let runtime: AgentRuntime | undefined;
|
|
145
134
|
return {
|
|
146
135
|
name: AGENT_SERVER_PLUGIN,
|
|
147
|
-
// The agent server's own tables; the host runs them before any setup, in this order.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...PgAgentStore.migrations(options.guildId),
|
|
151
|
-
],
|
|
136
|
+
// The agent server's own tables; the host runs them before any setup, in this order. The
|
|
137
|
+
// held actions' table is the runtime plugin's.
|
|
138
|
+
migrations: PgAgentStore.migrations(options.guildId),
|
|
152
139
|
provides: [AGENTS],
|
|
153
|
-
|
|
154
|
-
await runtime?.preflight?.();
|
|
155
|
-
},
|
|
140
|
+
requires: [RUNTIME],
|
|
156
141
|
setup: async (context) => {
|
|
157
142
|
const discord = context.services.get(DISCORD);
|
|
158
143
|
const schedules = context.services.get(SCHEDULES);
|
|
@@ -168,10 +153,8 @@ export function agentServerPlugin(
|
|
|
168
153
|
await studio.init();
|
|
169
154
|
// Absent when the skills addon is off: the agents then carry none.
|
|
170
155
|
const skills = context.services.find(SKILLS);
|
|
171
|
-
const { agents: store
|
|
172
|
-
|
|
173
|
-
options.guildId,
|
|
174
|
-
);
|
|
156
|
+
const { agents: store } = await openStores(context, options.guildId);
|
|
157
|
+
const running = context.services.get(RUNTIME);
|
|
175
158
|
const judge = providers.judge;
|
|
176
159
|
const confirmations = new ConfirmationJudge({
|
|
177
160
|
judge,
|
|
@@ -194,11 +177,7 @@ export function agentServerPlugin(
|
|
|
194
177
|
store,
|
|
195
178
|
channels: connection.agentChannels(options.guildId),
|
|
196
179
|
studio,
|
|
197
|
-
|
|
198
|
-
runtime: () => {
|
|
199
|
-
if (!runtime) throw new Error("the runtime is built with the team");
|
|
200
|
-
return runtime;
|
|
201
|
-
},
|
|
180
|
+
runtime: () => running,
|
|
202
181
|
confirmations,
|
|
203
182
|
scorer: new RelevanceScorer(judge, logger, options.owner),
|
|
204
183
|
models: {
|
|
@@ -247,50 +226,7 @@ export function agentServerPlugin(
|
|
|
247
226
|
skills: (name) => built.skillsOf(name),
|
|
248
227
|
turnChannel: (scope) => built.turnChannel(scope),
|
|
249
228
|
};
|
|
250
|
-
|
|
251
|
-
context.surfaces.prompts(channel, speaker);
|
|
252
|
-
// A plugin that fills the runtime slot replaces the whole conversation runtime; without
|
|
253
|
-
// one the agent server runs Pi, built from the options below.
|
|
254
|
-
const running: AgentRuntime = providers.filled.has("runtime")
|
|
255
|
-
? providers.runtime({
|
|
256
|
-
logger,
|
|
257
|
-
env: context.env,
|
|
258
|
-
owner: { id: options.owner.id, name: options.owner.name },
|
|
259
|
-
sessions: context.sessions,
|
|
260
|
-
toolTiers: context.toolTiers,
|
|
261
|
-
prompts,
|
|
262
|
-
agents: agentSessions,
|
|
263
|
-
confirmations: heldActions,
|
|
264
|
-
judge,
|
|
265
|
-
})
|
|
266
|
-
: new PiAgentRuntime({
|
|
267
|
-
owner: options.owner,
|
|
268
|
-
sessions: context.sessions,
|
|
269
|
-
agentDir: options.agentDir,
|
|
270
|
-
modelRuntime: options.modelRuntime,
|
|
271
|
-
dataDir: options.dataDir,
|
|
272
|
-
model: options.model,
|
|
273
|
-
thinking: options.thinking,
|
|
274
|
-
effort: new EffortJudge({
|
|
275
|
-
judge,
|
|
276
|
-
brief: AGENT_BRIEF,
|
|
277
|
-
fallback: options.thinking,
|
|
278
|
-
threshold: options.judgeThreshold,
|
|
279
|
-
logger,
|
|
280
|
-
}),
|
|
281
|
-
confirmations: heldActions,
|
|
282
|
-
toolTiers: context.toolTiers,
|
|
283
|
-
agents: agentSessions,
|
|
284
|
-
prompts,
|
|
285
|
-
logger,
|
|
286
|
-
...(options.interimText
|
|
287
|
-
? { interimText: options.interimText }
|
|
288
|
-
: {}),
|
|
289
|
-
...(options.interimPrimaryChars
|
|
290
|
-
? { interimPrimaryChars: options.interimPrimaryChars }
|
|
291
|
-
: {}),
|
|
292
|
-
});
|
|
293
|
-
runtime = running;
|
|
229
|
+
options.agents?.bind(agentSessions);
|
|
294
230
|
context.services.provide(AGENTS, {
|
|
295
231
|
team: built,
|
|
296
232
|
directory: store,
|
|
@@ -325,7 +261,6 @@ export function agentServerPlugin(
|
|
|
325
261
|
});
|
|
326
262
|
return {
|
|
327
263
|
services: [
|
|
328
|
-
{ name: "runtime", stop: () => running.dispose?.() },
|
|
329
264
|
{ name: "dashboard", stop: () => dashboard.stop() },
|
|
330
265
|
{
|
|
331
266
|
name: AGENT_TEAM_SERVICE,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { OWNER_TARGET } from "../agents/agent-claim.ts";
|
|
3
4
|
import type { SurfacePort } from "../contract/surface.ts";
|
|
4
5
|
import { scheduleCommands } from "../discord/schedule-commands.ts";
|
|
5
6
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
7
|
+
import {
|
|
8
|
+
ConfigError,
|
|
9
|
+
DelegationError,
|
|
10
|
+
ScheduleError,
|
|
11
|
+
} from "../domain/errors.ts";
|
|
6
12
|
import { messages } from "../i18n/index.ts";
|
|
7
13
|
import type { OwnerIdentity } from "../identity.ts";
|
|
8
14
|
import type { ModelRef, ThinkingLevel } from "../models.ts";
|
|
@@ -17,7 +23,9 @@ import { notifyExtension } from "../modules/notify/notify.ts";
|
|
|
17
23
|
import { precheckScriptHoldRule } from "../modules/schedules/precheck-tools.ts";
|
|
18
24
|
import { Scheduler } from "../modules/schedules/scheduler.ts";
|
|
19
25
|
import { schedulesExtension } from "../modules/schedules/schedules.ts";
|
|
26
|
+
import type { ErrorReporter } from "../ops/error-reporter.ts";
|
|
20
27
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
28
|
+
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
21
29
|
import {
|
|
22
30
|
AGENTS,
|
|
23
31
|
BACKGROUND_TURNS,
|
|
@@ -33,6 +41,11 @@ import { DISCORD } from "./discord.ts";
|
|
|
33
41
|
import { fixed } from "./session-tool.ts";
|
|
34
42
|
|
|
35
43
|
export interface ModulesOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Reports the process's own errors to a conversation, connected when the modules start; one
|
|
46
|
+
* that reports to an agent is the agent server's to connect.
|
|
47
|
+
*/
|
|
48
|
+
errorReporter?: ErrorReporter;
|
|
36
49
|
/** Who the tools serve in the owner's own sessions. */
|
|
37
50
|
owner: OwnerIdentity & { id: string };
|
|
38
51
|
/** The name a turn the process itself starts is written by. */
|
|
@@ -65,19 +78,32 @@ const MODULE_TIERS: Readonly<Record<string, Tier>> = {
|
|
|
65
78
|
|
|
66
79
|
/**
|
|
67
80
|
* The owner's modules: notifications, schedules, and delegated tasks, each a session tool, and
|
|
68
|
-
* the turns nobody wrote. The delegator's running jobs join the shutdown drain.
|
|
81
|
+
* the turns nobody wrote. The delegator's running jobs join the shutdown drain. Without Discord
|
|
82
|
+
* there are no owner's messages: no `notify_owner`, and a conversation no chat surface carries
|
|
83
|
+
* can neither schedule nor delegate. Without the owner's background target no session gets the
|
|
84
|
+
* schedule or delegation tools.
|
|
69
85
|
*/
|
|
70
86
|
export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
71
87
|
const { owner } = options;
|
|
72
|
-
//
|
|
88
|
+
// A conversation no chat surface carries posts its runs in the owner's messages, when there are any.
|
|
73
89
|
const channelFor = async (
|
|
74
90
|
channel: ChannelKey,
|
|
75
91
|
surfaces: SurfacePort,
|
|
76
|
-
ownerChannel: () => Promise<ChannelKey
|
|
77
|
-
|
|
92
|
+
ownerChannel: (() => Promise<ChannelKey>) | undefined,
|
|
93
|
+
refused: (message: string) => Error,
|
|
94
|
+
) => {
|
|
95
|
+
if (surfaces.of(channel)) return channel;
|
|
96
|
+
if (ownerChannel) return ownerChannel();
|
|
97
|
+
throw refused(
|
|
98
|
+
"this conversation has no chat surface to post a run in, and the host has no owner's messages to post it in instead",
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
// Set up once the modules set up; the preflight reads it after every plugin linked.
|
|
102
|
+
let reportsReach: (() => void) | undefined;
|
|
78
103
|
return {
|
|
79
104
|
name: "modules",
|
|
80
105
|
provides: [BACKGROUND_TURNS, DELEGATION],
|
|
106
|
+
preflight: () => reportsReach?.(),
|
|
81
107
|
setup: ({
|
|
82
108
|
conversations,
|
|
83
109
|
services,
|
|
@@ -89,13 +115,29 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
89
115
|
const schedules = services.get(SCHEDULES);
|
|
90
116
|
// Absent when a plugin list leaves the prechecks plugin out: then none can be attached.
|
|
91
117
|
const prechecks = services.find(PRECHECKS);
|
|
92
|
-
|
|
93
|
-
const
|
|
118
|
+
// Absent on a host without Discord: then there are no owner's messages and no threads.
|
|
119
|
+
const discord = services.find(DISCORD);
|
|
120
|
+
const connection = discord?.connection;
|
|
121
|
+
const ownerChannel = connection
|
|
122
|
+
? () => connection.ownerChannel()
|
|
123
|
+
: undefined;
|
|
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
|
-
const
|
|
98
|
-
channelFor(
|
|
127
|
+
const scheduleChannelFor = (channel: ChannelKey) =>
|
|
128
|
+
channelFor(
|
|
129
|
+
channel,
|
|
130
|
+
surfaces,
|
|
131
|
+
ownerChannel,
|
|
132
|
+
(message) => new ScheduleError(message),
|
|
133
|
+
);
|
|
134
|
+
const delegateChannelFor = (channel: ChannelKey) =>
|
|
135
|
+
channelFor(
|
|
136
|
+
channel,
|
|
137
|
+
surfaces,
|
|
138
|
+
ownerChannel,
|
|
139
|
+
(message) => new DelegationError(message),
|
|
140
|
+
);
|
|
99
141
|
const background = new ConversationBackgroundTurns({
|
|
100
142
|
conversations,
|
|
101
143
|
system: { id: "assistant", name: options.assistant },
|
|
@@ -113,7 +155,7 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
113
155
|
}),
|
|
114
156
|
targets: (name) => conversations.target(name),
|
|
115
157
|
deliver: (job, outcome) => background.runDelegated(job, outcome),
|
|
116
|
-
threads: discord.threads,
|
|
158
|
+
...(discord ? { threads: discord.threads } : {}),
|
|
117
159
|
logger,
|
|
118
160
|
});
|
|
119
161
|
services.provide(BACKGROUND_TURNS, background);
|
|
@@ -121,9 +163,60 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
121
163
|
// An agent session serves every speaker, so its tools name none.
|
|
122
164
|
const served = (session: SessionContext) =>
|
|
123
165
|
session.agent ? THE_SPEAKER : owner;
|
|
166
|
+
// Schedules and delegated reports run as the owner's background turns; without a plugin
|
|
167
|
+
// contributing that target, as on a host without the agent server, none could ever start,
|
|
168
|
+
// so no session gets the tools. Read when a session is made, after every plugin set up.
|
|
169
|
+
const ownerTurns = () =>
|
|
170
|
+
conversations.target(OWNER_TARGET.name) !== undefined;
|
|
171
|
+
// A reporter for an ops agent is the agent server's to connect.
|
|
172
|
+
const reporter =
|
|
173
|
+
options.errorReporter &&
|
|
174
|
+
"conversation" in options.errorReporter.destination
|
|
175
|
+
? options.errorReporter
|
|
176
|
+
: undefined;
|
|
177
|
+
if (reporter && "conversation" in reporter.destination) {
|
|
178
|
+
const channel = reporter.destination.conversation;
|
|
179
|
+
// A report that no surface can post, or that no conversation owns, would only be logged;
|
|
180
|
+
// its report turn is the owner's background turn, which runs only where a claim takes one.
|
|
181
|
+
reportsReach = () => {
|
|
182
|
+
if (!surfaces.of(channel))
|
|
183
|
+
throw new ConfigError(
|
|
184
|
+
`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.`,
|
|
185
|
+
);
|
|
186
|
+
if (!conversations.owns(channel))
|
|
187
|
+
throw new ConfigError(
|
|
188
|
+
`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.`,
|
|
189
|
+
);
|
|
190
|
+
if (!ownerTurns())
|
|
191
|
+
throw new ConfigError(
|
|
192
|
+
`config ops.conversation: no plugin contributes the "${OWNER_TARGET.name}" background target, so the report turns of ${JSON.stringify(channel)} could never run. Configure discord, whose agent server contributes it, add a plugin that contributes it, or leave ops out.`,
|
|
193
|
+
);
|
|
194
|
+
if (!conversations.takesBackground(channel))
|
|
195
|
+
throw new ConfigError(
|
|
196
|
+
`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.`,
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
124
200
|
return {
|
|
125
201
|
services: [
|
|
126
202
|
{ name: "delegator", busy: () => delegator.runningChannels() },
|
|
203
|
+
...(reporter
|
|
204
|
+
? [
|
|
205
|
+
{
|
|
206
|
+
name: "error-reports",
|
|
207
|
+
start: () =>
|
|
208
|
+
reporter.connect({
|
|
209
|
+
post: (channel, text) =>
|
|
210
|
+
surfaces.sendReply(channel, {
|
|
211
|
+
chunks: splitReply(text),
|
|
212
|
+
}),
|
|
213
|
+
turn: (channel, text) =>
|
|
214
|
+
background.runErrorReport(channel, text),
|
|
215
|
+
logger,
|
|
216
|
+
}),
|
|
217
|
+
},
|
|
218
|
+
]
|
|
219
|
+
: []),
|
|
127
220
|
],
|
|
128
221
|
// Saving a precheck script that calls a held tool waits for the owner, as the call would.
|
|
129
222
|
holdRules: [
|
|
@@ -134,34 +227,40 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
134
227
|
}),
|
|
135
228
|
],
|
|
136
229
|
sessionTools: [
|
|
137
|
-
|
|
230
|
+
...(connection
|
|
231
|
+
? [fixed("notify", () => notifyExtension(connection, owner))]
|
|
232
|
+
: []),
|
|
138
233
|
fixed("schedules", (session) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
234
|
+
ownerTurns()
|
|
235
|
+
? schedulesExtension(
|
|
236
|
+
{
|
|
237
|
+
store: schedules,
|
|
238
|
+
owner: { id: owner.id, name: owner.name },
|
|
239
|
+
channelFor: scheduleChannelFor,
|
|
240
|
+
...(prechecks ? { prechecks } : {}),
|
|
241
|
+
holds: () => sessions().holds,
|
|
242
|
+
},
|
|
243
|
+
session.homeChannel,
|
|
244
|
+
served(session),
|
|
245
|
+
session.agent ? agentChannelOf : undefined,
|
|
246
|
+
session.speaker,
|
|
247
|
+
)
|
|
248
|
+
: null,
|
|
152
249
|
),
|
|
153
250
|
fixed("delegate", (session) =>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
251
|
+
ownerTurns()
|
|
252
|
+
? delegateExtension(
|
|
253
|
+
{
|
|
254
|
+
delegator,
|
|
255
|
+
owner: { id: owner.id, name: owner.name },
|
|
256
|
+
channelFor: delegateChannelFor,
|
|
257
|
+
},
|
|
258
|
+
session.homeChannel,
|
|
259
|
+
served(session),
|
|
260
|
+
session.turnChannel,
|
|
261
|
+
session.speaker,
|
|
262
|
+
)
|
|
263
|
+
: null,
|
|
165
264
|
),
|
|
166
265
|
],
|
|
167
266
|
toolTiers: MODULE_TIERS,
|
|
@@ -4,7 +4,8 @@ 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 { MEMORY, PRECHECKS, SCHEDULES } from "../services.ts";
|
|
7
|
+
import { MEMORY, type MemoryStore, PRECHECKS, SCHEDULES } from "../services.ts";
|
|
8
|
+
import type { SessionTool } from "../sessions.ts";
|
|
8
9
|
import { THE_SPEAKER, type Tier } from "../speakers.ts";
|
|
9
10
|
import { fixed } from "./session-tool.ts";
|
|
10
11
|
|
|
@@ -14,12 +15,37 @@ export interface MemoryOptions {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/** Each speaker keeps a memory of their own, so every tier may use the tools; the owner's reaches only the owner's turns. */
|
|
17
|
-
const MEMORY_TIERS: Readonly<Record<string, Tier>> = {
|
|
18
|
+
export const MEMORY_TIERS: Readonly<Record<string, Tier>> = {
|
|
18
19
|
memory_add: "member",
|
|
19
20
|
memory_search: "member",
|
|
20
21
|
memory_remove: "member",
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
/** The memory tools and prompt block of every conversation, over the given store. */
|
|
25
|
+
export function memorySessionTool(
|
|
26
|
+
memory: MemoryStore,
|
|
27
|
+
owner: MemoryOptions["owner"],
|
|
28
|
+
): SessionTool {
|
|
29
|
+
return fixed("owner-memory", (session) => {
|
|
30
|
+
// An agent session serves every speaker, so its tools name none.
|
|
31
|
+
if (session.agent)
|
|
32
|
+
return ownerMemoryExtension(
|
|
33
|
+
memory,
|
|
34
|
+
owner.id,
|
|
35
|
+
THE_SPEAKER,
|
|
36
|
+
session.speaker,
|
|
37
|
+
);
|
|
38
|
+
// Any other conversation may admit a member or an admin, who reads and changes their own
|
|
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
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
/**
|
|
24
50
|
* The remembered facts of each speaker: the table, the store over it provided as `MEMORY`, and
|
|
25
51
|
* the memory tools and prompt block of every conversation. An addon; `memory: false` leaves it
|
|
@@ -35,18 +61,7 @@ export function memoryPlugin(options: MemoryOptions): RoundtablePlugin {
|
|
|
35
61
|
const memory = PgMemoryStore.attach(database(), owner.id);
|
|
36
62
|
services.provide(MEMORY, memory);
|
|
37
63
|
return {
|
|
38
|
-
sessionTools: [
|
|
39
|
-
fixed("owner-memory", (session) =>
|
|
40
|
-
ownerMemoryExtension(
|
|
41
|
-
memory,
|
|
42
|
-
owner.id,
|
|
43
|
-
// An agent session serves every speaker, so its tools name none.
|
|
44
|
-
session.agent ? THE_SPEAKER : owner,
|
|
45
|
-
// The owner's own chats have one speaker; the agent server's have several.
|
|
46
|
-
session.agent ? session.speaker : undefined,
|
|
47
|
-
),
|
|
48
|
-
),
|
|
49
|
-
],
|
|
64
|
+
sessionTools: [memorySessionTool(memory, owner)],
|
|
50
65
|
toolTiers: MEMORY_TIERS,
|
|
51
66
|
};
|
|
52
67
|
},
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
+
import { bool, optional, shape, text } from "./schema.ts";
|
|
3
|
+
|
|
4
|
+
/** The Discord settings, at the top level or in a Discord adapter. */
|
|
5
|
+
export const discordShape = shape({
|
|
6
|
+
token: text,
|
|
7
|
+
guild: text,
|
|
8
|
+
entryChannel: text,
|
|
9
|
+
rootCommand: optional(text),
|
|
10
|
+
admin: optional(bool),
|
|
11
|
+
refusalHint: optional(text),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** The adapters `adapters` takes, by the name their factory gives them. */
|
|
15
|
+
const ADAPTERS = ["discord"] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The configuration with its adapters in the places the host reads them: a Discord adapter's
|
|
19
|
+
* settings as the top-level `discord`. Throws a ConfigError naming the adapter by its place.
|
|
20
|
+
*/
|
|
21
|
+
// pi-lens-ignore: no-unknown-returns — the configuration as written is untyped until the schema checks it
|
|
22
|
+
export function liftAdapters(input: unknown): unknown {
|
|
23
|
+
if (typeof input !== "object" || input === null || !("adapters" in input))
|
|
24
|
+
return input;
|
|
25
|
+
const { adapters, ...rest } = input as Record<string, unknown>;
|
|
26
|
+
if (adapters === undefined) return rest;
|
|
27
|
+
if (!Array.isArray(adapters))
|
|
28
|
+
throw new ConfigError(
|
|
29
|
+
`config adapters: expected a list of adapters, such as [discord({ ... })] with discord from pi-roundtable/discord, got ${JSON.stringify(adapters)}. Fix the value in roundtable.config.ts.`,
|
|
30
|
+
);
|
|
31
|
+
const lifted: Record<string, unknown> = { ...rest };
|
|
32
|
+
adapters.forEach((entry: unknown, index) => {
|
|
33
|
+
const at = `adapters[${index}]`;
|
|
34
|
+
const name =
|
|
35
|
+
typeof entry === "object" && entry !== null
|
|
36
|
+
? (entry as { adapter?: unknown }).adapter
|
|
37
|
+
: undefined;
|
|
38
|
+
if (typeof name !== "string")
|
|
39
|
+
throw new ConfigError(
|
|
40
|
+
`config ${at}: expected an adapter, an object its factory makes such as discord({ ... }) from pi-roundtable/discord, got ${JSON.stringify(entry)}. Fix the value in roundtable.config.ts.`,
|
|
41
|
+
);
|
|
42
|
+
if (!(ADAPTERS as readonly string[]).includes(name))
|
|
43
|
+
throw new ConfigError(
|
|
44
|
+
`config ${at}: unknown adapter ${JSON.stringify(name)}. The adapters are ${ADAPTERS.join(", ")}; a chat network that comes as a plugin, such as pi-roundtable-webchat's webChat(), belongs in plugins.`,
|
|
45
|
+
);
|
|
46
|
+
if (lifted.discord !== undefined)
|
|
47
|
+
throw new ConfigError(
|
|
48
|
+
`config ${at}: Discord is configured twice, at the top level or by another adapter. Keep one.`,
|
|
49
|
+
);
|
|
50
|
+
const { adapter: _adapter, ...settings } = entry as Record<string, unknown>;
|
|
51
|
+
lifted.discord = shape({ discord: discordShape }).check(
|
|
52
|
+
settings,
|
|
53
|
+
at,
|
|
54
|
+
).discord;
|
|
55
|
+
});
|
|
56
|
+
return lifted;
|
|
57
|
+
}
|