pi-roundtable 0.23.1 → 0.24.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 +30 -0
- package/docs/plugins.md +39 -6
- package/package.json +1 -1
- package/src/characters/compose.ts +18 -3
- package/src/characters/index.ts +1 -0
- package/src/characters/parts-hats.ts +12 -12
- package/src/characters/parts-outfits.ts +12 -12
- package/src/characters/parts-props.ts +3 -3
- package/src/characters/skins.ts +36 -0
- package/src/cli/checks/discord.ts +4 -1
- package/src/core/agents/agent-store.ts +22 -0
- package/src/core/agents/agent-team.ts +5 -0
- package/src/core/agents/avatar-refresh.ts +77 -0
- package/src/core/agents/avatar-studio.ts +13 -2
- package/src/core/agents/team-lifecycle.ts +27 -2
- package/src/core/agents/team-options.ts +6 -0
- package/src/core/builtin/agent-server.ts +14 -0
- package/src/core/config/adapters.ts +8 -5
- package/src/core/config/agent-team.ts +36 -0
- package/src/core/config/config-schema.ts +138 -0
- package/src/core/config/config.ts +64 -133
- package/src/core/config/schema.ts +18 -0
- package/src/core/define-roundtable.ts +15 -7
- package/src/core/host-registration.ts +25 -0
- package/src/core/host.ts +3 -0
- package/src/core/plugin-removed.ts +85 -0
- package/src/core/plugin.ts +14 -77
- package/src/core/services.ts +21 -2
- package/src/core/shared/workspace-files.ts +7 -4
- package/src/index.ts +1 -0
- package/src/kit/index.ts +1 -0
- package/src/kit/workspace.ts +3 -0
- package/src/testing.ts +2 -0
|
@@ -1,54 +1,36 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { CATALOG, type Hex } from "../../characters/index.ts";
|
|
3
4
|
import type { AgentSeed } from "../agents/agent-rules.ts";
|
|
4
5
|
import { ConfigError } from "../domain/errors.ts";
|
|
5
6
|
import type { InterimTextMode } from "../domain/interim.ts";
|
|
6
7
|
import { DRAIN_LIMIT_MS } from "../drain.ts";
|
|
7
|
-
import {
|
|
8
|
+
import type { Locale } from "../i18n/index.ts";
|
|
8
9
|
import type { AccessRules } from "../identity/access-policy.ts";
|
|
9
10
|
import type { OwnerIdentity } from "../identity.ts";
|
|
10
11
|
import {
|
|
11
12
|
type EffortPolicy,
|
|
12
13
|
type ModelRef,
|
|
13
14
|
parseModelRef,
|
|
14
|
-
THINKING_LEVELS,
|
|
15
15
|
type ThinkingLevel,
|
|
16
16
|
} from "../models.ts";
|
|
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
20
|
import type { Tier, TierMembers } from "../speakers.ts";
|
|
21
|
-
import { type AccessConfig, accessOf
|
|
21
|
+
import { type AccessConfig, accessOf } from "./access.ts";
|
|
22
22
|
import {
|
|
23
23
|
type DiscordConfig,
|
|
24
|
-
discordShape,
|
|
25
24
|
liftAdapters,
|
|
26
25
|
type ResolvedDiscord,
|
|
27
26
|
resolvedDiscord,
|
|
28
27
|
} from "./adapters.ts";
|
|
29
|
-
import { checkAgentTeamKeys } from "./agent-team.ts";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
limitsOf,
|
|
34
|
-
limitsShape,
|
|
35
|
-
type ResolvedLimits,
|
|
36
|
-
} from "./limits.ts";
|
|
28
|
+
import { checkAgentServerOff, checkAgentTeamKeys } from "./agent-team.ts";
|
|
29
|
+
import type { AttachmentsConfig } from "./attachments.ts";
|
|
30
|
+
import { configSchema } from "./config-schema.ts";
|
|
31
|
+
import { type LimitsConfig, limitsOf, type ResolvedLimits } from "./limits.ts";
|
|
37
32
|
import { opsOf, type ResolvedOps } from "./ops.ts";
|
|
38
|
-
import {
|
|
39
|
-
bool,
|
|
40
|
-
characterInput,
|
|
41
|
-
guarded,
|
|
42
|
-
integer,
|
|
43
|
-
list,
|
|
44
|
-
number,
|
|
45
|
-
oneOf,
|
|
46
|
-
optional,
|
|
47
|
-
record,
|
|
48
|
-
shape,
|
|
49
|
-
text,
|
|
50
|
-
} from "./schema.ts";
|
|
51
|
-
import { type SkillsConfig, skillsField } from "./skills-config.ts";
|
|
33
|
+
import type { SkillsConfig } from "./skills-config.ts";
|
|
52
34
|
|
|
53
35
|
export type { DiscordConfig };
|
|
54
36
|
|
|
@@ -100,6 +82,17 @@ export interface RoundtableConfig extends LimitsConfig {
|
|
|
100
82
|
* instead, never both.
|
|
101
83
|
*/
|
|
102
84
|
discord?: DiscordConfig;
|
|
85
|
+
/**
|
|
86
|
+
* `false` leaves the agent server out of a host that has `discord`: no agent team, no
|
|
87
|
+
* `#dashboard` channel or pinned message, no agent channels or routes, no coordinator, and no
|
|
88
|
+
* skills addon. Discord, access, memory, commands and the plugins' claims work as before;
|
|
89
|
+
* `discord.entryChannel` is optional and `http.publicUrl` is no longer required (it serves only
|
|
90
|
+
* the agents' avatars). Refused together with anything only agents use: `agents`, `skills`,
|
|
91
|
+
* `ops.agent`, `discord.agentMemory`, `discord.agentSpeakers`, `discord.attachmentDownload`.
|
|
92
|
+
* Plugins that need the agent server (pi-roundtable-web, remote-mcp's default conversation)
|
|
93
|
+
* stop startup. Left out, the agent server runs; there is no `true`.
|
|
94
|
+
*/
|
|
95
|
+
agentServer?: false;
|
|
103
96
|
/**
|
|
104
97
|
* The chat networks the host talks through, each made by its adapter's factory, such as
|
|
105
98
|
* `discord()` from `pi-roundtable/discord`. A Discord adapter is the same as the top-level
|
|
@@ -154,6 +147,20 @@ export interface RoundtableConfig extends LimitsConfig {
|
|
|
154
147
|
* first. With `discord` the agent in `discord.entryChannel` is the coordinator, and this key is refused.
|
|
155
148
|
*/
|
|
156
149
|
coordinator?: string;
|
|
150
|
+
/**
|
|
151
|
+
* How the agents' characters are drawn, everywhere the host draws them: the web console, its
|
|
152
|
+
* round table and the agents' pictures on Discord. `skins` is exactly six `#rrggbb` skin tones
|
|
153
|
+
* that a look's `skin` index (0 to 5) picks from, such as `EAST_ASIAN_SKINS` from
|
|
154
|
+
* `pi-roundtable/characters`; default the six the package ships. Stored looks keep their index,
|
|
155
|
+
* so they take the new tones with no migration. A bad list stops the start.
|
|
156
|
+
*
|
|
157
|
+
* `refreshAvatars` (default false) renders every agent's Discord picture again each time the
|
|
158
|
+
* agent server starts, with the host's skins, and sets it where it differs from the stored one,
|
|
159
|
+
* so an agent that was not saved since a redraw or a `skins` change catches up. It overwrites
|
|
160
|
+
* an agent that still wears an old hand-drawn (deprecated) picture. It runs after the start
|
|
161
|
+
* without holding it back; a restart with nothing changed sets nothing.
|
|
162
|
+
*/
|
|
163
|
+
characters?: { skins?: readonly string[]; refreshAvatars?: boolean };
|
|
157
164
|
/**
|
|
158
165
|
* The `public` listener: the public address that reaches it, and the TCP port (default 3000) or
|
|
159
166
|
* unix socket the process listens on. `socketMode` is the socket file's permission bits
|
|
@@ -212,109 +219,6 @@ export interface RoundtableConfig extends LimitsConfig {
|
|
|
212
219
|
plugins?: RoundtablePlugin[];
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
const tierMembers = shape({
|
|
216
|
-
users: optional(list(text)),
|
|
217
|
-
roles: optional(list(text)),
|
|
218
|
-
everyone: optional(bool),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const isPolicy = (value: unknown): value is EffortPolicy =>
|
|
222
|
-
typeof value === "function";
|
|
223
|
-
const isPlugin = (value: unknown): value is RoundtablePlugin =>
|
|
224
|
-
typeof value === "object" &&
|
|
225
|
-
value !== null &&
|
|
226
|
-
typeof (value as { name?: unknown }).name === "string" &&
|
|
227
|
-
typeof (value as { setup?: unknown }).setup === "function";
|
|
228
|
-
|
|
229
|
-
const schema = shape({
|
|
230
|
-
name: optional(text),
|
|
231
|
-
owner: optional(
|
|
232
|
-
shape({
|
|
233
|
-
id: text,
|
|
234
|
-
name: text,
|
|
235
|
-
pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
|
|
236
|
-
}),
|
|
237
|
-
),
|
|
238
|
-
discord: optional(discordShape),
|
|
239
|
-
database: shape({ url: text }),
|
|
240
|
-
dataDir: text,
|
|
241
|
-
attachments: attachmentsShape,
|
|
242
|
-
agentDir: optional(text),
|
|
243
|
-
model: text,
|
|
244
|
-
thinking: optional(oneOf(...THINKING_LEVELS)),
|
|
245
|
-
judge: optional(
|
|
246
|
-
shape({ model: optional(text), threshold: optional(number(0, 1)) }),
|
|
247
|
-
),
|
|
248
|
-
effortPolicy: optional(
|
|
249
|
-
guarded("a function from a turn to its effort rule", isPolicy),
|
|
250
|
-
),
|
|
251
|
-
delegation: optional(
|
|
252
|
-
shape({
|
|
253
|
-
model: optional(text),
|
|
254
|
-
thinking: optional(oneOf(...THINKING_LEVELS)),
|
|
255
|
-
}),
|
|
256
|
-
),
|
|
257
|
-
locale: optional(
|
|
258
|
-
guarded("a locale, en or zh-TW", (value): value is Locale =>
|
|
259
|
-
isLocale(value),
|
|
260
|
-
),
|
|
261
|
-
),
|
|
262
|
-
timeZone: optional(text),
|
|
263
|
-
speakers: optional(
|
|
264
|
-
shape({ admins: optional(tierMembers), members: optional(tierMembers) }),
|
|
265
|
-
),
|
|
266
|
-
access: optional(accessShape),
|
|
267
|
-
background: optional(
|
|
268
|
-
shape({
|
|
269
|
-
perPrincipal: optional(
|
|
270
|
-
shape({
|
|
271
|
-
schedules: optional(integer(1, 10_000)),
|
|
272
|
-
delegations: optional(integer(1, 1_000)),
|
|
273
|
-
}),
|
|
274
|
-
),
|
|
275
|
-
}),
|
|
276
|
-
),
|
|
277
|
-
toolTiers: optional(record(oneOf<Tier>("owner", "admin", "member"))),
|
|
278
|
-
prompts: optional(shape({ shared: text, guest: optional(text) })),
|
|
279
|
-
agents: optional(
|
|
280
|
-
list(
|
|
281
|
-
shape({
|
|
282
|
-
name: text,
|
|
283
|
-
displayName: text,
|
|
284
|
-
prompt: text,
|
|
285
|
-
character: optional(characterInput),
|
|
286
|
-
avatarPrompt: optional(text),
|
|
287
|
-
channelId: optional(text),
|
|
288
|
-
}),
|
|
289
|
-
),
|
|
290
|
-
),
|
|
291
|
-
coordinator: optional(text),
|
|
292
|
-
http: optional(
|
|
293
|
-
shape({
|
|
294
|
-
publicUrl: optional(text),
|
|
295
|
-
port: optional(integer(1, 65535)),
|
|
296
|
-
hostname: optional(text),
|
|
297
|
-
socketPath: optional(text),
|
|
298
|
-
socketMode: optional(integer(0, 0o777)),
|
|
299
|
-
}),
|
|
300
|
-
),
|
|
301
|
-
avatar: optional(text),
|
|
302
|
-
workDir: optional(text),
|
|
303
|
-
scratchDir: optional(text),
|
|
304
|
-
skills: optional(skillsField),
|
|
305
|
-
memory: optional(bool),
|
|
306
|
-
ops: optional(shape({ agent: optional(text), conversation: optional(text) })),
|
|
307
|
-
drainSeconds: optional(integer(1, 3_600)),
|
|
308
|
-
...limitsShape,
|
|
309
|
-
interimText: optional(oneOf<InterimTextMode>("on", "off")),
|
|
310
|
-
interimPrimaryChars: optional(integer(1, 100_000)),
|
|
311
|
-
plugins: optional(
|
|
312
|
-
list(
|
|
313
|
-
guarded("a plugin, an object with a name and a setup function", isPlugin),
|
|
314
|
-
),
|
|
315
|
-
),
|
|
316
|
-
});
|
|
317
|
-
|
|
318
222
|
/** Logged once when `avatar` is set: agents are composed characters and no picture is drawn from it. */
|
|
319
223
|
export const AVATAR_DEPRECATION =
|
|
320
224
|
"agents are composed characters since 0.23; `avatar` is ignored";
|
|
@@ -340,6 +244,8 @@ export interface ResolvedConfig extends ResolvedLimits {
|
|
|
340
244
|
deprecations: string[];
|
|
341
245
|
/** Undefined on a host without Discord. */
|
|
342
246
|
discord?: ResolvedDiscord;
|
|
247
|
+
/** Whether the agent server runs: false when `agentServer: false`, and otherwise on a host with `discord` or `agents`. */
|
|
248
|
+
agentServer: boolean;
|
|
343
249
|
databaseUrl: string;
|
|
344
250
|
dataDir: string;
|
|
345
251
|
/** Absent `retention`: used files live as long as their conversation. */
|
|
@@ -361,9 +267,11 @@ export interface ResolvedConfig extends ResolvedLimits {
|
|
|
361
267
|
agents: AgentSeed[];
|
|
362
268
|
/** The coordinator's name; set only without `discord` and with `agents`: the configured one, else the first seed. */
|
|
363
269
|
coordinator?: string;
|
|
270
|
+
/** How the agents' characters are drawn: the six skin tones, the package's own unless `characters.skins` is set. */
|
|
271
|
+
characters: { skins: readonly Hex[]; refreshAvatars: boolean };
|
|
364
272
|
/** Undefined when no `http` is configured, and the host opens no `public` listener. */
|
|
365
273
|
http?: {
|
|
366
|
-
/** Always set with Discord. */
|
|
274
|
+
/** Always set with Discord, unless `agentServer` is false. */
|
|
367
275
|
publicUrl?: string;
|
|
368
276
|
port: number;
|
|
369
277
|
hostname?: string;
|
|
@@ -392,14 +300,28 @@ function modelOf(value: string, path: string): ModelRef {
|
|
|
392
300
|
return parsed;
|
|
393
301
|
}
|
|
394
302
|
|
|
303
|
+
/** The skin tones the configuration chose, or the package's own. */
|
|
304
|
+
function skinsOf(skins: readonly string[] | undefined): readonly Hex[] {
|
|
305
|
+
return (skins as readonly Hex[] | undefined) ?? CATALOG.skins;
|
|
306
|
+
}
|
|
307
|
+
|
|
395
308
|
/** The configuration checked against its schema, defaults filled; throws a ConfigError naming the key and the fix. */
|
|
396
309
|
export function resolveConfig(input: unknown): ResolvedConfig {
|
|
397
|
-
const config =
|
|
310
|
+
const config = configSchema.check(
|
|
311
|
+
liftAdapters(input),
|
|
312
|
+
"",
|
|
313
|
+
) as RoundtableConfig;
|
|
398
314
|
const name = config.name ?? "Roundtable";
|
|
399
315
|
const model = modelOf(config.model, "model");
|
|
400
316
|
const thinking = config.thinking ?? "medium";
|
|
401
317
|
const { discord, http } = config;
|
|
402
|
-
|
|
318
|
+
const agentServer = config.agentServer !== false;
|
|
319
|
+
if (!agentServer) checkAgentServerOff(config);
|
|
320
|
+
if (discord && agentServer && !discord.entryChannel)
|
|
321
|
+
throw new ConfigError(
|
|
322
|
+
"config discord.entryChannel: required, expected the id of the channel the coordinator lives in. Add it to roundtable.config.ts, or set agentServer: false for a host without agents.",
|
|
323
|
+
);
|
|
324
|
+
if (discord && agentServer && !http?.publicUrl)
|
|
403
325
|
throw new ConfigError(
|
|
404
326
|
"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.",
|
|
405
327
|
);
|
|
@@ -454,9 +376,16 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
454
376
|
toolTiers: config.toolTiers ?? {},
|
|
455
377
|
prompts: config.prompts ?? {},
|
|
456
378
|
agents: config.agents ?? [],
|
|
379
|
+
agentServer:
|
|
380
|
+
agentServer &&
|
|
381
|
+
(discord !== undefined || (config.agents?.length ?? 0) > 0),
|
|
457
382
|
...(!discord && config.agents?.[0]
|
|
458
383
|
? { coordinator: config.coordinator ?? config.agents[0].name }
|
|
459
384
|
: {}),
|
|
385
|
+
characters: {
|
|
386
|
+
skins: skinsOf(config.characters?.skins),
|
|
387
|
+
refreshAvatars: config.characters?.refreshAvatars ?? false,
|
|
388
|
+
},
|
|
460
389
|
...(http
|
|
461
390
|
? {
|
|
462
391
|
http: {
|
|
@@ -474,7 +403,9 @@ export function resolveConfig(input: unknown): ResolvedConfig {
|
|
|
474
403
|
workDir: config.workDir ?? `${config.dataDir}/work`,
|
|
475
404
|
scratchDir: config.scratchDir ?? join(tmpdir(), `${slug}-scratch`),
|
|
476
405
|
// Without Discord and without agents there is no one to carry skills, so the addon is off.
|
|
477
|
-
skills:
|
|
406
|
+
skills: agentServer
|
|
407
|
+
? (config.skills ?? (discord || config.agents?.length ? {} : false))
|
|
408
|
+
: false,
|
|
478
409
|
memory: config.memory ?? true,
|
|
479
410
|
...(ops ? { ops } : {}),
|
|
480
411
|
drainMs: (config.drainSeconds ?? DRAIN_LIMIT_MS / 1_000) * 1_000,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CharacterError,
|
|
3
3
|
type CharacterInput,
|
|
4
|
+
checkSkins,
|
|
5
|
+
type Hex,
|
|
4
6
|
resolveCharacter,
|
|
5
7
|
} from "../../characters/index.ts";
|
|
6
8
|
import { ConfigError } from "../domain/errors.ts";
|
|
@@ -155,6 +157,22 @@ export const characterInput: Field<CharacterInput> = {
|
|
|
155
157
|
},
|
|
156
158
|
};
|
|
157
159
|
|
|
160
|
+
/** The six skin tones of a host's agents, exactly six `#rrggbb` colors; a bad one is named by its place in the list. */
|
|
161
|
+
export const skinTones: Field<readonly Hex[]> = {
|
|
162
|
+
describe: "a list of exactly 6 #rrggbb colors, such as EAST_ASIAN_SKINS",
|
|
163
|
+
check: (value, path) => {
|
|
164
|
+
try {
|
|
165
|
+
return checkSkins(value, path);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof CharacterError)
|
|
168
|
+
throw new ConfigError(
|
|
169
|
+
`config ${error.message}. Fix the skins in roundtable.config.ts.`,
|
|
170
|
+
);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
158
176
|
/** `false`, or whatever `item` accepts: a feature that is switched off, or configured. */
|
|
159
177
|
export const orOff = <T>(item: Field<T>): Field<T | false> => ({
|
|
160
178
|
describe: `false, or ${item.describe}`,
|
|
@@ -212,7 +212,12 @@ function agentAssembly(
|
|
|
212
212
|
: [],
|
|
213
213
|
agentServer: agentServerPlugin({
|
|
214
214
|
...(discord
|
|
215
|
-
? {
|
|
215
|
+
? {
|
|
216
|
+
guildId: discord.guild,
|
|
217
|
+
...(discord.entryChannel
|
|
218
|
+
? { entryChannelId: discord.entryChannel }
|
|
219
|
+
: {}),
|
|
220
|
+
}
|
|
216
221
|
: config.coordinator
|
|
217
222
|
? { coordinator: config.coordinator }
|
|
218
223
|
: {}),
|
|
@@ -235,6 +240,7 @@ function agentAssembly(
|
|
|
235
240
|
// The pictures are public on the listener only where a public address reaches it.
|
|
236
241
|
...(config.http ? { avatarListener: "public" } : {}),
|
|
237
242
|
...(config.http?.publicUrl ? { avatarUrl: config.http.publicUrl } : {}),
|
|
243
|
+
...(config.characters.refreshAvatars ? { refreshAvatars: true } : {}),
|
|
238
244
|
avatarReference: join(ASSETS, "neutral.png"),
|
|
239
245
|
avatarNeutral: join(ASSETS, "neutral.png"),
|
|
240
246
|
// An ops agent's reports are the agent server's to deliver.
|
|
@@ -250,7 +256,8 @@ function agentAssembly(
|
|
|
250
256
|
* The host's options and the plugin list of a configured bot: the built-in plugins in their
|
|
251
257
|
* fixed order (each service one provides is read by the ones after it), then the operator's, then the scheduler, so a due schedule fires only once
|
|
252
258
|
* everything it reaches runs. The memory, Discord administration, and skills addons are left out
|
|
253
|
-
* when the configuration switches them off.
|
|
259
|
+
* when the configuration switches them off. With `agentServer: false` the agent server, its
|
|
260
|
+
* seeds and the skills addon are left out of a Discord host. Without `discord` the Discord plugins are left out,
|
|
254
261
|
* and the skills, the agent server and its seeds stay only when `agents` are configured: their
|
|
255
262
|
* team then lives in the rooms of the surfaces that host them, and the runtime still runs every
|
|
256
263
|
* turn of `context.turns`. Configuration mistakes stop here, naming the key and the fix.
|
|
@@ -294,11 +301,11 @@ export async function defineRoundtable(
|
|
|
294
301
|
// The agent server hands the runtime its per-agent settings once it sets up.
|
|
295
302
|
const agentSessions = agentSessionsSlot();
|
|
296
303
|
const assembly = discord ? discordAssembly(config, discord) : undefined;
|
|
297
|
-
// The agent server runs with Discord, and without it whenever agents are configured
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
304
|
+
// The agent server runs with Discord, and without it whenever agents are configured;
|
|
305
|
+
// `agentServer: false` leaves it out, with the skills addon and the seeds.
|
|
306
|
+
const agentTeam = config.agentServer
|
|
307
|
+
? agentAssembly(config, { modelRuntime, agentSessions, errorReporter })
|
|
308
|
+
: undefined;
|
|
302
309
|
return {
|
|
303
310
|
options: {
|
|
304
311
|
logger,
|
|
@@ -309,6 +316,7 @@ export async function defineRoundtable(
|
|
|
309
316
|
rootCommand: config.slug,
|
|
310
317
|
agentDir: config.agentDir,
|
|
311
318
|
scratchDir: config.scratchDir,
|
|
319
|
+
characterSkins: config.characters.skins,
|
|
312
320
|
},
|
|
313
321
|
database: { url: config.databaseUrl },
|
|
314
322
|
dataDir: config.dataDir,
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
|
+
import {
|
|
3
|
+
CATALOG,
|
|
4
|
+
CharacterError,
|
|
5
|
+
checkSkins,
|
|
6
|
+
type Hex,
|
|
7
|
+
} from "../characters/index.ts";
|
|
2
8
|
import { attachmentPort } from "./attachments/attachment-port.ts";
|
|
3
9
|
import { retentionServices } from "./attachments/attachment-retention.ts";
|
|
4
10
|
import type { AttachmentPort } from "./contract/attachments.ts";
|
|
5
11
|
import type { ConversationPort } from "./contract/channels.ts";
|
|
6
12
|
import type { ResolvedProviders } from "./contract/providers.ts";
|
|
7
13
|
import type { SurfacePort } from "./contract/surface.ts";
|
|
14
|
+
import { ConfigError } from "./domain/errors.ts";
|
|
8
15
|
import { NotLinkedError } from "./errors.ts";
|
|
9
16
|
import { EventBus } from "./events.ts";
|
|
10
17
|
import type { RoundtableOptions } from "./host.ts";
|
|
@@ -37,6 +44,20 @@ import { CONVERSATIONS, IDENTITY, RUNTIME } from "./services.ts";
|
|
|
37
44
|
import { setTimeZone } from "./time.ts";
|
|
38
45
|
import { type ToolTierTable, toolTiers } from "./tool-tiers.ts";
|
|
39
46
|
|
|
47
|
+
/** The skin tones the host's environment names, checked, or the package's own; a bad set refuses the start. */
|
|
48
|
+
function characterSkinsOf(skins: readonly Hex[] | undefined): readonly Hex[] {
|
|
49
|
+
if (skins === undefined) return CATALOG.skins;
|
|
50
|
+
try {
|
|
51
|
+
return checkSkins(skins, "environment.characterSkins");
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof CharacterError)
|
|
54
|
+
throw new ConfigError(
|
|
55
|
+
`${error.message}. Pass exactly six #rrggbb colors, such as EAST_ASIAN_SKINS from pi-roundtable/characters.`,
|
|
56
|
+
);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* What `Roundtable.check()` found: the plugins that would run, the services and HTTP routes they
|
|
42
63
|
* add, as the host would start them.
|
|
@@ -102,7 +123,10 @@ export class Linkage {
|
|
|
102
123
|
rootCommand = "roundtable",
|
|
103
124
|
agentDir,
|
|
104
125
|
scratchDir,
|
|
126
|
+
characterSkins,
|
|
105
127
|
} = this.#options.environment ?? {};
|
|
128
|
+
// Checked before anything is applied, so a bad set stops the start with the process untouched.
|
|
129
|
+
const skins = characterSkinsOf(characterSkins);
|
|
106
130
|
setLocale(locale, { assistant, root: rootCommand });
|
|
107
131
|
setTimeZone(timeZone);
|
|
108
132
|
// Pi packages such as pi-web-access read their config from the Pi agent directory.
|
|
@@ -112,6 +136,7 @@ export class Linkage {
|
|
|
112
136
|
timeZone,
|
|
113
137
|
now: () => new Date(),
|
|
114
138
|
...(scratchDir !== undefined ? { scratchDir } : {}),
|
|
139
|
+
characterSkins: skins,
|
|
115
140
|
};
|
|
116
141
|
}
|
|
117
142
|
|
package/src/core/host.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
|
+
import type { Hex } from "../characters/index.ts";
|
|
2
3
|
import type { AttachmentRetention } from "./attachments/attachment-retention.ts";
|
|
3
4
|
import { checkDatabase } from "./db/check-database.ts";
|
|
4
5
|
import { openPool, runMigrations } from "./db/migrations.ts";
|
|
@@ -33,6 +34,8 @@ export interface HostEnvironment {
|
|
|
33
34
|
agentDir?: string;
|
|
34
35
|
/** The host's scratch dir, which plugins read as `env.scratchDir`; unset gives them none. */
|
|
35
36
|
scratchDir?: string;
|
|
37
|
+
/** Six `#rrggbb` skin tones for the agents' characters, which plugins read as `env.characterSkins`; default the package's own. Anything but six valid colors stops the start. */
|
|
38
|
+
characterSkins?: readonly Hex[];
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export interface RoundtableOptions {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ChatSurface } from "./contract/surface.ts";
|
|
2
|
+
import { PluginError } from "./errors.ts";
|
|
3
|
+
import type {
|
|
4
|
+
Contribution,
|
|
5
|
+
EventHandlers,
|
|
6
|
+
RoundtablePlugin,
|
|
7
|
+
} from "./plugin.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The plugin fields of 0.1.0 that are gone, each with what replaces it. The host refuses a
|
|
11
|
+
* plugin that still has one, so a plugin written for 0.1.0 fails at once instead of being ignored.
|
|
12
|
+
*/
|
|
13
|
+
const REMOVED_PLUGIN_FIELDS = {
|
|
14
|
+
useCommands:
|
|
15
|
+
"the composed slash commands go to the Discord plugin's surface, not to a plugin; add the commands from setup with context.services.get(DISCORD).commands.add(...), with DISCORD from pi-roundtable/discord",
|
|
16
|
+
agentServer:
|
|
17
|
+
"give the plugin a service with startInBackground, and hear how it ended in a serviceStarted event handler",
|
|
18
|
+
stopTurn:
|
|
19
|
+
"put stop(channel) on the channel claim that owns the channel; the router asks only the owning claim",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/** The contribution parts of 0.1.0 that are gone, each with what replaces it. */
|
|
23
|
+
const REMOVED_PARTS = {
|
|
24
|
+
interactions:
|
|
25
|
+
"slash commands belong to the Discord plugin now; add them from setup with context.services.get(DISCORD).commands.add({ module, rootOptions }), with DISCORD from pi-roundtable/discord",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
/** The chat surface methods of 0.1.0 that are gone, each with what replaces it. */
|
|
29
|
+
const REMOVED_SURFACE_METHODS = {
|
|
30
|
+
useCommands:
|
|
31
|
+
"the host no longer composes slash commands or hands them to a surface; the Discord plugin composes them, and a plugin adds its own with context.services.get(DISCORD).commands.add(...) from pi-roundtable/discord",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const REMOVED_EVENT = {
|
|
35
|
+
agentServer:
|
|
36
|
+
"hear serviceStarted instead, which names the plugin and service whose background start ended",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Refuses a plugin that still has a field of 0.1.0 that is gone, naming what replaces it, so it
|
|
41
|
+
* fails where it is written or when the host links it instead of being ignored.
|
|
42
|
+
*/
|
|
43
|
+
export function refuseRemovedFields(plugin: RoundtablePlugin): void {
|
|
44
|
+
for (const [field, replacement] of Object.entries(REMOVED_PLUGIN_FIELDS))
|
|
45
|
+
if (field in plugin)
|
|
46
|
+
throw new PluginError(
|
|
47
|
+
`plugin ${plugin.name}: "${field}" was removed in 0.2.0; ${replacement}.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Refuses a contribution part of 0.1.0 that is gone, naming what replaces it. */
|
|
52
|
+
export function refuseRemovedParts(
|
|
53
|
+
plugin: string,
|
|
54
|
+
contribution: Contribution,
|
|
55
|
+
): void {
|
|
56
|
+
for (const [part, replacement] of Object.entries(REMOVED_PARTS))
|
|
57
|
+
if (part in contribution)
|
|
58
|
+
throw new PluginError(
|
|
59
|
+
`plugin ${plugin}: the "${part}" part was removed in 0.2.0; ${replacement}.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Refuses a chat surface that still has a method of 0.1.0 that is gone, naming what replaces it. */
|
|
64
|
+
export function refuseRemovedSurfaceMethods(
|
|
65
|
+
plugin: string,
|
|
66
|
+
surface: ChatSurface,
|
|
67
|
+
): void {
|
|
68
|
+
for (const [method, replacement] of Object.entries(REMOVED_SURFACE_METHODS))
|
|
69
|
+
if (method in surface)
|
|
70
|
+
throw new PluginError(
|
|
71
|
+
`plugin ${plugin}: surface ${surface.surface} has ${method}, which was removed in 0.2.0; ${replacement}.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Refuses an event handler of 0.1.0 that is gone, naming what replaces it. */
|
|
76
|
+
export function refuseRemovedEvents(
|
|
77
|
+
plugin: string,
|
|
78
|
+
events: EventHandlers,
|
|
79
|
+
): void {
|
|
80
|
+
for (const [event, replacement] of Object.entries(REMOVED_EVENT))
|
|
81
|
+
if (event in events)
|
|
82
|
+
throw new PluginError(
|
|
83
|
+
`plugin ${plugin}: the "${event}" event was removed in 0.2.0; ${replacement}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/core/plugin.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
|
+
import type { Hex } from "../characters/index.ts";
|
|
2
3
|
import type { AgentSeed } from "./agents/agent-store.ts";
|
|
3
4
|
import type { AttachmentPort } from "./contract/attachments.ts";
|
|
4
5
|
import type {
|
|
@@ -276,6 +277,13 @@ export interface HostEnv {
|
|
|
276
277
|
* without a hold. Plugins that run shells of their own (coding workers) give it to them too.
|
|
277
278
|
*/
|
|
278
279
|
readonly scratchDir?: string;
|
|
280
|
+
/**
|
|
281
|
+
* The six skin tones the host draws its agents' characters with, which `CharacterSpec.skin`
|
|
282
|
+
* indexes: the package's own unless the host configured `characters.skins`. A plugin that
|
|
283
|
+
* draws a character passes them as `composeSvg`'s `skins` option, so every picture of an
|
|
284
|
+
* agent wears the same ones.
|
|
285
|
+
*/
|
|
286
|
+
readonly characterSkins: readonly Hex[];
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
/** What the host gives every plugin. */
|
|
@@ -392,36 +400,6 @@ export interface RoundtablePlugin {
|
|
|
392
400
|
preflight?(): Promise<void> | void;
|
|
393
401
|
}
|
|
394
402
|
|
|
395
|
-
/**
|
|
396
|
-
* The plugin fields of 0.1.0 that are gone, each with what replaces it. The host refuses a
|
|
397
|
-
* plugin that still has one, so a plugin written for 0.1.0 fails at once instead of being ignored.
|
|
398
|
-
*/
|
|
399
|
-
const REMOVED_PLUGIN_FIELDS = {
|
|
400
|
-
useCommands:
|
|
401
|
-
"the composed slash commands go to the Discord plugin's surface, not to a plugin; add the commands from setup with context.services.get(DISCORD).commands.add(...), with DISCORD from pi-roundtable/discord",
|
|
402
|
-
agentServer:
|
|
403
|
-
"give the plugin a service with startInBackground, and hear how it ended in a serviceStarted event handler",
|
|
404
|
-
stopTurn:
|
|
405
|
-
"put stop(channel) on the channel claim that owns the channel; the router asks only the owning claim",
|
|
406
|
-
} as const;
|
|
407
|
-
|
|
408
|
-
/** The contribution parts of 0.1.0 that are gone, each with what replaces it. */
|
|
409
|
-
const REMOVED_PARTS = {
|
|
410
|
-
interactions:
|
|
411
|
-
"slash commands belong to the Discord plugin now; add them from setup with context.services.get(DISCORD).commands.add({ module, rootOptions }), with DISCORD from pi-roundtable/discord",
|
|
412
|
-
} as const;
|
|
413
|
-
|
|
414
|
-
/** The chat surface methods of 0.1.0 that are gone, each with what replaces it. */
|
|
415
|
-
const REMOVED_SURFACE_METHODS = {
|
|
416
|
-
useCommands:
|
|
417
|
-
"the host no longer composes slash commands or hands them to a surface; the Discord plugin composes them, and a plugin adds its own with context.services.get(DISCORD).commands.add(...) from pi-roundtable/discord",
|
|
418
|
-
} as const;
|
|
419
|
-
|
|
420
|
-
const REMOVED_EVENT = {
|
|
421
|
-
agentServer:
|
|
422
|
-
"hear serviceStarted instead, which names the plugin and service whose background start ended",
|
|
423
|
-
} as const;
|
|
424
|
-
|
|
425
403
|
/**
|
|
426
404
|
* The context one plugin's setup gets: the host's parts and that plugin's view of the services.
|
|
427
405
|
* `context.core` of 0.1.0 is gone; reading it throws a PluginError naming `context.services`, where
|
|
@@ -449,50 +427,9 @@ export function pluginContext(
|
|
|
449
427
|
return context;
|
|
450
428
|
}
|
|
451
429
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (field in plugin)
|
|
459
|
-
throw new PluginError(
|
|
460
|
-
`plugin ${plugin.name}: "${field}" was removed in 0.2.0; ${replacement}.`,
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** Refuses a contribution part of 0.1.0 that is gone, naming what replaces it. */
|
|
465
|
-
export function refuseRemovedParts(
|
|
466
|
-
plugin: string,
|
|
467
|
-
contribution: Contribution,
|
|
468
|
-
): void {
|
|
469
|
-
for (const [part, replacement] of Object.entries(REMOVED_PARTS))
|
|
470
|
-
if (part in contribution)
|
|
471
|
-
throw new PluginError(
|
|
472
|
-
`plugin ${plugin}: the "${part}" part was removed in 0.2.0; ${replacement}.`,
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** Refuses a chat surface that still has a method of 0.1.0 that is gone, naming what replaces it. */
|
|
477
|
-
export function refuseRemovedSurfaceMethods(
|
|
478
|
-
plugin: string,
|
|
479
|
-
surface: ChatSurface,
|
|
480
|
-
): void {
|
|
481
|
-
for (const [method, replacement] of Object.entries(REMOVED_SURFACE_METHODS))
|
|
482
|
-
if (method in surface)
|
|
483
|
-
throw new PluginError(
|
|
484
|
-
`plugin ${plugin}: surface ${surface.surface} has ${method}, which was removed in 0.2.0; ${replacement}.`,
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Refuses an event handler of 0.1.0 that is gone, naming what replaces it. */
|
|
489
|
-
export function refuseRemovedEvents(
|
|
490
|
-
plugin: string,
|
|
491
|
-
events: EventHandlers,
|
|
492
|
-
): void {
|
|
493
|
-
for (const [event, replacement] of Object.entries(REMOVED_EVENT))
|
|
494
|
-
if (event in events)
|
|
495
|
-
throw new PluginError(
|
|
496
|
-
`plugin ${plugin}: the "${event}" event was removed in 0.2.0; ${replacement}.`,
|
|
497
|
-
);
|
|
498
|
-
}
|
|
430
|
+
export {
|
|
431
|
+
refuseRemovedEvents,
|
|
432
|
+
refuseRemovedFields,
|
|
433
|
+
refuseRemovedParts,
|
|
434
|
+
refuseRemovedSurfaceMethods,
|
|
435
|
+
} from "./plugin-removed.ts";
|