Skip to main content

meerkat_runtime/
completion.rs

1//! Input completion waiters — allows callers to await terminal outcome of an accepted input.
2//!
3//! When a surface accepts an input via the runtime, it can optionally receive a
4//! `CompletionHandle` that resolves when the input reaches a terminal state
5//! (Consumed or Abandoned). This bridges the async accept/await pattern needed
6//! for surfaces that want synchronous-feeling turn execution through the runtime.
7//!
8//! `CompletionRegistry` is waiter plumbing only. Production code must never
9//! treat waiter presence, waiter counts, or sender membership as semantic
10//! runtime truth.
11
12use 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/// Mechanical failure while waiting for completion plumbing.
31///
32/// This is intentionally separate from [`CompletionOutcome`]: a closed waiter
33/// channel or missing generated completion authority is not a public runtime
34/// result class.
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum CompletionWaitError {
37    #[error("completion channel closed without an authorized result")]
38    ChannelClosed,
39    /// The exact executor attachment that admitted this request was replaced
40    /// before its waiter resolved. The old input is durably cancelled; a retry
41    /// is a new request owned by the successor attachment. Replacement
42    /// execution must never be reported as the old request's success.
43    #[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    /// Stable typed predicate for retryable attachment replacement. Surfaces
64    /// must not infer this class from an error string.
65    #[must_use]
66    pub const fn is_attachment_replaced(&self) -> bool {
67        matches!(self, Self::AttachmentReplaced)
68    }
69}
70
71/// Outcome delivered to a completion waiter.
72#[derive(Debug, Clone)]
73pub enum CompletionOutcome {
74    /// The input was successfully consumed and produced a result.
75    Completed(Box<RunResult>),
76    /// The input was consumed but produced no RunResult (e.g. context-append ops).
77    CompletedWithoutResult,
78    /// The input reached a callback boundary and requires external tool
79    /// fulfillment before the turn can continue.
80    CallbackPending {
81        tool_use_id: String,
82        tool_name: String,
83        args: Value,
84    },
85    /// One assistant tool-use batch is waiting on multiple external callbacks.
86    CallbackBatchPending {
87        pending_tool_calls: Vec<meerkat_core::error::PendingCallbackToolCall>,
88    },
89    /// The input reached the canonical cancellation terminal.
90    Cancelled,
91    /// The input was abandoned before completing, carrying typed failure
92    /// metadata so every surface sees the same structured turn error the
93    /// sibling [`AbandonedWithError`](Self::AbandonedWithError) carries.
94    Abandoned {
95        reason: String,
96        error: TurnErrorMetadata,
97    },
98    /// The input was abandoned before completing, with typed failure metadata.
99    AbandonedWithError {
100        reason: String,
101        error: TurnErrorMetadata,
102    },
103    /// The turn produced output, but a later runtime finalization step (the
104    /// durable commit) failed, so the run is NOT durably terminal. The produced
105    /// result is deliberately NOT carried on this outcome: a finalization
106    /// failure must be treated as failure by every surface, never surfaced as a
107    /// usable success result (that would be a false belief of success).
108    CompletedWithFinalizationFailure { error: TurnErrorMetadata },
109    /// The runtime was stopped or destroyed while the input was pending,
110    /// carrying typed failure metadata describing the termination cause.
111    RuntimeTerminated {
112        reason: String,
113        error: TurnErrorMetadata,
114    },
115}
116
117/// Project one generated-authority completion onto the canonical
118/// interaction-scoped terminal event. Both the comms live stream and durable
119/// placed-turn observation use this single mapping so result/failure semantics
120/// cannot drift between surfaces.
121pub 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/// Runtime-minted observation for post-completion cleanup.
203///
204/// Cleanup code can inspect this generated-facing observation, but it cannot
205/// rewrite the public [`CompletionOutcome`] that the waiter received.
206#[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    /// Return whether this runtime-minted observation proves that `session_id`
229    /// reached the machine-owned runtime-termination completion class.
230    ///
231    /// Cleanup relays use this narrow proof when the machine-owned unregister
232    /// saga wins the race and removes the runtime registration after the
233    /// executor's external-only cleanup, before the completion relay inspects
234    /// the registry. It must not be replaced with a generic "registration is
235    /// absent" check: absence alone carries no terminal authority.
236    #[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/// Result carried on completion waiter plumbing after generated authority has
277/// selected both the public result class and cleanup observation.
278#[derive(Debug)]
279struct CompletionDelivery {
280    outcome: CompletionOutcome,
281    cleanup_observation: CompletionCleanupObservation,
282}
283
284/// Snapshot of one input's registered completion waiters.
285///
286/// This is a diagnostic/supporting-carrier view only. Waiter counts are never
287/// semantic runtime truth.
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct CompletionWaiterEntrySnapshot {
290    pub input_id: InputId,
291    pub waiter_count: usize,
292}
293
294/// Diagnostic snapshot of the completion waiter registry.
295///
296/// This makes the carrier explicit for MeerkatMachine mapping work without
297/// promoting waiter plumbing into canonical runtime semantics.
298#[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/// Handle for awaiting the completion of an accepted input.
306#[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    /// Wait for the input to reach a terminal state or report mechanical waiter failure.
319    pub async fn try_wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
320        self.try_wait_delivery()
321            .await
322            .map(|delivery| delivery.outcome)
323    }
324
325    /// Wait for completion and return the generated cleanup observation carried
326    /// with the authorized public outcome.
327    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    /// Wait for the input to reach a terminal state or report mechanical waiter failure.
335    pub async fn wait(self) -> Result<CompletionOutcome, CompletionWaitError> {
336        self.try_wait().await
337    }
338
339    /// Wait for a test handle that is expected to resolve through generated authority.
340    #[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    /// Relay completion through a cleanup future before resolving the returned
348    /// handle. This lets surfaces transfer cleanup ownership immediately after
349    /// accepting runtime work while still returning a completion handle.
350    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    /// Relay completion through a cleanup future that can inspect the outcome.
365    ///
366    /// The cleanup future receives a runtime-minted cleanup observation and
367    /// cannot replace the completion result.
368    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    /// Relay completion through cleanup that can observe either generated
388    /// completion-cleanup evidence or a typed waiter failure.
389    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    /// Relay completion through required, fallible cleanup before publishing
409    /// the outcome. A cleanup failure withholds a successful runtime result;
410    /// when waiter delivery and cleanup both fail, both causes are preserved.
411    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    /// Mint a [`RuntimeTerminated`](Self::RuntimeTerminated) outcome from a
541    /// termination reason, attaching the typed terminal failure metadata every
542    /// surface keys off (runtime stop/destroy is a fatal terminal boundary).
543    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/// One generated-authority terminal decision projected onto every public
707/// consumer of a runtime completion. Interaction events and waiter delivery
708/// must come from this same non-cloneable bundle so no caller can preview the
709/// generated result twice and accidentally publish one class while returning
710/// another.
711#[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
729/// Consume generated completion authority exactly once and create the shared
730/// event/waiter terminal bundle for the whole runtime batch.
731pub(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/// Consume one generated completion authority for the exact directed batch
756/// and project the same authorized outcome onto every interaction ID.
757#[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/// Registry of pending completion waiters, keyed by InputId.
775///
776/// Uses `Vec<Sender>` per InputId to support multiple waiters for the same input
777/// (e.g. dedup of in-flight input registers a second waiter for the same InputId).
778#[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    /// Register a waiter for an input. Returns the handle the caller will await.
863    ///
864    /// Multiple waiters can be registered for the same InputId — all will be
865    /// resolved when the input reaches a terminal state.
866    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    /// Resolve all waiters for a completed input.
873    #[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    /// Mechanically deliver a terminal decision that already consumed the
944    /// generated result authority used for durable interaction publication.
945    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    /// Resolve all waiters for an input that completed without producing a RunResult.
964    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    /// Resolve all waiters for an input that reached a callback boundary.
993    #[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    /// Resolve all waiters for an input that reached the cancellation terminal.
1040    #[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    /// Resolve all waiters for an abandoned input with typed failure metadata.
1067    #[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    /// Resolve all waiters for a turn whose output exists but finalization
1107    /// failed after output production.
1108    #[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    /// Resolve all pending waiters with a termination error.
1145    ///
1146    /// The public termination result class is supplied by generated
1147    /// MeerkatMachine authority; this registry method only fans the authorized
1148    /// class out to waiter channels.
1149    #[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    /// Fail waiter plumbing whose input IDs are no longer pending after a
1219    /// reconciliation that intentionally preserves/requeues semantic work.
1220    /// Recycle/recover must not fabricate a runtime-terminal public outcome:
1221    /// any missing waiter recipient is an internal reconciliation fault, not
1222    /// a newly authorized input terminal.
1223    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    /// Snapshot the current waiter carrier without mutating it.
1240    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    /// Check if there are any pending waiters.
1260    ///
1261    /// Test-only introspection. Production code must treat the registry as
1262    /// waiter plumbing rather than semantic runtime truth.
1263    #[cfg(test)]
1264    pub fn debug_has_waiters(&self) -> bool {
1265        !self.waiters.is_empty()
1266    }
1267
1268    /// Number of pending waiters (total across all InputIds).
1269    ///
1270    /// Test-only introspection. Production code must treat the registry as
1271    /// waiter plumbing rather than semantic runtime truth.
1272    #[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 the registry (and thus the sender)
1684        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}