puffinflow 2.dev0__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 (55) hide show
  1. puffinflow/__init__.py +132 -0
  2. puffinflow/core/__init__.py +110 -0
  3. puffinflow/core/agent/__init__.py +320 -0
  4. puffinflow/core/agent/base.py +1635 -0
  5. puffinflow/core/agent/checkpoint.py +50 -0
  6. puffinflow/core/agent/context.py +521 -0
  7. puffinflow/core/agent/decorators/__init__.py +90 -0
  8. puffinflow/core/agent/decorators/builder.py +454 -0
  9. puffinflow/core/agent/decorators/flexible.py +714 -0
  10. puffinflow/core/agent/decorators/inspection.py +144 -0
  11. puffinflow/core/agent/dependencies.py +57 -0
  12. puffinflow/core/agent/scheduling/__init__.py +21 -0
  13. puffinflow/core/agent/scheduling/builder.py +160 -0
  14. puffinflow/core/agent/scheduling/exceptions.py +35 -0
  15. puffinflow/core/agent/scheduling/inputs.py +137 -0
  16. puffinflow/core/agent/scheduling/parser.py +209 -0
  17. puffinflow/core/agent/scheduling/scheduler.py +413 -0
  18. puffinflow/core/agent/state.py +141 -0
  19. puffinflow/core/config.py +62 -0
  20. puffinflow/core/coordination/__init__.py +137 -0
  21. puffinflow/core/coordination/agent_group.py +359 -0
  22. puffinflow/core/coordination/agent_pool.py +629 -0
  23. puffinflow/core/coordination/agent_team.py +577 -0
  24. puffinflow/core/coordination/coordinator.py +720 -0
  25. puffinflow/core/coordination/deadlock.py +1759 -0
  26. puffinflow/core/coordination/fluent_api.py +421 -0
  27. puffinflow/core/coordination/primitives.py +478 -0
  28. puffinflow/core/coordination/rate_limiter.py +520 -0
  29. puffinflow/core/observability/__init__.py +47 -0
  30. puffinflow/core/observability/agent.py +139 -0
  31. puffinflow/core/observability/alerting.py +73 -0
  32. puffinflow/core/observability/config.py +127 -0
  33. puffinflow/core/observability/context.py +88 -0
  34. puffinflow/core/observability/core.py +147 -0
  35. puffinflow/core/observability/decorators.py +105 -0
  36. puffinflow/core/observability/events.py +71 -0
  37. puffinflow/core/observability/interfaces.py +196 -0
  38. puffinflow/core/observability/metrics.py +137 -0
  39. puffinflow/core/observability/tracing.py +209 -0
  40. puffinflow/core/reliability/__init__.py +27 -0
  41. puffinflow/core/reliability/bulkhead.py +96 -0
  42. puffinflow/core/reliability/circuit_breaker.py +149 -0
  43. puffinflow/core/reliability/leak_detector.py +122 -0
  44. puffinflow/core/resources/__init__.py +77 -0
  45. puffinflow/core/resources/allocation.py +790 -0
  46. puffinflow/core/resources/pool.py +645 -0
  47. puffinflow/core/resources/quotas.py +567 -0
  48. puffinflow/core/resources/requirements.py +217 -0
  49. puffinflow/version.py +21 -0
  50. puffinflow-2.dev0.dist-info/METADATA +334 -0
  51. puffinflow-2.dev0.dist-info/RECORD +55 -0
  52. puffinflow-2.dev0.dist-info/WHEEL +5 -0
  53. puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
  54. puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
  55. puffinflow-2.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,149 @@
1
+ """Lightweight circuit breaker implementation."""
2
+
3
+ import asyncio
4
+ import time
5
+ from collections.abc import AsyncGenerator
6
+ from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Any, Optional
10
+
11
+
12
+ class CircuitState(Enum):
13
+ CLOSED = "closed" # Normal operation
14
+ OPEN = "open" # Failing, blocking requests
15
+ HALF_OPEN = "half_open" # Testing if service recovered
16
+
17
+
18
+ @dataclass
19
+ class CircuitBreakerConfig:
20
+ failure_threshold: int = 5
21
+ recovery_timeout: float = 60.0
22
+ success_threshold: int = 3 # For half-open state
23
+ timeout: float = 30.0
24
+ name: str = "default"
25
+
26
+
27
+ class CircuitBreakerError(Exception):
28
+ """Raised when circuit breaker is open"""
29
+
30
+ pass
31
+
32
+
33
+ class CircuitBreaker:
34
+ """Lightweight circuit breaker for state execution"""
35
+
36
+ def __init__(self, config: CircuitBreakerConfig):
37
+ self.config = config
38
+ self.state = CircuitState.CLOSED
39
+ self._failure_count = 0
40
+ self._success_count = 0
41
+ self._last_failure_time = 0
42
+ self._lock = asyncio.Lock()
43
+
44
+ @asynccontextmanager
45
+ async def protect(self) -> AsyncGenerator[None, None]:
46
+ """Context manager for protecting code blocks"""
47
+ async with self._lock:
48
+ await self._check_state()
49
+
50
+ if self.state == CircuitState.OPEN:
51
+ raise CircuitBreakerError(
52
+ f"Circuit breaker '{self.config.name}' is OPEN"
53
+ )
54
+
55
+ time.time()
56
+ try:
57
+ yield
58
+ await self._record_success()
59
+ except Exception:
60
+ await self._record_failure()
61
+ raise
62
+
63
+ async def _check_state(self) -> None:
64
+ """Check and update circuit breaker state"""
65
+ now = time.time()
66
+
67
+ if (
68
+ self.state == CircuitState.OPEN
69
+ and now - self._last_failure_time >= self.config.recovery_timeout
70
+ ):
71
+ self.state = CircuitState.HALF_OPEN
72
+ self._success_count = 0
73
+
74
+ elif self.state == CircuitState.HALF_OPEN:
75
+ if self._success_count >= self.config.success_threshold:
76
+ self.state = CircuitState.CLOSED
77
+ self._failure_count = 0
78
+
79
+ async def _record_success(self) -> None:
80
+ """Record successful execution"""
81
+ async with self._lock:
82
+ if self.state == CircuitState.HALF_OPEN:
83
+ self._success_count += 1
84
+ elif self.state == CircuitState.CLOSED:
85
+ self._failure_count = max(
86
+ 0, self._failure_count - 1
87
+ ) # Gradually recover
88
+
89
+ async def _record_failure(self) -> None:
90
+ """Record failed execution"""
91
+ async with self._lock:
92
+ now = time.time()
93
+ self._failure_count += 1
94
+ self._last_failure_time = int(now)
95
+ self._success_count = 0
96
+
97
+ if (
98
+ self.state == CircuitState.CLOSED
99
+ and self._failure_count >= self.config.failure_threshold
100
+ ) or self.state == CircuitState.HALF_OPEN:
101
+ self.state = CircuitState.OPEN
102
+
103
+ def get_metrics(self) -> dict[str, Any]:
104
+ """Get current metrics"""
105
+ return {
106
+ "name": self.config.name,
107
+ "state": self.state.value,
108
+ "failure_count": self._failure_count,
109
+ "success_count": self._success_count,
110
+ "last_failure_time": self._last_failure_time,
111
+ }
112
+
113
+ async def force_open(self) -> None:
114
+ """Manually open the circuit"""
115
+ async with self._lock:
116
+ self.state = CircuitState.OPEN
117
+ self._last_failure_time = int(time.time())
118
+
119
+ async def force_close(self) -> None:
120
+ """Manually close the circuit"""
121
+ async with self._lock:
122
+ self.state = CircuitState.CLOSED
123
+ self._failure_count = 0
124
+
125
+
126
+ # Global circuit breaker registry
127
+ class CircuitBreakerRegistry:
128
+ """Simple registry for circuit breakers"""
129
+
130
+ def __init__(self) -> None:
131
+ self._breakers: dict[str, CircuitBreaker] = {}
132
+
133
+ def get_or_create(
134
+ self, name: str, config: Optional[CircuitBreakerConfig] = None
135
+ ) -> CircuitBreaker:
136
+ """Get existing or create new circuit breaker"""
137
+ if name not in self._breakers:
138
+ if config is None:
139
+ config = CircuitBreakerConfig(name=name)
140
+ self._breakers[name] = CircuitBreaker(config)
141
+ return self._breakers[name]
142
+
143
+ def get_all_metrics(self) -> dict[str, dict[str, Any]]:
144
+ """Get metrics for all circuit breakers"""
145
+ return {name: breaker.get_metrics() for name, breaker in self._breakers.items()}
146
+
147
+
148
+ # Global registry instance
149
+ circuit_registry = CircuitBreakerRegistry()
@@ -0,0 +1,122 @@
1
+ """Resource leak detection."""
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Any, Optional
6
+
7
+
8
+ @dataclass
9
+ class ResourceLeak:
10
+ state_name: str
11
+ agent_name: str
12
+ resources: dict[str, float]
13
+ allocated_at: float
14
+ held_for_seconds: float
15
+ leak_threshold_seconds: float
16
+
17
+
18
+ @dataclass
19
+ class ResourceAllocation:
20
+ state_name: str
21
+ agent_name: str
22
+ resources: dict[str, float]
23
+ allocated_at: float
24
+
25
+
26
+ class ResourceLeakDetector:
27
+ """Detect and handle resource leaks"""
28
+
29
+ def __init__(self, leak_threshold_seconds: float = 300.0): # 5 minutes default
30
+ self.leak_threshold = leak_threshold_seconds
31
+ self.allocations: dict[str, ResourceAllocation] = {}
32
+ self.detected_leaks: list[ResourceLeak] = []
33
+ self._max_leaks_history = 100
34
+
35
+ def track_allocation(
36
+ self, state_name: str, agent_name: str, resources: dict[str, float]
37
+ ) -> None:
38
+ """Track resource allocation"""
39
+ key = f"{agent_name}:{state_name}"
40
+ self.allocations[key] = ResourceAllocation(
41
+ state_name=state_name,
42
+ agent_name=agent_name,
43
+ resources=resources.copy(),
44
+ allocated_at=time.time(),
45
+ )
46
+
47
+ def track_release(self, state_name: str, agent_name: str) -> None:
48
+ """Track resource release"""
49
+ key = f"{agent_name}:{state_name}"
50
+ if key in self.allocations:
51
+ del self.allocations[key]
52
+
53
+ def detect_leaks(self) -> list[ResourceLeak]:
54
+ """Find resources held too long"""
55
+ current_leaks = []
56
+ now = time.time()
57
+
58
+ for _key, allocation in self.allocations.items():
59
+ held_for = now - allocation.allocated_at
60
+
61
+ if held_for > self.leak_threshold:
62
+ leak = ResourceLeak(
63
+ state_name=allocation.state_name,
64
+ agent_name=allocation.agent_name,
65
+ resources=allocation.resources.copy(),
66
+ allocated_at=allocation.allocated_at,
67
+ held_for_seconds=held_for,
68
+ leak_threshold_seconds=self.leak_threshold,
69
+ )
70
+ current_leaks.append(leak)
71
+
72
+ # Add to history if not already there
73
+ if not any(
74
+ leak_item.state_name == leak.state_name
75
+ and leak_item.agent_name == leak.agent_name
76
+ and abs(leak_item.allocated_at - leak.allocated_at) < 1.0
77
+ for leak_item in self.detected_leaks
78
+ ):
79
+ self.detected_leaks.append(leak)
80
+
81
+ # Trim history
82
+ if len(self.detected_leaks) > self._max_leaks_history:
83
+ self.detected_leaks = self.detected_leaks[-self._max_leaks_history :]
84
+
85
+ return current_leaks
86
+
87
+ def get_metrics(self) -> dict[str, Any]:
88
+ """Get leak detection metrics"""
89
+ current_leaks = self.detect_leaks()
90
+
91
+ return {
92
+ "total_allocations": len(self.allocations),
93
+ "current_leaks": len(current_leaks),
94
+ "total_detected_leaks": len(self.detected_leaks),
95
+ "leak_threshold_seconds": self.leak_threshold,
96
+ "oldest_allocation_age": self._get_oldest_allocation_age(),
97
+ "leaks_by_agent": self._group_leaks_by_agent(current_leaks),
98
+ }
99
+
100
+ def _get_oldest_allocation_age(self) -> Optional[float]:
101
+ """Get age of oldest allocation"""
102
+ if not self.allocations:
103
+ return None
104
+
105
+ now = time.time()
106
+ oldest = min(alloc.allocated_at for alloc in self.allocations.values())
107
+ return now - oldest
108
+
109
+ def _group_leaks_by_agent(self, leaks: list[ResourceLeak]) -> dict[str, int]:
110
+ """Group leaks by agent"""
111
+ groups: dict[str, int] = {}
112
+ for leak in leaks:
113
+ groups[leak.agent_name] = groups.get(leak.agent_name, 0) + 1
114
+ return groups
115
+
116
+ def clear_leak_history(self) -> None:
117
+ """Clear leak detection history"""
118
+ self.detected_leaks.clear()
119
+
120
+
121
+ # Global leak detector instance
122
+ leak_detector = ResourceLeakDetector()
@@ -0,0 +1,77 @@
1
+ """Resource management module for workflow orchestrator."""
2
+
3
+ # Import submodules for import path tests
4
+ from . import allocation, pool, quotas, requirements
5
+ from .allocation import (
6
+ AllocationRequest,
7
+ AllocationResult,
8
+ AllocationStrategy,
9
+ BestFitAllocator,
10
+ FairShareAllocator,
11
+ FirstFitAllocator,
12
+ PriorityAllocator,
13
+ ResourceAllocator,
14
+ WorstFitAllocator,
15
+ )
16
+ from .pool import (
17
+ ResourceAllocationError,
18
+ ResourceOverflowError,
19
+ ResourcePool,
20
+ ResourceQuotaExceededError,
21
+ ResourceUsageStats,
22
+ )
23
+ from .quotas import (
24
+ QuotaExceededError,
25
+ QuotaLimit,
26
+ QuotaManager,
27
+ QuotaMetrics,
28
+ QuotaPolicy,
29
+ QuotaScope,
30
+ )
31
+ from .requirements import (
32
+ ResourceRequirements,
33
+ ResourceType,
34
+ )
35
+
36
+ __all__ = [
37
+ "AllocationRequest",
38
+ "AllocationResult",
39
+ # Allocation
40
+ "AllocationStrategy",
41
+ "BestFitAllocator",
42
+ "FairShareAllocator",
43
+ "FirstFitAllocator",
44
+ "PriorityAllocator",
45
+ "QuotaExceededError",
46
+ "QuotaLimit",
47
+ # Quotas
48
+ "QuotaManager",
49
+ "QuotaMetrics",
50
+ "QuotaPolicy",
51
+ "QuotaScope",
52
+ "ResourceAllocationError",
53
+ "ResourceAllocator",
54
+ "ResourceOverflowError",
55
+ # Pool
56
+ "ResourcePool",
57
+ "ResourceQuotaExceededError",
58
+ "ResourceRequirements",
59
+ # Requirements
60
+ "ResourceType",
61
+ "ResourceUsageStats",
62
+ "WorstFitAllocator",
63
+ "allocation",
64
+ # Submodules
65
+ "pool",
66
+ "quotas",
67
+ "requirements",
68
+ ]
69
+
70
+ # Clean up module namespace
71
+ import sys as _sys
72
+
73
+ _current_module = _sys.modules[__name__]
74
+ for _attr_name in dir(_current_module):
75
+ if not _attr_name.startswith("_") and _attr_name not in __all__:
76
+ delattr(_current_module, _attr_name)
77
+ del _sys, _current_module, _attr_name