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