pi-roundtable 0.22.5 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/README.md +35 -9
- package/README.zh-TW.md +51 -13
- package/docs/migrating-0.8.md +2 -2
- package/docs/plugins.md +351 -28
- package/examples/discord-adapter.ts +9 -1
- package/examples/seeds.test.ts +2 -2
- package/examples/seeds.ts +1 -2
- package/package.json +6 -2
- package/src/characters/catalog.ts +1055 -0
- package/src/characters/characters.css +68 -0
- package/src/characters/colors.ts +108 -0
- package/src/characters/compose.ts +281 -0
- package/src/characters/css.ts +63 -0
- package/src/characters/index.ts +33 -0
- package/src/characters/parse.ts +196 -0
- package/src/characters/parts-face.ts +354 -0
- package/src/characters/parts-hair.ts +56 -0
- package/src/characters/parts-hats.ts +57 -0
- package/src/characters/parts-outfits.ts +81 -0
- package/src/characters/parts-props.ts +137 -0
- package/src/characters/spec.ts +224 -0
- package/src/characters/suggest.ts +101 -0
- package/src/cli/checks/characters.ts +23 -0
- package/src/cli/checks/images.ts +5 -3
- package/src/cli/doctor.ts +2 -0
- package/src/core/agents/agent-claim.ts +30 -11
- package/src/core/agents/agent-dashboard.ts +1 -1
- package/src/core/agents/agent-messages.ts +43 -15
- package/src/core/agents/agent-prompt.ts +2 -4
- package/src/core/agents/agent-rules.ts +42 -3
- package/src/core/agents/agent-schema.ts +37 -15
- package/src/core/agents/agent-store.ts +57 -13
- package/src/core/agents/agent-team-fixture.ts +90 -14
- package/src/core/agents/agent-team.ts +83 -50
- package/src/core/agents/agent-tools.ts +94 -41
- package/src/core/agents/avatar-studio.ts +40 -4
- package/src/core/agents/character-png.ts +40 -0
- package/src/core/agents/group-messages.ts +7 -4
- package/src/core/agents/group-turns.ts +27 -14
- package/src/core/agents/team-editing.ts +99 -58
- package/src/core/agents/team-keys.ts +177 -11
- package/src/core/agents/team-lifecycle.ts +151 -84
- package/src/core/agents/team-options.ts +13 -6
- package/src/core/agents/team-status.ts +31 -13
- package/src/core/agents/team-text.ts +13 -3
- package/src/core/agents/team-turn-types.ts +25 -7
- package/src/core/agents/team-turns.ts +143 -28
- package/src/core/attachments/attachment-fetcher.ts +162 -64
- package/src/core/attachments/prompt-block.ts +7 -1
- package/src/core/attachments/tool-attachment.ts +20 -1
- package/src/core/attachments/turn-attachments.ts +18 -2
- package/src/core/builtin/agent-server.ts +103 -40
- package/src/core/builtin/discord.ts +4 -17
- package/src/core/builtin/modules.ts +6 -5
- package/src/core/builtin/threads.ts +37 -0
- package/src/core/config/agent-team.ts +43 -0
- package/src/core/config/config.ts +50 -59
- package/src/core/config/limits.ts +93 -0
- package/src/core/config/ops.ts +34 -0
- package/src/core/config/schema.ts +26 -0
- package/src/core/contract/channels.ts +20 -0
- package/src/core/contract/surface.ts +77 -7
- package/src/core/define-roundtable.ts +70 -42
- package/src/core/discord/agent-commands.ts +108 -86
- package/src/core/discord/agent-panel.ts +11 -2
- package/src/core/discord/connection.ts +1 -1
- package/src/core/discord/discord-surface.ts +10 -1
- package/src/core/discord/dispatch-thread-host.ts +1 -1
- package/src/core/domain/conversation.ts +24 -0
- package/src/core/holds.ts +221 -15
- package/src/core/host-registration.ts +2 -1
- package/src/core/http/listeners.ts +63 -3
- package/src/core/i18n/agent-panel.ts +14 -21
- package/src/core/interactions/prompts.ts +12 -0
- package/src/core/modes/autonomy.ts +23 -0
- package/src/core/modes/conversation-modes.ts +213 -0
- package/src/core/modes/modes-plugin.ts +24 -0
- package/src/core/modules/delegation/delegator.ts +4 -4
- package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
- package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
- package/src/core/modules/host-shell/shell-lexer.ts +493 -0
- package/src/core/modules/host-shell/shell-paths.ts +85 -4
- package/src/core/modules/host-shell/shell-policy.ts +341 -332
- package/src/core/modules/host-shell/shell-programs.ts +298 -0
- package/src/core/modules/host-shell/shell-quoting.ts +237 -0
- package/src/core/modules/host-shell/shell-scripts.ts +114 -0
- package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
- package/src/core/modules/schedules/precheck-tools.ts +17 -0
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/presence/direct-channels.ts +30 -0
- package/src/core/presentation/reply-body.ts +25 -0
- package/src/core/registry/contributions.ts +31 -7
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -15
- package/src/core/routing/surface-port.ts +5 -1
- package/src/core/runtime/extensions/approval-card.ts +1 -5
- package/src/core/runtime/extensions/approval-fields.ts +3 -7
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
- package/src/core/runtime/extensions/gate-refusals.ts +23 -0
- package/src/core/runtime/extensions/held-calls.ts +16 -1
- package/src/core/runtime/extensions/hold-describer.ts +58 -8
- package/src/core/runtime/extensions/hold-lookups.ts +19 -19
- package/src/core/runtime/extensions/yolo.ts +105 -0
- package/src/core/runtime/pi-agent-runtime.ts +3 -5
- package/src/core/runtime/runtime-plugin.ts +42 -1
- package/src/core/runtime/runtime-types.ts +11 -0
- package/src/core/runtime/turn-budget.ts +32 -5
- package/src/core/runtime/turn-limits.ts +14 -3
- package/src/core/services.ts +61 -9
- package/src/core/shared/attachment-reader.ts +6 -0
- package/src/core/shared/unix-server.ts +10 -1
- package/src/core/testing/modules.ts +12 -8
- package/src/core/testing/surface-threads.ts +58 -0
- package/src/core/testing/thread-host.ts +17 -5
- package/src/core/threads/dispatch-threads.ts +299 -0
- package/src/core/threads/host-threads.ts +19 -0
- package/src/core/tool-set.snapshot.json +5 -0
- package/src/index.ts +27 -1
- package/src/kit/characters.ts +5 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/shell.ts +1 -0
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/testing.ts +4 -0
- package/templates/agents.ts +1 -1
- package/src/core/discord/dispatch-threads.ts +0 -194
|
@@ -28,16 +28,33 @@ import {
|
|
|
28
28
|
RUNTIME,
|
|
29
29
|
SCHEDULES,
|
|
30
30
|
SKILLS,
|
|
31
|
+
THREADS,
|
|
31
32
|
} from "../services.ts";
|
|
32
33
|
import type { SessionTool } from "../sessions.ts";
|
|
33
34
|
import type { Tier } from "../speakers.ts";
|
|
34
35
|
import { DISCORD } from "./discord.ts";
|
|
35
36
|
import { agentOnly } from "./session-tool.ts";
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The id of the team on a host without Discord, in place of the Discord guild: the rows the team
|
|
40
|
+
* keeps are stored under it, as under the guild's id on a host with Discord.
|
|
41
|
+
*/
|
|
42
|
+
export const LOCAL_TEAM_ID = "local";
|
|
43
|
+
|
|
37
44
|
export interface AgentServerOptions {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The Discord guild the team lives in. Absent on a host without Discord: the team is then
|
|
47
|
+
* `LOCAL_TEAM_ID`, it has no channels, and its agents live in the rooms of the surfaces that
|
|
48
|
+
* host them.
|
|
49
|
+
*/
|
|
50
|
+
guildId?: string;
|
|
51
|
+
/** The channel the coordinator lives in; set with `guildId`, never with `coordinator`. */
|
|
52
|
+
entryChannelId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The coordinator's name on a host without Discord, where no entry channel names it; the first
|
|
55
|
+
* seed when absent. Refused with `entryChannelId`.
|
|
56
|
+
*/
|
|
57
|
+
coordinator?: string;
|
|
41
58
|
owner: OwnerIdentity & { id: string };
|
|
42
59
|
/** The assistant's display name, as the confirmation judge names it. */
|
|
43
60
|
assistant: string;
|
|
@@ -77,10 +94,13 @@ export interface AgentServerOptions {
|
|
|
77
94
|
attachmentDownload?: { deadlineMs?: number; idleMs?: number };
|
|
78
95
|
/** The prompt every agent starts with, and the one for speakers other than the owner. */
|
|
79
96
|
prompts: { shared: string; guest?: string };
|
|
80
|
-
/** The listener the avatar pictures are served on. */
|
|
81
|
-
avatarListener
|
|
82
|
-
/**
|
|
83
|
-
|
|
97
|
+
/** The listener the avatar pictures are served on; none mounts no route, and a surface serves them through `AGENTS.avatars.serve`. */
|
|
98
|
+
avatarListener?: string;
|
|
99
|
+
/**
|
|
100
|
+
* The origin the agents' pictures are served from. Without one a picture's URL is
|
|
101
|
+
* `/avatars/<hash>.png`, which the surface that serves the pictures resolves against its origin.
|
|
102
|
+
*/
|
|
103
|
+
avatarUrl?: string;
|
|
84
104
|
/** The assistant's neutral avatar: the style reference and the picture of an agent without one. */
|
|
85
105
|
avatarReference: string;
|
|
86
106
|
/** The picture used instead when `avatarReference` cannot be read as one; the failure is logged. */
|
|
@@ -127,41 +147,62 @@ export interface AgentServerStores {
|
|
|
127
147
|
/** Attaches the agent server's stores over the host's migrated pool. */
|
|
128
148
|
async function attachStores(
|
|
129
149
|
sql: SQL,
|
|
130
|
-
|
|
150
|
+
teamId: string,
|
|
131
151
|
): Promise<AgentServerStores> {
|
|
132
|
-
return { agents: await PgAgentStore.attach(sql,
|
|
152
|
+
return { agents: await PgAgentStore.attach(sql, teamId) };
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
/**
|
|
136
156
|
* The agent server: the team with its channels and dashboard, run on the runtime plugin's runtime,
|
|
137
157
|
* which it hands its per-agent settings. The skills the agents carry come from the skills addon
|
|
138
158
|
* when it is on. It claims the agent channels, starts the team in the background, and hands what
|
|
139
|
-
* it built to the plugins after it.
|
|
159
|
+
* it built to the plugins after it. On a host without Discord the team has no channels or
|
|
160
|
+
* dashboard: its agents live in the rooms of the surfaces that host them, and the host refuses to
|
|
161
|
+
* start when no surface does.
|
|
140
162
|
*/
|
|
141
163
|
export function agentServerPlugin(
|
|
142
164
|
options: AgentServerOptions,
|
|
143
165
|
/** Opens the stores; a test hands stand-ins, so the plugin can be set up without a database. */
|
|
144
166
|
openStores: (
|
|
145
167
|
context: Pick<PluginContext, "database">,
|
|
146
|
-
|
|
147
|
-
) => Promise<AgentServerStores> = (context,
|
|
148
|
-
attachStores(context.database(),
|
|
168
|
+
teamId: string,
|
|
169
|
+
) => Promise<AgentServerStores> = (context, teamId) =>
|
|
170
|
+
attachStores(context.database(), teamId),
|
|
149
171
|
): RoundtablePlugin {
|
|
172
|
+
if (options.entryChannelId !== undefined && options.coordinator !== undefined)
|
|
173
|
+
throw new ConfigError(
|
|
174
|
+
"the agent server: entryChannelId and coordinator both name the coordinator. Set the entry channel with Discord, the coordinator without it.",
|
|
175
|
+
);
|
|
176
|
+
const teamId = options.guildId ?? LOCAL_TEAM_ID;
|
|
177
|
+
/** Set once the plugin has set up; the preflight reads it after every plugin linked. */
|
|
178
|
+
let hostsRooms: (() => boolean) | undefined;
|
|
150
179
|
return {
|
|
151
180
|
name: AGENT_SERVER_PLUGIN,
|
|
152
181
|
// The agent server's own tables; the host runs them before any setup, in this order. The
|
|
153
182
|
// held actions' table is the runtime plugin's.
|
|
154
|
-
migrations: PgAgentStore.migrations(
|
|
183
|
+
migrations: PgAgentStore.migrations(teamId),
|
|
155
184
|
provides: [AGENTS],
|
|
156
185
|
requires: [RUNTIME],
|
|
186
|
+
// A team with no Discord channels is reached through rooms only; without a surface that
|
|
187
|
+
// hosts them its agents could never be spoken to.
|
|
188
|
+
preflight: () => {
|
|
189
|
+
if (hostsRooms && !hostsRooms())
|
|
190
|
+
throw new ConfigError(
|
|
191
|
+
"the agents need a surface that hosts their rooms, such as pi-roundtable-web, and none is installed. Without Discord channels there is nowhere to talk to them; add a plugin whose chat surface declares agentRooms: true, or configure discord.",
|
|
192
|
+
);
|
|
193
|
+
},
|
|
157
194
|
setup: async (context) => {
|
|
158
|
-
|
|
195
|
+
// Absent on a host without Discord: then the team has no channels or dashboard.
|
|
196
|
+
const discord = context.services.find(DISCORD);
|
|
197
|
+
// Dispatch threads on any surface that hosts them, Discord's or a room surface's.
|
|
198
|
+
const threads = context.services.find(THREADS);
|
|
159
199
|
const schedules = context.services.get(SCHEDULES);
|
|
160
|
-
const
|
|
200
|
+
const connection = discord?.connection;
|
|
161
201
|
const { logger, providers, surfaces } = context;
|
|
202
|
+
if (!connection) hostsRooms = () => surfaces.roomSurfaces().length > 0;
|
|
162
203
|
const studio = new FileAvatarStudio({
|
|
163
204
|
dir: join(options.dataDir, "avatars"),
|
|
164
|
-
publicUrl: options.avatarUrl,
|
|
205
|
+
...(options.avatarUrl ? { publicUrl: options.avatarUrl } : {}),
|
|
165
206
|
referencePath: options.avatarReference,
|
|
166
207
|
...(options.avatarNeutral
|
|
167
208
|
? {
|
|
@@ -179,7 +220,7 @@ export function agentServerPlugin(
|
|
|
179
220
|
await studio.init();
|
|
180
221
|
// Absent when the skills addon is off: the agents then carry none.
|
|
181
222
|
const skills = context.services.find(SKILLS);
|
|
182
|
-
const { agents: store } = await openStores(context,
|
|
223
|
+
const { agents: store } = await openStores(context, teamId);
|
|
183
224
|
const running = context.services.get(RUNTIME);
|
|
184
225
|
const judge = providers.judge;
|
|
185
226
|
const confirmations = new ConfirmationJudge({
|
|
@@ -189,8 +230,17 @@ export function agentServerPlugin(
|
|
|
189
230
|
logger,
|
|
190
231
|
});
|
|
191
232
|
const built: DiscordAgentTeam = new DiscordAgentTeam({
|
|
192
|
-
guildId:
|
|
193
|
-
|
|
233
|
+
guildId: teamId,
|
|
234
|
+
...(options.entryChannelId !== undefined
|
|
235
|
+
? { entryChannelId: options.entryChannelId }
|
|
236
|
+
: {}),
|
|
237
|
+
// Without an entry channel the coordinator is the named agent, else the first seed.
|
|
238
|
+
...(options.entryChannelId === undefined
|
|
239
|
+
? {
|
|
240
|
+
coordinator: () =>
|
|
241
|
+
options.coordinator ?? context.sessions().seeds[0]?.name,
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
194
244
|
seeds: () => context.sessions().seeds,
|
|
195
245
|
promptSections: () => context.sessions().prompt,
|
|
196
246
|
pluginTools: () => context.sessions().agentTools,
|
|
@@ -201,7 +251,9 @@ export function agentServerPlugin(
|
|
|
201
251
|
shellUser: options.shellUser,
|
|
202
252
|
...(options.scratchDir ? { scratchDir: options.scratchDir } : {}),
|
|
203
253
|
store,
|
|
204
|
-
|
|
254
|
+
...(connection && options.guildId
|
|
255
|
+
? { channels: connection.agentChannels(options.guildId) }
|
|
256
|
+
: {}),
|
|
205
257
|
studio,
|
|
206
258
|
runtime: () => running,
|
|
207
259
|
confirmations,
|
|
@@ -223,6 +275,7 @@ export function agentServerPlugin(
|
|
|
223
275
|
},
|
|
224
276
|
schedules,
|
|
225
277
|
queue: context.queue,
|
|
278
|
+
surfaces,
|
|
226
279
|
startTyping: (channel) => surfaces.startTyping(channel),
|
|
227
280
|
showStop: (channel) => surfaces.showStop(channel),
|
|
228
281
|
workDir: options.workDir,
|
|
@@ -231,10 +284,10 @@ export function agentServerPlugin(
|
|
|
231
284
|
? { guestPrompt: options.prompts.guest }
|
|
232
285
|
: {}),
|
|
233
286
|
...(skills ? { skills } : {}),
|
|
234
|
-
threads:
|
|
287
|
+
...(threads ? { threads } : {}),
|
|
235
288
|
logger,
|
|
236
289
|
});
|
|
237
|
-
connection
|
|
290
|
+
connection?.onChannelDeleted((channelId) => {
|
|
238
291
|
if (!built.owns(discordKey(channelId))) return;
|
|
239
292
|
void context.queue
|
|
240
293
|
.run(discordKey(channelId), () => built.channelDeleted(channelId))
|
|
@@ -263,21 +316,23 @@ export function agentServerPlugin(
|
|
|
263
316
|
});
|
|
264
317
|
const attachmentDir = (channel: ChannelKey) =>
|
|
265
318
|
ownerAttachmentDir(options.dataDir, channel);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
319
|
+
// The dashboard is a Discord message; a team without Discord has none.
|
|
320
|
+
const dashboard =
|
|
321
|
+
connection && options.guildId
|
|
322
|
+
? new AgentDashboard({
|
|
323
|
+
status: () => built.status(),
|
|
324
|
+
board: connection.agentDashboard(options.guildId),
|
|
325
|
+
lines: () => context.dashboard(),
|
|
326
|
+
logger,
|
|
327
|
+
})
|
|
328
|
+
: undefined;
|
|
329
|
+
if (dashboard) built.onChange(() => dashboard.changed());
|
|
273
330
|
const connectErrorReporter = () =>
|
|
274
331
|
options.errorReporter?.connect({
|
|
275
|
-
channelOf: (name) =>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
: undefined;
|
|
280
|
-
},
|
|
332
|
+
channelOf: (name) =>
|
|
333
|
+
store.agent(name)?.status === "active"
|
|
334
|
+
? built.roomsOf(name)[0]
|
|
335
|
+
: undefined,
|
|
281
336
|
post: (channel, text) =>
|
|
282
337
|
surfaces.sendReply(channel, { chunks: splitReply(text) }),
|
|
283
338
|
turn: (channel, text) =>
|
|
@@ -288,7 +343,9 @@ export function agentServerPlugin(
|
|
|
288
343
|
});
|
|
289
344
|
return {
|
|
290
345
|
services: [
|
|
291
|
-
|
|
346
|
+
...(dashboard
|
|
347
|
+
? [{ name: "dashboard", stop: () => dashboard.stop() }]
|
|
348
|
+
: []),
|
|
292
349
|
{
|
|
293
350
|
name: AGENT_TEAM_SERVICE,
|
|
294
351
|
// The dashboard shows the team, so it starts once the team has; the error
|
|
@@ -296,7 +353,7 @@ export function agentServerPlugin(
|
|
|
296
353
|
startInBackground: async () => {
|
|
297
354
|
try {
|
|
298
355
|
await built.start();
|
|
299
|
-
dashboard
|
|
356
|
+
dashboard?.start();
|
|
300
357
|
} finally {
|
|
301
358
|
connectErrorReporter();
|
|
302
359
|
}
|
|
@@ -315,10 +372,16 @@ export function agentServerPlugin(
|
|
|
315
372
|
? { attachmentDownload: options.attachmentDownload }
|
|
316
373
|
: {}),
|
|
317
374
|
logger,
|
|
318
|
-
|
|
375
|
+
...(discord
|
|
376
|
+
? { channelContext: (message) => discord.channelContext(message) }
|
|
377
|
+
: {}),
|
|
319
378
|
}),
|
|
320
379
|
],
|
|
321
|
-
|
|
380
|
+
// The pictures are public on a listener only where a public address reaches it.
|
|
381
|
+
http:
|
|
382
|
+
options.avatarListener && options.avatarUrl
|
|
383
|
+
? [studio.route(options.avatarListener)]
|
|
384
|
+
: [],
|
|
322
385
|
sessionTools: agentSessionTools(built),
|
|
323
386
|
toolTiers: AGENT_TIERS,
|
|
324
387
|
};
|
|
@@ -11,7 +11,6 @@ import type { DiscordConnection } from "../discord/connection.ts";
|
|
|
11
11
|
import { discordDirectChannel } from "../discord/direct-channel.ts";
|
|
12
12
|
import { DiscordOwners } from "../discord/discord-owners.ts";
|
|
13
13
|
import { DiscordSurface } from "../discord/discord-surface.ts";
|
|
14
|
-
import { DispatchThreads } from "../discord/dispatch-threads.ts";
|
|
15
14
|
import type {
|
|
16
15
|
CommandGuard,
|
|
17
16
|
CommandRegistrar,
|
|
@@ -23,7 +22,8 @@ import { restOf, restPoster } from "../discord/poster/rest-poster.ts";
|
|
|
23
22
|
import { stopButtonModule } from "../discord/stop-button.ts";
|
|
24
23
|
import type { InboundMessage } from "../domain/conversation.ts";
|
|
25
24
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
26
|
-
import { IDENTITY } from "../services.ts";
|
|
25
|
+
import { IDENTITY, THREADS } from "../services.ts";
|
|
26
|
+
import type { DispatchThreads } from "../threads/dispatch-threads.ts";
|
|
27
27
|
|
|
28
28
|
/** The Discord connection and what stands on it, as plugins use it. Provided by the Discord plugin. */
|
|
29
29
|
export interface DiscordServices {
|
|
@@ -161,19 +161,8 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
161
161
|
? {}
|
|
162
162
|
: { refusalHint: options.refusalHint }),
|
|
163
163
|
});
|
|
164
|
-
//
|
|
165
|
-
const threads =
|
|
166
|
-
host: {
|
|
167
|
-
open: (parentId, name, line) =>
|
|
168
|
-
connected.threadHost().open(parentId, name, line),
|
|
169
|
-
post: (threadId, text) => connected.threadHost().post(threadId, text),
|
|
170
|
-
send: (threadId, text) => connected.threadHost().send(threadId, text),
|
|
171
|
-
close: (threadId) => connected.threadHost().close(threadId),
|
|
172
|
-
},
|
|
173
|
-
ledgerPath: join(options.dataDir, "dispatch-threads.json"),
|
|
174
|
-
excluded: (channel) => conversations.postsInPlace(channel),
|
|
175
|
-
logger,
|
|
176
|
-
});
|
|
164
|
+
// The host's dispatch threads, which reach Discord's through its surface.
|
|
165
|
+
const threads = services.get(THREADS);
|
|
177
166
|
// The cards answer the interactions of held actions, and core posts the stop button, so
|
|
178
167
|
// core answers both; they come first, as they always have.
|
|
179
168
|
collection.registrar.add({ module: cards });
|
|
@@ -215,8 +204,6 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
215
204
|
}),
|
|
216
205
|
],
|
|
217
206
|
services: [
|
|
218
|
-
// The sweep reads threads through the connection, so it follows the surface's start.
|
|
219
|
-
{ name: "threads", start: () => void threads.sweep() },
|
|
220
207
|
// The copy texts of the last cards are on disk before the host exits.
|
|
221
208
|
{
|
|
222
209
|
name: "card-copies",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
IDENTITY,
|
|
40
40
|
PRECHECKS,
|
|
41
41
|
SCHEDULES,
|
|
42
|
+
THREADS,
|
|
42
43
|
} from "../services.ts";
|
|
43
44
|
import type { SessionContext } from "../sessions.ts";
|
|
44
45
|
import { DELEGATE_TOOL } from "../shared/delegate-tool.ts";
|
|
@@ -119,11 +120,11 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
119
120
|
const schedules = services.get(SCHEDULES);
|
|
120
121
|
// Absent when a plugin list leaves the prechecks plugin out: then none can be attached.
|
|
121
122
|
const prechecks = services.find(PRECHECKS);
|
|
122
|
-
// Absent
|
|
123
|
-
const
|
|
123
|
+
// Absent when a plugin list leaves the threads plugin out: then no dispatch opens a thread.
|
|
124
|
+
const threads = services.find(THREADS);
|
|
124
125
|
// Another agent's channel, for schedule_list; asked when a tool runs, after the agent server set up.
|
|
125
|
-
const agentChannelOf = (agent: string) =>
|
|
126
|
-
services.get(AGENTS).team.channelOf(agent);
|
|
126
|
+
const agentChannelOf = (agent: string, near: ChannelKey) =>
|
|
127
|
+
services.get(AGENTS).team.channelOf(agent, near);
|
|
127
128
|
// A conversation no chat surface carries keeps its runs in the creator's direct messages, where
|
|
128
129
|
// a claim must take them; anyone's elsewhere would run in a conversation that is not theirs.
|
|
129
130
|
const channelFor = async (
|
|
@@ -166,7 +167,7 @@ export function modulesPlugin(options: ModulesOptions): RoundtablePlugin {
|
|
|
166
167
|
}),
|
|
167
168
|
targets: (name) => conversations.target(name),
|
|
168
169
|
deliver: (job, outcome) => background.runDelegated(job, outcome),
|
|
169
|
-
...(
|
|
170
|
+
...(threads ? { threads } : {}),
|
|
170
171
|
logger,
|
|
171
172
|
});
|
|
172
173
|
services.provide(BACKGROUND_TURNS, background);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { RoundtablePlugin } from "../plugin.ts";
|
|
3
|
+
import { THREADS } from "../services.ts";
|
|
4
|
+
import { DispatchThreads } from "../threads/dispatch-threads.ts";
|
|
5
|
+
|
|
6
|
+
export interface ThreadsOptions {
|
|
7
|
+
/** Where the ledger of the threads still open is kept, as `dispatch-threads.json`. */
|
|
8
|
+
dataDir: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The dispatch threads of every surface that hosts them. The plugin provides one instance, over
|
|
13
|
+
* the host's surfaces; the Discord plugin hands the same one out as `DISCORD.threads`. After a
|
|
14
|
+
* restart it closes the threads the last run left open, once every service has started, so each
|
|
15
|
+
* surface is up to close its own.
|
|
16
|
+
*/
|
|
17
|
+
export function threadsPlugin(options: ThreadsOptions): RoundtablePlugin {
|
|
18
|
+
return {
|
|
19
|
+
name: "threads",
|
|
20
|
+
provides: [THREADS],
|
|
21
|
+
setup: ({ conversations, services, surfaces, logger }) => {
|
|
22
|
+
const threads = new DispatchThreads({
|
|
23
|
+
surfaces,
|
|
24
|
+
ledgerPath: join(options.dataDir, "dispatch-threads.json"),
|
|
25
|
+
// A channel whose claim keeps reports in place, such as an open channel, opens no thread.
|
|
26
|
+
excluded: (channel) => conversations.postsInPlace(channel),
|
|
27
|
+
logger,
|
|
28
|
+
});
|
|
29
|
+
services.provide(THREADS, threads);
|
|
30
|
+
return {
|
|
31
|
+
services: [
|
|
32
|
+
{ name: "threads", startInBackground: () => threads.sweep() },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
+
|
|
3
|
+
/** The keys of the configuration that shape the agent team. */
|
|
4
|
+
interface AgentTeamKeys {
|
|
5
|
+
agents?: readonly { name: string; channelId?: string }[];
|
|
6
|
+
coordinator?: string;
|
|
7
|
+
skills?: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Refuses what the agent team needs and a host without Discord lacks, naming the key; and the
|
|
12
|
+
* coordinator, which only a host without Discord names: with Discord it is the entry channel's agent.
|
|
13
|
+
*/
|
|
14
|
+
export function checkAgentTeamKeys(
|
|
15
|
+
config: AgentTeamKeys,
|
|
16
|
+
withDiscord: boolean,
|
|
17
|
+
): void {
|
|
18
|
+
const seeds = config.agents ?? [];
|
|
19
|
+
if (config.coordinator !== undefined) {
|
|
20
|
+
if (withDiscord)
|
|
21
|
+
throw new ConfigError(
|
|
22
|
+
"config coordinator: the entry channel's agent is the coordinator with discord. Remove coordinator, or move the agent to discord.entryChannel.",
|
|
23
|
+
);
|
|
24
|
+
if (seeds.length === 0)
|
|
25
|
+
throw new ConfigError(
|
|
26
|
+
"config coordinator: needs agents, and names one of them. Add the agent to agents, or remove coordinator.",
|
|
27
|
+
);
|
|
28
|
+
if (!seeds.some((seed) => seed.name === config.coordinator))
|
|
29
|
+
throw new ConfigError(
|
|
30
|
+
`config coordinator: ${JSON.stringify(config.coordinator)} is not one of agents. Name one of ${seeds.map((seed) => seed.name).join(", ")}.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (withDiscord) return;
|
|
34
|
+
const placed = seeds.findIndex((seed) => seed.channelId !== undefined);
|
|
35
|
+
if (placed >= 0)
|
|
36
|
+
throw new ConfigError(
|
|
37
|
+
`config agents[${placed}].channelId: a Discord channel, and Discord is not configured. Remove channelId, or configure discord.`,
|
|
38
|
+
);
|
|
39
|
+
if (config.skills && seeds.length === 0)
|
|
40
|
+
throw new ConfigError(
|
|
41
|
+
"config skills: the skills are the agents', and this host has none: without discord they need agents. Configure agents or discord, or remove skills.",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
import type { PerPrincipalLimits } from "../modules/background/personal-target.ts";
|
|
18
18
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
19
19
|
import { PRIMARY_CHARS } from "../runtime/interim-text.ts";
|
|
20
|
-
import type { ChannelKey } from "../sessions.ts";
|
|
21
20
|
import type { Tier, TierMembers } from "../speakers.ts";
|
|
22
21
|
import { type AccessConfig, accessOf, accessShape } from "./access.ts";
|
|
23
22
|
import {
|
|
@@ -27,9 +26,18 @@ import {
|
|
|
27
26
|
type ResolvedDiscord,
|
|
28
27
|
resolvedDiscord,
|
|
29
28
|
} from "./adapters.ts";
|
|
29
|
+
import { checkAgentTeamKeys } from "./agent-team.ts";
|
|
30
30
|
import { type AttachmentsConfig, attachmentsShape } from "./attachments.ts";
|
|
31
|
+
import {
|
|
32
|
+
type LimitsConfig,
|
|
33
|
+
limitsOf,
|
|
34
|
+
limitsShape,
|
|
35
|
+
type ResolvedLimits,
|
|
36
|
+
} from "./limits.ts";
|
|
37
|
+
import { opsOf, type ResolvedOps } from "./ops.ts";
|
|
31
38
|
import {
|
|
32
39
|
bool,
|
|
40
|
+
characterInput,
|
|
33
41
|
guarded,
|
|
34
42
|
integer,
|
|
35
43
|
list,
|
|
@@ -74,7 +82,7 @@ export interface BackgroundConfig {
|
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** What `roundtable.config.ts` gives `defineRoundtable`. */
|
|
77
|
-
export interface RoundtableConfig {
|
|
85
|
+
export interface RoundtableConfig extends LimitsConfig {
|
|
78
86
|
/** The assistant's display name; default "Roundtable". */
|
|
79
87
|
name?: string;
|
|
80
88
|
/**
|
|
@@ -85,9 +93,11 @@ export interface RoundtableConfig {
|
|
|
85
93
|
owner?: { id: string; name: string; pronouns?: Pronouns };
|
|
86
94
|
/**
|
|
87
95
|
* The Discord bot, its guild, and the agent server that lives there. Leave it out for a host
|
|
88
|
-
* without Discord: no Discord plugins
|
|
89
|
-
*
|
|
90
|
-
*
|
|
96
|
+
* without Discord: no Discord plugins and no tool that messages the owner on Discord;
|
|
97
|
+
* conversations come through the plugins' own surfaces. The agent team (`agents`, `skills`) then
|
|
98
|
+
* lives in the rooms of a surface that hosts them, such as pi-roundtable-web, and the host
|
|
99
|
+
* refuses to start without one. The same settings may come as `adapters: [discord({...})]`
|
|
100
|
+
* instead, never both.
|
|
91
101
|
*/
|
|
92
102
|
discord?: DiscordConfig;
|
|
93
103
|
/**
|
|
@@ -133,8 +143,17 @@ export interface RoundtableConfig {
|
|
|
133
143
|
toolTiers?: Record<string, Tier>;
|
|
134
144
|
/** Prompt files, read once at start; `shared` starts every agent's prompt. */
|
|
135
145
|
prompts?: { shared: string; guest?: string };
|
|
136
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* The first team, created once; an agent already stored is never overwritten. Without `discord`
|
|
148
|
+
* the team lives in the rooms of a surface that hosts them, so a seed has no `channelId`.
|
|
149
|
+
*/
|
|
137
150
|
agents?: AgentSeed[];
|
|
151
|
+
/**
|
|
152
|
+
* Without `discord`, the name of the agent that coordinates the team: the one the owner's
|
|
153
|
+
* general requests come to, which cannot be archived. It must be one of `agents`; default the
|
|
154
|
+
* first. With `discord` the agent in `discord.entryChannel` is the coordinator, and this key is refused.
|
|
155
|
+
*/
|
|
156
|
+
coordinator?: string;
|
|
138
157
|
/**
|
|
139
158
|
* The `public` listener: the public address that reaches it, and the TCP port (default 3000) or
|
|
140
159
|
* unix socket the process listens on. `socketMode` is the socket file's permission bits
|
|
@@ -149,11 +168,7 @@ export interface RoundtableConfig {
|
|
|
149
168
|
socketPath?: string;
|
|
150
169
|
socketMode?: number;
|
|
151
170
|
};
|
|
152
|
-
/**
|
|
153
|
-
* A picture of your own for agents without one, and the style reference for drawing them; default
|
|
154
|
-
* a plain bot. A PNG, JPEG or GIF of any side within the 64 MP decoded-pixel budget (an ordinary
|
|
155
|
-
* camera photo); one that cannot be read as a picture is logged and the plain bot is used.
|
|
156
|
-
*/
|
|
171
|
+
/** @deprecated Agents are composed characters since 0.23, so this picture is ignored and a warning is logged at start; removed in 1.0. */
|
|
157
172
|
avatar?: string;
|
|
158
173
|
/** The agents' shared working directory; default `<dataDir>/work`. */
|
|
159
174
|
workDir?: string;
|
|
@@ -166,7 +181,7 @@ export interface RoundtableConfig {
|
|
|
166
181
|
/**
|
|
167
182
|
* Where the skill registry reads built-in skills and keeps linked repositories. `false` leaves
|
|
168
183
|
* the `skills` addon out: agents carry no skills, and no skill tools exist. Stored skills stay
|
|
169
|
-
* in their tables. Without `discord` there
|
|
184
|
+
* in their tables. Without `discord` and `agents` there is no one to carry skills, so the addon is off.
|
|
170
185
|
*/
|
|
171
186
|
skills?: false | SkillsConfig;
|
|
172
187
|
/**
|
|
@@ -267,11 +282,13 @@ const schema = shape({
|
|
|
267
282
|
name: text,
|
|
268
283
|
displayName: text,
|
|
269
284
|
prompt: text,
|
|
270
|
-
|
|
285
|
+
character: optional(characterInput),
|
|
286
|
+
avatarPrompt: optional(text),
|
|
271
287
|
channelId: optional(text),
|
|
272
288
|
}),
|
|
273
289
|
),
|
|
274
290
|
),
|
|
291
|
+
coordinator: optional(text),
|
|
275
292
|
http: optional(
|
|
276
293
|
shape({
|
|
277
294
|
publicUrl: optional(text),
|
|
@@ -288,6 +305,7 @@ const schema = shape({
|
|
|
288
305
|
memory: optional(bool),
|
|
289
306
|
ops: optional(shape({ agent: optional(text), conversation: optional(text) })),
|
|
290
307
|
drainSeconds: optional(integer(1, 3_600)),
|
|
308
|
+
...limitsShape,
|
|
291
309
|
interimText: optional(oneOf<InterimTextMode>("on", "off")),
|
|
292
310
|
interimPrimaryChars: optional(integer(1, 100_000)),
|
|
293
311
|
plugins: optional(
|
|
@@ -297,8 +315,12 @@ const schema = shape({
|
|
|
297
315
|
),
|
|
298
316
|
});
|
|
299
317
|
|
|
318
|
+
/** Logged once when `avatar` is set: agents are composed characters and no picture is drawn from it. */
|
|
319
|
+
export const AVATAR_DEPRECATION =
|
|
320
|
+
"agents are composed characters since 0.23; `avatar` is ignored";
|
|
321
|
+
|
|
300
322
|
/** The configuration with every default filled in. */
|
|
301
|
-
export interface ResolvedConfig {
|
|
323
|
+
export interface ResolvedConfig extends ResolvedLimits {
|
|
302
324
|
name: string;
|
|
303
325
|
/**
|
|
304
326
|
* The process's short name: the logger's and the scratch dir's, and the root slash command's
|
|
@@ -337,6 +359,8 @@ export interface ResolvedConfig {
|
|
|
337
359
|
toolTiers: Record<string, Tier>;
|
|
338
360
|
prompts: { shared?: string; guest?: string };
|
|
339
361
|
agents: AgentSeed[];
|
|
362
|
+
/** The coordinator's name; set only without `discord` and with `agents`: the configured one, else the first seed. */
|
|
363
|
+
coordinator?: string;
|
|
340
364
|
/** Undefined when no `http` is configured, and the host opens no `public` listener. */
|
|
341
365
|
http?: {
|
|
342
366
|
/** Always set with Discord. */
|
|
@@ -352,7 +376,7 @@ export interface ResolvedConfig {
|
|
|
352
376
|
/** `false` when the skills addon is off. */
|
|
353
377
|
skills: false | SkillsConfig;
|
|
354
378
|
memory: boolean;
|
|
355
|
-
ops?:
|
|
379
|
+
ops?: ResolvedOps;
|
|
356
380
|
drainMs: number;
|
|
357
381
|
interimText: InterimTextMode;
|
|
358
382
|
interimPrimaryChars: number;
|
|
@@ -368,47 +392,6 @@ function modelOf(value: string, path: string): ModelRef {
|
|
|
368
392
|
return parsed;
|
|
369
393
|
}
|
|
370
394
|
|
|
371
|
-
/** Where the errors go: one of an agent or a conversation; an agent only where the agents live. */
|
|
372
|
-
function opsOf(
|
|
373
|
-
ops: { agent?: string; conversation?: string } | undefined,
|
|
374
|
-
withDiscord: boolean,
|
|
375
|
-
): ResolvedConfig["ops"] {
|
|
376
|
-
if (!ops) return undefined;
|
|
377
|
-
const { agent, conversation } = ops;
|
|
378
|
-
if (agent !== undefined && conversation !== undefined)
|
|
379
|
-
throw new ConfigError(
|
|
380
|
-
"config ops: name an agent or a conversation, not both. Keep the one the reports go to.",
|
|
381
|
-
);
|
|
382
|
-
if (agent !== undefined) {
|
|
383
|
-
if (!withDiscord)
|
|
384
|
-
throw new ConfigError(
|
|
385
|
-
'config ops.agent: the agents live in Discord, which is not configured. Report to a conversation a plugin\'s chat surface serves instead, with ops: { conversation: "<surface>:<id>" }; the web chat takes no error reports.',
|
|
386
|
-
);
|
|
387
|
-
return { agent };
|
|
388
|
-
}
|
|
389
|
-
if (conversation === undefined)
|
|
390
|
-
throw new ConfigError(
|
|
391
|
-
'config ops: name an agent or a conversation, such as ops: { agent: "infra" } or ops: { conversation: "<surface>:<id>" }.',
|
|
392
|
-
);
|
|
393
|
-
if (!/^[^:]+:.+$/.test(conversation))
|
|
394
|
-
throw new ConfigError(
|
|
395
|
-
`config ops.conversation: expected a conversation key <surface>:<id>, got ${JSON.stringify(conversation)}. Write the surface's prefix, a colon, and the conversation's id.`,
|
|
396
|
-
);
|
|
397
|
-
return { conversation: conversation as ChannelKey };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/** Refuses what only the agent server serves on a host without Discord, naming the key. */
|
|
401
|
-
function refuseAgentServerKeys(config: RoundtableConfig): void {
|
|
402
|
-
if (config.agents && config.agents.length > 0)
|
|
403
|
-
throw new ConfigError(
|
|
404
|
-
"config agents: the agents live in Discord, which is not configured. Configure discord, or remove agents.",
|
|
405
|
-
);
|
|
406
|
-
if (config.skills)
|
|
407
|
-
throw new ConfigError(
|
|
408
|
-
"config skills: the skills are the agents', and the agents live in Discord, which is not configured. Configure discord, or remove skills.",
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
395
|
/** The configuration checked against its schema, defaults filled; throws a ConfigError naming the key and the fix. */
|
|
413
396
|
export function resolveConfig(input: unknown): ResolvedConfig {
|
|
414
397
|
const config = schema.check(liftAdapters(input), "") as RoundtableConfig;
|
|
@@ -420,7 +403,7 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
420
403
|
throw new ConfigError(
|
|
421
404
|
"config http.publicUrl: required with discord, expected the public address that reaches the bot; Discord fetches the agents' avatars from it. Add it to roundtable.config.ts.",
|
|
422
405
|
);
|
|
423
|
-
|
|
406
|
+
checkAgentTeamKeys(config, discord !== undefined);
|
|
424
407
|
const slug =
|
|
425
408
|
discord?.rootCommand ?? name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
426
409
|
const ops = opsOf(config.ops, discord !== undefined);
|
|
@@ -430,6 +413,10 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
430
413
|
name,
|
|
431
414
|
slug,
|
|
432
415
|
...access,
|
|
416
|
+
deprecations: [
|
|
417
|
+
...access.deprecations,
|
|
418
|
+
...(config.avatar ? [AVATAR_DEPRECATION] : []),
|
|
419
|
+
],
|
|
433
420
|
owner: {
|
|
434
421
|
id: primaryOwner.id,
|
|
435
422
|
name: primaryOwner.name,
|
|
@@ -467,6 +454,9 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
467
454
|
toolTiers: config.toolTiers ?? {},
|
|
468
455
|
prompts: config.prompts ?? {},
|
|
469
456
|
agents: config.agents ?? [],
|
|
457
|
+
...(!discord && config.agents?.[0]
|
|
458
|
+
? { coordinator: config.coordinator ?? config.agents[0].name }
|
|
459
|
+
: {}),
|
|
470
460
|
...(http
|
|
471
461
|
? {
|
|
472
462
|
http: {
|
|
@@ -483,11 +473,12 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
483
473
|
...(config.avatar ? { avatar: config.avatar } : {}),
|
|
484
474
|
workDir: config.workDir ?? `${config.dataDir}/work`,
|
|
485
475
|
scratchDir: config.scratchDir ?? join(tmpdir(), `${slug}-scratch`),
|
|
486
|
-
// Without Discord there
|
|
487
|
-
skills: config.skills ?? (discord ? {} : false),
|
|
476
|
+
// Without Discord and without agents there is no one to carry skills, so the addon is off.
|
|
477
|
+
skills: config.skills ?? (discord || config.agents?.length ? {} : false),
|
|
488
478
|
memory: config.memory ?? true,
|
|
489
479
|
...(ops ? { ops } : {}),
|
|
490
480
|
drainMs: (config.drainSeconds ?? DRAIN_LIMIT_MS / 1_000) * 1_000,
|
|
481
|
+
...limitsOf(config),
|
|
491
482
|
interimText: config.interimText ?? "on",
|
|
492
483
|
interimPrimaryChars: config.interimPrimaryChars ?? PRIMARY_CHARS,
|
|
493
484
|
plugins: config.plugins ?? [],
|