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 session_storage: Option<Arc<dyn crate::session_services::SessionStorageStore>>,
147) -> Result<AssembledTurnContext> {
148 let ResolvedTurnContextInput {
149 snapshot,
150 messages,
151 message_source_sequence,
152 model,
153 resolved_model_id,
154 mcp_tool_definitions,
155 } = input;
156
157 let ResolvedRuntimeCapabilities {
158 effective_overlay,
159 resolved_capability_configs,
160 } = resolve_snapshot_capabilities(&snapshot, capability_registry);
161
162 let resolved_locale = extract_locale_override(&messages).or_else(|| snapshot.locale.clone());
163 let file_store =
164 file_store.map(|fs| crate::mount_fs::scoped_prompt_file_store(fs, snapshot.workspace_id));
165 let prompt_ctx = SystemPromptContext {
166 session_id: snapshot.session_id,
167 locale: resolved_locale.clone(),
168 file_store,
169 model: Some(model.model.clone()),
170 session_storage,
171 };
172 let compaction_policy = effective_overlay.capabilities.iter().find_map(|config| {
173 capability_registry
174 .get(config.capability_id())?
175 .compaction_policy(config.config_value())
176 });
177 let runtime_agent = build_runtime_agent(
178 &snapshot,
179 effective_overlay,
180 capability_registry,
181 &prompt_ctx,
182 &mcp_tool_definitions,
183 &model.model,
184 )
185 .await?;
186 let embedder_metadata = snapshot.embedder_metadata.clone();
187
188 Ok(AssembledTurnContext {
189 snapshot,
190 resolved_capability_configs,
191 messages,
192 message_source_sequence,
193 runtime_agent,
194 model,
195 resolved_model_id,
196 resolved_locale,
197 compaction_policy,
198 embedder_metadata,
199 })
200}
201
202pub fn resolve_snapshot_capabilities(
205 snapshot: &ResolvedExecutionSnapshot,
206 capability_registry: &CapabilityRegistry,
207) -> ResolvedRuntimeCapabilities {
208 let effective_overlay = AgentConfigOverlay {
209 system_prompt: snapshot.instructions.clone(),
210 capabilities: snapshot.capabilities.clone(),
211 initial_files: snapshot.initial_files.clone(),
212 network_access: snapshot.network_access.clone(),
213 default_model_id: snapshot.default_model_id,
214 tools: snapshot.tools.clone(),
215 max_iterations: snapshot.max_iterations,
216 parallel_tool_calls: snapshot.parallel_tool_calls,
217 mcp_servers: Default::default(),
218 };
219 let resolved_capability_configs =
220 resolve_capability_configs(&effective_overlay.capabilities, capability_registry)
221 .unwrap_or_else(|error| {
222 tracing::warn!(
223 error = ?error,
224 "failed to resolve capability configs; falling back to snapshot capabilities"
225 );
226 effective_overlay.capabilities.clone()
227 });
228 ResolvedRuntimeCapabilities {
229 effective_overlay,
230 resolved_capability_configs,
231 }
232}
233
234pub fn resolve_runtime_capabilities(
237 harness: &HarnessDefinition,
238 agent: Option<&AgentDefinition>,
239 session: &ExecutionSession,
240 capability_registry: &CapabilityRegistry,
241) -> ResolvedRuntimeCapabilities {
242 let effective_overlay = AgentConfigOverlay::fold(
243 [AgentConfigOverlay::from(harness)]
244 .into_iter()
245 .chain(agent.into_iter().map(AgentConfigOverlay::from))
246 .chain([AgentConfigOverlay::from(session)]),
247 );
248 let resolved_capability_configs =
249 resolve_capability_configs(&effective_overlay.capabilities, capability_registry)
250 .unwrap_or_else(|error| {
251 tracing::warn!(error = ?error, "failed to resolve capability configs");
252 effective_overlay.capabilities.clone()
253 });
254 ResolvedRuntimeCapabilities {
255 effective_overlay,
256 resolved_capability_configs,
257 }
258}
259
260async fn build_runtime_agent(
261 snapshot: &ResolvedExecutionSnapshot,
262 mut effective_overlay: AgentConfigOverlay,
263 capability_registry: &CapabilityRegistry,
264 prompt_ctx: &SystemPromptContext,
265 mcp_tool_definitions: &[ToolDefinition],
266 model: &str,
267) -> Result<RuntimeAgent> {
268 let mut runtime_agent = if let Some(ref blueprint_id) = snapshot.blueprint_id {
269 let blueprint = capability_registry.blueprint(blueprint_id).ok_or_else(|| {
270 anyhow::anyhow!(
271 "Unknown blueprint: \"{blueprint_id}\". Snapshot references a blueprint absent from the registry."
272 )
273 })?;
274 let blueprint_model = match &blueprint.model {
275 crate::capabilities::BlueprintModel::Fixed(model) => model.clone(),
276 crate::capabilities::BlueprintModel::Default(default) => snapshot
277 .blueprint_config
278 .as_ref()
279 .and_then(|config| config.get("model"))
280 .and_then(|value| value.as_str())
281 .map(str::to_owned)
282 .unwrap_or_else(|| default.clone()),
283 crate::capabilities::BlueprintModel::Inherit => model.to_owned(),
284 };
285 let mut prompt = blueprint.system_prompt.to_string();
286 if let Some(ref config) = snapshot.blueprint_config {
287 prompt.push_str(&format!("\n\n<config>\n{}\n</config>", config));
288 }
289 RuntimeAgentBuilder::new()
290 .system_prompt(&prompt)
291 .tools(blueprint.tool_definitions())
292 .model(&blueprint_model)
293 .max_iterations(blueprint.max_turns.unwrap_or(20))
294 .network_access(effective_overlay.network_access.clone())
295 .with_locale(prompt_ctx.locale.as_deref())
296 .build()
297 } else {
298 let overlay_tools = std::mem::take(&mut effective_overlay.tools);
299 RuntimeAgentBuilder::from_overlay(effective_overlay, capability_registry, prompt_ctx)
300 .await
301 .with_locale(prompt_ctx.locale.as_deref())
302 .tools(mcp_tool_definitions.iter().cloned())
303 .tools(overlay_tools)
304 .model(model)
305 .build()
306 };
307 if crate::progress_reporting::session_uses_report_progress(&snapshot.tags) {
308 runtime_agent = crate::progress_reporting::apply_report_progress_mode(runtime_agent);
309 }
310 Ok(runtime_agent)
311}
312
313fn extract_locale_override(messages: &[Message]) -> Option<String> {
314 messages
315 .iter()
316 .rev()
317 .find(|message| message.role == MessageRole::User)
318 .and_then(|message| message.controls.as_ref())
319 .and_then(|controls| controls.locale.as_deref())
320 .map(str::trim)
321 .filter(|value| !value.is_empty())
322 .map(str::to_owned)
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 struct CredentialCapturingDriver {
330 _secret: String,
331 }
332
333 #[async_trait::async_trait]
334 impl ChatDriver for CredentialCapturingDriver {
335 async fn chat_completion_stream(
336 &self,
337 _endpoint: &everruns_provider::ProviderEndpoint,
338 _messages: Vec<crate::LlmMessage>,
339 _config: &crate::LlmCallConfig,
340 ) -> crate::Result<crate::LlmResponseStream> {
341 unreachable!("debug-surface test never invokes the driver")
342 }
343 }
344
345 #[test]
346 fn resolved_model_execution_debug_is_credential_safe() {
347 let secret = "credential-marker-that-must-not-leak";
348 let resolved = ResolvedModelExecution {
349 model: "model-name".into(),
350 provider: crate::ProviderKey::new("provider-account"),
351 provider_type: DriverId::OpenAI,
352 driver: Arc::new(CredentialCapturingDriver {
353 _secret: secret.into(),
354 }),
355 };
356
357 let debug = format!("{resolved:?}");
358 assert!(debug.contains("provider-account"));
359 assert!(debug.contains("<opaque>"));
360 assert!(!debug.contains(secret));
361 }
362}