feat(session): implement streaming state with text delta and tool‑call diff handling

- Introduce `StreamingMessageState` to track full text, last tool calls, and completion.
- Add `StreamDiff`, `TextDelta`, and `TextDeltaKind` for describing incremental changes.
- SessionController now maintains a `stream_states` map keyed by response IDs.
- `apply_stream_chunk` uses the new state to emit append/replace text deltas and tool‑call updates, handling final chunks and cleanup.
- `Conversation` gains `set_stream_content` to replace streaming content and manage metadata.
- Ensure stream state is cleared on cancel, conversation reset, and controller clear.
This commit is contained in:
2025-10-18 07:15:12 +02:00
parent 4820a6706f
commit c7b7fe98ec
2 changed files with 247 additions and 8 deletions

View File

@@ -190,6 +190,46 @@ impl ConversationManager {
Ok(())
}
/// Replace the current streaming content for a message.
pub fn set_stream_content(
&mut self,
message_id: Uuid,
content: impl Into<String>,
is_final: bool,
) -> Result<()> {
let index = self
.message_index
.get(&message_id)
.copied()
.ok_or_else(|| crate::Error::Unknown(format!("Unknown message id: {message_id}")))?;
let conversation = self.active_mut();
if let Some(message) = conversation.messages.get_mut(index) {
message.content = content.into();
message.metadata.remove(PLACEHOLDER_FLAG);
message.timestamp = std::time::SystemTime::now();
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
message.metadata.insert(
LAST_CHUNK_TS.to_string(),
Value::Number(Number::from(millis)),
);
if is_final {
message
.metadata
.insert(STREAMING_FLAG.to_string(), Value::Bool(false));
self.streaming.remove(&message_id);
} else if let Some(info) = self.streaming.get_mut(&message_id) {
info.last_update = Instant::now();
}
}
Ok(())
}
/// Set placeholder text for a streaming message
pub fn set_stream_placeholder(
&mut self,
@@ -254,7 +294,11 @@ impl ConversationManager {
.ok_or_else(|| crate::Error::Unknown(format!("Unknown message id: {message_id}")))?;
if let Some(message) = self.active_mut().messages.get_mut(index) {
message.tool_calls = Some(tool_calls);
if tool_calls.is_empty() {
message.tool_calls = None;
} else {
message.tool_calls = Some(tool_calls);
}
}
Ok(())