flock-core 0.5.0b28__py3-none-any.whl → 0.5.56b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/__init__.py +12 -217
- flock/agent.py +678 -0
- flock/api/themes.py +71 -0
- flock/artifacts.py +79 -0
- flock/cli.py +75 -0
- flock/components.py +173 -0
- flock/dashboard/__init__.py +28 -0
- flock/dashboard/collector.py +283 -0
- flock/dashboard/events.py +182 -0
- flock/dashboard/launcher.py +230 -0
- flock/dashboard/service.py +537 -0
- flock/dashboard/websocket.py +235 -0
- flock/engines/__init__.py +6 -0
- flock/engines/dspy_engine.py +856 -0
- flock/examples.py +128 -0
- flock/{core/util → helper}/cli_helper.py +4 -3
- flock/{core/logging → logging}/__init__.py +2 -3
- flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
- flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
- flock/{core/logging → logging}/formatters/themed_formatter.py +69 -115
- flock/{core/logging → logging}/logging.py +77 -61
- flock/{core/logging → logging}/telemetry.py +20 -26
- flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
- flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +6 -9
- flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
- flock/{core/logging → logging}/trace_and_logged.py +20 -24
- flock/mcp/__init__.py +91 -0
- flock/{core/mcp/mcp_client.py → mcp/client.py} +103 -154
- flock/{core/mcp/mcp_config.py → mcp/config.py} +62 -117
- flock/mcp/manager.py +255 -0
- flock/mcp/servers/sse/__init__.py +1 -1
- flock/mcp/servers/sse/flock_sse_server.py +11 -53
- flock/mcp/servers/stdio/__init__.py +1 -1
- flock/mcp/servers/stdio/flock_stdio_server.py +8 -48
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +17 -62
- flock/mcp/servers/websockets/flock_websocket_server.py +7 -40
- flock/{core/mcp/flock_mcp_tool.py → mcp/tool.py} +16 -26
- flock/mcp/types/__init__.py +42 -0
- flock/{core/mcp → mcp}/types/callbacks.py +9 -15
- flock/{core/mcp → mcp}/types/factories.py +7 -6
- flock/{core/mcp → mcp}/types/handlers.py +13 -18
- flock/{core/mcp → mcp}/types/types.py +70 -74
- flock/{core/mcp → mcp}/util/helpers.py +1 -1
- flock/orchestrator.py +645 -0
- flock/registry.py +148 -0
- flock/runtime.py +262 -0
- flock/service.py +140 -0
- flock/store.py +69 -0
- flock/subscription.py +111 -0
- flock/themes/andromeda.toml +1 -1
- flock/themes/apple-system-colors.toml +1 -1
- flock/themes/arcoiris.toml +1 -1
- flock/themes/atomonelight.toml +1 -1
- flock/themes/ayu copy.toml +1 -1
- flock/themes/ayu-light.toml +1 -1
- flock/themes/belafonte-day.toml +1 -1
- flock/themes/belafonte-night.toml +1 -1
- flock/themes/blulocodark.toml +1 -1
- flock/themes/breeze.toml +1 -1
- flock/themes/broadcast.toml +1 -1
- flock/themes/brogrammer.toml +1 -1
- flock/themes/builtin-dark.toml +1 -1
- flock/themes/builtin-pastel-dark.toml +1 -1
- flock/themes/catppuccin-latte.toml +1 -1
- flock/themes/catppuccin-macchiato.toml +1 -1
- flock/themes/catppuccin-mocha.toml +1 -1
- flock/themes/cga.toml +1 -1
- flock/themes/chalk.toml +1 -1
- flock/themes/ciapre.toml +1 -1
- flock/themes/coffee-theme.toml +1 -1
- flock/themes/cyberpunkscarletprotocol.toml +1 -1
- flock/themes/dark+.toml +1 -1
- flock/themes/darkermatrix.toml +1 -1
- flock/themes/darkside.toml +1 -1
- flock/themes/desert.toml +1 -1
- flock/themes/django.toml +1 -1
- flock/themes/djangosmooth.toml +1 -1
- flock/themes/doomone.toml +1 -1
- flock/themes/dotgov.toml +1 -1
- flock/themes/dracula+.toml +1 -1
- flock/themes/duckbones.toml +1 -1
- flock/themes/encom.toml +1 -1
- flock/themes/espresso.toml +1 -1
- flock/themes/everblush.toml +1 -1
- flock/themes/fairyfloss.toml +1 -1
- flock/themes/fideloper.toml +1 -1
- flock/themes/fishtank.toml +1 -1
- flock/themes/flexoki-light.toml +1 -1
- flock/themes/floraverse.toml +1 -1
- flock/themes/framer.toml +1 -1
- flock/themes/galizur.toml +1 -1
- flock/themes/github.toml +1 -1
- flock/themes/grass.toml +1 -1
- flock/themes/grey-green.toml +1 -1
- flock/themes/gruvboxlight.toml +1 -1
- flock/themes/guezwhoz.toml +1 -1
- flock/themes/harper.toml +1 -1
- flock/themes/hax0r-blue.toml +1 -1
- flock/themes/hopscotch.256.toml +1 -1
- flock/themes/ic-green-ppl.toml +1 -1
- flock/themes/iceberg-dark.toml +1 -1
- flock/themes/japanesque.toml +1 -1
- flock/themes/jubi.toml +1 -1
- flock/themes/kibble.toml +1 -1
- flock/themes/kolorit.toml +1 -1
- flock/themes/kurokula.toml +1 -1
- flock/themes/materialdesigncolors.toml +1 -1
- flock/themes/matrix.toml +1 -1
- flock/themes/mellifluous.toml +1 -1
- flock/themes/midnight-in-mojave.toml +1 -1
- flock/themes/monokai-remastered.toml +1 -1
- flock/themes/monokai-soda.toml +1 -1
- flock/themes/neon.toml +1 -1
- flock/themes/neopolitan.toml +1 -1
- flock/themes/nord-light.toml +1 -1
- flock/themes/ocean.toml +1 -1
- flock/themes/onehalfdark.toml +1 -1
- flock/themes/onehalflight.toml +1 -1
- flock/themes/palenighthc.toml +1 -1
- flock/themes/paulmillr.toml +1 -1
- flock/themes/pencildark.toml +1 -1
- flock/themes/pnevma.toml +1 -1
- flock/themes/purple-rain.toml +1 -1
- flock/themes/purplepeter.toml +1 -1
- flock/themes/raycast-dark.toml +1 -1
- flock/themes/red-sands.toml +1 -1
- flock/themes/relaxed.toml +1 -1
- flock/themes/retro.toml +1 -1
- flock/themes/rose-pine.toml +1 -1
- flock/themes/royal.toml +1 -1
- flock/themes/ryuuko.toml +1 -1
- flock/themes/sakura.toml +1 -1
- flock/themes/scarlet-protocol.toml +1 -1
- flock/themes/seoulbones-dark.toml +1 -1
- flock/themes/shades-of-purple.toml +1 -1
- flock/themes/smyck.toml +1 -1
- flock/themes/softserver.toml +1 -1
- flock/themes/solarized-darcula.toml +1 -1
- flock/themes/square.toml +1 -1
- flock/themes/sugarplum.toml +1 -1
- flock/themes/thayer-bright.toml +1 -1
- flock/themes/tokyonight.toml +1 -1
- flock/themes/tomorrow.toml +1 -1
- flock/themes/ubuntu.toml +1 -1
- flock/themes/ultradark.toml +1 -1
- flock/themes/ultraviolent.toml +1 -1
- flock/themes/unikitty.toml +1 -1
- flock/themes/urple.toml +1 -1
- flock/themes/vesper.toml +1 -1
- flock/themes/vimbones.toml +1 -1
- flock/themes/wildcherry.toml +1 -1
- flock/themes/wilmersdorf.toml +1 -1
- flock/themes/wryan.toml +1 -1
- flock/themes/xcodedarkhc.toml +1 -1
- flock/themes/xcodelight.toml +1 -1
- flock/themes/zenbones-light.toml +1 -1
- flock/themes/zenwritten-dark.toml +1 -1
- flock/utilities.py +301 -0
- flock/{components/utility → utility}/output_utility_component.py +68 -53
- flock/visibility.py +107 -0
- flock_core-0.5.56b0.dist-info/METADATA +747 -0
- flock_core-0.5.56b0.dist-info/RECORD +398 -0
- flock_core-0.5.56b0.dist-info/entry_points.txt +2 -0
- {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/licenses/LICENSE +1 -1
- flock/adapter/__init__.py +0 -14
- flock/adapter/azure_adapter.py +0 -68
- flock/adapter/chroma_adapter.py +0 -73
- flock/adapter/faiss_adapter.py +0 -97
- flock/adapter/pinecone_adapter.py +0 -51
- flock/adapter/vector_base.py +0 -47
- flock/cli/assets/release_notes.md +0 -140
- flock/cli/config.py +0 -8
- flock/cli/constants.py +0 -36
- flock/cli/create_agent.py +0 -1
- flock/cli/create_flock.py +0 -280
- flock/cli/execute_flock.py +0 -620
- flock/cli/load_agent.py +0 -1
- flock/cli/load_examples.py +0 -1
- flock/cli/load_flock.py +0 -192
- flock/cli/load_release_notes.py +0 -20
- flock/cli/loaded_flock_cli.py +0 -254
- flock/cli/manage_agents.py +0 -459
- flock/cli/registry_management.py +0 -889
- flock/cli/runner.py +0 -41
- flock/cli/settings.py +0 -857
- flock/cli/utils.py +0 -135
- flock/cli/view_results.py +0 -29
- flock/cli/yaml_editor.py +0 -396
- flock/components/__init__.py +0 -30
- flock/components/evaluation/__init__.py +0 -9
- flock/components/evaluation/declarative_evaluation_component.py +0 -606
- flock/components/routing/__init__.py +0 -15
- flock/components/routing/conditional_routing_component.py +0 -494
- flock/components/routing/default_routing_component.py +0 -103
- flock/components/routing/llm_routing_component.py +0 -206
- flock/components/utility/__init__.py +0 -22
- flock/components/utility/example_utility_component.py +0 -250
- flock/components/utility/feedback_utility_component.py +0 -206
- flock/components/utility/memory_utility_component.py +0 -550
- flock/components/utility/metrics_utility_component.py +0 -700
- flock/config.py +0 -61
- flock/core/__init__.py +0 -110
- flock/core/agent/__init__.py +0 -16
- flock/core/agent/default_agent.py +0 -216
- flock/core/agent/flock_agent_components.py +0 -104
- flock/core/agent/flock_agent_execution.py +0 -101
- flock/core/agent/flock_agent_integration.py +0 -260
- flock/core/agent/flock_agent_lifecycle.py +0 -186
- flock/core/agent/flock_agent_serialization.py +0 -381
- flock/core/api/__init__.py +0 -10
- flock/core/api/custom_endpoint.py +0 -45
- flock/core/api/endpoints.py +0 -254
- flock/core/api/main.py +0 -162
- flock/core/api/models.py +0 -97
- flock/core/api/run_store.py +0 -224
- flock/core/api/runner.py +0 -44
- flock/core/api/service.py +0 -214
- flock/core/component/__init__.py +0 -15
- flock/core/component/agent_component_base.py +0 -309
- flock/core/component/evaluation_component.py +0 -62
- flock/core/component/routing_component.py +0 -74
- flock/core/component/utility_component.py +0 -69
- flock/core/config/flock_agent_config.py +0 -58
- flock/core/config/scheduled_agent_config.py +0 -40
- flock/core/context/context.py +0 -213
- flock/core/context/context_manager.py +0 -37
- flock/core/context/context_vars.py +0 -10
- flock/core/evaluation/utils.py +0 -396
- flock/core/execution/batch_executor.py +0 -369
- flock/core/execution/evaluation_executor.py +0 -438
- flock/core/execution/local_executor.py +0 -31
- flock/core/execution/opik_executor.py +0 -103
- flock/core/execution/temporal_executor.py +0 -164
- flock/core/flock.py +0 -634
- flock/core/flock_agent.py +0 -336
- flock/core/flock_factory.py +0 -613
- flock/core/flock_scheduler.py +0 -166
- flock/core/flock_server_manager.py +0 -136
- flock/core/interpreter/python_interpreter.py +0 -689
- flock/core/mcp/__init__.py +0 -1
- flock/core/mcp/flock_mcp_server.py +0 -680
- flock/core/mcp/mcp_client_manager.py +0 -201
- flock/core/mcp/types/__init__.py +0 -1
- flock/core/mixin/dspy_integration.py +0 -403
- flock/core/mixin/prompt_parser.py +0 -125
- flock/core/orchestration/__init__.py +0 -15
- flock/core/orchestration/flock_batch_processor.py +0 -94
- flock/core/orchestration/flock_evaluator.py +0 -113
- flock/core/orchestration/flock_execution.py +0 -295
- flock/core/orchestration/flock_initialization.py +0 -149
- flock/core/orchestration/flock_server_manager.py +0 -67
- flock/core/orchestration/flock_web_server.py +0 -117
- flock/core/registry/__init__.py +0 -45
- flock/core/registry/agent_registry.py +0 -69
- flock/core/registry/callable_registry.py +0 -139
- flock/core/registry/component_discovery.py +0 -142
- flock/core/registry/component_registry.py +0 -64
- flock/core/registry/config_mapping.py +0 -64
- flock/core/registry/decorators.py +0 -137
- flock/core/registry/registry_hub.py +0 -205
- flock/core/registry/server_registry.py +0 -57
- flock/core/registry/type_registry.py +0 -86
- flock/core/serialization/__init__.py +0 -13
- flock/core/serialization/callable_registry.py +0 -52
- flock/core/serialization/flock_serializer.py +0 -832
- flock/core/serialization/json_encoder.py +0 -41
- flock/core/serialization/secure_serializer.py +0 -175
- flock/core/serialization/serializable.py +0 -342
- flock/core/serialization/serialization_utils.py +0 -412
- flock/core/util/file_path_utils.py +0 -223
- flock/core/util/hydrator.py +0 -309
- flock/core/util/input_resolver.py +0 -164
- flock/core/util/loader.py +0 -59
- flock/core/util/splitter.py +0 -219
- flock/di.py +0 -27
- flock/platform/docker_tools.py +0 -49
- flock/platform/jaeger_install.py +0 -86
- flock/webapp/__init__.py +0 -1
- flock/webapp/app/__init__.py +0 -0
- flock/webapp/app/api/__init__.py +0 -0
- flock/webapp/app/api/agent_management.py +0 -241
- flock/webapp/app/api/execution.py +0 -709
- flock/webapp/app/api/flock_management.py +0 -129
- flock/webapp/app/api/registry_viewer.py +0 -30
- flock/webapp/app/chat.py +0 -665
- flock/webapp/app/config.py +0 -104
- flock/webapp/app/dependencies.py +0 -117
- flock/webapp/app/main.py +0 -1070
- flock/webapp/app/middleware.py +0 -113
- flock/webapp/app/models_ui.py +0 -7
- flock/webapp/app/services/__init__.py +0 -0
- flock/webapp/app/services/feedback_file_service.py +0 -363
- flock/webapp/app/services/flock_service.py +0 -337
- flock/webapp/app/services/sharing_models.py +0 -81
- flock/webapp/app/services/sharing_store.py +0 -762
- flock/webapp/app/templates/theme_mapper.html +0 -326
- flock/webapp/app/theme_mapper.py +0 -812
- flock/webapp/app/utils.py +0 -85
- flock/webapp/run.py +0 -215
- flock/webapp/static/css/chat.css +0 -301
- flock/webapp/static/css/components.css +0 -167
- flock/webapp/static/css/header.css +0 -39
- flock/webapp/static/css/layout.css +0 -46
- flock/webapp/static/css/sidebar.css +0 -127
- flock/webapp/static/css/two-pane.css +0 -48
- flock/webapp/templates/base.html +0 -200
- flock/webapp/templates/chat.html +0 -152
- flock/webapp/templates/chat_settings.html +0 -19
- flock/webapp/templates/flock_editor.html +0 -16
- flock/webapp/templates/index.html +0 -12
- flock/webapp/templates/partials/_agent_detail_form.html +0 -93
- flock/webapp/templates/partials/_agent_list.html +0 -18
- flock/webapp/templates/partials/_agent_manager_view.html +0 -51
- flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
- flock/webapp/templates/partials/_chat_container.html +0 -15
- flock/webapp/templates/partials/_chat_messages.html +0 -57
- flock/webapp/templates/partials/_chat_settings_form.html +0 -85
- flock/webapp/templates/partials/_create_flock_form.html +0 -50
- flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
- flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
- flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
- flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
- flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
- flock/webapp/templates/partials/_env_vars_table.html +0 -23
- flock/webapp/templates/partials/_execution_form.html +0 -118
- flock/webapp/templates/partials/_execution_view_container.html +0 -28
- flock/webapp/templates/partials/_flock_file_list.html +0 -23
- flock/webapp/templates/partials/_flock_properties_form.html +0 -52
- flock/webapp/templates/partials/_flock_upload_form.html +0 -16
- flock/webapp/templates/partials/_header_flock_status.html +0 -5
- flock/webapp/templates/partials/_load_manager_view.html +0 -49
- flock/webapp/templates/partials/_registry_table.html +0 -25
- flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
- flock/webapp/templates/partials/_results_display.html +0 -78
- flock/webapp/templates/partials/_settings_env_content.html +0 -9
- flock/webapp/templates/partials/_settings_theme_content.html +0 -14
- flock/webapp/templates/partials/_settings_view.html +0 -36
- flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
- flock/webapp/templates/partials/_share_link_snippet.html +0 -35
- flock/webapp/templates/partials/_sidebar.html +0 -74
- flock/webapp/templates/partials/_streaming_results_container.html +0 -195
- flock/webapp/templates/partials/_structured_data_view.html +0 -40
- flock/webapp/templates/partials/_theme_preview.html +0 -36
- flock/webapp/templates/registry_viewer.html +0 -84
- flock/webapp/templates/shared_run_page.html +0 -140
- flock/workflow/__init__.py +0 -0
- flock/workflow/activities.py +0 -196
- flock/workflow/agent_activities.py +0 -24
- flock/workflow/agent_execution_activity.py +0 -202
- flock/workflow/flock_workflow.py +0 -214
- flock/workflow/temporal_config.py +0 -96
- flock/workflow/temporal_setup.py +0 -68
- flock_core-0.5.0b28.dist-info/METADATA +0 -274
- flock_core-0.5.0b28.dist-info/RECORD +0 -561
- flock_core-0.5.0b28.dist-info/entry_points.txt +0 -2
- /flock/{core/logging → logging}/formatters/themes.py +0 -0
- /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
- /flock/{core/mcp → mcp}/util/__init__.py +0 -0
- {flock_core-0.5.0b28.dist-info → flock_core-0.5.56b0.dist-info}/WHEEL +0 -0
flock/orchestrator.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
"""Blackboard orchestrator and scheduling runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from asyncio import Task
|
|
8
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from flock.agent import Agent, AgentBuilder
|
|
16
|
+
from flock.artifacts import Artifact
|
|
17
|
+
from flock.helper.cli_helper import init_console
|
|
18
|
+
from flock.mcp import (
|
|
19
|
+
FlockMCPClientManager,
|
|
20
|
+
FlockMCPConfiguration,
|
|
21
|
+
FlockMCPConnectionConfiguration,
|
|
22
|
+
FlockMCPFeatureConfiguration,
|
|
23
|
+
ServerParameters,
|
|
24
|
+
)
|
|
25
|
+
from flock.registry import type_registry
|
|
26
|
+
from flock.runtime import Context
|
|
27
|
+
from flock.store import BlackboardStore, InMemoryBlackboardStore
|
|
28
|
+
from flock.visibility import AgentIdentity, PublicVisibility, Visibility
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
import builtins
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BoardHandle:
|
|
36
|
+
"""Handle exposed to components for publishing and inspection."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, orchestrator: Flock) -> None:
|
|
39
|
+
self._orchestrator = orchestrator
|
|
40
|
+
|
|
41
|
+
async def publish(self, artifact: Artifact) -> None:
|
|
42
|
+
await self._orchestrator._persist_and_schedule(artifact)
|
|
43
|
+
|
|
44
|
+
async def get(self, artifact_id) -> Artifact | None:
|
|
45
|
+
return await self._orchestrator.store.get(artifact_id)
|
|
46
|
+
|
|
47
|
+
async def list(self) -> builtins.list[Artifact]:
|
|
48
|
+
return await self._orchestrator.store.list()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Flock:
|
|
52
|
+
def _patch_litellm_proxy_imports(self) -> None:
|
|
53
|
+
"""Stub litellm proxy_server to avoid optional proxy deps when not used.
|
|
54
|
+
|
|
55
|
+
Some litellm versions import `litellm.proxy.proxy_server` during standard logging
|
|
56
|
+
to read `general_settings`, which pulls in optional dependencies like `apscheduler`.
|
|
57
|
+
We provide a stub so imports succeed but cold storage remains disabled.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
import sys
|
|
61
|
+
import types
|
|
62
|
+
|
|
63
|
+
if "litellm.proxy.proxy_server" not in sys.modules:
|
|
64
|
+
stub = types.ModuleType("litellm.proxy.proxy_server")
|
|
65
|
+
# Minimal surface that cold_storage_handler accesses
|
|
66
|
+
stub.general_settings = {}
|
|
67
|
+
sys.modules["litellm.proxy.proxy_server"] = stub
|
|
68
|
+
except Exception: # nosec B110 - Safe to ignore; worst case litellm will log a warning
|
|
69
|
+
# logger.debug(f"Failed to stub litellm proxy_server: {e}")
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
model: str | None = None,
|
|
75
|
+
*,
|
|
76
|
+
store: BlackboardStore | None = None,
|
|
77
|
+
max_agent_iterations: int = 1000,
|
|
78
|
+
) -> None:
|
|
79
|
+
self._patch_litellm_proxy_imports()
|
|
80
|
+
self.model = model
|
|
81
|
+
self.store: BlackboardStore = store or InMemoryBlackboardStore()
|
|
82
|
+
self._agents: dict[str, Agent] = {}
|
|
83
|
+
self._tasks: set[Task[Any]] = set()
|
|
84
|
+
self._processed: set[tuple[str, str]] = set()
|
|
85
|
+
self._lock = asyncio.Lock()
|
|
86
|
+
self.metrics: dict[str, float] = {"artifacts_published": 0, "agent_runs": 0}
|
|
87
|
+
# MCP integration
|
|
88
|
+
self._mcp_configs: dict[str, FlockMCPConfiguration] = {}
|
|
89
|
+
self._mcp_manager: FlockMCPClientManager | None = None
|
|
90
|
+
# T068: Circuit breaker for runaway agents
|
|
91
|
+
self.max_agent_iterations: int = max_agent_iterations
|
|
92
|
+
self._agent_iteration_count: dict[str, int] = {}
|
|
93
|
+
self.is_dashboard: bool = False
|
|
94
|
+
if not model:
|
|
95
|
+
self.model = os.getenv("DEFAULT_MODEL")
|
|
96
|
+
|
|
97
|
+
# Agent management -----------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def agent(self, name: str) -> AgentBuilder:
|
|
100
|
+
if name in self._agents:
|
|
101
|
+
raise ValueError(f"Agent '{name}' already registered.")
|
|
102
|
+
return AgentBuilder(self, name)
|
|
103
|
+
|
|
104
|
+
def register_agent(self, agent: Agent) -> None:
|
|
105
|
+
if agent.name in self._agents:
|
|
106
|
+
raise ValueError(f"Agent '{agent.name}' already registered.")
|
|
107
|
+
self._agents[agent.name] = agent
|
|
108
|
+
|
|
109
|
+
def get_agent(self, name: str) -> Agent:
|
|
110
|
+
return self._agents[name]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def agents(self) -> list[Agent]:
|
|
114
|
+
return list(self._agents.values())
|
|
115
|
+
|
|
116
|
+
# MCP management -------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def add_mcp(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
connection_params: ServerParameters,
|
|
122
|
+
*,
|
|
123
|
+
enable_tools_feature: bool = True,
|
|
124
|
+
enable_prompts_feature: bool = True,
|
|
125
|
+
enable_sampling_feature: bool = True,
|
|
126
|
+
enable_roots_feature: bool = True,
|
|
127
|
+
tool_whitelist: list[str] | None = None,
|
|
128
|
+
allow_all_tools: bool = True,
|
|
129
|
+
read_timeout_seconds: float = 300,
|
|
130
|
+
max_retries: int = 3,
|
|
131
|
+
**kwargs,
|
|
132
|
+
) -> Flock:
|
|
133
|
+
"""Register an MCP server for use by agents.
|
|
134
|
+
|
|
135
|
+
Architecture Decision: AD001 - Two-Level Architecture
|
|
136
|
+
MCP servers are registered at orchestrator level and assigned to agents.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
name: Unique identifier for this MCP server
|
|
140
|
+
connection_params: Server connection parameters
|
|
141
|
+
enable_tools_feature: Enable tool execution
|
|
142
|
+
enable_prompts_feature: Enable prompt templates
|
|
143
|
+
enable_sampling_feature: Enable LLM sampling requests
|
|
144
|
+
enable_roots_feature: Enable filesystem roots
|
|
145
|
+
tool_whitelist: Optional list of tool names to allow
|
|
146
|
+
allow_all_tools: If True, allow all tools (subject to whitelist)
|
|
147
|
+
read_timeout_seconds: Timeout for server communications
|
|
148
|
+
max_retries: Connection retry attempts
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
self for method chaining
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If server name already registered
|
|
155
|
+
"""
|
|
156
|
+
if name in self._mcp_configs:
|
|
157
|
+
raise ValueError(f"MCP server '{name}' is already registered.")
|
|
158
|
+
|
|
159
|
+
# Detect transport type
|
|
160
|
+
from flock.mcp.types import (
|
|
161
|
+
SseServerParameters,
|
|
162
|
+
StdioServerParameters,
|
|
163
|
+
StreamableHttpServerParameters,
|
|
164
|
+
WebsocketServerParameters,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if isinstance(connection_params, StdioServerParameters):
|
|
168
|
+
transport_type = "stdio"
|
|
169
|
+
elif isinstance(connection_params, WebsocketServerParameters):
|
|
170
|
+
transport_type = "websockets"
|
|
171
|
+
elif isinstance(connection_params, SseServerParameters):
|
|
172
|
+
transport_type = "sse"
|
|
173
|
+
elif isinstance(connection_params, StreamableHttpServerParameters):
|
|
174
|
+
transport_type = "streamable_http"
|
|
175
|
+
else:
|
|
176
|
+
transport_type = "custom"
|
|
177
|
+
|
|
178
|
+
# Build configuration
|
|
179
|
+
connection_config = FlockMCPConnectionConfiguration(
|
|
180
|
+
max_retries=max_retries,
|
|
181
|
+
connection_parameters=connection_params,
|
|
182
|
+
transport_type=transport_type,
|
|
183
|
+
read_timeout_seconds=read_timeout_seconds,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
feature_config = FlockMCPFeatureConfiguration(
|
|
187
|
+
tools_enabled=enable_tools_feature,
|
|
188
|
+
prompts_enabled=enable_prompts_feature,
|
|
189
|
+
sampling_enabled=enable_sampling_feature,
|
|
190
|
+
roots_enabled=enable_roots_feature,
|
|
191
|
+
tool_whitelist=tool_whitelist,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
mcp_config = FlockMCPConfiguration(
|
|
195
|
+
name=name,
|
|
196
|
+
allow_all_tools=allow_all_tools,
|
|
197
|
+
connection_config=connection_config,
|
|
198
|
+
feature_config=feature_config,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
self._mcp_configs[name] = mcp_config
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def get_mcp_manager(self) -> FlockMCPClientManager:
|
|
205
|
+
"""Get or create the MCP client manager.
|
|
206
|
+
|
|
207
|
+
Architecture Decision: AD005 - Lazy Connection Establishment
|
|
208
|
+
"""
|
|
209
|
+
if not self._mcp_configs:
|
|
210
|
+
raise RuntimeError("No MCP servers registered. Call add_mcp() first.")
|
|
211
|
+
|
|
212
|
+
if self._mcp_manager is None:
|
|
213
|
+
self._mcp_manager = FlockMCPClientManager(self._mcp_configs)
|
|
214
|
+
|
|
215
|
+
return self._mcp_manager
|
|
216
|
+
|
|
217
|
+
# Runtime --------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
async def run_until_idle(self) -> None:
|
|
220
|
+
while self._tasks:
|
|
221
|
+
await asyncio.sleep(0.01)
|
|
222
|
+
pending = {task for task in self._tasks if not task.done()}
|
|
223
|
+
self._tasks = pending
|
|
224
|
+
# T068: Reset circuit breaker counters when idle
|
|
225
|
+
self._agent_iteration_count.clear()
|
|
226
|
+
|
|
227
|
+
# Automatically shutdown MCP connections when idle
|
|
228
|
+
await self.shutdown()
|
|
229
|
+
|
|
230
|
+
async def direct_invoke(
|
|
231
|
+
self, agent: Agent, inputs: Sequence[BaseModel | Mapping[str, Any] | Artifact]
|
|
232
|
+
) -> list[Artifact]:
|
|
233
|
+
artifacts = [self._normalize_input(value, produced_by="__direct__") for value in inputs]
|
|
234
|
+
for artifact in artifacts:
|
|
235
|
+
self._mark_processed(artifact, agent)
|
|
236
|
+
await self._persist_and_schedule(artifact)
|
|
237
|
+
ctx = Context(board=BoardHandle(self), orchestrator=self, task_id=str(uuid4()))
|
|
238
|
+
self._record_agent_run(agent)
|
|
239
|
+
return await agent.execute(ctx, artifacts)
|
|
240
|
+
|
|
241
|
+
async def arun(self, agent_builder: AgentBuilder, *inputs: BaseModel) -> list[Artifact]:
|
|
242
|
+
artifacts = await self.direct_invoke(agent_builder.agent, list(inputs))
|
|
243
|
+
await self.run_until_idle()
|
|
244
|
+
return artifacts
|
|
245
|
+
|
|
246
|
+
def run(self, agent_builder: AgentBuilder, *inputs: BaseModel) -> list[Artifact]:
|
|
247
|
+
return asyncio.run(self.arun(agent_builder, *inputs))
|
|
248
|
+
|
|
249
|
+
async def shutdown(self) -> None:
|
|
250
|
+
"""Shutdown orchestrator and clean up resources."""
|
|
251
|
+
if self._mcp_manager is not None:
|
|
252
|
+
await self._mcp_manager.cleanup_all()
|
|
253
|
+
self._mcp_manager = None
|
|
254
|
+
|
|
255
|
+
def cli(self) -> Flock:
|
|
256
|
+
# Placeholder for CLI wiring (rich UI in Step 3)
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
async def serve(
|
|
260
|
+
self, *, dashboard: bool = False, host: str = "127.0.0.1", port: int = 8000
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Start HTTP service for the orchestrator (blocking).
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
dashboard: Enable real-time dashboard with WebSocket support (default: False)
|
|
266
|
+
host: Host to bind to (default: "127.0.0.1")
|
|
267
|
+
port: Port to bind to (default: 8000)
|
|
268
|
+
|
|
269
|
+
Examples:
|
|
270
|
+
# Basic HTTP API (no dashboard) - runs until interrupted
|
|
271
|
+
await orchestrator.serve()
|
|
272
|
+
|
|
273
|
+
# With dashboard (WebSocket + browser launch) - runs until interrupted
|
|
274
|
+
await orchestrator.serve(dashboard=True)
|
|
275
|
+
"""
|
|
276
|
+
if not dashboard:
|
|
277
|
+
# Standard service without dashboard
|
|
278
|
+
from flock.service import BlackboardHTTPService
|
|
279
|
+
|
|
280
|
+
service = BlackboardHTTPService(self)
|
|
281
|
+
await service.run_async(host=host, port=port)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Dashboard mode: integrate event collection and WebSocket
|
|
285
|
+
from flock.dashboard.collector import DashboardEventCollector
|
|
286
|
+
from flock.dashboard.launcher import DashboardLauncher
|
|
287
|
+
from flock.dashboard.service import DashboardHTTPService
|
|
288
|
+
from flock.dashboard.websocket import WebSocketManager
|
|
289
|
+
|
|
290
|
+
# Create dashboard components
|
|
291
|
+
websocket_manager = WebSocketManager()
|
|
292
|
+
event_collector = DashboardEventCollector()
|
|
293
|
+
event_collector.set_websocket_manager(websocket_manager)
|
|
294
|
+
|
|
295
|
+
# Store collector reference for agents added later
|
|
296
|
+
self._dashboard_collector = event_collector
|
|
297
|
+
|
|
298
|
+
# Inject event collector into all existing agents
|
|
299
|
+
for agent in self._agents.values():
|
|
300
|
+
# Insert at beginning of utilities list (highest priority)
|
|
301
|
+
agent.utilities.insert(0, event_collector)
|
|
302
|
+
|
|
303
|
+
# Start dashboard launcher (npm process + browser)
|
|
304
|
+
launcher = DashboardLauncher(port=port)
|
|
305
|
+
launcher.start()
|
|
306
|
+
|
|
307
|
+
# Create dashboard HTTP service
|
|
308
|
+
service = DashboardHTTPService(
|
|
309
|
+
orchestrator=self,
|
|
310
|
+
websocket_manager=websocket_manager,
|
|
311
|
+
event_collector=event_collector,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Store launcher for cleanup
|
|
315
|
+
self._dashboard_launcher = launcher
|
|
316
|
+
|
|
317
|
+
# Run service (blocking call)
|
|
318
|
+
try:
|
|
319
|
+
await service.run_async(host=host, port=port)
|
|
320
|
+
finally:
|
|
321
|
+
# Cleanup on exit
|
|
322
|
+
launcher.stop()
|
|
323
|
+
|
|
324
|
+
# Scheduling -----------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
async def publish(
|
|
327
|
+
self,
|
|
328
|
+
obj: BaseModel | dict | Artifact,
|
|
329
|
+
*,
|
|
330
|
+
visibility: Visibility | None = None,
|
|
331
|
+
correlation_id: str | None = None,
|
|
332
|
+
partition_key: str | None = None,
|
|
333
|
+
tags: set[str] | None = None,
|
|
334
|
+
is_dashboard: bool = False,
|
|
335
|
+
) -> Artifact:
|
|
336
|
+
"""Publish an artifact to the blackboard (event-driven).
|
|
337
|
+
|
|
338
|
+
All agents with matching subscriptions will be triggered according to
|
|
339
|
+
their filters (type, predicates, visibility, etc).
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
obj: Object to publish (BaseModel instance, dict, or Artifact)
|
|
343
|
+
visibility: Access control (defaults to PublicVisibility)
|
|
344
|
+
correlation_id: Optional correlation ID for request tracing
|
|
345
|
+
partition_key: Optional partition key for sharding
|
|
346
|
+
tags: Optional tags for channel-based routing
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
The published Artifact
|
|
350
|
+
|
|
351
|
+
Examples:
|
|
352
|
+
>>> # Publish a model instance (recommended)
|
|
353
|
+
>>> task = Task(name="Deploy", priority=5)
|
|
354
|
+
>>> await orchestrator.publish(task)
|
|
355
|
+
|
|
356
|
+
>>> # Publish with custom visibility
|
|
357
|
+
>>> await orchestrator.publish(
|
|
358
|
+
... task,
|
|
359
|
+
... visibility=PrivateVisibility(agents={"admin"})
|
|
360
|
+
... )
|
|
361
|
+
|
|
362
|
+
>>> # Publish with tags for channel routing
|
|
363
|
+
>>> await orchestrator.publish(task, tags={"urgent", "backend"})
|
|
364
|
+
"""
|
|
365
|
+
init_console(clear_screen=True, show_banner=True, model=self.model)
|
|
366
|
+
self.is_dashboard = is_dashboard
|
|
367
|
+
# Handle different input types
|
|
368
|
+
if isinstance(obj, Artifact):
|
|
369
|
+
# Already an artifact - publish as-is
|
|
370
|
+
artifact = obj
|
|
371
|
+
elif isinstance(obj, BaseModel):
|
|
372
|
+
# BaseModel instance - get type from registry
|
|
373
|
+
type_name = type_registry.name_for(type(obj))
|
|
374
|
+
artifact = Artifact(
|
|
375
|
+
type=type_name,
|
|
376
|
+
payload=obj.model_dump(),
|
|
377
|
+
produced_by="external",
|
|
378
|
+
visibility=visibility or PublicVisibility(),
|
|
379
|
+
correlation_id=correlation_id or uuid4(),
|
|
380
|
+
partition_key=partition_key,
|
|
381
|
+
tags=tags or set(),
|
|
382
|
+
)
|
|
383
|
+
elif isinstance(obj, dict):
|
|
384
|
+
# Dict must have 'type' key
|
|
385
|
+
if "type" not in obj:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
"Dict input must contain 'type' key. "
|
|
388
|
+
"Example: {'type': 'Task', 'name': 'foo', 'priority': 5}"
|
|
389
|
+
)
|
|
390
|
+
# Support both {'type': 'X', 'payload': {...}} and {'type': 'X', ...}
|
|
391
|
+
type_name = obj["type"]
|
|
392
|
+
if "payload" in obj:
|
|
393
|
+
payload = obj["payload"]
|
|
394
|
+
else:
|
|
395
|
+
payload = {k: v for k, v in obj.items() if k != "type"}
|
|
396
|
+
|
|
397
|
+
artifact = Artifact(
|
|
398
|
+
type=type_name,
|
|
399
|
+
payload=payload,
|
|
400
|
+
produced_by="external",
|
|
401
|
+
visibility=visibility or PublicVisibility(),
|
|
402
|
+
correlation_id=correlation_id,
|
|
403
|
+
partition_key=partition_key,
|
|
404
|
+
tags=tags or set(),
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
raise TypeError(
|
|
408
|
+
f"Cannot publish object of type {type(obj).__name__}. "
|
|
409
|
+
"Expected BaseModel, dict, or Artifact."
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Persist and schedule matching agents
|
|
413
|
+
await self._persist_and_schedule(artifact)
|
|
414
|
+
return artifact
|
|
415
|
+
|
|
416
|
+
async def publish_many(
|
|
417
|
+
self, objects: Iterable[BaseModel | dict | Artifact], **kwargs
|
|
418
|
+
) -> list[Artifact]:
|
|
419
|
+
"""Publish multiple artifacts at once (event-driven).
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
objects: Iterable of objects to publish
|
|
423
|
+
**kwargs: Passed to each publish() call (visibility, tags, etc)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of published Artifacts
|
|
427
|
+
|
|
428
|
+
Example:
|
|
429
|
+
>>> tasks = [
|
|
430
|
+
... Task(name="Deploy", priority=5),
|
|
431
|
+
... Task(name="Test", priority=3),
|
|
432
|
+
... Task(name="Document", priority=1),
|
|
433
|
+
... ]
|
|
434
|
+
>>> await orchestrator.publish_many(tasks, tags={"sprint-3"})
|
|
435
|
+
"""
|
|
436
|
+
artifacts = []
|
|
437
|
+
for obj in objects:
|
|
438
|
+
artifact = await self.publish(obj, **kwargs)
|
|
439
|
+
artifacts.append(artifact)
|
|
440
|
+
return artifacts
|
|
441
|
+
|
|
442
|
+
# -----------------------------------------------------------------------------
|
|
443
|
+
# NEW DIRECT INVOCATION API - Explicit Control
|
|
444
|
+
# -----------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
async def invoke(
|
|
447
|
+
self,
|
|
448
|
+
agent: Agent | AgentBuilder,
|
|
449
|
+
obj: BaseModel,
|
|
450
|
+
*,
|
|
451
|
+
publish_outputs: bool = True,
|
|
452
|
+
timeout: float | None = None,
|
|
453
|
+
) -> list[Artifact]:
|
|
454
|
+
"""Directly invoke a specific agent (bypasses subscription matching).
|
|
455
|
+
|
|
456
|
+
This executes the agent immediately without checking subscriptions or
|
|
457
|
+
predicates. Useful for testing or synchronous request-response patterns.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
agent: Agent or AgentBuilder to invoke
|
|
461
|
+
obj: Input object (BaseModel instance)
|
|
462
|
+
publish_outputs: If True, publish outputs to blackboard for cascade
|
|
463
|
+
timeout: Optional timeout in seconds
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Artifacts produced by the agent
|
|
467
|
+
|
|
468
|
+
Warning:
|
|
469
|
+
This bypasses subscription filters and predicates. For event-driven
|
|
470
|
+
coordination, use publish() instead.
|
|
471
|
+
|
|
472
|
+
Examples:
|
|
473
|
+
>>> # Testing: Execute agent without triggering others
|
|
474
|
+
>>> results = await orchestrator.invoke(
|
|
475
|
+
... agent,
|
|
476
|
+
... Task(name="test", priority=5),
|
|
477
|
+
... publish_outputs=False
|
|
478
|
+
... )
|
|
479
|
+
|
|
480
|
+
>>> # HTTP endpoint: Execute specific agent, allow cascade
|
|
481
|
+
>>> results = await orchestrator.invoke(
|
|
482
|
+
... movie_agent,
|
|
483
|
+
... Idea(topic="AI", genre="comedy"),
|
|
484
|
+
... publish_outputs=True
|
|
485
|
+
... )
|
|
486
|
+
>>> await orchestrator.run_until_idle()
|
|
487
|
+
"""
|
|
488
|
+
from asyncio import wait_for
|
|
489
|
+
from uuid import uuid4
|
|
490
|
+
|
|
491
|
+
# Get Agent instance
|
|
492
|
+
agent_obj = agent.agent if isinstance(agent, AgentBuilder) else agent
|
|
493
|
+
|
|
494
|
+
# Create artifact (don't publish to blackboard yet)
|
|
495
|
+
type_name = type_registry.name_for(type(obj))
|
|
496
|
+
artifact = Artifact(
|
|
497
|
+
type=type_name,
|
|
498
|
+
payload=obj.model_dump(),
|
|
499
|
+
produced_by="__direct__",
|
|
500
|
+
visibility=PublicVisibility(),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Execute agent directly
|
|
504
|
+
ctx = Context(board=BoardHandle(self), orchestrator=self, task_id=str(uuid4()))
|
|
505
|
+
self._record_agent_run(agent_obj)
|
|
506
|
+
|
|
507
|
+
# Execute with optional timeout
|
|
508
|
+
if timeout:
|
|
509
|
+
execution = agent_obj.execute(ctx, [artifact])
|
|
510
|
+
outputs = await wait_for(execution, timeout=timeout)
|
|
511
|
+
else:
|
|
512
|
+
outputs = await agent_obj.execute(ctx, [artifact])
|
|
513
|
+
|
|
514
|
+
# Optionally publish outputs to blackboard
|
|
515
|
+
if publish_outputs:
|
|
516
|
+
for output in outputs:
|
|
517
|
+
await self._persist_and_schedule(output)
|
|
518
|
+
|
|
519
|
+
return outputs
|
|
520
|
+
|
|
521
|
+
# Keep publish_external as deprecated alias
|
|
522
|
+
async def publish_external(
|
|
523
|
+
self,
|
|
524
|
+
type_name: str,
|
|
525
|
+
payload: dict[str, Any],
|
|
526
|
+
*,
|
|
527
|
+
visibility: Visibility | None = None,
|
|
528
|
+
correlation_id: str | None = None,
|
|
529
|
+
partition_key: str | None = None,
|
|
530
|
+
tags: set[str] | None = None,
|
|
531
|
+
) -> Artifact:
|
|
532
|
+
"""Deprecated: Use publish() instead.
|
|
533
|
+
|
|
534
|
+
This method will be removed in v2.0.
|
|
535
|
+
"""
|
|
536
|
+
import warnings
|
|
537
|
+
|
|
538
|
+
warnings.warn(
|
|
539
|
+
"publish_external() is deprecated. Use publish(obj) instead.",
|
|
540
|
+
DeprecationWarning,
|
|
541
|
+
stacklevel=2,
|
|
542
|
+
)
|
|
543
|
+
return await self.publish(
|
|
544
|
+
{"type": type_name, "payload": payload},
|
|
545
|
+
visibility=visibility,
|
|
546
|
+
correlation_id=correlation_id,
|
|
547
|
+
partition_key=partition_key,
|
|
548
|
+
tags=tags,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
async def _persist_and_schedule(self, artifact: Artifact) -> None:
|
|
552
|
+
await self.store.publish(artifact)
|
|
553
|
+
self.metrics["artifacts_published"] += 1
|
|
554
|
+
await self._schedule_artifact(artifact)
|
|
555
|
+
|
|
556
|
+
async def _schedule_artifact(self, artifact: Artifact) -> None:
|
|
557
|
+
for agent in self.agents:
|
|
558
|
+
identity = agent.identity
|
|
559
|
+
for subscription in agent.subscriptions:
|
|
560
|
+
if not subscription.accepts_events():
|
|
561
|
+
continue
|
|
562
|
+
# T066: Check prevent_self_trigger
|
|
563
|
+
if agent.prevent_self_trigger and artifact.produced_by == agent.name:
|
|
564
|
+
continue # Skip - agent produced this artifact (prevents feedback loops)
|
|
565
|
+
# T068: Circuit breaker - check iteration limit
|
|
566
|
+
iteration_count = self._agent_iteration_count.get(agent.name, 0)
|
|
567
|
+
if iteration_count >= self.max_agent_iterations:
|
|
568
|
+
# Agent hit iteration limit - possible infinite loop
|
|
569
|
+
continue
|
|
570
|
+
if not self._check_visibility(artifact, identity):
|
|
571
|
+
continue
|
|
572
|
+
if not subscription.matches(artifact):
|
|
573
|
+
continue
|
|
574
|
+
if self._seen_before(artifact, agent):
|
|
575
|
+
continue
|
|
576
|
+
# T068: Increment iteration counter
|
|
577
|
+
self._agent_iteration_count[agent.name] = iteration_count + 1
|
|
578
|
+
self._mark_processed(artifact, agent)
|
|
579
|
+
self._schedule_task(agent, [artifact])
|
|
580
|
+
|
|
581
|
+
def _schedule_task(self, agent: Agent, artifacts: list[Artifact]) -> None:
|
|
582
|
+
task = asyncio.create_task(self._run_agent_task(agent, artifacts))
|
|
583
|
+
self._tasks.add(task)
|
|
584
|
+
task.add_done_callback(self._tasks.discard)
|
|
585
|
+
|
|
586
|
+
def _record_agent_run(self, agent: Agent) -> None:
|
|
587
|
+
self.metrics["agent_runs"] += 1
|
|
588
|
+
|
|
589
|
+
def _mark_processed(self, artifact: Artifact, agent: Agent) -> None:
|
|
590
|
+
key = (str(artifact.id), agent.name)
|
|
591
|
+
self._processed.add(key)
|
|
592
|
+
|
|
593
|
+
def _seen_before(self, artifact: Artifact, agent: Agent) -> bool:
|
|
594
|
+
key = (str(artifact.id), agent.name)
|
|
595
|
+
return key in self._processed
|
|
596
|
+
|
|
597
|
+
async def _run_agent_task(self, agent: Agent, artifacts: list[Artifact]) -> None:
|
|
598
|
+
correlation_id = artifacts[0].correlation_id if artifacts else uuid4()
|
|
599
|
+
|
|
600
|
+
ctx = Context(
|
|
601
|
+
board=BoardHandle(self),
|
|
602
|
+
orchestrator=self,
|
|
603
|
+
task_id=str(uuid4()),
|
|
604
|
+
correlation_id=correlation_id, # NEW!
|
|
605
|
+
)
|
|
606
|
+
self._record_agent_run(agent)
|
|
607
|
+
await agent.execute(ctx, artifacts)
|
|
608
|
+
|
|
609
|
+
# Helpers --------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def _normalize_input(
|
|
612
|
+
self, value: BaseModel | Mapping[str, Any] | Artifact, *, produced_by: str
|
|
613
|
+
) -> Artifact:
|
|
614
|
+
if isinstance(value, Artifact):
|
|
615
|
+
return value
|
|
616
|
+
if isinstance(value, BaseModel):
|
|
617
|
+
model_cls = type(value)
|
|
618
|
+
type_name = type_registry.register(model_cls)
|
|
619
|
+
payload = value.model_dump()
|
|
620
|
+
elif isinstance(value, Mapping):
|
|
621
|
+
if "type" not in value:
|
|
622
|
+
raise ValueError("Mapping input must contain 'type'.")
|
|
623
|
+
type_name = value["type"]
|
|
624
|
+
payload = value.get("payload", {})
|
|
625
|
+
else: # pragma: no cover - defensive
|
|
626
|
+
raise TypeError("Unsupported input for direct invoke.")
|
|
627
|
+
return Artifact(type=type_name, payload=payload, produced_by=produced_by)
|
|
628
|
+
|
|
629
|
+
def _check_visibility(self, artifact: Artifact, identity: AgentIdentity) -> bool:
|
|
630
|
+
try:
|
|
631
|
+
return artifact.visibility.allows(identity)
|
|
632
|
+
except AttributeError: # pragma: no cover - fallback for dict vis
|
|
633
|
+
return True
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@asynccontextmanager
|
|
637
|
+
async def start_orchestrator(orchestrator: Flock): # pragma: no cover - CLI helper
|
|
638
|
+
try:
|
|
639
|
+
yield orchestrator
|
|
640
|
+
await orchestrator.run_until_idle()
|
|
641
|
+
finally:
|
|
642
|
+
pass
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
__all__ = ["Flock", "start_orchestrator"]
|