Skip to content

Commit 8a97c28

Browse files
committed
fix(loop): re-enable loop intervention system messages
**Problem:** Claude and other models were looping endlessly when stuck (calling same tool 25+ times). Loop detection was working but intervention message injection was DISABLED due to previous GitHub Copilot API 400 errors. **Root Cause:** Comment in code: "system message injection removed (was causing GitHub Copilot 400 errors)" The 400 errors were caused by bugs that were FIXED in previous commits: - Delta mode fixes (d1c3ea7) - User role requirement (5fff807) - Trailing whitespace (822c12c) **Solution:** Re-enabled system-reminder intervention message injection in both: 1. Basic loop detection path (AgentOrchestrator.swift line ~2318) 2. Streaming loop detection path (AgentOrchestrator.swift line ~3668) When loop detected (3+ consecutive calls to same tool): - Creates intervention message explaining the loop - Wraps in <system-reminder> tags (Claude-compatible format) - Appends to internalMessages for next API request - Model receives warning and changes behavior **Testing:** ✅ Build: PASS ✅ Claude: Stops looping after intervention (was 25+ calls, now stops at 3) ✅ GPT-4.1: Works correctly, no regressions ✅ No API errors (400s no longer occur due to previous fixes) ✅ Loop intervention message appears in API request ✅ Model adapts behavior after receiving intervention
1 parent 5fff807 commit 8a97c28

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

Sources/APIFramework/AgentOrchestrator.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,8 +2320,22 @@ public class AgentOrchestrator: ObservableObject, IterationController {
23202320
if count >= limit {
23212321
logger.warning("LOOP_DETECTION: Tool '\(toolName)' called \(count) times consecutively! Loop detected.")
23222322

2323+
/// Create intervention message to break the loop
2324+
let interventionMessage = """
2325+
LOOP DETECTED: You've called the '\(toolName)' tool \(count) times in a row without progress.
2326+
2327+
This indicates you may be stuck. Please:
2328+
1. Review the results you've already received
2329+
2. Try a different approach or tool
2330+
3. If the task cannot be completed, explain why to the user
2331+
2332+
Do NOT call '\(toolName)' again immediately - choose a different action.
2333+
"""
2334+
2335+
context.internalMessages.append(createSystemReminder(content: interventionMessage, model: model))
2336+
23232337
context.consecutiveToolCounts[toolName] = 0
2324-
logger.debug("LOOP_DETECTION: Detected '\(toolName)' loop (system message injection removed)")
2338+
logger.debug("LOOP_DETECTION: Injected intervention message for '\(toolName)' loop")
23252339
}
23262340
}
23272341

@@ -3658,8 +3672,22 @@ public class AgentOrchestrator: ObservableObject, IterationController {
36583672
if count >= limit {
36593673
logger.warning("LOOP_DETECTION_STREAMING: Tool '\(toolName)' called \(count) times consecutively! Loop detected.")
36603674

3675+
/// Create intervention message to break the loop
3676+
let interventionMessage = """
3677+
LOOP DETECTED: You've called the '\(toolName)' tool \(count) times in a row without progress.
3678+
3679+
This indicates you may be stuck. Please:
3680+
1. Review the results you've already received
3681+
2. Try a different approach or tool
3682+
3. If the task cannot be completed, explain why to the user
3683+
3684+
Do NOT call '\(toolName)' again immediately - choose a different action.
3685+
"""
3686+
3687+
context.internalMessages.append(createSystemReminder(content: interventionMessage, model: model))
3688+
36613689
context.consecutiveToolCounts[toolName] = 0
3662-
logger.debug("LOOP_DETECTION_STREAMING: Injected '\(toolName)' loop intervention message")
3690+
logger.debug("LOOP_DETECTION_STREAMING: Injected intervention message for '\(toolName)' loop")
36633691
}
36643692
}
36653693

@@ -5148,6 +5176,13 @@ public class AgentOrchestrator: ObservableObject, IterationController {
51485176
for (index, historyMessage) in conversationMessages.enumerated() {
51495177
var cleanContent = historyMessage.content
51505178

5179+
/// DIAGNOSTIC: Track isToolMessage flag at conversion
5180+
let hasPreview = cleanContent.contains("[TOOL_RESULT_PREVIEW]") || cleanContent.contains("[TOOL_RESULT_STORED]")
5181+
if hasPreview {
5182+
let contentPrefix = cleanContent.prefix(60).replacingOccurrences(of: "\n", with: " ")
5183+
logger.debug("CONVERT_TOOL_MSG: isToolMessage=\(historyMessage.isToolMessage), type=\(historyMessage.type), contentPrefix=[\(contentPrefix)]")
5184+
}
5185+
51515186
// Handle tool messages with proper role and toolCallId
51525187
if historyMessage.isToolMessage {
51535188
// Tool messages need role="tool" and toolCallId for proper API formatting

Sources/ConversationEngine/ConversationMessageBus.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,10 @@ public class ConversationMessageBus: ObservableObject {
567567
isToolMessage: true
568568
)
569569

570+
/// DIAGNOSTIC: Track isToolMessage flag at creation
571+
let contentPrefix = message.content.prefix(60).replacingOccurrences(of: "\n", with: " ")
572+
logger.debug("TOOL_MESSAGE_CREATED: id=\(message.id), isToolMessage=\(message.isToolMessage), contentPrefix=[\(contentPrefix)]")
573+
570574
appendMessage(message)
571575
scheduleSave()
572576

0 commit comments

Comments
 (0)