pi-roundtable 0.24.1 → 0.25.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 +17 -0
- package/docs/plugins.md +45 -1
- package/package.json +1 -1
- package/src/core/agents/agent-book.ts +155 -0
- package/src/core/agents/agent-ports.ts +5 -1
- package/src/core/agents/agent-schema.ts +3 -0
- package/src/core/agents/agent-statements.ts +179 -0
- package/src/core/agents/agent-store.ts +184 -191
- package/src/core/agents/agent-team-fixture.ts +21 -2
- package/src/core/agents/agent-team.ts +42 -36
- package/src/core/agents/agent-tools.ts +12 -6
- package/src/core/agents/team-editing.ts +150 -32
- package/src/core/agents/team-keys.ts +17 -0
- package/src/core/agents/team-lifecycle.ts +96 -23
- package/src/core/agents/team-options.ts +6 -3
- package/src/core/agents/team-selection.ts +28 -0
- package/src/core/builtin/agent-server.ts +29 -5
- package/src/core/builtin/prompts.ts +40 -0
- package/src/core/builtin/skills.ts +9 -5
- package/src/core/define-roundtable.ts +3 -0
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/cards.ts +1 -1
- package/src/core/modules/prompts/prompt-documents.ts +425 -0
- package/src/core/modules/prompts/prompt-history.ts +103 -0
- package/src/core/modules/skills/skill-contract.ts +52 -0
- package/src/core/modules/skills/skill-document-files.ts +66 -0
- package/src/core/modules/skills/skill-registry.ts +136 -8
- package/src/core/modules/skills/skill-rules.ts +94 -0
- package/src/core/modules/skills/skill-store.ts +88 -17
- package/src/core/modules/skills/skill-tools.ts +18 -10
- package/src/core/runtime/extensions/agent-prompt.ts +20 -5
- package/src/core/runtime/extensions/persona-prompt.ts +39 -0
- package/src/core/runtime/extensions/prompt-read.ts +22 -0
- package/src/core/runtime/session-factory.ts +51 -35
- package/src/core/services.ts +88 -33
- package/src/index.ts +13 -0
|
@@ -3,7 +3,12 @@ import type { CharacterSpec } from "../../characters/index.ts";
|
|
|
3
3
|
import type { Migration } from "../db/migrations.ts";
|
|
4
4
|
import { AgentError } from "../domain/errors.ts";
|
|
5
5
|
import type { ThinkingLevel } from "../models.ts";
|
|
6
|
+
import {
|
|
7
|
+
PromptHistory,
|
|
8
|
+
type PromptVersion,
|
|
9
|
+
} from "../modules/prompts/prompt-history.ts";
|
|
6
10
|
import type { AgentDirectory } from "../services.ts";
|
|
11
|
+
import { AgentBook } from "./agent-book.ts";
|
|
7
12
|
import {
|
|
8
13
|
type Agent,
|
|
9
14
|
type AgentGroup,
|
|
@@ -17,7 +22,6 @@ import {
|
|
|
17
22
|
checkPrompt,
|
|
18
23
|
checkThinking,
|
|
19
24
|
type GroupMessage,
|
|
20
|
-
MAX_GROUP_MEMBERS,
|
|
21
25
|
MIN_GROUP_MEMBERS,
|
|
22
26
|
} from "./agent-rules.ts";
|
|
23
27
|
import {
|
|
@@ -25,9 +29,9 @@ import {
|
|
|
25
29
|
agentMigrations,
|
|
26
30
|
agentsMigration,
|
|
27
31
|
type GroupRow,
|
|
28
|
-
toAgent,
|
|
29
32
|
toGroup,
|
|
30
33
|
} from "./agent-schema.ts";
|
|
34
|
+
import { AgentStatements } from "./agent-statements.ts";
|
|
31
35
|
import { GroupMessages } from "./group-messages.ts";
|
|
32
36
|
|
|
33
37
|
export {
|
|
@@ -55,22 +59,23 @@ import { isCheckDatabase } from "../db/check-database.ts";
|
|
|
55
59
|
* Agents, groups, and group messages, in the database. Agents and groups are also held
|
|
56
60
|
* in memory, because every inbound message asks and only this process changes them.
|
|
57
61
|
*/
|
|
58
|
-
export class PgAgentStore implements AgentDirectory {
|
|
62
|
+
export class PgAgentStore extends AgentBook implements AgentDirectory {
|
|
59
63
|
readonly #sql: SQL;
|
|
60
|
-
readonly #agents = new Map<string, Agent>();
|
|
61
|
-
/** Agents whose character column holds a value, readable or not. */
|
|
62
|
-
readonly #dressed = new Set<string>();
|
|
63
|
-
readonly #groups = new Map<string, AgentGroup>();
|
|
64
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. */
|
|
65
65
|
#queue: Promise<unknown> = Promise.resolve();
|
|
66
66
|
|
|
67
67
|
readonly #guild: string;
|
|
68
|
+
readonly #statements: AgentStatements;
|
|
68
69
|
readonly #messages: GroupMessages;
|
|
70
|
+
readonly #prompts: PromptHistory;
|
|
69
71
|
|
|
70
72
|
private constructor(sql: SQL, guildId: string) {
|
|
73
|
+
super();
|
|
71
74
|
this.#sql = sql;
|
|
72
75
|
this.#guild = guildId;
|
|
76
|
+
this.#statements = new AgentStatements(sql, guildId);
|
|
73
77
|
this.#messages = new GroupMessages(sql, guildId);
|
|
78
|
+
this.#prompts = new PromptHistory(sql, guildId);
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/** The store's tables; the host runs this before any store attaches. */
|
|
@@ -94,11 +99,11 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
94
99
|
.#sql`SELECT * FROM agents WHERE guild_id = ${this.#guild}`;
|
|
95
100
|
const groups: GroupRow[] = await this
|
|
96
101
|
.#sql`SELECT * FROM agent_groups WHERE guild_id = ${this.#guild}`;
|
|
97
|
-
this
|
|
98
|
-
this
|
|
99
|
-
this
|
|
100
|
-
for (const row of agents) this
|
|
101
|
-
for (const row of groups) this
|
|
102
|
+
this.agentMap.clear();
|
|
103
|
+
this.groupMap.clear();
|
|
104
|
+
this.dressed.clear();
|
|
105
|
+
for (const row of agents) this.keepAgentRow(row);
|
|
106
|
+
for (const row of groups) this.groupMap.set(row.name, toGroup(row));
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
/** Inserts seed agents that do not exist yet; existing rows, edited or not, are left alone. */
|
|
@@ -106,15 +111,14 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
106
111
|
return this.#serial(async () => {
|
|
107
112
|
const added: string[] = [];
|
|
108
113
|
for (const seed of seeds) {
|
|
109
|
-
const rows
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
RETURNING *`;
|
|
114
|
+
const rows = await this.#statements.insertSeed({
|
|
115
|
+
...seed,
|
|
116
|
+
avatarPrompt: seed.avatarPrompt ?? "",
|
|
117
|
+
channelId: seed.channelId ?? null,
|
|
118
|
+
});
|
|
115
119
|
// Only the inserted rows join the map: reloading it whole would drop a write that
|
|
116
120
|
// committed after its SELECT.
|
|
117
|
-
if (rows[0]) added.push(this
|
|
121
|
+
if (rows[0]) added.push(this.keepAgent(rows[0]).name);
|
|
118
122
|
}
|
|
119
123
|
return added;
|
|
120
124
|
});
|
|
@@ -126,66 +130,6 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
126
130
|
return run;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
agents(): Agent[] {
|
|
130
|
-
return [...this.#agents.values()];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
agent(name: string): Agent | undefined {
|
|
134
|
-
return this.#agents.get(name);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** The active agent with that name, or an error the caller can show. */
|
|
138
|
-
activeAgent(name: string): Agent {
|
|
139
|
-
const agent = this.#agents.get(name);
|
|
140
|
-
if (!agent)
|
|
141
|
-
throw new AgentError(
|
|
142
|
-
`There is no agent "${name}". Active agents: ${this.#activeNames()}.`,
|
|
143
|
-
);
|
|
144
|
-
if (agent.status !== "active")
|
|
145
|
-
throw new AgentError(
|
|
146
|
-
`Agent "${name}" is archived. Active agents: ${this.#activeNames()}.`,
|
|
147
|
-
);
|
|
148
|
-
return agent;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
agentByChannel(channelId: string): Agent | undefined {
|
|
152
|
-
for (const agent of this.#agents.values())
|
|
153
|
-
if (agent.channelId === channelId && agent.status === "active")
|
|
154
|
-
return agent;
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
groups(): AgentGroup[] {
|
|
159
|
-
return [...this.#groups.values()];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
group(name: string): AgentGroup | undefined {
|
|
163
|
-
return this.#groups.get(name);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
groupByChannel(channelId: string): AgentGroup | undefined {
|
|
167
|
-
for (const group of this.#groups.values())
|
|
168
|
-
if (group.channelId === channelId && group.status === "active")
|
|
169
|
-
return group;
|
|
170
|
-
return undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
#activeNames(): string {
|
|
174
|
-
return (
|
|
175
|
-
this.agents()
|
|
176
|
-
.flatMap((a) => (a.status === "active" ? [a.name] : []))
|
|
177
|
-
.join(", ") || "none"
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Names are shared between agents and groups, archived ones included. */
|
|
182
|
-
#checkFree(name: string): void {
|
|
183
|
-
if (this.#agents.has(name) || this.#groups.has(name))
|
|
184
|
-
throw new AgentError(
|
|
185
|
-
`The name "${name}" is taken, archived agents and groups included.`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
133
|
async createAgent(agent: {
|
|
190
134
|
name: string;
|
|
191
135
|
displayName: string;
|
|
@@ -203,23 +147,20 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
203
147
|
checkPrompt(agent.prompt);
|
|
204
148
|
const avatarPrompt = agent.avatarPrompt ?? "";
|
|
205
149
|
if (avatarPrompt !== "") checkAvatarPrompt(avatarPrompt);
|
|
206
|
-
this
|
|
207
|
-
const rows
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
150
|
+
this.checkFree(agent.name);
|
|
151
|
+
const rows = await this.#statements.insertAgent({
|
|
152
|
+
name: agent.name,
|
|
153
|
+
displayName: agent.displayName.trim(),
|
|
154
|
+
prompt: agent.prompt,
|
|
155
|
+
avatarPrompt,
|
|
156
|
+
channelId: agent.channelId ?? null,
|
|
157
|
+
avatarHash: agent.avatarHash ?? null,
|
|
158
|
+
character: agent.character ?? null,
|
|
159
|
+
});
|
|
160
|
+
return this.keepAgent(rows[0]);
|
|
214
161
|
});
|
|
215
162
|
}
|
|
216
163
|
|
|
217
|
-
/** A name that createAgent would accept, checked before its channel is made. */
|
|
218
|
-
checkNewName(name: string): void {
|
|
219
|
-
checkAgentName(name);
|
|
220
|
-
this.#checkFree(name);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
164
|
async updateAgent(
|
|
224
165
|
name: string,
|
|
225
166
|
change: {
|
|
@@ -233,6 +174,8 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
233
174
|
model?: string | null;
|
|
234
175
|
thinking?: ThinkingLevel | null;
|
|
235
176
|
},
|
|
177
|
+
/** Who made the change; a changed prompt is kept in the agent's prompt history under this name. */
|
|
178
|
+
by = "unknown",
|
|
236
179
|
): Promise<Agent> {
|
|
237
180
|
return this.#serial(async () => {
|
|
238
181
|
const current = this.activeAgent(name);
|
|
@@ -249,65 +192,112 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
249
192
|
change.thinking === undefined
|
|
250
193
|
? (current.thinking ?? null)
|
|
251
194
|
: change.thinking;
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
character
|
|
259
|
-
|
|
260
|
-
model
|
|
261
|
-
thinking
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
195
|
+
const row = {
|
|
196
|
+
displayName: change.displayName?.trim() ?? current.displayName,
|
|
197
|
+
prompt: change.prompt ?? current.prompt,
|
|
198
|
+
avatarPrompt: change.avatarPrompt ?? current.avatarPrompt,
|
|
199
|
+
avatarHash: change.avatarHash ?? current.avatarHash ?? null,
|
|
200
|
+
characterSet: change.character !== undefined,
|
|
201
|
+
character: change.character ?? null,
|
|
202
|
+
channelId: change.channelId ?? current.channelId ?? null,
|
|
203
|
+
model,
|
|
204
|
+
thinking,
|
|
205
|
+
};
|
|
206
|
+
const prompt = change.prompt;
|
|
207
|
+
// A new prompt and its history row commit together, so no change goes unrecorded.
|
|
208
|
+
const rows =
|
|
209
|
+
prompt !== undefined && prompt !== current.prompt
|
|
210
|
+
? await this.#sql.begin(async (tx) => {
|
|
211
|
+
const written = await new AgentStatements(
|
|
212
|
+
tx,
|
|
213
|
+
this.#guild,
|
|
214
|
+
).updateAgent(name, row);
|
|
215
|
+
await this.#prompts.record(
|
|
216
|
+
"agent",
|
|
217
|
+
name,
|
|
218
|
+
{ action: "set", text: prompt, previous: current.prompt, by },
|
|
219
|
+
tx,
|
|
220
|
+
);
|
|
221
|
+
return written;
|
|
222
|
+
})
|
|
223
|
+
: await this.#statements.updateAgent(name, row);
|
|
224
|
+
return this.keepAgent(rows[0]);
|
|
266
225
|
});
|
|
267
226
|
}
|
|
268
227
|
|
|
269
|
-
/**
|
|
228
|
+
/** The changes of an agent's prompt, newest first. */
|
|
229
|
+
promptHistory(name: string, limit?: number): Promise<PromptVersion[]> {
|
|
230
|
+
return this.#prompts.list("agent", name, limit);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Archives an agent and takes it out of every group; says which groups it left. A group this
|
|
235
|
+
* would leave with too few members is archived whole, its members kept, so it can be restored
|
|
236
|
+
* once they are all active again.
|
|
237
|
+
*/
|
|
270
238
|
async archiveAgent(name: string): Promise<AgentGroup[]> {
|
|
271
239
|
return this.#serial(async () => {
|
|
272
|
-
|
|
273
|
-
UPDATE agents SET status = 'archived', updated_at = now()
|
|
274
|
-
WHERE guild_id = ${this.#guild} AND name = ${name} RETURNING *`;
|
|
275
|
-
this.#keepAgent(rows[0]);
|
|
240
|
+
this.keepAgent((await this.#statements.archiveAgent(name))[0]);
|
|
276
241
|
const left: AgentGroup[] = [];
|
|
277
242
|
for (const group of this.groups()) {
|
|
278
243
|
if (group.status !== "active" || !group.members.includes(name))
|
|
279
244
|
continue;
|
|
280
245
|
const members = group.members.filter((m) => m !== name);
|
|
246
|
+
if (members.length < MIN_GROUP_MEMBERS) {
|
|
247
|
+
left.push(await this.#writeGroup(group.name, { status: "archived" }));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
281
250
|
const host = group.host === name ? (members[0] ?? name) : group.host;
|
|
282
|
-
left.push(
|
|
283
|
-
await this.#writeGroup(group.name, {
|
|
284
|
-
members,
|
|
285
|
-
host,
|
|
286
|
-
status: members.length < MIN_GROUP_MEMBERS ? "archived" : "active",
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
251
|
+
left.push(await this.#writeGroup(group.name, { members, host }));
|
|
289
252
|
}
|
|
290
253
|
return left;
|
|
291
254
|
});
|
|
292
255
|
}
|
|
293
256
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Returns an archived agent to the active ones, on a new channel when its old one is gone. It
|
|
259
|
+
* does not rejoin the groups it left.
|
|
260
|
+
*/
|
|
261
|
+
async restoreAgent(name: string, channelId?: string): Promise<Agent> {
|
|
262
|
+
return this.#serial(async () => {
|
|
263
|
+
const current = this.agentMap.get(name);
|
|
264
|
+
if (!current) throw new AgentError(`There is no agent "${name}".`);
|
|
265
|
+
if (current.status === "active")
|
|
266
|
+
throw new AgentError(`"${name}" is not archived.`);
|
|
267
|
+
const rows = await this.#statements.restoreAgent(
|
|
268
|
+
name,
|
|
269
|
+
channelId ?? current.channelId ?? null,
|
|
270
|
+
);
|
|
271
|
+
return this.keepAgent(rows[0]);
|
|
272
|
+
});
|
|
306
273
|
}
|
|
307
274
|
|
|
308
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Deletes an archived agent, its group cursors and its prompt history, and takes it out of the
|
|
277
|
+
* archived groups that still list it; its name is free again, and a new agent of that name
|
|
278
|
+
* starts with an empty history. An active agent is never deleted: archive it first.
|
|
279
|
+
*/
|
|
280
|
+
async deleteAgent(name: string): Promise<void> {
|
|
281
|
+
return this.#serial(async () => {
|
|
282
|
+
const current = this.agentMap.get(name);
|
|
283
|
+
if (!current) throw new AgentError(`There is no agent "${name}".`);
|
|
284
|
+
if (current.status === "active")
|
|
285
|
+
throw new AgentError(
|
|
286
|
+
`"${name}" is active; archive it before deleting it.`,
|
|
287
|
+
);
|
|
288
|
+
await this.#statements.deleteAgent(name);
|
|
289
|
+
this.agentMap.delete(name);
|
|
290
|
+
this.dressed.delete(name);
|
|
291
|
+
// An archived group still lists the agent; a new agent of this name must not join it unasked.
|
|
292
|
+
for (const group of this.groups()) {
|
|
293
|
+
if (!group.members.includes(name)) continue;
|
|
294
|
+
const members = group.members.filter((m) => m !== name);
|
|
295
|
+
await this.#writeGroup(group.name, {
|
|
296
|
+
members,
|
|
297
|
+
host: group.host === name ? (members[0] ?? "") : group.host,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
311
301
|
}
|
|
312
302
|
|
|
313
303
|
/**
|
|
@@ -320,12 +310,13 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
320
310
|
avatarHash: string,
|
|
321
311
|
): Promise<boolean> {
|
|
322
312
|
return this.#serial(async () => {
|
|
323
|
-
const rows
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
313
|
+
const rows = await this.#statements.dressIfUndressed(
|
|
314
|
+
name,
|
|
315
|
+
character,
|
|
316
|
+
avatarHash,
|
|
317
|
+
);
|
|
327
318
|
if (!rows[0]) return false;
|
|
328
|
-
this
|
|
319
|
+
this.keepAgentRow(rows[0]);
|
|
329
320
|
return true;
|
|
330
321
|
});
|
|
331
322
|
}
|
|
@@ -341,38 +332,17 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
341
332
|
avatarHash: string,
|
|
342
333
|
): Promise<boolean> {
|
|
343
334
|
return this.#serial(async () => {
|
|
344
|
-
const rows
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
335
|
+
const rows = await this.#statements.setAvatarIfLook(
|
|
336
|
+
name,
|
|
337
|
+
character,
|
|
338
|
+
avatarHash,
|
|
339
|
+
);
|
|
349
340
|
if (!rows[0]) return false;
|
|
350
|
-
this
|
|
341
|
+
this.keepAgentRow(rows[0]);
|
|
351
342
|
return true;
|
|
352
343
|
});
|
|
353
344
|
}
|
|
354
345
|
|
|
355
|
-
#checkMembers(members: readonly string[], host: string): void {
|
|
356
|
-
if (
|
|
357
|
-
new Set(members).size !== members.length ||
|
|
358
|
-
members.length < MIN_GROUP_MEMBERS ||
|
|
359
|
-
members.length > MAX_GROUP_MEMBERS
|
|
360
|
-
)
|
|
361
|
-
throw new AgentError(
|
|
362
|
-
`A group has ${MIN_GROUP_MEMBERS} to ${MAX_GROUP_MEMBERS} different members.`,
|
|
363
|
-
);
|
|
364
|
-
for (const member of members) this.activeAgent(member);
|
|
365
|
-
if (!members.includes(host))
|
|
366
|
-
throw new AgentError(`The host "${host}" must be one of the members.`);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Checks a new group before its channel is made. */
|
|
370
|
-
checkNewGroup(name: string, members: readonly string[], host: string): void {
|
|
371
|
-
checkAgentName(name);
|
|
372
|
-
this.#checkFree(name);
|
|
373
|
-
this.#checkMembers(members, host);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
346
|
async createGroup(group: {
|
|
377
347
|
name: string;
|
|
378
348
|
displayName: string;
|
|
@@ -384,12 +354,12 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
384
354
|
return this.#serial(async () => {
|
|
385
355
|
this.checkNewGroup(group.name, group.members, group.host);
|
|
386
356
|
checkDisplayName(group.displayName);
|
|
387
|
-
const rows
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return this
|
|
357
|
+
const rows = await this.#statements.insertGroup({
|
|
358
|
+
...group,
|
|
359
|
+
displayName: group.displayName.trim(),
|
|
360
|
+
channelId: group.channelId ?? null,
|
|
361
|
+
});
|
|
362
|
+
return this.keepGroup(rows[0]);
|
|
393
363
|
});
|
|
394
364
|
}
|
|
395
365
|
|
|
@@ -398,7 +368,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
398
368
|
change: { displayName?: string; members?: string[]; host?: string },
|
|
399
369
|
): Promise<AgentGroup> {
|
|
400
370
|
return this.#serial(async () => {
|
|
401
|
-
const current = this
|
|
371
|
+
const current = this.groupMap.get(name);
|
|
402
372
|
if (current?.status !== "active")
|
|
403
373
|
throw new AgentError(`There is no active group "${name}".`);
|
|
404
374
|
if (change.displayName !== undefined)
|
|
@@ -407,7 +377,7 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
407
377
|
const host =
|
|
408
378
|
change.host ??
|
|
409
379
|
(members.includes(current.host) ? current.host : (members[0] ?? ""));
|
|
410
|
-
this
|
|
380
|
+
this.checkMembers(members, host);
|
|
411
381
|
return this.#writeGroup(name, {
|
|
412
382
|
displayName: change.displayName?.trim() ?? current.displayName,
|
|
413
383
|
members,
|
|
@@ -421,6 +391,38 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
421
391
|
return this.#serial(() => this.#writeGroup(name, { status: "archived" }));
|
|
422
392
|
}
|
|
423
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Returns an archived group to the active ones, on a new channel when its old one is gone. Its
|
|
396
|
+
* members must all be active still and number enough; otherwise it refuses and stays archived.
|
|
397
|
+
*/
|
|
398
|
+
async restoreGroup(name: string, channelId?: string): Promise<AgentGroup> {
|
|
399
|
+
return this.#serial(async () => {
|
|
400
|
+
const current = this.checkRestorableGroup(name);
|
|
401
|
+
const rows = await this.#statements.restoreGroup(
|
|
402
|
+
name,
|
|
403
|
+
channelId ?? current.channelId ?? null,
|
|
404
|
+
);
|
|
405
|
+
return this.keepGroup(rows[0]);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Deletes an archived group with its messages and cursors, in the Discord channel's log and in
|
|
411
|
+
* each room's (`<group>@<surface>`); its name is free again. An active group is never deleted.
|
|
412
|
+
*/
|
|
413
|
+
async deleteGroup(name: string): Promise<void> {
|
|
414
|
+
return this.#serial(async () => {
|
|
415
|
+
const current = this.groupMap.get(name);
|
|
416
|
+
if (!current) throw new AgentError(`There is no group "${name}".`);
|
|
417
|
+
if (current.status === "active")
|
|
418
|
+
throw new AgentError(
|
|
419
|
+
`"${name}" is active; archive it before deleting it.`,
|
|
420
|
+
);
|
|
421
|
+
await this.#statements.deleteGroup(name);
|
|
422
|
+
this.groupMap.delete(name);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
424
426
|
async #writeGroup(
|
|
425
427
|
name: string,
|
|
426
428
|
change: {
|
|
@@ -430,24 +432,15 @@ export class PgAgentStore implements AgentDirectory {
|
|
|
430
432
|
status?: AgentStatus;
|
|
431
433
|
},
|
|
432
434
|
): Promise<AgentGroup> {
|
|
433
|
-
const current = this
|
|
435
|
+
const current = this.groupMap.get(name);
|
|
434
436
|
if (!current) throw new AgentError(`There is no group "${name}".`);
|
|
435
|
-
const rows
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
RETURNING *`;
|
|
443
|
-
return this.#keepGroup(rows[0]);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
#keepGroup(row: GroupRow | undefined): AgentGroup {
|
|
447
|
-
if (!row) throw new AgentError("the group was not found");
|
|
448
|
-
const group = toGroup(row);
|
|
449
|
-
this.#groups.set(group.name, group);
|
|
450
|
-
return group;
|
|
437
|
+
const rows = await this.#statements.updateGroup(name, {
|
|
438
|
+
displayName: change.displayName ?? current.displayName,
|
|
439
|
+
members: change.members ?? current.members,
|
|
440
|
+
host: change.host ?? current.host,
|
|
441
|
+
status: change.status ?? current.status,
|
|
442
|
+
});
|
|
443
|
+
return this.keepGroup(rows[0]);
|
|
451
444
|
}
|
|
452
445
|
|
|
453
446
|
appendGroupMessage(
|
|
@@ -203,6 +203,10 @@ export class FakeRuntime implements AgentTurnRunner {
|
|
|
203
203
|
async startFresh(session: ChannelKey) {
|
|
204
204
|
this.fresh.push(session);
|
|
205
205
|
}
|
|
206
|
+
deleted: ChannelKey[] = [];
|
|
207
|
+
async deleteConversation(session: ChannelKey) {
|
|
208
|
+
this.deleted.push(session);
|
|
209
|
+
}
|
|
206
210
|
usage = new Map<ChannelKey, ContextUse>();
|
|
207
211
|
contextUsage(session: ChannelKey) {
|
|
208
212
|
return this.usage.get(session);
|
|
@@ -244,7 +248,17 @@ export const script: {
|
|
|
244
248
|
prompt: PromptSection[];
|
|
245
249
|
/** The tools plugins defined for agents. */
|
|
246
250
|
tools: string[];
|
|
247
|
-
|
|
251
|
+
/** The prompt every agent's starts with; read before each turn, so a test may change it. */
|
|
252
|
+
shared: string;
|
|
253
|
+
/** The one a speaker other than the owner gets instead; empty falls back to `shared`. */
|
|
254
|
+
guest: string;
|
|
255
|
+
} = {
|
|
256
|
+
scores: undefined,
|
|
257
|
+
prompt: [],
|
|
258
|
+
tools: [],
|
|
259
|
+
shared: "Shared rules.",
|
|
260
|
+
guest: "",
|
|
261
|
+
};
|
|
248
262
|
let queue: ChannelQueue;
|
|
249
263
|
let team: AgentTeam;
|
|
250
264
|
let skillStore: TestStore<SkillStore>;
|
|
@@ -323,7 +337,9 @@ export function useTeamFixture(
|
|
|
323
337
|
"agent_groups",
|
|
324
338
|
"agent_group_messages",
|
|
325
339
|
"agent_group_cursors",
|
|
340
|
+
"prompt_history",
|
|
326
341
|
"skills",
|
|
342
|
+
"skill_versions",
|
|
327
343
|
"skill_groups",
|
|
328
344
|
"agent_skills",
|
|
329
345
|
])
|
|
@@ -361,6 +377,8 @@ export function useTeamFixture(
|
|
|
361
377
|
script.scores = undefined;
|
|
362
378
|
script.prompt = [];
|
|
363
379
|
script.tools = [];
|
|
380
|
+
script.shared = "Shared rules.";
|
|
381
|
+
script.guest = "";
|
|
364
382
|
queue = channelQueue();
|
|
365
383
|
removedSchedules.length = 0;
|
|
366
384
|
stops.length = 0;
|
|
@@ -420,7 +438,8 @@ export function useTeamFixture(
|
|
|
420
438
|
return () => stops.push(`hide ${channel}`);
|
|
421
439
|
},
|
|
422
440
|
workDir: "/tmp/agents/work",
|
|
423
|
-
sharedPrompt:
|
|
441
|
+
sharedPrompt: () => script.shared,
|
|
442
|
+
guestPrompt: () => script.guest,
|
|
424
443
|
skills,
|
|
425
444
|
logger: silentLogger(),
|
|
426
445
|
});
|