mod provider;
use crate::local::hooks::task_board_context::{TaskBoardContextHook, TaskBoardContextHookOptions};
use crate::local::storage::LocalStorage;
use crate::models::AgentState;
use crate::stakpak::storage::StakpakStorage;
use crate::stakpak::{StakpakApiClient, StakpakApiConfig};
use crate::storage::SessionStorage;
use stakpak_shared::hooks::{HookRegistry, LifecycleEvent};
use stakpak_shared::models::llm::{LLMProviderConfig, ProviderConfig};
use stakpak_shared::models::stakai_adapter::StakAIClient;
use std::sync::Arc;
pub const DEFAULT_STAKPAK_ENDPOINT: &str = "https://apiv2.stakpak.dev";
#[derive(Debug, Clone)]
pub struct StakpakConfig {
pub api_key: String,
pub api_endpoint: String,
}
impl StakpakConfig {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
api_endpoint: DEFAULT_STAKPAK_ENDPOINT.to_string(),
}
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.api_endpoint = endpoint.into();
self
}
}
#[derive(Debug, Default)]
pub struct AgentClientConfig {
pub stakpak: Option<StakpakConfig>,
pub providers: LLMProviderConfig,
pub store_path: Option<String>,
pub hook_registry: Option<HookRegistry<AgentState>>,
}
impl AgentClientConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_stakpak(mut self, config: StakpakConfig) -> Self {
self.stakpak = Some(config);
self
}
pub fn with_providers(mut self, providers: LLMProviderConfig) -> Self {
self.providers = providers;
self
}
pub fn with_store_path(mut self, path: impl Into<String>) -> Self {
self.store_path = Some(path.into());
self
}
pub fn with_hook_registry(mut self, registry: HookRegistry<AgentState>) -> Self {
self.hook_registry = Some(registry);
self
}
}
const DEFAULT_STORE_PATH: &str = ".stakpak/data/local.db";
#[derive(Clone)]
pub struct AgentClient {
pub(crate) stakai: StakAIClient,
pub(crate) stakpak_api: Option<StakpakApiClient>,
pub(crate) session_storage: Arc<dyn SessionStorage>,
pub(crate) hook_registry: Arc<HookRegistry<AgentState>>,
pub(crate) stakpak: Option<StakpakConfig>,
}
impl AgentClient {
pub async fn build_session_storage(
stakpak: Option<StakpakConfig>,
store_path: Option<String>,
profile_name: Option<String>,
) -> Result<Arc<dyn SessionStorage>, String> {
if let Some(stakpak) = stakpak
&& !stakpak.api_key.is_empty()
{
let storage = StakpakStorage::new_with_profile(
&stakpak.api_key,
&stakpak.api_endpoint,
profile_name,
)
.map_err(|e| format!("Failed to create Stakpak storage: {}", e))?;
Ok(Arc::new(storage))
} else {
let store_path = store_path.unwrap_or_else(|| {
std::env::var("HOME")
.map(|h| format!("{}/{}", h, DEFAULT_STORE_PATH))
.unwrap_or_else(|_| DEFAULT_STORE_PATH.to_string())
});
let storage = LocalStorage::new(&store_path)
.await
.map_err(|e| format!("Failed to create local storage: {}", e))?;
Ok(Arc::new(storage))
}
}
pub async fn new(config: AgentClientConfig) -> Result<Self, String> {
let mut providers = config.providers.clone();
if let Some(stakpak) = &config.stakpak
&& !stakpak.api_key.is_empty()
{
providers.providers.insert(
"stakpak".to_string(),
ProviderConfig::Stakpak {
api_key: Some(stakpak.api_key.clone()),
api_endpoint: Some(stakpak.api_endpoint.clone()),
auth: None,
},
);
}
let stakai = StakAIClient::new(&providers)
.map_err(|e| format!("Failed to create StakAI client: {}", e))?;
let stakpak_api = if let Some(stakpak) = &config.stakpak {
if !stakpak.api_key.is_empty() {
Some(
StakpakApiClient::new(&StakpakApiConfig {
api_key: stakpak.api_key.clone(),
api_endpoint: stakpak.api_endpoint.clone(),
})
.map_err(|e| format!("Failed to create Stakpak API client: {}", e))?,
)
} else {
None
}
} else {
None
};
let session_storage: Arc<dyn SessionStorage> = if let Some(stakpak) = &config.stakpak
&& !stakpak.api_key.is_empty()
{
Arc::new(
StakpakStorage::new(&stakpak.api_key, &stakpak.api_endpoint)
.map_err(|e| format!("Failed to create Stakpak storage: {}", e))?,
)
} else {
let store_path = config.store_path.clone().unwrap_or_else(|| {
std::env::var("HOME")
.map(|h| format!("{}/{}", h, DEFAULT_STORE_PATH))
.unwrap_or_else(|_| DEFAULT_STORE_PATH.to_string())
});
Arc::new(
LocalStorage::new(&store_path)
.await
.map_err(|e| format!("Failed to create local storage: {}", e))?,
)
};
let mut hook_registry = config.hook_registry.unwrap_or_default();
hook_registry.register(
LifecycleEvent::BeforeInference,
Box::new(TaskBoardContextHook::new(TaskBoardContextHookOptions {
keep_last_n_assistant_messages: Some(5), context_budget_threshold: Some(0.8), })),
);
let hook_registry = Arc::new(hook_registry);
Ok(Self {
stakai,
stakpak_api,
session_storage,
hook_registry,
stakpak: config.stakpak,
})
}
pub fn has_stakpak(&self) -> bool {
self.stakpak_api.is_some()
}
pub fn get_stakpak_api_endpoint(&self) -> &str {
self.stakpak
.as_ref()
.map(|s| s.api_endpoint.as_str())
.unwrap_or(DEFAULT_STAKPAK_ENDPOINT)
}
pub fn stakai(&self) -> &StakAIClient {
&self.stakai
}
pub fn stakpak_api(&self) -> Option<&StakpakApiClient> {
self.stakpak_api.as_ref()
}
pub fn hook_registry(&self) -> &Arc<HookRegistry<AgentState>> {
&self.hook_registry
}
pub fn session_storage(&self) -> &Arc<dyn SessionStorage> {
&self.session_storage
}
}
impl std::fmt::Debug for AgentClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentClient")
.field("has_stakpak", &self.has_stakpak())
.finish_non_exhaustive()
}
}