telegram-agent-kit 0.3.0 → 0.4.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/README.md +14 -3
- package/dist/index.d.ts +37 -2
- package/dist/index.js +36 -10
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
-
ESM-only. Runs on **Node
|
|
9
|
+
ESM-only. Runs on **Node 20+, Bun, Deno, and the browser** (the formatting core).
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -111,6 +111,12 @@ async function call(method: string, body: unknown, signal?: AbortSignal) {
|
|
|
111
111
|
> transport. If your bot has no rich endpoint, point them at plain `sendMessage`
|
|
112
112
|
> and set `rich: false` — the kit then renders via HTML only.
|
|
113
113
|
|
|
114
|
+
> **Note:** `rich: true` means *rich when it buys something*, not *rich always*.
|
|
115
|
+
> Drafts and final messages go out rich only for text that needs the rich
|
|
116
|
+
> renderer — today, a GFM table (`needsRich`); everything else is classic HTML, so
|
|
117
|
+
> ordinary replies keep the client's normal message font. `rich: false` is still
|
|
118
|
+
> the kill-switch: HTML only, always.
|
|
119
|
+
|
|
114
120
|
## How it works
|
|
115
121
|
|
|
116
122
|
The kit is three layers plus one optional adapter. The dependency direction is
|
|
@@ -151,8 +157,9 @@ You implement these; the kit drives them.
|
|
|
151
157
|
| `Checkpointer` | `{ snapshot(threadId), rollback(threadId, id) }` | Per-thread snapshot/rollback for clean recovery on a failed turn. |
|
|
152
158
|
| `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
159
|
|
|
154
|
-
A `RenderEvent` is
|
|
155
|
-
|
|
160
|
+
A `RenderEvent` is either `token` or `error`: `token` text is appended to the live draft,
|
|
161
|
+
an `error` rolls the turn back, logs the message, and — if you pass `errorNotice` — tells
|
|
162
|
+
the user in the chat instead of going silent.
|
|
156
163
|
|
|
157
164
|
## API reference
|
|
158
165
|
|
|
@@ -165,6 +172,8 @@ appends `token` text to the live draft and treats an `error` event as a rollback
|
|
|
165
172
|
- `chunkText(text)` / `safeSlice(text, max)` / `chunkRich(md)` — surrogate-safe splitting
|
|
166
173
|
(classic limit 4096, rich limit 32768).
|
|
167
174
|
- `repairRichTables(md)` · `neutralizeRichMedia(md)` · `extractTrailingCover(reply)` — rich helpers.
|
|
175
|
+
- `needsRich(md)` — does this text need the rich renderer (today: does it contain a GFM
|
|
176
|
+
table, outside code)? The gate behind `rich: true`; everything else goes classic.
|
|
168
177
|
|
|
169
178
|
**Draft engine**
|
|
170
179
|
|
|
@@ -177,6 +186,8 @@ appends `token` text to the live draft and treats an `error` event as a rollback
|
|
|
177
186
|
- `runTelegramTurn(opts)` — orchestrate one turn. Never throws out; every failure is caught and logged.
|
|
178
187
|
Accepts an optional `configurable` bag forwarded to your `AgentStream` as `context.configurable`,
|
|
179
188
|
for passing per-turn data (e.g. `pendingImages`) to the agent without widening the core input type.
|
|
189
|
+
Pass `errorNotice` (plain text, your language) to have a failed turn say so in the chat —
|
|
190
|
+
omit it and the user just sees the draft stop, which reads as being ignored.
|
|
180
191
|
- `sendReply(client, chatId, reply, opts, signal?)` / `sendText(...)` — the send path on its own.
|
|
181
192
|
- Types: `BotClient`, `AgentStream`, `Checkpointer`, `ThreadStore`, `RenderEvent`, `ChatKey`, `Logger`.
|
|
182
193
|
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,17 @@ type SendOpts = {
|
|
|
5
5
|
rich: boolean;
|
|
6
6
|
log: Logger;
|
|
7
7
|
};
|
|
8
|
-
/** Active reply path — rich when opts.rich
|
|
8
|
+
/** Active reply path — rich when `opts.rich`, but `sendRich` sends each chunk
|
|
9
|
+
* rich only if it actually needs the rich renderer (`needsRich`), else classic.
|
|
10
|
+
* `rich: true` means "rich when it buys something", not "rich always": the rich
|
|
11
|
+
* renderer sizes body text its own way with no Bot API field to override it, so
|
|
12
|
+
* pushing ordinary prose through it just makes every reply look unlike a normal
|
|
13
|
+
* message for nothing.
|
|
14
|
+
*
|
|
15
|
+
* Media is neutralized HERE, before the split, so it happens on BOTH paths:
|
|
16
|
+
* classic has no image renderer either (`mdToTelegramHtml` leaves ``
|
|
17
|
+
* verbatim), so prose that routes to classic would otherwise show raw image
|
|
18
|
+
* markdown. Idempotent, so `sendReply`'s photo-fallback re-entry is harmless. */
|
|
9
19
|
declare function sendText(client: BotClient, chatId: number, text: string, opts: SendOpts, signal?: AbortSignal): Promise<void>;
|
|
10
20
|
/** Final reply send for a completed turn (client.ts sendReply). */
|
|
11
21
|
declare function sendReply(client: BotClient, chatId: number, reply: string, opts: SendOpts, signal?: AbortSignal): Promise<void>;
|
|
@@ -78,6 +88,10 @@ type RunTelegramTurnOpts = {
|
|
|
78
88
|
};
|
|
79
89
|
draftConstants?: Partial<DraftConstants>;
|
|
80
90
|
configurable?: Record<string, unknown>;
|
|
91
|
+
/** Plain-text line sent to the chat when the stream errors out (model chain
|
|
92
|
+
* exhausted, graph threw). Without it the turn returns silently and the user
|
|
93
|
+
* sees only a draft that stops moving — indistinguishable from being ignored. */
|
|
94
|
+
errorNotice?: string;
|
|
81
95
|
};
|
|
82
96
|
declare function runTelegramTurn(opts: RunTelegramTurnOpts): Promise<void>;
|
|
83
97
|
|
|
@@ -137,6 +151,27 @@ declare function mdToTelegramHtml(md: string, opts?: MdToHtmlOpts): string;
|
|
|
137
151
|
* non-table prose (mid-body or trailing) is not eligible either (else the orphan
|
|
138
152
|
* glues onto non-table content). Pure and total. */
|
|
139
153
|
declare function repairRichTables(md: string): string;
|
|
154
|
+
/** Does `md` carry structure only the rich renderer can draw? Today that means
|
|
155
|
+
* one thing: a GFM table. Everything else a reply contains — bold, italic,
|
|
156
|
+
* code, links, quotes, lists — classic HTML renders fine, and classic keeps the
|
|
157
|
+
* client's normal message font, while the rich renderer sizes body text its own
|
|
158
|
+
* way with no Bot API knob to override it (the only size field in the whole
|
|
159
|
+
* rich schema is `RichBlockSectionHeading.size`). So routing prose to classic is
|
|
160
|
+
* the only way to keep ordinary replies looking like ordinary messages.
|
|
161
|
+
*
|
|
162
|
+
* Scanned per LINE, not per blank-line block: a table needs no blank line above
|
|
163
|
+
* it, and models routinely put one directly under a lead-in line, a heading, a
|
|
164
|
+
* list, or a closing fence. A block-first-line check misses every one of those
|
|
165
|
+
* (and every table after `\n\n\n` or in CRLF output, where the block split
|
|
166
|
+
* lands wrong) — and a missed table is the one case where classic is WORSE:
|
|
167
|
+
* it has no table renderer, so the reply goes out as literal pipes.
|
|
168
|
+
*
|
|
169
|
+
* Code regions are skipped, using the SAME `advanceCodeRegion` walk as
|
|
170
|
+
* `repairRichTables`: a table inside a fence or an HTML `<pre>`/`<code>` region
|
|
171
|
+
* is a literal example, and classic renders it as code perfectly well — it is
|
|
172
|
+
* not a reason to go rich. Only a header+delimiter pair counts; stray pipe rows
|
|
173
|
+
* print as literal pipes either way. Pure and total. */
|
|
174
|
+
declare function needsRich(md: string): boolean;
|
|
140
175
|
/** Extract a standalone cover image that is the LAST non-empty line of `md`:
|
|
141
176
|
* the whole line is a single `://…)` token. Returns the URL plus
|
|
142
177
|
* the body (the reply with that line removed, trailing whitespace trimmed).
|
|
@@ -164,4 +199,4 @@ declare function extractTrailingCover(md: string): {
|
|
|
164
199
|
* total. */
|
|
165
200
|
declare function neutralizeRichMedia(md: string): string;
|
|
166
201
|
|
|
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 };
|
|
202
|
+
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, needsRich, neutralizeRichMedia, repairRichTables, runTelegramTurn, safeSlice, sendReply, sendText };
|
package/dist/index.js
CHANGED
|
@@ -365,6 +365,16 @@ ${block}`;
|
|
|
365
365
|
}
|
|
366
366
|
return out.join("\n\n");
|
|
367
367
|
}
|
|
368
|
+
function needsRich(md) {
|
|
369
|
+
if (!md.includes("|")) return false;
|
|
370
|
+
const lines = md.split("\n");
|
|
371
|
+
let region = null;
|
|
372
|
+
for (let i = 0; i + 1 < lines.length; i++) {
|
|
373
|
+
if (region === null && isTableBlock(lines.slice(i, i + 2))) return true;
|
|
374
|
+
region = advanceCodeRegion(region, [lines[i] ?? ""]);
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
368
378
|
var TRAILING_COVER_RE = /^!\[[^\]]*\]\(\s*(https?:\/\/[^\s)]+)[^)]*\)$/;
|
|
369
379
|
function insideCodeRegion(lines, idx) {
|
|
370
380
|
return advanceCodeRegion(null, lines.slice(0, idx)) !== null;
|
|
@@ -461,9 +471,11 @@ async function sendClassic(client, chatId, text, signal) {
|
|
|
461
471
|
}
|
|
462
472
|
}
|
|
463
473
|
async function sendRich(client, chatId, markdown, log, signal) {
|
|
464
|
-
for (const piece of chunkRich(
|
|
465
|
-
|
|
466
|
-
|
|
474
|
+
for (const piece of chunkRich(repairRichTables(markdown))) {
|
|
475
|
+
if (!needsRich(piece)) {
|
|
476
|
+
await sendClassic(client, chatId, piece, signal);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
467
479
|
try {
|
|
468
480
|
await client.sendRichMessage({ chatId, markdown: piece }, signal);
|
|
469
481
|
} catch (err) {
|
|
@@ -479,8 +491,9 @@ async function sendRich(client, chatId, markdown, log, signal) {
|
|
|
479
491
|
}
|
|
480
492
|
}
|
|
481
493
|
async function sendText(client, chatId, text, opts, signal) {
|
|
482
|
-
|
|
483
|
-
|
|
494
|
+
const md = neutralizeRichMedia(text);
|
|
495
|
+
if (opts.rich) await sendRich(client, chatId, md, opts.log, signal);
|
|
496
|
+
else await sendClassic(client, chatId, md, signal);
|
|
484
497
|
}
|
|
485
498
|
async function sendCover(client, chatId, url, caption, signal) {
|
|
486
499
|
if (caption === void 0 || caption === "") {
|
|
@@ -519,7 +532,7 @@ async function sendReply(client, chatId, reply, opts, signal) {
|
|
|
519
532
|
else await sendCover(client, chatId, cover.url, cover.body, signal);
|
|
520
533
|
} catch (err) {
|
|
521
534
|
if (!isBadRequest(err)) throw err;
|
|
522
|
-
await sendText(client, chatId,
|
|
535
|
+
await sendText(client, chatId, reply, opts, signal);
|
|
523
536
|
return;
|
|
524
537
|
}
|
|
525
538
|
if (longBody) await sendText(client, chatId, cover.body, opts, signal);
|
|
@@ -574,7 +587,7 @@ function createDraftStreamer(deps) {
|
|
|
574
587
|
const controller = new AbortController();
|
|
575
588
|
inFlightController = controller;
|
|
576
589
|
lastAttemptAt = t;
|
|
577
|
-
const usingRich = richMode;
|
|
590
|
+
const usingRich = richMode && needsRich(text);
|
|
578
591
|
const op = usingRich ? client.sendRichMessageDraft(
|
|
579
592
|
{ chatId, draftId, markdown: text },
|
|
580
593
|
controller.signal
|
|
@@ -724,7 +737,7 @@ async function runTelegramTurn(opts) {
|
|
|
724
737
|
});
|
|
725
738
|
draft.start();
|
|
726
739
|
let reply = "";
|
|
727
|
-
let
|
|
740
|
+
let errorMessage;
|
|
728
741
|
for await (const ev of opts.agentStream(
|
|
729
742
|
{ messages: [{ role: "user", content: opts.userText }] },
|
|
730
743
|
{ threadId, signal: opts.signal, configurable: opts.configurable }
|
|
@@ -732,15 +745,27 @@ async function runTelegramTurn(opts) {
|
|
|
732
745
|
if (ev.type === "token") {
|
|
733
746
|
reply += ev.text;
|
|
734
747
|
draft.push(reply);
|
|
735
|
-
} else if (ev.type === "error")
|
|
748
|
+
} else if (ev.type === "error") errorMessage = ev.message;
|
|
736
749
|
}
|
|
737
|
-
if (
|
|
750
|
+
if (errorMessage !== void 0) {
|
|
751
|
+
log.error("telegram turn errored", {
|
|
752
|
+
chatId: opts.chatKey.chatId,
|
|
753
|
+
err: errorMessage
|
|
754
|
+
});
|
|
738
755
|
draftTornDown = true;
|
|
739
756
|
await draft.abort().catch(() => {
|
|
740
757
|
});
|
|
741
758
|
await opts.checkpointer.rollback(rollback.threadId, rollback.checkpointId).catch(
|
|
742
759
|
(e) => log.error("telegram rollback failed", { err: String(e) })
|
|
743
760
|
);
|
|
761
|
+
if (opts.errorNotice && !opts.signal?.aborted) {
|
|
762
|
+
await opts.client.sendMessage(
|
|
763
|
+
{ chatId: opts.chatKey.chatId, text: opts.errorNotice },
|
|
764
|
+
opts.signal
|
|
765
|
+
).catch(
|
|
766
|
+
(e) => log.error("telegram error notice failed", { err: String(e) })
|
|
767
|
+
);
|
|
768
|
+
}
|
|
744
769
|
return;
|
|
745
770
|
}
|
|
746
771
|
draftTornDown = true;
|
|
@@ -790,6 +815,7 @@ export {
|
|
|
790
815
|
extractTrailingCover,
|
|
791
816
|
isBadRequest,
|
|
792
817
|
mdToTelegramHtml,
|
|
818
|
+
needsRich,
|
|
793
819
|
neutralizeRichMedia,
|
|
794
820
|
repairRichTables,
|
|
795
821
|
runTelegramTurn,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telegram-agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Render markdown to Telegram, stream agent replies into live drafts, and drive a snapshot/rollback turn-loop — runtime-agnostic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"esm"
|
|
29
29
|
],
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
31
|
+
"node": ">=20"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist"
|
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "^2.4.0",
|
|
67
67
|
"@langchain/core": "^1.2.0",
|
|
68
|
-
"@types/node": "^
|
|
68
|
+
"@types/node": "^24.13.3",
|
|
69
69
|
"deepagents": "^1.10.5",
|
|
70
70
|
"tsup": "^8.3.0",
|
|
71
|
-
"typescript": "^
|
|
72
|
-
"vitest": "^
|
|
71
|
+
"typescript": "^6.0.3",
|
|
72
|
+
"vitest": "^4.1.10"
|
|
73
73
|
}
|
|
74
74
|
}
|