From 452c9d0363c831db71e74294880acfac583afa0a Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Tue, 27 Jan 2026 04:02:11 +0000 Subject: [PATCH] shelley: don't auto-start agent on 'continue in new conversation' Prompt: When we click on 'continue in new conversation', we send the message right away to the agent. The user often wants to add extra instructions. What would be a good way to get the conversation created, but not call the agent? Propose a solution for me. Follow-up: What do you think of option C where we just don't send the message? It's not really pending, and it's not really editable, but the user can add another message and hit send, and both will get sent. Follow-up: Yes, let's do it. When users click 'Continue in new conversation', they often want to add extra instructions before the agent starts working. This change: 1. Records the conversation summary as a user message 2. Does NOT start the agent loop automatically 3. Returns status 'created' instead of 'accepted' The user sees their summary message displayed and can then type additional instructions before sending. When they send a message, the agent will receive both the summary and their new instructions in context. This gives users control over when the agent starts, rather than immediately kicking off a potentially long-running task. Co-authored-by: Shelley --- server/handlers.go | 64 +++++++++++++----------------------------- test/server_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 528bda07bda562656533256d515481ccbfc9d7ae..03bbd0d89db6b01fe4e606af0550f3796fd719e5 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -782,7 +782,8 @@ type ContinueConversationRequest struct { } // handleContinueConversation handles POST /api/conversations/continue -// Creates a new conversation with a summary of the source conversation as the initial message +// Creates a new conversation with a summary of the source conversation as the initial user message, +// but does NOT start the agent. The user can then add additional instructions before sending. func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -826,7 +827,7 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque } summary := buildConversationSummary(sourceSlug, messages) - // Get LLM service for the requested model + // Determine model to use modelID := req.Model if modelID == "" && sourceConv.Model != nil { modelID = *sourceConv.Model @@ -835,13 +836,6 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque modelID = "qwen3-coder-fireworks" } - llmService, err := s.llmManager.GetService(modelID) - if err != nil { - s.logger.Error("Unsupported model requested", "model", modelID, "error", err) - http.Error(w, fmt.Sprintf("Unsupported model: %s", modelID), http.StatusBadRequest) - return - } - // Create new conversation with cwd from request or source conversation var cwdPtr *string if req.Cwd != "" { @@ -863,19 +857,8 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque Conversation: conversation, }) - // Get or create conversation manager - manager, err := s.getOrCreateConversationManager(ctx, conversationID) - if errors.Is(err, errConversationModelMismatch) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if err != nil { - s.logger.Error("Failed to get conversation manager", "conversationID", conversationID, "error", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Create user message with the summary + // Create and record the user message with the summary, but do NOT start the agent loop. + // This allows the user to see the summary and add additional instructions before sending. userMessage := llm.Message{ Role: llm.MessageRoleUser, Content: []llm.Content{ @@ -883,36 +866,29 @@ func (s *Server) handleContinueConversation(w http.ResponseWriter, r *http.Reque }, } - firstMessage, err := manager.AcceptUserMessage(ctx, llmService, modelID, userMessage) - if errors.Is(err, errConversationModelMismatch) { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if err != nil { - s.logger.Error("Failed to accept user message", "conversationID", conversationID, "error", err) + if err := s.recordMessage(ctx, conversationID, userMessage, llm.Usage{}); err != nil { + s.logger.Error("Failed to record summary message", "conversationID", conversationID, "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - // Generate slug for the new conversation - if firstMessage { - ctxNoCancel := context.WithoutCancel(ctx) - go func() { - slugCtx, cancel := context.WithTimeout(ctxNoCancel, 15*time.Second) - defer cancel() - _, err := slug.GenerateSlug(slugCtx, s.llmManager, s.db, s.logger, conversationID, summary, modelID) - if err != nil { - s.logger.Warn("Failed to generate slug for conversation", "conversationID", conversationID, "error", err) - } else { - go s.notifySubscribers(ctxNoCancel, conversationID) - } - }() - } + // Generate slug for the new conversation in background + ctxNoCancel := context.WithoutCancel(ctx) + go func() { + slugCtx, cancel := context.WithTimeout(ctxNoCancel, 15*time.Second) + defer cancel() + _, err := slug.GenerateSlug(slugCtx, s.llmManager, s.db, s.logger, conversationID, summary, modelID) + if err != nil { + s.logger.Warn("Failed to generate slug for conversation", "conversationID", conversationID, "error", err) + } else { + go s.notifySubscribers(ctxNoCancel, conversationID) + } + }() w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "accepted", + "status": "created", "conversation_id": conversationID, }) } diff --git a/test/server_test.go b/test/server_test.go index 1e27af8e3be76d97966af573145d474afc331335..223f555df507cedecd8602abe8da89b685272f0c 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -1455,5 +1455,73 @@ func TestContinueConversation(t *testing.T) { t.Error("Summary should contain tool name") } + // Verify that the agent was NOT started - there should only be a user message, + // no agent response. The user should be able to add instructions before sending. + var hasAgentMessage bool + for _, msg := range messages { + if msg.Type == string(db.MessageTypeAgent) { + hasAgentMessage = true + break + } + } + if hasAgentMessage { + t.Error("Expected no agent message - the agent should NOT be started automatically") + } + + // Verify the status is "created" not "accepted" (since agent wasn't started) + if status, ok := result["status"].(string); !ok || status != "created" { + t.Errorf("Expected status 'created', got %v", result["status"]) + } + t.Logf("Successfully continued conversation from %s to %s", sourceConv.ConversationID, newConversationID) + + // Now test that sending a follow-up message works correctly. + // The agent should receive both the summary message AND the new message. + followUpReq := map[string]string{ + "message": "Please focus on the bash commands.", + "model": "predictable", + } + followUpBody, _ := json.Marshal(followUpReq) + + followUpResp, err := http.Post(testServer.URL+"/api/conversation/"+newConversationID+"/chat", "application/json", bytes.NewBuffer(followUpBody)) + if err != nil { + t.Fatalf("Failed to send follow-up message: %v", err) + } + defer followUpResp.Body.Close() + + if followUpResp.StatusCode != http.StatusAccepted { + bodyBytes, _ := io.ReadAll(followUpResp.Body) + t.Fatalf("Expected status 202 for follow-up, got %d: %s", followUpResp.StatusCode, string(bodyBytes)) + } + + // Wait briefly for the agent to process + time.Sleep(200 * time.Millisecond) + + // Verify we now have messages: summary (user), follow-up (user), and agent response + updatedMessages, err := database.ListMessages(ctx, newConversationID) + if err != nil { + t.Fatalf("Failed to list updated messages: %v", err) + } + + // Count message types + userCount := 0 + agentCount := 0 + for _, msg := range updatedMessages { + switch msg.Type { + case string(db.MessageTypeUser): + userCount++ + case string(db.MessageTypeAgent): + agentCount++ + } + } + + // Should have 2 user messages (summary + follow-up) and at least 1 agent response + if userCount != 2 { + t.Errorf("Expected 2 user messages, got %d", userCount) + } + if agentCount < 1 { + t.Errorf("Expected at least 1 agent message after follow-up, got %d", agentCount) + } + + t.Logf("Follow-up message processed: %d user messages, %d agent messages", userCount, agentCount) }