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