Skip to main content

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}