pi-roundtable 0.26.3 → 0.27.1

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 (84) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +23 -0
  3. package/README.zh-TW.md +31 -8
  4. package/docs/plugins.md +176 -7
  5. package/package.json +1 -1
  6. package/src/core/agents/agent-book.ts +34 -1
  7. package/src/core/agents/agent-claim.ts +25 -5
  8. package/src/core/agents/agent-dashboard.ts +23 -2
  9. package/src/core/agents/agent-messages.ts +28 -10
  10. package/src/core/agents/agent-ports.ts +10 -0
  11. package/src/core/agents/agent-prompt.ts +17 -1
  12. package/src/core/agents/agent-rules.ts +11 -1
  13. package/src/core/agents/agent-schema.ts +22 -0
  14. package/src/core/agents/agent-statements.ts +16 -2
  15. package/src/core/agents/agent-store.ts +30 -20
  16. package/src/core/agents/agent-team-fake-runtime.ts +48 -0
  17. package/src/core/agents/agent-team-fixture.ts +32 -44
  18. package/src/core/agents/agent-team.ts +69 -74
  19. package/src/core/agents/agent-tool-schema.ts +84 -0
  20. package/src/core/agents/agent-tools.ts +50 -85
  21. package/src/core/agents/group-turns.ts +132 -41
  22. package/src/core/agents/helper-create.ts +180 -0
  23. package/src/core/agents/helper-ops.ts +33 -0
  24. package/src/core/agents/helper-test-kit.ts +72 -0
  25. package/src/core/agents/helper-tools.ts +104 -0
  26. package/src/core/agents/helper-work.ts +101 -0
  27. package/src/core/agents/message-target.ts +15 -23
  28. package/src/core/agents/routes.ts +347 -0
  29. package/src/core/agents/team-approval.ts +32 -0
  30. package/src/core/agents/team-editing.ts +97 -32
  31. package/src/core/agents/team-helpers.ts +216 -0
  32. package/src/core/agents/team-lifecycle.ts +26 -1
  33. package/src/core/agents/team-options.ts +8 -0
  34. package/src/core/agents/team-prompt.ts +75 -0
  35. package/src/core/agents/team-selection.ts +31 -13
  36. package/src/core/agents/team-status.ts +6 -0
  37. package/src/core/agents/team-text.ts +20 -4
  38. package/src/core/agents/team-turn-types.ts +12 -1
  39. package/src/core/agents/team-turns.ts +25 -31
  40. package/src/core/builtin/agent-server.ts +37 -2
  41. package/src/core/builtin/modules.ts +4 -4
  42. package/src/core/config/adapters.ts +12 -0
  43. package/src/core/config/agent-team.ts +2 -0
  44. package/src/core/config/config-schema.ts +4 -0
  45. package/src/core/config/config.ts +48 -2
  46. package/src/core/config/helpers.ts +60 -0
  47. package/src/core/config/ops.ts +8 -0
  48. package/src/core/config/routing.ts +296 -0
  49. package/src/core/contract/channels.ts +73 -2
  50. package/src/core/contract/surface.ts +31 -1
  51. package/src/core/define-roundtable.ts +8 -1
  52. package/src/core/discord/agent-discord.ts +67 -6
  53. package/src/core/discord/discord-surface.ts +24 -24
  54. package/src/core/discord/message-changes.ts +66 -0
  55. package/src/core/discord/poster/poster-checks.ts +62 -8
  56. package/src/core/discord/poster/poster.ts +41 -0
  57. package/src/core/discord/poster/rest-poster-read.ts +130 -0
  58. package/src/core/discord/poster/rest-poster.ts +38 -120
  59. package/src/core/domain/round.ts +31 -0
  60. package/src/core/host-registration.ts +15 -0
  61. package/src/core/host.ts +6 -0
  62. package/src/core/i18n/conversation.ts +3 -0
  63. package/src/core/i18n/dashboard.ts +4 -0
  64. package/src/core/ops/error-reporter.ts +4 -0
  65. package/src/core/ops/ops-alerts.ts +102 -6
  66. package/src/core/plugin.ts +17 -0
  67. package/src/core/registry/contributions.ts +1 -26
  68. package/src/core/registry/surface-service.ts +41 -0
  69. package/src/core/routing/channel-router.ts +54 -3
  70. package/src/core/routing/claim-events.ts +42 -0
  71. package/src/core/routing/message-observers.ts +86 -0
  72. package/src/core/routing/peer-bots.ts +149 -0
  73. package/src/core/routing/surface-port.ts +4 -0
  74. package/src/core/services.ts +7 -6
  75. package/src/core/testing/fake-poster-guild.ts +1 -1
  76. package/src/core/testing/fake-poster-reads.ts +96 -0
  77. package/src/core/testing/fake-poster.ts +62 -94
  78. package/src/core/tool-set.snapshot.json +8 -0
  79. package/src/discord/index.ts +2 -0
  80. package/src/index.ts +18 -1
  81. package/src/kit/channels.ts +1 -0
  82. package/src/kit/index.ts +5 -0
  83. package/src/kit/ops-alerts.ts +4 -0
  84. package/src/testing.ts +18 -1
@@ -0,0 +1,149 @@
1
+ import {
2
+ either,
3
+ type Field,
4
+ integer,
5
+ list,
6
+ optional,
7
+ shape,
8
+ text,
9
+ } from "../config/schema.ts";
10
+ import type { InboundMessage } from "../contract/channels.ts";
11
+ import { parseChannelKey } from "../contract/surface.ts";
12
+ import { ConfigError } from "../domain/errors.ts";
13
+ import type { ChannelKey } from "../sessions.ts";
14
+
15
+ /** One peer bot the host answers, and where. */
16
+ export interface PeerBotEntry {
17
+ /** The bot's user id on the surface, a Discord snowflake. */
18
+ id: string;
19
+ /** Answer it only in these servers; left out, in every server of the host. */
20
+ guilds?: readonly string[];
21
+ /** Answer it only in these channels; left out, in every channel. */
22
+ channels?: readonly string[];
23
+ }
24
+
25
+ /**
26
+ * The other bots whose messages the host answers like a guest's. Explicit ids only: there is no
27
+ * wildcard and no "all bots", and a host that leaves this out answers no bot.
28
+ */
29
+ export interface PeerBotsConfig {
30
+ /** The peer bots, each by its user id or as an entry that limits it to servers or channels. */
31
+ allow: readonly (string | PeerBotEntry)[];
32
+ /**
33
+ * The most messages from peer bots in a row, in one channel, that start a turn before the next
34
+ * one is ignored; a person's message in the channel starts the count over. Default 3.
35
+ */
36
+ maxRounds?: number;
37
+ }
38
+
39
+ /** The rounds in a row a channel answers peer bots when the host does not say. */
40
+ export const DEFAULT_PEER_ROUNDS = 3;
41
+
42
+ const SNOWFLAKE = /^[0-9]{17,20}$/;
43
+
44
+ /** A Discord id: 17 to 20 digits. */
45
+ const snowflake: Field<string> = {
46
+ describe: "a Discord id (17 to 20 digits)",
47
+ check: (value, path) => {
48
+ const id = text.check(value, path);
49
+ if (!SNOWFLAKE.test(id))
50
+ throw new ConfigError(
51
+ `config ${path}: expected a Discord id (17 to 20 digits), got ${JSON.stringify(id)}. Copy the id with Developer Mode on; a name or a wildcard is not accepted. Fix the value in roundtable.config.ts.`,
52
+ );
53
+ return id;
54
+ },
55
+ };
56
+
57
+ const entryShape = shape({
58
+ id: snowflake,
59
+ guilds: optional(list(snowflake)),
60
+ channels: optional(list(snowflake)),
61
+ });
62
+
63
+ /** `discord.peerBots`, or the `peerBots` of the host's options. */
64
+ export const peerBotsShape = shape({
65
+ allow: list(either(snowflake, entryShape)),
66
+ maxRounds: optional(integer(1, 100)),
67
+ });
68
+
69
+ /**
70
+ * The peer bots a host answers, and how many of their messages in a row a channel answers.
71
+ * Core-internal: the router holds it, and a claim reads only `InboundMessage.peerBot`.
72
+ */
73
+ export class PeerBots {
74
+ readonly #entries: ReadonlyMap<string, readonly PeerBotEntry[]>;
75
+ readonly #maxRounds: number;
76
+ readonly #surface: string;
77
+ /** Peer messages answered since a person last posted, by channel. */
78
+ readonly #rounds = new Map<ChannelKey, number>();
79
+
80
+ /** Throws a ConfigError naming `at` for an id that is not a snowflake. */
81
+ constructor(config: PeerBotsConfig, surface = "discord", at = "peerBots") {
82
+ const checked = peerBotsShape.check(config, at);
83
+ const entries = new Map<string, PeerBotEntry[]>();
84
+ for (const given of checked.allow) {
85
+ const entry = typeof given === "string" ? { id: given } : given;
86
+ entries.set(entry.id, [...(entries.get(entry.id) ?? []), entry]);
87
+ }
88
+ this.#entries = entries;
89
+ this.#maxRounds = checked.maxRounds ?? DEFAULT_PEER_ROUNDS;
90
+ this.#surface = surface;
91
+ }
92
+
93
+ /** Whether the host lists the bot that wrote this message, for this channel and server. */
94
+ lists(message: InboundMessage): boolean {
95
+ if (!message.authorIsBot || message.integration) return false;
96
+ const { surface, id: channelId } = parseChannelKey(message.channel);
97
+ if (surface !== this.#surface) return false;
98
+ return (this.#entries.get(message.authorId) ?? []).some(
99
+ (entry) =>
100
+ (!entry.guilds || entry.guilds.includes(message.space ?? "")) &&
101
+ (!entry.channels || entry.channels.includes(channelId)),
102
+ );
103
+ }
104
+
105
+ /** Whether the channel has answered its rounds of peer bots since a person last posted. */
106
+ exhausted(channel: ChannelKey): boolean {
107
+ return (this.#rounds.get(channel) ?? 0) >= this.#maxRounds;
108
+ }
109
+
110
+ /** Counts a peer bot's message the channel answered. */
111
+ answered(channel: ChannelKey): void {
112
+ this.#rounds.set(channel, (this.#rounds.get(channel) ?? 0) + 1);
113
+ }
114
+
115
+ /** A person posted in the channel: its rounds start over. */
116
+ personPosted(channel: ChannelKey): void {
117
+ this.#rounds.delete(channel);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Whether a claim that answers people may answer the author of this message: a person, or a bot
123
+ * the host lists as a peer (`discord.peerBots`) that mentions the bot or replies to it. Use it
124
+ * where a claim once checked `!message.authorIsBot`; the host's allowlist and rounds are already
125
+ * applied by the router, which alone sets `peerBot`. It says nothing of integrations and webhooks.
126
+ */
127
+ export function answersAuthor(
128
+ message: Pick<
129
+ InboundMessage,
130
+ "authorIsBot" | "peerBot" | "mentionsBot" | "repliesToBot"
131
+ >,
132
+ ): boolean {
133
+ if (!message.authorIsBot) return true;
134
+ return (
135
+ message.peerBot === true && (message.mentionsBot || message.repliesToBot)
136
+ );
137
+ }
138
+
139
+ /**
140
+ * The message as a listed peer bot's reaches the access rules: without the server roles it holds,
141
+ * which a rule written for people could otherwise use to lift a bot above what its identity and
142
+ * `everyone` give.
143
+ */
144
+ export function peerFacts(message: InboundMessage): InboundMessage {
145
+ const { authorRoleIds: _ids, actor, ...rest } = message;
146
+ if (!actor) return rest;
147
+ const { roles: _roles, ...bare } = actor;
148
+ return { ...rest, actor: bare };
149
+ }
@@ -60,10 +60,14 @@ export function surfacePort(
60
60
  linked()
61
61
  .filter((surface) => surface.agentRooms === true)
62
62
  .map((surface) => surface.surface),
63
+ names: () => linked().map((surface) => surface.surface),
63
64
  interim: (channel, as) => of(channel)?.interim?.(channel, as),
64
65
  progress: async (channel, event) => {
65
66
  await of(channel)?.progress?.(channel, event);
66
67
  },
68
+ groupRound: async (channel, round) => {
69
+ await of(channel)?.groupRound?.(channel, round);
70
+ },
67
71
  };
68
72
  }
69
73
 
@@ -2,6 +2,8 @@ import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
2
  import type { CharacterInput, CharacterSpec } from "../characters/index.ts";
3
3
  import type { Agent, AgentGroup } from "./agents/agent-rules.ts";
4
4
  import type { AvatarMode } from "./agents/agent-tools.ts";
5
+ import type { HelperTeam } from "./agents/helper-ops.ts";
6
+ import type { RoutingTable } from "./agents/routes.ts";
5
7
  import type { TeamStatus } from "./agents/team-status.ts";
6
8
  import type { ChannelContexts } from "./contract/channel-context.ts";
7
9
  import type {
@@ -181,12 +183,12 @@ export interface AgentServer {
181
183
  avatars: AvatarStudio;
182
184
  /** Where the agents' files are: the directories their shell and file tools run in. */
183
185
  workspace: AgentWorkspace;
186
+ routes?: RoutingTable; // where work no person started lands; the built-in server sets it
184
187
  }
185
188
 
186
189
  /**
187
190
  * The directories the agents work in, as configured. Every agent shares them: the core has no
188
- * workspace per agent. Read-only data, so a plugin that shows or serves the files can find them;
189
- * the directories may not exist yet.
191
+ * workspace per agent. Read-only data for a plugin that shows or serves the files; they may not exist yet.
190
192
  */
191
193
  export interface AgentWorkspace {
192
194
  /** The shell's shared working directory (`config.workDir`), where agents write without a hold. */
@@ -241,9 +243,8 @@ export interface NewAgent {
241
243
  /** Its opening task; without it the agent only exists. */
242
244
  task?: string;
243
245
  /**
244
- * The surface whose room the opening task runs in, so the person who asked sees it start
245
- * there. Without it, or for a surface that hosts no rooms, the task runs where `agent_create`
246
- * runs it: the agent's Discord channel, else its first room.
246
+ * The surface whose room the opening task runs in, so the person who asked sees it start there;
247
+ * without it, or for a surface with no rooms, where `agent_create` runs it: the agent's Discord channel, else its first room.
247
248
  */
248
249
  openIn?: string;
249
250
  category?: string;
@@ -262,7 +263,7 @@ export interface AgentChange {
262
263
  }
263
264
 
264
265
  /** The agent team as plugins use it: who is in it, what model they run, and their channels. */
265
- export interface AgentTeam {
266
+ export interface AgentTeam extends HelperTeam {
266
267
  /** The team's id: the Discord guild, or `"local"` on a host without Discord. */
267
268
  readonly guildId: string;
268
269
  /** Called when a turn starts or ends and when an agent or group changes. */
@@ -32,7 +32,7 @@ export interface FakePosterMessage {
32
32
  urls?: string[];
33
33
  /** When it was posted, in the fake's own clock (one more for each message). */
34
34
  at: number;
35
- /** The card, for a message `sayCard` posted; `content` then holds its text. */
35
+ /** The card, for a message `sayCard` or `postCard` posted; `content` then holds its text. */
36
36
  card?: DiscordCard;
37
37
  /** Discord's message type; absent for an ordinary message, 6 for the notice a pin makes. */
38
38
  type?: number;
@@ -0,0 +1,96 @@
1
+ import {
2
+ DISCORD_LIMITS,
3
+ type DiscordMessage,
4
+ type RecentMessage,
5
+ } from "../discord/poster/poster.ts";
6
+ import { checkSnowflake } from "../discord/poster/poster-checks.ts";
7
+ import { DiscordPosterError } from "../discord/poster/poster-errors.ts";
8
+ import { FakePosterGuild } from "./fake-poster-guild.ts";
9
+
10
+ /** The fake's pin calls and message reads, on top of its channel, thread and member calls. */
11
+ export abstract class FakePosterReads extends FakePosterGuild {
12
+ async pin(channelId: string, messageId: string): Promise<void> {
13
+ checkSnowflake("a channel", channelId);
14
+ checkSnowflake("a message", messageId);
15
+ this.sent("pin");
16
+ const channel = this.channel(channelId);
17
+ this.found(channelId, messageId);
18
+ if (channel.pins.includes(messageId)) return;
19
+ if (channel.pins.length >= DISCORD_LIMITS.pins)
20
+ throw new DiscordPosterError(
21
+ "rejected",
22
+ `a channel keeps at most ${DISCORD_LIMITS.pins} pins`,
23
+ { discordCode: 30003, status: 400 },
24
+ );
25
+ channel.pins.push(messageId);
26
+ if (this.pinNotices) {
27
+ this.next += 1n;
28
+ channel.messages.push({
29
+ id: this.next.toString(),
30
+ username: "bot",
31
+ content: "",
32
+ files: [],
33
+ blobs: [],
34
+ mentions: [],
35
+ authorId: this.botId,
36
+ type: 6,
37
+ referencedMessageId: messageId,
38
+ at: this.tick(),
39
+ });
40
+ }
41
+ }
42
+
43
+ async unpin(channelId: string, messageId: string): Promise<void> {
44
+ checkSnowflake("a channel", channelId);
45
+ checkSnowflake("a message", messageId);
46
+ this.sent("unpin");
47
+ const channel = this.channel(channelId);
48
+ channel.pins = channel.pins.filter((id) => id !== messageId);
49
+ }
50
+
51
+ async pinnedIds(channelId: string): Promise<string[]> {
52
+ checkSnowflake("a channel", channelId);
53
+ this.sent("pinnedIds");
54
+ return [...this.channel(channelId).pins].reverse();
55
+ }
56
+
57
+ async message(channelId: string, messageId: string): Promise<DiscordMessage> {
58
+ checkSnowflake("a channel", channelId);
59
+ checkSnowflake("a message", messageId);
60
+ this.sent("message");
61
+ const found = this.found(channelId, messageId);
62
+ return {
63
+ id: found.id,
64
+ authorId: found.webhookId ?? found.authorId ?? "0",
65
+ ...(found.webhookId ? { webhookId: found.webhookId } : {}),
66
+ content: found.content,
67
+ files: found.files.map((file, index) => ({
68
+ name: file.name,
69
+ contentType: found.blobs[index]?.contentType ?? "",
70
+ size: file.bytes,
71
+ url:
72
+ found.urls?.[index] ??
73
+ `https://cdn.test/${found.id}/${encodeURIComponent(file.name)}`,
74
+ })),
75
+ };
76
+ }
77
+
78
+ async recentMessages(
79
+ channelId: string,
80
+ limit: number,
81
+ ): Promise<RecentMessage[]> {
82
+ checkSnowflake("a channel", channelId);
83
+ this.sent("recentMessages");
84
+ return [...this.channel(channelId).messages]
85
+ .reverse()
86
+ .slice(0, Math.min(50, Math.max(1, Math.trunc(limit))))
87
+ .map((message) => ({
88
+ id: message.id,
89
+ type: message.type ?? 0,
90
+ authorId: message.webhookId ?? message.authorId ?? "0",
91
+ ...(message.referencedMessageId
92
+ ? { referencedMessageId: message.referencedMessageId }
93
+ : {}),
94
+ }));
95
+ }
96
+ }
@@ -1,16 +1,14 @@
1
- import {
2
- DISCORD_LIMITS,
3
- type DiscordCard,
4
- type DiscordMessage,
5
- type DiscordPoster,
6
- type PostedMessage,
7
- type RecentMessage,
1
+ import type {
2
+ DiscordCard,
3
+ DiscordPoster,
4
+ PostedMessage,
8
5
  } from "../discord/poster/poster.ts";
9
6
  import {
10
7
  cardBody,
11
8
  checkAvatar,
12
9
  checkedPost,
13
10
  checkedText,
11
+ checkedWebhookCard,
14
12
  checkSnowflake,
15
13
  checkWebhookName,
16
14
  invalid,
@@ -19,9 +17,9 @@ import { DiscordPosterError } from "../discord/poster/poster-errors.ts";
19
17
  import {
20
18
  cardText,
21
19
  type FakePosterChannel,
22
- FakePosterGuild,
23
20
  type FakePosterMessage,
24
21
  } from "./fake-poster-guild.ts";
22
+ import { FakePosterReads } from "./fake-poster-reads.ts";
25
23
 
26
24
  export type {
27
25
  FakePosterChannel,
@@ -108,7 +106,7 @@ export interface FakeDiscordPoster extends DiscordPoster {
108
106
  * the next call or to refuse direct messages from a person. The same scenarios run against it and
109
107
  * against the real poster over an emulated Discord, so the two answer alike.
110
108
  */
111
- class FakePosterImpl extends FakePosterGuild implements FakeDiscordPoster {
109
+ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
112
110
  readonly #webhooks = new Map<string, string>();
113
111
  readonly #madeListeners = new Set<
114
112
  (channelId: string, webhook: string) => Promise<void>
@@ -234,6 +232,61 @@ class FakePosterImpl extends FakePosterGuild implements FakeDiscordPoster {
234
232
  message.mentions = body.allowed_mentions.users;
235
233
  }
236
234
 
235
+ async postCard(
236
+ channelId: string,
237
+ post: Parameters<DiscordPoster["postCard"]>[1],
238
+ ): Promise<PostedMessage> {
239
+ checkSnowflake("a channel", channelId);
240
+ const { webhook, body } = checkedWebhookCard(post);
241
+ this.sent("postCard");
242
+ const channel = this.channel(channelId);
243
+ const webhookId = await this.#webhook(channelId, webhook);
244
+ this.next += 1n;
245
+ const message: FakePosterMessage = {
246
+ id: this.next.toString(),
247
+ username: body.username,
248
+ content: cardText(post.card),
249
+ files: [],
250
+ blobs: [],
251
+ mentions: [],
252
+ ...("avatar_url" in body ? { avatarUrl: body.avatar_url } : {}),
253
+ webhook,
254
+ webhookId,
255
+ card: post.card,
256
+ at: this.tick(),
257
+ };
258
+ channel.messages.push(message);
259
+ return { channelId, messageId: message.id };
260
+ }
261
+
262
+ async editWebhookCard(
263
+ channelId: string,
264
+ messageId: string,
265
+ card: DiscordCard,
266
+ webhooks: readonly string[],
267
+ ): Promise<void> {
268
+ checkSnowflake("a channel", channelId);
269
+ checkSnowflake("a message", messageId);
270
+ cardBody(card);
271
+ for (const name of webhooks) checkWebhookName(name);
272
+ this.sent("editWebhookCard");
273
+ const message = this.found(channelId, messageId);
274
+ if (!webhooks.some((name) => this.#ownedBy(message, channelId, name)))
275
+ throw new DiscordPosterError(
276
+ "message_gone",
277
+ "the message does not exist",
278
+ );
279
+ // A message posted without the Components V2 flag cannot become a card.
280
+ if (!message.card)
281
+ throw new DiscordPosterError(
282
+ "rejected",
283
+ "editing a card failed: that message is not a card",
284
+ { discordCode: 50035, status: 400 },
285
+ );
286
+ message.card = card;
287
+ message.content = cardText(card);
288
+ }
289
+
237
290
  async remove(
238
291
  channelId: string,
239
292
  messageId: string,
@@ -375,91 +428,6 @@ class FakePosterImpl extends FakePosterGuild implements FakeDiscordPoster {
375
428
  this.directs.push({ userId, content });
376
429
  return "sent";
377
430
  }
378
-
379
- async pin(channelId: string, messageId: string): Promise<void> {
380
- checkSnowflake("a channel", channelId);
381
- checkSnowflake("a message", messageId);
382
- this.sent("pin");
383
- const channel = this.channel(channelId);
384
- this.found(channelId, messageId);
385
- if (channel.pins.includes(messageId)) return;
386
- if (channel.pins.length >= DISCORD_LIMITS.pins)
387
- throw new DiscordPosterError(
388
- "rejected",
389
- `a channel keeps at most ${DISCORD_LIMITS.pins} pins`,
390
- { discordCode: 30003, status: 400 },
391
- );
392
- channel.pins.push(messageId);
393
- if (this.pinNotices) {
394
- this.next += 1n;
395
- channel.messages.push({
396
- id: this.next.toString(),
397
- username: "bot",
398
- content: "",
399
- files: [],
400
- blobs: [],
401
- mentions: [],
402
- authorId: this.botId,
403
- type: 6,
404
- referencedMessageId: messageId,
405
- at: this.tick(),
406
- });
407
- }
408
- }
409
-
410
- async unpin(channelId: string, messageId: string): Promise<void> {
411
- checkSnowflake("a channel", channelId);
412
- checkSnowflake("a message", messageId);
413
- this.sent("unpin");
414
- const channel = this.channel(channelId);
415
- channel.pins = channel.pins.filter((id) => id !== messageId);
416
- }
417
-
418
- async pinnedIds(channelId: string): Promise<string[]> {
419
- checkSnowflake("a channel", channelId);
420
- this.sent("pinnedIds");
421
- return [...this.channel(channelId).pins].reverse();
422
- }
423
-
424
- async message(channelId: string, messageId: string): Promise<DiscordMessage> {
425
- checkSnowflake("a channel", channelId);
426
- checkSnowflake("a message", messageId);
427
- this.sent("message");
428
- const found = this.found(channelId, messageId);
429
- return {
430
- id: found.id,
431
- authorId: found.webhookId ?? found.authorId ?? "0",
432
- ...(found.webhookId ? { webhookId: found.webhookId } : {}),
433
- content: found.content,
434
- files: found.files.map((file, index) => ({
435
- name: file.name,
436
- contentType: found.blobs[index]?.contentType ?? "",
437
- size: file.bytes,
438
- url:
439
- found.urls?.[index] ??
440
- `https://cdn.test/${found.id}/${encodeURIComponent(file.name)}`,
441
- })),
442
- };
443
- }
444
-
445
- async recentMessages(
446
- channelId: string,
447
- limit: number,
448
- ): Promise<RecentMessage[]> {
449
- checkSnowflake("a channel", channelId);
450
- this.sent("recentMessages");
451
- return [...this.channel(channelId).messages]
452
- .reverse()
453
- .slice(0, Math.min(50, Math.max(1, Math.trunc(limit))))
454
- .map((message) => ({
455
- id: message.id,
456
- type: message.type ?? 0,
457
- authorId: message.webhookId ?? message.authorId ?? "0",
458
- ...(message.referencedMessageId
459
- ? { referencedMessageId: message.referencedMessageId }
460
- : {}),
461
- }));
462
- }
463
431
  }
464
432
 
465
433
  /** A `DiscordPoster` in memory, to give a plugin under test as `DISCORD.poster`. */
@@ -89,6 +89,8 @@
89
89
  "discord_unpin_message",
90
90
  "group_create",
91
91
  "group_update",
92
+ "helper_create",
93
+ "helper_end",
92
94
  "memory_add",
93
95
  "memory_remove",
94
96
  "memory_search",
@@ -121,6 +123,8 @@
121
123
  "edit",
122
124
  "group_create",
123
125
  "group_update",
126
+ "helper_create",
127
+ "helper_end",
124
128
  "message_agent",
125
129
  "read",
126
130
  "skill_create",
@@ -179,6 +183,8 @@
179
183
  "discord_unpin_message",
180
184
  "group_create",
181
185
  "group_update",
186
+ "helper_create",
187
+ "helper_end",
182
188
  "memory_add",
183
189
  "memory_remove",
184
190
  "memory_search",
@@ -273,6 +279,8 @@
273
279
  "get_search_content": "member",
274
280
  "group_create": "admin",
275
281
  "group_update": "admin",
282
+ "helper_create": "member",
283
+ "helper_end": "member",
276
284
  "memory_add": "member",
277
285
  "memory_remove": "member",
278
286
  "memory_search": "member",
@@ -81,6 +81,7 @@ export {
81
81
  export type {
82
82
  CardButton,
83
83
  CardLink,
84
+ CardPicture,
84
85
  CardSelect,
85
86
  CategoryChannel,
86
87
  DiscordCard,
@@ -93,6 +94,7 @@ export type {
93
94
  RecentMessage,
94
95
  ThreadCloser,
95
96
  ThreadState,
97
+ WebhookCardPost,
96
98
  WebhookPost,
97
99
  } from "../core/discord/poster/poster.ts";
98
100
  export {
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ export {
4
4
  AGENT_SERVER_PRIORITY,
5
5
  OWNER_TARGET,
6
6
  } from "./core/agents/agent-claim.ts";
7
- export { characterOf } from "./core/agents/agent-rules.ts";
7
+ export { characterOf, isHelper } from "./core/agents/agent-rules.ts";
8
8
  export type {
9
9
  Agent,
10
10
  AgentGroup,
@@ -12,6 +12,8 @@ export type {
12
12
  AgentStatus,
13
13
  } from "./core/agents/agent-store.ts";
14
14
  export type { AvatarMode } from "./core/agents/agent-tools.ts";
15
+ export type { HelpersEnded, HelperTeam } from "./core/agents/helper-ops.ts";
16
+ export type { RouteKind, RoutingTable } from "./core/agents/routes.ts";
15
17
  export {
16
18
  agentRoomKey,
17
19
  groupRoomKey,
@@ -48,8 +50,15 @@ export type {
48
50
  TierConfig,
49
51
  } from "./core/config/config.ts";
50
52
  export type { ConversationsConfig } from "./core/config/conversations.ts";
53
+ export type { HelpersConfig } from "./core/config/helpers.ts";
51
54
  export type { LimitsConfig } from "./core/config/limits.ts";
52
55
  export type { ProfilesConfig } from "./core/config/profiles.ts";
56
+ export type {
57
+ AgentRoutes,
58
+ PlaceTarget,
59
+ RoutingConfig,
60
+ SurfaceRouting,
61
+ } from "./core/config/routing.ts";
53
62
  export type { SkillsConfig } from "./core/config/skills-config.ts";
54
63
  export type {
55
64
  AttachmentPort,
@@ -78,6 +87,8 @@ export type {
78
87
  ConversationKind,
79
88
  ConversationPort,
80
89
  InboundMessage,
90
+ MessageObserver,
91
+ ObserveFilter,
81
92
  QueuePort,
82
93
  ScheduledOutcome,
83
94
  } from "./core/contract/channels.ts";
@@ -101,6 +112,7 @@ export type { ServiceKey, Services } from "./core/contract/services.ts";
101
112
  export { serviceKey } from "./core/contract/services.ts";
102
113
  export type {
103
114
  ChatSurface,
115
+ SurfaceMessageEvents,
104
116
  SurfacePort,
105
117
  SurfaceThreads,
106
118
  } from "./core/contract/surface.ts";
@@ -163,6 +175,7 @@ export type {
163
175
  } from "./core/domain/interim.ts";
164
176
  export type { TurnConversation, TurnRequest } from "./core/domain/ports.ts";
165
177
  export type { HoldRefusal, TurnProgress } from "./core/domain/progress.ts";
178
+ export type { RoundEnd, RoundProgress } from "./core/domain/round.ts";
166
179
  export {
167
180
  type TurnLimit,
168
181
  type TurnTimeout,
@@ -355,6 +368,10 @@ export type {
355
368
  ConversationTurnInput,
356
369
  ConversationTurns,
357
370
  } from "./core/routing/conversation-turns.ts";
371
+ export type {
372
+ PeerBotEntry,
373
+ PeerBotsConfig,
374
+ } from "./core/routing/peer-bots.ts";
358
375
  export type {
359
376
  AgentChange,
360
377
  AgentDirectory,
@@ -18,4 +18,5 @@ export { attachmentsOf } from "../core/attachments/turn-attachments.ts";
18
18
  export type { ChannelQueue } from "../core/routing/channel-queue.ts";
19
19
  export { channelQueue } from "../core/routing/channel-queue.ts";
20
20
  export { withReference } from "../core/routing/message-text.ts";
21
+ export { answersAuthor } from "../core/routing/peer-bots.ts";
21
22
  export { outcome, settleTurn } from "../core/routing/settle-turn.ts";
package/src/kit/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  export type { ChannelQueue } from "./channels.ts";
4
4
  export {
5
+ answersAuthor,
5
6
  attachmentsOf,
6
7
  channelQueue,
7
8
  channelSegment,
@@ -118,9 +119,13 @@ export {
118
119
  } from "./mirror.ts";
119
120
  export { oidcIdentity, oidcProvider } from "./oidc.ts";
120
121
  export type {
122
+ OpsAlert,
121
123
  OpsAlertKind,
122
124
  OpsAlertsOptions,
123
125
  OpsRaiseOptions,
126
+ OpsResolution,
127
+ OpsSeverity,
128
+ OpsSink,
124
129
  } from "./ops-alerts.ts";
125
130
  export { OpsAlerts } from "./ops-alerts.ts";
126
131
  export {
@@ -2,8 +2,12 @@
2
2
  // Short alerts for the people who run a host, held to a window so a fault cannot flood them.
3
3
 
4
4
  export type {
5
+ OpsAlert,
5
6
  OpsAlertKind,
6
7
  OpsAlertsOptions,
7
8
  OpsRaiseOptions,
9
+ OpsResolution,
10
+ OpsSeverity,
11
+ OpsSink,
8
12
  } from "../core/ops/ops-alerts.ts";
9
13
  export { OpsAlerts } from "../core/ops/ops-alerts.ts";