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.

Files changed (91) hide show
  1. flock/__init__.py +1 -1
  2. flock/agent/__init__.py +30 -0
  3. flock/agent/builder_helpers.py +192 -0
  4. flock/agent/builder_validator.py +169 -0
  5. flock/agent/component_lifecycle.py +325 -0
  6. flock/agent/context_resolver.py +141 -0
  7. flock/agent/mcp_integration.py +212 -0
  8. flock/agent/output_processor.py +304 -0
  9. flock/api/__init__.py +20 -0
  10. flock/api/models.py +283 -0
  11. flock/{service.py → api/service.py} +121 -63
  12. flock/cli.py +2 -2
  13. flock/components/__init__.py +41 -0
  14. flock/components/agent/__init__.py +22 -0
  15. flock/{components.py → components/agent/base.py} +4 -3
  16. flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
  17. flock/components/orchestrator/__init__.py +22 -0
  18. flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
  19. flock/components/orchestrator/circuit_breaker.py +95 -0
  20. flock/components/orchestrator/collection.py +143 -0
  21. flock/components/orchestrator/deduplication.py +78 -0
  22. flock/core/__init__.py +30 -0
  23. flock/core/agent.py +953 -0
  24. flock/{artifacts.py → core/artifacts.py} +1 -1
  25. flock/{context_provider.py → core/context_provider.py} +3 -3
  26. flock/core/orchestrator.py +1102 -0
  27. flock/{store.py → core/store.py} +99 -454
  28. flock/{subscription.py → core/subscription.py} +1 -1
  29. flock/dashboard/collector.py +5 -5
  30. flock/dashboard/graph_builder.py +7 -7
  31. flock/dashboard/routes/__init__.py +21 -0
  32. flock/dashboard/routes/control.py +327 -0
  33. flock/dashboard/routes/helpers.py +340 -0
  34. flock/dashboard/routes/themes.py +76 -0
  35. flock/dashboard/routes/traces.py +521 -0
  36. flock/dashboard/routes/websocket.py +108 -0
  37. flock/dashboard/service.py +44 -1294
  38. flock/engines/dspy/__init__.py +20 -0
  39. flock/engines/dspy/artifact_materializer.py +216 -0
  40. flock/engines/dspy/signature_builder.py +474 -0
  41. flock/engines/dspy/streaming_executor.py +858 -0
  42. flock/engines/dspy_engine.py +45 -1330
  43. flock/engines/examples/simple_batch_engine.py +2 -2
  44. flock/examples.py +7 -7
  45. flock/logging/logging.py +1 -16
  46. flock/models/__init__.py +10 -0
  47. flock/models/system_artifacts.py +33 -0
  48. flock/orchestrator/__init__.py +45 -0
  49. flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
  50. flock/orchestrator/artifact_manager.py +168 -0
  51. flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
  52. flock/orchestrator/component_runner.py +389 -0
  53. flock/orchestrator/context_builder.py +167 -0
  54. flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
  55. flock/orchestrator/event_emitter.py +167 -0
  56. flock/orchestrator/initialization.py +184 -0
  57. flock/orchestrator/lifecycle_manager.py +226 -0
  58. flock/orchestrator/mcp_manager.py +202 -0
  59. flock/orchestrator/scheduler.py +189 -0
  60. flock/orchestrator/server_manager.py +234 -0
  61. flock/orchestrator/tracing.py +147 -0
  62. flock/storage/__init__.py +10 -0
  63. flock/storage/artifact_aggregator.py +158 -0
  64. flock/storage/in_memory/__init__.py +6 -0
  65. flock/storage/in_memory/artifact_filter.py +114 -0
  66. flock/storage/in_memory/history_aggregator.py +115 -0
  67. flock/storage/sqlite/__init__.py +10 -0
  68. flock/storage/sqlite/agent_history_queries.py +154 -0
  69. flock/storage/sqlite/consumption_loader.py +100 -0
  70. flock/storage/sqlite/query_builder.py +112 -0
  71. flock/storage/sqlite/query_params_builder.py +91 -0
  72. flock/storage/sqlite/schema_manager.py +168 -0
  73. flock/storage/sqlite/summary_queries.py +194 -0
  74. flock/utils/__init__.py +14 -0
  75. flock/utils/async_utils.py +67 -0
  76. flock/{runtime.py → utils/runtime.py} +3 -3
  77. flock/utils/time_utils.py +53 -0
  78. flock/utils/type_resolution.py +38 -0
  79. flock/{utilities.py → utils/utilities.py} +2 -2
  80. flock/utils/validation.py +57 -0
  81. flock/utils/visibility.py +79 -0
  82. flock/utils/visibility_utils.py +134 -0
  83. {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/METADATA +69 -61
  84. {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -31
  85. flock/agent.py +0 -1578
  86. flock/orchestrator.py +0 -1746
  87. /flock/{visibility.py → core/visibility.py} +0 -0
  88. /flock/{helper → utils}/cli_helper.py +0 -0
  89. {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
  90. {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
  91. {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"]