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
45impl 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 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 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 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 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 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 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 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 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 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
460fn 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}