flock-core 0.5.11__py3-none-any.whl → 0.5.21__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 (94) 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 → api/models.py} +0 -2
  11. flock/{service.py → api/service.py} +3 -3
  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/events.py +1 -1
  31. flock/dashboard/graph_builder.py +7 -7
  32. flock/dashboard/routes/__init__.py +21 -0
  33. flock/dashboard/routes/control.py +327 -0
  34. flock/dashboard/routes/helpers.py +340 -0
  35. flock/dashboard/routes/themes.py +76 -0
  36. flock/dashboard/routes/traces.py +521 -0
  37. flock/dashboard/routes/websocket.py +108 -0
  38. flock/dashboard/service.py +43 -1316
  39. flock/engines/dspy/__init__.py +20 -0
  40. flock/engines/dspy/artifact_materializer.py +216 -0
  41. flock/engines/dspy/signature_builder.py +474 -0
  42. flock/engines/dspy/streaming_executor.py +812 -0
  43. flock/engines/dspy_engine.py +45 -1330
  44. flock/engines/examples/simple_batch_engine.py +2 -2
  45. flock/engines/streaming/__init__.py +3 -0
  46. flock/engines/streaming/sinks.py +489 -0
  47. flock/examples.py +7 -7
  48. flock/logging/logging.py +1 -16
  49. flock/models/__init__.py +10 -0
  50. flock/orchestrator/__init__.py +45 -0
  51. flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
  52. flock/orchestrator/artifact_manager.py +168 -0
  53. flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
  54. flock/orchestrator/component_runner.py +389 -0
  55. flock/orchestrator/context_builder.py +167 -0
  56. flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
  57. flock/orchestrator/event_emitter.py +167 -0
  58. flock/orchestrator/initialization.py +184 -0
  59. flock/orchestrator/lifecycle_manager.py +226 -0
  60. flock/orchestrator/mcp_manager.py +202 -0
  61. flock/orchestrator/scheduler.py +189 -0
  62. flock/orchestrator/server_manager.py +234 -0
  63. flock/orchestrator/tracing.py +147 -0
  64. flock/storage/__init__.py +10 -0
  65. flock/storage/artifact_aggregator.py +158 -0
  66. flock/storage/in_memory/__init__.py +6 -0
  67. flock/storage/in_memory/artifact_filter.py +114 -0
  68. flock/storage/in_memory/history_aggregator.py +115 -0
  69. flock/storage/sqlite/__init__.py +10 -0
  70. flock/storage/sqlite/agent_history_queries.py +154 -0
  71. flock/storage/sqlite/consumption_loader.py +100 -0
  72. flock/storage/sqlite/query_builder.py +112 -0
  73. flock/storage/sqlite/query_params_builder.py +91 -0
  74. flock/storage/sqlite/schema_manager.py +168 -0
  75. flock/storage/sqlite/summary_queries.py +194 -0
  76. flock/utils/__init__.py +14 -0
  77. flock/utils/async_utils.py +67 -0
  78. flock/{runtime.py → utils/runtime.py} +3 -3
  79. flock/utils/time_utils.py +53 -0
  80. flock/utils/type_resolution.py +38 -0
  81. flock/{utilities.py → utils/utilities.py} +2 -2
  82. flock/utils/validation.py +57 -0
  83. flock/utils/visibility.py +79 -0
  84. flock/utils/visibility_utils.py +134 -0
  85. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/METADATA +19 -5
  86. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/RECORD +92 -34
  87. flock/agent.py +0 -1578
  88. flock/orchestrator.py +0 -1983
  89. /flock/{visibility.py → core/visibility.py} +0 -0
  90. /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
  91. /flock/{helper → utils}/cli_helper.py +0 -0
  92. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/WHEEL +0 -0
  93. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/entry_points.txt +0 -0
  94. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,168 @@
1
+ """Artifact publishing and persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import TYPE_CHECKING, Any
7
+ from uuid import uuid4
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from flock.core.artifacts import Artifact
12
+ from flock.core.visibility import PublicVisibility, Visibility
13
+ from flock.registry import type_registry
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from flock.core import Flock
18
+ from flock.core.store import BlackboardStore
19
+ from flock.orchestrator import AgentScheduler
20
+
21
+
22
+ class ArtifactManager:
23
+ """Manages artifact publishing and persistence.
24
+
25
+ Responsibilities:
26
+ - Normalize different input types (BaseModel, dict, Artifact)
27
+ - Persist artifacts to store
28
+ - Trigger scheduling after publish
29
+ - Handle batch publishing
30
+ """
31
+
32
+ def __init__(
33
+ self, orchestrator: Flock, store: BlackboardStore, scheduler: AgentScheduler
34
+ ):
35
+ """Initialize artifact manager.
36
+
37
+ Args:
38
+ orchestrator: Flock orchestrator instance
39
+ store: Blackboard store for persistence
40
+ scheduler: Scheduler for triggering agent execution
41
+ """
42
+ self._orchestrator = orchestrator
43
+ self._store = store
44
+ self._scheduler = scheduler
45
+ self._logger = orchestrator._logger
46
+
47
+ async def publish(
48
+ self,
49
+ obj: BaseModel | dict | Artifact,
50
+ *,
51
+ visibility: Visibility | None = None,
52
+ correlation_id: str | None = None,
53
+ partition_key: str | None = None,
54
+ tags: set[str] | None = None,
55
+ is_dashboard: bool = False,
56
+ ) -> Artifact:
57
+ """Publish an artifact to the blackboard (event-driven).
58
+
59
+ All agents with matching subscriptions will be triggered according to
60
+ their filters (type, predicates, visibility, etc).
61
+
62
+ Args:
63
+ obj: Object to publish (BaseModel instance, dict, or Artifact)
64
+ visibility: Access control (defaults to PublicVisibility)
65
+ correlation_id: Optional correlation ID for request tracing
66
+ partition_key: Optional partition key for sharding
67
+ tags: Optional tags for channel-based routing
68
+ is_dashboard: Internal flag for dashboard events
69
+
70
+ Returns:
71
+ The published Artifact
72
+
73
+ Examples:
74
+ >>> # Publish a model instance (recommended)
75
+ >>> task = Task(name="Deploy", priority=5)
76
+ >>> await artifact_manager.publish(task)
77
+
78
+ >>> # Publish with custom visibility
79
+ >>> await artifact_manager.publish(
80
+ ... task, visibility=PrivateVisibility(agents={"admin"})
81
+ ... )
82
+ """
83
+ # Handle different input types
84
+ if isinstance(obj, Artifact):
85
+ # Already an artifact - publish as-is
86
+ artifact = obj
87
+ elif isinstance(obj, BaseModel):
88
+ # BaseModel instance - get type from registry
89
+ type_name = type_registry.name_for(type(obj))
90
+ artifact = Artifact(
91
+ type=type_name,
92
+ payload=obj.model_dump(),
93
+ produced_by="external",
94
+ visibility=visibility or PublicVisibility(),
95
+ correlation_id=correlation_id or uuid4(),
96
+ partition_key=partition_key,
97
+ tags=tags or set(),
98
+ )
99
+ elif isinstance(obj, dict):
100
+ # Dict must have 'type' key
101
+ if "type" not in obj:
102
+ raise ValueError(
103
+ "Dict input must contain 'type' key. "
104
+ "Example: {'type': 'Task', 'name': 'foo', 'priority': 5}"
105
+ )
106
+ # Support both {'type': 'X', 'payload': {...}} and {'type': 'X', ...}
107
+ type_name = obj["type"]
108
+ if "payload" in obj:
109
+ payload = obj["payload"]
110
+ else:
111
+ payload = {k: v for k, v in obj.items() if k != "type"}
112
+
113
+ artifact = Artifact(
114
+ type=type_name,
115
+ payload=payload,
116
+ produced_by="external",
117
+ visibility=visibility or PublicVisibility(),
118
+ correlation_id=correlation_id,
119
+ partition_key=partition_key,
120
+ tags=tags or set(),
121
+ )
122
+ else:
123
+ raise TypeError(
124
+ f"Cannot publish object of type {type(obj).__name__}. "
125
+ "Expected BaseModel, dict, or Artifact."
126
+ )
127
+
128
+ # Persist and schedule matching agents
129
+ await self.persist_and_schedule(artifact)
130
+ return artifact
131
+
132
+ async def publish_many(
133
+ self, objects: Iterable[BaseModel | dict | Artifact], **kwargs: Any
134
+ ) -> list[Artifact]:
135
+ """Publish multiple artifacts at once (event-driven).
136
+
137
+ Args:
138
+ objects: Iterable of objects to publish
139
+ **kwargs: Passed to each publish() call (visibility, tags, etc)
140
+
141
+ Returns:
142
+ List of published Artifacts
143
+
144
+ Example:
145
+ >>> tasks = [
146
+ ... Task(name="Deploy", priority=5),
147
+ ... Task(name="Test", priority=3),
148
+ ... ]
149
+ >>> await artifact_manager.publish_many(tasks, tags={"sprint-3"})
150
+ """
151
+ artifacts = []
152
+ for obj in objects:
153
+ artifact = await self.publish(obj, **kwargs)
154
+ artifacts.append(artifact)
155
+ return artifacts
156
+
157
+ async def persist_and_schedule(self, artifact: Artifact) -> None:
158
+ """Persist artifact to store and trigger scheduling.
159
+
160
+ Args:
161
+ artifact: Artifact to publish
162
+ """
163
+ await self._store.publish(artifact)
164
+ self._orchestrator.metrics["artifacts_published"] += 1
165
+ await self._scheduler.schedule_artifact(artifact)
166
+
167
+
168
+ __all__ = ["ArtifactManager"]
@@ -16,8 +16,8 @@ from typing import TYPE_CHECKING
16
16
 
17
17
 
18
18
  if TYPE_CHECKING:
19
- from flock.artifacts import Artifact
20
- from flock.subscription import BatchSpec, Subscription
19
+ from flock.core.artifacts import Artifact
20
+ from flock.core.subscription import BatchSpec, Subscription
21
21
 
22
22
 
23
23
  class BatchAccumulator:
@@ -0,0 +1,389 @@
1
+ """Component lifecycle hook execution for orchestrator.
2
+
3
+ This module manages the execution of OrchestratorComponent hooks in priority order.
4
+ Components can modify artifacts, control scheduling decisions, and handle collection logic.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from asyncio import Task
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from flock.components.orchestrator import OrchestratorComponent
16
+ from flock.core import Agent
17
+ from flock.core.artifacts import Artifact
18
+ from flock.core.subscription import Subscription
19
+
20
+
21
+ class ComponentRunner:
22
+ """Executes orchestrator component hooks in priority order.
23
+
24
+ This class manages the component lifecycle including initialization,
25
+ artifact processing, scheduling decisions, and shutdown. All hooks
26
+ execute in priority order (lower priority number = earlier execution).
27
+
28
+ Attributes:
29
+ _components: List of orchestrator components (sorted by priority)
30
+ _logger: Logger instance for component execution tracking
31
+ _initialized: Flag to prevent multiple initializations
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ components: list[OrchestratorComponent],
37
+ logger: logging.Logger | None = None,
38
+ ) -> None:
39
+ """Initialize the component runner.
40
+
41
+ Args:
42
+ components: List of orchestrator components (should be pre-sorted by priority)
43
+ logger: Logger instance (defaults to module logger if not provided)
44
+ """
45
+ self._components = components
46
+ self._logger = logger or logging.getLogger(__name__)
47
+ self._initialized = False
48
+
49
+ @property
50
+ def components(self) -> list[OrchestratorComponent]:
51
+ """Get the list of registered components."""
52
+ return self._components
53
+
54
+ @property
55
+ def is_initialized(self) -> bool:
56
+ """Check if components have been initialized."""
57
+ return self._initialized
58
+
59
+ async def run_initialize(self, orchestrator: Any) -> None:
60
+ """Initialize all components in priority order (called once).
61
+
62
+ Executes on_initialize hook for each component. Sets _initialized
63
+ flag to prevent multiple initializations.
64
+
65
+ Args:
66
+ orchestrator: The Flock orchestrator instance
67
+ """
68
+ if self._initialized:
69
+ return
70
+
71
+ self._logger.info(
72
+ f"Initializing {len(self._components)} orchestrator components"
73
+ )
74
+
75
+ for component in self._components:
76
+ comp_name = component.name or component.__class__.__name__
77
+ self._logger.debug(
78
+ f"Initializing component: name={comp_name}, priority={component.priority}"
79
+ )
80
+
81
+ try:
82
+ await component.on_initialize(orchestrator)
83
+ except Exception as e:
84
+ self._logger.exception(
85
+ f"Component initialization failed: name={comp_name}, error={e!s}"
86
+ )
87
+ raise
88
+
89
+ self._initialized = True
90
+ self._logger.info(f"All components initialized: count={len(self._components)}")
91
+
92
+ async def run_artifact_published(
93
+ self, orchestrator: Any, artifact: Artifact
94
+ ) -> Artifact | None:
95
+ """Run on_artifact_published hooks (returns modified artifact or None to block).
96
+
97
+ Components execute in priority order, each receiving the artifact from the
98
+ previous component (chaining). If any component returns None, the artifact
99
+ is blocked and scheduling stops.
100
+
101
+ Args:
102
+ orchestrator: The Flock orchestrator instance
103
+ artifact: The artifact being published
104
+
105
+ Returns:
106
+ Modified artifact or None if blocked by a component
107
+ """
108
+ current_artifact = artifact
109
+
110
+ for component in self._components:
111
+ comp_name = component.name or component.__class__.__name__
112
+ self._logger.debug(
113
+ f"Running on_artifact_published: component={comp_name}, "
114
+ f"artifact_type={current_artifact.type}, artifact_id={current_artifact.id}"
115
+ )
116
+
117
+ try:
118
+ result = await component.on_artifact_published(
119
+ orchestrator, current_artifact
120
+ )
121
+
122
+ if result is None:
123
+ self._logger.info(
124
+ f"Artifact blocked by component: component={comp_name}, "
125
+ f"artifact_type={current_artifact.type}, artifact_id={current_artifact.id}"
126
+ )
127
+ return None
128
+
129
+ current_artifact = result
130
+ except Exception as e:
131
+ self._logger.exception(
132
+ f"Component hook failed: component={comp_name}, "
133
+ f"hook=on_artifact_published, error={e!s}"
134
+ )
135
+ raise
136
+
137
+ return current_artifact
138
+
139
+ async def run_before_schedule(
140
+ self,
141
+ orchestrator: Any,
142
+ artifact: Artifact,
143
+ agent: Agent,
144
+ subscription: Subscription,
145
+ ) -> Any:
146
+ """Run on_before_schedule hooks (returns CONTINUE, SKIP, or DEFER).
147
+
148
+ Components execute in priority order. First component to return SKIP or
149
+ DEFER stops execution and returns that decision.
150
+
151
+ Args:
152
+ orchestrator: The Flock orchestrator instance
153
+ artifact: The artifact being scheduled
154
+ agent: The target agent
155
+ subscription: The subscription being evaluated
156
+
157
+ Returns:
158
+ ScheduleDecision (CONTINUE, SKIP, or DEFER)
159
+ """
160
+ from flock.components.orchestrator import ScheduleDecision
161
+
162
+ for component in self._components:
163
+ comp_name = component.name or component.__class__.__name__
164
+
165
+ self._logger.debug(
166
+ f"Running on_before_schedule: component={comp_name}, "
167
+ f"agent={agent.name}, artifact_type={artifact.type}"
168
+ )
169
+
170
+ try:
171
+ decision = await component.on_before_schedule(
172
+ orchestrator, artifact, agent, subscription
173
+ )
174
+
175
+ if decision == ScheduleDecision.SKIP:
176
+ self._logger.info(
177
+ f"Scheduling skipped by component: component={comp_name}, "
178
+ f"agent={agent.name}, artifact_type={artifact.type}, decision=SKIP"
179
+ )
180
+ return ScheduleDecision.SKIP
181
+
182
+ if decision == ScheduleDecision.DEFER:
183
+ self._logger.debug(
184
+ f"Scheduling deferred by component: component={comp_name}, "
185
+ f"agent={agent.name}, decision=DEFER"
186
+ )
187
+ return ScheduleDecision.DEFER
188
+
189
+ except Exception as e:
190
+ self._logger.exception(
191
+ f"Component hook failed: component={comp_name}, "
192
+ f"hook=on_before_schedule, error={e!s}"
193
+ )
194
+ raise
195
+
196
+ return ScheduleDecision.CONTINUE
197
+
198
+ async def run_collect_artifacts(
199
+ self,
200
+ orchestrator: Any,
201
+ artifact: Artifact,
202
+ agent: Agent,
203
+ subscription: Subscription,
204
+ ) -> Any:
205
+ """Run on_collect_artifacts hooks (returns first non-None result).
206
+
207
+ Components execute in priority order. First component to return non-None
208
+ wins (short-circuit). If all return None, default is immediate scheduling.
209
+
210
+ Args:
211
+ orchestrator: The Flock orchestrator instance
212
+ artifact: The artifact being collected
213
+ agent: The target agent
214
+ subscription: The subscription being evaluated
215
+
216
+ Returns:
217
+ CollectionResult (complete=True/False, artifacts=[...])
218
+ """
219
+ from flock.components.orchestrator import CollectionResult
220
+
221
+ for component in self._components:
222
+ comp_name = component.name or component.__class__.__name__
223
+
224
+ self._logger.debug(
225
+ f"Running on_collect_artifacts: component={comp_name}, "
226
+ f"agent={agent.name}, artifact_type={artifact.type}"
227
+ )
228
+
229
+ try:
230
+ result = await component.on_collect_artifacts(
231
+ orchestrator, artifact, agent, subscription
232
+ )
233
+
234
+ if result is not None:
235
+ self._logger.debug(
236
+ f"Collection handled by component: component={comp_name}, "
237
+ f"complete={result.complete}, artifact_count={len(result.artifacts)}"
238
+ )
239
+ return result
240
+ except Exception as e:
241
+ self._logger.exception(
242
+ f"Component hook failed: component={comp_name}, "
243
+ f"hook=on_collect_artifacts, error={e!s}"
244
+ )
245
+ raise
246
+
247
+ # Default: immediate scheduling with single artifact
248
+ self._logger.debug(
249
+ f"No component handled collection, using default: "
250
+ f"agent={agent.name}, artifact_type={artifact.type}"
251
+ )
252
+ return CollectionResult.immediate([artifact])
253
+
254
+ async def run_before_agent_schedule(
255
+ self, orchestrator: Any, agent: Agent, artifacts: list[Artifact]
256
+ ) -> list[Artifact] | None:
257
+ """Run on_before_agent_schedule hooks (returns modified artifacts or None to block).
258
+
259
+ Components execute in priority order, each receiving artifacts from the
260
+ previous component (chaining). If any component returns None, scheduling
261
+ is blocked.
262
+
263
+ Args:
264
+ orchestrator: The Flock orchestrator instance
265
+ agent: The target agent
266
+ artifacts: List of artifacts to schedule
267
+
268
+ Returns:
269
+ Modified artifacts list or None if scheduling blocked
270
+ """
271
+ current_artifacts = artifacts
272
+
273
+ for component in self._components:
274
+ comp_name = component.name or component.__class__.__name__
275
+
276
+ self._logger.debug(
277
+ f"Running on_before_agent_schedule: component={comp_name}, "
278
+ f"agent={agent.name}, artifact_count={len(current_artifacts)}"
279
+ )
280
+
281
+ try:
282
+ result = await component.on_before_agent_schedule(
283
+ orchestrator, agent, current_artifacts
284
+ )
285
+
286
+ if result is None:
287
+ self._logger.info(
288
+ f"Agent scheduling blocked by component: component={comp_name}, "
289
+ f"agent={agent.name}"
290
+ )
291
+ return None
292
+
293
+ current_artifacts = result
294
+ except Exception as e:
295
+ self._logger.exception(
296
+ f"Component hook failed: component={comp_name}, "
297
+ f"hook=on_before_agent_schedule, error={e!s}"
298
+ )
299
+ raise
300
+
301
+ return current_artifacts
302
+
303
+ async def run_agent_scheduled(
304
+ self,
305
+ orchestrator: Any,
306
+ agent: Agent,
307
+ artifacts: list[Artifact],
308
+ task: Task[Any],
309
+ ) -> None:
310
+ """Run on_agent_scheduled hooks (notification only, non-blocking).
311
+
312
+ Components execute in priority order. Exceptions are logged but don't
313
+ prevent other components from executing or block scheduling.
314
+
315
+ Args:
316
+ orchestrator: The Flock orchestrator instance
317
+ agent: The scheduled agent
318
+ artifacts: List of artifacts for the agent
319
+ task: The asyncio task for agent execution
320
+ """
321
+ for component in self._components:
322
+ comp_name = component.name or component.__class__.__name__
323
+
324
+ self._logger.debug(
325
+ f"Running on_agent_scheduled: component={comp_name}, "
326
+ f"agent={agent.name}, artifact_count={len(artifacts)}"
327
+ )
328
+
329
+ try:
330
+ await component.on_agent_scheduled(orchestrator, agent, artifacts, task)
331
+ except Exception as e:
332
+ self._logger.warning(
333
+ f"Component notification hook failed (non-critical): "
334
+ f"component={comp_name}, hook=on_agent_scheduled, error={e!s}"
335
+ )
336
+ # Don't propagate - this is a notification hook
337
+
338
+ async def run_idle(self, orchestrator: Any) -> None:
339
+ """Run on_orchestrator_idle hooks when orchestrator becomes idle.
340
+
341
+ Components execute in priority order. Exceptions are logged but don't
342
+ prevent other components from executing.
343
+
344
+ Args:
345
+ orchestrator: The Flock orchestrator instance
346
+ """
347
+ self._logger.debug(
348
+ f"Running on_orchestrator_idle hooks: component_count={len(self._components)}"
349
+ )
350
+
351
+ for component in self._components:
352
+ comp_name = component.name or component.__class__.__name__
353
+
354
+ try:
355
+ await component.on_orchestrator_idle(orchestrator)
356
+ except Exception as e:
357
+ self._logger.warning(
358
+ f"Component idle hook failed (non-critical): "
359
+ f"component={comp_name}, hook=on_orchestrator_idle, error={e!s}"
360
+ )
361
+
362
+ async def run_shutdown(self, orchestrator: Any) -> None:
363
+ """Run on_shutdown hooks when orchestrator shuts down.
364
+
365
+ Components execute in priority order. Exceptions are logged but don't
366
+ prevent shutdown of other components (best-effort cleanup).
367
+
368
+ Args:
369
+ orchestrator: The Flock orchestrator instance
370
+ """
371
+ self._logger.info(
372
+ f"Shutting down {len(self._components)} orchestrator components"
373
+ )
374
+
375
+ for component in self._components:
376
+ comp_name = component.name or component.__class__.__name__
377
+ self._logger.debug(f"Shutting down component: name={comp_name}")
378
+
379
+ try:
380
+ await component.on_shutdown(orchestrator)
381
+ except Exception as e:
382
+ self._logger.exception(
383
+ f"Component shutdown failed: component={comp_name}, "
384
+ f"hook=on_shutdown, error={e!s}"
385
+ )
386
+ # Continue shutting down other components
387
+
388
+
389
+ __all__ = ["ComponentRunner"]