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
use super::{AgentConfig, AgentLoop, ModelMiddlewareHealthSnapshot, ModelMiddlewareObs};
use crate::llm::{LlmClient, ModelGenerationAdmission};
use crate::loop_checkpoint::LoopCheckpointSink;
use crate::session_lane_queue::SessionLaneQueue;
use crate::tools::{ToolContext, ToolExecutor};
use std::sync::Arc;
impl AgentLoop {
pub(crate) fn new(
llm_client: Arc<dyn LlmClient>,
tool_executor: Arc<ToolExecutor>,
tool_context: ToolContext,
config: AgentConfig,
) -> Self {
let model_generation_admission =
ModelGenerationAdmission::new(llm_client.model_generation_concurrency());
Self {
llm_client,
model_generation_admission,
shared_model_generation_admission: false,
middleware_obs: ModelMiddlewareObs::shared(),
tool_executor,
tool_context,
config,
command_queue: None,
checkpoint_sink: None,
checkpoint_run_id: None,
checkpoint_capability_binding: None,
bound_invocation: None,
capability_runtime: None,
}
}
/// Reuse the provider admission gate owned by the surrounding session.
///
/// `AgentLoop` instances are rebuilt for each host-direct call so they can
/// snapshot live tools and governance. Model-generation capacity is a
/// provider/session contract, not a per-call resource, and therefore must
/// survive those rebuilds.
pub(crate) fn with_model_generation_admission(
mut self,
admission: ModelGenerationAdmission,
) -> Self {
self.model_generation_admission = admission;
self.shared_model_generation_admission = true;
self
}
/// Reuse the middleware observation window owned by the surrounding session.
///
/// Loops are rebuilt per host-direct call; stage counters must survive those
/// rebuilds so hosts can inspect one secret-free health snapshot per session.
pub(crate) fn with_model_middleware_obs(
mut self,
middleware_obs: Arc<ModelMiddlewareObs>,
) -> Self {
self.middleware_obs = middleware_obs;
self
}
/// Return secret-free middleware stage counters for this loop's observation window.
///
/// The session facade reads the shared obs Arc directly; unit tests still
/// query health through [`AgentLoop`].
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn model_middleware_health(&self) -> ModelMiddlewareHealthSnapshot {
self.middleware_obs.snapshot()
}
pub(crate) fn with_capability_runtime(
mut self,
runtime: crate::capability::AgentCapabilityRuntime,
) -> Self {
self.capability_runtime = Some(runtime);
self
}
pub(crate) fn begin_capability_operation(
&self,
logical_turn: usize,
fallback_cancellation: &tokio_util::sync::CancellationToken,
label: &'static str,
) -> anyhow::Result<crate::capability::AgentCapabilityOperation> {
crate::capability::AgentCapabilityOperation::begin(
self.capability_runtime.as_ref(),
logical_turn,
fallback_cancellation,
label,
)
.map_err(Into::into)
}
/// Set the lane queue for priority-based tool execution.
///
/// When set, tools are routed through the lane queue which supports
/// External task handling for multi-machine parallel processing.
pub fn with_queue(mut self, queue: Arc<SessionLaneQueue>) -> Self {
self.command_queue = Some(queue);
self
}
/// Attach a per-tool-round checkpoint sink. After each completed
/// tool round the loop will call `sink.save_checkpoint(...)`.
///
/// The sink is independent from the run id: call
/// [`AgentLoop::set_checkpoint_run`] before executing to bind the
/// run id this execution will use.
pub fn with_checkpoint_sink(mut self, sink: Arc<dyn LoopCheckpointSink>) -> Self {
self.checkpoint_sink = Some(sink);
self
}
/// Bind the run id used by per-tool-round checkpoints. Called per
/// execution so a single `AgentLoop` (which is cheap to clone) can
/// host successive runs.
pub fn set_checkpoint_run(&mut self, run_id: impl Into<String>) {
self.checkpoint_run_id = Some(run_id.into());
}
pub(crate) fn with_checkpoint_capability_binding(
mut self,
binding: crate::capability::RunCapabilityBindingV1,
) -> Self {
self.checkpoint_capability_binding = Some(binding);
self
}
pub(crate) fn checkpoint_capability_binding(
&self,
) -> Option<&crate::capability::RunCapabilityBindingV1> {
self.checkpoint_capability_binding.as_ref()
}
/// Return the immutable Hook executor captured by this loop's Run
/// projection. Run-control requests must use this executor rather than a
/// mutable Session-level reference so policy cannot change mid-run.
pub(crate) fn hook_executor(&self) -> Option<Arc<dyn crate::hooks::HookExecutor>> {
self.config.hook_engine.clone()
}
}