pi-roundtable 0.24.2 → 0.26.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 +38 -0
- package/docs/plugins.md +174 -7
- package/package.json +1 -1
- package/src/cli/checks/agent-tools.ts +23 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/principal.ts +9 -2
- package/src/core/agents/agent-book.ts +155 -0
- package/src/core/agents/agent-ports.ts +5 -1
- package/src/core/agents/agent-prompt.ts +6 -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 +44 -36
- package/src/core/agents/agent-tools.ts +12 -6
- package/src/core/agents/team-editing.ts +154 -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 +9 -4
- package/src/core/agents/team-selection.ts +32 -0
- package/src/core/builtin/agent-server.ts +30 -5
- package/src/core/builtin/channel-context.ts +21 -0
- package/src/core/builtin/discord.ts +4 -1
- package/src/core/builtin/prompts.ts +40 -0
- package/src/core/builtin/skills.ts +9 -5
- package/src/core/config/config-schema.ts +22 -6
- package/src/core/config/config.ts +30 -2
- package/src/core/config/conversations.ts +21 -0
- package/src/core/config/profiles.ts +53 -0
- package/src/core/contract/channel-context.ts +106 -0
- package/src/core/contract/surface.ts +16 -0
- package/src/core/conversations/conversation-registry.ts +6 -0
- package/src/core/conversations/conversation-retention.ts +107 -0
- package/src/core/conversations/conversation-store.ts +6 -0
- package/src/core/conversations/conversations-plugin.ts +19 -3
- package/src/core/define-roundtable.ts +12 -2
- package/src/core/discord/channel-context.ts +47 -78
- package/src/core/discord/discord-surface.ts +16 -0
- package/src/core/events.ts +1 -0
- package/src/core/host-registration.ts +4 -0
- package/src/core/host.ts +3 -1
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/cards.ts +1 -1
- package/src/core/identity/actor-facts.ts +7 -0
- package/src/core/identity/identity-plugin.ts +27 -5
- package/src/core/identity/identity-port.ts +62 -0
- package/src/core/identity/identity-schema.ts +2 -0
- package/src/core/identity/identity-service.ts +45 -52
- package/src/core/identity/identity-view.ts +1 -0
- package/src/core/identity/person-profiles.ts +275 -0
- package/src/core/identity/principal-tables.ts +10 -0
- package/src/core/identity/profile-choices.ts +45 -0
- package/src/core/identity/profile-memory.ts +65 -0
- package/src/core/identity/profile-picture.ts +117 -0
- package/src/core/identity/profiles-view.ts +20 -0
- package/src/core/identity/profiles.ts +82 -0
- package/src/core/modules/host-shell/shell-policy.ts +18 -3
- 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/plugin.ts +40 -0
- package/src/core/registry/contributions.ts +8 -1
- package/src/core/runtime/chat-only.ts +32 -0
- package/src/core/runtime/conversation-sessions.ts +2 -1
- 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/runtime-plugin.ts +1 -0
- package/src/core/runtime/runtime-types.ts +3 -1
- package/src/core/runtime/session-factory.ts +70 -65
- package/src/core/runtime/session-helpers.ts +27 -0
- package/src/core/services.ts +97 -33
- package/src/index.ts +31 -0
- package/src/kit/index.ts +1 -0
- package/src/kit/oidc.ts +16 -0
- package/src/testing.ts +1 -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. */
|
|
@@ -165,9 +149,14 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
165
149
|
owner,
|
|
166
150
|
shellUser,
|
|
167
151
|
scratchDir,
|
|
152
|
+
agentTools,
|
|
168
153
|
} = this.#options;
|
|
169
154
|
const agent = store.agent(scope.name);
|
|
170
155
|
if (!agent) throw new AgentError(`no agent ${scope.name}`);
|
|
156
|
+
const shared =
|
|
157
|
+
typeof sharedPrompt === "function" ? sharedPrompt() : sharedPrompt;
|
|
158
|
+
const guest =
|
|
159
|
+
typeof guestPrompt === "function" ? guestPrompt() : guestPrompt;
|
|
171
160
|
const group = scope.group ? store.group(scope.group) : undefined;
|
|
172
161
|
const coordinator = coordinatorOf(store, this.#options);
|
|
173
162
|
const speaker = this.#turns.speakerOf(scope);
|
|
@@ -175,13 +164,14 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
175
164
|
agent,
|
|
176
165
|
...(speaker ? { speaker } : {}),
|
|
177
166
|
...(coordinator ? { coordinator } : {}),
|
|
178
|
-
shared
|
|
179
|
-
...(
|
|
167
|
+
shared,
|
|
168
|
+
...(guest ? { guestShared: guest } : {}),
|
|
180
169
|
workDir,
|
|
181
170
|
owner,
|
|
182
171
|
shellUser,
|
|
183
172
|
...(scratchDir ? { scratchDir } : {}),
|
|
184
173
|
images: this.#images(),
|
|
174
|
+
...(agentTools ? { agentTools } : {}),
|
|
185
175
|
...(group
|
|
186
176
|
? {
|
|
187
177
|
group: {
|
|
@@ -292,14 +282,21 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
292
282
|
return this.#lifecycle.channelDeleted(channelId);
|
|
293
283
|
}
|
|
294
284
|
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
* where no one owns it and so nothing answers.
|
|
298
|
-
*/
|
|
299
|
-
archive(caller: AgentTurnScope, name: string): Promise<string> {
|
|
285
|
+
/** Archives an agent or group like a deleted channel, keeping its channel under Archive. */
|
|
286
|
+
archive(caller: AgentTurnScope | undefined, name: string): Promise<string> {
|
|
300
287
|
return this.#lifecycle.archive(caller, name);
|
|
301
288
|
}
|
|
302
289
|
|
|
290
|
+
/** Returns an archived agent or group to the active ones; see `AgentTeam.restore`. */
|
|
291
|
+
restore(name: string): Promise<string> {
|
|
292
|
+
return this.#lifecycle.restore(name);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Deletes an archived agent or group for good; see `AgentTeam.remove`. */
|
|
296
|
+
remove(name: string, cleanup?: () => Promise<void>): Promise<string> {
|
|
297
|
+
return this.#lifecycle.remove(name, cleanup);
|
|
298
|
+
}
|
|
299
|
+
|
|
303
300
|
// ── Turns, run by TeamTurns ────────────────────────────────────────────
|
|
304
301
|
|
|
305
302
|
/**
|
|
@@ -442,8 +439,15 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
442
439
|
* Changes an agent's display name, prompt, model, or thinking level; `default` returns the
|
|
443
440
|
* model or thinking level to the assistant's. A model must be one the host runs.
|
|
444
441
|
*/
|
|
445
|
-
update(name: string, change: AgentChange): Promise<string> {
|
|
446
|
-
return this.#editing.update(name, change);
|
|
442
|
+
update(name: string, change: AgentChange, by?: string): Promise<string> {
|
|
443
|
+
return this.#editing.update(name, change, by);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** The changes of an agent's prompt, newest first. */
|
|
447
|
+
async promptHistory(name: string, limit?: number): Promise<PromptVersion[]> {
|
|
448
|
+
if (!this.#options.store.agent(name))
|
|
449
|
+
throw new AgentError(`There is no agent "${name}".`);
|
|
450
|
+
return this.#options.store.promptHistory(name, limit);
|
|
447
451
|
}
|
|
448
452
|
|
|
449
453
|
create(
|
|
@@ -453,6 +457,14 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
453
457
|
return this.#editing.create(caller, input);
|
|
454
458
|
}
|
|
455
459
|
|
|
460
|
+
/** An owner creates an agent outside any agent turn; see `AgentTeam.createAgent`. */
|
|
461
|
+
createAgent(
|
|
462
|
+
by: Speaker,
|
|
463
|
+
input: Parameters<TeamEditing["createFor"]>[1],
|
|
464
|
+
): Promise<Agent> {
|
|
465
|
+
return this.#editing.createFor(by, input);
|
|
466
|
+
}
|
|
467
|
+
|
|
456
468
|
/** Changes an agent's look; see `AgentTeam.dress`. */
|
|
457
469
|
dress(name: string, input: CharacterInput): Promise<Agent> {
|
|
458
470
|
return this.#editing.dress(name, input);
|
|
@@ -468,19 +480,15 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
|
|
|
468
480
|
return this.#editing.redrawAvatar(name, mode, text);
|
|
469
481
|
}
|
|
470
482
|
|
|
471
|
-
createGroup(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
members: string[];
|
|
475
|
-
host?: string;
|
|
476
|
-
category?: string;
|
|
477
|
-
}): Promise<string> {
|
|
483
|
+
createGroup(
|
|
484
|
+
input: Parameters<TeamEditing["createGroup"]>[0],
|
|
485
|
+
): Promise<string> {
|
|
478
486
|
return this.#editing.createGroup(input);
|
|
479
487
|
}
|
|
480
488
|
|
|
481
489
|
updateGroup(
|
|
482
490
|
name: string,
|
|
483
|
-
change:
|
|
491
|
+
change: Parameters<TeamEditing["updateGroup"]>[1],
|
|
484
492
|
): Promise<string> {
|
|
485
493
|
return this.#editing.updateGroup(name, change);
|
|
486
494
|
}
|
|
@@ -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
|
{
|
|
@@ -5,16 +5,17 @@ import {
|
|
|
5
5
|
resolveCharacter,
|
|
6
6
|
suggestCharacter,
|
|
7
7
|
} from "../../characters/index.ts";
|
|
8
|
+
import { parseChannelKey } from "../contract/surface.ts";
|
|
8
9
|
import { AgentError } from "../domain/errors.ts";
|
|
9
10
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
10
11
|
import type { AgentChange } from "../services.ts";
|
|
11
|
-
import { addressee } from "../speakers.ts";
|
|
12
|
+
import { addressee, type Speaker } from "../speakers.ts";
|
|
12
13
|
import { openingTaskText } from "./agent-prompt.ts";
|
|
13
14
|
import { characterOf, overBase } from "./agent-rules.ts";
|
|
14
15
|
import { modelSetting, thinkingSetting } from "./agent-settings.ts";
|
|
15
16
|
import type { Agent } from "./agent-store.ts";
|
|
16
17
|
import type { AvatarMode } from "./agent-tools.ts";
|
|
17
|
-
import { discordKey, keysOf } from "./team-keys.ts";
|
|
18
|
+
import { agentRoomKey, discordKey, keysOf, seatsOf } from "./team-keys.ts";
|
|
18
19
|
import { groupTopic, teamCategory } from "./team-layout.ts";
|
|
19
20
|
import type { TeamContext } from "./team-options.ts";
|
|
20
21
|
|
|
@@ -34,7 +35,11 @@ export class TeamEditing {
|
|
|
34
35
|
* Changes an agent's display name, prompt, model, or thinking level; `default` returns the
|
|
35
36
|
* model or thinking level to the assistant's. A model must be one the host runs.
|
|
36
37
|
*/
|
|
37
|
-
async update(
|
|
38
|
+
async update(
|
|
39
|
+
name: string,
|
|
40
|
+
change: AgentChange,
|
|
41
|
+
by?: string,
|
|
42
|
+
): Promise<string> {
|
|
38
43
|
const { store, logger } = this.#ctx.options;
|
|
39
44
|
if (Object.values(change).every((value) => value === undefined))
|
|
40
45
|
throw new AgentError(
|
|
@@ -44,14 +49,18 @@ export class TeamEditing {
|
|
|
44
49
|
this.#ctx.options.models.usable(),
|
|
45
50
|
);
|
|
46
51
|
const thinking = thinkingSetting(change.thinking);
|
|
47
|
-
const agent = await store.updateAgent(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const agent = await store.updateAgent(
|
|
53
|
+
name,
|
|
54
|
+
{
|
|
55
|
+
...(change.displayName !== undefined
|
|
56
|
+
? { displayName: change.displayName }
|
|
57
|
+
: {}),
|
|
58
|
+
...(change.prompt !== undefined ? { prompt: change.prompt } : {}),
|
|
59
|
+
...(model !== undefined ? { model } : {}),
|
|
60
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
61
|
+
},
|
|
62
|
+
by,
|
|
63
|
+
);
|
|
55
64
|
logger.info(
|
|
56
65
|
{
|
|
57
66
|
agent: name,
|
|
@@ -72,6 +81,7 @@ export class TeamEditing {
|
|
|
72
81
|
return `Updated "${agent.displayName}" (${agent.name}); it applies from its next turn. It runs ${runs} with thinking ${level}.`;
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/** The agent tool: an agent creates another, which starts on the opening task at once. */
|
|
75
85
|
async create(
|
|
76
86
|
caller: AgentTurnScope,
|
|
77
87
|
input: {
|
|
@@ -87,27 +97,109 @@ export class TeamEditing {
|
|
|
87
97
|
skills?: string[];
|
|
88
98
|
},
|
|
89
99
|
): 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
|
-
);
|
|
100
|
+
this.#refuseSkillsOff(input.skills);
|
|
96
101
|
const speaker = this.#ctx.turns.speakerOf(caller);
|
|
97
102
|
if (!speaker)
|
|
98
103
|
throw new AgentError(
|
|
99
104
|
"agent_create can only be used during an agent turn.",
|
|
100
105
|
);
|
|
106
|
+
// The opening task starts on the surface the caller's turn runs on, as the web form's does.
|
|
107
|
+
const working = this.#ctx.turns.workingIn(caller.name);
|
|
108
|
+
const made = await this.#create(
|
|
109
|
+
{
|
|
110
|
+
speaker,
|
|
111
|
+
by: caller.name,
|
|
112
|
+
creator: this.#ctx.options.store.agent(caller.name),
|
|
113
|
+
openIn: working ? parseChannelKey(working).surface : undefined,
|
|
114
|
+
},
|
|
115
|
+
input,
|
|
116
|
+
);
|
|
117
|
+
return made.text;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* An owner creates an agent from outside any agent turn (the web app's form; any other tier is
|
|
122
|
+
* refused, since the opening task runs as `speaker`): the same checks,
|
|
123
|
+
* picture, channel and opening task as `create`, and the model and thinking level the owner
|
|
124
|
+
* picked. Without a task the agent only exists, waiting in its room. Returns the new agent.
|
|
125
|
+
*/
|
|
126
|
+
async createFor(
|
|
127
|
+
speaker: Speaker,
|
|
128
|
+
input: {
|
|
129
|
+
name: string;
|
|
130
|
+
displayName: string;
|
|
131
|
+
prompt: string;
|
|
132
|
+
character?: CharacterInput;
|
|
133
|
+
task?: string;
|
|
134
|
+
openIn?: string;
|
|
135
|
+
category?: string;
|
|
136
|
+
skills?: string[];
|
|
137
|
+
model?: string;
|
|
138
|
+
thinking?: string;
|
|
139
|
+
},
|
|
140
|
+
): Promise<Agent> {
|
|
141
|
+
if (speaker.tier !== "owner")
|
|
142
|
+
throw new AgentError("Only an owner may create an agent here.");
|
|
143
|
+
const { model, thinking, task, openIn, ...rest } = input;
|
|
144
|
+
const made = await this.#create(
|
|
145
|
+
{ speaker, by: speaker.name, creator: undefined, openIn },
|
|
146
|
+
{ ...rest, ...(task ? { task } : {}) },
|
|
147
|
+
{ model, thinking },
|
|
148
|
+
);
|
|
149
|
+
return made.agent;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** An agent cannot carry skills on a host with skills switched off. */
|
|
153
|
+
#refuseSkillsOff(carried: string[] | undefined): void {
|
|
154
|
+
if (carried && carried.length > 0 && !this.#ctx.options.skills)
|
|
155
|
+
throw new AgentError(
|
|
156
|
+
"Skills are off on this host, so an agent cannot carry any. Create it without skills.",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async #create(
|
|
161
|
+
who: {
|
|
162
|
+
speaker: Speaker;
|
|
163
|
+
by: string;
|
|
164
|
+
creator: Agent | undefined;
|
|
165
|
+
/** The surface whose room runs the opening task. */
|
|
166
|
+
openIn?: string | undefined;
|
|
167
|
+
},
|
|
168
|
+
input: {
|
|
169
|
+
name: string;
|
|
170
|
+
displayName: string;
|
|
171
|
+
prompt: string;
|
|
172
|
+
/** @deprecated Ignored: agents are composed characters. */
|
|
173
|
+
avatarPrompt?: string;
|
|
174
|
+
character?: CharacterInput;
|
|
175
|
+
task?: string;
|
|
176
|
+
category?: string;
|
|
177
|
+
skills?: string[];
|
|
178
|
+
},
|
|
179
|
+
settings: {
|
|
180
|
+
model?: string | undefined;
|
|
181
|
+
thinking?: string | undefined;
|
|
182
|
+
} = {},
|
|
183
|
+
): Promise<{ agent: Agent; text: string }> {
|
|
184
|
+
const { store, studio, channels, queue, skills, surfaces, logger } =
|
|
185
|
+
this.#ctx.options;
|
|
186
|
+
this.#refuseSkillsOff(input.skills);
|
|
101
187
|
const {
|
|
102
188
|
category,
|
|
103
189
|
skills: carried = [],
|
|
104
190
|
character: wanted,
|
|
105
191
|
avatarPrompt: _ignored,
|
|
192
|
+
task,
|
|
106
193
|
...fields
|
|
107
194
|
} = input;
|
|
108
195
|
const parent = teamCategory(category, "Agents");
|
|
109
196
|
store.checkNewName(input.name);
|
|
110
197
|
skills?.checkRegistered(carried);
|
|
198
|
+
// A model or thinking level the host cannot run fails the create, before anything is made.
|
|
199
|
+
const model = await modelSetting(settings.model, () =>
|
|
200
|
+
this.#ctx.options.models.usable(),
|
|
201
|
+
);
|
|
202
|
+
const thinking = thinkingSetting(settings.thinking);
|
|
111
203
|
// The picture comes before the row and the channel: a render failure fails the create.
|
|
112
204
|
const taken = store
|
|
113
205
|
.agents()
|
|
@@ -123,49 +215,69 @@ export class TeamEditing {
|
|
|
123
215
|
input.displayName,
|
|
124
216
|
parent,
|
|
125
217
|
);
|
|
126
|
-
|
|
218
|
+
let agent = await store.createAgent({
|
|
127
219
|
...fields,
|
|
128
220
|
character,
|
|
129
221
|
avatarHash,
|
|
130
222
|
...(channelId ? { channelId } : {}),
|
|
131
223
|
});
|
|
224
|
+
if (model || thinking)
|
|
225
|
+
agent = await store.updateAgent(agent.name, {
|
|
226
|
+
...(model ? { model } : {}),
|
|
227
|
+
...(thinking ? { thinking } : {}),
|
|
228
|
+
});
|
|
132
229
|
if (skills && carried.length > 0)
|
|
133
230
|
await skills.attach(agent.name, carried, []);
|
|
134
231
|
logger.info(
|
|
135
|
-
{ agent: agent.name, by:
|
|
232
|
+
{ agent: agent.name, by: who.by, class: character.class },
|
|
136
233
|
"agent created",
|
|
137
234
|
);
|
|
138
235
|
this.#ctx.changed();
|
|
139
236
|
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
237
|
const where = channelId ? ` in <#${channelId}>` : "";
|
|
238
|
+
if (!task)
|
|
239
|
+
return {
|
|
240
|
+
agent,
|
|
241
|
+
text: `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}.`,
|
|
242
|
+
};
|
|
243
|
+
// The opening task runs in the room of the surface that asked, else where the agent is first
|
|
244
|
+
// reached: its channel, else its first room.
|
|
245
|
+
const rooms = surfaces?.roomSurfaces() ?? [];
|
|
246
|
+
const home =
|
|
247
|
+
who.openIn !== undefined && rooms.includes(who.openIn)
|
|
248
|
+
? agentRoomKey(who.openIn, agent.name)
|
|
249
|
+
: channelId
|
|
250
|
+
? discordKey(channelId)
|
|
251
|
+
: keysOf(store, agent.name, rooms)[0];
|
|
146
252
|
if (!home) {
|
|
147
253
|
logger.warn(
|
|
148
254
|
{ agent: agent.name },
|
|
149
255
|
"opening turn skipped: the agent has no channel or room",
|
|
150
256
|
);
|
|
151
|
-
return
|
|
257
|
+
return {
|
|
258
|
+
agent,
|
|
259
|
+
text: `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}. It has no room to start its opening task in yet.`,
|
|
260
|
+
};
|
|
152
261
|
}
|
|
153
262
|
void queue
|
|
154
263
|
.run(home, () =>
|
|
155
264
|
this.#ctx.turns.answerBackground(
|
|
156
265
|
home,
|
|
157
|
-
speaker,
|
|
266
|
+
who.speaker,
|
|
158
267
|
openingTaskText(
|
|
159
|
-
creator,
|
|
160
|
-
|
|
161
|
-
addressee(speaker, this.#ctx.options.owner),
|
|
268
|
+
who.creator,
|
|
269
|
+
task,
|
|
270
|
+
addressee(who.speaker, this.#ctx.options.owner),
|
|
162
271
|
),
|
|
163
272
|
),
|
|
164
273
|
)
|
|
165
274
|
.catch((error: unknown) =>
|
|
166
275
|
logger.error({ agent: agent.name, err: error }, "opening turn failed"),
|
|
167
276
|
);
|
|
168
|
-
return
|
|
277
|
+
return {
|
|
278
|
+
agent,
|
|
279
|
+
text: `Created "${agent.displayName}" (${agent.name})${where}; ${avatar}. It is starting on its opening task now.`,
|
|
280
|
+
};
|
|
169
281
|
}
|
|
170
282
|
|
|
171
283
|
/** The agent tool: changes a look, then says what the owner sees. */
|
|
@@ -260,7 +372,7 @@ export class TeamEditing {
|
|
|
260
372
|
(input.members.includes("coordinator")
|
|
261
373
|
? "coordinator"
|
|
262
374
|
: (input.members[0] ?? ""));
|
|
263
|
-
store.checkNewGroup(input.name, input.members, host);
|
|
375
|
+
store.checkNewGroup(input.name, input.members, host, input.displayName);
|
|
264
376
|
const topic = groupTopic({ ...fields, host, status: "active" }, store);
|
|
265
377
|
const channelId = await channels?.createChannel(input.name, topic, parent);
|
|
266
378
|
const group = await store.createGroup({
|
|
@@ -277,7 +389,17 @@ export class TeamEditing {
|
|
|
277
389
|
name: string,
|
|
278
390
|
change: { displayName?: string; members?: string[]; host?: string },
|
|
279
391
|
): Promise<string> {
|
|
280
|
-
const
|
|
392
|
+
const { store, surfaces } = this.#ctx.options;
|
|
393
|
+
const before = store.group(name);
|
|
394
|
+
const group = await store.updateGroup(name, change);
|
|
395
|
+
// A member taken out starts its seat over, so an agent of that name that joins later
|
|
396
|
+
// does not resume the old conversation.
|
|
397
|
+
const runtime = this.#ctx.options.runtime();
|
|
398
|
+
const rooms = surfaces?.roomSurfaces() ?? [];
|
|
399
|
+
for (const left of before?.members ?? [])
|
|
400
|
+
if (!group.members.includes(left))
|
|
401
|
+
for (const seat of seatsOf(group, left, rooms))
|
|
402
|
+
await runtime.startFresh(seat);
|
|
281
403
|
this.#ctx.retitle(group);
|
|
282
404
|
this.#ctx.changed();
|
|
283
405
|
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;
|