@poncho-ai/messaging 0.7.9 → 0.8.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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +14 -0
- package/dist/index.js +36 -3
- package/package.json +1 -1
- package/src/adapters/slack/index.ts +21 -1
- package/src/adapters/slack/utils.ts +32 -0
- package/src/bridge.ts +2 -2
- package/test/adapters/slack.test.ts +92 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/messaging@0.
|
|
2
|
+
> @poncho-ai/messaging@0.8.0 build /home/runner/work/poncho-ai/poncho-ai/packages/messaging
|
|
3
3
|
> tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m53.03 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 66ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 4862ms
|
|
14
14
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m11.66 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @poncho-ai/messaging
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`fb7ee97`](https://github.com/cesr/poncho-ai/commit/fb7ee97f7df0dda7318a7e59565e0b53285f10c4) Thanks [@cesr](https://github.com/cesr)! - feat: include thread context when Slack bot is @mentioned in a thread reply
|
|
8
|
+
|
|
9
|
+
When the bot is @mentioned in a thread (not the parent message), the adapter now fetches prior thread messages via `conversations.replies` and prepends them as context, so the agent understands what the conversation is about.
|
|
10
|
+
|
|
11
|
+
## 0.7.10
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#73](https://github.com/cesr/poncho-ai/pull/73) [`f72f202`](https://github.com/cesr/poncho-ai/commit/f72f202d839dbbb8240336ec76eb6340aba20f06) Thanks [@cesr](https://github.com/cesr)! - Fix Telegram approval message ordering: send accumulated assistant text before approval buttons so the conversation reads naturally. Skip empty bridge replies when text was already sent at checkpoint.
|
|
16
|
+
|
|
3
17
|
## 0.7.9
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -99,11 +99,11 @@ ${message.text}`;
|
|
|
99
99
|
}
|
|
100
100
|
break;
|
|
101
101
|
}
|
|
102
|
-
if (this.adapter.autoReply) {
|
|
102
|
+
if (this.adapter.autoReply && (accumulatedResponse.trim() || accumulatedFiles.length > 0)) {
|
|
103
103
|
await this.adapter.sendReply(message.threadRef, accumulatedResponse, {
|
|
104
104
|
files: accumulatedFiles.length > 0 ? accumulatedFiles : void 0
|
|
105
105
|
});
|
|
106
|
-
} else if (!this.adapter.hasSentInCurrentRequest) {
|
|
106
|
+
} else if (!this.adapter.autoReply && !this.adapter.hasSentInCurrentRequest) {
|
|
107
107
|
console.warn("[agent-bridge] tool mode completed without send_email being called; no reply sent");
|
|
108
108
|
}
|
|
109
109
|
} catch (error) {
|
|
@@ -203,6 +203,21 @@ var addReaction = async (token, channel, timestamp, reaction) => {
|
|
|
203
203
|
throw new Error(`Slack reactions.add failed: ${result.error}`);
|
|
204
204
|
}
|
|
205
205
|
};
|
|
206
|
+
var fetchThreadMessages = async (token, channel, threadTs, excludeTs) => {
|
|
207
|
+
const result = await slackFetch("conversations.replies", token, {
|
|
208
|
+
channel,
|
|
209
|
+
ts: threadTs,
|
|
210
|
+
limit: 50
|
|
211
|
+
});
|
|
212
|
+
if (!result.ok) {
|
|
213
|
+
console.warn(
|
|
214
|
+
`[slack-adapter] conversations.replies failed: ${result.error}`
|
|
215
|
+
);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const messages = result.messages ?? [];
|
|
219
|
+
return excludeTs ? messages.filter((m) => m.ts !== excludeTs) : messages;
|
|
220
|
+
};
|
|
206
221
|
var removeReaction = async (token, channel, timestamp, reaction) => {
|
|
207
222
|
const result = await slackFetch("reactions.remove", token, {
|
|
208
223
|
channel,
|
|
@@ -335,8 +350,26 @@ var SlackAdapter = class {
|
|
|
335
350
|
const messageTs = String(event.ts ?? "");
|
|
336
351
|
const channel = String(event.channel ?? "");
|
|
337
352
|
const userId = String(event.user ?? "");
|
|
353
|
+
let contextPrefix = "";
|
|
354
|
+
if (event.thread_ts && event.thread_ts !== event.ts) {
|
|
355
|
+
const threadMessages = await fetchThreadMessages(
|
|
356
|
+
this.botToken,
|
|
357
|
+
channel,
|
|
358
|
+
threadTs,
|
|
359
|
+
messageTs
|
|
360
|
+
// exclude the triggering message itself
|
|
361
|
+
);
|
|
362
|
+
if (threadMessages.length > 0) {
|
|
363
|
+
const formatted = threadMessages.map((m) => `<${m.user ?? "unknown"}>: ${m.text ?? ""}`).join("\n");
|
|
364
|
+
contextPrefix = `[Thread context]
|
|
365
|
+
${formatted}
|
|
366
|
+
|
|
367
|
+
[New message]
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
338
371
|
const message = {
|
|
339
|
-
text,
|
|
372
|
+
text: contextPrefix + text,
|
|
340
373
|
threadRef: {
|
|
341
374
|
platformThreadId: threadTs,
|
|
342
375
|
channelId: channel,
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import { verifySlackSignature } from "./verify.js";
|
|
10
10
|
import {
|
|
11
11
|
addReaction,
|
|
12
|
+
fetchThreadMessages,
|
|
12
13
|
postMessage,
|
|
13
14
|
removeReaction,
|
|
14
15
|
splitMessage,
|
|
@@ -189,8 +190,27 @@ export class SlackAdapter implements MessagingAdapter {
|
|
|
189
190
|
const channel = String(event.channel ?? "");
|
|
190
191
|
const userId = String(event.user ?? "");
|
|
191
192
|
|
|
193
|
+
// When the mention is inside a thread (thread_ts differs from ts),
|
|
194
|
+
// fetch prior thread messages so the agent has context.
|
|
195
|
+
let contextPrefix = "";
|
|
196
|
+
if (event.thread_ts && event.thread_ts !== event.ts) {
|
|
197
|
+
const threadMessages = await fetchThreadMessages(
|
|
198
|
+
this.botToken,
|
|
199
|
+
channel,
|
|
200
|
+
threadTs,
|
|
201
|
+
messageTs, // exclude the triggering message itself
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (threadMessages.length > 0) {
|
|
205
|
+
const formatted = threadMessages
|
|
206
|
+
.map((m) => `<${m.user ?? "unknown"}>: ${m.text ?? ""}`)
|
|
207
|
+
.join("\n");
|
|
208
|
+
contextPrefix = `[Thread context]\n${formatted}\n\n[New message]\n`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
192
212
|
const message: PonchoIncomingMessage = {
|
|
193
|
-
text,
|
|
213
|
+
text: contextPrefix + text,
|
|
194
214
|
threadRef: {
|
|
195
215
|
platformThreadId: threadTs,
|
|
196
216
|
channelId: channel,
|
|
@@ -90,6 +90,38 @@ export const addReaction = async (
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Fetch thread replies using `conversations.replies`.
|
|
95
|
+
* Returns messages sorted chronologically (oldest first), excluding the
|
|
96
|
+
* message identified by `excludeTs` (typically the triggering mention).
|
|
97
|
+
*/
|
|
98
|
+
export const fetchThreadMessages = async (
|
|
99
|
+
token: string,
|
|
100
|
+
channel: string,
|
|
101
|
+
threadTs: string,
|
|
102
|
+
excludeTs?: string,
|
|
103
|
+
): Promise<Array<{ user?: string; text?: string; ts: string }>> => {
|
|
104
|
+
const result = (await slackFetch("conversations.replies", token, {
|
|
105
|
+
channel,
|
|
106
|
+
ts: threadTs,
|
|
107
|
+
limit: 50,
|
|
108
|
+
})) as {
|
|
109
|
+
ok: boolean;
|
|
110
|
+
error?: string;
|
|
111
|
+
messages?: Array<{ user?: string; text?: string; ts: string }>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[slack-adapter] conversations.replies failed: ${result.error}`,
|
|
117
|
+
);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const messages = result.messages ?? [];
|
|
122
|
+
return excludeTs ? messages.filter((m) => m.ts !== excludeTs) : messages;
|
|
123
|
+
};
|
|
124
|
+
|
|
93
125
|
export const removeReaction = async (
|
|
94
126
|
token: string,
|
|
95
127
|
channel: string,
|
package/src/bridge.ts
CHANGED
|
@@ -138,11 +138,11 @@ export class AgentBridge {
|
|
|
138
138
|
break;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
if (this.adapter.autoReply) {
|
|
141
|
+
if (this.adapter.autoReply && (accumulatedResponse.trim() || accumulatedFiles.length > 0)) {
|
|
142
142
|
await this.adapter.sendReply(message.threadRef, accumulatedResponse, {
|
|
143
143
|
files: accumulatedFiles.length > 0 ? accumulatedFiles : undefined,
|
|
144
144
|
});
|
|
145
|
-
} else if (!this.adapter.hasSentInCurrentRequest) {
|
|
145
|
+
} else if (!this.adapter.autoReply && !this.adapter.hasSentInCurrentRequest) {
|
|
146
146
|
console.warn("[agent-bridge] tool mode completed without send_email being called; no reply sent");
|
|
147
147
|
}
|
|
148
148
|
} catch (error) {
|
|
@@ -238,4 +238,96 @@ describe("SlackAdapter", () => {
|
|
|
238
238
|
expect(received).toHaveLength(0);
|
|
239
239
|
expect(resStatus()).toBe(200);
|
|
240
240
|
});
|
|
241
|
+
|
|
242
|
+
it("includes thread context when mention is in a thread reply", async () => {
|
|
243
|
+
const adapter = new SlackAdapter();
|
|
244
|
+
await adapter.initialize();
|
|
245
|
+
const received: PonchoIncomingMessage[] = [];
|
|
246
|
+
adapter.onMessage(async (msg) => { received.push(msg); });
|
|
247
|
+
|
|
248
|
+
// Mock fetch to intercept conversations.replies
|
|
249
|
+
const originalFetch = globalThis.fetch;
|
|
250
|
+
vi.stubGlobal("fetch", vi.fn(async (url: string) => {
|
|
251
|
+
if (typeof url === "string" && url.includes("conversations.replies")) {
|
|
252
|
+
return {
|
|
253
|
+
json: async () => ({
|
|
254
|
+
ok: true,
|
|
255
|
+
messages: [
|
|
256
|
+
{ user: "U_PARENT", text: "What are the limitations of self-hosted?", ts: "100.0" },
|
|
257
|
+
{ user: "U2", text: "<@U1> can you help?", ts: "100.1" },
|
|
258
|
+
],
|
|
259
|
+
}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// Pass through for other Slack API calls (reactions)
|
|
263
|
+
return { json: async () => ({ ok: true }) };
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
let handler: any;
|
|
267
|
+
adapter.registerRoutes((_m, _p, h) => { handler = h; });
|
|
268
|
+
|
|
269
|
+
const body = JSON.stringify({
|
|
270
|
+
type: "event_callback",
|
|
271
|
+
event: {
|
|
272
|
+
type: "app_mention",
|
|
273
|
+
text: "<@U1> can you help?",
|
|
274
|
+
ts: "100.1",
|
|
275
|
+
thread_ts: "100.0",
|
|
276
|
+
channel: "C1",
|
|
277
|
+
user: "U2",
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const { req, res } = makeReqRes(body);
|
|
281
|
+
await handler(req, res);
|
|
282
|
+
|
|
283
|
+
// Wait for async handler
|
|
284
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
285
|
+
|
|
286
|
+
expect(received).toHaveLength(1);
|
|
287
|
+
expect(received[0]!.text).toContain("[Thread context]");
|
|
288
|
+
expect(received[0]!.text).toContain("What are the limitations of self-hosted?");
|
|
289
|
+
expect(received[0]!.text).toContain("[New message]");
|
|
290
|
+
expect(received[0]!.text).toContain("can you help?");
|
|
291
|
+
|
|
292
|
+
vi.stubGlobal("fetch", originalFetch);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("does not fetch thread context for top-level mentions", async () => {
|
|
296
|
+
const adapter = new SlackAdapter();
|
|
297
|
+
await adapter.initialize();
|
|
298
|
+
const received: PonchoIncomingMessage[] = [];
|
|
299
|
+
adapter.onMessage(async (msg) => { received.push(msg); });
|
|
300
|
+
|
|
301
|
+
const fetchSpy = vi.fn(async () => ({ json: async () => ({ ok: true }) }));
|
|
302
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
303
|
+
|
|
304
|
+
let handler: any;
|
|
305
|
+
adapter.registerRoutes((_m, _p, h) => { handler = h; });
|
|
306
|
+
|
|
307
|
+
// No thread_ts → top-level message
|
|
308
|
+
const body = JSON.stringify({
|
|
309
|
+
type: "event_callback",
|
|
310
|
+
event: {
|
|
311
|
+
type: "app_mention",
|
|
312
|
+
text: "<@U1> hello",
|
|
313
|
+
ts: "200.0",
|
|
314
|
+
channel: "C1",
|
|
315
|
+
user: "U2",
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const { req, res } = makeReqRes(body);
|
|
319
|
+
await handler(req, res);
|
|
320
|
+
|
|
321
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
322
|
+
|
|
323
|
+
expect(received).toHaveLength(1);
|
|
324
|
+
expect(received[0]!.text).toBe("hello");
|
|
325
|
+
// conversations.replies should NOT have been called
|
|
326
|
+
const repliesCalls = fetchSpy.mock.calls.filter(
|
|
327
|
+
([url]: any) => typeof url === "string" && url.includes("conversations.replies"),
|
|
328
|
+
);
|
|
329
|
+
expect(repliesCalls).toHaveLength(0);
|
|
330
|
+
|
|
331
|
+
vi.restoreAllMocks();
|
|
332
|
+
});
|
|
241
333
|
});
|