@friendlyrobot/discord-pi-agent 0.21.1 → 0.21.4
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/README.md +14 -3
- package/dist/agent-model-service.js +132 -0
- package/dist/agent-resource-service.js +70 -0
- package/dist/agent-service.js +189 -0
- package/dist/agent-turn-runner.js +148 -0
- package/dist/config.js +103 -0
- package/dist/debug-print.js +22 -0
- package/dist/discord-attachments.js +148 -0
- package/dist/discord-auth.d.ts +3 -2
- package/dist/discord-auth.js +37 -0
- package/dist/discord-gateway-client.js +49 -0
- package/dist/discord-media-resolution.js +107 -0
- package/dist/discord-message-handler.js +195 -0
- package/dist/discord-replies.js +112 -0
- package/dist/discord-typing.js +75 -0
- package/dist/index.js +57 -1904
- package/dist/logger.js +26 -0
- package/dist/markdown-table-transformer.js +138 -0
- package/dist/media-description.js +87 -0
- package/dist/message-chunker.js +38 -0
- package/dist/prompt-context.d.ts +3 -0
- package/dist/prompt-context.js +48 -0
- package/dist/prompt-queue.js +37 -0
- package/dist/session-commands.js +281 -0
- package/dist/session-registry.d.ts +1 -1
- package/dist/session-registry.js +73 -0
- package/dist/types.js +1 -0
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -1,1916 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...baseOptions,
|
|
25
|
-
transport: {
|
|
26
|
-
target: "pino-pretty",
|
|
27
|
-
options: {
|
|
28
|
-
colorize: true,
|
|
29
|
-
colorizeObjects: true,
|
|
30
|
-
levelFirst: true,
|
|
31
|
-
translateTime: "SYS:standard",
|
|
32
|
-
ignore: "pid,hostname",
|
|
33
|
-
singleLine: false,
|
|
34
|
-
messageFormat: "[{module}] {if direction}{direction} {end}{msg}"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}) : pino(baseOptions);
|
|
38
|
-
function createModuleLogger(moduleName) {
|
|
39
|
-
return logger.child({ module: moduleName });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/agent-model-service.ts
|
|
43
|
-
var logger2 = createModuleLogger("agent-model-service");
|
|
44
|
-
|
|
45
|
-
class AgentModelService {
|
|
46
|
-
config;
|
|
47
|
-
modelRegistry;
|
|
48
|
-
constructor(config, modelRegistry) {
|
|
49
|
-
this.config = config;
|
|
50
|
-
this.modelRegistry = modelRegistry;
|
|
51
|
-
}
|
|
52
|
-
findModel(provider, modelId) {
|
|
53
|
-
return this.modelRegistry.find(provider, modelId);
|
|
54
|
-
}
|
|
55
|
-
async ensureSessionHasConfiguredModel(session) {
|
|
56
|
-
if (session.model) {
|
|
57
|
-
logger2.debug({
|
|
58
|
-
model: `${session.model.provider}/${session.model.id}`
|
|
59
|
-
}, "retaining existing session model");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
|
|
63
|
-
const availableModels = await this.modelRegistry.getAvailable();
|
|
64
|
-
logger2.debug({
|
|
65
|
-
count: availableModels.length,
|
|
66
|
-
matches: availableModels.filter((model) => {
|
|
67
|
-
return model.provider === this.config.modelProvider;
|
|
68
|
-
}).map((model) => `${model.provider}/${model.id}`)
|
|
69
|
-
}, "available models");
|
|
70
|
-
if (!desiredModel) {
|
|
71
|
-
throw new Error(`Configured model not found: ${this.config.modelProvider}/${this.config.modelId}. Check your pi agent config and installed extensions.`);
|
|
72
|
-
}
|
|
73
|
-
logger2.info({
|
|
74
|
-
to: `${desiredModel.provider}/${desiredModel.id}`
|
|
75
|
-
}, "setting initial session model");
|
|
76
|
-
await session.setModel(desiredModel);
|
|
77
|
-
await this.applyConfiguredThinkingLevelForSession(session);
|
|
78
|
-
}
|
|
79
|
-
async listModels(session) {
|
|
80
|
-
const availableModels = await this.modelRegistry.getAvailable();
|
|
81
|
-
const currentDisplay = session?.model ? `${session.model.provider}/${session.model.id}` : null;
|
|
82
|
-
const lines = availableModels.map((model) => {
|
|
83
|
-
const display = `${model.provider}/${model.id}`;
|
|
84
|
-
const marker = currentDisplay === display ? " (current)" : "";
|
|
85
|
-
return ` ${display}${marker}`;
|
|
86
|
-
});
|
|
87
|
-
return [
|
|
88
|
-
`Available models (${availableModels.length}):`,
|
|
89
|
-
...lines,
|
|
90
|
-
`
|
|
91
|
-
Usage: !model <provider/modelId> to switch.`
|
|
92
|
-
].join(`
|
|
93
|
-
`);
|
|
94
|
-
}
|
|
95
|
-
async switchModel(provider, modelId, session) {
|
|
96
|
-
const model = this.modelRegistry.find(provider, modelId);
|
|
97
|
-
if (!model) {
|
|
98
|
-
const availableModels = await this.modelRegistry.getAvailable();
|
|
99
|
-
const matches = availableModels.filter((availableModel) => {
|
|
100
|
-
return availableModel.provider === provider;
|
|
101
|
-
}).map((availableModel) => {
|
|
102
|
-
return `${availableModel.provider}/${availableModel.id}`;
|
|
103
|
-
});
|
|
104
|
-
const hint = matches.length > 0 ? `
|
|
105
|
-
Models from "${provider}": ${matches.join(", ")}` : `
|
|
106
|
-
Use !model to see all available models.`;
|
|
107
|
-
return `Model not found: ${provider}/${modelId}.${hint}`;
|
|
108
|
-
}
|
|
109
|
-
if (isSameModel(session.model, model)) {
|
|
110
|
-
return `Already using ${provider}/${modelId}.`;
|
|
111
|
-
}
|
|
112
|
-
await session.setModel(model);
|
|
113
|
-
await this.applyConfiguredThinkingLevelForSession(session);
|
|
114
|
-
const thinkingInfo = session.supportsThinking() ? ` (thinking: ${session.thinkingLevel})` : "";
|
|
115
|
-
return `Switched to ${provider}/${modelId}${thinkingInfo}.`;
|
|
116
|
-
}
|
|
117
|
-
getCurrentModelDisplay(session) {
|
|
118
|
-
if (!session?.model) {
|
|
119
|
-
return "(no model selected)";
|
|
120
|
-
}
|
|
121
|
-
return `${session.model.provider}/${session.model.id}`;
|
|
122
|
-
}
|
|
123
|
-
getThinkingLevel(session) {
|
|
124
|
-
if (!session.supportsThinking()) {
|
|
125
|
-
return { current: "off", available: [], supported: false };
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
current: session.thinkingLevel,
|
|
129
|
-
available: session.getAvailableThinkingLevels(),
|
|
130
|
-
supported: true
|
|
1
|
+
import { AgentService } from "./agent-service";
|
|
2
|
+
import { resolveConfig } from "./config";
|
|
3
|
+
import { startGatewayClient } from "./discord-gateway-client";
|
|
4
|
+
import { createModuleLogger } from "./logger";
|
|
5
|
+
import { SessionRegistry } from "./session-registry";
|
|
6
|
+
const logger = createModuleLogger("index");
|
|
7
|
+
export { formatDiscordPromptTime } from "./prompt-context";
|
|
8
|
+
export { loadDiscordGatewayConfigFromEnv, resolveConfig } from "./config";
|
|
9
|
+
/**
|
|
10
|
+
* Start the unified Discord gateway. Supports DM and forum thread sessions
|
|
11
|
+
* out of the box. Set discordAllowedForumChannelIds to enable forum support.
|
|
12
|
+
*/
|
|
13
|
+
export async function startDiscordGateway(config) {
|
|
14
|
+
const resolvedConfig = resolveConfig(config);
|
|
15
|
+
const agentService = new AgentService(resolvedConfig);
|
|
16
|
+
logger.info("initializing agent service");
|
|
17
|
+
await agentService.initialize();
|
|
18
|
+
logger.info(agentService.getStatus(), "agent ready");
|
|
19
|
+
const accessConfig = {
|
|
20
|
+
discordAllowedUserId: resolvedConfig.discordAllowedUserId,
|
|
21
|
+
discordAllowedForumChannelIds: resolvedConfig.discordAllowedForumChannelIds,
|
|
22
|
+
discordAllowedUserIds: resolvedConfig.discordAllowedUserIds,
|
|
23
|
+
startupMessage: resolvedConfig.startupMessage,
|
|
131
24
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const available = session.getAvailableThinkingLevels();
|
|
138
|
-
if (!available.includes(level)) {
|
|
139
|
-
return `Invalid thinking level "${level}" for current model. Available: ${available.join(", ")}`;
|
|
25
|
+
const sessionRegistry = new SessionRegistry(agentService);
|
|
26
|
+
const client = await startGatewayClient(resolvedConfig, agentService, sessionRegistry, accessConfig);
|
|
27
|
+
const stop = createGatewayStopHandler(client, agentService, sessionRegistry, resolvedConfig);
|
|
28
|
+
if (resolvedConfig.shutdownOnSignals) {
|
|
29
|
+
registerSignalHandlers(stop);
|
|
140
30
|
}
|
|
141
|
-
session.setThinkingLevel(level);
|
|
142
|
-
return `Thinking level set to "${level}".`;
|
|
143
|
-
}
|
|
144
|
-
async applyConfiguredThinkingLevelForSession(session) {
|
|
145
|
-
if (session.supportsThinking()) {
|
|
146
|
-
const available = session.getAvailableThinkingLevels();
|
|
147
|
-
if (available.includes(this.config.thinkingLevel)) {
|
|
148
|
-
session.setThinkingLevel(this.config.thinkingLevel);
|
|
149
|
-
logger2.debug({
|
|
150
|
-
level: this.config.thinkingLevel
|
|
151
|
-
}, "thinking level applied");
|
|
152
|
-
} else {
|
|
153
|
-
logger2.debug({
|
|
154
|
-
requested: this.config.thinkingLevel,
|
|
155
|
-
available
|
|
156
|
-
}, "thinking level not available for model");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function isSameModel(currentModel, desiredModel) {
|
|
162
|
-
if (!currentModel) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
return currentModel.provider === desiredModel.provider && currentModel.id === desiredModel.id;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// src/agent-resource-service.ts
|
|
169
|
-
class AgentResourceService {
|
|
170
|
-
resourceLoader;
|
|
171
|
-
constructor(resourceLoader) {
|
|
172
|
-
this.resourceLoader = resourceLoader;
|
|
173
|
-
}
|
|
174
|
-
getSkillsSummary() {
|
|
175
|
-
const result = this.resourceLoader.getSkills();
|
|
176
|
-
const { skills } = result;
|
|
177
|
-
if (skills.length === 0) {
|
|
178
|
-
return "Skills: (none loaded)";
|
|
179
|
-
}
|
|
180
|
-
const names = skills.map((skill) => {
|
|
181
|
-
return skill.name;
|
|
182
|
-
});
|
|
183
|
-
return `Skills (${skills.length}): ${names.join(", ") || "(none)"}`;
|
|
184
|
-
}
|
|
185
|
-
getExtensionsSummary() {
|
|
186
|
-
const result = this.resourceLoader.getExtensions();
|
|
187
|
-
const { extensions, errors } = result;
|
|
188
|
-
if (extensions.length === 0) {
|
|
189
|
-
return "Extensions: (none loaded)";
|
|
190
|
-
}
|
|
191
|
-
const lines = extensions.map((extension) => {
|
|
192
|
-
const toolCount = extension.tools.size;
|
|
193
|
-
const commandCount = extension.commands.size;
|
|
194
|
-
const parts = [];
|
|
195
|
-
if (toolCount > 0) {
|
|
196
|
-
parts.push(`${toolCount} tool${toolCount !== 1 ? "s" : ""}`);
|
|
197
|
-
}
|
|
198
|
-
if (commandCount > 0) {
|
|
199
|
-
parts.push(`${commandCount} command${commandCount !== 1 ? "s" : ""}`);
|
|
200
|
-
}
|
|
201
|
-
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
202
|
-
return ` ${extension.path}${summary}`;
|
|
203
|
-
});
|
|
204
|
-
const header = `Extensions (${extensions.length}):`;
|
|
205
|
-
const errorLines = errors.length > 0 ? [
|
|
206
|
-
`Errors (${errors.length}):`,
|
|
207
|
-
...errors.map((error) => {
|
|
208
|
-
return ` ${error.path}: ${error.error}`;
|
|
209
|
-
})
|
|
210
|
-
] : [];
|
|
211
|
-
return [header, ...lines, ...errorLines].join(`
|
|
212
|
-
`);
|
|
213
|
-
}
|
|
214
|
-
async reloadResources() {
|
|
215
|
-
await this.resourceLoader.reload();
|
|
216
|
-
const extensions = this.resourceLoader.getExtensions().extensions.map((extension) => {
|
|
217
|
-
return extension.path;
|
|
218
|
-
});
|
|
219
|
-
const skills = this.resourceLoader.getSkills();
|
|
220
|
-
const skillNames = skills.skills.map((skill) => {
|
|
221
|
-
return skill.name;
|
|
222
|
-
});
|
|
223
|
-
const agentsFiles = this.resourceLoader.getAgentsFiles().agentsFiles.map((file) => {
|
|
224
|
-
return file.path;
|
|
225
|
-
});
|
|
226
|
-
return [
|
|
227
|
-
"Resources reloaded.",
|
|
228
|
-
`Extensions (${extensions.length}): ${extensions.join(", ") || "(none)"}`,
|
|
229
|
-
`Skills (${skills.skills.length}): ${skillNames.join(", ") || "(none)"}`,
|
|
230
|
-
`AGENTS.md files (${agentsFiles.length}): ${agentsFiles.join(", ") || "(none)"}`
|
|
231
|
-
].join(`
|
|
232
|
-
`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// src/debug-print.ts
|
|
237
|
-
function debugPrint(body, title) {
|
|
238
|
-
const WIDTH = 80;
|
|
239
|
-
const label = title ?? "DEBUG";
|
|
240
|
-
function centeredFence(text) {
|
|
241
|
-
const inner = ` ${text} `;
|
|
242
|
-
const padLen = Math.floor((WIDTH - inner.length) / 2);
|
|
243
|
-
const pad = "=".repeat(padLen);
|
|
244
|
-
const result = pad + inner + pad;
|
|
245
|
-
return result.length < WIDTH ? result + "=" : result;
|
|
246
|
-
}
|
|
247
|
-
console.info(centeredFence(label));
|
|
248
|
-
console.info(body);
|
|
249
|
-
console.info(centeredFence(`${label} END`));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// src/markdown-table-transformer.ts
|
|
253
|
-
import { Lexer } from "marked";
|
|
254
|
-
var logger3 = createModuleLogger("markdown-table-transformer");
|
|
255
|
-
var CODE_BLOCK_WRAPPER = "```\n{TABLE}\n```";
|
|
256
|
-
async function transformMarkdownTablesToCodeBlocks(text) {
|
|
257
|
-
const normalized = normalizeCodeFences(text);
|
|
258
|
-
const formatted = await formatWithPrettier(normalized);
|
|
259
|
-
const tokens = Lexer.lex(formatted);
|
|
260
|
-
const result = [];
|
|
261
|
-
for (const token of tokens) {
|
|
262
|
-
if (token.type === "table") {
|
|
263
|
-
result.push(CODE_BLOCK_WRAPPER.replace("{TABLE}", token.raw.trimEnd()));
|
|
264
|
-
} else {
|
|
265
|
-
result.push(token.raw);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return formatWithPrettier(result.join(""));
|
|
269
|
-
}
|
|
270
|
-
function normalizeCodeFences(text) {
|
|
271
|
-
const lines = text.split(`
|
|
272
|
-
`);
|
|
273
|
-
const result = [];
|
|
274
|
-
for (const line of lines) {
|
|
275
|
-
const trimmed = line.trimEnd();
|
|
276
|
-
if (trimmed.endsWith("```") && !trimmed.startsWith("```")) {
|
|
277
|
-
const beforeFence = trimmed.slice(0, -3).trimEnd();
|
|
278
|
-
if (beforeFence) {
|
|
279
|
-
result.push(beforeFence);
|
|
280
|
-
}
|
|
281
|
-
result.push("```");
|
|
282
|
-
} else if (trimmed.startsWith("```") && !isValidFenceLine(trimmed)) {
|
|
283
|
-
result.push("```");
|
|
284
|
-
const afterFence = trimmed.slice(3).trimStart();
|
|
285
|
-
if (afterFence) {
|
|
286
|
-
result.push(afterFence);
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
result.push(line);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return result.join(`
|
|
293
|
-
`);
|
|
294
|
-
}
|
|
295
|
-
function isValidFenceLine(line) {
|
|
296
|
-
const trimmed = line.trimEnd();
|
|
297
|
-
if (trimmed === "```") {
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
|
-
const afterFence = trimmed.slice(3);
|
|
301
|
-
if (!afterFence) {
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
if (afterFence.codePointAt(0) > 127) {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
const tokens = afterFence.trimStart().split(/\s+/);
|
|
308
|
-
if (tokens.length > 1) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
if (tokens[0].includes("`")) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
async function formatWithPrettier(text) {
|
|
317
|
-
const prettier = await import("prettier");
|
|
318
|
-
try {
|
|
319
|
-
const formatted = await prettier.format(text, {
|
|
320
|
-
parser: "markdown",
|
|
321
|
-
printWidth: 80
|
|
322
|
-
});
|
|
323
|
-
return formatted.trim();
|
|
324
|
-
} catch (error) {
|
|
325
|
-
logger3.error({
|
|
326
|
-
error
|
|
327
|
-
}, "Prettier formatting failed");
|
|
328
|
-
return text;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// src/agent-turn-runner.ts
|
|
333
|
-
var logger4 = createModuleLogger("agent-turn-runner");
|
|
334
|
-
async function runAgentTurn(session, prompt, options = {}) {
|
|
335
|
-
let streamedText = "";
|
|
336
|
-
let eventCount = 0;
|
|
337
|
-
let toolCount = 0;
|
|
338
|
-
const toolInputsByCallId = new Map;
|
|
339
|
-
const model = session.model ? `${session.model.provider}/${session.model.id}` : "none";
|
|
340
|
-
debugPrint(prompt, "Full Prompt");
|
|
341
|
-
const unsubscribe = session.subscribe((event) => {
|
|
342
|
-
eventCount += 1;
|
|
343
|
-
if (event.type === "message_update") {
|
|
344
|
-
if (event.assistantMessageEvent.type === "text_delta") {
|
|
345
|
-
streamedText += event.assistantMessageEvent.delta;
|
|
346
|
-
}
|
|
347
|
-
if (event.assistantMessageEvent.type === "thinking_delta") {}
|
|
348
|
-
}
|
|
349
|
-
if (event.type === "tool_execution_start") {
|
|
350
|
-
toolCount += 1;
|
|
351
|
-
const input = event.toolName === "bash" ? event.args.command : event.args;
|
|
352
|
-
toolInputsByCallId.set(event.toolCallId, input);
|
|
353
|
-
if (event.toolName === "bash") {
|
|
354
|
-
debugPrint(input, "CMD");
|
|
355
|
-
} else {
|
|
356
|
-
logger4.debug({
|
|
357
|
-
toolName: event.toolName
|
|
358
|
-
}, `agent tool start: [${event.toolName}]`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (event.type === "tool_execution_end") {
|
|
362
|
-
const input = toolInputsByCallId.get(event.toolCallId);
|
|
363
|
-
toolInputsByCallId.delete(event.toolCallId);
|
|
364
|
-
if (event.toolName === "bash") {
|
|
365
|
-
debugPrint(extractToolOutput(event.result), event.isError ? "BASH TOOL ERROR OUTPUT" : "BASH TOOL OUTPUT");
|
|
366
|
-
} else {
|
|
367
|
-
logger4.debug({
|
|
368
|
-
toolName: event.toolName,
|
|
369
|
-
isError: event.isError
|
|
370
|
-
}, `agent tool end: [${event.toolName}]`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
try {
|
|
375
|
-
await session.prompt(prompt, { images: options.images });
|
|
376
|
-
} finally {
|
|
377
|
-
unsubscribe();
|
|
378
|
-
}
|
|
379
|
-
const errorMessage = session.agent.state.errorMessage?.trim();
|
|
380
|
-
const fallbackText = getLatestAssistantText(session.messages);
|
|
381
|
-
const finalText = streamedText.trim() || fallbackText.trim();
|
|
382
|
-
if (errorMessage) {
|
|
383
|
-
return errorMessage;
|
|
384
|
-
}
|
|
385
|
-
if (finalText) {
|
|
386
|
-
const transformed = await transformMarkdownTablesToCodeBlocks(finalText);
|
|
387
|
-
debugPrint(finalText, "BEFORE TRANSFORM");
|
|
388
|
-
debugPrint(transformed, "TRANSFORMED");
|
|
389
|
-
return transformed;
|
|
390
|
-
}
|
|
391
|
-
return "No response generated.";
|
|
392
|
-
}
|
|
393
|
-
function extractToolOutput(output) {
|
|
394
|
-
if (typeof output === "object" && output !== null) {
|
|
395
|
-
const obj = output;
|
|
396
|
-
if (Array.isArray(obj.content)) {
|
|
397
|
-
return obj.content.map((item) => {
|
|
398
|
-
if (item.type === "text" && typeof item.text === "string") {
|
|
399
|
-
return item.text;
|
|
400
|
-
}
|
|
401
|
-
return JSON.stringify(item);
|
|
402
|
-
}).join(`
|
|
403
|
-
`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return String(output);
|
|
407
|
-
}
|
|
408
|
-
function getLatestAssistantText(messages) {
|
|
409
|
-
const latestAssistantMessage = [...messages].reverse().find((message) => {
|
|
410
|
-
return message.role === "assistant";
|
|
411
|
-
});
|
|
412
|
-
if (!latestAssistantMessage || !Array.isArray(latestAssistantMessage.content)) {
|
|
413
|
-
return "";
|
|
414
|
-
}
|
|
415
|
-
return latestAssistantMessage.content.filter((item) => {
|
|
416
|
-
return item.type === "text";
|
|
417
|
-
}).map((item) => {
|
|
418
|
-
return item.text;
|
|
419
|
-
}).join(`
|
|
420
|
-
`).trim();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// src/agent-service.ts
|
|
424
|
-
var logger5 = createModuleLogger("agent-service");
|
|
425
|
-
|
|
426
|
-
class AgentService {
|
|
427
|
-
config;
|
|
428
|
-
authStorage;
|
|
429
|
-
modelRegistry;
|
|
430
|
-
settingsManager;
|
|
431
|
-
resourceLoader;
|
|
432
|
-
session = null;
|
|
433
|
-
models;
|
|
434
|
-
resources;
|
|
435
|
-
constructor(config) {
|
|
436
|
-
this.config = config;
|
|
437
|
-
this.authStorage = AuthStorage.create(path.join(config.agentDir, "auth.json"));
|
|
438
|
-
this.modelRegistry = ModelRegistry.create(this.authStorage, path.join(config.agentDir, "models.json"));
|
|
439
|
-
this.settingsManager = SettingsManager.create(config.cwd, config.agentDir);
|
|
440
|
-
this.resourceLoader = new DefaultResourceLoader({
|
|
441
|
-
cwd: config.cwd,
|
|
442
|
-
agentDir: config.agentDir,
|
|
443
|
-
settingsManager: this.settingsManager
|
|
444
|
-
});
|
|
445
|
-
this.models = new AgentModelService(config, this.modelRegistry);
|
|
446
|
-
this.resources = new AgentResourceService(this.resourceLoader);
|
|
447
|
-
}
|
|
448
|
-
async initialize() {
|
|
449
|
-
await fs.mkdir(this.config.agentDir, { recursive: true });
|
|
450
|
-
await fs.mkdir(this.getSessionDir(), { recursive: true });
|
|
451
|
-
logger5.info({
|
|
452
|
-
cwd: this.config.cwd,
|
|
453
|
-
agentDir: this.config.agentDir,
|
|
454
|
-
sessionDir: this.getSessionDir(),
|
|
455
|
-
modelProvider: this.config.modelProvider,
|
|
456
|
-
modelId: this.config.modelId,
|
|
457
|
-
thinkingLevel: this.config.thinkingLevel
|
|
458
|
-
}, "config");
|
|
459
|
-
await this.resourceLoader.reload();
|
|
460
|
-
logger5.info({
|
|
461
|
-
extensions: this.resourceLoader.getExtensions().extensions.map((extension) => extension.path),
|
|
462
|
-
agentsFiles: this.resourceLoader.getAgentsFiles().agentsFiles.map((file) => file.path)
|
|
463
|
-
}, "resources loaded");
|
|
464
|
-
await this.createOrResumeSession();
|
|
465
|
-
await this.ensureConfiguredModel();
|
|
466
|
-
}
|
|
467
|
-
getSession() {
|
|
468
|
-
return this.session;
|
|
469
|
-
}
|
|
470
|
-
getAgentDir() {
|
|
471
|
-
return this.config.agentDir;
|
|
472
|
-
}
|
|
473
|
-
async createTemporarySession() {
|
|
474
|
-
const { session } = await createAgentSession({
|
|
475
|
-
cwd: this.config.cwd,
|
|
476
|
-
agentDir: this.config.agentDir,
|
|
477
|
-
authStorage: this.authStorage,
|
|
478
|
-
modelRegistry: this.modelRegistry,
|
|
479
|
-
resourceLoader: this.resourceLoader,
|
|
480
|
-
settingsManager: this.settingsManager,
|
|
481
|
-
sessionManager: SessionManager.inMemory(),
|
|
482
|
-
thinkingLevel: "off"
|
|
483
|
-
});
|
|
484
|
-
logger5.debug({ sessionId: session.sessionId }, "temporary session created");
|
|
485
|
-
return session;
|
|
486
|
-
}
|
|
487
|
-
async createSession(sessionDir) {
|
|
488
|
-
await fs.mkdir(sessionDir, { recursive: true });
|
|
489
|
-
const { session } = await createAgentSession({
|
|
490
|
-
cwd: this.config.cwd,
|
|
491
|
-
agentDir: this.config.agentDir,
|
|
492
|
-
authStorage: this.authStorage,
|
|
493
|
-
modelRegistry: this.modelRegistry,
|
|
494
|
-
resourceLoader: this.resourceLoader,
|
|
495
|
-
settingsManager: this.settingsManager,
|
|
496
|
-
sessionManager: SessionManager.continueRecent(this.config.cwd, sessionDir),
|
|
497
|
-
thinkingLevel: this.config.thinkingLevel
|
|
498
|
-
});
|
|
499
|
-
logger5.debug({
|
|
500
|
-
sessionDir,
|
|
501
|
-
sessionId: session.sessionId,
|
|
502
|
-
sessionFile: session.sessionFile
|
|
503
|
-
}, "scoped session created");
|
|
504
|
-
await this.models.ensureSessionHasConfiguredModel(session);
|
|
505
|
-
return session;
|
|
506
|
-
}
|
|
507
|
-
async prompt(text) {
|
|
508
|
-
const session = this.requireSession();
|
|
509
|
-
const transformedPrompt = await this.config.promptTransform({
|
|
510
|
-
rawContent: text,
|
|
511
|
-
discordMetadata: "",
|
|
512
|
-
now: () => "",
|
|
513
|
-
userMessage: () => text
|
|
514
|
-
});
|
|
515
|
-
return runAgentTurn(session, transformedPrompt);
|
|
516
|
-
}
|
|
517
|
-
async compact() {
|
|
518
|
-
const session = this.requireSession();
|
|
519
|
-
await session.compact();
|
|
520
|
-
return `Compaction finished for session ${session.sessionId}.`;
|
|
521
|
-
}
|
|
522
|
-
async resetSession() {
|
|
523
|
-
const previousSession = this.requireSession();
|
|
524
|
-
await previousSession.abort();
|
|
525
|
-
previousSession.dispose();
|
|
526
|
-
this.session = null;
|
|
527
|
-
const { session } = await createAgentSession({
|
|
528
|
-
cwd: this.config.cwd,
|
|
529
|
-
agentDir: this.config.agentDir,
|
|
530
|
-
authStorage: this.authStorage,
|
|
531
|
-
modelRegistry: this.modelRegistry,
|
|
532
|
-
resourceLoader: this.resourceLoader,
|
|
533
|
-
settingsManager: this.settingsManager,
|
|
534
|
-
sessionManager: SessionManager.create(this.config.cwd, this.getSessionDir()),
|
|
535
|
-
thinkingLevel: this.config.thinkingLevel
|
|
536
|
-
});
|
|
537
|
-
this.session = session;
|
|
538
|
-
await this.ensureConfiguredModel();
|
|
539
|
-
return `Started a fresh session. Old session kept at ${previousSession.sessionFile ?? "(unknown path)"}.`;
|
|
540
|
-
}
|
|
541
|
-
getStatus() {
|
|
542
|
-
const session = this.requireSession();
|
|
543
|
-
const model = this.models.getCurrentModelDisplay(session);
|
|
544
|
-
const contextUsage = session.getContextUsage();
|
|
545
|
-
const thinkingInfo = session.supportsThinking() ? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})` : "thinking: not supported";
|
|
546
31
|
return {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
thinkingInfo
|
|
32
|
+
client,
|
|
33
|
+
stop,
|
|
34
|
+
getStatus: () => {
|
|
35
|
+
return agentService.getStatus();
|
|
36
|
+
},
|
|
553
37
|
};
|
|
554
|
-
}
|
|
555
|
-
async shutdown() {
|
|
556
|
-
const session = this.session;
|
|
557
|
-
if (session) {
|
|
558
|
-
await session.abort();
|
|
559
|
-
session.dispose();
|
|
560
|
-
}
|
|
561
|
-
await this.settingsManager.flush();
|
|
562
|
-
}
|
|
563
|
-
async createOrResumeSession() {
|
|
564
|
-
const { session } = await createAgentSession({
|
|
565
|
-
cwd: this.config.cwd,
|
|
566
|
-
agentDir: this.config.agentDir,
|
|
567
|
-
authStorage: this.authStorage,
|
|
568
|
-
modelRegistry: this.modelRegistry,
|
|
569
|
-
resourceLoader: this.resourceLoader,
|
|
570
|
-
settingsManager: this.settingsManager,
|
|
571
|
-
sessionManager: SessionManager.continueRecent(this.config.cwd, this.getSessionDir()),
|
|
572
|
-
thinkingLevel: this.config.thinkingLevel
|
|
573
|
-
});
|
|
574
|
-
this.session = session;
|
|
575
|
-
logger5.info({
|
|
576
|
-
sessionId: session.sessionId,
|
|
577
|
-
sessionFile: session.sessionFile,
|
|
578
|
-
restoredModel: session.model ? `${session.model.provider}/${session.model.id}` : null
|
|
579
|
-
}, "session ready");
|
|
580
|
-
}
|
|
581
|
-
async ensureConfiguredModel() {
|
|
582
|
-
await this.models.ensureSessionHasConfiguredModel(this.requireSession());
|
|
583
|
-
}
|
|
584
|
-
requireSession() {
|
|
585
|
-
if (!this.session) {
|
|
586
|
-
throw new Error("Agent session has not been initialized.");
|
|
587
|
-
}
|
|
588
|
-
return this.session;
|
|
589
|
-
}
|
|
590
|
-
getSessionDir() {
|
|
591
|
-
return path.join(this.config.agentDir, "sessions");
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// src/config.ts
|
|
596
|
-
import path2 from "node:path";
|
|
597
|
-
import dotenv from "dotenv";
|
|
598
|
-
function resolveConfig(config) {
|
|
599
|
-
const discordAllowedUserId = requireNonEmptyConfigValue("discordAllowedUserId", config.discordAllowedUserId);
|
|
600
|
-
const cwd = requireNonEmptyConfigValue("cwd", config.cwd);
|
|
601
|
-
return {
|
|
602
|
-
discordBotToken: requireNonEmptyConfigValue("discordBotToken", config.discordBotToken),
|
|
603
|
-
discordAllowedUserId,
|
|
604
|
-
cwd,
|
|
605
|
-
agentDir: config.agentDir?.trim() || path2.join(cwd, ".pi-agent"),
|
|
606
|
-
modelProvider: config.modelProvider?.trim() || "openrouter",
|
|
607
|
-
modelId: config.modelId?.trim() || "anthropic/claude-3.5-haiku",
|
|
608
|
-
thinkingLevel: parseThinkingLevel(config.thinkingLevel) || "medium",
|
|
609
|
-
promptTimeZone: config.promptTimeZone?.trim() || "UTC",
|
|
610
|
-
promptLocale: config.promptLocale?.trim() || "en-AU",
|
|
611
|
-
promptTransform: config.promptTransform || defaultPromptTransform,
|
|
612
|
-
startupMessage: config.startupMessage === undefined ? "Bot is online and ready." : config.startupMessage,
|
|
613
|
-
shutdownOnSignals: config.shutdownOnSignals ?? true,
|
|
614
|
-
visionModelId: config.visionModelId?.trim() || null,
|
|
615
|
-
discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
|
|
616
|
-
discordAllowedUserIds: config.discordAllowedUserIds ?? [
|
|
617
|
-
discordAllowedUserId
|
|
618
|
-
]
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
function loadDiscordGatewayConfigFromEnv(overrides = {}) {
|
|
622
|
-
dotenv.config();
|
|
623
|
-
return resolveConfig({
|
|
624
|
-
discordBotToken: overrides.discordBotToken || process.env.DISCORD_BOT_TOKEN || "",
|
|
625
|
-
discordAllowedUserId: overrides.discordAllowedUserId || process.env.DISCORD_ALLOWED_USER_ID || "",
|
|
626
|
-
cwd: overrides.cwd || process.env.PI_AGENT_CWD || process.cwd(),
|
|
627
|
-
agentDir: overrides.agentDir || process.env.PI_AGENT_DIR,
|
|
628
|
-
modelProvider: overrides.modelProvider || process.env.PI_MODEL_PROVIDER,
|
|
629
|
-
modelId: overrides.modelId || process.env.PI_MODEL_ID,
|
|
630
|
-
thinkingLevel: parseThinkingLevel(overrides.thinkingLevel || process.env.PI_THINKING_LEVEL),
|
|
631
|
-
promptTimeZone: overrides.promptTimeZone || process.env.PI_PROMPT_TIME_ZONE,
|
|
632
|
-
promptLocale: overrides.promptLocale || process.env.PI_PROMPT_LOCALE,
|
|
633
|
-
promptTransform: overrides.promptTransform,
|
|
634
|
-
startupMessage: overrides.startupMessage ?? readStartupMessageFromEnv(),
|
|
635
|
-
shutdownOnSignals: overrides.shutdownOnSignals,
|
|
636
|
-
visionModelId: overrides.visionModelId ?? process.env.PI_VISION_MODEL_ID,
|
|
637
|
-
discordAllowedForumChannelIds: overrides.discordAllowedForumChannelIds ?? parseStringArrayFromEnv("DISCORD_FORUM_CHANNEL_IDS") ?? [],
|
|
638
|
-
discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS")
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
function requireNonEmptyConfigValue(name, value) {
|
|
642
|
-
const trimmedValue = value.trim();
|
|
643
|
-
if (!trimmedValue) {
|
|
644
|
-
throw new Error(`Missing required config value: ${name}`);
|
|
645
|
-
}
|
|
646
|
-
return trimmedValue;
|
|
647
|
-
}
|
|
648
|
-
function readStartupMessageFromEnv() {
|
|
649
|
-
const value = process.env.DISCORD_STARTUP_MESSAGE;
|
|
650
|
-
if (value === undefined) {
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
const trimmedValue = value.trim();
|
|
654
|
-
if (!trimmedValue || trimmedValue.toLowerCase() === "false") {
|
|
655
|
-
return false;
|
|
656
|
-
}
|
|
657
|
-
return trimmedValue;
|
|
658
|
-
}
|
|
659
|
-
function parseThinkingLevel(value) {
|
|
660
|
-
if (!value) {
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
const trimmed = value.trim().toLowerCase();
|
|
664
|
-
const validLevels = [
|
|
665
|
-
"off",
|
|
666
|
-
"minimal",
|
|
667
|
-
"low",
|
|
668
|
-
"medium",
|
|
669
|
-
"high",
|
|
670
|
-
"xhigh"
|
|
671
|
-
];
|
|
672
|
-
if (validLevels.includes(trimmed)) {
|
|
673
|
-
return trimmed;
|
|
674
|
-
}
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
function defaultPromptTransform(ctx) {
|
|
678
|
-
return [ctx.now(), ctx.discordMetadata, "", ctx.userMessage()].filter(Boolean).join(`
|
|
679
|
-
`);
|
|
680
38
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// src/discord-gateway-client.ts
|
|
690
|
-
import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
691
|
-
|
|
692
|
-
// src/session-registry.ts
|
|
693
|
-
import path3 from "node:path";
|
|
694
|
-
|
|
695
|
-
// src/prompt-queue.ts
|
|
696
|
-
class PromptQueue {
|
|
697
|
-
queue = [];
|
|
698
|
-
running = false;
|
|
699
|
-
enqueue(task) {
|
|
700
|
-
return new Promise((resolve, reject) => {
|
|
701
|
-
this.queue.push(async () => {
|
|
702
|
-
try {
|
|
703
|
-
resolve(await task());
|
|
704
|
-
} catch (error) {
|
|
705
|
-
reject(error);
|
|
39
|
+
function createGatewayStopHandler(client, agentService, sessionRegistry, config) {
|
|
40
|
+
let stopped = false;
|
|
41
|
+
return async () => {
|
|
42
|
+
if (stopped) {
|
|
43
|
+
return;
|
|
706
44
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
runNextTask() {
|
|
718
|
-
if (this.running) {
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
const next = this.queue.shift();
|
|
722
|
-
if (!next) {
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
this.running = true;
|
|
726
|
-
next().finally(() => {
|
|
727
|
-
this.running = false;
|
|
728
|
-
this.runNextTask();
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// src/message-chunker.ts
|
|
734
|
-
import { marked } from "marked";
|
|
735
|
-
var DISCORD_MESSAGE_LIMIT = 2000;
|
|
736
|
-
var SAFE_MESSAGE_LIMIT = 1900;
|
|
737
|
-
function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
738
|
-
if (text.length <= maxChunkSize) {
|
|
739
|
-
return [text];
|
|
740
|
-
}
|
|
741
|
-
const tokens = marked.lexer(text);
|
|
742
|
-
const chunks = [];
|
|
743
|
-
let currentTokens = [];
|
|
744
|
-
let currentSize = 0;
|
|
745
|
-
const flushChunk = () => {
|
|
746
|
-
if (currentTokens.length > 0) {
|
|
747
|
-
chunks.push(currentTokens.map((t) => t.raw).join("").trim());
|
|
748
|
-
currentTokens = [];
|
|
749
|
-
currentSize = 0;
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
for (const token of tokens) {
|
|
753
|
-
const size = token.raw.length;
|
|
754
|
-
if (currentSize + size > maxChunkSize && currentTokens.length > 0) {
|
|
755
|
-
flushChunk();
|
|
756
|
-
}
|
|
757
|
-
currentTokens.push(token);
|
|
758
|
-
currentSize += size;
|
|
759
|
-
}
|
|
760
|
-
flushChunk();
|
|
761
|
-
return chunks.map((chunk) => chunk.slice(0, DISCORD_MESSAGE_LIMIT));
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// src/discord-replies.ts
|
|
765
|
-
var logger6 = createModuleLogger("discord-replies");
|
|
766
|
-
var DISCORD_MESSAGE_LIMIT2 = 2000;
|
|
767
|
-
var FENCE_OVERHEAD = 8;
|
|
768
|
-
var MAX_CODE_FENCE_CONTENT = DISCORD_MESSAGE_LIMIT2 - FENCE_OVERHEAD;
|
|
769
|
-
function chunkByLines(text, maxSize) {
|
|
770
|
-
const lines = text.split(`
|
|
771
|
-
`);
|
|
772
|
-
const chunks = [];
|
|
773
|
-
let current = "";
|
|
774
|
-
for (const line of lines) {
|
|
775
|
-
const candidate = current ? current + `
|
|
776
|
-
` + line : line;
|
|
777
|
-
if (candidate.length > maxSize) {
|
|
778
|
-
if (current) {
|
|
779
|
-
chunks.push(current);
|
|
780
|
-
current = line;
|
|
781
|
-
} else {
|
|
782
|
-
chunks.push(line.slice(0, maxSize));
|
|
783
|
-
current = line.slice(maxSize);
|
|
784
|
-
}
|
|
785
|
-
} else {
|
|
786
|
-
current = candidate;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
if (current) {
|
|
790
|
-
chunks.push(current);
|
|
791
|
-
}
|
|
792
|
-
return chunks;
|
|
793
|
-
}
|
|
794
|
-
var DEFAULT_WORKING_EMOJI = "⚙️";
|
|
795
|
-
async function addWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
796
|
-
try {
|
|
797
|
-
await message.react(emoji);
|
|
798
|
-
} catch (error) {
|
|
799
|
-
logger6.debug({ messageId: message.id, error }, "failed to add working reaction");
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
async function removeWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
803
|
-
try {
|
|
804
|
-
const reaction = message.reactions.cache.get(emoji);
|
|
805
|
-
if (reaction) {
|
|
806
|
-
await reaction.users.remove(message.client.user);
|
|
807
|
-
}
|
|
808
|
-
} catch (error) {
|
|
809
|
-
logger6.debug({ messageId: message.id, error }, "failed to remove working reaction");
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async function sendReply(message, text) {
|
|
813
|
-
const channel = message.channel;
|
|
814
|
-
if (!channel.isSendable()) {
|
|
815
|
-
logger6.debug({
|
|
816
|
-
messageId: message.id
|
|
817
|
-
}, "reply skipped, channel not sendable");
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const chunks = chunkMessage(text);
|
|
821
|
-
const [firstChunk, ...remainingChunks] = chunks;
|
|
822
|
-
if (!firstChunk) {
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
try {
|
|
826
|
-
await message.reply(firstChunk);
|
|
827
|
-
for (const chunk of remainingChunks) {
|
|
828
|
-
await channel.send(chunk);
|
|
829
|
-
}
|
|
830
|
-
} catch (error) {
|
|
831
|
-
logger6.error({
|
|
832
|
-
messageId: message.id,
|
|
833
|
-
error
|
|
834
|
-
}, "send reply failed");
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
async function sendCommandReply(message, text) {
|
|
838
|
-
const channel = message.channel;
|
|
839
|
-
if (!channel.isSendable()) {
|
|
840
|
-
logger6.debug({
|
|
841
|
-
messageId: message.id
|
|
842
|
-
}, "command reply skipped, channel not sendable");
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
const chunks = chunkByLines(text, MAX_CODE_FENCE_CONTENT).map((c) => `\`\`\`
|
|
846
|
-
${c}
|
|
847
|
-
\`\`\``);
|
|
848
|
-
const [firstChunk, ...remainingChunks] = chunks;
|
|
849
|
-
if (!firstChunk) {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
try {
|
|
853
|
-
await message.reply(firstChunk);
|
|
854
|
-
for (const chunk of remainingChunks) {
|
|
855
|
-
await channel.send(chunk);
|
|
856
|
-
}
|
|
857
|
-
} catch (error) {
|
|
858
|
-
logger6.error({
|
|
859
|
-
messageId: message.id,
|
|
860
|
-
error
|
|
861
|
-
}, "send command reply failed");
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// src/session-registry.ts
|
|
866
|
-
function sessionDirForScope(agentDir, scope) {
|
|
867
|
-
if (scope === "dm") {
|
|
868
|
-
return path3.join(agentDir, "sessions");
|
|
869
|
-
}
|
|
870
|
-
if (scope.startsWith("thread:")) {
|
|
871
|
-
const threadId = scope.slice(7);
|
|
872
|
-
return path3.join(agentDir, "sessions", `thread-${threadId}`);
|
|
873
|
-
}
|
|
874
|
-
throw new Error(`Unknown session scope: ${scope}`);
|
|
875
|
-
}
|
|
876
|
-
var logger7 = createModuleLogger("session-registry");
|
|
877
|
-
|
|
878
|
-
class SessionRegistry {
|
|
879
|
-
scopes = new Map;
|
|
880
|
-
agentService;
|
|
881
|
-
constructor(agentService) {
|
|
882
|
-
this.agentService = agentService;
|
|
883
|
-
}
|
|
884
|
-
async getOrCreate(scope) {
|
|
885
|
-
const existing = this.scopes.get(scope);
|
|
886
|
-
if (existing) {
|
|
887
|
-
return { entry: existing, created: false };
|
|
888
|
-
}
|
|
889
|
-
const sessionDir = sessionDirForScope(this.agentService.getAgentDir(), scope);
|
|
890
|
-
const session = await this.agentService.createSession(sessionDir);
|
|
891
|
-
const promptQueue = new PromptQueue;
|
|
892
|
-
const entry = {
|
|
893
|
-
session,
|
|
894
|
-
promptQueue,
|
|
895
|
-
createdAt: new Date,
|
|
896
|
-
workingEmoji: DEFAULT_WORKING_EMOJI
|
|
897
|
-
};
|
|
898
|
-
this.scopes.set(scope, entry);
|
|
899
|
-
logger7.debug({
|
|
900
|
-
scope,
|
|
901
|
-
sessionDir,
|
|
902
|
-
sessionId: session.sessionId
|
|
903
|
-
}, "scope registered");
|
|
904
|
-
return { entry, created: true };
|
|
905
|
-
}
|
|
906
|
-
async remove(scope) {
|
|
907
|
-
const entry = this.scopes.get(scope);
|
|
908
|
-
if (!entry) {
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
logger7.debug({ scope }, "removing scope");
|
|
912
|
-
await entry.session.abort();
|
|
913
|
-
entry.session.dispose();
|
|
914
|
-
this.scopes.delete(scope);
|
|
915
|
-
}
|
|
916
|
-
get(scope) {
|
|
917
|
-
return this.scopes.get(scope);
|
|
918
|
-
}
|
|
919
|
-
getScopes() {
|
|
920
|
-
return Array.from(this.scopes.keys());
|
|
921
|
-
}
|
|
922
|
-
async shutdownAll() {
|
|
923
|
-
logger7.info({ count: this.scopes.size }, "shutting down all scopes");
|
|
924
|
-
const scopes = Array.from(this.scopes.keys());
|
|
925
|
-
for (const scope of scopes) {
|
|
926
|
-
await this.remove(scope);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// src/session-commands.ts
|
|
932
|
-
function getSessionStatusText(session, promptQueue, extras) {
|
|
933
|
-
const model = session.model ? `${session.model.provider}/${session.model.id}` : "(no model selected)";
|
|
934
|
-
const contextUsage = session.getContextUsage();
|
|
935
|
-
const contextLine = contextUsage ? contextUsage.tokens === null || contextUsage.percent === null ? `context: ?/${contextUsage.contextWindow}` : `context: ${contextUsage.tokens}/${contextUsage.contextWindow} (${Math.round(contextUsage.percent)}%)` : "context: (unavailable)";
|
|
936
|
-
const thinkingInfo = session.supportsThinking() ? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})` : "thinking: not supported";
|
|
937
|
-
const queueStatus = promptQueue.getSnapshot();
|
|
938
|
-
const lines = [
|
|
939
|
-
`model: ${model}`,
|
|
940
|
-
`session-id: ${session.sessionId}`,
|
|
941
|
-
`session-file: ${session.sessionFile ?? "(none)"}`,
|
|
942
|
-
`streaming: ${session.isStreaming}`,
|
|
943
|
-
thinkingInfo,
|
|
944
|
-
contextLine,
|
|
945
|
-
`queue-pending: ${queueStatus.pending}`,
|
|
946
|
-
`queue-busy: ${queueStatus.busy}`
|
|
947
|
-
];
|
|
948
|
-
if (extras?.tools && extras.tools.length > 0) {
|
|
949
|
-
const toolNames = extras.tools.map((tool) => tool.name);
|
|
950
|
-
lines.push("", `Tools (${extras.tools.length}): ${toolNames.join(", ")}`);
|
|
951
|
-
}
|
|
952
|
-
if (extras?.skillsSummary) {
|
|
953
|
-
lines.push("", extras.skillsSummary);
|
|
954
|
-
}
|
|
955
|
-
if (extras?.extensionsSummary) {
|
|
956
|
-
lines.push("", extras.extensionsSummary);
|
|
957
|
-
}
|
|
958
|
-
return lines.join(`
|
|
959
|
-
`);
|
|
960
|
-
}
|
|
961
|
-
function getEffectiveSession(context) {
|
|
962
|
-
return context.session ?? context.agentService.getSession();
|
|
963
|
-
}
|
|
964
|
-
function requireEffectiveSession(context) {
|
|
965
|
-
const session = getEffectiveSession(context);
|
|
966
|
-
if (!session) {
|
|
967
|
-
return {
|
|
968
|
-
handled: true,
|
|
969
|
-
response: "No active session."
|
|
45
|
+
stopped = true;
|
|
46
|
+
logger.info({
|
|
47
|
+
cwd: config.cwd,
|
|
48
|
+
agentDir: config.agentDir,
|
|
49
|
+
}, "stopping discord gateway");
|
|
50
|
+
client.destroy();
|
|
51
|
+
await sessionRegistry.shutdownAll();
|
|
52
|
+
await agentService.shutdown();
|
|
970
53
|
};
|
|
971
|
-
}
|
|
972
|
-
return { session };
|
|
973
54
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
handled: true,
|
|
982
|
-
response: [
|
|
983
|
-
"Commands:",
|
|
984
|
-
"!help - show this message",
|
|
985
|
-
"!status - show current session status",
|
|
986
|
-
"!thinking - show or set thinking/reasoning level",
|
|
987
|
-
"!model - list available models or switch to one",
|
|
988
|
-
"!compact - compact the persistent session",
|
|
989
|
-
"!reset-session - start a fresh persistent session",
|
|
990
|
-
"!reload - reload resources (AGENTS.md, extensions, skills, etc.)",
|
|
991
|
-
"!reaction - show or set the working reaction emoji",
|
|
992
|
-
extraCommands,
|
|
993
|
-
"Any other text goes to the agent session."
|
|
994
|
-
].filter(Boolean).join(`
|
|
995
|
-
`)
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
async function handleArchiveCommand(trimmedInput, context) {
|
|
999
|
-
if (trimmedInput !== "!archive") {
|
|
1000
|
-
return null;
|
|
1001
|
-
}
|
|
1002
|
-
if (!context.session) {
|
|
1003
|
-
return {
|
|
1004
|
-
handled: true,
|
|
1005
|
-
response: "!archive is only available in forum threads."
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
return {
|
|
1009
|
-
handled: true,
|
|
1010
|
-
archive: true,
|
|
1011
|
-
response: "Archiving thread and shutting down session."
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
async function handleStatusCommand(trimmedInput, context) {
|
|
1015
|
-
if (trimmedInput !== "!status") {
|
|
1016
|
-
return null;
|
|
1017
|
-
}
|
|
1018
|
-
const effectiveSession = requireEffectiveSession(context);
|
|
1019
|
-
if ("handled" in effectiveSession) {
|
|
1020
|
-
return effectiveSession;
|
|
1021
|
-
}
|
|
1022
|
-
const tools = effectiveSession.session.getAllTools();
|
|
1023
|
-
const extensionsSummary = context.agentService.resources.getExtensionsSummary();
|
|
1024
|
-
const skillsSummary = context.agentService.resources.getSkillsSummary();
|
|
1025
|
-
return {
|
|
1026
|
-
handled: true,
|
|
1027
|
-
response: getSessionStatusText(effectiveSession.session, context.promptQueue, {
|
|
1028
|
-
tools,
|
|
1029
|
-
extensionsSummary,
|
|
1030
|
-
skillsSummary
|
|
1031
|
-
})
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
async function handleThinkingCommand(trimmedInput, context) {
|
|
1035
|
-
if (trimmedInput !== "!thinking" && !trimmedInput.startsWith("!thinking ")) {
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
const effectiveSession = requireEffectiveSession(context);
|
|
1039
|
-
if ("handled" in effectiveSession) {
|
|
1040
|
-
return effectiveSession;
|
|
1041
|
-
}
|
|
1042
|
-
const parts = trimmedInput.split(" ");
|
|
1043
|
-
if (parts.length === 1) {
|
|
1044
|
-
const info = context.agentService.models.getThinkingLevel(effectiveSession.session);
|
|
1045
|
-
if (!info.supported) {
|
|
1046
|
-
return {
|
|
1047
|
-
handled: true,
|
|
1048
|
-
response: "Current model does not support reasoning/thinking."
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
return {
|
|
1052
|
-
handled: true,
|
|
1053
|
-
response: [
|
|
1054
|
-
`Current: ${info.current}`,
|
|
1055
|
-
`Available: ${info.available.join(", ")}`,
|
|
1056
|
-
`Usage: !thinking <level>`
|
|
1057
|
-
].join(`
|
|
1058
|
-
`)
|
|
1059
|
-
};
|
|
1060
|
-
}
|
|
1061
|
-
const requestedLevel = parts[1];
|
|
1062
|
-
return {
|
|
1063
|
-
handled: true,
|
|
1064
|
-
response: context.agentService.models.setThinkingLevel(effectiveSession.session, requestedLevel)
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
async function handleModelCommand(trimmedInput, context) {
|
|
1068
|
-
if (trimmedInput !== "!model" && !trimmedInput.startsWith("!model ")) {
|
|
1069
|
-
return null;
|
|
1070
|
-
}
|
|
1071
|
-
const effectiveSession = requireEffectiveSession(context);
|
|
1072
|
-
if ("handled" in effectiveSession) {
|
|
1073
|
-
return effectiveSession;
|
|
1074
|
-
}
|
|
1075
|
-
const parts = trimmedInput.split(" ");
|
|
1076
|
-
if (parts.length === 1) {
|
|
1077
|
-
const current = context.agentService.models.getCurrentModelDisplay(effectiveSession.session);
|
|
1078
|
-
const modelList = await context.agentService.models.listModels(effectiveSession.session);
|
|
1079
|
-
return {
|
|
1080
|
-
handled: true,
|
|
1081
|
-
response: `Current model: ${current}
|
|
1082
|
-
|
|
1083
|
-
${modelList}`
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
const argument = parts.slice(1).join(" ");
|
|
1087
|
-
const slashIndex = argument.indexOf("/");
|
|
1088
|
-
if (slashIndex === -1) {
|
|
1089
|
-
return {
|
|
1090
|
-
handled: true,
|
|
1091
|
-
response: `Usage: !model <provider/modelId>
|
|
1092
|
-
` + `Example: !model openrouter/anthropic/claude-sonnet-4
|
|
1093
|
-
` + "Use !model without args to see available models."
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
const provider = argument.substring(0, slashIndex).trim();
|
|
1097
|
-
const modelId = argument.substring(slashIndex + 1).trim();
|
|
1098
|
-
return {
|
|
1099
|
-
handled: true,
|
|
1100
|
-
response: await context.agentService.models.switchModel(provider, modelId, effectiveSession.session)
|
|
1101
|
-
};
|
|
1102
|
-
}
|
|
1103
|
-
async function handleCompactCommand(trimmedInput, context) {
|
|
1104
|
-
if (trimmedInput !== "!compact") {
|
|
1105
|
-
return null;
|
|
1106
|
-
}
|
|
1107
|
-
const effectiveSession = requireEffectiveSession(context);
|
|
1108
|
-
if ("handled" in effectiveSession) {
|
|
1109
|
-
return effectiveSession;
|
|
1110
|
-
}
|
|
1111
|
-
return {
|
|
1112
|
-
handled: true,
|
|
1113
|
-
response: await context.promptQueue.enqueue(async () => {
|
|
1114
|
-
await effectiveSession.session.compact();
|
|
1115
|
-
return `Compaction finished for session ${effectiveSession.session.sessionId}.`;
|
|
1116
|
-
})
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
async function handleReloadCommand(trimmedInput, context) {
|
|
1120
|
-
if (trimmedInput !== "!reload") {
|
|
1121
|
-
return null;
|
|
1122
|
-
}
|
|
1123
|
-
return {
|
|
1124
|
-
handled: true,
|
|
1125
|
-
response: await context.promptQueue.enqueue(async () => {
|
|
1126
|
-
return context.agentService.resources.reloadResources();
|
|
1127
|
-
})
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
async function handleResetSessionCommand(trimmedInput, context) {
|
|
1131
|
-
if (trimmedInput !== "!reset-session") {
|
|
1132
|
-
return null;
|
|
1133
|
-
}
|
|
1134
|
-
const effectiveSession = requireEffectiveSession(context);
|
|
1135
|
-
if ("handled" in effectiveSession) {
|
|
1136
|
-
return effectiveSession;
|
|
1137
|
-
}
|
|
1138
|
-
let newSession;
|
|
1139
|
-
const response = await context.promptQueue.enqueue(async () => {
|
|
1140
|
-
const previousSessionFile = effectiveSession.session.sessionFile;
|
|
1141
|
-
await effectiveSession.session.abort();
|
|
1142
|
-
effectiveSession.session.dispose();
|
|
1143
|
-
const sessionDir = sessionDirForScope(context.agentService.getAgentDir(), context.scope);
|
|
1144
|
-
newSession = await context.agentService.createSession(sessionDir);
|
|
1145
|
-
return `Started a fresh session. Old session kept at ${previousSessionFile ?? "(unknown path)"}.`;
|
|
1146
|
-
});
|
|
1147
|
-
return {
|
|
1148
|
-
handled: true,
|
|
1149
|
-
response,
|
|
1150
|
-
newSession
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
async function handleReactionCommand(trimmedInput, _context) {
|
|
1154
|
-
if (trimmedInput !== "!reaction" && !trimmedInput.startsWith("!reaction ")) {
|
|
1155
|
-
return null;
|
|
1156
|
-
}
|
|
1157
|
-
const parts = trimmedInput.split(" ");
|
|
1158
|
-
if (parts.length === 1) {
|
|
1159
|
-
return {
|
|
1160
|
-
handled: true,
|
|
1161
|
-
response: `Current working reaction: ${_context.workingEmoji}
|
|
1162
|
-
` + `Usage: !reaction <emoji> to change it.
|
|
1163
|
-
` + `Examples: !reaction \uD83D\uDD04 or !reaction ⏳`
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
const emoji = parts.slice(1).join(" ").trim();
|
|
1167
|
-
if (!emoji) {
|
|
1168
|
-
return {
|
|
1169
|
-
handled: true,
|
|
1170
|
-
response: "Please provide an emoji. Example: !reaction \uD83D\uDD04"
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
return {
|
|
1174
|
-
handled: true,
|
|
1175
|
-
workingEmoji: emoji,
|
|
1176
|
-
response: `Working reaction emoji set to ${emoji}`
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
var commandHandlers = [
|
|
1180
|
-
handleHelpCommand,
|
|
1181
|
-
handleArchiveCommand,
|
|
1182
|
-
handleStatusCommand,
|
|
1183
|
-
handleThinkingCommand,
|
|
1184
|
-
handleModelCommand,
|
|
1185
|
-
handleCompactCommand,
|
|
1186
|
-
handleReloadCommand,
|
|
1187
|
-
handleResetSessionCommand,
|
|
1188
|
-
handleReactionCommand
|
|
1189
|
-
];
|
|
1190
|
-
async function executeSessionCommand(input, context) {
|
|
1191
|
-
const trimmedInput = input.trim();
|
|
1192
|
-
if (!trimmedInput.startsWith("!")) {
|
|
1193
|
-
return { handled: false };
|
|
1194
|
-
}
|
|
1195
|
-
for (const handler of commandHandlers) {
|
|
1196
|
-
const result = await handler(trimmedInput, context);
|
|
1197
|
-
if (result) {
|
|
1198
|
-
return result;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
return {
|
|
1202
|
-
handled: true,
|
|
1203
|
-
response: `Unknown command: ${trimmedInput}. Try !help.`
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// src/discord-attachments.ts
|
|
1208
|
-
var logger8 = createModuleLogger("discord-attachments");
|
|
1209
|
-
var TEXT_ATTACHMENT_EXTENSIONS = [
|
|
1210
|
-
".txt",
|
|
1211
|
-
".md",
|
|
1212
|
-
".json",
|
|
1213
|
-
".csv",
|
|
1214
|
-
".log",
|
|
1215
|
-
".yml",
|
|
1216
|
-
".yaml",
|
|
1217
|
-
".xml",
|
|
1218
|
-
".toml",
|
|
1219
|
-
".ini",
|
|
1220
|
-
".cfg"
|
|
1221
|
-
];
|
|
1222
|
-
var MAX_TEXT_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
1223
|
-
var MEDIA_ATTACHMENT_EXTENSIONS = [
|
|
1224
|
-
".png",
|
|
1225
|
-
".jpg",
|
|
1226
|
-
".jpeg",
|
|
1227
|
-
".gif",
|
|
1228
|
-
".webp",
|
|
1229
|
-
".pdf",
|
|
1230
|
-
".docx",
|
|
1231
|
-
".doc",
|
|
1232
|
-
".pptx",
|
|
1233
|
-
".ppt",
|
|
1234
|
-
".xlsx",
|
|
1235
|
-
".xls"
|
|
1236
|
-
];
|
|
1237
|
-
var MAX_MEDIA_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
1238
|
-
var OFFICE_MIME_TYPES = new Set([
|
|
1239
|
-
"application/pdf",
|
|
1240
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1241
|
-
"application/msword",
|
|
1242
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1243
|
-
"application/vnd.ms-powerpoint",
|
|
1244
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1245
|
-
"application/vnd.ms-excel"
|
|
1246
|
-
]);
|
|
1247
|
-
function isSupportedTextAttachment(attachment) {
|
|
1248
|
-
const ext = attachment.name?.slice(attachment.name.lastIndexOf(".")).toLowerCase();
|
|
1249
|
-
return Boolean(ext && TEXT_ATTACHMENT_EXTENSIONS.includes(ext));
|
|
1250
|
-
}
|
|
1251
|
-
function isSupportedMediaAttachment(attachment) {
|
|
1252
|
-
const ext = attachment.name?.slice(attachment.name.lastIndexOf(".")).toLowerCase();
|
|
1253
|
-
if (!ext || !MEDIA_ATTACHMENT_EXTENSIONS.includes(ext)) {
|
|
1254
|
-
return false;
|
|
1255
|
-
}
|
|
1256
|
-
const contentType = attachment.contentType;
|
|
1257
|
-
if (!contentType) {
|
|
1258
|
-
return false;
|
|
1259
|
-
}
|
|
1260
|
-
return contentType.startsWith("image/") || OFFICE_MIME_TYPES.has(contentType);
|
|
1261
|
-
}
|
|
1262
|
-
async function readTextAttachments(message) {
|
|
1263
|
-
const attachments = message.attachments;
|
|
1264
|
-
if (attachments.size === 0) {
|
|
1265
|
-
return [];
|
|
1266
|
-
}
|
|
1267
|
-
const results = [];
|
|
1268
|
-
for (const [, attachment] of attachments) {
|
|
1269
|
-
if (!isSupportedTextAttachment(attachment)) {
|
|
1270
|
-
logger8.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
|
|
1271
|
-
continue;
|
|
1272
|
-
}
|
|
1273
|
-
if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
|
|
1274
|
-
logger8.warn({
|
|
1275
|
-
messageId: message.id,
|
|
1276
|
-
filename: attachment.name,
|
|
1277
|
-
size: attachment.size
|
|
1278
|
-
}, "attachment too large, skipping");
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
try {
|
|
1282
|
-
logger8.info({
|
|
1283
|
-
messageId: message.id,
|
|
1284
|
-
filename: attachment.name,
|
|
1285
|
-
size: attachment.size
|
|
1286
|
-
}, "fetching attachment");
|
|
1287
|
-
const response = await fetch(attachment.url);
|
|
1288
|
-
if (!response.ok) {
|
|
1289
|
-
logger8.warn({
|
|
1290
|
-
messageId: message.id,
|
|
1291
|
-
filename: attachment.name,
|
|
1292
|
-
status: response.status
|
|
1293
|
-
}, "failed to fetch attachment");
|
|
1294
|
-
continue;
|
|
1295
|
-
}
|
|
1296
|
-
const content = await response.text();
|
|
1297
|
-
results.push({ filename: attachment.name, content });
|
|
1298
|
-
} catch (error) {
|
|
1299
|
-
logger8.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
return results;
|
|
1303
|
-
}
|
|
1304
|
-
async function readMediaAttachments(message) {
|
|
1305
|
-
const attachments = message.attachments;
|
|
1306
|
-
if (attachments.size === 0) {
|
|
1307
|
-
return [];
|
|
1308
|
-
}
|
|
1309
|
-
const results = [];
|
|
1310
|
-
for (const [, attachment] of attachments) {
|
|
1311
|
-
if (!isSupportedMediaAttachment(attachment)) {
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
if (attachment.size > MAX_MEDIA_ATTACHMENT_SIZE_BYTES) {
|
|
1315
|
-
logger8.warn({
|
|
1316
|
-
messageId: message.id,
|
|
1317
|
-
filename: attachment.name,
|
|
1318
|
-
size: attachment.size
|
|
1319
|
-
}, "media attachment too large, skipping");
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
try {
|
|
1323
|
-
logger8.info({
|
|
1324
|
-
messageId: message.id,
|
|
1325
|
-
filename: attachment.name,
|
|
1326
|
-
size: attachment.size
|
|
1327
|
-
}, "fetching media attachment");
|
|
1328
|
-
const response = await fetch(attachment.url);
|
|
1329
|
-
if (!response.ok) {
|
|
1330
|
-
logger8.warn({
|
|
1331
|
-
messageId: message.id,
|
|
1332
|
-
filename: attachment.name,
|
|
1333
|
-
status: response.status
|
|
1334
|
-
}, "failed to fetch media attachment");
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
const buffer = await response.arrayBuffer();
|
|
1338
|
-
results.push({
|
|
1339
|
-
filename: attachment.name,
|
|
1340
|
-
data: Buffer.from(buffer).toString("base64"),
|
|
1341
|
-
mimeType: attachment.contentType ?? "application/octet-stream"
|
|
1342
|
-
});
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
logger8.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
return results;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// src/discord-auth.ts
|
|
1351
|
-
import { ChannelType } from "discord.js";
|
|
1352
|
-
function getAuthorDisplayName(message) {
|
|
1353
|
-
return message.member?.displayName || message.author.globalName || message.author.username;
|
|
1354
|
-
}
|
|
1355
|
-
function resolveMessageScope(message) {
|
|
1356
|
-
if (message.channel.type === ChannelType.DM) {
|
|
1357
|
-
return "dm";
|
|
1358
|
-
}
|
|
1359
|
-
if (message.channel.isThread()) {
|
|
1360
|
-
return `thread:${message.channel.id}`;
|
|
1361
|
-
}
|
|
1362
|
-
return null;
|
|
1363
|
-
}
|
|
1364
|
-
function isAuthorizedMessage(message, scope, accessConfig) {
|
|
1365
|
-
if (scope === "dm") {
|
|
1366
|
-
return message.author.id === accessConfig.discordAllowedUserId;
|
|
1367
|
-
}
|
|
1368
|
-
if (scope.startsWith("thread:")) {
|
|
1369
|
-
const channel = message.channel;
|
|
1370
|
-
if (!channel.isThread()) {
|
|
1371
|
-
return false;
|
|
1372
|
-
}
|
|
1373
|
-
const parentId = channel.parentId;
|
|
1374
|
-
if (!parentId || !accessConfig.discordAllowedForumChannelIds.includes(parentId)) {
|
|
1375
|
-
return false;
|
|
1376
|
-
}
|
|
1377
|
-
return accessConfig.discordAllowedUserIds.includes(message.author.id);
|
|
1378
|
-
}
|
|
1379
|
-
return false;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// src/media-description.ts
|
|
1383
|
-
var logger9 = createModuleLogger("media-description");
|
|
1384
|
-
async function describeMediaAttachment(agentService, imageData, mimeType, userText, visionModel) {
|
|
1385
|
-
const session = await agentService.createTemporarySession();
|
|
1386
|
-
await session.setModel(visionModel);
|
|
1387
|
-
const mediaType = getMediaType(mimeType);
|
|
1388
|
-
const imageContent = {
|
|
1389
|
-
type: "image",
|
|
1390
|
-
data: imageData,
|
|
1391
|
-
mimeType
|
|
1392
|
-
};
|
|
1393
|
-
let promptText;
|
|
1394
|
-
if (mediaType === "document") {
|
|
1395
|
-
promptText = userText.trim().length > 0 ? `The user sent a document with the following message: "${userText}". Please extract and summarize the text content of this document. Be thorough — include all important details, sections, and data from the document.` : "Please extract and summarize the text content of this document. Be thorough — include all important details, sections, data, and key points.";
|
|
1396
|
-
} else {
|
|
1397
|
-
promptText = userText.trim().length > 0 ? `The user sent this image with the following message: "${userText}". Please describe the image in detail and address any questions from the user's message.` : "Please describe this image in detail. What do you see?";
|
|
1398
|
-
}
|
|
1399
|
-
let text = "";
|
|
1400
|
-
try {
|
|
1401
|
-
await session.prompt(promptText, { images: [imageContent] });
|
|
1402
|
-
text = extractLastAssistantText(session);
|
|
1403
|
-
} catch (error) {
|
|
1404
|
-
logger9.error({ error, mimeType }, "vision model prompt failed");
|
|
1405
|
-
text = "(Vision model failed to process the file.)";
|
|
1406
|
-
} finally {
|
|
1407
|
-
session.dispose();
|
|
1408
|
-
}
|
|
1409
|
-
if (!text) {
|
|
1410
|
-
return "(Vision model returned no description.)";
|
|
1411
|
-
}
|
|
1412
|
-
logger9.debug({ textLength: text.length, mimeType }, "media described");
|
|
1413
|
-
return text;
|
|
1414
|
-
}
|
|
1415
|
-
function extractLastAssistantText(session) {
|
|
1416
|
-
const messages = session.messages;
|
|
1417
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
1418
|
-
const msg = messages[i];
|
|
1419
|
-
if (!msg || !isAssistantMessage(msg)) {
|
|
1420
|
-
continue;
|
|
1421
|
-
}
|
|
1422
|
-
const content = msg.content;
|
|
1423
|
-
if (!Array.isArray(content)) {
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
const textBlocks = [];
|
|
1427
|
-
for (const item of content) {
|
|
1428
|
-
if (typeof item === "object" && item !== null && "type" in item && item.type === "text") {
|
|
1429
|
-
textBlocks.push(item.text);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
return textBlocks.join(`
|
|
1433
|
-
`).trim();
|
|
1434
|
-
}
|
|
1435
|
-
return "";
|
|
1436
|
-
}
|
|
1437
|
-
function getMediaType(mimeType) {
|
|
1438
|
-
if (mimeType.startsWith("image/")) {
|
|
1439
|
-
return "image";
|
|
1440
|
-
}
|
|
1441
|
-
return "document";
|
|
1442
|
-
}
|
|
1443
|
-
function isAssistantMessage(msg) {
|
|
1444
|
-
return typeof msg === "object" && msg !== null && "role" in msg && msg.role === "assistant";
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// src/discord-media-resolution.ts
|
|
1448
|
-
var logger10 = createModuleLogger("discord-media-resolution");
|
|
1449
|
-
function parseProviderModelId(value) {
|
|
1450
|
-
const trimmed = value.trim();
|
|
1451
|
-
if (!trimmed) {
|
|
1452
|
-
return null;
|
|
1453
|
-
}
|
|
1454
|
-
const slashIndex = trimmed.indexOf("/");
|
|
1455
|
-
if (slashIndex === -1) {
|
|
1456
|
-
return null;
|
|
1457
|
-
}
|
|
1458
|
-
return {
|
|
1459
|
-
provider: trimmed.substring(0, slashIndex),
|
|
1460
|
-
modelId: trimmed.substring(slashIndex + 1)
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
function getMediaAttachmentLabel(filename, mimeType) {
|
|
1464
|
-
if (mimeType === "application/pdf") {
|
|
1465
|
-
return `[PDF: ${filename}]`;
|
|
1466
|
-
}
|
|
1467
|
-
if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || mimeType === "application/msword") {
|
|
1468
|
-
return `[Word: ${filename}]`;
|
|
1469
|
-
}
|
|
1470
|
-
if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || mimeType === "application/vnd.ms-excel") {
|
|
1471
|
-
return `[Excel: ${filename}]`;
|
|
1472
|
-
}
|
|
1473
|
-
if (mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation" || mimeType === "application/vnd.ms-powerpoint") {
|
|
1474
|
-
return `[PowerPoint: ${filename}]`;
|
|
1475
|
-
}
|
|
1476
|
-
return `[Image: ${filename}]`;
|
|
1477
|
-
}
|
|
1478
|
-
async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, currentModel, config, agentService) {
|
|
1479
|
-
const modelSupportsVision = currentModel?.input.includes("image") ?? false;
|
|
1480
|
-
if (modelSupportsVision) {
|
|
1481
|
-
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1482
|
-
logger10.info({
|
|
1483
|
-
count: mediaAttachments.length,
|
|
1484
|
-
filenames: names,
|
|
1485
|
-
model: currentModel ? `${currentModel.provider}/${currentModel.id}` : "none"
|
|
1486
|
-
}, "passing media natively to vision-capable model");
|
|
1487
|
-
return {
|
|
1488
|
-
content,
|
|
1489
|
-
images: mediaAttachments.map((media) => ({
|
|
1490
|
-
type: "image",
|
|
1491
|
-
data: media.data,
|
|
1492
|
-
mimeType: media.mimeType
|
|
1493
|
-
}))
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
if (!config.visionModelId) {
|
|
1497
|
-
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1498
|
-
logger10.info({ filenames: names }, "media attachments received but vision model not configured");
|
|
1499
|
-
const note = `
|
|
1500
|
-
|
|
1501
|
-
[User sent media attachment(s): ${names}]
|
|
1502
|
-
` + "(Media vision not configured. Set visionModelId to enable image/PDF/document understanding.)";
|
|
1503
|
-
return {
|
|
1504
|
-
content: content ? content + note : note,
|
|
1505
|
-
images: []
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
const parsedVisionModelId = parseProviderModelId(config.visionModelId);
|
|
1509
|
-
if (!parsedVisionModelId) {
|
|
1510
|
-
return { content, images: [] };
|
|
1511
|
-
}
|
|
1512
|
-
const visionModel = agentService.models.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
|
|
1513
|
-
if (!visionModel) {
|
|
1514
|
-
logger10.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
|
|
1515
|
-
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1516
|
-
const note = `
|
|
1517
|
-
|
|
1518
|
-
[User sent media attachment(s): ${names}]
|
|
1519
|
-
` + `(Vision model not found: ${config.visionModelId})`;
|
|
1520
|
-
return {
|
|
1521
|
-
content: content ? content + note : note,
|
|
1522
|
-
images: []
|
|
55
|
+
function registerSignalHandlers(stop) {
|
|
56
|
+
const handleSignal = (signal) => {
|
|
57
|
+
logger.info({ signal }, "received signal");
|
|
58
|
+
void stop().finally(() => {
|
|
59
|
+
logger.info("done");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
1523
62
|
};
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
count: mediaAttachments.length,
|
|
1527
|
-
visionModel: `${visionModel.provider}/${visionModel.id}`
|
|
1528
|
-
}, "describing media with vision model");
|
|
1529
|
-
const descriptions = [];
|
|
1530
|
-
for (const media of mediaAttachments) {
|
|
1531
|
-
const description = await describeMediaAttachment(agentService, media.data, media.mimeType, content, visionModel);
|
|
1532
|
-
const label = getMediaAttachmentLabel(media.filename, media.mimeType);
|
|
1533
|
-
descriptions.push(`${label}
|
|
1534
|
-
${description}`);
|
|
1535
|
-
}
|
|
1536
|
-
if (descriptions.length === 0) {
|
|
1537
|
-
return { content, images: [] };
|
|
1538
|
-
}
|
|
1539
|
-
const descriptionPrefix = descriptions.join(`
|
|
1540
|
-
|
|
1541
|
-
`);
|
|
1542
|
-
return {
|
|
1543
|
-
content: content ? `${descriptionPrefix}
|
|
1544
|
-
|
|
1545
|
-
---
|
|
1546
|
-
${content}` : descriptionPrefix,
|
|
1547
|
-
images: []
|
|
1548
|
-
};
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// src/discord-typing.ts
|
|
1552
|
-
var logger11 = createModuleLogger("discord-typing");
|
|
1553
|
-
var TYPING_INTERVAL_MS = 9000;
|
|
1554
|
-
var typingIntervals = new Map;
|
|
1555
|
-
async function sendTypingSafe(channel, channelKey) {
|
|
1556
|
-
try {
|
|
1557
|
-
const token = channel.client.token;
|
|
1558
|
-
const url = `https://discord.com/api/v10/channels/${channel.id}/typing`;
|
|
1559
|
-
const response = await fetch(url, {
|
|
1560
|
-
method: "POST",
|
|
1561
|
-
headers: { Authorization: `Bot ${token}` }
|
|
63
|
+
process.on("SIGINT", () => {
|
|
64
|
+
handleSignal("SIGINT");
|
|
1562
65
|
});
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1566
|
-
if (response.status === 429) {
|
|
1567
|
-
const body = await response.text();
|
|
1568
|
-
let retryMs = 3000;
|
|
1569
|
-
try {
|
|
1570
|
-
const parsed = JSON.parse(body);
|
|
1571
|
-
if (typeof parsed.retry_after === "number") {
|
|
1572
|
-
retryMs = parsed.retry_after * 1000 + 500;
|
|
1573
|
-
}
|
|
1574
|
-
} catch {}
|
|
1575
|
-
logger11.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
|
|
1576
|
-
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1577
|
-
await fetch(url, {
|
|
1578
|
-
method: "POST",
|
|
1579
|
-
headers: { Authorization: `Bot ${token}` }
|
|
1580
|
-
});
|
|
1581
|
-
logger11.warn({ channelKey }, "[TYPING] retry done");
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
logger11.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
|
|
1585
|
-
} catch (error) {
|
|
1586
|
-
logger11.error({ channelKey, error }, "[TYPING] FAILED");
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
function startTypingForChannel(channel, channelKey) {
|
|
1590
|
-
const existing = typingIntervals.get(channelKey);
|
|
1591
|
-
if (existing) {
|
|
1592
|
-
existing.refs += 1;
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
logger11.debug("[TYPING] started new interval");
|
|
1596
|
-
sendTypingSafe(channel, channelKey);
|
|
1597
|
-
const interval = setInterval(() => {
|
|
1598
|
-
sendTypingSafe(channel, channelKey);
|
|
1599
|
-
}, TYPING_INTERVAL_MS);
|
|
1600
|
-
typingIntervals.set(channelKey, { interval, refs: 1 });
|
|
1601
|
-
}
|
|
1602
|
-
function stopTypingForChannel(channelKey) {
|
|
1603
|
-
const entry = typingIntervals.get(channelKey);
|
|
1604
|
-
if (!entry) {
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
entry.refs -= 1;
|
|
1608
|
-
if (entry.refs <= 0) {
|
|
1609
|
-
clearInterval(entry.interval);
|
|
1610
|
-
typingIntervals.delete(channelKey);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// src/prompt-context.ts
|
|
1616
|
-
function formatDiscordPromptTime(date, options = {}) {
|
|
1617
|
-
const timeZone = options.timeZone || "UTC";
|
|
1618
|
-
const locale = options.locale || "en-AU";
|
|
1619
|
-
return new Intl.DateTimeFormat(locale, {
|
|
1620
|
-
timeZone,
|
|
1621
|
-
weekday: "short",
|
|
1622
|
-
day: "numeric",
|
|
1623
|
-
month: "short",
|
|
1624
|
-
year: "2-digit",
|
|
1625
|
-
hour: "2-digit",
|
|
1626
|
-
minute: "2-digit",
|
|
1627
|
-
hour12: false,
|
|
1628
|
-
timeZoneName: "short"
|
|
1629
|
-
}).format(date);
|
|
1630
|
-
}
|
|
1631
|
-
function wrapXmlTag(tag, content) {
|
|
1632
|
-
return `<${tag}>${content}</${tag}>`;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// src/discord-message-handler.ts
|
|
1636
|
-
var logger12 = createModuleLogger("discord-message-handler");
|
|
1637
|
-
function formatDiscordMessageMetadata(message, scope) {
|
|
1638
|
-
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
1639
|
-
const contextEntries = [
|
|
1640
|
-
["scope", scope === "dm" ? "dm" : "thread"],
|
|
1641
|
-
["sent_at", message.createdAt.toISOString()],
|
|
1642
|
-
["sent_at_local", formatDiscordPromptTime(message.createdAt)],
|
|
1643
|
-
["message_id", message.id],
|
|
1644
|
-
[
|
|
1645
|
-
"author_name",
|
|
1646
|
-
getAuthorDisplayName(message).replace(/\s+/g, " ").trim() || undefined
|
|
1647
|
-
],
|
|
1648
|
-
["author_id", message.author.id],
|
|
1649
|
-
[
|
|
1650
|
-
"thread_title",
|
|
1651
|
-
isThread ? (message.channel.name ?? "").replace(/\s+/g, " ").trim() : undefined
|
|
1652
|
-
],
|
|
1653
|
-
["thread_id", isThread ? message.channel.id : undefined],
|
|
1654
|
-
[
|
|
1655
|
-
"forum_channel_id",
|
|
1656
|
-
isThread ? message.channel.parentId ?? undefined : undefined
|
|
1657
|
-
]
|
|
1658
|
-
].filter((entry) => {
|
|
1659
|
-
return typeof entry[1] === "string" && entry[1].length > 0;
|
|
1660
|
-
});
|
|
1661
|
-
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
1662
|
-
return `<discord_message_context>${contextJson}</discord_message_context>`;
|
|
1663
|
-
}
|
|
1664
|
-
async function handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig) {
|
|
1665
|
-
if (message.author.bot) {
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
if (message.system) {
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
const scope = resolveMessageScope(message);
|
|
1672
|
-
if (scope === null) {
|
|
1673
|
-
logger12.debug({
|
|
1674
|
-
messageId: message.id,
|
|
1675
|
-
channelType: message.channel.type
|
|
1676
|
-
}, "unsupported channel type, ignoring");
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
if (!isAuthorizedMessage(message, scope, accessConfig)) {
|
|
1680
|
-
logger12.debug({
|
|
1681
|
-
messageId: message.id,
|
|
1682
|
-
authorId: message.author.id,
|
|
1683
|
-
scope
|
|
1684
|
-
}, "unauthorized");
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
let content = message.content.trim();
|
|
1688
|
-
const textAttachments = await readTextAttachments(message);
|
|
1689
|
-
if (textAttachments.length > 0) {
|
|
1690
|
-
const attachmentSuffix = textAttachments.map((attachment) => {
|
|
1691
|
-
return `
|
|
1692
|
-
|
|
1693
|
-
--- Attachment: ${attachment.filename} ---
|
|
1694
|
-
${attachment.content}`;
|
|
1695
|
-
}).join("");
|
|
1696
|
-
content = content ? content + attachmentSuffix : textAttachments[0].content;
|
|
1697
|
-
}
|
|
1698
|
-
const mediaAttachments = await readMediaAttachments(message);
|
|
1699
|
-
if (!content && mediaAttachments.length === 0) {
|
|
1700
|
-
logger12.debug({ messageId: message.id }, "ignored empty message (no text or images)");
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
logger12.info({
|
|
1704
|
-
scope,
|
|
1705
|
-
content
|
|
1706
|
-
}, "message received");
|
|
1707
|
-
const channelKey = message.channel.id;
|
|
1708
|
-
if (message.channel.isSendable()) {
|
|
1709
|
-
startTypingForChannel(message.channel, channelKey);
|
|
1710
|
-
}
|
|
1711
|
-
const { entry, created } = await sessionRegistry.getOrCreate(scope);
|
|
1712
|
-
const { session, promptQueue } = entry;
|
|
1713
|
-
if (created && scope.startsWith("thread:") && message.channel.isThread()) {
|
|
1714
|
-
logger12.info({
|
|
1715
|
-
scope,
|
|
1716
|
-
threadName: message.channel.name
|
|
1717
|
-
}, "new thread session");
|
|
1718
|
-
}
|
|
1719
|
-
const commandResult = await executeSessionCommand(content, {
|
|
1720
|
-
agentService,
|
|
1721
|
-
promptQueue,
|
|
1722
|
-
session,
|
|
1723
|
-
scope,
|
|
1724
|
-
workingEmoji: entry.workingEmoji
|
|
1725
|
-
});
|
|
1726
|
-
if (commandResult.handled) {
|
|
1727
|
-
stopTypingForChannel(channelKey);
|
|
1728
|
-
if (commandResult.workingEmoji) {
|
|
1729
|
-
entry.workingEmoji = commandResult.workingEmoji;
|
|
1730
|
-
logger12.info({ scope, emoji: commandResult.workingEmoji }, "working emoji updated");
|
|
1731
|
-
}
|
|
1732
|
-
if (commandResult.newSession) {
|
|
1733
|
-
entry.session = commandResult.newSession;
|
|
1734
|
-
logger12.info({ scope, sessionId: commandResult.newSession.sessionId }, "session replaced");
|
|
1735
|
-
}
|
|
1736
|
-
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
1737
|
-
logger12.info({ scope }, "archiving thread");
|
|
1738
|
-
const archiveChannel = message.channel;
|
|
1739
|
-
if (archiveChannel.isSendable()) {
|
|
1740
|
-
await archiveChannel.send(`\`\`\`
|
|
1741
|
-
${commandResult.response ?? "Archiving..."}
|
|
1742
|
-
\`\`\``);
|
|
1743
|
-
}
|
|
1744
|
-
try {
|
|
1745
|
-
if (archiveChannel.isThread()) {
|
|
1746
|
-
await archiveChannel.setArchived(true);
|
|
1747
|
-
}
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
logger12.error({ error }, "failed to archive thread");
|
|
1750
|
-
}
|
|
1751
|
-
await sessionRegistry.remove(scope);
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
|
-
logger12.info({
|
|
1755
|
-
messageId: message.id,
|
|
1756
|
-
command: content,
|
|
1757
|
-
hasResponse: Boolean(commandResult.response)
|
|
1758
|
-
}, `command handled: ${content}`);
|
|
1759
|
-
if (commandResult.response) {
|
|
1760
|
-
await sendCommandReply(message, commandResult.response);
|
|
1761
|
-
}
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
if (!message.channel.isSendable()) {
|
|
1765
|
-
stopTypingForChannel(channelKey);
|
|
1766
|
-
logger12.debug({ messageId: message.id }, "channel not sendable");
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
await addWorkingReaction(message, entry.workingEmoji);
|
|
1770
|
-
const queuePosition = promptQueue.getSnapshot().pending;
|
|
1771
|
-
if (queuePosition > 0) {
|
|
1772
|
-
await sendReply(message, `Queued. ${queuePosition} request(s) ahead of this one.`);
|
|
1773
|
-
}
|
|
1774
|
-
let response;
|
|
1775
|
-
try {
|
|
1776
|
-
response = await promptQueue.enqueue(async () => {
|
|
1777
|
-
let promptContent = content;
|
|
1778
|
-
let promptImages;
|
|
1779
|
-
if (mediaAttachments.length > 0) {
|
|
1780
|
-
const resolvedPromptMedia = await resolveMediaAttachmentsForPrompt(mediaAttachments, promptContent, session.model, config, agentService);
|
|
1781
|
-
promptContent = resolvedPromptMedia.content;
|
|
1782
|
-
if (resolvedPromptMedia.images.length > 0) {
|
|
1783
|
-
promptImages = resolvedPromptMedia.images;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
const discordMetadata = formatDiscordMessageMetadata(message, scope);
|
|
1787
|
-
const transformedPrompt = await config.promptTransform({
|
|
1788
|
-
rawContent: promptContent,
|
|
1789
|
-
discordMetadata,
|
|
1790
|
-
now: () => wrapXmlTag("datetime", formatDiscordPromptTime(new Date, {
|
|
1791
|
-
timeZone: config.promptTimeZone,
|
|
1792
|
-
locale: config.promptLocale
|
|
1793
|
-
})),
|
|
1794
|
-
userMessage: () => wrapXmlTag("user_message", promptContent)
|
|
1795
|
-
});
|
|
1796
|
-
return runAgentTurn(session, transformedPrompt, {
|
|
1797
|
-
images: promptImages
|
|
1798
|
-
});
|
|
1799
|
-
});
|
|
1800
|
-
} finally {
|
|
1801
|
-
stopTypingForChannel(channelKey);
|
|
1802
|
-
await removeWorkingReaction(message, entry.workingEmoji);
|
|
1803
|
-
}
|
|
1804
|
-
await sendReply(message, response);
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// src/discord-gateway-client.ts
|
|
1808
|
-
var logger13 = createModuleLogger("discord-gateway");
|
|
1809
|
-
async function startGatewayClient(config, agentService, sessionRegistry, accessConfig) {
|
|
1810
|
-
const client = new Client({
|
|
1811
|
-
intents: [
|
|
1812
|
-
GatewayIntentBits.DirectMessages,
|
|
1813
|
-
GatewayIntentBits.Guilds,
|
|
1814
|
-
GatewayIntentBits.GuildMessages,
|
|
1815
|
-
GatewayIntentBits.MessageContent
|
|
1816
|
-
],
|
|
1817
|
-
partials: [Partials.Channel]
|
|
1818
|
-
});
|
|
1819
|
-
client.once(Events.ClientReady, async (readyClient) => {
|
|
1820
|
-
logger13.info({ userTag: readyClient.user.tag }, "logged in");
|
|
1821
|
-
if (!accessConfig.startupMessage) {
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
try {
|
|
1825
|
-
const user = await readyClient.users.fetch(accessConfig.discordAllowedUserId);
|
|
1826
|
-
const dmChannel = await user.createDM();
|
|
1827
|
-
await dmChannel.send(accessConfig.startupMessage);
|
|
1828
|
-
logger13.info({
|
|
1829
|
-
userId: accessConfig.discordAllowedUserId
|
|
1830
|
-
}, "sent startup dm");
|
|
1831
|
-
} catch (error) {
|
|
1832
|
-
logger13.error({ error }, "failed to send startup dm");
|
|
1833
|
-
}
|
|
1834
|
-
});
|
|
1835
|
-
client.on(Events.MessageCreate, async (message) => {
|
|
1836
|
-
try {
|
|
1837
|
-
await handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig);
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
logger13.error({ error, direction: "IN" }, "message handling failed");
|
|
1840
|
-
await sendReply(message, "The bot hit an error while handling that message.");
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
client.on(Events.ThreadDelete, async (thread) => {
|
|
1844
|
-
const scope = `thread:${thread.id}`;
|
|
1845
|
-
logger13.info({ threadId: thread.id, scope }, "thread deleted");
|
|
1846
|
-
await sessionRegistry.remove(scope);
|
|
1847
|
-
});
|
|
1848
|
-
await client.login(config.discordBotToken);
|
|
1849
|
-
return client;
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// src/index.ts
|
|
1853
|
-
var logger14 = createModuleLogger("index");
|
|
1854
|
-
async function startDiscordGateway(config) {
|
|
1855
|
-
const resolvedConfig = resolveConfig(config);
|
|
1856
|
-
const agentService = new AgentService(resolvedConfig);
|
|
1857
|
-
logger14.info("initializing agent service");
|
|
1858
|
-
await agentService.initialize();
|
|
1859
|
-
logger14.info(agentService.getStatus(), "agent ready");
|
|
1860
|
-
const accessConfig = {
|
|
1861
|
-
discordAllowedUserId: resolvedConfig.discordAllowedUserId,
|
|
1862
|
-
discordAllowedForumChannelIds: resolvedConfig.discordAllowedForumChannelIds,
|
|
1863
|
-
discordAllowedUserIds: resolvedConfig.discordAllowedUserIds,
|
|
1864
|
-
startupMessage: resolvedConfig.startupMessage
|
|
1865
|
-
};
|
|
1866
|
-
const sessionRegistry = new SessionRegistry(agentService);
|
|
1867
|
-
const client = await startGatewayClient(resolvedConfig, agentService, sessionRegistry, accessConfig);
|
|
1868
|
-
const stop = createGatewayStopHandler(client, agentService, sessionRegistry, resolvedConfig);
|
|
1869
|
-
if (resolvedConfig.shutdownOnSignals) {
|
|
1870
|
-
registerSignalHandlers(stop);
|
|
1871
|
-
}
|
|
1872
|
-
return {
|
|
1873
|
-
client,
|
|
1874
|
-
stop,
|
|
1875
|
-
getStatus: () => {
|
|
1876
|
-
return agentService.getStatus();
|
|
1877
|
-
}
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
function createGatewayStopHandler(client, agentService, sessionRegistry, config) {
|
|
1881
|
-
let stopped = false;
|
|
1882
|
-
return async () => {
|
|
1883
|
-
if (stopped) {
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
stopped = true;
|
|
1887
|
-
logger14.info({
|
|
1888
|
-
cwd: config.cwd,
|
|
1889
|
-
agentDir: config.agentDir
|
|
1890
|
-
}, "stopping discord gateway");
|
|
1891
|
-
client.destroy();
|
|
1892
|
-
await sessionRegistry.shutdownAll();
|
|
1893
|
-
await agentService.shutdown();
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
function registerSignalHandlers(stop) {
|
|
1897
|
-
const handleSignal = (signal) => {
|
|
1898
|
-
logger14.info({ signal }, "received signal");
|
|
1899
|
-
stop().finally(() => {
|
|
1900
|
-
logger14.info("done");
|
|
1901
|
-
process.exit(0);
|
|
66
|
+
process.on("SIGTERM", () => {
|
|
67
|
+
handleSignal("SIGTERM");
|
|
1902
68
|
});
|
|
1903
|
-
};
|
|
1904
|
-
process.on("SIGINT", () => {
|
|
1905
|
-
handleSignal("SIGINT");
|
|
1906
|
-
});
|
|
1907
|
-
process.on("SIGTERM", () => {
|
|
1908
|
-
handleSignal("SIGTERM");
|
|
1909
|
-
});
|
|
1910
69
|
}
|
|
1911
|
-
export {
|
|
1912
|
-
startDiscordGateway,
|
|
1913
|
-
resolveConfig,
|
|
1914
|
-
loadDiscordGatewayConfigFromEnv,
|
|
1915
|
-
formatDiscordPromptTime
|
|
1916
|
-
};
|