From 6d04d15ce064c8f05f20f898983fb73154b6e44f Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:40:55 +0800 Subject: [PATCH] fix(tool-feedback): dedupe duplicate content and keep full explanations --- pkg/agent/agent_outbound.go | 13 ++-- pkg/agent/agent_test.go | 90 ++++++++++++++++++++------ pkg/agent/agent_utils.go | 10 ++- pkg/agent/pipeline_execute.go | 2 - pkg/agent/pipeline_llm.go | 1 - pkg/config/config.go | 2 +- pkg/utils/tool_feedback_dedupe.go | 39 +++++++++++ pkg/utils/tool_feedback_dedupe_test.go | 55 ++++++++++++++++ pkg/utils/visible_tool_calls.go | 3 - pkg/utils/visible_tool_calls_test.go | 33 ++++++++++ web/backend/api/session.go | 3 + web/backend/api/session_test.go | 83 ++++++++++++++++++++---- web/frontend/src/i18n/locales/en.json | 4 +- web/frontend/src/i18n/locales/zh.json | 4 +- 14 files changed, 288 insertions(+), 54 deletions(-) create mode 100644 pkg/utils/tool_feedback_dedupe.go create mode 100644 pkg/utils/tool_feedback_dedupe_test.go create mode 100644 pkg/utils/visible_tool_calls_test.go diff --git a/pkg/agent/agent_outbound.go b/pkg/agent/agent_outbound.go index fcf8cf1a1..1728f6f79 100644 --- a/pkg/agent/agent_outbound.go +++ b/pkg/agent/agent_outbound.go @@ -160,7 +160,14 @@ func (al *AgentLoop) publishPicoToolCallInterim( return } - if strings.TrimSpace(content) != "" { + visibleToolCalls := utils.BuildVisibleToolCalls( + toolCalls, + al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(), + ) + duplicateToolCallContent := len(visibleToolCalls) > 0 && + utils.ToolCallExplanationDuplicatesContent(content, toolCalls) + + if strings.TrimSpace(content) != "" && !duplicateToolCallContent { pubCtx, pubCancel := context.WithTimeout(ctx, 3*time.Second) err := al.bus.PublishOutbound(pubCtx, outboundMessageForTurn(ts, content)) pubCancel() @@ -175,10 +182,6 @@ func (al *AgentLoop) publishPicoToolCallInterim( } } - visibleToolCalls := utils.BuildVisibleToolCalls( - toolCalls, - al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(), - ) if len(visibleToolCalls) == 0 { return } diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index 17d169ca6..4047ab74d 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -1892,7 +1892,7 @@ func TestToolFeedbackExplanationFromResponse_UsesCurrentContentFirst(t *testing. {Role: "tool", Content: "tool output", ToolCallID: "call_1"}, } - got := toolFeedbackExplanationFromResponse(response, messages, 300) + got := toolFeedbackExplanationFromResponse(response, messages) if got != "Read README.md first" { t.Fatalf("toolFeedbackExplanationFromResponse() = %q, want current content", got) } @@ -1936,7 +1936,7 @@ func TestToolFeedbackExplanationFromResponse_UsesExplicitToolCallExtraContent(t {Role: "tool", Content: "tool output", ToolCallID: "call_1"}, } - got := toolFeedbackExplanationFromResponse(response, messages, 300) + got := toolFeedbackExplanationFromResponse(response, messages) if got != "Read README.md first to confirm the current project structure." { t.Fatalf("toolFeedbackExplanationFromResponse() = %q, want explicit tool feedback explanation", got) } @@ -1963,8 +1963,8 @@ func TestToolFeedbackExplanationForToolCall_PrefersToolSpecificExtraContent(t *t }, } - got1 := toolFeedbackExplanationForToolCall(response, response.ToolCalls[0], nil, 300) - got2 := toolFeedbackExplanationForToolCall(response, response.ToolCalls[1], nil, 300) + got1 := toolFeedbackExplanationForToolCall(response, response.ToolCalls[0], nil) + got2 := toolFeedbackExplanationForToolCall(response, response.ToolCalls[1], nil) if got1 != "Read README.md first." { t.Fatalf("toolFeedbackExplanationForToolCall() first = %q, want tool-specific explanation", got1) } @@ -1993,7 +1993,7 @@ func TestToolFeedbackExplanationForToolCall_DoesNotReuseAnotherToolCallExplanati {Role: "user", Content: "inspect the config and update the example"}, } - got := toolFeedbackExplanationForToolCall(response, response.ToolCalls[0], messages, 300) + got := toolFeedbackExplanationForToolCall(response, response.ToolCalls[0], messages) want := utils.ToolFeedbackContinuationHint + ": inspect the config and update the example" if got != want { t.Fatalf("toolFeedbackExplanationForToolCall() = %q, want %q", got, want) @@ -2012,13 +2012,31 @@ func TestToolFeedbackExplanationFromResponse_DoesNotUseReasoningContent(t *testi {Role: "tool", Content: "tool output", ToolCallID: "call_1"}, } - got := toolFeedbackExplanationFromResponse(response, messages, 300) + got := toolFeedbackExplanationFromResponse(response, messages) want := utils.ToolFeedbackContinuationHint + ": Inspect README.md and update the config example." if got != want { t.Fatalf("toolFeedbackExplanationFromResponse() = %q, want latest user content fallback", got) } } +func TestToolFeedbackExplanationForToolCall_DoesNotTruncateLongExplanation(t *testing.T) { + explanation := "Read README.md first to confirm the current project structure before editing the config example." + response := &providers.LLMResponse{ + ToolCalls: []providers.ToolCall{{ + ID: "call_1", + Name: "read_file", + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: explanation, + }, + }}, + } + + got := toolFeedbackExplanationForToolCall(response, response.ToolCalls[0], nil) + if got != explanation { + t.Fatalf("toolFeedbackExplanationForToolCall() = %q, want full explanation", got) + } +} + func TestToolFeedbackArgsPreview_UsesJSONAndTruncates(t *testing.T) { got := toolFeedbackArgsPreview(map[string]any{ "path": "README.md", @@ -2064,6 +2082,43 @@ func (m *picoInterleavedContentProvider) GetDefaultModel() string { return "pico-interleaved-content-model" } +type picoDistinctToolCallContentProvider struct { + calls int +} + +func (m *picoDistinctToolCallContentProvider) Chat( + ctx context.Context, + messages []providers.Message, + tools []providers.ToolDefinition, + model string, + opts map[string]any, +) (*providers.LLMResponse, error) { + m.calls++ + if m.calls == 1 { + return &providers.LLMResponse{ + Content: "intermediate model text", + ToolCalls: []providers.ToolCall{{ + ID: "call_tool_limit_test", + Type: "function", + Name: "tool_limit_test_tool", + Arguments: map[string]any{"value": "x"}, + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: "Read the file before replying.", + }, + }}, + }, nil + } + + return &providers.LLMResponse{ + Content: "final model text", + ToolCalls: []providers.ToolCall{}, + }, nil +} + +func (m *picoDistinctToolCallContentProvider) GetDefaultModel() string { + return "pico-distinct-tool-call-content-model" +} + type toolLimitOnlyProvider struct{} func (m *toolLimitOnlyProvider) Chat( @@ -4398,7 +4453,7 @@ func TestRun_PicoPublishesAssistantContentDuringToolCallsWithoutFinalDuplicate(t } msgBus := bus.NewMessageBus() - provider := &picoInterleavedContentProvider{} + provider := &picoDistinctToolCallContentProvider{} al := NewAgentLoop(cfg, msgBus, provider) agent := al.GetRegistry().GetDefaultAgent() @@ -4562,7 +4617,7 @@ func TestRun_PicoToolFeedbackSuppressesDuplicateInterimAssistantContent(t *testi outputs := make([]bus.OutboundMessage, 0, 3) deadline := time.After(2 * time.Second) - for len(outputs) < 3 { + for len(outputs) < 2 { select { case outbound := <-msgBus.OutboundChan(): outputs = append(outputs, outbound) @@ -4571,20 +4626,17 @@ func TestRun_PicoToolFeedbackSuppressesDuplicateInterimAssistantContent(t *testi } } - if outputs[0].Content != "intermediate model text" { - t.Fatalf("first outbound content = %q, want %q", outputs[0].Content, "intermediate model text") + if outputs[0].Context.Raw[metadataKeyMessageKind] != messageKindToolCalls { + t.Fatalf("first outbound = %+v, want tool_calls message", outputs[0]) } - if outputs[1].Context.Raw[metadataKeyMessageKind] != messageKindToolCalls { - t.Fatalf("second outbound = %+v, want tool_calls message", outputs[1]) + if outputs[0].Content != "" { + t.Fatalf("first outbound content = %q, want empty tool_calls content", outputs[0].Content) } - if outputs[1].Content != "" { - t.Fatalf("second outbound content = %q, want empty tool_calls content", outputs[1].Content) + if !strings.Contains(outputs[0].Context.Raw[metadataKeyToolCalls], "tool_limit_test_tool") { + t.Fatalf("first outbound tool_calls = %q, want tool name", outputs[0].Context.Raw[metadataKeyToolCalls]) } - if !strings.Contains(outputs[1].Context.Raw[metadataKeyToolCalls], "tool_limit_test_tool") { - t.Fatalf("second outbound tool_calls = %q, want tool name", outputs[1].Context.Raw[metadataKeyToolCalls]) - } - if outputs[2].Content != "final model text" { - t.Fatalf("third outbound content = %q, want %q", outputs[2].Content, "final model text") + if outputs[1].Content != "final model text" { + t.Fatalf("second outbound content = %q, want %q", outputs[1].Content, "final model text") } runCancel() diff --git a/pkg/agent/agent_utils.go b/pkg/agent/agent_utils.go index 4ba75cde4..bbfb3f2ae 100644 --- a/pkg/agent/agent_utils.go +++ b/pkg/agent/agent_utils.go @@ -115,7 +115,6 @@ func latestUserContent(messages []providers.Message) string { func toolFeedbackExplanationFromResponse( response *providers.LLMResponse, messages []providers.Message, - maxLen int, ) string { if response == nil { return "" @@ -127,7 +126,7 @@ func toolFeedbackExplanationFromResponse( if explanation == "" { explanation = toolFeedbackExplanationFromMessages(messages) } - return utils.Truncate(explanation, maxLen) + return explanation } func toolFeedbackExplanationFromToolCalls(toolCalls []providers.ToolCall) string { @@ -146,22 +145,21 @@ func toolFeedbackExplanationForToolCall( response *providers.LLMResponse, toolCall providers.ToolCall, messages []providers.Message, - maxLen int, ) string { if toolCall.ExtraContent != nil { if explanation := strings.TrimSpace(toolCall.ExtraContent.ToolFeedbackExplanation); explanation != "" { - return utils.Truncate(explanation, maxLen) + return explanation } } if response == nil { - return utils.Truncate(toolFeedbackExplanationFromMessages(messages), maxLen) + return toolFeedbackExplanationFromMessages(messages) } explanation := strings.TrimSpace(response.Content) if explanation == "" { explanation = toolFeedbackExplanationFromMessages(messages) } - return utils.Truncate(explanation, maxLen) + return explanation } func toolFeedbackExplanationFromMessages(messages []providers.Message) string { diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index c8ad93943..9935f2c9e 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -86,7 +86,6 @@ toolLoop: exec.response, tc, messages, - toolFeedbackMaxLen, ) feedbackMsg := utils.FormatToolFeedbackMessage( toolName, @@ -368,7 +367,6 @@ toolLoop: exec.response, tc, messages, - toolFeedbackMaxLen, ) feedbackMsg := utils.FormatToolFeedbackMessage( toolName, diff --git a/pkg/agent/pipeline_llm.go b/pkg/agent/pipeline_llm.go index 895f00489..6bf55fa39 100644 --- a/pkg/agent/pipeline_llm.go +++ b/pkg/agent/pipeline_llm.go @@ -478,7 +478,6 @@ func (p *Pipeline) CallLLM( exec.response, tc, exec.messages, - al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(), ) extraContent := tc.ExtraContent if strings.TrimSpace(toolFeedbackExplanation) != "" { diff --git a/pkg/config/config.go b/pkg/config/config.go index 6bb8d3ce6..305c3a5c0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -287,7 +287,7 @@ func (d *AgentDefaults) GetMaxMediaSize() int { return DefaultMaxMediaSize } -// GetToolFeedbackMaxArgsLength returns the max visible text length for tool feedback messages. +// GetToolFeedbackMaxArgsLength returns the max visible text length for tool argument previews. func (d *AgentDefaults) GetToolFeedbackMaxArgsLength() int { if d.ToolFeedback.MaxArgsLength > 0 { return d.ToolFeedback.MaxArgsLength diff --git a/pkg/utils/tool_feedback_dedupe.go b/pkg/utils/tool_feedback_dedupe.go new file mode 100644 index 000000000..b1adb60eb --- /dev/null +++ b/pkg/utils/tool_feedback_dedupe.go @@ -0,0 +1,39 @@ +package utils + +import ( + "strings" + + "github.com/sipeed/picoclaw/pkg/providers" +) + +func normalizeToolFeedbackComparisonText(text string) string { + text = strings.ReplaceAll(text, "\r\n", "\n") + text = strings.ReplaceAll(text, "\r", "\n") + text = strings.TrimSpace(text) + if text == "" { + return "" + } + return strings.Join(strings.Fields(text), " ") +} + +func ToolCallExplanationDuplicatesContent(content string, toolCalls []providers.ToolCall) bool { + normalizedContent := normalizeToolFeedbackComparisonText(content) + if normalizedContent == "" || len(toolCalls) == 0 { + return false + } + + for _, tc := range toolCalls { + if tc.ExtraContent == nil { + continue + } + explanation := normalizeToolFeedbackComparisonText(tc.ExtraContent.ToolFeedbackExplanation) + if explanation == "" { + continue + } + if explanation == normalizedContent { + return true + } + } + + return false +} diff --git a/pkg/utils/tool_feedback_dedupe_test.go b/pkg/utils/tool_feedback_dedupe_test.go new file mode 100644 index 000000000..cc587080f --- /dev/null +++ b/pkg/utils/tool_feedback_dedupe_test.go @@ -0,0 +1,55 @@ +package utils + +import ( + "testing" + + "github.com/sipeed/picoclaw/pkg/providers" +) + +func TestToolCallExplanationDuplicatesContent(t *testing.T) { + t.Run("exact duplicate", func(t *testing.T) { + toolCalls := []providers.ToolCall{{ + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: "Read the file before replying.", + }, + }} + + if !ToolCallExplanationDuplicatesContent("Read the file before replying.", toolCalls) { + t.Fatal("expected duplicated content to be detected") + } + }) + + t.Run("whitespace normalized duplicate", func(t *testing.T) { + toolCalls := []providers.ToolCall{{ + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: "Read the file\nbefore replying.", + }, + }} + + if !ToolCallExplanationDuplicatesContent(" Read the file before replying. ", toolCalls) { + t.Fatal("expected whitespace-only differences to be ignored") + } + }) + + t.Run("distinct content", func(t *testing.T) { + toolCalls := []providers.ToolCall{{ + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: "Read the file before replying.", + }, + }} + + if ToolCallExplanationDuplicatesContent( + "I will summarize the findings after reading the file.", + toolCalls, + ) { + t.Fatal("expected distinct content to remain visible") + } + }) + + t.Run("missing explanation", func(t *testing.T) { + toolCalls := []providers.ToolCall{{}} + if ToolCallExplanationDuplicatesContent("Read the file before replying.", toolCalls) { + t.Fatal("expected empty tool explanations to skip dedupe") + } + }) +} diff --git a/pkg/utils/visible_tool_calls.go b/pkg/utils/visible_tool_calls.go index 37a5f60da..8c4d89a51 100644 --- a/pkg/utils/visible_tool_calls.go +++ b/pkg/utils/visible_tool_calls.go @@ -39,9 +39,6 @@ func BuildVisibleToolCalls( explanation := "" if tc.ExtraContent != nil { explanation = strings.TrimSpace(tc.ExtraContent.ToolFeedbackExplanation) - if maxArgsLen > 0 { - explanation = Truncate(explanation, maxArgsLen) - } } if name == "" && explanation == "" && argsPreview == "" { continue diff --git a/pkg/utils/visible_tool_calls_test.go b/pkg/utils/visible_tool_calls_test.go new file mode 100644 index 000000000..fe9467c57 --- /dev/null +++ b/pkg/utils/visible_tool_calls_test.go @@ -0,0 +1,33 @@ +package utils + +import ( + "testing" + + "github.com/sipeed/picoclaw/pkg/providers" +) + +func TestBuildVisibleToolCalls_DoesNotTruncateExplanation(t *testing.T) { + explanation := "Read README.md first to confirm the current project structure before editing the config example." + toolCalls := []providers.ToolCall{{ + ID: "call_1", + Type: "function", + Function: &providers.FunctionCall{ + Name: "read_file", + Arguments: `{"path":"README.md","start_line":1,"end_line":10,"extra":"abcdefghijklmnopqrstuvwxyz"}`, + }, + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: explanation, + }, + }} + + visible := BuildVisibleToolCalls(toolCalls, 20) + if len(visible) != 1 { + t.Fatalf("len(visible) = %d, want 1", len(visible)) + } + if visible[0].ExtraContent == nil || visible[0].ExtraContent.ToolFeedbackExplanation != explanation { + t.Fatalf("visible explanation = %#v, want %q", visible[0].ExtraContent, explanation) + } + if visible[0].Function == nil || visible[0].Function.Arguments == "" { + t.Fatalf("visible function = %#v, want truncated args preview", visible[0].Function) + } +} diff --git a/web/backend/api/session.go b/web/backend/api/session.go index 7d0c11fb0..cc18ee6e1 100644 --- a/web/backend/api/session.go +++ b/web/backend/api/session.go @@ -549,6 +549,9 @@ func sessionTranscriptMessages( } content = "" } + if hasToolCallsMsg && utils.ToolCallExplanationDuplicatesContent(content, msg.ToolCalls) { + content = "" + } chatMsg := sessionChatMessage{ Role: "assistant", diff --git a/web/backend/api/session_test.go b/web/backend/api/session_test.go index 8ef26df5f..760935db7 100644 --- a/web/backend/api/session_test.go +++ b/web/backend/api/session_test.go @@ -901,6 +901,67 @@ func TestHandleListSessions_MessageCountUsesVisibleTranscript(t *testing.T) { } } +func TestHandleListSessions_DeduplicatesAssistantToolCallContentFromVisibleTranscript(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + + dir := sessionsTestDir(t, configPath) + store, err := memory.NewJSONLStore(dir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + sessionKey := picoSessionPrefix + "list-deduped-tool-content" + for _, msg := range []providers.Message{ + {Role: "user", Content: "check file"}, + { + Role: "assistant", + Content: "Read the file before replying.", + ToolCalls: []providers.ToolCall{ + { + ID: "call_1", + Type: "function", + Function: &providers.FunctionCall{ + Name: "read_file", + Arguments: `{"path":"README.md"}`, + }, + ExtraContent: &providers.ExtraContent{ + ToolFeedbackExplanation: "Read the file before replying.", + }, + }, + }, + }, + {Role: "tool", Content: "raw read_file result", ToolCallID: "call_1"}, + } { + if err := store.AddFullMessage(nil, sessionKey, msg); err != nil { + t.Fatalf("AddFullMessage() error = %v", err) + } + } + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/sessions", nil) + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var items []sessionListItem + if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if len(items) != 1 { + t.Fatalf("len(items) = %d, want 1", len(items)) + } + if items[0].MessageCount != 2 { + t.Fatalf("items[0].MessageCount = %d, want 2", items[0].MessageCount) + } +} + func TestHandleGetSession_DoesNotDuplicateAssistantToolCallContent(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() @@ -956,16 +1017,13 @@ func TestHandleGetSession_DoesNotDuplicateAssistantToolCallContent(t *testing.T) if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("Unmarshal() error = %v", err) } - if len(resp.Messages) != 3 { - t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages)) + if len(resp.Messages) != 2 { + t.Fatalf("len(resp.Messages) = %d, want 2", len(resp.Messages)) } if resp.Messages[0].Role != "user" || resp.Messages[0].Content != "check file" { t.Fatalf("first message = %#v, want user/check file", resp.Messages[0]) } - if resp.Messages[1].Content != "Read the file before replying." { - t.Fatalf("assistant content = %#v, want preserved assistant content", resp.Messages[1]) - } - toolCall := assertVisibleToolCallMessage(t, resp.Messages[2], "read_file") + toolCall := assertVisibleToolCallMessage(t, resp.Messages[1], "read_file") if toolCall.ExtraContent == nil || toolCall.ExtraContent.ToolFeedbackExplanation != "Read the file before replying." { t.Fatalf("tool call = %#v, want explanation", toolCall) @@ -1097,8 +1155,8 @@ func TestHandleGetSession_PreservesMediaWhenAssistantToolCallContentDuplicatesSu if resp.Messages[1].Role != "assistant" { t.Fatalf("assistant message role = %q, want assistant", resp.Messages[1].Role) } - if resp.Messages[1].Content != "Reviewing the generated screenshot." { - t.Fatalf("assistant content = %q, want preserved duplicated content with media", resp.Messages[1].Content) + if resp.Messages[1].Content != "" { + t.Fatalf("assistant content = %q, want duplicate content suppressed", resp.Messages[1].Content) } if len(resp.Messages[1].Media) != 1 || resp.Messages[1].Media[0] != "data:image/png;base64,abc123" { t.Fatalf("assistant media = %#v, want preserved media", resp.Messages[1].Media) @@ -1176,8 +1234,8 @@ func TestHandleGetSession_PreservesAttachmentsWhenAssistantToolCallContentDuplic if resp.Messages[1].Role != "assistant" { t.Fatalf("assistant message role = %q, want assistant", resp.Messages[1].Role) } - if resp.Messages[1].Content != "Reviewing the generated report." { - t.Fatalf("assistant content = %q, want preserved duplicated content", resp.Messages[1].Content) + if resp.Messages[1].Content != "" { + t.Fatalf("assistant content = %q, want duplicate content suppressed", resp.Messages[1].Content) } if len(resp.Messages[1].Attachments) != 1 { t.Fatalf("len(assistant.Attachments) = %d, want 1", len(resp.Messages[1].Attachments)) @@ -1256,13 +1314,12 @@ func TestHandleGetSession_UsesConfiguredToolFeedbackMaxArgsLength(t *testing.T) t.Fatalf("len(resp.Messages) = %d, want at least 2", len(resp.Messages)) } - wantPreview := utils.Truncate(explanation, 20) wantArgsPreview := visibleAssistantToolArgsPreview(providers.ToolCall{ Function: &providers.FunctionCall{Arguments: argsJSON}, }, 20) toolCall := assertVisibleToolCallMessage(t, resp.Messages[1], "read_file") - if toolCall.ExtraContent == nil || toolCall.ExtraContent.ToolFeedbackExplanation != wantPreview { - t.Fatalf("tool call = %#v, want preview %q", toolCall, wantPreview) + if toolCall.ExtraContent == nil || toolCall.ExtraContent.ToolFeedbackExplanation != explanation { + t.Fatalf("tool call = %#v, want full explanation %q", toolCall, explanation) } if toolCall.Function == nil || toolCall.Function.Arguments != wantArgsPreview { t.Fatalf("tool call = %#v, want args preview %q", toolCall, wantArgsPreview) diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index 6833fb481..e29a4ccdf 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -612,8 +612,8 @@ "tool_feedback_enabled_hint": "Send a short execution note into the current chat before each tool runs.", "tool_feedback_separate_messages": "Separate Feedback Messages", "tool_feedback_separate_messages_hint": "Keep each tool feedback update as its own chat message instead of reusing a single placeholder/progress message.", - "tool_feedback_max_args_length": "Tool Feedback Length", - "tool_feedback_max_args_length_hint": "Maximum number of characters shown in each tool feedback message. Set to 0 to use the default.", + "tool_feedback_max_args_length": "Tool Args Preview Length", + "tool_feedback_max_args_length_hint": "Maximum number of characters shown in each tool argument preview. Set to 0 to use the default.", "exec_enabled": "Allow Commands", "exec_enabled_hint": "Enable or disable command execution for the app. When disabled, no command requests will run.", "allow_remote": "Allow Remote Commands", diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index 542fff751..d9698c65e 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -612,8 +612,8 @@ "tool_feedback_enabled_hint": "在每次执行工具前,先向当前会话发送一条简短的执行说明", "tool_feedback_separate_messages": "分开发送反馈消息", "tool_feedback_separate_messages_hint": "让每次工具反馈都保留为独立消息,而不是反复复用同一条占位/进度消息", - "tool_feedback_max_args_length": "工具反馈长度", - "tool_feedback_max_args_length_hint": "每条工具反馈消息中展示的字符上限。设为 0 时使用默认值", + "tool_feedback_max_args_length": "工具参数预览长度", + "tool_feedback_max_args_length_hint": "每条工具参数预览中展示的字符上限。设为 0 时使用默认值", "exec_enabled": "允许命令执行", "exec_enabled_hint": "控制应用是否允许执行命令。关闭后,所有命令请求都不会执行", "allow_remote": "允许远程命令执行",