flock-core 0.5.0b27__py3-none-any.whl → 0.5.0b50__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 (357) hide show
  1. flock/__init__.py +12 -217
  2. flock/agent.py +678 -0
  3. flock/api/themes.py +71 -0
  4. flock/artifacts.py +79 -0
  5. flock/cli.py +75 -0
  6. flock/components.py +173 -0
  7. flock/dashboard/__init__.py +28 -0
  8. flock/dashboard/collector.py +283 -0
  9. flock/dashboard/events.py +182 -0
  10. flock/dashboard/launcher.py +230 -0
  11. flock/dashboard/service.py +537 -0
  12. flock/dashboard/websocket.py +235 -0
  13. flock/engines/__init__.py +6 -0
  14. flock/engines/dspy_engine.py +856 -0
  15. flock/examples.py +128 -0
  16. flock/{core/util → helper}/cli_helper.py +4 -3
  17. flock/{core/logging → logging}/__init__.py +2 -3
  18. flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
  19. flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
  20. flock/{core/logging → logging}/formatters/themed_formatter.py +69 -115
  21. flock/{core/logging → logging}/logging.py +77 -61
  22. flock/{core/logging → logging}/telemetry.py +20 -26
  23. flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
  24. flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +6 -9
  25. flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
  26. flock/{core/logging → logging}/trace_and_logged.py +20 -24
  27. flock/mcp/__init__.py +91 -0
  28. flock/{core/mcp/mcp_client.py → mcp/client.py} +103 -154
  29. flock/{core/mcp/mcp_config.py → mcp/config.py} +62 -117
  30. flock/mcp/manager.py +255 -0
  31. flock/mcp/servers/sse/__init__.py +1 -1
  32. flock/mcp/servers/sse/flock_sse_server.py +11 -53
  33. flock/mcp/servers/stdio/__init__.py +1 -1
  34. flock/mcp/servers/stdio/flock_stdio_server.py +8 -48
  35. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +17 -62
  36. flock/mcp/servers/websockets/flock_websocket_server.py +7 -40
  37. flock/{core/mcp/flock_mcp_tool.py → mcp/tool.py} +16 -26
  38. flock/mcp/types/__init__.py +42 -0
  39. flock/{core/mcp → mcp}/types/callbacks.py +9 -15
  40. flock/{core/mcp → mcp}/types/factories.py +7 -6
  41. flock/{core/mcp → mcp}/types/handlers.py +13 -18
  42. flock/{core/mcp → mcp}/types/types.py +70 -74
  43. flock/{core/mcp → mcp}/util/helpers.py +1 -1
  44. flock/orchestrator.py +645 -0
  45. flock/registry.py +148 -0
  46. flock/runtime.py +262 -0
  47. flock/service.py +140 -0
  48. flock/store.py +69 -0
  49. flock/subscription.py +111 -0
  50. flock/themes/andromeda.toml +1 -1
  51. flock/themes/apple-system-colors.toml +1 -1
  52. flock/themes/arcoiris.toml +1 -1
  53. flock/themes/atomonelight.toml +1 -1
  54. flock/themes/ayu copy.toml +1 -1
  55. flock/themes/ayu-light.toml +1 -1
  56. flock/themes/belafonte-day.toml +1 -1
  57. flock/themes/belafonte-night.toml +1 -1
  58. flock/themes/blulocodark.toml +1 -1
  59. flock/themes/breeze.toml +1 -1
  60. flock/themes/broadcast.toml +1 -1
  61. flock/themes/brogrammer.toml +1 -1
  62. flock/themes/builtin-dark.toml +1 -1
  63. flock/themes/builtin-pastel-dark.toml +1 -1
  64. flock/themes/catppuccin-latte.toml +1 -1
  65. flock/themes/catppuccin-macchiato.toml +1 -1
  66. flock/themes/catppuccin-mocha.toml +1 -1
  67. flock/themes/cga.toml +1 -1
  68. flock/themes/chalk.toml +1 -1
  69. flock/themes/ciapre.toml +1 -1
  70. flock/themes/coffee-theme.toml +1 -1
  71. flock/themes/cyberpunkscarletprotocol.toml +1 -1
  72. flock/themes/dark+.toml +1 -1
  73. flock/themes/darkermatrix.toml +1 -1
  74. flock/themes/darkside.toml +1 -1
  75. flock/themes/desert.toml +1 -1
  76. flock/themes/django.toml +1 -1
  77. flock/themes/djangosmooth.toml +1 -1
  78. flock/themes/doomone.toml +1 -1
  79. flock/themes/dotgov.toml +1 -1
  80. flock/themes/dracula+.toml +1 -1
  81. flock/themes/duckbones.toml +1 -1
  82. flock/themes/encom.toml +1 -1
  83. flock/themes/espresso.toml +1 -1
  84. flock/themes/everblush.toml +1 -1
  85. flock/themes/fairyfloss.toml +1 -1
  86. flock/themes/fideloper.toml +1 -1
  87. flock/themes/fishtank.toml +1 -1
  88. flock/themes/flexoki-light.toml +1 -1
  89. flock/themes/floraverse.toml +1 -1
  90. flock/themes/framer.toml +1 -1
  91. flock/themes/galizur.toml +1 -1
  92. flock/themes/github.toml +1 -1
  93. flock/themes/grass.toml +1 -1
  94. flock/themes/grey-green.toml +1 -1
  95. flock/themes/gruvboxlight.toml +1 -1
  96. flock/themes/guezwhoz.toml +1 -1
  97. flock/themes/harper.toml +1 -1
  98. flock/themes/hax0r-blue.toml +1 -1
  99. flock/themes/hopscotch.256.toml +1 -1
  100. flock/themes/ic-green-ppl.toml +1 -1
  101. flock/themes/iceberg-dark.toml +1 -1
  102. flock/themes/japanesque.toml +1 -1
  103. flock/themes/jubi.toml +1 -1
  104. flock/themes/kibble.toml +1 -1
  105. flock/themes/kolorit.toml +1 -1
  106. flock/themes/kurokula.toml +1 -1
  107. flock/themes/materialdesigncolors.toml +1 -1
  108. flock/themes/matrix.toml +1 -1
  109. flock/themes/mellifluous.toml +1 -1
  110. flock/themes/midnight-in-mojave.toml +1 -1
  111. flock/themes/monokai-remastered.toml +1 -1
  112. flock/themes/monokai-soda.toml +1 -1
  113. flock/themes/neon.toml +1 -1
  114. flock/themes/neopolitan.toml +1 -1
  115. flock/themes/nord-light.toml +1 -1
  116. flock/themes/ocean.toml +1 -1
  117. flock/themes/onehalfdark.toml +1 -1
  118. flock/themes/onehalflight.toml +1 -1
  119. flock/themes/palenighthc.toml +1 -1
  120. flock/themes/paulmillr.toml +1 -1
  121. flock/themes/pencildark.toml +1 -1
  122. flock/themes/pnevma.toml +1 -1
  123. flock/themes/purple-rain.toml +1 -1
  124. flock/themes/purplepeter.toml +1 -1
  125. flock/themes/raycast-dark.toml +1 -1
  126. flock/themes/red-sands.toml +1 -1
  127. flock/themes/relaxed.toml +1 -1
  128. flock/themes/retro.toml +1 -1
  129. flock/themes/rose-pine.toml +1 -1
  130. flock/themes/royal.toml +1 -1
  131. flock/themes/ryuuko.toml +1 -1
  132. flock/themes/sakura.toml +1 -1
  133. flock/themes/scarlet-protocol.toml +1 -1
  134. flock/themes/seoulbones-dark.toml +1 -1
  135. flock/themes/shades-of-purple.toml +1 -1
  136. flock/themes/smyck.toml +1 -1
  137. flock/themes/softserver.toml +1 -1
  138. flock/themes/solarized-darcula.toml +1 -1
  139. flock/themes/square.toml +1 -1
  140. flock/themes/sugarplum.toml +1 -1
  141. flock/themes/thayer-bright.toml +1 -1
  142. flock/themes/tokyonight.toml +1 -1
  143. flock/themes/tomorrow.toml +1 -1
  144. flock/themes/ubuntu.toml +1 -1
  145. flock/themes/ultradark.toml +1 -1
  146. flock/themes/ultraviolent.toml +1 -1
  147. flock/themes/unikitty.toml +1 -1
  148. flock/themes/urple.toml +1 -1
  149. flock/themes/vesper.toml +1 -1
  150. flock/themes/vimbones.toml +1 -1
  151. flock/themes/wildcherry.toml +1 -1
  152. flock/themes/wilmersdorf.toml +1 -1
  153. flock/themes/wryan.toml +1 -1
  154. flock/themes/xcodedarkhc.toml +1 -1
  155. flock/themes/xcodelight.toml +1 -1
  156. flock/themes/zenbones-light.toml +1 -1
  157. flock/themes/zenwritten-dark.toml +1 -1
  158. flock/utilities.py +301 -0
  159. flock/{components/utility → utility}/output_utility_component.py +68 -53
  160. flock/visibility.py +107 -0
  161. flock_core-0.5.0b50.dist-info/METADATA +747 -0
  162. flock_core-0.5.0b50.dist-info/RECORD +398 -0
  163. flock_core-0.5.0b50.dist-info/entry_points.txt +2 -0
  164. {flock_core-0.5.0b27.dist-info → flock_core-0.5.0b50.dist-info}/licenses/LICENSE +1 -1
  165. flock/adapter/__init__.py +0 -14
  166. flock/adapter/azure_adapter.py +0 -68
  167. flock/adapter/chroma_adapter.py +0 -73
  168. flock/adapter/faiss_adapter.py +0 -97
  169. flock/adapter/pinecone_adapter.py +0 -51
  170. flock/adapter/vector_base.py +0 -47
  171. flock/cli/assets/release_notes.md +0 -140
  172. flock/cli/config.py +0 -8
  173. flock/cli/constants.py +0 -36
  174. flock/cli/create_agent.py +0 -1
  175. flock/cli/create_flock.py +0 -280
  176. flock/cli/execute_flock.py +0 -620
  177. flock/cli/load_agent.py +0 -1
  178. flock/cli/load_examples.py +0 -1
  179. flock/cli/load_flock.py +0 -192
  180. flock/cli/load_release_notes.py +0 -20
  181. flock/cli/loaded_flock_cli.py +0 -254
  182. flock/cli/manage_agents.py +0 -459
  183. flock/cli/registry_management.py +0 -889
  184. flock/cli/runner.py +0 -41
  185. flock/cli/settings.py +0 -857
  186. flock/cli/utils.py +0 -135
  187. flock/cli/view_results.py +0 -29
  188. flock/cli/yaml_editor.py +0 -396
  189. flock/components/__init__.py +0 -30
  190. flock/components/evaluation/__init__.py +0 -9
  191. flock/components/evaluation/declarative_evaluation_component.py +0 -606
  192. flock/components/routing/__init__.py +0 -15
  193. flock/components/routing/conditional_routing_component.py +0 -494
  194. flock/components/routing/default_routing_component.py +0 -103
  195. flock/components/routing/llm_routing_component.py +0 -206
  196. flock/components/utility/__init__.py +0 -15
  197. flock/components/utility/memory_utility_component.py +0 -550
  198. flock/components/utility/metrics_utility_component.py +0 -700
  199. flock/config.py +0 -61
  200. flock/core/__init__.py +0 -110
  201. flock/core/agent/__init__.py +0 -16
  202. flock/core/agent/default_agent.py +0 -180
  203. flock/core/agent/flock_agent_components.py +0 -104
  204. flock/core/agent/flock_agent_execution.py +0 -101
  205. flock/core/agent/flock_agent_integration.py +0 -260
  206. flock/core/agent/flock_agent_lifecycle.py +0 -186
  207. flock/core/agent/flock_agent_serialization.py +0 -381
  208. flock/core/api/__init__.py +0 -10
  209. flock/core/api/custom_endpoint.py +0 -45
  210. flock/core/api/endpoints.py +0 -254
  211. flock/core/api/main.py +0 -162
  212. flock/core/api/models.py +0 -97
  213. flock/core/api/run_store.py +0 -224
  214. flock/core/api/runner.py +0 -44
  215. flock/core/api/service.py +0 -214
  216. flock/core/component/__init__.py +0 -15
  217. flock/core/component/agent_component_base.py +0 -309
  218. flock/core/component/evaluation_component.py +0 -62
  219. flock/core/component/routing_component.py +0 -74
  220. flock/core/component/utility_component.py +0 -69
  221. flock/core/config/flock_agent_config.py +0 -58
  222. flock/core/config/scheduled_agent_config.py +0 -40
  223. flock/core/context/context.py +0 -213
  224. flock/core/context/context_manager.py +0 -37
  225. flock/core/context/context_vars.py +0 -10
  226. flock/core/evaluation/utils.py +0 -396
  227. flock/core/execution/batch_executor.py +0 -369
  228. flock/core/execution/evaluation_executor.py +0 -438
  229. flock/core/execution/local_executor.py +0 -31
  230. flock/core/execution/opik_executor.py +0 -103
  231. flock/core/execution/temporal_executor.py +0 -164
  232. flock/core/flock.py +0 -634
  233. flock/core/flock_agent.py +0 -336
  234. flock/core/flock_factory.py +0 -551
  235. flock/core/flock_scheduler.py +0 -166
  236. flock/core/flock_server_manager.py +0 -136
  237. flock/core/interpreter/python_interpreter.py +0 -689
  238. flock/core/mcp/__init__.py +0 -1
  239. flock/core/mcp/flock_mcp_server.py +0 -680
  240. flock/core/mcp/mcp_client_manager.py +0 -201
  241. flock/core/mcp/types/__init__.py +0 -1
  242. flock/core/mixin/dspy_integration.py +0 -403
  243. flock/core/mixin/prompt_parser.py +0 -125
  244. flock/core/orchestration/__init__.py +0 -15
  245. flock/core/orchestration/flock_batch_processor.py +0 -94
  246. flock/core/orchestration/flock_evaluator.py +0 -113
  247. flock/core/orchestration/flock_execution.py +0 -295
  248. flock/core/orchestration/flock_initialization.py +0 -149
  249. flock/core/orchestration/flock_server_manager.py +0 -67
  250. flock/core/orchestration/flock_web_server.py +0 -117
  251. flock/core/registry/__init__.py +0 -45
  252. flock/core/registry/agent_registry.py +0 -69
  253. flock/core/registry/callable_registry.py +0 -139
  254. flock/core/registry/component_discovery.py +0 -142
  255. flock/core/registry/component_registry.py +0 -64
  256. flock/core/registry/config_mapping.py +0 -64
  257. flock/core/registry/decorators.py +0 -137
  258. flock/core/registry/registry_hub.py +0 -205
  259. flock/core/registry/server_registry.py +0 -57
  260. flock/core/registry/type_registry.py +0 -86
  261. flock/core/serialization/__init__.py +0 -13
  262. flock/core/serialization/callable_registry.py +0 -52
  263. flock/core/serialization/flock_serializer.py +0 -832
  264. flock/core/serialization/json_encoder.py +0 -41
  265. flock/core/serialization/secure_serializer.py +0 -175
  266. flock/core/serialization/serializable.py +0 -342
  267. flock/core/serialization/serialization_utils.py +0 -412
  268. flock/core/util/file_path_utils.py +0 -223
  269. flock/core/util/hydrator.py +0 -309
  270. flock/core/util/input_resolver.py +0 -164
  271. flock/core/util/loader.py +0 -59
  272. flock/core/util/splitter.py +0 -219
  273. flock/di.py +0 -27
  274. flock/platform/docker_tools.py +0 -49
  275. flock/platform/jaeger_install.py +0 -86
  276. flock/webapp/__init__.py +0 -1
  277. flock/webapp/app/__init__.py +0 -0
  278. flock/webapp/app/api/__init__.py +0 -0
  279. flock/webapp/app/api/agent_management.py +0 -241
  280. flock/webapp/app/api/execution.py +0 -709
  281. flock/webapp/app/api/flock_management.py +0 -129
  282. flock/webapp/app/api/registry_viewer.py +0 -30
  283. flock/webapp/app/chat.py +0 -665
  284. flock/webapp/app/config.py +0 -104
  285. flock/webapp/app/dependencies.py +0 -117
  286. flock/webapp/app/main.py +0 -1070
  287. flock/webapp/app/middleware.py +0 -113
  288. flock/webapp/app/models_ui.py +0 -7
  289. flock/webapp/app/services/__init__.py +0 -0
  290. flock/webapp/app/services/feedback_file_service.py +0 -363
  291. flock/webapp/app/services/flock_service.py +0 -337
  292. flock/webapp/app/services/sharing_models.py +0 -81
  293. flock/webapp/app/services/sharing_store.py +0 -598
  294. flock/webapp/app/templates/theme_mapper.html +0 -326
  295. flock/webapp/app/theme_mapper.py +0 -812
  296. flock/webapp/app/utils.py +0 -85
  297. flock/webapp/run.py +0 -215
  298. flock/webapp/static/css/chat.css +0 -301
  299. flock/webapp/static/css/components.css +0 -167
  300. flock/webapp/static/css/header.css +0 -39
  301. flock/webapp/static/css/layout.css +0 -46
  302. flock/webapp/static/css/sidebar.css +0 -127
  303. flock/webapp/static/css/two-pane.css +0 -48
  304. flock/webapp/templates/base.html +0 -200
  305. flock/webapp/templates/chat.html +0 -152
  306. flock/webapp/templates/chat_settings.html +0 -19
  307. flock/webapp/templates/flock_editor.html +0 -16
  308. flock/webapp/templates/index.html +0 -12
  309. flock/webapp/templates/partials/_agent_detail_form.html +0 -93
  310. flock/webapp/templates/partials/_agent_list.html +0 -18
  311. flock/webapp/templates/partials/_agent_manager_view.html +0 -51
  312. flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
  313. flock/webapp/templates/partials/_chat_container.html +0 -15
  314. flock/webapp/templates/partials/_chat_messages.html +0 -57
  315. flock/webapp/templates/partials/_chat_settings_form.html +0 -85
  316. flock/webapp/templates/partials/_create_flock_form.html +0 -50
  317. flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
  318. flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
  319. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
  320. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
  321. flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
  322. flock/webapp/templates/partials/_env_vars_table.html +0 -23
  323. flock/webapp/templates/partials/_execution_form.html +0 -118
  324. flock/webapp/templates/partials/_execution_view_container.html +0 -28
  325. flock/webapp/templates/partials/_flock_file_list.html +0 -23
  326. flock/webapp/templates/partials/_flock_properties_form.html +0 -52
  327. flock/webapp/templates/partials/_flock_upload_form.html +0 -16
  328. flock/webapp/templates/partials/_header_flock_status.html +0 -5
  329. flock/webapp/templates/partials/_load_manager_view.html +0 -49
  330. flock/webapp/templates/partials/_registry_table.html +0 -25
  331. flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
  332. flock/webapp/templates/partials/_results_display.html +0 -78
  333. flock/webapp/templates/partials/_settings_env_content.html +0 -9
  334. flock/webapp/templates/partials/_settings_theme_content.html +0 -14
  335. flock/webapp/templates/partials/_settings_view.html +0 -36
  336. flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
  337. flock/webapp/templates/partials/_share_link_snippet.html +0 -35
  338. flock/webapp/templates/partials/_sidebar.html +0 -74
  339. flock/webapp/templates/partials/_streaming_results_container.html +0 -195
  340. flock/webapp/templates/partials/_structured_data_view.html +0 -40
  341. flock/webapp/templates/partials/_theme_preview.html +0 -36
  342. flock/webapp/templates/registry_viewer.html +0 -84
  343. flock/webapp/templates/shared_run_page.html +0 -140
  344. flock/workflow/__init__.py +0 -0
  345. flock/workflow/activities.py +0 -196
  346. flock/workflow/agent_activities.py +0 -24
  347. flock/workflow/agent_execution_activity.py +0 -202
  348. flock/workflow/flock_workflow.py +0 -214
  349. flock/workflow/temporal_config.py +0 -96
  350. flock/workflow/temporal_setup.py +0 -68
  351. flock_core-0.5.0b27.dist-info/METADATA +0 -274
  352. flock_core-0.5.0b27.dist-info/RECORD +0 -559
  353. flock_core-0.5.0b27.dist-info/entry_points.txt +0 -2
  354. /flock/{core/logging → logging}/formatters/themes.py +0 -0
  355. /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
  356. /flock/{core/mcp → mcp}/util/__init__.py +0 -0
  357. {flock_core-0.5.0b27.dist-info → flock_core-0.5.0b50.dist-info}/WHEEL +0 -0
flock/agent.py ADDED
@@ -0,0 +1,678 @@
1
+ """Agent definitions and fluent builder APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from flock.artifacts import Artifact, ArtifactSpec
13
+ from flock.logging.logging import get_logger
14
+ from flock.registry import function_registry, type_registry
15
+ from flock.runtime import Context, EvalInputs, EvalResult
16
+ from flock.subscription import BatchSpec, JoinSpec, Subscription, TextPredicate
17
+ from flock.visibility import AgentIdentity, Visibility, ensure_visibility, only_for
18
+
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ if TYPE_CHECKING: # pragma: no cover - type hints only
23
+ from collections.abc import Callable, Iterable, Sequence
24
+
25
+ from flock.components import AgentComponent, EngineComponent
26
+ from flock.orchestrator import Flock
27
+
28
+
29
+ @dataclass
30
+ class AgentOutput:
31
+ spec: ArtifactSpec
32
+ default_visibility: Visibility
33
+
34
+ def apply(
35
+ self,
36
+ data: dict[str, Any],
37
+ *,
38
+ produced_by: str,
39
+ metadata: dict[str, Any] | None = None,
40
+ ) -> Artifact:
41
+ metadata = metadata or {}
42
+ return self.spec.build(
43
+ produced_by=produced_by,
44
+ data=data,
45
+ visibility=metadata.get("visibility", self.default_visibility),
46
+ correlation_id=metadata.get("correlation_id"),
47
+ partition_key=metadata.get("partition_key"),
48
+ tags=metadata.get("tags"),
49
+ version=metadata.get("version", 1),
50
+ )
51
+
52
+
53
+ class Agent:
54
+ """Executable agent constructed via `AgentBuilder`."""
55
+
56
+ def __init__(self, name: str, *, orchestrator: Flock) -> None:
57
+ self.name = name
58
+ self.description: str | None = None
59
+ self._orchestrator = orchestrator
60
+ self.subscriptions: list[Subscription] = []
61
+ self.outputs: list[AgentOutput] = []
62
+ self.utilities: list[AgentComponent] = []
63
+ self.engines: list[EngineComponent] = []
64
+ self.best_of_n: int = 1
65
+ self.best_of_score: Callable[[EvalResult], float] | None = None
66
+ self.max_concurrency: int = 1
67
+ self._semaphore = asyncio.Semaphore(self.max_concurrency)
68
+ self.calls_func: Callable[..., Any] | None = None
69
+ self.tools: set[Callable[..., Any]] = set()
70
+ self.labels: set[str] = set()
71
+ self.tenant_id: str | None = None
72
+ self.model: str | None = None
73
+ self.prevent_self_trigger: bool = True # T065: Prevent infinite feedback loops
74
+ # MCP integration
75
+ self.mcp_server_names: set[str] = set()
76
+
77
+ @property
78
+ def identity(self) -> AgentIdentity:
79
+ return AgentIdentity(name=self.name, labels=self.labels, tenant_id=self.tenant_id)
80
+
81
+ def set_max_concurrency(self, value: int) -> None:
82
+ self.max_concurrency = max(1, value)
83
+ self._semaphore = asyncio.Semaphore(self.max_concurrency)
84
+
85
+ async def run_direct(self, *inputs: BaseModel) -> list[Artifact]:
86
+ return await self._orchestrator.direct_invoke(self, list(inputs))
87
+
88
+ async def execute(self, ctx: Context, artifacts: list[Artifact]) -> list[Artifact]:
89
+ async with self._semaphore:
90
+ try:
91
+ self._resolve_engines()
92
+ self._resolve_utilities()
93
+ await self._run_initialize(ctx)
94
+ processed_inputs = await self._run_pre_consume(ctx, artifacts)
95
+ eval_inputs = EvalInputs(artifacts=processed_inputs, state=dict(ctx.state))
96
+ eval_inputs = await self._run_pre_evaluate(ctx, eval_inputs)
97
+ result = await self._run_engines(ctx, eval_inputs)
98
+ result = await self._run_post_evaluate(ctx, eval_inputs, result)
99
+ outputs = await self._make_outputs(ctx, result)
100
+ await self._run_post_publish(ctx, outputs)
101
+ if self.calls_func:
102
+ await self._invoke_call(ctx, outputs or processed_inputs)
103
+ return outputs
104
+ except Exception as exc:
105
+ await self._run_error(ctx, exc)
106
+ raise
107
+ finally:
108
+ await self._run_terminate(ctx)
109
+
110
+ async def _get_mcp_tools(self, ctx: Context) -> list[Callable]:
111
+ """Lazy-load MCP tools from assigned servers.
112
+
113
+ Architecture Decision: AD001 - Two-Level Architecture
114
+ Agents fetch tools from servers registered at orchestrator level.
115
+
116
+ Architecture Decision: AD003 - Tool Namespacing
117
+ All tools are namespaced as {server}__{tool}.
118
+
119
+ Architecture Decision: AD007 - Graceful Degradation
120
+ If MCP loading fails, returns empty list so agent continues with native tools.
121
+
122
+ Args:
123
+ ctx: Current execution context with agent_id and run_id
124
+
125
+ Returns:
126
+ List of DSPy-compatible tool callables
127
+ """
128
+ if not self.mcp_server_names:
129
+ # No MCP servers assigned to this agent
130
+ return []
131
+
132
+ try:
133
+ # Get the MCP manager from orchestrator
134
+ manager = self._orchestrator.get_mcp_manager()
135
+
136
+ # Import tool wrapper
137
+
138
+ # Fetch tools from all assigned servers
139
+ tools_dict = await manager.get_tools_for_agent(
140
+ agent_id=self.name,
141
+ run_id=ctx.task_id,
142
+ server_names=self.mcp_server_names,
143
+ )
144
+
145
+ # Convert to DSPy tool callables
146
+ dspy_tools = []
147
+ for namespaced_name, tool_info in tools_dict.items():
148
+ tool_info["server_name"]
149
+ flock_tool = tool_info["tool"] # Already a FlockMCPTool
150
+ client = tool_info["client"]
151
+
152
+ # Convert to DSPy tool
153
+ dspy_tool = flock_tool.as_dspy_tool(server=client)
154
+
155
+ # Update name to include namespace
156
+ dspy_tool.name = namespaced_name
157
+
158
+ dspy_tools.append(dspy_tool)
159
+
160
+ return dspy_tools
161
+
162
+ except Exception as e:
163
+ # Architecture Decision: AD007 - Graceful Degradation
164
+ # Agent continues with native tools only
165
+ logger.error(f"Failed to load MCP tools for agent {self.name}: {e}", exc_info=True)
166
+ return []
167
+
168
+ async def _run_initialize(self, ctx: Context) -> None:
169
+ for component in self.utilities:
170
+ await component.on_initialize(self, ctx)
171
+ for engine in self.engines:
172
+ await engine.on_initialize(self, ctx)
173
+
174
+ async def _run_pre_consume(self, ctx: Context, inputs: list[Artifact]) -> list[Artifact]:
175
+ current = inputs
176
+ for component in self.utilities:
177
+ current = await component.on_pre_consume(self, ctx, current)
178
+ return current
179
+
180
+ async def _run_pre_evaluate(self, ctx: Context, inputs: EvalInputs) -> EvalInputs:
181
+ current = inputs
182
+ for component in self.utilities:
183
+ current = await component.on_pre_evaluate(self, ctx, current)
184
+ return current
185
+
186
+ async def _run_engines(self, ctx: Context, inputs: EvalInputs) -> EvalResult:
187
+ engines = self._resolve_engines()
188
+ if not engines:
189
+ return EvalResult(artifacts=inputs.artifacts, state=inputs.state)
190
+
191
+ async def run_chain() -> EvalResult:
192
+ current_inputs = inputs
193
+ accumulated_logs: list[str] = []
194
+ accumulated_metrics: dict[str, float] = {}
195
+ for engine in engines:
196
+ current_inputs = await engine.on_pre_evaluate(self, ctx, current_inputs)
197
+ result = await engine.evaluate(self, ctx, current_inputs)
198
+
199
+ # AUTO-WRAP: If engine returns BaseModel instead of EvalResult, wrap it
200
+ from flock.runtime import EvalResult as ER
201
+
202
+ if isinstance(result, BaseModel) and not isinstance(result, ER):
203
+ result = ER.from_object(result, agent=self)
204
+
205
+ result = await engine.on_post_evaluate(self, ctx, current_inputs, result)
206
+ accumulated_logs.extend(result.logs)
207
+ accumulated_metrics.update(result.metrics)
208
+ merged_state = dict(current_inputs.state)
209
+ merged_state.update(result.state)
210
+ current_inputs = EvalInputs(
211
+ artifacts=result.artifacts or current_inputs.artifacts,
212
+ state=merged_state,
213
+ )
214
+ return EvalResult(
215
+ artifacts=current_inputs.artifacts,
216
+ state=current_inputs.state,
217
+ metrics=accumulated_metrics,
218
+ logs=accumulated_logs,
219
+ )
220
+
221
+ if self.best_of_n <= 1:
222
+ return await run_chain()
223
+
224
+ async with asyncio.TaskGroup() as tg: # Python 3.12
225
+ tasks: list[asyncio.Task[EvalResult]] = []
226
+ for _ in range(self.best_of_n):
227
+ tasks.append(tg.create_task(run_chain()))
228
+ results = [task.result() for task in tasks]
229
+ if not results:
230
+ return EvalResult(artifacts=[], state={})
231
+ if self.best_of_score is None:
232
+ return results[0]
233
+ return max(results, key=self.best_of_score)
234
+
235
+ async def _run_post_evaluate(
236
+ self, ctx: Context, inputs: EvalInputs, result: EvalResult
237
+ ) -> EvalResult:
238
+ current = result
239
+ for component in self.utilities:
240
+ current = await component.on_post_evaluate(self, ctx, inputs, current)
241
+ return current
242
+
243
+ async def _make_outputs(self, ctx: Context, result: EvalResult) -> list[Artifact]:
244
+ if not self.outputs:
245
+ # Utility agents may not publish anything
246
+ return list(result.artifacts)
247
+
248
+ produced: list[Artifact] = []
249
+ for output_decl in self.outputs:
250
+ payload = self._select_payload(output_decl, result)
251
+ if payload is None:
252
+ continue
253
+ metadata = {
254
+ "correlation_id": ctx.correlation_id # Need to add this to Context!
255
+ }
256
+ artifact = output_decl.apply(payload, produced_by=self.name, metadata=metadata)
257
+ produced.append(artifact)
258
+ await ctx.board.publish(artifact)
259
+ return produced
260
+
261
+ async def _run_post_publish(self, ctx: Context, artifacts: Sequence[Artifact]) -> None:
262
+ for artifact in artifacts:
263
+ for component in self.utilities:
264
+ await component.on_post_publish(self, ctx, artifact)
265
+
266
+ async def _invoke_call(self, ctx: Context, artifacts: Sequence[Artifact]) -> None:
267
+ func = self.calls_func
268
+ if func is None:
269
+ return
270
+ if not artifacts:
271
+ return
272
+ first = artifacts[0]
273
+ model_cls = type_registry.resolve(first.type)
274
+ payload = model_cls(**first.payload)
275
+ maybe_coro = func(payload)
276
+ if asyncio.iscoroutine(maybe_coro): # pragma: no cover - optional async support
277
+ await maybe_coro
278
+
279
+ async def _run_error(self, ctx: Context, error: Exception) -> None:
280
+ for component in self.utilities:
281
+ await component.on_error(self, ctx, error)
282
+ for engine in self.engines:
283
+ await engine.on_error(self, ctx, error)
284
+
285
+ async def _run_terminate(self, ctx: Context) -> None:
286
+ for component in self.utilities:
287
+ await component.on_terminate(self, ctx)
288
+ for engine in self.engines:
289
+ await engine.on_terminate(self, ctx)
290
+
291
+ def _resolve_engines(self) -> list[EngineComponent]:
292
+ if self.engines:
293
+ return self.engines
294
+ try:
295
+ from flock.engines import DSPyEngine
296
+ except Exception: # pragma: no cover - optional dependency issues
297
+ return []
298
+
299
+ default_engine = DSPyEngine(
300
+ model=self._orchestrator.model or os.getenv("DEFAULT_MODEL", "openai/gpt-4o-mini"),
301
+ instructions=self.description,
302
+ )
303
+ self.engines = [default_engine]
304
+ return self.engines
305
+
306
+ def _resolve_utilities(self) -> list[AgentComponent]:
307
+ if self.utilities:
308
+ return self.utilities
309
+ try:
310
+ from flock.utility.output_utility_component import (
311
+ OutputUtilityComponent,
312
+ )
313
+ except Exception: # pragma: no cover - optional dependency issues
314
+ return []
315
+
316
+ default_component = OutputUtilityComponent()
317
+ self.utilities = [default_component]
318
+ return self.utilities
319
+
320
+ def _select_payload(
321
+ self, output_decl: AgentOutput, result: EvalResult
322
+ ) -> dict[str, Any] | None:
323
+ from flock.registry import type_registry
324
+
325
+ if not result.artifacts:
326
+ return None
327
+
328
+ # Normalize the expected type name to canonical form
329
+ expected_canonical = type_registry.resolve_name(output_decl.spec.type_name)
330
+
331
+ for artifact in result.artifacts:
332
+ # Normalize artifact type name to canonical form for comparison
333
+ try:
334
+ artifact_canonical = type_registry.resolve_name(artifact.type)
335
+ if artifact_canonical == expected_canonical:
336
+ return artifact.payload
337
+ except Exception:
338
+ # If normalization fails, fall back to direct comparison
339
+ if artifact.type == output_decl.spec.type_name:
340
+ return artifact.payload
341
+
342
+ # Fallback to state entries keyed by type name
343
+ maybe_data = result.state.get(output_decl.spec.type_name)
344
+ if isinstance(maybe_data, dict):
345
+ return maybe_data
346
+ return None
347
+
348
+
349
+ class AgentBuilder:
350
+ """Fluent builder that also acts as the runtime agent handle."""
351
+
352
+ def __init__(self, orchestrator: Flock, name: str) -> None:
353
+ self._orchestrator = orchestrator
354
+ self._agent = Agent(name, orchestrator=orchestrator)
355
+ self._agent.model = orchestrator.model
356
+ orchestrator.register_agent(self._agent)
357
+
358
+ # Fluent configuration -------------------------------------------------
359
+
360
+ def description(self, text: str) -> AgentBuilder:
361
+ self._agent.description = text
362
+ return self
363
+
364
+ def consumes(
365
+ self,
366
+ *types: type[BaseModel],
367
+ where: Callable[[BaseModel], bool] | Sequence[Callable[[BaseModel], bool]] | None = None,
368
+ text: str | None = None,
369
+ min_p: float = 0.0,
370
+ from_agents: Iterable[str] | None = None,
371
+ channels: Iterable[str] | None = None,
372
+ join: dict | JoinSpec | None = None,
373
+ batch: dict | BatchSpec | None = None,
374
+ delivery: str = "exclusive",
375
+ mode: str = "both",
376
+ priority: int = 0,
377
+ ) -> AgentBuilder:
378
+ predicates: Sequence[Callable[[BaseModel], bool]] | None
379
+ if where is None:
380
+ predicates = None
381
+ elif callable(where):
382
+ predicates = [where]
383
+ else:
384
+ predicates = list(where)
385
+
386
+ join_spec = self._normalize_join(join)
387
+ batch_spec = self._normalize_batch(batch)
388
+ text_predicates = [TextPredicate(text=text, min_p=min_p)] if text else []
389
+ subscription = Subscription(
390
+ agent_name=self._agent.name,
391
+ types=types,
392
+ where=predicates,
393
+ text_predicates=text_predicates,
394
+ from_agents=from_agents,
395
+ channels=channels,
396
+ join=join_spec,
397
+ batch=batch_spec,
398
+ delivery=delivery,
399
+ mode=mode,
400
+ priority=priority,
401
+ )
402
+ self._agent.subscriptions.append(subscription)
403
+ return self
404
+
405
+ def publishes(
406
+ self, *types: type[BaseModel], visibility: Visibility | None = None
407
+ ) -> PublishBuilder:
408
+ outputs = []
409
+ for model in types:
410
+ spec = ArtifactSpec.from_model(model)
411
+ output = AgentOutput(spec=spec, default_visibility=ensure_visibility(visibility))
412
+ self._agent.outputs.append(output)
413
+ outputs.append(output)
414
+ # T074: Validate configuration after adding outputs
415
+ self._validate_self_trigger_risk()
416
+ return PublishBuilder(self, outputs)
417
+
418
+ def with_utilities(self, *components: AgentComponent) -> AgentBuilder:
419
+ self._agent.utilities.extend(components)
420
+ return self
421
+
422
+ def with_engines(self, *engines: EngineComponent) -> AgentBuilder:
423
+ self._agent.engines.extend(engines)
424
+ return self
425
+
426
+ def best_of(self, n: int, score: Callable[[EvalResult], float]) -> AgentBuilder:
427
+ self._agent.best_of_n = max(1, n)
428
+ self._agent.best_of_score = score
429
+ # T074: Validate best_of value
430
+ self._validate_best_of(n)
431
+ return self
432
+
433
+ def max_concurrency(self, n: int) -> AgentBuilder:
434
+ self._agent.set_max_concurrency(n)
435
+ # T074: Validate concurrency value
436
+ self._validate_concurrency(n)
437
+ return self
438
+
439
+ def calls(self, func: Callable[..., Any]) -> AgentBuilder:
440
+ function_registry.register(func)
441
+ self._agent.calls_func = func
442
+ return self
443
+
444
+ def with_tools(self, funcs: Iterable[Callable[..., Any]]) -> AgentBuilder:
445
+ self._agent.tools.update(funcs)
446
+ return self
447
+
448
+ def with_mcps(self, server_names: Iterable[str]) -> AgentBuilder:
449
+ """Assign MCP servers to this agent.
450
+
451
+ Architecture Decision: AD001 - Two-Level Architecture
452
+ Agents reference servers registered at orchestrator level.
453
+
454
+ Args:
455
+ server_names: Names of MCP servers this agent should use
456
+
457
+ Returns:
458
+ self for method chaining
459
+
460
+ Raises:
461
+ ValueError: If any server name is not registered with orchestrator
462
+
463
+ Example:
464
+ >>> agent = (
465
+ ... orchestrator.agent("file_agent")
466
+ ... .with_mcps(["filesystem", "github"])
467
+ ... .build()
468
+ ... )
469
+ """
470
+ # Convert to set for efficient lookup
471
+ server_set = set(server_names)
472
+
473
+ # Validate all servers exist in orchestrator
474
+ registered_servers = set(self._orchestrator._mcp_configs.keys())
475
+ invalid_servers = server_set - registered_servers
476
+
477
+ if invalid_servers:
478
+ available = list(registered_servers) if registered_servers else ["none"]
479
+ raise ValueError(
480
+ f"MCP servers not registered: {invalid_servers}. "
481
+ f"Available servers: {available}. "
482
+ f"Register servers using orchestrator.add_mcp() first."
483
+ )
484
+
485
+ # Store in agent
486
+ self._agent.mcp_server_names = server_set
487
+
488
+ return self
489
+
490
+ def labels(self, *labels: str) -> AgentBuilder:
491
+ self._agent.labels.update(labels)
492
+ return self
493
+
494
+ def tenant(self, tenant_id: str) -> AgentBuilder:
495
+ self._agent.tenant_id = tenant_id
496
+ return self
497
+
498
+ def prevent_self_trigger(self, enabled: bool = True) -> AgentBuilder:
499
+ """Prevent agent from being triggered by its own outputs.
500
+
501
+ When enabled (default), the orchestrator will skip scheduling this agent
502
+ for artifacts it produced itself. This prevents infinite feedback loops
503
+ when an agent consumes and publishes the same type.
504
+
505
+ Args:
506
+ enabled: True to prevent self-triggering (safe default),
507
+ False to allow feedback loops (advanced use case)
508
+
509
+ Returns:
510
+ AgentBuilder for method chaining
511
+
512
+ Example:
513
+ # Safe by default (recommended)
514
+ agent.consumes(Document).publishes(Document)
515
+ # Won't trigger on own outputs ✅
516
+
517
+ # Explicit feedback loop (use with caution!)
518
+ agent.consumes(Data, where=lambda d: d.depth < 10)
519
+ .publishes(Data)
520
+ .prevent_self_trigger(False) # Acknowledge risk
521
+ """
522
+ self._agent.prevent_self_trigger = enabled
523
+ return self
524
+
525
+ # Runtime helpers ------------------------------------------------------
526
+
527
+ def run(self, *inputs: BaseModel) -> RunHandle:
528
+ return RunHandle(self._agent, list(inputs))
529
+
530
+ def then(self, other: AgentBuilder) -> Pipeline:
531
+ return Pipeline([self, other])
532
+
533
+ # Validation -----------------------------------------------------------
534
+
535
+ def _validate_self_trigger_risk(self) -> None:
536
+ """T074: Warn if agent consumes and publishes same type (feedback loop risk)."""
537
+ from flock.logging.logging import get_logger
538
+
539
+ logger = get_logger(__name__)
540
+
541
+ # Get types agent consumes
542
+ consuming_types = set()
543
+ for sub in self._agent.subscriptions:
544
+ consuming_types.update(sub.type_names)
545
+
546
+ # Get types agent publishes
547
+ publishing_types = {output.spec.type_name for output in self._agent.outputs}
548
+
549
+ # Check for overlap
550
+ overlap = consuming_types.intersection(publishing_types)
551
+ if overlap and self._agent.prevent_self_trigger:
552
+ logger.warning(
553
+ f"Agent '{self._agent.name}' consumes and publishes {overlap}. "
554
+ f"Feedback loop risk detected. Agent has prevent_self_trigger=True (safe), "
555
+ f"but consider adding filtering: .consumes(Type, where=lambda x: ...) "
556
+ f"or use .prevent_self_trigger(False) for intentional feedback."
557
+ )
558
+
559
+ def _validate_best_of(self, n: int) -> None:
560
+ """T074: Warn if best_of value is excessively high."""
561
+ from flock.logging.logging import get_logger
562
+
563
+ logger = get_logger(__name__)
564
+
565
+ if n > 100:
566
+ logger.warning(
567
+ f"Agent '{self._agent.name}' has best_of({n}) which is very high. "
568
+ f"Typical values are 3-10. High values increase cost and latency. "
569
+ f"Consider reducing unless you have specific requirements."
570
+ )
571
+
572
+ def _validate_concurrency(self, n: int) -> None:
573
+ """T074: Warn if max_concurrency is excessively high."""
574
+ from flock.logging.logging import get_logger
575
+
576
+ logger = get_logger(__name__)
577
+
578
+ if n > 1000:
579
+ logger.warning(
580
+ f"Agent '{self._agent.name}' has max_concurrency({n}) which is very high. "
581
+ f"Typical values are 1-50. Excessive concurrency may cause resource issues. "
582
+ f"Consider reducing unless you have specific infrastructure."
583
+ )
584
+
585
+ # Utility --------------------------------------------------------------
586
+
587
+ def _normalize_join(self, value: dict | JoinSpec | None) -> JoinSpec | None:
588
+ if value is None or isinstance(value, JoinSpec):
589
+ return value
590
+ return JoinSpec(
591
+ kind=value.get("kind", "all_of"),
592
+ window=float(value.get("window", 0.0)),
593
+ by=value.get("by"),
594
+ )
595
+
596
+ def _normalize_batch(self, value: dict | BatchSpec | None) -> BatchSpec | None:
597
+ if value is None or isinstance(value, BatchSpec):
598
+ return value
599
+ return BatchSpec(
600
+ size=int(value.get("size", 1)),
601
+ within=float(value.get("within", 0.0)),
602
+ by=value.get("by"),
603
+ )
604
+
605
+ # Properties -----------------------------------------------------------
606
+
607
+ @property
608
+ def name(self) -> str:
609
+ return self._agent.name
610
+
611
+ @property
612
+ def agent(self) -> Agent:
613
+ return self._agent
614
+
615
+
616
+ class PublishBuilder:
617
+ """Helper returned by `.publishes(...)` to support `.only_for` sugar."""
618
+
619
+ def __init__(self, parent: AgentBuilder, outputs: Sequence[AgentOutput]) -> None:
620
+ self._parent = parent
621
+ self._outputs = list(outputs)
622
+
623
+ def only_for(self, *agent_names: str) -> AgentBuilder:
624
+ visibility = only_for(*agent_names)
625
+ for output in self._outputs:
626
+ output.default_visibility = visibility
627
+ return self._parent
628
+
629
+ def visibility(self, value: Visibility) -> AgentBuilder:
630
+ for output in self._outputs:
631
+ output.default_visibility = value
632
+ return self._parent
633
+
634
+ def __getattr__(self, item):
635
+ return getattr(self._parent, item)
636
+
637
+
638
+ class RunHandle:
639
+ """Represents a chained run starting from a given agent."""
640
+
641
+ def __init__(self, agent: Agent, inputs: list[BaseModel]) -> None:
642
+ self.agent = agent
643
+ self.inputs = inputs
644
+ self._chain: list[Agent] = [agent]
645
+
646
+ def then(self, builder: AgentBuilder) -> RunHandle:
647
+ self._chain.append(builder.agent)
648
+ return self
649
+
650
+ async def execute(self) -> list[Artifact]:
651
+ orchestrator = self.agent._orchestrator
652
+ artifacts = await orchestrator.direct_invoke(self.agent, self.inputs)
653
+ for agent in self._chain[1:]:
654
+ artifacts = await orchestrator.direct_invoke(agent, artifacts)
655
+ return artifacts
656
+
657
+
658
+ class Pipeline:
659
+ def __init__(self, builders: Sequence[AgentBuilder]) -> None:
660
+ self.builders = list(builders)
661
+
662
+ def then(self, builder: AgentBuilder) -> Pipeline:
663
+ self.builders.append(builder)
664
+ return self
665
+
666
+ async def execute(self) -> list[Artifact]:
667
+ orchestrator = self.builders[0].agent._orchestrator
668
+ artifacts: list[Artifact] = []
669
+ for builder in self.builders:
670
+ inputs = artifacts if artifacts else []
671
+ artifacts = await orchestrator.direct_invoke(builder.agent, inputs)
672
+ return artifacts
673
+
674
+
675
+ __all__ = [
676
+ "Agent",
677
+ "AgentBuilder",
678
+ ]