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}