1use std::time::Duration;
4
5use jiff::Timestamp;
6use serde::{Deserialize, Serialize};
7
8use crate::agent::TokenUsage;
9use crate::caps::ToolPolicy;
10use crate::id::{ChannelId, ConversationKey, MessageId, RunId, UserId};
11use crate::peer::PeerTaskRecord;
12use crate::{BoxError, async_trait};
13
14#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum StoreError {
18 #[error("the database schema version {found} is newer than this okbot supports ({supported})")]
20 TooNew {
21 found: u32,
23 supported: u32,
25 },
26 #[error("storage error: {0}")]
28 Backend(#[source] BoxError),
29}
30
31pub const PAIRING_MAX_FAILURES: u32 = 5;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case")]
37pub enum MessageRole {
38 User,
40 Bot,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct NewMessage {
47 pub conversation: ConversationKey,
49 pub role: MessageRole,
51 pub author_id: Option<UserId>,
53 pub author_name: String,
55 pub text: String,
57 pub message_id: Option<MessageId>,
59 pub at: Timestamp,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct StoredMessage {
66 pub role: MessageRole,
68 pub author_name: String,
70 pub text: String,
72 pub at: Timestamp,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct AgentRunRecord {
79 pub run_id: RunId,
81 pub conversation: ConversationKey,
83 pub backend: String,
85 pub model: Option<String>,
87 pub tools: ToolPolicy,
89 pub usage: Option<TokenUsage>,
91 pub duration: Duration,
93 pub error: Option<String>,
95 pub started_at: Timestamp,
97 pub person: Option<String>,
100}
101
102pub fn person_key(channel: &ChannelId, user: &UserId, person: Option<&str>) -> String {
106 match person {
107 Some(id) => format!("person:{id}"),
108 None => format!("{channel}/{user}"),
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
114pub struct Usage {
115 pub turns: u64,
117 pub tokens: u64,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123#[non_exhaustive]
124pub enum ToolDecision {
125 Allowed,
127 Denied,
129 Declined,
131}
132
133impl ToolDecision {
134 pub fn as_str(self) -> &'static str {
136 match self {
137 Self::Allowed => "allowed",
138 Self::Denied => "denied",
139 Self::Declined => "declined",
140 }
141 }
142
143 pub fn parse(value: &str) -> Option<Self> {
145 match value {
146 "allowed" => Some(Self::Allowed),
147 "denied" => Some(Self::Denied),
148 "declined" => Some(Self::Declined),
149 _ => None,
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct ToolCallRecord {
158 pub run_id: Option<RunId>,
160 pub command: Option<String>,
162 pub person: String,
164 pub conversation: ConversationKey,
166 pub server: String,
168 pub tool: String,
170 pub decision: ToolDecision,
172 pub error: Option<String>,
174 pub duration: Duration,
176 pub at: Timestamp,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct RoleGrant {
183 pub role: String,
185 pub account: String,
187 pub granted_by: String,
189 pub granted_at: Timestamp,
191 pub expires_at: Timestamp,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197pub enum FireState {
198 Sending,
200 Sent,
202 Failed,
204 Missed,
206}
207
208impl FireState {
209 pub fn as_str(self) -> &'static str {
211 match self {
212 Self::Sending => "sending",
213 Self::Sent => "sent",
214 Self::Failed => "failed",
215 Self::Missed => "missed",
216 }
217 }
218
219 pub fn parse(value: &str) -> Option<Self> {
221 match value {
222 "sending" => Some(Self::Sending),
223 "sent" => Some(Self::Sent),
224 "failed" => Some(Self::Failed),
225 "missed" => Some(Self::Missed),
226 _ => None,
227 }
228 }
229
230 pub fn is_final(self) -> bool {
232 !matches!(self, Self::Sending)
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct Fire {
239 pub task_id: String,
241 pub fire_at: Timestamp,
243 pub state: FireState,
245 pub attempts: u32,
247 pub updated_at: Timestamp,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct SealedEntry {
254 pub kind: String,
256 pub server: String,
258 pub subject: String,
260 pub updated_at: Timestamp,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct AutomationRun {
267 pub automation: String,
269 pub at: Timestamp,
271 pub items: u32,
273 pub action: String,
275 pub error: Option<String>,
277}
278
279fn unsupported() -> StoreError {
280 StoreError::Backend("not supported by this store".into())
281}
282
283#[async_trait]
285pub trait ConversationStore: Send + Sync {
286 async fn mark_seen(
288 &self,
289 channel: &ChannelId,
290 event_id: &str,
291 ttl: Duration,
292 ) -> Result<bool, StoreError>;
293
294 async fn append(&self, message: NewMessage) -> Result<(), StoreError>;
296
297 async fn recent(
299 &self,
300 conversation: &ConversationKey,
301 limit: usize,
302 ) -> Result<Vec<StoredMessage>, StoreError>;
303
304 async fn reset(&self, conversation: &ConversationKey) -> Result<(), StoreError>;
307
308 async fn session(
310 &self,
311 conversation: &ConversationKey,
312 ) -> Result<Option<(String, String)>, StoreError>;
313
314 async fn set_session(
316 &self,
317 conversation: &ConversationKey,
318 backend: &str,
319 session: &str,
320 ) -> Result<(), StoreError>;
321
322 async fn record_run(&self, run: AgentRunRecord) -> Result<(), StoreError>;
324
325 async fn owners(&self) -> Result<Vec<(ChannelId, UserId)>, StoreError>;
327
328 async fn remove_owner(&self, channel: &ChannelId, user: &UserId) -> Result<bool, StoreError>;
330
331 async fn create_pairing_code(&self, code: &str, ttl: Duration) -> Result<(), StoreError>;
333
334 async fn redeem_pairing_code(
340 &self,
341 code: &str,
342 channel: &ChannelId,
343 user: &UserId,
344 ) -> Result<bool, StoreError>;
345
346 async fn claim_fire(&self, task_id: &str, fire_at: Timestamp) -> Result<Fire, StoreError>;
349
350 async fn set_fire(
352 &self,
353 task_id: &str,
354 fire_at: Timestamp,
355 state: FireState,
356 attempts: u32,
357 ) -> Result<(), StoreError>;
358
359 async fn recent_fires(&self, limit: usize) -> Result<Vec<Fire>, StoreError>;
361
362 async fn set_direct_chat(
364 &self,
365 channel: &ChannelId,
366 user: &UserId,
367 conversation: &ConversationKey,
368 ) -> Result<(), StoreError>;
369
370 async fn direct_chat(
372 &self,
373 channel: &ChannelId,
374 user: &UserId,
375 ) -> Result<Option<ConversationKey>, StoreError>;
376
377 async fn usage_since(&self, person: &str, since: Timestamp) -> Result<Usage, StoreError>;
379
380 async fn record_tool_call(&self, call: ToolCallRecord) -> Result<(), StoreError>;
382
383 async fn tool_calls(
385 &self,
386 since: Timestamp,
387 limit: usize,
388 ) -> Result<Vec<ToolCallRecord>, StoreError>;
389
390 async fn grant_role(&self, grant: RoleGrant) -> Result<(), StoreError>;
393
394 async fn revoke_role(&self, role: &str, account: &str) -> Result<bool, StoreError>;
396
397 async fn role_grants(&self, now: Timestamp) -> Result<Vec<RoleGrant>, StoreError>;
399
400 async fn purge_before(&self, before: Timestamp) -> Result<u64, StoreError>;
403
404 async fn sealed(
406 &self,
407 kind: &str,
408 server: &str,
409 subject: &str,
410 ) -> Result<Option<Vec<u8>>, StoreError> {
411 let _ = (kind, server, subject);
412 Err(unsupported())
413 }
414
415 async fn set_sealed(
417 &self,
418 kind: &str,
419 server: &str,
420 subject: &str,
421 sealed: Vec<u8>,
422 ) -> Result<(), StoreError> {
423 let _ = (kind, server, subject, sealed);
424 Err(unsupported())
425 }
426
427 async fn delete_sealed(
429 &self,
430 kind: &str,
431 server: &str,
432 subject: &str,
433 ) -> Result<bool, StoreError> {
434 let _ = (kind, server, subject);
435 Err(unsupported())
436 }
437
438 async fn sealed_entries(&self, kind: &str) -> Result<Vec<SealedEntry>, StoreError> {
440 let _ = kind;
441 Err(unsupported())
442 }
443
444 async fn claim_items(
447 &self,
448 automation: &str,
449 items: Vec<String>,
450 ) -> Result<Vec<String>, StoreError> {
451 let _ = (automation, items);
452 Err(unsupported())
453 }
454
455 async fn release_items(&self, automation: &str, items: Vec<String>) -> Result<(), StoreError> {
457 let _ = (automation, items);
458 Err(unsupported())
459 }
460
461 async fn record_automation_run(&self, run: AutomationRun) -> Result<(), StoreError> {
463 let _ = run;
464 Err(unsupported())
465 }
466
467 async fn automation_runs(
469 &self,
470 automation: Option<&str>,
471 since: Timestamp,
472 limit: usize,
473 ) -> Result<Vec<AutomationRun>, StoreError> {
474 let _ = (automation, since, limit);
475 Err(unsupported())
476 }
477
478 async fn put_peer_task(&self, task: PeerTaskRecord) -> Result<(), StoreError> {
480 let _ = task;
481 Err(unsupported())
482 }
483
484 async fn peer_task(
486 &self,
487 inbound: bool,
488 id: &str,
489 ) -> Result<Option<PeerTaskRecord>, StoreError> {
490 let _ = (inbound, id);
491 Err(unsupported())
492 }
493
494 async fn peer_task_by_message(
496 &self,
497 inbound: bool,
498 team: &str,
499 peer: &str,
500 message_id: &str,
501 ) -> Result<Option<PeerTaskRecord>, StoreError> {
502 let _ = (inbound, team, peer, message_id);
503 Err(unsupported())
504 }
505
506 async fn peer_tasks(&self, limit: usize) -> Result<Vec<PeerTaskRecord>, StoreError> {
508 let _ = limit;
509 Err(unsupported())
510 }
511
512 async fn interrupt_peer_tasks(&self, now: Timestamp) -> Result<u64, StoreError> {
515 let _ = now;
516 Err(unsupported())
517 }
518}