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