Skip to main content

heartbit_core/channel/
mod.rs

1//! Channel base traits and in-process implementations.
2//!
3//! Platform-specific adapters (Telegram, Discord, Slack) and the
4//! Postgres-backed session store live in the heartbit umbrella crate.
5
6#![allow(missing_docs)]
7pub mod bridge;
8pub mod session;
9pub mod types;
10
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14
15use crate::agent::events::OnEvent;
16use crate::error::Error;
17use crate::llm::{OnApproval, OnText};
18use crate::memory::Memory;
19use crate::tool::builtins::OnQuestion;
20
21/// A media attachment from a messaging channel (photo, voice, document).
22pub struct MediaAttachment {
23    pub media_type: String,
24    pub data: Vec<u8>,
25    pub caption: Option<String>,
26}
27
28/// Trait for channel-specific bridges that produce agent callbacks.
29///
30/// Each messaging channel (Telegram, Discord, etc.) implements this trait
31/// so the same `RunTask` closure can drive any channel without duplication.
32pub trait ChannelBridge: Send + Sync {
33    fn make_on_text(self: Arc<Self>) -> Arc<OnText>;
34    fn make_on_event(self: Arc<Self>) -> Arc<OnEvent>;
35    fn make_on_approval(self: Arc<Self>) -> Arc<OnApproval>;
36    fn make_on_question(self: Arc<Self>) -> Arc<OnQuestion>;
37}
38
39/// Input for the `RunTask` callback.
40pub struct RunTaskInput {
41    pub task_text: String,
42    pub bridge: Arc<dyn ChannelBridge>,
43    /// Pre-existing shared memory store so sub-agent memory tools persist
44    /// across tasks. Passed as the raw (un-namespaced) store.
45    pub memory: Option<Arc<dyn Memory>>,
46    /// User-specific namespace prefix (e.g. `"tg:12345"`). Passed as `story_id`
47    /// to `build_orchestrator_from_config` for per-user memory isolation.
48    pub user_namespace: Option<String>,
49    /// Media attachments (photos, documents). Empty for text-only messages.
50    pub attachments: Vec<MediaAttachment>,
51}
52
53/// Callback type for running an agent task with bridge callbacks.
54///
55/// The CLI crate provides this closure to wire `build_orchestrator_from_config`
56/// with the channel bridge callbacks. Returns the agent's final text output.
57pub type RunTask = dyn Fn(RunTaskInput) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>>
58    + Send
59    + Sync;
60
61/// Callback type for memory consolidation on idle sessions.
62pub type ConsolidateSession =
63    dyn Fn(i64) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> + Send + Sync;
64
65/// Split a message into chunks that fit a platform's message-length limit.
66///
67/// Tries to split at newlines for readability; falls back to char boundaries.
68/// Shared by Discord, Slack, and other channel adapters.
69pub fn chunk_message(text: &str, max_len: usize) -> Vec<&str> {
70    if text.len() <= max_len {
71        return vec![text];
72    }
73    let mut chunks = Vec::new();
74    let mut remaining = text;
75    while !remaining.is_empty() {
76        if remaining.len() <= max_len {
77            chunks.push(remaining);
78            break;
79        }
80        // Try to split at a newline
81        let split_at = remaining[..max_len].rfind('\n').unwrap_or_else(|| {
82            // Fall back to char boundary
83            let mut pos = max_len;
84            while pos > 0 && !remaining.is_char_boundary(pos) {
85                pos -= 1;
86            }
87            pos
88        });
89        let split_at = if split_at == 0 {
90            max_len.min(remaining.len())
91        } else {
92            split_at
93        };
94        chunks.push(&remaining[..split_at]);
95        remaining = &remaining[split_at..];
96        // Skip leading newline after split
97        if remaining.starts_with('\n') {
98            remaining = &remaining[1..];
99        }
100    }
101    chunks
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn channel_bridge_is_object_safe() {
110        // Compile-time check: ChannelBridge can be used as a trait object.
111        fn _assert(_: &Arc<dyn ChannelBridge>) {}
112    }
113
114    #[test]
115    fn run_task_input_accepts_dyn_bridge() {
116        // Compile-time check: RunTaskInput.bridge is Arc<dyn ChannelBridge>.
117        fn _assert(bridge: Arc<dyn ChannelBridge>) {
118            let _input = RunTaskInput {
119                task_text: String::new(),
120                bridge,
121                memory: None,
122                user_namespace: None,
123                attachments: Vec::new(),
124            };
125        }
126    }
127}