@llblab/pi-telegram 0.11.2 → 0.12.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/AGENTS.md +15 -11
- package/BACKLOG.md +0 -10
- package/CHANGELOG.md +18 -0
- package/README.md +18 -14
- package/api/inbound.ts +14 -0
- package/api/keyboard.ts +10 -0
- package/api/outbound.ts +11 -0
- package/api/sections.ts +17 -0
- package/api/updates.ts +11 -0
- package/api/voice.ts +24 -0
- package/docs/README.md +7 -5
- package/docs/architecture.md +160 -225
- package/docs/callback-namespaces.md +3 -3
- package/docs/{inbound-handlers.md → inbound.md} +13 -10
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +13 -10
- package/docs/public-api.md +266 -0
- package/docs/{extension-sections.md → sections.md} +31 -27
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +17 -14
- package/index.ts +84 -238
- package/lib/bindings.ts +301 -0
- package/lib/commands.ts +114 -1
- package/lib/config.ts +43 -2
- package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
- package/lib/lifecycle.ts +41 -6
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +21 -10
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +1 -1
- package/lib/routing.ts +44 -3
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +33 -14
- package/package.json +11 -1
- package/lib/external-handlers.ts +0 -166
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Updates
|
|
2
2
|
|
|
3
|
-
`
|
|
3
|
+
`updates` owns Telegram update classification, default-routing plans, and the public update-handler registry. The internal `polling` domain owns the actual `getUpdates` loop, offsets, and abort/controller state.
|
|
4
|
+
|
|
5
|
+
`pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other pi extensions cannot open a competing polling connection against the same bot — the Telegram Bot API uses a per-bot `offset` cursor, and two loops race each other and lose updates.
|
|
4
6
|
|
|
5
7
|
This document describes the registry that lets layered pi extensions running in the same pi process hook into `pi-telegram`'s polling loop and react to inbound Telegram updates **before** `pi-telegram`'s default routing fires.
|
|
6
8
|
|
|
7
|
-
It is the runtime counterpart to [Callback Namespaces](./callback-namespaces.md): callback namespaces define how to share `callback_data` cleanly;
|
|
9
|
+
It is the runtime counterpart to [Callback Namespaces](./callback-namespaces.md): callback namespaces define how to share `callback_data` cleanly; update handlers define how to observe and optionally short-circuit the dispatch of those updates.
|
|
8
10
|
|
|
9
11
|
## When to use it
|
|
10
12
|
|
|
@@ -16,34 +18,34 @@ Use it when a layered extension needs to:
|
|
|
16
18
|
|
|
17
19
|
If the layered extension only needs to read assistant-visible callbacks, the existing `[callback] <data>` fallback documented in [Callback Namespaces](./callback-namespaces.md) is enough.
|
|
18
20
|
|
|
19
|
-
If the extension needs a durable top-level Telegram menu section with managed rendering, callback routing, authorization, and diagnostics, use the higher-level [Telegram Extension Sections](./
|
|
21
|
+
If the extension needs a durable top-level Telegram menu section with managed rendering, callback routing, authorization, and diagnostics, use the higher-level [Telegram Extension Sections](./sections.md) contract instead of a raw update handler.
|
|
20
22
|
|
|
21
23
|
## Constraints
|
|
22
24
|
|
|
23
|
-
- One bot, one pi process, one `getUpdates`
|
|
24
|
-
-
|
|
25
|
-
-
|
|
25
|
+
- One bot, one pi process, one `getUpdates` loop. This registry does **not** enable running multiple pi instances against the same bot.
|
|
26
|
+
- Handlers run in the polling loop. They must return quickly; long awaits delay subsequent updates.
|
|
27
|
+
- Handler errors are caught and logged silently so polling never breaks. If you need durable error reporting, do it inside your handler.
|
|
26
28
|
- The registry lives on `globalThis`. Module instance identity is not required, so layered extensions can reach it without importing `@llblab/pi-telegram`.
|
|
27
29
|
|
|
28
30
|
## Verdicts
|
|
29
31
|
|
|
30
|
-
Each
|
|
32
|
+
Each handler returns one of:
|
|
31
33
|
|
|
32
34
|
- `"consume"` — `pi-telegram` skips its default routing for this update.
|
|
33
|
-
- `"pass"` or `void` / `undefined` — `pi-telegram` routes the update normally. Other
|
|
35
|
+
- `"pass"` or `void` / `undefined` — `pi-telegram` routes the update normally. Other handlers registered after this one still run for the same update.
|
|
34
36
|
|
|
35
|
-
The first
|
|
37
|
+
The first handler that returns `"consume"` wins; later handlers are not called for that update.
|
|
36
38
|
|
|
37
|
-
## Registering
|
|
39
|
+
## Registering a handler
|
|
38
40
|
|
|
39
41
|
Two equivalent paths.
|
|
40
42
|
|
|
41
43
|
### Typed import (recommended when you can depend on `@llblab/pi-telegram`)
|
|
42
44
|
|
|
43
45
|
```ts
|
|
44
|
-
import {
|
|
46
|
+
import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
|
|
45
47
|
|
|
46
|
-
const off =
|
|
48
|
+
const off = registerTelegramUpdateHandler(async (update) => {
|
|
47
49
|
const cb = (update as { callback_query?: { id?: string; data?: string } })
|
|
48
50
|
.callback_query;
|
|
49
51
|
if (!cb?.data?.startsWith("myext:")) return "pass";
|
|
@@ -57,7 +59,7 @@ off();
|
|
|
57
59
|
|
|
58
60
|
### Zero-coupling globalThis lookup
|
|
59
61
|
|
|
60
|
-
When the layered extension prefers no `import` from `@llblab/pi-telegram`, so load order between the two extensions does not matter and either can be installed first, it must implement the **full v1 registry contract**, not just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on whatever object it finds at `globalThis.
|
|
62
|
+
When the layered extension prefers no `import` from `@llblab/pi-telegram`, so load order between the two extensions does not matter and either can be installed first, it must implement the **full v1 registry contract**, not just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on whatever object it finds at `globalThis.__piTelegramUpdateHandlerRegistry__`, so a partial object would silently break the first update.
|
|
61
63
|
|
|
62
64
|
pi-telegram defensively re-creates the registry if the object on `globalThis` is missing `add` or `dispatch`, validated as `version === 1`, `typeof add === "function"`, and `typeof dispatch === "function"`. Handlers registered against a malformed object are dropped — make sure your bootstrap implements all three fields.
|
|
63
65
|
|
|
@@ -67,21 +69,21 @@ type PiTelegramVerdict =
|
|
|
67
69
|
| "pass"
|
|
68
70
|
| void
|
|
69
71
|
| Promise<"consume" | "pass" | void>;
|
|
70
|
-
type
|
|
72
|
+
type PiTelegramUpdateHandler = (update: unknown) => PiTelegramVerdict;
|
|
71
73
|
|
|
72
|
-
interface
|
|
74
|
+
interface PiTelegramUpdateHandlerRegistry {
|
|
73
75
|
readonly version: 1;
|
|
74
|
-
add: (handler:
|
|
76
|
+
add: (handler: PiTelegramUpdateHandler) => () => void;
|
|
75
77
|
// Required: pi-telegram's polling loop calls this on every update.
|
|
76
78
|
dispatch: (update: unknown) => Promise<"consume" | "pass">;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
const REGISTRY_KEY = "
|
|
81
|
+
const REGISTRY_KEY = "__piTelegramUpdateHandlerRegistry__";
|
|
80
82
|
|
|
81
|
-
function getOrCreateRegistry():
|
|
83
|
+
function getOrCreateRegistry(): PiTelegramUpdateHandlerRegistry {
|
|
82
84
|
const g = globalThis as Record<string, unknown>;
|
|
83
85
|
const existing = g[REGISTRY_KEY] as
|
|
84
|
-
|
|
|
86
|
+
| PiTelegramUpdateHandlerRegistry
|
|
85
87
|
| undefined;
|
|
86
88
|
if (
|
|
87
89
|
existing &&
|
|
@@ -91,8 +93,8 @@ function getOrCreateRegistry(): PiTelegramExternalHandlerRegistry {
|
|
|
91
93
|
) {
|
|
92
94
|
return existing;
|
|
93
95
|
}
|
|
94
|
-
const handlers = new Set<
|
|
95
|
-
const registry:
|
|
96
|
+
const handlers = new Set<PiTelegramUpdateHandler>();
|
|
97
|
+
const registry: PiTelegramUpdateHandlerRegistry = {
|
|
96
98
|
version: 1,
|
|
97
99
|
add(handler) {
|
|
98
100
|
handlers.add(handler);
|
|
@@ -104,7 +106,7 @@ function getOrCreateRegistry(): PiTelegramExternalHandlerRegistry {
|
|
|
104
106
|
const result = await handler(update);
|
|
105
107
|
if (result === "consume") return "consume";
|
|
106
108
|
} catch {
|
|
107
|
-
// Never break polling because of
|
|
109
|
+
// Never break polling because of a handler error.
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
return "pass";
|
|
@@ -120,32 +122,32 @@ const off = getOrCreateRegistry().add((update) => {
|
|
|
120
122
|
});
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
The registry object on `globalThis.
|
|
125
|
+
The registry object on `globalThis.__piTelegramUpdateHandlerRegistry__` is versioned (`version: 1`) and stable across pi-telegram releases; future breaking changes will use a new schema version and a new key.
|
|
124
126
|
|
|
125
127
|
## Interaction with built-in routing
|
|
126
128
|
|
|
127
|
-
`pi-telegram` invokes registered
|
|
129
|
+
`pi-telegram` invokes registered handlers first, then routes the update through its own handlers: commands, app menu, queue menu, model menu, default prompt routing, and callback namespace fallback. If any handler returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
|
|
128
130
|
|
|
129
131
|
This means:
|
|
130
132
|
|
|
131
133
|
- Extensions can claim callback namespaces that `pi-telegram` would otherwise forward as `[callback] <data>` text.
|
|
132
134
|
- Extensions can observe updates by always returning `"pass"`.
|
|
133
|
-
- Extensions must not consume updates that belong to `pi-telegram`'s own prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) unless they are deliberately replacing that behavior.
|
|
135
|
+
- Extensions must not consume updates that belong to `pi-telegram`'s own prefixes (`compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, `section:`) unless they are deliberately replacing that behavior.
|
|
134
136
|
|
|
135
137
|
## Ownership semantics
|
|
136
138
|
|
|
137
|
-
The
|
|
139
|
+
The handler registry is ownership-agnostic and does not interact with the `locks.json` singleton lock documented in [Locks](./locks.md). When the locked polling runtime stops `pi-telegram`'s `getUpdates` loop, for example after ownership is moved to another pi process, handlers stop receiving updates because no updates are being fetched. They are not unregistered.
|
|
138
140
|
|
|
139
|
-
If a layered extension needs to react to ownership changes, it should observe `pi-telegram` lifecycle events through the standard pi extension hooks rather than through the
|
|
141
|
+
If a layered extension needs to react to ownership changes, it should observe `pi-telegram` lifecycle events through the standard pi extension hooks rather than through the handler registry.
|
|
140
142
|
|
|
141
143
|
## Not a multiplexer
|
|
142
144
|
|
|
143
|
-
This registry does not multiplex one bot across multiple pi processes, and it does not bypass Telegram's single-
|
|
145
|
+
This registry does not multiplex one bot across multiple pi processes, and it does not bypass Telegram's single-polling-connection-per-bot constraint. To run multiple pi instances on Telegram, give each instance its own bot and its own `~/.pi/agent` directory; the registry is for layered extensions inside **one** pi process.
|
|
144
146
|
|
|
145
147
|
## Relationship to extension sections
|
|
146
148
|
|
|
147
|
-
|
|
149
|
+
Update handlers are the raw update primitive. Extension sections are the structured Telegram UI layer above that primitive.
|
|
148
150
|
|
|
149
|
-
Use
|
|
151
|
+
Use update handlers for immediate update interception, custom callback namespaces, out-of-band Promise resolution, and update types that should not become a Telegram menu surface.
|
|
150
152
|
|
|
151
153
|
Use extension sections when the desired behavior is a menu-integrated UI: `render(ctx)`, managed callback dispatch, safe runtime ports, stale-callback handling, and diagnostics owned by `pi-telegram`.
|
package/docs/voice.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Voice Integration
|
|
2
2
|
|
|
3
|
-
Voice messages flow through an **inbound transcription → outbound voice reply** pipeline. This document describes the bridge's role in that pipeline; provider-specific mechanics (TTS/STT backends, voice IDs, languages) are owned by voice provider extensions. In `0.11.0`, this is a first-class extension surface: one companion extension can provide STT fallbacks for inbound voice/audio files and TTS fallbacks for outbound Telegram voice replies without owning a second bot
|
|
3
|
+
Voice messages flow through an **inbound transcription → outbound voice reply** pipeline. This document describes the bridge's role in that pipeline; provider-specific mechanics (TTS/STT backends, voice IDs, languages) are owned by voice provider extensions. In `0.11.0`, this is a first-class extension surface: one companion extension can provide STT fallbacks for inbound voice/audio files and TTS fallbacks for outbound Telegram voice replies without owning a second bot polling loop.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ Inbound handlers match `kind: "voice"` or `mime: "audio/*"` to run a transcripti
|
|
|
29
29
|
|
|
30
30
|
The transcription output becomes the raw text of the prompt.
|
|
31
31
|
|
|
32
|
-
Voice provider extensions can also register STT backends with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/
|
|
32
|
+
Voice provider extensions can also register STT backends with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. Inbound command-template handlers and programmatic inbound handlers remain the stronger generic paths and run first; if no matching handler produces output for a voice/audio file, registered transcription providers are tried as fallback in registration order. The first provider that returns non-empty text wins; providers that return `undefined` pass to the next provider, and provider failures are recorded before trying the next provider. This lets a full voice extension provide both TTS and STT without requiring `telegram.json` handler templates, while still preserving operator-configured inbound handlers as the stronger choice.
|
|
33
33
|
|
|
34
34
|
## Voice Reply Policy
|
|
35
35
|
|
|
@@ -79,7 +79,7 @@ The bridge shows a `record_voice` action while delivering and sends the final au
|
|
|
79
79
|
|
|
80
80
|
Providers can implement `getVoicePromptContribution(view)` to inject voice-specific instructions into voice-tagged prompts (for example: "Reply only with the spoken text"). The bridge appends the first non-empty provider contribution when `mirror` or `always` mode tags the turn.
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Import provider APIs from `@llblab/pi-telegram/voice`; see the TSDoc on `registerTelegramVoiceSynthesisProvider` and `TelegramVoiceSynthesisProviderResult` there for the exact interface.
|
|
83
83
|
|
|
84
84
|
The provider receives the raw agent text plus optional `{ lang?, rate? }`.
|
|
85
85
|
|
|
@@ -93,7 +93,7 @@ It must return one of:
|
|
|
93
93
|
|
|
94
94
|
**File format:** Telegram `sendVoice` requires **OGG/Opus** to display the message as a native voice note (waveform, inline playback). MP3 and other formats are accepted by the API but render as regular audio attachments (music note icon, filename visible). **Providers and outbound voice handlers must return `.ogg` or `.opus` files.** Returning non-OGG files causes the bridge to throw and fall back to text delivery.
|
|
95
95
|
|
|
96
|
-
Registration returns a disposer function for cleanup. Extensions should call
|
|
96
|
+
Registration returns a disposer function for cleanup. Stable provider registrations pass a durable `id` in options; omitted ids remain a compatibility path for older providers and receive generated session-local ids. Extensions should call disposers on shutdown or re-register safely on session start when their runtime is recreated.
|
|
97
97
|
|
|
98
98
|
## Outbound Voice Handlers
|
|
99
99
|
|
|
@@ -124,14 +124,17 @@ Priority for outbound voice delivery is: configured `outboundHandlers` with `typ
|
|
|
124
124
|
When the user's "Send Transcript" toggle is ON, return the clean spoken text as `transcriptText`. The bridge attaches it as the caption on the voice message. When the toggle is OFF, return only the audio path (no `transcriptText`).
|
|
125
125
|
|
|
126
126
|
```typescript
|
|
127
|
-
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/
|
|
128
|
-
|
|
129
|
-
registerTelegramVoiceSynthesisProvider(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
127
|
+
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/voice";
|
|
128
|
+
|
|
129
|
+
registerTelegramVoiceSynthesisProvider(
|
|
130
|
+
async (text, options) => {
|
|
131
|
+
const rewritten = rewriteWithSpeechTags(text);
|
|
132
|
+
const audioPath = await myTTS(rewritten, { language: options?.lang });
|
|
133
|
+
const sendTranscript = getUserSendTranscriptPreference(); // from your UI + telegram.json
|
|
134
|
+
return sendTranscript ? { audioPath, transcriptText: text } : { audioPath };
|
|
135
|
+
},
|
|
136
|
+
{ id: "my-voice-provider/tts" },
|
|
137
|
+
);
|
|
135
138
|
```
|
|
136
139
|
|
|
137
140
|
The bridge never sends a separate transcript message. Caption-only is the "ON" behavior.
|
|
@@ -141,7 +144,7 @@ The bridge never sends a separate transcript message. Caption-only is the "ON" b
|
|
|
141
144
|
Voice provider extensions can record runtime events that appear in `/telegram-status` alongside pi-telegram's own events:
|
|
142
145
|
|
|
143
146
|
```typescript
|
|
144
|
-
import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/
|
|
147
|
+
import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/outbound";
|
|
145
148
|
|
|
146
149
|
recordTelegramRuntimeEvent("xai-voice", new Error("TTS failed"), {
|
|
147
150
|
phase: "tts",
|
|
@@ -155,7 +158,7 @@ recordTelegramRuntimeEvent("xai-voice", new Error("TTS failed"), {
|
|
|
155
158
|
|
|
156
159
|
Voice provider extensions can register a Voice Extension Section (settings UI) via `registerTelegramSection`. The section can expose provider-specific controls such as TTS voice, language, speech style, transcript behavior, or STT/TTS enablement. Reply mode is a core pi-telegram setting and belongs in the built-in Settings menu.
|
|
157
160
|
|
|
158
|
-
**Note on resume:** Because the previous automatic persistent re-registration system has been removed, extensions are responsible for re-registering their Voice Extension Section on `session_start` if they want the menu to survive a `pi resume`. See `registerTelegramSection`
|
|
161
|
+
**Note on resume:** Because the previous automatic persistent re-registration system has been removed, extensions are responsible for re-registering their Voice Extension Section on `session_start` if they want the menu to survive a `pi resume`. See `registerTelegramSection` from `@llblab/pi-telegram/sections`.
|
|
159
162
|
|
|
160
163
|
## Prompt Guidance
|
|
161
164
|
|