1use crate::connections::{Connection, ConnectionStatus, ConnectionToken, NewConnection};
2use crate::{ScratchpadEntry, ToolAuthStore, ToolResponse};
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize, de::DeserializeOwned};
7use serde_json::Value;
8use std::{collections::HashMap, sync::Arc};
9use tokio::sync::oneshot;
10use utoipa::ToSchema;
11use uuid::Uuid;
12
13use crate::{
14 AgentEvent, CreateThreadRequest, Message, Task, TaskMessage, TaskStatus, Thread,
15 UpdateThreadRequest,
16};
17
18#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
22pub struct ThreadListFilter {
23 pub agent_id: Option<String>,
25 pub external_id: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub attributes: Option<serde_json::Value>,
30 pub search: Option<String>,
32 pub from_date: Option<DateTime<Utc>>,
34 pub to_date: Option<DateTime<Utc>>,
36 pub tags: Option<Vec<String>>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
42pub struct ThreadListResponse {
43 pub threads: Vec<crate::ThreadSummary>,
44 pub total: i64,
45 pub page: u32,
46 pub page_size: u32,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
51pub struct AgentUsageInfo {
52 pub agent_id: String,
53 pub agent_name: String,
54 pub thread_count: i64,
55}
56
57#[derive(Clone)]
59pub struct InitializedStores {
60 pub session_store: Arc<dyn SessionStore>,
61 pub agent_store: Arc<dyn AgentStore>,
62 pub task_store: Arc<dyn TaskStore>,
63 pub thread_store: Arc<dyn ThreadStore>,
64 pub tool_auth_store: Arc<dyn ToolAuthStore>,
65 pub scratchpad_store: Arc<dyn ScratchpadStore>,
66 pub memory_store: Option<Arc<dyn MemoryStore>>,
67 pub crawl_store: Option<Arc<dyn CrawlStore>>,
68 pub external_tool_calls_store: Arc<dyn ExternalToolCallsStore>,
69 pub prompt_template_store: Option<Arc<dyn PromptTemplateStore>>,
70 pub secret_store: Option<Arc<dyn SecretStore>>,
71 pub skill_store: Option<Arc<dyn SkillStore>>,
72 pub connection_store: Option<Arc<dyn ConnectionStore>>,
73 pub connection_token_store: Option<Arc<dyn ConnectionTokenStore>>,
74 pub provider_registry: Option<Arc<dyn crate::auth::ProviderRegistry>>,
75}
76impl InitializedStores {
77 pub fn set_tool_auth_store(&mut self, tool_auth_store: Arc<dyn ToolAuthStore>) {
78 self.tool_auth_store = tool_auth_store;
79 }
80
81 pub fn set_external_tool_calls_store(mut self, store: Arc<dyn ExternalToolCallsStore>) {
82 self.external_tool_calls_store = store;
83 }
84
85 pub fn set_session_store(&mut self, session_store: Arc<dyn SessionStore>) {
86 self.session_store = session_store;
87 }
88
89 pub fn set_agent_store(&mut self, agent_store: Arc<dyn AgentStore>) {
90 self.agent_store = agent_store;
91 }
92
93 pub fn with_task_store(&mut self, task_store: Arc<dyn TaskStore>) {
94 self.task_store = task_store;
95 }
96
97 pub fn with_thread_store(&mut self, thread_store: Arc<dyn ThreadStore>) {
98 self.thread_store = thread_store;
99 }
100
101 pub fn with_scratchpad_store(&mut self, scratchpad_store: Arc<dyn ScratchpadStore>) {
102 self.scratchpad_store = scratchpad_store;
103 }
104}
105
106impl std::fmt::Debug for InitializedStores {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.debug_struct("InitializedStores").finish()
109 }
110}
111
112#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, JsonSchema)]
113pub struct SessionSummary {
114 pub session_id: String,
115 pub keys: Vec<String>,
116 pub key_count: usize,
117 pub updated_at: Option<DateTime<Utc>>,
118}
119
120#[async_trait::async_trait]
122pub trait SessionStore: Send + Sync + std::fmt::Debug {
123 async fn clear_session(&self, namespace: &str) -> anyhow::Result<()>;
124
125 async fn set_value(&self, namespace: &str, key: &str, value: &Value) -> anyhow::Result<()>;
126
127 async fn set_value_with_expiry(
128 &self,
129 namespace: &str,
130 key: &str,
131 value: &Value,
132 expiry: Option<chrono::DateTime<chrono::Utc>>,
133 ) -> anyhow::Result<()>;
134
135 async fn get_value(&self, namespace: &str, key: &str) -> anyhow::Result<Option<Value>>;
136
137 async fn delete_value(&self, namespace: &str, key: &str) -> anyhow::Result<()>;
138
139 async fn get_all_values(&self, namespace: &str) -> anyhow::Result<HashMap<String, Value>>;
140
141 async fn list_sessions(
142 &self,
143 namespace: Option<&str>,
144 limit: Option<usize>,
145 offset: Option<usize>,
146 ) -> anyhow::Result<Vec<SessionSummary>>;
147}
148#[async_trait::async_trait]
149pub trait SessionStoreExt: SessionStore {
150 async fn set<T: Serialize + Sync>(
151 &self,
152 namespace: &str,
153 key: &str,
154 value: &T,
155 ) -> anyhow::Result<()> {
156 self.set_value(namespace, key, &serde_json::to_value(value)?)
157 .await
158 }
159 async fn set_with_expiry<T: Serialize + Sync>(
160 &self,
161 namespace: &str,
162 key: &str,
163 value: &T,
164 expiry: Option<chrono::DateTime<chrono::Utc>>,
165 ) -> anyhow::Result<()> {
166 self.set_value_with_expiry(namespace, key, &serde_json::to_value(value)?, expiry)
167 .await
168 }
169 async fn get<T: DeserializeOwned + Sync>(
170 &self,
171 namespace: &str,
172 key: &str,
173 ) -> anyhow::Result<Option<T>> {
174 match self.get_value(namespace, key).await? {
175 Some(b) => Ok(Some(serde_json::from_value(b)?)),
176 None => Ok(None),
177 }
178 }
179}
180impl<T: SessionStore + ?Sized> SessionStoreExt for T {}
181
182#[async_trait::async_trait]
184pub trait MemoryStore: Send + Sync {
185 async fn store_memory(
187 &self,
188 user_id: &str,
189 session_memory: SessionMemory,
190 ) -> anyhow::Result<()>;
191
192 async fn search_memories(
194 &self,
195 user_id: &str,
196 query: &str,
197 limit: Option<usize>,
198 ) -> anyhow::Result<Vec<String>>;
199
200 async fn get_user_memories(&self, user_id: &str) -> anyhow::Result<Vec<String>>;
202
203 async fn clear_user_memories(&self, user_id: &str) -> anyhow::Result<()>;
205}
206
207#[derive(Debug, Clone)]
208pub struct SessionMemory {
209 pub agent_id: String,
210 pub thread_id: String,
211 pub session_summary: String,
212 pub key_insights: Vec<String>,
213 pub important_facts: Vec<String>,
214 pub timestamp: chrono::DateTime<chrono::Utc>,
215}
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, JsonSchema)]
217#[serde(tag = "type", rename_all = "snake_case")]
218pub enum FilterMessageType {
219 Events,
220 Messages,
221 Artifacts,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
225pub struct MessageFilter {
226 pub filter: Option<Vec<FilterMessageType>>,
227 pub limit: Option<usize>,
228 pub offset: Option<usize>,
229}
230
231#[async_trait]
233pub trait TaskStore: Send + Sync {
234 fn init_task(
235 &self,
236 context_id: &str,
237 task_id: Option<&str>,
238 status: Option<TaskStatus>,
239 ) -> Task {
240 let task_id = task_id.unwrap_or(&Uuid::new_v4().to_string()).to_string();
241 Task {
242 id: task_id,
243 status: status.unwrap_or(TaskStatus::Pending),
244 created_at: chrono::Utc::now().timestamp_millis(),
245 updated_at: chrono::Utc::now().timestamp_millis(),
246 thread_id: context_id.to_string(),
247 parent_task_id: None,
248 }
249 }
250 async fn get_or_create_task(
251 &self,
252 thread_id: &str,
253 task_id: &str,
254 ) -> Result<(), anyhow::Error> {
255 match self.get_task(task_id).await? {
256 Some(task) => task,
257 None => {
258 self.create_task(thread_id, Some(task_id), Some(TaskStatus::Running))
259 .await?
260 }
261 };
262
263 Ok(())
264 }
265 async fn create_task(
266 &self,
267 context_id: &str,
268 task_id: Option<&str>,
269 task_status: Option<TaskStatus>,
270 ) -> anyhow::Result<Task>;
271 async fn get_task(&self, task_id: &str) -> anyhow::Result<Option<Task>>;
272 async fn update_task_status(&self, task_id: &str, status: TaskStatus) -> anyhow::Result<()>;
273 async fn add_event_to_task(&self, task_id: &str, event: AgentEvent) -> anyhow::Result<()>;
274 async fn add_message_to_task(&self, task_id: &str, message: &Message) -> anyhow::Result<()>;
275 async fn cancel_task(&self, task_id: &str) -> anyhow::Result<Task>;
276 async fn list_tasks(&self, thread_id: Option<&str>) -> anyhow::Result<Vec<Task>>;
277
278 async fn get_history(
279 &self,
280 thread_id: &str,
281 filter: Option<MessageFilter>,
282 ) -> anyhow::Result<Vec<(Task, Vec<TaskMessage>)>>;
283
284 async fn update_parent_task(
285 &self,
286 task_id: &str,
287 parent_task_id: Option<&str>,
288 ) -> anyhow::Result<()>;
289}
290
291#[async_trait]
293pub trait ThreadStore: Send + Sync {
294 fn as_any(&self) -> &dyn std::any::Any;
295 async fn create_thread(&self, request: CreateThreadRequest) -> anyhow::Result<Thread>;
296 async fn get_thread(&self, thread_id: &str) -> anyhow::Result<Option<Thread>>;
297 async fn update_thread(
298 &self,
299 thread_id: &str,
300 request: UpdateThreadRequest,
301 ) -> anyhow::Result<Thread>;
302 async fn delete_thread(&self, thread_id: &str) -> anyhow::Result<()>;
303
304 async fn list_threads(
307 &self,
308 filter: &ThreadListFilter,
309 limit: Option<u32>,
310 offset: Option<u32>,
311 ) -> anyhow::Result<ThreadListResponse>;
312
313 async fn update_thread_with_message(
314 &self,
315 thread_id: &str,
316 message: &str,
317 ) -> anyhow::Result<()>;
318
319 async fn get_home_stats(&self) -> anyhow::Result<HomeStats>;
321
322 async fn get_agents_by_usage(
326 &self,
327 search: Option<&str>,
328 ) -> anyhow::Result<Vec<AgentUsageInfo>>;
329
330 async fn get_agent_stats_map(
332 &self,
333 ) -> anyhow::Result<std::collections::HashMap<String, AgentStatsInfo>>;
334
335 async fn mark_message_read(
339 &self,
340 thread_id: &str,
341 message_id: &str,
342 ) -> anyhow::Result<MessageReadStatus>;
343
344 async fn get_message_read_status(
346 &self,
347 thread_id: &str,
348 message_id: &str,
349 ) -> anyhow::Result<Option<MessageReadStatus>>;
350
351 async fn get_thread_read_status(
353 &self,
354 thread_id: &str,
355 ) -> anyhow::Result<Vec<MessageReadStatus>>;
356
357 async fn vote_message(&self, request: VoteMessageRequest) -> anyhow::Result<MessageVote>;
362
363 async fn remove_vote(&self, thread_id: &str, message_id: &str) -> anyhow::Result<()>;
365
366 async fn get_user_vote(
368 &self,
369 thread_id: &str,
370 message_id: &str,
371 ) -> anyhow::Result<Option<MessageVote>>;
372
373 async fn get_message_vote_summary(
375 &self,
376 thread_id: &str,
377 message_id: &str,
378 ) -> anyhow::Result<MessageVoteSummary>;
379
380 async fn get_message_votes(
382 &self,
383 thread_id: &str,
384 message_id: &str,
385 ) -> anyhow::Result<Vec<MessageVote>>;
386}
387
388#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
390pub struct HomeStats {
391 pub total_agents: i64,
392 pub total_threads: i64,
393 pub total_messages: i64,
394 pub avg_run_time_ms: Option<f64>,
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub total_owned_agents: Option<i64>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub total_accessible_agents: Option<i64>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub most_active_agent: Option<MostActiveAgent>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub latest_threads: Option<Vec<LatestThreadInfo>>,
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub recently_used_agents: Option<Vec<RecentlyUsedAgent>>,
407 #[serde(skip_serializing_if = "Option::is_none")]
410 pub custom_metrics: Option<std::collections::HashMap<String, CustomMetric>>,
411}
412
413#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
415pub struct CustomMetric {
416 pub label: String,
418 pub value: String,
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub helper: Option<String>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub limit: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub raw_value: Option<i64>,
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub raw_limit: Option<i64>,
432}
433
434#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
435pub struct MostActiveAgent {
436 pub id: String,
437 pub name: String,
438 pub thread_count: i64,
439}
440
441#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
443pub struct RecentlyUsedAgent {
444 pub id: String,
445 pub name: String,
446 pub description: Option<String>,
447 pub last_used_at: chrono::DateTime<chrono::Utc>,
448}
449
450#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
451pub struct LatestThreadInfo {
452 pub id: String,
453 pub title: String,
454 pub agent_id: String,
455 pub agent_name: String,
456 pub updated_at: chrono::DateTime<chrono::Utc>,
457}
458
459#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
461pub struct AgentStatsInfo {
462 pub thread_count: i64,
463 pub sub_agent_usage_count: i64,
464 pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
465}
466
467#[async_trait]
468pub trait AgentStore: Send + Sync {
469 async fn list(
470 &self,
471 cursor: Option<String>,
472 limit: Option<usize>,
473 ) -> (Vec<crate::configuration::AgentConfig>, Option<String>);
474
475 async fn get(&self, name: &str) -> Option<crate::configuration::AgentConfig>;
476 async fn register(&self, config: crate::configuration::AgentConfig) -> anyhow::Result<()>;
477 async fn update(&self, config: crate::configuration::AgentConfig) -> anyhow::Result<()>;
479
480 async fn clear(&self) -> anyhow::Result<()>;
481
482 async fn delete(&self, id: &str) -> anyhow::Result<()>;
484
485 async fn get_with_cloud_metadata(
488 &self,
489 name: &str,
490 ) -> Option<(
491 crate::configuration::AgentConfig,
492 crate::configuration::AgentCloudMetadata,
493 )> {
494 self.get(name)
495 .await
496 .map(|c| (c, crate::configuration::AgentCloudMetadata::default()))
497 }
498
499 async fn list_with_cloud_metadata(
502 &self,
503 cursor: Option<String>,
504 limit: Option<usize>,
505 ) -> (
506 Vec<(
507 crate::configuration::AgentConfig,
508 crate::configuration::AgentCloudMetadata,
509 )>,
510 Option<String>,
511 ) {
512 let (configs, cursor) = self.list(cursor, limit).await;
513 (
514 configs
515 .into_iter()
516 .map(|c| (c, crate::configuration::AgentCloudMetadata::default()))
517 .collect(),
518 cursor,
519 )
520 }
521}
522
523#[async_trait::async_trait]
525pub trait ScratchpadStore: Send + Sync + std::fmt::Debug {
526 async fn add_entry(
528 &self,
529 thread_id: &str,
530 entry: ScratchpadEntry,
531 ) -> Result<(), crate::AgentError>;
532
533 async fn clear_entries(&self, thread_id: &str) -> Result<(), crate::AgentError>;
535
536 async fn get_entries(
538 &self,
539 thread_id: &str,
540 task_id: &str,
541 limit: Option<usize>,
542 ) -> Result<Vec<ScratchpadEntry>, crate::AgentError>;
543
544 async fn get_all_entries(
545 &self,
546 thread_id: &str,
547 limit: Option<usize>,
548 ) -> Result<Vec<ScratchpadEntry>, crate::AgentError>;
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
553pub struct CrawlResult {
554 pub id: String,
555 pub url: String,
556 pub title: Option<String>,
557 pub content: String,
558 pub html: Option<String>,
559 pub metadata: serde_json::Value,
560 pub links: Vec<String>,
561 pub images: Vec<String>,
562 pub status_code: Option<u16>,
563 pub crawled_at: chrono::DateTime<chrono::Utc>,
564 pub processing_time_ms: Option<u64>,
565}
566
567#[async_trait]
569pub trait CrawlStore: Send + Sync {
570 async fn store_crawl_result(&self, result: CrawlResult) -> anyhow::Result<String>;
572
573 async fn get_crawl_result(&self, id: &str) -> anyhow::Result<Option<CrawlResult>>;
575
576 async fn get_crawl_results_by_url(&self, url: &str) -> anyhow::Result<Vec<CrawlResult>>;
578
579 async fn get_recent_crawl_results(
581 &self,
582 limit: Option<usize>,
583 since: Option<chrono::DateTime<chrono::Utc>>,
584 ) -> anyhow::Result<Vec<CrawlResult>>;
585
586 async fn is_url_recently_crawled(
588 &self,
589 url: &str,
590 cache_duration: chrono::Duration,
591 ) -> anyhow::Result<Option<CrawlResult>>;
592
593 async fn delete_crawl_result(&self, id: &str) -> anyhow::Result<()>;
595
596 async fn cleanup_old_results(
598 &self,
599 before: chrono::DateTime<chrono::Utc>,
600 ) -> anyhow::Result<usize>;
601}
602
603#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema, JsonSchema)]
607#[serde(rename_all = "lowercase")]
608pub enum VoteType {
609 Upvote,
610 Downvote,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
615pub struct MessageReadStatus {
616 pub thread_id: String,
617 pub message_id: String,
618 pub user_id: String,
619 pub read_at: chrono::DateTime<chrono::Utc>,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
624pub struct MarkMessageReadRequest {
625 pub thread_id: String,
626 pub message_id: String,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
631pub struct MessageVote {
632 pub id: String,
633 pub thread_id: String,
634 pub message_id: String,
635 pub user_id: String,
636 pub vote_type: VoteType,
637 pub comment: Option<String>,
639 pub created_at: chrono::DateTime<chrono::Utc>,
640 pub updated_at: chrono::DateTime<chrono::Utc>,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
645#[schema(example = json!({"vote_type": "up"}))]
646pub struct VoteMessageRequest {
647 pub thread_id: String,
648 pub message_id: String,
649 pub vote_type: VoteType,
650 pub comment: Option<String>,
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
656pub struct MessageVoteSummary {
657 pub message_id: String,
658 pub upvotes: i64,
659 pub downvotes: i64,
660 pub user_vote: Option<VoteType>,
662}
663
664#[async_trait]
666pub trait ExternalToolCallsStore: Send + Sync + std::fmt::Debug {
667 async fn register_external_tool_call(
669 &self,
670 session_id: &str,
671 ) -> anyhow::Result<oneshot::Receiver<ToolResponse>>;
672
673 async fn complete_external_tool_call(
675 &self,
676 session_id: &str,
677 tool_response: ToolResponse,
678 ) -> anyhow::Result<()>;
679
680 async fn remove_tool_call(&self, session_id: &str) -> anyhow::Result<()>;
682
683 async fn list_pending_tool_calls(&self) -> anyhow::Result<Vec<String>>;
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
690pub struct PromptTemplateRecord {
691 pub id: String,
692 pub name: String,
693 pub template: String,
694 pub description: Option<String>,
695 pub version: Option<String>,
696 pub is_system: bool,
697 pub created_at: chrono::DateTime<chrono::Utc>,
698 pub updated_at: chrono::DateTime<chrono::Utc>,
699}
700
701#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
702#[schema(example = json!({"name": "greeting", "content": "Hello {{name}}, welcome to {{service}}!", "description": "A greeting template"}))]
703pub struct NewPromptTemplate {
704 pub name: String,
705 pub template: String,
706 pub description: Option<String>,
707 pub version: Option<String>,
708 #[serde(default)]
709 pub is_system: bool,
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
713pub struct UpdatePromptTemplate {
714 pub name: String,
715 pub template: String,
716 pub description: Option<String>,
717}
718
719#[async_trait]
720pub trait PromptTemplateStore: Send + Sync {
721 async fn list(&self) -> anyhow::Result<Vec<PromptTemplateRecord>>;
722 async fn get(&self, id: &str) -> anyhow::Result<Option<PromptTemplateRecord>>;
723 async fn get_by_names(&self, names: &[String]) -> anyhow::Result<Vec<PromptTemplateRecord>>;
725 async fn create(&self, template: NewPromptTemplate) -> anyhow::Result<PromptTemplateRecord>;
726 async fn update(
727 &self,
728 id: &str,
729 update: UpdatePromptTemplate,
730 ) -> anyhow::Result<PromptTemplateRecord>;
731 async fn delete(&self, id: &str) -> anyhow::Result<()>;
732 async fn clone_template(&self, id: &str) -> anyhow::Result<PromptTemplateRecord>;
733 async fn sync_system_templates(&self, templates: Vec<NewPromptTemplate>) -> anyhow::Result<()>;
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
739pub struct SecretRecord {
740 pub id: String,
741 pub key: String,
742 pub value: String,
743 pub created_at: chrono::DateTime<chrono::Utc>,
744 pub updated_at: chrono::DateTime<chrono::Utc>,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
748#[schema(example = json!({"key": "OPENAI_API_KEY", "value": "sk-..."}))]
749pub struct NewSecret {
750 pub key: String,
751 pub value: String,
752}
753
754#[async_trait]
755pub trait SecretStore: Send + Sync {
756 async fn list(&self) -> anyhow::Result<Vec<SecretRecord>>;
757 async fn get(&self, key: &str) -> anyhow::Result<Option<SecretRecord>>;
758 async fn create(&self, secret: NewSecret) -> anyhow::Result<SecretRecord>;
759 async fn update(&self, key: &str, value: &str) -> anyhow::Result<SecretRecord>;
760 async fn delete(&self, key: &str) -> anyhow::Result<()>;
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
766pub struct CustomProviderConfig {
767 pub id: String,
768 pub name: String,
769 pub base_url: String,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub project_id: Option<String>,
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
775pub struct CustomModelEntry {
776 pub provider: String,
777 pub model: String,
778 #[serde(default = "default_completion")]
780 pub capability: String,
781}
782
783fn default_completion() -> String {
784 "completion".to_string()
785}
786
787#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
789pub struct ConnectionProviderConfig {
790 pub id: String,
792 pub name: String,
794 pub authorization_url: String,
796 pub token_url: String,
798 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub refresh_url: Option<String>,
801 #[serde(default)]
803 pub scopes_supported: Vec<String>,
804 #[serde(default)]
806 pub default_scopes: Vec<String>,
807 #[serde(default)]
809 pub scope_mappings: std::collections::HashMap<String, String>,
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
814pub struct UpsertProviderRequest {
815 pub provider_id: String,
816 #[serde(default)]
817 pub secrets: std::collections::HashMap<String, String>,
818 #[serde(default)]
819 pub config: Option<CustomProviderConfig>,
820 #[serde(default)]
821 pub custom_models: Option<Vec<CustomModelEntry>>,
822 #[serde(default)]
824 pub default_model: Option<String>,
825 #[serde(default)]
827 pub connection_provider: Option<ConnectionProviderConfig>,
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
832pub struct UpsertProviderResponse {
833 pub provider_id: String,
834 pub secrets_saved: usize,
835 pub config_saved: bool,
836}
837
838#[async_trait]
839pub trait ProviderStore: Send + Sync {
840 async fn upsert_provider(
841 &self,
842 req: UpsertProviderRequest,
843 ) -> anyhow::Result<UpsertProviderResponse>;
844
845 async fn delete_provider(&self, provider_id: &str) -> anyhow::Result<()>;
846
847 async fn get_default_model(&self) -> anyhow::Result<Option<String>>;
848}
849
850#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, ToSchema, JsonSchema)]
855#[serde(rename_all = "lowercase")]
856pub enum ContextExecutionType {
857 #[default]
860 Inline,
861 Fork,
865}
866
867impl std::fmt::Display for ContextExecutionType {
868 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
869 match self {
870 ContextExecutionType::Inline => write!(f, "inline"),
871 ContextExecutionType::Fork => write!(f, "fork"),
872 }
873 }
874}
875
876impl std::str::FromStr for ContextExecutionType {
877 type Err = ();
878 fn from_str(s: &str) -> Result<Self, Self::Err> {
879 match s {
880 "fork" => Ok(ContextExecutionType::Fork),
881 _ => Ok(ContextExecutionType::Inline),
882 }
883 }
884}
885
886pub const SKILL_LISTING_BUDGET: usize = 2_000;
888pub const SKILL_DESCRIPTION_CAP: usize = 250;
890pub const DEFAULT_SKILL_MAX_TOKENS: u32 = 8000;
892
893#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
895pub struct SkillFrontmatter {
896 pub name: String,
897 #[serde(default)]
898 pub description: Option<String>,
899 #[serde(default)]
900 pub tags: Vec<String>,
901 #[serde(default)]
902 pub model: Option<String>,
903 #[serde(default)]
904 pub max_tokens: Option<u32>,
905 #[serde(default)]
906 pub can_spawn_tasks: bool,
907 #[serde(default)]
908 pub paths: Vec<String>,
909 #[serde(default)]
910 pub is_public: bool,
911}
912
913impl SkillFrontmatter {
914 pub fn effective_max_tokens(&self) -> u32 {
915 self.max_tokens.unwrap_or(DEFAULT_SKILL_MAX_TOKENS)
916 }
917
918 pub fn as_listing_line(&self) -> String {
919 let desc = self.description.as_deref().unwrap_or("No description");
920 let desc_truncated = if desc.len() > SKILL_DESCRIPTION_CAP {
921 format!("{}...", &desc[..SKILL_DESCRIPTION_CAP.min(desc.len())])
922 } else {
923 desc.to_string()
924 };
925 let mut meta = Vec::new();
926 if let Some(model) = &self.model {
927 meta.push(format!("model: {}", model));
928 }
929 if self.can_spawn_tasks {
930 meta.push("tasks: yes".to_string());
931 }
932 if meta.is_empty() {
933 format!("- {}: {}", self.name, desc_truncated)
934 } else {
935 format!("- {}: {} ({})", self.name, desc_truncated, meta.join(", "))
936 }
937 }
938}
939
940pub fn format_skill_listing(skills: &[SkillFrontmatter], budget_tokens: usize) -> String {
942 let budget_chars = budget_tokens * 4;
943 let mut result = String::new();
944 let mut remaining_chars = budget_chars;
945 for skill in skills {
946 let line = format!("{}\n", skill.as_listing_line());
947 if line.len() > remaining_chars {
948 let name_line = format!("- {}\n", skill.name);
949 if name_line.len() <= remaining_chars {
950 result.push_str(&name_line);
951 remaining_chars -= name_line.len();
952 } else {
953 break;
954 }
955 } else {
956 result.push_str(&line);
957 remaining_chars -= line.len();
958 }
959 }
960 result.trim_end().to_string()
961}
962
963#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
965pub struct SkillsListResponse {
966 pub skills: Vec<SkillListItem>,
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
972pub struct SkillListItem {
973 pub id: String,
974 #[serde(default)]
975 pub workspace_slug: String,
976 pub name: String,
977 #[serde(default)]
978 pub full_name: String,
979 #[serde(default)]
980 pub description: Option<String>,
981 #[serde(default)]
982 pub tags: Vec<String>,
983 #[serde(default)]
984 pub is_public: bool,
985 #[serde(default)]
986 pub is_system: bool,
987 #[serde(default)]
988 pub is_owner: bool,
989 #[serde(default)]
991 pub is_workspace: bool,
992 #[serde(default)]
993 pub star_count: i32,
994 #[serde(default)]
995 pub clone_count: i32,
996 #[serde(default)]
997 pub is_starred: bool,
998 pub created_at: chrono::DateTime<chrono::Utc>,
999 pub updated_at: chrono::DateTime<chrono::Utc>,
1000}
1001
1002#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
1003pub struct SkillRecord {
1004 pub id: String,
1005 #[serde(default)]
1007 pub workspace_slug: String,
1008 pub name: String,
1009 #[serde(default)]
1011 pub full_name: String,
1012 pub description: Option<String>,
1013 pub content: String,
1014 pub tags: Vec<String>,
1015 pub is_public: bool,
1016 pub is_system: bool,
1017 #[serde(default)]
1019 pub is_owner: bool,
1020 #[serde(default)]
1022 pub is_workspace: bool,
1023 pub star_count: i32,
1024 pub clone_count: i32,
1025 #[serde(default)]
1027 pub is_starred: bool,
1028 pub created_at: chrono::DateTime<chrono::Utc>,
1029 pub updated_at: chrono::DateTime<chrono::Utc>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1032 pub model: Option<String>,
1033 #[serde(default)]
1035 pub context: ContextExecutionType,
1036}
1037
1038#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
1039#[schema(example = json!({"name": "my-skill", "content": "# My Skill\nA helpful utility skill", "description": "A utility skill", "tags": ["utility"], "is_public": false}))]
1040pub struct NewSkill {
1041 pub name: String,
1042 pub description: Option<String>,
1043 pub content: String,
1044 #[serde(default)]
1045 pub tags: Vec<String>,
1046 #[serde(default)]
1047 pub is_public: bool,
1048 #[serde(default, skip_serializing_if = "Option::is_none")]
1049 pub model: Option<String>,
1050 #[serde(default)]
1051 pub context: ContextExecutionType,
1052}
1053
1054#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
1055pub struct UpdateSkill {
1056 pub name: Option<String>,
1057 pub description: Option<String>,
1058 pub content: Option<String>,
1059 pub tags: Option<Vec<String>>,
1060 pub is_public: Option<bool>,
1061 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub model: Option<String>,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub context: Option<ContextExecutionType>,
1065}
1066
1067#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1069#[serde(rename_all = "snake_case")]
1070pub enum SkillScope {
1071 #[default]
1073 Workspace,
1074 Starred,
1076 System,
1078 Discover,
1080 All,
1082}
1083
1084#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1086pub struct SkillFilter {
1087 #[serde(default)]
1089 pub scope: SkillScope,
1090 #[serde(default)]
1092 pub search: Option<String>,
1093 #[serde(default = "default_page")]
1095 pub page: i64,
1096 #[serde(default = "default_per_page")]
1098 pub per_page: i64,
1099}
1100
1101fn default_page() -> i64 {
1102 1
1103}
1104fn default_per_page() -> i64 {
1105 50
1106}
1107
1108#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
1110pub struct SkillListResponse {
1111 pub skills: Vec<SkillListItem>,
1112 pub total: i64,
1113 pub page: i64,
1114 pub per_page: i64,
1115 pub total_pages: i64,
1116}
1117
1118#[async_trait]
1119pub trait SkillStore: Send + Sync {
1120 async fn list(&self, filter: SkillFilter) -> anyhow::Result<SkillListResponse>;
1122 async fn get(&self, id: &str) -> anyhow::Result<Option<SkillRecord>>;
1123 async fn create(&self, skill: NewSkill) -> anyhow::Result<SkillRecord>;
1124 async fn update(&self, id: &str, update: UpdateSkill) -> anyhow::Result<SkillRecord>;
1125 async fn delete(&self, id: &str) -> anyhow::Result<()>;
1126 async fn star(&self, skill_id: &str) -> anyhow::Result<()>;
1127 async fn unstar(&self, skill_id: &str) -> anyhow::Result<()>;
1128 async fn clone_skill(&self, skill_id: &str) -> anyhow::Result<SkillRecord>;
1129}
1130
1131#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
1135pub struct UsageSnapshot {
1136 pub day_tokens: i64,
1137 pub week_tokens: i64,
1138 pub month_tokens: i64,
1139}
1140
1141#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
1143pub struct UsageLimits {
1144 pub daily_tokens: Option<i64>,
1145 pub weekly_tokens: Option<i64>,
1146 pub monthly_tokens: Option<i64>,
1147}
1148
1149#[derive(Debug, Clone)]
1151pub enum UsageCheckResult {
1152 Allowed,
1153 Denied { reason: String },
1154}
1155
1156#[async_trait]
1161pub trait UsageService: Send + Sync {
1162 async fn check_request(
1167 &self,
1168 workspace_id: &str,
1169 user_id: &str,
1170 is_llm: bool,
1171 auth_source: &str,
1172 ) -> UsageCheckResult;
1173
1174 async fn record_usage(
1176 &self,
1177 workspace_id: &str,
1178 user_id: &str,
1179 tokens_used: i64,
1180 ) -> anyhow::Result<()>;
1181
1182 async fn get_usage(&self, workspace_id: &str, user_id: &str) -> anyhow::Result<UsageSnapshot>;
1184
1185 async fn get_limits(&self, workspace_id: &str) -> anyhow::Result<UsageLimits>;
1187}
1188
1189#[derive(Debug, Clone)]
1192pub struct NoOpUsageService;
1193
1194#[async_trait]
1195impl UsageService for NoOpUsageService {
1196 async fn check_request(
1197 &self,
1198 _workspace_id: &str,
1199 _user_id: &str,
1200 _is_llm: bool,
1201 _auth_source: &str,
1202 ) -> UsageCheckResult {
1203 UsageCheckResult::Allowed
1204 }
1205
1206 async fn record_usage(
1207 &self,
1208 _workspace_id: &str,
1209 _user_id: &str,
1210 _tokens_used: i64,
1211 ) -> anyhow::Result<()> {
1212 Ok(())
1213 }
1214
1215 async fn get_usage(
1216 &self,
1217 _workspace_id: &str,
1218 _user_id: &str,
1219 ) -> anyhow::Result<UsageSnapshot> {
1220 Ok(UsageSnapshot::default())
1221 }
1222
1223 async fn get_limits(&self, _workspace_id: &str) -> anyhow::Result<UsageLimits> {
1224 Ok(UsageLimits::default())
1225 }
1226}
1227
1228#[async_trait]
1232pub trait ConnectionStore: Send + Sync + 'static {
1233 async fn create(&self, connection: NewConnection) -> anyhow::Result<Connection>;
1234 async fn get_by_id(&self, id: &str) -> anyhow::Result<Option<Connection>>;
1235 async fn list_by_workspace(&self, workspace_id: &str) -> anyhow::Result<Vec<Connection>>;
1236 async fn update_status(&self, id: &str, status: ConnectionStatus) -> anyhow::Result<()>;
1237 async fn update_skill_id(&self, id: &str, skill_id: uuid::Uuid) -> anyhow::Result<()>;
1238 async fn delete(&self, id: &str) -> anyhow::Result<()>;
1239 async fn get_by_provider(
1240 &self,
1241 workspace_id: &str,
1242 provider: &str,
1243 ) -> anyhow::Result<Option<Connection>>;
1244}
1245
1246#[async_trait]
1248pub trait ConnectionTokenStore: Send + Sync + 'static {
1249 async fn store_token(&self, connection_id: &str, token: ConnectionToken) -> anyhow::Result<()>;
1250 async fn get_token(&self, connection_id: &str) -> anyhow::Result<Option<ConnectionToken>>;
1251 async fn remove_token(&self, connection_id: &str) -> anyhow::Result<()>;
1252
1253 async fn refresh_token(
1260 &self,
1261 _connection_id: &str,
1262 _connection: &Connection,
1263 ) -> anyhow::Result<Option<ConnectionToken>> {
1264 Ok(None)
1265 }
1266
1267 async fn store_oauth_state(
1268 &self,
1269 state_key: &str,
1270 state: serde_json::Value,
1271 ) -> anyhow::Result<()>;
1272 async fn get_oauth_state(&self, state_key: &str) -> anyhow::Result<Option<serde_json::Value>>;
1273 async fn remove_oauth_state(&self, state_key: &str) -> anyhow::Result<()>;
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279
1280 #[test]
1281 fn test_skills_list_response_deserialize_cloud_format() {
1282 let json = r#"{"skills":[{"id":"abc","workspace_slug":"ws","name":"test","full_name":"ws/test","description":"desc","tags":["t"],"is_public":true,"is_system":false,"is_owner":true,"star_count":0,"clone_count":0,"is_starred":false,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z"}]}"#;
1283 let resp: SkillsListResponse = serde_json::from_str(json).unwrap();
1284 assert_eq!(resp.skills.len(), 1);
1285 assert_eq!(resp.skills[0].name, "test");
1286 assert_eq!(resp.skills[0].workspace_slug, "ws");
1287 assert_eq!(resp.skills[0].full_name, "ws/test");
1288 assert!(resp.skills[0].is_public);
1289 }
1290
1291 #[test]
1292 fn test_skills_list_response_deserialize_defaults() {
1293 let json = r#"{"skills":[{"id":"abc","name":"test","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z"}]}"#;
1294 let resp: SkillsListResponse = serde_json::from_str(json).unwrap();
1295 assert_eq!(resp.skills[0].workspace_slug, "");
1296 assert_eq!(resp.skills[0].full_name, "");
1297 assert!(!resp.skills[0].is_public);
1298 assert!(!resp.skills[0].is_owner);
1299 }
1300
1301 #[test]
1302 fn test_skills_list_response_roundtrip() {
1303 let resp = SkillsListResponse {
1304 skills: vec![SkillListItem {
1305 id: "id1".into(),
1306 workspace_slug: "local".into(),
1307 name: "my_skill".into(),
1308 full_name: "local/my_skill".into(),
1309 description: Some("A skill".into()),
1310 tags: vec!["tag1".into()],
1311 is_public: false,
1312 is_system: false,
1313 is_owner: true,
1314 is_workspace: true,
1315 star_count: 5,
1316 clone_count: 2,
1317 is_starred: true,
1318 created_at: chrono::Utc::now(),
1319 updated_at: chrono::Utc::now(),
1320 }],
1321 };
1322 let json = serde_json::to_string(&resp).unwrap();
1323 let decoded: SkillsListResponse = serde_json::from_str(&json).unwrap();
1324 assert_eq!(decoded.skills[0].name, "my_skill");
1325 assert_eq!(decoded.skills[0].star_count, 5);
1326 }
1327}