1use std::collections::HashMap;
13use std::future::Future;
14
15use meerkat_core::lifecycle::InputId;
16#[cfg(test)]
17use meerkat_core::lifecycle::RunId;
18use meerkat_core::lifecycle::core_executor::CoreApplyTerminal;
19use meerkat_core::types::{RunResult, SessionId};
20use meerkat_core::{TurnErrorMetadata, TurnTerminalCauseKind, TurnTerminalOutcome};
21use serde_json::Value;
22
23use crate::meerkat_machine::driver::{
24 RuntimeCompletionResultAttempt, RuntimeCompletionResultAuthority,
25 RuntimeCompletionResultRealized,
26};
27use crate::meerkat_machine::dsl::RuntimeCompletionResultClass;
28use crate::tokio::sync::oneshot;
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum CompletionWaitError {
37 #[error("completion channel closed without an authorized result")]
38 ChannelClosed,
39 #[error("executor attachment was replaced before request completion; retry the request")]
44 AttachmentReplaced,
45 #[error("{0}")]
46 AuthorityUnavailable(String),
47}
48
49impl CompletionWaitError {
50 pub fn wait_failure_observation(
51 &self,
52 ) -> crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation {
53 match self {
54 Self::ChannelClosed => {
55 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation::ChannelClosed
56 }
57 Self::AttachmentReplaced | Self::AuthorityUnavailable(_) => {
58 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailureObservation::AuthorityUnavailable
59 }
60 }
61 }
62
63 #[must_use]
66 pub const fn is_attachment_replaced(&self) -> bool {
67 matches!(self, Self::AttachmentReplaced)
68 }
69}
70
71#[derive(Debug, Clone)]
73pub enum CompletionOutcome {
74 Completed(Box<RunResult>),
76 CompletedWithoutResult,
78 CallbackPending { tool_name: String, args: Value },
81 Cancelled,
83 Abandoned {
87 reason: String,
88 error: TurnErrorMetadata,
89 },
90 AbandonedWithError {
92 reason: String,
93 error: TurnErrorMetadata,
94 },
95 CompletedWithFinalizationFailure { error: TurnErrorMetadata },
101 RuntimeTerminated {
104 reason: String,
105 error: TurnErrorMetadata,
106 },
107}
108
109pub fn interaction_terminal_event(
114 interaction_id: meerkat_core::interaction::InteractionId,
115 outcome: CompletionOutcome,
116) -> meerkat_core::event::AgentEvent {
117 use meerkat_core::event::{AgentEvent, InteractionFailureReason};
118
119 match outcome {
120 CompletionOutcome::Completed(result) => {
121 if let Some(extraction_error) = result.extraction_error {
122 AgentEvent::InteractionFailed {
123 interaction_id,
124 reason: InteractionFailureReason::ExtractionFailed {
125 last_output: extraction_error.last_output,
126 attempts: extraction_error.attempts,
127 reason: extraction_error.reason,
128 },
129 }
130 } else {
131 AgentEvent::InteractionComplete {
132 interaction_id,
133 result: result.text,
134 structured_output: result.structured_output,
135 }
136 }
137 }
138 CompletionOutcome::CompletedWithoutResult => AgentEvent::InteractionComplete {
139 interaction_id,
140 result: String::new(),
141 structured_output: None,
142 },
143 CompletionOutcome::CallbackPending { tool_name, args } => {
144 AgentEvent::InteractionCallbackPending {
145 interaction_id,
146 tool_name,
147 args,
148 }
149 }
150 CompletionOutcome::Cancelled => AgentEvent::InteractionFailed {
151 interaction_id,
152 reason: InteractionFailureReason::Cancelled,
153 },
154 CompletionOutcome::Abandoned { reason, .. }
155 | CompletionOutcome::AbandonedWithError { reason, .. }
156 | CompletionOutcome::RuntimeTerminated { reason, .. } => AgentEvent::InteractionFailed {
157 interaction_id,
158 reason: InteractionFailureReason::abandoned(reason),
159 },
160 CompletionOutcome::CompletedWithFinalizationFailure { error } => {
161 let detail = error
162 .detail
163 .unwrap_or_else(|| "turn finalization failed".to_string());
164 AgentEvent::InteractionFailed {
165 interaction_id,
166 reason: InteractionFailureReason::finalization_failed(detail),
167 }
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct CompletionCleanupObservation {
178 owner_session_id: SessionId,
179 owner_agent_runtime_id: Option<crate::meerkat_machine::dsl::AgentRuntimeId>,
180 owner_fence_token: Option<crate::meerkat_machine::dsl::FenceToken>,
181 owner_runtime_generation: Option<crate::meerkat_machine::dsl::Generation>,
182 owner_runtime_epoch_id: Option<crate::meerkat_machine::dsl::RuntimeEpochId>,
183 observed_outcome: crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome,
184}
185
186impl CompletionCleanupObservation {
187 fn from_realized_result(realized: RuntimeCompletionResultRealized) -> Self {
188 Self {
189 owner_session_id: realized.session_id().clone(),
190 owner_agent_runtime_id: realized.agent_runtime_id().cloned(),
191 owner_fence_token: realized.fence_token(),
192 owner_runtime_generation: realized.runtime_generation(),
193 owner_runtime_epoch_id: realized.runtime_epoch_id().cloned(),
194 observed_outcome: realized.cleanup_observation(),
195 }
196 }
197
198 #[must_use]
207 pub fn proves_runtime_termination_for(&self, session_id: &SessionId) -> bool {
208 self.owner_session_id == *session_id
209 && self.observed_outcome
210 == crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome::RuntimeTerminated
211 }
212
213 pub(crate) fn owner_session_id(&self) -> &SessionId {
214 &self.owner_session_id
215 }
216
217 pub(crate) fn owner_agent_runtime_id(
218 &self,
219 ) -> Option<&crate::meerkat_machine::dsl::AgentRuntimeId> {
220 self.owner_agent_runtime_id.as_ref()
221 }
222
223 pub(crate) fn owner_fence_token(&self) -> Option<crate::meerkat_machine::dsl::FenceToken> {
224 self.owner_fence_token
225 }
226
227 pub(crate) fn owner_runtime_generation(
228 &self,
229 ) -> Option<crate::meerkat_machine::dsl::Generation> {
230 self.owner_runtime_generation
231 }
232
233 pub(crate) fn owner_runtime_epoch_id(
234 &self,
235 ) -> Option<&crate::meerkat_machine::dsl::RuntimeEpochId> {
236 self.owner_runtime_epoch_id.as_ref()
237 }
238
239 pub(crate) fn observed_outcome(
240 &self,
241 ) -> crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome {
242 self.observed_outcome
243 }
244}
245
246#[derive(Debug)]
249struct CompletionDelivery {
250 outcome: CompletionOutcome,
251 cleanup_observation: CompletionCleanupObservation,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct CompletionWaiterEntrySnapshot {
260 pub input_id: InputId,
261 pub waiter_count: usize,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Default)]
269pub struct CompletionRegistrySnapshot {
270 pub input_count: usize,
271 pub waiter_count: usize,
272 pub waiting_inputs: Vec<CompletionWaiterEntrySnapshot>,
273}
274
275#[derive(Debug)]
277pub struct CompletionHandle {
278 rx: oneshot::Receiver<Result<CompletionDelivery, CompletionWaitError>>,
279}
280
281impl CompletionHandle {
282 async fn try_wait_delivery(self) -> Result<CompletionDelivery, CompletionWaitError> {
283 self.rx
284 .await
285 .unwrap_or(Err(CompletionWaitError::ChannelClosed))
286 }
287
288 pub async fn try_wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
290 self.try_wait_delivery()
291 .await
292 .map(|delivery| delivery.outcome)
293 }
294
295 pub async fn try_wait_with_cleanup_observation(
298 self,
299 ) -> Result<(CompletionOutcome, CompletionCleanupObservation), CompletionWaitError> {
300 let delivery = self.try_wait_delivery().await?;
301 Ok((delivery.outcome, delivery.cleanup_observation))
302 }
303
304 pub async fn wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
306 self.try_wait().await
307 }
308
309 #[cfg(test)]
311 pub(crate) async fn wait_authorized(self) -> CompletionOutcome {
312 self.wait()
313 .await
314 .expect("completion waiter closed without an authorized result")
315 }
316
317 pub fn with_cleanup<F, Fut>(self, cleanup: F) -> Self
321 where
322 F: FnOnce() -> Fut + Send + 'static,
323 Fut: Future<Output = ()> + Send + 'static,
324 {
325 let (tx, rx) = oneshot::channel();
326 crate::tokio::spawn(async move {
327 let outcome = self.try_wait_delivery().await;
328 cleanup().await;
329 let _ = tx.send(outcome);
330 });
331 Self { rx }
332 }
333
334 pub fn with_outcome_cleanup<F, Fut>(self, cleanup: F) -> Self
339 where
340 F: FnOnce(CompletionCleanupObservation) -> Fut + Send + 'static,
341 Fut: Future<Output = ()> + Send + 'static,
342 {
343 let (tx, rx) = oneshot::channel();
344 crate::tokio::spawn(async move {
345 let outcome = match self.try_wait_delivery().await {
346 Ok(delivery) => {
347 cleanup(delivery.cleanup_observation.clone()).await;
348 Ok(delivery)
349 }
350 Err(error) => Err(error),
351 };
352 let _ = tx.send(outcome);
353 });
354 Self { rx }
355 }
356
357 pub fn with_completion_cleanup<F, Fut>(self, cleanup: F) -> Self
360 where
361 F: FnOnce(Result<CompletionCleanupObservation, CompletionWaitError>) -> Fut
362 + Send
363 + 'static,
364 Fut: Future<Output = ()> + Send + 'static,
365 {
366 let (tx, rx) = oneshot::channel();
367 crate::tokio::spawn(async move {
368 let outcome = self.try_wait_delivery().await;
369 match &outcome {
370 Ok(delivery) => cleanup(Ok(delivery.cleanup_observation.clone())).await,
371 Err(error) => cleanup(Err(error.clone())).await,
372 }
373 let _ = tx.send(outcome);
374 });
375 Self { rx }
376 }
377
378 pub fn with_resultful_completion_cleanup<F, Fut>(self, cleanup: F) -> Self
382 where
383 F: FnOnce(Result<CompletionCleanupObservation, CompletionWaitError>) -> Fut
384 + Send
385 + 'static,
386 Fut: Future<Output = Result<(), CompletionWaitError>> + Send + 'static,
387 {
388 let (tx, rx) = oneshot::channel();
389 crate::tokio::spawn(async move {
390 let outcome = self.try_wait_delivery().await;
391 let cleanup_input = match &outcome {
392 Ok(delivery) => Ok(delivery.cleanup_observation.clone()),
393 Err(error) => Err(error.clone()),
394 };
395 let cleanup_result = cleanup(cleanup_input).await;
396 let gated = match (outcome, cleanup_result) {
397 (Ok(delivery), Ok(())) => Ok(delivery),
398 (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
399 (Err(primary_error), Ok(())) => Err(primary_error),
400 (Err(primary_error), Err(cleanup_error)) => {
401 Err(CompletionWaitError::AuthorityUnavailable(format!(
402 "{primary_error}; additionally failed required completion cleanup: {cleanup_error}"
403 )))
404 }
405 };
406 let _ = tx.send(gated);
407 });
408 Self { rx }
409 }
410
411 #[cfg(test)]
412 fn already_resolved_internal(
413 outcome: CompletionOutcome,
414 realized: RuntimeCompletionResultRealized,
415 ) -> Self {
416 let (tx, rx) = oneshot::channel();
417 let _ = tx.send(Ok(CompletionDelivery {
418 outcome,
419 cleanup_observation: CompletionCleanupObservation::from_realized_result(realized),
420 }));
421 Self { rx }
422 }
423
424 #[cfg(test)]
425 pub(crate) fn already_resolved_with_generated_class(
426 outcome: CompletionOutcome,
427 expected_class: crate::meerkat_machine::dsl::RuntimeCompletionResultClass,
428 terminal: crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation,
429 finalization: crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation,
430 ) -> Result<Self, crate::RuntimeDriverError> {
431 let run_id = if terminal
432 == crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::RuntimeTerminated
433 {
434 None
435 } else {
436 Some(RunId::new())
437 };
438 let authority =
439 crate::meerkat_machine::driver::machine_resolve_pre_resolved_runtime_completion_result(
440 run_id.as_ref(),
441 terminal,
442 finalization,
443 )?;
444 let attempt = authority.begin_surface_resolution();
445 if !attempt.allows(expected_class) {
446 let generated_class = attempt.class();
447 attempt.fail();
448 return Err(crate::RuntimeDriverError::Internal(format!(
449 "generated runtime completion authority returned {generated_class:?}, expected {expected_class:?}",
450 )));
451 }
452 Ok(Self::already_resolved_internal(outcome, attempt.realize()))
453 }
454
455 #[cfg(test)]
456 pub(crate) fn already_completed_without_result() -> Result<Self, crate::RuntimeDriverError> {
457 Self::already_resolved_with_generated_class(
458 CompletionOutcome::CompletedWithoutResult,
459 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::CompletedWithoutResult,
460 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::NoResult,
461 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
462 )
463 }
464
465 #[cfg(test)]
466 pub(crate) fn already_runtime_apply_failed(
467 reason: String,
468 error: TurnErrorMetadata,
469 ) -> Result<Self, crate::RuntimeDriverError> {
470 Self::already_resolved_with_generated_class(
471 CompletionOutcome::AbandonedWithError { reason, error },
472 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::AbandonedWithError,
473 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::NoResult,
474 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Failed,
475 )
476 }
477
478 #[cfg(test)]
479 pub(crate) fn already_runtime_terminated(
480 reason: String,
481 ) -> Result<Self, crate::RuntimeDriverError> {
482 Self::already_resolved_with_generated_class(
483 CompletionOutcome::runtime_terminated(&reason),
484 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::RuntimeTerminated,
485 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::RuntimeTerminated,
486 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
487 )
488 }
489
490 #[cfg(test)]
491 pub(crate) fn already_callback_pending(
492 tool_name: String,
493 args: Value,
494 ) -> Result<Self, crate::RuntimeDriverError> {
495 Self::already_resolved_with_generated_class(
496 CompletionOutcome::CallbackPending { tool_name, args },
497 crate::meerkat_machine::dsl::RuntimeCompletionResultClass::CallbackPending,
498 crate::meerkat_machine::dsl::RuntimeCompletionTerminalObservation::CallbackPending,
499 crate::meerkat_machine::dsl::RuntimeCompletionFinalizationObservation::Succeeded,
500 )
501 }
502}
503
504impl CompletionOutcome {
505 fn runtime_terminated(reason: &str) -> Self {
509 Self::RuntimeTerminated {
510 reason: reason.to_string(),
511 error: TurnErrorMetadata::terminal(
512 TurnTerminalCauseKind::FatalFailure,
513 TurnTerminalOutcome::Failed,
514 reason,
515 ),
516 }
517 }
518
519 pub fn abandoned_reason(&self) -> Option<&str> {
520 match self {
521 Self::Abandoned { reason, .. } | Self::AbandonedWithError { reason, .. } => {
522 Some(reason)
523 }
524 _ => None,
525 }
526 }
527
528 pub fn error_metadata(&self) -> Option<&TurnErrorMetadata> {
529 match self {
530 Self::Abandoned { error, .. }
531 | Self::AbandonedWithError { error, .. }
532 | Self::CompletedWithFinalizationFailure { error, .. }
533 | Self::RuntimeTerminated { error, .. } => Some(error),
534 _ => None,
535 }
536 }
537}
538
539fn authorized_completion_outcome(
540 terminal: Option<&CoreApplyTerminal>,
541 authority: RuntimeCompletionResultAuthority,
542 finalization_error: Option<TurnErrorMetadata>,
543 runtime_termination_reason: Option<&str>,
544) -> Result<(CompletionOutcome, CompletionCleanupObservation), CompletionWaitError> {
545 let attempt = authority.begin_surface_resolution();
546 if runtime_termination_reason.is_some()
547 && attempt.class() != RuntimeCompletionResultClass::RuntimeTerminated
548 {
549 let actual = attempt.class();
550 attempt.fail();
551 return Err(CompletionWaitError::AuthorityUnavailable(format!(
552 "runtime completion authority resolved {actual:?} with a runtime-termination reason"
553 )));
554 }
555 let outcome = match attempt.class() {
556 RuntimeCompletionResultClass::Completed => {
557 let Some(CoreApplyTerminal::RunResult(result)) = terminal else {
558 attempt.fail();
559 return Err(CompletionWaitError::AuthorityUnavailable(
560 "runtime completion authority resolved Completed without result payload"
561 .to_string(),
562 ));
563 };
564 CompletionOutcome::Completed(Box::new(result.as_ref().clone()))
565 }
566 RuntimeCompletionResultClass::CompletedWithoutResult => {
567 if !matches!(terminal, Some(CoreApplyTerminal::NoPendingBoundary) | None) {
568 attempt.fail();
569 return Err(CompletionWaitError::AuthorityUnavailable(
570 "runtime completion authority resolved CompletedWithoutResult with terminal payload"
571 .to_string(),
572 ));
573 }
574 CompletionOutcome::CompletedWithoutResult
575 }
576 RuntimeCompletionResultClass::CallbackPending => {
577 let Some(CoreApplyTerminal::CallbackPending { tool_name, args }) = terminal else {
578 attempt.fail();
579 return Err(CompletionWaitError::AuthorityUnavailable(
580 "runtime completion authority resolved CallbackPending without callback payload"
581 .to_string(),
582 ));
583 };
584 CompletionOutcome::CallbackPending {
585 tool_name: tool_name.clone(),
586 args: args.clone(),
587 }
588 }
589 RuntimeCompletionResultClass::Cancelled => {
590 if terminal.is_some() || finalization_error.is_some() {
591 attempt.fail();
592 return Err(CompletionWaitError::AuthorityUnavailable(
593 "runtime completion authority resolved Cancelled with payload".to_string(),
594 ));
595 }
596 CompletionOutcome::Cancelled
597 }
598 RuntimeCompletionResultClass::AbandonedWithError => {
599 if matches!(terminal, Some(CoreApplyTerminal::RunResult(_))) {
600 attempt.fail();
601 return Err(CompletionWaitError::AuthorityUnavailable(
602 "runtime completion authority resolved AbandonedWithError with result payload"
603 .to_string(),
604 ));
605 }
606 let Some(error) = finalization_error else {
607 attempt.fail();
608 return Err(CompletionWaitError::AuthorityUnavailable(
609 "runtime completion authority resolved AbandonedWithError without typed error"
610 .to_string(),
611 ));
612 };
613 let reason = error
614 .detail
615 .clone()
616 .unwrap_or_else(|| "runtime finalization failed".to_string());
617 CompletionOutcome::AbandonedWithError { reason, error }
618 }
619 RuntimeCompletionResultClass::CompletedWithFinalizationFailure => {
620 let Some(CoreApplyTerminal::RunResult(_)) = terminal else {
621 attempt.fail();
622 return Err(CompletionWaitError::AuthorityUnavailable(
623 "runtime completion authority resolved CompletedWithFinalizationFailure without result payload"
624 .to_string(),
625 ));
626 };
627 let Some(error) = finalization_error else {
628 attempt.fail();
629 return Err(CompletionWaitError::AuthorityUnavailable(
630 "runtime completion authority resolved finalization failure without typed error"
631 .to_string(),
632 ));
633 };
634 CompletionOutcome::CompletedWithFinalizationFailure { error }
635 }
636 RuntimeCompletionResultClass::RuntimeTerminated => {
637 if terminal.is_some() || finalization_error.is_some() {
638 attempt.fail();
639 return Err(CompletionWaitError::AuthorityUnavailable(
640 "runtime completion authority resolved RuntimeTerminated with payload"
641 .to_string(),
642 ));
643 }
644 let Some(reason) = runtime_termination_reason.filter(|reason| !reason.is_empty())
645 else {
646 attempt.fail();
647 return Err(CompletionWaitError::AuthorityUnavailable(
648 "runtime completion authority resolved RuntimeTerminated without its exact reason"
649 .to_string(),
650 ));
651 };
652 CompletionOutcome::runtime_terminated(reason)
653 }
654 };
655 Ok((
656 outcome,
657 CompletionCleanupObservation::from_realized_result(attempt.realize()),
658 ))
659}
660
661#[must_use = "authorized runtime terminal bundle must be published and/or delivered"]
667pub(crate) struct AuthorizedRuntimeTerminalBundle {
668 outcome: CompletionOutcome,
669 cleanup_observation: CompletionCleanupObservation,
670 interaction_events: Vec<meerkat_core::event::AgentEvent>,
671}
672
673impl AuthorizedRuntimeTerminalBundle {
674 pub(crate) fn interaction_events(&self) -> &[meerkat_core::event::AgentEvent] {
675 &self.interaction_events
676 }
677
678 #[cfg(test)]
679 fn into_interaction_events(self) -> Vec<meerkat_core::event::AgentEvent> {
680 self.interaction_events
681 }
682}
683
684pub(crate) fn authorize_runtime_terminal_bundle(
687 interaction_ids: &[meerkat_core::interaction::InteractionId],
688 terminal: Option<&CoreApplyTerminal>,
689 authority: RuntimeCompletionResultAuthority,
690 finalization_error: Option<TurnErrorMetadata>,
691 runtime_termination_reason: Option<&str>,
692) -> Result<AuthorizedRuntimeTerminalBundle, CompletionWaitError> {
693 let (outcome, cleanup_observation) = authorized_completion_outcome(
694 terminal,
695 authority,
696 finalization_error,
697 runtime_termination_reason,
698 )?;
699 let interaction_events = interaction_ids
700 .iter()
701 .map(|interaction_id| interaction_terminal_event(*interaction_id, outcome.clone()))
702 .collect();
703 Ok(AuthorizedRuntimeTerminalBundle {
704 outcome,
705 cleanup_observation,
706 interaction_events,
707 })
708}
709
710#[cfg(test)]
713pub(crate) fn authorized_interaction_terminal_events(
714 interaction_ids: &[meerkat_core::interaction::InteractionId],
715 terminal: Option<&CoreApplyTerminal>,
716 authority: RuntimeCompletionResultAuthority,
717 finalization_error: Option<TurnErrorMetadata>,
718) -> Result<Vec<meerkat_core::event::AgentEvent>, CompletionWaitError> {
719 authorize_runtime_terminal_bundle(
720 interaction_ids,
721 terminal,
722 authority,
723 finalization_error,
724 None,
725 )
726 .map(AuthorizedRuntimeTerminalBundle::into_interaction_events)
727}
728
729#[derive(Default)]
734pub(crate) struct CompletionRegistry {
735 waiters:
736 HashMap<InputId, Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>>,
737}
738
739impl CompletionRegistry {
740 pub(crate) fn new() -> Self {
741 Self::default()
742 }
743
744 fn take_waiters(
745 &mut self,
746 input_id: &InputId,
747 ) -> Option<Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>> {
748 self.waiters.remove(input_id)
749 }
750
751 fn send_outcome(
752 senders: Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>,
753 outcome: CompletionOutcome,
754 cleanup_observation: CompletionCleanupObservation,
755 ) {
756 for tx in senders {
757 let outcome = outcome.clone();
758 let _ = tx.send(Ok(CompletionDelivery {
759 outcome,
760 cleanup_observation: cleanup_observation.clone(),
761 }));
762 }
763 }
764
765 fn send_error(
766 senders: Vec<oneshot::Sender<Result<CompletionDelivery, CompletionWaitError>>>,
767 error: CompletionWaitError,
768 ) {
769 for tx in senders {
770 let _ = tx.send(Err(error.clone()));
771 }
772 }
773
774 fn authority_mismatch_error(
775 authority: &RuntimeCompletionResultAttempt,
776 expected: RuntimeCompletionResultClass,
777 ) -> CompletionWaitError {
778 CompletionWaitError::AuthorityUnavailable(format!(
779 "generated runtime completion authority returned {:?}, expected {expected:?}",
780 authority.class()
781 ))
782 }
783
784 fn fail_input_authority_mismatch(
785 &mut self,
786 input_id: &InputId,
787 authority: RuntimeCompletionResultAttempt,
788 expected: RuntimeCompletionResultClass,
789 ) {
790 let error = Self::authority_mismatch_error(&authority, expected);
791 authority.fail();
792 if let Some(senders) = self.take_waiters(input_id) {
793 Self::send_error(senders, error);
794 }
795 }
796
797 #[cfg(test)]
798 fn fail_inputs_authority_mismatch<I>(
799 &mut self,
800 input_ids: I,
801 authority: RuntimeCompletionResultAttempt,
802 expected: RuntimeCompletionResultClass,
803 ) where
804 I: IntoIterator<Item = InputId>,
805 {
806 let error = Self::authority_mismatch_error(&authority, expected);
807 authority.fail();
808 self.fail_inputs(input_ids, error);
809 }
810
811 fn cleanup_from_realized_attempt(
812 authority: RuntimeCompletionResultAttempt,
813 ) -> CompletionCleanupObservation {
814 CompletionCleanupObservation::from_realized_result(authority.realize())
815 }
816
817 pub(crate) fn register(&mut self, input_id: InputId) -> CompletionHandle {
822 let (tx, rx) = oneshot::channel();
823 self.waiters.entry(input_id).or_default().push(tx);
824 CompletionHandle { rx }
825 }
826
827 #[cfg(test)]
829 fn resolve_completed(
830 &mut self,
831 input_id: &InputId,
832 result: RunResult,
833 cleanup_observation: CompletionCleanupObservation,
834 ) {
835 if let Some(senders) = self.take_waiters(input_id) {
836 Self::send_outcome(
837 senders,
838 CompletionOutcome::Completed(Box::new(result)),
839 cleanup_observation,
840 );
841 }
842 }
843
844 #[cfg(test)]
845 pub(crate) fn resolve_completed_authorized(
846 &mut self,
847 input_id: &InputId,
848 result: RunResult,
849 authority: RuntimeCompletionResultAuthority,
850 ) {
851 let expected = RuntimeCompletionResultClass::Completed;
852 let attempt = authority.begin_surface_resolution();
853 if !attempt.allows(expected) {
854 self.fail_input_authority_mismatch(input_id, attempt, expected);
855 return;
856 }
857 self.resolve_completed(
858 input_id,
859 result,
860 Self::cleanup_from_realized_attempt(attempt),
861 );
862 }
863
864 #[cfg(test)]
865 pub(crate) fn resolve_runtime_completion_authorized<I>(
866 &mut self,
867 input_ids: I,
868 terminal: Option<&CoreApplyTerminal>,
869 authority: RuntimeCompletionResultAuthority,
870 finalization_error: Option<TurnErrorMetadata>,
871 ) where
872 I: IntoIterator<Item = InputId>,
873 {
874 let input_ids: Vec<InputId> = input_ids.into_iter().collect();
875 if !input_ids
876 .iter()
877 .any(|input_id| self.waiters.contains_key(input_id))
878 {
879 let attempt = authority.begin_surface_resolution();
880 attempt.abandon();
881 return;
882 }
883 let (outcome, cleanup_observation) =
884 match authorized_completion_outcome(terminal, authority, finalization_error, None) {
885 Ok(projected) => projected,
886 Err(error) => {
887 self.fail_inputs(input_ids, error);
888 return;
889 }
890 };
891 for input_id in input_ids {
892 if let Some(senders) = self.take_waiters(&input_id) {
893 Self::send_outcome(senders, outcome.clone(), cleanup_observation.clone());
894 }
895 }
896 }
897
898 pub(crate) fn resolve_authorized_runtime_terminal_bundle<I>(
901 &mut self,
902 input_ids: I,
903 bundle: AuthorizedRuntimeTerminalBundle,
904 ) where
905 I: IntoIterator<Item = InputId>,
906 {
907 for input_id in input_ids {
908 if let Some(senders) = self.take_waiters(&input_id) {
909 Self::send_outcome(
910 senders,
911 bundle.outcome.clone(),
912 bundle.cleanup_observation.clone(),
913 );
914 }
915 }
916 }
917
918 fn resolve_without_result(
920 &mut self,
921 input_id: &InputId,
922 cleanup_observation: CompletionCleanupObservation,
923 ) {
924 if let Some(senders) = self.take_waiters(input_id) {
925 Self::send_outcome(
926 senders,
927 CompletionOutcome::CompletedWithoutResult,
928 cleanup_observation,
929 );
930 }
931 }
932
933 pub(crate) fn resolve_without_result_authorized(
934 &mut self,
935 input_id: &InputId,
936 authority: RuntimeCompletionResultAuthority,
937 ) {
938 let expected = RuntimeCompletionResultClass::CompletedWithoutResult;
939 let attempt = authority.begin_surface_resolution();
940 if !attempt.allows(expected) {
941 self.fail_input_authority_mismatch(input_id, attempt, expected);
942 return;
943 }
944 self.resolve_without_result(input_id, Self::cleanup_from_realized_attempt(attempt));
945 }
946
947 #[cfg(test)]
949 fn resolve_callback_pending(
950 &mut self,
951 input_id: &InputId,
952 tool_name: String,
953 args: Value,
954 cleanup_observation: CompletionCleanupObservation,
955 ) {
956 if let Some(senders) = self.take_waiters(input_id) {
957 Self::send_outcome(
958 senders,
959 CompletionOutcome::CallbackPending { tool_name, args },
960 cleanup_observation,
961 );
962 }
963 }
964
965 #[cfg(test)]
966 pub(crate) fn resolve_callback_pending_authorized(
967 &mut self,
968 input_id: &InputId,
969 tool_name: String,
970 args: Value,
971 authority: RuntimeCompletionResultAuthority,
972 ) {
973 let expected = RuntimeCompletionResultClass::CallbackPending;
974 let attempt = authority.begin_surface_resolution();
975 if !attempt.allows(expected) {
976 self.fail_input_authority_mismatch(input_id, attempt, expected);
977 return;
978 }
979 self.resolve_callback_pending(
980 input_id,
981 tool_name,
982 args,
983 Self::cleanup_from_realized_attempt(attempt),
984 );
985 }
986
987 #[cfg(test)]
989 fn resolve_cancelled(
990 &mut self,
991 input_id: &InputId,
992 cleanup_observation: CompletionCleanupObservation,
993 ) {
994 if let Some(senders) = self.take_waiters(input_id) {
995 Self::send_outcome(senders, CompletionOutcome::Cancelled, cleanup_observation);
996 }
997 }
998
999 #[cfg(test)]
1000 pub(crate) fn resolve_cancelled_authorized(
1001 &mut self,
1002 input_id: &InputId,
1003 authority: RuntimeCompletionResultAuthority,
1004 ) {
1005 let expected = RuntimeCompletionResultClass::Cancelled;
1006 let attempt = authority.begin_surface_resolution();
1007 if !attempt.allows(expected) {
1008 self.fail_input_authority_mismatch(input_id, attempt, expected);
1009 return;
1010 }
1011 self.resolve_cancelled(input_id, Self::cleanup_from_realized_attempt(attempt));
1012 }
1013
1014 #[cfg(test)]
1016 fn resolve_abandoned_with_error(
1017 &mut self,
1018 input_id: &InputId,
1019 reason: String,
1020 error: TurnErrorMetadata,
1021 cleanup_observation: CompletionCleanupObservation,
1022 ) {
1023 if let Some(senders) = self.take_waiters(input_id) {
1024 Self::send_outcome(
1025 senders,
1026 CompletionOutcome::AbandonedWithError { reason, error },
1027 cleanup_observation,
1028 );
1029 }
1030 }
1031
1032 #[cfg(test)]
1033 pub(crate) fn resolve_abandoned_with_error_authorized(
1034 &mut self,
1035 input_id: &InputId,
1036 reason: String,
1037 error: TurnErrorMetadata,
1038 authority: RuntimeCompletionResultAuthority,
1039 ) {
1040 let expected = RuntimeCompletionResultClass::AbandonedWithError;
1041 let attempt = authority.begin_surface_resolution();
1042 if !attempt.allows(expected) {
1043 self.fail_input_authority_mismatch(input_id, attempt, expected);
1044 return;
1045 }
1046 self.resolve_abandoned_with_error(
1047 input_id,
1048 reason,
1049 error,
1050 Self::cleanup_from_realized_attempt(attempt),
1051 );
1052 }
1053
1054 #[cfg(test)]
1057 fn resolve_completed_with_finalization_failure(
1058 &mut self,
1059 input_id: &InputId,
1060 error: TurnErrorMetadata,
1061 cleanup_observation: CompletionCleanupObservation,
1062 ) {
1063 if let Some(senders) = self.take_waiters(input_id) {
1064 Self::send_outcome(
1065 senders,
1066 CompletionOutcome::CompletedWithFinalizationFailure { error },
1067 cleanup_observation,
1068 );
1069 }
1070 }
1071
1072 #[cfg(test)]
1073 pub(crate) fn resolve_completed_with_finalization_failure_authorized(
1074 &mut self,
1075 input_id: &InputId,
1076 error: TurnErrorMetadata,
1077 authority: RuntimeCompletionResultAuthority,
1078 ) {
1079 let expected = RuntimeCompletionResultClass::CompletedWithFinalizationFailure;
1080 let attempt = authority.begin_surface_resolution();
1081 if !attempt.allows(expected) {
1082 self.fail_input_authority_mismatch(input_id, attempt, expected);
1083 return;
1084 }
1085 self.resolve_completed_with_finalization_failure(
1086 input_id,
1087 error,
1088 Self::cleanup_from_realized_attempt(attempt),
1089 );
1090 }
1091
1092 #[cfg(test)]
1098 pub(crate) fn resolve_all_runtime_terminated(
1099 &mut self,
1100 reason: &str,
1101 authority: RuntimeCompletionResultAuthority,
1102 ) {
1103 let expected = RuntimeCompletionResultClass::RuntimeTerminated;
1104 let attempt = authority.begin_surface_resolution();
1105 if !attempt.allows(expected) {
1106 let error = Self::authority_mismatch_error(&attempt, expected);
1107 attempt.fail();
1108 self.fail_all_waiters(error);
1109 return;
1110 }
1111 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
1112 for (_, senders) in self.waiters.drain() {
1113 Self::send_outcome(
1114 senders,
1115 CompletionOutcome::runtime_terminated(reason),
1116 cleanup_observation.clone(),
1117 );
1118 }
1119 }
1120
1121 #[cfg(test)]
1122 pub(crate) fn resolve_inputs_runtime_terminated<I>(
1123 &mut self,
1124 input_ids: I,
1125 reason: &str,
1126 authority: RuntimeCompletionResultAuthority,
1127 ) where
1128 I: IntoIterator<Item = InputId>,
1129 {
1130 let input_ids: Vec<InputId> = input_ids.into_iter().collect();
1131 let expected = RuntimeCompletionResultClass::RuntimeTerminated;
1132 let attempt = authority.begin_surface_resolution();
1133 if !attempt.allows(expected) {
1134 self.fail_inputs_authority_mismatch(input_ids, attempt, expected);
1135 return;
1136 }
1137 let cleanup_observation = Self::cleanup_from_realized_attempt(attempt);
1138 for input_id in input_ids {
1139 if let Some(senders) = self.take_waiters(&input_id) {
1140 Self::send_outcome(
1141 senders,
1142 CompletionOutcome::runtime_terminated(reason),
1143 cleanup_observation.clone(),
1144 );
1145 }
1146 }
1147 }
1148
1149 pub(crate) fn fail_all_waiters(&mut self, error: CompletionWaitError) {
1150 for (_, senders) in self.waiters.drain() {
1151 Self::send_error(senders, error.clone());
1152 }
1153 }
1154
1155 pub(crate) fn fail_inputs<I>(&mut self, input_ids: I, error: CompletionWaitError)
1156 where
1157 I: IntoIterator<Item = InputId>,
1158 {
1159 for input_id in input_ids {
1160 if let Some(senders) = self.take_waiters(&input_id) {
1161 Self::send_error(senders, error.clone());
1162 }
1163 }
1164 }
1165
1166 pub(crate) fn fail_not_pending_waiters<F>(
1172 &mut self,
1173 mut is_still_pending: F,
1174 error: CompletionWaitError,
1175 ) where
1176 F: FnMut(&InputId) -> bool,
1177 {
1178 self.waiters.retain(|input_id, senders| {
1179 if is_still_pending(input_id) {
1180 return true;
1181 }
1182 Self::send_error(std::mem::take(senders), error.clone());
1183 false
1184 });
1185 }
1186
1187 pub(crate) fn diagnostic_snapshot(&self) -> CompletionRegistrySnapshot {
1189 let mut waiting_inputs: Vec<_> = self
1190 .waiters
1191 .iter()
1192 .map(|(input_id, senders)| CompletionWaiterEntrySnapshot {
1193 input_id: input_id.clone(),
1194 waiter_count: senders.len(),
1195 })
1196 .collect();
1197 waiting_inputs
1198 .sort_by(|left, right| left.input_id.to_string().cmp(&right.input_id.to_string()));
1199
1200 CompletionRegistrySnapshot {
1201 input_count: waiting_inputs.len(),
1202 waiter_count: waiting_inputs.iter().map(|entry| entry.waiter_count).sum(),
1203 waiting_inputs,
1204 }
1205 }
1206
1207 #[cfg(test)]
1212 pub fn debug_has_waiters(&self) -> bool {
1213 !self.waiters.is_empty()
1214 }
1215
1216 #[cfg(test)]
1221 pub fn debug_waiter_count(&self) -> usize {
1222 self.waiters.values().map(Vec::len).sum()
1223 }
1224}
1225
1226#[cfg(test)]
1227#[allow(clippy::unwrap_used, clippy::panic)]
1228mod tests {
1229 use super::*;
1230 use crate::meerkat_machine::dsl::{
1231 RuntimeCompletionObservedOutcome, RuntimeCompletionResultClass,
1232 };
1233 use meerkat_core::types::{SessionId, Usage};
1234
1235 fn make_run_result() -> RunResult {
1236 RunResult {
1237 text: "hello".into(),
1238 session_id: SessionId::new(),
1239 usage: Usage::default(),
1240 turns: 1,
1241 tool_calls: 0,
1242 terminal_cause_kind: None,
1243 structured_output: None,
1244 extraction_error: None,
1245 schema_warnings: None,
1246 skill_diagnostics: None,
1247 }
1248 }
1249
1250 fn authority(
1251 result_class: RuntimeCompletionResultClass,
1252 cleanup_observation: RuntimeCompletionObservedOutcome,
1253 ) -> RuntimeCompletionResultAuthority {
1254 crate::meerkat_machine::driver::test_runtime_completion_authority(
1255 result_class,
1256 cleanup_observation,
1257 )
1258 }
1259
1260 #[tokio::test]
1261 async fn register_and_complete() {
1262 let mut registry = CompletionRegistry::new();
1263 let input_id = InputId::new();
1264 let handle = registry.register(input_id.clone());
1265
1266 assert!(registry.debug_has_waiters());
1267 assert_eq!(registry.debug_waiter_count(), 1);
1268
1269 let result = make_run_result();
1270 registry.resolve_completed_authorized(
1271 &input_id,
1272 result,
1273 authority(
1274 RuntimeCompletionResultClass::Completed,
1275 RuntimeCompletionObservedOutcome::Completed,
1276 ),
1277 );
1278
1279 match handle.wait_authorized().await {
1280 CompletionOutcome::Completed(r) => assert_eq!(r.text, "hello"),
1281 other => panic!("Expected Completed, got {other:?}"),
1282 }
1283 }
1284
1285 #[tokio::test]
1286 async fn register_and_fail_waiter() {
1287 let mut registry = CompletionRegistry::new();
1288 let input_id = InputId::new();
1289 let handle = registry.register(input_id.clone());
1290
1291 registry.fail_inputs(
1292 [input_id],
1293 CompletionWaitError::AuthorityUnavailable("retired".into()),
1294 );
1295
1296 match handle.try_wait().await {
1297 Err(CompletionWaitError::AuthorityUnavailable(reason)) => assert_eq!(reason, "retired"),
1298 other => panic!("Expected wait error, got {other:?}"),
1299 }
1300 }
1301
1302 #[tokio::test]
1303 async fn mismatched_result_authority_fails_waiter_closed() {
1304 let mut registry = CompletionRegistry::new();
1305 let input_id = InputId::new();
1306 let handle = registry.register(input_id.clone());
1307
1308 registry.resolve_completed_authorized(
1309 &input_id,
1310 make_run_result(),
1311 authority(
1312 RuntimeCompletionResultClass::Cancelled,
1313 RuntimeCompletionObservedOutcome::Cancelled,
1314 ),
1315 );
1316
1317 assert!(!registry.debug_has_waiters());
1318 match handle.try_wait().await {
1319 Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1320 assert!(reason.contains("Cancelled"));
1321 assert!(reason.contains("Completed"));
1322 }
1323 other => panic!("Expected authority mismatch wait error, got {other:?}"),
1324 }
1325 }
1326
1327 #[tokio::test]
1328 async fn resolve_all_runtime_terminated() {
1329 let mut registry = CompletionRegistry::new();
1330 let h1 = registry.register(InputId::new());
1331 let h2 = registry.register(InputId::new());
1332
1333 registry.resolve_all_runtime_terminated(
1334 "runtime stopped",
1335 authority(
1336 RuntimeCompletionResultClass::RuntimeTerminated,
1337 RuntimeCompletionObservedOutcome::RuntimeTerminated,
1338 ),
1339 );
1340
1341 assert!(!registry.debug_has_waiters());
1342
1343 match h1.wait_authorized().await {
1344 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1345 assert_eq!(reason, "runtime stopped");
1346 }
1347 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1348 }
1349 match h2.wait_authorized().await {
1350 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1351 assert_eq!(reason, "runtime stopped");
1352 }
1353 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1354 }
1355 }
1356
1357 #[tokio::test]
1358 async fn runtime_terminal_bundle_preserves_one_exact_non_default_reason() {
1359 let input_id = InputId::new();
1360 let interaction_id = meerkat_core::interaction::InteractionId(input_id.0);
1361 let reason = "runtime destroyed by operator blue/17";
1362 let bundle = authorize_runtime_terminal_bundle(
1363 &[interaction_id],
1364 None,
1365 authority(
1366 RuntimeCompletionResultClass::RuntimeTerminated,
1367 RuntimeCompletionObservedOutcome::RuntimeTerminated,
1368 ),
1369 None,
1370 Some(reason),
1371 )
1372 .expect("generated runtime termination should authorize one exact terminal bundle");
1373
1374 assert!(matches!(
1375 bundle.interaction_events(),
1376 [meerkat_core::event::AgentEvent::InteractionFailed {
1377 interaction_id: emitted_id,
1378 reason: meerkat_core::event::InteractionFailureReason::Abandoned { detail },
1379 }] if *emitted_id == interaction_id && detail == reason
1380 ));
1381
1382 let mut registry = CompletionRegistry::new();
1383 let handle = registry.register(input_id.clone());
1384 registry.resolve_authorized_runtime_terminal_bundle([input_id], bundle);
1385 match handle.wait_authorized().await {
1386 CompletionOutcome::RuntimeTerminated {
1387 reason: waiter_reason,
1388 error,
1389 } => {
1390 assert_eq!(waiter_reason, reason);
1391 assert_eq!(error.detail.as_deref(), Some(reason));
1392 }
1393 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1394 }
1395 }
1396
1397 #[tokio::test]
1398 async fn mismatched_runtime_terminated_authority_fails_all_waiters_closed() {
1399 let mut registry = CompletionRegistry::new();
1400 let h1 = registry.register(InputId::new());
1401 let h2 = registry.register(InputId::new());
1402
1403 registry.resolve_all_runtime_terminated(
1404 "runtime stopped",
1405 authority(
1406 RuntimeCompletionResultClass::CompletedWithoutResult,
1407 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1408 ),
1409 );
1410
1411 assert!(!registry.debug_has_waiters());
1412 for handle in [h1, h2] {
1413 match handle.try_wait().await {
1414 Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1415 assert!(reason.contains("CompletedWithoutResult"));
1416 assert!(reason.contains("RuntimeTerminated"));
1417 }
1418 other => panic!("Expected authority mismatch wait error, got {other:?}"),
1419 }
1420 }
1421 }
1422
1423 #[tokio::test]
1424 async fn cleanup_rejects_observation_from_another_session() {
1425 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1426 let source_session_id = SessionId::new();
1427 let target_session_id = SessionId::new();
1428 adapter
1429 .prepare_bindings(source_session_id.clone())
1430 .await
1431 .expect("source session should prepare runtime bindings");
1432 adapter
1433 .prepare_bindings(target_session_id.clone())
1434 .await
1435 .expect("target session should prepare runtime bindings");
1436
1437 let input = crate::Input::Prompt(crate::PromptInput::new(
1438 "source session pending completion",
1439 None,
1440 ));
1441 let (_outcome, handle) = adapter
1442 .accept_input_with_completion(&source_session_id, input)
1443 .await
1444 .expect("source input should be accepted");
1445 let handle = handle.expect("source input should have a completion waiter");
1446 adapter
1447 .stop_runtime_executor(&source_session_id, "source stopped")
1448 .await
1449 .expect("source stop should resolve waiter");
1450 let (_outcome, observation) = handle
1451 .try_wait_with_cleanup_observation()
1452 .await
1453 .expect("waiter should resolve with generated cleanup observation");
1454
1455 assert_eq!(observation.owner_session_id(), &source_session_id);
1456 let err = adapter
1457 .resolve_runtime_completion_cleanup(
1458 &target_session_id,
1459 observation,
1460 false,
1461 crate::meerkat_machine::dsl::RuntimeCompletionLiveSessionObservation::Absent,
1462 )
1463 .await
1464 .expect_err("cleanup must reject an observation minted for another session");
1465 assert!(
1466 matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1467 "expected generated cleanup validation failure, got {err:?}"
1468 );
1469 }
1470
1471 #[tokio::test]
1472 async fn cleanup_rejects_stale_same_session_observation_after_rebinding() {
1473 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1474 let session_id = SessionId::new();
1475 adapter
1476 .prepare_bindings(session_id.clone())
1477 .await
1478 .expect("session should prepare initial runtime bindings");
1479
1480 let input = crate::Input::Prompt(crate::PromptInput::new(
1481 "same session pending completion",
1482 None,
1483 ));
1484 let (_outcome, handle) = adapter
1485 .accept_input_with_completion(&session_id, input)
1486 .await
1487 .expect("input should be accepted");
1488 let handle = handle.expect("input should have a completion waiter");
1489 adapter
1490 .stop_runtime_executor(&session_id, "first runtime stopped")
1491 .await
1492 .expect("stop should resolve waiter");
1493 let (_outcome, stale_observation) = handle
1494 .try_wait_with_cleanup_observation()
1495 .await
1496 .expect("waiter should resolve with generated cleanup observation");
1497
1498 adapter
1499 .unregister_session(&session_id)
1500 .await
1501 .expect("initial runtime binding should unregister cleanly");
1502 adapter
1503 .prepare_bindings(session_id.clone())
1504 .await
1505 .expect("session should prepare replacement runtime bindings");
1506
1507 let err = adapter
1508 .resolve_runtime_completion_cleanup(
1509 &session_id,
1510 stale_observation,
1511 false,
1512 crate::meerkat_machine::dsl::RuntimeCompletionLiveSessionObservation::Absent,
1513 )
1514 .await
1515 .expect_err("cleanup must reject an observation minted for a prior runtime binding");
1516 assert!(
1517 matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1518 "expected generated cleanup validation failure, got {err:?}"
1519 );
1520 }
1521
1522 #[tokio::test]
1523 async fn wait_failure_authority_releases_pre_admission_and_classifies_public_reason() {
1524 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1525 let session_id = SessionId::new();
1526 adapter
1527 .prepare_bindings(session_id.clone())
1528 .await
1529 .expect("session should prepare runtime bindings");
1530
1531 let authority = adapter
1532 .resolve_runtime_completion_wait_failure(
1533 &session_id,
1534 &CompletionWaitError::AuthorityUnavailable("missing generated result".into()),
1535 )
1536 .await
1537 .expect("wait-failure authority should resolve");
1538
1539 assert!(authority.releases_pre_admission());
1540 assert_eq!(
1541 authority.public_error_class,
1542 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailurePublicErrorClass::InternalError
1543 );
1544 assert_eq!(
1545 authority.public_reason,
1546 crate::meerkat_machine::dsl::RuntimeCompletionWaitFailurePublicReason::CompletionAuthorityUnavailable
1547 );
1548 assert!(!authority.resumable);
1549 }
1550
1551 #[tokio::test]
1552 async fn wait_failure_authority_rejects_missing_session_authority() {
1553 let adapter = crate::meerkat_machine::MeerkatMachine::ephemeral();
1554 let session_id = SessionId::new();
1555
1556 let err = adapter
1557 .resolve_runtime_completion_wait_failure(
1558 &session_id,
1559 &CompletionWaitError::ChannelClosed,
1560 )
1561 .await
1562 .expect_err("wait-failure authority must fail closed without a session authority");
1563
1564 assert!(
1565 matches!(err, crate::RuntimeDriverError::ValidationFailed { .. }),
1566 "expected generated wait-failure validation failure, got {err:?}"
1567 );
1568 }
1569
1570 #[tokio::test]
1571 async fn resolve_nonexistent_is_a_noop() {
1572 let mut registry = CompletionRegistry::new();
1573 registry.resolve_completed_authorized(
1574 &InputId::new(),
1575 make_run_result(),
1576 authority(
1577 RuntimeCompletionResultClass::Completed,
1578 RuntimeCompletionObservedOutcome::Completed,
1579 ),
1580 );
1581 registry.fail_inputs(
1582 [InputId::new()],
1583 CompletionWaitError::AuthorityUnavailable("gone".into()),
1584 );
1585 assert!(!registry.debug_has_waiters());
1586 }
1587
1588 #[tokio::test]
1589 async fn dropped_sender_gives_wait_error() {
1590 let mut registry = CompletionRegistry::new();
1591 let input_id = InputId::new();
1592 let handle = registry.register(input_id);
1593
1594 drop(registry);
1596
1597 assert!(matches!(
1598 handle.try_wait().await,
1599 Err(CompletionWaitError::ChannelClosed)
1600 ));
1601 }
1602
1603 #[tokio::test]
1604 async fn multi_waiter_all_receive_result() {
1605 let mut registry = CompletionRegistry::new();
1606 let input_id = InputId::new();
1607
1608 let h1 = registry.register(input_id.clone());
1609 let h2 = registry.register(input_id.clone());
1610 let h3 = registry.register(input_id.clone());
1611
1612 assert_eq!(registry.debug_waiter_count(), 3);
1613
1614 let result = make_run_result();
1615 registry.resolve_completed_authorized(
1616 &input_id,
1617 result,
1618 authority(
1619 RuntimeCompletionResultClass::Completed,
1620 RuntimeCompletionObservedOutcome::Completed,
1621 ),
1622 );
1623
1624 assert!(!registry.debug_has_waiters());
1625
1626 for handle in [h1, h2, h3] {
1627 match handle.wait_authorized().await {
1628 CompletionOutcome::Completed(r) => assert_eq!(r.text, "hello"),
1629 other => panic!("Expected Completed, got {other:?}"),
1630 }
1631 }
1632 }
1633
1634 #[tokio::test]
1635 async fn resolve_without_result_sends_variant() {
1636 let mut registry = CompletionRegistry::new();
1637 let input_id = InputId::new();
1638 let handle = registry.register(input_id.clone());
1639
1640 registry.resolve_without_result_authorized(
1641 &input_id,
1642 authority(
1643 RuntimeCompletionResultClass::CompletedWithoutResult,
1644 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1645 ),
1646 );
1647
1648 match handle.wait_authorized().await {
1649 CompletionOutcome::CompletedWithoutResult => {}
1650 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1651 }
1652 }
1653
1654 #[tokio::test]
1655 async fn resolve_without_result_multi_waiter() {
1656 let mut registry = CompletionRegistry::new();
1657 let input_id = InputId::new();
1658 let h1 = registry.register(input_id.clone());
1659 let h2 = registry.register(input_id.clone());
1660
1661 registry.resolve_without_result_authorized(
1662 &input_id,
1663 authority(
1664 RuntimeCompletionResultClass::CompletedWithoutResult,
1665 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1666 ),
1667 );
1668
1669 for handle in [h1, h2] {
1670 match handle.wait_authorized().await {
1671 CompletionOutcome::CompletedWithoutResult => {}
1672 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1673 }
1674 }
1675 }
1676
1677 #[tokio::test]
1678 async fn resolve_callback_pending_sends_variant() {
1679 let mut registry = CompletionRegistry::new();
1680 let input_id = InputId::new();
1681 let handle = registry.register(input_id.clone());
1682
1683 registry.resolve_callback_pending_authorized(
1684 &input_id,
1685 "browser".to_string(),
1686 serde_json::json!({ "url": "https://example.com" }),
1687 authority(
1688 RuntimeCompletionResultClass::CallbackPending,
1689 RuntimeCompletionObservedOutcome::CallbackPending,
1690 ),
1691 );
1692
1693 match handle.wait_authorized().await {
1694 CompletionOutcome::CallbackPending { tool_name, args } => {
1695 assert_eq!(tool_name, "browser");
1696 assert_eq!(args, serde_json::json!({ "url": "https://example.com" }));
1697 }
1698 other => panic!("Expected CallbackPending, got {other:?}"),
1699 }
1700 }
1701
1702 #[tokio::test]
1703 async fn resolve_cancelled_sends_variant() {
1704 let mut registry = CompletionRegistry::new();
1705 let input_id = InputId::new();
1706 let handle = registry.register(input_id.clone());
1707
1708 registry.resolve_cancelled_authorized(
1709 &input_id,
1710 authority(
1711 RuntimeCompletionResultClass::Cancelled,
1712 RuntimeCompletionObservedOutcome::Cancelled,
1713 ),
1714 );
1715
1716 match handle.wait_authorized().await {
1717 CompletionOutcome::Cancelled => {}
1718 other => panic!("Expected Cancelled, got {other:?}"),
1719 }
1720 }
1721
1722 #[tokio::test]
1723 async fn already_resolved_handle() {
1724 let handle = CompletionHandle::already_completed_without_result()
1725 .expect("generated completion authority should classify no-result completion");
1726 match handle.wait_authorized().await {
1727 CompletionOutcome::CompletedWithoutResult => {}
1728 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1729 }
1730 }
1731
1732 #[tokio::test]
1733 async fn outcome_cleanup_observes_and_relays_result() {
1734 use std::sync::Arc;
1735 use std::sync::atomic::{AtomicBool, Ordering};
1736
1737 let mut registry = CompletionRegistry::new();
1738 let input_id = InputId::new();
1739 let handle = registry.register(input_id.clone());
1740 let observed = Arc::new(AtomicBool::new(false));
1741 let cleanup_observed = Arc::clone(&observed);
1742 let handle = handle.with_outcome_cleanup(move |observation| async move {
1743 if observation.observed_outcome()
1744 == crate::meerkat_machine::dsl::RuntimeCompletionObservedOutcome::CompletedWithoutResult
1745 {
1746 cleanup_observed.store(true, Ordering::Release);
1747 }
1748 });
1749
1750 registry.resolve_without_result_authorized(
1751 &input_id,
1752 authority(
1753 RuntimeCompletionResultClass::CompletedWithoutResult,
1754 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1755 ),
1756 );
1757 match handle.wait_authorized().await {
1758 CompletionOutcome::CompletedWithoutResult => {}
1759 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1760 }
1761 assert!(observed.load(Ordering::Acquire));
1762 }
1763
1764 #[tokio::test]
1765 async fn resultful_completion_cleanup_withholds_success_and_combines_failures() {
1766 let handle = CompletionHandle::already_completed_without_result()
1767 .expect("generated completion authority should classify no-result completion")
1768 .with_resultful_completion_cleanup(|_| async {
1769 Err(CompletionWaitError::AuthorityUnavailable(
1770 "required cleanup failed".to_string(),
1771 ))
1772 });
1773 let error = handle
1774 .wait()
1775 .await
1776 .expect_err("required cleanup failure must withhold success");
1777 assert!(error.to_string().contains("required cleanup failed"));
1778
1779 let (tx, rx) = oneshot::channel();
1780 drop(tx);
1781 let handle =
1782 CompletionHandle { rx }.with_resultful_completion_cleanup(|completion| async move {
1783 assert!(matches!(
1784 completion,
1785 Err(CompletionWaitError::ChannelClosed)
1786 ));
1787 Err(CompletionWaitError::AuthorityUnavailable(
1788 "cleanup authority failed".to_string(),
1789 ))
1790 });
1791 let error = handle
1792 .wait()
1793 .await
1794 .expect_err("two failures must remain a failure");
1795 let rendered = error.to_string();
1796 assert!(rendered.contains("completion channel closed"));
1797 assert!(rendered.contains("cleanup authority failed"));
1798 }
1799
1800 #[tokio::test]
1801 async fn multi_waiter_terminated_on_reset() {
1802 let mut registry = CompletionRegistry::new();
1803 let input_id = InputId::new();
1804 let h1 = registry.register(input_id.clone());
1805 let h2 = registry.register(input_id);
1806
1807 registry.resolve_all_runtime_terminated(
1808 "runtime reset",
1809 authority(
1810 RuntimeCompletionResultClass::RuntimeTerminated,
1811 RuntimeCompletionObservedOutcome::RuntimeTerminated,
1812 ),
1813 );
1814
1815 for handle in [h1, h2] {
1816 match handle.wait_authorized().await {
1817 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1818 assert_eq!(reason, "runtime reset");
1819 }
1820 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1821 }
1822 }
1823 }
1824
1825 #[tokio::test]
1826 async fn fail_not_pending_keeps_pending_waiters_without_claiming_a_terminal() {
1827 let mut registry = CompletionRegistry::new();
1828 let keep_id = InputId::new();
1829 let drop_id = InputId::new();
1830
1831 let keep_handle = registry.register(keep_id.clone());
1832 let drop_handle = registry.register(drop_id.clone());
1833 registry.fail_not_pending_waiters(
1834 |input_id| input_id == &keep_id,
1835 CompletionWaitError::AuthorityUnavailable(
1836 "recycled input no longer pending".to_string(),
1837 ),
1838 );
1839 assert_eq!(registry.debug_waiter_count(), 1);
1840
1841 match drop_handle.try_wait().await {
1842 Err(CompletionWaitError::AuthorityUnavailable(reason)) => {
1843 assert_eq!(reason, "recycled input no longer pending");
1844 }
1845 other => panic!("Expected reconciliation plumbing failure, got {other:?}"),
1846 }
1847
1848 registry.resolve_without_result_authorized(
1849 &keep_id,
1850 authority(
1851 RuntimeCompletionResultClass::CompletedWithoutResult,
1852 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1853 ),
1854 );
1855 match keep_handle.wait_authorized().await {
1856 CompletionOutcome::CompletedWithoutResult => {}
1857 other => panic!("Expected CompletedWithoutResult, got {other:?}"),
1858 }
1859 }
1860
1861 #[tokio::test]
1862 async fn resolve_without_result_nonexistent_is_a_noop() {
1863 let mut registry = CompletionRegistry::new();
1864 registry.resolve_without_result_authorized(
1865 &InputId::new(),
1866 authority(
1867 RuntimeCompletionResultClass::CompletedWithoutResult,
1868 RuntimeCompletionObservedOutcome::CompletedWithoutResult,
1869 ),
1870 );
1871 assert!(!registry.debug_has_waiters());
1872 }
1873
1874 #[test]
1875 fn abandoned_carries_typed_error_metadata() {
1876 let error = TurnErrorMetadata::runtime_apply_failure("apply blew up");
1877 let outcome = CompletionOutcome::Abandoned {
1878 reason: "abandoned".into(),
1879 error: error.clone(),
1880 };
1881 assert_eq!(outcome.abandoned_reason(), Some("abandoned"));
1882 assert_eq!(outcome.error_metadata(), Some(&error));
1883 }
1884
1885 #[test]
1886 fn runtime_terminated_carries_typed_error_metadata() {
1887 let outcome = CompletionOutcome::runtime_terminated("runtime stopped");
1888 match &outcome {
1889 CompletionOutcome::RuntimeTerminated { reason, .. } => {
1890 assert_eq!(reason, "runtime stopped");
1891 }
1892 other => panic!("Expected RuntimeTerminated, got {other:?}"),
1893 }
1894 let metadata = outcome
1895 .error_metadata()
1896 .expect("RuntimeTerminated must carry typed turn error metadata");
1897 assert_eq!(metadata.kind, TurnTerminalCauseKind::FatalFailure);
1898 assert_eq!(metadata.outcome, Some(TurnTerminalOutcome::Failed));
1899 assert!(metadata.terminal);
1900 assert_eq!(metadata.detail.as_deref(), Some("runtime stopped"));
1901 }
1902}