loopctl/capabilities.rs
1//! Capability traits for the agent loop runtime.
2//!
3//! Each trait represents a single infrastructure capability that the
4//! agent loop can depend on. [`LoopManagers`](crate::managers::LoopManagers)
5//! implements all of them, but consumers can narrow their bounds to
6//! only the capabilities they need.
7//!
8//! # Traits
9//!
10//! | Trait | Purpose |
11//! |---------------------|---------------------------------------------------------------|
12//! | [`Observable`] | Lifecycle event observation |
13//! | [`Detectable`] | Loop and convergence detection |
14//! | [`FallbackCapable`] | Model fallback / circuit breaker |
15//! | [`Compactable`] | Context compaction |
16//! | `StreamCapable` | Resilient LLM streaming *(requires `streaming` feature)* |
17//! | [`PipelineAware`] | Middleware pipeline dispatch |
18//! | `Hookable` | Bidirectional lifecycle hooks *(requires `hooks` feature)* |
19//! | `HealthTrackable` | Per-tool health monitoring *(requires `tool_health` feature)* |
20//!
21//! # When to use
22//!
23//! Use these traits as bounds when you need a specific capability
24//! without pulling in the full [`LoopManagers`](crate::managers::LoopManagers):
25//!
26//! ```rust,ignore
27//! fn check_patterns(runtime: &impl Detectable) {
28//! let pattern = runtime.detection().record_tool_call("Read", hash);
29//! }
30//! ```
31
32use std::sync::Arc;
33
34use crate::compact::ContextManager;
35use crate::detection::DetectionManager;
36use crate::fallback::FallbackManager;
37#[cfg(feature = "hooks")]
38use crate::hooks::HookExecutor;
39use crate::middleware::ToolPipeline;
40use crate::observer::ObserverHost;
41#[cfg(feature = "streaming")]
42use crate::stream::handler::StreamHandler;
43#[cfg(feature = "tool_health")]
44use crate::tool::health::ToolHealthRegistry;
45
46/// Capability to emit lifecycle events to registered observers.
47///
48/// Observers receive read-only notifications at well-defined hook points
49/// in the agent loop. They cannot influence control flow — for that, see
50/// the `Hookable` trait *(requires `hooks` feature)*.
51///
52/// # Implementors
53///
54/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
55///
56/// # When to use
57///
58/// Use this trait bound when you need to notify observers of lifecycle
59/// events but don't need any other infrastructure capabilities.
60///
61/// ```rust,ignore
62/// fn run_step(runtime: &impl Observable) {
63/// runtime.observers().on_turn_start(&ctx);
64/// // ... do work ...
65/// runtime.observers().on_turn_end(&ctx);
66/// }
67/// ```
68pub trait Observable {
69 /// Returns the observer host for lifecycle event fan-out.
70 ///
71 /// The observer host holds every registered observer and dispatches
72 /// all lifecycle events (turn start/end, stream deltas, tool dispatch,
73 /// compaction) to them.
74 fn observers(&self) -> &ObserverHost;
75}
76
77/// Capability to detect repetitive loops and semantic convergence.
78///
79/// Loop detection catches when the agent repeats the same tool operations
80/// in a cycle. Convergence detection catches when successive assistant
81/// responses become semantically similar. Both are handled by
82/// [`DetectionManager`].
83///
84/// # Implementors
85///
86/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
87///
88/// # When to use
89///
90/// Use this trait bound when you need to record operations or responses
91/// and check whether a loop or convergence pattern has been detected.
92///
93/// ```rust,ignore
94/// fn check_patterns(runtime: &impl Detectable) {
95/// let pattern = runtime.detection().record_tool_call("Read", hash);
96/// if let DetectedPattern::LoopDetected { .. } = pattern {
97/// // intervention needed
98/// }
99/// }
100/// ```
101pub trait Detectable {
102 /// Returns the detection manager for loop and convergence detection.
103 ///
104 /// Use this to record tool calls or model responses and check whether
105 /// the agent is repeating itself or converging on a stable answer.
106 fn detection(&self) -> &DetectionManager;
107}
108
109/// Capability to fall back to an alternate model when the primary fails.
110///
111/// Wraps a [`FallbackManager`] that acts as a circuit breaker: after
112/// consecutive API failures exceed a threshold, requests are rerouted
113/// to a fallback model until the primary stabilises.
114///
115/// # Implementors
116///
117/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
118///
119/// # When to use
120///
121/// Use this trait bound when you need to record API failures or check
122/// whether the circuit breaker has tripped.
123///
124/// ```rust,ignore
125/// fn handle_stream_error(runtime: &impl FallbackCapable) {
126/// let tripped = runtime.fallback().record_failure(loopctl::fallback::FailureKind::Transient);
127/// if tripped {
128/// if let Some(model) = runtime.fallback().fallback_model() {
129/// // switch to fallback model
130/// }
131/// }
132/// }
133/// ```
134pub trait FallbackCapable {
135 /// Returns the fallback manager (circuit breaker) for API model fallback.
136 ///
137 /// Use this to record API failures and check whether the circuit
138 /// breaker has tripped, indicating the primary model is unavailable.
139 fn fallback(&self) -> &FallbackManager;
140}
141
142/// Capability to compact conversation context when token usage exceeds a threshold.
143///
144/// When a [`ContextManager`] is configured, the loop checks token usage
145/// after each turn and triggers compaction when usage exceeds the
146/// configured threshold. Compaction replaces conversation messages with
147/// a compressed version, preserving the most recent context.
148///
149/// # Implementors
150///
151/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
152///
153/// # When to use
154///
155/// Use this trait bound when you need to inspect or trigger context
156/// compaction during the agent loop. Useful for custom loop
157/// implementations that need to manage the context window directly.
158pub trait Compactable {
159 /// Returns the context manager, if compaction is configured.
160 ///
161 /// Returns `None` when no compactor is set — the loop will not
162 /// auto-compact, and the host must manage context size manually.
163 fn context_manager(&self) -> Option<&Arc<ContextManager>>;
164}
165
166/// Capability to store, retrieve, and consolidate agent memory.
167///
168/// When a [`LoopMemory`](crate::memory::LoopMemory) backend is
169/// configured, the engine stores tool-execution trajectories, retrieves
170/// relevant entries as context before each turn, and consolidates the store
171/// at the end of a successful run.
172///
173/// # Implementors
174///
175/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
176///
177/// # When to use
178///
179/// Use this trait bound when you need to access the memory backend during
180/// the agent loop — for example to inspect what was stored or trigger a
181/// manual consolidation.
182pub trait RememberCapable {
183 /// Returns the memory backend, if configured.
184 ///
185 /// Returns `None` when no memory store is attached.
186 fn memory(&self) -> Option<&Arc<dyn crate::memory::LoopMemory>>;
187}
188
189/// Capability to stream LLM responses with retry, timeout, and fallback.
190///
191/// When a [`StreamHandler`] is configured, the loop delegates streaming
192/// to it instead of using the basic inline logic. The handler provides
193/// automatic retries, per-event timeouts, and fallback to non-streaming
194/// mode.
195///
196/// # Implementors
197///
198/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
199///
200/// # When to use
201///
202/// Use this trait bound when you need to access the stream handler for
203/// resilient streaming. Useful for custom loop implementations
204/// that need to control streaming behaviour (timeouts, retries, fallback
205/// to non-streaming mode).
206#[cfg(feature = "streaming")]
207pub trait StreamCapable {
208 /// Returns the stream handler.
209 ///
210 /// Always returns a handler — when no resilient handler is configured,
211 /// returns a shared reference to [`StreamHandler::passthrough_default`]
212 /// (a no-resilience handler that yields the raw provider stream with no
213 /// retries, timeouts, or fallback). The engine's `stream_turn` always
214 /// routes through a handler; this never returns `None`.
215 fn stream_handler(&self) -> &StreamHandler;
216}
217
218/// Capability to run bidirectional hooks that can block actions.
219///
220/// Hooks differ from observers ([`Observable`]) in that they return
221/// [`HookAction`](crate::hooks::HookAction) to control whether an action
222/// proceeds. The executor stops at the first hook that returns a blocking
223/// result.
224///
225/// # Implementors
226///
227/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
228///
229/// # When to use
230///
231/// Use this trait bound when you need to run hooks before or after
232/// tool dispatch, compaction, or run start/end.
233#[cfg(feature = "hooks")]
234pub trait Hookable {
235 /// Returns the hook executor, if hooks are configured.
236 ///
237 /// Returns `None` when no hook executor is set — no pre/post hooks
238 /// will fire. Hooks can approve or reject actions, unlike observers.
239 fn hook_executor(&self) -> Option<&HookExecutor>;
240}
241
242/// Capability to dispatch tools through a middleware pipeline.
243///
244/// When a pipeline is configured, tool calls flow through middleware
245/// layers (timeouts, output limiting, etc.) before reaching the registry.
246///
247/// # Implementors
248///
249/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
250///
251/// # When to use
252///
253/// Use this trait bound when you need to dispatch a tool call through
254/// optional middleware.
255///
256/// ```rust,ignore
257/// async fn dispatch(runtime: &impl PipelineAware, ctx: ToolDispatchContext) {
258/// if let Some(pipeline) = runtime.pipeline() {
259/// pipeline.invoke(ctx).await
260/// } else {
261/// // direct dispatch
262/// }
263/// }
264/// ```
265pub trait PipelineAware {
266 /// Returns the tool middleware pipeline, if configured.
267 ///
268 /// Returns `None` when no pipeline is set — tool calls go directly
269 /// to the registry. When set, every tool call passes through the
270 /// pipeline's middleware layers before reaching the tool.
271 fn pipeline(&self) -> Option<&ToolPipeline>;
272}
273
274/// Capability to track per-tool health with circuit breakers.
275///
276/// Records success/failure and latency for every tool dispatch.
277/// Tools that exceed the failure threshold have their circuit breaker
278/// opened, blocking subsequent calls until recovery.
279///
280/// *Requires `tool_health` feature.*
281///
282/// # Implementors
283///
284/// - [`LoopManagers`](crate::managers::LoopManagers) — the framework's default implementation.
285#[cfg(feature = "tool_health")]
286pub trait HealthTrackable {
287 /// Returns the tool health registry, if health tracking is configured.
288 ///
289 /// Returns `None` when no health registry is set — per-tool circuit
290 /// breakers will not fire. When set, records success/failure counts
291 /// and opens breakers for unhealthy tools.
292 fn health_registry(&self) -> Option<&ToolHealthRegistry>;
293}