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