Skip to main content

runifold_model/
invocation.rs

1use std::{
2    future::Future,
3    pin::Pin,
4    time::{Duration, Instant},
5};
6
7use futures_core::Stream;
8use futures_util::{
9    StreamExt,
10    future::{Either, select},
11};
12use runifold_core::{CancellationToken, InvocationId, RunContext, RunId};
13
14use crate::{
15    ModelCapabilities, ModelError, ModelErrorKind, ModelRef, ModelRequest, ModelResponse,
16    ModelStreamAccumulator, ModelStreamEvent,
17};
18
19/// A boxed, sendable future returned by a model implementation.
20pub type ModelFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
21
22/// A provider-neutral stream of canonical model events.
23pub type ModelEventStream =
24    Pin<Box<dyn Stream<Item = Result<ModelStreamEvent, ModelError>> + Send + 'static>>;
25
26/// Execution scope for one model invocation.
27///
28/// It deliberately carries lifecycle data rather than provider configuration.
29/// Adapters must observe cancellation and should translate the deadline into
30/// their transport's timeout mechanism.
31#[derive(Clone, Debug)]
32pub struct ModelCallContext {
33    invocation_id: InvocationId,
34    run_id: Option<RunId>,
35    deadline: Option<Instant>,
36    cancellation: CancellationToken,
37}
38
39impl ModelCallContext {
40    /// Creates a standalone invocation context.
41    pub fn new() -> Self {
42        Self {
43            invocation_id: InvocationId::new(),
44            run_id: None,
45            deadline: None,
46            cancellation: CancellationToken::new(),
47        }
48    }
49
50    /// Creates an invocation scoped beneath a run.
51    pub fn for_run(run: &RunContext) -> Self {
52        Self {
53            invocation_id: InvocationId::new(),
54            run_id: Some(run.run_id()),
55            deadline: run.deadline(),
56            cancellation: run.cancellation().child_token(),
57        }
58    }
59
60    /// Returns this model invocation's identity.
61    pub const fn invocation_id(&self) -> InvocationId {
62        self.invocation_id
63    }
64
65    /// Returns the owning run identity, when invoked inside a run.
66    pub const fn run_id(&self) -> Option<RunId> {
67        self.run_id
68    }
69
70    /// Returns the effective deadline.
71    pub const fn deadline(&self) -> Option<Instant> {
72        self.deadline
73    }
74
75    /// Returns the remaining time before the deadline.
76    pub fn remaining(&self) -> Option<Duration> {
77        self.deadline
78            .map(|deadline| deadline.saturating_duration_since(Instant::now()))
79    }
80
81    /// Returns the invocation's hierarchical cancellation token.
82    pub const fn cancellation(&self) -> &CancellationToken {
83        &self.cancellation
84    }
85
86    /// Sets a deadline, retaining an existing earlier deadline.
87    #[must_use]
88    pub fn with_deadline(mut self, deadline: Instant) -> Self {
89        self.deadline = Some(
90            self.deadline
91                .map_or(deadline, |current| current.min(deadline)),
92        );
93        self
94    }
95
96    /// Replaces the cancellation root for an externally scoped invocation.
97    ///
98    /// The invocation receives a child token so provider attempts cannot
99    /// cancel their caller's broader operation.
100    #[must_use]
101    pub fn with_cancellation(mut self, cancellation: &CancellationToken) -> Self {
102        self.cancellation = cancellation.child_token();
103        self
104    }
105
106    /// Creates a distinct provider-attempt context under the same logical
107    /// invocation scope.
108    ///
109    /// The attempt receives a new invocation identity while inheriting the
110    /// run, effective deadline, and hierarchical cancellation.
111    #[must_use]
112    pub fn child_attempt(&self) -> Self {
113        Self {
114            invocation_id: InvocationId::new(),
115            run_id: self.run_id,
116            deadline: self.deadline,
117            cancellation: self.cancellation.child_token(),
118        }
119    }
120}
121
122impl Default for ModelCallContext {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128/// Object-safe boundary implemented by model provider adapters.
129///
130/// Streaming is the source of truth. [`Model::invoke`] is a canonical
131/// collector over that stream, so streamed and non-streamed calls cannot
132/// silently develop different normalization behavior.
133pub trait Model: Send + Sync {
134    /// Resolves capabilities for a provider-qualified model.
135    fn capabilities<'a>(
136        &'a self,
137        model: &'a ModelRef,
138    ) -> ModelFuture<'a, Result<ModelCapabilities, ModelError>>;
139
140    /// Opens a canonical event stream for a request.
141    fn stream(
142        &self,
143        request: ModelRequest,
144        context: ModelCallContext,
145    ) -> ModelFuture<'_, Result<ModelEventStream, ModelError>>;
146
147    /// Invokes the model and reconstructs its terminal response.
148    fn invoke(
149        &self,
150        request: ModelRequest,
151        context: ModelCallContext,
152    ) -> ModelFuture<'_, Result<ModelResponse, ModelError>> {
153        Box::pin(async move {
154            let cancellation = context.cancellation().clone();
155            let stream_future = self.stream(request, context);
156            let mut stream =
157                match select(Box::pin(cancellation.cancelled()), Box::pin(stream_future)).await {
158                    Either::Left(_) => return Err(cancelled_error()),
159                    Either::Right((result, _)) => result?,
160                };
161
162            let mut accumulator = ModelStreamAccumulator::new();
163            loop {
164                let next = stream.next();
165                match select(Box::pin(cancellation.cancelled()), Box::pin(next)).await {
166                    Either::Left(_) => return Err(cancelled_error()),
167                    Either::Right((Some(event), _)) => {
168                        if let Some(response) = accumulator.push(event?)? {
169                            return Ok(response);
170                        }
171                    }
172                    Either::Right((None, _)) => {
173                        return Err(ModelError::local(
174                            ModelErrorKind::Protocol,
175                            "model stream ended before a terminal response event",
176                        ));
177                    }
178                }
179            }
180        })
181    }
182}
183
184fn cancelled_error() -> ModelError {
185    ModelError::local(ModelErrorKind::Cancelled, "model invocation was cancelled")
186}