@mono-agent/telegram-adapter 0.3.0 → 0.4.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/README.md CHANGED
@@ -10,6 +10,10 @@ Telegram communication adapter for agent hosts. It provides a Bot API client, lo
10
10
 
11
11
  The adapter is opt-in: `telegram.enabled` / `MONO_AGENT_TELEGRAM_ENABLED` defaults to `false`. While disabled the loader skips credential validation and the channel reports `disabled` rather than `waiting_for_config`. Set `enabled: true` to turn it on; a missing bot token or allowlist then surfaces as a real `waiting_for_config` reason.
12
12
 
13
+ Inbound Telegram document, photo, audio, video, and voice messages are downloaded (subject to the MIME allowlist and ~20 MB cap) and delivered to the responder as transport-agnostic `AgentAttachment` bytes on the shared `AgentRequestBase.attachments` contract (decoded `mimeType` + base64 `data` + `name`). Captions remain the request text; media-only messages still get a concise text summary so existing text-only responder paths can reason about what arrived. The original Telegram file metadata (file id, sizes, kind) is preserved under `metadata.telegram.attachments`.
14
+
15
+ Downloads are gated by the configured allowlist and size cap, and a download failure skips the attachment without failing the run. Whether the model actually consumes the images/documents depends on the host runtime's vision/document support — the adapter forwards bytes but does not itself guarantee model-level understanding.
16
+
13
17
  ## Install / Usage
14
18
 
15
19
  ```bash
@@ -18,10 +22,10 @@ pnpm --filter @mono-agent/telegram-adapter run build
18
22
 
19
23
  ```ts
20
24
  import {
21
- TelegramAdapter,
22
- TelegramBotApiClient,
23
- TelegramLongPoller,
25
+ createTelegramBot,
26
+ createGrammyTelegramApi,
24
27
  loadTelegramAdapterConfig,
28
+ startTelegramAdapter,
25
29
  telegramFieldGroup,
26
30
  } from "@mono-agent/telegram-adapter";
27
31
  ```
@@ -30,16 +34,17 @@ Load adapter settings separately from core config, then pass a structural `Agent
30
34
 
31
35
  ## Public API
32
36
 
33
- - `TelegramAdapter`, `TelegramAdapterOptions`
34
- - `TelegramBotApiClient`, `TelegramApiError`
35
- - `TelegramLongPoller`
36
- - `TelegramMessageStream`, `splitTelegramText`
37
+ - `createTelegramBot`, `startTelegramAdapter`
38
+ - `createGrammyTelegramApi`, `TelegramApiError`
39
+ - `TelegramMessageStream`, `classifyTelegramError`
37
40
  - `loadTelegramAdapterConfig`, `redactTelegramAdapterConfig`, `telegramFieldGroup`
41
+ - `downloadTelegramAttachments` (+ `DownloadTelegramAttachmentsOptions`, `TelegramFileDownloader`): the inbound-bytes flow that maps `TelegramAttachment` metadata to `AgentAttachment` bytes on `request.attachments`
42
+ - `TelegramAttachment` and related Telegram-owned inbound attachment metadata types (preserved under `metadata.telegram.attachments`)
38
43
  - Telegram Bot API, request/response, and config types
39
44
 
40
45
  ## Dependency Boundary
41
46
 
42
- This adapter depends only on shared contracts and settings primitives inside the workspace. It does not depend on the harness, operator console, core config, memory, runtime package, or other adapters. Hosts compose those pieces outside the adapter.
47
+ This adapter depends only on shared `@mono-agent/agent-contracts` primitives inside the workspace. It does not depend on the harness, core config, memory, runtime package, or other adapters. Hosts compose those pieces outside the adapter.
43
48
 
44
49
  ## What This Package Does Not Own
45
50
 
package/dist/adapter.d.ts CHANGED
@@ -1,6 +1,55 @@
1
- import type { AgentRequestBase, AgentResponder as SharedAgentResponder, AgentResponse } from "@mono-agent/agent-contracts";
1
+ import type { AgentAttachment, AgentRequestBase, AgentResponder as SharedAgentResponder, AgentResponse } from "@mono-agent/agent-contracts";
2
2
  import { TelegramMessageStream, type AgentMessageStream, type TelegramMessageStreamLogger } from "./message-stream.js";
3
3
  import type { TelegramChatId, TelegramMessage, TelegramUpdate } from "./types.js";
4
+ export type TelegramAttachmentKind = "document" | "photo" | "audio" | "video" | "voice";
5
+ export interface TelegramAttachmentBase {
6
+ kind: TelegramAttachmentKind;
7
+ fileId: string;
8
+ fileUniqueId: string;
9
+ fileSize?: number;
10
+ }
11
+ export interface TelegramDocumentAttachment extends TelegramAttachmentBase {
12
+ kind: "document";
13
+ fileName?: string;
14
+ mimeType?: string;
15
+ }
16
+ export interface TelegramPhotoAttachmentSize {
17
+ fileId: string;
18
+ fileUniqueId: string;
19
+ width: number;
20
+ height: number;
21
+ fileSize?: number;
22
+ }
23
+ export interface TelegramPhotoAttachment extends TelegramAttachmentBase {
24
+ kind: "photo";
25
+ width: number;
26
+ height: number;
27
+ sizes: readonly TelegramPhotoAttachmentSize[];
28
+ }
29
+ export interface TelegramAudioAttachment extends TelegramAttachmentBase {
30
+ kind: "audio";
31
+ duration: number;
32
+ fileName?: string;
33
+ mimeType?: string;
34
+ }
35
+ export interface TelegramVideoAttachment extends TelegramAttachmentBase {
36
+ kind: "video";
37
+ duration: number;
38
+ width: number;
39
+ height: number;
40
+ fileName?: string;
41
+ mimeType?: string;
42
+ }
43
+ export interface TelegramVoiceAttachment extends TelegramAttachmentBase {
44
+ kind: "voice";
45
+ duration: number;
46
+ mimeType?: string;
47
+ }
48
+ export type TelegramAttachment = TelegramDocumentAttachment | TelegramPhotoAttachment | TelegramAudioAttachment | TelegramVideoAttachment | TelegramVoiceAttachment;
49
+ export interface TelegramAgentMessageInput {
50
+ text: string;
51
+ attachments: readonly TelegramAttachment[];
52
+ }
4
53
  export interface AgentRequest extends AgentRequestBase {
5
54
  conversationId: string;
6
55
  chatId: TelegramChatId;
@@ -9,6 +58,19 @@ export interface AgentRequest extends AgentRequestBase {
9
58
  userId?: number;
10
59
  username?: string;
11
60
  text: string;
61
+ /**
62
+ * Downloaded attachment bytes, ready for a vision/document-aware runtime, in
63
+ * the transport-agnostic {@link AgentAttachment} shape (base64 data + mime +
64
+ * name).
65
+ *
66
+ * BREAKING (intentional, unified contract): on earlier versions this field
67
+ * held Telegram-specific `TelegramAttachment[]` metadata. It is now
68
+ * `AgentAttachment[]` (matching Slack and the OpenAI-compatible channel). The
69
+ * original Telegram file metadata (fileId, sizes, kind, …) is preserved under
70
+ * `metadata.telegram.attachments` — custom responders that filtered by
71
+ * `fileId`/`sizes`/`kind` should read it from there.
72
+ */
73
+ attachments?: readonly AgentAttachment[];
12
74
  abortSignal: AbortSignal;
13
75
  metadata: {
14
76
  telegram: TelegramRequestMetadata;
@@ -27,6 +89,7 @@ export interface TelegramRequestMetadata {
27
89
  id: number;
28
90
  date?: number;
29
91
  };
92
+ attachments?: readonly TelegramAttachment[];
30
93
  from?: {
31
94
  id: number;
32
95
  isBot?: boolean;
@@ -60,7 +123,13 @@ export interface TelegramAdapterStreamOptions {
60
123
  retryCapMs?: number;
61
124
  retryBaseDelayMs?: number;
62
125
  showThoughts?: boolean;
126
+ showHints?: boolean;
63
127
  formatMarkdown?: boolean;
128
+ /**
129
+ * Deliver only the final answer with a "typing…" indicator while working,
130
+ * instead of streaming interim edits. Defaults to true for the Telegram bot.
131
+ */
132
+ finalOnly?: boolean;
64
133
  }
65
134
  export interface TelegramAdapterLogger extends TelegramMessageStreamLogger {
66
135
  info?(message: string, metadata?: Record<string, unknown>): void;
@@ -71,8 +140,65 @@ export declare const DEFAULT_MESSAGES: Required<TelegramAdapterMessages>;
71
140
  * Build the responder-facing {@link AgentRequest} from a Telegram update. The
72
141
  * grammY message handler passes `ctx.update` and `ctx.message`, which are
73
142
  * structurally compatible with the wire types this reads.
143
+ *
144
+ * `resolvedAttachments` are the downloaded {@link AgentAttachment} bytes (when
145
+ * available) that populate `request.attachments`; the original Telegram file
146
+ * metadata is always preserved under `metadata.telegram.attachments`.
147
+ */
148
+ export declare function buildAgentRequest(update: TelegramUpdate, message: TelegramMessage, input: TelegramAgentMessageInput, abortSignal: AbortSignal, resolvedAttachments?: readonly AgentAttachment[]): AgentRequest;
149
+ export declare function normalizeTelegramMessageInput(message: TelegramMessage): TelegramAgentMessageInput | undefined;
150
+ /**
151
+ * Merge a Telegram media-group (album) into a single input: Telegram delivers an
152
+ * album of N photos/videos as N separate messages sharing one `media_group_id`,
153
+ * with the caption on only one of them. We concatenate every message's
154
+ * attachments and take the single caption (first non-empty), so the agent sees
155
+ * all photos as one request instead of N single-attachment turns.
156
+ */
157
+ export declare function mergeTelegramMessageInputs(messages: readonly TelegramMessage[]): TelegramAgentMessageInput | undefined;
158
+ /** Telegram's hard cap for bot file downloads is 20 MB. */
159
+ export declare const DEFAULT_ATTACHMENT_MAX_BYTES: number;
160
+ /**
161
+ * MIME types the adapter will download and inline. Images flow to vision-capable
162
+ * runtimes; documents/text are saved to disk (and decoded inline for text/*).
163
+ */
164
+ export declare const DEFAULT_ATTACHMENT_MIME_ALLOWLIST: readonly string[];
165
+ /**
166
+ * Minimal seam over the Telegram Bot API needed to fetch attachment bytes:
167
+ * resolve a `file_id` to a `file_path` (getFile) then download it from the file
168
+ * URL. Both calls honor the request `abortSignal`.
169
+ */
170
+ export interface TelegramFileDownloader {
171
+ /** Resolve a `file_id` to a downloadable `file_path` (Bot API `getFile`). */
172
+ resolveFilePath(fileId: string, signal: AbortSignal): Promise<string | undefined>;
173
+ /**
174
+ * Download the file at `file_path` (GET on the file URL). `maxBytes`, when
175
+ * provided, lets the downloader abort an oversized transfer mid-stream instead
176
+ * of buffering the whole body first; the caller still re-checks the cap as a
177
+ * backstop, so a custom downloader that ignores `maxBytes` stays bounded.
178
+ */
179
+ download(filePath: string, signal: AbortSignal, maxBytes?: number): Promise<Uint8Array>;
180
+ }
181
+ export interface DownloadTelegramAttachmentsOptions {
182
+ /** Skip files larger than this many decoded bytes. Default ~20 MB. */
183
+ readonly maxBytes?: number;
184
+ /** Only download files whose MIME type is allowed. Defaults to images + common docs/text. */
185
+ readonly mimeAllowlist?: readonly string[];
186
+ /**
187
+ * Per-file download timeout (ms) for the default downloader, composed with the
188
+ * run abort signal. Defaults to 30000. Only consulted by the built-in
189
+ * downloader; custom downloaders manage their own timeouts.
190
+ */
191
+ readonly downloadTimeoutMs?: number;
192
+ readonly logger?: TelegramAdapterLogger;
193
+ }
194
+ /**
195
+ * Download the bytes for each inbound {@link TelegramAttachment} and map them to
196
+ * the transport-agnostic {@link AgentAttachment} shape. Enforces a byte cap and a
197
+ * MIME allowlist, ties every request to `abortSignal`, and skips (never throws on)
198
+ * an attachment whose download fails so the run still proceeds. Photos and audio
199
+ * without a declared MIME type fall back to sensible defaults.
74
200
  */
75
- export declare function buildAgentRequest(update: TelegramUpdate, message: TelegramMessage, text: string, abortSignal: AbortSignal): AgentRequest;
201
+ export declare function downloadTelegramAttachments(attachments: readonly TelegramAttachment[], downloader: TelegramFileDownloader, abortSignal: AbortSignal, options?: DownloadTelegramAttachmentsOptions): Promise<AgentAttachment[]>;
76
202
  /**
77
203
  * Deliver a terminal/system message (cancelled, error, …) in place. Such copy is
78
204
  * fixed text we author, not model output, so it is delivered as plain text
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,IAAI,oBAAoB,EACtC,aAAa,EACd,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EACjC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,cAAc,EAEf,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,YAAa,SAAQ,gBAAgB;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE;QACR,QAAQ,EAAE,uBAAuB,CAAC;QAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QACJ,EAAE,EAAE,cAAc,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED,YAAY,EAAE,aAAa,EAAE,CAAC;AAC9B,MAAM,MAAM,cAAc,GAAG,oBAAoB,CAAC,YAAY,EAAE,kBAAkB,EAAE,aAAa,CAAC,CAAC;AAEnG,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,wBAAwB,GAChC,MAAM,GACN,CAAC,CAAC,KAAK,EAAE,6BAA6B,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAsB,SAAQ,2BAA2B;IACxE,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAClE;AAED,eAAO,MAAM,kBAAkB,oDAAoD,CAAC;AAEpF,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,uBAAuB,CAU9D,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,GACvB,YAAY,CA6Bd;AAoDD;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,qBAAqB,GAAG,SAAS,GACxC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC5C,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACpD,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBlB"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -1,36 +1,47 @@
1
1
  import { TelegramMessageStream, } from "./message-stream.js";
2
2
  export const DEFAULT_ERROR_TEXT = "The agent failed while processing your message.";
3
3
  export const DEFAULT_MESSAGES = {
4
- welcomeText: "Hello! Send me a text message and I will pass it to the configured agent.",
5
- helpText: "Send a text message to talk to the agent. Use /cancel to stop the current response.",
4
+ welcomeText: "Hello! Send text or Telegram media. I pass your caption and download allowed attachments to share with the configured agent.",
5
+ helpText: "Send text, documents, photos, audio, video, or voice messages. I forward your caption and download supported attachments (within size/type limits) for the agent. Use /cancel to stop the current response.",
6
6
  busyText: "I am still working on your previous message. Use /cancel to stop it.",
7
7
  unauthorizedText: "This Telegram chat is not authorized to use this bot.",
8
8
  cancelledText: "Cancelled.",
9
9
  errorText: DEFAULT_ERROR_TEXT,
10
- unsupportedText: "I can only handle text messages in this adapter for now.",
10
+ unsupportedText: "I can handle text and Telegram document, photo, audio, video, or voice metadata in this adapter.",
11
11
  };
12
12
  /**
13
13
  * Build the responder-facing {@link AgentRequest} from a Telegram update. The
14
14
  * grammY message handler passes `ctx.update` and `ctx.message`, which are
15
15
  * structurally compatible with the wire types this reads.
16
+ *
17
+ * `resolvedAttachments` are the downloaded {@link AgentAttachment} bytes (when
18
+ * available) that populate `request.attachments`; the original Telegram file
19
+ * metadata is always preserved under `metadata.telegram.attachments`.
16
20
  */
17
- export function buildAgentRequest(update, message, text, abortSignal) {
21
+ export function buildAgentRequest(update, message, input, abortSignal, resolvedAttachments) {
18
22
  const from = metadataFromUser(message.from);
23
+ const telegramMetadata = {
24
+ updateId: update.update_id,
25
+ chat: metadataFromChat(message.chat),
26
+ message: metadataFromMessage(message),
27
+ };
28
+ if (input.attachments.length > 0) {
29
+ telegramMetadata.attachments = input.attachments;
30
+ }
19
31
  const request = {
20
32
  conversationId: `telegram:${String(message.chat.id)}`,
21
33
  chatId: message.chat.id,
22
34
  messageId: message.message_id,
23
35
  updateId: update.update_id,
24
- text,
36
+ text: input.text,
25
37
  abortSignal,
26
38
  metadata: {
27
- telegram: {
28
- updateId: update.update_id,
29
- chat: metadataFromChat(message.chat),
30
- message: metadataFromMessage(message),
31
- },
39
+ telegram: telegramMetadata,
32
40
  },
33
41
  };
42
+ if (resolvedAttachments !== undefined && resolvedAttachments.length > 0) {
43
+ request.attachments = resolvedAttachments;
44
+ }
34
45
  if (message.from?.id !== undefined) {
35
46
  request.userId = message.from.id;
36
47
  }
@@ -42,6 +53,50 @@ export function buildAgentRequest(update, message, text, abortSignal) {
42
53
  }
43
54
  return request;
44
55
  }
56
+ export function normalizeTelegramMessageInput(message) {
57
+ if (message.animation !== undefined) {
58
+ return undefined;
59
+ }
60
+ const text = normalizeMessageText(message);
61
+ const attachments = extractTelegramAttachments(message);
62
+ if (text.length === 0 && attachments.length === 0) {
63
+ return undefined;
64
+ }
65
+ return {
66
+ text: text.length > 0 ? text : summarizeTelegramAttachments(attachments),
67
+ attachments,
68
+ };
69
+ }
70
+ /**
71
+ * Merge a Telegram media-group (album) into a single input: Telegram delivers an
72
+ * album of N photos/videos as N separate messages sharing one `media_group_id`,
73
+ * with the caption on only one of them. We concatenate every message's
74
+ * attachments and take the single caption (first non-empty), so the agent sees
75
+ * all photos as one request instead of N single-attachment turns.
76
+ */
77
+ export function mergeTelegramMessageInputs(messages) {
78
+ const attachments = [];
79
+ let text = "";
80
+ for (const message of messages) {
81
+ if (message.animation !== undefined) {
82
+ continue;
83
+ }
84
+ if (text.length === 0) {
85
+ const messageText = normalizeMessageText(message);
86
+ if (messageText.length > 0) {
87
+ text = messageText;
88
+ }
89
+ }
90
+ attachments.push(...extractTelegramAttachments(message));
91
+ }
92
+ if (text.length === 0 && attachments.length === 0) {
93
+ return undefined;
94
+ }
95
+ return {
96
+ text: text.length > 0 ? text : summarizeTelegramAttachments(attachments),
97
+ attachments,
98
+ };
99
+ }
45
100
  function metadataFromChat(messageChat) {
46
101
  const chat = { id: messageChat.id };
47
102
  if (messageChat.type !== undefined) {
@@ -62,6 +117,328 @@ function metadataFromMessage(message) {
62
117
  }
63
118
  return metadata;
64
119
  }
120
+ function normalizeMessageText(message) {
121
+ return (message.text ?? message.caption ?? "").trim();
122
+ }
123
+ function extractTelegramAttachments(message) {
124
+ const attachments = [];
125
+ const document = attachmentFromDocument(message.document);
126
+ if (document !== undefined) {
127
+ attachments.push(document);
128
+ }
129
+ const photo = attachmentFromPhoto(message.photo);
130
+ if (photo !== undefined) {
131
+ attachments.push(photo);
132
+ }
133
+ const audio = attachmentFromAudio(message.audio);
134
+ if (audio !== undefined) {
135
+ attachments.push(audio);
136
+ }
137
+ const video = attachmentFromVideo(message.video);
138
+ if (video !== undefined) {
139
+ attachments.push(video);
140
+ }
141
+ const voice = attachmentFromVoice(message.voice);
142
+ if (voice !== undefined) {
143
+ attachments.push(voice);
144
+ }
145
+ return attachments;
146
+ }
147
+ function attachmentFromDocument(document) {
148
+ if (document === undefined) {
149
+ return undefined;
150
+ }
151
+ const attachment = {
152
+ kind: "document",
153
+ fileId: document.file_id,
154
+ fileUniqueId: document.file_unique_id,
155
+ };
156
+ addFileSize(attachment, document.file_size);
157
+ if (document.file_name !== undefined) {
158
+ attachment.fileName = document.file_name;
159
+ }
160
+ if (document.mime_type !== undefined) {
161
+ attachment.mimeType = document.mime_type;
162
+ }
163
+ return attachment;
164
+ }
165
+ function attachmentFromPhoto(photos) {
166
+ const sizes = photos?.map(photoSizeFromTelegram).filter(isDefined) ?? [];
167
+ if (sizes.length === 0) {
168
+ return undefined;
169
+ }
170
+ const largest = sizes.reduce((best, candidate) => candidate.width * candidate.height > best.width * best.height ? candidate : best);
171
+ const attachment = {
172
+ kind: "photo",
173
+ fileId: largest.fileId,
174
+ fileUniqueId: largest.fileUniqueId,
175
+ width: largest.width,
176
+ height: largest.height,
177
+ sizes,
178
+ };
179
+ addFileSize(attachment, largest.fileSize);
180
+ return attachment;
181
+ }
182
+ function attachmentFromAudio(audio) {
183
+ if (audio === undefined) {
184
+ return undefined;
185
+ }
186
+ const attachment = {
187
+ kind: "audio",
188
+ fileId: audio.file_id,
189
+ fileUniqueId: audio.file_unique_id,
190
+ duration: audio.duration,
191
+ };
192
+ addFileSize(attachment, audio.file_size);
193
+ if (audio.file_name !== undefined) {
194
+ attachment.fileName = audio.file_name;
195
+ }
196
+ if (audio.mime_type !== undefined) {
197
+ attachment.mimeType = audio.mime_type;
198
+ }
199
+ return attachment;
200
+ }
201
+ function attachmentFromVideo(video) {
202
+ if (video === undefined) {
203
+ return undefined;
204
+ }
205
+ const attachment = {
206
+ kind: "video",
207
+ fileId: video.file_id,
208
+ fileUniqueId: video.file_unique_id,
209
+ duration: video.duration,
210
+ width: video.width,
211
+ height: video.height,
212
+ };
213
+ addFileSize(attachment, video.file_size);
214
+ if (video.file_name !== undefined) {
215
+ attachment.fileName = video.file_name;
216
+ }
217
+ if (video.mime_type !== undefined) {
218
+ attachment.mimeType = video.mime_type;
219
+ }
220
+ return attachment;
221
+ }
222
+ function attachmentFromVoice(voice) {
223
+ if (voice === undefined) {
224
+ return undefined;
225
+ }
226
+ const attachment = {
227
+ kind: "voice",
228
+ fileId: voice.file_id,
229
+ fileUniqueId: voice.file_unique_id,
230
+ duration: voice.duration,
231
+ };
232
+ addFileSize(attachment, voice.file_size);
233
+ if (voice.mime_type !== undefined) {
234
+ attachment.mimeType = voice.mime_type;
235
+ }
236
+ return attachment;
237
+ }
238
+ function photoSizeFromTelegram(size) {
239
+ const attachmentSize = {
240
+ fileId: size.file_id,
241
+ fileUniqueId: size.file_unique_id,
242
+ width: size.width,
243
+ height: size.height,
244
+ };
245
+ addFileSize(attachmentSize, size.file_size);
246
+ return attachmentSize;
247
+ }
248
+ function addFileSize(target, fileSize) {
249
+ if (typeof fileSize === "number" && Number.isFinite(fileSize)) {
250
+ target.fileSize = fileSize;
251
+ }
252
+ }
253
+ function isDefined(value) {
254
+ return value !== undefined;
255
+ }
256
+ function summarizeTelegramAttachments(attachments) {
257
+ return attachments.map(describeTelegramAttachment).join("\n");
258
+ }
259
+ function describeTelegramAttachment(attachment) {
260
+ const details = attachmentDetails(attachment);
261
+ return details.length === 0
262
+ ? `Telegram ${attachment.kind}`
263
+ : `Telegram ${attachment.kind}: ${details.join(", ")}`;
264
+ }
265
+ function attachmentDetails(attachment) {
266
+ const details = [];
267
+ if ("fileName" in attachment && attachment.fileName !== undefined) {
268
+ details.push(attachment.fileName);
269
+ }
270
+ if ("mimeType" in attachment && attachment.mimeType !== undefined) {
271
+ details.push(attachment.mimeType);
272
+ }
273
+ if ("width" in attachment && "height" in attachment) {
274
+ details.push(`${attachment.width}x${attachment.height}`);
275
+ }
276
+ if ("duration" in attachment) {
277
+ details.push(`${attachment.duration}s`);
278
+ }
279
+ if (attachment.fileSize !== undefined) {
280
+ details.push(formatBytes(attachment.fileSize));
281
+ }
282
+ return details;
283
+ }
284
+ function formatBytes(bytes) {
285
+ if (bytes < 1024) {
286
+ return `${bytes} B`;
287
+ }
288
+ if (bytes < 1024 * 1024) {
289
+ return `${Math.round(bytes / 1024)} KB`;
290
+ }
291
+ return `${Math.round(bytes / (1024 * 1024))} MB`;
292
+ }
293
+ /** Telegram's hard cap for bot file downloads is 20 MB. */
294
+ export const DEFAULT_ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
295
+ /**
296
+ * MIME types the adapter will download and inline. Images flow to vision-capable
297
+ * runtimes; documents/text are saved to disk (and decoded inline for text/*).
298
+ */
299
+ export const DEFAULT_ATTACHMENT_MIME_ALLOWLIST = [
300
+ "image/png",
301
+ "image/jpeg",
302
+ "image/gif",
303
+ "image/webp",
304
+ "application/pdf",
305
+ "application/json",
306
+ "application/zip",
307
+ "application/msword",
308
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
309
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
310
+ "application/vnd.ms-excel",
311
+ "text/plain",
312
+ "text/markdown",
313
+ "text/csv",
314
+ "text/html",
315
+ // Audio (Telegram voice messages normalize to audio/ogg; see attachmentMimeType).
316
+ "audio/ogg",
317
+ "audio/mpeg",
318
+ "audio/mp4",
319
+ "audio/aac",
320
+ "audio/wav",
321
+ "audio/webm",
322
+ "audio/flac",
323
+ // Video.
324
+ "video/mp4",
325
+ "video/mpeg",
326
+ "video/quicktime",
327
+ "video/webm",
328
+ ];
329
+ /**
330
+ * Download the bytes for each inbound {@link TelegramAttachment} and map them to
331
+ * the transport-agnostic {@link AgentAttachment} shape. Enforces a byte cap and a
332
+ * MIME allowlist, ties every request to `abortSignal`, and skips (never throws on)
333
+ * an attachment whose download fails so the run still proceeds. Photos and audio
334
+ * without a declared MIME type fall back to sensible defaults.
335
+ */
336
+ export async function downloadTelegramAttachments(attachments, downloader, abortSignal, options) {
337
+ const maxBytes = options?.maxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES;
338
+ const allowlist = new Set((options?.mimeAllowlist ?? DEFAULT_ATTACHMENT_MIME_ALLOWLIST).map((mime) => mime.toLowerCase()));
339
+ const logger = options?.logger;
340
+ const resolved = [];
341
+ for (const attachment of attachments) {
342
+ if (abortSignal.aborted) {
343
+ break;
344
+ }
345
+ const source = attachmentSource(attachment);
346
+ const mimeType = source.mimeType.toLowerCase();
347
+ if (!allowlist.has(mimeType)) {
348
+ logger?.debug?.("Skipping Telegram attachment with disallowed MIME type.", {
349
+ mimeType: source.mimeType,
350
+ name: source.name,
351
+ });
352
+ continue;
353
+ }
354
+ if (source.declaredSize !== undefined && source.declaredSize > maxBytes) {
355
+ logger?.debug?.("Skipping oversized Telegram attachment.", {
356
+ sizeBytes: source.declaredSize,
357
+ maxBytes,
358
+ name: source.name,
359
+ });
360
+ continue;
361
+ }
362
+ try {
363
+ const filePath = await downloader.resolveFilePath(source.fileId, abortSignal);
364
+ if (filePath === undefined) {
365
+ logger?.warn?.("Telegram getFile returned no file_path; skipping attachment.", {
366
+ fileId: source.fileId,
367
+ name: source.name,
368
+ });
369
+ continue;
370
+ }
371
+ const bytes = await downloader.download(filePath, abortSignal, maxBytes);
372
+ if (bytes.byteLength > maxBytes) {
373
+ logger?.warn?.("Telegram attachment exceeded the size cap after download; skipping.", {
374
+ sizeBytes: bytes.byteLength,
375
+ maxBytes,
376
+ name: source.name,
377
+ });
378
+ continue;
379
+ }
380
+ resolved.push(buildAgentAttachment(source, mimeType, bytes));
381
+ }
382
+ catch (error) {
383
+ // Download failures never fail the run — skip the attachment and continue.
384
+ logger?.warn?.("Failed to download Telegram attachment; skipping it.", {
385
+ fileId: source.fileId,
386
+ name: source.name,
387
+ error: error instanceof Error ? error.message : String(error),
388
+ });
389
+ }
390
+ }
391
+ return resolved;
392
+ }
393
+ function buildAgentAttachment(source, mimeType, bytes) {
394
+ const kind = mimeType.startsWith("image/") ? "image" : "document";
395
+ const attachment = {
396
+ kind,
397
+ mimeType: source.mimeType,
398
+ data: Buffer.from(bytes).toString("base64"),
399
+ sizeBytes: bytes.byteLength,
400
+ };
401
+ if (source.name !== undefined) {
402
+ attachment.name = source.name;
403
+ }
404
+ if (source.durationSeconds !== undefined) {
405
+ attachment.durationSeconds = source.durationSeconds;
406
+ }
407
+ if (mimeType.startsWith("text/")) {
408
+ attachment.text = Buffer.from(bytes).toString("utf8");
409
+ }
410
+ return attachment;
411
+ }
412
+ function attachmentSource(attachment) {
413
+ const name = "fileName" in attachment ? attachment.fileName : undefined;
414
+ return {
415
+ fileId: attachment.fileId,
416
+ mimeType: attachmentMimeType(attachment),
417
+ name,
418
+ declaredSize: attachment.fileSize,
419
+ durationSeconds: "duration" in attachment ? attachment.duration : undefined,
420
+ };
421
+ }
422
+ function attachmentMimeType(attachment) {
423
+ if ("mimeType" in attachment && attachment.mimeType !== undefined) {
424
+ return attachment.mimeType;
425
+ }
426
+ if (attachment.kind === "photo") {
427
+ return "image/jpeg";
428
+ }
429
+ if (attachment.kind === "voice") {
430
+ return "audio/ogg";
431
+ }
432
+ // Telegram may omit mime_type on audio/video; fall back to a sensible default
433
+ // on the allowlist so the attachment is not skipped as application/octet-stream.
434
+ if (attachment.kind === "audio") {
435
+ return "audio/mpeg";
436
+ }
437
+ if (attachment.kind === "video") {
438
+ return "video/mp4";
439
+ }
440
+ return "application/octet-stream";
441
+ }
65
442
  function metadataFromUser(user) {
66
443
  if (user === undefined) {
67
444
  return undefined;