From 8abf5512a49050f295960b6b0f50ce2af0419c65 Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Wed, 30 Apr 2025 14:51:43 +0100 Subject: [PATCH] fix(core): Add recursive loop detection and move loop detection (#101) * fix(core): Add recursive loop detection and move loop detection Signed-off-by: Richard Palethorpe * fix(ci): Free up space after installation to avoid out of space error Signed-off-by: Richard Palethorpe --------- Signed-off-by: Richard Palethorpe --- .github/workflows/tests.yml | 12 +++++++-- core/agent/actions.go | 9 +++++-- core/agent/agent.go | 51 ++++++++++++++++++++----------------- core/agent/options.go | 1 + 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a56b492..1f47b10 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,16 +30,24 @@ jobs: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update - sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make docker version docker run --rm hello-world - uses: actions/setup-go@v5 with: go-version: '>=1.17.0' + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo apt-get clean + docker system prune -af || true + df -h + - name: Run tests run: | - sudo apt-get update && sudo apt-get install -y make make tests #sudo mv coverage/coverage.txt coverage.txt #sudo chmod 777 coverage.txt diff --git a/core/agent/actions.go b/core/agent/actions.go index 0cae1b3..7b248e7 100644 --- a/core/agent/actions.go +++ b/core/agent/actions.go @@ -480,13 +480,18 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC }, c...) } + reasoningAction := action.NewReasoning() thought, err := a.decision(job, c, - types.Actions{action.NewReasoning()}.ToTools(), - action.NewReasoning().Definition().Name.String(), maxRetries) + types.Actions{reasoningAction}.ToTools(), + reasoningAction.Definition().Name.String(), maxRetries) if err != nil { return nil, nil, "", err } + if thought.actioName != "" && thought.actioName != reasoningAction.Definition().Name.String() { + return nil, nil, "", fmt.Errorf("Expected reasoning action not: %s", thought.actioName) + } + originalReasoning := "" response := &action.ReasoningResponse{} if thought.actionParams != nil { diff --git a/core/agent/agent.go b/core/agent/agent.go index a1f1ac8..9f5baf2 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -492,13 +492,18 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me return conv } -func (a *Agent) consumeJob(job *types.Job, role string) { +func (a *Agent) consumeJob(job *types.Job, role string, retries int) { if err := job.GetContext().Err(); err != nil { job.Result.Finish(fmt.Errorf("expired")) return } + if retries < 1 { + job.Result.Finish(fmt.Errorf("Exceeded recursive retries")) + return + } + a.Lock() paused := a.pause a.Unlock() @@ -561,7 +566,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) { xlog.Error("Error generating parameters, trying again", "error", err) // try again job.SetNextAction(&chosenAction, nil, reasoning) - a.consumeJob(job, role) + a.consumeJob(job, role, retries - 1) return } actionParams = p.actionParams @@ -579,24 +584,6 @@ 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) - a.reply(job, role, conv, actionParams, chosenAction, reasoning) - return - } - } - //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 @@ -650,7 +637,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) { xlog.Error("Error generating parameters, trying again", "error", err) // try again job.SetNextAction(&chosenAction, nil, reasoning) - a.consumeJob(job, role) + a.consumeJob(job, role, retries - 1) return } actionParams = params.actionParams @@ -670,6 +657,22 @@ func (a *Agent) consumeJob(job *types.Job, role string) { return } + if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 { + count := 0 + for _, pastAction := range job.GetPastActions() { + if pastAction.Action.Definition().Name == chosenAction.Definition().Name && + pastAction.Params.String() == actionParams.String() { + count++ + } + } + if count > a.options.loopDetectionSteps { + xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name) + a.reply(job, role, conv, actionParams, chosenAction, reasoning) + return + } + xlog.Debug("Checked for loops", "action", chosenAction.Definition().Name, "count", count) + } + job.AddPastAction(chosenAction, &actionParams) if !job.Callback(types.ActionCurrentState{ @@ -783,7 +786,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) { // The agent decided to do another action // call ourselves again job.SetNextAction(&followingAction, &followingParams, reasoning) - a.consumeJob(job, role) + a.consumeJob(job, role, retries) return } @@ -988,7 +991,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) { types.WithReasoningCallback(a.options.reasoningCallback), types.WithResultCallback(a.options.resultCallback), ) - a.consumeJob(whatNext, SystemRole) + a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps) xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name) @@ -1072,7 +1075,7 @@ func (a *Agent) run(timer *time.Timer) error { <-timer.C } xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job) - a.consumeJob(job, UserRole) + a.consumeJob(job, UserRole, a.options.loopDetectionSteps) timer.Reset(a.options.periodicRuns) case <-a.context.Done(): // Agent has been canceled, return error diff --git a/core/agent/options.go b/core/agent/options.go index d754458..55319a3 100644 --- a/core/agent/options.go +++ b/core/agent/options.go @@ -69,6 +69,7 @@ func defaultOptions() *options { return &options{ parallelJobs: 1, periodicRuns: 15 * time.Minute, + loopDetectionSteps: 10, LLMAPI: llmOptions{ APIURL: "http://localhost:8080", Model: "gpt-4",