tirea_agentos/runtime/
resolve.rs

1use super::agent_tools::{
2    AgentOutputTool, AgentRecoveryPlugin, AgentRunTool, AgentStopTool, AgentToolsPlugin,
3    AGENT_RECOVERY_PLUGIN_ID, AGENT_TOOLS_PLUGIN_ID,
4};
5use super::background_tasks::{
6    BackgroundTasksPlugin, TaskCancelTool, TaskOutputTool, TaskStatusTool,
7    BACKGROUND_TASKS_PLUGIN_ID,
8};
9use super::context::{policy_for_model, ContextPlugin, CONTEXT_PLUGIN_ID};
10use super::policy::{filter_tools_in_place, set_runtime_policy_from_definition_if_absent};
11#[cfg(feature = "skills")]
12pub(crate) use super::skills_wiring::SkillsSystemWiring;
13use super::stop_policy::{StopPolicyPlugin, STOP_POLICY_PLUGIN_ID};
14use super::{behavior::CompositeBehavior, AgentOs, AgentOsResolveError, StopPolicy};
15use crate::composition::{
16    AgentCatalog, AgentDefinition, AgentOsBuilder, AgentOsWiringError, AgentRegistry,
17    InMemoryAgentCatalog, InMemoryAgentRegistry, RegistryBundle, StopConditionSpec, SystemWiring,
18    ToolBehaviorBundle, ToolExecutionMode, WiringContext,
19};
20use crate::contracts::runtime::behavior::{AgentBehavior, NoOpBehavior};
21use crate::contracts::runtime::tool_call::Tool;
22use crate::contracts::runtime::ToolExecutor;
23use crate::contracts::RunPolicy;
24#[cfg(feature = "skills")]
25use crate::extensions::skills::{InMemorySkillRegistry, Skill, SkillRegistry};
26use crate::runtime::loop_runner::{
27    BaseAgent, GenaiLlmExecutor, LlmExecutor, ParallelToolExecutor, ResolvedRun,
28    SequentialToolExecutor,
29};
30use genai::{chat::ChatOptions, Client};
31use std::collections::HashMap;
32use std::sync::Arc;
33use tirea_contract::runtime::state::{StateActionDeserializerRegistry, StateScopeRegistry};
34use tirea_state::LatticeRegistry;
35
36use super::bundle_merge::{ensure_unique_behavior_ids, merge_wiring_bundles, ResolvedBehaviors};
37
38#[derive(Clone)]
39struct ResolvedModelRuntime {
40    model: String,
41    chat_options: Option<ChatOptions>,
42    llm_executor: Arc<dyn LlmExecutor>,
43}
44
45// ---------------------------------------------------------------------------
46// AgentOs wiring implementation
47// ---------------------------------------------------------------------------
48
49impl AgentOs {
50    pub fn builder() -> AgentOsBuilder {
51        AgentOsBuilder::new()
52    }
53
54    pub fn client(&self) -> Client {
55        self.default_client.clone()
56    }
57
58    #[cfg(feature = "skills")]
59    pub fn skill_list(&self) -> Option<Vec<Arc<dyn Skill>>> {
60        self.skills_registry.as_ref().map(|registry| {
61            let mut skills: Vec<Arc<dyn Skill>> = registry.snapshot().into_values().collect();
62            skills.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));
63            skills
64        })
65    }
66
67    pub(crate) fn agent_catalog(&self) -> Arc<dyn AgentCatalog> {
68        self.agent_catalog.clone()
69    }
70
71    pub fn agent(&self, agent_id: &str) -> Option<AgentDefinition> {
72        self.agents.get(agent_id)
73    }
74
75    /// Return all registered agent ids in stable order.
76    pub fn agent_ids(&self) -> Vec<String> {
77        let mut ids = self.agents.ids();
78        ids.sort();
79        ids
80    }
81
82    pub fn tools(&self) -> HashMap<String, Arc<dyn Tool>> {
83        self.base_tools.snapshot()
84    }
85
86    /// Collect reserved behavior IDs from all system wirings + internal IDs.
87    pub(crate) fn reserved_behavior_ids(
88        system_wirings: &[Arc<dyn SystemWiring>],
89    ) -> Vec<&'static str> {
90        let mut ids = vec![
91            AGENT_TOOLS_PLUGIN_ID,
92            AGENT_RECOVERY_PLUGIN_ID,
93            BACKGROUND_TASKS_PLUGIN_ID,
94            CONTEXT_PLUGIN_ID,
95            STOP_POLICY_PLUGIN_ID,
96        ];
97        for wiring in system_wirings {
98            ids.extend_from_slice(wiring.reserved_behavior_ids());
99        }
100        ids
101    }
102
103    fn resolve_behavior_id_list(
104        &self,
105        behavior_ids: &[String],
106    ) -> Result<Vec<Arc<dyn AgentBehavior>>, AgentOsWiringError> {
107        let reserved = Self::reserved_behavior_ids(&self.system_wirings);
108        let mut out: Vec<Arc<dyn AgentBehavior>> = Vec::new();
109        for id in behavior_ids {
110            let id = id.trim();
111            if reserved.contains(&id) {
112                return Err(AgentOsWiringError::ReservedBehaviorId(id.to_string()));
113            }
114            let p = self
115                .behaviors
116                .get(id)
117                .ok_or_else(|| AgentOsWiringError::BehaviorNotFound(id.to_string()))?;
118            out.push(p);
119        }
120        Ok(out)
121    }
122
123    fn resolve_stop_condition_id_list(
124        &self,
125        stop_condition_ids: &[String],
126    ) -> Result<Vec<Arc<dyn StopPolicy>>, AgentOsWiringError> {
127        let mut out = Vec::new();
128        for id in stop_condition_ids {
129            let id = id.trim();
130            let p = self
131                .stop_policies
132                .get(id)
133                .ok_or_else(|| AgentOsWiringError::StopConditionNotFound(id.to_string()))?;
134            out.push(p);
135        }
136        Ok(out)
137    }
138
139    fn ensure_agent_tools_plugin_not_installed(
140        plugins: &[Arc<dyn AgentBehavior>],
141    ) -> Result<(), AgentOsWiringError> {
142        for existing in plugins.iter().map(|p| p.id()) {
143            if existing == AGENT_TOOLS_PLUGIN_ID {
144                return Err(AgentOsWiringError::AgentToolsBehaviorAlreadyInstalled(
145                    existing.to_string(),
146                ));
147            }
148            if existing == AGENT_RECOVERY_PLUGIN_ID {
149                return Err(AgentOsWiringError::AgentRecoveryBehaviorAlreadyInstalled(
150                    existing.to_string(),
151                ));
152            }
153        }
154        Ok(())
155    }
156
157    fn freeze_agent_registry(&self) -> Arc<dyn AgentRegistry> {
158        let mut frozen = InMemoryAgentRegistry::new();
159        frozen.extend_upsert(self.agents.snapshot());
160        Arc::new(frozen)
161    }
162
163    fn freeze_agent_catalog(&self) -> Arc<dyn AgentCatalog> {
164        let mut frozen = InMemoryAgentCatalog::new();
165        frozen.extend_upsert(self.agent_catalog.snapshot());
166        Arc::new(frozen)
167    }
168
169    #[cfg(feature = "skills")]
170    fn freeze_skill_registry(&self) -> Option<Arc<dyn SkillRegistry>> {
171        self.skills_registry.as_ref().map(|registry| {
172            let mut frozen = InMemorySkillRegistry::new();
173            frozen.extend_upsert(registry.snapshot().into_values().collect());
174            Arc::new(frozen) as Arc<dyn SkillRegistry>
175        })
176    }
177
178    fn background_task_store(&self) -> Option<Arc<super::background_tasks::TaskStore>> {
179        self.agent_state_store
180            .as_ref()
181            .map(|store| Arc::new(super::background_tasks::TaskStore::new(store.clone())))
182    }
183
184    fn with_registry_overrides(
185        &self,
186        agents: Arc<dyn AgentRegistry>,
187        agent_catalog: Arc<dyn AgentCatalog>,
188        #[cfg(feature = "skills")] skills_registry: Option<Arc<dyn SkillRegistry>>,
189    ) -> Self {
190        let mut cloned = self.clone();
191        cloned.agents = agents;
192        cloned.agent_catalog = agent_catalog;
193        #[cfg(feature = "skills")]
194        {
195            cloned.skills_registry = skills_registry;
196        }
197        cloned
198    }
199
200    fn build_agent_tool_wiring_bundles(
201        &self,
202        resolved_plugins: &[Arc<dyn AgentBehavior>],
203        agents_registry: Arc<dyn AgentRegistry>,
204    ) -> Result<Vec<Arc<dyn RegistryBundle>>, AgentOsWiringError> {
205        Self::ensure_agent_tools_plugin_not_installed(resolved_plugins)?;
206
207        #[cfg(feature = "skills")]
208        let pinned_os = {
209            let frozen_skills = self.freeze_skill_registry();
210            let frozen_agent_catalog = self.freeze_agent_catalog();
211            self.with_registry_overrides(
212                agents_registry.clone(),
213                frozen_agent_catalog,
214                frozen_skills,
215            )
216        };
217        #[cfg(not(feature = "skills"))]
218        let pinned_os = {
219            let frozen_agent_catalog = self.freeze_agent_catalog();
220            self.with_registry_overrides(agents_registry.clone(), frozen_agent_catalog)
221        };
222
223        let run_tool: Arc<dyn Tool> = Arc::new(AgentRunTool::new(
224            pinned_os.clone(),
225            self.sub_agent_handles.clone(),
226        ));
227        let stop_tool: Arc<dyn Tool> = Arc::new(AgentStopTool::with_os(
228            pinned_os.clone(),
229            self.sub_agent_handles.clone(),
230        ));
231        let output_tool: Arc<dyn Tool> = Arc::new(AgentOutputTool::new(pinned_os));
232        let task_store = self.background_task_store();
233        let task_status_tool: Arc<dyn Tool> = Arc::new(
234            TaskStatusTool::new(self.background_tasks.clone()).with_task_store(task_store.clone()),
235        );
236        let task_cancel_tool: Arc<dyn Tool> = Arc::new(
237            TaskCancelTool::new(self.background_tasks.clone()).with_task_store(task_store.clone()),
238        );
239        let task_output_tool: Arc<dyn Tool> = Arc::new(
240            TaskOutputTool::new(
241                self.background_tasks.clone(),
242                self.agent_state_store.clone(),
243            )
244            .with_task_store(task_store.clone()),
245        );
246
247        let tools_plugin =
248            AgentToolsPlugin::new(self.freeze_agent_catalog(), self.sub_agent_handles.clone())
249                .with_limits(
250                    self.agent_tools.discovery_max_entries,
251                    self.agent_tools.discovery_max_chars,
252                );
253        let recovery_plugin = AgentRecoveryPlugin::new(self.sub_agent_handles.clone());
254        let background_tasks_plugin =
255            BackgroundTasksPlugin::new(self.background_tasks.clone()).with_task_store(task_store);
256
257        let tools_bundle: Arc<dyn RegistryBundle> = Arc::new(
258            ToolBehaviorBundle::new(AGENT_TOOLS_PLUGIN_ID)
259                .with_tool(run_tool)
260                .with_tool(stop_tool)
261                .with_tool(output_tool)
262                .with_behavior(Arc::new(tools_plugin)),
263        );
264        let recovery_bundle: Arc<dyn RegistryBundle> = Arc::new(
265            ToolBehaviorBundle::new(AGENT_RECOVERY_PLUGIN_ID)
266                .with_behavior(Arc::new(recovery_plugin)),
267        );
268        let background_tasks_bundle: Arc<dyn RegistryBundle> = Arc::new(
269            ToolBehaviorBundle::new(BACKGROUND_TASKS_PLUGIN_ID)
270                .with_tool(task_status_tool)
271                .with_tool(task_cancel_tool)
272                .with_tool(task_output_tool)
273                .with_behavior(Arc::new(background_tasks_plugin)),
274        );
275
276        Ok(vec![tools_bundle, recovery_bundle, background_tasks_bundle])
277    }
278
279    #[cfg(test)]
280    pub(crate) fn wire_behaviors_into(
281        &self,
282        definition: AgentDefinition,
283    ) -> Result<Vec<Arc<dyn AgentBehavior>>, AgentOsWiringError> {
284        if definition.behavior_ids.is_empty() {
285            return Ok(Vec::new());
286        }
287
288        let resolved_plugins = self.resolve_behavior_id_list(&definition.behavior_ids)?;
289        ResolvedBehaviors::default()
290            .with_agent_default(resolved_plugins)
291            .into_plugins()
292    }
293
294    fn wire_into(
295        &self,
296        definition: AgentDefinition,
297        tools: &mut HashMap<String, Arc<dyn Tool>>,
298        model_runtime: &ResolvedModelRuntime,
299    ) -> Result<BaseAgent, AgentOsWiringError> {
300        let resolved_plugins = self.resolve_behavior_id_list(&definition.behavior_ids)?;
301        let frozen_agents = self.freeze_agent_registry();
302
303        // Run all system wirings generically.
304        let wiring_ctx = WiringContext {
305            resolved_behaviors: &resolved_plugins,
306            existing_tools: tools,
307            agent_definition: &definition,
308        };
309        let mut system_bundles = Vec::new();
310        for wiring in &self.system_wirings {
311            let bundles = wiring.wire(&wiring_ctx)?;
312            system_bundles.extend(bundles);
313        }
314
315        // Agent tools stay hardcoded (internal, needs &self/AgentOs access).
316        system_bundles
317            .extend(self.build_agent_tool_wiring_bundles(&resolved_plugins, frozen_agents)?);
318
319        let system_plugins = merge_wiring_bundles(&system_bundles, tools)?;
320        let mut all_plugins = ResolvedBehaviors::default()
321            .with_global(system_plugins)
322            .with_agent_default(resolved_plugins)
323            .into_plugins()?;
324
325        // Context plugin: logical compression (compaction) + hard truncation.
326        let context_policy = policy_for_model(&model_runtime.model);
327        all_plugins.push(Arc::new(
328            ContextPlugin::new(context_policy).with_llm_summarizer(
329                model_runtime.model.clone(),
330                model_runtime.llm_executor.clone(),
331                model_runtime.chat_options.clone(),
332            ),
333        ));
334
335        // Resolve stop conditions from stop_condition_ids
336        let stop_conditions =
337            self.resolve_stop_condition_id_list(&definition.stop_condition_ids)?;
338        let specs = synthesize_stop_specs(&definition);
339        let stop_plugin = StopPolicyPlugin::new(stop_conditions, specs);
340        if !stop_plugin.is_empty() {
341            all_plugins.push(Arc::new(stop_plugin));
342            ensure_unique_behavior_ids(&all_plugins)?;
343        }
344
345        Ok(build_base_agent_from_definition(definition, all_plugins))
346    }
347
348    fn resolve_model_runtime(
349        &self,
350        definition: &AgentDefinition,
351    ) -> Result<ResolvedModelRuntime, AgentOsResolveError> {
352        if self.models.is_empty() {
353            return Ok(ResolvedModelRuntime {
354                model: definition.model.clone(),
355                chat_options: definition.chat_options.clone(),
356                llm_executor: Arc::new(GenaiLlmExecutor::new(self.default_client.clone())),
357            });
358        }
359
360        let Some(def) = self.models.get(&definition.model) else {
361            return Err(AgentOsResolveError::ModelNotFound(definition.model.clone()));
362        };
363
364        let Some(client) = self.providers.get(&def.provider) else {
365            return Err(AgentOsResolveError::ProviderNotFound {
366                provider_id: def.provider.clone(),
367                model_id: definition.model.clone(),
368            });
369        };
370
371        Ok(ResolvedModelRuntime {
372            model: def.model.clone(),
373            chat_options: def
374                .chat_options
375                .clone()
376                .or_else(|| definition.chat_options.clone()),
377            llm_executor: Arc::new(GenaiLlmExecutor::new(client)),
378        })
379    }
380
381    #[cfg(all(test, feature = "skills"))]
382    pub(crate) fn wire_skills_into(
383        &self,
384        definition: AgentDefinition,
385        tools: &mut HashMap<String, Arc<dyn Tool>>,
386    ) -> Result<BaseAgent, AgentOsWiringError> {
387        let resolved_plugins = self.resolve_behavior_id_list(&definition.behavior_ids)?;
388
389        // Build skills wiring via SystemWiring iteration (only skills wirings apply).
390        let wiring_ctx = WiringContext {
391            resolved_behaviors: &resolved_plugins,
392            existing_tools: tools,
393            agent_definition: &definition,
394        };
395        let mut skills_bundles = Vec::new();
396        for wiring in &self.system_wirings {
397            let bundles = wiring.wire(&wiring_ctx)?;
398            skills_bundles.extend(bundles);
399        }
400
401        let skills_plugins = merge_wiring_bundles(&skills_bundles, tools)?;
402        let mut all_plugins = ResolvedBehaviors::default()
403            .with_global(skills_plugins)
404            .with_agent_default(resolved_plugins)
405            .into_plugins()?;
406
407        let stop_conditions =
408            self.resolve_stop_condition_id_list(&definition.stop_condition_ids)?;
409        let specs = synthesize_stop_specs(&definition);
410        let stop_plugin = StopPolicyPlugin::new(stop_conditions, specs);
411        if !stop_plugin.is_empty() {
412            all_plugins.push(Arc::new(stop_plugin));
413            ensure_unique_behavior_ids(&all_plugins)?;
414        }
415
416        Ok(build_base_agent_from_definition(definition, all_plugins))
417    }
418
419    /// Check whether an agent with the given ID is registered.
420    pub fn validate_agent(&self, agent_id: &str) -> Result<(), AgentOsResolveError> {
421        if self.agents.get(agent_id).is_some() {
422            Ok(())
423        } else {
424            Err(AgentOsResolveError::AgentNotFound(agent_id.to_string()))
425        }
426    }
427
428    /// Resolve an agent's static wiring: config, tools, and run policy.
429    pub fn resolve(&self, agent_id: &str) -> Result<ResolvedRun, AgentOsResolveError> {
430        let definition = self
431            .agents
432            .get(agent_id)
433            .ok_or_else(|| AgentOsResolveError::AgentNotFound(agent_id.to_string()))?;
434
435        let mut run_policy = RunPolicy::new();
436        set_runtime_policy_from_definition_if_absent(&mut run_policy, &definition);
437
438        let model_runtime = self.resolve_model_runtime(&definition)?;
439        let allowed_tools = definition.allowed_tools.clone();
440        let excluded_tools = definition.excluded_tools.clone();
441        let mut tools = self.base_tools.snapshot();
442        let mut cfg = self.wire_into(definition, &mut tools, &model_runtime)?;
443        filter_tools_in_place(
444            &mut tools,
445            allowed_tools.as_deref(),
446            excluded_tools.as_deref(),
447        );
448        cfg.model = model_runtime.model;
449        cfg.chat_options = model_runtime.chat_options;
450        cfg.llm_executor = Some(model_runtime.llm_executor);
451        Ok(ResolvedRun {
452            agent: cfg,
453            tools,
454            run_policy,
455            parent_tool_call_id: None,
456        })
457    }
458}
459
460/// Merge explicit `stop_condition_specs` with implicit `max_rounds` from the
461/// definition. If the user already declared a `MaxRounds` spec, `max_rounds`
462/// is NOT added a second time.
463fn synthesize_stop_specs(definition: &AgentDefinition) -> Vec<StopConditionSpec> {
464    let mut specs = definition.stop_condition_specs.clone();
465    let has_explicit_max_rounds = specs
466        .iter()
467        .any(|s| matches!(s, StopConditionSpec::MaxRounds { .. }));
468    if !has_explicit_max_rounds && definition.max_rounds > 0 {
469        specs.push(StopConditionSpec::MaxRounds {
470            rounds: definition.max_rounds,
471        });
472    }
473    specs
474}
475
476fn build_base_agent_from_definition(
477    definition: AgentDefinition,
478    behaviors: Vec<Arc<dyn AgentBehavior>>,
479) -> BaseAgent {
480    let definition = normalize_definition_models(definition);
481    let tool_executor: Arc<dyn ToolExecutor> = match definition.tool_execution_mode {
482        ToolExecutionMode::Sequential => Arc::new(SequentialToolExecutor),
483        ToolExecutionMode::ParallelBatchApproval => {
484            Arc::new(ParallelToolExecutor::batch_approval())
485        }
486        ToolExecutionMode::ParallelStreaming => Arc::new(ParallelToolExecutor::streaming()),
487    };
488
489    let mut lattice_registry = LatticeRegistry::new();
490    for behavior in &behaviors {
491        behavior.register_lattice_paths(&mut lattice_registry);
492    }
493
494    let mut state_scope_registry = StateScopeRegistry::new();
495    for behavior in &behaviors {
496        behavior.register_state_scopes(&mut state_scope_registry);
497    }
498
499    let mut state_action_deserializer_registry = StateActionDeserializerRegistry::new();
500    for behavior in &behaviors {
501        behavior.register_state_action_deserializers(&mut state_action_deserializer_registry);
502    }
503
504    let behavior: Arc<dyn AgentBehavior> = if behaviors.is_empty() {
505        Arc::new(NoOpBehavior)
506    } else {
507        Arc::new(CompositeBehavior::new(definition.id.clone(), behaviors))
508    };
509
510    BaseAgent {
511        id: definition.id,
512        model: definition.model,
513        system_prompt: definition.system_prompt,
514        max_rounds: definition.max_rounds,
515        tool_executor,
516        chat_options: definition.chat_options,
517        fallback_models: definition.fallback_models,
518        llm_retry_policy: definition.llm_retry_policy,
519        behavior,
520        lattice_registry: Arc::new(lattice_registry),
521        state_scope_registry: Arc::new(state_scope_registry),
522        step_tool_provider: None,
523        llm_executor: None,
524        state_action_deserializer_registry: Arc::new(state_action_deserializer_registry),
525    }
526}
527
528fn normalize_definition_models(mut definition: AgentDefinition) -> AgentDefinition {
529    definition.model = definition.model.trim().to_string();
530    definition.fallback_models = definition
531        .fallback_models
532        .into_iter()
533        .map(|model| model.trim().to_string())
534        .filter(|model| !model.is_empty())
535        .collect();
536    definition
537}
538
539#[cfg(test)]
540pub(crate) fn normalize_definition_models_for_test(definition: AgentDefinition) -> AgentDefinition {
541    normalize_definition_models(definition)
542}