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.
- package/CHANGELOG.md +48 -0
- package/README.md +23 -0
- package/README.zh-TW.md +31 -8
- package/docs/plugins.md +176 -7
- package/package.json +1 -1
- package/src/core/agents/agent-book.ts +34 -1
- package/src/core/agents/agent-claim.ts +25 -5
- package/src/core/agents/agent-dashboard.ts +23 -2
- package/src/core/agents/agent-messages.ts +28 -10
- package/src/core/agents/agent-ports.ts +10 -0
- package/src/core/agents/agent-prompt.ts +17 -1
- package/src/core/agents/agent-rules.ts +11 -1
- package/src/core/agents/agent-schema.ts +22 -0
- package/src/core/agents/agent-statements.ts +16 -2
- package/src/core/agents/agent-store.ts +30 -20
- package/src/core/agents/agent-team-fake-runtime.ts +48 -0
- package/src/core/agents/agent-team-fixture.ts +32 -44
- package/src/core/agents/agent-team.ts +69 -74
- package/src/core/agents/agent-tool-schema.ts +84 -0
- package/src/core/agents/agent-tools.ts +50 -85
- package/src/core/agents/group-turns.ts +132 -41
- package/src/core/agents/helper-create.ts +180 -0
- package/src/core/agents/helper-ops.ts +33 -0
- package/src/core/agents/helper-test-kit.ts +72 -0
- package/src/core/agents/helper-tools.ts +104 -0
- package/src/core/agents/helper-work.ts +101 -0
- package/src/core/agents/message-target.ts +15 -23
- package/src/core/agents/routes.ts +347 -0
- package/src/core/agents/team-approval.ts +32 -0
- package/src/core/agents/team-editing.ts +97 -32
- package/src/core/agents/team-helpers.ts +216 -0
- package/src/core/agents/team-lifecycle.ts +26 -1
- package/src/core/agents/team-options.ts +8 -0
- package/src/core/agents/team-prompt.ts +75 -0
- package/src/core/agents/team-selection.ts +31 -13
- package/src/core/agents/team-status.ts +6 -0
- package/src/core/agents/team-text.ts +20 -4
- package/src/core/agents/team-turn-types.ts +12 -1
- package/src/core/agents/team-turns.ts +25 -31
- package/src/core/builtin/agent-server.ts +37 -2
- package/src/core/builtin/modules.ts +4 -4
- package/src/core/config/adapters.ts +12 -0
- package/src/core/config/agent-team.ts +2 -0
- package/src/core/config/config-schema.ts +4 -0
- package/src/core/config/config.ts +48 -2
- package/src/core/config/helpers.ts +60 -0
- package/src/core/config/ops.ts +8 -0
- package/src/core/config/routing.ts +296 -0
- package/src/core/contract/channels.ts +73 -2
- package/src/core/contract/surface.ts +31 -1
- package/src/core/define-roundtable.ts +8 -1
- package/src/core/discord/agent-discord.ts +67 -6
- package/src/core/discord/discord-surface.ts +24 -24
- package/src/core/discord/message-changes.ts +66 -0
- package/src/core/discord/poster/poster-checks.ts +62 -8
- package/src/core/discord/poster/poster.ts +41 -0
- package/src/core/discord/poster/rest-poster-read.ts +130 -0
- package/src/core/discord/poster/rest-poster.ts +38 -120
- package/src/core/domain/round.ts +31 -0
- package/src/core/host-registration.ts +15 -0
- package/src/core/host.ts +6 -0
- package/src/core/i18n/conversation.ts +3 -0
- package/src/core/i18n/dashboard.ts +4 -0
- package/src/core/ops/error-reporter.ts +4 -0
- package/src/core/ops/ops-alerts.ts +102 -6
- package/src/core/plugin.ts +17 -0
- package/src/core/registry/contributions.ts +1 -26
- package/src/core/registry/surface-service.ts +41 -0
- package/src/core/routing/channel-router.ts +54 -3
- package/src/core/routing/claim-events.ts +42 -0
- package/src/core/routing/message-observers.ts +86 -0
- package/src/core/routing/peer-bots.ts +149 -0
- package/src/core/routing/surface-port.ts +4 -0
- package/src/core/services.ts +7 -6
- package/src/core/testing/fake-poster-guild.ts +1 -1
- package/src/core/testing/fake-poster-reads.ts +96 -0
- package/src/core/testing/fake-poster.ts +62 -94
- package/src/core/tool-set.snapshot.json +8 -0
- package/src/discord/index.ts +2 -0
- package/src/index.ts +18 -1
- package/src/kit/channels.ts +1 -0
- package/src/kit/index.ts +5 -0
- package/src/kit/ops-alerts.ts +4 -0
- 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
|
|
package/src/core/services.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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",
|
package/src/discord/index.ts
CHANGED
|
@@ -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,
|
package/src/kit/channels.ts
CHANGED
|
@@ -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 {
|
package/src/kit/ops-alerts.ts
CHANGED
|
@@ -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";
|