pi-roundtable 0.22.4 → 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 (132) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +36 -10
  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 +7 -3
  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/http/websocket.ts +2 -1
  75. package/src/core/i18n/agent-panel.ts +14 -21
  76. package/src/core/interactions/prompts.ts +12 -0
  77. package/src/core/modes/autonomy.ts +23 -0
  78. package/src/core/modes/conversation-modes.ts +213 -0
  79. package/src/core/modes/modes-plugin.ts +24 -0
  80. package/src/core/modules/delegation/delegator.ts +4 -4
  81. package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
  82. package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
  83. package/src/core/modules/host-shell/shell-lexer.ts +493 -0
  84. package/src/core/modules/host-shell/shell-paths.ts +85 -4
  85. package/src/core/modules/host-shell/shell-policy.ts +341 -332
  86. package/src/core/modules/host-shell/shell-programs.ts +298 -0
  87. package/src/core/modules/host-shell/shell-quoting.ts +237 -0
  88. package/src/core/modules/host-shell/shell-scripts.ts +114 -0
  89. package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
  90. package/src/core/modules/schedules/precheck-tools.ts +17 -0
  91. package/src/core/modules/schedules/schedules.ts +9 -3
  92. package/src/core/plugin.ts +8 -1
  93. package/src/core/presence/direct-channels.ts +30 -0
  94. package/src/core/presentation/reply-body.ts +25 -0
  95. package/src/core/registry/contributions.ts +31 -7
  96. package/src/core/registry/providers.ts +1 -1
  97. package/src/core/routing/conversation-turns.ts +2 -15
  98. package/src/core/routing/surface-port.ts +5 -1
  99. package/src/core/runtime/extensions/approval-card.ts +1 -5
  100. package/src/core/runtime/extensions/approval-fields.ts +3 -7
  101. package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
  102. package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
  103. package/src/core/runtime/extensions/gate-refusals.ts +23 -0
  104. package/src/core/runtime/extensions/held-calls.ts +16 -1
  105. package/src/core/runtime/extensions/hold-describer.ts +58 -8
  106. package/src/core/runtime/extensions/hold-lookups.ts +19 -19
  107. package/src/core/runtime/extensions/yolo.ts +105 -0
  108. package/src/core/runtime/pi-agent-runtime.ts +3 -5
  109. package/src/core/runtime/runtime-plugin.ts +42 -1
  110. package/src/core/runtime/runtime-types.ts +11 -0
  111. package/src/core/runtime/turn-budget.ts +32 -5
  112. package/src/core/runtime/turn-limits.ts +14 -3
  113. package/src/core/services.ts +61 -9
  114. package/src/core/shared/attachment-reader.ts +6 -0
  115. package/src/core/shared/unix-server.ts +10 -1
  116. package/src/core/testing/modules.ts +12 -8
  117. package/src/core/testing/surface-threads.ts +58 -0
  118. package/src/core/testing/thread-host.ts +17 -5
  119. package/src/core/threads/dispatch-threads.ts +299 -0
  120. package/src/core/threads/host-threads.ts +19 -0
  121. package/src/core/tool-set.snapshot.json +5 -0
  122. package/src/index.ts +27 -1
  123. package/src/kit/characters.ts +5 -0
  124. package/src/kit/index.ts +2 -0
  125. package/src/kit/shell.ts +1 -0
  126. package/src/kit/support.ts +1 -1
  127. package/src/kit/threads.ts +1 -1
  128. package/src/testing.ts +4 -0
  129. package/templates/adapters/web/package.json.tmpl +1 -1
  130. package/templates/agents.ts +1 -1
  131. package/templates/package.json.tmpl +1 -1
  132. package/src/core/discord/dispatch-threads.ts +0 -194
@@ -4,10 +4,13 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { SQL } from "bun";
6
6
  import type { ContextUse } from "../contract/runtime.ts";
7
+ import type { ChatSurface } from "../contract/surface.ts";
7
8
  import { NO_ATTACHMENTS } from "../domain/attachment.ts";
8
9
  import type {
9
10
  ChannelKey,
11
+ OutboundReply,
10
12
  PendingConfirmation,
13
+ PostAuthor,
11
14
  TurnResult,
12
15
  } from "../domain/conversation.ts";
13
16
  import { AgentRunError } from "../domain/errors.ts";
@@ -36,7 +39,10 @@ import type {
36
39
  } from "./agent-ports.ts";
37
40
  import type { AgentSeed } from "./agent-rules.ts";
38
41
  import { PgAgentStore as AgentStore } from "./agent-store.ts";
39
- import { DiscordAgentTeam as AgentTeam } from "./agent-team.ts";
42
+ import {
43
+ DiscordAgentTeam as AgentTeam,
44
+ type AgentTeamOptions,
45
+ } from "./agent-team.ts";
40
46
  import { discordKey } from "./team-keys.ts";
41
47
 
42
48
  export const ENTRY = "1000";
@@ -124,6 +130,50 @@ export class FakeChannels implements AgentChannels {
124
130
  }
125
131
  }
126
132
 
133
+ /**
134
+ * The surfaces a team replies through in rooms. `rooms` names the surfaces that host rooms and
135
+ * starts empty, so the Discord suites run as if no surface did; a room suite sets it.
136
+ */
137
+ class FakeSurfaces {
138
+ rooms: string[] = [];
139
+ replies: { channel: ChannelKey; reply: OutboundReply }[] = [];
140
+ /** Interim posts as `channel name/agent: text`, edits included. */
141
+ interims: string[] = [];
142
+ roomSurfaces() {
143
+ return this.rooms;
144
+ }
145
+ /** A room's surface carries no files, as a surface that does not declare `supportsFiles`. */
146
+ of(_channel: ChannelKey): ChatSurface | undefined {
147
+ return undefined;
148
+ }
149
+ async sendReply(channel: ChannelKey, reply: OutboundReply) {
150
+ this.replies.push({ channel, reply });
151
+ }
152
+ interim(channel: ChannelKey, as?: PostAuthor) {
153
+ if (!as) return undefined;
154
+ return {
155
+ post: async (text: string) => {
156
+ this.interims.push(`${channel} ${as.name}/${as.agent}: ${text}`);
157
+ return {
158
+ edit: async (change: string) => {
159
+ this.interims.push(
160
+ `${channel} ${as.name}/${as.agent}: edit ${change}`,
161
+ );
162
+ },
163
+ };
164
+ },
165
+ };
166
+ }
167
+ /** What the room was told, as `agent: text`. */
168
+ texts(channel: ChannelKey) {
169
+ return this.replies.flatMap((r) =>
170
+ r.channel === channel
171
+ ? [`${r.reply.author?.agent}: ${r.reply.chunks.join("")}`]
172
+ : [],
173
+ );
174
+ }
175
+ }
176
+
127
177
  /** Answers every turn with a scripted reply; records what each turn received. */
128
178
  export class FakeRuntime implements AgentTurnRunner {
129
179
  turns: TurnRequest[] = [];
@@ -166,10 +216,12 @@ export const HOOK_TIMEOUT_MS = 30_000;
166
216
  // Runs against a real PostgreSQL, only when ROUNDTABLE_TEST_DATABASE_URL is set.
167
217
  let store: TestStore<AgentStore>;
168
218
  let channels: FakeChannels;
219
+ let surfaces: FakeSurfaces;
169
220
  let runtime: FakeRuntime;
170
221
  export interface TeamFixture {
171
222
  store: TestStore<AgentStore>;
172
223
  channels: FakeChannels;
224
+ surfaces: FakeSurfaces;
173
225
  runtime: FakeRuntime;
174
226
  queue: ChannelQueue;
175
227
  team: AgentTeam;
@@ -206,29 +258,29 @@ export const eventLog: string[] = [];
206
258
  const PLUGIN_SELECTION = { tools: ["memory_add"], groups: ["workspace"] };
207
259
 
208
260
  /** The team a fresh database starts with: a coordinator in the entry channel, then two specialists. */
209
- function seeds(entryChannelId: string): AgentSeed[] {
261
+ function seeds(entryChannelId: string | undefined): AgentSeed[] {
210
262
  return [
211
263
  {
212
264
  name: "coordinator",
213
265
  displayName: "Coordinator",
214
- channelId: entryChannelId,
266
+ ...(entryChannelId ? { channelId: entryChannelId } : {}),
215
267
  prompt:
216
268
  "You are the chief of staff of the owner's agent team. You hand work that belongs to a specialist to that agent with message_agent, one owner per task, and propose a new agent when a lasting responsibility has no owner.",
217
- avatarPrompt: "A chief of staff mid-command, pointing the way.",
269
+ character: { class: "navigator", palette: "amber" },
218
270
  },
219
271
  {
220
272
  name: "doctor",
221
273
  displayName: "Doctor",
222
274
  prompt:
223
275
  "You keep the owner's agents healthy: read an agent's prompt with agent_get, find the instruction that caused a misbehavior, and fix it with agent_update.",
224
- avatarPrompt: "A bot doctor holding up a giant stethoscope.",
276
+ character: { class: "healer", palette: "rose" },
225
277
  },
226
278
  {
227
279
  name: "infra",
228
280
  displayName: "Infra",
229
281
  prompt:
230
282
  "You manage the owner's servers. Check before you change: read state first, state what you will change and why, then act.",
231
- avatarPrompt: "An infra engineer battling a server rack.",
283
+ character: { class: "engineer", palette: "purpure" },
232
284
  },
233
285
  ];
234
286
  }
@@ -247,8 +299,19 @@ export async function settle(): Promise<void> {
247
299
  await Bun.sleep(20);
248
300
  }
249
301
 
250
- /** Registers the fixture's per-test setup in the describe it is called from. */
251
- export function useTeamFixture(): void {
302
+ /**
303
+ * Registers the fixture's per-test setup in the describe it is called from. With `local`, the team
304
+ * runs as on a host without Discord: no channels and no entry channel, the coordinator named by
305
+ * the configuration, and the rooms of surface `app`.
306
+ */
307
+ export function useTeamFixture(
308
+ options: {
309
+ local?: boolean;
310
+ /** The studio the team renders with, called each test; the default fakes pictures. */
311
+ studio?: () => AgentTeamOptions["studio"];
312
+ } = {},
313
+ ): void {
314
+ const { local = false } = options;
252
315
  beforeEach(async () => {
253
316
  // The expected texts are the neutral English catalog's.
254
317
  useTestLocale();
@@ -292,6 +355,8 @@ export function useTeamFixture(): void {
292
355
  skills.init();
293
356
  await skills.link("acme/kit", "skills");
294
357
  channels = new FakeChannels();
358
+ surfaces = new FakeSurfaces();
359
+ if (local) surfaces.rooms = ["app"];
295
360
  runtime = new FakeRuntime();
296
361
  script.scores = undefined;
297
362
  script.prompt = [];
@@ -304,13 +369,19 @@ export function useTeamFixture(): void {
304
369
  owner: TEST_OWNER,
305
370
  toolTiers: FIXTURE_TIERS,
306
371
  shellUser: "mcops",
307
- guildId: "1",
308
- entryChannelId: ENTRY,
309
- seeds: () => seeds(ENTRY),
372
+ guildId: local ? "local" : "1",
373
+ ...(local
374
+ ? { coordinator: "coordinator" }
375
+ : { entryChannelId: ENTRY, channels }),
376
+ seeds: () => seeds(local ? undefined : ENTRY),
310
377
  store,
311
- channels,
312
- studio: {
378
+ surfaces,
379
+ studio: options.studio?.() ?? {
313
380
  url: (hash) => `https://x/avatars/${hash ?? "default"}.png`,
381
+ render: async (spec) =>
382
+ new Bun.CryptoHasher("sha256")
383
+ .update(JSON.stringify(spec))
384
+ .digest("hex"),
314
385
  draw: async () => "a".repeat(64),
315
386
  edit: async () => "b".repeat(64),
316
387
  },
@@ -335,7 +406,11 @@ export function useTeamFixture(): void {
335
406
  },
336
407
  schedules: {
337
408
  forChannel: async (channel) =>
338
- channel === discordKey("2000") ? [{ id: 7 }] : [],
409
+ channel === discordKey("2000")
410
+ ? [{ id: 7 }]
411
+ : channel === "app:agent.doctor"
412
+ ? [{ id: 8 }]
413
+ : [],
339
414
  remove: async (id) => removedSchedules.push(id),
340
415
  },
341
416
  queue,
@@ -353,6 +428,7 @@ export function useTeamFixture(): void {
353
428
  Object.assign(fx, {
354
429
  store,
355
430
  channels,
431
+ surfaces,
356
432
  runtime,
357
433
  queue,
358
434
  team,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+ import type { CharacterInput } from "../../characters/index.ts";
2
3
  import type { TurnAttachments } from "../domain/attachment.ts";
3
4
  import type { ChannelKey, TurnResult } from "../domain/conversation.ts";
4
5
  import { AgentError, ScheduleError } from "../domain/errors.ts";
@@ -6,7 +7,7 @@ import type { AgentTurnScope } from "../domain/ports.ts";
6
7
  import type { ThinkingSetting } from "../models.ts";
7
8
  import { SHELL_TOOLS } from "../modules/host-shell/shell-policy.ts";
8
9
  import type { ResolvedSkill } from "../modules/skills/skill-rules.ts";
9
- import type { AgentTeam } from "../services.ts";
10
+ import type { AgentChange, AgentTeam } from "../services.ts";
10
11
  import type { TurnSelection } from "../sessions.ts";
11
12
  import { type Speaker, THE_SPEAKER } from "../speakers.ts";
12
13
  import { agentSystemPrompt } from "./agent-prompt.ts";
@@ -14,13 +15,20 @@ import type { Agent } from "./agent-store.ts";
14
15
  import {
15
16
  AGENT_TOOLS,
16
17
  type AgentOps,
17
- AVATAR_TOOL,
18
18
  type AvatarMode,
19
19
  agentToolsExtension,
20
20
  MESSAGE_AGENT_TOOL,
21
21
  } from "./agent-tools.ts";
22
22
  import { TeamEditing } from "./team-editing.ts";
23
- import { channelOwner, discordKey } from "./team-keys.ts";
23
+ import {
24
+ agentRoomKey,
25
+ channelOwner,
26
+ coordinatorOf,
27
+ discordIdOf,
28
+ discordKey,
29
+ keysOf,
30
+ roomSurfaceOf,
31
+ } from "./team-keys.ts";
24
32
  import {
25
33
  layoutText,
26
34
  planArrangement,
@@ -33,6 +41,7 @@ import {
33
41
  agentDetails,
34
42
  channelIdFromRef,
35
43
  messageLine,
44
+ NO_CHANNELS,
36
45
  teamListText,
37
46
  } from "./team-text.ts";
38
47
  import { TeamTurns } from "./team-turns.ts";
@@ -94,9 +103,36 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
94
103
  return teamStatus(this.#context);
95
104
  }
96
105
 
97
- /** The agent or group that owns a channel of the agent server. */
106
+ /**
107
+ * The agent or group that owns a channel of the agent server: by Discord channel, or by room on
108
+ * a surface that hosts rooms.
109
+ */
98
110
  owns(channel: ChannelKey): "agent" | "group" | undefined {
99
- return channelOwner(this.#options.store, channel);
111
+ const { store, surfaces } = this.#options;
112
+ return channelOwner(
113
+ store,
114
+ channel,
115
+ discordIdOf(channel) === undefined
116
+ ? (surfaces?.roomSurfaces() ?? [])
117
+ : [],
118
+ );
119
+ }
120
+
121
+ /**
122
+ * The name of the coordinator: the agent in the entry channel, or, on a host without Discord,
123
+ * the one the configuration names; undefined while none is active.
124
+ */
125
+ coordinator(): string | undefined {
126
+ return coordinatorOf(this.#options.store, this.#options)?.name;
127
+ }
128
+
129
+ /**
130
+ * Every key an agent or group is addressed by: its Discord channel when it has one, then its
131
+ * room on each surface that hosts rooms; empty for an unknown name.
132
+ */
133
+ roomsOf(name: string): ChannelKey[] {
134
+ const { store, surfaces } = this.#options;
135
+ return keysOf(store, name, surfaces?.roomSurfaces() ?? []);
100
136
  }
101
137
 
102
138
  /** The tools every agent turn runs with: the plugins', plus the shell and agent tools. */
@@ -110,9 +146,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
110
146
  ...SHELL_TOOLS,
111
147
  // Group members hand off by mentioning each other, so a group seat has no message_agent.
112
148
  ...AGENT_TOOLS.filter(
113
- (tool) =>
114
- !(scope?.group && tool === MESSAGE_AGENT_TOOL) &&
115
- !(tool === AVATAR_TOOL && !this.#avatars()),
149
+ (tool) => !(scope?.group && tool === MESSAGE_AGENT_TOOL),
116
150
  ),
117
151
  ...(this.#options.pluginTools?.() ?? []),
118
152
  ]),
@@ -128,7 +162,6 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
128
162
  workDir,
129
163
  sharedPrompt,
130
164
  guestPrompt,
131
- entryChannelId,
132
165
  owner,
133
166
  shellUser,
134
167
  scratchDir,
@@ -136,7 +169,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
136
169
  const agent = store.agent(scope.name);
137
170
  if (!agent) throw new AgentError(`no agent ${scope.name}`);
138
171
  const group = scope.group ? store.group(scope.group) : undefined;
139
- const coordinator = store.agentByChannel(entryChannelId);
172
+ const coordinator = coordinatorOf(store, this.#options);
140
173
  const speaker = this.#turns.speakerOf(scope);
141
174
  const base = agentSystemPrompt({
142
175
  agent,
@@ -148,8 +181,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
148
181
  owner,
149
182
  shellUser,
150
183
  ...(scratchDir ? { scratchDir } : {}),
151
- avatars: this.#avatars(),
152
- images: this.#avatars(),
184
+ images: this.#images(),
153
185
  ...(group
154
186
  ? {
155
187
  group: {
@@ -193,15 +225,14 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
193
225
  return current ? [current, ...usable.filter((m) => m !== current)] : usable;
194
226
  }
195
227
 
196
- /** Whether agents can be offered drawing: an image provider is configured. */
197
- #avatars(): boolean {
228
+ /** Whether agents can be offered generate_image: an image provider is configured. */
229
+ #images(): boolean {
198
230
  return this.#options.studio.canDraw !== false;
199
231
  }
200
232
 
201
233
  /** The agent tools of one agent session. */
202
234
  extension(scope: AgentTurnScope): ExtensionFactory {
203
235
  return agentToolsExtension(this, scope, THE_SPEAKER, {
204
- avatars: this.#avatars(),
205
236
  skills: this.#options.skills !== undefined,
206
237
  });
207
238
  }
@@ -211,12 +242,23 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
211
242
  return this.#options.skills?.carried(name).skills ?? [];
212
243
  }
213
244
 
214
- /** Another active agent's channel, for schedule_list. */
215
- channelOf(name: string): ChannelKey {
245
+ /**
246
+ * Another active agent's channel, for schedule_list: its room on the surface `near` is on when
247
+ * that surface hosts rooms, otherwise its Discord channel, otherwise its room on the first
248
+ * surface that hosts rooms.
249
+ */
250
+ channelOf(name: string, near?: ChannelKey): ChannelKey {
216
251
  try {
217
252
  const agent = this.#options.store.activeAgent(name);
218
- if (!agent.channelId) throw new AgentError(`${name} has no channel yet`);
219
- return discordKey(agent.channelId);
253
+ const rooms = this.#options.surfaces?.roomSurfaces() ?? [];
254
+ const surface = near === undefined ? undefined : roomSurfaceOf(near);
255
+ if (surface !== undefined && rooms.includes(surface))
256
+ return agentRoomKey(surface, name);
257
+ if (agent.channelId) return discordKey(agent.channelId);
258
+ const room = rooms[0];
259
+ if (room === undefined)
260
+ throw new AgentError(`${name} has no channel yet`);
261
+ return agentRoomKey(room, name);
220
262
  } catch (error) {
221
263
  throw new ScheduleError(
222
264
  error instanceof Error ? error.message : String(error),
@@ -228,8 +270,8 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
228
270
 
229
271
  /**
230
272
  * Inserts the seed agents, gives every active agent a channel, archives agents and groups
231
- * whose channels were deleted while the assistant was down, and draws missing avatars in the
232
- * background.
273
+ * whose channels were deleted while the assistant was down, and dresses agents without a
274
+ * character in the background.
233
275
  */
234
276
  start(): Promise<void> {
235
277
  return this.#lifecycle.start();
@@ -331,8 +373,10 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
331
373
  around: string | undefined,
332
374
  limit: number | undefined,
333
375
  ): Promise<string> {
376
+ const { channels } = this.#options;
377
+ if (!channels) throw new AgentError(NO_CHANNELS);
334
378
  const id = channelIdFromRef(channel);
335
- const messages = await this.#options.channels.read(id, {
379
+ const messages = await channels.read(id, {
336
380
  limit: Math.min(Math.max(limit ?? 20, 1), READ_LIMIT),
337
381
  ...(around ? { around } : {}),
338
382
  });
@@ -344,12 +388,11 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
344
388
  // ── AgentOps ───────────────────────────────────────────────────────────
345
389
 
346
390
  async list(): Promise<string> {
347
- return `${teamListText(this.#options.store)}\n\n${await this.#layoutText()}`;
348
- }
349
-
350
- async #layoutText(): Promise<string> {
351
391
  const { store, channels } = this.#options;
352
- return layoutText(await channels.layout(), store);
392
+ const list = teamListText(store);
393
+ return channels
394
+ ? `${list}\n\n${layoutText(await channels.layout(), store)}`
395
+ : list;
353
396
  }
354
397
 
355
398
  /**
@@ -361,6 +404,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
361
404
  entries: { category: string; names: string[] }[],
362
405
  ): Promise<string> {
363
406
  const { store, channels, logger } = this.#options;
407
+ if (!channels) throw new AgentError(NO_CHANNELS);
364
408
  const listed = resolveArrangement(entries, store);
365
409
  const { layout, remove } = planArrangement(listed, await channels.layout());
366
410
  await channels.arrange(layout, remove);
@@ -372,7 +416,7 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
372
416
  remove.length > 0
373
417
  ? ` ${remove.length} empty team categories were deleted.`
374
418
  : "";
375
- return `Arranged the channels.${removed}\n\n${await this.#layoutText()}`;
419
+ return `Arranged the channels.${removed}\n\n${layoutText(await channels.layout(), store)}`;
376
420
  }
377
421
 
378
422
  get(name: string): string {
@@ -384,7 +428,6 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
384
428
  agent,
385
429
  models.defaults,
386
430
  skills?.describeCarried(agent.name),
387
- this.#avatars(),
388
431
  );
389
432
  }
390
433
 
@@ -394,38 +437,28 @@ export class DiscordAgentTeam implements AgentOps, AgentTeam {
394
437
  * Changes an agent's display name, prompt, model, or thinking level; `default` returns the
395
438
  * model or thinking level to the assistant's. A model must be one the host runs.
396
439
  */
397
- update(
398
- name: string,
399
- change: {
400
- displayName?: string;
401
- prompt?: string;
402
- model?: string;
403
- thinking?: string;
404
- },
405
- ): Promise<string> {
440
+ update(name: string, change: AgentChange): Promise<string> {
406
441
  return this.#editing.update(name, change);
407
442
  }
408
443
 
409
444
  create(
410
445
  caller: AgentTurnScope,
411
- input: {
412
- name: string;
413
- displayName: string;
414
- prompt: string;
415
- avatarPrompt?: string;
416
- task: string;
417
- category?: string;
418
- skills?: string[];
419
- },
446
+ input: Parameters<TeamEditing["create"]>[1],
420
447
  ): Promise<string> {
421
448
  return this.#editing.create(caller, input);
422
449
  }
423
450
 
424
- avatar(name: string, mode: AvatarMode, text?: string): Promise<string> {
425
- return this.#editing.avatar(name, mode, text);
451
+ /** Changes an agent's look; see `AgentTeam.dress`. */
452
+ dress(name: string, input: CharacterInput): Promise<Agent> {
453
+ return this.#editing.dress(name, input);
454
+ }
455
+
456
+ /** The agent tool that changes a look; see `AgentTeam.dress`. */
457
+ avatar(name: string, look: CharacterInput): Promise<string> {
458
+ return this.#editing.avatar(name, look);
426
459
  }
427
460
 
428
- /** Draws and stores a new picture; the owner's panel and the agent tool both use it. */
461
+ /** @deprecated Use `dress`; removed in 1.0. Draws and stores a new picture. */
429
462
  redrawAvatar(name: string, mode: AvatarMode, text?: string): Promise<Agent> {
430
463
  return this.#editing.redrawAvatar(name, mode, text);
431
464
  }