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::{
19 Capability, CapabilityRegistry, CapabilityStatus, collect_capability_mcp_servers,
20 resolve_capability_configs,
21};
22use everruns_core::config_layer::AgentConfigOverlay;
23use everruns_core::driver_registry::{DriverId, DriverRegistry};
24use everruns_core::error::{AgentLoopError, Result};
25use everruns_core::events::{
26 Event, EventContext, EventData, EventRequest, InputMessageData, OutputMessageCompletedData,
27 ToolCompletedData,
28};
29use everruns_core::harness::Harness;
30use everruns_core::llmsim_driver::{LlmSimConfig, LlmSimDriver};
31use everruns_core::message::{ContentPart, Message};
32use everruns_core::platform_definition::PlatformDefinition;
33use everruns_core::plugins::{PluginFileSet, compile_plugin};
34use everruns_core::runtime_context::{AssembledTurnContext, inspect_turn_context};
35use everruns_core::session::{Session, SessionStatus};
36use everruns_core::session_file::{InitialFile, SessionFile};
37use everruns_core::tools::ToolResultImage;
38use everruns_core::traits::{
39 AgentStore, EventEmitter, HarnessStore, ProviderStore, ResolvedModel, SessionMutator,
40 SessionStorageStore, SessionStore, UserConnectionResolver,
41};
42use everruns_core::turn::{TurnAction, TurnContext, TurnOutcome, TurnStateMachine, TurnStopReason};
43use everruns_core::typed_id::{AgentId, HarnessId, OrgId, SessionId};
44use everruns_core::{
45 AgentCapabilityConfig, CapabilityId, InputMessage, MessageRetriever, SessionFileSystem,
46 SessionFileSystemFactoryContext, plugin_capability_id, resolve_runtime_capabilities,
47};
48use sha2::{Digest, Sha256};
49use std::path::Path;
50use std::sync::Arc;
51
52const HASH_INPUT_CAP_BYTES: usize = 128;
58
59pub fn in_process_internal_org_id(public_org_id: &str) -> i64 {
72 if public_org_id == everruns_core::DEFAULT_ORG_PUBLIC_ID {
73 return everruns_core::DEFAULT_ORG_ID;
74 }
75
76 let Ok(parsed) = public_org_id.parse::<OrgId>() else {
77 return hash_public_org_id(public_org_id);
78 };
79 let raw: u128 = parsed.uuid().as_u128();
80 if raw == 0 {
81 return hash_public_org_id(public_org_id);
82 }
83
84 if raw <= i64::MAX as u128 {
87 return raw as i64;
88 }
89
90 hash_public_org_id(public_org_id)
91}
92
93fn hash_public_org_id(public_org_id: &str) -> i64 {
98 let bytes = public_org_id.as_bytes();
99 let bounded = &bytes[..bytes.len().min(HASH_INPUT_CAP_BYTES)];
100 let digest = Sha256::digest(bounded);
101 let mut buf = [0u8; 8];
102 buf.copy_from_slice(&digest[..8]);
103 let raw = u64::from_be_bytes(buf);
104 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
105}
106
107#[derive(Debug, Clone)]
108pub struct TurnResult {
109 pub response: String,
111 pub iterations: usize,
113 pub tool_calls_count: usize,
115 pub success: bool,
117 pub error: Option<String>,
119 pub stop_reason: TurnStopReason,
121 pub turn_id: everruns_core::typed_id::TurnId,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct CapabilityDelta {
132 pub capability_id: String,
134 pub changed: bool,
136 pub active: bool,
138 pub surfaces_dirty: bool,
140}
141
142impl TurnResult {
143 fn from_outcome(outcome: TurnOutcome, turn_id: everruns_core::typed_id::TurnId) -> Self {
144 let stop_reason = outcome.stop_reason();
145 match outcome {
146 TurnOutcome::Success {
147 response,
148 iterations,
149 tool_calls_count,
150 ..
151 } => Self {
152 response,
153 iterations,
154 tool_calls_count,
155 success: true,
156 error: None,
157 stop_reason,
158 turn_id,
159 },
160 TurnOutcome::Failed {
161 error, iterations, ..
162 } => Self {
163 response: String::new(),
164 iterations,
165 tool_calls_count: 0,
166 success: false,
167 error: Some(error),
168 stop_reason,
169 turn_id,
170 },
171 TurnOutcome::MaxIterationsReached {
172 response,
173 iterations,
174 tool_calls_count,
175 } => Self {
176 response,
177 iterations,
178 tool_calls_count,
179 success: true,
180 error: None,
181 stop_reason,
182 turn_id,
183 },
184 TurnOutcome::Sealed {
187 reason,
188 response,
189 iterations,
190 tool_calls_count,
191 } => Self {
192 response,
193 iterations,
194 tool_calls_count,
195 success: false,
196 error: Some(format!("turn sealed: {reason}")),
197 stop_reason,
198 turn_id,
199 },
200 }
201 }
202}
203
204pub struct InProcessRuntimeBuilder {
214 platform_definition: PlatformDefinition,
215 llm_sim_config: Option<LlmSimConfig>,
216 default_model: Option<ResolvedModel>,
217 backends: Option<RuntimeBackends>,
218 session_file_system_factory_context: SessionFileSystemFactoryContext,
219 harnesses: Vec<Harness>,
220 agents: Vec<Agent>,
221 sessions: Vec<Session>,
222 default_session_id: Option<SessionId>,
223 seeded_files: Vec<(SessionId, InitialFile)>,
224 mcp_auth_provider: Option<Arc<dyn everruns_mcp::McpAuthProvider>>,
225 provider_retry_config: Option<everruns_core::llm_retry::LlmRetryConfig>,
226 provider_stall_timeout: Option<std::time::Duration>,
227 plugin_capability_configs: Vec<AgentCapabilityConfig>,
233 plugin_warnings: Vec<String>,
235}
236
237impl Default for InProcessRuntimeBuilder {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243impl InProcessRuntimeBuilder {
244 pub fn new() -> Self {
252 Self {
253 platform_definition: PlatformDefinition::builder()
254 .capability_registry(CapabilityRegistry::runtime_builtins())
255 .driver_registry(DriverRegistry::new())
256 .session_file_system_factory(Arc::new(InMemorySessionFileSystemFactory))
257 .build(),
258 llm_sim_config: None,
259 default_model: None,
260 backends: None,
261 session_file_system_factory_context: SessionFileSystemFactoryContext::new(),
262 harnesses: Vec::new(),
263 agents: Vec::new(),
264 sessions: Vec::new(),
265 default_session_id: None,
266 seeded_files: Vec::new(),
267 mcp_auth_provider: None,
268 provider_retry_config: None,
269 provider_stall_timeout: None,
270 plugin_capability_configs: Vec::new(),
271 plugin_warnings: Vec::new(),
272 }
273 }
274
275 pub fn mcp_auth_provider(mut self, provider: Arc<dyn everruns_mcp::McpAuthProvider>) -> Self {
279 self.mcp_auth_provider = Some(provider);
280 self
281 }
282
283 pub fn platform_definition(mut self, platform_definition: PlatformDefinition) -> Self {
285 self.platform_definition = platform_definition;
286 self
287 }
288
289 pub fn capability<C: Capability + 'static>(mut self, capability: C) -> Self {
291 self.platform_definition
292 .capability_registry_mut()
293 .register(capability);
294 self
295 }
296
297 pub fn driver_registry(mut self, driver_registry: DriverRegistry) -> Self {
299 *self.platform_definition.driver_registry_mut() = driver_registry;
300 self
301 }
302
303 pub fn llm_sim(mut self, config: LlmSimConfig) -> Self {
305 self.llm_sim_config = Some(config);
306 self
307 }
308
309 pub fn default_model(mut self, model: ResolvedModel) -> Self {
311 self.default_model = Some(model);
312 self
313 }
314
315 pub fn backends(mut self, backends: RuntimeBackends) -> Self {
317 self.backends = Some(backends);
318 self
319 }
320
321 pub fn with_session_task_registry(
325 mut self,
326 registry: Arc<dyn everruns_core::session_task::SessionTaskRegistry>,
327 ) -> Self {
328 let backends = self
329 .backends
330 .take()
331 .unwrap_or_else(RuntimeBackends::in_memory);
332 self.backends = Some(backends.with_session_task_registry(registry));
333 self
334 }
335
336 pub fn with_schedule_store_factory(
338 mut self,
339 factory: crate::backends::ScheduleStoreFactory,
340 ) -> Self {
341 let backends = self
342 .backends
343 .take()
344 .unwrap_or_else(RuntimeBackends::in_memory);
345 self.backends = Some(backends.with_schedule_store_factory(factory));
346 self
347 }
348
349 pub fn with_platform_store_factory(
351 mut self,
352 factory: crate::backends::PlatformStoreFactory,
353 ) -> Self {
354 let backends = self
355 .backends
356 .take()
357 .unwrap_or_else(RuntimeBackends::in_memory);
358 self.backends = Some(backends.with_platform_store_factory(factory));
359 self
360 }
361
362 pub fn session_file_system_factory_context(
364 mut self,
365 context: SessionFileSystemFactoryContext,
366 ) -> Self {
367 self.session_file_system_factory_context = context;
368 self
369 }
370
371 pub fn provider_retry_config(
373 mut self,
374 config: everruns_core::llm_retry::LlmRetryConfig,
375 ) -> Self {
376 self.provider_retry_config = Some(config);
377 self
378 }
379
380 pub fn provider_stall_timeout(mut self, timeout: std::time::Duration) -> Self {
382 self.provider_stall_timeout = Some(timeout);
383 self
384 }
385
386 pub fn harness(mut self, harness: Harness) -> Self {
388 self.harnesses.push(harness);
389 self
390 }
391
392 pub fn agent(mut self, agent: Agent) -> Self {
394 self.agents.push(agent);
395 self
396 }
397
398 pub fn session(mut self, session: Session) -> Self {
400 self.sessions.push(session);
401 self
402 }
403
404 pub fn single_session<F>(mut self, configure: F) -> Self
409 where
410 F: FnOnce(SingleSessionBuilder) -> SingleSessionBuilder,
411 {
412 let (harness, agent, session, session_id) =
413 configure(SingleSessionBuilder::default()).build();
414 self.harnesses.push(harness);
415 self.agents.push(agent);
416 self.sessions.push(session);
417 self.default_session_id = Some(session_id);
418 self
419 }
420
421 pub fn seed_text_file(
425 mut self,
426 session_id: SessionId,
427 path: impl Into<String>,
428 content: impl Into<String>,
429 ) -> Self {
430 self.seeded_files.push((
431 session_id,
432 InitialFile {
433 path: path.into(),
434 content: content.into(),
435 encoding: "text".to_string(),
436 is_readonly: false,
437 },
438 ));
439 self
440 }
441
442 pub fn with_plugin_dir(mut self, path: &Path) -> Result<Self> {
463 let file_set = PluginFileSet::from_dir(path)
464 .map_err(|e| AgentLoopError::config(format!("plugin directory load failed: {e}")))?;
465 let compiled = compile_plugin(&file_set)
466 .map_err(|e| AgentLoopError::config(format!("plugin compilation failed: {e}")))?;
467
468 for warning in &compiled.warnings {
469 tracing::warn!(plugin = %compiled.definition.name, warning = %warning, "plugin compile warning");
470 }
471 self.plugin_warnings.extend(compiled.warnings);
472
473 let cap_id = plugin_capability_id(&compiled.definition.name);
474 let hydrated_config = serde_json::to_value(&compiled.definition)
475 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
476 self.plugin_capability_configs
477 .push(AgentCapabilityConfig::with_config(cap_id, hydrated_config));
478
479 Ok(self)
480 }
481
482 pub fn plugin_capability(&self, name: &str) -> Option<AgentCapabilityConfig> {
488 let cap_id = plugin_capability_id(name);
489 self.plugin_capability_configs
490 .iter()
491 .find(|c| c.capability_id() == cap_id)
492 .cloned()
493 }
494
495 pub async fn build(mut self) -> Result<InProcessRuntime> {
500 let backends = match self.backends.take() {
501 Some(backends) => backends,
502 None => RuntimeBackends::in_memory(),
503 };
504 let file_store = resolve_session_file_system(
505 &self.platform_definition,
506 self.session_file_system_factory_context.clone(),
507 )
508 .await?;
509
510 if let Some(config) = self.llm_sim_config.take() {
511 let driver = LlmSimDriver::new(config);
512 self.platform_definition
515 .driver_registry_mut()
516 .register_or_replace(DriverId::LlmSim, move |_config| Box::new(driver.clone()));
517
518 if self.default_model.is_none() {
519 self.default_model = Some(ResolvedModel {
520 model: "llmsim-model".to_string(),
521 provider_type: DriverId::LlmSim,
522 api_key: Some("fake-key".to_string()),
523 base_url: None,
524 provider_metadata: None,
525 });
526 }
527 }
528
529 let default_model = self.default_model.ok_or_else(|| {
530 AgentLoopError::config(
531 "in-process runtime requires a default model; call \
532 InProcessRuntimeBuilder::default_model(...) or \
533 InProcessRuntimeBuilder::llm_sim(...)",
534 )
535 })?;
536
537 backends
538 .provider_store
539 .set_default_model(default_model)
540 .await?;
541
542 for harness in &mut self.harnesses {
546 hydrate_plugin_refs(&mut harness.capabilities, &self.plugin_capability_configs);
547 }
548 for agent in &mut self.agents {
549 hydrate_plugin_refs(&mut agent.capabilities, &self.plugin_capability_configs);
550 }
551 for session in &mut self.sessions {
552 hydrate_plugin_refs(&mut session.capabilities, &self.plugin_capability_configs);
553 }
554
555 for harness in &self.harnesses {
556 backends.harness_store.add_harness(harness.clone()).await?;
557 }
558 for agent in &self.agents {
559 backends.agent_store.add_agent(agent.clone()).await?;
560 }
561 for session in &self.sessions {
562 backends.session_store.add_session(session.clone()).await?;
563 }
564
565 for session in &self.sessions {
566 seed_runtime_initial_files(
567 backends.harness_store.as_ref(),
568 backends.agent_store.as_ref(),
569 file_store.as_ref(),
570 session,
571 )
572 .await?;
573 }
574
575 for (session_id, file) in &self.seeded_files {
576 file_store.seed_initial_file(*session_id, file).await?;
577 }
578
579 let persisting_emitter =
580 PersistingEventEmitter::new(backends.event_bus.clone(), backends.message_store.clone());
581
582 let (session_task_registry, session_wake_queue) = match backends.session_task_registry {
588 Some(inner) => {
589 let wake_queue = Arc::new(everruns_core::SessionWakeQueue::new());
590 let observing = everruns_core::ObservingTaskRegistry::new(inner)
591 .with_observer(wake_queue.clone());
592 let wrapped: Arc<dyn everruns_core::session_task::SessionTaskRegistry> =
593 Arc::new(observing);
594 (Some(wrapped), Some(wake_queue))
595 }
596 None => (None, None),
597 };
598
599 Ok(InProcessRuntime {
600 platform_definition: Arc::new(self.platform_definition),
601 harness_store: backends.harness_store,
602 agent_store: backends.agent_store,
603 session_store: backends.session_store,
604 default_session_id: self.default_session_id,
605 message_store: backends.message_store,
606 compaction_checkpoint_store: backends.compaction_checkpoint_store,
607 provider_store: backends.provider_store,
608 event_bus: backends.event_bus,
609 persisting_emitter,
610 file_store,
611 storage_store: backends.storage_store,
612 connection_resolver: backends.connection_resolver,
613 session_task_registry,
614 session_wake_queue,
615 schedule_store_factory: backends.schedule_store_factory,
616 platform_store_factory: backends.platform_store_factory,
617 mcp_auth_provider: self
618 .mcp_auth_provider
619 .unwrap_or_else(|| Arc::new(everruns_mcp::NoAuthProvider)),
620 provider_retry_config: self.provider_retry_config,
621 provider_stall_timeout: self.provider_stall_timeout,
622 mcp_discovery_cache: Arc::new(crate::mcp_cache::McpDiscoveryCache::new()),
623 plugin_warnings: self.plugin_warnings,
624 })
625 }
626}
627
628async fn resolve_session_file_system(
629 platform_definition: &PlatformDefinition,
630 file_system_factory_context: SessionFileSystemFactoryContext,
631) -> Result<Arc<dyn SessionFileSystem>> {
632 let file_system_factory = platform_definition.session_file_system_factory();
633 if file_system_factory.is_disabled() {
634 Ok(Arc::new(InMemorySessionFileStore::new()))
635 } else {
636 Ok(file_system_factory
637 .create_session_file_system(file_system_factory_context)
638 .await?)
639 }
640}
641
642#[derive(Clone)]
643pub struct InProcessRuntime {
649 platform_definition: Arc<PlatformDefinition>,
650 harness_store: Arc<dyn RuntimeHarnessStore>,
651 agent_store: Arc<dyn RuntimeAgentStore>,
652 session_store: Arc<dyn RuntimeSessionStore>,
653 default_session_id: Option<SessionId>,
654 message_store: Arc<dyn RuntimeMessageStore>,
655 compaction_checkpoint_store: Arc<dyn everruns_core::CompactionCheckpointStore>,
656 provider_store: Arc<dyn RuntimeProviderStore>,
657 event_bus: Arc<dyn EventBus>,
658 persisting_emitter: PersistingEventEmitter,
659 file_store: Arc<dyn SessionFileSystem>,
660 storage_store: Arc<dyn SessionStorageStore>,
661 connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
662 session_task_registry: Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>>,
663 session_wake_queue: Option<Arc<everruns_core::SessionWakeQueue>>,
667 schedule_store_factory: Option<crate::backends::ScheduleStoreFactory>,
668 platform_store_factory: Option<crate::backends::PlatformStoreFactory>,
669 mcp_auth_provider: Arc<dyn everruns_mcp::McpAuthProvider>,
670 provider_retry_config: Option<everruns_core::llm_retry::LlmRetryConfig>,
671 provider_stall_timeout: Option<std::time::Duration>,
672 mcp_discovery_cache: Arc<crate::mcp_cache::McpDiscoveryCache>,
673 plugin_warnings: Vec<String>,
676}
677
678impl InProcessRuntime {
679 fn mcp_client(&self) -> Arc<everruns_mcp::McpClient> {
682 Arc::new(everruns_mcp::McpClient::new(
683 self.platform_definition.egress_service(),
684 self.mcp_auth_provider.clone(),
685 ))
686 }
687
688 async fn session_mcp_servers(
690 &self,
691 session: &Session,
692 agent: Option<&Agent>,
693 ) -> everruns_core::ScopedMcpServers {
694 let harness_chain = self
695 .harness_store
696 .get_harness_chain(session.harness_id)
697 .await
698 .unwrap_or_default();
699 let resolved = resolve_runtime_capabilities(
700 &harness_chain,
701 agent,
702 session,
703 self.platform_definition.capability_registry(),
704 );
705 let contributed = collect_capability_mcp_servers(
706 &resolved.resolved_capability_configs,
707 self.platform_definition.capability_registry(),
708 );
709 let explicit = crate::mcp::merge_session_scoped_servers(&harness_chain, agent, session);
710 everruns_core::merge_scoped_mcp_servers(&contributed, &explicit)
711 }
712 pub fn builder() -> InProcessRuntimeBuilder {
714 InProcessRuntimeBuilder::new()
715 }
716
717 pub fn default_session_id(&self) -> Option<SessionId> {
720 self.default_session_id
721 }
722
723 pub fn plugin_warnings(&self) -> &[String] {
728 &self.plugin_warnings
729 }
730
731 pub async fn activate_capability(
737 &self,
738 session_id: SessionId,
739 capability: impl Into<AgentCapabilityConfig>,
740 ) -> Result<CapabilityDelta> {
741 let mut capability = capability.into();
742 let registry = self.platform_definition.capability_registry();
743 let registered = registry.get(capability.capability_id()).ok_or_else(|| {
744 AgentLoopError::config(format!(
745 "unknown capability: {}",
746 capability.capability_id()
747 ))
748 })?;
749 if registered.status() != CapabilityStatus::Available {
750 return Err(AgentLoopError::config(format!(
751 "capability is not available: {}",
752 capability.capability_id()
753 )));
754 }
755 registered
756 .validate_config(&capability.config)
757 .map_err(|error| {
758 AgentLoopError::config(format!("invalid capability config: {error}"))
759 })?;
760
761 let canonical_id = registered.id().to_string();
762 capability.capability_ref = CapabilityId::new(canonical_id.clone());
763 let context = self.load_context(session_id).await?;
764 if context
765 .resolved_capability_configs
766 .iter()
767 .any(|config| config.capability_id() == canonical_id)
768 {
769 return Ok(CapabilityDelta {
770 capability_id: canonical_id,
771 changed: false,
772 active: true,
773 surfaces_dirty: false,
774 });
775 }
776
777 let mut candidate = context.effective_overlay.capabilities;
778 candidate.push(capability.clone());
779 resolve_capability_configs(&candidate, registry)
780 .map_err(|error| AgentLoopError::config(error.to_string()))?;
781
782 self.session_store
783 .upsert_session_capability(session_id, capability)
784 .await?;
785 self.mcp_discovery_cache
786 .invalidate_session(session_id.uuid());
787 Ok(CapabilityDelta {
788 capability_id: canonical_id,
789 changed: true,
790 active: true,
791 surfaces_dirty: true,
792 })
793 }
794
795 pub async fn deactivate_capability(
800 &self,
801 session_id: SessionId,
802 capability_id: &str,
803 ) -> Result<CapabilityDelta> {
804 let registry = self.platform_definition.capability_registry();
805 let registered = registry.get(capability_id).ok_or_else(|| {
806 AgentLoopError::config(format!("unknown capability: {capability_id}"))
807 })?;
808 let canonical_id = registered.id().to_string();
809 let context = self.load_context(session_id).await?;
810 if !context
811 .resolved_capability_configs
812 .iter()
813 .any(|config| config.capability_id() == canonical_id)
814 {
815 return Ok(CapabilityDelta {
816 capability_id: canonical_id,
817 changed: false,
818 active: false,
819 surfaces_dirty: false,
820 });
821 }
822
823 let session_capability_id = context
824 .session
825 .capabilities
826 .iter()
827 .find(|config| {
828 registry
829 .get(config.capability_id())
830 .is_some_and(|capability| capability.id() == canonical_id)
831 })
832 .map(|config| config.capability_id().to_string())
833 .ok_or_else(|| {
834 AgentLoopError::config(format!(
835 "capability {canonical_id} is inherited and cannot be deactivated at the session layer"
836 ))
837 })?;
838
839 self.session_store
840 .remove_session_capability(session_id, &session_capability_id)
841 .await?;
842 self.mcp_discovery_cache
843 .invalidate_session(session_id.uuid());
844 Ok(CapabilityDelta {
845 capability_id: canonical_id,
846 changed: true,
847 active: false,
848 surfaces_dirty: true,
849 })
850 }
851
852 pub async fn run_turn(
858 &self,
859 session_id: SessionId,
860 input: impl Into<InputMessage>,
861 ) -> Result<TurnResult> {
862 let session = self
863 .session_store
864 .get_session(session_id)
865 .await?
866 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
867
868 let input_message = self
873 .message_store
874 .add_input_message(session_id, input.into())
875 .await?;
876 self.event_bus
877 .emit(EventRequest::new(
878 session_id,
879 EventContext::empty(),
880 InputMessageData::new(input_message.clone()),
881 ))
882 .await?;
883
884 let assembled = self
885 .inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
886 .await?;
887 let synthetic_agent_id = session
888 .agent_id
889 .unwrap_or_else(|| AgentId::from_uuid(session.id.uuid()));
890 let org_id = in_process_internal_org_id(&session.organization_id);
891 let mut state_machine = TurnStateMachine::new(
892 TurnContext::new(session_id, input_message.id, synthetic_agent_id, org_id),
893 assembled.runtime_agent.max_iterations,
894 );
895
896 let mut previous_response_id: Option<String> = None;
897 let mut last_reason_result: Option<everruns_core::ReasonResult> = None;
898
899 loop {
900 match state_machine.next_action() {
901 TurnAction::ExecuteInput => {
902 let ctx = state_machine.context();
903 let base_context =
904 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
905 .with_workspace_id(session.workspace_id);
906 execute_input_activity(
907 self,
908 org_id,
909 InputAtomInput {
910 context: base_context,
911 },
912 )
913 .await?;
914 state_machine.on_input_completed();
915 }
916 TurnAction::ExecuteReason => {
917 let ctx = state_machine.context();
918 let session_id = ctx.session_id;
919 self.drain_and_inject_wakes(session_id).await?;
925 let base_context =
926 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
927 .with_workspace_id(session.workspace_id);
928 let reason_result = execute_reason_activity(
929 self,
930 org_id,
931 ReasonInput {
932 context: base_context.next_exec(),
933 harness_id: session.harness_id,
934 agent_id: session.agent_id,
935 org_id,
936 mcp_tool_definitions: vec![],
937 previous_response_id: previous_response_id.take(),
938 iteration: state_machine.current_iteration() as u32 + 1,
939 },
940 )
941 .await?;
942 previous_response_id = reason_result.response_id.clone();
943 let has_pending_wakes = self
948 .session_wake_queue
949 .as_ref()
950 .is_some_and(|q| q.has_pending(session_id));
951 state_machine.on_reason_completed(
952 reason_result.text.clone(),
953 reason_result.tool_calls.len(),
954 reason_result.success,
955 reason_result.error.clone(),
956 reason_result.finish_reason.clone(),
957 has_pending_wakes,
958 );
959 if reason_result.has_tool_calls {
960 last_reason_result = Some(reason_result);
961 }
962 }
963 TurnAction::ExecuteAct => {
964 let reason_result = last_reason_result
965 .take()
966 .expect("ExecuteAct requires a prior ReasonResult");
967 let ctx = state_machine.context();
968 let base_context =
969 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
970 .with_workspace_id(session.workspace_id);
971 execute_act_activity(
972 self,
973 ActInput {
974 org_id: Some(org_id),
975 context: base_context.next_exec(),
976 harness_id: session.harness_id,
977 agent_id: session.agent_id,
978 tool_calls: reason_result.tool_calls,
979 tool_definitions: reason_result.tool_definitions,
980 locale: reason_result.locale,
981 blueprint_id: None,
982 network_access: reason_result.network_access,
983 parallel_tool_calls: reason_result.parallel_tool_calls,
986 },
987 )
988 .await?;
989 state_machine.on_act_completed();
990 }
991 TurnAction::Complete(outcome) => {
992 let ctx = state_machine.context();
993 let lifecycle =
994 RuntimeSessionLifecycle::new(self.clone(), org_id, ctx.session_id);
995 let turn_succeeded = matches!(
996 &outcome,
997 TurnOutcome::Success { .. } | TurnOutcome::MaxIterationsReached { .. }
998 );
999 match &outcome {
1000 TurnOutcome::Success { iterations, .. }
1001 | TurnOutcome::MaxIterationsReached { iterations, .. } => {
1002 lifecycle
1003 .turn_completed(
1004 ctx.turn_id,
1005 ctx.input_message_id,
1006 *iterations as u32,
1007 None,
1008 None,
1009 )
1010 .await;
1011 }
1012 TurnOutcome::Failed { error, .. } => {
1013 lifecycle
1014 .turn_failed(ctx.turn_id, ctx.input_message_id, error, None)
1015 .await;
1016 }
1017 TurnOutcome::Sealed {
1018 reason, iterations, ..
1019 } => {
1020 lifecycle
1021 .turn_sealed(
1022 ctx.turn_id,
1023 ctx.input_message_id,
1024 reason.as_str(),
1025 *iterations as u32,
1026 None,
1027 )
1028 .await;
1029 }
1030 }
1031 lifecycle
1034 .fire_turn_end_hooks(
1035 session.harness_id,
1036 session.agent_id,
1037 ctx.turn_id,
1038 turn_succeeded,
1039 )
1040 .await;
1041 return Ok(TurnResult::from_outcome(outcome, ctx.turn_id));
1042 }
1043 }
1044 }
1045 }
1046
1047 pub async fn run_text_turn(
1048 &self,
1049 session_id: SessionId,
1050 text: impl Into<String>,
1051 ) -> Result<TurnResult> {
1052 self.run_turn(session_id, InputMessage::user(text)).await
1053 }
1054
1055 async fn drain_and_inject_wakes(&self, session_id: SessionId) -> Result<usize> {
1066 let Some(queue) = &self.session_wake_queue else {
1067 return Ok(0);
1068 };
1069 let wakes = queue.drain(session_id);
1070 if wakes.is_empty() {
1071 return Ok(0);
1072 }
1073 let count = wakes.len();
1074 for wake in wakes {
1075 let message = self
1080 .message_store
1081 .add_input_message(session_id, InputMessage::user(wake.text))
1082 .await?;
1083 self.event_bus
1084 .emit(EventRequest::new(
1085 session_id,
1086 EventContext::empty(),
1087 InputMessageData::new(message),
1088 ))
1089 .await?;
1090 }
1091 Ok(count)
1092 }
1093
1094 pub async fn messages(&self, session_id: SessionId) -> Result<Vec<Message>> {
1096 self.message_store.load(session_id).await
1097 }
1098
1099 pub async fn read_file(
1101 &self,
1102 session_id: SessionId,
1103 path: &str,
1104 ) -> Result<Option<SessionFile>> {
1105 self.file_store.read_file(session_id, path).await
1106 }
1107
1108 pub async fn load_context(&self, session_id: SessionId) -> Result<AssembledTurnContext> {
1110 let session = self
1111 .session_store
1112 .get_session(session_id)
1113 .await?
1114 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1115 self.inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
1116 .await
1117 }
1118
1119 pub async fn events(&self) -> Result<Vec<Event>> {
1124 Ok(self.event_bus.collected_events().await)
1125 }
1126
1127 pub async fn execute_command(
1137 &self,
1138 session_id: SessionId,
1139 request: everruns_core::command::ExecuteCommandRequest,
1140 ) -> Result<everruns_core::command::CommandResult> {
1141 let ctx = self.load_context(session_id).await?;
1142 let registry = self.platform_definition.capability_registry();
1143 let host = everruns_core::command_host::StoreCommandHost::new(
1147 session_id,
1148 self.harness_store.clone(),
1149 self.agent_store.clone(),
1150 self.session_store.clone(),
1151 self.message_store.clone(),
1152 self.provider_store.clone(),
1153 registry.clone(),
1154 self.platform_definition.driver_registry().clone(),
1155 )
1156 .with_file_store(self.file_store.clone())
1157 .with_assembled_context(ctx.clone());
1158 let exec_ctx =
1159 everruns_core::command::CommandExecutionContext::new(session_id, Arc::new(host));
1160 for config in &ctx.resolved_capability_configs {
1161 let Some(capability) = registry.get(config.capability_id()) else {
1162 continue;
1163 };
1164 if capability.commands().iter().any(|c| c.name == request.name) {
1165 return capability.execute_command(&request, &exec_ctx).await;
1166 }
1167 }
1168 Err(AgentLoopError::config(format!(
1169 "no capability declares command /{}",
1170 request.name
1171 )))
1172 }
1173
1174 pub async fn list_commands(
1184 &self,
1185 session_id: SessionId,
1186 ) -> Result<Vec<everruns_core::command::CommandDescriptor>> {
1187 let ctx = self.load_context(session_id).await?;
1188 let registry = self.platform_definition.capability_registry();
1189 let mut seen = std::collections::HashSet::new();
1190 let mut commands = Vec::new();
1191 for config in &ctx.resolved_capability_configs {
1192 let Some(capability) = registry.get(config.capability_id()) else {
1193 continue;
1194 };
1195 for command in capability.commands() {
1196 if seen.insert(command.name.clone()) {
1197 commands.push(command);
1198 }
1199 }
1200 }
1201 Ok(commands)
1202 }
1203
1204 async fn inspect_context_with_ids(
1205 &self,
1206 session_id: SessionId,
1207 harness_id: everruns_core::HarnessId,
1208 agent_id: Option<AgentId>,
1209 ) -> Result<AssembledTurnContext> {
1210 inspect_turn_context(
1211 self.harness_store.as_ref(),
1212 self.agent_store.as_ref(),
1213 self.session_store.as_ref(),
1214 self.message_store.as_ref(),
1215 self.provider_store.as_ref(),
1216 self.platform_definition.capability_registry(),
1217 session_id,
1218 harness_id,
1219 agent_id,
1220 &[],
1221 Some(self.file_store.clone()),
1222 )
1223 .await
1224 }
1225}
1226
1227#[async_trait]
1228impl RuntimeHostAdapter for InProcessRuntime {
1229 async fn get_agent(&self, _org_id: i64, agent_id: AgentId) -> Result<Option<Agent>> {
1230 self.agent_store.get_agent(agent_id).await
1231 }
1232
1233 async fn get_harness(&self, _org_id: i64, harness_id: HarnessId) -> Result<Option<Harness>> {
1234 let chain = self.harness_store.get_harness_chain(harness_id).await?;
1235 Ok(chain.into_iter().last())
1236 }
1237
1238 async fn set_session_status(
1239 &self,
1240 _org_id: i64,
1241 session_id: SessionId,
1242 _status: SessionStatus,
1243 ) -> Result<Session> {
1244 self.session_store
1248 .get_session(session_id)
1249 .await?
1250 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))
1251 }
1252
1253 async fn load_turn_context(
1254 &self,
1255 _org_id: i64,
1256 session_id: SessionId,
1257 ) -> Result<RuntimeHostTurnContext> {
1258 let mut session = self
1259 .session_store
1260 .get_session(session_id)
1261 .await?
1262 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1263 everruns_core::ard_attachment::apply_session_attachments(
1266 self.storage_store.as_ref(),
1267 &mut session,
1268 )
1269 .await;
1270 let agent = match session.agent_id {
1271 Some(agent_id) => self.agent_store.get_agent(agent_id).await?,
1272 None => None,
1273 };
1274 let messages = self.message_store.load(session_id).await?;
1275 let model = self.provider_store.get_default_model().await?;
1276
1277 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1280 let mcp_tool_definitions = if scoped_servers.is_empty() {
1281 vec![]
1282 } else {
1283 crate::mcp::discover_tool_definitions(
1284 &self.mcp_discovery_cache,
1285 self.mcp_client(),
1286 session_id.uuid(),
1287 &scoped_servers,
1288 )
1289 .await
1290 };
1291
1292 Ok(RuntimeHostTurnContext {
1293 agent,
1294 session,
1295 messages,
1296 model,
1297 mcp_tool_definitions,
1298 })
1299 }
1300
1301 async fn mcp_executor(
1302 &self,
1303 _org_id: i64,
1304 session_id: SessionId,
1305 ) -> Option<Arc<everruns_mcp::McpExecutor>> {
1306 let session = self.session_store.get_session(session_id).await.ok()??;
1307 let agent = match session.agent_id {
1308 Some(agent_id) => self.agent_store.get_agent(agent_id).await.ok().flatten(),
1309 None => None,
1310 };
1311 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1312 crate::mcp::build_executor(self.mcp_client(), &scoped_servers)
1313 }
1314
1315 fn capability_registry(&self) -> CapabilityRegistry {
1316 self.platform_definition.capability_registry().clone()
1317 }
1318
1319 fn driver_registry(&self) -> DriverRegistry {
1320 self.platform_definition.driver_registry().clone()
1321 }
1322
1323 fn harness_store(&self, _org_id: i64) -> Arc<dyn HarnessStore> {
1324 self.harness_store.clone()
1325 }
1326
1327 fn agent_store(&self, _org_id: i64) -> Arc<dyn AgentStore> {
1328 self.agent_store.clone()
1329 }
1330
1331 fn session_store(&self, _org_id: i64) -> Arc<dyn SessionStore> {
1332 self.session_store.clone()
1333 }
1334
1335 fn session_mutator(&self, _org_id: i64) -> Arc<dyn SessionMutator> {
1336 self.session_store.clone()
1337 }
1338
1339 fn provider_store(&self, _org_id: i64) -> Arc<dyn ProviderStore> {
1340 self.provider_store.clone()
1341 }
1342
1343 fn message_store(&self) -> Arc<dyn MessageRetriever> {
1344 self.message_store.clone()
1345 }
1346
1347 fn compaction_checkpoint_store(
1348 &self,
1349 ) -> Option<Arc<dyn everruns_core::CompactionCheckpointStore>> {
1350 Some(self.compaction_checkpoint_store.clone())
1351 }
1352
1353 fn event_emitter(&self) -> Arc<dyn EventEmitter> {
1354 Arc::new(self.persisting_emitter.clone())
1355 }
1356
1357 fn file_store(&self) -> Arc<dyn SessionFileSystem> {
1358 self.file_store.clone()
1359 }
1360
1361 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
1362 Some(self.storage_store.clone())
1363 }
1364
1365 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
1366 self.connection_resolver.clone()
1367 }
1368
1369 fn session_task_registry(
1370 &self,
1371 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
1372 self.session_task_registry.clone()
1373 }
1374
1375 fn schedule_store(
1376 &self,
1377 org_id: i64,
1378 ) -> Option<Arc<dyn everruns_core::traits::SessionScheduleStore>> {
1379 self.schedule_store_factory
1380 .as_ref()
1381 .map(|factory| factory(org_id))
1382 }
1383
1384 fn platform_store(
1385 &self,
1386 org_id: i64,
1387 session_id: SessionId,
1388 ) -> Option<Arc<dyn everruns_core::platform_store::PlatformStore>> {
1389 self.platform_store_factory
1390 .as_ref()
1391 .map(|factory| factory(org_id, session_id))
1392 }
1393
1394 fn utility_llm_service(&self) -> Option<Arc<dyn everruns_core::UtilityLlmService>> {
1395 Some(self.platform_definition.utility_llm_service())
1396 }
1397
1398 fn egress_service(&self) -> Option<Arc<dyn everruns_core::EgressService>> {
1399 Some(self.platform_definition.egress_service())
1400 }
1401
1402 fn provider_retry_config(&self) -> Option<everruns_core::llm_retry::LlmRetryConfig> {
1403 self.provider_retry_config.clone()
1404 }
1405
1406 fn provider_stall_timeout(&self) -> Option<std::time::Duration> {
1407 self.provider_stall_timeout
1408 }
1409}
1410
1411#[derive(Clone)]
1412struct PersistingEventEmitter {
1413 inner: Arc<dyn EventBus>,
1414 message_store: Arc<dyn RuntimeMessageStore>,
1415}
1416
1417impl PersistingEventEmitter {
1418 fn new(inner: Arc<dyn EventBus>, message_store: Arc<dyn RuntimeMessageStore>) -> Self {
1419 Self {
1420 inner,
1421 message_store,
1422 }
1423 }
1424}
1425
1426#[async_trait]
1427impl EventEmitter for PersistingEventEmitter {
1428 async fn emit(&self, request: EventRequest) -> Result<Event> {
1429 let event = self.inner.emit(request.clone()).await?;
1430 if let Some(message) = message_from_event(&event.data) {
1431 self.message_store
1432 .store_message(request.session_id, message)
1433 .await?;
1434 }
1435 Ok(event)
1436 }
1437}
1438
1439fn effective_overlay(
1440 harness_chain: &[Harness],
1441 agent: Option<&Agent>,
1442 session: &Session,
1443) -> AgentConfigOverlay {
1444 let harness_layers = harness_chain.iter().map(AgentConfigOverlay::from);
1445 let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
1446 AgentConfigOverlay::fold(
1447 harness_layers
1448 .chain(agent_layers)
1449 .chain([AgentConfigOverlay::from(session)]),
1450 )
1451}
1452
1453fn hydrate_plugin_refs(
1460 capabilities: &mut [AgentCapabilityConfig],
1461 plugin_configs: &[AgentCapabilityConfig],
1462) {
1463 for cap in capabilities.iter_mut() {
1464 let cap_id = cap.capability_id();
1465 if !everruns_core::is_plugin_capability(cap_id) {
1466 continue;
1467 }
1468 let is_bare = cap.config.is_null()
1470 || cap
1471 .config
1472 .as_object()
1473 .map(|o| o.is_empty())
1474 .unwrap_or(false);
1475 if !is_bare {
1476 continue;
1477 }
1478 if let Some(hydrated) = plugin_configs.iter().find(|c| c.capability_id() == cap_id) {
1479 cap.config = hydrated.config.clone();
1480 }
1481 }
1482}
1483
1484async fn seed_runtime_initial_files(
1485 harness_store: &dyn RuntimeHarnessStore,
1486 agent_store: &dyn RuntimeAgentStore,
1487 file_store: &dyn SessionFileSystem,
1488 session: &Session,
1489) -> Result<()> {
1490 let harness_chain = harness_store.get_harness_chain(session.harness_id).await?;
1491 if harness_chain.is_empty() {
1492 return Err(AgentLoopError::store(format!(
1493 "harness not found while seeding files: {}",
1494 session.harness_id
1495 )));
1496 }
1497 let agent = match session.agent_id {
1498 Some(agent_id) => Some(
1499 agent_store
1500 .get_agent(agent_id)
1501 .await?
1502 .ok_or_else(|| AgentLoopError::store(format!("agent not found: {agent_id}")))?,
1503 ),
1504 None => None,
1505 };
1506 let overlay = effective_overlay(&harness_chain, agent.as_ref(), session);
1507 let seed_key = SessionId::from_uuid(session.workspace_id.uuid());
1510 for file in &overlay.initial_files {
1511 file_store.seed_initial_file(seed_key, file).await?;
1512 }
1513 Ok(())
1514}
1515
1516fn message_from_event(data: &EventData) -> Option<Message> {
1517 match data {
1518 EventData::InputMessage(data) => Some(data.message.clone()),
1519 EventData::OutputMessageCompleted(OutputMessageCompletedData { message, .. }) => {
1520 Some(message.clone())
1521 }
1522 EventData::ToolCompleted(data) => Some(tool_completed_to_message(data.clone())),
1523 _ => None,
1524 }
1525}
1526
1527fn tool_completed_to_message(data: ToolCompletedData) -> Message {
1528 let mut images: Vec<ToolResultImage> = Vec::new();
1529 let metadata = tool_result_metadata(&data);
1530 let result = data.result.map(|parts| {
1531 for part in &parts {
1532 if let ContentPart::Image(img) = part
1533 && let (Some(base64), Some(media_type)) = (&img.base64, &img.media_type)
1534 {
1535 images.push(ToolResultImage {
1536 base64: base64.clone(),
1537 media_type: media_type.clone(),
1538 });
1539 }
1540 }
1541
1542 let text_parts: Vec<&ContentPart> = parts
1543 .iter()
1544 .filter(|part| matches!(part, ContentPart::Text(_)))
1545 .collect();
1546 if text_parts.len() == 1
1547 && let ContentPart::Text(text) = text_parts[0]
1548 {
1549 return parse_structured_tool_result_text(&text.text);
1550 }
1551 if !text_parts.is_empty() {
1552 serde_json::to_value(&text_parts).unwrap_or_default()
1553 } else {
1554 serde_json::Value::Null
1555 }
1556 });
1557
1558 let mut message = if images.is_empty() {
1559 Message::tool_result(&data.tool_call_id, result, data.error)
1560 } else {
1561 Message::tool_result_with_images(&data.tool_call_id, result, images)
1562 };
1563 message.metadata = metadata;
1564 message
1565}
1566
1567fn tool_result_metadata(
1568 data: &ToolCompletedData,
1569) -> Option<std::collections::HashMap<String, serde_json::Value>> {
1570 let mut metadata = std::collections::HashMap::new();
1571 metadata.insert("tool_name".to_string(), serde_json::json!(data.tool_name));
1572 if let Some(fingerprint) = &data.tool_call_fingerprint {
1573 metadata.insert(
1574 "tool_call_fingerprint".to_string(),
1575 serde_json::json!(fingerprint),
1576 );
1577 }
1578 if let Some(fingerprint) = &data.tool_result_fingerprint {
1579 metadata.insert(
1580 "tool_result_fingerprint".to_string(),
1581 serde_json::json!(fingerprint),
1582 );
1583 }
1584 (!metadata.is_empty()).then_some(metadata)
1585}
1586
1587fn parse_structured_tool_result_text(text: &str) -> serde_json::Value {
1588 let trimmed = text.trim_start();
1589 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
1590 return serde_json::Value::String(text.to_string());
1591 }
1592
1593 match serde_json::from_str(text) {
1594 Ok(value @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => value,
1595 _ => serde_json::Value::String(text.to_string()),
1596 }
1597}
1598
1599#[cfg(test)]
1600mod tool_completed_replay_tests {
1601 use super::*;
1602
1603 #[test]
1604 fn tool_completed_replay_preserves_json_object_shape() {
1605 let data = ToolCompletedData::success(
1606 "call_read".to_string(),
1607 "read_file".to_string(),
1608 vec![ContentPart::text(
1609 serde_json::json!({
1610 "path": "/workspace/src/lib.rs",
1611 "content": "1|fn main() {}"
1612 })
1613 .to_string(),
1614 )],
1615 Some(1),
1616 );
1617
1618 let message = tool_completed_to_message(data);
1619 let result = message
1620 .tool_result_content()
1621 .and_then(|content| content.result.as_ref())
1622 .expect("tool result should be present");
1623
1624 assert_eq!(result["path"], "/workspace/src/lib.rs");
1625 assert_eq!(result["content"], "1|fn main() {}");
1626 }
1627
1628 #[test]
1629 fn tool_completed_replay_keeps_scalar_json_as_text() {
1630 let data = ToolCompletedData::success(
1631 "call_scalar".to_string(),
1632 "custom_tool".to_string(),
1633 vec![ContentPart::text("123")],
1634 Some(1),
1635 );
1636
1637 let message = tool_completed_to_message(data);
1638 let result = message
1639 .tool_result_content()
1640 .and_then(|content| content.result.as_ref())
1641 .expect("tool result should be present");
1642
1643 assert_eq!(result, &serde_json::Value::String("123".to_string()));
1644 }
1645
1646 #[test]
1647 fn tool_completed_replay_preserves_fingerprints_as_metadata() {
1648 let data = ToolCompletedData::success(
1649 "call_read".to_string(),
1650 "read_file".to_string(),
1651 vec![ContentPart::text("{}")],
1652 Some(1),
1653 )
1654 .with_fingerprints("sha256:call".to_string(), "sha256:result".to_string());
1655
1656 let message = tool_completed_to_message(data);
1657 let metadata = message.metadata.expect("metadata should be present");
1658
1659 assert_eq!(metadata["tool_name"], "read_file");
1660 assert_eq!(metadata["tool_call_fingerprint"], "sha256:call");
1661 assert_eq!(metadata["tool_result_fingerprint"], "sha256:result");
1662 }
1663}
1664
1665#[cfg(test)]
1666mod org_id_mapping_tests {
1667 use super::*;
1668 use everruns_core::{DEFAULT_ORG_ID, DEFAULT_ORG_PUBLIC_ID, org_public_id_from_internal};
1669
1670 #[test]
1671 fn default_public_id_maps_to_default_org() {
1672 assert_eq!(
1673 in_process_internal_org_id(DEFAULT_ORG_PUBLIC_ID),
1674 DEFAULT_ORG_ID
1675 );
1676 }
1677
1678 #[test]
1679 fn invalid_public_id_does_not_fall_back_to_default() {
1680 for invalid in [
1681 "",
1682 "not-an-org",
1683 "org_short",
1684 "org_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
1685 "ORG_00000000000000000000000000000001",
1686 ] {
1687 let mapped = in_process_internal_org_id(invalid);
1688 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1689 assert!(
1690 mapped >= 2,
1691 "invalid input {invalid:?} should not map to default"
1692 );
1693 }
1694 }
1695
1696 #[test]
1697 fn zero_public_id_does_not_fall_back_to_default() {
1698 let mapped = in_process_internal_org_id("org_00000000000000000000000000000000");
1701 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1702 assert!(mapped >= 2, "all-zero id should not map to default");
1703 }
1704
1705 #[test]
1706 fn synthetic_public_id_round_trips_with_internal_helper() {
1707 for internal in [1_i64, 2, 42, 1_000_000, i64::MAX - 1, i64::MAX] {
1708 let public = org_public_id_from_internal(internal);
1709 assert_eq!(
1710 in_process_internal_org_id(&public),
1711 internal,
1712 "round-trip failed for internal={internal}"
1713 );
1714 }
1715 }
1716
1717 #[test]
1718 fn distinct_synthetic_ids_map_to_distinct_internal_ids() {
1719 let a = org_public_id_from_internal(7);
1720 let b = org_public_id_from_internal(8);
1721 assert_ne!(a, b);
1722 assert_ne!(
1723 in_process_internal_org_id(&a),
1724 in_process_internal_org_id(&b)
1725 );
1726 }
1727
1728 #[test]
1729 fn high_entropy_uuid_style_id_hashes_into_reserved_range() {
1730 let high = "org_80000000000000000000000000000000";
1734 let mapped = in_process_internal_org_id(high);
1735 assert!(mapped >= 2, "mapped id {mapped} must be >= 2");
1736 assert_ne!(mapped, DEFAULT_ORG_ID);
1737
1738 assert_eq!(mapped, in_process_internal_org_id(high));
1740 }
1741
1742 #[test]
1743 fn high_entropy_ids_are_isolated_from_each_other() {
1744 let a = in_process_internal_org_id("org_80000000000000000000000000000001");
1745 let b = in_process_internal_org_id("org_80000000000000000000000000000002");
1746 assert_ne!(a, b);
1747 assert_ne!(a, DEFAULT_ORG_ID);
1748 assert_ne!(b, DEFAULT_ORG_ID);
1749 }
1750
1751 #[test]
1752 fn hash_uses_stable_sha256_truncation() {
1753 let mapped = in_process_internal_org_id("org_80000000000000000000000000000000");
1757 let expected = {
1758 let digest = sha2::Sha256::digest(b"org_80000000000000000000000000000000");
1759 let mut buf = [0u8; 8];
1760 buf.copy_from_slice(&digest[..8]);
1761 let raw = u64::from_be_bytes(buf);
1762 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
1763 };
1764 assert_eq!(mapped, expected);
1765 }
1766
1767 #[test]
1768 fn oversize_input_is_bounded_and_does_not_collide_silently() {
1769 let oversize = "x".repeat(super::HASH_INPUT_CAP_BYTES * 4);
1775 let mapped = in_process_internal_org_id(&oversize);
1776 assert!(mapped >= 2);
1777 assert_ne!(mapped, DEFAULT_ORG_ID);
1778 }
1779}