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 let (session_task_registry, session_wake_queue) = match backends.session_task_registry {
539 Some(inner) => {
540 let wake_queue = Arc::new(everruns_core::SessionWakeQueue::new());
541 let observing = everruns_core::ObservingTaskRegistry::new(inner)
542 .with_observer(wake_queue.clone());
543 let wrapped: Arc<dyn everruns_core::session_task::SessionTaskRegistry> =
544 Arc::new(observing);
545 (Some(wrapped), Some(wake_queue))
546 }
547 None => (None, None),
548 };
549
550 Ok(InProcessRuntime {
551 platform_definition: Arc::new(self.platform_definition),
552 harness_store: backends.harness_store,
553 agent_store: backends.agent_store,
554 session_store: backends.session_store,
555 default_session_id: self.default_session_id,
556 message_store: backends.message_store,
557 provider_store: backends.provider_store,
558 event_bus: backends.event_bus,
559 persisting_emitter,
560 file_store,
561 storage_store: backends.storage_store,
562 connection_resolver: backends.connection_resolver,
563 session_task_registry,
564 session_wake_queue,
565 schedule_store_factory: backends.schedule_store_factory,
566 platform_store_factory: backends.platform_store_factory,
567 mcp_auth_provider: self
568 .mcp_auth_provider
569 .unwrap_or_else(|| Arc::new(everruns_mcp::NoAuthProvider)),
570 mcp_discovery_cache: Arc::new(crate::mcp_cache::McpDiscoveryCache::new()),
571 plugin_warnings: self.plugin_warnings,
572 })
573 }
574}
575
576async fn resolve_session_file_system(
577 platform_definition: &PlatformDefinition,
578 file_system_factory_context: SessionFileSystemFactoryContext,
579) -> Result<Arc<dyn SessionFileSystem>> {
580 let file_system_factory = platform_definition.session_file_system_factory();
581 if file_system_factory.is_disabled() {
582 Ok(Arc::new(InMemorySessionFileStore::new()))
583 } else {
584 Ok(file_system_factory
585 .create_session_file_system(file_system_factory_context)
586 .await?)
587 }
588}
589
590#[derive(Clone)]
591pub struct InProcessRuntime {
597 platform_definition: Arc<PlatformDefinition>,
598 harness_store: Arc<dyn RuntimeHarnessStore>,
599 agent_store: Arc<dyn RuntimeAgentStore>,
600 session_store: Arc<dyn RuntimeSessionStore>,
601 default_session_id: Option<SessionId>,
602 message_store: Arc<dyn RuntimeMessageStore>,
603 provider_store: Arc<dyn RuntimeProviderStore>,
604 event_bus: Arc<dyn EventBus>,
605 persisting_emitter: PersistingEventEmitter,
606 file_store: Arc<dyn SessionFileSystem>,
607 storage_store: Arc<dyn SessionStorageStore>,
608 connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
609 session_task_registry: Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>>,
610 session_wake_queue: Option<Arc<everruns_core::SessionWakeQueue>>,
614 schedule_store_factory: Option<crate::backends::ScheduleStoreFactory>,
615 platform_store_factory: Option<crate::backends::PlatformStoreFactory>,
616 mcp_auth_provider: Arc<dyn everruns_mcp::McpAuthProvider>,
617 mcp_discovery_cache: Arc<crate::mcp_cache::McpDiscoveryCache>,
618 plugin_warnings: Vec<String>,
621}
622
623impl InProcessRuntime {
624 fn mcp_client(&self) -> Arc<everruns_mcp::McpClient> {
627 Arc::new(everruns_mcp::McpClient::new(
628 self.platform_definition.egress_service(),
629 self.mcp_auth_provider.clone(),
630 ))
631 }
632
633 async fn session_mcp_servers(
635 &self,
636 session: &Session,
637 agent: Option<&Agent>,
638 ) -> everruns_core::ScopedMcpServers {
639 let harness_chain = self
640 .harness_store
641 .get_harness_chain(session.harness_id)
642 .await
643 .unwrap_or_default();
644 crate::mcp::merge_session_scoped_servers(&harness_chain, agent, session)
645 }
646 pub fn builder() -> InProcessRuntimeBuilder {
648 InProcessRuntimeBuilder::new()
649 }
650
651 pub fn default_session_id(&self) -> Option<SessionId> {
654 self.default_session_id
655 }
656
657 pub fn plugin_warnings(&self) -> &[String] {
662 &self.plugin_warnings
663 }
664
665 pub async fn run_turn(
671 &self,
672 session_id: SessionId,
673 input: impl Into<InputMessage>,
674 ) -> Result<TurnResult> {
675 let session = self
676 .session_store
677 .get_session(session_id)
678 .await?
679 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
680
681 let input_message = self
686 .message_store
687 .add_input_message(session_id, input.into())
688 .await?;
689 self.event_bus
690 .emit(EventRequest::new(
691 session_id,
692 EventContext::empty(),
693 InputMessageData::new(input_message.clone()),
694 ))
695 .await?;
696
697 let assembled = self
698 .inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
699 .await?;
700 let synthetic_agent_id = session
701 .agent_id
702 .unwrap_or_else(|| AgentId::from_uuid(session.id.uuid()));
703 let org_id = in_process_internal_org_id(&session.organization_id);
704 let mut state_machine = TurnStateMachine::new(
705 TurnContext::new(session_id, input_message.id, synthetic_agent_id, org_id),
706 assembled.runtime_agent.max_iterations,
707 );
708
709 let mut previous_response_id: Option<String> = None;
710 let mut last_reason_result: Option<everruns_core::ReasonResult> = None;
711
712 loop {
713 match state_machine.next_action() {
714 TurnAction::ExecuteInput => {
715 let ctx = state_machine.context();
716 let base_context =
717 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
718 .with_workspace_id(session.workspace_id);
719 execute_input_activity(
720 self,
721 org_id,
722 InputAtomInput {
723 context: base_context,
724 },
725 )
726 .await?;
727 state_machine.on_input_completed();
728 }
729 TurnAction::ExecuteReason => {
730 let ctx = state_machine.context();
731 let session_id = ctx.session_id;
732 self.drain_and_inject_wakes(session_id).await?;
738 let base_context =
739 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
740 .with_workspace_id(session.workspace_id);
741 let reason_result = execute_reason_activity(
742 self,
743 org_id,
744 ReasonInput {
745 context: base_context.next_exec(),
746 harness_id: session.harness_id,
747 agent_id: session.agent_id,
748 org_id,
749 mcp_tool_definitions: vec![],
750 previous_response_id: previous_response_id.take(),
751 iteration: state_machine.current_iteration() as u32 + 1,
752 },
753 )
754 .await?;
755 previous_response_id = reason_result.response_id.clone();
756 let has_pending_wakes = self
761 .session_wake_queue
762 .as_ref()
763 .is_some_and(|q| q.has_pending(session_id));
764 state_machine.on_reason_completed(
765 reason_result.text.clone(),
766 reason_result.has_tool_calls,
767 reason_result.tool_calls.len(),
768 reason_result.success,
769 reason_result.error.clone(),
770 has_pending_wakes,
771 );
772 if reason_result.has_tool_calls {
773 last_reason_result = Some(reason_result);
774 }
775 }
776 TurnAction::ExecuteAct => {
777 let reason_result = last_reason_result
778 .take()
779 .expect("ExecuteAct requires a prior ReasonResult");
780 let ctx = state_machine.context();
781 let base_context =
782 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
783 .with_workspace_id(session.workspace_id);
784 execute_act_activity(
785 self,
786 ActInput {
787 org_id: Some(org_id),
788 context: base_context.next_exec(),
789 harness_id: session.harness_id,
790 agent_id: session.agent_id,
791 tool_calls: reason_result.tool_calls,
792 tool_definitions: reason_result.tool_definitions,
793 locale: reason_result.locale,
794 blueprint_id: None,
795 network_access: reason_result.network_access,
796 parallel_tool_calls: reason_result.parallel_tool_calls,
799 },
800 )
801 .await?;
802 state_machine.on_act_completed();
803 }
804 TurnAction::Complete(outcome) => {
805 let ctx = state_machine.context();
806 let lifecycle =
807 RuntimeSessionLifecycle::new(self.clone(), org_id, ctx.session_id);
808 let turn_succeeded = matches!(
809 &outcome,
810 TurnOutcome::Success { .. } | TurnOutcome::MaxIterationsReached { .. }
811 );
812 match &outcome {
813 TurnOutcome::Success { iterations, .. }
814 | TurnOutcome::MaxIterationsReached { iterations, .. } => {
815 lifecycle
816 .turn_completed(
817 ctx.turn_id,
818 ctx.input_message_id,
819 *iterations as u32,
820 None,
821 None,
822 )
823 .await;
824 }
825 TurnOutcome::Failed { error, .. } => {
826 lifecycle
827 .turn_failed(ctx.turn_id, ctx.input_message_id, error, None)
828 .await;
829 }
830 TurnOutcome::Sealed {
831 reason, iterations, ..
832 } => {
833 lifecycle
834 .turn_sealed(
835 ctx.turn_id,
836 ctx.input_message_id,
837 reason.as_str(),
838 *iterations as u32,
839 None,
840 )
841 .await;
842 }
843 }
844 lifecycle
847 .fire_turn_end_hooks(
848 session.harness_id,
849 session.agent_id,
850 ctx.turn_id,
851 turn_succeeded,
852 )
853 .await;
854 return Ok(TurnResult::from_outcome(outcome, ctx.turn_id));
855 }
856 }
857 }
858 }
859
860 pub async fn run_text_turn(
861 &self,
862 session_id: SessionId,
863 text: impl Into<String>,
864 ) -> Result<TurnResult> {
865 self.run_turn(session_id, InputMessage::user(text)).await
866 }
867
868 async fn drain_and_inject_wakes(&self, session_id: SessionId) -> Result<usize> {
879 let Some(queue) = &self.session_wake_queue else {
880 return Ok(0);
881 };
882 let wakes = queue.drain(session_id);
883 if wakes.is_empty() {
884 return Ok(0);
885 }
886 let count = wakes.len();
887 for wake in wakes {
888 let message = self
893 .message_store
894 .add_input_message(session_id, InputMessage::user(wake.text))
895 .await?;
896 self.event_bus
897 .emit(EventRequest::new(
898 session_id,
899 EventContext::empty(),
900 InputMessageData::new(message),
901 ))
902 .await?;
903 }
904 Ok(count)
905 }
906
907 pub async fn messages(&self, session_id: SessionId) -> Result<Vec<Message>> {
909 self.message_store.load(session_id).await
910 }
911
912 pub async fn read_file(
914 &self,
915 session_id: SessionId,
916 path: &str,
917 ) -> Result<Option<SessionFile>> {
918 self.file_store.read_file(session_id, path).await
919 }
920
921 pub async fn load_context(&self, session_id: SessionId) -> Result<AssembledTurnContext> {
923 let session = self
924 .session_store
925 .get_session(session_id)
926 .await?
927 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
928 self.inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
929 .await
930 }
931
932 pub async fn events(&self) -> Result<Vec<Event>> {
937 Ok(self.event_bus.collected_events().await)
938 }
939
940 pub async fn execute_command(
950 &self,
951 session_id: SessionId,
952 request: everruns_core::command::ExecuteCommandRequest,
953 ) -> Result<everruns_core::command::CommandResult> {
954 let ctx = self.load_context(session_id).await?;
955 let registry = self.platform_definition.capability_registry();
956 let host = everruns_core::command_host::StoreCommandHost::new(
960 session_id,
961 self.harness_store.clone(),
962 self.agent_store.clone(),
963 self.session_store.clone(),
964 self.message_store.clone(),
965 self.provider_store.clone(),
966 registry.clone(),
967 self.platform_definition.driver_registry().clone(),
968 )
969 .with_file_store(self.file_store.clone())
970 .with_assembled_context(ctx.clone());
971 let exec_ctx =
972 everruns_core::command::CommandExecutionContext::new(session_id, Arc::new(host));
973 for config in &ctx.resolved_capability_configs {
974 let Some(capability) = registry.get(config.capability_id()) else {
975 continue;
976 };
977 if capability.commands().iter().any(|c| c.name == request.name) {
978 return capability.execute_command(&request, &exec_ctx).await;
979 }
980 }
981 Err(AgentLoopError::config(format!(
982 "no capability declares command /{}",
983 request.name
984 )))
985 }
986
987 pub async fn list_commands(
997 &self,
998 session_id: SessionId,
999 ) -> Result<Vec<everruns_core::command::CommandDescriptor>> {
1000 let ctx = self.load_context(session_id).await?;
1001 let registry = self.platform_definition.capability_registry();
1002 let mut seen = std::collections::HashSet::new();
1003 let mut commands = Vec::new();
1004 for config in &ctx.resolved_capability_configs {
1005 let Some(capability) = registry.get(config.capability_id()) else {
1006 continue;
1007 };
1008 for command in capability.commands() {
1009 if seen.insert(command.name.clone()) {
1010 commands.push(command);
1011 }
1012 }
1013 }
1014 Ok(commands)
1015 }
1016
1017 async fn inspect_context_with_ids(
1018 &self,
1019 session_id: SessionId,
1020 harness_id: everruns_core::HarnessId,
1021 agent_id: Option<AgentId>,
1022 ) -> Result<AssembledTurnContext> {
1023 inspect_turn_context(
1024 self.harness_store.as_ref(),
1025 self.agent_store.as_ref(),
1026 self.session_store.as_ref(),
1027 self.message_store.as_ref(),
1028 self.provider_store.as_ref(),
1029 self.platform_definition.capability_registry(),
1030 session_id,
1031 harness_id,
1032 agent_id,
1033 &[],
1034 Some(self.file_store.clone()),
1035 )
1036 .await
1037 }
1038}
1039
1040#[async_trait]
1041impl RuntimeHostAdapter for InProcessRuntime {
1042 async fn get_agent(&self, _org_id: i64, agent_id: AgentId) -> Result<Option<Agent>> {
1043 self.agent_store.get_agent(agent_id).await
1044 }
1045
1046 async fn get_harness(&self, _org_id: i64, harness_id: HarnessId) -> Result<Option<Harness>> {
1047 let chain = self.harness_store.get_harness_chain(harness_id).await?;
1048 Ok(chain.into_iter().last())
1049 }
1050
1051 async fn set_session_status(
1052 &self,
1053 _org_id: i64,
1054 session_id: SessionId,
1055 _status: SessionStatus,
1056 ) -> Result<Session> {
1057 self.session_store
1061 .get_session(session_id)
1062 .await?
1063 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))
1064 }
1065
1066 async fn load_turn_context(
1067 &self,
1068 _org_id: i64,
1069 session_id: SessionId,
1070 ) -> Result<RuntimeHostTurnContext> {
1071 let mut session = self
1072 .session_store
1073 .get_session(session_id)
1074 .await?
1075 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1076 everruns_core::ard_attachment::apply_session_attachments(
1079 self.storage_store.as_ref(),
1080 &mut session,
1081 )
1082 .await;
1083 let agent = match session.agent_id {
1084 Some(agent_id) => self.agent_store.get_agent(agent_id).await?,
1085 None => None,
1086 };
1087 let messages = self.message_store.load(session_id).await?;
1088 let model = self.provider_store.get_default_model().await?;
1089
1090 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1093 let mcp_tool_definitions = if scoped_servers.is_empty() {
1094 vec![]
1095 } else {
1096 crate::mcp::discover_tool_definitions(
1097 &self.mcp_discovery_cache,
1098 self.mcp_client(),
1099 session_id.uuid(),
1100 &scoped_servers,
1101 )
1102 .await
1103 };
1104
1105 Ok(RuntimeHostTurnContext {
1106 agent,
1107 session,
1108 messages,
1109 model,
1110 mcp_tool_definitions,
1111 })
1112 }
1113
1114 async fn mcp_executor(
1115 &self,
1116 _org_id: i64,
1117 session_id: SessionId,
1118 ) -> Option<Arc<everruns_mcp::McpExecutor>> {
1119 let session = self.session_store.get_session(session_id).await.ok()??;
1120 let agent = match session.agent_id {
1121 Some(agent_id) => self.agent_store.get_agent(agent_id).await.ok().flatten(),
1122 None => None,
1123 };
1124 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1125 crate::mcp::build_executor(self.mcp_client(), &scoped_servers)
1126 }
1127
1128 fn capability_registry(&self) -> CapabilityRegistry {
1129 self.platform_definition.capability_registry().clone()
1130 }
1131
1132 fn driver_registry(&self) -> DriverRegistry {
1133 self.platform_definition.driver_registry().clone()
1134 }
1135
1136 fn harness_store(&self, _org_id: i64) -> Arc<dyn HarnessStore> {
1137 self.harness_store.clone()
1138 }
1139
1140 fn agent_store(&self, _org_id: i64) -> Arc<dyn AgentStore> {
1141 self.agent_store.clone()
1142 }
1143
1144 fn session_store(&self, _org_id: i64) -> Arc<dyn SessionStore> {
1145 self.session_store.clone()
1146 }
1147
1148 fn session_mutator(&self, _org_id: i64) -> Arc<dyn SessionMutator> {
1149 self.session_store.clone()
1150 }
1151
1152 fn provider_store(&self, _org_id: i64) -> Arc<dyn ProviderStore> {
1153 self.provider_store.clone()
1154 }
1155
1156 fn message_store(&self) -> Arc<dyn MessageRetriever> {
1157 self.message_store.clone()
1158 }
1159
1160 fn event_emitter(&self) -> Arc<dyn EventEmitter> {
1161 Arc::new(self.persisting_emitter.clone())
1162 }
1163
1164 fn file_store(&self) -> Arc<dyn SessionFileSystem> {
1165 self.file_store.clone()
1166 }
1167
1168 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
1169 Some(self.storage_store.clone())
1170 }
1171
1172 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
1173 self.connection_resolver.clone()
1174 }
1175
1176 fn session_task_registry(
1177 &self,
1178 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
1179 self.session_task_registry.clone()
1180 }
1181
1182 fn schedule_store(
1183 &self,
1184 org_id: i64,
1185 ) -> Option<Arc<dyn everruns_core::traits::SessionScheduleStore>> {
1186 self.schedule_store_factory
1187 .as_ref()
1188 .map(|factory| factory(org_id))
1189 }
1190
1191 fn platform_store(
1192 &self,
1193 org_id: i64,
1194 session_id: SessionId,
1195 ) -> Option<Arc<dyn everruns_core::platform_store::PlatformStore>> {
1196 self.platform_store_factory
1197 .as_ref()
1198 .map(|factory| factory(org_id, session_id))
1199 }
1200
1201 fn utility_llm_service(&self) -> Option<Arc<dyn everruns_core::UtilityLlmService>> {
1202 Some(self.platform_definition.utility_llm_service())
1203 }
1204
1205 fn egress_service(&self) -> Option<Arc<dyn everruns_core::EgressService>> {
1206 Some(self.platform_definition.egress_service())
1207 }
1208}
1209
1210#[derive(Clone)]
1211struct PersistingEventEmitter {
1212 inner: Arc<dyn EventBus>,
1213 message_store: Arc<dyn RuntimeMessageStore>,
1214}
1215
1216impl PersistingEventEmitter {
1217 fn new(inner: Arc<dyn EventBus>, message_store: Arc<dyn RuntimeMessageStore>) -> Self {
1218 Self {
1219 inner,
1220 message_store,
1221 }
1222 }
1223}
1224
1225#[async_trait]
1226impl EventEmitter for PersistingEventEmitter {
1227 async fn emit(&self, request: EventRequest) -> Result<Event> {
1228 let event = self.inner.emit(request.clone()).await?;
1229 if let Some(message) = message_from_event(&event.data) {
1230 self.message_store
1231 .store_message(request.session_id, message)
1232 .await?;
1233 }
1234 Ok(event)
1235 }
1236}
1237
1238fn effective_overlay(
1239 harness_chain: &[Harness],
1240 agent: Option<&Agent>,
1241 session: &Session,
1242) -> AgentConfigOverlay {
1243 let harness_layers = harness_chain.iter().map(AgentConfigOverlay::from);
1244 let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
1245 AgentConfigOverlay::fold(
1246 harness_layers
1247 .chain(agent_layers)
1248 .chain([AgentConfigOverlay::from(session)]),
1249 )
1250}
1251
1252fn hydrate_plugin_refs(
1259 capabilities: &mut [AgentCapabilityConfig],
1260 plugin_configs: &[AgentCapabilityConfig],
1261) {
1262 for cap in capabilities.iter_mut() {
1263 let cap_id = cap.capability_id();
1264 if !everruns_core::is_plugin_capability(cap_id) {
1265 continue;
1266 }
1267 let is_bare = cap.config.is_null()
1269 || cap
1270 .config
1271 .as_object()
1272 .map(|o| o.is_empty())
1273 .unwrap_or(false);
1274 if !is_bare {
1275 continue;
1276 }
1277 if let Some(hydrated) = plugin_configs.iter().find(|c| c.capability_id() == cap_id) {
1278 cap.config = hydrated.config.clone();
1279 }
1280 }
1281}
1282
1283async fn seed_runtime_initial_files(
1284 harness_store: &dyn RuntimeHarnessStore,
1285 agent_store: &dyn RuntimeAgentStore,
1286 file_store: &dyn SessionFileSystem,
1287 session: &Session,
1288) -> Result<()> {
1289 let harness_chain = harness_store.get_harness_chain(session.harness_id).await?;
1290 if harness_chain.is_empty() {
1291 return Err(AgentLoopError::store(format!(
1292 "harness not found while seeding files: {}",
1293 session.harness_id
1294 )));
1295 }
1296 let agent = match session.agent_id {
1297 Some(agent_id) => Some(
1298 agent_store
1299 .get_agent(agent_id)
1300 .await?
1301 .ok_or_else(|| AgentLoopError::store(format!("agent not found: {agent_id}")))?,
1302 ),
1303 None => None,
1304 };
1305 let overlay = effective_overlay(&harness_chain, agent.as_ref(), session);
1306 let seed_key = SessionId::from_uuid(session.workspace_id.uuid());
1309 for file in &overlay.initial_files {
1310 file_store.seed_initial_file(seed_key, file).await?;
1311 }
1312 Ok(())
1313}
1314
1315fn message_from_event(data: &EventData) -> Option<Message> {
1316 match data {
1317 EventData::InputMessage(data) => Some(data.message.clone()),
1318 EventData::OutputMessageCompleted(OutputMessageCompletedData { message, .. }) => {
1319 Some(message.clone())
1320 }
1321 EventData::ToolCompleted(data) => Some(tool_completed_to_message(data.clone())),
1322 _ => None,
1323 }
1324}
1325
1326fn tool_completed_to_message(data: ToolCompletedData) -> Message {
1327 let mut images: Vec<ToolResultImage> = Vec::new();
1328 let metadata = tool_result_metadata(&data);
1329 let result = data.result.map(|parts| {
1330 for part in &parts {
1331 if let ContentPart::Image(img) = part
1332 && let (Some(base64), Some(media_type)) = (&img.base64, &img.media_type)
1333 {
1334 images.push(ToolResultImage {
1335 base64: base64.clone(),
1336 media_type: media_type.clone(),
1337 });
1338 }
1339 }
1340
1341 let text_parts: Vec<&ContentPart> = parts
1342 .iter()
1343 .filter(|part| matches!(part, ContentPart::Text(_)))
1344 .collect();
1345 if text_parts.len() == 1
1346 && let ContentPart::Text(text) = text_parts[0]
1347 {
1348 return parse_structured_tool_result_text(&text.text);
1349 }
1350 if !text_parts.is_empty() {
1351 serde_json::to_value(&text_parts).unwrap_or_default()
1352 } else {
1353 serde_json::Value::Null
1354 }
1355 });
1356
1357 let mut message = if images.is_empty() {
1358 Message::tool_result(&data.tool_call_id, result, data.error)
1359 } else {
1360 Message::tool_result_with_images(&data.tool_call_id, result, images)
1361 };
1362 message.metadata = metadata;
1363 message
1364}
1365
1366fn tool_result_metadata(
1367 data: &ToolCompletedData,
1368) -> Option<std::collections::HashMap<String, serde_json::Value>> {
1369 let mut metadata = std::collections::HashMap::new();
1370 metadata.insert("tool_name".to_string(), serde_json::json!(data.tool_name));
1371 if let Some(fingerprint) = &data.tool_call_fingerprint {
1372 metadata.insert(
1373 "tool_call_fingerprint".to_string(),
1374 serde_json::json!(fingerprint),
1375 );
1376 }
1377 if let Some(fingerprint) = &data.tool_result_fingerprint {
1378 metadata.insert(
1379 "tool_result_fingerprint".to_string(),
1380 serde_json::json!(fingerprint),
1381 );
1382 }
1383 (!metadata.is_empty()).then_some(metadata)
1384}
1385
1386fn parse_structured_tool_result_text(text: &str) -> serde_json::Value {
1387 let trimmed = text.trim_start();
1388 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
1389 return serde_json::Value::String(text.to_string());
1390 }
1391
1392 match serde_json::from_str(text) {
1393 Ok(value @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => value,
1394 _ => serde_json::Value::String(text.to_string()),
1395 }
1396}
1397
1398#[cfg(test)]
1399mod tool_completed_replay_tests {
1400 use super::*;
1401
1402 #[test]
1403 fn tool_completed_replay_preserves_json_object_shape() {
1404 let data = ToolCompletedData::success(
1405 "call_read".to_string(),
1406 "read_file".to_string(),
1407 vec![ContentPart::text(
1408 serde_json::json!({
1409 "path": "/workspace/src/lib.rs",
1410 "content": "1|fn main() {}"
1411 })
1412 .to_string(),
1413 )],
1414 Some(1),
1415 );
1416
1417 let message = tool_completed_to_message(data);
1418 let result = message
1419 .tool_result_content()
1420 .and_then(|content| content.result.as_ref())
1421 .expect("tool result should be present");
1422
1423 assert_eq!(result["path"], "/workspace/src/lib.rs");
1424 assert_eq!(result["content"], "1|fn main() {}");
1425 }
1426
1427 #[test]
1428 fn tool_completed_replay_keeps_scalar_json_as_text() {
1429 let data = ToolCompletedData::success(
1430 "call_scalar".to_string(),
1431 "custom_tool".to_string(),
1432 vec![ContentPart::text("123")],
1433 Some(1),
1434 );
1435
1436 let message = tool_completed_to_message(data);
1437 let result = message
1438 .tool_result_content()
1439 .and_then(|content| content.result.as_ref())
1440 .expect("tool result should be present");
1441
1442 assert_eq!(result, &serde_json::Value::String("123".to_string()));
1443 }
1444
1445 #[test]
1446 fn tool_completed_replay_preserves_fingerprints_as_metadata() {
1447 let data = ToolCompletedData::success(
1448 "call_read".to_string(),
1449 "read_file".to_string(),
1450 vec![ContentPart::text("{}")],
1451 Some(1),
1452 )
1453 .with_fingerprints("sha256:call".to_string(), "sha256:result".to_string());
1454
1455 let message = tool_completed_to_message(data);
1456 let metadata = message.metadata.expect("metadata should be present");
1457
1458 assert_eq!(metadata["tool_name"], "read_file");
1459 assert_eq!(metadata["tool_call_fingerprint"], "sha256:call");
1460 assert_eq!(metadata["tool_result_fingerprint"], "sha256:result");
1461 }
1462}
1463
1464#[cfg(test)]
1465mod org_id_mapping_tests {
1466 use super::*;
1467 use everruns_core::{DEFAULT_ORG_ID, DEFAULT_ORG_PUBLIC_ID, org_public_id_from_internal};
1468
1469 #[test]
1470 fn default_public_id_maps_to_default_org() {
1471 assert_eq!(
1472 in_process_internal_org_id(DEFAULT_ORG_PUBLIC_ID),
1473 DEFAULT_ORG_ID
1474 );
1475 }
1476
1477 #[test]
1478 fn invalid_public_id_does_not_fall_back_to_default() {
1479 for invalid in [
1480 "",
1481 "not-an-org",
1482 "org_short",
1483 "org_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
1484 "ORG_00000000000000000000000000000001",
1485 ] {
1486 let mapped = in_process_internal_org_id(invalid);
1487 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1488 assert!(
1489 mapped >= 2,
1490 "invalid input {invalid:?} should not map to default"
1491 );
1492 }
1493 }
1494
1495 #[test]
1496 fn zero_public_id_does_not_fall_back_to_default() {
1497 let mapped = in_process_internal_org_id("org_00000000000000000000000000000000");
1500 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1501 assert!(mapped >= 2, "all-zero id should not map to default");
1502 }
1503
1504 #[test]
1505 fn synthetic_public_id_round_trips_with_internal_helper() {
1506 for internal in [1_i64, 2, 42, 1_000_000, i64::MAX - 1, i64::MAX] {
1507 let public = org_public_id_from_internal(internal);
1508 assert_eq!(
1509 in_process_internal_org_id(&public),
1510 internal,
1511 "round-trip failed for internal={internal}"
1512 );
1513 }
1514 }
1515
1516 #[test]
1517 fn distinct_synthetic_ids_map_to_distinct_internal_ids() {
1518 let a = org_public_id_from_internal(7);
1519 let b = org_public_id_from_internal(8);
1520 assert_ne!(a, b);
1521 assert_ne!(
1522 in_process_internal_org_id(&a),
1523 in_process_internal_org_id(&b)
1524 );
1525 }
1526
1527 #[test]
1528 fn high_entropy_uuid_style_id_hashes_into_reserved_range() {
1529 let high = "org_80000000000000000000000000000000";
1533 let mapped = in_process_internal_org_id(high);
1534 assert!(mapped >= 2, "mapped id {mapped} must be >= 2");
1535 assert_ne!(mapped, DEFAULT_ORG_ID);
1536
1537 assert_eq!(mapped, in_process_internal_org_id(high));
1539 }
1540
1541 #[test]
1542 fn high_entropy_ids_are_isolated_from_each_other() {
1543 let a = in_process_internal_org_id("org_80000000000000000000000000000001");
1544 let b = in_process_internal_org_id("org_80000000000000000000000000000002");
1545 assert_ne!(a, b);
1546 assert_ne!(a, DEFAULT_ORG_ID);
1547 assert_ne!(b, DEFAULT_ORG_ID);
1548 }
1549
1550 #[test]
1551 fn hash_uses_stable_sha256_truncation() {
1552 let mapped = in_process_internal_org_id("org_80000000000000000000000000000000");
1556 let expected = {
1557 let digest = sha2::Sha256::digest(b"org_80000000000000000000000000000000");
1558 let mut buf = [0u8; 8];
1559 buf.copy_from_slice(&digest[..8]);
1560 let raw = u64::from_be_bytes(buf);
1561 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
1562 };
1563 assert_eq!(mapped, expected);
1564 }
1565
1566 #[test]
1567 fn oversize_input_is_bounded_and_does_not_collide_silently() {
1568 let oversize = "x".repeat(super::HASH_INPUT_CAP_BYTES * 4);
1574 let mapped = in_process_internal_org_id(&oversize);
1575 assert!(mapped >= 2);
1576 assert_ne!(mapped, DEFAULT_ORG_ID);
1577 }
1578}