1use crate::registry::{EmptyToolRegistry, ResolveToolRequest, ToolRegistry, ToolResolution};
4use crate::subscription::{CanonicalEventSubscription, SubscriptionStatus};
5use crate::tools::{NoToolRuntime, ToolRuntime};
6use monoloop_contracts::{
7 CanonicalUnit, CanonicalUnitEvent, InterpretationEnd, InterpreterOutputEvent, LoopEnd,
8 LoopEndKind, LoopError, LoopId, LoopLimits, LoopOutputEvent, LoopScope, MonoloopRunId,
9 OutboundToolOutcome, OutboundToolResult, ToolActionId, ToolExecutionId, ToolRequestState,
10 UnitId,
11};
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
14use std::sync::Arc;
15use tokio::sync::{mpsc, oneshot, Mutex};
16
17pub struct StartLoop {
19 pub monoloop_run_id: MonoloopRunId,
21 pub loop_id: LoopId,
23 pub scope: LoopScope,
25 pub subscription: CanonicalEventSubscription,
27 pub tool_registry: Arc<dyn ToolRegistry>,
29 pub tool_runtime: Arc<dyn ToolRuntime>,
31 pub output_capacity: usize,
33 pub limits: LoopLimits,
35}
36
37pub struct LoopHandle {
39 pub loop_id: LoopId,
41 pub control: LoopControl,
43 pub health: LoopHealth,
45 pub completion: LoopCompletion,
47 pub output: Mutex<mpsc::Receiver<LoopOutputEvent>>,
49}
50
51#[derive(Clone)]
53pub struct LoopControl {
54 cancelled: Arc<AtomicBool>,
55 notify: Arc<tokio::sync::Notify>,
56}
57
58impl LoopControl {
59 fn new() -> Self {
60 Self {
61 cancelled: Arc::new(AtomicBool::new(false)),
62 notify: Arc::new(tokio::sync::Notify::new()),
63 }
64 }
65
66 pub fn cancel(&self) {
68 self.cancelled.store(true, Ordering::SeqCst);
69 self.notify.notify_waiters();
70 }
71
72 fn is_cancelled(&self) -> bool {
73 self.cancelled.load(Ordering::SeqCst)
74 }
75}
76
77#[derive(Clone, Debug, Default)]
79pub struct LoopHealth {
80 pub events_received: Arc<AtomicU64>,
82 pub tools_unavailable: Arc<AtomicU64>,
84}
85
86pub struct LoopCompletion {
88 rx: Mutex<Option<oneshot::Receiver<LoopEnd>>>,
89}
90
91impl LoopCompletion {
92 pub async fn wait(self) -> LoopEnd {
94 let mut guard = self.rx.lock().await;
95 let rx = guard.take().expect("LoopCompletion polled twice");
96 rx.await.unwrap_or(LoopEnd {
97 monoloop_run_id: MonoloopRunId::new("unknown"),
98 loop_id: LoopId::new("unknown"),
99 kind: LoopEndKind::InvariantFailed,
100 delivery_events_received: 0,
101 duplicate_events: 0,
102 tools_unavailable: 0,
103 outbound_results_emitted: 0,
104 safe_diagnostics: vec!["completion dropped".into()],
105 })
106 }
107}
108
109#[derive(Clone, Debug, Default)]
111pub struct DefaultLoopRuntime;
112
113impl DefaultLoopRuntime {
114 pub fn new() -> Self {
116 Self
117 }
118
119 pub fn start(&self, request: StartLoop) -> Result<LoopHandle, LoopError> {
121 spawn_loop(request)
122 }
123
124 pub fn start_empty(
126 &self,
127 monoloop_run_id: MonoloopRunId,
128 loop_id: LoopId,
129 scope: LoopScope,
130 subscription: CanonicalEventSubscription,
131 limits: LoopLimits,
132 ) -> Result<LoopHandle, LoopError> {
133 self.start(StartLoop {
134 monoloop_run_id,
135 loop_id,
136 scope,
137 subscription,
138 tool_registry: Arc::new(EmptyToolRegistry::new()),
139 tool_runtime: Arc::new(NoToolRuntime::new()),
140 output_capacity: limits.max_output_queue,
141 limits,
142 })
143 }
144}
145
146fn spawn_loop(request: StartLoop) -> Result<LoopHandle, LoopError> {
147 let control = LoopControl::new();
148 let health = LoopHealth::default();
149 let (out_tx, out_rx) = mpsc::channel(request.output_capacity.max(1));
150 let (end_tx, end_rx) = oneshot::channel();
151
152 let loop_id = request.loop_id.clone();
153 let control_task = control.clone();
154 let health_task = health.clone();
155
156 tokio::spawn(async move {
157 let mut owner = LoopOwner {
158 monoloop_run_id: request.monoloop_run_id,
159 loop_id: request.loop_id,
160 scope: request.scope,
161 registry: request.tool_registry,
162 runtime: request.tool_runtime,
163 limits: request.limits,
164 out_tx,
165 control: control_task,
166 health: health_task,
167 last_seq: 0,
168 actions: HashMap::new(),
169 dedup: HashMap::new(),
170 delivery_events: 0,
171 duplicates: 0,
172 tools_unavailable: 0,
173 outbound_results: 0,
174 diagnostics: Vec::new(),
175 ended: false,
176 };
177 owner.run(request.subscription, end_tx).await;
178 });
179
180 Ok(LoopHandle {
181 loop_id,
182 control,
183 health,
184 completion: LoopCompletion {
185 rx: Mutex::new(Some(end_rx)),
186 },
187 output: Mutex::new(out_rx),
188 })
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192enum ActionState {
193 ObservedWaiting,
194 RequestReady,
195 Unavailable,
196 Incomplete,
197}
198
199struct ActionRecord {
200 #[allow(dead_code)]
201 tool_action_id: ToolActionId,
202 #[allow(dead_code)]
203 unit_id: UnitId,
204 last_generation: u64,
205 state: ActionState,
206 dispatched: bool,
207}
208
209struct LoopOwner {
210 monoloop_run_id: MonoloopRunId,
211 loop_id: LoopId,
212 scope: LoopScope,
213 registry: Arc<dyn ToolRegistry>,
214 runtime: Arc<dyn ToolRuntime>,
215 limits: LoopLimits,
216 out_tx: mpsc::Sender<LoopOutputEvent>,
217 control: LoopControl,
218 health: LoopHealth,
219 last_seq: u64,
220 actions: HashMap<String, ActionRecord>,
221 dedup: HashMap<String, u64>,
222 delivery_events: u64,
223 duplicates: u64,
224 tools_unavailable: u64,
225 outbound_results: u64,
226 diagnostics: Vec<String>,
227 ended: bool,
228}
229
230impl LoopOwner {
231 async fn run(
232 &mut self,
233 mut subscription: CanonicalEventSubscription,
234 end_tx: oneshot::Sender<LoopEnd>,
235 ) {
236 loop {
237 if self.control.is_cancelled() {
238 self.finish(LoopEndKind::Cancelled, end_tx).await;
239 return;
240 }
241
242 tokio::select! {
243 biased;
244 _ = self.control.notify.notified() => {
245 if self.control.is_cancelled() {
246 self.finish(LoopEndKind::Cancelled, end_tx).await;
247 return;
248 }
249 }
250 msg = subscription.recv() => {
251 match msg {
252 None => {
253 self.finish(LoopEndKind::Drained, end_tx).await;
254 return;
255 }
256 Some(Err(SubscriptionStatus::Gap(_)))
257 | Some(Err(SubscriptionStatus::Lost)) => {
258 self.finish(LoopEndKind::SubscriptionLost, end_tx).await;
259 return;
260 }
261 Some(Err(SubscriptionStatus::Opened | SubscriptionStatus::Closing)) => {}
262 Some(Ok(delivered)) => {
263 if let Err(kind) = self.on_delivered(delivered).await {
264 self.finish(kind, end_tx).await;
265 return;
266 }
267 }
268 }
269 }
270 }
271 }
272 }
273
274 async fn on_delivered(
275 &mut self,
276 delivered: crate::subscription::DeliveredEvent,
277 ) -> Result<(), LoopEndKind> {
278 if self.last_seq > 0 && delivered.delivery_sequence != self.last_seq + 1 {
280 self.diag(format!(
281 "delivery gap: expected {}, got {}",
282 self.last_seq + 1,
283 delivered.delivery_sequence
284 ));
285 return Err(LoopEndKind::SubscriptionLost);
286 }
287 self.last_seq = delivered.delivery_sequence;
288 self.delivery_events += 1;
289 self.health.events_received.fetch_add(1, Ordering::Relaxed);
290
291 match delivered.event {
292 InterpreterOutputEvent::Unit(ev) => self.on_unit(*ev).await,
293 InterpreterOutputEvent::Ended(end) => {
294 self.on_interpretation_end(end).await?;
295 Ok(())
297 }
298 }
299 }
300
301 async fn on_unit(&mut self, event: CanonicalUnitEvent) -> Result<(), LoopEndKind> {
302 let snap = event.snapshot();
303 if !self.in_scope(snap) {
304 return Ok(());
306 }
307
308 let CanonicalUnit::Tool(tool) = &snap.unit else {
310 return Ok(());
311 };
312 let key = format!(
313 "{}:{}",
314 snap.interpretation_id.as_str(),
315 tool.tool_action_id.as_str()
316 );
317 let dig_key = format!("{}:{}", key, snap.unit_generation);
318 if let Some(prev) = self.dedup.get(&dig_key) {
319 if *prev == snap.unit_generation {
320 self.duplicates += 1;
321 return Ok(());
322 }
323 }
324 if self.dedup.len() >= self.limits.max_dedup_entries {
325 return Err(LoopEndKind::InvariantFailed);
326 }
327 self.dedup.insert(dig_key, snap.unit_generation);
328
329 match tool.request_state {
330 ToolRequestState::Assembling => {
331 self.track_waiting(
332 &key,
333 tool.tool_action_id.clone(),
334 snap.unit_id.clone(),
335 snap.unit_generation,
336 );
337 }
338 ToolRequestState::Ready => {
339 self.on_request_ready(
340 &key,
341 tool.tool_action_id.clone(),
342 snap.unit_id.clone(),
343 snap.unit_generation,
344 tool.tool_name.clone(),
345 tool.request_payload.clone(),
346 snap,
347 )
348 .await?;
349 }
350 ToolRequestState::Incomplete | ToolRequestState::Malformed => {
351 let rec = self.actions.entry(key).or_insert_with(|| ActionRecord {
352 tool_action_id: tool.tool_action_id.clone(),
353 unit_id: snap.unit_id.clone(),
354 last_generation: 0,
355 state: ActionState::Incomplete,
356 dispatched: false,
357 });
358 if snap.unit_generation >= rec.last_generation {
359 rec.last_generation = snap.unit_generation;
360 rec.state = ActionState::Incomplete;
361 }
362 }
363 }
364 Ok(())
365 }
366
367 fn track_waiting(
368 &mut self,
369 key: &str,
370 tool_action_id: ToolActionId,
371 unit_id: UnitId,
372 generation: u64,
373 ) {
374 let rec = self
375 .actions
376 .entry(key.to_string())
377 .or_insert_with(|| ActionRecord {
378 tool_action_id,
379 unit_id,
380 last_generation: 0,
381 state: ActionState::ObservedWaiting,
382 dispatched: false,
383 });
384 if generation < rec.last_generation {
385 return; }
387 rec.last_generation = generation;
388 if rec.state != ActionState::Unavailable && !rec.dispatched {
389 rec.state = ActionState::ObservedWaiting;
390 }
391 }
392
393 #[allow(clippy::too_many_arguments)]
394 async fn on_request_ready(
395 &mut self,
396 key: &str,
397 tool_action_id: ToolActionId,
398 unit_id: UnitId,
399 generation: u64,
400 tool_name: Option<String>,
401 request_payload: Option<String>,
402 snap: &monoloop_contracts::CanonicalUnitSnapshot,
403 ) -> Result<(), LoopEndKind> {
404 if self.actions.len() >= self.limits.max_tool_actions && !self.actions.contains_key(key) {
405 return Err(LoopEndKind::InvariantFailed);
406 }
407
408 let rec = self
409 .actions
410 .entry(key.to_string())
411 .or_insert_with(|| ActionRecord {
412 tool_action_id: tool_action_id.clone(),
413 unit_id: unit_id.clone(),
414 last_generation: 0,
415 state: ActionState::RequestReady,
416 dispatched: false,
417 });
418
419 if generation < rec.last_generation {
420 return Ok(()); }
422 rec.last_generation = generation;
423
424 if rec.dispatched {
426 self.duplicates += 1;
427 return Ok(());
428 }
429
430 let Some(name) = tool_name else {
431 self.diag("ToolRequestReady missing tool name".into());
432 return Ok(());
433 };
434 let Some(payload) = request_payload else {
435 self.diag("ToolRequestReady missing payload".into());
436 return Ok(());
437 };
438
439 rec.state = ActionState::RequestReady;
440 rec.dispatched = true;
441
442 if self
443 .out_tx
444 .send(LoopOutputEvent::ToolDispatchRequested {
445 tool_action_id: tool_action_id.clone(),
446 request_generation: generation,
447 })
448 .await
449 .is_err()
450 {
451 return Err(LoopEndKind::OutputFailed);
452 }
453
454 let resolution = self
455 .registry
456 .resolve(ResolveToolRequest {
457 tool_action_id: tool_action_id.clone(),
458 tool_name: name.clone(),
459 request_payload: payload.clone(),
460 })
461 .await
462 .map_err(|_| LoopEndKind::InvariantFailed)?;
463
464 match resolution {
465 ToolResolution::Unavailable(reason) => {
466 rec.state = ActionState::Unavailable;
467 self.tools_unavailable += 1;
468 self.health
469 .tools_unavailable
470 .fetch_add(1, Ordering::Relaxed);
471
472 if self
473 .out_tx
474 .send(LoopOutputEvent::ToolUnavailable {
475 tool_action_id: tool_action_id.clone(),
476 reason,
477 })
478 .await
479 .is_err()
480 {
481 return Err(LoopEndKind::OutputFailed);
482 }
483
484 let result = OutboundToolResult {
485 outbound_result_id: uuid::Uuid::new_v4().to_string(),
486 monoloop_run_id: self.monoloop_run_id.clone(),
487 loop_id: self.loop_id.clone(),
488 source_interpretation_id: snap.interpretation_id.clone(),
489 source_connection_id: snap.connection_id.clone(),
490 external_session_id: snap.external_session_id.clone(),
491 tool_action_id,
492 request_generation: generation,
493 tool_execution_id: None,
494 outcome: OutboundToolOutcome::ToolUnavailable,
495 payload: format!("{reason:?}"),
496 source_unit_id: unit_id,
497 };
498 self.outbound_results += 1;
499 if self
500 .out_tx
501 .send(LoopOutputEvent::OutboundToolResult(result))
502 .await
503 .is_err()
504 {
505 return Err(LoopEndKind::OutputFailed);
506 }
507 }
509 ToolResolution::Available(_) => {
510 let execution_id = ToolExecutionId::generate();
511 let handle = match self.runtime.start(crate::tools::StartToolExecution {
512 execution_id: execution_id.clone(),
513 tool_action_id: tool_action_id.as_str().to_string(),
514 tool_name: name,
515 request_payload: payload,
516 request_generation: generation,
517 }) {
518 Ok(h) => h,
519 Err(e) => {
520 self.diag(format!("tool runtime start failed: {e}"));
521 if self
522 .out_tx
523 .send(LoopOutputEvent::OutboundToolResult(OutboundToolResult {
524 outbound_result_id: uuid::Uuid::new_v4().to_string(),
525 monoloop_run_id: self.monoloop_run_id.clone(),
526 loop_id: self.loop_id.clone(),
527 source_interpretation_id: snap.interpretation_id.clone(),
528 source_connection_id: snap.connection_id.clone(),
529 external_session_id: snap.external_session_id.clone(),
530 tool_action_id,
531 request_generation: generation,
532 tool_execution_id: None,
533 outcome: OutboundToolOutcome::ExecutionFailed,
534 payload: e.0,
535 source_unit_id: unit_id,
536 }))
537 .await
538 .is_err()
539 {
540 return Err(LoopEndKind::OutputFailed);
541 }
542 self.outbound_results += 1;
543 return Ok(());
544 }
545 };
546
547 let terminal = if let Some(rx) = handle.completion {
548 rx.await.unwrap_or(crate::tools::ToolRuntimeTerminal {
549 outcome: OutboundToolOutcome::ExecutionLost,
550 payload: "completion lost".into(),
551 })
552 } else {
553 crate::tools::ToolRuntimeTerminal {
554 outcome: OutboundToolOutcome::ExecutionFailed,
555 payload: "runtime returned no completion".into(),
556 }
557 };
558
559 self.outbound_results += 1;
560 if self
561 .out_tx
562 .send(LoopOutputEvent::OutboundToolResult(OutboundToolResult {
563 outbound_result_id: uuid::Uuid::new_v4().to_string(),
564 monoloop_run_id: self.monoloop_run_id.clone(),
565 loop_id: self.loop_id.clone(),
566 source_interpretation_id: snap.interpretation_id.clone(),
567 source_connection_id: snap.connection_id.clone(),
568 external_session_id: snap.external_session_id.clone(),
569 tool_action_id,
570 request_generation: generation,
571 tool_execution_id: Some(execution_id),
572 outcome: terminal.outcome,
573 payload: terminal.payload,
574 source_unit_id: unit_id,
575 }))
576 .await
577 .is_err()
578 {
579 return Err(LoopEndKind::OutputFailed);
580 }
581 }
582 }
583 Ok(())
584 }
585
586 async fn on_interpretation_end(&mut self, _end: InterpretationEnd) -> Result<(), LoopEndKind> {
587 Ok(())
589 }
590
591 fn in_scope(&self, snap: &monoloop_contracts::CanonicalUnitSnapshot) -> bool {
592 if self.scope.accept_all_in_run {
593 return true;
594 }
595 if !self.scope.accepted_interpretation_ids.is_empty()
596 && !self
597 .scope
598 .accepted_interpretation_ids
599 .iter()
600 .any(|id| id.as_str() == snap.interpretation_id.as_str())
601 {
602 return false;
603 }
604 if !self.scope.accepted_connection_ids.is_empty()
605 && !self
606 .scope
607 .accepted_connection_ids
608 .iter()
609 .any(|id| id.as_str() == snap.connection_id.as_str())
610 {
611 return false;
612 }
613 if !self.scope.accepted_external_session_ids.is_empty() {
614 match &snap.external_session_id {
615 Some(ext) => {
616 if !self
617 .scope
618 .accepted_external_session_ids
619 .iter()
620 .any(|id| id.as_str() == ext.as_str())
621 {
622 return false;
623 }
624 }
625 None => return false,
626 }
627 }
628 true
629 }
630
631 fn diag(&mut self, msg: String) {
632 if self.diagnostics.len() < 32 {
633 self.diagnostics.push(msg);
634 }
635 }
636
637 async fn finish(&mut self, kind: LoopEndKind, end_tx: oneshot::Sender<LoopEnd>) {
638 if self.ended {
639 return;
640 }
641 self.ended = true;
642 let end = LoopEnd {
643 monoloop_run_id: self.monoloop_run_id.clone(),
644 loop_id: self.loop_id.clone(),
645 kind,
646 delivery_events_received: self.delivery_events,
647 duplicate_events: self.duplicates,
648 tools_unavailable: self.tools_unavailable,
649 outbound_results_emitted: self.outbound_results,
650 safe_diagnostics: self.diagnostics.clone(),
651 };
652 let _ = self
653 .out_tx
654 .send(LoopOutputEvent::LoopEnded(end.clone()))
655 .await;
656 let _ = end_tx.send(end);
657 }
658}