diff --git a/core/agent/agent.go b/core/agent/agent.go index 3840511..818aa03 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -489,6 +489,23 @@ func (a *Agent) consumeJob(job *types.Job, role string) { } } + // check if the agent is looping over the same action + // if so, we need to stop it + if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 { + count := map[string]int{} + for i := len(job.GetPastActions()) - 1; i >= 0; i-- { + pastAction := job.GetPastActions()[i] + if pastAction.Action.Definition().Name == chosenAction.Definition().Name && + pastAction.Params.String() == actionParams.String() { + count[chosenAction.Definition().Name.String()]++ + } + } + if count[chosenAction.Definition().Name.String()] > a.options.loopDetectionSteps { + xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name) + chosenAction = nil + } + } + //xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning) if chosenAction == nil { // If no action was picked up, the reasoning is the message returned by the assistant @@ -551,6 +568,8 @@ func (a *Agent) consumeJob(job *types.Job, role string) { return } + job.AddPastAction(chosenAction, &actionParams) + var err error conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv) if err != nil { diff --git a/core/agent/agent_test.go b/core/agent/agent_test.go index 3e22c1a..fca844e 100644 --- a/core/agent/agent_test.go +++ b/core/agent/agent_test.go @@ -126,6 +126,7 @@ var _ = Describe("Agent test", func() { agent, err := New( WithLLMAPIURL(apiURL), WithModel(testModel), + WithLoopDetectionSteps(3), // WithRandomIdentity(), WithActions(&TestAction{response: map[string]string{ "boston": testActionResult, diff --git a/core/agent/options.go b/core/agent/options.go index 1a4ba4c..831016a 100644 --- a/core/agent/options.go +++ b/core/agent/options.go @@ -28,6 +28,7 @@ type options struct { canStopItself bool initiateConversations bool + loopDetectionSteps int forceReasoning bool canPlan bool characterfile string @@ -113,6 +114,13 @@ func WithTimeout(timeout string) Option { } } +func WithLoopDetectionSteps(steps int) Option { + return func(o *options) error { + o.loopDetectionSteps = steps + return nil + } +} + func WithConversationsPath(path string) Option { return func(o *options) error { o.conversationsPath = path diff --git a/core/state/config.go b/core/state/config.go index 8d46fe2..2d7a110 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -56,6 +56,7 @@ type AgentConfig struct { EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"` EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"` KnowledgeBaseResults int `json:"kb_results" form:"kb_results"` + LoopDetectionSteps int `json:"loop_detection_steps" form:"loop_detection_steps"` CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"` SystemPrompt string `json:"system_prompt" form:"system_prompt"` LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"` @@ -250,6 +251,15 @@ func NewAgentConfigMeta( HelpText: "Enable agent to explain its reasoning process", Tags: config.Tags{Section: "AdvancedSettings"}, }, + { + Name: "loop_detection_steps", + Label: "Max Loop Detection Steps", + Type: "number", + DefaultValue: 5, + Min: 1, + Step: 1, + Tags: config.Tags{Section: "AdvancedSettings"}, + }, }, MCPServers: []config.Field{ { diff --git a/core/state/pool.go b/core/state/pool.go index 2cb7f29..85217cf 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -461,6 +461,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults)) } + if config.LoopDetectionSteps > 0 { + opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps)) + } + xlog.Info("Starting agent", "name", name, "config", config) agent, err := New(opts...) diff --git a/core/types/job.go b/core/types/job.go index 78d6ca8..9b157fb 100644 --- a/core/types/job.go +++ b/core/types/job.go @@ -20,6 +20,7 @@ type Job struct { UUID string Metadata map[string]interface{} + pastActions []*ActionRequest nextAction *Action nextActionParams *ActionParams nextActionReasoning string @@ -28,6 +29,11 @@ type Job struct { cancel context.CancelFunc } +type ActionRequest struct { + Action Action + Params *ActionParams +} + type JobOption func(*Job) func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption { @@ -82,6 +88,17 @@ func (j *Job) SetNextAction(action *Action, params *ActionParams, reasoning stri j.nextActionReasoning = reasoning } +func (j *Job) AddPastAction(action Action, params *ActionParams) { + j.pastActions = append(j.pastActions, &ActionRequest{ + Action: action, + Params: params, + }) +} + +func (j *Job) GetPastActions() []*ActionRequest { + return j.pastActions +} + func (j *Job) GetNextAction() (*Action, *ActionParams, string) { return j.nextAction, j.nextActionParams, j.nextActionReasoning }