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