1use crate::backends::{
6 EventBus, RuntimeAgentStore, RuntimeBackends, RuntimeHarnessStore, RuntimeMessageStore,
7 RuntimeProviderStore, RuntimeSessionStore,
8};
9use crate::builders::SingleSessionBuilder;
10use crate::host::{
11 RuntimeHostAdapter, RuntimeHostTurnContext, execute_act_activity, execute_input_activity,
12 execute_reason_activity_with_prompt_messages,
13};
14use crate::in_memory::{InMemorySessionFileStore, InMemorySessionFileSystemFactory};
15use crate::turn_strategy::{RuntimeTurnPlan, RuntimeTurnState};
16use async_trait::async_trait;
17use chrono::Utc;
18use everruns_core::agent::Agent;
19use everruns_core::atoms::{AtomContext, InputAtomInput, ReasonInput};
20use everruns_core::capabilities::{
21 Capability, CapabilityRegistry, CapabilityStatus, collect_capability_mcp_servers,
22 resolve_capability_configs,
23};
24use everruns_core::config_layer::AgentConfigOverlay;
25use everruns_core::driver_registry::{DriverId, DriverRegistry};
26use everruns_core::error::{AgentLoopError, Result};
27use everruns_core::events::{
28 Event, EventContext, EventData, EventRequest, InputMessageData, OutputMessageCompletedData,
29 ToolCompletedData,
30};
31use everruns_core::harness::Harness;
32use everruns_core::llmsim_driver::{LlmSimConfig, LlmSimDriver};
33use everruns_core::message::{ContentPart, Message};
34use everruns_core::platform_definition::PlatformDefinition;
35use everruns_core::plugins::{PluginFileSet, compile_plugin};
36use everruns_core::runtime_context::{AssembledTurnContext, inspect_turn_context};
37use everruns_core::session::{Session, SessionStatus};
38use everruns_core::session_file::{InitialFile, SessionFile};
39use everruns_core::tools::ToolResultImage;
40use everruns_core::traits::{
41 AgentStore, EventEmitter, HarnessStore, ProviderStore, ResolvedModel, SessionMutator,
42 SessionStorageStore, SessionStore, UserConnectionResolver,
43};
44use everruns_core::turn::TurnStopReason;
45use everruns_core::typed_id::{AgentId, HarnessId, MessageId, OrgId, SessionId, TurnId};
46use everruns_core::{
47 AgentCapabilityConfig, CapabilityId, InputMessage, MessageRetriever, SessionFileSystem,
48 SessionFileSystemFactoryContext, plugin_capability_id, resolve_runtime_capabilities,
49};
50use everruns_engine::{ActOutcome, plan_after_act, plan_after_process_input, plan_after_reason};
51use sha2::{Digest, Sha256};
52use std::path::Path;
53use std::sync::Arc;
54
55const HASH_INPUT_CAP_BYTES: usize = 128;
61
62pub fn in_process_internal_org_id(public_org_id: &str) -> i64 {
75 if public_org_id == everruns_core::DEFAULT_ORG_PUBLIC_ID {
76 return everruns_core::DEFAULT_ORG_ID;
77 }
78
79 let Ok(parsed) = public_org_id.parse::<OrgId>() else {
80 return hash_public_org_id(public_org_id);
81 };
82 let raw: u128 = parsed.uuid().as_u128();
83 if raw == 0 {
84 return hash_public_org_id(public_org_id);
85 }
86
87 if raw <= i64::MAX as u128 {
90 return raw as i64;
91 }
92
93 hash_public_org_id(public_org_id)
94}
95
96fn hash_public_org_id(public_org_id: &str) -> i64 {
101 let bytes = public_org_id.as_bytes();
102 let bounded = &bytes[..bytes.len().min(HASH_INPUT_CAP_BYTES)];
103 let digest = Sha256::digest(bounded);
104 let mut buf = [0u8; 8];
105 buf.copy_from_slice(&digest[..8]);
106 let raw = u64::from_be_bytes(buf);
107 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
108}
109
110#[derive(Debug, Clone)]
111pub struct TurnResult {
112 pub response: String,
114 pub iterations: usize,
116 pub tool_calls_count: usize,
118 pub success: bool,
120 pub error: Option<String>,
122 pub stop_reason: TurnStopReason,
124 pub turn_id: everruns_core::typed_id::TurnId,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct CapabilityDelta {
135 pub capability_id: String,
137 pub changed: bool,
139 pub active: bool,
141 pub surfaces_dirty: bool,
143}
144
145fn finish_turn(
153 turn_id: everruns_core::typed_id::TurnId,
154 stop_reason: TurnStopReason,
155 error: Option<String>,
156 response: String,
157 iterations: usize,
158 tool_calls_count: usize,
159) -> TurnResult {
160 if error.is_some() {
161 return TurnResult {
162 response: String::new(),
163 iterations,
164 tool_calls_count: 0,
165 success: false,
166 error,
167 stop_reason,
168 turn_id,
169 };
170 }
171
172 TurnResult {
173 response,
174 iterations,
175 tool_calls_count,
176 success: true,
177 error: None,
178 stop_reason,
179 turn_id,
180 }
181}
182
183pub struct InProcessRuntimeBuilder {
193 platform_definition: PlatformDefinition,
194 llm_sim_config: Option<LlmSimConfig>,
195 default_model: Option<ResolvedModel>,
196 backends: Option<RuntimeBackends>,
197 session_file_system_factory_context: SessionFileSystemFactoryContext,
198 harnesses: Vec<Harness>,
199 agents: Vec<Agent>,
200 sessions: Vec<Session>,
201 default_session_id: Option<SessionId>,
202 seeded_files: Vec<(SessionId, InitialFile)>,
203 mcp_auth_provider: Option<Arc<dyn everruns_mcp::McpAuthProvider>>,
204 provider_retry_config: Option<everruns_core::llm_retry::LlmRetryConfig>,
205 provider_stall_timeout: Option<std::time::Duration>,
206 plugin_capability_configs: Vec<AgentCapabilityConfig>,
212 plugin_warnings: Vec<String>,
214}
215
216impl Default for InProcessRuntimeBuilder {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222impl InProcessRuntimeBuilder {
223 pub fn new() -> Self {
231 Self {
232 platform_definition: PlatformDefinition::builder()
233 .capability_registry(CapabilityRegistry::runtime_builtins())
234 .driver_registry(DriverRegistry::new())
235 .session_file_system_factory(Arc::new(InMemorySessionFileSystemFactory))
236 .build(),
237 llm_sim_config: None,
238 default_model: None,
239 backends: None,
240 session_file_system_factory_context: SessionFileSystemFactoryContext::new(),
241 harnesses: Vec::new(),
242 agents: Vec::new(),
243 sessions: Vec::new(),
244 default_session_id: None,
245 seeded_files: Vec::new(),
246 mcp_auth_provider: None,
247 provider_retry_config: None,
248 provider_stall_timeout: None,
249 plugin_capability_configs: Vec::new(),
250 plugin_warnings: Vec::new(),
251 }
252 }
253
254 pub fn mcp_auth_provider(mut self, provider: Arc<dyn everruns_mcp::McpAuthProvider>) -> Self {
258 self.mcp_auth_provider = Some(provider);
259 self
260 }
261
262 pub fn platform_definition(mut self, platform_definition: PlatformDefinition) -> Self {
264 self.platform_definition = platform_definition;
265 self
266 }
267
268 pub fn capability<C: Capability + 'static>(mut self, capability: C) -> Self {
270 self.platform_definition
271 .capability_registry_mut()
272 .register(capability);
273 self
274 }
275
276 pub fn driver_registry(mut self, driver_registry: DriverRegistry) -> Self {
278 *self.platform_definition.driver_registry_mut() = driver_registry;
279 self
280 }
281
282 pub fn llm_sim(mut self, config: LlmSimConfig) -> Self {
284 self.llm_sim_config = Some(config);
285 self
286 }
287
288 pub fn default_model(mut self, model: ResolvedModel) -> Self {
290 self.default_model = Some(model);
291 self
292 }
293
294 pub fn backends(mut self, backends: RuntimeBackends) -> Self {
296 self.backends = Some(backends);
297 self
298 }
299
300 pub fn with_session_task_registry(
304 mut self,
305 registry: Arc<dyn everruns_core::session_task::SessionTaskRegistry>,
306 ) -> Self {
307 let backends = self
308 .backends
309 .take()
310 .unwrap_or_else(RuntimeBackends::in_memory);
311 self.backends = Some(backends.with_session_task_registry(registry));
312 self
313 }
314
315 pub fn with_schedule_store_factory(
317 mut self,
318 factory: crate::backends::ScheduleStoreFactory,
319 ) -> Self {
320 let backends = self
321 .backends
322 .take()
323 .unwrap_or_else(RuntimeBackends::in_memory);
324 self.backends = Some(backends.with_schedule_store_factory(factory));
325 self
326 }
327
328 pub fn with_platform_store_factory(
330 mut self,
331 factory: crate::backends::PlatformStoreFactory,
332 ) -> Self {
333 let backends = self
334 .backends
335 .take()
336 .unwrap_or_else(RuntimeBackends::in_memory);
337 self.backends = Some(backends.with_platform_store_factory(factory));
338 self
339 }
340
341 pub fn session_file_system_factory_context(
343 mut self,
344 context: SessionFileSystemFactoryContext,
345 ) -> Self {
346 self.session_file_system_factory_context = context;
347 self
348 }
349
350 pub fn provider_retry_config(
352 mut self,
353 config: everruns_core::llm_retry::LlmRetryConfig,
354 ) -> Self {
355 self.provider_retry_config = Some(config);
356 self
357 }
358
359 pub fn provider_stall_timeout(mut self, timeout: std::time::Duration) -> Self {
361 self.provider_stall_timeout = Some(timeout);
362 self
363 }
364
365 pub fn harness(mut self, harness: Harness) -> Self {
367 self.harnesses.push(harness);
368 self
369 }
370
371 pub fn agent(mut self, agent: Agent) -> Self {
373 self.agents.push(agent);
374 self
375 }
376
377 pub fn session(mut self, session: Session) -> Self {
379 self.sessions.push(session);
380 self
381 }
382
383 pub fn single_session<F>(mut self, configure: F) -> Self
388 where
389 F: FnOnce(SingleSessionBuilder) -> SingleSessionBuilder,
390 {
391 let (harness, agent, session, session_id) =
392 configure(SingleSessionBuilder::default()).build();
393 self.harnesses.push(harness);
394 self.agents.push(agent);
395 self.sessions.push(session);
396 self.default_session_id = Some(session_id);
397 self
398 }
399
400 pub fn seed_text_file(
404 mut self,
405 session_id: SessionId,
406 path: impl Into<String>,
407 content: impl Into<String>,
408 ) -> Self {
409 self.seeded_files.push((
410 session_id,
411 InitialFile {
412 path: path.into(),
413 content: content.into(),
414 encoding: "text".to_string(),
415 is_readonly: false,
416 },
417 ));
418 self
419 }
420
421 pub fn with_plugin_dir(mut self, path: &Path) -> Result<Self> {
442 let file_set = PluginFileSet::from_dir(path)
443 .map_err(|e| AgentLoopError::config(format!("plugin directory load failed: {e}")))?;
444 let compiled = compile_plugin(&file_set)
445 .map_err(|e| AgentLoopError::config(format!("plugin compilation failed: {e}")))?;
446
447 for warning in &compiled.warnings {
448 tracing::warn!(plugin = %compiled.definition.name, warning = %warning, "plugin compile warning");
449 }
450 self.plugin_warnings.extend(compiled.warnings);
451
452 let cap_id = plugin_capability_id(&compiled.definition.name);
453 let hydrated_config = serde_json::to_value(&compiled.definition)
454 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
455 self.plugin_capability_configs
456 .push(AgentCapabilityConfig::with_config(cap_id, hydrated_config));
457
458 Ok(self)
459 }
460
461 pub fn plugin_capability(&self, name: &str) -> Option<AgentCapabilityConfig> {
467 let cap_id = plugin_capability_id(name);
468 self.plugin_capability_configs
469 .iter()
470 .find(|c| c.capability_id() == cap_id)
471 .cloned()
472 }
473
474 pub async fn build(mut self) -> Result<InProcessRuntime> {
479 let backends = match self.backends.take() {
480 Some(backends) => backends,
481 None => RuntimeBackends::in_memory(),
482 };
483 let file_store = resolve_session_file_system(
484 &self.platform_definition,
485 self.session_file_system_factory_context.clone(),
486 )
487 .await?;
488
489 if let Some(config) = self.llm_sim_config.take() {
490 let driver = LlmSimDriver::new(config);
491 self.platform_definition
494 .driver_registry_mut()
495 .register_or_replace(DriverId::LlmSim, move |_config| Box::new(driver.clone()));
496
497 if self.default_model.is_none() {
498 self.default_model = Some(ResolvedModel {
499 model: "llmsim-model".to_string(),
500 provider_type: DriverId::LlmSim,
501 api_key: Some("fake-key".to_string()),
502 base_url: None,
503 provider_metadata: None,
504 });
505 }
506 }
507
508 let default_model = self.default_model.ok_or_else(|| {
509 AgentLoopError::config(
510 "in-process runtime requires a default model; call \
511 InProcessRuntimeBuilder::default_model(...) or \
512 InProcessRuntimeBuilder::llm_sim(...)",
513 )
514 })?;
515
516 backends
517 .provider_store
518 .set_default_model(default_model)
519 .await?;
520
521 for harness in &mut self.harnesses {
525 hydrate_plugin_refs(&mut harness.capabilities, &self.plugin_capability_configs);
526 }
527 for agent in &mut self.agents {
528 hydrate_plugin_refs(&mut agent.capabilities, &self.plugin_capability_configs);
529 }
530 for session in &mut self.sessions {
531 hydrate_plugin_refs(&mut session.capabilities, &self.plugin_capability_configs);
532 }
533
534 for harness in &self.harnesses {
535 backends.harness_store.add_harness(harness.clone()).await?;
536 }
537 for agent in &self.agents {
538 backends.agent_store.add_agent(agent.clone()).await?;
539 }
540 for session in &self.sessions {
541 backends.session_store.add_session(session.clone()).await?;
542 }
543
544 for session in &self.sessions {
545 seed_runtime_initial_files(
546 backends.harness_store.as_ref(),
547 backends.agent_store.as_ref(),
548 file_store.as_ref(),
549 session,
550 )
551 .await?;
552 }
553
554 for (session_id, file) in &self.seeded_files {
555 file_store.seed_initial_file(*session_id, file).await?;
556 }
557
558 let persisting_emitter =
559 PersistingEventEmitter::new(backends.event_bus.clone(), backends.message_store.clone());
560
561 let (session_task_registry, session_wake_queue) = match backends.session_task_registry {
567 Some(inner) => {
568 let wake_queue = Arc::new(everruns_core::SessionWakeQueue::new());
569 let observing = everruns_core::ObservingTaskRegistry::new(inner)
570 .with_observer(wake_queue.clone());
571 let wrapped: Arc<dyn everruns_core::session_task::SessionTaskRegistry> =
572 Arc::new(observing);
573 (Some(wrapped), Some(wake_queue))
574 }
575 None => (None, None),
576 };
577
578 Ok(InProcessRuntime {
579 platform_definition: Arc::new(self.platform_definition),
580 harness_store: backends.harness_store,
581 agent_store: backends.agent_store,
582 session_store: backends.session_store,
583 default_session_id: self.default_session_id,
584 message_store: backends.message_store,
585 compaction_checkpoint_store: backends.compaction_checkpoint_store,
586 provider_store: backends.provider_store,
587 event_bus: backends.event_bus,
588 persisting_emitter,
589 file_store,
590 storage_store: backends.storage_store,
591 connection_resolver: backends.connection_resolver,
592 session_task_registry,
593 session_wake_queue,
594 schedule_store_factory: backends.schedule_store_factory,
595 platform_store_factory: backends.platform_store_factory,
596 mcp_auth_provider: self
597 .mcp_auth_provider
598 .unwrap_or_else(|| Arc::new(everruns_mcp::NoAuthProvider)),
599 provider_retry_config: self.provider_retry_config,
600 provider_stall_timeout: self.provider_stall_timeout,
601 mcp_discovery_cache: Arc::new(crate::mcp_cache::McpDiscoveryCache::new()),
602 plugin_warnings: self.plugin_warnings,
603 })
604 }
605}
606
607async fn resolve_session_file_system(
608 platform_definition: &PlatformDefinition,
609 file_system_factory_context: SessionFileSystemFactoryContext,
610) -> Result<Arc<dyn SessionFileSystem>> {
611 let file_system_factory = platform_definition.session_file_system_factory();
612 if file_system_factory.is_disabled() {
613 Ok(Arc::new(InMemorySessionFileStore::new()))
614 } else {
615 Ok(file_system_factory
616 .create_session_file_system(file_system_factory_context)
617 .await?)
618 }
619}
620
621#[derive(Clone)]
622pub struct InProcessRuntime {
628 platform_definition: Arc<PlatformDefinition>,
629 harness_store: Arc<dyn RuntimeHarnessStore>,
630 agent_store: Arc<dyn RuntimeAgentStore>,
631 session_store: Arc<dyn RuntimeSessionStore>,
632 default_session_id: Option<SessionId>,
633 message_store: Arc<dyn RuntimeMessageStore>,
634 compaction_checkpoint_store: Arc<dyn everruns_core::CompactionCheckpointStore>,
635 provider_store: Arc<dyn RuntimeProviderStore>,
636 event_bus: Arc<dyn EventBus>,
637 persisting_emitter: PersistingEventEmitter,
638 file_store: Arc<dyn SessionFileSystem>,
639 storage_store: Arc<dyn SessionStorageStore>,
640 connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
641 session_task_registry: Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>>,
642 session_wake_queue: Option<Arc<everruns_core::SessionWakeQueue>>,
646 schedule_store_factory: Option<crate::backends::ScheduleStoreFactory>,
647 platform_store_factory: Option<crate::backends::PlatformStoreFactory>,
648 mcp_auth_provider: Arc<dyn everruns_mcp::McpAuthProvider>,
649 provider_retry_config: Option<everruns_core::llm_retry::LlmRetryConfig>,
650 provider_stall_timeout: Option<std::time::Duration>,
651 mcp_discovery_cache: Arc<crate::mcp_cache::McpDiscoveryCache>,
652 plugin_warnings: Vec<String>,
655}
656
657impl InProcessRuntime {
658 fn mcp_client(&self) -> Arc<everruns_mcp::McpClient> {
661 Arc::new(everruns_mcp::McpClient::new(
662 self.platform_definition.egress_service(),
663 self.mcp_auth_provider.clone(),
664 ))
665 }
666
667 async fn session_mcp_servers(
669 &self,
670 session: &Session,
671 agent: Option<&Agent>,
672 ) -> everruns_core::ScopedMcpServers {
673 let harness_chain = self
674 .harness_store
675 .get_harness_chain(session.harness_id)
676 .await
677 .unwrap_or_default();
678 let resolved = resolve_runtime_capabilities(
679 &harness_chain,
680 agent,
681 session,
682 self.platform_definition.capability_registry(),
683 );
684 let contributed = collect_capability_mcp_servers(
685 &resolved.resolved_capability_configs,
686 self.platform_definition.capability_registry(),
687 );
688 let explicit = crate::mcp::merge_session_scoped_servers(&harness_chain, agent, session);
689 everruns_core::merge_scoped_mcp_servers(&contributed, &explicit)
690 }
691 pub fn builder() -> InProcessRuntimeBuilder {
693 InProcessRuntimeBuilder::new()
694 }
695
696 pub fn default_session_id(&self) -> Option<SessionId> {
699 self.default_session_id
700 }
701
702 pub fn plugin_warnings(&self) -> &[String] {
707 &self.plugin_warnings
708 }
709
710 pub async fn activate_capability(
716 &self,
717 session_id: SessionId,
718 capability: impl Into<AgentCapabilityConfig>,
719 ) -> Result<CapabilityDelta> {
720 let mut capability = capability.into();
721 let registry = self.platform_definition.capability_registry();
722 let registered = registry.get(capability.capability_id()).ok_or_else(|| {
723 AgentLoopError::config(format!(
724 "unknown capability: {}",
725 capability.capability_id()
726 ))
727 })?;
728 if registered.status() != CapabilityStatus::Available {
729 return Err(AgentLoopError::config(format!(
730 "capability is not available: {}",
731 capability.capability_id()
732 )));
733 }
734 registered
735 .validate_config(&capability.config)
736 .map_err(|error| {
737 AgentLoopError::config(format!("invalid capability config: {error}"))
738 })?;
739
740 let canonical_id = registered.id().to_string();
741 capability.capability_ref = CapabilityId::new(canonical_id.clone());
742 let context = self.load_context(session_id).await?;
743 if context
744 .resolved_capability_configs
745 .iter()
746 .any(|config| config.capability_id() == canonical_id)
747 {
748 return Ok(CapabilityDelta {
749 capability_id: canonical_id,
750 changed: false,
751 active: true,
752 surfaces_dirty: false,
753 });
754 }
755
756 let mut candidate = context.effective_overlay.capabilities;
757 candidate.push(capability.clone());
758 resolve_capability_configs(&candidate, registry)
759 .map_err(|error| AgentLoopError::config(error.to_string()))?;
760
761 self.session_store
762 .upsert_session_capability(session_id, capability)
763 .await?;
764 self.mcp_discovery_cache
765 .invalidate_session(session_id.uuid());
766 Ok(CapabilityDelta {
767 capability_id: canonical_id,
768 changed: true,
769 active: true,
770 surfaces_dirty: true,
771 })
772 }
773
774 pub async fn deactivate_capability(
779 &self,
780 session_id: SessionId,
781 capability_id: &str,
782 ) -> Result<CapabilityDelta> {
783 let registry = self.platform_definition.capability_registry();
784 let registered = registry.get(capability_id).ok_or_else(|| {
785 AgentLoopError::config(format!("unknown capability: {capability_id}"))
786 })?;
787 let canonical_id = registered.id().to_string();
788 let context = self.load_context(session_id).await?;
789 if !context
790 .resolved_capability_configs
791 .iter()
792 .any(|config| config.capability_id() == canonical_id)
793 {
794 return Ok(CapabilityDelta {
795 capability_id: canonical_id,
796 changed: false,
797 active: false,
798 surfaces_dirty: false,
799 });
800 }
801
802 let session_capability_id = context
803 .session
804 .capabilities
805 .iter()
806 .find(|config| {
807 registry
808 .get(config.capability_id())
809 .is_some_and(|capability| capability.id() == canonical_id)
810 })
811 .map(|config| config.capability_id().to_string())
812 .ok_or_else(|| {
813 AgentLoopError::config(format!(
814 "capability {canonical_id} is inherited and cannot be deactivated at the session layer"
815 ))
816 })?;
817
818 self.session_store
819 .remove_session_capability(session_id, &session_capability_id)
820 .await?;
821 self.mcp_discovery_cache
822 .invalidate_session(session_id.uuid());
823 Ok(CapabilityDelta {
824 capability_id: canonical_id,
825 changed: true,
826 active: false,
827 surfaces_dirty: true,
828 })
829 }
830
831 pub async fn run_turn(
838 &self,
839 session_id: SessionId,
840 input: impl Into<InputMessage>,
841 ) -> Result<TurnResult> {
842 let session = self
843 .session_store
844 .get_session(session_id)
845 .await?
846 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
847
848 let input_message = self
853 .message_store
854 .add_input_message(session_id, input.into())
855 .await?;
856 self.event_bus
857 .emit(EventRequest::new(
858 session_id,
859 EventContext::empty(),
860 InputMessageData::new(input_message.clone()),
861 ))
862 .await?;
863
864 let org_id = in_process_internal_org_id(&session.organization_id);
865 let turn_id = TurnId::new();
866
867 let mut state = RuntimeTurnState {
873 org_id,
874 session_id,
875 harness_id: session.harness_id,
876 agent_id: session.agent_id,
877 input_message_id: input_message.id,
878 turn_id: None,
879 previous_response_id: None,
880 iteration: 1,
881 request_id: None,
882 started_at: None,
883 cumulative_usage: None,
884 tool_call_count: 0,
885 llm_call_count: 0,
886 time_to_first_token_ms: None,
887 final_message_id: None,
888 final_answer_preview: None,
889 };
890
891 let base_context = |exec: bool| {
892 let context = AtomContext::new(session_id, turn_id, input_message.id)
893 .with_workspace_id(session.workspace_id);
894 if exec { context.next_exec() } else { context }
895 };
896
897 execute_input_activity(
901 self,
902 org_id,
903 InputAtomInput {
904 context: base_context(false),
905 },
906 )
907 .await?;
908 let mut plan = plan_after_process_input(&state, Some(turn_id), Utc::now());
909
910 let mut iterations: usize = 0;
913 let mut tool_calls_count: usize = 0;
914 let mut last_response = String::new();
915
916 loop {
917 match plan {
918 RuntimeTurnPlan::ScheduleReason(next_state) => {
919 state = next_state;
920 let wake_message_ids = self.drain_and_inject_wakes(session_id).await?;
926 let mut prompt_message_ids = wake_message_ids;
927 if state.iteration == 1 {
928 prompt_message_ids.insert(0, input_message.id);
929 }
930 let reason_result = execute_reason_activity_with_prompt_messages(
931 self,
932 org_id,
933 ReasonInput {
934 context: base_context(true),
935 harness_id: session.harness_id,
936 agent_id: session.agent_id,
937 org_id,
938 mcp_tool_definitions: vec![],
939 previous_response_id: state.previous_response_id.clone(),
940 iteration: state.iteration,
941 },
942 prompt_message_ids,
943 )
944 .await?;
945
946 iterations += 1;
947 if !reason_result.text.is_empty() {
948 last_response = reason_result.text.clone();
949 }
950
951 let pending_user_message_count = usize::from(
957 self.session_wake_queue
958 .as_ref()
959 .is_some_and(|q| q.has_pending(session_id)),
960 );
961
962 let act_scheduling =
963 crate::turn_strategy::resolve_act_scheduling(self, &state, &reason_result)
964 .await?;
965 let (next, effects) = plan_after_reason(
966 &state,
967 reason_result,
968 pending_user_message_count,
969 Utc::now(),
970 act_scheduling,
971 );
972 crate::turn_strategy::perform_effects(self, org_id, session_id, effects).await;
973 plan = next;
974 }
975 RuntimeTurnPlan::ScheduleAct(act_plan) => {
976 tool_calls_count += act_plan.input.tool_calls.len();
977 state = *act_plan.resume_state;
981 state.previous_response_id = act_plan.previous_response_id;
982 state.iteration = act_plan.iteration;
983 state.request_id = act_plan.request_id;
984
985 let act_result = execute_act_activity(self, act_plan.input).await?;
986 let outcome = ActOutcome {
987 blocked: act_result.blocked,
988 waiting_for_tool_results: act_result.waiting_for_tool_results,
989 };
990 let setup_connection_hint_enabled =
991 crate::turn_strategy::resolve_setup_connection_hint(
992 self, org_id, session_id, outcome,
993 )
994 .await;
995 let (next, effects) =
996 plan_after_act(&state, outcome, setup_connection_hint_enabled);
997 crate::turn_strategy::perform_effects(self, org_id, session_id, effects).await;
998 plan = next;
999 }
1000 RuntimeTurnPlan::Complete { stop_reason, error } => {
1001 return Ok(finish_turn(
1002 turn_id,
1003 stop_reason,
1004 error,
1005 last_response,
1006 iterations,
1007 tool_calls_count,
1008 ));
1009 }
1010 RuntimeTurnPlan::WaitForToolResults { .. } => {
1014 return Ok(finish_turn(
1015 turn_id,
1016 TurnStopReason::EndTurn,
1017 None,
1018 last_response,
1019 iterations,
1020 tool_calls_count,
1021 ));
1022 }
1023 }
1024 }
1025 }
1026
1027 pub async fn run_text_turn(
1028 &self,
1029 session_id: SessionId,
1030 text: impl Into<String>,
1031 ) -> Result<TurnResult> {
1032 self.run_turn(session_id, InputMessage::user(text)).await
1033 }
1034
1035 async fn drain_and_inject_wakes(&self, session_id: SessionId) -> Result<Vec<MessageId>> {
1047 let Some(queue) = &self.session_wake_queue else {
1048 return Ok(vec![]);
1049 };
1050 let wakes = queue.drain(session_id);
1051 if wakes.is_empty() {
1052 return Ok(vec![]);
1053 }
1054 let mut message_ids = Vec::with_capacity(wakes.len());
1055 for wake in wakes {
1056 let message = self
1061 .message_store
1062 .add_input_message(session_id, InputMessage::user(wake.text))
1063 .await?;
1064 message_ids.push(message.id);
1065 self.event_bus
1066 .emit(EventRequest::new(
1067 session_id,
1068 EventContext::empty(),
1069 InputMessageData::new(message),
1070 ))
1071 .await?;
1072 }
1073 Ok(message_ids)
1074 }
1075
1076 pub async fn messages(&self, session_id: SessionId) -> Result<Vec<Message>> {
1078 self.message_store.load(session_id).await
1079 }
1080
1081 pub async fn read_file(
1083 &self,
1084 session_id: SessionId,
1085 path: &str,
1086 ) -> Result<Option<SessionFile>> {
1087 self.file_store.read_file(session_id, path).await
1088 }
1089
1090 pub async fn load_context(&self, session_id: SessionId) -> Result<AssembledTurnContext> {
1092 let session = self
1093 .session_store
1094 .get_session(session_id)
1095 .await?
1096 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1097 self.inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
1098 .await
1099 }
1100
1101 pub async fn events(&self) -> Result<Vec<Event>> {
1106 Ok(self.event_bus.collected_events().await)
1107 }
1108
1109 pub async fn execute_command(
1119 &self,
1120 session_id: SessionId,
1121 request: everruns_core::command::ExecuteCommandRequest,
1122 ) -> Result<everruns_core::command::CommandResult> {
1123 let ctx = self.load_context(session_id).await?;
1124 let registry = self.platform_definition.capability_registry();
1125 let host = everruns_core::command_host::StoreCommandHost::new(
1129 session_id,
1130 self.harness_store.clone(),
1131 self.agent_store.clone(),
1132 self.session_store.clone(),
1133 self.message_store.clone(),
1134 self.provider_store.clone(),
1135 registry.clone(),
1136 self.platform_definition.driver_registry().clone(),
1137 )
1138 .with_file_store(self.file_store.clone())
1139 .with_assembled_context(ctx.clone());
1140 let exec_ctx =
1141 everruns_core::command::CommandExecutionContext::new(session_id, Arc::new(host));
1142 for config in &ctx.resolved_capability_configs {
1143 let Some(capability) = registry.get(config.capability_id()) else {
1144 continue;
1145 };
1146 if capability.commands().iter().any(|c| c.name == request.name) {
1147 return capability.execute_command(&request, &exec_ctx).await;
1148 }
1149 }
1150 Err(AgentLoopError::config(format!(
1151 "no capability declares command /{}",
1152 request.name
1153 )))
1154 }
1155
1156 pub async fn list_commands(
1166 &self,
1167 session_id: SessionId,
1168 ) -> Result<Vec<everruns_core::command::CommandDescriptor>> {
1169 let ctx = self.load_context(session_id).await?;
1170 let registry = self.platform_definition.capability_registry();
1171 let mut seen = std::collections::HashSet::new();
1172 let mut commands = Vec::new();
1173 for config in &ctx.resolved_capability_configs {
1174 let Some(capability) = registry.get(config.capability_id()) else {
1175 continue;
1176 };
1177 for command in capability.commands() {
1178 if seen.insert(command.name.clone()) {
1179 commands.push(command);
1180 }
1181 }
1182 }
1183 Ok(commands)
1184 }
1185
1186 async fn inspect_context_with_ids(
1187 &self,
1188 session_id: SessionId,
1189 harness_id: everruns_core::HarnessId,
1190 agent_id: Option<AgentId>,
1191 ) -> Result<AssembledTurnContext> {
1192 inspect_turn_context(
1193 self.harness_store.as_ref(),
1194 self.agent_store.as_ref(),
1195 self.session_store.as_ref(),
1196 self.message_store.as_ref(),
1197 self.provider_store.as_ref(),
1198 self.platform_definition.capability_registry(),
1199 session_id,
1200 harness_id,
1201 agent_id,
1202 &[],
1203 Some(self.file_store.clone()),
1204 )
1205 .await
1206 }
1207}
1208
1209#[async_trait]
1210impl RuntimeHostAdapter for InProcessRuntime {
1211 async fn get_agent(&self, _org_id: i64, agent_id: AgentId) -> Result<Option<Agent>> {
1212 self.agent_store.get_agent(agent_id).await
1213 }
1214
1215 async fn get_harness(&self, _org_id: i64, harness_id: HarnessId) -> Result<Option<Harness>> {
1216 let chain = self.harness_store.get_harness_chain(harness_id).await?;
1217 Ok(chain.into_iter().last())
1218 }
1219
1220 async fn set_session_status(
1221 &self,
1222 _org_id: i64,
1223 session_id: SessionId,
1224 _status: SessionStatus,
1225 ) -> Result<Session> {
1226 self.session_store
1230 .get_session(session_id)
1231 .await?
1232 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))
1233 }
1234
1235 async fn load_turn_context(
1236 &self,
1237 _org_id: i64,
1238 session_id: SessionId,
1239 ) -> Result<RuntimeHostTurnContext> {
1240 let mut session = self
1241 .session_store
1242 .get_session(session_id)
1243 .await?
1244 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1245 everruns_core::ard_attachment::apply_session_attachments(
1248 self.storage_store.as_ref(),
1249 &mut session,
1250 )
1251 .await;
1252 let agent = match session.agent_id {
1253 Some(agent_id) => self.agent_store.get_agent(agent_id).await?,
1254 None => None,
1255 };
1256 let messages = self.message_store.load(session_id).await?;
1257 let model = self.provider_store.get_default_model().await?;
1258
1259 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1262 let mcp_tool_definitions = if scoped_servers.is_empty() {
1263 vec![]
1264 } else {
1265 crate::mcp::discover_tool_definitions(
1266 &self.mcp_discovery_cache,
1267 self.mcp_client(),
1268 session_id.uuid(),
1269 &scoped_servers,
1270 )
1271 .await
1272 };
1273
1274 Ok(RuntimeHostTurnContext {
1275 agent,
1276 session,
1277 messages,
1278 model,
1279 mcp_tool_definitions,
1280 })
1281 }
1282
1283 async fn mcp_executor(
1284 &self,
1285 _org_id: i64,
1286 session_id: SessionId,
1287 ) -> Option<Arc<everruns_mcp::McpExecutor>> {
1288 let session = self.session_store.get_session(session_id).await.ok()??;
1289 let agent = match session.agent_id {
1290 Some(agent_id) => self.agent_store.get_agent(agent_id).await.ok().flatten(),
1291 None => None,
1292 };
1293 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1294 crate::mcp::build_executor(self.mcp_client(), &scoped_servers)
1295 }
1296
1297 fn capability_registry(&self) -> CapabilityRegistry {
1298 self.platform_definition.capability_registry().clone()
1299 }
1300
1301 fn driver_registry(&self) -> DriverRegistry {
1302 self.platform_definition.driver_registry().clone()
1303 }
1304
1305 fn harness_store(&self, _org_id: i64) -> Arc<dyn HarnessStore> {
1306 self.harness_store.clone()
1307 }
1308
1309 fn agent_store(&self, _org_id: i64) -> Arc<dyn AgentStore> {
1310 self.agent_store.clone()
1311 }
1312
1313 fn session_store(&self, _org_id: i64) -> Arc<dyn SessionStore> {
1314 self.session_store.clone()
1315 }
1316
1317 fn session_mutator(&self, _org_id: i64) -> Arc<dyn SessionMutator> {
1318 self.session_store.clone()
1319 }
1320
1321 fn provider_store(&self, _org_id: i64) -> Arc<dyn ProviderStore> {
1322 self.provider_store.clone()
1323 }
1324
1325 fn message_store(&self) -> Arc<dyn MessageRetriever> {
1326 self.message_store.clone()
1327 }
1328
1329 fn compaction_checkpoint_store(
1330 &self,
1331 ) -> Option<Arc<dyn everruns_core::CompactionCheckpointStore>> {
1332 Some(self.compaction_checkpoint_store.clone())
1333 }
1334
1335 fn event_emitter(&self) -> Arc<dyn EventEmitter> {
1336 Arc::new(self.persisting_emitter.clone())
1337 }
1338
1339 fn file_store(&self) -> Arc<dyn SessionFileSystem> {
1340 self.file_store.clone()
1341 }
1342
1343 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
1344 Some(self.storage_store.clone())
1345 }
1346
1347 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
1348 self.connection_resolver.clone()
1349 }
1350
1351 fn session_task_registry(
1352 &self,
1353 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
1354 self.session_task_registry.clone()
1355 }
1356
1357 fn schedule_store(
1358 &self,
1359 org_id: i64,
1360 ) -> Option<Arc<dyn everruns_core::traits::SessionScheduleStore>> {
1361 self.schedule_store_factory
1362 .as_ref()
1363 .map(|factory| factory(org_id))
1364 }
1365
1366 fn platform_store(
1367 &self,
1368 org_id: i64,
1369 session_id: SessionId,
1370 ) -> Option<Arc<dyn everruns_platform::PlatformStore>> {
1371 self.platform_store_factory
1372 .as_ref()
1373 .map(|factory| factory(org_id, session_id))
1374 }
1375
1376 fn utility_llm_service(&self) -> Option<Arc<dyn everruns_core::UtilityLlmService>> {
1377 Some(self.platform_definition.utility_llm_service())
1378 }
1379
1380 fn egress_service(&self) -> Option<Arc<dyn everruns_core::EgressService>> {
1381 Some(self.platform_definition.egress_service())
1382 }
1383
1384 fn provider_retry_config(&self) -> Option<everruns_core::llm_retry::LlmRetryConfig> {
1385 self.provider_retry_config.clone()
1386 }
1387
1388 fn provider_stall_timeout(&self) -> Option<std::time::Duration> {
1389 self.provider_stall_timeout
1390 }
1391}
1392
1393#[derive(Clone)]
1394struct PersistingEventEmitter {
1395 inner: Arc<dyn EventBus>,
1396 message_store: Arc<dyn RuntimeMessageStore>,
1397}
1398
1399impl PersistingEventEmitter {
1400 fn new(inner: Arc<dyn EventBus>, message_store: Arc<dyn RuntimeMessageStore>) -> Self {
1401 Self {
1402 inner,
1403 message_store,
1404 }
1405 }
1406}
1407
1408#[async_trait]
1409impl EventEmitter for PersistingEventEmitter {
1410 async fn emit(&self, request: EventRequest) -> Result<Event> {
1411 let event = self.inner.emit(request.clone()).await?;
1412 if let Some(message) = message_from_event(&event.data) {
1413 self.message_store
1414 .store_message(request.session_id, message)
1415 .await?;
1416 }
1417 Ok(event)
1418 }
1419}
1420
1421fn effective_overlay(
1422 harness_chain: &[Harness],
1423 agent: Option<&Agent>,
1424 session: &Session,
1425) -> AgentConfigOverlay {
1426 let harness_layers = harness_chain.iter().map(AgentConfigOverlay::from);
1427 let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
1428 AgentConfigOverlay::fold(
1429 harness_layers
1430 .chain(agent_layers)
1431 .chain([AgentConfigOverlay::from(session)]),
1432 )
1433}
1434
1435fn hydrate_plugin_refs(
1442 capabilities: &mut [AgentCapabilityConfig],
1443 plugin_configs: &[AgentCapabilityConfig],
1444) {
1445 for cap in capabilities.iter_mut() {
1446 let cap_id = cap.capability_id();
1447 if !everruns_core::is_plugin_capability(cap_id) {
1448 continue;
1449 }
1450 let is_bare = cap.config.is_null()
1452 || cap
1453 .config
1454 .as_object()
1455 .map(|o| o.is_empty())
1456 .unwrap_or(false);
1457 if !is_bare {
1458 continue;
1459 }
1460 if let Some(hydrated) = plugin_configs.iter().find(|c| c.capability_id() == cap_id) {
1461 cap.config = hydrated.config.clone();
1462 }
1463 }
1464}
1465
1466async fn seed_runtime_initial_files(
1467 harness_store: &dyn RuntimeHarnessStore,
1468 agent_store: &dyn RuntimeAgentStore,
1469 file_store: &dyn SessionFileSystem,
1470 session: &Session,
1471) -> Result<()> {
1472 let harness_chain = harness_store.get_harness_chain(session.harness_id).await?;
1473 if harness_chain.is_empty() {
1474 return Err(AgentLoopError::store(format!(
1475 "harness not found while seeding files: {}",
1476 session.harness_id
1477 )));
1478 }
1479 let agent = match session.agent_id {
1480 Some(agent_id) => Some(
1481 agent_store
1482 .get_agent(agent_id)
1483 .await?
1484 .ok_or_else(|| AgentLoopError::store(format!("agent not found: {agent_id}")))?,
1485 ),
1486 None => None,
1487 };
1488 let overlay = effective_overlay(&harness_chain, agent.as_ref(), session);
1489 let seed_key = SessionId::from_uuid(session.workspace_id.uuid());
1492 for file in &overlay.initial_files {
1493 file_store.seed_initial_file(seed_key, file).await?;
1494 }
1495 Ok(())
1496}
1497
1498fn message_from_event(data: &EventData) -> Option<Message> {
1499 match data {
1500 EventData::InputMessage(data) => Some(data.message.clone()),
1501 EventData::OutputMessageCompleted(OutputMessageCompletedData { message, .. }) => {
1502 Some(message.clone())
1503 }
1504 EventData::ToolCompleted(data) => Some(tool_completed_to_message(data.clone())),
1505 _ => None,
1506 }
1507}
1508
1509fn tool_completed_to_message(data: ToolCompletedData) -> Message {
1510 let mut images: Vec<ToolResultImage> = Vec::new();
1511 let metadata = tool_result_metadata(&data);
1512 let result = data.result.map(|parts| {
1513 for part in &parts {
1514 if let ContentPart::Image(img) = part
1515 && let (Some(base64), Some(media_type)) = (&img.base64, &img.media_type)
1516 {
1517 images.push(ToolResultImage {
1518 base64: base64.clone(),
1519 media_type: media_type.clone(),
1520 });
1521 }
1522 }
1523
1524 let text_parts: Vec<&ContentPart> = parts
1525 .iter()
1526 .filter(|part| matches!(part, ContentPart::Text(_)))
1527 .collect();
1528 if text_parts.len() == 1
1529 && let ContentPart::Text(text) = text_parts[0]
1530 {
1531 return parse_structured_tool_result_text(&text.text);
1532 }
1533 if !text_parts.is_empty() {
1534 serde_json::to_value(&text_parts).unwrap_or_default()
1535 } else {
1536 serde_json::Value::Null
1537 }
1538 });
1539
1540 let mut message = if images.is_empty() {
1541 Message::tool_result(&data.tool_call_id, result, data.error)
1542 } else {
1543 Message::tool_result_with_images(&data.tool_call_id, result, images)
1544 };
1545 message.metadata = metadata;
1546 message
1547}
1548
1549fn tool_result_metadata(
1550 data: &ToolCompletedData,
1551) -> Option<std::collections::HashMap<String, serde_json::Value>> {
1552 let mut metadata = std::collections::HashMap::new();
1553 metadata.insert("tool_name".to_string(), serde_json::json!(data.tool_name));
1554 if let Some(fingerprint) = &data.tool_call_fingerprint {
1555 metadata.insert(
1556 "tool_call_fingerprint".to_string(),
1557 serde_json::json!(fingerprint),
1558 );
1559 }
1560 if let Some(fingerprint) = &data.tool_result_fingerprint {
1561 metadata.insert(
1562 "tool_result_fingerprint".to_string(),
1563 serde_json::json!(fingerprint),
1564 );
1565 }
1566 (!metadata.is_empty()).then_some(metadata)
1567}
1568
1569fn parse_structured_tool_result_text(text: &str) -> serde_json::Value {
1570 let trimmed = text.trim_start();
1571 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
1572 return serde_json::Value::String(text.to_string());
1573 }
1574
1575 match serde_json::from_str(text) {
1576 Ok(value @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => value,
1577 _ => serde_json::Value::String(text.to_string()),
1578 }
1579}
1580
1581#[cfg(test)]
1582mod tool_completed_replay_tests {
1583 use super::*;
1584
1585 #[test]
1586 fn tool_completed_replay_preserves_json_object_shape() {
1587 let data = ToolCompletedData::success(
1588 "call_read".to_string(),
1589 "read_file".to_string(),
1590 vec![ContentPart::text(
1591 serde_json::json!({
1592 "path": "/workspace/src/lib.rs",
1593 "content": "1|fn main() {}"
1594 })
1595 .to_string(),
1596 )],
1597 Some(1),
1598 );
1599
1600 let message = tool_completed_to_message(data);
1601 let result = message
1602 .tool_result_content()
1603 .and_then(|content| content.result.as_ref())
1604 .expect("tool result should be present");
1605
1606 assert_eq!(result["path"], "/workspace/src/lib.rs");
1607 assert_eq!(result["content"], "1|fn main() {}");
1608 }
1609
1610 #[test]
1611 fn tool_completed_replay_keeps_scalar_json_as_text() {
1612 let data = ToolCompletedData::success(
1613 "call_scalar".to_string(),
1614 "custom_tool".to_string(),
1615 vec![ContentPart::text("123")],
1616 Some(1),
1617 );
1618
1619 let message = tool_completed_to_message(data);
1620 let result = message
1621 .tool_result_content()
1622 .and_then(|content| content.result.as_ref())
1623 .expect("tool result should be present");
1624
1625 assert_eq!(result, &serde_json::Value::String("123".to_string()));
1626 }
1627
1628 #[test]
1629 fn tool_completed_replay_preserves_fingerprints_as_metadata() {
1630 let data = ToolCompletedData::success(
1631 "call_read".to_string(),
1632 "read_file".to_string(),
1633 vec![ContentPart::text("{}")],
1634 Some(1),
1635 )
1636 .with_fingerprints("sha256:call".to_string(), "sha256:result".to_string());
1637
1638 let message = tool_completed_to_message(data);
1639 let metadata = message.metadata.expect("metadata should be present");
1640
1641 assert_eq!(metadata["tool_name"], "read_file");
1642 assert_eq!(metadata["tool_call_fingerprint"], "sha256:call");
1643 assert_eq!(metadata["tool_result_fingerprint"], "sha256:result");
1644 }
1645}
1646
1647#[cfg(test)]
1648mod org_id_mapping_tests {
1649 use super::*;
1650 use everruns_core::{DEFAULT_ORG_ID, DEFAULT_ORG_PUBLIC_ID, org_public_id_from_internal};
1651
1652 #[test]
1653 fn default_public_id_maps_to_default_org() {
1654 assert_eq!(
1655 in_process_internal_org_id(DEFAULT_ORG_PUBLIC_ID),
1656 DEFAULT_ORG_ID
1657 );
1658 }
1659
1660 #[test]
1661 fn invalid_public_id_does_not_fall_back_to_default() {
1662 for invalid in [
1663 "",
1664 "not-an-org",
1665 "org_short",
1666 "org_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
1667 "ORG_00000000000000000000000000000001",
1668 ] {
1669 let mapped = in_process_internal_org_id(invalid);
1670 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1671 assert!(
1672 mapped >= 2,
1673 "invalid input {invalid:?} should not map to default"
1674 );
1675 }
1676 }
1677
1678 #[test]
1679 fn zero_public_id_does_not_fall_back_to_default() {
1680 let mapped = in_process_internal_org_id("org_00000000000000000000000000000000");
1683 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1684 assert!(mapped >= 2, "all-zero id should not map to default");
1685 }
1686
1687 #[test]
1688 fn synthetic_public_id_round_trips_with_internal_helper() {
1689 for internal in [1_i64, 2, 42, 1_000_000, i64::MAX - 1, i64::MAX] {
1690 let public = org_public_id_from_internal(internal);
1691 assert_eq!(
1692 in_process_internal_org_id(&public),
1693 internal,
1694 "round-trip failed for internal={internal}"
1695 );
1696 }
1697 }
1698
1699 #[test]
1700 fn distinct_synthetic_ids_map_to_distinct_internal_ids() {
1701 let a = org_public_id_from_internal(7);
1702 let b = org_public_id_from_internal(8);
1703 assert_ne!(a, b);
1704 assert_ne!(
1705 in_process_internal_org_id(&a),
1706 in_process_internal_org_id(&b)
1707 );
1708 }
1709
1710 #[test]
1711 fn high_entropy_uuid_style_id_hashes_into_reserved_range() {
1712 let high = "org_80000000000000000000000000000000";
1716 let mapped = in_process_internal_org_id(high);
1717 assert!(mapped >= 2, "mapped id {mapped} must be >= 2");
1718 assert_ne!(mapped, DEFAULT_ORG_ID);
1719
1720 assert_eq!(mapped, in_process_internal_org_id(high));
1722 }
1723
1724 #[test]
1725 fn high_entropy_ids_are_isolated_from_each_other() {
1726 let a = in_process_internal_org_id("org_80000000000000000000000000000001");
1727 let b = in_process_internal_org_id("org_80000000000000000000000000000002");
1728 assert_ne!(a, b);
1729 assert_ne!(a, DEFAULT_ORG_ID);
1730 assert_ne!(b, DEFAULT_ORG_ID);
1731 }
1732
1733 #[test]
1734 fn hash_uses_stable_sha256_truncation() {
1735 let mapped = in_process_internal_org_id("org_80000000000000000000000000000000");
1739 let expected = {
1740 let digest = sha2::Sha256::digest(b"org_80000000000000000000000000000000");
1741 let mut buf = [0u8; 8];
1742 buf.copy_from_slice(&digest[..8]);
1743 let raw = u64::from_be_bytes(buf);
1744 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
1745 };
1746 assert_eq!(mapped, expected);
1747 }
1748
1749 #[test]
1750 fn oversize_input_is_bounded_and_does_not_collide_silently() {
1751 let oversize = "x".repeat(super::HASH_INPUT_CAP_BYTES * 4);
1757 let mapped = in_process_internal_org_id(&oversize);
1758 assert!(mapped >= 2);
1759 assert_ne!(mapped, DEFAULT_ORG_ID);
1760 }
1761}