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
|
@@ -5,7 +5,7 @@ import type { ChannelKey, TurnResult } from "../domain/conversation.ts";
|
|
|
5
5
|
import { AgentError, ScheduleError } from "../domain/errors.ts";
|
|
6
6
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
7
7
|
import type { ThinkingSetting } from "../models.ts";
|
|
8
|
-
import {
|
|
8
|
+
import type { PromptVersion } from "../modules/prompts/prompt-history.ts";
|
|
9
9
|
import type { ResolvedSkill } from "../modules/skills/skill-rules.ts";
|
|
10
10
|
import type { AgentChange, AgentTeam } from "../services.ts";
|
|
11
11
|
import type { TurnSelection } from "../sessions.ts";
|
|
@@ -13,11 +13,9 @@ import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
|
13
13
|
import { agentSystemPrompt } from "./agent-prompt.ts";
|
|
14
14
|
import type { Agent } from "./agent-store.ts";
|
|
15
15
|
import {
|
|
16
|
-
AGENT_TOOLS,
|
|
17
16
|
type AgentOps,
|
|
18
17
|
type AvatarMode,
|
|
19
18
|
agentToolsExtension,
|
|
20
|
-
MESSAGE_AGENT_TOOL,
|
|
21
19
|
} from "./agent-tools.ts";
|
|
22
20
|
import { TeamEditing } from "./team-editing.ts";
|
|
23
21
|
import {
|
|
@@ -36,6 +34,7 @@ import {
|
|
|
36
34
|
} from "./team-layout.ts";
|
|
37
35
|
import { TeamLifecycle } from "./team-lifecycle.ts";
|
|
38
36
|
import type { AgentTeamOptions, TeamContext } from "./team-options.ts";
|
|
37
|
+
import { teamSelection } from "./team-selection.ts";
|
|
39
38
|
import { type TeamStatus, teamStatus } from "./team-status.ts";
|
|
40
39
|
import {
|
|
41
40
|
agentDetails,
|
|
@@ -137,22 +136,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
137
136
|
|
|
138
137
|
/** The tools every agent turn runs with: the plugins', plus the shell and agent tools. */
|
|
139
138
|
selection(scope?: AgentTurnScope): TurnSelection {
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
id: "agent",
|
|
143
|
-
tools: [
|
|
144
|
-
...new Set([
|
|
145
|
-
...plugged.tools,
|
|
146
|
-
...SHELL_TOOLS,
|
|
147
|
-
// Group members hand off by mentioning each other, so a group seat has no message_agent.
|
|
148
|
-
...AGENT_TOOLS.filter(
|
|
149
|
-
(tool) => !(scope?.group && tool === MESSAGE_AGENT_TOOL),
|
|
150
|
-
),
|
|
151
|
-
...(this.#options.pluginTools?.() ?? []),
|
|
152
|
-
]),
|
|
153
|
-
],
|
|
154
|
-
groups: plugged.groups,
|
|
155
|
-
};
|
|
139
|
+
return teamSelection(this.#options, scope);
|
|
156
140
|
}
|
|
157
141
|
|
|
158
142
|
/** The system prompt addition of an agent session, read before each of its runs. */
|
|
@@ -168,6 +152,10 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
168
152
|
} = this.#options;
|
|
169
153
|
const agent = store.agent(scope.name);
|
|
170
154
|
if (!agent) throw new AgentError(`no agent ${scope.name}`);
|
|
155
|
+
const shared =
|
|
156
|
+
typeof sharedPrompt === "function" ? sharedPrompt() : sharedPrompt;
|
|
157
|
+
const guest =
|
|
158
|
+
typeof guestPrompt === "function" ? guestPrompt() : guestPrompt;
|
|
171
159
|
const group = scope.group ? store.group(scope.group) : undefined;
|
|
172
160
|
const coordinator = coordinatorOf(store, this.#options);
|
|
173
161
|
const speaker = this.#turns.speakerOf(scope);
|
|
@@ -175,8 +163,8 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
175
163
|
agent,
|
|
176
164
|
...(speaker ? { speaker } : {}),
|
|
177
165
|
...(coordinator ? { coordinator } : {}),
|
|
178
|
-
shared
|
|
179
|
-
...(
|
|
166
|
+
shared,
|
|
167
|
+
...(guest ? { guestShared: guest } : {}),
|
|
180
168
|
workDir,
|
|
181
169
|
owner,
|
|
182
170
|
shellUser,
|
|
@@ -292,14 +280,21 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
292
280
|
return this.#lifecycle.channelDeleted(channelId);
|
|
293
281
|
}
|
|
294
282
|
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
* where no one owns it and so nothing answers.
|
|
298
|
-
*/
|
|
299
|
-
archive(caller: AgentTurnScope, name: string): Promise<string> {
|
|
283
|
+
/** Archives an agent or group like a deleted channel, keeping its channel under Archive. */
|
|
284
|
+
archive(caller: AgentTurnScope | undefined, name: string): Promise<string> {
|
|
300
285
|
return this.#lifecycle.archive(caller, name);
|
|
301
286
|
}
|
|
302
287
|
|
|
288
|
+
/** Returns an archived agent or group to the active ones; see `AgentTeam.restore`. */
|
|
289
|
+
restore(name: string): Promise<string> {
|
|
290
|
+
return this.#lifecycle.restore(name);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Deletes an archived agent or group for good; see `AgentTeam.remove`. */
|
|
294
|
+
remove(name: string, cleanup?: () => Promise<void>): Promise<string> {
|
|
295
|
+
return this.#lifecycle.remove(name, cleanup);
|
|
296
|
+
}
|
|
297
|
+
|
|
303
298
|
// ── Turns, run by TeamTurns ────────────────────────────────────────────
|
|
304
299
|
|
|
305
300
|
/**
|
|
@@ -442,8 +437,15 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
442
437
|
* Changes an agent's display name, prompt, model, or thinking level; `default` returns the
|
|
443
438
|
* model or thinking level to the assistant's. A model must be one the host runs.
|
|
444
439
|
*/
|
|
445
|
-
update(name: string, change: AgentChange): Promise<string> {
|
|
446
|
-
return this.#editing.update(name, change);
|
|
440
|
+
update(name: string, change: AgentChange, by?: string): Promise<string> {
|
|
441
|
+
return this.#editing.update(name, change, by);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The changes of an agent's prompt, newest first. */
|
|
445
|
+
async promptHistory(name: string, limit?: number): Promise<PromptVersion[]> {
|
|
446
|
+
if (!this.#options.store.agent(name))
|
|
447
|
+
throw new AgentError(`There is no agent "${name}".`);
|
|
448
|
+
return this.#options.store.promptHistory(name, limit);
|
|
447
449
|
}
|
|
448
450
|
|
|
449
451
|
create(
|
|
@@ -453,6 +455,14 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
453
455
|
return this.#editing.create(caller, input);
|
|
454
456
|
}
|
|
455
457
|
|
|
458
|
+
/** An owner creates an agent outside any agent turn; see `AgentTeam.createAgent`. */
|
|
459
|
+
createAgent(
|
|
460
|
+
by: Speaker,
|
|
461
|
+
input: Parameters<TeamEditing["createFor"]>[1],
|
|
462
|
+
): Promise<Agent> {
|
|
463
|
+
return this.#editing.createFor(by, input);
|
|
464
|
+
}
|
|
465
|
+
|
|
456
466
|
/** Changes an agent's look; see `AgentTeam.dress`. */
|
|
457
467
|
dress(name: string, input: CharacterInput): Promise<Agent> {
|
|
458
468
|
return this.#editing.dress(name, input);
|
|
@@ -468,19 +478,15 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
468
478
|
return this.#editing.redrawAvatar(name, mode, text);
|
|
469
479
|
}
|
|
470
480
|
|
|
471
|
-
createGroup(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
members: string[];
|
|
475
|
-
host?: string;
|
|
476
|
-
category?: string;
|
|
477
|
-
}): Promise<string> {
|
|
481
|
+
createGroup(
|
|
482
|
+
input: Parameters<TeamEditing["createGroup"]>[0],
|
|
483
|
+
): Promise<string> {
|
|
478
484
|
return this.#editing.createGroup(input);
|
|
479
485
|
}
|
|
480
486
|
|
|
481
487
|
updateGroup(
|
|
482
488
|
name: string,
|
|
483
|
-
change:
|
|
489
|
+
change: Parameters<TeamEditing["updateGroup"]>[1],
|
|
484
490
|
): Promise<string> {
|
|
485
491
|
return this.#editing.updateGroup(name, change);
|
|
486
492
|
}
|
|
@@ -65,6 +65,8 @@ export interface AgentOps {
|
|
|
65
65
|
model?: string;
|
|
66
66
|
thinking?: string;
|
|
67
67
|
},
|
|
68
|
+
/** Who changes it: the agent whose turn asks, for the prompt history. */
|
|
69
|
+
by?: string,
|
|
68
70
|
): Promise<string>;
|
|
69
71
|
create(
|
|
70
72
|
caller: AgentTurnScope,
|
|
@@ -245,12 +247,16 @@ function toolDefs(
|
|
|
245
247
|
const prompt = optionalString(i, "prompt");
|
|
246
248
|
const model = optionalString(i, "model");
|
|
247
249
|
const thinking = optionalString(i, "thinking");
|
|
248
|
-
return ops.update(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
return ops.update(
|
|
251
|
+
requiredString(i, "name"),
|
|
252
|
+
{
|
|
253
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
254
|
+
...(prompt !== undefined ? { prompt } : {}),
|
|
255
|
+
...(model !== undefined ? { model } : {}),
|
|
256
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
257
|
+
},
|
|
258
|
+
scope.name,
|
|
259
|
+
);
|
|
254
260
|
},
|
|
255
261
|
},
|
|
256
262
|
{
|
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
import { AgentError } from "../domain/errors.ts";
|
|
9
9
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
10
10
|
import type { AgentChange } from "../services.ts";
|
|
11
|
-
import { addressee } from "../speakers.ts";
|
|
11
|
+
import { addressee, type Speaker } from "../speakers.ts";
|
|
12
12
|
import { openingTaskText } from "./agent-prompt.ts";
|
|
13
13
|
import { characterOf, overBase } from "./agent-rules.ts";
|
|
14
14
|
import { modelSetting, thinkingSetting } from "./agent-settings.ts";
|
|
15
15
|
import type { Agent } from "./agent-store.ts";
|
|
16
16
|
import type { AvatarMode } from "./agent-tools.ts";
|
|
17
|
-
import { discordKey, keysOf } from "./team-keys.ts";
|
|
17
|
+
import { agentRoomKey, discordKey, keysOf, seatsOf } from "./team-keys.ts";
|
|
18
18
|
import { groupTopic, teamCategory } from "./team-layout.ts";
|
|
19
19
|
import type { TeamContext } from "./team-options.ts";
|
|
20
20
|
|
|
@@ -34,7 +34,11 @@ export class TeamEditing {
|
|
|
34
34
|
* Changes an agent's display name, prompt, model, or thinking level; `default` returns the
|
|
35
35
|
* model or thinking level to the assistant's. A model must be one the host runs.
|
|
36
36
|
*/
|
|
37
|
-
async update(
|
|
37
|
+
async update(
|
|
38
|
+
name: string,
|
|
39
|
+
change: AgentChange,
|
|
40
|
+
by?: string,
|
|
41
|
+
): Promise<string> {
|
|
38
42
|
const { store, logger } = this.#ctx.options;
|
|
39
43
|
if (Object.values(change).every((value) => value === undefined))
|
|
40
44
|
throw new AgentError(
|
|
@@ -44,14 +48,18 @@ export class TeamEditing {
|
|
|
44
48
|
this.#ctx.options.models.usable(),
|
|
45
49
|
);
|
|
46
50
|
const thinking = thinkingSetting(change.thinking);
|
|
47
|
-
const agent = await store.updateAgent(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const agent = await store.updateAgent(
|
|
52
|
+
name,
|
|
53
|
+
{
|
|
54
|
+
...(change.displayName !== undefined
|
|
55
|
+
? { displayName: change.displayName }
|
|
56
|
+
: {}),
|
|
57
|
+
...(change.prompt !== undefined ? { prompt: change.prompt } : {}),
|
|
58
|
+
...(model !== undefined ? { model } : {}),
|
|
59
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
60
|
+
},
|
|
61
|
+
by,
|
|
62
|
+
);
|
|
55
63
|
logger.info(
|
|
56
64
|
{
|
|
57
65
|
agent: name,
|
|
@@ -72,6 +80,7 @@ export class TeamEditing {
|
|
|
72
80
|
return `Updated "${agent.displayName}" (${agent.name}); it applies from its next turn. It runs ${runs} with thinking ${level}.`;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/** The agent tool: an agent creates another, which starts on the opening task at once. */
|
|
75
84
|
async create(
|
|
76
85
|
caller: AgentTurnScope,
|
|
77
86
|
input: {
|
|
@@ -87,27 +96,106 @@ export class TeamEditing {
|
|
|
87
96
|
skills?: string[];
|
|
88
97
|
},
|
|
89
98
|
): Promise<string> {
|
|
90
|
-
|
|
91
|
-
this.#ctx.options;
|
|
92
|
-
if (input.skills && input.skills.length > 0 && !skills)
|
|
93
|
-
throw new AgentError(
|
|
94
|
-
"Skills are off on this host, so an agent cannot carry any. Create it without skills.",
|
|
95
|
-
);
|
|
99
|
+
this.#refuseSkillsOff(input.skills);
|
|
96
100
|
const speaker = this.#ctx.turns.speakerOf(caller);
|
|
97
101
|
if (!speaker)
|
|
98
102
|
throw new AgentError(
|
|
99
103
|
"agent_create can only be used during an agent turn.",
|
|
100
104
|
);
|
|
105
|
+
const made = await this.#create(
|
|
106
|
+
{
|
|
107
|
+
speaker,
|
|
108
|
+
by: caller.name,
|
|
109
|
+
creator: this.#ctx.options.store.agent(caller.name),
|
|
110
|
+
},
|
|
111
|
+
input,
|
|
112
|
+
);
|
|
113
|
+
return made.text;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* An owner creates an agent from outside any agent turn (the web app's form; any other tier is
|
|
118
|
+
* refused, since the opening task runs as `speaker`): the same checks,
|
|
119
|
+
* picture, channel and opening task as `create`, and the model and thinking level the owner
|
|
120
|
+
* picked. Without a task the agent only exists, waiting in its room. Returns the new agent.
|
|
121
|
+
*/
|
|
122
|
+
async createFor(
|
|
123
|
+
speaker: Speaker,
|
|
124
|
+
input: {
|
|
125
|
+
name: string;
|
|
126
|
+
displayName: string;
|
|
127
|
+
prompt: string;
|
|
128
|
+
character?: CharacterInput;
|
|
129
|
+
task?: string;
|
|
130
|
+
openIn?: string;
|
|
131
|
+
category?: string;
|
|
132
|
+
skills?: string[];
|
|
133
|
+
model?: string;
|
|
134
|
+
thinking?: string;
|
|
135
|
+
},
|
|
136
|
+
): Promise<Agent> {
|
|
137
|
+
if (speaker.tier !== "owner")
|
|
138
|
+
throw new AgentError("Only an owner may create an agent here.");
|
|
139
|
+
const { model, thinking, task, openIn, ...rest } = input;
|
|
140
|
+
const made = await this.#create(
|
|
141
|
+
{ speaker, by: speaker.name, creator: undefined, openIn },
|
|
142
|
+
{ ...rest, ...(task ? { task } : {}) },
|
|
143
|
+
{ model, thinking },
|
|
144
|
+
);
|
|
145
|
+
return made.agent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** An agent cannot carry skills on a host with skills switched off. */
|
|
149
|
+
#refuseSkillsOff(carried: string[] | undefined): void {
|
|
150
|
+
if (carried && carried.length > 0 && !this.#ctx.options.skills)
|
|
151
|
+
throw new AgentError(
|
|
152
|
+
"Skills are off on this host, so an agent cannot carry any. Create it without skills.",
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async #create(
|
|
157
|
+
who: {
|
|
158
|
+
speaker: Speaker;
|
|
159
|
+
by: string;
|
|
160
|
+
creator: Agent | undefined;
|
|
161
|
+
/** The surface whose room runs the opening task. */
|
|
162
|
+
openIn?: string | undefined;
|
|
163
|
+
},
|
|
164
|
+
input: {
|
|
165
|
+
name: string;
|
|
166
|
+
displayName: string;
|
|
167
|
+
prompt: string;
|
|
168
|
+
/** @deprecated Ignored: agents are composed characters. */
|
|
169
|
+
avatarPrompt?: string;
|
|
170
|
+
character?: CharacterInput;
|
|
171
|
+
task?: string;
|
|
172
|
+
category?: string;
|
|
173
|
+
skills?: string[];
|
|
174
|
+
},
|
|
175
|
+
settings: {
|
|
176
|
+
model?: string | undefined;
|
|
177
|
+
thinking?: string | undefined;
|
|
178
|
+
} = {},
|
|
179
|
+
): Promise<{ agent: Agent; text: string }> {
|
|
180
|
+
const { store, studio, channels, queue, skills, surfaces, logger } =
|
|
181
|
+
this.#ctx.options;
|
|
182
|
+
this.#refuseSkillsOff(input.skills);
|
|
101
183
|
const {
|
|
102
184
|
category,
|
|
103
185
|
skills: carried = [],
|
|
104
186
|
character: wanted,
|
|
105
187
|
avatarPrompt: _ignored,
|
|
188
|
+
task,
|
|
106
189
|
...fields
|
|
107
190
|
} = input;
|
|
108
191
|
const parent = teamCategory(category, "Agents");
|
|
109
192
|
store.checkNewName(input.name);
|
|
110
193
|
skills?.checkRegistered(carried);
|
|
194
|
+
// A model or thinking level the host cannot run fails the create, before anything is made.
|
|
195
|
+
const model = await modelSetting(settings.model, () =>
|
|
196
|
+
this.#ctx.options.models.usable(),
|
|
197
|
+
);
|
|
198
|
+
const thinking = thinkingSetting(settings.thinking);
|
|
111
199
|
// The picture comes before the row and the channel: a render failure fails the create.
|
|
112
200
|
const taken = store
|
|
113
201
|
.agents()
|
|
@@ -123,49 +211,69 @@ export class TeamEditing {
|
|
|
123
211
|
input.displayName,
|
|
124
212
|
parent,
|
|
125
213
|
);
|
|
126
|
-
|
|
214
|
+
let agent = await store.createAgent({
|
|
127
215
|
...fields,
|
|
128
216
|
character,
|
|
129
217
|
avatarHash,
|
|
130
218
|
...(channelId ? { channelId } : {}),
|
|
131
219
|
});
|
|
220
|
+
if (model || thinking)
|
|
221
|
+
agent = await store.updateAgent(agent.name, {
|
|
222
|
+
...(model ? { model } : {}),
|
|
223
|
+
...(thinking ? { thinking } : {}),
|
|
224
|
+
});
|
|
132
225
|
if (skills && carried.length > 0)
|
|
133
226
|
await skills.attach(agent.name, carried, []);
|
|
134
227
|
logger.info(
|
|
135
|
-
{ agent: agent.name, by:
|
|
228
|
+
{ agent: agent.name, by: who.by, class: character.class },
|
|
136
229
|
"agent created",
|
|
137
230
|
);
|
|
138
231
|
this.#ctx.changed();
|
|
139
232
|
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];
|
|
144
|
-
const creator = store.agent(caller.name);
|
|
145
233
|
const where = channelId ? ` in <#${channelId}>` : "";
|
|
234
|
+
if (!task)
|
|
235
|
+
return {
|
|
236
|
+
agent,
|
|
237
|
+
text: `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}.`,
|
|
238
|
+
};
|
|
239
|
+
// The opening task runs in the room of the surface that asked, else where the agent is first
|
|
240
|
+
// reached: its channel, else its first room.
|
|
241
|
+
const rooms = surfaces?.roomSurfaces() ?? [];
|
|
242
|
+
const home =
|
|
243
|
+
who.openIn !== undefined && rooms.includes(who.openIn)
|
|
244
|
+
? agentRoomKey(who.openIn, agent.name)
|
|
245
|
+
: channelId
|
|
246
|
+
? discordKey(channelId)
|
|
247
|
+
: keysOf(store, agent.name, rooms)[0];
|
|
146
248
|
if (!home) {
|
|
147
249
|
logger.warn(
|
|
148
250
|
{ agent: agent.name },
|
|
149
251
|
"opening turn skipped: the agent has no channel or room",
|
|
150
252
|
);
|
|
151
|
-
return
|
|
253
|
+
return {
|
|
254
|
+
agent,
|
|
255
|
+
text: `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}. It has no room to start its opening task in yet.`,
|
|
256
|
+
};
|
|
152
257
|
}
|
|
153
258
|
void queue
|
|
154
259
|
.run(home, () =>
|
|
155
260
|
this.#ctx.turns.answerBackground(
|
|
156
261
|
home,
|
|
157
|
-
speaker,
|
|
262
|
+
who.speaker,
|
|
158
263
|
openingTaskText(
|
|
159
|
-
creator,
|
|
160
|
-
|
|
161
|
-
addressee(speaker, this.#ctx.options.owner),
|
|
264
|
+
who.creator,
|
|
265
|
+
task,
|
|
266
|
+
addressee(who.speaker, this.#ctx.options.owner),
|
|
162
267
|
),
|
|
163
268
|
),
|
|
164
269
|
)
|
|
165
270
|
.catch((error: unknown) =>
|
|
166
271
|
logger.error({ agent: agent.name, err: error }, "opening turn failed"),
|
|
167
272
|
);
|
|
168
|
-
return
|
|
273
|
+
return {
|
|
274
|
+
agent,
|
|
275
|
+
text: `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}. It is starting on its opening task now.`,
|
|
276
|
+
};
|
|
169
277
|
}
|
|
170
278
|
|
|
171
279
|
/** The agent tool: changes a look, then says what the owner sees. */
|
|
@@ -260,7 +368,7 @@ export class TeamEditing {
|
|
|
260
368
|
(input.members.includes("coordinator")
|
|
261
369
|
? "coordinator"
|
|
262
370
|
: (input.members[0] ?? ""));
|
|
263
|
-
store.checkNewGroup(input.name, input.members, host);
|
|
371
|
+
store.checkNewGroup(input.name, input.members, host, input.displayName);
|
|
264
372
|
const topic = groupTopic({ ...fields, host, status: "active" }, store);
|
|
265
373
|
const channelId = await channels?.createChannel(input.name, topic, parent);
|
|
266
374
|
const group = await store.createGroup({
|
|
@@ -277,7 +385,17 @@ export class TeamEditing {
|
|
|
277
385
|
name: string,
|
|
278
386
|
change: { displayName?: string; members?: string[]; host?: string },
|
|
279
387
|
): Promise<string> {
|
|
280
|
-
const
|
|
388
|
+
const { store, surfaces } = this.#ctx.options;
|
|
389
|
+
const before = store.group(name);
|
|
390
|
+
const group = await store.updateGroup(name, change);
|
|
391
|
+
// A member taken out starts its seat over, so an agent of that name that joins later
|
|
392
|
+
// does not resume the old conversation.
|
|
393
|
+
const runtime = this.#ctx.options.runtime();
|
|
394
|
+
const rooms = surfaces?.roomSurfaces() ?? [];
|
|
395
|
+
for (const left of before?.members ?? [])
|
|
396
|
+
if (!group.members.includes(left))
|
|
397
|
+
for (const seat of seatsOf(group, left, rooms))
|
|
398
|
+
await runtime.startFresh(seat);
|
|
281
399
|
this.#ctx.retitle(group);
|
|
282
400
|
this.#ctx.changed();
|
|
283
401
|
return `Updated the group "${group.displayName}": members ${group.members.join(", ")}; host ${group.host}.`;
|
|
@@ -217,3 +217,20 @@ export function keysOf(
|
|
|
217
217
|
];
|
|
218
218
|
return [];
|
|
219
219
|
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Every conversation of an agent's seat in a group: the Discord channel's, when the group has
|
|
223
|
+
* one, then each room's on `roomSurfaces`.
|
|
224
|
+
*/
|
|
225
|
+
export function seatsOf(
|
|
226
|
+
group: AgentGroup,
|
|
227
|
+
agent: string,
|
|
228
|
+
roomSurfaces: readonly string[],
|
|
229
|
+
): ChannelKey[] {
|
|
230
|
+
return [
|
|
231
|
+
...(group.channelId ? [groupSessionKey(group, agent)] : []),
|
|
232
|
+
...roomSurfaces.map((surface) =>
|
|
233
|
+
groupSessionKey(group, agent, groupRoomKey(surface, group.name)),
|
|
234
|
+
),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
7
7
|
import { AgentError } from "../domain/errors.ts";
|
|
8
8
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
9
|
+
import { messages } from "../i18n/index.ts";
|
|
9
10
|
import type { AgentChannels } from "./agent-ports.ts";
|
|
10
11
|
import { overBase } from "./agent-rules.ts";
|
|
11
12
|
import type { Agent, AgentGroup } from "./agent-store.ts";
|
|
@@ -16,11 +17,11 @@ import {
|
|
|
16
17
|
discordIdOf,
|
|
17
18
|
groupLogName,
|
|
18
19
|
groupOfChannel,
|
|
19
|
-
groupRoomKey,
|
|
20
20
|
groupSessionKey,
|
|
21
21
|
homeScope,
|
|
22
22
|
keysOf,
|
|
23
23
|
roomScope,
|
|
24
|
+
seatsOf,
|
|
24
25
|
} from "./team-keys.ts";
|
|
25
26
|
import { categoryKind, groupTopic } from "./team-layout.ts";
|
|
26
27
|
import type { TeamContext } from "./team-options.ts";
|
|
@@ -193,9 +194,13 @@ export class TeamLifecycle {
|
|
|
193
194
|
* Archives an agent or group like a deleted channel, then keeps its channel under Archive,
|
|
194
195
|
* where no one owns it and so nothing answers; an agent or group without a Discord channel
|
|
195
196
|
* only stops answering in its rooms. Runs at once, not in the target's queue: the caller may be
|
|
196
|
-
* a member of the group it archives, and the round stops at its next turn.
|
|
197
|
+
* a member of the group it archives, and the round stops at its next turn. An owner's call from
|
|
198
|
+
* outside any turn has no `caller`, so no agent is protected from archiving itself.
|
|
197
199
|
*/
|
|
198
|
-
async archive(
|
|
200
|
+
async archive(
|
|
201
|
+
caller: AgentTurnScope | undefined,
|
|
202
|
+
name: string,
|
|
203
|
+
): Promise<string> {
|
|
199
204
|
const { store, channels, logger } = this.#ctx.options;
|
|
200
205
|
const agent = store.agent(name);
|
|
201
206
|
const group = agent ? undefined : store.group(name);
|
|
@@ -208,7 +213,7 @@ export class TeamLifecycle {
|
|
|
208
213
|
throw new AgentError(`"${name}" is already archived.`);
|
|
209
214
|
if (agent && coordinatorOf(store, this.#ctx.options)?.name === name)
|
|
210
215
|
throw new AgentError("The coordinator cannot be archived.");
|
|
211
|
-
if (agent && name === caller
|
|
216
|
+
if (agent && name === caller?.name)
|
|
212
217
|
throw new AgentError("You cannot archive yourself; ask another agent.");
|
|
213
218
|
const { channelId } = target;
|
|
214
219
|
if (agent) await this.#archiveAgent(agent);
|
|
@@ -232,6 +237,91 @@ export class TeamLifecycle {
|
|
|
232
237
|
return `Archived "${target.displayName}"; its channel <#${channelId}> is kept under Archive.`;
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Returns an archived agent or group to the active ones: its channel leaves Archive for its
|
|
242
|
+
* category (a new channel replaces one deleted meanwhile), and it answers again from its next
|
|
243
|
+
* message. An agent does not rejoin the groups it left; a group needs all its members active.
|
|
244
|
+
*/
|
|
245
|
+
async restore(name: string): Promise<string> {
|
|
246
|
+
const { store, channels, logger } = this.#ctx.options;
|
|
247
|
+
const agent = store.agent(name);
|
|
248
|
+
const group = agent ? undefined : store.group(name);
|
|
249
|
+
const target = agent ?? group;
|
|
250
|
+
if (!target)
|
|
251
|
+
throw new AgentError(
|
|
252
|
+
`There is no agent or group "${name}"; see agent_list.`,
|
|
253
|
+
);
|
|
254
|
+
if (target.status === "active")
|
|
255
|
+
throw new AgentError(`"${name}" is not archived.`);
|
|
256
|
+
if (group) store.checkRestorableGroup(name);
|
|
257
|
+
// The store checks again when it restores. A member archived, or a second restore of the
|
|
258
|
+
// same name, between the two checks leaves the channel moved or made with no owner; it
|
|
259
|
+
// then waits under its category until the next restore claims it or an owner deletes it.
|
|
260
|
+
let { channelId } = target;
|
|
261
|
+
if (channels) {
|
|
262
|
+
const category = agent ? "Agents" : "Groups";
|
|
263
|
+
if (channelId && (await channels.exists(channelId)))
|
|
264
|
+
await channels.placeIn(channelId, category);
|
|
265
|
+
else if (agent || channelId)
|
|
266
|
+
channelId = await channels.createChannel(
|
|
267
|
+
name,
|
|
268
|
+
agent ? agent.displayName : groupTopic(target as AgentGroup, store),
|
|
269
|
+
category,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (agent) await store.restoreAgent(name, channelId);
|
|
273
|
+
else {
|
|
274
|
+
const restored = await store.restoreGroup(name, channelId);
|
|
275
|
+
this.retitle(restored);
|
|
276
|
+
}
|
|
277
|
+
logger.info({ name, kind: agent ? "agent" : "group" }, "restored");
|
|
278
|
+
this.#ctx.changed();
|
|
279
|
+
return `Restored "${target.displayName}"; it answers again.${agent ? " It is in no group until it is added to one." : ""}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Deletes an archived agent or group for good, its name free again. Its carried skills are
|
|
284
|
+
* detached, and the runtime forgets its conversations (an agent's own, its seats in every
|
|
285
|
+
* group, a group's members' seats), so nothing is left for a later agent or group of that name;
|
|
286
|
+
* a Discord channel it had stays under Archive with its history, since Discord is never asked
|
|
287
|
+
* to delete one. An active agent or group is refused: archive it first, and so is an
|
|
288
|
+
* agent the host's seeds name, since the next start would seed it again.
|
|
289
|
+
*/
|
|
290
|
+
async remove(name: string, cleanup?: () => Promise<void>): Promise<string> {
|
|
291
|
+
const { store, skills, seeds, logger } = this.#ctx.options;
|
|
292
|
+
const agent = store.agent(name);
|
|
293
|
+
const group = agent ? undefined : store.group(name);
|
|
294
|
+
const target = agent ?? group;
|
|
295
|
+
if (!target)
|
|
296
|
+
throw new AgentError(
|
|
297
|
+
`There is no agent or group "${name}"; see agent_list.`,
|
|
298
|
+
);
|
|
299
|
+
// Every start seeds the configured agents that are missing, so a deleted one would return.
|
|
300
|
+
if (agent && seeds().some((seed) => seed.name === name))
|
|
301
|
+
throw new AgentError(messages().agentSeededNotDeleted);
|
|
302
|
+
if (target.status === "active")
|
|
303
|
+
throw new AgentError(
|
|
304
|
+
`"${name}" is active; archive it before deleting it.`,
|
|
305
|
+
);
|
|
306
|
+
await cleanup?.();
|
|
307
|
+
// What a name keeps in the runtime would be picked up by the next agent or group made under it.
|
|
308
|
+
const runtime = this.#ctx.options.runtime();
|
|
309
|
+
const rooms = this.#rooms();
|
|
310
|
+
const seats = group
|
|
311
|
+
? group.members.flatMap((member) => seatsOf(group, member, rooms))
|
|
312
|
+
: store.groups().flatMap((other) => seatsOf(other, name, rooms));
|
|
313
|
+
const kept = [...keysOf(store, name, rooms), ...seats];
|
|
314
|
+
for (const key of kept) await runtime.deleteConversation(key);
|
|
315
|
+
if (agent) {
|
|
316
|
+
await store.deleteAgent(name);
|
|
317
|
+
const carried = skills?.carriedNames(name) ?? [];
|
|
318
|
+
if (skills && carried.length > 0) await skills.attach(name, [], carried);
|
|
319
|
+
} else await store.deleteGroup(name);
|
|
320
|
+
logger.info({ name, kind: agent ? "agent" : "group" }, "deleted");
|
|
321
|
+
this.#ctx.changed();
|
|
322
|
+
return `Deleted "${target.displayName}"; the name "${name}" is free again.`;
|
|
323
|
+
}
|
|
324
|
+
|
|
235
325
|
async #archiveAgent(agent: Agent): Promise<void> {
|
|
236
326
|
const { store, schedules, logger } = this.#ctx.options;
|
|
237
327
|
const runtime = this.#ctx.options.runtime();
|
|
@@ -244,7 +334,7 @@ export class TeamLifecycle {
|
|
|
244
334
|
await runtime.startFresh(key);
|
|
245
335
|
}
|
|
246
336
|
for (const group of left) {
|
|
247
|
-
for (const seat of
|
|
337
|
+
for (const seat of seatsOf(group, agent.name, rooms))
|
|
248
338
|
await runtime.startFresh(seat);
|
|
249
339
|
if (group.status === "active") this.retitle(group);
|
|
250
340
|
}
|
|
@@ -261,7 +351,7 @@ export class TeamLifecycle {
|
|
|
261
351
|
const rooms = this.#rooms();
|
|
262
352
|
await store.archiveGroup(group.name);
|
|
263
353
|
for (const member of group.members)
|
|
264
|
-
for (const seat of
|
|
354
|
+
for (const seat of seatsOf(group, member, rooms))
|
|
265
355
|
await runtime.startFresh(seat);
|
|
266
356
|
logger.info({ group: group.name }, "group archived");
|
|
267
357
|
this.#ctx.changed();
|
|
@@ -272,23 +362,6 @@ export class TeamLifecycle {
|
|
|
272
362
|
return this.#ctx.options.surfaces?.roomSurfaces() ?? [];
|
|
273
363
|
}
|
|
274
364
|
|
|
275
|
-
/**
|
|
276
|
-
* Every conversation of an agent's seat in a group: the Discord channel's, when the group has
|
|
277
|
-
* one, then each room's.
|
|
278
|
-
*/
|
|
279
|
-
#seats(
|
|
280
|
-
group: AgentGroup,
|
|
281
|
-
agent: string,
|
|
282
|
-
rooms: readonly string[],
|
|
283
|
-
): ChannelKey[] {
|
|
284
|
-
return [
|
|
285
|
-
...(group.channelId ? [groupSessionKey(group, agent)] : []),
|
|
286
|
-
...rooms.map((surface) =>
|
|
287
|
-
groupSessionKey(group, agent, groupRoomKey(surface, group.name)),
|
|
288
|
-
),
|
|
289
|
-
];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
365
|
/** Writes a group's members and host into its channel topic, without waiting on Discord; a group without a channel has no topic. */
|
|
293
366
|
retitle(group: AgentGroup): void {
|
|
294
367
|
const { channels, store, logger } = this.#ctx.options;
|