@copilotkit/channels-telegram 0.0.3

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/adapter.d.ts +117 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +584 -0
  6. package/dist/built-in-context.d.ts +22 -0
  7. package/dist/built-in-context.d.ts.map +1 -0
  8. package/dist/built-in-context.js +77 -0
  9. package/dist/built-in-tools.d.ts +23 -0
  10. package/dist/built-in-tools.d.ts.map +1 -0
  11. package/dist/built-in-tools.js +46 -0
  12. package/dist/chunked-edit-stream.d.ts +70 -0
  13. package/dist/chunked-edit-stream.d.ts.map +1 -0
  14. package/dist/chunked-edit-stream.js +197 -0
  15. package/dist/conversation-store.d.ts +56 -0
  16. package/dist/conversation-store.d.ts.map +1 -0
  17. package/dist/conversation-store.js +98 -0
  18. package/dist/download-files.d.ts +68 -0
  19. package/dist/download-files.d.ts.map +1 -0
  20. package/dist/download-files.js +157 -0
  21. package/dist/event-renderer.d.ts +39 -0
  22. package/dist/event-renderer.d.ts.map +1 -0
  23. package/dist/event-renderer.js +295 -0
  24. package/dist/format-fallback.d.ts +6 -0
  25. package/dist/format-fallback.d.ts.map +1 -0
  26. package/dist/format-fallback.js +21 -0
  27. package/dist/index.d.ts +20 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/interaction.d.ts +65 -0
  31. package/dist/interaction.d.ts.map +1 -0
  32. package/dist/interaction.js +159 -0
  33. package/dist/listener.d.ts +16 -0
  34. package/dist/listener.d.ts.map +1 -0
  35. package/dist/listener.js +419 -0
  36. package/dist/render/budget.d.ts +18 -0
  37. package/dist/render/budget.d.ts.map +1 -0
  38. package/dist/render/budget.js +24 -0
  39. package/dist/render/telegram.d.ts +11 -0
  40. package/dist/render/telegram.d.ts.map +1 -0
  41. package/dist/render/telegram.js +429 -0
  42. package/dist/telegram-html.d.ts +22 -0
  43. package/dist/telegram-html.d.ts.map +1 -0
  44. package/dist/telegram-html.js +98 -0
  45. package/dist/types.d.ts +83 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +2 -0
  48. package/package.json +57 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Telegram-platform-universal frontend tools — tools every Telegram bot
3
+ * benefits from, regardless of what the bot does. Apps spread
4
+ * `defaultTelegramTools` into the `tools:` config they pass to
5
+ * `createBot`.
6
+ */
7
+ import { z } from "zod";
8
+ import type { BotTool } from "@copilotkit/channels";
9
+ export declare const lookupTelegramUserTool: BotTool<z.ZodObject<{
10
+ query: z.ZodString;
11
+ }, "strip", z.ZodTypeAny, {
12
+ query: string;
13
+ }, {
14
+ query: string;
15
+ }>>;
16
+ /**
17
+ * The flat list of tools the SDK ships. Spread into your
18
+ * `createBot({tools: …})`:
19
+ *
20
+ * tools: [...defaultTelegramTools, ...myAppTools],
21
+ */
22
+ export declare const defaultTelegramTools: ReadonlyArray<BotTool>;
23
+ //# sourceMappingURL=built-in-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"built-in-tools.d.ts","sourceRoot":"","sources":["../src/built-in-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AASpD,eAAO,MAAM,sBAAsB;;;;;;GAuBjC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,OAAO,CAEvD,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Telegram-platform-universal frontend tools — tools every Telegram bot
3
+ * benefits from, regardless of what the bot does. Apps spread
4
+ * `defaultTelegramTools` into the `tools:` config they pass to
5
+ * `createBot`.
6
+ */
7
+ import { z } from "zod";
8
+ import { defineBotTool } from "@copilotkit/channels";
9
+ const lookupSchema = z.object({
10
+ query: z
11
+ .string()
12
+ .min(1)
13
+ .describe("Handle, display name, or first name of the person to look up."),
14
+ });
15
+ export const lookupTelegramUserTool = defineBotTool({
16
+ name: "lookup_telegram_user",
17
+ description: "Resolve a person to a Telegram user ID so you can mention them. " +
18
+ "Accepts a handle (`ada`), display name (`Ada Lovelace`), or first name. " +
19
+ "Returns an object with `found`, and on success a " +
20
+ "`mention` string (e.g. `@ada` or `tg://user?id=7`) — put that string verbatim " +
21
+ "in your reply to ping them. If `found` is false, write the plain " +
22
+ "name instead.",
23
+ parameters: lookupSchema,
24
+ async handler({ query }, { thread }) {
25
+ const u = await thread.lookupUser(query);
26
+ return u
27
+ ? {
28
+ found: true,
29
+ query,
30
+ userId: u.id,
31
+ name: u.name,
32
+ handle: u.handle,
33
+ mention: u.handle ? "@" + u.handle : "tg://user?id=" + u.id,
34
+ }
35
+ : { found: false, query };
36
+ },
37
+ });
38
+ /**
39
+ * The flat list of tools the SDK ships. Spread into your
40
+ * `createBot({tools: …})`:
41
+ *
42
+ * tools: [...defaultTelegramTools, ...myAppTools],
43
+ */
44
+ export const defaultTelegramTools = [
45
+ lookupTelegramUserTool,
46
+ ];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Per-Telegram-message streaming text state machine.
3
+ *
4
+ * Mirrors the Slack MessageStream + ChunkedMessageStream pattern but uses
5
+ * numeric Telegram message IDs instead of Slack `ts` strings.
6
+ *
7
+ * ChunkedEditStream owns N underlying EditStream instances (one per Telegram
8
+ * message). It accepts the full accumulated text on `append()`, decides where
9
+ * the chunk boundaries are (frozen once a message has been posted — no reflow),
10
+ * mints new Telegram messages via `postPlaceholder` as needed, and dispatches
11
+ * the right slice to each chunk's stream.
12
+ *
13
+ * `transform` (e.g. telegramHtml) is applied just before every `editAt` call.
14
+ */
15
+ export interface ChunkedEditStreamConfig {
16
+ /**
17
+ * Soft per-message raw char limit. Defaults to Math.floor(TELEGRAM_LIMITS.messageText / 2).
18
+ * The headroom below the 4096 hard cap exists because `transform` (telegramHtml) can
19
+ * expand raw chars significantly: `&`→`&amp;` (5×), `<`/`>`→`&lt;`/`&gt;` (4×),
20
+ * bold/link markdown → HTML tags. Halving the limit is a risk-reduction heuristic that
21
+ * keeps the HTML-transformed slice under 4096 for typical prose/markdown content (which
22
+ * expands modestly); it is NOT a hard guarantee against adversarial all-entity input
23
+ * (e.g. 2048 raw `&` chars → ~10 240 transformed chars).
24
+ */
25
+ limit?: number;
26
+ /** Throttle floor (ms) between consecutive edits per message. Defaults to 1000. */
27
+ minIntervalMs?: number;
28
+ /** Posts a new Telegram message with placeholder text; resolves with the message id. */
29
+ postPlaceholder: (text: string) => Promise<number>;
30
+ /** Edits the Telegram message with the given id to contain `text`. */
31
+ editAt: (messageId: number, text: string) => Promise<void>;
32
+ /**
33
+ * Optional transformer applied to each chunk's text before `editAt` /
34
+ * `postPlaceholder` — e.g. telegramHtml markdown→HTML conversion.
35
+ */
36
+ transform?: (text: string) => string;
37
+ }
38
+ export declare class ChunkedEditStream {
39
+ private buffer;
40
+ /** Sorted character positions where a chunk ends (= where the next begins). */
41
+ private boundaries;
42
+ private streams;
43
+ /** Serialises new-chunk creation so async postPlaceholder calls don't race. */
44
+ private setupPromise;
45
+ private finished;
46
+ private readonly limit;
47
+ private readonly minIntervalMs;
48
+ private readonly postPlaceholder;
49
+ private readonly editAt;
50
+ private readonly transform;
51
+ constructor(config: ChunkedEditStreamConfig);
52
+ /**
53
+ * Feed the FULL accumulated text so far. The stream will work out which
54
+ * slice belongs to which Telegram message and throttle edits accordingly.
55
+ */
56
+ append(fullText: string): void;
57
+ /** Flush all pending edits and wait until every message reflects its final text. */
58
+ finish(): Promise<void>;
59
+ /** Number of Telegram messages posted so far. */
60
+ get chunkCount(): number;
61
+ /**
62
+ * Walk forward from the last frozen boundary, freezing new ones whenever
63
+ * the active chunk's length exceeds the soft limit. Choose the break point
64
+ * at the last newline (or last space) within the window; fall back to a hard
65
+ * cut if neither is found. Boundaries never move once frozen.
66
+ */
67
+ private refreezeBoundaries;
68
+ private ensureStreamsAndDispatch;
69
+ }
70
+ //# sourceMappingURL=chunked-edit-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunked-edit-stream.d.ts","sourceRoot":"","sources":["../src/chunked-edit-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,sEAAsE;IACtE,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AA+FD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAM;IACpB,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAAoB;IACnC,+EAA+E;IAC/E,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqD;IAC5E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;gBAEzC,MAAM,EAAE,uBAAuB;IAQ3C;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAU9B,oFAAoF;IAC9E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAS7B,iDAAiD;IACjD,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;YAgBZ,wBAAwB;CAsCvC"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Per-Telegram-message streaming text state machine.
3
+ *
4
+ * Mirrors the Slack MessageStream + ChunkedMessageStream pattern but uses
5
+ * numeric Telegram message IDs instead of Slack `ts` strings.
6
+ *
7
+ * ChunkedEditStream owns N underlying EditStream instances (one per Telegram
8
+ * message). It accepts the full accumulated text on `append()`, decides where
9
+ * the chunk boundaries are (frozen once a message has been posted — no reflow),
10
+ * mints new Telegram messages via `postPlaceholder` as needed, and dispatches
11
+ * the right slice to each chunk's stream.
12
+ *
13
+ * `transform` (e.g. telegramHtml) is applied just before every `editAt` call.
14
+ */
15
+ import { TELEGRAM_LIMITS } from "./render/budget.js";
16
+ /**
17
+ * Default raw-char limit per chunk. Set to half of Telegram's 4096 hard cap as a
18
+ * generous headroom heuristic: typical prose/markdown content expands modestly under
19
+ * telegramHtml, so this keeps the rendered output well under 4096 in practice.
20
+ * It is NOT a hard guarantee — adversarial all-entity input can still exceed the cap.
21
+ */
22
+ const DEFAULT_LIMIT = Math.floor(TELEGRAM_LIMITS.messageText / 2); // 2048
23
+ const DEFAULT_MIN_INTERVAL_MS = 1000;
24
+ class EditStream {
25
+ buffer = "";
26
+ posted = "";
27
+ queue = Promise.resolve();
28
+ lastFlushedAt = 0;
29
+ flushTimer;
30
+ minIntervalMs;
31
+ update;
32
+ /** Non-null when the terminal flush (from finish()) failed. */
33
+ terminalError = undefined;
34
+ constructor(config) {
35
+ this.update = config.update;
36
+ this.minIntervalMs = config.minIntervalMs;
37
+ }
38
+ append(text) {
39
+ if (text === this.buffer)
40
+ return;
41
+ this.buffer = text;
42
+ this.scheduleFlush();
43
+ }
44
+ async finish() {
45
+ if (this.flushTimer) {
46
+ clearTimeout(this.flushTimer);
47
+ this.flushTimer = undefined;
48
+ }
49
+ this.enqueueFlush(/* terminal */ true);
50
+ await this.queue;
51
+ if (this.terminalError !== undefined) {
52
+ throw this.terminalError;
53
+ }
54
+ }
55
+ scheduleFlush() {
56
+ if (this.flushTimer)
57
+ return;
58
+ const elapsed = Date.now() - this.lastFlushedAt;
59
+ const delay = Math.max(0, this.minIntervalMs - elapsed);
60
+ this.flushTimer = setTimeout(() => {
61
+ this.flushTimer = undefined;
62
+ this.enqueueFlush(false);
63
+ }, delay);
64
+ }
65
+ enqueueFlush(terminal) {
66
+ this.queue = this.queue.then(() => this.flushNow(terminal));
67
+ }
68
+ async flushNow(terminal) {
69
+ if (this.buffer === this.posted)
70
+ return;
71
+ const text = this.buffer;
72
+ try {
73
+ await this.update(text);
74
+ // Only advance posted after a successful edit so a transient failure
75
+ // leaves the text eligible for retry on the next flush.
76
+ this.posted = text;
77
+ }
78
+ catch (err) {
79
+ if (terminal) {
80
+ // Surface terminal-flush failures to finish() so the caller can react.
81
+ this.terminalError = err;
82
+ }
83
+ else {
84
+ // Intermediate flush: log but leave posted unchanged so the next
85
+ // flush will re-attempt the current buffer.
86
+ console.error("[chunked-edit-stream] editAt failed:", err);
87
+ }
88
+ }
89
+ finally {
90
+ this.lastFlushedAt = Date.now();
91
+ }
92
+ }
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // ChunkedEditStream
96
+ // ---------------------------------------------------------------------------
97
+ export class ChunkedEditStream {
98
+ buffer = "";
99
+ /** Sorted character positions where a chunk ends (= where the next begins). */
100
+ boundaries = [];
101
+ streams = [];
102
+ /** Serialises new-chunk creation so async postPlaceholder calls don't race. */
103
+ setupPromise = Promise.resolve();
104
+ finished = false;
105
+ limit;
106
+ minIntervalMs;
107
+ postPlaceholder;
108
+ editAt;
109
+ transform;
110
+ constructor(config) {
111
+ this.limit = config.limit ?? DEFAULT_LIMIT;
112
+ this.minIntervalMs = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
113
+ this.postPlaceholder = config.postPlaceholder;
114
+ this.editAt = config.editAt;
115
+ this.transform = config.transform ?? ((t) => t);
116
+ }
117
+ /**
118
+ * Feed the FULL accumulated text so far. The stream will work out which
119
+ * slice belongs to which Telegram message and throttle edits accordingly.
120
+ */
121
+ append(fullText) {
122
+ if (this.finished)
123
+ return;
124
+ if (fullText === this.buffer)
125
+ return;
126
+ this.buffer = fullText;
127
+ this.refreezeBoundaries();
128
+ this.setupPromise = this.setupPromise.then(() => this.ensureStreamsAndDispatch());
129
+ }
130
+ /** Flush all pending edits and wait until every message reflects its final text. */
131
+ async finish() {
132
+ this.finished = true;
133
+ this.setupPromise = this.setupPromise.then(() => this.ensureStreamsAndDispatch());
134
+ await this.setupPromise;
135
+ for (const s of this.streams)
136
+ await s.finish();
137
+ }
138
+ /** Number of Telegram messages posted so far. */
139
+ get chunkCount() {
140
+ return this.streams.length;
141
+ }
142
+ /**
143
+ * Walk forward from the last frozen boundary, freezing new ones whenever
144
+ * the active chunk's length exceeds the soft limit. Choose the break point
145
+ * at the last newline (or last space) within the window; fall back to a hard
146
+ * cut if neither is found. Boundaries never move once frozen.
147
+ */
148
+ refreezeBoundaries() {
149
+ const minAdvance = Math.max(1, Math.floor(this.limit / 2));
150
+ let lastFrozen = this.boundaries.at(-1) ?? 0;
151
+ while (this.buffer.length - lastFrozen > this.limit) {
152
+ const window = this.buffer.slice(lastFrozen, lastFrozen + this.limit);
153
+ let breakAt = window.lastIndexOf("\n");
154
+ if (breakAt < this.limit / 4)
155
+ breakAt = window.lastIndexOf(" ");
156
+ // Bug 1 fix: enforce a minimum advance floor so adversarial input
157
+ // (e.g. leading spaces / early newlines) doesn't produce ~1-char chunks.
158
+ if (breakAt < minAdvance)
159
+ breakAt = this.limit - 1;
160
+ const candidate = lastFrozen + breakAt + 1;
161
+ this.boundaries.push(candidate);
162
+ lastFrozen = candidate;
163
+ }
164
+ }
165
+ async ensureStreamsAndDispatch() {
166
+ if (this.buffer.length === 0)
167
+ return;
168
+ // Bug 2 fix: if the last boundary lands exactly at buffer.length the final
169
+ // "chunk" would be an empty string — drop that phantom boundary so we don't
170
+ // post a blank placeholder message.
171
+ while (this.boundaries.length > 0 &&
172
+ this.boundaries[this.boundaries.length - 1] >= this.buffer.length) {
173
+ this.boundaries.pop();
174
+ }
175
+ const chunkCount = this.boundaries.length + 1;
176
+ while (this.streams.length < chunkCount) {
177
+ // Plain-text placeholder (no Markdown): postPlaceholder sends with
178
+ // parse_mode:"HTML", so Markdown italics would render literal underscores.
179
+ const placeholder = "…";
180
+ const messageId = await this.postPlaceholder(placeholder);
181
+ this.streams.push(new EditStream({
182
+ update: (text) => this.editAt(messageId, this.transform(text) || " "),
183
+ minIntervalMs: this.minIntervalMs,
184
+ }));
185
+ }
186
+ // Dispatch slices to each message's stream.
187
+ for (let i = 0; i < chunkCount; i++) {
188
+ const start = i === 0 ? 0 : this.boundaries[i - 1];
189
+ const end = i < this.boundaries.length ? this.boundaries[i] : this.buffer.length;
190
+ const slice = this.buffer.slice(start, end);
191
+ // Bug 2 fix: never dispatch an empty slice.
192
+ if (slice.length > 0) {
193
+ this.streams[i].append(slice);
194
+ }
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,56 @@
1
+ import type { AbstractAgent } from "@ag-ui/client";
2
+ import type { ThreadMessage } from "@copilotkit/channels-ui";
3
+ import type { ConversationStore, AgentSession } from "@copilotkit/channels";
4
+ import type { ReplyTarget } from "./types.js";
5
+ import type { AgentContentPart } from "./download-files.js";
6
+ /**
7
+ * Telegram conversation store with stable threadIds.
8
+ *
9
+ * Unlike Slack (which rebuilds history from the platform API and mints a fresh
10
+ * threadId per turn), Telegram's Bot API cannot read chat history. This store
11
+ * therefore:
12
+ * - assigns each conversation a **stable** threadId (`tg-thread-<key>`) so the
13
+ * agent's own persistence layer can accumulate state across turns; and
14
+ * - maintains an in-memory message log (lost on restart, capped at 200 entries)
15
+ * that callers can use for lightweight within-session context.
16
+ */
17
+ export declare class TelegramConversationStore implements ConversationStore {
18
+ private readonly sessions;
19
+ private readonly history;
20
+ /**
21
+ * Per-conversation queue of user messages awaiting delivery to the agent.
22
+ *
23
+ * The listener enqueues each turn's user message (text, or text + file
24
+ * parts) BEFORE invoking the bot handler that calls `runAgent` →
25
+ * {@link getOrCreate}, which drains the queue into the cached agent. This is
26
+ * how Telegram delivers the user's turn to the agent: unlike Slack, the Bot
27
+ * API cannot read chat history, so `Thread.run()` (which only injects a
28
+ * message when `extra.prompt` is set) would otherwise never see the input.
29
+ */
30
+ private readonly pending;
31
+ private threadIdFor;
32
+ /**
33
+ * Enqueue a user message to be delivered to the agent on the next
34
+ * {@link getOrCreate} for `conversationKey`. Content is either a plain text
35
+ * string or an array of AG-UI content parts (text + media).
36
+ */
37
+ enqueueUserMessage(conversationKey: string, content: string | AgentContentPart[]): void;
38
+ /**
39
+ * Return the cached AgentSession for `conversationKey`, or create one.
40
+ *
41
+ * On first call for a key `makeAgent` is invoked exactly once with the
42
+ * stable threadId; subsequent calls return the same cached session without
43
+ * invoking `makeAgent` again.
44
+ */
45
+ getOrCreate(conversationKey: string, _replyTarget: ReplyTarget, makeAgent: (threadId: string) => AbstractAgent): Promise<AgentSession>;
46
+ /**
47
+ * Append `msg` to the in-memory history for `conversationKey`.
48
+ * Drops the oldest entry when the list exceeds {@link MAX_HISTORY}.
49
+ */
50
+ recordMessage(conversationKey: string, msg: ThreadMessage): void;
51
+ /** Return the recorded message history for `conversationKey` (or `[]`). */
52
+ getMessages(conversationKey: string): ThreadMessage[];
53
+ /** Whether a session exists for `conversationKey`. */
54
+ has(conversationKey: string): boolean;
55
+ }
56
+ //# sourceMappingURL=conversation-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-store.d.ts","sourceRoot":"","sources":["../src/conversation-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAK5D;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,YAAW,iBAAiB;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGpB;IAEJ,OAAO,CAAC,WAAW;IAInB;;;;OAIG;IACH,kBAAkB,CAChB,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,GACnC,IAAI;IASP;;;;;;OAMG;IACG,WAAW,CACf,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,WAAW,EACzB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,GAC7C,OAAO,CAAC,YAAY,CAAC;IA2BxB;;;OAGG;IACH,aAAa,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI;IAYhE,2EAA2E;IAC3E,WAAW,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,EAAE;IAIrD,sDAAsD;IACtD,GAAG,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO;CAGtC"}
@@ -0,0 +1,98 @@
1
+ /** Maximum number of messages retained per conversation (oldest dropped). */
2
+ const MAX_HISTORY = 200;
3
+ /**
4
+ * Telegram conversation store with stable threadIds.
5
+ *
6
+ * Unlike Slack (which rebuilds history from the platform API and mints a fresh
7
+ * threadId per turn), Telegram's Bot API cannot read chat history. This store
8
+ * therefore:
9
+ * - assigns each conversation a **stable** threadId (`tg-thread-<key>`) so the
10
+ * agent's own persistence layer can accumulate state across turns; and
11
+ * - maintains an in-memory message log (lost on restart, capped at 200 entries)
12
+ * that callers can use for lightweight within-session context.
13
+ */
14
+ export class TelegramConversationStore {
15
+ sessions = new Map();
16
+ history = new Map();
17
+ /**
18
+ * Per-conversation queue of user messages awaiting delivery to the agent.
19
+ *
20
+ * The listener enqueues each turn's user message (text, or text + file
21
+ * parts) BEFORE invoking the bot handler that calls `runAgent` →
22
+ * {@link getOrCreate}, which drains the queue into the cached agent. This is
23
+ * how Telegram delivers the user's turn to the agent: unlike Slack, the Bot
24
+ * API cannot read chat history, so `Thread.run()` (which only injects a
25
+ * message when `extra.prompt` is set) would otherwise never see the input.
26
+ */
27
+ pending = new Map();
28
+ threadIdFor(conversationKey) {
29
+ return `tg-thread-${conversationKey}`;
30
+ }
31
+ /**
32
+ * Enqueue a user message to be delivered to the agent on the next
33
+ * {@link getOrCreate} for `conversationKey`. Content is either a plain text
34
+ * string or an array of AG-UI content parts (text + media).
35
+ */
36
+ enqueueUserMessage(conversationKey, content) {
37
+ let list = this.pending.get(conversationKey);
38
+ if (!list) {
39
+ list = [];
40
+ this.pending.set(conversationKey, list);
41
+ }
42
+ list.push({ content });
43
+ }
44
+ /**
45
+ * Return the cached AgentSession for `conversationKey`, or create one.
46
+ *
47
+ * On first call for a key `makeAgent` is invoked exactly once with the
48
+ * stable threadId; subsequent calls return the same cached session without
49
+ * invoking `makeAgent` again.
50
+ */
51
+ async getOrCreate(conversationKey, _replyTarget, makeAgent) {
52
+ let session = this.sessions.get(conversationKey);
53
+ if (!session) {
54
+ const threadId = this.threadIdFor(conversationKey);
55
+ const agent = makeAgent(threadId);
56
+ session = { agent };
57
+ this.sessions.set(conversationKey, session);
58
+ }
59
+ // Drain any user messages the listener enqueued for this turn into the
60
+ // agent so they actually reach the model. Clears the queue so a later
61
+ // getOrCreate (e.g. a follow-up run) does not re-deliver them.
62
+ const queued = this.pending.get(conversationKey);
63
+ if (queued && queued.length > 0) {
64
+ for (const { content } of queued) {
65
+ session.agent.addMessage({
66
+ id: globalThis.crypto.randomUUID(),
67
+ role: "user",
68
+ content,
69
+ });
70
+ }
71
+ this.pending.delete(conversationKey);
72
+ }
73
+ return session;
74
+ }
75
+ /**
76
+ * Append `msg` to the in-memory history for `conversationKey`.
77
+ * Drops the oldest entry when the list exceeds {@link MAX_HISTORY}.
78
+ */
79
+ recordMessage(conversationKey, msg) {
80
+ let list = this.history.get(conversationKey);
81
+ if (!list) {
82
+ list = [];
83
+ this.history.set(conversationKey, list);
84
+ }
85
+ list.push(msg);
86
+ if (list.length > MAX_HISTORY) {
87
+ list.shift();
88
+ }
89
+ }
90
+ /** Return the recorded message history for `conversationKey` (or `[]`). */
91
+ getMessages(conversationKey) {
92
+ return this.history.get(conversationKey) ?? [];
93
+ }
94
+ /** Whether a session exists for `conversationKey`. */
95
+ has(conversationKey) {
96
+ return this.sessions.has(conversationKey);
97
+ }
98
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Inbound file transport. Telegram messages can carry file attachments; this
3
+ * turns them into AG-UI multimodal message content the agent's model can
4
+ * read — images, audio, video, and documents as their respective binary parts —
5
+ * downloading each from the Telegram Bot API file endpoint.
6
+ *
7
+ * The bridge is transport-only: it delivers the bytes to the agent and lets
8
+ * the app decide what to do with them. Anything it can't represent is skipped
9
+ * with a short note so the agent knows a file was dropped and why.
10
+ */
11
+ /** The subset of a Telegram file object we use. */
12
+ export interface TelegramFileRef {
13
+ fileId: string;
14
+ fileName?: string;
15
+ mimeType?: string;
16
+ size?: number;
17
+ }
18
+ /** A base64 data source, shared by every binary media part. */
19
+ type MediaDataSource = {
20
+ type: "data";
21
+ value: string;
22
+ mimeType: string;
23
+ };
24
+ /**
25
+ * AG-UI multimodal content parts (shape the runtime's converter expects).
26
+ *
27
+ * Binary media (image/audio/video/document) is passed straight through as a
28
+ * data part; the agent's model decides what it can actually consume.
29
+ */
30
+ export type AgentContentPart = {
31
+ type: "text";
32
+ text: string;
33
+ } | {
34
+ type: "image";
35
+ source: MediaDataSource;
36
+ } | {
37
+ type: "audio";
38
+ source: MediaDataSource;
39
+ } | {
40
+ type: "video";
41
+ source: MediaDataSource;
42
+ } | {
43
+ type: "document";
44
+ source: MediaDataSource;
45
+ };
46
+ /** Tunables for inbound file handling (all optional; sane defaults). */
47
+ export interface FileDeliveryConfig {
48
+ /** Skip a single file larger than this many bytes. Default 8 MiB. */
49
+ maxBytesPerFile?: number;
50
+ /** Process at most this many files per message. Default 5. */
51
+ maxFiles?: number;
52
+ }
53
+ /**
54
+ * Download a message's files and turn them into AG-UI content parts. Returns
55
+ * the parts plus human-readable `notes` for anything skipped (appended to the
56
+ * message as a text part by the caller).
57
+ *
58
+ * @param files List of Telegram file references to process.
59
+ * @param token Telegram bot token used to build the download URL.
60
+ * @param getFilePath Resolves a fileId to a Telegram file path (e.g. via getFile API).
61
+ * @param config Optional delivery configuration.
62
+ */
63
+ export declare function buildFileContentParts(files: TelegramFileRef[], token: string, getFilePath: (fileId: string) => Promise<string>, config?: FileDeliveryConfig): Promise<{
64
+ parts: AgentContentPart[];
65
+ notes: string[];
66
+ }>;
67
+ export {};
68
+ //# sourceMappingURL=download-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download-files.d.ts","sourceRoot":"","sources":["../src/download-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,KAAK,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAElD,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA0DD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,eAAe,EAAE,EACxB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EAChD,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC;IAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAoGzD"}