tirea_agent_loop/runtime/loop_runner/
config.rs1use 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#[derive(Debug, Clone)]
16pub struct LlmRetryPolicy {
17 pub max_attempts_per_model: usize,
19 pub initial_backoff_ms: u64,
21 pub max_backoff_ms: u64,
23 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
38pub struct StepToolInput<'a> {
40 pub state: &'a RunContext,
42}
43
44#[derive(Clone, Default)]
46pub struct StepToolSnapshot {
47 pub tools: HashMap<String, Arc<dyn Tool>>,
49 pub descriptors: Vec<ToolDescriptor>,
51}
52
53impl StepToolSnapshot {
54 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#[async_trait]
66pub trait StepToolProvider: Send + Sync {
67 async fn provide(&self, input: StepToolInput<'_>) -> Result<StepToolSnapshot, AgentLoopError>;
69}
70
71#[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#[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#[derive(Clone)]
139pub struct AgentConfig {
140 pub id: String,
142 pub model: String,
144 pub system_prompt: String,
146 pub max_rounds: usize,
148 pub tool_executor: Arc<dyn ToolExecutor>,
150 pub chat_options: Option<ChatOptions>,
152 pub fallback_models: Vec<String>,
156 pub llm_retry_policy: LlmRetryPolicy,
158 pub plugins: Vec<Arc<dyn AgentPlugin>>,
160 pub stop_conditions: Vec<Arc<dyn StopPolicy>>,
166 pub stop_condition_specs: Vec<StopConditionSpec>,
171 pub step_tool_provider: Option<Arc<dyn StepToolProvider>>,
176 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 #[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 #[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 #[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 #[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 #[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 pub fn has_plugins(&self) -> bool {
290 !self.plugins.is_empty()
291 }
292}