Skip to main content

everruns_host/
runtime.rs

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