1use async_trait::async_trait;
6use everruns_core::atoms::{
7 ActAtom, ActInput, ActResult, Atom, InputAtom, InputAtomInput, InputAtomResult, ReasonAtom,
8 ReasonInput, ReasonResult,
9};
10use everruns_core::capabilities::{
11 SystemPromptContext, collect_capabilities_with_configs, report_result_tool_for_child_session,
12 report_task_progress_tool_for_child_session,
13};
14use everruns_core::events::{
15 EventContext, EventRequest, OutputMessageCompletedData, SessionActivatedData, SessionIdledData,
16 TurnCompletedData, TurnFailedData, TurnStartedData,
17};
18use everruns_core::message::{ContentPart, Message};
19use everruns_core::message_retriever::MessageRetriever;
20use everruns_core::platform_store::PlatformStore;
21use everruns_core::session::SessionStatus;
22use everruns_core::tools::Tool;
23use everruns_core::traits::{
24 AgentStore, BudgetChecker, EventEmitter, HarnessStore, ImageArtifactStore, ImageResolver,
25 LeasedResourceStore, PaymentAuthority, ProviderCredentialStore, ProviderStore, ResolvedModel,
26 SessionCreationAuthority, SessionFileSystem, SessionMutator, SessionResourceRegistry,
27 SessionScheduleStore, SessionSqlDbStoreRef, SessionStorageStore, SessionStore,
28 UserConnectionResolver,
29};
30use everruns_core::typed_id::{AgentId, HarnessId, MessageId, SessionId, TurnId};
31use everruns_core::vector_store::KnowledgeIndexSearch;
32use everruns_core::{
33 Agent, CapabilityRegistry, CapabilityStatus, DependencyBlocker, DriverRegistry, EgressService,
34 ErrorDisclosure, Harness, Session, TokenUsage, ToolDefinition, ToolRegistry, UserFacingError,
35 UtilityLlmService, assemble_turn_context, org_public_id_from_internal,
36 resolve_runtime_capabilities,
37};
38use std::sync::Arc;
39use tracing::warn;
40
41#[derive(Debug, Clone)]
43pub struct RuntimeHostTurnContext {
44 pub agent: Option<Agent>,
45 pub session: Session,
46 pub messages: Vec<Message>,
47 pub model: Option<ResolvedModel>,
48 pub mcp_tool_definitions: Vec<ToolDefinition>,
49}
50
51#[async_trait]
62pub trait RuntimeHostAdapter: Send + Sync + Clone + 'static {
63 async fn get_agent(
64 &self,
65 org_id: i64,
66 agent_id: AgentId,
67 ) -> everruns_core::error::Result<Option<Agent>>;
68
69 async fn get_harness(
70 &self,
71 org_id: i64,
72 harness_id: HarnessId,
73 ) -> everruns_core::error::Result<Option<Harness>>;
74
75 async fn set_session_status(
76 &self,
77 org_id: i64,
78 session_id: SessionId,
79 status: SessionStatus,
80 ) -> everruns_core::error::Result<Session>;
81
82 async fn load_turn_context(
83 &self,
84 org_id: i64,
85 session_id: SessionId,
86 ) -> everruns_core::error::Result<RuntimeHostTurnContext>;
87
88 fn capability_registry(&self) -> CapabilityRegistry;
89
90 fn driver_registry(&self) -> DriverRegistry;
91
92 fn harness_store(&self, org_id: i64) -> Arc<dyn HarnessStore>;
93
94 fn agent_store(&self, org_id: i64) -> Arc<dyn AgentStore>;
95
96 fn session_store(&self, org_id: i64) -> Arc<dyn SessionStore>;
97
98 fn session_mutator(&self, org_id: i64) -> Arc<dyn SessionMutator>;
99
100 fn provider_store(&self, org_id: i64) -> Arc<dyn ProviderStore>;
101
102 fn message_store(&self) -> Arc<dyn MessageRetriever>;
103
104 fn event_emitter(&self) -> Arc<dyn EventEmitter>;
105
106 fn file_store(&self) -> Arc<dyn SessionFileSystem>;
107
108 fn image_resolver(&self, _org_id: i64) -> Option<Arc<dyn ImageResolver>> {
109 None
110 }
111
112 fn image_artifact_store(&self, _org_id: i64) -> Option<Arc<dyn ImageArtifactStore>> {
113 None
114 }
115
116 fn provider_credential_store(&self, _org_id: i64) -> Option<Arc<dyn ProviderCredentialStore>> {
117 None
118 }
119
120 fn utility_llm_service(&self) -> Option<Arc<dyn UtilityLlmService>> {
121 None
122 }
123
124 fn egress_service(&self) -> Option<Arc<dyn EgressService>> {
125 None
126 }
127
128 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
129 None
130 }
131
132 fn knowledge_store(&self) -> Option<Arc<dyn everruns_core::traits::KnowledgeStore>> {
134 None
135 }
136
137 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
138 None
139 }
140
141 fn sqldb_store(&self) -> Option<SessionSqlDbStoreRef> {
142 None
143 }
144
145 fn leased_resource_store(&self) -> Option<Arc<dyn LeasedResourceStore>> {
146 None
147 }
148
149 fn session_resource_registry(&self) -> Option<Arc<dyn SessionResourceRegistry>> {
150 None
151 }
152
153 fn session_task_registry(
154 &self,
155 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
156 None
157 }
158
159 fn schedule_store(&self, _org_id: i64) -> Option<Arc<dyn SessionScheduleStore>> {
160 None
161 }
162
163 fn platform_store(
164 &self,
165 _org_id: i64,
166 _session_id: SessionId,
167 ) -> Option<Arc<dyn PlatformStore>> {
168 None
169 }
170
171 fn knowledge_index_search(&self, _org_id: i64) -> Option<Arc<dyn KnowledgeIndexSearch>> {
175 None
176 }
177
178 fn budget_checker(
179 &self,
180 _org_id: i64,
181 _agent_id: Option<AgentId>,
182 ) -> Option<Arc<dyn BudgetChecker>> {
183 None
184 }
185
186 fn payment_authority(
187 &self,
188 _org_id: i64,
189 _agent_id: Option<AgentId>,
190 ) -> Option<Arc<dyn PaymentAuthority>> {
191 None
192 }
193
194 fn session_creation_authority(
195 &self,
196 _org_id: i64,
197 _session_id: SessionId,
198 ) -> Option<Arc<dyn SessionCreationAuthority>> {
199 None
200 }
201
202 fn outbound_tool_rate_limiter(
205 &self,
206 _org_id: i64,
207 ) -> Option<Arc<dyn everruns_core::OutboundToolRateLimiter>> {
208 None
209 }
210
211 fn durable_tool_result_store(&self) -> Option<Arc<dyn everruns_core::DurableToolResultStore>> {
214 None
215 }
216
217 fn subagent_spawn_store(&self) -> Option<Arc<dyn everruns_core::SubagentSpawnStore>> {
220 None
221 }
222
223 fn stream_heartbeater(&self) -> Option<Arc<dyn everruns_core::StreamHeartbeater>> {
226 None
227 }
228
229 fn partial_stream_store(&self) -> Option<Arc<dyn everruns_core::PartialStreamStore>> {
232 None
233 }
234
235 fn reasoning_effort_handle(
244 &self,
245 _session_id: SessionId,
246 ) -> Option<everruns_core::ReasoningEffortHandle> {
247 None
248 }
249
250 fn provider_stall_timeout(&self) -> Option<std::time::Duration> {
253 None
254 }
255
256 async fn mcp_executor(
260 &self,
261 _org_id: i64,
262 _session_id: SessionId,
263 ) -> Option<Arc<everruns_mcp::McpExecutor>> {
264 None
265 }
266}
267
268struct RuntimeExecutionCapabilities {
269 tool_registry: ToolRegistry,
270 post_tool_hooks: Vec<Arc<dyn everruns_core::PostToolExecHook>>,
271 pre_tool_hooks: Vec<Arc<dyn everruns_core::atoms::PreToolUseHook>>,
272 tool_call_hooks: Vec<Arc<dyn everruns_core::ToolCallHook>>,
273 subagent_nesting_policy: everruns_core::SubagentNestingPolicy,
274}
275
276fn subagent_nesting_policy_from_configs(
277 resolved_capability_configs: &[everruns_core::capability_types::AgentCapabilityConfig],
278) -> everruns_core::SubagentNestingPolicy {
279 let subagents_config = resolved_capability_configs.iter().find(|config| {
280 config.capability_id() == everruns_core::capabilities::SUBAGENTS_CAPABILITY_ID
281 });
282
283 let configured_depth = subagents_config
284 .and_then(|config| {
285 config
286 .config
287 .get("max_subagent_depth")
288 .or_else(|| config.config.get("max_depth"))
289 })
290 .and_then(|value| value.as_u64())
291 .and_then(|value| u32::try_from(value).ok());
292 let configured_max_active = subagents_config
293 .and_then(|config| {
294 config
295 .config
296 .get("max_active_descendant_tasks")
297 .or_else(|| config.config.get("max_concurrent_descendant_tasks"))
298 })
299 .and_then(|value| value.as_u64())
300 .and_then(|value| u32::try_from(value).ok());
301 let configured_max_total = subagents_config
302 .and_then(|config| config.config.get("max_total_descendant_tasks"))
303 .and_then(|value| value.as_u64())
304 .and_then(|value| u32::try_from(value).ok());
305 let configured_max_active_detached = subagents_config
306 .and_then(|config| config.config.get("max_active_detached_tasks"))
307 .and_then(|value| value.as_u64())
308 .and_then(|value| u32::try_from(value).ok());
309 let configured_max_total_detached = subagents_config
310 .and_then(|config| config.config.get("max_total_detached_tasks"))
311 .and_then(|value| value.as_u64())
312 .and_then(|value| u32::try_from(value).ok());
313
314 everruns_core::SubagentNestingPolicy::default()
315 .with_agent_override(configured_depth)
316 .with_agent_task_caps_override(configured_max_active, configured_max_total)
317 .with_agent_detached_task_caps_override(
318 configured_max_active_detached,
319 configured_max_total_detached,
320 )
321}
322
323fn finalize_specs_from_configs(
333 resolved_capability_configs: &[everruns_core::capability_types::AgentCapabilityConfig],
334 capability_registry: &CapabilityRegistry,
335) -> Vec<everruns_core::user_hook_types::UserHookSpec> {
336 let mut hook_contributions: Vec<(String, Vec<everruns_core::user_hook_types::UserHookSpec>)> =
337 Vec::new();
338 let mut disabled_contributions: Vec<String> = Vec::new();
339 for config in resolved_capability_configs {
340 let Some(capability) = capability_registry.get(config.capability_id()) else {
341 continue;
342 };
343 let specs = capability.user_hooks_with_config(&config.config);
344 if !specs.is_empty() {
345 hook_contributions.push((config.capability_id().to_string(), specs));
346 }
347 if config.capability_id() == "user_hooks" {
348 disabled_contributions.extend(
349 everruns_core::capabilities::user_hooks::disabled_contributions(&config.config),
350 );
351 }
352 }
353 everruns_core::hook_adapter::finalize_hook_specs(hook_contributions, &disabled_contributions)
354}
355
356async fn collect_lifecycle_hook_specs<A: RuntimeHostAdapter>(
361 adapter: &A,
362 org_id: i64,
363 session_id: SessionId,
364 harness_id: HarnessId,
365 agent_id: Option<AgentId>,
366) -> everruns_core::error::Result<(
367 Vec<everruns_core::user_hook_types::UserHookSpec>,
368 Arc<dyn everruns_core::hook_executor::BashHookDispatcher>,
369)> {
370 let capability_registry = adapter.capability_registry();
371 let harness_chain = adapter
372 .harness_store(org_id)
373 .get_harness_chain(harness_id)
374 .await?;
375 if harness_chain.is_empty() {
376 return Err(everruns_core::error::AgentLoopError::harness_not_found(
377 harness_id,
378 ));
379 }
380 let session = adapter
381 .session_store(org_id)
382 .get_session(session_id)
383 .await?
384 .ok_or_else(|| everruns_core::error::AgentLoopError::session_not_found(session_id))?;
385 let agent = match agent_id {
386 Some(agent_id) => adapter.agent_store(org_id).get_agent(agent_id).await?,
387 None => None,
388 };
389 let resolved = resolve_runtime_capabilities(
390 &harness_chain,
391 agent.as_ref(),
392 &session,
393 &capability_registry,
394 );
395 let specs =
396 finalize_specs_from_configs(&resolved.resolved_capability_configs, &capability_registry);
397 let dispatcher: Arc<dyn everruns_core::hook_executor::BashHookDispatcher> = Arc::new(
398 everruns_core::hook_dispatch::BashkitShellHookDispatcher::new(adapter.file_store()),
399 );
400 Ok((specs, dispatcher))
401}
402
403async fn load_execution_capabilities<A: RuntimeHostAdapter>(
404 adapter: &A,
405 org_id: i64,
406 session_id: SessionId,
407 harness_id: HarnessId,
408 agent_id: Option<AgentId>,
409 locale: Option<String>,
410 blueprint_id: Option<&str>,
411) -> everruns_core::error::Result<RuntimeExecutionCapabilities> {
412 let capability_registry = adapter.capability_registry();
413 if let Some(blueprint_id) = blueprint_id {
414 let mut registry = ToolRegistry::with_defaults();
415 let blueprint = capability_registry.blueprint(blueprint_id).ok_or_else(|| {
416 everruns_core::error::AgentLoopError::config(format!(
417 "Blueprint \"{blueprint_id}\" not found in registry"
418 ))
419 })?;
420 for tool in blueprint.tools {
421 registry.register_boxed(tool);
422 }
423 return Ok(RuntimeExecutionCapabilities {
424 tool_registry: registry,
425 post_tool_hooks: Vec::new(),
426 pre_tool_hooks: Vec::new(),
427 tool_call_hooks: Vec::new(),
428 subagent_nesting_policy: everruns_core::SubagentNestingPolicy::default(),
429 });
430 }
431
432 let harness_chain = adapter
433 .harness_store(org_id)
434 .get_harness_chain(harness_id)
435 .await?;
436 if harness_chain.is_empty() {
437 return Err(everruns_core::error::AgentLoopError::harness_not_found(
438 harness_id,
439 ));
440 }
441
442 let session = adapter
443 .session_store(org_id)
444 .get_session(session_id)
445 .await?
446 .ok_or_else(|| everruns_core::error::AgentLoopError::session_not_found(session_id))?;
447
448 let agent_store = adapter.agent_store(org_id);
449 let agent = match agent_id {
450 Some(agent_id) => Some(
451 agent_store
452 .get_agent(agent_id)
453 .await?
454 .ok_or_else(|| everruns_core::error::AgentLoopError::agent_not_found(agent_id))?,
455 ),
456 None => None,
457 };
458
459 let resolved = resolve_runtime_capabilities(
460 &harness_chain,
461 agent.as_ref(),
462 &session,
463 &capability_registry,
464 );
465 let prompt_ctx = SystemPromptContext {
472 session_id,
473 locale: locale.or(session.locale.clone()),
474 file_store: Some(everruns_core::MountFs::wrap(
478 everruns_core::WorkspaceScopedFileSystem::wrap(
479 adapter.file_store(),
480 session.workspace_id,
481 ),
482 )),
483 model: None,
484 };
485 let collected = collect_capabilities_with_configs(
486 &resolved.resolved_capability_configs,
487 &capability_registry,
488 &prompt_ctx,
489 )
490 .await;
491
492 let mut registry = ToolRegistry::with_defaults();
493 for tool in collected.tools {
494 registry.register_boxed(tool);
495 }
496
497 let mut post_tool_hooks: Vec<Arc<dyn everruns_core::PostToolExecHook>> = resolved
502 .resolved_capability_configs
503 .iter()
504 .flat_map(|config| {
505 capability_registry
506 .get(config.capability_id())
507 .filter(|capability| capability.status() == CapabilityStatus::Available)
508 .map(|capability| capability.post_tool_exec_hooks_with_config(&config.config))
509 .unwrap_or_default()
510 })
511 .collect();
512 post_tool_hooks.sort_by_key(|hook| hook.priority());
515
516 let user_hook_specs =
523 finalize_specs_from_configs(&resolved.resolved_capability_configs, &capability_registry);
524 let mut pre_tool_hooks: Vec<Arc<dyn everruns_core::atoms::PreToolUseHook>> = resolved
527 .resolved_capability_configs
528 .iter()
529 .flat_map(|config| {
530 capability_registry
531 .get(config.capability_id())
532 .filter(|capability| capability.status() == CapabilityStatus::Available)
533 .map(|capability| capability.pre_tool_use_hooks_with_config(&config.config))
534 .unwrap_or_default()
535 })
536 .collect();
537 if !user_hook_specs.is_empty() {
538 let dispatcher: Arc<dyn everruns_core::hook_executor::BashHookDispatcher> = Arc::new(
539 everruns_core::hook_dispatch::BashkitShellHookDispatcher::new(adapter.file_store()),
540 );
541 post_tool_hooks.extend(everruns_core::hook_adapter::build_post_tool_use_hooks(
542 &user_hook_specs,
543 dispatcher.clone(),
544 ));
545 pre_tool_hooks.extend(everruns_core::hook_adapter::build_pre_tool_use_hooks(
546 &user_hook_specs,
547 dispatcher,
548 ));
549 }
550
551 let tool_call_hooks = collected.tool_call_hooks;
562
563 Ok(RuntimeExecutionCapabilities {
564 tool_registry: registry,
565 post_tool_hooks,
566 pre_tool_hooks,
567 tool_call_hooks,
568 subagent_nesting_policy: subagent_nesting_policy_from_configs(
569 &resolved.resolved_capability_configs,
570 ),
571 })
572}
573
574pub struct RuntimeSessionLifecycle<A: RuntimeHostAdapter> {
576 adapter: A,
577 org_id: i64,
578 session_id: SessionId,
579}
580
581impl<A: RuntimeHostAdapter> RuntimeSessionLifecycle<A> {
582 pub fn new(adapter: A, org_id: i64, session_id: SessionId) -> Self {
583 Self {
584 adapter,
585 org_id,
586 session_id,
587 }
588 }
589
590 async fn set_session_status(&self, status: SessionStatus, action: &'static str) {
591 if let Err(error) = self
592 .adapter
593 .set_session_status(self.org_id, self.session_id, status)
594 .await
595 {
596 warn!(
597 session_id = %self.session_id,
598 org_id = self.org_id,
599 action,
600 %error,
601 "runtime host lifecycle status update failed"
602 );
603 }
604 }
605
606 async fn emit_event(&self, request: EventRequest) {
607 let event_type = request.event_type.clone();
608 if let Err(error) = self.adapter.event_emitter().emit(request).await {
609 warn!(
610 session_id = %self.session_id,
611 org_id = self.org_id,
612 event_type,
613 %error,
614 "runtime host lifecycle event emission failed"
615 );
616 }
617 }
618
619 pub async fn turn_started(&self, turn_id: TurnId, input_message_id: MessageId) {
620 let input_content = self
621 .adapter
622 .message_store()
623 .get(self.session_id, input_message_id)
624 .await
625 .ok()
626 .flatten()
627 .map(|message| message.content_to_llm_string());
628
629 self.set_session_status(SessionStatus::Active, "turn_started")
630 .await;
631
632 self.emit_event(EventRequest::new(
633 self.session_id,
634 EventContext::turn(turn_id, input_message_id),
635 SessionActivatedData {
636 turn_id,
637 input_message_id,
638 },
639 ))
640 .await;
641
642 self.emit_event(EventRequest::new(
643 self.session_id,
644 EventContext::turn(turn_id, input_message_id),
645 TurnStartedData {
646 turn_id,
647 input_message_id,
648 input_content,
649 },
650 ))
651 .await;
652 }
653
654 pub async fn emit_turn_completed(&self, input_message_id: MessageId, data: TurnCompletedData) {
655 let turn_id = data.turn_id;
656 self.emit_event(EventRequest::new(
657 self.session_id,
658 EventContext::turn(turn_id, input_message_id),
659 data,
660 ))
661 .await;
662 }
663
664 pub async fn emit_session_idled(
665 &self,
666 turn_id: TurnId,
667 input_message_id: MessageId,
668 iterations: Option<u32>,
669 usage: Option<TokenUsage>,
670 ) {
671 self.set_session_status(SessionStatus::Idle, "emit_session_idled")
672 .await;
673
674 self.emit_event(EventRequest::new(
675 self.session_id,
676 EventContext::turn(turn_id, input_message_id),
677 SessionIdledData {
678 turn_id,
679 iterations,
680 usage,
681 },
682 ))
683 .await;
684 }
685
686 pub async fn turn_completed(
687 &self,
688 turn_id: TurnId,
689 input_message_id: MessageId,
690 iterations: u32,
691 usage: Option<TokenUsage>,
692 input_content: Option<String>,
693 ) {
694 self.emit_turn_completed(
695 input_message_id,
696 TurnCompletedData {
697 turn_id,
698 iterations,
699 duration_ms: None,
700 usage: usage.clone(),
701 input_content,
702 final_message_id: None,
703 final_answer_preview: None,
704 time_to_first_token_ms: None,
705 tool_call_count: None,
706 llm_call_count: None,
707 status: Some("completed".to_string()),
708 },
709 )
710 .await;
711 self.emit_session_idled(turn_id, input_message_id, Some(iterations), usage)
712 .await;
713 }
714
715 pub async fn turn_sealed(
722 &self,
723 turn_id: TurnId,
724 input_message_id: MessageId,
725 reason: &str,
726 iterations: u32,
727 usage: Option<TokenUsage>,
728 ) {
729 let context = EventContext::turn(turn_id, input_message_id);
730
731 self.emit_event(EventRequest::new(
732 self.session_id,
733 context.clone(),
734 everruns_core::events::TurnSealedData {
735 turn_id,
736 reason: reason.to_string(),
737 detail: None,
738 iterations: Some(iterations),
739 usage: usage.clone(),
740 },
741 ))
742 .await;
743
744 self.emit_session_idled(turn_id, input_message_id, Some(iterations), usage)
745 .await;
746 }
747
748 pub async fn fire_turn_end_hooks(
752 &self,
753 harness_id: HarnessId,
754 agent_id: Option<AgentId>,
755 turn_id: TurnId,
756 success: bool,
757 ) {
758 let (specs, dispatcher) = match collect_lifecycle_hook_specs(
759 &self.adapter,
760 self.org_id,
761 self.session_id,
762 harness_id,
763 agent_id,
764 )
765 .await
766 {
767 Ok(pair) => pair,
768 Err(error) => {
769 warn!(
770 session_id = %self.session_id,
771 %error,
772 "failed to collect turn_end hook specs; skipping"
773 );
774 return;
775 }
776 };
777 let hooks = everruns_core::lifecycle_hooks::build_turn_lifecycle_hooks(
778 &specs,
779 everruns_core::user_hook_types::HookEvent::TurnEnd,
780 dispatcher,
781 );
782 if hooks.is_empty() {
783 return;
784 }
785 let ctx = everruns_core::lifecycle_hooks::TurnHookContext {
786 session_id: self.session_id,
787 turn_id: Some(turn_id),
788 org_id: org_public_id_from_internal(self.org_id).parse().ok(),
789 agent_id: agent_id.map(|a| a.to_string()),
790 };
791 everruns_core::lifecycle_hooks::run_turn_end_hooks(
792 &hooks,
793 &ctx,
794 serde_json::json!({ "success": success }),
795 )
796 .await;
797 }
798
799 pub async fn user_prompt_blocked(
804 &self,
805 turn_id: TurnId,
806 input_message_id: MessageId,
807 reason: &str,
808 user_message: Option<&str>,
809 ) {
810 let user_error =
811 UserFacingError::new(everruns_core::user_facing_error_codes::BLOCKED_BY_HOOK);
812 let shown = user_message.unwrap_or(reason);
813 let mut error_message = Message::assistant(shown);
814 let mut metadata = std::collections::HashMap::new();
815 user_error.apply_to_message_metadata(&mut metadata);
816 error_message.metadata = Some(metadata);
817
818 self.emit_event(EventRequest::new(
819 self.session_id,
820 EventContext::turn(turn_id, input_message_id),
821 OutputMessageCompletedData::new(error_message).with_user_facing_error(&user_error),
822 ))
823 .await;
824
825 self.turn_failed(turn_id, input_message_id, reason, Some(&user_error))
826 .await;
827 }
828
829 pub async fn turn_failed(
830 &self,
831 turn_id: TurnId,
832 input_message_id: MessageId,
833 error: &str,
834 user_error: Option<&UserFacingError>,
835 ) {
836 self.turn_failed_with_disclosure(turn_id, input_message_id, error, user_error, None)
837 .await;
838 }
839
840 pub async fn turn_failed_with_disclosure(
844 &self,
845 turn_id: TurnId,
846 input_message_id: MessageId,
847 error: &str,
848 user_error: Option<&UserFacingError>,
849 disclosure: Option<ErrorDisclosure>,
850 ) {
851 self.set_session_status(SessionStatus::Idle, "turn_failed")
852 .await;
853
854 self.emit_event(EventRequest::new(
855 self.session_id,
856 EventContext::turn(turn_id, input_message_id),
857 {
858 let mut data = TurnFailedData {
859 turn_id,
860 error: error.to_string(),
861 error_code: None,
862 error_fields: None,
863 error_disclosure: disclosure.map(|mode| mode.as_str().to_string()),
864 };
865 if let Some(user_error) = user_error {
866 user_error.apply_to_event_fields(&mut data.error_code, &mut data.error_fields);
867 }
868 data
869 },
870 ))
871 .await;
872
873 self.emit_event(EventRequest::new(
874 self.session_id,
875 EventContext::turn(turn_id, input_message_id),
876 SessionIdledData {
877 turn_id,
878 iterations: None,
879 usage: None,
880 },
881 ))
882 .await;
883 }
884
885 pub async fn waiting_for_tool_results(&self) {
886 self.set_session_status(
887 SessionStatus::WaitingForToolResults,
888 "waiting_for_tool_results",
889 )
890 .await;
891 }
892
893 pub async fn dependency_blocked(
894 &self,
895 turn_id: TurnId,
896 input_message_id: MessageId,
897 blocker: DependencyBlocker,
898 ) {
899 let user_error = UserFacingError::new(blocker.error_code())
900 .with_field(
901 "dependency",
902 match blocker {
903 DependencyBlocker::HarnessArchived | DependencyBlocker::HarnessDeleted => {
904 "harness"
905 }
906 DependencyBlocker::AgentArchived | DependencyBlocker::AgentDeleted => "agent",
907 },
908 )
909 .with_field(
910 "state",
911 match blocker {
912 DependencyBlocker::HarnessArchived | DependencyBlocker::AgentArchived => {
913 "archived"
914 }
915 DependencyBlocker::HarnessDeleted | DependencyBlocker::AgentDeleted => {
916 "deleted"
917 }
918 },
919 );
920 let mut error_message = Message::assistant(blocker.message());
921 let mut metadata = std::collections::HashMap::new();
922 user_error.apply_to_message_metadata(&mut metadata);
923 error_message.metadata = Some(metadata);
924
925 self.emit_event(EventRequest::new(
926 self.session_id,
927 EventContext::turn(turn_id, input_message_id),
928 OutputMessageCompletedData::new(error_message).with_user_facing_error(&user_error),
929 ))
930 .await;
931
932 self.turn_failed(
933 turn_id,
934 input_message_id,
935 blocker.message(),
936 Some(&user_error),
937 )
938 .await;
939 }
940}
941
942pub async fn detect_dependency_blocker<A: RuntimeHostAdapter>(
943 adapter: &A,
944 org_id: i64,
945 harness_id: HarnessId,
946 agent_id: Option<AgentId>,
947) -> everruns_core::error::Result<Option<DependencyBlocker>> {
948 let harness_store = adapter.harness_store(org_id);
949 let agent_store = adapter.agent_store(org_id);
950 everruns_core::detect_dependency_blocker(
951 harness_store.as_ref(),
952 agent_store.as_ref(),
953 harness_id,
954 agent_id,
955 )
956 .await
957}
958
959pub async fn execute_input_activity<A: RuntimeHostAdapter>(
960 adapter: &A,
961 org_id: i64,
962 input: InputAtomInput,
963) -> everruns_core::error::Result<InputAtomResult> {
964 if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
968 handle.set(None);
969 }
970
971 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
972 .turn_started(input.context.turn_id, input.context.input_message_id)
973 .await;
974
975 let atom = InputAtom::new(adapter.message_store());
976 atom.execute(input).await
977}
978
979struct UserPromptHookResult {
986 decision: everruns_core::lifecycle_hooks::UserPromptDecision,
987 original_message: String,
988}
989
990async fn run_user_prompt_submit_for_turn<A: RuntimeHostAdapter>(
991 adapter: &A,
992 org_id: i64,
993 input: &ReasonInput,
994) -> everruns_core::error::Result<Option<UserPromptHookResult>> {
995 let (specs, dispatcher) = match collect_lifecycle_hook_specs(
996 adapter,
997 org_id,
998 input.context.session_id,
999 input.harness_id,
1000 input.agent_id,
1001 )
1002 .await
1003 {
1004 Ok(pair) => pair,
1005 Err(error) => {
1006 warn!(
1007 session_id = %input.context.session_id,
1008 %error,
1009 "failed to collect user_prompt_submit hook specs; continuing without them"
1010 );
1011 return Ok(None);
1012 }
1013 };
1014 let hooks = everruns_core::lifecycle_hooks::build_turn_lifecycle_hooks(
1015 &specs,
1016 everruns_core::user_hook_types::HookEvent::UserPromptSubmit,
1017 dispatcher,
1018 );
1019 if hooks.is_empty() {
1020 return Ok(None);
1021 }
1022
1023 let message_text = adapter
1024 .message_store()
1025 .get(input.context.session_id, input.context.input_message_id)
1026 .await
1027 .ok()
1028 .flatten()
1029 .map(|m| m.content_to_llm_string())
1030 .unwrap_or_default();
1031
1032 let ctx = everruns_core::lifecycle_hooks::TurnHookContext {
1033 session_id: input.context.session_id,
1034 turn_id: Some(input.context.turn_id),
1035 org_id: org_public_id_from_internal(org_id).parse().ok(),
1036 agent_id: input.agent_id.map(|a| a.to_string()),
1037 };
1038 let original_message = message_text.clone();
1039 let decision =
1040 everruns_core::lifecycle_hooks::run_user_prompt_submit_hooks(&hooks, &ctx, message_text)
1041 .await;
1042 Ok(Some(UserPromptHookResult {
1043 decision,
1044 original_message,
1045 }))
1046}
1047
1048pub async fn execute_reason_activity<A: RuntimeHostAdapter>(
1049 adapter: &A,
1050 org_id: i64,
1051 input: ReasonInput,
1052) -> everruns_core::error::Result<ReasonResult> {
1053 if let Some(blocker) =
1054 detect_dependency_blocker(adapter, org_id, input.harness_id, input.agent_id).await?
1055 {
1056 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1057 .dependency_blocked(
1058 input.context.turn_id,
1059 input.context.input_message_id,
1060 blocker,
1061 )
1062 .await;
1063 return Ok(ReasonResult {
1064 success: false,
1065 text: blocker.message().to_string(),
1066 tool_calls: vec![],
1067 has_tool_calls: false,
1068 tool_definitions: vec![],
1069 max_iterations: everruns_core::runtime_agent::default_max_iterations(),
1070 error: Some("dependency_unavailable".to_string()),
1071 user_facing_error: None,
1072 error_disclosure: None,
1073 usage: None,
1074 output_message_id: None,
1075 time_to_first_token_ms: None,
1076 response_id: None,
1077 locale: None,
1078 network_access: None,
1079 parallel_tool_calls: None,
1080 });
1081 }
1082
1083 let mut user_prompt_message_override = None;
1091 if input.iteration <= 1
1092 && let Some(hook_result) = run_user_prompt_submit_for_turn(adapter, org_id, &input).await?
1093 {
1094 match hook_result.decision {
1095 everruns_core::lifecycle_hooks::UserPromptDecision::Block {
1096 reason,
1097 user_message,
1098 } => {
1099 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1100 .user_prompt_blocked(
1101 input.context.turn_id,
1102 input.context.input_message_id,
1103 &reason,
1104 user_message.as_deref(),
1105 )
1106 .await;
1107 return Ok(ReasonResult {
1108 success: false,
1109 text: user_message.unwrap_or_else(|| reason.clone()),
1110 tool_calls: vec![],
1111 has_tool_calls: false,
1112 tool_definitions: vec![],
1113 max_iterations: everruns_core::runtime_agent::default_max_iterations(),
1114 error: Some("blocked_by_user_prompt_hook".to_string()),
1115 user_facing_error: None,
1116 error_disclosure: None,
1117 usage: None,
1118 output_message_id: None,
1119 time_to_first_token_ms: None,
1120 response_id: None,
1121 locale: None,
1122 network_access: None,
1123 parallel_tool_calls: None,
1124 });
1125 }
1126 everruns_core::lifecycle_hooks::UserPromptDecision::Continue { message } => {
1127 if message != hook_result.original_message {
1128 user_prompt_message_override = Some(message);
1129 }
1130 }
1131 }
1132 }
1133
1134 let mut turn_context = adapter
1135 .load_turn_context(org_id, input.context.session_id)
1136 .await?;
1137 if let Some(registry) = adapter.session_task_registry() {
1138 let session_store = adapter.session_store(org_id);
1139 if let Some(tool) = report_result_tool_for_child_session(
1140 input.context.session_id,
1141 session_store.as_ref(),
1142 registry.as_ref(),
1143 )
1144 .await?
1145 {
1146 turn_context.mcp_tool_definitions.push(tool.to_definition());
1147 }
1148 if let Some(tool) = report_task_progress_tool_for_child_session(
1149 input.context.session_id,
1150 session_store.as_ref(),
1151 registry.as_ref(),
1152 )
1153 .await?
1154 {
1155 turn_context.mcp_tool_definitions.push(tool.to_definition());
1156 }
1157 }
1158
1159 let mut atom = ReasonAtom::new(
1160 adapter.harness_store(org_id),
1161 adapter.agent_store(org_id),
1162 adapter.session_store(org_id),
1163 adapter.message_store(),
1164 adapter.provider_store(org_id),
1165 adapter.capability_registry(),
1166 adapter.driver_registry(),
1167 adapter.event_emitter(),
1168 )
1169 .with_file_store(adapter.file_store());
1170 if let Some(image_resolver) = adapter.image_resolver(org_id) {
1171 atom = atom.with_image_resolver(image_resolver);
1172 }
1173 if let Some(hb) = adapter.stream_heartbeater() {
1174 atom = atom.with_stream_heartbeater(hb);
1175 }
1176 if let Some(timeout) = adapter.provider_stall_timeout() {
1177 atom = atom.with_provider_stall_timeout(timeout);
1178 }
1179 if let Some(store) = adapter.partial_stream_store() {
1180 atom = atom.with_partial_stream_store(store);
1181 }
1182 if let Some(store) = adapter.durable_tool_result_store() {
1183 atom = atom.with_durable_tool_result_store(store);
1184 }
1185 if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
1186 atom = atom.with_reasoning_effort_handle(handle);
1187 }
1188 if let Some(utility_llm_service) = adapter.utility_llm_service() {
1189 atom = atom.with_utility_llm_service(utility_llm_service);
1190 }
1191 if let Some(schedule_store) = adapter.schedule_store(org_id) {
1194 atom = atom.with_schedule_store(schedule_store);
1195 }
1196
1197 let input = ReasonInput {
1198 mcp_tool_definitions: turn_context.mcp_tool_definitions,
1199 ..input
1200 };
1201
1202 if let Some(message_override) = user_prompt_message_override {
1203 let mut assembled = assemble_turn_context(
1204 adapter.harness_store(org_id).as_ref(),
1205 adapter.agent_store(org_id).as_ref(),
1206 adapter.session_store(org_id).as_ref(),
1207 adapter.message_store().as_ref(),
1208 adapter.provider_store(org_id).as_ref(),
1209 &adapter.capability_registry(),
1210 input.context.session_id,
1211 input.harness_id,
1212 input.agent_id,
1213 &input.mcp_tool_definitions,
1214 Some(adapter.file_store()),
1215 )
1216 .await?;
1217
1218 let message = assembled
1219 .messages
1220 .iter_mut()
1221 .find(|message| message.id == input.context.input_message_id)
1222 .ok_or_else(|| {
1223 everruns_core::error::AgentLoopError::config(
1224 "user_prompt_submit mutation: input message not found in assembled context",
1225 )
1226 })?;
1227
1228 message
1233 .content
1234 .retain(|part| !matches!(part, ContentPart::Text(_)));
1235 message
1236 .content
1237 .insert(0, ContentPart::text(message_override));
1238
1239 return atom.execute_with_assembled_context(input, assembled).await;
1240 }
1241
1242 atom.execute(input).await
1243}
1244
1245pub async fn execute_act_activity<A: RuntimeHostAdapter>(
1246 adapter: &A,
1247 input: ActInput,
1248) -> everruns_core::error::Result<ActResult> {
1249 let org_id = input.org_id.ok_or_else(|| {
1250 everruns_core::error::AgentLoopError::config(
1251 "ActInput.org_id must be set for runtime host execution",
1252 )
1253 })?;
1254
1255 if let Some(blocker) =
1256 detect_dependency_blocker(adapter, org_id, input.harness_id, input.agent_id).await?
1257 {
1258 RuntimeSessionLifecycle::new(adapter.clone(), org_id, input.context.session_id)
1259 .dependency_blocked(
1260 input.context.turn_id,
1261 input.context.input_message_id,
1262 blocker,
1263 )
1264 .await;
1265 return Ok(ActResult {
1266 results: vec![],
1267 completed: true,
1268 success_count: 0,
1269 error_count: 1,
1270 waiting_for_tool_results: false,
1271 blocked: true,
1272 client_tool_calls: vec![],
1273 client_tool_definitions: vec![],
1274 });
1275 }
1276
1277 let execution_capabilities = load_execution_capabilities(
1278 adapter,
1279 org_id,
1280 input.context.session_id,
1281 input.harness_id,
1282 input.agent_id,
1283 input.locale.clone(),
1284 input.blueprint_id.as_deref(),
1285 )
1286 .await?;
1287 let mut tool_registry = execution_capabilities.tool_registry;
1288
1289 if input
1290 .tool_definitions
1291 .iter()
1292 .any(|definition| definition.name() == "report_result")
1293 && let Some(registry) = adapter.session_task_registry()
1294 && let Some(tool) = report_result_tool_for_child_session(
1295 input.context.session_id,
1296 adapter.session_store(org_id).as_ref(),
1297 registry.as_ref(),
1298 )
1299 .await?
1300 {
1301 tool_registry.register_boxed(Box::new(tool.with_file_store(adapter.file_store())));
1302 }
1303 if input
1304 .tool_definitions
1305 .iter()
1306 .any(|definition| definition.name() == "report_task_progress")
1307 && let Some(registry) = adapter.session_task_registry()
1308 && let Some(tool) = report_task_progress_tool_for_child_session(
1309 input.context.session_id,
1310 adapter.session_store(org_id).as_ref(),
1311 registry.as_ref(),
1312 )
1313 .await?
1314 {
1315 tool_registry.register_boxed(Box::new(tool));
1316 }
1317
1318 let mut mcp_invoker: Option<Arc<dyn everruns_core::McpToolInvoker>> = None;
1328 if let Some(mcp) = adapter.mcp_executor(org_id, input.context.session_id).await {
1329 let invoker: Arc<dyn everruns_core::McpToolInvoker> = mcp;
1330 for tool in everruns_core::build_mcp_proxy_tools(&input.tool_definitions, invoker.clone()) {
1331 tool_registry.register_boxed(tool);
1332 }
1333 mcp_invoker = Some(Arc::new(everruns_core::ScopedMcpToolInvoker::new(
1334 &input.tool_definitions,
1335 invoker,
1336 )));
1337 }
1338
1339 let builtin_tool_registry = Arc::new(tool_registry.clone());
1340 let executor: Arc<dyn everruns_core::traits::ToolExecutor> = Arc::new(tool_registry);
1341
1342 let mut atom =
1343 ActAtom::with_file_store(executor, adapter.event_emitter(), adapter.file_store())
1344 .with_session_store(adapter.session_store(org_id))
1345 .with_session_mutator(adapter.session_mutator(org_id))
1346 .with_agent_store(adapter.agent_store(org_id))
1347 .with_tool_registry(builtin_tool_registry)
1348 .with_org_id(
1349 org_public_id_from_internal(org_id)
1350 .parse()
1351 .expect("internal org id converts to valid public org id"),
1352 )
1353 .with_capability_registry(adapter.capability_registry())
1354 .with_post_tool_hooks(execution_capabilities.post_tool_hooks)
1355 .with_pre_tool_hooks(execution_capabilities.pre_tool_hooks)
1356 .with_tool_call_hooks(execution_capabilities.tool_call_hooks)
1357 .with_subagent_nesting_policy(execution_capabilities.subagent_nesting_policy);
1358
1359 if let Some(storage_store) = adapter.storage_store() {
1360 atom = atom.with_storage_store(storage_store);
1361 }
1362 if let Some(knowledge_store) = adapter.knowledge_store() {
1363 atom = atom.with_knowledge_store(knowledge_store);
1364 }
1365 if let Some(image_store) = adapter.image_artifact_store(org_id) {
1366 atom = atom.with_image_store(image_store);
1367 }
1368 if let Some(provider_credential_store) = adapter.provider_credential_store(org_id) {
1369 atom = atom.with_provider_credential_store(provider_credential_store);
1370 }
1371 if let Some(utility_llm_service) = adapter.utility_llm_service() {
1372 atom = atom.with_utility_llm_service(utility_llm_service);
1373 }
1374 if let Some(invoker) = mcp_invoker {
1375 atom = atom.with_mcp_invoker(invoker);
1376 }
1377 if let Some(egress_service) = adapter.egress_service() {
1378 atom = atom.with_egress_service(egress_service);
1379 }
1380 if let Some(connection_resolver) = adapter.connection_resolver() {
1381 atom = atom.with_connection_resolver(connection_resolver);
1382 }
1383 if let Some(sqldb_store) = adapter.sqldb_store() {
1384 atom = atom.with_sqldb_store(sqldb_store);
1385 }
1386 if let Some(leased_resource_store) = adapter.leased_resource_store() {
1387 atom = atom.with_leased_resource_store(leased_resource_store);
1388 }
1389 if let Some(registry) = adapter.session_resource_registry() {
1390 atom = atom.with_session_resource_registry(registry);
1391 }
1392 if let Some(registry) = adapter.session_task_registry() {
1393 atom = atom.with_session_task_registry(registry);
1394 }
1395 if let Some(schedule_store) = adapter.schedule_store(org_id) {
1396 atom = atom.with_schedule_store(schedule_store);
1397 }
1398 if let Some(platform_store) = adapter.platform_store(org_id, input.context.session_id) {
1399 atom = atom.with_platform_store(platform_store);
1400 }
1401 if let Some(knowledge_index_search) = adapter.knowledge_index_search(org_id) {
1402 atom = atom.with_knowledge_index_search(knowledge_index_search);
1403 }
1404 if let Some(budget_checker) = adapter.budget_checker(org_id, input.agent_id) {
1405 atom = atom.with_budget_checker(budget_checker);
1406 }
1407 if let Some(payment_authority) = adapter.payment_authority(org_id, input.agent_id) {
1408 atom = atom.with_payment_authority(payment_authority);
1409 }
1410 if let Some(authority) = adapter.session_creation_authority(org_id, input.context.session_id) {
1411 atom = atom.with_session_creation_authority(authority);
1412 }
1413 if let Some(limiter) = adapter.outbound_tool_rate_limiter(org_id) {
1414 atom = atom.with_outbound_tool_rate_limiter(limiter);
1415 }
1416 if let Some(store) = adapter.durable_tool_result_store() {
1417 atom = atom.with_durable_tool_result_store(store);
1418 }
1419 if let Some(store) = adapter.subagent_spawn_store() {
1420 atom = atom.with_subagent_spawn_store(store);
1421 }
1422 if let Some(handle) = adapter.reasoning_effort_handle(input.context.session_id) {
1423 atom = atom.with_reasoning_effort_handle(handle);
1424 }
1425
1426 atom.execute(input).await
1427}