Skip to main content

everruns_runtime/
runtime.rs

1// In-process runtime builder and runner.
2// Decision: the public runtime is in-memory today, but uses the same core atoms
3// and capability resolution path as the durable worker so behavior stays close.
4
5use crate::backends::{
6    EventBus, RuntimeAgentStore, RuntimeBackends, RuntimeHarnessStore, RuntimeMessageStore,
7    RuntimeProviderStore, RuntimeSessionStore,
8};
9use crate::builders::SingleSessionBuilder;
10use crate::host::{
11    RuntimeHostAdapter, RuntimeHostTurnContext, execute_act_activity, execute_input_activity,
12    execute_reason_activity_with_prompt_messages,
13};
14use crate::in_memory::{InMemorySessionFileStore, InMemorySessionFileSystemFactory};
15use crate::turn_strategy::{RuntimeTurnPlan, RuntimeTurnState};
16use async_trait::async_trait;
17use chrono::Utc;
18use everruns_core::agent::Agent;
19use everruns_core::atoms::{AtomContext, InputAtomInput, ReasonInput};
20use everruns_core::capabilities::{
21    Capability, CapabilityRegistry, CapabilityStatus, collect_capability_mcp_servers,
22    resolve_capability_configs,
23};
24use everruns_core::config_layer::AgentConfigOverlay;
25use everruns_core::driver_registry::{DriverId, DriverRegistry};
26use everruns_core::error::{AgentLoopError, Result};
27use everruns_core::events::{
28    Event, EventContext, EventData, EventRequest, InputMessageData, OutputMessageCompletedData,
29    ToolCompletedData,
30};
31use everruns_core::harness::Harness;
32use everruns_core::llmsim_driver::{LlmSimConfig, LlmSimDriver};
33use everruns_core::message::{ContentPart, Message};
34use everruns_core::platform_definition::PlatformDefinition;
35use everruns_core::plugins::{PluginFileSet, compile_plugin};
36use everruns_core::runtime_context::{AssembledTurnContext, inspect_turn_context};
37use everruns_core::session::{Session, SessionStatus};
38use everruns_core::session_file::{InitialFile, SessionFile};
39use everruns_core::tools::ToolResultImage;
40use everruns_core::traits::{
41    AgentStore, EventEmitter, HarnessStore, ProviderStore, ResolvedModel, SessionMutator,
42    SessionStorageStore, SessionStore, UserConnectionResolver,
43};
44use everruns_core::turn::TurnStopReason;
45use everruns_core::typed_id::{AgentId, HarnessId, MessageId, OrgId, SessionId, TurnId};
46use everruns_core::{
47    AgentCapabilityConfig, CapabilityId, InputMessage, MessageRetriever, SessionFileSystem,
48    SessionFileSystemFactoryContext, plugin_capability_id, resolve_runtime_capabilities,
49};
50use everruns_engine::{ActOutcome, plan_after_act, plan_after_process_input, plan_after_reason};
51use sha2::{Digest, Sha256};
52use std::path::Path;
53use std::sync::Arc;
54
55/// Cap on the input length hashed by [`hash_public_org_id`].
56///
57/// Legitimate org public ids are `org_<32hex>` (36 bytes). Bounding the
58/// hashed prefix keeps worst-case cost predictable when an attacker-controlled
59/// session carries an oversize string.
60const HASH_INPUT_CAP_BYTES: usize = 128;
61
62/// Derive an internal `i64` org id from the public `org_<32hex>` form on a
63/// [`Session`].
64///
65/// Round-trip with [`everruns_core::org_public_id_from_internal`]: when the
66/// public id was produced by that helper (i.e. the upper bits are zero and
67/// the value fits in a positive `i64`), this returns the original internal
68/// id unchanged. Other values are mapped into `[2, i64::MAX]` by hashing the
69/// original public id string so runtime namespaces do not fail open to the
70/// shared default org and avoid arithmetic collision gadgets.
71///
72/// Exposed so embedders (e.g. `everruns-local`) can scope per-org stores to the
73/// same internal id the act path resolves a session's org to.
74pub fn in_process_internal_org_id(public_org_id: &str) -> i64 {
75    if public_org_id == everruns_core::DEFAULT_ORG_PUBLIC_ID {
76        return everruns_core::DEFAULT_ORG_ID;
77    }
78
79    let Ok(parsed) = public_org_id.parse::<OrgId>() else {
80        return hash_public_org_id(public_org_id);
81    };
82    let raw: u128 = parsed.uuid().as_u128();
83    if raw == 0 {
84        return hash_public_org_id(public_org_id);
85    }
86
87    // Synthetic ids from `org_public_id_from_internal(i64)` always fit here,
88    // so the in-process runtime sees the same `org_id` the server used.
89    if raw <= i64::MAX as u128 {
90        return raw as i64;
91    }
92
93    hash_public_org_id(public_org_id)
94}
95
96// Use SHA-256 with a fixed truncation scheme so the mapping is stable across
97// Rust/binary upgrades and predictable for any embedder. Input is bounded to
98// `HASH_INPUT_CAP_BYTES` so attacker-controlled oversize org strings cannot
99// drive unbounded hashing work.
100fn hash_public_org_id(public_org_id: &str) -> i64 {
101    let bytes = public_org_id.as_bytes();
102    let bounded = &bytes[..bytes.len().min(HASH_INPUT_CAP_BYTES)];
103    let digest = Sha256::digest(bounded);
104    let mut buf = [0u8; 8];
105    buf.copy_from_slice(&digest[..8]);
106    let raw = u64::from_be_bytes(buf);
107    ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
108}
109
110#[derive(Debug, Clone)]
111pub struct TurnResult {
112    /// Final text response produced by the turn.
113    pub response: String,
114    /// Number of reason iterations executed.
115    pub iterations: usize,
116    /// Total number of tool calls executed during the turn.
117    pub tool_calls_count: usize,
118    /// Whether the turn completed without an unrecoverable failure.
119    pub success: bool,
120    /// Failure message when `success` is false.
121    pub error: Option<String>,
122    /// Structured reason the turn stopped.
123    pub stop_reason: TurnStopReason,
124    /// Turn identifier used to correlate emitted events.
125    pub turn_id: everruns_core::typed_id::TurnId,
126}
127
128/// Result of changing the session-scoped capability set of a live runtime.
129///
130/// A changed result is the refresh seam for embedders: every subsequent reason
131/// or act boundary reassembles model and execution surfaces from the updated
132/// session overlay.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct CapabilityDelta {
135    /// Canonical capability id.
136    pub capability_id: String,
137    /// Whether the session overlay changed.
138    pub changed: bool,
139    /// Whether the capability is active after the operation.
140    pub active: bool,
141    /// Whether prompt, tool, hook, command, or MCP surfaces must be refreshed.
142    pub surfaces_dirty: bool,
143}
144
145/// Summarize a terminal engine plan into the public [`TurnResult`].
146///
147/// The engine reports the stop reason and the carried error; the host supplies
148/// the running summaries it accumulated while executing the planned steps. A
149/// carried error is the failure signal (it is exactly what the pre-engine state
150/// machine kept in `pending_error`), and a failed turn reports no response and
151/// no tool calls, matching the previous `TurnOutcome::Failed` mapping.
152fn finish_turn(
153    turn_id: everruns_core::typed_id::TurnId,
154    stop_reason: TurnStopReason,
155    error: Option<String>,
156    response: String,
157    iterations: usize,
158    tool_calls_count: usize,
159) -> TurnResult {
160    if error.is_some() {
161        return TurnResult {
162            response: String::new(),
163            iterations,
164            tool_calls_count: 0,
165            success: false,
166            error,
167            stop_reason,
168            turn_id,
169        };
170    }
171
172    TurnResult {
173        response,
174        iterations,
175        tool_calls_count,
176        success: true,
177        error: None,
178        stop_reason,
179        turn_id,
180    }
181}
182
183/// Builder for the public in-process runtime.
184///
185/// The builder owns a standalone runtime bundle:
186/// - `PlatformDefinition` for capabilities and drivers
187/// - in-memory stores for sessions, files, storage, memory, and messages
188/// - seeded harness/agent/session entities
189///
190/// `build()` returns an [`InProcessRuntime`] that can execute turns in-process
191/// without the durable engine or the control-plane server.
192pub struct InProcessRuntimeBuilder {
193    platform_definition: PlatformDefinition,
194    llm_sim_config: Option<LlmSimConfig>,
195    default_model: Option<ResolvedModel>,
196    backends: Option<RuntimeBackends>,
197    session_file_system_factory_context: SessionFileSystemFactoryContext,
198    harnesses: Vec<Harness>,
199    agents: Vec<Agent>,
200    sessions: Vec<Session>,
201    default_session_id: Option<SessionId>,
202    seeded_files: Vec<(SessionId, InitialFile)>,
203    mcp_auth_provider: Option<Arc<dyn everruns_mcp::McpAuthProvider>>,
204    provider_retry_config: Option<everruns_core::llm_retry::LlmRetryConfig>,
205    provider_stall_timeout: Option<std::time::Duration>,
206    /// Hydrated capability configs for plugins loaded via [`Self::with_plugin_dir`].
207    ///
208    /// Keyed by `plugin:{name}`. Agents and harnesses reference these by the
209    /// same `plugin:{name}` capability ref; the hydrated config carries the
210    /// compiled `DeclarativeCapabilityDefinition` so no registry entry is needed.
211    plugin_capability_configs: Vec<AgentCapabilityConfig>,
212    /// Non-fatal warnings collected during plugin compilation.
213    plugin_warnings: Vec<String>,
214}
215
216impl Default for InProcessRuntimeBuilder {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222impl InProcessRuntimeBuilder {
223    /// Create a builder with runtime-safe built-in capabilities and no implicit
224    /// LLM driver.
225    ///
226    /// Embedders must either:
227    /// - call [`Self::llm_sim`] for deterministic local examples/tests, or
228    /// - register their own driver(s) on the platform definition and set a
229    ///   default model via [`Self::default_model`].
230    pub fn new() -> Self {
231        Self {
232            platform_definition: PlatformDefinition::builder()
233                .capability_registry(CapabilityRegistry::runtime_builtins())
234                .driver_registry(DriverRegistry::new())
235                .session_file_system_factory(Arc::new(InMemorySessionFileSystemFactory))
236                .build(),
237            llm_sim_config: None,
238            default_model: None,
239            backends: None,
240            session_file_system_factory_context: SessionFileSystemFactoryContext::new(),
241            harnesses: Vec::new(),
242            agents: Vec::new(),
243            sessions: Vec::new(),
244            default_session_id: None,
245            seeded_files: Vec::new(),
246            mcp_auth_provider: None,
247            provider_retry_config: None,
248            provider_stall_timeout: None,
249            plugin_capability_configs: Vec::new(),
250            plugin_warnings: Vec::new(),
251        }
252    }
253
254    /// Set the auth provider used to acquire credentials for scoped MCP
255    /// servers (knowledge/integrations/runtime-mcp.md D3). Defaults to no credentials, suitable
256    /// for unauthenticated servers or servers carrying literal auth headers.
257    pub fn mcp_auth_provider(mut self, provider: Arc<dyn everruns_mcp::McpAuthProvider>) -> Self {
258        self.mcp_auth_provider = Some(provider);
259        self
260    }
261
262    /// Replace the platform definition used by the runtime.
263    pub fn platform_definition(mut self, platform_definition: PlatformDefinition) -> Self {
264        self.platform_definition = platform_definition;
265        self
266    }
267
268    /// Register an additional capability on the runtime platform.
269    pub fn capability<C: Capability + 'static>(mut self, capability: C) -> Self {
270        self.platform_definition
271            .capability_registry_mut()
272            .register(capability);
273        self
274    }
275
276    /// Replace the platform driver registry.
277    pub fn driver_registry(mut self, driver_registry: DriverRegistry) -> Self {
278        *self.platform_definition.driver_registry_mut() = driver_registry;
279        self
280    }
281
282    /// Register the built-in `llmsim` driver for deterministic local execution.
283    pub fn llm_sim(mut self, config: LlmSimConfig) -> Self {
284        self.llm_sim_config = Some(config);
285        self
286    }
287
288    /// Set the runtime default model used when sessions/agents do not override it.
289    pub fn default_model(mut self, model: ResolvedModel) -> Self {
290        self.default_model = Some(model);
291        self
292    }
293
294    /// Supply a custom backend bundle instead of the built-in in-memory stores.
295    pub fn backends(mut self, backends: RuntimeBackends) -> Self {
296        self.backends = Some(backends);
297        self
298    }
299
300    /// Inject a session-task registry. Convenience over `backends(...)` that
301    /// initializes an in-memory backend bundle on first use, so embedders can
302    /// add the registry without assembling a full `RuntimeBackends`.
303    pub fn with_session_task_registry(
304        mut self,
305        registry: Arc<dyn everruns_core::session_task::SessionTaskRegistry>,
306    ) -> Self {
307        let backends = self
308            .backends
309            .take()
310            .unwrap_or_else(RuntimeBackends::in_memory);
311        self.backends = Some(backends.with_session_task_registry(registry));
312        self
313    }
314
315    /// Inject a per-org schedule store factory (see [`RuntimeBackends`]).
316    pub fn with_schedule_store_factory(
317        mut self,
318        factory: crate::backends::ScheduleStoreFactory,
319    ) -> Self {
320        let backends = self
321            .backends
322            .take()
323            .unwrap_or_else(RuntimeBackends::in_memory);
324        self.backends = Some(backends.with_schedule_store_factory(factory));
325        self
326    }
327
328    /// Inject a per-(org, session) platform store factory (see [`RuntimeBackends`]).
329    pub fn with_platform_store_factory(
330        mut self,
331        factory: crate::backends::PlatformStoreFactory,
332    ) -> Self {
333        let backends = self
334            .backends
335            .take()
336            .unwrap_or_else(RuntimeBackends::in_memory);
337        self.backends = Some(backends.with_platform_store_factory(factory));
338        self
339    }
340
341    /// Supply host dependencies needed by the platform session filesystem factory.
342    pub fn session_file_system_factory_context(
343        mut self,
344        context: SessionFileSystemFactoryContext,
345    ) -> Self {
346        self.session_file_system_factory_context = context;
347        self
348    }
349
350    /// Override the bounded provider-recovery policy for this runtime.
351    pub fn provider_retry_config(
352        mut self,
353        config: everruns_core::llm_retry::LlmRetryConfig,
354    ) -> Self {
355        self.provider_retry_config = Some(config);
356        self
357    }
358
359    /// Override the no-output provider stream stall timeout.
360    pub fn provider_stall_timeout(mut self, timeout: std::time::Duration) -> Self {
361        self.provider_stall_timeout = Some(timeout);
362        self
363    }
364
365    /// Seed a harness into the runtime store.
366    pub fn harness(mut self, harness: Harness) -> Self {
367        self.harnesses.push(harness);
368        self
369    }
370
371    /// Seed an agent into the runtime store.
372    pub fn agent(mut self, agent: Agent) -> Self {
373        self.agents.push(agent);
374        self
375    }
376
377    /// Seed a session into the runtime store.
378    pub fn session(mut self, session: Session) -> Self {
379        self.sessions.push(session);
380        self
381    }
382
383    /// Seed one harness, one agent, and one session with a compact sub-builder.
384    ///
385    /// The generated session id is exposed from the built runtime via
386    /// [`InProcessRuntime::default_session_id`].
387    pub fn single_session<F>(mut self, configure: F) -> Self
388    where
389        F: FnOnce(SingleSessionBuilder) -> SingleSessionBuilder,
390    {
391        let (harness, agent, session, session_id) =
392            configure(SingleSessionBuilder::default()).build();
393        self.harnesses.push(harness);
394        self.agents.push(agent);
395        self.sessions.push(session);
396        self.default_session_id = Some(session_id);
397        self
398    }
399
400    /// Seed an additional text file directly into a session workspace.
401    ///
402    /// This is applied after harness/agent/session `initial_files` are merged.
403    pub fn seed_text_file(
404        mut self,
405        session_id: SessionId,
406        path: impl Into<String>,
407        content: impl Into<String>,
408    ) -> Self {
409        self.seeded_files.push((
410            session_id,
411            InitialFile {
412                path: path.into(),
413                content: content.into(),
414                encoding: "text".to_string(),
415                is_readonly: false,
416            },
417        ));
418        self
419    }
420
421    /// Load a plugin from a local directory and make it available as a
422    /// `plugin:{name}` capability.
423    ///
424    /// Reads the plugin directory via [`PluginFileSet::from_dir`] and compiles
425    /// it with [`compile_plugin`] at call time. A compilation failure is
426    /// surfaced immediately as a configuration error so the problem is visible
427    /// before the runtime is built. Non-fatal compilation warnings are logged
428    /// via `tracing::warn!` and also collected so they can be inspected on the
429    /// built runtime via [`InProcessRuntime::plugin_warnings`].
430    ///
431    /// After loading, agents and harnesses can reference the plugin by its
432    /// `plugin:{name}` capability ref. The hydrated config carries the compiled
433    /// `DeclarativeCapabilityDefinition`, which the core capability resolution
434    /// path recognises without a registry entry (same path as declarative
435    /// capabilities).
436    ///
437    /// When using [`Self::single_session`], call
438    /// [`SingleSessionBuilder::agent_plugin`] to add the capability ref to the
439    /// seeded agent, or use [`AgentBuilder::capability`] / `with_capability`
440    /// directly.
441    pub fn with_plugin_dir(mut self, path: &Path) -> Result<Self> {
442        let file_set = PluginFileSet::from_dir(path)
443            .map_err(|e| AgentLoopError::config(format!("plugin directory load failed: {e}")))?;
444        let compiled = compile_plugin(&file_set)
445            .map_err(|e| AgentLoopError::config(format!("plugin compilation failed: {e}")))?;
446
447        for warning in &compiled.warnings {
448            tracing::warn!(plugin = %compiled.definition.name, warning = %warning, "plugin compile warning");
449        }
450        self.plugin_warnings.extend(compiled.warnings);
451
452        let cap_id = plugin_capability_id(&compiled.definition.name);
453        let hydrated_config = serde_json::to_value(&compiled.definition)
454            .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
455        self.plugin_capability_configs
456            .push(AgentCapabilityConfig::with_config(cap_id, hydrated_config));
457
458        Ok(self)
459    }
460
461    /// Return a hydrated `AgentCapabilityConfig` for a previously loaded plugin.
462    ///
463    /// Returns `None` when no plugin with that name was loaded via
464    /// [`Self::with_plugin_dir`]. Primarily used by callers that need the
465    /// hydrated config to seed it onto a harness or agent before building.
466    pub fn plugin_capability(&self, name: &str) -> Option<AgentCapabilityConfig> {
467        let cap_id = plugin_capability_id(name);
468        self.plugin_capability_configs
469            .iter()
470            .find(|c| c.capability_id() == cap_id)
471            .cloned()
472    }
473
474    /// Build the in-process runtime.
475    ///
476    /// Returns a configuration error when no default model is available after
477    /// applying explicit configuration and any requested `llmsim` setup.
478    pub async fn build(mut self) -> Result<InProcessRuntime> {
479        let backends = match self.backends.take() {
480            Some(backends) => backends,
481            None => RuntimeBackends::in_memory(),
482        };
483        let file_store = resolve_session_file_system(
484            &self.platform_definition,
485            self.session_file_system_factory_context.clone(),
486        )
487        .await?;
488
489        if let Some(config) = self.llm_sim_config.take() {
490            let driver = LlmSimDriver::new(config);
491            // Replace intentionally: the platform may already register a
492            // built-in LlmSim driver, and the builder's config takes precedence.
493            self.platform_definition
494                .driver_registry_mut()
495                .register_or_replace(DriverId::LlmSim, move |_config| Box::new(driver.clone()));
496
497            if self.default_model.is_none() {
498                self.default_model = Some(ResolvedModel {
499                    model: "llmsim-model".to_string(),
500                    provider_type: DriverId::LlmSim,
501                    api_key: Some("fake-key".to_string()),
502                    base_url: None,
503                    provider_metadata: None,
504                });
505            }
506        }
507
508        let default_model = self.default_model.ok_or_else(|| {
509            AgentLoopError::config(
510                "in-process runtime requires a default model; call \
511                 InProcessRuntimeBuilder::default_model(...) or \
512                 InProcessRuntimeBuilder::llm_sim(...)",
513            )
514        })?;
515
516        backends
517            .provider_store
518            .set_default_model(default_model)
519            .await?;
520
521        // Hydrate bare plugin: refs in harnesses/agents/sessions with the
522        // compiled definition config so the capability resolution path can
523        // deserialise them without a registry entry (same path as declarative:).
524        for harness in &mut self.harnesses {
525            hydrate_plugin_refs(&mut harness.capabilities, &self.plugin_capability_configs);
526        }
527        for agent in &mut self.agents {
528            hydrate_plugin_refs(&mut agent.capabilities, &self.plugin_capability_configs);
529        }
530        for session in &mut self.sessions {
531            hydrate_plugin_refs(&mut session.capabilities, &self.plugin_capability_configs);
532        }
533
534        for harness in &self.harnesses {
535            backends.harness_store.add_harness(harness.clone()).await?;
536        }
537        for agent in &self.agents {
538            backends.agent_store.add_agent(agent.clone()).await?;
539        }
540        for session in &self.sessions {
541            backends.session_store.add_session(session.clone()).await?;
542        }
543
544        for session in &self.sessions {
545            seed_runtime_initial_files(
546                backends.harness_store.as_ref(),
547                backends.agent_store.as_ref(),
548                file_store.as_ref(),
549                session,
550            )
551            .await?;
552        }
553
554        for (session_id, file) in &self.seeded_files {
555            file_store.seed_initial_file(*session_id, file).await?;
556        }
557
558        let persisting_emitter =
559            PersistingEventEmitter::new(backends.event_bus.clone(), backends.message_store.clone());
560
561        // Mid-turn wake delivery (EVE-681, part A): when a task registry is
562        // present, wrap it so qualifying task transitions fan out to a
563        // per-session `SessionWakeQueue`. The turn loop drains that queue at
564        // each reason iteration boundary. Without a registry there is no
565        // background work to wake on, so the queue is left absent (inert).
566        let (session_task_registry, session_wake_queue) = match backends.session_task_registry {
567            Some(inner) => {
568                let wake_queue = Arc::new(everruns_core::SessionWakeQueue::new());
569                let observing = everruns_core::ObservingTaskRegistry::new(inner)
570                    .with_observer(wake_queue.clone());
571                let wrapped: Arc<dyn everruns_core::session_task::SessionTaskRegistry> =
572                    Arc::new(observing);
573                (Some(wrapped), Some(wake_queue))
574            }
575            None => (None, None),
576        };
577
578        Ok(InProcessRuntime {
579            platform_definition: Arc::new(self.platform_definition),
580            harness_store: backends.harness_store,
581            agent_store: backends.agent_store,
582            session_store: backends.session_store,
583            default_session_id: self.default_session_id,
584            message_store: backends.message_store,
585            compaction_checkpoint_store: backends.compaction_checkpoint_store,
586            provider_store: backends.provider_store,
587            event_bus: backends.event_bus,
588            persisting_emitter,
589            file_store,
590            storage_store: backends.storage_store,
591            connection_resolver: backends.connection_resolver,
592            session_task_registry,
593            session_wake_queue,
594            schedule_store_factory: backends.schedule_store_factory,
595            platform_store_factory: backends.platform_store_factory,
596            mcp_auth_provider: self
597                .mcp_auth_provider
598                .unwrap_or_else(|| Arc::new(everruns_mcp::NoAuthProvider)),
599            provider_retry_config: self.provider_retry_config,
600            provider_stall_timeout: self.provider_stall_timeout,
601            mcp_discovery_cache: Arc::new(crate::mcp_cache::McpDiscoveryCache::new()),
602            plugin_warnings: self.plugin_warnings,
603        })
604    }
605}
606
607async fn resolve_session_file_system(
608    platform_definition: &PlatformDefinition,
609    file_system_factory_context: SessionFileSystemFactoryContext,
610) -> Result<Arc<dyn SessionFileSystem>> {
611    let file_system_factory = platform_definition.session_file_system_factory();
612    if file_system_factory.is_disabled() {
613        Ok(Arc::new(InMemorySessionFileStore::new()))
614    } else {
615        Ok(file_system_factory
616            .create_session_file_system(file_system_factory_context)
617            .await?)
618    }
619}
620
621#[derive(Clone)]
622/// Public in-process runtime backed by either in-memory or custom stores.
623///
624/// This runtime is intended for embedders who want to execute Everruns
625/// harnesses inside their own process while controlling capabilities,
626/// harness definitions, and driver registrations directly in Rust.
627pub struct InProcessRuntime {
628    platform_definition: Arc<PlatformDefinition>,
629    harness_store: Arc<dyn RuntimeHarnessStore>,
630    agent_store: Arc<dyn RuntimeAgentStore>,
631    session_store: Arc<dyn RuntimeSessionStore>,
632    default_session_id: Option<SessionId>,
633    message_store: Arc<dyn RuntimeMessageStore>,
634    compaction_checkpoint_store: Arc<dyn everruns_core::CompactionCheckpointStore>,
635    provider_store: Arc<dyn RuntimeProviderStore>,
636    event_bus: Arc<dyn EventBus>,
637    persisting_emitter: PersistingEventEmitter,
638    file_store: Arc<dyn SessionFileSystem>,
639    storage_store: Arc<dyn SessionStorageStore>,
640    connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
641    session_task_registry: Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>>,
642    /// Mid-turn wake queue fed by `session_task_registry` transitions and
643    /// drained at each reason iteration boundary (EVE-681, part A). Present iff
644    /// a task registry was configured.
645    session_wake_queue: Option<Arc<everruns_core::SessionWakeQueue>>,
646    schedule_store_factory: Option<crate::backends::ScheduleStoreFactory>,
647    platform_store_factory: Option<crate::backends::PlatformStoreFactory>,
648    mcp_auth_provider: Arc<dyn everruns_mcp::McpAuthProvider>,
649    provider_retry_config: Option<everruns_core::llm_retry::LlmRetryConfig>,
650    provider_stall_timeout: Option<std::time::Duration>,
651    mcp_discovery_cache: Arc<crate::mcp_cache::McpDiscoveryCache>,
652    /// Non-fatal warnings collected during plugin compilation (see
653    /// [`InProcessRuntimeBuilder::with_plugin_dir`]).
654    plugin_warnings: Vec<String>,
655}
656
657impl InProcessRuntime {
658    /// Build the shared MCP client over the platform egress boundary and the
659    /// configured auth provider.
660    fn mcp_client(&self) -> Arc<everruns_mcp::McpClient> {
661        Arc::new(everruns_mcp::McpClient::new(
662            self.platform_definition.egress_service(),
663            self.mcp_auth_provider.clone(),
664        ))
665    }
666
667    /// Resolve the effective scoped MCP servers for a session.
668    async fn session_mcp_servers(
669        &self,
670        session: &Session,
671        agent: Option<&Agent>,
672    ) -> everruns_core::ScopedMcpServers {
673        let harness_chain = self
674            .harness_store
675            .get_harness_chain(session.harness_id)
676            .await
677            .unwrap_or_default();
678        let resolved = resolve_runtime_capabilities(
679            &harness_chain,
680            agent,
681            session,
682            self.platform_definition.capability_registry(),
683        );
684        let contributed = collect_capability_mcp_servers(
685            &resolved.resolved_capability_configs,
686            self.platform_definition.capability_registry(),
687        );
688        let explicit = crate::mcp::merge_session_scoped_servers(&harness_chain, agent, session);
689        everruns_core::merge_scoped_mcp_servers(&contributed, &explicit)
690    }
691    /// Create a builder for the in-process runtime.
692    pub fn builder() -> InProcessRuntimeBuilder {
693        InProcessRuntimeBuilder::new()
694    }
695
696    /// Return the default session id seeded by
697    /// [`InProcessRuntimeBuilder::single_session`], if one was configured.
698    pub fn default_session_id(&self) -> Option<SessionId> {
699        self.default_session_id
700    }
701
702    /// Return non-fatal warnings collected during plugin compilation.
703    ///
704    /// Warnings are also emitted at `tracing::warn!` level when
705    /// [`InProcessRuntimeBuilder::with_plugin_dir`] is called.
706    pub fn plugin_warnings(&self) -> &[String] {
707        &self.plugin_warnings
708    }
709
710    /// Activate a registered capability on a running session.
711    ///
712    /// The capability is validated and dependency-resolved before the session
713    /// overlay changes. Conversation history and the session identity are
714    /// untouched. Re-activating an already-effective capability is a no-op.
715    pub async fn activate_capability(
716        &self,
717        session_id: SessionId,
718        capability: impl Into<AgentCapabilityConfig>,
719    ) -> Result<CapabilityDelta> {
720        let mut capability = capability.into();
721        let registry = self.platform_definition.capability_registry();
722        let registered = registry.get(capability.capability_id()).ok_or_else(|| {
723            AgentLoopError::config(format!(
724                "unknown capability: {}",
725                capability.capability_id()
726            ))
727        })?;
728        if registered.status() != CapabilityStatus::Available {
729            return Err(AgentLoopError::config(format!(
730                "capability is not available: {}",
731                capability.capability_id()
732            )));
733        }
734        registered
735            .validate_config(&capability.config)
736            .map_err(|error| {
737                AgentLoopError::config(format!("invalid capability config: {error}"))
738            })?;
739
740        let canonical_id = registered.id().to_string();
741        capability.capability_ref = CapabilityId::new(canonical_id.clone());
742        let context = self.load_context(session_id).await?;
743        if context
744            .resolved_capability_configs
745            .iter()
746            .any(|config| config.capability_id() == canonical_id)
747        {
748            return Ok(CapabilityDelta {
749                capability_id: canonical_id,
750                changed: false,
751                active: true,
752                surfaces_dirty: false,
753            });
754        }
755
756        let mut candidate = context.effective_overlay.capabilities;
757        candidate.push(capability.clone());
758        resolve_capability_configs(&candidate, registry)
759            .map_err(|error| AgentLoopError::config(error.to_string()))?;
760
761        self.session_store
762            .upsert_session_capability(session_id, capability)
763            .await?;
764        self.mcp_discovery_cache
765            .invalidate_session(session_id.uuid());
766        Ok(CapabilityDelta {
767            capability_id: canonical_id,
768            changed: true,
769            active: true,
770            surfaces_dirty: true,
771        })
772    }
773
774    /// Deactivate a capability previously activated on this running session.
775    ///
776    /// Capabilities inherited from the agent or harness cannot be removed by a
777    /// session-scoped operation; callers must change their owning layer.
778    pub async fn deactivate_capability(
779        &self,
780        session_id: SessionId,
781        capability_id: &str,
782    ) -> Result<CapabilityDelta> {
783        let registry = self.platform_definition.capability_registry();
784        let registered = registry.get(capability_id).ok_or_else(|| {
785            AgentLoopError::config(format!("unknown capability: {capability_id}"))
786        })?;
787        let canonical_id = registered.id().to_string();
788        let context = self.load_context(session_id).await?;
789        if !context
790            .resolved_capability_configs
791            .iter()
792            .any(|config| config.capability_id() == canonical_id)
793        {
794            return Ok(CapabilityDelta {
795                capability_id: canonical_id,
796                changed: false,
797                active: false,
798                surfaces_dirty: false,
799            });
800        }
801
802        let session_capability_id = context
803            .session
804            .capabilities
805            .iter()
806            .find(|config| {
807                registry
808                    .get(config.capability_id())
809                    .is_some_and(|capability| capability.id() == canonical_id)
810            })
811            .map(|config| config.capability_id().to_string())
812            .ok_or_else(|| {
813                AgentLoopError::config(format!(
814                    "capability {canonical_id} is inherited and cannot be deactivated at the session layer"
815                ))
816            })?;
817
818        self.session_store
819            .remove_session_capability(session_id, &session_capability_id)
820            .await?;
821        self.mcp_discovery_cache
822            .invalidate_session(session_id.uuid());
823        Ok(CapabilityDelta {
824            capability_id: canonical_id,
825            changed: true,
826            active: false,
827            surfaces_dirty: true,
828        })
829    }
830
831    /// Execute one turn for an existing session.
832    ///
833    /// The input message is stored in the runtime history, an `input.message`
834    /// event is emitted, and the turn then runs `input -> reason -> act` as
835    /// planned step-by-step by [`everruns_engine`] — the same planner the
836    /// durable worker drives its turns with.
837    pub async fn run_turn(
838        &self,
839        session_id: SessionId,
840        input: impl Into<InputMessage>,
841    ) -> Result<TurnResult> {
842        let session = self
843            .session_store
844            .get_session(session_id)
845            .await?
846            .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
847
848        // Input message is recorded directly (and emitted via the raw bus so
849        // that PersistingEventEmitter does not double-store it). All
850        // subsequent activity-emitted events flow through the persisting
851        // emitter the adapter hands out.
852        let input_message = self
853            .message_store
854            .add_input_message(session_id, input.into())
855            .await?;
856        self.event_bus
857            .emit(EventRequest::new(
858                session_id,
859                EventContext::empty(),
860                InputMessageData::new(input_message.clone()),
861            ))
862            .await?;
863
864        let org_id = in_process_internal_org_id(&session.organization_id);
865        let turn_id = TurnId::new();
866
867        // Engine-planned turn loop (EVE-842). Every reason-vs-act-vs-complete
868        // decision comes from `everruns-engine`; this loop only executes the
869        // host operation each plan names and performs the lifecycle effects the
870        // engine returns as data. There is no second copy of the planning brain
871        // in the runtime.
872        let mut state = RuntimeTurnState {
873            org_id,
874            session_id,
875            harness_id: session.harness_id,
876            agent_id: session.agent_id,
877            input_message_id: input_message.id,
878            turn_id: None,
879            previous_response_id: None,
880            iteration: 1,
881            request_id: None,
882            started_at: None,
883            cumulative_usage: None,
884            tool_call_count: 0,
885            llm_call_count: 0,
886            time_to_first_token_ms: None,
887            final_message_id: None,
888            final_answer_preview: None,
889        };
890
891        let base_context = |exec: bool| {
892            let context = AtomContext::new(session_id, turn_id, input_message.id)
893                .with_workspace_id(session.workspace_id);
894            if exec { context.next_exec() } else { context }
895        };
896
897        // `process_input` is the turn's fixed entry step: the durable host
898        // enqueues it before any planning, and it is what mints the turn id the
899        // planner then carries.
900        execute_input_activity(
901            self,
902            org_id,
903            InputAtomInput {
904                context: base_context(false),
905            },
906        )
907        .await?;
908        let mut plan = plan_after_process_input(&state, Some(turn_id), Utc::now());
909
910        // Host-side bookkeeping for the returned `TurnResult`. These are
911        // summaries of what the host executed, not inputs to any decision.
912        let mut iterations: usize = 0;
913        let mut tool_calls_count: usize = 0;
914        let mut last_response = String::new();
915
916        loop {
917            match plan {
918                RuntimeTurnPlan::ScheduleReason(next_state) => {
919                    state = next_state;
920                    // Iteration boundary: drain queued task wakes and inject
921                    // them before the LLM call so this reason reacts to them
922                    // (EVE-681, part A). Draining here also delivers wakes that
923                    // arrived while the session was idle, on the next turn's
924                    // first iteration (between-turn fallback).
925                    let wake_message_ids = self.drain_and_inject_wakes(session_id).await?;
926                    let mut prompt_message_ids = wake_message_ids;
927                    if state.iteration == 1 {
928                        prompt_message_ids.insert(0, input_message.id);
929                    }
930                    let reason_result = execute_reason_activity_with_prompt_messages(
931                        self,
932                        org_id,
933                        ReasonInput {
934                            context: base_context(true),
935                            harness_id: session.harness_id,
936                            agent_id: session.agent_id,
937                            org_id,
938                            mcp_tool_definitions: vec![],
939                            previous_response_id: state.previous_response_id.clone(),
940                            iteration: state.iteration,
941                        },
942                        prompt_message_ids,
943                    )
944                    .await?;
945
946                    iterations += 1;
947                    if !reason_result.text.is_empty() {
948                        last_response = reason_result.text.clone();
949                    }
950
951                    // If a wake landed during this reason (e.g. a background
952                    // task settling on another task), continue a would-idle turn
953                    // so it is delivered on the very next iteration rather than
954                    // after the session idles. The engine reads this as the
955                    // steering-message count the durable host reports.
956                    let pending_user_message_count = usize::from(
957                        self.session_wake_queue
958                            .as_ref()
959                            .is_some_and(|q| q.has_pending(session_id)),
960                    );
961
962                    let act_scheduling =
963                        crate::turn_strategy::resolve_act_scheduling(self, &state, &reason_result)
964                            .await?;
965                    let (next, effects) = plan_after_reason(
966                        &state,
967                        reason_result,
968                        pending_user_message_count,
969                        Utc::now(),
970                        act_scheduling,
971                    );
972                    crate::turn_strategy::perform_effects(self, org_id, session_id, effects).await;
973                    plan = next;
974                }
975                RuntimeTurnPlan::ScheduleAct(act_plan) => {
976                    tool_calls_count += act_plan.input.tool_calls.len();
977                    // Same resume contract the durable host applies when it
978                    // dequeues the act task: the plan's response id / iteration
979                    // / request id override the carried state.
980                    state = *act_plan.resume_state;
981                    state.previous_response_id = act_plan.previous_response_id;
982                    state.iteration = act_plan.iteration;
983                    state.request_id = act_plan.request_id;
984
985                    let act_result = execute_act_activity(self, act_plan.input).await?;
986                    let outcome = ActOutcome {
987                        blocked: act_result.blocked,
988                        waiting_for_tool_results: act_result.waiting_for_tool_results,
989                    };
990                    let setup_connection_hint_enabled =
991                        crate::turn_strategy::resolve_setup_connection_hint(
992                            self, org_id, session_id, outcome,
993                        )
994                        .await;
995                    let (next, effects) =
996                        plan_after_act(&state, outcome, setup_connection_hint_enabled);
997                    crate::turn_strategy::perform_effects(self, org_id, session_id, effects).await;
998                    plan = next;
999                }
1000                RuntimeTurnPlan::Complete { stop_reason, error } => {
1001                    return Ok(finish_turn(
1002                        turn_id,
1003                        stop_reason,
1004                        error,
1005                        last_response,
1006                        iterations,
1007                        tool_calls_count,
1008                    ));
1009                }
1010                // The in-process runtime has no external tool-result delivery
1011                // path, so a pause resolves the turn here. The session has
1012                // already been marked `waiting_for_tool_results` by the effect.
1013                RuntimeTurnPlan::WaitForToolResults { .. } => {
1014                    return Ok(finish_turn(
1015                        turn_id,
1016                        TurnStopReason::EndTurn,
1017                        None,
1018                        last_response,
1019                        iterations,
1020                        tool_calls_count,
1021                    ));
1022                }
1023            }
1024        }
1025    }
1026
1027    pub async fn run_text_turn(
1028        &self,
1029        session_id: SessionId,
1030        text: impl Into<String>,
1031    ) -> Result<TurnResult> {
1032        self.run_turn(session_id, InputMessage::user(text)).await
1033    }
1034
1035    /// Drain any queued task wakes for `session_id` and inject them into the
1036    /// conversation as user messages so the next reason reacts to them
1037    /// (EVE-681, part A). Returns the ids of the injected messages so prompt
1038    /// hooks can inspect them before the next provider call.
1039    ///
1040    /// Called at the top of every reason iteration — before the LLM call — so a
1041    /// task completion that landed during the previous act (or while idle) is
1042    /// visible to the very next iteration. `SessionWakeQueue::drain` is the
1043    /// exactly-once claim point: a drained wake is removed and never delivered
1044    /// twice, so a wake is delivered mid-turn XOR on the next turn's first
1045    /// drain, never both.
1046    async fn drain_and_inject_wakes(&self, session_id: SessionId) -> Result<Vec<MessageId>> {
1047        let Some(queue) = &self.session_wake_queue else {
1048            return Ok(vec![]);
1049        };
1050        let wakes = queue.drain(session_id);
1051        if wakes.is_empty() {
1052            return Ok(vec![]);
1053        }
1054        let mut message_ids = Vec::with_capacity(wakes.len());
1055        for wake in wakes {
1056            // Persist the wake as a user message (history reload picks it up)
1057            // and emit the input event on the raw bus so it appears in the turn
1058            // span without the persisting emitter double-storing it — mirroring
1059            // how `run_turn` records the initial input message.
1060            let message = self
1061                .message_store
1062                .add_input_message(session_id, InputMessage::user(wake.text))
1063                .await?;
1064            message_ids.push(message.id);
1065            self.event_bus
1066                .emit(EventRequest::new(
1067                    session_id,
1068                    EventContext::empty(),
1069                    InputMessageData::new(message),
1070                ))
1071                .await?;
1072        }
1073        Ok(message_ids)
1074    }
1075
1076    /// Load the current message history for a session.
1077    pub async fn messages(&self, session_id: SessionId) -> Result<Vec<Message>> {
1078        self.message_store.load(session_id).await
1079    }
1080
1081    /// Read a file from the in-memory session filesystem.
1082    pub async fn read_file(
1083        &self,
1084        session_id: SessionId,
1085        path: &str,
1086    ) -> Result<Option<SessionFile>> {
1087        self.file_store.read_file(session_id, path).await
1088    }
1089
1090    /// Assemble the current runtime context for a session without executing a turn.
1091    pub async fn load_context(&self, session_id: SessionId) -> Result<AssembledTurnContext> {
1092        let session = self
1093            .session_store
1094            .get_session(session_id)
1095            .await?
1096            .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1097        self.inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
1098            .await
1099    }
1100
1101    /// Return all collected events from the runtime event bus.
1102    ///
1103    /// Event buses that do not retain events return an empty `Vec` (see
1104    /// [`EventBus::collected_events`]).
1105    pub async fn events(&self) -> Result<Vec<Event>> {
1106        Ok(self.event_bus.collected_events().await)
1107    }
1108
1109    /// Execute a system command declared by a registered capability.
1110    ///
1111    /// Looks up the first capability whose `commands()` includes the named
1112    /// command (in capability-resolution order) and delegates to its
1113    /// `execute_command`. Returns an error if no capability declares the
1114    /// requested name. The coding-CLI example uses this for `/model`
1115    /// (provided by `ModelSwitcherCapability`) so the dispatch path stays
1116    /// inside the capability instead of the TUI's local `handle_command`
1117    /// branches.
1118    pub async fn execute_command(
1119        &self,
1120        session_id: SessionId,
1121        request: everruns_core::command::ExecuteCommandRequest,
1122    ) -> Result<everruns_core::command::CommandResult> {
1123        let ctx = self.load_context(session_id).await?;
1124        let registry = self.platform_definition.capability_registry();
1125        // Context-aware commands (e.g. /btw) get the same store-backed host
1126        // facilities the server provides; the already-assembled context seeds
1127        // the host so dispatch and execution assemble it once.
1128        let host = everruns_core::command_host::StoreCommandHost::new(
1129            session_id,
1130            self.harness_store.clone(),
1131            self.agent_store.clone(),
1132            self.session_store.clone(),
1133            self.message_store.clone(),
1134            self.provider_store.clone(),
1135            registry.clone(),
1136            self.platform_definition.driver_registry().clone(),
1137        )
1138        .with_file_store(self.file_store.clone())
1139        .with_assembled_context(ctx.clone());
1140        let exec_ctx =
1141            everruns_core::command::CommandExecutionContext::new(session_id, Arc::new(host));
1142        for config in &ctx.resolved_capability_configs {
1143            let Some(capability) = registry.get(config.capability_id()) else {
1144                continue;
1145            };
1146            if capability.commands().iter().any(|c| c.name == request.name) {
1147                return capability.execute_command(&request, &exec_ctx).await;
1148            }
1149        }
1150        Err(AgentLoopError::config(format!(
1151            "no capability declares command /{}",
1152            request.name
1153        )))
1154    }
1155
1156    /// List slash commands available for a session.
1157    ///
1158    /// Resolves the session's harness/agent capability chain and aggregates
1159    /// commands declared via [`Capability::commands`], deduplicated by name
1160    /// (first occurrence wins, matching the order of resolved capabilities).
1161    /// This is the embedded equivalent of the server's
1162    /// `GET /v1/sessions/{id}/commands` system-commands list — skill
1163    /// commands are not included here because skills are discovered via the
1164    /// platform filesystem rather than the capability registry.
1165    pub async fn list_commands(
1166        &self,
1167        session_id: SessionId,
1168    ) -> Result<Vec<everruns_core::command::CommandDescriptor>> {
1169        let ctx = self.load_context(session_id).await?;
1170        let registry = self.platform_definition.capability_registry();
1171        let mut seen = std::collections::HashSet::new();
1172        let mut commands = Vec::new();
1173        for config in &ctx.resolved_capability_configs {
1174            let Some(capability) = registry.get(config.capability_id()) else {
1175                continue;
1176            };
1177            for command in capability.commands() {
1178                if seen.insert(command.name.clone()) {
1179                    commands.push(command);
1180                }
1181            }
1182        }
1183        Ok(commands)
1184    }
1185
1186    async fn inspect_context_with_ids(
1187        &self,
1188        session_id: SessionId,
1189        harness_id: everruns_core::HarnessId,
1190        agent_id: Option<AgentId>,
1191    ) -> Result<AssembledTurnContext> {
1192        inspect_turn_context(
1193            self.harness_store.as_ref(),
1194            self.agent_store.as_ref(),
1195            self.session_store.as_ref(),
1196            self.message_store.as_ref(),
1197            self.provider_store.as_ref(),
1198            self.platform_definition.capability_registry(),
1199            session_id,
1200            harness_id,
1201            agent_id,
1202            &[],
1203            Some(self.file_store.clone()),
1204        )
1205        .await
1206    }
1207}
1208
1209#[async_trait]
1210impl RuntimeHostAdapter for InProcessRuntime {
1211    async fn get_agent(&self, _org_id: i64, agent_id: AgentId) -> Result<Option<Agent>> {
1212        self.agent_store.get_agent(agent_id).await
1213    }
1214
1215    async fn get_harness(&self, _org_id: i64, harness_id: HarnessId) -> Result<Option<Harness>> {
1216        let chain = self.harness_store.get_harness_chain(harness_id).await?;
1217        Ok(chain.into_iter().last())
1218    }
1219
1220    async fn set_session_status(
1221        &self,
1222        _org_id: i64,
1223        session_id: SessionId,
1224        _status: SessionStatus,
1225    ) -> Result<Session> {
1226        // The in-process runtime does not persist status. Lifecycle callers
1227        // still emit their events; downstream consumers in-process don't
1228        // observe session.status.
1229        self.session_store
1230            .get_session(session_id)
1231            .await?
1232            .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))
1233    }
1234
1235    async fn load_turn_context(
1236        &self,
1237        _org_id: i64,
1238        session_id: SessionId,
1239    ) -> Result<RuntimeHostTurnContext> {
1240        let mut session = self
1241            .session_store
1242            .get_session(session_id)
1243            .await?
1244            .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1245        // Fold runtime ARD attachments into the session config layer before
1246        // scoped MCP servers / capabilities are resolved (resource_discovery).
1247        everruns_core::ard_attachment::apply_session_attachments(
1248            self.storage_store.as_ref(),
1249            &mut session,
1250        )
1251        .await;
1252        let agent = match session.agent_id {
1253            Some(agent_id) => self.agent_store.get_agent(agent_id).await?,
1254            None => None,
1255        };
1256        let messages = self.message_store.load(session_id).await?;
1257        let model = self.provider_store.get_default_model().await?;
1258
1259        // Discover tools from the session's scoped MCP servers so they appear
1260        // to the LLM alongside built-in tools (knowledge/integrations/runtime-mcp.md D4).
1261        let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1262        let mcp_tool_definitions = if scoped_servers.is_empty() {
1263            vec![]
1264        } else {
1265            crate::mcp::discover_tool_definitions(
1266                &self.mcp_discovery_cache,
1267                self.mcp_client(),
1268                session_id.uuid(),
1269                &scoped_servers,
1270            )
1271            .await
1272        };
1273
1274        Ok(RuntimeHostTurnContext {
1275            agent,
1276            session,
1277            messages,
1278            model,
1279            mcp_tool_definitions,
1280        })
1281    }
1282
1283    async fn mcp_executor(
1284        &self,
1285        _org_id: i64,
1286        session_id: SessionId,
1287    ) -> Option<Arc<everruns_mcp::McpExecutor>> {
1288        let session = self.session_store.get_session(session_id).await.ok()??;
1289        let agent = match session.agent_id {
1290            Some(agent_id) => self.agent_store.get_agent(agent_id).await.ok().flatten(),
1291            None => None,
1292        };
1293        let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1294        crate::mcp::build_executor(self.mcp_client(), &scoped_servers)
1295    }
1296
1297    fn capability_registry(&self) -> CapabilityRegistry {
1298        self.platform_definition.capability_registry().clone()
1299    }
1300
1301    fn driver_registry(&self) -> DriverRegistry {
1302        self.platform_definition.driver_registry().clone()
1303    }
1304
1305    fn harness_store(&self, _org_id: i64) -> Arc<dyn HarnessStore> {
1306        self.harness_store.clone()
1307    }
1308
1309    fn agent_store(&self, _org_id: i64) -> Arc<dyn AgentStore> {
1310        self.agent_store.clone()
1311    }
1312
1313    fn session_store(&self, _org_id: i64) -> Arc<dyn SessionStore> {
1314        self.session_store.clone()
1315    }
1316
1317    fn session_mutator(&self, _org_id: i64) -> Arc<dyn SessionMutator> {
1318        self.session_store.clone()
1319    }
1320
1321    fn provider_store(&self, _org_id: i64) -> Arc<dyn ProviderStore> {
1322        self.provider_store.clone()
1323    }
1324
1325    fn message_store(&self) -> Arc<dyn MessageRetriever> {
1326        self.message_store.clone()
1327    }
1328
1329    fn compaction_checkpoint_store(
1330        &self,
1331    ) -> Option<Arc<dyn everruns_core::CompactionCheckpointStore>> {
1332        Some(self.compaction_checkpoint_store.clone())
1333    }
1334
1335    fn event_emitter(&self) -> Arc<dyn EventEmitter> {
1336        Arc::new(self.persisting_emitter.clone())
1337    }
1338
1339    fn file_store(&self) -> Arc<dyn SessionFileSystem> {
1340        self.file_store.clone()
1341    }
1342
1343    fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
1344        Some(self.storage_store.clone())
1345    }
1346
1347    fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
1348        self.connection_resolver.clone()
1349    }
1350
1351    fn session_task_registry(
1352        &self,
1353    ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
1354        self.session_task_registry.clone()
1355    }
1356
1357    fn schedule_store(
1358        &self,
1359        org_id: i64,
1360    ) -> Option<Arc<dyn everruns_core::traits::SessionScheduleStore>> {
1361        self.schedule_store_factory
1362            .as_ref()
1363            .map(|factory| factory(org_id))
1364    }
1365
1366    fn platform_store(
1367        &self,
1368        org_id: i64,
1369        session_id: SessionId,
1370    ) -> Option<Arc<dyn everruns_platform::PlatformStore>> {
1371        self.platform_store_factory
1372            .as_ref()
1373            .map(|factory| factory(org_id, session_id))
1374    }
1375
1376    fn utility_llm_service(&self) -> Option<Arc<dyn everruns_core::UtilityLlmService>> {
1377        Some(self.platform_definition.utility_llm_service())
1378    }
1379
1380    fn egress_service(&self) -> Option<Arc<dyn everruns_core::EgressService>> {
1381        Some(self.platform_definition.egress_service())
1382    }
1383
1384    fn provider_retry_config(&self) -> Option<everruns_core::llm_retry::LlmRetryConfig> {
1385        self.provider_retry_config.clone()
1386    }
1387
1388    fn provider_stall_timeout(&self) -> Option<std::time::Duration> {
1389        self.provider_stall_timeout
1390    }
1391}
1392
1393#[derive(Clone)]
1394struct PersistingEventEmitter {
1395    inner: Arc<dyn EventBus>,
1396    message_store: Arc<dyn RuntimeMessageStore>,
1397}
1398
1399impl PersistingEventEmitter {
1400    fn new(inner: Arc<dyn EventBus>, message_store: Arc<dyn RuntimeMessageStore>) -> Self {
1401        Self {
1402            inner,
1403            message_store,
1404        }
1405    }
1406}
1407
1408#[async_trait]
1409impl EventEmitter for PersistingEventEmitter {
1410    async fn emit(&self, request: EventRequest) -> Result<Event> {
1411        let event = self.inner.emit(request.clone()).await?;
1412        if let Some(message) = message_from_event(&event.data) {
1413            self.message_store
1414                .store_message(request.session_id, message)
1415                .await?;
1416        }
1417        Ok(event)
1418    }
1419}
1420
1421fn effective_overlay(
1422    harness_chain: &[Harness],
1423    agent: Option<&Agent>,
1424    session: &Session,
1425) -> AgentConfigOverlay {
1426    let harness_layers = harness_chain.iter().map(AgentConfigOverlay::from);
1427    let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
1428    AgentConfigOverlay::fold(
1429        harness_layers
1430            .chain(agent_layers)
1431            .chain([AgentConfigOverlay::from(session)]),
1432    )
1433}
1434
1435/// Replace bare `plugin:{name}` refs (empty config) with the hydrated version
1436/// (config = serialised `DeclarativeCapabilityDefinition`) so that the core
1437/// capability resolution path can deserialise them without a registry entry.
1438///
1439/// Only replaces entries whose config is empty / `null`; entries that already
1440/// carry a non-empty config are left unchanged so explicit overrides are honoured.
1441fn hydrate_plugin_refs(
1442    capabilities: &mut [AgentCapabilityConfig],
1443    plugin_configs: &[AgentCapabilityConfig],
1444) {
1445    for cap in capabilities.iter_mut() {
1446        let cap_id = cap.capability_id();
1447        if !everruns_core::is_plugin_capability(cap_id) {
1448            continue;
1449        }
1450        // Only replace if the config is missing / empty so explicit overrides are honoured.
1451        let is_bare = cap.config.is_null()
1452            || cap
1453                .config
1454                .as_object()
1455                .map(|o| o.is_empty())
1456                .unwrap_or(false);
1457        if !is_bare {
1458            continue;
1459        }
1460        if let Some(hydrated) = plugin_configs.iter().find(|c| c.capability_id() == cap_id) {
1461            cap.config = hydrated.config.clone();
1462        }
1463    }
1464}
1465
1466async fn seed_runtime_initial_files(
1467    harness_store: &dyn RuntimeHarnessStore,
1468    agent_store: &dyn RuntimeAgentStore,
1469    file_store: &dyn SessionFileSystem,
1470    session: &Session,
1471) -> Result<()> {
1472    let harness_chain = harness_store.get_harness_chain(session.harness_id).await?;
1473    if harness_chain.is_empty() {
1474        return Err(AgentLoopError::store(format!(
1475            "harness not found while seeding files: {}",
1476            session.harness_id
1477        )));
1478    }
1479    let agent = match session.agent_id {
1480        Some(agent_id) => Some(
1481            agent_store
1482                .get_agent(agent_id)
1483                .await?
1484                .ok_or_else(|| AgentLoopError::store(format!("agent not found: {agent_id}")))?,
1485        ),
1486        None => None,
1487    };
1488    let overlay = effective_overlay(&harness_chain, agent.as_ref(), session);
1489    // Seed into the session's workspace (a shared workspace differs from the
1490    // session id; the default 1:1 case is equal).
1491    let seed_key = SessionId::from_uuid(session.workspace_id.uuid());
1492    for file in &overlay.initial_files {
1493        file_store.seed_initial_file(seed_key, file).await?;
1494    }
1495    Ok(())
1496}
1497
1498fn message_from_event(data: &EventData) -> Option<Message> {
1499    match data {
1500        EventData::InputMessage(data) => Some(data.message.clone()),
1501        EventData::OutputMessageCompleted(OutputMessageCompletedData { message, .. }) => {
1502            Some(message.clone())
1503        }
1504        EventData::ToolCompleted(data) => Some(tool_completed_to_message(data.clone())),
1505        _ => None,
1506    }
1507}
1508
1509fn tool_completed_to_message(data: ToolCompletedData) -> Message {
1510    let mut images: Vec<ToolResultImage> = Vec::new();
1511    let metadata = tool_result_metadata(&data);
1512    let result = data.result.map(|parts| {
1513        for part in &parts {
1514            if let ContentPart::Image(img) = part
1515                && let (Some(base64), Some(media_type)) = (&img.base64, &img.media_type)
1516            {
1517                images.push(ToolResultImage {
1518                    base64: base64.clone(),
1519                    media_type: media_type.clone(),
1520                });
1521            }
1522        }
1523
1524        let text_parts: Vec<&ContentPart> = parts
1525            .iter()
1526            .filter(|part| matches!(part, ContentPart::Text(_)))
1527            .collect();
1528        if text_parts.len() == 1
1529            && let ContentPart::Text(text) = text_parts[0]
1530        {
1531            return parse_structured_tool_result_text(&text.text);
1532        }
1533        if !text_parts.is_empty() {
1534            serde_json::to_value(&text_parts).unwrap_or_default()
1535        } else {
1536            serde_json::Value::Null
1537        }
1538    });
1539
1540    let mut message = if images.is_empty() {
1541        Message::tool_result(&data.tool_call_id, result, data.error)
1542    } else {
1543        Message::tool_result_with_images(&data.tool_call_id, result, images)
1544    };
1545    message.metadata = metadata;
1546    message
1547}
1548
1549fn tool_result_metadata(
1550    data: &ToolCompletedData,
1551) -> Option<std::collections::HashMap<String, serde_json::Value>> {
1552    let mut metadata = std::collections::HashMap::new();
1553    metadata.insert("tool_name".to_string(), serde_json::json!(data.tool_name));
1554    if let Some(fingerprint) = &data.tool_call_fingerprint {
1555        metadata.insert(
1556            "tool_call_fingerprint".to_string(),
1557            serde_json::json!(fingerprint),
1558        );
1559    }
1560    if let Some(fingerprint) = &data.tool_result_fingerprint {
1561        metadata.insert(
1562            "tool_result_fingerprint".to_string(),
1563            serde_json::json!(fingerprint),
1564        );
1565    }
1566    (!metadata.is_empty()).then_some(metadata)
1567}
1568
1569fn parse_structured_tool_result_text(text: &str) -> serde_json::Value {
1570    let trimmed = text.trim_start();
1571    if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
1572        return serde_json::Value::String(text.to_string());
1573    }
1574
1575    match serde_json::from_str(text) {
1576        Ok(value @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => value,
1577        _ => serde_json::Value::String(text.to_string()),
1578    }
1579}
1580
1581#[cfg(test)]
1582mod tool_completed_replay_tests {
1583    use super::*;
1584
1585    #[test]
1586    fn tool_completed_replay_preserves_json_object_shape() {
1587        let data = ToolCompletedData::success(
1588            "call_read".to_string(),
1589            "read_file".to_string(),
1590            vec![ContentPart::text(
1591                serde_json::json!({
1592                    "path": "/workspace/src/lib.rs",
1593                    "content": "1|fn main() {}"
1594                })
1595                .to_string(),
1596            )],
1597            Some(1),
1598        );
1599
1600        let message = tool_completed_to_message(data);
1601        let result = message
1602            .tool_result_content()
1603            .and_then(|content| content.result.as_ref())
1604            .expect("tool result should be present");
1605
1606        assert_eq!(result["path"], "/workspace/src/lib.rs");
1607        assert_eq!(result["content"], "1|fn main() {}");
1608    }
1609
1610    #[test]
1611    fn tool_completed_replay_keeps_scalar_json_as_text() {
1612        let data = ToolCompletedData::success(
1613            "call_scalar".to_string(),
1614            "custom_tool".to_string(),
1615            vec![ContentPart::text("123")],
1616            Some(1),
1617        );
1618
1619        let message = tool_completed_to_message(data);
1620        let result = message
1621            .tool_result_content()
1622            .and_then(|content| content.result.as_ref())
1623            .expect("tool result should be present");
1624
1625        assert_eq!(result, &serde_json::Value::String("123".to_string()));
1626    }
1627
1628    #[test]
1629    fn tool_completed_replay_preserves_fingerprints_as_metadata() {
1630        let data = ToolCompletedData::success(
1631            "call_read".to_string(),
1632            "read_file".to_string(),
1633            vec![ContentPart::text("{}")],
1634            Some(1),
1635        )
1636        .with_fingerprints("sha256:call".to_string(), "sha256:result".to_string());
1637
1638        let message = tool_completed_to_message(data);
1639        let metadata = message.metadata.expect("metadata should be present");
1640
1641        assert_eq!(metadata["tool_name"], "read_file");
1642        assert_eq!(metadata["tool_call_fingerprint"], "sha256:call");
1643        assert_eq!(metadata["tool_result_fingerprint"], "sha256:result");
1644    }
1645}
1646
1647#[cfg(test)]
1648mod org_id_mapping_tests {
1649    use super::*;
1650    use everruns_core::{DEFAULT_ORG_ID, DEFAULT_ORG_PUBLIC_ID, org_public_id_from_internal};
1651
1652    #[test]
1653    fn default_public_id_maps_to_default_org() {
1654        assert_eq!(
1655            in_process_internal_org_id(DEFAULT_ORG_PUBLIC_ID),
1656            DEFAULT_ORG_ID
1657        );
1658    }
1659
1660    #[test]
1661    fn invalid_public_id_does_not_fall_back_to_default() {
1662        for invalid in [
1663            "",
1664            "not-an-org",
1665            "org_short",
1666            "org_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
1667            "ORG_00000000000000000000000000000001",
1668        ] {
1669            let mapped = in_process_internal_org_id(invalid);
1670            assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1671            assert!(
1672                mapped >= 2,
1673                "invalid input {invalid:?} should not map to default"
1674            );
1675        }
1676    }
1677
1678    #[test]
1679    fn zero_public_id_does_not_fall_back_to_default() {
1680        // org_public_id_from_internal never produces this; a hand-crafted
1681        // all-zeros id is treated as invalid (raw == 0).
1682        let mapped = in_process_internal_org_id("org_00000000000000000000000000000000");
1683        assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1684        assert!(mapped >= 2, "all-zero id should not map to default");
1685    }
1686
1687    #[test]
1688    fn synthetic_public_id_round_trips_with_internal_helper() {
1689        for internal in [1_i64, 2, 42, 1_000_000, i64::MAX - 1, i64::MAX] {
1690            let public = org_public_id_from_internal(internal);
1691            assert_eq!(
1692                in_process_internal_org_id(&public),
1693                internal,
1694                "round-trip failed for internal={internal}"
1695            );
1696        }
1697    }
1698
1699    #[test]
1700    fn distinct_synthetic_ids_map_to_distinct_internal_ids() {
1701        let a = org_public_id_from_internal(7);
1702        let b = org_public_id_from_internal(8);
1703        assert_ne!(a, b);
1704        assert_ne!(
1705            in_process_internal_org_id(&a),
1706            in_process_internal_org_id(&b)
1707        );
1708    }
1709
1710    #[test]
1711    fn high_entropy_uuid_style_id_hashes_into_reserved_range() {
1712        // First valid UUID-style id whose raw u128 exceeds i64::MAX
1713        // (top bit of the u128 set). It must hash to a positive i64 that
1714        // is neither 0 nor DEFAULT_ORG_ID.
1715        let high = "org_80000000000000000000000000000000";
1716        let mapped = in_process_internal_org_id(high);
1717        assert!(mapped >= 2, "mapped id {mapped} must be >= 2");
1718        assert_ne!(mapped, DEFAULT_ORG_ID);
1719
1720        // Mapping is deterministic.
1721        assert_eq!(mapped, in_process_internal_org_id(high));
1722    }
1723
1724    #[test]
1725    fn high_entropy_ids_are_isolated_from_each_other() {
1726        let a = in_process_internal_org_id("org_80000000000000000000000000000001");
1727        let b = in_process_internal_org_id("org_80000000000000000000000000000002");
1728        assert_ne!(a, b);
1729        assert_ne!(a, DEFAULT_ORG_ID);
1730        assert_ne!(b, DEFAULT_ORG_ID);
1731    }
1732
1733    #[test]
1734    fn hash_uses_stable_sha256_truncation() {
1735        // SHA-256 with fixed big-endian first-8-byte truncation gives a value
1736        // we can pin. If this assertion ever breaks, callers depending on
1737        // build-stable mapping must be re-audited.
1738        let mapped = in_process_internal_org_id("org_80000000000000000000000000000000");
1739        let expected = {
1740            let digest = sha2::Sha256::digest(b"org_80000000000000000000000000000000");
1741            let mut buf = [0u8; 8];
1742            buf.copy_from_slice(&digest[..8]);
1743            let raw = u64::from_be_bytes(buf);
1744            ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
1745        };
1746        assert_eq!(mapped, expected);
1747    }
1748
1749    #[test]
1750    fn oversize_input_is_bounded_and_does_not_collide_silently() {
1751        // Inputs past HASH_INPUT_CAP_BYTES are truncated before hashing, so
1752        // two oversize strings that agree on the first cap bytes map to the
1753        // same internal id. We only assert the result stays in the safe
1754        // [2, i64::MAX] range and is not DEFAULT_ORG_ID — the cap exists to
1755        // bound work, not to widen the input space.
1756        let oversize = "x".repeat(super::HASH_INPUT_CAP_BYTES * 4);
1757        let mapped = in_process_internal_org_id(&oversize);
1758        assert!(mapped >= 2);
1759        assert_ne!(mapped, DEFAULT_ORG_ID);
1760    }
1761}