ag_ui/server/emit/message.rs
1//! Streaming one text message.
2
3use crate::{Event, MessageId, TextMessageRole};
4
5use crate::server::agent::AgentState;
6use crate::server::emit::EventSink;
7use crate::server::error::Result;
8use crate::server::state::RunState;
9
10/// One open text message.
11///
12/// Created by [`RunContext::assistant_message`](crate::server::RunContext::assistant_message).
13/// `TEXT_MESSAGE_START` has already gone out by the time you hold one; `Drop`
14/// emits `TEXT_MESSAGE_END` if [`end`](Self::end) was not called.
15///
16/// ```
17/// # use ag_ui::RunAgentInput;
18/// # use ag_ui::server::RunContext;
19/// # let (mut ctx, mut events) = RunContext::<()>::new(RunAgentInput::new("t", "r"))?;
20/// let mut message = ctx.assistant_message()?;
21/// for word in ["Hello", ", ", "world"] {
22/// message.delta(word)?;
23/// }
24/// message.end()?;
25/// assert_eq!(events.drain().len(), 5);
26/// # Ok::<(), ag_ui::server::Error>(())
27/// ```
28#[derive(Debug)]
29pub struct MessageHandle<'a, S> {
30 sink: &'a mut EventSink,
31 state: &'a mut RunState<S>,
32 id: MessageId,
33 ended: bool,
34}
35
36impl<'a, S> MessageHandle<'a, S> {
37 /// Emits `TEXT_MESSAGE_START` and takes the message.
38 ///
39 /// Returns `Err` without producing a handle when the start could not be
40 /// emitted, so a failed open never leaves a terminator to be emitted for a
41 /// message that does not exist.
42 pub(crate) fn start(
43 sink: &'a mut EventSink,
44 state: &'a mut RunState<S>,
45 id: MessageId,
46 role: TextMessageRole,
47 ) -> Result<Self> {
48 sink.emit(Event::text_message_start(id.clone(), role))?;
49 Ok(Self {
50 sink,
51 state,
52 id,
53 ended: false,
54 })
55 }
56
57 /// The id every event of this message carries.
58 pub fn id(&self) -> &MessageId {
59 &self.id
60 }
61
62 /// Appends text — `TEXT_MESSAGE_CONTENT`.
63 ///
64 /// No `.await`: see the [module docs](crate::server::emit) for why the emit path is
65 /// synchronous.
66 pub fn delta(&mut self, text: impl Into<String>) -> Result<()> {
67 self.sink
68 .emit(Event::text_message_content(self.id.clone(), text))
69 }
70
71 /// Emits an unrelated event without closing the message.
72 ///
73 /// For the unordered families — `STATE_*`, `ACTIVITY_*`, `CUSTOM`, `RAW` —
74 /// which may legally interleave with a message. Opening a second message
75 /// through here is a protocol violation the verifier will reject.
76 pub fn emit(&mut self, event: Event) -> Result<()> {
77 self.sink.emit(event)
78 }
79
80 /// Emits `TEXT_MESSAGE_END` and consumes the handle.
81 ///
82 /// Only worth calling over letting the handle drop when you want to see the
83 /// error: `Drop` cannot report one.
84 pub fn end(mut self) -> Result<()> {
85 self.ended = true;
86 self.sink.emit(Event::text_message_end(self.id.clone()))
87 }
88}
89
90/// The run's state, reachable while the message is open — an agent that
91/// narrates what it is doing changes both in the same breath. See
92/// [`ToolCallHandle`](crate::server::ToolCallHandle), where this matters most.
93impl<S: AgentState> MessageHandle<'_, S> {
94 /// The typed state, as of the last publish.
95 pub fn state(&self) -> &S {
96 self.state.get()
97 }
98
99 /// The typed state, mutably. Nothing is emitted until you call
100 /// [`publish_state`](Self::publish_state).
101 pub fn state_mut(&mut self) -> &mut S {
102 self.state.get_mut()
103 }
104
105 /// Publishes whatever [`state_mut`](Self::state_mut) left behind, as a
106 /// `STATE_SNAPSHOT` or a `STATE_DELTA` inside this message's brackets.
107 ///
108 /// A no-op when nothing changed since the last publish.
109 pub fn publish_state(&mut self) -> Result<()> {
110 self.state.publish(self.sink)
111 }
112}
113
114impl<S> Drop for MessageHandle<'_, S> {
115 fn drop(&mut self) {
116 if !self.ended {
117 // Nowhere to report a failure to; a dead channel or a cancelled run
118 // makes the terminator moot anyway.
119 let _ = self.sink.emit(Event::text_message_end(self.id.clone()));
120 }
121 }
122}