ag_ui/client/agent.rs
1//! The low-level API: start a run, get its events.
2//!
3//! [`RemoteAgent`] adds almost nothing to [`Transport`] — a request builder, and
4//! a stream that flattens connecting into streaming. That is the point. Anything
5//! that wants the events *as they were sent* — a proxy, a recorder, a bridge to
6//! another protocol, a test — should stay at this level.
7//!
8//! For a UI, [`Session`](crate::client::Session) sits on top of this and does the
9//! assembling.
10//!
11//! ```no_run
12//! # #[cfg(feature = "http")]
13//! # async fn example() -> Result<(), ag_ui::client::Error> {
14//! use ag_ui::client::{HttpAgent, RunParams};
15//! use futures_util::StreamExt;
16//!
17//! let agent = HttpAgent::builder("https://example.com/agent")
18//! .header("authorization", "Bearer …")
19//! .build()?;
20//!
21//! let mut events = agent.run(
22//! RunParams::new("thread-1", "run-1").user("msg-1", "What is the weather?"),
23//! );
24//!
25//! while let Some(event) = events.next().await {
26//! println!("{:?}", event?.event_type());
27//! }
28//! # Ok(())
29//! # }
30//! ```
31
32use crate::{
33 Context, Message, MessageId, ResumeEntry, RunAgentInput, RunId, ThreadId, Tool, UserContent,
34};
35use futures_util::TryStreamExt;
36use serde_json::Value;
37
38use crate::client::transport::{EventStream, Transport, boxed_stream};
39
40#[cfg(feature = "http")]
41use crate::client::error::Result;
42#[cfg(feature = "http")]
43use crate::client::transport::{HttpTransport, HttpTransportBuilder};
44
45/// What to send when starting a run.
46///
47/// A builder over [`RunAgentInput`]: the two ids are required, everything else
48/// has a sensible empty default. `agent.run(…)` takes anything that converts
49/// into the input, so a hand-built [`RunAgentInput`] works just as well.
50///
51/// [`RunAgentInput`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/input/struct.RunAgentInput.html
52#[derive(Clone, Debug, Default, PartialEq)]
53pub struct RunParams {
54 input: RunAgentInput,
55}
56
57impl RunParams {
58 /// A run in `thread_id`, identified by `run_id`.
59 pub fn new(thread_id: impl Into<ThreadId>, run_id: impl Into<RunId>) -> Self {
60 Self {
61 input: RunAgentInput::new(thread_id, run_id),
62 }
63 }
64
65 /// Sets the conversation history, oldest first.
66 #[must_use]
67 pub fn messages(mut self, messages: impl Into<Vec<Message>>) -> Self {
68 self.input.messages = messages.into();
69 self
70 }
71
72 /// Appends one message.
73 #[must_use]
74 pub fn message(mut self, message: impl Into<Message>) -> Self {
75 self.input.messages.push(message.into());
76 self
77 }
78
79 /// Appends a user message — the common case, spelled short.
80 #[must_use]
81 pub fn user(self, id: impl Into<MessageId>, content: impl Into<UserContent>) -> Self {
82 self.message(Message::user(id, content))
83 }
84
85 /// Sets the shared state the agent starts from.
86 #[must_use]
87 pub fn state(mut self, state: impl Into<Value>) -> Self {
88 self.input.state = state.into();
89 self
90 }
91
92 /// Offers tools for this run.
93 #[must_use]
94 pub fn tools(mut self, tools: impl Into<Vec<Tool>>) -> Self {
95 self.input.tools = tools.into();
96 self
97 }
98
99 /// Sets the ambient context entries.
100 #[must_use]
101 pub fn context(mut self, context: impl Into<Vec<Context>>) -> Self {
102 self.input.context = context.into();
103 self
104 }
105
106 /// Sets the passthrough properties, which the protocol never interprets.
107 #[must_use]
108 pub fn forwarded_props(mut self, props: impl Into<Value>) -> Self {
109 self.input.forwarded_props = props.into();
110 self
111 }
112
113 /// Answers the interrupts a previous run paused on. See
114 /// [`crate::client::interrupts`].
115 #[must_use]
116 pub fn resume(mut self, entries: impl Into<Vec<ResumeEntry>>) -> Self {
117 self.input.resume = Some(entries.into());
118 self
119 }
120
121 /// Records the run that spawned this one, for nested agents.
122 #[must_use]
123 pub fn parent_run_id(mut self, run_id: impl Into<RunId>) -> Self {
124 self.input.parent_run_id = Some(run_id.into());
125 self
126 }
127
128 /// The request this describes.
129 pub fn into_input(self) -> RunAgentInput {
130 self.input
131 }
132}
133
134impl From<RunParams> for RunAgentInput {
135 fn from(params: RunParams) -> Self {
136 params.input
137 }
138}
139
140impl From<RunAgentInput> for RunParams {
141 fn from(input: RunAgentInput) -> Self {
142 Self { input }
143 }
144}
145
146/// A remote agent, over any [`Transport`].
147///
148#[cfg_attr(
149 feature = "http",
150 doc = "[`HttpAgent`] is this over HTTP; the type is generic so that a wasm"
151)]
152#[cfg_attr(
153 not(feature = "http"),
154 doc = "`HttpAgent` (feature `http`) is this over HTTP; the type is generic so that a wasm"
155)]
156/// transport, an in-process agent, or a recorded fixture substitutes without
157/// anything above noticing.
158///
159/// # Not [`crate::server::Agent`]
160///
161/// The two crates sit on opposite ends of the same wire, and the word "agent"
162/// means the opposite thing at each end, so they do not share a name.
163/// [`crate::server::Agent`] is a *trait you implement* to be an agent;
164/// `RemoteAgent` is a *handle you hold* onto someone else's. An agent that calls
165/// another agent — the composition case — needs both in one file, and
166/// `impl Agent for X { … self.upstream: RemoteAgent<_> … }` reads correctly
167/// only because they are spelled differently.
168///
169/// [`crate::server::Agent`]: https://kimsoungryoul.github.io/ag-ui-rust/api/ag_ui/server/agent/trait.Agent.html
170#[derive(Clone, Debug, Default)]
171pub struct RemoteAgent<T> {
172 transport: T,
173}
174
175impl<T> RemoteAgent<T> {
176 /// An agent reached through `transport`.
177 pub fn new(transport: T) -> Self {
178 Self { transport }
179 }
180
181 /// The transport underneath.
182 pub fn transport(&self) -> &T {
183 &self.transport
184 }
185
186 /// Unwraps the transport.
187 pub fn into_transport(self) -> T {
188 self.transport
189 }
190}
191
192impl<T: Transport> RemoteAgent<T> {
193 /// Starts a run and streams its events, exactly as the agent sent them.
194 ///
195 /// Nothing is normalized, verified or assembled here — chunk events arrive
196 /// as chunk events. That is what a proxy wants; a UI wants
197 /// [`Session`](crate::client::Session).
198 ///
199 /// Connecting is folded into the stream: a transport that cannot reach the
200 /// agent yields one error item and ends.
201 pub fn run(&self, params: impl Into<RunAgentInput>) -> EventStream {
202 let connecting = self.transport.run(params.into());
203 boxed_stream(futures_util::stream::once(connecting).try_flatten())
204 }
205}
206
207/// An agent reached over HTTP.
208#[cfg(feature = "http")]
209pub type HttpAgent = RemoteAgent<HttpTransport>;
210
211#[cfg(feature = "http")]
212impl RemoteAgent<HttpTransport> {
213 /// A builder for an agent at `url`.
214 pub fn builder(url: impl AsRef<str>) -> HttpAgentBuilder {
215 HttpAgentBuilder {
216 transport: HttpTransport::builder(url),
217 }
218 }
219
220 /// An agent at `url`, with default settings.
221 ///
222 /// # Errors
223 ///
224 /// [`Error::Config`](crate::client::Error::Config) when the URL does not parse.
225 pub fn http(url: impl AsRef<str>) -> Result<Self> {
226 Ok(Self::new(HttpTransport::new(url)?))
227 }
228}
229
230/// Builds an [`HttpAgent`]: base URL, headers, timeouts.
231#[cfg(feature = "http")]
232#[derive(Clone, Debug)]
233pub struct HttpAgentBuilder {
234 transport: HttpTransportBuilder,
235}
236
237#[cfg(feature = "http")]
238impl HttpAgentBuilder {
239 /// Adds a header to every request.
240 #[must_use]
241 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
242 self.transport = self.transport.header(name, value);
243 self
244 }
245
246 /// Adds several headers.
247 #[must_use]
248 pub fn headers<K, V>(mut self, headers: impl IntoIterator<Item = (K, V)>) -> Self
249 where
250 K: Into<String>,
251 V: Into<String>,
252 {
253 self.transport = self.transport.headers(headers);
254 self
255 }
256
257 /// Bounds the whole run, streaming included.
258 #[must_use]
259 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
260 self.transport = self.transport.timeout(timeout);
261 self
262 }
263
264 /// Bounds connection setup only, leaving the stream unbounded.
265 #[must_use]
266 pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
267 self.transport = self.transport.connect_timeout(timeout);
268 self
269 }
270
271 /// Uses a caller-supplied `reqwest` client.
272 #[must_use]
273 pub fn client(mut self, client: reqwest::Client) -> Self {
274 self.transport = self.transport.client(client);
275 self
276 }
277
278 /// Builds the agent.
279 ///
280 /// # Errors
281 ///
282 /// [`Error::Config`](crate::client::Error::Config) when the URL or a header is not
283 /// valid.
284 pub fn build(self) -> Result<HttpAgent> {
285 Ok(RemoteAgent::new(self.transport.build()?))
286 }
287}