1use async_trait::async_trait;
2use futures::stream::{Stream, StreamExt};
3use parking_lot::RwLock;
4use serde_json::Value;
5use std::collections::{HashMap, HashSet};
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::{Arc, Weak};
10use std::time::Instant;
11use tracing::{debug, error, info, instrument, warn};
12
13pub(crate) type ToolResourceLocks = Arc<RwLock<HashMap<String, Weak<tokio::sync::Mutex<()>>>>>;
15
16struct ToolResourceGuards {
20 guards: Vec<tokio::sync::OwnedMutexGuard<()>>,
21 locks: ToolResourceLocks,
22}
23
24#[derive(Clone)]
25struct StoredSessionRestore {
26 snapshot: AgentSnapshot,
27 metadata: Option<ai_agents_core::SessionMetadata>,
28}
29
30struct RuntimeSessionRestorePoint {
31 snapshot: AgentSnapshot,
32 metadata: ai_agents_core::SessionMetadata,
33 actor_id: Option<String>,
34 session_id: Option<String>,
35}
36
37impl Drop for ToolResourceGuards {
38 fn drop(&mut self) {
39 self.guards.clear();
40 self.locks.write().retain(|_, lock| lock.strong_count() > 0);
41 }
42}
43
44#[derive(Clone)]
48struct RuntimeSafetySnapshot {
49 version: u64,
50 emergency_deny: bool,
51 tool_security: ToolSecurityEngine,
52 tool_scope_override: Option<Vec<String>>,
53}
54
55#[derive(Clone, Copy)]
59struct ToolDecisionVersions {
60 policy: u64,
61 registry: u64,
62 runtime_control: u64,
63}
64
65#[derive(Clone)]
69struct ToolApprovalBinding {
70 canonical_id: String,
71 arguments: Value,
72 confirmation_required: bool,
73 policy_version: u64,
74 runtime_control_version: u64,
75 reviewed_tool: Arc<dyn ai_agents_core::Tool>,
76}
77
78fn merge_approved_record(record: &mut Option<ToolApprovalRecord>) {
82 if record
83 .as_ref()
84 .is_some_and(|record| matches!(record.status, ToolApprovalStatus::Modified))
85 {
86 return;
87 }
88 *record = Some(ToolApprovalRecord {
89 status: ToolApprovalStatus::Approved,
90 reason: None,
91 modified_arguments: None,
92 });
93}
94
95impl ToolApprovalBinding {
96 fn is_stale(
98 &self,
99 canonical_id: &str,
100 arguments: &Value,
101 confirmation_required: bool,
102 versions: ToolDecisionVersions,
103 resolved_tool: &Arc<dyn ai_agents_core::Tool>,
104 ) -> bool {
105 self.canonical_id != canonical_id
106 || self.arguments != *arguments
107 || self.confirmation_required != confirmation_required
108 || self.policy_version != versions.policy
109 || self.runtime_control_version != versions.runtime_control
110 || !Arc::ptr_eq(&self.reviewed_tool, resolved_tool)
111 }
112}
113
114use crate::turn_context::{current_turn_actor_context, scope_actor_context};
115
116use ai_agents_context::{ContextManager, ContextProvider, TemplateRenderer};
117use ai_agents_core::traits::storage::StorageCapability;
118use ai_agents_core::{
119 AgentError, AgentSnapshot, AgentStorage, ChatMessage, FinishReason, LLMError, LLMProvider,
120 LLMResponse, LLMToolDefinition, LLMToolRequest, PermissionOutcome, Result, ToolActorContext,
121 ToolApprovalRecord, ToolApprovalStatus, ToolCallSource, ToolCancellationToken, ToolChoice,
122 ToolExecutionContext, ToolExecutionRecord, ToolExecutionRequest, ToolInvoker,
123 ToolPolicyDecisionRecord, ToolResult,
124};
125use ai_agents_disambiguation::{
126 ClarificationObserver, ClarificationParseFuture, ClarificationQuestionFuture,
127 DisambiguationConfig, DisambiguationContext, DisambiguationManager, DisambiguationResult,
128};
129use ai_agents_hitl::{
130 ApprovalHandler, ApprovalResolvedOutcome, ApprovalResult, ApprovalTrigger, HITLCheckResult,
131 HITLEngine, RejectAllHandler, TimeoutAction,
132};
133use ai_agents_hooks::{AgentHooks, NoopHooks};
134use ai_agents_llm::LLMRegistry;
135use ai_agents_memory::{
136 CompressResult, EvictionReason, Memory, MemoryBudgetEvent, MemoryCompressEvent,
137 MemoryEvictEvent, MemoryTokenBudget, OverflowStrategy,
138};
139use ai_agents_observability::{
140 EventStatus, EventType, ObservabilityManager, ObservationPurpose, SpanContext,
141 current_observation_context, new_session_id as new_observation_session_id,
142 resolve_language_from_context, with_observation_context, with_observation_purpose,
143};
144use ai_agents_process::{
145 ProcessData, ProcessProcessor, ProcessPurposeHint, ProcessStageFuture, ProcessStageObserver,
146};
147use ai_agents_reasoning::{
148 CriterionResult, EvaluationResult, Plan, PlanAction, PlanStatus, PlanStep, ReasoningConfig,
149 ReasoningMetadata, ReasoningMode, ReasoningOutput, ReflectionAttempt, ReflectionConfig,
150 ReflectionMetadata, StepFailureAction,
151};
152use ai_agents_recovery::{
153 ByRoleFilter, ContextOverflowAction, FilterConfig, IntoClassifiedError, KeepRecentFilter,
154 LLMFailureAction, MessageFilter, RecoveryManager, SkipPatternFilter, ToolFailureAction,
155};
156use ai_agents_relationships::RelationshipManager;
157use ai_agents_skills::{SkillDefinition, SkillExecutor, SkillRouter};
158use ai_agents_state::{
159 PromptMode, StateAction, StateMachine, StateMachineSnapshot, StateTransitionEvent, ToolRef,
160 Transition, TransitionContext, TransitionEvaluator, TransitionTiming, evaluate_guard,
161};
162use ai_agents_storage::{StorageConfig as StorageStorageConfig, create_storage};
163use ai_agents_tools::{
164 CommandRunner, ConditionEvaluator, DiagnosticsProvider, EvaluationContext, LLMGetter,
165 QuestionHandler, SecurityCheckResult, TodoItem, ToolCallRecord, ToolRegistry,
166 ToolSecurityConfig, ToolSecurityEngine,
167};
168
169use super::{
170 Agent, AgentInfo, AgentResponse, ParallelToolsConfig, StreamChunk, StreamingConfig, ToolCall,
171};
172use crate::optimization::{
173 AwaitBeforeNextTurn, BackgroundMaintenanceQueue, BackgroundOverflowPolicy, MainResponseDraft,
174 MaintenanceMode, MaintenanceSequenceKey, RuntimeBranch, RuntimeBranchResult,
175 RuntimeBranchStatus, RuntimeCommitBehavior, RuntimeConfig, RuntimeOptimizationKind,
176 RuntimeTaskPriority, RuntimeTaskPurpose, ScheduledBranchSet, SkillCandidate,
177 StreamingDraftResult, TransitionCandidate, TurnBranchScheduler, TurnOptimizationContext,
178};
179use crate::spec::StorageConfig;
180
181enum ToolCallOutcome {
183 Continue,
185 TransitionFired,
187 Rejected(AgentResponse),
189}
190
191#[derive(Clone)]
192struct MainToolProtocol {
193 choice: Option<ToolChoice>,
194 tool_ids: Vec<String>,
195 definitions: Vec<LLMToolDefinition>,
196}
197
198struct MainProviderResponse {
199 response: LLMResponse,
200 used_native_tools: bool,
201}
202
203struct CommittedTextResponse<'a> {
207 processed_input: &'a str,
208 input_context: &'a HashMap<String, Value>,
209 answer: String,
210 reasoning_mode: ReasoningMode,
211 auto_detected: bool,
212 iterations: u32,
213 thinking_content: Option<String>,
214 all_tool_calls: Vec<ToolCall>,
215}
216
217struct AgentResponseParts {
221 content: String,
222 all_tool_calls: Vec<ToolCall>,
223 reasoning_mode: ReasoningMode,
224 auto_detected: bool,
225 iterations: u32,
226 thinking: Option<String>,
227 reflection_metadata: Option<ReflectionMetadata>,
228}
229
230enum SkillRouteResult {
232 NoMatch,
234 Response { skill_id: String, content: String },
236 NeedsClarification(AgentResponse),
238}
239
240enum ParallelTransitionSelection {
242 Candidate(TransitionCandidate),
244 NoMatch,
246 ReservationExhausted,
248}
249
250enum PostLoopResult {
252 NoTransition(String),
254 Transitioned(String),
256 NeedsRedispatch,
259}
260
261struct RootTurnCleanup<'a> {
262 agent: &'a RuntimeAgent,
263}
264
265impl<'a> RootTurnCleanup<'a> {
266 fn new(agent: &'a RuntimeAgent) -> Self {
267 Self { agent }
268 }
269}
270
271impl Drop for RootTurnCleanup<'_> {
272 fn drop(&mut self) {
273 self.agent.end_root_turn();
274 }
275}
276
277#[derive(Debug)]
279struct RuntimeControlState {
280 snapshot_guard: RwLock<()>,
282 version: AtomicU64,
284 emergency_deny: Arc<AtomicBool>,
286 tool_security_override: RwLock<Option<ToolSecurityEngine>>,
288 tool_scope_override: RwLock<Option<Vec<String>>>,
290}
291
292impl Default for RuntimeControlState {
293 fn default() -> Self {
294 Self {
295 snapshot_guard: RwLock::new(()),
296 version: AtomicU64::new(1),
297 emergency_deny: Arc::new(AtomicBool::new(false)),
298 tool_security_override: RwLock::new(None),
299 tool_scope_override: RwLock::new(None),
300 }
301 }
302}
303
304#[derive(Clone)]
306pub struct RuntimeControlHandle {
307 state: Arc<RuntimeControlState>,
308}
309
310impl RuntimeControlHandle {
311 pub fn version(&self) -> u64 {
313 self.state.version.load(Ordering::SeqCst)
314 }
315
316 fn bump(&self) -> u64 {
317 self.state.version.fetch_add(1, Ordering::SeqCst) + 1
318 }
319
320 pub fn set_tool_security(&self, config: ToolSecurityConfig) -> u64 {
322 let _guard = self.state.snapshot_guard.write();
323 let generation = self.bump();
324 *self.state.tool_security_override.write() = Some(
325 ToolSecurityEngine::new_with_policy_version(config, generation),
326 );
327 generation
328 }
329
330 pub fn clear_tool_security_override(&self) -> u64 {
332 let _guard = self.state.snapshot_guard.write();
333 *self.state.tool_security_override.write() = None;
334 self.bump()
335 }
336
337 pub fn set_tool_scope(&self, tool_ids: Vec<String>) -> u64 {
339 let _guard = self.state.snapshot_guard.write();
340 *self.state.tool_scope_override.write() = Some(tool_ids);
341 self.bump()
342 }
343
344 pub fn clear_tool_scope_override(&self) -> u64 {
346 let _guard = self.state.snapshot_guard.write();
347 *self.state.tool_scope_override.write() = None;
348 self.bump()
349 }
350
351 pub fn set_emergency_deny(&self, enabled: bool) -> u64 {
353 let _guard = self.state.snapshot_guard.write();
354 self.state.emergency_deny.store(enabled, Ordering::SeqCst);
355 self.bump()
356 }
357
358 pub fn cancel_all(&self) -> u64 {
360 self.set_emergency_deny(true)
361 }
362}
363
364pub struct RuntimeAgent {
365 info: AgentInfo,
366 llm_registry: Arc<LLMRegistry>,
367 memory: Arc<dyn Memory>,
368 tools: Arc<ToolRegistry>,
369 skills: Vec<SkillDefinition>,
370 skill_router: Option<SkillRouter>,
371 skill_executor: Option<SkillExecutor>,
372 base_system_prompt: String,
373 max_iterations: u32,
374 iteration_count: RwLock<u32>,
375 max_context_tokens: u32,
376 memory_token_budget: Option<MemoryTokenBudget>,
377 recovery_manager: RecoveryManager,
378 tool_security: ToolSecurityEngine,
379 process_processor: Option<ProcessProcessor>,
380 message_filters: RwLock<HashMap<String, Arc<dyn MessageFilter>>>,
381 state_machine: Option<Arc<StateMachine>>,
382 transition_evaluator: Option<Arc<dyn TransitionEvaluator>>,
383 context_manager: Arc<ContextManager>,
384 template_renderer: TemplateRenderer,
385 tool_call_history: RwLock<Vec<ToolCallRecord>>,
386 parallel_tools: ParallelToolsConfig,
387 streaming: StreamingConfig,
388 hooks: Arc<dyn AgentHooks>,
389 hitl_engine: Option<HITLEngine>,
390 approval_handler: Arc<dyn ApprovalHandler>,
391 storage_config: StorageConfig,
392 storage: RwLock<Option<Arc<dyn AgentStorage>>>,
393 storage_init: tokio::sync::Mutex<()>,
394 reasoning_config: ReasoningConfig,
395 reflection_config: ReflectionConfig,
396 disambiguation_manager: Option<DisambiguationManager>,
397 persona_manager: Option<Arc<ai_agents_persona::PersonaManager>>,
399 pending_skill_id: RwLock<Option<String>>,
403 current_plan: RwLock<Option<Plan>>,
404 declared_tool_ids: Option<Vec<String>>,
406 context_initialized: AtomicBool,
408 spawner: Option<Arc<crate::spawner::AgentSpawner>>,
410 spawner_registry: Option<Arc<crate::spawner::AgentRegistry>>,
412 redispatch_depth: RwLock<u32>,
415 active_turn_context: RwLock<Option<TurnOptimizationContext>>,
417 root_user_message_committed: AtomicBool,
419 actor_id: RwLock<Option<String>>,
421 fact_store: RwLock<Option<Arc<ai_agents_facts::FactStore>>>,
423 fact_extractor: RwLock<Option<Arc<dyn ai_agents_facts::FactExtractor>>>,
426 actor_facts_cache: Arc<RwLock<HashMap<String, Vec<ai_agents_core::KeyFact>>>>,
428 messages_since_extraction: Arc<RwLock<usize>>,
430 actor_memory_config: Option<ai_agents_facts::ActorMemoryConfig>,
432 facts_config: Option<ai_agents_facts::FactsConfig>,
434 session_metadata: RwLock<ai_agents_core::SessionMetadata>,
436 current_session_id: RwLock<Option<String>>,
438 relationship_manager: Option<Arc<RelationshipManager>>,
440 observability_manager: Option<Arc<ObservabilityManager>>,
442 runtime_config: RuntimeConfig,
444 background_maintenance: Arc<BackgroundMaintenanceQueue>,
446 resource_locks: ToolResourceLocks,
448 runtime_control: Arc<RuntimeControlState>,
450}
451
452impl std::fmt::Debug for RuntimeAgent {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 f.debug_struct("RuntimeAgent")
455 .field("info", &self.info)
456 .field("base_system_prompt", &self.base_system_prompt)
457 .field("max_iterations", &self.max_iterations)
458 .field("skills_count", &self.skills.len())
459 .field("max_context_tokens", &self.max_context_tokens)
460 .field("has_state_machine", &self.state_machine.is_some())
461 .field("parallel_tools", &self.parallel_tools)
462 .field("streaming", &self.streaming)
463 .field("has_hooks", &true)
464 .field("has_hitl", &self.hitl_engine.is_some())
465 .field("storage_type", &self.storage_config.storage_type())
466 .field("reasoning_mode", &self.reasoning_config.mode)
467 .field("reflection_enabled", &self.reflection_config.enabled)
468 .field("declared_tool_ids", &self.declared_tool_ids)
469 .field("has_persona", &self.persona_manager.is_some())
470 .field("has_observability", &self.observability_manager.is_some())
471 .finish_non_exhaustive()
472 }
473}
474
475struct ObservabilityClarificationObserver;
476
477impl ClarificationObserver for ObservabilityClarificationObserver {
478 fn observe_question<'a>(
480 &'a self,
481 future: ClarificationQuestionFuture<'a>,
482 ) -> ClarificationQuestionFuture<'a> {
483 Box::pin(async move {
484 with_observation_purpose(ObservationPurpose::DisambiguationClarification, future).await
485 })
486 }
487
488 fn observe_parse<'a>(
490 &'a self,
491 future: ClarificationParseFuture<'a>,
492 ) -> ClarificationParseFuture<'a> {
493 Box::pin(async move {
494 with_observation_purpose(ObservationPurpose::DisambiguationClarification, future).await
495 })
496 }
497}
498
499struct ObservabilityProcessStageObserver;
500
501impl ProcessStageObserver for ObservabilityProcessStageObserver {
502 fn observe<'a>(
504 &'a self,
505 hint: ProcessPurposeHint,
506 future: ProcessStageFuture<'a>,
507 ) -> ProcessStageFuture<'a> {
508 Box::pin(async move {
509 with_observation_purpose(observation_purpose_for_process(hint), future).await
510 })
511 }
512}
513
514struct RegistryLLMGetter {
515 registry: Arc<LLMRegistry>,
516}
517
518impl LLMGetter for RegistryLLMGetter {
519 fn get_llm(&self, alias: &str) -> Option<Arc<dyn LLMProvider>> {
520 self.registry.get(alias).ok()
521 }
522}
523
524impl RuntimeAgent {
525 #[allow(clippy::too_many_arguments)]
526 pub fn new(
527 info: AgentInfo,
528 llm_registry: Arc<LLMRegistry>,
529 memory: Arc<dyn Memory>,
530 tools: Arc<ToolRegistry>,
531 skills: Vec<SkillDefinition>,
532 system_prompt: String,
533 max_iterations: u32,
534 ) -> Self {
535 let (skill_router, skill_executor) = if !skills.is_empty() {
536 let router_llm = llm_registry.router().ok();
537 let router = router_llm.map(|llm| SkillRouter::new(llm, skills.clone()));
538 let executor = SkillExecutor::new(llm_registry.clone(), tools.clone());
539 (router, Some(executor))
540 } else {
541 (None, None)
542 };
543
544 let context_manager =
545 ContextManager::new(HashMap::new(), info.name.clone(), info.version.clone());
546
547 Self {
548 info,
549 llm_registry,
550 memory,
551 tools,
552 skills,
553 skill_router,
554 skill_executor,
555 base_system_prompt: system_prompt,
556 max_iterations,
557 iteration_count: RwLock::new(0),
558 max_context_tokens: 128000,
559 memory_token_budget: None,
560 recovery_manager: RecoveryManager::default(),
561 tool_security: ToolSecurityEngine::default(),
562 process_processor: None,
563 message_filters: RwLock::new(HashMap::new()),
564 state_machine: None,
565 transition_evaluator: None,
566 context_manager: Arc::new(context_manager),
567 template_renderer: TemplateRenderer::new(),
568 tool_call_history: RwLock::new(Vec::new()),
569 parallel_tools: ParallelToolsConfig::default(),
570 streaming: StreamingConfig::default(),
571 hooks: Arc::new(NoopHooks),
572 hitl_engine: None,
573 approval_handler: Arc::new(RejectAllHandler::new()),
574 storage_config: StorageConfig::default(),
575 storage: RwLock::new(None),
576 storage_init: tokio::sync::Mutex::new(()),
577 reasoning_config: ReasoningConfig::default(),
578 reflection_config: ReflectionConfig::default(),
579 disambiguation_manager: None,
580 persona_manager: None,
581 pending_skill_id: RwLock::new(None),
582 current_plan: RwLock::new(None),
583 declared_tool_ids: None,
584 context_initialized: AtomicBool::new(false),
585 spawner: None,
586 spawner_registry: None,
587 redispatch_depth: RwLock::new(0),
588 active_turn_context: RwLock::new(None),
589 root_user_message_committed: AtomicBool::new(false),
590 actor_id: RwLock::new(None),
591 fact_store: RwLock::new(None),
592 fact_extractor: RwLock::new(None),
593 actor_facts_cache: Arc::new(RwLock::new(HashMap::new())),
594 messages_since_extraction: Arc::new(RwLock::new(0)),
595 actor_memory_config: None,
596 facts_config: None,
597 session_metadata: RwLock::new(ai_agents_core::SessionMetadata::default()),
598 current_session_id: RwLock::new(None),
599 relationship_manager: None,
600 observability_manager: None,
601 runtime_config: RuntimeConfig::default(),
602 background_maintenance: Arc::new(BackgroundMaintenanceQueue::default()),
603 resource_locks: new_tool_resource_locks(),
604 runtime_control: Arc::new(RuntimeControlState::default()),
605 }
606 }
607
608 pub fn with_declared_tool_ids(mut self, ids: Option<Vec<String>>) -> Self {
609 self.declared_tool_ids = ids;
610 self
611 }
612
613 pub fn with_storage_config(mut self, config: StorageConfig) -> Self {
614 self.storage_config = config;
615 self
616 }
617
618 pub fn with_storage(self, storage: Arc<dyn AgentStorage>) -> Self {
619 *self.storage.write() = Some(storage);
620 self
621 }
622
623 pub(crate) fn with_shared_resource_locks(mut self, locks: ToolResourceLocks) -> Self {
624 self.resource_locks = locks;
625 self
626 }
627
628 pub fn with_reasoning(mut self, config: ReasoningConfig) -> Self {
629 self.reasoning_config = config;
630 self
631 }
632
633 pub fn with_reflection(mut self, config: ReflectionConfig) -> Self {
634 self.reflection_config = config;
635 self
636 }
637
638 pub fn with_relationships(mut self, manager: Arc<RelationshipManager>) -> Self {
640 self.relationship_manager = Some(manager);
641 self
642 }
643
644 pub fn with_observability(mut self, manager: Arc<ObservabilityManager>) -> Self {
646 self.observability_manager = Some(manager);
647 self
648 }
649
650 pub fn with_runtime_config(mut self, config: RuntimeConfig) -> Self {
652 let max_tasks = config.optimization.post_turn.max_background_tasks;
653 self.background_maintenance = Arc::new(BackgroundMaintenanceQueue::new(max_tasks));
654 self.runtime_config = config;
655 self
656 }
657
658 pub fn runtime_config(&self) -> &RuntimeConfig {
660 &self.runtime_config
661 }
662
663 pub async fn flush_background_tasks(&self) -> Result<()> {
665 self.background_maintenance.flush_all().await
666 }
667
668 pub async fn flush_background_tasks_for_actor(&self, actor_id: &str) -> Result<()> {
670 self.background_maintenance.flush_scope(actor_id).await
671 }
672
673 pub async fn flush_background_tasks_for_purpose(
675 &self,
676 purpose: RuntimeTaskPurpose,
677 ) -> Result<()> {
678 self.background_maintenance.flush_purpose(purpose).await
679 }
680
681 pub async fn flush_background_tasks_for_actor_purpose(
683 &self,
684 actor_id: &str,
685 purpose: RuntimeTaskPurpose,
686 ) -> Result<()> {
687 self.background_maintenance
688 .flush_scope_purpose(actor_id, purpose)
689 .await
690 }
691
692 pub async fn shutdown_background_tasks(&self) -> Result<()> {
694 self.flush_background_tasks().await
695 }
696
697 pub fn observability(&self) -> Option<Arc<ObservabilityManager>> {
699 self.observability_manager.clone()
700 }
701
702 async fn export_observability_if_configured(&self) {
704 let Some(manager) = self.observability_manager.as_ref() else {
705 return;
706 };
707 let export = &manager.config().export;
708 if !export.write_report && !export.write_raw_events {
709 return;
710 }
711 if let Err(error) = manager.export().await {
712 warn!(error = %error, "Observability export failed");
713 }
714 }
715
716 pub fn relationship_manager(&self) -> Option<Arc<RelationshipManager>> {
718 self.relationship_manager.clone()
719 }
720
721 fn current_turn_actor_context(&self) -> Option<crate::TurnActorContext> {
722 current_turn_actor_context()
723 }
724
725 fn effective_actor_id(&self) -> Option<String> {
726 self.current_turn_actor_context()
727 .and_then(|ctx| ctx.effective_actor_id().map(|id| id.to_string()))
728 .or_else(|| self.actor_id.read().clone())
729 }
730
731 fn effective_origin_actor_id(&self) -> Option<String> {
732 self.current_turn_actor_context()
733 .and_then(|ctx| ctx.origin_actor_id.clone())
734 .or_else(|| self.actor_id.read().clone())
735 }
736
737 fn record_session_actor_if_needed(&self) {
738 if let Some(actor_id) = self.effective_origin_actor_id() {
739 let mut meta = self.session_metadata.write();
740 meta.actor_id = Some(actor_id.clone());
741 if !meta.actors.iter().any(|a| a == &actor_id) {
742 meta.actors.push(actor_id);
743 }
744 }
745 }
746
747 fn outbound_actor_context(&self) -> crate::TurnActorContext {
748 let mut context = self.current_turn_actor_context().unwrap_or_default();
749 if context.origin_actor_id.is_none() {
750 context.origin_actor_id = self.effective_origin_actor_id();
751 }
752 context.sender_agent_id = Some(self.info.id.clone());
753 context
754 }
755
756 fn observation_session_id(&self) -> Option<String> {
758 let mut current = self.current_session_id.write();
759 if current.is_none() {
760 *current = Some(new_observation_session_id());
761 }
762 current.clone()
763 }
764
765 fn build_observation_context(&self, actor_id: Option<String>) -> Option<SpanContext> {
767 let manager = self.observability_manager.as_ref()?;
768 let context = self.build_context_with_overlays();
769 let language = resolve_language_from_context(manager.config(), &context);
770 let context = current_observation_context()
771 .map(|parent| parent.child_for_agent(self.info.id.clone()).with_new_turn())
772 .unwrap_or_else(|| SpanContext::new_root(self.info.id.clone()));
773 Some(
774 context
775 .with_actor(actor_id.or_else(|| self.effective_actor_id()))
776 .with_session(self.observation_session_id())
777 .with_state(self.current_state())
778 .with_language(Some(language)),
779 )
780 }
781
782 fn current_runtime_observation_context(
784 &self,
785 purpose: ObservationPurpose,
786 ) -> Option<SpanContext> {
787 let manager = self.observability_manager.as_ref()?;
788 let context = self.build_context_with_overlays();
789 let language = resolve_language_from_context(manager.config(), &context);
790 let mut observation = current_observation_context()
791 .unwrap_or_else(|| SpanContext::new_root(self.info.id.clone()));
792 observation.agent_id = self.info.id.clone();
793 observation.actor_id = self.effective_actor_id();
794 observation.session_id = self.observation_session_id();
795 observation.state = self.current_state();
796 observation.language = Some(language);
797 observation.purpose = purpose;
798 Some(observation)
799 }
800
801 async fn observe_purpose<F, T>(&self, purpose: ObservationPurpose, future: F) -> T
803 where
804 F: Future<Output = T>,
805 {
806 if let Some(context) = self.current_runtime_observation_context(purpose) {
807 with_observation_context(context, future).await
808 } else {
809 future.await
810 }
811 }
812
813 fn chat_with_actor_context_boxed<'a>(
815 &'a self,
816 input: &'a str,
817 actor_context: crate::TurnActorContext,
818 ) -> Pin<Box<dyn Future<Output = Result<AgentResponse>> + Send + 'a>> {
819 Box::pin(async move {
820 let actor_id = actor_context.effective_actor_id().map(str::to_string);
821 let run = async move {
822 scope_actor_context(
823 actor_context,
824 Box::pin(async move { self.run_loop(input).await }),
825 )
826 .await
827 };
828 let result = if let Some(context) = self.build_observation_context(actor_id) {
829 with_observation_context(context, run).await
830 } else {
831 run.await
832 };
833 self.export_observability_if_configured().await;
834 result
835 })
836 }
837
838 pub async fn chat_with_actor_context(
842 &self,
843 input: &str,
844 actor_context: crate::TurnActorContext,
845 ) -> Result<AgentResponse> {
846 self.chat_with_actor_context_boxed(input, actor_context)
847 .await
848 }
849
850 pub async fn chat_as_actor(&self, actor_id: &str, input: &str) -> Result<AgentResponse> {
852 let actor_context = crate::TurnActorContext::new().with_origin_actor(actor_id);
853 self.chat_with_actor_context(input, actor_context).await
854 }
855
856 pub async fn load_actor_relationship(&self) -> Result<()> {
858 self.maybe_load_actor_relationship().await;
859 Ok(())
860 }
861
862 pub async fn update_relationship_dimension(
864 &self,
865 dimension: &str,
866 delta: f64,
867 reason: Option<&str>,
868 ) -> Result<ai_agents_relationships::DimensionChange> {
869 self.update_relationship_dimension_for_perspective(
870 ai_agents_relationships::RelationshipPerspective::AgentToActor,
871 dimension,
872 delta,
873 reason,
874 )
875 .await
876 }
877
878 pub async fn update_relationship_dimension_for_perspective(
882 &self,
883 perspective: ai_agents_relationships::RelationshipPerspective,
884 dimension: &str,
885 delta: f64,
886 reason: Option<&str>,
887 ) -> Result<ai_agents_relationships::DimensionChange> {
888 let manager = self
889 .relationship_manager
890 .as_ref()
891 .ok_or_else(|| AgentError::Config("Relationship memory is not configured".into()))?;
892 let actor_id = self.effective_actor_id().ok_or_else(|| {
893 AgentError::Config("No actor ID set. Use set_actor_id() first".into())
894 })?;
895 let change = manager.update_dimension_for_perspective(
896 &actor_id,
897 perspective,
898 dimension,
899 delta,
900 1.0,
901 reason.unwrap_or("manual relationship update"),
902 )?;
903 self.persist_actor_relationship(&actor_id).await?;
904 info!(
905 actor_id = %actor_id,
906 perspective = %change.perspective,
907 dimension = %change.dimension,
908 delta = change.delta,
909 current = change.current,
910 "relationship updated manually"
911 );
912 self.hooks
913 .on_relationship_change(&actor_id, std::slice::from_ref(&change))
914 .await;
915 Ok(change)
916 }
917
918 pub fn reasoning_config(&self) -> &ReasoningConfig {
919 &self.reasoning_config
920 }
921
922 pub fn reflection_config(&self) -> &ReflectionConfig {
923 &self.reflection_config
924 }
925
926 pub fn with_facts_config(
929 mut self,
930 actor_memory_config: Option<ai_agents_facts::ActorMemoryConfig>,
931 facts_config: Option<ai_agents_facts::FactsConfig>,
932 ) -> Self {
933 self.actor_memory_config = actor_memory_config;
934 self.facts_config = facts_config;
935 self
936 }
937
938 pub fn with_facts(
941 mut self,
942 store: Arc<ai_agents_facts::FactStore>,
943 extractor: Option<Arc<dyn ai_agents_facts::FactExtractor>>,
944 actor_memory_config: Option<ai_agents_facts::ActorMemoryConfig>,
945 facts_config: Option<ai_agents_facts::FactsConfig>,
946 ) -> Self {
947 *self.fact_store.write() = Some(store);
948 *self.fact_extractor.write() = extractor;
949 self.actor_memory_config = actor_memory_config;
950 self.facts_config = facts_config;
951 self
952 }
953
954 pub fn fact_store(&self) -> Option<Arc<ai_agents_facts::FactStore>> {
956 self.fact_store.read().clone()
957 }
958
959 pub fn actor_id(&self) -> Option<String> {
961 self.actor_id.read().clone()
962 }
963
964 pub fn set_actor_id(&self, actor_id: &str) -> ai_agents_core::Result<()> {
966 *self.actor_id.write() = Some(actor_id.to_string());
967 {
968 let mut meta = self.session_metadata.write();
969 meta.actor_id = Some(actor_id.to_string());
970 if !meta.actors.iter().any(|a| a == actor_id) {
971 meta.actors.push(actor_id.to_string());
972 }
973 }
974 Ok(())
975 }
976
977 pub fn clear_actor_id(&self) {
979 *self.actor_id.write() = None;
980 self.session_metadata.write().actor_id = None;
981 }
982
983 pub fn set_user_id(&self, user_id: &str) -> ai_agents_core::Result<()> {
985 self.set_actor_id(user_id)
986 }
987
988 pub async fn load_actor_memory(&self) -> ai_agents_core::Result<()> {
990 let actor_id = match self.effective_actor_id() {
991 Some(id) => id,
992 None => return Ok(()),
993 };
994
995 let store_opt = self.fact_store.read().clone();
996 if let Some(store) = store_opt {
997 let facts = store.get_facts(&actor_id).await?;
998 let count = facts.len();
999 self.actor_facts_cache
1000 .write()
1001 .insert(actor_id.clone(), facts);
1002 self.hooks.on_actor_memory_loaded(&actor_id, count).await;
1003 tracing::debug!("loaded {} facts for actor {}", count, actor_id);
1004 }
1005
1006 Ok(())
1007 }
1008
1009 async fn maybe_load_actor_memory(&self) {
1011 let Some(actor_id) = self.effective_actor_id() else {
1012 return;
1013 };
1014 if self.actor_facts_cache.read().contains_key(&actor_id) {
1015 return;
1016 }
1017 let _ = self.load_actor_memory().await;
1018 }
1019
1020 async fn pre_turn_session_lifecycle(&self) {
1022 if *self.redispatch_depth.read() > 0 {
1023 return;
1024 }
1025 self.resolve_actor_id_from_context();
1026 self.await_background_before_next_turn().await;
1027 self.record_session_actor_if_needed();
1028 self.maybe_load_actor_memory().await;
1029 self.maybe_load_actor_relationship().await;
1030 *self.messages_since_extraction.write() += 1;
1031 }
1032
1033 async fn post_turn_session_lifecycle(&self) -> Result<()> {
1035 if *self.redispatch_depth.read() > 0 {
1036 return Ok(());
1037 }
1038 *self.messages_since_extraction.write() += 1;
1039 self.run_post_turn_maintenance().await
1040 }
1041
1042 fn begin_root_turn(&self) {
1044 if *self.redispatch_depth.read() == 0 {
1045 let mut guard = self.active_turn_context.write();
1046 if guard.is_none() {
1047 self.root_user_message_committed
1048 .store(false, Ordering::SeqCst);
1049 let max_calls = self
1050 .runtime_config
1051 .optimization
1052 .max_speculative_llm_calls_per_turn;
1053 *guard = Some(TurnOptimizationContext::new(
1054 String::new(),
1055 HashMap::new(),
1056 max_calls,
1057 ));
1058 }
1059 }
1060 }
1061
1062 fn update_active_turn_context(
1063 &self,
1064 processed_input: &str,
1065 input_context: HashMap<String, Value>,
1066 ) {
1067 if *self.redispatch_depth.read() > 0 {
1068 return;
1069 }
1070 let max_calls = self
1071 .runtime_config
1072 .optimization
1073 .max_speculative_llm_calls_per_turn;
1074 let mut guard = self.active_turn_context.write();
1075 match guard.as_mut() {
1076 Some(context) => {
1077 context.processed_input = processed_input.to_string();
1078 context.input_context = input_context;
1079 context.max_speculative_llm_calls = max_calls;
1080 }
1081 None => {
1082 *guard = Some(TurnOptimizationContext::new(
1083 processed_input,
1084 input_context,
1085 max_calls,
1086 ));
1087 }
1088 }
1089 }
1090
1091 async fn commit_root_user_message(&self, processed_input: &str) -> Result<()> {
1093 if *self.redispatch_depth.read() > 0 {
1094 return Ok(());
1095 }
1096 if !self
1097 .root_user_message_committed
1098 .swap(true, Ordering::SeqCst)
1099 {
1100 self.memory
1101 .add_message(ChatMessage::user(processed_input))
1102 .await?;
1103 if let Some(context) = self.active_turn_context.write().as_mut() {
1104 context.mark_user_message_committed();
1105 }
1106 }
1107 Ok(())
1108 }
1109
1110 fn end_root_turn(&self) {
1112 if *self.redispatch_depth.read() == 0 {
1113 self.root_user_message_committed
1114 .store(false, Ordering::SeqCst);
1115 *self.active_turn_context.write() = None;
1116 }
1117 }
1118
1119 fn reserve_active_speculative_llm_call(&self, kind: RuntimeOptimizationKind) -> bool {
1120 self.begin_root_turn();
1121 let mut guard = self.active_turn_context.write();
1122 let Some(context) = guard.as_mut() else {
1123 return false;
1124 };
1125 context.reserve_speculative_llm_call_for(kind)
1126 }
1127
1128 fn branch_context_preview(&self) -> String {
1129 let context = self.build_context_with_overlays();
1130 let mut value = serde_json::to_string_pretty(&context).unwrap_or_else(|_| "{}".to_string());
1131 const MAX_CONTEXT_PREVIEW_CHARS: usize = 2048;
1132 if value.chars().count() > MAX_CONTEXT_PREVIEW_CHARS {
1133 value = value
1134 .chars()
1135 .take(MAX_CONTEXT_PREVIEW_CHARS)
1136 .collect::<String>();
1137 value.push_str("...");
1138 }
1139 value
1140 }
1141
1142 async fn await_background_before_next_turn(&self) {
1144 let optimization = &self.runtime_config.optimization;
1145 if !optimization.enabled {
1146 return;
1147 }
1148 let actor_id = self.effective_actor_id();
1149 let post = &optimization.post_turn;
1150 self.await_background_task(
1151 post.facts.await_before_next_turn,
1152 RuntimeTaskPurpose::PostTurnFacts,
1153 actor_id.as_deref(),
1154 "facts",
1155 )
1156 .await;
1157 self.await_background_task(
1158 post.relationships.await_before_next_turn,
1159 RuntimeTaskPurpose::PostTurnRelationship,
1160 actor_id.as_deref(),
1161 "relationships",
1162 )
1163 .await;
1164 }
1165
1166 async fn await_background_task(
1167 &self,
1168 policy: AwaitBeforeNextTurn,
1169 purpose: RuntimeTaskPurpose,
1170 actor_id: Option<&str>,
1171 label: &str,
1172 ) {
1173 match policy {
1174 AwaitBeforeNextTurn::Never => {}
1175 AwaitBeforeNextTurn::Always => {
1176 if let Err(error) = self.flush_background_tasks_for_purpose(purpose).await {
1177 warn!(label = label, error = %error, "background maintenance flush failed");
1178 }
1179 }
1180 AwaitBeforeNextTurn::SameActor => {
1181 if let Some(actor_id) = actor_id
1182 && let Err(error) = self
1183 .flush_background_tasks_for_actor_purpose(actor_id, purpose)
1184 .await
1185 {
1186 warn!(label = label, actor_id = %actor_id, error = %error, "actor background maintenance flush failed");
1187 }
1188 }
1189 }
1190 }
1191
1192 async fn run_post_turn_maintenance(&self) -> Result<()> {
1194 let optimization = &self.runtime_config.optimization;
1195 if !optimization.enabled {
1196 self.auto_extract_facts().await;
1197 self.auto_update_relationship().await;
1198 return Ok(());
1199 }
1200
1201 let facts_mode = effective_maintenance_mode(
1202 optimization.post_turn.facts.mode,
1203 optimization.parallel_post_turn_memory,
1204 );
1205 let relationships_mode = effective_maintenance_mode(
1206 optimization.post_turn.relationships.mode,
1207 optimization.parallel_post_turn_memory,
1208 );
1209
1210 match (facts_mode, relationships_mode) {
1211 (MaintenanceMode::InlineSerial, MaintenanceMode::InlineSerial) => {
1212 self.auto_extract_facts().await;
1213 self.auto_update_relationship().await;
1214 }
1215 (MaintenanceMode::InlineParallel, MaintenanceMode::InlineParallel) => {
1216 let facts = self.auto_extract_facts();
1217 let relationships = self.auto_update_relationship();
1218 tokio::join!(facts, relationships);
1219 }
1220 (MaintenanceMode::Background, MaintenanceMode::Background) => {
1221 self.schedule_facts_background().await?;
1222 self.schedule_relationship_background().await?;
1223 }
1224 (MaintenanceMode::Background, MaintenanceMode::InlineParallel)
1225 | (MaintenanceMode::Background, MaintenanceMode::InlineSerial) => {
1226 self.schedule_facts_background().await?;
1227 self.auto_update_relationship().await;
1228 }
1229 (MaintenanceMode::InlineParallel, MaintenanceMode::Background)
1230 | (MaintenanceMode::InlineSerial, MaintenanceMode::Background) => {
1231 self.auto_extract_facts().await;
1232 self.schedule_relationship_background().await?;
1233 }
1234 _ => {
1235 self.auto_extract_facts().await;
1236 self.auto_update_relationship().await;
1237 }
1238 }
1239 Ok(())
1240 }
1241
1242 async fn schedule_facts_background(&self) -> Result<()> {
1243 let policy = self.runtime_config.optimization.post_turn.facts.clone();
1244 let should_extract = self
1245 .facts_config
1246 .as_ref()
1247 .map(|c| c.enabled && c.auto_extract)
1248 .unwrap_or(false);
1249 if !should_extract {
1250 return Ok(());
1251 }
1252 let msgs_since = *self.messages_since_extraction.read();
1253 if msgs_since < 2 {
1254 return Ok(());
1255 }
1256 let Some(actor_id) = self.effective_actor_id() else {
1257 self.record_skipped_maintenance(
1258 "facts",
1259 ObservationPurpose::FactsExtraction,
1260 "missing_actor",
1261 Some(&policy),
1262 );
1263 return Ok(());
1264 };
1265 let Some(extractor) = self.fact_extractor.read().clone() else {
1266 return Ok(());
1267 };
1268 let messages = match self.memory.get_messages(None).await {
1269 Ok(messages) => messages,
1270 Err(error) => {
1271 warn!(error = %error, "failed to snapshot messages for fact extraction");
1272 return Ok(());
1273 }
1274 };
1275 let recent: Vec<_> = messages
1276 .iter()
1277 .rev()
1278 .take(msgs_since)
1279 .rev()
1280 .cloned()
1281 .collect();
1282 if recent.is_empty() {
1283 return Ok(());
1284 }
1285 let existing = self
1286 .actor_facts_cache
1287 .read()
1288 .get(&actor_id)
1289 .cloned()
1290 .unwrap_or_default();
1291 let categories = self
1292 .facts_config
1293 .as_ref()
1294 .map(|c| c.custom_categories.clone())
1295 .unwrap_or_default();
1296 let store = self.fact_store.read().clone();
1297 let cache = Arc::clone(&self.actor_facts_cache);
1298 let counter = Arc::clone(&self.messages_since_extraction);
1299 let hooks = Arc::clone(&self.hooks);
1300 let agent_id = self.info.id.clone();
1301 let observation = current_observation_context();
1302 let key = MaintenanceSequenceKey::actor(
1303 agent_id,
1304 actor_id.clone(),
1305 RuntimeTaskPurpose::PostTurnFacts,
1306 );
1307 let actor_for_task = actor_id.clone();
1308 let task = async move {
1309 let run = async move {
1310 let facts = extractor
1311 .extract(&recent, &existing, Some(&actor_for_task), &categories)
1312 .await?;
1313 if !facts.is_empty() {
1314 if let Some(store) = store {
1315 let authoritative = store.add_facts(&actor_for_task, facts.clone()).await?;
1316 cache.write().insert(actor_for_task.clone(), authoritative);
1317 } else {
1318 cache
1319 .write()
1320 .entry(actor_for_task.clone())
1321 .or_default()
1322 .extend(facts.clone());
1323 }
1324 {
1325 let mut count = counter.write();
1326 if *count <= msgs_since {
1327 *count = 0;
1328 } else {
1329 *count -= msgs_since;
1330 }
1331 }
1332 hooks.on_facts_extracted(&actor_for_task, &facts).await;
1333 }
1334 Ok(())
1335 };
1336 if let Some(context) = observation {
1337 with_observation_context(
1338 context.with_purpose(ObservationPurpose::FactsExtraction),
1339 run,
1340 )
1341 .await
1342 } else {
1343 run.await
1344 }
1345 };
1346 self.spawn_or_handle_background(Some(key), task, "facts", &policy)
1347 .await
1348 }
1349
1350 async fn schedule_relationship_background(&self) -> Result<()> {
1351 let policy = self
1352 .runtime_config
1353 .optimization
1354 .post_turn
1355 .relationships
1356 .clone();
1357 let Some(manager) = self.relationship_manager.as_ref().cloned() else {
1358 return Ok(());
1359 };
1360 let Some(actor_id) = self.effective_actor_id() else {
1361 self.record_skipped_maintenance(
1362 "relationships",
1363 ObservationPurpose::RelationshipUpdate,
1364 "missing_actor",
1365 Some(&policy),
1366 );
1367 return Ok(());
1368 };
1369 let recent_messages = manager.config().auto_update.recent_messages;
1370 let messages = match self.memory.get_messages(Some(recent_messages)).await {
1371 Ok(messages) => messages,
1372 Err(error) => {
1373 warn!(actor = %actor_id, error = %error, "failed to snapshot messages for relationship update");
1374 return Ok(());
1375 }
1376 };
1377 let storage = self.storage.read().clone();
1378 let hooks = Arc::clone(&self.hooks);
1379 let agent_id = self.info.id.clone();
1380 let observation = current_observation_context();
1381 let key = MaintenanceSequenceKey::actor(
1382 agent_id.clone(),
1383 actor_id.clone(),
1384 RuntimeTaskPurpose::PostTurnRelationship,
1385 );
1386 let actor_for_task = actor_id.clone();
1387 let task = async move {
1388 let run = async move {
1389 if manager.config().auto_update.enabled {
1390 let update = manager.auto_update(&actor_for_task, &messages).await?;
1391 if !update.changes.is_empty() {
1392 hooks
1393 .on_relationship_change(&actor_for_task, &update.changes)
1394 .await;
1395 }
1396 if let Some(ref event) = update.event {
1397 hooks.on_notable_event(&actor_for_task, event).await;
1398 }
1399 }
1400 if manager.config().persistence.enabled
1401 && let (Some(storage), Some(value)) =
1402 (storage, manager.relationship_as_value(&actor_for_task)?)
1403 {
1404 storage
1405 .save_relationship(&agent_id, &actor_for_task, &value)
1406 .await?;
1407 }
1408 Ok(())
1409 };
1410 if let Some(context) = observation {
1411 with_observation_context(
1412 context.with_purpose(ObservationPurpose::RelationshipUpdate),
1413 run,
1414 )
1415 .await
1416 } else {
1417 run.await
1418 }
1419 };
1420 self.spawn_or_handle_background(Some(key), task, "relationships", &policy)
1421 .await
1422 }
1423
1424 async fn spawn_or_handle_background<F>(
1426 &self,
1427 key: Option<MaintenanceSequenceKey>,
1428 task: F,
1429 label: &'static str,
1430 policy: &crate::optimization::config::MaintenanceTaskPolicy,
1431 ) -> Result<()>
1432 where
1433 F: Future<Output = Result<()>> + Send + 'static,
1434 {
1435 if self.background_maintenance.is_full() {
1436 match self
1437 .runtime_config
1438 .optimization
1439 .post_turn
1440 .on_background_overflow
1441 {
1442 BackgroundOverflowPolicy::RunInline => {
1443 record_background_maintenance_event(
1444 self.observability_manager.as_ref(),
1445 label,
1446 EventStatus::Success,
1447 0,
1448 "inline_overflow",
1449 None,
1450 Some(policy),
1451 );
1452 let start = Instant::now();
1453 match task.await {
1454 Ok(()) => record_background_maintenance_event(
1455 self.observability_manager.as_ref(),
1456 label,
1457 EventStatus::Success,
1458 start.elapsed().as_millis() as u64,
1459 "inline_completed",
1460 None,
1461 Some(policy),
1462 ),
1463 Err(error) => {
1464 warn!(label = label, error = %error, "inline maintenance fallback failed");
1465 record_background_maintenance_event(
1466 self.observability_manager.as_ref(),
1467 label,
1468 EventStatus::Error,
1469 start.elapsed().as_millis() as u64,
1470 "inline_failed",
1471 Some(error.to_string()),
1472 Some(policy),
1473 );
1474 return Err(error);
1475 }
1476 }
1477 }
1478 BackgroundOverflowPolicy::Drop => {
1479 self.record_skipped_maintenance(
1480 label,
1481 ObservationPurpose::Other(label.to_string()),
1482 "queue_full",
1483 Some(policy),
1484 );
1485 }
1486 BackgroundOverflowPolicy::Error => {
1487 record_background_maintenance_event(
1488 self.observability_manager.as_ref(),
1489 label,
1490 EventStatus::Error,
1491 0,
1492 "queue_full",
1493 None,
1494 Some(policy),
1495 );
1496 warn!(label = label, "background maintenance queue full");
1497 return Err(AgentError::Other(format!(
1498 "background maintenance queue is full for {}",
1499 label
1500 )));
1501 }
1502 }
1503 return Ok(());
1504 }
1505
1506 record_background_maintenance_event(
1507 self.observability_manager.as_ref(),
1508 label,
1509 EventStatus::Success,
1510 0,
1511 "scheduled",
1512 None,
1513 Some(policy),
1514 );
1515 let manager = self.observability_manager.clone();
1516 let policy_for_task = policy.clone();
1517 let observed_task = async move {
1518 let start = Instant::now();
1519 let result = task.await;
1520 match &result {
1521 Ok(()) => record_background_maintenance_event(
1522 manager.as_ref(),
1523 label,
1524 EventStatus::Success,
1525 start.elapsed().as_millis() as u64,
1526 "completed",
1527 None,
1528 Some(&policy_for_task),
1529 ),
1530 Err(error) => record_background_maintenance_event(
1531 manager.as_ref(),
1532 label,
1533 EventStatus::Error,
1534 start.elapsed().as_millis() as u64,
1535 "failed",
1536 Some(error.to_string()),
1537 Some(&policy_for_task),
1538 ),
1539 }
1540 result
1541 };
1542
1543 if let Err(error) = self.background_maintenance.spawn(key, observed_task) {
1544 record_background_maintenance_event(
1545 self.observability_manager.as_ref(),
1546 label,
1547 EventStatus::Error,
1548 0,
1549 "spawn_failed",
1550 Some(error.to_string()),
1551 Some(policy),
1552 );
1553 warn!(label = label, error = %error, "background maintenance spawn failed");
1554 return Err(error);
1555 }
1556 Ok(())
1557 }
1558
1559 fn record_skipped_maintenance(
1561 &self,
1562 label: &str,
1563 purpose: ObservationPurpose,
1564 reason: &str,
1565 policy: Option<&crate::optimization::config::MaintenanceTaskPolicy>,
1566 ) {
1567 if let Some(manager) = self.observability_manager.as_ref() {
1568 let mut tags = background_maintenance_tags(label, "skipped", Some(reason), policy);
1569 tags.insert("runtime.skip_reason".to_string(), reason.to_string());
1570 manager.record_lifecycle_event(
1571 EventType::MemoryOperation {
1572 operation: format!("{}_maintenance", label),
1573 },
1574 purpose,
1575 EventStatus::Skipped,
1576 0,
1577 tags,
1578 None,
1579 );
1580 }
1581 }
1582
1583 pub fn actor_facts(&self) -> Vec<ai_agents_core::KeyFact> {
1585 let Some(actor_id) = self.effective_actor_id() else {
1586 return Vec::new();
1587 };
1588 self.actor_facts_cache
1589 .read()
1590 .get(&actor_id)
1591 .cloned()
1592 .unwrap_or_default()
1593 }
1594
1595 pub fn relationship_memory_text(&self) -> Option<String> {
1597 self.format_relationship_for_context().map(|(_, text)| text)
1598 }
1599
1600 pub async fn extract_facts(
1602 &self,
1603 last_n: usize,
1604 ) -> ai_agents_core::Result<Vec<ai_agents_core::KeyFact>> {
1605 self.extract_facts_with_source(last_n, "manual").await
1606 }
1607
1608 async fn extract_facts_with_source(
1609 &self,
1610 last_n: usize,
1611 source: &'static str,
1612 ) -> ai_agents_core::Result<Vec<ai_agents_core::KeyFact>> {
1613 let extractor = match self.fact_extractor.read().clone() {
1614 Some(e) => e,
1615 None => return Ok(vec![]),
1616 };
1617
1618 let messages = self.memory.get_messages(None).await?;
1619 let recent: Vec<_> = messages.iter().rev().take(last_n).rev().cloned().collect();
1620
1621 if recent.is_empty() {
1622 return Ok(vec![]);
1623 }
1624
1625 let actor_id = self.effective_actor_id();
1626 let existing = actor_id
1627 .as_ref()
1628 .and_then(|aid| self.actor_facts_cache.read().get(aid).cloned())
1629 .unwrap_or_default();
1630
1631 let categories = self
1632 .facts_config
1633 .as_ref()
1634 .map(|c| c.custom_categories.clone())
1635 .unwrap_or_default();
1636
1637 let facts = self
1638 .observe_purpose(
1639 ObservationPurpose::FactsExtraction,
1640 extractor.extract(&recent, &existing, actor_id.as_deref(), &categories),
1641 )
1642 .await?;
1643
1644 if !facts.is_empty() {
1646 let fact_store_opt = self.fact_store.read().clone();
1647 let mut stored_total = 0usize;
1648 let mut cache_updated = false;
1649 if let (Some(store), Some(aid)) = (fact_store_opt, &actor_id) {
1650 let authoritative = store.add_facts(aid, facts.clone()).await?;
1652 stored_total = authoritative.len();
1653 self.actor_facts_cache
1654 .write()
1655 .insert(aid.clone(), authoritative);
1656 cache_updated = true;
1657 } else if let Some(aid) = &actor_id {
1658 let mut cache = self.actor_facts_cache.write();
1659 let entry = cache.entry(aid.clone()).or_default();
1660 entry.extend(facts.clone());
1661 stored_total = entry.len();
1662 cache_updated = true;
1663 }
1664
1665 info!(
1666 actor_id = %actor_id.as_deref().unwrap_or("<none>"),
1667 source = source,
1668 requested_messages = last_n,
1669 message_count = recent.len(),
1670 extracted_count = facts.len(),
1671 cache_updated = cache_updated,
1672 stored_total = stored_total,
1673 "facts extracted"
1674 );
1675
1676 if let Some(ref aid) = actor_id {
1677 self.hooks.on_facts_extracted(aid, &facts).await;
1678 }
1679 }
1680
1681 Ok(facts)
1682 }
1683
1684 fn resolve_actor_id_from_context(&self) {
1687 if self
1688 .current_turn_actor_context()
1689 .and_then(|ctx| ctx.effective_actor_id().map(str::to_string))
1690 .is_some()
1691 {
1692 return;
1693 }
1694
1695 if let Some(ref am_config) = self.actor_memory_config
1696 && am_config.identification.method == ai_agents_facts::IdentificationMethod::FromContext
1697 && let Some(ref path) = am_config.identification.context_path
1698 {
1699 let val = self
1701 .context_manager
1702 .get_path(path)
1703 .or_else(|| self.context_manager.get(path));
1704 if let Some(val) = val
1705 && let Some(id_str) = val.as_str()
1706 {
1707 let current = self.actor_id.read().clone();
1708 if current.as_deref() != Some(id_str) {
1709 *self.actor_id.write() = Some(id_str.to_string());
1710 let mut meta = self.session_metadata.write();
1711 meta.actor_id = Some(id_str.to_string());
1712 if !meta.actors.iter().any(|a| a == id_str) {
1713 meta.actors.push(id_str.to_string());
1714 }
1715 }
1716 }
1717 }
1718 }
1719
1720 fn format_actor_facts_for_context(&self) -> String {
1722 let should_inject = self
1724 .facts_config
1725 .as_ref()
1726 .map(|c| c.inject_in_context)
1727 .unwrap_or(true);
1728 if !should_inject {
1729 return String::new();
1730 }
1731
1732 let Some(actor_id) = self.effective_actor_id() else {
1733 return String::new();
1734 };
1735
1736 let facts = self
1737 .actor_facts_cache
1738 .read()
1739 .get(&actor_id)
1740 .cloned()
1741 .unwrap_or_default();
1742 if facts.is_empty() {
1743 return String::new();
1744 }
1745
1746 let am_config = self.actor_memory_config.as_ref();
1747 let facts_budget = self
1750 .memory_token_budget
1751 .as_ref()
1752 .map(|b| b.allocation.facts as usize)
1753 .filter(|n| *n > 0);
1754 let default_max = am_config.map(|c| c.injection.max_tokens).unwrap_or(800);
1755 let max_tokens = facts_budget.unwrap_or(default_max);
1756
1757 let filtered: Vec<ai_agents_core::KeyFact> = if let Some(cfg) = am_config {
1759 if cfg.injection.mode == ai_agents_facts::InjectionMode::OnDemand {
1760 return String::new();
1761 }
1762 if cfg.injection.mode == ai_agents_facts::InjectionMode::Category
1763 && !cfg.injection.categories.is_empty()
1764 {
1765 facts
1766 .iter()
1767 .filter(|f| {
1768 cfg.injection
1769 .categories
1770 .iter()
1771 .any(|c| f.category.to_string() == *c)
1772 })
1773 .cloned()
1774 .collect()
1775 } else {
1776 facts.clone()
1777 }
1778 } else {
1779 facts.clone()
1780 };
1781
1782 if filtered.is_empty() {
1783 return String::new();
1784 }
1785
1786 if let Some(store) = self.fact_store.read().clone() {
1787 store.format_for_context(&filtered, max_tokens)
1788 } else {
1789 String::new()
1790 }
1791 }
1792
1793 fn build_context_with_staged(&self, staged: &HashMap<String, Value>) -> HashMap<String, Value> {
1794 let context = self.build_context_with_overlays();
1795 let mut root = Value::Object(context.into_iter().collect());
1796 for (path, value) in staged {
1797 if let Ok(updated) = ai_agents_core::set_dot_path(root.clone(), path, value.clone()) {
1798 root = updated;
1799 }
1800 }
1801 match root {
1802 Value::Object(obj) => obj.into_iter().collect(),
1803 _ => HashMap::new(),
1804 }
1805 }
1806
1807 fn build_context_with_overlays(&self) -> HashMap<String, Value> {
1808 let mut context = self.context_manager.get_all();
1809 let mut root = Value::Object(context.clone().into_iter().collect());
1810
1811 if let Some(turn_ctx) = self.current_turn_actor_context() {
1812 if let Some(ref origin_actor_id) = turn_ctx.origin_actor_id
1813 && let Ok(updated) = ai_agents_core::set_dot_path(
1814 root.clone(),
1815 "interaction.origin_actor_id",
1816 serde_json::json!(origin_actor_id),
1817 )
1818 {
1819 root = updated;
1820 }
1821 if let Some(ref sender_agent_id) = turn_ctx.sender_agent_id
1822 && let Ok(updated) = ai_agents_core::set_dot_path(
1823 root.clone(),
1824 "interaction.sender_agent_id",
1825 serde_json::json!(sender_agent_id),
1826 )
1827 {
1828 root = updated;
1829 }
1830 }
1831
1832 if let Some(ref actor_id) = self.effective_actor_id()
1833 && let Ok(updated) = ai_agents_core::set_dot_path(
1834 root.clone(),
1835 "interaction.actor_id",
1836 serde_json::json!(actor_id),
1837 )
1838 {
1839 root = updated;
1840 }
1841
1842 if let Some(manager) = self.relationship_manager.as_ref()
1843 && let Some(actor_id) = self.effective_actor_id()
1844 && let Some(value) = manager.to_context_value(&actor_id)
1845 && let Ok(updated) = ai_agents_core::set_dot_path(
1846 root.clone(),
1847 &manager.config().injection.context_path,
1848 value,
1849 )
1850 {
1851 root = updated;
1852 }
1853
1854 if let Value::Object(obj) = root {
1855 context = obj.into_iter().collect();
1856 }
1857
1858 context
1859 }
1860
1861 fn resolve_actor_name_from_context(&self) -> Option<String> {
1862 for path in ["actor.name", "user.name", "player.name", "customer.name"] {
1863 if let Some(value) = self.context_manager.get_path(path)
1864 && let Some(name) = value.as_str()
1865 {
1866 return Some(name.to_string());
1867 }
1868 }
1869 None
1870 }
1871
1872 async fn maybe_load_actor_relationship(&self) {
1873 let Some(manager) = self.relationship_manager.as_ref() else {
1874 return;
1875 };
1876 let Some(actor_id) = self.effective_actor_id() else {
1877 return;
1878 };
1879
1880 let mut should_fire_loaded = false;
1881 if manager.get(&actor_id).is_none() {
1882 let mut loaded = false;
1883 if manager.config().persistence.enabled {
1884 let storage = self.storage.read().clone();
1885 if let Some(storage) = storage {
1886 match storage.load_relationship(&self.info.id, &actor_id).await {
1887 Ok(Some(value)) => match manager.insert_from_value(value) {
1888 Ok(_) => loaded = true,
1889 Err(e) => {
1890 warn!(actor = %actor_id, error = %e, "failed to restore relationship")
1891 }
1892 },
1893 Ok(None) => {}
1894 Err(e) => {
1895 warn!(actor = %actor_id, error = %e, "failed to load relationship")
1896 }
1897 }
1898 }
1899 }
1900
1901 if !loaded {
1902 manager.get_or_create(&actor_id, self.resolve_actor_name_from_context().as_deref());
1903 }
1904 should_fire_loaded = true;
1905 }
1906
1907 let actor_name = self.resolve_actor_name_from_context();
1908 let relationship = manager.touch_interaction(&actor_id, actor_name.as_deref());
1909 if should_fire_loaded {
1910 self.hooks
1911 .on_relationship_loaded(&actor_id, &relationship)
1912 .await;
1913 }
1914 }
1915
1916 fn format_relationship_for_context(&self) -> Option<(String, String)> {
1917 let manager = self.relationship_manager.as_ref()?;
1918 if !manager.config().injection.enabled {
1919 return None;
1920 }
1921 let actor_id = self.effective_actor_id()?;
1922 let relationship = manager.get(&actor_id)?;
1923 let local_cap = manager.config().injection.max_tokens;
1924 let global_cap = self
1925 .memory_token_budget
1926 .as_ref()
1927 .map(|b| b.allocation.relationships as usize)
1928 .filter(|n| *n > 0);
1929 let max_tokens = global_cap.map(|g| g.min(local_cap)).unwrap_or(local_cap);
1930 let text = ai_agents_relationships::format_relationship(
1931 &relationship,
1932 &manager.config().injection.format,
1933 max_tokens,
1934 );
1935 if text.is_empty() {
1936 None
1937 } else {
1938 Some((manager.config().injection.prompt_variable.clone(), text))
1939 }
1940 }
1941
1942 async fn persist_actor_relationship(&self, actor_id: &str) -> Result<()> {
1943 let Some(manager) = self.relationship_manager.as_ref() else {
1944 return Ok(());
1945 };
1946 if !manager.config().persistence.enabled {
1947 return Ok(());
1948 }
1949 let storage = self.storage.read().clone();
1950 let Some(storage) = storage else {
1951 return Ok(());
1952 };
1953 if let Some(value) = manager.relationship_as_value(actor_id)? {
1954 storage
1955 .save_relationship(&self.info.id, actor_id, &value)
1956 .await?;
1957 }
1958 Ok(())
1959 }
1960
1961 async fn auto_update_relationship(&self) {
1962 let Some(manager) = self.relationship_manager.as_ref() else {
1963 return;
1964 };
1965 let Some(actor_id) = self.effective_actor_id() else {
1966 return;
1967 };
1968 if !manager.config().auto_update.enabled {
1969 let _ = self.persist_actor_relationship(&actor_id).await;
1970 return;
1971 }
1972
1973 let recent_messages = manager.config().auto_update.recent_messages;
1974 let messages = match self.memory.get_messages(Some(recent_messages)).await {
1975 Ok(messages) => messages,
1976 Err(e) => {
1977 warn!(actor = %actor_id, error = %e, "failed to read messages for relationship update");
1978 return;
1979 }
1980 };
1981
1982 match self
1983 .observe_purpose(
1984 ObservationPurpose::RelationshipUpdate,
1985 manager.auto_update(&actor_id, &messages),
1986 )
1987 .await
1988 {
1989 Ok(update) => {
1990 if !update.changes.is_empty() {
1991 self.hooks
1992 .on_relationship_change(&actor_id, &update.changes)
1993 .await;
1994 }
1995 if let Some(ref event) = update.event {
1996 self.hooks.on_notable_event(&actor_id, event).await;
1997 }
1998 let persisted = match self.persist_actor_relationship(&actor_id).await {
1999 Ok(()) => true,
2000 Err(e) => {
2001 warn!(actor = %actor_id, error = %e, "failed to persist relationship");
2002 false
2003 }
2004 };
2005 if !update.changes.is_empty() || update.event.is_some() {
2006 let changed_dimensions: Vec<String> = update
2007 .changes
2008 .iter()
2009 .map(|change| format!("{}:{}", change.perspective, change.dimension))
2010 .collect();
2011 info!(
2012 actor_id = %actor_id,
2013 change_count = update.changes.len(),
2014 changed_dimensions = ?changed_dimensions,
2015 event_present = update.event.is_some(),
2016 persisted = persisted,
2017 "relationship updated"
2018 );
2019 } else {
2020 debug!(actor_id = %actor_id, persisted = persisted, "relationship evaluation ran but found no changes");
2021 }
2022 }
2023 Err(e) => warn!(actor = %actor_id, error = %e, "relationship update failed"),
2024 }
2025 }
2026
2027 async fn auto_extract_facts(&self) {
2029 let should_extract = self
2030 .facts_config
2031 .as_ref()
2032 .map(|c| c.enabled && c.auto_extract)
2033 .unwrap_or(false);
2034
2035 if !should_extract {
2036 debug!("fact extraction skipped because auto extraction is disabled");
2037 return;
2038 }
2039
2040 let msgs_since = *self.messages_since_extraction.read();
2041 if msgs_since < 2 {
2042 debug!(
2043 messages_since_extraction = msgs_since,
2044 "fact extraction skipped until threshold is reached"
2045 );
2046 return;
2047 }
2048
2049 match self.extract_facts_with_source(msgs_since, "auto").await {
2050 Ok(facts) => {
2051 if !facts.is_empty() {
2052 *self.messages_since_extraction.write() = 0;
2053 } else {
2054 debug!("fact extraction ran but found no new facts");
2055 }
2056 }
2057 Err(e) => {
2058 warn!("fact extraction failed: {}", e);
2059 }
2060 }
2061 }
2062
2063 pub fn with_persona(mut self, manager: Arc<ai_agents_persona::PersonaManager>) -> Self {
2064 self.persona_manager = Some(manager);
2065 self
2066 }
2067
2068 pub fn persona_manager(&self) -> Option<&Arc<ai_agents_persona::PersonaManager>> {
2069 self.persona_manager.as_ref()
2070 }
2071
2072 pub fn with_disambiguation(mut self, config: DisambiguationConfig) -> Self {
2073 if config.is_enabled() {
2074 let manager = DisambiguationManager::new(config, Arc::clone(&self.llm_registry))
2075 .with_clarification_observer(Arc::new(ObservabilityClarificationObserver));
2076 self.disambiguation_manager = Some(manager);
2077 }
2078 self
2079 }
2080
2081 pub fn disambiguation_manager(&self) -> Option<&DisambiguationManager> {
2082 self.disambiguation_manager.as_ref()
2083 }
2084
2085 pub fn has_disambiguation(&self) -> bool {
2086 self.disambiguation_manager
2087 .as_ref()
2088 .is_some_and(|m| m.is_enabled())
2089 }
2090
2091 pub async fn init_storage(&self) -> Result<()> {
2092 let _guard = self.storage_init.lock().await;
2096 let mut storage = self.storage.read().clone();
2097 if storage.is_none() && !self.storage_config.is_none() {
2098 let storage_config = self.convert_storage_config();
2099 storage = create_storage(&storage_config).await?;
2100 *self.storage.write() = storage.clone();
2101 }
2102
2103 self.validate_storage_requirements(storage.as_deref())?;
2104 self.complete_facts_init().await;
2105 Ok(())
2106 }
2107
2108 fn validate_storage_requirements(&self, storage: Option<&dyn AgentStorage>) -> Result<()> {
2109 let facts_required = self
2110 .facts_config
2111 .as_ref()
2112 .is_some_and(|config| config.enabled)
2113 || self
2114 .actor_memory_config
2115 .as_ref()
2116 .is_some_and(|config| config.enabled);
2117 let relationships_required = self
2118 .relationship_manager
2119 .as_ref()
2120 .is_some_and(|manager| manager.config().persistence.enabled);
2121
2122 let Some(storage) = storage else {
2123 let mut requirements = Vec::new();
2124 if facts_required {
2125 requirements.push("actor facts or actor memory");
2126 }
2127 if relationships_required {
2128 requirements.push("persistent relationships");
2129 }
2130 if requirements.is_empty() {
2131 return Ok(());
2132 }
2133 return Err(AgentError::Config(format!(
2134 "Storage is required for enabled {} but none is configured or injected",
2135 requirements.join(" and ")
2136 )));
2137 };
2138
2139 if facts_required && !storage.supports(StorageCapability::ActorFacts) {
2143 return Err(AgentError::UnsupportedStorageCapability(
2144 StorageCapability::ActorFacts,
2145 ));
2146 }
2147 if relationships_required && !storage.supports(StorageCapability::ActorRelationships) {
2148 return Err(AgentError::UnsupportedStorageCapability(
2149 StorageCapability::ActorRelationships,
2150 ));
2151 }
2152 Ok(())
2153 }
2154
2155 async fn complete_facts_init(&self) {
2158 if self.fact_store.read().is_some() {
2159 return;
2160 }
2161 let storage = match self.storage.read().clone() {
2162 Some(s) => s,
2163 None => return,
2164 };
2165
2166 let facts_enabled = self
2167 .facts_config
2168 .as_ref()
2169 .map(|f| f.enabled)
2170 .unwrap_or(false);
2171 let actor_memory_enabled = self
2172 .actor_memory_config
2173 .as_ref()
2174 .map(|a| a.enabled)
2175 .unwrap_or(false);
2176
2177 if !facts_enabled && !actor_memory_enabled {
2178 return;
2179 }
2180
2181 let fc = self.facts_config.clone().unwrap_or_default();
2182 let store = Arc::new(ai_agents_facts::FactStore::new(
2183 storage,
2184 self.info.id.clone(),
2185 fc.clone(),
2186 ));
2187
2188 let extractor: Option<Arc<dyn ai_agents_facts::FactExtractor>> = if facts_enabled {
2189 let extractor_llm = fc
2190 .extractor_llm
2191 .as_ref()
2192 .and_then(|alias| self.llm_registry.get(alias).ok())
2193 .or_else(|| self.llm_registry.router().ok())
2194 .or_else(|| self.llm_registry.default().ok());
2195 extractor_llm.map(|llm| {
2196 Arc::new(ai_agents_facts::LLMFactExtractor::new(llm, fc.clone()))
2197 as Arc<dyn ai_agents_facts::FactExtractor>
2198 })
2199 } else {
2200 None
2201 };
2202
2203 *self.fact_store.write() = Some(store);
2204 *self.fact_extractor.write() = extractor;
2205 debug!(
2206 agent = %self.info.id,
2207 facts_enabled,
2208 actor_memory_enabled,
2209 "facts storage initialized"
2210 );
2211 }
2212
2213 fn convert_storage_config(&self) -> StorageStorageConfig {
2214 crate::spec::storage::to_storage_config(&self.storage_config)
2215 }
2216
2217 pub fn storage(&self) -> Option<Arc<dyn AgentStorage>> {
2218 self.storage.read().clone()
2219 }
2220
2221 pub fn storage_config(&self) -> &StorageConfig {
2222 &self.storage_config
2223 }
2224
2225 pub fn spawner(&self) -> Option<&Arc<crate::spawner::AgentSpawner>> {
2227 self.spawner.as_ref()
2228 }
2229
2230 pub fn spawner_registry(&self) -> Option<&Arc<crate::spawner::AgentRegistry>> {
2232 self.spawner_registry.as_ref()
2233 }
2234
2235 pub fn has_spawner(&self) -> bool {
2236 self.spawner_registry.is_some()
2237 }
2238
2239 pub fn with_spawner_handles(
2240 mut self,
2241 spawner: Arc<crate::spawner::AgentSpawner>,
2242 registry: Arc<crate::spawner::AgentRegistry>,
2243 ) -> Self {
2244 self.spawner = Some(spawner);
2245 self.spawner_registry = Some(registry);
2246 self
2247 }
2248
2249 pub fn with_hooks(mut self, hooks: Arc<dyn AgentHooks>) -> Self {
2250 self.hooks = hooks;
2251 self
2252 }
2253
2254 pub fn with_parallel_tools(mut self, config: ParallelToolsConfig) -> Self {
2255 self.parallel_tools = config;
2256 self
2257 }
2258
2259 pub fn with_streaming(mut self, config: StreamingConfig) -> Self {
2260 self.streaming = config;
2261 self
2262 }
2263
2264 pub fn with_hitl(mut self, engine: HITLEngine, handler: Arc<dyn ApprovalHandler>) -> Self {
2265 self.hitl_engine = Some(engine);
2266 self.approval_handler = handler;
2267 self
2268 }
2269
2270 pub fn with_max_context_tokens(mut self, tokens: u32) -> Self {
2271 self.max_context_tokens = tokens;
2272 self
2273 }
2274
2275 pub fn with_memory_token_budget(mut self, budget: MemoryTokenBudget) -> Self {
2276 self.memory_token_budget = Some(budget);
2277 self
2278 }
2279
2280 pub fn with_recovery_manager(mut self, manager: RecoveryManager) -> Self {
2281 self.recovery_manager = manager;
2282 self
2283 }
2284
2285 pub fn with_tool_security(mut self, engine: ToolSecurityEngine) -> Self {
2286 self.tool_security = engine;
2287 self
2288 }
2289
2290 pub fn runtime_control(&self) -> RuntimeControlHandle {
2292 RuntimeControlHandle {
2293 state: Arc::clone(&self.runtime_control),
2294 }
2295 }
2296
2297 pub fn set_question_handler(&self, handler: Option<Arc<dyn QuestionHandler>>) {
2299 self.tools.set_question_handler(handler);
2300 }
2301
2302 pub fn set_diagnostics_provider(&self, provider: Arc<dyn DiagnosticsProvider>) {
2304 self.tools.set_diagnostics_provider(provider);
2305 }
2306
2307 pub fn set_command_runner(&self, runner: Arc<dyn CommandRunner>) {
2309 self.tools.set_command_runner(runner);
2310 }
2311
2312 pub fn set_web_search_provider(&self, provider: Arc<dyn ai_agents_tools::WebSearchProvider>) {
2314 self.tools.set_web_search_provider(provider);
2315 }
2316
2317 pub fn todos(&self) -> Vec<TodoItem> {
2319 self.tools.todos()
2320 }
2321
2322 fn active_tool_security(&self) -> ToolSecurityEngine {
2324 self.runtime_control
2325 .tool_security_override
2326 .read()
2327 .clone()
2328 .unwrap_or_else(|| self.tool_security.clone())
2329 }
2330
2331 fn runtime_safety_snapshot(&self) -> RuntimeSafetySnapshot {
2333 let _guard = self.runtime_control.snapshot_guard.read();
2334 RuntimeSafetySnapshot {
2335 version: self.runtime_control.version.load(Ordering::SeqCst),
2336 emergency_deny: self.runtime_control.emergency_deny.load(Ordering::SeqCst),
2337 tool_security: self
2338 .runtime_control
2339 .tool_security_override
2340 .read()
2341 .clone()
2342 .unwrap_or_else(|| self.tool_security.clone()),
2343 tool_scope_override: self.runtime_control.tool_scope_override.read().clone(),
2344 }
2345 }
2346
2347 fn admit_tool_execution(
2349 &self,
2350 expected_runtime_version: u64,
2351 expected_policy_version: u64,
2352 canonical_id: &str,
2353 ) -> SecurityCheckResult {
2354 let _guard = self.runtime_control.snapshot_guard.read();
2355 if self.runtime_control.emergency_deny.load(Ordering::SeqCst) {
2356 return SecurityCheckResult::Block {
2357 reason: "runtime emergency deny is enabled".to_string(),
2358 };
2359 }
2360 let runtime_version = self.runtime_control.version.load(Ordering::SeqCst);
2361 let security_engine = self
2362 .runtime_control
2363 .tool_security_override
2364 .read()
2365 .clone()
2366 .unwrap_or_else(|| self.tool_security.clone());
2367 if runtime_version != expected_runtime_version
2368 || security_engine.policy_version() != expected_policy_version
2369 {
2370 return SecurityCheckResult::Block {
2371 reason: "runtime safety controls changed before admission".to_string(),
2372 };
2373 }
2374 security_engine.admit_tool_execution(canonical_id)
2375 }
2376
2377 pub fn with_process_processor(mut self, processor: ProcessProcessor) -> Self {
2378 let processor = processor.with_stage_observer(Arc::new(ObservabilityProcessStageObserver));
2379 self.process_processor = Some(processor);
2380 self
2381 }
2382
2383 pub fn with_state_machine(
2384 mut self,
2385 state_machine: Arc<StateMachine>,
2386 evaluator: Arc<dyn TransitionEvaluator>,
2387 ) -> Self {
2388 self.state_machine = Some(state_machine);
2389 self.transition_evaluator = Some(evaluator);
2390 self
2391 }
2392
2393 pub fn with_context_manager(mut self, manager: Arc<ContextManager>) -> Self {
2394 self.context_manager = manager;
2395 self
2396 }
2397
2398 pub fn register_message_filter(&self, name: impl Into<String>, filter: Arc<dyn MessageFilter>) {
2399 self.message_filters.write().insert(name.into(), filter);
2400 }
2401
2402 pub fn set_context(&self, key: &str, value: Value) -> Result<()> {
2403 self.context_manager.update(key, value)
2404 }
2405
2406 pub fn update_context(&self, path: &str, value: Value) -> Result<()> {
2407 self.context_manager.update(path, value)
2408 }
2409
2410 pub fn get_context(&self) -> HashMap<String, Value> {
2411 self.build_context_with_overlays()
2412 }
2413
2414 pub fn remove_context(&self, key: &str) -> Option<Value> {
2415 self.context_manager.remove(key)
2416 }
2417
2418 pub async fn refresh_context(&self, key: &str) -> Result<()> {
2419 self.context_manager.refresh(key).await
2420 }
2421
2422 pub fn register_context_provider(&self, name: &str, provider: Arc<dyn ContextProvider>) {
2423 self.context_manager.register_provider(name, provider);
2424 }
2425
2426 pub fn current_state(&self) -> Option<String> {
2427 self.state_machine.as_ref().map(|sm| sm.current())
2428 }
2429
2430 pub async fn transition_to(&self, state: &str) -> Result<()> {
2431 if let Some(ref sm) = self.state_machine {
2432 let from_state = sm.current();
2433 let history_before = sm.history();
2434 self.execute_state_exit_actions(&from_state).await;
2435 sm.transition_to(state, "manual transition")?;
2436 let entered = sm.current();
2437 let is_reentry =
2438 Self::state_was_previously_entered(&entered, &from_state, &history_before);
2439 self.execute_state_enter_actions(&entered, is_reentry).await;
2440 info!(to = %entered, "Manual state transition");
2441 }
2442 Ok(())
2443 }
2444
2445 pub fn state_history(&self) -> Vec<StateTransitionEvent> {
2446 self.state_machine
2447 .as_ref()
2448 .map(|sm| sm.history())
2449 .unwrap_or_default()
2450 }
2451
2452 pub fn session_metadata(&self) -> ai_agents_core::SessionMetadata {
2454 self.session_metadata.read().clone()
2455 }
2456
2457 pub async fn delete_actor_data(&self, actor_id: &str) -> Result<()> {
2460 let allowed = self
2461 .actor_memory_config
2462 .as_ref()
2463 .map(|c| c.privacy.allow_deletion)
2464 .unwrap_or(true);
2465 if !allowed {
2466 return Err(AgentError::Config(
2467 "privacy.allow_deletion is false; actor data deletion is not permitted".into(),
2468 ));
2469 }
2470 let storage = self.storage.read().clone();
2471 if let Some(storage) = storage {
2472 if !storage.supports(StorageCapability::ActorDataDeletion) {
2476 return Err(AgentError::UnsupportedStorageCapability(
2477 StorageCapability::ActorDataDeletion,
2478 ));
2479 }
2480 storage.delete_actor_data(&self.info.id, actor_id).await?;
2481 } else {
2482 let store = { self.fact_store.read().clone() };
2486 if let Some(store) = store {
2487 store.delete_actor_data(actor_id).await?;
2488 }
2489 }
2490 if let Some(manager) = self.relationship_manager.as_ref() {
2491 manager.remove(actor_id);
2492 }
2493 self.actor_facts_cache.write().remove(actor_id);
2494 Ok(())
2495 }
2496
2497 pub fn set_session_metadata(&self, meta: ai_agents_core::SessionMetadata) {
2499 *self.session_metadata.write() = meta;
2500 }
2501
2502 pub async fn cleanup_expired_sessions(&self) -> Result<usize> {
2504 let storage = self.storage.read().clone();
2505 match storage {
2506 Some(s) => {
2507 let count = s.cleanup_expired().await?;
2508 if count > 0 {
2509 self.hooks.on_sessions_expired(count).await;
2510 }
2511 Ok(count)
2512 }
2513 None => Err(AgentError::Config(
2514 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2515 )),
2516 }
2517 }
2518
2519 pub async fn list_sessions_filtered(
2521 &self,
2522 filter: &ai_agents_core::SessionFilter,
2523 ) -> Result<Vec<ai_agents_core::SessionSummary>> {
2524 let storage = self.storage.read().clone();
2525 match storage {
2526 Some(s) => s.list_sessions_filtered(filter).await,
2527 None => Err(AgentError::Config(
2528 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2529 )),
2530 }
2531 }
2532
2533 pub async fn save_state(&self) -> Result<AgentSnapshot> {
2534 let memory_snapshot = self.memory.snapshot().await?;
2535 let state_machine_snapshot = self.state_machine.as_ref().map(|sm| sm.snapshot());
2536 let context_snapshot = self.context_manager.snapshot();
2537
2538 let mut snapshot = AgentSnapshot::new(self.info.id.clone())
2539 .with_memory(memory_snapshot)
2540 .with_context(context_snapshot)
2541 .with_state_machine(
2542 state_machine_snapshot.unwrap_or_else(|| StateMachineSnapshot {
2543 current_state: String::new(),
2544 previous_state: None,
2545 turn_count: 0,
2546 no_transition_count: 0,
2547 history: vec![],
2548 }),
2549 );
2550
2551 if let Some(ref persona) = self.persona_manager {
2552 snapshot.persona = Some(persona.snapshot_as_value()?);
2553 }
2554
2555 if let Some(ref relationships) = self.relationship_manager {
2556 snapshot.relationships = Some(relationships.snapshot_as_value()?);
2557 }
2558
2559 Ok(snapshot)
2560 }
2561
2562 pub async fn save_state_full(&self) -> Result<AgentSnapshot> {
2564 let mut snapshot = self.save_state().await?;
2565 if let Some(ref registry) = self.spawner_registry {
2566 let entries = registry.list_with_specs();
2567 if !entries.is_empty() {
2568 snapshot = snapshot.with_spawned_agents(entries);
2569 }
2570 }
2571 Ok(snapshot)
2572 }
2573
2574 pub async fn restore_state(&self, snapshot: AgentSnapshot) -> Result<()> {
2575 self.memory.restore(snapshot.memory).await?;
2576
2577 if let (Some(sm), Some(sm_snapshot)) = (&self.state_machine, snapshot.state_machine)
2578 && !sm_snapshot.current_state.is_empty()
2579 {
2580 sm.restore(sm_snapshot)?;
2581 }
2582
2583 self.context_manager.restore(snapshot.context);
2584
2585 if let (Some(persona_value), Some(persona_manager)) =
2586 (snapshot.persona, &self.persona_manager)
2587 {
2588 persona_manager.restore_from_value(persona_value)?;
2589 }
2590
2591 if let (Some(relationship_value), Some(relationship_manager)) =
2592 (snapshot.relationships, &self.relationship_manager)
2593 {
2594 relationship_manager.restore_from_value(relationship_value)?;
2595 }
2596
2597 info!(agent_id = %snapshot.agent_id, "State restored");
2598 Ok(())
2599 }
2600
2601 pub async fn save_to(&self, storage: &dyn AgentStorage, session_id: &str) -> Result<()> {
2602 let snapshot = self.save_state().await?;
2603 storage.save(session_id, &snapshot).await
2604 }
2605
2606 async fn load_session_restore(
2607 storage: &dyn AgentStorage,
2608 session_id: &str,
2609 ) -> Result<Option<StoredSessionRestore>> {
2610 let Some(snapshot) = storage.load(session_id).await? else {
2611 return Ok(None);
2612 };
2613 let metadata = if storage.supports(StorageCapability::SessionMetadata) {
2617 storage.load_metadata(session_id).await?
2618 } else {
2619 None
2620 };
2621 Ok(Some(StoredSessionRestore { snapshot, metadata }))
2622 }
2623
2624 async fn capture_session_restore_point(&self) -> Result<RuntimeSessionRestorePoint> {
2625 Ok(RuntimeSessionRestorePoint {
2626 snapshot: self.save_state().await?,
2627 metadata: self.session_metadata(),
2628 actor_id: self.actor_id(),
2629 session_id: self.current_session_id.read().clone(),
2630 })
2631 }
2632
2633 async fn apply_session_restore_unchecked(
2634 &self,
2635 session_id: &str,
2636 stored: StoredSessionRestore,
2637 ) -> Result<()> {
2638 self.restore_state(stored.snapshot).await?;
2639 let metadata = stored.metadata.unwrap_or_default();
2640 if let Some(actor_id) = metadata.actor_id.as_deref() {
2641 self.set_actor_id(actor_id)?;
2642 } else {
2643 self.clear_actor_id();
2644 }
2645 self.set_session_metadata(metadata);
2646 *self.current_session_id.write() = Some(session_id.to_string());
2647 Ok(())
2648 }
2649
2650 async fn restore_session_restore_point(
2651 &self,
2652 restore_point: &RuntimeSessionRestorePoint,
2653 ) -> Result<()> {
2654 self.restore_state(restore_point.snapshot.clone()).await?;
2655 if let Some(actor_id) = restore_point.actor_id.as_deref() {
2656 self.set_actor_id(actor_id)?;
2657 } else {
2658 self.clear_actor_id();
2659 }
2660 self.set_session_metadata(restore_point.metadata.clone());
2661 *self.current_session_id.write() = restore_point.session_id.clone();
2662 Ok(())
2663 }
2664
2665 async fn apply_session_restore(
2666 &self,
2667 session_id: &str,
2668 stored: StoredSessionRestore,
2669 ) -> Result<()> {
2670 let before = self.capture_session_restore_point().await?;
2671 if let Err(error) = self
2672 .apply_session_restore_unchecked(session_id, stored)
2673 .await
2674 {
2675 return match self.restore_session_restore_point(&before).await {
2676 Ok(()) => Err(error),
2677 Err(rollback_error) => Err(AgentError::Other(format!(
2678 "Session restore failed: {error}; rollback failed: {rollback_error}"
2679 ))),
2680 };
2681 }
2682 Ok(())
2683 }
2684
2685 async fn rollback_session_restore_set(
2686 parent: Option<(&RuntimeAgent, &RuntimeSessionRestorePoint)>,
2687 children: &[(String, Arc<RuntimeAgent>, RuntimeSessionRestorePoint)],
2688 ) -> Vec<String> {
2689 let mut errors = Vec::new();
2690 if let Some((agent, restore_point)) = parent
2691 && let Err(error) = agent.restore_session_restore_point(restore_point).await
2692 {
2693 errors.push(format!("parent: {error}"));
2694 }
2695 for (id, agent, restore_point) in children {
2696 if let Err(error) = agent.restore_session_restore_point(restore_point).await {
2697 errors.push(format!("child '{id}': {error}"));
2698 }
2699 }
2700 errors
2701 }
2702
2703 fn restore_failure(error: impl std::fmt::Display, rollback_errors: Vec<String>) -> AgentError {
2704 if rollback_errors.is_empty() {
2705 AgentError::Other(format!(
2706 "Session restore failed: {error}; runtime state was rolled back"
2707 ))
2708 } else {
2709 AgentError::Other(format!(
2710 "Session restore failed: {error}; rollback also failed for {}",
2711 rollback_errors.join(", ")
2712 ))
2713 }
2714 }
2715
2716 pub async fn load_from(&self, storage: &dyn AgentStorage, session_id: &str) -> Result<bool> {
2717 let Some(stored) = Self::load_session_restore(storage, session_id).await? else {
2718 return Ok(false);
2719 };
2720 self.apply_session_restore(session_id, stored).await?;
2721 Ok(true)
2722 }
2723
2724 pub async fn save_session(&self, session_id: &str) -> Result<()> {
2725 let storage = self.storage.read().clone();
2726 match storage {
2727 Some(s) => {
2728 let is_new = {
2730 let cur = self.current_session_id.read().clone();
2731 cur.as_deref() != Some(session_id)
2732 };
2733 if is_new {
2734 *self.current_session_id.write() = Some(session_id.to_string());
2735 self.hooks.on_session_created(session_id).await;
2736 }
2737
2738 {
2740 let now = chrono::Utc::now();
2741 let msg_count = self
2742 .memory
2743 .get_messages(None)
2744 .await
2745 .map(|v| v.len())
2746 .unwrap_or(0);
2747 let mut meta = self.session_metadata.write();
2748 meta.last_active = now;
2749 meta.message_count = msg_count;
2750 if meta.actor_id.is_none() {
2751 meta.actor_id = self.actor_id.read().clone();
2752 }
2753 }
2754
2755 let snapshot = self.save_state().await?;
2756 if s.supports(StorageCapability::SessionMetadata) {
2760 let metadata = self.session_metadata.read().clone();
2761 s.save_snapshot_with_metadata(session_id, &snapshot, &metadata)
2762 .await
2763 } else {
2764 s.save(session_id, &snapshot).await
2765 }
2766 }
2767 None => Err(AgentError::Config(
2768 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2769 )),
2770 }
2771 }
2772
2773 pub async fn load_session(&self, session_id: &str) -> Result<bool> {
2774 let storage = self.storage.read().clone();
2775 match storage {
2776 Some(storage) => self.load_from(storage.as_ref(), session_id).await,
2777 None => Err(AgentError::Config(
2778 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2779 )),
2780 }
2781 }
2782
2783 pub async fn restore_session_full(&self, session_id: &str) -> Result<usize> {
2785 self.init_storage().await?;
2786 let storage = self.storage.read().clone().ok_or_else(|| {
2787 AgentError::Config(
2788 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2789 )
2790 })?;
2791 let target_parent = Self::load_session_restore(storage.as_ref(), session_id)
2792 .await?
2793 .ok_or_else(|| AgentError::Persistence(format!("Session not found: {session_id}")))?;
2794 let manifest = target_parent
2795 .snapshot
2796 .spawned_agents
2797 .clone()
2798 .unwrap_or_default();
2799
2800 let registry = self.spawner_registry.as_ref().cloned();
2801 let spawner = if manifest.is_empty() {
2802 self.spawner.as_ref().cloned()
2803 } else {
2804 Some(self.spawner.as_ref().cloned().ok_or_else(|| {
2805 AgentError::Config(
2806 "Saved session contains child agents but this runtime has no spawner".into(),
2807 )
2808 })?)
2809 };
2810 let registry = if manifest.is_empty() {
2811 registry
2812 } else {
2813 Some(registry.ok_or_else(|| {
2814 AgentError::Config(
2815 "Saved session contains child agents but this runtime has no registry".into(),
2816 )
2817 })?)
2818 };
2819
2820 let mut target_ids = HashSet::with_capacity(manifest.len());
2821 let mut prepared = Vec::with_capacity(manifest.len());
2822 for entry in manifest {
2823 if !target_ids.insert(entry.id.clone()) {
2824 return Err(AgentError::InvalidSpec(format!(
2825 "Saved child manifest contains duplicate ID: {}",
2826 entry.id
2827 )));
2828 }
2829 let spec = crate::spec::AgentSpec::from_yaml_strict(&entry.spec_yaml)?;
2830 spawner
2831 .as_ref()
2832 .expect("non-empty manifests require a spawner")
2833 .validate_explicit_child(&entry.id, &spec)?;
2834 prepared.push((entry.id, spec));
2835 }
2836
2837 let current_ids = registry
2838 .as_ref()
2839 .map(|registry| {
2840 registry
2841 .list()
2842 .into_iter()
2843 .map(|info| info.id)
2844 .collect::<HashSet<_>>()
2845 })
2846 .unwrap_or_default();
2847 let removal_count = current_ids.difference(&target_ids).count();
2848 let additions = prepared
2849 .iter()
2850 .filter(|(id, _)| !current_ids.contains(id))
2851 .cloned()
2852 .collect::<Vec<_>>();
2853
2854 let mut existing = Vec::new();
2855 if let Some(registry) = registry.as_ref() {
2856 for (id, _) in prepared.iter().filter(|(id, _)| current_ids.contains(id)) {
2857 let agent = registry.get(id).ok_or_else(|| {
2858 AgentError::Config(format!("Retained child disappeared during restore: {id}"))
2859 })?;
2860 let child_storage = agent.storage().ok_or_else(|| {
2861 AgentError::Config(format!("Child '{id}' has no storage for session restore"))
2862 })?;
2863 let stored = Self::load_session_restore(child_storage.as_ref(), session_id)
2864 .await?
2865 .ok_or_else(|| {
2866 AgentError::Persistence(format!(
2867 "Child '{id}' has no saved session '{session_id}'"
2868 ))
2869 })?;
2870 existing.push((id.clone(), agent, stored));
2871 }
2872 }
2873
2874 let mut staged = Vec::with_capacity(additions.len());
2875 if !additions.is_empty() {
2876 let spawner = spawner
2877 .as_ref()
2878 .expect("restored additions require a spawner");
2879 let reservations = spawner.reserve_restore_capacity(additions.len(), removal_count)?;
2880 for ((id, spec), reservation) in additions.into_iter().zip(reservations) {
2881 let spawned = spawner
2882 .spawn_with_reserved_capacity(id.clone(), spec, reservation)
2883 .await?;
2884 let child_storage = spawned.agent.storage().ok_or_else(|| {
2885 AgentError::Config(format!("Child '{id}' has no storage for session restore"))
2886 })?;
2887 let stored = Self::load_session_restore(child_storage.as_ref(), session_id)
2888 .await?
2889 .ok_or_else(|| {
2890 AgentError::Persistence(format!(
2891 "Child '{id}' has no saved session '{session_id}'"
2892 ))
2893 })?;
2894 staged.push((spawned, stored));
2895 }
2896 } else if let Some(spawner) = spawner.as_ref() {
2897 spawner.reserve_restore_capacity(0, removal_count)?;
2898 }
2899
2900 let parent_before = self.capture_session_restore_point().await?;
2901 let mut existing_before = Vec::with_capacity(existing.len());
2902 for (id, agent, _) in &existing {
2903 existing_before.push((
2904 id.clone(),
2905 Arc::clone(agent),
2906 agent.capture_session_restore_point().await?,
2907 ));
2908 }
2909
2910 for (_, agent, stored) in &existing {
2914 if let Err(error) = agent
2915 .apply_session_restore_unchecked(session_id, stored.clone())
2916 .await
2917 {
2918 drop(staged);
2919 let rollback_errors =
2920 Self::rollback_session_restore_set(None, &existing_before).await;
2921 return Err(Self::restore_failure(error, rollback_errors));
2922 }
2923 }
2924 for (spawned, stored) in &staged {
2925 if let Err(error) = spawned
2926 .agent
2927 .apply_session_restore_unchecked(session_id, stored.clone())
2928 .await
2929 {
2930 drop(staged);
2931 let rollback_errors =
2932 Self::rollback_session_restore_set(None, &existing_before).await;
2933 return Err(Self::restore_failure(error, rollback_errors));
2934 }
2935 }
2936 if let Err(error) = self
2937 .apply_session_restore_unchecked(session_id, target_parent)
2938 .await
2939 {
2940 drop(staged);
2941 let rollback_errors =
2942 Self::rollback_session_restore_set(Some((self, &parent_before)), &existing_before)
2943 .await;
2944 return Err(Self::restore_failure(error, rollback_errors));
2945 }
2946
2947 if let Some(registry) = registry.as_ref()
2948 && let Err(error) = registry
2949 .reconcile(
2950 &target_ids,
2951 staged.into_iter().map(|(spawned, _)| spawned).collect(),
2952 )
2953 .await
2954 {
2955 let rollback_errors =
2956 Self::rollback_session_restore_set(Some((self, &parent_before)), &existing_before)
2957 .await;
2958 return Err(Self::restore_failure(error, rollback_errors));
2959 }
2960
2961 Ok(target_ids.len())
2962 }
2963
2964 pub async fn delete_session(&self, session_id: &str) -> Result<()> {
2965 let storage = self.storage.read().clone();
2966 match storage {
2967 Some(s) => s.delete(session_id).await,
2968 None => Err(AgentError::Config(
2969 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2970 )),
2971 }
2972 }
2973
2974 pub async fn list_sessions(&self) -> Result<Vec<String>> {
2975 let storage = self.storage.read().clone();
2976 match storage {
2977 Some(s) => s.list_sessions().await,
2978 None => Err(AgentError::Config(
2979 "No storage configured. Use with_storage_config() or with_storage() first".into(),
2980 )),
2981 }
2982 }
2983
2984 fn estimate_tokens(&self, text: &str) -> u32 {
2985 (text.len() as f32 / 4.0).ceil() as u32
2986 }
2987
2988 fn estimate_total_tokens(&self, messages: &[ChatMessage]) -> u32 {
2989 messages
2990 .iter()
2991 .map(|m| self.estimate_tokens(&m.content))
2992 .sum()
2993 }
2994
2995 fn truncate_context(&self, messages: &mut Vec<ChatMessage>, keep_recent: usize) {
2996 if messages.len() <= keep_recent + 1 {
2997 return;
2998 }
2999 let system_msg = messages.remove(0);
3000 let to_remove = messages.len().saturating_sub(keep_recent);
3001 messages.drain(..to_remove);
3002 messages.insert(0, system_msg);
3003 }
3004
3005 fn get_filter(&self, config: &FilterConfig) -> Arc<dyn MessageFilter> {
3006 match config {
3007 FilterConfig::KeepRecent(n) => Arc::new(KeepRecentFilter::new(*n)),
3008 FilterConfig::ByRole { keep_roles } => Arc::new(ByRoleFilter::new(keep_roles.clone())),
3009 FilterConfig::SkipPattern { skip_if_contains } => {
3010 Arc::new(SkipPatternFilter::new(skip_if_contains.clone()))
3011 }
3012 FilterConfig::Custom { name } => {
3013 let filters = self.message_filters.read();
3014 filters
3015 .get(name)
3016 .cloned()
3017 .unwrap_or_else(|| Arc::new(KeepRecentFilter::new(10)))
3018 }
3019 }
3020 }
3021
3022 async fn summarize_context(
3023 &self,
3024 messages: &mut Vec<ChatMessage>,
3025 summarizer_llm: Option<&str>,
3026 max_summary_tokens: u32,
3027 custom_prompt: Option<&str>,
3028 keep_recent: usize,
3029 filter: Option<&FilterConfig>,
3030 ) -> Result<()> {
3031 let system_msg = messages.remove(0);
3032
3033 let to_summarize_count = messages.len().saturating_sub(keep_recent);
3034 if to_summarize_count == 0 {
3035 messages.insert(0, system_msg);
3036 return Ok(());
3037 }
3038
3039 let recent_msgs: Vec<ChatMessage> = messages.drain(to_summarize_count..).collect();
3040 let mut to_summarize = std::mem::take(messages);
3041
3042 if let Some(filter_config) = filter {
3043 let filter = self.get_filter(filter_config);
3044 to_summarize = filter.filter(to_summarize);
3045 }
3046
3047 if to_summarize.is_empty() {
3048 *messages = recent_msgs;
3049 messages.insert(0, system_msg);
3050 return Ok(());
3051 }
3052
3053 let conversation_text = to_summarize
3054 .iter()
3055 .map(|m| format!("{:?}: {}", m.role, m.content))
3056 .collect::<Vec<_>>()
3057 .join("\n");
3058
3059 let default_prompt = format!(
3060 "Summarize the following conversation in under {} tokens, preserving key information:\n\n{}",
3061 max_summary_tokens, conversation_text
3062 );
3063
3064 let summary_prompt = custom_prompt
3065 .map(|p| format!("{}\n\n{}", p, conversation_text))
3066 .unwrap_or(default_prompt);
3067
3068 let summarizer = if let Some(alias) = summarizer_llm {
3069 self.llm_registry
3070 .get(alias)
3071 .map_err(|e| AgentError::Config(e.to_string()))?
3072 } else {
3073 self.llm_registry
3074 .router()
3075 .or_else(|_| self.llm_registry.default())
3076 .map_err(|e| AgentError::Config(e.to_string()))?
3077 };
3078
3079 let summary_msgs = vec![ChatMessage::user(&summary_prompt)];
3080 let response = self
3081 .observe_purpose(
3082 ObservationPurpose::Summarization,
3083 summarizer.complete(&summary_msgs, None),
3084 )
3085 .await?;
3086
3087 let summary_message = ChatMessage::system(format!(
3088 "[Previous conversation summary]\n{}",
3089 response.content
3090 ));
3091
3092 *messages = vec![system_msg, summary_message];
3093 messages.extend(recent_msgs);
3094
3095 debug!(
3096 summarized_count = to_summarize_count,
3097 kept_recent = keep_recent,
3098 "Context summarized"
3099 );
3100
3101 Ok(())
3102 }
3103
3104 fn render_system_prompt(&self) -> Result<String> {
3105 let mut context = self.build_context_with_overlays();
3106
3107 let facts_text = self.format_actor_facts_for_context();
3109 if !facts_text.is_empty() {
3110 context.insert(
3111 "actor_facts".to_string(),
3112 serde_json::Value::String(facts_text),
3113 );
3114 }
3115
3116 if let Some((key, text)) = self.format_relationship_for_context() {
3117 context.insert(key, serde_json::Value::String(text));
3118 }
3119
3120 self.template_renderer
3121 .render(&self.base_system_prompt, &context)
3122 }
3123
3124 fn get_top_level_tool_ids_for_scope(&self, scope_override: Option<&[String]>) -> Vec<String> {
3126 scope_override
3127 .or(self.declared_tool_ids.as_deref())
3128 .map(|ids| {
3129 ids.iter()
3130 .filter_map(|id| self.tools.canonical_id(id))
3131 .collect::<Vec<_>>()
3132 })
3133 .unwrap_or_default()
3134 }
3135
3136 async fn get_available_tool_ids(&self) -> Result<Vec<String>> {
3137 let scope_override = self.runtime_control.tool_scope_override.read().clone();
3138 self.get_available_tool_ids_for_scope(scope_override.as_deref())
3139 .await
3140 }
3141
3142 async fn get_available_tool_ids_for_scope(
3144 &self,
3145 scope_override: Option<&[String]>,
3146 ) -> Result<Vec<String>> {
3147 let top_level = self.get_top_level_tool_ids_for_scope(scope_override);
3148 if top_level.is_empty() {
3149 return Ok(Vec::new());
3150 }
3151
3152 match self.get_current_tool_refs() {
3153 Some(tool_refs) => {
3154 if tool_refs.is_empty() {
3155 return Ok(Vec::new());
3156 }
3157
3158 let eval_ctx = self.build_evaluation_context().await?;
3159 let llm_getter = RegistryLLMGetter {
3160 registry: self.llm_registry.clone(),
3161 };
3162 let evaluator = ConditionEvaluator::new(llm_getter);
3163
3164 let mut available = Vec::new();
3165 for tool_ref in &tool_refs {
3166 let tool_id = tool_ref.id();
3167 let Some(canonical_id) = self.tools.canonical_id(tool_id) else {
3168 continue;
3169 };
3170 if !top_level.iter().any(|id| id == &canonical_id) {
3171 debug!(tool = %canonical_id, "Tool not in top-level grant, skipping");
3172 continue;
3173 }
3174
3175 if let Some(condition) = tool_ref.condition() {
3176 match evaluator.evaluate(condition, &eval_ctx).await {
3177 Ok(true) => {
3178 available.push(canonical_id);
3179 }
3180 Ok(false) => {
3181 debug!(tool = tool_id, "Tool condition not met, skipping");
3182 }
3183 Err(e) => {
3184 warn!(tool = tool_id, error = %e, "Error evaluating tool condition");
3185 }
3186 }
3187 } else {
3188 available.push(canonical_id);
3189 }
3190 }
3191
3192 Ok(available)
3193 }
3194 None => Ok(top_level),
3195 }
3196 }
3197
3198 fn get_current_tool_refs(&self) -> Option<Vec<ToolRef>> {
3202 if let Some(ref sm) = self.state_machine
3203 && let Some(state_def) = sm.current_definition()
3204 {
3205 let parent_def = sm.get_parent_definition();
3206 if let Some(effective) = state_def.get_effective_tools(parent_def.as_ref()) {
3207 return Some(effective.into_iter().cloned().collect());
3208 }
3209 }
3210 None
3211 }
3212
3213 async fn build_evaluation_context(&self) -> Result<EvaluationContext> {
3214 let context = self.build_context_with_overlays();
3215 let messages = self.memory.get_messages(Some(10)).await?;
3216 let tool_history = self.tool_call_history.read().clone();
3217
3218 let (state_name, turn_count, previous_state) = if let Some(ref sm) = self.state_machine {
3219 (Some(sm.current()), sm.turn_count(), sm.previous())
3220 } else {
3221 (None, 0, None)
3222 };
3223
3224 Ok(EvaluationContext::default()
3225 .with_context(context)
3226 .with_state(state_name, turn_count, previous_state)
3227 .with_called_tools(tool_history)
3228 .with_messages(messages))
3229 }
3230
3231 fn record_tool_call(&self, tool_id: &str, result: Value) {
3232 self.tool_call_history.write().push(ToolCallRecord {
3233 tool_id: tool_id.to_string(),
3234 result,
3235 timestamp: chrono::Utc::now(),
3236 });
3237 }
3238
3239 async fn get_effective_system_prompt_with_persona_hooks(
3240 &self,
3241 fire_persona_hooks: bool,
3242 include_tool_prompt: bool,
3243 ) -> Result<String> {
3244 let rendered_base = self.render_system_prompt()?;
3245
3246 let persona_prefix = if let Some(ref persona) = self.persona_manager {
3247 let context = self.build_context_with_overlays();
3248 if fire_persona_hooks {
3249 let render_result = persona.render_prompt(&context)?;
3250 for content in &render_result.newly_revealed {
3251 self.hooks.on_secret_revealed(content).await;
3252 }
3253 render_result.prompt
3254 } else {
3255 persona.render_prompt_preview(&context)?
3256 }
3257 } else {
3258 String::new()
3259 };
3260
3261 if let Some(ref sm) = self.state_machine
3262 && let Some(state_def) = sm.current_definition()
3263 {
3264 let state_prompt = if let Some(ref prompt) = state_def.prompt {
3265 let context = self.build_context_with_overlays();
3266 self.template_renderer.render_with_state(
3267 prompt,
3268 &context,
3269 &sm.current(),
3270 sm.previous().as_deref(),
3271 sm.turn_count(),
3272 state_def.max_turns,
3273 )?
3274 } else {
3275 String::new()
3276 };
3277
3278 let combined = match state_def.prompt_mode {
3279 PromptMode::Append => {
3280 if state_prompt.is_empty() {
3281 rendered_base
3282 } else {
3283 format!(
3284 "{}\n\n[Current State: {}]\n{}",
3285 rendered_base,
3286 sm.current(),
3287 state_prompt
3288 )
3289 }
3290 }
3291 PromptMode::Replace => {
3292 if state_prompt.is_empty() {
3293 rendered_base
3294 } else {
3295 state_prompt
3296 }
3297 }
3298 PromptMode::Prepend => {
3299 if state_prompt.is_empty() {
3300 rendered_base
3301 } else {
3302 format!("{}\n\n{}", state_prompt, rendered_base)
3303 }
3304 }
3305 };
3306
3307 let with_persona = if persona_prefix.is_empty() {
3309 combined
3310 } else {
3311 format!("{}\n\n{}", persona_prefix, combined)
3312 };
3313
3314 if include_tool_prompt {
3315 let available_tool_ids = self.get_available_tool_ids().await?;
3316 if !available_tool_ids.is_empty() {
3317 let tools_prompt = self.tools.generate_scoped_prompt_with_mode(
3318 &available_tool_ids,
3319 None,
3320 self.parallel_tools.enabled,
3321 self.runtime_config.tool_schema_prompt_mode,
3322 );
3323 if !tools_prompt.is_empty() {
3324 return Ok(format!("{}\n\n{}", with_persona, tools_prompt));
3325 }
3326 }
3327 }
3328 return Ok(with_persona);
3329 }
3330
3331 let with_persona = if persona_prefix.is_empty() {
3333 rendered_base
3334 } else {
3335 format!("{}\n\n{}", persona_prefix, rendered_base)
3336 };
3337
3338 if include_tool_prompt {
3339 let available_tool_ids = self.get_available_tool_ids().await?;
3340 let tools_prompt = self.tools.generate_scoped_prompt_with_mode(
3341 &available_tool_ids,
3342 None,
3343 self.parallel_tools.enabled,
3344 self.runtime_config.tool_schema_prompt_mode,
3345 );
3346 if !tools_prompt.is_empty() {
3347 return Ok(format!("{}\n\n{}", with_persona, tools_prompt));
3348 }
3349 }
3350 Ok(with_persona)
3351 }
3352
3353 fn get_state_llm(&self) -> Result<Arc<dyn LLMProvider>> {
3354 if let Some(ref sm) = self.state_machine
3355 && let Some(state_def) = sm.current_definition()
3356 && let Some(ref llm_alias) = state_def.llm
3357 {
3358 return self
3359 .llm_registry
3360 .get(llm_alias)
3361 .map_err(|e| AgentError::Config(e.to_string()));
3362 }
3363 self.llm_registry
3364 .default()
3365 .map_err(|e| AgentError::Config(e.to_string()))
3366 }
3367
3368 fn get_effective_reasoning_config(&self) -> ReasoningConfig {
3369 if let Some(ref sm) = self.state_machine
3370 && let Some(state_def) = sm.current_definition()
3371 && let Some(ref state_reasoning) = state_def.reasoning
3372 {
3373 return state_reasoning.clone();
3374 }
3375 self.reasoning_config.clone()
3376 }
3377
3378 fn get_effective_reflection_config(&self) -> ReflectionConfig {
3379 if let Some(ref sm) = self.state_machine
3380 && let Some(state_def) = sm.current_definition()
3381 && let Some(ref state_reflection) = state_def.reflection
3382 {
3383 return state_reflection.clone();
3384 }
3385 self.reflection_config.clone()
3386 }
3387
3388 fn get_skill_reasoning_config(&self, skill: &SkillDefinition) -> ReasoningConfig {
3389 skill
3390 .reasoning
3391 .clone()
3392 .unwrap_or_else(|| self.get_effective_reasoning_config())
3393 }
3394
3395 fn get_skill_reflection_config(&self, skill: &SkillDefinition) -> ReflectionConfig {
3396 skill
3397 .reflection
3398 .clone()
3399 .unwrap_or_else(|| self.get_effective_reflection_config())
3400 }
3401
3402 async fn build_disambiguation_context(&self) -> Result<DisambiguationContext> {
3403 let recent_messages: Vec<String> = self
3404 .memory
3405 .get_messages(Some(5))
3406 .await?
3407 .iter()
3408 .rev()
3409 .map(|m| format!("{:?}: {}", m.role, m.content))
3410 .collect();
3411
3412 let current_state = self.current_state().map(|s| s.to_string());
3413
3414 let state_prompt: Option<String> = self
3417 .state_machine
3418 .as_ref()
3419 .and_then(|sm| sm.current_definition())
3420 .and_then(|def| def.prompt.clone());
3421
3422 let available_tools: Vec<String> = self
3423 .get_available_tool_ids()
3424 .await
3425 .unwrap_or_else(|_| self.tools.list_ids());
3426
3427 let available_skills: Vec<String> = self.skills.iter().map(|s| s.id.clone()).collect();
3428
3429 let user_context = self.build_context_with_overlays();
3430
3431 let available_intents: Vec<String> = if let Some(ref sm) = self.state_machine {
3433 sm.current_definition()
3434 .map(|def| {
3435 def.transitions
3436 .iter()
3437 .filter_map(|t| t.intent.clone())
3438 .collect()
3439 })
3440 .unwrap_or_default()
3441 } else {
3442 Vec::new()
3443 };
3444
3445 Ok(DisambiguationContext::from_agent_state(
3446 recent_messages,
3447 current_state,
3448 state_prompt,
3449 available_tools,
3450 available_skills,
3451 available_intents,
3452 user_context,
3453 ))
3454 }
3455
3456 fn get_available_skills(&self) -> Vec<&SkillDefinition> {
3457 if let Some(ref sm) = self.state_machine
3458 && let Some(state_def) = sm.current_definition()
3459 {
3460 let parent_def = sm.get_parent_definition();
3461 let effective_skills = state_def.get_effective_skills(parent_def.as_ref());
3462 if !effective_skills.is_empty() {
3463 return self
3464 .skills
3465 .iter()
3466 .filter(|s| effective_skills.contains(&&s.id))
3467 .collect();
3468 }
3469 }
3470 self.skills.iter().collect()
3471 }
3472
3473 async fn build_messages(&self) -> Result<Vec<ChatMessage>> {
3474 self.build_messages_internal(true, None, true).await
3475 }
3476
3477 async fn build_messages_for_draft(&self, user_message: &str) -> Result<Vec<ChatMessage>> {
3478 self.build_messages_internal(false, Some(user_message), true)
3479 .await
3480 }
3481
3482 async fn build_messages_internal(
3483 &self,
3484 fire_persona_hooks: bool,
3485 ephemeral_user_message: Option<&str>,
3486 include_tool_prompt: bool,
3487 ) -> Result<Vec<ChatMessage>> {
3488 let system_prompt = self
3489 .get_effective_system_prompt_with_persona_hooks(fire_persona_hooks, include_tool_prompt)
3490 .await?;
3491 let mut messages = vec![ChatMessage::system(&system_prompt)];
3492
3493 let context = self.memory.get_context().await?;
3494 let history = if let Some(ref budget) = self.memory_token_budget {
3495 context.to_llm_messages_with_allocation(&budget.allocation)
3496 } else {
3497 context.to_llm_messages()
3498 };
3499 messages.extend(history);
3500 if let Some(user_message) = ephemeral_user_message {
3501 messages.push(ChatMessage::user(user_message));
3502 }
3503
3504 let total_tokens = self.estimate_total_tokens(&messages);
3505
3506 if total_tokens > self.max_context_tokens {
3507 debug!(
3508 total = total_tokens,
3509 limit = self.max_context_tokens,
3510 "Context overflow"
3511 );
3512
3513 match &self.recovery_manager.config().llm.on_context_overflow {
3514 ContextOverflowAction::Error => {
3515 return Err(AgentError::LLM(format!(
3516 "Context overflow: {} tokens > {} limit",
3517 total_tokens, self.max_context_tokens
3518 )));
3519 }
3520 ContextOverflowAction::Truncate { keep_recent } => {
3521 self.truncate_context(&mut messages, *keep_recent);
3522 }
3523 ContextOverflowAction::Summarize {
3524 summarizer_llm,
3525 max_summary_tokens,
3526 custom_prompt,
3527 keep_recent,
3528 filter,
3529 } => {
3530 self.summarize_context(
3531 &mut messages,
3532 summarizer_llm.as_deref(),
3533 *max_summary_tokens,
3534 custom_prompt.as_deref(),
3535 *keep_recent,
3536 filter.as_ref(),
3537 )
3538 .await?;
3539 }
3540 }
3541 }
3542
3543 Ok(messages)
3544 }
3545
3546 async fn main_tool_protocol(
3547 &self,
3548 llm: &dyn LLMProvider,
3549 ephemeral_new_turn: bool,
3550 ) -> Result<MainToolProtocol> {
3551 let mut choice = llm.configured_tool_choice();
3552 if matches!(choice.as_ref(), Some(ToolChoice::None)) {
3553 return Ok(MainToolProtocol {
3554 choice,
3555 tool_ids: Vec::new(),
3556 definitions: Vec::new(),
3557 });
3558 }
3559
3560 let mut tool_ids = self.get_available_tool_ids().await?;
3561 tool_ids.sort();
3562 tool_ids.dedup();
3563 if let Some(ToolChoice::Specific(expected)) = choice.as_ref() {
3564 let canonical = self.tools.canonical_id(expected).ok_or_else(|| {
3565 AgentError::Config(format!(
3566 "specific tool choice '{expected}' is not registered"
3567 ))
3568 })?;
3569 if canonical != *expected {
3570 return Err(AgentError::Config(format!(
3571 "specific tool choice must use canonical ID '{canonical}', not '{expected}'"
3572 )));
3573 }
3574 if !tool_ids.iter().any(|tool_id| tool_id == expected) {
3575 return Err(AgentError::Config(format!(
3576 "specific tool choice '{expected}' is outside the effective tool grant"
3577 )));
3578 }
3579 }
3580 if matches!(
3581 choice.as_ref(),
3582 Some(ToolChoice::Required | ToolChoice::Specific(_))
3583 ) && tool_ids.is_empty()
3584 {
3585 return Err(AgentError::Config(
3586 "required tool choice has no tool inside the effective grant".to_string(),
3587 ));
3588 }
3589 if !ephemeral_new_turn
3590 && let Some(configured_choice) = choice.as_ref()
3591 && matches!(
3592 configured_choice,
3593 ToolChoice::Required | ToolChoice::Specific(_)
3594 )
3595 && self
3596 .tool_choice_satisfied_in_current_turn(configured_choice, &tool_ids)
3597 .await?
3598 {
3599 choice = Some(ToolChoice::Auto);
3600 }
3601 if let Some(ToolChoice::Specific(expected)) = choice.as_ref() {
3602 tool_ids.retain(|tool_id| tool_id == expected);
3603 }
3604
3605 let definitions = tool_ids
3606 .iter()
3607 .map(|tool_id| {
3608 let tool = self.tools.get(tool_id).ok_or_else(|| {
3609 AgentError::Config(format!(
3610 "effective tool '{tool_id}' disappeared before provider exposure"
3611 ))
3612 })?;
3613 Ok(LLMToolDefinition {
3614 name: tool_id.clone(),
3615 description: tool.description().to_string(),
3616 input_schema: tool.input_schema(),
3617 })
3618 })
3619 .collect::<Result<Vec<_>>>()?;
3620
3621 Ok(MainToolProtocol {
3625 choice,
3626 tool_ids,
3627 definitions,
3628 })
3629 }
3630
3631 async fn tool_choice_satisfied_in_current_turn(
3632 &self,
3633 choice: &ToolChoice,
3634 effective_tool_ids: &[String],
3635 ) -> Result<bool> {
3636 let messages = self.memory.get_messages(None).await?;
3637 let mut saw_tool_result = false;
3638 for message in messages.iter().rev() {
3639 match message.role {
3640 ai_agents_core::Role::Tool | ai_agents_core::Role::Function => {
3641 saw_tool_result = true;
3642 }
3643 ai_agents_core::Role::Assistant if saw_tool_result => {
3644 let Some(calls) = self.parse_tool_calls(&message.content) else {
3645 continue;
3646 };
3647 let calls_are_effective = !calls.is_empty()
3648 && calls.iter().all(|call| {
3649 self.tools
3650 .canonical_id(&call.name)
3651 .is_some_and(|canonical| effective_tool_ids.contains(&canonical))
3652 });
3653 return Ok(calls_are_effective
3654 && match choice {
3655 ToolChoice::Required => true,
3656 ToolChoice::Specific(expected) => calls.iter().all(|call| {
3657 self.tools.canonical_id(&call.name).as_deref()
3658 == Some(expected.as_str())
3659 }),
3660 _ => false,
3661 });
3662 }
3663 ai_agents_core::Role::User => return Ok(false),
3664 _ => {}
3665 }
3666 }
3667 Ok(false)
3668 }
3669
3670 fn provider_can_use_native_tools(
3671 &self,
3672 llm: &dyn LLMProvider,
3673 protocol: &MainToolProtocol,
3674 ) -> bool {
3675 let Some(choice) = protocol.choice.as_ref() else {
3676 return false;
3677 };
3678 if matches!(choice, ToolChoice::None) || protocol.definitions.is_empty() {
3679 return false;
3680 }
3681 llm.supports_tool_choice(choice)
3682 && protocol.definitions.iter().all(|definition| {
3683 !definition.name.is_empty()
3684 && definition.name.len() <= 64
3685 && definition
3686 .name
3687 .bytes()
3688 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
3689 })
3690 }
3691
3692 fn prompt_messages_for_tool_protocol(
3693 &self,
3694 messages: &[ChatMessage],
3695 protocol: &MainToolProtocol,
3696 corrective: bool,
3697 ) -> Vec<ChatMessage> {
3698 let mut messages = messages.to_vec();
3699 let Some(choice) = protocol.choice.as_ref() else {
3700 return messages;
3701 };
3702 if matches!(choice, ToolChoice::None) || protocol.tool_ids.is_empty() {
3703 return messages;
3704 }
3705
3706 let mut tool_prompt = self.tools.generate_scoped_prompt_with_mode(
3707 &protocol.tool_ids,
3708 None,
3709 self.parallel_tools.enabled,
3710 self.runtime_config.tool_schema_prompt_mode,
3711 );
3712 match choice {
3713 ToolChoice::Required => tool_prompt.push_str(
3714 "\n\nYou must call at least one listed tool before giving a final answer.",
3715 ),
3716 ToolChoice::Specific(tool_id) => tool_prompt.push_str(&format!(
3717 "\n\nYou must call the '{tool_id}' tool before giving a final answer."
3718 )),
3719 ToolChoice::Auto => {}
3720 ToolChoice::None => return messages,
3721 _ => return messages,
3722 }
3723 if let Some(system) = messages
3724 .iter_mut()
3725 .find(|message| message.role == ai_agents_core::Role::System)
3726 {
3727 system.content.push_str("\n\n");
3728 system.content.push_str(&tool_prompt);
3729 } else {
3730 messages.insert(0, ChatMessage::system(tool_prompt));
3731 }
3732 if corrective {
3733 let instruction = match choice {
3734 ToolChoice::Required => {
3735 "Your previous response did not call a required tool. Call at least one listed tool now and return only the JSON tool call."
3736 }
3737 ToolChoice::Specific(tool_id) => {
3738 messages.push(ChatMessage::user(format!(
3739 "Your previous response did not call the required '{tool_id}' tool. Call it now and return only the JSON tool call."
3740 )));
3741 return messages;
3742 }
3743 _ => return messages,
3744 };
3745 messages.push(ChatMessage::user(instruction));
3746 }
3747 messages
3748 }
3749
3750 async fn invoke_main_provider(
3751 &self,
3752 llm: Arc<dyn LLMProvider>,
3753 messages: &[ChatMessage],
3754 protocol: &MainToolProtocol,
3755 corrective: bool,
3756 ) -> std::result::Result<MainProviderResponse, LLMError> {
3757 let use_native = self.provider_can_use_native_tools(llm.as_ref(), protocol);
3758 let response = if use_native {
3759 let request = LLMToolRequest {
3760 tools: protocol.definitions.clone(),
3761 choice: protocol
3762 .choice
3763 .clone()
3764 .expect("native tool requests require an explicit choice"),
3765 };
3766 self.observe_purpose(
3767 ObservationPurpose::MainResponse,
3768 llm.complete_with_tools(messages, None, &request),
3769 )
3770 .await?
3771 } else {
3772 let prompt_messages =
3773 self.prompt_messages_for_tool_protocol(messages, protocol, corrective);
3774 self.observe_purpose(
3775 ObservationPurpose::MainResponse,
3776 llm.complete(&prompt_messages, None),
3777 )
3778 .await?
3779 };
3780 Ok(MainProviderResponse {
3781 response,
3782 used_native_tools: use_native,
3783 })
3784 }
3785
3786 async fn complete_main_attempt_with_recovery(
3787 &self,
3788 llm: Arc<dyn LLMProvider>,
3789 messages: &[ChatMessage],
3790 protocol: &MainToolProtocol,
3791 corrective: bool,
3792 ) -> Result<MainProviderResponse> {
3793 let primary_result = if self.recovery_manager.config().default.max_retries > 0 {
3794 self.recovery_manager
3795 .with_retry("llm_call", None, || {
3796 let llm = Arc::clone(&llm);
3797 async move {
3798 self.invoke_main_provider(llm, messages, protocol, corrective)
3799 .await
3800 .map_err(|error| error.classify())
3801 }
3802 })
3803 .await
3804 .map_err(|error| AgentError::LLM(error.to_string()))
3805 } else {
3806 self.invoke_main_provider(Arc::clone(&llm), messages, protocol, corrective)
3807 .await
3808 .map_err(|error| AgentError::LLM(error.to_string()))
3809 };
3810
3811 match primary_result {
3812 Ok(response) => Ok(response),
3813 Err(primary_error) => match &self.recovery_manager.config().llm.on_failure {
3814 LLMFailureAction::FallbackLlm { fallback_llm } => {
3815 let fallback = self.llm_registry.get(fallback_llm).map_err(|error| {
3816 AgentError::Config(format!(
3817 "Fallback LLM '{fallback_llm}' not found: {error}"
3818 ))
3819 })?;
3820 self.invoke_main_provider(fallback, messages, protocol, corrective)
3821 .await
3822 .map_err(|error| AgentError::LLM(error.to_string()))
3823 }
3824 LLMFailureAction::FallbackResponse { message } => {
3825 if matches!(
3826 protocol.choice.as_ref(),
3827 Some(ToolChoice::Required | ToolChoice::Specific(_))
3828 ) {
3829 Err(AgentError::LLM(format!(
3830 "Required tool selection failed and cannot be satisfied by a static fallback response: {primary_error}"
3831 )))
3832 } else {
3833 Ok(MainProviderResponse {
3834 response: LLMResponse::new(message.clone(), FinishReason::Stop),
3835 used_native_tools: false,
3836 })
3837 }
3838 }
3839 LLMFailureAction::Error => Err(primary_error),
3840 },
3841 }
3842 }
3843
3844 fn normalize_main_provider_response(
3845 &self,
3846 mut response: LLMResponse,
3847 protocol: &MainToolProtocol,
3848 ) -> Result<(LLMResponse, bool)> {
3849 let native_calls = response
3850 .tool_calls()
3851 .map_err(|error| AgentError::LLM(error.to_string()))?;
3852 let calls = match native_calls {
3853 Some(calls) => {
3854 let markers = calls
3855 .iter()
3856 .map(|call| {
3857 serde_json::json!({
3858 "_ai_agents_native_tool_call": true,
3859 "id": call.id,
3860 "tool": call.name,
3861 "arguments": call.arguments,
3862 })
3863 })
3864 .collect::<Vec<_>>();
3865 response.content = if markers.len() == 1 {
3866 markers[0].to_string()
3867 } else {
3868 serde_json::Value::Array(markers).to_string()
3869 };
3870 Some(calls)
3871 }
3872 None if !matches!(protocol.choice.as_ref(), Some(ToolChoice::None)) => {
3873 self.parse_tool_calls(response.content.trim())
3874 }
3875 None => None,
3876 };
3877
3878 if protocol.choice.is_some()
3879 && let Some(calls) = calls.as_ref()
3880 && calls.iter().any(|call| {
3881 self.tools
3882 .canonical_id(&call.name)
3883 .is_none_or(|canonical| !protocol.tool_ids.contains(&canonical))
3884 })
3885 {
3886 return Err(AgentError::LLM(
3887 "Provider returned a tool call outside the effective grant".to_string(),
3888 ));
3889 }
3890
3891 let compliant = match protocol.choice.as_ref() {
3892 Some(ToolChoice::Required) => calls.as_ref().is_some_and(|calls| !calls.is_empty()),
3893 Some(ToolChoice::Specific(expected)) => calls.as_ref().is_some_and(|calls| {
3894 !calls.is_empty()
3895 && calls.iter().all(|call| {
3896 self.tools.canonical_id(&call.name).as_deref() == Some(expected.as_str())
3897 })
3898 }),
3899 _ => true,
3900 };
3901 Ok((response, compliant))
3902 }
3903
3904 async fn complete_main_llm_with_recovery(
3905 &self,
3906 llm: Arc<dyn LLMProvider>,
3907 messages: &[ChatMessage],
3908 protocol: &MainToolProtocol,
3909 ) -> Result<LLMResponse> {
3910 let first = self
3911 .complete_main_attempt_with_recovery(Arc::clone(&llm), messages, protocol, false)
3912 .await?;
3913 let (response, compliant) =
3914 self.normalize_main_provider_response(first.response, protocol)?;
3915 if compliant {
3916 return Ok(response);
3917 }
3918 if first.used_native_tools {
3919 return Err(AgentError::LLM(
3920 "Provider returned no compliant native call for required tool choice".to_string(),
3921 ));
3922 }
3923
3924 let corrected = self
3925 .complete_main_attempt_with_recovery(llm, messages, protocol, true)
3926 .await?;
3927 let (response, compliant) =
3928 self.normalize_main_provider_response(corrected.response, protocol)?;
3929 if compliant {
3930 return Ok(response);
3931 }
3932 Err(AgentError::LLM(
3933 "Provider returned no compliant tool call after one corrective retry".to_string(),
3934 ))
3935 }
3936
3937 fn is_native_tool_call_content(content: &str) -> bool {
3938 let Ok(value) = serde_json::from_str::<serde_json::Value>(content) else {
3939 return false;
3940 };
3941 match value {
3942 serde_json::Value::Array(values) => {
3943 !values.is_empty()
3944 && values.iter().all(|value| {
3945 value
3946 .get("_ai_agents_native_tool_call")
3947 .and_then(|marker| marker.as_bool())
3948 == Some(true)
3949 })
3950 }
3951 serde_json::Value::Object(map) => {
3952 map.get("_ai_agents_native_tool_call")
3953 .and_then(|marker| marker.as_bool())
3954 == Some(true)
3955 }
3956 _ => false,
3957 }
3958 }
3959
3960 fn tool_result_message(
3961 tool_call: &ToolCall,
3962 output: &str,
3963 native_tool_call: bool,
3964 ) -> ChatMessage {
3965 if !native_tool_call {
3966 return ChatMessage::function(&tool_call.name, output);
3967 }
3968 let output = serde_json::from_str::<serde_json::Value>(output)
3969 .unwrap_or_else(|_| serde_json::Value::String(output.to_string()));
3970 ChatMessage::function(
3971 &tool_call.name,
3972 serde_json::json!({
3973 "_ai_agents_native_tool_result": true,
3974 "id": tool_call.id,
3975 "tool": tool_call.name,
3976 "output": output,
3977 })
3978 .to_string(),
3979 )
3980 }
3981
3982 fn parse_main_tool_calls(
3983 &self,
3984 content: &str,
3985 protocol: &MainToolProtocol,
3986 ) -> Option<Vec<ToolCall>> {
3987 if matches!(protocol.choice.as_ref(), Some(ToolChoice::None)) {
3988 None
3989 } else {
3990 self.parse_tool_calls(content)
3991 }
3992 }
3993
3994 fn parse_tool_calls(&self, content: &str) -> Option<Vec<ToolCall>> {
3995 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(content) {
3997 if let Some(arr) = parsed.as_array() {
3999 let calls: Vec<ToolCall> = arr
4000 .iter()
4001 .filter_map(|v| self.extract_tool_call_from_value(v))
4002 .collect();
4003 if !calls.is_empty() {
4004 return Some(calls);
4005 }
4006 }
4007 if let Some(tool_call) = self.extract_tool_call_from_value(&parsed) {
4009 return Some(vec![tool_call]);
4010 }
4011 }
4012
4013 if let Some(json_str) = self.extract_json_from_content(content)
4015 && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&json_str)
4016 {
4017 if let Some(arr) = parsed.as_array() {
4019 let calls: Vec<ToolCall> = arr
4020 .iter()
4021 .filter_map(|v| self.extract_tool_call_from_value(v))
4022 .collect();
4023 if !calls.is_empty() {
4024 return Some(calls);
4025 }
4026 }
4027 if let Some(tool_call) = self.extract_tool_call_from_value(&parsed) {
4029 return Some(vec![tool_call]);
4030 }
4031 }
4032
4033 None
4034 }
4035
4036 fn extract_tool_call_from_value(&self, parsed: &serde_json::Value) -> Option<ToolCall> {
4037 if let Some(tool_name) = parsed.get("tool").and_then(|v| v.as_str()) {
4038 let arguments = parsed
4039 .get("arguments")
4040 .cloned()
4041 .unwrap_or(serde_json::json!({}));
4042 return Some(ToolCall {
4043 id: parsed
4044 .get("id")
4045 .and_then(|value| value.as_str())
4046 .filter(|id| !id.is_empty())
4047 .map(str::to_string)
4048 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
4049 name: tool_name.to_string(),
4050 arguments,
4051 });
4052 }
4053 None
4054 }
4055
4056 fn extract_json_from_content(&self, content: &str) -> Option<String> {
4058 if let Some(result) = self.extract_json_array_from_content(content) {
4060 return Some(result);
4061 }
4062 self.extract_json_object_from_content(content)
4063 }
4064
4065 fn extract_json_array_from_content(&self, content: &str) -> Option<String> {
4067 let start = content.find('[')?;
4068 let content_from_start = &content[start..];
4069
4070 let mut depth = 0;
4071 let mut end = 0;
4072 for (i, ch) in content_from_start.char_indices() {
4073 match ch {
4074 '[' => depth += 1,
4075 ']' => {
4076 depth -= 1;
4077 if depth == 0 {
4078 end = i + 1;
4079 break;
4080 }
4081 }
4082 _ => {}
4083 }
4084 }
4085
4086 if end > 0 {
4087 let json_str = &content_from_start[..end];
4088 if json_str.contains("\"tool\"") {
4090 return Some(json_str.to_string());
4091 }
4092 }
4093
4094 None
4095 }
4096
4097 fn extract_json_object_from_content(&self, content: &str) -> Option<String> {
4099 let start = content.find('{')?;
4100 let content_from_start = &content[start..];
4101
4102 let mut depth = 0;
4104 let mut end = 0;
4105 for (i, ch) in content_from_start.char_indices() {
4106 match ch {
4107 '{' => depth += 1,
4108 '}' => {
4109 depth -= 1;
4110 if depth == 0 {
4111 end = i + 1;
4112 break;
4113 }
4114 }
4115 _ => {}
4116 }
4117 }
4118
4119 if end > 0 {
4120 let json_str = &content_from_start[..end];
4121 if json_str.contains("\"tool\"") {
4123 return Some(json_str.to_string());
4124 }
4125 }
4126
4127 None
4128 }
4129
4130 #[allow(clippy::too_many_arguments)]
4134 fn record_from_parts(
4135 &self,
4136 request: &ToolExecutionRequest,
4137 canonical_id: String,
4138 executed_arguments: Value,
4139 started_at: chrono::DateTime<chrono::Utc>,
4140 start: Instant,
4141 executed: bool,
4142 success: bool,
4143 output: String,
4144 metadata: HashMap<String, Value>,
4145 policy: ToolPolicyDecisionRecord,
4146 approval: Option<ToolApprovalRecord>,
4147 timed_out: bool,
4148 output_truncated: bool,
4149 ) -> ToolExecutionRecord {
4150 let versions = ToolDecisionVersions {
4151 policy: self.active_tool_security().policy_version(),
4152 registry: self.tools.version(),
4153 runtime_control: self.runtime_control.version.load(Ordering::SeqCst),
4154 };
4155 self.record_from_parts_at(
4156 request,
4157 canonical_id,
4158 executed_arguments,
4159 started_at,
4160 start,
4161 executed,
4162 success,
4163 output,
4164 metadata,
4165 policy,
4166 approval,
4167 timed_out,
4168 output_truncated,
4169 versions,
4170 )
4171 }
4172
4173 #[allow(clippy::too_many_arguments)]
4175 fn record_from_parts_at(
4176 &self,
4177 request: &ToolExecutionRequest,
4178 canonical_id: String,
4179 executed_arguments: Value,
4180 started_at: chrono::DateTime<chrono::Utc>,
4181 start: Instant,
4182 executed: bool,
4183 success: bool,
4184 output: String,
4185 metadata: HashMap<String, Value>,
4186 policy: ToolPolicyDecisionRecord,
4187 approval: Option<ToolApprovalRecord>,
4188 timed_out: bool,
4189 output_truncated: bool,
4190 versions: ToolDecisionVersions,
4191 ) -> ToolExecutionRecord {
4192 ToolExecutionRecord {
4193 call_id: request.call_id.clone(),
4194 requested_name: request.requested_name.clone(),
4195 canonical_id,
4196 source: request.source.clone(),
4197 arguments: request.arguments.clone(),
4198 executed_arguments,
4199 policy_version: versions.policy,
4200 registry_version: versions.registry,
4201 runtime_config_version: versions.runtime_control,
4202 executed,
4203 success,
4204 output,
4205 metadata,
4206 policy,
4207 approval,
4208 started_at,
4209 duration_ms: start.elapsed().as_millis() as u64,
4210 timed_out,
4211 cancelled: false,
4212 cancellation_reason: None,
4213 output_truncated,
4214 }
4215 }
4216
4217 async fn finish_tool_record(&self, record: &ToolExecutionRecord) {
4219 let result = ToolResult {
4220 success: record.success,
4221 output: record.model_output_string(),
4222 metadata: if record.metadata.is_empty() {
4223 None
4224 } else {
4225 Some(record.metadata.clone())
4226 },
4227 };
4228 self.hooks
4229 .on_tool_complete(&record.canonical_id, &result, record.duration_ms)
4230 .await;
4231 self.hooks.on_tool_execution_record(record).await;
4232 self.record_tool_call(&record.canonical_id, record.model_output_value());
4233 if !record.success {
4234 self.hooks
4235 .on_error(&AgentError::Tool(record.output.clone()))
4236 .await;
4237 }
4238 }
4239
4240 async fn finish_tool_record_after_resource_guards(
4242 &self,
4243 resource_guards: ToolResourceGuards,
4244 record: &ToolExecutionRecord,
4245 ) {
4246 drop(resource_guards);
4247 self.finish_tool_record(record).await;
4248 }
4249
4250 async fn execute_resolved_tool_once(
4252 &self,
4253 tool: Arc<dyn ai_agents_core::Tool>,
4254 args: Value,
4255 ctx: ToolExecutionContext,
4256 timeout_ms: u64,
4257 ) -> Result<(ToolResult, bool, bool, bool)> {
4258 if self.runtime_control.emergency_deny.load(Ordering::SeqCst) {
4259 return Ok((
4260 ToolResult::error("Tool execution cancelled by runtime control"),
4261 false,
4262 true,
4263 false,
4264 ));
4265 }
4266 let invoked = Arc::new(AtomicBool::new(false));
4270 let invoked_by_future = Arc::clone(&invoked);
4271 let actor_context = current_turn_actor_context();
4272 let future = async move {
4273 invoked_by_future.store(true, Ordering::SeqCst);
4274 if let Some(actor_context) = actor_context {
4275 scope_actor_context(actor_context, tool.execute(args, ctx)).await
4276 } else {
4277 tool.execute(args, ctx).await
4278 }
4279 };
4280 tokio::pin!(future);
4281 let timeout = tokio::time::sleep(std::time::Duration::from_millis(timeout_ms));
4282 tokio::pin!(timeout);
4283 let mut cancel_tick = tokio::time::interval(std::time::Duration::from_millis(50));
4284
4285 loop {
4286 tokio::select! {
4287 result = &mut future => return Ok((result, false, false, true)),
4288 _ = &mut timeout => {
4289 return Ok((
4290 ToolResult::error("Tool execution timed out"),
4291 true,
4292 false,
4293 invoked.load(Ordering::SeqCst),
4294 ));
4295 }
4296 _ = cancel_tick.tick() => {
4297 if self.runtime_control.emergency_deny.load(Ordering::SeqCst) {
4298 return Ok((
4299 ToolResult::error("Tool execution cancelled by runtime control"),
4300 false,
4301 true,
4302 invoked.load(Ordering::SeqCst),
4303 ));
4304 }
4305 }
4306 }
4307 }
4308 }
4309
4310 fn truncate_tool_output(output: String, max_chars: Option<usize>) -> (String, bool) {
4312 let Some(max_chars) = max_chars else {
4313 return (output, false);
4314 };
4315 let mut chars = output.chars();
4316 let truncated: String = chars.by_ref().take(max_chars).collect();
4317 if chars.next().is_some() {
4318 (truncated, true)
4319 } else {
4320 (output, false)
4321 }
4322 }
4323
4324 async fn acquire_tool_resource_locks(&self, keys: &[String]) -> Option<ToolResourceGuards> {
4326 let locks = {
4327 let mut table = self.resource_locks.write();
4328 table.retain(|_, lock| lock.strong_count() > 0);
4329 keys.iter()
4330 .map(|key| {
4331 if let Some(lock) = table.get(key).and_then(Weak::upgrade) {
4332 lock
4333 } else {
4334 let lock = Arc::new(tokio::sync::Mutex::new(()));
4335 table.insert(key.clone(), Arc::downgrade(&lock));
4336 lock
4337 }
4338 })
4339 .collect::<Vec<_>>()
4340 };
4341 let mut resource_guards = ToolResourceGuards {
4342 guards: Vec::with_capacity(locks.len()),
4343 locks: Arc::clone(&self.resource_locks),
4344 };
4345 let mut locks = locks.into_iter();
4346 while let Some(lock) = locks.next() {
4347 let mut lock = Box::pin(lock.lock_owned());
4348 loop {
4349 tokio::select! {
4350 guard = &mut lock => {
4351 resource_guards.guards.push(guard);
4352 break;
4353 }
4354 _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
4355 if self.runtime_control.emergency_deny.load(Ordering::SeqCst) {
4356 drop(lock);
4357 drop(locks);
4358 drop(resource_guards);
4359 return None;
4360 }
4361 }
4362 }
4363 }
4364 }
4365 Some(resource_guards)
4366 }
4367
4368 async fn run_tool_with_retries(
4370 &self,
4371 canonical_id: &str,
4372 tool: Arc<dyn ai_agents_core::Tool>,
4373 args: Value,
4374 ctx: ToolExecutionContext,
4375 timeout_ms: u64,
4376 max_retries: u32,
4377 ) -> Result<(ToolResult, bool, bool, bool)> {
4378 let max_retries = if ctx.classification.safely_retryable {
4379 max_retries
4380 } else {
4381 0
4382 };
4383 let mut attempts = 0;
4384 let mut invoked = false;
4385 loop {
4386 let (result, timed_out, cancelled, attempt_invoked) = self
4387 .execute_resolved_tool_once(tool.clone(), args.clone(), ctx.clone(), timeout_ms)
4388 .await?;
4389 invoked |= attempt_invoked;
4390 if result.success || timed_out || cancelled || attempts >= max_retries {
4391 return Ok((result, timed_out, cancelled, invoked));
4392 }
4393 attempts += 1;
4394 warn!(tool = %canonical_id, attempt = attempts, error = %result.output, "Retrying failed tool call");
4395 }
4396 }
4397
4398 fn execute_tool_record(
4400 &self,
4401 request: ToolExecutionRequest,
4402 ) -> Pin<Box<dyn Future<Output = Result<ToolExecutionRecord>> + Send + '_>> {
4403 Box::pin(self.execute_tool_record_inner(request))
4404 }
4405
4406 async fn execute_tool_record_inner(
4407 &self,
4408 request: ToolExecutionRequest,
4409 ) -> Result<ToolExecutionRecord> {
4410 let started_at = chrono::Utc::now();
4411 let start = Instant::now();
4412 info!(tool = %request.requested_name, args = %request.arguments, "Executing tool");
4413
4414 if self.runtime_control.emergency_deny.load(Ordering::SeqCst) {
4415 let record = self.record_from_parts(
4416 &request,
4417 request.requested_name.clone(),
4418 request.arguments.clone(),
4419 started_at,
4420 start,
4421 false,
4422 false,
4423 "Tool execution is disabled by runtime control".to_string(),
4424 HashMap::new(),
4425 ToolPolicyDecisionRecord::deny("runtime emergency deny is enabled"),
4426 None,
4427 false,
4428 false,
4429 );
4430 self.finish_tool_record(&record).await;
4431 return Ok(record);
4432 }
4433
4434 let Some(resolved) = self.tools.resolve(&request.requested_name) else {
4435 let record = self.record_from_parts(
4436 &request,
4437 request.requested_name.clone(),
4438 request.arguments.clone(),
4439 started_at,
4440 start,
4441 false,
4442 false,
4443 format!("Tool '{}' is unavailable", request.requested_name),
4444 HashMap::new(),
4445 ToolPolicyDecisionRecord::unavailable(format!(
4446 "Tool '{}' is not registered",
4447 request.requested_name
4448 )),
4449 None,
4450 false,
4451 false,
4452 );
4453 self.finish_tool_record(&record).await;
4454 return Ok(record);
4455 };
4456
4457 let canonical_id = resolved.identity.canonical_id.clone();
4458
4459 let available_tool_ids = self.get_available_tool_ids().await?;
4460 if !available_tool_ids.iter().any(|id| id == &canonical_id) {
4461 let record = self.record_from_parts(
4462 &request,
4463 canonical_id.clone(),
4464 request.arguments.clone(),
4465 started_at,
4466 start,
4467 false,
4468 false,
4469 format!(
4470 "Tool '{}' is not available in the current scope",
4471 canonical_id
4472 ),
4473 HashMap::new(),
4474 ToolPolicyDecisionRecord::deny(format!(
4475 "Tool '{}' is not granted by the current top-level and state tool scope",
4476 canonical_id
4477 )),
4478 None,
4479 false,
4480 false,
4481 );
4482 self.finish_tool_record(&record).await;
4483 return Ok(record);
4484 }
4485
4486 let approval_control_snapshot = self.runtime_safety_snapshot();
4487 let security_engine = approval_control_snapshot.tool_security.clone();
4488 let bindings = resolved.tool.policy_bindings();
4489 let mut executed_arguments = security_engine.prepare_tool_arguments_with_bindings(
4490 &canonical_id,
4491 &request.arguments,
4492 &bindings,
4493 );
4494 self.hooks
4495 .on_tool_start(&canonical_id, &executed_arguments)
4496 .await;
4497
4498 let mut metadata = HashMap::new();
4499 let safety = resolved.tool.safety_metadata();
4500 let classification = resolved.tool.classify_call(&executed_arguments);
4501 let limits = security_engine.effective_limits(&canonical_id, &safety, &classification);
4502 metadata.insert(
4503 "classification".to_string(),
4504 serde_json::to_value(&classification).unwrap_or(Value::Null),
4505 );
4506 metadata.insert(
4507 "effective_limits".to_string(),
4508 serde_json::to_value(&limits).unwrap_or(Value::Null),
4509 );
4510 let policy_snapshot = security_engine.policy_snapshot(&canonical_id);
4511 if !policy_snapshot.is_null() {
4512 metadata.insert("policy_snapshot".to_string(), policy_snapshot.clone());
4513 }
4514
4515 let mut approval_record = Some(ToolApprovalRecord {
4516 status: ToolApprovalStatus::NotRequired,
4517 reason: None,
4518 modified_arguments: None,
4519 });
4520
4521 let mut security_result = security_engine
4522 .validate_tool_execution_with_bindings(&canonical_id, &executed_arguments, &bindings)
4523 .await?;
4524 match &security_result {
4525 SecurityCheckResult::Allow => {}
4526 SecurityCheckResult::Warn { message } => {
4527 warn!(tool = %canonical_id, message = %message, "Tool security warning");
4528 }
4529 SecurityCheckResult::Block { reason } => {
4530 let record = self.record_from_parts(
4531 &request,
4532 canonical_id,
4533 executed_arguments,
4534 started_at,
4535 start,
4536 false,
4537 false,
4538 format!("Denied: {}", reason),
4539 metadata,
4540 ToolPolicyDecisionRecord::deny(reason.clone()),
4541 approval_record,
4542 false,
4543 false,
4544 );
4545 self.finish_tool_record(&record).await;
4546 return Ok(record);
4547 }
4548 SecurityCheckResult::Unavailable { reason } => {
4549 let record = self.record_from_parts(
4550 &request,
4551 canonical_id,
4552 executed_arguments,
4553 started_at,
4554 start,
4555 false,
4556 false,
4557 format!("Unavailable: {}", reason),
4558 metadata,
4559 ToolPolicyDecisionRecord::unavailable(reason.clone()),
4560 approval_record,
4561 false,
4562 false,
4563 );
4564 self.finish_tool_record(&record).await;
4565 return Ok(record);
4566 }
4567 SecurityCheckResult::RequireConfirmation { message } => {
4568 if self.hitl_engine.is_none() {
4569 approval_record = Some(ToolApprovalRecord {
4570 status: ToolApprovalStatus::Unavailable,
4571 reason: Some("No HITL engine configured".to_string()),
4572 modified_arguments: None,
4573 });
4574 let record = self.record_from_parts(
4575 &request,
4576 canonical_id,
4577 executed_arguments,
4578 started_at,
4579 start,
4580 false,
4581 false,
4582 format!("Approval unavailable: {}", message),
4583 metadata,
4584 ToolPolicyDecisionRecord::approval(message.clone()),
4585 approval_record,
4586 false,
4587 false,
4588 );
4589 self.finish_tool_record(&record).await;
4590 return Ok(record);
4591 }
4592
4593 let check_result = HITLCheckResult::required(
4594 ApprovalTrigger::tool(&canonical_id, executed_arguments.clone()),
4595 HashMap::new(),
4596 message.clone(),
4597 None,
4598 );
4599 match self.request_hitl_approval(check_result).await? {
4600 ApprovalResult::Approved => {
4601 merge_approved_record(&mut approval_record);
4602 }
4603 ApprovalResult::Modified { changes } => {
4604 if let Some(obj) = executed_arguments.as_object_mut() {
4605 for (key, value) in changes {
4606 obj.insert(key, value);
4607 }
4608 }
4609 security_result = security_engine
4610 .validate_tool_execution_with_bindings(
4611 &canonical_id,
4612 &executed_arguments,
4613 &bindings,
4614 )
4615 .await?;
4616 if !matches!(
4617 security_result,
4618 SecurityCheckResult::Allow
4619 | SecurityCheckResult::Warn { .. }
4620 | SecurityCheckResult::RequireConfirmation { .. }
4621 ) {
4622 let reason = security_result
4623 .reason()
4624 .unwrap_or("modified arguments failed policy")
4625 .to_string();
4626 let record = self.record_from_parts(
4627 &request,
4628 canonical_id,
4629 executed_arguments.clone(),
4630 started_at,
4631 start,
4632 false,
4633 false,
4634 reason.clone(),
4635 metadata,
4636 ToolPolicyDecisionRecord::deny(reason),
4637 Some(ToolApprovalRecord {
4638 status: ToolApprovalStatus::Modified,
4639 reason: None,
4640 modified_arguments: Some(executed_arguments),
4641 }),
4642 false,
4643 false,
4644 );
4645 self.finish_tool_record(&record).await;
4646 return Ok(record);
4647 }
4648 approval_record = Some(ToolApprovalRecord {
4649 status: ToolApprovalStatus::Modified,
4650 reason: None,
4651 modified_arguments: Some(executed_arguments.clone()),
4652 });
4653 }
4654 ApprovalResult::Rejected { reason } => {
4655 let reason = reason.unwrap_or_else(|| "rejected".to_string());
4656 approval_record = Some(ToolApprovalRecord {
4657 status: ToolApprovalStatus::Rejected,
4658 reason: Some(reason.clone()),
4659 modified_arguments: None,
4660 });
4661 let record = self.record_from_parts(
4662 &request,
4663 canonical_id,
4664 executed_arguments,
4665 started_at,
4666 start,
4667 false,
4668 false,
4669 format!("Approval rejected: {}", reason),
4670 metadata,
4671 ToolPolicyDecisionRecord::approval(reason),
4672 approval_record,
4673 false,
4674 false,
4675 );
4676 self.finish_tool_record(&record).await;
4677 return Ok(record);
4678 }
4679 ApprovalResult::Timeout => {
4680 approval_record = Some(ToolApprovalRecord {
4681 status: ToolApprovalStatus::Timeout,
4682 reason: Some("approval timeout".to_string()),
4683 modified_arguments: None,
4684 });
4685 let record = self.record_from_parts(
4686 &request,
4687 canonical_id,
4688 executed_arguments,
4689 started_at,
4690 start,
4691 false,
4692 false,
4693 "Approval timed out".to_string(),
4694 metadata,
4695 ToolPolicyDecisionRecord::approval("approval timeout"),
4696 approval_record,
4697 false,
4698 false,
4699 );
4700 self.finish_tool_record(&record).await;
4701 return Ok(record);
4702 }
4703 }
4704 }
4705 }
4706
4707 if canonical_id == "command" && !self.tools.command_runner_available() {
4708 let record = self.record_from_parts(
4709 &request,
4710 canonical_id.clone(),
4711 executed_arguments.clone(),
4712 started_at,
4713 start,
4714 false,
4715 false,
4716 "Command runner is unavailable".to_string(),
4717 metadata,
4718 ToolPolicyDecisionRecord::unavailable("command runner is unavailable"),
4719 Some(ToolApprovalRecord {
4720 status: ToolApprovalStatus::Unavailable,
4721 reason: Some("command runner is unavailable".to_string()),
4722 modified_arguments: None,
4723 }),
4724 false,
4725 false,
4726 );
4727 self.finish_tool_record(&record).await;
4728 return Ok(record);
4729 }
4730
4731 if approval_record
4732 .as_ref()
4733 .is_some_and(|record| matches!(record.status, ToolApprovalStatus::NotRequired))
4734 && let Some(message) =
4735 security_engine.classification_approval_message(&canonical_id, &classification)
4736 {
4737 if self.hitl_engine.is_none() {
4738 approval_record = Some(ToolApprovalRecord {
4739 status: ToolApprovalStatus::Unavailable,
4740 reason: Some("No HITL engine configured".to_string()),
4741 modified_arguments: None,
4742 });
4743 let record = self.record_from_parts(
4744 &request,
4745 canonical_id,
4746 executed_arguments,
4747 started_at,
4748 start,
4749 false,
4750 false,
4751 format!("Approval unavailable: {}", message),
4752 metadata,
4753 ToolPolicyDecisionRecord::approval(message),
4754 approval_record,
4755 false,
4756 false,
4757 );
4758 self.finish_tool_record(&record).await;
4759 return Ok(record);
4760 }
4761 let check_result = HITLCheckResult::required(
4762 ApprovalTrigger::tool(&canonical_id, executed_arguments.clone()),
4763 HashMap::new(),
4764 message.clone(),
4765 None,
4766 );
4767 match self.request_hitl_approval(check_result).await? {
4768 ApprovalResult::Approved => {
4769 merge_approved_record(&mut approval_record);
4770 }
4771 ApprovalResult::Modified { changes } => {
4772 if let Some(obj) = executed_arguments.as_object_mut() {
4773 for (key, value) in changes {
4774 obj.insert(key, value);
4775 }
4776 }
4777 let modified_security = security_engine
4778 .validate_tool_execution_with_bindings(
4779 &canonical_id,
4780 &executed_arguments,
4781 &bindings,
4782 )
4783 .await?;
4784 if !matches!(
4785 modified_security,
4786 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. }
4787 ) {
4788 let reason = modified_security
4789 .reason()
4790 .unwrap_or("modified arguments failed policy")
4791 .to_string();
4792 let record = self.record_from_parts(
4793 &request,
4794 canonical_id,
4795 executed_arguments.clone(),
4796 started_at,
4797 start,
4798 false,
4799 false,
4800 reason.clone(),
4801 metadata,
4802 ToolPolicyDecisionRecord::deny(reason),
4803 Some(ToolApprovalRecord {
4804 status: ToolApprovalStatus::Modified,
4805 reason: None,
4806 modified_arguments: Some(executed_arguments),
4807 }),
4808 false,
4809 false,
4810 );
4811 self.finish_tool_record(&record).await;
4812 return Ok(record);
4813 }
4814 approval_record = Some(ToolApprovalRecord {
4815 status: ToolApprovalStatus::Modified,
4816 reason: None,
4817 modified_arguments: Some(executed_arguments.clone()),
4818 });
4819 }
4820 ApprovalResult::Rejected { reason } => {
4821 let reason = reason.unwrap_or_else(|| "rejected".to_string());
4822 let record = self.record_from_parts(
4823 &request,
4824 canonical_id,
4825 executed_arguments,
4826 started_at,
4827 start,
4828 false,
4829 false,
4830 format!("Approval rejected: {}", reason),
4831 metadata,
4832 ToolPolicyDecisionRecord::approval(reason.clone()),
4833 Some(ToolApprovalRecord {
4834 status: ToolApprovalStatus::Rejected,
4835 reason: Some(reason),
4836 modified_arguments: None,
4837 }),
4838 false,
4839 false,
4840 );
4841 self.finish_tool_record(&record).await;
4842 return Ok(record);
4843 }
4844 ApprovalResult::Timeout => {
4845 let record = self.record_from_parts(
4846 &request,
4847 canonical_id,
4848 executed_arguments,
4849 started_at,
4850 start,
4851 false,
4852 false,
4853 "Approval timed out".to_string(),
4854 metadata,
4855 ToolPolicyDecisionRecord::approval("approval timeout"),
4856 Some(ToolApprovalRecord {
4857 status: ToolApprovalStatus::Timeout,
4858 reason: Some("approval timeout".to_string()),
4859 modified_arguments: None,
4860 }),
4861 false,
4862 false,
4863 );
4864 self.finish_tool_record(&record).await;
4865 return Ok(record);
4866 }
4867 }
4868 }
4869
4870 if canonical_id == "diagnostics" && !self.tools.diagnostics_available() {
4871 let record = self.record_from_parts(
4872 &request,
4873 canonical_id.clone(),
4874 executed_arguments.clone(),
4875 started_at,
4876 start,
4877 false,
4878 false,
4879 "Diagnostics provider is unavailable".to_string(),
4880 metadata,
4881 ToolPolicyDecisionRecord::unavailable("diagnostics provider is unavailable"),
4882 Some(ToolApprovalRecord {
4883 status: ToolApprovalStatus::Unavailable,
4884 reason: Some("diagnostics provider is unavailable".to_string()),
4885 modified_arguments: None,
4886 }),
4887 false,
4888 false,
4889 );
4890 self.finish_tool_record(&record).await;
4891 return Ok(record);
4892 }
4893
4894 if canonical_id == "web_search" && !self.tools.web_search_available() {
4895 let record = self.record_from_parts(
4896 &request,
4897 canonical_id.clone(),
4898 executed_arguments.clone(),
4899 started_at,
4900 start,
4901 false,
4902 false,
4903 "Web search provider is unavailable".to_string(),
4904 metadata,
4905 ToolPolicyDecisionRecord::unavailable("web search provider is unavailable"),
4906 Some(ToolApprovalRecord {
4907 status: ToolApprovalStatus::Unavailable,
4908 reason: Some("web search provider is unavailable".to_string()),
4909 modified_arguments: None,
4910 }),
4911 false,
4912 false,
4913 );
4914 self.finish_tool_record(&record).await;
4915 return Ok(record);
4916 }
4917
4918 let hitl_lang_ctx = self.build_hitl_language_context();
4919 if let Some(ref hitl_engine) = self.hitl_engine {
4920 let check_result = self
4921 .observe_purpose(
4922 ObservationPurpose::HitlLocalization,
4923 hitl_engine.check_tool_with_localization(
4924 &canonical_id,
4925 &executed_arguments,
4926 &hitl_lang_ctx,
4927 self.approval_handler.as_ref(),
4928 Some(&self.llm_registry),
4929 ),
4930 )
4931 .await?;
4932 if check_result.is_required() {
4933 match self.request_hitl_approval(check_result).await? {
4934 ApprovalResult::Approved => {
4935 merge_approved_record(&mut approval_record);
4936 }
4937 ApprovalResult::Modified { changes } => {
4938 if let Some(obj) = executed_arguments.as_object_mut() {
4939 for (key, value) in changes {
4940 obj.insert(key, value);
4941 }
4942 }
4943 let modified_security = security_engine
4944 .validate_tool_execution_with_bindings(
4945 &canonical_id,
4946 &executed_arguments,
4947 &bindings,
4948 )
4949 .await?;
4950 if !matches!(
4951 modified_security,
4952 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. }
4953 ) {
4954 let reason = modified_security
4955 .reason()
4956 .unwrap_or("modified arguments failed policy")
4957 .to_string();
4958 let record = self.record_from_parts(
4959 &request,
4960 canonical_id,
4961 executed_arguments.clone(),
4962 started_at,
4963 start,
4964 false,
4965 false,
4966 reason.clone(),
4967 metadata,
4968 ToolPolicyDecisionRecord::deny(reason),
4969 Some(ToolApprovalRecord {
4970 status: ToolApprovalStatus::Modified,
4971 reason: None,
4972 modified_arguments: Some(executed_arguments),
4973 }),
4974 false,
4975 false,
4976 );
4977 self.finish_tool_record(&record).await;
4978 return Ok(record);
4979 }
4980 approval_record = Some(ToolApprovalRecord {
4981 status: ToolApprovalStatus::Modified,
4982 reason: None,
4983 modified_arguments: Some(executed_arguments.clone()),
4984 });
4985 }
4986 ApprovalResult::Rejected { reason } => {
4987 let reason = reason.unwrap_or_else(|| "rejected".to_string());
4988 let record = self.record_from_parts(
4989 &request,
4990 canonical_id,
4991 executed_arguments,
4992 started_at,
4993 start,
4994 false,
4995 false,
4996 format!("Approval rejected: {}", reason),
4997 metadata,
4998 ToolPolicyDecisionRecord::approval(reason.clone()),
4999 Some(ToolApprovalRecord {
5000 status: ToolApprovalStatus::Rejected,
5001 reason: Some(reason),
5002 modified_arguments: None,
5003 }),
5004 false,
5005 false,
5006 );
5007 self.finish_tool_record(&record).await;
5008 return Ok(record);
5009 }
5010 ApprovalResult::Timeout => {
5011 let record = self.record_from_parts(
5012 &request,
5013 canonical_id,
5014 executed_arguments,
5015 started_at,
5016 start,
5017 false,
5018 false,
5019 "Approval timed out".to_string(),
5020 metadata,
5021 ToolPolicyDecisionRecord::approval("approval timeout"),
5022 Some(ToolApprovalRecord {
5023 status: ToolApprovalStatus::Timeout,
5024 reason: Some("approval timeout".to_string()),
5025 modified_arguments: None,
5026 }),
5027 false,
5028 false,
5029 );
5030 self.finish_tool_record(&record).await;
5031 return Ok(record);
5032 }
5033 }
5034 }
5035
5036 let condition_check = self
5037 .observe_purpose(
5038 ObservationPurpose::HitlLocalization,
5039 hitl_engine.check_conditions_with_localization(
5040 &executed_arguments,
5041 &hitl_lang_ctx,
5042 self.approval_handler.as_ref(),
5043 Some(&self.llm_registry),
5044 ),
5045 )
5046 .await?;
5047 if condition_check.is_required() {
5048 match self.request_hitl_approval(condition_check).await? {
5049 ApprovalResult::Approved => {
5050 merge_approved_record(&mut approval_record);
5051 }
5052 ApprovalResult::Modified { changes } => {
5053 if let Some(obj) = executed_arguments.as_object_mut() {
5054 for (key, value) in changes {
5055 obj.insert(key, value);
5056 }
5057 }
5058 let modified_security = security_engine
5059 .validate_tool_execution_with_bindings(
5060 &canonical_id,
5061 &executed_arguments,
5062 &bindings,
5063 )
5064 .await?;
5065 if !matches!(
5066 modified_security,
5067 SecurityCheckResult::Allow | SecurityCheckResult::Warn { .. }
5068 ) {
5069 let reason = modified_security
5070 .reason()
5071 .unwrap_or("modified arguments failed policy")
5072 .to_string();
5073 let record = self.record_from_parts(
5074 &request,
5075 canonical_id,
5076 executed_arguments,
5077 started_at,
5078 start,
5079 false,
5080 false,
5081 reason.clone(),
5082 metadata,
5083 ToolPolicyDecisionRecord::deny(reason),
5084 approval_record,
5085 false,
5086 false,
5087 );
5088 self.finish_tool_record(&record).await;
5089 return Ok(record);
5090 }
5091 approval_record = Some(ToolApprovalRecord {
5092 status: ToolApprovalStatus::Modified,
5093 reason: None,
5094 modified_arguments: Some(executed_arguments.clone()),
5095 });
5096 }
5097 ApprovalResult::Rejected { reason } => {
5098 let reason = reason.unwrap_or_else(|| "rejected".to_string());
5099 let record = self.record_from_parts(
5100 &request,
5101 canonical_id,
5102 executed_arguments,
5103 started_at,
5104 start,
5105 false,
5106 false,
5107 format!("Approval rejected: {}", reason),
5108 metadata,
5109 ToolPolicyDecisionRecord::approval(reason.clone()),
5110 Some(ToolApprovalRecord {
5111 status: ToolApprovalStatus::Rejected,
5112 reason: Some(reason),
5113 modified_arguments: None,
5114 }),
5115 false,
5116 false,
5117 );
5118 self.finish_tool_record(&record).await;
5119 return Ok(record);
5120 }
5121 ApprovalResult::Timeout => {
5122 let record = self.record_from_parts(
5123 &request,
5124 canonical_id,
5125 executed_arguments,
5126 started_at,
5127 start,
5128 false,
5129 false,
5130 "Approval timed out".to_string(),
5131 metadata,
5132 ToolPolicyDecisionRecord::approval("approval timeout"),
5133 Some(ToolApprovalRecord {
5134 status: ToolApprovalStatus::Timeout,
5135 reason: Some("approval timeout".to_string()),
5136 modified_arguments: None,
5137 }),
5138 false,
5139 false,
5140 );
5141 self.finish_tool_record(&record).await;
5142 return Ok(record);
5143 }
5144 }
5145 }
5146 }
5147
5148 executed_arguments = security_engine.prepare_tool_arguments_with_bindings(
5153 &canonical_id,
5154 &executed_arguments,
5155 &bindings,
5156 );
5157 if let Some(record) = approval_record.as_mut()
5158 && matches!(record.status, ToolApprovalStatus::Modified)
5159 {
5160 record.modified_arguments = Some(executed_arguments.clone());
5161 }
5162 let binding_security_result = security_engine
5163 .validate_tool_execution_with_bindings(&canonical_id, &executed_arguments, &bindings)
5164 .await?;
5165 let approval_confirmation_required = matches!(
5166 binding_security_result,
5167 SecurityCheckResult::RequireConfirmation { .. }
5168 ) || security_engine
5169 .classification_approval_message(
5170 &canonical_id,
5171 &resolved.tool.classify_call(&executed_arguments),
5172 )
5173 .is_some();
5174 let approval_binding = approval_record.as_ref().and_then(|record| {
5175 matches!(
5176 record.status,
5177 ToolApprovalStatus::Approved | ToolApprovalStatus::Modified
5178 )
5179 .then(|| ToolApprovalBinding {
5180 canonical_id: canonical_id.clone(),
5181 arguments: executed_arguments.clone(),
5182 confirmation_required: approval_confirmation_required,
5183 policy_version: security_engine.policy_version(),
5184 runtime_control_version: approval_control_snapshot.version,
5185 reviewed_tool: Arc::clone(&resolved.tool),
5186 })
5187 });
5188
5189 let control_snapshot = self.runtime_safety_snapshot();
5194 let resolved = self.tools.resolve(&request.requested_name);
5195 let registry_version = self.tools.version();
5196 let versions = ToolDecisionVersions {
5197 policy: control_snapshot.tool_security.policy_version(),
5198 registry: registry_version,
5199 runtime_control: control_snapshot.version,
5200 };
5201 metadata.insert(
5202 "runtime_scope_snapshot".to_string(),
5203 serde_json::to_value(&control_snapshot.tool_scope_override).unwrap_or(Value::Null),
5204 );
5205 let resolved = match resolved {
5206 Some(resolved) => resolved,
5207 None => {
5208 let reason = format!(
5209 "Tool '{}' became unavailable after approval",
5210 request.requested_name
5211 );
5212 let record = self.record_from_parts_at(
5213 &request,
5214 request.requested_name.clone(),
5215 executed_arguments,
5216 started_at,
5217 start,
5218 false,
5219 false,
5220 reason.clone(),
5221 metadata,
5222 ToolPolicyDecisionRecord::unavailable(reason),
5223 approval_record,
5224 false,
5225 false,
5226 versions,
5227 );
5228 self.finish_tool_record(&record).await;
5229 return Ok(record);
5230 }
5231 };
5232
5233 let canonical_id = resolved.identity.canonical_id.clone();
5234 let bindings = resolved.tool.policy_bindings();
5235 let final_arguments = control_snapshot
5236 .tool_security
5237 .prepare_tool_arguments_with_bindings(&canonical_id, &executed_arguments, &bindings);
5238 if let Some(record) = approval_record.as_mut()
5239 && matches!(record.status, ToolApprovalStatus::Modified)
5240 {
5241 record.modified_arguments = Some(final_arguments.clone());
5242 }
5243 let classification = resolved.tool.classify_call(&final_arguments);
5244 let safety = resolved.tool.safety_metadata();
5245 let security_engine = control_snapshot.tool_security;
5246 let limits = security_engine.effective_limits(&canonical_id, &safety, &classification);
5247 let policy_snapshot = security_engine.policy_snapshot(&canonical_id);
5248 let resource_lock_keys =
5249 tool_resource_lock_keys(&canonical_id, &final_arguments, &bindings, &classification);
5250 metadata.insert(
5251 "classification".to_string(),
5252 serde_json::to_value(&classification).unwrap_or(Value::Null),
5253 );
5254 metadata.insert(
5255 "effective_limits".to_string(),
5256 serde_json::to_value(&limits).unwrap_or(Value::Null),
5257 );
5258 metadata.insert(
5259 "resource_lock_keys".to_string(),
5260 serde_json::to_value(&resource_lock_keys).unwrap_or(Value::Null),
5261 );
5262 if policy_snapshot.is_null() {
5263 metadata.remove("policy_snapshot");
5264 } else {
5265 metadata.insert("policy_snapshot".to_string(), policy_snapshot.clone());
5266 }
5267
5268 let final_denial = |canonical_id: String,
5269 output: String,
5270 policy: ToolPolicyDecisionRecord,
5271 metadata: HashMap<String, Value>| {
5272 self.record_from_parts_at(
5273 &request,
5274 canonical_id,
5275 final_arguments.clone(),
5276 started_at,
5277 start,
5278 false,
5279 false,
5280 output,
5281 metadata,
5282 policy,
5283 approval_record.clone(),
5284 false,
5285 false,
5286 versions,
5287 )
5288 };
5289
5290 if control_snapshot.emergency_deny {
5291 let reason = "Tool execution is disabled by runtime control".to_string();
5292 let record = final_denial(
5293 canonical_id,
5294 reason.clone(),
5295 ToolPolicyDecisionRecord::deny(reason),
5296 metadata,
5297 );
5298 self.finish_tool_record(&record).await;
5299 return Ok(record);
5300 }
5301
5302 let available_tool_ids = self
5303 .get_available_tool_ids_for_scope(control_snapshot.tool_scope_override.as_deref())
5304 .await?;
5305 metadata.insert(
5306 "available_tool_ids_snapshot".to_string(),
5307 serde_json::to_value(&available_tool_ids).unwrap_or(Value::Null),
5308 );
5309 if !available_tool_ids
5310 .iter()
5311 .any(|tool_id| tool_id == &canonical_id)
5312 {
5313 let reason = format!(
5314 "Tool '{}' is not available in the final runtime scope",
5315 canonical_id
5316 );
5317 let record = final_denial(
5318 canonical_id,
5319 reason.clone(),
5320 ToolPolicyDecisionRecord::deny(reason),
5321 metadata,
5322 );
5323 self.finish_tool_record(&record).await;
5324 return Ok(record);
5325 }
5326
5327 let final_security_result = security_engine
5332 .validate_tool_execution_with_bindings(&canonical_id, &final_arguments, &bindings)
5333 .await?;
5334 match &final_security_result {
5335 SecurityCheckResult::Block { reason } => {
5336 let record = final_denial(
5337 canonical_id,
5338 format!("Denied: {}", reason),
5339 ToolPolicyDecisionRecord::deny(reason.clone()),
5340 metadata,
5341 );
5342 self.finish_tool_record(&record).await;
5343 return Ok(record);
5344 }
5345 SecurityCheckResult::Unavailable { reason } => {
5346 let record = final_denial(
5347 canonical_id,
5348 format!("Unavailable: {}", reason),
5349 ToolPolicyDecisionRecord::unavailable(reason.clone()),
5350 metadata,
5351 );
5352 self.finish_tool_record(&record).await;
5353 return Ok(record);
5354 }
5355 SecurityCheckResult::Warn { message } => {
5356 warn!(tool = %canonical_id, message = %message, "Tool security warning after approval");
5357 }
5358 SecurityCheckResult::Allow | SecurityCheckResult::RequireConfirmation { .. } => {}
5359 }
5360 let final_confirmation_required = matches!(
5361 final_security_result,
5362 SecurityCheckResult::RequireConfirmation { .. }
5363 ) || security_engine
5364 .classification_approval_message(&canonical_id, &classification)
5365 .is_some();
5366 let stale_approval = approval_binding.as_ref().is_some_and(|binding| {
5367 binding.is_stale(
5368 &canonical_id,
5369 &final_arguments,
5370 final_confirmation_required,
5371 versions,
5372 &resolved.tool,
5373 )
5374 });
5375 if stale_approval {
5376 let reason = "Approval became stale before final admission".to_string();
5377 let record = final_denial(
5378 canonical_id,
5379 reason.clone(),
5380 ToolPolicyDecisionRecord::deny(reason),
5381 metadata,
5382 );
5383 self.finish_tool_record(&record).await;
5384 return Ok(record);
5385 }
5386 if final_confirmation_required && approval_binding.is_none() {
5387 let reason = "Final policy requires fresh approval".to_string();
5388 let record = final_denial(
5389 canonical_id,
5390 reason.clone(),
5391 ToolPolicyDecisionRecord::approval(reason),
5392 metadata,
5393 );
5394 self.finish_tool_record(&record).await;
5395 return Ok(record);
5396 }
5397
5398 let unavailable_reason = match canonical_id.as_str() {
5399 "command" if !self.tools.command_runner_available() => {
5400 Some("command runner is unavailable")
5401 }
5402 "diagnostics" if !self.tools.diagnostics_available() => {
5403 Some("diagnostics provider is unavailable")
5404 }
5405 "web_search" if !self.tools.web_search_available() => {
5406 Some("web search provider is unavailable")
5407 }
5408 _ => None,
5409 };
5410 if let Some(reason) = unavailable_reason {
5411 let record = final_denial(
5412 canonical_id,
5413 reason.to_string(),
5414 ToolPolicyDecisionRecord::unavailable(reason),
5415 metadata,
5416 );
5417 self.finish_tool_record(&record).await;
5418 return Ok(record);
5419 }
5420
5421 let Some(resource_guards) = self.acquire_tool_resource_locks(&resource_lock_keys).await
5426 else {
5427 let reason = "Tool execution cancelled while waiting for resource locks".to_string();
5428 let record = final_denial(
5429 canonical_id,
5430 reason.clone(),
5431 ToolPolicyDecisionRecord::deny(reason),
5432 metadata,
5433 );
5434 self.finish_tool_record(&record).await;
5435 return Ok(record);
5436 };
5437
5438 let admission =
5440 self.admit_tool_execution(versions.runtime_control, versions.policy, &canonical_id);
5441 if !matches!(admission, SecurityCheckResult::Allow) {
5442 let latest_control = self.runtime_safety_snapshot();
5443 let reason = admission
5444 .reason()
5445 .unwrap_or("tool admission was denied")
5446 .to_string();
5447 let policy = if admission.is_unavailable() {
5448 ToolPolicyDecisionRecord::unavailable(reason.clone())
5449 } else {
5450 ToolPolicyDecisionRecord::deny(reason.clone())
5451 };
5452 let record = self.record_from_parts_at(
5453 &request,
5454 canonical_id,
5455 final_arguments,
5456 started_at,
5457 start,
5458 false,
5459 false,
5460 reason,
5461 metadata,
5462 policy,
5463 approval_record,
5464 false,
5465 false,
5466 ToolDecisionVersions {
5467 policy: latest_control.tool_security.policy_version(),
5468 registry: versions.registry,
5469 runtime_control: latest_control.version,
5470 },
5471 );
5472 self.finish_tool_record_after_resource_guards(resource_guards, &record)
5473 .await;
5474 return Ok(record);
5475 }
5476 let executed_arguments = final_arguments;
5477
5478 let tool_config = self.recovery_manager.get_tool_config(&canonical_id);
5479 let timeout_ms = limits
5480 .timeout_ms
5481 .unwrap_or_else(|| security_engine.get_tool_timeout(&canonical_id));
5482 let deadline = Some(started_at + chrono::Duration::milliseconds(timeout_ms as i64));
5483 let turn_actor = current_turn_actor_context();
5484 let actor = ToolActorContext {
5485 actor_id: turn_actor
5486 .as_ref()
5487 .and_then(|context| context.effective_actor_id().map(str::to_string))
5488 .or_else(|| self.actor_id()),
5489 origin_actor_id: turn_actor
5490 .as_ref()
5491 .and_then(|context| context.origin_actor_id.clone()),
5492 sender_agent_id: turn_actor
5493 .as_ref()
5494 .and_then(|context| context.sender_agent_id.clone()),
5495 };
5496 let tool_context = ToolExecutionContext {
5497 requested_name: request.requested_name.clone(),
5498 canonical_id: canonical_id.clone(),
5499 display_name: resolved.identity.display_name.clone(),
5500 provider_id: resolved.identity.provider_id.clone(),
5501 registry_version: versions.registry,
5502 policy_version: versions.policy,
5503 runtime_control_version: versions.runtime_control,
5504 call_id: request.call_id.clone(),
5505 source: request.source.clone(),
5506 actor,
5507 cancellation: ToolCancellationToken::new(
5508 Arc::clone(&self.runtime_control.emergency_deny),
5509 Some("runtime control cancellation".to_string()),
5510 ),
5511 started_at,
5512 deadline,
5513 permission: ToolPolicyDecisionRecord::allow(),
5514 approval: approval_record.clone(),
5515 classification: classification.clone(),
5516 safety,
5517 limits: limits.clone(),
5518 policy_snapshot,
5519 custom_config: security_engine.custom_config(&canonical_id),
5520 };
5521 let (mut result, timed_out, cancelled, invoked) = self
5522 .run_tool_with_retries(
5523 &canonical_id,
5524 resolved.tool.clone(),
5525 executed_arguments.clone(),
5526 tool_context,
5527 timeout_ms,
5528 tool_config.max_retries,
5529 )
5530 .await?;
5531
5532 if !result.success {
5533 match &tool_config.on_failure {
5534 ToolFailureAction::Skip => {
5535 result = ToolResult::ok(format!(
5536 "{{\"skipped\": true, \"reason\": \"Tool '{}' was skipped after failure\"}}",
5537 canonical_id
5538 ));
5539 }
5540 ToolFailureAction::Fallback { fallback_tool } => {
5541 drop(resource_guards);
5542 let fallback_request = ToolExecutionRequest::new(
5543 request.call_id.clone(),
5544 fallback_tool.clone(),
5545 executed_arguments,
5546 ToolCallSource::Fallback {
5547 original_tool: canonical_id,
5548 },
5549 );
5550 return Box::pin(self.execute_tool_record(fallback_request)).await;
5551 }
5552 ToolFailureAction::ReportError => {}
5553 }
5554 }
5555
5556 let output_cap = limits.max_output_chars;
5557 let (output, output_truncated) =
5558 Self::truncate_tool_output(result.output.clone(), output_cap);
5559 if let Some(result_metadata) = result.metadata {
5560 metadata.extend(result_metadata);
5561 }
5562 let mut record = self.record_from_parts_at(
5563 &request,
5564 canonical_id,
5565 executed_arguments,
5566 started_at,
5567 start,
5568 invoked,
5569 result.success,
5570 output,
5571 metadata,
5572 ToolPolicyDecisionRecord::allow(),
5573 approval_record,
5574 timed_out,
5575 output_truncated,
5576 versions,
5577 );
5578 record.cancelled = cancelled;
5579 if cancelled {
5580 record.cancellation_reason = Some("runtime control cancellation".to_string());
5581 }
5582 self.finish_tool_record_after_resource_guards(resource_guards, &record)
5583 .await;
5584 Ok(record)
5585 }
5586
5587 #[instrument(skip(self, tool_call), fields(tool = %tool_call.name))]
5588 async fn execute_tool_smart(&self, tool_call: &ToolCall) -> Result<String> {
5589 let record = self
5590 .execute_tool_record(ToolExecutionRequest::new(
5591 tool_call.id.clone(),
5592 tool_call.name.clone(),
5593 tool_call.arguments.clone(),
5594 ToolCallSource::Model,
5595 ))
5596 .await?;
5597 if record.success {
5598 Ok(record.model_output_string())
5599 } else if matches!(record.policy.outcome, PermissionOutcome::RequiresApproval) {
5600 Err(AgentError::HITLRejected(record.model_output_string()))
5601 } else {
5602 Err(AgentError::Tool(record.model_output_string()))
5603 }
5604 }
5605
5606 async fn select_skill_candidate(&self, input: &str) -> Result<Option<SkillCandidate>> {
5612 let Some(ref router) = self.skill_router else {
5613 return Ok(None);
5614 };
5615 let available_skills = self.get_available_skills();
5616 if available_skills.is_empty() {
5617 return Ok(None);
5618 }
5619 let skill_ids: Vec<&str> = available_skills.iter().map(|s| s.id.as_str()).collect();
5620 let Some(skill_id) = self
5621 .observe_purpose(
5622 ObservationPurpose::SkillRouting,
5623 router.select_skill_filtered(input, &skill_ids),
5624 )
5625 .await?
5626 else {
5627 return Ok(None);
5628 };
5629 let skill = router
5630 .get_skill(&skill_id)
5631 .cloned()
5632 .ok_or_else(|| AgentError::Skill(format!("Skill not found: {}", skill_id)))?;
5633 info!(skill_id = %skill_id, "Skill selected");
5634 Ok(Some(SkillCandidate::new(skill_id, skill)))
5635 }
5636
5637 async fn commit_skill_candidate_route_result(
5642 &self,
5643 candidate: SkillCandidate,
5644 input: &str,
5645 ) -> Result<SkillRouteResult> {
5646 let skill_id = candidate.skill_id;
5647 let skill = candidate.skill;
5648 if let Some(ref skill_disambig) = skill.disambiguation
5649 && skill_disambig.enabled.unwrap_or(false)
5650 && let Some(ref disambiguator) = self.disambiguation_manager
5651 {
5652 let context = self.build_disambiguation_context().await?;
5653 let state_override = self
5654 .state_machine
5655 .as_ref()
5656 .and_then(|sm| sm.current_definition())
5657 .and_then(|def| def.disambiguation.clone());
5658
5659 match self
5660 .observe_purpose(
5661 ObservationPurpose::DisambiguationDetection,
5662 disambiguator.process_input_with_override(
5663 input,
5664 &context,
5665 state_override.as_ref(),
5666 Some(skill_disambig),
5667 ),
5668 )
5669 .await?
5670 {
5671 DisambiguationResult::Clear => {
5672 debug!(skill_id = %skill_id, "Skill disambiguation: clear");
5673 }
5674 DisambiguationResult::NeedsClarification {
5675 question,
5676 detection,
5677 } => {
5678 info!(
5679 skill_id = %skill_id,
5680 ambiguity_type = ?detection.ambiguity_type,
5681 confidence = detection.confidence,
5682 "Skill requires clarification before execution"
5683 );
5684 *self.pending_skill_id.write() = Some(skill_id.clone());
5685 return Ok(SkillRouteResult::NeedsClarification(
5686 AgentResponse::new(&question.question).with_metadata(
5687 "disambiguation",
5688 serde_json::json!({
5689 "status": "awaiting_clarification",
5690 "skill_id": skill_id,
5691 "options": question.options,
5692 "clarifying": question.clarifying,
5693 "detection": {
5694 "type": detection.ambiguity_type,
5695 "confidence": detection.confidence,
5696 "what_is_unclear": detection.what_is_unclear,
5697 }
5698 }),
5699 ),
5700 ));
5701 }
5702 DisambiguationResult::Clarified { enriched_input, .. } => {
5703 info!(skill_id = %skill_id, enriched = %enriched_input, "Skill disambiguation clarified");
5704 return Ok(SkillRouteResult::Response {
5705 skill_id,
5706 content: self.execute_skill(&skill, &enriched_input).await?,
5707 });
5708 }
5709 DisambiguationResult::ProceedWithBestGuess { enriched_input } => {
5710 info!(skill_id = %skill_id, "Skill disambiguation best guess");
5711 return Ok(SkillRouteResult::Response {
5712 skill_id,
5713 content: self.execute_skill(&skill, &enriched_input).await?,
5714 });
5715 }
5716 DisambiguationResult::GiveUp { reason } => {
5717 warn!(skill_id = %skill_id, reason = %reason, "Skill disambiguation gave up");
5718 let apology = self
5719 .generate_localized_apology(
5720 "Generate a brief, polite apology saying you couldn't understand the request. Be concise.",
5721 &reason,
5722 )
5723 .await
5724 .unwrap_or_else(|_| {
5725 format!("I'm sorry, I couldn't understand your request: {}", reason)
5726 });
5727 return Ok(SkillRouteResult::NeedsClarification(AgentResponse::new(
5728 &apology,
5729 )));
5730 }
5731 DisambiguationResult::Escalate { reason } => {
5732 info!(skill_id = %skill_id, reason = %reason, "Skill disambiguation escalating");
5733 let apology = self
5734 .generate_localized_apology(
5735 "Explain briefly that you're transferring the user to a human agent for help.",
5736 &reason,
5737 )
5738 .await
5739 .unwrap_or_else(|_| {
5740 format!("I need human assistance to help with your request: {}", reason)
5741 });
5742 return Ok(SkillRouteResult::NeedsClarification(AgentResponse::new(
5743 &apology,
5744 )));
5745 }
5746 DisambiguationResult::Abandoned { .. } => {
5747 debug!(skill_id = %skill_id, "Skill disambiguation abandoned");
5748 return Ok(SkillRouteResult::NoMatch);
5749 }
5750 }
5751 }
5752 Ok(SkillRouteResult::Response {
5753 skill_id,
5754 content: self.execute_skill(&skill, input).await?,
5755 })
5756 }
5757
5758 async fn try_skill_route(&self, input: &str) -> Result<SkillRouteResult> {
5760 if let Some(candidate) = self.select_skill_candidate(input).await? {
5761 self.commit_skill_candidate_route_result(candidate, input)
5762 .await
5763 } else {
5764 Ok(SkillRouteResult::NoMatch)
5765 }
5766 }
5767
5768 async fn execute_skill(&self, skill: &SkillDefinition, input: &str) -> Result<String> {
5770 if let Some(ref executor) = self.skill_executor {
5771 let skill_reasoning = self.get_skill_reasoning_config(skill);
5772 let skill_reflection = self.get_skill_reflection_config(skill);
5773
5774 debug!(
5775 skill_id = %skill.id,
5776 reasoning_mode = ?skill_reasoning.mode,
5777 reflection_enabled = ?skill_reflection.enabled,
5778 "Skill reasoning/reflection config"
5779 );
5780
5781 let response = self
5782 .observe_purpose(
5783 ObservationPurpose::SkillPrompt,
5784 executor.execute_with_invoker(skill, input, serde_json::json!({}), self),
5785 )
5786 .await?;
5787
5788 if skill_reflection.requires_evaluation() && skill_reflection.is_enabled() {
5789 let should_reflect = self
5790 .should_reflect_with_config(input, &response, &skill_reflection)
5791 .await?;
5792 if should_reflect {
5793 let evaluated = self
5794 .evaluate_and_retry_with_config(input, response, &skill_reflection)
5795 .await?;
5796 return Ok(evaluated);
5797 }
5798 }
5799
5800 return Ok(response);
5801 }
5802 Err(AgentError::Skill(
5803 "No skill executor configured".to_string(),
5804 ))
5805 }
5806
5807 async fn execute_skill_by_id(&self, skill_id: &str, input: &str) -> Result<String> {
5810 let skill = self
5811 .skill_router
5812 .as_ref()
5813 .and_then(|r| r.get_skill(skill_id).cloned())
5814 .ok_or_else(|| AgentError::Skill(format!("Skill not found: {}", skill_id)))?;
5815 self.execute_skill(&skill, input).await
5816 }
5817
5818 async fn should_reflect_with_config(
5819 &self,
5820 input: &str,
5821 response: &str,
5822 config: &ReflectionConfig,
5823 ) -> Result<bool> {
5824 if !config.requires_evaluation() {
5825 return Ok(false);
5826 }
5827
5828 if config.is_enabled() {
5829 return Ok(true);
5830 }
5831
5832 let evaluator_llm = config
5833 .evaluator_llm
5834 .as_ref()
5835 .and_then(|alias| self.llm_registry.get(alias).ok())
5836 .or_else(|| self.llm_registry.router().ok())
5837 .or_else(|| self.llm_registry.default().ok());
5838
5839 let Some(llm) = evaluator_llm else {
5840 return Ok(false);
5841 };
5842
5843 let response_preview: String = response.chars().take(500).collect();
5844 let prompt = format!(
5845 r#"Should this response be evaluated for quality? Consider if it's a complex or important response.
5846
5847User query: "{}"
5848Response: "{}"
5849
5850Answer YES or NO only."#,
5851 input, response_preview
5852 );
5853
5854 let messages = vec![ChatMessage::user(&prompt)];
5855 let result = self
5856 .observe_purpose(
5857 ObservationPurpose::ReflectionDecision,
5858 llm.complete(&messages, None),
5859 )
5860 .await;
5861
5862 match result {
5863 Ok(resp) => Ok(resp.content.trim().to_uppercase().contains("YES")),
5864 Err(_) => Ok(false),
5865 }
5866 }
5867
5868 async fn evaluate_and_retry_with_config(
5869 &self,
5870 input: &str,
5871 mut response: String,
5872 config: &ReflectionConfig,
5873 ) -> Result<String> {
5874 let llm = self.get_state_llm()?;
5875 let mut attempts = 0u32;
5876 let max_retries = config.max_retries;
5877
5878 loop {
5879 let evaluation = self
5880 .evaluate_response_with_config(input, &response, config)
5881 .await?;
5882
5883 if evaluation.passed || attempts >= max_retries {
5884 info!(
5885 passed = evaluation.passed,
5886 confidence = evaluation.confidence,
5887 attempts = attempts + 1,
5888 "Skill reflection evaluation complete"
5889 );
5890 return Ok(response);
5891 }
5892
5893 debug!(
5894 attempt = attempts + 1,
5895 failed_criteria = evaluation.failed_criteria().count(),
5896 "Skill response did not meet criteria, retrying"
5897 );
5898
5899 let feedback: Vec<String> = evaluation
5900 .failed_criteria()
5901 .map(|c| format!("- {}", c.criterion))
5902 .collect();
5903
5904 let retry_prompt = format!(
5905 "Your previous response did not meet these criteria:\n{}\n\nPlease provide an improved response to: {}",
5906 feedback.join("\n"),
5907 input
5908 );
5909
5910 let messages = vec![ChatMessage::user(&retry_prompt)];
5911 let retry_response = self
5912 .observe_purpose(
5913 ObservationPurpose::ReflectionEvaluation,
5914 llm.complete(&messages, None),
5915 )
5916 .await
5917 .map_err(|e| AgentError::LLM(e.to_string()))?;
5918
5919 response = retry_response.content.trim().to_string();
5920 attempts += 1;
5921 }
5922 }
5923
5924 async fn evaluate_response_with_config(
5925 &self,
5926 input: &str,
5927 response: &str,
5928 config: &ReflectionConfig,
5929 ) -> Result<EvaluationResult> {
5930 let evaluator_llm = config
5931 .evaluator_llm
5932 .as_ref()
5933 .and_then(|alias| self.llm_registry.get(alias).ok())
5934 .or_else(|| self.llm_registry.router().ok())
5935 .or_else(|| self.llm_registry.default().ok())
5936 .ok_or_else(|| AgentError::Config("No LLM available for evaluation".into()))?;
5937
5938 let criteria = &config.criteria;
5939 let criteria_list = criteria
5940 .iter()
5941 .enumerate()
5942 .map(|(i, c)| format!("{}. {}", i + 1, c))
5943 .collect::<Vec<_>>()
5944 .join("\n");
5945
5946 let prompt = format!(
5947 r#"Evaluate this response against the criteria.
5948
5949User query: "{}"
5950
5951Response to evaluate: "{}"
5952
5953Criteria:
5954{}
5955
5956For each criterion, respond with:
5957- criterion number
5958- PASS or FAIL
5959- brief reason
5960
5961Then provide overall confidence (0.0 to 1.0) and whether it passes overall.
5962
5963Format:
59641. PASS/FAIL - reason
59652. PASS/FAIL - reason
5966...
5967CONFIDENCE: 0.X
5968OVERALL: PASS/FAIL"#,
5969 input, response, criteria_list
5970 );
5971
5972 let messages = vec![ChatMessage::user(&prompt)];
5973 let eval_response = self
5974 .observe_purpose(
5975 ObservationPurpose::ReflectionEvaluation,
5976 evaluator_llm.complete(&messages, None),
5977 )
5978 .await
5979 .map_err(|e| AgentError::LLM(format!("Evaluation failed: {}", e)))?;
5980
5981 let content = eval_response.content.to_uppercase();
5982 let llm_pass = content.contains("OVERALL: PASS");
5983
5984 let confidence = content
5985 .lines()
5986 .find(|l| l.contains("CONFIDENCE:"))
5987 .and_then(|l| {
5988 l.split(':')
5989 .nth(1)
5990 .and_then(|v| v.trim().parse::<f32>().ok())
5991 })
5992 .unwrap_or(if llm_pass { 0.8 } else { 0.4 });
5993
5994 let overall_pass = llm_pass && confidence >= config.pass_threshold;
5997
5998 let mut criteria_results = Vec::new();
5999 for (i, criterion) in criteria.iter().enumerate() {
6000 let line_marker = format!("{}.", i + 1);
6001 let passed = eval_response
6002 .content
6003 .lines()
6004 .find(|l| l.contains(&line_marker))
6005 .map(|l| l.to_uppercase().contains("PASS"))
6006 .unwrap_or(overall_pass);
6007
6008 if passed {
6009 criteria_results.push(CriterionResult::pass(criterion));
6010 } else {
6011 criteria_results.push(CriterionResult::fail(criterion, "Did not meet criterion"));
6012 }
6013 }
6014
6015 Ok(EvaluationResult::new(overall_pass, confidence).with_criteria(criteria_results))
6016 }
6017
6018 async fn process_input(&self, input: &str) -> Result<ProcessData> {
6020 if let Some(processor) = self.get_state_process_processor() {
6021 let purpose = observation_purpose_for_process(processor.input_purpose_hint());
6022 return self
6023 .observe_purpose(purpose, processor.process_input(input))
6024 .await;
6025 }
6026 if let Some(ref processor) = self.process_processor {
6027 let purpose = observation_purpose_for_process(processor.input_purpose_hint());
6028 self.observe_purpose(purpose, processor.process_input(input))
6029 .await
6030 } else {
6031 Ok(ProcessData::new(input))
6032 }
6033 }
6034
6035 async fn process_output(
6037 &self,
6038 output: &str,
6039 input_context: &std::collections::HashMap<String, serde_json::Value>,
6040 ) -> Result<ProcessData> {
6041 if let Some(processor) = self.get_state_process_processor() {
6042 let purpose = observation_purpose_for_process(processor.output_purpose_hint());
6043 return self
6044 .observe_purpose(purpose, processor.process_output(output, input_context))
6045 .await;
6046 }
6047 if let Some(ref processor) = self.process_processor {
6048 let purpose = observation_purpose_for_process(processor.output_purpose_hint());
6049 self.observe_purpose(purpose, processor.process_output(output, input_context))
6050 .await
6051 } else {
6052 Ok(ProcessData::new(output))
6053 }
6054 }
6055
6056 fn get_state_process_processor(&self) -> Option<ProcessProcessor> {
6058 let sm = self.state_machine.as_ref()?;
6059 let def = sm.current_definition()?;
6060 let config = def.process.as_ref()?;
6061 let mut processor = ProcessProcessor::new(config.clone());
6062 if let Some(ref registry) = Some(self.llm_registry.clone()) {
6063 processor = processor.with_llm_registry(registry.clone());
6064 }
6065 processor = processor.with_stage_observer(Arc::new(ObservabilityProcessStageObserver));
6066 Some(processor)
6067 }
6068
6069 async fn check_turn_timeout(&self) -> Result<()> {
6070 if let Some(ref sm) = self.state_machine
6071 && let Some(timeout_state) = sm.check_timeout()
6072 {
6073 let from_state = sm.current();
6074 let history_before = sm.history();
6075 self.execute_state_exit_actions(&from_state).await;
6076 sm.transition_to(&timeout_state, "max_turns exceeded")?;
6077 let entered = sm.current();
6078 let is_reentry =
6079 Self::state_was_previously_entered(&entered, &from_state, &history_before);
6080 self.execute_state_enter_actions(&entered, is_reentry).await;
6081 info!(to = %entered, "Timeout transition");
6082 }
6083 Ok(())
6084 }
6085
6086 fn increment_turn(&self) {
6087 if let Some(ref sm) = self.state_machine {
6088 sm.increment_turn();
6089 }
6090 }
6091
6092 fn transitions_available_for_commit(&self) -> Option<(Vec<Transition>, String)> {
6093 let sm = self.state_machine.as_ref()?;
6094 let current = sm.current();
6095 let transitions: Vec<_> = sm
6096 .auto_transitions()
6097 .into_iter()
6098 .filter(|t| match t.cooldown_turns {
6099 Some(cd) if cd > 0 => {
6100 let resolved = sm.config().resolve_full_path(¤t, &t.to);
6101 !sm.is_on_cooldown(&resolved, cd)
6102 }
6103 _ => true,
6104 })
6105 .collect();
6106 Some((transitions, current))
6107 }
6108
6109 fn transition_reason(transition: &Transition) -> String {
6110 if transition.when.is_empty() {
6111 "guard condition met".to_string()
6112 } else {
6113 transition.when.clone()
6114 }
6115 }
6116
6117 fn build_transition_context(
6119 &self,
6120 user_message: &str,
6121 response: &str,
6122 current_state: &str,
6123 staged: Option<&HashMap<String, Value>>,
6124 ) -> TransitionContext {
6125 let context_map = staged
6126 .map(|writes| self.build_context_with_staged(writes))
6127 .unwrap_or_else(|| self.build_context_with_overlays());
6128 TransitionContext::new(user_message, response, current_state).with_context(context_map)
6129 }
6130
6131 async fn select_transition_candidate(
6133 &self,
6134 user_message: &str,
6135 response: &str,
6136 ) -> Result<Option<TransitionCandidate>> {
6137 let Some((transitions, current_state)) = self.transitions_available_for_commit() else {
6138 return Ok(None);
6139 };
6140 let transitions: Vec<Transition> = transitions
6141 .into_iter()
6142 .filter(|transition| matches!(transition.timing, TransitionTiming::PostResponse))
6143 .collect();
6144 if transitions.is_empty() {
6145 return Ok(None);
6146 }
6147 let Some(evaluator) = self.transition_evaluator.as_ref() else {
6148 return Ok(None);
6149 };
6150 let context = self.build_transition_context(user_message, response, ¤t_state, None);
6151 let selected = self
6152 .observe_purpose(
6153 ObservationPurpose::StateTransitionEvaluation,
6154 evaluator.select_transition(&transitions, &context),
6155 )
6156 .await?;
6157 Ok(selected.map(|index| {
6158 let transition = transitions[index].clone();
6159 TransitionCandidate::new(
6160 current_state,
6161 transition.clone(),
6162 Self::transition_reason(&transition),
6163 )
6164 }))
6165 }
6166
6167 fn select_deterministic_transition_candidate(
6169 &self,
6170 user_message: &str,
6171 current_state: &str,
6172 transitions: &[Transition],
6173 staged: &HashMap<String, Value>,
6174 ) -> Option<TransitionCandidate> {
6175 let context = self.build_transition_context(user_message, "", current_state, Some(staged));
6176
6177 for transition in transitions {
6178 if let Some(guard) = transition.guard.as_ref()
6179 && evaluate_guard(guard, &context)
6180 {
6181 return Some(TransitionCandidate::new(
6182 current_state,
6183 transition.clone(),
6184 Self::transition_reason(transition),
6185 ));
6186 }
6187 }
6188
6189 let resolved_intent = context
6190 .context
6191 .get("resolved_intent")
6192 .and_then(Value::as_str)
6193 .filter(|value| !value.is_empty());
6194 if let Some(resolved_intent) = resolved_intent {
6195 for transition in transitions {
6196 if transition.intent.as_deref() == Some(resolved_intent) {
6197 return Some(TransitionCandidate::new(
6198 current_state,
6199 transition.clone(),
6200 Self::transition_reason(transition),
6201 ));
6202 }
6203 }
6204 }
6205
6206 None
6207 }
6208
6209 async fn commit_transition_candidate(&self, candidate: &TransitionCandidate) -> Result<bool> {
6211 self.commit_transition_target(&candidate.from_state, candidate.target(), &candidate.reason)
6212 .await
6213 }
6214
6215 async fn approve_transition_target(&self, from_state: &str, target: &str) -> Result<bool> {
6217 let approved = self.check_state_hitl(Some(from_state), target).await?;
6218 if !approved {
6219 info!(to = %target, "State transition rejected by HITL");
6220 }
6221 Ok(approved)
6222 }
6223
6224 async fn apply_transition_target(
6226 &self,
6227 from_state: &str,
6228 target: &str,
6229 reason: &str,
6230 staged: Option<&HashMap<String, Value>>,
6231 ) -> Result<bool> {
6232 let Some(ref sm) = self.state_machine else {
6233 return Ok(false);
6234 };
6235
6236 let history_before = sm.history();
6237 self.execute_state_exit_actions(from_state).await;
6238 sm.transition_to(target, reason)?;
6239 sm.reset_no_transition();
6240 if let Some(staged) = staged {
6241 self.commit_staged_context_writes(staged).await;
6242 }
6243 let entered = sm.current();
6244 let is_reentry = Self::state_was_previously_entered(&entered, from_state, &history_before);
6245 self.execute_state_enter_actions(&entered, is_reentry).await;
6246 self.hooks
6247 .on_state_transition(Some(from_state), &entered, reason)
6248 .await;
6249 info!(from = %from_state, to = %entered, "State transition");
6250 Ok(true)
6251 }
6252
6253 async fn commit_transition_target(
6255 &self,
6256 from_state: &str,
6257 target: &str,
6258 reason: &str,
6259 ) -> Result<bool> {
6260 if !self.approve_transition_target(from_state, target).await? {
6261 return Ok(false);
6262 }
6263 self.apply_transition_target(from_state, target, reason, None)
6264 .await
6265 }
6266
6267 async fn apply_pre_response_transition_candidate(
6269 &self,
6270 candidate: &TransitionCandidate,
6271 staged: &HashMap<String, Value>,
6272 processed_input: &str,
6273 ) -> Result<bool> {
6274 self.commit_root_user_message(processed_input).await?;
6275 self.apply_transition_target(
6276 &candidate.from_state,
6277 candidate.target(),
6278 &candidate.reason,
6279 Some(staged),
6280 )
6281 .await
6282 }
6283
6284 async fn commit_pre_response_transition_candidate(
6286 &self,
6287 candidate: &TransitionCandidate,
6288 staged: &HashMap<String, Value>,
6289 processed_input: &str,
6290 ) -> Result<bool> {
6291 if !self
6292 .approve_transition_target(&candidate.from_state, candidate.target())
6293 .await?
6294 {
6295 return Ok(false);
6296 }
6297 self.apply_pre_response_transition_candidate(candidate, staged, processed_input)
6298 .await
6299 }
6300
6301 async fn handle_transition_miss(&self, current_state: &str) -> Result<bool> {
6303 let Some(ref sm) = self.state_machine else {
6304 return Ok(false);
6305 };
6306 sm.increment_no_transition();
6307 let Some(fallback) = sm.check_fallback() else {
6308 return Ok(false);
6309 };
6310 self.commit_transition_target(current_state, &fallback, "fallback after no transitions")
6311 .await
6312 }
6313
6314 async fn evaluate_transitions(&self, user_message: &str, response: &str) -> Result<bool> {
6316 let Some((transitions, current_state)) = self.transitions_available_for_commit() else {
6317 return Ok(false);
6318 };
6319 if transitions.is_empty() {
6320 return Ok(false);
6321 }
6322 if let Some(candidate) = self
6323 .select_transition_candidate(user_message, response)
6324 .await?
6325 {
6326 return self.commit_transition_candidate(&candidate).await;
6327 }
6328 self.handle_transition_miss(¤t_state).await
6329 }
6330
6331 async fn try_pre_response_transition(
6333 &self,
6334 processed_input: &str,
6335 ) -> Result<Option<AgentResponse>> {
6336 let optimization = &self.runtime_config.optimization;
6337 if !optimization.enabled || !optimization.pre_response_deterministic_transitions {
6338 return Ok(None);
6339 }
6340 let Some((transitions, current_state)) = self.transitions_available_for_commit() else {
6341 return Ok(None);
6342 };
6343 let eligible: Vec<Transition> = transitions
6344 .into_iter()
6345 .filter(|transition| !transition.requires_response)
6346 .filter(|transition| matches!(transition.timing, TransitionTiming::PreResponse))
6347 .collect();
6348 if eligible.is_empty() {
6349 return Ok(None);
6350 }
6351
6352 let empty_staged = HashMap::new();
6353 let mut extracted_staged: Option<HashMap<String, Value>> = None;
6354 let mut selected: Option<(TransitionCandidate, HashMap<String, Value>)> = None;
6355
6356 for transition in &eligible {
6357 let use_extractors = optimization.pre_response_extractors || transition.run_extractors;
6358 let staged_for_eval = if use_extractors {
6359 if extracted_staged.is_none() {
6360 extracted_staged =
6361 Some(self.run_context_extractors_staged(processed_input).await);
6362 }
6363 extracted_staged.as_ref().unwrap_or(&empty_staged)
6364 } else {
6365 &empty_staged
6366 };
6367
6368 if let Some(candidate) = self.select_deterministic_transition_candidate(
6369 processed_input,
6370 ¤t_state,
6371 std::slice::from_ref(transition),
6372 staged_for_eval,
6373 ) {
6374 let staged_for_commit = if use_extractors {
6375 staged_for_eval.clone()
6376 } else {
6377 HashMap::new()
6378 };
6379 selected = Some((candidate, staged_for_commit));
6380 break;
6381 }
6382 }
6383
6384 let Some((candidate, staged)) = selected else {
6385 return Ok(None);
6386 };
6387
6388 if !self
6389 .commit_pre_response_transition_candidate(&candidate, &staged, processed_input)
6390 .await?
6391 {
6392 return Ok(None);
6393 }
6394 self.redispatch_current_state(processed_input)
6395 .await
6396 .map(Some)
6397 }
6398
6399 async fn try_speculative_branches(
6404 &self,
6405 processed_input: &str,
6406 input_context: &HashMap<String, Value>,
6407 ) -> Result<Option<AgentResponse>> {
6408 let optimization = &self.runtime_config.optimization;
6409 if !optimization.enabled {
6410 return Ok(None);
6411 }
6412
6413 let effective_reasoning_mode = self.get_effective_reasoning_config().mode.clone();
6414 if !matches!(
6415 effective_reasoning_mode,
6416 ReasoningMode::None | ReasoningMode::Auto
6417 ) {
6418 return Ok(None);
6419 }
6420
6421 let mut transition_enabled =
6422 optimization.speculative_state_transitions && self.has_parallel_transition_candidates();
6423 let mut skill_enabled = optimization.speculative_skill_routing
6424 && self.skill_router.is_some()
6425 && self.pending_skill_id.read().is_none();
6426 let mut reasoning_enabled = optimization.speculative_reasoning_auto
6427 && matches!(effective_reasoning_mode, ReasoningMode::Auto);
6428
6429 if matches!(effective_reasoning_mode, ReasoningMode::Auto)
6430 && (!reasoning_enabled || optimization.max_speculative_llm_calls_per_turn < 2)
6431 {
6432 return Ok(None);
6433 }
6434
6435 if !transition_enabled && !skill_enabled && !reasoning_enabled {
6436 return Ok(None);
6437 }
6438
6439 let mut optional_slots = optimization.max_parallel_runtime_tasks.saturating_sub(1);
6440 let mut speculative_call_slots = optimization
6441 .max_speculative_llm_calls_per_turn
6442 .saturating_sub(1);
6443 if reasoning_enabled {
6444 if optional_slots == 0 || speculative_call_slots == 0 {
6445 return Ok(None);
6446 }
6447 optional_slots -= 1;
6448 speculative_call_slots -= 1;
6449 }
6450 if transition_enabled {
6451 if optional_slots == 0 {
6452 transition_enabled = false;
6453 } else {
6454 optional_slots -= 1;
6455 }
6456 }
6457 if skill_enabled && (optional_slots == 0 || speculative_call_slots == 0) {
6458 skill_enabled = false;
6459 }
6460
6461 if !transition_enabled && !skill_enabled && !reasoning_enabled {
6462 return Ok(None);
6463 }
6464
6465 let main_kind = if transition_enabled {
6466 RuntimeOptimizationKind::ParallelStateTransition
6467 } else if skill_enabled {
6468 RuntimeOptimizationKind::SpeculativeSkillRouting
6469 } else {
6470 RuntimeOptimizationKind::SpeculativeReasoningAuto
6471 };
6472 if !self.reserve_active_speculative_llm_call(main_kind) {
6473 return Ok(None);
6474 }
6475
6476 let mut branch_set = ScheduledBranchSet::new(optimization.max_parallel_runtime_tasks)?;
6477 let main_branch = RuntimeBranch::new(
6478 RuntimeTaskPurpose::MainResponse,
6479 main_kind,
6480 RuntimeTaskPriority::Normal,
6481 RuntimeCommitBehavior::FinalResponse,
6482 );
6483 let transition_branch = RuntimeBranch::new(
6484 RuntimeTaskPurpose::StateTransition,
6485 RuntimeOptimizationKind::ParallelStateTransition,
6486 RuntimeTaskPriority::Critical,
6487 RuntimeCommitBehavior::TransitionDecision,
6488 );
6489 let skill_branch = RuntimeBranch::new(
6490 RuntimeTaskPurpose::SkillRouting,
6491 RuntimeOptimizationKind::SpeculativeSkillRouting,
6492 RuntimeTaskPriority::High,
6493 RuntimeCommitBehavior::SkillSelection,
6494 );
6495 let reasoning_branch = RuntimeBranch::new(
6496 RuntimeTaskPurpose::ReasoningJudge,
6497 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6498 RuntimeTaskPriority::Normal,
6499 RuntimeCommitBehavior::ReasoningDecision,
6500 );
6501 let main_id = main_branch.branch_id();
6502 let transition_id = transition_branch.branch_id();
6503 let skill_id = skill_branch.branch_id();
6504 let reasoning_id = reasoning_branch.branch_id();
6505
6506 let main_id_for_future = main_id.clone();
6507 if !branch_set.schedule(
6508 main_branch,
6509 Box::pin(async move {
6510 match crate::optimization::observability::with_branch_observation(
6511 &main_id_for_future,
6512 main_kind,
6513 RuntimeCommitBehavior::FinalResponse,
6514 self.generate_main_response_draft(processed_input, &ReasoningMode::None),
6515 )
6516 .await
6517 {
6518 Ok(draft) => RuntimeBranchResult::MainDraft(draft),
6519 Err(error) => RuntimeBranchResult::Failed(error),
6520 }
6521 }),
6522 ) {
6523 return Ok(None);
6524 }
6525
6526 if transition_enabled {
6527 let transition_id_for_future = transition_id.clone();
6528 if !branch_set.schedule(
6529 transition_branch,
6530 Box::pin(async move {
6531 match crate::optimization::observability::with_branch_observation(
6532 &transition_id_for_future,
6533 RuntimeOptimizationKind::ParallelStateTransition,
6534 RuntimeCommitBehavior::TransitionDecision,
6535 self.select_parallel_transition_candidate(processed_input),
6536 )
6537 .await
6538 {
6539 Ok(ParallelTransitionSelection::Candidate(candidate)) => {
6540 RuntimeBranchResult::Transition(Some(candidate))
6541 }
6542 Ok(ParallelTransitionSelection::NoMatch) => {
6543 RuntimeBranchResult::Transition(None)
6544 }
6545 Ok(ParallelTransitionSelection::ReservationExhausted) => {
6546 RuntimeBranchResult::Cancelled
6547 }
6548 Err(error) => RuntimeBranchResult::Failed(error),
6549 }
6550 }),
6551 ) {
6552 transition_enabled = false;
6553 }
6554 }
6555
6556 if skill_enabled {
6557 let skill_id_for_future = skill_id.clone();
6558 if !branch_set.schedule(
6559 skill_branch,
6560 Box::pin(async move {
6561 if !self.reserve_active_speculative_llm_call(
6562 RuntimeOptimizationKind::SpeculativeSkillRouting,
6563 ) {
6564 return RuntimeBranchResult::Cancelled;
6565 }
6566 match crate::optimization::observability::with_branch_observation(
6567 &skill_id_for_future,
6568 RuntimeOptimizationKind::SpeculativeSkillRouting,
6569 RuntimeCommitBehavior::SkillSelection,
6570 self.select_skill_candidate(processed_input),
6571 )
6572 .await
6573 {
6574 Ok(candidate) => RuntimeBranchResult::Skill(candidate),
6575 Err(error) => RuntimeBranchResult::Failed(error),
6576 }
6577 }),
6578 ) {
6579 skill_enabled = false;
6580 }
6581 }
6582
6583 if reasoning_enabled {
6584 let reasoning_id_for_future = reasoning_id.clone();
6585 if !branch_set.schedule(
6586 reasoning_branch,
6587 Box::pin(async move {
6588 if !self.reserve_active_speculative_llm_call(
6589 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6590 ) {
6591 return RuntimeBranchResult::Cancelled;
6592 }
6593 match crate::optimization::observability::with_branch_observation(
6594 &reasoning_id_for_future,
6595 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6596 RuntimeCommitBehavior::ReasoningDecision,
6597 self.determine_reasoning_mode_strict(processed_input),
6598 )
6599 .await
6600 {
6601 Ok(mode) => RuntimeBranchResult::Reasoning(mode),
6602 Err(error) => RuntimeBranchResult::Failed(error),
6603 }
6604 }),
6605 ) {
6606 reasoning_enabled = false;
6607 }
6608 }
6609
6610 if matches!(effective_reasoning_mode, ReasoningMode::Auto) && !reasoning_enabled {
6611 self.finalize_pending_branches(branch_set.cancel_pending());
6612 return Ok(None);
6613 }
6614
6615 if !transition_enabled && !skill_enabled && !reasoning_enabled {
6616 self.finalize_pending_branches(branch_set.cancel_pending());
6617 return Ok(None);
6618 }
6619
6620 let mut main_pending = true;
6621 let mut skill_pending = skill_enabled;
6622 let mut reasoning_pending = reasoning_enabled;
6623 let mut transition_finalized = !transition_enabled;
6624 let mut skill_finalized = !skill_enabled;
6625 let mut reasoning_finalized = !reasoning_enabled;
6626 let mut main_result: Option<Result<MainResponseDraft>> = None;
6627 let mut transition_candidate: Option<TransitionCandidate> = None;
6628 let mut skill_candidate: Option<SkillCandidate> = None;
6629 let mut reasoning_decision: Option<ReasoningMode> = None;
6630 let mut transition_fallback_required = false;
6631 let mut skill_fallback_required = false;
6632 let mut reasoning_fallback_required = false;
6633
6634 loop {
6635 if let Some(candidate) = transition_candidate.take() {
6636 if self
6637 .approve_transition_target(&candidate.from_state, candidate.target())
6638 .await?
6639 {
6640 self.finalize_pending_branches(branch_set.cancel_pending());
6642 if !main_pending {
6643 self.finalize_branch_loss(
6644 &main_id,
6645 main_kind,
6646 RuntimeCommitBehavior::FinalResponse,
6647 false,
6648 main_result.as_ref().map(|result| result.is_err()),
6649 );
6650 }
6651 if skill_enabled && !skill_pending {
6652 self.finalize_branch_loss(
6653 &skill_id,
6654 RuntimeOptimizationKind::SpeculativeSkillRouting,
6655 RuntimeCommitBehavior::SkillSelection,
6656 false,
6657 Some(false),
6658 );
6659 }
6660 if reasoning_enabled && !reasoning_pending {
6661 self.finalize_branch_loss(
6662 &reasoning_id,
6663 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6664 RuntimeCommitBehavior::ReasoningDecision,
6665 false,
6666 Some(false),
6667 );
6668 }
6669 if !self
6670 .apply_pre_response_transition_candidate(
6671 &candidate,
6672 &HashMap::new(),
6673 processed_input,
6674 )
6675 .await?
6676 {
6677 self.finalize_optional_branch(
6678 &transition_id,
6679 RuntimeOptimizationKind::ParallelStateTransition,
6680 RuntimeCommitBehavior::TransitionDecision,
6681 "discarded",
6682 false,
6683 );
6684 return Ok(None);
6685 }
6686 self.finalize_optional_branch(
6687 &transition_id,
6688 RuntimeOptimizationKind::ParallelStateTransition,
6689 RuntimeCommitBehavior::TransitionDecision,
6690 "committed",
6691 true,
6692 );
6693 return self
6694 .redispatch_current_state(processed_input)
6695 .await
6696 .map(Some);
6697 }
6698 self.finalize_optional_branch(
6699 &transition_id,
6700 RuntimeOptimizationKind::ParallelStateTransition,
6701 RuntimeCommitBehavior::TransitionDecision,
6702 "discarded",
6703 false,
6704 );
6705 transition_finalized = true;
6706 }
6707
6708 if transition_finalized && skill_candidate.is_some() {
6709 let candidate = skill_candidate.take().unwrap();
6710 self.finalize_optional_branch(
6711 &skill_id,
6712 RuntimeOptimizationKind::SpeculativeSkillRouting,
6713 RuntimeCommitBehavior::SkillSelection,
6714 "committed",
6715 true,
6716 );
6717 if !main_pending {
6718 self.finalize_branch_loss(
6719 &main_id,
6720 main_kind,
6721 RuntimeCommitBehavior::FinalResponse,
6722 false,
6723 main_result.as_ref().map(|result| result.is_err()),
6724 );
6725 }
6726 if reasoning_enabled && !reasoning_pending {
6727 self.finalize_branch_loss(
6728 &reasoning_id,
6729 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6730 RuntimeCommitBehavior::ReasoningDecision,
6731 false,
6732 Some(false),
6733 );
6734 }
6735 self.finalize_pending_branches(branch_set.cancel_pending());
6736 self.commit_root_user_message(processed_input).await?;
6737 return match self
6738 .commit_skill_candidate_route_result(candidate, processed_input)
6739 .await?
6740 {
6741 SkillRouteResult::Response { skill_id, content } => self
6742 .handle_skill_response(processed_input, &skill_id, content, input_context)
6743 .await
6744 .map(Some),
6745 SkillRouteResult::NeedsClarification(response) => {
6746 if response
6747 .metadata
6748 .as_ref()
6749 .and_then(|m| m.get("disambiguation"))
6750 .and_then(|d| d.get("status"))
6751 .and_then(|s| s.as_str())
6752 == Some("awaiting_clarification")
6753 {
6754 self.memory
6755 .add_message(ChatMessage::assistant(&response.content))
6756 .await?;
6757 }
6758 self.finish_turn_if_root(&response).await?;
6759 Ok(Some(response))
6760 }
6761 SkillRouteResult::NoMatch => Ok(None),
6762 };
6763 }
6764
6765 if transition_finalized
6766 && skill_finalized
6767 && let Some(reasoning_mode) = reasoning_decision.take()
6768 {
6769 if !matches!(reasoning_mode, ReasoningMode::None) {
6770 self.finalize_optional_branch(
6771 &reasoning_id,
6772 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6773 RuntimeCommitBehavior::ReasoningDecision,
6774 "committed",
6775 true,
6776 );
6777 if !main_pending {
6778 self.finalize_branch_loss(
6779 &main_id,
6780 main_kind,
6781 RuntimeCommitBehavior::FinalResponse,
6782 false,
6783 main_result.as_ref().map(|result| result.is_err()),
6784 );
6785 }
6786 self.finalize_pending_branches(branch_set.cancel_pending());
6787 self.commit_root_user_message(processed_input).await?;
6788 return if matches!(reasoning_mode, ReasoningMode::PlanAndExecute) {
6789 self.handle_plan_and_execute(processed_input, input_context, true)
6790 .await
6791 .map(Some)
6792 } else {
6793 self.run_committed_response_loop_with_reasoning(
6794 processed_input,
6795 input_context,
6796 reasoning_mode,
6797 true,
6798 )
6799 .await
6800 .map(Some)
6801 };
6802 }
6803 self.finalize_optional_branch(
6804 &reasoning_id,
6805 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6806 RuntimeCommitBehavior::ReasoningDecision,
6807 "committed",
6808 true,
6809 );
6810 reasoning_finalized = true;
6811 }
6812
6813 if transition_finalized && skill_finalized && reasoning_finalized {
6814 if transition_fallback_required
6815 || skill_fallback_required
6816 || reasoning_fallback_required
6817 {
6818 if !main_pending {
6819 self.finalize_branch_loss(
6820 &main_id,
6821 main_kind,
6822 RuntimeCommitBehavior::FinalResponse,
6823 false,
6824 main_result.as_ref().map(|result| result.is_err()),
6825 );
6826 }
6827 self.finalize_pending_branches(branch_set.cancel_pending());
6828 return Ok(None);
6829 }
6830
6831 if let Some(result) = main_result.take() {
6832 let draft = match result {
6833 Ok(draft) => draft,
6834 Err(error) => {
6835 self.finalize_optional_branch(
6836 &main_id,
6837 main_kind,
6838 RuntimeCommitBehavior::FinalResponse,
6839 "failed",
6840 false,
6841 );
6842 self.finalize_pending_branches(branch_set.cancel_pending());
6843 return Err(error);
6844 }
6845 };
6846 self.finalize_optional_branch(
6847 &main_id,
6848 main_kind,
6849 RuntimeCommitBehavior::FinalResponse,
6850 "committed",
6851 true,
6852 );
6853 self.finalize_pending_branches(branch_set.cancel_pending());
6854 return self
6855 .commit_main_response_draft(
6856 processed_input,
6857 input_context,
6858 draft,
6859 ReasoningMode::None,
6860 reasoning_enabled,
6861 )
6862 .await
6863 .map(Some);
6864 }
6865 }
6866
6867 if branch_set.is_empty() {
6868 return Ok(None);
6869 }
6870
6871 let Some(outcome) = branch_set.next_completed().await else {
6872 return Ok(None);
6873 };
6874 let branch_id = outcome.branch.branch_id();
6875 match outcome.result {
6876 RuntimeBranchResult::MainDraft(draft) => {
6877 main_pending = false;
6878 main_result = Some(Ok(draft));
6879 }
6880 RuntimeBranchResult::Transition(candidate) => {
6881 if let Some(candidate) = candidate {
6882 transition_candidate = Some(candidate);
6883 } else {
6884 self.finalize_optional_branch(
6885 &transition_id,
6886 RuntimeOptimizationKind::ParallelStateTransition,
6887 RuntimeCommitBehavior::TransitionDecision,
6888 "discarded",
6889 false,
6890 );
6891 transition_finalized = true;
6892 }
6893 }
6894 RuntimeBranchResult::Skill(candidate) => {
6895 skill_pending = false;
6896 if let Some(candidate) = candidate {
6897 skill_candidate = Some(candidate);
6898 } else {
6899 self.finalize_optional_branch(
6900 &skill_id,
6901 RuntimeOptimizationKind::SpeculativeSkillRouting,
6902 RuntimeCommitBehavior::SkillSelection,
6903 "discarded",
6904 false,
6905 );
6906 skill_finalized = true;
6907 }
6908 }
6909 RuntimeBranchResult::Reasoning(mode) => {
6910 reasoning_pending = false;
6911 reasoning_decision = Some(mode);
6912 }
6913 RuntimeBranchResult::Failed(error) => {
6914 if branch_id == main_id {
6915 main_pending = false;
6916 main_result = Some(Err(error));
6917 } else if branch_id == transition_id {
6918 self.finalize_optional_branch(
6919 &transition_id,
6920 RuntimeOptimizationKind::ParallelStateTransition,
6921 RuntimeCommitBehavior::TransitionDecision,
6922 "failed",
6923 false,
6924 );
6925 transition_finalized = true;
6926 } else if branch_id == skill_id {
6927 skill_pending = false;
6928 self.finalize_optional_branch(
6929 &skill_id,
6930 RuntimeOptimizationKind::SpeculativeSkillRouting,
6931 RuntimeCommitBehavior::SkillSelection,
6932 "failed",
6933 false,
6934 );
6935 skill_finalized = true;
6936 } else if branch_id == reasoning_id {
6937 reasoning_pending = false;
6938 self.finalize_optional_branch(
6939 &reasoning_id,
6940 RuntimeOptimizationKind::SpeculativeReasoningAuto,
6941 RuntimeCommitBehavior::ReasoningDecision,
6942 "failed",
6943 false,
6944 );
6945 reasoning_finalized = true;
6946 }
6947 }
6948 RuntimeBranchResult::Cancelled => {
6949 self.finalize_optional_branch(
6950 &branch_id,
6951 outcome.branch.optimization,
6952 outcome.branch.commit_behavior,
6953 "cancelled",
6954 false,
6955 );
6956 if branch_id == main_id {
6957 main_pending = false;
6958 main_result =
6959 Some(Err(AgentError::Other("main branch cancelled".to_string())));
6960 } else if branch_id == transition_id {
6961 transition_finalized = true;
6962 transition_fallback_required = true;
6963 } else if branch_id == skill_id {
6964 skill_pending = false;
6965 skill_finalized = true;
6966 skill_fallback_required = true;
6967 } else if branch_id == reasoning_id {
6968 reasoning_pending = false;
6969 reasoning_finalized = true;
6970 reasoning_fallback_required = true;
6971 }
6972 }
6973 }
6974 }
6975 }
6976
6977 fn finalize_pending_branches(&self, branches: Vec<RuntimeBranch>) {
6978 for branch in branches {
6979 self.finalize_optional_branch(
6980 &branch.branch_id(),
6981 branch.optimization,
6982 branch.commit_behavior,
6983 "cancelled",
6984 false,
6985 );
6986 }
6987 }
6988
6989 fn finalize_branch_loss(
6994 &self,
6995 branch_id: &str,
6996 optimization: RuntimeOptimizationKind,
6997 commit_behavior: RuntimeCommitBehavior,
6998 pending: bool,
6999 completed_failed: Option<bool>,
7000 ) {
7001 let status = if pending {
7002 "cancelled"
7003 } else if completed_failed.unwrap_or(false) {
7004 "failed"
7005 } else {
7006 "discarded"
7007 };
7008 self.finalize_optional_branch(branch_id, optimization, commit_behavior, status, false);
7009 }
7010
7011 fn finalize_optional_branch(
7016 &self,
7017 branch_id: &str,
7018 optimization: RuntimeOptimizationKind,
7019 commit_behavior: RuntimeCommitBehavior,
7020 status: &str,
7021 winner: bool,
7022 ) {
7023 crate::optimization::observability::finalize_branch(
7024 self.observability_manager.as_ref(),
7025 branch_id,
7026 status,
7027 winner,
7028 optimization,
7029 commit_behavior,
7030 );
7031 }
7032
7033 fn has_parallel_transition_candidates(&self) -> bool {
7038 self.transitions_available_for_commit()
7039 .map(|(transitions, _)| {
7040 transitions
7041 .iter()
7042 .any(|transition| matches!(transition.timing, TransitionTiming::Parallel))
7043 })
7044 .unwrap_or(false)
7045 }
7046
7047 async fn select_parallel_transition_candidate(
7052 &self,
7053 processed_input: &str,
7054 ) -> Result<ParallelTransitionSelection> {
7055 let Some((transitions, current_state)) = self.transitions_available_for_commit() else {
7056 return Ok(ParallelTransitionSelection::NoMatch);
7057 };
7058 let parallel: Vec<Transition> = transitions
7059 .into_iter()
7060 .filter(|transition| matches!(transition.timing, TransitionTiming::Parallel))
7061 .filter(|transition| !transition.requires_response)
7062 .collect();
7063 if parallel.is_empty() {
7064 return Ok(ParallelTransitionSelection::NoMatch);
7065 }
7066 let empty_staged = HashMap::new();
7067 if let Some(candidate) = self.select_deterministic_transition_candidate(
7068 processed_input,
7069 ¤t_state,
7070 ¶llel,
7071 &empty_staged,
7072 ) {
7073 return Ok(ParallelTransitionSelection::Candidate(candidate));
7074 }
7075 let when_transitions: Vec<(usize, &Transition)> = parallel
7076 .iter()
7077 .enumerate()
7078 .filter(|(_, transition)| !transition.when.trim().is_empty())
7079 .collect();
7080 if when_transitions.is_empty() {
7081 return Ok(ParallelTransitionSelection::NoMatch);
7082 }
7083 let llm = self
7084 .llm_registry
7085 .router()
7086 .or_else(|_| self.llm_registry.default())
7087 .map_err(|e| AgentError::Config(e.to_string()))?;
7088 let conditions = when_transitions
7089 .iter()
7090 .enumerate()
7091 .map(|(display_idx, (_, transition))| {
7092 format!("{}. {}", display_idx + 1, transition.when)
7093 })
7094 .collect::<Vec<_>>()
7095 .join("\n");
7096 if !self
7097 .reserve_active_speculative_llm_call(RuntimeOptimizationKind::ParallelStateTransition)
7098 {
7099 return Ok(ParallelTransitionSelection::ReservationExhausted);
7100 }
7101 let context_preview = self.branch_context_preview();
7102 let prompt = format!(
7103 "Based only on the current user message and context, which transition condition is met?\n\nCurrent state: {}\nUser message: {}\nContext:\n{}\n\nConditions:\n{}\n0. None of the above\n\nReply with ONLY the number (0-{}).",
7104 current_state,
7105 processed_input,
7106 context_preview,
7107 conditions,
7108 when_transitions.len()
7109 );
7110 let response = self
7111 .observe_purpose(
7112 ObservationPurpose::StateTransitionEvaluation,
7113 llm.complete(&[ChatMessage::user(prompt)], None),
7114 )
7115 .await
7116 .map_err(|e| AgentError::LLM(e.to_string()))?;
7117 let choice = response.content.trim().parse::<usize>().unwrap_or(0);
7118 if choice == 0 || choice > when_transitions.len() {
7119 return Ok(ParallelTransitionSelection::NoMatch);
7120 }
7121 let transition = when_transitions[choice - 1].1.clone();
7122 Ok(ParallelTransitionSelection::Candidate(
7123 TransitionCandidate::new(
7124 current_state,
7125 transition.clone(),
7126 Self::transition_reason(&transition),
7127 ),
7128 ))
7129 }
7130
7131 async fn redispatch_current_state(&self, processed_input: &str) -> Result<AgentResponse> {
7133 const MAX_REDISPATCH_DEPTH: u32 = 3;
7134 let current_depth = *self.redispatch_depth.read();
7135 if current_depth >= MAX_REDISPATCH_DEPTH {
7136 warn!(depth = current_depth, "Re-dispatch depth limit reached");
7137 let response = AgentResponse::new("");
7138 self.finish_turn_if_root(&response).await?;
7139 return Ok(response);
7140 }
7141 *self.redispatch_depth.write() += 1;
7142 if let Some(context) = self.active_turn_context.write().as_mut() {
7143 context.enter_redispatch();
7144 }
7145 let result = Box::pin(self.run_loop_internal(processed_input)).await;
7146 *self.redispatch_depth.write() -= 1;
7147 if let Some(context) = self.active_turn_context.write().as_mut() {
7148 context.exit_redispatch();
7149 }
7150 let response = result?;
7151 self.finish_turn_if_root(&response).await?;
7152 Ok(response)
7153 }
7154
7155 async fn finish_turn_if_root(&self, response: &AgentResponse) -> Result<()> {
7157 if *self.redispatch_depth.read() == 0 {
7158 self.post_turn_session_lifecycle().await?;
7159 if let Some(context) = self.active_turn_context.write().as_mut() {
7160 context.mark_post_turn_lifecycle_completed();
7161 }
7162 self.hooks.on_response(response).await;
7163 self.end_root_turn();
7164 }
7165 Ok(())
7166 }
7167
7168 async fn execute_state_exit_actions(&self, state_path: &str) {
7170 if let Some(ref sm) = self.state_machine
7171 && let Some(def) = sm.get_definition(state_path)
7172 && !def.on_exit.is_empty()
7173 {
7174 debug!(state = %state_path, count = def.on_exit.len(), "Executing on_exit actions");
7175 self.execute_state_actions(&def.on_exit).await;
7176 }
7177 }
7178
7179 fn state_was_previously_entered(
7181 state_path: &str,
7182 from_state: &str,
7183 history_before: &[StateTransitionEvent],
7184 ) -> bool {
7185 state_path == from_state
7186 || history_before
7187 .iter()
7188 .any(|event| event.from == state_path || event.to == state_path)
7189 }
7190
7191 async fn execute_state_enter_actions(&self, state_path: &str, is_reentry: bool) {
7193 if let Some(ref sm) = self.state_machine
7194 && let Some(def) = sm.get_definition(state_path)
7195 {
7196 if is_reentry && !def.on_reenter.is_empty() {
7197 debug!(state = %state_path, count = def.on_reenter.len(), "Executing on_reenter actions");
7198 self.execute_state_actions(&def.on_reenter).await;
7199 } else if !def.on_enter.is_empty() {
7200 debug!(state = %state_path, count = def.on_enter.len(), "Executing on_enter actions");
7201 self.execute_state_actions(&def.on_enter).await;
7202 }
7203 }
7204 }
7205
7206 async fn execute_state_actions(&self, actions: &[StateAction]) {
7208 for (action_index, action) in actions.iter().enumerate() {
7209 match action {
7210 StateAction::Tool { tool, args } => {
7211 let raw_args = args.clone().unwrap_or(Value::Object(Default::default()));
7212 let args_value = self.render_action_args(&raw_args);
7213 let state = self.state_machine.as_ref().map(|sm| sm.current());
7214 let request = ToolExecutionRequest::new(
7215 uuid::Uuid::new_v4().to_string(),
7216 tool.clone(),
7217 args_value,
7218 ToolCallSource::StateAction {
7219 state,
7220 action_index,
7221 },
7222 );
7223 match self.execute_tool_record(request).await {
7224 Ok(record) if record.success => {
7225 debug!(tool = %record.canonical_id, "State action: tool executed");
7226 let _ = self.context_manager.set(
7227 "last_tool_result",
7228 serde_json::Value::String(record.model_output_string()),
7229 );
7230 let _ = self.context_manager.set(
7231 "last_tool_record",
7232 serde_json::to_value(record).unwrap_or(Value::Null),
7233 );
7234 }
7235 Ok(record) => {
7236 warn!(tool = %record.canonical_id, error = %record.output, "State action: tool failed");
7237 }
7238 Err(e) => {
7239 warn!(tool = %tool, error = %e, "State action: tool failed")
7240 }
7241 }
7242 }
7243 StateAction::Skill { skill } => {
7244 if let Some(ref executor) = self.skill_executor {
7245 if let Some(def) = self.skills.iter().find(|s| s.id == *skill) {
7246 match executor
7247 .execute_with_invoker(def, "", serde_json::json!({}), self)
7248 .await
7249 {
7250 Ok(_) => debug!(skill = %skill, "State action: skill executed"),
7251 Err(e) => {
7252 warn!(skill = %skill, error = %e, "State action: skill failed")
7253 }
7254 }
7255 } else {
7256 warn!(skill = %skill, "State action: skill not found");
7257 }
7258 }
7259 }
7260 StateAction::SetContext { set_context } => {
7261 for (key, value) in set_context {
7262 if let Err(e) = self.context_manager.set(key, value.clone()) {
7263 warn!(key = %key, error = %e, "State action: set_context failed");
7264 } else {
7265 debug!(key = %key, "State action: context set");
7266 }
7267 }
7268 }
7269 StateAction::Prompt {
7270 prompt,
7271 llm,
7272 store_as,
7273 } => {
7274 let llm_result = if let Some(alias) = llm {
7275 self.llm_registry.get(alias)
7276 } else {
7277 self.llm_registry.default()
7278 };
7279 match llm_result {
7280 Ok(llm_provider) => {
7281 let context = self.build_context_with_overlays();
7283 let rendered_prompt = self
7284 .template_renderer
7285 .render(prompt, &context)
7286 .unwrap_or_else(|_| prompt.clone());
7287 let recent =
7288 self.memory.get_messages(Some(5)).await.unwrap_or_default();
7289 let mut messages: Vec<ChatMessage> = recent;
7290 messages.push(ChatMessage::user(&rendered_prompt));
7291 match self
7292 .observe_purpose(
7293 ObservationPurpose::StateAction,
7294 llm_provider.complete(&messages, None),
7295 )
7296 .await
7297 {
7298 Ok(response) => {
7299 if let Some(key) = store_as {
7300 let _ = self
7301 .context_manager
7302 .set(key, Value::String(response.content));
7303 debug!(key = %key, "State action: prompt result stored");
7304 }
7305 }
7306 Err(e) => {
7307 warn!(error = %e, "State action: prompt LLM call failed");
7308 }
7309 }
7310 }
7311 Err(e) => {
7312 warn!(error = %e, "State action: LLM not found for prompt");
7313 }
7314 }
7315 }
7316 }
7317 }
7318 }
7319
7320 async fn run_context_extractors_staged(&self, user_message: &str) -> HashMap<String, Value> {
7321 let extractors = match &self.state_machine {
7322 Some(sm) => match sm.current_definition() {
7323 Some(def) if !def.extract.is_empty() => def.extract.clone(),
7324 _ => return HashMap::new(),
7325 },
7326 None => return HashMap::new(),
7327 };
7328
7329 let mut staged = HashMap::new();
7330 for extractor in &extractors {
7331 let prompt = if let Some(ref custom) = extractor.llm_extract {
7332 format!(
7333 "User message:\n\"{}\"\n\nInstruction:\n{}",
7334 user_message, custom
7335 )
7336 } else if let Some(ref desc) = extractor.description {
7337 format!(
7338 "From the following message, extract: {}\n\n\
7339 Message: \"{}\"\n\n\
7340 If the information is present, return ONLY the extracted value.\n\
7341 If NOT present, return exactly: __NONE__",
7342 desc, user_message
7343 )
7344 } else {
7345 continue;
7346 };
7347
7348 let llm = match self
7349 .llm_registry
7350 .get(&extractor.llm)
7351 .or_else(|_| self.llm_registry.get("router"))
7352 .or_else(|_| self.llm_registry.get("default"))
7353 {
7354 Ok(llm) => llm,
7355 Err(e) => {
7356 warn!(key = %extractor.key, error = %e, "Extractor LLM not found");
7357 continue;
7358 }
7359 };
7360
7361 let messages = vec![ChatMessage::user(&prompt)];
7362 match self
7363 .observe_purpose(
7364 ObservationPurpose::ContextExtraction,
7365 llm.complete(&messages, None),
7366 )
7367 .await
7368 {
7369 Ok(response) => {
7370 let value = response.content.trim().to_string();
7371 if value != "__NONE__" && !value.is_empty() {
7372 staged.insert(
7373 extractor.key.clone(),
7374 serde_json::Value::String(value.clone()),
7375 );
7376 debug!(key = %extractor.key, value = %value, "Context extracted");
7377 } else if extractor.required {
7378 warn!(key = %extractor.key, "Required extraction returned no value");
7379 }
7380 }
7381 Err(e) => {
7382 warn!(key = %extractor.key, error = %e, "Context extraction LLM call failed");
7383 }
7384 }
7385 }
7386 staged
7387 }
7388
7389 async fn commit_staged_context_writes(&self, staged: &HashMap<String, Value>) {
7390 for (key, value) in staged {
7391 if let Err(error) = self.context_manager.update(key, value.clone()) {
7392 warn!(key = %key, error = %error, "staged context write failed");
7393 }
7394 }
7395 }
7396
7397 async fn run_context_extractors(&self, user_message: &str) {
7399 let staged = self.run_context_extractors_staged(user_message).await;
7400 self.commit_staged_context_writes(&staged).await;
7401 }
7402
7403 async fn check_memory_compression(&self) -> Result<()> {
7404 if self.memory.needs_compression() {
7405 let result = self.memory.compress(None).await?;
7406 if let CompressResult::Compressed {
7407 messages_summarized,
7408 new_summary_length,
7409 tokens_saved,
7410 } = result
7411 {
7412 let event = MemoryCompressEvent::new(
7413 messages_summarized,
7414 tokens_saved,
7415 new_summary_length as u32,
7416 );
7417 self.hooks.on_memory_compress(&event).await;
7418 debug!(
7419 messages = messages_summarized,
7420 tokens_saved = tokens_saved,
7421 "Memory compressed"
7422 );
7423 }
7424 }
7425
7426 self.handle_memory_overflow().await?;
7428 self.check_memory_budget().await;
7429
7430 Ok(())
7431 }
7432
7433 async fn check_memory_budget(&self) {
7434 let Some(ref budget) = self.memory_token_budget else {
7435 return;
7436 };
7437
7438 let context = match self.memory.get_context().await {
7439 Ok(ctx) => ctx,
7440 Err(_) => return,
7441 };
7442
7443 let used_tokens = context.estimated_tokens();
7445 if budget.is_over_warn_threshold(used_tokens) {
7446 let event = MemoryBudgetEvent::new("memory", used_tokens, budget.total);
7447 self.hooks.on_memory_budget_warning(&event).await;
7448 debug!(
7449 used = used_tokens,
7450 total = budget.total,
7451 percent = event.usage_percent,
7452 "Memory budget warning"
7453 );
7454 }
7455
7456 if let Some(ref summary) = context.summary {
7458 let summary_tokens = ai_agents_memory::estimate_tokens(summary);
7459 let summary_budget = budget.allocation.summary;
7460 if summary_budget > 0 {
7461 let warn_threshold =
7462 (summary_budget as f64 * budget.warn_at_percent as f64 / 100.0) as u32;
7463 if summary_tokens >= warn_threshold {
7464 let event = MemoryBudgetEvent::new("summary", summary_tokens, summary_budget);
7465 self.hooks.on_memory_budget_warning(&event).await;
7466 }
7467 }
7468 }
7469
7470 let recent_tokens: u32 = context
7472 .messages
7473 .iter()
7474 .map(ai_agents_memory::estimate_message_tokens)
7475 .sum();
7476 let recent_budget = budget.allocation.recent_messages;
7477 if recent_budget > 0 {
7478 let warn_threshold =
7479 (recent_budget as f64 * budget.warn_at_percent as f64 / 100.0) as u32;
7480 if recent_tokens >= warn_threshold {
7481 let event = MemoryBudgetEvent::new("recent_messages", recent_tokens, recent_budget);
7482 self.hooks.on_memory_budget_warning(&event).await;
7483 }
7484 }
7485
7486 let relationship_budget = budget.allocation.relationships;
7487 if relationship_budget > 0 {
7488 let relationship_tokens = self
7489 .relationship_memory_text()
7490 .map(|text| ai_agents_memory::estimate_tokens(&text))
7491 .unwrap_or(0);
7492 let warn_threshold =
7493 (relationship_budget as f64 * budget.warn_at_percent as f64 / 100.0) as u32;
7494 if relationship_tokens >= warn_threshold {
7495 let event = MemoryBudgetEvent::new(
7496 "relationships",
7497 relationship_tokens,
7498 relationship_budget,
7499 );
7500 self.hooks.on_memory_budget_warning(&event).await;
7501 }
7502 }
7503 }
7504
7505 async fn handle_memory_overflow(&self) -> Result<()> {
7506 let Some(ref budget) = self.memory_token_budget else {
7507 return Ok(());
7508 };
7509
7510 let context = self.memory.get_context().await?;
7511 let used_tokens = context.estimated_tokens();
7512
7513 if used_tokens <= budget.total {
7514 return Ok(());
7515 }
7516
7517 match budget.overflow_strategy {
7518 OverflowStrategy::TruncateOldest => {
7519 let tokens_to_free = used_tokens - budget.total;
7520 let messages_to_evict = self.calculate_eviction_count(tokens_to_free);
7521 if messages_to_evict > 0 {
7522 self.evict_messages(messages_to_evict, EvictionReason::TokenBudgetExceeded)
7523 .await?;
7524 }
7525 }
7526 OverflowStrategy::SummarizeMore => {
7527 let max_attempts = context.total_messages.max(1);
7528 for _ in 0..max_attempts {
7529 match self.memory.compress(None).await? {
7530 CompressResult::Compressed {
7531 messages_summarized,
7532 ..
7533 } if messages_summarized > 0 => {
7534 let context = self.memory.get_context().await?;
7535 if context.estimated_tokens() <= budget.total {
7536 return Ok(());
7537 }
7538 }
7539 _ => break,
7540 }
7541 }
7542 let context = self.memory.get_context().await?;
7543 let used_tokens = context.estimated_tokens();
7544 if used_tokens > budget.total {
7545 return Err(AgentError::MemoryBudgetExceeded {
7546 used: used_tokens,
7547 budget: budget.total,
7548 });
7549 }
7550 }
7551 OverflowStrategy::Error => {
7552 return Err(AgentError::MemoryBudgetExceeded {
7553 used: used_tokens,
7554 budget: budget.total,
7555 });
7556 }
7557 }
7558 Ok(())
7559 }
7560
7561 fn calculate_eviction_count(&self, tokens_to_free: u32) -> usize {
7562 ((tokens_to_free as f64 / 50.0).ceil() as usize).max(1)
7564 }
7565
7566 async fn evict_messages(&self, count: usize, reason: EvictionReason) -> Result<()> {
7567 let evicted = self.memory.evict_oldest(count).await?;
7568 if !evicted.is_empty() {
7569 let event = MemoryEvictEvent {
7570 reason,
7571 messages_evicted: evicted.len(),
7572 importance_scores: vec![],
7573 };
7574 self.hooks.on_memory_evict(&event).await;
7575 debug!(count = evicted.len(), "Messages evicted from memory");
7576 }
7577 Ok(())
7578 }
7579
7580 #[instrument(skip(self, input), fields(agent = %self.info.name))]
7581 async fn determine_reasoning_mode(&self, input: &str) -> Result<ReasoningMode> {
7582 match self.determine_reasoning_mode_strict(input).await {
7583 Ok(mode) => Ok(mode),
7584 Err(_) => Ok(ReasoningMode::None),
7585 }
7586 }
7587
7588 async fn determine_reasoning_mode_strict(&self, input: &str) -> Result<ReasoningMode> {
7589 let effective_config = self.get_effective_reasoning_config();
7590
7591 if !matches!(effective_config.mode, ReasoningMode::Auto) {
7592 return Ok(effective_config.mode.clone());
7593 }
7594
7595 let judge_llm = effective_config
7596 .judge_llm
7597 .as_ref()
7598 .and_then(|alias| self.llm_registry.get(alias).ok())
7599 .or_else(|| self.llm_registry.router().ok())
7600 .or_else(|| self.llm_registry.default().ok());
7601
7602 let Some(llm) = judge_llm else {
7603 return Ok(ReasoningMode::None);
7604 };
7605
7606 let prompt = format!(
7607 r#"Analyze this user request and determine the appropriate reasoning mode.
7608
7609User request: "{}"
7610
7611Choose ONE of these modes:
7612- none: Simple queries, greetings, direct answers (fastest)
7613- cot: Complex analysis, multi-step reasoning, math problems
7614- react: Tasks requiring multiple tool calls with observation
7615- plan_and_execute: Complex multi-step tasks requiring coordination
7616
7617Respond with ONLY the mode name (none, cot, react, or plan_and_execute)."#,
7618 input
7619 );
7620
7621 let messages = vec![ChatMessage::user(&prompt)];
7622 let response = self
7623 .observe_purpose(
7624 ObservationPurpose::ReflectionDecision,
7625 llm.complete(&messages, None),
7626 )
7627 .await
7628 .map_err(|e| AgentError::LLM(e.to_string()))?;
7629
7630 let mode_str = response.content.trim().to_lowercase();
7631 Ok(match mode_str.as_str() {
7632 "cot" => ReasoningMode::CoT,
7633 "react" => ReasoningMode::React,
7634 "plan_and_execute" => ReasoningMode::PlanAndExecute,
7635 _ => ReasoningMode::None,
7636 })
7637 }
7638
7639 async fn should_reflect(&self, input: &str, response: &str) -> Result<bool> {
7640 let effective_config = self.get_effective_reflection_config();
7641
7642 if !effective_config.requires_evaluation() {
7643 return Ok(false);
7644 }
7645
7646 if effective_config.is_enabled() {
7647 return Ok(true);
7648 }
7649
7650 let evaluator_llm = effective_config
7651 .evaluator_llm
7652 .as_ref()
7653 .and_then(|alias| self.llm_registry.get(alias).ok())
7654 .or_else(|| self.llm_registry.router().ok())
7655 .or_else(|| self.llm_registry.default().ok());
7656
7657 let Some(llm) = evaluator_llm else {
7658 return Ok(false);
7659 };
7660
7661 let response_preview: String = response.chars().take(500).collect();
7662 let prompt = format!(
7663 r#"Should this response be evaluated for quality? Consider if it's a complex or important response.
7664
7665User query: "{}"
7666Response: "{}"
7667
7668Answer YES or NO only."#,
7669 input, response_preview
7670 );
7671
7672 let messages = vec![ChatMessage::user(&prompt)];
7673 let result = self
7674 .observe_purpose(
7675 ObservationPurpose::ReflectionDecision,
7676 llm.complete(&messages, None),
7677 )
7678 .await;
7679
7680 match result {
7681 Ok(resp) => Ok(resp.content.trim().to_uppercase().contains("YES")),
7682 Err(_) => Ok(false),
7683 }
7684 }
7685
7686 fn build_cot_system_prompt(&self, base_prompt: &str) -> String {
7687 format!(
7688 "{}\n\n<instruction>\nThink through this step by step before answering:\n1. Understand what is being asked\n2. Break down the problem\n3. Work through each part\n4. Provide your final answer\n\nShow your thinking process, then give your final answer.\n</instruction>",
7689 base_prompt
7690 )
7691 }
7692
7693 fn build_react_system_prompt(&self, base_prompt: &str) -> String {
7694 format!(
7695 "{}\n\n<instruction>\nUse the Reason-Act-Observe pattern:\n1. Thought: Think about what to do\n2. Action: Use a tool if needed\n3. Observation: Analyze the result\n4. Repeat until you have the answer\n\nFormat your response showing Thought/Action/Observation steps.\n</instruction>",
7696 base_prompt
7697 )
7698 }
7699
7700 async fn generate_plan(&self, input: &str) -> Result<Plan> {
7701 let effective = self.get_effective_reasoning_config();
7702 let planning_config = effective.get_planning();
7703
7704 let planner_llm = planning_config
7705 .and_then(|c| c.planner_llm.as_ref())
7706 .and_then(|alias| self.llm_registry.get(alias).ok())
7707 .or_else(|| self.llm_registry.router().ok())
7708 .or_else(|| self.llm_registry.default().ok())
7709 .ok_or_else(|| AgentError::Config("No LLM available for planning".into()))?;
7710
7711 let mut available_tool_ids: Vec<String> = self
7712 .get_available_tool_ids()
7713 .await
7714 .unwrap_or_else(|_| self.tools.list_ids());
7715 let mut available_skills: Vec<String> = self.skills.iter().map(|s| s.id.clone()).collect();
7716
7717 if let Some(config) = planning_config {
7719 if !config.available.tools.is_all() {
7720 available_tool_ids.retain(|t| config.available.tools.allows(t));
7721 }
7722 if !config.available.skills.is_all() {
7723 available_skills.retain(|s| config.available.skills.allows(s));
7724 }
7725 }
7726
7727 let tool_descriptions: Vec<String> = available_tool_ids
7730 .iter()
7731 .filter_map(|id| {
7732 self.tools.get(id).map(|tool| {
7733 let schema = tool.input_schema();
7734 let args_desc = schema
7735 .get("properties")
7736 .and_then(|p| serde_json::to_string(p).ok())
7737 .unwrap_or_else(|| "{}".to_string());
7738 format!(
7739 "- {} ({}): {}\n Arguments: {}",
7740 id,
7741 tool.name(),
7742 tool.description(),
7743 args_desc
7744 )
7745 })
7746 })
7747 .collect();
7748
7749 let tools_section = if tool_descriptions.is_empty() {
7750 "Available tools: none".to_string()
7751 } else {
7752 format!("Available tools:\n{}", tool_descriptions.join("\n"))
7753 };
7754
7755 let skills_section = if available_skills.is_empty() {
7756 "Available skills: none".to_string()
7757 } else {
7758 format!("Available skills: {}", available_skills.join(", "))
7759 };
7760
7761 let prompt = format!(
7762 r#"Create a step-by-step plan to accomplish this goal.
7763
7764Goal: "{}"
7765
7766{}
7767
7768{}
7769
7770Create a plan with clear steps. For each step, specify:
7771- description: What this step accomplishes
7772- action_type: "tool", "skill", "think", or "respond"
7773- action_target: The tool/skill id (if applicable)
7774- args: The arguments object matching the tool's schema (if action_type is "tool")
7775- dependencies: List of step IDs this depends on (empty if none)
7776
7777Respond in JSON format:
7778{{
7779 "steps": [
7780 {{"id": "step1", "description": "...", "action_type": "tool", "action_target": "tool_id", "args": {{"required_field": "value"}}, "dependencies": []}},
7781 {{"id": "step2", "description": "...", "action_type": "think", "action_target": "...", "dependencies": ["step1"]}}
7782 ]
7783}}"#,
7784 input, tools_section, skills_section,
7785 );
7786
7787 let messages = vec![ChatMessage::user(&prompt)];
7788 let response = self
7789 .observe_purpose(
7790 ObservationPurpose::PlanGeneration,
7791 planner_llm.complete(&messages, None),
7792 )
7793 .await
7794 .map_err(|e| AgentError::LLM(format!("Planning failed: {}", e)))?;
7795
7796 let mut plan = Plan::new(input);
7797
7798 if let Some(json_start) = response.content.find('{')
7799 && let Some(json_end) = response.content.rfind('}')
7800 {
7801 let json_str = &response.content[json_start..=json_end];
7802 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_str)
7803 && let Some(steps) = parsed.get("steps").and_then(|s| s.as_array())
7804 {
7805 for step_value in steps {
7806 let id = step_value
7807 .get("id")
7808 .and_then(|v| v.as_str())
7809 .unwrap_or("step");
7810 let desc = step_value
7811 .get("description")
7812 .and_then(|v| v.as_str())
7813 .unwrap_or("");
7814 let action_type = step_value
7815 .get("action_type")
7816 .and_then(|v| v.as_str())
7817 .unwrap_or("think");
7818 let action_target = step_value
7819 .get("action_target")
7820 .and_then(|v| v.as_str())
7821 .unwrap_or("");
7822 let args = step_value
7823 .get("args")
7824 .cloned()
7825 .unwrap_or(serde_json::json!({}));
7826 let deps: Vec<String> = step_value
7827 .get("dependencies")
7828 .and_then(|v| v.as_array())
7829 .map(|arr| {
7830 arr.iter()
7831 .filter_map(|v| v.as_str().map(String::from))
7832 .collect()
7833 })
7834 .unwrap_or_default();
7835
7836 let action = match action_type {
7837 "tool" => PlanAction::tool(action_target, args),
7838 "skill" => PlanAction::skill(action_target),
7839 "respond" => PlanAction::respond(action_target),
7840 _ => PlanAction::think(desc),
7841 };
7842
7843 let step = PlanStep::new(desc, action)
7844 .with_id(id)
7845 .with_dependencies(deps);
7846 plan.add_step(step);
7847 }
7848 }
7849 }
7850
7851 if plan.steps.is_empty() {
7852 plan.add_step(PlanStep::new(
7853 "Process the request",
7854 PlanAction::think(input),
7855 ));
7856 plan.add_step(PlanStep::new(
7857 "Provide response",
7858 PlanAction::respond("Answer based on analysis"),
7859 ));
7860 }
7861
7862 Ok(plan)
7863 }
7864
7865 async fn execute_plan(&self, plan: &mut Plan) -> Result<String> {
7866 let llm = self.get_state_llm()?;
7867 let mut results: HashMap<String, serde_json::Value> = HashMap::new();
7868 let effective = self.get_effective_reasoning_config();
7869 let max_steps = effective.get_planning().map(|c| c.max_steps).unwrap_or(10);
7870
7871 plan.status = PlanStatus::InProgress;
7872
7873 for step_idx in 0..plan.steps.len().min(max_steps as usize) {
7874 let step = &plan.steps[step_idx];
7875
7876 let deps_satisfied = step.dependencies.iter().all(|dep| {
7877 plan.steps
7878 .iter()
7879 .find(|s| &s.id == dep)
7880 .map(|s| s.status.is_completed())
7881 .unwrap_or(false)
7882 });
7883
7884 if !deps_satisfied {
7885 continue;
7886 }
7887
7888 plan.steps[step_idx].mark_running();
7889
7890 let result = match &plan.steps[step_idx].action {
7891 PlanAction::Tool { tool, args } => {
7892 let has_dep_results = plan.steps[step_idx]
7898 .dependencies
7899 .iter()
7900 .any(|dep| results.contains_key(dep));
7901
7902 let final_args = if has_dep_results {
7903 let dep_context: String = plan.steps[step_idx]
7904 .dependencies
7905 .iter()
7906 .filter_map(|dep| results.get(dep).map(|r| format!("{}: {}", dep, r)))
7907 .collect::<Vec<_>>()
7908 .join("\n");
7909
7910 let tool_schema = self
7911 .tools
7912 .get(tool)
7913 .map(|t| {
7914 let schema = t.input_schema();
7915 let props = schema
7916 .get("properties")
7917 .and_then(|p| serde_json::to_string(p).ok())
7918 .unwrap_or_else(|| "{}".to_string());
7919 format!(
7920 "{}: {}\nArguments schema: {}",
7921 t.id(),
7922 t.description(),
7923 props
7924 )
7925 })
7926 .unwrap_or_default();
7927
7928 let step_desc = &plan.steps[step_idx].description;
7929 let arg_prompt = format!(
7930 "Generate the JSON arguments for a tool call.\n\n\
7931 Tool: {}\n\n\
7932 Task: {}\n\n\
7933 Previous step results:\n{}\n\n\
7934 Planner's draft arguments: {}\n\n\
7935 Produce ONLY a valid JSON object with the correct argument values.\n\
7936 Use actual values from the previous step results, not template references.",
7937 tool_schema,
7938 step_desc,
7939 dep_context,
7940 serde_json::to_string(args).unwrap_or_default()
7941 );
7942 let messages = vec![ChatMessage::user(&arg_prompt)];
7943 match self
7944 .observe_purpose(
7945 ObservationPurpose::PlanStep,
7946 llm.complete(&messages, None),
7947 )
7948 .await
7949 {
7950 Ok(resp) => {
7951 let content = resp.content.trim();
7952 let json_start = content.find('{');
7954 let json_end = content.rfind('}');
7955 if let (Some(start), Some(end)) = (json_start, json_end) {
7956 serde_json::from_str(&content[start..=end])
7957 .unwrap_or_else(|_| args.clone())
7958 } else {
7959 args.clone()
7960 }
7961 }
7962 Err(_) => args.clone(),
7963 }
7964 } else {
7965 args.clone()
7966 };
7967
7968 let request = ToolExecutionRequest::new(
7969 uuid::Uuid::new_v4().to_string(),
7970 tool.clone(),
7971 final_args,
7972 ToolCallSource::Plan {
7973 step_index: step_idx,
7974 },
7975 );
7976 match self.execute_tool_record(request).await {
7977 Ok(record) if record.success => {
7978 serde_json::json!({ "output": record.model_output_string() })
7979 }
7980 Ok(record) => {
7981 plan.steps[step_idx].mark_failed(record.model_output_string());
7982 continue;
7983 }
7984 Err(e) => {
7985 plan.steps[step_idx].mark_failed(e.to_string());
7986 continue;
7987 }
7988 }
7989 }
7990 PlanAction::Skill { skill } => {
7991 if let Some(skill_def) = self.skills.iter().find(|s| &s.id == skill) {
7992 if let Some(ref executor) = self.skill_executor {
7993 match executor
7994 .execute_with_invoker(skill_def, "", serde_json::json!({}), self)
7995 .await
7996 {
7997 Ok(output) => serde_json::json!({ "output": output }),
7998 Err(e) => {
7999 plan.steps[step_idx].mark_failed(e.to_string());
8000 continue;
8001 }
8002 }
8003 } else {
8004 serde_json::json!({ "output": "Skill executor not available" })
8005 }
8006 } else {
8007 plan.steps[step_idx].mark_failed("Skill not found");
8008 continue;
8009 }
8010 }
8011 PlanAction::Think { prompt } => {
8012 let context: String = results
8013 .iter()
8014 .map(|(k, v)| format!("{}: {}", k, v))
8015 .collect::<Vec<_>>()
8016 .join("\n");
8017
8018 let think_prompt = format!("Context:\n{}\n\nTask: {}", context, prompt);
8019 let messages = vec![ChatMessage::user(&think_prompt)];
8020
8021 match self
8022 .observe_purpose(
8023 ObservationPurpose::PlanStep,
8024 llm.complete(&messages, None),
8025 )
8026 .await
8027 {
8028 Ok(resp) => serde_json::json!({ "output": resp.content }),
8029 Err(e) => {
8030 plan.steps[step_idx].mark_failed(e.to_string());
8031 continue;
8032 }
8033 }
8034 }
8035 PlanAction::Respond { template } => {
8036 let context: String = results
8037 .iter()
8038 .map(|(k, v)| format!("{}: {}", k, v))
8039 .collect::<Vec<_>>()
8040 .join("\n");
8041
8042 let respond_prompt = format!(
8043 "Based on this context:\n{}\n\nGenerate a response following this template/instruction: {}",
8044 context, template
8045 );
8046 let messages = vec![ChatMessage::user(&respond_prompt)];
8047
8048 match self
8049 .observe_purpose(
8050 ObservationPurpose::PlanStep,
8051 llm.complete(&messages, None),
8052 )
8053 .await
8054 {
8055 Ok(resp) => serde_json::json!({ "output": resp.content }),
8056 Err(e) => {
8057 plan.steps[step_idx].mark_failed(e.to_string());
8058 continue;
8059 }
8060 }
8061 }
8062 };
8063
8064 results.insert(plan.steps[step_idx].id.clone(), result.clone());
8065 plan.steps[step_idx].mark_completed(Some(result));
8066 }
8067
8068 let has_failures = plan.steps.iter().any(|s| s.status.is_failed());
8070 if has_failures {
8071 let failed_ids: Vec<String> = plan
8072 .steps
8073 .iter()
8074 .filter(|s| s.status.is_failed())
8075 .map(|s| s.id.clone())
8076 .collect();
8077 plan.status = PlanStatus::Failed {
8078 error: format!("Steps failed: {}", failed_ids.join(", ")),
8079 };
8080 } else {
8081 plan.status = PlanStatus::Completed;
8082 }
8083
8084 let all_outputs: Vec<String> = plan
8086 .steps
8087 .iter()
8088 .filter(|s| s.status.is_completed())
8089 .filter_map(|s| {
8090 s.result
8091 .as_ref()
8092 .and_then(|r| r.get("output"))
8093 .and_then(|o| o.as_str())
8094 .map(|o| format!("{}: {}", s.description, o))
8095 })
8096 .collect();
8097
8098 if all_outputs.is_empty() {
8099 return Ok("Plan execution completed but produced no results.".to_string());
8100 }
8101
8102 if all_outputs.len() == 1 {
8103 return Ok(all_outputs.into_iter().next().unwrap());
8104 }
8105
8106 let context = all_outputs.join("\n\n");
8108 let prompt = format!(
8109 "You completed a multi-step plan for: \"{}\"\n\nStep results:\n{}\n\nProvide a coherent final response that synthesizes these results.",
8110 plan.goal, context
8111 );
8112 let messages = vec![ChatMessage::user(&prompt)];
8113 match self
8114 .observe_purpose(ObservationPurpose::PlanStep, llm.complete(&messages, None))
8115 .await
8116 {
8117 Ok(resp) => Ok(resp.content.trim().to_string()),
8118 Err(_) => Ok(context),
8119 }
8120 }
8121
8122 async fn evaluate_response(&self, input: &str, response: &str) -> Result<EvaluationResult> {
8123 let effective_config = self.get_effective_reflection_config();
8124 self.evaluate_response_with_config(input, response, &effective_config)
8125 .await
8126 }
8127
8128 fn extract_thinking(&self, content: &str) -> (Option<String>, String) {
8129 if let Some(start) = content.find("<thinking>")
8130 && let Some(end) = content.find("</thinking>")
8131 {
8132 let thinking = content[start + 10..end].trim().to_string();
8133 let answer = content[end + 11..].trim().to_string();
8134 return (Some(thinking), answer);
8135 }
8136 (None, content.to_string())
8137 }
8138
8139 fn format_response_with_thinking(&self, thinking: Option<&str>, answer: &str) -> String {
8140 match self.get_effective_reasoning_config().output {
8141 ReasoningOutput::Hidden => answer.to_string(),
8142 ReasoningOutput::Visible => {
8143 if let Some(t) = thinking {
8144 format!("Thinking:\n{}\n\nAnswer:\n{}", t, answer)
8145 } else {
8146 answer.to_string()
8147 }
8148 }
8149 ReasoningOutput::Tagged => {
8150 if let Some(t) = thinking {
8151 format!("<thinking>{}</thinking>\n{}", t, answer)
8152 } else {
8153 answer.to_string()
8154 }
8155 }
8156 }
8157 }
8158
8159 async fn run_loop(&self, input: &str) -> Result<AgentResponse> {
8160 self.init_storage().await?;
8164 self.begin_root_turn();
8165 let _root_cleanup = RootTurnCleanup::new(self);
8166 info!(input_len = input.len(), "Starting chat");
8167
8168 self.hooks.on_message_received(input).await;
8169
8170 if !self.context_initialized.swap(true, Ordering::SeqCst) {
8174 self.context_manager.initialize().await?;
8175 debug!("Context manager initialized (defaults, env, builtins)");
8176 }
8177
8178 self.check_turn_timeout().await?;
8179 self.context_manager.refresh_per_turn().await?;
8180
8181 self.clear_disambiguation_context();
8184
8185 if let Some(ref disambiguator) = self.disambiguation_manager {
8187 let disambiguation_context = self.build_disambiguation_context().await?;
8188
8189 let state_override = self
8191 .state_machine
8192 .as_ref()
8193 .and_then(|sm| sm.current_definition())
8194 .and_then(|def| def.disambiguation.clone());
8195
8196 match self
8197 .observe_purpose(
8198 ObservationPurpose::DisambiguationDetection,
8199 disambiguator.process_input_with_override(
8200 input,
8201 &disambiguation_context,
8202 state_override.as_ref(),
8203 None,
8204 ),
8205 )
8206 .await?
8207 {
8208 DisambiguationResult::Clear => {
8209 debug!("Input is clear, proceeding normally");
8210 }
8211 DisambiguationResult::NeedsClarification {
8212 question,
8213 detection,
8214 } => {
8215 info!(
8216 ambiguity_type = ?detection.ambiguity_type,
8217 confidence = detection.confidence,
8218 "Input requires clarification"
8219 );
8220
8221 self.commit_root_user_message(input).await?;
8222 self.memory
8223 .add_message(ChatMessage::assistant(&question.question))
8224 .await?;
8225
8226 let response = AgentResponse::new(&question.question).with_metadata(
8227 "disambiguation",
8228 serde_json::json!({
8229 "status": "awaiting_clarification",
8230 "options": question.options,
8231 "clarifying": question.clarifying,
8232 "detection": {
8233 "type": detection.ambiguity_type,
8234 "confidence": detection.confidence,
8235 "what_is_unclear": detection.what_is_unclear,
8236 }
8237 }),
8238 );
8239 self.finish_turn_if_root(&response).await?;
8240 return Ok(response);
8241 }
8242 DisambiguationResult::Clarified {
8243 enriched_input,
8244 resolved,
8245 ..
8246 } => {
8247 info!(
8248 resolved_count = resolved.len(),
8249 enriched = %enriched_input,
8250 "Input clarified, injecting resolved intent into context"
8251 );
8252
8253 for (key, value) in &resolved {
8256 let context_key = format!("disambiguation.{}", key);
8257 let _ = self.context_manager.set(&context_key, value.clone());
8258 }
8259
8260 if let Some(intent) = resolved.get("intent") {
8261 let _ = self.context_manager.set("resolved_intent", intent.clone());
8262 }
8263
8264 let _ = self
8265 .context_manager
8266 .set("disambiguation.resolved", serde_json::Value::Bool(true));
8267
8268 let skill_id = self.pending_skill_id.read().clone();
8272 if let Some(skill_id) = skill_id {
8273 info!(skill_id = %skill_id, "Re-checking skill disambiguation on clarified input");
8274 return self
8275 .recheck_skill_disambiguation(&skill_id, &enriched_input)
8276 .await;
8277 }
8278
8279 return self.run_loop_internal(&enriched_input).await;
8280 }
8281 DisambiguationResult::ProceedWithBestGuess { enriched_input } => {
8282 info!("Proceeding with best guess interpretation");
8283
8284 let skill_id = self.pending_skill_id.read().clone();
8286 if let Some(skill_id) = skill_id {
8287 info!(skill_id = %skill_id, "Re-checking skill disambiguation on best-guess input");
8288 return self
8289 .recheck_skill_disambiguation(&skill_id, &enriched_input)
8290 .await;
8291 }
8292
8293 return self.run_loop_internal(&enriched_input).await;
8294 }
8295 DisambiguationResult::GiveUp { reason } => {
8296 *self.pending_skill_id.write() = None;
8297 warn!(reason = %reason, "Disambiguation gave up");
8298 let apology = self
8299 .generate_localized_apology(
8300 "Generate a brief, polite apology saying you couldn't understand the request. Be concise.",
8301 &reason,
8302 )
8303 .await
8304 .unwrap_or_else(|_| {
8305 format!("I'm sorry, I couldn't understand your request: {}", reason)
8306 });
8307 let response = AgentResponse::new(&apology);
8308 self.finish_turn_if_root(&response).await?;
8309 return Ok(response);
8310 }
8311 DisambiguationResult::Escalate { reason } => {
8312 *self.pending_skill_id.write() = None;
8313 info!(reason = %reason, "Escalating to human");
8314 if let Some(ref hitl) = self.hitl_engine {
8315 let trigger =
8316 ApprovalTrigger::condition("disambiguation_escalation", reason.clone());
8317 let mut context_map = HashMap::new();
8318 context_map.insert("original_input".to_string(), serde_json::json!(input));
8319 context_map.insert("reason".to_string(), serde_json::json!(&reason));
8320 let check_result = HITLCheckResult::required(
8321 trigger,
8322 context_map,
8323 format!("User request needs human assistance: {}", reason),
8324 Some(hitl.config().default_timeout_seconds),
8325 );
8326 let result = self.request_hitl_approval(check_result).await?;
8327 if matches!(
8328 result,
8329 ApprovalResult::Approved | ApprovalResult::Modified { .. }
8330 ) {
8331 return self.run_loop_internal(input).await;
8332 }
8333 }
8334 let apology = self
8335 .generate_localized_apology(
8336 "Explain briefly that you're transferring the user to a human agent for help.",
8337 &reason,
8338 )
8339 .await
8340 .unwrap_or_else(|_| {
8341 format!("I need human assistance to help with your request: {}", reason)
8342 });
8343 let response = AgentResponse::new(&apology);
8344 self.finish_turn_if_root(&response).await?;
8345 return Ok(response);
8346 }
8347 DisambiguationResult::Abandoned { new_input } => {
8348 *self.pending_skill_id.write() = None;
8349
8350 info!(
8351 has_new_input = new_input.is_some(),
8352 "Clarification abandoned by user"
8353 );
8354
8355 self.commit_root_user_message(input).await?;
8356
8357 match new_input {
8358 Some(fresh_input) => {
8359 return self.run_loop_internal(&fresh_input).await;
8362 }
8363 None => {
8364 let ack = self
8366 .generate_localized_apology(
8367 "The user changed their mind about their previous request. \
8368 Generate a brief, friendly acknowledgment (e.g. 'OK, no problem. What else can I help with?'). \
8369 Do NOT apologize excessively. Be concise.",
8370 "User abandoned clarification",
8371 )
8372 .await
8373 .unwrap_or_else(|_| {
8374 "OK, no problem. What else can I help with?".to_string()
8375 });
8376
8377 self.memory
8378 .add_message(ChatMessage::assistant(&ack))
8379 .await?;
8380
8381 let response = AgentResponse::new(&ack);
8382 self.finish_turn_if_root(&response).await?;
8383 return Ok(response);
8384 }
8385 }
8386 }
8387 }
8388 }
8389
8390 self.run_loop_internal(input).await
8391 }
8392
8393 async fn generate_localized_apology(&self, instruction: &str, reason: &str) -> Result<String> {
8395 let llm = self.llm_registry.router().map_err(|e| {
8396 AgentError::LLM(format!(
8397 "Router LLM not available for localized response: {}",
8398 e
8399 ))
8400 })?;
8401
8402 let recent: Vec<String> = self
8403 .memory
8404 .get_messages(Some(3))
8405 .await?
8406 .iter()
8407 .map(|m| m.content.clone())
8408 .collect();
8409
8410 let context_hint = if recent.is_empty() {
8411 String::new()
8412 } else {
8413 format!(
8414 "\nRecent conversation (detect the user's language from this):\n{}\n",
8415 recent.join("\n")
8416 )
8417 };
8418
8419 let prompt = format!(
8420 "{}\nReason: {}\n{}Respond in the same language as the user. Output ONLY the message, nothing else.",
8421 instruction, reason, context_hint
8422 );
8423
8424 let messages = vec![ChatMessage::user(&prompt)];
8425 let response = self
8426 .observe_purpose(
8427 ObservationPurpose::DisambiguationClarification,
8428 llm.complete(&messages, None),
8429 )
8430 .await
8431 .map_err(|e| AgentError::LLM(format!("Localized response generation failed: {}", e)))?;
8432
8433 Ok(response.content.trim().to_string())
8434 }
8435
8436 fn render_action_args(&self, args: &Value) -> Value {
8440 let context = self.build_context_with_overlays();
8441 match args {
8442 Value::Object(map) => {
8443 let mut rendered = serde_json::Map::new();
8444 for (k, v) in map {
8445 match v {
8446 Value::String(s) if s.contains("{{") => {
8447 match self.template_renderer.render(s, &context) {
8448 Ok(rendered_str) => {
8449 rendered.insert(k.clone(), Value::String(rendered_str));
8450 }
8451 Err(_) => {
8452 rendered.insert(k.clone(), v.clone());
8453 }
8454 }
8455 }
8456 _ => {
8457 rendered.insert(k.clone(), v.clone());
8458 }
8459 }
8460 }
8461 Value::Object(rendered)
8462 }
8463 _ => args.clone(),
8464 }
8465 }
8466
8467 fn clear_disambiguation_context(&self) {
8469 let _ = self
8470 .context_manager
8471 .set("resolved_intent", serde_json::Value::Null);
8472
8473 let all = self.context_manager.get_all();
8474 for key in all.keys() {
8475 if key.starts_with("disambiguation.") {
8476 let _ = self.context_manager.set(key, serde_json::Value::Null);
8477 }
8478 }
8479 }
8480
8481 async fn recheck_skill_disambiguation(
8487 &self,
8488 skill_id: &str,
8489 enriched_input: &str,
8490 ) -> Result<AgentResponse> {
8491 let skill = self
8492 .skill_router
8493 .as_ref()
8494 .and_then(|r| r.get_skill(skill_id).cloned());
8495
8496 if let Some(ref skill) = skill
8498 && let Some(ref skill_disambig) = skill.disambiguation
8499 && skill_disambig.enabled.unwrap_or(false)
8500 && let Some(ref disambiguator) = self.disambiguation_manager
8501 {
8502 let context = self.build_disambiguation_context().await?;
8503 let state_override = self
8504 .state_machine
8505 .as_ref()
8506 .and_then(|sm| sm.current_definition())
8507 .and_then(|def| def.disambiguation.clone());
8508
8509 match self
8510 .observe_purpose(
8511 ObservationPurpose::DisambiguationDetection,
8512 disambiguator.process_input_with_override(
8513 enriched_input,
8514 &context,
8515 state_override.as_ref(),
8516 Some(skill_disambig),
8517 ),
8518 )
8519 .await?
8520 {
8521 DisambiguationResult::Clear => {
8522 debug!(skill_id = %skill_id, "Skill re-check: all fields present");
8523 }
8524 DisambiguationResult::NeedsClarification {
8525 question,
8526 detection,
8527 } => {
8528 info!(
8529 skill_id = %skill_id,
8530 ambiguity_type = ?detection.ambiguity_type,
8531 what_is_unclear = ?detection.what_is_unclear,
8532 "Skill re-check: still missing fields, asking again"
8533 );
8534 self.memory
8538 .add_message(ChatMessage::user(enriched_input))
8539 .await?;
8540 self.memory
8541 .add_message(ChatMessage::assistant(&question.question))
8542 .await?;
8543
8544 let response = AgentResponse::new(&question.question).with_metadata(
8545 "disambiguation",
8546 serde_json::json!({
8547 "status": "awaiting_clarification",
8548 "skill_id": skill_id,
8549 "options": question.options,
8550 "clarifying": question.clarifying,
8551 "detection": {
8552 "type": detection.ambiguity_type,
8553 "confidence": detection.confidence,
8554 "what_is_unclear": detection.what_is_unclear,
8555 }
8556 }),
8557 );
8558 self.finish_turn_if_root(&response).await?;
8559 return Ok(response);
8560 }
8561 DisambiguationResult::Clarified {
8562 enriched_input: re_enriched,
8563 ..
8564 } => {
8565 debug!(skill_id = %skill_id, "Skill re-check: clarified immediately, executing");
8566 *self.pending_skill_id.write() = None;
8568 let skill_response = self.execute_skill_by_id(skill_id, &re_enriched).await?;
8569 self.memory
8570 .add_message(ChatMessage::user(&re_enriched))
8571 .await?;
8572 return self
8573 .handle_skill_response(
8574 &re_enriched,
8575 skill_id,
8576 skill_response,
8577 &HashMap::new(),
8578 )
8579 .await;
8580 }
8581 DisambiguationResult::ProceedWithBestGuess {
8582 enriched_input: re_enriched,
8583 } => {
8584 debug!(skill_id = %skill_id, "Skill re-check: proceeding with best guess");
8585 *self.pending_skill_id.write() = None;
8586 let skill_response = self.execute_skill_by_id(skill_id, &re_enriched).await?;
8587 self.memory
8588 .add_message(ChatMessage::user(&re_enriched))
8589 .await?;
8590 return self
8591 .handle_skill_response(
8592 &re_enriched,
8593 skill_id,
8594 skill_response,
8595 &HashMap::new(),
8596 )
8597 .await;
8598 }
8599 DisambiguationResult::GiveUp { reason } => {
8600 *self.pending_skill_id.write() = None;
8601 let apology = self
8602 .generate_localized_apology(
8603 "Generate a brief, polite apology saying you couldn't understand the request. Be concise.",
8604 &reason,
8605 )
8606 .await
8607 .unwrap_or_else(|_| {
8608 format!("I'm sorry, I couldn't understand your request: {}", reason)
8609 });
8610 let response = AgentResponse::new(&apology);
8611 self.finish_turn_if_root(&response).await?;
8612 return Ok(response);
8613 }
8614 DisambiguationResult::Escalate { reason } => {
8615 *self.pending_skill_id.write() = None;
8616 let apology = self
8617 .generate_localized_apology(
8618 "Explain briefly that you're transferring the user to a human agent for help.",
8619 &reason,
8620 )
8621 .await
8622 .unwrap_or_else(|_| {
8623 format!("I need human assistance to help with your request: {}", reason)
8624 });
8625 let response = AgentResponse::new(&apology);
8626 self.finish_turn_if_root(&response).await?;
8627 return Ok(response);
8628 }
8629 DisambiguationResult::Abandoned { new_input } => {
8630 *self.pending_skill_id.write() = None;
8633 debug!(skill_id = %skill_id, "Skill re-check: abandoned by user");
8634 if let Some(fresh) = new_input {
8635 return self.run_loop_internal(&fresh).await;
8636 }
8637 let ack = self
8638 .generate_localized_apology(
8639 "The user changed their mind about their previous request. \
8640 Generate a brief, friendly acknowledgment (e.g. 'OK, no problem. What else can I help with?'). \
8641 Do NOT apologize excessively. Be concise.",
8642 "User abandoned clarification",
8643 )
8644 .await
8645 .unwrap_or_else(|_| {
8646 "OK, no problem. What else can I help with?".to_string()
8647 });
8648 self.memory
8649 .add_message(ChatMessage::assistant(&ack))
8650 .await?;
8651 let response = AgentResponse::new(&ack);
8652 self.finish_turn_if_root(&response).await?;
8653 return Ok(response);
8654 }
8655 }
8656 }
8657
8658 *self.pending_skill_id.write() = None;
8660 let skill_response = self.execute_skill_by_id(skill_id, enriched_input).await?;
8661 self.memory
8662 .add_message(ChatMessage::user(enriched_input))
8663 .await?;
8664 self.handle_skill_response(enriched_input, skill_id, skill_response, &HashMap::new())
8665 .await
8666 }
8667
8668 async fn handle_skill_response(
8671 &self,
8672 processed_input: &str,
8673 skill_id: &str,
8674 skill_response: String,
8675 input_context: &HashMap<String, Value>,
8676 ) -> Result<AgentResponse> {
8677 let output_data = self.process_output(&skill_response, input_context).await?;
8678 let final_response = output_data.content;
8679
8680 self.memory
8681 .add_message(ChatMessage::assistant(&final_response))
8682 .await?;
8683
8684 self.check_memory_compression().await?;
8685
8686 self.increment_turn();
8687 self.evaluate_transitions(processed_input, &final_response)
8688 .await?;
8689
8690 let response = AgentResponse::new(final_response)
8691 .with_metadata("skill_id", serde_json::json!(skill_id));
8692 self.finish_turn_if_root(&response).await?;
8693 Ok(response)
8694 }
8695
8696 async fn handle_plan_and_execute(
8699 &self,
8700 processed_input: &str,
8701 input_context: &HashMap<String, Value>,
8702 auto_detected: bool,
8703 ) -> Result<AgentResponse> {
8704 let effective = self.get_effective_reasoning_config();
8705 let plan_reflection = effective
8706 .get_planning()
8707 .map(|c| c.reflection.clone())
8708 .unwrap_or_default();
8709
8710 let max_attempts = if plan_reflection.enabled {
8711 1 + plan_reflection.max_replans
8712 } else {
8713 1
8714 };
8715
8716 let mut plan = self.generate_plan(processed_input).await?;
8717 info!(
8718 plan_id = %plan.id,
8719 steps = plan.steps.len(),
8720 "Plan generated"
8721 );
8722
8723 let mut plan_result = String::new();
8724
8725 for attempt in 0..max_attempts {
8726 *self.current_plan.write() = Some(plan.clone());
8727 plan_result = self.execute_plan(&mut plan).await?;
8728
8729 info!(
8730 plan_status = ?plan.status,
8731 completed_steps = plan.completed_steps().count(),
8732 attempt = attempt + 1,
8733 "Plan execution completed"
8734 );
8735
8736 if !plan_reflection.enabled {
8737 break;
8738 }
8739
8740 let has_failures = plan.steps.iter().any(|s| s.status.is_failed());
8741 if !has_failures {
8742 break;
8743 }
8744
8745 if attempt + 1 >= max_attempts {
8746 break;
8747 }
8748
8749 match plan_reflection.on_step_failure {
8750 StepFailureAction::Replan => {
8751 info!(attempt = attempt + 1, "Plan had failures, replanning");
8752 plan = self.generate_plan(processed_input).await?;
8753 }
8754 StepFailureAction::Abort => {
8755 warn!("Plan step failed, aborting");
8756 break;
8757 }
8758 StepFailureAction::Skip | StepFailureAction::Continue => {
8759 break;
8760 }
8761 }
8762 }
8763
8764 *self.current_plan.write() = Some(plan);
8765
8766 let output_data = self.process_output(&plan_result, input_context).await?;
8767 let final_content = output_data.content;
8768
8769 self.memory
8770 .add_message(ChatMessage::assistant(&final_content))
8771 .await?;
8772
8773 self.check_memory_compression().await?;
8774 self.increment_turn();
8775 self.evaluate_transitions(processed_input, &final_content)
8776 .await?;
8777
8778 let reasoning_metadata =
8779 ReasoningMetadata::new(ReasoningMode::PlanAndExecute).with_auto_detected(auto_detected);
8780
8781 let response = AgentResponse::new(&final_content).with_metadata(
8782 "reasoning",
8783 serde_json::to_value(&reasoning_metadata).unwrap_or_default(),
8784 );
8785
8786 self.finish_turn_if_root(&response).await?;
8787 Ok(response)
8788 }
8789
8790 fn inject_reasoning_prompt(
8792 &self,
8793 messages: &mut [ChatMessage],
8794 reasoning_mode: &ReasoningMode,
8795 is_first_iteration: bool,
8796 ) {
8797 if !is_first_iteration {
8798 return;
8799 }
8800 match reasoning_mode {
8801 ReasoningMode::CoT => {
8802 if let Some(msg) = messages.first_mut()
8803 && matches!(msg.role, ai_agents_core::Role::System)
8804 {
8805 msg.content = self.build_cot_system_prompt(&msg.content);
8806 debug!("Applied Chain-of-Thought system prompt");
8807 }
8808 }
8809 ReasoningMode::React => {
8810 if let Some(msg) = messages.first_mut()
8811 && matches!(msg.role, ai_agents_core::Role::System)
8812 {
8813 msg.content = self.build_react_system_prompt(&msg.content);
8814 debug!("Applied ReAct system prompt");
8815 }
8816 }
8817 _ => {}
8818 }
8819 }
8820
8821 async fn generate_main_response_draft(
8826 &self,
8827 processed_input: &str,
8828 reasoning_mode: &ReasoningMode,
8829 ) -> Result<MainResponseDraft> {
8830 let llm = self.get_state_llm()?;
8831 let protocol = self.main_tool_protocol(llm.as_ref(), true).await?;
8832 let mut messages = self
8833 .build_messages_internal(false, Some(processed_input), protocol.choice.is_none())
8834 .await?;
8835 self.inject_reasoning_prompt(&mut messages, reasoning_mode, true);
8836 let response = self
8837 .complete_main_llm_with_recovery(llm, &messages, &protocol)
8838 .await?;
8839 let content = response.content.trim().to_string();
8840 let (thinking, answer) = self.extract_thinking(&content);
8841 if let Some(calls) = self.parse_main_tool_calls(&content, &protocol) {
8842 return Ok(MainResponseDraft::ToolCalls {
8843 raw_content: content,
8844 calls,
8845 thinking,
8846 });
8847 }
8848 Ok(MainResponseDraft::Text {
8849 raw_content: answer,
8850 thinking,
8851 })
8852 }
8853
8854 async fn commit_main_response_draft(
8859 &self,
8860 processed_input: &str,
8861 input_context: &HashMap<String, Value>,
8862 draft: MainResponseDraft,
8863 reasoning_mode: ReasoningMode,
8864 auto_detected: bool,
8865 ) -> Result<AgentResponse> {
8866 self.commit_root_user_message(processed_input).await?;
8867 match draft {
8868 MainResponseDraft::Text {
8869 raw_content,
8870 thinking,
8871 } => {
8872 self.finish_text_response_from_model(CommittedTextResponse {
8873 processed_input,
8874 input_context,
8875 answer: raw_content,
8876 reasoning_mode,
8877 auto_detected,
8878 iterations: 1,
8879 thinking_content: thinking,
8880 all_tool_calls: Vec::new(),
8881 })
8882 .await
8883 }
8884 MainResponseDraft::ToolCalls {
8885 raw_content,
8886 calls,
8887 thinking: _,
8888 } => {
8889 let mut all_tool_calls = Vec::new();
8890 match self
8891 .handle_tool_calls(processed_input, &raw_content, calls, &mut all_tool_calls)
8892 .await?
8893 {
8894 ToolCallOutcome::Rejected(response) => {
8895 self.finish_turn_if_root(&response).await?;
8896 Ok(response)
8897 }
8898 ToolCallOutcome::Continue | ToolCallOutcome::TransitionFired => {
8899 self.continue_after_committed_tool_draft(processed_input)
8900 .await
8901 }
8902 }
8903 }
8904 }
8905 }
8906
8907 async fn continue_after_committed_tool_draft(
8912 &self,
8913 processed_input: &str,
8914 ) -> Result<AgentResponse> {
8915 *self.redispatch_depth.write() += 1;
8916 if let Some(context) = self.active_turn_context.write().as_mut() {
8917 context.enter_redispatch();
8918 }
8919 let result = Box::pin(self.run_loop_internal(processed_input)).await;
8920 *self.redispatch_depth.write() -= 1;
8921 if let Some(context) = self.active_turn_context.write().as_mut() {
8922 context.exit_redispatch();
8923 }
8924 let response = result?;
8925 self.finish_turn_if_root(&response).await?;
8926 Ok(response)
8927 }
8928
8929 async fn finish_text_response_from_model(
8934 &self,
8935 response: CommittedTextResponse<'_>,
8936 ) -> Result<AgentResponse> {
8937 let CommittedTextResponse {
8938 processed_input,
8939 input_context,
8940 answer,
8941 reasoning_mode,
8942 auto_detected,
8943 iterations,
8944 thinking_content,
8945 all_tool_calls,
8946 } = response;
8947 let output_data = self.process_output(&answer, input_context).await?;
8948 let mut final_content = if output_data.metadata.rejected {
8949 output_data
8950 .metadata
8951 .rejection_reason
8952 .unwrap_or_else(|| answer.to_string())
8953 } else {
8954 output_data.content
8955 };
8956 let llm = self.get_state_llm()?;
8957 let reflection_metadata;
8958 (final_content, reflection_metadata) = self
8959 .run_reflection(&*llm, processed_input, final_content)
8960 .await?;
8961 final_content =
8962 self.format_response_with_thinking(thinking_content.as_deref(), &final_content);
8963 let final_content = {
8964 let result = self
8965 .post_loop_processing(processed_input, final_content)
8966 .await?;
8967 self.apply_post_loop_result(processed_input, result).await?
8968 };
8969 let response = self.build_agent_response(AgentResponseParts {
8970 content: final_content,
8971 all_tool_calls,
8972 reasoning_mode,
8973 auto_detected,
8974 iterations,
8975 thinking: thinking_content,
8976 reflection_metadata,
8977 });
8978 self.finish_turn_if_root(&response).await?;
8979 Ok(response)
8980 }
8981
8982 async fn run_committed_response_loop_with_reasoning(
8987 &self,
8988 processed_input: &str,
8989 input_context: &HashMap<String, Value>,
8990 reasoning_mode: ReasoningMode,
8991 auto_detected: bool,
8992 ) -> Result<AgentResponse> {
8993 self.commit_root_user_message(processed_input).await?;
8994 let llm = self.get_state_llm()?;
8995 let mut iterations = 0u32;
8996 let mut all_tool_calls = Vec::new();
8997 let mut thinking_content = None;
8998 loop {
8999 let effective_max = if reasoning_mode != ReasoningMode::None {
9000 let rc = self.get_effective_reasoning_config();
9001 self.max_iterations.min(rc.max_iterations)
9002 } else {
9003 self.max_iterations
9004 };
9005 if iterations >= effective_max {
9006 return Err(AgentError::Other(format!(
9007 "Max iterations ({}) exceeded",
9008 effective_max
9009 )));
9010 }
9011 iterations += 1;
9012 *self.iteration_count.write() = iterations;
9013 let protocol = self.main_tool_protocol(llm.as_ref(), false).await?;
9014 let mut messages = self
9015 .build_messages_internal(true, None, protocol.choice.is_none())
9016 .await?;
9017 self.inject_reasoning_prompt(&mut messages, &reasoning_mode, iterations == 1);
9018 self.hooks.on_llm_start(&messages).await;
9019 let llm_start = Instant::now();
9020 let response = self
9021 .complete_main_llm_with_recovery(Arc::clone(&llm), &messages, &protocol)
9022 .await?;
9023 let llm_duration_ms = llm_start.elapsed().as_millis() as u64;
9024 self.hooks.on_llm_complete(&response, llm_duration_ms).await;
9025 let content = response.content.trim();
9026 if let Some(tool_calls) = self.parse_main_tool_calls(content, &protocol) {
9027 match self
9028 .handle_tool_calls(processed_input, content, tool_calls, &mut all_tool_calls)
9029 .await?
9030 {
9031 ToolCallOutcome::Continue | ToolCallOutcome::TransitionFired => continue,
9032 ToolCallOutcome::Rejected(resp) => {
9033 self.finish_turn_if_root(&resp).await?;
9034 return Ok(resp);
9035 }
9036 }
9037 }
9038 let (extracted_thinking, answer) = self.extract_thinking(content);
9039 if extracted_thinking.is_some() {
9040 thinking_content = extracted_thinking;
9041 }
9042 return self
9043 .finish_text_response_from_model(CommittedTextResponse {
9044 processed_input,
9045 input_context,
9046 answer,
9047 reasoning_mode,
9048 auto_detected,
9049 iterations,
9050 thinking_content,
9051 all_tool_calls,
9052 })
9053 .await;
9054 }
9055 }
9056
9057 async fn handle_tool_calls(
9059 &self,
9060 processed_input: &str,
9061 content: &str,
9062 tool_calls: Vec<ToolCall>,
9063 all_tool_calls: &mut Vec<ToolCall>,
9064 ) -> Result<ToolCallOutcome> {
9065 let transition_fired = self.evaluate_transitions(processed_input, content).await?;
9069 if transition_fired {
9070 self.memory
9071 .add_message(ChatMessage::assistant(
9072 "(Transitioned to new state — tool call handled by workflow)",
9073 ))
9074 .await?;
9075 return Ok(ToolCallOutcome::TransitionFired);
9076 }
9077
9078 self.memory
9080 .add_message(ChatMessage::assistant(content))
9081 .await?;
9082 let native_tool_call = Self::is_native_tool_call_content(content);
9083
9084 let results = self.execute_tools_parallel(&tool_calls).await;
9085
9086 for ((_id, result), tool_call) in results.into_iter().zip(tool_calls.iter()) {
9087 match result {
9088 Ok(output) => {
9089 self.memory
9090 .add_message(Self::tool_result_message(
9091 tool_call,
9092 &output,
9093 native_tool_call,
9094 ))
9095 .await?;
9096 }
9097 Err(e) => {
9098 if matches!(e, AgentError::HITLRejected(_)) {
9100 self.memory
9101 .add_message(ChatMessage::assistant(format!(
9102 "The operation was rejected by the approver: {}",
9103 e
9104 )))
9105 .await?;
9106 return Ok(ToolCallOutcome::Rejected(AgentResponse {
9108 content: format!("Operation cancelled: {}", e),
9109 metadata: None,
9110 tool_calls: Some(all_tool_calls.clone()),
9111 }));
9112 }
9113 self.memory
9114 .add_message(Self::tool_result_message(
9115 tool_call,
9116 &format!("Error: {}", e),
9117 native_tool_call,
9118 ))
9119 .await?;
9120 }
9121 }
9122 all_tool_calls.push(tool_call.clone());
9123 }
9124 Ok(ToolCallOutcome::Continue)
9125 }
9126
9127 async fn run_reflection(
9129 &self,
9130 llm: &dyn LLMProvider,
9131 processed_input: &str,
9132 mut content: String,
9133 ) -> Result<(String, Option<ReflectionMetadata>)> {
9134 let should_reflect = self.should_reflect(processed_input, &content).await?;
9135 if !should_reflect {
9136 return Ok((content, None));
9137 }
9138
9139 info!("Starting response reflection evaluation");
9140 let mut attempts = 0u32;
9141 let max_retries = self.reflection_config.max_retries;
9142 let mut history: Vec<ReflectionAttempt> = Vec::new();
9143
9144 loop {
9145 let evaluation = self.evaluate_response(processed_input, &content).await?;
9146
9147 if evaluation.passed || attempts >= max_retries {
9148 info!(
9149 passed = evaluation.passed,
9150 confidence = evaluation.confidence,
9151 attempts = attempts + 1,
9152 "Reflection evaluation complete"
9153 );
9154 let reflection_metadata = Some(
9155 ReflectionMetadata::new(evaluation)
9156 .with_attempts(attempts + 1)
9157 .with_history(history),
9158 );
9159 return Ok((content, reflection_metadata));
9160 }
9161
9162 debug!(
9163 attempt = attempts + 1,
9164 failed_criteria = evaluation.failed_criteria().count(),
9165 "Response did not meet criteria, retrying"
9166 );
9167
9168 history.push(
9169 ReflectionAttempt::new(&content, evaluation.clone())
9170 .with_feedback("Response did not meet quality criteria"),
9171 );
9172
9173 let feedback: Vec<String> = evaluation
9174 .failed_criteria()
9175 .map(|c| format!("- {}", c.criterion))
9176 .collect();
9177
9178 let retry_prompt = format!(
9179 "Your previous response did not meet these criteria:\n{}\n\nPlease provide an improved response.",
9180 feedback.join("\n")
9181 );
9182
9183 self.memory
9184 .add_message(ChatMessage::user(&retry_prompt))
9185 .await?;
9186
9187 let retry_messages = self.build_messages().await?;
9188 let retry_response = self
9189 .observe_purpose(
9190 ObservationPurpose::ReflectionEvaluation,
9191 llm.complete(&retry_messages, None),
9192 )
9193 .await
9194 .map_err(|e| AgentError::LLM(e.to_string()))?;
9195
9196 content = retry_response.content.trim().to_string();
9197 attempts += 1;
9198 }
9199 }
9200
9201 async fn post_loop_processing(
9204 &self,
9205 processed_input: &str,
9206 content: String,
9207 ) -> Result<PostLoopResult> {
9208 self.increment_turn();
9213
9214 self.run_context_extractors(processed_input).await;
9216
9217 let transitioned = self.evaluate_transitions(processed_input, &content).await?;
9218
9219 if !transitioned {
9220 self.memory
9221 .add_message(ChatMessage::assistant(&content))
9222 .await?;
9223 self.check_memory_compression().await?;
9224 return Ok(PostLoopResult::NoTransition(content));
9225 }
9226
9227 if !self.should_regenerate_after_transition() {
9229 self.memory
9230 .add_message(ChatMessage::assistant(&content))
9231 .await?;
9232 self.check_memory_compression().await?;
9233 return Ok(PostLoopResult::Transitioned(content));
9234 }
9235
9236 if self.needs_redispatch_for_new_state() {
9240 info!("Post-transition NeedsRedispatch: new state requires full dispatch");
9241 return Ok(PostLoopResult::NeedsRedispatch);
9244 }
9245
9246 self.memory
9249 .add_message(ChatMessage::assistant(&content))
9250 .await?;
9251 self.check_memory_compression().await?;
9252
9253 let new_llm = self.get_state_llm()?;
9259 let mut final_content;
9260
9261 for post_iter in 0..self.max_iterations {
9262 let protocol = self.main_tool_protocol(new_llm.as_ref(), false).await?;
9263 let new_messages = self
9264 .build_messages_internal(true, None, protocol.choice.is_none())
9265 .await?;
9266 if post_iter == 0
9267 && let Some(system_msg) = new_messages.first()
9268 && system_msg.role == ai_agents_core::Role::System
9269 {
9270 debug!(
9271 prompt_preview =
9272 &system_msg.content[system_msg.content.len().saturating_sub(200)..],
9273 "Post-transition system prompt (last 200 chars)"
9274 );
9275 }
9276
9277 let new_response = self
9278 .complete_main_llm_with_recovery(Arc::clone(&new_llm), &new_messages, &protocol)
9279 .await?;
9280 final_content = new_response.content.trim().to_string();
9281
9282 if let Some(tool_calls) = self.parse_main_tool_calls(&final_content, &protocol) {
9285 let native_tool_call = Self::is_native_tool_call_content(&final_content);
9286 debug!(
9287 post_iter = post_iter,
9288 tools = tool_calls.len(),
9289 "Post-transition tool call detected, executing"
9290 );
9291
9292 self.memory
9293 .add_message(ChatMessage::assistant(&final_content))
9294 .await?;
9295
9296 let results = self.execute_tools_parallel(&tool_calls).await;
9297 for ((_id, result), tool_call) in results.into_iter().zip(tool_calls.iter()) {
9298 match result {
9299 Ok(output) => {
9300 self.memory
9301 .add_message(Self::tool_result_message(
9302 tool_call,
9303 &output,
9304 native_tool_call,
9305 ))
9306 .await?;
9307 }
9308 Err(e) => {
9309 self.memory
9310 .add_message(Self::tool_result_message(
9311 tool_call,
9312 &format!("Error: {}", e),
9313 native_tool_call,
9314 ))
9315 .await?;
9316 }
9317 }
9318 }
9319 continue;
9321 }
9322
9323 self.memory
9325 .add_message(ChatMessage::assistant(&final_content))
9326 .await?;
9327 return Ok(PostLoopResult::Transitioned(final_content));
9328 }
9329
9330 final_content = "Post-transition processing completed.".to_string();
9332 self.memory
9333 .add_message(ChatMessage::assistant(&final_content))
9334 .await?;
9335
9336 Ok(PostLoopResult::Transitioned(final_content))
9337 }
9338
9339 fn should_regenerate_after_transition(&self) -> bool {
9342 if let Some(ref sm) = self.state_machine {
9343 if !sm.config().regenerate_on_transition {
9345 return false;
9346 }
9347 if let Some(def) = sm.current_definition()
9349 && let Some(regen) = def.regenerate_on_enter
9350 {
9351 return regen;
9352 }
9353 }
9354 true
9355 }
9356
9357 fn needs_redispatch_for_new_state(&self) -> bool {
9360 if let Some(ref sm) = self.state_machine
9361 && let Some(def) = sm.current_definition()
9362 {
9363 if def.concurrent.is_some()
9364 || def.group_chat.is_some()
9365 || def.pipeline.is_some()
9366 || def.handoff.is_some()
9367 || def.delegate.is_some()
9368 {
9369 return true;
9370 }
9371 let effective = self.get_effective_reasoning_config();
9373 if !matches!(effective.mode, ReasoningMode::None) {
9374 return true;
9375 }
9376 }
9377 false
9378 }
9379
9380 async fn apply_post_loop_result(
9383 &self,
9384 processed_input: &str,
9385 result: PostLoopResult,
9386 ) -> Result<String> {
9387 match result {
9388 PostLoopResult::NoTransition(content) | PostLoopResult::Transitioned(content) => {
9389 Ok(content)
9390 }
9391 PostLoopResult::NeedsRedispatch => {
9392 const MAX_REDISPATCH_DEPTH: u32 = 3;
9393 let current_depth = *self.redispatch_depth.read();
9394 if current_depth >= MAX_REDISPATCH_DEPTH {
9395 warn!(
9396 depth = current_depth,
9397 "Post-transition re-dispatch depth limit reached, returning empty response"
9398 );
9399 let content = String::new();
9400 self.memory
9401 .add_message(ChatMessage::assistant(&content))
9402 .await?;
9403 return Ok(content);
9404 }
9405 *self.redispatch_depth.write() += 1;
9406 if let Some(context) = self.active_turn_context.write().as_mut() {
9407 context.enter_redispatch();
9408 }
9409 info!(
9410 depth = current_depth + 1,
9411 "Re-dispatching for new state after transition"
9412 );
9413 let resp = Box::pin(self.run_loop_internal(processed_input)).await;
9414 *self.redispatch_depth.write() -= 1;
9415 if let Some(context) = self.active_turn_context.write().as_mut() {
9416 context.exit_redispatch();
9417 }
9418 resp.map(|r| r.content)
9419 }
9420 }
9421 }
9422
9423 fn build_agent_response(&self, parts: AgentResponseParts) -> AgentResponse {
9425 let AgentResponseParts {
9426 content,
9427 all_tool_calls,
9428 reasoning_mode,
9429 auto_detected,
9430 iterations,
9431 thinking,
9432 reflection_metadata,
9433 } = parts;
9434 let reasoning_metadata = ReasoningMetadata::new(reasoning_mode.clone())
9435 .with_thinking(thinking.clone().unwrap_or_default())
9436 .with_iterations(iterations)
9437 .with_auto_detected(auto_detected);
9438
9439 let mut response = AgentResponse::new(&content);
9440 if !all_tool_calls.is_empty() {
9441 response = response.with_tool_calls(all_tool_calls);
9442 }
9443
9444 if let Some(state) = self.current_state() {
9445 response = response.with_metadata("current_state", serde_json::json!(state));
9446 }
9447
9448 response = response.with_metadata(
9449 "reasoning",
9450 serde_json::to_value(&reasoning_metadata).unwrap_or_default(),
9451 );
9452
9453 if let Some(ref refl_meta) = reflection_metadata {
9454 response = response.with_metadata(
9455 "reflection",
9456 serde_json::to_value(refl_meta).unwrap_or_default(),
9457 );
9458 }
9459
9460 response
9461 }
9462
9463 async fn handle_delegated_state(
9465 &self,
9466 input: &str,
9467 delegate_id: &str,
9468 state_def: &ai_agents_state::StateDefinition,
9469 ) -> Result<AgentResponse> {
9470 use std::time::Instant;
9471
9472 let registry = self.spawner_registry.as_ref().ok_or_else(|| {
9473 AgentError::Config(format!(
9474 "State delegates to '{}' but no agent registry is configured. \
9475 Add a spawner section with auto_spawn to your YAML.",
9476 delegate_id
9477 ))
9478 })?;
9479
9480 let state_name = self
9481 .state_machine
9482 .as_ref()
9483 .map(|sm| sm.current())
9484 .unwrap_or_else(|| "unknown".to_string());
9485
9486 self.hooks.on_delegate_start(delegate_id, &state_name).await;
9487 let start = Instant::now();
9488
9489 let delegate = registry.get(delegate_id).ok_or_else(|| {
9490 AgentError::Other(format!(
9491 "State '{}' delegates to '{}' but no agent with that ID exists in the registry.",
9492 state_name, delegate_id
9493 ))
9494 })?;
9495
9496 let context_mode = state_def.delegate_context.clone().unwrap_or_default();
9498 let effective_input = self
9499 .observe_purpose(
9500 ObservationPurpose::OrchestrationRouting,
9501 crate::orchestration::context::prepare_delegate_input(
9502 input,
9503 &context_mode,
9504 &*self.memory,
9505 self.llm_registry.get("router").ok().as_deref(),
9506 ),
9507 )
9508 .await?;
9509
9510 let response = delegate
9511 .chat_with_actor_context(&effective_input, self.outbound_actor_context())
9512 .await?;
9513
9514 let duration_ms = start.elapsed().as_millis() as u64;
9515 self.hooks
9516 .on_delegate_complete(delegate_id, &state_name, duration_ms)
9517 .await;
9518
9519 let ctx_key = format!("delegation.{}.last_response", delegate_id);
9521 let _ = self.context_manager.set(
9522 &ctx_key,
9523 serde_json::Value::String(response.content.clone()),
9524 );
9525
9526 let _ = self.context_manager.set(
9528 "orchestration",
9529 serde_json::json!({
9530 "type": "delegate",
9531 "agent": delegate_id,
9532 "state": state_name,
9533 "response": response.content,
9534 "duration_ms": duration_ms,
9535 }),
9536 );
9537
9538 self.commit_root_user_message(input).await?;
9539
9540 let post_result = self
9543 .post_loop_processing(
9544 input,
9545 format!("[Delegated to {}]: {}", delegate_id, response.content),
9546 )
9547 .await?;
9548 let final_content = self.apply_post_loop_result(input, post_result).await?;
9549
9550 let mut result = AgentResponse::new(final_content);
9551
9552 let metadata = serde_json::json!({
9553 "orchestration": {
9554 "type": "delegate",
9555 "agent": delegate_id,
9556 "state": state_name,
9557 "response": response.content,
9558 "duration_ms": duration_ms,
9559 }
9560 });
9561 result.metadata = Some(
9562 serde_json::from_value::<std::collections::HashMap<String, serde_json::Value>>(
9563 metadata,
9564 )
9565 .unwrap_or_default(),
9566 );
9567
9568 self.finish_turn_if_root(&result).await?;
9569 Ok(result)
9570 }
9571
9572 async fn handle_concurrent_state(
9574 &self,
9575 input: &str,
9576 config: &ai_agents_state::ConcurrentStateConfig,
9577 ) -> Result<AgentResponse> {
9578 use std::time::Instant;
9579
9580 let registry = self.spawner_registry.as_ref().ok_or_else(|| {
9581 AgentError::Config(
9582 "Concurrent state requires an agent registry. Add a spawner section.".into(),
9583 )
9584 })?;
9585
9586 let context_mode = config.context_mode.clone().unwrap_or_default();
9591 let context_input = self
9592 .observe_purpose(
9593 ObservationPurpose::OrchestrationRouting,
9594 crate::orchestration::context::prepare_delegate_input(
9595 input,
9596 &context_mode,
9597 &*self.memory,
9598 self.llm_registry.get("router").ok().as_deref(),
9599 ),
9600 )
9601 .await?;
9602
9603 let effective_input = if let Some(ref tmpl) = config.input {
9604 render_concurrent_template(tmpl, &context_input, &self.build_context_with_overlays())
9605 .unwrap_or_else(|_| context_input.clone())
9606 } else {
9607 context_input
9608 };
9609
9610 let start = Instant::now();
9611
9612 let llm_name = config
9613 .aggregation
9614 .synthesizer_llm
9615 .as_deref()
9616 .unwrap_or("router");
9617 let llm_provider = self.llm_registry.get(llm_name).ok();
9618
9619 let vote_parallelism = if self.runtime_config.optimization.enabled
9620 && self
9621 .runtime_config
9622 .optimization
9623 .parallel_orchestration_vote_extraction
9624 {
9625 Some(self.runtime_config.optimization.max_parallel_runtime_tasks)
9626 } else {
9627 None
9628 };
9629
9630 let result = self
9631 .observe_purpose(
9632 ObservationPurpose::OrchestrationAggregation,
9633 scope_actor_context(
9634 self.outbound_actor_context(),
9635 crate::orchestration::concurrent(
9636 registry,
9637 &effective_input,
9638 &config.agents,
9639 &config.aggregation,
9640 llm_provider.as_deref(),
9641 config.min_required,
9642 config.timeout_ms,
9643 config.on_partial_failure.clone(),
9644 vote_parallelism,
9645 ),
9646 ),
9647 )
9648 .await?;
9649
9650 let duration_ms = start.elapsed().as_millis() as u64;
9651 let agent_ids: Vec<String> = config.agents.iter().map(|a| a.id().to_string()).collect();
9652 let strategy = format!("{:?}", config.aggregation.strategy);
9653 self.hooks
9654 .on_concurrent_complete(&agent_ids, &strategy, duration_ms)
9655 .await;
9656
9657 let _ = self.context_manager.set(
9659 "concurrent.result",
9660 serde_json::Value::String(result.response.content.clone()),
9661 );
9662
9663 let agents_json: Vec<serde_json::Value> = result
9665 .agent_results
9666 .iter()
9667 .map(|ar| {
9668 serde_json::json!({
9669 "id": ar.agent_id,
9670 "response": ar.response.as_ref().map(|r| r.content.as_str()),
9671 "success": ar.success,
9672 "error": ar.error,
9673 "duration_ms": ar.duration_ms,
9674 })
9675 })
9676 .collect();
9677
9678 let _ = self.context_manager.set(
9680 "orchestration",
9681 serde_json::json!({
9682 "type": "concurrent",
9683 "result": result.response.content,
9684 "strategy": strategy,
9685 "agents": agents_json,
9686 "duration_ms": duration_ms,
9687 }),
9688 );
9689
9690 self.commit_root_user_message(input).await?;
9691
9692 let post_result = self
9693 .post_loop_processing(input, result.response.content.clone())
9694 .await?;
9695 let final_content = self.apply_post_loop_result(input, post_result).await?;
9696
9697 let mut response = AgentResponse::new(final_content);
9698 let metadata = serde_json::json!({
9699 "orchestration": {
9700 "type": "concurrent",
9701 "result": result.response.content,
9702 "strategy": strategy,
9703 "agents": agents_json,
9704 "duration_ms": duration_ms,
9705 }
9706 });
9707 response.metadata = Some(
9708 serde_json::from_value::<std::collections::HashMap<String, serde_json::Value>>(
9709 metadata,
9710 )
9711 .unwrap_or_default(),
9712 );
9713
9714 self.finish_turn_if_root(&response).await?;
9715 Ok(response)
9716 }
9717
9718 async fn handle_group_chat_state(
9720 &self,
9721 input: &str,
9722 config: &ai_agents_state::GroupChatStateConfig,
9723 ) -> Result<AgentResponse> {
9724 use std::time::Instant;
9725
9726 let registry = self.spawner_registry.as_ref().ok_or_else(|| {
9727 AgentError::Config(
9728 "Group chat state requires an agent registry. Add a spawner section.".into(),
9729 )
9730 })?;
9731
9732 let start = Instant::now();
9733
9734 let llm_provider = self.llm_registry.get("router").ok();
9735
9736 let context_mode = config.context_mode.clone().unwrap_or_default();
9738 let context_input = self
9739 .observe_purpose(
9740 ObservationPurpose::OrchestrationRouting,
9741 crate::orchestration::context::prepare_delegate_input(
9742 input,
9743 &context_mode,
9744 &*self.memory,
9745 self.llm_registry.get("router").ok().as_deref(),
9746 ),
9747 )
9748 .await?;
9749
9750 let effective_topic = if let Some(ref tmpl) = config.input {
9752 render_concurrent_template(tmpl, &context_input, &self.build_context_with_overlays())
9753 .unwrap_or_else(|_| context_input.clone())
9754 } else {
9755 context_input
9756 };
9757
9758 let result = self
9759 .observe_purpose(
9760 ObservationPurpose::OrchestrationConversation,
9761 scope_actor_context(
9762 self.outbound_actor_context(),
9763 crate::orchestration::group_chat(
9764 registry,
9765 &effective_topic,
9766 config,
9767 llm_provider.as_deref(),
9768 Some(&*self.hooks),
9769 ),
9770 ),
9771 )
9772 .await?;
9773
9774 let duration_ms = start.elapsed().as_millis() as u64;
9775
9776 let _ = self.context_manager.set(
9778 "group_chat.conclusion",
9779 serde_json::Value::String(result.response.content.clone()),
9780 );
9781
9782 let transcript_json: Vec<serde_json::Value> = result
9784 .transcript
9785 .iter()
9786 .map(|t| {
9787 serde_json::json!({
9788 "speaker": t.speaker,
9789 "round": t.round,
9790 "content": t.content,
9791 })
9792 })
9793 .collect();
9794
9795 let _ = self.context_manager.set(
9797 "orchestration",
9798 serde_json::json!({
9799 "type": "group_chat",
9800 "conclusion": result.response.content,
9801 "transcript": transcript_json,
9802 "rounds": result.rounds_completed,
9803 "termination": result.termination_reason,
9804 "duration_ms": duration_ms,
9805 }),
9806 );
9807
9808 self.commit_root_user_message(input).await?;
9809
9810 let post_result = self
9811 .post_loop_processing(input, result.response.content.clone())
9812 .await?;
9813 let final_content = self.apply_post_loop_result(input, post_result).await?;
9814
9815 let mut response = AgentResponse::new(final_content);
9816 let metadata = serde_json::json!({
9817 "orchestration": {
9818 "type": "group_chat",
9819 "conclusion": result.response.content,
9820 "transcript": transcript_json,
9821 "rounds": result.rounds_completed,
9822 "termination": result.termination_reason,
9823 "duration_ms": duration_ms,
9824 }
9825 });
9826 response.metadata = Some(
9827 serde_json::from_value::<std::collections::HashMap<String, serde_json::Value>>(
9828 metadata,
9829 )
9830 .unwrap_or_default(),
9831 );
9832
9833 self.finish_turn_if_root(&response).await?;
9834 Ok(response)
9835 }
9836
9837 async fn handle_pipeline_state(
9839 &self,
9840 input: &str,
9841 config: &ai_agents_state::PipelineStateConfig,
9842 ) -> Result<AgentResponse> {
9843 use std::time::Instant;
9844
9845 let registry = self.spawner_registry.as_ref().ok_or_else(|| {
9846 AgentError::Config(
9847 "Pipeline state requires an agent registry. Add a spawner section.".into(),
9848 )
9849 })?;
9850
9851 let start = Instant::now();
9852
9853 let stages: Vec<crate::orchestration::PipelineStage> = config
9854 .stages
9855 .iter()
9856 .map(|entry| {
9857 let mut stage = crate::orchestration::PipelineStage::id(entry.id());
9858 if let Some(tmpl) = entry.input() {
9859 stage = stage.with_input(tmpl);
9860 }
9861 stage
9862 })
9863 .collect();
9864
9865 let context_mode = config.context_mode.clone().unwrap_or_default();
9867 let context_input = self
9868 .observe_purpose(
9869 ObservationPurpose::OrchestrationRouting,
9870 crate::orchestration::context::prepare_delegate_input(
9871 input,
9872 &context_mode,
9873 &*self.memory,
9874 self.llm_registry.get("router").ok().as_deref(),
9875 ),
9876 )
9877 .await?;
9878
9879 let context_values = self.build_context_with_overlays();
9880 let result = self
9881 .observe_purpose(
9882 ObservationPurpose::OrchestrationRouting,
9883 scope_actor_context(
9884 self.outbound_actor_context(),
9885 crate::orchestration::pipeline(
9886 registry,
9887 &context_input,
9888 &stages,
9889 config.timeout_ms,
9890 Some(&*self.hooks),
9891 Some(&context_values),
9892 ),
9893 ),
9894 )
9895 .await?;
9896
9897 let duration_ms = start.elapsed().as_millis() as u64;
9898
9899 let _ = self.context_manager.set(
9901 "pipeline.result",
9902 serde_json::Value::String(result.response.content.clone()),
9903 );
9904
9905 let stages_json: Vec<serde_json::Value> = result
9907 .stage_outputs
9908 .iter()
9909 .map(|s| {
9910 serde_json::json!({
9911 "agent_id": s.agent_id,
9912 "output": s.output,
9913 "duration_ms": s.duration_ms,
9914 "skipped": s.skipped,
9915 })
9916 })
9917 .collect();
9918
9919 let _ = self.context_manager.set(
9921 "orchestration",
9922 serde_json::json!({
9923 "type": "pipeline",
9924 "result": result.response.content,
9925 "stages": stages_json,
9926 "duration_ms": duration_ms,
9927 }),
9928 );
9929
9930 self.commit_root_user_message(input).await?;
9931
9932 let post_result = self
9933 .post_loop_processing(input, result.response.content.clone())
9934 .await?;
9935 let final_content = self.apply_post_loop_result(input, post_result).await?;
9936
9937 let mut response = AgentResponse::new(final_content);
9938 let metadata = serde_json::json!({
9939 "orchestration": {
9940 "type": "pipeline",
9941 "result": result.response.content,
9942 "stages": stages_json,
9943 "duration_ms": duration_ms,
9944 }
9945 });
9946 response.metadata = Some(
9947 serde_json::from_value::<std::collections::HashMap<String, serde_json::Value>>(
9948 metadata,
9949 )
9950 .unwrap_or_default(),
9951 );
9952
9953 self.finish_turn_if_root(&response).await?;
9954 Ok(response)
9955 }
9956
9957 async fn handle_handoff_state(
9959 &self,
9960 input: &str,
9961 config: &ai_agents_state::HandoffStateConfig,
9962 ) -> Result<AgentResponse> {
9963 use std::time::Instant;
9964
9965 let registry = self.spawner_registry.as_ref().ok_or_else(|| {
9966 AgentError::Config(
9967 "Handoff state requires an agent registry. Add a spawner section.".into(),
9968 )
9969 })?;
9970
9971 let llm = self
9972 .llm_registry
9973 .get("router")
9974 .map_err(|_| AgentError::Config("Handoff state requires a router LLM.".into()))?;
9975
9976 let start = Instant::now();
9977
9978 let context_mode = config.context_mode.clone().unwrap_or_default();
9980 let context_input = self
9981 .observe_purpose(
9982 ObservationPurpose::OrchestrationRouting,
9983 crate::orchestration::context::prepare_delegate_input(
9984 input,
9985 &context_mode,
9986 &*self.memory,
9987 self.llm_registry.get("router").ok().as_deref(),
9988 ),
9989 )
9990 .await?;
9991
9992 let effective_input = if let Some(ref tmpl) = config.input {
9994 render_concurrent_template(tmpl, &context_input, &self.build_context_with_overlays())
9995 .unwrap_or_else(|_| context_input.clone())
9996 } else {
9997 context_input
9998 };
9999
10000 let result = self
10001 .observe_purpose(
10002 ObservationPurpose::OrchestrationRouting,
10003 scope_actor_context(
10004 self.outbound_actor_context(),
10005 crate::orchestration::handoff(
10006 registry,
10007 &effective_input,
10008 &config.initial_agent,
10009 &config.available_agents,
10010 config.max_handoffs,
10011 llm.as_ref(),
10012 Some(&*self.hooks),
10013 ),
10014 ),
10015 )
10016 .await?;
10017
10018 let duration_ms = start.elapsed().as_millis() as u64;
10019
10020 let _ = self.context_manager.set(
10022 "handoff.result",
10023 serde_json::Value::String(result.response.content.clone()),
10024 );
10025
10026 let chain_json: Vec<serde_json::Value> = result
10028 .handoff_chain
10029 .iter()
10030 .map(|h| {
10031 serde_json::json!({
10032 "from": h.from_agent,
10033 "to": h.to_agent,
10034 "reason": h.reason,
10035 })
10036 })
10037 .collect();
10038
10039 let _ = self.context_manager.set(
10041 "orchestration",
10042 serde_json::json!({
10043 "type": "handoff",
10044 "result": result.response.content,
10045 "final_agent": result.final_agent,
10046 "handoff_chain": chain_json,
10047 "duration_ms": duration_ms,
10048 }),
10049 );
10050
10051 self.commit_root_user_message(input).await?;
10052
10053 let post_result = self
10054 .post_loop_processing(input, result.response.content.clone())
10055 .await?;
10056 let final_content = self.apply_post_loop_result(input, post_result).await?;
10057
10058 let mut response = AgentResponse::new(final_content);
10059 let metadata = serde_json::json!({
10060 "orchestration": {
10061 "type": "handoff",
10062 "result": result.response.content,
10063 "final_agent": result.final_agent,
10064 "handoff_chain": chain_json,
10065 "duration_ms": duration_ms,
10066 }
10067 });
10068 response.metadata = Some(
10069 serde_json::from_value::<std::collections::HashMap<String, serde_json::Value>>(
10070 metadata,
10071 )
10072 .unwrap_or_default(),
10073 );
10074
10075 self.finish_turn_if_root(&response).await?;
10076 Ok(response)
10077 }
10078
10079 async fn run_loop_internal(&self, input: &str) -> Result<AgentResponse> {
10081 self.begin_root_turn();
10082 self.pre_turn_session_lifecycle().await;
10084
10085 let input_data = self.process_input(input).await?;
10086 self.update_active_turn_context(&input_data.content, input_data.context.clone());
10087
10088 for (key, value) in &input_data.context {
10091 let _ = self.context_manager.set(key, value.clone());
10092 }
10093
10094 if input_data.metadata.rejected {
10095 let reason = input_data
10096 .metadata
10097 .rejection_reason
10098 .unwrap_or_else(|| "Input rejected".to_string());
10099 warn!(reason = %reason, "Input rejected");
10100 let response = AgentResponse::new(reason);
10101 self.finish_turn_if_root(&response).await?;
10102 return Ok(response);
10103 }
10104
10105 let processed_input = &input_data.content;
10106
10107 if let Some(response) = self.try_pre_response_transition(processed_input).await? {
10108 return Ok(response);
10109 }
10110
10111 if let Some(ref sm) = self.state_machine
10113 && let Some(def) = sm.current_definition()
10114 {
10115 if let Some(ref delegate_id) = def.delegate {
10116 return self
10117 .handle_delegated_state(processed_input, delegate_id, &def)
10118 .await;
10119 }
10120 if let Some(ref concurrent_config) = def.concurrent {
10121 return self
10122 .handle_concurrent_state(processed_input, concurrent_config)
10123 .await;
10124 }
10125 if let Some(ref group_chat_config) = def.group_chat {
10126 return self
10127 .handle_group_chat_state(processed_input, group_chat_config)
10128 .await;
10129 }
10130 if let Some(ref pipeline_config) = def.pipeline {
10131 return self
10132 .handle_pipeline_state(processed_input, pipeline_config)
10133 .await;
10134 }
10135 if let Some(ref handoff_config) = def.handoff {
10136 return self
10137 .handle_handoff_state(processed_input, handoff_config)
10138 .await;
10139 }
10140 }
10141
10142 if let Some(response) =
10147 Box::pin(self.try_speculative_branches(processed_input, &input_data.context)).await?
10148 {
10149 return Ok(response);
10150 }
10151
10152 match self.try_skill_route(processed_input).await? {
10153 SkillRouteResult::Response { skill_id, content } => {
10154 self.commit_root_user_message(processed_input).await?;
10155 return self
10156 .handle_skill_response(processed_input, &skill_id, content, &input_data.context)
10157 .await;
10158 }
10159 SkillRouteResult::NeedsClarification(response) => {
10160 self.commit_root_user_message(processed_input).await?;
10161 if let Some(q) = response
10162 .metadata
10163 .as_ref()
10164 .and_then(|m| m.get("disambiguation"))
10165 .and_then(|d| d.get("status"))
10166 .and_then(|s| s.as_str())
10167 && q == "awaiting_clarification"
10168 {
10169 self.memory
10172 .add_message(ChatMessage::assistant(&response.content))
10173 .await?;
10174 }
10175 self.finish_turn_if_root(&response).await?;
10176 return Ok(response);
10177 }
10178 SkillRouteResult::NoMatch => {} }
10180
10181 let effective_reasoning = self.get_effective_reasoning_config();
10182 let reasoning_mode = self.determine_reasoning_mode(processed_input).await?;
10183 let auto_detected = matches!(effective_reasoning.mode, ReasoningMode::Auto);
10184
10185 info!(
10186 reasoning_mode = ?reasoning_mode,
10187 auto_detected = auto_detected,
10188 reflection_enabled = ?self.reflection_config.enabled,
10189 "Reasoning mode determined"
10190 );
10191
10192 if matches!(reasoning_mode, ReasoningMode::PlanAndExecute) {
10193 self.commit_root_user_message(processed_input).await?;
10194 return self
10195 .handle_plan_and_execute(processed_input, &input_data.context, auto_detected)
10196 .await;
10197 }
10198
10199 self.commit_root_user_message(processed_input).await?;
10200
10201 let mut iterations = 0u32;
10202 let mut all_tool_calls: Vec<ToolCall> = Vec::new();
10203 let mut thinking_content: Option<String> = None;
10204
10205 let llm = self.get_state_llm()?;
10206
10207 loop {
10208 let effective_max = if reasoning_mode != ReasoningMode::None {
10210 let rc = self.get_effective_reasoning_config();
10211 self.max_iterations.min(rc.max_iterations)
10212 } else {
10213 self.max_iterations
10214 };
10215
10216 if iterations >= effective_max {
10217 let err = AgentError::Other(format!("Max iterations ({}) exceeded", effective_max));
10218 self.hooks.on_error(&err).await;
10219 error!(iterations = iterations, "Max iterations exceeded");
10220 return Err(err);
10221 }
10222 iterations += 1;
10223 *self.iteration_count.write() = iterations;
10224
10225 debug!(iteration = iterations, max = effective_max, "LLM call");
10226
10227 let protocol = self.main_tool_protocol(llm.as_ref(), false).await?;
10228 let mut messages = self
10229 .build_messages_internal(true, None, protocol.choice.is_none())
10230 .await?;
10231 self.inject_reasoning_prompt(&mut messages, &reasoning_mode, iterations == 1);
10232
10233 self.hooks.on_llm_start(&messages).await;
10234 let llm_start = Instant::now();
10235 let response = self
10236 .complete_main_llm_with_recovery(Arc::clone(&llm), &messages, &protocol)
10237 .await?;
10238
10239 let llm_duration_ms = llm_start.elapsed().as_millis() as u64;
10240 self.hooks.on_llm_complete(&response, llm_duration_ms).await;
10241
10242 let content = response.content.trim();
10243
10244 if let Some(tool_calls) = self.parse_main_tool_calls(content, &protocol) {
10245 match self
10246 .handle_tool_calls(processed_input, content, tool_calls, &mut all_tool_calls)
10247 .await?
10248 {
10249 ToolCallOutcome::Continue | ToolCallOutcome::TransitionFired => continue,
10250 ToolCallOutcome::Rejected(resp) => {
10251 self.finish_turn_if_root(&resp).await?;
10252 return Ok(resp);
10253 }
10254 }
10255 }
10256
10257 let (extracted_thinking, answer) = self.extract_thinking(content);
10258 if extracted_thinking.is_some() {
10259 thinking_content = extracted_thinking;
10260 }
10261
10262 let output_data = self.process_output(&answer, &input_data.context).await?;
10263
10264 let mut final_content = if output_data.metadata.rejected {
10265 output_data
10266 .metadata
10267 .rejection_reason
10268 .unwrap_or_else(|| answer.to_string())
10269 } else {
10270 output_data.content
10271 };
10272
10273 let reflection_metadata;
10275 (final_content, reflection_metadata) = self
10276 .run_reflection(&*llm, processed_input, final_content)
10277 .await?;
10278
10279 final_content =
10280 self.format_response_with_thinking(thinking_content.as_deref(), &final_content);
10281
10282 let final_content = {
10286 let result = self
10287 .post_loop_processing(processed_input, final_content)
10288 .await?;
10289 self.apply_post_loop_result(processed_input, result).await?
10290 };
10291
10292 let reflected = reflection_metadata.is_some();
10293 let reasoning_mode_debug = format!("{:?}", reasoning_mode);
10294
10295 let response = self.build_agent_response(AgentResponseParts {
10296 content: final_content,
10297 all_tool_calls,
10298 reasoning_mode,
10299 auto_detected,
10300 iterations,
10301 thinking: thinking_content,
10302 reflection_metadata,
10303 });
10304
10305 self.finish_turn_if_root(&response).await?;
10306
10307 let tool_call_count = response.tool_calls.as_ref().map(|tc| tc.len()).unwrap_or(0);
10308 info!(
10309 tool_calls = tool_call_count,
10310 response_len = response.content.len(),
10311 reasoning_mode = %reasoning_mode_debug,
10312 reflected = reflected,
10313 "Chat completed"
10314 );
10315 return Ok(response);
10316 }
10317 }
10318
10319 async fn generate_buffered_streaming_draft(
10320 &self,
10321 processed_input: &str,
10322 routing_resolved: Arc<AtomicBool>,
10323 ) -> Result<StreamingDraftResult> {
10324 let llm = self.get_state_llm()?;
10325 if llm.configured_tool_choice().is_some() {
10326 let draft = self
10327 .generate_main_response_draft(processed_input, &ReasoningMode::None)
10328 .await?;
10329 return Ok(StreamingDraftResult::new(draft, Vec::new()));
10330 }
10331 let messages = self.build_messages_for_draft(processed_input).await?;
10332 let mut stream = self
10333 .observe_purpose(
10334 ObservationPurpose::MainResponse,
10335 llm.complete_stream(&messages, None),
10336 )
10337 .await
10338 .map_err(|e| AgentError::LLM(e.to_string()))?;
10339 let mut buffer = crate::optimization::StreamBranchBuffer::new(self.streaming.buffer_size)?;
10340 let mut chunks = Vec::new();
10341 let mut accumulated = String::new();
10342 while let Some(chunk_result) = stream.next().await {
10343 let chunk = chunk_result.map_err(|e| AgentError::LLM(e.to_string()))?;
10344 accumulated.push_str(&chunk.delta);
10345 let stream_chunk = StreamChunk::content(chunk.delta);
10346 if routing_resolved.load(Ordering::SeqCst) {
10347 chunks.push(stream_chunk);
10348 } else {
10349 buffer.push(stream_chunk)?;
10350 }
10351 }
10352 chunks.splice(0..0, buffer.drain());
10353 let content = accumulated.trim().to_string();
10354 let draft = if let Some(calls) = self.parse_tool_calls(&content) {
10355 MainResponseDraft::ToolCalls {
10356 raw_content: content,
10357 calls,
10358 thinking: None,
10359 }
10360 } else {
10361 MainResponseDraft::Text {
10362 raw_content: content,
10363 thinking: None,
10364 }
10365 };
10366 Ok(StreamingDraftResult::new(draft, chunks))
10367 }
10368
10369 async fn try_buffered_streaming_branches(
10370 &self,
10371 processed_input: &str,
10372 input_context: &HashMap<String, Value>,
10373 ) -> Result<Option<(AgentResponse, Vec<StreamChunk>)>> {
10374 let optimization = &self.runtime_config.optimization;
10375 if !optimization.enabled {
10376 return Ok(None);
10377 }
10378 let transition_enabled =
10379 optimization.speculative_state_transitions && self.has_parallel_transition_candidates();
10380 if !transition_enabled {
10381 return Ok(None);
10382 }
10383 let mut branch_scheduler =
10384 TurnBranchScheduler::new(optimization.max_parallel_runtime_tasks)?;
10385 if !branch_scheduler.reserve_task() {
10386 return Ok(None);
10387 }
10388 if !self
10389 .reserve_active_speculative_llm_call(RuntimeOptimizationKind::BufferedStreamingRouting)
10390 {
10391 branch_scheduler.release_task();
10392 return Ok(None);
10393 }
10394 if !branch_scheduler.reserve_task() {
10395 branch_scheduler.release_task();
10396 return Ok(None);
10397 }
10398 let mut main_branch = RuntimeBranch::new(
10399 RuntimeTaskPurpose::MainResponse,
10400 RuntimeOptimizationKind::BufferedStreamingRouting,
10401 RuntimeTaskPriority::Normal,
10402 RuntimeCommitBehavior::FinalResponse,
10403 );
10404 let mut transition_branch = RuntimeBranch::new(
10405 RuntimeTaskPurpose::StateTransition,
10406 RuntimeOptimizationKind::ParallelStateTransition,
10407 RuntimeTaskPriority::Critical,
10408 RuntimeCommitBehavior::TransitionDecision,
10409 );
10410 let main_id = main_branch.branch_id();
10411 let transition_id = transition_branch.branch_id();
10412 let routing_resolved = Arc::new(AtomicBool::new(false));
10413 let mut main_future =
10414 Box::pin(crate::optimization::observability::with_branch_observation(
10415 &main_id,
10416 RuntimeOptimizationKind::BufferedStreamingRouting,
10417 RuntimeCommitBehavior::FinalResponse,
10418 self.generate_buffered_streaming_draft(
10419 processed_input,
10420 Arc::clone(&routing_resolved),
10421 ),
10422 ));
10423 let mut transition_future =
10424 Box::pin(crate::optimization::observability::with_branch_observation(
10425 &transition_id,
10426 RuntimeOptimizationKind::ParallelStateTransition,
10427 RuntimeCommitBehavior::TransitionDecision,
10428 self.select_parallel_transition_candidate(processed_input),
10429 ));
10430 let mut main_pending = true;
10431 let mut transition_pending = true;
10432 let mut main_result: Option<Result<StreamingDraftResult>> = None;
10433 let mut transition_finalized = false;
10434 let mut transition_candidate: Option<TransitionCandidate> = None;
10435 loop {
10436 if let Some(candidate) = transition_candidate.take() {
10437 if self
10438 .approve_transition_target(&candidate.from_state, candidate.target())
10439 .await?
10440 {
10441 drop(main_future);
10443 drop(transition_future);
10444 self.finalize_branch_loss(
10445 &main_id,
10446 RuntimeOptimizationKind::BufferedStreamingRouting,
10447 RuntimeCommitBehavior::FinalResponse,
10448 main_pending,
10449 main_result.as_ref().map(|result| result.is_err()),
10450 );
10451 if !self
10452 .apply_pre_response_transition_candidate(
10453 &candidate,
10454 &HashMap::new(),
10455 processed_input,
10456 )
10457 .await?
10458 {
10459 self.finalize_optional_branch(
10460 &transition_id,
10461 RuntimeOptimizationKind::ParallelStateTransition,
10462 RuntimeCommitBehavior::TransitionDecision,
10463 "discarded",
10464 false,
10465 );
10466 return Ok(None);
10467 }
10468 self.finalize_optional_branch(
10469 &transition_id,
10470 RuntimeOptimizationKind::ParallelStateTransition,
10471 RuntimeCommitBehavior::TransitionDecision,
10472 "committed",
10473 true,
10474 );
10475 let response = self.redispatch_current_state(processed_input).await?;
10476 return Ok(Some((
10477 response.clone(),
10478 vec![StreamChunk::content(response.content)],
10479 )));
10480 }
10481 self.finalize_optional_branch(
10482 &transition_id,
10483 RuntimeOptimizationKind::ParallelStateTransition,
10484 RuntimeCommitBehavior::TransitionDecision,
10485 "discarded",
10486 false,
10487 );
10488 routing_resolved.store(true, Ordering::SeqCst);
10489 transition_finalized = true;
10490 }
10491 if transition_finalized && let Some(result) = main_result.take() {
10492 let stream_draft = match result {
10493 Ok(stream_draft) => stream_draft,
10494 Err(error) => {
10495 self.finalize_optional_branch(
10496 &main_id,
10497 RuntimeOptimizationKind::BufferedStreamingRouting,
10498 RuntimeCommitBehavior::FinalResponse,
10499 "failed",
10500 false,
10501 );
10502 return Err(error);
10503 }
10504 };
10505 let raw_draft_content = stream_draft.draft.raw_content().to_string();
10506 let buffered_chunks = stream_draft.chunks;
10507 self.finalize_optional_branch(
10508 &main_id,
10509 RuntimeOptimizationKind::BufferedStreamingRouting,
10510 RuntimeCommitBehavior::FinalResponse,
10511 "committed",
10512 true,
10513 );
10514 let response = self
10515 .commit_main_response_draft(
10516 processed_input,
10517 input_context,
10518 stream_draft.draft,
10519 ReasoningMode::None,
10520 false,
10521 )
10522 .await?;
10523 let chunks = if response.content == raw_draft_content {
10524 buffered_chunks
10525 } else {
10526 vec![StreamChunk::content(response.content.clone())]
10527 };
10528 return Ok(Some((response, chunks)));
10529 }
10530 tokio::select! {
10531 result = &mut main_future, if main_pending => {
10532 main_pending = false;
10533 main_branch.transition_to(RuntimeBranchStatus::Completed)?;
10534 main_result = Some(result);
10535 }
10536 result = &mut transition_future, if transition_pending => {
10537 transition_pending = false;
10538 transition_branch.transition_to(RuntimeBranchStatus::Completed)?;
10539 match result {
10540 Ok(ParallelTransitionSelection::Candidate(candidate)) => {
10541 transition_candidate = Some(candidate)
10542 }
10543 Ok(ParallelTransitionSelection::NoMatch) => {
10544 self.finalize_optional_branch(
10545 &transition_id,
10546 RuntimeOptimizationKind::ParallelStateTransition,
10547 RuntimeCommitBehavior::TransitionDecision,
10548 "discarded",
10549 false,
10550 );
10551 routing_resolved.store(true, Ordering::SeqCst);
10552 transition_finalized = true;
10553 }
10554 Ok(ParallelTransitionSelection::ReservationExhausted) => {
10555 self.finalize_optional_branch(
10556 &transition_id,
10557 RuntimeOptimizationKind::ParallelStateTransition,
10558 RuntimeCommitBehavior::TransitionDecision,
10559 "cancelled",
10560 false,
10561 );
10562 routing_resolved.store(true, Ordering::SeqCst);
10563 self.finalize_branch_loss(
10564 &main_id,
10565 RuntimeOptimizationKind::BufferedStreamingRouting,
10566 RuntimeCommitBehavior::FinalResponse,
10567 main_pending,
10568 main_result.as_ref().map(|result| result.is_err()),
10569 );
10570 return Ok(None);
10571 }
10572 Err(_) => {
10573 self.finalize_optional_branch(
10574 &transition_id,
10575 RuntimeOptimizationKind::ParallelStateTransition,
10576 RuntimeCommitBehavior::TransitionDecision,
10577 "failed",
10578 false,
10579 );
10580 routing_resolved.store(true, Ordering::SeqCst);
10581 transition_finalized = true;
10582 }
10583 }
10584 }
10585 }
10586 }
10587 }
10588
10589 fn run_loop_internal_stream<'a>(
10593 &'a self,
10594 input: &'a str,
10595 ) -> Pin<Box<dyn Stream<Item = StreamChunk> + Send + 'a>> {
10596 let include_tool_events = self.streaming.include_tool_events;
10597 let include_state_events = self.streaming.include_state_events;
10598
10599 Box::pin(async_stream::stream! {
10600 self.begin_root_turn();
10601 self.pre_turn_session_lifecycle().await;
10603
10604 let input_data = match self.process_input(input).await {
10605 Ok(data) => data,
10606 Err(e) => {
10607 yield StreamChunk::error(e.to_string());
10608 return;
10609 }
10610 };
10611 self.update_active_turn_context(&input_data.content, input_data.context.clone());
10612
10613 for (key, value) in &input_data.context {
10615 let _ = self.context_manager.set(key, value.clone());
10616 }
10617
10618 if input_data.metadata.rejected {
10619 let reason = input_data
10620 .metadata
10621 .rejection_reason
10622 .unwrap_or_else(|| "Input rejected".to_string());
10623 warn!(reason = %reason, "Input rejected (stream)");
10624 yield StreamChunk::error(reason);
10625 return;
10626 }
10627
10628 let processed_input = &input_data.content;
10629
10630 if self.runtime_config.optimization.enabled
10631 && matches!(
10632 self.runtime_config.optimization.streaming_policy,
10633 crate::optimization::StreamingOptimizationPolicy::BufferUntilRoutingDone
10634 )
10635 {
10636 match Box::pin(self.try_buffered_streaming_branches(processed_input, &input_data.context)).await {
10641 Ok(Some((_response, chunks))) => {
10642 for chunk in chunks {
10643 yield chunk;
10644 }
10645 yield StreamChunk::Done {};
10646 return;
10647 }
10648 Ok(None) => {}
10649 Err(e) => {
10650 yield StreamChunk::error(e.to_string());
10651 return;
10652 }
10653 }
10654 }
10655
10656 if self.runtime_config.optimization.enabled
10657 && matches!(
10658 self.runtime_config.optimization.streaming_policy,
10659 crate::optimization::StreamingOptimizationPolicy::PreflightOnly
10660 )
10661 {
10662 match self.try_pre_response_transition(processed_input).await {
10663 Ok(Some(response)) => {
10664 yield StreamChunk::content(&response.content);
10665 yield StreamChunk::Done {};
10666 return;
10667 }
10668 Ok(None) => {}
10669 Err(e) => {
10670 yield StreamChunk::error(e.to_string());
10671 return;
10672 }
10673 }
10674 }
10675
10676 if let Some(ref sm) = self.state_machine
10678 && let Some(def) = sm.current_definition()
10679 {
10680 let orchestration_result = if let Some(ref delegate_id) = def.delegate {
10681 Some(self.handle_delegated_state(processed_input, delegate_id, &def).await)
10682 } else if let Some(ref concurrent_config) = def.concurrent {
10683 Some(self.handle_concurrent_state(processed_input, concurrent_config).await)
10684 } else if let Some(ref group_chat_config) = def.group_chat {
10685 Some(self.handle_group_chat_state(processed_input, group_chat_config).await)
10686 } else if let Some(ref pipeline_config) = def.pipeline {
10687 Some(self.handle_pipeline_state(processed_input, pipeline_config).await)
10688 } else if let Some(ref handoff_config) = def.handoff {
10689 Some(self.handle_handoff_state(processed_input, handoff_config).await)
10690 } else {
10691 None
10692 };
10693
10694 if let Some(result) = orchestration_result {
10695 match result {
10696 Ok(response) => {
10697 yield StreamChunk::content(&response.content);
10698 yield StreamChunk::Done {};
10699 }
10700 Err(e) => {
10701 yield StreamChunk::error(e.to_string());
10702 }
10703 }
10704 return;
10705 }
10706 }
10707
10708 match self.try_skill_route(processed_input).await {
10710 Ok(SkillRouteResult::Response { skill_id, content }) => {
10711 if let Err(e) = self.commit_root_user_message(processed_input).await {
10712 yield StreamChunk::error(e.to_string());
10713 return;
10714 }
10715 match self.handle_skill_response(processed_input, &skill_id, content, &input_data.context).await {
10716 Ok(resp) => {
10717 yield StreamChunk::content(&resp.content);
10718 yield StreamChunk::Done {};
10719 return;
10720 }
10721 Err(e) => {
10722 yield StreamChunk::error(e.to_string());
10723 return;
10724 }
10725 }
10726 }
10727 Ok(SkillRouteResult::NeedsClarification(response)) => {
10728 if let Err(e) = self.commit_root_user_message(processed_input).await {
10729 yield StreamChunk::error(e.to_string());
10730 return;
10731 }
10732 let _ = self.memory.add_message(ChatMessage::assistant(&response.content)).await;
10733 if let Err(e) = self.finish_turn_if_root(&response).await {
10734 yield StreamChunk::error(e.to_string());
10735 return;
10736 }
10737 yield StreamChunk::content(&response.content);
10738 yield StreamChunk::Done {};
10739 return;
10740 }
10741 Ok(SkillRouteResult::NoMatch) => {} Err(e) => {
10743 yield StreamChunk::error(e.to_string());
10744 return;
10745 }
10746 }
10747
10748 let effective_reasoning = self.get_effective_reasoning_config();
10750 let reasoning_mode = match self.determine_reasoning_mode(processed_input).await {
10751 Ok(mode) => mode,
10752 Err(e) => {
10753 yield StreamChunk::error(e.to_string());
10754 return;
10755 }
10756 };
10757 let auto_detected = matches!(effective_reasoning.mode, ReasoningMode::Auto);
10758
10759 info!(
10760 reasoning_mode = ?reasoning_mode,
10761 auto_detected = auto_detected,
10762 "Reasoning mode determined (stream)"
10763 );
10764
10765 if matches!(reasoning_mode, ReasoningMode::PlanAndExecute) {
10767 if let Err(e) = self.commit_root_user_message(processed_input).await {
10768 yield StreamChunk::error(e.to_string());
10769 return;
10770 }
10771 match self.handle_plan_and_execute(processed_input, &input_data.context, auto_detected).await {
10772 Ok(resp) => {
10773 yield StreamChunk::content(&resp.content);
10774 yield StreamChunk::Done {};
10775 return;
10776 }
10777 Err(e) => {
10778 yield StreamChunk::error(e.to_string());
10779 return;
10780 }
10781 }
10782 }
10783
10784 if let Err(e) = self.commit_root_user_message(processed_input).await {
10785 yield StreamChunk::error(e.to_string());
10786 return;
10787 }
10788
10789 let llm = match self.get_state_llm() {
10790 Ok(llm) => llm,
10791 Err(e) => {
10792 yield StreamChunk::error(e.to_string());
10793 return;
10794 }
10795 };
10796
10797 let mut iterations = 0u32;
10798 let mut all_tool_calls: Vec<ToolCall> = Vec::new();
10799 let mut thinking_content: Option<String> = None;
10800
10801 loop {
10802 let effective_max = if reasoning_mode != ReasoningMode::None {
10804 let rc = self.get_effective_reasoning_config();
10805 self.max_iterations.min(rc.max_iterations)
10806 } else {
10807 self.max_iterations
10808 };
10809
10810 if iterations >= effective_max {
10811 let err_msg = format!("Max iterations ({}) exceeded", effective_max);
10812 let err = AgentError::Other(err_msg.clone());
10813 self.hooks.on_error(&err).await;
10814 error!(iterations = iterations, "Max iterations exceeded (stream)");
10815 yield StreamChunk::error(err_msg);
10816 return;
10817 }
10818 iterations += 1;
10819 *self.iteration_count.write() = iterations;
10820
10821 debug!(iteration = iterations, max = effective_max, "LLM call (stream)");
10822
10823 let protocol = match self.main_tool_protocol(llm.as_ref(), false).await {
10824 Ok(protocol) => protocol,
10825 Err(e) => {
10826 yield StreamChunk::error(e.to_string());
10827 return;
10828 }
10829 };
10830 let mut messages = match self
10831 .build_messages_internal(true, None, protocol.choice.is_none())
10832 .await
10833 {
10834 Ok(m) => m,
10835 Err(e) => {
10836 yield StreamChunk::error(e.to_string());
10837 return;
10838 }
10839 };
10840 self.inject_reasoning_prompt(&mut messages, &reasoning_mode, iterations == 1);
10841
10842 self.hooks.on_llm_start(&messages).await;
10843 let llm_start = Instant::now();
10844
10845 let reflection_active = self
10848 .should_reflect(processed_input, "")
10849 .await
10850 .unwrap_or_default();
10851
10852 let buffered_decision = reflection_active || protocol.choice.is_some();
10853 let content = if buffered_decision {
10854 let response = match self
10858 .complete_main_llm_with_recovery(
10859 Arc::clone(&llm),
10860 &messages,
10861 &protocol,
10862 )
10863 .await
10864 {
10865 Ok(r) => r,
10866 Err(e) => {
10867 yield StreamChunk::error(e.to_string());
10868 return;
10869 }
10870 };
10871 let llm_duration_ms = llm_start.elapsed().as_millis() as u64;
10872 self.hooks.on_llm_complete(&response, llm_duration_ms).await;
10873 response.content.trim().to_string()
10874 } else {
10875 let llm_stream = match self
10877 .observe_purpose(
10878 ObservationPurpose::MainResponse,
10879 llm.complete_stream(&messages, None),
10880 )
10881 .await
10882 {
10883 Ok(s) => s,
10884 Err(e) => {
10885 yield StreamChunk::error(e.to_string());
10886 return;
10887 }
10888 };
10889 let mut accumulated = String::new();
10890 let mut stream_inner = llm_stream;
10891 while let Some(chunk_result) = stream_inner.next().await {
10892 match chunk_result {
10893 Ok(chunk) => {
10894 accumulated.push_str(&chunk.delta);
10895 yield StreamChunk::content(chunk.delta);
10896 }
10897 Err(e) => {
10898 yield StreamChunk::error(e.to_string());
10899 return;
10900 }
10901 }
10902 }
10903 let llm_duration_ms = llm_start.elapsed().as_millis() as u64;
10904 let llm_response = ai_agents_core::LLMResponse::new(
10906 accumulated.trim(),
10907 ai_agents_core::FinishReason::Stop,
10908 );
10909 self.hooks.on_llm_complete(&llm_response, llm_duration_ms).await;
10910 accumulated.trim().to_string()
10911 };
10912
10913 if let Some(tool_calls) = self.parse_main_tool_calls(&content, &protocol) {
10915 let native_tool_call = Self::is_native_tool_call_content(&content);
10916 let transition_fired = match self.evaluate_transitions(processed_input, &content).await {
10919 Ok(v) => v,
10920 Err(e) => {
10921 yield StreamChunk::error(e.to_string());
10922 return;
10923 }
10924 };
10925 if transition_fired {
10926 let _ = self.memory.add_message(ChatMessage::assistant(
10927 "(Transitioned to new state — tool call handled by workflow)",
10928 )).await;
10929
10930 if include_state_events
10931 && let Some(state) = self.current_state()
10932 {
10933 yield StreamChunk::state_transition(None, state);
10934 }
10935 continue;
10936 }
10937
10938 let _ = self.memory.add_message(ChatMessage::assistant(&content)).await;
10940
10941 let results = self.execute_tools_parallel(&tool_calls).await;
10943
10944 for ((_id, result), tool_call) in results.into_iter().zip(tool_calls.iter()) {
10945 if include_tool_events {
10946 yield StreamChunk::tool_start(&tool_call.id, &tool_call.name);
10947 }
10948
10949 match result {
10950 Ok(output) => {
10951 if include_tool_events {
10952 yield StreamChunk::tool_result(
10953 &tool_call.id,
10954 &tool_call.name,
10955 &output,
10956 true,
10957 );
10958 }
10959 let _ = self.memory
10960 .add_message(Self::tool_result_message(
10961 tool_call,
10962 &output,
10963 native_tool_call,
10964 ))
10965 .await;
10966 }
10967 Err(e) => {
10968 if matches!(e, AgentError::HITLRejected(_)) {
10969 let _ = self.memory.add_message(ChatMessage::assistant(
10970 format!("The operation was rejected by the approver: {}", e),
10971 )).await;
10972 let response = AgentResponse {
10973 content: format!("Operation cancelled: {}", e),
10974 metadata: None,
10975 tool_calls: Some(all_tool_calls.clone()),
10976 };
10977 if let Err(finalize_error) = self.finish_turn_if_root(&response).await {
10978 yield StreamChunk::error(finalize_error.to_string());
10979 return;
10980 }
10981 yield StreamChunk::error(response.content);
10982 yield StreamChunk::Done {};
10983 return;
10984 }
10985 if include_tool_events {
10986 yield StreamChunk::tool_result(
10987 &tool_call.id,
10988 &tool_call.name,
10989 e.to_string(),
10990 false,
10991 );
10992 }
10993 let _ = self.memory
10994 .add_message(Self::tool_result_message(
10995 tool_call,
10996 &format!("Error: {}", e),
10997 native_tool_call,
10998 ))
10999 .await;
11000 }
11001 }
11002 all_tool_calls.push(tool_call.clone());
11003
11004 if include_tool_events {
11005 yield StreamChunk::tool_end(&tool_call.id);
11006 }
11007 }
11008 continue;
11009 }
11010
11011 let (extracted_thinking, answer) = self.extract_thinking(&content);
11013 if extracted_thinking.is_some() {
11014 thinking_content = extracted_thinking;
11015 }
11016
11017 let output_data = match self.process_output(&answer, &input_data.context).await {
11018 Ok(d) => d,
11019 Err(e) => {
11020 yield StreamChunk::error(e.to_string());
11021 return;
11022 }
11023 };
11024
11025 let final_content = if output_data.metadata.rejected {
11026 output_data
11027 .metadata
11028 .rejection_reason
11029 .unwrap_or_else(|| answer.to_string())
11030 } else {
11031 output_data.content
11032 };
11033
11034 let (final_content, _reflection_metadata) = match self
11036 .run_reflection(&*llm, processed_input, final_content)
11037 .await
11038 {
11039 Ok(r) => r,
11040 Err(e) => {
11041 yield StreamChunk::error(e.to_string());
11042 return;
11043 }
11044 };
11045
11046 let final_content = self.format_response_with_thinking(
11047 thinking_content.as_deref(),
11048 &final_content,
11049 );
11050
11051 if buffered_decision {
11053 yield StreamChunk::content(&final_content);
11054 }
11055
11056 let post_result = match self
11060 .post_loop_processing(processed_input, final_content)
11061 .await
11062 {
11063 Ok(r) => r,
11064 Err(e) => {
11065 yield StreamChunk::error(e.to_string());
11066 return;
11067 }
11068 };
11069
11070 let (final_content, transitioned) = match post_result {
11071 PostLoopResult::NoTransition(content) => (content, false),
11072 PostLoopResult::Transitioned(content) => (content, true),
11073 PostLoopResult::NeedsRedispatch => {
11074 const MAX_REDISPATCH_DEPTH: u32 = 3;
11075 let current_depth = *self.redispatch_depth.read();
11076 let content = if current_depth >= MAX_REDISPATCH_DEPTH {
11077 warn!(
11078 depth = current_depth,
11079 "Post-transition re-dispatch depth limit reached (stream)"
11080 );
11081 let c = String::new();
11082 let _ = self.memory.add_message(ChatMessage::assistant(&c)).await;
11083 c
11084 } else {
11085 *self.redispatch_depth.write() += 1;
11086 if let Some(context) = self.active_turn_context.write().as_mut() {
11087 context.enter_redispatch();
11088 }
11089 info!(
11090 depth = current_depth + 1,
11091 "Re-dispatching for new state after transition (stream)"
11092 );
11093 let result = self.run_loop_internal(processed_input).await;
11094 *self.redispatch_depth.write() -= 1;
11095 if let Some(context) = self.active_turn_context.write().as_mut() {
11096 context.exit_redispatch();
11097 }
11098 match result {
11099 Ok(resp) => resp.content,
11100 Err(e) => {
11101 yield StreamChunk::error(e.to_string());
11102 return;
11103 }
11104 }
11105 };
11106 (content, true)
11107 }
11108 };
11109
11110 if transitioned {
11111 if include_state_events
11112 && let Some(state) = self.current_state()
11113 {
11114 yield StreamChunk::state_transition(None, state);
11115 }
11116 yield StreamChunk::content(&final_content);
11118 }
11119
11120 let final_response = AgentResponse::new(&final_content);
11122 if let Err(e) = self.finish_turn_if_root(&final_response).await {
11123 yield StreamChunk::error(e.to_string());
11124 return;
11125 }
11126
11127 yield StreamChunk::Done {};
11128 return;
11129 }
11130 })
11131 }
11132
11133 fn run_loop_stream<'a>(
11136 &'a self,
11137 input: &'a str,
11138 ) -> Pin<Box<dyn Stream<Item = StreamChunk> + Send + 'a>> {
11139 Box::pin(async_stream::stream! {
11140 self.begin_root_turn();
11141 let _root_cleanup = RootTurnCleanup::new(self);
11142 self.hooks.on_message_received(input).await;
11143
11144 if !self.context_initialized.swap(true, Ordering::SeqCst) {
11146 if let Err(e) = self.context_manager.initialize().await {
11147 yield StreamChunk::error(e.to_string());
11148 return;
11149 }
11150 debug!("Context manager initialized (defaults, env, builtins)");
11151 }
11152
11153 if let Err(e) = self.check_turn_timeout().await {
11154 yield StreamChunk::error(e.to_string());
11155 return;
11156 }
11157 if let Err(e) = self.context_manager.refresh_per_turn().await {
11158 yield StreamChunk::error(e.to_string());
11159 return;
11160 }
11161
11162 self.clear_disambiguation_context();
11164
11165 if let Some(ref disambiguator) = self.disambiguation_manager {
11167 let disambiguation_context = match self.build_disambiguation_context().await {
11168 Ok(ctx) => ctx,
11169 Err(e) => {
11170 yield StreamChunk::error(e.to_string());
11171 return;
11172 }
11173 };
11174
11175 let state_override = self
11176 .state_machine
11177 .as_ref()
11178 .and_then(|sm| sm.current_definition())
11179 .and_then(|def| def.disambiguation.clone());
11180
11181 let result = match self
11182 .observe_purpose(
11183 ObservationPurpose::DisambiguationDetection,
11184 disambiguator.process_input_with_override(
11185 input,
11186 &disambiguation_context,
11187 state_override.as_ref(),
11188 None,
11189 ),
11190 )
11191 .await
11192 {
11193 Ok(r) => r,
11194 Err(e) => {
11195 yield StreamChunk::error(e.to_string());
11196 return;
11197 }
11198 };
11199
11200 match result {
11201 DisambiguationResult::Clear => {
11202 debug!("Input is clear, proceeding normally (stream)");
11203 }
11204 DisambiguationResult::NeedsClarification {
11205 question,
11206 detection,
11207 } => {
11208 info!(
11209 ambiguity_type = ?detection.ambiguity_type,
11210 confidence = detection.confidence,
11211 "Input requires clarification (stream)"
11212 );
11213 if let Err(e) = self.commit_root_user_message(input).await {
11214 yield StreamChunk::error(e.to_string());
11215 return;
11216 }
11217 let _ = self
11218 .memory
11219 .add_message(ChatMessage::assistant(&question.question))
11220 .await;
11221 let response = AgentResponse::new(&question.question);
11222 if let Err(e) = self.finish_turn_if_root(&response).await {
11223 yield StreamChunk::error(e.to_string());
11224 return;
11225 }
11226 yield StreamChunk::content(&question.question);
11227 yield StreamChunk::Done {};
11228 return;
11229 }
11230 DisambiguationResult::Clarified {
11231 enriched_input,
11232 resolved,
11233 ..
11234 } => {
11235 info!(
11236 resolved_count = resolved.len(),
11237 enriched = %enriched_input,
11238 "Input clarified (stream)"
11239 );
11240 for (key, value) in &resolved {
11241 let context_key = format!("disambiguation.{}", key);
11242 let _ = self.context_manager.set(&context_key, value.clone());
11243 }
11244 if let Some(intent) = resolved.get("intent") {
11245 let _ = self.context_manager.set("resolved_intent", intent.clone());
11246 }
11247 let _ = self
11248 .context_manager
11249 .set("disambiguation.resolved", serde_json::Value::Bool(true));
11250
11251 let skill_id = self.pending_skill_id.read().clone();
11255 if let Some(skill_id) = skill_id {
11256 info!(skill_id = %skill_id, "Re-checking skill disambiguation on clarified input (stream)");
11257 match self.recheck_skill_disambiguation(&skill_id, &enriched_input).await {
11258 Ok(resp) => {
11259 yield StreamChunk::content(&resp.content);
11260 yield StreamChunk::Done {};
11261 return;
11262 }
11263 Err(e) => {
11264 yield StreamChunk::error(e.to_string());
11265 return;
11266 }
11267 }
11268 }
11269
11270 let mut inner = self.run_loop_internal_stream(&enriched_input);
11272 while let Some(chunk) = inner.next().await {
11273 yield chunk;
11274 }
11275 return;
11276 }
11277 DisambiguationResult::ProceedWithBestGuess { enriched_input } => {
11278 info!("Proceeding with best guess (stream)");
11279
11280 let skill_id = self.pending_skill_id.read().clone();
11282 if let Some(skill_id) = skill_id {
11283 info!(skill_id = %skill_id, "Re-checking skill disambiguation on best-guess input (stream)");
11284 match self.recheck_skill_disambiguation(&skill_id, &enriched_input).await {
11285 Ok(resp) => {
11286 yield StreamChunk::content(&resp.content);
11287 yield StreamChunk::Done {};
11288 return;
11289 }
11290 Err(e) => {
11291 yield StreamChunk::error(e.to_string());
11292 return;
11293 }
11294 }
11295 }
11296
11297 let mut inner = self.run_loop_internal_stream(&enriched_input);
11298 while let Some(chunk) = inner.next().await {
11299 yield chunk;
11300 }
11301 return;
11302 }
11303 DisambiguationResult::GiveUp { reason } => {
11304 *self.pending_skill_id.write() = None;
11305 warn!(reason = %reason, "Disambiguation gave up (stream)");
11306 let apology = self
11307 .generate_localized_apology(
11308 "Generate a brief, polite apology saying you couldn't understand the request. Be concise.",
11309 &reason,
11310 )
11311 .await
11312 .unwrap_or_else(|_| {
11313 format!("I'm sorry, I couldn't understand your request: {}", reason)
11314 });
11315 let response = AgentResponse::new(&apology);
11316 if let Err(e) = self.finish_turn_if_root(&response).await {
11317 yield StreamChunk::error(e.to_string());
11318 return;
11319 }
11320 yield StreamChunk::content(&apology);
11321 yield StreamChunk::Done {};
11322 return;
11323 }
11324 DisambiguationResult::Escalate { reason } => {
11325 *self.pending_skill_id.write() = None;
11326 info!(reason = %reason, "Escalating to human (stream)");
11327 if let Some(ref hitl) = self.hitl_engine {
11328 let trigger =
11329 ApprovalTrigger::condition("disambiguation_escalation", reason.clone());
11330 let mut context_map = HashMap::new();
11331 context_map.insert("original_input".to_string(), serde_json::json!(input));
11332 context_map.insert("reason".to_string(), serde_json::json!(&reason));
11333 let check_result = HITLCheckResult::required(
11334 trigger,
11335 context_map,
11336 format!("User request needs human assistance: {}", reason),
11337 Some(hitl.config().default_timeout_seconds),
11338 );
11339 match self.request_hitl_approval(check_result).await {
11340 Ok(ApprovalResult::Approved | ApprovalResult::Modified { .. }) => {
11341 let mut inner = self.run_loop_internal_stream(input);
11342 while let Some(chunk) = inner.next().await {
11343 yield chunk;
11344 }
11345 return;
11346 }
11347 Ok(_) => {}
11348 Err(e) => {
11349 yield StreamChunk::error(e.to_string());
11350 return;
11351 }
11352 }
11353 }
11354 let apology = self
11355 .generate_localized_apology(
11356 "Explain briefly that you're transferring the user to a human agent for help.",
11357 &reason,
11358 )
11359 .await
11360 .unwrap_or_else(|_| {
11361 format!("I need human assistance to help with your request: {}", reason)
11362 });
11363 let response = AgentResponse::new(&apology);
11364 if let Err(e) = self.finish_turn_if_root(&response).await {
11365 yield StreamChunk::error(e.to_string());
11366 return;
11367 }
11368 yield StreamChunk::content(&apology);
11369 yield StreamChunk::Done {};
11370 return;
11371 }
11372 DisambiguationResult::Abandoned { new_input } => {
11373 *self.pending_skill_id.write() = None;
11374
11375 info!(
11376 has_new_input = new_input.is_some(),
11377 "Clarification abandoned by user (stream)"
11378 );
11379
11380 if let Err(e) = self.commit_root_user_message(input).await {
11381 yield StreamChunk::error(e.to_string());
11382 return;
11383 }
11384
11385 match new_input {
11386 Some(fresh_input) => {
11387 let mut inner = self.run_loop_internal_stream(&fresh_input);
11389 while let Some(chunk) = inner.next().await {
11390 yield chunk;
11391 }
11392 return;
11393 }
11394 None => {
11395 let ack = self
11397 .generate_localized_apology(
11398 "The user changed their mind about their previous request. \
11399 Generate a brief, friendly acknowledgment (e.g. 'OK, no problem. What else can I help with?'). \
11400 Do NOT apologize excessively. Be concise.",
11401 "User abandoned clarification",
11402 )
11403 .await
11404 .unwrap_or_else(|_| {
11405 "OK, no problem. What else can I help with?".to_string()
11406 });
11407
11408 let _ = self
11409 .memory
11410 .add_message(ChatMessage::assistant(&ack))
11411 .await;
11412
11413 let response = AgentResponse::new(&ack);
11414 if let Err(e) = self.finish_turn_if_root(&response).await {
11415 yield StreamChunk::error(e.to_string());
11416 return;
11417 }
11418 yield StreamChunk::content(&ack);
11419 yield StreamChunk::Done {};
11420 return;
11421 }
11422 }
11423 }
11424 }
11425 }
11426
11427 let mut inner = self.run_loop_internal_stream(input);
11429 while let Some(chunk) = inner.next().await {
11430 yield chunk;
11431 }
11432 })
11433 }
11434
11435 pub fn info(&self) -> AgentInfo {
11436 self.info.clone()
11437 }
11438
11439 pub fn skills(&self) -> &[SkillDefinition] {
11440 &self.skills
11441 }
11442
11443 pub async fn reset(&self) -> Result<()> {
11444 self.memory.clear().await?;
11445 *self.iteration_count.write() = 0;
11446 self.tool_call_history.write().clear();
11447 *self.pending_skill_id.write() = None;
11448 if let Some(ref sm) = self.state_machine {
11449 sm.reset();
11450 }
11451 Ok(())
11452 }
11453
11454 pub fn max_context_tokens(&self) -> u32 {
11455 self.max_context_tokens
11456 }
11457
11458 pub fn llm_registry(&self) -> &Arc<LLMRegistry> {
11459 &self.llm_registry
11460 }
11461
11462 pub fn state_machine(&self) -> Option<&Arc<StateMachine>> {
11463 self.state_machine.as_ref()
11464 }
11465
11466 pub fn context_manager(&self) -> &Arc<ContextManager> {
11467 &self.context_manager
11468 }
11469
11470 pub fn tool_call_history(&self) -> Vec<ToolCallRecord> {
11471 self.tool_call_history.read().clone()
11472 }
11473
11474 pub fn memory_token_budget(&self) -> Option<&MemoryTokenBudget> {
11475 self.memory_token_budget.as_ref()
11476 }
11477
11478 pub fn parallel_tools_config(&self) -> &ParallelToolsConfig {
11479 &self.parallel_tools
11480 }
11481
11482 pub fn streaming_config(&self) -> &StreamingConfig {
11483 &self.streaming
11484 }
11485
11486 pub fn hooks(&self) -> &Arc<dyn AgentHooks> {
11487 &self.hooks
11488 }
11489
11490 pub fn hitl_engine(&self) -> Option<&HITLEngine> {
11491 self.hitl_engine.as_ref()
11492 }
11493
11494 pub fn approval_handler(&self) -> &Arc<dyn ApprovalHandler> {
11495 &self.approval_handler
11496 }
11497
11498 fn build_hitl_language_context(&self) -> HashMap<String, Value> {
11500 let mut ctx = HashMap::new();
11501 for key in &["user.language", "input.detected.language", "language"] {
11502 if let Some(val) = self.context_manager.get(key) {
11503 ctx.insert(key.to_string(), val);
11504 }
11505 }
11506 ctx
11507 }
11508
11509 async fn request_hitl_approval(&self, check_result: HITLCheckResult) -> Result<ApprovalResult> {
11511 let Some(request) = check_result.into_request() else {
11512 return Ok(ApprovalResult::Approved);
11513 };
11514
11515 self.hooks.on_approval_requested(&request).await;
11516
11517 let timeout = request.timeout;
11518
11519 let raw_result = if let Some(duration) = timeout {
11520 match tokio::time::timeout(
11521 duration,
11522 self.approval_handler.request_approval(request.clone()),
11523 )
11524 .await
11525 {
11526 Ok(result) => result,
11527 Err(_) => ApprovalResult::timeout(),
11528 }
11529 } else {
11530 self.approval_handler
11531 .request_approval(request.clone())
11532 .await
11533 };
11534
11535 self.hooks
11536 .on_approval_result(&request.id, &raw_result)
11537 .await;
11538
11539 let (outcome, effective_result): (ApprovalResolvedOutcome, Result<ApprovalResult>) =
11540 match &raw_result {
11541 ApprovalResult::Approved => (
11542 ApprovalResolvedOutcome::Approved,
11543 Ok(ApprovalResult::Approved),
11544 ),
11545 ApprovalResult::Rejected { reason } => (
11546 ApprovalResolvedOutcome::Rejected {
11547 reason: reason.clone(),
11548 },
11549 Ok(ApprovalResult::Rejected {
11550 reason: reason.clone(),
11551 }),
11552 ),
11553 ApprovalResult::Modified { changes } => (
11554 ApprovalResolvedOutcome::Modified {
11555 changes: changes.clone(),
11556 },
11557 Ok(ApprovalResult::Modified {
11558 changes: changes.clone(),
11559 }),
11560 ),
11561 ApprovalResult::Timeout => {
11562 if let Some(ref engine) = self.hitl_engine {
11563 match engine.config().on_timeout {
11564 TimeoutAction::Approve => (
11565 ApprovalResolvedOutcome::Approved,
11566 Ok(ApprovalResult::Approved),
11567 ),
11568 TimeoutAction::Reject => {
11569 let reason = Some("Timeout".to_string());
11570 (
11571 ApprovalResolvedOutcome::Rejected {
11572 reason: reason.clone(),
11573 },
11574 Ok(ApprovalResult::Rejected { reason }),
11575 )
11576 }
11577 TimeoutAction::Error => {
11578 let message = "HITL approval timeout".to_string();
11579 (
11580 ApprovalResolvedOutcome::Error {
11581 message: message.clone(),
11582 },
11583 Err(AgentError::Other(message)),
11584 )
11585 }
11586 }
11587 } else {
11588 let reason = Some("Timeout (no engine)".to_string());
11589 (
11590 ApprovalResolvedOutcome::Rejected {
11591 reason: reason.clone(),
11592 },
11593 Ok(ApprovalResult::Rejected { reason }),
11594 )
11595 }
11596 }
11597 };
11598
11599 self.hooks
11600 .on_approval_resolved(&request, &raw_result, &outcome)
11601 .await;
11602
11603 effective_result
11604 }
11605
11606 pub async fn check_state_hitl(&self, from: Option<&str>, to: &str) -> Result<bool> {
11607 if let Some(ref hitl_engine) = self.hitl_engine {
11608 let hitl_lang_ctx = self.build_hitl_language_context();
11609 let check_result = self
11610 .observe_purpose(
11611 ObservationPurpose::HitlLocalization,
11612 hitl_engine.check_state_transition_with_localization(
11613 from,
11614 to,
11615 &hitl_lang_ctx,
11616 self.approval_handler.as_ref(),
11617 Some(&self.llm_registry),
11618 ),
11619 )
11620 .await?;
11621 if check_result.is_required() {
11622 let result = self.request_hitl_approval(check_result).await?;
11623 return Ok(matches!(
11624 result,
11625 ApprovalResult::Approved | ApprovalResult::Modified { .. }
11626 ));
11627 }
11628 }
11629 Ok(true)
11630 }
11631
11632 async fn execute_tools_parallel(
11634 &self,
11635 tool_calls: &[ToolCall],
11636 ) -> Vec<(String, Result<String>)> {
11637 let can_run_parallel = tool_calls.iter().all(|tc| {
11638 self.tools
11639 .resolve(&tc.name)
11640 .map(|resolved| resolved.tool.classify_call(&tc.arguments).concurrency_safe)
11641 .unwrap_or(false)
11642 });
11643
11644 if !self.parallel_tools.enabled || tool_calls.len() <= 1 || !can_run_parallel {
11645 let mut results = Vec::new();
11646 for tc in tool_calls {
11647 let result = self
11648 .observe_purpose(
11649 current_observation_context()
11650 .map(|context| context.purpose)
11651 .unwrap_or_default(),
11652 self.execute_tool_smart(tc),
11653 )
11654 .await;
11655 results.push((tc.id.clone(), result));
11656 }
11657 return results;
11658 }
11659
11660 let chunks: Vec<_> = tool_calls
11661 .chunks(self.parallel_tools.max_parallel)
11662 .collect();
11663
11664 let mut all_results = Vec::new();
11665
11666 for chunk in chunks {
11667 let futures: Vec<_> = chunk
11668 .iter()
11669 .map(|tc| {
11670 let tc = tc.clone();
11671 async move {
11672 let result = self.execute_tool_smart(&tc).await;
11673 (tc.id.clone(), result)
11674 }
11675 })
11676 .collect();
11677
11678 let results = futures::future::join_all(futures).await;
11679 all_results.extend(results);
11680 }
11681
11682 all_results
11683 }
11684
11685 pub async fn chat_stream<'a>(
11687 &'a self,
11688 input: &'a str,
11689 ) -> Result<Pin<Box<dyn Stream<Item = StreamChunk> + Send + 'a>>> {
11690 self.init_storage().await?;
11694 info!(input_len = input.len(), "Starting streaming chat");
11695 let inner = self.run_loop_stream(input);
11696 if let Some(context) = self.build_observation_context(None) {
11697 let stream: Pin<Box<dyn Stream<Item = StreamChunk> + Send + 'a>> =
11698 Box::pin(async_stream::stream! {
11699 let mut inner = inner;
11700 loop {
11701 let next = with_observation_context(context.clone(), inner.next()).await;
11702 match next {
11703 Some(chunk) => yield chunk,
11704 None => break,
11705 }
11706 }
11707 self.export_observability_if_configured().await;
11708 });
11709 Ok(stream)
11710 } else {
11711 Ok(inner)
11712 }
11713 }
11714}
11715
11716#[async_trait]
11717impl ToolInvoker for RuntimeAgent {
11718 async fn invoke_tool(&self, request: ToolExecutionRequest) -> Result<ToolExecutionRecord> {
11719 self.execute_tool_record(request).await
11720 }
11721}
11722
11723#[async_trait]
11724impl Agent for RuntimeAgent {
11725 async fn chat(&self, input: &str) -> Result<AgentResponse> {
11726 let result = if let Some(context) = self.build_observation_context(None) {
11727 with_observation_context(context, self.run_loop(input)).await
11728 } else {
11729 self.run_loop(input).await
11730 };
11731 self.export_observability_if_configured().await;
11732 result
11733 }
11734
11735 fn info(&self) -> AgentInfo {
11736 self.info.clone()
11737 }
11738
11739 async fn reset(&self) -> Result<()> {
11740 self.memory.clear().await?;
11741 *self.iteration_count.write() = 0;
11742 self.tool_call_history.write().clear();
11743 if let Some(ref sm) = self.state_machine {
11744 sm.reset();
11745 }
11746 Ok(())
11747 }
11748}
11749
11750fn background_maintenance_tags(
11760 label: &str,
11761 stage: &str,
11762 reason: Option<&str>,
11763 policy: Option<&crate::optimization::config::MaintenanceTaskPolicy>,
11764) -> HashMap<String, String> {
11765 let mut tags = HashMap::new();
11766 tags.insert("runtime.background".to_string(), "true".to_string());
11767 tags.insert("runtime.maintenance".to_string(), label.to_string());
11768 tags.insert("runtime.maintenance_stage".to_string(), stage.to_string());
11769 if let Some(policy) = policy {
11770 tags.insert(
11771 "runtime.await_before_next_turn".to_string(),
11772 await_before_next_turn_label(policy.await_before_next_turn).to_string(),
11773 );
11774 tags.insert(
11775 "runtime.maintenance_mode".to_string(),
11776 maintenance_mode_label(policy.mode).to_string(),
11777 );
11778 }
11779 if let Some(reason) = reason {
11780 tags.insert("runtime.reason".to_string(), reason.to_string());
11781 }
11782 tags
11783}
11784
11785fn await_before_next_turn_label(policy: AwaitBeforeNextTurn) -> &'static str {
11786 match policy {
11787 AwaitBeforeNextTurn::Never => "never",
11788 AwaitBeforeNextTurn::SameActor => "same_actor",
11789 AwaitBeforeNextTurn::Always => "always",
11790 }
11791}
11792
11793fn maintenance_mode_label(mode: MaintenanceMode) -> &'static str {
11794 match mode {
11795 MaintenanceMode::InlineSerial => "inline_serial",
11796 MaintenanceMode::InlineParallel => "inline_parallel",
11797 MaintenanceMode::Background => "background",
11798 }
11799}
11800
11801fn record_background_maintenance_event(
11803 manager: Option<&Arc<ObservabilityManager>>,
11804 label: &str,
11805 status: EventStatus,
11806 duration_ms: u64,
11807 stage: &str,
11808 reason: Option<String>,
11809 policy: Option<&crate::optimization::config::MaintenanceTaskPolicy>,
11810) {
11811 if let Some(manager) = manager {
11812 manager.record_lifecycle_event(
11813 EventType::MemoryOperation {
11814 operation: format!("{}_background_{}", label, stage),
11815 },
11816 ObservationPurpose::Other(format!("{}_maintenance", label)),
11817 status,
11818 duration_ms,
11819 background_maintenance_tags(label, stage, reason.as_deref(), policy),
11820 None,
11821 );
11822 }
11823}
11824
11825fn effective_maintenance_mode(mode: MaintenanceMode, force_parallel: bool) -> MaintenanceMode {
11826 if force_parallel && matches!(mode, MaintenanceMode::InlineSerial) {
11827 MaintenanceMode::InlineParallel
11828 } else {
11829 mode
11830 }
11831}
11832
11833fn observation_purpose_for_process(hint: ProcessPurposeHint) -> ObservationPurpose {
11834 match hint {
11835 ProcessPurposeHint::Detect => ObservationPurpose::ProcessDetect,
11836 ProcessPurposeHint::Extract => ObservationPurpose::ProcessExtract,
11837 ProcessPurposeHint::Validate => ObservationPurpose::ProcessValidate,
11838 ProcessPurposeHint::Transform | ProcessPurposeHint::Other => {
11839 ObservationPurpose::ProcessTransform
11840 }
11841 }
11842}
11843
11844fn new_tool_resource_locks() -> ToolResourceLocks {
11845 Arc::new(RwLock::new(HashMap::new()))
11846}
11847
11848fn tool_resource_lock_keys(
11853 _canonical_id: &str,
11854 args: &Value,
11855 bindings: &ai_agents_core::ToolPolicyBindings,
11856 classification: &ai_agents_core::ToolCallClassification,
11857) -> Vec<String> {
11858 if classification.concurrency_safe {
11859 return Vec::new();
11860 }
11861
11862 let mut keys = Vec::new();
11863 let mut has_path_resource = false;
11864 for binding in &bindings.path_fields {
11865 let value = value_at_argument_path(args, &binding.field)
11866 .cloned()
11867 .or_else(|| {
11868 binding
11869 .default_path
11870 .as_ref()
11871 .map(|path| Value::String(path.clone()))
11872 });
11873 if let Some(value) = value {
11874 collect_resource_strings(&value, |_| {
11875 has_path_resource = true;
11876 });
11877 }
11878 }
11879 for binding in &bindings.domain_fields {
11880 if let Some(value) = value_at_argument_path(args, &binding.field) {
11881 collect_resource_strings(value, |domain| {
11882 let normalized = if binding.is_url {
11883 normalized_url_resource_key(domain)
11884 } else {
11885 domain.trim().trim_end_matches('.').to_ascii_lowercase()
11886 };
11887 keys.push(format!("domain:{}", normalized));
11888 });
11889 }
11890 }
11891 for binding in &bindings.command_fields {
11892 if !matches!(binding.kind, ai_agents_core::CommandBindingKind::Cwd) {
11893 continue;
11894 }
11895 if let Some(value) = value_at_argument_path(args, &binding.field) {
11896 collect_resource_strings(value, |_| {
11897 has_path_resource = true;
11898 });
11899 }
11900 }
11901 if has_path_resource {
11902 keys.push("path-mutation:global".to_string());
11903 }
11904 if keys.is_empty() {
11905 keys.push("side-effect:unbound".to_string());
11906 }
11907 keys.sort();
11908 keys.dedup();
11909 keys
11910}
11911
11912fn value_at_argument_path<'a>(value: &'a Value, field: &str) -> Option<&'a Value> {
11913 let mut current = value;
11914 for segment in field.split('.') {
11915 if segment.is_empty() {
11916 return None;
11917 }
11918 current = current.get(segment)?;
11919 }
11920 Some(current)
11921}
11922
11923fn collect_resource_strings(value: &Value, mut collect: impl FnMut(&str)) {
11924 match value {
11925 Value::String(value) => collect(value),
11926 Value::Array(values) => {
11927 for value in values {
11928 if let Some(value) = value.as_str() {
11929 collect(value);
11930 }
11931 }
11932 }
11933 _ => {}
11934 }
11935}
11936
11937fn normalized_url_resource_key(value: &str) -> String {
11938 let value = value.trim();
11939 let Some((scheme, remainder)) = value.split_once("://") else {
11940 return value.to_ascii_lowercase();
11941 };
11942 let authority_end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len());
11943 let (authority, suffix) = remainder.split_at(authority_end);
11944 format!(
11945 "{}://{}{}",
11946 scheme.to_ascii_lowercase(),
11947 authority.to_ascii_lowercase(),
11948 suffix
11949 )
11950}
11951
11952fn render_concurrent_template(
11953 template: &str,
11954 user_input: &str,
11955 context_values: &std::collections::HashMap<String, serde_json::Value>,
11956) -> Result<String> {
11957 let mut env = minijinja::Environment::new();
11958 env.add_template("concurrent", template)
11959 .map_err(|e| AgentError::Other(format!("Concurrent template parse error: {}", e)))?;
11960
11961 let mut ctx = std::collections::BTreeMap::new();
11962 ctx.insert("user_input".to_string(), minijinja::Value::from(user_input));
11963
11964 let context_obj = minijinja::Value::from_serialize(context_values);
11966 ctx.insert("context".to_string(), context_obj);
11967
11968 let tmpl = env
11969 .get_template("concurrent")
11970 .map_err(|e| AgentError::Other(format!("Concurrent template error: {}", e)))?;
11971
11972 tmpl.render(minijinja::Value::from_serialize(&ctx))
11973 .map_err(|e| AgentError::Other(format!("Concurrent template render error: {}", e)))
11974}
11975
11976#[cfg(test)]
11977mod tests {
11978 use super::*;
11979 use crate::AgentBuilder;
11980 use ai_agents_core::{LLMChunk, LLMConfig, LLMError, LLMFeature, Tool};
11981 use ai_agents_llm::mock::MockLLMProvider;
11982 use ai_agents_tools::{
11983 CalculatorTool, CopyPathTool, DeletePathTool, FileWriteTool, MovePathTool,
11984 };
11985
11986 fn mock_with_response(response: &str) -> MockLLMProvider {
11987 let mut mock = MockLLMProvider::new("test");
11988 mock.set_response(response);
11989 mock
11990 }
11991
11992 fn mock_with_responses(responses: Vec<&str>) -> MockLLMProvider {
11993 let mut mock = MockLLMProvider::new("test");
11994 mock.set_responses(responses.into_iter().map(String::from).collect(), true);
11995 mock
11996 }
11997
11998 #[tokio::test]
11999 async fn native_required_choice_executes_through_the_shared_tool_path() {
12000 let mut mock = MockLLMProvider::new("native-required");
12001 mock.set_tool_choice(Some(ToolChoice::Required));
12002 mock.add_response(
12003 LLMResponse::new("", FinishReason::ToolCall)
12004 .with_tool_calls(vec![ToolCall {
12005 id: "provider-call-1".to_string(),
12006 name: "calculator".to_string(),
12007 arguments: serde_json::json!({"expression": "2 + 2"}),
12008 }])
12009 .unwrap(),
12010 );
12011 mock.add_response(LLMResponse::new("The answer is 4.", FinishReason::Stop));
12012 let observed = mock.clone();
12013 let agent = AgentBuilder::new()
12014 .system_prompt("Use the calculator when needed.")
12015 .llm(Arc::new(mock))
12016 .tool(Arc::new(CalculatorTool::new()))
12017 .build()
12018 .unwrap();
12019
12020 let response = agent.chat("What is 2 + 2?").await.unwrap();
12021
12022 assert_eq!(response.content, "The answer is 4.");
12023 assert_eq!(
12024 response.tool_calls.as_ref().unwrap()[0].id,
12025 "provider-call-1"
12026 );
12027 let calls = observed.call_history();
12028 assert_eq!(calls.len(), 2);
12029 assert!(matches!(
12030 calls[0].request.as_ref().map(|request| &request.choice),
12031 Some(ToolChoice::Required)
12032 ));
12033 assert!(matches!(
12034 calls[1].request.as_ref().map(|request| &request.choice),
12035 Some(ToolChoice::Auto)
12036 ));
12037 }
12038
12039 #[tokio::test]
12040 async fn prompt_fallback_uses_one_corrective_retry() {
12041 let mut mock = MockLLMProvider::new("prompt-required");
12042 mock.set_tool_choice(Some(ToolChoice::Required));
12043 mock.set_native_tool_support(false);
12044 mock.set_responses(
12045 vec![
12046 "I can calculate that.".to_string(),
12047 r#"{"tool":"calculator","arguments":{"expression":"2 + 2"}}"#.to_string(),
12048 "The answer is 4.".to_string(),
12049 ],
12050 false,
12051 );
12052 let observed = mock.clone();
12053 let agent = AgentBuilder::new()
12054 .system_prompt("Use tools.")
12055 .llm(Arc::new(mock))
12056 .tool(Arc::new(CalculatorTool::new()))
12057 .build()
12058 .unwrap();
12059
12060 let response = agent.chat("What is 2 + 2?").await.unwrap();
12061
12062 assert_eq!(response.content, "The answer is 4.");
12063 assert_eq!(observed.call_count(), 3);
12064 let corrective = &observed.call_history()[1].messages;
12065 assert!(
12066 corrective
12067 .last()
12068 .unwrap()
12069 .content
12070 .contains("previous response")
12071 );
12072 }
12073
12074 #[tokio::test]
12075 async fn prompt_fallback_fails_after_one_noncompliant_retry() {
12076 let mut mock = MockLLMProvider::new("prompt-required-failure");
12077 mock.set_tool_choice(Some(ToolChoice::Required));
12078 mock.set_native_tool_support(false);
12079 mock.set_responses(
12080 vec!["No tool.".to_string(), "Still no tool.".to_string()],
12081 false,
12082 );
12083 let observed = mock.clone();
12084 let agent = AgentBuilder::new()
12085 .system_prompt("Use tools.")
12086 .llm(Arc::new(mock))
12087 .tool(Arc::new(CalculatorTool::new()))
12088 .build()
12089 .unwrap();
12090
12091 let error = agent.chat("What is 2 + 2?").await.unwrap_err();
12092
12093 assert!(error.to_string().contains("one corrective retry"));
12094 assert_eq!(observed.call_count(), 2);
12095 }
12096
12097 #[tokio::test]
12098 async fn specific_choice_cannot_widen_the_effective_grant() {
12099 let mut mock = MockLLMProvider::new("specific-outside-grant");
12100 mock.set_tool_choice(Some(ToolChoice::Specific("random".to_string())));
12101 let observed = mock.clone();
12102 let agent = AgentBuilder::new()
12103 .system_prompt("Use tools.")
12104 .llm(Arc::new(mock))
12105 .tool(Arc::new(CalculatorTool::new()))
12106 .build()
12107 .unwrap();
12108
12109 let error = agent.chat("Generate a value.").await.unwrap_err();
12110
12111 assert!(error.to_string().contains("is not registered"));
12112 assert_eq!(observed.call_count(), 0);
12113 }
12114
12115 #[tokio::test]
12116 async fn none_choice_exposes_no_tool_protocol() {
12117 let mut mock = MockLLMProvider::new("no-tools");
12118 mock.set_tool_choice(Some(ToolChoice::None));
12119 mock.set_response(r#"{"tool":"calculator","arguments":{"expression":"2 + 2"}}"#);
12120 let observed = mock.clone();
12121 let agent = AgentBuilder::new()
12122 .system_prompt("Answer directly.")
12123 .llm(Arc::new(mock))
12124 .tool(Arc::new(CalculatorTool::new()))
12125 .build()
12126 .unwrap();
12127
12128 let response = agent.chat("Hello").await.unwrap();
12129
12130 assert!(response.tool_calls.is_none());
12131 assert_eq!(observed.call_count(), 1);
12132 let call = observed.last_call().unwrap();
12133 assert!(call.request.is_none());
12134 assert!(
12135 call.messages
12136 .iter()
12137 .all(|message| !message.content.contains("Available tools:"))
12138 );
12139 }
12140
12141 struct RuntimeStorage {
12142 capabilities: Box<[StorageCapability]>,
12143 snapshots: RwLock<HashMap<String, AgentSnapshot>>,
12144 metadata: RwLock<HashMap<String, ai_agents_core::SessionMetadata>>,
12145 metadata_save_calls: AtomicU64,
12146 metadata_load_calls: AtomicU64,
12147 fail_metadata_save: AtomicBool,
12148 fail_metadata_load: AtomicBool,
12149 }
12150
12151 impl RuntimeStorage {
12152 fn new(capabilities: impl IntoIterator<Item = StorageCapability>) -> Self {
12153 Self {
12154 capabilities: capabilities.into_iter().collect(),
12155 snapshots: RwLock::new(HashMap::new()),
12156 metadata: RwLock::new(HashMap::new()),
12157 metadata_save_calls: AtomicU64::new(0),
12158 metadata_load_calls: AtomicU64::new(0),
12159 fail_metadata_save: AtomicBool::new(false),
12160 fail_metadata_load: AtomicBool::new(false),
12161 }
12162 }
12163 }
12164
12165 #[async_trait]
12166 impl AgentStorage for RuntimeStorage {
12167 fn supports(&self, capability: StorageCapability) -> bool {
12168 self.capabilities.contains(&capability)
12169 }
12170
12171 async fn save(&self, session_id: &str, snapshot: &AgentSnapshot) -> Result<()> {
12172 self.snapshots
12173 .write()
12174 .insert(session_id.to_string(), snapshot.clone());
12175 Ok(())
12176 }
12177
12178 async fn load(&self, session_id: &str) -> Result<Option<AgentSnapshot>> {
12179 Ok(self.snapshots.read().get(session_id).cloned())
12180 }
12181
12182 async fn delete(&self, session_id: &str) -> Result<()> {
12183 self.snapshots.write().remove(session_id);
12184 Ok(())
12185 }
12186
12187 async fn list_sessions(&self) -> Result<Vec<String>> {
12188 Ok(self.snapshots.read().keys().cloned().collect())
12189 }
12190
12191 async fn save_snapshot_with_metadata(
12192 &self,
12193 session_id: &str,
12194 snapshot: &AgentSnapshot,
12195 metadata: &ai_agents_core::SessionMetadata,
12196 ) -> Result<()> {
12197 self.metadata_save_calls.fetch_add(1, Ordering::SeqCst);
12198 if self.fail_metadata_save.load(Ordering::SeqCst) {
12199 return Err(AgentError::Persistence("metadata save failed".into()));
12200 }
12201 self.snapshots
12202 .write()
12203 .insert(session_id.to_string(), snapshot.clone());
12204 self.metadata
12205 .write()
12206 .insert(session_id.to_string(), metadata.clone());
12207 Ok(())
12208 }
12209
12210 async fn save_metadata(
12211 &self,
12212 session_id: &str,
12213 metadata: &ai_agents_core::SessionMetadata,
12214 ) -> Result<()> {
12215 self.metadata_save_calls.fetch_add(1, Ordering::SeqCst);
12216 if self.fail_metadata_save.load(Ordering::SeqCst) {
12217 return Err(AgentError::Persistence("metadata save failed".into()));
12218 }
12219 self.metadata
12220 .write()
12221 .insert(session_id.to_string(), metadata.clone());
12222 Ok(())
12223 }
12224
12225 async fn load_metadata(
12226 &self,
12227 session_id: &str,
12228 ) -> Result<Option<ai_agents_core::SessionMetadata>> {
12229 self.metadata_load_calls.fetch_add(1, Ordering::SeqCst);
12230 if self.fail_metadata_load.load(Ordering::SeqCst) {
12231 return Err(AgentError::Persistence("metadata load failed".into()));
12232 }
12233 Ok(self.metadata.read().get(session_id).cloned())
12234 }
12235 }
12236
12237 fn runtime_storage_agent() -> RuntimeAgent {
12238 AgentBuilder::new()
12239 .system_prompt("Test runtime storage integration.")
12240 .llm(Arc::new(mock_with_response("done")))
12241 .build()
12242 .unwrap()
12243 }
12244
12245 fn restore_spec(id: &str) -> crate::spec::AgentSpec {
12246 crate::spec::AgentSpec {
12247 name: id.to_string(),
12248 system_prompt: format!("Restore child {id}."),
12249 ..crate::spec::AgentSpec::default()
12250 }
12251 }
12252
12253 fn restore_entry(id: &str) -> ai_agents_core::SpawnedAgentEntry {
12254 ai_agents_core::SpawnedAgentEntry {
12255 id: id.to_string(),
12256 name: id.to_string(),
12257 spec_yaml: serde_yaml::to_string(&restore_spec(id)).unwrap(),
12258 }
12259 }
12260
12261 fn restore_spawner(
12262 storage: Arc<RuntimeStorage>,
12263 max_agents: usize,
12264 ) -> (
12265 Arc<crate::spawner::AgentSpawner>,
12266 Arc<crate::spawner::AgentRegistry>,
12267 ) {
12268 let mut llms = LLMRegistry::new();
12269 llms.register("default", Arc::new(mock_with_response("done")));
12270 (
12271 Arc::new(
12272 crate::spawner::AgentSpawner::new()
12273 .with_shared_llms(llms)
12274 .with_shared_storage(storage)
12275 .with_max_agents(max_agents),
12276 ),
12277 Arc::new(crate::spawner::AgentRegistry::new()),
12278 )
12279 }
12280
12281 async fn save_restore_target(
12282 parent: &RuntimeAgent,
12283 storage: &RuntimeStorage,
12284 session_id: &str,
12285 entries: Vec<ai_agents_core::SpawnedAgentEntry>,
12286 ) {
12287 let mut snapshot = parent.save_state().await.unwrap();
12288 snapshot.spawned_agents = Some(entries);
12289 storage.save(session_id, &snapshot).await.unwrap();
12290 storage
12291 .save_metadata(session_id, &ai_agents_core::SessionMetadata::default())
12292 .await
12293 .unwrap();
12294 }
12295
12296 #[tokio::test]
12297 async fn storage_init_requires_storage_for_actor_facts() {
12298 let facts = ai_agents_facts::FactsConfig {
12299 enabled: true,
12300 ..Default::default()
12301 };
12302 let agent = runtime_storage_agent().with_facts_config(None, Some(facts));
12303
12304 let error = agent.init_storage().await.unwrap_err();
12305 assert!(matches!(
12306 error,
12307 AgentError::Config(message)
12308 if message.contains("actor facts or actor memory")
12309 && message.contains("none is configured or injected")
12310 ));
12311 }
12312
12313 #[tokio::test]
12314 async fn storage_init_validates_actor_facts_capability() {
12315 let storage = Arc::new(RuntimeStorage::new([StorageCapability::Snapshot]));
12316 let actor_memory = ai_agents_facts::ActorMemoryConfig {
12317 enabled: true,
12318 ..Default::default()
12319 };
12320 let agent = runtime_storage_agent()
12321 .with_storage(storage)
12322 .with_facts_config(Some(actor_memory), None);
12323
12324 assert!(matches!(
12325 agent.init_storage().await,
12326 Err(AgentError::UnsupportedStorageCapability(
12327 StorageCapability::ActorFacts
12328 ))
12329 ));
12330 }
12331
12332 #[tokio::test]
12333 async fn blocking_chat_rejects_unsupported_required_storage() {
12334 let storage = Arc::new(RuntimeStorage::new([StorageCapability::Snapshot]));
12335 let facts = ai_agents_facts::FactsConfig {
12336 enabled: true,
12337 ..Default::default()
12338 };
12339 let agent = runtime_storage_agent()
12340 .with_storage(storage)
12341 .with_facts_config(None, Some(facts));
12342
12343 assert!(matches!(
12344 agent.chat("hello").await,
12345 Err(AgentError::UnsupportedStorageCapability(
12346 StorageCapability::ActorFacts
12347 ))
12348 ));
12349 }
12350
12351 #[tokio::test]
12352 async fn streaming_chat_rejects_unsupported_required_storage_before_stream_creation() {
12353 let storage = Arc::new(RuntimeStorage::new([StorageCapability::Snapshot]));
12354 let config = ai_agents_relationships::RelationshipConfig {
12355 enabled: true,
12356 ..Default::default()
12357 };
12358 let manager = Arc::new(RelationshipManager::from_config(config).unwrap());
12359 let agent = runtime_storage_agent()
12360 .with_storage(storage)
12361 .with_relationships(manager);
12362
12363 assert!(matches!(
12364 agent.chat_stream("hello").await,
12365 Err(AgentError::UnsupportedStorageCapability(
12366 StorageCapability::ActorRelationships
12367 ))
12368 ));
12369 }
12370
12371 #[tokio::test]
12372 async fn storage_init_completes_facts_for_injected_storage() {
12373 let storage = Arc::new(RuntimeStorage::new([
12374 StorageCapability::Snapshot,
12375 StorageCapability::ActorFacts,
12376 ]));
12377 let facts = ai_agents_facts::FactsConfig {
12378 enabled: true,
12379 ..Default::default()
12380 };
12381 let agent = runtime_storage_agent()
12382 .with_storage(storage)
12383 .with_facts_config(None, Some(facts));
12384
12385 agent.init_storage().await.unwrap();
12386 assert!(agent.fact_store().is_some());
12387 }
12388
12389 #[tokio::test]
12390 async fn storage_init_requires_storage_for_persistent_relationships() {
12391 let config = ai_agents_relationships::RelationshipConfig {
12392 enabled: true,
12393 ..Default::default()
12394 };
12395 let manager = Arc::new(RelationshipManager::from_config(config).unwrap());
12396 let agent = runtime_storage_agent().with_relationships(manager);
12397
12398 let error = agent.init_storage().await.unwrap_err();
12399 assert!(matches!(
12400 error,
12401 AgentError::Config(message)
12402 if message.contains("persistent relationships")
12403 && message.contains("none is configured or injected")
12404 ));
12405 }
12406
12407 #[tokio::test]
12408 async fn storage_init_validates_persistent_relationships_capability() {
12409 let storage = Arc::new(RuntimeStorage::new([StorageCapability::Snapshot]));
12410 let config = ai_agents_relationships::RelationshipConfig {
12411 enabled: true,
12412 ..Default::default()
12413 };
12414 let manager = Arc::new(RelationshipManager::from_config(config).unwrap());
12415 let agent = runtime_storage_agent()
12416 .with_storage(storage)
12417 .with_relationships(manager);
12418
12419 assert!(matches!(
12420 agent.init_storage().await,
12421 Err(AgentError::UnsupportedStorageCapability(
12422 StorageCapability::ActorRelationships
12423 ))
12424 ));
12425 }
12426
12427 #[tokio::test]
12428 async fn session_restore_updates_identity_and_clears_stale_actor_binding() {
12429 let storage = Arc::new(RuntimeStorage::new([
12430 StorageCapability::Snapshot,
12431 StorageCapability::SessionMetadata,
12432 ]));
12433 let agent = runtime_storage_agent().with_storage(storage.clone());
12434 agent.set_actor_id("old-actor").unwrap();
12435 agent.save_session("old").await.unwrap();
12436 storage
12437 .save("target", &agent.save_state().await.unwrap())
12438 .await
12439 .unwrap();
12440 storage
12441 .save_metadata("target", &ai_agents_core::SessionMetadata::default())
12442 .await
12443 .unwrap();
12444
12445 assert!(agent.load_session("target").await.unwrap());
12446
12447 assert_eq!(agent.current_session_id.read().as_deref(), Some("target"));
12448 assert_eq!(agent.actor_id(), None);
12449 }
12450
12451 #[tokio::test]
12452 async fn complete_restore_reconciles_growth_shrink_and_empty_topologies() {
12453 let storage = Arc::new(RuntimeStorage::new([
12454 StorageCapability::Snapshot,
12455 StorageCapability::SessionMetadata,
12456 ]));
12457 let (spawner, registry) = restore_spawner(storage.clone(), 3);
12458 let parent = runtime_storage_agent()
12459 .with_storage(storage.clone())
12460 .with_spawner_handles(Arc::clone(&spawner), Arc::clone(®istry));
12461
12462 for id in ["a", "b"] {
12463 let spawned = spawner
12464 .spawn_with_id(id.to_string(), restore_spec(id))
12465 .await
12466 .unwrap();
12467 spawned.agent.save_session("grow").await.unwrap();
12468 registry.register(spawned).await.unwrap();
12469 }
12470 let staged_c = crate::spawner::storage::NamespacedStorage::new(storage.clone(), "c");
12471 staged_c
12472 .save("grow", &AgentSnapshot::new("c".into()))
12473 .await
12474 .unwrap();
12475 staged_c
12476 .save_metadata("grow", &ai_agents_core::SessionMetadata::default())
12477 .await
12478 .unwrap();
12479 save_restore_target(
12480 &parent,
12481 storage.as_ref(),
12482 "grow",
12483 vec![restore_entry("a"), restore_entry("b"), restore_entry("c")],
12484 )
12485 .await;
12486
12487 assert_eq!(parent.restore_session_full("grow").await.unwrap(), 3);
12488 assert_eq!(registry.count(), 3);
12489 assert!(registry.contains("c"));
12490 assert_eq!(spawner.spawned_count(), 3);
12491
12492 for id in ["a", "b"] {
12493 registry
12494 .get(id)
12495 .unwrap()
12496 .save_session("shrink")
12497 .await
12498 .unwrap();
12499 }
12500 save_restore_target(
12501 &parent,
12502 storage.as_ref(),
12503 "shrink",
12504 vec![restore_entry("a"), restore_entry("b")],
12505 )
12506 .await;
12507
12508 assert_eq!(parent.restore_session_full("shrink").await.unwrap(), 2);
12509 assert_eq!(registry.count(), 2);
12510 assert!(!registry.contains("c"));
12511 assert_eq!(spawner.spawned_count(), 2);
12512
12513 save_restore_target(&parent, storage.as_ref(), "empty", Vec::new()).await;
12514
12515 assert_eq!(parent.restore_session_full("empty").await.unwrap(), 0);
12516 assert_eq!(registry.count(), 0);
12517 assert_eq!(spawner.spawned_count(), 0);
12518 assert_eq!(parent.current_session_id.read().as_deref(), Some("empty"));
12519 }
12520
12521 #[tokio::test]
12522 async fn storage_session_metadata_is_called_only_when_advertised() {
12523 let storage = Arc::new(RuntimeStorage::new([StorageCapability::Snapshot]));
12524 storage.fail_metadata_save.store(true, Ordering::SeqCst);
12525 storage.fail_metadata_load.store(true, Ordering::SeqCst);
12526 let agent = runtime_storage_agent().with_storage(storage.clone());
12527
12528 agent.save_session("session").await.unwrap();
12529 assert!(agent.load_session("session").await.unwrap());
12530 assert_eq!(storage.metadata_save_calls.load(Ordering::SeqCst), 0);
12531 assert_eq!(storage.metadata_load_calls.load(Ordering::SeqCst), 0);
12532 }
12533
12534 #[cfg(feature = "sqlite")]
12535 #[tokio::test]
12536 async fn sqlite_runtime_save_filter_reopen_and_reload_stay_consistent() {
12537 let directory =
12538 std::env::temp_dir().join(format!("ai-agents-runtime-sqlite-{}", uuid::Uuid::new_v4()));
12539 let path = directory.join("sessions.sqlite");
12540 let path_string = path.to_string_lossy().into_owned();
12541 let storage = Arc::new(
12542 ai_agents_storage::SqliteStorage::new(&path_string)
12543 .await
12544 .unwrap(),
12545 );
12546 let agent = runtime_storage_agent().with_storage(storage.clone());
12547 agent.set_session_metadata(ai_agents_core::SessionMetadata {
12548 tags: vec!["initial".into()],
12549 ..Default::default()
12550 });
12551 agent.chat("persist this turn").await.unwrap();
12552 agent.save_session("session").await.unwrap();
12553
12554 agent.set_session_metadata(ai_agents_core::SessionMetadata {
12555 tags: vec!["updated".into()],
12556 ..Default::default()
12557 });
12558 agent.save_session("session").await.unwrap();
12559 assert!(
12560 agent
12561 .list_sessions_filtered(&ai_agents_core::SessionFilter {
12562 tags: Some(vec!["initial".into()]),
12563 ..Default::default()
12564 })
12565 .await
12566 .unwrap()
12567 .is_empty()
12568 );
12569 assert_eq!(
12570 agent
12571 .list_sessions_filtered(&ai_agents_core::SessionFilter {
12572 tags: Some(vec!["updated".into()]),
12573 ..Default::default()
12574 })
12575 .await
12576 .unwrap()
12577 .len(),
12578 1
12579 );
12580 drop(agent);
12581 storage.close().await;
12582 drop(storage);
12583
12584 let reopened_storage = Arc::new(
12585 ai_agents_storage::SqliteStorage::new(&path_string)
12586 .await
12587 .unwrap(),
12588 );
12589 let restored = runtime_storage_agent().with_storage(reopened_storage.clone());
12590 assert!(restored.load_session("session").await.unwrap());
12591 assert_eq!(restored.session_metadata().tags, vec!["updated"]);
12592 assert_eq!(
12593 restored.current_session_id.read().as_deref(),
12594 Some("session")
12595 );
12596 assert!(restored.save_state().await.unwrap().memory.messages.len() >= 2);
12597 assert_eq!(
12598 restored
12599 .list_sessions_filtered(&ai_agents_core::SessionFilter {
12600 tags: Some(vec!["updated".into()]),
12601 ..Default::default()
12602 })
12603 .await
12604 .unwrap()
12605 .len(),
12606 1
12607 );
12608
12609 drop(restored);
12610 reopened_storage.close().await;
12611 drop(reopened_storage);
12612 std::fs::remove_dir_all(directory).unwrap();
12613 }
12614
12615 #[tokio::test]
12616 async fn storage_session_metadata_backend_failures_propagate() {
12617 let storage = Arc::new(RuntimeStorage::new([
12618 StorageCapability::Snapshot,
12619 StorageCapability::SessionMetadata,
12620 ]));
12621 let agent = runtime_storage_agent().with_storage(storage.clone());
12622
12623 agent.save_session("session").await.unwrap();
12624 storage
12625 .save("target", &agent.save_state().await.unwrap())
12626 .await
12627 .unwrap();
12628 storage.fail_metadata_load.store(true, Ordering::SeqCst);
12629 assert!(matches!(
12630 agent.load_session("target").await,
12631 Err(AgentError::Persistence(message)) if message == "metadata load failed"
12632 ));
12633 assert_eq!(agent.current_session_id.read().as_deref(), Some("session"));
12634
12635 storage.fail_metadata_save.store(true, Ordering::SeqCst);
12636 assert!(matches!(
12637 agent.save_session("session").await,
12638 Err(AgentError::Persistence(message)) if message == "metadata save failed"
12639 ));
12640 }
12641
12642 struct ProviderFutureDropSignal {
12643 dropped: Arc<AtomicBool>,
12644 }
12645
12646 impl Drop for ProviderFutureDropSignal {
12647 fn drop(&mut self) {
12648 self.dropped.store(true, Ordering::SeqCst);
12649 }
12650 }
12651
12652 struct BufferedLockingProvider {
12653 lock: Arc<tokio::sync::Mutex<()>>,
12654 stream_started: Arc<tokio::sync::Notify>,
12655 stream_dropped: Arc<AtomicBool>,
12656 committed_after_drop: Arc<AtomicBool>,
12657 }
12658
12659 #[async_trait]
12660 impl LLMProvider for BufferedLockingProvider {
12661 async fn complete(
12662 &self,
12663 _messages: &[ChatMessage],
12664 _config: Option<&LLMConfig>,
12665 ) -> std::result::Result<LLMResponse, LLMError> {
12666 let _guard = self.lock.lock().await;
12667 self.committed_after_drop
12668 .store(self.stream_dropped.load(Ordering::SeqCst), Ordering::SeqCst);
12669 Ok(LLMResponse::new(
12670 "Committed technical response.",
12671 FinishReason::Stop,
12672 ))
12673 }
12674
12675 async fn complete_stream(
12676 &self,
12677 _messages: &[ChatMessage],
12678 _config: Option<&LLMConfig>,
12679 ) -> std::result::Result<
12680 Box<dyn Stream<Item = std::result::Result<LLMChunk, LLMError>> + Unpin + Send>,
12681 LLMError,
12682 > {
12683 let _guard = self.lock.lock().await;
12684 let _drop_signal = ProviderFutureDropSignal {
12685 dropped: Arc::clone(&self.stream_dropped),
12686 };
12687 self.stream_started.notify_one();
12688 std::future::pending().await
12689 }
12690
12691 fn provider_name(&self) -> &str {
12692 "buffered-locking"
12693 }
12694
12695 fn supports(&self, _feature: LLMFeature) -> bool {
12696 false
12697 }
12698 }
12699
12700 struct PendingDropStream {
12701 dropped: Arc<AtomicBool>,
12702 dropped_notify: Arc<tokio::sync::Notify>,
12703 }
12704
12705 impl Stream for PendingDropStream {
12706 type Item = std::result::Result<LLMChunk, LLMError>;
12707
12708 fn poll_next(
12709 self: Pin<&mut Self>,
12710 _cx: &mut std::task::Context<'_>,
12711 ) -> std::task::Poll<Option<Self::Item>> {
12712 std::task::Poll::Pending
12713 }
12714 }
12715
12716 impl Drop for PendingDropStream {
12717 fn drop(&mut self) {
12718 self.dropped.store(true, Ordering::SeqCst);
12719 self.dropped_notify.notify_one();
12720 }
12721 }
12722
12723 struct EstablishedStreamProvider {
12724 stream_started: Arc<tokio::sync::Notify>,
12725 stream_dropped: Arc<AtomicBool>,
12726 stream_dropped_notify: Arc<tokio::sync::Notify>,
12727 committed_after_drop: Arc<AtomicBool>,
12728 }
12729
12730 #[async_trait]
12731 impl LLMProvider for EstablishedStreamProvider {
12732 async fn complete(
12733 &self,
12734 _messages: &[ChatMessage],
12735 _config: Option<&LLMConfig>,
12736 ) -> std::result::Result<LLMResponse, LLMError> {
12737 if !self.stream_dropped.load(Ordering::SeqCst) {
12738 self.stream_dropped_notify.notified().await;
12739 }
12740 self.committed_after_drop
12741 .store(self.stream_dropped.load(Ordering::SeqCst), Ordering::SeqCst);
12742 Ok(LLMResponse::new(
12743 "Committed technical response.",
12744 FinishReason::Stop,
12745 ))
12746 }
12747
12748 async fn complete_stream(
12749 &self,
12750 _messages: &[ChatMessage],
12751 _config: Option<&LLMConfig>,
12752 ) -> std::result::Result<
12753 Box<dyn Stream<Item = std::result::Result<LLMChunk, LLMError>> + Unpin + Send>,
12754 LLMError,
12755 > {
12756 self.stream_started.notify_one();
12757 Ok(Box::new(PendingDropStream {
12758 dropped: Arc::clone(&self.stream_dropped),
12759 dropped_notify: Arc::clone(&self.stream_dropped_notify),
12760 }))
12761 }
12762
12763 fn provider_name(&self) -> &str {
12764 "established-stream"
12765 }
12766
12767 fn supports(&self, _feature: LLMFeature) -> bool {
12768 false
12769 }
12770 }
12771
12772 struct FirstCallLockingProvider {
12773 lock: Arc<tokio::sync::Mutex<()>>,
12774 first_started: Arc<tokio::sync::Notify>,
12775 first_dropped: Arc<AtomicBool>,
12776 committed_after_drop: Arc<AtomicBool>,
12777 calls: AtomicU64,
12778 }
12779
12780 #[async_trait]
12781 impl LLMProvider for FirstCallLockingProvider {
12782 async fn complete(
12783 &self,
12784 _messages: &[ChatMessage],
12785 _config: Option<&LLMConfig>,
12786 ) -> std::result::Result<LLMResponse, LLMError> {
12787 let _guard = self.lock.lock().await;
12788 let call = self.calls.fetch_add(1, Ordering::SeqCst);
12789 if call == 0 {
12790 let _drop_signal = ProviderFutureDropSignal {
12791 dropped: Arc::clone(&self.first_dropped),
12792 };
12793 self.first_started.notify_one();
12794 return std::future::pending().await;
12795 }
12796 self.committed_after_drop
12797 .store(self.first_dropped.load(Ordering::SeqCst), Ordering::SeqCst);
12798 Ok(LLMResponse::new(
12799 "Committed technical response.",
12800 FinishReason::Stop,
12801 ))
12802 }
12803
12804 async fn complete_stream(
12805 &self,
12806 _messages: &[ChatMessage],
12807 _config: Option<&LLMConfig>,
12808 ) -> std::result::Result<
12809 Box<dyn Stream<Item = std::result::Result<LLMChunk, LLMError>> + Unpin + Send>,
12810 LLMError,
12811 > {
12812 Err(LLMError::Other(
12813 "streaming is not used in this test".to_string(),
12814 ))
12815 }
12816
12817 fn provider_name(&self) -> &str {
12818 "first-call-locking"
12819 }
12820
12821 fn supports(&self, _feature: LLMFeature) -> bool {
12822 false
12823 }
12824 }
12825
12826 struct RoutingAfterProviderStart {
12827 provider_started: Arc<tokio::sync::Notify>,
12828 }
12829
12830 #[async_trait]
12831 impl LLMProvider for RoutingAfterProviderStart {
12832 async fn complete(
12833 &self,
12834 _messages: &[ChatMessage],
12835 _config: Option<&LLMConfig>,
12836 ) -> std::result::Result<LLMResponse, LLMError> {
12837 self.provider_started.notified().await;
12838 Ok(LLMResponse::new("1", FinishReason::Stop))
12839 }
12840
12841 async fn complete_stream(
12842 &self,
12843 _messages: &[ChatMessage],
12844 _config: Option<&LLMConfig>,
12845 ) -> std::result::Result<
12846 Box<dyn Stream<Item = std::result::Result<LLMChunk, LLMError>> + Unpin + Send>,
12847 LLMError,
12848 > {
12849 Err(LLMError::Other(
12850 "streaming is not used in this test".to_string(),
12851 ))
12852 }
12853
12854 fn provider_name(&self) -> &str {
12855 "routing-after-start"
12856 }
12857
12858 fn supports(&self, _feature: LLMFeature) -> bool {
12859 false
12860 }
12861 }
12862
12863 struct ResponseCountingHooks {
12865 responses: Arc<std::sync::atomic::AtomicUsize>,
12866 }
12867
12868 struct ContextEchoTool;
12870
12871 #[async_trait]
12872 impl ai_agents_core::Tool for ContextEchoTool {
12873 fn id(&self) -> &str {
12874 "context_echo"
12875 }
12876
12877 fn name(&self) -> &str {
12878 "Context Echo"
12879 }
12880
12881 fn description(&self) -> &str {
12882 "Returns selected execution context fields."
12883 }
12884
12885 fn input_schema(&self) -> Value {
12886 serde_json::json!({"type": "object"})
12887 }
12888
12889 fn policy_bindings(&self) -> ai_agents_core::ToolPolicyBindings {
12890 ai_agents_core::ToolPolicyBindings {
12891 path_fields: vec![ai_agents_core::PathPolicyBinding::read("path")],
12892 result_limit_fields: vec![ai_agents_core::ResultLimitBinding::new(
12893 "max_results",
12894 ai_agents_core::ResultLimitKind::MaxResults,
12895 )],
12896 ..Default::default()
12897 }
12898 }
12899
12900 async fn execute(
12901 &self,
12902 _args: Value,
12903 ctx: ai_agents_core::ToolExecutionContext,
12904 ) -> ToolResult {
12905 ToolResult::ok(
12906 serde_json::json!({
12907 "requested_name": ctx.requested_name,
12908 "canonical_id": ctx.canonical_id,
12909 "display_name": ctx.display_name,
12910 "max_results": ctx.limits.max_results,
12911 "custom_config": ctx.custom_config,
12912 })
12913 .to_string(),
12914 )
12915 }
12916 }
12917
12918 struct SlowTool;
12920
12921 struct FlakyWriteTool {
12923 calls: Arc<std::sync::atomic::AtomicUsize>,
12924 }
12925
12926 struct LockedWriteTool {
12928 active: Arc<std::sync::atomic::AtomicUsize>,
12929 max_active: Arc<std::sync::atomic::AtomicUsize>,
12930 }
12931
12932 struct MultiResourceWriteTool {
12933 active: Arc<std::sync::atomic::AtomicUsize>,
12934 max_active: Arc<std::sync::atomic::AtomicUsize>,
12935 }
12936
12937 #[derive(Clone)]
12938 struct PathMutationGate {
12939 entered: Arc<AtomicBool>,
12940 entered_notify: Arc<tokio::sync::Notify>,
12941 release: Arc<tokio::sync::Notify>,
12942 }
12943
12944 impl PathMutationGate {
12945 fn new() -> Self {
12946 Self {
12947 entered: Arc::new(AtomicBool::new(false)),
12948 entered_notify: Arc::new(tokio::sync::Notify::new()),
12949 release: Arc::new(tokio::sync::Notify::new()),
12950 }
12951 }
12952
12953 async fn wait_until_entered(&self) {
12954 if !self.entered.load(Ordering::SeqCst) {
12955 self.entered_notify.notified().await;
12956 }
12957 }
12958
12959 fn release(&self) {
12960 self.release.notify_one();
12961 }
12962 }
12963
12964 struct BlockingPathMutationTool {
12965 id: &'static str,
12966 path_fields: Vec<ai_agents_core::PathPolicyBinding>,
12967 gate: PathMutationGate,
12968 }
12969
12970 struct NoBindingWriteTool {
12971 active: Arc<std::sync::atomic::AtomicUsize>,
12972 max_active: Arc<std::sync::atomic::AtomicUsize>,
12973 }
12974
12975 struct RecoveryTestTool {
12976 id: String,
12977 succeeds: bool,
12978 calls: Arc<std::sync::atomic::AtomicUsize>,
12979 }
12980
12981 struct BlockingApprovalHandler {
12982 entered: Arc<tokio::sync::Barrier>,
12983 release: Arc<tokio::sync::Notify>,
12984 result: ApprovalResult,
12985 }
12986
12987 struct ReentrantToolHooks {
12988 agent: parking_lot::Mutex<Option<Weak<RuntimeAgent>>>,
12989 invoked: AtomicBool,
12990 nested_success: AtomicBool,
12991 }
12992
12993 #[async_trait]
12994 impl ai_agents_core::Tool for SlowTool {
12995 fn id(&self) -> &str {
12996 "slow"
12997 }
12998
12999 fn name(&self) -> &str {
13000 "Slow"
13001 }
13002
13003 fn description(&self) -> &str {
13004 "Waits until cancelled or timed out."
13005 }
13006
13007 fn input_schema(&self) -> Value {
13008 serde_json::json!({"type": "object"})
13009 }
13010
13011 async fn execute(
13012 &self,
13013 _args: Value,
13014 _ctx: ai_agents_core::ToolExecutionContext,
13015 ) -> ToolResult {
13016 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
13017 ToolResult::ok("done")
13018 }
13019 }
13020
13021 #[async_trait]
13022 impl ai_agents_core::Tool for FlakyWriteTool {
13023 fn id(&self) -> &str {
13024 "flaky_write"
13025 }
13026
13027 fn name(&self) -> &str {
13028 "Flaky Write"
13029 }
13030
13031 fn description(&self) -> &str {
13032 "Fails on the first write attempt."
13033 }
13034
13035 fn input_schema(&self) -> Value {
13036 serde_json::json!({"type": "object", "properties": {"path": {"type": "string"}}})
13037 }
13038
13039 fn policy_bindings(&self) -> ai_agents_core::ToolPolicyBindings {
13040 ai_agents_core::ToolPolicyBindings {
13041 path_fields: vec![ai_agents_core::PathPolicyBinding::write("path")],
13042 ..Default::default()
13043 }
13044 }
13045
13046 fn safety_metadata(&self) -> ai_agents_core::ToolSafetyMetadata {
13047 ai_agents_core::ToolSafetyMetadata {
13048 read_only: false,
13049 concurrency_safe: false,
13050 operation: ai_agents_core::ToolOperationKind::Write,
13051 side_effect_level: ai_agents_core::ToolSideEffectLevel::LocalWrite,
13052 requires_network: false,
13053 destructive: false,
13054 open_world: false,
13055 host_dependent: false,
13056 requires_user_interaction: false,
13057 supports_cancellation: true,
13058 default_requires_approval: false,
13059 should_defer_schema: false,
13060 max_output_chars: Some(1024),
13061 max_result_size_chars: Some(1024),
13062 }
13063 }
13064
13065 fn classify_call(&self, _args: &Value) -> ai_agents_core::ToolCallClassification {
13066 let mut classification =
13067 ai_agents_core::ToolCallClassification::from_metadata(&self.safety_metadata());
13068 classification.safely_retryable = false;
13069 classification
13070 }
13071
13072 async fn execute(
13073 &self,
13074 _args: Value,
13075 _ctx: ai_agents_core::ToolExecutionContext,
13076 ) -> ToolResult {
13077 let call = self.calls.fetch_add(1, Ordering::SeqCst);
13078 if call == 0 {
13079 ToolResult::error("first failure")
13080 } else {
13081 ToolResult::ok("second success")
13082 }
13083 }
13084 }
13085
13086 #[async_trait]
13087 impl ai_agents_core::Tool for LockedWriteTool {
13088 fn id(&self) -> &str {
13089 "locked_write"
13090 }
13091
13092 fn name(&self) -> &str {
13093 "Locked Write"
13094 }
13095
13096 fn description(&self) -> &str {
13097 "Tracks concurrent execution on one resource."
13098 }
13099
13100 fn input_schema(&self) -> Value {
13101 serde_json::json!({"type": "object", "properties": {"path": {"type": "string"}}})
13102 }
13103
13104 fn policy_bindings(&self) -> ai_agents_core::ToolPolicyBindings {
13105 ai_agents_core::ToolPolicyBindings {
13106 path_fields: vec![ai_agents_core::PathPolicyBinding::write("path")],
13107 ..Default::default()
13108 }
13109 }
13110
13111 fn safety_metadata(&self) -> ai_agents_core::ToolSafetyMetadata {
13112 ai_agents_core::ToolSafetyMetadata {
13113 read_only: false,
13114 concurrency_safe: false,
13115 operation: ai_agents_core::ToolOperationKind::Write,
13116 side_effect_level: ai_agents_core::ToolSideEffectLevel::LocalWrite,
13117 requires_network: false,
13118 destructive: false,
13119 open_world: false,
13120 host_dependent: false,
13121 requires_user_interaction: false,
13122 supports_cancellation: true,
13123 default_requires_approval: false,
13124 should_defer_schema: false,
13125 max_output_chars: Some(1024),
13126 max_result_size_chars: Some(1024),
13127 }
13128 }
13129
13130 async fn execute(
13131 &self,
13132 _args: Value,
13133 _ctx: ai_agents_core::ToolExecutionContext,
13134 ) -> ToolResult {
13135 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
13136 loop {
13137 let current_max = self.max_active.load(Ordering::SeqCst);
13138 if active <= current_max {
13139 break;
13140 }
13141 if self
13142 .max_active
13143 .compare_exchange(current_max, active, Ordering::SeqCst, Ordering::SeqCst)
13144 .is_ok()
13145 {
13146 break;
13147 }
13148 }
13149 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
13150 self.active.fetch_sub(1, Ordering::SeqCst);
13151 ToolResult::ok("done")
13152 }
13153 }
13154
13155 #[async_trait]
13156 impl ai_agents_core::Tool for MultiResourceWriteTool {
13157 fn id(&self) -> &str {
13158 "multi_resource_write"
13159 }
13160
13161 fn name(&self) -> &str {
13162 "Multi Resource Write"
13163 }
13164
13165 fn description(&self) -> &str {
13166 "Tracks concurrent execution across source and destination resources."
13167 }
13168
13169 fn input_schema(&self) -> Value {
13170 serde_json::json!({"type": "object"})
13171 }
13172
13173 fn policy_bindings(&self) -> ai_agents_core::ToolPolicyBindings {
13174 ai_agents_core::ToolPolicyBindings {
13175 path_fields: vec![
13176 ai_agents_core::PathPolicyBinding::read_write("source_path"),
13177 ai_agents_core::PathPolicyBinding::write("destination_path"),
13178 ],
13179 ..Default::default()
13180 }
13181 }
13182
13183 fn safety_metadata(&self) -> ai_agents_core::ToolSafetyMetadata {
13184 LockedWriteTool {
13185 active: Arc::clone(&self.active),
13186 max_active: Arc::clone(&self.max_active),
13187 }
13188 .safety_metadata()
13189 }
13190
13191 async fn execute(
13192 &self,
13193 _args: Value,
13194 _ctx: ai_agents_core::ToolExecutionContext,
13195 ) -> ToolResult {
13196 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
13197 self.max_active.fetch_max(active, Ordering::SeqCst);
13198 tokio::time::sleep(std::time::Duration::from_millis(75)).await;
13199 self.active.fetch_sub(1, Ordering::SeqCst);
13200 ToolResult::ok("done")
13201 }
13202 }
13203
13204 #[async_trait]
13205 impl ai_agents_core::Tool for BlockingPathMutationTool {
13206 fn id(&self) -> &str {
13207 self.id
13208 }
13209
13210 fn name(&self) -> &str {
13211 self.id
13212 }
13213
13214 fn description(&self) -> &str {
13215 "Blocks a path mutation until the test releases it."
13216 }
13217
13218 fn input_schema(&self) -> Value {
13219 serde_json::json!({"type": "object"})
13220 }
13221
13222 fn policy_bindings(&self) -> ai_agents_core::ToolPolicyBindings {
13223 ai_agents_core::ToolPolicyBindings {
13224 path_fields: self.path_fields.clone(),
13225 ..Default::default()
13226 }
13227 }
13228
13229 fn safety_metadata(&self) -> ai_agents_core::ToolSafetyMetadata {
13230 ai_agents_core::ToolSafetyMetadata {
13231 read_only: false,
13232 concurrency_safe: false,
13233 operation: ai_agents_core::ToolOperationKind::Write,
13234 side_effect_level: ai_agents_core::ToolSideEffectLevel::LocalWrite,
13235 requires_network: false,
13236 destructive: false,
13237 open_world: false,
13238 host_dependent: false,
13239 requires_user_interaction: false,
13240 supports_cancellation: true,
13241 default_requires_approval: false,
13242 should_defer_schema: false,
13243 max_output_chars: Some(1024),
13244 max_result_size_chars: Some(1024),
13245 }
13246 }
13247
13248 async fn execute(
13249 &self,
13250 _args: Value,
13251 _ctx: ai_agents_core::ToolExecutionContext,
13252 ) -> ToolResult {
13253 self.gate.entered.store(true, Ordering::SeqCst);
13254 self.gate.entered_notify.notify_one();
13255 self.gate.release.notified().await;
13256 ToolResult::ok("done")
13257 }
13258 }
13259
13260 #[async_trait]
13261 impl ai_agents_core::Tool for NoBindingWriteTool {
13262 fn id(&self) -> &str {
13263 "no_binding_write"
13264 }
13265
13266 fn name(&self) -> &str {
13267 "No Binding Write"
13268 }
13269
13270 fn description(&self) -> &str {
13271 "Tracks concurrent execution without resource bindings."
13272 }
13273
13274 fn input_schema(&self) -> Value {
13275 serde_json::json!({"type": "object"})
13276 }
13277
13278 fn safety_metadata(&self) -> ai_agents_core::ToolSafetyMetadata {
13279 LockedWriteTool {
13280 active: Arc::clone(&self.active),
13281 max_active: Arc::clone(&self.max_active),
13282 }
13283 .safety_metadata()
13284 }
13285
13286 async fn execute(
13287 &self,
13288 _args: Value,
13289 _ctx: ai_agents_core::ToolExecutionContext,
13290 ) -> ToolResult {
13291 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
13292 self.max_active.fetch_max(active, Ordering::SeqCst);
13293 tokio::time::sleep(std::time::Duration::from_millis(75)).await;
13294 self.active.fetch_sub(1, Ordering::SeqCst);
13295 ToolResult::ok("done")
13296 }
13297 }
13298
13299 #[async_trait]
13300 impl ai_agents_core::Tool for RecoveryTestTool {
13301 fn id(&self) -> &str {
13302 &self.id
13303 }
13304
13305 fn name(&self) -> &str {
13306 &self.id
13307 }
13308
13309 fn description(&self) -> &str {
13310 "Records recovery execution and returns a configured result."
13311 }
13312
13313 fn input_schema(&self) -> Value {
13314 serde_json::json!({"type": "object"})
13315 }
13316
13317 fn policy_bindings(&self) -> ai_agents_core::ToolPolicyBindings {
13318 ai_agents_core::ToolPolicyBindings {
13319 path_fields: vec![ai_agents_core::PathPolicyBinding::write("path")],
13320 ..Default::default()
13321 }
13322 }
13323
13324 fn safety_metadata(&self) -> ai_agents_core::ToolSafetyMetadata {
13325 ai_agents_core::ToolSafetyMetadata {
13326 read_only: false,
13327 concurrency_safe: false,
13328 operation: ai_agents_core::ToolOperationKind::Write,
13329 side_effect_level: ai_agents_core::ToolSideEffectLevel::LocalWrite,
13330 requires_network: false,
13331 destructive: false,
13332 open_world: false,
13333 host_dependent: false,
13334 requires_user_interaction: false,
13335 supports_cancellation: true,
13336 default_requires_approval: false,
13337 should_defer_schema: false,
13338 max_output_chars: Some(1024),
13339 max_result_size_chars: Some(1024),
13340 }
13341 }
13342
13343 async fn execute(
13344 &self,
13345 _args: Value,
13346 _ctx: ai_agents_core::ToolExecutionContext,
13347 ) -> ToolResult {
13348 self.calls.fetch_add(1, Ordering::SeqCst);
13349 if self.succeeds {
13350 ToolResult::ok(format!("{} succeeded", self.id))
13351 } else {
13352 ToolResult::error(format!("{} failed", self.id))
13353 }
13354 }
13355 }
13356
13357 #[async_trait]
13358 impl ApprovalHandler for BlockingApprovalHandler {
13359 async fn request_approval(
13360 &self,
13361 _request: ai_agents_hitl::ApprovalRequest,
13362 ) -> ApprovalResult {
13363 self.entered.wait().await;
13364 self.release.notified().await;
13365 self.result.clone()
13366 }
13367 }
13368
13369 #[async_trait]
13370 impl AgentHooks for ReentrantToolHooks {
13371 async fn on_tool_complete(&self, tool: &str, _result: &ToolResult, _duration_ms: u64) {
13372 if tool != "reentrant_write" || self.invoked.swap(true, Ordering::SeqCst) {
13373 return;
13374 }
13375 let agent = self.agent.lock().as_ref().and_then(Weak::upgrade);
13376 if let Some(agent) = agent {
13377 let result = agent
13378 .invoke_tool(ToolExecutionRequest::new(
13379 "nested-hook-call",
13380 "reentrant_write",
13381 serde_json::json!({"path": "./hook.txt"}),
13382 ToolCallSource::Manual,
13383 ))
13384 .await;
13385 self.nested_success
13386 .store(result.is_ok_and(|record| record.success), Ordering::SeqCst);
13387 }
13388 }
13389 }
13390
13391 #[async_trait]
13392 impl AgentHooks for ResponseCountingHooks {
13393 async fn on_response(&self, _response: &AgentResponse) {
13394 self.responses.fetch_add(1, Ordering::SeqCst);
13395 }
13396 }
13397
13398 struct ApprovalRecordingHooks {
13399 events: parking_lot::Mutex<Vec<String>>,
13400 }
13401
13402 impl ApprovalRecordingHooks {
13403 fn new() -> Self {
13404 Self {
13405 events: parking_lot::Mutex::new(Vec::new()),
13406 }
13407 }
13408
13409 fn events(&self) -> Vec<String> {
13410 self.events.lock().clone()
13411 }
13412 }
13413
13414 #[async_trait]
13415 impl AgentHooks for ApprovalRecordingHooks {
13416 async fn on_approval_result(&self, request_id: &str, result: &ApprovalResult) {
13417 self.events.lock().push(format!(
13418 "raw:{}:{}",
13419 request_id,
13420 approval_result_name(result)
13421 ));
13422 }
13423
13424 async fn on_approval_resolved(
13425 &self,
13426 request: &ai_agents_hitl::ApprovalRequest,
13427 raw_result: &ApprovalResult,
13428 outcome: &ApprovalResolvedOutcome,
13429 ) {
13430 self.events.lock().push(format!(
13431 "resolved:{}:{}:{}",
13432 request.id,
13433 approval_result_name(raw_result),
13434 approval_outcome_name(outcome)
13435 ));
13436 }
13437 }
13438
13439 fn approval_result_name(result: &ApprovalResult) -> &'static str {
13440 match result {
13441 ApprovalResult::Approved => "approved",
13442 ApprovalResult::Rejected { .. } => "rejected",
13443 ApprovalResult::Modified { .. } => "modified",
13444 ApprovalResult::Timeout => "timeout",
13445 }
13446 }
13447
13448 fn approval_outcome_name(outcome: &ApprovalResolvedOutcome) -> &'static str {
13449 match outcome {
13450 ApprovalResolvedOutcome::Approved => "approved",
13451 ApprovalResolvedOutcome::Rejected { .. } => "rejected",
13452 ApprovalResolvedOutcome::Modified { .. } => "modified",
13453 ApprovalResolvedOutcome::Error { .. } => "error",
13454 }
13455 }
13456
13457 fn assert_correlated_approval_events(
13458 events: &[String],
13459 raw_status: &str,
13460 outcome_status: &str,
13461 ) {
13462 assert_eq!(events.len(), 2);
13463 let raw: Vec<_> = events[0].split(':').collect();
13464 let resolved: Vec<_> = events[1].split(':').collect();
13465 assert_eq!(raw[0], "raw");
13466 assert_eq!(resolved[0], "resolved");
13467 assert_eq!(raw[1], resolved[1]);
13468 assert_eq!(raw[2], raw_status);
13469 assert_eq!(resolved[2], raw_status);
13470 assert_eq!(resolved[3], outcome_status);
13471 }
13472
13473 fn approval_security_config(policy_enabled: bool) -> ToolSecurityConfig {
13474 let mut security = ToolSecurityConfig {
13475 enabled: true,
13476 fail_closed: true,
13477 ..Default::default()
13478 };
13479 let policy = ai_agents_tools::ToolPolicyConfig {
13480 enabled: policy_enabled,
13481 write_paths: vec![".".to_string()],
13482 require_confirmation: true,
13483 ..Default::default()
13484 };
13485 security.tools.insert("locked_write".to_string(), policy);
13486 security
13487 }
13488
13489 struct MutationTestWorkspace {
13490 root: std::path::PathBuf,
13491 }
13492
13493 impl MutationTestWorkspace {
13494 fn new() -> Self {
13495 let root = std::env::temp_dir().join(format!(
13496 "ai-agents-runtime-mutation-{}",
13497 uuid::Uuid::new_v4()
13498 ));
13499 std::fs::create_dir_all(&root).unwrap();
13500 Self { root }
13501 }
13502 }
13503
13504 impl Drop for MutationTestWorkspace {
13505 fn drop(&mut self) {
13506 let _ = std::fs::remove_dir_all(&self.root);
13507 }
13508 }
13509
13510 async fn wait_for_resource_lock_strong_count(locks: &ToolResourceLocks, minimum: usize) {
13511 tokio::time::timeout(std::time::Duration::from_secs(2), async {
13512 loop {
13513 let strong_count = locks
13514 .read()
13515 .get("path-mutation:global")
13516 .map_or(0, |lock| lock.strong_count());
13517 if strong_count >= minimum {
13518 break;
13519 }
13520 tokio::task::yield_now().await;
13521 }
13522 })
13523 .await
13524 .expect("path mutation call did not reach the shared lock");
13525 }
13526
13527 async fn assert_path_mutation_pair_serialized(
13528 first_id: &'static str,
13529 first_fields: Vec<ai_agents_core::PathPolicyBinding>,
13530 first_args: Value,
13531 second_id: &'static str,
13532 second_fields: Vec<ai_agents_core::PathPolicyBinding>,
13533 second_args: Value,
13534 ) {
13535 let locks = new_tool_resource_locks();
13536 let first_gate = PathMutationGate::new();
13537 let second_gate = PathMutationGate::new();
13538 second_gate.release();
13539 let agent = Arc::new(
13540 AgentBuilder::new()
13541 .system_prompt("Test global path mutation locking.")
13542 .llm(Arc::new(mock_with_response("done")))
13543 .tool(Arc::new(BlockingPathMutationTool {
13544 id: first_id,
13545 path_fields: first_fields,
13546 gate: first_gate.clone(),
13547 }))
13548 .tool(Arc::new(BlockingPathMutationTool {
13549 id: second_id,
13550 path_fields: second_fields,
13551 gate: second_gate.clone(),
13552 }))
13553 .build()
13554 .unwrap()
13555 .with_shared_resource_locks(Arc::clone(&locks)),
13556 );
13557
13558 let first = {
13559 let agent = Arc::clone(&agent);
13560 tokio::spawn(async move {
13561 agent
13562 .invoke_tool(ToolExecutionRequest::new(
13563 format!("{}-first", first_id),
13564 first_id,
13565 first_args,
13566 ToolCallSource::Manual,
13567 ))
13568 .await
13569 .unwrap()
13570 })
13571 };
13572 first_gate.wait_until_entered().await;
13573
13574 let second = {
13575 let agent = Arc::clone(&agent);
13576 tokio::spawn(async move {
13577 agent
13578 .invoke_tool(ToolExecutionRequest::new(
13579 format!("{}-second", second_id),
13580 second_id,
13581 second_args,
13582 ToolCallSource::Manual,
13583 ))
13584 .await
13585 .unwrap()
13586 })
13587 };
13588 wait_for_resource_lock_strong_count(&locks, 2).await;
13589 assert!(!second_gate.entered.load(Ordering::SeqCst));
13590 assert!(!second.is_finished());
13591
13592 first_gate.release();
13593 let (first, second) = tokio::time::timeout(std::time::Duration::from_secs(2), async {
13594 tokio::join!(first, second)
13595 })
13596 .await
13597 .expect("serialized path mutation calls did not finish");
13598 assert!(first.unwrap().success);
13599 assert!(second.unwrap().success);
13600 assert!(second_gate.entered.load(Ordering::SeqCst));
13601 assert!(locks.read().is_empty());
13602 }
13603
13604 #[derive(Clone, Copy)]
13605 enum MutationDenial {
13606 Policy,
13607 Approval,
13608 }
13609
13610 fn mutation_denial_security_config(
13611 tool_id: &str,
13612 workspace: &std::path::Path,
13613 denial: MutationDenial,
13614 ) -> ToolSecurityConfig {
13615 let workspace = workspace.to_string_lossy().into_owned();
13616 let mut policy = ai_agents_tools::ToolPolicyConfig {
13617 read_paths: vec![workspace.clone()],
13618 write_paths: vec![workspace.clone()],
13619 ..Default::default()
13620 };
13621 match denial {
13622 MutationDenial::Policy => policy.blocked_paths = vec![workspace],
13623 MutationDenial::Approval => policy.require_confirmation = true,
13624 }
13625
13626 let mut security = ToolSecurityConfig {
13627 enabled: true,
13628 fail_closed: true,
13629 ..Default::default()
13630 };
13631 security.tools.insert(tool_id.to_string(), policy);
13632 security
13633 }
13634
13635 async fn assert_path_mutation_denied(tool: Arc<dyn Tool>, denial: MutationDenial) {
13636 let workspace = MutationTestWorkspace::new();
13637 let tool_id = tool.id().to_string();
13638 let preserved = workspace.root.join(format!("{}-preserved.txt", tool_id));
13639 let destination = workspace.root.join(format!("{}-destination.txt", tool_id));
13640 std::fs::write(&preserved, "preserved").unwrap();
13641 let arguments = match tool_id.as_str() {
13642 "copy_path" | "move_path" => serde_json::json!({
13643 "source_path": preserved.to_string_lossy(),
13644 "destination_path": destination.to_string_lossy(),
13645 "dry_run": false
13646 }),
13647 "delete_path" => serde_json::json!({
13648 "path": preserved.to_string_lossy(),
13649 "recursive": false,
13650 "dry_run": false
13651 }),
13652 _ => panic!("unsupported mutation tool: {}", tool_id),
13653 };
13654 let security = mutation_denial_security_config(&tool_id, &workspace.root, denial);
13655 let builder = AgentBuilder::new()
13656 .system_prompt("Test mutation denial.")
13657 .llm(Arc::new(mock_with_response("done")))
13658 .tool(tool)
13659 .tool_security(ToolSecurityEngine::new(security));
13660 let builder = match denial {
13661 MutationDenial::Policy => builder,
13662 MutationDenial::Approval => builder
13663 .hitl_engine(HITLEngine::new(ai_agents_hitl::HITLConfig::default()))
13664 .approval_handler(Arc::new(RejectAllHandler::new())),
13665 };
13666 let agent = builder.build().unwrap();
13667
13668 let record = agent
13669 .invoke_tool(ToolExecutionRequest::new(
13670 format!("{}-denied", tool_id),
13671 tool_id.clone(),
13672 arguments,
13673 ToolCallSource::Manual,
13674 ))
13675 .await
13676 .unwrap();
13677
13678 assert!(!record.executed, "{} must not be invoked", tool_id);
13679 assert!(!record.success);
13680 match denial {
13681 MutationDenial::Policy => {
13682 assert_eq!(record.policy.outcome, PermissionOutcome::Deny);
13683 assert!(record.approval.as_ref().is_some_and(|approval| matches!(
13684 &approval.status,
13685 ToolApprovalStatus::NotRequired
13686 )));
13687 }
13688 MutationDenial::Approval => {
13689 assert_eq!(record.policy.outcome, PermissionOutcome::RequiresApproval);
13690 assert!(record.approval.as_ref().is_some_and(|approval| matches!(
13691 &approval.status,
13692 ToolApprovalStatus::Rejected
13693 )));
13694 }
13695 }
13696 assert_eq!(std::fs::read_to_string(&preserved).unwrap(), "preserved");
13697 assert!(!destination.exists());
13698 }
13699
13700 fn recovery_manager_with_fallbacks(
13701 fallbacks: impl IntoIterator<Item = (String, String)>,
13702 ) -> RecoveryManager {
13703 use ai_agents_recovery::{ErrorRecoveryConfig, ToolRecoveryConfig, ToolRetryConfig};
13704
13705 let per_tool = fallbacks
13706 .into_iter()
13707 .map(|(tool, fallback_tool)| {
13708 (
13709 tool,
13710 ToolRetryConfig {
13711 max_retries: 0,
13712 timeout_ms: Some(1_000),
13713 on_failure: ToolFailureAction::Fallback { fallback_tool },
13714 },
13715 )
13716 })
13717 .collect();
13718 RecoveryManager::new(ErrorRecoveryConfig {
13719 tools: ToolRecoveryConfig {
13720 per_tool,
13721 ..Default::default()
13722 },
13723 ..Default::default()
13724 })
13725 }
13726
13727 fn approval_check() -> HITLCheckResult {
13728 HITLCheckResult::required(
13729 ApprovalTrigger::tool("test", serde_json::json!({})),
13730 HashMap::new(),
13731 "Approve?",
13732 None,
13733 )
13734 }
13735
13736 fn agent_with_approval_result(
13737 raw_result: ApprovalResult,
13738 timeout_action: TimeoutAction,
13739 hooks: Arc<ApprovalRecordingHooks>,
13740 ) -> RuntimeAgent {
13741 use ai_agents_hitl::{CallbackHandler, HITLConfig};
13742
13743 let config = HITLConfig {
13744 on_timeout: timeout_action,
13745 ..Default::default()
13746 };
13747 let handler = CallbackHandler::new(move |_| raw_result.clone());
13748 AgentBuilder::new()
13749 .system_prompt("Test HITL hooks.")
13750 .llm(Arc::new(mock_with_response("done")))
13751 .build()
13752 .unwrap()
13753 .with_hooks(hooks)
13754 .with_hitl(HITLEngine::new(config), Arc::new(handler))
13755 }
13756
13757 #[tokio::test]
13758 async fn approval_hooks_expose_direct_effective_decisions_after_raw_results() {
13759 let cases = vec![
13760 (ApprovalResult::Approved, "approved"),
13761 (
13762 ApprovalResult::Rejected {
13763 reason: Some("denied".to_string()),
13764 },
13765 "rejected",
13766 ),
13767 (
13768 ApprovalResult::Modified {
13769 changes: HashMap::from([("value".to_string(), serde_json::json!(2))]),
13770 },
13771 "modified",
13772 ),
13773 ];
13774
13775 for (raw_result, expected) in cases {
13776 let hooks = Arc::new(ApprovalRecordingHooks::new());
13777 let agent =
13778 agent_with_approval_result(raw_result, TimeoutAction::Reject, hooks.clone());
13779
13780 let result = agent.request_hitl_approval(approval_check()).await.unwrap();
13781
13782 assert_eq!(approval_result_name(&result), expected);
13783 assert_correlated_approval_events(&hooks.events(), expected, expected);
13784 }
13785 }
13786
13787 #[tokio::test]
13788 async fn approval_hooks_expose_timeout_policy_decisions() {
13789 for (timeout_action, expected) in [
13790 (TimeoutAction::Approve, "approved"),
13791 (TimeoutAction::Reject, "rejected"),
13792 ] {
13793 let hooks = Arc::new(ApprovalRecordingHooks::new());
13794 let agent =
13795 agent_with_approval_result(ApprovalResult::Timeout, timeout_action, hooks.clone());
13796
13797 let result = agent.request_hitl_approval(approval_check()).await.unwrap();
13798
13799 assert_eq!(approval_result_name(&result), expected);
13800 assert_correlated_approval_events(&hooks.events(), "timeout", expected);
13801 }
13802 }
13803
13804 #[tokio::test]
13805 async fn timeout_error_fires_correlated_resolved_error_before_returning() {
13806 let hooks = Arc::new(ApprovalRecordingHooks::new());
13807 let agent = agent_with_approval_result(
13808 ApprovalResult::Timeout,
13809 TimeoutAction::Error,
13810 hooks.clone(),
13811 );
13812
13813 let error = agent
13814 .request_hitl_approval(approval_check())
13815 .await
13816 .unwrap_err();
13817
13818 assert!(error.to_string().contains("HITL approval timeout"));
13819 assert_correlated_approval_events(&hooks.events(), "timeout", "error");
13820 }
13821
13822 #[tokio::test]
13824 async fn test_integration_yaml_to_chat_basic() {
13825 let mock = mock_with_response("Hello! How can I help you?");
13826 let agent = AgentBuilder::new()
13827 .system_prompt("You are a test assistant.")
13828 .llm(Arc::new(mock))
13829 .build()
13830 .unwrap();
13831
13832 let response = agent.chat("Hi").await.unwrap();
13833 assert!(!response.content.is_empty());
13834 assert_eq!(response.content, "Hello! How can I help you?");
13835 }
13836
13837 #[tokio::test]
13839 async fn test_integration_multi_turn_conversation() {
13840 let mock = mock_with_responses(vec![
13841 "Hello! I'm your assistant.",
13842 "The weather is sunny today.",
13843 "Goodbye!",
13844 ]);
13845 let agent = AgentBuilder::new()
13846 .system_prompt("You are helpful.")
13847 .llm(Arc::new(mock))
13848 .build()
13849 .unwrap();
13850
13851 let r1 = agent.chat("Hi").await.unwrap();
13852 assert_eq!(r1.content, "Hello! I'm your assistant.");
13853
13854 let r2 = agent.chat("What's the weather?").await.unwrap();
13855 assert_eq!(r2.content, "The weather is sunny today.");
13856
13857 let r3 = agent.chat("Bye").await.unwrap();
13858 assert_eq!(r3.content, "Goodbye!");
13859
13860 let messages = agent.memory.get_messages(None).await.unwrap();
13862 assert_eq!(messages.len(), 6);
13864 }
13865
13866 #[test]
13867 fn later_approval_preserves_modified_evidence() {
13868 let arguments = serde_json::json!({"dry_run": true});
13869 let mut record = Some(ToolApprovalRecord {
13870 status: ToolApprovalStatus::Modified,
13871 reason: None,
13872 modified_arguments: Some(arguments.clone()),
13873 });
13874
13875 merge_approved_record(&mut record);
13876
13877 let record = record.unwrap();
13878 assert!(matches!(record.status, ToolApprovalStatus::Modified));
13879 assert_eq!(record.modified_arguments, Some(arguments));
13880 }
13881
13882 #[test]
13883 fn approval_binding_rejects_replaced_tool_implementation() {
13884 let reviewed_tool: Arc<dyn ai_agents_core::Tool> = Arc::new(ContextEchoTool);
13885 let same_tool = Arc::clone(&reviewed_tool);
13886 let replacement_tool: Arc<dyn ai_agents_core::Tool> = Arc::new(ContextEchoTool);
13887 let arguments = serde_json::json!({"path": "."});
13888 let versions = ToolDecisionVersions {
13889 policy: 2,
13890 registry: 3,
13891 runtime_control: 4,
13892 };
13893 let binding = ToolApprovalBinding {
13894 canonical_id: "context_echo".to_string(),
13895 arguments: arguments.clone(),
13896 confirmation_required: true,
13897 policy_version: versions.policy,
13898 runtime_control_version: versions.runtime_control,
13899 reviewed_tool,
13900 };
13901
13902 assert!(!binding.is_stale("context_echo", &arguments, true, versions, &same_tool,));
13903 assert!(binding.is_stale(
13904 "context_echo",
13905 &arguments,
13906 true,
13907 versions,
13908 &replacement_tool,
13909 ));
13910 }
13911
13912 #[tokio::test]
13913 async fn approved_mutation_to_dry_run_remains_executable() {
13914 use ai_agents_hitl::CallbackHandler;
13915
13916 let handler = CallbackHandler::new(|_| ApprovalResult::Modified {
13917 changes: HashMap::from([("dry_run".to_string(), serde_json::json!(true))]),
13918 });
13919 let agent = AgentBuilder::new()
13920 .system_prompt("Test safer approval modifications.")
13921 .llm(Arc::new(mock_with_response("done")))
13922 .tool(Arc::new(ai_agents_tools::FileWriteTool::new()))
13923 .hitl_engine(HITLEngine::new(ai_agents_hitl::HITLConfig::default()))
13924 .approval_handler(Arc::new(handler))
13925 .build()
13926 .unwrap();
13927
13928 let record = agent
13929 .invoke_tool(ToolExecutionRequest::new(
13930 "approved-dry-run",
13931 "file_write",
13932 serde_json::json!({
13933 "path": "./approval-dry-run.txt",
13934 "content": "not written"
13935 }),
13936 ToolCallSource::Manual,
13937 ))
13938 .await
13939 .unwrap();
13940
13941 assert!(record.executed);
13942 assert!(record.success);
13943 assert_eq!(record.executed_arguments["dry_run"], true);
13944 assert!(matches!(
13945 record.approval.as_ref().map(|approval| &approval.status),
13946 Some(ToolApprovalStatus::Modified)
13947 ));
13948 let output: Value = serde_json::from_str(&record.output).unwrap();
13949 assert_eq!(output["mutation_performed"], false);
13950 }
13951
13952 #[tokio::test]
13953 async fn context_preserves_requested_and_canonical_identity() {
13954 let mock = mock_with_response("hello");
13955 let mut tools = ai_agents_tools::ToolRegistry::new();
13956 tools.register(Arc::new(ContextEchoTool)).unwrap();
13957
13958 let mut security = ToolSecurityConfig {
13959 enabled: true,
13960 fail_closed: true,
13961 ..Default::default()
13962 };
13963 let mut policy = ai_agents_tools::ToolPolicyConfig {
13964 read_paths: vec![".".to_string()],
13965 max_results: Some(7),
13966 ..Default::default()
13967 };
13968 policy
13969 .config
13970 .insert("backend".to_string(), serde_json::json!("memory"));
13971 security.tools.insert("context_echo".to_string(), policy);
13972
13973 let agent = AgentBuilder::new()
13974 .system_prompt("You are helpful.")
13975 .llm(Arc::new(mock))
13976 .tools(tools)
13977 .tool_security(ToolSecurityEngine::new(security))
13978 .build()
13979 .unwrap();
13980
13981 let record = agent
13982 .invoke_tool(ToolExecutionRequest::new(
13983 "ctx-call",
13984 "Context Echo",
13985 serde_json::json!({"path": ".", "max_results": 99}),
13986 ToolCallSource::Manual,
13987 ))
13988 .await
13989 .unwrap();
13990
13991 assert!(record.success);
13992 assert!(matches!(&record.source, ToolCallSource::Manual));
13993 assert_eq!(record.requested_name, "Context Echo");
13994 assert_eq!(record.canonical_id, "context_echo");
13995 assert_eq!(record.policy.outcome, PermissionOutcome::Allow);
13996 assert_eq!(record.executed_arguments["max_results"], 7);
13997 let output: Value = serde_json::from_str(&record.output).unwrap();
13998 assert_eq!(output["requested_name"], "Context Echo");
13999 assert_eq!(output["canonical_id"], "context_echo");
14000 assert_eq!(output["max_results"], 7);
14001 assert_eq!(output["custom_config"]["backend"], "memory");
14002 assert!(record.metadata.contains_key("effective_limits"));
14003 assert!(record.metadata.contains_key("policy_snapshot"));
14004 }
14005
14006 #[tokio::test]
14007 async fn test_runtime_control_cancels_active_tool_call() {
14008 let mock = mock_with_response("hello");
14009 let agent = Arc::new(
14010 AgentBuilder::new()
14011 .system_prompt("You are helpful.")
14012 .llm(Arc::new(mock))
14013 .tool(Arc::new(SlowTool))
14014 .build()
14015 .unwrap(),
14016 );
14017 let control = agent.runtime_control();
14018 let running_agent = Arc::clone(&agent);
14019 let handle = tokio::spawn(async move {
14020 running_agent
14021 .invoke_tool(ToolExecutionRequest::new(
14022 "slow-call",
14023 "slow",
14024 serde_json::json!({}),
14025 ToolCallSource::Manual,
14026 ))
14027 .await
14028 .unwrap()
14029 });
14030
14031 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
14032 control.cancel_all();
14033 let record = handle.await.unwrap();
14034
14035 assert!(record.executed);
14036 assert!(record.cancelled);
14037 assert!(!record.success);
14038 assert!(record.cancellation_reason.is_some());
14039 }
14040
14041 #[tokio::test]
14042 async fn non_idempotent_tool_calls_are_not_retried() {
14043 use ai_agents_recovery::{ErrorRecoveryConfig, ToolRecoveryConfig, ToolRetryConfig};
14044
14045 let mock = mock_with_response("hello");
14046 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14047 let agent = AgentBuilder::new()
14048 .system_prompt("You are helpful.")
14049 .llm(Arc::new(mock))
14050 .tool(Arc::new(FlakyWriteTool {
14051 calls: Arc::clone(&calls),
14052 }))
14053 .recovery_manager(RecoveryManager::new(ErrorRecoveryConfig {
14054 tools: ToolRecoveryConfig {
14055 default: ToolRetryConfig {
14056 max_retries: 2,
14057 ..Default::default()
14058 },
14059 ..Default::default()
14060 },
14061 ..Default::default()
14062 }))
14063 .build()
14064 .unwrap();
14065
14066 let record = agent
14067 .invoke_tool(ToolExecutionRequest::new(
14068 "flaky-call",
14069 "flaky_write",
14070 serde_json::json!({"path": "./tmp.txt"}),
14071 ToolCallSource::Manual,
14072 ))
14073 .await
14074 .unwrap();
14075
14076 assert!(!record.success);
14077 assert_eq!(calls.load(Ordering::SeqCst), 1);
14078 }
14079
14080 #[tokio::test]
14081 async fn side_effecting_tools_are_serialized_per_resource() {
14082 let mock = mock_with_response("hello");
14083 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14084 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14085 let agent = Arc::new(
14086 AgentBuilder::new()
14087 .system_prompt("You are helpful.")
14088 .llm(Arc::new(mock))
14089 .tool(Arc::new(LockedWriteTool {
14090 active: Arc::clone(&active),
14091 max_active: Arc::clone(&max_active),
14092 }))
14093 .build()
14094 .unwrap(),
14095 );
14096
14097 let left = {
14098 let agent = Arc::clone(&agent);
14099 tokio::spawn(async move {
14100 agent
14101 .invoke_tool(ToolExecutionRequest::new(
14102 "lock-1",
14103 "locked_write",
14104 serde_json::json!({"path": "./same.txt"}),
14105 ToolCallSource::Manual,
14106 ))
14107 .await
14108 .unwrap()
14109 })
14110 };
14111 let right = {
14112 let agent = Arc::clone(&agent);
14113 tokio::spawn(async move {
14114 agent
14115 .invoke_tool(ToolExecutionRequest::new(
14116 "lock-2",
14117 "locked_write",
14118 serde_json::json!({"path": "./same.txt"}),
14119 ToolCallSource::Manual,
14120 ))
14121 .await
14122 .unwrap()
14123 })
14124 };
14125
14126 let left = left.await.unwrap();
14127 let right = right.await.unwrap();
14128 assert!(left.success);
14129 assert!(right.success);
14130 assert_eq!(max_active.load(Ordering::SeqCst), 1);
14131 }
14132
14133 #[tokio::test]
14134 async fn path_resources_use_shared_global_lock_and_cleanup() {
14135 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14136 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14137 let bindings = ai_agents_core::ToolPolicyBindings {
14138 path_fields: vec![
14139 ai_agents_core::PathPolicyBinding::read_write("source_path"),
14140 ai_agents_core::PathPolicyBinding::write("destination_path"),
14141 ],
14142 ..Default::default()
14143 };
14144 let classification = ai_agents_core::ToolCallClassification::from_metadata(
14145 &MultiResourceWriteTool {
14146 active: Arc::clone(&active),
14147 max_active: Arc::clone(&max_active),
14148 }
14149 .safety_metadata(),
14150 );
14151 let left_args = serde_json::json!({
14152 "source_path": "./a/../first.txt",
14153 "destination_path": "./second.txt"
14154 });
14155 let right_args = serde_json::json!({
14156 "source_path": "./second.txt",
14157 "destination_path": "./first.txt"
14158 });
14159 let left_keys = tool_resource_lock_keys(
14160 "multi_resource_write",
14161 &left_args,
14162 &bindings,
14163 &classification,
14164 );
14165 let right_keys = tool_resource_lock_keys(
14166 "multi_resource_write",
14167 &right_args,
14168 &bindings,
14169 &classification,
14170 );
14171 assert_eq!(left_keys, right_keys);
14172 assert_eq!(left_keys, vec!["path-mutation:global".to_string()]);
14173
14174 let locks = new_tool_resource_locks();
14175 let build_agent = || {
14176 AgentBuilder::new()
14177 .system_prompt("Test shared resource locks.")
14178 .llm(Arc::new(mock_with_response("done")))
14179 .tool(Arc::new(MultiResourceWriteTool {
14180 active: Arc::clone(&active),
14181 max_active: Arc::clone(&max_active),
14182 }))
14183 .build()
14184 .unwrap()
14185 .with_shared_resource_locks(Arc::clone(&locks))
14186 };
14187 let left_agent = Arc::new(build_agent());
14188 let right_agent = Arc::new(build_agent());
14189 let left = tokio::spawn(async move {
14190 left_agent
14191 .invoke_tool(ToolExecutionRequest::new(
14192 "multi-left",
14193 "multi_resource_write",
14194 left_args,
14195 ToolCallSource::Manual,
14196 ))
14197 .await
14198 .unwrap()
14199 });
14200 let right = tokio::spawn(async move {
14201 right_agent
14202 .invoke_tool(ToolExecutionRequest::new(
14203 "multi-right",
14204 "multi_resource_write",
14205 right_args,
14206 ToolCallSource::Manual,
14207 ))
14208 .await
14209 .unwrap()
14210 });
14211 let (left, right) = tokio::time::timeout(std::time::Duration::from_secs(2), async {
14212 tokio::join!(left, right)
14213 })
14214 .await
14215 .expect("reversed resource acquisition must not deadlock");
14216
14217 assert!(left.unwrap().success);
14218 assert!(right.unwrap().success);
14219 assert_eq!(max_active.load(Ordering::SeqCst), 1);
14220 assert!(locks.read().is_empty());
14221 }
14222
14223 #[tokio::test]
14224 async fn global_path_lock_serializes_copy_destination_with_file_write() {
14225 assert_path_mutation_pair_serialized(
14226 "copy_path",
14227 CopyPathTool::new().policy_bindings().path_fields,
14228 serde_json::json!({
14229 "source_path": "./source.txt",
14230 "destination_path": "./shared.txt"
14231 }),
14232 "file_write",
14233 FileWriteTool::new().policy_bindings().path_fields,
14234 serde_json::json!({"path": "./shared.txt"}),
14235 )
14236 .await;
14237 }
14238
14239 #[tokio::test]
14240 async fn parent_and_spawned_runtime_share_global_path_lock() {
14241 let workspace = MutationTestWorkspace::new();
14242 let destination = workspace.root.join("spawned.txt");
14243 let parent_gate = PathMutationGate::new();
14244 let parent = Arc::new(
14245 AgentBuilder::from_yaml(
14246 r#"
14247name: LockParent
14248system_prompt: parent
14249llm:
14250 default: default
14251tools:
14252 - parent_path_write
14253spawner:
14254 shared_llms: true
14255"#,
14256 )
14257 .unwrap()
14258 .llm(Arc::new(mock_with_response("done")))
14259 .auto_configure_spawner()
14260 .await
14261 .unwrap()
14262 .tool(Arc::new(BlockingPathMutationTool {
14263 id: "parent_path_write",
14264 path_fields: vec![ai_agents_core::PathPolicyBinding::write("path")],
14265 gate: parent_gate.clone(),
14266 }))
14267 .build()
14268 .unwrap(),
14269 );
14270
14271 let mut child_spec = crate::spec::AgentSpec {
14272 name: "LockChild".to_string(),
14273 system_prompt: "child".to_string(),
14274 tools: Some(vec![crate::spec::ToolEntry::Simple(
14275 "file_write".to_string(),
14276 )]),
14277 ..Default::default()
14278 };
14279 child_spec.tool_security.enabled = true;
14280 child_spec.tool_security.fail_closed = true;
14281 let file_write_policy = ai_agents_tools::ToolPolicyConfig {
14282 write_paths: vec![workspace.root.to_string_lossy().into_owned()],
14283 allow_without_confirmation: true,
14284 ..Default::default()
14285 };
14286 child_spec
14287 .tool_security
14288 .tools
14289 .insert("file_write".to_string(), file_write_policy);
14290 let spawned = parent
14291 .spawner()
14292 .unwrap()
14293 .spawn_from_spec(child_spec)
14294 .await
14295 .unwrap();
14296 assert!(Arc::ptr_eq(
14297 &parent.resource_locks,
14298 &spawned.agent.resource_locks
14299 ));
14300
14301 let parent_call = {
14302 let parent = Arc::clone(&parent);
14303 let destination = destination.clone();
14304 tokio::spawn(async move {
14305 parent
14306 .invoke_tool(ToolExecutionRequest::new(
14307 "parent-lock-holder",
14308 "parent_path_write",
14309 serde_json::json!({"path": destination}),
14310 ToolCallSource::Manual,
14311 ))
14312 .await
14313 .unwrap()
14314 })
14315 };
14316 parent_gate.wait_until_entered().await;
14317
14318 let child_call = {
14319 let child = Arc::clone(&spawned.agent);
14320 let destination = destination.clone();
14321 tokio::spawn(async move {
14322 child
14323 .invoke_tool(ToolExecutionRequest::new(
14324 "spawned-file-write",
14325 "file_write",
14326 serde_json::json!({
14327 "path": destination,
14328 "content": "spawned",
14329 "dry_run": false
14330 }),
14331 ToolCallSource::Manual,
14332 ))
14333 .await
14334 .unwrap()
14335 })
14336 };
14337 wait_for_resource_lock_strong_count(&parent.resource_locks, 2).await;
14338 assert!(!child_call.is_finished());
14339
14340 parent_gate.release();
14341 let (parent_record, child_record) =
14342 tokio::time::timeout(std::time::Duration::from_secs(2), async {
14343 tokio::join!(parent_call, child_call)
14344 })
14345 .await
14346 .expect("parent and spawned path mutations did not finish");
14347 assert!(parent_record.unwrap().success);
14348 assert!(child_record.unwrap().success);
14349 assert_eq!(std::fs::read_to_string(destination).unwrap(), "spawned");
14350 assert!(parent.resource_locks.read().is_empty());
14351 }
14352
14353 #[tokio::test]
14354 async fn cancelled_global_path_lock_waiter_does_not_retain_weak_entry() {
14355 let locks = new_tool_resource_locks();
14356 let holder_gate = PathMutationGate::new();
14357 let waiter_gate = PathMutationGate::new();
14358 waiter_gate.release();
14359 let holder = Arc::new(
14360 AgentBuilder::new()
14361 .system_prompt("Hold the global path lock.")
14362 .llm(Arc::new(mock_with_response("done")))
14363 .tool(Arc::new(BlockingPathMutationTool {
14364 id: "holder_write",
14365 path_fields: vec![ai_agents_core::PathPolicyBinding::write("path")],
14366 gate: holder_gate.clone(),
14367 }))
14368 .build()
14369 .unwrap()
14370 .with_shared_resource_locks(Arc::clone(&locks)),
14371 );
14372 let waiter = Arc::new(
14373 AgentBuilder::new()
14374 .system_prompt("Wait for the global path lock.")
14375 .llm(Arc::new(mock_with_response("done")))
14376 .tool(Arc::new(BlockingPathMutationTool {
14377 id: "waiter_write",
14378 path_fields: vec![ai_agents_core::PathPolicyBinding::write("path")],
14379 gate: waiter_gate.clone(),
14380 }))
14381 .build()
14382 .unwrap()
14383 .with_shared_resource_locks(Arc::clone(&locks)),
14384 );
14385
14386 let holder_call = {
14387 let holder = Arc::clone(&holder);
14388 tokio::spawn(async move {
14389 holder
14390 .invoke_tool(ToolExecutionRequest::new(
14391 "holder-call",
14392 "holder_write",
14393 serde_json::json!({"path": "./shared.txt"}),
14394 ToolCallSource::Manual,
14395 ))
14396 .await
14397 .unwrap()
14398 })
14399 };
14400 holder_gate.wait_until_entered().await;
14401
14402 let waiter_call = {
14403 let waiter = Arc::clone(&waiter);
14404 tokio::spawn(async move {
14405 waiter
14406 .invoke_tool(ToolExecutionRequest::new(
14407 "waiter-call",
14408 "waiter_write",
14409 serde_json::json!({"path": "./shared.txt"}),
14410 ToolCallSource::Manual,
14411 ))
14412 .await
14413 .unwrap()
14414 })
14415 };
14416 wait_for_resource_lock_strong_count(&locks, 2).await;
14417 waiter.runtime_control().cancel_all();
14418
14419 let waiter_record = tokio::time::timeout(std::time::Duration::from_secs(2), waiter_call)
14420 .await
14421 .expect("cancelled lock waiter did not finish")
14422 .unwrap();
14423 assert!(!waiter_record.executed);
14424 assert!(!waiter_gate.entered.load(Ordering::SeqCst));
14425 assert_eq!(
14426 locks
14427 .read()
14428 .get("path-mutation:global")
14429 .map_or(0, |lock| lock.strong_count()),
14430 1
14431 );
14432
14433 holder_gate.release();
14434 let holder_record = tokio::time::timeout(std::time::Duration::from_secs(2), holder_call)
14435 .await
14436 .expect("lock holder did not finish")
14437 .unwrap();
14438 assert!(holder_record.success);
14439 assert!(locks.read().is_empty());
14440 }
14441
14442 #[tokio::test]
14443 async fn path_mutation_policy_and_approval_denials_do_not_invoke_tools() {
14444 for denial in [MutationDenial::Policy, MutationDenial::Approval] {
14445 let tools: [Arc<dyn Tool>; 3] = [
14446 Arc::new(CopyPathTool::new()),
14447 Arc::new(MovePathTool::new()),
14448 Arc::new(DeletePathTool::new()),
14449 ];
14450 for tool in tools {
14451 assert_path_mutation_denied(tool, denial).await;
14452 }
14453 }
14454 }
14455
14456 #[tokio::test]
14457 async fn approval_argument_changes_are_rechecked_against_final_scope() {
14458 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14459 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14460 let entered = Arc::new(tokio::sync::Barrier::new(2));
14461 let release = Arc::new(tokio::sync::Notify::new());
14462 let handler = Arc::new(BlockingApprovalHandler {
14463 entered: Arc::clone(&entered),
14464 release: Arc::clone(&release),
14465 result: ApprovalResult::Modified {
14466 changes: HashMap::from([(
14467 "path".to_string(),
14468 Value::String("./after-approval.txt".to_string()),
14469 )]),
14470 },
14471 });
14472 let agent = Arc::new(
14473 AgentBuilder::new()
14474 .system_prompt("Test final scope validation.")
14475 .llm(Arc::new(mock_with_response("done")))
14476 .tool(Arc::new(LockedWriteTool {
14477 active: Arc::clone(&active),
14478 max_active: Arc::clone(&max_active),
14479 }))
14480 .tool_security(ToolSecurityEngine::new(approval_security_config(true)))
14481 .hitl_engine(HITLEngine::new(ai_agents_hitl::HITLConfig::default()))
14482 .approval_handler(handler)
14483 .build()
14484 .unwrap(),
14485 );
14486 let control = agent.runtime_control();
14487 let running = Arc::clone(&agent);
14488 let call = tokio::spawn(async move {
14489 running
14490 .invoke_tool(ToolExecutionRequest::new(
14491 "approval-scope",
14492 "locked_write",
14493 serde_json::json!({"path": "./before-approval.txt"}),
14494 ToolCallSource::Manual,
14495 ))
14496 .await
14497 .unwrap()
14498 });
14499 entered.wait().await;
14500 let expected_version = control.set_tool_scope(Vec::new());
14501 release.notify_one();
14502 let record = call.await.unwrap();
14503
14504 assert!(!record.executed);
14505 assert!(!record.success);
14506 assert_eq!(record.runtime_config_version, expected_version);
14507 assert_eq!(record.executed_arguments["path"], "./after-approval.txt");
14508 assert_eq!(max_active.load(Ordering::SeqCst), 0);
14509 assert_eq!(
14510 record.metadata["runtime_scope_snapshot"],
14511 serde_json::json!([])
14512 );
14513 }
14514
14515 #[tokio::test]
14516 async fn approval_is_rechecked_against_final_policy_snapshot() {
14517 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14518 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14519 let entered = Arc::new(tokio::sync::Barrier::new(2));
14520 let release = Arc::new(tokio::sync::Notify::new());
14521 let handler = Arc::new(BlockingApprovalHandler {
14522 entered: Arc::clone(&entered),
14523 release: Arc::clone(&release),
14524 result: ApprovalResult::Approved,
14525 });
14526 let agent = Arc::new(
14527 AgentBuilder::new()
14528 .system_prompt("Test final policy validation.")
14529 .llm(Arc::new(mock_with_response("done")))
14530 .tool(Arc::new(LockedWriteTool {
14531 active: Arc::clone(&active),
14532 max_active: Arc::clone(&max_active),
14533 }))
14534 .tool_security(ToolSecurityEngine::new(approval_security_config(true)))
14535 .hitl_engine(HITLEngine::new(ai_agents_hitl::HITLConfig::default()))
14536 .approval_handler(handler)
14537 .build()
14538 .unwrap(),
14539 );
14540 let control = agent.runtime_control();
14541 let running = Arc::clone(&agent);
14542 let call = tokio::spawn(async move {
14543 running
14544 .invoke_tool(ToolExecutionRequest::new(
14545 "approval-policy",
14546 "locked_write",
14547 serde_json::json!({"path": "./policy.txt"}),
14548 ToolCallSource::Manual,
14549 ))
14550 .await
14551 .unwrap()
14552 });
14553 entered.wait().await;
14554 let expected_version = control.set_tool_security(approval_security_config(false));
14555 release.notify_one();
14556 let record = call.await.unwrap();
14557
14558 assert!(!record.executed);
14559 assert!(!record.success);
14560 assert_eq!(record.runtime_config_version, expected_version);
14561 assert_eq!(record.policy.outcome, PermissionOutcome::Unavailable);
14562 assert_eq!(max_active.load(Ordering::SeqCst), 0);
14563 assert!(record.metadata.contains_key("policy_snapshot"));
14564 }
14565
14566 #[tokio::test]
14567 async fn persistent_override_preserves_rate_history_within_generation() {
14568 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14569 let agent = AgentBuilder::new()
14570 .system_prompt("Test persistent policy overrides.")
14571 .llm(Arc::new(mock_with_response("done")))
14572 .tool(Arc::new(RecoveryTestTool {
14573 id: "limited_override".to_string(),
14574 succeeds: true,
14575 calls: Arc::clone(&calls),
14576 }))
14577 .build()
14578 .unwrap();
14579 let mut security = ToolSecurityConfig {
14580 enabled: true,
14581 fail_closed: true,
14582 ..Default::default()
14583 };
14584 let policy = ai_agents_tools::ToolPolicyConfig {
14585 write_paths: vec![".".to_string()],
14586 rate_limit: Some(1),
14587 ..Default::default()
14588 };
14589 security
14590 .tools
14591 .insert("limited_override".to_string(), policy);
14592 let generation = agent.runtime_control().set_tool_security(security);
14593
14594 let first = agent
14595 .invoke_tool(ToolExecutionRequest::new(
14596 "limited-first",
14597 "limited_override",
14598 serde_json::json!({"path": "./limited.txt"}),
14599 ToolCallSource::Manual,
14600 ))
14601 .await
14602 .unwrap();
14603 let second = agent
14604 .invoke_tool(ToolExecutionRequest::new(
14605 "limited-second",
14606 "limited_override",
14607 serde_json::json!({"path": "./limited.txt"}),
14608 ToolCallSource::Manual,
14609 ))
14610 .await
14611 .unwrap();
14612
14613 assert!(first.success);
14614 assert_eq!(first.policy_version, generation);
14615 assert!(!second.executed);
14616 assert!(second.output.contains("Rate limit exceeded"));
14617 assert_eq!(second.policy_version, generation);
14618 assert_eq!(calls.load(Ordering::SeqCst), 1);
14619 }
14620
14621 #[tokio::test]
14622 async fn concurrent_rate_admission_consumes_capacity_atomically() {
14623 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14624 let tool = Arc::new(RecoveryTestTool {
14625 id: "atomic_rate".to_string(),
14626 succeeds: true,
14627 calls: Arc::clone(&calls),
14628 });
14629 let arguments = serde_json::json!({"path": "./atomic-rate.txt"});
14630 let bindings = tool.policy_bindings();
14631 let classification = tool.classify_call(&arguments);
14632 let resource_keys =
14633 tool_resource_lock_keys(tool.id(), &arguments, &bindings, &classification);
14634 let mut security = ToolSecurityConfig {
14635 enabled: true,
14636 fail_closed: true,
14637 ..Default::default()
14638 };
14639 let policy = ai_agents_tools::ToolPolicyConfig {
14640 write_paths: vec![".".to_string()],
14641 rate_limit: Some(1),
14642 ..Default::default()
14643 };
14644 security.tools.insert(tool.id().to_string(), policy);
14645 let agent = Arc::new(
14646 AgentBuilder::new()
14647 .system_prompt("Test atomic rate admission.")
14648 .llm(Arc::new(mock_with_response("done")))
14649 .tool(tool)
14650 .tool_security(ToolSecurityEngine::new(security))
14651 .build()
14652 .unwrap(),
14653 );
14654 let held = agent
14655 .acquire_tool_resource_locks(&resource_keys)
14656 .await
14657 .unwrap();
14658 let left = {
14659 let agent = Arc::clone(&agent);
14660 let arguments = arguments.clone();
14661 tokio::spawn(async move {
14662 agent
14663 .invoke_tool(ToolExecutionRequest::new(
14664 "atomic-rate-left",
14665 "atomic_rate",
14666 arguments,
14667 ToolCallSource::Manual,
14668 ))
14669 .await
14670 .unwrap()
14671 })
14672 };
14673 let right = {
14674 let agent = Arc::clone(&agent);
14675 tokio::spawn(async move {
14676 agent
14677 .invoke_tool(ToolExecutionRequest::new(
14678 "atomic-rate-right",
14679 "atomic_rate",
14680 arguments,
14681 ToolCallSource::Manual,
14682 ))
14683 .await
14684 .unwrap()
14685 })
14686 };
14687 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
14688 drop(held);
14689 let (left, right) = tokio::join!(left, right);
14690 let records = [left.unwrap(), right.unwrap()];
14691
14692 assert_eq!(records.iter().filter(|record| record.success).count(), 1);
14693 assert_eq!(records.iter().filter(|record| record.executed).count(), 1);
14694 assert!(
14695 records.iter().any(|record| {
14696 !record.executed && record.output.contains("Rate limit exceeded")
14697 })
14698 );
14699 assert_eq!(calls.load(Ordering::SeqCst), 1);
14700 }
14701
14702 #[tokio::test]
14703 async fn changed_policy_generation_invalidates_pending_approval() {
14704 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14705 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14706 let entered = Arc::new(tokio::sync::Barrier::new(2));
14707 let release = Arc::new(tokio::sync::Notify::new());
14708 let handler = Arc::new(BlockingApprovalHandler {
14709 entered: Arc::clone(&entered),
14710 release: Arc::clone(&release),
14711 result: ApprovalResult::Approved,
14712 });
14713 let agent = Arc::new(
14714 AgentBuilder::new()
14715 .system_prompt("Test stale approval denial.")
14716 .llm(Arc::new(mock_with_response("done")))
14717 .tool(Arc::new(LockedWriteTool {
14718 active: Arc::clone(&active),
14719 max_active: Arc::clone(&max_active),
14720 }))
14721 .tool_security(ToolSecurityEngine::new(approval_security_config(true)))
14722 .hitl_engine(HITLEngine::new(ai_agents_hitl::HITLConfig::default()))
14723 .approval_handler(handler)
14724 .build()
14725 .unwrap(),
14726 );
14727 let running = Arc::clone(&agent);
14728 let call = tokio::spawn(async move {
14729 running
14730 .invoke_tool(ToolExecutionRequest::new(
14731 "stale-approval",
14732 "locked_write",
14733 serde_json::json!({"path": "./stale.txt"}),
14734 ToolCallSource::Manual,
14735 ))
14736 .await
14737 .unwrap()
14738 });
14739 entered.wait().await;
14740 let generation = agent
14741 .runtime_control()
14742 .set_tool_security(approval_security_config(true));
14743 release.notify_one();
14744 let record = call.await.unwrap();
14745
14746 assert!(!record.executed);
14747 assert!(record.output.contains("Approval became stale"));
14748 assert_eq!(record.policy_version, generation);
14749 assert_eq!(max_active.load(Ordering::SeqCst), 0);
14750 }
14751
14752 #[tokio::test]
14753 async fn final_policy_reapplies_argument_caps_after_approval_changes() {
14754 use ai_agents_hitl::CallbackHandler;
14755
14756 let mut security = ToolSecurityConfig {
14757 enabled: true,
14758 fail_closed: true,
14759 ..Default::default()
14760 };
14761 let policy = ai_agents_tools::ToolPolicyConfig {
14762 read_paths: vec![".".to_string()],
14763 max_results: Some(5),
14764 require_confirmation: true,
14765 ..Default::default()
14766 };
14767 security.tools.insert("context_echo".to_string(), policy);
14768 let handler = CallbackHandler::new(|_| ApprovalResult::Modified {
14769 changes: HashMap::from([("max_results".to_string(), serde_json::json!(99))]),
14770 });
14771 let agent = AgentBuilder::new()
14772 .system_prompt("Test final argument caps.")
14773 .llm(Arc::new(mock_with_response("done")))
14774 .tool(Arc::new(ContextEchoTool))
14775 .tool_security(ToolSecurityEngine::new(security))
14776 .hitl_engine(HITLEngine::new(ai_agents_hitl::HITLConfig::default()))
14777 .approval_handler(Arc::new(handler))
14778 .build()
14779 .unwrap();
14780
14781 let record = agent
14782 .invoke_tool(ToolExecutionRequest::new(
14783 "final-cap",
14784 "context_echo",
14785 serde_json::json!({"path": ".", "max_results": 1}),
14786 ToolCallSource::Manual,
14787 ))
14788 .await
14789 .unwrap();
14790
14791 assert!(record.success);
14792 assert_eq!(record.executed_arguments["max_results"], 5);
14793 assert_eq!(
14794 record.approval.unwrap().modified_arguments.unwrap()["max_results"],
14795 5
14796 );
14797 }
14798
14799 #[tokio::test]
14800 async fn no_binding_writes_use_canonical_fallback_lock() {
14801 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14802 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14803 let agent = Arc::new(
14804 AgentBuilder::new()
14805 .system_prompt("Test fallback resource locks.")
14806 .llm(Arc::new(mock_with_response("done")))
14807 .tool(Arc::new(NoBindingWriteTool {
14808 active: Arc::clone(&active),
14809 max_active: Arc::clone(&max_active),
14810 }))
14811 .build()
14812 .unwrap(),
14813 );
14814 let left = {
14815 let agent = Arc::clone(&agent);
14816 tokio::spawn(async move {
14817 agent
14818 .invoke_tool(ToolExecutionRequest::new(
14819 "no-binding-left",
14820 "no_binding_write",
14821 serde_json::json!({}),
14822 ToolCallSource::Manual,
14823 ))
14824 .await
14825 .unwrap()
14826 })
14827 };
14828 let right = {
14829 let agent = Arc::clone(&agent);
14830 tokio::spawn(async move {
14831 agent
14832 .invoke_tool(ToolExecutionRequest::new(
14833 "no-binding-right",
14834 "no_binding_write",
14835 serde_json::json!({}),
14836 ToolCallSource::Manual,
14837 ))
14838 .await
14839 .unwrap()
14840 })
14841 };
14842 let (left, right) = tokio::join!(left, right);
14843
14844 assert!(left.unwrap().success);
14845 assert!(right.unwrap().success);
14846 assert_eq!(max_active.load(Ordering::SeqCst), 1);
14847 }
14848
14849 #[tokio::test]
14850 async fn parent_and_child_paths_share_a_resource_lock() {
14851 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14852 let max_active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14853 let agent = Arc::new(
14854 AgentBuilder::new()
14855 .system_prompt("Test parent child resource locks.")
14856 .llm(Arc::new(mock_with_response("done")))
14857 .tool(Arc::new(LockedWriteTool {
14858 active: Arc::clone(&active),
14859 max_active: Arc::clone(&max_active),
14860 }))
14861 .build()
14862 .unwrap(),
14863 );
14864 let parent = format!("./lock-parent-{}", uuid::Uuid::new_v4());
14865 let child = format!("{}/child.txt", parent);
14866 let left = {
14867 let agent = Arc::clone(&agent);
14868 tokio::spawn(async move {
14869 agent
14870 .invoke_tool(ToolExecutionRequest::new(
14871 "parent-lock",
14872 "locked_write",
14873 serde_json::json!({"path": parent}),
14874 ToolCallSource::Manual,
14875 ))
14876 .await
14877 .unwrap()
14878 })
14879 };
14880 let right = {
14881 let agent = Arc::clone(&agent);
14882 tokio::spawn(async move {
14883 agent
14884 .invoke_tool(ToolExecutionRequest::new(
14885 "child-lock",
14886 "locked_write",
14887 serde_json::json!({"path": child}),
14888 ToolCallSource::Manual,
14889 ))
14890 .await
14891 .unwrap()
14892 })
14893 };
14894 let (left, right) = tokio::join!(left, right);
14895
14896 assert!(left.unwrap().success);
14897 assert!(right.unwrap().success);
14898 assert_eq!(max_active.load(Ordering::SeqCst), 1);
14899 }
14900
14901 #[tokio::test]
14902 async fn tool_hooks_can_reenter_after_resource_guards_are_dropped() {
14903 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14904 let hooks = Arc::new(ReentrantToolHooks {
14905 agent: parking_lot::Mutex::new(None),
14906 invoked: AtomicBool::new(false),
14907 nested_success: AtomicBool::new(false),
14908 });
14909 let agent = Arc::new(
14910 AgentBuilder::new()
14911 .system_prompt("Test hook reentrancy.")
14912 .llm(Arc::new(mock_with_response("done")))
14913 .tool(Arc::new(RecoveryTestTool {
14914 id: "reentrant_write".to_string(),
14915 succeeds: true,
14916 calls: Arc::clone(&calls),
14917 }))
14918 .hooks(hooks.clone())
14919 .build()
14920 .unwrap(),
14921 );
14922 *hooks.agent.lock() = Some(Arc::downgrade(&agent));
14923 let record = tokio::time::timeout(
14924 std::time::Duration::from_secs(2),
14925 agent.invoke_tool(ToolExecutionRequest::new(
14926 "outer-hook-call",
14927 "reentrant_write",
14928 serde_json::json!({"path": "./hook.txt"}),
14929 ToolCallSource::Manual,
14930 )),
14931 )
14932 .await
14933 .expect("tool completion hook must not retain resource guards")
14934 .unwrap();
14935
14936 assert!(record.success);
14937 assert!(hooks.nested_success.load(Ordering::SeqCst));
14938 assert_eq!(calls.load(Ordering::SeqCst), 2);
14939 }
14940
14941 #[tokio::test]
14942 async fn fallback_releases_primary_resource_locks() {
14943 let primary_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14944 let fallback_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
14945 let agent = AgentBuilder::new()
14946 .system_prompt("Test fallback execution.")
14947 .llm(Arc::new(mock_with_response("done")))
14948 .tool(Arc::new(RecoveryTestTool {
14949 id: "primary".to_string(),
14950 succeeds: false,
14951 calls: Arc::clone(&primary_calls),
14952 }))
14953 .tool(Arc::new(RecoveryTestTool {
14954 id: "fallback".to_string(),
14955 succeeds: true,
14956 calls: Arc::clone(&fallback_calls),
14957 }))
14958 .recovery_manager(recovery_manager_with_fallbacks([(
14959 "primary".to_string(),
14960 "fallback".to_string(),
14961 )]))
14962 .build()
14963 .unwrap();
14964 let record = tokio::time::timeout(
14965 std::time::Duration::from_secs(2),
14966 agent.invoke_tool(ToolExecutionRequest::new(
14967 "fallback-call",
14968 "primary",
14969 serde_json::json!({"path": "./shared.txt"}),
14970 ToolCallSource::Manual,
14971 )),
14972 )
14973 .await
14974 .expect("fallback must not retain the primary resource guard")
14975 .unwrap();
14976
14977 assert!(record.success);
14978 assert_eq!(record.canonical_id, "fallback");
14979 assert_eq!(record.call_id, "fallback-call");
14980 assert!(matches!(record.source, ToolCallSource::Fallback { .. }));
14981 assert_eq!(primary_calls.load(Ordering::SeqCst), 1);
14982 assert_eq!(fallback_calls.load(Ordering::SeqCst), 1);
14983 }
14984
14985 #[tokio::test]
14986 async fn diagnostics_without_provider_records_unavailable_without_execution() {
14987 let mock = mock_with_response("hello");
14988 let yaml = r#"
14989name: DiagnosticsNoProviderAgent
14990system_prompt: "Review diagnostics."
14991tools: [diagnostics]
14992"#;
14993 let agent = AgentBuilder::from_yaml(yaml)
14994 .unwrap()
14995 .llm(Arc::new(mock))
14996 .auto_configure_features()
14997 .unwrap()
14998 .build()
14999 .unwrap();
15000
15001 let record = agent
15002 .invoke_tool(ToolExecutionRequest::new(
15003 "diagnostics-call",
15004 "diagnostics",
15005 serde_json::json!({}),
15006 ToolCallSource::Manual,
15007 ))
15008 .await
15009 .unwrap();
15010
15011 assert!(!record.executed);
15012 assert!(!record.success);
15013 assert_eq!(record.policy.outcome, PermissionOutcome::Unavailable);
15014 }
15015
15016 #[tokio::test]
15017 async fn web_search_without_provider_records_unavailable_without_execution() {
15018 let mock = mock_with_response("hello");
15019 let yaml = r#"
15020name: WebSearchNoProviderAgent
15021system_prompt: "You search the web."
15022tools: [web_search]
15023"#;
15024 let agent = AgentBuilder::from_yaml(yaml)
15025 .unwrap()
15026 .llm(Arc::new(mock))
15027 .auto_configure_features()
15028 .unwrap()
15029 .build()
15030 .unwrap();
15031
15032 let record = agent
15033 .invoke_tool(ToolExecutionRequest::new(
15034 "web-search-call",
15035 "web_search",
15036 serde_json::json!({"query": "rust async"}),
15037 ToolCallSource::Manual,
15038 ))
15039 .await
15040 .unwrap();
15041
15042 assert!(!record.executed);
15043 assert!(!record.success);
15044 assert_eq!(record.policy.outcome, PermissionOutcome::Unavailable);
15045 }
15046
15047 #[tokio::test]
15048 async fn test_spawner_section_does_not_grant_core_tools_when_top_level_tools_omitted() {
15049 let mock = mock_with_response("hello");
15050 let yaml = r#"
15051name: SpawnerNoGrantAgent
15052system_prompt: "You manage agents."
15053spawner:
15054 max_agents: 2
15055"#;
15056 let agent = AgentBuilder::from_yaml(yaml)
15057 .unwrap()
15058 .llm(Arc::new(mock))
15059 .auto_configure_features()
15060 .unwrap()
15061 .auto_configure_spawner()
15062 .await
15063 .unwrap()
15064 .build()
15065 .unwrap();
15066
15067 let available = agent.get_available_tool_ids().await.unwrap();
15068 assert!(available.is_empty());
15069 }
15070
15071 #[tokio::test]
15072 async fn test_spawner_section_does_not_grant_core_tools_when_top_level_tools_empty() {
15073 let mock = mock_with_response("hello");
15074 let yaml = r#"
15075name: EmptySpawnerNoGrantAgent
15076system_prompt: "You manage agents."
15077tools: []
15078spawner:
15079 max_agents: 2
15080"#;
15081 let agent = AgentBuilder::from_yaml(yaml)
15082 .unwrap()
15083 .llm(Arc::new(mock))
15084 .auto_configure_features()
15085 .unwrap()
15086 .auto_configure_spawner()
15087 .await
15088 .unwrap()
15089 .build()
15090 .unwrap();
15091
15092 let available = agent.get_available_tool_ids().await.unwrap();
15093 assert!(available.is_empty());
15094 }
15095
15096 #[tokio::test]
15097 async fn test_management_tools_flag_grants_core_tools_when_top_level_tools_empty() {
15098 let mock = mock_with_response("hello");
15099 let yaml = r#"
15100name: ManagementGrantAgent
15101system_prompt: "You manage agents."
15102tools: []
15103spawner:
15104 management_tools: true
15105"#;
15106 let agent = AgentBuilder::from_yaml(yaml)
15107 .unwrap()
15108 .llm(Arc::new(mock))
15109 .auto_configure_features()
15110 .unwrap()
15111 .auto_configure_spawner()
15112 .await
15113 .unwrap()
15114 .build()
15115 .unwrap();
15116
15117 let available = agent.get_available_tool_ids().await.unwrap();
15118 assert_eq!(available.len(), 4);
15119 assert!(available.contains(&"spawn_agent".to_string()));
15120 assert!(available.contains(&"send_agent_message".to_string()));
15121 assert!(available.contains(&"list_agents".to_string()));
15122 assert!(available.contains(&"remove_agent".to_string()));
15123 }
15124
15125 #[tokio::test]
15126 async fn test_management_tools_flag_grants_core_tools_when_top_level_tools_omitted() {
15127 let mock = mock_with_response("hello");
15128 let yaml = r#"
15129name: ManagementOmittedToolsGrantAgent
15130system_prompt: "You manage agents."
15131spawner:
15132 management_tools: true
15133"#;
15134 let agent = AgentBuilder::from_yaml(yaml)
15135 .unwrap()
15136 .llm(Arc::new(mock))
15137 .auto_configure_features()
15138 .unwrap()
15139 .auto_configure_spawner()
15140 .await
15141 .unwrap()
15142 .build()
15143 .unwrap();
15144
15145 let available = agent.get_available_tool_ids().await.unwrap();
15146 assert_eq!(available.len(), 4);
15147 assert!(available.contains(&"spawn_agent".to_string()));
15148 assert!(available.contains(&"send_agent_message".to_string()));
15149 assert!(available.contains(&"list_agents".to_string()));
15150 assert!(available.contains(&"remove_agent".to_string()));
15151 }
15152
15153 #[tokio::test]
15154 async fn test_management_tools_selected_grants_only_selected_tools() {
15155 let mock = mock_with_response("hello");
15156 let yaml = r#"
15157name: ManagementSelectedGrantAgent
15158system_prompt: "You manage agents."
15159tools: []
15160spawner:
15161 management_tools:
15162 - spawn_agent
15163 - send_agent_message
15164 - list_agents
15165"#;
15166 let agent = AgentBuilder::from_yaml(yaml)
15167 .unwrap()
15168 .llm(Arc::new(mock))
15169 .auto_configure_features()
15170 .unwrap()
15171 .auto_configure_spawner()
15172 .await
15173 .unwrap()
15174 .build()
15175 .unwrap();
15176
15177 let available = agent.get_available_tool_ids().await.unwrap();
15178 assert_eq!(available.len(), 3);
15179 assert!(available.contains(&"spawn_agent".to_string()));
15180 assert!(available.contains(&"send_agent_message".to_string()));
15181 assert!(available.contains(&"list_agents".to_string()));
15182 assert!(!available.contains(&"remove_agent".to_string()));
15183 }
15184
15185 #[tokio::test]
15186 async fn test_orchestration_tools_flag_grants_tools_when_top_level_tools_empty() {
15187 let mock = mock_with_response("hello");
15188 let yaml = r#"
15189name: OrchestrationGrantAgent
15190system_prompt: "You coordinate agents."
15191llms:
15192 default:
15193 provider: openai
15194 model: gpt-4
15195 router:
15196 provider: openai
15197 model: gpt-4
15198llm:
15199 default: default
15200 router: router
15201tools: []
15202spawner:
15203 orchestration_tools: true
15204"#;
15205 let agent = AgentBuilder::from_yaml(yaml)
15206 .unwrap()
15207 .llm(Arc::new(mock))
15208 .auto_configure_features()
15209 .unwrap()
15210 .auto_configure_spawner()
15211 .await
15212 .unwrap()
15213 .build()
15214 .unwrap();
15215
15216 let available = agent.get_available_tool_ids().await.unwrap();
15217 assert_eq!(available.len(), 5);
15218 assert!(available.contains(&"route_to_agent".to_string()));
15219 assert!(available.contains(&"pipeline_process".to_string()));
15220 assert!(available.contains(&"concurrent_ask".to_string()));
15221 assert!(available.contains(&"group_discussion".to_string()));
15222 assert!(available.contains(&"handoff_conversation".to_string()));
15223 }
15224
15225 #[tokio::test]
15226 async fn test_persona_evolve_flag_grants_tool_when_top_level_tools_empty() {
15227 let mock = mock_with_response("hello");
15228 let yaml = r#"
15229name: PersonaGrantAgent
15230system_prompt: "You can evolve persona."
15231llm:
15232 provider: openai
15233 model: gpt-4
15234tools: []
15235persona:
15236 identity:
15237 name: "Guide"
15238 role: "Helper"
15239 evolution:
15240 enabled: true
15241 allow_llm_evolve: true
15242 mutable_fields:
15243 - traits.personality
15244"#;
15245 let agent = AgentBuilder::from_yaml(yaml)
15246 .unwrap()
15247 .llm(Arc::new(mock))
15248 .build()
15249 .unwrap();
15250
15251 let available = agent.get_available_tool_ids().await.unwrap();
15252 assert_eq!(available, vec!["persona_evolve".to_string()]);
15253 }
15254
15255 #[tokio::test]
15256 async fn test_persona_evolve_flag_grants_tool_when_top_level_tools_omitted() {
15257 let mock = mock_with_response("hello");
15258 let yaml = r#"
15259name: PersonaOmittedToolsGrantAgent
15260system_prompt: "You can evolve persona."
15261llm:
15262 provider: openai
15263 model: gpt-4
15264persona:
15265 identity:
15266 name: "Guide"
15267 role: "Helper"
15268 evolution:
15269 enabled: true
15270 allow_llm_evolve: true
15271 mutable_fields:
15272 - traits.personality
15273"#;
15274 let agent = AgentBuilder::from_yaml(yaml)
15275 .unwrap()
15276 .llm(Arc::new(mock))
15277 .build()
15278 .unwrap();
15279
15280 let available = agent.get_available_tool_ids().await.unwrap();
15281 assert_eq!(available, vec!["persona_evolve".to_string()]);
15282 }
15283
15284 #[tokio::test]
15285 async fn test_omitted_yaml_tools_exposes_no_tools() {
15286 let mock = mock_with_response("hello");
15287 let yaml = r#"
15288name: NoToolsAgent
15289system_prompt: "You are helpful."
15290"#;
15291 let agent = AgentBuilder::from_yaml(yaml)
15292 .unwrap()
15293 .llm(Arc::new(mock))
15294 .auto_configure_features()
15295 .unwrap()
15296 .build()
15297 .unwrap();
15298
15299 let available = agent.get_available_tool_ids().await.unwrap();
15300 assert!(available.is_empty());
15301 }
15302
15303 #[tokio::test]
15304 async fn test_state_tools_cannot_widen_top_level_grant() {
15305 let mock = mock_with_response("hello");
15306 let yaml = r#"
15307name: NarrowToolsAgent
15308system_prompt: "You are helpful."
15309tools:
15310 - calculator
15311states:
15312 initial: current
15313 states:
15314 current:
15315 tools: [datetime]
15316"#;
15317 let agent = AgentBuilder::from_yaml(yaml)
15318 .unwrap()
15319 .llm(Arc::new(mock))
15320 .auto_configure_features()
15321 .unwrap()
15322 .build()
15323 .unwrap();
15324
15325 let available = agent.get_available_tool_ids().await.unwrap();
15326 assert!(available.is_empty());
15327 }
15328
15329 #[tokio::test]
15331 async fn test_integration_tool_execution() {
15332 let mock = mock_with_responses(vec![
15334 r#"I'll calculate that for you.
15336[TOOL_CALL: {"name": "calculator", "arguments": {"expression": "2+2"}}]"#,
15337 "The answer is 4.",
15339 ]);
15340 let mut tools = ai_agents_tools::ToolRegistry::new();
15341 tools
15342 .register(Arc::new(ai_agents_tools::CalculatorTool))
15343 .unwrap();
15344
15345 let agent = AgentBuilder::new()
15346 .system_prompt("You are a calculator assistant.")
15347 .llm(Arc::new(mock))
15348 .tools(tools)
15349 .build()
15350 .unwrap();
15351
15352 let response = agent.chat("What is 2+2?").await.unwrap();
15353 assert!(!response.content.is_empty());
15355 }
15356
15357 #[tokio::test]
15358 async fn test_tool_hitl_rejection_finalizes_blocking_turn() {
15359 let responses = Arc::new(std::sync::atomic::AtomicUsize::new(0));
15360 let hooks = Arc::new(ResponseCountingHooks {
15361 responses: Arc::clone(&responses),
15362 });
15363 let mock = mock_with_response(r#"{"tool":"echo","arguments":{"message":"hello"}}"#);
15364 let yaml = r#"
15365name: ToolRejectAgent
15366system_prompt: "You use tools when requested."
15367tools:
15368 - echo
15369hitl:
15370 tools:
15371 echo:
15372 require_approval: true
15373 approval_message: "Approve echo?"
15374"#;
15375 let agent = AgentBuilder::from_yaml(yaml)
15376 .unwrap()
15377 .llm(Arc::new(mock))
15378 .auto_configure_features()
15379 .unwrap()
15380 .hooks(hooks)
15381 .build()
15382 .unwrap();
15383
15384 let response = agent.chat("echo hello").await.unwrap();
15385
15386 assert!(
15387 response.content.contains("Operation cancelled"),
15388 "unexpected response: {}",
15389 response.content
15390 );
15391 assert_eq!(responses.load(Ordering::SeqCst), 1);
15392 let messages = agent.memory.get_messages(None).await.unwrap();
15393 assert_eq!(messages.len(), 3);
15394 assert_eq!(messages[0].content, "echo hello");
15395 assert!(messages[1].content.contains("\"tool\":\"echo\""));
15396 assert!(messages[2].content.contains("rejected by the approver"));
15397 }
15398
15399 #[tokio::test]
15400 async fn test_tool_hitl_rejection_finalizes_streaming_turn() {
15401 use futures::StreamExt;
15402
15403 let responses = Arc::new(std::sync::atomic::AtomicUsize::new(0));
15404 let hooks = Arc::new(ResponseCountingHooks {
15405 responses: Arc::clone(&responses),
15406 });
15407 let mock = mock_with_response(r#"{"tool":"echo","arguments":{"message":"hello"}}"#);
15408 let yaml = r#"
15409name: ToolRejectStreamingAgent
15410system_prompt: "You use tools when requested."
15411tools:
15412 - echo
15413streaming:
15414 enabled: true
15415hitl:
15416 tools:
15417 echo:
15418 require_approval: true
15419 approval_message: "Approve echo?"
15420"#;
15421 let agent = AgentBuilder::from_yaml(yaml)
15422 .unwrap()
15423 .llm(Arc::new(mock))
15424 .auto_configure_features()
15425 .unwrap()
15426 .hooks(hooks)
15427 .build()
15428 .unwrap();
15429
15430 let mut stream = agent.chat_stream("echo hello").await.unwrap();
15431 let mut terminal_error = String::new();
15432 let mut done = false;
15433 while let Some(chunk) = stream.next().await {
15434 match chunk {
15435 StreamChunk::Error { message } => terminal_error = message,
15436 StreamChunk::Done {} => {
15437 done = true;
15438 break;
15439 }
15440 _ => {}
15441 }
15442 }
15443
15444 assert!(done);
15445 assert!(
15446 terminal_error.contains("Operation cancelled"),
15447 "unexpected terminal error: {}",
15448 terminal_error
15449 );
15450 assert_eq!(responses.load(Ordering::SeqCst), 1);
15451 let messages = agent.memory.get_messages(None).await.unwrap();
15452 assert_eq!(messages.len(), 3);
15453 assert_eq!(messages[0].content, "echo hello");
15454 assert!(messages[1].content.contains("\"tool\":\"echo\""));
15455 assert!(messages[2].content.contains("rejected by the approver"));
15456 }
15457
15458 #[tokio::test]
15459 async fn test_pre_response_guard_transition_skips_old_state_llm() {
15460 let mock = mock_with_response("Billing state response");
15461 let call_counter = mock.clone();
15462 let yaml = r#"
15463name: OptimizedStateAgent
15464system_prompt: "You route before answering."
15465runtime:
15466 optimization:
15467 enabled: true
15468 pre_response_deterministic_transitions: true
15469states:
15470 initial: greeting
15471 states:
15472 greeting:
15473 prompt: "Old state prompt that should be skipped."
15474 transitions:
15475 - to: billing
15476 guard:
15477 context:
15478 topic:
15479 eq: billing
15480 timing: pre_response
15481 billing:
15482 prompt: "Answer from the billing state."
15483"#;
15484 let agent = AgentBuilder::from_yaml(yaml)
15485 .unwrap()
15486 .llm(Arc::new(mock))
15487 .build()
15488 .unwrap();
15489 agent
15490 .set_context("topic", serde_json::json!("billing"))
15491 .unwrap();
15492
15493 let response = agent.chat("I need billing help").await.unwrap();
15494
15495 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15496 assert_eq!(response.content, "Billing state response");
15497 assert_eq!(call_counter.call_count(), 1);
15498 assert_eq!(agent.actor_facts().len(), 0);
15499 }
15500
15501 #[tokio::test]
15502 async fn test_set_context_supports_dotted_paths_for_pre_response_guards() {
15503 let mock = mock_with_response("Billing state response");
15504 let call_counter = mock.clone();
15505 let yaml = r#"
15506name: OptimizedStateAgent
15507system_prompt: "You route before answering."
15508runtime:
15509 optimization:
15510 enabled: true
15511 pre_response_deterministic_transitions: true
15512context:
15513 request:
15514 type: runtime
15515 default:
15516 topic: general
15517states:
15518 initial: greeting
15519 states:
15520 greeting:
15521 prompt: "Old state prompt that should be skipped."
15522 transitions:
15523 - to: billing
15524 guard:
15525 context:
15526 request.topic:
15527 eq: billing
15528 timing: pre_response
15529 billing:
15530 prompt: "Answer from the billing state."
15531"#;
15532 let agent = AgentBuilder::from_yaml(yaml)
15533 .unwrap()
15534 .llm(Arc::new(mock))
15535 .build()
15536 .unwrap();
15537 agent
15538 .set_context("request.topic", serde_json::json!("billing"))
15539 .unwrap();
15540
15541 let response = agent.chat("I need billing help").await.unwrap();
15542
15543 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15544 assert_eq!(response.content, "Billing state response");
15545 assert_eq!(call_counter.call_count(), 1);
15546 assert_eq!(
15547 agent.get_context().get("request"),
15548 Some(&serde_json::json!({"topic": "billing"}))
15549 );
15550 }
15551
15552 #[tokio::test]
15553 async fn test_pre_response_rejection_does_not_commit_staged_context_or_user() {
15554 let mock = mock_with_response("billing");
15555 let yaml = r#"
15556name: OptimizedStateAgent
15557system_prompt: "You route before answering."
15558runtime:
15559 optimization:
15560 enabled: true
15561 pre_response_deterministic_transitions: true
15562hitl:
15563 states:
15564 billing:
15565 on_enter: require_approval
15566 approval_message: "Approve billing route?"
15567states:
15568 initial: greeting
15569 states:
15570 greeting:
15571 prompt: "Old state prompt."
15572 extract:
15573 - key: topic
15574 description: "Support topic"
15575 transitions:
15576 - to: billing
15577 guard:
15578 context:
15579 topic:
15580 eq: billing
15581 timing: pre_response
15582 run_extractors: true
15583 billing:
15584 prompt: "Billing state."
15585"#;
15586 let agent = AgentBuilder::from_yaml(yaml)
15587 .unwrap()
15588 .llm(Arc::new(mock))
15589 .build()
15590 .unwrap();
15591
15592 let response = agent
15593 .try_pre_response_transition("billing please")
15594 .await
15595 .unwrap();
15596
15597 assert!(response.is_none());
15598 assert_eq!(agent.current_state().as_deref(), Some("greeting"));
15599 assert!(!agent.get_context().contains_key("topic"));
15600 assert_eq!(agent.memory.get_messages(None).await.unwrap().len(), 0);
15601 }
15602
15603 #[tokio::test]
15604 async fn test_pre_response_extractor_commits_context_on_winning_path() {
15605 let mock = mock_with_responses(vec!["billing", "Billing response"]);
15606 let yaml = r#"
15607name: OptimizedStateAgent
15608system_prompt: "You route before answering."
15609runtime:
15610 optimization:
15611 enabled: true
15612 pre_response_deterministic_transitions: true
15613states:
15614 initial: greeting
15615 states:
15616 greeting:
15617 prompt: "Old state prompt."
15618 extract:
15619 - key: topic
15620 description: "Support topic"
15621 transitions:
15622 - to: billing
15623 guard:
15624 context:
15625 topic:
15626 eq: billing
15627 timing: pre_response
15628 run_extractors: true
15629 billing:
15630 prompt: "Billing state."
15631"#;
15632 let agent = AgentBuilder::from_yaml(yaml)
15633 .unwrap()
15634 .llm(Arc::new(mock))
15635 .build()
15636 .unwrap();
15637
15638 let response = agent.chat("billing please").await.unwrap();
15639
15640 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15641 assert_eq!(response.content, "Billing response");
15642 assert_eq!(
15643 agent.get_context().get("topic"),
15644 Some(&serde_json::json!("billing"))
15645 );
15646 }
15647
15648 #[tokio::test]
15649 async fn test_pre_response_extractor_miss_does_not_mutate_context() {
15650 let mock = mock_with_response("__NONE__");
15651 let yaml = r#"
15652name: OptimizedStateAgent
15653system_prompt: "You route before answering."
15654runtime:
15655 optimization:
15656 enabled: true
15657 pre_response_deterministic_transitions: true
15658states:
15659 initial: greeting
15660 states:
15661 greeting:
15662 prompt: "Old state prompt."
15663 extract:
15664 - key: topic
15665 description: "Support topic"
15666 transitions:
15667 - to: billing
15668 guard:
15669 context:
15670 topic:
15671 eq: billing
15672 timing: pre_response
15673 run_extractors: true
15674 billing:
15675 prompt: "Billing state."
15676"#;
15677 let agent = AgentBuilder::from_yaml(yaml)
15678 .unwrap()
15679 .llm(Arc::new(mock))
15680 .build()
15681 .unwrap();
15682
15683 let response = agent.try_pre_response_transition("hello").await.unwrap();
15684
15685 assert!(response.is_none());
15686 assert_eq!(agent.current_state().as_deref(), Some("greeting"));
15687 assert!(!agent.get_context().contains_key("topic"));
15688 }
15689
15690 #[tokio::test]
15691 async fn test_default_guard_transition_stays_post_response() {
15692 let mock = mock_with_responses(vec!["Greeting response", "Billing response"]);
15693 let call_counter = mock.clone();
15694 let yaml = r#"
15695name: TimingAgent
15696system_prompt: "You route carefully."
15697runtime:
15698 optimization:
15699 enabled: true
15700 pre_response_deterministic_transitions: true
15701states:
15702 initial: greeting
15703 states:
15704 greeting:
15705 prompt: "Old state prompt."
15706 transitions:
15707 - to: billing
15708 guard:
15709 context:
15710 topic:
15711 eq: billing
15712 billing:
15713 prompt: "Billing state."
15714"#;
15715 let agent = AgentBuilder::from_yaml(yaml)
15716 .unwrap()
15717 .llm(Arc::new(mock))
15718 .build()
15719 .unwrap();
15720 agent
15721 .set_context("topic", serde_json::json!("billing"))
15722 .unwrap();
15723
15724 let response = agent.chat("billing please").await.unwrap();
15725
15726 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15727 assert_eq!(response.content, "Billing response");
15728 assert_eq!(call_counter.call_count(), 2);
15729 }
15730
15731 #[tokio::test]
15732 async fn test_explicit_post_response_guard_transition_stays_post_response() {
15733 let mock = mock_with_responses(vec!["Greeting response", "Billing response"]);
15734 let call_counter = mock.clone();
15735 let yaml = r#"
15736name: TimingAgent
15737system_prompt: "You route carefully."
15738runtime:
15739 optimization:
15740 enabled: true
15741 pre_response_deterministic_transitions: true
15742states:
15743 initial: greeting
15744 states:
15745 greeting:
15746 prompt: "Old state prompt."
15747 transitions:
15748 - to: billing
15749 guard:
15750 context:
15751 topic:
15752 eq: billing
15753 timing: post_response
15754 billing:
15755 prompt: "Billing state."
15756"#;
15757 let agent = AgentBuilder::from_yaml(yaml)
15758 .unwrap()
15759 .llm(Arc::new(mock))
15760 .build()
15761 .unwrap();
15762 agent
15763 .set_context("topic", serde_json::json!("billing"))
15764 .unwrap();
15765
15766 let response = agent.chat("billing please").await.unwrap();
15767
15768 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15769 assert_eq!(response.content, "Billing response");
15770 assert_eq!(call_counter.call_count(), 2);
15771 }
15772
15773 #[tokio::test]
15774 async fn test_pre_response_extractors_are_transition_scoped() {
15775 let mock = mock_with_responses(vec!["billing", "Billing response"]);
15776 let yaml = r#"
15777name: ScopedExtractorAgent
15778system_prompt: "You route carefully."
15779runtime:
15780 optimization:
15781 enabled: true
15782 pre_response_deterministic_transitions: true
15783states:
15784 initial: greeting
15785 states:
15786 greeting:
15787 prompt: "Old state prompt."
15788 extract:
15789 - key: topic
15790 description: "Support topic"
15791 transitions:
15792 - to: wrong
15793 guard:
15794 context:
15795 topic:
15796 eq: billing
15797 timing: pre_response
15798 - to: billing
15799 guard:
15800 context:
15801 topic:
15802 eq: billing
15803 timing: pre_response
15804 run_extractors: true
15805 wrong:
15806 prompt: "Wrong state."
15807 billing:
15808 prompt: "Billing state."
15809"#;
15810 let agent = AgentBuilder::from_yaml(yaml)
15811 .unwrap()
15812 .llm(Arc::new(mock))
15813 .build()
15814 .unwrap();
15815
15816 let response = agent.chat("billing please").await.unwrap();
15817
15818 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15819 assert_eq!(response.content, "Billing response");
15820 }
15821
15822 #[tokio::test]
15823 async fn test_pre_response_resolved_intent_routes_early() {
15824 let mock = mock_with_response("Billing response");
15825 let yaml = r#"
15826name: IntentAgent
15827system_prompt: "You route carefully."
15828runtime:
15829 optimization:
15830 enabled: true
15831 pre_response_deterministic_transitions: true
15832states:
15833 initial: greeting
15834 states:
15835 greeting:
15836 prompt: "Old state prompt."
15837 transitions:
15838 - to: billing
15839 intent: billing
15840 timing: pre_response
15841 billing:
15842 prompt: "Billing state."
15843"#;
15844 let agent = AgentBuilder::from_yaml(yaml)
15845 .unwrap()
15846 .llm(Arc::new(mock))
15847 .build()
15848 .unwrap();
15849 agent
15850 .set_context("resolved_intent", serde_json::json!("billing"))
15851 .unwrap();
15852
15853 let response = agent
15854 .try_pre_response_transition("I need billing help")
15855 .await
15856 .unwrap()
15857 .unwrap();
15858
15859 assert_eq!(agent.current_state().as_deref(), Some("billing"));
15860 assert_eq!(response.content, "Billing response");
15861 }
15862
15863 #[tokio::test]
15864 async fn test_background_overflow_error_surfaces() {
15865 let mut config = RuntimeConfig::default();
15866 config.optimization.enabled = true;
15867 config.optimization.post_turn.max_background_tasks = 1;
15868 config.optimization.post_turn.on_background_overflow = BackgroundOverflowPolicy::Error;
15869 let policy = crate::optimization::MaintenanceTaskPolicy {
15870 mode: MaintenanceMode::Background,
15871 await_before_next_turn: AwaitBeforeNextTurn::Always,
15872 };
15873 let agent = AgentBuilder::new()
15874 .system_prompt("You are helpful.")
15875 .llm(Arc::new(mock_with_response("ok")))
15876 .build()
15877 .unwrap()
15878 .with_runtime_config(config);
15879 agent
15880 .background_maintenance
15881 .spawn(None, async { std::future::pending::<Result<()>>().await })
15882 .unwrap();
15883
15884 let result = agent
15885 .spawn_or_handle_background(None, async { Ok(()) }, "facts", &policy)
15886 .await;
15887
15888 assert!(result.is_err());
15889 }
15890
15891 #[tokio::test]
15892 async fn test_speculative_reasoning_low_cap_uses_serial_reasoning() {
15893 let default_mock = mock_with_response("Plain draft response");
15894 let router_mock = mock_with_response("cot");
15895 let router_counter = router_mock.clone();
15896 let yaml = r#"
15897name: ReasoningReservationAgent
15898system_prompt: "You answer plainly unless reasoning wins."
15899llm:
15900 default: default
15901 router: router
15902observability:
15903 enabled: true
15904 export:
15905 write_raw_events: true
15906reasoning:
15907 mode: auto
15908 judge_llm: router
15909runtime:
15910 optimization:
15911 enabled: true
15912 max_speculative_llm_calls_per_turn: 1
15913 speculative_reasoning_auto: true
15914 max_parallel_runtime_tasks: 2
15915"#;
15916 let agent = AgentBuilder::from_yaml(yaml)
15917 .unwrap()
15918 .llm_alias("default", Arc::new(default_mock))
15919 .llm_alias("router", Arc::new(router_mock))
15920 .build()
15921 .unwrap();
15922
15923 let response = agent.chat("hello").await.unwrap();
15924
15925 assert_eq!(response.content, "Plain draft response");
15926 assert_eq!(router_counter.call_count(), 1);
15927 let events = agent.observability().unwrap().raw_events();
15928 assert!(!events.iter().any(|event| {
15929 event.dimensions.get("commit_behavior") == Some(&"reasoning_decision".to_string())
15930 }));
15931 }
15932
15933 #[tokio::test]
15934 async fn test_forced_reasoning_skips_plain_speculative_draft() {
15935 let mock = mock_with_response("Reasoned response");
15936 let yaml = r#"
15937name: ForcedReasoningAgent
15938system_prompt: "You reason before answering."
15939observability:
15940 enabled: true
15941 export:
15942 write_raw_events: true
15943reasoning:
15944 mode: cot
15945runtime:
15946 optimization:
15947 enabled: true
15948 max_speculative_llm_calls_per_turn: 2
15949 speculative_state_transitions: true
15950 max_parallel_runtime_tasks: 2
15951states:
15952 initial: triage
15953 states:
15954 triage:
15955 prompt: "Answer from triage."
15956 transitions:
15957 - to: billing
15958 guard:
15959 context:
15960 route:
15961 eq: billing
15962 timing: parallel
15963 billing:
15964 prompt: "Billing state."
15965"#;
15966 let agent = AgentBuilder::from_yaml(yaml)
15967 .unwrap()
15968 .llm(Arc::new(mock))
15969 .build()
15970 .unwrap();
15971
15972 let response = agent.chat("hello").await.unwrap();
15973
15974 assert_eq!(response.content, "Reasoned response");
15975 let events = agent.observability().unwrap().raw_events();
15976 assert!(
15977 !events
15978 .iter()
15979 .any(|event| event.dimensions.contains_key("branch_status"))
15980 );
15981 }
15982
15983 #[tokio::test]
15984 async fn test_speculative_skill_low_cap_uses_serial_skill_route() {
15985 let default_mock = mock_with_response("Skill committed response");
15986 let router_mock = mock_with_response("helper");
15987 let router_counter = router_mock.clone();
15988 let yaml = r#"
15989name: SkillReservationAgent
15990system_prompt: "Use skills when they match."
15991llm:
15992 default: default
15993 router: router
15994observability:
15995 enabled: true
15996 export:
15997 write_raw_events: true
15998runtime:
15999 optimization:
16000 enabled: true
16001 max_speculative_llm_calls_per_turn: 1
16002 speculative_skill_routing: true
16003 max_parallel_runtime_tasks: 2
16004skills:
16005 - id: helper
16006 description: "Answer helper requests"
16007 trigger: "User asks for helper"
16008 steps:
16009 - prompt: "Answer the helper request: {{ user_input }}"
16010"#;
16011 let agent = AgentBuilder::from_yaml(yaml)
16012 .unwrap()
16013 .llm_alias("default", Arc::new(default_mock))
16014 .llm_alias("router", Arc::new(router_mock))
16015 .build()
16016 .unwrap();
16017
16018 let response = agent.chat("please use helper").await.unwrap();
16019
16020 assert_eq!(response.content, "Skill committed response");
16021 assert_eq!(router_counter.call_count(), 1);
16022 let events = agent.observability().unwrap().raw_events();
16023 assert!(
16024 !events
16025 .iter()
16026 .any(|event| event.dimensions.contains_key("branch_status"))
16027 );
16028 }
16029
16030 #[tokio::test]
16031 async fn test_parallel_transition_low_cap_allows_deterministic_route() {
16032 let mock = mock_with_response("unused");
16033 let call_counter = mock.clone();
16034 let yaml = r#"
16035name: ParallelTransitionLowCapAgent
16036system_prompt: "Route before stale responses when safe."
16037runtime:
16038 optimization:
16039 enabled: true
16040 max_speculative_llm_calls_per_turn: 1
16041 speculative_state_transitions: true
16042 max_parallel_runtime_tasks: 2
16043states:
16044 initial: triage
16045 states:
16046 triage:
16047 prompt: "Triage state."
16048 transitions:
16049 - to: billing
16050 guard:
16051 context:
16052 route:
16053 eq: billing
16054 timing: parallel
16055 billing:
16056 prompt: "Billing state."
16057"#;
16058 let agent = AgentBuilder::from_yaml(yaml)
16059 .unwrap()
16060 .llm(Arc::new(mock))
16061 .build()
16062 .unwrap();
16063 agent
16064 .set_context("route", serde_json::json!("billing"))
16065 .unwrap();
16066 agent.update_active_turn_context("billing help", HashMap::new());
16067 assert!(
16068 agent.reserve_active_speculative_llm_call(
16069 RuntimeOptimizationKind::ParallelStateTransition
16070 )
16071 );
16072
16073 let selection = agent
16074 .select_parallel_transition_candidate("billing help")
16075 .await
16076 .unwrap();
16077 agent.end_root_turn();
16078
16079 match selection {
16080 ParallelTransitionSelection::Candidate(candidate) => {
16081 assert_eq!(candidate.target(), "billing");
16082 }
16083 ParallelTransitionSelection::NoMatch => panic!("deterministic route did not match"),
16084 ParallelTransitionSelection::ReservationExhausted => {
16085 panic!("deterministic route consumed LLM budget")
16086 }
16087 }
16088 assert_eq!(call_counter.call_count(), 0);
16089 }
16090
16091 #[tokio::test]
16092 async fn speculative_transition_drops_loser_before_state_actions() {
16093 let lock = Arc::new(tokio::sync::Mutex::new(()));
16094 let first_started = Arc::new(tokio::sync::Notify::new());
16095 let first_dropped = Arc::new(AtomicBool::new(false));
16096 let committed_after_drop = Arc::new(AtomicBool::new(false));
16097 let default = Arc::new(FirstCallLockingProvider {
16098 lock,
16099 first_started: Arc::clone(&first_started),
16100 first_dropped: Arc::clone(&first_dropped),
16101 committed_after_drop: Arc::clone(&committed_after_drop),
16102 calls: AtomicU64::new(0),
16103 });
16104 let router = Arc::new(RoutingAfterProviderStart {
16105 provider_started: first_started,
16106 });
16107 let yaml = r#"
16108name: SpeculativeCancellationAgent
16109system_prompt: "Route before committed work."
16110llm:
16111 default: default
16112 router: router
16113runtime:
16114 optimization:
16115 enabled: true
16116 max_speculative_llm_calls_per_turn: 2
16117 speculative_state_transitions: true
16118 max_parallel_runtime_tasks: 2
16119states:
16120 initial: triage
16121 states:
16122 triage:
16123 prompt: "Triage state."
16124 transitions:
16125 - to: technical
16126 when: "The request needs technical support"
16127 timing: parallel
16128 technical:
16129 prompt: "Technical state."
16130 on_enter:
16131 - prompt: "Prepare technical context."
16132 llm: default
16133 store_as: preparation
16134"#;
16135 let agent = AgentBuilder::from_yaml(yaml)
16136 .unwrap()
16137 .llm_alias("default", default)
16138 .llm_alias("router", router)
16139 .build()
16140 .unwrap();
16141
16142 let response = tokio::time::timeout(
16143 std::time::Duration::from_secs(2),
16144 agent.chat("I cannot log in because of AUTH-17."),
16145 )
16146 .await
16147 .expect("committed work must not wait on the losing provider future")
16148 .unwrap();
16149
16150 assert_eq!(response.content, "Committed technical response.");
16151 assert_eq!(agent.current_state().as_deref(), Some("technical"));
16152 assert!(first_dropped.load(Ordering::SeqCst));
16153 assert!(committed_after_drop.load(Ordering::SeqCst));
16154 }
16155
16156 #[tokio::test]
16157 async fn buffered_transition_drops_stale_stream_before_redispatch() {
16158 use futures::StreamExt;
16159
16160 let lock = Arc::new(tokio::sync::Mutex::new(()));
16161 let stream_started = Arc::new(tokio::sync::Notify::new());
16162 let stream_dropped = Arc::new(AtomicBool::new(false));
16163 let committed_after_drop = Arc::new(AtomicBool::new(false));
16164 let default = Arc::new(BufferedLockingProvider {
16165 lock,
16166 stream_started: Arc::clone(&stream_started),
16167 stream_dropped: Arc::clone(&stream_dropped),
16168 committed_after_drop: Arc::clone(&committed_after_drop),
16169 });
16170 let router = Arc::new(RoutingAfterProviderStart {
16171 provider_started: stream_started,
16172 });
16173 let yaml = r#"
16174name: BufferedCancellationAgent
16175system_prompt: "Hide stale streamed output."
16176llm:
16177 default: default
16178 router: router
16179streaming:
16180 enabled: true
16181 buffer_size: 8
16182runtime:
16183 optimization:
16184 enabled: true
16185 max_speculative_llm_calls_per_turn: 2
16186 speculative_state_transitions: true
16187 streaming_policy: buffer_until_routing_done
16188 max_parallel_runtime_tasks: 2
16189states:
16190 initial: triage
16191 states:
16192 triage:
16193 prompt: "Triage state."
16194 transitions:
16195 - to: technical
16196 when: "The request needs technical support"
16197 timing: parallel
16198 technical:
16199 prompt: "Technical state."
16200"#;
16201 let agent = AgentBuilder::from_yaml(yaml)
16202 .unwrap()
16203 .llm_alias("default", default)
16204 .llm_alias("router", router)
16205 .build()
16206 .unwrap();
16207
16208 let content = tokio::time::timeout(std::time::Duration::from_secs(2), async {
16209 let mut stream = agent
16210 .chat_stream("AUTH-17 needs technical help.")
16211 .await
16212 .unwrap();
16213 let mut content = String::new();
16214 while let Some(chunk) = stream.next().await {
16215 match chunk {
16216 StreamChunk::Content { text } => content.push_str(&text),
16217 StreamChunk::Done {} => break,
16218 StreamChunk::Error { message } => panic!("unexpected stream error: {message}"),
16219 _ => {}
16220 }
16221 }
16222 content
16223 })
16224 .await
16225 .expect("redispatch must not wait on the stale streaming future");
16226
16227 assert_eq!(content, "Committed technical response.");
16228 assert_eq!(agent.current_state().as_deref(), Some("technical"));
16229 assert!(stream_dropped.load(Ordering::SeqCst));
16230 assert!(committed_after_drop.load(Ordering::SeqCst));
16231 }
16232
16233 #[tokio::test]
16234 async fn buffered_transition_drops_established_stream_before_redispatch() {
16235 use futures::StreamExt;
16236
16237 let stream_started = Arc::new(tokio::sync::Notify::new());
16238 let stream_dropped = Arc::new(AtomicBool::new(false));
16239 let stream_dropped_notify = Arc::new(tokio::sync::Notify::new());
16240 let committed_after_drop = Arc::new(AtomicBool::new(false));
16241 let default = Arc::new(EstablishedStreamProvider {
16242 stream_started: Arc::clone(&stream_started),
16243 stream_dropped: Arc::clone(&stream_dropped),
16244 stream_dropped_notify,
16245 committed_after_drop: Arc::clone(&committed_after_drop),
16246 });
16247 let router = Arc::new(RoutingAfterProviderStart {
16248 provider_started: stream_started,
16249 });
16250 let yaml = r#"
16251name: EstablishedStreamCancellationAgent
16252system_prompt: "Hide stale streamed output."
16253llm:
16254 default: default
16255 router: router
16256streaming:
16257 enabled: true
16258 buffer_size: 8
16259runtime:
16260 optimization:
16261 enabled: true
16262 max_speculative_llm_calls_per_turn: 2
16263 speculative_state_transitions: true
16264 streaming_policy: buffer_until_routing_done
16265 max_parallel_runtime_tasks: 2
16266states:
16267 initial: triage
16268 states:
16269 triage:
16270 prompt: "Triage state."
16271 transitions:
16272 - to: technical
16273 when: "The request needs technical support"
16274 timing: parallel
16275 technical:
16276 prompt: "Technical state."
16277"#;
16278 let agent = AgentBuilder::from_yaml(yaml)
16279 .unwrap()
16280 .llm_alias("default", default)
16281 .llm_alias("router", router)
16282 .build()
16283 .unwrap();
16284
16285 let content = tokio::time::timeout(std::time::Duration::from_secs(2), async {
16286 let mut stream = agent
16287 .chat_stream("AUTH-17 needs technical help.")
16288 .await
16289 .unwrap();
16290 let mut content = String::new();
16291 while let Some(chunk) = stream.next().await {
16292 match chunk {
16293 StreamChunk::Content { text } => content.push_str(&text),
16294 StreamChunk::Done {} => break,
16295 StreamChunk::Error { message } => panic!("unexpected stream error: {message}"),
16296 _ => {}
16297 }
16298 }
16299 content
16300 })
16301 .await
16302 .expect("redispatch must wait for the established stale stream to be dropped");
16303
16304 assert_eq!(content, "Committed technical response.");
16305 assert_eq!(agent.current_state().as_deref(), Some("technical"));
16306 assert!(stream_dropped.load(Ordering::SeqCst));
16307 assert!(committed_after_drop.load(Ordering::SeqCst));
16308 }
16309
16310 #[tokio::test]
16311 async fn test_buffered_streaming_transition_reservation_falls_back() {
16312 use futures::StreamExt;
16313
16314 let mock = mock_with_responses(vec![
16315 "Serial streaming response",
16316 "Serial streaming response",
16317 ]);
16318 let router_mock = mock_with_response("1");
16319 let router_counter = router_mock.clone();
16320 let yaml = r#"
16321name: BufferedReservationFallbackAgent
16322system_prompt: "Stream normally if speculative routing cannot be evaluated."
16323llm:
16324 default: default
16325 router: router
16326observability:
16327 enabled: true
16328 export:
16329 write_raw_events: true
16330streaming:
16331 enabled: true
16332 buffer_size: 8
16333runtime:
16334 optimization:
16335 enabled: true
16336 max_speculative_llm_calls_per_turn: 1
16337 speculative_state_transitions: true
16338 streaming_policy: buffer_until_routing_done
16339 max_parallel_runtime_tasks: 2
16340states:
16341 initial: triage
16342 states:
16343 triage:
16344 prompt: "Triage state."
16345 transitions:
16346 - to: billing
16347 guard:
16348 context:
16349 route:
16350 eq: billing
16351 when: "User asks about billing"
16352 timing: parallel
16353 billing:
16354 prompt: "Billing state."
16355"#;
16356 let agent = AgentBuilder::from_yaml(yaml)
16357 .unwrap()
16358 .llm_alias("default", Arc::new(mock))
16359 .llm_alias("router", Arc::new(router_mock))
16360 .build()
16361 .unwrap();
16362
16363 let mut stream = agent.chat_stream("hello").await.unwrap();
16364 let mut content = String::new();
16365 let mut error = None;
16366 while let Some(chunk) = stream.next().await {
16367 match chunk {
16368 StreamChunk::Content { text } => content.push_str(&text),
16369 StreamChunk::Error { message } => error = Some(message),
16370 StreamChunk::Done {} => break,
16371 _ => {}
16372 }
16373 }
16374
16375 assert_eq!(error, None);
16376 assert_eq!(content, "Serial streaming response");
16377 assert_eq!(router_counter.call_count(), 0);
16378 let events = agent.observability().unwrap().raw_events();
16379 assert!(events.iter().any(|event| {
16380 event.dimensions.get("branch_status") == Some(&"cancelled".to_string())
16381 && event.dimensions.get("commit_behavior")
16382 == Some(&"transition_decision".to_string())
16383 }));
16384 }
16385
16386 #[tokio::test]
16387 async fn test_blocking_error_cleanup_resets_root_turn_for_next_chat() {
16388 let mut mock = mock_with_response("Recovered response");
16389 mock.set_error("boom");
16390 let mut handle = mock.clone();
16391 let agent = AgentBuilder::new()
16392 .system_prompt("You are helpful.")
16393 .llm(Arc::new(mock))
16394 .build()
16395 .unwrap();
16396
16397 assert!(agent.chat("first").await.is_err());
16398 handle.clear_error();
16399 let response = agent.chat("second").await.unwrap();
16400
16401 assert_eq!(response.content, "Recovered response");
16402 let messages = agent.memory.get_messages(None).await.unwrap();
16403 let user_count = messages
16404 .iter()
16405 .filter(|message| message.role == ai_agents_core::Role::User)
16406 .count();
16407 assert_eq!(user_count, 2);
16408 }
16409
16410 #[tokio::test]
16411 async fn test_streaming_error_cleanup_resets_root_turn_for_next_chat() {
16412 use futures::StreamExt;
16413
16414 let mut mock = mock_with_response("Recovered response");
16415 mock.set_error("stream boom");
16416 let mut handle = mock.clone();
16417 let agent = AgentBuilder::new()
16418 .system_prompt("You are helpful.")
16419 .llm(Arc::new(mock))
16420 .build()
16421 .unwrap();
16422
16423 let mut stream = agent.chat_stream("first").await.unwrap();
16424 let mut saw_error = false;
16425 while let Some(chunk) = stream.next().await {
16426 if matches!(chunk, StreamChunk::Error { .. }) {
16427 saw_error = true;
16428 }
16429 }
16430 assert!(saw_error);
16431
16432 handle.clear_error();
16433 let response = agent.chat("second").await.unwrap();
16434
16435 assert_eq!(response.content, "Recovered response");
16436 let messages = agent.memory.get_messages(None).await.unwrap();
16437 let user_count = messages
16438 .iter()
16439 .filter(|message| message.role == ai_agents_core::Role::User)
16440 .count();
16441 assert_eq!(user_count, 2);
16442 }
16443
16444 #[tokio::test]
16445 async fn test_buffered_streaming_route_miss_releases_buffer_limit() {
16446 use futures::StreamExt;
16447
16448 let mut mock = mock_with_response("one two three");
16449 mock.set_latency(10);
16450 let yaml = r#"
16451name: BufferedMissAgent
16452system_prompt: "You stream safely."
16453llm:
16454 default: default
16455streaming:
16456 enabled: true
16457 buffer_size: 1
16458runtime:
16459 optimization:
16460 enabled: true
16461 max_speculative_llm_calls_per_turn: 2
16462 speculative_state_transitions: true
16463 streaming_policy: buffer_until_routing_done
16464 max_parallel_runtime_tasks: 2
16465states:
16466 initial: triage
16467 states:
16468 triage:
16469 prompt: "Answer from triage."
16470 transitions:
16471 - to: billing
16472 guard:
16473 context:
16474 route:
16475 eq: billing
16476 timing: parallel
16477 billing:
16478 prompt: "Billing state."
16479"#;
16480 let agent = AgentBuilder::from_yaml(yaml)
16481 .unwrap()
16482 .llm_alias("default", Arc::new(mock))
16483 .build()
16484 .unwrap();
16485
16486 let mut stream = agent.chat_stream("hello").await.unwrap();
16487 let mut content = String::new();
16488 let mut error = None;
16489 while let Some(chunk) = stream.next().await {
16490 match chunk {
16491 StreamChunk::Content { text } => content.push_str(&text),
16492 StreamChunk::Error { message } => error = Some(message),
16493 StreamChunk::Done {} => break,
16494 _ => {}
16495 }
16496 }
16497
16498 assert_eq!(error, None);
16499 assert_eq!(content, "one two three");
16500 }
16501
16502 #[tokio::test]
16503 async fn test_buffered_streaming_main_failure_finalizes_branch() {
16504 use futures::StreamExt;
16505
16506 let mock = mock_with_response("one two");
16507 let mut router_mock = mock_with_response("0");
16508 router_mock.set_latency(50);
16509 let yaml = r#"
16510name: BufferedFailureAgent
16511system_prompt: "You stream safely."
16512llm:
16513 default: default
16514 router: router
16515observability:
16516 enabled: true
16517 export:
16518 write_raw_events: true
16519streaming:
16520 enabled: true
16521 buffer_size: 1
16522runtime:
16523 optimization:
16524 enabled: true
16525 max_speculative_llm_calls_per_turn: 2
16526 speculative_state_transitions: true
16527 streaming_policy: buffer_until_routing_done
16528 max_parallel_runtime_tasks: 2
16529states:
16530 initial: triage
16531 states:
16532 triage:
16533 prompt: "Ask for the category."
16534 transitions:
16535 - to: billing
16536 when: "User asks about billing"
16537 timing: parallel
16538 billing:
16539 prompt: "Billing state."
16540"#;
16541 let agent = AgentBuilder::from_yaml(yaml)
16542 .unwrap()
16543 .llm_alias("default", Arc::new(mock))
16544 .llm_alias("router", Arc::new(router_mock))
16545 .build()
16546 .unwrap();
16547
16548 let mut stream = agent.chat_stream("hello").await.unwrap();
16549 let mut error = String::new();
16550 while let Some(chunk) = stream.next().await {
16551 if let StreamChunk::Error { message } = chunk {
16552 error = message;
16553 }
16554 }
16555
16556 assert!(
16557 error.contains("stream buffer filled"),
16558 "unexpected stream error: {}",
16559 error
16560 );
16561 let events = agent.observability().unwrap().raw_events();
16562 assert!(events.iter().any(|event| {
16563 event.dimensions.get("branch_status") == Some(&"failed".to_string())
16564 && event.dimensions.get("commit_behavior") == Some(&"final_response".to_string())
16565 && event.dimensions.get("optimization")
16566 == Some(&"buffered_streaming_routing".to_string())
16567 }));
16568 }
16569
16570 #[tokio::test]
16571 async fn test_streaming_preflight_does_not_emit_old_state_content() {
16572 use futures::StreamExt;
16573
16574 let mock = mock_with_response("Billing streamed response");
16575 let yaml = r#"
16576name: StreamingOptimizedAgent
16577system_prompt: "You route before streaming."
16578runtime:
16579 optimization:
16580 enabled: true
16581 pre_response_deterministic_transitions: true
16582streaming:
16583 enabled: true
16584states:
16585 initial: greeting
16586 states:
16587 greeting:
16588 prompt: "OLD_STATE_SENTINEL"
16589 transitions:
16590 - to: billing
16591 guard:
16592 context:
16593 topic:
16594 eq: billing
16595 timing: pre_response
16596 billing:
16597 prompt: "Billing state."
16598"#;
16599 let agent = AgentBuilder::from_yaml(yaml)
16600 .unwrap()
16601 .llm(Arc::new(mock))
16602 .build()
16603 .unwrap();
16604 agent
16605 .set_context("topic", serde_json::json!("billing"))
16606 .unwrap();
16607
16608 let mut stream = agent.chat_stream("billing please").await.unwrap();
16609 let mut content = String::new();
16610 while let Some(chunk) = stream.next().await {
16611 match chunk {
16612 StreamChunk::Content { text } => content.push_str(&text),
16613 StreamChunk::Error { message } => panic!("stream error: {}", message),
16614 StreamChunk::Done {} => break,
16615 _ => {}
16616 }
16617 }
16618
16619 assert_eq!(agent.current_state().as_deref(), Some("billing"));
16620 assert!(content.contains("Billing streamed response"));
16621 assert!(!content.contains("OLD_STATE_SENTINEL"));
16622 }
16623
16624 #[tokio::test]
16626 async fn test_integration_state_machine_basic() {
16627 let yaml = r#"
16628name: StateAgent
16629system_prompt: "You are a support agent."
16630states:
16631 initial: greeting
16632 states:
16633 greeting:
16634 prompt: "Welcome the user warmly."
16635 transitions:
16636 - to: support
16637 when: "User needs help"
16638 auto: true
16639 support:
16640 prompt: "Help solve the user's problem."
16641"#;
16642 let mock = mock_with_responses(vec![
16643 "Welcome! How can I help?", "1", "I'll help you with that.", ]);
16647 let builder = AgentBuilder::from_yaml(yaml).unwrap();
16648 let agent = builder.llm(Arc::new(mock)).build().unwrap();
16649
16650 assert_eq!(agent.current_state(), Some("greeting".to_string()));
16651 let _ = agent.chat("I need help").await.unwrap();
16652 }
16655
16656 #[tokio::test]
16658 async fn test_integration_state_on_enter_set_context() {
16659 let yaml = r#"
16660name: ActionAgent
16661system_prompt: "You are helpful."
16662states:
16663 initial: step1
16664 states:
16665 step1:
16666 prompt: "Step 1"
16667 on_exit:
16668 - set_context:
16669 step1_exited: true
16670 transitions:
16671 - to: step2
16672 when: "always"
16673 auto: true
16674 step2:
16675 prompt: "Step 2"
16676 on_enter:
16677 - set_context:
16678 step2_entered: true
16679"#;
16680 let mock = mock_with_responses(vec![
16682 "Processing step 1.",
16683 "0", ]);
16685 let builder = AgentBuilder::from_yaml(yaml).unwrap();
16686 let agent = builder.llm(Arc::new(mock)).build().unwrap();
16687
16688 assert_eq!(agent.current_state(), Some("step1".to_string()));
16689
16690 agent.transition_to("step2").await.unwrap();
16692
16693 assert_eq!(agent.current_state(), Some("step2".to_string()));
16694
16695 let ctx = agent.get_context();
16697 assert_eq!(ctx.get("step1_exited"), Some(&serde_json::json!(true)));
16698 assert_eq!(ctx.get("step2_entered"), Some(&serde_json::json!(true)));
16699 }
16700
16701 #[tokio::test]
16702 async fn state_action_tool_preserves_source_in_stored_record() {
16703 let yaml = r#"
16704name: StateActionToolAgent
16705system_prompt: "You are helpful."
16706tools:
16707 - context_echo
16708states:
16709 initial: idle
16710 states:
16711 idle:
16712 prompt: "Idle"
16713 active:
16714 prompt: "Active"
16715 on_enter:
16716 - set_context:
16717 action_started: true
16718 - tool: context_echo
16719 args: {}
16720"#;
16721 let agent = AgentBuilder::from_yaml(yaml)
16722 .unwrap()
16723 .llm(Arc::new(mock_with_response("unused")))
16724 .tool(Arc::new(ContextEchoTool))
16725 .build()
16726 .unwrap();
16727
16728 agent.transition_to("active").await.unwrap();
16729
16730 let record: ToolExecutionRecord = serde_json::from_value(
16731 agent
16732 .get_context()
16733 .get("last_tool_record")
16734 .cloned()
16735 .expect("successful state action must store its execution record"),
16736 )
16737 .unwrap();
16738 assert!(record.executed);
16739 assert!(record.success);
16740 assert_eq!(record.canonical_id, "context_echo");
16741 assert!(matches!(
16742 &record.source,
16743 ToolCallSource::StateAction {
16744 state: Some(state),
16745 action_index: 1,
16746 } if state == "active"
16747 ));
16748 }
16749
16750 #[tokio::test]
16751 async fn test_ordinary_transition_uses_on_enter_then_on_reenter() {
16752 let yaml = r#"
16753name: OrdinaryLifecycleAgent
16754system_prompt: "You are helpful."
16755states:
16756 initial: intake
16757 regenerate_on_transition: false
16758 states:
16759 intake:
16760 prompt: "Intake"
16761 transitions:
16762 - to: drafting
16763 guard:
16764 context:
16765 route:
16766 eq: drafting
16767 drafting:
16768 prompt: "Drafting"
16769 on_enter:
16770 - set_context:
16771 draft_version: 1
16772 on_reenter:
16773 - set_context:
16774 draft_version: 2
16775 transitions:
16776 - to: review
16777 guard:
16778 context:
16779 route:
16780 eq: review
16781 review:
16782 prompt: "Review"
16783 on_enter:
16784 - set_context:
16785 review_entry: first
16786 transitions:
16787 - to: drafting
16788 guard:
16789 context:
16790 route:
16791 eq: drafting
16792"#;
16793 let agent = AgentBuilder::from_yaml(yaml)
16794 .unwrap()
16795 .llm(Arc::new(mock_with_responses(vec![
16796 "Intake response",
16797 "Draft response",
16798 "Review response",
16799 ])))
16800 .build()
16801 .unwrap();
16802
16803 agent
16804 .set_context("route", serde_json::json!("drafting"))
16805 .unwrap();
16806 agent.chat("Start a draft").await.unwrap();
16807 assert_eq!(agent.current_state().as_deref(), Some("drafting"));
16808 assert_eq!(
16809 agent.get_context().get("draft_version"),
16810 Some(&serde_json::json!(1))
16811 );
16812
16813 agent
16814 .set_context("route", serde_json::json!("review"))
16815 .unwrap();
16816 agent.chat("Review this").await.unwrap();
16817 assert_eq!(agent.current_state().as_deref(), Some("review"));
16818 assert_eq!(
16819 agent.get_context().get("review_entry"),
16820 Some(&serde_json::json!("first"))
16821 );
16822
16823 agent
16824 .set_context("route", serde_json::json!("drafting"))
16825 .unwrap();
16826 agent.chat("Revise this").await.unwrap();
16827 assert_eq!(agent.current_state().as_deref(), Some("drafting"));
16828 assert_eq!(
16829 agent.get_context().get("draft_version"),
16830 Some(&serde_json::json!(2))
16831 );
16832 }
16833
16834 #[tokio::test]
16835 async fn test_manual_transition_uses_on_enter_then_on_reenter() {
16836 let yaml = r#"
16837name: ManualLifecycleAgent
16838system_prompt: "You are helpful."
16839states:
16840 initial: intake
16841 states:
16842 intake:
16843 prompt: "Intake"
16844 drafting:
16845 prompt: "Drafting"
16846 on_enter:
16847 - set_context:
16848 draft_version: 1
16849 on_reenter:
16850 - set_context:
16851 draft_version: 2
16852 review:
16853 prompt: "Review"
16854"#;
16855 let agent = AgentBuilder::from_yaml(yaml)
16856 .unwrap()
16857 .llm(Arc::new(mock_with_response("unused")))
16858 .build()
16859 .unwrap();
16860
16861 assert!(!agent.get_context().contains_key("draft_version"));
16862 agent.transition_to("drafting").await.unwrap();
16863 assert_eq!(agent.current_state().as_deref(), Some("drafting"));
16864 assert_eq!(
16865 agent.get_context().get("draft_version"),
16866 Some(&serde_json::json!(1))
16867 );
16868
16869 agent.transition_to("review").await.unwrap();
16870 agent.transition_to("drafting").await.unwrap();
16871 assert_eq!(agent.current_state().as_deref(), Some("drafting"));
16872 assert_eq!(
16873 agent.get_context().get("draft_version"),
16874 Some(&serde_json::json!(2))
16875 );
16876 }
16877
16878 #[tokio::test]
16879 async fn test_timeout_transition_uses_on_enter_then_on_reenter() {
16880 let yaml = r#"
16881name: TimeoutLifecycleAgent
16882system_prompt: "You are helpful."
16883states:
16884 initial: intake
16885 regenerate_on_transition: false
16886 states:
16887 intake:
16888 prompt: "Intake"
16889 max_turns: 1
16890 timeout_to: drafting
16891 drafting:
16892 prompt: "Drafting"
16893 max_turns: 1
16894 timeout_to: review
16895 on_enter:
16896 - set_context:
16897 draft_version: 1
16898 on_reenter:
16899 - set_context:
16900 draft_version: 2
16901 review:
16902 prompt: "Review"
16903 max_turns: 1
16904 timeout_to: drafting
16905 on_enter:
16906 - set_context:
16907 review_entry: first
16908"#;
16909 let agent = AgentBuilder::from_yaml(yaml)
16910 .unwrap()
16911 .llm(Arc::new(mock_with_responses(vec![
16912 "Intake",
16913 "First draft",
16914 "Review",
16915 "Revised draft",
16916 ])))
16917 .build()
16918 .unwrap();
16919
16920 agent.chat("First turn").await.unwrap();
16921 assert_eq!(agent.current_state().as_deref(), Some("intake"));
16922 assert!(!agent.get_context().contains_key("draft_version"));
16923
16924 agent.chat("Second turn").await.unwrap();
16925 assert_eq!(agent.current_state().as_deref(), Some("drafting"));
16926 assert_eq!(
16927 agent.get_context().get("draft_version"),
16928 Some(&serde_json::json!(1))
16929 );
16930
16931 agent.chat("Third turn").await.unwrap();
16932 assert_eq!(agent.current_state().as_deref(), Some("review"));
16933 assert_eq!(
16934 agent.get_context().get("review_entry"),
16935 Some(&serde_json::json!("first"))
16936 );
16937
16938 agent.chat("Fourth turn").await.unwrap();
16939 assert_eq!(agent.current_state().as_deref(), Some("drafting"));
16940 assert_eq!(
16941 agent.get_context().get("draft_version"),
16942 Some(&serde_json::json!(2))
16943 );
16944 }
16945
16946 #[tokio::test]
16948 async fn test_integration_process_normalize() {
16949 let yaml = r#"
16950name: ProcessAgent
16951system_prompt: "You are helpful."
16952process:
16953 input:
16954 - type: normalize
16955 config:
16956 trim: true
16957 collapse_whitespace: true
16958"#;
16959 let mock = mock_with_response("Got your message.");
16960 let builder = AgentBuilder::from_yaml(yaml).unwrap();
16961 let agent = builder.llm(Arc::new(mock.clone())).build().unwrap();
16962
16963 let _ = agent.chat(" hello world ").await.unwrap();
16964
16965 let history = mock.call_history();
16967 assert!(!history.is_empty());
16968 let last_call = history.last().unwrap();
16970 let user_msg = last_call
16971 .messages
16972 .iter()
16973 .find(|m| m.role == ai_agents_core::Role::User)
16974 .unwrap();
16975 assert_eq!(user_msg.content, "hello world");
16976 }
16977
16978 #[tokio::test]
16982 async fn test_integration_memory_compression() {
16983 let yaml = r#"
16984name: MemoryAgent
16985system_prompt: "You are helpful."
16986memory:
16987 type: compacting
16988 max_messages: 100
16989 compress_threshold: 5
16990 max_recent_messages: 3
16991 summarize_batch_size: 2
16992"#;
16993 let responses: Vec<&str> = (0..8).map(|_| "Response from assistant.").collect();
16995 let mock = mock_with_responses(responses);
16996 let builder = AgentBuilder::from_yaml(yaml).unwrap();
16997 let agent = builder.llm(Arc::new(mock)).build().unwrap();
16998
16999 for i in 0..6 {
17001 let _ = agent.chat(&format!("Message {}", i)).await.unwrap();
17002 }
17003
17004 let messages = agent.memory.get_messages(None).await.unwrap();
17007 assert!(messages.len() <= 12); }
17011
17012 #[tokio::test]
17014 async fn test_integration_multi_llm_registry() {
17015 let mut mock_default = MockLLMProvider::new("default");
17016 mock_default.set_response("Default LLM response.");
17017 let mut mock_router = MockLLMProvider::new("router");
17018 mock_router.set_response("Router response.");
17019
17020 let agent = AgentBuilder::new()
17021 .system_prompt("You are helpful.")
17022 .llm_alias("default", Arc::new(mock_default))
17023 .llm_alias("router", Arc::new(mock_router))
17024 .build()
17025 .unwrap();
17026
17027 let response = agent.chat("Hello").await.unwrap();
17028 assert_eq!(response.content, "Default LLM response.");
17029 }
17030
17031 #[tokio::test]
17033 async fn test_integration_agent_reset() {
17034 let mock = mock_with_responses(vec!["Hello!", "Hello again!"]);
17035 let agent = AgentBuilder::new()
17036 .system_prompt("You are helpful.")
17037 .llm(Arc::new(mock))
17038 .build()
17039 .unwrap();
17040
17041 let _ = agent.chat("Hi").await.unwrap();
17042 let messages = agent.memory.get_messages(None).await.unwrap();
17043 assert_eq!(messages.len(), 2); agent.reset().await.unwrap();
17046 let messages = agent.memory.get_messages(None).await.unwrap();
17047 assert_eq!(messages.len(), 0);
17048 }
17049
17050 #[tokio::test]
17052 async fn test_integration_process_validate_reject() {
17053 use ai_agents_process::{ProcessConfig, ProcessProcessor};
17054
17055 let validate_config = ai_agents_process::ValidateStage {
17056 id: Some("length_check".to_string()),
17057 condition: None,
17058 config: ai_agents_process::ValidateConfig {
17059 rules: vec![ai_agents_process::ValidationRule::MinLength {
17060 min_length: 10,
17061 on_fail: ai_agents_process::ValidationAction {
17062 action: ai_agents_process::ValidationActionType::Reject,
17063 message: None,
17064 },
17065 }],
17066 ..Default::default()
17067 },
17068 };
17069 let process_config = ProcessConfig {
17070 input: vec![ai_agents_process::ProcessStage::Validate(validate_config)],
17071 ..Default::default()
17072 };
17073 let processor = ProcessProcessor::new(process_config);
17074
17075 let mock = mock_with_response("Should not reach here.");
17076 let agent = AgentBuilder::new()
17077 .system_prompt("You are helpful.")
17078 .llm(Arc::new(mock))
17079 .process_processor(processor)
17080 .build()
17081 .unwrap();
17082
17083 let response = agent.chat("Hi").await.unwrap();
17084 assert!(
17086 response.content.contains("rejected")
17087 || response.content.contains("Input rejected")
17088 || response.content.contains("too short")
17089 || response.content.contains("Too short")
17090 || response.content.len() < 50, "Expected rejection response, got: {}",
17092 response.content
17093 );
17094 }
17095
17096 #[tokio::test]
17098 async fn test_llm_fallback_on_failure() {
17099 use ai_agents_recovery::{ErrorRecoveryConfig, LLMFailureAction, LLMRecoveryConfig};
17100
17101 let mut primary = MockLLMProvider::new("primary");
17102 primary.set_error("Primary LLM is unavailable");
17103
17104 let mut fallback = MockLLMProvider::new("fallback");
17105 fallback.set_response("Fallback response works!");
17106
17107 let agent = AgentBuilder::new()
17108 .system_prompt("You are helpful.")
17109 .llm_alias("default", Arc::new(primary))
17110 .llm_alias("backup", Arc::new(fallback))
17111 .recovery_manager(RecoveryManager::new(ErrorRecoveryConfig {
17112 llm: LLMRecoveryConfig {
17113 on_failure: LLMFailureAction::FallbackLlm {
17114 fallback_llm: "backup".to_string(),
17115 },
17116 ..Default::default()
17117 },
17118 ..Default::default()
17119 }))
17120 .build()
17121 .unwrap();
17122
17123 let response = agent.chat("Hello").await.unwrap();
17124 assert!(
17125 response.content.contains("Fallback response"),
17126 "Expected fallback response, got: {}",
17127 response.content
17128 );
17129 }
17130
17131 #[tokio::test]
17133 async fn test_llm_fallback_response_static_message() {
17134 use ai_agents_recovery::{ErrorRecoveryConfig, LLMFailureAction, LLMRecoveryConfig};
17135
17136 let mut primary = MockLLMProvider::new("primary");
17137 primary.set_error("Primary LLM is unavailable");
17138
17139 let agent = AgentBuilder::new()
17140 .system_prompt("You are helpful.")
17141 .llm(Arc::new(primary))
17142 .recovery_manager(RecoveryManager::new(ErrorRecoveryConfig {
17143 llm: LLMRecoveryConfig {
17144 on_failure: LLMFailureAction::FallbackResponse {
17145 message: "I am temporarily unavailable. Please try again later."
17146 .to_string(),
17147 },
17148 ..Default::default()
17149 },
17150 ..Default::default()
17151 }))
17152 .build()
17153 .unwrap();
17154
17155 let response = agent.chat("Hello").await.unwrap();
17156 assert!(
17157 response.content.contains("temporarily unavailable"),
17158 "Expected static fallback message, got: {}",
17159 response.content
17160 );
17161 }
17162
17163 #[tokio::test]
17165 async fn test_tool_failure_skip() {
17166 use ai_agents_recovery::{
17167 ErrorRecoveryConfig, ToolFailureAction, ToolRecoveryConfig, ToolRetryConfig,
17168 };
17169
17170 let mock = mock_with_responses(vec![
17172 r#"I'll use the nonexistent tool.
17173[TOOL_CALL: {"name": "nonexistent_tool", "arguments": {}}]"#,
17174 "The tool was unavailable, but I can still help you.",
17175 ]);
17176
17177 let agent = AgentBuilder::new()
17178 .system_prompt("You are helpful.")
17179 .llm(Arc::new(mock))
17180 .recovery_manager(RecoveryManager::new(ErrorRecoveryConfig {
17181 tools: ToolRecoveryConfig {
17182 default: ToolRetryConfig {
17183 max_retries: 0,
17184 timeout_ms: None,
17185 on_failure: ToolFailureAction::Skip,
17186 },
17187 ..Default::default()
17188 },
17189 ..Default::default()
17190 }))
17191 .build()
17192 .unwrap();
17193
17194 let response = agent.chat("Use the nonexistent tool").await;
17196 assert!(
17197 response.is_ok(),
17198 "Expected Ok with skip policy, got: {:?}",
17199 response
17200 );
17201 }
17202}