@sympoies/dsh-telegram 0.0.0-bootstrap.0 → 0.6.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 +1 -2
- package/NOTICE +19 -0
- package/README.md +589 -2
- package/cordis.patch.yml +14 -0
- package/lib/access.d.ts +76 -0
- package/lib/access.d.ts.map +1 -0
- package/lib/access.js +137 -0
- package/lib/access.js.map +1 -0
- package/lib/client.js +835 -0
- package/lib/commands.d.ts +51 -0
- package/lib/commands.d.ts.map +1 -0
- package/lib/commands.js +83 -0
- package/lib/commands.js.map +1 -0
- package/lib/config.d.ts +125 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +97 -0
- package/lib/config.js.map +1 -0
- package/lib/diagnostics.d.ts +58 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +59 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/failures.d.ts +71 -0
- package/lib/failures.d.ts.map +1 -0
- package/lib/failures.js +139 -0
- package/lib/failures.js.map +1 -0
- package/lib/harness/host.d.ts +106 -0
- package/lib/harness/host.d.ts.map +1 -0
- package/lib/harness/host.js +142 -0
- package/lib/harness/host.js.map +1 -0
- package/lib/harness/message.d.ts +45 -0
- package/lib/harness/message.d.ts.map +1 -0
- package/lib/harness/message.js +28 -0
- package/lib/harness/message.js.map +1 -0
- package/lib/harness/model-selection.d.ts +82 -0
- package/lib/harness/model-selection.d.ts.map +1 -0
- package/lib/harness/model-selection.js +103 -0
- package/lib/harness/model-selection.js.map +1 -0
- package/lib/harness/questions-seam.d.ts +34 -0
- package/lib/harness/questions-seam.d.ts.map +1 -0
- package/lib/harness/questions-seam.js +45 -0
- package/lib/harness/questions-seam.js.map +1 -0
- package/lib/harness/types.d.ts +108 -0
- package/lib/harness/types.d.ts.map +1 -0
- package/lib/harness/types.js +23 -0
- package/lib/harness/types.js.map +1 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +977 -0
- package/lib/index.js.map +1 -0
- package/lib/interact/approvals.d.ts +59 -0
- package/lib/interact/approvals.d.ts.map +1 -0
- package/lib/interact/approvals.js +132 -0
- package/lib/interact/approvals.js.map +1 -0
- package/lib/interact/pending.d.ts +56 -0
- package/lib/interact/pending.d.ts.map +1 -0
- package/lib/interact/pending.js +89 -0
- package/lib/interact/pending.js.map +1 -0
- package/lib/interact/questions.d.ts +100 -0
- package/lib/interact/questions.d.ts.map +1 -0
- package/lib/interact/questions.js +255 -0
- package/lib/interact/questions.js.map +1 -0
- package/lib/interact/surface.d.ts +33 -0
- package/lib/interact/surface.d.ts.map +1 -0
- package/lib/interact/surface.js +39 -0
- package/lib/interact/surface.js.map +1 -0
- package/lib/interact/text-capture.d.ts +38 -0
- package/lib/interact/text-capture.d.ts.map +1 -0
- package/lib/interact/text-capture.js +75 -0
- package/lib/interact/text-capture.js.map +1 -0
- package/lib/media/collect.d.ts +157 -0
- package/lib/media/collect.d.ts.map +1 -0
- package/lib/media/collect.js +235 -0
- package/lib/media/collect.js.map +1 -0
- package/lib/media/extractor.d.ts +117 -0
- package/lib/media/extractor.d.ts.map +1 -0
- package/lib/media/extractor.js +207 -0
- package/lib/media/extractor.js.map +1 -0
- package/lib/media/intake.d.ts +57 -0
- package/lib/media/intake.d.ts.map +1 -0
- package/lib/media/intake.js +115 -0
- package/lib/media/intake.js.map +1 -0
- package/lib/media/limits.d.ts +69 -0
- package/lib/media/limits.d.ts.map +1 -0
- package/lib/media/limits.js +97 -0
- package/lib/media/limits.js.map +1 -0
- package/lib/media/ocr.d.ts +66 -0
- package/lib/media/ocr.d.ts.map +1 -0
- package/lib/media/ocr.js +131 -0
- package/lib/media/ocr.js.map +1 -0
- package/lib/media/screenshot.d.ts +57 -0
- package/lib/media/screenshot.d.ts.map +1 -0
- package/lib/media/screenshot.js +101 -0
- package/lib/media/screenshot.js.map +1 -0
- package/lib/media/vision.d.ts +60 -0
- package/lib/media/vision.d.ts.map +1 -0
- package/lib/media/vision.js +74 -0
- package/lib/media/vision.js.map +1 -0
- package/lib/render/clamp.d.ts +34 -0
- package/lib/render/clamp.d.ts.map +1 -0
- package/lib/render/clamp.js +65 -0
- package/lib/render/clamp.js.map +1 -0
- package/lib/render/escape.d.ts +15 -0
- package/lib/render/escape.d.ts.map +1 -0
- package/lib/render/escape.js +17 -0
- package/lib/render/escape.js.map +1 -0
- package/lib/reply/activity.d.ts +31 -0
- package/lib/reply/activity.d.ts.map +1 -0
- package/lib/reply/activity.js +88 -0
- package/lib/reply/activity.js.map +1 -0
- package/lib/reply/rich-stream.d.ts +173 -0
- package/lib/reply/rich-stream.d.ts.map +1 -0
- package/lib/reply/rich-stream.js +286 -0
- package/lib/reply/rich-stream.js.map +1 -0
- package/lib/reply/split-markdown.d.ts +21 -0
- package/lib/reply/split-markdown.d.ts.map +1 -0
- package/lib/reply/split-markdown.js +87 -0
- package/lib/reply/split-markdown.js.map +1 -0
- package/lib/reply/turn-bridge.d.ts +143 -0
- package/lib/reply/turn-bridge.d.ts.map +1 -0
- package/lib/reply/turn-bridge.js +182 -0
- package/lib/reply/turn-bridge.js.map +1 -0
- package/lib/router.d.ts +415 -0
- package/lib/router.d.ts.map +1 -0
- package/lib/router.js +663 -0
- package/lib/router.js.map +1 -0
- package/lib/secrets.d.ts +30 -0
- package/lib/secrets.d.ts.map +1 -0
- package/lib/secrets.js +51 -0
- package/lib/secrets.js.map +1 -0
- package/lib/session/bindings.d.ts +81 -0
- package/lib/session/bindings.d.ts.map +1 -0
- package/lib/session/bindings.js +170 -0
- package/lib/session/bindings.js.map +1 -0
- package/lib/session/history.d.ts +50 -0
- package/lib/session/history.d.ts.map +1 -0
- package/lib/session/history.js +127 -0
- package/lib/session/history.js.map +1 -0
- package/lib/session/models.d.ts +99 -0
- package/lib/session/models.d.ts.map +1 -0
- package/lib/session/models.js +123 -0
- package/lib/session/models.js.map +1 -0
- package/lib/session/permission.d.ts +83 -0
- package/lib/session/permission.d.ts.map +1 -0
- package/lib/session/permission.js +107 -0
- package/lib/session/permission.js.map +1 -0
- package/lib/session/picker.d.ts +49 -0
- package/lib/session/picker.d.ts.map +1 -0
- package/lib/session/picker.js +107 -0
- package/lib/session/picker.js.map +1 -0
- package/lib/session/preferences.d.ts +55 -0
- package/lib/session/preferences.d.ts.map +1 -0
- package/lib/session/preferences.js +102 -0
- package/lib/session/preferences.js.map +1 -0
- package/lib/session/recovery.d.ts +66 -0
- package/lib/session/recovery.d.ts.map +1 -0
- package/lib/session/recovery.js +112 -0
- package/lib/session/recovery.js.map +1 -0
- package/lib/session/runner.d.ts +223 -0
- package/lib/session/runner.d.ts.map +1 -0
- package/lib/session/runner.js +260 -0
- package/lib/session/runner.js.map +1 -0
- package/lib/session/workspaces.d.ts +28 -0
- package/lib/session/workspaces.d.ts.map +1 -0
- package/lib/session/workspaces.js +53 -0
- package/lib/session/workspaces.js.map +1 -0
- package/lib/telegram/addressing.d.ts +39 -0
- package/lib/telegram/addressing.d.ts.map +1 -0
- package/lib/telegram/addressing.js +81 -0
- package/lib/telegram/addressing.js.map +1 -0
- package/lib/telegram/albums.d.ts +46 -0
- package/lib/telegram/albums.d.ts.map +1 -0
- package/lib/telegram/albums.js +104 -0
- package/lib/telegram/albums.js.map +1 -0
- package/lib/telegram/api.d.ts +238 -0
- package/lib/telegram/api.d.ts.map +1 -0
- package/lib/telegram/api.js +518 -0
- package/lib/telegram/api.js.map +1 -0
- package/lib/telegram/poller.d.ts +59 -0
- package/lib/telegram/poller.d.ts.map +1 -0
- package/lib/telegram/poller.js +94 -0
- package/lib/telegram/poller.js.map +1 -0
- package/lib/telegram/types.d.ts +130 -0
- package/lib/telegram/types.d.ts.map +1 -0
- package/lib/telegram/types.js +9 -0
- package/lib/telegram/types.js.map +1 -0
- package/lib/telegram/typing.d.ts +47 -0
- package/lib/telegram/typing.d.ts.map +1 -0
- package/lib/telegram/typing.js +91 -0
- package/lib/telegram/typing.js.map +1 -0
- package/lib/versions.d.ts +63 -0
- package/lib/versions.d.ts.map +1 -0
- package/lib/versions.js +135 -0
- package/lib/versions.js.map +1 -0
- package/package.json +94 -10
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming one agent turn as a rich message.
|
|
3
|
+
*
|
|
4
|
+
* The agent writes markdown, and since Bot API 10.1 Telegram parses markdown
|
|
5
|
+
* itself — tables, headings, ordered and task lists, fenced code, footnotes,
|
|
6
|
+
* math — so the reply is forwarded almost verbatim rather than approximated
|
|
7
|
+
* in HTML. The message cap rises from 4096 to 32768 characters with it.
|
|
8
|
+
*
|
|
9
|
+
* Telegram offers two ways to show a reply as it is written, and they are not
|
|
10
|
+
* interchangeable:
|
|
11
|
+
*
|
|
12
|
+
* - **Private chats** get `sendRichMessageDraft`: an ephemeral preview that
|
|
13
|
+
* animates between frames carrying the same draft id. It expires after 30
|
|
14
|
+
* seconds and is never persisted, so the turn must end with a real send.
|
|
15
|
+
* - **Groups have no draft API at all.** There the finished reply is simply
|
|
16
|
+
* sent when it is ready.
|
|
17
|
+
*
|
|
18
|
+
* Nothing is sent until there is something worth showing — the first text, or
|
|
19
|
+
* the name of a tool the agent reached for. An ellipsis posted the moment a
|
|
20
|
+
* turn opens says only that a message arrived, which the user already knows,
|
|
21
|
+
* and in a group it is a permanent message saying it. Telegram's own typing
|
|
22
|
+
* indicator covers that stretch far better, and {@link onVisible} is what
|
|
23
|
+
* hands it back once this has something real to show.
|
|
24
|
+
*
|
|
25
|
+
* The draft's expiry is the subtle part: a turn that spends two minutes in a
|
|
26
|
+
* tool call emits no text, so without a heartbeat the preview would vanish and
|
|
27
|
+
* the user would think the bot had died.
|
|
28
|
+
*/
|
|
29
|
+
import type { ChatTarget } from '../interact/surface.js';
|
|
30
|
+
import type { Logger } from '../harness/types.js';
|
|
31
|
+
/** Telegram's rich-message character cap. */
|
|
32
|
+
export declare const RICH_MESSAGE_LIMIT = 32768;
|
|
33
|
+
/** Default gap between frames; Telegram throttles rapid updates to one chat. */
|
|
34
|
+
export declare const DEFAULT_THROTTLE_MS = 1200;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a conversation can show a reply as it is written.
|
|
37
|
+
*
|
|
38
|
+
* Two independent reasons it cannot, and conflating them is how the operator's
|
|
39
|
+
* switch came to do nothing: streaming may be turned off, or the conversation
|
|
40
|
+
* may be a group, which has no draft API at all. Telegram gives groups and
|
|
41
|
+
* channels negative ids, which is the only signal available before a message
|
|
42
|
+
* arrives.
|
|
43
|
+
*
|
|
44
|
+
* @param chatId - the conversation's Telegram id.
|
|
45
|
+
* @param streamingEnabled - the operator's setting.
|
|
46
|
+
*/
|
|
47
|
+
export declare function canStreamTo(chatId: string, streamingEnabled: boolean): boolean;
|
|
48
|
+
/** The Bot API surface a reply needs. */
|
|
49
|
+
export interface RichChat {
|
|
50
|
+
sendRichMessage(options: {
|
|
51
|
+
chatId: string;
|
|
52
|
+
markdown: string;
|
|
53
|
+
threadId?: number;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
messageId: number;
|
|
56
|
+
}>;
|
|
57
|
+
sendRichMessageDraft(options: {
|
|
58
|
+
chatId: string;
|
|
59
|
+
draftId: number;
|
|
60
|
+
markdown: string;
|
|
61
|
+
threadId?: number;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/** Construction options. */
|
|
65
|
+
export interface RichReplyOptions {
|
|
66
|
+
readonly chat: RichChat;
|
|
67
|
+
readonly target: ChatTarget;
|
|
68
|
+
/** Private chats stream through drafts; groups have no draft API. */
|
|
69
|
+
readonly canDraft: boolean;
|
|
70
|
+
/** Stable for the turn: Telegram animates frames sharing a draft id. */
|
|
71
|
+
readonly draftId: number;
|
|
72
|
+
readonly throttleMs?: number;
|
|
73
|
+
readonly limit?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Called once, when this turn first shows something in the chat.
|
|
76
|
+
*
|
|
77
|
+
* Whoever was standing in for it until then — the typing indicator — can
|
|
78
|
+
* stop at that point.
|
|
79
|
+
*/
|
|
80
|
+
readonly onVisible?: () => void;
|
|
81
|
+
readonly logger?: Logger;
|
|
82
|
+
/** Injected so a test never waits on a real timer. */
|
|
83
|
+
readonly heartbeatMs?: number;
|
|
84
|
+
}
|
|
85
|
+
export declare class RichReplyStream {
|
|
86
|
+
private readonly options;
|
|
87
|
+
private readonly chat;
|
|
88
|
+
private readonly target;
|
|
89
|
+
private readonly throttleMs;
|
|
90
|
+
private readonly limit;
|
|
91
|
+
private readonly heartbeatMs;
|
|
92
|
+
private readonly logger;
|
|
93
|
+
/** Raw markdown received so far. */
|
|
94
|
+
private buffer;
|
|
95
|
+
/** What the last frame showed, so an unchanged frame is skipped. */
|
|
96
|
+
private shown;
|
|
97
|
+
/**
|
|
98
|
+
* What the agent is doing, shown above the text while it works.
|
|
99
|
+
*
|
|
100
|
+
* Draft-only: Telegram accepts the thinking block in a draft and nowhere
|
|
101
|
+
* else, and the finished reply should carry the answer, not the scaffolding
|
|
102
|
+
* that produced it.
|
|
103
|
+
*/
|
|
104
|
+
private activity;
|
|
105
|
+
private started;
|
|
106
|
+
private finished;
|
|
107
|
+
private timer;
|
|
108
|
+
/** Whether anything has appeared in the chat for this turn yet. */
|
|
109
|
+
private visible;
|
|
110
|
+
private heartbeat;
|
|
111
|
+
private lastFrame;
|
|
112
|
+
private queue;
|
|
113
|
+
constructor(options: RichReplyOptions);
|
|
114
|
+
/**
|
|
115
|
+
* Open the turn.
|
|
116
|
+
*
|
|
117
|
+
* Deliberately sends nothing. Until the agent writes a word or names a tool
|
|
118
|
+
* there is nothing to show that the typing indicator is not already showing
|
|
119
|
+
* better, and a message posted here would be an ellipsis the user has to
|
|
120
|
+
* look at for the rest of the turn.
|
|
121
|
+
*/
|
|
122
|
+
start(): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Say what the agent is doing, above whatever text has arrived.
|
|
125
|
+
*
|
|
126
|
+
* @param activity - an escaped one-line description, or undefined to clear.
|
|
127
|
+
*/
|
|
128
|
+
showActivity(activity: string | undefined): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Add streamed markdown.
|
|
131
|
+
*
|
|
132
|
+
* @param delta - the new fragment.
|
|
133
|
+
*/
|
|
134
|
+
append(delta: string): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Close the turn, persisting the reply.
|
|
137
|
+
*
|
|
138
|
+
* @param finalText - authoritative full text when the caller has one.
|
|
139
|
+
*/
|
|
140
|
+
finish(finalText?: string): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Close the turn on an error, keeping whatever text had already streamed.
|
|
143
|
+
*
|
|
144
|
+
* @param error - the failure to show under the partial answer.
|
|
145
|
+
*/
|
|
146
|
+
fail(error: unknown): Promise<void>;
|
|
147
|
+
/** Write the finished reply where it will survive the draft's expiry. */
|
|
148
|
+
private persist;
|
|
149
|
+
/** Post one finished chunk. */
|
|
150
|
+
private send;
|
|
151
|
+
/** Show one draft frame, remembering it so the heartbeat can repeat it. */
|
|
152
|
+
private draft;
|
|
153
|
+
/** Report, once, that this turn now shows something in the chat. */
|
|
154
|
+
private becameVisible;
|
|
155
|
+
/** Flush now, or arm a timer for the rest of the throttle window. */
|
|
156
|
+
private schedule;
|
|
157
|
+
/** Send the current buffer as a draft frame, if anything moved. */
|
|
158
|
+
private frame;
|
|
159
|
+
/**
|
|
160
|
+
* Keep the preview alive through a silent stretch.
|
|
161
|
+
*
|
|
162
|
+
* A draft lapses 30 seconds after its last frame, and a turn can spend far
|
|
163
|
+
* longer inside one tool call without emitting a character.
|
|
164
|
+
*/
|
|
165
|
+
private armHeartbeat;
|
|
166
|
+
/** Re-send the frame already showing, to hold it past the draft's expiry. */
|
|
167
|
+
private repeat;
|
|
168
|
+
/** Run one send after the previous one, containing its failures. */
|
|
169
|
+
private enqueue;
|
|
170
|
+
/** Disarm the throttle and the heartbeat. */
|
|
171
|
+
private stopTimers;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=rich-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-stream.d.ts","sourceRoot":"","sources":["../../src/reply/rich-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming one agent turn as a rich message.
|
|
3
|
+
*
|
|
4
|
+
* The agent writes markdown, and since Bot API 10.1 Telegram parses markdown
|
|
5
|
+
* itself — tables, headings, ordered and task lists, fenced code, footnotes,
|
|
6
|
+
* math — so the reply is forwarded almost verbatim rather than approximated
|
|
7
|
+
* in HTML. The message cap rises from 4096 to 32768 characters with it.
|
|
8
|
+
*
|
|
9
|
+
* Telegram offers two ways to show a reply as it is written, and they are not
|
|
10
|
+
* interchangeable:
|
|
11
|
+
*
|
|
12
|
+
* - **Private chats** get `sendRichMessageDraft`: an ephemeral preview that
|
|
13
|
+
* animates between frames carrying the same draft id. It expires after 30
|
|
14
|
+
* seconds and is never persisted, so the turn must end with a real send.
|
|
15
|
+
* - **Groups have no draft API at all.** There the finished reply is simply
|
|
16
|
+
* sent when it is ready.
|
|
17
|
+
*
|
|
18
|
+
* Nothing is sent until there is something worth showing — the first text, or
|
|
19
|
+
* the name of a tool the agent reached for. An ellipsis posted the moment a
|
|
20
|
+
* turn opens says only that a message arrived, which the user already knows,
|
|
21
|
+
* and in a group it is a permanent message saying it. Telegram's own typing
|
|
22
|
+
* indicator covers that stretch far better, and {@link onVisible} is what
|
|
23
|
+
* hands it back once this has something real to show.
|
|
24
|
+
*
|
|
25
|
+
* The draft's expiry is the subtle part: a turn that spends two minutes in a
|
|
26
|
+
* tool call emits no text, so without a heartbeat the preview would vanish and
|
|
27
|
+
* the user would think the bot had died.
|
|
28
|
+
*/
|
|
29
|
+
import { SILENT_LOGGER } from '../harness/types.js';
|
|
30
|
+
import { splitMarkdown } from './split-markdown.js';
|
|
31
|
+
import { thinkingBlock } from './activity.js';
|
|
32
|
+
/** Telegram's rich-message character cap. */
|
|
33
|
+
export const RICH_MESSAGE_LIMIT = 32_768;
|
|
34
|
+
/** Default gap between frames; Telegram throttles rapid updates to one chat. */
|
|
35
|
+
export const DEFAULT_THROTTLE_MS = 1200;
|
|
36
|
+
/**
|
|
37
|
+
* How often to re-send an unchanged draft.
|
|
38
|
+
*
|
|
39
|
+
* Comfortably inside the 30-second expiry, so a long silent tool call cannot
|
|
40
|
+
* let the preview lapse.
|
|
41
|
+
*/
|
|
42
|
+
const HEARTBEAT_MS = 20_000;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a conversation can show a reply as it is written.
|
|
45
|
+
*
|
|
46
|
+
* Two independent reasons it cannot, and conflating them is how the operator's
|
|
47
|
+
* switch came to do nothing: streaming may be turned off, or the conversation
|
|
48
|
+
* may be a group, which has no draft API at all. Telegram gives groups and
|
|
49
|
+
* channels negative ids, which is the only signal available before a message
|
|
50
|
+
* arrives.
|
|
51
|
+
*
|
|
52
|
+
* @param chatId - the conversation's Telegram id.
|
|
53
|
+
* @param streamingEnabled - the operator's setting.
|
|
54
|
+
*/
|
|
55
|
+
export function canStreamTo(chatId, streamingEnabled) {
|
|
56
|
+
return streamingEnabled && !chatId.startsWith('-');
|
|
57
|
+
}
|
|
58
|
+
export class RichReplyStream {
|
|
59
|
+
options;
|
|
60
|
+
chat;
|
|
61
|
+
target;
|
|
62
|
+
throttleMs;
|
|
63
|
+
limit;
|
|
64
|
+
heartbeatMs;
|
|
65
|
+
logger;
|
|
66
|
+
/** Raw markdown received so far. */
|
|
67
|
+
buffer = '';
|
|
68
|
+
/** What the last frame showed, so an unchanged frame is skipped. */
|
|
69
|
+
shown = '';
|
|
70
|
+
/**
|
|
71
|
+
* What the agent is doing, shown above the text while it works.
|
|
72
|
+
*
|
|
73
|
+
* Draft-only: Telegram accepts the thinking block in a draft and nowhere
|
|
74
|
+
* else, and the finished reply should carry the answer, not the scaffolding
|
|
75
|
+
* that produced it.
|
|
76
|
+
*/
|
|
77
|
+
activity;
|
|
78
|
+
started = false;
|
|
79
|
+
finished = false;
|
|
80
|
+
timer;
|
|
81
|
+
/** Whether anything has appeared in the chat for this turn yet. */
|
|
82
|
+
visible = false;
|
|
83
|
+
heartbeat;
|
|
84
|
+
lastFrame = 0;
|
|
85
|
+
queue = Promise.resolve();
|
|
86
|
+
constructor(options) {
|
|
87
|
+
this.options = options;
|
|
88
|
+
this.chat = options.chat;
|
|
89
|
+
this.target = options.target;
|
|
90
|
+
this.throttleMs = options.throttleMs ?? DEFAULT_THROTTLE_MS;
|
|
91
|
+
this.limit = options.limit ?? RICH_MESSAGE_LIMIT;
|
|
92
|
+
this.heartbeatMs = options.heartbeatMs ?? HEARTBEAT_MS;
|
|
93
|
+
this.logger = options.logger ?? SILENT_LOGGER;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Open the turn.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately sends nothing. Until the agent writes a word or names a tool
|
|
99
|
+
* there is nothing to show that the typing indicator is not already showing
|
|
100
|
+
* better, and a message posted here would be an ellipsis the user has to
|
|
101
|
+
* look at for the rest of the turn.
|
|
102
|
+
*/
|
|
103
|
+
async start() {
|
|
104
|
+
if (this.started)
|
|
105
|
+
return;
|
|
106
|
+
this.started = true;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Say what the agent is doing, above whatever text has arrived.
|
|
110
|
+
*
|
|
111
|
+
* @param activity - an escaped one-line description, or undefined to clear.
|
|
112
|
+
*/
|
|
113
|
+
async showActivity(activity) {
|
|
114
|
+
if (this.finished || !this.options.canDraft)
|
|
115
|
+
return;
|
|
116
|
+
if (this.activity === activity)
|
|
117
|
+
return;
|
|
118
|
+
this.activity = activity;
|
|
119
|
+
if (!this.started)
|
|
120
|
+
return await this.start();
|
|
121
|
+
await this.schedule();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Add streamed markdown.
|
|
125
|
+
*
|
|
126
|
+
* @param delta - the new fragment.
|
|
127
|
+
*/
|
|
128
|
+
async append(delta) {
|
|
129
|
+
if (this.finished || delta === '')
|
|
130
|
+
return;
|
|
131
|
+
if (!this.started)
|
|
132
|
+
await this.start();
|
|
133
|
+
this.buffer += delta;
|
|
134
|
+
// Only a draft can show progress; a group waits for the finished reply.
|
|
135
|
+
if (this.options.canDraft)
|
|
136
|
+
await this.schedule();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Close the turn, persisting the reply.
|
|
140
|
+
*
|
|
141
|
+
* @param finalText - authoritative full text when the caller has one.
|
|
142
|
+
*/
|
|
143
|
+
async finish(finalText) {
|
|
144
|
+
if (this.finished)
|
|
145
|
+
return;
|
|
146
|
+
if (finalText !== undefined)
|
|
147
|
+
this.buffer = finalText;
|
|
148
|
+
if (!this.started && this.buffer === '')
|
|
149
|
+
return;
|
|
150
|
+
if (!this.started)
|
|
151
|
+
await this.start();
|
|
152
|
+
this.finished = true;
|
|
153
|
+
this.stopTimers();
|
|
154
|
+
await this.enqueue(() => this.persist(this.buffer));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Close the turn on an error, keeping whatever text had already streamed.
|
|
158
|
+
*
|
|
159
|
+
* @param error - the failure to show under the partial answer.
|
|
160
|
+
*/
|
|
161
|
+
async fail(error) {
|
|
162
|
+
if (this.finished)
|
|
163
|
+
return;
|
|
164
|
+
this.finished = true;
|
|
165
|
+
this.stopTimers();
|
|
166
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
167
|
+
const body = this.buffer === '' ? '' : `${this.buffer}\n\n`;
|
|
168
|
+
await this.enqueue(() => this.persist(`${body}⚠️ ${reason}`));
|
|
169
|
+
}
|
|
170
|
+
/** Write the finished reply where it will survive the draft's expiry. */
|
|
171
|
+
async persist(markdown) {
|
|
172
|
+
const chunks = splitMarkdown(markdown, this.limit);
|
|
173
|
+
if (chunks.length === 0)
|
|
174
|
+
return;
|
|
175
|
+
for (const chunk of chunks)
|
|
176
|
+
await this.send(chunk);
|
|
177
|
+
}
|
|
178
|
+
/** Post one finished chunk. */
|
|
179
|
+
async send(markdown) {
|
|
180
|
+
await this.chat.sendRichMessage({
|
|
181
|
+
chatId: this.target.chatId,
|
|
182
|
+
markdown,
|
|
183
|
+
...(this.target.threadId !== undefined ? { threadId: this.target.threadId } : {}),
|
|
184
|
+
});
|
|
185
|
+
this.becameVisible();
|
|
186
|
+
}
|
|
187
|
+
/** Show one draft frame, remembering it so the heartbeat can repeat it. */
|
|
188
|
+
async draft(markdown) {
|
|
189
|
+
const thinking = thinkingBlock(this.activity);
|
|
190
|
+
const composed = thinking === '' ? markdown : `${thinking}\n\n${markdown}`;
|
|
191
|
+
// A draft cannot exceed the message cap either; the tail is what the user
|
|
192
|
+
// is watching, and the whole text lands when the turn is persisted.
|
|
193
|
+
const frame = composed.length > this.limit ? composed.slice(-this.limit) : composed;
|
|
194
|
+
await this.chat.sendRichMessageDraft({
|
|
195
|
+
chatId: this.target.chatId,
|
|
196
|
+
draftId: this.options.draftId,
|
|
197
|
+
markdown: frame,
|
|
198
|
+
...(this.target.threadId !== undefined ? { threadId: this.target.threadId } : {}),
|
|
199
|
+
});
|
|
200
|
+
this.shown = frame;
|
|
201
|
+
// Armed here rather than at start, because until the first frame there is
|
|
202
|
+
// no preview to keep alive.
|
|
203
|
+
this.armHeartbeat();
|
|
204
|
+
this.becameVisible();
|
|
205
|
+
}
|
|
206
|
+
/** Report, once, that this turn now shows something in the chat. */
|
|
207
|
+
becameVisible() {
|
|
208
|
+
if (this.visible)
|
|
209
|
+
return;
|
|
210
|
+
this.visible = true;
|
|
211
|
+
this.options.onVisible?.();
|
|
212
|
+
}
|
|
213
|
+
/** Flush now, or arm a timer for the rest of the throttle window. */
|
|
214
|
+
async schedule() {
|
|
215
|
+
if (this.throttleMs === 0)
|
|
216
|
+
return await this.enqueue(() => this.frame());
|
|
217
|
+
const due = this.lastFrame + this.throttleMs - Date.now();
|
|
218
|
+
if (due <= 0) {
|
|
219
|
+
this.lastFrame = Date.now();
|
|
220
|
+
return await this.enqueue(() => this.frame());
|
|
221
|
+
}
|
|
222
|
+
if (this.timer)
|
|
223
|
+
return;
|
|
224
|
+
this.timer = setTimeout(() => {
|
|
225
|
+
this.timer = undefined;
|
|
226
|
+
this.lastFrame = Date.now();
|
|
227
|
+
void this.enqueue(() => this.frame());
|
|
228
|
+
}, due);
|
|
229
|
+
}
|
|
230
|
+
/** Send the current buffer as a draft frame, if anything moved. */
|
|
231
|
+
async frame() {
|
|
232
|
+
if (this.finished)
|
|
233
|
+
return;
|
|
234
|
+
// The activity line alone is worth a frame: during a long tool call it is
|
|
235
|
+
// the only thing that changes, and it is the whole point of showing it.
|
|
236
|
+
// With neither there is nothing to draw, and Telegram refuses an empty
|
|
237
|
+
// draft anyway — so the previous frame stays, which is what the reader
|
|
238
|
+
// was looking at.
|
|
239
|
+
if (this.buffer === '' && this.activity === undefined)
|
|
240
|
+
return;
|
|
241
|
+
await this.draft(this.buffer);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Keep the preview alive through a silent stretch.
|
|
245
|
+
*
|
|
246
|
+
* A draft lapses 30 seconds after its last frame, and a turn can spend far
|
|
247
|
+
* longer inside one tool call without emitting a character.
|
|
248
|
+
*/
|
|
249
|
+
armHeartbeat() {
|
|
250
|
+
if (this.heartbeat || this.heartbeatMs <= 0)
|
|
251
|
+
return;
|
|
252
|
+
this.heartbeat = setInterval(() => {
|
|
253
|
+
if (this.finished || this.shown === '')
|
|
254
|
+
return;
|
|
255
|
+
// Re-sends exactly what is on screen. Keeping a preview alive means
|
|
256
|
+
// repeating it, not replacing it with something that says less.
|
|
257
|
+
void this.enqueue(() => this.repeat());
|
|
258
|
+
}, this.heartbeatMs);
|
|
259
|
+
}
|
|
260
|
+
/** Re-send the frame already showing, to hold it past the draft's expiry. */
|
|
261
|
+
async repeat() {
|
|
262
|
+
await this.chat.sendRichMessageDraft({
|
|
263
|
+
chatId: this.target.chatId,
|
|
264
|
+
draftId: this.options.draftId,
|
|
265
|
+
markdown: this.shown,
|
|
266
|
+
...(this.target.threadId !== undefined ? { threadId: this.target.threadId } : {}),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** Run one send after the previous one, containing its failures. */
|
|
270
|
+
enqueue(task) {
|
|
271
|
+
this.queue = this.queue.then(task).catch((error) => {
|
|
272
|
+
this.logger.warn('[dsh-telegram] reply delivery failed', error);
|
|
273
|
+
});
|
|
274
|
+
return this.queue;
|
|
275
|
+
}
|
|
276
|
+
/** Disarm the throttle and the heartbeat. */
|
|
277
|
+
stopTimers() {
|
|
278
|
+
if (this.timer)
|
|
279
|
+
clearTimeout(this.timer);
|
|
280
|
+
this.timer = undefined;
|
|
281
|
+
if (this.heartbeat)
|
|
282
|
+
clearInterval(this.heartbeat);
|
|
283
|
+
this.heartbeat = undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=rich-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rich-stream.js","sourceRoot":"","sources":["../../src/reply/rich-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting a reply that outgrows one message.
|
|
3
|
+
*
|
|
4
|
+
* At 32768 characters this is rare, but a file listing or a long diff still
|
|
5
|
+
* reaches it. Telegram parses the markdown itself, so the split has to happen
|
|
6
|
+
* where the markdown stays coherent: cutting inside a fenced code block would
|
|
7
|
+
* leave the first chunk with an unterminated fence and the second beginning
|
|
8
|
+
* with a stray one, and Telegram would render both wrongly.
|
|
9
|
+
*
|
|
10
|
+
* So the split walks block boundaries — blank lines first, then line breaks —
|
|
11
|
+
* and closes and reopens a fence it had to cut through.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Split markdown into chunks Telegram will accept.
|
|
15
|
+
*
|
|
16
|
+
* @param markdown - the reply as the agent wrote it.
|
|
17
|
+
* @param limit - maximum characters per message.
|
|
18
|
+
* @returns chunks in order; empty when there is no visible content.
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitMarkdown(markdown: string, limit: number): string[];
|
|
21
|
+
//# sourceMappingURL=split-markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split-markdown.d.ts","sourceRoot":"","sources":["../../src/reply/split-markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCvE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting a reply that outgrows one message.
|
|
3
|
+
*
|
|
4
|
+
* At 32768 characters this is rare, but a file listing or a long diff still
|
|
5
|
+
* reaches it. Telegram parses the markdown itself, so the split has to happen
|
|
6
|
+
* where the markdown stays coherent: cutting inside a fenced code block would
|
|
7
|
+
* leave the first chunk with an unterminated fence and the second beginning
|
|
8
|
+
* with a stray one, and Telegram would render both wrongly.
|
|
9
|
+
*
|
|
10
|
+
* So the split walks block boundaries — blank lines first, then line breaks —
|
|
11
|
+
* and closes and reopens a fence it had to cut through.
|
|
12
|
+
*/
|
|
13
|
+
/** Opening or closing fence of a code block, capturing its language. */
|
|
14
|
+
const FENCE = /^\s{0,3}(`{3,}|~{3,})\s*(\S*)/;
|
|
15
|
+
/** How far back from the limit a cleaner boundary is still worth taking. */
|
|
16
|
+
const BOUNDARY_SEARCH_RATIO = 0.5;
|
|
17
|
+
/**
|
|
18
|
+
* Split markdown into chunks Telegram will accept.
|
|
19
|
+
*
|
|
20
|
+
* @param markdown - the reply as the agent wrote it.
|
|
21
|
+
* @param limit - maximum characters per message.
|
|
22
|
+
* @returns chunks in order; empty when there is no visible content.
|
|
23
|
+
*/
|
|
24
|
+
export function splitMarkdown(markdown, limit) {
|
|
25
|
+
const trimmed = markdown.trim();
|
|
26
|
+
if (trimmed === '')
|
|
27
|
+
return [];
|
|
28
|
+
if (trimmed.length <= limit)
|
|
29
|
+
return [trimmed];
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let rest = trimmed;
|
|
32
|
+
/** The fence a previous chunk was cut inside, to reopen at the next start. */
|
|
33
|
+
let openFence;
|
|
34
|
+
while (rest.length > 0) {
|
|
35
|
+
const prefix = openFence ? `${openFence.marker}${openFence.language}\n` : '';
|
|
36
|
+
const budget = limit - prefix.length;
|
|
37
|
+
if (rest.length <= budget) {
|
|
38
|
+
chunks.push(prefix + rest);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
const cut = findCut(rest, budget);
|
|
42
|
+
const body = rest.slice(0, cut);
|
|
43
|
+
const carried = openFence;
|
|
44
|
+
openFence = fenceLeftOpen(body, openFence);
|
|
45
|
+
const suffix = openFence ? `\n${openFence.marker}` : '';
|
|
46
|
+
chunks.push(prefix + body.trimEnd() + suffix);
|
|
47
|
+
rest = rest.slice(cut).replace(/^\n+/, '');
|
|
48
|
+
if (carried && !openFence)
|
|
49
|
+
openFence = undefined;
|
|
50
|
+
}
|
|
51
|
+
return chunks;
|
|
52
|
+
}
|
|
53
|
+
/** The latest clean boundary within `budget`, or a hard cut when there is none. */
|
|
54
|
+
function findCut(markdown, budget) {
|
|
55
|
+
const floor = Math.floor(budget * BOUNDARY_SEARCH_RATIO);
|
|
56
|
+
for (const boundary of ['\n\n', '\n']) {
|
|
57
|
+
const at = markdown.lastIndexOf(boundary, budget - boundary.length);
|
|
58
|
+
if (at >= floor)
|
|
59
|
+
return at + boundary.length;
|
|
60
|
+
}
|
|
61
|
+
return budget;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether a chunk ended inside a code fence, and which one.
|
|
65
|
+
*
|
|
66
|
+
* Fences toggle, so folding them in order tells us what is still open at the
|
|
67
|
+
* cut — the only construct where reopening matters, because Telegram treats
|
|
68
|
+
* the rest of the document as code until the fence closes.
|
|
69
|
+
*/
|
|
70
|
+
function fenceLeftOpen(body, carried) {
|
|
71
|
+
let open = carried;
|
|
72
|
+
for (const line of body.split('\n')) {
|
|
73
|
+
const match = FENCE.exec(line);
|
|
74
|
+
if (!match)
|
|
75
|
+
continue;
|
|
76
|
+
const marker = match[1];
|
|
77
|
+
if (open) {
|
|
78
|
+
// A closing fence is the same character, at least as long.
|
|
79
|
+
if (marker[0] === open.marker[0] && marker.length >= open.marker.length)
|
|
80
|
+
open = undefined;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
open = { marker, language: match[2] ?? '' };
|
|
84
|
+
}
|
|
85
|
+
return open;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=split-markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split-markdown.js","sourceRoot":"","sources":["../../src/reply/split-markdown.ts"],"names":[],"mappings":"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"}
|