Skip to main content

tirea_agent_loop/runtime/loop_runner/
config.rs

1use super::tool_exec::{ParallelToolExecutor, SequentialToolExecutor};
2use super::AgentLoopError;
3use crate::contracts::plugin::AgentPlugin;
4use crate::contracts::runtime::{LlmExecutor, StopPolicy, ToolExecutor};
5use crate::contracts::tool::{Tool, ToolDescriptor};
6use crate::contracts::RunContext;
7use crate::contracts::StopConditionSpec;
8use async_trait::async_trait;
9use genai::chat::ChatOptions;
10use genai::Client;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14/// Retry strategy for LLM inference calls.
15#[derive(Debug, Clone)]
16pub struct LlmRetryPolicy {
17    /// Max attempts per model candidate (must be >= 1).
18    pub max_attempts_per_model: usize,
19    /// Initial backoff for retries in milliseconds.
20    pub initial_backoff_ms: u64,
21    /// Max backoff cap in milliseconds.
22    pub max_backoff_ms: u64,
23    /// Retry stream startup failures before any output is emitted.
24    pub retry_stream_start: bool,
25}
26
27impl Default for LlmRetryPolicy {
28    fn default() -> Self {
29        Self {
30            max_attempts_per_model: 2,
31            initial_backoff_ms: 250,
32            max_backoff_ms: 2_000,
33            retry_stream_start: true,
34        }
35    }
36}
37
38/// Input context passed to per-step tool providers.
39pub struct StepToolInput<'a> {
40    /// Current run context at step boundary.
41    pub state: &'a RunContext,
42}
43
44/// Tool snapshot resolved for one step.
45#[derive(Clone, Default)]
46pub struct StepToolSnapshot {
47    /// Concrete tool map used for this step.
48    pub tools: HashMap<String, Arc<dyn Tool>>,
49    /// Tool descriptors exposed to plugins/LLM for this step.
50    pub descriptors: Vec<ToolDescriptor>,
51}
52
53impl StepToolSnapshot {
54    /// Build a step snapshot from a concrete tool map.
55    pub fn from_tools(tools: HashMap<String, Arc<dyn Tool>>) -> Self {
56        let descriptors = tools
57            .values()
58            .map(|tool| tool.descriptor().clone())
59            .collect();
60        Self { tools, descriptors }
61    }
62}
63
64/// Provider that resolves the tool snapshot for each step.
65#[async_trait]
66pub trait StepToolProvider: Send + Sync {
67    /// Resolve tool map + descriptors for the current step.
68    async fn provide(&self, input: StepToolInput<'_>) -> Result<StepToolSnapshot, AgentLoopError>;
69}
70
71/// Default LLM executor backed by `genai::Client`.
72#[derive(Clone)]
73pub struct GenaiLlmExecutor {
74    client: Client,
75}
76
77impl GenaiLlmExecutor {
78    pub fn new(client: Client) -> Self {
79        Self { client }
80    }
81}
82
83impl std::fmt::Debug for GenaiLlmExecutor {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct("GenaiLlmExecutor").finish()
86    }
87}
88
89#[async_trait]
90impl LlmExecutor for GenaiLlmExecutor {
91    async fn exec_chat_response(
92        &self,
93        model: &str,
94        chat_req: genai::chat::ChatRequest,
95        options: Option<&ChatOptions>,
96    ) -> genai::Result<genai::chat::ChatResponse> {
97        self.client.exec_chat(model, chat_req, options).await
98    }
99
100    async fn exec_chat_stream_events(
101        &self,
102        model: &str,
103        chat_req: genai::chat::ChatRequest,
104        options: Option<&ChatOptions>,
105    ) -> genai::Result<crate::contracts::runtime::LlmEventStream> {
106        let resp = self
107            .client
108            .exec_chat_stream(model, chat_req, options)
109            .await?;
110        Ok(Box::pin(resp.stream))
111    }
112
113    fn name(&self) -> &'static str {
114        "genai_client"
115    }
116}
117
118/// Static provider that always returns the same tool map.
119#[derive(Clone, Default)]
120pub struct StaticStepToolProvider {
121    tools: HashMap<String, Arc<dyn Tool>>,
122}
123
124impl StaticStepToolProvider {
125    pub fn new(tools: HashMap<String, Arc<dyn Tool>>) -> Self {
126        Self { tools }
127    }
128}
129
130#[async_trait]
131impl StepToolProvider for StaticStepToolProvider {
132    async fn provide(&self, _input: StepToolInput<'_>) -> Result<StepToolSnapshot, AgentLoopError> {
133        Ok(StepToolSnapshot::from_tools(self.tools.clone()))
134    }
135}
136
137/// Runtime configuration for the agent loop.
138#[derive(Clone)]
139pub struct AgentConfig {
140    /// Unique identifier for this agent.
141    pub id: String,
142    /// Model identifier (e.g., "gpt-4", "claude-3-opus").
143    pub model: String,
144    /// System prompt for the LLM.
145    pub system_prompt: String,
146    /// Maximum number of tool call rounds before stopping.
147    pub max_rounds: usize,
148    /// Tool execution strategy (parallel, sequential, or custom).
149    pub tool_executor: Arc<dyn ToolExecutor>,
150    /// Chat options for the LLM.
151    pub chat_options: Option<ChatOptions>,
152    /// Fallback model ids used when the primary model fails.
153    ///
154    /// Evaluated in order after `model`.
155    pub fallback_models: Vec<String>,
156    /// Retry policy for LLM inference failures.
157    pub llm_retry_policy: LlmRetryPolicy,
158    /// Plugins to run during the agent loop.
159    pub plugins: Vec<Arc<dyn AgentPlugin>>,
160    /// Composable stop policies checked after each tool-call round.
161    ///
162    /// When empty (and `stop_condition_specs` is also empty), a default
163    /// [`crate::engine::stop_conditions::MaxRounds`] condition is created from `max_rounds`.
164    /// When non-empty, `max_rounds` is ignored.
165    pub stop_conditions: Vec<Arc<dyn StopPolicy>>,
166    /// Declarative stop condition specs, resolved to `Arc<dyn StopPolicy>`
167    /// at runtime.
168    ///
169    /// Specs are appended after explicit `stop_conditions` in evaluation order.
170    pub stop_condition_specs: Vec<StopConditionSpec>,
171    /// Optional per-step tool provider.
172    ///
173    /// When not set, the loop uses a static provider derived from the `tools`
174    /// map passed to `run_step` / `run_loop` / `run_loop_stream`.
175    pub step_tool_provider: Option<Arc<dyn StepToolProvider>>,
176    /// Optional LLM executor override.
177    ///
178    /// When not set, the loop uses [`GenaiLlmExecutor`] with `Client::default()`.
179    pub llm_executor: Option<Arc<dyn LlmExecutor>>,
180}
181
182impl Default for AgentConfig {
183    fn default() -> Self {
184        Self {
185            id: "default".to_string(),
186            model: "gpt-4o-mini".to_string(),
187            system_prompt: String::new(),
188            max_rounds: 10,
189            tool_executor: Arc::new(ParallelToolExecutor),
190            chat_options: Some(
191                ChatOptions::default()
192                    .with_capture_usage(true)
193                    .with_capture_reasoning_content(true)
194                    .with_capture_tool_calls(true),
195            ),
196            fallback_models: Vec::new(),
197            llm_retry_policy: LlmRetryPolicy::default(),
198            plugins: Vec::new(),
199            stop_conditions: Vec::new(),
200            stop_condition_specs: Vec::new(),
201            step_tool_provider: None,
202            llm_executor: None,
203        }
204    }
205}
206
207impl std::fmt::Debug for AgentConfig {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        f.debug_struct("AgentConfig")
210            .field("id", &self.id)
211            .field("model", &self.model)
212            .field(
213                "system_prompt",
214                &format!("[{} chars]", self.system_prompt.len()),
215            )
216            .field("max_rounds", &self.max_rounds)
217            .field("tool_executor", &self.tool_executor.name())
218            .field("chat_options", &self.chat_options)
219            .field("fallback_models", &self.fallback_models)
220            .field("llm_retry_policy", &self.llm_retry_policy)
221            .field("plugins", &format!("[{} plugins]", self.plugins.len()))
222            .field(
223                "stop_conditions",
224                &format!("[{} conditions]", self.stop_conditions.len()),
225            )
226            .field("stop_condition_specs", &self.stop_condition_specs)
227            .field(
228                "step_tool_provider",
229                &self.step_tool_provider.as_ref().map(|_| "<set>"),
230            )
231            .field(
232                "llm_executor",
233                &self
234                    .llm_executor
235                    .as_ref()
236                    .map(|executor| executor.name())
237                    .unwrap_or("genai_client(default)"),
238            )
239            .finish()
240    }
241}
242
243impl AgentConfig {
244    tirea_contract::impl_shared_agent_builder_methods!();
245    tirea_contract::impl_loop_config_builder_methods!();
246
247    /// Set tool executor strategy.
248    #[must_use]
249    pub fn with_tool_executor(mut self, executor: Arc<dyn ToolExecutor>) -> Self {
250        self.tool_executor = executor;
251        self
252    }
253
254    /// Set parallel tool execution convenience strategy.
255    #[must_use]
256    pub fn with_parallel_tools(mut self, parallel: bool) -> Self {
257        self.tool_executor = if parallel {
258            Arc::new(ParallelToolExecutor)
259        } else {
260            Arc::new(SequentialToolExecutor)
261        };
262        self
263    }
264
265    /// Set static tool map (wraps in [`StaticStepToolProvider`]).
266    ///
267    /// Prefer passing tools directly to [`run_loop`] / [`run_loop_stream`];
268    /// use this only when you need to set tools via `step_tool_provider`.
269    #[must_use]
270    pub fn with_tools(self, tools: HashMap<String, Arc<dyn Tool>>) -> Self {
271        self.with_step_tool_provider(Arc::new(StaticStepToolProvider::new(tools)))
272    }
273
274    /// Set per-step tool provider.
275    #[must_use]
276    pub fn with_step_tool_provider(mut self, provider: Arc<dyn StepToolProvider>) -> Self {
277        self.step_tool_provider = Some(provider);
278        self
279    }
280
281    /// Set LLM executor.
282    #[must_use]
283    pub fn with_llm_executor(mut self, executor: Arc<dyn LlmExecutor>) -> Self {
284        self.llm_executor = Some(executor);
285        self
286    }
287
288    /// Check if any plugins are configured.
289    pub fn has_plugins(&self) -> bool {
290        !self.plugins.is_empty()
291    }
292}