1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! Configuration for the agent loop.
use std::sync::Arc;
use crate::agent_options::{ApproveToolFn, GetApiKeyFn};
use crate::async_context_transformer::AsyncContextTransformer;
use crate::fallback::ModelFallback;
use crate::message_provider::MessageProvider;
use crate::retry::{DefaultRetryStrategy, RetryStrategy};
use crate::stream::{StreamFn, StreamOptions};
use crate::tool::{AgentTool, ApprovalMode};
use crate::tool_execution_policy::ToolExecutionPolicy;
use crate::types::{AgentMessage, ModelSpec};
use super::ConvertToLlmFn;
// ─── AgentLoopRuntimeState ──────────────────────────────────────────────────
#[derive(Default)]
pub(crate) struct AgentLoopRuntimeState {
/// Shared snapshot of loop-local pending messages for pause checkpoints.
pending_messages: Arc<crate::pause_state::PendingMessageSnapshot>,
/// Shared snapshot of the loop's full `context_messages` for pause checkpoints.
///
/// Updated after each turn's pending-message drain so that `Agent::pause()`
/// can reconstruct the complete message history even for messages that have
/// been moved out of the shared pending queue and into loop-local context.
loop_context: Arc<crate::pause_state::LoopContextSnapshot>,
}
impl AgentLoopRuntimeState {
#[must_use]
pub(crate) fn from_snapshots(
pending_messages: Arc<crate::pause_state::PendingMessageSnapshot>,
loop_context: Arc<crate::pause_state::LoopContextSnapshot>,
) -> Self {
Self {
pending_messages,
loop_context,
}
}
pub(crate) fn clear_pending_messages(&self) {
self.pending_messages.clear();
}
pub(crate) fn replace_pending_messages(&self, messages: &[AgentMessage]) {
self.pending_messages.replace(messages);
}
pub(crate) fn replace_loop_context(&self, messages: &[AgentMessage]) {
self.loop_context.replace(messages);
}
}
// ─── AgentLoopConfig ─────────────────────────────────────────────────────────
/// Configuration for the agent loop.
///
/// Carries the model spec, stream options, retry strategy, stream function,
/// tools, and all the hooks that the loop calls at various points.
pub struct AgentLoopConfig {
/// Optional agent name used for transfer chain safety enforcement.
///
/// When set, the loop pushes this name onto the [`TransferChain`](crate::transfer::TransferChain)
/// at startup so circular transfers back to this agent are detected.
pub agent_name: Option<String>,
/// Optional transfer chain carried from a previous handoff.
///
/// When set, the loop resumes transfer safety checks from this chain.
pub transfer_chain: Option<crate::transfer::TransferChain>,
/// Model specification passed through to `StreamFn`.
pub model: ModelSpec,
/// Stream options passed through to `StreamFn`.
pub stream_options: StreamOptions,
/// Retry strategy applied to model calls.
pub retry_strategy: Box<dyn RetryStrategy>,
/// The pluggable streaming function that calls the LLM provider.
pub stream_fn: Arc<dyn StreamFn>,
/// Available tools for the agent to call.
pub tools: Vec<Arc<dyn AgentTool>>,
/// Converts an `AgentMessage` to an `LlmMessage` for the provider.
/// Returns `None` to filter out custom or UI-only messages.
pub convert_to_llm: Box<ConvertToLlmFn>,
/// Optional hook called before `convert_to_llm`; used for context pruning,
/// token budget enforcement, or external context injection.
/// When the overflow signal is set, the transformer should prune more
/// aggressively.
pub transform_context: Option<Arc<dyn crate::context_transformer::ContextTransformer>>,
/// Optional async callback for dynamic API key resolution.
pub get_api_key: Option<Box<GetApiKeyFn>>,
/// Optional provider polled for steering and follow-up messages.
///
/// [`MessageProvider::poll_steering`] is called after each tool execution batch.
/// [`MessageProvider::poll_follow_up`] is called when the agent would otherwise stop.
pub message_provider: Option<Arc<dyn MessageProvider>>,
/// Runtime-only loop state owned by `Agent` or initialized internally for
/// low-level loop callers.
pub(crate) runtime_state: AgentLoopRuntimeState,
/// Optional async callback for approving/rejecting tool calls before execution.
/// When `Some` and `approval_mode` is `Enabled`, each tool call is sent through
/// this callback before dispatch. Rejected tools return an error result to the LLM.
pub approve_tool: Option<Box<ApproveToolFn>>,
/// Controls whether the approval gate is active. Defaults to `Enabled`.
pub approval_mode: ApprovalMode,
/// Pre-turn policies evaluated before each LLM call.
pub pre_turn_policies: Vec<Arc<dyn crate::policy::PreTurnPolicy>>,
/// Pre-dispatch policies evaluated per tool call, before approval.
pub pre_dispatch_policies: Vec<Arc<dyn crate::policy::PreDispatchPolicy>>,
/// Post-turn policies evaluated after each completed turn.
pub post_turn_policies: Vec<Arc<dyn crate::policy::PostTurnPolicy>>,
/// Post-loop policies evaluated after the inner loop exits.
pub post_loop_policies: Vec<Arc<dyn crate::policy::PostLoopPolicy>>,
/// Optional async context transformer (runs before the sync transformer).
///
/// Enables async operations like fetching summaries or RAG retrieval
/// before context compaction.
pub async_transform_context: Option<Arc<dyn AsyncContextTransformer>>,
/// Optional metrics collector invoked at the end of each turn with
/// per-turn timing, token usage, and cost data.
pub metrics_collector: Option<Arc<dyn crate::metrics::MetricsCollector>>,
/// Optional model fallback chain tried when the primary model exhausts
/// its retry budget on a retryable error.
pub fallback: Option<ModelFallback>,
/// Controls how tool calls within a turn are dispatched.
///
/// Defaults to [`ToolExecutionPolicy::Concurrent`] for backward
/// compatibility.
pub tool_execution_policy: ToolExecutionPolicy,
/// Session key-value state store shared with tools and policies.
pub session_state: Arc<std::sync::RwLock<crate::SessionState>>,
/// Optional credential resolver for tool authentication.
pub credential_resolver: Option<Arc<dyn crate::credential::CredentialResolver>>,
/// Timeout applied around a credential resolver's `resolve()` call.
/// Defaults to 30 seconds; see [`AgentOptions::with_credential_timeout`](crate::AgentOptions::with_credential_timeout).
pub credential_timeout: std::time::Duration,
/// Optional context caching configuration.
pub cache_config: Option<crate::context_cache::CacheConfig>,
/// Mutable cache state tracking turns since last write.
pub cache_state: std::sync::Mutex<crate::context_cache::CacheState>,
/// Optional dynamic system prompt closure (called fresh each turn).
///
/// Its output is injected as a user-role message after the system prompt
/// to avoid invalidating provider-side caches.
pub dynamic_system_prompt: Option<Arc<dyn Fn() -> String + Send + Sync>>,
}
impl AgentLoopConfig {
/// Create a loop configuration with public runtime defaults.
///
/// The state used by `Agent::pause()` is initialized internally so low-level
/// loop callers do not need to construct private checkpoint plumbing.
#[must_use]
pub fn new(
model: ModelSpec,
stream_fn: Arc<dyn StreamFn>,
convert_to_llm: Box<ConvertToLlmFn>,
) -> Self {
Self {
agent_name: None,
transfer_chain: None,
model,
stream_options: StreamOptions::default(),
retry_strategy: Box::new(DefaultRetryStrategy::default()),
stream_fn,
tools: vec![],
convert_to_llm,
transform_context: None,
get_api_key: None,
message_provider: None,
runtime_state: AgentLoopRuntimeState::default(),
approve_tool: None,
approval_mode: ApprovalMode::default(),
pre_turn_policies: vec![],
pre_dispatch_policies: vec![],
post_turn_policies: vec![],
post_loop_policies: vec![],
async_transform_context: None,
metrics_collector: None,
fallback: None,
tool_execution_policy: ToolExecutionPolicy::default(),
session_state: Arc::new(std::sync::RwLock::new(crate::SessionState::new())),
credential_resolver: None,
credential_timeout: std::time::Duration::from_secs(30),
cache_config: None,
cache_state: std::sync::Mutex::new(crate::context_cache::CacheState::default()),
dynamic_system_prompt: None,
}
}
pub(crate) fn with_runtime_snapshots(
mut self,
pending_message_snapshot: Arc<crate::pause_state::PendingMessageSnapshot>,
loop_context_snapshot: Arc<crate::pause_state::LoopContextSnapshot>,
) -> Self {
self.runtime_state =
AgentLoopRuntimeState::from_snapshots(pending_message_snapshot, loop_context_snapshot);
self
}
}
impl std::fmt::Debug for AgentLoopConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentLoopConfig")
.field("model", &self.model)
.field("stream_options", &self.stream_options)
.field("tools", &format_args!("[{} tool(s)]", self.tools.len()))
.field(
"pre_turn_policies",
&format_args!("[{} policy(ies)]", self.pre_turn_policies.len()),
)
.field(
"pre_dispatch_policies",
&format_args!("[{} policy(ies)]", self.pre_dispatch_policies.len()),
)
.field(
"post_turn_policies",
&format_args!("[{} policy(ies)]", self.post_turn_policies.len()),
)
.field(
"post_loop_policies",
&format_args!("[{} policy(ies)]", self.post_loop_policies.len()),
)
.field("tool_execution_policy", &self.tool_execution_policy)
.finish_non_exhaustive()
}
}