Skip to main content

longbridge/agent/
stream.rs

1use std::sync::{
2    Arc,
3    atomic::{AtomicU64, Ordering},
4};
5
6use futures_util::{Stream, StreamExt};
7use tokio::{sync::Notify, task::AbortHandle};
8
9use crate::{
10    Error, Result,
11    agent::types::{ConversationResponse, ConversationStreamEvent},
12};
13
14/// Drive a conversation event stream to completion, invoking `on_event` for
15/// every event, and returning the final [`ConversationResponse`] once a
16/// [`ConversationStreamEvent::WorkflowFinished`] or
17/// [`ConversationStreamEvent::HumanInteractionRequired`] event is observed
18/// (or an error if the stream ends before either happens). An interrupted
19/// run emits `HumanInteractionRequired` instead of `WorkflowFinished`, never
20/// both, so exactly one of the two is expected per run.
21///
22/// Used by binding layers that are call-scoped-callback shaped (C, C++,
23/// Node.js) — every other binding either pulls synchronously
24/// ([`conversation_stream_iter`]) or drives with real backpressure
25/// ([`ConversationStreamSubscription`]).
26pub async fn drive_conversation_stream<S, F>(
27    mut stream: S,
28    mut on_event: F,
29) -> Result<ConversationResponse>
30where
31    S: Stream<Item = Result<ConversationStreamEvent>> + Send + Unpin,
32    F: FnMut(ConversationStreamEvent) + Send,
33{
34    let mut final_response = None;
35    while let Some(event) = stream.next().await {
36        let event = event?;
37        match &event {
38            ConversationStreamEvent::WorkflowFinished(resp)
39            | ConversationStreamEvent::HumanInteractionRequired(resp) => {
40                final_response = Some(resp.clone());
41            }
42            _ => {}
43        }
44        on_event(event);
45    }
46    final_response.ok_or(Error::ConversationStreamEnded)
47}
48
49/// A blocking [`Iterator`] over conversation stream events, backed by a
50/// background task on the shared runtime ([`crate::runtime_handle`]). Useful
51/// for sync/FFI bindings that need to pull events one at a time from a plain OS
52/// thread instead of polling a [`Stream`] directly.
53pub struct ConversationStreamIter(std::sync::mpsc::Receiver<Result<ConversationStreamEvent>>);
54
55impl Iterator for ConversationStreamIter {
56    type Item = Result<ConversationStreamEvent>;
57
58    fn next(&mut self) -> Option<Self::Item> {
59        self.0.recv().ok()
60    }
61}
62
63/// Adapt a conversation event [`Stream`] into a blocking
64/// [`ConversationStreamIter`].
65pub fn conversation_stream_iter(
66    stream: impl Stream<Item = Result<ConversationStreamEvent>> + Send + 'static,
67) -> ConversationStreamIter {
68    let (tx, rx) = std::sync::mpsc::channel();
69    let mut stream = Box::pin(stream);
70    crate::runtime_handle().spawn(async move {
71        while let Some(item) = stream.next().await {
72            if tx.send(item).is_err() {
73                break; // receiver dropped, caller stopped iterating early
74            }
75        }
76    });
77    ConversationStreamIter(rx)
78}
79
80/// Bridges a conversation event [`Stream`] to a Reactive-Streams-style consumer
81/// with real backpressure, matching `java.util.concurrent.Flow.Subscription`'s
82/// `request(n)`/`cancel()` contract. Only Java's `Flow.Publisher` exposure
83/// needs this — every other binding either pulls synchronously
84/// ([`conversation_stream_iter`]) or has no flow control at all
85/// ([`drive_conversation_stream`]).
86pub struct ConversationStreamSubscription {
87    demand: Arc<(AtomicU64, Notify)>,
88    abort: AbortHandle,
89}
90
91impl ConversationStreamSubscription {
92    /// Spawn a background task that waits for demand, pulls one item at a time
93    /// from `stream` once demand is available, and dispatches
94    /// `on_next`/`on_error`/`on_complete` (each of these is expected to call
95    /// back into the JVM via a JNI `Subscriber` reference).
96    ///
97    /// Drains all the way to the stream's natural end rather than stopping as
98    /// soon as a [`ConversationStreamEvent::WorkflowFinished`] is seen —
99    /// against the real API, the server sometimes emits a few more
100    /// housekeeping events (e.g. a `chat_title_updated`-shaped
101    /// [`ConversationStreamEvent::Other`]) after `workflow_finished` and
102    /// before actually closing the connection, so stopping early would
103    /// silently drop them and abandon the connection while the server still
104    /// had something to say.
105    pub fn spawn<S, F1, F2, F3>(stream: S, on_next: F1, on_error: F2, on_complete: F3) -> Self
106    where
107        S: Stream<Item = Result<ConversationStreamEvent>> + Send + 'static,
108        F1: Fn(ConversationStreamEvent) + Send + Sync + 'static,
109        F2: FnOnce(Error) + Send + 'static,
110        F3: FnOnce() + Send + 'static,
111    {
112        let demand = Arc::new((AtomicU64::new(0), Notify::new()));
113        let demand2 = demand.clone();
114        let handle = crate::runtime_handle().spawn(async move {
115            let mut stream = Box::pin(stream);
116            loop {
117                // wait until `request(n)` has added at least one credit
118                while demand2.0.load(Ordering::Acquire) == 0 {
119                    demand2.1.notified().await;
120                }
121                match stream.next().await {
122                    Some(Ok(event)) => {
123                        demand2.0.fetch_sub(1, Ordering::AcqRel);
124                        on_next(event);
125                    }
126                    Some(Err(err)) => {
127                        on_error(err);
128                        break;
129                    }
130                    None => {
131                        on_complete();
132                        break;
133                    }
134                }
135            }
136        });
137        Self {
138            demand,
139            abort: handle.abort_handle(),
140        }
141    }
142
143    /// Called from `Flow.Subscription.request(n)` (any JVM thread).
144    pub fn request(&self, n: u64) {
145        self.demand.0.fetch_add(n, Ordering::AcqRel);
146        self.demand.1.notify_one();
147    }
148
149    /// Called from `Flow.Subscription.cancel()`.
150    pub fn cancel(&self) {
151        self.abort.abort();
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use futures_util::stream;
158
159    use super::*;
160    use crate::agent::types::{ChatFinishedPayload, ChatStartedPayload, Interrupt};
161
162    // Regression test for the interrupted-run gap:
163    // https://github.com/longbridge/developers/pull/1176 confirms an
164    // interrupted run never emits `WorkflowFinished` — before this fix,
165    // `drive_conversation_stream` would run to the end of such a stream
166    // without ever setting `final_response` and return
167    // `Error::ConversationStreamEnded`.
168    #[tokio::test]
169    async fn drive_conversation_stream_terminates_on_human_interaction_required() {
170        let interrupt_resp = ConversationResponse::from_stream_interrupt(
171            Some(("ct_1".to_string(), "1".to_string())),
172            Interrupt {
173                node_id: "n_ask_human".to_string(),
174                tool_call_id: "call_1".to_string(),
175                questions: vec![],
176                interactions: vec![],
177                message_id: 1,
178                chat_id: 1,
179            },
180        );
181        let events: Vec<Result<ConversationStreamEvent>> = vec![
182            Ok(ConversationStreamEvent::ChatStarted(ChatStartedPayload {
183                chat_uid: "ct_1".to_string(),
184                message_id: "1".to_string(),
185                chat_id: 0,
186                error: String::new(),
187                error_message: String::new(),
188            })),
189            Ok(ConversationStreamEvent::HumanInteractionRequired(
190                interrupt_resp,
191            )),
192            Ok(ConversationStreamEvent::ChatFinished(
193                ChatFinishedPayload::default(),
194            )),
195        ];
196
197        let mut seen = 0;
198        let resp = drive_conversation_stream(stream::iter(events), |_| seen += 1)
199            .await
200            .unwrap();
201        assert_eq!(seen, 3);
202        assert_eq!(
203            resp.status,
204            crate::agent::types::ConversationStatus::Interrupted
205        );
206        assert!(resp.interrupt.is_some());
207    }
208}