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 plugin_capability_configs: Vec<AgentCapabilityConfig>,
231 plugin_warnings: Vec<String>,
233}
234
235impl Default for InProcessRuntimeBuilder {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241impl InProcessRuntimeBuilder {
242 pub fn new() -> Self {
250 Self {
251 platform_definition: PlatformDefinition::builder()
252 .capability_registry(CapabilityRegistry::runtime_builtins())
253 .driver_registry(DriverRegistry::new())
254 .session_file_system_factory(Arc::new(InMemorySessionFileSystemFactory))
255 .build(),
256 llm_sim_config: None,
257 default_model: None,
258 backends: None,
259 session_file_system_factory_context: SessionFileSystemFactoryContext::new(),
260 harnesses: Vec::new(),
261 agents: Vec::new(),
262 sessions: Vec::new(),
263 default_session_id: None,
264 seeded_files: Vec::new(),
265 mcp_auth_provider: None,
266 plugin_capability_configs: Vec::new(),
267 plugin_warnings: Vec::new(),
268 }
269 }
270
271 pub fn mcp_auth_provider(mut self, provider: Arc<dyn everruns_mcp::McpAuthProvider>) -> Self {
275 self.mcp_auth_provider = Some(provider);
276 self
277 }
278
279 pub fn platform_definition(mut self, platform_definition: PlatformDefinition) -> Self {
281 self.platform_definition = platform_definition;
282 self
283 }
284
285 pub fn capability<C: Capability + 'static>(mut self, capability: C) -> Self {
287 self.platform_definition
288 .capability_registry_mut()
289 .register(capability);
290 self
291 }
292
293 pub fn driver_registry(mut self, driver_registry: DriverRegistry) -> Self {
295 *self.platform_definition.driver_registry_mut() = driver_registry;
296 self
297 }
298
299 pub fn llm_sim(mut self, config: LlmSimConfig) -> Self {
301 self.llm_sim_config = Some(config);
302 self
303 }
304
305 pub fn default_model(mut self, model: ResolvedModel) -> Self {
307 self.default_model = Some(model);
308 self
309 }
310
311 pub fn backends(mut self, backends: RuntimeBackends) -> Self {
313 self.backends = Some(backends);
314 self
315 }
316
317 pub fn with_session_task_registry(
321 mut self,
322 registry: Arc<dyn everruns_core::session_task::SessionTaskRegistry>,
323 ) -> Self {
324 let backends = self
325 .backends
326 .take()
327 .unwrap_or_else(RuntimeBackends::in_memory);
328 self.backends = Some(backends.with_session_task_registry(registry));
329 self
330 }
331
332 pub fn with_schedule_store_factory(
334 mut self,
335 factory: crate::backends::ScheduleStoreFactory,
336 ) -> Self {
337 let backends = self
338 .backends
339 .take()
340 .unwrap_or_else(RuntimeBackends::in_memory);
341 self.backends = Some(backends.with_schedule_store_factory(factory));
342 self
343 }
344
345 pub fn with_platform_store_factory(
347 mut self,
348 factory: crate::backends::PlatformStoreFactory,
349 ) -> Self {
350 let backends = self
351 .backends
352 .take()
353 .unwrap_or_else(RuntimeBackends::in_memory);
354 self.backends = Some(backends.with_platform_store_factory(factory));
355 self
356 }
357
358 pub fn session_file_system_factory_context(
360 mut self,
361 context: SessionFileSystemFactoryContext,
362 ) -> Self {
363 self.session_file_system_factory_context = context;
364 self
365 }
366
367 pub fn harness(mut self, harness: Harness) -> Self {
369 self.harnesses.push(harness);
370 self
371 }
372
373 pub fn agent(mut self, agent: Agent) -> Self {
375 self.agents.push(agent);
376 self
377 }
378
379 pub fn session(mut self, session: Session) -> Self {
381 self.sessions.push(session);
382 self
383 }
384
385 pub fn single_session<F>(mut self, configure: F) -> Self
390 where
391 F: FnOnce(SingleSessionBuilder) -> SingleSessionBuilder,
392 {
393 let (harness, agent, session, session_id) =
394 configure(SingleSessionBuilder::default()).build();
395 self.harnesses.push(harness);
396 self.agents.push(agent);
397 self.sessions.push(session);
398 self.default_session_id = Some(session_id);
399 self
400 }
401
402 pub fn seed_text_file(
406 mut self,
407 session_id: SessionId,
408 path: impl Into<String>,
409 content: impl Into<String>,
410 ) -> Self {
411 self.seeded_files.push((
412 session_id,
413 InitialFile {
414 path: path.into(),
415 content: content.into(),
416 encoding: "text".to_string(),
417 is_readonly: false,
418 },
419 ));
420 self
421 }
422
423 pub fn with_plugin_dir(mut self, path: &Path) -> Result<Self> {
444 let file_set = PluginFileSet::from_dir(path)
445 .map_err(|e| AgentLoopError::config(format!("plugin directory load failed: {e}")))?;
446 let compiled = compile_plugin(&file_set)
447 .map_err(|e| AgentLoopError::config(format!("plugin compilation failed: {e}")))?;
448
449 for warning in &compiled.warnings {
450 tracing::warn!(plugin = %compiled.definition.name, warning = %warning, "plugin compile warning");
451 }
452 self.plugin_warnings.extend(compiled.warnings);
453
454 let cap_id = plugin_capability_id(&compiled.definition.name);
455 let hydrated_config = serde_json::to_value(&compiled.definition)
456 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
457 self.plugin_capability_configs
458 .push(AgentCapabilityConfig::with_config(cap_id, hydrated_config));
459
460 Ok(self)
461 }
462
463 pub fn plugin_capability(&self, name: &str) -> Option<AgentCapabilityConfig> {
469 let cap_id = plugin_capability_id(name);
470 self.plugin_capability_configs
471 .iter()
472 .find(|c| c.capability_id() == cap_id)
473 .cloned()
474 }
475
476 pub async fn build(mut self) -> Result<InProcessRuntime> {
481 let backends = match self.backends.take() {
482 Some(backends) => backends,
483 None => RuntimeBackends::in_memory(),
484 };
485 let file_store = resolve_session_file_system(
486 &self.platform_definition,
487 self.session_file_system_factory_context.clone(),
488 )
489 .await?;
490
491 if let Some(config) = self.llm_sim_config.take() {
492 let driver = LlmSimDriver::new(config);
493 self.platform_definition
496 .driver_registry_mut()
497 .register_or_replace(DriverId::LlmSim, move |_config| Box::new(driver.clone()));
498
499 if self.default_model.is_none() {
500 self.default_model = Some(ResolvedModel {
501 model: "llmsim-model".to_string(),
502 provider_type: DriverId::LlmSim,
503 api_key: Some("fake-key".to_string()),
504 base_url: None,
505 provider_metadata: None,
506 });
507 }
508 }
509
510 let default_model = self.default_model.ok_or_else(|| {
511 AgentLoopError::config(
512 "in-process runtime requires a default model; call \
513 InProcessRuntimeBuilder::default_model(...) or \
514 InProcessRuntimeBuilder::llm_sim(...)",
515 )
516 })?;
517
518 backends
519 .provider_store
520 .set_default_model(default_model)
521 .await?;
522
523 for harness in &mut self.harnesses {
527 hydrate_plugin_refs(&mut harness.capabilities, &self.plugin_capability_configs);
528 }
529 for agent in &mut self.agents {
530 hydrate_plugin_refs(&mut agent.capabilities, &self.plugin_capability_configs);
531 }
532 for session in &mut self.sessions {
533 hydrate_plugin_refs(&mut session.capabilities, &self.plugin_capability_configs);
534 }
535
536 for harness in &self.harnesses {
537 backends.harness_store.add_harness(harness.clone()).await?;
538 }
539 for agent in &self.agents {
540 backends.agent_store.add_agent(agent.clone()).await?;
541 }
542 for session in &self.sessions {
543 backends.session_store.add_session(session.clone()).await?;
544 }
545
546 for session in &self.sessions {
547 seed_runtime_initial_files(
548 backends.harness_store.as_ref(),
549 backends.agent_store.as_ref(),
550 file_store.as_ref(),
551 session,
552 )
553 .await?;
554 }
555
556 for (session_id, file) in &self.seeded_files {
557 file_store.seed_initial_file(*session_id, file).await?;
558 }
559
560 let persisting_emitter =
561 PersistingEventEmitter::new(backends.event_bus.clone(), backends.message_store.clone());
562
563 let (session_task_registry, session_wake_queue) = match backends.session_task_registry {
569 Some(inner) => {
570 let wake_queue = Arc::new(everruns_core::SessionWakeQueue::new());
571 let observing = everruns_core::ObservingTaskRegistry::new(inner)
572 .with_observer(wake_queue.clone());
573 let wrapped: Arc<dyn everruns_core::session_task::SessionTaskRegistry> =
574 Arc::new(observing);
575 (Some(wrapped), Some(wake_queue))
576 }
577 None => (None, None),
578 };
579
580 Ok(InProcessRuntime {
581 platform_definition: Arc::new(self.platform_definition),
582 harness_store: backends.harness_store,
583 agent_store: backends.agent_store,
584 session_store: backends.session_store,
585 default_session_id: self.default_session_id,
586 message_store: backends.message_store,
587 compaction_checkpoint_store: backends.compaction_checkpoint_store,
588 provider_store: backends.provider_store,
589 event_bus: backends.event_bus,
590 persisting_emitter,
591 file_store,
592 storage_store: backends.storage_store,
593 connection_resolver: backends.connection_resolver,
594 session_task_registry,
595 session_wake_queue,
596 schedule_store_factory: backends.schedule_store_factory,
597 platform_store_factory: backends.platform_store_factory,
598 mcp_auth_provider: self
599 .mcp_auth_provider
600 .unwrap_or_else(|| Arc::new(everruns_mcp::NoAuthProvider)),
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 mcp_discovery_cache: Arc<crate::mcp_cache::McpDiscoveryCache>,
650 plugin_warnings: Vec<String>,
653}
654
655impl InProcessRuntime {
656 fn mcp_client(&self) -> Arc<everruns_mcp::McpClient> {
659 Arc::new(everruns_mcp::McpClient::new(
660 self.platform_definition.egress_service(),
661 self.mcp_auth_provider.clone(),
662 ))
663 }
664
665 async fn session_mcp_servers(
667 &self,
668 session: &Session,
669 agent: Option<&Agent>,
670 ) -> everruns_core::ScopedMcpServers {
671 let harness_chain = self
672 .harness_store
673 .get_harness_chain(session.harness_id)
674 .await
675 .unwrap_or_default();
676 let resolved = resolve_runtime_capabilities(
677 &harness_chain,
678 agent,
679 session,
680 self.platform_definition.capability_registry(),
681 );
682 let contributed = collect_capability_mcp_servers(
683 &resolved.resolved_capability_configs,
684 self.platform_definition.capability_registry(),
685 );
686 let explicit = crate::mcp::merge_session_scoped_servers(&harness_chain, agent, session);
687 everruns_core::merge_scoped_mcp_servers(&contributed, &explicit)
688 }
689 pub fn builder() -> InProcessRuntimeBuilder {
691 InProcessRuntimeBuilder::new()
692 }
693
694 pub fn default_session_id(&self) -> Option<SessionId> {
697 self.default_session_id
698 }
699
700 pub fn plugin_warnings(&self) -> &[String] {
705 &self.plugin_warnings
706 }
707
708 pub async fn activate_capability(
714 &self,
715 session_id: SessionId,
716 capability: impl Into<AgentCapabilityConfig>,
717 ) -> Result<CapabilityDelta> {
718 let mut capability = capability.into();
719 let registry = self.platform_definition.capability_registry();
720 let registered = registry.get(capability.capability_id()).ok_or_else(|| {
721 AgentLoopError::config(format!(
722 "unknown capability: {}",
723 capability.capability_id()
724 ))
725 })?;
726 if registered.status() != CapabilityStatus::Available {
727 return Err(AgentLoopError::config(format!(
728 "capability is not available: {}",
729 capability.capability_id()
730 )));
731 }
732 registered
733 .validate_config(&capability.config)
734 .map_err(|error| {
735 AgentLoopError::config(format!("invalid capability config: {error}"))
736 })?;
737
738 let canonical_id = registered.id().to_string();
739 capability.capability_ref = CapabilityId::new(canonical_id.clone());
740 let context = self.load_context(session_id).await?;
741 if context
742 .resolved_capability_configs
743 .iter()
744 .any(|config| config.capability_id() == canonical_id)
745 {
746 return Ok(CapabilityDelta {
747 capability_id: canonical_id,
748 changed: false,
749 active: true,
750 surfaces_dirty: false,
751 });
752 }
753
754 let mut candidate = context.effective_overlay.capabilities;
755 candidate.push(capability.clone());
756 resolve_capability_configs(&candidate, registry)
757 .map_err(|error| AgentLoopError::config(error.to_string()))?;
758
759 self.session_store
760 .upsert_session_capability(session_id, capability)
761 .await?;
762 self.mcp_discovery_cache
763 .invalidate_session(session_id.uuid());
764 Ok(CapabilityDelta {
765 capability_id: canonical_id,
766 changed: true,
767 active: true,
768 surfaces_dirty: true,
769 })
770 }
771
772 pub async fn deactivate_capability(
777 &self,
778 session_id: SessionId,
779 capability_id: &str,
780 ) -> Result<CapabilityDelta> {
781 let registry = self.platform_definition.capability_registry();
782 let registered = registry.get(capability_id).ok_or_else(|| {
783 AgentLoopError::config(format!("unknown capability: {capability_id}"))
784 })?;
785 let canonical_id = registered.id().to_string();
786 let context = self.load_context(session_id).await?;
787 if !context
788 .resolved_capability_configs
789 .iter()
790 .any(|config| config.capability_id() == canonical_id)
791 {
792 return Ok(CapabilityDelta {
793 capability_id: canonical_id,
794 changed: false,
795 active: false,
796 surfaces_dirty: false,
797 });
798 }
799
800 let session_capability_id = context
801 .session
802 .capabilities
803 .iter()
804 .find(|config| {
805 registry
806 .get(config.capability_id())
807 .is_some_and(|capability| capability.id() == canonical_id)
808 })
809 .map(|config| config.capability_id().to_string())
810 .ok_or_else(|| {
811 AgentLoopError::config(format!(
812 "capability {canonical_id} is inherited and cannot be deactivated at the session layer"
813 ))
814 })?;
815
816 self.session_store
817 .remove_session_capability(session_id, &session_capability_id)
818 .await?;
819 self.mcp_discovery_cache
820 .invalidate_session(session_id.uuid());
821 Ok(CapabilityDelta {
822 capability_id: canonical_id,
823 changed: true,
824 active: false,
825 surfaces_dirty: true,
826 })
827 }
828
829 pub async fn run_turn(
835 &self,
836 session_id: SessionId,
837 input: impl Into<InputMessage>,
838 ) -> Result<TurnResult> {
839 let session = self
840 .session_store
841 .get_session(session_id)
842 .await?
843 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
844
845 let input_message = self
850 .message_store
851 .add_input_message(session_id, input.into())
852 .await?;
853 self.event_bus
854 .emit(EventRequest::new(
855 session_id,
856 EventContext::empty(),
857 InputMessageData::new(input_message.clone()),
858 ))
859 .await?;
860
861 let assembled = self
862 .inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
863 .await?;
864 let synthetic_agent_id = session
865 .agent_id
866 .unwrap_or_else(|| AgentId::from_uuid(session.id.uuid()));
867 let org_id = in_process_internal_org_id(&session.organization_id);
868 let mut state_machine = TurnStateMachine::new(
869 TurnContext::new(session_id, input_message.id, synthetic_agent_id, org_id),
870 assembled.runtime_agent.max_iterations,
871 );
872
873 let mut previous_response_id: Option<String> = None;
874 let mut last_reason_result: Option<everruns_core::ReasonResult> = None;
875
876 loop {
877 match state_machine.next_action() {
878 TurnAction::ExecuteInput => {
879 let ctx = state_machine.context();
880 let base_context =
881 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
882 .with_workspace_id(session.workspace_id);
883 execute_input_activity(
884 self,
885 org_id,
886 InputAtomInput {
887 context: base_context,
888 },
889 )
890 .await?;
891 state_machine.on_input_completed();
892 }
893 TurnAction::ExecuteReason => {
894 let ctx = state_machine.context();
895 let session_id = ctx.session_id;
896 self.drain_and_inject_wakes(session_id).await?;
902 let base_context =
903 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
904 .with_workspace_id(session.workspace_id);
905 let reason_result = execute_reason_activity(
906 self,
907 org_id,
908 ReasonInput {
909 context: base_context.next_exec(),
910 harness_id: session.harness_id,
911 agent_id: session.agent_id,
912 org_id,
913 mcp_tool_definitions: vec![],
914 previous_response_id: previous_response_id.take(),
915 iteration: state_machine.current_iteration() as u32 + 1,
916 },
917 )
918 .await?;
919 previous_response_id = reason_result.response_id.clone();
920 let has_pending_wakes = self
925 .session_wake_queue
926 .as_ref()
927 .is_some_and(|q| q.has_pending(session_id));
928 state_machine.on_reason_completed(
929 reason_result.text.clone(),
930 reason_result.tool_calls.len(),
931 reason_result.success,
932 reason_result.error.clone(),
933 reason_result.finish_reason.clone(),
934 has_pending_wakes,
935 );
936 if reason_result.has_tool_calls {
937 last_reason_result = Some(reason_result);
938 }
939 }
940 TurnAction::ExecuteAct => {
941 let reason_result = last_reason_result
942 .take()
943 .expect("ExecuteAct requires a prior ReasonResult");
944 let ctx = state_machine.context();
945 let base_context =
946 AtomContext::new(ctx.session_id, ctx.turn_id, ctx.input_message_id)
947 .with_workspace_id(session.workspace_id);
948 execute_act_activity(
949 self,
950 ActInput {
951 org_id: Some(org_id),
952 context: base_context.next_exec(),
953 harness_id: session.harness_id,
954 agent_id: session.agent_id,
955 tool_calls: reason_result.tool_calls,
956 tool_definitions: reason_result.tool_definitions,
957 locale: reason_result.locale,
958 blueprint_id: None,
959 network_access: reason_result.network_access,
960 parallel_tool_calls: reason_result.parallel_tool_calls,
963 },
964 )
965 .await?;
966 state_machine.on_act_completed();
967 }
968 TurnAction::Complete(outcome) => {
969 let ctx = state_machine.context();
970 let lifecycle =
971 RuntimeSessionLifecycle::new(self.clone(), org_id, ctx.session_id);
972 let turn_succeeded = matches!(
973 &outcome,
974 TurnOutcome::Success { .. } | TurnOutcome::MaxIterationsReached { .. }
975 );
976 match &outcome {
977 TurnOutcome::Success { iterations, .. }
978 | TurnOutcome::MaxIterationsReached { iterations, .. } => {
979 lifecycle
980 .turn_completed(
981 ctx.turn_id,
982 ctx.input_message_id,
983 *iterations as u32,
984 None,
985 None,
986 )
987 .await;
988 }
989 TurnOutcome::Failed { error, .. } => {
990 lifecycle
991 .turn_failed(ctx.turn_id, ctx.input_message_id, error, None)
992 .await;
993 }
994 TurnOutcome::Sealed {
995 reason, iterations, ..
996 } => {
997 lifecycle
998 .turn_sealed(
999 ctx.turn_id,
1000 ctx.input_message_id,
1001 reason.as_str(),
1002 *iterations as u32,
1003 None,
1004 )
1005 .await;
1006 }
1007 }
1008 lifecycle
1011 .fire_turn_end_hooks(
1012 session.harness_id,
1013 session.agent_id,
1014 ctx.turn_id,
1015 turn_succeeded,
1016 )
1017 .await;
1018 return Ok(TurnResult::from_outcome(outcome, ctx.turn_id));
1019 }
1020 }
1021 }
1022 }
1023
1024 pub async fn run_text_turn(
1025 &self,
1026 session_id: SessionId,
1027 text: impl Into<String>,
1028 ) -> Result<TurnResult> {
1029 self.run_turn(session_id, InputMessage::user(text)).await
1030 }
1031
1032 async fn drain_and_inject_wakes(&self, session_id: SessionId) -> Result<usize> {
1043 let Some(queue) = &self.session_wake_queue else {
1044 return Ok(0);
1045 };
1046 let wakes = queue.drain(session_id);
1047 if wakes.is_empty() {
1048 return Ok(0);
1049 }
1050 let count = wakes.len();
1051 for wake in wakes {
1052 let message = self
1057 .message_store
1058 .add_input_message(session_id, InputMessage::user(wake.text))
1059 .await?;
1060 self.event_bus
1061 .emit(EventRequest::new(
1062 session_id,
1063 EventContext::empty(),
1064 InputMessageData::new(message),
1065 ))
1066 .await?;
1067 }
1068 Ok(count)
1069 }
1070
1071 pub async fn messages(&self, session_id: SessionId) -> Result<Vec<Message>> {
1073 self.message_store.load(session_id).await
1074 }
1075
1076 pub async fn read_file(
1078 &self,
1079 session_id: SessionId,
1080 path: &str,
1081 ) -> Result<Option<SessionFile>> {
1082 self.file_store.read_file(session_id, path).await
1083 }
1084
1085 pub async fn load_context(&self, session_id: SessionId) -> Result<AssembledTurnContext> {
1087 let session = self
1088 .session_store
1089 .get_session(session_id)
1090 .await?
1091 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1092 self.inspect_context_with_ids(session_id, session.harness_id, session.agent_id)
1093 .await
1094 }
1095
1096 pub async fn events(&self) -> Result<Vec<Event>> {
1101 Ok(self.event_bus.collected_events().await)
1102 }
1103
1104 pub async fn execute_command(
1114 &self,
1115 session_id: SessionId,
1116 request: everruns_core::command::ExecuteCommandRequest,
1117 ) -> Result<everruns_core::command::CommandResult> {
1118 let ctx = self.load_context(session_id).await?;
1119 let registry = self.platform_definition.capability_registry();
1120 let host = everruns_core::command_host::StoreCommandHost::new(
1124 session_id,
1125 self.harness_store.clone(),
1126 self.agent_store.clone(),
1127 self.session_store.clone(),
1128 self.message_store.clone(),
1129 self.provider_store.clone(),
1130 registry.clone(),
1131 self.platform_definition.driver_registry().clone(),
1132 )
1133 .with_file_store(self.file_store.clone())
1134 .with_assembled_context(ctx.clone());
1135 let exec_ctx =
1136 everruns_core::command::CommandExecutionContext::new(session_id, Arc::new(host));
1137 for config in &ctx.resolved_capability_configs {
1138 let Some(capability) = registry.get(config.capability_id()) else {
1139 continue;
1140 };
1141 if capability.commands().iter().any(|c| c.name == request.name) {
1142 return capability.execute_command(&request, &exec_ctx).await;
1143 }
1144 }
1145 Err(AgentLoopError::config(format!(
1146 "no capability declares command /{}",
1147 request.name
1148 )))
1149 }
1150
1151 pub async fn list_commands(
1161 &self,
1162 session_id: SessionId,
1163 ) -> Result<Vec<everruns_core::command::CommandDescriptor>> {
1164 let ctx = self.load_context(session_id).await?;
1165 let registry = self.platform_definition.capability_registry();
1166 let mut seen = std::collections::HashSet::new();
1167 let mut commands = Vec::new();
1168 for config in &ctx.resolved_capability_configs {
1169 let Some(capability) = registry.get(config.capability_id()) else {
1170 continue;
1171 };
1172 for command in capability.commands() {
1173 if seen.insert(command.name.clone()) {
1174 commands.push(command);
1175 }
1176 }
1177 }
1178 Ok(commands)
1179 }
1180
1181 async fn inspect_context_with_ids(
1182 &self,
1183 session_id: SessionId,
1184 harness_id: everruns_core::HarnessId,
1185 agent_id: Option<AgentId>,
1186 ) -> Result<AssembledTurnContext> {
1187 inspect_turn_context(
1188 self.harness_store.as_ref(),
1189 self.agent_store.as_ref(),
1190 self.session_store.as_ref(),
1191 self.message_store.as_ref(),
1192 self.provider_store.as_ref(),
1193 self.platform_definition.capability_registry(),
1194 session_id,
1195 harness_id,
1196 agent_id,
1197 &[],
1198 Some(self.file_store.clone()),
1199 )
1200 .await
1201 }
1202}
1203
1204#[async_trait]
1205impl RuntimeHostAdapter for InProcessRuntime {
1206 async fn get_agent(&self, _org_id: i64, agent_id: AgentId) -> Result<Option<Agent>> {
1207 self.agent_store.get_agent(agent_id).await
1208 }
1209
1210 async fn get_harness(&self, _org_id: i64, harness_id: HarnessId) -> Result<Option<Harness>> {
1211 let chain = self.harness_store.get_harness_chain(harness_id).await?;
1212 Ok(chain.into_iter().last())
1213 }
1214
1215 async fn set_session_status(
1216 &self,
1217 _org_id: i64,
1218 session_id: SessionId,
1219 _status: SessionStatus,
1220 ) -> Result<Session> {
1221 self.session_store
1225 .get_session(session_id)
1226 .await?
1227 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))
1228 }
1229
1230 async fn load_turn_context(
1231 &self,
1232 _org_id: i64,
1233 session_id: SessionId,
1234 ) -> Result<RuntimeHostTurnContext> {
1235 let mut session = self
1236 .session_store
1237 .get_session(session_id)
1238 .await?
1239 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
1240 everruns_core::ard_attachment::apply_session_attachments(
1243 self.storage_store.as_ref(),
1244 &mut session,
1245 )
1246 .await;
1247 let agent = match session.agent_id {
1248 Some(agent_id) => self.agent_store.get_agent(agent_id).await?,
1249 None => None,
1250 };
1251 let messages = self.message_store.load(session_id).await?;
1252 let model = self.provider_store.get_default_model().await?;
1253
1254 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1257 let mcp_tool_definitions = if scoped_servers.is_empty() {
1258 vec![]
1259 } else {
1260 crate::mcp::discover_tool_definitions(
1261 &self.mcp_discovery_cache,
1262 self.mcp_client(),
1263 session_id.uuid(),
1264 &scoped_servers,
1265 )
1266 .await
1267 };
1268
1269 Ok(RuntimeHostTurnContext {
1270 agent,
1271 session,
1272 messages,
1273 model,
1274 mcp_tool_definitions,
1275 })
1276 }
1277
1278 async fn mcp_executor(
1279 &self,
1280 _org_id: i64,
1281 session_id: SessionId,
1282 ) -> Option<Arc<everruns_mcp::McpExecutor>> {
1283 let session = self.session_store.get_session(session_id).await.ok()??;
1284 let agent = match session.agent_id {
1285 Some(agent_id) => self.agent_store.get_agent(agent_id).await.ok().flatten(),
1286 None => None,
1287 };
1288 let scoped_servers = self.session_mcp_servers(&session, agent.as_ref()).await;
1289 crate::mcp::build_executor(self.mcp_client(), &scoped_servers)
1290 }
1291
1292 fn capability_registry(&self) -> CapabilityRegistry {
1293 self.platform_definition.capability_registry().clone()
1294 }
1295
1296 fn driver_registry(&self) -> DriverRegistry {
1297 self.platform_definition.driver_registry().clone()
1298 }
1299
1300 fn harness_store(&self, _org_id: i64) -> Arc<dyn HarnessStore> {
1301 self.harness_store.clone()
1302 }
1303
1304 fn agent_store(&self, _org_id: i64) -> Arc<dyn AgentStore> {
1305 self.agent_store.clone()
1306 }
1307
1308 fn session_store(&self, _org_id: i64) -> Arc<dyn SessionStore> {
1309 self.session_store.clone()
1310 }
1311
1312 fn session_mutator(&self, _org_id: i64) -> Arc<dyn SessionMutator> {
1313 self.session_store.clone()
1314 }
1315
1316 fn provider_store(&self, _org_id: i64) -> Arc<dyn ProviderStore> {
1317 self.provider_store.clone()
1318 }
1319
1320 fn message_store(&self) -> Arc<dyn MessageRetriever> {
1321 self.message_store.clone()
1322 }
1323
1324 fn compaction_checkpoint_store(
1325 &self,
1326 ) -> Option<Arc<dyn everruns_core::CompactionCheckpointStore>> {
1327 Some(self.compaction_checkpoint_store.clone())
1328 }
1329
1330 fn event_emitter(&self) -> Arc<dyn EventEmitter> {
1331 Arc::new(self.persisting_emitter.clone())
1332 }
1333
1334 fn file_store(&self) -> Arc<dyn SessionFileSystem> {
1335 self.file_store.clone()
1336 }
1337
1338 fn storage_store(&self) -> Option<Arc<dyn SessionStorageStore>> {
1339 Some(self.storage_store.clone())
1340 }
1341
1342 fn connection_resolver(&self) -> Option<Arc<dyn UserConnectionResolver>> {
1343 self.connection_resolver.clone()
1344 }
1345
1346 fn session_task_registry(
1347 &self,
1348 ) -> Option<Arc<dyn everruns_core::session_task::SessionTaskRegistry>> {
1349 self.session_task_registry.clone()
1350 }
1351
1352 fn schedule_store(
1353 &self,
1354 org_id: i64,
1355 ) -> Option<Arc<dyn everruns_core::traits::SessionScheduleStore>> {
1356 self.schedule_store_factory
1357 .as_ref()
1358 .map(|factory| factory(org_id))
1359 }
1360
1361 fn platform_store(
1362 &self,
1363 org_id: i64,
1364 session_id: SessionId,
1365 ) -> Option<Arc<dyn everruns_core::platform_store::PlatformStore>> {
1366 self.platform_store_factory
1367 .as_ref()
1368 .map(|factory| factory(org_id, session_id))
1369 }
1370
1371 fn utility_llm_service(&self) -> Option<Arc<dyn everruns_core::UtilityLlmService>> {
1372 Some(self.platform_definition.utility_llm_service())
1373 }
1374
1375 fn egress_service(&self) -> Option<Arc<dyn everruns_core::EgressService>> {
1376 Some(self.platform_definition.egress_service())
1377 }
1378}
1379
1380#[derive(Clone)]
1381struct PersistingEventEmitter {
1382 inner: Arc<dyn EventBus>,
1383 message_store: Arc<dyn RuntimeMessageStore>,
1384}
1385
1386impl PersistingEventEmitter {
1387 fn new(inner: Arc<dyn EventBus>, message_store: Arc<dyn RuntimeMessageStore>) -> Self {
1388 Self {
1389 inner,
1390 message_store,
1391 }
1392 }
1393}
1394
1395#[async_trait]
1396impl EventEmitter for PersistingEventEmitter {
1397 async fn emit(&self, request: EventRequest) -> Result<Event> {
1398 let event = self.inner.emit(request.clone()).await?;
1399 if let Some(message) = message_from_event(&event.data) {
1400 self.message_store
1401 .store_message(request.session_id, message)
1402 .await?;
1403 }
1404 Ok(event)
1405 }
1406}
1407
1408fn effective_overlay(
1409 harness_chain: &[Harness],
1410 agent: Option<&Agent>,
1411 session: &Session,
1412) -> AgentConfigOverlay {
1413 let harness_layers = harness_chain.iter().map(AgentConfigOverlay::from);
1414 let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
1415 AgentConfigOverlay::fold(
1416 harness_layers
1417 .chain(agent_layers)
1418 .chain([AgentConfigOverlay::from(session)]),
1419 )
1420}
1421
1422fn hydrate_plugin_refs(
1429 capabilities: &mut [AgentCapabilityConfig],
1430 plugin_configs: &[AgentCapabilityConfig],
1431) {
1432 for cap in capabilities.iter_mut() {
1433 let cap_id = cap.capability_id();
1434 if !everruns_core::is_plugin_capability(cap_id) {
1435 continue;
1436 }
1437 let is_bare = cap.config.is_null()
1439 || cap
1440 .config
1441 .as_object()
1442 .map(|o| o.is_empty())
1443 .unwrap_or(false);
1444 if !is_bare {
1445 continue;
1446 }
1447 if let Some(hydrated) = plugin_configs.iter().find(|c| c.capability_id() == cap_id) {
1448 cap.config = hydrated.config.clone();
1449 }
1450 }
1451}
1452
1453async fn seed_runtime_initial_files(
1454 harness_store: &dyn RuntimeHarnessStore,
1455 agent_store: &dyn RuntimeAgentStore,
1456 file_store: &dyn SessionFileSystem,
1457 session: &Session,
1458) -> Result<()> {
1459 let harness_chain = harness_store.get_harness_chain(session.harness_id).await?;
1460 if harness_chain.is_empty() {
1461 return Err(AgentLoopError::store(format!(
1462 "harness not found while seeding files: {}",
1463 session.harness_id
1464 )));
1465 }
1466 let agent = match session.agent_id {
1467 Some(agent_id) => Some(
1468 agent_store
1469 .get_agent(agent_id)
1470 .await?
1471 .ok_or_else(|| AgentLoopError::store(format!("agent not found: {agent_id}")))?,
1472 ),
1473 None => None,
1474 };
1475 let overlay = effective_overlay(&harness_chain, agent.as_ref(), session);
1476 let seed_key = SessionId::from_uuid(session.workspace_id.uuid());
1479 for file in &overlay.initial_files {
1480 file_store.seed_initial_file(seed_key, file).await?;
1481 }
1482 Ok(())
1483}
1484
1485fn message_from_event(data: &EventData) -> Option<Message> {
1486 match data {
1487 EventData::InputMessage(data) => Some(data.message.clone()),
1488 EventData::OutputMessageCompleted(OutputMessageCompletedData { message, .. }) => {
1489 Some(message.clone())
1490 }
1491 EventData::ToolCompleted(data) => Some(tool_completed_to_message(data.clone())),
1492 _ => None,
1493 }
1494}
1495
1496fn tool_completed_to_message(data: ToolCompletedData) -> Message {
1497 let mut images: Vec<ToolResultImage> = Vec::new();
1498 let metadata = tool_result_metadata(&data);
1499 let result = data.result.map(|parts| {
1500 for part in &parts {
1501 if let ContentPart::Image(img) = part
1502 && let (Some(base64), Some(media_type)) = (&img.base64, &img.media_type)
1503 {
1504 images.push(ToolResultImage {
1505 base64: base64.clone(),
1506 media_type: media_type.clone(),
1507 });
1508 }
1509 }
1510
1511 let text_parts: Vec<&ContentPart> = parts
1512 .iter()
1513 .filter(|part| matches!(part, ContentPart::Text(_)))
1514 .collect();
1515 if text_parts.len() == 1
1516 && let ContentPart::Text(text) = text_parts[0]
1517 {
1518 return parse_structured_tool_result_text(&text.text);
1519 }
1520 if !text_parts.is_empty() {
1521 serde_json::to_value(&text_parts).unwrap_or_default()
1522 } else {
1523 serde_json::Value::Null
1524 }
1525 });
1526
1527 let mut message = if images.is_empty() {
1528 Message::tool_result(&data.tool_call_id, result, data.error)
1529 } else {
1530 Message::tool_result_with_images(&data.tool_call_id, result, images)
1531 };
1532 message.metadata = metadata;
1533 message
1534}
1535
1536fn tool_result_metadata(
1537 data: &ToolCompletedData,
1538) -> Option<std::collections::HashMap<String, serde_json::Value>> {
1539 let mut metadata = std::collections::HashMap::new();
1540 metadata.insert("tool_name".to_string(), serde_json::json!(data.tool_name));
1541 if let Some(fingerprint) = &data.tool_call_fingerprint {
1542 metadata.insert(
1543 "tool_call_fingerprint".to_string(),
1544 serde_json::json!(fingerprint),
1545 );
1546 }
1547 if let Some(fingerprint) = &data.tool_result_fingerprint {
1548 metadata.insert(
1549 "tool_result_fingerprint".to_string(),
1550 serde_json::json!(fingerprint),
1551 );
1552 }
1553 (!metadata.is_empty()).then_some(metadata)
1554}
1555
1556fn parse_structured_tool_result_text(text: &str) -> serde_json::Value {
1557 let trimmed = text.trim_start();
1558 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
1559 return serde_json::Value::String(text.to_string());
1560 }
1561
1562 match serde_json::from_str(text) {
1563 Ok(value @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => value,
1564 _ => serde_json::Value::String(text.to_string()),
1565 }
1566}
1567
1568#[cfg(test)]
1569mod tool_completed_replay_tests {
1570 use super::*;
1571
1572 #[test]
1573 fn tool_completed_replay_preserves_json_object_shape() {
1574 let data = ToolCompletedData::success(
1575 "call_read".to_string(),
1576 "read_file".to_string(),
1577 vec![ContentPart::text(
1578 serde_json::json!({
1579 "path": "/workspace/src/lib.rs",
1580 "content": "1|fn main() {}"
1581 })
1582 .to_string(),
1583 )],
1584 Some(1),
1585 );
1586
1587 let message = tool_completed_to_message(data);
1588 let result = message
1589 .tool_result_content()
1590 .and_then(|content| content.result.as_ref())
1591 .expect("tool result should be present");
1592
1593 assert_eq!(result["path"], "/workspace/src/lib.rs");
1594 assert_eq!(result["content"], "1|fn main() {}");
1595 }
1596
1597 #[test]
1598 fn tool_completed_replay_keeps_scalar_json_as_text() {
1599 let data = ToolCompletedData::success(
1600 "call_scalar".to_string(),
1601 "custom_tool".to_string(),
1602 vec![ContentPart::text("123")],
1603 Some(1),
1604 );
1605
1606 let message = tool_completed_to_message(data);
1607 let result = message
1608 .tool_result_content()
1609 .and_then(|content| content.result.as_ref())
1610 .expect("tool result should be present");
1611
1612 assert_eq!(result, &serde_json::Value::String("123".to_string()));
1613 }
1614
1615 #[test]
1616 fn tool_completed_replay_preserves_fingerprints_as_metadata() {
1617 let data = ToolCompletedData::success(
1618 "call_read".to_string(),
1619 "read_file".to_string(),
1620 vec![ContentPart::text("{}")],
1621 Some(1),
1622 )
1623 .with_fingerprints("sha256:call".to_string(), "sha256:result".to_string());
1624
1625 let message = tool_completed_to_message(data);
1626 let metadata = message.metadata.expect("metadata should be present");
1627
1628 assert_eq!(metadata["tool_name"], "read_file");
1629 assert_eq!(metadata["tool_call_fingerprint"], "sha256:call");
1630 assert_eq!(metadata["tool_result_fingerprint"], "sha256:result");
1631 }
1632}
1633
1634#[cfg(test)]
1635mod org_id_mapping_tests {
1636 use super::*;
1637 use everruns_core::{DEFAULT_ORG_ID, DEFAULT_ORG_PUBLIC_ID, org_public_id_from_internal};
1638
1639 #[test]
1640 fn default_public_id_maps_to_default_org() {
1641 assert_eq!(
1642 in_process_internal_org_id(DEFAULT_ORG_PUBLIC_ID),
1643 DEFAULT_ORG_ID
1644 );
1645 }
1646
1647 #[test]
1648 fn invalid_public_id_does_not_fall_back_to_default() {
1649 for invalid in [
1650 "",
1651 "not-an-org",
1652 "org_short",
1653 "org_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
1654 "ORG_00000000000000000000000000000001",
1655 ] {
1656 let mapped = in_process_internal_org_id(invalid);
1657 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1658 assert!(
1659 mapped >= 2,
1660 "invalid input {invalid:?} should not map to default"
1661 );
1662 }
1663 }
1664
1665 #[test]
1666 fn zero_public_id_does_not_fall_back_to_default() {
1667 let mapped = in_process_internal_org_id("org_00000000000000000000000000000000");
1670 assert_ne!(mapped, everruns_core::DEFAULT_ORG_ID);
1671 assert!(mapped >= 2, "all-zero id should not map to default");
1672 }
1673
1674 #[test]
1675 fn synthetic_public_id_round_trips_with_internal_helper() {
1676 for internal in [1_i64, 2, 42, 1_000_000, i64::MAX - 1, i64::MAX] {
1677 let public = org_public_id_from_internal(internal);
1678 assert_eq!(
1679 in_process_internal_org_id(&public),
1680 internal,
1681 "round-trip failed for internal={internal}"
1682 );
1683 }
1684 }
1685
1686 #[test]
1687 fn distinct_synthetic_ids_map_to_distinct_internal_ids() {
1688 let a = org_public_id_from_internal(7);
1689 let b = org_public_id_from_internal(8);
1690 assert_ne!(a, b);
1691 assert_ne!(
1692 in_process_internal_org_id(&a),
1693 in_process_internal_org_id(&b)
1694 );
1695 }
1696
1697 #[test]
1698 fn high_entropy_uuid_style_id_hashes_into_reserved_range() {
1699 let high = "org_80000000000000000000000000000000";
1703 let mapped = in_process_internal_org_id(high);
1704 assert!(mapped >= 2, "mapped id {mapped} must be >= 2");
1705 assert_ne!(mapped, DEFAULT_ORG_ID);
1706
1707 assert_eq!(mapped, in_process_internal_org_id(high));
1709 }
1710
1711 #[test]
1712 fn high_entropy_ids_are_isolated_from_each_other() {
1713 let a = in_process_internal_org_id("org_80000000000000000000000000000001");
1714 let b = in_process_internal_org_id("org_80000000000000000000000000000002");
1715 assert_ne!(a, b);
1716 assert_ne!(a, DEFAULT_ORG_ID);
1717 assert_ne!(b, DEFAULT_ORG_ID);
1718 }
1719
1720 #[test]
1721 fn hash_uses_stable_sha256_truncation() {
1722 let mapped = in_process_internal_org_id("org_80000000000000000000000000000000");
1726 let expected = {
1727 let digest = sha2::Sha256::digest(b"org_80000000000000000000000000000000");
1728 let mut buf = [0u8; 8];
1729 buf.copy_from_slice(&digest[..8]);
1730 let raw = u64::from_be_bytes(buf);
1731 ((raw % ((i64::MAX - 1) as u64)) as i64) + 2
1732 };
1733 assert_eq!(mapped, expected);
1734 }
1735
1736 #[test]
1737 fn oversize_input_is_bounded_and_does_not_collide_silently() {
1738 let oversize = "x".repeat(super::HASH_INPUT_CAP_BYTES * 4);
1744 let mapped = in_process_internal_org_id(&oversize);
1745 assert!(mapped >= 2);
1746 assert_ne!(mapped, DEFAULT_ORG_ID);
1747 }
1748}