telegram-agent-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Kalinichenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # telegram-agent-kit
2
+
3
+ > Wire LLM agents to Telegram: render Markdown, stream replies into a live native draft, and drive a snapshot/rollback turn-loop — over thin injected interfaces, with **zero runtime dependencies** in the core.
4
+
5
+ [![npm](https://img.shields.io/npm/v/telegram-agent-kit.svg)](https://www.npmjs.com/package/telegram-agent-kit)
6
+ [![license](https://img.shields.io/npm/l/telegram-agent-kit.svg)](./LICENSE)
7
+ [![types](https://img.shields.io/npm/types/telegram-agent-kit.svg)](https://www.typescriptlang.org/)
8
+
9
+ ESM-only. Runs on **Node 18+, Bun, Deno, and the browser** (the formatting core).
10
+
11
+ ---
12
+
13
+ ## Why this exists
14
+
15
+ Connecting an LLM agent to a Telegram bot looks simple until you hit the edges:
16
+
17
+ - LLMs emit arbitrary Markdown — Telegram speaks a small, strict HTML subset. A
18
+ single unclosed tag makes the Bot API reject the whole message.
19
+ - Token streaming into a *live draft* means you render **partial** Markdown many
20
+ times a second, where marks and fences are routinely mid-token.
21
+ - A failed turn must roll back cleanly so the conversation thread isn't corrupted.
22
+
23
+ `telegram-agent-kit` solves these once. Notably, it's the only JS package that
24
+ renders Markdown to **Telegram-HTML** with **totality** (never throws on arbitrary
25
+ LLM output) *and* a **streaming/partial** mode (auto-closes unclosed marks and
26
+ fences, so a live draft never flashes broken markup).
27
+
28
+ It is **runtime-agnostic**: you supply all I/O through small injected interfaces,
29
+ and the kit owns the orchestration. No HTTP client, no framework, no globals.
30
+
31
+ ## Features
32
+
33
+ - **Markdown → Telegram-HTML** that never throws, with a `partial` mode for live drafts.
34
+ - **Live draft streaming** — a throttle / keepalive / typing-heartbeat / drain state
35
+ machine that animates one native Telegram draft from a growing string.
36
+ - **Turn-loop orchestration** — `snapshot → stream → animate → finalize → reply`,
37
+ with rollback on error and a guarantee it never throws out.
38
+ - **Resilient send path** — automatic chunking, surrogate-safe splitting, and
39
+ deterministic `400` fallbacks (rich → HTML → plain text; photo → text).
40
+ - **Optional deepagents adapter** on a separate subpath, so the core never pulls in
41
+ langchain.
42
+
43
+ ## Installation
44
+
45
+ ```sh
46
+ npm install telegram-agent-kit
47
+ ```
48
+
49
+ The optional `telegram-agent-kit/deepagents` subpath needs its peers — install them
50
+ only if you use it:
51
+
52
+ ```sh
53
+ npm install @langchain/core deepagents
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ Implement a thin `BotClient` over the Bot API, then drive one turn per incoming
59
+ message. The example uses the deepagents adapter, but any `AgentStream` works.
60
+
61
+ ```ts
62
+ import { runTelegramTurn, TelegramApiError, type BotClient } from 'telegram-agent-kit';
63
+ import { toAgentStream } from 'telegram-agent-kit/deepagents';
64
+
65
+ // 1. Raw Bot API primitives — one HTTP call each. Throw TelegramApiError on a
66
+ // Bot API error so the kit's deterministic-400 fallbacks can fire.
67
+ const client: BotClient = {
68
+ sendMessage: (p, signal) => call('sendMessage', { chat_id: p.chatId, text: p.text, parse_mode: p.parseMode }, signal),
69
+ sendRichMessage: (p, signal) => call('sendMessage', { chat_id: p.chatId, text: p.markdown }, signal),
70
+ sendPhoto: (p, signal) => call('sendPhoto', { chat_id: p.chatId, photo: p.url, caption: p.caption, parse_mode: p.parseMode }, signal),
71
+ sendChatAction: (p, signal) => call('sendChatAction', { chat_id: p.chatId, action: p.action ?? 'typing' }, signal),
72
+ sendMessageDraft: (p, signal) => call('sendMessageDraft', { chat_id: p.chatId, draft_id: p.draftId, text: p.text }, signal),
73
+ sendRichMessageDraft:(p, signal) => call('sendRichMessageDraft',{ chat_id: p.chatId, draft_id: p.draftId, text: p.markdown },signal),
74
+ };
75
+
76
+ // 2. Drive one turn.
77
+ await runTelegramTurn({
78
+ chatKey: { chatId, agentId: 'main' },
79
+ userText,
80
+ draftId: updateId, // non-zero, unique per turn — reused for every draft write
81
+ rich: true,
82
+ client,
83
+ agentStream: toAgentStream(agent), // your deepagents agent
84
+ checkpointer: {
85
+ snapshot: (threadId) => saver.snapshotId(threadId),
86
+ rollback: (threadId, id) => saver.rollbackThread(threadId, id),
87
+ },
88
+ threadStore: {
89
+ resolve: (key) => threads.resolve(key.chatId, key.agentId),
90
+ touch: (key, now) => threads.touch(key.chatId, key.agentId, now),
91
+ },
92
+ log: console,
93
+ });
94
+
95
+ // Thin transport helper.
96
+ async function call(method: string, body: unknown, signal?: AbortSignal) {
97
+ const res = await fetch(`https://api.telegram.org/bot${TOKEN}/${method}`, {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/json' },
100
+ body: JSON.stringify(body),
101
+ signal,
102
+ });
103
+ if (!res.ok) {
104
+ const j = await res.json().catch(() => ({}));
105
+ throw new TelegramApiError(res.status, j.description);
106
+ }
107
+ }
108
+ ```
109
+
110
+ > **Note:** `sendRichMessage` / `sendRichMessageDraft` map to your bot's rich-text
111
+ > transport. If your bot has no rich endpoint, point them at plain `sendMessage`
112
+ > and set `rich: false` — the kit then renders via HTML only.
113
+
114
+ ## How it works
115
+
116
+ The kit is three layers plus one optional adapter. The dependency direction is
117
+ strictly **Bridge → Draft → Formatting**; lower layers never import higher ones,
118
+ and the core never imports the adapter.
119
+
120
+ ```
121
+ ┌─────────────────────────────────────────────────────────────┐
122
+ │ /deepagents (optional subpath) │
123
+ │ toAgentStream · streamAgent → AgentStream │
124
+ └─────────────────────────────────────────────────────────────┘
125
+ ┌─────────────────────────────────────────────────────────────┐
126
+ │ Bridge runTelegramTurn · sendReply · sendText │
127
+ │ + the four interfaces you implement │
128
+ └─────────────────────────────────────────────────────────────┘
129
+ ┌─────────────────────────────────────────────────────────────┐
130
+ │ Draft createDraftStreamer (throttle/keepalive/drain)│
131
+ └─────────────────────────────────────────────────────────────┘
132
+ ┌─────────────────────────────────────────────────────────────┐
133
+ │ Format mdToTelegramHtml · chunk* · rich helpers │
134
+ │ pure, zero deps, browser-safe │
135
+ └─────────────────────────────────────────────────────────────┘
136
+ ```
137
+
138
+ **Transport vs. orchestration.** You provide *raw* Bot API primitives — one HTTP
139
+ call each, no chunking, rendering, or fallback. The kit owns all orchestration over
140
+ them: HTML rendering, chunking, the rich → classic and photo → text `400` fallbacks,
141
+ and the trailing-cover photo flow.
142
+
143
+ ## The four interfaces
144
+
145
+ You implement these; the kit drives them.
146
+
147
+ | Interface | Shape | Role |
148
+ | -------------- | --------------------------------------------------------------------- | ---- |
149
+ | `BotClient` | `sendMessage`, `sendRichMessage`, `sendPhoto`, `sendChatAction`, `sendMessageDraft`, `sendRichMessageDraft` | Raw transport. Each throws `TelegramApiError` on a Bot API error. |
150
+ | `AgentStream` | `(input, { threadId, signal }) => AsyncIterable<RenderEvent>` | Your agent. `threadId` **must** reach it so the checkpointer writes to the snapshotted thread. |
151
+ | `Checkpointer` | `{ snapshot(threadId), rollback(threadId, id) }` | Per-thread snapshot/rollback for clean recovery on a failed turn. |
152
+ | `ThreadStore` | `{ resolve(chatKey, now), touch(chatKey, now) }` | Maps `{ chatId, agentId }` to a thread id (so two bots over one chat id don't collide). |
153
+
154
+ A `RenderEvent` is one of `token`, `tool_start`, `tool_end`, or `error`. The kit
155
+ appends `token` text to the live draft and treats an `error` event as a rollback.
156
+
157
+ ## API reference
158
+
159
+ ### Core entry — `telegram-agent-kit`
160
+
161
+ **Formatting** (pure, zero deps)
162
+
163
+ - `mdToTelegramHtml(md, opts?)` — Markdown → Telegram-HTML. Never throws. `opts.partial`
164
+ auto-closes unclosed marks/fences for live drafts.
165
+ - `chunkText(text)` / `safeSlice(text, max)` / `chunkRich(md)` — surrogate-safe splitting
166
+ (classic limit 4096, rich limit 32768).
167
+ - `repairRichTables(md)` · `neutralizeRichMedia(md)` · `extractTrailingCover(reply)` — rich helpers.
168
+
169
+ **Draft engine**
170
+
171
+ - `createDraftStreamer(deps)` → `{ start(), push(fullText), finalize(), abort() }`.
172
+ - `DEFAULT_DRAFT_CONSTANTS` / `DraftConstants` — overridable tunables (throttle, keepalive,
173
+ typing heartbeat, preview cap, drain, …).
174
+
175
+ **Bridge**
176
+
177
+ - `runTelegramTurn(opts)` — orchestrate one turn. Never throws out; every failure is caught and logged.
178
+ - `sendReply(client, chatId, reply, opts, signal?)` / `sendText(...)` — the send path on its own.
179
+ - Types: `BotClient`, `AgentStream`, `Checkpointer`, `ThreadStore`, `RenderEvent`, `ChatKey`, `Logger`.
180
+
181
+ **Errors**
182
+
183
+ - `TelegramApiError` — throw this from `BotClient` primitives (carries `error_code`).
184
+ - `isBadRequest(err)` — true only for a deterministic `400` (rejected, safe to retry on a degraded path).
185
+
186
+ ### Optional entry — `telegram-agent-kit/deepagents`
187
+
188
+ - `toAgentStream(agent)` → `AgentStream` — adapts a deepagents/langgraph agent to the kit's contract.
189
+ - `streamAgent(agent, input, config, signal?)` — lower-level event stream if you need direct control.
190
+
191
+ > `@langchain/core` and `deepagents` are **type-only, optional** peers. The built
192
+ > `/deepagents` bundle contains no runtime import of either, so the core stays
193
+ > dependency-free.
194
+
195
+ ## Design guarantees
196
+
197
+ These are intentional and enforced by tests:
198
+
199
+ - **`mdToTelegramHtml` is total** — it never throws on any LLM output. The send path
200
+ also wraps it and falls back to plain text as defence-in-depth.
201
+ - **Deterministic `400` fallbacks** keyed off `isBadRequest`: rich → classic HTML →
202
+ plain text, and photo → text. Any non-`400` error always propagates.
203
+ - **`runTelegramTurn` never throws out** — snapshot happens only after a turn isn't
204
+ skipped, rollback fires only on a real failure, and draft teardown is idempotent.
205
+ - **Surrogate-safe splitting** — chunking never severs a UTF-16 surrogate pair.
206
+
207
+ ## Development
208
+
209
+ ```sh
210
+ npm run build # tsup → dist/ (ESM + .d.ts) for both entry points
211
+ npm run typecheck # tsc --noEmit
212
+ npm test # vitest run
213
+ npm run lint # biome check .
214
+ npm run format # biome format --write .
215
+ ```
216
+
217
+ Run a single test:
218
+
219
+ ```sh
220
+ npx vitest run test/format/md-to-html.test.ts # one file
221
+ npx vitest run -t "renders nested bold" # by name
222
+ ```
223
+
224
+ > Build before testing if you touch `/deepagents`: one test greps the built bundle
225
+ > to prove it carries no runtime import of the optional peers (it skips when `dist/`
226
+ > is absent). CI runs `build` before `test` for this reason.
227
+
228
+ ## Contributing
229
+
230
+ Issues and pull requests are welcome. Before opening a PR, please run
231
+ `npm run lint`, `npm run typecheck`, and `npm test` — and `npm run build` if your
232
+ change touches the `/deepagents` entry. See [CHANGELOG.md](./CHANGELOG.md) for the
233
+ project history.
234
+
235
+ ## License
236
+
237
+ [MIT](./LICENSE) © Ivan Kalinichenko
@@ -0,0 +1,20 @@
1
+ import { R as RenderEvent, A as AgentStream } from '../interfaces-BRu3W_FZ.js';
2
+ import { RunnableConfig } from '@langchain/core/runnables';
3
+ import { createDeepAgent } from 'deepagents';
4
+
5
+ type Agent$1 = ReturnType<typeof createDeepAgent>;
6
+ type StreamAgentInput = {
7
+ messages: Array<{
8
+ role: 'user';
9
+ content: string;
10
+ }>;
11
+ };
12
+ declare function streamAgent(agent: Agent$1, input: StreamAgentInput, config: RunnableConfig, signal?: AbortSignal): AsyncIterable<RenderEvent>;
13
+
14
+ type Agent = Parameters<typeof streamAgent>[0];
15
+ /** Adapt a deepagents agent to the kit's runtime-agnostic AgentStream: build
16
+ * the langgraph config from context.threadId, forward the signal. This is the
17
+ * ONLY place the langgraph config shape lives. */
18
+ declare function toAgentStream(agent: Agent): AgentStream;
19
+
20
+ export { RenderEvent, type StreamAgentInput, streamAgent, toAgentStream };
@@ -0,0 +1,76 @@
1
+ // src/deepagents/stream-agent.ts
2
+ function extractText(content) {
3
+ if (typeof content === "string") return content;
4
+ if (Array.isArray(content)) {
5
+ return content.filter(
6
+ (p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
7
+ ).map((p) => p.text).join("");
8
+ }
9
+ return "";
10
+ }
11
+ function extractToolError(output) {
12
+ if (typeof output !== "object" || output === null) return void 0;
13
+ const o = output;
14
+ if (o.status !== "error") return void 0;
15
+ const c = o.content;
16
+ if (typeof c === "string") return c;
17
+ try {
18
+ return JSON.stringify(c);
19
+ } catch {
20
+ return String(c);
21
+ }
22
+ }
23
+ async function* streamAgent(agent, input, config, signal) {
24
+ const toolStart = /* @__PURE__ */ new Map();
25
+ const iter = agent.streamEvents(input, {
26
+ ...config,
27
+ signal,
28
+ version: "v2"
29
+ });
30
+ try {
31
+ for await (const ev of iter) {
32
+ if (ev.event === "on_chat_model_stream") {
33
+ if (ev.metadata?.langgraph_node !== "model_request") continue;
34
+ const text = extractText(ev.data?.chunk?.content);
35
+ if (text) yield { type: "token", text };
36
+ } else if (ev.event === "on_tool_start") {
37
+ toolStart.set(ev.run_id, Date.now());
38
+ yield {
39
+ type: "tool_start",
40
+ id: ev.run_id,
41
+ name: ev.name ?? "unknown",
42
+ args: ev.data?.input
43
+ };
44
+ } else if (ev.event === "on_tool_end" || ev.event === "on_tool_error") {
45
+ const startedAt = toolStart.get(ev.run_id) ?? Date.now();
46
+ toolStart.delete(ev.run_id);
47
+ const error = ev.event === "on_tool_error" ? ev.data?.error instanceof Error ? ev.data.error.message : String(ev.data?.error ?? "tool error") : extractToolError(ev.data?.output);
48
+ yield {
49
+ type: "tool_end",
50
+ id: ev.run_id,
51
+ durationMs: Date.now() - startedAt,
52
+ ...error !== void 0 ? { error } : {}
53
+ };
54
+ }
55
+ }
56
+ } catch (err) {
57
+ yield {
58
+ type: "error",
59
+ message: signal?.aborted ? "canceled" : err instanceof Error ? err.message : String(err)
60
+ };
61
+ }
62
+ }
63
+
64
+ // src/deepagents/to-agent-stream.ts
65
+ function toAgentStream(agent) {
66
+ return (input, context) => streamAgent(
67
+ agent,
68
+ input,
69
+ { configurable: { thread_id: context.threadId } },
70
+ context.signal
71
+ );
72
+ }
73
+ export {
74
+ streamAgent,
75
+ toAgentStream
76
+ };
@@ -0,0 +1,167 @@
1
+ import { B as BotClient, L as Logger, C as ChatKey, A as AgentStream, a as Checkpointer, T as ThreadStore } from './interfaces-BRu3W_FZ.js';
2
+ export { b as AgentStreamContext, R as RenderEvent, S as StreamInput } from './interfaces-BRu3W_FZ.js';
3
+
4
+ type SendOpts = {
5
+ rich: boolean;
6
+ log: Logger;
7
+ };
8
+ /** Active reply path — rich when opts.rich, else classic. */
9
+ declare function sendText(client: BotClient, chatId: number, text: string, opts: SendOpts, signal?: AbortSignal): Promise<void>;
10
+ /** Final reply send for a completed turn (client.ts sendReply). */
11
+ declare function sendReply(client: BotClient, chatId: number, reply: string, opts: SendOpts, signal?: AbortSignal): Promise<void>;
12
+
13
+ /** DraftStreamer tunables — overridable via DraftStreamerDeps.constants.
14
+ * Defaults mirror the machine-spirit values (the draft-streaming design
15
+ * spec): native drafts tolerate frequent writes; the keepalive must beat the
16
+ * ~30s draft-expiry window; the typing action must re-fire before its ~5s
17
+ * lifetime lapses. */
18
+ type DraftConstants = {
19
+ throttleMs: number;
20
+ keepaliveMs: number;
21
+ typingHeartbeatMs: number;
22
+ previewCap: number;
23
+ maxFailures: number;
24
+ drainMs: number;
25
+ tickMs: number;
26
+ };
27
+ declare const DEFAULT_DRAFT_CONSTANTS: DraftConstants;
28
+
29
+ type IntervalHandle = ReturnType<typeof setInterval>;
30
+ type DraftStreamerDeps = {
31
+ client: Pick<BotClient, 'sendMessageDraft' | 'sendRichMessageDraft' | 'sendChatAction'>;
32
+ chatId: number;
33
+ draftId: number;
34
+ rich: boolean;
35
+ log: Logger;
36
+ /** Override any subset of the draft tunables; defaults fill the rest. */
37
+ constants?: Partial<DraftConstants>;
38
+ now?: () => number;
39
+ schedule?: (fn: () => void, ms: number) => IntervalHandle;
40
+ cancel?: (h: IntervalHandle) => void;
41
+ delay?: (ms: number) => {
42
+ promise: Promise<void>;
43
+ cancel: () => void;
44
+ };
45
+ };
46
+ type DraftStreamer = {
47
+ start(): void;
48
+ push(fullText: string): void;
49
+ finalize(): Promise<void>;
50
+ abort(): Promise<void>;
51
+ };
52
+ declare function createDraftStreamer(deps: DraftStreamerDeps): DraftStreamer;
53
+
54
+ type TurnContext = {
55
+ chatKey: ChatKey;
56
+ userText: string;
57
+ };
58
+ type RunTelegramTurnOpts = {
59
+ chatKey: ChatKey;
60
+ userText: string;
61
+ draftId: number;
62
+ rich: boolean;
63
+ client: BotClient;
64
+ agentStream: AgentStream;
65
+ checkpointer: Checkpointer;
66
+ threadStore: ThreadStore;
67
+ signal?: AbortSignal;
68
+ now?: () => number;
69
+ log?: Logger;
70
+ hooks?: {
71
+ preStream?: (ctx: TurnContext) => void | {
72
+ skip?: boolean;
73
+ } | Promise<void | {
74
+ skip?: boolean;
75
+ }>;
76
+ beforeTurn?: (ctx: TurnContext) => void | Promise<void>;
77
+ afterTurn?: (ctx: TurnContext) => void | Promise<void>;
78
+ };
79
+ makeDraftStreamer?: (deps: DraftStreamerDeps) => DraftStreamer;
80
+ draftConstants?: Partial<DraftConstants>;
81
+ };
82
+ declare function runTelegramTurn(opts: RunTelegramTurnOpts): Promise<void>;
83
+
84
+ /** Thrown by BotClient transport primitives on a Bot API error. The
85
+ * `error_code` is the Bot API numeric code; 400 is the deterministic
86
+ * "rejected, not delivered" class the kit's fallbacks key off. */
87
+ declare class TelegramApiError extends Error {
88
+ readonly error_code: number;
89
+ readonly description?: string | undefined;
90
+ constructor(error_code: number, description?: string | undefined);
91
+ }
92
+ /** True only for a deterministic 400 (rejected, not delivered) — safe to
93
+ * retry on a degraded path without risking a double-send. */
94
+ declare function isBadRequest(err: unknown): err is TelegramApiError;
95
+
96
+ /** Split into ≤TELEGRAM_LIMIT UTF-16-code-unit chunks without severing a
97
+ * surrogate pair. Prefers paragraph/line boundaries, hard-cuts otherwise. */
98
+ declare function chunkText(text: string): string[];
99
+ /** Surrogate-safe hard truncation to `max` UTF-16 code units. Mirrors the
100
+ * high-surrogate guard in chunkText so a draft cap never severs a pair. */
101
+ declare function safeSlice(text: string, max: number): string;
102
+ /** Block-aware splitter for rich markdown. Most replies fit RICH_LIMIT in one
103
+ * message; when they don't, split only on blank-line (paragraph) boundaries
104
+ * so a table/list/quote is never cut. A single block over the limit is
105
+ * hard-cut surrogate-safely (rare — see spec §9). */
106
+ declare function chunkRich(md: string): string[];
107
+
108
+ /** Markdown → Telegram-HTML renderer (channel-local). Tokenize-on-raw,
109
+ * escape-at-emission, total (never throws). Deliberately NOT named
110
+ * markdown.ts — src/runtime/markdown.ts owns that name in greps. Spec:
111
+ * docs/superpowers/specs/2026-06-07-telegram-markdown-rendering-design.md */
112
+ type MdToHtmlOpts = {
113
+ /** Draft mode: auto-close unclosed inline marks / fences at end of input. */
114
+ partial?: boolean;
115
+ };
116
+ declare function mdToTelegramHtml(md: string, opts?: MdToHtmlOpts): string;
117
+
118
+ /** Re-absorb table rows the model detached with a blank line. A GFM table ends
119
+ * at a blank line, so a reply that puts a summary/total row a blank line below
120
+ * the body (observed live: `| Итого |` after `\n\n`, with cells that DID match
121
+ * the header) turns that row into a standalone paragraph — Telegram's rich
122
+ * renderer then prints it with literal pipes instead of inside the table. This
123
+ * drops the separating blank line whenever a pipe-only paragraph (not itself a
124
+ * new header+delimiter table) directly follows a table, rejoining the orphan;
125
+ * it repeats so several stacked orphan rows all fold back in. Genuinely
126
+ * separate tables (the next block has its own delimiter) and non-table content
127
+ * are left untouched. Code regions are tracked and skipped — both fenced code
128
+ * blocks (`` ``` `` / `~~~`) AND HTML `<pre>`/`<code>` regions: a blank-line-
129
+ * split table inside one is a literal example, not an orphan, so it is left
130
+ * verbatim (mirroring `neutralizeRichMedia`'s code-region skip — both run on
131
+ * the same send path, so they must agree on what is literal code). Merge
132
+ * eligibility of the preceding block is *tracked as it is emitted* (a table
133
+ * emitted OUTSIDE any code region whose last line is still a table row), never
134
+ * recomputed from its text — a table-shaped block that was code inside a region
135
+ * still looks like a table once the region closes, so an outside orphan must not
136
+ * fold into it, and a block that opens as a table but whose body contains
137
+ * non-table prose (mid-body or trailing) is not eligible either (else the orphan
138
+ * glues onto non-table content). Pure and total. */
139
+ declare function repairRichTables(md: string): string;
140
+ /** Extract a standalone cover image that is the LAST non-empty line of `md`:
141
+ * the whole line is a single `![alt](http(s)://…)` token. Returns the URL plus
142
+ * the body (the reply with that line removed, trailing whitespace trimmed).
143
+ * Anything else — no image, an image mid-text, an inline image, a
144
+ * non-HTTP(S)/angle-bracketed URL, or an image line inside an open code region —
145
+ * returns null. Narrow by design: only a standalone trailing line triggers the
146
+ * photo path, so non-media replies and inline URLs never fire it. */
147
+ declare function extractTrailingCover(md: string): {
148
+ url: string;
149
+ body: string;
150
+ } | null;
151
+ /** Rewrite HTTP(S) media image syntax `![alt](url ["title"])` to a plain link
152
+ * (bare URL when alt is empty) so a rich send never attempts a media block —
153
+ * media needs send-rights and fails for non-parse reasons (§4.2). `tg://`
154
+ * image syntax (custom emoji / date-time) is left untouched: it is an inline
155
+ * entity, not a media send. Dropping only the `!` keeps any "title" intact.
156
+ *
157
+ * Code regions are skipped: a fenced block (closed, or unterminated and thus
158
+ * code through EOF), an inline code span, an HTML `<pre>`/`<code>` region
159
+ * (Telegram's Rich HTML subset — fixed-width code, body is literal), or a
160
+ * backslash-escaped `!` (only when preceded by an ODD run of backslashes — even
161
+ * runs are literal `\` pairs and leave the `!` live) is literal to Telegram
162
+ * (never a media send), so its `![](…)` must stay verbatim — neutralizing there
163
+ * would corrupt a literal Markdown/code example for no benefit. Pure and
164
+ * total. */
165
+ declare function neutralizeRichMedia(md: string): string;
166
+
167
+ export { AgentStream, BotClient, ChatKey, Checkpointer, DEFAULT_DRAFT_CONSTANTS, type DraftConstants, type DraftStreamer, type DraftStreamerDeps, Logger, type MdToHtmlOpts, type RunTelegramTurnOpts, TelegramApiError, ThreadStore, type TurnContext, chunkRich, chunkText, createDraftStreamer, extractTrailingCover, isBadRequest, mdToTelegramHtml, neutralizeRichMedia, repairRichTables, runTelegramTurn, safeSlice, sendReply, sendText };