Skip to main content

agent_sdk/agent_loop/
builder.rs

1use crate::context::{CompactionConfig, ContextCompactor};
2use crate::hooks::{AgentHooks, DefaultHooks};
3use crate::llm::LlmProvider;
4use crate::skills::Skill;
5use crate::stores::{InMemoryStore, MessageStore, StateStore, ToolExecutionStore};
6use crate::tools::ToolRegistry;
7use crate::types::AgentConfig;
8use std::sync::Arc;
9
10use super::AgentLoop;
11
12/// Builder for constructing an `AgentLoop`.
13///
14/// # Example
15///
16/// ```ignore
17/// let agent = AgentLoop::builder()
18///     .provider(my_provider)
19///     .tools(my_tools)
20///     .config(AgentConfig::default())
21///     .build();
22/// ```
23pub struct AgentLoopBuilder<Ctx, P, H, M, S> {
24    provider: Option<P>,
25    tools: Option<ToolRegistry<Ctx>>,
26    hooks: Option<H>,
27    message_store: Option<M>,
28    state_store: Option<S>,
29    config: Option<AgentConfig>,
30    compaction_config: Option<CompactionConfig>,
31    compactor: Option<Arc<dyn ContextCompactor>>,
32    execution_store: Option<Arc<dyn ToolExecutionStore>>,
33}
34
35impl<Ctx> AgentLoopBuilder<Ctx, (), (), (), ()> {
36    /// Create a new builder with no components set.
37    #[must_use]
38    pub fn new() -> Self {
39        Self {
40            provider: None,
41            tools: None,
42            hooks: None,
43            message_store: None,
44            state_store: None,
45            config: None,
46            compaction_config: None,
47            compactor: None,
48            execution_store: None,
49        }
50    }
51}
52
53impl<Ctx> Default for AgentLoopBuilder<Ctx, (), (), (), ()> {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl<Ctx, P, H, M, S> AgentLoopBuilder<Ctx, P, H, M, S> {
60    /// Set the LLM provider.
61    #[must_use]
62    pub fn provider<P2: LlmProvider>(self, provider: P2) -> AgentLoopBuilder<Ctx, P2, H, M, S> {
63        AgentLoopBuilder {
64            provider: Some(provider),
65            tools: self.tools,
66            hooks: self.hooks,
67            message_store: self.message_store,
68            state_store: self.state_store,
69            config: self.config,
70            compaction_config: self.compaction_config,
71            compactor: self.compactor,
72            execution_store: self.execution_store,
73        }
74    }
75
76    /// Set the tool registry.
77    #[must_use]
78    pub fn tools(mut self, tools: ToolRegistry<Ctx>) -> Self {
79        self.tools = Some(tools);
80        self
81    }
82
83    /// Set the agent hooks.
84    #[must_use]
85    pub fn hooks<H2: AgentHooks>(self, hooks: H2) -> AgentLoopBuilder<Ctx, P, H2, M, S> {
86        AgentLoopBuilder {
87            provider: self.provider,
88            tools: self.tools,
89            hooks: Some(hooks),
90            message_store: self.message_store,
91            state_store: self.state_store,
92            config: self.config,
93            compaction_config: self.compaction_config,
94            compactor: self.compactor,
95            execution_store: self.execution_store,
96        }
97    }
98
99    /// Set the message store.
100    #[must_use]
101    pub fn message_store<M2: MessageStore>(
102        self,
103        message_store: M2,
104    ) -> AgentLoopBuilder<Ctx, P, H, M2, S> {
105        AgentLoopBuilder {
106            provider: self.provider,
107            tools: self.tools,
108            hooks: self.hooks,
109            message_store: Some(message_store),
110            state_store: self.state_store,
111            config: self.config,
112            compaction_config: self.compaction_config,
113            compactor: self.compactor,
114            execution_store: self.execution_store,
115        }
116    }
117
118    /// Set the state store.
119    #[must_use]
120    pub fn state_store<S2: StateStore>(
121        self,
122        state_store: S2,
123    ) -> AgentLoopBuilder<Ctx, P, H, M, S2> {
124        AgentLoopBuilder {
125            provider: self.provider,
126            tools: self.tools,
127            hooks: self.hooks,
128            message_store: self.message_store,
129            state_store: Some(state_store),
130            config: self.config,
131            compaction_config: self.compaction_config,
132            compactor: self.compactor,
133            execution_store: self.execution_store,
134        }
135    }
136
137    /// Set the execution store for tool idempotency.
138    ///
139    /// When set, tool executions will be tracked using a write-ahead pattern:
140    /// 1. Record execution intent BEFORE calling the tool
141    /// 2. Update with result AFTER completion
142    /// 3. On retry, return cached result if execution already completed
143    ///
144    /// # Example
145    ///
146    /// ```ignore
147    /// use agent_sdk::{builder, stores::InMemoryExecutionStore};
148    ///
149    /// let agent = builder()
150    ///     .provider(my_provider)
151    ///     .execution_store(InMemoryExecutionStore::new())
152    ///     .build();
153    /// ```
154    #[must_use]
155    pub fn execution_store(mut self, store: impl ToolExecutionStore + 'static) -> Self {
156        self.execution_store = Some(Arc::new(store));
157        self
158    }
159
160    /// Set the agent configuration.
161    #[must_use]
162    pub fn config(mut self, config: AgentConfig) -> Self {
163        self.config = Some(config);
164        self
165    }
166
167    /// Enable context compaction with the given configuration.
168    ///
169    /// When enabled, the agent will automatically compact conversation history
170    /// when it exceeds the configured token threshold.
171    ///
172    /// # Example
173    ///
174    /// ```ignore
175    /// use agent_sdk::{builder, context::CompactionConfig};
176    ///
177    /// let agent = builder()
178    ///     .provider(my_provider)
179    ///     .with_compaction(CompactionConfig::default())
180    ///     .build();
181    /// ```
182    #[must_use]
183    pub const fn with_compaction(mut self, config: CompactionConfig) -> Self {
184        self.compaction_config = Some(config);
185        self
186    }
187
188    /// Enable context compaction with default settings.
189    ///
190    /// This is a convenience method equivalent to:
191    /// ```ignore
192    /// builder.with_compaction(CompactionConfig::default())
193    /// ```
194    #[must_use]
195    pub fn with_auto_compaction(self) -> Self {
196        self.with_compaction(CompactionConfig::default())
197    }
198
199    /// Override the default compactor with a custom implementation.
200    #[must_use]
201    pub fn with_custom_compactor(mut self, compactor: impl ContextCompactor + 'static) -> Self {
202        self.compactor = Some(Arc::new(compactor));
203        self
204    }
205
206    /// Apply a skill configuration.
207    ///
208    /// This merges the skill's system prompt with the existing configuration
209    /// and filters tools based on the skill's allowed/denied lists.
210    ///
211    /// # Example
212    ///
213    /// ```ignore
214    /// let skill = Skill::new("code-review", "You are a code reviewer...")
215    ///     .with_denied_tools(vec!["bash".into()]);
216    ///
217    /// let agent = builder()
218    ///     .provider(provider)
219    ///     .tools(tools)
220    ///     .with_skill(skill)
221    ///     .build();
222    /// ```
223    #[must_use]
224    pub fn with_skill(mut self, skill: Skill) -> Self
225    where
226        Ctx: Send + Sync + 'static,
227    {
228        // Filter tools based on skill configuration first (before moving skill)
229        if let Some(ref mut tools) = self.tools {
230            tools.filter(|name| skill.is_tool_allowed(name));
231        }
232
233        // Merge system prompt
234        let mut config = self.config.take().unwrap_or_default();
235        if config.system_prompt.is_empty() {
236            config.system_prompt = skill.system_prompt;
237        } else {
238            config.system_prompt = format!("{}\n\n{}", config.system_prompt, skill.system_prompt);
239        }
240        self.config = Some(config);
241
242        self
243    }
244}
245
246impl<Ctx, P> AgentLoopBuilder<Ctx, P, (), (), ()>
247where
248    Ctx: Send + Sync + 'static,
249    P: LlmProvider + 'static,
250{
251    /// Build the agent loop with default hooks and in-memory stores.
252    ///
253    /// This is a convenience method that uses:
254    /// - `DefaultHooks` for hooks
255    /// - `InMemoryStore` for message store
256    /// - `InMemoryStore` for state store
257    /// - `AgentConfig::default()` if no config is set
258    ///
259    /// # Panics
260    ///
261    /// Panics if a provider has not been set.
262    #[must_use]
263    pub fn build(self) -> AgentLoop<Ctx, P, DefaultHooks, InMemoryStore, InMemoryStore> {
264        let provider = self.provider.expect("provider is required");
265        let tools = self.tools.unwrap_or_default();
266        let config = self.config.unwrap_or_default();
267
268        AgentLoop {
269            provider: Arc::new(provider),
270            tools: Arc::new(tools),
271            hooks: Arc::new(DefaultHooks),
272            message_store: Arc::new(InMemoryStore::new()),
273            state_store: Arc::new(InMemoryStore::new()),
274            config,
275            compaction_config: self.compaction_config,
276            compactor: self.compactor,
277            execution_store: self.execution_store,
278        }
279    }
280}
281
282impl<Ctx, P, H, M, S> AgentLoopBuilder<Ctx, P, H, M, S>
283where
284    Ctx: Send + Sync + 'static,
285    P: LlmProvider + 'static,
286    H: AgentHooks + 'static,
287    M: MessageStore + 'static,
288    S: StateStore + 'static,
289{
290    /// Build the agent loop with all custom components.
291    ///
292    /// # Panics
293    ///
294    /// Panics if any of the following have not been set:
295    /// - `provider`
296    /// - `hooks`
297    /// - `message_store`
298    /// - `state_store`
299    #[must_use]
300    pub fn build_with_stores(self) -> AgentLoop<Ctx, P, H, M, S> {
301        let provider = self.provider.expect("provider is required");
302        let tools = self.tools.unwrap_or_default();
303        let hooks = self
304            .hooks
305            .expect("hooks is required when using build_with_stores");
306        let message_store = self
307            .message_store
308            .expect("message_store is required when using build_with_stores");
309        let state_store = self
310            .state_store
311            .expect("state_store is required when using build_with_stores");
312        let config = self.config.unwrap_or_default();
313
314        AgentLoop {
315            provider: Arc::new(provider),
316            tools: Arc::new(tools),
317            hooks: Arc::new(hooks),
318            message_store: Arc::new(message_store),
319            state_store: Arc::new(state_store),
320            config,
321            compaction_config: self.compaction_config,
322            compactor: self.compactor,
323            execution_store: self.execution_store,
324        }
325    }
326}