diff --git a/internal/agent/engine.go b/internal/agent/engine.go index 5fa5692b..0a79b0a3 100644 --- a/internal/agent/engine.go +++ b/internal/agent/engine.go @@ -339,9 +339,24 @@ func (e *AgentEngine) executeLoop( state.IsComplete = true state.RoundSteps = append(state.RoundSteps, step) - // Emit final answer done marker + // When the LLM stops without calling final_answer tool, its response content + // was only streamed as thinking events. We must also emit it as an answer event + // so the frontend can render it in the main content area (UI depends on answer events). + answerID := generateEventID("answer") + if response.Content != "" { + e.eventBus.Emit(ctx, event.Event{ + ID: answerID, + Type: event.EventAgentFinalAnswer, + SessionID: sessionID, + Data: event.AgentFinalAnswerData{ + Content: response.Content, + Done: false, + }, + }) + } + e.eventBus.Emit(ctx, event.Event{ - ID: generateEventID("answer-done"), + ID: answerID, Type: event.EventAgentFinalAnswer, SessionID: sessionID, Data: event.AgentFinalAnswerData{ diff --git a/internal/handler/session/agent_stream_handler.go b/internal/handler/session/agent_stream_handler.go index db1a3e55..f53c3b23 100644 --- a/internal/handler/session/agent_stream_handler.go +++ b/internal/handler/session/agent_stream_handler.go @@ -445,6 +445,42 @@ func (h *AgentStreamHandler) handleComplete(ctx context.Context, evt event.Event } } + // Fallback: if no answer events were streamed but we have a final answer, + // emit it as answer events so the frontend can render it properly. + // This guards against edge cases where the LLM stops without calling final_answer. + if h.finalAnswer == "" && data.FinalAnswer != "" { + logger.GetLogger(h.ctx).Warnf( + "No answer events were streamed, emitting fallback answer (len=%d)", len(data.FinalAnswer), + ) + fallbackID := fmt.Sprintf("answer-fallback-%d", time.Now().UnixMilli()) + if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{ + ID: fallbackID, + Type: types.ResponseTypeAnswer, + Content: data.FinalAnswer, + Done: false, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "event_id": fallbackID, + "is_fallback": true, + }, + }); err != nil { + logger.GetLogger(h.ctx).Errorf("Append fallback answer event failed: %v", err) + } + if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{ + ID: fallbackID, + Type: types.ResponseTypeAnswer, + Content: "", + Done: true, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "event_id": fallbackID, + "is_fallback": true, + }, + }); err != nil { + logger.GetLogger(h.ctx).Errorf("Append fallback answer done event failed: %v", err) + } + } + // Send completion event to stream manager so SSE can detect completion if err := h.streamManager.AppendEvent(h.ctx, h.sessionID, h.assistantMessageID, interfaces.StreamEvent{ ID: evt.ID,