1use super::super::bootstrap::RuntimeBootstrap;
4use super::super::channel_registry::{ChannelBinding, LiveChannel};
5use super::super::error::StartupError;
6use super::super::host_tools::HostToolRegistry;
7use super::super::mcp::McpGatewayHandle;
8use super::super::state::RuntimeState;
9use super::admission::admit;
10use super::capacity::{ReservationPool, ReservationPoolError};
11use super::coordinator::WorkerMessage;
12use super::ledger::LifecycleLedger;
13use super::mcp_listener::DEFAULT_MCP_MAX_ROUTES;
14use super::shutdown::ShutdownTicket;
15use super::supervisor::{
16 run_supervisor, wait_until_drain_complete, ControlCommand, RuntimeShared, STATE_ACCEPTING,
17 STATE_QUIESCING, STATE_STARTING, STATE_STOPPED,
18};
19use crate::transaction::mcp::McpGateway;
20use monoloop_contracts::{
21 AdmissionError, AdmissionReceipt, ChannelId, ChannelKind, ShutdownWaitOutcome,
22 TerminationDisposition, TerminationMode, TransactionSelector, TransactionSubmitRequest,
23};
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
26use std::sync::{Arc, Mutex};
27use std::thread::JoinHandle as OsJoinHandle;
28use std::time::Duration;
29use tokio::sync::{mpsc, oneshot, Notify};
30
31#[must_use = "RuntimeOwner must begin_shutdown and wait_stopped until Stopped"]
33pub struct RuntimeOwner {
34 shared: Arc<RuntimeShared>,
35 thread: Option<OsJoinHandle<()>>,
37 thread_exited: Option<oneshot::Receiver<()>>,
39 pool: Arc<ReservationPool>,
40 channels: Arc<HashMap<ChannelId, LiveChannel>>,
42}
43
44#[derive(Clone)]
46pub struct TransactionRuntimeHandle {
47 shared: Arc<RuntimeShared>,
48 pool: Arc<ReservationPool>,
49 max_tools: usize,
50 channels: Arc<HashMap<ChannelId, LiveChannel>>,
52 tools: HostToolRegistry,
53}
54
55pub struct StartedRuntime {
57 pub owner: RuntimeOwner,
59 pub handle: TransactionRuntimeHandle,
61}
62
63impl StartedRuntime {
64 pub fn start(bootstrap: RuntimeBootstrap) -> Result<Self, StartupError> {
66 bootstrap.config.validate()?;
67
68 let max_schema = bootstrap
72 .config
73 .transaction_limits
74 .max_tool_schema_bytes
75 .max(1);
76 for spec in bootstrap.tools.specs_sorted() {
77 let schema_bytes = serde_json::to_vec(spec.input_schema.as_value())
78 .map(|b| b.len())
79 .unwrap_or(usize::MAX);
80 if schema_bytes > max_schema {
81 return Err(StartupError::InvalidConfig(
82 "tool schema exceeds max_tool_schema_bytes",
83 ));
84 }
85 }
86
87 let mut live: HashMap<ChannelId, LiveChannel> = HashMap::new();
88 let mut capacity_pairs: Vec<(ChannelId, usize)> = Vec::new();
89 for (id, binding) in bootstrap.channels.iter() {
90 binding.descriptor().validate()?;
91 let instance = binding
92 .connector_factory
93 .create()
94 .map_err(StartupError::from)?;
95 match binding.kind {
96 ChannelKind::ExternalAgent if instance.sessions.is_none() => {
97 return Err(StartupError::SessionAdapterMismatch(
98 "ExternalAgent requires SessionAdapter",
99 ));
100 }
101 ChannelKind::DirectLlm if instance.sessions.is_some() => {
102 return Err(StartupError::SessionAdapterMismatch(
103 "DirectLlm must not carry SessionAdapter",
104 ));
105 }
106 _ => {}
107 }
108 let channel_max = binding
109 .limits
110 .max_active_transactions
111 .min(bootstrap.config.transaction_limits.max_active_per_channel);
112 if channel_max == 0 {
113 return Err(StartupError::InvalidConfig(
114 "channel max_active_transactions must be nonzero",
115 ));
116 }
117 capacity_pairs.push((id.clone(), channel_max));
118 live.insert(
119 id.clone(),
120 LiveChannel {
121 binding: clone_binding(binding),
122 instance,
123 },
124 );
125 }
126
127 let max_active = bootstrap.config.transaction_limits.max_active_transactions;
128 if max_active == 0 {
129 return Err(StartupError::InvalidConfig(
130 "max_active_transactions must be nonzero",
131 ));
132 }
133 let pool = ReservationPool::try_new(max_active, capacity_pairs).map_err(|e| match e {
134 ReservationPoolError::ZeroGlobal => {
135 StartupError::InvalidConfig("max_active_transactions must be nonzero")
136 }
137 ReservationPoolError::ZeroChannel => {
138 StartupError::InvalidConfig("channel capacity must be nonzero")
139 }
140 })?;
141
142 let start_capacity = bootstrap.config.start_queue_capacity.unwrap_or(max_active);
147 let (start_tx, start_rx) = mpsc::channel(start_capacity);
148 let control_capacity = bootstrap
149 .config
150 .transaction_limits
151 .max_actor_commands
152 .max(1);
153 let (control_tx, control_rx) = mpsc::channel(control_capacity);
154 let worker_capacity = max_active.saturating_add(8);
155 let (worker_tx, worker_rx) = mpsc::channel::<WorkerMessage>(worker_capacity);
156 let spawn_capacity = max_active.saturating_mul(8).max(32);
157 let (task_spawner, spawn_rx) =
158 super::task_spawner::TransactionTaskSpawner::channel(spawn_capacity);
159 let channels = Arc::new(live);
160 let shared = Arc::new(RuntimeShared {
161 state: AtomicU8::new(STATE_STARTING),
162 ledger: Mutex::new(LifecycleLedger::new()),
163 start_tx,
164 control_tx,
165 worker_tx,
166 wake: Notify::new(),
167 channels: Arc::clone(&channels),
168 default_deadline: bootstrap.config.transaction_limits.transaction_deadline,
169 cleanup_deadline: bootstrap.config.transaction_limits.cleanup_deadline,
170 terminal_event_delivery_deadline: bootstrap
171 .config
172 .transaction_limits
173 .terminal_event_delivery_deadline,
174 task_spawner,
175 shutdown_generation: AtomicU64::new(0),
176 shutdown_report: Mutex::new(None),
177 completions_published: AtomicU64::new(0),
178 completions_receiver_dropped: AtomicU64::new(0),
179 completions_invariant_failed: AtomicU64::new(0),
180 runtime_shutdown_terminals: AtomicU64::new(0),
181 owned_tasks: AtomicU32::new(0),
182 live_connector_owners: AtomicU32::new(0),
183 enable_mcp_listener: bootstrap.config.enable_mcp_listener,
184 mcp_listen_addr: Mutex::new(None),
185 mcp_gateway: Mutex::new(None),
186 mcp_cancel: Mutex::new(None),
187 block_stopped: bootstrap.config.block_stopped.clone(),
188 hold_start: bootstrap.config.hold_start.clone(),
189 hold_control: bootstrap.config.hold_control.clone(),
190 hold_finalizer_after_seal: bootstrap.config.hold_finalizer_after_seal.clone(),
191 hold_executor_teardown: bootstrap.config.hold_executor_teardown.clone(),
192 inject_non_yielding_service: bootstrap.config.inject_non_yielding_service,
193 inject_join_only_spill: bootstrap.config.inject_join_only_spill.clone(),
194 drain_complete: std::sync::atomic::AtomicBool::new(false),
195 tools_registry: bootstrap.tools.clone(),
196 shared_tool_capacity: crate::transaction::tool_capacity::SharedToolCapacity::new(
197 bootstrap
198 .config
199 .transaction_limits
200 .max_active_transactions
201 .saturating_mul(4)
202 .max(8),
203 ),
204 tool_spill: Arc::new(crate::transaction::dispatcher::OrphanToolPermitSet::new()),
205 owned_processes: Arc::new(AtomicU32::new(0)),
206 process_registry: Arc::new(
207 crate::transaction::owned_process_registry::OwnedProcessRegistry::new(),
208 ),
209 transaction_limits: bootstrap.config.transaction_limits.clone(),
210 });
211
212 let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), StartupError>>();
213 let (exited_tx, exited_rx) = oneshot::channel::<()>();
214 let teardown_gate = bootstrap.config.hold_executor_teardown.clone();
215 let shared_thread = Arc::clone(&shared);
216 let mcp_max_routes = bootstrap
217 .config
218 .transaction_limits
219 .max_active_transactions
220 .saturating_mul(2)
221 .max(DEFAULT_MCP_MAX_ROUTES);
222 let thread = std::thread::Builder::new()
223 .name("monoloop-runtime".into())
224 .spawn(move || {
225 let rt = match tokio::runtime::Builder::new_multi_thread()
228 .worker_threads(2)
229 .max_blocking_threads(2)
230 .enable_all()
231 .thread_name("monoloop-worker")
232 .build()
233 {
234 Ok(rt) => rt,
235 Err(_) => {
236 let _ = ready_tx.send(Err(StartupError::ExecutorUnavailable));
237 return;
238 }
239 };
240 rt.block_on(async {
245 let mcp_prepared = if shared_thread.enable_mcp_listener {
246 match tokio::net::TcpListener::bind("127.0.0.1:0").await {
247 Ok(listener) => {
248 let request_owner: Option<
249 std::sync::Arc<dyn crate::transaction::mcp::McpRequestOwner>,
250 > = Some(std::sync::Arc::new(
251 super::mcp_request_owner::SupervisedMcpRequestOwner::new(
252 shared_thread.task_spawner.clone(),
253 ),
254 ));
255 match McpGateway::prepare_from_tokio_listener(
256 listener,
257 mcp_max_routes,
258 request_owner,
259 ) {
260 Ok(prepared) => {
261 super::mcp_listener::publish_runtime_mcp(
262 &shared_thread,
263 &prepared,
264 );
265 Some(prepared)
266 }
267 Err(_) => {
268 let _ = ready_tx.send(Err(StartupError::InvalidConfig(
269 "MCP gateway prepare failed",
270 )));
271 return;
272 }
273 }
274 }
275 Err(_) => {
276 let _ = ready_tx.send(Err(StartupError::InvalidConfig(
277 "MCP loopback bind failed",
278 )));
279 return;
280 }
281 }
282 } else {
283 None
284 };
285 shared_thread.state.store(STATE_ACCEPTING, Ordering::SeqCst);
286 let _ = ready_tx.send(Ok(()));
287 run_supervisor(
288 shared_thread,
289 start_rx,
290 control_rx,
291 worker_rx,
292 spawn_rx,
293 mcp_prepared,
294 )
295 .await;
296 });
297 if let Some(gate) = teardown_gate {
299 let rt_gate = tokio::runtime::Builder::new_current_thread()
300 .enable_all()
301 .build();
302 if let Ok(rt_gate) = rt_gate {
303 rt_gate.block_on(gate.wait_released());
304 }
305 }
306 rt.shutdown_timeout(Duration::from_secs(2));
309 let _ = exited_tx.send(());
310 })
311 .map_err(|_| StartupError::ExecutorUnavailable)?;
312
313 ready_rx
314 .recv_timeout(Duration::from_secs(5))
315 .map_err(|_| StartupError::ExecutorUnavailable)??;
316
317 let tools = bootstrap.tools;
318 let max_tools = bootstrap
319 .config
320 .transaction_limits
321 .max_tools_per_transaction;
322 let handle = TransactionRuntimeHandle {
323 shared: Arc::clone(&shared),
324 pool: Arc::clone(&pool),
325 max_tools,
326 channels: Arc::clone(&channels),
327 tools,
328 };
329 let owner = RuntimeOwner {
330 shared,
331 thread: Some(thread),
332 thread_exited: Some(exited_rx),
333 pool,
334 channels,
335 };
336 Ok(StartedRuntime { owner, handle })
337 }
338}
339
340impl RuntimeOwner {
341 pub fn state(&self) -> RuntimeState {
343 self.shared.runtime_state()
344 }
345
346 pub fn ledger_len(&self) -> usize {
348 self.shared.ledger.lock().map(|l| l.len()).unwrap_or(0)
349 }
350
351 pub fn owned_task_count(&self) -> u32 {
353 self.shared.owned_tasks.load(Ordering::SeqCst)
354 }
355
356 pub fn live_connector_owners(&self) -> u32 {
358 self.shared.live_connector_owners.load(Ordering::SeqCst)
359 }
360
361 pub fn tool_spill_pending(&self) -> usize {
363 self.shared.tool_spill.pending_count()
364 }
365
366 pub fn global_reservations(&self) -> usize {
368 self.pool.global_active()
369 }
370
371 pub fn channel_reservations(&self, channel: &ChannelId) -> usize {
373 self.pool.channel_active(channel)
374 }
375
376 pub fn channel_count(&self) -> usize {
378 self.channels.len()
379 }
380
381 pub fn mcp_local_addr(&self) -> Option<std::net::SocketAddr> {
383 self.shared.mcp_listen_addr.lock().ok().and_then(|g| *g)
384 }
385
386 pub fn mcp_gateway(&self) -> Option<McpGatewayHandle> {
388 self.shared.mcp_gateway.lock().ok().and_then(|g| g.clone())
389 }
390
391 pub fn begin_shutdown(&self) -> ShutdownTicket {
397 {
401 let _ledger = self.shared.ledger.lock().unwrap_or_else(|e| e.into_inner());
402 let _ = self.shared.state.compare_exchange(
403 STATE_ACCEPTING,
404 STATE_QUIESCING,
405 Ordering::SeqCst,
406 Ordering::SeqCst,
407 );
408 if self.shared.state.load(Ordering::SeqCst) == STATE_STARTING {
409 self.shared.state.store(STATE_QUIESCING, Ordering::SeqCst);
410 }
411 }
412 let generation = match self.shared.shutdown_generation.compare_exchange(
415 0,
416 1,
417 Ordering::SeqCst,
418 Ordering::SeqCst,
419 ) {
420 Ok(_) => 1,
421 Err(existing) => existing,
422 };
423 let _ = self
424 .shared
425 .control_tx
426 .try_send(ControlCommand::BeginShutdown);
427 self.shared.wake.notify_waiters();
428 ShutdownTicket { generation }
429 }
430
431 pub async fn wait_stopped(&mut self, deadline: Duration) -> ShutdownWaitOutcome {
436 if self.shared.state.load(Ordering::SeqCst) == STATE_ACCEPTING {
437 let _ = self.begin_shutdown();
438 }
439 if self.shared.state.load(Ordering::SeqCst) == STATE_STOPPED && self.thread.is_none() {
440 let report = self
441 .shared
442 .shutdown_report
443 .lock()
444 .unwrap_or_else(|e| e.into_inner())
445 .clone()
446 .unwrap_or_else(|| self.shared.final_report());
447 return ShutdownWaitOutcome::Stopped(report);
448 }
449
450 let start = tokio::time::Instant::now();
451 if let Err(timed_out) = wait_until_drain_complete(&self.shared, deadline).await {
453 return timed_out;
454 }
455
456 let _ = self
457 .shared
458 .control_tx
459 .try_send(ControlCommand::StopSupervisor);
460 self.shared.wake.notify_one();
461
462 let remaining = deadline.saturating_sub(start.elapsed());
464 if let Some(rx) = self.thread_exited.as_mut() {
465 match tokio::time::timeout(remaining, &mut *rx).await {
466 Ok(Ok(())) | Ok(Err(_)) => {
467 self.thread_exited = None;
468 }
469 Err(_) => {
470 return ShutdownWaitOutcome::TimedOut(self.shared.snapshot());
472 }
473 }
474 }
475 if let Some(thread) = self.thread.take() {
476 let _ = thread.join();
478 }
479 self.shared.state.store(STATE_STOPPED, Ordering::SeqCst);
480 let report = self
481 .shared
482 .shutdown_report
483 .lock()
484 .unwrap_or_else(|e| e.into_inner())
485 .clone()
486 .unwrap_or_else(|| self.shared.final_report());
487 ShutdownWaitOutcome::Stopped(report)
488 }
489}
490
491impl Drop for RuntimeOwner {
492 fn drop(&mut self) {
493 if let Some(gate) = self.shared.block_stopped.as_ref() {
495 gate.release();
496 }
497 if let Some(gate) = self.shared.hold_start.as_ref() {
498 gate.release();
499 }
500 if let Some(gate) = self.shared.hold_control.as_ref() {
501 gate.release();
502 }
503 if let Some(gate) = self.shared.hold_finalizer_after_seal.as_ref() {
504 gate.release();
505 }
506 if let Some(gate) = self.shared.hold_executor_teardown.as_ref() {
507 gate.release();
508 }
509 if let Some(inject) = self.shared.inject_join_only_spill.as_ref() {
510 inject.release();
511 }
512 if self.shared.state.load(Ordering::SeqCst) != STATE_STOPPED {
513 let _ = self.begin_shutdown();
514 let _ = self
515 .shared
516 .control_tx
517 .try_send(ControlCommand::StopSupervisor);
518 self.shared.wake.notify_one();
519 }
520 if let Some(thread) = self.thread.take() {
526 match thread.join() {
529 Ok(()) => {
530 self.shared.state.store(STATE_STOPPED, Ordering::SeqCst);
531 }
532 Err(_) => {
533 self.shared.state.store(STATE_STOPPED, Ordering::SeqCst);
534 }
535 }
536 }
537 self.thread_exited = None;
538 }
539}
540
541impl TransactionRuntimeHandle {
542 pub fn state(&self) -> RuntimeState {
544 self.shared.runtime_state()
545 }
546
547 pub fn mcp_local_addr(&self) -> Option<std::net::SocketAddr> {
549 self.shared.mcp_listen_addr.lock().ok().and_then(|g| *g)
550 }
551
552 pub fn mcp_gateway(&self) -> Option<McpGatewayHandle> {
554 self.shared.mcp_gateway.lock().ok().and_then(|g| g.clone())
555 }
556
557 pub fn submit(
559 &self,
560 request: TransactionSubmitRequest,
561 ) -> Result<AdmissionReceipt, AdmissionError> {
562 admit(
563 &self.shared,
564 &self.pool,
565 self.channels.as_ref(),
566 &self.tools,
567 self.max_tools,
568 request,
569 )
570 }
571
572 pub fn terminate(
574 &self,
575 selector: TransactionSelector,
576 mode: TerminationMode,
577 ) -> TerminationDisposition {
578 let tx = match selector {
579 TransactionSelector::Transaction(id) => id,
580 TransactionSelector::Session(key) => {
581 let ledger = self.shared.ledger.lock().unwrap_or_else(|e| e.into_inner());
582 match ledger.transaction_for_session(&key) {
583 Some(id) => id,
584 None => return TerminationDisposition::NotFound,
585 }
586 }
587 };
588 {
591 let ledger = self.shared.ledger.lock().unwrap_or_else(|e| e.into_inner());
592 match ledger.get(&tx) {
593 None => return TerminationDisposition::NotFound,
594 Some(entry) => {
595 if let Some(term) = entry.terminal.as_ref() {
596 let upgrade = matches!(mode, TerminationMode::ForceTerminate { .. })
597 && term.kind == monoloop_contracts::TransactionEndKind::Cancelled;
598 if !upgrade {
599 return TerminationDisposition::AlreadyTerminal;
600 }
601 }
602 }
603 }
604 }
605 let cmd = match mode {
606 TerminationMode::Cancel { .. } => ControlCommand::Cancel(tx),
607 TerminationMode::ForceTerminate { .. } => ControlCommand::ForceTerminate(tx),
608 };
609 match self.shared.control_tx.try_send(cmd) {
610 Ok(()) => {
611 self.shared.wake.notify_one();
612 TerminationDisposition::Accepted
613 }
614 Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
615 TerminationDisposition::ControlCapacityExceeded
616 }
617 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
618 TerminationDisposition::RuntimeClosed
619 }
620 }
621 }
622}
623
624fn clone_binding(binding: &ChannelBinding) -> ChannelBinding {
625 ChannelBinding {
626 id: binding.id.clone(),
627 kind: binding.kind,
628 tool_mode: binding.tool_mode,
629 connector_factory: Arc::clone(&binding.connector_factory),
630 encoder: Arc::clone(&binding.encoder),
631 interpreter: Arc::clone(&binding.interpreter),
632 endpoint_ref: binding.endpoint_ref.clone(),
633 credential_ref: binding.credential_ref.clone(),
634 defaults: binding.defaults.clone(),
635 capabilities: binding.capabilities.clone(),
636 limits: binding.limits.clone(),
637 }
638}