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}