Skip to main content

everruns_core/
runtime_context.rs

1//! Pure turn-context transformations over host-resolved execution inputs.
2//!
3//! Store access, lifecycle validation, model lookup, provider configuration,
4//! and driver creation belong to `everruns-host`. The kernel receives the
5//! neutral snapshot, already-filtered messages, a credential-free model spec,
6//! and an opaque ready driver.
7
8use 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/// Narrow host seam used by callers that cannot preassemble a context before
29/// invoking a reason atom. Implementations live outside the kernel.
30#[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    /// Session being executed.
41    pub session_id: SessionId,
42    /// Harness expected by the scheduled turn.
43    pub harness_id: crate::HarnessId,
44    /// Agent expected by the scheduled turn, when any.
45    pub agent_id: Option<crate::AgentId>,
46    /// Host-discovered MCP tools available for this turn.
47    pub mcp_tool_definitions: Vec<ToolDefinition>,
48}
49
50/// Credential-safe provider input prepared by a runtime host.
51///
52/// The model and provider identities are safe values. Authentication and
53/// endpoint configuration are captured only inside the opaque driver.
54#[derive(Clone)]
55pub struct ResolvedModelExecution {
56    /// Provider model name.
57    pub model: String,
58    /// Open provider account identity.
59    pub provider: crate::ProviderKey,
60    /// Registered driver integration kind.
61    pub provider_type: DriverId,
62    /// Ready provider driver. Credential-bearing construction state stays
63    /// opaque and is redacted from Debug output.
64    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/// Host-resolved values consumed by the pure context assembler.
79#[derive(Debug, Clone)]
80pub struct ResolvedTurnContextInput {
81    /// Effective, secret-free execution snapshot.
82    pub snapshot: ResolvedExecutionSnapshot,
83    /// Already-filtered model-visible history.
84    pub messages: Vec<Message>,
85    /// Highest canonical history sequence represented by `messages`.
86    pub message_source_sequence: Option<i64>,
87    /// Credential-safe model identity and opaque ready driver.
88    pub model: ResolvedModelExecution,
89    /// Configured model ID selected for this turn, when any.
90    pub resolved_model_id: Option<ModelId>,
91    /// Host-discovered MCP tool definitions.
92    pub mcp_tool_definitions: Vec<ToolDefinition>,
93}
94
95/// Credential-safe context consumed by kernel reason execution.
96#[derive(Debug, Clone)]
97pub struct AssembledTurnContext {
98    /// Effective, secret-free execution snapshot.
99    pub snapshot: ResolvedExecutionSnapshot,
100    /// Capability configurations after dependency expansion.
101    pub resolved_capability_configs: Vec<AgentCapabilityConfig>,
102    /// Filtered conversation history visible to the model.
103    pub messages: Vec<Message>,
104    /// Highest canonical history sequence represented by `messages`.
105    pub message_source_sequence: Option<i64>,
106    /// Fully assembled runtime agent for this turn.
107    pub runtime_agent: RuntimeAgent,
108    /// Credential-safe model identity and opaque ready driver.
109    pub model: ResolvedModelExecution,
110    /// Configured model ID selected for this turn, when any.
111    pub resolved_model_id: Option<ModelId>,
112    /// Locale selected from message controls or snapshot defaults.
113    pub resolved_locale: Option<String>,
114    /// Capability-owned compaction policy, when configured.
115    pub compaction_policy: Option<Arc<dyn CompactionPolicy>>,
116    /// Effective embedder metadata folded by the host loading seam.
117    pub embedder_metadata: BTreeMap<String, String>,
118}
119
120impl AssembledTurnContext {
121    /// Session correlation ID without exposing a session record.
122    pub fn session_id(&self) -> SessionId {
123        self.snapshot.session_id
124    }
125
126    /// Cumulative usage projected into the execution snapshot.
127    pub fn cumulative_usage(&self) -> Option<TokenUsage> {
128        self.snapshot.cumulative_usage.clone()
129    }
130}
131
132/// Capability resolution over a neutral execution snapshot.
133#[derive(Debug, Clone)]
134pub struct ResolvedRuntimeCapabilities {
135    /// Effective configuration overlay.
136    pub effective_overlay: AgentConfigOverlay,
137    /// Capability configurations after dependency expansion.
138    pub resolved_capability_configs: Vec<AgentCapabilityConfig>,
139}
140
141/// Build a kernel context from values already resolved by a host.
142pub 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
200/// Resolve capabilities and reconstruct the effective overlay represented by
201/// the already-folded snapshot.
202pub 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
232/// Pure overlay/capability transformation retained for callers that are
233/// projecting loaded definitions at their own host boundary.
234pub 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}