mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-03 04:18:07 +00:00
feat(agent): exclude MCP bridge tools from read-only streak detector (#400)
MCP tools (mcp_*) are user-defined external integrations where read-heavy workflows are expected and legitimate. The read-only streak detector was designed for filesystem tool loops but caught MCP tools in the default fallback, triggering false positives on workflows like "query inbox + read 10 emails + summarize." Treat mcp_* tools as neutral (same as exec/bash) since GoClaw cannot determine whether an MCP tool is read or write. Closes #399
This commit is contained in:
@@ -118,14 +118,17 @@ func (s *toolLoopState) detect(toolName string, argsHash string) (level, message
|
||||
}
|
||||
|
||||
// recordMutation updates the read-only streak based on tool type.
|
||||
// Mutating tools reset the streak; exec is neutral (ambiguous); all others increment.
|
||||
// Mutating tools reset the streak; exec/bash/mcp are neutral (ambiguous); all others increment.
|
||||
func (s *toolLoopState) recordMutation(toolName string) {
|
||||
if mutatingTools[toolName] {
|
||||
s.readOnlyStreak = 0
|
||||
return
|
||||
}
|
||||
if toolName == "exec" || toolName == "bash" {
|
||||
return // ambiguous — neither reset nor increment
|
||||
// exec/bash: ambiguous (could be ls or rm).
|
||||
// mcp_*: user-defined external tools — GoClaw cannot determine read vs write.
|
||||
// Neither reset nor increment the read-only streak.
|
||||
if toolName == "exec" || toolName == "bash" || strings.HasPrefix(toolName, "mcp_") {
|
||||
return
|
||||
}
|
||||
s.readOnlyStreak++
|
||||
}
|
||||
|
||||
@@ -171,6 +171,45 @@ func TestReadOnlyStreak_ExecNeutral(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyStreak_MCPNeutral(t *testing.T) {
|
||||
var s toolLoopState
|
||||
// 5 reads → streak = 5
|
||||
for range 5 {
|
||||
s.recordMutation("read_file")
|
||||
}
|
||||
// MCP tools should not reset or increment (same as exec)
|
||||
s.recordMutation("mcp_gmail__query_gmail_emails")
|
||||
if s.readOnlyStreak != 5 {
|
||||
t.Fatalf("expected streak 5 after mcp tool, got %d", s.readOnlyStreak)
|
||||
}
|
||||
s.recordMutation("mcp_gmail__get_gmail_email")
|
||||
if s.readOnlyStreak != 5 {
|
||||
t.Fatalf("expected streak 5 after second mcp tool, got %d", s.readOnlyStreak)
|
||||
}
|
||||
// 7 more reads → streak = 12, should hit critical
|
||||
for range 7 {
|
||||
s.recordMutation("list_files")
|
||||
}
|
||||
if s.readOnlyStreak != 12 {
|
||||
t.Fatalf("expected streak 12, got %d", s.readOnlyStreak)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyStreak_MCPOnlyNeverTriggers(t *testing.T) {
|
||||
var s toolLoopState
|
||||
// 20 consecutive MCP tool calls → streak should stay 0
|
||||
for range 20 {
|
||||
s.recordMutation("mcp_gmail__query_gmail_emails")
|
||||
}
|
||||
if s.readOnlyStreak != 0 {
|
||||
t.Fatalf("expected streak 0 after 20 mcp-only calls, got %d", s.readOnlyStreak)
|
||||
}
|
||||
level, _ := s.detectReadOnlyStreak()
|
||||
if level != "" {
|
||||
t.Fatalf("expected no detection for mcp-only calls, got %q", level)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Same-result cross-args detection ---
|
||||
|
||||
func TestSameResult_Warning(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user