Skip to main content

switchyard_libsy/core/
algorithm.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The [`Algorithm`] trait and its [`Driver`] — the orchestration contract every
5//! algorithm implements and the offload channel it uses for routing-time model calls.
6
7use std::{
8    collections::HashMap, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc,
9    time::Instant,
10};
11
12use async_trait::async_trait;
13use futures::{FutureExt, Stream, StreamExt};
14use parking_lot::Mutex;
15use serde_json::Value;
16use tokio::sync::{mpsc, oneshot};
17use tokio_stream::wrappers::ReceiverStream;
18use tracing::Instrument;
19
20/// The request/response protocol types come from [`switchyard_protocol`].
21/// [`switchyard_protocol::LlmRequest`] is the normalized request;
22/// [`switchyard_protocol::AggLlmResponse`] is the buffered response;
23/// [`switchyard_protocol::LlmResponseChunk`] is normalized streaming content;
24/// [`switchyard_protocol::LlmResponseStreamEvent`] is its host/algorithm envelope; and
25/// [`switchyard_protocol::LlmResponse`] carries either a live
26/// [`switchyard_protocol::LlmResponseStream`] or the terminal aggregate.
27use switchyard_protocol::{Category, ModelId, Request, Response};
28
29use crate::{DriverError, LibsyError, Result, observability};
30
31/// A boxed, `Send` stream of [`Step`]s — the output of
32/// [`Algorithm::run_stream`]. Boxed so the trait method that produces it keeps
33/// `Arc<dyn Algorithm>` object-safe.
34pub type StepStream = Pin<Box<dyn Stream<Item = Result<Step>> + Send>>;
35
36/// The models one algorithm run may use, grouped by [`Category`]. Within a
37/// category they are ordered best-first.
38///
39/// Delegated sub-agent work gets its own groups, reachable only through
40/// [`Driver::for_subagent`]. Keeping them separate is what stops a sub-agent's
41/// `capable` from resolving to the parent's, and stops the parent falling back
42/// onto a model only its sub-agents were given.
43///
44/// One run's driver clones all read the same value, so it is passed as
45/// `Arc<RuntimeModels>` rather than cloned per driver.
46#[derive(Clone, Debug, Default)]
47pub struct RuntimeModels {
48    /// When using subagents this is the parent agent category.
49    by_category: HashMap<Category, Vec<ModelId>>,
50    subagent: Option<HashMap<Category, Vec<ModelId>>>,
51}
52
53impl RuntimeModels {
54    /// The models available to the algorithm itself.
55    pub fn new(by_category: HashMap<Category, Vec<ModelId>>) -> Self {
56        Self {
57            by_category,
58            subagent: None,
59        }
60    }
61
62    /// Adds the groups used for delegated sub-agent work.
63    pub fn with_subagent(mut self, models: HashMap<Category, Vec<ModelId>>) -> Self {
64        self.subagent = Some(models);
65        self
66    }
67
68    /// The models in `category`, ordered best-first.
69    pub fn models_for(&self, category: &Category) -> &[ModelId] {
70        self.by_category.get(category).map_or(&[], Vec::as_slice)
71    }
72
73    /// The models delegated sub-agent work uses for `category`, ordered best-first.
74    pub fn subagent_models_for(&self, category: &Category) -> &[ModelId] {
75        self.subagent
76            .as_ref()
77            .and_then(|models| models.get(category))
78            .map_or(&[], Vec::as_slice)
79    }
80}
81
82impl From<HashMap<Category, Vec<ModelId>>> for RuntimeModels {
83    fn from(by_category: HashMap<Category, Vec<ModelId>>) -> Self {
84        Self::new(by_category)
85    }
86}
87
88/// Which of a [`RuntimeModels`]' groups a driver reads.
89#[derive(Clone, Copy)]
90enum Scope {
91    Parent,
92    Subagent,
93}
94
95/// An offloaded model call, surfaced inside [`Step::CallModel`].
96///
97/// The host reads the public fields, performs (or delegates) the model call, and fulfills it
98/// with [`respond`](Self::respond) — unblocking the algorithm's [`Driver::call_model`] on the
99/// other side. `switchyard-llm-client`'s `run` is the ready-made consumer that does this for
100/// you.
101///
102/// [`Driver::call_model`] stamps the first candidate model onto the request before publishing
103/// the call. A consumer that falls through to a later candidate must re-stamp it.
104pub struct CallModel {
105    /// The name of the algorithm that produced this call, so a host instrumenting the
106    /// calls it serves can attribute its own spans to the algorithm behind them.
107    pub algorithm: String,
108    /// The request to serve; its `model` is stamped with the first candidate.
109    pub request: Request,
110    /// Candidate models, tried in order until one answers. Never empty.
111    pub models: Vec<ModelId>,
112    /// Return client errors to the algorithm so it can apply its fallback policy.
113    pub recover_errors: bool,
114    /// How to send the response back to the algorithm. `None` once the call is recorded.
115    reply: Option<oneshot::Sender<Result<Response>>>,
116    started: Instant,
117}
118
119impl CallModel {
120    /// Fulfill the promise with the caller's model-call result. Pass `Err(..)` to
121    /// propagate a failed model call back to the algorithm. Consumes the promise: it
122    /// can only be fulfilled once.
123    pub fn respond(mut self, result: Result<Response>) -> Result<()> {
124        self.record(result.is_ok());
125        self.reply
126            .take()
127            .ok_or(DriverError::ResponseDropped)?
128            .send(result)
129            .map_err(|_| DriverError::ResponseDropped.into())
130    }
131
132    /// Record a failed call and return its error to stop [`drive`].
133    /// Leaves the promise unfulfilled so the driver can cancel the algorithm.
134    pub fn fail(mut self, error: LibsyError) -> Result<()> {
135        self.reply = None;
136        self.record(false);
137        Err(error)
138    }
139
140    fn record(&self, is_ok: bool) {
141        observability::record_llm_call(
142            &self.algorithm,
143            self.models
144                .first()
145                .map(ModelId::as_str)
146                .unwrap_or("NoTargets"),
147            self.started.elapsed(),
148            is_ok,
149        );
150    }
151}
152
153impl Drop for CallModel {
154    fn drop(&mut self) {
155        if self.reply.is_some() {
156            self.record(false);
157        }
158    }
159}
160
161/// The terminal result of routing.
162pub struct RoutingOutcome {
163    /// Models selected by the algorithm, ordered best model first.
164    pub selected_model_ids: Vec<ModelId>,
165    /// The request after all routing-time rewrites, stamped with the selected model.
166    pub request: Request,
167    /// A response produced while routing, or `None` when the client must make the answer call.
168    pub response: Option<Response>,
169    /// Outcome identity and optional algorithm evidence.
170    ///
171    /// Constructors leave this empty; [`Algorithm::run_stream`] fills it before publishing a
172    /// successful outcome.
173    pub metadata: Option<crate::OutcomeMetadata>,
174}
175
176impl RoutingOutcome {
177    /// The model the algorithm recommends, the best model for this request.
178    /// `LibsyError::NoTargets` if the algorithm selected no models, which should be impossible.
179    pub fn selected_model_id(&self) -> Result<&ModelId> {
180        self.selected_model_ids.first().ok_or(LibsyError::NoTargets)
181    }
182
183    /// The decision is that client should send this `request`. The `selected_model_id`
184    /// will be written into it by this function.
185    /// If that fails client should try the `fallback_models` in order.
186    pub fn route_to(
187        selected_model_id: ModelId,
188        fallback_models: Vec<ModelId>,
189        mut request: Request,
190    ) -> Self {
191        request.llm_request.model = Some(selected_model_id.to_string());
192        let mut selected_model_ids = Vec::with_capacity(1 + fallback_models.len());
193        selected_model_ids.push(selected_model_id);
194        selected_model_ids.extend(fallback_models);
195        Self {
196            selected_model_ids,
197            request,
198            response: None,
199            metadata: None,
200        }
201    }
202
203    /// Algorithm generated the response as part of the routing decision. Here it is.
204    /// The `request` will have the `selected_model_id` written into it by this function.
205    pub fn answered(selected_model_id: ModelId, mut request: Request, response: Response) -> Self {
206        request.llm_request.model = Some(selected_model_id.to_string());
207        Self {
208            selected_model_ids: vec![selected_model_id],
209            request,
210            response: Some(response),
211            metadata: None,
212        }
213    }
214}
215
216/// How an algorithm's [`route`](Algorithm::route) makes model calls.
217#[derive(Clone)]
218pub struct Driver {
219    step_tx: mpsc::Sender<Result<Step>>,
220
221    /// The owning algorithm's telemetry label, stamped onto every call this driver publishes.
222    algorithm: String,
223
224    /// Run-scoped evidence shared by driver clones and attached only to a successful outcome.
225    evidence: Arc<Mutex<Option<Value>>>,
226
227    /// Every group this run may route over, shared by all driver clones.
228    models: Arc<RuntimeModels>,
229
230    /// Which of those groups this driver reads.
231    scope: Scope,
232}
233
234impl Driver {
235    /// Build an empty driver with its step channel ready. Created per call by
236    /// [`run_stream`](Algorithm::run_stream). Also returns the Step receiver.
237    pub(crate) fn new(
238        algorithm: &str,
239        models: Arc<RuntimeModels>,
240    ) -> (Self, mpsc::Receiver<Result<Step>>) {
241        // Capacity one keeps the algorithm paced by the stream consumer. It limits queued steps,
242        // not model calls already pulled from the stream, which can still run at the same time.
243        // A larger buffer would use more memory and let the algorithm run farther ahead with
244        // little benefit because reading a step is cheap compared with serving a model call.
245        let (step_tx, step_rx) = mpsc::channel(1);
246        (
247            Self {
248                step_tx,
249                algorithm: algorithm.to_string(),
250                evidence: Arc::new(Mutex::new(None)),
251                models,
252                scope: Scope::Parent,
253            },
254            step_rx,
255        )
256    }
257
258    /// Replace the current run's evidence when a component makes the final decision.
259    pub(crate) fn set_evidence(&self, evidence: Value) {
260        *self.evidence.lock() = Some(evidence);
261    }
262
263    /// Supply fallback evidence without replacing a decision made earlier in the cascade.
264    pub(crate) fn set_evidence_if_empty(&self, evidence: Value) {
265        let mut current = self.evidence.lock();
266        if current.is_none() {
267            *current = Some(evidence);
268        }
269    }
270
271    /// Publish a model call and await the consumer's response.
272    ///
273    /// Errors if the stream is closed or the call failed.
274    /// The await is wrapped in a `libsy.llm_call` span measuring *fulfillment* as
275    /// the algorithm observes it (host queueing/serving included; a streamed
276    /// response resolves when its stream handle arrives). The host records call metrics
277    /// through [`CallModel::respond`] or [`CallModel::fail`]; outcome and token usage
278    /// are recorded on the span when the promise resolves. The provider call itself is the
279    /// host's, and is instrumented by whoever makes it.
280    pub async fn call_model(&self, request: Request, models: Vec<ModelId>) -> Result<Response> {
281        self.call_model_with_error_recovery(request, models, false)
282            .await
283    }
284
285    /// Allows a routing policy to handle a failed call when recovery is enabled.
286    #[tracing::instrument(
287        target = "libsy",
288        name = "libsy.llm_call",
289        skip_all,
290        fields(
291            algorithm = self.algorithm,
292            selected_model = %models.first().map(ModelId::as_str).unwrap_or("NoTargets"),
293            openinference.span.kind = "CHAIN",
294            outcome = tracing::field::Empty,
295            input_tokens = tracing::field::Empty,
296            output_tokens = tracing::field::Empty,
297            total_tokens = tracing::field::Empty,
298            reasoning_tokens = tracing::field::Empty,
299        )
300    )]
301    pub(crate) async fn call_model_with_error_recovery(
302        &self,
303        mut request: Request,
304        models: Vec<ModelId>,
305        recover_errors: bool,
306    ) -> Result<Response> {
307        let Some(selected_model_id) = models.first() else {
308            return Err(LibsyError::NoTargets);
309        };
310        request.llm_request.model = Some(selected_model_id.to_string());
311        let started = Instant::now();
312        let (reply, response) = oneshot::channel::<Result<Response>>();
313        let call = CallModel {
314            algorithm: self.algorithm.clone(),
315            request,
316            models,
317            recover_errors,
318            reply: Some(reply),
319            started,
320        };
321        let result = async {
322            self.step_tx
323                .send(Ok(Step::CallModel(Box::new(call))))
324                .await
325                .map_err(|_| DriverError::StreamClosed)?;
326            response
327                .await
328                .map_err(|_| LibsyError::from(DriverError::ResponseDropped))?
329        }
330        .await;
331        observability::record_llm_call_span(&result, &tracing::Span::current());
332        result
333    }
334
335    /// The available models for this category, typically ordered best-first.
336    pub fn models_for(&self, category: &Category) -> &[ModelId] {
337        match self.scope {
338            Scope::Parent => self.models.models_for(category),
339            Scope::Subagent => self.models.subagent_models_for(category),
340        }
341    }
342
343    /// The first available model for `category`.
344    pub fn first_model_for(&self, category: &Category) -> Result<&ModelId> {
345        self.models_for(category)
346            .first()
347            .ok_or_else(|| LibsyError::AlgorithmError {
348                message: format!("no models available for category {}", category.as_str()),
349            })
350    }
351
352    /// A driver scoped to delegated sub-agent work: its categories are the
353    /// sub-agent's own, and the parent's are no longer reachable through it.
354    pub fn for_subagent(&self) -> Result<Self> {
355        if self.models.subagent.is_none() {
356            return Err(LibsyError::AlgorithmError {
357                message: "delegated work has no sub-agent models".to_string(),
358            });
359        }
360        Ok(Self {
361            scope: Scope::Subagent,
362            ..self.clone()
363        })
364    }
365
366    /// Emit the terminal step: [`Step::Done`] on `Ok`, or an `Err` stream
367    /// item on failure. Internal: called once by [`run_stream`](Algorithm::run_stream)
368    /// when the algorithm finishes.
369    pub(crate) async fn finish(&self, result: Result<RoutingOutcome>) -> Result<()> {
370        let result = result.map(|mut outcome| {
371            let metadata = outcome.metadata.get_or_insert_with(|| {
372                crate::OutcomeMetadata::new(self.algorithm.clone(), self.evidence.lock().take())
373            });
374            observability::record_outcome(metadata, &outcome.selected_model_ids);
375            outcome
376        });
377        let selected_model = result
378            .as_ref()
379            .ok()
380            .and_then(|outcome| outcome.selected_model_id().ok().cloned());
381        let step = result.map(|outcome| Step::Done(Box::new(outcome)));
382        self.step_tx
383            .send(step)
384            .await
385            .map_err(|_| DriverError::StreamClosed)?;
386        if let Some(selected_model) = selected_model {
387            observability::record_decision(&self.algorithm, &selected_model);
388        }
389        Ok(())
390    }
391}
392
393/// One item in the stream returned by [`Algorithm::run_stream`].
394pub enum Step {
395    /// The algorithm needs this model call performed. The host serves it and fulfills
396    /// it with [`CallModel::respond`]. Boxed: it is by far the largest variant.
397    CallModel(Box<CallModel>),
398    /// The algorithm finished with its routing outcome — the last step of a run.
399    Done(Box<RoutingOutcome>),
400}
401
402/// Drive [`Algorithm::run_stream`] to completion, handing each offloaded call to `serve`.
403///
404/// Returns the final [`RoutingOutcome`].
405/// `serve` owns the call: it performs it however the host likes and must fulfill the promise
406/// with [`CallModel::respond`]. A failed *model* call belongs in `respond` — the
407/// algorithm may route around it. To stop routing on a model-call failure, return
408/// [`CallModel::fail`] instead. Returning `Err` from `serve` aborts the whole run.
409/// Calls are served concurrently, so an algorithm that offloads several at once (hedging, fan-out)
410/// gets real parallelism.
411///
412/// libsy performs no I/O; this is only the mechanics of consuming its own step stream, kept
413/// here so every host does not reimplement the same loop. `switchyard-llm-client`'s `run`
414/// is this function plus an HTTP client.
415pub async fn drive<F, Fut>(
416    algorithm: Arc<dyn Algorithm>,
417    request: Request,
418    models: Arc<RuntimeModels>,
419    serve: F,
420) -> Result<RoutingOutcome>
421where
422    F: Fn(CallModel) -> Fut,
423    Fut: Future<Output = Result<()>>,
424{
425    let stream = algorithm.run_stream(request, models);
426    tokio::pin!(stream);
427
428    let mut in_flight = futures::stream::FuturesUnordered::new();
429    let mut final_outcome: Option<RoutingOutcome> = None;
430
431    loop {
432        tokio::select! {
433            Some(result) = in_flight.next() => match result {
434                Ok(()) => {}, // CallModel completed successfully
435                Err(err) => return Err(err), // CallModel failed, propagate the error
436            },
437            step = stream.next() => {
438                match step {
439                    None => break, // stream has ended, no more steps
440                    Some(item) => match item? {
441                        Step::CallModel(call) => in_flight.push(serve(*call)),
442                        Step::Done(outcome) => {
443                            final_outcome = Some(*outcome);
444                            break;
445                        }
446                    }
447                }
448            },
449        }
450    }
451    final_outcome.ok_or(LibsyError::MissingFinalResponse)
452}
453
454/// Recover the message from an algorithm's panic.
455fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
456    payload
457        .downcast_ref::<&'static str>()
458        .map(|message| (*message).to_string())
459        .or_else(|| payload.downcast_ref::<String>().cloned())
460        .unwrap_or_else(|| "unknown panic payload".to_string())
461}
462
463/// Abort guard
464struct AbortOnDrop(tokio::task::AbortHandle);
465
466impl Drop for AbortOnDrop {
467    fn drop(&mut self) {
468        self.0.abort();
469    }
470}
471
472/// Errors unless `targets` contains `name`.
473///
474/// Config target names must be resolved before an algorithm is built. This list contains
475/// model IDs, not target names.
476pub(crate) fn ensure_model_is_target(targets: &[ModelId], name: &ModelId) -> Result<()> {
477    targets
478        .iter()
479        .any(|target| target == name)
480        .then_some(())
481        .ok_or_else(|| LibsyError::TargetNotFound {
482            target: name.clone(),
483        })
484}
485
486/// Key for routing affinity: a root request by its session, a child request by its session
487/// and agent.
488#[derive(Clone, Hash, PartialEq, Eq)]
489pub(crate) enum RoutingIdentity {
490    /// Root request, keyed by session ID.
491    Session(String),
492    /// Child request, keyed by session and agent IDs.
493    Subagent { session: String, agent: String },
494}
495
496impl RoutingIdentity {
497    /// Builds a root or child identity from non-empty request metadata.
498    ///
499    /// A child request missing either ID returns `None`, so it keeps no routing history
500    /// rather than sharing the parent's.
501    pub(crate) fn from_request(request: &Request) -> Option<Self> {
502        let metadata = request.metadata.as_ref()?;
503        let session = metadata.session_id.as_deref().filter(|id| !id.is_empty())?;
504        if metadata.is_subagent {
505            let agent = metadata.agent_id.as_deref().filter(|id| !id.is_empty())?;
506            Some(Self::Subagent {
507                session: session.to_string(),
508                agent: agent.to_string(),
509            })
510        } else {
511            Some(Self::Session(session.to_string()))
512        }
513    }
514}
515
516/// An optimization strategy. Implement [`route`](Self::route);
517/// callers drive it with [`run_stream`](Self::run_stream), serving each [`Step::CallModel`]
518/// it emits. `switchyard-llm-client`'s `run` is the ready-made consumer that does this
519/// over HTTP.
520///
521/// Methods take `self: Arc<Self>`: one algorithm (`Arc<dyn Algorithm>`) is shared across
522/// requests and run concurrently, so it owns its thread-safety and any shared state.
523///
524/// # Concurrency
525///
526/// A host may run the same algorithm concurrently for many requests. Implementations
527/// must synchronize their own mutable shared state. Each call to [`run_stream`](Self::run_stream)
528/// creates an independent [`Driver`], so model-call promises and emitted [`Step`]s cannot
529/// cross between runs.
530///
531/// # Observability
532///
533/// [`run_stream`](Self::run_stream) creates a `libsy.run` span, and each offloaded model
534/// call creates a nested `libsy.llm_call` span. Successful outcomes record their
535/// [`OutcomeMetadata::outcome_id`](crate::OutcomeMetadata::outcome_id) on `libsy.run`,
536/// alongside `selected_model_ids` (an ordered OpenTelemetry string array).
537/// `algorithm` and `switchyard.algorithm` retain the run's [`Algorithm::name`].
538/// Optional `evidence.source`, `evidence.verdict`, `evidence.trigger`, and
539/// `evidence.reason_code` are strings; `evidence.score`, `evidence.confidence`, and
540/// `evidence.threshold` are numbers. Unknown evidence fields are not exported.
541/// These fields are span attributes, never metric labels.
542///
543/// The run/call observability helpers retain `outcome` status and operational metrics,
544/// but omit error details and arbitrary request extra metadata. Algorithms and hosts
545/// may emit their own logs. Errors still reach the caller unchanged.
546/// The host controls the tracing subscriber and global OpenTelemetry
547/// meter provider; libsy installs no exporter and performs no telemetry network I/O.
548#[async_trait]
549pub trait Algorithm: Send + Sync + 'static {
550    /// Stable, low-cardinality name identifying this algorithm — the
551    /// `algorithm` attribute on every span, metric, and log line the crate
552    /// emits for its runs.
553    fn name(&self) -> &str;
554
555    /// Run one request to completion: make routing-time model calls with
556    /// [`Driver::call_model`] and return the terminal [`RoutingOutcome`].
557    /// The method an algorithm implements; [`run_stream`](Self::run_stream) drives it.
558    async fn route(self: Arc<Self>, driver: Driver, request: Request) -> Result<RoutingOutcome>;
559
560    /// Process a request to completion, returning a stream of [`Step`]s.
561    ///
562    /// The consumer must fulfill every [`Step::CallModel`] before the algorithm can
563    /// continue. Every run ends with exactly one terminal item — [`Step::Done`] on
564    /// success, an `Err` item on failure, including when the algorithm panics. Dropping
565    /// the stream aborts the spawned algorithm task.
566    ///
567    /// Every invocation owns a separate [`Driver`].
568    fn run_stream(self: Arc<Self>, request: Request, models: Arc<RuntimeModels>) -> StepStream {
569        let (driver, step_rx) = Driver::new(self.name(), models);
570        let span = observability::run_span(self.name(), &request);
571        let handle = tokio::spawn(
572            async move {
573                let algorithm = self.name().to_string();
574                // Catch a panicking algorithm so the run still publishes a terminal step.
575                let route = AssertUnwindSafe(self.route(driver.clone(), request)).catch_unwind();
576                let result = observability::observe_run(&algorithm, async move {
577                    route.await.unwrap_or_else(|payload| {
578                        Err(LibsyError::AlgorithmError {
579                            message: format!(
580                                "algorithm task panicked: {}",
581                                panic_message(payload.as_ref())
582                            ),
583                        })
584                    })
585                })
586                .await;
587
588                let _ = driver.finish(result).await;
589            }
590            .instrument(span),
591        );
592        // Dropping the stream aborts the algorithm task when its consumer goes away.
593        let abort_guard = AbortOnDrop(handle.abort_handle());
594        Box::pin(ReceiverStream::new(step_rx).map(move |step| {
595            // link abort guard to stream
596            let _keep_alive = &abort_guard;
597            step
598        }))
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use std::collections::HashMap;
605
606    use super::*;
607    use crate::core::testing::{Serve, ServeResult, echo, reply, test_drive};
608    use futures::StreamExt;
609    use switchyard_protocol::{
610        LlmResponse, LlmResponseChunk, completion_text, text_request, text_response,
611    };
612
613    #[derive(Debug, thiserror::Error)]
614    #[error("{0}")]
615    struct TestError(&'static str);
616
617    fn test_error(message: &'static str) -> LibsyError {
618        LibsyError::external("test", TestError(message))
619    }
620
621    /// Trivial algo used only to exercise the orchestrator: calls the first target
622    /// and returns its response as the routing outcome.
623    struct TestAlgo {
624        target_set: Vec<ModelId>,
625    }
626
627    #[async_trait]
628    impl Algorithm for TestAlgo {
629        fn name(&self) -> &str {
630            "test"
631        }
632
633        async fn route(
634            self: Arc<Self>,
635            driver: Driver,
636            request: Request,
637        ) -> Result<RoutingOutcome> {
638            let target = self
639                .target_set
640                .first()
641                .ok_or(LibsyError::NoTargets)?
642                .clone();
643            let response = driver
644                .call_model(request.clone(), vec![target.clone()])
645                .await?;
646            driver.set_evidence(serde_json::json!({"source": "test"}));
647            driver.set_evidence_if_empty(serde_json::json!({"source": "ignored"}));
648            Ok(RoutingOutcome::answered(target, request, response))
649        }
650    }
651
652    /// Build a shared `TestAlgo` over the given target set.
653    fn orch(target_set: Vec<ModelId>) -> Arc<dyn Algorithm> {
654        Arc::new(TestAlgo { target_set })
655    }
656
657    fn request() -> Request {
658        Request {
659            llm_request: text_request(Some("auto".to_string()), "hi".to_string()),
660            raw_request: None,
661            metadata: None,
662        }
663    }
664
665    #[test]
666    fn routing_outcome_constructors_stamp_selection_and_preserve_payloads() {
667        let outcome = RoutingOutcome::route_to(
668            "selected".into(),
669            target_set(&["fallback-one", "fallback-two"]),
670            request(),
671        );
672
673        assert_eq!(
674            outcome.selected_model_ids,
675            target_set(&["selected", "fallback-one", "fallback-two"])
676        );
677        assert_eq!(outcome.request.model_id().as_deref(), Some("selected"));
678        assert!(outcome.response.is_none());
679        assert!(outcome.metadata.is_none());
680
681        let outcome = RoutingOutcome::route_to("only".into(), Vec::new(), request());
682        assert_eq!(outcome.selected_model_ids, target_set(&["only"]));
683
684        let outcome = RoutingOutcome::answered(
685            "answered".into(),
686            request(),
687            Response {
688                llm_response: LlmResponse::Agg(text_response(None, "existing")),
689                metadata: None,
690                upstream_headers: http::HeaderMap::new(),
691            },
692        );
693
694        assert_eq!(outcome.selected_model_ids, target_set(&["answered"]));
695        assert_eq!(outcome.request.model_id().as_deref(), Some("answered"));
696        assert_eq!(
697            outcome
698                .response
699                .as_ref()
700                .and_then(|response| response.llm_response.as_agg())
701                .map(completion_text),
702            Some("existing".to_string())
703        );
704    }
705
706    fn target_set(names: &[&str]) -> Vec<ModelId> {
707        names.iter().map(|name| ModelId::from(*name)).collect()
708    }
709
710    #[tokio::test]
711    async fn typed_driver_preserves_call_and_stream_boundaries() -> Result<()> {
712        tokio::time::timeout(std::time::Duration::from_secs(1), async {
713            // Distinct oneshots keep reverse-order replies paired with their producers, and a
714            // retained call remains pending until the host responds.
715            let (driver, mut step_rx) = Driver::new("test", Arc::new(RuntimeModels::default()));
716            let first_driver = driver.clone();
717            let mut first = tokio::spawn(async move {
718                first_driver
719                    .call_model(request(), vec![ModelId::from("first")])
720                    .await
721            });
722            let second = tokio::spawn(async move {
723                driver
724                    .call_model(request(), vec![ModelId::from("second")])
725                    .await
726            });
727
728            let mut calls = HashMap::new();
729            for _ in 0..2 {
730                let step = step_rx.recv().await.ok_or(DriverError::StreamClosed)??;
731                let Step::CallModel(call) = step else {
732                    return Err(test_error("expected a CallModel step"));
733                };
734                let selected_model = call
735                    .models
736                    .first()
737                    .ok_or_else(|| test_error("model call has no candidates"))?
738                    .to_string();
739                calls.insert(selected_model, call);
740            }
741            assert!(
742                tokio::time::timeout(std::time::Duration::from_millis(20), &mut first)
743                    .await
744                    .is_err(),
745                "call completed before the host responded"
746            );
747            calls
748                .remove("second")
749                .ok_or_else(|| test_error("missing second call"))?
750                .respond(Ok(reply("second response")))?;
751            calls
752                .remove("first")
753                .ok_or_else(|| test_error("missing first call"))?
754                .respond(Ok(reply("first response")))?;
755
756            let first_response = first
757                .await
758                .map_err(|source| LibsyError::external("joining a test task", source))??;
759            let second_response = second
760                .await
761                .map_err(|source| LibsyError::external("joining a test task", source))??;
762            assert_eq!(
763                first_response.llm_response.as_agg().map(completion_text),
764                Some("first response".to_string())
765            );
766            assert_eq!(
767                second_response.llm_response.as_agg().map(completion_text),
768                Some("second response".to_string())
769            );
770
771            // Dropping the host-facing promise closes only that call's reply channel.
772            let (driver, mut step_rx) = Driver::new("test", Arc::new(RuntimeModels::default()));
773            let producer = tokio::spawn(async move {
774                driver
775                    .call_model(request(), vec![ModelId::from("dropped")])
776                    .await
777            });
778            let step = step_rx.recv().await.ok_or(DriverError::StreamClosed)??;
779            let Step::CallModel(call) = step else {
780                return Err(test_error("expected a CallModel step"));
781            };
782            drop(call);
783            let result = producer
784                .await
785                .map_err(|source| LibsyError::external("joining a test task", source))?;
786            assert!(matches!(
787                result,
788                Err(LibsyError::Driver(DriverError::ResponseDropped))
789            ));
790
791            // A standalone driver reports the typed step receiver disappearing at its next call.
792            let (driver, step_rx) = Driver::new("test", Arc::new(RuntimeModels::default()));
793            drop(step_rx);
794            let result = driver
795                .call_model(request(), vec![ModelId::from("closed")])
796                .await;
797            assert!(matches!(
798                result,
799                Err(LibsyError::Driver(DriverError::StreamClosed))
800            ));
801            Ok(())
802        })
803        .await
804        .map_err(|error| LibsyError::external("waiting for typed driver boundaries", error))?
805    }
806
807    #[test]
808    fn target_lookup_returns_the_missing_target() {
809        let error = ensure_model_is_target(&target_set(&[]), &ModelId::from("missing")).err();
810        assert!(matches!(
811            error,
812            Some(LibsyError::TargetNotFound { target }) if target == "missing"
813        ));
814    }
815
816    /// Build a single-target algo, plus a `serve` that answers it as a token stream
817    /// replaying `chunks` in order (as `Ok` items).
818    fn streaming_orch(chunks: Vec<LlmResponseChunk>) -> (Arc<dyn Algorithm>, impl Serve) {
819        let algo = orch(target_set(&["stream/model"]));
820        let serve = move |_target: ModelId, _request: Request| {
821            let chunks = chunks.clone();
822            async move {
823                let stream =
824                    futures::stream::iter(chunks.into_iter().map(|chunk| Ok(chunk.into()))).boxed();
825                Ok(Response {
826                    llm_response: LlmResponse::Stream(stream),
827                    metadata: None,
828                    upstream_headers: http::HeaderMap::new(),
829                })
830            }
831        };
832        (algo, serve)
833    }
834
835    #[tokio::test]
836    async fn run_returns_a_streamed_response_the_caller_aggregates() -> Result<()> {
837        // A streaming client -> its chunks flow through the promise and `Done`,
838        // and `run` returns the live stream untouched for the caller to fold.
839        let (orch, serve) = streaming_orch(vec![
840            LlmResponseChunk::MessageStart {
841                id: Some("m1".to_string()),
842                model: Some("stream/model".to_string()),
843            },
844            LlmResponseChunk::TextDelta {
845                index: 0,
846                text: "hel".to_string(),
847            },
848            LlmResponseChunk::TextDelta {
849                index: 0,
850                text: "lo".to_string(),
851            },
852            LlmResponseChunk::MessageStop {
853                reason: Some("stop".to_string()),
854            },
855        ]);
856        let (selected_model, response) = test_drive(orch, request(), serve).await?;
857        // The run handed back the live stream; the caller folds it to a buffered aggregate.
858        let agg = response
859            .llm_response
860            .into_agg()
861            .await
862            .map_err(|error| LibsyError::external("aggregating response stream", error))?;
863        assert_eq!(completion_text(&agg), "hello");
864        assert_eq!(agg.model.as_deref(), Some("stream/model"));
865        assert_eq!(selected_model, "stream/model");
866        Ok(())
867    }
868
869    #[tokio::test]
870    async fn aggregating_a_streamed_response_propagates_a_mid_stream_error() -> Result<()> {
871        // The run succeeds and returns the stream; the in-band `Error` chunk surfaces only
872        // when the caller aggregates it.
873        let (orch, serve) = streaming_orch(vec![
874            LlmResponseChunk::TextDelta {
875                index: 0,
876                text: "partial".to_string(),
877            },
878            LlmResponseChunk::StreamError {
879                message: "upstream exploded".to_string(),
880            },
881        ]);
882        let (_, response) = test_drive(orch, request(), serve).await?;
883        match response.llm_response.into_agg().await {
884            Ok(_) => panic!("expected a mid-stream error, got an aggregate"),
885            Err(err) => {
886                assert!(err.to_string().contains("upstream exploded"));
887                Ok(())
888            }
889        }
890    }
891
892    #[tokio::test]
893    async fn run_offloads_via_promise_then_finishes() -> Result<()> {
894        // Every call is offloaded via a promise the orchestrator surfaces as a
895        // `CallModel` step for us to fulfill.
896        let stream = orch(target_set(&["offload/model"]))
897            .run_stream(request(), Arc::new(RuntimeModels::default()));
898        tokio::pin!(stream);
899
900        let mut saw_call = false;
901        let mut final_completion = None;
902        while let Some(step) = stream.next().await {
903            match step? {
904                Step::CallModel(call) => {
905                    saw_call = true;
906                    assert_eq!(call.models, vec![ModelId::from("offload/model")]);
907                    // Fulfilling the promise is the "real" model call the caller makes.
908                    call.respond(Ok(Response {
909                        llm_response: LlmResponse::Agg(text_response(
910                            None,
911                            "fulfilled".to_string(),
912                        )),
913                        metadata: None,
914                        upstream_headers: http::HeaderMap::new(),
915                    }))?;
916                }
917                Step::Done(outcome) => {
918                    let metadata = outcome
919                        .metadata
920                        .as_ref()
921                        .expect("run_stream should attach outcome metadata");
922                    assert_eq!(metadata.algorithm, "test");
923                    assert_eq!(
924                        uuid::Uuid::parse_str(metadata.outcome_id())
925                            .expect("outcome id should be a UUID")
926                            .get_version_num(),
927                        7
928                    );
929                    assert_eq!(
930                        metadata.evidence,
931                        Some(serde_json::json!({"source": "test"}))
932                    );
933                    let response = outcome
934                        .response
935                        .ok_or_else(|| test_error("expected an answered outcome"))?;
936                    final_completion = Some(
937                        response
938                            .llm_response
939                            .as_agg()
940                            .map(completion_text)
941                            .unwrap_or_default(),
942                    );
943                }
944            }
945        }
946
947        assert!(saw_call, "expected a CallModel step before Done");
948        assert_eq!(
949            final_completion.ok_or_else(|| test_error("no Done step"))?,
950            "fulfilled"
951        );
952        Ok(())
953    }
954
955    #[tokio::test(flavor = "multi_thread", worker_threads = 12)]
956    async fn requests_are_processed_in_parallel() -> Result<()> {
957        use std::time::Duration;
958        use tokio::sync::Barrier;
959
960        const N: usize = 12;
961
962        // Serving blocks until all N concurrent calls have arrived. If requests were
963        // serialized (one algorithm behind a `Mutex`), only one call could be in flight,
964        // the barrier would never reach N, and the test would time out. It passes only
965        // because the shared algorithm is driven concurrently across requests.
966        let barrier = Arc::new(Barrier::new(N));
967        // One shared algorithm driven by many concurrent requests.
968        let algo = orch(target_set(&["m"]));
969
970        let mut handles = Vec::new();
971        for _ in 0..N {
972            let algo = algo.clone();
973            let barrier = barrier.clone();
974            let serve = move |target: ModelId, _request: Request| {
975                let barrier = barrier.clone();
976                async move {
977                    barrier.wait().await;
978                    Ok(reply(target))
979                }
980            };
981            handles.push(tokio::spawn(async move {
982                test_drive(algo, request(), serve)
983                    .await
984                    .map(|(_, response)| {
985                        response
986                            .llm_response
987                            .as_agg()
988                            .map(completion_text)
989                            .unwrap_or_default()
990                    })
991            }));
992        }
993
994        for handle in handles {
995            // The timeout turns a serialization deadlock into a failure, not a hang.
996            let completion = tokio::time::timeout(Duration::from_secs(5), handle)
997                .await
998                .map_err(|error| LibsyError::external("waiting for test task", error))?
999                .map_err(|source| LibsyError::external("joining a test task", source))??;
1000            assert_eq!(completion, "m");
1001        }
1002        Ok(())
1003    }
1004
1005    #[tokio::test]
1006    async fn offload_error_propagates_back_to_the_algorithm() -> Result<()> {
1007        // A client-less target offloads its call; we fulfill the promise with an
1008        // Err, which must flow back through `call_model_target` into the algorithm and
1009        // out as an error step — not a response.
1010        let stream = orch(target_set(&["offload/model"]))
1011            .run_stream(request(), Arc::new(RuntimeModels::default()));
1012        tokio::pin!(stream);
1013
1014        let mut saw_error = false;
1015        while let Some(step) = stream.next().await {
1016            match step {
1017                Ok(Step::CallModel(call)) => {
1018                    call.respond(Err(test_error("upstream model call failed")))?;
1019                }
1020                Ok(Step::Done(..)) => {
1021                    return Err(test_error(
1022                        "expected the offload error to propagate, got a response",
1023                    ));
1024                }
1025                Err(err) => {
1026                    // The algorithm's `call_model_target` saw the error via the promise.
1027                    assert!(err.to_string().contains("upstream model call failed"));
1028                    saw_error = true;
1029                }
1030            }
1031        }
1032
1033        assert!(saw_error, "expected an error step");
1034        Ok(())
1035    }
1036
1037    #[tokio::test]
1038    async fn dropping_the_stream_cancels_the_algorithm_task() -> Result<()> {
1039        use std::sync::atomic::{AtomicBool, Ordering};
1040        use std::time::Duration;
1041        use tokio::sync::mpsc;
1042
1043        // Sets a flag when dropped, so we can observe whether the algorithm task was
1044        // cancelled/dropped.
1045        struct DropGuard(Arc<AtomicBool>);
1046        impl Drop for DropGuard {
1047            fn drop(&mut self) {
1048                self.0.store(true, Ordering::SeqCst);
1049            }
1050        }
1051
1052        struct StuckAlgo {
1053            started: mpsc::UnboundedSender<()>,
1054            dropped: Arc<AtomicBool>,
1055        }
1056
1057        #[async_trait]
1058        impl Algorithm for StuckAlgo {
1059            fn name(&self) -> &str {
1060                "stuck"
1061            }
1062
1063            async fn route(
1064                self: Arc<Self>,
1065                _driver: Driver,
1066                _request: Request,
1067            ) -> Result<RoutingOutcome> {
1068                let _guard = DropGuard(self.dropped.clone());
1069                let _ = self.started.send(());
1070                // Await forever without ever touching the driver.
1071                std::future::pending::<()>().await;
1072                unreachable!()
1073            }
1074        }
1075
1076        let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1077        let dropped = Arc::new(AtomicBool::new(false));
1078        let algo: Arc<dyn Algorithm> = Arc::new(StuckAlgo {
1079            started: started_tx,
1080            dropped: dropped.clone(),
1081        });
1082
1083        let stream = algo.run_stream(request(), Arc::new(RuntimeModels::default()));
1084        started_rx
1085            .recv()
1086            .await
1087            .ok_or_else(|| test_error("task never started"))?;
1088        drop(stream);
1089        tokio::time::sleep(Duration::from_millis(100)).await;
1090
1091        assert!(
1092            dropped.load(Ordering::SeqCst),
1093            "algorithm task was NOT cancelled after dropping the stream"
1094        );
1095        Ok(())
1096    }
1097
1098    #[tokio::test]
1099    async fn route_panic_surfaces_as_a_stream_error() -> Result<()> {
1100        // An algorithm whose task panics must surface an `Err` step carrying the panic
1101        // message, not abort the process from an unobserved detached task.
1102        struct Panicky;
1103
1104        #[async_trait]
1105        impl Algorithm for Panicky {
1106            fn name(&self) -> &str {
1107                "panicky"
1108            }
1109
1110            async fn route(
1111                self: Arc<Self>,
1112                _driver: Driver,
1113                _request: Request,
1114            ) -> Result<RoutingOutcome> {
1115                panic!("boom");
1116            }
1117        }
1118
1119        let algo: Arc<dyn Algorithm> = Arc::new(Panicky);
1120        let stream = algo.run_stream(request(), Arc::new(RuntimeModels::default()));
1121        tokio::pin!(stream);
1122
1123        let mut saw_error = false;
1124        while let Some(step) = stream.next().await {
1125            match step {
1126                Err(err) => {
1127                    // The panic message is preserved, not flattened into an opaque failure.
1128                    assert!(err.to_string().contains("algorithm task panicked: boom"));
1129                    saw_error = true;
1130                }
1131                Ok(_) => return Err(test_error("expected the panic to surface as an error step")),
1132            }
1133        }
1134
1135        assert!(saw_error, "expected an error step from the panicked task");
1136        Ok(())
1137    }
1138
1139    /// A panicking algorithm must publish its terminal step even when it left a `Driver`
1140    /// clone alive in another task. That clone holds the step channel open, so a run that
1141    /// merely unwound would never terminate and the consumer would wait forever.
1142    #[tokio::test]
1143    async fn a_panic_with_a_leaked_driver_clone_still_terminates_the_run() -> Result<()> {
1144        struct LeakyPanic;
1145
1146        #[async_trait]
1147        impl Algorithm for LeakyPanic {
1148            fn name(&self) -> &str {
1149                "leaky_panic"
1150            }
1151
1152            async fn route(
1153                self: Arc<Self>,
1154                driver: Driver,
1155                _request: Request,
1156            ) -> Result<RoutingOutcome> {
1157                tokio::spawn(async move {
1158                    // Outlives the panic below, keeping a sender clone alive.
1159                    let _keep_alive = driver;
1160                    std::future::pending::<()>().await;
1161                });
1162                tokio::task::yield_now().await;
1163                panic!("boom");
1164            }
1165        }
1166
1167        let algo: Arc<dyn Algorithm> = Arc::new(LeakyPanic);
1168        // The timeout turns the hang this guards against into a failure rather than a hang.
1169        let result = tokio::time::timeout(
1170            std::time::Duration::from_secs(1),
1171            test_drive(algo, request(), echo()),
1172        )
1173        .await
1174        .map_err(|error| LibsyError::external("waiting for the panicked run to end", error))?;
1175
1176        match result {
1177            Ok(_) => Err(test_error(
1178                "expected the panic to end the run with an error",
1179            )),
1180            Err(err) => {
1181                assert!(err.to_string().contains("algorithm task panicked: boom"));
1182                Ok(())
1183            }
1184        }
1185    }
1186
1187    #[tokio::test]
1188    async fn cancelling_run_cancels_the_algorithm_task() -> Result<()> {
1189        use std::sync::atomic::{AtomicBool, Ordering};
1190        use std::time::Duration;
1191        use tokio::sync::mpsc;
1192
1193        // Sets a flag when dropped, so we can observe whether the algorithm task was
1194        // cancelled once the `run` future driving it is dropped.
1195        struct DropGuard(Arc<AtomicBool>);
1196        impl Drop for DropGuard {
1197            fn drop(&mut self) {
1198                self.0.store(true, Ordering::SeqCst);
1199            }
1200        }
1201
1202        struct StuckAlgo {
1203            started: mpsc::UnboundedSender<()>,
1204            dropped: Arc<AtomicBool>,
1205        }
1206
1207        #[async_trait]
1208        impl Algorithm for StuckAlgo {
1209            fn name(&self) -> &str {
1210                "stuck"
1211            }
1212
1213            async fn route(
1214                self: Arc<Self>,
1215                _driver: Driver,
1216                _request: Request,
1217            ) -> Result<RoutingOutcome> {
1218                let _guard = DropGuard(self.dropped.clone());
1219                let _ = self.started.send(());
1220                // Hang forever without ever touching the driver, so only cancellation
1221                // (not a dropped step channel) can stop this task.
1222                std::future::pending::<()>().await;
1223                unreachable!()
1224            }
1225        }
1226
1227        let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1228        let dropped = Arc::new(AtomicBool::new(false));
1229        let algo: Arc<dyn Algorithm> = Arc::new(StuckAlgo {
1230            started: started_tx,
1231            dropped: dropped.clone(),
1232        });
1233
1234        // Drive the run on its own task, wait until the algorithm task is up, then cancel
1235        // it — dropping its future (and the `run_stream` stream it holds).
1236        let run_task = tokio::spawn(async move { test_drive(algo, request(), echo()).await });
1237        started_rx
1238            .recv()
1239            .await
1240            .ok_or_else(|| test_error("task never started"))?;
1241        run_task.abort();
1242        tokio::time::sleep(Duration::from_millis(100)).await;
1243
1244        assert!(
1245            dropped.load(Ordering::SeqCst),
1246            "algorithm task was NOT cancelled after cancelling run"
1247        );
1248        Ok(())
1249    }
1250
1251    // --- first-wins hedging: `run` must not wait on losing speculative calls -------------
1252
1253    /// Offloads two targets concurrently and returns the first to resolve, dropping the
1254    /// loser's call (first-wins hedging).
1255    struct Hedge {
1256        winner: String,
1257        loser: String,
1258    }
1259
1260    #[async_trait]
1261    impl Algorithm for Hedge {
1262        fn name(&self) -> &str {
1263            "hedge"
1264        }
1265
1266        async fn route(
1267            self: Arc<Self>,
1268            driver: Driver,
1269            request: Request,
1270        ) -> Result<RoutingOutcome> {
1271            let outcome_request = request.clone();
1272            let win = driver.call_model(request.clone(), vec![self.winner.clone().into()]);
1273            let lose = driver.call_model(request, vec![self.loser.clone().into()]);
1274            // First to resolve wins; `select!` drops the losing future (and its promise).
1275            tokio::select! {
1276                res = win => Ok(RoutingOutcome::answered(
1277                    self.winner.clone().into(),
1278                    outcome_request,
1279                    res?,
1280                )),
1281                res = lose => Ok(RoutingOutcome::answered(
1282                    self.loser.clone().into(),
1283                    outcome_request,
1284                    res?,
1285                )),
1286            }
1287        }
1288    }
1289
1290    /// Builds a hedging algo and the `serve` that drives it: the winner is gated behind the
1291    /// loser starting (so the loser's serve is guaranteed in flight when the winner wins),
1292    /// and the loser finishes after `loser_delay` — or never, when `None`.
1293    fn hedge(loser_delay: Option<std::time::Duration>) -> (Arc<dyn Algorithm>, impl Serve) {
1294        let started = Arc::new(tokio::sync::Notify::new());
1295        let algo = Arc::new(Hedge {
1296            winner: "winner".to_string(),
1297            loser: "loser".to_string(),
1298        });
1299        let serve = move |target: ModelId, _request: Request| {
1300            let started = started.clone();
1301            async move {
1302                if target == "loser" {
1303                    started.notify_one();
1304                    match loser_delay {
1305                        Some(delay) => tokio::time::sleep(delay).await,
1306                        None => std::future::pending::<()>().await,
1307                    }
1308                } else {
1309                    started.notified().await;
1310                }
1311                Ok(reply(target))
1312            }
1313        };
1314        (algo, serve)
1315    }
1316
1317    #[tokio::test]
1318    async fn run_returns_the_winner_without_a_late_loser_overwriting_it() -> Result<()> {
1319        // The loser responds 50ms after the winner has already won. `run` must return the
1320        // winner, not the loser's `respond`-to-a-dropped-receiver error.
1321        let (algo, serve) = hedge(Some(std::time::Duration::from_millis(50)));
1322        let (_, response) = test_drive(algo, request(), serve).await?;
1323        assert_eq!(
1324            response
1325                .llm_response
1326                .as_agg()
1327                .map(completion_text)
1328                .unwrap_or_default(),
1329            "winner"
1330        );
1331        Ok(())
1332    }
1333
1334    #[tokio::test]
1335    async fn run_returns_the_winner_without_hanging_on_a_pending_loser() -> Result<()> {
1336        // The loser never resolves. `run` must return the winner promptly, not hang
1337        // waiting for the in-flight loser.
1338        let (algo, serve) = hedge(None);
1339        let run = test_drive(algo, request(), serve);
1340        let (_, response) = tokio::time::timeout(std::time::Duration::from_secs(1), run)
1341            .await
1342            .map_err(|error| LibsyError::external("waiting for pending loser", error))??;
1343        assert_eq!(
1344            response
1345                .llm_response
1346                .as_agg()
1347                .map(completion_text)
1348                .unwrap_or_default(),
1349            "winner"
1350        );
1351        Ok(())
1352    }
1353
1354    #[tokio::test]
1355    async fn run_surfaces_a_terminal_error_with_many_calls_in_flight() -> Result<()> {
1356        use std::sync::atomic::{AtomicUsize, Ordering};
1357
1358        // A large fan-out (10 matched the old, now-removed concurrency cap). The terminal
1359        // error must still reach the caller with all of these calls pending.
1360        const N: usize = 10;
1361
1362        // Fans out N calls, then errors as soon as all N are in flight — exercising a
1363        // terminal failure emitted while the offloaded calls are still pending.
1364        struct FanOutThenError {
1365            all_started: Arc<tokio::sync::Notify>,
1366            n: usize,
1367        }
1368
1369        #[async_trait]
1370        impl Algorithm for FanOutThenError {
1371            fn name(&self) -> &str {
1372                "fan_out_then_error"
1373            }
1374
1375            async fn route(
1376                self: Arc<Self>,
1377                driver: Driver,
1378                request: Request,
1379            ) -> Result<RoutingOutcome> {
1380                let offloads = futures::future::join_all(
1381                    (0..self.n)
1382                        .map(|i| driver.call_model(request.clone(), vec![format!("m{i}").into()])),
1383                );
1384                tokio::select! {
1385                    _ = offloads => Err(test_error("offloads unexpectedly completed")),
1386                    _ = self.all_started.notified() => {
1387                        Err(test_error("terminal error while calls pending"))
1388                    }
1389                }
1390            }
1391        }
1392
1393        let all_started = Arc::new(tokio::sync::Notify::new());
1394        let algo: Arc<dyn Algorithm> = Arc::new(FanOutThenError {
1395            all_started: all_started.clone(),
1396            n: N,
1397        });
1398
1399        // Serving enters each call; once all N are in flight it signals, then pends forever.
1400        let started = Arc::new(AtomicUsize::new(0));
1401        let serve = move |_target: ModelId, _request: Request| {
1402            let started = started.clone();
1403            let all_started = all_started.clone();
1404            async move {
1405                if started.fetch_add(1, Ordering::SeqCst) + 1 == N {
1406                    all_started.notify_one();
1407                }
1408                std::future::pending::<ServeResult>().await
1409            }
1410        };
1411
1412        // With the cap gone, the driver keeps polling the stream even with N calls in
1413        // flight, so the terminal error surfaces promptly instead of hanging.
1414        let run = test_drive(algo, request(), serve);
1415        let result = tokio::time::timeout(std::time::Duration::from_millis(500), run)
1416            .await
1417            .map_err(|error| {
1418                LibsyError::external("waiting for terminal error with full call cap", error)
1419            })?;
1420        match result {
1421            Ok(_) => Err(test_error("expected the terminal error, got a response")),
1422            Err(err) => {
1423                assert!(
1424                    err.to_string()
1425                        .contains("terminal error while calls pending")
1426                );
1427                Ok(())
1428            }
1429        }
1430    }
1431}