flock-core 0.5.10__py3-none-any.whl → 0.5.20__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 flock-core might be problematic. Click here for more details.
- flock/__init__.py +1 -1
- flock/agent/__init__.py +30 -0
- flock/agent/builder_helpers.py +192 -0
- flock/agent/builder_validator.py +169 -0
- flock/agent/component_lifecycle.py +325 -0
- flock/agent/context_resolver.py +141 -0
- flock/agent/mcp_integration.py +212 -0
- flock/agent/output_processor.py +304 -0
- flock/api/__init__.py +20 -0
- flock/api/models.py +283 -0
- flock/{service.py → api/service.py} +121 -63
- flock/cli.py +2 -2
- flock/components/__init__.py +41 -0
- flock/components/agent/__init__.py +22 -0
- flock/{components.py → components/agent/base.py} +4 -3
- flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
- flock/components/orchestrator/__init__.py +22 -0
- flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
- flock/components/orchestrator/circuit_breaker.py +95 -0
- flock/components/orchestrator/collection.py +143 -0
- flock/components/orchestrator/deduplication.py +78 -0
- flock/core/__init__.py +30 -0
- flock/core/agent.py +953 -0
- flock/{artifacts.py → core/artifacts.py} +1 -1
- flock/{context_provider.py → core/context_provider.py} +3 -3
- flock/core/orchestrator.py +1102 -0
- flock/{store.py → core/store.py} +99 -454
- flock/{subscription.py → core/subscription.py} +1 -1
- flock/dashboard/collector.py +5 -5
- flock/dashboard/graph_builder.py +7 -7
- flock/dashboard/routes/__init__.py +21 -0
- flock/dashboard/routes/control.py +327 -0
- flock/dashboard/routes/helpers.py +340 -0
- flock/dashboard/routes/themes.py +76 -0
- flock/dashboard/routes/traces.py +521 -0
- flock/dashboard/routes/websocket.py +108 -0
- flock/dashboard/service.py +44 -1294
- flock/engines/dspy/__init__.py +20 -0
- flock/engines/dspy/artifact_materializer.py +216 -0
- flock/engines/dspy/signature_builder.py +474 -0
- flock/engines/dspy/streaming_executor.py +858 -0
- flock/engines/dspy_engine.py +45 -1330
- flock/engines/examples/simple_batch_engine.py +2 -2
- flock/examples.py +7 -7
- flock/logging/logging.py +1 -16
- flock/models/__init__.py +10 -0
- flock/models/system_artifacts.py +33 -0
- flock/orchestrator/__init__.py +45 -0
- flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
- flock/orchestrator/artifact_manager.py +168 -0
- flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
- flock/orchestrator/component_runner.py +389 -0
- flock/orchestrator/context_builder.py +167 -0
- flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
- flock/orchestrator/event_emitter.py +167 -0
- flock/orchestrator/initialization.py +184 -0
- flock/orchestrator/lifecycle_manager.py +226 -0
- flock/orchestrator/mcp_manager.py +202 -0
- flock/orchestrator/scheduler.py +189 -0
- flock/orchestrator/server_manager.py +234 -0
- flock/orchestrator/tracing.py +147 -0
- flock/storage/__init__.py +10 -0
- flock/storage/artifact_aggregator.py +158 -0
- flock/storage/in_memory/__init__.py +6 -0
- flock/storage/in_memory/artifact_filter.py +114 -0
- flock/storage/in_memory/history_aggregator.py +115 -0
- flock/storage/sqlite/__init__.py +10 -0
- flock/storage/sqlite/agent_history_queries.py +154 -0
- flock/storage/sqlite/consumption_loader.py +100 -0
- flock/storage/sqlite/query_builder.py +112 -0
- flock/storage/sqlite/query_params_builder.py +91 -0
- flock/storage/sqlite/schema_manager.py +168 -0
- flock/storage/sqlite/summary_queries.py +194 -0
- flock/utils/__init__.py +14 -0
- flock/utils/async_utils.py +67 -0
- flock/{runtime.py → utils/runtime.py} +3 -3
- flock/utils/time_utils.py +53 -0
- flock/utils/type_resolution.py +38 -0
- flock/{utilities.py → utils/utilities.py} +2 -2
- flock/utils/validation.py +57 -0
- flock/utils/visibility.py +79 -0
- flock/utils/visibility_utils.py +134 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/METADATA +69 -61
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -31
- flock/agent.py +0 -1578
- flock/orchestrator.py +0 -1746
- /flock/{visibility.py → core/visibility.py} +0 -0
- /flock/{helper → utils}/cli_helper.py +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Event emission for real-time dashboard updates.
|
|
2
|
+
|
|
3
|
+
Phase 5A: Extracted from orchestrator.py to reduce coupling to dashboard.
|
|
4
|
+
|
|
5
|
+
This module handles WebSocket event emission for dashboard visualization of
|
|
6
|
+
batch and correlation logic operations. Separating this code reduces the
|
|
7
|
+
orchestrator's dependency on dashboard-specific components.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from flock.core.artifacts import Artifact
|
|
17
|
+
from flock.core.subscription import Subscription
|
|
18
|
+
from flock.orchestrator.batch_accumulator import BatchEngine
|
|
19
|
+
from flock.orchestrator.correlation_engine import CorrelationEngine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EventEmitter:
|
|
23
|
+
"""Manages WebSocket event emission for dashboard updates.
|
|
24
|
+
|
|
25
|
+
This module is responsible for broadcasting real-time events about
|
|
26
|
+
batch accumulation and correlation group status to connected dashboard
|
|
27
|
+
clients via WebSocket.
|
|
28
|
+
|
|
29
|
+
Phase 5A: Extracted to reduce orchestrator coupling to dashboard.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, websocket_manager: Any | None = None):
|
|
33
|
+
"""Initialize EventEmitter with WebSocket manager.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
websocket_manager: WebSocket manager instance for broadcasting events.
|
|
37
|
+
If None, event emission is disabled (dashboard not active).
|
|
38
|
+
"""
|
|
39
|
+
self._websocket_manager = websocket_manager
|
|
40
|
+
|
|
41
|
+
def set_websocket_manager(self, websocket_manager: Any | None) -> None:
|
|
42
|
+
"""Update the WebSocket manager (called when dashboard is enabled).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
websocket_manager: WebSocket manager instance for broadcasting
|
|
46
|
+
"""
|
|
47
|
+
self._websocket_manager = websocket_manager
|
|
48
|
+
|
|
49
|
+
async def emit_correlation_updated(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
correlation_engine: CorrelationEngine,
|
|
53
|
+
agent_name: str,
|
|
54
|
+
subscription_index: int,
|
|
55
|
+
artifact: Artifact,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Emit CorrelationGroupUpdatedEvent for real-time dashboard updates.
|
|
58
|
+
|
|
59
|
+
Called when an artifact is added to a correlation group that is not yet complete.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
correlation_engine: CorrelationEngine instance with current state
|
|
63
|
+
agent_name: Name of the agent with the JoinSpec subscription
|
|
64
|
+
subscription_index: Index of the subscription in the agent's subscriptions list
|
|
65
|
+
artifact: The artifact that triggered this update
|
|
66
|
+
"""
|
|
67
|
+
# Only emit if dashboard is enabled
|
|
68
|
+
if self._websocket_manager is None:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Import _get_correlation_groups helper from dashboard service
|
|
72
|
+
from flock.dashboard.routes.helpers import _get_correlation_groups
|
|
73
|
+
|
|
74
|
+
# Get current correlation groups state from engine
|
|
75
|
+
groups = _get_correlation_groups(
|
|
76
|
+
correlation_engine, agent_name, subscription_index
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not groups:
|
|
80
|
+
return # No groups to report (shouldn't happen, but defensive)
|
|
81
|
+
|
|
82
|
+
# Find the group that was just updated (match by last updated time or artifact ID)
|
|
83
|
+
# For now, we'll emit an event for the FIRST group that's still waiting
|
|
84
|
+
# In practice, the artifact we just added should be in one of these groups
|
|
85
|
+
for group_state in groups:
|
|
86
|
+
if not group_state["is_complete"]:
|
|
87
|
+
# Import CorrelationGroupUpdatedEvent
|
|
88
|
+
from flock.dashboard.events import CorrelationGroupUpdatedEvent
|
|
89
|
+
|
|
90
|
+
# Build and emit event
|
|
91
|
+
event = CorrelationGroupUpdatedEvent(
|
|
92
|
+
agent_name=agent_name,
|
|
93
|
+
subscription_index=subscription_index,
|
|
94
|
+
correlation_key=group_state["correlation_key"],
|
|
95
|
+
collected_types=group_state["collected_types"],
|
|
96
|
+
required_types=group_state["required_types"],
|
|
97
|
+
waiting_for=group_state["waiting_for"],
|
|
98
|
+
elapsed_seconds=group_state["elapsed_seconds"],
|
|
99
|
+
expires_in_seconds=group_state["expires_in_seconds"],
|
|
100
|
+
expires_in_artifacts=group_state["expires_in_artifacts"],
|
|
101
|
+
artifact_id=str(artifact.id),
|
|
102
|
+
artifact_type=artifact.type,
|
|
103
|
+
is_complete=group_state["is_complete"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Broadcast via WebSocket
|
|
107
|
+
await self._websocket_manager.broadcast(event)
|
|
108
|
+
break # Only emit one event per artifact addition
|
|
109
|
+
|
|
110
|
+
async def emit_batch_item_added(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
batch_engine: BatchEngine,
|
|
114
|
+
agent_name: str,
|
|
115
|
+
subscription_index: int,
|
|
116
|
+
subscription: Subscription,
|
|
117
|
+
artifact: Artifact,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Emit BatchItemAddedEvent for real-time dashboard updates.
|
|
120
|
+
|
|
121
|
+
Called when an artifact is added to a batch that hasn't reached flush threshold.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
batch_engine: BatchEngine instance with current state
|
|
125
|
+
agent_name: Name of the agent with the BatchSpec subscription
|
|
126
|
+
subscription_index: Index of the subscription in the agent's subscriptions list
|
|
127
|
+
subscription: The subscription with BatchSpec configuration
|
|
128
|
+
artifact: The artifact that triggered this update
|
|
129
|
+
"""
|
|
130
|
+
# Only emit if dashboard is enabled
|
|
131
|
+
if self._websocket_manager is None:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# Import _get_batch_state helper from dashboard service
|
|
135
|
+
from flock.dashboard.routes.helpers import _get_batch_state
|
|
136
|
+
|
|
137
|
+
# Get current batch state from engine
|
|
138
|
+
batch_state = _get_batch_state(
|
|
139
|
+
batch_engine, agent_name, subscription_index, subscription.batch
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if not batch_state:
|
|
143
|
+
return # No batch to report (shouldn't happen, but defensive)
|
|
144
|
+
|
|
145
|
+
# Import BatchItemAddedEvent
|
|
146
|
+
from flock.dashboard.events import BatchItemAddedEvent
|
|
147
|
+
|
|
148
|
+
# Build and emit event
|
|
149
|
+
event = BatchItemAddedEvent(
|
|
150
|
+
agent_name=agent_name,
|
|
151
|
+
subscription_index=subscription_index,
|
|
152
|
+
items_collected=batch_state["items_collected"],
|
|
153
|
+
items_target=batch_state.get("items_target"),
|
|
154
|
+
items_remaining=batch_state.get("items_remaining"),
|
|
155
|
+
elapsed_seconds=batch_state["elapsed_seconds"],
|
|
156
|
+
timeout_seconds=batch_state.get("timeout_seconds"),
|
|
157
|
+
timeout_remaining_seconds=batch_state.get("timeout_remaining_seconds"),
|
|
158
|
+
will_flush=batch_state["will_flush"],
|
|
159
|
+
artifact_id=str(artifact.id),
|
|
160
|
+
artifact_type=artifact.type,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Broadcast via WebSocket
|
|
164
|
+
await self._websocket_manager.broadcast(event)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["EventEmitter"]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Orchestrator initialization helper.
|
|
2
|
+
|
|
3
|
+
Handles component setup and state initialization.
|
|
4
|
+
Extracted from orchestrator.py to reduce __init__ complexity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from flock.core.store import InMemoryBlackboardStore
|
|
15
|
+
from flock.orchestrator.artifact_collector import ArtifactCollector
|
|
16
|
+
from flock.orchestrator.batch_accumulator import BatchEngine
|
|
17
|
+
from flock.orchestrator.component_runner import ComponentRunner
|
|
18
|
+
from flock.orchestrator.context_builder import ContextBuilder
|
|
19
|
+
from flock.orchestrator.correlation_engine import CorrelationEngine
|
|
20
|
+
from flock.orchestrator.event_emitter import EventEmitter
|
|
21
|
+
from flock.orchestrator.lifecycle_manager import LifecycleManager
|
|
22
|
+
from flock.orchestrator.mcp_manager import MCPManager
|
|
23
|
+
from flock.orchestrator.tracing import TracingManager
|
|
24
|
+
from flock.utils.cli_helper import init_console
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from flock.components.orchestrator import OrchestratorComponent
|
|
29
|
+
from flock.core.store import BlackboardStore
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OrchestratorInitializer:
|
|
33
|
+
"""Handles orchestrator component initialization.
|
|
34
|
+
|
|
35
|
+
Centralizes the complex setup logic from Flock.__init__ into
|
|
36
|
+
a focused helper class.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def initialize_components(
|
|
41
|
+
store: BlackboardStore | None,
|
|
42
|
+
context_provider: Any,
|
|
43
|
+
max_agent_iterations: int,
|
|
44
|
+
logger: logging.Logger,
|
|
45
|
+
model: str | None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Initialize all orchestrator components and state.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
store: Blackboard storage backend (or None for default)
|
|
51
|
+
context_provider: Global context provider for agents
|
|
52
|
+
max_agent_iterations: Circuit breaker limit
|
|
53
|
+
logger: Logger instance
|
|
54
|
+
model: Default LLM model
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary of initialized components and state
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
>>> logger = logging.getLogger(__name__)
|
|
61
|
+
>>> components = OrchestratorInitializer.initialize_components(
|
|
62
|
+
... store=None,
|
|
63
|
+
... context_provider=None,
|
|
64
|
+
... max_agent_iterations=1000,
|
|
65
|
+
... logger=logger,
|
|
66
|
+
... model="openai/gpt-4.1",
|
|
67
|
+
... )
|
|
68
|
+
>>> orchestrator.store = components["store"]
|
|
69
|
+
>>> orchestrator._scheduler = components["scheduler"]
|
|
70
|
+
"""
|
|
71
|
+
# Initialize console (with error handling for encoding issues)
|
|
72
|
+
try:
|
|
73
|
+
init_console(clear_screen=True, show_banner=True, model=model)
|
|
74
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
75
|
+
# Skip banner on Windows consoles with encoding issues (e.g., tests, CI)
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Basic state
|
|
79
|
+
resolved_store = store or InMemoryBlackboardStore()
|
|
80
|
+
agents: dict[str, Any] = {}
|
|
81
|
+
lock = asyncio.Lock()
|
|
82
|
+
metrics: dict[str, float] = {"artifacts_published": 0, "agent_runs": 0}
|
|
83
|
+
agent_iteration_count: dict[str, int] = {}
|
|
84
|
+
|
|
85
|
+
# Engines
|
|
86
|
+
artifact_collector = ArtifactCollector()
|
|
87
|
+
correlation_engine = CorrelationEngine()
|
|
88
|
+
batch_engine = BatchEngine()
|
|
89
|
+
|
|
90
|
+
# Phase 5A modules
|
|
91
|
+
context_builder = ContextBuilder(
|
|
92
|
+
store=resolved_store,
|
|
93
|
+
default_context_provider=context_provider,
|
|
94
|
+
)
|
|
95
|
+
event_emitter = EventEmitter(websocket_manager=None)
|
|
96
|
+
lifecycle_manager = LifecycleManager(
|
|
97
|
+
correlation_engine=correlation_engine,
|
|
98
|
+
batch_engine=batch_engine,
|
|
99
|
+
cleanup_interval=0.1,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Phase 3 modules
|
|
103
|
+
mcp_manager_instance = MCPManager()
|
|
104
|
+
tracing_manager = TracingManager()
|
|
105
|
+
|
|
106
|
+
# Auto-workflow tracing feature flag
|
|
107
|
+
auto_workflow_enabled = os.getenv(
|
|
108
|
+
"FLOCK_AUTO_WORKFLOW_TRACE", "false"
|
|
109
|
+
).lower() in {
|
|
110
|
+
"true",
|
|
111
|
+
"1",
|
|
112
|
+
"yes",
|
|
113
|
+
"on",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
# Basic state
|
|
118
|
+
"store": resolved_store,
|
|
119
|
+
"agents": agents,
|
|
120
|
+
"lock": lock,
|
|
121
|
+
"metrics": metrics,
|
|
122
|
+
"agent_iteration_count": agent_iteration_count,
|
|
123
|
+
# Engines
|
|
124
|
+
"artifact_collector": artifact_collector,
|
|
125
|
+
"correlation_engine": correlation_engine,
|
|
126
|
+
"batch_engine": batch_engine,
|
|
127
|
+
# Phase 5A modules
|
|
128
|
+
"context_builder": context_builder,
|
|
129
|
+
"event_emitter": event_emitter,
|
|
130
|
+
"lifecycle_manager": lifecycle_manager,
|
|
131
|
+
# Phase 3 modules
|
|
132
|
+
"mcp_manager_instance": mcp_manager_instance,
|
|
133
|
+
"tracing_manager": tracing_manager,
|
|
134
|
+
# Feature flags
|
|
135
|
+
"auto_workflow_enabled": auto_workflow_enabled,
|
|
136
|
+
# Placeholders
|
|
137
|
+
"websocket_manager": None,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def initialize_components_and_runner(
|
|
142
|
+
components_list: list[OrchestratorComponent],
|
|
143
|
+
max_agent_iterations: int,
|
|
144
|
+
logger: logging.Logger,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Initialize built-in components and create ComponentRunner.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
components_list: List to populate with components
|
|
150
|
+
max_agent_iterations: Circuit breaker limit
|
|
151
|
+
logger: Logger instance
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dictionary with component_runner and updated components list
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
>>> components = []
|
|
158
|
+
>>> result = OrchestratorInitializer.initialize_components_and_runner(
|
|
159
|
+
... components, max_agent_iterations=1000, logger=logger
|
|
160
|
+
... )
|
|
161
|
+
>>> component_runner = result["component_runner"]
|
|
162
|
+
"""
|
|
163
|
+
from flock.components.orchestrator import (
|
|
164
|
+
BuiltinCollectionComponent,
|
|
165
|
+
CircuitBreakerComponent,
|
|
166
|
+
DeduplicationComponent,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Add built-in components
|
|
170
|
+
components_list.append(
|
|
171
|
+
CircuitBreakerComponent(max_iterations=max_agent_iterations)
|
|
172
|
+
)
|
|
173
|
+
components_list.append(DeduplicationComponent())
|
|
174
|
+
components_list.append(BuiltinCollectionComponent())
|
|
175
|
+
|
|
176
|
+
# Sort by priority
|
|
177
|
+
components_list.sort(key=lambda c: c.priority)
|
|
178
|
+
|
|
179
|
+
# Create ComponentRunner
|
|
180
|
+
component_runner = ComponentRunner(components_list, logger)
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"component_runner": component_runner,
|
|
184
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Lifecycle management for background tasks and cleanup.
|
|
2
|
+
|
|
3
|
+
Phase 5A: Extracted from orchestrator.py to isolate background task coordination.
|
|
4
|
+
|
|
5
|
+
This module handles background tasks for batch timeouts and correlation cleanup,
|
|
6
|
+
reducing orchestrator complexity and centralizing async task management.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
from asyncio import Task
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from flock.orchestrator.batch_accumulator import BatchEngine
|
|
19
|
+
from flock.orchestrator.correlation_engine import CorrelationEngine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LifecycleManager:
|
|
23
|
+
"""Manages background tasks for batch and correlation lifecycle.
|
|
24
|
+
|
|
25
|
+
This module centralizes all background task management for:
|
|
26
|
+
- Correlation group cleanup (time-based expiry)
|
|
27
|
+
- Batch timeout checking (timeout-based flushing)
|
|
28
|
+
|
|
29
|
+
Phase 5A: Extracted to reduce orchestrator complexity and improve testability.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
correlation_engine: CorrelationEngine,
|
|
36
|
+
batch_engine: BatchEngine,
|
|
37
|
+
cleanup_interval: float = 0.1,
|
|
38
|
+
):
|
|
39
|
+
"""Initialize LifecycleManager with engines and intervals.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
correlation_engine: Engine managing correlation groups
|
|
43
|
+
batch_engine: Engine managing batch accumulation
|
|
44
|
+
cleanup_interval: How often to check for expiry (seconds, default: 0.1)
|
|
45
|
+
"""
|
|
46
|
+
self._correlation_engine = correlation_engine
|
|
47
|
+
self._batch_engine = batch_engine
|
|
48
|
+
self._cleanup_interval = cleanup_interval
|
|
49
|
+
|
|
50
|
+
# Background tasks
|
|
51
|
+
self._correlation_cleanup_task: Task[Any] | None = None
|
|
52
|
+
self._batch_timeout_task: Task[Any] | None = None
|
|
53
|
+
|
|
54
|
+
# Callback for batch timeout flushing (set by orchestrator)
|
|
55
|
+
self._batch_timeout_callback: Any | None = None
|
|
56
|
+
|
|
57
|
+
self._logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
async def start_correlation_cleanup(self) -> None:
|
|
60
|
+
"""Start background correlation cleanup loop if not already running.
|
|
61
|
+
|
|
62
|
+
This ensures expired correlation groups are periodically discarded.
|
|
63
|
+
Called when there are pending correlations during run_until_idle.
|
|
64
|
+
"""
|
|
65
|
+
if (
|
|
66
|
+
self._correlation_cleanup_task is None
|
|
67
|
+
or self._correlation_cleanup_task.done()
|
|
68
|
+
):
|
|
69
|
+
self._correlation_cleanup_task = asyncio.create_task(
|
|
70
|
+
self._correlation_cleanup_loop()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def set_batch_timeout_callback(self, callback: Any) -> None:
|
|
74
|
+
"""Set the callback to invoke when batches timeout.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
callback: Async function to call when timeout checking. Should handle
|
|
78
|
+
flushing expired batches and scheduling agent tasks.
|
|
79
|
+
"""
|
|
80
|
+
self._batch_timeout_callback = callback
|
|
81
|
+
|
|
82
|
+
async def start_batch_timeout_checker(self) -> None:
|
|
83
|
+
"""Start background batch timeout checker loop if not already running.
|
|
84
|
+
|
|
85
|
+
This ensures timeout-expired batches are periodically flushed.
|
|
86
|
+
Called when there are pending batches during run_until_idle.
|
|
87
|
+
"""
|
|
88
|
+
if self._batch_timeout_task is None or self._batch_timeout_task.done():
|
|
89
|
+
self._batch_timeout_task = asyncio.create_task(
|
|
90
|
+
self._batch_timeout_checker_loop()
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def shutdown(self) -> None:
|
|
94
|
+
"""Cancel and cleanup all background tasks.
|
|
95
|
+
|
|
96
|
+
Called during orchestrator shutdown to ensure clean resource cleanup.
|
|
97
|
+
"""
|
|
98
|
+
# Cancel correlation cleanup task if running
|
|
99
|
+
if self._correlation_cleanup_task and not self._correlation_cleanup_task.done():
|
|
100
|
+
self._correlation_cleanup_task.cancel()
|
|
101
|
+
try:
|
|
102
|
+
await self._correlation_cleanup_task
|
|
103
|
+
except asyncio.CancelledError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# Cancel batch timeout checker if running
|
|
107
|
+
if self._batch_timeout_task and not self._batch_timeout_task.done():
|
|
108
|
+
self._batch_timeout_task.cancel()
|
|
109
|
+
try:
|
|
110
|
+
await self._batch_timeout_task
|
|
111
|
+
except asyncio.CancelledError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Background Loops ─────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async def _correlation_cleanup_loop(self) -> None:
|
|
117
|
+
"""Background task that periodically cleans up expired correlation groups.
|
|
118
|
+
|
|
119
|
+
Runs continuously until all correlation groups are cleared or orchestrator shuts down.
|
|
120
|
+
Checks every 100ms for time-based expired correlations and discards them.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
while True:
|
|
124
|
+
await asyncio.sleep(self._cleanup_interval)
|
|
125
|
+
self._cleanup_expired_correlations()
|
|
126
|
+
|
|
127
|
+
# Stop if no correlation groups remain
|
|
128
|
+
if not self._correlation_engine.correlation_groups:
|
|
129
|
+
self._correlation_cleanup_task = None
|
|
130
|
+
break
|
|
131
|
+
except asyncio.CancelledError:
|
|
132
|
+
# Clean shutdown
|
|
133
|
+
self._correlation_cleanup_task = None
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
def _cleanup_expired_correlations(self) -> None:
|
|
137
|
+
"""Clean up all expired correlation groups across all subscriptions.
|
|
138
|
+
|
|
139
|
+
Called periodically by background task to enforce time-based correlation windows.
|
|
140
|
+
Discards incomplete correlations that have exceeded their time window.
|
|
141
|
+
"""
|
|
142
|
+
# Get all active subscription keys
|
|
143
|
+
for agent_name, subscription_index in list(
|
|
144
|
+
self._correlation_engine.correlation_groups.keys()
|
|
145
|
+
):
|
|
146
|
+
self._correlation_engine.cleanup_expired(agent_name, subscription_index)
|
|
147
|
+
|
|
148
|
+
async def _batch_timeout_checker_loop(self) -> None:
|
|
149
|
+
"""Background task that periodically checks for batch timeouts.
|
|
150
|
+
|
|
151
|
+
Runs continuously until all batches are cleared or orchestrator shuts down.
|
|
152
|
+
Checks every 100ms for expired batches and flushes them via callback.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
while True:
|
|
156
|
+
await asyncio.sleep(self._cleanup_interval)
|
|
157
|
+
|
|
158
|
+
# Call the timeout callback to check and flush expired batches
|
|
159
|
+
if self._batch_timeout_callback:
|
|
160
|
+
await self._batch_timeout_callback()
|
|
161
|
+
|
|
162
|
+
# Stop if no batches remain
|
|
163
|
+
if not self._batch_engine.batches:
|
|
164
|
+
self._batch_timeout_task = None
|
|
165
|
+
break
|
|
166
|
+
except asyncio.CancelledError:
|
|
167
|
+
# Clean shutdown
|
|
168
|
+
self._batch_timeout_task = None
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
# Helper Methods ───────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async def check_batch_timeouts(self, orchestrator_callback: Any) -> None:
|
|
174
|
+
"""Check all batches for timeout expiry and invoke callback for expired batches.
|
|
175
|
+
|
|
176
|
+
This method is called periodically by the background timeout checker
|
|
177
|
+
or manually (in tests) to enforce timeout-based batching.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
orchestrator_callback: Async function to call for each expired batch.
|
|
181
|
+
Signature: async def callback(agent_name: str, subscription_index: int,
|
|
182
|
+
artifacts: list[Artifact]) -> None
|
|
183
|
+
"""
|
|
184
|
+
expired_batches = self._batch_engine.check_timeouts()
|
|
185
|
+
|
|
186
|
+
for agent_name, subscription_index in expired_batches:
|
|
187
|
+
# Flush the expired batch
|
|
188
|
+
artifacts = self._batch_engine.flush_batch(agent_name, subscription_index)
|
|
189
|
+
|
|
190
|
+
if artifacts is not None:
|
|
191
|
+
# Invoke orchestrator callback to schedule task
|
|
192
|
+
await orchestrator_callback(agent_name, subscription_index, artifacts)
|
|
193
|
+
|
|
194
|
+
async def flush_all_batches(self, orchestrator_callback: Any) -> None:
|
|
195
|
+
"""Flush all partial batches (for shutdown - ensures zero data loss).
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
orchestrator_callback: Async function to call for each flushed batch.
|
|
199
|
+
Signature: async def callback(agent_name: str, subscription_index: int,
|
|
200
|
+
artifacts: list[Artifact]) -> None
|
|
201
|
+
"""
|
|
202
|
+
all_batches = self._batch_engine.flush_all()
|
|
203
|
+
|
|
204
|
+
for agent_name, subscription_index, artifacts in all_batches:
|
|
205
|
+
# Invoke orchestrator callback to schedule task
|
|
206
|
+
await orchestrator_callback(agent_name, subscription_index, artifacts)
|
|
207
|
+
|
|
208
|
+
# Properties ───────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def has_pending_correlations(self) -> bool:
|
|
212
|
+
"""Check if there are any pending correlation groups."""
|
|
213
|
+
return any(
|
|
214
|
+
groups and any(group.waiting_artifacts for group in groups.values())
|
|
215
|
+
for groups in self._correlation_engine.correlation_groups.values()
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def has_pending_batches(self) -> bool:
|
|
220
|
+
"""Check if there are any pending batches."""
|
|
221
|
+
return any(
|
|
222
|
+
accumulator.artifacts for accumulator in self._batch_engine.batches.values()
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
__all__ = ["LifecycleManager"]
|