@llblab/pi-telegram 0.12.0 → 0.13.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 +6 -5
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +25 -3
- package/README.md +3 -33
- package/docs/README.md +1 -1
- package/docs/architecture.md +2 -1
- package/docs/command-templates.md +18 -16
- package/docs/inbound.md +2 -2
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +158 -4
- package/docs/sections.md +5 -5
- package/docs/voice.md +13 -8
- package/index.ts +4 -4
- package/lib/bindings.ts +2 -4
- package/lib/command-templates.ts +249 -60
- package/lib/config.ts +1 -2
- package/lib/inbound.ts +26 -17
- package/lib/locks.ts +4 -1
- package/lib/outbound-buttons.ts +226 -0
- package/lib/outbound-markup.ts +357 -0
- package/lib/outbound-voice.ts +263 -0
- package/lib/outbound.ts +87 -852
- package/lib/preview.ts +1 -1
- package/lib/queue.ts +3 -0
- package/lib/replies.ts +4 -1
- package/lib/status.ts +13 -0
- package/lib/{api.ts → telegram-api.ts} +4 -4
- package/lib/text-groups.ts +3 -2
- package/lib/voice.ts +35 -8
- package/package.json +12 -12
package/AGENTS.md
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
|
|
98
98
|
- Keep comments and user-facing docs in English unless the surrounding file already follows another convention
|
|
99
99
|
- Each project `.ts` file should start with a short multi-line responsibility header comment that explains the file boundary to future maintainers; source-module headers must include `Zones:` tags for cross-cutting responsibility areas
|
|
100
|
-
- Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `
|
|
100
|
+
- Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `queue.ts`, `updates.ts`, and `queue.test.ts` over redundant `telegram-*` filename prefixes. Exception: the concrete Bot API transport domain is named `telegram-api.ts` / `telegram-api.test.ts` to avoid ambiguity with the public `/api/*.ts` package membranes
|
|
101
101
|
- Keep test helpers with the mirrored domain suite by default because test files mirror module-domain boundaries; introduce shared `tests/fixtures` only when multiple domain suites truly reuse the same setup
|
|
102
102
|
- Prefer targeted edits, keeping `index.ts` as the orchestration layer and moving reusable logic into flat `/lib` domain modules when a subsystem becomes large enough to earn extraction
|
|
103
103
|
- Keep composition wiring DRY with small local adapters or owning-domain contracts when repetition appears, but do not hide live mutable session state behind broad facades just to reduce repeated closures
|
|
@@ -109,7 +109,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
|
|
|
109
109
|
|
|
110
110
|
- Scheduling and lifecycle: `queue`, `runtime`, `lifecycle`, `locks`
|
|
111
111
|
- Telegram transport and inbound flow: `api`, `polling`, `updates`, `routing`, `media`, `turns`, `inbound`, `config`, `setup`
|
|
112
|
-
- Response surfaces: `preview`, `replies`, `rendering`, `keyboard`, `outbound-attachments`, `outbound`, `voice`, `status`
|
|
112
|
+
- Response surfaces: `preview`, `replies`, `rendering`, `keyboard`, `outbound-markup`, `outbound-attachments`, `outbound`, `voice`, `status`
|
|
113
113
|
- Controls and application menu UI: `commands`, `menu`, `menu-model`, `menu-thinking`, `menu-status`, `menu-queue`, `model`, `prompts`
|
|
114
114
|
- Extension platform: `sections` owns section registry, token mapping, callback dispatch, context building, and its globalThis bridge; `voice` owns the voice-provider registry and its globalThis bridge
|
|
115
115
|
- Pi SDK boundary: `pi` owns direct pi imports and bound extension API ports; `bindings` owns pi-facing command/tool/lifecycle registration wiring extracted from the entrypoint
|
|
@@ -119,7 +119,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
|
|
|
119
119
|
- Keep preview appearance logic in the rendering domain and preview transport/lifecycle logic in the preview domain so richer streaming strategies can evolve without entangling Telegram delivery state with Markdown formatting rules
|
|
120
120
|
- Keep direct `node:*` file-operation dependencies out of `index.ts` when an owning domain exists; the entrypoint should compose ports while domains own local filesystem details such as temp-dir preparation, attachment stats, and turn image reads
|
|
121
121
|
- In `index.ts`, prefer namespace imports for local bridge domains so orchestration reads as domain-scoped calls such as `Queue.*`, `Turns.*`, and `Rendering.*` instead of long flat import lists
|
|
122
|
-
- Keep the local `index.ts` plus `/lib/*.ts` import graph acyclic; `tests/invariants.test.ts` guards this boundary plus shared-bucket bans, empty interface-extension shell regressions, pi SDK centralization, source-only entrypoint Node-runtime/local-adapter/process/direct-pi access avoidance, runtime-domain isolation, structural leaf-domain import isolation, menu/model boundary drift, API/config default coupling, structural update/media coupling to API transport shapes, and attachment coupling to queue/inbound media/API helpers as domains keep evolving
|
|
122
|
+
- Keep the local `index.ts` plus `/lib/*.ts` import graph acyclic; `tests/invariants.test.ts` guards this boundary plus shared-bucket bans, empty interface-extension shell regressions, pi SDK centralization, source-only entrypoint Node-runtime/local-adapter/process/direct-pi access avoidance, runtime-domain isolation, structural leaf-domain import isolation, menu/model boundary drift, Telegram API/config default coupling, structural update/media coupling to Telegram API transport shapes, and attachment coupling to queue/inbound media/Telegram API helpers as domains keep evolving
|
|
123
123
|
- Do not reintroduce shared bucket domains such as `lib/constants.ts`, `lib/types.ts`, `lib/globals.ts`, or broad global-augmentation files; constants, registry keys, state interfaces, and concrete transport shapes should stay in their owning domains, and `index.ts` should not grow new shared magic constants
|
|
124
124
|
- Keep remaining `index.ts` code focused on cross-domain adapter wiring that needs live extension state, pi callbacks, Telegram API ports, or status updates; do not extract one-off closures solely to reduce line count
|
|
125
125
|
- Domain-specific queue planning, preview transport/controller behavior, rendering, Telegram API transport, menu state, and command behavior should stay in their owning domains instead of moving to `/lib/runtime.ts` solely to shrink `index.ts`
|
|
@@ -153,12 +153,13 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
|
|
|
153
153
|
- Unknown callback data not owned by pi-telegram prefixes (`compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `section:`, `settings:`) may be forwarded as `[callback] <data>` after built-in handlers decline it; update-handler extensions should follow `docs/callback-namespaces.md` and must not poll the same bot independently
|
|
154
154
|
- Command templates stay compact and shell-free: no `command` field, no shell execution, inline defaults are allowed as `{name=default}`, `template` may be a string or an ordered composition array, only `args`/`defaults` inherit into leaves, top-level `timeout` wraps composed sequences, stdout pipes to the next step's stdin by default, and multi-step work should use `template: [...]` rather than provider-specific fields; `pipe` is only a legacy local alias
|
|
155
155
|
- Command-template documentation examples should use portable executable placeholders such as `/path/to/stt` and `/path/to/tts`, not host-local skill paths or machine-specific install locations
|
|
156
|
+
- Abstract pi-telegram docs and README examples must not leak real companion-extension identities into generic provider ids, runtime-event categories, or sample code. Use neutral ids such as `@scope/voice-provider/tts` and reserve real companion names for explicit companion-extension lists or case-study references.
|
|
156
157
|
|
|
157
158
|
## 9. Extension Sections Conventions
|
|
158
159
|
|
|
159
160
|
- `Section identity`: use the same identity-key rules as the Extension Locks Standard (`package.json/name` → canonical id); no separate `owner` field
|
|
160
161
|
- `Token mapping`: Telegram's 64-byte `callback_data` limit forces compact numeric tokens (`section:0:action:payload`). Section authors never hand-roll `section:` strings — use `ctx.callbackData(action, payload?)`
|
|
161
|
-
- `Navigation hierarchy`: Back buttons are auto-prepended by `ctx.edit()`
|
|
162
|
+
- `Navigation hierarchy`: Back buttons are auto-prepended by `ctx.edit()` only. Root views use `⬆️ Main menu` → `menu:back`. Nested views from `handleCallback` use `⬆️ Back` → `section:<token>:open`. Settings views use `⬆️ Back` → `settings:list`. `ctx.open()` sends standalone chat messages without auto-navigation
|
|
162
163
|
- `Context ports`: sections receive `TelegramSectionContext` / `TelegramSectionCallbackContext` with `answerCallback`, `edit`, `open`, `enqueuePrompt`, and `callbackData`. No filesystem access, no raw bot clients, no second polling loop
|
|
163
164
|
- `Settings indicators`: use `settings.getLabel()` for dynamic status rows in the Settings submenu (e.g., `🟢`/`⚫️` based on internal state). Called on every Settings list render
|
|
164
165
|
- `Handler fallback`: `section.handleCallback` runs first; if it returns `"pass"` and `settings.handleCallback` exists, the settings handler runs with a fresh context carrying `backCallback="settings:list"`
|
|
@@ -168,7 +169,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
|
|
|
168
169
|
- `Section separators`: extension-injected main-menu rows appear before the **⚙️ Settings** row. Extension settings rows appear before built-in Proactive push controls
|
|
169
170
|
- `Model button format`: use `provider/ModelId` format (e.g., `anthropic/claude-sonnet-4-5`) across model menu buttons and status row. The compact `provider/id` form is canonical
|
|
170
171
|
- `Section domain ownership`: `lib/sections.ts` owns the registry, token mapping, callback dispatch, and context building. `lib/menu.ts` dispatches `section:` callbacks before built-in handling. `lib/menu-status.ts` injects section rows. `lib/menu-settings.ts` injects settings rows and passes `sectionRegistry` through callback deps
|
|
171
|
-
- `Callback routing order`: button actions → queue menu → settings menu → section callbacks → built-in menu handling → `[callback]` fallback. Settings menu callbacks always pass `sectionRegistry` to `updateTelegramSettingsMenuMessage` and `handleTelegramSettingsMenuCallbackAction`
|
|
172
|
+
- `Callback routing order`: button actions → compact confirmations → queue menu → settings menu → section callbacks → built-in menu handling → `[callback]` fallback. Settings menu callbacks always pass `sectionRegistry` to `updateTelegramSettingsMenuMessage` and `handleTelegramSettingsMenuCallbackAction`
|
|
172
173
|
|
|
173
174
|
## 9. Pre-Task Preparation Protocol
|
|
174
175
|
|
package/BACKLOG.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
-
|
|
3
|
+
## 0.13.0: Command Template Standard, Voice Hardening, And Domain Cleanup (2026-05-22)
|
|
4
|
+
|
|
5
|
+
- `[Architecture]` Extracted outbound assistant-action markup parsing into `lib/outbound-markup.ts` and removed the temporary Voice/Outbound/Queue import-cycle allowance. Impact: project source imports are fully acyclic again while preserving existing voice and outbound helper exports.
|
|
6
|
+
- `[Tests]` Updated the Pi SDK centralization invariant to guard the current `@earendil-works/*` package scope as well as the legacy scope. Impact: new direct SDK imports outside `lib/pi.ts` are caught again.
|
|
7
|
+
- `[Voice Providers]` Generated compatibility ids for STT/TTS providers now use monotonic counters with registry probing, and provider disposers now remove only their own registered instance. Impact: anonymous ids stay collision-safe across stale registry entries/module reloads, and stale disposers cannot delete replacement providers.
|
|
8
|
+
- `[Security]` Telegram temp directories, downloaded inbound files, and `locks.json` writes now use explicit private filesystem modes. Impact: private attachments and local polling ownership metadata are less exposed on permissive-umask or shared hosts.
|
|
9
|
+
- `[Docs]` Clarified that `voice.sendTranscript` is the bridge-owned transcript preference and updated provider examples to gate `transcriptText` with `getTelegramVoiceSendTranscript(config)`. Impact: companion voice providers no longer need to invent duplicate transcript/reply-policy UI.
|
|
10
|
+
- `[Tests]` Added a long-session runtime regression that preserves queued work across `/abort`, explicit `/next`, and in-flight model switching. Impact: the high-risk queue/dispatch path now has coverage for an operator session that chains abort recovery into model-switch continuation.
|
|
11
|
+
- `[Naming]` Renamed the concrete Bot API transport domain from `lib/api.ts` / `tests/api.test.ts` to `lib/telegram-api.ts` / `tests/telegram-api.test.ts`. Impact: the transport owner is clearer and no longer competes conceptually with the public `/api/*.ts` package membranes.
|
|
12
|
+
- `[Status]` `/telegram-status` recent runtime events now include a category summary before the detailed event list. Impact: provider, handler, and transport failure categories are easier to scan during companion-extension diagnostics.
|
|
13
|
+
- `[Tests]` Added a combined `telegram_voice` + `telegram_button` outbound planner smoke regression and corrected the text-group non-contiguous-tail fixture to reflect the widened split-message id gap. Impact: the rendering/markup watchlist now has direct coverage for mixed assistant action markup without changing runtime behavior.
|
|
14
|
+
- `[Audit]` Added fixture lifecycle coverage for STT/TTS provider re-registration across session-start/resume/reload-style boundaries, and scanned the local workspace for removed `@llblab/pi-telegram/lib/*` companion imports. Impact: the remaining hotfix lifecycle and migration audit items are now closed in this environment.
|
|
15
|
+
- `[Refactor]` Split assistant-authored button planning, callback storage, callback handling, and button prompt-turn construction from `lib/outbound.ts` into `lib/outbound-buttons.ts`, with symmetric `tests/outbound-buttons.test.ts` coverage. Impact: the outbound domain is smaller and button behavior now has a direct module-level test peer while existing `lib/outbound.ts` exports remain compatible.
|
|
16
|
+
- `[Tests]` Added symmetric `tests/outbound-markup.test.ts` coverage for `lib/outbound-markup.ts`, including top-level comment collection, fenced-code exclusion, partial-comment stripping, attribute parsing, and multi-voice planning. Impact: assistant action markup has direct module-level regression coverage instead of relying only on umbrella outbound tests.
|
|
17
|
+
- `[Tests]` Added symmetric `tests/menu-status.test.ts` coverage for `lib/menu-status.ts`, including status reply markup rows, voice-active thinking suppression, callback routing, and status message send/update helpers. Impact: status-menu behavior now has a direct module-level regression peer.
|
|
18
|
+
- `[Tests]` Added symmetric `tests/menu-thinking.test.ts` coverage for `lib/menu-thinking.ts`, including reply markup, thinking-level callbacks, invalid/voice/non-reasoning guards, and menu send/update helpers. Impact: thinking-menu behavior now has direct module-level regression coverage.
|
|
19
|
+
- `[Tests]` Added symmetric `tests/setup.test.ts` coverage for `lib/setup.ts`, including token defaults, prompt mode selection, setup success/failure, and prompt-runtime guard cleanup after errors. Impact: `/telegram-setup` prompt behavior now has direct module-level regression coverage.
|
|
20
|
+
- `[Tests]` Added symmetric `tests/menu-settings.test.ts` coverage for `lib/menu-settings.ts`, including built-in settings markup, detail menu active states, settings mutations, and stale-message fallback toggles. Impact: settings-menu behavior now has direct module-level regression coverage.
|
|
21
|
+
- `[Context]` Cleared the completed backlog down to `No open work` and tightened the `lib/outbound.ts` domain header after button/markup extraction. Impact: historical completion notes stay in the changelog, while backlog and domain ownership now reflect current reality.
|
|
22
|
+
- `[Refactor]` Split native Telegram voice delivery from `lib/outbound.ts` into `lib/outbound-voice.ts`, with symmetric `tests/outbound-voice.test.ts` coverage. Impact: outbound voice upload/provider orchestration now has a direct domain owner and `lib/outbound.ts` is reduced to the outbound surface, command handlers, text transforms, artifact composition, and compatibility re-exports.
|
|
23
|
+
- `[Command Templates]` Replaced `pi-telegram`'s command-template implementation with the current `pi-actors` standard as a deliberate breaking 0.x minor change. Impact: templates now use `parallel` and `when`, support string `timeout`/`delay`/`retry`, inherited default references, `{value??fallback}`, `{flag?yes:no}`, and empty-arg filtering; old local `mode`, `critical`, and `pipe` shapes were removed/migrated to `parallel`, `failure`, and `template: [...]`.
|
|
24
|
+
- `[Docs]` Removed real companion-extension identities from abstract voice-provider examples and README guidance, keeping concrete names only in the explicit Companion Extensions list. Impact: pi-telegram documentation now keeps bridge responsibilities separate from third-party companion-extension ownership.
|
|
25
|
+
- `[Docs]` Integrated public API smoke examples for Extension Sections, raw updates, inbound handlers, outbound handlers, and voice providers into `docs/public-api.md` using only stable public membranes. Impact: companion-extension authors get copyable patterns in the canonical public API guide and avoid removed `@llblab/pi-telegram/lib/*` imports.
|
|
26
|
+
- `[Docs]` Updated README release positioning and corrected Extension Sections docs so `ctx.edit()` owns automatic Back-row insertion, `ctx.open()` is documented as a standalone chat-message sender, and section callbacks appear before built-in menu callbacks. Impact: companion extension authors get an accurate public contract.
|
|
27
|
+
- `[Backlog]` Restored concrete near-term follow-up slices for public API migration, provider lifecycle, smoke examples, diagnostics, voice-domain DAG cleanup, section examples, and voice transcript config clarity. Impact: README's open-work link points to actionable development priorities again.
|
|
6
28
|
|
|
7
29
|
## 0.12.0: Public API Membranes, Telegram UX Safety, And Extension Interop
|
|
8
30
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Telegram runtime adapter for π.**
|
|
6
6
|
|
|
7
|
-
`pi-telegram` turns a private Telegram DM into a session-local operator console for π. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, lets other extensions share one bot, and turns assistant-authored intent into native Telegram artifacts.
|
|
7
|
+
`pi-telegram` turns a private Telegram DM into a session-local operator console for π. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, lets other extensions share one bot, and turns assistant-authored intent into native Telegram artifacts. It is also a voice-provider platform: companion extensions can supply Telegram transcription and synthesis providers while `pi-telegram` keeps ownership of transport, queueing, and reply policy.
|
|
8
8
|
|
|
9
9
|
This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
10
10
|
|
|
@@ -199,39 +199,9 @@ If `telegram.json` explicitly sets a valid `voice.replyMode`, prompts include co
|
|
|
199
199
|
|
|
200
200
|
In `mirror` and `always` modes, the bridge transparently intercepts agent text responses and routes them through the outbound voice pipeline. Configured `outboundHandlers` with `type: "voice"` run first in their configured order; zero-config registered synthesis providers run after them as progressive fallbacks. If several synthesis providers are installed, they are tried in registration order and the first one that returns a valid `.ogg`/`.opus` artifact handles the reply; `undefined`, errors, or invalid output fall through to the next provider. If every voice generator fails, the bridge falls back to sending the text reply instead.
|
|
201
201
|
|
|
202
|
-
Voice synthesis provider extensions
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/voice";
|
|
206
|
-
import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/outbound";
|
|
207
|
-
|
|
208
|
-
// Return path only (backward compatible)
|
|
209
|
-
const dispose = registerTelegramVoiceSynthesisProvider(
|
|
210
|
-
async (text, { lang, rate }) => {
|
|
211
|
-
const path = await myTTS(text, { language: lang });
|
|
212
|
-
return path; // must be .ogg or .opus
|
|
213
|
-
},
|
|
214
|
-
{ id: "pi-xai-voice/tts" },
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
// Return path + transcript caption
|
|
218
|
-
const dispose2 = registerTelegramVoiceSynthesisProvider(
|
|
219
|
-
async (text, { lang, rate }) => {
|
|
220
|
-
const rewritten = rewriteWithSpeechTags(text); // internal TTS optimization
|
|
221
|
-
const path = await myTTS(rewritten, { language: lang });
|
|
222
|
-
return { audioPath: path, transcriptText: text };
|
|
223
|
-
},
|
|
224
|
-
{ id: "pi-xai-voice/tts-with-transcript" },
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// Surface diagnostics in /telegram-status
|
|
228
|
-
recordTelegramRuntimeEvent("xai-voice", new Error("TTS complete"), {
|
|
229
|
-
phase: "tts",
|
|
230
|
-
durationMs: 1200,
|
|
231
|
-
});
|
|
232
|
-
```
|
|
202
|
+
Voice synthesis provider extensions register TTS backends at runtime through public API domain subpaths. Multiple synthesis providers can be registered; stable provider registrations pass a durable abstract id owned by the companion extension, such as `"@scope/voice-provider/tts"`. The bridge tries configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers in registration order until one succeeds. Providers and handlers receive the text to synthesize and optional `lang`/`rate` hints from `<!-- telegram_voice -->` markup or the automatic interception path. Voice delivery must produce `.ogg` or `.opus` files.
|
|
233
203
|
|
|
234
|
-
|
|
204
|
+
Provider code examples, transcript-caption behavior, and diagnostics patterns live in [Voice Integration](./docs/voice.md) and [Public API](./docs/public-api.md). The important boundary is: providers own TTS and any private optimization, while pi-telegram owns reply policy, prompt context, fallback ordering, and Telegram transport.
|
|
235
205
|
|
|
236
206
|
### Extension interop
|
|
237
207
|
|
package/docs/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Living index of project documentation in `/docs`.
|
|
|
5
5
|
## Documents
|
|
6
6
|
|
|
7
7
|
- [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, queueing model, rendering pipeline, and interactive controls
|
|
8
|
-
- [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, and compatibility boundaries
|
|
8
|
+
- [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, smoke examples, and compatibility boundaries
|
|
9
9
|
- [command-templates.md](./command-templates.md) — Portable command-template standard core
|
|
10
10
|
- [inbound.md](./inbound.md) — Local `pi-telegram` inbound text/media handler bus, programmatic inbound handlers, registered STT provider fallbacks, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
|
|
11
11
|
- [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, voice synthesis provider fallback priority, artifact outputs, and callback routing
|
package/docs/architecture.md
CHANGED
|
@@ -46,7 +46,8 @@ The repository uses a **Flat Domain DAG**:
|
|
|
46
46
|
- `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
|
|
47
47
|
- `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
|
|
48
48
|
- `preview` / `replies` / `rendering`: streaming preview lifecycle, final reply delivery, reply parameters, Telegram HTML rendering, chunking, and stable preview snapshots.
|
|
49
|
-
- `outbound`:
|
|
49
|
+
- `outbound-markup`: top-level assistant action comment parsing, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
50
|
+
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
50
51
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, and photo/document delivery classification.
|
|
51
52
|
- `status`: status bar/status-message rendering, queue-lane summaries, redacted event ring, and grouped diagnostics.
|
|
52
53
|
- `lifecycle` / `prompts` / `prompt-templates` / `pi`: π hook registration, Telegram prompt guidance, prompt-template discovery/expansion, and centralized direct π SDK imports.
|
|
@@ -33,14 +33,14 @@ There is no portable `command` field. The command is derived from `template`: af
|
|
|
33
33
|
Common object fields:
|
|
34
34
|
|
|
35
35
|
- `label`: Optional human label for diagnostics and parallel branch reports.
|
|
36
|
-
- `
|
|
36
|
+
- `parallel`: Optional boolean execution flag for array templates. Default is sequential execution; `true` runs children concurrently when the host execution layer supports branch fanout.
|
|
37
|
+
- `when`: Optional boolean or condition string. Falsy values skip the node. String forms may reference a flag name, `!flag`, or a placeholder expression such as `{flag?yes:}`.
|
|
37
38
|
- `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `timeout:int=60000` before persistence.
|
|
38
39
|
- `defaults`: Placeholder default values by name.
|
|
39
|
-
- `timeout`: Optional execution timeout in milliseconds. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
|
|
40
|
-
- `delay`: Optional wait in milliseconds before starting this node. Default is no delay.
|
|
40
|
+
- `timeout`: Optional execution timeout in milliseconds, as a number or placeholder-resolved string. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely.
|
|
41
|
+
- `delay`: Optional wait in milliseconds before starting this node, as a number or placeholder-resolved string. Default is no delay.
|
|
41
42
|
- `output`: Optional result selector. Default is `"stdout"`; runtime values such as `"ogg"` are valid.
|
|
42
|
-
- `retry`: Optional max attempts including the first. Default is `1`.
|
|
43
|
-
- `critical`: Optional boolean. Backward-compatible alias for `failure: "root"`.
|
|
43
|
+
- `retry`: Optional max attempts including the first, as a number or placeholder-resolved string. Default is `1`.
|
|
44
44
|
- `failure`: Optional failure propagation scope: `continue`, `branch`, or `root`. Default is `continue`.
|
|
45
45
|
- `recover`: Optional command template run between failed retry attempts. Recovery output is ignored; recovery failure stops retries.
|
|
46
46
|
- `template`: Required command string or ordered composition array.
|
|
@@ -68,6 +68,8 @@ Supported forms:
|
|
|
68
68
|
| `{name}` | Required value from runtime values or `defaults` |
|
|
69
69
|
| `{name=default}` | Inline default when no value is provided |
|
|
70
70
|
| `{items[index]}` | Array item selected by literal or repeat index |
|
|
71
|
+
| `{value??fallback}` | Fallback when the value is absent or falsy |
|
|
72
|
+
| `{flag?yes:no}` | Conditional text selected by flag truthiness |
|
|
71
73
|
|
|
72
74
|
Resolution order is runtime values → `defaults` → inline default → error. Default values that are themselves a single placeholder, such as `{prompt}` resolving to `{prompts[index]}`, are resolved recursively with a small depth guard. A repeat node may set `repeat` to `{items.length}` when an array arg should determine fanout width.
|
|
73
75
|
|
|
@@ -127,8 +129,8 @@ template="echo 'literal words' {text}"
|
|
|
127
129
|
|
|
128
130
|
Composition rules:
|
|
129
131
|
|
|
130
|
-
- Execute leaves in order
|
|
131
|
-
- Execute child templates concurrently when `
|
|
132
|
+
- Execute leaves in order by default
|
|
133
|
+
- Execute child templates concurrently when `parallel` is `true`
|
|
132
134
|
- Parallel composition uses soft-quorum semantics by default: failed children are reported as degraded branches unless failure propagation escalates
|
|
133
135
|
- Non-critical failures are recorded and execution continues, while `failure: "branch"` stops the current branch and `failure: "root"` aborts the root composition
|
|
134
136
|
- Treat the whole composition as one handler for selector matching and fallback
|
|
@@ -163,7 +165,7 @@ Composition rules:
|
|
|
163
165
|
|
|
164
166
|
```json
|
|
165
167
|
{
|
|
166
|
-
"
|
|
168
|
+
"parallel": true,
|
|
167
169
|
"repeat": 8,
|
|
168
170
|
"template": "render page{_(index+1)}.html --prev page{_(prev+1)}.html --next page{_(next+1)}.html --zero page{_index}.html"
|
|
169
171
|
}
|
|
@@ -198,7 +200,7 @@ Parallel nodes use the same object shape. Flags come first and `template` stays
|
|
|
198
200
|
"template": [
|
|
199
201
|
"prepare {out_dir}",
|
|
200
202
|
{
|
|
201
|
-
"
|
|
203
|
+
"parallel": true,
|
|
202
204
|
"template": [
|
|
203
205
|
{
|
|
204
206
|
"label": "gpt-5.5",
|
|
@@ -232,7 +234,7 @@ exit: 1
|
|
|
232
234
|
stderr: provider balance exhausted
|
|
233
235
|
```
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
Use `template: [...]` for ordered composition. Older local `pipe` aliases are not part of the 0.13.0 command-template standard.
|
|
236
238
|
|
|
237
239
|
## Fail-Open Default Policy
|
|
238
240
|
|
|
@@ -250,7 +252,7 @@ Use `failure` when a node should stop more aggressively:
|
|
|
250
252
|
|
|
251
253
|
```json
|
|
252
254
|
{
|
|
253
|
-
"
|
|
255
|
+
"parallel": true,
|
|
254
256
|
"template": [
|
|
255
257
|
{
|
|
256
258
|
"label": "agent-a",
|
|
@@ -276,7 +278,7 @@ Use `failure` when a node should stop more aggressively:
|
|
|
276
278
|
|
|
277
279
|
If `agent-a-validate` fails, `agent-a-push` is skipped, `agent-b` can still finish, and the parallel join reports degraded branch coverage.
|
|
278
280
|
|
|
279
|
-
`
|
|
281
|
+
Use `failure: "root"` to abort the root composition. Older local `critical: true` shapes are not part of the 0.13.0 command-template standard.
|
|
280
282
|
|
|
281
283
|
## Retry
|
|
282
284
|
|
|
@@ -334,13 +336,13 @@ The standard uses a single `template` field that grows with the user's needs:
|
|
|
334
336
|
string → leaf command
|
|
335
337
|
string[] → sequential composition
|
|
336
338
|
{ template } → leaf command object
|
|
337
|
-
{
|
|
338
|
-
{
|
|
339
|
+
{ parallel, template } → parallel subtree
|
|
340
|
+
{ parallel, when, args, defaults, delay, retry, failure, recover, output, template } → full node
|
|
339
341
|
```
|
|
340
342
|
|
|
341
|
-
Start with a string. Add composition when needed. Add `
|
|
343
|
+
Start with a string. Add composition when needed. Add `parallel: true` when independent work can run concurrently. Add `when` for conditional nodes. Add delay when launch pacing matters. Add retry when flaky. Add `failure` when propagation scope matters. Add `recover` when a retried node needs cleanup before another attempt. Same contract, growing capability, no dead weight.
|
|
342
344
|
|
|
343
|
-
`
|
|
345
|
+
`parallel: true` is the synchronous fanout shape. Saved JSON recipes and detached lifecycle concerns such as logs, cancellation, and durable state belong to host-specific recipe/async-run standards, not to command templates.
|
|
344
346
|
|
|
345
347
|
## Trust Boundary
|
|
346
348
|
|
package/docs/inbound.md
CHANGED
|
@@ -49,7 +49,7 @@ Legacy `telegram.json` files may still define `attachmentHandlers` for media/fil
|
|
|
49
49
|
|
|
50
50
|
At runtime, `attachmentHandlers` is appended after `inboundHandlers`. Existing configs continue to work, while new configs should use `inboundHandlers`.
|
|
51
51
|
|
|
52
|
-
Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are independent selectors: if `mime` is present, `type` is not required. Wildcards such as `audio/*` or `text/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically.
|
|
52
|
+
Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are independent selectors: if `mime` is present, `type` is not required. Wildcards such as `audio/*` or `text/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
|
|
53
53
|
|
|
54
54
|
`defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` is only a string-array declaration of supported placeholders; defaults belong in `defaults` or inline placeholders such as `{lang=ru}`. Examples prefer explicit flag-style CLIs such as `--file {file}` and `--lang {lang=ru}` for readability, but positional forms such as `/path/to/stt {file} {lang=ru} {model=voxtral-mini-latest}` are equally valid when the target script supports them.
|
|
55
55
|
|
|
@@ -90,7 +90,7 @@ If a matching handler fails with a non-zero exit code, the runtime records diagn
|
|
|
90
90
|
|
|
91
91
|
Extensions can register programmatic inbound handlers with `registerTelegramInboundHandler(kind, handler)` from `@llblab/pi-telegram/inbound`. This is the code-level counterpart to configured `inboundHandlers`; use it for extension-owned transformations that are not voice-specific.
|
|
92
92
|
|
|
93
|
-
Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. This is the zero-config extension path for voice/audio input:
|
|
93
|
+
Voice extensions can register STT providers with `registerTelegramVoiceTranscriptionProvider()` from `@llblab/pi-telegram/voice`. This is the zero-config extension path for voice/audio input: a companion extension can transcribe Telegram voice notes without requiring the operator to write an `inboundHandlers` command template.
|
|
94
94
|
|
|
95
95
|
Priority stays explicit and predictable:
|
|
96
96
|
|
package/docs/outbound.md
CHANGED
|
@@ -18,7 +18,7 @@ An outbound handler is selected by `type`. Text replies and assistant markup map
|
|
|
18
18
|
|
|
19
19
|
The voice pipeline is detailed below: configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers.
|
|
20
20
|
|
|
21
|
-
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically.
|
|
21
|
+
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0.
|
|
22
22
|
|
|
23
23
|
## Text Handler Config
|
|
24
24
|
|
package/docs/public-api.md
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "@llblab/pi-telegram/voice";
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private.
|
|
28
|
+
`0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private. See [Public API Smoke Examples](#public-api-smoke-examples) below for minimal companion-extension patterns that avoid implementation imports.
|
|
29
29
|
|
|
30
30
|
## User-Facing API
|
|
31
31
|
|
|
@@ -159,7 +159,7 @@ Contract:
|
|
|
159
159
|
|
|
160
160
|
- `id` is unique per active registry. Duplicate ids are rejected.
|
|
161
161
|
- `ctx.callbackData(action, payload?)` builds compact `section:` callbacks and validates Telegram's 64-byte limit.
|
|
162
|
-
- `ctx.edit()`
|
|
162
|
+
- `ctx.edit()` auto-prepends the correct Back/Main-menu row. `ctx.open()` sends a standalone chat message without auto-navigation.
|
|
163
163
|
- Section errors are isolated and surfaced as callback popups/diagnostics.
|
|
164
164
|
|
|
165
165
|
Full behavior: [Extension Sections](./sections.md).
|
|
@@ -233,16 +233,170 @@ const offStt = registerTelegramVoiceTranscriptionProvider(
|
|
|
233
233
|
const offTts = registerTelegramVoiceSynthesisProvider(
|
|
234
234
|
async (text, options) => {
|
|
235
235
|
const audioPath = await synthesizeOggOpus(text, options);
|
|
236
|
-
return
|
|
236
|
+
return getTelegramVoiceSendTranscript(getCurrentTelegramConfigView())
|
|
237
|
+
? { audioPath, transcriptText: text }
|
|
238
|
+
: { audioPath };
|
|
237
239
|
},
|
|
238
240
|
{ id: "@scope/my-extension/tts" },
|
|
239
241
|
);
|
|
240
242
|
```
|
|
241
243
|
|
|
242
|
-
Stable voice-provider registrations pass a durable `id`. Omitting `id` is a compatibility path for older providers and receives a generated session-local id. Providers return `undefined` to pass. TTS providers must return `.ogg` or `.opus` files for native Telegram voice notes.
|
|
244
|
+
Stable voice-provider registrations pass a durable `id`. Omitting `id` is a compatibility path for older providers and receives a generated session-local id. Providers return `undefined` to pass. TTS providers must return `.ogg` or `.opus` files for native Telegram voice notes. `voice.sendTranscript` is the bridge-owned transcript preference; providers that expose captions should gate `transcriptText` with `getTelegramVoiceSendTranscript(config)` instead of defining a second reply-policy toggle.
|
|
243
245
|
|
|
244
246
|
Full behavior: [Voice Integration](./voice.md).
|
|
245
247
|
|
|
248
|
+
## Public API Smoke Examples
|
|
249
|
+
|
|
250
|
+
Minimal companion-extension examples that import only stable `@llblab/pi-telegram/*` public membranes. Copy one into an extension `index.ts`, load it beside `pi-telegram`, and verify that it starts without importing any `@llblab/pi-telegram/lib/*` implementation path.
|
|
251
|
+
|
|
252
|
+
### Extension Sections
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
256
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/sections";
|
|
257
|
+
|
|
258
|
+
export default function demoSection(pi: ExtensionAPI) {
|
|
259
|
+
let unregister: (() => void) | undefined;
|
|
260
|
+
pi.on("session_start", async () => {
|
|
261
|
+
unregister?.();
|
|
262
|
+
unregister = registerTelegramSection({
|
|
263
|
+
id: "demo-section/status",
|
|
264
|
+
label: "🧩 Demo section",
|
|
265
|
+
order: 50,
|
|
266
|
+
render: () => ({
|
|
267
|
+
text: "<b>Demo section</b>\n\nThis section was rendered by a companion extension.",
|
|
268
|
+
replyMarkup: { inline_keyboard: [] },
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
pi.on("session_shutdown", async () => {
|
|
273
|
+
unregister?.();
|
|
274
|
+
unregister = undefined;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Raw Update Handler
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
283
|
+
import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
|
|
284
|
+
|
|
285
|
+
export default function demoUpdates(pi: ExtensionAPI) {
|
|
286
|
+
let unregister: (() => void) | undefined;
|
|
287
|
+
pi.on("session_start", async () => {
|
|
288
|
+
unregister?.();
|
|
289
|
+
unregister = registerTelegramUpdateHandler((update) => {
|
|
290
|
+
if (!update || typeof update !== "object") return "pass";
|
|
291
|
+
return "pass";
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
pi.on("session_shutdown", async () => {
|
|
295
|
+
unregister?.();
|
|
296
|
+
unregister = undefined;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Inbound Handler
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
305
|
+
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
|
|
306
|
+
|
|
307
|
+
export default function demoInbound(pi: ExtensionAPI) {
|
|
308
|
+
let unregister: (() => void) | undefined;
|
|
309
|
+
pi.on("session_start", async () => {
|
|
310
|
+
unregister?.();
|
|
311
|
+
unregister = registerTelegramInboundHandler("text/*", async (file) => {
|
|
312
|
+
if (!file.path.endsWith(".demo.txt")) return undefined;
|
|
313
|
+
return `Demo inbound handler saw ${file.fileName ?? file.path}`;
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
pi.on("session_shutdown", async () => {
|
|
317
|
+
unregister?.();
|
|
318
|
+
unregister = undefined;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Outbound Handler
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
327
|
+
import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
|
|
328
|
+
|
|
329
|
+
export default function demoOutbound(pi: ExtensionAPI) {
|
|
330
|
+
let unregister: (() => void) | undefined;
|
|
331
|
+
pi.on("session_start", async () => {
|
|
332
|
+
unregister?.();
|
|
333
|
+
unregister = registerTelegramOutboundHandler("text", async (text) => {
|
|
334
|
+
if (!text.includes("[demo-outbound]")) return undefined;
|
|
335
|
+
return text.replace("[demo-outbound]", "Demo outbound handler:");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
pi.on("session_shutdown", async () => {
|
|
339
|
+
unregister?.();
|
|
340
|
+
unregister = undefined;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Voice Providers
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
349
|
+
import {
|
|
350
|
+
getTelegramVoiceSendTranscript,
|
|
351
|
+
registerTelegramVoiceSynthesisProvider,
|
|
352
|
+
registerTelegramVoiceTranscriptionProvider,
|
|
353
|
+
} from "@llblab/pi-telegram/voice";
|
|
354
|
+
|
|
355
|
+
export default function demoVoice(pi: ExtensionAPI) {
|
|
356
|
+
let unregisterTts: (() => void) | undefined;
|
|
357
|
+
let unregisterStt: (() => void) | undefined;
|
|
358
|
+
let currentConfig: { voice?: { sendTranscript?: boolean } } = {};
|
|
359
|
+
pi.on("session_start", async () => {
|
|
360
|
+
unregisterTts?.();
|
|
361
|
+
unregisterStt?.();
|
|
362
|
+
unregisterTts = registerTelegramVoiceSynthesisProvider(
|
|
363
|
+
async (text) => {
|
|
364
|
+
const audioPath = await synthesizeDemoOgg(text);
|
|
365
|
+
return getTelegramVoiceSendTranscript(currentConfig)
|
|
366
|
+
? { audioPath, transcriptText: text }
|
|
367
|
+
: { audioPath };
|
|
368
|
+
},
|
|
369
|
+
{ id: "demo-voice/tts" },
|
|
370
|
+
);
|
|
371
|
+
unregisterStt = registerTelegramVoiceTranscriptionProvider(
|
|
372
|
+
async (file) => {
|
|
373
|
+
if (file.kind !== "voice" && file.kind !== "audio") return undefined;
|
|
374
|
+
return { text: `Demo transcript for ${file.fileName ?? file.path}` };
|
|
375
|
+
},
|
|
376
|
+
{ id: "demo-voice/stt" },
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
pi.on("session_shutdown", async () => {
|
|
380
|
+
unregisterTts?.();
|
|
381
|
+
unregisterStt?.();
|
|
382
|
+
unregisterTts = undefined;
|
|
383
|
+
unregisterStt = undefined;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function synthesizeDemoOgg(_text: string): Promise<string> {
|
|
388
|
+
throw new Error("Replace synthesizeDemoOgg with a real OGG/Opus generator.");
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Smoke Checklist
|
|
393
|
+
|
|
394
|
+
- The extension imports only `@llblab/pi-telegram/sections`, `/updates`, `/inbound`, `/outbound`, `/voice`, or `/keyboard`.
|
|
395
|
+
- It does not import `@llblab/pi-telegram/lib/*`.
|
|
396
|
+
- It registers on `session_start` and disposes on `session_shutdown`.
|
|
397
|
+
- Stable high-level registrations use durable ids.
|
|
398
|
+
- Failures are visible during manual testing through `/telegram-status` or extension-owned logging.
|
|
399
|
+
|
|
246
400
|
## Callback Namespaces
|
|
247
401
|
|
|
248
402
|
Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
package/docs/sections.md
CHANGED
|
@@ -197,8 +197,8 @@ The token is an implementation detail. Section authors **never** write `section:
|
|
|
197
197
|
4. Compact confirmation callbacks (`compact:*`)
|
|
198
198
|
5. Queue menu callbacks (`queue:*`)
|
|
199
199
|
6. Settings menu callbacks (`settings:*`)
|
|
200
|
-
7.
|
|
201
|
-
8.
|
|
200
|
+
7. Section callbacks (`section:*`)
|
|
201
|
+
8. Built-in menu callbacks (`menu:*`, `model:*`, `thinking:*`, `status:*`)
|
|
202
202
|
9. Unknown callbacks fall back to `[callback]` prompt text
|
|
203
203
|
|
|
204
204
|
### Handler return values
|
|
@@ -226,14 +226,14 @@ Section errors are caught and surfaced as popup text. No unhandled exceptions le
|
|
|
226
226
|
|
|
227
227
|
## 8. Navigation Hierarchy
|
|
228
228
|
|
|
229
|
-
`ctx.edit()`
|
|
229
|
+
`ctx.edit()` automatically prepends a Back row for menu-bound views. The Back target depends on the navigation level:
|
|
230
230
|
|
|
231
231
|
- Section root (from main menu): `⬆️ Main menu` → `menu:back`
|
|
232
232
|
- Section sub-view (`ctx.edit()` in handler): `⬆️ Back` → `section:<token>:open`
|
|
233
233
|
- Settings root (from Settings list): `⬆️ Back` → `settings:list`
|
|
234
234
|
- Settings sub-view (`ctx.edit()` in settings handler): `⬆️ Back` → `settings:list`
|
|
235
235
|
|
|
236
|
-
Section authors do not need to manage the Back button — it is added automatically and deduplicated when already present.
|
|
236
|
+
Section authors do not need to manage the Back button for `ctx.edit()` — it is added automatically and deduplicated when already present. `ctx.open()` sends a standalone chat message and does not prepend a Back row.
|
|
237
237
|
|
|
238
238
|
```
|
|
239
239
|
Main menu
|
|
@@ -257,7 +257,7 @@ interface TelegramSectionContext {
|
|
|
257
257
|
answerCallback(text?: string): Promise<void>;
|
|
258
258
|
/** Edit the current message (auto-prepends Back row) */
|
|
259
259
|
edit(view: TelegramSectionView): Promise<void>;
|
|
260
|
-
/** Send a
|
|
260
|
+
/** Send a standalone chat message without auto-navigation */
|
|
261
261
|
open(view: TelegramSectionView): Promise<void>;
|
|
262
262
|
/** Enqueue a plain-text prompt turn */
|
|
263
263
|
enqueuePrompt(prompt: string): Promise<void>;
|
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.
|
|
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. 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
|
|
|
@@ -67,12 +67,12 @@ The reply policy itself remains a built-in pi-telegram setting (`voice.replyMode
|
|
|
67
67
|
|
|
68
68
|
## Outbound Voice Synthesis Provider Registration
|
|
69
69
|
|
|
70
|
-
Voice synthesis provider extensions
|
|
70
|
+
Voice synthesis provider extensions register themselves through `registerTelegramVoiceSynthesisProvider()`. The bridge only provides the registration seam and the actual delivery to Telegram. **The provider is fully responsible for**:
|
|
71
71
|
|
|
72
72
|
- Text optimisation / speech-style rewriting
|
|
73
73
|
- Adding speech tags (when desired)
|
|
74
74
|
- Running TTS + ffmpeg conversion to OGG/Opus
|
|
75
|
-
- Deciding whether to return `transcriptText` at all
|
|
75
|
+
- Deciding whether to return `transcriptText` at all based on the bridge-owned `voice.sendTranscript` preference when the provider has access to the current Telegram config
|
|
76
76
|
- `transcriptText` (when returned) is attached by the bridge as the voice message **caption** only. Separate transcript messages are no longer sent.
|
|
77
77
|
|
|
78
78
|
The bridge shows a `record_voice` action while delivering and sends the final audio with Telegram `sendVoice`. When a provider returns `transcriptText`, the bridge attaches it as the voice caption.
|
|
@@ -86,7 +86,7 @@ The provider receives the raw agent text plus optional `{ lang?, rate? }`.
|
|
|
86
86
|
It must return one of:
|
|
87
87
|
|
|
88
88
|
- `string` — path to a ready `.ogg` or `.opus` file
|
|
89
|
-
- `{ audioPath: string, transcriptText?: string }` — `audioPath` must be OGG/Opus. When `transcriptText` is present it is attached as the voice message **caption**.
|
|
89
|
+
- `{ audioPath: string, transcriptText?: string }` — `audioPath` must be OGG/Opus. When `transcriptText` is present it is attached as the voice message **caption**. Providers should treat pi-telegram's `voice.sendTranscript` as the bridge-owned transcript preference instead of inventing a second reply-policy UI.
|
|
90
90
|
- `undefined` — skip this text block
|
|
91
91
|
|
|
92
92
|
**Important:** Providers are fully responsible for producing a clean, TTS-optimised native voice file. The bridge may also run configured outbound voice command templates for users who prefer process-boundary handlers instead of provider extensions.
|
|
@@ -124,20 +124,25 @@ 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 {
|
|
127
|
+
import {
|
|
128
|
+
getTelegramVoiceSendTranscript,
|
|
129
|
+
registerTelegramVoiceSynthesisProvider,
|
|
130
|
+
} from "@llblab/pi-telegram/voice";
|
|
128
131
|
|
|
129
132
|
registerTelegramVoiceSynthesisProvider(
|
|
130
133
|
async (text, options) => {
|
|
131
134
|
const rewritten = rewriteWithSpeechTags(text);
|
|
132
135
|
const audioPath = await myTTS(rewritten, { language: options?.lang });
|
|
133
|
-
const sendTranscript =
|
|
136
|
+
const sendTranscript = getTelegramVoiceSendTranscript(
|
|
137
|
+
getCurrentTelegramConfigView(),
|
|
138
|
+
);
|
|
134
139
|
return sendTranscript ? { audioPath, transcriptText: text } : { audioPath };
|
|
135
140
|
},
|
|
136
141
|
{ id: "my-voice-provider/tts" },
|
|
137
142
|
);
|
|
138
143
|
```
|
|
139
144
|
|
|
140
|
-
The bridge never sends a separate transcript message. Caption-only is the "ON" behavior.
|
|
145
|
+
`getCurrentTelegramConfigView()` represents whatever current `TelegramConfig` view your extension already owns or receives; pi-telegram does not require providers to read config directly. The bridge never sends a separate transcript message. Caption-only is the "ON" behavior.
|
|
141
146
|
|
|
142
147
|
### Surfacing provider diagnostics
|
|
143
148
|
|
|
@@ -146,7 +151,7 @@ Voice provider extensions can record runtime events that appear in `/telegram-st
|
|
|
146
151
|
```typescript
|
|
147
152
|
import { recordTelegramRuntimeEvent } from "@llblab/pi-telegram/outbound";
|
|
148
153
|
|
|
149
|
-
recordTelegramRuntimeEvent("
|
|
154
|
+
recordTelegramRuntimeEvent("voice-provider", new Error("TTS failed"), {
|
|
150
155
|
phase: "tts",
|
|
151
156
|
text: text.slice(0, 50),
|
|
152
157
|
});
|