Skip to main content

Channel

Trait Channel 

pub trait Channel:
    Send
    + Sync
    + Attributable {
Show 51 methods // Required methods fn name(&self) -> &str; fn send<'life0, 'life1, 'async_trait>( &'life0 self, message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait; fn listen<'life0, 'async_trait>( &'life0 self, tx: Sender<ChannelMessage>, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait; // Provided methods fn send_final<'life0, 'life1, 'async_trait>( &'life0 self, message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn set_cancel_token(&self, _token: CancellationToken) { ... } fn uses_cancel_token(&self) -> bool { ... } fn health_check<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = bool> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait { ... } fn present_model_picker<'life0, 'life1, 'async_trait>( &'life0 self, _request: &'life1 ChannelModelPickerRequest, ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn listener_health(&self) -> Option<ListenerHealth> { ... } fn send_choice<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, recipient: &'life1 str, prompt: &'life2 str, options: &'life3 [(String, String)], ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn start_typing<'life0, 'life1, 'async_trait>( &'life0 self, _recipient: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn stop_typing<'life0, 'life1, 'async_trait>( &'life0 self, _recipient: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn supports_draft_updates(&self) -> bool { ... } fn supports_native_polls(&self) -> bool { ... } fn supports_outbound_send(&self) -> bool { ... } fn self_handle(&self) -> Option<String> { ... } fn self_addressed_mention(&self) -> Option<String> { ... } fn drop_self_messages(&self, msg: &ChannelMessage) -> bool { ... } fn forge_request<'life0, 'async_trait>( &'life0 self, _request: ForgeApiRequest, ) -> Pin<Box<dyn Future<Output = Result<ForgeApiResponse, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait { ... } fn is_direct_message(&self, _msg: &ChannelMessage) -> bool { ... } fn supports_multi_message_streaming(&self) -> bool { ... } fn supports_turn_flush_narration(&self) -> bool { ... } fn multi_message_delay_ms(&self) -> u64 { ... } fn multi_message_confirmed_offset<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = usize> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn send_draft<'life0, 'life1, 'async_trait>( &'life0 self, _message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<Option<String>, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn update_draft<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn update_draft_progress<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn update_draft_progress_batch<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, recipient: &'life1 str, message_id: &'life2 str, texts: &'life3 [String], ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn update_typed_draft_progress_batch<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, recipient: &'life1 str, message_id: &'life2 str, progress: &'life3 [DraftProgress], ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn update_draft_lifecycle<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _event: ProgressEvent, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn flush_draft_turn<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn discard_draft_turn<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn finalize_draft<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, _suppress_voice: bool, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn cancel_draft<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn add_reaction<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, _emoji: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn remove_reaction<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, _emoji: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn pin_message<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn unpin_message<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn redact_message<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, _reason: Option<String>, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn create_room<'life0, 'life1, 'async_trait>( &'life0 self, _options: &'life1 RoomCreationOptions, ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn send_poll<'life0, 'life1, 'async_trait>( &'life0 self, _poll: &'life1 PollRequest, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn invite_user<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _room_id: &'life1 str, _user_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn room_context(&self, _room_id: &str) -> Option<ChannelRoomContext> { ... } fn request_approval<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _request: &'life2 ChannelApprovalRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<ChannelApprovalResponse>, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn request_approval_attributed<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, recipient: &'life1 str, request: &'life2 ChannelApprovalRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<AttributedApprovalResponse>, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn request_approval_attributed_with_timeout<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, recipient: &'life1 str, request: &'life2 ChannelApprovalRequest, timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<AttributedApprovalResponse>, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn send_gate_prompt<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _prompt: &'life2 ChannelGatePrompt, ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn finalize_gate_prompt<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _reference: &'life1 str, _outcome: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn request_choice<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _question: &'life1 str, _choices: &'life2 [String], _timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<String>, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn request_multi_choice<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _question: &'life1 str, _choices: &'life2 [String], _min_items: usize, _max_items: usize, _timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<String>>, Error>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn supports_free_form_ask(&self) -> bool { ... }
}
Expand description

Core channel trait — implement for any messaging platform.

Every Channel is Attributable: the orchestrator’s spawn site opens attribution_span!(&*ch) so log emissions from within listen() / send() inherit channel = <type>.<alias>.

Required Methods§

Source

fn name(&self) -> &str

Human-readable channel name

Source

fn send<'life0, 'life1, 'async_trait>( &'life0 self, message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Send a message through this channel

Source

fn listen<'life0, 'async_trait>( &'life0 self, tx: Sender<ChannelMessage>, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Start listening for incoming messages (long-running)

Provided Methods§

Source

fn send_final<'life0, 'life1, 'async_trait>( &'life0 self, message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Send the completed assistant response for a turn.

Most channels use the ordinary send path. Channels with delivery policy that applies only to final responses can override this without changing the shared message shape or affecting system and approval sends.

Source

fn set_cancel_token(&self, _token: CancellationToken)

Receive the supervisor lifecycle token before listen() is called. Channels that internally wait for shutdown can subscribe instead of independently catching SIGINT.

Source

fn uses_cancel_token(&self) -> bool

Whether this channel participates in the cooperative cancellation contract via set_cancel_token. Only participating channels get a bounded grace period for cleanup after cancellation.

Source

fn health_check<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = bool> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Check if channel is healthy

Source

fn present_model_picker<'life0, 'life1, 'async_trait>( &'life0 self, _request: &'life1 ChannelModelPickerRequest, ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Present a channel-native model picker for a bare /model command.

The picker is presentation only: a confirmed selection must re-enter the runtime through the existing /model <ref> command path so model resolution and session scoping keep a single owner. request carries the reply scope (reply_target/thread_ts) the picker must preserve and a restart-scoped route snapshot it must not exceed; see ChannelModelPickerRequest for the per-field contract.

Returns Ok(true) when the channel presented a picker and took over the interaction, Ok(false) when it did not (e.g. a channel without a native picker, an alias or owner mismatch, or an empty requesting_user_id) — the caller then falls back to the ordinary text response. Returning Err reports a presentation failure; the caller logs it and falls back to the text response as well. The default implementation returns Ok(false) for every channel, keeping the existing text behavior.

Source

fn listener_health(&self) -> Option<ListenerHealth>

Listener health as the channel itself last observed it.

health_check is an active probe: implementations reach the remote service to answer it, and some of them send a real message or open a broker connection to do so. That makes it safe to call on demand — from channel doctor — and unsafe to call on a timer, so a recurring caller must not use it.

This is the recurring-safe counterpart. It is synchronous and returns state the channel recorded while it was already talking to the service, so reading it performs no I/O and cannot block, send, or connect.

None — the default — means the channel offers no signal at all, and the caller should fall back to whatever it knew before asking. A channel that does implement this reports a ListenerHealth, which is a three-state answer rather than a boolean: “I have nothing to say yet” is not the same claim as “I am working”, and a caller that collapses them reports a listener as healthy before it has ever connected.

An implementation owns its own freshness rule, because only the channel knows how often a working listener completes an exchange. A success from long enough ago that it is no longer evidence must be reported as ListenerHealth::Unhealthy, not as Healthy forever.

Source

fn send_choice<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, recipient: &'life1 str, prompt: &'life2 str, options: &'life3 [(String, String)], ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Send a discrete-choice prompt with options.

Each (callback_id, label) pair represents one choice. Whether the callback_id round-trips on inbound is channel-specific:

  • WhatsApp Cloud preserves the callback_id exactly — it appears as interactive.button_reply.id / interactive.list_reply.id, surfaced as [choice]<callback_id> in the inbound ChannelMessage.content.
  • Signal uses native polls. Real signal-cli pollVote payloads round-trip selected option indexes, surfaced as [choice-index]N with a 1-based index. Some alternate pollAnswer payloads may include selected titles and surface as [choice]<label>, but callers needing stable callback ids should maintain a side map keyed by poll option index.
  • Default text fallback (Matrix, SMS, IRC, mock) renders a numbered list with a “reply with name or number” hint and relies on the consumer’s own matcher to resolve the user’s text reply against the option list.

options.is_empty() is treated as “send the prompt as plain text with no choices” rather than rendering an empty selection hint; if prompt is also empty the call is a no-op (returns Ok(())).

prompt is the question / title; rendered ABOVE the options.

Source

fn start_typing<'life0, 'life1, 'async_trait>( &'life0 self, _recipient: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Signal that the bot is processing a response (e.g. “typing” indicator).

Source

fn stop_typing<'life0, 'life1, 'async_trait>( &'life0 self, _recipient: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Stop any active typing indicator.

Source

fn supports_draft_updates(&self) -> bool

Whether this channel supports progressive message updates via draft edits.

Source

fn supports_native_polls(&self) -> bool

Whether this channel can post a native poll through Channel::send_poll. Callers check this before offering one, so a channel without native polls keeps whatever fallback the caller already had.

Source

fn supports_outbound_send(&self) -> bool

Whether send actually delivers a message OUTBOUND on this channel. Default true. An INBOUND-ONLY transport (e.g. an AMQP trigger source whose send is a deliberate no-op that returns Ok) overrides this to false, so a surface that must genuinely deliver - such as the SOP approval route adapter - can refuse to route to it (and report the misconfiguration) instead of silently succeeding without sending anything.

Source

fn self_handle(&self) -> Option<String>

Self-loop guard for multi-agent runs: the bot’s own handle/identity on this channel, so the orchestrator can drop inbound events whose sender matches. A bot must never respond to its own messages, even if a misconfigured peer group lists the bot’s handle as an external peer.

Channels that handle inbound traffic must override this. The default None disables both layers of the self-loop guard (drop_self_messages here and the agent-loop fallback), leaving the channel unprotected from looping on its own outbound. Outbound-only channels can keep the default.

Source

fn self_addressed_mention(&self) -> Option<String>

The exact form the bot expects when addressed by users on this channel (e.g. Discord <@snowflake>, Telegram @bot_username). Returned verbatim into the per-channel system prompt. Default None for channels with no inbound mention concept. Channels that override self_handle should usually override this too.

Source

fn drop_self_messages(&self, msg: &ChannelMessage) -> bool

Whether the orchestrator should drop an inbound message as self-authored (multi-agent self-loop guard). Default compares msg.sender against Self::self_handle case-insensitively after stripping a leading @ from each side. Override only for platforms whose identity comparison is non-string.

Source

fn forge_request<'life0, 'async_trait>( &'life0 self, _request: ForgeApiRequest, ) -> Pin<Box<dyn Future<Output = Result<ForgeApiResponse, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Source

fn is_direct_message(&self, _msg: &ChannelMessage) -> bool

Whether an inbound message is a direct one-to-one conversation with the bot. A DM is definitionally addressed to the bot, so the orchestrator skips the reply-intent classifier and goes straight to the tool-capable agent turn. Default false: channels that cannot prove a one-to-one context keep the classifier precheck.

Source

fn supports_multi_message_streaming(&self) -> bool

Whether this channel supports multi-message streaming delivery.

Source

fn supports_turn_flush_narration(&self) -> bool

Whether this channel implements the turn-flush narration contract: flush_draft_turn delivers each completed agent text turn as a permanent outbound message, and the orchestrator therefore routes that narration through the outbound hook + leak-detection boundary and orders it ahead of the approval prompt via a flush barrier.

Distinct from Self::supports_multi_message_streaming: a channel can stream multiple messages another way (e.g. paragraph edits via update_draft) without implementing flush_draft_turn. Only channels that override flush_draft_turn may return true here, otherwise the orchestrator runs outbound policy on phantom flushes that deliver nothing. Default false.

Source

fn multi_message_delay_ms(&self) -> u64

Minimum delay (ms) between successive messages in multi-message mode. The message boundary is channel-specific (for example, Telegram uses completed agent turns).

Source

fn multi_message_confirmed_offset<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = usize> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Confirmed-delivery byte offset for a MultiMessage draft: how many bytes of the cumulative visible text previously handed to update_draft have already been emitted on the transport as paragraph messages (including their trailing \n\n delimiters).

The orchestrator reads this before finalize_draft so it can reconcile a sanitized final response against the paragraphs that are already on the wire without stranding or replaying content. Channels that do not support multi-message streaming keep the default of 0 (nothing confirmed).

Source

fn send_draft<'life0, 'life1, 'async_trait>( &'life0 self, _message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<Option<String>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Send an initial draft message. Returns a platform-specific message ID for later edits.

Source

fn update_draft<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Update a previously sent draft message with new accumulated content.

Source

fn update_draft_progress<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Show a progress/status update (e.g. tool execution status).

Source

fn update_draft_progress_batch<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, recipient: &'life1 str, message_id: &'life2 str, texts: &'life3 [String], ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Show a batch of progress/status updates as one coalesced draft update.

Channels with expensive edit APIs should override this so an upstream burst does not turn into one awaited network edit per progress item.

Source

fn update_typed_draft_progress_batch<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, recipient: &'life1 str, message_id: &'life2 str, progress: &'life3 [DraftProgress], ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Show a batch of progress updates without discarding their source kind.

The default preserves compatibility for channels that only need text; channels that coalesce semantically different progress should override this method and keep the kind through their local draft buffer.

Source

fn update_draft_lifecycle<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _event: ProgressEvent, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Show localized lifecycle progress produced from a typed runtime event. Unlike raw tool status text, this input is trusted channel chrome.

Source

fn flush_draft_turn<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Flush the current agent text turn as an outbound message in multi-message streaming mode. Called when an LLM turn completes (e.g. before tool execution). Default: no-op.

Source

fn discard_draft_turn<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Discard a completed agent text turn WITHOUT sending it, marking that narration consumed for this draft. Called when the outbound hook cancels a narration flush: the cancelled turn must never be re-offered (no resurrection), but narration produced by a later turn must still be deliverable. Advancing the channel’s delivered-prefix bookkeeping over the cancelled text achieves both. Default: no-op.

Source

fn finalize_draft<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, _text: &'life3 str, _suppress_voice: bool, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Finalize a draft with the complete response (e.g. apply Markdown formatting). suppress_voice forces text delivery even on voice-only peers.

If finalization must chunk a long answer and a later chunk fails after an earlier one was accepted, return FinalizePartialDelivery rather than a generic error: it tells the caller the accepted prefix is already posted so it must not resend the whole answer (which would duplicate the delivered chunks). A generic Err still means nothing was committed and a full-message fallback is safe.

Source

fn cancel_draft<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Cancel and remove a previously sent draft message if the channel supports it.

Source

fn add_reaction<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, _emoji: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Add a reaction (emoji) to a message.

Source

fn remove_reaction<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, _emoji: &'life3 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Remove a reaction (emoji) from a message previously added by this bot.

Source

fn pin_message<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Pin a message in the channel.

Source

fn unpin_message<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Unpin a previously pinned message.

Source

fn redact_message<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _channel_id: &'life1 str, _message_id: &'life2 str, _reason: Option<String>, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Redact (delete) a message from the channel.

Source

fn create_room<'life0, 'life1, 'async_trait>( &'life0 self, _options: &'life1 RoomCreationOptions, ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Create a new platform room/conversation when the channel supports it.

Source

fn send_poll<'life0, 'life1, 'async_trait>( &'life0 self, _poll: &'life1 PollRequest, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Post a native poll when the channel supports it.

Source

fn invite_user<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _room_id: &'life1 str, _user_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Invite a user to an existing platform room/conversation.

Source

fn room_context(&self, _room_id: &str) -> Option<ChannelRoomContext>

Context this channel supplies about room_id, when the adapter can provide it and the operator has opted the alias in.

Returns None by default: a channel that does not implement this, or a room the operator has not opted in, contributes nothing to the prompt. Absence is the safe state, so the default is the safe one.

Source

fn request_approval<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _request: &'life2 ChannelApprovalRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<ChannelApprovalResponse>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Request interactive tool-call approval from the channel operator.

Returns Ok(Some(response)) when the operator answers within the channel’s configured approval_timeout_secs; timeouts surface as Deny. Returns Ok(None) only for channels that do not implement the prompt at all — the caller falls back to its default policy (typically auto-deny).

Source

fn request_approval_attributed<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, recipient: &'life1 str, request: &'life2 ChannelApprovalRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<AttributedApprovalResponse>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Like Channel::request_approval, but also reports WHO produced the decision: which back-channel answered (decided_by) and whether a person answered at all (ApprovalSource).

Override this whenever the channel can synthesize a response. The Channel::request_approval contract collapses “the operator denied” and “nobody answered in time” into the same Some(Deny), so the default below cannot tell them apart and assumes ApprovalSource::Operator. Any implementation that manufactures a Deny on timeout, on a dropped responder, or on an unreachable approver MUST implement this method and report the real source — otherwise the runtime tells the model a human refused when none was ever asked.

The convention in-tree is to put the real body here and let Channel::request_approval delegate to it, so the logic lives once.

Source

fn request_approval_attributed_with_timeout<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, recipient: &'life1 str, request: &'life2 ChannelApprovalRequest, timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<AttributedApprovalResponse>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Request an attributed approval using a caller-owned response budget.

The default keeps the existing caller-side timeout semantics. Channels with a claim-aware deadline handoff may override this so their pending decision and any bounded resolution grace share one deadline owner.

Source

fn send_gate_prompt<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _recipient: &'life1 str, _prompt: &'life2 ChannelGatePrompt, ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Present a long-lived, out-of-band gate prompt (e.g. a parked SOP approval) on this channel, rendered natively — an embed with one button per choice on Discord, an inline keyboard on Telegram, and so on.

UNLIKE Channel::request_approval this does NOT wait for the answer: the prompt outlives the call (and daemon restarts). The choice comes back through the channel’s normal INBOUND path — a component click (stamped with an internal sop.gate: marker by the channel’s own interaction producer, never derivable from message text) or a plain <choice> <reference> text reply — which the orchestrator resolves against the parked gate.

Default Ok(false) = “no native prompt on this channel”; the caller then falls back to a plain text notice carrying the reply instructions.

Source

fn finalize_gate_prompt<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _reference: &'life1 str, _outcome: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Mark a previously sent gate prompt as resolved: strip its interactive controls and replace the body with outcome (e.g. “Approved by @user — run resumed”), so a decided gate cannot be clicked again and the decision is visible in place. reference is the same correlation key the prompt was sent with. Best-effort: Ok(false) when this channel has nothing to finalize (no native prompt, or the mapping was lost to a restart) — the gate state itself is never affected.

Source

fn request_choice<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _question: &'life1 str, _choices: &'life2 [String], _timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<String>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Ask the user a multiple-choice question and return the chosen option’s text.

Returns Ok(Some(answer)) if the channel handled the question natively (e.g. ACP elicitation/create with a single-select enum schema, or the legacy session/request_permission fallback for older ACP clients; Telegram inline keyboard; etc.). Returns Ok(None) to signal the caller should fall back to the generic send + listen flow. Default impl returns None.

Free-form (no-choices) questions are not modeled by this method. Multiple-choice support landed via ACP elicitation/create (see the ACP elicitation RFD: https://agentclientprotocol.com/rfds/elicitation); free-form text is tracked under that spec’s Phase 2.

Source

fn request_multi_choice<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _question: &'life1 str, _choices: &'life2 [String], _min_items: usize, _max_items: usize, _timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<String>>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Source

fn supports_free_form_ask(&self) -> bool

Whether this channel can answer free-form (no-choices) ask_user questions via the standard send + listen flow. Channels that only handle structured choices return false so callers fail fast with a useful error instead of timing out on listen.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementations on Foreign Types§

Source§

impl Channel for RpcApprovalChannel

Source§

fn supports_outbound_send(&self) -> bool

send above is a deliberate no-op: the Code tab renders agent output from the RPC turn stream, not from generic channel sends. Declaring it here lets surfaces that must genuinely deliver (poll’s text fallback, escalate_to_human) refuse this channel instead of reporting success for a message the user never saw.

Source§

fn supports_free_form_ask(&self) -> bool

Free-form text elicitation is Phase 2 of the elicitation rollout — the same answer as AcpChannel. Until that lands, tools like ask_user (with no choices) and escalate_to_human (with wait_for_response) fail fast on the Code tab.

Source§

fn name(&self) -> &str

Source§

fn send<'life0, 'life1, 'async_trait>( &'life0 self, _message: &'life1 SendMessage, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, RpcApprovalChannel: 'async_trait,

Source§

fn listen<'life0, 'async_trait>( &'life0 self, _tx: Sender<ChannelMessage>, ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, RpcApprovalChannel: 'async_trait,

Source§

fn request_approval<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, recipient: &'life1 str, request: &'life2 ChannelApprovalRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<ChannelApprovalResponse>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, RpcApprovalChannel: 'async_trait,

Source§

fn request_approval_attributed<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, recipient: &'life1 str, request: &'life2 ChannelApprovalRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<AttributedApprovalResponse>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, RpcApprovalChannel: 'async_trait,

Source§

fn request_choice<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, question: &'life1 str, choices: &'life2 [String], timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<String>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, RpcApprovalChannel: 'async_trait,

Source§

fn request_multi_choice<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, question: &'life1 str, choices: &'life2 [String], min_items: usize, max_items: usize, timeout: Duration, ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<String>>, Error>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, RpcApprovalChannel: 'async_trait,

Implementors§