Skip to content

Feat/telegram#31

Closed
leszek3737 wants to merge 5 commits intomainfrom
feat/telegram
Closed

Feat/telegram#31
leszek3737 wants to merge 5 commits intomainfrom
feat/telegram

Conversation

@leszek3737
Copy link
Copy Markdown
Owner

@leszek3737 leszek3737 commented Apr 12, 2026

Type

  • Agent template (TOML)
  • Skill (Python/JS/Prompt)
  • Channel adapter
  • LLM provider
  • Built-in tool
  • Bug fix
  • Feature (Rust)
  • Documentation / Translation
  • Refactor / Performance
  • CI / Tooling
  • Other

Summary

Changes

Attribution

  • This PR preserves author attribution for any adapted prior work (Co-authored-by, commit preservation, or explicit credit in the PR body)

Testing

  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace passes
  • Live integration tested (if applicable)

Security

  • No new unsafe code
  • No secrets or API keys in diff
  • User input validated at boundaries

Summary by Sourcery

Extend Telegram channel capabilities, rich media handling, and polling support across the stack, while wiring a new channel_send-based poll API into the runtime and updating tool profiles accordingly.

New Features:

  • Add structured bot command definitions and automatic registration of Telegram slash commands in the bot menu.
  • Introduce support for Telegram audio, animation, sticker, media group albums, and poll messages, including inbound poll answers.
  • Expose a channel_send-based API for creating polls/quizzes from agents, including kernel, tool runner, and prompt hints.
  • Add per-poll context tracking so poll answers can be mapped back to questions/options in logs and dispatch text.

Enhancements:

  • Improve Telegram long-polling to handle poll_answer updates and to re-register bot commands on /start or first message.
  • Extend ChannelContent and MediaGroup types to model new media and poll variants and surface them through bridge logging/dispatch formatting.
  • Document channel_send in tool hints and include it in Messaging and Automation tool profiles so agents can send rich media where allowed.
  • Refine uninstall_hand logic and related API error formatting without changing behavior.

Tests:

  • Add tests for new ChannelContent to-text formatting and Telegram audio parsing semantics.
  • Update prompt builder tests to cover channel_send-specific guidance when the tool is granted.
  • Adjust tool profile tests to assert presence and counts for the new channel_send capability.

…nds, and auto-registration

Add 7 new ChannelContent variants (DeleteMessage, Audio, Animation, Sticker,
MediaGroup, Poll, PollAnswer) with full send/receive support in the Telegram
adapter. New API methods: sendAudio, sendAnimation, sendSticker, sendMediaGroup,
sendPoll, deleteMessage, setMyCommands, deleteMyCommands, getMyCommands.

Inbound parsing now handles audio files (as Audio, not Voice), animations
(checked before video to avoid misclassification), stickers, and poll answers.

Bot command menu is auto-registered on start() using built-in defaults
(BUILTIN_COMMANDS) when no custom commands are configured, and re-registered
on /start or first incoming message via the polling loop.

Bridge updated with exhaustive match arms in content_to_text and
dispatch_message for all new variants, plus 8 new unit tests.
Agents using Messaging or Automation profiles had no way to send images,
files, or other media to channel users because channel_send was only
available in the Full profile. This made the agent suggest using the API
directly instead of its built-in tools.
The channel awareness section in the system prompt told agents about
formatting and character limits but never mentioned channel_send.
Agents had the tool available but did not know to use it for sending
images or files, proposing raw API calls instead.

Add a channel_send hint to the Channel section when the tool is granted,
including the recipient ID so the agent can use it immediately. Also add
a tool_hint entry so channel_send appears with a description in the
Your Tools section.
Add send_channel_poll to KernelHandle trait and kernel implementation.
Extend channel_send tool with poll_question, poll_options, poll_is_quiz,
poll_correct_option, poll_explanation parameters. Telegram adapter stores
poll context (question + options) keyed by poll_id and attaches it to
inbound PollAnswer messages so agents see readable answers instead of
bare numeric indices.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 12, 2026

Reviewer's Guide

Extends the Telegram channel adapter and related infrastructure to support richer Telegram features (bot command menu, audio/animation/sticker/media groups/polls, message deletion) and exposes a new channel_send-based poll capability to agents, along with minor refactors and test updates across bridge, runtime, kernel, hands, API, and types crates.

Sequence diagram for sending a poll via channel_send tool

sequenceDiagram
    actor User
    participant Agent
    participant ToolRunner
    participant KernelHandle
    participant TelegramAdapter
    participant TelegramApi as Telegram_Bot_API

    User->>Agent: Ask a question requiring a poll
    Agent->>ToolRunner: Call tool_channel_send with poll_question + poll_options
    ToolRunner->>ToolRunner: Validate poll_options (>= 2)
    ToolRunner->>KernelHandle: send_channel_poll(channel, recipient, question, options, is_quiz, correct_option_id, explanation)

    KernelHandle->>TelegramAdapter: send(user, ChannelContent::Poll)
    TelegramAdapter->>TelegramAdapter: Build PollParams from ChannelContent::Poll
    TelegramAdapter->>TelegramApi: sendPoll(chat_id, question, options, type=regular|quiz, ...)
    TelegramApi-->>TelegramAdapter: HTTP response with poll id
    TelegramAdapter->>TelegramAdapter: Store PollContext in poll_contexts keyed by poll id

    TelegramAdapter-->>KernelHandle: Ok(())
    KernelHandle-->>ToolRunner: Ok(())
    ToolRunner-->>Agent: "Poll sent to recipient on channel..."
    Agent-->>User: Confirms that a poll has been posted
Loading

Sequence diagram for handling Telegram poll_answer updates

sequenceDiagram
    actor TelegramUser
    participant TelegramApp as Telegram_Client
    participant TelegramApi as Telegram_Bot_API
    participant PollLoop as Telegram_polling_loop
    participant Bridge as ChannelBridge
    participant Agent

    TelegramUser->>TelegramApp: Answer poll
    TelegramApp->>TelegramApi: submit poll_answer

    loop Long polling
        PollLoop->>TelegramApi: getUpdates(allowed_updates includes poll_answer)
        TelegramApi-->>PollLoop: Update with poll_answer
    end

    PollLoop->>PollLoop: Extract poll_id, user info, option_ids
    PollLoop->>PollLoop: Look up PollContext in poll_contexts by poll_id
    PollLoop->>PollLoop: Build ChannelMessage with ChannelContent::PollAnswer
    PollLoop->>Bridge: tx.send(ChannelMessage)

    Bridge->>Bridge: dispatch_message formats human-readable text
    Bridge-->>Agent: Deliver poll answer context
    Agent->>Agent: Reason over user choices
    Agent-->>TelegramUser: Follow-up message or action based on poll answer
Loading

Class diagram for extended Telegram channel types and poll support

classDiagram
    class TelegramAdapter {
        -api_base_url: String
        -token: SecretString
        -commands: Vec~BotCommand~
        -poll_contexts: DashMap~String, PollContext~
        +with_commands(commands: Vec~BotCommand~) TelegramAdapter
        +api_send_audio(chat_id: i64, audio_url: &str, caption: Option~&str~, title: Option~&str~, performer: Option~&str~, thread_id: Option~i64~) Result~(), Error~
        +api_send_animation(chat_id: i64, animation_url: &str, caption: Option~&str~, thread_id: Option~i64~) Result~(), Error~
        +api_send_sticker(chat_id: i64, file_id: &str, thread_id: Option~i64~) Result~(), Error~
        +api_send_media_group(chat_id: i64, items: &[MediaGroupItem], thread_id: Option~i64~) Result~(), Error~
        +api_send_poll(params: &PollParams) Result~String, Error~
        +api_delete_message(chat_id: i64, message_id: i64) Result~(), Error~
        +api_set_my_commands(commands: &[BotCommand]) Result~(), Error~
        +api_delete_my_commands() Result~(), Error~
        +api_get_my_commands() Vec~BotCommand~
    }

    class TelegramApiCtx {
        -api_base_url: String
        -token: SecretString
        -client: Client
        +get_file_url(file_id: &str) Option~String~
        +set_my_commands(commands: &[BotCommand]) async
    }

    class BotCommand {
        +command: String
        +description: String
    }

    class PollContext {
        +question: String
        +options: Vec~String~
    }

    class PollParams {
        +chat_id: i64
        +question: &str
        +options: &[String]
        +is_quiz: bool
        +correct_option_id: Option~u8~
        +explanation: Option~&str~
        +thread_id: Option~i64~
    }

    class MediaGroupItem {
        <<enum>>
        Photo
        Video
    }

    class ChannelContent {
        <<enum>>
        Text
        Image
        File
        Interactive
        ButtonCallback
        Voice
        Location
        DeleteMessage
        Audio
        Animation
        Sticker
        MediaGroup
        Poll
        PollAnswer
    }

    class ChannelContent_DeleteMessage {
        +message_id: String
    }

    class ChannelContent_Audio {
        +url: String
        +caption: Option~String~
        +duration_seconds: u32
        +title: Option~String~
        +performer: Option~String~
    }

    class ChannelContent_Animation {
        +url: String
        +caption: Option~String~
        +duration_seconds: u32
    }

    class ChannelContent_Sticker {
        +file_id: String
    }

    class ChannelContent_MediaGroup {
        +items: Vec~MediaGroupItem~
    }

    class ChannelContent_Poll {
        +question: String
        +options: Vec~String~
        +is_quiz: bool
        +correct_option_id: Option~u8~
        +explanation: Option~String~
    }

    class ChannelContent_PollAnswer {
        +poll_id: String
        +option_ids: Vec~u8~
    }

    class LibreFangKernel {
        +send_channel_poll(channel: &str, recipient: &str, question: &str, options: &[String], is_quiz: bool, correct_option_id: Option~u8~, explanation: Option~&str~) Result~(), String~
    }

    class KernelHandle {
        <<trait>>
        +send_channel_poll(channel: &str, recipient: &str, question: &str, options: &[String], is_quiz: bool, correct_option_id: Option~u8~, explanation: Option~&str~) Result~(), String~
    }

    TelegramAdapter --> BotCommand : uses
    TelegramAdapter --> PollContext : stores
    TelegramAdapter --> PollParams : uses
    TelegramAdapter --> MediaGroupItem : uses
    TelegramApiCtx --> BotCommand : uses
    ChannelContent o-- ChannelContent_DeleteMessage
    ChannelContent o-- ChannelContent_Audio
    ChannelContent o-- ChannelContent_Animation
    ChannelContent o-- ChannelContent_Sticker
    ChannelContent o-- ChannelContent_MediaGroup
    ChannelContent o-- ChannelContent_Poll
    ChannelContent o-- ChannelContent_PollAnswer
    ChannelContent_MediaGroup --> MediaGroupItem
    LibreFangKernel ..|> KernelHandle
    LibreFangKernel --> ChannelContent_Poll
    LibreFangKernel --> TelegramAdapter
Loading

File-Level Changes

Change Details Files
Add rich Telegram message types (audio, animation, sticker, media groups, polls) and message deletion support to the channel adapter.
  • Define new ChannelContent variants and a MediaGroupItem enum to model richer media and poll messages.
  • Extend TelegramAdapter with helper APIs for sendAudio, sendAnimation, sendSticker, sendMediaGroup, sendPoll, and deleteMessage, including rate-limit handling and best-effort deletion.
  • Wire new ChannelContent variants into TelegramAdapter::send to dispatch appropriate Telegram API calls and track poll contexts for later poll_answer handling.
  • Enhance extract_telegram_content to map incoming Telegram audio, animation, sticker, and updated audio semantics into the new ChannelContent variants and update existing tests accordingly.
crates/librefang-channels/src/types.rs
crates/librefang-channels/src/telegram.rs
Support Telegram polls end-to-end, including inbound poll answers and agent-accessible poll creation via the channel_send tool.
  • Handle Telegram poll_answer updates in the long-polling loop, constructing ChannelContent::PollAnswer messages with metadata (question/options) when available.
  • Extend bridge content_to_text and dispatch_message logging to cover Poll and PollAnswer, including resolving selected option texts from metadata.
  • Add KernelHandle::send_channel_poll (trait + implementation) and plumb it through tool_channel_send so agents can create polls/quizzes with question/options/correct answer/explanation parameters.
  • Update prompt_builder to hint about channel_send for media/polls and register channel_send in appropriate ToolProfile variants, with tests for the new guidance paths.
crates/librefang-channels/src/telegram.rs
crates/librefang-channels/src/bridge.rs
crates/librefang-runtime/src/tool_runner.rs
crates/librefang-kernel/src/kernel.rs
crates/librefang-runtime/src/kernel_handle.rs
crates/librefang-runtime/src/prompt_builder.rs
crates/librefang-types/src/agent.rs
Introduce Telegram bot command menu management and configurable bot commands.
  • Define a BotCommand struct and BUILTIN_COMMANDS list plus a helper to build default command vectors.
  • Add commands field and with_commands builder to TelegramAdapter, and register commands via api_set_my_commands during start(), with automatic re-registration on first message or /start within the polling loop using TelegramApiCtx::set_my_commands.
  • Provide api_set_my_commands, api_delete_my_commands, and api_get_my_commands helper methods for external inspection and management of bot commands, with logging and best-effort error handling.
crates/librefang-channels/src/telegram.rs
Improve bridge and runtime user-visible text/logging for new content types and channel_send usage.
  • Extend bridge::content_to_text and dispatch_message formatting for DeleteMessage, Audio, Animation, Sticker, MediaGroup, Poll, and PollAnswer, including richer poll answer formatting using metadata.
  • Add tests covering the new content_to_text branches to ensure non-empty and informative strings for all new variants.
  • Update prompt_builder::build_channel_section signature and callers to pass granted_tools, and add conditional guidance text when channel_send is available, with tests verifying presence/absence of hints.
crates/librefang-channels/src/bridge.rs
crates/librefang-runtime/src/prompt_builder.rs
Minor refactors and formatting tweaks in hands registry and skills API error handling, plus tool profile updates for channel_send.
  • Refactor HandRegistry::uninstall_hand and related tests for cleaner formatting while preserving behavior, including workspace path handling assertions.
  • Slightly simplify skills::uninstall_hand API error mapping for BuiltinHand cases without changing semantics.
  • Update ToolProfile::Messaging and ::Automation to include channel_send and adjust tests for the new tool counts.
crates/librefang-hands/src/registry.rs
crates/librefang-api/src/routes/skills.rs
crates/librefang-types/src/agent.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions bot added area/kernel Core kernel (scheduling, RBAC, workflows) area/runtime Agent loop, LLM drivers, WASM sandbox area/channels Messaging channel adapters labels Apr 12, 2026
Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The new PollContext entries in TelegramAdapter are inserted on every outbound poll but never removed; consider cleaning them up when they are no longer needed (e.g., after receiving poll_answer or after a timeout) to avoid unbounded growth of poll_contexts.
  • There are now two separate implementations of setMyCommands (on TelegramApiCtx and TelegramAdapter) that build nearly identical payloads and perform similar HTTP calls; consider refactoring to share a single helper to reduce duplication and keep behavior consistent.
  • The channel_send tool schema and docstring state that poll_question is mutually exclusive with image_url/file_url/file_path, but tool_channel_send does not currently validate or error on such combinations—adding an explicit check would prevent confusing mixed invocations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `PollContext` entries in `TelegramAdapter` are inserted on every outbound poll but never removed; consider cleaning them up when they are no longer needed (e.g., after receiving `poll_answer` or after a timeout) to avoid unbounded growth of `poll_contexts`.
- There are now two separate implementations of `setMyCommands` (on `TelegramApiCtx` and `TelegramAdapter`) that build nearly identical payloads and perform similar HTTP calls; consider refactoring to share a single helper to reduce duplication and keep behavior consistent.
- The `channel_send` tool schema and docstring state that `poll_question` is mutually exclusive with `image_url`/`file_url`/`file_path`, but `tool_channel_send` does not currently validate or error on such combinations—adding an explicit check would prevent confusing mixed invocations.

## Individual Comments

### Comment 1
<location path="crates/librefang-channels/src/telegram.rs" line_range="727-736" />
<code_context>
+            self.token.as_str()
+        );
+        let body = serde_json::json!({});
+        let resp = self.client.post(&url).json(&body).send().await?;
+        if !resp.status().is_success() {
+            let body_text = resp.text().await.unwrap_or_default();
+            warn!("Telegram deleteMyCommands failed: {body_text}");
+        }
+        Ok(())
+    }
+
+    /// Call `getMyCommands` and return the currently registered bot commands.
+    ///
+    /// Returns an empty vec on failure (best-effort).
+    pub async fn api_get_my_commands(&self) -> Vec<BotCommand> {
+        let url = format!(
+            "{}/bot{}/getMyCommands",
+            self.api_base_url,
+            self.token.as_str()
+        );
+        let resp = match self
+            .client
+            .post(&url)
+            .json(&serde_json::json!({}))
+            .send()
+            .await
+        {
</code_context>
<issue_to_address>
**issue (bug_risk):** Non-429 sendMediaGroup failures are logged but treated as success, which can silently drop albums.

In `api_send_media_group`, non-429, non-success responses only emit a warning but still return `Ok(())`, so callers will assume the media group was sent even on 4xx/5xx responses. This can silently drop albums. Please return an error (or otherwise surface failure, e.g., via `Result`/bool) so higher layers can retry, notify the user, or avoid reporting success. The behavior should be consistent with the retry path, which already returns an error on repeated failure.
</issue_to_address>

### Comment 2
<location path="crates/librefang-channels/src/telegram.rs" line_range="764-767" />
<code_context>
+        params: &PollParams<'_>,
+    ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
+        let url = format!("{}/bot{}/sendPoll", self.api_base_url, self.token.as_str());
+        let option_values: Vec<serde_json::Value> = params
+            .options
+            .iter()
+            .map(|o| serde_json::json!({"text": o}))
+            .collect();
+
</code_context>
<issue_to_address>
**issue (bug_risk):** sendPoll builds options as objects with a `text` field, which does not match Telegram's expected API schema.

In `api_send_poll`, `options` is serialized as an array of `{ "text": <option> }` objects, but the Telegram Bot API `sendPoll` method expects an array of strings (e.g., `["A", "B"]`). This mismatch will likely trigger 400 errors. Instead of wrapping each option in `{text: ...}`, pass `params.options` directly (e.g., via `serde_json::json!(params.options)` or as a `Vec<String>`).
</issue_to_address>

### Comment 3
<location path="crates/librefang-runtime/src/tool_runner.rs" line_range="2865-2874" />
<code_context>
+    if let Some(poll_question) = input.get("poll_question").and_then(|v| v.as_str()) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Poll handling in channel_send does not enforce mutual exclusivity with other media fields, despite the schema description.

The implementation only checks `poll_options` length and proceeds, so when `poll_question` is set alongside fields like `image_url`, the image is silently ignored while the call still succeeds, which conflicts with the documented mutual exclusivity. Please either add validation that rejects requests combining `poll_question` with other media/file fields, or update the documentation to clarify that non-poll fields are ignored when sending polls.

Suggested implementation:

```rust
                    "poll_options": { "type": "array", "items": { "type": "string" }, "description": "Answer options for the poll (2-10 items, required with poll_question). When poll_question is set, any other media/file fields in the request are ignored." },

```

To fully align the documentation with the runtime behavior, similar clarifications should be added to:
1. The `poll_question` field description in the same schema object, stating that it is not mutually exclusive in validation and that other media/file fields are ignored when it is provided.
2. Any higher-level API or user-facing documentation that currently describes `poll_question` as mutually exclusive with other media/file fields, to reflect that such fields are accepted but ignored when sending polls.
If you instead prefer enforcing strict mutual exclusivity, the `if let Some(poll_question)` block should be updated to detect the presence of other media/file keys in `input` and return the appropriate validation error type used elsewhere in `channel_send` when conflicting fields are detected.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +727 to +736
let resp = self.client.post(&url).json(&body).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body_text = resp.text().await.unwrap_or_default();

if status.as_u16() == 429 {
let retry_after = extract_retry_after(&body_text, RETRY_AFTER_DEFAULT_SECS);
warn!("Telegram sendMediaGroup rate limited, retrying after {retry_after}s");
tokio::time::sleep(Duration::from_secs(retry_after)).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Non-429 sendMediaGroup failures are logged but treated as success, which can silently drop albums.

In api_send_media_group, non-429, non-success responses only emit a warning but still return Ok(()), so callers will assume the media group was sent even on 4xx/5xx responses. This can silently drop albums. Please return an error (or otherwise surface failure, e.g., via Result/bool) so higher layers can retry, notify the user, or avoid reporting success. The behavior should be consistent with the retry path, which already returns an error on repeated failure.

Comment on lines +764 to +767
let option_values: Vec<serde_json::Value> = params
.options
.iter()
.map(|o| serde_json::json!({"text": o}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): sendPoll builds options as objects with a text field, which does not match Telegram's expected API schema.

In api_send_poll, options is serialized as an array of { "text": <option> } objects, but the Telegram Bot API sendPoll method expects an array of strings (e.g., ["A", "B"]). This mismatch will likely trigger 400 errors. Instead of wrapping each option in {text: ...}, pass params.options directly (e.g., via serde_json::json!(params.options) or as a Vec<String>).

Comment on lines +2865 to +2874
if let Some(poll_question) = input.get("poll_question").and_then(|v| v.as_str()) {
let poll_options: Vec<String> = input
.get("poll_options")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Poll handling in channel_send does not enforce mutual exclusivity with other media fields, despite the schema description.

The implementation only checks poll_options length and proceeds, so when poll_question is set alongside fields like image_url, the image is silently ignored while the call still succeeds, which conflicts with the documented mutual exclusivity. Please either add validation that rejects requests combining poll_question with other media/file fields, or update the documentation to clarify that non-poll fields are ignored when sending polls.

Suggested implementation:

                    "poll_options": { "type": "array", "items": { "type": "string" }, "description": "Answer options for the poll (2-10 items, required with poll_question). When poll_question is set, any other media/file fields in the request are ignored." },

To fully align the documentation with the runtime behavior, similar clarifications should be added to:

  1. The poll_question field description in the same schema object, stating that it is not mutually exclusive in validation and that other media/file fields are ignored when it is provided.
  2. Any higher-level API or user-facing documentation that currently describes poll_question as mutually exclusive with other media/file fields, to reflect that such fields are accepted but ignored when sending polls.
    If you instead prefer enforcing strict mutual exclusivity, the if let Some(poll_question) block should be updated to detect the presence of other media/file keys in input and return the appropriate validation error type used elsewhere in channel_send when conflicting fields are detected.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly expands the communication channel capabilities, primarily focusing on the Telegram adapter. It introduces support for rich media types such as audio, animations, stickers, media groups, and polls, along with message deletion and bot command menu management. The channel_send tool and system prompt generation have been updated to enable agents to utilize these new features. Review feedback highlights critical error-handling issues in the Telegram adapter where API failures for media groups and polls were being logged but not propagated as errors to the caller.

return Ok(());
}

warn!("Telegram sendMediaGroup failed ({status}): {body_text}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The function logs a warning but returns Ok(()) when the Telegram API call fails (and it's not a 429 rate limit). This will lead the caller to believe the media group was successfully sent when it wasn't. It should return an error instead.

Suggested change
warn!("Telegram sendMediaGroup failed ({status}): {body_text}");
return Err(format!("Telegram sendMediaGroup failed ({status}): {body_text}").into());

Comment on lines +813 to +814
warn!("Telegram sendPoll failed ({status}): {body_text}");
return Ok(String::new());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to api_send_media_group, this function logs a warning but returns Ok(String::new()) on failure. This swallows the error and prevents the caller from knowing that the poll failed to send. It should return an error.

Suggested change
warn!("Telegram sendPoll failed ({status}): {body_text}");
return Ok(String::new());
warn!("Telegram sendPoll failed ({status}): {body_text}");
return Err(format!("Telegram sendPoll failed ({status}): {body_text}").into());

@leszek3737 leszek3737 closed this Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/channels Messaging channel adapters area/kernel Core kernel (scheduling, RBAC, workflows) area/runtime Agent loop, LLM drivers, WASM sandbox

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant