@friendlyrobot/discord-pi-agent 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +346 -345
- package/dist/prompt-context.d.ts +2 -13
- package/dist/session-commands.d.ts +5 -0
- package/dist/types.d.ts +14 -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
|
-
export {
|
|
2
|
+
export { formatDiscordPromptTime } 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,12 @@ 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
|
+
discordMetadata: "",
|
|
512
|
+
now: () => "",
|
|
513
|
+
userMessage: () => text
|
|
514
|
+
});
|
|
510
515
|
return runAgentTurn(session, transformedPrompt);
|
|
511
516
|
}
|
|
512
517
|
async compact() {
|
|
@@ -603,7 +608,7 @@ function resolveConfig(config) {
|
|
|
603
608
|
thinkingLevel: parseThinkingLevel(config.thinkingLevel) || "medium",
|
|
604
609
|
promptTimeZone: config.promptTimeZone?.trim() || "UTC",
|
|
605
610
|
promptLocale: config.promptLocale?.trim() || "en-AU",
|
|
606
|
-
promptTransform: config.promptTransform ||
|
|
611
|
+
promptTransform: config.promptTransform || defaultPromptTransform,
|
|
607
612
|
startupMessage: config.startupMessage === undefined ? "Bot is online and ready." : config.startupMessage,
|
|
608
613
|
shutdownOnSignals: config.shutdownOnSignals ?? true,
|
|
609
614
|
visionModelId: config.visionModelId?.trim() || null,
|
|
@@ -669,8 +674,9 @@ function parseThinkingLevel(value) {
|
|
|
669
674
|
}
|
|
670
675
|
return;
|
|
671
676
|
}
|
|
672
|
-
function
|
|
673
|
-
return
|
|
677
|
+
function defaultPromptTransform(ctx) {
|
|
678
|
+
return [ctx.now(), ctx.discordMetadata, "", ctx.userMessage()].filter(Boolean).join(`
|
|
679
|
+
`);
|
|
674
680
|
}
|
|
675
681
|
function parseStringArrayFromEnv(key) {
|
|
676
682
|
const value = process.env[key];
|
|
@@ -683,6 +689,245 @@ function parseStringArrayFromEnv(key) {
|
|
|
683
689
|
// src/discord-gateway-client.ts
|
|
684
690
|
import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
685
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);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
this.runNextTask();
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
getSnapshot() {
|
|
712
|
+
return {
|
|
713
|
+
pending: this.queue.length,
|
|
714
|
+
busy: this.running
|
|
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
|
+
|
|
686
931
|
// src/session-commands.ts
|
|
687
932
|
function getSessionStatusText(session, promptQueue, extras) {
|
|
688
933
|
const model = session.model ? `${session.model.provider}/${session.model.id}` : "(no model selected)";
|
|
@@ -886,22 +1131,23 @@ async function handleResetSessionCommand(trimmedInput, context) {
|
|
|
886
1131
|
if (trimmedInput !== "!reset-session") {
|
|
887
1132
|
return null;
|
|
888
1133
|
}
|
|
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
|
-
};
|
|
1134
|
+
const effectiveSession = requireEffectiveSession(context);
|
|
1135
|
+
if ("handled" in effectiveSession) {
|
|
1136
|
+
return effectiveSession;
|
|
899
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
|
+
});
|
|
900
1147
|
return {
|
|
901
1148
|
handled: true,
|
|
902
|
-
response
|
|
903
|
-
|
|
904
|
-
})
|
|
1149
|
+
response,
|
|
1150
|
+
newSession
|
|
905
1151
|
};
|
|
906
1152
|
}
|
|
907
1153
|
async function handleReactionCommand(trimmedInput, _context) {
|
|
@@ -959,7 +1205,7 @@ async function executeSessionCommand(input, context) {
|
|
|
959
1205
|
}
|
|
960
1206
|
|
|
961
1207
|
// src/discord-attachments.ts
|
|
962
|
-
var
|
|
1208
|
+
var logger8 = createModuleLogger("discord-attachments");
|
|
963
1209
|
var TEXT_ATTACHMENT_EXTENSIONS = [
|
|
964
1210
|
".txt",
|
|
965
1211
|
".md",
|
|
@@ -1021,11 +1267,11 @@ async function readTextAttachments(message) {
|
|
|
1021
1267
|
const results = [];
|
|
1022
1268
|
for (const [, attachment] of attachments) {
|
|
1023
1269
|
if (!isSupportedTextAttachment(attachment)) {
|
|
1024
|
-
|
|
1270
|
+
logger8.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
|
|
1025
1271
|
continue;
|
|
1026
1272
|
}
|
|
1027
1273
|
if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
|
|
1028
|
-
|
|
1274
|
+
logger8.warn({
|
|
1029
1275
|
messageId: message.id,
|
|
1030
1276
|
filename: attachment.name,
|
|
1031
1277
|
size: attachment.size
|
|
@@ -1033,14 +1279,14 @@ async function readTextAttachments(message) {
|
|
|
1033
1279
|
continue;
|
|
1034
1280
|
}
|
|
1035
1281
|
try {
|
|
1036
|
-
|
|
1282
|
+
logger8.info({
|
|
1037
1283
|
messageId: message.id,
|
|
1038
1284
|
filename: attachment.name,
|
|
1039
1285
|
size: attachment.size
|
|
1040
1286
|
}, "fetching attachment");
|
|
1041
1287
|
const response = await fetch(attachment.url);
|
|
1042
1288
|
if (!response.ok) {
|
|
1043
|
-
|
|
1289
|
+
logger8.warn({
|
|
1044
1290
|
messageId: message.id,
|
|
1045
1291
|
filename: attachment.name,
|
|
1046
1292
|
status: response.status
|
|
@@ -1050,7 +1296,7 @@ async function readTextAttachments(message) {
|
|
|
1050
1296
|
const content = await response.text();
|
|
1051
1297
|
results.push({ filename: attachment.name, content });
|
|
1052
1298
|
} catch (error) {
|
|
1053
|
-
|
|
1299
|
+
logger8.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
|
|
1054
1300
|
}
|
|
1055
1301
|
}
|
|
1056
1302
|
return results;
|
|
@@ -1066,7 +1312,7 @@ async function readMediaAttachments(message) {
|
|
|
1066
1312
|
continue;
|
|
1067
1313
|
}
|
|
1068
1314
|
if (attachment.size > MAX_MEDIA_ATTACHMENT_SIZE_BYTES) {
|
|
1069
|
-
|
|
1315
|
+
logger8.warn({
|
|
1070
1316
|
messageId: message.id,
|
|
1071
1317
|
filename: attachment.name,
|
|
1072
1318
|
size: attachment.size
|
|
@@ -1074,14 +1320,14 @@ async function readMediaAttachments(message) {
|
|
|
1074
1320
|
continue;
|
|
1075
1321
|
}
|
|
1076
1322
|
try {
|
|
1077
|
-
|
|
1323
|
+
logger8.info({
|
|
1078
1324
|
messageId: message.id,
|
|
1079
1325
|
filename: attachment.name,
|
|
1080
1326
|
size: attachment.size
|
|
1081
1327
|
}, "fetching media attachment");
|
|
1082
1328
|
const response = await fetch(attachment.url);
|
|
1083
1329
|
if (!response.ok) {
|
|
1084
|
-
|
|
1330
|
+
logger8.warn({
|
|
1085
1331
|
messageId: message.id,
|
|
1086
1332
|
filename: attachment.name,
|
|
1087
1333
|
status: response.status
|
|
@@ -1095,7 +1341,7 @@ async function readMediaAttachments(message) {
|
|
|
1095
1341
|
mimeType: attachment.contentType ?? "application/octet-stream"
|
|
1096
1342
|
});
|
|
1097
1343
|
} catch (error) {
|
|
1098
|
-
|
|
1344
|
+
logger8.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
|
|
1099
1345
|
}
|
|
1100
1346
|
}
|
|
1101
1347
|
return results;
|
|
@@ -1133,140 +1379,8 @@ function isAuthorizedMessage(message, scope, accessConfig) {
|
|
|
1133
1379
|
return false;
|
|
1134
1380
|
}
|
|
1135
1381
|
|
|
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
1382
|
// src/media-description.ts
|
|
1269
|
-
var
|
|
1383
|
+
var logger9 = createModuleLogger("media-description");
|
|
1270
1384
|
async function describeMediaAttachment(agentService, imageData, mimeType, userText, visionModel) {
|
|
1271
1385
|
const session = await agentService.createTemporarySession();
|
|
1272
1386
|
await session.setModel(visionModel);
|
|
@@ -1287,7 +1401,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
|
|
|
1287
1401
|
await session.prompt(promptText, { images: [imageContent] });
|
|
1288
1402
|
text = extractLastAssistantText(session);
|
|
1289
1403
|
} catch (error) {
|
|
1290
|
-
|
|
1404
|
+
logger9.error({ error, mimeType }, "vision model prompt failed");
|
|
1291
1405
|
text = "(Vision model failed to process the file.)";
|
|
1292
1406
|
} finally {
|
|
1293
1407
|
session.dispose();
|
|
@@ -1295,7 +1409,7 @@ async function describeMediaAttachment(agentService, imageData, mimeType, userTe
|
|
|
1295
1409
|
if (!text) {
|
|
1296
1410
|
return "(Vision model returned no description.)";
|
|
1297
1411
|
}
|
|
1298
|
-
|
|
1412
|
+
logger9.debug({ textLength: text.length, mimeType }, "media described");
|
|
1299
1413
|
return text;
|
|
1300
1414
|
}
|
|
1301
1415
|
function extractLastAssistantText(session) {
|
|
@@ -1331,7 +1445,7 @@ function isAssistantMessage(msg) {
|
|
|
1331
1445
|
}
|
|
1332
1446
|
|
|
1333
1447
|
// src/discord-media-resolution.ts
|
|
1334
|
-
var
|
|
1448
|
+
var logger10 = createModuleLogger("discord-media-resolution");
|
|
1335
1449
|
function parseProviderModelId(value) {
|
|
1336
1450
|
const trimmed = value.trim();
|
|
1337
1451
|
if (!trimmed) {
|
|
@@ -1365,7 +1479,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1365
1479
|
const modelSupportsVision = currentModel?.input.includes("image") ?? false;
|
|
1366
1480
|
if (modelSupportsVision) {
|
|
1367
1481
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1368
|
-
|
|
1482
|
+
logger10.info({
|
|
1369
1483
|
count: mediaAttachments.length,
|
|
1370
1484
|
filenames: names,
|
|
1371
1485
|
model: currentModel ? `${currentModel.provider}/${currentModel.id}` : "none"
|
|
@@ -1381,7 +1495,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1381
1495
|
}
|
|
1382
1496
|
if (!config.visionModelId) {
|
|
1383
1497
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1384
|
-
|
|
1498
|
+
logger10.info({ filenames: names }, "media attachments received but vision model not configured");
|
|
1385
1499
|
const note = `
|
|
1386
1500
|
|
|
1387
1501
|
[User sent media attachment(s): ${names}]
|
|
@@ -1397,7 +1511,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1397
1511
|
}
|
|
1398
1512
|
const visionModel = agentService.models.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
|
|
1399
1513
|
if (!visionModel) {
|
|
1400
|
-
|
|
1514
|
+
logger10.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
|
|
1401
1515
|
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1402
1516
|
const note = `
|
|
1403
1517
|
|
|
@@ -1408,7 +1522,7 @@ async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, curre
|
|
|
1408
1522
|
images: []
|
|
1409
1523
|
};
|
|
1410
1524
|
}
|
|
1411
|
-
|
|
1525
|
+
logger10.info({
|
|
1412
1526
|
count: mediaAttachments.length,
|
|
1413
1527
|
visionModel: `${visionModel.provider}/${visionModel.id}`
|
|
1414
1528
|
}, "describing media with vision model");
|
|
@@ -1435,7 +1549,7 @@ ${content}` : descriptionPrefix,
|
|
|
1435
1549
|
}
|
|
1436
1550
|
|
|
1437
1551
|
// src/discord-typing.ts
|
|
1438
|
-
var
|
|
1552
|
+
var logger11 = createModuleLogger("discord-typing");
|
|
1439
1553
|
var TYPING_INTERVAL_MS = 9000;
|
|
1440
1554
|
var typingIntervals = new Map;
|
|
1441
1555
|
async function sendTypingSafe(channel, channelKey) {
|
|
@@ -1458,18 +1572,18 @@ async function sendTypingSafe(channel, channelKey) {
|
|
|
1458
1572
|
retryMs = parsed.retry_after * 1000 + 500;
|
|
1459
1573
|
}
|
|
1460
1574
|
} catch {}
|
|
1461
|
-
|
|
1575
|
+
logger11.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
|
|
1462
1576
|
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1463
1577
|
await fetch(url, {
|
|
1464
1578
|
method: "POST",
|
|
1465
1579
|
headers: { Authorization: `Bot ${token}` }
|
|
1466
1580
|
});
|
|
1467
|
-
|
|
1581
|
+
logger11.warn({ channelKey }, "[TYPING] retry done");
|
|
1468
1582
|
return;
|
|
1469
1583
|
}
|
|
1470
|
-
|
|
1584
|
+
logger11.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
|
|
1471
1585
|
} catch (error) {
|
|
1472
|
-
|
|
1586
|
+
logger11.error({ channelKey, error }, "[TYPING] FAILED");
|
|
1473
1587
|
}
|
|
1474
1588
|
}
|
|
1475
1589
|
function startTypingForChannel(channel, channelKey) {
|
|
@@ -1478,7 +1592,7 @@ function startTypingForChannel(channel, channelKey) {
|
|
|
1478
1592
|
existing.refs += 1;
|
|
1479
1593
|
return;
|
|
1480
1594
|
}
|
|
1481
|
-
|
|
1595
|
+
logger11.debug("[TYPING] started new interval");
|
|
1482
1596
|
sendTypingSafe(channel, channelKey);
|
|
1483
1597
|
const interval = setInterval(() => {
|
|
1484
1598
|
sendTypingSafe(channel, channelKey);
|
|
@@ -1499,31 +1613,6 @@ function stopTypingForChannel(channelKey) {
|
|
|
1499
1613
|
}
|
|
1500
1614
|
|
|
1501
1615
|
// src/prompt-context.ts
|
|
1502
|
-
function buildDiscordMessageContextPrompt(userMessage, options) {
|
|
1503
|
-
const contextEntries = [
|
|
1504
|
-
["scope", options.scope],
|
|
1505
|
-
["sent_at", options.sentAt],
|
|
1506
|
-
["sent_at_local", options.sentAtLocal],
|
|
1507
|
-
["message_id", options.messageId],
|
|
1508
|
-
["author_name", normalizeContextValue(options.authorName)],
|
|
1509
|
-
["author_id", options.authorId],
|
|
1510
|
-
["thread_title", normalizeContextValue(options.threadTitle)],
|
|
1511
|
-
["thread_id", options.threadId],
|
|
1512
|
-
["forum_channel_id", options.forumChannelId ?? undefined]
|
|
1513
|
-
].filter((entry) => {
|
|
1514
|
-
return typeof entry[1] === "string" && entry[1].trim().length > 0;
|
|
1515
|
-
});
|
|
1516
|
-
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
1517
|
-
return [
|
|
1518
|
-
"<discord_message_context>",
|
|
1519
|
-
contextJson,
|
|
1520
|
-
"</discord_message_context>",
|
|
1521
|
-
"",
|
|
1522
|
-
"User message:",
|
|
1523
|
-
userMessage.trim()
|
|
1524
|
-
].join(`
|
|
1525
|
-
`);
|
|
1526
|
-
}
|
|
1527
1616
|
function formatDiscordPromptTime(date, options = {}) {
|
|
1528
1617
|
const timeZone = options.timeZone || "UTC";
|
|
1529
1618
|
const locale = options.locale || "en-AU";
|
|
@@ -1539,31 +1628,38 @@ function formatDiscordPromptTime(date, options = {}) {
|
|
|
1539
1628
|
timeZoneName: "short"
|
|
1540
1629
|
}).format(date);
|
|
1541
1630
|
}
|
|
1542
|
-
function
|
|
1543
|
-
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
return value.replace(/\s+/g, " ").trim();
|
|
1631
|
+
function wrapXmlTag(tag, content) {
|
|
1632
|
+
return `<${tag}>${content}</${tag}>`;
|
|
1547
1633
|
}
|
|
1548
1634
|
|
|
1549
1635
|
// src/discord-message-handler.ts
|
|
1550
|
-
var
|
|
1551
|
-
function
|
|
1636
|
+
var logger12 = createModuleLogger("discord-message-handler");
|
|
1637
|
+
function formatDiscordMessageMetadata(message, scope) {
|
|
1552
1638
|
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
1553
|
-
|
|
1554
|
-
scope
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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;
|
|
1566
1660
|
});
|
|
1661
|
+
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
1662
|
+
return `<discord_message_context>${contextJson}</discord_message_context>`;
|
|
1567
1663
|
}
|
|
1568
1664
|
async function handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig) {
|
|
1569
1665
|
if (message.author.bot) {
|
|
@@ -1574,14 +1670,14 @@ async function handleDiscordMessage(message, config, agentService, sessionRegist
|
|
|
1574
1670
|
}
|
|
1575
1671
|
const scope = resolveMessageScope(message);
|
|
1576
1672
|
if (scope === null) {
|
|
1577
|
-
|
|
1673
|
+
logger12.debug({
|
|
1578
1674
|
messageId: message.id,
|
|
1579
1675
|
channelType: message.channel.type
|
|
1580
1676
|
}, "unsupported channel type, ignoring");
|
|
1581
1677
|
return;
|
|
1582
1678
|
}
|
|
1583
1679
|
if (!isAuthorizedMessage(message, scope, accessConfig)) {
|
|
1584
|
-
|
|
1680
|
+
logger12.debug({
|
|
1585
1681
|
messageId: message.id,
|
|
1586
1682
|
authorId: message.author.id,
|
|
1587
1683
|
scope
|
|
@@ -1601,10 +1697,10 @@ ${attachment.content}`;
|
|
|
1601
1697
|
}
|
|
1602
1698
|
const mediaAttachments = await readMediaAttachments(message);
|
|
1603
1699
|
if (!content && mediaAttachments.length === 0) {
|
|
1604
|
-
|
|
1700
|
+
logger12.debug({ messageId: message.id }, "ignored empty message (no text or images)");
|
|
1605
1701
|
return;
|
|
1606
1702
|
}
|
|
1607
|
-
|
|
1703
|
+
logger12.info({
|
|
1608
1704
|
scope,
|
|
1609
1705
|
content
|
|
1610
1706
|
}, "message received");
|
|
@@ -1615,7 +1711,7 @@ ${attachment.content}`;
|
|
|
1615
1711
|
const { entry, created } = await sessionRegistry.getOrCreate(scope);
|
|
1616
1712
|
const { session, promptQueue } = entry;
|
|
1617
1713
|
if (created && scope.startsWith("thread:") && message.channel.isThread()) {
|
|
1618
|
-
|
|
1714
|
+
logger12.info({
|
|
1619
1715
|
scope,
|
|
1620
1716
|
threadName: message.channel.name
|
|
1621
1717
|
}, "new thread session");
|
|
@@ -1624,16 +1720,21 @@ ${attachment.content}`;
|
|
|
1624
1720
|
agentService,
|
|
1625
1721
|
promptQueue,
|
|
1626
1722
|
session,
|
|
1723
|
+
scope,
|
|
1627
1724
|
workingEmoji: entry.workingEmoji
|
|
1628
1725
|
});
|
|
1629
1726
|
if (commandResult.handled) {
|
|
1630
1727
|
stopTypingForChannel(channelKey);
|
|
1631
1728
|
if (commandResult.workingEmoji) {
|
|
1632
1729
|
entry.workingEmoji = commandResult.workingEmoji;
|
|
1633
|
-
|
|
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");
|
|
1634
1735
|
}
|
|
1635
1736
|
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
1636
|
-
|
|
1737
|
+
logger12.info({ scope }, "archiving thread");
|
|
1637
1738
|
const archiveChannel = message.channel;
|
|
1638
1739
|
if (archiveChannel.isSendable()) {
|
|
1639
1740
|
await archiveChannel.send(`\`\`\`
|
|
@@ -1645,12 +1746,12 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1645
1746
|
await archiveChannel.setArchived(true);
|
|
1646
1747
|
}
|
|
1647
1748
|
} catch (error) {
|
|
1648
|
-
|
|
1749
|
+
logger12.error({ error }, "failed to archive thread");
|
|
1649
1750
|
}
|
|
1650
1751
|
await sessionRegistry.remove(scope);
|
|
1651
1752
|
return;
|
|
1652
1753
|
}
|
|
1653
|
-
|
|
1754
|
+
logger12.info({
|
|
1654
1755
|
messageId: message.id,
|
|
1655
1756
|
command: content,
|
|
1656
1757
|
hasResponse: Boolean(commandResult.response)
|
|
@@ -1662,7 +1763,7 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1662
1763
|
}
|
|
1663
1764
|
if (!message.channel.isSendable()) {
|
|
1664
1765
|
stopTypingForChannel(channelKey);
|
|
1665
|
-
|
|
1766
|
+
logger12.debug({ messageId: message.id }, "channel not sendable");
|
|
1666
1767
|
return;
|
|
1667
1768
|
}
|
|
1668
1769
|
await addWorkingReaction(message, entry.workingEmoji);
|
|
@@ -1682,8 +1783,16 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1682
1783
|
promptImages = resolvedPromptMedia.images;
|
|
1683
1784
|
}
|
|
1684
1785
|
}
|
|
1685
|
-
const
|
|
1686
|
-
const transformedPrompt = await config.promptTransform(
|
|
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
|
+
});
|
|
1687
1796
|
return runAgentTurn(session, transformedPrompt, {
|
|
1688
1797
|
images: promptImages
|
|
1689
1798
|
});
|
|
@@ -1696,7 +1805,7 @@ ${commandResult.response ?? "Archiving..."}
|
|
|
1696
1805
|
}
|
|
1697
1806
|
|
|
1698
1807
|
// src/discord-gateway-client.ts
|
|
1699
|
-
var
|
|
1808
|
+
var logger13 = createModuleLogger("discord-gateway");
|
|
1700
1809
|
async function startGatewayClient(config, agentService, sessionRegistry, accessConfig) {
|
|
1701
1810
|
const client = new Client({
|
|
1702
1811
|
intents: [
|
|
@@ -1708,7 +1817,7 @@ async function startGatewayClient(config, agentService, sessionRegistry, accessC
|
|
|
1708
1817
|
partials: [Partials.Channel]
|
|
1709
1818
|
});
|
|
1710
1819
|
client.once(Events.ClientReady, async (readyClient) => {
|
|
1711
|
-
|
|
1820
|
+
logger13.info({ userTag: readyClient.user.tag }, "logged in");
|
|
1712
1821
|
if (!accessConfig.startupMessage) {
|
|
1713
1822
|
return;
|
|
1714
1823
|
}
|
|
@@ -1716,137 +1825,30 @@ async function startGatewayClient(config, agentService, sessionRegistry, accessC
|
|
|
1716
1825
|
const user = await readyClient.users.fetch(accessConfig.discordAllowedUserId);
|
|
1717
1826
|
const dmChannel = await user.createDM();
|
|
1718
1827
|
await dmChannel.send(accessConfig.startupMessage);
|
|
1719
|
-
|
|
1828
|
+
logger13.info({
|
|
1720
1829
|
userId: accessConfig.discordAllowedUserId
|
|
1721
1830
|
}, "sent startup dm");
|
|
1722
1831
|
} catch (error) {
|
|
1723
|
-
|
|
1832
|
+
logger13.error({ error }, "failed to send startup dm");
|
|
1724
1833
|
}
|
|
1725
1834
|
});
|
|
1726
1835
|
client.on(Events.MessageCreate, async (message) => {
|
|
1727
1836
|
try {
|
|
1728
1837
|
await handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig);
|
|
1729
1838
|
} catch (error) {
|
|
1730
|
-
|
|
1839
|
+
logger13.error({ error, direction: "IN" }, "message handling failed");
|
|
1731
1840
|
await sendReply(message, "The bot hit an error while handling that message.");
|
|
1732
1841
|
}
|
|
1733
1842
|
});
|
|
1734
1843
|
client.on(Events.ThreadDelete, async (thread) => {
|
|
1735
1844
|
const scope = `thread:${thread.id}`;
|
|
1736
|
-
|
|
1845
|
+
logger13.info({ threadId: thread.id, scope }, "thread deleted");
|
|
1737
1846
|
await sessionRegistry.remove(scope);
|
|
1738
1847
|
});
|
|
1739
1848
|
await client.login(config.discordBotToken);
|
|
1740
1849
|
return client;
|
|
1741
1850
|
}
|
|
1742
1851
|
|
|
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
1852
|
// src/index.ts
|
|
1851
1853
|
var logger14 = createModuleLogger("index");
|
|
1852
1854
|
async function startDiscordGateway(config) {
|
|
@@ -1910,6 +1912,5 @@ export {
|
|
|
1910
1912
|
startDiscordGateway,
|
|
1911
1913
|
resolveConfig,
|
|
1912
1914
|
loadDiscordGatewayConfigFromEnv,
|
|
1913
|
-
formatDiscordPromptTime
|
|
1914
|
-
buildDiscordMessageContextPrompt
|
|
1915
|
+
formatDiscordPromptTime
|
|
1915
1916
|
};
|
package/dist/prompt-context.d.ts
CHANGED
|
@@ -2,17 +2,6 @@ export type DiscordPromptTimeFormatOptions = {
|
|
|
2
2
|
timeZone?: string;
|
|
3
3
|
locale?: string;
|
|
4
4
|
};
|
|
5
|
-
export type DiscordPromptScope = "dm" | "thread";
|
|
6
|
-
export type DiscordMessageContextPromptOptions = {
|
|
7
|
-
scope: DiscordPromptScope;
|
|
8
|
-
messageId: string;
|
|
9
|
-
authorId: string;
|
|
10
|
-
sentAt?: string;
|
|
11
|
-
sentAtLocal?: string;
|
|
12
|
-
authorName?: string;
|
|
13
|
-
threadId?: string;
|
|
14
|
-
threadTitle?: string;
|
|
15
|
-
forumChannelId?: string | null;
|
|
16
|
-
};
|
|
17
|
-
export declare function buildDiscordMessageContextPrompt(userMessage: string, options: DiscordMessageContextPromptOptions): string;
|
|
18
5
|
export declare function formatDiscordPromptTime(date: Date, options?: DiscordPromptTimeFormatOptions): string;
|
|
6
|
+
/** Wrap content in an XML-style tag: `<tag>content</tag>`. */
|
|
7
|
+
export declare function wrapXmlTag(tag: string, content: string): string;
|
|
@@ -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,18 @@
|
|
|
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 XML wrapping. */
|
|
6
|
+
rawContent: string;
|
|
7
|
+
/** XML string with Discord message metadata (context JSON block). */
|
|
8
|
+
discordMetadata: string;
|
|
9
|
+
/** Returns current datetime formatted using the gateway config's
|
|
10
|
+
* promptTimeZone and promptLocale, wrapped in a <datetime> tag. */
|
|
11
|
+
now: () => string;
|
|
12
|
+
/** Returns rawContent wrapped in a <user_message> tag. */
|
|
13
|
+
userMessage: () => string;
|
|
14
|
+
};
|
|
15
|
+
export type PromptTransform = (ctx: PromptTransformContext) => string | Promise<string>;
|
|
3
16
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
4
17
|
export type DiscordGatewayConfig = {
|
|
5
18
|
discordBotToken: string;
|