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