use std::sync::Arc;
use crate::compact::ContextManager;
use crate::detection::DetectionManager;
use crate::detection::{ConvergenceAction, DetectedPattern};
use crate::fallback::FallbackManager;
#[cfg(feature = "hooks")]
use crate::hooks::HookExecutor;
use crate::middleware::ToolPipeline;
use crate::observer::{ConvergenceDetectedContext, LoopDetectedContext};
use crate::observer::{LoopObserver, ObserverHost};
#[cfg(feature = "streaming")]
use crate::stream::handler::StreamHandler;
#[cfg(feature = "tool_health")]
use crate::tool::health::ToolHealthRegistry;
pub use crate::capabilities::*;
pub struct LoopManagers {
fallback: FallbackManager,
detection: DetectionManager,
observer_host: ObserverHost,
tool_pipeline: Option<ToolPipeline>,
context_manager: Option<Arc<ContextManager>>,
#[cfg(feature = "streaming")]
stream_handler: Option<StreamHandler>,
#[cfg(feature = "hooks")]
hook_executor: Option<Arc<HookExecutor>>,
#[cfg(feature = "tool_health")]
health_registry: Option<Arc<ToolHealthRegistry>>,
memory: Option<Arc<dyn crate::memory::LoopMemory>>,
}
impl LoopManagers {
#[must_use]
pub fn new() -> Self {
Self {
fallback: FallbackManager::default(),
detection: DetectionManager::default(),
observer_host: ObserverHost::new(),
tool_pipeline: None,
context_manager: None,
#[cfg(feature = "streaming")]
stream_handler: None,
#[cfg(feature = "hooks")]
hook_executor: None,
#[cfg(feature = "tool_health")]
health_registry: None,
memory: None,
}
}
#[must_use]
pub fn with_fallback(mut self, fallback: FallbackManager) -> Self {
self.fallback = fallback;
self
}
#[must_use]
pub fn with_detection(mut self, detection: DetectionManager) -> Self {
self.detection = detection;
self
}
#[must_use]
pub fn with_observer(mut self, observer: Arc<dyn LoopObserver>) -> Self {
self.observer_host.register(observer);
self
}
pub fn observers(&self) -> &ObserverHost {
&self.observer_host
}
#[must_use]
pub fn with_pipeline(mut self, pipeline: ToolPipeline) -> Self {
self.tool_pipeline = Some(pipeline);
self
}
#[must_use]
#[cfg(feature = "tool_health")]
pub fn with_health_registry(mut self, registry: Arc<ToolHealthRegistry>) -> Self {
self.health_registry = Some(registry);
self
}
#[must_use]
#[cfg(feature = "streaming")]
pub fn with_stream_handler(mut self, handler: StreamHandler) -> Self {
self.stream_handler = Some(handler);
self
}
#[must_use]
#[cfg(feature = "hooks")]
pub fn with_hook_executor(mut self, executor: Arc<HookExecutor>) -> Self {
self.hook_executor = Some(executor);
self
}
pub fn register_observer(&mut self, observer: Arc<dyn LoopObserver>) {
self.observer_host.register(observer);
}
pub fn set_pipeline(&mut self, pipeline: ToolPipeline) {
self.tool_pipeline = Some(pipeline);
}
#[must_use]
pub fn with_context_manager(mut self, manager: Arc<ContextManager>) -> Self {
self.context_manager = Some(manager);
self
}
pub fn set_context_manager(&mut self, manager: Arc<ContextManager>) {
self.context_manager = Some(manager);
}
#[cfg(feature = "streaming")]
pub fn set_stream_handler(&mut self, handler: StreamHandler) {
self.stream_handler = Some(handler);
}
#[cfg(feature = "hooks")]
pub fn set_hook_executor(&mut self, executor: Arc<HookExecutor>) {
self.hook_executor = Some(executor);
}
#[cfg(feature = "tool_health")]
pub fn set_health_registry(&mut self, registry: Arc<ToolHealthRegistry>) {
self.health_registry = Some(registry);
}
#[must_use]
pub fn with_memory(mut self, memory: Arc<dyn crate::memory::LoopMemory>) -> Self {
self.memory = Some(memory);
self
}
pub fn set_memory(&mut self, memory: Arc<dyn crate::memory::LoopMemory>) {
self.memory = Some(memory);
}
#[must_use]
pub fn memory(&self) -> Option<&Arc<dyn crate::memory::LoopMemory>> {
self.memory.as_ref()
}
pub fn reset_all(&self) {
self.fallback.reset();
self.detection.reset();
self.observer_host.reset_all();
}
pub fn notify_detected_pattern(
&self,
pattern: &crate::detection::DetectedPattern,
turn: usize,
) {
match pattern {
DetectedPattern::NoPattern => {}
DetectedPattern::LoopDetected {
repetitions,
pattern_description,
} => {
tracing::warn!(
repetitions,
pattern = %pattern_description,
turn,
"loop detected"
);
self.observer_host.on_loop_detected(&LoopDetectedContext {
pattern: pattern_description.clone(),
repetitions: *repetitions,
});
}
DetectedPattern::ConvergenceDetected {
similarity,
consecutive_count,
} => {
tracing::warn!(similarity, consecutive_count, turn, "convergence detected");
let action = self.detection.config().on_converge;
let action_str = match action {
ConvergenceAction::Stop => "stop",
ConvergenceAction::Warn => "warn",
ConvergenceAction::Compact => "compact",
ConvergenceAction::AskUser => "ask_user",
ConvergenceAction::SwitchPhase => "switch_phase",
};
self.observer_host
.on_convergence_detected(&ConvergenceDetectedContext {
action: action_str.to_string(),
});
}
}
}
}
impl Default for LoopManagers {
fn default() -> Self {
Self::new()
}
}
impl crate::capabilities::Observable for LoopManagers {
fn observers(&self) -> &ObserverHost {
&self.observer_host
}
}
impl crate::capabilities::Detectable for LoopManagers {
fn detection(&self) -> &DetectionManager {
&self.detection
}
}
impl crate::capabilities::FallbackCapable for LoopManagers {
fn fallback(&self) -> &FallbackManager {
&self.fallback
}
}
impl crate::capabilities::Compactable for LoopManagers {
fn context_manager(&self) -> Option<&Arc<ContextManager>> {
self.context_manager.as_ref()
}
}
impl crate::capabilities::RememberCapable for LoopManagers {
fn memory(&self) -> Option<&Arc<dyn crate::memory::LoopMemory>> {
self.memory.as_ref()
}
}
#[cfg(feature = "streaming")]
impl crate::capabilities::StreamCapable for LoopManagers {
fn stream_handler(&self) -> &StreamHandler {
self.stream_handler
.as_ref()
.unwrap_or(StreamHandler::passthrough_default())
}
}
#[cfg(feature = "hooks")]
impl crate::capabilities::Hookable for LoopManagers {
fn hook_executor(&self) -> Option<&HookExecutor> {
self.hook_executor.as_deref()
}
}
impl crate::capabilities::PipelineAware for LoopManagers {
fn pipeline(&self) -> Option<&ToolPipeline> {
self.tool_pipeline.as_ref()
}
}
#[cfg(feature = "tool_health")]
impl crate::capabilities::HealthTrackable for LoopManagers {
fn health_registry(&self) -> Option<&ToolHealthRegistry> {
self.health_registry.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detection::{DetectedPattern, DetectionManager};
#[test]
fn test_runtime_default() {
let managers = LoopManagers::default();
assert!(managers.fallback().active_model().is_none());
}
#[test]
fn test_runtime_with_custom_fallback() {
let fallback = FallbackManager::for_model("my-model");
let managers = LoopManagers::new().with_fallback(fallback);
assert_eq!(
managers.fallback().active_model().as_deref(),
Some("my-model")
);
}
#[test]
fn test_reset_all() {
let managers = LoopManagers::new();
managers.reset_all();
}
#[test]
fn test_runtime_contains_detection_manager() {
let managers = LoopManagers::new();
assert_eq!(managers.detection().config().loop_threshold, 3);
assert_eq!(managers.detection().config().stop_threshold, 10);
}
#[test]
fn test_runtime_with_custom_detection() {
use crate::detection::DetectionConfig;
let config = DetectionConfig {
loop_threshold: 7,
stop_threshold: 20,
..Default::default()
};
let detection = DetectionManager::new_with_config(config).unwrap();
let managers = LoopManagers::new().with_detection(detection);
assert_eq!(managers.detection().config().loop_threshold, 7);
assert_eq!(managers.detection().config().stop_threshold, 20);
assert!(managers.fallback().active_model().is_none());
}
#[test]
fn test_reset_all_clears_detection() {
let managers = LoopManagers::new();
let _ = managers.detection().record_tool_call("Read", 12345);
managers.reset_all();
let pattern = managers.detection().record_tool_call("Read", 12345);
assert!(matches!(pattern, DetectedPattern::NoPattern));
}
#[test]
fn test_capability_traits_are_object_safe() {
fn _assert_observable(_: &dyn Observable) {}
fn _assert_detectable(_: &dyn Detectable) {}
fn _assert_fallback(_: &dyn FallbackCapable) {}
fn _assert_pipeline(_: &dyn PipelineAware) {}
fn _assert_compactable(_: &dyn Compactable) {}
#[cfg(feature = "streaming")]
fn _assert_stream_capable(_: &dyn StreamCapable) {}
let managers = LoopManagers::new();
_assert_observable(&managers);
_assert_detectable(&managers);
_assert_fallback(&managers);
_assert_pipeline(&managers);
_assert_compactable(&managers);
#[cfg(feature = "streaming")]
_assert_stream_capable(&managers);
}
#[test]
fn test_pipeline_defaults_to_none() {
let managers = LoopManagers::new();
assert!(managers.pipeline().is_none());
}
#[test]
fn test_observers_accessible_via_trait() {
let managers = LoopManagers::new();
let _: &ObserverHost = managers.observers();
}
#[test]
fn test_context_manager_defaults_to_none() {
let managers = LoopManagers::new();
assert!(managers.context_manager().is_none());
}
#[test]
#[cfg(feature = "streaming")]
fn test_stream_handler_defaults_to_passthrough() {
let managers = LoopManagers::new();
let handler = managers.stream_handler();
assert_eq!(
handler.timeout_config().total_stream_timeout,
std::time::Duration::MAX
);
}
#[test]
fn test_set_pipeline_returns_some() {
use crate::middleware::ToolPipeline;
use crate::tool::ToolRegistry;
use std::sync::Arc;
let registry = Arc::new(ToolRegistry::new());
let pipeline = ToolPipeline::new(registry);
let mut managers = LoopManagers::new();
assert!(managers.pipeline().is_none());
managers.set_pipeline(pipeline);
assert!(managers.pipeline().is_some());
}
#[test]
fn test_set_context_manager_returns_some() {
use crate::compact::{ContextManager, TruncatingCompactor};
let compactor = TruncatingCompactor::new();
let manager = ContextManager::new(Arc::new(compactor));
let mut managers = LoopManagers::new();
assert!(managers.context_manager().is_none());
managers.set_context_manager(Arc::new(manager));
assert!(managers.context_manager().is_some());
}
#[test]
#[cfg(feature = "streaming")]
fn test_set_stream_handler_overrides_passthrough() {
use crate::stream::handler::StreamHandler;
let handler = StreamHandler::new();
let mut managers = LoopManagers::new();
assert_eq!(
managers
.stream_handler()
.timeout_config()
.total_stream_timeout,
std::time::Duration::MAX
);
managers.set_stream_handler(handler);
assert_eq!(
managers
.stream_handler()
.timeout_config()
.total_stream_timeout,
std::time::Duration::from_mins(15)
);
}
#[test]
fn test_register_observer_increments_count() {
use crate::observer::LoopObserver;
use std::sync::Arc;
struct NopObserver;
impl LoopObserver for NopObserver {
fn name(&self) -> &'static str {
"NopObserver"
}
}
let mut managers = LoopManagers::new();
assert!(managers.observers().is_empty());
managers.register_observer(Arc::new(NopObserver));
assert_eq!(managers.observers().len(), 1);
managers.register_observer(Arc::new(NopObserver));
assert_eq!(managers.observers().len(), 2);
}
#[test]
fn test_reset_all_clears_fallback() {
let managers = LoopManagers::new();
let _ = managers
.fallback()
.record_failure(crate::fallback::FailureKind::Transient);
managers.reset_all();
assert!(managers.fallback().active_model().is_none());
}
#[test]
fn test_reset_all_clears_observers() {
use crate::observer::LoopObserver;
use std::sync::Arc;
struct NopObserver;
impl LoopObserver for NopObserver {
fn name(&self) -> &'static str {
"NopObserver"
}
}
let mut managers = LoopManagers::new();
managers.register_observer(Arc::new(NopObserver));
assert_eq!(managers.observers().len(), 1);
managers.reset_all();
assert_eq!(managers.observers().len(), 1);
}
#[test]
fn test_generic_bounds_accept_runtime() {
fn accepts_observable(_: &impl Observable) {}
fn accepts_detectable(_: &impl Detectable) {}
fn accepts_fallback(_: &impl FallbackCapable) {}
fn accepts_compactable(_: &impl Compactable) {}
#[cfg(feature = "streaming")]
fn accepts_stream_capable(_: &impl StreamCapable) {}
fn accepts_pipeline(_: &impl PipelineAware) {}
fn accepts_multi_bound(_: &(impl Observable + Detectable + FallbackCapable)) {}
let managers = LoopManagers::new();
accepts_observable(&managers);
accepts_detectable(&managers);
accepts_fallback(&managers);
accepts_compactable(&managers);
#[cfg(feature = "streaming")]
accepts_stream_capable(&managers);
accepts_pipeline(&managers);
accepts_multi_bound(&managers);
}
}