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.

@@ -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
- # Prepare next turn state
585
- next_state = turn_state.next_turn(compacted=compacted_this_turn)
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
- next_messages = self._prepare_recursive_messages(
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
- # Add tool results
593
- for r in tool_results:
594
- next_messages.append(
595
- Message(
596
- role="tool",
597
- content=r.content,
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
- return [Message(role="user", content=guidance_message)]
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