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
|
// Save tool result message to session
|
||||||
agent.Sessions.AddFullMessage(opts.SessionKey, toolResultMsg)
|
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
|
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.
|
// maybeSummarize triggers summarization if the session history exceeds thresholds.
|
||||||
func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, chatID string) {
|
func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, chatID string) {
|
||||||
newHistory := agent.Sessions.GetHistory(sessionKey)
|
newHistory := agent.Sessions.GetHistory(sessionKey)
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,39 @@ func (m *simpleMockProvider) GetDefaultModel() string {
|
||||||
return "mock-model"
|
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
|
// mockCustomTool is a simple mock tool for registration testing
|
||||||
type mockCustomTool struct{}
|
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
|
// failFirstMockProvider fails on the first N calls with a specific error
|
||||||
type failFirstMockProvider struct {
|
type failFirstMockProvider struct {
|
||||||
failures int
|
failures int
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue