1use super::*;
4
5pub fn start_mobkit_runtime(
6 config: MobKitConfig,
7 agent_events: Vec<EventEnvelope<UnifiedEvent>>,
8 timeout: Duration,
9) -> Result<MobkitRuntimeHandle, MobkitRuntimeError> {
10 start_mobkit_runtime_with_options(config, agent_events, timeout, RuntimeOptions::default())
11}
12
13pub fn start_mobkit_runtime_with_options(
14 config: MobKitConfig,
15 agent_events: Vec<EventEnvelope<UnifiedEvent>>,
16 timeout: Duration,
17 options: RuntimeOptions,
18) -> Result<MobkitRuntimeHandle, MobkitRuntimeError> {
19 let delivery_runtime_epoch_ms = current_time_ms();
20 let mut lifecycle_events = Vec::new();
21 let mut seq = 0_u64;
22 lifecycle_events.push(LifecycleEvent {
23 seq,
24 stage: LifecycleStage::MobStarted,
25 });
26 seq += 1;
27
28 let mut supervisor_transitions = Vec::new();
29 let mut module_events = Vec::new();
30 let mut loaded_modules = BTreeSet::new();
31 let mut live_children = BTreeMap::new();
32
33 for module_id in &config.discovery.modules {
34 let module = config
35 .modules
36 .iter()
37 .find(|module| &module.id == module_id)
38 .ok_or_else(|| {
39 MobkitRuntimeError::Config(ConfigResolutionError::ModuleNotConfigured(
40 module_id.clone(),
41 ))
42 })?;
43
44 let pre_spawn = config
45 .pre_spawn
46 .iter()
47 .find(|data| data.module_id == *module_id);
48
49 let mut start_result = supervise_module_start(module, pre_spawn, timeout, &options);
50 supervisor_transitions.append(&mut start_result.transitions);
51 if let Some(error) = start_result.terminal_error.as_ref() {
52 let timestamp_ms = current_time_ms();
53 module_events.push(EventEnvelope {
54 event_id: format!("evt-supervisor-warning-{}-{timestamp_ms}", module.id),
55 source: "module".to_string(),
56 timestamp_ms,
57 event: UnifiedEvent::Module(ModuleEvent {
58 module: module.id.clone(),
59 event_type: "supervisor.warning".to_string(),
60 payload: serde_json::json!({
61 "error": format!("{error:?}")
62 }),
63 }),
64 });
65 }
66 if let Some(event) = start_result.event {
67 loaded_modules.insert(module_id.clone());
68 if let Some(child) = start_result.child {
69 live_children.insert(module_id.clone(), child);
70 }
71 module_events.push(event);
72 }
73 }
74
75 lifecycle_events.push(LifecycleEvent {
76 seq,
77 stage: LifecycleStage::ModulesStarted,
78 });
79 seq += 1;
80
81 let merged_events = merge_unified_events(module_events, agent_events);
82 lifecycle_events.push(LifecycleEvent {
83 seq,
84 stage: LifecycleStage::MergedStreamStarted,
85 });
86
87 let memory_backend = match options.memory_backend.as_ref() {
88 Some(MemoryBackendConfig::LocalJson(config)) => Some(
89 LocalJsonMemoryStoreAdapter::from_config(config)
90 .map_err(MobkitRuntimeError::MemoryBackend)?,
91 ),
92 Some(MemoryBackendConfig::Elephant(legacy)) => {
93 tracing::warn!(
94 "memory backend kind 'elephant' is deprecated: it only health-checks the \
95 endpoint and persists the ledger as local JSON; use kind 'local_json' with \
96 an optional health_check_endpoint"
97 );
98 Some(
99 LocalJsonMemoryStoreAdapter::from_config(&LocalJsonMemoryBackendConfig::from(
100 legacy.clone(),
101 ))
102 .map_err(MobkitRuntimeError::MemoryBackend)?,
103 )
104 }
105 None => None,
106 };
107 let persisted_memory = match memory_backend.as_ref() {
108 Some(backend) => backend
109 .read_state()
110 .map_err(MobkitRuntimeError::MemoryBackend)?,
111 None => PersistedMemoryState::default(),
112 };
113 let mut memory_assertions = persisted_memory
114 .assertions
115 .into_iter()
116 .filter_map(|assertion| {
117 let entity = MobkitRuntimeHandle::canonical_memory_token(&assertion.entity)?;
118 let topic = MobkitRuntimeHandle::canonical_memory_token(&assertion.topic)?;
119 let store = MobkitRuntimeHandle::canonical_memory_store(&assertion.store)?;
120 let fact = assertion.fact.trim();
121 if fact.is_empty() {
122 return None;
123 }
124 Some(MemoryAssertion {
125 assertion_id: assertion.assertion_id,
126 entity,
127 topic,
128 store,
129 fact: fact.to_string(),
130 metadata: assertion.metadata,
131 indexed_at_ms: assertion.indexed_at_ms,
132 })
133 })
134 .collect::<Vec<_>>();
135 while memory_assertions.len() > MEMORY_ASSERTIONS_MAX_RETAINED {
136 memory_assertions.remove(0);
137 }
138 let mut memory_conflicts = BTreeMap::new();
139 for signal in persisted_memory.conflicts {
140 let Some(entity) = MobkitRuntimeHandle::canonical_memory_token(&signal.entity) else {
141 continue;
142 };
143 let Some(topic) = MobkitRuntimeHandle::canonical_memory_token(&signal.topic) else {
144 continue;
145 };
146 let Some(store) = MobkitRuntimeHandle::canonical_memory_store(&signal.store) else {
147 continue;
148 };
149 let normalized_signal = MemoryConflictSignal {
150 entity: entity.clone(),
151 topic: topic.clone(),
152 store: store.clone(),
153 reason: signal
154 .reason
155 .as_deref()
156 .map(str::trim)
157 .filter(|value| !value.is_empty())
158 .map(ToString::to_string),
159 updated_at_ms: signal.updated_at_ms,
160 };
161 memory_conflicts.insert(
162 MemoryConflictKey {
163 entity,
164 topic,
165 store,
166 },
167 normalized_signal,
168 );
169 }
170 let memory_sequence = memory_assertions
171 .iter()
172 .filter_map(|assertion| parse_memory_assertion_sequence(&assertion.assertion_id))
173 .max()
174 .map(|last_sequence| last_sequence.saturating_add(1))
175 .unwrap_or(memory_assertions.len() as u64);
176
177 Ok(MobkitRuntimeHandle {
178 config,
179 runtime_options: options,
180 loaded_modules,
181 live_children,
182 lifecycle_events,
183 supervisor_report: SupervisorReport {
184 transitions: supervisor_transitions,
185 },
186 merged_events,
187 scheduling_claims: BTreeSet::new(),
188 scheduling_claim_ticks: BTreeMap::new(),
189 scheduling_last_due_ticks: BTreeMap::new(),
190 scheduling_dispatch_sequence: 0,
191 routing_sequence: 0,
192 routing_resolutions: BTreeMap::new(),
193 routing_resolution_order: Vec::new(),
194 runtime_routes: BTreeMap::new(),
195 delivery_sequence: 0,
196 delivery_runtime_epoch_ms,
197 delivery_now_floor_ms: 0,
198 delivery_clock_ms: 0,
199 delivery_history: Vec::new(),
200 delivery_idempotency: BTreeMap::new(),
201 delivery_idempotency_by_delivery: BTreeMap::new(),
202 delivery_rate_window_counts: BTreeMap::new(),
203 gating_sequence: 0,
204 gating_pending: BTreeMap::new(),
205 gating_pending_order: Vec::new(),
206 gating_audit: Vec::new(),
207 gating_resolution_observers: GatingResolutionObservers::default(),
208 memory_sequence,
209 memory_assertions,
210 memory_conflicts,
211 memory_backend,
212 running: true,
213 })
214}
215
216fn parse_memory_assertion_sequence(assertion_id: &str) -> Option<u64> {
217 assertion_id
218 .strip_prefix("memory-assert-")
219 .and_then(|suffix| suffix.parse::<u64>().ok())
220}