From 8f1237d64fac68902b7c242fa546e357cf993252 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Thu, 19 Mar 2026 17:21:13 +0800 Subject: [PATCH] feat(agent): enhance event emission for final answers and fallback handling - Updated the AgentEngine to emit final answer events even when the LLM stops without calling the final_answer tool, ensuring the frontend can render responses correctly. - Added fallback logic in the AgentStreamHandler to emit final answers as fallback events when no answer events are streamed, improving robustness in edge cases. --- internal/agent/engine.go | 19 ++++++++-- .../handler/session/agent_stream_handler.go | 36 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) 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,