loom-agent 0.3.3__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.
Files changed (52) hide show
  1. loom/__init__.py +1 -0
  2. loom/adapters/converters.py +77 -0
  3. loom/adapters/registry.py +43 -0
  4. loom/api/factory.py +77 -0
  5. loom/api/main.py +201 -0
  6. loom/builtin/__init__.py +3 -0
  7. loom/builtin/memory/__init__.py +3 -0
  8. loom/builtin/memory/metabolic.py +96 -0
  9. loom/builtin/memory/pso.py +41 -0
  10. loom/builtin/memory/sanitizers.py +39 -0
  11. loom/builtin/memory/validators.py +55 -0
  12. loom/config/tool.py +63 -0
  13. loom/infra/__init__.py +0 -0
  14. loom/infra/llm.py +44 -0
  15. loom/infra/logging.py +42 -0
  16. loom/infra/store.py +39 -0
  17. loom/infra/transport/memory.py +112 -0
  18. loom/infra/transport/nats.py +170 -0
  19. loom/infra/transport/redis.py +161 -0
  20. loom/interfaces/llm.py +45 -0
  21. loom/interfaces/memory.py +50 -0
  22. loom/interfaces/store.py +29 -0
  23. loom/interfaces/transport.py +35 -0
  24. loom/kernel/__init__.py +0 -0
  25. loom/kernel/base_interceptor.py +97 -0
  26. loom/kernel/bus.py +85 -0
  27. loom/kernel/dispatcher.py +58 -0
  28. loom/kernel/interceptors/__init__.py +14 -0
  29. loom/kernel/interceptors/adaptive.py +567 -0
  30. loom/kernel/interceptors/budget.py +60 -0
  31. loom/kernel/interceptors/depth.py +45 -0
  32. loom/kernel/interceptors/hitl.py +51 -0
  33. loom/kernel/interceptors/studio.py +129 -0
  34. loom/kernel/interceptors/timeout.py +27 -0
  35. loom/kernel/state.py +71 -0
  36. loom/memory/hierarchical.py +124 -0
  37. loom/node/__init__.py +0 -0
  38. loom/node/agent.py +252 -0
  39. loom/node/base.py +121 -0
  40. loom/node/crew.py +105 -0
  41. loom/node/router.py +77 -0
  42. loom/node/tool.py +50 -0
  43. loom/protocol/__init__.py +0 -0
  44. loom/protocol/cloudevents.py +73 -0
  45. loom/protocol/interfaces.py +164 -0
  46. loom/protocol/mcp.py +97 -0
  47. loom/protocol/memory_operations.py +51 -0
  48. loom/protocol/patch.py +93 -0
  49. loom_agent-0.3.3.dist-info/LICENSE +204 -0
  50. loom_agent-0.3.3.dist-info/METADATA +139 -0
  51. loom_agent-0.3.3.dist-info/RECORD +52 -0
  52. loom_agent-0.3.3.dist-info/WHEEL +4 -0
@@ -0,0 +1,567 @@
1
+ """
2
+ Adaptive Control Interceptor (SDE Noise Control)
3
+
4
+ Refactored with Human Factors Engineering Principles:
5
+ - Framework: DETECTS anomalies (what happened)
6
+ - Developer: CONFIGURES strategies (how to respond)
7
+ - System: EXECUTES strategies (action)
8
+
9
+ This separates concerns and gives developers full control over
10
+ recovery behaviors while framework handles detection logic.
11
+ """
12
+
13
+ from abc import ABC, abstractmethod
14
+ from enum import Enum, auto
15
+ from dataclasses import dataclass, field
16
+ from typing import Dict, Optional, Callable, Awaitable, List, Any, Union
17
+
18
+ from loom.protocol.cloudevents import CloudEvent
19
+ from loom.kernel.base_interceptor import Interceptor
20
+
21
+
22
+ # =============================================================================
23
+ # 1. ANOMALY TYPES (Framework defines WHAT can be detected)
24
+ # =============================================================================
25
+
26
+ class AnomalyType(Enum):
27
+ """
28
+ Types of anomalies that the framework can detect.
29
+ Framework responsibility: Detection only.
30
+ """
31
+ REPETITIVE_REASONING = auto() # Agent repeating same thoughts
32
+ HALLUCINATION = auto() # Agent producing unreliable content
33
+ STALLED = auto() # Agent stuck, no progress
34
+ VALIDATION_FAILED = auto() # Output failed quality checks
35
+ DEPTH_EXCEEDED = auto() # Fractal depth limit hit
36
+ BUDGET_EXHAUSTED = auto() # Token/resource budget depleted
37
+ TIMEOUT_APPROACHING = auto() # Deadline pressure
38
+ TOOL_ERROR = auto() # Tool execution failed
39
+ CONVERGENCE_DETECTED = auto() # Agent successfully converging (positive)
40
+
41
+
42
+ # =============================================================================
43
+ # 2. RECOVERY ACTIONS (Framework defines WHAT can be done)
44
+ # =============================================================================
45
+
46
+ class RecoveryAction(Enum):
47
+ """
48
+ Atomic recovery actions that framework can execute.
49
+ These are building blocks; developers compose them into strategies.
50
+ """
51
+ # Temperature Control
52
+ INCREASE_TEMPERATURE = auto() # Add noise to escape local minima
53
+ DECREASE_TEMPERATURE = auto() # Reduce noise for convergence
54
+ RESET_TEMPERATURE = auto() # Return to default
55
+
56
+ # Prompt Engineering
57
+ INJECT_REFLECTION_PROMPT = auto() # Force agent to reflect
58
+ INJECT_SIMPLIFY_PROMPT = auto() # Ask agent to simplify approach
59
+ INJECT_ALTERNATIVE_PROMPT = auto() # Suggest trying different approach
60
+
61
+ # Control Flow
62
+ TRIGGER_HITL = auto() # Human-in-the-Loop approval
63
+ FORCE_TERMINATE = auto() # Stop execution immediately
64
+ RETRY_CURRENT_STEP = auto() # Retry the same step
65
+ ROLLBACK_TO_CHECKPOINT = auto() # Restore previous state
66
+
67
+ # Model/Resource Control
68
+ SWITCH_MODEL = auto() # Try different LLM
69
+ REDUCE_MAX_TOKENS = auto() # Limit output length
70
+ EXTEND_TIMEOUT = auto() # Give more time
71
+
72
+ # Observability
73
+ EMIT_WARNING_EVENT = auto() # Alert but continue
74
+ LOG_FOR_ANALYSIS = auto() # Record for post-mortem
75
+
76
+ # Developer Extension
77
+ CUSTOM_HANDLER = auto() # Execute developer callback
78
+
79
+
80
+ # =============================================================================
81
+ # 3. RECOVERY STRATEGY (Developer configures HOW to respond)
82
+ # =============================================================================
83
+
84
+ @dataclass
85
+ class RecoveryStrategy:
86
+ """
87
+ A strategy is a sequence of actions with parameters.
88
+
89
+ Developers configure these to define recovery behavior.
90
+ """
91
+ actions: List[RecoveryAction]
92
+ params: Dict[str, Any] = field(default_factory=dict)
93
+
94
+ # Optional custom handler for CUSTOM_HANDLER action
95
+ custom_handler: Optional[Callable[[CloudEvent, 'AnomalyContext'], Awaitable[Optional[CloudEvent]]]] = None
96
+
97
+ # Strategy metadata
98
+ description: str = ""
99
+ max_retries: int = 3 # How many times to try this strategy
100
+
101
+
102
+ @dataclass
103
+ class AnomalyContext:
104
+ """
105
+ Context passed to custom handlers with full anomaly information.
106
+ """
107
+ anomaly_type: AnomalyType
108
+ event: CloudEvent
109
+ agent_id: str
110
+ occurrence_count: int # How many times this anomaly occurred for this agent
111
+ history: List[Dict] # Recent anomaly history
112
+ metadata: Dict[str, Any] = field(default_factory=dict)
113
+
114
+
115
+ # =============================================================================
116
+ # 4. ADAPTIVE CONFIG (Developer's control surface)
117
+ # =============================================================================
118
+
119
+ @dataclass
120
+ class AdaptiveConfig:
121
+ """
122
+ Developer-facing configuration for adaptive control.
123
+
124
+ Maps anomaly types to recovery strategies.
125
+
126
+ Example Usage:
127
+ ```python
128
+ config = AdaptiveConfig(
129
+ strategies={
130
+ AnomalyType.REPETITIVE_REASONING: RecoveryStrategy(
131
+ actions=[RecoveryAction.INJECT_REFLECTION_PROMPT, RecoveryAction.INCREASE_TEMPERATURE],
132
+ params={"temperature_delta": 0.2, "reflection_prompt": "你似乎在重复。请尝试不同的方法。"}
133
+ ),
134
+ AnomalyType.STALLED: RecoveryStrategy(
135
+ actions=[RecoveryAction.TRIGGER_HITL],
136
+ description="让人工介入处理停滞情况"
137
+ ),
138
+ AnomalyType.HALLUCINATION: RecoveryStrategy(
139
+ actions=[RecoveryAction.CUSTOM_HANDLER],
140
+ custom_handler=my_hallucination_handler
141
+ )
142
+ }
143
+ )
144
+ ```
145
+ """
146
+ # Core strategy mapping
147
+ strategies: Dict[AnomalyType, RecoveryStrategy] = field(default_factory=dict)
148
+
149
+ # Global defaults
150
+ default_temperature: float = 0.5
151
+ temperature_bounds: tuple = (0.0, 1.0) # (min, max)
152
+
153
+ # Detection thresholds (developer can tune sensitivity)
154
+ repetition_threshold: int = 3 # N similar thoughts = repetitive
155
+ stall_threshold_seconds: float = 30.0 # No progress for N seconds = stalled
156
+
157
+ # Escalation config
158
+ escalation_enabled: bool = True
159
+ escalation_after_failures: int = 3 # After N failed recoveries, escalate
160
+ escalation_strategy: Optional[RecoveryStrategy] = None # Ultimate fallback
161
+
162
+ def get_strategy(self, anomaly: AnomalyType) -> Optional[RecoveryStrategy]:
163
+ """Get configured strategy for an anomaly type."""
164
+ return self.strategies.get(anomaly)
165
+
166
+
167
+ # =============================================================================
168
+ # 5. DEFAULT STRATEGIES (Sensible defaults, fully overridable)
169
+ # =============================================================================
170
+
171
+ def create_default_config() -> AdaptiveConfig:
172
+ """
173
+ Factory for default configuration.
174
+
175
+ Developers can use this as a starting point:
176
+ ```python
177
+ config = create_default_config()
178
+ config.strategies[AnomalyType.STALLED] = my_custom_strategy
179
+ ```
180
+ """
181
+ return AdaptiveConfig(
182
+ strategies={
183
+ AnomalyType.REPETITIVE_REASONING: RecoveryStrategy(
184
+ actions=[RecoveryAction.EMIT_WARNING_EVENT, RecoveryAction.INJECT_REFLECTION_PROMPT],
185
+ params={"reflection_prompt": "I notice you may be repeating yourself. Please try a different approach."},
186
+ description="Warn and suggest reflection on repetition"
187
+ ),
188
+ AnomalyType.STALLED: RecoveryStrategy(
189
+ actions=[RecoveryAction.EMIT_WARNING_EVENT, RecoveryAction.RETRY_CURRENT_STEP],
190
+ params={"max_retries": 2},
191
+ description="Warn and retry on stall"
192
+ ),
193
+ AnomalyType.VALIDATION_FAILED: RecoveryStrategy(
194
+ actions=[RecoveryAction.LOG_FOR_ANALYSIS, RecoveryAction.RETRY_CURRENT_STEP],
195
+ description="Log and retry on validation failure"
196
+ ),
197
+ AnomalyType.CONVERGENCE_DETECTED: RecoveryStrategy(
198
+ actions=[RecoveryAction.DECREASE_TEMPERATURE],
199
+ params={"temperature_delta": -0.2},
200
+ description="Reduce noise when converging"
201
+ ),
202
+ },
203
+ escalation_strategy=RecoveryStrategy(
204
+ actions=[RecoveryAction.TRIGGER_HITL, RecoveryAction.FORCE_TERMINATE],
205
+ description="Ultimate fallback: human intervention then terminate"
206
+ )
207
+ )
208
+
209
+
210
+ # =============================================================================
211
+ # 6. ANOMALY DETECTOR (Framework's detection logic)
212
+ # =============================================================================
213
+
214
+ class AnomalyDetector:
215
+ """
216
+ Detects anomalies based on event patterns.
217
+
218
+ Framework responsibility: Detection ONLY.
219
+ Does NOT decide how to handle - that's the Strategy's job.
220
+ """
221
+
222
+ def __init__(self, config: AdaptiveConfig):
223
+ self.config = config
224
+ self._thought_history: Dict[str, List[str]] = {} # agent_id -> recent thoughts
225
+ self._last_progress: Dict[str, float] = {} # agent_id -> timestamp
226
+
227
+ def detect(self, event: CloudEvent, agent_id: str) -> Optional[AnomalyType]:
228
+ """
229
+ Analyze event and return detected anomaly type (if any).
230
+ """
231
+ import time
232
+
233
+ if event.type == "agent.thought":
234
+ thought = event.data.get("thought", "")
235
+ if self._is_repetitive(agent_id, thought):
236
+ return AnomalyType.REPETITIVE_REASONING
237
+
238
+ elif event.type == "agent.stalled":
239
+ return AnomalyType.STALLED
240
+
241
+ elif event.type == "validation.failed":
242
+ return AnomalyType.VALIDATION_FAILED
243
+
244
+ elif event.type == "task.completed" or event.type == "agent.success":
245
+ return AnomalyType.CONVERGENCE_DETECTED
246
+
247
+ # Check for stall by time
248
+ current_time = time.time()
249
+ last = self._last_progress.get(agent_id, current_time)
250
+ if current_time - last > self.config.stall_threshold_seconds:
251
+ return AnomalyType.STALLED
252
+
253
+ self._last_progress[agent_id] = current_time
254
+ return None
255
+
256
+ def _is_repetitive(self, agent_id: str, thought: str) -> bool:
257
+ """Check if agent is repeating similar thoughts."""
258
+ history = self._thought_history.setdefault(agent_id, [])
259
+
260
+ # Simple similarity check (can be enhanced with embeddings)
261
+ similar_count = sum(1 for h in history[-10:] if self._similarity(h, thought) > 0.8)
262
+
263
+ history.append(thought)
264
+ if len(history) > 50:
265
+ history.pop(0)
266
+
267
+ return similar_count >= self.config.repetition_threshold
268
+
269
+ def _similarity(self, a: str, b: str) -> float:
270
+ """Simple text similarity (placeholder for more sophisticated methods)."""
271
+ if not a or not b:
272
+ return 0.0
273
+ # Jaccard similarity on word sets
274
+ set_a, set_b = set(a.lower().split()), set(b.lower().split())
275
+ if not set_a or not set_b:
276
+ return 0.0
277
+ return len(set_a & set_b) / len(set_a | set_b)
278
+
279
+ def reset(self, agent_id: str):
280
+ """Clear state for an agent."""
281
+ self._thought_history.pop(agent_id, None)
282
+ self._last_progress.pop(agent_id, None)
283
+
284
+
285
+ # =============================================================================
286
+ # 7. STRATEGY EXECUTOR (Framework executes developer's choices)
287
+ # =============================================================================
288
+
289
+ class StrategyExecutor:
290
+ """
291
+ Executes recovery strategies.
292
+
293
+ Framework responsibility: Execute actions as configured.
294
+ Does NOT make decisions - follows developer's strategy.
295
+ """
296
+
297
+ def __init__(self, config: AdaptiveConfig, dispatcher=None):
298
+ self.config = config
299
+ self.dispatcher = dispatcher
300
+ self._current_temps: Dict[str, float] = {} # agent_id -> temperature
301
+
302
+ async def execute(
303
+ self,
304
+ strategy: RecoveryStrategy,
305
+ context: AnomalyContext,
306
+ event: CloudEvent
307
+ ) -> Optional[CloudEvent]:
308
+ """
309
+ Execute a recovery strategy, returning modified event (or None to block).
310
+ """
311
+ modified_event = event
312
+
313
+ for action in strategy.actions:
314
+ result = await self._execute_action(action, strategy, context, modified_event)
315
+
316
+ if result is None:
317
+ # Action requested to block event
318
+ return None
319
+ modified_event = result
320
+
321
+ return modified_event
322
+
323
+ async def _execute_action(
324
+ self,
325
+ action: RecoveryAction,
326
+ strategy: RecoveryStrategy,
327
+ context: AnomalyContext,
328
+ event: CloudEvent
329
+ ) -> Optional[CloudEvent]:
330
+ """Execute a single recovery action."""
331
+
332
+ params = strategy.params
333
+ agent_id = context.agent_id
334
+
335
+ if action == RecoveryAction.INCREASE_TEMPERATURE:
336
+ delta = params.get("temperature_delta", 0.2)
337
+ return self._adjust_temperature(event, agent_id, delta)
338
+
339
+ elif action == RecoveryAction.DECREASE_TEMPERATURE:
340
+ delta = params.get("temperature_delta", -0.2)
341
+ return self._adjust_temperature(event, agent_id, delta)
342
+
343
+ elif action == RecoveryAction.RESET_TEMPERATURE:
344
+ self._current_temps[agent_id] = self.config.default_temperature
345
+ return self._inject_temperature(event, self.config.default_temperature)
346
+
347
+ elif action == RecoveryAction.INJECT_REFLECTION_PROMPT:
348
+ prompt = params.get("reflection_prompt", "Please reflect on your approach.")
349
+ return self._inject_system_hint(event, prompt)
350
+
351
+ elif action == RecoveryAction.INJECT_SIMPLIFY_PROMPT:
352
+ prompt = params.get("simplify_prompt", "Try a simpler approach.")
353
+ return self._inject_system_hint(event, prompt)
354
+
355
+ elif action == RecoveryAction.INJECT_ALTERNATIVE_PROMPT:
356
+ prompt = params.get("alternative_prompt", "Consider an alternative method.")
357
+ return self._inject_system_hint(event, prompt)
358
+
359
+ elif action == RecoveryAction.TRIGGER_HITL:
360
+ # Emit HITL request event
361
+ if self.dispatcher:
362
+ await self.dispatcher.dispatch(CloudEvent.create(
363
+ source="/kernel/adaptive",
364
+ type="hitl.request",
365
+ subject=agent_id,
366
+ data={
367
+ "anomaly": context.anomaly_type.name,
368
+ "reason": strategy.description,
369
+ "context": context.metadata
370
+ }
371
+ ))
372
+ # Block until approved (or let HITL interceptor handle)
373
+ event.extensions = event.extensions or {}
374
+ event.extensions["requires_hitl"] = True
375
+ return event
376
+
377
+ elif action == RecoveryAction.FORCE_TERMINATE:
378
+ # Return None to block the event
379
+ if self.dispatcher:
380
+ await self.dispatcher.dispatch(CloudEvent.create(
381
+ source="/kernel/adaptive",
382
+ type="agent.terminated",
383
+ subject=agent_id,
384
+ data={
385
+ "reason": f"Force terminated due to: {context.anomaly_type.name}",
386
+ "strategy": strategy.description
387
+ }
388
+ ))
389
+ return None
390
+
391
+ elif action == RecoveryAction.EMIT_WARNING_EVENT:
392
+ if self.dispatcher:
393
+ await self.dispatcher.dispatch(CloudEvent.create(
394
+ source="/kernel/adaptive",
395
+ type="agent.warning",
396
+ subject=agent_id,
397
+ data={
398
+ "anomaly": context.anomaly_type.name,
399
+ "occurrence_count": context.occurrence_count,
400
+ "message": strategy.description
401
+ }
402
+ ))
403
+ return event
404
+
405
+ elif action == RecoveryAction.LOG_FOR_ANALYSIS:
406
+ # Log for post-mortem (implement logging)
407
+ event.extensions = event.extensions or {}
408
+ event.extensions.setdefault("anomaly_log", []).append({
409
+ "type": context.anomaly_type.name,
410
+ "count": context.occurrence_count,
411
+ "timestamp": event.time
412
+ })
413
+ return event
414
+
415
+ elif action == RecoveryAction.CUSTOM_HANDLER:
416
+ if strategy.custom_handler:
417
+ return await strategy.custom_handler(event, context)
418
+ return event
419
+
420
+ # Default: pass through unchanged
421
+ return event
422
+
423
+ def _adjust_temperature(self, event: CloudEvent, agent_id: str, delta: float) -> CloudEvent:
424
+ """Adjust temperature by delta, respecting bounds."""
425
+ current = self._current_temps.get(agent_id, self.config.default_temperature)
426
+ new_temp = max(
427
+ self.config.temperature_bounds[0],
428
+ min(self.config.temperature_bounds[1], current + delta)
429
+ )
430
+ self._current_temps[agent_id] = new_temp
431
+ return self._inject_temperature(event, new_temp)
432
+
433
+ def _inject_temperature(self, event: CloudEvent, temperature: float) -> CloudEvent:
434
+ """Inject temperature into event extensions."""
435
+ extensions = event.extensions or {}
436
+ llm_config = extensions.get("llm_config_override", {})
437
+ llm_config["temperature"] = temperature
438
+ extensions["llm_config_override"] = llm_config
439
+ event.extensions = extensions
440
+ return event
441
+
442
+ def _inject_system_hint(self, event: CloudEvent, hint: str) -> CloudEvent:
443
+ """Inject a system hint for the agent."""
444
+ extensions = event.extensions or {}
445
+ extensions["system_hint"] = hint
446
+ event.extensions = extensions
447
+ return event
448
+
449
+
450
+ # =============================================================================
451
+ # 8. MAIN INTERCEPTOR (Orchestrates Detection → Strategy → Execution)
452
+ # =============================================================================
453
+
454
+ class AdaptiveLLMInterceptor(Interceptor):
455
+ """
456
+ Implements the "Dynamic Feedback" term of the Controlled Fractal equation.
457
+
458
+ Refactored with Human Factors Engineering Principles:
459
+ - Framework DETECTS (AnomalyDetector)
460
+ - Developer CONFIGURES (AdaptiveConfig)
461
+ - Framework EXECUTES (StrategyExecutor)
462
+
463
+ Usage:
464
+ ```python
465
+ # Use defaults
466
+ interceptor = AdaptiveLLMInterceptor()
467
+
468
+ # Or customize
469
+ config = create_default_config()
470
+ config.strategies[AnomalyType.STALLED] = RecoveryStrategy(
471
+ actions=[RecoveryAction.CUSTOM_HANDLER],
472
+ custom_handler=my_stall_handler
473
+ )
474
+ interceptor = AdaptiveLLMInterceptor(config=config)
475
+
476
+ app.dispatcher.add_interceptor(interceptor)
477
+ ```
478
+ """
479
+
480
+ def __init__(self, config: Optional[AdaptiveConfig] = None, dispatcher=None):
481
+ self.config = config or create_default_config()
482
+ self.detector = AnomalyDetector(self.config)
483
+ self.executor = StrategyExecutor(self.config, dispatcher)
484
+
485
+ # Track anomaly occurrences per agent
486
+ self._anomaly_counts: Dict[str, Dict[AnomalyType, int]] = {}
487
+ self._anomaly_history: Dict[str, List[Dict]] = {}
488
+
489
+ def set_dispatcher(self, dispatcher):
490
+ """Set dispatcher after construction (for DI)."""
491
+ self.executor.dispatcher = dispatcher
492
+
493
+ async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
494
+ """
495
+ Detect anomalies and execute configured strategies.
496
+ """
497
+ # Identify agent
498
+ agent_id = event.subject or event.source
499
+ if not agent_id:
500
+ return event
501
+
502
+ # Detect anomaly
503
+ anomaly = self.detector.detect(event, agent_id)
504
+
505
+ if anomaly is None:
506
+ return event
507
+
508
+ # Update occurrence count
509
+ agent_counts = self._anomaly_counts.setdefault(agent_id, {})
510
+ agent_counts[anomaly] = agent_counts.get(anomaly, 0) + 1
511
+ occurrence_count = agent_counts[anomaly]
512
+
513
+ # Build context
514
+ context = AnomalyContext(
515
+ anomaly_type=anomaly,
516
+ event=event,
517
+ agent_id=agent_id,
518
+ occurrence_count=occurrence_count,
519
+ history=self._anomaly_history.get(agent_id, []),
520
+ metadata={"extensions": event.extensions}
521
+ )
522
+
523
+ # Record in history
524
+ history = self._anomaly_history.setdefault(agent_id, [])
525
+ history.append({"type": anomaly.name, "count": occurrence_count})
526
+ if len(history) > 100:
527
+ history.pop(0)
528
+
529
+ # Get strategy (developer configured)
530
+ strategy = self.config.get_strategy(anomaly)
531
+
532
+ # Check for escalation
533
+ if strategy and occurrence_count > strategy.max_retries and self.config.escalation_enabled:
534
+ if occurrence_count > self.config.escalation_after_failures:
535
+ strategy = self.config.escalation_strategy
536
+
537
+ if strategy is None:
538
+ # No strategy configured for this anomaly - pass through
539
+ return event
540
+
541
+ # Execute strategy
542
+ return await self.executor.execute(strategy, context, event)
543
+
544
+ async def post_invoke(self, event: CloudEvent) -> None:
545
+ """
546
+ Post-processing: reset state on success.
547
+ """
548
+ if event.type in ("task.completed", "agent.success", "node.response"):
549
+ agent_id = event.source
550
+ if agent_id:
551
+ self.detector.reset(agent_id)
552
+ self._anomaly_counts.pop(agent_id, None)
553
+ self._anomaly_history.pop(agent_id, None)
554
+
555
+ async def on_feedback_event(self, event: CloudEvent):
556
+ """
557
+ External event handler for feedback events.
558
+ Register this with the event bus:
559
+
560
+ ```python
561
+ bus.subscribe("agent.stalled/*", interceptor.on_feedback_event)
562
+ bus.subscribe("validation.failed/*", interceptor.on_feedback_event)
563
+ ```
564
+ """
565
+ # Detection is handled in pre_invoke via detector
566
+ # This is for external feedback that triggers state updates
567
+ pass
@@ -0,0 +1,60 @@
1
+ """
2
+ Budget Interceptor
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+ from loom.kernel.base_interceptor import Interceptor
7
+ from loom.protocol.cloudevents import CloudEvent
8
+
9
+ class BudgetExceededError(Exception):
10
+ pass
11
+
12
+ class BudgetInterceptor(Interceptor):
13
+ """
14
+ Controls resource usage (tokens/cost) per agent/node.
15
+ """
16
+
17
+ def __init__(self, max_tokens: int = 100000):
18
+ self.max_tokens = max_tokens
19
+ # Usage tracking: {node_id: tokens_used}
20
+ self._usage: Dict[str, int] = {}
21
+
22
+ async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
23
+ # Check if this is a request that consumes budget?
24
+ # Typically we check the SOURCE (Who is asking).
25
+ # If Agent A asks Tool B, Agent A is spending budget?
26
+ # Or if Agent A sends "node.response", it used tokens to generate it.
27
+
28
+ # Policy: Check usage of the SOURCE node.
29
+ # If usage > max, block.
30
+
31
+ node_id = event.source.split("/")[-1]
32
+ current_usage = self._usage.get(node_id, 0)
33
+
34
+ if current_usage >= self.max_tokens:
35
+ raise BudgetExceededError(
36
+ f"Node {node_id} exceeded token budget: {current_usage}/{self.max_tokens}"
37
+ )
38
+
39
+ return event
40
+
41
+ async def post_invoke(self, event: CloudEvent) -> None:
42
+ # Update usage based on event type or result
43
+ # For LLM-based agents, we usually get usage in the "agent.thought" or "node.response".
44
+ # In this demo system, we don't have real token counts from MockLLM.
45
+ # We'll heuristic: 1 char = 1 token for demo.
46
+
47
+ node_id = event.source.split("/")[-1]
48
+
49
+ tokens = 0
50
+ if event.data and isinstance(event.data, dict):
51
+ # If explicit usage field exists
52
+ if "usage" in event.data:
53
+ tokens = event.data["usage"].get("total_tokens", 0)
54
+ else:
55
+ # Heuristic
56
+ content = str(event.data.get("thought", "") or event.data.get("result", "") or "")
57
+ tokens = len(content) // 4 # Approx
58
+
59
+ if tokens > 0:
60
+ self._usage[node_id] = self._usage.get(node_id, 0) + tokens
@@ -0,0 +1,45 @@
1
+ """
2
+ Depth Interceptor
3
+ """
4
+
5
+ from typing import Optional, Any
6
+ from loom.kernel.base_interceptor import Interceptor
7
+ from loom.protocol.cloudevents import CloudEvent
8
+
9
+ class RecursionLimitExceededError(Exception):
10
+ pass
11
+
12
+ class DepthInterceptor(Interceptor):
13
+ """
14
+ Prevents infinite fractal recursion.
15
+ """
16
+
17
+ def __init__(self, max_depth: int = 5):
18
+ self.max_depth = max_depth
19
+
20
+ async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
21
+ # We need to know the current depth.
22
+ # CloudEvents 1.0 doesn't have a standard depth field.
23
+ # We assume it is propagated in extension attribute `depth` or inside `tracestate`.
24
+
25
+ # If it's a new request from User, depth is 0.
26
+ # If it's a sub-request, parent should have incremented it.
27
+
28
+ # But Interceptor is on the Sender side (Dispatcher)?
29
+ # Yes, Dispatcher is shared or per-node.
30
+ # If Dispatcher is centralized (LoomApp), it intercepts ALL events.
31
+
32
+ # We check `event.extensions`.
33
+ current_depth = int(getattr(event, "depth", 0) or 0)
34
+
35
+ if current_depth > self.max_depth:
36
+ raise RecursionLimitExceededError(f"Max recursion depth {self.max_depth} exceeded.")
37
+
38
+ # When an Agent receives an event and sends a NEW event (Tool Call),
39
+ # the Agent is responsible for correct propagation (depth+1).
40
+ # This interceptor essentially Gates it.
41
+
42
+ return event
43
+
44
+ async def post_invoke(self, event: CloudEvent) -> None:
45
+ pass