@friendlyrobot/discord-pi-agent 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +338 -318
- package/dist/session-commands.d.ts +5 -0
- package/dist/types.d.ts +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,8 +43,9 @@ Every Discord prompt is wrapped with lightweight Discord context before `promptT
|
|
|
43
43
|
}
|
|
44
44
|
</discord_message_context>
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
<user_message>
|
|
47
47
|
...
|
|
48
|
+
</user_message>
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
DM prompts omit thread-only fields. `sent_at_local` uses `promptTimeZone` and `promptLocale`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DiscordGateway, DiscordGatewayConfig } from "./types";
|
|
2
2
|
export { buildDiscordMessageContextPrompt, formatDiscordPromptTime, type DiscordMessageContextPromptOptions, type DiscordPromptScope, type DiscordPromptTimeFormatOptions, } from "./prompt-context";
|
|
3
3
|
export { loadDiscordGatewayConfigFromEnv, resolveConfig } from "./config";
|
|
4
|
-
export type { AgentStatus, DiscordGateway, DiscordGatewayConfig, PromptTransform, ResolvedDiscordGatewayConfig, GatewayAccessConfig, } from "./types";
|
|
4
|
+
export type { AgentStatus, DiscordGateway, DiscordGatewayConfig, PromptTransform, PromptTransformContext, ResolvedDiscordGatewayConfig, GatewayAccessConfig, } from "./types";
|
|
5
5
|
/**
|
|
6
6
|
* Start the unified Discord gateway. Supports DM and forum thread sessions
|
|
7
7
|
* out of the box. Set discordAllowedForumChannelIds to enable forum support.
|
package/dist/index.js
CHANGED
|
@@ -506,7 +506,11 @@ class AgentService {
|
|
|
506
506
|
}
|
|
507
507
|
async prompt(text) {
|
|
508
508
|
const session = this.requireSession();
|
|
509
|
-
const transformedPrompt = await this.config.promptTransform(
|
|
509
|
+
const transformedPrompt = await this.config.promptTransform({
|
|
510
|
+
rawContent: text,
|
|
511
|
+
now: () => new Date().toISOString(),
|
|
512
|
+
wrapWithDiscordContext: () => text
|
|
513
|
+
});
|
|
510
514
|
return runAgentTurn(session, transformedPrompt);
|
|
511
515
|
}
|
|
512
516
|
async compact() {
|
|
@@ -603,7 +607,7 @@ function resolveConfig(config) {
|
|
|
603
607
|
thinkingLevel: parseThinkingLevel(config.thinkingLevel) || "medium",
|
|
604
608
|
promptTimeZone: config.promptTimeZone?.trim() || "UTC",
|
|
605
609
|
promptLocale: config.promptLocale?.trim() || "en-AU",
|
|
606
|
-
promptTransform: config.promptTransform ||
|
|
610
|
+
promptTransform: config.promptTransform || defaultPromptTransform,
|
|
607
611
|
startupMessage: config.startupMessage === undefined ? "Bot is online and ready." : config.startupMessage,
|
|
608
612
|
shutdownOnSignals: config.shutdownOnSignals ?? true,
|
|
609
613
|
visionModelId: config.visionModelId?.trim() || null,
|
|
@@ -669,8 +673,8 @@ function parseThinkingLevel(value) {
|
|
|
669
673
|
}
|
|
670
674
|
return;
|
|
671
675
|
}
|
|
672
|
-
function
|
|
673
|
-
return
|
|
676
|
+
function defaultPromptTransform(ctx) {
|
|
677
|
+
return ctx.wrapWithDiscordContext();
|
|
674
678
|
}
|
|
675
679
|
function parseStringArrayFromEnv(key) {
|
|
676
680
|
const value = process.env[key];
|
|
@@ -683,6 +687,245 @@ function parseStringArrayFromEnv(key) {
|
|
|
683
687
|
// src/discord-gateway-client.ts
|
|
684
688
|
import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
685
689
|
|
|
690
|
+
// src/session-registry.ts
|
|
691
|
+
import path3 from "node:path";
|
|
692
|
+
|
|
693
|
+
// src/prompt-queue.ts
|
|
694
|
+
class PromptQueue {
|
|
695
|
+
queue = [];
|
|
696
|
+
running = false;
|
|
697
|
+
enqueue(task) {
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
this.queue.push(async () => {
|
|
700
|
+
try {
|
|
701
|
+
resolve(await task());
|
|
702
|
+
} catch (error) {
|
|
703
|
+
reject(error);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
this.runNextTask();
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
getSnapshot() {
|
|
710
|
+
return {
|
|
711
|
+
pending: this.queue.length,
|
|
712
|
+
busy: this.running
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
runNextTask() {
|
|
716
|
+
if (this.running) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const next = this.queue.shift();
|
|
720
|
+
if (!next) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
this.running = true;
|
|
724
|
+
next().finally(() => {
|
|
725
|
+
this.running = false;
|
|
726
|
+
this.runNextTask();
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/message-chunker.ts
|
|
732
|
+
import { marked } from "marked";
|
|
733
|
+
var DISCORD_MESSAGE_LIMIT = 2000;
|
|
734
|
+
var SAFE_MESSAGE_LIMIT = 1900;
|
|
735
|
+
function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
736
|
+
if (text.length <= maxChunkSize) {
|
|
737
|
+
return [text];
|
|
738
|
+
}
|
|
739
|
+
const tokens = marked.lexer(text);
|
|
740
|
+
const chunks = [];
|
|
741
|
+
let currentTokens = [];
|
|
742
|
+
let currentSize = 0;
|
|
743
|
+
const flushChunk = () => {
|
|
744
|
+
if (currentTokens.length > 0) {
|
|
745
|
+
chunks.push(currentTokens.map((t) => t.raw).join("").trim());
|
|
746
|
+
currentTokens = [];
|
|
747
|
+
currentSize = 0;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
for (const token of tokens) {
|
|
751
|
+
const size = token.raw.length;
|
|
752
|
+
if (currentSize + size > maxChunkSize && currentTokens.length > 0) {
|
|
753
|
+
flushChunk();
|
|
754
|
+
}
|
|
755
|
+
currentTokens.push(token);
|
|
756
|
+
currentSize += size;
|
|
757
|
+
}
|
|
758
|
+
flushChunk();
|
|
759
|
+
return chunks.map((chunk) => chunk.slice(0, DISCORD_MESSAGE_LIMIT));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/discord-replies.ts
|
|
763
|
+
var logger6 = createModuleLogger("discord-replies");
|
|
764
|
+
var DISCORD_MESSAGE_LIMIT2 = 2000;
|
|
765
|
+
var FENCE_OVERHEAD = 8;
|
|
766
|
+
var MAX_CODE_FENCE_CONTENT = DISCORD_MESSAGE_LIMIT2 - FENCE_OVERHEAD;
|
|
767
|
+
function chunkByLines(text, maxSize) {
|
|
768
|
+
const lines = text.split(`
|
|
769
|
+
`);
|
|
770
|
+
const chunks = [];
|
|
771
|
+
let current = "";
|
|
772
|
+
for (const line of lines) {
|
|
773
|
+
const candidate = current ? current + `
|
|
774
|
+
` + line : line;
|
|
775
|
+
if (candidate.length > maxSize) {
|
|
776
|
+
if (current) {
|
|
777
|
+
chunks.push(current);
|
|
778
|
+
current = line;
|
|
779
|
+
} else {
|
|
780
|
+
chunks.push(line.slice(0, maxSize));
|
|
781
|
+
current = line.slice(maxSize);
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
current = candidate;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (current) {
|
|
788
|
+
chunks.push(current);
|
|
789
|
+
}
|
|
790
|
+
return chunks;
|
|
791
|
+
}
|
|
792
|
+
var DEFAULT_WORKING_EMOJI = "⚙️";
|
|
793
|
+
async function addWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
794
|
+
try {
|
|
795
|
+
await message.react(emoji);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
logger6.debug({ messageId: message.id, error }, "failed to add working reaction");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function removeWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
801
|
+
try {
|
|
802
|
+
const reaction = message.reactions.cache.get(emoji);
|
|
803
|
+
if (reaction) {
|
|
804
|
+
await reaction.users.remove(message.client.user);
|
|
805
|
+
}
|
|
806
|
+
} catch (error) {
|
|
807
|
+
logger6.debug({ messageId: message.id, error }, "failed to remove working reaction");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function sendReply(message, text) {
|
|
811
|
+
const channel = message.channel;
|
|
812
|
+
if (!channel.isSendable()) {
|
|
813
|
+
logger6.debug({
|
|
814
|
+
messageId: message.id
|
|
815
|
+
}, "reply skipped, channel not sendable");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const chunks = chunkMessage(text);
|
|
819
|
+
const [firstChunk, ...remainingChunks] = chunks;
|
|
820
|
+
if (!firstChunk) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
await message.reply(firstChunk);
|
|
825
|
+
for (const chunk of remainingChunks) {
|
|
826
|
+
await channel.send(chunk);
|
|
827
|
+
}
|
|
828
|
+
} catch (error) {
|
|
829
|
+
logger6.error({
|
|
830
|
+
messageId: message.id,
|
|
831
|
+
error
|
|
832
|
+
}, "send reply failed");
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function sendCommandReply(message, text) {
|
|
836
|
+
const channel = message.channel;
|
|
837
|
+
if (!channel.isSendable()) {
|
|
838
|
+
logger6.debug({
|
|
839
|
+
messageId: message.id
|
|
840
|
+
}, "command reply skipped, channel not sendable");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const chunks = chunkByLines(text, MAX_CODE_FENCE_CONTENT).map((c) => `\`\`\`
|
|
844
|
+
${c}
|
|
845
|
+
\`\`\``);
|
|
846
|
+
const [firstChunk, ...remainingChunks] = chunks;
|
|
847
|
+
if (!firstChunk) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
await message.reply(firstChunk);
|
|
852
|
+
for (const chunk of remainingChunks) {
|
|
853
|
+
await channel.send(chunk);
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
logger6.error({
|
|
857
|
+
messageId: message.id,
|
|
858
|
+
error
|
|
859
|
+
}, "send command reply failed");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/session-registry.ts
|
|
864
|
+
function sessionDirForScope(agentDir, scope) {
|
|
865
|
+
if (scope === "dm") {
|
|
866
|
+
return path3.join(agentDir, "sessions");
|
|
867
|
+
}
|
|
868
|
+
if (scope.startsWith("thread:")) {
|
|
869
|
+
const threadId = scope.slice(7);
|
|
870
|
+
return path3.join(agentDir, "sessions", `thread-${threadId}`);
|
|
871
|
+
}
|
|
872
|
+
throw new Error(`Unknown session scope: ${scope}`);
|
|
873
|
+
}
|
|
874
|
+
var logger7 = createModuleLogger("session-registry");
|
|
875
|
+
|
|
876
|
+
class SessionRegistry {
|
|
877
|
+
scopes = new Map;
|
|
878
|
+
agentService;
|
|
879
|
+
constructor(agentService) {
|
|
880
|
+
this.agentService = agentService;
|
|
881
|
+
}
|
|
882
|
+
async getOrCreate(scope) {
|
|
883
|
+
const existing = this.scopes.get(scope);
|
|
884
|
+
if (existing) {
|
|
885
|
+
return { entry: existing, created: false };
|
|
886
|
+
}
|
|
887
|
+
const sessionDir = sessionDirForScope(this.agentService.getAgentDir(), scope);
|
|
888
|
+
const session = await this.agentService.createSession(sessionDir);
|
|
889
|
+
const promptQueue = new PromptQueue;
|
|
890
|
+
const entry = {
|
|
891
|
+
session,
|
|
892
|
+
promptQueue,
|
|
893
|
+
createdAt: new Date,
|
|
894
|
+
workingEmoji: DEFAULT_WORKING_EMOJI
|
|
895
|
+
};
|
|
896
|
+
this.scopes.set(scope, entry);
|
|
897
|
+
logger7.debug({
|
|
898
|
+
scope,
|
|
899
|
+
sessionDir,
|
|
900
|
+
sessionId: session.sessionId
|
|
901
|
+
}, "scope registered");
|
|
902
|
+
return { entry, created: true };
|
|
903
|
+
}
|
|
904
|
+
async remove(scope) {
|
|
905
|
+
const entry = this.scopes.get(scope);
|
|
906
|
+
if (!entry) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
logger7.debug({ scope }, "removing scope");
|
|
910
|
+
await entry.session.abort();
|
|
911
|
+
entry.session.dispose();
|
|
912
|
+
this.scopes.delete(scope);
|
|
913
|
+
}
|
|
914
|
+
get(scope) {
|
|
915
|
+
return this.scopes.get(scope);
|
|
916
|
+
}
|
|
917
|
+
getScopes() {
|
|
918
|
+
return Array.from(this.scopes.keys());
|
|
919
|
+
}
|
|
920
|
+
async shutdownAll() {
|
|
921
|
+
logger7.info({ count: this.scopes.size }, "shutting down all scopes");
|
|
922
|
+
const scopes = Array.from(this.scopes.keys());
|
|
923
|
+
for (const scope of scopes) {
|
|
924
|
+
await this.remove(scope);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
686
929
|
// src/session-commands.ts
|
|
687
930
|
function getSessionStatusText(session, promptQueue, extras) {
|
|
688
931
|
const model = session.model ? `${session.model.provider}/${session.model.id}` : "(no model selected)";
|
|
@@ -886,22 +1129,23 @@ async function handleResetSessionCommand(trimmedInput, context) {
|
|
|
886
1129
|
if (trimmedInput !== "!reset-session") {
|
|
887
1130
|
return null;
|
|
888
1131
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
response: await context.promptQueue.enqueue(async () => {
|
|
893
|
-
const previousSession = context.session;
|
|
894
|
-
await previousSession.abort();
|
|
895
|
-
previousSession.dispose();
|
|
896
|
-
return `Session reset. Old session kept at ${previousSession.sessionFile ?? "(unknown path)"}. Use !archive to archive the thread and start fresh.`;
|
|
897
|
-
})
|
|
898
|
-
};
|
|
1132
|
+
const effectiveSession = requireEffectiveSession(context);
|
|
1133
|
+
if ("handled" in effectiveSession) {
|
|
1134
|
+
return effectiveSession;
|
|
899
1135
|
}
|
|
1136
|
+
let newSession;
|
|
1137
|
+
const response = await context.promptQueue.enqueue(async () => {
|
|
1138
|
+
const previousSessionFile = effectiveSession.session.sessionFile;
|
|
1139
|
+
await effectiveSession.session.abort();
|
|
1140
|
+
effectiveSession.session.dispose();
|
|
1141
|
+
const sessionDir = sessionDirForScope(context.agentService.getAgentDir(), context.scope);
|
|
1142
|
+
newSession = await context.agentService.createSession(sessionDir);
|
|
1143
|
+
return `Started a fresh session. Old session kept at ${previousSessionFile ?? "(unknown path)"}.`;
|
|
1144
|
+
});
|
|
900
1145
|
return {
|
|
901
1146
|
handled: true,
|
|
902
|
-
response
|
|
903
|
-
|
|
904
|
-
})
|
|
1147
|
+
response,
|
|
1148
|
+
newSession
|
|
905
1149
|
};
|
|
906
1150
|
}
|
|
907
1151
|
async function handleReactionCommand(trimmedInput, _context) {
|
|
@@ -959,7 +1203,7 @@ async function executeSessionCommand(input, context) {
|
|
|
959
1203
|
}
|
|
960
1204
|
|
|
961
1205
|
// src/discord-attachments.ts
|
|
962
|
-
var
|
|
1206
|
+
var logger8 = createModuleLogger("discord-attachments");
|
|
963
1207
|
var TEXT_ATTACHMENT_EXTENSIONS = [
|
|
964
1208
|
".txt",
|
|
965
1209
|
".md",
|
|
@@ -1021,11 +1265,11 @@ async function readTextAttachments(message) {
|
|
|
1021
1265
|
const results = [];
|
|
1022
1266
|
for (const [, attachment] of attachments) {
|
|
1023
1267
|
if (!isSupportedTextAttachment(attachment)) {
|
|
1024
|
-
|
|
1268
|
+
logger8.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
|
|
1025
1269
|
continue;
|
|
1026
1270
|
}
|
|
1027
1271
|
if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
|
|
1028
|
-
|
|
1272
|
+
logger8.warn({
|
|
1029
1273
|
messageId: message.id,
|
|
1030
1274
|
filename: attachment.name,
|
|
1031
1275
|
size: attachment.size
|
|
@@ -1033,14 +1277,14 @@ async function readTextAttachments(message) {
|
|
|
1033
1277
|
continue;
|
|
1034
1278
|
}
|
|
1035
1279
|
try {
|
|
1036
|
-
|
|
1280
|
+
logger8.info({
|
|
1037
1281
|
messageId: message.id,
|
|
1038
1282
|
filename: attachment.name,
|
|
1039
1283
|
size: attachment.size
|
|
1040
1284
|
}, "fetching attachment");
|
|
1041
1285
|
const response = await fetch(attachment.url);
|
|
1042
1286
|
if (!response.ok) {
|
|
1043
|
-
|
|
1287
|
+
logger8.warn({
|
|
1044
1288
|
messageId: message.id,
|
|
1045
1289
|
filename: attachment.name,
|
|
1046
1290
|
status: response.status
|
|
@@ -1050,7 +1294,7 @@ async function readTextAttachments(message) {
|
|
|
1050
1294
|
const content = await response.text();
|
|
1051
1295
|
results.push({ filename: attachment.name, content });
|
|
1052
1296
|
} catch (error) {
|
|
1053
|
-
|
|
1297
|
+
logger8.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
|
|
1054
1298
|
}
|
|
1055
1299
|
}
|
|
1056
1300
|
return results;
|
|
@@ -1066,7 +1310,7 @@ async function readMediaAttachments(message) {
|
|
|
1066
1310
|
continue;
|
|
1067
1311
|
}
|
|
1068
1312
|
if (attachment.size > MAX_MEDIA_ATTACHMENT_SIZE_BYTES) {
|
|
1069
|
-
|
|
1313
|
+
logger8.warn({
|
|
1070
1314
|
messageId: message.id,
|
|
1071
1315
|
filename: attachment.name,
|
|
1072
1316
|
size: attachment.size
|
|
@@ -1074,14 +1318,14 @@ async function readMediaAttachments(message) {
|
|
|
1074
1318
|
continue;
|
|
1075
1319
|
}
|
|
1076
1320
|
try {
|
|
1077
|
-
|
|
1321
|
+
logger8.info({
|
|
1078
1322
|
messageId: message.id,
|
|
1079
1323
|
filename: attachment.name,
|
|
1080
1324
|
size: attachment.size
|
|
1081
1325
|
}, "fetching media attachment");
|
|
1082
1326
|
const response = await fetch(attachment.url);
|
|
1083
1327
|
if (!response.ok) {
|
|
1084
|
-
|
|
1328
|
+
logger8.warn({
|
|
1085
1329
|
messageId: message.id,
|
|
1086
1330
|
filename: attachment.name,
|
|
1087
1331
|
status: response.status
|
|
@@ -1095,7 +1339,7 @@ async function readMediaAttachments(message) {
|
|
|
1095
1339
|
mimeType: attachment.contentType ?? "application/octet-stream"
|
|
1096
1340
|
});
|
|
1097
1341
|
} catch (error) {
|
|
1098
|
-
|
|
1342
|
+
logger8.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
|
|
1099
1343
|
}
|
|
1100
1344
|
}
|
|
1101
1345
|
return results;
|
|
@@ -1133,140 +1377,8 @@ function isAuthorizedMessage(message, scope, accessConfig) {
|
|
|
1133
1377
|
return false;
|
|
1134
1378
|
}
|
|
1135
1379
|
|
|
1136
|
-
// src/message-chunker.ts
|
|
1137
|
-
import { marked } from "marked";
|
|
1138
|
-
var DISCORD_MESSAGE_LIMIT = 2000;
|
|
1139
|
-
var SAFE_MESSAGE_LIMIT = 1900;
|
|
1140
|
-
function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
1141
|
-
if (text.length <= maxChunkSize) {
|
|
1142
|
-
return [text];
|
|
1143
|
-
}
|
|
1144
|
-
const tokens = marked.lexer(text);
|
|
1145
|
-
const chunks = [];
|
|
1146
|
-
let currentTokens = [];
|
|
1147
|
-
let currentSize = 0;
|
|
1148
|
-
const flushChunk = () => {
|
|
1149
|
-
if (currentTokens.length > 0) {
|
|
1150
|
-
chunks.push(currentTokens.map((t) => t.raw).join("").trim());
|
|
1151
|
-
currentTokens = [];
|
|
1152
|
-
currentSize = 0;
|
|
1153
|
-
}
|
|
1154
|
-
};
|
|
1155
|
-
for (const token of tokens) {
|
|
1156
|
-
const size = token.raw.length;
|
|
1157
|
-
if (currentSize + size > maxChunkSize && currentTokens.length > 0) {
|
|
1158
|
-
flushChunk();
|
|
1159
|
-
}
|
|
1160
|
-
currentTokens.push(token);
|
|
1161
|
-
currentSize += size;
|
|
1162
|
-
}
|
|
1163
|
-
flushChunk();
|
|
1164
|
-
return chunks.map((chunk) => chunk.slice(0, DISCORD_MESSAGE_LIMIT));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// src/discord-replies.ts
|
|
1168
|
-
var logger7 = createModuleLogger("discord-replies");
|
|
1169
|
-
var DISCORD_MESSAGE_LIMIT2 = 2000;
|
|
1170
|
-
var FENCE_OVERHEAD = 8;
|
|
1171
|
-
var MAX_CODE_FENCE_CONTENT = DISCORD_MESSAGE_LIMIT2 - FENCE_OVERHEAD;
|
|
1172
|
-
function chunkByLines(text, maxSize) {
|
|
1173
|
-
const lines = text.split(`
|
|
1174
|
-
`);
|
|
1175
|
-
const chunks = [];
|
|
1176
|
-
let current = "";
|
|
1177
|
-
for (const line of lines) {
|
|
1178
|
-
const candidate = current ? current + `
|
|
1179
|
-
` + line : line;
|
|
1180
|
-
if (candidate.length > maxSize) {
|
|
1181
|
-
if (current) {
|
|
1182
|
-
chunks.push(current);
|
|
1183
|
-
current = line;
|
|
1184
|
-
} else {
|
|
1185
|
-
chunks.push(line.slice(0, maxSize));
|
|
1186
|
-
current = line.slice(maxSize);
|
|
1187
|
-
}
|
|
1188
|
-
} else {
|
|
1189
|
-
current = candidate;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
if (current) {
|
|
1193
|
-
chunks.push(current);
|
|
1194
|
-
}
|
|
1195
|
-
return chunks;
|
|
1196
|
-
}
|
|
1197
|
-
var DEFAULT_WORKING_EMOJI = "⚙️";
|
|
1198
|
-
async function addWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
1199
|
-
try {
|
|
1200
|
-
await message.react(emoji);
|
|
1201
|
-
} catch (error) {
|
|
1202
|
-
logger7.debug({ messageId: message.id, error }, "failed to add working reaction");
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
async function removeWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
1206
|
-
try {
|
|
1207
|
-
const reaction = message.reactions.cache.get(emoji);
|
|
1208
|
-
if (reaction) {
|
|
1209
|
-
await reaction.users.remove(message.client.user);
|
|
1210
|
-
}
|
|
1211
|
-
} catch (error) {
|
|
1212
|
-
logger7.debug({ messageId: message.id, error }, "failed to remove working reaction");
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
async function sendReply(message, text) {
|
|
1216
|
-
const channel = message.channel;
|
|
1217
|
-
if (!channel.isSendable()) {
|
|
1218
|
-
logger7.debug({
|
|
1219
|
-
messageId: message.id
|
|
1220
|
-
}, "reply skipped, channel not sendable");
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
const chunks = chunkMessage(text);
|
|
1224
|
-
const [firstChunk, ...remainingChunks] = chunks;
|
|
1225
|
-
if (!firstChunk) {
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
try {
|
|
1229
|
-
await message.reply(firstChunk);
|
|
1230
|
-
for (const chunk of remainingChunks) {
|
|
1231
|
-
await channel.send(chunk);
|
|
1232
|
-
}
|
|
1233
|
-
} catch (error) {
|
|
1234
|
-
logger7.error({
|
|
1235
|
-
messageId: message.id,
|
|
1236
|
-
error
|
|
1237
|
-
}, "send reply failed");
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
async function sendCommandReply(message, text) {
|
|
1241
|
-
const channel = message.channel;
|
|
1242
|
-
if (!channel.isSendable()) {
|
|
1243
|
-
logger7.debug({
|
|
1244
|
-
messageId: message.id
|
|
1245
|
-
}, "command reply skipped, channel not sendable");
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const chunks = chunkByLines(text, MAX_CODE_FENCE_CONTENT).map((c) => `\`\`\`
|
|
1249
|
-
${c}
|
|
1250
|
-
\`\`\``);
|
|
1251
|
-
const [firstChunk, ...remainingChunks] = chunks;
|
|
1252
|
-
if (!firstChunk) {
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
try {
|
|
1256
|
-
await message.reply(firstChunk);
|
|
1257
|
-
for (const chunk of remainingChunks) {
|
|
1258
|
-
await channel.send(chunk);
|
|
1259
|
-
}
|
|
1260
|
-
} catch (error) {
|
|
1261
|
-
logger7.error({
|
|
1262
|
-
messageId: message.id,
|
|
1263
|
-
error
|
|
1264
|
-
}, "send command reply failed");
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
1380
|
// src/media-description.ts
|
|
1269
|
-
var
|
|
1381
|
+
var logger9 = createModuleLogger("media-description");
|
|
1270
1382
|
async function describeMediaAttachment(agentService, imageData, mimeType, userText, visionModel) {
|
|
1271
1383
|
const session = await agentService.createTemporarySession();
|
|
1272
1384
|
await session.setModel(visionModel);
|
|
@@ -1287,7 +1399,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
|
|
|
1287
1399
|
await session.prompt(promptText, { images: [imageContent] });
|
|
1288
1400
|
text = extractLastAssistantText(session);
|
|
1289
1401
|
} catch (error) {
|
|
1290
|
-
|
|
1402
|
+
logger9.error({ error, mimeType }, "vision model prompt failed");
|
|
1291
1403
|
text = "(Vision model failed to process the file.)";
|
|
1292
1404
|
} finally {
|
|
1293
1405
|
session.dispose();
|
|
@@ -1295,7 +1407,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
|
|
|
1295
1407
|
if (!text) {
|
|
1296
1408
|
return "(Vision model returned no description.)";
|
|
1297
1409
|
}
|
|
1298
|
-
|
|
1410
|
+
logger9.debug({ textLength: text.length, mimeType }, "media described");
|
|
1299
1411
|
return text;
|
|
1300
1412
|
}
|
|
1301
1413
|
function extractLastAssistantText(session) {
|
|
@@ -1331,7 +1443,7 @@ function isAssistantMessage(msg) {
|
|
|
1331
1443
|
}
|
|
1332
1444
|
|
|
1333
1445
|
// src/discord-media-resolution.ts
|
|
1334
|
-
var
|
|
1446
|
+
var logger10 = createModuleLogger("discord-media-resolution");
|
|
1335
1447
|
function parseProviderModelId(value) {
|
|
1336
1448
|
const trimmed = value.trim();
|
|
1337
1449
|
if (!trimmed) {
|
|
@@ -1365,7 +1477,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1365
1477
|
const modelSupportsVision = currentModel?.input.includes("image") ?? false;
|
|
1366
1478
|
if (modelSupportsVision) {
|
|
1367
1479
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1368
|
-
|
|
1480
|
+
logger10.info({
|
|
1369
1481
|
count: mediaAttachments.length,
|
|
1370
1482
|
filenames: names,
|
|
1371
1483
|
model: currentModel ? `${currentModel.provider}/${currentModel.id}` : "none"
|
|
@@ -1381,7 +1493,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1381
1493
|
}
|
|
1382
1494
|
if (!config.visionModelId) {
|
|
1383
1495
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1384
|
-
|
|
1496
|
+
logger10.info({ filenames: names }, "media attachments received but vision model not configured");
|
|
1385
1497
|
const note = `
|
|
1386
1498
|
|
|
1387
1499
|
[User sent media attachment(s): ${names}]
|
|
@@ -1397,7 +1509,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1397
1509
|
}
|
|
1398
1510
|
const visionModel = agentService.models.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
|
|
1399
1511
|
if (!visionModel) {
|
|
1400
|
-
|
|
1512
|
+
logger10.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
|
|
1401
1513
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1402
1514
|
const note = `
|
|
1403
1515
|
|
|
@@ -1408,7 +1520,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1408
1520
|
images: []
|
|
1409
1521
|
};
|
|
1410
1522
|
}
|
|
1411
|
-
|
|
1523
|
+
logger10.info({
|
|
1412
1524
|
count: mediaAttachments.length,
|
|
1413
1525
|
visionModel: `${visionModel.provider}/${visionModel.id}`
|
|
1414
1526
|
}, "describing media with vision model");
|
|
@@ -1435,7 +1547,7 @@ ${content}` : descriptionPrefix,
|
|
|
1435
1547
|
}
|
|
1436
1548
|
|
|
1437
1549
|
// src/discord-typing.ts
|
|
1438
|
-
var
|
|
1550
|
+
var logger11 = createModuleLogger("discord-typing");
|
|
1439
1551
|
var TYPING_INTERVAL_MS = 9000;
|
|
1440
1552
|
var typingIntervals = new Map;
|
|
1441
1553
|
async function sendTypingSafe(channel, channelKey) {
|
|
@@ -1458,18 +1570,18 @@ async function sendTypingSafe(channel, channelKey) {
|
|
|
1458
1570
|
retryMs = parsed.retry_after * 1000 + 500;
|
|
1459
1571
|
}
|
|
1460
1572
|
} catch {}
|
|
1461
|
-
|
|
1573
|
+
logger11.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
|
|
1462
1574
|
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1463
1575
|
await fetch(url, {
|
|
1464
1576
|
method: "POST",
|
|
1465
1577
|
headers: { Authorization: `Bot ${token}` }
|
|
1466
1578
|
});
|
|
1467
|
-
|
|
1579
|
+
logger11.warn({ channelKey }, "[TYPING] retry done");
|
|
1468
1580
|
return;
|
|
1469
1581
|
}
|
|
1470
|
-
|
|
1582
|
+
logger11.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
|
|
1471
1583
|
} catch (error) {
|
|
1472
|
-
|
|
1584
|
+
logger11.error({ channelKey, error }, "[TYPING] FAILED");
|
|
1473
1585
|
}
|
|
1474
1586
|
}
|
|
1475
1587
|
function startTypingForChannel(channel, channelKey) {
|
|
@@ -1478,7 +1590,7 @@ function startTypingForChannel(channel, channelKey) {
|
|
|
1478
1590
|
existing.refs += 1;
|
|
1479
1591
|
return;
|
|
1480
1592
|
}
|
|
1481
|
-
|
|
1593
|
+
logger11.debug("[TYPING] started new interval");
|
|
1482
1594
|
sendTypingSafe(channel, channelKey);
|
|
1483
1595
|
const interval = setInterval(() => {
|
|
1484
1596
|
sendTypingSafe(channel, channelKey);
|
|
@@ -1515,12 +1627,9 @@ function buildDiscordMessageContextPrompt(userMessage, options) {
|
|
|
1515
1627
|
});
|
|
1516
1628
|
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
1517
1629
|
return [
|
|
1518
|
-
"
|
|
1519
|
-
contextJson,
|
|
1520
|
-
"</discord_message_context>",
|
|
1630
|
+
wrapXmlTag("discord_message_context", contextJson),
|
|
1521
1631
|
"",
|
|
1522
|
-
"
|
|
1523
|
-
userMessage.trim()
|
|
1632
|
+
wrapXmlTag("user_message", userMessage.trim())
|
|
1524
1633
|
].join(`
|
|
1525
1634
|
`);
|
|
1526
1635
|
}
|
|
@@ -1539,6 +1648,9 @@ function formatDiscordPromptTime(date, options = {}) {
|
|
|
1539
1648
|
timeZoneName: "short"
|
|
1540
1649
|
}).format(date);
|
|
1541
1650
|
}
|
|
1651
|
+
function wrapXmlTag(tag, content) {
|
|
1652
|
+
return `<${tag}>${content}</${tag}>`;
|
|
1653
|
+
}
|
|
1542
1654
|
function normalizeContextValue(value) {
|
|
1543
1655
|
if (value === undefined) {
|
|
1544
1656
|
return;
|
|
@@ -1547,23 +1659,33 @@ function normalizeContextValue(value) {
|
|
|
1547
1659
|
}
|
|
1548
1660
|
|
|
1549
1661
|
// src/discord-message-handler.ts
|
|
1550
|
-
var
|
|
1551
|
-
function
|
|
1662
|
+
var logger12 = createModuleLogger("discord-message-handler");
|
|
1663
|
+
function buildPromptTransformContext(message, scope, content, config) {
|
|
1552
1664
|
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
sentAtLocal: formatDiscordPromptTime(message.createdAt, {
|
|
1557
|
-
timeZone: config.promptTimeZone,
|
|
1558
|
-
locale: config.promptLocale
|
|
1559
|
-
}),
|
|
1560
|
-
messageId: message.id,
|
|
1561
|
-
authorId: message.author.id,
|
|
1562
|
-
authorName: getAuthorDisplayName(message),
|
|
1563
|
-
threadId: isThread ? message.channel.id : undefined,
|
|
1564
|
-
threadTitle: isThread ? message.channel.name : undefined,
|
|
1565
|
-
forumChannelId: isThread ? message.channel.parentId : undefined
|
|
1665
|
+
const sentAtLocal = formatDiscordPromptTime(message.createdAt, {
|
|
1666
|
+
timeZone: config.promptTimeZone,
|
|
1667
|
+
locale: config.promptLocale
|
|
1566
1668
|
});
|
|
1669
|
+
return {
|
|
1670
|
+
rawContent: content,
|
|
1671
|
+
now: () => `<datetime>${sentAtLocal}</datetime>`,
|
|
1672
|
+
wrapWithDiscordContext: () => {
|
|
1673
|
+
return buildDiscordMessageContextPrompt(content, {
|
|
1674
|
+
scope: scope === "dm" ? "dm" : "thread",
|
|
1675
|
+
sentAt: message.createdAt.toISOString(),
|
|
1676
|
+
sentAtLocal: formatDiscordPromptTime(message.createdAt, {
|
|
1677
|
+
timeZone: config.promptTimeZone,
|
|
1678
|
+
locale: config.promptLocale
|
|
1679
|
+
}),
|
|
1680
|
+
messageId: message.id,
|
|
1681
|
+
authorId: message.author.id,
|
|
1682
|
+
authorName: getAuthorDisplayName(message),
|
|
1683
|
+
threadId: isThread ? message.channel.id : undefined,
|
|
1684
|
+
threadTitle: isThread ? message.channel.name : undefined,
|
|
1685
|
+
forumChannelId: isThread ? message.channel.parentId : undefined
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1567
1689
|
}
|
|
1568
1690
|
async function handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig) {
|
|
1569
1691
|
if (message.author.bot) {
|
|
@@ -1574,14 +1696,14 @@ async function handleDiscordMessage(message, config, agentService, sessionRegist
|
|
|
1574
1696
|
}
|
|
1575
1697
|
const scope = resolveMessageScope(message);
|
|
1576
1698
|
if (scope === null) {
|
|
1577
|
-
|
|
1699
|
+
logger12.debug({
|
|
1578
1700
|
messageId: message.id,
|
|
1579
1701
|
channelType: message.channel.type
|
|
1580
1702
|
}, "unsupported channel type, ignoring");
|
|
1581
1703
|
return;
|
|
1582
1704
|
}
|
|
1583
1705
|
if (!isAuthorizedMessage(message, scope, accessConfig)) {
|
|
1584
|
-
|
|
1706
|
+
logger12.debug({
|
|
1585
1707
|
messageId: message.id,
|
|
1586
1708
|
authorId: message.author.id,
|
|
1587
1709
|
scope
|
|
@@ -1601,10 +1723,10 @@ ${attachment.content}`;
|
|
|
1601
1723
|
}
|
|
1602
1724
|
const mediaAttachments = await readMediaAttachments(message);
|
|
1603
1725
|
if (!content && mediaAttachments.length === 0) {
|
|
1604
|
-
|
|
1726
|
+
logger12.debug({ messageId: message.id }, "ignored empty message (no text or images)");
|
|
1605
1727
|
return;
|
|
1606
1728
|
}
|
|
1607
|
-
|
|
1729
|
+
logger12.info({
|
|
1608
1730
|
scope,
|
|
1609
1731
|
content
|
|
1610
1732
|
}, "message received");
|
|
@@ -1615,7 +1737,7 @@ ${attachment.content}`;
|
|
|
1615
1737
|
const { entry, created } = await sessionRegistry.getOrCreate(scope);
|
|
1616
1738
|
const { session, promptQueue } = entry;
|
|
1617
1739
|
if (created && scope.startsWith("thread:") && message.channel.isThread()) {
|
|
1618
|
-
|
|
1740
|
+
logger12.info({
|
|
1619
1741
|
scope,
|
|
1620
1742
|
threadName: message.channel.name
|
|
1621
1743
|
}, "new thread session");
|
|
@@ -1624,16 +1746,21 @@ ${attachment.content}`;
|
|
|
1624
1746
|
agentService,
|
|
1625
1747
|
promptQueue,
|
|
1626
1748
|
session,
|
|
1749
|
+
scope,
|
|
1627
1750
|
workingEmoji: entry.workingEmoji
|
|
1628
1751
|
});
|
|
1629
1752
|
if (commandResult.handled) {
|
|
1630
1753
|
stopTypingForChannel(channelKey);
|
|
1631
1754
|
if (commandResult.workingEmoji) {
|
|
1632
1755
|
entry.workingEmoji = commandResult.workingEmoji;
|
|
1633
|
-
|
|
1756
|
+
logger12.info({ scope, emoji: commandResult.workingEmoji }, "working emoji updated");
|
|
1757
|
+
}
|
|
1758
|
+
if (commandResult.newSession) {
|
|
1759
|
+
entry.session = commandResult.newSession;
|
|
1760
|
+
logger12.info({ scope, sessionId: commandResult.newSession.sessionId }, "session replaced");
|
|
1634
1761
|
}
|
|
1635
1762
|
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
1636
|
-
|
|
1763
|
+
logger12.info({ scope }, "archiving thread");
|
|
1637
1764
|
const archiveChannel = message.channel;
|
|
1638
1765
|
if (archiveChannel.isSendable()) {
|
|
1639
1766
|
await archiveChannel.send(`\`\`\`
|
|
@@ -1645,12 +1772,12 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1645
1772
|
await archiveChannel.setArchived(true);
|
|
1646
1773
|
}
|
|
1647
1774
|
} catch (error) {
|
|
1648
|
-
|
|
1775
|
+
logger12.error({ error }, "failed to archive thread");
|
|
1649
1776
|
}
|
|
1650
1777
|
await sessionRegistry.remove(scope);
|
|
1651
1778
|
return;
|
|
1652
1779
|
}
|
|
1653
|
-
|
|
1780
|
+
logger12.info({
|
|
1654
1781
|
messageId: message.id,
|
|
1655
1782
|
command: content,
|
|
1656
1783
|
hasResponse: Boolean(commandResult.response)
|
|
@@ -1662,7 +1789,7 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1662
1789
|
}
|
|
1663
1790
|
if (!message.channel.isSendable()) {
|
|
1664
1791
|
stopTypingForChannel(channelKey);
|
|
1665
|
-
|
|
1792
|
+
logger12.debug({ messageId: message.id }, "channel not sendable");
|
|
1666
1793
|
return;
|
|
1667
1794
|
}
|
|
1668
1795
|
await addWorkingReaction(message, entry.workingEmoji);
|
|
@@ -1682,8 +1809,8 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1682
1809
|
promptImages = resolvedPromptMedia.images;
|
|
1683
1810
|
}
|
|
1684
1811
|
}
|
|
1685
|
-
const
|
|
1686
|
-
const transformedPrompt = await config.promptTransform(
|
|
1812
|
+
const transformCtx = buildPromptTransformContext(message, scope, promptContent, config);
|
|
1813
|
+
const transformedPrompt = await config.promptTransform(transformCtx);
|
|
1687
1814
|
return runAgentTurn(session, transformedPrompt, {
|
|
1688
1815
|
images: promptImages
|
|
1689
1816
|
});
|
|
@@ -1696,7 +1823,7 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1696
1823
|
}
|
|
1697
1824
|
|
|
1698
1825
|
// src/discord-gateway-client.ts
|
|
1699
|
-
var
|
|
1826
|
+
var logger13 = createModuleLogger("discord-gateway");
|
|
1700
1827
|
async function startGatewayClient(config, agentService, sessionRegistry, accessConfig) {
|
|
1701
1828
|
const client = new Client({
|
|
1702
1829
|
intents: [
|
|
@@ -1708,7 +1835,7 @@ async function startGatewayClient(config, agentService, sessionRegistry, accessC
|
|
|
1708
1835
|
partials: [Partials.Channel]
|
|
1709
1836
|
});
|
|
1710
1837
|
client.once(Events.ClientReady, async (readyClient) => {
|
|
1711
|
-
|
|
1838
|
+
logger13.info({ userTag: readyClient.user.tag }, "logged in");
|
|
1712
1839
|
if (!accessConfig.startupMessage) {
|
|
1713
1840
|
return;
|
|
1714
1841
|
}
|
|
@@ -1716,137 +1843,30 @@ async function startGatewayClient(config, agentService, sessionRegistry, accessC
|
|
|
1716
1843
|
const user = await readyClient.users.fetch(accessConfig.discordAllowedUserId);
|
|
1717
1844
|
const dmChannel = await user.createDM();
|
|
1718
1845
|
await dmChannel.send(accessConfig.startupMessage);
|
|
1719
|
-
|
|
1846
|
+
logger13.info({
|
|
1720
1847
|
userId: accessConfig.discordAllowedUserId
|
|
1721
1848
|
}, "sent startup dm");
|
|
1722
1849
|
} catch (error) {
|
|
1723
|
-
|
|
1850
|
+
logger13.error({ error }, "failed to send startup dm");
|
|
1724
1851
|
}
|
|
1725
1852
|
});
|
|
1726
1853
|
client.on(Events.MessageCreate, async (message) => {
|
|
1727
1854
|
try {
|
|
1728
1855
|
await handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig);
|
|
1729
1856
|
} catch (error) {
|
|
1730
|
-
|
|
1857
|
+
logger13.error({ error, direction: "IN" }, "message handling failed");
|
|
1731
1858
|
await sendReply(message, "The bot hit an error while handling that message.");
|
|
1732
1859
|
}
|
|
1733
1860
|
});
|
|
1734
1861
|
client.on(Events.ThreadDelete, async (thread) => {
|
|
1735
1862
|
const scope = `thread:${thread.id}`;
|
|
1736
|
-
|
|
1863
|
+
logger13.info({ threadId: thread.id, scope }, "thread deleted");
|
|
1737
1864
|
await sessionRegistry.remove(scope);
|
|
1738
1865
|
});
|
|
1739
1866
|
await client.login(config.discordBotToken);
|
|
1740
1867
|
return client;
|
|
1741
1868
|
}
|
|
1742
1869
|
|
|
1743
|
-
// src/session-registry.ts
|
|
1744
|
-
import path3 from "node:path";
|
|
1745
|
-
|
|
1746
|
-
// src/prompt-queue.ts
|
|
1747
|
-
class PromptQueue {
|
|
1748
|
-
queue = [];
|
|
1749
|
-
running = false;
|
|
1750
|
-
enqueue(task) {
|
|
1751
|
-
return new Promise((resolve, reject) => {
|
|
1752
|
-
this.queue.push(async () => {
|
|
1753
|
-
try {
|
|
1754
|
-
resolve(await task());
|
|
1755
|
-
} catch (error) {
|
|
1756
|
-
reject(error);
|
|
1757
|
-
}
|
|
1758
|
-
});
|
|
1759
|
-
this.runNextTask();
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
getSnapshot() {
|
|
1763
|
-
return {
|
|
1764
|
-
pending: this.queue.length,
|
|
1765
|
-
busy: this.running
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
runNextTask() {
|
|
1769
|
-
if (this.running) {
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
const next = this.queue.shift();
|
|
1773
|
-
if (!next) {
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1776
|
-
this.running = true;
|
|
1777
|
-
next().finally(() => {
|
|
1778
|
-
this.running = false;
|
|
1779
|
-
this.runNextTask();
|
|
1780
|
-
});
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
// src/session-registry.ts
|
|
1785
|
-
function sessionDirForScope(agentDir, scope) {
|
|
1786
|
-
if (scope === "dm") {
|
|
1787
|
-
return path3.join(agentDir, "sessions");
|
|
1788
|
-
}
|
|
1789
|
-
if (scope.startsWith("thread:")) {
|
|
1790
|
-
const threadId = scope.slice(7);
|
|
1791
|
-
return path3.join(agentDir, "sessions", `thread-${threadId}`);
|
|
1792
|
-
}
|
|
1793
|
-
throw new Error(`Unknown session scope: ${scope}`);
|
|
1794
|
-
}
|
|
1795
|
-
var logger13 = createModuleLogger("session-registry");
|
|
1796
|
-
|
|
1797
|
-
class SessionRegistry {
|
|
1798
|
-
scopes = new Map;
|
|
1799
|
-
agentService;
|
|
1800
|
-
constructor(agentService) {
|
|
1801
|
-
this.agentService = agentService;
|
|
1802
|
-
}
|
|
1803
|
-
async getOrCreate(scope) {
|
|
1804
|
-
const existing = this.scopes.get(scope);
|
|
1805
|
-
if (existing) {
|
|
1806
|
-
return { entry: existing, created: false };
|
|
1807
|
-
}
|
|
1808
|
-
const sessionDir = sessionDirForScope(this.agentService.getAgentDir(), scope);
|
|
1809
|
-
const session = await this.agentService.createSession(sessionDir);
|
|
1810
|
-
const promptQueue = new PromptQueue;
|
|
1811
|
-
const entry = {
|
|
1812
|
-
session,
|
|
1813
|
-
promptQueue,
|
|
1814
|
-
createdAt: new Date,
|
|
1815
|
-
workingEmoji: DEFAULT_WORKING_EMOJI
|
|
1816
|
-
};
|
|
1817
|
-
this.scopes.set(scope, entry);
|
|
1818
|
-
logger13.debug({
|
|
1819
|
-
scope,
|
|
1820
|
-
sessionDir,
|
|
1821
|
-
sessionId: session.sessionId
|
|
1822
|
-
}, "scope registered");
|
|
1823
|
-
return { entry, created: true };
|
|
1824
|
-
}
|
|
1825
|
-
async remove(scope) {
|
|
1826
|
-
const entry = this.scopes.get(scope);
|
|
1827
|
-
if (!entry) {
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
logger13.debug({ scope }, "removing scope");
|
|
1831
|
-
await entry.session.abort();
|
|
1832
|
-
entry.session.dispose();
|
|
1833
|
-
this.scopes.delete(scope);
|
|
1834
|
-
}
|
|
1835
|
-
get(scope) {
|
|
1836
|
-
return this.scopes.get(scope);
|
|
1837
|
-
}
|
|
1838
|
-
getScopes() {
|
|
1839
|
-
return Array.from(this.scopes.keys());
|
|
1840
|
-
}
|
|
1841
|
-
async shutdownAll() {
|
|
1842
|
-
logger13.info({ count: this.scopes.size }, "shutting down all scopes");
|
|
1843
|
-
const scopes = Array.from(this.scopes.keys());
|
|
1844
|
-
for (const scope of scopes) {
|
|
1845
|
-
await this.remove(scope);
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
1870
|
// src/index.ts
|
|
1851
1871
|
var logger14 = createModuleLogger("index");
|
|
1852
1872
|
async function startDiscordGateway(config) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AgentService } from "./agent-service";
|
|
3
3
|
import type { PromptQueue } from "./prompt-queue";
|
|
4
|
+
import type { SessionScope } from "./session-registry";
|
|
4
5
|
export type CommandResult = {
|
|
5
6
|
handled: boolean;
|
|
6
7
|
response?: string;
|
|
@@ -8,11 +9,15 @@ export type CommandResult = {
|
|
|
8
9
|
archive?: boolean;
|
|
9
10
|
/** When set, update the session's working reaction emoji. */
|
|
10
11
|
workingEmoji?: string;
|
|
12
|
+
/** When set, the command created a new session that should replace the current one. */
|
|
13
|
+
newSession?: AgentSession;
|
|
11
14
|
};
|
|
12
15
|
export type CommandContext = {
|
|
13
16
|
agentService: AgentService;
|
|
14
17
|
promptQueue: PromptQueue;
|
|
15
18
|
session?: AgentSession;
|
|
19
|
+
/** Session scope ("dm" or "thread:<id>") for session lifecycle commands. */
|
|
20
|
+
scope: SessionScope;
|
|
16
21
|
/** Current working reaction emoji for this session. */
|
|
17
22
|
workingEmoji: string;
|
|
18
23
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { Client } from "discord.js";
|
|
2
|
-
|
|
2
|
+
/** Context object passed to promptTransform so consumers can optionally
|
|
3
|
+
* wrap the raw content with Discord metadata. */
|
|
4
|
+
export type PromptTransformContext = {
|
|
5
|
+
/** The raw user content without any Discord context wrapping. */
|
|
6
|
+
rawContent: string;
|
|
7
|
+
/** Returns the message timestamp formatted using the gateway config's
|
|
8
|
+
* promptTimeZone and promptLocale. */
|
|
9
|
+
now: () => string;
|
|
10
|
+
/** Wraps the raw content with Discord message metadata (scope, author,
|
|
11
|
+
* timestamps, thread info). Returns the traditional wrapped prompt. */
|
|
12
|
+
wrapWithDiscordContext: () => string;
|
|
13
|
+
};
|
|
14
|
+
export type PromptTransform = (ctx: PromptTransformContext) => string | Promise<string>;
|
|
3
15
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
4
16
|
export type DiscordGatewayConfig = {
|
|
5
17
|
discordBotToken: string;
|