Skip to main content

everruns_runtime/
host.rs

1// Shared host orchestration for embedded and durable execution hosts.
2// Decision: everruns-runtime owns worker-facing turn phase execution so
3// durable/server-backed hosts reuse the same input/reason/act wiring.
4
5use async_trait::async_trait;
6use everruns_core::atoms::{
7    ActAtom, ActInput, ActResult, Atom, InputAtom, InputAtomInput, InputAtomResult, ReasonAtom,
8    ReasonInput, ReasonResult,
9};
10use everruns_core::capabilities::{
11    SystemPromptContext, collect_capabilities_with_configs, report_result_tool_for_child_session,
12    report_task_progress_tool_for_child_session,
13};
14use everruns_core::events::{
15    EventContext, EventRequest, OutputMessageCompletedData, SessionActivatedData, SessionIdledData,
16    TurnCompletedData, TurnFailedData, TurnStartedData,
17};
18use everruns_core::message::{ContentPart, Message};
19use everruns_core::message_retriever::MessageRetriever;
20use everruns_core::platform_store::PlatformStore;
21use everruns_core::session::SessionStatus;
22use everruns_core::tools::Tool;
23use everruns_core::traits::{
24    AgentStore, BudgetChecker, EventEmitter, HarnessStore, ImageArtifactStore, ImageResolver,
25    LeasedResourceStore, PaymentAuthority, ProviderCredentialStore, ProviderStore, ResolvedModel,
26    SessionFileSystem, SessionMutator, SessionResourceRegistry, SessionScheduleStore,
27    SessionSqlDbStoreRef, SessionStorageStore, SessionStore, UserConnectionResolver,
28};
29use everruns_core::typed_id::{AgentId, HarnessId, MessageId, SessionId, TurnId};
30use everruns_core::vector_store::KnowledgeIndexSearch;
31use everruns_core::{
32    Agent, CapabilityRegistry, CapabilityStatus, DependencyBlocker, DriverRegistry, EgressService,
33    ErrorDisclosure, Harness, Session, TokenUsage, ToolDefinition, ToolRegistry, UserFacingError,
34    UtilityLlmService, assemble_turn_context, org_public_id_from_internal,
35    resolve_runtime_capabilities,
36};
37use std::sync::Arc;
38use tracing::warn;
39
40/// Turn context loaded in one batched call for runtime host execution.
41#[derive(Debug, Clone)]
42pub struct RuntimeHostTurnContext {
43    pub agent: Option<Agent>,
44    pub session: Session,
45    pub messages: Vec<Message>,
46    pub model: Option<ResolvedModel>,
47    pub mcp_tool_definitions: Vec<ToolDefinition>,
48}
49
50/// Public adapter contract for server-backed or durable runtime hosts.
51///
52/// `everruns-runtime` owns shared host orchestration for both embedded and
53/// durable execution. That includes phase execution (`input -> reason -> act`),
54/// lifecycle emission, and the generic turn-strategy decisions used by durable
55/// or custom hosts.
56///
57/// Host crates implement this trait to provide persistence, session-lifecycle
58/// plumbing, event delivery, and their own orchestration backend. The durable
59/// engine itself remains outside this crate.
60#[async_trait]
61pub trait RuntimeHostAdapter: Send + Sync + Clone + 'static {
62    async fn get_agent(
63        &self,
64        org_id: i64,
65        agent_id: AgentId,
66    ) -> everruns_core::error::Result<Option<Agent>>;
67
68    async fn get_harness(
69        &self,
70        org_id: i64,
71        harness_id: HarnessId,
72    ) -> everruns_core::error::Result<Option<Harness>>;
73
74    async fn set_session_status(
75        &self,
76        org_id: i64,
77        session_id: SessionId,
78        status: SessionStatus,
79    ) -> everruns_core::error::Result<Session>;
80
81    async fn load_turn_context(
82        &self,
83        org_id: i64,
84        session_id: SessionId,
85    ) -> everruns_core::error::Result<RuntimeHostTurnContext>;
86
87    fn capability_registry(&self) -> CapabilityRegistry;
88
89    fn driver_registry(&self) -> DriverRegistry;
90
91    fn harness_store(&self, org_id: i64) -> Arc<dyn HarnessStore>;
92
93    fn agent_store(&self, org_id: i64) -> Arc<dyn AgentStore>;
94
95    fn session_store(&self, org_id: i64) -> Arc<dyn SessionStore>;
96
97    fn session_mutator(&self, org_id: i64) -> Arc<dyn SessionMutator>;
98
99    fn provider_store(&self, org_id: i64) -> Arc<dyn ProviderStore>;
100
101    fn message_store(&self) -> Arc<dyn MessageRetriever>;
102
103    fn event_emitter(&self) -> Arc<dyn EventEmitter>;
104
105    fn file_store(&self) -> Arc<dyn SessionFileSystem>;
106
107    fn image_resolver(&self, _org_id: i64) -> Option<Arc<dyn ImageResolver>> {
108        None
109    }
110
111    fn image_artifact_store(&self, _org_id: i64) -> Option<Arc<dyn ImageArtifactStore>> {
112        None
113    }
114
115    fn provider_credential_store(&self, _org_id: i64) -> Option<Arc<dyn ProviderCredentialStore>> {
116        None
117    }
118
119    fn utility_llm_service(&self) -> Option<Arc<dyn UtilityLlmService>> {
120        None
121    }
122
123    fn egress_service(&self) -> Option<Arc<dyn EgressService>> {
124        None
125    }
126
127    fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
128        None
129    }
130
131    /// Knowledge store backing the `search_knowledge` tool. Default: none.
132    fn knowledge_store(&self) -> Option<Arc<dyn everruns_core::traits::KnowledgeStore>> {
133        None
134    }
135
136    fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
137        None
138    }
139
140    fn sqldb_store(&self) -> Option<SessionSqlDbStoreRef> {
141        None
142    }
143
144    fn leased_resource_store(&self) -> Option<Arc<dyn LeasedResourceStore>> {
145        None
146    }
147
148    fn session_resource_registry(&self) -> Option<Arc<dyn SessionResourceRegistry>> {
149        None
150    }
151
152    fn session_task_registry(
153        &self,
154    ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
155        None
156    }
157
158    fn schedule_store(&self, _org_id: i64) -> Option<Arc<dyn SessionScheduleStore>> {
159        None
160    }
161
162    fn platform_store(
163        &self,
164        _org_id: i64,
165        _session_id: SessionId,
166    ) -> Option<Arc<dyn PlatformStore>> {
167        None
168    }
169
170    /// Get the Knowledge Index search service for the `search_index` tool.
171    /// Org-scoped; returns None when retrieval is not available (e.g. gRPC
172    /// workers without a search RPC, or in-memory test backends).
173    fn knowledge_index_search(&self, _org_id: i64) -> Option<Arc<dyn KnowledgeIndexSearch>> {
174        None
175    }
176
177    fn budget_checker(
178        &self,
179        _org_id: i64,
180        _agent_id: Option<AgentId>,
181    ) -> Option<Arc<dyn BudgetChecker>> {
182        None
183    }
184
185    fn payment_authority(
186        &self,
187        _org_id: i64,
188        _agent_id: Option<AgentId>,
189    ) -> Option<Arc<dyn PaymentAuthority>> {
190        None
191    }
192
193    /// Per-org outbound tool-call rate limiter (TM-TOOL-009).
194    /// Default: `None` (no rate limiting — suitable for in-process / test environments).
195    fn outbound_tool_rate_limiter(
196        &self,
197        _org_id: i64,
198    ) -> Option<Arc<dyn everruns_core::OutboundToolRateLimiter>> {
199        None
200    }
201
202    /// Per-turn durable tool result store for act-activity idempotency (EVE-530).
203    /// Default: `None` (no durable claim/settle — every execution runs tools fresh).
204    fn durable_tool_result_store(&self) -> Option<Arc<dyn everruns_core::DurableToolResultStore>> {
205        None
206    }
207
208    /// Durable subagent spawn handle store for reattach on reclaim (EVE-535).
209    /// Default: `None` (no spawn dedup — dev/test mode or hosts without durable execution).
210    fn subagent_spawn_store(&self) -> Option<Arc<dyn everruns_core::SubagentSpawnStore>> {
211        None
212    }
213
214    /// Stream-liveness heartbeater for the Reason activity (EVE-531).
215    /// Default: `None` (no heartbeats sent — durable workers supply one).
216    fn stream_heartbeater(&self) -> Option<Arc<dyn everruns_core::StreamHeartbeater>> {
217        None
218    }
219
220    /// Partial-stream store for ContinuePartial recovery (EVE-532).
221    /// Default: `None` (no recovery; in-memory and dev hosts use this default).
222    fn partial_stream_store(&self) -> Option<Arc<dyn everruns_core::PartialStreamStore>> {
223        None
224    }
225
226    /// Live, turn-scoped reasoning-effort handle for the given session (EVE-595).
227    ///
228    /// When a host returns a handle, the Reason activity re-reads it on every
229    /// LLM step and the Act activity hands the same instance to each tool's
230    /// `ToolContext`. A tool can then change effort mid-turn and have subsequent
231    /// LLM steps in the same turn observe it. Hosts MUST return the *same*
232    /// handle instance for a session across reason/act activities of one turn.
233    /// Default: `None` (effort is resolved solely from message controls).
234    fn reasoning_effort_handle(
235        &self,
236        _session_id: SessionId,
237    ) -> Option<everruns_core::ReasoningEffortHandle> {
238        None
239    }
240
241    /// Provider stall timeout for the Reason activity (EVE-531).
242    /// Default: `None` (use built-in 120s default).
243    fn provider_stall_timeout(&self) -> Option<std::time::Duration> {
244        None
245    }
246
247    /// MCP executor routing `mcp_*` tool calls for this session, if the host
248    /// configures MCP (specs/runtime-mcp.md D4). Default: `None`, so hosts
249    /// without scoped MCP servers keep the plain tool registry unchanged.
250    async fn mcp_executor(
251        &self,
252        _org_id: i64,
253        _session_id: SessionId,
254    ) -> Option<Arc<everruns_mcp::McpExecutor>> {
255        None
256    }
257}
258
259struct RuntimeExecutionCapabilities {
260    tool_registry: ToolRegistry,
261    post_tool_hooks: Vec<Arc<dyn everruns_core::PostToolExecHook>>,
262    pre_tool_hooks: Vec<Arc<dyn everruns_core::atoms::PreToolUseHook>>,
263    tool_call_hooks: Vec<Arc<dyn everruns_core::ToolCallHook>>,
264    subagent_nesting_policy: everruns_core::SubagentNestingPolicy,
265}
266
267fn subagent_nesting_policy_from_configs(
268    resolved_capability_configs: &[everruns_core::capability_types::AgentCapabilityConfig],
269) -> everruns_core::SubagentNestingPolicy {
270    let subagents_config = resolved_capability_configs.iter().find(|config| {
271        config.capability_id() == everruns_core::capabilities::SUBAGENTS_CAPABILITY_ID
272    });
273
274    let configured_depth = subagents_config
275        .and_then(|config| {
276            config
277                .config
278                .get("max_subagent_depth")
279                .or_else(|| config.config.get("max_depth"))
280        })
281        .and_then(|value| value.as_u64())
282        .and_then(|value| u32::try_from(value).ok());
283    let configured_max_active = subagents_config
284        .and_then(|config| {
285            config
286                .config
287                .get("max_active_descendant_tasks")
288                .or_else(|| config.config.get("max_concurrent_descendant_tasks"))
289        })
290        .and_then(|value| value.as_u64())
291        .and_then(|value| u32::try_from(value).ok());
292    let configured_max_total = subagents_config
293        .and_then(|config| config.config.get("max_total_descendant_tasks"))
294        .and_then(|value| value.as_u64())
295        .and_then(|value| u32::try_from(value).ok());
296
297    everruns_core::SubagentNestingPolicy::default()
298        .with_agent_override(configured_depth)
299        .with_agent_task_caps_override(configured_max_active, configured_max_total)
300}
301
302/// Collect and finalize user-hook specs for a session from its resolved
303/// capability configs, plus the shared bash dispatcher used to run them.
304///
305/// This is the single place hook specs are gathered so every firing point —
306/// the act path (`load_execution_capabilities`) and the lifecycle firing
307/// points (`execute_reason_activity` for `user_prompt_submit`, turn completion
308/// for `turn_end`, and the server session paths) — applies identical
309/// `finalize_hook_specs` semantics: `{capability_id}:` namespace stamping,
310/// stable default ids, and `disabled_contributions` muting (TM-HOOK-004).
311fn finalize_specs_from_configs(
312    resolved_capability_configs: &[everruns_core::capability_types::AgentCapabilityConfig],
313    capability_registry: &CapabilityRegistry,
314) -> Vec<everruns_core::user_hook_types::UserHookSpec> {
315    let mut hook_contributions: Vec<(String, Vec<everruns_core::user_hook_types::UserHookSpec>)> =
316        Vec::new();
317    let mut disabled_contributions: Vec<String> = Vec::new();
318    for config in resolved_capability_configs {
319        let Some(capability) = capability_registry.get(config.capability_id()) else {
320            continue;
321        };
322        let specs = capability.user_hooks_with_config(&config.config);
323        if !specs.is_empty() {
324            hook_contributions.push((config.capability_id().to_string(), specs));
325        }
326        if config.capability_id() == "user_hooks" {
327            disabled_contributions.extend(
328                everruns_core::capabilities::user_hooks::disabled_contributions(&config.config),
329            );
330        }
331    }
332    everruns_core::hook_adapter::finalize_hook_specs(hook_contributions, &disabled_contributions)
333}
334
335/// Resolve a session's capability configs and collect finalized hook specs.
336/// Used by the lifecycle firing points, which need specs outside the act path.
337/// Returns `(specs, dispatcher)`; `specs` is empty when the session has no
338/// hook-contributing capabilities.
339async fn collect_lifecycle_hook_specs<A: RuntimeHostAdapter>(
340    adapter: &A,
341    org_id: i64,
342    session_id: SessionId,
343    harness_id: HarnessId,
344    agent_id: Option<AgentId>,
345) -> everruns_core::error::Result<(
346    Vec<everruns_core::user_hook_types::UserHookSpec>,
347    Arc<dyn everruns_core::hook_executor::BashHookDispatcher>,
348)> {
349    let capability_registry = adapter.capability_registry();
350    let harness_chain = adapter
351        .harness_store(org_id)
352        .get_harness_chain(harness_id)
353        .await?;
354    if harness_chain.is_empty() {
355        return Err(everruns_core::error::AgentLoopError::harness_not_found(
356            harness_id,
357        ));
358    }
359    let session = adapter
360        .session_store(org_id)
361        .get_session(session_id)
362        .await?
363        .ok_or_else(|| everruns_core::error::AgentLoopError::session_not_found(session_id))?;
364    let agent = match agent_id {
365        Some(agent_id) => adapter.agent_store(org_id).get_agent(agent_id).await?,
366        None => None,
367    };
368    let resolved = resolve_runtime_capabilities(
369        &harness_chain,
370        agent.as_ref(),
371        &session,
372        &capability_registry,
373    );
374    let specs =
375        finalize_specs_from_configs(&resolved.resolved_capability_configs, &capability_registry);
376    let dispatcher: Arc<dyn everruns_core::hook_executor::BashHookDispatcher> = Arc::new(
377        everruns_core::hook_dispatch::BashkitShellHookDispatcher::new(adapter.file_store()),
378    );
379    Ok((specs, dispatcher))
380}
381
382async fn load_execution_capabilities<A: RuntimeHostAdapter>(
383    adapter: &A,
384    org_id: i64,
385    session_id: SessionId,
386    harness_id: HarnessId,
387    agent_id: Option<AgentId>,
388    locale: Option<String>,
389    blueprint_id: Option<&str>,
390) -> everruns_core::error::Result<RuntimeExecutionCapabilities> {
391    let capability_registry = adapter.capability_registry();
392    if let Some(blueprint_id) = blueprint_id {
393        let mut registry = ToolRegistry::with_defaults();
394        let blueprint = capability_registry.blueprint(blueprint_id).ok_or_else(|| {
395            everruns_core::error::AgentLoopError::config(format!(
396                "Blueprint \"{blueprint_id}\" not found in registry"
397            ))
398        })?;
399        for tool in blueprint.tools {
400            registry.register_boxed(tool);
401        }
402        return Ok(RuntimeExecutionCapabilities {
403            tool_registry: registry,
404            post_tool_hooks: Vec::new(),
405            pre_tool_hooks: Vec::new(),
406            tool_call_hooks: Vec::new(),
407            subagent_nesting_policy: everruns_core::SubagentNestingPolicy::default(),
408        });
409    }
410
411    let harness_chain = adapter
412        .harness_store(org_id)
413        .get_harness_chain(harness_id)
414        .await?;
415    if harness_chain.is_empty() {
416        return Err(everruns_core::error::AgentLoopError::harness_not_found(
417            harness_id,
418        ));
419    }
420
421    let session = adapter
422        .session_store(org_id)
423        .get_session(session_id)
424        .await?
425        .ok_or_else(|| everruns_core::error::AgentLoopError::session_not_found(session_id))?;
426
427    let agent_store = adapter.agent_store(org_id);
428    let agent = match agent_id {
429        Some(agent_id) => Some(
430            agent_store
431                .get_agent(agent_id)
432                .await?
433                .ok_or_else(|| everruns_core::error::AgentLoopError::agent_not_found(agent_id))?,
434        ),
435        None => None,
436    };
437
438    let resolved = resolve_runtime_capabilities(
439        &harness_chain,
440        agent.as_ref(),
441        &session,
442        &capability_registry,
443    );
444    // Executor (act) path: this builds the worker-side tool registry, not the
445    // model-visible tool list. The model is left unset, so a model-adaptive
446    // capability like `auto_tool_search` resolves to its provider-agnostic
447    // client-side mechanism here. That registers the `tool_search` tool in the
448    // executor, which is a harmless superset: on native models the reason path
449    // never shows that tool to the model, so it is simply never called.
450    let prompt_ctx = SystemPromptContext {
451        session_id,
452        locale: locale.or(session.locale.clone()),
453        // Pin system-prompt file reads to the session's workspace (the default
454        // 1:1 case is a transparent pass-through), then resolve through the
455        // mount resolver (EVE-660): `/workspace` is a mount + cwd.
456        file_store: Some(everruns_core::MountFs::wrap(
457            everruns_core::WorkspaceScopedFileSystem::wrap(
458                adapter.file_store(),
459                session.workspace_id,
460            ),
461        )),
462        model: None,
463    };
464    let collected = collect_capabilities_with_configs(
465        &resolved.resolved_capability_configs,
466        &capability_registry,
467        &prompt_ctx,
468    )
469    .await;
470
471    let mut registry = ToolRegistry::with_defaults();
472    for tool in collected.tools {
473        registry.register_boxed(tool);
474    }
475
476    // Only `Available` capabilities contribute hooks, matching
477    // `collect_capabilities_with_configs` (which skips non-available
478    // capabilities). This keeps a `ComingSoon`/unavailable capability from
479    // affecting execution via any of its hook seams.
480    let mut post_tool_hooks: Vec<Arc<dyn everruns_core::PostToolExecHook>> = resolved
481        .resolved_capability_configs
482        .iter()
483        .flat_map(|config| {
484            capability_registry
485                .get(config.capability_id())
486                .filter(|capability| capability.status() == CapabilityStatus::Available)
487                .map(|capability| capability.post_tool_exec_hooks_with_config(&config.config))
488                .unwrap_or_default()
489        })
490        .collect();
491    // Tool-output guardrails must inspect the original result before other
492    // capability hooks can persist or compact it into secondary surfaces.
493    post_tool_hooks.sort_by_key(|hook| hook.priority());
494
495    // User-hook contributions (see `specs/user-hooks.md`). `finalize_specs_from_configs`
496    // gathers specs across every resolved capability — both the user-facing
497    // `user_hooks` capability and any capability that bundles hooks — and applies
498    // `finalize_hook_specs` (namespace stamping, stable ids, `disabled_contributions`
499    // muting; TM-HOOK-004). The same helper backs the lifecycle firing points so
500    // every event finalizes specs identically.
501    let user_hook_specs =
502        finalize_specs_from_configs(&resolved.resolved_capability_configs, &capability_registry);
503    // Capability-contributed pre-tool hooks run first (e.g. approval gating),
504    // then user-hook (`PreToolUse`) specs. The first hook to block wins.
505    let mut pre_tool_hooks: Vec<Arc<dyn everruns_core::atoms::PreToolUseHook>> = resolved
506        .resolved_capability_configs
507        .iter()
508        .flat_map(|config| {
509            capability_registry
510                .get(config.capability_id())
511                .filter(|capability| capability.status() == CapabilityStatus::Available)
512                .map(|capability| capability.pre_tool_use_hooks_with_config(&config.config))
513                .unwrap_or_default()
514        })
515        .collect();
516    if !user_hook_specs.is_empty() {
517        let dispatcher: Arc<dyn everruns_core::hook_executor::BashHookDispatcher> = Arc::new(
518            everruns_core::hook_dispatch::BashkitShellHookDispatcher::new(adapter.file_store()),
519        );
520        post_tool_hooks.extend(everruns_core::hook_adapter::build_post_tool_use_hooks(
521            &user_hook_specs,
522            dispatcher.clone(),
523        ));
524        pre_tool_hooks.extend(everruns_core::hook_adapter::build_pre_tool_use_hooks(
525            &user_hook_specs,
526            dispatcher,
527        ));
528    }
529
530    // Use the hook list assembled by `collect_capabilities_with_configs` as the
531    // single source of truth. It already contains every explicit capability
532    // `tool_call_hooks()` followed by the generated `CapabilityNarrationHook`
533    // adapters — one per collected capability plus any auto-activated
534    // cross-cutting capability such as `background_execution`. Re-deriving only
535    // the explicit subset here dropped capability-owned narration, so tools fell
536    // back to generic `Ran {display_name}` lines (EVE-601). Explicit hooks stay
537    // first in this list, so model-authored narration (`human_intent`) keeps its
538    // precedence over default `Tool::narrate()`, and only available capabilities
539    // contributed because collection skips non-available ones.
540    let tool_call_hooks = collected.tool_call_hooks;
541
542    Ok(RuntimeExecutionCapabilities {
543        tool_registry: registry,
544        post_tool_hooks,
545        pre_tool_hooks,
546        tool_call_hooks,
547        subagent_nesting_policy: subagent_nesting_policy_from_configs(
548            &resolved.resolved_capability_configs,
549        ),
550    })
551}
552
553/// Shared lifecycle helper for runtime-backed hosts.
554pub struct RuntimeSessionLifecycle<A: RuntimeHostAdapter> {
555    adapter: A,
556    org_id: i64,
557    session_id: SessionId,
558}
559
560impl<A: RuntimeHostAdapter> RuntimeSessionLifecycle<A> {
561    pub fn new(adapter: A, org_id: i64, session_id: SessionId) -> Self {
562        Self {
563            adapter,
564            org_id,
565            session_id,
566        }
567    }
568
569    async fn set_session_status(&self, status: SessionStatus, action: &'static str) {
570        if let Err(error) = self
571            .adapter
572            .set_session_status(self.org_id, self.session_id, status)
573            .await
574        {
575            warn!(
576                session_id = %self.session_id,
577                org_id = self.org_id,
578                action,
579                %error,
580                "runtime host lifecycle status update failed"
581            );
582        }
583    }
584
585    async fn emit_event(&self, request: EventRequest) {
586        let event_type = request.event_type.clone();
587        if let Err(error) = self.adapter.event_emitter().emit(request).await {
588            warn!(
589                session_id = %self.session_id,
590                org_id = self.org_id,
591                event_type,
592                %error,
593                "runtime host lifecycle event emission failed"
594            );
595        }
596    }
597
598    pub async fn turn_started(&self, turn_id: TurnId, input_message_id: MessageId) {
599        let input_content = self
600            .adapter
601            .message_store()
602            .get(self.session_id, input_message_id)
603            .await
604            .ok()
605            .flatten()
606            .map(|message| message.content_to_llm_string());
607
608        self.set_session_status(SessionStatus::Active, "turn_started")
609            .await;
610
611        self.emit_event(EventRequest::new(
612            self.session_id,
613            EventContext::turn(turn_id, input_message_id),
614            SessionActivatedData {
615                turn_id,
616                input_message_id,
617            },
618        ))
619        .await;
620
621        self.emit_event(EventRequest::new(
622            self.session_id,
623            EventContext::turn(turn_id, input_message_id),
624            TurnStartedData {
625                turn_id,
626                input_message_id,
627                input_content,
628            },
629        ))
630        .await;
631    }
632
633    pub async fn emit_turn_completed(&self, input_message_id: MessageId, data: TurnCompletedData) {
634        let turn_id = data.turn_id;
635        self.emit_event(EventRequest::new(
636            self.session_id,
637            EventContext::turn(turn_id, input_message_id),
638            data,
639        ))
640        .await;
641    }
642
643    pub async fn emit_session_idled(
644        &self,
645        turn_id: TurnId,
646        input_message_id: MessageId,
647        iterations: Option<u32>,
648        usage: Option<TokenUsage>,
649    ) {
650        self.set_session_status(SessionStatus::Idle, "emit_session_idled")
651            .await;
652
653        self.emit_event(EventRequest::new(
654            self.session_id,
655            EventContext::turn(turn_id, input_message_id),
656            SessionIdledData {
657                turn_id,
658                iterations,
659                usage,
660            },
661        ))
662        .await;
663    }
664
665    pub async fn turn_completed(
666        &self,
667        turn_id: TurnId,
668        input_message_id: MessageId,
669        iterations: u32,
670        usage: Option<TokenUsage>,
671        input_content: Option<String>,
672    ) {
673        self.emit_turn_completed(
674            input_message_id,
675            TurnCompletedData {
676                turn_id,
677                iterations,
678                duration_ms: None,
679                usage: usage.clone(),
680                input_content,
681                final_message_id: None,
682                final_answer_preview: None,
683                time_to_first_token_ms: None,
684                tool_call_count: None,
685                llm_call_count: None,
686                status: Some("completed".to_string()),
687            },
688        )
689        .await;
690        self.emit_session_idled(turn_id, input_message_id, Some(iterations), usage)
691            .await;
692    }
693
694    /// Turn was deliberately sealed (EVE-534): emit `turn.sealed` + a
695    /// user-facing message + `session.idled`, and idle the session.
696    ///
697    /// Distinct from `turn_completed` (success) and `turn_failed` (error). The
698    /// session returns to `idle` so the UI unblocks; the Sealed state is
699    /// observable via the `turn.sealed` event and its `reason`.
700    pub async fn turn_sealed(
701        &self,
702        turn_id: TurnId,
703        input_message_id: MessageId,
704        reason: &str,
705        iterations: u32,
706        usage: Option<TokenUsage>,
707    ) {
708        let context = EventContext::turn(turn_id, input_message_id);
709
710        self.emit_event(EventRequest::new(
711            self.session_id,
712            context.clone(),
713            everruns_core::events::TurnSealedData {
714                turn_id,
715                reason: reason.to_string(),
716                detail: None,
717                iterations: Some(iterations),
718                usage: usage.clone(),
719            },
720        ))
721        .await;
722
723        self.emit_session_idled(turn_id, input_message_id, Some(iterations), usage)
724            .await;
725    }
726
727    /// Fire `turn_end` lifecycle hooks (advisory). Collects the session's hook
728    /// specs and runs every `turn_end` hook; failures are logged, never fatal.
729    /// `harness_id`/`agent_id` are required to resolve the capability chain.
730    pub async fn fire_turn_end_hooks(
731        &self,
732        harness_id: HarnessId,
733        agent_id: Option<AgentId>,
734        turn_id: TurnId,
735        success: bool,
736    ) {
737        let (specs, dispatcher) = match collect_lifecycle_hook_specs(
738            &self.adapter,
739            self.org_id,
740            self.session_id,
741            harness_id,
742            agent_id,
743        )
744        .await
745        {
746            Ok(pair) => pair,
747            Err(error) => {
748                warn!(
749                    session_id = %self.session_id,
750                    %error,
751                    "failed to collect turn_end hook specs; skipping"
752                );
753                return;
754            }
755        };
756        let hooks = everruns_core::lifecycle_hooks::build_turn_lifecycle_hooks(
757            &specs,
758            everruns_core::user_hook_types::HookEvent::TurnEnd,
759            dispatcher,
760        );
761        if hooks.is_empty() {
762            return;
763        }
764        let ctx = everruns_core::lifecycle_hooks::TurnHookContext {
765            session_id: self.session_id,
766            turn_id: Some(turn_id),
767            org_id: org_public_id_from_internal(self.org_id).parse().ok(),
768            agent_id: agent_id.map(|a| a.to_string()),
769        };
770        everruns_core::lifecycle_hooks::run_turn_end_hooks(
771            &hooks,
772            &ctx,
773            serde_json::json!({ "success": success }),
774        )
775        .await;
776    }
777
778    /// Abort a turn because a `user_prompt_submit` hook returned `Block`.
779    /// Reuses the dependency-blocked failure shape: emit a user-facing message
780    /// carrying the hook's `user_message` (or `reason`), then mark the turn
781    /// failed and idle the session.
782    pub async fn user_prompt_blocked(
783        &self,
784        turn_id: TurnId,
785        input_message_id: MessageId,
786        reason: &str,
787        user_message: Option<&str>,
788    ) {
789        let user_error =
790            UserFacingError::new(everruns_core::user_facing_error_codes::BLOCKED_BY_HOOK);
791        let shown = user_message.unwrap_or(reason);
792        let mut error_message = Message::assistant(shown);
793        let mut metadata = std::collections::HashMap::new();
794        user_error.apply_to_message_metadata(&mut metadata);
795        error_message.metadata = Some(metadata);
796
797        self.emit_event(EventRequest::new(
798            self.session_id,
799            EventContext::turn(turn_id, input_message_id),
800            OutputMessageCompletedData::new(error_message).with_user_facing_error(&user_error),
801        ))
802        .await;
803
804        self.turn_failed(turn_id, input_message_id, reason, Some(&user_error))
805            .await;
806    }
807
808    pub async fn turn_failed(
809        &self,
810        turn_id: TurnId,
811        input_message_id: MessageId,
812        error: &str,
813        user_error: Option<&UserFacingError>,
814    ) {
815        self.turn_failed_with_disclosure(turn_id, input_message_id, error, user_error, None)
816            .await;
817    }
818
819    /// `turn_failed` with the applied error-disclosure mode recorded on the
820    /// event. `user_error` (and the `error` text shown alongside it) must
821    /// already be disclosure-filtered by the caller.
822    pub async fn turn_failed_with_disclosure(
823        &self,
824        turn_id: TurnId,
825        input_message_id: MessageId,
826        error: &str,
827        user_error: Option<&UserFacingError>,
828        disclosure: Option<ErrorDisclosure>,
829    ) {
830        self.set_session_status(SessionStatus::Idle, "turn_failed")
831            .await;
832
833        self.emit_event(EventRequest::new(
834            self.session_id,
835            EventContext::turn(turn_id, input_message_id),
836            {
837                let mut data = TurnFailedData {
838                    turn_id,
839                    error: error.to_string(),
840                    error_code: None,
841                    error_fields: None,
842                    error_disclosure: disclosure.map(|mode| mode.as_str().to_string()),
843                };
844                if let Some(user_error) = user_error {
845                    user_error.apply_to_event_fields(&mut data.error_code, &mut data.error_fields);
846                }
847                data
848            },
849        ))
850        .await;
851
852        self.emit_event(EventRequest::new(
853            self.session_id,
854            EventContext::turn(turn_id, input_message_id),
855            SessionIdledData {
856                turn_id,
857                iterations: None,
858                usage: None,
859            },
860        ))
861        .await;
862    }
863
864    pub async fn waiting_for_tool_results(&self) {
865        self.set_session_status(
866            SessionStatus::WaitingForToolResults,
867            "waiting_for_tool_results",
868        )
869        .await;
870    }
871
872    pub async fn dependency_blocked(
873        &self,
874        turn_id: TurnId,
875        input_message_id: MessageId,
876        blocker: DependencyBlocker,
877    ) {
878        let user_error = UserFacingError::new(blocker.error_code())
879            .with_field(
880                "dependency",
881                match blocker {
882                    DependencyBlocker::HarnessArchived | DependencyBlocker::HarnessDeleted => {
883                        "harness"
884                    }
885                    DependencyBlocker::AgentArchived | DependencyBlocker::AgentDeleted => "agent",
886                },
887            )
888            .with_field(
889                "state",
890                match blocker {
891                    DependencyBlocker::HarnessArchived | DependencyBlocker::AgentArchived => {
892                        "archived"
893                    }
894                    DependencyBlocker::HarnessDeleted | DependencyBlocker::AgentDeleted => {
895                        "deleted"
896                    }
897                },
898            );
899        let mut error_message = Message::assistant(blocker.message());
900        let mut metadata = std::collections::HashMap::new();
901        user_error.apply_to_message_metadata(&mut metadata);
902        error_message.metadata = Some(metadata);
903
904        self.emit_event(EventRequest::new(
905            self.session_id,
906            EventContext::turn(turn_id, input_message_id),
907            OutputMessageCompletedData::new(error_message).with_user_facing_error(&user_error),
908        ))
909        .await;
910
911        self.turn_failed(
912            turn_id,
913            input_message_id,
914            blocker.message(),
915            Some(&user_error),
916        )
917        .await;
918    }
919}
920
921pub async fn detect_dependency_blocker<A: RuntimeHostAdapter>(
922    adapter: &A,
923    org_id: i64,
924    harness_id: HarnessId,
925    agent_id: Option<AgentId>,
926) -> everruns_core::error::Result<Option<DependencyBlocker>> {
927    let harness_store = adapter.harness_store(org_id);
928    let agent_store = adapter.agent_store(org_id);
929    everruns_core::detect_dependency_blocker(
930        harness_store.as_ref(),
931        agent_store.as_ref(),
932        harness_id,
933        agent_id,
934    )
935    .await
936}
937
938pub async fn execute_input_activity<A: RuntimeHostAdapter>(
939    adapter: &A,
940    org_id: i64,
941    input: InputAtomInput,
942) -> everruns_core::error::Result<InputAtomResult> {
943    // The live effort override is turn-scoped. Clear any value left by the
944    // previous turn before ReasonAtom can prefer it over this turn's message
945    // controls.
946    if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
947        handle.set(None);
948    }
949
950    RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
951        .turn_started(input.context.turn_id, input.context.input_message_id)
952        .await;
953
954    let atom = InputAtom::new(adapter.message_store());
955    atom.execute(input).await
956}
957
958/// Collect `user_prompt_submit` hooks for this turn and run them against the
959/// inbound user message text. Returns `None` when the session has no such
960/// hooks (the common case — no overhead beyond the spec collection, which is
961/// skipped early). Errors loading specs are logged and treated as "no hooks"
962/// so a hook-collection failure never blocks a turn that wasn't asking to be
963/// hooked.
964struct UserPromptHookResult {
965    decision: everruns_core::lifecycle_hooks::UserPromptDecision,
966    original_message: String,
967}
968
969async fn run_user_prompt_submit_for_turn<A: RuntimeHostAdapter>(
970    adapter: &A,
971    org_id: i64,
972    input: &ReasonInput,
973) -> everruns_core::error::Result<Option<UserPromptHookResult>> {
974    let (specs, dispatcher) = match collect_lifecycle_hook_specs(
975        adapter,
976        org_id,
977        input.context.session_id,
978        input.harness_id,
979        input.agent_id,
980    )
981    .await
982    {
983        Ok(pair) => pair,
984        Err(error) => {
985            warn!(
986                session_id = %input.context.session_id,
987                %error,
988                "failed to collect user_prompt_submit hook specs; continuing without them"
989            );
990            return Ok(None);
991        }
992    };
993    let hooks = everruns_core::lifecycle_hooks::build_turn_lifecycle_hooks(
994        &specs,
995        everruns_core::user_hook_types::HookEvent::UserPromptSubmit,
996        dispatcher,
997    );
998    if hooks.is_empty() {
999        return Ok(None);
1000    }
1001
1002    let message_text = adapter
1003        .message_store()
1004        .get(input.context.session_id, input.context.input_message_id)
1005        .await
1006        .ok()
1007        .flatten()
1008        .map(|m| m.content_to_llm_string())
1009        .unwrap_or_default();
1010
1011    let ctx = everruns_core::lifecycle_hooks::TurnHookContext {
1012        session_id: input.context.session_id,
1013        turn_id: Some(input.context.turn_id),
1014        org_id: org_public_id_from_internal(org_id).parse().ok(),
1015        agent_id: input.agent_id.map(|a| a.to_string()),
1016    };
1017    let original_message = message_text.clone();
1018    let decision =
1019        everruns_core::lifecycle_hooks::run_user_prompt_submit_hooks(&hooks, &ctx, message_text)
1020            .await;
1021    Ok(Some(UserPromptHookResult {
1022        decision,
1023        original_message,
1024    }))
1025}
1026
1027pub async fn execute_reason_activity<A: RuntimeHostAdapter>(
1028    adapter: &A,
1029    org_id: i64,
1030    input: ReasonInput,
1031) -> everruns_core::error::Result<ReasonResult> {
1032    if let Some(blocker) =
1033        detect_dependency_blocker(adapter, org_id, input.harness_id, input.agent_id).await?
1034    {
1035        RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1036            .dependency_blocked(
1037                input.context.turn_id,
1038                input.context.input_message_id,
1039                blocker,
1040            )
1041            .await;
1042        return Ok(ReasonResult {
1043            success: false,
1044            text: blocker.message().to_string(),
1045            tool_calls: vec![],
1046            has_tool_calls: false,
1047            tool_definitions: vec![],
1048            max_iterations: everruns_core::runtime_agent::default_max_iterations(),
1049            error: Some("dependency_unavailable".to_string()),
1050            user_facing_error: None,
1051            error_disclosure: None,
1052            usage: None,
1053            output_message_id: None,
1054            time_to_first_token_ms: None,
1055            response_id: None,
1056            locale: None,
1057            network_access: None,
1058            parallel_tool_calls: None,
1059        });
1060    }
1061
1062    // user_prompt_submit hook (see `specs/user-hooks.md`). Fires once per turn,
1063    // on the first reason iteration, before the LLM is consulted — the closest
1064    // choke point to "inbound user message accepted, before reason" that both
1065    // the in-process loop and the durable worker share. A `Block` aborts the
1066    // turn by reusing the same failure path as `dependency_blocked`: emit a
1067    // user-facing message + turn.failed, idle the session, and return a
1068    // non-success `ReasonResult` so no LLM/act work runs.
1069    let mut user_prompt_message_override = None;
1070    if input.iteration <= 1
1071        && let Some(hook_result) = run_user_prompt_submit_for_turn(adapter, org_id, &input).await?
1072    {
1073        match hook_result.decision {
1074            everruns_core::lifecycle_hooks::UserPromptDecision::Block {
1075                reason,
1076                user_message,
1077            } => {
1078                RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1079                    .user_prompt_blocked(
1080                        input.context.turn_id,
1081                        input.context.input_message_id,
1082                        &reason,
1083                        user_message.as_deref(),
1084                    )
1085                    .await;
1086                return Ok(ReasonResult {
1087                    success: false,
1088                    text: user_message.unwrap_or_else(|| reason.clone()),
1089                    tool_calls: vec![],
1090                    has_tool_calls: false,
1091                    tool_definitions: vec![],
1092                    max_iterations: everruns_core::runtime_agent::default_max_iterations(),
1093                    error: Some("blocked_by_user_prompt_hook".to_string()),
1094                    user_facing_error: None,
1095                    error_disclosure: None,
1096                    usage: None,
1097                    output_message_id: None,
1098                    time_to_first_token_ms: None,
1099                    response_id: None,
1100                    locale: None,
1101                    network_access: None,
1102                    parallel_tool_calls: None,
1103                });
1104            }
1105            everruns_core::lifecycle_hooks::UserPromptDecision::Continue { message } => {
1106                if message != hook_result.original_message {
1107                    user_prompt_message_override = Some(message);
1108                }
1109            }
1110        }
1111    }
1112
1113    let mut turn_context = adapter
1114        .load_turn_context(org_id, input.context.session_id)
1115        .await?;
1116    if let Some(registry) = adapter.session_task_registry() {
1117        let session_store = adapter.session_store(org_id);
1118        if let Some(tool) = report_result_tool_for_child_session(
1119            input.context.session_id,
1120            session_store.as_ref(),
1121            registry.as_ref(),
1122        )
1123        .await?
1124        {
1125            turn_context.mcp_tool_definitions.push(tool.to_definition());
1126        }
1127        if let Some(tool) = report_task_progress_tool_for_child_session(
1128            input.context.session_id,
1129            session_store.as_ref(),
1130            registry.as_ref(),
1131        )
1132        .await?
1133        {
1134            turn_context.mcp_tool_definitions.push(tool.to_definition());
1135        }
1136    }
1137
1138    let mut atom = ReasonAtom::new(
1139        adapter.harness_store(org_id),
1140        adapter.agent_store(org_id),
1141        adapter.session_store(org_id),
1142        adapter.message_store(),
1143        adapter.provider_store(org_id),
1144        adapter.capability_registry(),
1145        adapter.driver_registry(),
1146        adapter.event_emitter(),
1147    )
1148    .with_file_store(adapter.file_store());
1149    if let Some(image_resolver) = adapter.image_resolver(org_id) {
1150        atom = atom.with_image_resolver(image_resolver);
1151    }
1152    if let Some(hb) = adapter.stream_heartbeater() {
1153        atom = atom.with_stream_heartbeater(hb);
1154    }
1155    if let Some(timeout) = adapter.provider_stall_timeout() {
1156        atom = atom.with_provider_stall_timeout(timeout);
1157    }
1158    if let Some(store) = adapter.partial_stream_store() {
1159        atom = atom.with_partial_stream_store(store);
1160    }
1161    if let Some(store) = adapter.durable_tool_result_store() {
1162        atom = atom.with_durable_tool_result_store(store);
1163    }
1164    if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
1165        atom = atom.with_reasoning_effort_handle(handle);
1166    }
1167    if let Some(utility_llm_service) = adapter.utility_llm_service() {
1168        atom = atom.with_utility_llm_service(utility_llm_service);
1169    }
1170    // Schedule store powers the `usage_limit_auto_continue` capability, which
1171    // schedules a continuation after a provider usage limit resets.
1172    if let Some(schedule_store) = adapter.schedule_store(org_id) {
1173        atom = atom.with_schedule_store(schedule_store);
1174    }
1175
1176    let input = ReasonInput {
1177        mcp_tool_definitions: turn_context.mcp_tool_definitions,
1178        ..input
1179    };
1180
1181    if let Some(message_override) = user_prompt_message_override {
1182        let mut assembled = assemble_turn_context(
1183            adapter.harness_store(org_id).as_ref(),
1184            adapter.agent_store(org_id).as_ref(),
1185            adapter.session_store(org_id).as_ref(),
1186            adapter.message_store().as_ref(),
1187            adapter.provider_store(org_id).as_ref(),
1188            &adapter.capability_registry(),
1189            input.context.session_id,
1190            input.harness_id,
1191            input.agent_id,
1192            &input.mcp_tool_definitions,
1193            Some(adapter.file_store()),
1194        )
1195        .await?;
1196
1197        let message = assembled
1198            .messages
1199            .iter_mut()
1200            .find(|message| message.id == input.context.input_message_id)
1201            .ok_or_else(|| {
1202                everruns_core::error::AgentLoopError::config(
1203                    "user_prompt_submit mutation: input message not found in assembled context",
1204                )
1205            })?;
1206
1207        // user_prompt_submit mutations are enforcement controls for the
1208        // provider-bound prompt. Apply them to the assembled context only
1209        // so persisted user history remains an audit record of the input.
1210        // Preserve non-text parts (images, files); replace only text parts.
1211        message
1212            .content
1213            .retain(|part| !matches!(part, ContentPart::Text(_)));
1214        message
1215            .content
1216            .insert(0, ContentPart::text(message_override));
1217
1218        return atom.execute_with_assembled_context(input, assembled).await;
1219    }
1220
1221    atom.execute(input).await
1222}
1223
1224pub async fn execute_act_activity<A: RuntimeHostAdapter>(
1225    adapter: &A,
1226    input: ActInput,
1227) -> everruns_core::error::Result<ActResult> {
1228    let org_id = input.org_id.ok_or_else(|| {
1229        everruns_core::error::AgentLoopError::config(
1230            "ActInput.org_id must be set for runtime host execution",
1231        )
1232    })?;
1233
1234    if let Some(blocker) =
1235        detect_dependency_blocker(adapter, org_id, input.harness_id, input.agent_id).await?
1236    {
1237        RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1238            .dependency_blocked(
1239                input.context.turn_id,
1240                input.context.input_message_id,
1241                blocker,
1242            )
1243            .await;
1244        return Ok(ActResult {
1245            results: vec![],
1246            completed: true,
1247            success_count: 0,
1248            error_count: 1,
1249            waiting_for_tool_results: false,
1250            blocked: true,
1251            client_tool_calls: vec![],
1252            client_tool_definitions: vec![],
1253        });
1254    }
1255
1256    let execution_capabilities = load_execution_capabilities(
1257        adapter,
1258        org_id,
1259        input.context.session_id,
1260        input.harness_id,
1261        input.agent_id,
1262        input.locale.clone(),
1263        input.blueprint_id.as_deref(),
1264    )
1265    .await?;
1266    let mut tool_registry = execution_capabilities.tool_registry;
1267
1268    if input
1269        .tool_definitions
1270        .iter()
1271        .any(|definition| definition.name() == "report_result")
1272        && let Some(registry) = adapter.session_task_registry()
1273        && let Some(tool) = report_result_tool_for_child_session(
1274            input.context.session_id,
1275            adapter.session_store(org_id).as_ref(),
1276            registry.as_ref(),
1277        )
1278        .await?
1279    {
1280        tool_registry.register_boxed(Box::new(tool.with_file_store(adapter.file_store())));
1281    }
1282    if input
1283        .tool_definitions
1284        .iter()
1285        .any(|definition| definition.name() == "report_task_progress")
1286        && let Some(registry) = adapter.session_task_registry()
1287        && let Some(tool) = report_task_progress_tool_for_child_session(
1288            input.context.session_id,
1289            adapter.session_store(org_id).as_ref(),
1290            registry.as_ref(),
1291        )
1292        .await?
1293    {
1294        tool_registry.register_boxed(Box::new(tool));
1295    }
1296
1297    // Register the session's MCP tools as first-class registry tools, so they
1298    // execute through the regular `ToolExecutor` path and are visible to
1299    // everything that introspects the registry (spawn_background, tool_search,
1300    // openai_tool_search namespaces, ...). The turn's tool definitions already
1301    // include the discovered MCP tools, so no re-discovery is needed; the host's
1302    // MCP executor supplies execution (specs/runtime-mcp.md D5).
1303    // The MCP invoker is reused below for the guardrails `mcp` check, which
1304    // delegates a guardrail decision to an external endpoint over the same
1305    // scoped-MCP client/auth (specs/guardrails.md).
1306    let mut mcp_invoker: Option<Arc<dyn everruns_core::McpToolInvoker>> = None;
1307    if let Some(mcp) = adapter.mcp_executor(org_id, input.context.session_id).await {
1308        let invoker: Arc<dyn everruns_core::McpToolInvoker> = mcp;
1309        for tool in everruns_core::build_mcp_proxy_tools(&input.tool_definitions, invoker.clone()) {
1310            tool_registry.register_boxed(tool);
1311        }
1312        mcp_invoker = Some(Arc::new(everruns_core::ScopedMcpToolInvoker::new(
1313            &input.tool_definitions,
1314            invoker,
1315        )));
1316    }
1317
1318    let builtin_tool_registry = Arc::new(tool_registry.clone());
1319    let executor: Arc<dyn everruns_core::traits::ToolExecutor> = Arc::new(tool_registry);
1320
1321    let mut atom =
1322        ActAtom::with_file_store(executor, adapter.event_emitter(), adapter.file_store())
1323            .with_session_store(adapter.session_store(org_id))
1324            .with_session_mutator(adapter.session_mutator(org_id))
1325            .with_agent_store(adapter.agent_store(org_id))
1326            .with_tool_registry(builtin_tool_registry)
1327            .with_org_id(
1328                org_public_id_from_internal(org_id)
1329                    .parse()
1330                    .expect("internal org id converts to valid public org id"),
1331            )
1332            .with_capability_registry(adapter.capability_registry())
1333            .with_post_tool_hooks(execution_capabilities.post_tool_hooks)
1334            .with_pre_tool_hooks(execution_capabilities.pre_tool_hooks)
1335            .with_tool_call_hooks(execution_capabilities.tool_call_hooks)
1336            .with_subagent_nesting_policy(execution_capabilities.subagent_nesting_policy);
1337
1338    if let Some(storage_store) = adapter.storage_store() {
1339        atom = atom.with_storage_store(storage_store);
1340    }
1341    if let Some(knowledge_store) = adapter.knowledge_store() {
1342        atom = atom.with_knowledge_store(knowledge_store);
1343    }
1344    if let Some(image_store) = adapter.image_artifact_store(org_id) {
1345        atom = atom.with_image_store(image_store);
1346    }
1347    if let Some(provider_credential_store) = adapter.provider_credential_store(org_id) {
1348        atom = atom.with_provider_credential_store(provider_credential_store);
1349    }
1350    if let Some(utility_llm_service) = adapter.utility_llm_service() {
1351        atom = atom.with_utility_llm_service(utility_llm_service);
1352    }
1353    if let Some(invoker) = mcp_invoker {
1354        atom = atom.with_mcp_invoker(invoker);
1355    }
1356    if let Some(egress_service) = adapter.egress_service() {
1357        atom = atom.with_egress_service(egress_service);
1358    }
1359    if let Some(connection_resolver) = adapter.connection_resolver() {
1360        atom = atom.with_connection_resolver(connection_resolver);
1361    }
1362    if let Some(sqldb_store) = adapter.sqldb_store() {
1363        atom = atom.with_sqldb_store(sqldb_store);
1364    }
1365    if let Some(leased_resource_store) = adapter.leased_resource_store() {
1366        atom = atom.with_leased_resource_store(leased_resource_store);
1367    }
1368    if let Some(registry) = adapter.session_resource_registry() {
1369        atom = atom.with_session_resource_registry(registry);
1370    }
1371    if let Some(registry) = adapter.session_task_registry() {
1372        atom = atom.with_session_task_registry(registry);
1373    }
1374    if let Some(schedule_store) = adapter.schedule_store(org_id) {
1375        atom = atom.with_schedule_store(schedule_store);
1376    }
1377    if let Some(platform_store) = adapter.platform_store(org_id, input.context.session_id) {
1378        atom = atom.with_platform_store(platform_store);
1379    }
1380    if let Some(knowledge_index_search) = adapter.knowledge_index_search(org_id) {
1381        atom = atom.with_knowledge_index_search(knowledge_index_search);
1382    }
1383    if let Some(budget_checker) = adapter.budget_checker(org_id, input.agent_id) {
1384        atom = atom.with_budget_checker(budget_checker);
1385    }
1386    if let Some(payment_authority) = adapter.payment_authority(org_id, input.agent_id) {
1387        atom = atom.with_payment_authority(payment_authority);
1388    }
1389    if let Some(limiter) = adapter.outbound_tool_rate_limiter(org_id) {
1390        atom = atom.with_outbound_tool_rate_limiter(limiter);
1391    }
1392    if let Some(store) = adapter.durable_tool_result_store() {
1393        atom = atom.with_durable_tool_result_store(store);
1394    }
1395    if let Some(store) = adapter.subagent_spawn_store() {
1396        atom = atom.with_subagent_spawn_store(store);
1397    }
1398    if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
1399        atom = atom.with_reasoning_effort_handle(handle);
1400    }
1401
1402    atom.execute(input).await
1403}