Skip to main content

okbot_core/
knowledge.rs

1//! The knowledge port.
2
3use std::fmt;
4use std::time::Duration;
5
6use crate::agent::McpServer;
7use crate::caps::ToolPolicy;
8use crate::id::{ChannelId, ConversationKey, UserId, UserRef};
9use crate::message::Attachment;
10use crate::secret::Secret;
11use crate::{BoxError, async_trait};
12
13/// A knowledge failure.
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum KnowledgeError {
17    /// The bundle could not be opened or indexed.
18    #[error("knowledge bundle: {0}")]
19    Bundle(#[source] BoxError),
20    /// Any other failure.
21    #[error("knowledge error: {0}")]
22    Other(#[source] BoxError),
23}
24
25/// Who is asking, which decides what knowledge and memory is visible.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Requester {
28    /// The conversation.
29    pub conversation: ConversationKey,
30    /// The user.
31    pub user: UserRef,
32    /// The room may see documents marked `audience: private`.
33    pub internal: bool,
34    /// The person this account belongs to, when the operator linked several accounts
35    /// (`[people.<id>]`): they share one profile and personal memory.
36    pub person: Option<Person>,
37    /// The requester's personal data (profile, memory, tasks, notes) is reachable: a one-to-one
38    /// chat, a room the operator marked personal, or the terminal. In other group chats everyone
39    /// reads the answer, so only the room's and shared data are.
40    pub personal: bool,
41}
42
43/// Several platform accounts of one human, linked by the operator.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Person {
46    /// Id from `okbot.toml` (`luan`): a lowercase slug.
47    pub id: String,
48    /// Every linked account, as `(channel, user)`.
49    pub accounts: Vec<(ChannelId, UserId)>,
50}
51
52/// What a run needs from the knowledge tools.
53#[derive(Debug, Clone, Copy)]
54pub struct RunInfo<'a> {
55    /// The run's tool policy.
56    pub tools: ToolPolicy,
57    /// How long the access stays valid.
58    pub ttl: Duration,
59    /// Media the user sent with the messages being answered (offered through a viewing tool).
60    pub attachments: &'a [Attachment],
61    /// The requester administers the bot (the owner of a personal bot, or `[bot] admins`): only
62    /// they may create routines and save documents to the shared knowledge.
63    pub admin: bool,
64}
65
66/// Knowledge placed in the system prompt.
67#[derive(Debug, Clone, PartialEq, Eq, Default)]
68pub struct KnowledgeContext {
69    /// The bot's persona documents.
70    pub persona: String,
71    /// The bundle catalog, as visible to the requester.
72    pub catalog: String,
73    /// The requester's profile, if one exists.
74    pub user_profile: Option<String>,
75}
76
77/// Access to knowledge tools for one agent run. Dropping it revokes the access.
78pub struct RunGrant {
79    /// MCP servers to give the agent.
80    pub mcp: Vec<McpServer>,
81    /// Environment variables carrying the run token.
82    pub env: Vec<(String, Secret<String>)>,
83    _revoke_on_drop: Box<dyn Send + Sync>,
84}
85
86impl RunGrant {
87    /// Creates a grant; `guard` is dropped (and should revoke the token) when the grant is dropped.
88    pub fn new(
89        mcp: Vec<McpServer>,
90        env: Vec<(String, Secret<String>)>,
91        guard: impl Send + Sync + 'static,
92    ) -> Self {
93        Self {
94            mcp,
95            env,
96            _revoke_on_drop: Box::new(guard),
97        }
98    }
99
100    /// A grant without tools, for backends that cannot use MCP.
101    pub fn none() -> Self {
102        Self::new(Vec::new(), Vec::new(), ())
103    }
104}
105
106impl fmt::Debug for RunGrant {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("RunGrant")
109            .field("mcp", &self.mcp)
110            .field("env", &self.env)
111            .finish_non_exhaustive()
112    }
113}
114
115/// The bot's knowledge and memory (design ยง5).
116#[async_trait]
117pub trait KnowledgeProvider: Send + Sync {
118    /// Persona, catalog and profile for the system prompt.
119    async fn context(&self, who: &Requester) -> Result<KnowledgeContext, KnowledgeError>;
120
121    /// Issues tool access for one run, scoped to `who`.
122    async fn grant(&self, who: &Requester, run: &RunInfo<'_>) -> Result<RunGrant, KnowledgeError>;
123
124    /// Text search for backends without MCP: the most relevant excerpts for `query`, or an empty
125    /// string when nothing matches.
126    async fn excerpts(&self, who: &Requester, query: &str) -> Result<String, KnowledgeError>;
127}