use crate::connections::{Connection, ConnectionStatus, ConnectionToken, NewConnection};
use crate::{ScratchpadEntry, ToolAuthStore, ToolResponse};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::oneshot;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::{
AgentEvent, CreateThreadRequest, Message, Task, TaskMessage, TaskStatus, Thread,
UpdateThreadRequest,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct ThreadListFilter {
pub agent_id: Option<String>,
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<serde_json::Value>,
pub search: Option<String>,
pub from_date: Option<DateTime<Utc>>,
pub to_date: Option<DateTime<Utc>>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct ThreadListResponse {
pub threads: Vec<crate::ThreadSummary>,
pub total: i64,
pub page: u32,
pub page_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct AgentUsageInfo {
pub agent_id: String,
pub agent_name: String,
pub thread_count: i64,
}
#[derive(Clone)]
pub struct InitializedStores {
pub session_store: Arc<dyn SessionStore>,
pub agent_store: Arc<dyn AgentStore>,
pub task_store: Arc<dyn TaskStore>,
pub thread_store: Arc<dyn ThreadStore>,
pub tool_auth_store: Arc<dyn ToolAuthStore>,
pub scratchpad_store: Arc<dyn ScratchpadStore>,
pub memory_store: Option<Arc<dyn MemoryStore>>,
pub crawl_store: Option<Arc<dyn CrawlStore>>,
pub external_tool_calls_store: Arc<dyn ExternalToolCallsStore>,
pub prompt_template_store: Option<Arc<dyn PromptTemplateStore>>,
pub secret_store: Option<Arc<dyn SecretStore>>,
pub skill_store: Option<Arc<dyn SkillStore>>,
pub connection_store: Option<Arc<dyn ConnectionStore>>,
pub connection_token_store: Option<Arc<dyn ConnectionTokenStore>>,
pub provider_registry: Option<Arc<dyn crate::auth::ProviderRegistry>>,
}
impl InitializedStores {
pub fn set_tool_auth_store(&mut self, tool_auth_store: Arc<dyn ToolAuthStore>) {
self.tool_auth_store = tool_auth_store;
}
pub fn set_external_tool_calls_store(mut self, store: Arc<dyn ExternalToolCallsStore>) {
self.external_tool_calls_store = store;
}
pub fn set_session_store(&mut self, session_store: Arc<dyn SessionStore>) {
self.session_store = session_store;
}
pub fn set_agent_store(&mut self, agent_store: Arc<dyn AgentStore>) {
self.agent_store = agent_store;
}
pub fn with_task_store(&mut self, task_store: Arc<dyn TaskStore>) {
self.task_store = task_store;
}
pub fn with_thread_store(&mut self, thread_store: Arc<dyn ThreadStore>) {
self.thread_store = thread_store;
}
pub fn with_scratchpad_store(&mut self, scratchpad_store: Arc<dyn ScratchpadStore>) {
self.scratchpad_store = scratchpad_store;
}
}
impl std::fmt::Debug for InitializedStores {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InitializedStores").finish()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, JsonSchema)]
pub struct SessionSummary {
pub session_id: String,
pub keys: Vec<String>,
pub key_count: usize,
pub updated_at: Option<DateTime<Utc>>,
}
#[async_trait::async_trait]
pub trait SessionStore: Send + Sync + std::fmt::Debug {
async fn clear_session(&self, namespace: &str) -> anyhow::Result<()>;
async fn set_value(&self, namespace: &str, key: &str, value: &Value) -> anyhow::Result<()>;
async fn set_value_with_expiry(
&self,
namespace: &str,
key: &str,
value: &Value,
expiry: Option<chrono::DateTime<chrono::Utc>>,
) -> anyhow::Result<()>;
async fn get_value(&self, namespace: &str, key: &str) -> anyhow::Result<Option<Value>>;
async fn delete_value(&self, namespace: &str, key: &str) -> anyhow::Result<()>;
async fn get_all_values(&self, namespace: &str) -> anyhow::Result<HashMap<String, Value>>;
async fn list_sessions(
&self,
namespace: Option<&str>,
limit: Option<usize>,
offset: Option<usize>,
) -> anyhow::Result<Vec<SessionSummary>>;
}
#[async_trait::async_trait]
pub trait SessionStoreExt: SessionStore {
async fn set<T: Serialize + Sync>(
&self,
namespace: &str,
key: &str,
value: &T,
) -> anyhow::Result<()> {
self.set_value(namespace, key, &serde_json::to_value(value)?)
.await
}
async fn set_with_expiry<T: Serialize + Sync>(
&self,
namespace: &str,
key: &str,
value: &T,
expiry: Option<chrono::DateTime<chrono::Utc>>,
) -> anyhow::Result<()> {
self.set_value_with_expiry(namespace, key, &serde_json::to_value(value)?, expiry)
.await
}
async fn get<T: DeserializeOwned + Sync>(
&self,
namespace: &str,
key: &str,
) -> anyhow::Result<Option<T>> {
match self.get_value(namespace, key).await? {
Some(b) => Ok(Some(serde_json::from_value(b)?)),
None => Ok(None),
}
}
}
impl<T: SessionStore + ?Sized> SessionStoreExt for T {}
#[async_trait::async_trait]
pub trait MemoryStore: Send + Sync {
async fn store_memory(
&self,
user_id: &str,
session_memory: SessionMemory,
) -> anyhow::Result<()>;
async fn search_memories(
&self,
user_id: &str,
query: &str,
limit: Option<usize>,
) -> anyhow::Result<Vec<String>>;
async fn get_user_memories(&self, user_id: &str) -> anyhow::Result<Vec<String>>;
async fn clear_user_memories(&self, user_id: &str) -> anyhow::Result<()>;
}
#[derive(Debug, Clone)]
pub struct SessionMemory {
pub agent_id: String,
pub thread_id: String,
pub session_summary: String,
pub key_insights: Vec<String>,
pub important_facts: Vec<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FilterMessageType {
Events,
Messages,
Artifacts,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct MessageFilter {
pub filter: Option<Vec<FilterMessageType>>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[async_trait]
pub trait TaskStore: Send + Sync {
fn init_task(
&self,
context_id: &str,
task_id: Option<&str>,
status: Option<TaskStatus>,
) -> Task {
let task_id = task_id.unwrap_or(&Uuid::new_v4().to_string()).to_string();
Task {
id: task_id,
status: status.unwrap_or(TaskStatus::Pending),
created_at: chrono::Utc::now().timestamp_millis(),
updated_at: chrono::Utc::now().timestamp_millis(),
thread_id: context_id.to_string(),
parent_task_id: None,
}
}
async fn get_or_create_task(
&self,
thread_id: &str,
task_id: &str,
) -> Result<(), anyhow::Error> {
match self.get_task(task_id).await? {
Some(task) => task,
None => {
self.create_task(thread_id, Some(task_id), Some(TaskStatus::Running))
.await?
}
};
Ok(())
}
async fn create_task(
&self,
context_id: &str,
task_id: Option<&str>,
task_status: Option<TaskStatus>,
) -> anyhow::Result<Task>;
async fn get_task(&self, task_id: &str) -> anyhow::Result<Option<Task>>;
async fn update_task_status(&self, task_id: &str, status: TaskStatus) -> anyhow::Result<()>;
async fn add_event_to_task(&self, task_id: &str, event: AgentEvent) -> anyhow::Result<()>;
async fn add_message_to_task(&self, task_id: &str, message: &Message) -> anyhow::Result<()>;
async fn cancel_task(&self, task_id: &str) -> anyhow::Result<Task>;
async fn list_tasks(&self, thread_id: Option<&str>) -> anyhow::Result<Vec<Task>>;
async fn get_history(
&self,
thread_id: &str,
filter: Option<MessageFilter>,
) -> anyhow::Result<Vec<(Task, Vec<TaskMessage>)>>;
async fn update_parent_task(
&self,
task_id: &str,
parent_task_id: Option<&str>,
) -> anyhow::Result<()>;
}
#[async_trait]
pub trait ThreadStore: Send + Sync {
fn as_any(&self) -> &dyn std::any::Any;
async fn create_thread(&self, request: CreateThreadRequest) -> anyhow::Result<Thread>;
async fn get_thread(&self, thread_id: &str) -> anyhow::Result<Option<Thread>>;
async fn update_thread(
&self,
thread_id: &str,
request: UpdateThreadRequest,
) -> anyhow::Result<Thread>;
async fn delete_thread(&self, thread_id: &str) -> anyhow::Result<()>;
async fn list_threads(
&self,
filter: &ThreadListFilter,
limit: Option<u32>,
offset: Option<u32>,
) -> anyhow::Result<ThreadListResponse>;
async fn update_thread_with_message(
&self,
thread_id: &str,
message: &str,
) -> anyhow::Result<()>;
async fn get_home_stats(&self) -> anyhow::Result<HomeStats>;
async fn get_agents_by_usage(
&self,
search: Option<&str>,
) -> anyhow::Result<Vec<AgentUsageInfo>>;
async fn get_agent_stats_map(
&self,
) -> anyhow::Result<std::collections::HashMap<String, AgentStatsInfo>>;
async fn mark_message_read(
&self,
thread_id: &str,
message_id: &str,
) -> anyhow::Result<MessageReadStatus>;
async fn get_message_read_status(
&self,
thread_id: &str,
message_id: &str,
) -> anyhow::Result<Option<MessageReadStatus>>;
async fn get_thread_read_status(
&self,
thread_id: &str,
) -> anyhow::Result<Vec<MessageReadStatus>>;
async fn vote_message(&self, request: VoteMessageRequest) -> anyhow::Result<MessageVote>;
async fn remove_vote(&self, thread_id: &str, message_id: &str) -> anyhow::Result<()>;
async fn get_user_vote(
&self,
thread_id: &str,
message_id: &str,
) -> anyhow::Result<Option<MessageVote>>;
async fn get_message_vote_summary(
&self,
thread_id: &str,
message_id: &str,
) -> anyhow::Result<MessageVoteSummary>;
async fn get_message_votes(
&self,
thread_id: &str,
message_id: &str,
) -> anyhow::Result<Vec<MessageVote>>;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
pub struct HomeStats {
pub total_agents: i64,
pub total_threads: i64,
pub total_messages: i64,
pub avg_run_time_ms: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_owned_agents: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_accessible_agents: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub most_active_agent: Option<MostActiveAgent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_threads: Option<Vec<LatestThreadInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recently_used_agents: Option<Vec<RecentlyUsedAgent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_metrics: Option<std::collections::HashMap<String, CustomMetric>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
pub struct CustomMetric {
pub label: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub helper: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_value: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_limit: Option<i64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
pub struct MostActiveAgent {
pub id: String,
pub name: String,
pub thread_count: i64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
pub struct RecentlyUsedAgent {
pub id: String,
pub name: String,
pub description: Option<String>,
pub last_used_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
pub struct LatestThreadInfo {
pub id: String,
pub title: String,
pub agent_id: String,
pub agent_name: String,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, ToSchema, JsonSchema)]
pub struct AgentStatsInfo {
pub thread_count: i64,
pub sub_agent_usage_count: i64,
pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[async_trait]
pub trait AgentStore: Send + Sync {
async fn list(
&self,
cursor: Option<String>,
limit: Option<usize>,
) -> (Vec<crate::configuration::AgentConfig>, Option<String>);
async fn get(&self, name: &str) -> Option<crate::configuration::AgentConfig>;
async fn register(&self, config: crate::configuration::AgentConfig) -> anyhow::Result<()>;
async fn update(&self, config: crate::configuration::AgentConfig) -> anyhow::Result<()>;
async fn clear(&self) -> anyhow::Result<()>;
async fn delete(&self, id: &str) -> anyhow::Result<()>;
async fn get_with_cloud_metadata(
&self,
name: &str,
) -> Option<(
crate::configuration::AgentConfig,
crate::configuration::AgentCloudMetadata,
)> {
self.get(name)
.await
.map(|c| (c, crate::configuration::AgentCloudMetadata::default()))
}
async fn list_with_cloud_metadata(
&self,
cursor: Option<String>,
limit: Option<usize>,
) -> (
Vec<(
crate::configuration::AgentConfig,
crate::configuration::AgentCloudMetadata,
)>,
Option<String>,
) {
let (configs, cursor) = self.list(cursor, limit).await;
(
configs
.into_iter()
.map(|c| (c, crate::configuration::AgentCloudMetadata::default()))
.collect(),
cursor,
)
}
}
#[async_trait::async_trait]
pub trait ScratchpadStore: Send + Sync + std::fmt::Debug {
async fn add_entry(
&self,
thread_id: &str,
entry: ScratchpadEntry,
) -> Result<(), crate::AgentError>;
async fn clear_entries(&self, thread_id: &str) -> Result<(), crate::AgentError>;
async fn get_entries(
&self,
thread_id: &str,
task_id: &str,
limit: Option<usize>,
) -> Result<Vec<ScratchpadEntry>, crate::AgentError>;
async fn get_all_entries(
&self,
thread_id: &str,
limit: Option<usize>,
) -> Result<Vec<ScratchpadEntry>, crate::AgentError>;
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct CrawlResult {
pub id: String,
pub url: String,
pub title: Option<String>,
pub content: String,
pub html: Option<String>,
pub metadata: serde_json::Value,
pub links: Vec<String>,
pub images: Vec<String>,
pub status_code: Option<u16>,
pub crawled_at: chrono::DateTime<chrono::Utc>,
pub processing_time_ms: Option<u64>,
}
#[async_trait]
pub trait CrawlStore: Send + Sync {
async fn store_crawl_result(&self, result: CrawlResult) -> anyhow::Result<String>;
async fn get_crawl_result(&self, id: &str) -> anyhow::Result<Option<CrawlResult>>;
async fn get_crawl_results_by_url(&self, url: &str) -> anyhow::Result<Vec<CrawlResult>>;
async fn get_recent_crawl_results(
&self,
limit: Option<usize>,
since: Option<chrono::DateTime<chrono::Utc>>,
) -> anyhow::Result<Vec<CrawlResult>>;
async fn is_url_recently_crawled(
&self,
url: &str,
cache_duration: chrono::Duration,
) -> anyhow::Result<Option<CrawlResult>>;
async fn delete_crawl_result(&self, id: &str) -> anyhow::Result<()>;
async fn cleanup_old_results(
&self,
before: chrono::DateTime<chrono::Utc>,
) -> anyhow::Result<usize>;
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VoteType {
Upvote,
Downvote,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct MessageReadStatus {
pub thread_id: String,
pub message_id: String,
pub user_id: String,
pub read_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct MarkMessageReadRequest {
pub thread_id: String,
pub message_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct MessageVote {
pub id: String,
pub thread_id: String,
pub message_id: String,
pub user_id: String,
pub vote_type: VoteType,
pub comment: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
#[schema(example = json!({"vote_type": "up"}))]
pub struct VoteMessageRequest {
pub thread_id: String,
pub message_id: String,
pub vote_type: VoteType,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
pub struct MessageVoteSummary {
pub message_id: String,
pub upvotes: i64,
pub downvotes: i64,
pub user_vote: Option<VoteType>,
}
#[async_trait]
pub trait ExternalToolCallsStore: Send + Sync + std::fmt::Debug {
async fn register_external_tool_call(
&self,
session_id: &str,
) -> anyhow::Result<oneshot::Receiver<ToolResponse>>;
async fn complete_external_tool_call(
&self,
session_id: &str,
tool_response: ToolResponse,
) -> anyhow::Result<()>;
async fn remove_tool_call(&self, session_id: &str) -> anyhow::Result<()>;
async fn list_pending_tool_calls(&self) -> anyhow::Result<Vec<String>>;
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct PromptTemplateRecord {
pub id: String,
pub name: String,
pub template: String,
pub description: Option<String>,
pub version: Option<String>,
pub is_system: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
#[schema(example = json!({"name": "greeting", "content": "Hello {{name}}, welcome to {{service}}!", "description": "A greeting template"}))]
pub struct NewPromptTemplate {
pub name: String,
pub template: String,
pub description: Option<String>,
pub version: Option<String>,
#[serde(default)]
pub is_system: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct UpdatePromptTemplate {
pub name: String,
pub template: String,
pub description: Option<String>,
}
#[async_trait]
pub trait PromptTemplateStore: Send + Sync {
async fn list(&self) -> anyhow::Result<Vec<PromptTemplateRecord>>;
async fn get(&self, id: &str) -> anyhow::Result<Option<PromptTemplateRecord>>;
async fn get_by_names(&self, names: &[String]) -> anyhow::Result<Vec<PromptTemplateRecord>>;
async fn create(&self, template: NewPromptTemplate) -> anyhow::Result<PromptTemplateRecord>;
async fn update(
&self,
id: &str,
update: UpdatePromptTemplate,
) -> anyhow::Result<PromptTemplateRecord>;
async fn delete(&self, id: &str) -> anyhow::Result<()>;
async fn clone_template(&self, id: &str) -> anyhow::Result<PromptTemplateRecord>;
async fn sync_system_templates(&self, templates: Vec<NewPromptTemplate>) -> anyhow::Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct SecretRecord {
pub id: String,
pub key: String,
pub value: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
#[schema(example = json!({"key": "OPENAI_API_KEY", "value": "sk-..."}))]
pub struct NewSecret {
pub key: String,
pub value: String,
}
#[async_trait]
pub trait SecretStore: Send + Sync {
async fn list(&self) -> anyhow::Result<Vec<SecretRecord>>;
async fn get(&self, key: &str) -> anyhow::Result<Option<SecretRecord>>;
async fn create(&self, secret: NewSecret) -> anyhow::Result<SecretRecord>;
async fn update(&self, key: &str, value: &str) -> anyhow::Result<SecretRecord>;
async fn delete(&self, key: &str) -> anyhow::Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct CustomProviderConfig {
pub id: String,
pub name: String,
pub base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct CustomModelEntry {
pub provider: String,
pub model: String,
#[serde(default = "default_completion")]
pub capability: String,
}
fn default_completion() -> String {
"completion".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct ConnectionProviderConfig {
pub id: String,
pub name: String,
pub authorization_url: String,
pub token_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(default)]
pub scopes_supported: Vec<String>,
#[serde(default)]
pub default_scopes: Vec<String>,
#[serde(default)]
pub scope_mappings: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct UpsertProviderRequest {
pub provider_id: String,
#[serde(default)]
pub secrets: std::collections::HashMap<String, String>,
#[serde(default)]
pub config: Option<CustomProviderConfig>,
#[serde(default)]
pub custom_models: Option<Vec<CustomModelEntry>>,
#[serde(default)]
pub default_model: Option<String>,
#[serde(default)]
pub connection_provider: Option<ConnectionProviderConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct UpsertProviderResponse {
pub provider_id: String,
pub secrets_saved: usize,
pub config_saved: bool,
}
#[async_trait]
pub trait ProviderStore: Send + Sync {
async fn upsert_provider(
&self,
req: UpsertProviderRequest,
) -> anyhow::Result<UpsertProviderResponse>;
async fn delete_provider(&self, provider_id: &str) -> anyhow::Result<()>;
async fn get_default_model(&self) -> anyhow::Result<Option<String>>;
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, ToSchema, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ContextExecutionType {
#[default]
Inline,
Fork,
}
impl std::fmt::Display for ContextExecutionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContextExecutionType::Inline => write!(f, "inline"),
ContextExecutionType::Fork => write!(f, "fork"),
}
}
}
impl std::str::FromStr for ContextExecutionType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"fork" => Ok(ContextExecutionType::Fork),
_ => Ok(ContextExecutionType::Inline),
}
}
}
pub const SKILL_LISTING_BUDGET: usize = 2_000;
pub const SKILL_DESCRIPTION_CAP: usize = 250;
pub const DEFAULT_SKILL_MAX_TOKENS: u32 = 8000;
#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
pub struct SkillFrontmatter {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub can_spawn_tasks: bool,
#[serde(default)]
pub paths: Vec<String>,
#[serde(default)]
pub is_public: bool,
}
impl SkillFrontmatter {
pub fn effective_max_tokens(&self) -> u32 {
self.max_tokens.unwrap_or(DEFAULT_SKILL_MAX_TOKENS)
}
pub fn as_listing_line(&self) -> String {
let desc = self.description.as_deref().unwrap_or("No description");
let desc_truncated = if desc.len() > SKILL_DESCRIPTION_CAP {
format!("{}...", &desc[..SKILL_DESCRIPTION_CAP.min(desc.len())])
} else {
desc.to_string()
};
let mut meta = Vec::new();
if let Some(model) = &self.model {
meta.push(format!("model: {}", model));
}
if self.can_spawn_tasks {
meta.push("tasks: yes".to_string());
}
if meta.is_empty() {
format!("- {}: {}", self.name, desc_truncated)
} else {
format!("- {}: {} ({})", self.name, desc_truncated, meta.join(", "))
}
}
}
pub fn format_skill_listing(skills: &[SkillFrontmatter], budget_tokens: usize) -> String {
let budget_chars = budget_tokens * 4;
let mut result = String::new();
let mut remaining_chars = budget_chars;
for skill in skills {
let line = format!("{}\n", skill.as_listing_line());
if line.len() > remaining_chars {
let name_line = format!("- {}\n", skill.name);
if name_line.len() <= remaining_chars {
result.push_str(&name_line);
remaining_chars -= name_line.len();
} else {
break;
}
} else {
result.push_str(&line);
remaining_chars -= line.len();
}
}
result.trim_end().to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct SkillsListResponse {
pub skills: Vec<SkillListItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct SkillListItem {
pub id: String,
#[serde(default)]
pub workspace_slug: String,
pub name: String,
#[serde(default)]
pub full_name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub is_public: bool,
#[serde(default)]
pub is_system: bool,
#[serde(default)]
pub is_owner: bool,
#[serde(default)]
pub is_workspace: bool,
#[serde(default)]
pub star_count: i32,
#[serde(default)]
pub clone_count: i32,
#[serde(default)]
pub is_starred: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct SkillRecord {
pub id: String,
#[serde(default)]
pub workspace_slug: String,
pub name: String,
#[serde(default)]
pub full_name: String,
pub description: Option<String>,
pub content: String,
pub tags: Vec<String>,
pub is_public: bool,
pub is_system: bool,
#[serde(default)]
pub is_owner: bool,
#[serde(default)]
pub is_workspace: bool,
pub star_count: i32,
pub clone_count: i32,
#[serde(default)]
pub is_starred: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default)]
pub context: ContextExecutionType,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
#[schema(example = json!({"name": "my-skill", "content": "# My Skill\nA helpful utility skill", "description": "A utility skill", "tags": ["utility"], "is_public": false}))]
pub struct NewSkill {
pub name: String,
pub description: Option<String>,
pub content: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub is_public: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default)]
pub context: ContextExecutionType,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct UpdateSkill {
pub name: Option<String>,
pub description: Option<String>,
pub content: Option<String>,
pub tags: Option<Vec<String>>,
pub is_public: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<ContextExecutionType>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SkillScope {
#[default]
Workspace,
Starred,
System,
Discover,
All,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillFilter {
#[serde(default)]
pub scope: SkillScope,
#[serde(default)]
pub search: Option<String>,
#[serde(default = "default_page")]
pub page: i64,
#[serde(default = "default_per_page")]
pub per_page: i64,
}
fn default_page() -> i64 {
1
}
fn default_per_page() -> i64 {
50
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct SkillListResponse {
pub skills: Vec<SkillListItem>,
pub total: i64,
pub page: i64,
pub per_page: i64,
pub total_pages: i64,
}
#[async_trait]
pub trait SkillStore: Send + Sync {
async fn list(&self, filter: SkillFilter) -> anyhow::Result<SkillListResponse>;
async fn get(&self, id: &str) -> anyhow::Result<Option<SkillRecord>>;
async fn create(&self, skill: NewSkill) -> anyhow::Result<SkillRecord>;
async fn update(&self, id: &str, update: UpdateSkill) -> anyhow::Result<SkillRecord>;
async fn delete(&self, id: &str) -> anyhow::Result<()>;
async fn star(&self, skill_id: &str) -> anyhow::Result<()>;
async fn unstar(&self, skill_id: &str) -> anyhow::Result<()>;
async fn clone_skill(&self, skill_id: &str) -> anyhow::Result<SkillRecord>;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct UsageSnapshot {
pub day_tokens: i64,
pub week_tokens: i64,
pub month_tokens: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct UsageLimits {
pub daily_tokens: Option<i64>,
pub weekly_tokens: Option<i64>,
pub monthly_tokens: Option<i64>,
}
#[derive(Debug, Clone)]
pub enum UsageCheckResult {
Allowed,
Denied { reason: String },
}
#[async_trait]
pub trait UsageService: Send + Sync {
async fn check_request(
&self,
workspace_id: &str,
user_id: &str,
is_llm: bool,
auth_source: &str,
) -> UsageCheckResult;
async fn record_usage(
&self,
workspace_id: &str,
user_id: &str,
tokens_used: i64,
) -> anyhow::Result<()>;
async fn get_usage(&self, workspace_id: &str, user_id: &str) -> anyhow::Result<UsageSnapshot>;
async fn get_limits(&self, workspace_id: &str) -> anyhow::Result<UsageLimits>;
}
#[derive(Debug, Clone)]
pub struct NoOpUsageService;
#[async_trait]
impl UsageService for NoOpUsageService {
async fn check_request(
&self,
_workspace_id: &str,
_user_id: &str,
_is_llm: bool,
_auth_source: &str,
) -> UsageCheckResult {
UsageCheckResult::Allowed
}
async fn record_usage(
&self,
_workspace_id: &str,
_user_id: &str,
_tokens_used: i64,
) -> anyhow::Result<()> {
Ok(())
}
async fn get_usage(
&self,
_workspace_id: &str,
_user_id: &str,
) -> anyhow::Result<UsageSnapshot> {
Ok(UsageSnapshot::default())
}
async fn get_limits(&self, _workspace_id: &str) -> anyhow::Result<UsageLimits> {
Ok(UsageLimits::default())
}
}
#[async_trait]
pub trait ConnectionStore: Send + Sync + 'static {
async fn create(&self, connection: NewConnection) -> anyhow::Result<Connection>;
async fn get_by_id(&self, id: &str) -> anyhow::Result<Option<Connection>>;
async fn list_by_workspace(&self, workspace_id: &str) -> anyhow::Result<Vec<Connection>>;
async fn update_status(&self, id: &str, status: ConnectionStatus) -> anyhow::Result<()>;
async fn update_skill_id(&self, id: &str, skill_id: uuid::Uuid) -> anyhow::Result<()>;
async fn delete(&self, id: &str) -> anyhow::Result<()>;
async fn get_by_provider(
&self,
workspace_id: &str,
provider: &str,
) -> anyhow::Result<Option<Connection>>;
}
#[async_trait]
pub trait ConnectionTokenStore: Send + Sync + 'static {
async fn store_token(&self, connection_id: &str, token: ConnectionToken) -> anyhow::Result<()>;
async fn get_token(&self, connection_id: &str) -> anyhow::Result<Option<ConnectionToken>>;
async fn remove_token(&self, connection_id: &str) -> anyhow::Result<()>;
async fn refresh_token(
&self,
_connection_id: &str,
_connection: &Connection,
) -> anyhow::Result<Option<ConnectionToken>> {
Ok(None)
}
async fn store_oauth_state(
&self,
state_key: &str,
state: serde_json::Value,
) -> anyhow::Result<()>;
async fn get_oauth_state(&self, state_key: &str) -> anyhow::Result<Option<serde_json::Value>>;
async fn remove_oauth_state(&self, state_key: &str) -> anyhow::Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skills_list_response_deserialize_cloud_format() {
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"}]}"#;
let resp: SkillsListResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.skills.len(), 1);
assert_eq!(resp.skills[0].name, "test");
assert_eq!(resp.skills[0].workspace_slug, "ws");
assert_eq!(resp.skills[0].full_name, "ws/test");
assert!(resp.skills[0].is_public);
}
#[test]
fn test_skills_list_response_deserialize_defaults() {
let json = r#"{"skills":[{"id":"abc","name":"test","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z"}]}"#;
let resp: SkillsListResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.skills[0].workspace_slug, "");
assert_eq!(resp.skills[0].full_name, "");
assert!(!resp.skills[0].is_public);
assert!(!resp.skills[0].is_owner);
}
#[test]
fn test_skills_list_response_roundtrip() {
let resp = SkillsListResponse {
skills: vec![SkillListItem {
id: "id1".into(),
workspace_slug: "local".into(),
name: "my_skill".into(),
full_name: "local/my_skill".into(),
description: Some("A skill".into()),
tags: vec!["tag1".into()],
is_public: false,
is_system: false,
is_owner: true,
is_workspace: true,
star_count: 5,
clone_count: 2,
is_starred: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}],
};
let json = serde_json::to_string(&resp).unwrap();
let decoded: SkillsListResponse = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.skills[0].name, "my_skill");
assert_eq!(decoded.skills[0].star_count, 5);
}
}