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
|
@@ -22,13 +22,14 @@ import { outcome } from "../routing/settle-turn.ts";
|
|
|
22
22
|
import { systemTurn } from "../routing/system-turns.ts";
|
|
23
23
|
import { addressee, attributed, type Speaker } from "../speakers.ts";
|
|
24
24
|
import type { DiscordAgentTeam } from "./agent-team.ts";
|
|
25
|
-
import { discordIdOf } from "./team-keys.ts";
|
|
25
|
+
import { discordIdOf, roomOf } from "./team-keys.ts";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The agent server outranks every other claim on the Discord channels it owns, and its guild's
|
|
29
29
|
* other channels stay silent even when another plugin's mode or a mention would answer there; a claim of a
|
|
30
|
-
* plugin must use a higher priority to answer in that guild.
|
|
31
|
-
*
|
|
30
|
+
* plugin must use a higher priority to answer in that guild. Of another surface it owns only
|
|
31
|
+
* the rooms of an agent or group of the team on a surface that declares `agentRooms`, never
|
|
32
|
+
* another of its keys. It becomes the core's reserved branch once the team moves into the core
|
|
32
33
|
* (pi-roundtable batch 7).
|
|
33
34
|
*/
|
|
34
35
|
export const AGENT_SERVER_PRIORITY = 100;
|
|
@@ -58,7 +59,12 @@ export interface AgentClaimOptions {
|
|
|
58
59
|
| "startFresh"
|
|
59
60
|
>;
|
|
60
61
|
runtime: Pick<AgentRuntime, "steer" | "stop">;
|
|
61
|
-
|
|
62
|
+
/**
|
|
63
|
+
* `roomSurfaces` names the surfaces whose agent and group rooms the claim may own, and `of`
|
|
64
|
+
* finds the surface that reads a message's attachments; without them the claim is Discord's alone.
|
|
65
|
+
*/
|
|
66
|
+
surface: Pick<SurfacePort, "react" | "unreact"> &
|
|
67
|
+
Partial<Pick<SurfacePort, "roomSurfaces" | "of">>;
|
|
62
68
|
/** Where a channel's attachments are saved. */
|
|
63
69
|
attachmentDir: (channel: ChannelKey) => string;
|
|
64
70
|
/** The downloads' limits: all of a message's files together (default 60 s), and one file without a byte (default 10 s). */
|
|
@@ -82,14 +88,24 @@ export interface AgentClaimOptions {
|
|
|
82
88
|
*/
|
|
83
89
|
export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
84
90
|
const { team, runtime, surface, attachmentDir, logger, fetchImpl } = options;
|
|
85
|
-
const attachments = (message: InboundMessage) =>
|
|
86
|
-
|
|
91
|
+
const attachments = (message: InboundMessage) => {
|
|
92
|
+
// A surface that made a message's attachments reads them itself; they are never fetched.
|
|
93
|
+
const surfaceOf = surface.of?.(message.channel);
|
|
94
|
+
const reader = surfaceOf?.readAttachment?.bind(surfaceOf);
|
|
95
|
+
return attachmentsOf(
|
|
87
96
|
message,
|
|
88
97
|
attachmentDir(message.channel),
|
|
89
98
|
logger,
|
|
90
99
|
fetchImpl,
|
|
91
|
-
|
|
100
|
+
{
|
|
101
|
+
...options.attachmentDownload,
|
|
102
|
+
...(surfaceOf?.maxAttachmentBytes === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { maxBytes: surfaceOf.maxAttachmentBytes }),
|
|
105
|
+
...(reader ? { read: (ref) => reader(message.channel, ref) } : {}),
|
|
106
|
+
},
|
|
92
107
|
);
|
|
108
|
+
};
|
|
93
109
|
/** The channel context of a message in an agent's channel; none, logged, when it cannot be read. */
|
|
94
110
|
const around = async (
|
|
95
111
|
message: InboundMessage,
|
|
@@ -148,11 +164,14 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
148
164
|
return {
|
|
149
165
|
name: "agent-server",
|
|
150
166
|
priority: AGENT_SERVER_PRIORITY,
|
|
151
|
-
//
|
|
167
|
+
// Discord's channels, or the room of an agent or group on a surface that hosts rooms: another
|
|
168
|
+
// surface's key is never the agent server's, whatever its id, and a room needs a known name.
|
|
152
169
|
owns: (channel, space) =>
|
|
153
|
-
discordIdOf(channel) !== undefined
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
discordIdOf(channel) !== undefined
|
|
171
|
+
? team.owns(channel) !== undefined ||
|
|
172
|
+
(space !== undefined && space === team.guildId)
|
|
173
|
+
: roomOf(channel, surface.roomSurfaces?.() ?? []) !== undefined &&
|
|
174
|
+
team.owns(channel) !== undefined,
|
|
156
175
|
admit: (message) => {
|
|
157
176
|
const owned = team.owns(message.channel);
|
|
158
177
|
// The agent server's other channels are the owner's notes: nothing there is answered.
|
|
@@ -61,7 +61,7 @@ function groupBlock(group: GroupStatus, now: Date): string {
|
|
|
61
61
|
? text.dashLastActive(when(group.lastActive, now))
|
|
62
62
|
: text.dashNotActive;
|
|
63
63
|
return [
|
|
64
|
-
`**${group.displayName}
|
|
64
|
+
`**${group.displayName}**${group.channelId ? ` <#${group.channelId}>` : ""}`,
|
|
65
65
|
text.dashGroupMembers(group.members, group.host),
|
|
66
66
|
state,
|
|
67
67
|
`-# ${last}`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChannelKey } from "../domain/conversation.ts";
|
|
1
2
|
import { AgentError } from "../domain/errors.ts";
|
|
2
3
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
3
4
|
import { messages } from "../i18n/index.ts";
|
|
@@ -12,7 +13,13 @@ import {
|
|
|
12
13
|
returnedAnswerPost,
|
|
13
14
|
} from "./agent-prompt.ts";
|
|
14
15
|
import type { Agent } from "./agent-store.ts";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
agentRoomKey,
|
|
18
|
+
discordKey,
|
|
19
|
+
homeScope,
|
|
20
|
+
roomScope,
|
|
21
|
+
roomSurfaceOf,
|
|
22
|
+
} from "./team-keys.ts";
|
|
16
23
|
import {
|
|
17
24
|
type Chain,
|
|
18
25
|
type Exchange,
|
|
@@ -34,7 +41,11 @@ export class AgentMessages {
|
|
|
34
41
|
throw new AgentError("message_agent is for other agents, not yourself.");
|
|
35
42
|
const target = store.activeAgent(to);
|
|
36
43
|
const sender = store.activeAgent(caller.name);
|
|
37
|
-
|
|
44
|
+
// A turn in a room of another surface talks to the other agents' rooms there, never in Discord.
|
|
45
|
+
const parent = this.#host.turnChannel(caller);
|
|
46
|
+
const via = roomSurfaceOf(parent);
|
|
47
|
+
if (via === undefined && !target.channelId)
|
|
48
|
+
throw new AgentError(`${to} has no channel yet.`);
|
|
38
49
|
// Every agent turn runs through #turn, which registers its chain; a message from anywhere
|
|
39
50
|
// else would start an uncounted chain, so it is refused.
|
|
40
51
|
const chain = this.#host.chains.get(caller.session);
|
|
@@ -47,11 +58,13 @@ export class AgentMessages {
|
|
|
47
58
|
chainLimitReason(addressee(chain.speaker, this.#host.options.owner)),
|
|
48
59
|
);
|
|
49
60
|
chain.messages += 1;
|
|
50
|
-
const targetChannel =
|
|
51
|
-
|
|
61
|
+
const targetChannel = this.#home(target, via);
|
|
62
|
+
// The thread opens in the sender's turn channel, on its surface when that hosts threads; a
|
|
63
|
+
// surface that hosts none leaves the answer returning to the sender's room.
|
|
64
|
+
const exchange = this.#openExchange(parent, sender, target, text);
|
|
52
65
|
void queue
|
|
53
66
|
.run(targetChannel, async () =>
|
|
54
|
-
this.#deliver(sender, target, text, chain, await exchange),
|
|
67
|
+
this.#deliver(sender, target, text, chain, await exchange, via),
|
|
55
68
|
)
|
|
56
69
|
.catch((error: unknown) =>
|
|
57
70
|
logger.error(
|
|
@@ -66,17 +79,24 @@ export class AgentMessages {
|
|
|
66
79
|
return `Sent to "${target.displayName}". It works in its own channel; its answer comes back to you as a new turn.`;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
/** The agent's own channel: its Discord channel, or its room on the surface of a room turn. */
|
|
83
|
+
#home(agent: Agent, via: string | undefined): ChannelKey {
|
|
84
|
+
return via === undefined
|
|
85
|
+
? discordKey(agent.channelId ?? "")
|
|
86
|
+
: agentRoomKey(via, agent.name);
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
/**
|
|
70
|
-
* Opens the message's thread in the sender's turn channel
|
|
71
|
-
* the sender's name; undefined where no thread opens. Never
|
|
90
|
+
* Opens the message's thread in the sender's turn channel, a Discord channel or a room, and
|
|
91
|
+
* posts the request there under the sender's name; undefined where no thread opens. Never
|
|
92
|
+
* rejects.
|
|
72
93
|
*/
|
|
73
94
|
async #openExchange(
|
|
74
|
-
|
|
95
|
+
parent: ChannelKey,
|
|
75
96
|
sender: Agent,
|
|
76
97
|
target: Agent,
|
|
77
98
|
text: string,
|
|
78
99
|
): Promise<Exchange | undefined> {
|
|
79
|
-
const parent = this.#host.turnChannel(caller);
|
|
80
100
|
const thread = await this.#host.options.threads?.open(
|
|
81
101
|
parent,
|
|
82
102
|
`→ ${target.displayName}`,
|
|
@@ -105,9 +125,10 @@ export class AgentMessages {
|
|
|
105
125
|
text: string,
|
|
106
126
|
chain: Chain,
|
|
107
127
|
exchange: Exchange | undefined,
|
|
128
|
+
via: string | undefined,
|
|
108
129
|
): Promise<void> {
|
|
109
130
|
try {
|
|
110
|
-
await this.#exchange(sender, target, text, chain, exchange);
|
|
131
|
+
await this.#exchange(sender, target, text, chain, exchange, via);
|
|
111
132
|
} finally {
|
|
112
133
|
await exchange?.thread.close();
|
|
113
134
|
}
|
|
@@ -119,9 +140,10 @@ export class AgentMessages {
|
|
|
119
140
|
text: string,
|
|
120
141
|
chain: Chain,
|
|
121
142
|
exchange: Exchange | undefined,
|
|
143
|
+
via: string | undefined,
|
|
122
144
|
): Promise<void> {
|
|
123
145
|
const { queue, logger } = this.#host.options;
|
|
124
|
-
const targetChannel =
|
|
146
|
+
const targetChannel = this.#home(target, via);
|
|
125
147
|
await this.#host.post(targetChannel, this.#host.current(sender), {
|
|
126
148
|
chunks: splitReply(
|
|
127
149
|
deliveredMessagePost(this.#host.current(target), text),
|
|
@@ -129,7 +151,7 @@ export class AgentMessages {
|
|
|
129
151
|
});
|
|
130
152
|
const result = await this.#host.turn(
|
|
131
153
|
target,
|
|
132
|
-
homeScope(target),
|
|
154
|
+
via === undefined ? homeScope(target) : roomScope(target, targetChannel),
|
|
133
155
|
targetChannel,
|
|
134
156
|
agentMessageText(
|
|
135
157
|
this.#host.current(sender),
|
|
@@ -142,8 +164,12 @@ export class AgentMessages {
|
|
|
142
164
|
if (thread && !result.ok)
|
|
143
165
|
await thread.close(messages().agentNoReply(result.error.message));
|
|
144
166
|
const current = this.#host.options.store.agent(sender.name);
|
|
145
|
-
if (
|
|
146
|
-
|
|
167
|
+
if (
|
|
168
|
+
current?.status !== "active" ||
|
|
169
|
+
(via === undefined && !current.channelId)
|
|
170
|
+
)
|
|
171
|
+
return;
|
|
172
|
+
const senderChannel = this.#home(current, via);
|
|
147
173
|
// With a thread the answer goes there, leaving the sender's channel its own reply.
|
|
148
174
|
if (result.ok)
|
|
149
175
|
await this.#host
|
|
@@ -173,7 +199,9 @@ export class AgentMessages {
|
|
|
173
199
|
.run(senderChannel, () =>
|
|
174
200
|
this.#host.turn(
|
|
175
201
|
current,
|
|
176
|
-
|
|
202
|
+
via === undefined
|
|
203
|
+
? homeScope(current)
|
|
204
|
+
: roomScope(current, senderChannel),
|
|
177
205
|
senderChannel,
|
|
178
206
|
followUp,
|
|
179
207
|
chain,
|
|
@@ -23,8 +23,6 @@ export function agentSystemPrompt(input: {
|
|
|
23
23
|
group?: { group: AgentGroup; members: readonly Agent[] };
|
|
24
24
|
/** Who the turn is for; without one, or the owner, the prompt speaks to the owner. */
|
|
25
25
|
speaker?: Speaker;
|
|
26
|
-
/** Whether agents can redraw avatars; false without an image provider. Default true. */
|
|
27
|
-
avatars?: boolean;
|
|
28
26
|
/** Whether the session offers generate_image; true only with an image provider. Default false. */
|
|
29
27
|
images?: boolean;
|
|
30
28
|
}): string {
|
|
@@ -38,7 +36,7 @@ export function agentSystemPrompt(input: {
|
|
|
38
36
|
(guest && input.guestShared) || shared,
|
|
39
37
|
`You are "${agent.displayName}" (agent name \`${agent.name}\`), one of ${o.name}'s agents in ${o.his} Discord agent server. Each agent owns one channel and one conversation; you all share ${o.his} tools and ${o.his} memory.`,
|
|
40
38
|
roleText(agent, coordinator, who),
|
|
41
|
-
`The team: agent_list shows every agent and group; agent_get and agent_update read and improve any agent's prompt, display name, model, and thinking level, yours included; agent_create adds an agent with its own channel;
|
|
39
|
+
`The team: agent_list shows every agent and group; agent_get and agent_update read and improve any agent's prompt, display name, model, and thinking level, yours included; agent_create adds an agent with its own channel; agent_avatar changes an agent's look (class, colors, parts); schedule_list with agent reads another agent's schedules. A message marked as coming from another agent is that agent speaking, not ${w.name}; only ${w.name} approves held actions.`,
|
|
42
40
|
`Your shell and file tools run on ${o.his} VPS as the user ${shellUser}, in the shared workspace ${workDir}.${input.scratchDir ? ` Write temporary files under the scratch dir ${input.scratchDir} ($TMPDIR), not elsewhere in /tmp; make them with \`mktemp -p "$TMPDIR"\`, since a bare \`mktemp\` ignores $TMPDIR on some systems.` : ""} Commands that are destructive or change the system, and writes outside the workspace${input.scratchDir ? " and the scratch dir" : ""}, are held for ${w.his} confirmation: tell ${w.him} exactly what will run and ask ${w.him} to confirm. To show a file you have, attach it to your reply with attach_file or give its path to discord_send_message; never read an image or base64 a file to send it.${input.images ? " To make a new picture, describe it to generate_image, which saves it and attaches it to your reply." : ""}`,
|
|
43
41
|
];
|
|
44
42
|
if (group) {
|
|
@@ -79,7 +77,7 @@ function roleText(
|
|
|
79
77
|
if (coordinator?.name === agent.name)
|
|
80
78
|
return `You are the team's coordinator: ${o.name}'s general requests come to you, and you hand specialist work to the agent that owns it.`;
|
|
81
79
|
const who = coordinator
|
|
82
|
-
? `The team's coordinator is "${coordinator.displayName}" (\`${coordinator.name}\`) in <#${coordinator.channelId}
|
|
80
|
+
? `The team's coordinator is "${coordinator.displayName}" (\`${coordinator.name}\`)${coordinator.channelId ? ` in <#${coordinator.channelId}>` : ""}.`
|
|
83
81
|
: "The team's coordinator is the agent in the server's entry channel.";
|
|
84
82
|
return `${who} You are a specialist: speak of yourself only as "${agent.displayName}", never as the coordinator. Team work ${o.name} asks of you directly, such as creating agents or groups, archiving, or arranging channels, you do in your own name. When your role below is empty or only a placeholder, ask ${o.him} what your job is before taking on other work.`;
|
|
85
83
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CharacterInput,
|
|
3
|
+
type CharacterSpec,
|
|
4
|
+
suggestCharacter,
|
|
5
|
+
} from "../../characters/index.ts";
|
|
1
6
|
import { AgentError } from "../domain/errors.ts";
|
|
2
7
|
import { THINKING_LEVELS, type ThinkingLevel } from "../models.ts";
|
|
3
8
|
|
|
@@ -14,7 +19,9 @@ export interface Agent {
|
|
|
14
19
|
displayName: string;
|
|
15
20
|
prompt: string;
|
|
16
21
|
avatarPrompt: string;
|
|
17
|
-
/**
|
|
22
|
+
/** What the agent looks like; absent only on a row read before the team assigned one. */
|
|
23
|
+
character?: CharacterSpec;
|
|
24
|
+
/** The rendered picture's content hash, when one exists. */
|
|
18
25
|
avatarHash?: string;
|
|
19
26
|
/** Its channel in the agent server; a seed agent has none until one is created. */
|
|
20
27
|
channelId?: string;
|
|
@@ -28,7 +35,8 @@ export interface Agent {
|
|
|
28
35
|
export interface AgentGroup {
|
|
29
36
|
name: string;
|
|
30
37
|
displayName: string;
|
|
31
|
-
|
|
38
|
+
/** Its Discord channel; absent in a team without Discord, whose groups live in rooms only. */
|
|
39
|
+
channelId?: string;
|
|
32
40
|
/** Agent names in the group's order. */
|
|
33
41
|
members: string[];
|
|
34
42
|
host: string;
|
|
@@ -39,7 +47,10 @@ export interface AgentSeed {
|
|
|
39
47
|
name: string;
|
|
40
48
|
displayName: string;
|
|
41
49
|
prompt: string;
|
|
42
|
-
|
|
50
|
+
/** The agent's look, resolved over the class preset; without one the team suggests it from the role. */
|
|
51
|
+
character?: CharacterInput;
|
|
52
|
+
/** @deprecated Agents are composed characters; this is ignored and removed in 1.0. */
|
|
53
|
+
avatarPrompt?: string;
|
|
43
54
|
channelId?: string;
|
|
44
55
|
}
|
|
45
56
|
|
|
@@ -104,3 +115,31 @@ export function checkAvatarPrompt(prompt: string): void {
|
|
|
104
115
|
`An avatar prompt must have 1 to ${MAX_AVATAR_PROMPT_CHARS} characters; this one has ${prompt.length}.`,
|
|
105
116
|
);
|
|
106
117
|
}
|
|
118
|
+
|
|
119
|
+
/** An agent's look: the one it was dressed in, otherwise the one its role suggests. */
|
|
120
|
+
export function characterOf(
|
|
121
|
+
agent: Pick<Agent, "name" | "displayName" | "prompt" | "character">,
|
|
122
|
+
): CharacterSpec {
|
|
123
|
+
return agent.character ?? suggestCharacter(agent);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* An input read over a base look. A new class alone keeps the base's colors, since the agent's
|
|
128
|
+
* colors are its own, and a class keeps the base's skin and build unless the input names them, so
|
|
129
|
+
* two agents of one class still differ.
|
|
130
|
+
*/
|
|
131
|
+
export function overBase(
|
|
132
|
+
input: CharacterInput,
|
|
133
|
+
base: CharacterSpec,
|
|
134
|
+
): CharacterInput {
|
|
135
|
+
if (input === null || typeof input !== "object" || input.class === undefined)
|
|
136
|
+
return input;
|
|
137
|
+
return {
|
|
138
|
+
skin: base.skin,
|
|
139
|
+
build: base.build,
|
|
140
|
+
...(input.palette === undefined && input.colors === undefined
|
|
141
|
+
? { colors: base.colors }
|
|
142
|
+
: {}),
|
|
143
|
+
...input,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readCharacter } from "../../characters/index.ts";
|
|
1
2
|
import { scopeToGuild } from "../db/guild-scope.ts";
|
|
2
3
|
import type { Migration } from "../db/migrations.ts";
|
|
3
4
|
import type { ThinkingLevel } from "../models.ts";
|
|
@@ -9,6 +10,8 @@ export interface AgentRow {
|
|
|
9
10
|
prompt: string;
|
|
10
11
|
avatar_prompt: string;
|
|
11
12
|
avatar_hash: string | null;
|
|
13
|
+
/** jsonb: the stored spec, an object, or null before the team dressed the agent. */
|
|
14
|
+
character: unknown | null;
|
|
12
15
|
channel_id: string | null;
|
|
13
16
|
model: string | null;
|
|
14
17
|
thinking: ThinkingLevel | null;
|
|
@@ -18,28 +21,32 @@ export interface AgentRow {
|
|
|
18
21
|
export interface GroupRow {
|
|
19
22
|
name: string;
|
|
20
23
|
display_name: string;
|
|
21
|
-
channel_id: string;
|
|
24
|
+
channel_id: string | null;
|
|
22
25
|
members: string;
|
|
23
26
|
host: string;
|
|
24
27
|
status: AgentStatus;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
export const toAgent = (row: AgentRow): Agent =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
30
|
+
export const toAgent = (row: AgentRow): Agent => {
|
|
31
|
+
const character = readCharacter(row.character);
|
|
32
|
+
return {
|
|
33
|
+
name: row.name,
|
|
34
|
+
displayName: row.display_name,
|
|
35
|
+
prompt: row.prompt,
|
|
36
|
+
avatarPrompt: row.avatar_prompt,
|
|
37
|
+
...(character ? { character } : {}),
|
|
38
|
+
...(row.avatar_hash ? { avatarHash: row.avatar_hash } : {}),
|
|
39
|
+
...(row.channel_id ? { channelId: row.channel_id } : {}),
|
|
40
|
+
...(row.model ? { model: row.model } : {}),
|
|
41
|
+
...(row.thinking ? { thinking: row.thinking } : {}),
|
|
42
|
+
status: row.status,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
38
45
|
|
|
39
46
|
export const toGroup = (row: GroupRow): AgentGroup => ({
|
|
40
47
|
name: row.name,
|
|
41
48
|
displayName: row.display_name,
|
|
42
|
-
channelId: row.channel_id,
|
|
49
|
+
...(row.channel_id ? { channelId: row.channel_id } : {}),
|
|
43
50
|
members: row.members.split(",").filter(Boolean),
|
|
44
51
|
host: row.host,
|
|
45
52
|
status: row.status,
|
|
@@ -95,8 +102,9 @@ export const agentsMigration: Migration = {
|
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
/**
|
|
98
|
-
* The migrations of the store's tables for the guild the process serves
|
|
99
|
-
* each row's guild, with existing rows given `guildId
|
|
105
|
+
* The migrations of the store's tables for the guild the process serves, or the local team
|
|
106
|
+
* without Discord: the tables, then each row's guild, with existing rows given `guildId`, then
|
|
107
|
+
* the group channel made optional.
|
|
100
108
|
*/
|
|
101
109
|
export function agentMigrations(guildId: string): Migration[] {
|
|
102
110
|
return [
|
|
@@ -111,5 +119,19 @@ export function agentMigrations(guildId: string): Migration[] {
|
|
|
111
119
|
{ table: "agent_group_cursors", key: ["group_name", "agent_name"] },
|
|
112
120
|
]),
|
|
113
121
|
},
|
|
122
|
+
// A team without Discord has groups that live in rooms only, with no channel.
|
|
123
|
+
{
|
|
124
|
+
name: "agent-groups-channel-optional",
|
|
125
|
+
up: async (sql) => {
|
|
126
|
+
await sql`ALTER TABLE agent_groups ALTER COLUMN channel_id DROP NOT NULL`;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// The composed look, dressed in code at the team's start; rows keep their drawn picture until then.
|
|
130
|
+
{
|
|
131
|
+
name: "agents-character",
|
|
132
|
+
up: async (sql) => {
|
|
133
|
+
await sql`ALTER TABLE agents ADD COLUMN IF NOT EXISTS character jsonb`;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
114
136
|
];
|
|
115
137
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
|
+
import type { CharacterSpec } from "../../characters/index.ts";
|
|
2
3
|
import type { Migration } from "../db/migrations.ts";
|
|
3
4
|
import { AgentError } from "../domain/errors.ts";
|
|
4
5
|
import type { ThinkingLevel } from "../models.ts";
|
|
@@ -57,6 +58,8 @@ import { isCheckDatabase } from "../db/check-database.ts";
|
|
|
57
58
|
export class PgAgentStore implements AgentDirectory {
|
|
58
59
|
readonly #sql: SQL;
|
|
59
60
|
readonly #agents = new Map<string, Agent>();
|
|
61
|
+
/** Agents whose character column holds a value, readable or not. */
|
|
62
|
+
readonly #dressed = new Set<string>();
|
|
60
63
|
readonly #groups = new Map<string, AgentGroup>();
|
|
61
64
|
/** The tail of the agent and group writes, which run one at a time so none reads a map another is about to change. */
|
|
62
65
|
#queue: Promise<unknown> = Promise.resolve();
|
|
@@ -93,7 +96,8 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
93
96
|
.#sql`SELECT * FROM agent_groups WHERE guild_id = ${this.#guild}`;
|
|
94
97
|
this.#agents.clear();
|
|
95
98
|
this.#groups.clear();
|
|
96
|
-
|
|
99
|
+
this.#dressed.clear();
|
|
100
|
+
for (const row of agents) this.#keepAgentRow(row);
|
|
97
101
|
for (const row of groups) this.#groups.set(row.name, toGroup(row));
|
|
98
102
|
}
|
|
99
103
|
|
|
@@ -104,7 +108,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
104
108
|
for (const seed of seeds) {
|
|
105
109
|
const rows: AgentRow[] = await this.#sql`
|
|
106
110
|
INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id)
|
|
107
|
-
VALUES (${this.#guild}, ${seed.name}, ${seed.displayName}, ${seed.prompt}, ${seed.avatarPrompt},
|
|
111
|
+
VALUES (${this.#guild}, ${seed.name}, ${seed.displayName}, ${seed.prompt}, ${seed.avatarPrompt ?? ""},
|
|
108
112
|
${seed.channelId ?? null})
|
|
109
113
|
ON CONFLICT DO NOTHING
|
|
110
114
|
RETURNING *`;
|
|
@@ -186,20 +190,25 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
186
190
|
name: string;
|
|
187
191
|
displayName: string;
|
|
188
192
|
prompt: string;
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
/** @deprecated Ignored by the team; kept for a store written by an older host. */
|
|
194
|
+
avatarPrompt?: string;
|
|
195
|
+
character?: CharacterSpec;
|
|
196
|
+
avatarHash?: string;
|
|
197
|
+
/** Absent in a team without Discord, whose agents live in rooms. */
|
|
198
|
+
channelId?: string;
|
|
191
199
|
}): Promise<Agent> {
|
|
192
200
|
return this.#serial(async () => {
|
|
193
201
|
checkAgentName(agent.name);
|
|
194
202
|
checkDisplayName(agent.displayName);
|
|
195
203
|
checkPrompt(agent.prompt);
|
|
196
|
-
|
|
197
|
-
if (
|
|
204
|
+
const avatarPrompt = agent.avatarPrompt ?? "";
|
|
205
|
+
if (avatarPrompt !== "") checkAvatarPrompt(avatarPrompt);
|
|
198
206
|
this.#checkFree(agent.name);
|
|
199
207
|
const rows: AgentRow[] = await this.#sql`
|
|
200
|
-
INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id)
|
|
201
|
-
VALUES (${this.#guild}, ${agent.name}, ${agent.displayName.trim()}, ${agent.prompt}, ${
|
|
202
|
-
${agent.channelId}
|
|
208
|
+
INSERT INTO agents (guild_id, name, display_name, prompt, avatar_prompt, channel_id, avatar_hash, character)
|
|
209
|
+
VALUES (${this.#guild}, ${agent.name}, ${agent.displayName.trim()}, ${agent.prompt}, ${avatarPrompt},
|
|
210
|
+
${agent.channelId ?? null}, ${agent.avatarHash ?? null},
|
|
211
|
+
${agent.character ?? null}::jsonb)
|
|
203
212
|
RETURNING *`;
|
|
204
213
|
return this.#keepAgent(rows[0]);
|
|
205
214
|
});
|
|
@@ -218,6 +227,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
218
227
|
prompt?: string;
|
|
219
228
|
avatarPrompt?: string;
|
|
220
229
|
avatarHash?: string;
|
|
230
|
+
character?: CharacterSpec;
|
|
221
231
|
channelId?: string;
|
|
222
232
|
/** `null` returns it to the assistant's. */
|
|
223
233
|
model?: string | null;
|
|
@@ -245,6 +255,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
245
255
|
prompt = ${change.prompt ?? current.prompt},
|
|
246
256
|
avatar_prompt = ${change.avatarPrompt ?? current.avatarPrompt},
|
|
247
257
|
avatar_hash = ${change.avatarHash ?? current.avatarHash ?? null},
|
|
258
|
+
character = CASE WHEN ${change.character !== undefined} THEN ${change.character ?? null}::jsonb ELSE character END,
|
|
248
259
|
channel_id = ${change.channelId ?? current.channelId ?? null},
|
|
249
260
|
model = ${model},
|
|
250
261
|
thinking = ${thinking},
|
|
@@ -282,11 +293,43 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
282
293
|
|
|
283
294
|
#keepAgent(row: AgentRow | undefined): Agent {
|
|
284
295
|
if (!row) throw new AgentError("the agent was not found");
|
|
296
|
+
return this.#keepAgentRow(row);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#keepAgentRow(row: AgentRow): Agent {
|
|
285
300
|
const agent = toAgent(row);
|
|
286
301
|
this.#agents.set(agent.name, agent);
|
|
302
|
+
if (row.character !== null && row.character !== undefined)
|
|
303
|
+
this.#dressed.add(agent.name);
|
|
304
|
+
else this.#dressed.delete(agent.name);
|
|
287
305
|
return agent;
|
|
288
306
|
}
|
|
289
307
|
|
|
308
|
+
/** Whether the stored column holds a character, even one this version cannot read. */
|
|
309
|
+
hasStoredCharacter(name: string): boolean {
|
|
310
|
+
return this.#dressed.has(name);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Stores a character and picture only while the agent still has none, so a look chosen in the
|
|
315
|
+
* meantime wins. Returns whether it was written.
|
|
316
|
+
*/
|
|
317
|
+
async dressIfUndressed(
|
|
318
|
+
name: string,
|
|
319
|
+
character: CharacterSpec,
|
|
320
|
+
avatarHash: string,
|
|
321
|
+
): Promise<boolean> {
|
|
322
|
+
return this.#serial(async () => {
|
|
323
|
+
const rows: AgentRow[] = await this.#sql`
|
|
324
|
+
UPDATE agents SET character = ${character}::jsonb, avatar_hash = ${avatarHash}, updated_at = now()
|
|
325
|
+
WHERE guild_id = ${this.#guild} AND name = ${name} AND character IS NULL
|
|
326
|
+
RETURNING *`;
|
|
327
|
+
if (!rows[0]) return false;
|
|
328
|
+
this.#keepAgentRow(rows[0]);
|
|
329
|
+
return true;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
290
333
|
#checkMembers(members: readonly string[], host: string): void {
|
|
291
334
|
if (
|
|
292
335
|
new Set(members).size !== members.length ||
|
|
@@ -311,7 +354,8 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
311
354
|
async createGroup(group: {
|
|
312
355
|
name: string;
|
|
313
356
|
displayName: string;
|
|
314
|
-
|
|
357
|
+
/** Absent in a team without Discord, whose groups live in rooms. */
|
|
358
|
+
channelId?: string;
|
|
315
359
|
members: string[];
|
|
316
360
|
host: string;
|
|
317
361
|
}): Promise<AgentGroup> {
|
|
@@ -320,7 +364,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
320
364
|
checkDisplayName(group.displayName);
|
|
321
365
|
const rows: GroupRow[] = await this.#sql`
|
|
322
366
|
INSERT INTO agent_groups (guild_id, name, display_name, channel_id, members, host)
|
|
323
|
-
VALUES (${this.#guild}, ${group.name}, ${group.displayName.trim()}, ${group.channelId},
|
|
367
|
+
VALUES (${this.#guild}, ${group.name}, ${group.displayName.trim()}, ${group.channelId ?? null},
|
|
324
368
|
${group.members.join(",")}, ${group.host})
|
|
325
369
|
RETURNING *`;
|
|
326
370
|
return this.#keepGroup(rows[0]);
|
|
@@ -406,7 +450,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
406
450
|
}
|
|
407
451
|
|
|
408
452
|
/** Marks every message so far as received by every member, for a group started over. */
|
|
409
|
-
catchUp(group: AgentGroup): Promise<void> {
|
|
410
|
-
return this.#messages.catchUp(group);
|
|
453
|
+
catchUp(group: AgentGroup, log?: string): Promise<void> {
|
|
454
|
+
return this.#messages.catchUp(group, log);
|
|
411
455
|
}
|
|
412
456
|
}
|