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.
Files changed (129) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +35 -9
  3. package/README.zh-TW.md +51 -13
  4. package/docs/migrating-0.8.md +2 -2
  5. package/docs/plugins.md +350 -28
  6. package/examples/discord-adapter.ts +9 -1
  7. package/examples/seeds.test.ts +2 -2
  8. package/examples/seeds.ts +1 -2
  9. package/package.json +6 -2
  10. package/src/characters/catalog.ts +1055 -0
  11. package/src/characters/characters.css +68 -0
  12. package/src/characters/colors.ts +108 -0
  13. package/src/characters/compose.ts +281 -0
  14. package/src/characters/css.ts +63 -0
  15. package/src/characters/index.ts +33 -0
  16. package/src/characters/parse.ts +196 -0
  17. package/src/characters/parts-face.ts +354 -0
  18. package/src/characters/parts-hair.ts +56 -0
  19. package/src/characters/parts-hats.ts +57 -0
  20. package/src/characters/parts-outfits.ts +81 -0
  21. package/src/characters/parts-props.ts +137 -0
  22. package/src/characters/spec.ts +224 -0
  23. package/src/characters/suggest.ts +101 -0
  24. package/src/cli/checks/characters.ts +23 -0
  25. package/src/cli/checks/images.ts +5 -3
  26. package/src/cli/doctor.ts +2 -0
  27. package/src/core/agents/agent-claim.ts +30 -11
  28. package/src/core/agents/agent-dashboard.ts +1 -1
  29. package/src/core/agents/agent-messages.ts +43 -15
  30. package/src/core/agents/agent-prompt.ts +2 -4
  31. package/src/core/agents/agent-rules.ts +42 -3
  32. package/src/core/agents/agent-schema.ts +37 -15
  33. package/src/core/agents/agent-store.ts +57 -13
  34. package/src/core/agents/agent-team-fixture.ts +90 -14
  35. package/src/core/agents/agent-team.ts +83 -50
  36. package/src/core/agents/agent-tools.ts +94 -41
  37. package/src/core/agents/avatar-studio.ts +40 -4
  38. package/src/core/agents/character-png.ts +40 -0
  39. package/src/core/agents/group-messages.ts +7 -4
  40. package/src/core/agents/group-turns.ts +27 -14
  41. package/src/core/agents/team-editing.ts +99 -58
  42. package/src/core/agents/team-keys.ts +177 -11
  43. package/src/core/agents/team-lifecycle.ts +151 -84
  44. package/src/core/agents/team-options.ts +13 -6
  45. package/src/core/agents/team-status.ts +31 -13
  46. package/src/core/agents/team-text.ts +13 -3
  47. package/src/core/agents/team-turn-types.ts +25 -7
  48. package/src/core/agents/team-turns.ts +143 -28
  49. package/src/core/attachments/attachment-fetcher.ts +162 -64
  50. package/src/core/attachments/prompt-block.ts +7 -1
  51. package/src/core/attachments/tool-attachment.ts +20 -1
  52. package/src/core/attachments/turn-attachments.ts +18 -2
  53. package/src/core/builtin/agent-server.ts +103 -40
  54. package/src/core/builtin/discord.ts +4 -17
  55. package/src/core/builtin/modules.ts +6 -5
  56. package/src/core/builtin/threads.ts +37 -0
  57. package/src/core/config/agent-team.ts +43 -0
  58. package/src/core/config/config.ts +50 -59
  59. package/src/core/config/limits.ts +93 -0
  60. package/src/core/config/ops.ts +34 -0
  61. package/src/core/config/schema.ts +26 -0
  62. package/src/core/contract/channels.ts +20 -0
  63. package/src/core/contract/surface.ts +77 -7
  64. package/src/core/define-roundtable.ts +70 -42
  65. package/src/core/discord/agent-commands.ts +108 -86
  66. package/src/core/discord/agent-panel.ts +11 -2
  67. package/src/core/discord/connection.ts +1 -1
  68. package/src/core/discord/discord-surface.ts +10 -1
  69. package/src/core/discord/dispatch-thread-host.ts +1 -1
  70. package/src/core/domain/conversation.ts +24 -0
  71. package/src/core/holds.ts +221 -15
  72. package/src/core/host-registration.ts +2 -1
  73. package/src/core/http/listeners.ts +63 -3
  74. package/src/core/i18n/agent-panel.ts +14 -21
  75. package/src/core/interactions/prompts.ts +12 -0
  76. package/src/core/modes/autonomy.ts +23 -0
  77. package/src/core/modes/conversation-modes.ts +213 -0
  78. package/src/core/modes/modes-plugin.ts +24 -0
  79. package/src/core/modules/delegation/delegator.ts +4 -4
  80. package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
  81. package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
  82. package/src/core/modules/host-shell/shell-lexer.ts +493 -0
  83. package/src/core/modules/host-shell/shell-paths.ts +85 -4
  84. package/src/core/modules/host-shell/shell-policy.ts +341 -332
  85. package/src/core/modules/host-shell/shell-programs.ts +298 -0
  86. package/src/core/modules/host-shell/shell-quoting.ts +237 -0
  87. package/src/core/modules/host-shell/shell-scripts.ts +114 -0
  88. package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
  89. package/src/core/modules/schedules/precheck-tools.ts +17 -0
  90. package/src/core/modules/schedules/schedules.ts +9 -3
  91. package/src/core/plugin.ts +8 -1
  92. package/src/core/presence/direct-channels.ts +30 -0
  93. package/src/core/presentation/reply-body.ts +25 -0
  94. package/src/core/registry/contributions.ts +31 -7
  95. package/src/core/registry/providers.ts +1 -1
  96. package/src/core/routing/conversation-turns.ts +2 -15
  97. package/src/core/routing/surface-port.ts +5 -1
  98. package/src/core/runtime/extensions/approval-card.ts +1 -5
  99. package/src/core/runtime/extensions/approval-fields.ts +3 -7
  100. package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
  101. package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
  102. package/src/core/runtime/extensions/gate-refusals.ts +23 -0
  103. package/src/core/runtime/extensions/held-calls.ts +16 -1
  104. package/src/core/runtime/extensions/hold-describer.ts +58 -8
  105. package/src/core/runtime/extensions/hold-lookups.ts +19 -19
  106. package/src/core/runtime/extensions/yolo.ts +105 -0
  107. package/src/core/runtime/pi-agent-runtime.ts +3 -5
  108. package/src/core/runtime/runtime-plugin.ts +42 -1
  109. package/src/core/runtime/runtime-types.ts +11 -0
  110. package/src/core/runtime/turn-budget.ts +32 -5
  111. package/src/core/runtime/turn-limits.ts +14 -3
  112. package/src/core/services.ts +61 -9
  113. package/src/core/shared/attachment-reader.ts +6 -0
  114. package/src/core/shared/unix-server.ts +10 -1
  115. package/src/core/testing/modules.ts +12 -8
  116. package/src/core/testing/surface-threads.ts +58 -0
  117. package/src/core/testing/thread-host.ts +17 -5
  118. package/src/core/threads/dispatch-threads.ts +299 -0
  119. package/src/core/threads/host-threads.ts +19 -0
  120. package/src/core/tool-set.snapshot.json +5 -0
  121. package/src/index.ts +27 -1
  122. package/src/kit/characters.ts +5 -0
  123. package/src/kit/index.ts +2 -0
  124. package/src/kit/shell.ts +1 -0
  125. package/src/kit/support.ts +1 -1
  126. package/src/kit/threads.ts +1 -1
  127. package/src/testing.ts +4 -0
  128. package/templates/agents.ts +1 -1
  129. 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 { AgentGroup } from "./agent-store.ts";
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
- channelIdOf,
7
- discordKey,
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 draws missing avatars in the
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
- if (group.status !== "active") continue;
53
- if (!(await channels.exists(group.channelId))) {
54
- await this.channelDeleted(group.channelId);
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(group.channelId));
83
+ const home = layout.find((c) => c.channelIds.includes(channelId));
59
84
  if (home && categoryKind(home.name) === "Groups") continue;
60
- if (await channels.placeIn(group.channelId, "Groups"))
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
- * Without an image provider, gives each active agent that has no picture the one made from
84
- * its display name, and says so once instead of once per agent on every boot.
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 #generateMissingAvatars(): Promise<void> {
87
- const { store, studio, logger } = this.#ctx.options;
88
- if (!studio.fallback) return;
89
- let generated = 0;
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 (agent.status !== "active" || agent.avatarHash) continue;
106
+ if (
107
+ agent.status !== "active" ||
108
+ agent.character ||
109
+ store.hasStoredCharacter(agent.name)
110
+ )
111
+ continue;
92
112
  try {
93
- const avatarHash = await studio.fallback(agent.displayName, agent.name);
94
- await store.updateAgent(agent.name, { avatarHash });
95
- generated++;
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 }, "avatar not generated");
128
+ logger.warn({ agent: agent.name, err: error }, "agent not dressed");
98
129
  }
99
130
  }
100
- if (generated > 0)
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
- const id = channelIdOf(channel);
112
- const agent = store.agentByChannel(id);
113
- if (agent) {
114
- await runtime.startFresh(homeScope(agent).session);
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.groupByChannel(id);
119
- if (!group) return;
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
- return this.#archive(channelId);
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. Runs at once, not in the target's queue: the
135
- * caller may be a member of the group it archives, and the round stops at its next turn.
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, entryChannelId, channels, logger } = this.#ctx.options;
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 && agent.channelId === entryChannelId)
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.channelId;
153
- if (!channelId) throw new AgentError(`${name} has no channel.`);
154
- await this.#archive(channelId);
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 #archive(channelId: string): Promise<void> {
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 agent = store.agentByChannel(channelId);
176
- if (agent) {
177
- const left = await store.archiveAgent(agent.name);
178
- const home = discordKey(channelId);
179
- for (const schedule of await schedules.forChannel(home))
180
- await schedules.remove(schedule.id, home);
181
- await runtime.startFresh(home);
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 = store.groupByChannel(channelId);
194
- if (!group) return;
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
- await runtime.startFresh(groupSessionKey(group, member));
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
- /** Writes a group's members and host into its channel topic, without waiting on Discord. */
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(group.channelId, groupTopic(group, store))
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
- /** The agent server; its entry channel is where the coordinator lives. */
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
- * `canDraw: false` marks a host without an image provider: agents are offered no drawing and
23
- * get `fallback` pictures. A studio that leaves `canDraw` out draws.
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" | "fallback">>;
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 { channelIdOf, discordKey } from "./team-keys.ts";
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
- channelId: string;
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
- const home = agent.channelId ? discordKey(agent.channelId) : undefined;
48
- const workingIn = turns.workingIn(agent.name);
49
- const queued = home ? queue.size(home) : 0;
50
- const context = home ? runtime.contextUsage?.(home) : undefined;
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: channelIdOf(workingIn) } : {}),
58
- waiting: Math.max(0, queued - (workingIn === home ? 1 : 0)),
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: home ? (await schedules.forChannel(home)).length : 0,
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: queue.size(discordKey(group.channelId)),
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}" — <#${g.channelId}>, ${g.status}; members ${g.members.join(", ")}; host ${g.host}`,
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
- ...(avatars ? [`Avatar prompt: ${agent.avatarPrompt}`] : []),
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: string;
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: Pick<AgentChannels, "post" | "interim">;
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;