telegram-agent-kit 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/deepagents/index.js +1 -0
- package/dist/index.d.ts +33 -2
- package/dist/index.js +21 -7
- 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
|
|
@@ -166,6 +172,8 @@ the user in the chat instead of going silent.
|
|
|
166
172
|
- `chunkText(text)` / `safeSlice(text, max)` / `chunkRich(md)` — surrogate-safe splitting
|
|
167
173
|
(classic limit 4096, rich limit 32768).
|
|
168
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.
|
|
169
177
|
|
|
170
178
|
**Draft engine**
|
|
171
179
|
|
|
@@ -181,6 +189,7 @@ the user in the chat instead of going silent.
|
|
|
181
189
|
Pass `errorNotice` (plain text, your language) to have a failed turn say so in the chat —
|
|
182
190
|
omit it and the user just sees the draft stop, which reads as being ignored.
|
|
183
191
|
- `sendReply(client, chatId, reply, opts, signal?)` / `sendText(...)` — the send path on its own.
|
|
192
|
+
`opts` is `{ rich: boolean, log: Logger }`, with the same `rich` semantics as above.
|
|
184
193
|
- Types: `BotClient`, `AgentStream`, `Checkpointer`, `ThreadStore`, `RenderEvent`, `ChatKey`, `Logger`.
|
|
185
194
|
|
|
186
195
|
**Errors**
|
|
@@ -196,6 +205,10 @@ the user in the chat instead of going silent.
|
|
|
196
205
|
`__pregel_*` LangGraph internal-execution key — are stripped so the kit retains full control over
|
|
197
206
|
checkpoint routing and execution.
|
|
198
207
|
- `streamAgent(agent, input, config, signal?)` — lower-level event stream if you need direct control.
|
|
208
|
+
Yields tokens from the **root** agent only: a delegated agent (a `subagents` entry, or the built-in
|
|
209
|
+
`task` tool) runs its own model node and LangChain replays its events into the parent stream, so
|
|
210
|
+
without this its monologue would interleave with the reply. Naming the root agent does not change
|
|
211
|
+
what streams.
|
|
199
212
|
|
|
200
213
|
> `@langchain/core` and `deepagents` are **type-only, optional** peers. The built
|
|
201
214
|
> `/deepagents` bundle contains no runtime import of either, so the core stays
|
package/dist/deepagents/index.js
CHANGED
|
@@ -18,6 +18,7 @@ async function* streamAgent(agent, input, config, signal) {
|
|
|
18
18
|
for await (const ev of iter) {
|
|
19
19
|
if (ev.event === "on_chat_model_stream") {
|
|
20
20
|
if (ev.metadata?.langgraph_node !== "model_request") continue;
|
|
21
|
+
if (ev.metadata?.checkpoint_ns?.includes("|")) continue;
|
|
21
22
|
const text = extractText(ev.data?.chunk?.content);
|
|
22
23
|
if (text) yield { type: "token", text };
|
|
23
24
|
}
|
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>;
|
|
@@ -141,6 +151,27 @@ declare function mdToTelegramHtml(md: string, opts?: MdToHtmlOpts): string;
|
|
|
141
151
|
* non-table prose (mid-body or trailing) is not eligible either (else the orphan
|
|
142
152
|
* glues onto non-table content). Pure and total. */
|
|
143
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;
|
|
144
175
|
/** Extract a standalone cover image that is the LAST non-empty line of `md`:
|
|
145
176
|
* the whole line is a single `://…)` token. Returns the URL plus
|
|
146
177
|
* the body (the reply with that line removed, trailing whitespace trimmed).
|
|
@@ -168,4 +199,4 @@ declare function extractTrailingCover(md: string): {
|
|
|
168
199
|
* total. */
|
|
169
200
|
declare function neutralizeRichMedia(md: string): string;
|
|
170
201
|
|
|
171
|
-
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
|
|
@@ -802,6 +815,7 @@ export {
|
|
|
802
815
|
extractTrailingCover,
|
|
803
816
|
isBadRequest,
|
|
804
817
|
mdToTelegramHtml,
|
|
818
|
+
needsRich,
|
|
805
819
|
neutralizeRichMedia,
|
|
806
820
|
repairRichTables,
|
|
807
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.1",
|
|
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
|
}
|