@friendlyrobot/discord-pi-agent 0.21.3 → 0.22.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/dist/agent-turn-runner.d.ts +7 -0
- package/dist/agent-turn-runner.js +11 -2
- package/dist/discord-auth.d.ts +3 -2
- package/dist/discord-message-handler.js +181 -136
- package/dist/discord-replies.d.ts +2 -0
- package/dist/discord-replies.js +10 -4
- package/dist/prompt-context.d.ts +3 -0
- package/dist/prompt-context.js +29 -0
- package/dist/session-registry.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
3
|
+
type ToolLifecycleEvent = {
|
|
4
|
+
toolName: string;
|
|
5
|
+
toolCallId: string;
|
|
6
|
+
isError?: boolean;
|
|
7
|
+
};
|
|
3
8
|
type CollectReplyOptions = {
|
|
4
9
|
images?: ImageContent[];
|
|
10
|
+
onToolStart?: (event: ToolLifecycleEvent) => void | Promise<void>;
|
|
11
|
+
onToolEnd?: (event: ToolLifecycleEvent) => void | Promise<void>;
|
|
5
12
|
};
|
|
6
13
|
export declare function runAgentTurn(session: AgentSession, prompt: string, options?: CollectReplyOptions): Promise<string>;
|
|
7
14
|
export {};
|
|
@@ -33,6 +33,10 @@ export async function runAgentTurn(session, prompt, options = {}) {
|
|
|
33
33
|
toolCount += 1;
|
|
34
34
|
const input = event.toolName === "bash" ? event.args.command : event.args;
|
|
35
35
|
toolInputsByCallId.set(event.toolCallId, input);
|
|
36
|
+
void options.onToolStart?.({
|
|
37
|
+
toolName: event.toolName,
|
|
38
|
+
toolCallId: event.toolCallId,
|
|
39
|
+
});
|
|
36
40
|
if (event.toolName === "bash") {
|
|
37
41
|
debugPrint(input, "CMD");
|
|
38
42
|
// logger.debug(
|
|
@@ -52,6 +56,11 @@ export async function runAgentTurn(session, prompt, options = {}) {
|
|
|
52
56
|
if (event.type === "tool_execution_end") {
|
|
53
57
|
const input = toolInputsByCallId.get(event.toolCallId);
|
|
54
58
|
toolInputsByCallId.delete(event.toolCallId);
|
|
59
|
+
void options.onToolEnd?.({
|
|
60
|
+
toolName: event.toolName,
|
|
61
|
+
toolCallId: event.toolCallId,
|
|
62
|
+
isError: event.isError,
|
|
63
|
+
});
|
|
55
64
|
if (event.toolName === "bash") {
|
|
56
65
|
debugPrint(extractToolOutput(event.result), event.isError ? "BASH TOOL ERROR OUTPUT" : "BASH TOOL OUTPUT");
|
|
57
66
|
// logger.debug(
|
|
@@ -138,10 +147,10 @@ function getLatestAssistantText(messages) {
|
|
|
138
147
|
}
|
|
139
148
|
return latestAssistantMessage.content
|
|
140
149
|
.filter((item) => {
|
|
141
|
-
return item.type === "text";
|
|
150
|
+
return item.type === "text" && typeof item.text === "string";
|
|
142
151
|
})
|
|
143
152
|
.map((item) => {
|
|
144
|
-
return item.text
|
|
153
|
+
return item.text;
|
|
145
154
|
})
|
|
146
155
|
.join("\n")
|
|
147
156
|
.trim();
|
package/dist/discord-auth.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type Message } from "discord.js";
|
|
2
|
+
import type { SessionScope } from "./session-registry";
|
|
2
3
|
import type { GatewayAccessConfig } from "./types";
|
|
3
4
|
export declare function getAuthorDisplayName(message: Message): string;
|
|
4
5
|
/**
|
|
5
6
|
* Determine the session scope from an incoming message.
|
|
6
7
|
* Returns null for unsupported channel types (silently ignored).
|
|
7
8
|
*/
|
|
8
|
-
export declare function resolveMessageScope(message: Message):
|
|
9
|
-
export declare function isAuthorizedMessage(message: Message, scope:
|
|
9
|
+
export declare function resolveMessageScope(message: Message): SessionScope | null;
|
|
10
|
+
export declare function isAuthorizedMessage(message: Message, scope: SessionScope, accessConfig: GatewayAccessConfig): boolean;
|
|
@@ -1,59 +1,57 @@
|
|
|
1
|
-
import { executeSessionCommand } from "./session-commands";
|
|
2
1
|
import { readMediaAttachments, readTextAttachments, } from "./discord-attachments";
|
|
3
|
-
import {
|
|
4
|
-
import { addWorkingReaction, removeWorkingReaction, sendCommandReply, sendReply, } from "./discord-replies";
|
|
2
|
+
import { isAuthorizedMessage, resolveMessageScope } from "./discord-auth";
|
|
5
3
|
import { resolveMediaAttachmentsForPrompt } from "./discord-media-resolution";
|
|
4
|
+
import { addReaction, addWorkingReaction, removeReaction, removeWorkingReaction, sendCommandReply, sendReply, } from "./discord-replies";
|
|
5
|
+
import { executeSessionCommand } from "./session-commands";
|
|
6
6
|
import { startTypingForChannel, stopTypingForChannel } from "./discord-typing";
|
|
7
7
|
import { createModuleLogger } from "./logger";
|
|
8
|
-
import { formatDiscordPromptTime, wrapXmlTag } from "./prompt-context";
|
|
8
|
+
import { buildDiscordMessageMetadata, formatDiscordPromptTime, wrapXmlTag, } from "./prompt-context";
|
|
9
9
|
import { runAgentTurn } from "./agent-turn-runner";
|
|
10
10
|
const logger = createModuleLogger("discord-message-handler");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
["message_id", message.id],
|
|
19
|
-
[
|
|
20
|
-
"author_name",
|
|
21
|
-
getAuthorDisplayName(message).replace(/\s+/g, " ").trim() || undefined,
|
|
22
|
-
],
|
|
23
|
-
["author_id", message.author.id],
|
|
24
|
-
[
|
|
25
|
-
"thread_title",
|
|
26
|
-
isThread
|
|
27
|
-
? (message.channel.name ?? "").replace(/\s+/g, " ").trim()
|
|
28
|
-
: undefined,
|
|
29
|
-
],
|
|
30
|
-
["thread_id", isThread ? message.channel.id : undefined],
|
|
31
|
-
[
|
|
32
|
-
"forum_channel_id",
|
|
33
|
-
isThread ? (message.channel.parentId ?? undefined) : undefined,
|
|
34
|
-
],
|
|
35
|
-
].filter((entry) => {
|
|
36
|
-
return typeof entry[1] === "string" && entry[1].length > 0;
|
|
37
|
-
});
|
|
38
|
-
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
39
|
-
return `<discord_message_context>${contextJson}</discord_message_context>`;
|
|
40
|
-
}
|
|
11
|
+
const TOOL_REACTION_EMOJIS = {
|
|
12
|
+
bash: "🖥️",
|
|
13
|
+
edit: "✏️",
|
|
14
|
+
read: "👀",
|
|
15
|
+
write: "📝",
|
|
16
|
+
};
|
|
17
|
+
const DEFAULT_TOOL_REACTION_EMOJI = "🔧";
|
|
41
18
|
export async function handleDiscordMessage(message, config, agentService, sessionRegistry, accessConfig) {
|
|
42
|
-
|
|
43
|
-
|
|
19
|
+
const preparedMessage = await prepareDiscordMessage(message, accessConfig);
|
|
20
|
+
if (!preparedMessage) {
|
|
44
21
|
return;
|
|
45
22
|
}
|
|
46
|
-
|
|
47
|
-
|
|
23
|
+
logger.info({
|
|
24
|
+
scope: preparedMessage.scope,
|
|
25
|
+
content: preparedMessage.content,
|
|
26
|
+
}, "message received");
|
|
27
|
+
const channelKey = message.channel.id;
|
|
28
|
+
startTypingIfPossible(message, channelKey);
|
|
29
|
+
const { entry, created } = await sessionRegistry.getOrCreate(preparedMessage.scope);
|
|
30
|
+
logNewThreadSession(message, preparedMessage.scope, created);
|
|
31
|
+
const commandResult = await executeSessionCommand(preparedMessage.content, {
|
|
32
|
+
agentService,
|
|
33
|
+
promptQueue: entry.promptQueue,
|
|
34
|
+
session: entry.session,
|
|
35
|
+
scope: preparedMessage.scope,
|
|
36
|
+
workingEmoji: entry.workingEmoji,
|
|
37
|
+
});
|
|
38
|
+
if (commandResult.handled) {
|
|
39
|
+
await handleCommandResult(message, sessionRegistry, preparedMessage.scope, entry, commandResult, preparedMessage.content, channelKey);
|
|
48
40
|
return;
|
|
49
41
|
}
|
|
42
|
+
await processAgentPrompt(message, config, agentService, entry, preparedMessage, channelKey);
|
|
43
|
+
}
|
|
44
|
+
async function prepareDiscordMessage(message, accessConfig) {
|
|
45
|
+
if (message.author.bot || message.system) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
50
48
|
const scope = resolveMessageScope(message);
|
|
51
49
|
if (scope === null) {
|
|
52
50
|
logger.debug({
|
|
53
51
|
messageId: message.id,
|
|
54
52
|
channelType: message.channel.type,
|
|
55
53
|
}, "unsupported channel type, ignoring");
|
|
56
|
-
return;
|
|
54
|
+
return null;
|
|
57
55
|
}
|
|
58
56
|
if (!isAuthorizedMessage(message, scope, accessConfig)) {
|
|
59
57
|
logger.debug({
|
|
@@ -61,129 +59,176 @@ export async function handleDiscordMessage(message, config, agentService, sessio
|
|
|
61
59
|
authorId: message.author.id,
|
|
62
60
|
scope,
|
|
63
61
|
}, "unauthorized");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
let content = message.content.trim();
|
|
67
|
-
const textAttachments = await readTextAttachments(message);
|
|
68
|
-
if (textAttachments.length > 0) {
|
|
69
|
-
const attachmentSuffix = textAttachments
|
|
70
|
-
.map((attachment) => {
|
|
71
|
-
return `\n\n--- Attachment: ${attachment.filename} ---\n${attachment.content}`;
|
|
72
|
-
})
|
|
73
|
-
.join("");
|
|
74
|
-
content = content
|
|
75
|
-
? content + attachmentSuffix
|
|
76
|
-
: textAttachments[0].content;
|
|
62
|
+
return null;
|
|
77
63
|
}
|
|
64
|
+
const content = await buildMessageContent(message);
|
|
78
65
|
const mediaAttachments = await readMediaAttachments(message);
|
|
79
66
|
if (!content && mediaAttachments.length === 0) {
|
|
80
67
|
logger.debug({ messageId: message.id }, "ignored empty message (no text or images)");
|
|
81
|
-
return;
|
|
68
|
+
return null;
|
|
82
69
|
}
|
|
83
|
-
|
|
84
|
-
// direction: "IN",
|
|
70
|
+
return {
|
|
85
71
|
scope,
|
|
86
|
-
// messageId: message.id,
|
|
87
|
-
// authorId: message.author.id,
|
|
88
|
-
// channelType: message.channel.type,
|
|
89
72
|
content,
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
mediaAttachments,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function buildMessageContent(message) {
|
|
77
|
+
const baseContent = message.content.trim();
|
|
78
|
+
const textAttachments = await readTextAttachments(message);
|
|
79
|
+
if (textAttachments.length === 0) {
|
|
80
|
+
return baseContent;
|
|
81
|
+
}
|
|
82
|
+
const attachmentText = textAttachments
|
|
83
|
+
.map((attachment) => {
|
|
84
|
+
return `\n\n--- Attachment: ${attachment.filename} ---\n${attachment.content}`;
|
|
85
|
+
})
|
|
86
|
+
.join("");
|
|
87
|
+
if (!baseContent) {
|
|
88
|
+
return textAttachments[0]?.content ?? "";
|
|
89
|
+
}
|
|
90
|
+
return baseContent + attachmentText;
|
|
91
|
+
}
|
|
92
|
+
function startTypingIfPossible(message, channelKey) {
|
|
92
93
|
if (message.channel.isSendable()) {
|
|
93
94
|
startTypingForChannel(message.channel, channelKey);
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (created
|
|
98
|
-
|
|
99
|
-
scope,
|
|
100
|
-
threadName: message.channel.name,
|
|
101
|
-
}, "new thread session");
|
|
96
|
+
}
|
|
97
|
+
function logNewThreadSession(message, scope, created) {
|
|
98
|
+
if (!created || !scope.startsWith("thread:") || !message.channel.isThread()) {
|
|
99
|
+
return;
|
|
102
100
|
}
|
|
103
|
-
|
|
104
|
-
agentService,
|
|
105
|
-
promptQueue,
|
|
106
|
-
session,
|
|
101
|
+
logger.info({
|
|
107
102
|
scope,
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (archiveChannel.isSendable()) {
|
|
124
|
-
await archiveChannel.send(`\`\`\`\n${commandResult.response ?? "Archiving..."}\n\`\`\``);
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
if (archiveChannel.isThread()) {
|
|
128
|
-
await archiveChannel.setArchived(true);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
logger.error({ error }, "failed to archive thread");
|
|
133
|
-
}
|
|
134
|
-
await sessionRegistry.remove(scope);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
logger.info({
|
|
138
|
-
messageId: message.id,
|
|
139
|
-
command: content,
|
|
140
|
-
hasResponse: Boolean(commandResult.response),
|
|
141
|
-
}, `command handled: ${content}`);
|
|
142
|
-
if (commandResult.response) {
|
|
143
|
-
await sendCommandReply(message, commandResult.response);
|
|
144
|
-
}
|
|
103
|
+
threadName: message.channel.name,
|
|
104
|
+
}, "new thread session");
|
|
105
|
+
}
|
|
106
|
+
async function handleCommandResult(message, sessionRegistry, scope, entry, commandResult, content, channelKey) {
|
|
107
|
+
stopTypingForChannel(channelKey);
|
|
108
|
+
if (commandResult.workingEmoji) {
|
|
109
|
+
entry.workingEmoji = commandResult.workingEmoji;
|
|
110
|
+
logger.info({ scope, emoji: commandResult.workingEmoji }, "working emoji updated");
|
|
111
|
+
}
|
|
112
|
+
if (commandResult.newSession) {
|
|
113
|
+
entry.session = commandResult.newSession;
|
|
114
|
+
logger.info({ scope, sessionId: commandResult.newSession.sessionId }, "session replaced");
|
|
115
|
+
}
|
|
116
|
+
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
117
|
+
await archiveThreadSession(message, sessionRegistry, scope, commandResult);
|
|
145
118
|
return;
|
|
146
119
|
}
|
|
120
|
+
logger.info({
|
|
121
|
+
messageId: message.id,
|
|
122
|
+
command: content,
|
|
123
|
+
hasResponse: Boolean(commandResult.response),
|
|
124
|
+
}, `command handled: ${content}`);
|
|
125
|
+
if (commandResult.response) {
|
|
126
|
+
await sendCommandReply(message, commandResult.response);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function archiveThreadSession(message, sessionRegistry, scope, commandResult) {
|
|
130
|
+
logger.info({ scope }, "archiving thread");
|
|
131
|
+
if (message.channel.isSendable()) {
|
|
132
|
+
await message.channel.send(`\`\`\`\n${commandResult.response ?? "Archiving..."}\n\`\`\``);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (message.channel.isThread()) {
|
|
136
|
+
await message.channel.setArchived(true);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error({ error }, "failed to archive thread");
|
|
141
|
+
}
|
|
142
|
+
await sessionRegistry.remove(scope);
|
|
143
|
+
}
|
|
144
|
+
function resolveToolReactionEmoji(toolName) {
|
|
145
|
+
return TOOL_REACTION_EMOJIS[toolName] ?? DEFAULT_TOOL_REACTION_EMOJI;
|
|
146
|
+
}
|
|
147
|
+
async function processAgentPrompt(message, config, agentService, entry, preparedMessage, channelKey) {
|
|
147
148
|
if (!message.channel.isSendable()) {
|
|
148
149
|
stopTypingForChannel(channelKey);
|
|
149
150
|
logger.debug({ messageId: message.id }, "channel not sendable");
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
153
|
await addWorkingReaction(message, entry.workingEmoji);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
let response;
|
|
154
|
+
await notifyIfPromptQueued(message, entry.promptQueue.getSnapshot().pending);
|
|
155
|
+
const toolEmojiByCallId = new Map();
|
|
156
|
+
const activeToolCountByEmoji = new Map();
|
|
158
157
|
try {
|
|
159
|
-
response = await promptQueue.enqueue(async () => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
158
|
+
const response = await entry.promptQueue.enqueue(async () => {
|
|
159
|
+
const promptInput = await buildPromptInput(message, config, agentService, entry, preparedMessage);
|
|
160
|
+
return runAgentTurn(entry.session, promptInput.prompt, {
|
|
161
|
+
images: promptInput.images,
|
|
162
|
+
onToolStart: async ({ toolName, toolCallId }) => {
|
|
163
|
+
const emoji = resolveToolReactionEmoji(toolName);
|
|
164
|
+
toolEmojiByCallId.set(toolCallId, emoji);
|
|
165
|
+
const activeCount = activeToolCountByEmoji.get(emoji) ?? 0;
|
|
166
|
+
activeToolCountByEmoji.set(emoji, activeCount + 1);
|
|
167
|
+
if (activeCount === 0) {
|
|
168
|
+
await addReaction(message, emoji);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
onToolEnd: async ({ toolCallId }) => {
|
|
172
|
+
const emoji = toolEmojiByCallId.get(toolCallId);
|
|
173
|
+
if (!emoji) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
toolEmojiByCallId.delete(toolCallId);
|
|
177
|
+
const activeCount = activeToolCountByEmoji.get(emoji) ?? 0;
|
|
178
|
+
if (activeCount <= 1) {
|
|
179
|
+
activeToolCountByEmoji.delete(emoji);
|
|
180
|
+
await removeReaction(message, emoji);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
activeToolCountByEmoji.set(emoji, activeCount - 1);
|
|
184
|
+
},
|
|
181
185
|
});
|
|
182
186
|
});
|
|
187
|
+
await sendReply(message, response);
|
|
183
188
|
}
|
|
184
189
|
finally {
|
|
185
190
|
stopTypingForChannel(channelKey);
|
|
191
|
+
const activeEmojis = Array.from(activeToolCountByEmoji.keys());
|
|
192
|
+
await Promise.all(activeEmojis.map(async (emoji) => {
|
|
193
|
+
await removeReaction(message, emoji);
|
|
194
|
+
}));
|
|
186
195
|
await removeWorkingReaction(message, entry.workingEmoji);
|
|
187
196
|
}
|
|
188
|
-
|
|
197
|
+
}
|
|
198
|
+
async function notifyIfPromptQueued(message, pendingCount) {
|
|
199
|
+
if (pendingCount > 0) {
|
|
200
|
+
await sendReply(message, `Queued. ${pendingCount} request(s) ahead of this one.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function buildPromptInput(message, config, agentService, entry, preparedMessage) {
|
|
204
|
+
const resolvedPromptMedia = await resolvePromptMedia(preparedMessage, entry, config, agentService);
|
|
205
|
+
const discordMetadata = buildDiscordMessageMetadata(message, preparedMessage.scope);
|
|
206
|
+
const prompt = await config.promptTransform({
|
|
207
|
+
rawContent: resolvedPromptMedia.content,
|
|
208
|
+
discordMetadata,
|
|
209
|
+
now: () => {
|
|
210
|
+
return wrapXmlTag("datetime", formatDiscordPromptTime(new Date(), {
|
|
211
|
+
timeZone: config.promptTimeZone,
|
|
212
|
+
locale: config.promptLocale,
|
|
213
|
+
}));
|
|
214
|
+
},
|
|
215
|
+
userMessage: () => {
|
|
216
|
+
return wrapXmlTag("user_message", resolvedPromptMedia.content);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
prompt,
|
|
221
|
+
images: resolvedPromptMedia.images.length > 0
|
|
222
|
+
? resolvedPromptMedia.images
|
|
223
|
+
: undefined,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function resolvePromptMedia(preparedMessage, entry, config, agentService) {
|
|
227
|
+
if (preparedMessage.mediaAttachments.length === 0) {
|
|
228
|
+
return {
|
|
229
|
+
content: preparedMessage.content,
|
|
230
|
+
images: [],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return resolveMediaAttachmentsForPrompt(preparedMessage.mediaAttachments, preparedMessage.content, entry.session.model, config, agentService);
|
|
189
234
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Message } from "discord.js";
|
|
2
2
|
export declare const DEFAULT_WORKING_EMOJI = "\u2699\uFE0F";
|
|
3
|
+
export declare function addReaction(message: Message, emoji: string): Promise<void>;
|
|
4
|
+
export declare function removeReaction(message: Message, emoji: string): Promise<void>;
|
|
3
5
|
export declare function addWorkingReaction(message: Message, emoji?: string): Promise<void>;
|
|
4
6
|
export declare function removeWorkingReaction(message: Message, emoji?: string): Promise<void>;
|
|
5
7
|
export declare function sendReply(message: Message, text: string): Promise<void>;
|
package/dist/discord-replies.js
CHANGED
|
@@ -35,15 +35,15 @@ function chunkByLines(text, maxSize) {
|
|
|
35
35
|
return chunks;
|
|
36
36
|
}
|
|
37
37
|
export const DEFAULT_WORKING_EMOJI = "⚙️";
|
|
38
|
-
export async function
|
|
38
|
+
export async function addReaction(message, emoji) {
|
|
39
39
|
try {
|
|
40
40
|
await message.react(emoji);
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
|
-
logger.debug({ messageId: message.id, error }, "failed to add
|
|
43
|
+
logger.debug({ messageId: message.id, emoji, error }, "failed to add reaction");
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
export async function
|
|
46
|
+
export async function removeReaction(message, emoji) {
|
|
47
47
|
try {
|
|
48
48
|
const reaction = message.reactions.cache.get(emoji);
|
|
49
49
|
if (reaction) {
|
|
@@ -51,9 +51,15 @@ export async function removeWorkingReaction(message, emoji = DEFAULT_WORKING_EMO
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
|
-
logger.debug({ messageId: message.id, error }, "failed to remove
|
|
54
|
+
logger.debug({ messageId: message.id, emoji, error }, "failed to remove reaction");
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
export async function addWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
58
|
+
await addReaction(message, emoji);
|
|
59
|
+
}
|
|
60
|
+
export async function removeWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
|
|
61
|
+
await removeReaction(message, emoji);
|
|
62
|
+
}
|
|
57
63
|
export async function sendReply(message, text) {
|
|
58
64
|
const channel = message.channel;
|
|
59
65
|
if (!channel.isSendable()) {
|
package/dist/prompt-context.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { Message } from "discord.js";
|
|
2
|
+
import type { SessionScope } from "./session-registry";
|
|
1
3
|
export type DiscordPromptTimeFormatOptions = {
|
|
2
4
|
timeZone?: string;
|
|
3
5
|
locale?: string;
|
|
4
6
|
};
|
|
5
7
|
export declare function formatDiscordPromptTime(date: Date, options?: DiscordPromptTimeFormatOptions): string;
|
|
8
|
+
export declare function buildDiscordMessageMetadata(message: Message, scope: SessionScope): string;
|
|
6
9
|
/** Wrap content in an XML-style tag: `<tag>content</tag>`. */
|
|
7
10
|
export declare function wrapXmlTag(tag: string, content: string): string;
|
package/dist/prompt-context.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAuthorDisplayName } from "./discord-auth";
|
|
1
2
|
export function formatDiscordPromptTime(date, options = {}) {
|
|
2
3
|
const timeZone = options.timeZone || "UTC";
|
|
3
4
|
const locale = options.locale || "en-AU";
|
|
@@ -13,6 +14,34 @@ export function formatDiscordPromptTime(date, options = {}) {
|
|
|
13
14
|
timeZoneName: "short",
|
|
14
15
|
}).format(date);
|
|
15
16
|
}
|
|
17
|
+
export function buildDiscordMessageMetadata(message, scope) {
|
|
18
|
+
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
19
|
+
const contextEntries = [
|
|
20
|
+
["scope", scope === "dm" ? "dm" : "thread"],
|
|
21
|
+
["sent_at", message.createdAt.toISOString()],
|
|
22
|
+
["sent_at_local", formatDiscordPromptTime(message.createdAt)],
|
|
23
|
+
["message_id", message.id],
|
|
24
|
+
[
|
|
25
|
+
"author_name",
|
|
26
|
+
getAuthorDisplayName(message).replace(/\s+/g, " ").trim() || undefined,
|
|
27
|
+
],
|
|
28
|
+
["author_id", message.author.id],
|
|
29
|
+
[
|
|
30
|
+
"thread_title",
|
|
31
|
+
isThread
|
|
32
|
+
? (message.channel.name ?? "").replace(/\s+/g, " ").trim()
|
|
33
|
+
: undefined,
|
|
34
|
+
],
|
|
35
|
+
["thread_id", isThread ? message.channel.id : undefined],
|
|
36
|
+
[
|
|
37
|
+
"forum_channel_id",
|
|
38
|
+
isThread ? (message.channel.parentId ?? undefined) : undefined,
|
|
39
|
+
],
|
|
40
|
+
].filter((entry) => {
|
|
41
|
+
return typeof entry[1] === "string" && entry[1].length > 0;
|
|
42
|
+
});
|
|
43
|
+
return wrapXmlTag("discord_message_context", JSON.stringify(Object.fromEntries(contextEntries), null, 2));
|
|
44
|
+
}
|
|
16
45
|
/** Wrap content in an XML-style tag: `<tag>content</tag>`. */
|
|
17
46
|
export function wrapXmlTag(tag, content) {
|
|
18
47
|
return `<${tag}>${content}</${tag}>`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AgentService } from "./agent-service";
|
|
3
3
|
import { PromptQueue } from "./prompt-queue";
|
|
4
|
-
export type SessionScope = string
|
|
4
|
+
export type SessionScope = "dm" | `thread:${string}`;
|
|
5
5
|
export type ScopedSessionEntry = {
|
|
6
6
|
session: AgentSession;
|
|
7
7
|
promptQueue: PromptQueue;
|