@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/messaging@0.6.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
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
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 46.08 KB
11
- ESM ⚡️ Build success in 161ms
10
+ ESM dist/index.js 51.09 KB
11
+ ESM ⚡️ Build success in 67ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 2515ms
14
- DTS dist/index.d.ts 10.53 KB
13
+ DTS ⚡️ Build success in 5004ms
14
+ DTS dist/index.d.ts 11.24 KB
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: string;
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 = "Continue";
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.6.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.5.0"
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 = "Continue";
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 { TelegramAdapterOptions } from "./adapters/telegram/index.js";
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
@@ -117,7 +117,7 @@ export interface AgentRunner {
117
117
  run(
118
118
  conversationId: string,
119
119
  input: {
120
- task: string;
120
+ task?: string;
121
121
  messages: Message[];
122
122
  files?: FileAttachment[];
123
123
  metadata?: {
@@ -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
-  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
7
-
8
- ✓ test/adapters/resend.test.ts  (13 tests) 4ms
9
- stderr | test/bridge.test.ts > AgentBridge > posts an error message and cleans up on runner failure
10
- [agent-bridge] handleMessage error: Model overloaded
11
-
12
- stderr | test/bridge.test.ts > AgentBridge > skips sendReply when autoReply is false
13
- [agent-bridge] tool mode completed without send_email being called; no reply sent
14
-
15
- stderr | test/bridge.test.ts > AgentBridge > suppresses error reply when hasSentInCurrentRequest is true
16
- [agent-bridge] handleMessage error: Oops
17
-
18
- stderr | test/bridge.test.ts > AgentBridge > calls resetRequestState before handling each message
19
- ❯ test/bridge.test.ts  (15 tests | 1 failed) 11ms
20
-  ❯ test/bridge.test.ts > AgentBridge > derives deterministic conversation IDs from ThreadRef
21
- [agent-bridge] tool mode completed without send_email being called; no reply sent
22
-  → expected 'c61bdca0-71e1-4785-869c-5552b90054c5' to be 'test:C001:ts_123' // Object.is equality
23
-
24
- ✓ test/adapters/email-utils.test.ts  (44 tests) 7ms
25
- ✓ test/adapters/slack.test.ts  (17 tests) 63ms
26
-
27
- ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
28
-
29
-  FAIL  test/bridge.test.ts > AgentBridge > derives deterministic conversation IDs from ThreadRef
30
- AssertionError: expected 'c61bdca0-71e1-4785-869c-5552b90054c5' to be 'test:C001:ts_123' // Object.is equality
31
-
32
- - Expected
33
- + Received
34
-
35
- - test:C001:ts_123
36
- + c61bdca0-71e1-4785-869c-5552b90054c5
37
-
38
-  ❯ test/bridge.test.ts:88:33
39
-  86| 
40
-  87|  expect(runner._runs).toHaveLength(1);
41
-  88|  expect(runner._runs[0]!.id).toBe("test:C001:ts_123");
42
-  |  ^
43
-  89|  });
44
-  90| 
45
-
46
-  Test Files  1 failed | 3 passed (4)
47
- ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
48
-
49
-  Tests  1 failed | 88 passed (89)
50
-  Start at  21:53:18
51
-  Duration  639ms (transform 246ms, setup 0ms, collect 323ms, tests 85ms, environment 0ms, prepare 392ms)
52
-
53
-  ELIFECYCLE  Test failed. See above for more details.