loom-agent 0.0.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/api/__init__.py +19 -0
- loom/api/v0_0_3.py +1 -0
- loom/builtin/retriever/faiss_store.py +403 -0
- loom/core/agent_executor.py +212 -26
- loom/core/events.py +3 -0
- loom/core/recursion_control.py +298 -0
- loom/core/turn_state.py +58 -6
- loom/retrieval/__init__.py +61 -0
- loom/retrieval/domain_adapter.py +195 -0
- loom/retrieval/embedding_retriever.py +393 -0
- loom_agent-0.0.5.dist-info/METADATA +561 -0
- {loom_agent-0.0.4.dist-info → loom_agent-0.0.5.dist-info}/RECORD +14 -8
- loom_agent-0.0.4.dist-info/METADATA +0 -292
- {loom_agent-0.0.4.dist-info → loom_agent-0.0.5.dist-info}/WHEEL +0 -0
- {loom_agent-0.0.4.dist-info → loom_agent-0.0.5.dist-info}/licenses/LICENSE +0 -0
loom/core/agent_executor.py
CHANGED
|
@@ -20,6 +20,7 @@ from loom.core.context_assembly import ComponentPriority, ContextAssembler
|
|
|
20
20
|
from loom.core.events import AgentEvent, AgentEventType, ToolResult
|
|
21
21
|
from loom.core.execution_context import ExecutionContext
|
|
22
22
|
from loom.core.permissions import PermissionManager
|
|
23
|
+
from loom.core.recursion_control import RecursionMonitor, RecursionState
|
|
23
24
|
from loom.core.steering_control import SteeringControl
|
|
24
25
|
from loom.core.tool_orchestrator import ToolOrchestrator
|
|
25
26
|
from loom.core.tool_pipeline import ToolExecutionPipeline
|
|
@@ -127,6 +128,9 @@ class AgentExecutor:
|
|
|
127
128
|
task_handlers: Optional[List[TaskHandler]] = None,
|
|
128
129
|
unified_context: Optional["UnifiedExecutionContext"] = None,
|
|
129
130
|
enable_unified_coordination: bool = True,
|
|
131
|
+
# Phase 2: Recursion Control
|
|
132
|
+
enable_recursion_control: bool = True,
|
|
133
|
+
recursion_monitor: Optional[RecursionMonitor] = None,
|
|
130
134
|
) -> None:
|
|
131
135
|
self.llm = llm
|
|
132
136
|
self.tools = tools or {}
|
|
@@ -144,11 +148,17 @@ class AgentExecutor:
|
|
|
144
148
|
self.callbacks = callbacks or []
|
|
145
149
|
self.enable_steering = enable_steering
|
|
146
150
|
self.task_handlers = task_handlers or []
|
|
147
|
-
|
|
151
|
+
|
|
148
152
|
# Unified coordination
|
|
149
153
|
self.unified_context = unified_context
|
|
150
154
|
self.enable_unified_coordination = enable_unified_coordination
|
|
151
|
-
|
|
155
|
+
|
|
156
|
+
# Phase 2: Recursion control
|
|
157
|
+
self.enable_recursion_control = enable_recursion_control
|
|
158
|
+
self.recursion_monitor = recursion_monitor or RecursionMonitor(
|
|
159
|
+
max_iterations=max_iterations
|
|
160
|
+
)
|
|
161
|
+
|
|
152
162
|
# Initialize unified coordination if enabled
|
|
153
163
|
if self.enable_unified_coordination and UnifiedExecutionContext and IntelligentCoordinator:
|
|
154
164
|
self._setup_unified_coordination()
|
|
@@ -301,6 +311,56 @@ class AgentExecutor:
|
|
|
301
311
|
metadata={"parent_turn_id": turn_state.parent_turn_id},
|
|
302
312
|
)
|
|
303
313
|
|
|
314
|
+
# Phase 2: Advanced recursion control (optional)
|
|
315
|
+
if self.enable_recursion_control:
|
|
316
|
+
# Build recursion state from turn state
|
|
317
|
+
recursion_state = RecursionState(
|
|
318
|
+
iteration=turn_state.turn_counter,
|
|
319
|
+
tool_call_history=turn_state.tool_call_history,
|
|
320
|
+
error_count=turn_state.error_count,
|
|
321
|
+
last_outputs=turn_state.last_outputs
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Check for termination conditions
|
|
325
|
+
termination_reason = self.recursion_monitor.check_termination(
|
|
326
|
+
recursion_state
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if termination_reason:
|
|
330
|
+
# Emit termination event
|
|
331
|
+
yield AgentEvent(
|
|
332
|
+
type=AgentEventType.RECURSION_TERMINATED,
|
|
333
|
+
metadata={
|
|
334
|
+
"reason": termination_reason.value,
|
|
335
|
+
"iteration": turn_state.turn_counter,
|
|
336
|
+
"tool_call_history": turn_state.tool_call_history[-5:],
|
|
337
|
+
"error_count": turn_state.error_count
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Add termination message to prompt LLM to finish
|
|
342
|
+
termination_msg = self.recursion_monitor.build_termination_message(
|
|
343
|
+
termination_reason
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Add termination guidance as system message
|
|
347
|
+
messages = messages + [
|
|
348
|
+
Message(role="system", content=termination_msg)
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
# Note: We continue execution but with termination guidance
|
|
352
|
+
# The LLM will receive the termination message and should wrap up
|
|
353
|
+
|
|
354
|
+
# Check for early warnings (not terminating yet, just warning)
|
|
355
|
+
elif warning_msg := self.recursion_monitor.should_add_warning(
|
|
356
|
+
recursion_state,
|
|
357
|
+
warning_threshold=0.8
|
|
358
|
+
):
|
|
359
|
+
# Add warning as system message
|
|
360
|
+
messages = messages + [
|
|
361
|
+
Message(role="system", content=warning_msg)
|
|
362
|
+
]
|
|
363
|
+
|
|
304
364
|
# Base case 1: Maximum recursion depth reached
|
|
305
365
|
if turn_state.is_final:
|
|
306
366
|
yield AgentEvent(
|
|
@@ -581,22 +641,37 @@ class AgentExecutor:
|
|
|
581
641
|
# Phase 5: Recursive Call (Tail Recursion)
|
|
582
642
|
# ==========================================
|
|
583
643
|
|
|
584
|
-
#
|
|
585
|
-
|
|
644
|
+
# Phase 2: Track tool calls and errors for recursion control
|
|
645
|
+
tool_names_called = [tc.name for tc in tc_models]
|
|
646
|
+
had_tool_errors = any(r.is_error for r in tool_results)
|
|
647
|
+
|
|
648
|
+
# Extract output for loop detection (use first tool result or content)
|
|
649
|
+
output_sample = None
|
|
650
|
+
if tool_results:
|
|
651
|
+
output_sample = tool_results[0].content[:200] # First 200 chars
|
|
652
|
+
elif content:
|
|
653
|
+
output_sample = content[:200]
|
|
654
|
+
|
|
655
|
+
# Prepare next turn state with recursion tracking
|
|
656
|
+
next_state = turn_state.next_turn(
|
|
657
|
+
compacted=compacted_this_turn,
|
|
658
|
+
tool_calls=tool_names_called,
|
|
659
|
+
had_error=had_tool_errors,
|
|
660
|
+
output=output_sample
|
|
661
|
+
)
|
|
586
662
|
|
|
587
|
-
# Prepare next turn messages with intelligent context guidance
|
|
588
|
-
|
|
663
|
+
# Phase 3: Prepare next turn messages with intelligent context guidance
|
|
664
|
+
# This now includes tool results, compression, and recursion hints
|
|
665
|
+
next_messages = await self._prepare_recursive_messages(
|
|
589
666
|
messages, tool_results, turn_state, context
|
|
590
667
|
)
|
|
591
|
-
|
|
592
|
-
#
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
tool_call_id=r.tool_call_id,
|
|
599
|
-
)
|
|
668
|
+
|
|
669
|
+
# Check if compression was applied and emit event
|
|
670
|
+
if "last_compression" in context.metadata:
|
|
671
|
+
comp_info = context.metadata.pop("last_compression")
|
|
672
|
+
yield AgentEvent(
|
|
673
|
+
type=AgentEventType.COMPRESSION_APPLIED,
|
|
674
|
+
metadata=comp_info
|
|
600
675
|
)
|
|
601
676
|
|
|
602
677
|
# Emit recursion event
|
|
@@ -606,6 +681,8 @@ class AgentExecutor:
|
|
|
606
681
|
"from_turn": turn_state.turn_id,
|
|
607
682
|
"to_turn": next_state.turn_id,
|
|
608
683
|
"depth": next_state.turn_counter,
|
|
684
|
+
"tools_called": tool_names_called,
|
|
685
|
+
"message_count": len(next_messages),
|
|
609
686
|
},
|
|
610
687
|
)
|
|
611
688
|
|
|
@@ -617,7 +694,7 @@ class AgentExecutor:
|
|
|
617
694
|
# Intelligent Recursion Methods
|
|
618
695
|
# ==========================================
|
|
619
696
|
|
|
620
|
-
def _prepare_recursive_messages(
|
|
697
|
+
async def _prepare_recursive_messages(
|
|
621
698
|
self,
|
|
622
699
|
messages: List[Message],
|
|
623
700
|
tool_results: List[ToolResult],
|
|
@@ -625,22 +702,131 @@ class AgentExecutor:
|
|
|
625
702
|
context: ExecutionContext,
|
|
626
703
|
) -> List[Message]:
|
|
627
704
|
"""
|
|
628
|
-
智能准备递归调用的消息
|
|
629
|
-
|
|
630
|
-
|
|
705
|
+
Phase 3: 智能准备递归调用的消息
|
|
706
|
+
|
|
707
|
+
确保工具结果正确传递到下一轮,并进行必要的上下文优化
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
messages: 当前轮次的消息
|
|
711
|
+
tool_results: 工具执行结果
|
|
712
|
+
turn_state: 当前轮次状态
|
|
713
|
+
context: 执行上下文
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
准备好的下一轮消息列表
|
|
631
717
|
"""
|
|
632
|
-
#
|
|
718
|
+
# 1. 分析工具结果(用于生成智能指导)
|
|
633
719
|
result_analysis = self._analyze_tool_results(tool_results)
|
|
634
|
-
|
|
635
|
-
# 获取原始任务
|
|
636
720
|
original_task = self._extract_original_task(messages)
|
|
637
|
-
|
|
638
|
-
# 生成智能指导消息
|
|
721
|
+
|
|
722
|
+
# 2. 生成智能指导消息
|
|
639
723
|
guidance_message = self._generate_recursion_guidance(
|
|
640
724
|
original_task, result_analysis, turn_state.turn_counter
|
|
641
725
|
)
|
|
642
|
-
|
|
643
|
-
|
|
726
|
+
|
|
727
|
+
# 3. 构建下一轮消息:用户指导
|
|
728
|
+
next_messages = [Message(role="user", content=guidance_message)]
|
|
729
|
+
|
|
730
|
+
# 4. 添加工具结果消息(关键:确保工具结果被传递)
|
|
731
|
+
for result in tool_results:
|
|
732
|
+
next_messages.append(Message(
|
|
733
|
+
role="tool",
|
|
734
|
+
content=result.content,
|
|
735
|
+
tool_call_id=result.tool_call_id,
|
|
736
|
+
metadata=result.metadata or {}
|
|
737
|
+
))
|
|
738
|
+
|
|
739
|
+
# 5. Phase 3: 检查上下文长度
|
|
740
|
+
estimated_tokens = self._estimate_tokens(next_messages)
|
|
741
|
+
compression_applied = False
|
|
742
|
+
|
|
743
|
+
if estimated_tokens > self.max_context_tokens:
|
|
744
|
+
# 触发压缩(如果有 compressor)
|
|
745
|
+
if self.compressor:
|
|
746
|
+
tokens_before = estimated_tokens
|
|
747
|
+
next_messages = await self._compress_messages(next_messages)
|
|
748
|
+
tokens_after = self._estimate_tokens(next_messages)
|
|
749
|
+
compression_applied = True
|
|
750
|
+
|
|
751
|
+
# Store compression info for later emission
|
|
752
|
+
context.metadata["last_compression"] = {
|
|
753
|
+
"tokens_before": tokens_before,
|
|
754
|
+
"tokens_after": tokens_after,
|
|
755
|
+
"trigger": "recursive_message_preparation"
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
# 6. Phase 3: 添加递归深度提示(深度递归时)
|
|
759
|
+
if turn_state.turn_counter > 3:
|
|
760
|
+
hint_content = self._build_recursion_hint(
|
|
761
|
+
turn_state.turn_counter,
|
|
762
|
+
turn_state.max_iterations
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
hint = Message(
|
|
766
|
+
role="system",
|
|
767
|
+
content=hint_content
|
|
768
|
+
)
|
|
769
|
+
next_messages.append(hint)
|
|
770
|
+
|
|
771
|
+
return next_messages
|
|
772
|
+
|
|
773
|
+
def _estimate_tokens(self, messages: List[Message]) -> int:
|
|
774
|
+
"""
|
|
775
|
+
估算消息列表的 token 数量
|
|
776
|
+
|
|
777
|
+
使用简单的启发式方法:字符数 / 4
|
|
778
|
+
生产环境中应使用具体模型的 tokenizer
|
|
779
|
+
"""
|
|
780
|
+
return count_messages_tokens(messages)
|
|
781
|
+
|
|
782
|
+
async def _compress_messages(
|
|
783
|
+
self,
|
|
784
|
+
messages: List[Message]
|
|
785
|
+
) -> List[Message]:
|
|
786
|
+
"""
|
|
787
|
+
压缩消息列表(如果有 compressor)
|
|
788
|
+
|
|
789
|
+
这个方法会调用配置的 compressor 来减少上下文长度
|
|
790
|
+
"""
|
|
791
|
+
if not self.compressor:
|
|
792
|
+
return messages
|
|
793
|
+
|
|
794
|
+
try:
|
|
795
|
+
compressed, metadata = await self.compressor.compress(messages)
|
|
796
|
+
|
|
797
|
+
# Update compression metrics
|
|
798
|
+
self.metrics.metrics.compressions = (
|
|
799
|
+
getattr(self.metrics.metrics, "compressions", 0) + 1
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
return compressed
|
|
803
|
+
except Exception as e:
|
|
804
|
+
# If compression fails, return original messages
|
|
805
|
+
self.metrics.metrics.total_errors += 1
|
|
806
|
+
await self._emit(
|
|
807
|
+
"error",
|
|
808
|
+
{"stage": "message_compression", "message": str(e)}
|
|
809
|
+
)
|
|
810
|
+
return messages
|
|
811
|
+
|
|
812
|
+
def _build_recursion_hint(self, current_depth: int, max_depth: int) -> str:
|
|
813
|
+
"""
|
|
814
|
+
构建递归深度提示消息
|
|
815
|
+
|
|
816
|
+
在深度递归时提醒 LLM 注意进度和避免重复
|
|
817
|
+
"""
|
|
818
|
+
remaining = max_depth - current_depth
|
|
819
|
+
progress = (current_depth / max_depth) * 100
|
|
820
|
+
|
|
821
|
+
hint = f"""🔄 Recursion Status:
|
|
822
|
+
- Depth: {current_depth}/{max_depth} ({progress:.0f}% of maximum)
|
|
823
|
+
- Remaining iterations: {remaining}
|
|
824
|
+
|
|
825
|
+
Please review the tool results above and make meaningful progress towards completing the task.
|
|
826
|
+
Avoid calling the same tool repeatedly with the same arguments unless necessary.
|
|
827
|
+
If you have enough information, please provide your final answer."""
|
|
828
|
+
|
|
829
|
+
return hint
|
|
644
830
|
|
|
645
831
|
def _analyze_tool_results(self, tool_results: List[ToolResult]) -> Dict[str, Any]:
|
|
646
832
|
"""分析工具结果类型和质量"""
|
loom/core/events.py
CHANGED
|
@@ -114,6 +114,9 @@ class AgentEventType(Enum):
|
|
|
114
114
|
RECURSION = "recursion"
|
|
115
115
|
"""Recursive call initiated (tt mode)"""
|
|
116
116
|
|
|
117
|
+
RECURSION_TERMINATED = "recursion_terminated"
|
|
118
|
+
"""Recursion terminated due to loop detection or limits (Phase 2 optimization)"""
|
|
119
|
+
|
|
117
120
|
AGENT_FINISH = "agent_finish"
|
|
118
121
|
"""Agent execution finished successfully"""
|
|
119
122
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recursion Control for Agent Executor
|
|
3
|
+
|
|
4
|
+
Provides generic recursion termination detection to prevent infinite loops
|
|
5
|
+
in agent execution. This is a framework-level capability that doesn't depend
|
|
6
|
+
on specific business logic.
|
|
7
|
+
|
|
8
|
+
New in Loom 0.0.4: Phase 2 - Execution Layer Optimization
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import List, Optional, Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TerminationReason(str, Enum):
|
|
19
|
+
"""Reasons for terminating recursive execution"""
|
|
20
|
+
|
|
21
|
+
MAX_ITERATIONS = "max_iterations"
|
|
22
|
+
"""Maximum iteration limit reached"""
|
|
23
|
+
|
|
24
|
+
DUPLICATE_TOOLS = "duplicate_tools"
|
|
25
|
+
"""Detected repeated tool calls (same tool called multiple times in a row)"""
|
|
26
|
+
|
|
27
|
+
LOOP_DETECTED = "loop_detected"
|
|
28
|
+
"""Detected a pattern loop in outputs"""
|
|
29
|
+
|
|
30
|
+
ERROR_THRESHOLD = "error_threshold"
|
|
31
|
+
"""Error rate exceeded acceptable threshold"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RecursionState:
|
|
36
|
+
"""
|
|
37
|
+
State information for recursion monitoring.
|
|
38
|
+
|
|
39
|
+
This is a separate state object from TurnState to avoid coupling
|
|
40
|
+
the recursion control logic with the turn management logic.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
iteration: Current iteration count (0-based)
|
|
44
|
+
tool_call_history: List of tool names called in recent iterations
|
|
45
|
+
error_count: Number of errors encountered so far
|
|
46
|
+
last_outputs: Recent output samples for loop detection
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
iteration: int
|
|
50
|
+
"""Current iteration count (0-based)"""
|
|
51
|
+
|
|
52
|
+
tool_call_history: List[str]
|
|
53
|
+
"""History of tool names called (for duplicate detection)"""
|
|
54
|
+
|
|
55
|
+
error_count: int
|
|
56
|
+
"""Number of errors encountered during execution"""
|
|
57
|
+
|
|
58
|
+
last_outputs: List[Any]
|
|
59
|
+
"""Recent outputs for loop pattern detection"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RecursionMonitor:
|
|
63
|
+
"""
|
|
64
|
+
Generic recursion monitoring and termination detection.
|
|
65
|
+
|
|
66
|
+
This monitor provides framework-level recursion control without
|
|
67
|
+
depending on any specific business logic. It detects common
|
|
68
|
+
infinite loop patterns:
|
|
69
|
+
|
|
70
|
+
1. Maximum iteration limit
|
|
71
|
+
2. Repeated tool calls (same tool called N times in a row)
|
|
72
|
+
3. Loop patterns in outputs
|
|
73
|
+
4. High error rates
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
```python
|
|
77
|
+
# Create monitor with custom thresholds
|
|
78
|
+
monitor = RecursionMonitor(
|
|
79
|
+
max_iterations=50,
|
|
80
|
+
duplicate_threshold=3,
|
|
81
|
+
loop_detection_window=5,
|
|
82
|
+
error_threshold=0.5
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Check if should terminate
|
|
86
|
+
state = RecursionState(
|
|
87
|
+
iteration=10,
|
|
88
|
+
tool_call_history=["search", "search", "search"],
|
|
89
|
+
error_count=2,
|
|
90
|
+
last_outputs=[]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
reason = monitor.check_termination(state)
|
|
94
|
+
if reason:
|
|
95
|
+
message = monitor.build_termination_message(reason)
|
|
96
|
+
print(f"Terminating: {message}")
|
|
97
|
+
```
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
max_iterations: int = 50,
|
|
103
|
+
duplicate_threshold: int = 3,
|
|
104
|
+
loop_detection_window: int = 5,
|
|
105
|
+
error_threshold: float = 0.5
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
Initialize recursion monitor.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
max_iterations: Maximum number of recursive iterations allowed
|
|
112
|
+
duplicate_threshold: Number of consecutive duplicate tool calls before terminating
|
|
113
|
+
loop_detection_window: Window size for loop pattern detection
|
|
114
|
+
error_threshold: Maximum error rate (errors/iterations) before terminating
|
|
115
|
+
"""
|
|
116
|
+
self.max_iterations = max_iterations
|
|
117
|
+
self.duplicate_threshold = duplicate_threshold
|
|
118
|
+
self.loop_detection_window = loop_detection_window
|
|
119
|
+
self.error_threshold = error_threshold
|
|
120
|
+
|
|
121
|
+
def check_termination(
|
|
122
|
+
self,
|
|
123
|
+
state: RecursionState
|
|
124
|
+
) -> Optional[TerminationReason]:
|
|
125
|
+
"""
|
|
126
|
+
Check if recursive execution should terminate.
|
|
127
|
+
|
|
128
|
+
This method runs multiple checks in priority order:
|
|
129
|
+
1. Max iterations (highest priority - hard limit)
|
|
130
|
+
2. Duplicate tool calls (likely stuck)
|
|
131
|
+
3. Loop patterns (repeating behavior)
|
|
132
|
+
4. Error threshold (too many failures)
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
state: Current recursion state
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
TerminationReason if should terminate, None to continue
|
|
139
|
+
"""
|
|
140
|
+
# Check 1: Maximum iterations (hard limit)
|
|
141
|
+
if state.iteration >= self.max_iterations:
|
|
142
|
+
return TerminationReason.MAX_ITERATIONS
|
|
143
|
+
|
|
144
|
+
# Check 2: Duplicate tool calls (likely stuck)
|
|
145
|
+
if self._detect_duplicate_tools(state.tool_call_history):
|
|
146
|
+
return TerminationReason.DUPLICATE_TOOLS
|
|
147
|
+
|
|
148
|
+
# Check 3: Loop patterns in outputs
|
|
149
|
+
if self._detect_loop_pattern(state.last_outputs):
|
|
150
|
+
return TerminationReason.LOOP_DETECTED
|
|
151
|
+
|
|
152
|
+
# Check 4: Error rate threshold
|
|
153
|
+
if self._check_error_threshold(state):
|
|
154
|
+
return TerminationReason.ERROR_THRESHOLD
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _detect_duplicate_tools(self, tool_history: List[str]) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Detect if the same tool has been called too many times in a row.
|
|
161
|
+
|
|
162
|
+
This indicates the agent is stuck in a loop, repeatedly trying
|
|
163
|
+
the same tool without making progress.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
tool_history: List of tool names (most recent last)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if duplicate pattern detected
|
|
170
|
+
"""
|
|
171
|
+
if len(tool_history) < self.duplicate_threshold:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Check last N tool calls
|
|
175
|
+
recent = tool_history[-self.duplicate_threshold:]
|
|
176
|
+
|
|
177
|
+
# All the same? -> Stuck in loop
|
|
178
|
+
return len(set(recent)) == 1
|
|
179
|
+
|
|
180
|
+
def _detect_loop_pattern(self, outputs: List[Any]) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Detect if outputs are repeating in a pattern.
|
|
183
|
+
|
|
184
|
+
This checks if the agent is generating the same outputs
|
|
185
|
+
repeatedly, indicating a stuck state.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
outputs: Recent output values
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if loop pattern detected
|
|
192
|
+
"""
|
|
193
|
+
if len(outputs) < self.loop_detection_window * 2:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
window_size = self.loop_detection_window
|
|
197
|
+
recent = outputs[-window_size * 2:]
|
|
198
|
+
|
|
199
|
+
# Split into two halves and compare
|
|
200
|
+
first_half = recent[:window_size]
|
|
201
|
+
second_half = recent[window_size:]
|
|
202
|
+
|
|
203
|
+
# If both halves are identical, we have a loop
|
|
204
|
+
return first_half == second_half
|
|
205
|
+
|
|
206
|
+
def _check_error_threshold(self, state: RecursionState) -> bool:
|
|
207
|
+
"""
|
|
208
|
+
Check if error rate exceeds acceptable threshold.
|
|
209
|
+
|
|
210
|
+
Too many errors indicate the agent cannot complete the task
|
|
211
|
+
and should stop trying.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
state: Current recursion state
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
True if error rate exceeds threshold
|
|
218
|
+
"""
|
|
219
|
+
if state.iteration == 0:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
error_rate = state.error_count / state.iteration
|
|
223
|
+
return error_rate > self.error_threshold
|
|
224
|
+
|
|
225
|
+
def build_termination_message(
|
|
226
|
+
self,
|
|
227
|
+
reason: TerminationReason
|
|
228
|
+
) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Build a user-friendly termination message.
|
|
231
|
+
|
|
232
|
+
This message is injected into the conversation to prompt
|
|
233
|
+
the LLM to complete the task with available information.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
reason: The termination reason
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Formatted termination message
|
|
240
|
+
"""
|
|
241
|
+
messages = {
|
|
242
|
+
TerminationReason.DUPLICATE_TOOLS: (
|
|
243
|
+
"⚠️ Detected repeated tool calls. "
|
|
244
|
+
"Please proceed with available information."
|
|
245
|
+
),
|
|
246
|
+
TerminationReason.LOOP_DETECTED: (
|
|
247
|
+
"⚠️ Detected execution loop. "
|
|
248
|
+
"Please break the pattern and complete the task."
|
|
249
|
+
),
|
|
250
|
+
TerminationReason.MAX_ITERATIONS: (
|
|
251
|
+
"⚠️ Maximum iterations reached. "
|
|
252
|
+
"Please provide the best answer with current information."
|
|
253
|
+
),
|
|
254
|
+
TerminationReason.ERROR_THRESHOLD: (
|
|
255
|
+
"⚠️ Too many errors occurred. "
|
|
256
|
+
"Please complete the task with current information."
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return messages.get(reason, "Please complete the task now.")
|
|
261
|
+
|
|
262
|
+
def should_add_warning(
|
|
263
|
+
self,
|
|
264
|
+
state: RecursionState,
|
|
265
|
+
warning_threshold: float = 0.8
|
|
266
|
+
) -> Optional[str]:
|
|
267
|
+
"""
|
|
268
|
+
Check if a warning should be added before termination.
|
|
269
|
+
|
|
270
|
+
This provides early warning when approaching limits,
|
|
271
|
+
giving the agent a chance to wrap up gracefully.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
state: Current recursion state
|
|
275
|
+
warning_threshold: Fraction of limit at which to warn (0.0-1.0)
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Warning message if applicable, None otherwise
|
|
279
|
+
"""
|
|
280
|
+
# Check if approaching max iterations
|
|
281
|
+
progress = state.iteration / self.max_iterations
|
|
282
|
+
if progress >= warning_threshold:
|
|
283
|
+
remaining = self.max_iterations - state.iteration
|
|
284
|
+
return (
|
|
285
|
+
f"⚠️ Approaching iteration limit ({remaining} remaining). "
|
|
286
|
+
f"Please work towards completing the task."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Check if tool calls are becoming repetitive
|
|
290
|
+
if len(state.tool_call_history) >= self.duplicate_threshold - 1:
|
|
291
|
+
recent = state.tool_call_history[-(self.duplicate_threshold - 1):]
|
|
292
|
+
if len(set(recent)) == 1:
|
|
293
|
+
return (
|
|
294
|
+
f"⚠️ You've called '{recent[0]}' multiple times. "
|
|
295
|
+
f"Consider trying a different approach or completing the task."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return None
|