From 59eb2bf153d7f57121adabc0497fb481ce10abd2 Mon Sep 17 00:00:00 2001 From: buua436 Date: Tue, 22 Sep 2026 19:16:16 +0800 Subject: [PATCH] fix: prevent agent message template feedback (#20042) --- internal/service/agent.go | 24 ++++++++++++++++++----- internal/service/agent_run_e2e_test.go | 4 ++-- internal/service/agent_test.go | 27 ++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/internal/service/agent.go b/internal/service/agent.go index f81ab5708a..3e606bdc9d 100644 --- a/internal/service/agent.go +++ b/internal/service/agent.go @@ -2310,7 +2310,7 @@ func (s *AgentService) buildRunFunc(canvasID string, versionRow *entity.UserCanv return state, err } if shouldTreatAsCompletedLoopRun(err, answer) { - appendAssistantHistory(state, assistantOutput) + appendAssistantHistory(state, partialAssistantOutput(answer, downloads, attachment)) if persistErr := s.persistAgentRunSession(ctx, canvasID, userID, sessionID, messageID, userInput, answer, thinking, referencePayload, dsl, state, true); persistErr != nil { s.markRunFailed(ctx2, runID, "persist session: "+persistErr.Error()) return nil, canvas.NewInternalRunError( @@ -2331,7 +2331,7 @@ func (s *AgentService) buildRunFunc(canvasID string, versionRow *entity.UserCanv wfPayload := map[string]interface{}{ "inputs": map[string]any{"query": userInput}, - "outputs": workflowOutputs(answer, downloads, attachment), + "outputs": workflowOutputsFromTerminal(assistantOutput), "elapsed_time": now - startedAt, "created_at": now, } @@ -2370,8 +2370,11 @@ func (s *AgentService) buildRunFunc(canvasID string, versionRow *entity.UserCanv return nil, canvasInvokeError(err) } - // Emit message + message_end (mirrors Python's ans dict). - appendAssistantHistory(state, assistantOutput) + // Persist the Agent answer for subsequent Agent prompts. The terminal + // Message output is presentation data and may wrap the answer with + // template literals; retaining it in the prompt history makes the model + // reproduce and amplify those literals on later turns. + appendAssistantHistory(state, partialAssistantOutput(answer, downloads, attachment)) if persistErr := s.persistAgentRunSession(ctx, canvasID, userID, sessionID, messageID, userInput, answer, thinking, referencePayload, dsl, state, true); persistErr != nil { s.markRunFailed(ctx2, runID, "persist session: "+persistErr.Error()) return nil, canvas.NewInternalRunError( @@ -2394,7 +2397,7 @@ func (s *AgentService) buildRunFunc(canvasID string, versionRow *entity.UserCanv // per-run token usage across all LLM calls in this turn. wfPayload := map[string]interface{}{ "inputs": map[string]any{"query": userInput}, - "outputs": workflowOutputs(answer, downloads, attachment), + "outputs": workflowOutputsFromTerminal(assistantOutput), "elapsed_time": now - startedAt, "created_at": now, } @@ -2506,6 +2509,17 @@ func workflowOutputs(content string, downloads, attachment any) any { return out } +// workflowOutputsFromTerminal preserves the terminal component's complete +// visible content in workflow_finished while retaining the established compact +// string shape when it has no downloads or attachment. +func workflowOutputsFromTerminal(output map[string]any) any { + if len(output) == 0 { + return "" + } + content, _ := output["content"].(string) + return workflowOutputs(content, output["downloads"], output["attachment"]) +} + // emptyAttachmentValue reports whether an attachment descriptor is // absent or empty (nil or an empty map), matching the omitempty // semantics of the message_end frame. diff --git a/internal/service/agent_run_e2e_test.go b/internal/service/agent_run_e2e_test.go index 49eb47364c..da08c65b31 100644 --- a/internal/service/agent_run_e2e_test.go +++ b/internal/service/agent_run_e2e_test.go @@ -327,8 +327,8 @@ func TestRunAgent_SessionHistoryFeedsSysHistoryAndPersists(t *testing.T) { } if !strings.HasPrefix(secondHistory[0], "assistant: ") || !strings.Contains(secondHistory[0], `'content': '["user: hi"]'`) || - !strings.Contains(secondHistory[0], `'downloads': []`) { - t.Fatalf("assistant history entry = %q, want persisted Message content and downloads", secondHistory[0]) + strings.Contains(secondHistory[0], `'downloads': []`) { + t.Fatalf("assistant history entry = %q, want raw Agent output without Message presentation fields", secondHistory[0]) } if secondHistory[1] != "user: again" || secondHistory[2] != "user: hi" { t.Fatalf("sorted user history = %#v, want [user: again, user: hi]", secondHistory[1:]) diff --git a/internal/service/agent_test.go b/internal/service/agent_test.go index 3189a7024f..c41a4a8c06 100644 --- a/internal/service/agent_test.go +++ b/internal/service/agent_test.go @@ -407,6 +407,33 @@ func TestWorkflowOutputs_WithAttachment(t *testing.T) { } } +func TestWorkflowOutputsFromTerminalUsesMessageContent(t *testing.T) { + output := map[string]any{ + "content": "****\nvisible answer\n++++", + "downloads": []any{}, + } + if got := workflowOutputsFromTerminal(output); got != "****\nvisible answer\n++++" { + t.Fatalf("workflowOutputsFromTerminal = %#v, want complete Message content", got) + } +} + +func TestPartialAssistantOutputExcludesMessagePresentation(t *testing.T) { + state := canvas.NewCanvasState("run-history", "session-history") + appendAssistantHistory(state, partialAssistantOutput("model answer", nil, nil)) + + history := state.SnapshotHistory() + if len(history) != 1 { + t.Fatalf("history length = %d, want 1", len(history)) + } + if history[0]["content"] != "model answer" { + t.Fatalf("history content = %#v, want raw Agent answer", history[0]["content"]) + } + payload, _ := history[0]["payload"].(map[string]any) + if _, ok := payload["downloads"]; ok { + t.Fatalf("history payload unexpectedly contains Message presentation fields: %#v", payload) + } +} + // TestGetVersion_Success verifies getting a specific version by ID. func TestGetVersion_Success(t *testing.T) { testDB := setupServiceTestDB(t)