Skip to main content

agent_sdk/agent_loop/
builder.rs

1use crate::authority::EventAuthority;
2use crate::context::{CompactionConfig, ContextCompactor};
3use crate::hooks::{AgentHooks, DefaultHooks};
4use crate::llm::LlmProvider;
5#[cfg(feature = "skills")]
6use crate::skills::Skill;
7use crate::stores::{EventStore, InMemoryStore, MessageStore, StateStore, ToolExecutionStore};
8use crate::tools::ToolRegistry;
9use crate::types::AgentConfig;
10use std::sync::Arc;
11
12use super::AgentLoop;
13
14/// Builder for constructing an `AgentLoop`.
15///
16/// # Example
17///
18/// ```ignore
19/// let agent = AgentLoop::builder()
20///     .provider(my_provider)
21///     .tools(my_tools)
22///     .config(AgentConfig::default())
23///     .build();
24/// ```
25pub struct AgentLoopBuilder<Ctx, P, H, M, S> {
26    provider: Option<P>,
27    tools: Option<ToolRegistry<Ctx>>,
28    hooks: Option<H>,
29    message_store: Option<M>,
30    state_store: Option<S>,
31    event_store: Option<Arc<dyn EventStore>>,
32    event_authority: Option<Arc<dyn EventAuthority>>,
33    config: Option<AgentConfig>,
34    compaction_config: Option<CompactionConfig>,
35    compactor: Option<Arc<dyn ContextCompactor>>,
36    execution_store: Option<Arc<dyn ToolExecutionStore>>,
37    audit_sink: Option<Arc<dyn crate::hooks::ToolAuditSink>>,
38    #[cfg(feature = "otel")]
39    observability_store: Option<Arc<dyn crate::observability::ObservabilityStore>>,
40}
41
42impl<Ctx> AgentLoopBuilder<Ctx, (), (), (), ()> {
43    /// Create a new builder with no components set.
44    #[must_use]
45    pub fn new() -> Self {
46        Self {
47            provider: None,
48            tools: None,
49            hooks: None,
50            message_store: None,
51            state_store: None,
52            event_store: None,
53            event_authority: None,
54            config: None,
55            compaction_config: None,
56            compactor: None,
57            execution_store: None,
58            audit_sink: None,
59            #[cfg(feature = "otel")]
60            observability_store: None,
61        }
62    }
63}
64
65impl<Ctx> Default for AgentLoopBuilder<Ctx, (), (), (), ()> {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl<Ctx, P, H, M, S> AgentLoopBuilder<Ctx, P, H, M, S> {
72    /// Set the LLM provider.
73    #[must_use]
74    pub fn provider<P2: LlmProvider>(self, provider: P2) -> AgentLoopBuilder<Ctx, P2, H, M, S> {
75        AgentLoopBuilder {
76            provider: Some(provider),
77            tools: self.tools,
78            hooks: self.hooks,
79            message_store: self.message_store,
80            state_store: self.state_store,
81            event_store: self.event_store,
82            event_authority: self.event_authority,
83            config: self.config,
84            compaction_config: self.compaction_config,
85            compactor: self.compactor,
86            execution_store: self.execution_store,
87            audit_sink: self.audit_sink,
88            #[cfg(feature = "otel")]
89            observability_store: self.observability_store,
90        }
91    }
92
93    /// Set the tool registry.
94    #[must_use]
95    pub fn tools(mut self, tools: ToolRegistry<Ctx>) -> Self {
96        self.tools = Some(tools);
97        self
98    }
99
100    /// Set the agent hooks.
101    #[must_use]
102    pub fn hooks<H2: AgentHooks>(self, hooks: H2) -> AgentLoopBuilder<Ctx, P, H2, M, S> {
103        AgentLoopBuilder {
104            provider: self.provider,
105            tools: self.tools,
106            hooks: Some(hooks),
107            message_store: self.message_store,
108            state_store: self.state_store,
109            event_store: self.event_store,
110            event_authority: self.event_authority,
111            config: self.config,
112            compaction_config: self.compaction_config,
113            compactor: self.compactor,
114            execution_store: self.execution_store,
115            audit_sink: self.audit_sink,
116            #[cfg(feature = "otel")]
117            observability_store: self.observability_store,
118        }
119    }
120
121    /// Set the message store.
122    #[must_use]
123    pub fn message_store<M2: MessageStore>(
124        self,
125        message_store: M2,
126    ) -> AgentLoopBuilder<Ctx, P, H, M2, S> {
127        AgentLoopBuilder {
128            provider: self.provider,
129            tools: self.tools,
130            hooks: self.hooks,
131            message_store: Some(message_store),
132            state_store: self.state_store,
133            event_store: self.event_store,
134            event_authority: self.event_authority,
135            config: self.config,
136            compaction_config: self.compaction_config,
137            compactor: self.compactor,
138            execution_store: self.execution_store,
139            audit_sink: self.audit_sink,
140            #[cfg(feature = "otel")]
141            observability_store: self.observability_store,
142        }
143    }
144
145    /// Set the state store.
146    #[must_use]
147    pub fn state_store<S2: StateStore>(
148        self,
149        state_store: S2,
150    ) -> AgentLoopBuilder<Ctx, P, H, M, S2> {
151        AgentLoopBuilder {
152            provider: self.provider,
153            tools: self.tools,
154            hooks: self.hooks,
155            message_store: self.message_store,
156            state_store: Some(state_store),
157            event_store: self.event_store,
158            event_authority: self.event_authority,
159            config: self.config,
160            compaction_config: self.compaction_config,
161            compactor: self.compactor,
162            execution_store: self.execution_store,
163            audit_sink: self.audit_sink,
164            #[cfg(feature = "otel")]
165            observability_store: self.observability_store,
166        }
167    }
168
169    /// Set the authoritative event store for the loop lifecycle.
170    #[must_use]
171    pub fn event_store(mut self, store: Arc<dyn EventStore>) -> Self {
172        self.event_store = Some(store);
173        self
174    }
175
176    /// Set the event authority for envelope creation.
177    ///
178    /// When set, the authority governs how events are wrapped in envelopes
179    /// (sequence numbers, event IDs, timestamps).  In server mode the
180    /// authority seeds sequences from durable storage so ordering is
181    /// continuous across turns within a thread.
182    ///
183    /// When not set, a fresh [`LocalEventAuthority`](crate::authority::LocalEventAuthority)
184    /// starting at sequence 0 is created for each run.
185    #[must_use]
186    pub fn event_authority(mut self, authority: Arc<dyn EventAuthority>) -> Self {
187        self.event_authority = Some(authority);
188        self
189    }
190
191    /// Set the execution store for tool idempotency.
192    ///
193    /// When set, tool executions will be tracked using a write-ahead pattern:
194    /// 1. Record execution intent BEFORE calling the tool
195    /// 2. Update with result AFTER completion
196    /// 3. On retry, return cached result if execution already completed
197    ///
198    /// # Example
199    ///
200    /// ```ignore
201    /// use agent_sdk::{builder, stores::InMemoryExecutionStore};
202    ///
203    /// let agent = builder()
204    ///     .provider(my_provider)
205    ///     .execution_store(InMemoryExecutionStore::new())
206    ///     .build();
207    /// ```
208    #[must_use]
209    pub fn execution_store(mut self, store: impl ToolExecutionStore + 'static) -> Self {
210        self.execution_store = Some(Arc::new(store));
211        self
212    }
213
214    /// Set the execution store from a shared `Arc`.
215    ///
216    /// Use this when the caller needs to retain a handle to the store
217    /// (for inspection, pre-population, or sharing across loops). See
218    /// [`Self::execution_store`] for the standard owned form.
219    #[must_use]
220    pub fn execution_store_shared(mut self, store: Arc<dyn ToolExecutionStore>) -> Self {
221        self.execution_store = Some(store);
222        self
223    }
224
225    /// Set the authoritative tool audit sink.
226    ///
227    /// When set, the agent loop emits a
228    /// [`ToolAuditRecord`](crate::advanced::ToolAuditRecord) at every tool
229    /// lifecycle transition — blocked, requires-confirmation, cached,
230    /// replayed, invalidated, completed, and persistence-failed. This
231    /// gives servers a complete audit trail without relying on the weaker
232    /// `post_tool_use` hook.
233    ///
234    /// Defaults to [`NoopAuditSink`](crate::hooks::NoopAuditSink) when
235    /// not set.
236    #[must_use]
237    pub fn audit_sink(mut self, sink: impl crate::hooks::ToolAuditSink + 'static) -> Self {
238        self.audit_sink = Some(Arc::new(sink));
239        self
240    }
241
242    /// Set the audit sink from a shared `Arc`.
243    ///
244    /// Use this when the caller needs to retain a handle to the sink
245    /// (e.g. to inspect captured records from tests, or to share a
246    /// single durable sink across multiple agent loops). Passing an
247    /// `Arc<dyn ToolAuditSink>` here avoids the `Arc<Arc<S>>` double
248    /// wrap that happens when callers `Arc::clone(&sink)` a sink they
249    /// already wrapped and hand it to [`Self::audit_sink`].
250    ///
251    /// See [`Self::audit_sink`] for the standard owned form.
252    #[must_use]
253    pub fn audit_sink_shared(mut self, sink: Arc<dyn crate::hooks::ToolAuditSink>) -> Self {
254        self.audit_sink = Some(sink);
255        self
256    }
257
258    /// Set the observability store for `GenAI` payload capture.
259    #[cfg(feature = "otel")]
260    #[must_use]
261    pub fn observability_store(
262        mut self,
263        store: impl crate::observability::ObservabilityStore + 'static,
264    ) -> Self {
265        self.observability_store = Some(Arc::new(store));
266        self
267    }
268
269    /// Set the agent configuration.
270    #[must_use]
271    pub fn config(mut self, config: AgentConfig) -> Self {
272        self.config = Some(config);
273        self
274    }
275
276    /// Enable context compaction with the given configuration.
277    ///
278    /// When enabled, the agent will automatically compact conversation history
279    /// when it exceeds the configured token threshold.
280    ///
281    /// # Example
282    ///
283    /// ```ignore
284    /// use agent_sdk::{builder, context::CompactionConfig};
285    ///
286    /// let agent = builder()
287    ///     .provider(my_provider)
288    ///     .with_compaction(CompactionConfig::default())
289    ///     .build();
290    /// ```
291    #[must_use]
292    pub const fn with_compaction(mut self, config: CompactionConfig) -> Self {
293        self.compaction_config = Some(config);
294        self
295    }
296
297    /// Enable context compaction with default settings.
298    ///
299    /// This is a convenience method equivalent to:
300    /// ```ignore
301    /// builder.with_compaction(CompactionConfig::default())
302    /// ```
303    #[must_use]
304    pub fn with_auto_compaction(self) -> Self {
305        self.with_compaction(CompactionConfig::default())
306    }
307
308    /// Override the default compactor with a custom implementation.
309    #[must_use]
310    pub fn with_custom_compactor(mut self, compactor: impl ContextCompactor + 'static) -> Self {
311        self.compactor = Some(Arc::new(compactor));
312        self
313    }
314
315    /// Apply a skill configuration.
316    ///
317    /// This merges the skill's system prompt with the existing configuration
318    /// and filters tools based on the skill's allowed/denied lists.
319    ///
320    /// Available when the `skills` feature is enabled.
321    ///
322    /// # Example
323    ///
324    /// ```ignore
325    /// let skill = Skill::new("code-review", "You are a code reviewer...")
326    ///     .with_denied_tools(vec!["bash".into()]);
327    ///
328    /// let agent = builder()
329    ///     .provider(provider)
330    ///     .tools(tools)
331    ///     .with_skill(skill)
332    ///     .build();
333    /// ```
334    #[cfg(feature = "skills")]
335    #[must_use]
336    pub fn with_skill(mut self, skill: Skill) -> Self
337    where
338        Ctx: Send + Sync + 'static,
339    {
340        // Filter tools based on skill configuration first (before moving skill)
341        if let Some(ref mut tools) = self.tools {
342            tools.filter(|name| skill.is_tool_allowed(name));
343        }
344
345        // Merge system prompt
346        let mut config = self.config.take().unwrap_or_default();
347        if config.system_prompt.is_empty() {
348            config.system_prompt = skill.system_prompt;
349        } else {
350            config.system_prompt = format!("{}\n\n{}", config.system_prompt, skill.system_prompt);
351        }
352        self.config = Some(config);
353
354        self
355    }
356}
357
358impl<Ctx, P> AgentLoopBuilder<Ctx, P, (), (), ()>
359where
360    Ctx: Send + Sync + 'static,
361    P: LlmProvider + 'static,
362{
363    /// Build the agent loop with default hooks and in-memory message/state stores.
364    ///
365    /// This is a convenience method that uses:
366    /// - `DefaultHooks` for hooks
367    /// - `InMemoryStore` for message store
368    /// - `InMemoryStore` for state store
369    /// - `InMemoryEventStore` for the event store, when none was set
370    /// - `AgentConfig::default()` if no config is set
371    ///
372    /// Supplying an [`event_store`](Self::event_store) is optional for this
373    /// convenience build — a fresh [`InMemoryEventStore`](crate::InMemoryEventStore)
374    /// is used by default so the 30-second path needs no `Arc` ceremony. Wire
375    /// a durable store explicitly when you need persistence across process
376    /// restarts.
377    ///
378    /// # Panics
379    ///
380    /// Panics if a provider has not been set.
381    #[must_use]
382    pub fn build(self) -> AgentLoop<Ctx, P, DefaultHooks, InMemoryStore, InMemoryStore> {
383        let Some(provider) = self.provider else {
384            panic!("provider is required");
385        };
386        let event_store = self
387            .event_store
388            .unwrap_or_else(|| Arc::new(crate::stores::InMemoryEventStore::new()));
389        let tools = self.tools.unwrap_or_default();
390        let config = self.config.unwrap_or_default();
391
392        AgentLoop {
393            provider: Arc::new(provider),
394            tools: Arc::new(tools),
395            hooks: Arc::new(DefaultHooks),
396            message_store: Arc::new(InMemoryStore::new()),
397            state_store: Arc::new(InMemoryStore::new()),
398            event_store,
399            event_authority: self.event_authority,
400            config,
401            compaction_config: self.compaction_config,
402            compactor: self.compactor,
403            execution_store: self.execution_store,
404            audit_sink: self
405                .audit_sink
406                .unwrap_or_else(|| Arc::new(crate::hooks::NoopAuditSink)),
407            #[cfg(feature = "otel")]
408            observability_store: self.observability_store,
409        }
410    }
411}
412
413impl<Ctx, P, H, M, S> AgentLoopBuilder<Ctx, P, H, M, S>
414where
415    Ctx: Send + Sync + 'static,
416    P: LlmProvider + 'static,
417    H: AgentHooks + 'static,
418    M: MessageStore + 'static,
419    S: StateStore + 'static,
420{
421    /// Build the agent loop with all custom components.
422    ///
423    /// # Panics
424    ///
425    /// Panics if any of the following have not been set:
426    /// - `provider`
427    /// - `hooks`
428    /// - `message_store`
429    /// - `state_store`
430    /// - `event_store`
431    #[must_use]
432    pub fn build_with_stores(self) -> AgentLoop<Ctx, P, H, M, S> {
433        let Some(provider) = self.provider else {
434            panic!("provider is required");
435        };
436        let tools = self.tools.unwrap_or_default();
437        let Some(hooks) = self.hooks else {
438            panic!("hooks is required when using build_with_stores");
439        };
440        let Some(message_store) = self.message_store else {
441            panic!("message_store is required when using build_with_stores");
442        };
443        let Some(state_store) = self.state_store else {
444            panic!("state_store is required when using build_with_stores");
445        };
446        let Some(event_store) = self.event_store else {
447            panic!("event_store is required when using build_with_stores");
448        };
449        let config = self.config.unwrap_or_default();
450
451        AgentLoop {
452            provider: Arc::new(provider),
453            tools: Arc::new(tools),
454            hooks: Arc::new(hooks),
455            message_store: Arc::new(message_store),
456            state_store: Arc::new(state_store),
457            event_store,
458            event_authority: self.event_authority,
459            config,
460            compaction_config: self.compaction_config,
461            compactor: self.compactor,
462            execution_store: self.execution_store,
463            audit_sink: self
464                .audit_sink
465                .unwrap_or_else(|| Arc::new(crate::hooks::NoopAuditSink)),
466            #[cfg(feature = "otel")]
467            observability_store: self.observability_store,
468        }
469    }
470}