1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
impl Client {
async fn receive_one(&mut self) -> Result<Option<ContentBlock>> {
// Check interrupt flag before attempting to receive
// Uses SeqCst to ensure we see the latest value from any thread
if self.interrupted.load(Ordering::SeqCst) {
// Return None but leave current_stream intact so callers can
// distinguish "interrupted a live stream" (current_stream is Some)
// from "interrupt after stream already ended" (current_stream is None).
return Ok(None);
}
// Drain non-content events (finish reason, reasoning) into client state and keep
// polling, so `receive()` continues to hand the caller content blocks only.
loop {
// No active stream
let Some(stream) = &mut self.current_stream else {
return Ok(None);
};
match stream.next().await {
Some(Ok(StreamEvent::Block(block))) => return Ok(Some(block)),
Some(Ok(StreamEvent::Reasoning(reasoning))) => {
// Recorded for `reasoning()`, deliberately never pushed to history.
// Traces run to tens of kilobytes, so take the buffer by move when there
// is nothing to append to — which is every stream outside the auto loop.
if self.last_reasoning.is_empty() {
self.last_reasoning = reasoning;
} else {
self.last_reasoning.push_str(&reasoning);
}
}
Some(Ok(StreamEvent::Finish(reason))) => {
self.last_finish_reason = Some(reason);
}
Some(Err(e)) => return Err(e),
None => {
// Natural EOF — mark stream as fully consumed
self.current_stream = None;
return Ok(None);
}
}
}
}
/// Collects all blocks from the current stream into a vector.
///
/// Internal helper for auto-execution mode. This method buffers the entire
/// response in memory, which is necessary to determine if the response contains
/// tool calls before returning anything to the caller.
///
/// # Returns
///
/// - `Ok(vec)`: Successfully collected all blocks
/// - `Err(e)`: Error during collection or interrupted
///
/// # Memory Usage
///
/// This buffers the entire response, which can be large for long completions.
/// Consider the memory implications when using auto-execution mode.
///
/// # Interruption
///
/// Checks interrupt flag during collection and returns error if interrupted.
async fn collect_all_blocks(&mut self) -> Result<Vec<ContentBlock>> {
let mut blocks = Vec::new();
// Consume entire stream into vector
while let Some(block) = self.receive_one().await? {
// Check interrupt during collection for responsiveness
if self.interrupted.load(Ordering::SeqCst) {
self.current_stream = None;
return Err(Error::other(
"Operation interrupted during block collection",
));
}
blocks.push(block);
}
Ok(blocks)
}
/// Executes a tool by name with the given input.
///
/// Internal helper for auto-execution mode. Looks up the tool in the registered
/// tools list and executes it with the provided input.
///
/// # Parameters
///
/// - `tool_name`: Name of the tool to execute
/// - `input`: JSON value containing tool parameters
///
/// # Returns
///
/// - `Ok(result)`: Tool executed successfully, returns result as JSON
/// - `Err(e)`: Tool not found or execution failed
///
/// # Error Handling
///
/// If the tool is not found in the registry, returns a ToolError.
/// If execution fails, the error from the tool is propagated.
async fn execute_tool_internal(
&self,
tool_name: &str,
input: serde_json::Value,
) -> Result<serde_json::Value> {
// Find tool in registered tools by name
let tool = self
.options
.tools()
.iter()
.find(|t| t.name() == tool_name)
.ok_or_else(|| Error::tool(format!("Tool '{}' not found", tool_name)))?;
// Execute the tool's async function
tool.execute(input).await
}
/// Auto-execution loop that handles tool calls automatically.
///
/// This is the core implementation of automatic tool execution mode. It:
///
/// 1. Collects all blocks from the current stream
/// 2. Separates text blocks from tool use blocks
/// 3. If there are tool blocks:
/// - Executes PreToolUse hooks (can modify/block)
/// - Executes each tool via its registered function
/// - Executes PostToolUse hooks (can modify result)
/// - Adds results to history
/// - Continues conversation with send("")
/// 4. Repeats until text-only response or max iterations
/// 5. Returns all final text blocks
///
/// # Returns
///
/// - `Ok(blocks)`: Final text blocks after all tool iterations
/// - `Err(e)`: Error during execution, stream processing, or interruption
///
/// # Iteration Limit
///
/// The loop is bounded by `options.max_tool_iterations` to prevent infinite loops.
/// When the limit is reached, the loop stops and returns whatever text blocks
/// have been collected so far.
///
/// # Hook Integration
///
/// Hooks are executed for each tool call:
/// - **PreToolUse**: Can modify input or block execution entirely
/// - **PostToolUse**: Can modify the result before it's added to history
///
/// If a hook blocks execution, a JSON error response is used as the tool result.
///
/// # State Management
///
/// The loop maintains history by adding:
/// - Assistant messages with text + tool use blocks
/// - User messages with tool result blocks
///
/// This creates a proper conversation flow that the model can follow.
///
/// # Error Recovery
///
/// If a tool execution fails, the error is converted to a JSON error response
/// and added as the tool result. This allows the conversation to continue
/// and lets the model handle the error.
async fn auto_execute_loop(&mut self) -> Result<Vec<ContentBlock>> {
use crate::types::ToolResultBlock;
// Track iterations to prevent infinite loops
let mut iteration = 0;
let max_iterations = self.options.max_tool_iterations();
loop {
// ========================================================================
// STEP 1: Collect all blocks from current stream
// ========================================================================
// Buffer the entire response to determine if it contains tool calls
let blocks = self.collect_all_blocks().await?;
// Empty response means stream ended or was interrupted
if blocks.is_empty() {
return Ok(Vec::new());
}
// ========================================================================
// STEP 2: Separate text blocks from tool use blocks
// ========================================================================
// The model can return a mix of text and tool calls in one response
let mut text_blocks = Vec::new();
let mut tool_blocks = Vec::new();
for block in blocks {
match block {
ContentBlock::Text(_) => text_blocks.push(block),
ContentBlock::ToolUse(_) => tool_blocks.push(block),
ContentBlock::ToolResult(_) | ContentBlock::Image(_) => {} // Ignore ToolResult and Image variants
}
}
// ========================================================================
// STEP 3: Check if we're done (no tool calls)
// ========================================================================
// If the response contains no tool calls, we've reached the final answer
if tool_blocks.is_empty() {
// Add assistant's final text response to history
if !text_blocks.is_empty() {
let assistant_msg = Message::assistant(text_blocks.clone());
self.history.push(assistant_msg);
}
// Return text blocks to caller via buffered receive()
return Ok(text_blocks);
}
// ========================================================================
// STEP 4: Check iteration limit BEFORE executing tools
// ========================================================================
// Increment counter and check if we've hit the max
iteration += 1;
if iteration > max_iterations {
// Max iterations reached - stop execution and return what we have
// This prevents infinite tool-calling loops.
//
// The SDK, not the model, ended this operation. The last stream's reason was
// `ToolCalls` — true of that generation, but misleading as the answer to
// "why did this stop?", which is the question `finish_reason()` exists to
// answer. Report the reason we are responsible for.
self.last_finish_reason = Some(FinishReason::MaxToolIterations);
if !text_blocks.is_empty() {
let assistant_msg = Message::assistant(text_blocks.clone());
self.history.push(assistant_msg);
}
return Ok(text_blocks);
}
// ========================================================================
// STEP 5: Add assistant message to history
// ========================================================================
// The assistant message includes BOTH text and tool use blocks
// This preserves the full context for future turns
let mut all_blocks = text_blocks.clone();
all_blocks.extend(tool_blocks.clone());
let assistant_msg = Message::assistant(all_blocks);
self.history.push(assistant_msg);
// ========================================================================
// STEP 6: Execute all tools and collect results
// ========================================================================
for block in tool_blocks {
if let ContentBlock::ToolUse(tool_use) = block {
let has_pre_hooks = !self.options.hooks().pre_tool_use.is_empty();
let has_post_hooks = !self.options.hooks().post_tool_use.is_empty();
let mut history_snapshot = if has_pre_hooks || has_post_hooks {
Some(serialize_history_snapshot(&self.history)?)
} else {
None
};
// ============================================================
// Execute PreToolUse hooks
// ============================================================
use crate::hooks::PreToolUseEvent;
// Track whether to execute and what input to use
let mut tool_input = tool_use.input().clone();
let mut should_execute = true;
let mut block_reason = None;
if has_pre_hooks {
let pre_history = if has_post_hooks {
history_snapshot
.as_ref()
.expect("hook history snapshot should exist")
.clone()
} else {
history_snapshot
.take()
.expect("hook history snapshot should exist")
};
let pre_event = PreToolUseEvent::new(
tool_use.name().to_string(),
tool_use.input().clone(),
tool_use.id().to_string(),
pre_history,
);
if let Some(decision) =
self.options.hooks().execute_pre_tool_use(pre_event).await
{
if !decision.continue_execution() {
should_execute = false;
block_reason = decision.reason().map(ToString::to_string);
} else if let Some(modified) = decision.modified_input() {
tool_input = modified.clone();
}
}
}
// ============================================================
// Execute tool (or create error result if blocked)
// ============================================================
let result = if should_execute {
// Actually execute the tool
match self
.execute_tool_internal(tool_use.name(), tool_input.clone())
.await
{
Ok(res) => res, // Success - use the result
Err(e) => {
// Tool execution failed - convert to JSON error
// This allows the conversation to continue
serde_json::json!({
"error": e.to_string(),
"tool": tool_use.name(),
"id": tool_use.id()
})
}
}
} else {
// Tool blocked by PreToolUse hook - create error result
serde_json::json!({
"error": "Tool execution blocked by hook",
"reason": block_reason.unwrap_or_else(|| "No reason provided".to_string()),
"tool": tool_use.name(),
"id": tool_use.id()
})
};
// ============================================================
// Execute PostToolUse hooks
// ============================================================
let mut final_result = result;
if has_post_hooks {
use crate::hooks::PostToolUseEvent;
let pending_result = Message::user_with_blocks(vec![
ContentBlock::ToolResult(ToolResultBlock::new(
tool_use.id(),
final_result.clone(),
)),
]);
let mut post_history_snapshot = history_snapshot
.take()
.expect("hook history snapshot should exist");
post_history_snapshot.push(serialize_history_message(&pending_result)?);
let post_event = PostToolUseEvent::new(
tool_use.name().to_string(),
tool_input,
tool_use.id().to_string(),
final_result.clone(),
post_history_snapshot,
);
if let Some(decision) = self
.options
.hooks()
.execute_post_tool_use(post_event)
.await
{
// modified_input is historical naming for result replacement.
if let Some(modified) = decision.modified_input() {
final_result = modified.clone();
}
}
}
// ============================================================
// Add tool result to history
// ============================================================
// Tool results are added as user messages (per OpenAI convention)
let tool_result = ToolResultBlock::new(tool_use.id(), final_result);
let tool_result_msg =
Message::user_with_blocks(vec![ContentBlock::ToolResult(tool_result)]);
self.history.push(tool_result_msg);
}
}
// ========================================================================
// STEP 7: Continue conversation to get next response
// ========================================================================
// Send empty string to continue - the history contains all context.
//
// `send()` starts a new stream, which resets the per-stream observations. That is
// right for the finish reason (the last round's is the one that matters) but wrong
// for reasoning: the deliberation that chose these tools is exactly the part worth
// keeping, so carry it across the boundary and let the next stream append to it.
let carried_reasoning = std::mem::take(&mut self.last_reasoning);
self.send("").await?;
self.last_reasoning = carried_reasoning;
// Loop continues to collect and process the next response
// This will either be more tool calls or the final text answer
}
}
}