Skip to main content

ai_ui_core/
adapter.rs

1//! The model adapter contract, ported from `chat-model-adapter.ts`.
2//!
3//! A [`ChatModelAdapter`] receives the thread up to the new assistant message
4//! and returns a stream of [`ChatModelRunResult`]s. As upstream, each result is
5//! cumulative: its `content` replaces everything the adapter produced so far in
6//! this step, and a `status` or `metadata` field updates only what it names. An
7//! adapter that produces one answer returns a one-item stream ([`once`]).
8
9use crate::abort::AbortSignal;
10use crate::types::{
11    AssistantMessage, AssistantMessagePart, JsonObject, MessageId, MessageStatus, RunConfig,
12    ThreadMessage, ThreadStep,
13};
14use futures_core::Stream;
15use serde_json::Value;
16use std::collections::BTreeMap;
17use std::fmt;
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::Arc;
21use std::task::{Context, Poll};
22
23/// A tool the model may call (`Tool` in `model-context/tool.ts`, without the
24/// frontend `execute`/`render` hooks, which belong to the tool registry).
25#[derive(Clone, Debug, Default, PartialEq)]
26pub struct ToolDefinition {
27    /// What the tool does, for the model.
28    pub description: Option<String>,
29    /// JSON Schema of the arguments.
30    pub parameters: Value,
31}
32
33/// System prompt, tools and settings contributed by the app (`ModelContext`).
34#[derive(Clone, Debug, Default, PartialEq)]
35pub struct ModelContext {
36    /// System prompt to send first.
37    pub system: Option<String>,
38    /// Tools the model may call, by name.
39    pub tools: BTreeMap<String, ToolDefinition>,
40    /// Model settings such as temperature, in the form the backend expects.
41    pub call_settings: JsonObject,
42    /// Backend configuration such as the model name.
43    pub config: JsonObject,
44}
45
46/// Everything an adapter receives for one step (`ChatModelRunOptions`).
47#[derive(Clone, Debug)]
48pub struct ChatModelRunOptions {
49    /// The selected path up to and including the message being answered.
50    pub messages: Arc<[Arc<ThreadMessage>]>,
51    /// The run's configuration.
52    pub run_config: RunConfig,
53    /// Fires when the run is cancelled; stop work and end the stream.
54    pub abort_signal: AbortSignal,
55    /// The app's system prompt, tools and settings.
56    pub context: ModelContext,
57    /// Id of the assistant message this step fills.
58    pub assistant_message_id: MessageId,
59    /// The thread's remote id, when it has one.
60    pub thread_id: Option<String>,
61    /// The message being answered.
62    pub parent_id: Option<MessageId>,
63    /// The assistant message being answered, as it was when this step started
64    /// (`unstable_getMessage()` upstream). Its content is non-empty when a run
65    /// continues after tool results; its metadata holds the thread state.
66    pub message: AssistantMessage,
67}
68
69/// Metadata updates in a run result. Lists are appended to what the message
70/// had when the step started; `custom` keys are merged.
71#[derive(Clone, Debug, Default, PartialEq)]
72pub struct RunResultMetadata {
73    /// App state to merge into the message (`aui-state` operations, applied).
74    pub state: Option<Value>,
75    /// Annotations to append.
76    pub annotations: Option<Vec<Value>>,
77    /// Data entries to append.
78    pub data: Option<Vec<Value>>,
79    /// The steps taken so far.
80    pub steps: Option<Vec<ThreadStep>>,
81    /// Timing information for the run.
82    pub timing: Option<Value>,
83    /// Keys to merge into the message's custom metadata.
84    pub custom: Option<JsonObject>,
85}
86
87/// One cumulative update from an adapter (`ChatModelRunResult`).
88#[derive(Clone, Debug, Default, PartialEq)]
89pub struct ChatModelRunResult {
90    /// The content so far; each result replaces the previous one's.
91    pub content: Option<Vec<AssistantMessagePart>>,
92    /// The message status, if the adapter sets one; `None` lets the runtime decide.
93    pub status: Option<MessageStatus>,
94    /// Metadata updates carried by this result.
95    pub metadata: Option<RunResultMetadata>,
96}
97
98impl ChatModelRunResult {
99    /// The content so far, with no status change.
100    pub fn content(content: Vec<AssistantMessagePart>) -> Self {
101        Self {
102            content: Some(content),
103            ..Self::default()
104        }
105    }
106    /// A single text part.
107    pub fn text(text: impl Into<String>) -> Self {
108        Self::content(vec![AssistantMessagePart::text(text)])
109    }
110    /// Set the message status.
111    pub fn with_status(mut self, status: MessageStatus) -> Self {
112        self.status = Some(status);
113        self
114    }
115}
116
117/// Why an adapter stopped.
118#[derive(Clone, Debug, PartialEq)]
119#[non_exhaustive]
120pub enum RunError {
121    /// The run was cancelled; the message becomes `incomplete/cancelled`.
122    Aborted,
123    /// The model or transport failed; the message becomes `incomplete/error`
124    /// with `error`, and the runtime returns this error from the run future.
125    Failed {
126        /// What went wrong, for display.
127        message: String,
128        /// Structured error detail stored with the message, if any.
129        error: Option<Value>,
130    },
131}
132
133impl RunError {
134    /// A failure with a message and no structured detail.
135    pub fn failed(message: impl Into<String>) -> Self {
136        Self::Failed {
137            message: message.into(),
138            error: None,
139        }
140    }
141    /// The value stored as the message status's `error`.
142    pub fn status_error(&self) -> Option<Value> {
143        match self {
144            Self::Aborted => None,
145            Self::Failed { message, error } => Some(
146                error
147                    .clone()
148                    .unwrap_or_else(|| Value::String(message.clone())),
149            ),
150        }
151    }
152}
153
154impl fmt::Display for RunError {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            Self::Aborted => f.write_str("run aborted"),
158            Self::Failed { message, .. } => f.write_str(message),
159        }
160    }
161}
162impl std::error::Error for RunError {}
163
164/// The stream an adapter returns. It is polled on the runtime's thread.
165pub type RunStream = Pin<Box<dyn Stream<Item = Result<ChatModelRunResult, RunError>>>>;
166
167/// A model backend (`ChatModelAdapter`).
168pub trait ChatModelAdapter {
169    /// Start one step and return the stream of cumulative results.
170    fn run(&self, options: ChatModelRunOptions) -> RunStream;
171}
172
173impl<F> ChatModelAdapter for F
174where
175    F: Fn(ChatModelRunOptions) -> RunStream,
176{
177    fn run(&self, options: ChatModelRunOptions) -> RunStream {
178        self(options)
179    }
180}
181
182/// A stream with the single result of `future` (a `Promise`-returning adapter).
183pub fn once<F>(future: F) -> RunStream
184where
185    F: Future<Output = Result<ChatModelRunResult, RunError>> + 'static,
186{
187    Box::pin(Once {
188        future: Some(Box::pin(future)),
189    })
190}
191
192/// A stream of ready results, for scripted adapters and tests.
193pub fn from_results(
194    results: impl IntoIterator<Item = Result<ChatModelRunResult, RunError>>,
195) -> RunStream {
196    Box::pin(Iter {
197        items: results.into_iter().collect::<Vec<_>>().into_iter(),
198    })
199}
200
201type ResultFuture = Pin<Box<dyn Future<Output = Result<ChatModelRunResult, RunError>>>>;
202
203struct Once {
204    future: Option<ResultFuture>,
205}
206
207impl Stream for Once {
208    type Item = Result<ChatModelRunResult, RunError>;
209    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
210        let Some(future) = self.future.as_mut() else {
211            return Poll::Ready(None);
212        };
213        match future.as_mut().poll(cx) {
214            Poll::Ready(result) => {
215                self.future = None;
216                Poll::Ready(Some(result))
217            }
218            Poll::Pending => Poll::Pending,
219        }
220    }
221}
222
223struct Iter {
224    items: std::vec::IntoIter<Result<ChatModelRunResult, RunError>>,
225}
226
227impl Stream for Iter {
228    type Item = Result<ChatModelRunResult, RunError>;
229    fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
230        Poll::Ready(self.items.next())
231    }
232}