@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.
@@ -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();
@@ -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): string | null;
9
- export declare function isAuthorizedMessage(message: Message, scope: string, accessConfig: GatewayAccessConfig): boolean;
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 { getAuthorDisplayName, isAuthorizedMessage, resolveMessageScope, } from "./discord-auth";
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
- /** Build a Discord metadata XML string for a message. */
12
- function formatDiscordMessageMetadata(message, scope) {
13
- const isThread = scope.startsWith("thread:") && message.channel.isThread();
14
- const contextEntries = [
15
- ["scope", scope === "dm" ? "dm" : "thread"],
16
- ["sent_at", message.createdAt.toISOString()],
17
- ["sent_at_local", formatDiscordPromptTime(message.createdAt)],
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
- if (message.author.bot) {
43
- // logger.debug("ignored bot message");
19
+ const preparedMessage = await prepareDiscordMessage(message, accessConfig);
20
+ if (!preparedMessage) {
44
21
  return;
45
22
  }
46
- if (message.system) {
47
- // logger.debug({ messageId: message.id }, "ignored system message");
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
- logger.info({
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
- }, "message received");
91
- const channelKey = message.channel.id;
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
- const { entry, created } = await sessionRegistry.getOrCreate(scope);
96
- const { session, promptQueue } = entry;
97
- if (created && scope.startsWith("thread:") && message.channel.isThread()) {
98
- logger.info({
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
- const commandResult = await executeSessionCommand(content, {
104
- agentService,
105
- promptQueue,
106
- session,
101
+ logger.info({
107
102
  scope,
108
- workingEmoji: entry.workingEmoji,
109
- });
110
- if (commandResult.handled) {
111
- stopTypingForChannel(channelKey);
112
- if (commandResult.workingEmoji) {
113
- entry.workingEmoji = commandResult.workingEmoji;
114
- logger.info({ scope, emoji: commandResult.workingEmoji }, "working emoji updated");
115
- }
116
- if (commandResult.newSession) {
117
- entry.session = commandResult.newSession;
118
- logger.info({ scope, sessionId: commandResult.newSession.sessionId }, "session replaced");
119
- }
120
- if (commandResult.archive && scope.startsWith("thread:")) {
121
- logger.info({ scope }, "archiving thread");
122
- const archiveChannel = message.channel;
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
- const queuePosition = promptQueue.getSnapshot().pending;
154
- if (queuePosition > 0) {
155
- await sendReply(message, `Queued. ${queuePosition} request(s) ahead of this one.`);
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
- let promptContent = content;
161
- let promptImages;
162
- if (mediaAttachments.length > 0) {
163
- const resolvedPromptMedia = await resolveMediaAttachmentsForPrompt(mediaAttachments, promptContent, session.model, config, agentService);
164
- promptContent = resolvedPromptMedia.content;
165
- if (resolvedPromptMedia.images.length > 0) {
166
- promptImages = resolvedPromptMedia.images;
167
- }
168
- }
169
- const discordMetadata = formatDiscordMessageMetadata(message, scope);
170
- const transformedPrompt = await config.promptTransform({
171
- rawContent: promptContent,
172
- discordMetadata,
173
- now: () => wrapXmlTag("datetime", formatDiscordPromptTime(new Date(), {
174
- timeZone: config.promptTimeZone,
175
- locale: config.promptLocale,
176
- })),
177
- userMessage: () => wrapXmlTag("user_message", promptContent),
178
- });
179
- return runAgentTurn(session, transformedPrompt, {
180
- images: promptImages,
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
- await sendReply(message, response);
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>;
@@ -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 addWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
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 working reaction");
43
+ logger.debug({ messageId: message.id, emoji, error }, "failed to add reaction");
44
44
  }
45
45
  }
46
- export async function removeWorkingReaction(message, emoji = DEFAULT_WORKING_EMOJI) {
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 working reaction");
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()) {
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@friendlyrobot/discord-pi-agent",
3
- "version": "0.21.3",
3
+ "version": "0.22.0",
4
4
  "description": "Reusable Discord gateway for persistent pi agent sessions",
5
5
  "license": "MIT",
6
6
  "type": "module",