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