diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 5b98f9c701..eced7bc7bb 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -75,7 +75,9 @@ fn track_tool_telemetry(content: &MessageContent, all_messages: &[Message]) { #[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] pub struct ChatRequest { - messages: Vec, + user_message: Message, + #[serde(default)] + conversation_so_far: Option>, session_id: String, recipe_name: Option, recipe_version: Option, @@ -233,7 +235,8 @@ pub async fn reply( let stream = ReceiverStream::new(rx); let cancel_token = CancellationToken::new(); - let messages = Conversation::new_unvalidated(request.messages); + let user_message = request.user_message; + let conversation_so_far = request.conversation_so_far; let task_cancel = cancel_token.clone(); let task_tx = tx.clone(); @@ -255,7 +258,7 @@ pub async fn reply( } }; - let session = match SessionManager::get_session(&session_id, false).await { + let session = match SessionManager::get_session(&session_id, true).await { Ok(metadata) => metadata, Err(e) => { tracing::error!("Failed to read session for {}: {}", session_id, e); @@ -278,20 +281,21 @@ pub async fn reply( retry_config: None, }; - let user_message = match messages.last() { - Some(msg) => msg, - _ => { - let _ = stream_event( - MessageEvent::Error { - error: "Reply started with empty messages".to_string(), - }, - &task_tx, - &task_cancel, - ) - .await; - return; + let mut all_messages = match conversation_so_far { + Some(history) => { + let conv = Conversation::new_unvalidated(history); + if let Err(e) = SessionManager::replace_conversation(&session_id, &conv).await { + tracing::warn!( + "Failed to replace session conversation for {}: {}", + session_id, + e + ); + } + conv } + None => session.conversation.unwrap_or_default(), }; + all_messages.push(user_message.clone()); let mut stream = match agent .reply( @@ -316,8 +320,6 @@ pub async fn reply( } }; - let mut all_messages = messages.clone(); - let mut heartbeat_interval = tokio::time::interval(Duration::from_millis(500)); loop { tokio::select! { @@ -478,7 +480,8 @@ mod tests { .header("x-secret-key", "test-secret") .body(Body::from( serde_json::to_string(&ChatRequest { - messages: vec![Message::user().with_text("test message")], + user_message: Message::user().with_text("test message"), + conversation_so_far: None, session_id: "test-session".to_string(), recipe_name: None, recipe_version: None, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 4d0e0a6342..ca63e46a53 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -2715,15 +2715,16 @@ "ChatRequest": { "type": "object", "required": [ - "messages", + "user_message", "session_id" ], "properties": { - "messages": { + "conversation_so_far": { "type": "array", "items": { "$ref": "#/components/schemas/Message" - } + }, + "nullable": true }, "recipe_name": { "type": "string", @@ -2735,6 +2736,9 @@ }, "session_id": { "type": "string" + }, + "user_message": { + "$ref": "#/components/schemas/Message" } } }, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 0166872c13..ea60af4a5a 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -59,10 +59,11 @@ export type CallToolResponse = { }; export type ChatRequest = { - messages: Array; + conversation_so_far?: Array | null; recipe_name?: string | null; recipe_version?: string | null; session_id: string; + user_message: Message; }; export type CheckProviderRequest = { diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 01555361b5..41580f7609 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -292,12 +292,13 @@ export function useChatStream({ window.dispatchEvent(new CustomEvent('session-created')); } - // Build message list: add new message if provided, otherwise continue with existing + const newMessage = hasNewMessage + ? createUserMessage(userMessage) + : messagesRef.current[messagesRef.current.length - 1]; const currentMessages = hasNewMessage - ? [...messagesRef.current, createUserMessage(userMessage)] + ? [...messagesRef.current, newMessage] : [...messagesRef.current]; - // Update UI with new message before streaming if (hasNewMessage) { updateMessages(currentMessages); } @@ -310,7 +311,7 @@ export function useChatStream({ const { stream } = await reply({ body: { session_id: sessionId, - messages: currentMessages, + user_message: newMessage, }, throwOnError: true, signal: abortControllerRef.current.signal, @@ -356,7 +357,7 @@ export function useChatStream({ const { stream } = await reply({ body: { session_id: sessionId, - messages: currentMessages, + user_message: responseMessage, }, throwOnError: true, signal: abortControllerRef.current.signal,