1use crate::{
56 error::{CostError, LlmError, StoreError, ToolError},
57 types::{
58 AgentEvent, ApprovalContext, ApprovalDecision, ArtifactRecord, LlmCapabilities, LlmRequest,
59 LlmResponse, LlmStream, SessionId, SessionState, TokenUsage, ToolCall, ToolDescriptor,
60 ToolResult, TurnCheckpoint,
61 },
62};
63
64#[async_trait::async_trait]
68pub trait LlmPort: Send + Sync {
69 #[must_use]
71 fn capabilities(&self) -> LlmCapabilities {
72 LlmCapabilities::default()
73 }
74
75 async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, LlmError>;
77
78 async fn complete_stream(&self, req: LlmRequest) -> Result<LlmStream, LlmError>;
80}
81
82#[async_trait::async_trait]
86pub trait ToolCatalogPort: Send + Sync {
87 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError>;
89}
90
91#[async_trait::async_trait]
93pub trait ToolExecutorPort: Send + Sync {
94 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError>;
96}
97
98#[async_trait::async_trait]
102pub trait ToolPort: Send + Sync {
103 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError>;
105
106 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError>;
108}
109
110#[async_trait::async_trait]
111impl<T> ToolCatalogPort for T
112where
113 T: ToolPort + ?Sized,
114{
115 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
116 ToolPort::list_tools(self).await
117 }
118}
119
120#[async_trait::async_trait]
121impl<T> ToolExecutorPort for T
122where
123 T: ToolPort + ?Sized,
124{
125 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
126 ToolPort::call_tool(self, call).await
127 }
128}
129
130pub trait ToolPolicyPort: Send + Sync {
132 fn is_tool_allowed(
134 &self,
135 tool: &str,
136 deny_tools: &[String],
137 allow_tools: Option<&[String]>,
138 ) -> bool;
139}
140
141#[async_trait::async_trait]
143pub trait ApprovalPort: Send + Sync {
144 async fn approve_tool_call(
146 &self,
147 call: &ToolCall,
148 context: &ApprovalContext,
149 ) -> Result<ApprovalDecision, ToolError>;
150}
151
152#[async_trait::async_trait]
154pub trait TurnCheckpointStorePort: Send + Sync {
155 async fn save_checkpoint(&self, checkpoint: &TurnCheckpoint) -> Result<(), StoreError>;
156 async fn load_latest(
157 &self,
158 session_id: &SessionId,
159 ) -> Result<Option<TurnCheckpoint>, StoreError>;
160}
161
162#[async_trait::async_trait]
164pub trait ArtifactStorePort: Send + Sync {
165 async fn put(&self, artifact: ArtifactRecord) -> Result<(), StoreError>;
166 async fn list_by_session(
167 &self,
168 session_id: &SessionId,
169 ) -> Result<Vec<ArtifactRecord>, StoreError>;
170}
171
172#[async_trait::async_trait]
174pub trait CostMeterPort: Send + Sync {
175 async fn check_budget(&self, session_id: &SessionId) -> Result<(), CostError>;
176 async fn record_llm_usage(
177 &self,
178 session_id: &SessionId,
179 model: &str,
180 usage: &TokenUsage,
181 ) -> Result<(), CostError>;
182 async fn record_tool_result(
183 &self,
184 session_id: &SessionId,
185 tool_result: &ToolResult,
186 ) -> Result<(), CostError>;
187}
188
189#[async_trait::async_trait]
193pub trait SessionStore: Send + Sync {
194 async fn load(&self, id: &SessionId) -> Result<Option<SessionState>, StoreError>;
196
197 async fn save(&self, id: &SessionId, state: &SessionState) -> Result<(), StoreError>;
199}
200
201pub trait EventSink: Send + Sync {
205 fn emit(&self, event: AgentEvent);
207}
208
209#[cfg(test)]
212mod tests {
213 use std::sync::{Arc, Mutex};
214
215 use super::*;
216
217 struct MockLlm {
220 response: LlmResponse,
221 }
222
223 #[async_trait::async_trait]
224 impl LlmPort for MockLlm {
225 async fn complete(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
226 Ok(self.response.clone())
227 }
228
229 async fn complete_stream(&self, _req: LlmRequest) -> Result<LlmStream, LlmError> {
230 Err(LlmError::Provider("streaming not implemented in mock".into()))
231 }
232 }
233
234 struct MockToolPort {
237 tools: Vec<ToolDescriptor>,
238 }
239
240 #[async_trait::async_trait]
241 impl ToolPort for MockToolPort {
242 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, ToolError> {
243 Ok(self.tools.clone())
244 }
245
246 async fn call_tool(&self, call: ToolCall) -> Result<ToolResult, ToolError> {
247 Ok(ToolResult {
248 name: call.name,
249 output: serde_json::json!({"result": "mock"}),
250 is_error: false,
251 })
252 }
253 }
254
255 struct MockSessionStore {
258 inner: Mutex<std::collections::HashMap<SessionId, SessionState>>,
259 }
260
261 impl MockSessionStore {
262 fn new() -> Self {
263 Self { inner: Mutex::new(std::collections::HashMap::new()) }
264 }
265 }
266
267 #[async_trait::async_trait]
268 impl SessionStore for MockSessionStore {
269 async fn load(&self, id: &SessionId) -> Result<Option<SessionState>, StoreError> {
270 let map = self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
271 Ok(map.get(id).cloned())
272 }
273
274 async fn save(&self, id: &SessionId, state: &SessionState) -> Result<(), StoreError> {
275 let mut map = self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
276 map.insert(id.clone(), state.clone());
277 Ok(())
278 }
279 }
280
281 struct MockEventSink {
284 events: Mutex<Vec<AgentEvent>>,
285 }
286
287 impl MockEventSink {
288 fn new() -> Self {
289 Self { events: Mutex::new(Vec::new()) }
290 }
291 }
292
293 impl EventSink for MockEventSink {
294 fn emit(&self, event: AgentEvent) {
295 let mut events = self.events.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
296 events.push(event);
297 }
298 }
299
300 #[tokio::test]
303 async fn llm_port_complete() {
304 let llm: Arc<dyn LlmPort> = Arc::new(MockLlm {
305 response: LlmResponse {
306 content: "Hello!".into(),
307 usage: crate::types::TokenUsage { prompt_tokens: 10, completion_tokens: 5 },
308 finish_reason: crate::types::FinishReason::Stop,
309 tool_calls: Vec::new(),
310 },
311 });
312
313 let req = LlmRequest { model: "test-model".into(), messages: vec![], tools: vec![] };
314
315 let resp = llm.complete(req).await;
316 assert!(resp.is_ok(), "complete should succeed");
317 if let Ok(resp) = resp {
318 assert_eq!(resp.content, "Hello!");
319 assert_eq!(resp.usage.total(), 15);
320 }
321 }
322
323 #[tokio::test]
324 async fn tool_port_list_and_call() {
325 let tools: Arc<dyn ToolPort> = Arc::new(MockToolPort {
326 tools: vec![ToolDescriptor {
327 id: "test/echo".into(),
328 description: "Echo tool".into(),
329 input_schema: serde_json::json!({}),
330 source: crate::types::ToolSource::Local,
331 }],
332 });
333
334 let listed = tools.list_tools().await;
335 assert!(listed.is_ok(), "list_tools should succeed");
336 if let Ok(listed) = listed {
337 assert_eq!(listed.len(), 1);
338 assert_eq!(listed[0].id, "test/echo");
339 }
340
341 let result = tools
342 .call_tool(ToolCall {
343 name: "test/echo".into(),
344 arguments: serde_json::json!({"msg": "hi"}),
345 })
346 .await;
347 assert!(result.is_ok(), "call_tool should succeed");
348 if let Ok(result) = result {
349 assert!(!result.is_error);
350 }
351 }
352
353 #[tokio::test]
354 async fn session_store_roundtrip() {
355 let store: Arc<dyn SessionStore> = Arc::new(MockSessionStore::new());
356
357 let id = "session-1".to_string();
358 let loaded_before = store.load(&id).await;
359 assert!(matches!(loaded_before, Ok(None)));
360
361 let state = SessionState {
362 messages: vec![crate::types::Message {
363 role: crate::types::Role::User,
364 content: "hello".into(),
365 }],
366 ..Default::default()
367 };
368
369 let saved = store.save(&id, &state).await;
370 assert!(saved.is_ok(), "save should succeed");
371
372 let loaded = store.load(&id).await;
373 assert!(matches!(loaded, Ok(Some(_))), "load should return stored state");
374 if let Ok(Some(loaded)) = loaded {
375 assert_eq!(loaded.messages.len(), 1);
376 assert_eq!(loaded.messages[0].content, "hello");
377 }
378 }
379
380 #[test]
381 fn event_sink_collect() {
382 let sink = MockEventSink::new();
383 sink.emit(AgentEvent::TurnStarted { session_id: "s1".into() });
384 sink.emit(AgentEvent::Error { error: "boom".into() });
385
386 let events = sink.events.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
387 assert_eq!(events.len(), 2);
388 }
389
390 #[test]
391 fn turn_policy_defaults() {
392 let policy = crate::types::TurnPolicy::default();
393 assert_eq!(policy.max_steps, 12);
394 assert_eq!(policy.max_tool_calls, 8);
395 assert_eq!(policy.max_consecutive_errors, 2);
396 assert_eq!(policy.turn_timeout_ms, 90_000);
397 assert_eq!(policy.tool_timeout_ms, 15_000);
398 }
399}