@copilotkit/channels-discord 0.0.2
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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/__tests__/ephemeral.test.d.ts +2 -0
- package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
- package/dist/__tests__/ephemeral.test.js +26 -0
- package/dist/__tests__/intents.test.d.ts +2 -0
- package/dist/__tests__/intents.test.d.ts.map +1 -0
- package/dist/__tests__/intents.test.js +8 -0
- package/dist/__tests__/modal-submit.test.d.ts +2 -0
- package/dist/__tests__/modal-submit.test.d.ts.map +1 -0
- package/dist/__tests__/modal-submit.test.js +26 -0
- package/dist/__tests__/reactions.test.d.ts +2 -0
- package/dist/__tests__/reactions.test.d.ts.map +1 -0
- package/dist/__tests__/reactions.test.js +87 -0
- package/dist/adapter.d.ts +118 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +567 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/adapter.test.js +343 -0
- package/dist/auto-close-streaming.d.ts +61 -0
- package/dist/auto-close-streaming.d.ts.map +1 -0
- package/dist/auto-close-streaming.js +276 -0
- package/dist/auto-close-streaming.test.d.ts +2 -0
- package/dist/auto-close-streaming.test.d.ts.map +1 -0
- package/dist/auto-close-streaming.test.js +149 -0
- package/dist/built-in-context.d.ts +20 -0
- package/dist/built-in-context.d.ts.map +1 -0
- package/dist/built-in-context.js +30 -0
- package/dist/built-in-context.test.d.ts +2 -0
- package/dist/built-in-context.test.d.ts.map +1 -0
- package/dist/built-in-context.test.js +21 -0
- package/dist/built-in-tools.d.ts +4 -0
- package/dist/built-in-tools.d.ts.map +1 -0
- package/dist/built-in-tools.js +27 -0
- package/dist/built-in-tools.test.d.ts +2 -0
- package/dist/built-in-tools.test.d.ts.map +1 -0
- package/dist/built-in-tools.test.js +30 -0
- package/dist/chunked-message-stream.d.ts +94 -0
- package/dist/chunked-message-stream.d.ts.map +1 -0
- package/dist/chunked-message-stream.js +203 -0
- package/dist/chunked-message-stream.test.d.ts +2 -0
- package/dist/chunked-message-stream.test.d.ts.map +1 -0
- package/dist/chunked-message-stream.test.js +384 -0
- package/dist/commands.d.ts +29 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +193 -0
- package/dist/commands.test.d.ts +2 -0
- package/dist/commands.test.d.ts.map +1 -0
- package/dist/commands.test.js +281 -0
- package/dist/conversation-store.d.ts +45 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/conversation-store.js +78 -0
- package/dist/conversation-store.test.d.ts +2 -0
- package/dist/conversation-store.test.d.ts.map +1 -0
- package/dist/conversation-store.test.js +106 -0
- package/dist/discord-listener.d.ts +88 -0
- package/dist/discord-listener.d.ts.map +1 -0
- package/dist/discord-listener.js +118 -0
- package/dist/discord-listener.test.d.ts +2 -0
- package/dist/discord-listener.test.d.ts.map +1 -0
- package/dist/discord-listener.test.js +361 -0
- package/dist/download-files.d.ts +94 -0
- package/dist/download-files.d.ts.map +1 -0
- package/dist/download-files.js +145 -0
- package/dist/download-files.test.d.ts +2 -0
- package/dist/download-files.test.d.ts.map +1 -0
- package/dist/download-files.test.js +181 -0
- package/dist/event-renderer.d.ts +36 -0
- package/dist/event-renderer.d.ts.map +1 -0
- package/dist/event-renderer.js +270 -0
- package/dist/event-renderer.test.d.ts +2 -0
- package/dist/event-renderer.test.d.ts.map +1 -0
- package/dist/event-renderer.test.js +160 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/interaction.d.ts +11 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +138 -0
- package/dist/interaction.test.d.ts +2 -0
- package/dist/interaction.test.d.ts.map +1 -0
- package/dist/interaction.test.js +120 -0
- package/dist/markdown.d.ts +7 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +66 -0
- package/dist/markdown.test.d.ts +2 -0
- package/dist/markdown.test.d.ts.map +1 -0
- package/dist/markdown.test.js +47 -0
- package/dist/message-stream.d.ts +45 -0
- package/dist/message-stream.d.ts.map +1 -0
- package/dist/message-stream.js +76 -0
- package/dist/message-stream.test.d.ts +2 -0
- package/dist/message-stream.test.d.ts.map +1 -0
- package/dist/message-stream.test.js +162 -0
- package/dist/pending-interactions.d.ts +26 -0
- package/dist/pending-interactions.d.ts.map +1 -0
- package/dist/pending-interactions.js +50 -0
- package/dist/pending-interactions.test.d.ts +2 -0
- package/dist/pending-interactions.test.d.ts.map +1 -0
- package/dist/pending-interactions.test.js +73 -0
- package/dist/render/budget.d.ts +28 -0
- package/dist/render/budget.d.ts.map +1 -0
- package/dist/render/budget.js +49 -0
- package/dist/render/budget.test.d.ts +2 -0
- package/dist/render/budget.test.d.ts.map +1 -0
- package/dist/render/budget.test.js +59 -0
- package/dist/render/components-v2.d.ts +16 -0
- package/dist/render/components-v2.d.ts.map +1 -0
- package/dist/render/components-v2.js +396 -0
- package/dist/render/components-v2.test.d.ts +2 -0
- package/dist/render/components-v2.test.d.ts.map +1 -0
- package/dist/render/components-v2.test.js +395 -0
- package/dist/render/modal.d.ts +12 -0
- package/dist/render/modal.d.ts.map +1 -0
- package/dist/render/modal.js +44 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.test.d.ts +2 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +12 -0
- package/package.json +56 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { jsonSchemaToDiscordOptions, buildCommandBody, registerCommands, } from "./commands.js";
|
|
3
|
+
describe("jsonSchemaToDiscordOptions", () => {
|
|
4
|
+
it("maps primitive properties to typed options with required flags", () => {
|
|
5
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
priority: {
|
|
9
|
+
type: "string",
|
|
10
|
+
enum: ["low", "high"],
|
|
11
|
+
description: "How urgent",
|
|
12
|
+
},
|
|
13
|
+
count: { type: "integer" },
|
|
14
|
+
notify: { type: "boolean" },
|
|
15
|
+
},
|
|
16
|
+
required: ["priority"],
|
|
17
|
+
});
|
|
18
|
+
// String option type = 3, Integer = 4, Boolean = 5 (Discord ApplicationCommandOptionType).
|
|
19
|
+
const priority = opts.find((o) => o.name === "priority");
|
|
20
|
+
expect(priority.type).toBe(3);
|
|
21
|
+
expect(priority.required).toBe(true);
|
|
22
|
+
expect(priority.choices).toEqual([
|
|
23
|
+
{ name: "low", value: "low" },
|
|
24
|
+
{ name: "high", value: "high" },
|
|
25
|
+
]);
|
|
26
|
+
expect(opts.find((o) => o.name === "count").type).toBe(4);
|
|
27
|
+
expect(opts.find((o) => o.name === "notify").type).toBe(5);
|
|
28
|
+
expect(opts.find((o) => o.name === "count").required).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("degrades an unsupported (nested object) property to a free-text string option", () => {
|
|
31
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
32
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: { meta: { type: "object" } },
|
|
35
|
+
});
|
|
36
|
+
expect(opts[0]).toMatchObject({ name: "meta", type: 3 });
|
|
37
|
+
expect(warn).toHaveBeenCalled();
|
|
38
|
+
warn.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
it("returns [] for a free-text command (no options schema)", () => {
|
|
41
|
+
expect(jsonSchemaToDiscordOptions(undefined)).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
it("orders required options before optional ones (Discord requirement)", () => {
|
|
44
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
note: { type: "string" },
|
|
48
|
+
priority: { type: "string" },
|
|
49
|
+
},
|
|
50
|
+
required: ["priority"],
|
|
51
|
+
});
|
|
52
|
+
// `note` is declared first but is optional; `priority` must come first.
|
|
53
|
+
expect(opts.map((o) => o.name)).toEqual(["priority", "note"]);
|
|
54
|
+
expect(opts[0].required).toBe(true);
|
|
55
|
+
expect(opts[1].required).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it("builds numeric choices for an integer enum", () => {
|
|
58
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: { level: { type: "integer", enum: [1, 2, 3] } },
|
|
61
|
+
});
|
|
62
|
+
const level = opts.find((o) => o.name === "level");
|
|
63
|
+
expect(level.type).toBe(4);
|
|
64
|
+
expect(level.choices).toEqual([
|
|
65
|
+
{ name: "1", value: 1 },
|
|
66
|
+
{ name: "2", value: 2 },
|
|
67
|
+
{ name: "3", value: 3 },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
it("skips non-numeric enum entries for an integer option (NaN would reject the batch)", () => {
|
|
71
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
72
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: { level: { type: "integer", enum: [1, "x", 3] } },
|
|
75
|
+
});
|
|
76
|
+
const level = opts.find((o) => o.name === "level");
|
|
77
|
+
expect(level.type).toBe(4);
|
|
78
|
+
// "x" → NaN → dropped; only finite values survive.
|
|
79
|
+
expect(level.choices).toEqual([
|
|
80
|
+
{ name: "1", value: 1 },
|
|
81
|
+
{ name: "3", value: 3 },
|
|
82
|
+
]);
|
|
83
|
+
expect(warn).toHaveBeenCalled();
|
|
84
|
+
warn.mockRestore();
|
|
85
|
+
});
|
|
86
|
+
it("normalizes a nullable array type (['string','null']) to a String option, not the warn path", () => {
|
|
87
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
88
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: { note: { type: ["string", "null"] } },
|
|
91
|
+
});
|
|
92
|
+
const note = opts.find((o) => o.name === "note");
|
|
93
|
+
expect(note.type).toBe(3); // String, not the default-warn fallthrough
|
|
94
|
+
expect(warn).not.toHaveBeenCalled();
|
|
95
|
+
warn.mockRestore();
|
|
96
|
+
});
|
|
97
|
+
it("omits the choices key (not []) when an integer enum has no numeric entries", () => {
|
|
98
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
99
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: { level: { type: "integer", enum: ["a", "b", "c"] } },
|
|
102
|
+
});
|
|
103
|
+
const level = opts.find((o) => o.name === "level");
|
|
104
|
+
expect(level.type).toBe(4);
|
|
105
|
+
// No choices key at all — an empty `choices: []` would make Discord reject the batch.
|
|
106
|
+
expect("choices" in level).toBe(false);
|
|
107
|
+
expect(level.choices).toBeUndefined();
|
|
108
|
+
warn.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
it("drops non-integer values from an integer enum (1.5 is not an integer)", () => {
|
|
111
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: { level: { type: "integer", enum: [1, 1.5, 2] } },
|
|
114
|
+
});
|
|
115
|
+
const level = opts.find((o) => o.name === "level");
|
|
116
|
+
expect(level.choices).toEqual([
|
|
117
|
+
{ name: "1", value: 1 },
|
|
118
|
+
{ name: "2", value: 2 },
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
it("keeps non-integer finite values for a number (float) enum", () => {
|
|
122
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: { ratio: { type: "number", enum: [1, 1.5, 2] } },
|
|
125
|
+
});
|
|
126
|
+
const ratio = opts.find((o) => o.name === "ratio");
|
|
127
|
+
expect(ratio.type).toBe(10);
|
|
128
|
+
expect(ratio.choices).toEqual([
|
|
129
|
+
{ name: "1", value: 1 },
|
|
130
|
+
{ name: "1.5", value: 1.5 },
|
|
131
|
+
{ name: "2", value: 2 },
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
it("skips an empty-name choice (String('') is whitespace, Number('') is a finite 0)", () => {
|
|
135
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
136
|
+
const intOpts = jsonSchemaToDiscordOptions({
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: { level: { type: "integer", enum: ["", 1] } },
|
|
139
|
+
});
|
|
140
|
+
expect(intOpts.find((o) => o.name === "level").choices).toEqual([
|
|
141
|
+
{ name: "1", value: 1 },
|
|
142
|
+
]);
|
|
143
|
+
const strOpts = jsonSchemaToDiscordOptions({
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: { tag: { type: "string", enum: ["", " ", "ok"] } },
|
|
146
|
+
});
|
|
147
|
+
expect(strOpts.find((o) => o.name === "tag").choices).toEqual([
|
|
148
|
+
{ name: "ok", value: "ok" },
|
|
149
|
+
]);
|
|
150
|
+
warn.mockRestore();
|
|
151
|
+
});
|
|
152
|
+
it("clamps an enum with more than 25 values to exactly 25 choices", () => {
|
|
153
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
154
|
+
const thirty = Array.from({ length: 30 }, (_, i) => i + 1);
|
|
155
|
+
const intOpts = jsonSchemaToDiscordOptions({
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: { level: { type: "integer", enum: thirty } },
|
|
158
|
+
});
|
|
159
|
+
expect(intOpts.find((o) => o.name === "level").choices.length).toBe(25);
|
|
160
|
+
const strOpts = jsonSchemaToDiscordOptions({
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: { tag: { type: "string", enum: thirty.map((n) => `v${n}`) } },
|
|
163
|
+
});
|
|
164
|
+
expect(strOpts.find((o) => o.name === "tag").choices.length).toBe(25);
|
|
165
|
+
warn.mockRestore();
|
|
166
|
+
});
|
|
167
|
+
it("clamps a command to at most 25 options", () => {
|
|
168
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
169
|
+
const properties = {};
|
|
170
|
+
for (let i = 0; i < 30; i++)
|
|
171
|
+
properties[`opt${i}`] = { type: "string" };
|
|
172
|
+
const opts = jsonSchemaToDiscordOptions({ type: "object", properties });
|
|
173
|
+
expect(opts.length).toBe(25);
|
|
174
|
+
warn.mockRestore();
|
|
175
|
+
});
|
|
176
|
+
it("normalizes an option name to a lowercase valid slug (<=32, matches ^[-_a-z0-9]+$)", () => {
|
|
177
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: { "My Opt Name!": { type: "string" } },
|
|
180
|
+
});
|
|
181
|
+
const name = opts[0].name;
|
|
182
|
+
expect(name).toMatch(/^[-_a-z0-9]+$/);
|
|
183
|
+
expect(name.length).toBeGreaterThanOrEqual(1);
|
|
184
|
+
expect(name.length).toBeLessThanOrEqual(32);
|
|
185
|
+
});
|
|
186
|
+
it("skips a string choice whose value exceeds 100 chars (a truncated value would fail enum validation), but keeps a <=100-char member with its exact value", () => {
|
|
187
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
188
|
+
const long = "z".repeat(120);
|
|
189
|
+
const short = "z".repeat(100);
|
|
190
|
+
const strOpts = jsonSchemaToDiscordOptions({
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: { tag: { type: "string", enum: [long, short, "ok"] } },
|
|
193
|
+
});
|
|
194
|
+
const choices = strOpts.find((o) => o.name === "tag").choices;
|
|
195
|
+
// The >100-char member is dropped entirely — NOT emitted with a truncated value.
|
|
196
|
+
expect(choices.some((c) => String(c.value).endsWith("…") || String(c.value).length > 100)).toBe(false);
|
|
197
|
+
expect(choices.some((c) => c.value === long)).toBe(false);
|
|
198
|
+
// The <=100-char members survive with their exact (round-trippable) value.
|
|
199
|
+
expect(choices).toContainEqual({ name: short, value: short });
|
|
200
|
+
expect(choices).toContainEqual({ name: "ok", value: "ok" });
|
|
201
|
+
expect(warn).toHaveBeenCalled();
|
|
202
|
+
warn.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
it("truncates a long choice name (display-only) while keeping the value exact when <=100 chars", () => {
|
|
205
|
+
// A value of exactly 100 chars is kept verbatim; the name is display-only and may be truncated,
|
|
206
|
+
// but at 100 chars it is already within the name cap so it too stays intact.
|
|
207
|
+
const value = "v".repeat(100);
|
|
208
|
+
const strOpts = jsonSchemaToDiscordOptions({
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: { tag: { type: "string", enum: [value] } },
|
|
211
|
+
});
|
|
212
|
+
const choice = strOpts.find((o) => o.name === "tag").choices[0];
|
|
213
|
+
expect(choice.value).toBe(value);
|
|
214
|
+
expect(String(choice.value).length).toBe(100);
|
|
215
|
+
expect(choice.name.length).toBeLessThanOrEqual(100);
|
|
216
|
+
const intOpts = jsonSchemaToDiscordOptions({
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
level: { type: "integer", enum: [Number("1".repeat(120))] },
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
// A 120-digit number → Infinity (not finite/integer) → dropped, so just assert no crash.
|
|
223
|
+
expect(intOpts.find((o) => o.name === "level")).toBeTruthy();
|
|
224
|
+
});
|
|
225
|
+
it("truncates an option description longer than 100 chars", () => {
|
|
226
|
+
const long = "x".repeat(250);
|
|
227
|
+
const opts = jsonSchemaToDiscordOptions({
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: { note: { type: "string", description: long } },
|
|
230
|
+
});
|
|
231
|
+
const note = opts.find((o) => o.name === "note");
|
|
232
|
+
expect(note.description.length).toBeLessThanOrEqual(100);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
describe("buildCommandBody", () => {
|
|
236
|
+
it("truncates a command description longer than 100 chars", () => {
|
|
237
|
+
const long = "y".repeat(250);
|
|
238
|
+
const body = buildCommandBody({
|
|
239
|
+
name: "triage",
|
|
240
|
+
description: long,
|
|
241
|
+
options: undefined,
|
|
242
|
+
});
|
|
243
|
+
expect(body.description.length).toBeLessThanOrEqual(100);
|
|
244
|
+
});
|
|
245
|
+
it("normalizes a command name to a lowercase valid slug (<=32, matches ^[-_a-z0-9]+$)", () => {
|
|
246
|
+
const body = buildCommandBody({
|
|
247
|
+
name: "MyCmd Name!",
|
|
248
|
+
description: "x",
|
|
249
|
+
options: undefined,
|
|
250
|
+
});
|
|
251
|
+
expect(body.name).toMatch(/^[-_a-z0-9]+$/);
|
|
252
|
+
expect(body.name).toBe(body.name.toLowerCase());
|
|
253
|
+
expect(body.name.length).toBeGreaterThanOrEqual(1);
|
|
254
|
+
expect(body.name.length).toBeLessThanOrEqual(32);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe("registerCommands", () => {
|
|
258
|
+
const spec = {
|
|
259
|
+
name: "triage",
|
|
260
|
+
description: "Triage the thread",
|
|
261
|
+
options: undefined,
|
|
262
|
+
};
|
|
263
|
+
it("registers to a guild when guildId is set", async () => {
|
|
264
|
+
const put = vi.fn(async (_route, _opts) => { });
|
|
265
|
+
await registerCommands({ put }, "app-1", "guild-9", [spec]);
|
|
266
|
+
expect(put).toHaveBeenCalledTimes(1);
|
|
267
|
+
const [route, body] = put.mock.calls[0];
|
|
268
|
+
expect(String(route)).toContain("guild-9");
|
|
269
|
+
expect(body.body[0]).toMatchObject({
|
|
270
|
+
name: "triage",
|
|
271
|
+
description: "Triage the thread",
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
it("registers globally when guildId is absent", async () => {
|
|
275
|
+
const put = vi.fn(async (_route, _opts) => { });
|
|
276
|
+
await registerCommands({ put }, "app-1", undefined, [spec]);
|
|
277
|
+
const [route] = put.mock.calls[0];
|
|
278
|
+
expect(String(route)).not.toContain("guild");
|
|
279
|
+
expect(String(route)).toContain("app-1");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
2
|
+
import type { ConversationStore, AgentSession, ReplyTarget as BotReplyTarget } from "@copilotkit/channels";
|
|
3
|
+
import type { DiscordAttachmentRef, FileDeliveryConfig } from "./download-files.js";
|
|
4
|
+
/** A single Discord message, normalized for history reconstruction. */
|
|
5
|
+
export interface DiscordHistoryMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
content: string;
|
|
8
|
+
authorId?: string;
|
|
9
|
+
authorIsBot?: boolean;
|
|
10
|
+
attachments: DiscordAttachmentRef[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Backed entirely by the Discord channel: every turn pulls the current
|
|
14
|
+
* channel/thread history, translates it into the AG-UI message shape, and hands
|
|
15
|
+
* a fresh agent to the turn-runner with its `messages` set to that history.
|
|
16
|
+
* Mirrors bot-slack's store — the platform is the durable storage, so the
|
|
17
|
+
* bridge keeps no separate per-conversation state. The inbound (triggering)
|
|
18
|
+
* message is part of the reconstructed history, so the app passes NO prompt.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DiscordConversationStore implements ConversationStore {
|
|
21
|
+
private deps;
|
|
22
|
+
constructor(deps: {
|
|
23
|
+
/** Channel history, OLDEST→NEWEST. */
|
|
24
|
+
fetchHistory: (channelId: string) => Promise<DiscordHistoryMessage[]>;
|
|
25
|
+
/** The bot's own user id (lazily resolved after `ready`). */
|
|
26
|
+
botUserId: () => string;
|
|
27
|
+
/** Inbound-file handling tunables (size caps, fetch impl, …). */
|
|
28
|
+
filesConfig?: FileDeliveryConfig;
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Build a fresh AgentSession for this conversation by fetching its Discord
|
|
32
|
+
* history and translating it into the AG-UI message shape. Rebuilt every
|
|
33
|
+
* turn (no in-memory cache) — the Discord channel is the source of truth.
|
|
34
|
+
*/
|
|
35
|
+
getOrCreate(conversationKey: string, _replyTarget: BotReplyTarget, makeAgent: (threadId: string) => AbstractAgent): Promise<AgentSession>;
|
|
36
|
+
/**
|
|
37
|
+
* Fetch the channel history (oldest→newest) and translate it into AG-UI
|
|
38
|
+
* messages, folding the bot's chunked replies into a single assistant turn,
|
|
39
|
+
* skipping the bot's own stream placeholders, stripping mention tokens from
|
|
40
|
+
* user text, and turning a user message's uploaded files into multimodal
|
|
41
|
+
* content (bot-slack parity).
|
|
42
|
+
*/
|
|
43
|
+
private reconstruct;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=conversation-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-store.d.ts","sourceRoot":"","sources":["../src/conversation-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EACV,iBAAiB,EACjB,YAAY,EACZ,WAAW,IAAI,cAAc,EAC9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAEV,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,qBAAqB,CAAC;AAG7B,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,oBAAoB,EAAE,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,YAAW,iBAAiB;IAE9D,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE;QACZ,sCAAsC;QACtC,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;QACtE,6DAA6D;QAC7D,SAAS,EAAE,MAAM,MAAM,CAAC;QACxB,iEAAiE;QACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;KAClC;IAGH;;;;OAIG;IACG,WAAW,CACf,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,cAAc,EAC5B,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,GAC7C,OAAO,CAAC,YAAY,CAAC;IAOxB;;;;;;OAMG;YACW,WAAW;CA+C1B"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { buildFileContentParts } from "./download-files.js";
|
|
2
|
+
import { STREAM_PLACEHOLDERS } from "./chunked-message-stream.js";
|
|
3
|
+
/**
|
|
4
|
+
* Backed entirely by the Discord channel: every turn pulls the current
|
|
5
|
+
* channel/thread history, translates it into the AG-UI message shape, and hands
|
|
6
|
+
* a fresh agent to the turn-runner with its `messages` set to that history.
|
|
7
|
+
* Mirrors bot-slack's store — the platform is the durable storage, so the
|
|
8
|
+
* bridge keeps no separate per-conversation state. The inbound (triggering)
|
|
9
|
+
* message is part of the reconstructed history, so the app passes NO prompt.
|
|
10
|
+
*/
|
|
11
|
+
export class DiscordConversationStore {
|
|
12
|
+
deps;
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a fresh AgentSession for this conversation by fetching its Discord
|
|
18
|
+
* history and translating it into the AG-UI message shape. Rebuilt every
|
|
19
|
+
* turn (no in-memory cache) — the Discord channel is the source of truth.
|
|
20
|
+
*/
|
|
21
|
+
async getOrCreate(conversationKey, _replyTarget, makeAgent) {
|
|
22
|
+
const agent = makeAgent(conversationKey);
|
|
23
|
+
const history = await this.reconstruct(conversationKey);
|
|
24
|
+
agent.messages = history;
|
|
25
|
+
return { agent };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fetch the channel history (oldest→newest) and translate it into AG-UI
|
|
29
|
+
* messages, folding the bot's chunked replies into a single assistant turn,
|
|
30
|
+
* skipping the bot's own stream placeholders, stripping mention tokens from
|
|
31
|
+
* user text, and turning a user message's uploaded files into multimodal
|
|
32
|
+
* content (bot-slack parity).
|
|
33
|
+
*/
|
|
34
|
+
async reconstruct(channelId) {
|
|
35
|
+
const botId = this.deps.botUserId();
|
|
36
|
+
const messages = await this.deps.fetchHistory(channelId);
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const m of messages) {
|
|
39
|
+
const isBot = m.authorIsBot || m.authorId === botId;
|
|
40
|
+
// Skip the bot's own streaming placeholders ("_thinking…_" / "…(continued)").
|
|
41
|
+
if (isBot && STREAM_PLACEHOLDERS.includes(m.content))
|
|
42
|
+
continue;
|
|
43
|
+
const text = isBot
|
|
44
|
+
? m.content
|
|
45
|
+
: m.content
|
|
46
|
+
.replace(/<@!?\d+>/g, "")
|
|
47
|
+
.replace(/\s+/g, " ")
|
|
48
|
+
.trim();
|
|
49
|
+
const hasFiles = !isBot && m.attachments.length > 0;
|
|
50
|
+
if (!text && !hasFiles)
|
|
51
|
+
continue;
|
|
52
|
+
const role = isBot ? "assistant" : "user";
|
|
53
|
+
// User message with uploaded files → multimodal content (the agent's
|
|
54
|
+
// model reads the images / decoded text).
|
|
55
|
+
if (hasFiles) {
|
|
56
|
+
const parts = await buildFileContentParts(m.attachments, this.deps.filesConfig);
|
|
57
|
+
const content = [];
|
|
58
|
+
if (text)
|
|
59
|
+
content.push({ type: "text", text });
|
|
60
|
+
content.push(...parts);
|
|
61
|
+
if (content.length === 0)
|
|
62
|
+
continue; // nothing usable
|
|
63
|
+
out.push({ id: m.id, role, content });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Fold consecutive same-role *string* messages — the bot's chunked
|
|
67
|
+
// replies are one assistant turn rendered as N Discord messages.
|
|
68
|
+
const tail = out[out.length - 1];
|
|
69
|
+
if (tail && tail.role === role && typeof tail.content === "string") {
|
|
70
|
+
tail.content = `${tail.content}\n${text}`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out.push({ id: m.id, role, content: text });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-store.test.d.ts","sourceRoot":"","sources":["../src/conversation-store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { DiscordConversationStore } from "./conversation-store.js";
|
|
3
|
+
/** Captures the `messages` assigned onto the fake agent by the store. */
|
|
4
|
+
function fakeAgent() {
|
|
5
|
+
return { threadId: "", messages: undefined };
|
|
6
|
+
}
|
|
7
|
+
describe("DiscordConversationStore", () => {
|
|
8
|
+
it("reconstructs channel history into AG-UI messages each turn", async () => {
|
|
9
|
+
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
10
|
+
// A stubbed fetch for buildFileContentParts so no real network happens.
|
|
11
|
+
const fetchImpl = vi.fn(async () => ({
|
|
12
|
+
ok: true,
|
|
13
|
+
arrayBuffer: async () => pngBytes.buffer,
|
|
14
|
+
}));
|
|
15
|
+
const history = [
|
|
16
|
+
{
|
|
17
|
+
id: "m1",
|
|
18
|
+
content: "<@123> hey bot",
|
|
19
|
+
authorId: "u1",
|
|
20
|
+
authorIsBot: false,
|
|
21
|
+
attachments: [],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "m2",
|
|
25
|
+
content: "_thinking…_", // bot placeholder — must be skipped
|
|
26
|
+
authorId: "bot-1",
|
|
27
|
+
authorIsBot: true,
|
|
28
|
+
attachments: [],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "m3",
|
|
32
|
+
content: "here is the",
|
|
33
|
+
authorId: "bot-1",
|
|
34
|
+
authorIsBot: true,
|
|
35
|
+
attachments: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "m4",
|
|
39
|
+
content: "real answer", // consecutive bot string → folded into m3
|
|
40
|
+
authorId: "bot-1",
|
|
41
|
+
authorIsBot: true,
|
|
42
|
+
attachments: [],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "m5",
|
|
46
|
+
content: "<@123> look at this",
|
|
47
|
+
authorId: "u1",
|
|
48
|
+
authorIsBot: false,
|
|
49
|
+
attachments: [
|
|
50
|
+
{
|
|
51
|
+
url: "https://cdn.discord/a.png",
|
|
52
|
+
name: "a.png",
|
|
53
|
+
contentType: "image/png",
|
|
54
|
+
size: pngBytes.length,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
const fetchHistory = vi.fn(async () => history);
|
|
60
|
+
const store = new DiscordConversationStore({
|
|
61
|
+
fetchHistory,
|
|
62
|
+
botUserId: () => "bot-1",
|
|
63
|
+
filesConfig: { fetchImpl },
|
|
64
|
+
});
|
|
65
|
+
const agent = fakeAgent();
|
|
66
|
+
const session = await store.getOrCreate("c1", { channelId: "c1" }, () => agent);
|
|
67
|
+
expect(fetchHistory).toHaveBeenCalledWith("c1");
|
|
68
|
+
expect(session.agent).toBe(agent);
|
|
69
|
+
expect(agent.messages).toEqual([
|
|
70
|
+
// user text, mention token stripped
|
|
71
|
+
{ id: "m1", role: "user", content: "hey bot" },
|
|
72
|
+
// placeholder skipped; two consecutive bot strings folded into one turn
|
|
73
|
+
{ id: "m3", role: "assistant", content: "here is the\nreal answer" },
|
|
74
|
+
// user message with an image → multimodal content (text part + image part)
|
|
75
|
+
{
|
|
76
|
+
id: "m5",
|
|
77
|
+
role: "user",
|
|
78
|
+
content: [
|
|
79
|
+
{ type: "text", text: "look at this" },
|
|
80
|
+
{
|
|
81
|
+
type: "image",
|
|
82
|
+
source: {
|
|
83
|
+
type: "data",
|
|
84
|
+
value: Buffer.from(pngBytes).toString("base64"),
|
|
85
|
+
mimeType: "image/png",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it("rebuilds from the channel every turn (no in-memory caching)", async () => {
|
|
93
|
+
const fetchHistory = vi.fn(async () => []);
|
|
94
|
+
const store = new DiscordConversationStore({
|
|
95
|
+
fetchHistory,
|
|
96
|
+
botUserId: () => "bot-1",
|
|
97
|
+
});
|
|
98
|
+
const makeAgent = vi.fn(() => fakeAgent());
|
|
99
|
+
await store.getOrCreate("c1", { channelId: "c1" }, makeAgent);
|
|
100
|
+
await store.getOrCreate("c1", { channelId: "c1" }, makeAgent);
|
|
101
|
+
// A fresh agent + a fresh history fetch per turn — the channel is the
|
|
102
|
+
// source of truth, so nothing is cached.
|
|
103
|
+
expect(makeAgent).toHaveBeenCalledTimes(2);
|
|
104
|
+
expect(fetchHistory).toHaveBeenCalledTimes(2);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { IncomingReaction } from "@copilotkit/channels";
|
|
2
|
+
import type { IncomingTurn, ReplyTarget } from "./types.js";
|
|
3
|
+
import type { PendingInteractions } from "./pending-interactions.js";
|
|
4
|
+
interface MessageLike {
|
|
5
|
+
author: {
|
|
6
|
+
id: string;
|
|
7
|
+
bot?: boolean;
|
|
8
|
+
username?: string;
|
|
9
|
+
globalName?: string | null;
|
|
10
|
+
};
|
|
11
|
+
content: string;
|
|
12
|
+
channelId: string;
|
|
13
|
+
guildId?: string | null;
|
|
14
|
+
mentions: {
|
|
15
|
+
has(id: string): boolean;
|
|
16
|
+
users?: {
|
|
17
|
+
has(id: string): boolean;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
channel: {
|
|
21
|
+
isDMBased(): boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface ChatInputLike {
|
|
25
|
+
isChatInputCommand(): boolean;
|
|
26
|
+
/** Discord interaction id — used as the pending-interaction triggerId. */
|
|
27
|
+
id: string;
|
|
28
|
+
commandName: string;
|
|
29
|
+
channelId: string;
|
|
30
|
+
guildId?: string | null;
|
|
31
|
+
user: {
|
|
32
|
+
id: string;
|
|
33
|
+
username?: string;
|
|
34
|
+
globalName?: string | null;
|
|
35
|
+
};
|
|
36
|
+
options: {
|
|
37
|
+
data: ReadonlyArray<{
|
|
38
|
+
name: string;
|
|
39
|
+
value: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
/** discord.js interaction state — set once the interaction has been deferred. */
|
|
43
|
+
deferred?: boolean;
|
|
44
|
+
/** discord.js interaction state — set once a reply has been sent. */
|
|
45
|
+
replied?: boolean;
|
|
46
|
+
/** Remove the (deferred) reply. Used to clear a dangling ephemeral ack. */
|
|
47
|
+
deleteReply?: () => Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
export interface ClientLike {
|
|
50
|
+
on(event: "messageCreate", cb: (msg: MessageLike) => void): void;
|
|
51
|
+
on(event: "interactionCreate", cb: (i: ChatInputLike) => void): void;
|
|
52
|
+
on(event: "messageReactionAdd" | "messageReactionRemove", cb: (reaction: unknown, user: unknown) => void): void;
|
|
53
|
+
on(event: string, cb: (arg: unknown) => void): void;
|
|
54
|
+
}
|
|
55
|
+
export interface IncomingCommandRaw {
|
|
56
|
+
command: string;
|
|
57
|
+
text: string;
|
|
58
|
+
rawOptions: Record<string, unknown>;
|
|
59
|
+
conversationKey: string;
|
|
60
|
+
replyTarget: ReplyTarget;
|
|
61
|
+
senderUserId: string;
|
|
62
|
+
/** Pending-interaction triggerId (the live interaction id) — backs `openModal`. */
|
|
63
|
+
triggerId?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ListenerConfig {
|
|
66
|
+
client: ClientLike;
|
|
67
|
+
/**
|
|
68
|
+
* The bot's own user id. May be a getter so the adapter can attach the
|
|
69
|
+
* listener once at startup and supply the id lazily after the `ready`
|
|
70
|
+
* event (when it first becomes known).
|
|
71
|
+
*/
|
|
72
|
+
botUserId: string | (() => string);
|
|
73
|
+
onTurn(turn: IncomingTurn): void | Promise<void>;
|
|
74
|
+
onCommand(cmd: IncomingCommandRaw): void | Promise<void>;
|
|
75
|
+
/** Optional: called when a user adds or removes a reaction. */
|
|
76
|
+
onReaction?: (evt: IncomingReaction) => void | Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Pending-interaction registry for slash commands. The live command
|
|
79
|
+
* interaction is registered (arming an auto-`deferReply`), dispatched with a
|
|
80
|
+
* `triggerId` so a handler may `openModal` first, then settled. When absent,
|
|
81
|
+
* the command path skips registration (no modal support / no ack).
|
|
82
|
+
*/
|
|
83
|
+
commandPending?: PendingInteractions;
|
|
84
|
+
}
|
|
85
|
+
/** Wire Gateway events to normalized turns/commands. Mirrors attachSlackListener. */
|
|
86
|
+
export declare function attachDiscordListener(cfg: ListenerConfig): void;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=discord-listener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord-listener.d.ts","sourceRoot":"","sources":["../src/discord-listener.ts"],"names":[],"mappings":"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"}
|