pi-roundtable 0.22.5 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -1
- package/README.md +35 -9
- package/README.zh-TW.md +51 -13
- package/docs/migrating-0.8.md +2 -2
- package/docs/plugins.md +350 -28
- package/examples/discord-adapter.ts +9 -1
- package/examples/seeds.test.ts +2 -2
- package/examples/seeds.ts +1 -2
- package/package.json +6 -2
- package/src/characters/catalog.ts +1055 -0
- package/src/characters/characters.css +68 -0
- package/src/characters/colors.ts +108 -0
- package/src/characters/compose.ts +281 -0
- package/src/characters/css.ts +63 -0
- package/src/characters/index.ts +33 -0
- package/src/characters/parse.ts +196 -0
- package/src/characters/parts-face.ts +354 -0
- package/src/characters/parts-hair.ts +56 -0
- package/src/characters/parts-hats.ts +57 -0
- package/src/characters/parts-outfits.ts +81 -0
- package/src/characters/parts-props.ts +137 -0
- package/src/characters/spec.ts +224 -0
- package/src/characters/suggest.ts +101 -0
- package/src/cli/checks/characters.ts +23 -0
- package/src/cli/checks/images.ts +5 -3
- package/src/cli/doctor.ts +2 -0
- package/src/core/agents/agent-claim.ts +30 -11
- package/src/core/agents/agent-dashboard.ts +1 -1
- package/src/core/agents/agent-messages.ts +43 -15
- package/src/core/agents/agent-prompt.ts +2 -4
- package/src/core/agents/agent-rules.ts +42 -3
- package/src/core/agents/agent-schema.ts +37 -15
- package/src/core/agents/agent-store.ts +57 -13
- package/src/core/agents/agent-team-fixture.ts +90 -14
- package/src/core/agents/agent-team.ts +83 -50
- package/src/core/agents/agent-tools.ts +94 -41
- package/src/core/agents/avatar-studio.ts +40 -4
- package/src/core/agents/character-png.ts +40 -0
- package/src/core/agents/group-messages.ts +7 -4
- package/src/core/agents/group-turns.ts +27 -14
- package/src/core/agents/team-editing.ts +99 -58
- package/src/core/agents/team-keys.ts +177 -11
- package/src/core/agents/team-lifecycle.ts +151 -84
- package/src/core/agents/team-options.ts +13 -6
- package/src/core/agents/team-status.ts +31 -13
- package/src/core/agents/team-text.ts +13 -3
- package/src/core/agents/team-turn-types.ts +25 -7
- package/src/core/agents/team-turns.ts +143 -28
- package/src/core/attachments/attachment-fetcher.ts +162 -64
- package/src/core/attachments/prompt-block.ts +7 -1
- package/src/core/attachments/tool-attachment.ts +20 -1
- package/src/core/attachments/turn-attachments.ts +18 -2
- package/src/core/builtin/agent-server.ts +103 -40
- package/src/core/builtin/discord.ts +4 -17
- package/src/core/builtin/modules.ts +6 -5
- package/src/core/builtin/threads.ts +37 -0
- package/src/core/config/agent-team.ts +43 -0
- package/src/core/config/config.ts +50 -59
- package/src/core/config/limits.ts +93 -0
- package/src/core/config/ops.ts +34 -0
- package/src/core/config/schema.ts +26 -0
- package/src/core/contract/channels.ts +20 -0
- package/src/core/contract/surface.ts +77 -7
- package/src/core/define-roundtable.ts +70 -42
- package/src/core/discord/agent-commands.ts +108 -86
- package/src/core/discord/agent-panel.ts +11 -2
- package/src/core/discord/connection.ts +1 -1
- package/src/core/discord/discord-surface.ts +10 -1
- package/src/core/discord/dispatch-thread-host.ts +1 -1
- package/src/core/domain/conversation.ts +24 -0
- package/src/core/holds.ts +221 -15
- package/src/core/host-registration.ts +2 -1
- package/src/core/http/listeners.ts +63 -3
- package/src/core/i18n/agent-panel.ts +14 -21
- package/src/core/interactions/prompts.ts +12 -0
- package/src/core/modes/autonomy.ts +23 -0
- package/src/core/modes/conversation-modes.ts +213 -0
- package/src/core/modes/modes-plugin.ts +24 -0
- package/src/core/modules/delegation/delegator.ts +4 -4
- package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
- package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
- package/src/core/modules/host-shell/shell-lexer.ts +493 -0
- package/src/core/modules/host-shell/shell-paths.ts +85 -4
- package/src/core/modules/host-shell/shell-policy.ts +341 -332
- package/src/core/modules/host-shell/shell-programs.ts +298 -0
- package/src/core/modules/host-shell/shell-quoting.ts +237 -0
- package/src/core/modules/host-shell/shell-scripts.ts +114 -0
- package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
- package/src/core/modules/schedules/precheck-tools.ts +17 -0
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/presence/direct-channels.ts +30 -0
- package/src/core/presentation/reply-body.ts +25 -0
- package/src/core/registry/contributions.ts +31 -7
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -15
- package/src/core/routing/surface-port.ts +5 -1
- package/src/core/runtime/extensions/approval-card.ts +1 -5
- package/src/core/runtime/extensions/approval-fields.ts +3 -7
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
- package/src/core/runtime/extensions/gate-refusals.ts +23 -0
- package/src/core/runtime/extensions/held-calls.ts +16 -1
- package/src/core/runtime/extensions/hold-describer.ts +58 -8
- package/src/core/runtime/extensions/hold-lookups.ts +19 -19
- package/src/core/runtime/extensions/yolo.ts +105 -0
- package/src/core/runtime/pi-agent-runtime.ts +3 -5
- package/src/core/runtime/runtime-plugin.ts +42 -1
- package/src/core/runtime/runtime-types.ts +11 -0
- package/src/core/runtime/turn-budget.ts +32 -5
- package/src/core/runtime/turn-limits.ts +14 -3
- package/src/core/services.ts +61 -9
- package/src/core/shared/attachment-reader.ts +6 -0
- package/src/core/shared/unix-server.ts +10 -1
- package/src/core/testing/modules.ts +12 -8
- package/src/core/testing/surface-threads.ts +58 -0
- package/src/core/testing/thread-host.ts +17 -5
- package/src/core/threads/dispatch-threads.ts +299 -0
- package/src/core/threads/host-threads.ts +19 -0
- package/src/core/tool-set.snapshot.json +5 -0
- package/src/index.ts +27 -1
- package/src/kit/characters.ts +5 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/shell.ts +1 -0
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/testing.ts +4 -0
- package/templates/agents.ts +1 -1
- package/src/core/discord/dispatch-threads.ts +0 -194
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CharacterSpec,
|
|
3
|
+
resolveCharacter,
|
|
4
|
+
suggestCharacter,
|
|
5
|
+
} from "../../characters/index.ts";
|
|
1
6
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
2
7
|
import { AgentError } from "../domain/errors.ts";
|
|
3
8
|
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
4
|
-
import type {
|
|
9
|
+
import type { AgentChannels } from "./agent-ports.ts";
|
|
10
|
+
import { overBase } from "./agent-rules.ts";
|
|
11
|
+
import type { Agent, AgentGroup } from "./agent-store.ts";
|
|
5
12
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
agentOfChannel,
|
|
14
|
+
coordinatorOf,
|
|
15
|
+
discordIdOf,
|
|
16
|
+
groupLogName,
|
|
17
|
+
groupOfChannel,
|
|
18
|
+
groupRoomKey,
|
|
8
19
|
groupSessionKey,
|
|
9
20
|
homeScope,
|
|
21
|
+
keysOf,
|
|
22
|
+
roomScope,
|
|
10
23
|
} from "./team-keys.ts";
|
|
11
24
|
import { categoryKind, groupTopic } from "./team-layout.ts";
|
|
12
25
|
import type { TeamContext } from "./team-options.ts";
|
|
@@ -23,13 +36,23 @@ export class TeamLifecycle {
|
|
|
23
36
|
|
|
24
37
|
/**
|
|
25
38
|
* Inserts the seed agents, gives every active agent a channel, archives agents and groups
|
|
26
|
-
* whose channels were deleted while the assistant was down, and
|
|
27
|
-
* background.
|
|
39
|
+
* whose channels were deleted while the assistant was down, and dresses agents without a
|
|
40
|
+
* character in the background. Without Discord channels the agents live in rooms, which need no setup, so
|
|
41
|
+
* none is made or checked.
|
|
28
42
|
*/
|
|
29
43
|
async start(): Promise<void> {
|
|
30
44
|
const { store, channels, seeds, logger } = this.#ctx.options;
|
|
31
45
|
const added = await store.seed(seeds());
|
|
32
46
|
if (added.length > 0) logger.info({ added }, "seed agents added");
|
|
47
|
+
if (channels) await this.#startChannels(channels);
|
|
48
|
+
this.#ctx.changed();
|
|
49
|
+
void this.#dressMissing().then((dressed) => {
|
|
50
|
+
if (dressed) this.#ctx.changed();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async #startChannels(channels: AgentChannels): Promise<void> {
|
|
55
|
+
const { store, logger } = this.#ctx.options;
|
|
33
56
|
for (const agent of store.agents()) {
|
|
34
57
|
if (agent.status !== "active") continue;
|
|
35
58
|
if (!agent.channelId) {
|
|
@@ -49,93 +72,106 @@ export class TeamLifecycle {
|
|
|
49
72
|
}
|
|
50
73
|
const layout = await channels.layout();
|
|
51
74
|
for (const group of store.groups()) {
|
|
52
|
-
|
|
53
|
-
if (!
|
|
54
|
-
|
|
75
|
+
// A group of a team that once ran without Discord lives in rooms only.
|
|
76
|
+
if (group.status !== "active" || !group.channelId) continue;
|
|
77
|
+
const { channelId } = group;
|
|
78
|
+
if (!(await channels.exists(channelId))) {
|
|
79
|
+
await this.channelDeleted(channelId);
|
|
55
80
|
continue;
|
|
56
81
|
}
|
|
57
82
|
this.retitle(group);
|
|
58
|
-
const home = layout.find((c) => c.channelIds.includes(
|
|
83
|
+
const home = layout.find((c) => c.channelIds.includes(channelId));
|
|
59
84
|
if (home && categoryKind(home.name) === "Groups") continue;
|
|
60
|
-
if (await channels.placeIn(
|
|
85
|
+
if (await channels.placeIn(channelId, "Groups"))
|
|
61
86
|
logger.info({ group: group.name }, "group channel moved to Groups");
|
|
62
87
|
}
|
|
63
|
-
this.#ctx.changed();
|
|
64
|
-
void this.#drawMissingAvatars();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async #drawMissingAvatars(): Promise<void> {
|
|
68
|
-
const { store, studio, logger } = this.#ctx.options;
|
|
69
|
-
if (studio.canDraw === false) return this.#generateMissingAvatars();
|
|
70
|
-
for (const agent of store.agents()) {
|
|
71
|
-
if (agent.status !== "active" || agent.avatarHash) continue;
|
|
72
|
-
try {
|
|
73
|
-
const hash = await studio.draw(agent.avatarPrompt);
|
|
74
|
-
await store.updateAgent(agent.name, { avatarHash: hash });
|
|
75
|
-
logger.info({ agent: agent.name }, "avatar drawn");
|
|
76
|
-
} catch (error) {
|
|
77
|
-
logger.warn({ agent: agent.name, err: error }, "avatar not drawn");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
91
|
+
* Dresses every active agent that has no character: the seed of that name gives it (its fields
|
|
92
|
+
* over what the role suggests), otherwise the suggestion for the role, spread over the looks
|
|
93
|
+
* already worn. The picture is rendered and replaces the old hash. A failure is logged for that
|
|
94
|
+
* agent, which keeps its picture until the next start; a second pass finds nothing to do.
|
|
85
95
|
*/
|
|
86
|
-
async #
|
|
87
|
-
const { store, studio, logger } = this.#ctx.options;
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
async #dressMissing(): Promise<boolean> {
|
|
97
|
+
const { store, studio, seeds, logger } = this.#ctx.options;
|
|
98
|
+
const seeded = new Map(seeds().map((seed) => [seed.name, seed]));
|
|
99
|
+
const worn: CharacterSpec[] = store
|
|
100
|
+
.agents()
|
|
101
|
+
.flatMap((agent) =>
|
|
102
|
+
agent.status === "active" && agent.character ? [agent.character] : [],
|
|
103
|
+
);
|
|
104
|
+
let dressed = false;
|
|
90
105
|
for (const agent of store.agents()) {
|
|
91
|
-
if (
|
|
106
|
+
if (
|
|
107
|
+
agent.status !== "active" ||
|
|
108
|
+
agent.character ||
|
|
109
|
+
store.hasStoredCharacter(agent.name)
|
|
110
|
+
)
|
|
111
|
+
continue;
|
|
92
112
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
const suggested = suggestCharacter(agent, worn);
|
|
114
|
+
const wanted = seeded.get(agent.name)?.character;
|
|
115
|
+
const character = wanted
|
|
116
|
+
? resolveCharacter(overBase(wanted, suggested), suggested)
|
|
117
|
+
: suggested;
|
|
118
|
+
const avatarHash = await studio.render(character);
|
|
119
|
+
if (!(await store.dressIfUndressed(agent.name, character, avatarHash)))
|
|
120
|
+
continue;
|
|
121
|
+
dressed = true;
|
|
122
|
+
worn.push(character);
|
|
123
|
+
logger.info(
|
|
124
|
+
{ agent: agent.name, class: character.class },
|
|
125
|
+
"agent dressed",
|
|
126
|
+
);
|
|
96
127
|
} catch (error) {
|
|
97
|
-
logger.warn({ agent: agent.name, err: error }, "
|
|
128
|
+
logger.warn({ agent: agent.name, err: error }, "agent not dressed");
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
|
-
|
|
101
|
-
logger.info(
|
|
102
|
-
{ generated },
|
|
103
|
-
"no image provider is configured; agents without a picture got one generated from their display name",
|
|
104
|
-
);
|
|
131
|
+
return dressed;
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
/** Starts an agent's or every group member's conversation over; call inside the channel's queue. */
|
|
108
135
|
async startFresh(channel: ChannelKey): Promise<void> {
|
|
109
|
-
const { store } = this.#ctx.options;
|
|
136
|
+
const { store, surfaces } = this.#ctx.options;
|
|
110
137
|
const runtime = this.#ctx.options.runtime();
|
|
111
|
-
|
|
112
|
-
const agent = store
|
|
113
|
-
if (agent) {
|
|
114
|
-
await runtime.startFresh(
|
|
138
|
+
// A Discord channel and each room are conversations of their own: a room starts over alone.
|
|
139
|
+
const agent = agentOfChannel(store, surfaces, channel);
|
|
140
|
+
if (agent?.status === "active") {
|
|
141
|
+
await runtime.startFresh(
|
|
142
|
+
discordIdOf(channel) === undefined
|
|
143
|
+
? roomScope(agent, channel).session
|
|
144
|
+
: homeScope(agent).session,
|
|
145
|
+
);
|
|
115
146
|
this.#ctx.changed();
|
|
116
147
|
return;
|
|
117
148
|
}
|
|
118
|
-
const group = store
|
|
119
|
-
if (
|
|
149
|
+
const group = groupOfChannel(store, surfaces, channel);
|
|
150
|
+
if (group?.status !== "active") return;
|
|
120
151
|
for (const member of group.members)
|
|
121
|
-
await runtime.startFresh(groupSessionKey(group, member));
|
|
122
|
-
await store.catchUp(group);
|
|
152
|
+
await runtime.startFresh(groupSessionKey(group, member, channel));
|
|
153
|
+
await store.catchUp(group, groupLogName(group, channel));
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
// ── Archive ────────────────────────────────────────────────────────────
|
|
126
157
|
|
|
127
158
|
/** A deleted channel archives the agent or group that owned it. */
|
|
128
|
-
channelDeleted(channelId: string): Promise<void> {
|
|
129
|
-
|
|
159
|
+
async channelDeleted(channelId: string): Promise<void> {
|
|
160
|
+
const { store } = this.#ctx.options;
|
|
161
|
+
const agent = store.agentByChannel(channelId);
|
|
162
|
+
if (agent) return this.#archiveAgent(agent);
|
|
163
|
+
const group = store.groupByChannel(channelId);
|
|
164
|
+
if (group) await this.#archiveGroup(group);
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
/**
|
|
133
168
|
* Archives an agent or group like a deleted channel, then keeps its channel under Archive,
|
|
134
|
-
* where no one owns it and so nothing answers
|
|
135
|
-
*
|
|
169
|
+
* where no one owns it and so nothing answers; an agent or group without a Discord channel
|
|
170
|
+
* only stops answering in its rooms. Runs at once, not in the target's queue: the caller may be
|
|
171
|
+
* a member of the group it archives, and the round stops at its next turn.
|
|
136
172
|
*/
|
|
137
173
|
async archive(caller: AgentTurnScope, name: string): Promise<string> {
|
|
138
|
-
const { store,
|
|
174
|
+
const { store, channels, logger } = this.#ctx.options;
|
|
139
175
|
const agent = store.agent(name);
|
|
140
176
|
const group = agent ? undefined : store.group(name);
|
|
141
177
|
const target = agent ?? group;
|
|
@@ -145,13 +181,15 @@ export class TeamLifecycle {
|
|
|
145
181
|
);
|
|
146
182
|
if (target.status !== "active")
|
|
147
183
|
throw new AgentError(`"${name}" is already archived.`);
|
|
148
|
-
if (agent &&
|
|
184
|
+
if (agent && coordinatorOf(store, this.#ctx.options)?.name === name)
|
|
149
185
|
throw new AgentError("The coordinator cannot be archived.");
|
|
150
186
|
if (agent && name === caller.name)
|
|
151
187
|
throw new AgentError("You cannot archive yourself; ask another agent.");
|
|
152
|
-
const channelId = target
|
|
153
|
-
if (
|
|
154
|
-
await this.#
|
|
188
|
+
const { channelId } = target;
|
|
189
|
+
if (agent) await this.#archiveAgent(agent);
|
|
190
|
+
else if (group) await this.#archiveGroup(group);
|
|
191
|
+
if (!channelId || !channels)
|
|
192
|
+
return `Archived "${target.displayName}"; it no longer answers anywhere.`;
|
|
155
193
|
try {
|
|
156
194
|
await channels.removeWebhook(channelId);
|
|
157
195
|
} catch (error) {
|
|
@@ -169,41 +207,70 @@ export class TeamLifecycle {
|
|
|
169
207
|
return `Archived "${target.displayName}"; its channel <#${channelId}> is kept under Archive.`;
|
|
170
208
|
}
|
|
171
209
|
|
|
172
|
-
async #
|
|
210
|
+
async #archiveAgent(agent: Agent): Promise<void> {
|
|
173
211
|
const { store, schedules, logger } = this.#ctx.options;
|
|
174
212
|
const runtime = this.#ctx.options.runtime();
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const schedule of await schedules.forChannel(
|
|
180
|
-
await schedules.remove(schedule.id,
|
|
181
|
-
await runtime.startFresh(
|
|
182
|
-
for (const group of left) {
|
|
183
|
-
await runtime.startFresh(groupSessionKey(group, agent.name));
|
|
184
|
-
if (group.status === "active") this.retitle(group);
|
|
185
|
-
}
|
|
186
|
-
logger.info(
|
|
187
|
-
{ agent: agent.name, groups: left.map((g) => g.name) },
|
|
188
|
-
"agent archived",
|
|
189
|
-
);
|
|
190
|
-
this.#ctx.changed();
|
|
191
|
-
return;
|
|
213
|
+
const rooms = this.#rooms();
|
|
214
|
+
const left = await store.archiveAgent(agent.name);
|
|
215
|
+
// The agent's Discord channel and each of its rooms are conversations of their own.
|
|
216
|
+
for (const key of keysOf(store, agent.name, rooms)) {
|
|
217
|
+
for (const schedule of await schedules.forChannel(key))
|
|
218
|
+
await schedules.remove(schedule.id, key);
|
|
219
|
+
await runtime.startFresh(key);
|
|
192
220
|
}
|
|
193
|
-
const group
|
|
194
|
-
|
|
221
|
+
for (const group of left) {
|
|
222
|
+
for (const seat of this.#seats(group, agent.name, rooms))
|
|
223
|
+
await runtime.startFresh(seat);
|
|
224
|
+
if (group.status === "active") this.retitle(group);
|
|
225
|
+
}
|
|
226
|
+
logger.info(
|
|
227
|
+
{ agent: agent.name, groups: left.map((g) => g.name) },
|
|
228
|
+
"agent archived",
|
|
229
|
+
);
|
|
230
|
+
this.#ctx.changed();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async #archiveGroup(group: AgentGroup): Promise<void> {
|
|
234
|
+
const { store, logger } = this.#ctx.options;
|
|
235
|
+
const runtime = this.#ctx.options.runtime();
|
|
236
|
+
const rooms = this.#rooms();
|
|
195
237
|
await store.archiveGroup(group.name);
|
|
196
238
|
for (const member of group.members)
|
|
197
|
-
|
|
239
|
+
for (const seat of this.#seats(group, member, rooms))
|
|
240
|
+
await runtime.startFresh(seat);
|
|
198
241
|
logger.info({ group: group.name }, "group archived");
|
|
199
242
|
this.#ctx.changed();
|
|
200
243
|
}
|
|
201
244
|
|
|
202
|
-
/**
|
|
245
|
+
/** The surfaces that host rooms; none for a team without a surface port. */
|
|
246
|
+
#rooms(): readonly string[] {
|
|
247
|
+
return this.#ctx.options.surfaces?.roomSurfaces() ?? [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Every conversation of an agent's seat in a group: the Discord channel's, when the group has
|
|
252
|
+
* one, then each room's.
|
|
253
|
+
*/
|
|
254
|
+
#seats(
|
|
255
|
+
group: AgentGroup,
|
|
256
|
+
agent: string,
|
|
257
|
+
rooms: readonly string[],
|
|
258
|
+
): ChannelKey[] {
|
|
259
|
+
return [
|
|
260
|
+
...(group.channelId ? [groupSessionKey(group, agent)] : []),
|
|
261
|
+
...rooms.map((surface) =>
|
|
262
|
+
groupSessionKey(group, agent, groupRoomKey(surface, group.name)),
|
|
263
|
+
),
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Writes a group's members and host into its channel topic, without waiting on Discord; a group without a channel has no topic. */
|
|
203
268
|
retitle(group: AgentGroup): void {
|
|
204
269
|
const { channels, store, logger } = this.#ctx.options;
|
|
270
|
+
const { channelId } = group;
|
|
271
|
+
if (!channels || !channelId) return;
|
|
205
272
|
channels
|
|
206
|
-
.setTopic(
|
|
273
|
+
.setTopic(channelId, groupTopic(group, store))
|
|
207
274
|
.catch((error: unknown) =>
|
|
208
275
|
logger.warn(
|
|
209
276
|
{ group: group.name, err: error },
|
|
@@ -9,7 +9,10 @@ import type { TeamTurns, TeamTurnsOptions } from "./team-turns.ts";
|
|
|
9
9
|
|
|
10
10
|
export interface AgentTeamOptions
|
|
11
11
|
extends Omit<TeamTurnsOptions, "selection" | "changed"> {
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The team's id: the Discord guild, or `LOCAL_TEAM_ID` on a host without Discord. It scopes the
|
|
14
|
+
* rows the team keeps.
|
|
15
|
+
*/
|
|
13
16
|
guildId: string;
|
|
14
17
|
/** The first team, read and inserted once on start; an agent already stored is never overwritten. */
|
|
15
18
|
seeds: () => readonly AgentSeed[];
|
|
@@ -17,13 +20,17 @@ export interface AgentTeamOptions
|
|
|
17
20
|
promptSections?: () => readonly PromptSection[];
|
|
18
21
|
/** The tools plugins defined for agents, read before each agent turn. */
|
|
19
22
|
pluginTools?: () => readonly string[];
|
|
20
|
-
channels: AgentChannels;
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* The Discord channels the agents live in; absent on a host without Discord. Then no channel is
|
|
25
|
+
* made, arranged or read, and the agents live in the rooms of the surfaces that host them.
|
|
26
|
+
*/
|
|
27
|
+
channels?: AgentChannels;
|
|
28
|
+
/**
|
|
29
|
+
* Agents are composed characters, rendered by `render`. `canDraw: false` marks a host without
|
|
30
|
+
* an image provider, which is not told about generate_image; a studio that leaves it out has one.
|
|
24
31
|
*/
|
|
25
|
-
studio: Pick<AvatarStudio, "url" | "draw" | "edit"> &
|
|
26
|
-
Partial<Pick<AvatarStudio, "canDraw"
|
|
32
|
+
studio: Pick<AvatarStudio, "url" | "render" | "draw" | "edit"> &
|
|
33
|
+
Partial<Pick<AvatarStudio, "canDraw">>;
|
|
27
34
|
/** The tools and groups the plugins give every agent, read before each agent turn. */
|
|
28
35
|
pluginSelection: () => ToolSelection;
|
|
29
36
|
models: AgentModels;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingSetting } from "../models.ts";
|
|
2
2
|
import type { ContextUse } from "./agent-ports.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { discordIdOf, keysOf } from "./team-keys.ts";
|
|
4
4
|
import type { TeamContext } from "./team-options.ts";
|
|
5
5
|
|
|
6
6
|
/** An active agent as the dashboard shows it. */
|
|
@@ -12,10 +12,12 @@ export interface AgentStatus {
|
|
|
12
12
|
thinking: ThinkingSetting;
|
|
13
13
|
/** The channel of its running turn: its own, a group's, or where it answers a message. */
|
|
14
14
|
workingIn?: string;
|
|
15
|
-
/** Turns waiting in its own channel. */
|
|
15
|
+
/** Turns waiting in its own channel and in each of its rooms. */
|
|
16
16
|
waiting: number;
|
|
17
|
+
/** The context use of its conversation that has one: its Discord channel's, else the first room's. */
|
|
17
18
|
context?: ContextUse;
|
|
18
19
|
lastActive?: Date;
|
|
20
|
+
/** Its schedules, across its Discord channel and its rooms. */
|
|
19
21
|
schedules: number;
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -23,10 +25,11 @@ export interface AgentStatus {
|
|
|
23
25
|
export interface GroupStatus {
|
|
24
26
|
name: string;
|
|
25
27
|
displayName: string;
|
|
26
|
-
|
|
28
|
+
/** Its Discord channel; absent in a team without Discord. */
|
|
29
|
+
channelId?: string;
|
|
27
30
|
members: string[];
|
|
28
31
|
host: string;
|
|
29
|
-
/** Messages of its channel running or waiting, a round counting as one. */
|
|
32
|
+
/** Messages of its channel and rooms running or waiting, a round counting as one. */
|
|
30
33
|
busy: number;
|
|
31
34
|
lastActive?: Date;
|
|
32
35
|
}
|
|
@@ -41,24 +44,36 @@ export async function teamStatus(ctx: TeamContext): Promise<TeamStatus> {
|
|
|
41
44
|
const { options, turns } = ctx;
|
|
42
45
|
const { store, queue, schedules } = options;
|
|
43
46
|
const runtime = options.runtime();
|
|
47
|
+
const rooms = options.surfaces?.roomSurfaces() ?? [];
|
|
44
48
|
const agents: AgentStatus[] = [];
|
|
45
49
|
for (const agent of store.agents()) {
|
|
46
50
|
if (agent.status !== "active") continue;
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
+
// Its Discord channel and each room are conversations of their own, and all count.
|
|
52
|
+
const keys = keysOf(store, agent.name, rooms);
|
|
53
|
+
const working = turns.workingIn(agent.name);
|
|
54
|
+
// The board links Discord channels; a turn in a room has none to link.
|
|
55
|
+
const workingIn = working === undefined ? undefined : discordIdOf(working);
|
|
56
|
+
const queued = keys.reduce((sum, key) => sum + queue.size(key), 0);
|
|
57
|
+
const context = keys
|
|
58
|
+
.map((key) => runtime.contextUsage?.(key))
|
|
59
|
+
.find((use) => use !== undefined);
|
|
60
|
+
let scheduled = 0;
|
|
61
|
+
for (const key of keys)
|
|
62
|
+
scheduled += (await schedules.forChannel(key)).length;
|
|
51
63
|
const lastActive = turns.lastActive(agent.name);
|
|
52
64
|
agents.push({
|
|
53
65
|
name: agent.name,
|
|
54
66
|
displayName: agent.displayName,
|
|
55
67
|
...(agent.channelId ? { channelId: agent.channelId } : {}),
|
|
56
68
|
...ctx.modelOf(agent.name),
|
|
57
|
-
...(workingIn ? { workingIn
|
|
58
|
-
waiting: Math.max(
|
|
69
|
+
...(workingIn ? { workingIn } : {}),
|
|
70
|
+
waiting: Math.max(
|
|
71
|
+
0,
|
|
72
|
+
queued - (working !== undefined && keys.includes(working) ? 1 : 0),
|
|
73
|
+
),
|
|
59
74
|
...(context ? { context } : {}),
|
|
60
75
|
...(lastActive ? { lastActive } : {}),
|
|
61
|
-
schedules:
|
|
76
|
+
schedules: scheduled,
|
|
62
77
|
});
|
|
63
78
|
}
|
|
64
79
|
const groups: GroupStatus[] = store.groups().flatMap((group) => {
|
|
@@ -68,10 +83,13 @@ export async function teamStatus(ctx: TeamContext): Promise<TeamStatus> {
|
|
|
68
83
|
{
|
|
69
84
|
name: group.name,
|
|
70
85
|
displayName: group.displayName,
|
|
71
|
-
channelId: group.channelId,
|
|
86
|
+
...(group.channelId ? { channelId: group.channelId } : {}),
|
|
72
87
|
members: group.members.map((m) => store.agent(m)?.displayName ?? m),
|
|
73
88
|
host: store.agent(group.host)?.displayName ?? group.host,
|
|
74
|
-
busy:
|
|
89
|
+
busy: keysOf(store, group.name, rooms).reduce(
|
|
90
|
+
(sum, key) => sum + queue.size(key),
|
|
91
|
+
0,
|
|
92
|
+
),
|
|
75
93
|
...(lastActive ? { lastActive } : {}),
|
|
76
94
|
},
|
|
77
95
|
];
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { CATALOG, type CharacterSpec } from "../../characters/index.ts";
|
|
1
2
|
import { AgentError } from "../domain/errors.ts";
|
|
2
3
|
import { assistantName } from "../i18n/index.ts";
|
|
3
4
|
import type { ThinkingSetting } from "../models.ts";
|
|
4
5
|
import type { AgentDirectory } from "../services.ts";
|
|
5
6
|
import { zonedStamp } from "../time.ts";
|
|
6
7
|
import type { ChannelMessage } from "./agent-ports.ts";
|
|
8
|
+
import { characterOf } from "./agent-rules.ts";
|
|
7
9
|
import { describeThinking } from "./agent-settings.ts";
|
|
8
10
|
import type { Agent } from "./agent-store.ts";
|
|
9
11
|
|
|
12
|
+
/** Said by the tools that work on Discord channels when the team has none. */
|
|
13
|
+
export const NO_CHANNELS =
|
|
14
|
+
"This host has no Discord channels: the agents live in rooms of a chat surface, so there is nothing to arrange or read here.";
|
|
15
|
+
|
|
10
16
|
/** One message as channel_read shows it: id, local time, author, text, attachments. */
|
|
11
17
|
export function messageLine(message: ChannelMessage): string {
|
|
12
18
|
const files =
|
|
@@ -36,17 +42,21 @@ export function teamListText(store: Pick<AgentDirectory, "agents" | "groups">) {
|
|
|
36
42
|
.groups()
|
|
37
43
|
.map(
|
|
38
44
|
(g) =>
|
|
39
|
-
`- ${g.name} "${g.displayName}" —
|
|
45
|
+
`- ${g.name} "${g.displayName}" — ${g.channelId ? `<#${g.channelId}>` : "no channel"}, ${g.status}; members ${g.members.join(", ")}; host ${g.host}`,
|
|
40
46
|
);
|
|
41
47
|
return `Agents:\n${agents.join("\n") || "none"}\n\nGroups:\n${groups.join("\n") || "none"}`;
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
/** A look in a few words: the class label, then the palette or the primary color. */
|
|
51
|
+
export function lookText(character: CharacterSpec): string {
|
|
52
|
+
return `${CATALOG.classes[character.class].label}, ${character.palette ?? character.colors.primary}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/** One agent as agent_get shows it; unset settings name the assistant's, and no skills line while the skills addon is off. */
|
|
45
56
|
export function agentDetails(
|
|
46
57
|
agent: Agent,
|
|
47
58
|
defaults: { model: string; thinking: ThinkingSetting },
|
|
48
59
|
skills: string | undefined,
|
|
49
|
-
avatars = true,
|
|
50
60
|
): string {
|
|
51
61
|
return [
|
|
52
62
|
`Name: ${agent.name}`,
|
|
@@ -56,7 +66,7 @@ export function agentDetails(
|
|
|
56
66
|
`Model: ${agent.model ?? `${assistantName()}'s (${defaults.model})`}`,
|
|
57
67
|
`Thinking: ${agent.thinking ?? `${assistantName()}'s (${describeThinking(defaults.thinking)})`}`,
|
|
58
68
|
...(skills === undefined ? [] : [`Skills: ${skills}`]),
|
|
59
|
-
|
|
69
|
+
`Look: ${lookText(characterOf(agent))}`,
|
|
60
70
|
`Prompt:\n${agent.prompt}`,
|
|
61
71
|
].join("\n");
|
|
62
72
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { QueuePort } from "../contract/channels.ts";
|
|
2
|
-
import type {
|
|
3
|
-
DispatchThread,
|
|
4
|
-
DispatchThreads,
|
|
5
|
-
} from "../discord/dispatch-threads.ts";
|
|
2
|
+
import type { SurfacePort } from "../contract/surface.ts";
|
|
6
3
|
import type { TurnAttachments } from "../domain/attachment.ts";
|
|
7
4
|
import type {
|
|
8
5
|
ChannelKey,
|
|
@@ -16,6 +13,10 @@ import type { EventSink } from "../plugin.ts";
|
|
|
16
13
|
import type { AvatarStudio } from "../services.ts";
|
|
17
14
|
import type { TurnSelection } from "../sessions.ts";
|
|
18
15
|
import type { Speaker } from "../speakers.ts";
|
|
16
|
+
import type {
|
|
17
|
+
DispatchThread,
|
|
18
|
+
DispatchThreads,
|
|
19
|
+
} from "../threads/dispatch-threads.ts";
|
|
19
20
|
import type { ToolTiers } from "../tool-tiers.ts";
|
|
20
21
|
import type { AgentChannels, AgentTurnRunner } from "./agent-ports.ts";
|
|
21
22
|
import type { Agent, PgAgentStore } from "./agent-store.ts";
|
|
@@ -39,12 +40,29 @@ export interface Exchange {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface TeamTurnsOptions {
|
|
42
|
-
/** The agent server's entry channel, where the coordinator lives. */
|
|
43
|
-
entryChannelId
|
|
43
|
+
/** The agent server's entry channel, where the coordinator lives; absent on a host without Discord. */
|
|
44
|
+
entryChannelId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The coordinator's name on a host without Discord, where there is no entry channel to find it
|
|
47
|
+
* by; a function is asked each time, for a name that is only known once the plugins linked.
|
|
48
|
+
* Ignored when `entryChannelId` is set.
|
|
49
|
+
*/
|
|
50
|
+
coordinator?: string | (() => string | undefined);
|
|
44
51
|
/** Who the agents work for, as their prompts and group history name them. */
|
|
45
52
|
owner: OwnerIdentity;
|
|
46
53
|
store: PgAgentStore;
|
|
47
|
-
channels
|
|
54
|
+
/** The Discord channels the agents post in; absent on a host without Discord, where every reply goes through a room's surface. */
|
|
55
|
+
channels?: Pick<AgentChannels, "post" | "interim">;
|
|
56
|
+
/**
|
|
57
|
+
* The surfaces that host agent rooms. A reply in a room goes through `sendReply` with the
|
|
58
|
+
* agent as author, and its interim posts through `interim`, never through the Discord
|
|
59
|
+
* channels; without it no key but Discord's is the team's. `of` tells whether the room's
|
|
60
|
+
* surface carries reply files.
|
|
61
|
+
*/
|
|
62
|
+
surfaces?: Pick<
|
|
63
|
+
SurfacePort,
|
|
64
|
+
"sendReply" | "interim" | "roomSurfaces" | "of" | "progress"
|
|
65
|
+
>;
|
|
48
66
|
studio: Pick<AvatarStudio, "url">;
|
|
49
67
|
/** Set once the runtime exists, which itself needs the team's tools. */
|
|
50
68
|
runtime: () => AgentTurnRunner;
|