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