1use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use crate::agent_definition::AgentDefinition;
12use crate::capabilities::{CapabilityRegistry, SystemPromptContext, resolve_capability_configs};
13use crate::compaction_policy::CompactionPolicy;
14use crate::config_layer::AgentConfigOverlay;
15use crate::driver_registry::ChatDriver;
16use crate::error::Result;
17use crate::events::TokenUsage;
18use crate::harness_definition::HarnessDefinition;
19use crate::message::{Message, MessageRole};
20use crate::provider::DriverId;
21use crate::runtime_agent::{RuntimeAgent, RuntimeAgentBuilder};
22use crate::session::ExecutionSession;
23use crate::session_files::SessionFileSystem;
24use crate::tool_types::ToolDefinition;
25use crate::typed_id::{ModelId, SessionId};
26use crate::{AgentCapabilityConfig, ResolvedExecutionSnapshot};
27
28#[async_trait::async_trait]
31pub trait TurnContextResolver: Send + Sync {
32 async fn resolve_turn_context(
33 &self,
34 request: TurnContextRequest,
35 ) -> Result<AssembledTurnContext>;
36}
37
38#[derive(Debug, Clone)]
39pub struct TurnContextRequest {
40 pub session_id: SessionId,
42 pub harness_id: crate::HarnessId,
44 pub agent_id: Option<crate::AgentId>,
46 pub mcp_tool_definitions: Vec<ToolDefinition>,
48}
49
50#[derive(Clone)]
55pub struct ResolvedModelExecution {
56 pub model: String,
58 pub provider: crate::ProviderKey,
60 pub provider_type: DriverId,
62 pub driver: Arc<dyn ChatDriver>,
65}
66
67impl std::fmt::Debug for ResolvedModelExecution {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("ResolvedModelExecution")
70 .field("model", &self.model)
71 .field("provider", &self.provider)
72 .field("provider_type", &self.provider_type)
73 .field("driver", &"<opaque>")
74 .finish()
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct ResolvedTurnContextInput {
81 pub snapshot: ResolvedExecutionSnapshot,
83 pub messages: Vec<Message>,
85 pub message_source_sequence: Option<i64>,
87 pub model: ResolvedModelExecution,
89 pub resolved_model_id: Option<ModelId>,
91 pub mcp_tool_definitions: Vec<ToolDefinition>,
93}
94
95#[derive(Debug, Clone)]
97pub struct AssembledTurnContext {
98 pub snapshot: ResolvedExecutionSnapshot,
100 pub resolved_capability_configs: Vec<AgentCapabilityConfig>,
102 pub messages: Vec<Message>,
104 pub message_source_sequence: Option<i64>,
106 pub runtime_agent: RuntimeAgent,
108 pub model: ResolvedModelExecution,
110 pub resolved_model_id: Option<ModelId>,
112 pub resolved_locale: Option<String>,
114 pub compaction_policy: Option<Arc<dyn CompactionPolicy>>,
116 pub embedder_metadata: BTreeMap<String, String>,
118}
119
120impl AssembledTurnContext {
121 pub fn session_id(&self) -> SessionId {
123 self.snapshot.session_id
124 }
125
126 pub fn cumulative_usage(&self) -> Option<TokenUsage> {
128 self.snapshot.cumulative_usage.clone()
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct ResolvedRuntimeCapabilities {
135 pub effective_overlay: AgentConfigOverlay,
137 pub resolved_capability_configs: Vec<AgentCapabilityConfig>,
139}
140
141pub async fn assemble_resolved_turn_context(
143 input: ResolvedTurnContextInput,
144 capability_registry: &CapabilityRegistry,
145 file_store: Option<Arc<dyn SessionFileSystem>>,
146) -> Result<AssembledTurnContext> {
147 let ResolvedTurnContextInput {
148 snapshot,
149 messages,
150 message_source_sequence,
151 model,
152 resolved_model_id,
153 mcp_tool_definitions,
154 } = input;
155
156 let ResolvedRuntimeCapabilities {
157 effective_overlay,
158 resolved_capability_configs,
159 } = resolve_snapshot_capabilities(&snapshot, capability_registry);
160
161 let resolved_locale = extract_locale_override(&messages).or_else(|| snapshot.locale.clone());
162 let file_store =
163 file_store.map(|fs| crate::mount_fs::scoped_prompt_file_store(fs, snapshot.workspace_id));
164 let prompt_ctx = SystemPromptContext {
165 session_id: snapshot.session_id,
166 locale: resolved_locale.clone(),
167 file_store,
168 model: Some(model.model.clone()),
169 };
170 let compaction_policy = effective_overlay.capabilities.iter().find_map(|config| {
171 capability_registry
172 .get(config.capability_id())?
173 .compaction_policy(config.config_value())
174 });
175 let runtime_agent = build_runtime_agent(
176 &snapshot,
177 effective_overlay,
178 capability_registry,
179 &prompt_ctx,
180 &mcp_tool_definitions,
181 &model.model,
182 )
183 .await?;
184 let embedder_metadata = snapshot.embedder_metadata.clone();
185
186 Ok(AssembledTurnContext {
187 snapshot,
188 resolved_capability_configs,
189 messages,
190 message_source_sequence,
191 runtime_agent,
192 model,
193 resolved_model_id,
194 resolved_locale,
195 compaction_policy,
196 embedder_metadata,
197 })
198}
199
200pub fn resolve_snapshot_capabilities(
203 snapshot: &ResolvedExecutionSnapshot,
204 capability_registry: &CapabilityRegistry,
205) -> ResolvedRuntimeCapabilities {
206 let effective_overlay = AgentConfigOverlay {
207 system_prompt: snapshot.instructions.clone(),
208 capabilities: snapshot.capabilities.clone(),
209 initial_files: snapshot.initial_files.clone(),
210 network_access: snapshot.network_access.clone(),
211 default_model_id: snapshot.default_model_id,
212 tools: snapshot.tools.clone(),
213 max_iterations: snapshot.max_iterations,
214 parallel_tool_calls: snapshot.parallel_tool_calls,
215 mcp_servers: Default::default(),
216 };
217 let resolved_capability_configs =
218 resolve_capability_configs(&effective_overlay.capabilities, capability_registry)
219 .unwrap_or_else(|error| {
220 tracing::warn!(
221 error = ?error,
222 "failed to resolve capability configs; falling back to snapshot capabilities"
223 );
224 effective_overlay.capabilities.clone()
225 });
226 ResolvedRuntimeCapabilities {
227 effective_overlay,
228 resolved_capability_configs,
229 }
230}
231
232pub fn resolve_runtime_capabilities(
235 harness: &HarnessDefinition,
236 agent: Option<&AgentDefinition>,
237 session: &ExecutionSession,
238 capability_registry: &CapabilityRegistry,
239) -> ResolvedRuntimeCapabilities {
240 let effective_overlay = AgentConfigOverlay::fold(
241 [AgentConfigOverlay::from(harness)]
242 .into_iter()
243 .chain(agent.into_iter().map(AgentConfigOverlay::from))
244 .chain([AgentConfigOverlay::from(session)]),
245 );
246 let resolved_capability_configs =
247 resolve_capability_configs(&effective_overlay.capabilities, capability_registry)
248 .unwrap_or_else(|error| {
249 tracing::warn!(error = ?error, "failed to resolve capability configs");
250 effective_overlay.capabilities.clone()
251 });
252 ResolvedRuntimeCapabilities {
253 effective_overlay,
254 resolved_capability_configs,
255 }
256}
257
258async fn build_runtime_agent(
259 snapshot: &ResolvedExecutionSnapshot,
260 mut effective_overlay: AgentConfigOverlay,
261 capability_registry: &CapabilityRegistry,
262 prompt_ctx: &SystemPromptContext,
263 mcp_tool_definitions: &[ToolDefinition],
264 model: &str,
265) -> Result<RuntimeAgent> {
266 let mut runtime_agent = if let Some(ref blueprint_id) = snapshot.blueprint_id {
267 let blueprint = capability_registry.blueprint(blueprint_id).ok_or_else(|| {
268 anyhow::anyhow!(
269 "Unknown blueprint: \"{blueprint_id}\". Snapshot references a blueprint absent from the registry."
270 )
271 })?;
272 let blueprint_model = match &blueprint.model {
273 crate::capabilities::BlueprintModel::Fixed(model) => model.clone(),
274 crate::capabilities::BlueprintModel::Default(default) => snapshot
275 .blueprint_config
276 .as_ref()
277 .and_then(|config| config.get("model"))
278 .and_then(|value| value.as_str())
279 .map(str::to_owned)
280 .unwrap_or_else(|| default.clone()),
281 crate::capabilities::BlueprintModel::Inherit => model.to_owned(),
282 };
283 let mut prompt = blueprint.system_prompt.to_string();
284 if let Some(ref config) = snapshot.blueprint_config {
285 prompt.push_str(&format!("\n\n<config>\n{}\n</config>", config));
286 }
287 RuntimeAgentBuilder::new()
288 .system_prompt(&prompt)
289 .tools(blueprint.tool_definitions())
290 .model(&blueprint_model)
291 .max_iterations(blueprint.max_turns.unwrap_or(20))
292 .network_access(effective_overlay.network_access.clone())
293 .with_locale(prompt_ctx.locale.as_deref())
294 .build()
295 } else {
296 let overlay_tools = std::mem::take(&mut effective_overlay.tools);
297 RuntimeAgentBuilder::from_overlay(effective_overlay, capability_registry, prompt_ctx)
298 .await
299 .with_locale(prompt_ctx.locale.as_deref())
300 .tools(mcp_tool_definitions.iter().cloned())
301 .tools(overlay_tools)
302 .model(model)
303 .build()
304 };
305 if crate::progress_reporting::session_uses_report_progress(&snapshot.tags) {
306 runtime_agent = crate::progress_reporting::apply_report_progress_mode(runtime_agent);
307 }
308 Ok(runtime_agent)
309}
310
311fn extract_locale_override(messages: &[Message]) -> Option<String> {
312 messages
313 .iter()
314 .rev()
315 .find(|message| message.role == MessageRole::User)
316 .and_then(|message| message.controls.as_ref())
317 .and_then(|controls| controls.locale.as_deref())
318 .map(str::trim)
319 .filter(|value| !value.is_empty())
320 .map(str::to_owned)
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 struct CredentialCapturingDriver {
328 _secret: String,
329 }
330
331 #[async_trait::async_trait]
332 impl ChatDriver for CredentialCapturingDriver {
333 async fn chat_completion_stream(
334 &self,
335 _endpoint: &everruns_provider::ProviderEndpoint,
336 _messages: Vec<crate::LlmMessage>,
337 _config: &crate::LlmCallConfig,
338 ) -> crate::Result<crate::LlmResponseStream> {
339 unreachable!("debug-surface test never invokes the driver")
340 }
341 }
342
343 #[test]
344 fn resolved_model_execution_debug_is_credential_safe() {
345 let secret = "credential-marker-that-must-not-leak";
346 let resolved = ResolvedModelExecution {
347 model: "model-name".into(),
348 provider: crate::ProviderKey::new("provider-account"),
349 provider_type: DriverId::OpenAI,
350 driver: Arc::new(CredentialCapturingDriver {
351 _secret: secret.into(),
352 }),
353 };
354
355 let debug = format!("{resolved:?}");
356 assert!(debug.contains("provider-account"));
357 assert!(debug.contains("<opaque>"));
358 assert!(!debug.contains(secret));
359 }
360}