@@ -1006,6 +1006,43 @@ class AgentLoop(
10061006 rawContent = response.thinkingContent
10071007 }
10081008
1009+ // Detect token-loop: finish_reason=length + no tool calls → model hit max_tokens in repetitive loop
1010+ // Also detect repetitive content (e.g. same sentence repeated 100+ times)
1011+ val isTokenLoop = response.finishReason == " length" && response.toolCalls.isNullOrEmpty()
1012+ val isRepetitiveContent = rawContent != null && rawContent.length > 500 && isHighlyRepetitive(rawContent)
1013+ if ((isTokenLoop || isRepetitiveContent) && emptyResponseRetryAttempts < MAX_EMPTY_RESPONSE_RETRY_ATTEMPTS ) {
1014+ emptyResponseRetryAttempts++
1015+ val reason = if (isTokenLoop) " finish_reason=length, no tools" else " repetitive content (${rawContent!! .length} chars)"
1016+ writeLog(" ⚠️ LLM 陷入 token 循环: $reason (retry $emptyResponseRetryAttempts /$MAX_EMPTY_RESPONSE_RETRY_ATTEMPTS )" )
1017+ Log .w(TAG , " ⚠️ Token loop detected: $reason , retry $emptyResponseRetryAttempts " )
1018+
1019+ if (contextManager != null ) {
1020+ writeLog(" 🔄 压缩上下文后重试..." )
1021+ val recoveryResult = contextManager.handleContextOverflow(
1022+ error = Exception (" Token loop: $reason " ),
1023+ messages = messages
1024+ )
1025+ when (recoveryResult) {
1026+ is ContextRecoveryResult .Recovered -> {
1027+ messages.clear()
1028+ messages.addAll(recoveryResult.messages)
1029+ _progressFlow .emit(ProgressUpdate .ContextRecovered (
1030+ strategy = recoveryResult.strategy,
1031+ attempt = recoveryResult.attempt
1032+ ))
1033+ }
1034+ is ContextRecoveryResult .CannotRecover -> {
1035+ val ctxTokens = resolveContextWindowTokens()
1036+ pruneOldToolResults(messages, ctxTokens)
1037+ ToolResultContextGuard .enforceContextBudget(messages, ctxTokens)
1038+ }
1039+ }
1040+ } else {
1041+ writeLog(" 🔄 无 contextManager,直接重试..." )
1042+ }
1043+ continue
1044+ }
1045+
10091046 // Detect suspicious default text from LLM (e.g. "无响应")
10101047 // Instead of silently accepting, try to compact context and retry
10111048 val isSuspiciousResponse = rawContent == " 无响应" || rawContent == " 无响应。" || rawContent == " 没有响应"
@@ -1515,6 +1552,30 @@ class AgentLoop(
15151552 yieldSignal = null
15161553 Log .d(TAG , " AgentLoop reset for steer-restart" )
15171554 }
1555+
1556+ /* *
1557+ * Detect highly repetitive content (model stuck in a loop).
1558+ * Takes the first 50 chars as a "chunk" and checks how many times it repeats.
1559+ * Returns true if >50% of the content is the same chunk repeated.
1560+ */
1561+ private fun isHighlyRepetitive (content : String ): Boolean {
1562+ if (content.length < 500 ) return false
1563+ val chunkSize = 50
1564+ val chunk = content.take(chunkSize)
1565+ if (chunk.isBlank()) return false
1566+ var repeatCount = 0
1567+ var pos = 0
1568+ while (pos + chunkSize <= content.length) {
1569+ if (content.regionMatches(pos, chunk, 0 , chunkSize)) {
1570+ repeatCount++
1571+ } else {
1572+ break // Stop at first mismatch — repetitive content is typically at the start
1573+ }
1574+ pos + = chunkSize
1575+ }
1576+ val repeatedChars = repeatCount * chunkSize
1577+ return repeatedChars > content.length * 0.5
1578+ }
15181579}
15191580
15201581/* *
0 commit comments