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 /// The run reads untrusted data (an automation, ADR 0013): the bot's own tools only read
65 /// (no memory, task, note or knowledge writes).
66 pub read_only: bool,
67}
68
69/// Knowledge placed in the system prompt.
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
71pub struct KnowledgeContext {
72 /// The bot's persona documents.
73 pub persona: String,
74 /// The bundle catalog, as visible to the requester.
75 pub catalog: String,
76 /// The requester's profile, if one exists.
77 pub user_profile: Option<String>,
78}
79
80/// Access to knowledge tools for one agent run. Dropping it revokes the access.
81pub struct RunGrant {
82 /// MCP servers to give the agent.
83 pub mcp: Vec<McpServer>,
84 /// Environment variables carrying the run token.
85 pub env: Vec<(String, Secret<String>)>,
86 _revoke_on_drop: Box<dyn Send + Sync>,
87}
88
89impl RunGrant {
90 /// Creates a grant; `guard` is dropped (and should revoke the token) when the grant is dropped.
91 pub fn new(
92 mcp: Vec<McpServer>,
93 env: Vec<(String, Secret<String>)>,
94 guard: impl Send + Sync + 'static,
95 ) -> Self {
96 Self {
97 mcp,
98 env,
99 _revoke_on_drop: Box::new(guard),
100 }
101 }
102
103 /// A grant without tools, for backends that cannot use MCP.
104 pub fn none() -> Self {
105 Self::new(Vec::new(), Vec::new(), ())
106 }
107}
108
109impl fmt::Debug for RunGrant {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.debug_struct("RunGrant")
112 .field("mcp", &self.mcp)
113 .field("env", &self.env)
114 .finish_non_exhaustive()
115 }
116}
117
118/// The bot's knowledge and memory (design ยง5).
119#[async_trait]
120pub trait KnowledgeProvider: Send + Sync {
121 /// Persona, catalog and profile for the system prompt.
122 async fn context(&self, who: &Requester) -> Result<KnowledgeContext, KnowledgeError>;
123
124 /// Issues tool access for one run, scoped to `who`.
125 async fn grant(&self, who: &Requester, run: &RunInfo<'_>) -> Result<RunGrant, KnowledgeError>;
126
127 /// Text search for backends without MCP: the most relevant excerpts for `query`, or an empty
128 /// string when nothing matches.
129 async fn excerpts(&self, who: &Requester, query: &str) -> Result<String, KnowledgeError>;
130}