Skip to main content

okbot_core/
caps.rs

1//! Capabilities of channels and agents, and the tool policy.
2
3use serde::{Deserialize, Serialize};
4
5/// The Markdown flavour a platform displays.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "kebab-case")]
8#[non_exhaustive]
9pub enum MarkdownDialect {
10    /// CommonMark, shown as-is (console).
11    CommonMark,
12    /// Zalo Bot `parse_mode = "markdown"`: no links, code blocks or tables.
13    ZaloMd,
14    /// Google Chat text formatting.
15    GChatText,
16    /// Discord Markdown.
17    DiscordMd,
18    /// Slack mrkdwn.
19    SlackMrkdwn,
20    /// Telegram `parse_mode = "HTML"`: `<b>`, `<i>`, `<s>`, `<code>`, `<pre>`, `<a>`,
21    /// `<blockquote>`; no tables or headings.
22    TelegramHtml,
23    /// No formatting at all.
24    Plain,
25}
26
27/// How a platform counts message length.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "kebab-case")]
30#[non_exhaustive]
31pub enum LengthUnit {
32    /// Unicode scalar values (`char`s).
33    Chars,
34    /// UTF-16 code units (JavaScript/Java string length).
35    Utf16,
36}
37
38/// What a channel can do. The runtime adapts its behaviour to these values.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct ChannelCaps {
41    /// Longest message the platform accepts, in [`ChannelCaps::length_unit`].
42    pub max_message_len: usize,
43    /// How the platform counts length.
44    pub length_unit: LengthUnit,
45    /// Formatting dialect.
46    pub dialect: MarkdownDialect,
47    /// Whether replies can go into threads.
48    pub threads: bool,
49    /// Whether sent messages can be edited (enables "thinking…" and progressive replies).
50    pub edit: bool,
51    /// Whether a typing indicator exists.
52    pub typing: bool,
53}
54
55impl ChannelCaps {
56    /// Conservative defaults: plain CommonMark, 2000 chars, nothing optional.
57    pub fn basic() -> Self {
58        Self {
59            max_message_len: 2000,
60            length_unit: LengthUnit::Chars,
61            dialect: MarkdownDialect::CommonMark,
62            threads: false,
63            edit: false,
64            typing: false,
65        }
66    }
67}
68
69/// Which tools an agent may use in a room (design §4.4). No policy grants a shell or file access.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
71#[serde(rename_all = "kebab-case")]
72#[non_exhaustive]
73pub enum ToolPolicy {
74    /// okbot's MCP tools plus the agent CLI's built-in web search and fetch. The default.
75    #[default]
76    Web,
77    /// okbot's MCP tools only.
78    McpOnly,
79}
80
81/// What an agent backend can do natively. Missing capabilities are emulated or the backend is skipped.
82#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
83pub struct AgentCaps {
84    /// Accepts a separate system prompt (otherwise it is prepended to the prompt).
85    pub system_prompt: bool,
86    /// Accepts MCP servers per run.
87    pub mcp: bool,
88    /// Tool policies the backend can enforce. A run is never started with an unenforceable policy.
89    pub tool_policies: Vec<ToolPolicy>,
90    /// Can resume a native session.
91    pub resume: bool,
92    /// Can stream partial output.
93    pub stream: bool,
94}
95
96impl AgentCaps {
97    /// Whether `policy` can be enforced.
98    pub fn enforces(&self, policy: ToolPolicy) -> bool {
99        self.tool_policies.contains(&policy)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn tool_policy_names() {
109        assert_eq!(
110            serde_json::to_string(&ToolPolicy::McpOnly).unwrap(),
111            "\"mcp-only\""
112        );
113        assert_eq!(ToolPolicy::default(), ToolPolicy::Web);
114    }
115}