1use crate::{SessionMutator, SessionMutatorExt};
7use async_trait::async_trait;
8use everruns_core::capabilities::{
9 Capability, SystemPromptContext, collect_capabilities_with_configs,
10};
11use everruns_core::events::{
12 EventContext, EventRequest, OutputMessageCompletedData, SessionActivatedData, SessionIdledData,
13 SessionModelChangedData, TurnCompletedData, TurnFailedData, TurnStartedData,
14};
15use everruns_core::message::{ContentPart, Message, MessageRole};
16use everruns_core::message_retriever::MessageRetriever;
17use everruns_core::runtime_context::AssembledTurnContext;
18use everruns_core::session::SessionExecutionState;
19use everruns_core::{
20 CapabilityRegistry, CapabilityStatus, ClassifierService, DependencyBlocker, EgressService,
21 ResolvedExecutionSnapshot, TokenUsage, ToolRegistry, UtilityLlmService,
22 org_public_id_from_internal, resolve_runtime_capabilities,
23};
24use everruns_core::{
25 connection_services::ProviderCredentialStore, connection_services::UserConnectionResolver,
26 delegation_services::SessionCreationAuthority, event_emitter::EventEmitter,
27 execution_loading::AgentStore, execution_loading::HarnessStore,
28 execution_loading::SessionStore, file_services::FileResolver,
29 image_services::ImageArtifactStore, image_services::ImageResolver,
30 provider_resolution::ProviderStore, session_files::SessionFileSystem,
31 session_services::LeasedResourceStore, session_services::SessionResourceRegistry,
32 session_services::SessionScheduleStore, session_services::SessionStorageStore,
33 tool_context::ToolContextServices, tool_execution::BudgetChecker,
34 tool_execution::PaymentAuthority,
35};
36use everruns_engine::{
37 ActAtom, ActInput, ActResult, InputAtom, InputAtomInput, InputAtomResult, ReasonAtom,
38 ReasonInput, ReasonResult,
39};
40use everruns_provider::driver_registry::DriverRegistry;
41use everruns_provider::tool_types::ToolDefinition;
42use everruns_provider::typed_id::{AgentId, HarnessId, MessageId, ModelId, SessionId, TurnId};
43use everruns_provider::user_facing_error::{ErrorDisclosure, UserFacingError};
44use std::sync::Arc;
45use tracing::warn;
46
47struct MessageFilterOnlyCapability(Arc<dyn Capability>);
50
51impl Capability for MessageFilterOnlyCapability {
52 fn id(&self) -> &str {
53 self.0.id()
54 }
55
56 fn aliases(&self) -> Vec<&'static str> {
57 self.0.aliases()
58 }
59
60 fn name(&self) -> &str {
61 self.0.name()
62 }
63
64 fn description(&self) -> &str {
65 self.0.description()
66 }
67
68 fn status(&self) -> CapabilityStatus {
69 self.0.status()
70 }
71
72 fn message_filter_provider(
73 &self,
74 ) -> Option<Arc<dyn everruns_core::message_filter::MessageFilterProvider>> {
75 self.0.message_filter_provider()
76 }
77
78 fn message_filter_config(
79 &self,
80 config: &serde_json::Value,
81 compaction_enabled: bool,
82 ) -> serde_json::Value {
83 self.0.message_filter_config(config, compaction_enabled)
84 }
85}
86
87#[cfg(feature = "bashkit")]
88fn bash_hook_dispatcher(
89 file_store: Arc<dyn SessionFileSystem>,
90) -> Arc<dyn everruns_core::hook_executor::BashHookDispatcher> {
91 Arc::new(everruns_integrations_bashkit::BashkitShellHookDispatcher::new(file_store))
92}
93
94#[cfg(not(feature = "bashkit"))]
95fn bash_hook_dispatcher(
96 _file_store: Arc<dyn SessionFileSystem>,
97) -> Arc<dyn everruns_core::hook_executor::BashHookDispatcher> {
98 struct DisabledDispatcher;
99
100 #[async_trait]
101 impl everruns_core::hook_executor::BashHookDispatcher for DisabledDispatcher {
102 async fn dispatch(
103 &self,
104 _payload: &everruns_core::hook_executor::HookPayload,
105 _command: &str,
106 _extra_env: &std::collections::BTreeMap<String, String>,
107 _opts: &everruns_core::hook_executor::ExecutorOpts,
108 ) -> std::result::Result<everruns_core::hook_executor::BashExecOutput, String> {
109 Err("bash hooks require the everruns-host `bashkit` feature".to_string())
110 }
111 }
112
113 Arc::new(DisabledDispatcher)
114}
115
116#[derive(Debug, Clone)]
123pub struct ResolvedTurnInputs {
124 pub snapshot: ResolvedExecutionSnapshot,
126 pub messages: Vec<Message>,
128 pub mcp_tool_definitions: Vec<ToolDefinition>,
130}
131
132#[async_trait]
143pub trait RuntimeHostAdapter: Send + Sync + Clone + 'static {
144 fn turn_cancellation(&self) -> Option<tokio::sync::watch::Receiver<bool>> {
146 None
147 }
148 async fn set_session_status(
151 &self,
152 org_id: i64,
153 session_id: SessionId,
154 status: SessionExecutionState,
155 ) -> everruns_provider::error::Result<()>;
156
157 async fn load_resolved_turn(
165 &self,
166 org_id: i64,
167 session_id: SessionId,
168 ) -> everruns_provider::error::Result<ResolvedTurnInputs>;
169
170 fn capability_registry(&self) -> CapabilityRegistry;
171
172 fn driver_registry(&self) -> DriverRegistry;
173
174 fn harness_store(&self, org_id: i64) -> Arc<dyn HarnessStore>;
175
176 fn agent_store(&self, org_id: i64) -> Arc<dyn AgentStore>;
177
178 fn session_store(&self, org_id: i64) -> Arc<dyn SessionStore>;
179
180 fn session_mutator(&self, org_id: i64) -> Arc<dyn SessionMutator>;
181
182 fn provider_store(&self, org_id: i64) -> Arc<dyn ProviderStore>;
183
184 fn message_store(&self) -> Arc<dyn MessageRetriever>;
185
186 fn native_async_store(
187 &self,
188 ) -> Option<Arc<dyn everruns_core::native_async_store::NativeAsyncStore>> {
189 None
190 }
191
192 fn compaction_checkpoint_store(
193 &self,
194 ) -> Option<Arc<dyn everruns_core::CompactionCheckpointStore>> {
195 None
196 }
197
198 fn event_emitter(&self) -> Arc<dyn EventEmitter>;
199
200 fn file_store(&self) -> Arc<dyn SessionFileSystem>;
201
202 fn image_resolver(&self, _org_id: i64) -> Option<Arc<dyn ImageResolver>> {
203 None
204 }
205
206 fn file_resolver(&self, _org_id: i64) -> Option<Arc<dyn FileResolver>> {
207 None
208 }
209
210 fn image_artifact_store(&self, _org_id: i64) -> Option<Arc<dyn ImageArtifactStore>> {
211 None
212 }
213
214 fn provider_credential_store(&self, _org_id: i64) -> Option<Arc<dyn ProviderCredentialStore>> {
215 None
216 }
217
218 fn utility_llm_service(&self) -> Option<Arc<dyn UtilityLlmService>> {
219 None
220 }
221
222 fn classifier(&self) -> Option<Arc<dyn ClassifierService>> {
224 None
225 }
226
227 fn egress_service(&self) -> Option<Arc<dyn EgressService>> {
228 None
229 }
230
231 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
232 None
233 }
234
235 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
236 None
237 }
238
239 fn tool_context_extensions(
241 &self,
242 _org_id: i64,
243 _session_id: SessionId,
244 ) -> everruns_core::tool_context::ToolContextExtensions {
245 Default::default()
246 }
247
248 fn subagent_delegate(
250 &self,
251 _org_id: i64,
252 _session_id: SessionId,
253 ) -> Option<Arc<dyn everruns_core::subagent_delegation::SubagentSessionDelegate>> {
254 None
255 }
256
257 fn tool_augmentor(&self) -> Option<Arc<dyn crate::HostToolAugmentor>> {
259 None
260 }
261
262 fn leased_resource_store(&self) -> Option<Arc<dyn LeasedResourceStore>> {
263 None
264 }
265
266 fn session_resource_registry(&self) -> Option<Arc<dyn SessionResourceRegistry>> {
267 None
268 }
269
270 fn session_task_registry(
271 &self,
272 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
273 None
274 }
275
276 fn schedule_store(&self, _org_id: i64) -> Option<Arc<dyn SessionScheduleStore>> {
277 None
278 }
279
280 fn budget_checker(
281 &self,
282 _org_id: i64,
283 _agent_id: Option<AgentId>,
284 ) -> Option<Arc<dyn BudgetChecker>> {
285 None
286 }
287
288 fn payment_authority(
289 &self,
290 _org_id: i64,
291 _agent_id: Option<AgentId>,
292 ) -> Option<Arc<dyn PaymentAuthority>> {
293 None
294 }
295
296 fn session_creation_authority(
297 &self,
298 _org_id: i64,
299 _session_id: SessionId,
300 ) -> Option<Arc<dyn SessionCreationAuthority>> {
301 None
302 }
303
304 fn outbound_tool_rate_limiter(
307 &self,
308 _org_id: i64,
309 ) -> Option<Arc<dyn everruns_core::tool_execution::OutboundToolRateLimiter>> {
310 None
311 }
312
313 fn durable_tool_result_store(
316 &self,
317 ) -> Option<Arc<dyn everruns_core::durability::DurableToolResultStore>> {
318 None
319 }
320
321 fn subagent_spawn_store(
324 &self,
325 ) -> Option<Arc<dyn everruns_core::delegation_services::SubagentSpawnStore>> {
326 None
327 }
328
329 fn stream_heartbeater(&self) -> Option<Arc<dyn everruns_core::durability::StreamHeartbeater>> {
332 None
333 }
334
335 fn partial_stream_store(
338 &self,
339 ) -> Option<Arc<dyn everruns_core::durability::PartialStreamStore>> {
340 None
341 }
342
343 fn reasoning_effort_handle(
352 &self,
353 _session_id: SessionId,
354 ) -> Option<everruns_core::tool_context::ReasoningEffortHandle> {
355 None
356 }
357
358 fn provider_stall_timeout(&self) -> Option<std::time::Duration> {
361 None
362 }
363
364 fn provider_retry_config(&self) -> Option<everruns_provider::llm_retry::LlmRetryConfig> {
367 None
368 }
369
370 async fn mcp_executor(
374 &self,
375 _org_id: i64,
376 _session_id: SessionId,
377 ) -> Option<Arc<dyn everruns_core::McpToolInvoker>> {
378 None
379 }
380}
381
382struct RuntimeExecutionCapabilities {
383 tool_registry: ToolRegistry,
384 post_tool_hooks: Vec<Arc<dyn everruns_core::tool_hooks::PostToolExecHook>>,
385 pre_tool_hooks: Vec<Arc<dyn everruns_core::tool_hooks::PreToolUseHook>>,
386 tool_call_hooks: Vec<Arc<dyn everruns_core::ToolCallHook>>,
387 subagent_nesting_policy: everruns_core::delegation_services::SubagentNestingPolicy,
388}
389
390fn subagent_nesting_policy_from_configs(
391 resolved_capability_configs: &[everruns_capability::CapabilityRef],
392) -> everruns_core::delegation_services::SubagentNestingPolicy {
393 let subagents_config = resolved_capability_configs
394 .iter()
395 .find(|config| config.capability_id() == "subagents");
396
397 let configured_depth = subagents_config
398 .and_then(|config| {
399 config
400 .config_value()
401 .get("max_subagent_depth")
402 .or_else(|| config.config_value().get("max_depth"))
403 })
404 .and_then(|value| value.as_u64())
405 .and_then(|value| u32::try_from(value).ok());
406 let configured_max_active = subagents_config
407 .and_then(|config| {
408 config
409 .config_value()
410 .get("max_active_descendant_tasks")
411 .or_else(|| config.config_value().get("max_concurrent_descendant_tasks"))
412 })
413 .and_then(|value| value.as_u64())
414 .and_then(|value| u32::try_from(value).ok());
415 let configured_max_total = subagents_config
416 .and_then(|config| config.config_value().get("max_total_descendant_tasks"))
417 .and_then(|value| value.as_u64())
418 .and_then(|value| u32::try_from(value).ok());
419 let configured_max_active_detached = subagents_config
420 .and_then(|config| config.config_value().get("max_active_detached_tasks"))
421 .and_then(|value| value.as_u64())
422 .and_then(|value| u32::try_from(value).ok());
423 let configured_max_total_detached = subagents_config
424 .and_then(|config| config.config_value().get("max_total_detached_tasks"))
425 .and_then(|value| value.as_u64())
426 .and_then(|value| u32::try_from(value).ok());
427
428 everruns_core::delegation_services::SubagentNestingPolicy::default()
429 .with_agent_override(configured_depth)
430 .with_agent_task_caps_override(configured_max_active, configured_max_total)
431 .with_agent_detached_task_caps_override(
432 configured_max_active_detached,
433 configured_max_total_detached,
434 )
435}
436
437fn finalize_specs_from_configs(
447 resolved_capability_configs: &[everruns_capability::CapabilityRef],
448 capability_registry: &CapabilityRegistry,
449 tool_augmentor: Option<&dyn crate::HostToolAugmentor>,
450) -> Vec<everruns_core::user_hook_types::UserHookSpec> {
451 let mut hook_contributions: Vec<(String, Vec<everruns_core::user_hook_types::UserHookSpec>)> =
452 Vec::new();
453 let mut disabled_contributions: Vec<String> = Vec::new();
454 for config in resolved_capability_configs {
455 let Some(capability) = capability_registry.get(config.capability_id()) else {
456 continue;
457 };
458 let specs = capability.user_hooks_with_config(config.config_value());
459 if !specs.is_empty() {
460 hook_contributions.push((config.capability_id().to_string(), specs));
461 }
462 if let Some(augmentor) = tool_augmentor {
463 disabled_contributions.extend(
464 augmentor
465 .disabled_hook_contributions(config.capability_id(), config.config_value()),
466 );
467 }
468 }
469 everruns_core::hook_adapter::finalize_hook_specs(hook_contributions, &disabled_contributions)
470}
471
472async fn collect_lifecycle_hook_specs<A: RuntimeHostAdapter>(
477 adapter: &A,
478 org_id: i64,
479 session_id: SessionId,
480 harness_id: HarnessId,
481 agent_id: Option<AgentId>,
482) -> everruns_provider::error::Result<(
483 Vec<everruns_core::user_hook_types::UserHookSpec>,
484 Arc<dyn everruns_core::hook_executor::BashHookDispatcher>,
485)> {
486 let capability_registry = adapter.capability_registry();
487 let harness = adapter
488 .harness_store(org_id)
489 .get_harness(harness_id)
490 .await?
491 .ok_or_else(|| everruns_provider::error::AgentLoopError::harness_not_found(harness_id))?;
492 let session = adapter
493 .session_store(org_id)
494 .get_session(session_id)
495 .await?
496 .ok_or_else(|| everruns_provider::error::AgentLoopError::session_not_found(session_id))?;
497 let agent = match agent_id {
498 Some(agent_id) => adapter.agent_store(org_id).get_agent(agent_id).await?,
499 None => None,
500 };
501 let resolved =
502 resolve_runtime_capabilities(&harness, agent.as_ref(), &session, &capability_registry);
503 let tool_augmentor = adapter.tool_augmentor();
504 let specs = finalize_specs_from_configs(
505 &resolved.resolved_capability_configs,
506 &capability_registry,
507 tool_augmentor.as_deref(),
508 );
509 let dispatcher = bash_hook_dispatcher(adapter.file_store());
510 Ok((specs, dispatcher))
511}
512
513async fn load_execution_capabilities<A: RuntimeHostAdapter>(
514 adapter: &A,
515 org_id: i64,
516 session_id: SessionId,
517 harness_id: HarnessId,
518 agent_id: Option<AgentId>,
519 locale: Option<String>,
520 blueprint_id: Option<&str>,
521) -> everruns_provider::error::Result<RuntimeExecutionCapabilities> {
522 let capability_registry = adapter.capability_registry();
523 if let Some(blueprint_id) = blueprint_id {
524 let mut registry = ToolRegistry::with_defaults();
525 #[cfg(feature = "builtins")]
526 everruns_builtins::register_default_tools(&mut registry);
527 let blueprint = capability_registry.blueprint(blueprint_id).ok_or_else(|| {
528 everruns_provider::error::AgentLoopError::config(format!(
529 "Blueprint \"{blueprint_id}\" not found in registry"
530 ))
531 })?;
532 for tool in blueprint.tools {
533 registry.register_boxed(tool);
534 }
535 return Ok(RuntimeExecutionCapabilities {
536 tool_registry: registry,
537 post_tool_hooks: Vec::new(),
538 pre_tool_hooks: Vec::new(),
539 tool_call_hooks: Vec::new(),
540 subagent_nesting_policy:
541 everruns_core::delegation_services::SubagentNestingPolicy::default(),
542 });
543 }
544
545 let harness = adapter
546 .harness_store(org_id)
547 .get_harness(harness_id)
548 .await?
549 .ok_or_else(|| everruns_provider::error::AgentLoopError::harness_not_found(harness_id))?;
550
551 let session = adapter
552 .session_store(org_id)
553 .get_session(session_id)
554 .await?
555 .ok_or_else(|| everruns_provider::error::AgentLoopError::session_not_found(session_id))?;
556
557 let agent_store = adapter.agent_store(org_id);
558 let agent =
559 match agent_id {
560 Some(agent_id) => Some(agent_store.get_agent(agent_id).await?.ok_or_else(|| {
561 everruns_provider::error::AgentLoopError::agent_not_found(agent_id)
562 })?),
563 None => None,
564 };
565
566 let resolved =
567 resolve_runtime_capabilities(&harness, agent.as_ref(), &session, &capability_registry);
568 let prompt_ctx = SystemPromptContext {
575 session_id,
576 locale: locale.or(session.locale.clone()),
577 file_store: Some(everruns_core::scoped_prompt_file_store(
584 adapter.file_store(),
585 session.workspace_id,
586 )),
587 model: None,
588 session_storage: None,
589 };
590 let collected = collect_capabilities_with_configs(
591 &resolved.resolved_capability_configs,
592 &capability_registry,
593 &prompt_ctx,
594 )
595 .await;
596
597 let mut registry = ToolRegistry::with_defaults();
598 #[cfg(feature = "builtins")]
599 everruns_builtins::register_default_tools(&mut registry);
600 for tool in collected.tools {
601 registry.register_boxed(tool);
602 }
603
604 let mut post_tool_hooks: Vec<Arc<dyn everruns_core::tool_hooks::PostToolExecHook>> = resolved
609 .resolved_capability_configs
610 .iter()
611 .flat_map(|config| {
612 capability_registry
613 .get(config.capability_id())
614 .filter(|capability| capability.status().is_active())
615 .map(|capability| {
616 capability.post_tool_exec_hooks_with_config(config.config_value())
617 })
618 .unwrap_or_default()
619 })
620 .collect();
621 post_tool_hooks.sort_by_key(|hook| hook.priority());
624
625 let tool_augmentor = adapter.tool_augmentor();
632 let user_hook_specs = finalize_specs_from_configs(
633 &resolved.resolved_capability_configs,
634 &capability_registry,
635 tool_augmentor.as_deref(),
636 );
637 if user_hook_specs
642 .iter()
643 .any(|spec| spec.event == everruns_core::user_hook_types::HookEvent::UserPromptSubmit)
644 {
645 registry.unregister("query_history");
646 }
647 let mut pre_tool_hooks: Vec<Arc<dyn everruns_core::tool_hooks::PreToolUseHook>> = resolved
650 .resolved_capability_configs
651 .iter()
652 .flat_map(|config| {
653 capability_registry
654 .get(config.capability_id())
655 .filter(|capability| capability.status().is_active())
656 .map(|capability| capability.pre_tool_use_hooks_with_config(config.config_value()))
657 .unwrap_or_default()
658 })
659 .collect();
660 if !user_hook_specs.is_empty() {
661 let dispatcher = bash_hook_dispatcher(adapter.file_store());
662 post_tool_hooks.extend(everruns_core::hook_adapter::build_post_tool_use_hooks(
663 &user_hook_specs,
664 dispatcher.clone(),
665 ));
666 pre_tool_hooks.extend(everruns_core::hook_adapter::build_pre_tool_use_hooks(
667 &user_hook_specs,
668 dispatcher,
669 ));
670 }
671
672 let tool_call_hooks = collected.tool_call_hooks;
683
684 Ok(RuntimeExecutionCapabilities {
685 tool_registry: registry,
686 post_tool_hooks,
687 pre_tool_hooks,
688 tool_call_hooks,
689 subagent_nesting_policy: subagent_nesting_policy_from_configs(
690 &resolved.resolved_capability_configs,
691 ),
692 })
693}
694
695fn runtime_tool_context_services<A: RuntimeHostAdapter>(
696 adapter: &A,
697 org_id: i64,
698 session_id: SessionId,
699 agent_id: Option<AgentId>,
700 tool_registry: Option<Arc<ToolRegistry>>,
701 mcp_invoker: Option<Arc<dyn everruns_core::McpToolInvoker>>,
702 subagent_nesting_policy: everruns_core::delegation_services::SubagentNestingPolicy,
703) -> ToolContextServices {
704 let extensions = {
705 let mut extensions = adapter.tool_context_extensions(org_id, session_id);
706 extensions.insert(Arc::new(SessionMutatorExt(adapter.session_mutator(org_id))));
707 extensions
708 };
709 ToolContextServices {
710 file_store: Some(adapter.file_store()),
711 storage_store: adapter.storage_store(),
712 image_store: adapter.image_artifact_store(org_id),
713 provider_credential_store: adapter.provider_credential_store(org_id),
714 utility_llm_service: adapter.utility_llm_service(),
715 classifier: adapter.classifier(),
716 mcp_invoker,
717 egress_service: adapter.egress_service(),
718 message_retriever: Some(adapter.message_store()),
719 session_store: Some(adapter.session_store(org_id)),
720 agent_store: Some(adapter.agent_store(org_id)),
721 connection_resolver: adapter.connection_resolver(),
722 schedule_store: adapter.schedule_store(org_id),
723 subagent_delegate: adapter.subagent_delegate(org_id, session_id),
724 extensions,
725 leased_resource_store: adapter.leased_resource_store(),
726 session_resource_registry: adapter.session_resource_registry(),
727 session_task_registry: adapter.session_task_registry(),
728 event_emitter: Some(adapter.event_emitter()),
729 capability_registry: Some(adapter.capability_registry()),
730 tool_registry,
731 org_id: Some(
732 org_public_id_from_internal(org_id)
733 .parse()
734 .expect("internal org id converts to valid public org id"),
735 ),
736 network_access: None,
737 budget_checker: adapter.budget_checker(org_id, agent_id),
738 payment_authority: adapter.payment_authority(org_id, agent_id),
739 session_creation_authority: adapter.session_creation_authority(org_id, session_id),
740 subagent_spawn_store: adapter.subagent_spawn_store(),
741 subagent_nesting_policy,
742 reasoning_effort_handle: adapter.reasoning_effort_handle(session_id),
743 }
744}
745
746#[derive(Debug, Default)]
749struct TurnAgentIdentity {
750 id: Option<AgentId>,
751 name: Option<String>,
752 description: Option<String>,
753}
754
755pub struct RuntimeSessionLifecycle<A: RuntimeHostAdapter> {
756 adapter: A,
757 org_id: i64,
758 session_id: SessionId,
759}
760
761impl<A: RuntimeHostAdapter> RuntimeSessionLifecycle<A> {
762 pub fn new(adapter: A, org_id: i64, session_id: SessionId) -> Self {
763 Self {
764 adapter,
765 org_id,
766 session_id,
767 }
768 }
769
770 async fn set_session_status(
771 &self,
772 status: SessionExecutionState,
773 _action: &'static str,
774 ) -> everruns_provider::error::Result<()> {
775 self.adapter
776 .set_session_status(self.org_id, self.session_id, status)
777 .await
778 }
779
780 async fn emit_event(&self, request: EventRequest) -> everruns_provider::error::Result<()> {
781 self.adapter.event_emitter().emit(request).await.map(|_| ())
782 }
783
784 pub async fn turn_started(
785 &self,
786 turn_id: TurnId,
787 input_message_id: MessageId,
788 ) -> everruns_provider::error::Result<()> {
789 let input_content = self
790 .adapter
791 .message_store()
792 .get(self.session_id, input_message_id)
793 .await
794 .ok()
795 .flatten()
796 .map(|message| message.content_to_llm_string());
797
798 self.set_session_status(SessionExecutionState::Active, "turn_started")
799 .await?;
800
801 self.emit_event(EventRequest::new(
802 self.session_id,
803 EventContext::turn(turn_id, input_message_id),
804 SessionActivatedData {
805 turn_id,
806 input_message_id,
807 },
808 ))
809 .await?;
810
811 let agent = self.agent_identity().await;
812 self.emit_event(EventRequest::new(
813 self.session_id,
814 EventContext::turn(turn_id, input_message_id),
815 TurnStartedData {
816 turn_id,
817 input_message_id,
818 input_content,
819 agent_id: agent.id,
820 agent_name: agent.name,
821 agent_description: agent.description,
822 },
823 ))
824 .await?;
825 Ok(())
826 }
827
828 async fn agent_identity(&self) -> TurnAgentIdentity {
832 let agent_id = self
833 .adapter
834 .session_store(self.org_id)
835 .get_session(self.session_id)
836 .await
837 .ok()
838 .flatten()
839 .and_then(|session| session.agent_id);
840 let Some(agent_id) = agent_id else {
841 return TurnAgentIdentity::default();
842 };
843 let agent = self
844 .adapter
845 .agent_store(self.org_id)
846 .get_agent(agent_id)
847 .await
848 .ok()
849 .flatten();
850 TurnAgentIdentity {
851 id: Some(agent_id),
852 name: agent
855 .as_ref()
856 .map(|a| a.display_name.clone().unwrap_or_else(|| a.name.clone())),
857 description: agent.and_then(|a| a.description),
858 }
859 }
860
861 pub async fn emit_turn_completed(
862 &self,
863 input_message_id: MessageId,
864 data: TurnCompletedData,
865 ) -> everruns_provider::error::Result<()> {
866 let turn_id = data.turn_id;
867 self.emit_event(EventRequest::new(
868 self.session_id,
869 EventContext::turn(turn_id, input_message_id),
870 data,
871 ))
872 .await
873 }
874
875 pub async fn emit_session_idled(
876 &self,
877 turn_id: TurnId,
878 input_message_id: MessageId,
879 iterations: Option<u32>,
880 usage: Option<TokenUsage>,
881 ) -> everruns_provider::error::Result<()> {
882 self.set_session_status(SessionExecutionState::Idle, "emit_session_idled")
883 .await?;
884
885 self.emit_event(EventRequest::new(
886 self.session_id,
887 EventContext::turn(turn_id, input_message_id),
888 SessionIdledData {
889 turn_id,
890 iterations,
891 usage,
892 },
893 ))
894 .await
895 }
896
897 pub async fn turn_completed(
898 &self,
899 turn_id: TurnId,
900 input_message_id: MessageId,
901 iterations: u32,
902 usage: Option<TokenUsage>,
903 input_content: Option<String>,
904 ) -> everruns_provider::error::Result<()> {
905 self.emit_turn_completed(
906 input_message_id,
907 TurnCompletedData {
908 turn_id,
909 iterations,
910 duration_ms: None,
911 usage: usage.clone(),
912 input_content,
913 final_message_id: None,
914 final_answer_preview: None,
915 time_to_first_token_ms: None,
916 tool_call_count: None,
917 llm_call_count: None,
918 status: Some("completed".to_string()),
919 },
920 )
921 .await?;
922 self.emit_session_idled(turn_id, input_message_id, Some(iterations), usage)
923 .await
924 }
925
926 pub async fn turn_sealed(
933 &self,
934 turn_id: TurnId,
935 input_message_id: MessageId,
936 reason: &str,
937 iterations: u32,
938 usage: Option<TokenUsage>,
939 ) -> everruns_provider::error::Result<()> {
940 let context = EventContext::turn(turn_id, input_message_id);
941
942 self.emit_event(EventRequest::new(
943 self.session_id,
944 context.clone(),
945 everruns_core::events::TurnSealedData {
946 turn_id,
947 reason: reason.to_string(),
948 detail: None,
949 iterations: Some(iterations),
950 usage: usage.clone(),
951 },
952 ))
953 .await?;
954
955 self.emit_session_idled(turn_id, input_message_id, Some(iterations), usage)
956 .await
957 }
958
959 pub async fn fire_turn_end_hooks(
963 &self,
964 harness_id: HarnessId,
965 agent_id: Option<AgentId>,
966 turn_id: TurnId,
967 success: bool,
968 ) {
969 let (specs, dispatcher) = match collect_lifecycle_hook_specs(
970 &self.adapter,
971 self.org_id,
972 self.session_id,
973 harness_id,
974 agent_id,
975 )
976 .await
977 {
978 Ok(pair) => pair,
979 Err(error) => {
980 warn!(
981 session_id = %self.session_id,
982 %error,
983 "failed to collect turn_end hook specs; skipping"
984 );
985 return;
986 }
987 };
988 let hooks = everruns_core::lifecycle_hooks::build_turn_lifecycle_hooks(
989 &specs,
990 everruns_core::user_hook_types::HookEvent::TurnEnd,
991 dispatcher,
992 );
993 if hooks.is_empty() {
994 return;
995 }
996 let ctx = everruns_core::lifecycle_hooks::TurnHookContext {
997 session_id: self.session_id,
998 turn_id: Some(turn_id),
999 org_id: org_public_id_from_internal(self.org_id).parse().ok(),
1000 agent_id: agent_id.map(|a| a.to_string()),
1001 };
1002 everruns_core::lifecycle_hooks::run_turn_end_hooks(
1003 &hooks,
1004 &ctx,
1005 serde_json::json!({ "success": success }),
1006 )
1007 .await;
1008 }
1009
1010 pub async fn user_prompt_blocked(
1015 &self,
1016 turn_id: TurnId,
1017 input_message_id: MessageId,
1018 reason: &str,
1019 user_message: Option<&str>,
1020 ) -> everruns_provider::error::Result<()> {
1021 let user_error =
1022 UserFacingError::new(everruns_provider::user_facing_error::codes::BLOCKED_BY_HOOK);
1023 let shown = user_message.unwrap_or(reason);
1024 let mut error_message = Message::assistant(shown);
1025 let mut metadata = std::collections::HashMap::new();
1026 user_error.apply_to_message_metadata(&mut metadata);
1027 error_message.metadata = Some(metadata);
1028
1029 self.emit_event(EventRequest::new(
1030 self.session_id,
1031 EventContext::turn(turn_id, input_message_id),
1032 OutputMessageCompletedData::new(error_message).with_user_facing_error(&user_error),
1033 ))
1034 .await?;
1035
1036 self.turn_failed(turn_id, input_message_id, reason, Some(&user_error))
1037 .await
1038 }
1039
1040 pub async fn turn_failed(
1041 &self,
1042 turn_id: TurnId,
1043 input_message_id: MessageId,
1044 error: &str,
1045 user_error: Option<&UserFacingError>,
1046 ) -> everruns_provider::error::Result<()> {
1047 self.turn_failed_with_disclosure(turn_id, input_message_id, error, user_error, None)
1048 .await
1049 }
1050
1051 pub async fn turn_failed_with_disclosure(
1055 &self,
1056 turn_id: TurnId,
1057 input_message_id: MessageId,
1058 error: &str,
1059 user_error: Option<&UserFacingError>,
1060 disclosure: Option<ErrorDisclosure>,
1061 ) -> everruns_provider::error::Result<()> {
1062 self.set_session_status(SessionExecutionState::Idle, "turn_failed")
1063 .await?;
1064
1065 self.emit_event(EventRequest::new(
1066 self.session_id,
1067 EventContext::turn(turn_id, input_message_id),
1068 {
1069 let mut data = TurnFailedData {
1070 turn_id,
1071 error: error.to_string(),
1072 error_code: None,
1073 error_fields: None,
1074 error_disclosure: disclosure.map(|mode| mode.as_str().to_string()),
1075 };
1076 if let Some(user_error) = user_error {
1077 user_error.apply_to_event_fields(&mut data.error_code, &mut data.error_fields);
1078 }
1079 data
1080 },
1081 ))
1082 .await?;
1083
1084 self.emit_event(EventRequest::new(
1085 self.session_id,
1086 EventContext::turn(turn_id, input_message_id),
1087 SessionIdledData {
1088 turn_id,
1089 iterations: None,
1090 usage: None,
1091 },
1092 ))
1093 .await
1094 }
1095
1096 pub async fn waiting_for_tool_results(&self) -> everruns_provider::error::Result<()> {
1097 self.set_session_status(
1098 SessionExecutionState::WaitingForToolResults,
1099 "waiting_for_tool_results",
1100 )
1101 .await
1102 }
1103
1104 pub async fn dependency_blocked(
1105 &self,
1106 turn_id: TurnId,
1107 input_message_id: MessageId,
1108 blocker: DependencyBlocker,
1109 ) -> everruns_provider::error::Result<()> {
1110 let user_error = UserFacingError::new(blocker.error_code())
1111 .with_field(
1112 "dependency",
1113 match blocker {
1114 DependencyBlocker::HarnessArchived | DependencyBlocker::HarnessDeleted => {
1115 "harness"
1116 }
1117 DependencyBlocker::AgentArchived | DependencyBlocker::AgentDeleted => "agent",
1118 },
1119 )
1120 .with_field(
1121 "state",
1122 match blocker {
1123 DependencyBlocker::HarnessArchived | DependencyBlocker::AgentArchived => {
1124 "archived"
1125 }
1126 DependencyBlocker::HarnessDeleted | DependencyBlocker::AgentDeleted => {
1127 "deleted"
1128 }
1129 },
1130 );
1131 let mut error_message = Message::assistant(blocker.message());
1132 let mut metadata = std::collections::HashMap::new();
1133 user_error.apply_to_message_metadata(&mut metadata);
1134 error_message.metadata = Some(metadata);
1135
1136 self.emit_event(EventRequest::new(
1137 self.session_id,
1138 EventContext::turn(turn_id, input_message_id),
1139 OutputMessageCompletedData::new(error_message).with_user_facing_error(&user_error),
1140 ))
1141 .await?;
1142
1143 self.turn_failed(
1144 turn_id,
1145 input_message_id,
1146 blocker.message(),
1147 Some(&user_error),
1148 )
1149 .await
1150 }
1151}
1152
1153pub async fn detect_dependency_blocker<A: RuntimeHostAdapter>(
1154 adapter: &A,
1155 org_id: i64,
1156 harness_id: HarnessId,
1157 agent_id: Option<AgentId>,
1158) -> everruns_provider::error::Result<Option<DependencyBlocker>> {
1159 let harness_store = adapter.harness_store(org_id);
1160 let agent_store = adapter.agent_store(org_id);
1161 if let Some(blocker) = harness_store.get_harness_blocker(harness_id).await? {
1162 return Ok(Some(blocker));
1163 }
1164 if let Some(agent_id) = agent_id
1165 && let Some(blocker) = agent_store.get_agent_blocker(agent_id).await?
1166 {
1167 return Ok(Some(blocker));
1168 }
1169 Ok(None)
1170}
1171
1172pub async fn execute_input_activity<A: RuntimeHostAdapter>(
1173 adapter: &A,
1174 org_id: i64,
1175 input: InputAtomInput,
1176) -> everruns_provider::error::Result<InputAtomResult> {
1177 if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
1181 handle.set(None);
1182 }
1183
1184 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1185 .turn_started(input.context.turn_id, input.context.input_message_id)
1186 .await?;
1187
1188 let atom = InputAtom::new(adapter.message_store());
1189 atom.execute(input).await
1190}
1191
1192pub(crate) struct UserPromptHookResult {
1199 pub(crate) decision: everruns_core::lifecycle_hooks::UserPromptDecision,
1200 pub(crate) original_message: String,
1201}
1202
1203pub(crate) async fn run_user_prompt_submit_for_message<A: RuntimeHostAdapter>(
1204 adapter: &A,
1205 org_id: i64,
1206 input: &ReasonInput,
1207 message_text: String,
1208) -> everruns_provider::error::Result<Option<UserPromptHookResult>> {
1209 let (specs, dispatcher) = match collect_lifecycle_hook_specs(
1210 adapter,
1211 org_id,
1212 input.context.session_id,
1213 input.harness_id,
1214 input.agent_id,
1215 )
1216 .await
1217 {
1218 Ok(pair) => pair,
1219 Err(error) => {
1220 warn!(
1221 session_id = %input.context.session_id,
1222 %error,
1223 "failed to collect user_prompt_submit hook specs; continuing without them"
1224 );
1225 return Ok(None);
1226 }
1227 };
1228 let hooks = everruns_core::lifecycle_hooks::build_turn_lifecycle_hooks(
1229 &specs,
1230 everruns_core::user_hook_types::HookEvent::UserPromptSubmit,
1231 dispatcher,
1232 );
1233 if hooks.is_empty() {
1234 return Ok(None);
1235 }
1236
1237 let ctx = everruns_core::lifecycle_hooks::TurnHookContext {
1238 session_id: input.context.session_id,
1239 turn_id: Some(input.context.turn_id),
1240 org_id: org_public_id_from_internal(org_id).parse().ok(),
1241 agent_id: input.agent_id.map(|a| a.to_string()),
1242 };
1243 let original_message = message_text.clone();
1244 let decision =
1245 everruns_core::lifecycle_hooks::run_user_prompt_submit_hooks(&hooks, &ctx, message_text)
1246 .await;
1247 Ok(Some(UserPromptHookResult {
1248 decision,
1249 original_message,
1250 }))
1251}
1252
1253async fn run_user_prompt_submit_for_turn<A: RuntimeHostAdapter>(
1254 adapter: &A,
1255 org_id: i64,
1256 input: &ReasonInput,
1257) -> everruns_provider::error::Result<Option<UserPromptHookResult>> {
1258 let message_text = adapter
1259 .message_store()
1260 .get(input.context.session_id, input.context.input_message_id)
1261 .await
1262 .ok()
1263 .flatten()
1264 .map(|m| m.content_to_llm_string())
1265 .unwrap_or_default();
1266 run_user_prompt_submit_for_message(adapter, org_id, input, message_text).await
1267}
1268
1269pub async fn execute_reason_activity<A: RuntimeHostAdapter>(
1270 adapter: &A,
1271 org_id: i64,
1272 input: ReasonInput,
1273) -> everruns_provider::error::Result<ReasonResult> {
1274 let prompt_message_ids = (input.iteration <= 1)
1275 .then_some(input.context.input_message_id)
1276 .into_iter()
1277 .collect();
1278 execute_reason_activity_with_prompt_messages(adapter, org_id, input, prompt_message_ids).await
1279}
1280
1281pub async fn execute_reason_activity_with_prompt_messages<A: RuntimeHostAdapter>(
1286 adapter: &A,
1287 org_id: i64,
1288 input: ReasonInput,
1289 prompt_message_ids: Vec<MessageId>,
1290) -> everruns_provider::error::Result<ReasonResult> {
1291 if let Some(blocker) =
1292 detect_dependency_blocker(adapter, org_id, input.harness_id, input.agent_id).await?
1293 {
1294 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1295 .dependency_blocked(
1296 input.context.turn_id,
1297 input.context.input_message_id,
1298 blocker,
1299 )
1300 .await?;
1301 return Ok(ReasonResult {
1302 native_counts: None,
1303 success: false,
1304 text: blocker.message().to_string(),
1305 tool_calls: vec![],
1306 has_tool_calls: false,
1307 tool_definitions: vec![],
1308 max_iterations: everruns_core::runtime_agent::default_max_iterations(),
1309 error: Some("dependency_unavailable".to_string()),
1310 user_facing_error: None,
1311 error_disclosure: None,
1312 usage: None,
1313 output_message_id: None,
1314 time_to_first_token_ms: None,
1315 response_id: None,
1316 finish_reason: None,
1317 locale: None,
1318 network_access: None,
1319 parallel_tool_calls: None,
1320 });
1321 }
1322
1323 let mut user_prompt_message_overrides = Vec::new();
1328 for message_id in prompt_message_ids {
1329 let mut hook_input = input.clone();
1330 hook_input.context.input_message_id = message_id;
1331 let Some(hook_result) =
1332 run_user_prompt_submit_for_turn(adapter, org_id, &hook_input).await?
1333 else {
1334 continue;
1335 };
1336 match hook_result.decision {
1337 everruns_core::lifecycle_hooks::UserPromptDecision::Block {
1338 reason,
1339 user_message,
1340 } => {
1341 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1342 .user_prompt_blocked(
1343 input.context.turn_id,
1344 input.context.input_message_id,
1345 &reason,
1346 user_message.as_deref(),
1347 )
1348 .await?;
1349 return Ok(ReasonResult {
1350 native_counts: None,
1351 success: false,
1352 text: user_message.unwrap_or_else(|| reason.clone()),
1353 tool_calls: vec![],
1354 has_tool_calls: false,
1355 tool_definitions: vec![],
1356 max_iterations: everruns_core::runtime_agent::default_max_iterations(),
1357 error: Some("blocked_by_user_prompt_hook".to_string()),
1358 user_facing_error: None,
1359 error_disclosure: None,
1360 usage: None,
1361 output_message_id: None,
1362 time_to_first_token_ms: None,
1363 response_id: None,
1364 finish_reason: None,
1365 locale: None,
1366 network_access: None,
1367 parallel_tool_calls: None,
1368 });
1369 }
1370 everruns_core::lifecycle_hooks::UserPromptDecision::Continue { message } => {
1371 if message != hook_result.original_message {
1372 user_prompt_message_overrides.push((message_id, message));
1373 }
1374 }
1375 }
1376 }
1377
1378 let validation_session = adapter
1382 .session_store(org_id)
1383 .get_session(input.context.session_id)
1384 .await?
1385 .ok_or_else(|| {
1386 everruns_provider::error::AgentLoopError::session_not_found(input.context.session_id)
1387 })?;
1388 let validation_capabilities = load_execution_capabilities(
1389 adapter,
1390 org_id,
1391 input.context.session_id,
1392 input.harness_id,
1393 input.agent_id,
1394 validation_session.locale.clone(),
1395 validation_session.blueprint_id.as_deref(),
1396 )
1397 .await?;
1398 let query_history_allowed = validation_capabilities
1399 .tool_registry
1400 .get("query_history")
1401 .is_some();
1402 let validation_services = runtime_tool_context_services(
1403 adapter,
1404 org_id,
1405 input.context.session_id,
1406 input.agent_id,
1407 Some(Arc::new(validation_capabilities.tool_registry.clone())),
1408 None,
1409 validation_capabilities.subagent_nesting_policy,
1410 );
1411 validation_capabilities
1412 .tool_registry
1413 .validate_context_services(&validation_services)?;
1414
1415 let mut turn_inputs = adapter
1416 .load_resolved_turn(org_id, input.context.session_id)
1417 .await?;
1418 if let Some(augmentor) = adapter.tool_augmentor() {
1419 augmentor
1420 .augment_reason_tools(
1421 input.context.session_id,
1422 adapter.session_store(org_id),
1423 adapter.session_task_registry(),
1424 &mut turn_inputs.mcp_tool_definitions,
1425 )
1426 .await?;
1427 }
1428
1429 let reason_capability_registry = {
1430 let mut registry = adapter.capability_registry();
1431 if !query_history_allowed {
1432 let query_history_owner = registry
1438 .list()
1439 .into_iter()
1440 .find(|capability| {
1441 capability
1442 .tool_definitions()
1443 .iter()
1444 .any(|tool| tool.name() == "query_history")
1445 })
1446 .map(Arc::clone);
1447 if let Some(capability) = query_history_owner {
1448 registry.register(MessageFilterOnlyCapability(capability));
1449 }
1450 }
1451 registry
1452 };
1453 let context_resolver = crate::runtime_context::StoreTurnContextResolver::new(
1454 adapter.harness_store(org_id),
1455 adapter.agent_store(org_id),
1456 adapter.session_store(org_id),
1457 adapter.message_store(),
1458 adapter.provider_store(org_id),
1459 reason_capability_registry.clone(),
1460 adapter.driver_registry(),
1461 )
1462 .with_file_store(adapter.file_store());
1463 let context_resolver = match adapter.storage_store() {
1464 Some(store) => context_resolver.with_session_storage(store),
1465 None => context_resolver,
1466 };
1467 let mut atom = ReasonAtom::new(
1468 context_resolver,
1469 adapter.message_store(),
1470 reason_capability_registry.clone(),
1471 adapter.event_emitter(),
1472 );
1473 if let Some(image_resolver) = adapter.image_resolver(org_id) {
1474 atom = atom.with_image_resolver(image_resolver);
1475 }
1476 if let Some(file_resolver) = adapter.file_resolver(org_id) {
1477 atom = atom.with_file_resolver(file_resolver);
1478 }
1479 if let Some(hb) = adapter.stream_heartbeater() {
1480 atom = atom.with_stream_heartbeater(hb);
1481 }
1482 if let Some(timeout) = adapter.provider_stall_timeout() {
1483 atom = atom.with_provider_stall_timeout(timeout);
1484 }
1485 if let Some(config) = adapter.provider_retry_config() {
1486 atom = atom.with_provider_retry_config(config);
1487 }
1488 if let Some(store) = adapter.partial_stream_store() {
1489 atom = atom.with_partial_stream_store(store);
1490 }
1491 if let Some(store) = adapter.durable_tool_result_store() {
1492 atom = atom.with_durable_tool_result_store(store);
1493 }
1494 if let Some(store) = adapter.compaction_checkpoint_store() {
1495 atom = atom.with_compaction_checkpoint_store(store);
1496 }
1497 if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
1498 atom = atom.with_reasoning_effort_handle(handle);
1499 }
1500 if let Some(utility_llm_service) = adapter.utility_llm_service() {
1501 atom = atom.with_utility_llm_service(utility_llm_service);
1502 }
1503 if let Some(classifier) = adapter.classifier() {
1504 atom = atom.with_classifier(classifier);
1505 }
1506 if let Some(schedule_store) = adapter.schedule_store(org_id) {
1509 atom = atom.with_schedule_store(schedule_store);
1510 }
1511
1512 let mut assembled = crate::runtime_context::assemble_turn_context_from_snapshot(
1513 turn_inputs.snapshot,
1514 adapter.message_store().as_ref(),
1515 adapter.provider_store(org_id).as_ref(),
1516 &reason_capability_registry,
1517 &adapter.driver_registry(),
1518 &turn_inputs.mcp_tool_definitions,
1519 Some(adapter.file_store()),
1520 adapter.storage_store(),
1524 )
1525 .await?;
1526 let input = ReasonInput {
1527 mcp_tool_definitions: turn_inputs.mcp_tool_definitions,
1528 ..input
1529 };
1530
1531 if !user_prompt_message_overrides.is_empty() {
1532 for (message_id, message_override) in user_prompt_message_overrides {
1533 let message = assembled
1534 .messages
1535 .iter_mut()
1536 .find(|message| message.id == message_id)
1537 .ok_or_else(|| {
1538 everruns_provider::error::AgentLoopError::config(
1539 "user_prompt_submit mutation: input message not found in assembled context",
1540 )
1541 })?;
1542
1543 message
1546 .content
1547 .retain(|part| !matches!(part, ContentPart::Text(_)));
1548 message
1549 .content
1550 .insert(0, ContentPart::text(message_override));
1551 }
1552 }
1553 if input.iteration <= 1 {
1558 emit_model_change_if_switched(adapter, org_id, &input, &assembled).await;
1559 }
1560
1561 crate::native_async::execute_reason(adapter, org_id, input, assembled, atom).await
1562}
1563
1564async fn emit_model_change_if_switched<A: RuntimeHostAdapter>(
1569 adapter: &A,
1570 org_id: i64,
1571 input: &ReasonInput,
1572 assembled: &AssembledTurnContext,
1573) {
1574 let Some((previous_model_id, model_id)) = model_switch(&assembled.messages) else {
1575 return;
1576 };
1577 if assembled.resolved_model_id != Some(model_id) {
1578 return;
1581 }
1582
1583 let previous_model_name = adapter
1587 .provider_store(org_id)
1588 .get_model_spec(previous_model_id)
1589 .await
1590 .ok()
1591 .flatten()
1592 .map(|spec| spec.model);
1593
1594 let request = EventRequest::new(
1595 input.context.session_id,
1596 EventContext::turn(input.context.turn_id, input.context.input_message_id),
1597 SessionModelChangedData {
1598 previous_model_id: Some(previous_model_id),
1599 previous_model_name,
1600 model_id,
1601 model_name: assembled.model.model.clone(),
1602 },
1603 );
1604 if let Err(e) = adapter.event_emitter().emit(request).await {
1605 warn!(error = %e, "Failed to emit session.model.changed event");
1606 }
1607}
1608
1609fn model_switch(messages: &[Message]) -> Option<(ModelId, ModelId)> {
1616 let mut user_model_ids = messages
1617 .iter()
1618 .rev()
1619 .filter(|message| message.role == MessageRole::User)
1620 .map(|message| {
1621 message
1622 .controls
1623 .as_ref()
1624 .and_then(|controls| controls.model_id)
1625 });
1626
1627 let model_id = user_model_ids.next().flatten()?;
1631 let previous_model_id = user_model_ids.next().flatten()?;
1632 (previous_model_id != model_id).then_some((previous_model_id, model_id))
1633}
1634
1635pub async fn execute_act_activity<A: RuntimeHostAdapter>(
1636 adapter: &A,
1637 input: ActInput,
1638) -> everruns_provider::error::Result<ActResult> {
1639 let org_id = input.org_id.ok_or_else(|| {
1640 everruns_provider::error::AgentLoopError::config(
1641 "ActInput.org_id must be set for runtime host execution",
1642 )
1643 })?;
1644
1645 if let Some(blocker) =
1646 detect_dependency_blocker(adapter, org_id, input.harness_id, input.agent_id).await?
1647 {
1648 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1649 .dependency_blocked(
1650 input.context.turn_id,
1651 input.context.input_message_id,
1652 blocker,
1653 )
1654 .await?;
1655 return Ok(ActResult {
1656 results: vec![],
1657 completed: true,
1658 success_count: 0,
1659 error_count: 1,
1660 waiting_for_tool_results: false,
1661 waiting_for_url_elicitation: false,
1662 blocked: true,
1663 client_tool_calls: vec![],
1664 client_tool_definitions: vec![],
1665 });
1666 }
1667
1668 let execution_capabilities = load_execution_capabilities(
1669 adapter,
1670 org_id,
1671 input.context.session_id,
1672 input.harness_id,
1673 input.agent_id,
1674 input.locale.clone(),
1675 input.blueprint_id.as_deref(),
1676 )
1677 .await?;
1678 let mut tool_registry = execution_capabilities.tool_registry;
1679
1680 if let Some(augmentor) = adapter.tool_augmentor() {
1681 augmentor
1682 .augment_act_tools(
1683 input.context.session_id,
1684 adapter.session_store(org_id),
1685 adapter.session_task_registry(),
1686 adapter.file_store(),
1687 &input.tool_definitions,
1688 &mut tool_registry,
1689 )
1690 .await?;
1691 }
1692
1693 let mut mcp_invoker: Option<Arc<dyn everruns_core::McpToolInvoker>> = None;
1703 if let Some(mcp) = adapter.mcp_executor(org_id, input.context.session_id).await {
1704 let invoker: Arc<dyn everruns_core::McpToolInvoker> = mcp;
1705 for tool in everruns_core::build_mcp_proxy_tools(&input.tool_definitions, invoker.clone()) {
1706 tool_registry.register_boxed(tool);
1707 }
1708 mcp_invoker = Some(Arc::new(everruns_core::ScopedMcpToolInvoker::new(
1709 &input.tool_definitions,
1710 invoker,
1711 )));
1712 }
1713
1714 let builtin_tool_registry = Arc::new(tool_registry.clone());
1715 let context_services = runtime_tool_context_services(
1716 adapter,
1717 org_id,
1718 input.context.session_id,
1719 input.agent_id,
1720 Some(builtin_tool_registry),
1721 mcp_invoker,
1722 execution_capabilities.subagent_nesting_policy,
1723 );
1724 tool_registry.validate_context_services(&context_services)?;
1725 let executor: Arc<dyn everruns_core::tool_execution::ToolExecutor> = Arc::new(tool_registry);
1726
1727 let mut atom = ActAtom::new(executor, adapter.event_emitter())
1728 .with_context_services(context_services)
1729 .with_post_tool_hooks(execution_capabilities.post_tool_hooks)
1730 .with_pre_tool_hooks(execution_capabilities.pre_tool_hooks)
1731 .with_tool_call_hooks(execution_capabilities.tool_call_hooks);
1732
1733 #[cfg(feature = "builtins")]
1734 {
1735 atom = atom.with_final_post_tool_hook(Arc::new(everruns_builtins::PersistOutputHook));
1736 }
1737
1738 if let Some(limiter) = adapter.outbound_tool_rate_limiter(org_id) {
1739 atom = atom.with_outbound_tool_rate_limiter(limiter);
1740 }
1741 if let Some(store) = adapter.durable_tool_result_store() {
1742 atom = atom.with_durable_tool_result_store(store);
1743 }
1744
1745 atom.execute(input).await
1746}