mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix(tool-feedback): dedupe duplicate content and keep full explanations
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
+71
-19
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) != "" {
|
||||
|
||||
Reference in New Issue
Block a user