fix(agent): stop message-only tool call loops
This commit is contained in:
parent
aef1e8e8c4
commit
7ff33ad7e6
2 changed files with 105 additions and 0 deletions
|
|
@ -1133,11 +1133,28 @@ func (al *AgentLoop) runLLMIteration(
|
|||
// Save tool result message to session
|
||||
agent.Sessions.AddFullMessage(opts.SessionKey, toolResultMsg)
|
||||
}
|
||||
|
||||
// If this round only executed message tool calls, stop the loop here.
|
||||
// This prevents "ack-on-ack" loops where the model keeps calling message.
|
||||
if areAllToolCallsMessage(normalizedToolCalls) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return finalContent, iteration, nil
|
||||
}
|
||||
|
||||
func areAllToolCallsMessage(calls []providers.ToolCall) bool {
|
||||
if len(calls) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, tc := range calls {
|
||||
if tc.Name != "message" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
// maybeSummarize triggers summarization if the session history exceeds thresholds.
|
||||
func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, chatID string) {
|
||||
newHistory := agent.Sessions.GetHistory(sessionKey)
|
||||
|
|
|
|||
|
|
@ -323,6 +323,39 @@ func (m *simpleMockProvider) GetDefaultModel() string {
|
|||
return "mock-model"
|
||||
}
|
||||
|
||||
type messageOnlyLoopProvider struct {
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (m *messageOnlyLoopProvider) Chat(
|
||||
ctx context.Context,
|
||||
messages []providers.Message,
|
||||
tools []providers.ToolDefinition,
|
||||
model string,
|
||||
opts map[string]any,
|
||||
) (*providers.LLMResponse, error) {
|
||||
m.callCount++
|
||||
return &providers.LLMResponse{
|
||||
ToolCalls: []providers.ToolCall{
|
||||
{
|
||||
ID: fmt.Sprintf("tc-%d", m.callCount),
|
||||
Name: "message",
|
||||
Arguments: map[string]any{
|
||||
"content": "Context compressed",
|
||||
},
|
||||
Function: &providers.FunctionCall{
|
||||
Name: "message",
|
||||
Arguments: `{"content":"Context compressed"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *messageOnlyLoopProvider) GetDefaultModel() string {
|
||||
return "mock-message-only-model"
|
||||
}
|
||||
|
||||
// mockCustomTool is a simple mock tool for registration testing
|
||||
type mockCustomTool struct{}
|
||||
|
||||
|
|
@ -448,6 +481,61 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRunLLMIteration_MessageOnlyToolCallsStopAfterOneRound(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &messageOnlyLoopProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
al.beginRoundForMessageTool(agent)
|
||||
al.updateToolContexts(agent, "telegram", "chat-1")
|
||||
|
||||
_, iteration, err := al.runLLMIteration(
|
||||
context.Background(),
|
||||
agent,
|
||||
[]providers.Message{
|
||||
{Role: "system", Content: "test system prompt"},
|
||||
{Role: "user", Content: "压缩上下文"},
|
||||
},
|
||||
processOptions{
|
||||
SessionKey: "test-session",
|
||||
Channel: "telegram",
|
||||
ChatID: "chat-1",
|
||||
SendResponse: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("runLLMIteration failed: %v", err)
|
||||
}
|
||||
|
||||
if iteration != 1 {
|
||||
t.Fatalf("expected runLLMIteration to stop after 1 iteration, got %d", iteration)
|
||||
}
|
||||
if provider.callCount != 1 {
|
||||
t.Fatalf("expected provider to be called once, got %d", provider.callCount)
|
||||
}
|
||||
}
|
||||
|
||||
// failFirstMockProvider fails on the first N calls with a specific error
|
||||
type failFirstMockProvider struct {
|
||||
failures int
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue