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 <io@richiejp.com>

* fix(ci): Free up space after installation to avoid out of space error

Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
This commit is contained in:
Richard Palethorpe
2025-04-30 14:51:43 +01:00
committed by GitHub
parent 45dd74d27c
commit 8abf5512a4
4 changed files with 45 additions and 28 deletions

View File

@@ -30,16 +30,24 @@ jobs:
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update 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 version
docker run --rm hello-world docker run --rm hello-world
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '>=1.17.0' 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 - name: Run tests
run: | run: |
sudo apt-get update && sudo apt-get install -y make
make tests make tests
#sudo mv coverage/coverage.txt coverage.txt #sudo mv coverage/coverage.txt coverage.txt
#sudo chmod 777 coverage.txt #sudo chmod 777 coverage.txt

View File

@@ -480,13 +480,18 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
}, c...) }, c...)
} }
reasoningAction := action.NewReasoning()
thought, err := a.decision(job, thought, err := a.decision(job,
c, c,
types.Actions{action.NewReasoning()}.ToTools(), types.Actions{reasoningAction}.ToTools(),
action.NewReasoning().Definition().Name.String(), maxRetries) reasoningAction.Definition().Name.String(), maxRetries)
if err != nil { if err != nil {
return nil, nil, "", err 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 := "" originalReasoning := ""
response := &action.ReasoningResponse{} response := &action.ReasoningResponse{}
if thought.actionParams != nil { if thought.actionParams != nil {

View File

@@ -492,13 +492,18 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me
return conv 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 { if err := job.GetContext().Err(); err != nil {
job.Result.Finish(fmt.Errorf("expired")) job.Result.Finish(fmt.Errorf("expired"))
return return
} }
if retries < 1 {
job.Result.Finish(fmt.Errorf("Exceeded recursive retries"))
return
}
a.Lock() a.Lock()
paused := a.pause paused := a.pause
a.Unlock() a.Unlock()
@@ -561,7 +566,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
xlog.Error("Error generating parameters, trying again", "error", err) xlog.Error("Error generating parameters, trying again", "error", err)
// try again // try again
job.SetNextAction(&chosenAction, nil, reasoning) job.SetNextAction(&chosenAction, nil, reasoning)
a.consumeJob(job, role) a.consumeJob(job, role, retries - 1)
return return
} }
actionParams = p.actionParams 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) //xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning)
if chosenAction == nil { if chosenAction == nil {
// If no action was picked up, the reasoning is the message returned by the assistant // 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) xlog.Error("Error generating parameters, trying again", "error", err)
// try again // try again
job.SetNextAction(&chosenAction, nil, reasoning) job.SetNextAction(&chosenAction, nil, reasoning)
a.consumeJob(job, role) a.consumeJob(job, role, retries - 1)
return return
} }
actionParams = params.actionParams actionParams = params.actionParams
@@ -670,6 +657,22 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
return 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) job.AddPastAction(chosenAction, &actionParams)
if !job.Callback(types.ActionCurrentState{ 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 // The agent decided to do another action
// call ourselves again // call ourselves again
job.SetNextAction(&followingAction, &followingParams, reasoning) job.SetNextAction(&followingAction, &followingParams, reasoning)
a.consumeJob(job, role) a.consumeJob(job, role, retries)
return return
} }
@@ -988,7 +991,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
types.WithReasoningCallback(a.options.reasoningCallback), types.WithReasoningCallback(a.options.reasoningCallback),
types.WithResultCallback(a.options.resultCallback), 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) 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 <-timer.C
} }
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job) 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) timer.Reset(a.options.periodicRuns)
case <-a.context.Done(): case <-a.context.Done():
// Agent has been canceled, return error // Agent has been canceled, return error

View File

@@ -69,6 +69,7 @@ func defaultOptions() *options {
return &options{ return &options{
parallelJobs: 1, parallelJobs: 1,
periodicRuns: 15 * time.Minute, periodicRuns: 15 * time.Minute,
loopDetectionSteps: 10,
LLMAPI: llmOptions{ LLMAPI: llmOptions{
APIURL: "http://localhost:8080", APIURL: "http://localhost:8080",
Model: "gpt-4", Model: "gpt-4",