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