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

@@ -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 {

View File

@@ -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

View File

@@ -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",