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}