@llblab/pi-kit 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/README.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +29 -23
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +12 -21
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -4
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +16 -6
- package/node_modules/@llblab/pi-telegram/lib/media.ts +12 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +9 -8
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +218 -83
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +78 -36
- package/node_modules/@llblab/pi-telegram/lib/status.ts +13 -4
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +10 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +61 -207
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +35 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +71 -110
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/configuration.md +15 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +14 -0
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- Expose only resources declared by each included package. Prefer published distribution entrypoints over source entrypoints when both exist.
|
|
25
25
|
- Do not copy extension source, Skills, or documentation into this repository.
|
|
26
26
|
- Preserve package independence: a kit release may advance any subset of included extensions without forcing lockstep extension releases.
|
|
27
|
+
- Derive the kit version bump from the highest dependency bump in the synchronized release cohort: dependency patch-only updates require a kit patch bump, while any dependency minor update requires a kit minor bump. Do not let lower-level dependency bumps force a larger kit bump.
|
|
27
28
|
- Use English for public and repository-facing text.
|
|
28
29
|
- Do not commit, publish, tag, create a remote repository, or mutate Pi settings without explicit authorization.
|
|
29
30
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.3.1 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- `Telegram Patch Release`: Advances `@llblab/pi-telegram` to `0.42.1`, accepting prompt-only CML button cells such as `{|e2}` and their optional selected-style form while preserving prompt-as-label fallback and fail-closed empty prompt, style, and voice atoms.
|
|
8
|
+
|
|
9
|
+
## 0.3.0 - 2026-09-01
|
|
10
|
+
|
|
11
|
+
- `Telegram Minor Release`: Advances `@llblab/pi-telegram` to `0.42.0`, bringing tolerant JSON and compact CML action parsing, positional voice action cells, Thread-aware generated-button prompts, and focused proactive control-surface guidance into the bundled kit.
|
|
12
|
+
- `Release Cohort Policy`: Codifies that the kit follows the highest dependency bump in each synchronized cohort, so dependency patch-only updates produce kit patches while any dependency minor update produces a kit minor.
|
|
13
|
+
|
|
5
14
|
## 0.2.0 - 2026-08-31
|
|
6
15
|
|
|
7
16
|
- `Actors Minor Releases`: Advances `@llblab/pi-actors` to `0.52.0`, bringing root-owned follow-ups, durable completion epochs, presentation acknowledgment, recovery-safe scheduling, explicit urgent steering, and settled packed-package parity into the bundled kit.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| `@llblab/pi-actors` | `0.52.0` | Extension and Skills |
|
|
10
10
|
| `@llblab/pi-codex-usage` | `0.9.4` | Extension |
|
|
11
11
|
| `@llblab/pi-grow-loop` | `0.7.3` | Extension and Skills |
|
|
12
|
-
| `@llblab/pi-telegram` | `0.
|
|
12
|
+
| `@llblab/pi-telegram` | `0.42.1` | Extension and Skills |
|
|
13
13
|
|
|
14
14
|
Versions are exact by design. Updating an extension does not change an installed kit until this repository explicitly advances that dependency and publishes a new kit version.
|
|
15
15
|
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.42.1: Prompt-Only Button Cells
|
|
6
|
+
|
|
7
|
+
- `Prompt-Only CML Buttons`: Accepts `{|prompt}` and `{|prompt|selected_style}` as canonical button cells equivalent to prompt-only JSON, so established prompt fallback supplies both visible text and queued input without a separately authored label; empty one-atom cells, prompts, styles, and voice atoms still fail closed.
|
|
8
|
+
|
|
9
|
+
## 0.42.0: Compact Action Grammar
|
|
10
|
+
|
|
11
|
+
- `Adaptive Action Grammar`: Extracts the first semantically valid JSON or positional CML payload from noisy `telegram_button` and `telegram_voice` envelopes, tolerates bounded trailing commas and unmatched matrix noise, keeps malformed named JSON on the JSON path, and retains legacy attribute parsing as undocumented compatibility.
|
|
12
|
+
- `Voice Action Cells`: Adds compact `{text}`, `{text|lang}`, and `{text|lang|rate}` voice actions while preserving JSON for multiline, named, and escape-heavy payloads; each voice comment remains one independent artifact and matrix-shaped voice payloads fail closed.
|
|
13
|
+
- `Thread-Aware Buttons`: Tags prompts created by generated-button callbacks with the same resolved Telegram Thread label as ordinary inbound turns, preserving current local labels, persisted bindings, target routing, queue ordering, and callback selection behavior.
|
|
14
|
+
- `Focused Agent Guidance`: Makes generated control surfaces proactively emit useful emoji-led prompt buttons, keeps Telegram action syntax under the bridge owner, and moves specialized delivery, configuration, diagnosis, layout, state, and capability guidance behind conditional references to reduce default context without dropping safety constraints.
|
|
15
|
+
|
|
16
|
+
## 0.41.1: Focused Operator Status Hotfix
|
|
17
|
+
|
|
18
|
+
- `Next Turn Feedback`: Replies one Pi-aligned fully bold abort notice to the aborted Telegram turn, falls back to the `/next` command for local work, preserves completed intermediate output, and suppresses pending aborted assistant text so one forced transition no longer produces a contradictory second reply.
|
|
19
|
+
- `Status Telemetry`: Keeps the Tokens row compact with input/output totals and moves cache read/write plus the latest cache-hit rate into a dedicated Cache row.
|
|
20
|
+
- `Reply Transcription`: Runs replied voice and other media through the inbound handler/provider pipeline and appends provenance-scoped outputs inside the reply context, including Guest Mode, instead of leaving quoted audio as an untranscribed attachment.
|
|
21
|
+
- `Information Hierarchy`: Brings access-denial and Threaded Mode chooser headings back under the common fully bold emoji-led sentence/colon contract.
|
|
22
|
+
|
|
5
23
|
## 0.41.0: Connected Companion Presence
|
|
6
24
|
|
|
7
25
|
- `Always-On Projection`: Treats authorized Telegram connection as the public-output boundary for local, autonomous, and unclassified Pi work, always delivering completed visible blocks in order instead of exposing an independent proactive-push opt-out.
|
|
@@ -10,7 +10,7 @@ It is a **runtime adapter**, not a remote terminal. Start or supervise work in t
|
|
|
10
10
|
|
|
11
11
|
Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. While Telegram is connected, local, autonomous, and unclassified extension follow-up work also projects visible checkpoints and the final answer to the authorized Telegram target once and in order, preserving assistant-authored `telegram_button` comments as interactive prompt buttons. This connected companion projection is always active rather than configurable. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
|
|
12
12
|
|
|
13
|
-
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.
|
|
13
|
+
This repository is an actively maintained standalone 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.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -218,11 +218,11 @@ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limi
|
|
|
218
218
|
|
|
219
219
|
### Voice And Media
|
|
220
220
|
|
|
221
|
-
Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `manual` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions
|
|
221
|
+
Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `manual` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions prefer positional `{text}`, `{text|lang}`, or `{text|lang|rate}` cells and use JSON for multiline content, named fields, or escaping.
|
|
222
222
|
|
|
223
223
|
### Buttons And Callbacks
|
|
224
224
|
|
|
225
|
-
Assistant replies can include top-level hidden `telegram_button` comments
|
|
225
|
+
Assistant replies can include top-level hidden `telegram_button` comments containing a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One adaptive matrix may mix named JSON objects with positional CML cells; separators are optional and one trailing comma is tolerated, including inside JSON objects. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style` set to `primary`, `success`, or `danger`; omitting the first atom leaves the existing prompt-as-label fallback in charge, while the optional style still requires a non-empty prompt. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
|
|
226
226
|
|
|
227
227
|
### Threaded Mode And Multi-Instance Bus
|
|
228
228
|
|
|
@@ -301,7 +301,7 @@ Immediate controls:
|
|
|
301
301
|
- `/start` opens the main inline application menu.
|
|
302
302
|
- `/model`, `/thinking`, `/queue`, and `/settings` are hidden shortcuts to menu sections.
|
|
303
303
|
- `/compact` opens an inline confirmation dialog and then runs compaction when the bridge is idle.
|
|
304
|
-
- `/next` dispatches the next queued turn, aborting Pi first when needed.
|
|
304
|
+
- `/next` dispatches the next queued turn, aborting Pi first when needed. When an active Telegram turn is aborted, its single Pi-aligned informational notice replies to a pre-abort snapshot of that turn; otherwise the command message is the fallback target. Aborted pending assistant text is not projected as a second reply, while already completed intermediate output remains visible.
|
|
305
305
|
- `/abort` aborts active work while preserving queued items. Abort-history preservation is enabled only for Telegram-owned active turns; later local/non-Telegram agent starts clear stale abort-history mode so the next Telegram prompt appends instead of absorbing old queued turns as history.
|
|
306
306
|
- `/stop` aborts and clears waiting Telegram queue items.
|
|
307
307
|
|
|
@@ -336,7 +336,7 @@ Assistant delivery guarantees:
|
|
|
336
336
|
- Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
|
|
337
337
|
- Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
|
|
338
338
|
- Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest: the remote private-chat identity wins, then non-owner caller metadata, with a non-bot replied peer available only as a final identity fallback when stronger conversation evidence is absent; username falls back to the remote display name and numeric id. Reply attribution still belongs independently in `[reply|from:...]`, and a replied bot can never define or replace the current `[telegram|guest:...]` location identity. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context. Media embedded in inbound Telegram `rich_message` blocks is downloaded like ordinary message media and stays attached to its forward-source block instead of being mislabeled as current-user material.
|
|
339
|
-
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text.
|
|
339
|
+
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text. Replied media runs through the same inbound handlers and voice transcription providers as current-message media, with provenance-scoped `[outputs|from:…]` appended inside the reply block.
|
|
340
340
|
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
|
|
341
341
|
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
|
|
342
342
|
- Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call.
|
|
@@ -352,10 +352,10 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
|
|
|
352
352
|
|
|
353
353
|
Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
|
|
354
354
|
|
|
355
|
-
Assistant-authored final-message actions use hidden top-level comments
|
|
355
|
+
Assistant-authored final-message actions use hidden top-level comments:
|
|
356
356
|
|
|
357
|
-
- `telegram_voice` accepts
|
|
358
|
-
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, positional Compact Matrix Literal
|
|
357
|
+
- `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
|
|
358
|
+
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
|
|
359
359
|
|
|
360
360
|
Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
|
|
361
361
|
|
|
@@ -390,7 +390,7 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
|
|
|
390
390
|
|
|
391
391
|
## Diagnostics And Operational Behavior
|
|
392
392
|
|
|
393
|
-
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its Tokens row
|
|
393
|
+
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states; the Telegram status menu gives compaction precedence over generic active or pending work. Its compact Tokens row shows only input and output totals, while the adjacent Cache row groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's cache-read share of prompt tokens rather than a misleading cumulative-session ratio; the labels remain distinct from companion-provided usage limits. Observed automatic compaction sends the same start and completion notices as the manual command without duplicating notices for command-owned compaction. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
|
|
394
394
|
|
|
395
395
|
Queue reaction behavior, lane-tail transitions, Keep/Skip independence, multi-reaction precedence, and the Bot API reaction-removal limitation are defined in [Priority, Reactions, Keep, and Skip](#priority-reactions-keep-and-skip). Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation, and dropping marked heads cannot leave status permanently queued.
|
|
396
396
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Adaptive Button Literal
|
|
2
2
|
|
|
3
|
-
> Status: Portable CML v3 standard
|
|
3
|
+
> Status: Portable CML v3 button standard; `pi-telegram` also reuses its positional-cell mechanics for compact voice actions.
|
|
4
4
|
|
|
5
5
|
Adaptive Button Literal is one bounded-depth matrix grammar over a shared button AST. It accepts strict JSON button objects, positional Compact Matrix Literal (CML) cells, or both in the same matrix and row. Commas between completed matrix or row elements are optional, so producers can progressively compress representation without changing runtime meaning.
|
|
6
6
|
|
|
@@ -58,13 +58,16 @@ A two-atom cell separates label and prompt:
|
|
|
58
58
|
{Pause|music::pause}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Only the first label position may be empty. Prompt-only `{|e2}` is equivalent to JSON `{"prompt":"e2"}`: the existing button fallback uses `e2` as both visible text and queued prompt without requiring a separately authored label.
|
|
62
|
+
|
|
63
|
+
A three-atom cell adds the selected style and retains the same optional-label form:
|
|
62
64
|
|
|
63
65
|
```text
|
|
64
66
|
{Stop|music::stop|danger}
|
|
67
|
+
{|e2|success}
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
The style atom is accepted only as `primary`, `success`, or `danger`.
|
|
70
|
+
The prompt and style atoms remain required. The style atom is accepted only as `primary`, `success`, or `danger`.
|
|
68
71
|
|
|
69
72
|
## Adaptive Grammar
|
|
70
73
|
|
|
@@ -78,16 +81,16 @@ row := "[" ws cell (boundary cell)* ws "]"
|
|
|
78
81
|
cell := json-object | positional-cell
|
|
79
82
|
boundary := ws [","] ws
|
|
80
83
|
positional-cell := "{" atom "}"
|
|
81
|
-
| "{" atom "|" atom "}"
|
|
82
|
-
| "{" atom "|" atom "|" atom "}"
|
|
84
|
+
| "{" [atom] "|" atom "}"
|
|
85
|
+
| "{" [atom] "|" atom "|" atom "}"
|
|
83
86
|
atom := atom-unit+
|
|
84
87
|
atom-unit := ordinary | "\|" | "\}" | "\\"
|
|
85
88
|
ws := *(SP | HTAB | CR | LF)
|
|
86
89
|
```
|
|
87
90
|
|
|
88
|
-
`boundary` occurs
|
|
91
|
+
`boundary` occurs after one complete element. It may contain one comma or no comma; one trailing comma before a closing row or matrix delimiter is also tolerated. Element delimiters keep empty adjacency unambiguous. Leading and repeated commas remain invalid.
|
|
89
92
|
|
|
90
|
-
A `json-object` is one complete
|
|
93
|
+
A `json-object` is one complete JSON object. Strict JSON is attempted first; a bounded recovery removes commas immediately before `}` or `]` outside strings and retries. Property names, strings, escaping, nested values, and all other internals remain strict; missing property commas are not invented.
|
|
91
94
|
|
|
92
95
|
Rows cannot contain rows. The grammar never recurses beyond one row inside the top-level matrix.
|
|
93
96
|
|
|
@@ -111,29 +114,32 @@ A conforming parser:
|
|
|
111
114
|
|
|
112
115
|
1. Attempts strict JSON first for sources beginning with `{` or `[`. Successful JSON is validated only against the existing button matrix schema and never reinterpreted.
|
|
113
116
|
2. If strict JSON parsing fails, parses the original source with the adaptive grammar.
|
|
114
|
-
3. Tries one complete strict JSON object at each cell boundary before positional interpretation.
|
|
115
|
-
4.
|
|
116
|
-
5.
|
|
117
|
-
6. Rejects empty
|
|
117
|
+
3. Tries one complete strict JSON object, then bounded trailing-comma recovery, at each cell boundary before positional interpretation.
|
|
118
|
+
4. Keeps JSON-shaped named objects on the JSON path when validation fails instead of exposing their source as positional text.
|
|
119
|
+
5. Accepts at most one comma between elements or immediately before a closing row or matrix delimiter, while rejecting leading, repeated, or property-level omitted commas.
|
|
120
|
+
6. Rejects empty matrices, rows, one-atom cells, prompts, styles, and nesting deeper than one row; only the first label position may be empty in a two- or three-atom button cell.
|
|
118
121
|
7. Decodes only `\|`, `\}`, and `\\` in positional cells.
|
|
119
|
-
8.
|
|
120
|
-
9. Returns no partial rows or cells
|
|
121
|
-
10. Runs
|
|
122
|
+
8. Extracts the first complete valid payload from a tolerant comment envelope and ignores unrelated text or isolated unmatched matrix brackets around it.
|
|
123
|
+
9. Returns no partial rows or cells from a balanced malformed candidate.
|
|
124
|
+
10. Runs over host-bounded payloads with fixed grammar depth.
|
|
122
125
|
|
|
123
126
|
Malformed JSON-looking input receives no generic recovery. It is accepted only if it independently forms a complete valid adaptive literal.
|
|
124
127
|
|
|
125
|
-
## Telegram
|
|
128
|
+
## Telegram Profiles
|
|
126
129
|
|
|
127
|
-
For `telegram_button`
|
|
130
|
+
For `telegram_button` comments:
|
|
128
131
|
|
|
129
132
|
- JSON `value` keeps its existing label/prompt fallback semantics.
|
|
130
|
-
- Positional `{value}` is equivalent to JSON `{"value":"value"}
|
|
133
|
+
- Positional `{value}` is equivalent to JSON `{"value":"value"}`; a lone JSON `label` or `prompt` has the same both-fields shorthand semantics.
|
|
134
|
+
- Positional `{|prompt}` is equivalent to JSON `{"prompt":"prompt"}` and therefore uses the prompt as both visible text and queued prompt.
|
|
131
135
|
- Positional `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
|
|
132
|
-
- Positional `{label|prompt|selected_style}`
|
|
136
|
+
- Positional `{label|prompt|selected_style}` and `{|prompt|selected_style}` are equivalent to their corresponding JSON objects.
|
|
133
137
|
- Top-level cells become full-width rows.
|
|
134
138
|
- Nested rows become horizontal keyboard rows.
|
|
135
139
|
- Invalid payloads are stripped with their recognized action comment and register no callbacks.
|
|
136
140
|
|
|
141
|
+
For `telegram_voice`, one positional cell maps `{text}`, `{text|lang}`, or `{text|lang|rate}` to one voice artifact. JSON object cells remain available for named fields, escaping, and multiline text. Voice comments do not accept matrix or row composition.
|
|
142
|
+
|
|
137
143
|
Example:
|
|
138
144
|
|
|
139
145
|
```html
|
|
@@ -151,7 +157,7 @@ The grammar imposes no visual row-width maximum. Renderer and interaction policy
|
|
|
151
157
|
Accepted classes include:
|
|
152
158
|
|
|
153
159
|
- Strict JSON objects and matrices.
|
|
154
|
-
- Positional singleton, two-atom, and styled cells.
|
|
160
|
+
- Positional singleton, two-atom, prompt-only `{|prompt}`, and styled cells.
|
|
155
161
|
- Matrices and rows with commas, without commas, or a mixture of boundaries.
|
|
156
162
|
- Named JSON and positional cells mixed in one matrix or row.
|
|
157
163
|
- Literal commas inside positional atoms and strict JSON strings.
|
|
@@ -160,17 +166,17 @@ Accepted classes include:
|
|
|
160
166
|
|
|
161
167
|
Rejected classes include:
|
|
162
168
|
|
|
163
|
-
- Empty payloads, matrices, rows,
|
|
164
|
-
- Leading
|
|
169
|
+
- Empty payloads, matrices, rows, one-atom cells, prompts, or style atoms; an empty label is valid only as the first position of a two- or three-atom button cell.
|
|
170
|
+
- Leading or repeated element commas.
|
|
165
171
|
- Missing commas between properties inside a JSON object.
|
|
166
172
|
- Deeper row nesting.
|
|
167
173
|
- Missing, crossed, or mismatched delimiters.
|
|
168
174
|
- A third positional separator, unknown style, unknown escape, or trailing backslash.
|
|
169
|
-
- Internal control characters
|
|
175
|
+
- Internal control characters.
|
|
170
176
|
- Valid JSON that fails the existing JSON action schema.
|
|
171
177
|
|
|
172
178
|
Every rejected case proves zero callback registration.
|
|
173
179
|
|
|
174
180
|
## Versioning
|
|
175
181
|
|
|
176
|
-
This document defines CML v3. V3 extends the v2 positional grammar with strict JSON object cells, mixed representation,
|
|
182
|
+
This document defines CML v3. V3 extends the v2 positional grammar with strict JSON object cells, mixed representation, optional element-boundary commas, and prompt-only button cells that preserve the established JSON fallback semantics. It does not make JSON object internals permissive and does not add deeper structures. Future versions must preserve strict-JSON-first routing, bounded depth, atomic rejection, and an explicit discriminator for any new meaning at a security or ownership boundary.
|
|
@@ -67,7 +67,7 @@ Built-in placeholders for text handlers:
|
|
|
67
67
|
|
|
68
68
|
## Media/File Handlers
|
|
69
69
|
|
|
70
|
-
Media/file handlers keep the legacy attachment-handler behavior: downloaded files are matched by `mime`, `type`, or `match`, then each file runs the first successful matching handler. Downloaded files with `mime: "text/plain"` or any `text/*` MIME type have a built-in fail-open handler that reads UTF-8 content into `[outputs]` when no configured handler produced output. Composition is useful for pipelines such as voice transcription followed by machine translation, so the agent receives translated `[outputs]` instead of the raw STT language.
|
|
70
|
+
Media/file handlers keep the legacy attachment-handler behavior: downloaded files are matched by `mime`, `type`, or `match`, then each file runs the first successful matching handler. Attachments from a replied-to message use the same handler/provider pipeline independently; their `[outputs|from:…]` section remains inside that `[reply]` context after its attachment list instead of being attributed to the current message. Downloaded files with `mime: "text/plain"` or any `text/*` MIME type have a built-in fail-open handler that reads UTF-8 content into `[outputs]` when no configured handler produced output. Composition is useful for pipelines such as voice transcription followed by machine translation, so the agent receives translated `[outputs]` instead of the raw STT language.
|
|
71
71
|
|
|
72
72
|
Built-in placeholders for media/file handlers:
|
|
73
73
|
|
|
@@ -129,45 +129,36 @@ Synthesis providers receive the extracted `telegram_voice` text plus optional `l
|
|
|
129
129
|
|
|
130
130
|
## Voice Markup
|
|
131
131
|
|
|
132
|
-
Assistant replies can include hidden voice actions
|
|
132
|
+
Assistant replies can include hidden voice actions as a positional compact cell or JSON object:
|
|
133
133
|
|
|
134
134
|
```md
|
|
135
135
|
Full text answer stays here.
|
|
136
136
|
|
|
137
|
-
<!-- telegram_voice {
|
|
137
|
+
<!-- telegram_voice {Short spoken companion summary.|ru|+30%} -->
|
|
138
138
|
|
|
139
|
-
<!-- telegram_voice text
|
|
139
|
+
<!-- telegram_voice {"text":"First line.\nSecond line.","lang":"ru"} -->
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
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.
|
|
142
|
+
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. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
|
|
143
143
|
|
|
144
144
|
## Buttons Markup
|
|
145
145
|
|
|
146
|
-
Assistant replies can include one or many button actions through
|
|
146
|
+
Assistant replies can include one or many button actions through a top-level `telegram_button` comment:
|
|
147
147
|
|
|
148
148
|
```md
|
|
149
149
|
I can continue.
|
|
150
150
|
|
|
151
|
-
<!-- telegram_button {"label":"Continue","prompt":"Continue with the current plan.","selected_style":"primary"} -->
|
|
152
|
-
|
|
153
|
-
<!-- telegram_button label="Show risks" prompt="List the main risks first." selected_style="danger" -->
|
|
154
|
-
|
|
155
|
-
<!-- telegram_button {"value":"Done"} -->
|
|
156
|
-
|
|
157
|
-
<!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
|
|
158
|
-
|
|
159
|
-
<!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
|
|
160
|
-
|
|
161
151
|
<!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
|
|
152
|
+
|
|
153
|
+
<!-- telegram_button {"label":"▶️ Continue","prompt":"Continue with the current plan.","selected_style":"primary"} -->
|
|
162
154
|
```
|
|
163
155
|
|
|
164
156
|
Rules:
|
|
165
157
|
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
|
|
158
|
+
- 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`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
159
|
+
- 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.
|
|
169
160
|
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
|
|
170
|
-
- 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 third unescaped CML separator, empty atoms, unknown selected styles, and deeper nesting are rejected atomically. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
161
|
+
- 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 third unescaped CML separator, empty prompt/style atoms, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Only the first label position may be empty in a two- or three-atom button cell. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
171
162
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
172
163
|
- After Telegram accepts a generated 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.
|
|
173
164
|
- 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.
|
|
@@ -184,8 +175,8 @@ The extension injects prompt guidance by context:
|
|
|
184
175
|
- For ordinary local/TUI prompts, the compact routing note points to the bundled `telegram-bridge` Skill and forbids Telegram use unless explicitly requested.
|
|
185
176
|
- For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
|
|
186
177
|
- For Telegram-originated turns, write the full technical answer as normal Markdown.
|
|
187
|
-
- Add `telegram_voice` with
|
|
188
|
-
- Add `telegram_button` with a JSON object, JSON matrix, or
|
|
178
|
+
- 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.
|
|
179
|
+
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
|
|
189
180
|
- 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 top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
|
|
190
181
|
- 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.
|
|
191
182
|
|
|
@@ -50,7 +50,7 @@ Stable commands inside the paired Telegram DM:
|
|
|
50
50
|
|
|
51
51
|
- `/start` — pair when needed and open the main application menu.
|
|
52
52
|
- `/compact` — open confirmation and compact when idle.
|
|
53
|
-
- `/next` — dispatch the next queued turn, aborting active work first when needed.
|
|
53
|
+
- `/next` — dispatch the next queued turn, aborting active work first when needed; one Pi-aligned informational reply anchors to a pre-abort snapshot of the Telegram turn or falls back to the command, and aborted pending assistant text is suppressed.
|
|
54
54
|
- `/continue` — enqueue a priority `continue` prompt.
|
|
55
55
|
- `/abort` — abort active work and keep the queue; abort-history is scoped to Telegram-owned active turns.
|
|
56
56
|
- `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
|
|
@@ -65,8 +65,8 @@ This command surface is a mobile companion subset, not a raw terminal-command br
|
|
|
65
65
|
- `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. 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.
|
|
66
66
|
- `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. 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 for requested cross-target delivery. 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.
|
|
67
67
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `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.
|
|
68
|
-
- `telegram_voice` hidden comments request Telegram-native voice delivery through
|
|
69
|
-
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md)
|
|
68
|
+
- `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.
|
|
69
|
+
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. 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`. 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. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
|
|
70
70
|
|
|
71
71
|
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.
|
|
72
72
|
|
|
@@ -336,7 +336,7 @@ Contract:
|
|
|
336
336
|
- Providers are synchronous because `/start` status text is rendered inline with the menu.
|
|
337
337
|
- Return `undefined` when the line is not relevant for the active model.
|
|
338
338
|
- Provider failures are isolated and skipped so optional companion status cannot break the core Telegram menu.
|
|
339
|
-
- The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Tokens, Cost, and Context, capitalizing the first label character for Telegram UI consistency. Tokens
|
|
339
|
+
- The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Tokens, Cache, Cost, and Context, capitalizing the first label character for Telegram UI consistency. Tokens contains only `↑` input and `↓` output totals. Cache groups `R` cache-read tokens, `W` cache-write tokens, and `CH` for the latest assistant request's `cacheRead / (input + cacheRead + cacheWrite)`, shown to one decimal place only after the session reports cache activity. These precise labels avoid conflating token telemetry with companion-provided usage-limit rows.
|
|
340
340
|
|
|
341
341
|
## Updates
|
|
342
342
|
|
|
@@ -428,6 +428,9 @@ export function createTelegramActivityBridgeRuntime(deps: {
|
|
|
428
428
|
onAssistantEvent(event) {
|
|
429
429
|
getRuntime()?.onAssistantEvent(event);
|
|
430
430
|
},
|
|
431
|
+
onAssistantMessageEnd(stopReason) {
|
|
432
|
+
getRuntime()?.onAssistantMessageEnd(stopReason);
|
|
433
|
+
},
|
|
431
434
|
onToolStart(event) {
|
|
432
435
|
getRuntime()?.onToolStart(event);
|
|
433
436
|
},
|
|
@@ -491,6 +494,7 @@ export interface TelegramActivityRuntime {
|
|
|
491
494
|
recordInputSource: (source: TelegramActivityInputSource) => void;
|
|
492
495
|
onAgentStart: (activeTelegramTarget?: TelegramActivityTarget) => void;
|
|
493
496
|
onAssistantEvent: (event: TelegramAssistantStreamEvent) => void;
|
|
497
|
+
onAssistantMessageEnd: (stopReason?: string) => void;
|
|
494
498
|
onToolStart: (event: {
|
|
495
499
|
toolCallId: string;
|
|
496
500
|
toolName: string;
|
|
@@ -677,6 +681,9 @@ export function createTelegramActivityRuntime(deps: {
|
|
|
677
681
|
}
|
|
678
682
|
if (event.type === "error") flushPendingSegment("terminal-partial");
|
|
679
683
|
},
|
|
684
|
+
onAssistantMessageEnd(stopReason) {
|
|
685
|
+
if (stopReason === "aborted") pendingAssistantSegment = undefined;
|
|
686
|
+
},
|
|
680
687
|
onToolStart(event) {
|
|
681
688
|
emit({ type: "tool-start", ...event });
|
|
682
689
|
},
|
|
@@ -1218,6 +1218,9 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1218
1218
|
},
|
|
1219
1219
|
onMessageEnd(event, ctx) {
|
|
1220
1220
|
if (!isSessionContextActive(ctx)) return;
|
|
1221
|
+
if (event.message.role === "assistant") {
|
|
1222
|
+
activityRuntime.onAssistantMessageEnd(event.message.stopReason);
|
|
1223
|
+
}
|
|
1221
1224
|
terminalAssistantMessagePendingDelivery =
|
|
1222
1225
|
event.message.role === "assistant" &&
|
|
1223
1226
|
event.message.stopReason !== "toolUse" &&
|
|
@@ -1049,6 +1049,9 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
1049
1049
|
text: string,
|
|
1050
1050
|
options?: { parseMode?: "HTML" },
|
|
1051
1051
|
) => Promise<void>;
|
|
1052
|
+
getActiveTurnReply?: () =>
|
|
1053
|
+
| ((text: string, options?: { parseMode?: "HTML" }) => Promise<void>)
|
|
1054
|
+
| undefined;
|
|
1052
1055
|
sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"];
|
|
1053
1056
|
assertExecutionCurrent?: (message: TMessage) => void;
|
|
1054
1057
|
}
|
|
@@ -1272,6 +1275,9 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1272
1275
|
text: string,
|
|
1273
1276
|
options?: { parseMode?: "HTML" },
|
|
1274
1277
|
) => Promise<void>;
|
|
1278
|
+
getActiveTurnReply?: () =>
|
|
1279
|
+
| ((text: string, options?: { parseMode?: "HTML" }) => Promise<void>)
|
|
1280
|
+
| undefined;
|
|
1275
1281
|
}): Promise<void> {
|
|
1276
1282
|
deps.clearPendingModelSwitch();
|
|
1277
1283
|
if (!deps.hasQueuedItems()) {
|
|
@@ -1282,16 +1288,19 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1282
1288
|
return;
|
|
1283
1289
|
}
|
|
1284
1290
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
1291
|
+
const activeTurnReply = deps.getActiveTurnReply?.();
|
|
1285
1292
|
deps.clearFoldForDispatch();
|
|
1286
1293
|
deps.abortCurrentTurn();
|
|
1287
1294
|
deps.updateStatus();
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
"Aborted! Dispatching next queued turn.",
|
|
1292
|
-
),
|
|
1293
|
-
{ parseMode: "HTML" },
|
|
1295
|
+
const notice = formatTelegramInformationHeading(
|
|
1296
|
+
"⏩",
|
|
1297
|
+
"Operation aborted. Dispatching next queued turn.",
|
|
1294
1298
|
);
|
|
1299
|
+
if (activeTurnReply) {
|
|
1300
|
+
await activeTurnReply(notice, { parseMode: "HTML" });
|
|
1301
|
+
} else {
|
|
1302
|
+
await deps.sendTextReply(notice, { parseMode: "HTML" });
|
|
1303
|
+
}
|
|
1295
1304
|
return;
|
|
1296
1305
|
}
|
|
1297
1306
|
if (!deps.isIdle()) {
|
|
@@ -1799,6 +1808,7 @@ async function handleTelegramCommandRuntime<
|
|
|
1799
1808
|
deps.setFoldQueuedPromptsIntoHistory(false),
|
|
1800
1809
|
updateStatus: updateStatusFor(commandCtx),
|
|
1801
1810
|
sendTextReply: sendReplyFor(nextMessage),
|
|
1811
|
+
getActiveTurnReply: deps.getActiveTurnReply,
|
|
1802
1812
|
});
|
|
1803
1813
|
},
|
|
1804
1814
|
handleContinue: async (nextMessage, commandCtx) => {
|
|
@@ -366,6 +366,7 @@ export function extractTelegramReplyContextText(
|
|
|
366
366
|
export function buildTelegramReplyContextBlock(
|
|
367
367
|
message: TelegramMediaMessage,
|
|
368
368
|
replyFiles: Pick<DownloadedTelegramFile, "path">[] = [],
|
|
369
|
+
replyOutputs: readonly string[] = [],
|
|
369
370
|
): string {
|
|
370
371
|
const from = formatTelegramUser(message.reply_to_message?.from);
|
|
371
372
|
const header = from ? `[reply|from:${from}]` : "[reply]";
|
|
@@ -379,10 +380,19 @@ export function buildTelegramReplyContextBlock(
|
|
|
379
380
|
? replyFiles.map((file) => `- /${basename(file.path)}`)
|
|
380
381
|
: replyFiles.map((file) => `- ${file.path}`);
|
|
381
382
|
const replyBlock = text ? `${header} ${text}` : header;
|
|
383
|
+
const sections = [replyBlock];
|
|
382
384
|
if (fileLines.length > 0) {
|
|
383
|
-
|
|
385
|
+
sections.push(`${attachmentHeader}\n${fileLines.join("\n")}`);
|
|
386
|
+
}
|
|
387
|
+
if (replyOutputs.length > 0) {
|
|
388
|
+
const outputHeader = `[outputs${from ? `|from:${from}` : ""}]`;
|
|
389
|
+
sections.push(
|
|
390
|
+
`${outputHeader}\n${replyOutputs.map((output) => `- ${output}`).join("\n")}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (text || fileLines.length > 0 || replyOutputs.length > 0) {
|
|
394
|
+
return sections.join("\n\n");
|
|
384
395
|
}
|
|
385
|
-
if (text) return replyBlock;
|
|
386
396
|
return "";
|
|
387
397
|
}
|
|
388
398
|
|
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
} from "./keyboard.ts";
|
|
13
13
|
import {
|
|
14
14
|
parseTelegramActionPayloadRows,
|
|
15
|
-
parseTopLevelTelegramComment,
|
|
16
15
|
replaceTopLevelHtmlComments,
|
|
17
16
|
} from "./outbound-markup.ts";
|
|
18
17
|
import {
|
|
@@ -113,8 +112,10 @@ function parseTelegramButtonAction(
|
|
|
113
112
|
payload: Record<string, unknown>,
|
|
114
113
|
): TelegramOutboundButtonAction | undefined {
|
|
115
114
|
const value = getTelegramButtonString(payload, "value");
|
|
116
|
-
const
|
|
117
|
-
const
|
|
115
|
+
const explicitLabel = getTelegramButtonString(payload, "label");
|
|
116
|
+
const explicitPrompt = getTelegramButtonString(payload, "prompt");
|
|
117
|
+
const label = explicitLabel ?? value ?? explicitPrompt;
|
|
118
|
+
const prompt = explicitPrompt ?? value ?? explicitLabel;
|
|
118
119
|
if (!label || !prompt) return undefined;
|
|
119
120
|
const selectedStyle = payload.selected_style;
|
|
120
121
|
return {
|
|
@@ -179,10 +180,9 @@ export function planTelegramButtonReply(
|
|
|
179
180
|
): TelegramButtonReplyPlan {
|
|
180
181
|
const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
|
|
181
182
|
const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
|
|
182
|
-
const command =
|
|
183
|
-
|
|
184
|
-
);
|
|
185
|
-
if (!command) return comment.raw;
|
|
183
|
+
const command = "telegram_button";
|
|
184
|
+
const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
|
|
185
|
+
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
186
186
|
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
187
187
|
if (!payloadRows) return "";
|
|
188
188
|
const actionRows = payloadRows.map((payloadRow) =>
|
|
@@ -225,8 +225,9 @@ export function createTelegramButtonPromptTurn(options: {
|
|
|
225
225
|
queueOrder: number;
|
|
226
226
|
action: TelegramOutboundButtonAction;
|
|
227
227
|
target?: TelegramQueueTarget;
|
|
228
|
+
telegramPrefix?: string;
|
|
228
229
|
}): PendingTelegramTurn {
|
|
229
|
-
const prompt =
|
|
230
|
+
const prompt = `${options.telegramPrefix ?? "[telegram]"} ${options.action.prompt}`;
|
|
230
231
|
return {
|
|
231
232
|
kind: "prompt",
|
|
232
233
|
chatId: options.chatId,
|