pi-roundtable 0.22.5 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/README.md +35 -9
- package/README.zh-TW.md +51 -13
- package/docs/migrating-0.8.md +2 -2
- package/docs/plugins.md +351 -28
- package/examples/discord-adapter.ts +9 -1
- package/examples/seeds.test.ts +2 -2
- package/examples/seeds.ts +1 -2
- package/package.json +6 -2
- package/src/characters/catalog.ts +1055 -0
- package/src/characters/characters.css +68 -0
- package/src/characters/colors.ts +108 -0
- package/src/characters/compose.ts +281 -0
- package/src/characters/css.ts +63 -0
- package/src/characters/index.ts +33 -0
- package/src/characters/parse.ts +196 -0
- package/src/characters/parts-face.ts +354 -0
- package/src/characters/parts-hair.ts +56 -0
- package/src/characters/parts-hats.ts +57 -0
- package/src/characters/parts-outfits.ts +81 -0
- package/src/characters/parts-props.ts +137 -0
- package/src/characters/spec.ts +224 -0
- package/src/characters/suggest.ts +101 -0
- package/src/cli/checks/characters.ts +23 -0
- package/src/cli/checks/images.ts +5 -3
- package/src/cli/doctor.ts +2 -0
- package/src/core/agents/agent-claim.ts +30 -11
- package/src/core/agents/agent-dashboard.ts +1 -1
- package/src/core/agents/agent-messages.ts +43 -15
- package/src/core/agents/agent-prompt.ts +2 -4
- package/src/core/agents/agent-rules.ts +42 -3
- package/src/core/agents/agent-schema.ts +37 -15
- package/src/core/agents/agent-store.ts +57 -13
- package/src/core/agents/agent-team-fixture.ts +90 -14
- package/src/core/agents/agent-team.ts +83 -50
- package/src/core/agents/agent-tools.ts +94 -41
- package/src/core/agents/avatar-studio.ts +40 -4
- package/src/core/agents/character-png.ts +40 -0
- package/src/core/agents/group-messages.ts +7 -4
- package/src/core/agents/group-turns.ts +27 -14
- package/src/core/agents/team-editing.ts +99 -58
- package/src/core/agents/team-keys.ts +177 -11
- package/src/core/agents/team-lifecycle.ts +151 -84
- package/src/core/agents/team-options.ts +13 -6
- package/src/core/agents/team-status.ts +31 -13
- package/src/core/agents/team-text.ts +13 -3
- package/src/core/agents/team-turn-types.ts +25 -7
- package/src/core/agents/team-turns.ts +143 -28
- package/src/core/attachments/attachment-fetcher.ts +162 -64
- package/src/core/attachments/prompt-block.ts +7 -1
- package/src/core/attachments/tool-attachment.ts +20 -1
- package/src/core/attachments/turn-attachments.ts +18 -2
- package/src/core/builtin/agent-server.ts +103 -40
- package/src/core/builtin/discord.ts +4 -17
- package/src/core/builtin/modules.ts +6 -5
- package/src/core/builtin/threads.ts +37 -0
- package/src/core/config/agent-team.ts +43 -0
- package/src/core/config/config.ts +50 -59
- package/src/core/config/limits.ts +93 -0
- package/src/core/config/ops.ts +34 -0
- package/src/core/config/schema.ts +26 -0
- package/src/core/contract/channels.ts +20 -0
- package/src/core/contract/surface.ts +77 -7
- package/src/core/define-roundtable.ts +70 -42
- package/src/core/discord/agent-commands.ts +108 -86
- package/src/core/discord/agent-panel.ts +11 -2
- package/src/core/discord/connection.ts +1 -1
- package/src/core/discord/discord-surface.ts +10 -1
- package/src/core/discord/dispatch-thread-host.ts +1 -1
- package/src/core/domain/conversation.ts +24 -0
- package/src/core/holds.ts +221 -15
- package/src/core/host-registration.ts +2 -1
- package/src/core/http/listeners.ts +63 -3
- package/src/core/i18n/agent-panel.ts +14 -21
- package/src/core/interactions/prompts.ts +12 -0
- package/src/core/modes/autonomy.ts +23 -0
- package/src/core/modes/conversation-modes.ts +213 -0
- package/src/core/modes/modes-plugin.ts +24 -0
- package/src/core/modules/delegation/delegator.ts +4 -4
- package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
- package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
- package/src/core/modules/host-shell/shell-lexer.ts +493 -0
- package/src/core/modules/host-shell/shell-paths.ts +85 -4
- package/src/core/modules/host-shell/shell-policy.ts +341 -332
- package/src/core/modules/host-shell/shell-programs.ts +298 -0
- package/src/core/modules/host-shell/shell-quoting.ts +237 -0
- package/src/core/modules/host-shell/shell-scripts.ts +114 -0
- package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
- package/src/core/modules/schedules/precheck-tools.ts +17 -0
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/presence/direct-channels.ts +30 -0
- package/src/core/presentation/reply-body.ts +25 -0
- package/src/core/registry/contributions.ts +31 -7
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -15
- package/src/core/routing/surface-port.ts +5 -1
- package/src/core/runtime/extensions/approval-card.ts +1 -5
- package/src/core/runtime/extensions/approval-fields.ts +3 -7
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
- package/src/core/runtime/extensions/gate-refusals.ts +23 -0
- package/src/core/runtime/extensions/held-calls.ts +16 -1
- package/src/core/runtime/extensions/hold-describer.ts +58 -8
- package/src/core/runtime/extensions/hold-lookups.ts +19 -19
- package/src/core/runtime/extensions/yolo.ts +105 -0
- package/src/core/runtime/pi-agent-runtime.ts +3 -5
- package/src/core/runtime/runtime-plugin.ts +42 -1
- package/src/core/runtime/runtime-types.ts +11 -0
- package/src/core/runtime/turn-budget.ts +32 -5
- package/src/core/runtime/turn-limits.ts +14 -3
- package/src/core/services.ts +61 -9
- package/src/core/shared/attachment-reader.ts +6 -0
- package/src/core/shared/unix-server.ts +10 -1
- package/src/core/testing/modules.ts +12 -8
- package/src/core/testing/surface-threads.ts +58 -0
- package/src/core/testing/thread-host.ts +17 -5
- package/src/core/threads/dispatch-threads.ts +299 -0
- package/src/core/threads/host-threads.ts +19 -0
- package/src/core/tool-set.snapshot.json +5 -0
- package/src/index.ts +27 -1
- package/src/kit/characters.ts +5 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/shell.ts +1 -0
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/testing.ts +4 -0
- package/templates/agents.ts +1 -1
- package/src/core/discord/dispatch-threads.ts +0 -194
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CharacterError,
|
|
3
|
+
type CharacterInput,
|
|
4
|
+
type CharacterSpec,
|
|
5
|
+
resolveCharacter,
|
|
6
|
+
suggestCharacter,
|
|
7
|
+
} from "../../characters/index.ts";
|
|
1
8
|
import { AgentError } from "../domain/errors.ts";
|
|
2
9
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
3
|
-
import {
|
|
10
|
+
import type { AgentChange } from "../services.ts";
|
|
4
11
|
import { addressee } from "../speakers.ts";
|
|
5
12
|
import { openingTaskText } from "./agent-prompt.ts";
|
|
13
|
+
import { characterOf, overBase } from "./agent-rules.ts";
|
|
6
14
|
import { modelSetting, thinkingSetting } from "./agent-settings.ts";
|
|
7
15
|
import type { Agent } from "./agent-store.ts";
|
|
8
16
|
import type { AvatarMode } from "./agent-tools.ts";
|
|
9
|
-
import { discordKey } from "./team-keys.ts";
|
|
17
|
+
import { discordKey, keysOf } from "./team-keys.ts";
|
|
10
18
|
import { groupTopic, teamCategory } from "./team-layout.ts";
|
|
11
19
|
import type { TeamContext } from "./team-options.ts";
|
|
12
20
|
|
|
@@ -26,15 +34,7 @@ export class TeamEditing {
|
|
|
26
34
|
* Changes an agent's display name, prompt, model, or thinking level; `default` returns the
|
|
27
35
|
* model or thinking level to the assistant's. A model must be one the host runs.
|
|
28
36
|
*/
|
|
29
|
-
async update(
|
|
30
|
-
name: string,
|
|
31
|
-
change: {
|
|
32
|
-
displayName?: string;
|
|
33
|
-
prompt?: string;
|
|
34
|
-
model?: string;
|
|
35
|
-
thinking?: string;
|
|
36
|
-
},
|
|
37
|
-
): Promise<string> {
|
|
37
|
+
async update(name: string, change: AgentChange): Promise<string> {
|
|
38
38
|
const { store, logger } = this.#ctx.options;
|
|
39
39
|
if (Object.values(change).every((value) => value === undefined))
|
|
40
40
|
throw new AgentError(
|
|
@@ -78,14 +78,16 @@ export class TeamEditing {
|
|
|
78
78
|
name: string;
|
|
79
79
|
displayName: string;
|
|
80
80
|
prompt: string;
|
|
81
|
-
/**
|
|
81
|
+
/** @deprecated Ignored: agents are composed characters. */
|
|
82
82
|
avatarPrompt?: string;
|
|
83
|
+
/** Fields of its look; any left out come from the look its role suggests. */
|
|
84
|
+
character?: CharacterInput;
|
|
83
85
|
task: string;
|
|
84
86
|
category?: string;
|
|
85
87
|
skills?: string[];
|
|
86
88
|
},
|
|
87
89
|
): Promise<string> {
|
|
88
|
-
const { store, studio, channels, queue, skills, logger } =
|
|
90
|
+
const { store, studio, channels, queue, skills, surfaces, logger } =
|
|
89
91
|
this.#ctx.options;
|
|
90
92
|
if (input.skills && input.skills.length > 0 && !skills)
|
|
91
93
|
throw new AgentError(
|
|
@@ -96,45 +98,58 @@ export class TeamEditing {
|
|
|
96
98
|
throw new AgentError(
|
|
97
99
|
"agent_create can only be used during an agent turn.",
|
|
98
100
|
);
|
|
99
|
-
const drawing = studio.canDraw !== false;
|
|
100
101
|
const {
|
|
101
102
|
category,
|
|
102
103
|
skills: carried = [],
|
|
103
|
-
|
|
104
|
+
character: wanted,
|
|
105
|
+
avatarPrompt: _ignored,
|
|
104
106
|
...fields
|
|
105
107
|
} = input;
|
|
106
|
-
if (drawing && avatarPrompt.trim() === "")
|
|
107
|
-
throw new AgentError("Give avatar_prompt: the avatar is drawn from it.");
|
|
108
108
|
const parent = teamCategory(category, "Agents");
|
|
109
109
|
store.checkNewName(input.name);
|
|
110
110
|
skills?.checkRegistered(carried);
|
|
111
|
-
|
|
111
|
+
// The picture comes before the row and the channel: a render failure fails the create.
|
|
112
|
+
const taken = store
|
|
113
|
+
.agents()
|
|
114
|
+
.flatMap((other) =>
|
|
115
|
+
other.status === "active" && other.character ? [other.character] : [],
|
|
116
|
+
);
|
|
117
|
+
const suggested = suggestCharacter(input, taken);
|
|
118
|
+
const character = this.#resolve(wanted ?? {}, suggested);
|
|
119
|
+
const avatarHash = await studio.render(character);
|
|
120
|
+
// Without Discord channels the agent lives in its rooms, which exist as soon as it does.
|
|
121
|
+
const channelId = await channels?.createChannel(
|
|
112
122
|
input.name,
|
|
113
123
|
input.displayName,
|
|
114
124
|
parent,
|
|
115
125
|
);
|
|
116
126
|
const agent = await store.createAgent({
|
|
117
127
|
...fields,
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
character,
|
|
129
|
+
avatarHash,
|
|
130
|
+
...(channelId ? { channelId } : {}),
|
|
120
131
|
});
|
|
121
132
|
if (skills && carried.length > 0)
|
|
122
133
|
await skills.attach(agent.name, carried, []);
|
|
123
|
-
logger.info(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
? this.#redraw(agent.name, "redraw")
|
|
127
|
-
: this.#generate(agent)
|
|
128
|
-
).then(
|
|
129
|
-
() =>
|
|
130
|
-
drawing
|
|
131
|
-
? "its avatar is drawn"
|
|
132
|
-
: "its avatar is generated from its display name",
|
|
133
|
-
(error: unknown) =>
|
|
134
|
-
`its avatar could not be drawn (${error instanceof Error ? error.message : String(error)}), so it uses ${assistantName()}'s for now`,
|
|
134
|
+
logger.info(
|
|
135
|
+
{ agent: agent.name, by: caller.name, class: character.class },
|
|
136
|
+
"agent created",
|
|
135
137
|
);
|
|
136
|
-
|
|
138
|
+
this.#ctx.changed();
|
|
139
|
+
const avatar = `its picture is the ${character.class} look`;
|
|
140
|
+
// The opening task runs where the agent is first reached: its channel, else its first room.
|
|
141
|
+
const home = channelId
|
|
142
|
+
? discordKey(channelId)
|
|
143
|
+
: keysOf(store, agent.name, surfaces?.roomSurfaces() ?? [])[0];
|
|
137
144
|
const creator = store.agent(caller.name);
|
|
145
|
+
const where = channelId ? ` in <#${channelId}>` : "";
|
|
146
|
+
if (!home) {
|
|
147
|
+
logger.warn(
|
|
148
|
+
{ agent: agent.name },
|
|
149
|
+
"opening turn skipped: the agent has no channel or room",
|
|
150
|
+
);
|
|
151
|
+
return `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}. It has no room to start its opening task in yet.`;
|
|
152
|
+
}
|
|
138
153
|
void queue
|
|
139
154
|
.run(home, () =>
|
|
140
155
|
this.#ctx.turns.answerBackground(
|
|
@@ -150,33 +165,45 @@ export class TeamEditing {
|
|
|
150
165
|
.catch((error: unknown) =>
|
|
151
166
|
logger.error({ agent: agent.name, err: error }, "opening turn failed"),
|
|
152
167
|
);
|
|
153
|
-
return `Created "${agent.displayName}" (${agent.name})
|
|
168
|
+
return `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}. It is starting on its opening task now.`;
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
171
|
+
/** The agent tool: changes a look, then says what the owner sees. */
|
|
172
|
+
async avatar(name: string, look: CharacterInput): Promise<string> {
|
|
173
|
+
const agent = await this.dress(name, look);
|
|
174
|
+
return `"${agent.displayName}" now looks like a ${characterOf(agent).class}; the new picture shows from its next message.`;
|
|
159
175
|
}
|
|
160
176
|
|
|
161
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Changes an agent's look. The input is resolved over the agent's current character; a new
|
|
179
|
+
* class keeps the agent's colors unless the input names a palette or colors. The picture is
|
|
180
|
+
* rendered before anything is stored, and the character and picture are stored together.
|
|
181
|
+
*/
|
|
182
|
+
async dress(name: string, input: CharacterInput): Promise<Agent> {
|
|
183
|
+
const { store, studio, logger } = this.#ctx.options;
|
|
184
|
+
const agent = store.activeAgent(name);
|
|
185
|
+
const base = characterOf(agent);
|
|
186
|
+
const character = this.#resolve(input, base);
|
|
187
|
+
let avatarHash: string;
|
|
188
|
+
try {
|
|
189
|
+
avatarHash = await studio.render(character);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new AgentError(
|
|
192
|
+
`The picture could not be rendered: ${error instanceof Error ? error.message : String(error)}. The previous look stays.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const dressed = await store.updateAgent(name, { character, avatarHash });
|
|
196
|
+
logger.info({ agent: name, class: character.class }, "agent dressed");
|
|
197
|
+
this.#ctx.changed();
|
|
198
|
+
return dressed;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @deprecated Use `dress`; removed in 1.0. Draws and stores a new picture. */
|
|
162
202
|
async redrawAvatar(
|
|
163
203
|
name: string,
|
|
164
204
|
mode: AvatarMode,
|
|
165
205
|
text?: string,
|
|
166
206
|
): Promise<Agent> {
|
|
167
|
-
return this.#redraw(name, mode, text);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Gives an agent the picture made from its display name; a host without an image provider has no other. */
|
|
171
|
-
async #generate(agent: Agent): Promise<Agent> {
|
|
172
|
-
const { store, studio } = this.#ctx.options;
|
|
173
|
-
if (!studio.fallback)
|
|
174
|
-
throw new AgentError("The avatar studio has no generated avatars.");
|
|
175
|
-
const avatarHash = await studio.fallback(agent.displayName, agent.name);
|
|
176
|
-
return store.updateAgent(agent.name, { avatarHash });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async #redraw(name: string, mode: AvatarMode, text?: string): Promise<Agent> {
|
|
180
207
|
const { store, studio, logger } = this.#ctx.options;
|
|
181
208
|
const agent = store.activeAgent(name);
|
|
182
209
|
if (studio.canDraw === false) throw new AgentError(NO_IMAGE_PROVIDER);
|
|
@@ -205,6 +232,19 @@ export class TeamEditing {
|
|
|
205
232
|
return store.updateAgent(name, { avatarHash: hash });
|
|
206
233
|
}
|
|
207
234
|
|
|
235
|
+
/**
|
|
236
|
+
* An input over a base look (see overBase); a bad field is an AgentError that names it.
|
|
237
|
+
*/
|
|
238
|
+
#resolve(input: CharacterInput, base: CharacterSpec): CharacterSpec {
|
|
239
|
+
try {
|
|
240
|
+
return resolveCharacter(overBase(input, base), base);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof CharacterError)
|
|
243
|
+
throw new AgentError(`The look is not usable: ${error.message}.`);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
208
248
|
async createGroup(input: {
|
|
209
249
|
name: string;
|
|
210
250
|
displayName: string;
|
|
@@ -221,15 +261,16 @@ export class TeamEditing {
|
|
|
221
261
|
? "coordinator"
|
|
222
262
|
: (input.members[0] ?? ""));
|
|
223
263
|
store.checkNewGroup(input.name, input.members, host);
|
|
224
|
-
const topic = groupTopic(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
264
|
+
const topic = groupTopic({ ...fields, host, status: "active" }, store);
|
|
265
|
+
const channelId = await channels?.createChannel(input.name, topic, parent);
|
|
266
|
+
const group = await store.createGroup({
|
|
267
|
+
...fields,
|
|
268
|
+
host,
|
|
269
|
+
...(channelId ? { channelId } : {}),
|
|
270
|
+
});
|
|
230
271
|
logger.info({ group: group.name, members: group.members }, "group created");
|
|
231
272
|
this.#ctx.changed();
|
|
232
|
-
return `Created the group "${group.displayName}" in <#${channelId}
|
|
273
|
+
return `Created the group "${group.displayName}"${channelId ? ` in <#${channelId}>` : ""} with ${group.members.join(", ")}; host ${group.host}.`;
|
|
233
274
|
}
|
|
234
275
|
|
|
235
276
|
async updateGroup(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { channelKey, parseChannelKey } from "../contract/surface.ts";
|
|
2
2
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
3
|
+
import { AgentError } from "../domain/errors.ts";
|
|
3
4
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
4
5
|
import type { AgentDirectory } from "../services.ts";
|
|
5
6
|
import type { Agent, AgentGroup } from "./agent-store.ts";
|
|
@@ -20,34 +21,199 @@ export function discordIdOf(channel: ChannelKey): string | undefined {
|
|
|
20
21
|
/** The id part of a key the caller already knows is a Discord one, because the agent server owns it. */
|
|
21
22
|
export const channelIdOf = (channel: ChannelKey) => parseChannelKey(channel).id;
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
export const
|
|
25
|
-
`
|
|
24
|
+
/** The key of an agent's room on a surface that serves agent rooms. */
|
|
25
|
+
export const agentRoomKey = (surface: string, agent: string): ChannelKey =>
|
|
26
|
+
channelKey(surface, `agent.${agent}`);
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
+
/** The key of a group's room on a surface that serves agent rooms. */
|
|
29
|
+
export const groupRoomKey = (surface: string, group: string): ChannelKey =>
|
|
30
|
+
channelKey(surface, `group.${group}`);
|
|
31
|
+
|
|
32
|
+
const ROOM_ID = /^(agent|group)\.([a-z0-9-]{1,32})$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Which agent's or group's room a key is, when its surface is one of `roomSurfaces` and its id has
|
|
36
|
+
* the exact shape of a room; undefined for anything else, so no other key is ever taken for one.
|
|
37
|
+
* Discord never hosts rooms.
|
|
38
|
+
*/
|
|
39
|
+
export function roomOf(
|
|
40
|
+
key: ChannelKey,
|
|
41
|
+
roomSurfaces: readonly string[],
|
|
42
|
+
): { kind: "agent" | "group"; name: string } | undefined {
|
|
43
|
+
const at = typeof key === "string" ? key.indexOf(":") : -1;
|
|
44
|
+
if (at <= 0) return undefined;
|
|
45
|
+
const surface = key.slice(0, at);
|
|
46
|
+
if (surface === DISCORD_SURFACE || !roomSurfaces.includes(surface))
|
|
47
|
+
return undefined;
|
|
48
|
+
const match = ROOM_ID.exec(key.slice(at + 1));
|
|
49
|
+
if (!match) return undefined;
|
|
50
|
+
return { kind: match[1] as "agent" | "group", name: match[2] as string };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What the surfaces say of rooms: which of them host agent rooms, asked only for a key that is not a Discord one. */
|
|
54
|
+
interface RoomHosts {
|
|
55
|
+
roomSurfaces(): readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The agent whose Discord channel or room the key is; an archived one is returned too. */
|
|
59
|
+
export function agentOfChannel(
|
|
60
|
+
store: Pick<AgentDirectory, "agent" | "agentByChannel">,
|
|
61
|
+
hosts: RoomHosts | undefined,
|
|
62
|
+
channel: ChannelKey,
|
|
63
|
+
): Agent | undefined {
|
|
64
|
+
const id = discordIdOf(channel);
|
|
65
|
+
if (id !== undefined) return store.agentByChannel(id);
|
|
66
|
+
const room = roomOf(channel, hosts?.roomSurfaces() ?? []);
|
|
67
|
+
return room?.kind === "agent" ? store.agent(room.name) : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The group whose Discord channel or room the key is; an archived one is returned too. */
|
|
71
|
+
export function groupOfChannel(
|
|
72
|
+
store: Pick<AgentDirectory, "group" | "groupByChannel">,
|
|
73
|
+
hosts: RoomHosts | undefined,
|
|
74
|
+
channel: ChannelKey,
|
|
75
|
+
): AgentGroup | undefined {
|
|
76
|
+
const id = discordIdOf(channel);
|
|
77
|
+
if (id !== undefined) return store.groupByChannel(id);
|
|
78
|
+
const room = roomOf(channel, hosts?.roomSurfaces() ?? []);
|
|
79
|
+
return room?.kind === "group" ? store.group(room.name) : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The surface of a key that is not a Discord channel's; undefined for a Discord channel. */
|
|
83
|
+
export const roomSurfaceOf = (room: ChannelKey): string | undefined => {
|
|
84
|
+
const at = typeof room === "string" ? room.indexOf(":") : -1;
|
|
85
|
+
if (at <= 0) return undefined;
|
|
86
|
+
const surface = room.slice(0, at);
|
|
87
|
+
return surface === DISCORD_SURFACE ? undefined : surface;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* An agent's conversation inside a group, apart from its own. In a Discord group channel, or
|
|
92
|
+
* without a room, it is keyed by the channel; in a room of another surface, by the surface and the
|
|
93
|
+
* group's name, so the room's seats are conversations of their own.
|
|
94
|
+
*/
|
|
95
|
+
export function groupSessionKey(
|
|
96
|
+
group: AgentGroup,
|
|
97
|
+
agent: string,
|
|
98
|
+
room?: ChannelKey,
|
|
99
|
+
): ChannelKey {
|
|
100
|
+
const surface = room === undefined ? undefined : roomSurfaceOf(room);
|
|
101
|
+
if (surface !== undefined)
|
|
102
|
+
return `agentgroup:${surface}.group.${group.name}.${agent}`;
|
|
103
|
+
// A group of a team without Discord has rooms only, so it never has a seat of its own to key.
|
|
104
|
+
if (!group.channelId)
|
|
105
|
+
throw new AgentError(`the group ${group.name} has no Discord channel`);
|
|
106
|
+
return `agentgroup:${group.channelId}.${agent}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The name a group's messages are logged under: the group's own in its Discord channel, and
|
|
111
|
+
* `<group>@<surface>` in a room, so each conversation keeps its own history and cursors.
|
|
112
|
+
*/
|
|
113
|
+
export function groupLogName(group: AgentGroup, room: ChannelKey): string {
|
|
114
|
+
const surface = roomSurfaceOf(room);
|
|
115
|
+
return surface === undefined ? group.name : `${group.name}@${surface}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The coordinator: with Discord, the agent in the entry channel; without it, the agent the
|
|
120
|
+
* configuration names. Undefined while none is active.
|
|
121
|
+
*/
|
|
122
|
+
export function coordinatorOf(
|
|
123
|
+
store: Pick<AgentDirectory, "agent" | "agentByChannel">,
|
|
124
|
+
team: {
|
|
125
|
+
entryChannelId?: string;
|
|
126
|
+
coordinator?: string | (() => string | undefined);
|
|
127
|
+
},
|
|
128
|
+
): Agent | undefined {
|
|
129
|
+
if (team.entryChannelId !== undefined)
|
|
130
|
+
return store.agentByChannel(team.entryChannelId);
|
|
131
|
+
const name =
|
|
132
|
+
typeof team.coordinator === "function"
|
|
133
|
+
? team.coordinator()
|
|
134
|
+
: team.coordinator;
|
|
135
|
+
const named = name ? store.agent(name) : undefined;
|
|
136
|
+
return named?.status === "active" ? named : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** An agent's own channel conversation. */
|
|
28
140
|
export function homeScope(agent: Agent): AgentTurnScope {
|
|
29
141
|
const home = discordKey(agent.channelId ?? "");
|
|
30
142
|
return { name: agent.name, session: home, home };
|
|
31
143
|
}
|
|
32
144
|
|
|
33
|
-
/** An agent's conversation
|
|
34
|
-
export function
|
|
145
|
+
/** An agent's conversation in its room: the room is its session and its home. */
|
|
146
|
+
export function roomScope(agent: Agent, room: ChannelKey): AgentTurnScope {
|
|
147
|
+
return { name: agent.name, session: room, home: room };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* An agent's conversation inside a group: in a Discord group channel, or without a room, as
|
|
152
|
+
* before; in a room, the room's seat, whose home is the agent's own room on that surface.
|
|
153
|
+
*/
|
|
154
|
+
export function groupScope(
|
|
155
|
+
agent: Agent,
|
|
156
|
+
group: AgentGroup,
|
|
157
|
+
room?: ChannelKey,
|
|
158
|
+
): AgentTurnScope {
|
|
159
|
+
const surface = room === undefined ? undefined : roomSurfaceOf(room);
|
|
35
160
|
return {
|
|
36
161
|
name: agent.name,
|
|
37
|
-
session: groupSessionKey(group, agent.name),
|
|
38
|
-
home:
|
|
162
|
+
session: groupSessionKey(group, agent.name, room),
|
|
163
|
+
home:
|
|
164
|
+
surface === undefined
|
|
165
|
+
? discordKey(agent.channelId ?? "")
|
|
166
|
+
: agentRoomKey(surface, agent.name),
|
|
39
167
|
group: group.name,
|
|
40
168
|
};
|
|
41
169
|
}
|
|
42
170
|
|
|
43
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* The agent or group that owns a channel of the agent server: by Discord channel, or by room on
|
|
173
|
+
* one of `roomSurfaces`. An archived agent or group owns nothing.
|
|
174
|
+
*/
|
|
44
175
|
export function channelOwner(
|
|
45
|
-
store: Pick<AgentDirectory, "agentByChannel" | "groupByChannel"
|
|
176
|
+
store: Pick<AgentDirectory, "agentByChannel" | "groupByChannel"> &
|
|
177
|
+
Partial<Pick<AgentDirectory, "agent" | "group">>,
|
|
46
178
|
channel: ChannelKey,
|
|
179
|
+
roomSurfaces: readonly string[] = [],
|
|
47
180
|
): "agent" | "group" | undefined {
|
|
48
181
|
const id = discordIdOf(channel);
|
|
49
|
-
if (id === undefined)
|
|
182
|
+
if (id === undefined) {
|
|
183
|
+
const room = roomOf(channel, roomSurfaces);
|
|
184
|
+
if (!room) return undefined;
|
|
185
|
+
if (room.kind === "agent")
|
|
186
|
+
return store.agent?.(room.name)?.status === "active"
|
|
187
|
+
? "agent"
|
|
188
|
+
: undefined;
|
|
189
|
+
return store.group?.(room.name)?.status === "active" ? "group" : undefined;
|
|
190
|
+
}
|
|
50
191
|
if (store.agentByChannel(id)) return "agent";
|
|
51
192
|
if (store.groupByChannel(id)) return "group";
|
|
52
193
|
return undefined;
|
|
53
194
|
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Every key an agent or group is addressed by: its Discord channel, when it has one, then its
|
|
198
|
+
* room on each of `roomSurfaces`; none for a name the team does not know. Archived ones count,
|
|
199
|
+
* so what they left behind can be cleared.
|
|
200
|
+
*/
|
|
201
|
+
export function keysOf(
|
|
202
|
+
store: Pick<AgentDirectory, "agent" | "group">,
|
|
203
|
+
name: string,
|
|
204
|
+
roomSurfaces: readonly string[],
|
|
205
|
+
): ChannelKey[] {
|
|
206
|
+
const agent = store.agent(name);
|
|
207
|
+
if (agent)
|
|
208
|
+
return [
|
|
209
|
+
...(agent.channelId ? [discordKey(agent.channelId)] : []),
|
|
210
|
+
...roomSurfaces.map((surface) => agentRoomKey(surface, name)),
|
|
211
|
+
];
|
|
212
|
+
const group = store.group(name);
|
|
213
|
+
if (group)
|
|
214
|
+
return [
|
|
215
|
+
...(group.channelId ? [discordKey(group.channelId)] : []),
|
|
216
|
+
...roomSurfaces.map((surface) => groupRoomKey(surface, name)),
|
|
217
|
+
];
|
|
218
|
+
return [];
|
|
219
|
+
}
|