pi-roundtable 0.22.5 → 0.23.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 +55 -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 +350 -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
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import {
|
|
4
|
+
CATALOG,
|
|
5
|
+
type CharacterInput,
|
|
6
|
+
CLASS_IDS,
|
|
7
|
+
type PaletteName,
|
|
8
|
+
} from "../../characters/index.ts";
|
|
3
9
|
import { AgentError } from "../domain/errors.ts";
|
|
4
10
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
5
11
|
import { assistantName, messages } from "../i18n/index.ts";
|
|
@@ -14,16 +20,17 @@ import {
|
|
|
14
20
|
} from "../runtime/text-tools.ts";
|
|
15
21
|
import { timeZone } from "../time.ts";
|
|
16
22
|
import {
|
|
17
|
-
MAX_AVATAR_PROMPT_CHARS,
|
|
18
23
|
MAX_GROUP_MEMBERS,
|
|
19
24
|
MAX_PROMPT_CHARS,
|
|
20
25
|
MIN_GROUP_MEMBERS,
|
|
21
26
|
} from "./agent-store.ts";
|
|
22
27
|
|
|
28
|
+
const PALETTE_NAMES = Object.keys(CATALOG.palettes) as PaletteName[];
|
|
29
|
+
|
|
23
30
|
/** The tool that hands a request to another agent; a group seat has none. */
|
|
24
31
|
export const MESSAGE_AGENT_TOOL = "message_agent";
|
|
25
32
|
|
|
26
|
-
/** The tool that
|
|
33
|
+
/** The tool that changes an agent's look; every host offers it. */
|
|
27
34
|
export const AVATAR_TOOL = "agent_avatar";
|
|
28
35
|
|
|
29
36
|
export const AGENT_TOOLS = [
|
|
@@ -40,6 +47,10 @@ export const AGENT_TOOLS = [
|
|
|
40
47
|
"channel_read",
|
|
41
48
|
] as const;
|
|
42
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @deprecated Agents are composed characters and no longer drawn; use `AgentTeam.dress`.
|
|
52
|
+
* Removed in 1.0.
|
|
53
|
+
*/
|
|
43
54
|
export type AvatarMode = "redraw" | "new_prompt" | "edit";
|
|
44
55
|
|
|
45
56
|
/** What the agent tools do; the agent team implements it. Each returns text for the model. */
|
|
@@ -61,14 +72,15 @@ export interface AgentOps {
|
|
|
61
72
|
name: string;
|
|
62
73
|
displayName: string;
|
|
63
74
|
prompt: string;
|
|
64
|
-
/**
|
|
65
|
-
|
|
75
|
+
/** Fields of its look; any left out come from the look its role suggests. */
|
|
76
|
+
character?: CharacterInput;
|
|
66
77
|
task: string;
|
|
67
78
|
category?: string;
|
|
68
79
|
skills?: string[];
|
|
69
80
|
},
|
|
70
81
|
): Promise<string>;
|
|
71
|
-
|
|
82
|
+
/** Changes an agent's look; a field left out stays as it is. */
|
|
83
|
+
avatar(name: string, look: CharacterInput): Promise<string>;
|
|
72
84
|
/** Queues the message and returns at once. */
|
|
73
85
|
message(caller: AgentTurnScope, to: string, text: string): string;
|
|
74
86
|
createGroup(input: {
|
|
@@ -106,12 +118,35 @@ const PROMPT = Type.String({
|
|
|
106
118
|
description:
|
|
107
119
|
"The agent's role prompt, in English: its job, sources, boundaries, and how it reports. Short and stated positively.",
|
|
108
120
|
});
|
|
109
|
-
const
|
|
110
|
-
Type.String({
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
const enumOf = (ids: readonly string[], description?: string) =>
|
|
122
|
+
Type.String({ enum: [...ids], ...(description ? { description } : {}) });
|
|
123
|
+
const CLASS = enumOf(
|
|
124
|
+
CLASS_IDS,
|
|
125
|
+
`The agent's character class, which sets its outfit, hat, and prop. ${CLASS_IDS.map((id) => `${id}: ${CATALOG.classes[id].jobs}`).join("; ")}.`,
|
|
126
|
+
);
|
|
127
|
+
const COLOR = enumOf(
|
|
128
|
+
PALETTE_NAMES,
|
|
129
|
+
"The agent's color palette. Pick one unlike the other agents' (agent_get shows each look).",
|
|
130
|
+
);
|
|
131
|
+
const PARTS = Type.Object(
|
|
132
|
+
{
|
|
133
|
+
outfit: Type.Optional(enumOf(CATALOG.outfits)),
|
|
134
|
+
hat: Type.Optional(enumOf(CATALOG.hats)),
|
|
135
|
+
prop: Type.Optional(enumOf(CATALOG.props)),
|
|
136
|
+
hair: Type.Optional(enumOf(CATALOG.hair)),
|
|
137
|
+
beard: Type.Optional(enumOf([...CATALOG.beards, "none"])),
|
|
138
|
+
accessory: Type.Optional(enumOf([...CATALOG.accessories, "none"])),
|
|
139
|
+
build: Type.Optional(enumOf(["slim", "regular", "stout"])),
|
|
140
|
+
skin: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
|
|
141
|
+
hair_color: Type.Optional(
|
|
142
|
+
Type.String({
|
|
143
|
+
pattern: "^#[0-9a-fA-F]{6}$",
|
|
144
|
+
description: "A hex color such as #3b2a1a.",
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{ description: "Single parts to change; any left out stay as they are." },
|
|
149
|
+
);
|
|
115
150
|
const category = (kind: "Agents" | "Groups") =>
|
|
116
151
|
Type.String({
|
|
117
152
|
description: `The category its channel opens under: ${kind} (the default) or ${kind}-<suffix>, such as ${kind}-work; made when missing. Keep to the categories agent_list shows unless a new one fits better.`,
|
|
@@ -122,12 +157,38 @@ const MEMBERS = Type.Array(Type.String(), {
|
|
|
122
157
|
description: "Agent names in speaking-tie order.",
|
|
123
158
|
});
|
|
124
159
|
|
|
160
|
+
const PART_FIELDS = {
|
|
161
|
+
outfit: "outfit",
|
|
162
|
+
hat: "hat",
|
|
163
|
+
prop: "prop",
|
|
164
|
+
hair: "hair",
|
|
165
|
+
beard: "beard",
|
|
166
|
+
accessory: "accessory",
|
|
167
|
+
build: "build",
|
|
168
|
+
skin: "skin",
|
|
169
|
+
hair_color: "hairColor",
|
|
170
|
+
} as const;
|
|
171
|
+
|
|
172
|
+
/** The look a tool call names: class, color as palette, and parts; nothing it leaves out. */
|
|
173
|
+
function lookOf(input: Record<string, unknown>): CharacterInput {
|
|
174
|
+
const look: Record<string, unknown> = {};
|
|
175
|
+
const klass = optionalString(input, "class");
|
|
176
|
+
const color = optionalString(input, "color");
|
|
177
|
+
if (klass) look.class = klass;
|
|
178
|
+
if (color) look.palette = color;
|
|
179
|
+
const parts = input.parts;
|
|
180
|
+
if (parts !== null && typeof parts === "object")
|
|
181
|
+
for (const [key, field] of Object.entries(PART_FIELDS)) {
|
|
182
|
+
const value = (parts as Record<string, unknown>)[key];
|
|
183
|
+
if (value !== undefined) look[field] = value;
|
|
184
|
+
}
|
|
185
|
+
return look as CharacterInput;
|
|
186
|
+
}
|
|
187
|
+
|
|
125
188
|
type AgentToolDef = TextToolDef<(typeof AGENT_TOOLS)[number]>;
|
|
126
189
|
|
|
127
190
|
/** What the agent tools offer, by what the host can do. */
|
|
128
191
|
export interface AgentToolOptions {
|
|
129
|
-
/** Whether an image provider is configured; without one there is no avatar prompt and no agent_avatar. */
|
|
130
|
-
avatars: boolean;
|
|
131
192
|
/** Whether the skills addon is on; without it agent_create takes no skills. */
|
|
132
193
|
skills: boolean;
|
|
133
194
|
}
|
|
@@ -136,7 +197,7 @@ function toolDefs(
|
|
|
136
197
|
ops: AgentOps,
|
|
137
198
|
scope: AgentTurnScope,
|
|
138
199
|
owner: OwnerIdentity,
|
|
139
|
-
{
|
|
200
|
+
{ skills: skillsOn }: AgentToolOptions,
|
|
140
201
|
): AgentToolDef[] {
|
|
141
202
|
const o = ownerWords(owner);
|
|
142
203
|
const defs: AgentToolDef[] = [
|
|
@@ -152,7 +213,7 @@ function toolDefs(
|
|
|
152
213
|
name: "agent_get",
|
|
153
214
|
label: "Read agent",
|
|
154
215
|
description:
|
|
155
|
-
"Read an agent's display name, status, channel, model, thinking level, skills,
|
|
216
|
+
"Read an agent's display name, status, channel, model, thinking level, skills, look, and prompt.",
|
|
156
217
|
parameters: Type.Object({ name: NAME }),
|
|
157
218
|
run: (i) => ops.get(requiredString(i, "name")),
|
|
158
219
|
},
|
|
@@ -195,7 +256,8 @@ function toolDefs(
|
|
|
195
256
|
{
|
|
196
257
|
name: "agent_create",
|
|
197
258
|
label: "Create agent",
|
|
198
|
-
description:
|
|
259
|
+
description:
|
|
260
|
+
"Create a new agent with its own channel and a character picture; its look follows its role unless class and color are given. It starts at once on the opening task in its channel. Its name is permanent.",
|
|
199
261
|
parameters: Type.Object({
|
|
200
262
|
name: Type.String({
|
|
201
263
|
pattern: "^[a-z0-9-]{1,32}$",
|
|
@@ -204,7 +266,8 @@ function toolDefs(
|
|
|
204
266
|
}),
|
|
205
267
|
display_name: DISPLAY,
|
|
206
268
|
prompt: PROMPT,
|
|
207
|
-
|
|
269
|
+
class: Type.Optional(CLASS),
|
|
270
|
+
color: Type.Optional(COLOR),
|
|
208
271
|
task: Type.String({
|
|
209
272
|
minLength: 1,
|
|
210
273
|
maxLength: 4000,
|
|
@@ -225,13 +288,12 @@ function toolDefs(
|
|
|
225
288
|
run: (i) => {
|
|
226
289
|
const category = optionalString(i, "category");
|
|
227
290
|
const skills = stringList(i, "skills");
|
|
291
|
+
const look = lookOf(i);
|
|
228
292
|
return ops.create(scope, {
|
|
229
293
|
name: requiredString(i, "name"),
|
|
230
294
|
displayName: requiredString(i, "display_name"),
|
|
231
295
|
prompt: requiredString(i, "prompt"),
|
|
232
|
-
...(
|
|
233
|
-
? { avatarPrompt: requiredString(i, "avatar_prompt") }
|
|
234
|
-
: {}),
|
|
296
|
+
...(Object.keys(look).length > 0 ? { character: look } : {}),
|
|
235
297
|
task: requiredString(i, "task"),
|
|
236
298
|
...(category ? { category } : {}),
|
|
237
299
|
...(skills && skills.length > 0 ? { skills } : {}),
|
|
@@ -240,30 +302,21 @@ function toolDefs(
|
|
|
240
302
|
},
|
|
241
303
|
{
|
|
242
304
|
name: AVATAR_TOOL,
|
|
243
|
-
label: "
|
|
305
|
+
label: "Change look",
|
|
244
306
|
description:
|
|
245
|
-
"
|
|
307
|
+
"Change an agent's look: its class, color, or single parts. What you leave out stays as it is; a new class keeps the agent's colors unless you give a color. The new picture shows from its next message.",
|
|
246
308
|
parameters: Type.Object({
|
|
247
309
|
name: NAME,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Type.Literal("edit"),
|
|
252
|
-
]),
|
|
253
|
-
text: Type.Optional(
|
|
254
|
-
Type.String({
|
|
255
|
-
maxLength: MAX_AVATAR_PROMPT_CHARS,
|
|
256
|
-
description:
|
|
257
|
-
"The new avatar prompt for new_prompt, or the instruction for edit.",
|
|
258
|
-
}),
|
|
259
|
-
),
|
|
310
|
+
class: Type.Optional(CLASS),
|
|
311
|
+
color: Type.Optional(COLOR),
|
|
312
|
+
parts: Type.Optional(PARTS),
|
|
260
313
|
}),
|
|
261
|
-
run: (i) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
run: (i) => {
|
|
315
|
+
const look = lookOf(i);
|
|
316
|
+
if (Object.keys(look).length === 0)
|
|
317
|
+
throw new AgentError("Give a class, a color, or parts to change.");
|
|
318
|
+
return ops.avatar(requiredString(i, "name"), look);
|
|
319
|
+
},
|
|
267
320
|
},
|
|
268
321
|
{
|
|
269
322
|
name: "group_create",
|
|
@@ -391,7 +444,7 @@ function toolDefs(
|
|
|
391
444
|
run: (i) =>
|
|
392
445
|
ops.message(scope, requiredString(i, "to"), requiredString(i, "text")),
|
|
393
446
|
});
|
|
394
|
-
return
|
|
447
|
+
return defs;
|
|
395
448
|
}
|
|
396
449
|
|
|
397
450
|
/** Registers the agent tools for one agent session. */
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import type { CharacterSpec } from "../../characters/index.ts";
|
|
5
|
+
import { resolveCharacter } from "../../characters/index.ts";
|
|
4
6
|
import { squarePng } from "../attachments/square-png.ts";
|
|
5
7
|
import type { ImageDrawer } from "../contract/providers.ts";
|
|
8
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
6
9
|
import { ProviderError } from "../errors.ts";
|
|
7
10
|
import type { HttpRoute } from "../http/listeners.ts";
|
|
8
11
|
import type { AvatarStudio } from "../services.ts";
|
|
12
|
+
import { characterPng } from "./character-png.ts";
|
|
9
13
|
import { fallbackAvatar } from "./fallback-avatar.ts";
|
|
10
14
|
|
|
11
15
|
const AVATAR_SIDE = 512;
|
|
@@ -19,14 +23,20 @@ export type { ImageDrawer };
|
|
|
19
23
|
export interface AvatarStudioOptions {
|
|
20
24
|
/** Where pictures are kept, one `<sha256>.png` each. */
|
|
21
25
|
dir: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Origin the pictures are served from, such as https://assistant.example.com. Without one a
|
|
28
|
+
* picture's URL is the path `/avatars/<hash>.png`, which a surface that serves the pictures
|
|
29
|
+
* itself, through `serve`, resolves against its own origin.
|
|
30
|
+
*/
|
|
31
|
+
publicUrl?: string;
|
|
24
32
|
/** The assistant's neutral avatar: the style reference and the picture of an agent without one. */
|
|
25
33
|
referencePath: string;
|
|
26
34
|
/** The neutral avatar shipped with the package; the reference is replaced by it when the reference cannot be read as a picture. */
|
|
27
35
|
neutralPath?: string;
|
|
28
36
|
/** Told the error code of a reference that was replaced by `neutralPath`. */
|
|
29
37
|
onReferenceFailed?: (code: string) => void;
|
|
38
|
+
/** Rasterizes a character to a PNG; canvas with librsvg by default. A seam for tests. */
|
|
39
|
+
rasterize?: typeof characterPng;
|
|
30
40
|
/** Draws the pictures. Without one, the studio only makes each agent a picture from its name. */
|
|
31
41
|
draw?: ImageDrawer;
|
|
32
42
|
}
|
|
@@ -40,15 +50,30 @@ export class FileAvatarStudio implements AvatarStudio {
|
|
|
40
50
|
readonly #options: AvatarStudioOptions;
|
|
41
51
|
#defaultHash: string | undefined;
|
|
42
52
|
#referencePath: string;
|
|
53
|
+
readonly #rasterize: typeof characterPng;
|
|
43
54
|
|
|
44
55
|
constructor(options: AvatarStudioOptions) {
|
|
45
56
|
this.#options = options;
|
|
57
|
+
this.#rasterize = options.rasterize ?? characterPng;
|
|
46
58
|
this.#referencePath = options.referencePath;
|
|
47
59
|
mkdirSync(options.dir, { recursive: true });
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Stores the assistant's neutral avatar, the picture of an agent that has none yet. First
|
|
64
|
+
* rasterizes a probe character: the agents' pictures need canvas with SVG support (librsvg),
|
|
65
|
+
* and a host without it refuses to start instead of failing on the first agent.
|
|
66
|
+
*/
|
|
51
67
|
async init(): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
await this.#rasterize(resolveCharacter({ class: "wizard" }), {
|
|
70
|
+
size: 32,
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new ConfigError(
|
|
74
|
+
`the agent server needs canvas with SVG support (librsvg) to draw the agents' pictures: ${error instanceof Error ? error.message : String(error)}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
52
77
|
try {
|
|
53
78
|
this.#defaultHash = await this.#referenceStore();
|
|
54
79
|
} catch (error) {
|
|
@@ -81,10 +106,17 @@ export class FileAvatarStudio implements AvatarStudio {
|
|
|
81
106
|
url(hash: string | undefined): string {
|
|
82
107
|
const chosen = hash ?? this.#defaultHash;
|
|
83
108
|
if (!chosen) throw new Error("the avatar studio was not initialized");
|
|
84
|
-
return `${this.#options.publicUrl}/avatars/${chosen}.png`;
|
|
109
|
+
return `${this.#options.publicUrl ?? ""}/avatars/${chosen}.png`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Renders a character as the agent's picture and stores it by content hash; the same spec gives the same hash. */
|
|
113
|
+
async render(spec: CharacterSpec): Promise<string> {
|
|
114
|
+
return this.#keep(await this.#rasterize(spec, { size: AVATAR_SIDE }));
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
/**
|
|
118
|
+
* @deprecated Agents are composed characters and no longer drawn; removed in 1.0.
|
|
119
|
+
*
|
|
88
120
|
* Draws a new picture from an avatar prompt: the assistant's face as an anime character in the
|
|
89
121
|
* role's pose; returns its hash. Outfit and background colours come from the avatar prompt,
|
|
90
122
|
* so agents stay easy to tell apart.
|
|
@@ -180,6 +212,10 @@ export class FileAvatarStudio implements AvatarStudio {
|
|
|
180
212
|
size: AVATAR_SIDE,
|
|
181
213
|
...(maxSide === undefined ? {} : { maxSide }),
|
|
182
214
|
});
|
|
215
|
+
return this.#keep(png);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#keep(png: Uint8Array): string {
|
|
183
219
|
const hash = createHash("sha256").update(png).digest("hex");
|
|
184
220
|
const path = this.#path(hash);
|
|
185
221
|
if (!existsSync(path)) writeFileSync(path, png);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createCanvas, loadImage } from "canvas";
|
|
2
|
+
import {
|
|
3
|
+
type CharacterSpec,
|
|
4
|
+
type ComposeOptions,
|
|
5
|
+
composeSvg,
|
|
6
|
+
STATE_EXPRESSION,
|
|
7
|
+
} from "../../characters/index.ts";
|
|
8
|
+
|
|
9
|
+
const PNG_SIDE = 512;
|
|
10
|
+
|
|
11
|
+
/** What a Discord avatar shows: the idle face on the modern light disc. */
|
|
12
|
+
const AVATAR_OPTIONS: ComposeOptions = {
|
|
13
|
+
look: "modern-light",
|
|
14
|
+
frame: "disc",
|
|
15
|
+
expr: STATE_EXPRESSION.idle,
|
|
16
|
+
state: "idle",
|
|
17
|
+
still: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A character as a PNG: the composer's SVG, rasterized by canvas (librsvg). The SVG is composer
|
|
22
|
+
* output only, never text from a user or an agent. Each picture on a page needs its own `uid`,
|
|
23
|
+
* so this passes one explicitly.
|
|
24
|
+
*/
|
|
25
|
+
export async function characterPng(
|
|
26
|
+
spec: CharacterSpec,
|
|
27
|
+
options: ComposeOptions = {},
|
|
28
|
+
): Promise<Uint8Array> {
|
|
29
|
+
const size = options.size ?? PNG_SIDE;
|
|
30
|
+
const svg = composeSvg(spec, {
|
|
31
|
+
...AVATAR_OPTIONS,
|
|
32
|
+
uid: "png",
|
|
33
|
+
...options,
|
|
34
|
+
size,
|
|
35
|
+
});
|
|
36
|
+
const image = await loadImage(Buffer.from(svg));
|
|
37
|
+
const canvas = createCanvas(size, size);
|
|
38
|
+
canvas.getContext("2d").drawImage(image, 0, 0, size, size);
|
|
39
|
+
return canvas.toBuffer("image/png");
|
|
40
|
+
}
|
|
@@ -75,15 +75,18 @@ export class GroupMessages {
|
|
|
75
75
|
DO UPDATE SET last_id = GREATEST(agent_group_cursors.last_id, EXCLUDED.last_id)`;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Marks every message so far as received by every member, for a group started over; `log` is
|
|
80
|
+
* the conversation's log, the group's own name unless it is a room's.
|
|
81
|
+
*/
|
|
82
|
+
async catchUp(group: AgentGroup, log: string = group.name): Promise<void> {
|
|
80
83
|
const rows: { id: string | number | null }[] = await this.#sql`
|
|
81
84
|
SELECT max(id) AS id FROM agent_group_messages
|
|
82
|
-
WHERE guild_id = ${this.#guild} AND group_name = ${
|
|
85
|
+
WHERE guild_id = ${this.#guild} AND group_name = ${log}`;
|
|
83
86
|
const last = rows[0]?.id;
|
|
84
87
|
if (last === null || last === undefined) return;
|
|
85
88
|
for (const member of group.members)
|
|
86
|
-
await this.advanceCursor(
|
|
89
|
+
await this.advanceCursor(log, member, Number(last));
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|
|
@@ -4,14 +4,19 @@ import { AgentError } from "../domain/errors.ts";
|
|
|
4
4
|
import { messages } from "../i18n/index.ts";
|
|
5
5
|
import type { Speaker } from "../speakers.ts";
|
|
6
6
|
import { groupTurnText } from "./agent-prompt.ts";
|
|
7
|
-
import type { Agent } from "./agent-store.ts";
|
|
7
|
+
import type { Agent, AgentGroup } from "./agent-store.ts";
|
|
8
8
|
import {
|
|
9
9
|
MAX_ROUND_REPLIES,
|
|
10
10
|
mentionedMembers,
|
|
11
11
|
planRound,
|
|
12
12
|
RECENT_FOR_SCORING,
|
|
13
13
|
} from "./group-round.ts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
groupLogName,
|
|
16
|
+
groupOfChannel,
|
|
17
|
+
groupScope,
|
|
18
|
+
groupSessionKey,
|
|
19
|
+
} from "./team-keys.ts";
|
|
15
20
|
import {
|
|
16
21
|
BACKLOG_LIMIT,
|
|
17
22
|
type Chain,
|
|
@@ -27,6 +32,15 @@ export class GroupTurns {
|
|
|
27
32
|
this.#host = host;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
/** The active group whose Discord channel or room the key is. */
|
|
36
|
+
#groupOf(channel: ChannelKey): AgentGroup {
|
|
37
|
+
const { store, surfaces } = this.#host.options;
|
|
38
|
+
const group = groupOfChannel(store, surfaces, channel);
|
|
39
|
+
if (group?.status !== "active")
|
|
40
|
+
throw new AgentError(`no group owns ${channel}`);
|
|
41
|
+
return group;
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
/**
|
|
31
45
|
* One owner message in a group channel: scored, then answered by the chosen members one at
|
|
32
46
|
* a time, each seeing every group message since its last turn. Call inside the queue.
|
|
@@ -41,16 +55,15 @@ export class GroupTurns {
|
|
|
41
55
|
): Promise<void> {
|
|
42
56
|
const { store, scorer, confirmations, logger } = this.#host.options;
|
|
43
57
|
const runtime = this.#host.options.runtime();
|
|
44
|
-
const group =
|
|
45
|
-
|
|
58
|
+
const group = this.#groupOf(channel);
|
|
59
|
+
// Each conversation of a group keeps its own log: the Discord channel's under the group's
|
|
60
|
+
// name, a room's under `<group>@<surface>`.
|
|
61
|
+
const log = groupLogName(group, channel);
|
|
46
62
|
const members = group.members
|
|
47
63
|
.map((name) => store.agent(name))
|
|
48
64
|
.filter((m): m is Agent => m?.status === "active");
|
|
49
|
-
const recent = await store.recentGroupMessages(
|
|
50
|
-
|
|
51
|
-
RECENT_FOR_SCORING,
|
|
52
|
-
);
|
|
53
|
-
await store.appendGroupMessage(group.name, {
|
|
65
|
+
const recent = await store.recentGroupMessages(log, RECENT_FOR_SCORING);
|
|
66
|
+
await store.appendGroupMessage(log, {
|
|
54
67
|
author: speaker.tier === "owner" ? "owner" : `human:${speaker.id}`,
|
|
55
68
|
authorName:
|
|
56
69
|
speaker.tier === "owner" ? this.#host.options.owner.name : speaker.name,
|
|
@@ -63,7 +76,7 @@ export class GroupTurns {
|
|
|
63
76
|
const approvals = new Map<string, boolean>();
|
|
64
77
|
for (const member of members) {
|
|
65
78
|
const pending = await runtime.heldActions(
|
|
66
|
-
groupSessionKey(group, member.name),
|
|
79
|
+
groupSessionKey(group, member.name, channel),
|
|
67
80
|
);
|
|
68
81
|
// Only a speaker whose tier holds the held tools may approve or wake a seat for them.
|
|
69
82
|
if (!pending || !this.#host.mayApprove(speaker, pending)) continue;
|
|
@@ -100,10 +113,10 @@ export class GroupTurns {
|
|
|
100
113
|
if (member?.status !== "active") continue;
|
|
101
114
|
const first = !spoken.has(name);
|
|
102
115
|
spoken.add(name);
|
|
103
|
-
const backlog = await store.backlog(
|
|
116
|
+
const backlog = await store.backlog(log, name, BACKLOG_LIMIT);
|
|
104
117
|
// Delivered once the session has the text, so a message never comes twice.
|
|
105
118
|
if (backlog.lastId !== undefined)
|
|
106
|
-
await store.advanceCursor(
|
|
119
|
+
await store.advanceCursor(log, name, backlog.lastId);
|
|
107
120
|
// Attachments and a confirmation belong to a member's first turn of the round only.
|
|
108
121
|
const extra: TurnExtra = { interactive: true };
|
|
109
122
|
if (first) {
|
|
@@ -112,7 +125,7 @@ export class GroupTurns {
|
|
|
112
125
|
}
|
|
113
126
|
const result = await this.#host.turn(
|
|
114
127
|
member,
|
|
115
|
-
groupScope(member, group),
|
|
128
|
+
groupScope(member, group, channel),
|
|
116
129
|
channel,
|
|
117
130
|
groupTurnText(group, member, backlog),
|
|
118
131
|
chain,
|
|
@@ -121,7 +134,7 @@ export class GroupTurns {
|
|
|
121
134
|
replies += 1;
|
|
122
135
|
last = member;
|
|
123
136
|
if (!result.ok) continue;
|
|
124
|
-
await store.appendGroupMessage(
|
|
137
|
+
await store.appendGroupMessage(log, {
|
|
125
138
|
author: name,
|
|
126
139
|
authorName: this.#host.current(member).displayName,
|
|
127
140
|
text: result.text,
|