Skip to main content

chimera_core/
session.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use futures_core::Stream;
6
7use crate::{AgentError, AgentEvent, Input, TurnOptions, TurnResult, Usage};
8
9pub trait Session: Send {
10    /// Run a turn and return the buffered result.
11    ///
12    /// Default implementation calls `turn_stream` then `collect_turn`.
13    fn turn(
14        &mut self,
15        input: Input,
16        options: TurnOptions,
17    ) -> Pin<Box<dyn Future<Output = Result<TurnOutput, AgentError>> + Send + '_>> {
18        Box::pin(async move { self.turn_stream(input, options).await?.collect_turn().await })
19    }
20
21    /// Run a turn and return a stream of events.
22    fn turn_stream(
23        &mut self,
24        input: Input,
25        options: TurnOptions,
26    ) -> Pin<Box<dyn Future<Output = Result<EventStream, AgentError>> + Send + '_>>;
27
28    /// Session ID (populated after the first turn completes).
29    fn session_id(&self) -> Option<&str>;
30
31    /// Interrupt the current turn. No-op if idle.
32    fn interrupt(&mut self) -> Pin<Box<dyn Future<Output = Result<(), AgentError>> + Send + '_>>;
33}
34
35#[must_use = "streams do nothing unless polled"]
36pub struct EventStream {
37    inner: Pin<Box<dyn Stream<Item = Result<AgentEvent, AgentError>> + Send>>,
38}
39
40impl EventStream {
41    pub fn new(
42        stream: impl Stream<Item = Result<AgentEvent, AgentError>> + Send + 'static,
43    ) -> Self {
44        Self {
45            inner: Box::pin(stream),
46        }
47    }
48
49    pub fn from_receiver(rx: tokio::sync::mpsc::Receiver<Result<AgentEvent, AgentError>>) -> Self {
50        Self::new(ReceiverStream { inner: rx })
51    }
52
53    /// Consume the stream and collect into a `TurnOutput`.
54    pub async fn collect_turn(mut self) -> Result<TurnOutput, AgentError> {
55        let mut events = Vec::new();
56        let mut response = None;
57        let mut response_deltas = String::new();
58        let mut saw_delta = false;
59        let mut usage = None;
60        let mut result = None;
61        let mut tool_calls: Vec<ToolCallOutput> = Vec::new();
62
63        loop {
64            match std::future::poll_fn(|cx| self.inner.as_mut().poll_next(cx)).await {
65                Some(Ok(event)) => {
66                    match &event {
67                        AgentEvent::Message { text, .. } => {
68                            response = Some(text.clone());
69                        }
70                        AgentEvent::TextDelta { delta, .. } => {
71                            response_deltas.push_str(delta);
72                            saw_delta = true;
73                        }
74                        AgentEvent::TurnCompleted {
75                            usage: u,
76                            result: r,
77                        } => {
78                            usage.clone_from(u);
79                            result.clone_from(r);
80                        }
81                        AgentEvent::TurnFailed { message } => {
82                            return Err(AgentError::TurnFailed {
83                                message: message.clone(),
84                            });
85                        }
86                        AgentEvent::ToolCall {
87                            id,
88                            name,
89                            arguments,
90                        } => {
91                            tool_calls.push(ToolCallOutput {
92                                id: id.clone(),
93                                name: name.clone(),
94                                arguments: arguments.clone(),
95                            });
96                        }
97                        _ => {}
98                    }
99                    events.push(event);
100                }
101                Some(Err(e)) => return Err(e),
102                None => break,
103            }
104        }
105
106        if response.is_none() && saw_delta {
107            response = Some(response_deltas);
108        }
109
110        Ok(TurnOutput {
111            events,
112            response,
113            tool_calls,
114            usage,
115            result,
116        })
117    }
118}
119
120impl Stream for EventStream {
121    type Item = Result<AgentEvent, AgentError>;
122
123    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
124        self.inner.as_mut().poll_next(cx)
125    }
126}
127
128/// Adapts `mpsc::Receiver` to `Stream` without pulling in `tokio-stream`.
129struct ReceiverStream {
130    inner: tokio::sync::mpsc::Receiver<Result<AgentEvent, AgentError>>,
131}
132
133impl Stream for ReceiverStream {
134    type Item = Result<AgentEvent, AgentError>;
135
136    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
137        self.inner.poll_recv(cx)
138    }
139}
140
141/// A single tool/function call made by the model during a turn.
142#[derive(Debug, Clone)]
143pub struct ToolCallOutput {
144    pub id: String,
145    pub name: String,
146    /// Raw JSON string of the arguments object.
147    pub arguments: String,
148}
149
150#[derive(Debug, Clone)]
151pub struct TurnOutput {
152    pub events: Vec<AgentEvent>,
153    pub response: Option<String>,
154    /// Tool calls made by the model, in call order.
155    pub tool_calls: Vec<ToolCallOutput>,
156    pub usage: Option<Usage>,
157    pub result: Option<TurnResult>,
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[tokio::test]
165    async fn collect_turn_extracts_response() {
166        let (tx, rx) = tokio::sync::mpsc::channel(16);
167
168        tx.send(Ok(AgentEvent::TurnStarted)).await.unwrap();
169        tx.send(Ok(AgentEvent::Message {
170            id: "msg-1".into(),
171            text: "Hello world".into(),
172        }))
173        .await
174        .unwrap();
175        tx.send(Ok(AgentEvent::TurnCompleted {
176            usage: Some(Usage {
177                input_tokens: Some(10),
178                output_tokens: Some(5),
179                ..Default::default()
180            }),
181            result: None,
182        }))
183        .await
184        .unwrap();
185        drop(tx);
186
187        let stream = EventStream::from_receiver(rx);
188        let output = stream.collect_turn().await.unwrap();
189
190        assert_eq!(output.response.as_deref(), Some("Hello world"));
191        assert_eq!(output.events.len(), 3);
192        assert_eq!(output.usage.as_ref().unwrap().input_tokens, Some(10));
193    }
194
195    #[tokio::test]
196    async fn collect_turn_fails_on_turn_failed() {
197        let (tx, rx) = tokio::sync::mpsc::channel(16);
198
199        tx.send(Ok(AgentEvent::TurnStarted)).await.unwrap();
200        tx.send(Ok(AgentEvent::TurnFailed {
201            message: "something broke".into(),
202        }))
203        .await
204        .unwrap();
205        drop(tx);
206
207        let stream = EventStream::from_receiver(rx);
208        let err = stream.collect_turn().await.unwrap_err();
209
210        match err {
211            AgentError::TurnFailed { message } => assert_eq!(message, "something broke"),
212            _ => panic!("expected TurnFailed, got {err:?}"),
213        }
214    }
215
216    #[tokio::test]
217    async fn collect_turn_uses_last_message() {
218        let (tx, rx) = tokio::sync::mpsc::channel(16);
219
220        tx.send(Ok(AgentEvent::Message {
221            id: "1".into(),
222            text: "first".into(),
223        }))
224        .await
225        .unwrap();
226        tx.send(Ok(AgentEvent::Message {
227            id: "2".into(),
228            text: "second".into(),
229        }))
230        .await
231        .unwrap();
232        tx.send(Ok(AgentEvent::TurnCompleted {
233            usage: None,
234            result: None,
235        }))
236        .await
237        .unwrap();
238        drop(tx);
239
240        let stream = EventStream::from_receiver(rx);
241        let output = stream.collect_turn().await.unwrap();
242
243        assert_eq!(output.response.as_deref(), Some("second"));
244    }
245
246    #[tokio::test]
247    async fn collect_turn_uses_deltas_when_no_message() {
248        let (tx, rx) = tokio::sync::mpsc::channel(16);
249
250        tx.send(Ok(AgentEvent::TextDelta {
251            id: "block_0".into(),
252            delta: "Hello".into(),
253        }))
254        .await
255        .unwrap();
256        tx.send(Ok(AgentEvent::TextDelta {
257            id: "block_0".into(),
258            delta: " world".into(),
259        }))
260        .await
261        .unwrap();
262        tx.send(Ok(AgentEvent::TurnCompleted {
263            usage: None,
264            result: None,
265        }))
266        .await
267        .unwrap();
268        drop(tx);
269
270        let stream = EventStream::from_receiver(rx);
271        let output = stream.collect_turn().await.unwrap();
272
273        assert_eq!(output.response.as_deref(), Some("Hello world"));
274    }
275
276    #[tokio::test]
277    async fn collect_turn_propagates_stream_error() {
278        let (tx, rx) = tokio::sync::mpsc::channel(16);
279
280        tx.send(Ok(AgentEvent::TurnStarted)).await.unwrap();
281        tx.send(Err(AgentError::ProcessFailed {
282            code: 1,
283            stderr: "crash".into(),
284        }))
285        .await
286        .unwrap();
287        drop(tx);
288
289        let stream = EventStream::from_receiver(rx);
290        let err = stream.collect_turn().await.unwrap_err();
291
292        match err {
293            AgentError::ProcessFailed { code, .. } => assert_eq!(code, 1),
294            _ => panic!("expected ProcessFailed, got {err:?}"),
295        }
296    }
297
298    #[tokio::test]
299    async fn empty_stream_produces_empty_output() {
300        let (_tx, rx) = tokio::sync::mpsc::channel::<Result<AgentEvent, AgentError>>(1);
301        drop(_tx);
302
303        let stream = EventStream::from_receiver(rx);
304        let output = stream.collect_turn().await.unwrap();
305
306        assert!(output.events.is_empty());
307        assert!(output.response.is_none());
308        assert!(output.usage.is_none());
309    }
310}