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 +21 -0
- package/README.md +237 -0
- package/dist/deepagents/index.d.ts +20 -0
- package/dist/deepagents/index.js +76 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +803 -0
- package/dist/interfaces-BRu3W_FZ.d.ts +81 -0
- package/package.json +74 -0
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
|
+
[](https://www.npmjs.com/package/telegram-agent-kit)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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 `://…)` 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 `` 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 };
|