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    /// What interactive replies can use (ADR 0015).
54    #[serde(default)]
55    pub ui: UiCaps,
56}
57
58/// The parts of a [`Prompt`](crate::Prompt) a channel shows natively. Whatever is missing is
59/// shown as text (cards, a numbered menu, a list of fields to type).
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
61#[serde(default)]
62pub struct UiCaps {
63    /// Buttons, including link buttons.
64    pub buttons: bool,
65    /// A menu of options ([`Select`](crate::Select)).
66    pub select: bool,
67    /// Forms behind buttons.
68    pub forms: bool,
69    /// Cards with a title, shown apart from the text.
70    pub cards: bool,
71    /// A card's chart as a picture ([`Card::image`](crate::Card::image), ADR 0016).
72    pub images: bool,
73}
74
75impl UiCaps {
76    /// Everything (Discord, Slack).
77    pub fn all() -> Self {
78        Self {
79            buttons: true,
80            select: true,
81            forms: true,
82            cards: true,
83            images: true,
84        }
85    }
86}
87
88impl ChannelCaps {
89    /// Conservative defaults: plain CommonMark, 2000 chars, nothing optional.
90    pub fn basic() -> Self {
91        Self {
92            max_message_len: 2000,
93            length_unit: LengthUnit::Chars,
94            dialect: MarkdownDialect::CommonMark,
95            threads: false,
96            edit: false,
97            typing: false,
98            ui: UiCaps::default(),
99        }
100    }
101}
102
103/// Which tools an agent may use in a room (design §4.4). No policy grants a shell or file access.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
105#[serde(rename_all = "kebab-case")]
106#[non_exhaustive]
107pub enum ToolPolicy {
108    /// okbot's MCP tools plus the agent CLI's built-in web search and fetch. The default.
109    #[default]
110    Web,
111    /// okbot's MCP tools only.
112    McpOnly,
113}
114
115/// How the bot answers (ADR 0015).
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
117#[serde(rename_all = "kebab-case")]
118#[non_exhaustive]
119pub enum ReplyMode {
120    /// Chat text only. The default.
121    #[default]
122    Text,
123    /// Text, plus cards, buttons, menus and forms the agent shows with its `show` tool, natively
124    /// where the platform has them and as text elsewhere.
125    Interactive,
126}
127
128/// What an agent backend can do natively. Missing capabilities are emulated or the backend is skipped.
129#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
130pub struct AgentCaps {
131    /// Accepts a separate system prompt (otherwise it is prepended to the prompt).
132    pub system_prompt: bool,
133    /// Accepts MCP servers per run.
134    pub mcp: bool,
135    /// Tool policies the backend can enforce. A run is never started with an unenforceable policy.
136    pub tool_policies: Vec<ToolPolicy>,
137    /// Can resume a native session.
138    pub resume: bool,
139    /// Can stream partial output.
140    pub stream: bool,
141}
142
143impl AgentCaps {
144    /// Whether `policy` can be enforced.
145    pub fn enforces(&self, policy: ToolPolicy) -> bool {
146        self.tool_policies.contains(&policy)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn tool_policy_names() {
156        assert_eq!(
157            serde_json::to_string(&ToolPolicy::McpOnly).unwrap(),
158            "\"mcp-only\""
159        );
160        assert_eq!(ToolPolicy::default(), ToolPolicy::Web);
161    }
162}