flock-core 0.5.5__py3-none-any.whl → 0.5.6__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/agent.py +134 -17
- flock/components.py +4 -0
- flock/dashboard/collector.py +2 -0
- flock/orchestrator.py +386 -155
- flock/orchestrator_component.py +686 -0
- {flock_core-0.5.5.dist-info → flock_core-0.5.6.dist-info}/METADATA +67 -3
- {flock_core-0.5.5.dist-info → flock_core-0.5.6.dist-info}/RECORD +10 -9
- {flock_core-0.5.5.dist-info → flock_core-0.5.6.dist-info}/WHEEL +0 -0
- {flock_core-0.5.5.dist-info → flock_core-0.5.6.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.5.dist-info → flock_core-0.5.6.dist-info}/licenses/LICENSE +0 -0
flock/orchestrator.py
CHANGED
|
@@ -8,7 +8,7 @@ import os
|
|
|
8
8
|
from asyncio import Task
|
|
9
9
|
from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence
|
|
10
10
|
from contextlib import asynccontextmanager
|
|
11
|
-
from datetime import datetime,
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import TYPE_CHECKING, Any
|
|
14
14
|
from uuid import uuid4
|
|
@@ -31,9 +31,15 @@ from flock.mcp import (
|
|
|
31
31
|
FlockMCPFeatureConfiguration,
|
|
32
32
|
ServerParameters,
|
|
33
33
|
)
|
|
34
|
+
from flock.orchestrator_component import (
|
|
35
|
+
CollectionResult,
|
|
36
|
+
OrchestratorComponent,
|
|
37
|
+
ScheduleDecision,
|
|
38
|
+
)
|
|
34
39
|
from flock.registry import type_registry
|
|
35
40
|
from flock.runtime import Context
|
|
36
41
|
from flock.store import BlackboardStore, ConsumptionRecord, InMemoryBlackboardStore
|
|
42
|
+
from flock.subscription import Subscription
|
|
37
43
|
from flock.visibility import AgentIdentity, PublicVisibility, Visibility
|
|
38
44
|
|
|
39
45
|
|
|
@@ -153,6 +159,25 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
153
159
|
"yes",
|
|
154
160
|
"on",
|
|
155
161
|
}
|
|
162
|
+
|
|
163
|
+
# Phase 2: OrchestratorComponent system
|
|
164
|
+
self._components: list[OrchestratorComponent] = []
|
|
165
|
+
self._components_initialized: bool = False
|
|
166
|
+
|
|
167
|
+
# Auto-add built-in components
|
|
168
|
+
from flock.orchestrator_component import (
|
|
169
|
+
BuiltinCollectionComponent,
|
|
170
|
+
CircuitBreakerComponent,
|
|
171
|
+
DeduplicationComponent,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.add_component(CircuitBreakerComponent(max_iterations=max_agent_iterations))
|
|
175
|
+
self.add_component(DeduplicationComponent())
|
|
176
|
+
self.add_component(BuiltinCollectionComponent())
|
|
177
|
+
|
|
178
|
+
# Log orchestrator initialization
|
|
179
|
+
self._logger.debug("Orchestrator initialized: components=[]")
|
|
180
|
+
|
|
156
181
|
if not model:
|
|
157
182
|
self.model = os.getenv("DEFAULT_MODEL")
|
|
158
183
|
|
|
@@ -203,6 +228,47 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
203
228
|
def agents(self) -> list[Agent]:
|
|
204
229
|
return list(self._agents.values())
|
|
205
230
|
|
|
231
|
+
# Component management -------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def add_component(self, component: OrchestratorComponent) -> Flock:
|
|
234
|
+
"""Add an OrchestratorComponent to this orchestrator.
|
|
235
|
+
|
|
236
|
+
Components execute in priority order (lower priority number = earlier).
|
|
237
|
+
Multiple components can have the same priority.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
component: Component to add (must be an OrchestratorComponent instance)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Self for method chaining
|
|
244
|
+
|
|
245
|
+
Examples:
|
|
246
|
+
>>> # Add single component
|
|
247
|
+
>>> flock = Flock("openai/gpt-4.1")
|
|
248
|
+
>>> flock.add_component(CircuitBreakerComponent(max_iterations=500))
|
|
249
|
+
|
|
250
|
+
>>> # Method chaining
|
|
251
|
+
>>> flock.add_component(CircuitBreakerComponent()) \\
|
|
252
|
+
... .add_component(MetricsComponent()) \\
|
|
253
|
+
... .add_component(DeduplicationComponent())
|
|
254
|
+
|
|
255
|
+
>>> # Custom priority (lower = earlier)
|
|
256
|
+
>>> flock.add_component(
|
|
257
|
+
... CustomComponent(priority=5, name="early_component")
|
|
258
|
+
... )
|
|
259
|
+
"""
|
|
260
|
+
self._components.append(component)
|
|
261
|
+
self._components.sort(key=lambda c: c.priority)
|
|
262
|
+
|
|
263
|
+
# Log component addition
|
|
264
|
+
comp_name = component.name or component.__class__.__name__
|
|
265
|
+
self._logger.info(
|
|
266
|
+
f"Component added: name={comp_name}, "
|
|
267
|
+
f"priority={component.priority}, total_components={len(self._components)}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return self
|
|
271
|
+
|
|
206
272
|
# MCP management -------------------------------------------------------
|
|
207
273
|
|
|
208
274
|
def add_mcp(
|
|
@@ -506,11 +572,15 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
506
572
|
self._agent_iteration_count.clear()
|
|
507
573
|
return
|
|
508
574
|
|
|
575
|
+
# Notify components that orchestrator reached idle state
|
|
576
|
+
if self._components_initialized:
|
|
577
|
+
await self._run_idle()
|
|
578
|
+
|
|
509
579
|
# T068: Reset circuit breaker counters when idle
|
|
510
580
|
self._agent_iteration_count.clear()
|
|
511
581
|
|
|
512
582
|
# Automatically shutdown MCP connections when idle
|
|
513
|
-
await self.shutdown()
|
|
583
|
+
await self.shutdown(include_components=False)
|
|
514
584
|
|
|
515
585
|
async def direct_invoke(
|
|
516
586
|
self, agent: Agent, inputs: Sequence[BaseModel | Mapping[str, Any] | Artifact]
|
|
@@ -579,8 +649,17 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
579
649
|
"""
|
|
580
650
|
return asyncio.run(self.arun(agent_builder, *inputs))
|
|
581
651
|
|
|
582
|
-
async def shutdown(self) -> None:
|
|
583
|
-
"""Shutdown orchestrator and clean up resources.
|
|
652
|
+
async def shutdown(self, *, include_components: bool = True) -> None:
|
|
653
|
+
"""Shutdown orchestrator and clean up resources.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
include_components: Whether to invoke component shutdown hooks.
|
|
657
|
+
Internal callers (e.g., run_until_idle) disable this to avoid
|
|
658
|
+
tearing down component state between cascades.
|
|
659
|
+
"""
|
|
660
|
+
if include_components and self._components_initialized:
|
|
661
|
+
await self._run_shutdown()
|
|
662
|
+
|
|
584
663
|
# Cancel correlation cleanup task if running
|
|
585
664
|
if self._correlation_cleanup_task and not self._correlation_cleanup_task.done():
|
|
586
665
|
self._correlation_cleanup_task.cancel()
|
|
@@ -658,8 +737,8 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
658
737
|
|
|
659
738
|
# Inject event collector into all existing agents
|
|
660
739
|
for agent in self._agents.values():
|
|
661
|
-
#
|
|
662
|
-
agent.
|
|
740
|
+
# Add dashboard collector with priority ordering handled by agent
|
|
741
|
+
agent._add_utilities([event_collector])
|
|
663
742
|
|
|
664
743
|
# Start dashboard launcher (npm process + browser)
|
|
665
744
|
launcher_kwargs: dict[str, Any] = {"port": port}
|
|
@@ -892,188 +971,340 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
892
971
|
|
|
893
972
|
return outputs
|
|
894
973
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
self
|
|
898
|
-
|
|
899
|
-
payload: dict[str, Any],
|
|
900
|
-
*,
|
|
901
|
-
visibility: Visibility | None = None,
|
|
902
|
-
correlation_id: str | None = None,
|
|
903
|
-
partition_key: str | None = None,
|
|
904
|
-
tags: set[str] | None = None,
|
|
905
|
-
) -> Artifact:
|
|
906
|
-
"""Deprecated: Use publish() instead.
|
|
974
|
+
async def _persist_and_schedule(self, artifact: Artifact) -> None:
|
|
975
|
+
await self.store.publish(artifact)
|
|
976
|
+
self.metrics["artifacts_published"] += 1
|
|
977
|
+
await self._schedule_artifact(artifact)
|
|
907
978
|
|
|
908
|
-
|
|
979
|
+
# Component Hook Runners ───────────────────────────────────────
|
|
980
|
+
|
|
981
|
+
async def _run_initialize(self) -> None:
|
|
982
|
+
"""Initialize all components in priority order (called once).
|
|
983
|
+
|
|
984
|
+
Executes on_initialize hook for each component. Sets _components_initialized
|
|
985
|
+
flag to prevent multiple initializations.
|
|
986
|
+
"""
|
|
987
|
+
if self._components_initialized:
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
self._logger.info(f"Initializing {len(self._components)} orchestrator components")
|
|
991
|
+
|
|
992
|
+
for component in self._components:
|
|
993
|
+
comp_name = component.name or component.__class__.__name__
|
|
994
|
+
self._logger.debug(
|
|
995
|
+
f"Initializing component: name={comp_name}, priority={component.priority}"
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
try:
|
|
999
|
+
await component.on_initialize(self)
|
|
1000
|
+
except Exception as e:
|
|
1001
|
+
self._logger.exception(
|
|
1002
|
+
f"Component initialization failed: name={comp_name}, error={e!s}"
|
|
1003
|
+
)
|
|
1004
|
+
raise
|
|
1005
|
+
|
|
1006
|
+
self._components_initialized = True
|
|
1007
|
+
self._logger.info(f"All components initialized: count={len(self._components)}")
|
|
1008
|
+
|
|
1009
|
+
async def _run_artifact_published(self, artifact: Artifact) -> Artifact | None:
|
|
1010
|
+
"""Run on_artifact_published hooks (returns modified artifact or None to block).
|
|
1011
|
+
|
|
1012
|
+
Components execute in priority order, each receiving the artifact from the
|
|
1013
|
+
previous component (chaining). If any component returns None, the artifact
|
|
1014
|
+
is blocked and scheduling stops.
|
|
1015
|
+
"""
|
|
1016
|
+
current_artifact = artifact
|
|
1017
|
+
|
|
1018
|
+
for component in self._components:
|
|
1019
|
+
comp_name = component.name or component.__class__.__name__
|
|
1020
|
+
self._logger.debug(
|
|
1021
|
+
f"Running on_artifact_published: component={comp_name}, "
|
|
1022
|
+
f"artifact_type={current_artifact.type}, artifact_id={current_artifact.id}"
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
try:
|
|
1026
|
+
result = await component.on_artifact_published(self, current_artifact)
|
|
1027
|
+
|
|
1028
|
+
if result is None:
|
|
1029
|
+
self._logger.info(
|
|
1030
|
+
f"Artifact blocked by component: component={comp_name}, "
|
|
1031
|
+
f"artifact_type={current_artifact.type}, artifact_id={current_artifact.id}"
|
|
1032
|
+
)
|
|
1033
|
+
return None
|
|
1034
|
+
|
|
1035
|
+
current_artifact = result
|
|
1036
|
+
except Exception as e:
|
|
1037
|
+
self._logger.exception(
|
|
1038
|
+
f"Component hook failed: component={comp_name}, "
|
|
1039
|
+
f"hook=on_artifact_published, error={e!s}"
|
|
1040
|
+
)
|
|
1041
|
+
raise
|
|
1042
|
+
|
|
1043
|
+
return current_artifact
|
|
1044
|
+
|
|
1045
|
+
async def _run_before_schedule(
|
|
1046
|
+
self, artifact: Artifact, agent: Agent, subscription: Subscription
|
|
1047
|
+
) -> ScheduleDecision:
|
|
1048
|
+
"""Run on_before_schedule hooks (returns CONTINUE, SKIP, or DEFER).
|
|
1049
|
+
|
|
1050
|
+
Components execute in priority order. First component to return SKIP or
|
|
1051
|
+
DEFER stops execution and returns that decision.
|
|
909
1052
|
"""
|
|
910
|
-
import
|
|
1053
|
+
from flock.orchestrator_component import ScheduleDecision
|
|
1054
|
+
|
|
1055
|
+
for component in self._components:
|
|
1056
|
+
comp_name = component.name or component.__class__.__name__
|
|
911
1057
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1058
|
+
self._logger.debug(
|
|
1059
|
+
f"Running on_before_schedule: component={comp_name}, "
|
|
1060
|
+
f"agent={agent.name}, artifact_type={artifact.type}"
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
try:
|
|
1064
|
+
decision = await component.on_before_schedule(self, artifact, agent, subscription)
|
|
1065
|
+
|
|
1066
|
+
if decision == ScheduleDecision.SKIP:
|
|
1067
|
+
self._logger.info(
|
|
1068
|
+
f"Scheduling skipped by component: component={comp_name}, "
|
|
1069
|
+
f"agent={agent.name}, artifact_type={artifact.type}, decision=SKIP"
|
|
1070
|
+
)
|
|
1071
|
+
return ScheduleDecision.SKIP
|
|
1072
|
+
|
|
1073
|
+
if decision == ScheduleDecision.DEFER:
|
|
1074
|
+
self._logger.debug(
|
|
1075
|
+
f"Scheduling deferred by component: component={comp_name}, "
|
|
1076
|
+
f"agent={agent.name}, decision=DEFER"
|
|
1077
|
+
)
|
|
1078
|
+
return ScheduleDecision.DEFER
|
|
1079
|
+
|
|
1080
|
+
except Exception as e:
|
|
1081
|
+
self._logger.exception(
|
|
1082
|
+
f"Component hook failed: component={comp_name}, "
|
|
1083
|
+
f"hook=on_before_schedule, error={e!s}"
|
|
1084
|
+
)
|
|
1085
|
+
raise
|
|
1086
|
+
|
|
1087
|
+
return ScheduleDecision.CONTINUE
|
|
1088
|
+
|
|
1089
|
+
async def _run_collect_artifacts(
|
|
1090
|
+
self, artifact: Artifact, agent: Agent, subscription: Subscription
|
|
1091
|
+
) -> CollectionResult:
|
|
1092
|
+
"""Run on_collect_artifacts hooks (returns first non-None result).
|
|
1093
|
+
|
|
1094
|
+
Components execute in priority order. First component to return non-None
|
|
1095
|
+
wins (short-circuit). If all return None, default is immediate scheduling.
|
|
1096
|
+
"""
|
|
1097
|
+
from flock.orchestrator_component import CollectionResult
|
|
1098
|
+
|
|
1099
|
+
for component in self._components:
|
|
1100
|
+
comp_name = component.name or component.__class__.__name__
|
|
1101
|
+
|
|
1102
|
+
self._logger.debug(
|
|
1103
|
+
f"Running on_collect_artifacts: component={comp_name}, "
|
|
1104
|
+
f"agent={agent.name}, artifact_type={artifact.type}"
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
try:
|
|
1108
|
+
result = await component.on_collect_artifacts(self, artifact, agent, subscription)
|
|
1109
|
+
|
|
1110
|
+
if result is not None:
|
|
1111
|
+
self._logger.debug(
|
|
1112
|
+
f"Collection handled by component: component={comp_name}, "
|
|
1113
|
+
f"complete={result.complete}, artifact_count={len(result.artifacts)}"
|
|
1114
|
+
)
|
|
1115
|
+
return result
|
|
1116
|
+
except Exception as e:
|
|
1117
|
+
self._logger.exception(
|
|
1118
|
+
f"Component hook failed: component={comp_name}, "
|
|
1119
|
+
f"hook=on_collect_artifacts, error={e!s}"
|
|
1120
|
+
)
|
|
1121
|
+
raise
|
|
1122
|
+
|
|
1123
|
+
# Default: immediate scheduling with single artifact
|
|
1124
|
+
self._logger.debug(
|
|
1125
|
+
f"No component handled collection, using default: "
|
|
1126
|
+
f"agent={agent.name}, artifact_type={artifact.type}"
|
|
916
1127
|
)
|
|
917
|
-
return
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1128
|
+
return CollectionResult.immediate([artifact])
|
|
1129
|
+
|
|
1130
|
+
async def _run_before_agent_schedule(
|
|
1131
|
+
self, agent: Agent, artifacts: list[Artifact]
|
|
1132
|
+
) -> list[Artifact] | None:
|
|
1133
|
+
"""Run on_before_agent_schedule hooks (returns modified artifacts or None to block).
|
|
1134
|
+
|
|
1135
|
+
Components execute in priority order, each receiving artifacts from the
|
|
1136
|
+
previous component (chaining). If any component returns None, scheduling
|
|
1137
|
+
is blocked.
|
|
1138
|
+
"""
|
|
1139
|
+
current_artifacts = artifacts
|
|
1140
|
+
|
|
1141
|
+
for component in self._components:
|
|
1142
|
+
comp_name = component.name or component.__class__.__name__
|
|
1143
|
+
|
|
1144
|
+
self._logger.debug(
|
|
1145
|
+
f"Running on_before_agent_schedule: component={comp_name}, "
|
|
1146
|
+
f"agent={agent.name}, artifact_count={len(current_artifacts)}"
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
try:
|
|
1150
|
+
result = await component.on_before_agent_schedule(self, agent, current_artifacts)
|
|
1151
|
+
|
|
1152
|
+
if result is None:
|
|
1153
|
+
self._logger.info(
|
|
1154
|
+
f"Agent scheduling blocked by component: component={comp_name}, "
|
|
1155
|
+
f"agent={agent.name}"
|
|
1156
|
+
)
|
|
1157
|
+
return None
|
|
1158
|
+
|
|
1159
|
+
current_artifacts = result
|
|
1160
|
+
except Exception as e:
|
|
1161
|
+
self._logger.exception(
|
|
1162
|
+
f"Component hook failed: component={comp_name}, "
|
|
1163
|
+
f"hook=on_before_agent_schedule, error={e!s}"
|
|
1164
|
+
)
|
|
1165
|
+
raise
|
|
1166
|
+
|
|
1167
|
+
return current_artifacts
|
|
1168
|
+
|
|
1169
|
+
async def _run_agent_scheduled(
|
|
1170
|
+
self, agent: Agent, artifacts: list[Artifact], task: Task[Any]
|
|
1171
|
+
) -> None:
|
|
1172
|
+
"""Run on_agent_scheduled hooks (notification only, non-blocking).
|
|
1173
|
+
|
|
1174
|
+
Components execute in priority order. Exceptions are logged but don't
|
|
1175
|
+
prevent other components from executing or block scheduling.
|
|
1176
|
+
"""
|
|
1177
|
+
for component in self._components:
|
|
1178
|
+
comp_name = component.name or component.__class__.__name__
|
|
1179
|
+
|
|
1180
|
+
self._logger.debug(
|
|
1181
|
+
f"Running on_agent_scheduled: component={comp_name}, "
|
|
1182
|
+
f"agent={agent.name}, artifact_count={len(artifacts)}"
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
try:
|
|
1186
|
+
await component.on_agent_scheduled(self, agent, artifacts, task)
|
|
1187
|
+
except Exception as e:
|
|
1188
|
+
self._logger.warning(
|
|
1189
|
+
f"Component notification hook failed (non-critical): "
|
|
1190
|
+
f"component={comp_name}, hook=on_agent_scheduled, error={e!s}"
|
|
1191
|
+
)
|
|
1192
|
+
# Don't propagate - this is a notification hook
|
|
1193
|
+
|
|
1194
|
+
async def _run_idle(self) -> None:
|
|
1195
|
+
"""Run on_orchestrator_idle hooks when orchestrator becomes idle.
|
|
1196
|
+
|
|
1197
|
+
Components execute in priority order. Exceptions are logged but don't
|
|
1198
|
+
prevent other components from executing.
|
|
1199
|
+
"""
|
|
1200
|
+
self._logger.debug(
|
|
1201
|
+
f"Running on_orchestrator_idle hooks: component_count={len(self._components)}"
|
|
923
1202
|
)
|
|
924
1203
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1204
|
+
for component in self._components:
|
|
1205
|
+
comp_name = component.name or component.__class__.__name__
|
|
1206
|
+
|
|
1207
|
+
try:
|
|
1208
|
+
await component.on_orchestrator_idle(self)
|
|
1209
|
+
except Exception as e:
|
|
1210
|
+
self._logger.warning(
|
|
1211
|
+
f"Component idle hook failed (non-critical): "
|
|
1212
|
+
f"component={comp_name}, hook=on_orchestrator_idle, error={e!s}"
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
async def _run_shutdown(self) -> None:
|
|
1216
|
+
"""Run on_shutdown hooks when orchestrator shuts down.
|
|
1217
|
+
|
|
1218
|
+
Components execute in priority order. Exceptions are logged but don't
|
|
1219
|
+
prevent shutdown of other components (best-effort cleanup).
|
|
1220
|
+
"""
|
|
1221
|
+
self._logger.info(f"Shutting down {len(self._components)} orchestrator components")
|
|
1222
|
+
|
|
1223
|
+
for component in self._components:
|
|
1224
|
+
comp_name = component.name or component.__class__.__name__
|
|
1225
|
+
self._logger.debug(f"Shutting down component: name={comp_name}")
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
await component.on_shutdown(self)
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
self._logger.exception(
|
|
1231
|
+
f"Component shutdown failed: component={comp_name}, "
|
|
1232
|
+
f"hook=on_shutdown, error={e!s}"
|
|
1233
|
+
)
|
|
1234
|
+
# Continue shutting down other components
|
|
1235
|
+
|
|
1236
|
+
# Scheduling ───────────────────────────────────────────────────
|
|
929
1237
|
|
|
930
1238
|
async def _schedule_artifact(self, artifact: Artifact) -> None:
|
|
1239
|
+
"""Schedule agents for an artifact using component hooks.
|
|
1240
|
+
|
|
1241
|
+
Refactored to use OrchestratorComponent hook system for extensibility.
|
|
1242
|
+
Components can modify artifact, control scheduling, and handle collection.
|
|
1243
|
+
"""
|
|
1244
|
+
# Phase 3: Initialize components on first artifact
|
|
1245
|
+
if not self._components_initialized:
|
|
1246
|
+
await self._run_initialize()
|
|
1247
|
+
|
|
1248
|
+
# Phase 3: Component hook - artifact published (can transform or block)
|
|
1249
|
+
artifact = await self._run_artifact_published(artifact)
|
|
1250
|
+
if artifact is None:
|
|
1251
|
+
return # Artifact blocked by component
|
|
1252
|
+
|
|
931
1253
|
for agent in self.agents:
|
|
932
1254
|
identity = agent.identity
|
|
933
1255
|
for subscription in agent.subscriptions:
|
|
934
1256
|
if not subscription.accepts_events():
|
|
935
1257
|
continue
|
|
1258
|
+
|
|
936
1259
|
# T066: Check prevent_self_trigger
|
|
937
1260
|
if agent.prevent_self_trigger and artifact.produced_by == agent.name:
|
|
938
1261
|
continue # Skip - agent produced this artifact (prevents feedback loops)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if iteration_count >= self.max_agent_iterations:
|
|
942
|
-
# Agent hit iteration limit - possible infinite loop
|
|
943
|
-
continue
|
|
1262
|
+
|
|
1263
|
+
# Visibility check
|
|
944
1264
|
if not self._check_visibility(artifact, identity):
|
|
945
1265
|
continue
|
|
1266
|
+
|
|
1267
|
+
# Subscription match check
|
|
946
1268
|
if not subscription.matches(artifact):
|
|
947
1269
|
continue
|
|
948
|
-
if self._seen_before(artifact, agent):
|
|
949
|
-
continue
|
|
950
1270
|
|
|
951
|
-
#
|
|
952
|
-
|
|
953
|
-
# Use CorrelationEngine for JoinSpec (correlated AND gates)
|
|
954
|
-
subscription_index = agent.subscriptions.index(subscription)
|
|
955
|
-
completed_group = self._correlation_engine.add_artifact(
|
|
956
|
-
artifact=artifact,
|
|
957
|
-
subscription=subscription,
|
|
958
|
-
subscription_index=subscription_index,
|
|
959
|
-
)
|
|
1271
|
+
# Phase 3: Component hook - before schedule (circuit breaker, deduplication, etc.)
|
|
1272
|
+
from flock.orchestrator_component import ScheduleDecision
|
|
960
1273
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
)
|
|
966
|
-
self._correlation_cleanup_task = asyncio.create_task(
|
|
967
|
-
self._correlation_cleanup_loop()
|
|
968
|
-
)
|
|
969
|
-
|
|
970
|
-
if completed_group is None:
|
|
971
|
-
# Still waiting for correlation to complete
|
|
972
|
-
# Phase 1.2: Emit real-time correlation update event
|
|
973
|
-
await self._emit_correlation_updated_event(
|
|
974
|
-
agent_name=agent.name,
|
|
975
|
-
subscription_index=subscription_index,
|
|
976
|
-
artifact=artifact,
|
|
977
|
-
)
|
|
978
|
-
continue
|
|
979
|
-
|
|
980
|
-
# Correlation complete! Get all correlated artifacts
|
|
981
|
-
artifacts = completed_group.get_artifacts()
|
|
982
|
-
else:
|
|
983
|
-
# AND GATE LOGIC: Use artifact collector for simple AND gates (no correlation)
|
|
984
|
-
is_complete, artifacts = self._artifact_collector.add_artifact(
|
|
985
|
-
agent, subscription, artifact
|
|
986
|
-
)
|
|
1274
|
+
decision = await self._run_before_schedule(artifact, agent, subscription)
|
|
1275
|
+
if decision == ScheduleDecision.SKIP:
|
|
1276
|
+
continue # Skip this subscription
|
|
1277
|
+
if decision == ScheduleDecision.DEFER:
|
|
1278
|
+
continue # Defer for later (batching/correlation)
|
|
987
1279
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
# BatchSpec BATCHING: Check if subscription has batch accumulator
|
|
993
|
-
if subscription.batch is not None:
|
|
994
|
-
# Add to batch accumulator
|
|
995
|
-
subscription_index = agent.subscriptions.index(subscription)
|
|
996
|
-
|
|
997
|
-
# COMBINED FEATURES: JoinSpec + BatchSpec
|
|
998
|
-
# If we have JoinSpec, artifacts is a correlated GROUP - treat as single batch item
|
|
999
|
-
# If we have AND gate, artifacts is a complete set - treat as single batch item
|
|
1000
|
-
# Otherwise (single type), add each artifact individually
|
|
1001
|
-
|
|
1002
|
-
if subscription.join is not None or len(subscription.type_models) > 1:
|
|
1003
|
-
# JoinSpec or AND gate: Treat artifact group as ONE batch item
|
|
1004
|
-
should_flush = self._batch_engine.add_artifact_group(
|
|
1005
|
-
artifacts=artifacts,
|
|
1006
|
-
subscription=subscription,
|
|
1007
|
-
subscription_index=subscription_index,
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
# Start timeout checker if this batch has a timeout and checker not running
|
|
1011
|
-
if subscription.batch.timeout and self._batch_timeout_task is None:
|
|
1012
|
-
self._batch_timeout_task = asyncio.create_task(
|
|
1013
|
-
self._batch_timeout_checker_loop()
|
|
1014
|
-
)
|
|
1015
|
-
else:
|
|
1016
|
-
# Single type subscription: Add each artifact individually
|
|
1017
|
-
should_flush = False
|
|
1018
|
-
for single_artifact in artifacts:
|
|
1019
|
-
should_flush = self._batch_engine.add_artifact(
|
|
1020
|
-
artifact=single_artifact,
|
|
1021
|
-
subscription=subscription,
|
|
1022
|
-
subscription_index=subscription_index,
|
|
1023
|
-
)
|
|
1024
|
-
|
|
1025
|
-
# Start timeout checker if this batch has a timeout and checker not running
|
|
1026
|
-
if subscription.batch.timeout and self._batch_timeout_task is None:
|
|
1027
|
-
self._batch_timeout_task = asyncio.create_task(
|
|
1028
|
-
self._batch_timeout_checker_loop()
|
|
1029
|
-
)
|
|
1030
|
-
|
|
1031
|
-
if should_flush:
|
|
1032
|
-
# Size threshold reached! Flush batch now
|
|
1033
|
-
break
|
|
1034
|
-
|
|
1035
|
-
if not should_flush:
|
|
1036
|
-
# Batch not full yet - wait for more artifacts
|
|
1037
|
-
# Phase 1.2: Emit real-time batch update event
|
|
1038
|
-
await self._emit_batch_item_added_event(
|
|
1039
|
-
agent_name=agent.name,
|
|
1040
|
-
subscription_index=subscription_index,
|
|
1041
|
-
subscription=subscription,
|
|
1042
|
-
artifact=artifact,
|
|
1043
|
-
)
|
|
1044
|
-
continue
|
|
1045
|
-
|
|
1046
|
-
# Flush the batch and get all accumulated artifacts
|
|
1047
|
-
batched_artifacts = self._batch_engine.flush_batch(
|
|
1048
|
-
agent.name, subscription_index
|
|
1049
|
-
)
|
|
1280
|
+
# Phase 3: Component hook - collect artifacts (handles AND gates, correlation, batching)
|
|
1281
|
+
collection = await self._run_collect_artifacts(artifact, agent, subscription)
|
|
1282
|
+
if not collection.complete:
|
|
1283
|
+
continue # Still collecting (AND gate, correlation, or batch incomplete)
|
|
1050
1284
|
|
|
1051
|
-
|
|
1052
|
-
# No batch to flush (shouldn't happen, but defensive)
|
|
1053
|
-
continue
|
|
1285
|
+
artifacts = collection.artifacts
|
|
1054
1286
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1287
|
+
# Phase 3: Component hook - before agent schedule (final validation/transformation)
|
|
1288
|
+
artifacts = await self._run_before_agent_schedule(agent, artifacts)
|
|
1289
|
+
if artifacts is None:
|
|
1290
|
+
continue # Scheduling blocked by component
|
|
1057
1291
|
|
|
1058
|
-
# Complete! Schedule agent with
|
|
1059
|
-
#
|
|
1060
|
-
self._agent_iteration_count[agent.name] = iteration_count + 1
|
|
1061
|
-
|
|
1062
|
-
# Mark all artifacts as processed (prevent duplicate triggers)
|
|
1063
|
-
for collected_artifact in artifacts:
|
|
1064
|
-
self._mark_processed(collected_artifact, agent)
|
|
1065
|
-
|
|
1066
|
-
# Schedule agent with ALL artifacts (batched, correlated, or AND gate complete)
|
|
1067
|
-
# NEW: Mark as batch execution if flushed from BatchSpec
|
|
1292
|
+
# Complete! Schedule agent with collected artifacts
|
|
1293
|
+
# Schedule agent task
|
|
1068
1294
|
is_batch_execution = subscription.batch is not None
|
|
1069
|
-
self._schedule_task(agent, artifacts, is_batch=is_batch_execution)
|
|
1295
|
+
task = self._schedule_task(agent, artifacts, is_batch=is_batch_execution)
|
|
1296
|
+
|
|
1297
|
+
# Phase 3: Component hook - agent scheduled (notification)
|
|
1298
|
+
await self._run_agent_scheduled(agent, artifacts, task)
|
|
1070
1299
|
|
|
1071
1300
|
def _schedule_task(
|
|
1072
1301
|
self, agent: Agent, artifacts: list[Artifact], is_batch: bool = False
|
|
1073
|
-
) ->
|
|
1302
|
+
) -> Task[Any]:
|
|
1303
|
+
"""Schedule agent task and return the task handle."""
|
|
1074
1304
|
task = asyncio.create_task(self._run_agent_task(agent, artifacts, is_batch=is_batch))
|
|
1075
1305
|
self._tasks.add(task)
|
|
1076
1306
|
task.add_done_callback(self._tasks.discard)
|
|
1307
|
+
return task
|
|
1077
1308
|
|
|
1078
1309
|
def _record_agent_run(self, agent: Agent) -> None:
|
|
1079
1310
|
self.metrics["agent_runs"] += 1
|