@poncho-ai/messaging 0.6.0 → 0.7.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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +26 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +154 -1
- package/package.json +2 -2
- package/src/adapters/telegram/index.ts +159 -0
- package/src/adapters/telegram/utils.ts +79 -0
- package/src/bridge.ts +2 -2
- package/src/index.ts +5 -1
- package/src/types.ts +1 -1
- package/.turbo/turbo-test.log +0 -53
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/messaging@0.
|
|
2
|
+
> @poncho-ai/messaging@0.7.1 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[32m51.09 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 67ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 5004ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m11.24 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @poncho-ai/messaging
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#42](https://github.com/cesr/poncho-ai/pull/42) [`e58a984`](https://github.com/cesr/poncho-ai/commit/e58a984efaa673b649318102bbf735fb4c2f9172) Thanks [@cesr](https://github.com/cesr)! - Add continuation model and fire-and-forget subagents
|
|
8
|
+
|
|
9
|
+
**Continuation model**: Agents no longer send a synthetic `"Continue"` user message between steps. Instead, the harness injects a transient signal when needed, and the full internal message chain is preserved across continuations so the LLM never loses context. `RunInput` gains `disableSoftDeadline` and `RunResult` gains `continuationMessages`.
|
|
10
|
+
|
|
11
|
+
**Fire-and-forget subagents**: Subagents now run asynchronously in the background. `spawn_subagent` returns immediately with a subagent ID; results are delivered back to the parent conversation as a callback once the subagent completes. Subagents cannot spawn their own subagents. The web UI shows results in a collapsible disclosure and reconnects the live event stream automatically when the parent agent resumes.
|
|
12
|
+
|
|
13
|
+
**Bug fixes**:
|
|
14
|
+
- Fixed a race condition where concurrent runs on the same harness instance could assign a subagent or browser tab to the wrong parent conversation (shared `_currentRunConversationId` field replaced with per-run `ToolContext.conversationId`).
|
|
15
|
+
- Fixed Upstash KV store silently dropping large values by switching from URL-path encoding to request body format for `SET`/`SETEX` commands.
|
|
16
|
+
- Fixed empty assistant content blocks causing Anthropic `text content blocks must be non-empty` errors.
|
|
17
|
+
|
|
18
|
+
**Client**: Added `getConversationStatus()` and `waitForSubagents` option on `sendMessage()`.
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`e58a984`](https://github.com/cesr/poncho-ai/commit/e58a984efaa673b649318102bbf735fb4c2f9172)]:
|
|
21
|
+
- @poncho-ai/sdk@1.6.0
|
|
22
|
+
|
|
23
|
+
## 0.7.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- [`26be28a`](https://github.com/cesr/poncho-ai/commit/26be28a958f2eb27dd78225f1cf80b67b16d673d) Thanks [@cesr](https://github.com/cesr)! - Add tool approval support in Telegram via inline keyboard buttons. When the agent needs approval for a tool call, the bot sends Approve/Deny buttons to the chat. After all decisions are made, the run resumes and the response is delivered. Approvals from the web UI for Telegram conversations are also routed back to the chat.
|
|
28
|
+
|
|
3
29
|
## 0.6.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ interface AgentRunner {
|
|
|
71
71
|
messages: Message[];
|
|
72
72
|
}>;
|
|
73
73
|
run(conversationId: string, input: {
|
|
74
|
-
task
|
|
74
|
+
task?: string;
|
|
75
75
|
messages: Message[];
|
|
76
76
|
files?: FileAttachment[];
|
|
77
77
|
metadata?: {
|
|
@@ -200,6 +200,12 @@ declare class ResendAdapter implements MessagingAdapter {
|
|
|
200
200
|
private fetchAndDownloadAttachments;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
interface TelegramApprovalInfo {
|
|
204
|
+
approvalId: string;
|
|
205
|
+
tool: string;
|
|
206
|
+
input: Record<string, unknown>;
|
|
207
|
+
}
|
|
208
|
+
type TelegramApprovalDecisionHandler = (approvalId: string, approved: boolean, chatId: string) => Promise<void>;
|
|
203
209
|
interface TelegramAdapterOptions {
|
|
204
210
|
botTokenEnv?: string;
|
|
205
211
|
webhookSecretEnv?: string;
|
|
@@ -217,7 +223,9 @@ declare class TelegramAdapter implements MessagingAdapter {
|
|
|
217
223
|
private readonly webhookSecretEnv;
|
|
218
224
|
private readonly allowedUserIds;
|
|
219
225
|
private handler;
|
|
226
|
+
private approvalDecisionHandler;
|
|
220
227
|
private readonly sessionCounters;
|
|
228
|
+
private readonly approvalMessageIds;
|
|
221
229
|
private lastUpdateId;
|
|
222
230
|
constructor(options?: TelegramAdapterOptions);
|
|
223
231
|
initialize(): Promise<void>;
|
|
@@ -227,7 +235,13 @@ declare class TelegramAdapter implements MessagingAdapter {
|
|
|
227
235
|
files?: FileAttachment[];
|
|
228
236
|
}): Promise<void>;
|
|
229
237
|
indicateProcessing(threadRef: ThreadRef): Promise<() => Promise<void>>;
|
|
238
|
+
onApprovalDecision(handler: TelegramApprovalDecisionHandler): void;
|
|
239
|
+
sendApprovalRequest(chatId: string, approvals: TelegramApprovalInfo[], opts?: {
|
|
240
|
+
message_thread_id?: number;
|
|
241
|
+
}): Promise<void>;
|
|
242
|
+
updateApprovalMessage(approvalId: string, decision: "approved" | "denied", tool: string): Promise<void>;
|
|
230
243
|
private handleRequest;
|
|
244
|
+
private handleCallbackQuery;
|
|
231
245
|
private sessionKey;
|
|
232
246
|
private extractFiles;
|
|
233
247
|
}
|
|
@@ -284,4 +298,4 @@ declare function markdownToEmailHtml(text: string): string;
|
|
|
284
298
|
*/
|
|
285
299
|
declare function matchesSenderPattern(sender: string, patterns: string[] | undefined): boolean;
|
|
286
300
|
|
|
287
|
-
export { AgentBridge, type AgentBridgeOptions, type AgentRunner, type FileAttachment, type IncomingMessage, type IncomingMessageHandler, type MessagingAdapter, ResendAdapter, type ResendAdapterOptions, type RouteHandler, type RouteRegistrar, SlackAdapter, type SlackAdapterOptions, TelegramAdapter, type TelegramAdapterOptions, type ThreadRef, buildReplyHeaders, buildReplySubject, conversationIdFromThread, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
|
|
301
|
+
export { AgentBridge, type AgentBridgeOptions, type AgentRunner, type FileAttachment, type IncomingMessage, type IncomingMessageHandler, type MessagingAdapter, ResendAdapter, type ResendAdapterOptions, type RouteHandler, type RouteRegistrar, SlackAdapter, type SlackAdapterOptions, TelegramAdapter, type TelegramAdapterOptions, type TelegramApprovalDecisionHandler, type TelegramApprovalInfo, type ThreadRef, buildReplyHeaders, buildReplySubject, conversationIdFromThread, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
|
package/dist/index.js
CHANGED
|
@@ -87,7 +87,7 @@ ${message.text}`;
|
|
|
87
87
|
if (result.continuation && continuationCount < MAX_CONTINUATIONS && (stepBudget <= 0 || totalSteps < stepBudget)) {
|
|
88
88
|
continuationCount++;
|
|
89
89
|
console.log(`[agent-bridge] continuation ${continuationCount}/${MAX_CONTINUATIONS} for ${conversationId}`);
|
|
90
|
-
currentTask =
|
|
90
|
+
currentTask = void 0;
|
|
91
91
|
currentFiles = void 0;
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
@@ -1080,6 +1080,49 @@ var sendDocument = async (token, chatId, docData, opts) => {
|
|
|
1080
1080
|
throw new Error(`Telegram sendDocument failed: ${result.description}`);
|
|
1081
1081
|
}
|
|
1082
1082
|
};
|
|
1083
|
+
var sendMessageWithKeyboard = async (token, chatId, text, keyboard, opts) => {
|
|
1084
|
+
const body = {
|
|
1085
|
+
chat_id: chatId,
|
|
1086
|
+
text,
|
|
1087
|
+
reply_markup: { inline_keyboard: keyboard }
|
|
1088
|
+
};
|
|
1089
|
+
if (opts?.reply_to_message_id) {
|
|
1090
|
+
body.reply_parameters = {
|
|
1091
|
+
message_id: opts.reply_to_message_id,
|
|
1092
|
+
allow_sending_without_reply: true
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if (opts?.message_thread_id) {
|
|
1096
|
+
body.message_thread_id = opts.message_thread_id;
|
|
1097
|
+
}
|
|
1098
|
+
const result = await telegramFetch(token, "sendMessage", body);
|
|
1099
|
+
if (!result.ok) {
|
|
1100
|
+
throw new Error(`Telegram sendMessage (keyboard) failed: ${result.description}`);
|
|
1101
|
+
}
|
|
1102
|
+
const msg = result.result;
|
|
1103
|
+
return msg.message_id;
|
|
1104
|
+
};
|
|
1105
|
+
var editMessageText = async (token, chatId, messageId, text, opts) => {
|
|
1106
|
+
const body = {
|
|
1107
|
+
chat_id: chatId,
|
|
1108
|
+
message_id: messageId,
|
|
1109
|
+
text
|
|
1110
|
+
};
|
|
1111
|
+
if (opts?.reply_markup) {
|
|
1112
|
+
body.reply_markup = opts.reply_markup;
|
|
1113
|
+
}
|
|
1114
|
+
const result = await telegramFetch(token, "editMessageText", body);
|
|
1115
|
+
if (!result.ok) {
|
|
1116
|
+
throw new Error(`Telegram editMessageText failed: ${result.description}`);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
var answerCallbackQuery = async (token, callbackQueryId, opts) => {
|
|
1120
|
+
const body = {
|
|
1121
|
+
callback_query_id: callbackQueryId
|
|
1122
|
+
};
|
|
1123
|
+
if (opts?.text) body.text = opts.text;
|
|
1124
|
+
await telegramFetch(token, "answerCallbackQuery", body);
|
|
1125
|
+
};
|
|
1083
1126
|
var sendChatAction = async (token, chatId, action) => {
|
|
1084
1127
|
await telegramFetch(token, "sendChatAction", {
|
|
1085
1128
|
chat_id: chatId,
|
|
@@ -1161,7 +1204,9 @@ var TelegramAdapter = class {
|
|
|
1161
1204
|
webhookSecretEnv;
|
|
1162
1205
|
allowedUserIds;
|
|
1163
1206
|
handler;
|
|
1207
|
+
approvalDecisionHandler;
|
|
1164
1208
|
sessionCounters = /* @__PURE__ */ new Map();
|
|
1209
|
+
approvalMessageIds = /* @__PURE__ */ new Map();
|
|
1165
1210
|
lastUpdateId = 0;
|
|
1166
1211
|
constructor(options = {}) {
|
|
1167
1212
|
this.botTokenEnv = options.botTokenEnv ?? "TELEGRAM_BOT_TOKEN";
|
|
@@ -1233,6 +1278,72 @@ var TelegramAdapter = class {
|
|
|
1233
1278
|
};
|
|
1234
1279
|
}
|
|
1235
1280
|
// -----------------------------------------------------------------------
|
|
1281
|
+
// Approval support
|
|
1282
|
+
// -----------------------------------------------------------------------
|
|
1283
|
+
onApprovalDecision(handler) {
|
|
1284
|
+
this.approvalDecisionHandler = handler;
|
|
1285
|
+
}
|
|
1286
|
+
async sendApprovalRequest(chatId, approvals, opts) {
|
|
1287
|
+
const MAX_INPUT_LENGTH = 3500;
|
|
1288
|
+
for (const approval of approvals) {
|
|
1289
|
+
let inputSummary = Object.entries(approval.input).map(([k, v]) => ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join("\n");
|
|
1290
|
+
if (inputSummary.length > MAX_INPUT_LENGTH) {
|
|
1291
|
+
inputSummary = inputSummary.slice(0, MAX_INPUT_LENGTH) + "\n \u2026(truncated)";
|
|
1292
|
+
}
|
|
1293
|
+
const text = [
|
|
1294
|
+
`\u{1F527} Tool approval required: ${approval.tool}`,
|
|
1295
|
+
"",
|
|
1296
|
+
inputSummary ? `Input:
|
|
1297
|
+
${inputSummary}` : "(no input)"
|
|
1298
|
+
].join("\n");
|
|
1299
|
+
const keyboard = [
|
|
1300
|
+
[
|
|
1301
|
+
{ text: "\u2705 Approve", callback_data: `a:${approval.approvalId}` },
|
|
1302
|
+
{ text: "\u274C Deny", callback_data: `d:${approval.approvalId}` }
|
|
1303
|
+
]
|
|
1304
|
+
];
|
|
1305
|
+
try {
|
|
1306
|
+
const messageId = await sendMessageWithKeyboard(
|
|
1307
|
+
this.botToken,
|
|
1308
|
+
chatId,
|
|
1309
|
+
text,
|
|
1310
|
+
keyboard,
|
|
1311
|
+
{ message_thread_id: opts?.message_thread_id }
|
|
1312
|
+
);
|
|
1313
|
+
this.approvalMessageIds.set(approval.approvalId, {
|
|
1314
|
+
chatId,
|
|
1315
|
+
messageId
|
|
1316
|
+
});
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
console.error(
|
|
1319
|
+
"[telegram-adapter] failed to send approval request:",
|
|
1320
|
+
err instanceof Error ? err.message : err
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async updateApprovalMessage(approvalId, decision, tool) {
|
|
1326
|
+
const tracked = this.approvalMessageIds.get(approvalId);
|
|
1327
|
+
if (!tracked) return;
|
|
1328
|
+
const icon = decision === "approved" ? "\u2705" : "\u274C";
|
|
1329
|
+
const label = decision === "approved" ? "Approved" : "Denied";
|
|
1330
|
+
const text = `${icon} ${label}: ${tool}`;
|
|
1331
|
+
try {
|
|
1332
|
+
await editMessageText(
|
|
1333
|
+
this.botToken,
|
|
1334
|
+
tracked.chatId,
|
|
1335
|
+
tracked.messageId,
|
|
1336
|
+
text
|
|
1337
|
+
);
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
console.warn(
|
|
1340
|
+
"[telegram-adapter] failed to update approval message:",
|
|
1341
|
+
err instanceof Error ? err.message : err
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
this.approvalMessageIds.delete(approvalId);
|
|
1345
|
+
}
|
|
1346
|
+
// -----------------------------------------------------------------------
|
|
1236
1347
|
// HTTP request handling
|
|
1237
1348
|
// -----------------------------------------------------------------------
|
|
1238
1349
|
async handleRequest(req, res) {
|
|
@@ -1259,6 +1370,17 @@ var TelegramAdapter = class {
|
|
|
1259
1370
|
return;
|
|
1260
1371
|
}
|
|
1261
1372
|
this.lastUpdateId = payload.update_id;
|
|
1373
|
+
if (payload.callback_query) {
|
|
1374
|
+
res.writeHead(200);
|
|
1375
|
+
res.end();
|
|
1376
|
+
void this.handleCallbackQuery(payload.callback_query).catch((err) => {
|
|
1377
|
+
console.error(
|
|
1378
|
+
"[telegram-adapter] callback_query error:",
|
|
1379
|
+
err instanceof Error ? err.message : err
|
|
1380
|
+
);
|
|
1381
|
+
});
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1262
1384
|
const message = payload.message;
|
|
1263
1385
|
if (!message) {
|
|
1264
1386
|
res.writeHead(200);
|
|
@@ -1350,6 +1472,37 @@ var TelegramAdapter = class {
|
|
|
1350
1472
|
});
|
|
1351
1473
|
}
|
|
1352
1474
|
// -----------------------------------------------------------------------
|
|
1475
|
+
// Callback query handling
|
|
1476
|
+
// -----------------------------------------------------------------------
|
|
1477
|
+
async handleCallbackQuery(query) {
|
|
1478
|
+
if (this.allowedUserIds && !this.allowedUserIds.includes(query.from.id)) {
|
|
1479
|
+
await answerCallbackQuery(this.botToken, query.id, {
|
|
1480
|
+
text: "You are not authorized to approve tools."
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const data = query.data;
|
|
1485
|
+
if (!data) {
|
|
1486
|
+
await answerCallbackQuery(this.botToken, query.id);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const isApprove = data.startsWith("a:");
|
|
1490
|
+
const isDeny = data.startsWith("d:");
|
|
1491
|
+
if (!isApprove && !isDeny) {
|
|
1492
|
+
await answerCallbackQuery(this.botToken, query.id);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const approvalId = data.slice(2);
|
|
1496
|
+
const approved = isApprove;
|
|
1497
|
+
const chatId = query.message?.chat.id ? String(query.message.chat.id) : void 0;
|
|
1498
|
+
await answerCallbackQuery(this.botToken, query.id, {
|
|
1499
|
+
text: approved ? "Approved" : "Denied"
|
|
1500
|
+
});
|
|
1501
|
+
if (this.approvalDecisionHandler && chatId) {
|
|
1502
|
+
await this.approvalDecisionHandler(approvalId, approved, chatId);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// -----------------------------------------------------------------------
|
|
1353
1506
|
// Helpers
|
|
1354
1507
|
// -----------------------------------------------------------------------
|
|
1355
1508
|
sessionKey(message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Messaging platform adapters for Poncho agents (Slack, Telegram, etc.)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@poncho-ai/sdk": "1.
|
|
23
|
+
"@poncho-ai/sdk": "1.6.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"resend": ">=4.0.0"
|
|
@@ -9,15 +9,19 @@ import type {
|
|
|
9
9
|
} from "../../types.js";
|
|
10
10
|
import { verifyTelegramSecret } from "./verify.js";
|
|
11
11
|
import {
|
|
12
|
+
type TelegramInlineKeyboardButton,
|
|
12
13
|
type TelegramMessage,
|
|
13
14
|
type TelegramUpdate,
|
|
15
|
+
answerCallbackQuery,
|
|
14
16
|
downloadFile,
|
|
17
|
+
editMessageText,
|
|
15
18
|
getFile,
|
|
16
19
|
getMe,
|
|
17
20
|
isBotMentioned,
|
|
18
21
|
sendChatAction,
|
|
19
22
|
sendDocument,
|
|
20
23
|
sendMessage,
|
|
24
|
+
sendMessageWithKeyboard,
|
|
21
25
|
sendPhoto,
|
|
22
26
|
splitMessage,
|
|
23
27
|
stripMention,
|
|
@@ -26,6 +30,18 @@ import {
|
|
|
26
30
|
const TYPING_INTERVAL_MS = 4_000;
|
|
27
31
|
const NEW_COMMAND_RE = /^\/new(?:@(\S+))?$/i;
|
|
28
32
|
|
|
33
|
+
export interface TelegramApprovalInfo {
|
|
34
|
+
approvalId: string;
|
|
35
|
+
tool: string;
|
|
36
|
+
input: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TelegramApprovalDecisionHandler = (
|
|
40
|
+
approvalId: string,
|
|
41
|
+
approved: boolean,
|
|
42
|
+
chatId: string,
|
|
43
|
+
) => Promise<void>;
|
|
44
|
+
|
|
29
45
|
export interface TelegramAdapterOptions {
|
|
30
46
|
botTokenEnv?: string;
|
|
31
47
|
webhookSecretEnv?: string;
|
|
@@ -53,7 +69,9 @@ export class TelegramAdapter implements MessagingAdapter {
|
|
|
53
69
|
private readonly webhookSecretEnv: string;
|
|
54
70
|
private readonly allowedUserIds: number[] | undefined;
|
|
55
71
|
private handler: IncomingMessageHandler | undefined;
|
|
72
|
+
private approvalDecisionHandler: TelegramApprovalDecisionHandler | undefined;
|
|
56
73
|
private readonly sessionCounters = new Map<string, number>();
|
|
74
|
+
private readonly approvalMessageIds = new Map<string, { chatId: string; messageId: number }>();
|
|
57
75
|
private lastUpdateId = 0;
|
|
58
76
|
|
|
59
77
|
constructor(options: TelegramAdapterOptions = {}) {
|
|
@@ -148,6 +166,92 @@ export class TelegramAdapter implements MessagingAdapter {
|
|
|
148
166
|
};
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
// -----------------------------------------------------------------------
|
|
170
|
+
// Approval support
|
|
171
|
+
// -----------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
onApprovalDecision(handler: TelegramApprovalDecisionHandler): void {
|
|
174
|
+
this.approvalDecisionHandler = handler;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async sendApprovalRequest(
|
|
178
|
+
chatId: string,
|
|
179
|
+
approvals: TelegramApprovalInfo[],
|
|
180
|
+
opts?: { message_thread_id?: number },
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
const MAX_INPUT_LENGTH = 3500;
|
|
183
|
+
|
|
184
|
+
for (const approval of approvals) {
|
|
185
|
+
let inputSummary = Object.entries(approval.input)
|
|
186
|
+
.map(([k, v]) => ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
187
|
+
.join("\n");
|
|
188
|
+
|
|
189
|
+
if (inputSummary.length > MAX_INPUT_LENGTH) {
|
|
190
|
+
inputSummary = inputSummary.slice(0, MAX_INPUT_LENGTH) + "\n …(truncated)";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const text = [
|
|
194
|
+
`🔧 Tool approval required: ${approval.tool}`,
|
|
195
|
+
"",
|
|
196
|
+
inputSummary ? `Input:\n${inputSummary}` : "(no input)",
|
|
197
|
+
].join("\n");
|
|
198
|
+
|
|
199
|
+
const keyboard: TelegramInlineKeyboardButton[][] = [
|
|
200
|
+
[
|
|
201
|
+
{ text: "✅ Approve", callback_data: `a:${approval.approvalId}` },
|
|
202
|
+
{ text: "❌ Deny", callback_data: `d:${approval.approvalId}` },
|
|
203
|
+
],
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const messageId = await sendMessageWithKeyboard(
|
|
208
|
+
this.botToken,
|
|
209
|
+
chatId,
|
|
210
|
+
text,
|
|
211
|
+
keyboard,
|
|
212
|
+
{ message_thread_id: opts?.message_thread_id },
|
|
213
|
+
);
|
|
214
|
+
this.approvalMessageIds.set(approval.approvalId, {
|
|
215
|
+
chatId,
|
|
216
|
+
messageId,
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error(
|
|
220
|
+
"[telegram-adapter] failed to send approval request:",
|
|
221
|
+
err instanceof Error ? err.message : err,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async updateApprovalMessage(
|
|
228
|
+
approvalId: string,
|
|
229
|
+
decision: "approved" | "denied",
|
|
230
|
+
tool: string,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const tracked = this.approvalMessageIds.get(approvalId);
|
|
233
|
+
if (!tracked) return;
|
|
234
|
+
|
|
235
|
+
const icon = decision === "approved" ? "✅" : "❌";
|
|
236
|
+
const label = decision === "approved" ? "Approved" : "Denied";
|
|
237
|
+
const text = `${icon} ${label}: ${tool}`;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await editMessageText(
|
|
241
|
+
this.botToken,
|
|
242
|
+
tracked.chatId,
|
|
243
|
+
tracked.messageId,
|
|
244
|
+
text,
|
|
245
|
+
);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.warn(
|
|
248
|
+
"[telegram-adapter] failed to update approval message:",
|
|
249
|
+
err instanceof Error ? err.message : err,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
this.approvalMessageIds.delete(approvalId);
|
|
253
|
+
}
|
|
254
|
+
|
|
151
255
|
// -----------------------------------------------------------------------
|
|
152
256
|
// HTTP request handling
|
|
153
257
|
// -----------------------------------------------------------------------
|
|
@@ -187,6 +291,19 @@ export class TelegramAdapter implements MessagingAdapter {
|
|
|
187
291
|
}
|
|
188
292
|
this.lastUpdateId = payload.update_id;
|
|
189
293
|
|
|
294
|
+
// -- Callback query (inline keyboard button press) ---------------------
|
|
295
|
+
if (payload.callback_query) {
|
|
296
|
+
res.writeHead(200);
|
|
297
|
+
res.end();
|
|
298
|
+
void this.handleCallbackQuery(payload.callback_query).catch((err) => {
|
|
299
|
+
console.error(
|
|
300
|
+
"[telegram-adapter] callback_query error:",
|
|
301
|
+
err instanceof Error ? err.message : err,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
190
307
|
const message = payload.message;
|
|
191
308
|
if (!message) {
|
|
192
309
|
res.writeHead(200);
|
|
@@ -313,6 +430,48 @@ export class TelegramAdapter implements MessagingAdapter {
|
|
|
313
430
|
});
|
|
314
431
|
}
|
|
315
432
|
|
|
433
|
+
// -----------------------------------------------------------------------
|
|
434
|
+
// Callback query handling
|
|
435
|
+
// -----------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
private async handleCallbackQuery(
|
|
438
|
+
query: import("./utils.js").TelegramCallbackQuery,
|
|
439
|
+
): Promise<void> {
|
|
440
|
+
if (this.allowedUserIds && !this.allowedUserIds.includes(query.from.id)) {
|
|
441
|
+
await answerCallbackQuery(this.botToken, query.id, {
|
|
442
|
+
text: "You are not authorized to approve tools.",
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const data = query.data;
|
|
448
|
+
if (!data) {
|
|
449
|
+
await answerCallbackQuery(this.botToken, query.id);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const isApprove = data.startsWith("a:");
|
|
454
|
+
const isDeny = data.startsWith("d:");
|
|
455
|
+
if (!isApprove && !isDeny) {
|
|
456
|
+
await answerCallbackQuery(this.botToken, query.id);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const approvalId = data.slice(2);
|
|
461
|
+
const approved = isApprove;
|
|
462
|
+
const chatId = query.message?.chat.id
|
|
463
|
+
? String(query.message.chat.id)
|
|
464
|
+
: undefined;
|
|
465
|
+
|
|
466
|
+
await answerCallbackQuery(this.botToken, query.id, {
|
|
467
|
+
text: approved ? "Approved" : "Denied",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
if (this.approvalDecisionHandler && chatId) {
|
|
471
|
+
await this.approvalDecisionHandler(approvalId, approved, chatId);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
316
475
|
// -----------------------------------------------------------------------
|
|
317
476
|
// Helpers
|
|
318
477
|
// -----------------------------------------------------------------------
|
|
@@ -55,9 +55,22 @@ export interface TelegramMessage {
|
|
|
55
55
|
message_thread_id?: number;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface TelegramCallbackQuery {
|
|
59
|
+
id: string;
|
|
60
|
+
from: TelegramUser;
|
|
61
|
+
message?: TelegramMessage;
|
|
62
|
+
data?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TelegramInlineKeyboardButton {
|
|
66
|
+
text: string;
|
|
67
|
+
callback_data?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
export interface TelegramUpdate {
|
|
59
71
|
update_id: number;
|
|
60
72
|
message?: TelegramMessage;
|
|
73
|
+
callback_query?: TelegramCallbackQuery;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
// ---------------------------------------------------------------------------
|
|
@@ -259,6 +272,72 @@ export const sendDocument = async (
|
|
|
259
272
|
}
|
|
260
273
|
};
|
|
261
274
|
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Inline keyboard messages
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
export const sendMessageWithKeyboard = async (
|
|
280
|
+
token: string,
|
|
281
|
+
chatId: number | string,
|
|
282
|
+
text: string,
|
|
283
|
+
keyboard: TelegramInlineKeyboardButton[][],
|
|
284
|
+
opts?: { reply_to_message_id?: number; message_thread_id?: number },
|
|
285
|
+
): Promise<number> => {
|
|
286
|
+
const body: Record<string, unknown> = {
|
|
287
|
+
chat_id: chatId,
|
|
288
|
+
text,
|
|
289
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
290
|
+
};
|
|
291
|
+
if (opts?.reply_to_message_id) {
|
|
292
|
+
body.reply_parameters = {
|
|
293
|
+
message_id: opts.reply_to_message_id,
|
|
294
|
+
allow_sending_without_reply: true,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (opts?.message_thread_id) {
|
|
298
|
+
body.message_thread_id = opts.message_thread_id;
|
|
299
|
+
}
|
|
300
|
+
const result = await telegramFetch(token, "sendMessage", body);
|
|
301
|
+
if (!result.ok) {
|
|
302
|
+
throw new Error(`Telegram sendMessage (keyboard) failed: ${result.description}`);
|
|
303
|
+
}
|
|
304
|
+
const msg = result.result as { message_id: number };
|
|
305
|
+
return msg.message_id;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const editMessageText = async (
|
|
309
|
+
token: string,
|
|
310
|
+
chatId: number | string,
|
|
311
|
+
messageId: number,
|
|
312
|
+
text: string,
|
|
313
|
+
opts?: { reply_markup?: { inline_keyboard: TelegramInlineKeyboardButton[][] } },
|
|
314
|
+
): Promise<void> => {
|
|
315
|
+
const body: Record<string, unknown> = {
|
|
316
|
+
chat_id: chatId,
|
|
317
|
+
message_id: messageId,
|
|
318
|
+
text,
|
|
319
|
+
};
|
|
320
|
+
if (opts?.reply_markup) {
|
|
321
|
+
body.reply_markup = opts.reply_markup;
|
|
322
|
+
}
|
|
323
|
+
const result = await telegramFetch(token, "editMessageText", body);
|
|
324
|
+
if (!result.ok) {
|
|
325
|
+
throw new Error(`Telegram editMessageText failed: ${result.description}`);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const answerCallbackQuery = async (
|
|
330
|
+
token: string,
|
|
331
|
+
callbackQueryId: string,
|
|
332
|
+
opts?: { text?: string },
|
|
333
|
+
): Promise<void> => {
|
|
334
|
+
const body: Record<string, unknown> = {
|
|
335
|
+
callback_query_id: callbackQueryId,
|
|
336
|
+
};
|
|
337
|
+
if (opts?.text) body.text = opts.text;
|
|
338
|
+
await telegramFetch(token, "answerCallbackQuery", body);
|
|
339
|
+
};
|
|
340
|
+
|
|
262
341
|
// ---------------------------------------------------------------------------
|
|
263
342
|
// Typing indicator
|
|
264
343
|
// ---------------------------------------------------------------------------
|
package/src/bridge.ts
CHANGED
|
@@ -79,7 +79,7 @@ export class AgentBridge {
|
|
|
79
79
|
const subjectLine = message.subject ? `Subject: ${message.subject}` : "";
|
|
80
80
|
const header = [senderLine, subjectLine].filter(Boolean).join("\n");
|
|
81
81
|
|
|
82
|
-
let currentTask = `${header}\n\n${message.text}`;
|
|
82
|
+
let currentTask: string | undefined = `${header}\n\n${message.text}`;
|
|
83
83
|
let currentFiles = message.files;
|
|
84
84
|
let accumulatedResponse = "";
|
|
85
85
|
let accumulatedFiles: FileAttachment[] = [];
|
|
@@ -122,7 +122,7 @@ export class AgentBridge {
|
|
|
122
122
|
) {
|
|
123
123
|
continuationCount++;
|
|
124
124
|
console.log(`[agent-bridge] continuation ${continuationCount}/${MAX_CONTINUATIONS} for ${conversationId}`);
|
|
125
|
-
currentTask =
|
|
125
|
+
currentTask = undefined;
|
|
126
126
|
currentFiles = undefined;
|
|
127
127
|
continue;
|
|
128
128
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,11 @@ export type { SlackAdapterOptions } from "./adapters/slack/index.js";
|
|
|
16
16
|
export { ResendAdapter } from "./adapters/resend/index.js";
|
|
17
17
|
export type { ResendAdapterOptions } from "./adapters/resend/index.js";
|
|
18
18
|
export { TelegramAdapter } from "./adapters/telegram/index.js";
|
|
19
|
-
export type {
|
|
19
|
+
export type {
|
|
20
|
+
TelegramAdapterOptions,
|
|
21
|
+
TelegramApprovalInfo,
|
|
22
|
+
TelegramApprovalDecisionHandler,
|
|
23
|
+
} from "./adapters/telegram/index.js";
|
|
20
24
|
|
|
21
25
|
export {
|
|
22
26
|
buildReplyHeaders,
|
package/src/types.ts
CHANGED
package/.turbo/turbo-test.log
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @poncho-ai/messaging@0.5.1 test /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
|
|
3
|
-
> vitest
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/cesar/Dev/latitude/poncho-ai/packages/messaging[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m test/adapters/resend.test.ts [2m ([22m[2m13 tests[22m[2m)[22m[90m 4[2mms[22m[39m
|
|
9
|
-
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2mposts an error message and cleans up on runner failure[22m[39m
|
|
10
|
-
[agent-bridge] handleMessage error: Model overloaded
|
|
11
|
-
|
|
12
|
-
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2mskips sendReply when autoReply is false[22m[39m
|
|
13
|
-
[agent-bridge] tool mode completed without send_email being called; no reply sent
|
|
14
|
-
|
|
15
|
-
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2msuppresses error reply when hasSentInCurrentRequest is true[22m[39m
|
|
16
|
-
[agent-bridge] handleMessage error: Oops
|
|
17
|
-
|
|
18
|
-
[90mstderr[2m | test/bridge.test.ts[2m > [22m[2mAgentBridge[2m > [22m[2mcalls resetRequestState before handling each message[22m[39m
|
|
19
|
-
[33m❯[39m test/bridge.test.ts [2m ([22m[2m15 tests[22m [2m|[22m [31m1 failed[39m[2m)[22m[90m 11[2mms[22m[39m
|
|
20
|
-
[31m [33m❯[31m test/bridge.test.ts[2m > [22mAgentBridge[2m > [22mderives deterministic conversation IDs from ThreadRef[39m
|
|
21
|
-
[agent-bridge] tool mode completed without send_email being called; no reply sent
|
|
22
|
-
[31m → expected 'c61bdca0-71e1-4785-869c-5552b90054c5' to be 'test:C001:ts_123' // Object.is equality[39m
|
|
23
|
-
|
|
24
|
-
[32m✓[39m test/adapters/email-utils.test.ts [2m ([22m[2m44 tests[22m[2m)[22m[90m 7[2mms[22m[39m
|
|
25
|
-
[32m✓[39m test/adapters/slack.test.ts [2m ([22m[2m17 tests[22m[2m)[22m[90m 63[2mms[22m[39m
|
|
26
|
-
|
|
27
|
-
[31m⎯⎯⎯⎯⎯⎯⎯[1m[7m Failed Tests 1 [27m[22m⎯⎯⎯⎯⎯⎯⎯[39m
|
|
28
|
-
|
|
29
|
-
[31m[1m[7m FAIL [27m[22m[39m test/bridge.test.ts[2m > [22mAgentBridge[2m > [22mderives deterministic conversation IDs from ThreadRef
|
|
30
|
-
[31m[1mAssertionError[22m: expected 'c61bdca0-71e1-4785-869c-5552b90054c5' to be 'test:C001:ts_123' // Object.is equality[39m
|
|
31
|
-
|
|
32
|
-
[32m- Expected[39m
|
|
33
|
-
[31m+ Received[39m
|
|
34
|
-
|
|
35
|
-
[32m- test:C001:ts_123[39m
|
|
36
|
-
[31m+ c61bdca0-71e1-4785-869c-5552b90054c5[39m
|
|
37
|
-
|
|
38
|
-
[36m [2m❯[22m test/bridge.test.ts:[2m88:33[22m[39m
|
|
39
|
-
[90m 86| [39m
|
|
40
|
-
[90m 87| [39m [34mexpect[39m(runner[33m.[39m_runs)[33m.[39m[34mtoHaveLength[39m([34m1[39m)[33m;[39m
|
|
41
|
-
[90m 88| [39m [34mexpect[39m(runner[33m.[39m_runs[[34m0[39m][33m![39m[33m.[39mid)[33m.[39m[34mtoBe[39m([32m"test:C001:ts_123"[39m)[33m;[39m
|
|
42
|
-
[90m | [39m [31m^[39m
|
|
43
|
-
[90m 89| [39m })[33m;[39m
|
|
44
|
-
[90m 90| [39m
|
|
45
|
-
|
|
46
|
-
[2m Test Files [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m3 passed[39m[22m[90m (4)[39m
|
|
47
|
-
[31m[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯[22m[39m
|
|
48
|
-
|
|
49
|
-
[2m Tests [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m88 passed[39m[22m[90m (89)[39m
|
|
50
|
-
[2m Start at [22m 21:53:18
|
|
51
|
-
[2m Duration [22m 639ms[2m (transform 246ms, setup 0ms, collect 323ms, tests 85ms, environment 0ms, prepare 392ms)[22m
|
|
52
|
-
|
|
53
|
-
ELIFECYCLE Test failed. See above for more details.
|