1use crate::backends::{
6 EventBus, RuntimeAgentStore, RuntimeBackends, RuntimeHarnessStore, RuntimeMessageStore,
7 RuntimeProviderStore, RuntimeSessionStore,
8};
9use crate::builders::SingleSessionBuilder;
10use crate::host::{
11 RuntimeHostAdapter, RuntimeHostTurnContext, RuntimeSessionLifecycle, execute_act_activity,
12 execute_input_activity, execute_reason_activity,
13};
14use crate::in_memory::{InMemorySessionFileStore, InMemorySessionFileSystemFactory};
15use async_trait::async_trait;
16use everruns_core::agent::Agent;
17use everruns_core::atoms::{ActInput, AtomContext, InputAtomInput, ReasonInput};
18use everruns_core::capabilities::{Capability, CapabilityRegistry};
19use everruns_core::config_layer::AgentConfigOverlay;
20use everruns_core::driver_registry::{DriverId, DriverRegistry};
21use everruns_core::error::{AgentLoopError, Result};
22use everruns_core::events::{
23 Event, EventContext, EventData, EventRequest, InputMessageData, OutputMessageCompletedData,
24 ToolCompletedData,
25};
26use everruns_core::harness::Harness;
27use everruns_core::llmsim_driver::{LlmSimConfig, LlmSimDriver};
28use everruns_core::message::{ContentPart, Message};
29use everruns_core::platform_definition::PlatformDefinition;
30use everruns_core::plugins::{PluginFileSet, compile_plugin};
31use everruns_core::runtime_context::{AssembledTurnContext, inspect_turn_context};
32use everruns_core::session::{Session, SessionStatus};
33use everruns_core::session_file::{InitialFile, SessionFile};
34use everruns_core::tools::ToolResultImage;
35use everruns_core::traits::{
36 AgentStore, EventEmitter, HarnessStore, ProviderStore, ResolvedModel, SessionMutator,
37 SessionStorageStore, SessionStore, UserConnectionResolver,
38};
39use everruns_core::turn::{TurnAction, TurnContext, TurnOutcome, TurnStateMachine};
40use everruns_core::typed_id::{AgentId, HarnessId, OrgId, SessionId};
41use everruns_core::{
42 AgentCapabilityConfig, InputMessage, MessageRetriever, SessionFileSystem,
43 SessionFileSystemFactoryContext, plugin_capability_id,
44};
45use sha2::{Digest, Sha256};
46use std::path::Path;
47use std::sync::Arc;
48
49const HASH_INPUT_CAP_BYTES: usize = 128;
55
56pub fn in_process_internal_org_id(public_org_id: &str) -> i64 {
69 if public_org_id == everruns_core::DEFAULT_ORG_PUBLIC_ID {
70 return everruns_core::DEFAULT_ORG_ID;
71 }
72
73 let Ok(parsed) = public_org_id.parse::<OrgId>() else {
74 return hash_public_org_id(public_org_id);
75 };
76 let raw: u128 = parsed.uuid().as_u128();
77 if raw == 0 {
78 return hash_public_org_id(public_org_id);
79 }
80
81 if raw <= i64::MAX as u128 {
84 return raw as i64;
85 }
86
87 hash_public_org_id(public_org_id)
88}
89
90fn hash_public_org_id(public_org_id: &str) -> i64 {
95 let bytes = public_org_id.as_bytes();
96 let bounded = &bytes[..bytes.len().min(HASH_INPUT_CAP_BYTES)];
97 let digest = Sha256::digest(bounded);
98 let mut buf = [0u8; 8];
99 buf.copy_from_slice(&digest[..8]);
100 let raw = u64::from_be_bytes(buf);
101 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
102}
103
104#[derive(Debug, Clone)]
105pub struct TurnResult {
106 pub response: String,
108 pub iterations: usize,
110 pub tool_calls_count: usize,
112 pub success: bool,
114 pub error: Option<String>,
116 pub turn_id: everruns_core::typed_id::TurnId,
118}
119
120impl TurnResult {
121 fn from_outcome(outcome: TurnOutcome, turn_id: everruns_core::typed_id::TurnId) -> Self {
122 match outcome {
123 TurnOutcome::Success {
124 response,
125 iterations,
126 tool_calls_count,
127 } => Self {
128 response,
129 iterations,
130 tool_calls_count,
131 success: true,
132 error: None,
133 turn_id,
134 },
135 TurnOutcome::Failed { error, iterations } => Self {
136 response: String::new(),
137 iterations,
138 tool_calls_count: 0,
139 success: false,
140 error: Some(error),
141 turn_id,
142 },
143 TurnOutcome::MaxIterationsReached {
144 response,
145 iterations,
146 tool_calls_count,
147 } => Self {
148 response,
149 iterations,
150 tool_calls_count,
151 success: true,
152 error: None,
153 turn_id,
154 },
155 TurnOutcome::Sealed {
158 reason,
159 response,
160 iterations,
161 tool_calls_count,
162 } => Self {
163 response,
164 iterations,
165 tool_calls_count,
166 success: false,
167 error: Some(format!("turn sealed: {reason}")),
168 turn_id,
169 },
170 }
171 }
172}
173
174pub struct InProcessRuntimeBuilder {
184 platform_definition: PlatformDefinition,
185 llm_sim_config: Option<LlmSimConfig>,
186 default_model: Option<ResolvedModel>,
187 backends: Option<RuntimeBackends>,
188 session_file_system_factory_context: SessionFileSystemFactoryContext,
189 harnesses: Vec<Harness>,
190 agents: Vec<Agent>,
191 sessions: Vec<Session>,
192 default_session_id: Option<SessionId>,
193 seeded_files: Vec<(SessionId, InitialFile)>,
194 mcp_auth_provider: Option<Arc<dyn everruns_mcp::McpAuthProvider>>,
195 plugin_capability_configs: Vec<AgentCapabilityConfig>,
201 plugin_warnings: Vec<String>,
203}
204
205impl Default for InProcessRuntimeBuilder {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211impl InProcessRuntimeBuilder {
212 pub fn new() -> Self {
220 Self {
221 platform_definition: PlatformDefinition::builder()
222 .capability_registry(CapabilityRegistry::runtime_builtins())
223 .driver_registry(DriverRegistry::new())
224 .session_file_system_factory(Arc::new(InMemorySessionFileSystemFactory))
225 .build(),
226 llm_sim_config: None,
227 default_model: None,
228 backends: None,
229 session_file_system_factory_context: SessionFileSystemFactoryContext::new(),
230 harnesses: Vec::new(),
231 agents: Vec::new(),
232 sessions: Vec::new(),
233 default_session_id: None,
234 seeded_files: Vec::new(),
235 mcp_auth_provider: None,
236 plugin_capability_configs: Vec::new(),
237 plugin_warnings: Vec::new(),
238 }
239 }
240
241 pub fn mcp_auth_provider(mut self, provider: Arc<dyn everruns_mcp::McpAuthProvider>) -> Self {
245 self.mcp_auth_provider = Some(provider);
246 self
247 }
248
249 pub fn platform_definition(mut self, platform_definition: PlatformDefinition) -> Self {
251 self.platform_definition = platform_definition;
252 self
253 }
254
255 pub fn capability<C: Capability + 'static>(mut self, capability: C) -> Self {
257 self.platform_definition
258 .capability_registry_mut()
259 .register(capability);
260 self
261 }
262
263 pub fn driver_registry(mut self, driver_registry: DriverRegistry) -> Self {
265 *self.platform_definition.driver_registry_mut() = driver_registry;
266 self
267 }
268
269 pub fn llm_sim(mut self, config: LlmSimConfig) -> Self {
271 self.llm_sim_config = Some(config);
272 self
273 }
274
275 pub fn default_model(mut self, model: ResolvedModel) -> Self {
277 self.default_model = Some(model);
278 self
279 }
280
281 pub fn backends(mut self, backends: RuntimeBackends) -> Self {
283 self.backends = Some(backends);
284 self
285 }
286
287 pub fn with_session_task_registry(
291 mut self,
292 registry: Arc<dyn everruns_core::session_task::SessionTaskRegistry>,
293 ) -> Self {
294 let backends = self
295 .backends
296 .take()
297 .unwrap_or_else(RuntimeBackends::in_memory);
298 self.backends = Some(backends.with_session_task_registry(registry));
299 self
300 }
301
302 pub fn with_schedule_store_factory(
304 mut self,
305 factory: crate::backends::ScheduleStoreFactory,
306 ) -> Self {
307 let backends = self
308 .backends
309 .take()
310 .unwrap_or_else(RuntimeBackends::in_memory);
311 self.backends = Some(backends.with_schedule_store_factory(factory));
312 self
313 }
314
315 pub fn with_platform_store_factory(
317 mut self,
318 factory: crate::backends::PlatformStoreFactory,
319 ) -> Self {
320 let backends = self
321 .backends
322 .take()
323 .unwrap_or_else(RuntimeBackends::in_memory);
324 self.backends = Some(backends.with_platform_store_factory(factory));
325 self
326 }
327
328 pub fn session_file_system_factory_context(
330 mut self,
331 context: SessionFileSystemFactoryContext,
332 ) -> Self {
333 self.session_file_system_factory_context = context;
334 self
335 }
336
337 pub fn harness(mut self, harness: Harness) -> Self {
339 self.harnesses.push(harness);
340 self
341 }
342
343 pub fn agent(mut self, agent: Agent) -> Self {
345 self.agents.push(agent);
346 self
347 }
348
349 pub fn session(mut self, session: Session) -> Self {
351 self.sessions.push(session);
352 self
353 }
354
355 pub fn single_session<F>(mut self, configure: F) -> Self
360 where
361 F: FnOnce(SingleSessionBuilder) -> SingleSessionBuilder,
362 {
363 let (harness, agent, session, session_id) =
364 configure(SingleSessionBuilder::default()).build();
365 self.harnesses.push(harness);
366 self.agents.push(agent);
367 self.sessions.push(session);
368 self.default_session_id = Some(session_id);
369 self
370 }
371
372 pub fn seed_text_file(
376 mut self,
377 session_id: SessionId,
378 path: impl Into<String>,
379 content: impl Into<String>,
380 ) -> Self {
381 self.seeded_files.push((
382 session_id,
383 InitialFile {
384 path: path.into(),
385 content: content.into(),
386 encoding: "text".to_string(),
387 is_readonly: false,
388 },
389 ));
390 self
391 }
392
393 pub fn with_plugin_dir(mut self, path: &Path) -> Result<Self> {
414 let file_set = PluginFileSet::from_dir(path)
415 .map_err(|e| AgentLoopError::config(format!("plugin directory load failed: {e}")))?;
416 let compiled = compile_plugin(&file_set)
417 .map_err(|e| AgentLoopError::config(format!("plugin compilation failed: {e}")))?;
418
419 for warning in &compiled.warnings {
420 tracing::warn!(plugin = %compiled.definition.name, warning = %warning, "plugin compile warning");
421 }
422 self.plugin_warnings.extend(compiled.warnings);
423
424 let cap_id = plugin_capability_id(&compiled.definition.name);
425 let hydrated_config = serde_json::to_value(&compiled.definition)
426 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
427 self.plugin_capability_configs
428 .push(AgentCapabilityConfig::with_config(cap_id, hydrated_config));
429
430 Ok(self)
431 }
432
433 pub fn plugin_capability(&self, name: &str) -> Option<AgentCapabilityConfig> {
439 let cap_id = plugin_capability_id(name);
440 self.plugin_capability_configs
441 .iter()
442 .find(|c| c.capability_id() == cap_id)
443 .cloned()
444 }
445
446 pub async fn build(mut self) -> Result<InProcessRuntime> {
451 let backends = match self.backends.take() {
452 Some(backends) => backends,
453 None => RuntimeBackends::in_memory(),
454 };
455 let file_store = resolve_session_file_system(
456 &self.platform_definition,
457 self.session_file_system_factory_context.clone(),
458 )
459 .await?;
460
461 if let Some(config) = self.llm_sim_config.take() {
462 let driver = LlmSimDriver::new(config);
463 self.platform_definition
466 .driver_registry_mut()
467 .register_or_replace(DriverId::LlmSim, move |_config| Box::new(driver.clone()));
468
469 if self.default_model.is_none() {
470 self.default_model = Some(ResolvedModel {
471 model: "llmsim-model".to_string(),
472 provider_type: DriverId::LlmSim,
473 api_key: Some("fake-key".to_string()),
474 base_url: None,
475 provider_metadata: None,
476 });
477 }
478 }
479
480 let default_model = self.default_model.ok_or_else(|| {
481 AgentLoopError::config(
482 "in-process runtime requires a default model; call \
483 InProcessRuntimeBuilder::default_model(...) or \
484 InProcessRuntimeBuilder::llm_sim(...)",
485 )
486 })?;
487
488 backends
489 .provider_store
490 .set_default_model(default_model)
491 .await?;
492
493 for harness in &mut self.harnesses {
497 hydrate_plugin_refs(&mut harness.capabilities, &self.plugin_capability_configs);
498 }
499 for agent in &mut self.agents {
500 hydrate_plugin_refs(&mut agent.capabilities, &self.plugin_capability_configs);
501 }
502 for session in &mut self.sessions {
503 hydrate_plugin_refs(&mut session.capabilities, &self.plugin_capability_configs);
504 }
505
506 for harness in &self.harnesses {
507 backends.harness_store.add_harness(harness.clone()).await?;
508 }
509 for agent in &self.agents {
510 backends.agent_store.add_agent(agent.clone()).await?;
511 }
512 for session in &self.sessions {
513 backends.session_store.add_session(session.clone()).await?;
514 }
515
516 for session in &self.sessions {
517 seed_runtime_initial_files(
518 backends.harness_store.as_ref(),
519 backends.agent_store.as_ref(),
520 file_store.as_ref(),
521 session,
522 )
523 .await?;
524 }
525
526 for (session_id, file) in &self.seeded_files {
527 file_store.seed_initial_file(*session_id, file).await?;
528 }
529
530 let persisting_emitter =
531 PersistingEventEmitter::new(backends.event_bus.clone(), backends.message_store.clone());
532
533 Ok(InProcessRuntime {
534 platform_definition: Arc::new(self.platform_definition),
535 harness_store: backends.harness_store,
536 agent_store: backends.agent_store,
537 session_store: backends.session_store,
538 default_session_id: self.default_session_id,
539 message_store: backends.message_store,
540 provider_store: backends.provider_store,
541 event_bus: backends.event_bus,
542 persisting_emitter,
543 file_store,
544 storage_store: backends.storage_store,
545 connection_resolver: backends.connection_resolver,
546 session_task_registry: backends.session_task_registry,
547 schedule_store_factory: backends.schedule_store_factory,
548 platform_store_factory: backends.platform_store_factory,
549 mcp_auth_provider: self
550 .mcp_auth_provider
551 .unwrap_or_else(|| Arc::new(everruns_mcp::NoAuthProvider)),
552 mcp_discovery_cache: Arc::new(crate::mcp_cache::McpDiscoveryCache::new()),
553 plugin_warnings: self.plugin_warnings,
554 })
555 }
556}
557
558async fn resolve_session_file_system(
559 platform_definition: &PlatformDefinition,
560 file_system_factory_context: SessionFileSystemFactoryContext,
561) -> Result<Arc<dyn SessionFileSystem>> {
562 let file_system_factory = platform_definition.session_file_system_factory();
563 if file_system_factory.is_disabled() {
564 Ok(Arc::new(InMemorySessionFileStore::new()))
565 } else {
566 Ok(file_system_factory
567 .create_session_file_system(file_system_factory_context)
568 .await?)
569 }
570}
571
572#[derive(Clone)]
573pub struct InProcessRuntime {
579 platform_definition: Arc<PlatformDefinition>,
580 harness_store: Arc<dyn RuntimeHarnessStore>,
581 agent_store: Arc<dyn RuntimeAgentStore>,
582 session_store: Arc<dyn RuntimeSessionStore>,
583 default_session_id: Option<SessionId>,
584 message_store: Arc<dyn RuntimeMessageStore>,
585 provider_store: Arc<dyn RuntimeProviderStore>,
586 event_bus: Arc<dyn EventBus>,
587 persisting_emitter: PersistingEventEmitter,
588 file_store: Arc<dyn SessionFileSystem>,
589 storage_store: Arc<dyn SessionStorageStore>,
590 connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
591 session_task_registry: Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>>,
592 schedule_store_factory: Option<crate::backends::ScheduleStoreFactory>,
593 platform_store_factory: Option<crate::backends::PlatformStoreFactory>,
594 mcp_auth_provider: Arc<dyn everruns_mcp::McpAuthProvider>,
595 mcp_discovery_cache: Arc<crate::mcp_cache::McpDiscoveryCache>,
596 plugin_warnings: Vec<String>,
599}
600
601impl InProcessRuntime {
602 fn mcp_client(&self) -> Arc<everruns_mcp::McpClient> {
605 Arc::new(everruns_mcp::McpClient::new(
606 self.platform_definition.egress_service(),
607 self.mcp_auth_provider.clone(),
608 ))
609 }
610
611 async fn session_mcp_servers(
613 &self,
614 session: &Session,
615 agent: Option<&Agent>,
616 ) -> everruns_core::ScopedMcpServers {
617 let harness_chain = self
618 .harness_store
619 .get_harness_chain(session.harness_id)
620 .await
621 .unwrap_or_default();
622 crate::mcp::merge_session_scoped_servers(&harness_chain, agent, session)
623 }
624 pub fn builder() -> InProcessRuntimeBuilder {
626 InProcessRuntimeBuilder::new()
627 }
628
629 pub fn default_session_id(&self) -> Option<SessionId> {
632 self.default_session_id
633 }
634
635 pub fn plugin_warnings(&self) -> &[String] {
640 &self.plugin_warnings
641 }
642
643 pub async fn run_turn(
649 &self,
650 session_id: SessionId,
651 input: impl Into<InputMessage>,
652 ) -> Result<TurnResult> {
653 let session = self
654 .session_store
655 .get_session(session_id)
656 .await?
657 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
658
659 let input_message = self
664 .message_store
665 .add_input_message(session_id, input.into())
666 .await?;
667 self.event_bus
668 .emit(EventRequest::new(
669 session_id,
670 EventContext::empty(),
671 InputMessageData::new(input_message.clone()),
672 ))
673 .await?;
674
675 let assembled = self
676 .inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
677 .await?;
678 let synthetic_agent_id = session
679 .agent_id
680 .unwrap_or_else(|| AgentId::from_uuid(session.id.uuid()));
681 let org_id = in_process_internal_org_id(&session.organization_id);
682 let mut state_machine = TurnStateMachine::new(
683 TurnContext::new(session_id, input_message.id, synthetic_agent_id, org_id),
684 assembled.runtime_agent.max_iterations,
685 );
686
687 let mut previous_response_id: Option<String> = None;
688 let mut last_reason_result: Option<everruns_core::ReasonResult> = None;
689
690 loop {
691 match state_machine.next_action() {
692 TurnAction::ExecuteInput => {
693 let ctx = state_machine.context();
694 let base_context =
695 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
696 .with_workspace_id(session.workspace_id);
697 execute_input_activity(
698 self,
699 org_id,
700 InputAtomInput {
701 context: base_context,
702 },
703 )
704 .await?;
705 state_machine.on_input_completed();
706 }
707 TurnAction::ExecuteReason => {
708 let ctx = state_machine.context();
709 let base_context =
710 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
711 .with_workspace_id(session.workspace_id);
712 let reason_result = execute_reason_activity(
713 self,
714 org_id,
715 ReasonInput {
716 context: base_context.next_exec(),
717 harness_id: session.harness_id,
718 agent_id: session.agent_id,
719 org_id,
720 mcp_tool_definitions: vec![],
721 previous_response_id: previous_response_id.take(),
722 iteration: state_machine.current_iteration() as u32 + 1,
723 },
724 )
725 .await?;
726 previous_response_id = reason_result.response_id.clone();
727 state_machine.on_reason_completed(
728 reason_result.text.clone(),
729 reason_result.has_tool_calls,
730 reason_result.tool_calls.len(),
731 reason_result.success,
732 reason_result.error.clone(),
733 false,
734 );
735 if reason_result.has_tool_calls {
736 last_reason_result = Some(reason_result);
737 }
738 }
739 TurnAction::ExecuteAct => {
740 let reason_result = last_reason_result
741 .take()
742 .expect("ExecuteAct requires a prior ReasonResult");
743 let ctx = state_machine.context();
744 let base_context =
745 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
746 .with_workspace_id(session.workspace_id);
747 execute_act_activity(
748 self,
749 ActInput {
750 org_id: Some(org_id),
751 context: base_context.next_exec(),
752 harness_id: session.harness_id,
753 agent_id: session.agent_id,
754 tool_calls: reason_result.tool_calls,
755 tool_definitions: reason_result.tool_definitions,
756 locale: reason_result.locale,
757 blueprint_id: None,
758 network_access: reason_result.network_access,
759 parallel_tool_calls: reason_result.parallel_tool_calls,
762 },
763 )
764 .await?;
765 state_machine.on_act_completed();
766 }
767 TurnAction::Complete(outcome) => {
768 let ctx = state_machine.context();
769 let lifecycle =
770 RuntimeSessionLifecycle::new(self.clone(), org_id, ctx.session_id);
771 let turn_succeeded = matches!(
772 &outcome,
773 TurnOutcome::Success { .. } | TurnOutcome::MaxIterationsReached { .. }
774 );
775 match &outcome {
776 TurnOutcome::Success { iterations, .. }
777 | TurnOutcome::MaxIterationsReached { iterations, .. } => {
778 lifecycle
779 .turn_completed(
780 ctx.turn_id,
781 ctx.input_message_id,
782 *iterations as u32,
783 None,
784 None,
785 )
786 .await;
787 }
788 TurnOutcome::Failed { error, .. } => {
789 lifecycle
790 .turn_failed(ctx.turn_id, ctx.input_message_id, error, None)
791 .await;
792 }
793 TurnOutcome::Sealed {
794 reason, iterations, ..
795 } => {
796 lifecycle
797 .turn_sealed(
798 ctx.turn_id,
799 ctx.input_message_id,
800 reason.as_str(),
801 *iterations as u32,
802 None,
803 )
804 .await;
805 }
806 }
807 lifecycle
810 .fire_turn_end_hooks(
811 session.harness_id,
812 session.agent_id,
813 ctx.turn_id,
814 turn_succeeded,
815 )
816 .await;
817 return Ok(TurnResult::from_outcome(outcome, ctx.turn_id));
818 }
819 }
820 }
821 }
822
823 pub async fn run_text_turn(
824 &self,
825 session_id: SessionId,
826 text: impl Into<String>,
827 ) -> Result<TurnResult> {
828 self.run_turn(session_id, InputMessage::user(text)).await
829 }
830
831 pub async fn messages(&self, session_id: SessionId) -> Result<Vec<Message>> {
833 self.message_store.load(session_id).await
834 }
835
836 pub async fn read_file(
838 &self,
839 session_id: SessionId,
840 path: &str,
841 ) -> Result<Option<SessionFile>> {
842 self.file_store.read_file(session_id, path).await
843 }
844
845 pub async fn load_context(&self, session_id: SessionId) -> Result<AssembledTurnContext> {
847 let session = self
848 .session_store
849 .get_session(session_id)
850 .await?
851 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
852 self.inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
853 .await
854 }
855
856 pub async fn events(&self) -> Result<Vec<Event>> {
861 Ok(self.event_bus.collected_events().await)
862 }
863
864 pub async fn execute_command(
874 &self,
875 session_id: SessionId,
876 request: everruns_core::command::ExecuteCommandRequest,
877 ) -> Result<everruns_core::command::CommandResult> {
878 let ctx = self.load_context(session_id).await?;
879 let registry = self.platform_definition.capability_registry();
880 let host = everruns_core::command_host::StoreCommandHost::new(
884 session_id,
885 self.harness_store.clone(),
886 self.agent_store.clone(),
887 self.session_store.clone(),
888 self.message_store.clone(),
889 self.provider_store.clone(),
890 registry.clone(),
891 self.platform_definition.driver_registry().clone(),
892 )
893 .with_file_store(self.file_store.clone())
894 .with_assembled_context(ctx.clone());
895 let exec_ctx =
896 everruns_core::command::CommandExecutionContext::new(session_id, Arc::new(host));
897 for config in &ctx.resolved_capability_configs {
898 let Some(capability) = registry.get(config.capability_id()) else {
899 continue;
900 };
901 if capability.commands().iter().any(|c| c.name == request.name) {
902 return capability.execute_command(&request, &exec_ctx).await;
903 }
904 }
905 Err(AgentLoopError::config(format!(
906 "no capability declares command /{}",
907 request.name
908 )))
909 }
910
911 pub async fn list_commands(
921 &self,
922 session_id: SessionId,
923 ) -> Result<Vec<everruns_core::command::CommandDescriptor>> {
924 let ctx = self.load_context(session_id).await?;
925 let registry = self.platform_definition.capability_registry();
926 let mut seen = std::collections::HashSet::new();
927 let mut commands = Vec::new();
928 for config in &ctx.resolved_capability_configs {
929 let Some(capability) = registry.get(config.capability_id()) else {
930 continue;
931 };
932 for command in capability.commands() {
933 if seen.insert(command.name.clone()) {
934 commands.push(command);
935 }
936 }
937 }
938 Ok(commands)
939 }
940
941 async fn inspect_context_with_ids(
942 &self,
943 session_id: SessionId,
944 harness_id: everruns_core::HarnessId,
945 agent_id: Option<AgentId>,
946 ) -> Result<AssembledTurnContext> {
947 inspect_turn_context(
948 self.harness_store.as_ref(),
949 self.agent_store.as_ref(),
950 self.session_store.as_ref(),
951 self.message_store.as_ref(),
952 self.provider_store.as_ref(),
953 self.platform_definition.capability_registry(),
954 session_id,
955 harness_id,
956 agent_id,
957 &[],
958 Some(self.file_store.clone()),
959 )
960 .await
961 }
962}
963
964#[async_trait]
965impl RuntimeHostAdapter for InProcessRuntime {
966 async fn get_agent(&self, _org_id: i64, agent_id: AgentId) -> Result<Option<Agent>> {
967 self.agent_store.get_agent(agent_id).await
968 }
969
970 async fn get_harness(&self, _org_id: i64, harness_id: HarnessId) -> Result<Option<Harness>> {
971 let chain = self.harness_store.get_harness_chain(harness_id).await?;
972 Ok(chain.into_iter().last())
973 }
974
975 async fn set_session_status(
976 &self,
977 _org_id: i64,
978 session_id: SessionId,
979 _status: SessionStatus,
980 ) -> Result<Session> {
981 self.session_store
985 .get_session(session_id)
986 .await?
987 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))
988 }
989
990 async fn load_turn_context(
991 &self,
992 _org_id: i64,
993 session_id: SessionId,
994 ) -> Result<RuntimeHostTurnContext> {
995 let mut session = self
996 .session_store
997 .get_session(session_id)
998 .await?
999 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1000 everruns_core::ard_attachment::apply_session_attachments(
1003 self.storage_store.as_ref(),
1004 &mut session,
1005 )
1006 .await;
1007 let agent = match session.agent_id {
1008 Some(agent_id) => self.agent_store.get_agent(agent_id).await?,
1009 None => None,
1010 };
1011 let messages = self.message_store.load(session_id).await?;
1012 let model = self.provider_store.get_default_model().await?;
1013
1014 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1017 let mcp_tool_definitions = if scoped_servers.is_empty() {
1018 vec![]
1019 } else {
1020 crate::mcp::discover_tool_definitions(
1021 &self.mcp_discovery_cache,
1022 self.mcp_client(),
1023 session_id.uuid(),
1024 &scoped_servers,
1025 )
1026 .await
1027 };
1028
1029 Ok(RuntimeHostTurnContext {
1030 agent,
1031 session,
1032 messages,
1033 model,
1034 mcp_tool_definitions,
1035 })
1036 }
1037
1038 async fn mcp_executor(
1039 &self,
1040 _org_id: i64,
1041 session_id: SessionId,
1042 ) -> Option<Arc<everruns_mcp::McpExecutor>> {
1043 let session = self.session_store.get_session(session_id).await.ok()??;
1044 let agent = match session.agent_id {
1045 Some(agent_id) => self.agent_store.get_agent(agent_id).await.ok().flatten(),
1046 None => None,
1047 };
1048 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1049 crate::mcp::build_executor(self.mcp_client(), &scoped_servers)
1050 }
1051
1052 fn capability_registry(&self) -> CapabilityRegistry {
1053 self.platform_definition.capability_registry().clone()
1054 }
1055
1056 fn driver_registry(&self) -> DriverRegistry {
1057 self.platform_definition.driver_registry().clone()
1058 }
1059
1060 fn harness_store(&self, _org_id: i64) -> Arc<dyn HarnessStore> {
1061 self.harness_store.clone()
1062 }
1063
1064 fn agent_store(&self, _org_id: i64) -> Arc<dyn AgentStore> {
1065 self.agent_store.clone()
1066 }
1067
1068 fn session_store(&self, _org_id: i64) -> Arc<dyn SessionStore> {
1069 self.session_store.clone()
1070 }
1071
1072 fn session_mutator(&self, _org_id: i64) -> Arc<dyn SessionMutator> {
1073 self.session_store.clone()
1074 }
1075
1076 fn provider_store(&self, _org_id: i64) -> Arc<dyn ProviderStore> {
1077 self.provider_store.clone()
1078 }
1079
1080 fn message_store(&self) -> Arc<dyn MessageRetriever> {
1081 self.message_store.clone()
1082 }
1083
1084 fn event_emitter(&self) -> Arc<dyn EventEmitter> {
1085 Arc::new(self.persisting_emitter.clone())
1086 }
1087
1088 fn file_store(&self) -> Arc<dyn SessionFileSystem> {
1089 self.file_store.clone()
1090 }
1091
1092 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
1093 Some(self.storage_store.clone())
1094 }
1095
1096 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
1097 self.connection_resolver.clone()
1098 }
1099
1100 fn session_task_registry(
1101 &self,
1102 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
1103 self.session_task_registry.clone()
1104 }
1105
1106 fn schedule_store(
1107 &self,
1108 org_id: i64,
1109 ) -> Option<Arc<dyn everruns_core::traits::SessionScheduleStore>> {
1110 self.schedule_store_factory
1111 .as_ref()
1112 .map(|factory| factory(org_id))
1113 }
1114
1115 fn platform_store(
1116 &self,
1117 org_id: i64,
1118 session_id: SessionId,
1119 ) -> Option<Arc<dyn everruns_core::platform_store::PlatformStore>> {
1120 self.platform_store_factory
1121 .as_ref()
1122 .map(|factory| factory(org_id, session_id))
1123 }
1124
1125 fn utility_llm_service(&self) -> Option<Arc<dyn everruns_core::UtilityLlmService>> {
1126 Some(self.platform_definition.utility_llm_service())
1127 }
1128
1129 fn egress_service(&self) -> Option<Arc<dyn everruns_core::EgressService>> {
1130 Some(self.platform_definition.egress_service())
1131 }
1132}
1133
1134#[derive(Clone)]
1135struct PersistingEventEmitter {
1136 inner: Arc<dyn EventBus>,
1137 message_store: Arc<dyn RuntimeMessageStore>,
1138}
1139
1140impl PersistingEventEmitter {
1141 fn new(inner: Arc<dyn EventBus>, message_store: Arc<dyn RuntimeMessageStore>) -> Self {
1142 Self {
1143 inner,
1144 message_store,
1145 }
1146 }
1147}
1148
1149#[async_trait]
1150impl EventEmitter for PersistingEventEmitter {
1151 async fn emit(&self, request: EventRequest) -> Result<Event> {
1152 let event = self.inner.emit(request.clone()).await?;
1153 if let Some(message) = message_from_event(&event.data) {
1154 self.message_store
1155 .store_message(request.session_id, message)
1156 .await?;
1157 }
1158 Ok(event)
1159 }
1160}
1161
1162fn effective_overlay(
1163 harness_chain: &[Harness],
1164 agent: Option<&Agent>,
1165 session: &Session,
1166) -> AgentConfigOverlay {
1167 let harness_layers = harness_chain.iter().map(AgentConfigOverlay::from);
1168 let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
1169 AgentConfigOverlay::fold(
1170 harness_layers
1171 .chain(agent_layers)
1172 .chain([AgentConfigOverlay::from(session)]),
1173 )
1174}
1175
1176fn hydrate_plugin_refs(
1183 capabilities: &mut [AgentCapabilityConfig],
1184 plugin_configs: &[AgentCapabilityConfig],
1185) {
1186 for cap in capabilities.iter_mut() {
1187 let cap_id = cap.capability_id();
1188 if !everruns_core::is_plugin_capability(cap_id) {
1189 continue;
1190 }
1191 let is_bare = cap.config.is_null()
1193 || cap
1194 .config
1195 .as_object()
1196 .map(|o| o.is_empty())
1197 .unwrap_or(false);
1198 if !is_bare {
1199 continue;
1200 }
1201 if let Some(hydrated) = plugin_configs.iter().find(|c| c.capability_id() == cap_id) {
1202 cap.config = hydrated.config.clone();
1203 }
1204 }
1205}
1206
1207async fn seed_runtime_initial_files(
1208 harness_store: &dyn RuntimeHarnessStore,
1209 agent_store: &dyn RuntimeAgentStore,
1210 file_store: &dyn SessionFileSystem,
1211 session: &Session,
1212) -> Result<()> {
1213 let harness_chain = harness_store.get_harness_chain(session.harness_id).await?;
1214 if harness_chain.is_empty() {
1215 return Err(AgentLoopError::store(format!(
1216 "harness not found while seeding files: {}",
1217 session.harness_id
1218 )));
1219 }
1220 let agent = match session.agent_id {
1221 Some(agent_id) => Some(
1222 agent_store
1223 .get_agent(agent_id)
1224 .await?
1225 .ok_or_else(|| AgentLoopError::store(format!("agent not found: {agent_id}")))?,
1226 ),
1227 None => None,
1228 };
1229 let overlay = effective_overlay(&harness_chain, agent.as_ref(), session);
1230 let seed_key = SessionId::from_uuid(session.workspace_id.uuid());
1233 for file in &overlay.initial_files {
1234 file_store.seed_initial_file(seed_key, file).await?;
1235 }
1236 Ok(())
1237}
1238
1239fn message_from_event(data: &EventData) -> Option<Message> {
1240 match data {
1241 EventData::InputMessage(data) => Some(data.message.clone()),
1242 EventData::OutputMessageCompleted(OutputMessageCompletedData { message, .. }) => {
1243 Some(message.clone())
1244 }
1245 EventData::ToolCompleted(data) => Some(tool_completed_to_message(data.clone())),
1246 _ => None,
1247 }
1248}
1249
1250fn tool_completed_to_message(data: ToolCompletedData) -> Message {
1251 let mut images: Vec<ToolResultImage> = Vec::new();
1252 let metadata = tool_result_metadata(&data);
1253 let result = data.result.map(|parts| {
1254 for part in &parts {
1255 if let ContentPart::Image(img) = part
1256 && let (Some(base64), Some(media_type)) = (&img.base64, &img.media_type)
1257 {
1258 images.push(ToolResultImage {
1259 base64: base64.clone(),
1260 media_type: media_type.clone(),
1261 });
1262 }
1263 }
1264
1265 let text_parts: Vec<&ContentPart> = parts
1266 .iter()
1267 .filter(|part| matches!(part, ContentPart::Text(_)))
1268 .collect();
1269 if text_parts.len() == 1
1270 && let ContentPart::Text(text) = text_parts[0]
1271 {
1272 return parse_structured_tool_result_text(&text.text);
1273 }
1274 if !text_parts.is_empty() {
1275 serde_json::to_value(&text_parts).unwrap_or_default()
1276 } else {
1277 serde_json::Value::Null
1278 }
1279 });
1280
1281 let mut message = if images.is_empty() {
1282 Message::tool_result(&data.tool_call_id, result, data.error)
1283 } else {
1284 Message::tool_result_with_images(&data.tool_call_id, result, images)
1285 };
1286 message.metadata = metadata;
1287 message
1288}
1289
1290fn tool_result_metadata(
1291 data: &ToolCompletedData,
1292) -> Option<std::collections::HashMap<String, serde_json::Value>> {
1293 let mut metadata = std::collections::HashMap::new();
1294 metadata.insert("tool_name".to_string(), serde_json::json!(data.tool_name));
1295 if let Some(fingerprint) = &data.tool_call_fingerprint {
1296 metadata.insert(
1297 "tool_call_fingerprint".to_string(),
1298 serde_json::json!(fingerprint),
1299 );
1300 }
1301 if let Some(fingerprint) = &data.tool_result_fingerprint {
1302 metadata.insert(
1303 "tool_result_fingerprint".to_string(),
1304 serde_json::json!(fingerprint),
1305 );
1306 }
1307 (!metadata.is_empty()).then_some(metadata)
1308}
1309
1310fn parse_structured_tool_result_text(text: &str) -> serde_json::Value {
1311 let trimmed = text.trim_start();
1312 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
1313 return serde_json::Value::String(text.to_string());
1314 }
1315
1316 match serde_json::from_str(text) {
1317 Ok(value @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => value,
1318 _ => serde_json::Value::String(text.to_string()),
1319 }
1320}
1321
1322#[cfg(test)]
1323mod tool_completed_replay_tests {
1324 use super::*;
1325
1326 #[test]
1327 fn tool_completed_replay_preserves_json_object_shape() {
1328 let data = ToolCompletedData::success(
1329 "call_read".to_string(),
1330 "read_file".to_string(),
1331 vec![ContentPart::text(
1332 serde_json::json!({
1333 "path": "/workspace/src/lib.rs",
1334 "content": "1|fn main() {}"
1335 })
1336 .to_string(),
1337 )],
1338 Some(1),
1339 );
1340
1341 let message = tool_completed_to_message(data);
1342 let result = message
1343 .tool_result_content()
1344 .and_then(|content| content.result.as_ref())
1345 .expect("tool result should be present");
1346
1347 assert_eq!(result["path"], "/workspace/src/lib.rs");
1348 assert_eq!(result["content"], "1|fn main() {}");
1349 }
1350
1351 #[test]
1352 fn tool_completed_replay_keeps_scalar_json_as_text() {
1353 let data = ToolCompletedData::success(
1354 "call_scalar".to_string(),
1355 "custom_tool".to_string(),
1356 vec![ContentPart::text("123")],
1357 Some(1),
1358 );
1359
1360 let message = tool_completed_to_message(data);
1361 let result = message
1362 .tool_result_content()
1363 .and_then(|content| content.result.as_ref())
1364 .expect("tool result should be present");
1365
1366 assert_eq!(result, &serde_json::Value::String("123".to_string()));
1367 }
1368
1369 #[test]
1370 fn tool_completed_replay_preserves_fingerprints_as_metadata() {
1371 let data = ToolCompletedData::success(
1372 "call_read".to_string(),
1373 "read_file".to_string(),
1374 vec![ContentPart::text("{}")],
1375 Some(1),
1376 )
1377 .with_fingerprints("sha256:call".to_string(), "sha256:result".to_string());
1378
1379 let message = tool_completed_to_message(data);
1380 let metadata = message.metadata.expect("metadata should be present");
1381
1382 assert_eq!(metadata["tool_name"], "read_file");
1383 assert_eq!(metadata["tool_call_fingerprint"], "sha256:call");
1384 assert_eq!(metadata["tool_result_fingerprint"], "sha256:result");
1385 }
1386}
1387
1388#[cfg(test)]
1389mod org_id_mapping_tests {
1390 use super::*;
1391 use everruns_core::{DEFAULT_ORG_ID, DEFAULT_ORG_PUBLIC_ID, org_public_id_from_internal};
1392
1393 #[test]
1394 fn default_public_id_maps_to_default_org() {
1395 assert_eq!(
1396 in_process_internal_org_id(DEFAULT_ORG_PUBLIC_ID),
1397 DEFAULT_ORG_ID
1398 );
1399 }
1400
1401 #[test]
1402 fn invalid_public_id_does_not_fall_back_to_default() {
1403 for invalid in [
1404 "",
1405 "not-an-org",
1406 "org_short",
1407 "org_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
1408 "ORG_00000000000000000000000000000001",
1409 ] {
1410 let mapped = in_process_internal_org_id(invalid);
1411 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1412 assert!(
1413 mapped >= 2,
1414 "invalid input {invalid:?} should not map to default"
1415 );
1416 }
1417 }
1418
1419 #[test]
1420 fn zero_public_id_does_not_fall_back_to_default() {
1421 let mapped = in_process_internal_org_id("org_00000000000000000000000000000000");
1424 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1425 assert!(mapped >= 2, "all-zero id should not map to default");
1426 }
1427
1428 #[test]
1429 fn synthetic_public_id_round_trips_with_internal_helper() {
1430 for internal in [1_i64, 2, 42, 1_000_000, i64::MAX - 1, i64::MAX] {
1431 let public = org_public_id_from_internal(internal);
1432 assert_eq!(
1433 in_process_internal_org_id(&public),
1434 internal,
1435 "round-trip failed for internal={internal}"
1436 );
1437 }
1438 }
1439
1440 #[test]
1441 fn distinct_synthetic_ids_map_to_distinct_internal_ids() {
1442 let a = org_public_id_from_internal(7);
1443 let b = org_public_id_from_internal(8);
1444 assert_ne!(a, b);
1445 assert_ne!(
1446 in_process_internal_org_id(&a),
1447 in_process_internal_org_id(&b)
1448 );
1449 }
1450
1451 #[test]
1452 fn high_entropy_uuid_style_id_hashes_into_reserved_range() {
1453 let high = "org_80000000000000000000000000000000";
1457 let mapped = in_process_internal_org_id(high);
1458 assert!(mapped >= 2, "mapped id {mapped} must be >= 2");
1459 assert_ne!(mapped, DEFAULT_ORG_ID);
1460
1461 assert_eq!(mapped, in_process_internal_org_id(high));
1463 }
1464
1465 #[test]
1466 fn high_entropy_ids_are_isolated_from_each_other() {
1467 let a = in_process_internal_org_id("org_80000000000000000000000000000001");
1468 let b = in_process_internal_org_id("org_80000000000000000000000000000002");
1469 assert_ne!(a, b);
1470 assert_ne!(a, DEFAULT_ORG_ID);
1471 assert_ne!(b, DEFAULT_ORG_ID);
1472 }
1473
1474 #[test]
1475 fn hash_uses_stable_sha256_truncation() {
1476 let mapped = in_process_internal_org_id("org_80000000000000000000000000000000");
1480 let expected = {
1481 let digest = sha2::Sha256::digest(b"org_80000000000000000000000000000000");
1482 let mut buf = [0u8; 8];
1483 buf.copy_from_slice(&digest[..8]);
1484 let raw = u64::from_be_bytes(buf);
1485 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
1486 };
1487 assert_eq!(mapped, expected);
1488 }
1489
1490 #[test]
1491 fn oversize_input_is_bounded_and_does_not_collide_silently() {
1492 let oversize = "x".repeat(super::HASH_INPUT_CAP_BYTES * 4);
1498 let mapped = in_process_internal_org_id(&oversize);
1499 assert!(mapped >= 2);
1500 assert_ne!(mapped, DEFAULT_ORG_ID);
1501 }
1502}