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.
- loom/__init__.py +1 -0
- loom/adapters/converters.py +77 -0
- loom/adapters/registry.py +43 -0
- loom/api/factory.py +77 -0
- loom/api/main.py +201 -0
- loom/builtin/__init__.py +3 -0
- loom/builtin/memory/__init__.py +3 -0
- loom/builtin/memory/metabolic.py +96 -0
- loom/builtin/memory/pso.py +41 -0
- loom/builtin/memory/sanitizers.py +39 -0
- loom/builtin/memory/validators.py +55 -0
- loom/config/tool.py +63 -0
- loom/infra/__init__.py +0 -0
- loom/infra/llm.py +44 -0
- loom/infra/logging.py +42 -0
- loom/infra/store.py +39 -0
- loom/infra/transport/memory.py +112 -0
- loom/infra/transport/nats.py +170 -0
- loom/infra/transport/redis.py +161 -0
- loom/interfaces/llm.py +45 -0
- loom/interfaces/memory.py +50 -0
- loom/interfaces/store.py +29 -0
- loom/interfaces/transport.py +35 -0
- loom/kernel/__init__.py +0 -0
- loom/kernel/base_interceptor.py +97 -0
- loom/kernel/bus.py +85 -0
- loom/kernel/dispatcher.py +58 -0
- loom/kernel/interceptors/__init__.py +14 -0
- loom/kernel/interceptors/adaptive.py +567 -0
- loom/kernel/interceptors/budget.py +60 -0
- loom/kernel/interceptors/depth.py +45 -0
- loom/kernel/interceptors/hitl.py +51 -0
- loom/kernel/interceptors/studio.py +129 -0
- loom/kernel/interceptors/timeout.py +27 -0
- loom/kernel/state.py +71 -0
- loom/memory/hierarchical.py +124 -0
- loom/node/__init__.py +0 -0
- loom/node/agent.py +252 -0
- loom/node/base.py +121 -0
- loom/node/crew.py +105 -0
- loom/node/router.py +77 -0
- loom/node/tool.py +50 -0
- loom/protocol/__init__.py +0 -0
- loom/protocol/cloudevents.py +73 -0
- loom/protocol/interfaces.py +164 -0
- loom/protocol/mcp.py +97 -0
- loom/protocol/memory_operations.py +51 -0
- loom/protocol/patch.py +93 -0
- loom_agent-0.3.3.dist-info/LICENSE +204 -0
- loom_agent-0.3.3.dist-info/METADATA +139 -0
- loom_agent-0.3.3.dist-info/RECORD +52 -0
- 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
|