stakpak_api/client/
mod.rs1mod provider;
10
11use crate::local::hooks::task_board_context::{TaskBoardContextHook, TaskBoardContextHookOptions};
12use crate::local::storage::LocalStorage;
13use crate::models::AgentState;
14use crate::stakpak::storage::StakpakStorage;
15use crate::stakpak::{StakpakApiClient, StakpakApiConfig};
16use crate::storage::SessionStorage;
17
18use stakpak_shared::hooks::{HookRegistry, LifecycleEvent};
19use stakpak_shared::models::llm::{LLMProviderConfig, ProviderConfig};
20use stakpak_shared::models::stakai_adapter::StakAIClient;
21use std::sync::Arc;
22
23pub const DEFAULT_STAKPAK_ENDPOINT: &str = "https://apiv2.stakpak.dev";
29
30#[derive(Debug, Clone)]
32pub struct StakpakConfig {
33 pub api_key: String,
35 pub api_endpoint: String,
37}
38
39impl StakpakConfig {
40 pub fn new(api_key: impl Into<String>) -> Self {
41 Self {
42 api_key: api_key.into(),
43 api_endpoint: DEFAULT_STAKPAK_ENDPOINT.to_string(),
44 }
45 }
46
47 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
48 self.api_endpoint = endpoint.into();
49 self
50 }
51}
52
53#[derive(Debug, Default)]
55pub struct AgentClientConfig {
56 pub stakpak: Option<StakpakConfig>,
58 pub providers: LLMProviderConfig,
60 pub store_path: Option<String>,
62 pub hook_registry: Option<HookRegistry<AgentState>>,
64}
65
66impl AgentClientConfig {
67 pub fn new() -> Self {
69 Self::default()
70 }
71
72 pub fn with_stakpak(mut self, config: StakpakConfig) -> Self {
76 self.stakpak = Some(config);
77 self
78 }
79
80 pub fn with_providers(mut self, providers: LLMProviderConfig) -> Self {
82 self.providers = providers;
83 self
84 }
85
86 pub fn with_store_path(mut self, path: impl Into<String>) -> Self {
88 self.store_path = Some(path.into());
89 self
90 }
91
92 pub fn with_hook_registry(mut self, registry: HookRegistry<AgentState>) -> Self {
94 self.hook_registry = Some(registry);
95 self
96 }
97}
98
99const DEFAULT_STORE_PATH: &str = ".stakpak/data/local.db";
104
105#[derive(Clone)]
112pub struct AgentClient {
113 pub(crate) stakai: StakAIClient,
115 pub(crate) stakpak_api: Option<StakpakApiClient>,
117 pub(crate) session_storage: Arc<dyn SessionStorage>,
119 pub(crate) hook_registry: Arc<HookRegistry<AgentState>>,
121 pub(crate) stakpak: Option<StakpakConfig>,
123}
124
125impl AgentClient {
126 pub async fn new(config: AgentClientConfig) -> Result<Self, String> {
128 let mut providers = config.providers.clone();
130 if let Some(stakpak) = &config.stakpak
131 && !stakpak.api_key.is_empty()
132 {
133 providers.providers.insert(
134 "stakpak".to_string(),
135 ProviderConfig::Stakpak {
136 api_key: Some(stakpak.api_key.clone()),
137 api_endpoint: Some(stakpak.api_endpoint.clone()),
138 auth: None,
139 },
140 );
141 }
142
143 let stakai = StakAIClient::new(&providers)
145 .map_err(|e| format!("Failed to create StakAI client: {}", e))?;
146
147 let stakpak_api = if let Some(stakpak) = &config.stakpak {
149 if !stakpak.api_key.is_empty() {
150 Some(
151 StakpakApiClient::new(&StakpakApiConfig {
152 api_key: stakpak.api_key.clone(),
153 api_endpoint: stakpak.api_endpoint.clone(),
154 })
155 .map_err(|e| format!("Failed to create Stakpak API client: {}", e))?,
156 )
157 } else {
158 None
159 }
160 } else {
161 None
162 };
163
164 let session_storage: Arc<dyn SessionStorage> = if let Some(stakpak) = &config.stakpak
166 && !stakpak.api_key.is_empty()
167 {
168 Arc::new(
169 StakpakStorage::new(&stakpak.api_key, &stakpak.api_endpoint)
170 .map_err(|e| format!("Failed to create Stakpak storage: {}", e))?,
171 )
172 } else {
173 let store_path = config.store_path.clone().unwrap_or_else(|| {
174 std::env::var("HOME")
175 .map(|h| format!("{}/{}", h, DEFAULT_STORE_PATH))
176 .unwrap_or_else(|_| DEFAULT_STORE_PATH.to_string())
177 });
178 Arc::new(
179 LocalStorage::new(&store_path)
180 .await
181 .map_err(|e| format!("Failed to create local storage: {}", e))?,
182 )
183 };
184
185 let mut hook_registry = config.hook_registry.unwrap_or_default();
187 hook_registry.register(
188 LifecycleEvent::BeforeInference,
189 Box::new(TaskBoardContextHook::new(TaskBoardContextHookOptions {
190 keep_last_n_assistant_messages: Some(5), context_budget_threshold: Some(0.8), })),
193 );
194 let hook_registry = Arc::new(hook_registry);
195
196 Ok(Self {
197 stakai,
198 stakpak_api,
199 session_storage,
200 hook_registry,
201 stakpak: config.stakpak,
202 })
203 }
204
205 pub fn has_stakpak(&self) -> bool {
207 self.stakpak_api.is_some()
208 }
209
210 pub fn get_stakpak_api_endpoint(&self) -> &str {
212 self.stakpak
213 .as_ref()
214 .map(|s| s.api_endpoint.as_str())
215 .unwrap_or(DEFAULT_STAKPAK_ENDPOINT)
216 }
217
218 pub fn stakai(&self) -> &StakAIClient {
220 &self.stakai
221 }
222
223 pub fn stakpak_api(&self) -> Option<&StakpakApiClient> {
225 self.stakpak_api.as_ref()
226 }
227
228 pub fn hook_registry(&self) -> &Arc<HookRegistry<AgentState>> {
230 &self.hook_registry
231 }
232
233 pub fn session_storage(&self) -> &Arc<dyn SessionStorage> {
237 &self.session_storage
238 }
239}
240
241impl std::fmt::Debug for AgentClient {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 f.debug_struct("AgentClient")
245 .field("has_stakpak", &self.has_stakpak())
246 .finish_non_exhaustive()
247 }
248}