Skip to main content

open_agent/client/
state.rs

1/// Stateful client for multi-turn conversations with automatic history management.
2///
3/// The `Client` is the primary interface for building conversational AI applications.
4/// It maintains conversation history, manages streaming responses, and provides two
5/// modes of operation: manual and automatic tool execution.
6///
7/// # State Management
8///
9/// The client maintains several pieces of state that persist across multiple turns:
10///
11/// - **Conversation History**: Complete record of all messages exchanged
12/// - **Active Stream**: Currently active SSE stream being consumed
13/// - **Interrupt Flag**: Thread-safe cancellation signal
14/// - **Auto-Execution Buffer**: Cached blocks for auto-execution mode
15/// - **Last Finish Reason**: Why the most recent stream stopped, via `finish_reason()`
16///
17/// # Operating Modes
18///
19/// ## Manual Mode (default)
20///
21/// In manual mode, the client streams blocks directly to the caller. When the model
22/// requests a tool, you receive a `ToolUseBlock`, execute the tool yourself, add the
23/// result with `add_tool_result()`, and continue the conversation.
24///
25/// **Advantages**:
26/// - Full control over tool execution
27/// - Custom error handling per tool
28/// - Ability to modify tool inputs/outputs
29/// - Interactive debugging capabilities
30///
31/// ## Automatic Mode (`auto_execute_tools = true`)
32///
33/// In automatic mode, the client executes tools transparently and only returns the
34/// final text response after all tool iterations complete.
35///
36/// **Advantages**:
37/// - Simpler API for common use cases
38/// - Built-in retry logic via hooks
39/// - Automatic conversation continuation
40/// - Configurable iteration limits
41///
42/// # Thread Safety
43///
44/// The client is NOT thread-safe for concurrent use. However, the interrupt mechanism
45/// uses `Arc<AtomicBool>` which can be safely shared across threads to signal cancellation.
46///
47/// # Memory Management
48///
49/// - History grows unbounded by default (consider clearing periodically)
50/// - Streams are consumed lazily (low memory footprint during streaming)
51/// - Auto-execution buffers entire response (higher memory in auto mode)
52///
53/// # Examples
54///
55/// ## Basic Multi-Turn Conversation
56///
57/// ```rust,no_run
58/// use open_agent::{Client, AgentOptions, ContentBlock};
59///
60/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
61/// let mut client = Client::new(AgentOptions::builder()
62///     .model("gpt-4")
63///     .api_key("sk-...")
64///     .build()?)?;
65///
66/// // First question
67/// client.send("What's the capital of France?").await?;
68/// while let Some(block) = client.receive().await? {
69///     if let ContentBlock::Text(text) = block {
70///         print!("{}", text.text); // fragments of "Paris is the capital of France."
71///     }
72/// }
73///
74/// // Follow-up question - history is automatically maintained
75/// client.send("What's its population?").await?;
76/// while let Some(block) = client.receive().await? {
77///     if let ContentBlock::Text(text) = block {
78///         print!("{}", text.text); // fragments of the answer, in order
79///     }
80/// }
81/// # Ok(())
82/// # }
83/// ```
84///
85/// ## Manual Tool Execution
86///
87/// ```rust,no_run
88/// use open_agent::{Client, AgentOptions, ContentBlock, Tool};
89/// use serde_json::json;
90///
91/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
92/// let calculator = Tool::new(
93///     "calculator",
94///     "Performs arithmetic",
95///     json!({"type": "object"}),
96///     |input| Box::pin(async move { Ok(json!({"result": 42})) })
97/// );
98///
99/// let mut client = Client::new(AgentOptions::builder()
100///     .model("gpt-4")
101///     .api_key("sk-...")
102///     .tools(vec![calculator])
103///     .build()?)?;
104///
105/// client.send("What's 2+2?").await?;
106///
107/// while let Some(block) = client.receive().await? {
108///     match block {
109///         ContentBlock::ToolUse(tool_use) => {
110///             // Execute tool manually
111///             let result = json!({"result": 4});
112///             client.add_tool_result(tool_use.id(), result)?;
113///
114///             // Continue conversation to get model's response
115///             client.send("").await?;
116///         }
117///         ContentBlock::Text(text) => {
118///             print!("{}", text.text); // fragments of "The result is 4."
119///         }
120///         ContentBlock::ToolResult(_) | ContentBlock::Image(_) => {}
121///     }
122/// }
123/// # Ok(())
124/// # }
125/// ```
126///
127/// ## Automatic Tool Execution
128///
129/// ```rust,no_run
130/// use open_agent::{Client, AgentOptions, ContentBlock, Tool};
131/// use serde_json::json;
132///
133/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
134/// let calculator = Tool::new(
135///     "calculator",
136///     "Performs arithmetic",
137///     json!({"type": "object"}),
138///     |input| Box::pin(async move { Ok(json!({"result": 42})) })
139/// );
140///
141/// let mut client = Client::new(AgentOptions::builder()
142///     .model("gpt-4")
143///     .api_key("sk-...")
144///     .tools(vec![calculator])
145///     .auto_execute_tools(true)  // Enable auto-execution
146///     .build()?)?;
147///
148/// client.send("What's 2+2?").await?;
149///
150/// // Tools execute automatically - you only receive final text
151/// while let Some(block) = client.receive().await? {
152///     if let ContentBlock::Text(text) = block {
153///         print!("{}", text.text); // fragments of "The result is 4."
154///     }
155/// }
156/// # Ok(())
157/// # }
158/// ```
159///
160/// ## With Interruption
161///
162/// ```rust,no_run
163/// use open_agent::{Client, AgentOptions};
164/// use std::time::Duration;
165///
166/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
167/// let mut client = Client::new(AgentOptions::default())?;
168///
169/// // Start a long-running query
170/// client.send("Write a very long story").await?;
171///
172/// // Spawn a task to interrupt after timeout
173/// let interrupt_handle = client.interrupt_handle();
174/// tokio::spawn(async move {
175///     tokio::time::sleep(Duration::from_secs(5)).await;
176///     interrupt_handle.store(true, std::sync::atomic::Ordering::SeqCst);
177/// });
178///
179/// // This loop will stop when interrupted
180/// while let Some(block) = client.receive().await? {
181///     // Process blocks...
182/// }
183///
184/// // Client is still usable after interruption
185/// client.send("What's 2+2?").await?;
186/// # Ok(())
187/// # }
188/// ```
189pub struct Client {
190    /// Configuration options including model, API key, tools, hooks, etc.
191    ///
192    /// This field contains all the settings that control how the client behaves.
193    /// It's set once during construction and cannot be modified (though you can
194    /// access it via `options()` for inspection).
195    options: AgentOptions,
196
197    /// Complete conversation history as a sequence of messages.
198    ///
199    /// Each message contains a role (System/User/Assistant/Tool) and content blocks.
200    /// History grows unbounded by default - use `clear_history()` to reset.
201    ///
202    /// **Important**: The history includes ALL messages, not just user/assistant.
203    /// This includes tool results and intermediate assistant messages from tool calls.
204    history: Vec<Message>,
205
206    /// Currently active SSE stream being consumed.
207    ///
208    /// This is `Some(stream)` while a response is being received, and `None` when
209    /// no request is in flight or after a response completes.
210    ///
211    /// The stream is set by `send()` and consumed by `receive()`. When the stream
212    /// is exhausted, `receive()` returns `Ok(None)` and sets this back to `None`.
213    current_stream: Option<EventStream>,
214
215    /// Reusable HTTP client for making API requests.
216    ///
217    /// Configured once during construction with the timeout from `AgentOptions`.
218    /// Reusing the same client across requests enables connection pooling and
219    /// better performance for multi-turn conversations.
220    http_client: reqwest::Client,
221
222    /// Thread-safe interrupt flag for cancellation.
223    ///
224    /// This `Arc<AtomicBool>` can be cloned and shared across threads or async tasks
225    /// to signal cancellation. When set to `true`, the next `receive()` call will
226    /// return `Ok(None)` and clear the current stream.
227    ///
228    /// The flag is automatically reset to `false` at the start of each `send()` call.
229    ///
230    /// **Thread Safety**: Can be safely accessed from multiple threads using atomic
231    /// operations. However, only one thread should call `send()`/`receive()`.
232    interrupted: Arc<AtomicBool>,
233
234    /// Buffer of content blocks for auto-execution mode.
235    ///
236    /// When `auto_execute_tools` is enabled, `receive()` internally calls the
237    /// auto-execution loop which buffers all final text blocks here. Subsequent
238    /// calls to `receive()` return blocks from this buffer one at a time.
239    ///
240    /// **Only used when `options.auto_execute_tools == true`**.
241    ///
242    /// The buffer is cleared when starting a new auto-execution loop.
243    auto_exec_buffer: Vec<ContentBlock>,
244
245    /// Current read position in the auto-execution buffer.
246    ///
247    /// Tracks which block to return next when `receive()` is called in auto mode.
248    /// Reset to 0 when the buffer is refilled with a new response.
249    ///
250    /// **Only used when `options.auto_execute_tools == true`**.
251    auto_exec_index: usize,
252
253    /// Accumulator for assistant response blocks in manual mode.
254    ///
255    /// In manual mode, `receive()` streams blocks one at a time to the caller.
256    /// This buffer collects those blocks so that when the stream ends, the
257    /// complete assistant message can be added to conversation history.
258    ///
259    /// **Only used when `options.auto_execute_tools == false`**.
260    manual_receive_buffer: Vec<ContentBlock>,
261
262    /// Why the most recent stream stopped generating.
263    ///
264    /// Set from the terminating `StreamEvent::Finish` as `receive()` drains the stream, and
265    /// reset when a new request starts. `None` means no stream has completed since the last
266    /// `send()` — read it after the receive loop, not during.
267    ///
268    /// In auto-execution mode this reports the final generation of the loop, which is the one
269    /// that produced the text the caller sees.
270    last_finish_reason: Option<FinishReason>,
271
272    /// Reasoning text accumulated from the most recent stream.
273    ///
274    /// Stays empty unless `options.include_reasoning()` is enabled. Kept out of `history` so
275    /// chain-of-thought is never replayed back to the model as if it were assistant output.
276    ///
277    /// Accumulates across the rounds of an auto-execution tool loop; `auto_execute_loop`
278    /// carries it over each internal `send("")` that `start_request` would otherwise clear.
279    last_reasoning: String,
280}