@llblab/pi-kit 0.37.0 → 0.38.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/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-state-flow/README.md +30 -14
- package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
- package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
- package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
- package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
- package/node_modules/@llblab/pi-state-flow/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
- package/package.json +3 -3
|
@@ -49,7 +49,7 @@ An app name is a unique lowercase ASCII identifier accepted by the runtime's pat
|
|
|
49
49
|
|
|
50
50
|
## Inference-Bypass Syntax
|
|
51
51
|
|
|
52
|
-
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone
|
|
52
|
+
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone `telegram_button` fenced blocks between paragraphs (three or more backticks or tildes, horizontal indentation/whitespace tolerated, same-marker closure no shorter than the opening); both singleton cells and mixed matrices retain the same generation/revision-bound routing. In-body controls acknowledge clicks without recoloring the message; HTML compatibility moves them to the footer. A bound action is encoded entirely in the prompt string:
|
|
53
53
|
|
|
54
54
|
```ebnf
|
|
55
55
|
bound-action = app "::" method [ "(" json-value ")" ]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
|
|
4
4
|
|
|
5
|
-
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden
|
|
5
|
+
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden HTML comments, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. Complete action comments activate by their delimiters and accepted payload regardless of surrounding whitespace or Markdown, including code fences; Telegram preview and final delivery remove every assistant-authored `<!-- … -->` block regardless of Markdown position or owning extension. Unclosed comment tails are withheld, comment-only text plans send no message, and the Pi terminal transcript remains unchanged. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. During an active Telegram turn, `telegram_message` rejects an implicit or same-turn target so ordinary final delivery remains the sole current-target response. Its `thread` argument accepts a case-insensitive live thread name or numeric id: the bridge preflights one live owner, sends visibly, then admits the text as a source-attributed turn in that instance. Unknown, ambiguous, same-target, offline, and cross-chat destinations fail before sending. Existing `chat_id` plus `thread_id` targeting stays compatible; registered followers use authenticated, generation-fenced bus routing. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded `telegram_button` comments become buttons attached to that text message.
|
|
6
6
|
|
|
7
7
|
Text handlers use the portable [Command Template Standard](./command-templates.md). Programmatic outbound handlers use `registerTelegramOutboundHandler(kind, handler)`. Voice replies can use configured command-template handlers or the provider API described in [Voice Integration](./voice.md).
|
|
8
8
|
|
|
@@ -163,11 +163,11 @@ Full text answer stays here.
|
|
|
163
163
|
<!-- telegram_voice {"text":"First line.\nSecond line.","lang":"ru"} -->
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Prefer `{text}`, `{text|lang}`, or `{text|lang|rate}`. Use JSON when multiline content, named fields, or escaping earns the extra syntax; equivalent JSON `text` or `value` supplies the spoken payload, with explicit `text` taking precedence. Each comment creates one voice artifact, so voice cells do not accept button-style matrix composition.
|
|
166
|
+
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Prefer `{text}`, `{text|lang}`, or `{text|lang|rate}`. Use JSON when multiline content, named fields, or escaping earns the extra syntax; equivalent JSON `text` or `value` supplies the spoken payload, with explicit `text` taking precedence. Each comment creates one voice artifact, so voice cells do not accept button-style matrix composition. Complete voice comments activate anywhere, including inline, indented, quoted and fenced text; surrounding Markdown is not an action boundary. Escape the opening delimiter as `<!--` for literal examples.
|
|
167
167
|
|
|
168
168
|
## Buttons Markup
|
|
169
169
|
|
|
170
|
-
Assistant replies can include one or many button actions through a
|
|
170
|
+
Assistant replies can include one or many button actions through a `telegram_button` HTML comment:
|
|
171
171
|
|
|
172
172
|
```md
|
|
173
173
|
I can continue.
|
|
@@ -181,15 +181,15 @@ Rules:
|
|
|
181
181
|
|
|
182
182
|
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling, permitting an empty style atom in this form: `{|Next||1}`. An omitted fourth position stays enabled. JSON uses boolean `disabled`; legacy attributes accept `disabled="true"` or `disabled="false"`. Invalid disabled values reject the candidate matrix atomically. Disabled cells require no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank, equivalent to JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`. They retain their row and label, serialize as `{ text, disabled: {} }` (non-breaking-space text for a blank cell), and register no callback, prompt, or bound app action. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
183
183
|
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`, one-field `{value}`, or prompt-only `{|prompt}` is present, that string supplies both visible label and queued prompt. An explicit counterpart takes precedence over `value`. Use JSON with `\n` escapes for multiline prompts.
|
|
184
|
-
-
|
|
184
|
+
- Complete HTML comments activate by their delimiters and accepted payload, regardless of indentation, surrounding text, lists, quotes or code fences. Multiple comments on one line are independent. Escape the opening delimiter as `<!--` for literal examples; backticks and backslashes do not disable action comments.
|
|
185
185
|
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a fourth unescaped CML separator, empty enabled-cell prompts or three-atom styles, invalid disabled flags, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Multi-atom cells may omit the label; four-atom cells may additionally omit the selected style, and disabled cells may omit the prompt. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
186
186
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
187
|
-
- Standalone
|
|
187
|
+
- Standalone fences opened by at least three backticks or tildes and the exact name `telegram_button` use the same singleton/matrix JSON/CML grammar. Delimiter lines accept horizontal indentation, optional spaces/tabs before the name, and trailing whitespace. A closing line must use the same marker with at least the opening length and only surrounding horizontal whitespace; inline, quoted/list-prefixed delimiters and name suffixes do not activate a block. Button wrappers accept adjacent complete JSON/CML cells and matrices with optional commas between groups. Their normalized rows concatenate in source order: `[{A|a}] [{B|b}]` equals `[{A|a}{B|b}]`; a singleton cell is one vertical row and horizontal peers still require a row nested inside a matrix. Any invalid group in a fence rejects the entire block before callback registration. Complete valid blocks become native `<tg-button-row>` elements in `InputRichMessage.markdown`, at their original positions. Each row supports at most eight buttons and must fit a Rich Message chunk; validation precedes callback registration for the entire block. Labels are escaped, prompts never enter the native markup, and disabled cells have no callback. Missing closures, trailing payload garbage, and invalid blocks produce no controls. Other language names and enclosing code examples stay literal. Draft previews withhold complete and unfinished action fences without registering actions. HTML compatibility projects fenced rows into the bottom keyboard before comment-authored footer rows; source ordering within each group is retained.
|
|
188
188
|
- In-body controls share prompt/app dispatch, TTL, generation/revision checks, and sent-message target ownership with footer controls. Their acknowledgement is the selection feedback; `selected_style` does not recolor the Rich body. No callback-triggered body rewrite is introduced, preventing a stale click from overwriting newer app output. The operator confirmed the post-reload current-client smoke: singleton CML and mixed JSON/CML in-body rows, their ordinary prompt callbacks, a labeled disabled control, and the separate footer callback work as intended. Client type was not recorded. The operator also confirmed that HTML mode projects the in-body groups into the bottom keyboard as intended. Second-client coverage, follower routing, blank cells, and app-method dispatch/revision rejection remain live-gated. Bot API evidence: `InputRichBlockButtons` / Rich Markdown `tg-button-row` and `RichMessageButton` use ordinary `callback_data` and support disabled controls.
|
|
189
189
|
- After Telegram accepts a generated footer button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
190
190
|
- When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
Prefer separate comment lines for readability; inline comments also activate. Do not use tool calls for ordinary Telegram-turn buttons. The agent writes Markdown plus hidden comments; the bridge strips comments and attaches Telegram `reply_markup` after `agent_end`. For local/TUI-originated direct sends, put the same Markdown and `telegram_button` comments in `telegram_message(text)`.
|
|
193
193
|
|
|
194
194
|
Buttons are built in and do not need a command template because they are pure Telegram reply markup plus callback routing.
|
|
195
195
|
|
|
@@ -203,8 +203,8 @@ The extension injects prompt guidance by context:
|
|
|
203
203
|
- For Telegram-originated turns, the compact note routes the agent to `telegram-bridge` for voice/button/direct-delivery/Threaded Mode/formatting/debug guidance and to `show-me` when the user needs a visual explanation of work or behavior.
|
|
204
204
|
- For ordinary Telegram-originated answers, write the full technical answer as phone-width Markdown. `show-me` may instead deliver a focused self-contained HTML artifact when explicitly requested or when spatial density earns it, while keeping the immediate reply useful.
|
|
205
205
|
- Add `telegram_voice` with positional CML by default or JSON when multiline content, named fields, or escaping requires it. A companion summary is optional, no specific summary format is required.
|
|
206
|
-
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone
|
|
207
|
-
- For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include
|
|
206
|
+
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone `telegram_button` fenced block for placement between paragraphs. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` and boolean `disabled` are optional. Keep at least one useful enabled action on an interactive surface. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
|
|
207
|
+
- For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
|
|
208
208
|
- Prefer meaningful visible parent text when it adds context; for a button-only answer, rely on the bridge's automatic `☑️ **Choose an option:**` fallback rather than manufacturing duplicate text.
|
|
209
209
|
|
|
210
210
|
This keeps the agent focused on semantics, prevents Telegram action syntax from leaking into normal local replies, and lets the bridge handle low-latency Telegram adaptation.
|
|
@@ -68,7 +68,7 @@ An unbound Thread chooser offers **⛔️ Cancel routing** to the current paired
|
|
|
68
68
|
|
|
69
69
|
### Tools and assistant-authored actions
|
|
70
70
|
|
|
71
|
-
Every assistant-authored HTML comment is transport-private on Telegram: previews and final replies remove `<!-- … -->` blocks regardless of Markdown nesting or which extension owns the comment,
|
|
71
|
+
Every assistant-authored HTML comment is transport-private on Telegram: previews and final replies remove `<!-- … -->` blocks regardless of Markdown nesting or which extension owns the comment, and complete recognized comments activate voice or buttons independently of surrounding whitespace or Markdown, including code fences. Escape the opening delimiter as `<!--` for literal examples. Unclosed comment tails are withheld and a comment-only result sends no text message; Pi's terminal transcript remains unchanged.
|
|
72
72
|
|
|
73
73
|
- `telegram_bind({ app, script, argument? } | { app, method, argument? })` installs and initializes one canonical managed Generative App module under `<agent-dir>/genapps/<app>/<app>.mjs`, or invokes one named method on an installed app. Installation rejects silent replacement and noncanonical/symlink sources. Methods receive immutable JSON state, one optional JSON argument, cancellation, revision, and a bounded non-shell process port; successful state changes commit to `state.json` plus `states.jsonl`, while output-only methods leave history unchanged. After one-shot `tgbtn` resolution, a complete `app::method` or `app::method(<strict JSON>)` prompt invokes the installed app before Pi queue admission and sends its planned Markdown/buttons directly; malformed or failed bound actions never fall back to a model prompt. Direct app-output buttons retain hidden source revisions and stale actions fail before method execution; sibling processes serialize transitions and recover dead lock owners. Bound actions send a fresh message by default and retain the clicked button's selected state on its prior surface. A result may opt into `viewMode: "edit"` to replace the callback message and keyboard in place, with one fresh-send fallback only for that explicit action. Agent-mediated initial-surface revisions, process-birth lock proof, automatic refresh, and voice output are not implemented.
|
|
74
74
|
- `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files before any separate final text; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message with media first. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
|
|
@@ -77,7 +77,7 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
|
|
|
77
77
|
- `telegram_message(text, chat_id?, media?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. Channel delivery accepts `media` as one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video (photo ≤ 10 MiB, video ≤ 50 MiB), uploaded through multipart `sendPhoto`/`sendVideo` with `text` as its HTML caption (≤ 1024 visible characters); unsupported media types and albums are rejected before issuance, and the durable channel-post journal binds media identity and caption so duplicate or lost-acknowledgement retries never re-upload. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
|
|
78
78
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. Compiled npm/git installations expose them through the package `pi.skills` manifest so Pi resource filters remain authoritative; a raw TypeScript checkout under Pi's `extensions` directory contributes its source Skill root at runtime instead. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
79
79
|
- `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
|
|
80
|
-
- `telegram_button` hidden comments create footer buttons; standalone
|
|
80
|
+
- `telegram_button` hidden comments create footer buttons; standalone `telegram_button` fenced blocks create button rows between paragraphs in Native Rich Markdown. Delimiter lines tolerate horizontal indentation and whitespace before the exact name. Open with at least three backticks or tildes; close on a separate line with the same marker and no shorter a run, allowing only surrounding spaces/tabs. Enclosing code fences keep button-block examples literal. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing; both wrappers also accept adjacent complete JSON/CML cells and matrices with optional separators, concatenating their normalized rows in source order. Cells remain singleton rows and nested horizontal rows retain their grouping. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. HTML action comments activate regardless of surrounding Markdown; escape their opening delimiter for literal examples. Button fences require standalone delimiter lines outside enclosing code examples and quote/list prefixes, but allow horizontal indentation. Bare JSON/CML in prose never activates.
|
|
81
81
|
|
|
82
82
|
Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
|
|
83
83
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
parseTelegramActionPayloadRows,
|
|
16
16
|
parseTelegramButtonPayloadRows,
|
|
17
17
|
replaceTelegramButtonFences,
|
|
18
|
-
|
|
18
|
+
replaceTelegramHtmlComments,
|
|
19
19
|
} from "./outbound-markup.ts";
|
|
20
20
|
import {
|
|
21
21
|
type PendingTelegramTurn,
|
|
@@ -237,8 +237,21 @@ export function planTelegramButtonReply(
|
|
|
237
237
|
),
|
|
238
238
|
);
|
|
239
239
|
};
|
|
240
|
+
const footerKeyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
|
|
241
|
+
const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
|
|
242
|
+
const command = "telegram_button";
|
|
243
|
+
const normalizedContent = comment.content
|
|
244
|
+
.replace(/^\s+/, "")
|
|
245
|
+
.replace(/^!/, "");
|
|
246
|
+
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
247
|
+
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
248
|
+
if (!payloadRows) return "";
|
|
249
|
+
const rows = buildRows(payloadRows, false);
|
|
250
|
+
if (rows) footerKeyboard.push(...rows);
|
|
251
|
+
return "";
|
|
252
|
+
});
|
|
240
253
|
const withRichButtons = replaceTelegramButtonFences(
|
|
241
|
-
|
|
254
|
+
stripped,
|
|
242
255
|
(payload, closed) => {
|
|
243
256
|
if (!closed) return "";
|
|
244
257
|
const payloadRows = parseTelegramButtonPayloadRows(payload);
|
|
@@ -253,19 +266,8 @@ export function planTelegramButtonReply(
|
|
|
253
266
|
return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
|
|
254
267
|
},
|
|
255
268
|
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const normalizedContent = comment.content
|
|
259
|
-
.replace(/^\s+/, "")
|
|
260
|
-
.replace(/^!/, "");
|
|
261
|
-
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
262
|
-
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
263
|
-
if (!payloadRows) return "";
|
|
264
|
-
const rows = buildRows(payloadRows, false);
|
|
265
|
-
if (rows) keyboard.push(...rows);
|
|
266
|
-
return "";
|
|
267
|
-
});
|
|
268
|
-
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
|
|
269
|
+
keyboard.push(...footerKeyboard);
|
|
270
|
+
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(withRichButtons);
|
|
269
271
|
return {
|
|
270
272
|
markdown:
|
|
271
273
|
keyboard.length > 0 && !visibleMarkdown
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram outbound markup parsing helpers
|
|
3
3
|
* Zones: telegram outbound, assistant markup
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns delimiter-based assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { isWireRecord as isTelegramActionPayload } from "./wire.ts";
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface TelegramHtmlComment {
|
|
10
10
|
raw: string;
|
|
11
11
|
content: string;
|
|
12
12
|
start: number;
|
|
@@ -34,7 +34,7 @@ function getMarkdownLineText(
|
|
|
34
34
|
function getTopLevelOpeningFence(
|
|
35
35
|
line: string,
|
|
36
36
|
): TelegramTopLevelFenceState | undefined {
|
|
37
|
-
const match = line.match(/^
|
|
37
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})/);
|
|
38
38
|
const sequence = match?.[1];
|
|
39
39
|
if (!sequence) return undefined;
|
|
40
40
|
return {
|
|
@@ -47,7 +47,7 @@ function isTopLevelClosingFence(
|
|
|
47
47
|
line: string,
|
|
48
48
|
fence: TelegramTopLevelFenceState,
|
|
49
49
|
): boolean {
|
|
50
|
-
const match = line.match(/^
|
|
50
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})[ \t]*$/);
|
|
51
51
|
const sequence = match?.[1];
|
|
52
52
|
return (
|
|
53
53
|
!!sequence &&
|
|
@@ -56,38 +56,21 @@ function isTopLevelClosingFence(
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function
|
|
60
|
-
comments:
|
|
59
|
+
export function collectTelegramHtmlComments(markdown: string): {
|
|
60
|
+
comments: TelegramHtmlComment[];
|
|
61
61
|
openCommentStart?: number;
|
|
62
62
|
} {
|
|
63
|
-
const comments:
|
|
63
|
+
const comments: TelegramHtmlComment[] = [];
|
|
64
64
|
let offset = 0;
|
|
65
|
-
let fence: TelegramTopLevelFenceState | undefined;
|
|
66
65
|
while (offset < markdown.length) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (nextFence) {
|
|
76
|
-
fence = nextFence;
|
|
77
|
-
offset = lineEnd;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (line.startsWith("<!--")) {
|
|
81
|
-
const closeIndex = markdown.indexOf("-->", offset + 4);
|
|
82
|
-
if (closeIndex === -1) return { comments, openCommentStart: offset };
|
|
83
|
-
const end = closeIndex + 3;
|
|
84
|
-
const raw = markdown.slice(offset, end);
|
|
85
|
-
const content = raw.slice(4, -3);
|
|
86
|
-
comments.push({ raw, content, start: offset, end });
|
|
87
|
-
offset = getMarkdownLineEnd(markdown, end);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
offset = lineEnd;
|
|
66
|
+
const start = markdown.indexOf("<!--", offset);
|
|
67
|
+
if (start === -1) break;
|
|
68
|
+
const closeIndex = markdown.indexOf("-->", start + 4);
|
|
69
|
+
if (closeIndex === -1) return { comments, openCommentStart: start };
|
|
70
|
+
const end = closeIndex + 3;
|
|
71
|
+
const raw = markdown.slice(start, end);
|
|
72
|
+
comments.push({ raw, content: raw.slice(4, -3), start, end });
|
|
73
|
+
offset = end;
|
|
91
74
|
}
|
|
92
75
|
return { comments };
|
|
93
76
|
}
|
|
@@ -122,7 +105,7 @@ export function replaceTelegramButtonFences(
|
|
|
122
105
|
continue;
|
|
123
106
|
} else {
|
|
124
107
|
fence = getTopLevelOpeningFence(line);
|
|
125
|
-
if (fence &&
|
|
108
|
+
if (fence && /^[ \t]*(?:`{3,}|~{3,})[ \t]*telegram_button[ \t]*$/.test(line)) {
|
|
126
109
|
actionStart = offset;
|
|
127
110
|
contentStart = end;
|
|
128
111
|
}
|
|
@@ -136,11 +119,11 @@ export function replaceTelegramButtonFences(
|
|
|
136
119
|
return result + markdown.slice(copied);
|
|
137
120
|
}
|
|
138
121
|
|
|
139
|
-
export function
|
|
122
|
+
export function replaceTelegramHtmlComments(
|
|
140
123
|
markdown: string,
|
|
141
|
-
replacer: (comment:
|
|
124
|
+
replacer: (comment: TelegramHtmlComment) => string,
|
|
142
125
|
): string {
|
|
143
|
-
const { comments } =
|
|
126
|
+
const { comments } = collectTelegramHtmlComments(markdown);
|
|
144
127
|
if (comments.length === 0) return markdown;
|
|
145
128
|
let result = "";
|
|
146
129
|
let offset = 0;
|
|
@@ -152,32 +135,9 @@ export function replaceTopLevelHtmlComments(
|
|
|
152
135
|
return result + markdown.slice(offset);
|
|
153
136
|
}
|
|
154
137
|
|
|
155
|
-
function
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
let offset = 0;
|
|
159
|
-
let fence: TelegramTopLevelFenceState | undefined;
|
|
160
|
-
while (offset < markdown.length) {
|
|
161
|
-
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
162
|
-
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
163
|
-
const isLastLine = lineEnd >= markdown.length;
|
|
164
|
-
if (fence) {
|
|
165
|
-
if (isTopLevelClosingFence(line, fence)) fence = undefined;
|
|
166
|
-
offset = lineEnd;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const nextFence = getTopLevelOpeningFence(line);
|
|
170
|
-
if (nextFence) {
|
|
171
|
-
fence = nextFence;
|
|
172
|
-
offset = lineEnd;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (isLastLine && (line === "<" || line === "<!" || line === "<!-")) {
|
|
176
|
-
return offset;
|
|
177
|
-
}
|
|
178
|
-
offset = lineEnd;
|
|
179
|
-
}
|
|
180
|
-
return -1;
|
|
138
|
+
function findOpenOrPartialHtmlCommentIndex(markdown: string): number {
|
|
139
|
+
const start = markdown.indexOf("<!--");
|
|
140
|
+
return start >= 0 ? start : (markdown.match(/<(?:!-?)?$/u)?.index ?? -1);
|
|
181
141
|
}
|
|
182
142
|
|
|
183
143
|
function parseTolerantTelegramAttributes(
|
|
@@ -263,7 +223,7 @@ function blankTelegramActionSpan(
|
|
|
263
223
|
* with every structured span blanked; implausible `[` starts are skipped without a parse attempt.
|
|
264
224
|
*/
|
|
265
225
|
function scanTelegramActionComment<T>(
|
|
266
|
-
comment:
|
|
226
|
+
comment: TelegramHtmlComment,
|
|
267
227
|
command: string,
|
|
268
228
|
parseStructured: (content: string, offset: number) => T | undefined,
|
|
269
229
|
): { payload: T } | { attributeEnvelope: string } | undefined {
|
|
@@ -287,7 +247,7 @@ function scanTelegramActionComment<T>(
|
|
|
287
247
|
}
|
|
288
248
|
|
|
289
249
|
function parseTelegramActionPayload(
|
|
290
|
-
comment:
|
|
250
|
+
comment: TelegramHtmlComment,
|
|
291
251
|
command: string,
|
|
292
252
|
): Record<string, unknown> | undefined {
|
|
293
253
|
const scanned = scanTelegramActionComment(
|
|
@@ -594,7 +554,7 @@ export function parseTelegramButtonPayloadRows(
|
|
|
594
554
|
}
|
|
595
555
|
|
|
596
556
|
export function parseTelegramActionPayloadRows(
|
|
597
|
-
comment:
|
|
557
|
+
comment: TelegramHtmlComment,
|
|
598
558
|
command: string,
|
|
599
559
|
): Record<string, unknown>[][] | undefined {
|
|
600
560
|
const scanned = scanTelegramActionComment(
|
|
@@ -665,7 +625,7 @@ export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
|
665
625
|
replaceTelegramButtonFences(markdown, () => ""),
|
|
666
626
|
);
|
|
667
627
|
const openBlockIndex =
|
|
668
|
-
|
|
628
|
+
findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
669
629
|
const previewMarkdown =
|
|
670
630
|
openBlockIndex >= 0
|
|
671
631
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
@@ -678,7 +638,7 @@ export function stripTelegramCommentMarkupForDelivery(
|
|
|
678
638
|
): string {
|
|
679
639
|
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
680
640
|
const openBlockIndex =
|
|
681
|
-
|
|
641
|
+
findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
682
642
|
const deliveryMarkdown =
|
|
683
643
|
openBlockIndex >= 0
|
|
684
644
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
@@ -716,7 +676,7 @@ export function planTelegramVoiceReply(
|
|
|
716
676
|
const voiceReplies: TelegramVoiceReplyItem[] = [];
|
|
717
677
|
let lang: string | undefined;
|
|
718
678
|
let rate: string | undefined;
|
|
719
|
-
const stripped =
|
|
679
|
+
const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
|
|
720
680
|
const command = "telegram_voice";
|
|
721
681
|
const normalizedContent = comment.content
|
|
722
682
|
.replace(/^\s+/, "")
|
|
@@ -849,11 +849,11 @@ export function createTelegramOutboundReplyPlanner(
|
|
|
849
849
|
...(options?.binding ? { binding: options.binding } : {}),
|
|
850
850
|
});
|
|
851
851
|
|
|
852
|
-
//
|
|
853
|
-
const voiceReply = planTelegramVoiceReply(
|
|
852
|
+
// Extract voice from the original text before button-fence rendering consumes it.
|
|
853
|
+
const voiceReply = planTelegramVoiceReply(markdown);
|
|
854
854
|
|
|
855
855
|
return {
|
|
856
|
-
markdown:
|
|
856
|
+
markdown: stripTelegramCommentMarkupForDelivery(buttonReply.markdown),
|
|
857
857
|
...(buttonReply.replyMarkup
|
|
858
858
|
? { replyMarkup: buttonReply.replyMarkup }
|
|
859
859
|
: {}),
|
|
@@ -34,7 +34,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
|
|
|
34
34
|
"For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
|
|
35
35
|
"For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.",
|
|
36
36
|
"For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
|
|
37
|
-
"Add buttons by embedding the same
|
|
37
|
+
"Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
38
38
|
"During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
|
|
39
39
|
] as const;
|
|
40
40
|
|
|
@@ -360,7 +360,7 @@ export interface TelegramTypingLoopStarterDeps<
|
|
|
360
360
|
getDefaultChatId: () => number | undefined;
|
|
361
361
|
sendTypingAction: (
|
|
362
362
|
chatId: number,
|
|
363
|
-
options?: { message_thread_id?: number; assertAuthority?: () => void },
|
|
363
|
+
options?: { message_thread_id?: number; assertAuthority?: () => void; isCancelled?: () => boolean },
|
|
364
364
|
) => Promise<unknown>;
|
|
365
365
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
366
366
|
isContextActive?: (ctx: TContext) => boolean;
|
|
@@ -454,6 +454,7 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
454
454
|
try {
|
|
455
455
|
await deps.sendTypingAction(targetChatId, {
|
|
456
456
|
...actionOptions,
|
|
457
|
+
isCancelled: () => !active,
|
|
457
458
|
// This stays process-local; adapters recheck it before HTTP/IPC issuance, not just loop ticks.
|
|
458
459
|
assertAuthority() {
|
|
459
460
|
if (!active || !canContinue()) throw new Error("Typing context authority changed.");
|
|
@@ -407,8 +407,8 @@ export interface TelegramApiCallOptions {
|
|
|
407
407
|
|
|
408
408
|
export class TelegramApiAuthorityError extends Error {
|
|
409
409
|
readonly requestIssued: boolean;
|
|
410
|
-
constructor(requestIssued: boolean) {
|
|
411
|
-
super("Telegram API call authority is unavailable.");
|
|
410
|
+
constructor(requestIssued: boolean, options?: ErrorOptions) {
|
|
411
|
+
super("Telegram API call authority is unavailable.", options);
|
|
412
412
|
this.name = "TelegramApiAuthorityError";
|
|
413
413
|
this.requestIssued = requestIssued;
|
|
414
414
|
}
|
|
@@ -418,11 +418,12 @@ export class TelegramApiAuthorityError extends Error {
|
|
|
418
418
|
export function assertTelegramApiCallAuthority(
|
|
419
419
|
assertAuthority: (() => void) | undefined,
|
|
420
420
|
requestIssued: boolean,
|
|
421
|
+
errorOptions?: ErrorOptions,
|
|
421
422
|
): void {
|
|
422
423
|
try {
|
|
423
424
|
assertAuthority?.();
|
|
424
425
|
} catch {
|
|
425
|
-
throw new TelegramApiAuthorityError(requestIssued);
|
|
426
|
+
throw new TelegramApiAuthorityError(requestIssued, errorOptions);
|
|
426
427
|
}
|
|
427
428
|
}
|
|
428
429
|
|
|
@@ -847,6 +848,8 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
847
848
|
/** Typing issuance guard is local authority, never Bot API body or IPC data. */
|
|
848
849
|
export interface TelegramTypingActionOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
|
|
849
850
|
message_thread_id?: number;
|
|
851
|
+
/** Diagnostic observation only: an ended loop does not grant issuance, success or retry. */
|
|
852
|
+
isCancelled?: () => boolean;
|
|
850
853
|
}
|
|
851
854
|
|
|
852
855
|
export interface TelegramBridgeApiRuntime extends Pick<
|
|
@@ -1649,7 +1652,7 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1649
1652
|
} catch (error) {
|
|
1650
1653
|
// Authority refusal is never a transport failure, retry grant or acknowledged success.
|
|
1651
1654
|
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
1652
|
-
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1655
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued, { cause: error });
|
|
1653
1656
|
const retryable =
|
|
1654
1657
|
isRetryableTelegramApiError(error) &&
|
|
1655
1658
|
!(
|
|
@@ -2189,11 +2192,14 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2189
2192
|
);
|
|
2190
2193
|
return true as TResponse;
|
|
2191
2194
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2195
|
+
const diagnosticError = error instanceof TelegramApiAuthorityError && Object.hasOwn(error, "cause")
|
|
2196
|
+
? error.cause : error;
|
|
2197
|
+
if (!options?.isExpectedError?.(error))
|
|
2198
|
+
deps.recordRuntimeEvent(
|
|
2199
|
+
"api",
|
|
2200
|
+
diagnosticError,
|
|
2201
|
+
withTelegramTransportDiagnostics(diagnosticError, { method }),
|
|
2202
|
+
);
|
|
2197
2203
|
throw error;
|
|
2198
2204
|
})
|
|
2199
2205
|
.finally(() => {
|
|
@@ -2311,14 +2317,21 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2311
2317
|
: {}),
|
|
2312
2318
|
}),
|
|
2313
2319
|
sendTypingAction: createTelegramChatActionSender(
|
|
2314
|
-
(chatId, action, options) =>
|
|
2315
|
-
|
|
2320
|
+
(chatId, action, options) => {
|
|
2321
|
+
const isCancelled = options?.isCancelled;
|
|
2322
|
+
return callRecorded<boolean>("sendChatAction", {
|
|
2316
2323
|
chat_id: chatId,
|
|
2317
2324
|
action,
|
|
2318
2325
|
...(options?.message_thread_id !== undefined
|
|
2319
2326
|
? { message_thread_id: options.message_thread_id }
|
|
2320
2327
|
: {}),
|
|
2321
|
-
}, {
|
|
2328
|
+
}, {
|
|
2329
|
+
assertAuthority: options?.assertAuthority,
|
|
2330
|
+
...(isCancelled ? {
|
|
2331
|
+
isExpectedError: (error: unknown) => error instanceof TelegramApiAuthorityError && !Object.hasOwn(error, "cause") && isCancelled() === true,
|
|
2332
|
+
} : {}),
|
|
2333
|
+
});
|
|
2334
|
+
},
|
|
2322
2335
|
"typing",
|
|
2323
2336
|
),
|
|
2324
2337
|
sendRecordVoiceAction: createTelegramChatActionSender(
|
|
@@ -16,12 +16,12 @@ Use Telegram as a mobile companion to the current Pi session. Preserve the exact
|
|
|
16
16
|
| Explicitly send from local/TUI to Telegram | `telegram_message` or `telegram_attach` |
|
|
17
17
|
| Explicitly send to a different live Thread | `telegram_message(thread=...)` |
|
|
18
18
|
| Add buttons | Hidden comment for footer; `telegram_button` fence for in-body rows |
|
|
19
|
-
| Add explicit voice |
|
|
19
|
+
| Add explicit voice | Hidden action comment |
|
|
20
20
|
| Build a repeated deterministic interaction | Follow `generative-apps` |
|
|
21
21
|
|
|
22
22
|
A connected Telegram session proves capability, not user intent. Use Telegram features on Telegram-originated turns or explicit Telegram delivery requests only. Never call `telegram_message` for the current active target.
|
|
23
23
|
|
|
24
|
-
For
|
|
24
|
+
For delivery, Thread routing, configuration or diagnosis, read the applicable reference under [Conditional References](#conditional-references).
|
|
25
25
|
|
|
26
26
|
## Turn Context
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
|
|
|
40
40
|
|
|
41
41
|
## Assistant Actions
|
|
42
42
|
|
|
43
|
-
`telegram_button` and `telegram_voice` are markup, not tools.
|
|
43
|
+
`telegram_button` and `telegram_voice` are markup, not tools. Complete comments activate regardless of surrounding Markdown. Escape `<!--` as `<!--` for literal examples; backticks and backslashes do not disable actions. Comments create footer buttons or voice artifacts. In-body fences: ≥3 backticks/tildes, optional indentation/whitespace, exact `telegram_button`; separate same-marker closure, no shorter. Both wrappers accept a singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only complete recognized comments or valid button fences activate actions; comment-only output sends no text message.
|
|
44
44
|
|
|
45
45
|
### Shared Encoding Rule
|
|
46
46
|
|
|
@@ -61,7 +61,7 @@ Every enabled button has a self-contained prompt and an optional selection style
|
|
|
61
61
|
- `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
|
|
62
62
|
- Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
|
|
63
63
|
- Both wrappers concatenate rows from adjacent JSON/CML cells or matrices, with optional commas. Cells are vertical; horizontal peers require a nested row: `[[{A|a}{B|b}]]`.
|
|
64
|
-
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them.
|
|
64
|
+
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Enclosing code fences and quoted/list-prefixed button fences remain literal.
|
|
65
65
|
- Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
|
|
66
66
|
|
|
67
67
|
A single in-body button (the four-backtick wrapper below makes this a literal example):
|
|
@@ -124,6 +124,6 @@ Before replying:
|
|
|
124
124
|
|
|
125
125
|
- Use the ordinary path for the current target and direct tools only for explicit other delivery.
|
|
126
126
|
- Attach requested files rather than only mentioning them.
|
|
127
|
-
- Keep
|
|
127
|
+
- Keep comments complete and button fences top-level and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
|
|
128
128
|
- Give every enabled button a self-contained prompt; preserve confirmation for dangerous actions.
|
|
129
129
|
- Expose no secret or hidden reasoning.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"@llblab/pi-clean-room": "0.3.0",
|
|
50
50
|
"@llblab/pi-codex-usage": "0.13.2",
|
|
51
51
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
52
|
-
"@llblab/pi-state-flow": "0.
|
|
53
|
-
"@llblab/pi-telegram": "0.58.
|
|
52
|
+
"@llblab/pi-state-flow": "0.27.1",
|
|
53
|
+
"@llblab/pi-telegram": "0.58.3",
|
|
54
54
|
"@llblab/skills": "1.15.0"
|
|
55
55
|
},
|
|
56
56
|
"bundledDependencies": [
|