mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 20:38:58 +08:00
fix loop
This commit is contained in:
@@ -1764,6 +1764,38 @@ func containsAnySubstring(text string, values ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func hasToolMessages(messages []providers.Message) bool {
|
||||
for _, m := range messages {
|
||||
if strings.EqualFold(strings.TrimSpace(m.Role), "tool") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldRetryAfterDeferralNoTools(content string, iteration int, alreadyRetried bool, hasToolOutput bool, systemMode bool) bool {
|
||||
if systemMode || alreadyRetried || hasToolOutput {
|
||||
return false
|
||||
}
|
||||
// Only apply on first planning turn to preserve direct-answer behavior for normal QA.
|
||||
if iteration > 1 {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(strings.TrimSpace(content))
|
||||
if lower == "" {
|
||||
return false
|
||||
}
|
||||
waitCue := containsAnySubstring(lower,
|
||||
"请稍等", "稍等", "等一下", "我先", "先查看", "需要先查看", "先检查",
|
||||
"please wait", "wait a moment", "let me check", "i need to check", "i'll check", "checking",
|
||||
)
|
||||
verifyCue := containsAnySubstring(lower,
|
||||
"查看", "检查", "确认", "工作区", "状态",
|
||||
"check", "inspect", "verify", "workspace", "status", "confirm",
|
||||
)
|
||||
return waitCue && verifyCue
|
||||
}
|
||||
|
||||
func formatRunStateReport(rs runState) string {
|
||||
lines := []string{
|
||||
fmt.Sprintf("Run ID: %s", rs.runID),
|
||||
@@ -2655,6 +2687,20 @@ func (al *AgentLoop) runLLMToolLoop(
|
||||
})
|
||||
|
||||
if len(response.ToolCalls) == 0 {
|
||||
if shouldRetryAfterDeferralNoTools(response.Content, state.iteration, state.deferralRetried, hasToolMessages(messages), systemMode) {
|
||||
state.deferralRetried = true
|
||||
messages = append(messages, providers.Message{
|
||||
Role: "user",
|
||||
Content: "Do not ask the user to wait. If verification is needed, call the required tools now and then provide the result in the same run.",
|
||||
})
|
||||
if !systemMode {
|
||||
logger.WarnCF("agent", "Detected deferral-style direct reply without tool calls; forcing another tool-planning round", map[string]interface{}{
|
||||
"iteration": iteration,
|
||||
"session_key": sessionKey,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
state.finalContent = response.Content
|
||||
state.consecutiveAllToolErrorRounds = 0
|
||||
state.repeatedToolCallRounds = 0
|
||||
@@ -2840,6 +2886,7 @@ type toolLoopState struct {
|
||||
lastReflectDecision string
|
||||
lastReflectConfidence float64
|
||||
lastReflectIteration int
|
||||
deferralRetried bool
|
||||
}
|
||||
|
||||
type toolActOutcome struct {
|
||||
|
||||
@@ -174,6 +174,32 @@ func (t replayTool) ResourceKeys(args map[string]interface{}) []string {
|
||||
return t.resourceKeys(args)
|
||||
}
|
||||
|
||||
type deferralRetryProvider struct {
|
||||
planCalls int
|
||||
}
|
||||
|
||||
func (p *deferralRetryProvider) Chat(ctx context.Context, messages []providers.Message, defs []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
if len(defs) == 0 {
|
||||
return &providers.LLMResponse{Content: "finalized"}, nil
|
||||
}
|
||||
p.planCalls++
|
||||
switch p.planCalls {
|
||||
case 1:
|
||||
return &providers.LLMResponse{Content: "需要先查看一下当前工作区才能确认,请稍等。"}, nil
|
||||
case 2:
|
||||
return &providers.LLMResponse{
|
||||
Content: "先检查状态",
|
||||
ToolCalls: []providers.ToolCall{
|
||||
{ID: "tc-status-1", Name: "read_file", Arguments: map[string]interface{}{"path": "README.md"}},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return &providers.LLMResponse{Content: "已完成状态检查,当前一切正常。"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *deferralRetryProvider) GetDefaultModel() string { return "test-model" }
|
||||
|
||||
func TestActToolCalls_BudgetTruncationReplay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -600,6 +626,70 @@ func TestShouldForceSelfRepairHeuristic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRetryAfterDeferralNoTools(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !shouldRetryAfterDeferralNoTools("需要先查看一下当前工作区才能确认,请稍等。", 1, false, false, false) {
|
||||
t.Fatalf("expected deferral text to trigger retry")
|
||||
}
|
||||
if shouldRetryAfterDeferralNoTools("这里是直接答案。", 1, false, false, false) {
|
||||
t.Fatalf("did not expect normal direct answer to trigger retry")
|
||||
}
|
||||
if shouldRetryAfterDeferralNoTools("需要先查看一下当前工作区才能确认,请稍等。", 2, false, false, false) {
|
||||
t.Fatalf("did not expect retry after first iteration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLLMToolLoop_RecoversFromDeferralWithoutTools(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var toolExecCount int32
|
||||
reg := tools.NewToolRegistry()
|
||||
reg.Register(replayToolImpl{
|
||||
name: "read_file",
|
||||
run: func(ctx context.Context, args map[string]interface{}) (string, error) {
|
||||
atomic.AddInt32(&toolExecCount, 1)
|
||||
return "README content", nil
|
||||
},
|
||||
})
|
||||
|
||||
provider := &deferralRetryProvider{}
|
||||
al := &AgentLoop{
|
||||
provider: provider,
|
||||
providersByProxy: map[string]providers.LLMProvider{"proxy": provider},
|
||||
modelsByProxy: map[string][]string{"proxy": []string{"test-model"}},
|
||||
proxy: "proxy",
|
||||
model: "test-model",
|
||||
maxIterations: 5,
|
||||
llmCallTimeout: 3 * time.Second,
|
||||
tools: reg,
|
||||
sessions: session.NewSessionManager(""),
|
||||
workspace: t.TempDir(),
|
||||
}
|
||||
|
||||
msgs := []providers.Message{
|
||||
{Role: "system", Content: "test system"},
|
||||
{Role: "user", Content: "当前状态"},
|
||||
}
|
||||
|
||||
out, iterations, err := al.runLLMToolLoop(context.Background(), msgs, "deferral:test", false, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("runLLMToolLoop error: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(out) == "" {
|
||||
t.Fatalf("expected non-empty output")
|
||||
}
|
||||
if provider.planCalls < 3 {
|
||||
t.Fatalf("expected additional planning round after deferral, got planCalls=%d", provider.planCalls)
|
||||
}
|
||||
if atomic.LoadInt32(&toolExecCount) == 0 {
|
||||
t.Fatalf("expected tool execution after deferral recovery")
|
||||
}
|
||||
if iterations < 3 {
|
||||
t.Fatalf("expected at least 3 iterations, got %d", iterations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelfRepairMemoryPromptDedup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user