feat: real-time tool call streaming indicator with blinking UI
- Add ToolParsingHint enum (Detected/Active/Complete) for UI feedback - New UiWriter methods: print_tool_streaming_hint(), print_tool_streaming_active() - Refactor ConsoleUiWriter state to use atomics in ParsingHintState - Add tool_call_streaming field to CompletionChunk for provider hints - Anthropic provider sends streaming hints when tool name detected - New streaming helpers: make_tool_streaming_hint(), make_tool_streaming_active() Parser improvements: - Add is_json_invalidated() to detect false positive tool patterns - Fix tool result poisoning when file contents contain partial JSON - Unescaped newlines in strings or prose after JSON invalidates detection User sees ' ● tool_name |' immediately when tool call starts streaming, with blinking indicator while args are received.
This commit is contained in:
@@ -112,7 +112,7 @@ use tracing::{debug, error};
|
||||
use crate::{
|
||||
streaming::{
|
||||
decode_utf8_streaming, make_final_chunk, make_final_chunk_with_reason, make_text_chunk,
|
||||
make_tool_chunk,
|
||||
make_tool_chunk, make_tool_streaming_active, make_tool_streaming_hint,
|
||||
},
|
||||
CompletionChunk, CompletionRequest, CompletionResponse, CompletionStream, LLMProvider, Message,
|
||||
MessageRole, Tool, ToolCall, Usage,
|
||||
@@ -512,6 +512,12 @@ impl AnthropicProvider {
|
||||
} else {
|
||||
// Arguments are empty, we'll accumulate them from partial_json
|
||||
debug!("Tool call has empty args, will accumulate from partial_json");
|
||||
// Send a streaming hint so the UI can show the tool name immediately
|
||||
let hint_chunk = make_tool_streaming_hint(name.clone());
|
||||
if tx.send(Ok(hint_chunk)).await.is_err() {
|
||||
debug!("Receiver dropped, stopping stream");
|
||||
return accumulated_usage;
|
||||
}
|
||||
current_tool_calls.push(tool_call);
|
||||
partial_tool_json.clear();
|
||||
}
|
||||
@@ -550,6 +556,12 @@ impl AnthropicProvider {
|
||||
"Accumulated tool JSON: {}",
|
||||
partial_tool_json
|
||||
);
|
||||
// Send an active hint to trigger UI blink
|
||||
let active_chunk = make_tool_streaming_active();
|
||||
if tx.send(Ok(active_chunk)).await.is_err() {
|
||||
debug!("Receiver dropped, stopping stream");
|
||||
return accumulated_usage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +494,7 @@ impl DatabricksProvider {
|
||||
usage: None,
|
||||
tool_calls: None,
|
||||
stop_reason: None,
|
||||
tool_call_streaming: None,
|
||||
};
|
||||
if tx.send(Ok(text_chunk)).await.is_err() {
|
||||
debug!("Receiver dropped");
|
||||
|
||||
@@ -205,6 +205,8 @@ pub struct CompletionChunk {
|
||||
pub usage: Option<Usage>, // Add usage tracking for streaming
|
||||
/// Stop reason from the API (e.g., "end_turn", "max_tokens", "stop_sequence")
|
||||
pub stop_reason: Option<String>,
|
||||
/// Tool call currently being streamed (name only, for UI hint)
|
||||
pub tool_call_streaming: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -158,6 +158,7 @@ impl OpenAIProvider {
|
||||
tool_calls,
|
||||
usage: accumulated_usage.clone(),
|
||||
stop_reason: None, // TODO: Extract from OpenAI response
|
||||
tool_call_streaming: None,
|
||||
};
|
||||
let _ = tx.send(Ok(final_chunk)).await;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ pub fn make_final_chunk(tool_calls: Vec<ToolCall>, usage: Option<Usage>) -> Comp
|
||||
Some(tool_calls)
|
||||
},
|
||||
stop_reason: None,
|
||||
tool_call_streaming: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ pub fn make_final_chunk_with_reason(tool_calls: Vec<ToolCall>, usage: Option<Usa
|
||||
Some(tool_calls)
|
||||
},
|
||||
stop_reason,
|
||||
tool_call_streaming: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +90,7 @@ pub fn make_text_chunk(content: String) -> CompletionChunk {
|
||||
usage: None,
|
||||
tool_calls: None,
|
||||
stop_reason: None,
|
||||
tool_call_streaming: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,5 +102,31 @@ pub fn make_tool_chunk(tool_calls: Vec<ToolCall>) -> CompletionChunk {
|
||||
usage: None,
|
||||
tool_calls: Some(tool_calls),
|
||||
stop_reason: None,
|
||||
tool_call_streaming: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a hint chunk indicating a tool call is being streamed.
|
||||
pub fn make_tool_streaming_hint(tool_name: String) -> CompletionChunk {
|
||||
CompletionChunk {
|
||||
content: String::new(),
|
||||
finished: false,
|
||||
usage: None,
|
||||
tool_calls: None,
|
||||
stop_reason: None,
|
||||
tool_call_streaming: Some(tool_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a hint chunk indicating a tool call is still actively streaming.
|
||||
/// This is used to trigger UI updates (like blinking indicators) during long tool calls.
|
||||
pub fn make_tool_streaming_active() -> CompletionChunk {
|
||||
CompletionChunk {
|
||||
content: String::new(),
|
||||
finished: false,
|
||||
usage: None,
|
||||
tool_calls: None,
|
||||
stop_reason: None,
|
||||
tool_call_streaming: Some(String::new()), // Empty string signals "active" vs "detected"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user