Skip to main content

rig_core/serve/
layer.rs

1//! Handler composition with pre-dispatch decisions and post-answer verdicts.
2//! Recording observes the handler a layer wraps: denied dispatches leave no record,
3//! and replaced answers retain the handler's original outcome for replay.
4//!
5//! ```
6//! use rig_core::serve::BeforeServe;
7//!
8//! let decision = BeforeServe::deny("Not authorized");
9//! assert!(matches!(decision, BeforeServe::Deny(_)));
10//! ```
11
12use std::sync::{Arc, Mutex};
13
14use futures::StreamExt;
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    effect::{Answered, EffectId, EffectKind, HandlerDescriptor, family},
19    error::{ErrorKind, ErrorReport},
20    wasm_compat::{WasmCompatSend, WasmCompatSync},
21};
22
23use super::{Dispatch, ErasedHandler, Reply, Serve, stream_truncated};
24use crate::streaming::Relayed;
25
26/// What a layer decides about a dispatch before the handler sees it.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "decision", rename_all = "snake_case")]
29pub enum BeforeServe {
30    /// Serve it as it is.
31    Proceed,
32    /// Serve this instead. A patch never changes the family or a tool call's
33    /// target name: either change returns an `Internal` report without reaching
34    /// the next layer or dispatching. Tool argument patches remain supported.
35    Patch(EffectKind),
36    /// Returns this report without invoking the handler or recording a dispatch.
37    /// The report's kind is preserved; [`BeforeServe::deny`] uses `ErrorKind::Denied`.
38    Deny(ErrorReport),
39}
40
41impl BeforeServe {
42    /// A denial by policy: `ErrorKind::Denied`, never retryable.
43    pub fn deny(reason: impl Into<String>) -> Self {
44        Self::Deny(ErrorReport::new(ErrorKind::Denied, reason).with_retryable(false))
45    }
46}
47
48/// What a layer decides about an answer on its way out.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(tag = "verdict", rename_all = "snake_case")]
51pub enum AfterServe {
52    /// The consumer receives what the handler answered.
53    Keep,
54    /// The consumer receives this instead; the record keeps the handler's
55    /// answer. Over a streaming dispatch the events were already delivered
56    /// as they came, so a replacement is the reply's end: a completion
57    /// replaces the response the stream ends with (its
58    /// [`Relayed::Done`](crate::streaming::Relayed::Done)), which is what
59    /// the consumer's fold returns, and an error ends the stream with that
60    /// error. A replacement of another family reaches the consumer as an
61    /// `Internal` error naming the layer.
62    Replace(Result<Answered, ErrorReport>),
63}
64
65/// Host policy before a handler and after its first answer. A suspended
66/// verdict keeps execution in flight; driver cancellation drops its future.
67/// Streamed verdict futures are polled on the stream consumer's thread.
68/// The layer name identifies policy when recording and validating replay.
69pub trait Intercept: WasmCompatSend + WasmCompatSync + 'static {
70    /// The layer's name, as the log records it.
71    fn name(&self) -> String;
72
73    /// Before the handler: the dispatch as it will be served, or not.
74    fn before(
75        &self,
76        id: EffectId,
77        kind: &EffectKind,
78    ) -> impl Future<Output = BeforeServe> + WasmCompatSend;
79
80    /// After the handler: the answer as the consumer will receive it. For a
81    /// streaming dispatch `outcome` is the fold of the events (the
82    /// completion the record stores).
83    fn after(
84        &self,
85        id: EffectId,
86        kind: &EffectKind,
87        outcome: &Result<Answered, ErrorReport>,
88    ) -> impl Future<Output = AfterServe> + WasmCompatSend;
89}
90
91/// A handler wrapped in a policy: a [`Serve`] like any other, registered
92/// under the inner handler's descriptor (with the layer's name added,
93/// outermost first). Built with [`ErasedHandler::layered`].
94pub struct Layer<I: Intercept> {
95    inner: ErasedHandler,
96    intercept: Arc<I>,
97}
98
99impl<I: Intercept> Layer<I> {
100    /// `intercept` around `inner`.
101    pub(crate) fn new(inner: ErasedHandler, intercept: I) -> Self {
102        Self {
103            inner,
104            intercept: Arc::new(intercept),
105        }
106    }
107
108    fn internal(&self, message: String) -> ErrorReport {
109        ErrorReport::new(
110            ErrorKind::Internal,
111            format!("layer `{}`: {message}", self.intercept.name()),
112        )
113        .with_retryable(false)
114    }
115}
116
117impl<I: Intercept> Serve for Layer<I> {
118    type Family = family::Dynamic;
119
120    fn descriptor(&self) -> HandlerDescriptor {
121        let mut descriptor = self.inner.descriptor();
122        descriptor.layers.insert(0, self.intercept.name());
123        descriptor
124    }
125
126    fn key(&self) -> crate::effect::HandlerKey {
127        self.inner.key().clone()
128    }
129
130    fn footprint(&self, kind: &EffectKind) -> Option<crate::tool::Touches> {
131        self.inner.footprint(kind)
132    }
133
134    fn check(&self, kind: &EffectKind) -> Vec<crate::completion::UnsupportedOption> {
135        self.inner.check(kind)
136    }
137
138    async fn serve(&self, kind: EffectKind, mut dispatch: Dispatch) -> Reply {
139        let id = dispatch.id();
140        let name = self.intercept.name();
141        let kind = match self.intercept.before(id, &kind).await {
142            BeforeServe::Proceed => kind,
143            BeforeServe::Patch(patched) => {
144                if patched.family() != kind.family() {
145                    dispatch.discard(&name);
146                    return Reply::Outcome(Err(self.internal(format!(
147                        "patched a {} effect into a {} effect; a layer never changes the family",
148                        kind.family(),
149                        patched.family()
150                    ))));
151                }
152                if let (
153                    EffectKind::ToolCall { name: original, .. },
154                    EffectKind::ToolCall {
155                        name: replacement, ..
156                    },
157                ) = (&kind, &patched)
158                    && original != replacement
159                {
160                    dispatch.discard(&name);
161                    return Reply::Outcome(Err(self.internal(format!(
162                        "patched tool target `{original}` into `{replacement}`; a layer never changes the bound tool"
163                    ))));
164                }
165                dispatch.patched(&patched);
166                patched
167            }
168            BeforeServe::Deny(report) => {
169                dispatch.discard(&name);
170                return Reply::Outcome(Err(report));
171            }
172        };
173        if !dispatch.is_stream() {
174            let folded = Arc::new(Mutex::new(None));
175            let inner = dispatch.inner(Some(folded.clone()));
176            let attribution = dispatch.attribution();
177            let outcome = self
178                .inner
179                .handle(kind.clone(), inner)
180                .await
181                .folded_outcome(Some(folded))
182                .await;
183            return Reply::Outcome(match self.intercept.after(id, &kind, &outcome).await {
184                AfterServe::Keep => outcome,
185                AfterServe::Replace(replacement) => {
186                    attribution.replaced(&name);
187                    replacement
188                }
189            });
190        }
191        let folded = Arc::new(Mutex::new(None));
192        let inner = dispatch.inner(Some(folded.clone()));
193        let attribution = dispatch.attribution();
194        let stream = self
195            .inner
196            .handle(kind.clone(), inner)
197            .await
198            .into_stream()
199            .fuse();
200        let intercept = self.intercept.clone();
201        Reply::Stream(Box::pin(futures::stream::unfold(
202            (stream, intercept, kind, folded, false, attribution),
203            move |(mut stream, intercept, kind, folded, mut decided, attribution)| async move {
204                let item = stream.next().await;
205                if decided && item.is_none() {
206                    return None;
207                }
208                let outcome = if decided {
209                    None
210                } else {
211                    folded
212                        .lock()
213                        .unwrap_or_else(std::sync::PoisonError::into_inner)
214                        .take()
215                };
216                let item = if let Some(outcome) = outcome {
217                    decided = true;
218                    match intercept.after(id, &kind, &outcome).await {
219                        AfterServe::Keep => item.unwrap_or_else(|| Err(stream_truncated())),
220                        AfterServe::Replace(Err(report)) => {
221                            attribution.replaced(&intercept.name());
222                            Err(report)
223                        }
224                        AfterServe::Replace(Ok(Answered::Completion(response))) => {
225                            attribution.replaced(&intercept.name());
226                            Ok(Relayed::Done(response))
227                        }
228                        AfterServe::Replace(Ok(other)) => {
229                            attribution.replaced(&intercept.name());
230                            Err(ErrorReport::new(
231                                ErrorKind::Internal,
232                                format!(
233                                    "layer `{}`: replaced a streamed completion with a {} outcome",
234                                    intercept.name(),
235                                    other.family()
236                                ),
237                            )
238                            .with_retryable(false))
239                        }
240                    }
241                } else {
242                    item?
243                };
244                Some((
245                    item,
246                    (stream, intercept, kind, folded, decided, attribution),
247                ))
248            },
249        )))
250    }
251}
252
253#[cfg(test)]
254mod tests;