From 4a86e5e3bb50cf63b1ec2723ca5685ba4f2833b5 Mon Sep 17 00:00:00 2001 From: mason5052 Date: Thu, 5 Mar 2026 19:37:06 -0500 Subject: [PATCH 1/2] fix: add iteration cap and repeating escalation to performAgentChain The performAgentChain loop has no iteration cap, allowing infinite loops when a model repeatedly calls the same tool. The repeating detector returns a message (not an error), so the loop never breaks. Add two safety mechanisms: - Hard cap of 100 iterations on the main agent chain loop - Escalation to error after 5 consecutive repeating detections (8 total identical calls: 3 before first detection + 5 detections) The soft "please try another tool" response is preserved for the first 4 detections, giving the LLM a chance to course-correct before aborting. Closes #175 Signed-off-by: mason5052 --- backend/pkg/providers/performer.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/backend/pkg/providers/performer.go b/backend/pkg/providers/performer.go index 446a0cad..dc4be696 100644 --- a/backend/pkg/providers/performer.go +++ b/backend/pkg/providers/performer.go @@ -26,11 +26,13 @@ import ( ) const ( - maxRetriesToCallSimpleChain = 3 - maxRetriesToCallAgentChain = 3 - maxRetriesToCallFunction = 3 - maxReflectorCallsPerChain = 3 - delayBetweenRetries = 5 * time.Second + maxRetriesToCallSimpleChain = 3 + maxRetriesToCallAgentChain = 3 + maxRetriesToCallFunction = 3 + maxReflectorCallsPerChain = 3 + maxAgentChainIterations = 100 + maxRepeatingDetectionsBeforeErr = 5 + delayBetweenRetries = 5 * time.Second ) type callResult struct { @@ -91,7 +93,13 @@ func (fp *flowProvider) performAgentChain( groupID := fmt.Sprintf("flow-%d", fp.flowID) toolTypeMapping := tools.GetToolTypeMapping() - for { + for iteration := 0; ; iteration++ { + if iteration >= maxAgentChainIterations { + msg := fmt.Sprintf("agent chain exceeded maximum iterations (%d)", maxAgentChainIterations) + logger.WithField("iteration", iteration).Error(msg) + return fmt.Errorf("%s", msg) + } + result, err := fp.callWithRetries(ctx, chain, optAgentType, executor) if err != nil { logger.WithError(err).Error("failed to call agent chain") @@ -256,6 +264,12 @@ func (fp *flowProvider) execToolCall( }) if detector.detect(toolCall) { + if len(detector.funcCalls) >= RepeatingToolCallThreshold+maxRepeatingDetectionsBeforeErr-1 { + errMsg := fmt.Sprintf("tool '%s' repeated %d times consecutively, aborting chain", funcName, len(detector.funcCalls)) + logger.WithField("repeat_count", len(detector.funcCalls)).Error(errMsg) + return "", fmt.Errorf("%s", errMsg) + } + response := fmt.Sprintf("tool call '%s' is repeating, please try another tool", funcName) _, observation := obs.Observer.NewObservation(ctx) From 917b3716b5d262036753bfa32869685d14568384 Mon Sep 17 00:00:00 2001 From: mason5052 Date: Thu, 5 Mar 2026 20:40:11 -0500 Subject: [PATCH 2/2] fix: address review feedback on iteration cap and repeating escalation - Rename maxRepeatingDetectionsBeforeErr to maxSoftDetectionsBeforeAbort for clarity (name now matches behavior: 4 soft warnings before abort) - Adjust threshold value from 5 to 4 and remove -1 from condition (same runtime behavior: abort on 7th consecutive identical call) - Use errors.New() instead of fmt.Errorf("%s", msg) for non-formatted error strings (more idiomatic Go) Signed-off-by: mason5052 --- backend/pkg/providers/performer.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/pkg/providers/performer.go b/backend/pkg/providers/performer.go index dc4be696..ab353458 100644 --- a/backend/pkg/providers/performer.go +++ b/backend/pkg/providers/performer.go @@ -31,7 +31,7 @@ const ( maxRetriesToCallFunction = 3 maxReflectorCallsPerChain = 3 maxAgentChainIterations = 100 - maxRepeatingDetectionsBeforeErr = 5 + maxSoftDetectionsBeforeAbort = 4 delayBetweenRetries = 5 * time.Second ) @@ -97,7 +97,7 @@ func (fp *flowProvider) performAgentChain( if iteration >= maxAgentChainIterations { msg := fmt.Sprintf("agent chain exceeded maximum iterations (%d)", maxAgentChainIterations) logger.WithField("iteration", iteration).Error(msg) - return fmt.Errorf("%s", msg) + return errors.New(msg) } result, err := fp.callWithRetries(ctx, chain, optAgentType, executor) @@ -264,10 +264,10 @@ func (fp *flowProvider) execToolCall( }) if detector.detect(toolCall) { - if len(detector.funcCalls) >= RepeatingToolCallThreshold+maxRepeatingDetectionsBeforeErr-1 { + if len(detector.funcCalls) >= RepeatingToolCallThreshold+maxSoftDetectionsBeforeAbort { errMsg := fmt.Sprintf("tool '%s' repeated %d times consecutively, aborting chain", funcName, len(detector.funcCalls)) logger.WithField("repeat_count", len(detector.funcCalls)).Error(errMsg) - return "", fmt.Errorf("%s", errMsg) + return "", errors.New(errMsg) } response := fmt.Sprintf("tool call '%s' is repeating, please try another tool", funcName)