flock-core 0.4.542__py3-none-any.whl → 0.5.0__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 +1079 -0
- flock/api/themes.py +71 -0
- flock/artifacts.py +86 -0
- flock/cli.py +147 -0
- flock/components.py +189 -0
- flock/dashboard/__init__.py +30 -0
- flock/dashboard/collector.py +559 -0
- flock/dashboard/events.py +188 -0
- flock/dashboard/graph_builder.py +563 -0
- flock/dashboard/launcher.py +235 -0
- flock/dashboard/models/graph.py +156 -0
- flock/dashboard/service.py +991 -0
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
- flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
- flock/dashboard/static_v2/index.html +13 -0
- flock/dashboard/websocket.py +246 -0
- flock/engines/__init__.py +6 -0
- flock/engines/dspy_engine.py +932 -0
- flock/examples.py +131 -0
- flock/frontend/README.md +778 -0
- flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
- flock/frontend/index.html +12 -0
- flock/frontend/package-lock.json +4337 -0
- flock/frontend/package.json +48 -0
- flock/frontend/src/App.tsx +139 -0
- flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
- flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
- flock/frontend/src/components/common/BuildInfo.tsx +39 -0
- flock/frontend/src/components/common/EmptyState.module.css +115 -0
- flock/frontend/src/components/common/EmptyState.tsx +128 -0
- flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
- flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
- flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
- flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
- flock/frontend/src/components/controls/PublishControl.css +547 -0
- flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
- flock/frontend/src/components/controls/PublishControl.tsx +432 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +58 -0
- flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
- flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
- flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +374 -0
- flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
- flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
- flock/frontend/src/components/details/RunStatusTab.tsx +348 -0
- flock/frontend/src/components/details/tabs.test.tsx +1015 -0
- flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
- flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
- flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
- flock/frontend/src/components/filters/FilterPills.module.css +220 -0
- flock/frontend/src/components/filters/FilterPills.test.tsx +189 -0
- flock/frontend/src/components/filters/FilterPills.tsx +143 -0
- flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
- flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
- flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
- flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
- flock/frontend/src/components/filters/TagFilter.tsx +21 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +115 -0
- flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +110 -0
- flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
- flock/frontend/src/components/graph/AgentNode.test.tsx +77 -0
- flock/frontend/src/components/graph/AgentNode.tsx +324 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +613 -0
- flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
- flock/frontend/src/components/graph/MessageNode.test.tsx +64 -0
- flock/frontend/src/components/graph/MessageNode.tsx +129 -0
- flock/frontend/src/components/graph/MiniMap.tsx +47 -0
- flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
- flock/frontend/src/components/layout/DashboardLayout.css +420 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +287 -0
- flock/frontend/src/components/layout/Header.module.css +88 -0
- flock/frontend/src/components/layout/Header.tsx +52 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +450 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
- flock/frontend/src/components/modules/JsonAttributeRenderer.tsx +140 -0
- flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +93 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +223 -0
- flock/frontend/src/components/modules/TraceModuleJaeger.tsx +1971 -0
- flock/frontend/src/components/modules/TraceModuleJaegerWrapper.tsx +13 -0
- flock/frontend/src/components/modules/registerModules.ts +29 -0
- flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
- flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
- flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
- flock/frontend/src/components/settings/MultiSelect.tsx +235 -0
- flock/frontend/src/components/settings/SettingsPanel.css +327 -0
- flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
- flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
- flock/frontend/src/components/settings/TracingSettings.tsx +404 -0
- flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
- flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
- flock/frontend/src/hooks/useModulePersistence.ts +154 -0
- flock/frontend/src/hooks/useModules.ts +157 -0
- flock/frontend/src/hooks/usePersistence.ts +141 -0
- flock/frontend/src/main.tsx +13 -0
- flock/frontend/src/services/api.ts +337 -0
- flock/frontend/src/services/graphService.test.ts +330 -0
- flock/frontend/src/services/graphService.ts +75 -0
- flock/frontend/src/services/indexeddb.test.ts +793 -0
- flock/frontend/src/services/indexeddb.ts +848 -0
- flock/frontend/src/services/layout.test.ts +437 -0
- flock/frontend/src/services/layout.ts +357 -0
- flock/frontend/src/services/themeApplicator.ts +140 -0
- flock/frontend/src/services/themeService.ts +77 -0
- flock/frontend/src/services/websocket.ts +650 -0
- flock/frontend/src/store/filterStore.test.ts +250 -0
- flock/frontend/src/store/filterStore.ts +272 -0
- flock/frontend/src/store/graphStore.test.ts +570 -0
- flock/frontend/src/store/graphStore.ts +462 -0
- flock/frontend/src/store/moduleStore.test.ts +253 -0
- flock/frontend/src/store/moduleStore.ts +75 -0
- flock/frontend/src/store/settingsStore.ts +188 -0
- flock/frontend/src/store/streamStore.ts +68 -0
- flock/frontend/src/store/uiStore.test.ts +54 -0
- flock/frontend/src/store/uiStore.ts +122 -0
- flock/frontend/src/store/wsStore.ts +34 -0
- flock/frontend/src/styles/index.css +15 -0
- flock/frontend/src/styles/scrollbar.css +47 -0
- flock/frontend/src/styles/variables.css +488 -0
- flock/frontend/src/test/setup.ts +1 -0
- flock/frontend/src/types/filters.ts +47 -0
- flock/frontend/src/types/graph.ts +95 -0
- flock/frontend/src/types/modules.ts +10 -0
- flock/frontend/src/types/theme.ts +55 -0
- flock/frontend/src/utils/artifacts.ts +24 -0
- flock/frontend/src/utils/mockData.ts +98 -0
- flock/frontend/src/utils/performance.ts +16 -0
- flock/frontend/src/vite-env.d.ts +17 -0
- flock/frontend/tsconfig.json +27 -0
- flock/frontend/tsconfig.node.json +11 -0
- flock/frontend/vite.config.ts +25 -0
- flock/frontend/vitest.config.ts +11 -0
- flock/{core/util → helper}/cli_helper.py +9 -5
- flock/{core/logging → logging}/__init__.py +2 -3
- flock/logging/auto_trace.py +159 -0
- 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 -107
- flock/{core/logging → logging}/logging.py +78 -61
- flock/{core/logging → logging}/telemetry.py +66 -26
- flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
- flock/logging/telemetry_exporter/duckdb_exporter.py +216 -0
- flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +13 -10
- flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
- flock/logging/trace_and_logged.py +304 -0
- flock/mcp/__init__.py +91 -0
- flock/{core/mcp/mcp_client.py → mcp/client.py} +131 -158
- flock/{core/mcp/mcp_config.py → mcp/config.py} +86 -132
- flock/mcp/manager.py +286 -0
- flock/mcp/servers/sse/__init__.py +1 -1
- flock/mcp/servers/sse/flock_sse_server.py +16 -58
- flock/mcp/servers/stdio/__init__.py +1 -1
- flock/mcp/servers/stdio/flock_stdio_server.py +13 -53
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +22 -67
- flock/mcp/servers/websockets/flock_websocket_server.py +12 -45
- flock/{core/mcp/flock_mcp_tool_base.py → mcp/tool.py} +24 -78
- flock/mcp/types/__init__.py +42 -0
- flock/{core/mcp → mcp}/types/callbacks.py +12 -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 +3 -3
- flock/orchestrator.py +970 -0
- flock/registry.py +148 -0
- flock/runtime.py +262 -0
- flock/service.py +277 -0
- flock/store.py +1214 -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/darkmatrix.toml +2 -2
- flock/themes/darkside.toml +1 -1
- flock/themes/deep.toml +2 -2
- 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 +5 -5
- 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/utility/output_utility_component.py +226 -0
- flock/visibility.py +107 -0
- flock_core-0.5.0.dist-info/METADATA +964 -0
- flock_core-0.5.0.dist-info/RECORD +525 -0
- flock_core-0.5.0.dist-info/entry_points.txt +2 -0
- {flock_core-0.4.542.dist-info → flock_core-0.5.0.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/config.py +0 -56
- flock/core/__init__.py +0 -44
- flock/core/api/__init__.py +0 -10
- flock/core/api/custom_endpoint.py +0 -45
- flock/core/api/endpoints.py +0 -262
- flock/core/api/main.py +0 -162
- flock/core/api/models.py +0 -101
- flock/core/api/run_store.py +0 -224
- flock/core/api/runner.py +0 -44
- flock/core/api/service.py +0 -214
- flock/core/config/flock_agent_config.py +0 -11
- flock/core/config/scheduled_agent_config.py +0 -40
- flock/core/context/context.py +0 -214
- flock/core/context/context_manager.py +0 -40
- flock/core/context/context_vars.py +0 -11
- flock/core/evaluation/utils.py +0 -395
- 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 -166
- flock/core/flock.py +0 -1003
- flock/core/flock_agent.py +0 -1258
- flock/core/flock_evaluator.py +0 -60
- flock/core/flock_factory.py +0 -513
- flock/core/flock_module.py +0 -207
- flock/core/flock_registry.py +0 -702
- flock/core/flock_router.py +0 -83
- 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/logging/live_capture.py +0 -137
- flock/core/logging/trace_and_logged.py +0 -59
- flock/core/mcp/__init__.py +0 -1
- flock/core/mcp/flock_mcp_server.py +0 -640
- flock/core/mcp/mcp_client_manager.py +0 -201
- flock/core/mcp/types/__init__.py +0 -1
- flock/core/mixin/dspy_integration.py +0 -445
- flock/core/mixin/prompt_parser.py +0 -125
- flock/core/serialization/__init__.py +0 -13
- flock/core/serialization/callable_registry.py +0 -52
- flock/core/serialization/flock_serializer.py +0 -854
- 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 -409
- flock/core/util/file_path_utils.py +0 -223
- flock/core/util/hydrator.py +0 -309
- flock/core/util/input_resolver.py +0 -141
- flock/core/util/loader.py +0 -59
- flock/core/util/splitter.py +0 -219
- flock/di.py +0 -41
- flock/evaluators/__init__.py +0 -1
- flock/evaluators/declarative/__init__.py +0 -1
- flock/evaluators/declarative/declarative_evaluator.py +0 -217
- flock/evaluators/memory/memory_evaluator.py +0 -90
- flock/evaluators/test/test_case_evaluator.py +0 -38
- flock/evaluators/zep/zep_evaluator.py +0 -59
- flock/modules/__init__.py +0 -1
- flock/modules/assertion/__init__.py +0 -1
- flock/modules/assertion/assertion_module.py +0 -286
- flock/modules/callback/__init__.py +0 -1
- flock/modules/callback/callback_module.py +0 -91
- flock/modules/enterprise_memory/README.md +0 -99
- flock/modules/enterprise_memory/enterprise_memory_module.py +0 -526
- flock/modules/mem0/__init__.py +0 -1
- flock/modules/mem0/mem0_module.py +0 -126
- flock/modules/mem0_async/__init__.py +0 -1
- flock/modules/mem0_async/async_mem0_module.py +0 -126
- flock/modules/memory/__init__.py +0 -1
- flock/modules/memory/memory_module.py +0 -429
- flock/modules/memory/memory_parser.py +0 -125
- flock/modules/memory/memory_storage.py +0 -736
- flock/modules/output/__init__.py +0 -1
- flock/modules/output/output_module.py +0 -196
- flock/modules/performance/__init__.py +0 -1
- flock/modules/performance/metrics_module.py +0 -678
- flock/modules/zep/__init__.py +0 -1
- flock/modules/zep/zep_module.py +0 -192
- flock/platform/docker_tools.py +0 -49
- flock/platform/jaeger_install.py +0 -86
- flock/routers/__init__.py +0 -1
- flock/routers/agent/__init__.py +0 -1
- flock/routers/agent/agent_router.py +0 -236
- flock/routers/agent/handoff_agent.py +0 -58
- flock/routers/conditional/conditional_router.py +0 -486
- flock/routers/default/__init__.py +0 -1
- flock/routers/default/default_router.py +0 -80
- flock/routers/feedback/feedback_router.py +0 -114
- flock/routers/list_generator/list_generator_router.py +0 -166
- flock/routers/llm/__init__.py +0 -1
- flock/routers/llm/llm_router.py +0 -365
- flock/tools/__init__.py +0 -0
- flock/tools/azure_tools.py +0 -781
- flock/tools/code_tools.py +0 -167
- flock/tools/file_tools.py +0 -149
- flock/tools/github_tools.py +0 -157
- flock/tools/markdown_tools.py +0 -205
- flock/tools/system_tools.py +0 -9
- flock/tools/text_tools.py +0 -810
- flock/tools/web_tools.py +0 -92
- flock/tools/zendesk_tools.py +0 -501
- 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 -237
- flock/webapp/app/api/execution.py +0 -503
- flock/webapp/app/api/flock_management.py +0 -125
- flock/webapp/app/api/registry_viewer.py +0 -29
- flock/webapp/app/chat.py +0 -662
- flock/webapp/app/config.py +0 -104
- flock/webapp/app/dependencies.py +0 -117
- flock/webapp/app/main.py +0 -1086
- 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 -345
- flock/webapp/app/services/sharing_models.py +0 -81
- flock/webapp/app/services/sharing_store.py +0 -597
- flock/webapp/app/templates/theme_mapper.html +0 -326
- flock/webapp/app/theme_mapper.py +0 -811
- flock/webapp/app/utils.py +0 -85
- flock/webapp/run.py +0 -219
- 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 -281
- flock/webapp/static/css/sidebar.css +0 -127
- flock/webapp/static/css/two-pane.css +0 -48
- flock/webapp/templates/base.html +0 -389
- 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 -127
- 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/_live_logs.html +0 -13
- 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/_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 -237
- flock/workflow/agent_activities.py +0 -24
- flock/workflow/agent_execution_activity.py +0 -240
- flock/workflow/flock_workflow.py +0 -225
- flock/workflow/temporal_config.py +0 -96
- flock/workflow/temporal_setup.py +0 -60
- flock_core-0.4.542.dist-info/METADATA +0 -676
- flock_core-0.4.542.dist-info/RECORD +0 -572
- flock_core-0.4.542.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.4.542.dist-info → flock_core-0.5.0.dist-info}/WHEEL +0 -0
flock/orchestrator.py
ADDED
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
"""Blackboard orchestrator and scheduling runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from asyncio import Task
|
|
9
|
+
from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from opentelemetry import trace
|
|
17
|
+
from opentelemetry.trace import Status, StatusCode
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from flock.agent import Agent, AgentBuilder
|
|
21
|
+
from flock.artifacts import Artifact
|
|
22
|
+
from flock.helper.cli_helper import init_console
|
|
23
|
+
from flock.logging.auto_trace import AutoTracedMeta
|
|
24
|
+
from flock.mcp import (
|
|
25
|
+
FlockMCPClientManager,
|
|
26
|
+
FlockMCPConfiguration,
|
|
27
|
+
FlockMCPConnectionConfiguration,
|
|
28
|
+
FlockMCPFeatureConfiguration,
|
|
29
|
+
ServerParameters,
|
|
30
|
+
)
|
|
31
|
+
from flock.registry import type_registry
|
|
32
|
+
from flock.runtime import Context
|
|
33
|
+
from flock.store import BlackboardStore, ConsumptionRecord, InMemoryBlackboardStore
|
|
34
|
+
from flock.visibility import AgentIdentity, PublicVisibility, Visibility
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
import builtins
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BoardHandle:
|
|
42
|
+
"""Handle exposed to components for publishing and inspection."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, orchestrator: Flock) -> None:
|
|
45
|
+
self._orchestrator = orchestrator
|
|
46
|
+
|
|
47
|
+
async def publish(self, artifact: Artifact) -> None:
|
|
48
|
+
await self._orchestrator._persist_and_schedule(artifact)
|
|
49
|
+
|
|
50
|
+
async def get(self, artifact_id) -> Artifact | None:
|
|
51
|
+
return await self._orchestrator.store.get(artifact_id)
|
|
52
|
+
|
|
53
|
+
async def list(self) -> builtins.list[Artifact]:
|
|
54
|
+
return await self._orchestrator.store.list()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Flock(metaclass=AutoTracedMeta):
|
|
58
|
+
"""Main orchestrator for blackboard-based agent coordination.
|
|
59
|
+
|
|
60
|
+
All public methods are automatically traced via OpenTelemetry.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def _patch_litellm_proxy_imports(self) -> None:
|
|
64
|
+
"""Stub litellm proxy_server to avoid optional proxy deps when not used.
|
|
65
|
+
|
|
66
|
+
Some litellm versions import `litellm.proxy.proxy_server` during standard logging
|
|
67
|
+
to read `general_settings`, which pulls in optional dependencies like `apscheduler`.
|
|
68
|
+
We provide a stub so imports succeed but cold storage remains disabled.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
import sys
|
|
72
|
+
import types
|
|
73
|
+
|
|
74
|
+
if "litellm.proxy.proxy_server" not in sys.modules:
|
|
75
|
+
stub = types.ModuleType("litellm.proxy.proxy_server")
|
|
76
|
+
# Minimal surface that cold_storage_handler accesses
|
|
77
|
+
stub.general_settings = {}
|
|
78
|
+
sys.modules["litellm.proxy.proxy_server"] = stub
|
|
79
|
+
except Exception: # nosec B110 - Safe to ignore; worst case litellm will log a warning
|
|
80
|
+
# logger.debug(f"Failed to stub litellm proxy_server: {e}")
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
model: str | None = None,
|
|
86
|
+
*,
|
|
87
|
+
store: BlackboardStore | None = None,
|
|
88
|
+
max_agent_iterations: int = 1000,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Initialize the Flock orchestrator for blackboard-based agent coordination.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
model: Default LLM model for agents (e.g., "openai/gpt-4.1").
|
|
94
|
+
Can be overridden per-agent. If None, uses DEFAULT_MODEL env var.
|
|
95
|
+
store: Custom blackboard storage backend. Defaults to InMemoryBlackboardStore.
|
|
96
|
+
max_agent_iterations: Circuit breaker limit to prevent runaway agent loops.
|
|
97
|
+
Defaults to 1000 iterations per agent before reset.
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
>>> # Basic initialization with default model
|
|
101
|
+
>>> flock = Flock("openai/gpt-4.1")
|
|
102
|
+
|
|
103
|
+
>>> # Custom storage backend
|
|
104
|
+
>>> flock = Flock(
|
|
105
|
+
... "openai/gpt-4o",
|
|
106
|
+
... store=CustomBlackboardStore()
|
|
107
|
+
... )
|
|
108
|
+
|
|
109
|
+
>>> # Circuit breaker configuration
|
|
110
|
+
>>> flock = Flock(
|
|
111
|
+
... "openai/gpt-4.1",
|
|
112
|
+
... max_agent_iterations=500
|
|
113
|
+
... )
|
|
114
|
+
"""
|
|
115
|
+
self._patch_litellm_proxy_imports()
|
|
116
|
+
self._logger = logging.getLogger(__name__)
|
|
117
|
+
self.model = model
|
|
118
|
+
self.store: BlackboardStore = store or InMemoryBlackboardStore()
|
|
119
|
+
self._agents: dict[str, Agent] = {}
|
|
120
|
+
self._tasks: set[Task[Any]] = set()
|
|
121
|
+
self._processed: set[tuple[str, str]] = set()
|
|
122
|
+
self._lock = asyncio.Lock()
|
|
123
|
+
self.metrics: dict[str, float] = {"artifacts_published": 0, "agent_runs": 0}
|
|
124
|
+
# MCP integration
|
|
125
|
+
self._mcp_configs: dict[str, FlockMCPConfiguration] = {}
|
|
126
|
+
self._mcp_manager: FlockMCPClientManager | None = None
|
|
127
|
+
# T068: Circuit breaker for runaway agents
|
|
128
|
+
self.max_agent_iterations: int = max_agent_iterations
|
|
129
|
+
self._agent_iteration_count: dict[str, int] = {}
|
|
130
|
+
self.is_dashboard: bool = False
|
|
131
|
+
# Unified tracing support
|
|
132
|
+
self._workflow_span = None
|
|
133
|
+
self._auto_workflow_enabled = os.getenv("FLOCK_AUTO_WORKFLOW_TRACE", "false").lower() in {
|
|
134
|
+
"true",
|
|
135
|
+
"1",
|
|
136
|
+
"yes",
|
|
137
|
+
"on",
|
|
138
|
+
}
|
|
139
|
+
if not model:
|
|
140
|
+
self.model = os.getenv("DEFAULT_MODEL")
|
|
141
|
+
|
|
142
|
+
# Agent management -----------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def agent(self, name: str) -> AgentBuilder:
|
|
145
|
+
"""Create a new agent using the fluent builder API.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
name: Unique identifier for the agent. Used for visibility controls and metrics.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
AgentBuilder for fluent configuration
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If an agent with this name already exists
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
>>> # Basic agent
|
|
158
|
+
>>> pizza_agent = (
|
|
159
|
+
... flock.agent("pizza_master")
|
|
160
|
+
... .description("Creates delicious pizza recipes")
|
|
161
|
+
... .consumes(DreamPizza)
|
|
162
|
+
... .publishes(Pizza)
|
|
163
|
+
... )
|
|
164
|
+
|
|
165
|
+
>>> # Advanced agent with filtering
|
|
166
|
+
>>> critic = (
|
|
167
|
+
... flock.agent("critic")
|
|
168
|
+
... .consumes(Movie, where=lambda m: m.rating >= 8)
|
|
169
|
+
... .publishes(Review)
|
|
170
|
+
... .with_utilities(RateLimiter(max_calls=10))
|
|
171
|
+
... )
|
|
172
|
+
"""
|
|
173
|
+
if name in self._agents:
|
|
174
|
+
raise ValueError(f"Agent '{name}' already registered.")
|
|
175
|
+
return AgentBuilder(self, name)
|
|
176
|
+
|
|
177
|
+
def register_agent(self, agent: Agent) -> None:
|
|
178
|
+
if agent.name in self._agents:
|
|
179
|
+
raise ValueError(f"Agent '{agent.name}' already registered.")
|
|
180
|
+
self._agents[agent.name] = agent
|
|
181
|
+
|
|
182
|
+
def get_agent(self, name: str) -> Agent:
|
|
183
|
+
return self._agents[name]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def agents(self) -> list[Agent]:
|
|
187
|
+
return list(self._agents.values())
|
|
188
|
+
|
|
189
|
+
# MCP management -------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def add_mcp(
|
|
192
|
+
self,
|
|
193
|
+
name: str,
|
|
194
|
+
connection_params: ServerParameters,
|
|
195
|
+
*,
|
|
196
|
+
enable_tools_feature: bool = True,
|
|
197
|
+
enable_prompts_feature: bool = True,
|
|
198
|
+
enable_sampling_feature: bool = True,
|
|
199
|
+
enable_roots_feature: bool = True,
|
|
200
|
+
mount_points: list[str] | None = None,
|
|
201
|
+
tool_whitelist: list[str] | None = None,
|
|
202
|
+
read_timeout_seconds: float = 300,
|
|
203
|
+
max_retries: int = 3,
|
|
204
|
+
**kwargs,
|
|
205
|
+
) -> Flock:
|
|
206
|
+
"""Register an MCP server for use by agents.
|
|
207
|
+
|
|
208
|
+
Architecture Decision: AD001 - Two-Level Architecture
|
|
209
|
+
MCP servers are registered at orchestrator level and assigned to agents.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
name: Unique identifier for this MCP server
|
|
213
|
+
connection_params: Server connection parameters
|
|
214
|
+
enable_tools_feature: Enable tool execution
|
|
215
|
+
enable_prompts_feature: Enable prompt templates
|
|
216
|
+
enable_sampling_feature: Enable LLM sampling requests
|
|
217
|
+
enable_roots_feature: Enable filesystem roots
|
|
218
|
+
tool_whitelist: Optional list of tool names to allow
|
|
219
|
+
read_timeout_seconds: Timeout for server communications
|
|
220
|
+
max_retries: Connection retry attempts
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
self for method chaining
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If server name already registered
|
|
227
|
+
"""
|
|
228
|
+
if name in self._mcp_configs:
|
|
229
|
+
raise ValueError(f"MCP server '{name}' is already registered.")
|
|
230
|
+
|
|
231
|
+
# Detect transport type
|
|
232
|
+
from flock.mcp.types import (
|
|
233
|
+
SseServerParameters,
|
|
234
|
+
StdioServerParameters,
|
|
235
|
+
StreamableHttpServerParameters,
|
|
236
|
+
WebsocketServerParameters,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if isinstance(connection_params, StdioServerParameters):
|
|
240
|
+
transport_type = "stdio"
|
|
241
|
+
elif isinstance(connection_params, WebsocketServerParameters):
|
|
242
|
+
transport_type = "websockets"
|
|
243
|
+
elif isinstance(connection_params, SseServerParameters):
|
|
244
|
+
transport_type = "sse"
|
|
245
|
+
elif isinstance(connection_params, StreamableHttpServerParameters):
|
|
246
|
+
transport_type = "streamable_http"
|
|
247
|
+
else:
|
|
248
|
+
transport_type = "custom"
|
|
249
|
+
|
|
250
|
+
mcp_roots = None
|
|
251
|
+
if mount_points:
|
|
252
|
+
from pathlib import Path as PathLib
|
|
253
|
+
|
|
254
|
+
from flock.mcp.types import MCPRoot
|
|
255
|
+
|
|
256
|
+
mcp_roots = []
|
|
257
|
+
for path in mount_points:
|
|
258
|
+
# Normalize the path
|
|
259
|
+
if path.startswith("file://"):
|
|
260
|
+
# Already a file URI
|
|
261
|
+
uri = path
|
|
262
|
+
# Extract path from URI for name
|
|
263
|
+
path_str = path.replace("file://", "")
|
|
264
|
+
# the test:// path-prefix is used by testing servers such as the mcp-everything server.
|
|
265
|
+
elif path.startswith("test://"):
|
|
266
|
+
# Already a test URI
|
|
267
|
+
uri = path
|
|
268
|
+
# Extract path from URI for name
|
|
269
|
+
path_str = path.replace("test://", "")
|
|
270
|
+
else:
|
|
271
|
+
# Convert to absolute path and create URI
|
|
272
|
+
abs_path = PathLib(path).resolve()
|
|
273
|
+
uri = f"file://{abs_path}"
|
|
274
|
+
path_str = str(abs_path)
|
|
275
|
+
|
|
276
|
+
# Extract a meaningful name (last component of path)
|
|
277
|
+
name = PathLib(path_str).name or path_str.rstrip("/").split("/")[-1] or "root"
|
|
278
|
+
mcp_roots.append(MCPRoot(uri=uri, name=name))
|
|
279
|
+
|
|
280
|
+
# Build configuration
|
|
281
|
+
connection_config = FlockMCPConnectionConfiguration(
|
|
282
|
+
max_retries=max_retries,
|
|
283
|
+
connection_parameters=connection_params,
|
|
284
|
+
transport_type=transport_type,
|
|
285
|
+
read_timeout_seconds=read_timeout_seconds,
|
|
286
|
+
mount_points=mcp_roots,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
feature_config = FlockMCPFeatureConfiguration(
|
|
290
|
+
tools_enabled=enable_tools_feature,
|
|
291
|
+
prompts_enabled=enable_prompts_feature,
|
|
292
|
+
sampling_enabled=enable_sampling_feature,
|
|
293
|
+
roots_enabled=enable_roots_feature,
|
|
294
|
+
tool_whitelist=tool_whitelist,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
mcp_config = FlockMCPConfiguration(
|
|
298
|
+
name=name,
|
|
299
|
+
connection_config=connection_config,
|
|
300
|
+
feature_config=feature_config,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
self._mcp_configs[name] = mcp_config
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def get_mcp_manager(self) -> FlockMCPClientManager:
|
|
307
|
+
"""Get or create the MCP client manager.
|
|
308
|
+
|
|
309
|
+
Architecture Decision: AD005 - Lazy Connection Establishment
|
|
310
|
+
"""
|
|
311
|
+
if not self._mcp_configs:
|
|
312
|
+
raise RuntimeError("No MCP servers registered. Call add_mcp() first.")
|
|
313
|
+
|
|
314
|
+
if self._mcp_manager is None:
|
|
315
|
+
self._mcp_manager = FlockMCPClientManager(self._mcp_configs)
|
|
316
|
+
|
|
317
|
+
return self._mcp_manager
|
|
318
|
+
|
|
319
|
+
# Unified Tracing ------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
@asynccontextmanager
|
|
322
|
+
async def traced_run(self, name: str = "workflow") -> AsyncGenerator[Any, None]:
|
|
323
|
+
"""Context manager for wrapping an entire execution in a single unified trace.
|
|
324
|
+
|
|
325
|
+
This creates a parent span that encompasses all operations (publish, run_until_idle, etc.)
|
|
326
|
+
within the context, ensuring they all belong to the same trace_id for better observability.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
name: Name for the workflow trace (default: "workflow")
|
|
330
|
+
|
|
331
|
+
Yields:
|
|
332
|
+
The workflow span for optional manual attribute setting
|
|
333
|
+
|
|
334
|
+
Examples:
|
|
335
|
+
# Explicit workflow tracing (recommended)
|
|
336
|
+
async with flock.traced_run("pizza_workflow"):
|
|
337
|
+
await flock.publish(pizza_idea)
|
|
338
|
+
await flock.run_until_idle()
|
|
339
|
+
# All operations now share the same trace_id!
|
|
340
|
+
|
|
341
|
+
# Custom attributes
|
|
342
|
+
async with flock.traced_run("data_pipeline") as span:
|
|
343
|
+
span.set_attribute("pipeline.version", "2.0")
|
|
344
|
+
await flock.publish(data)
|
|
345
|
+
await flock.run_until_idle()
|
|
346
|
+
"""
|
|
347
|
+
tracer = trace.get_tracer(__name__)
|
|
348
|
+
with tracer.start_as_current_span(name) as span:
|
|
349
|
+
# Set workflow-level attributes
|
|
350
|
+
span.set_attribute("flock.workflow", True)
|
|
351
|
+
span.set_attribute("workflow.name", name)
|
|
352
|
+
span.set_attribute("workflow.flock_id", str(id(self)))
|
|
353
|
+
|
|
354
|
+
# Store span for nested operations to use
|
|
355
|
+
prev_workflow_span = self._workflow_span
|
|
356
|
+
self._workflow_span = span
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
yield span
|
|
360
|
+
span.set_status(Status(StatusCode.OK))
|
|
361
|
+
except Exception as e:
|
|
362
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
363
|
+
span.record_exception(e)
|
|
364
|
+
raise
|
|
365
|
+
finally:
|
|
366
|
+
# Restore previous workflow span
|
|
367
|
+
self._workflow_span = prev_workflow_span
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def clear_traces(db_path: str = ".flock/traces.duckdb") -> dict[str, Any]:
|
|
371
|
+
"""Clear all traces from the DuckDB database.
|
|
372
|
+
|
|
373
|
+
Useful for resetting debug sessions or cleaning up test data.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
db_path: Path to the DuckDB database file (default: ".flock/traces.duckdb")
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Dictionary with operation results:
|
|
380
|
+
- deleted_count: Number of spans deleted
|
|
381
|
+
- success: Whether operation succeeded
|
|
382
|
+
- error: Error message if failed
|
|
383
|
+
|
|
384
|
+
Examples:
|
|
385
|
+
# Clear all traces
|
|
386
|
+
result = Flock.clear_traces()
|
|
387
|
+
print(f"Deleted {result['deleted_count']} spans")
|
|
388
|
+
|
|
389
|
+
# Custom database path
|
|
390
|
+
result = Flock.clear_traces(".flock/custom_traces.duckdb")
|
|
391
|
+
|
|
392
|
+
# Check if operation succeeded
|
|
393
|
+
if result['success']:
|
|
394
|
+
print("Traces cleared successfully!")
|
|
395
|
+
else:
|
|
396
|
+
print(f"Error: {result['error']}")
|
|
397
|
+
"""
|
|
398
|
+
try:
|
|
399
|
+
from pathlib import Path
|
|
400
|
+
|
|
401
|
+
import duckdb
|
|
402
|
+
|
|
403
|
+
db_file = Path(db_path)
|
|
404
|
+
if not db_file.exists():
|
|
405
|
+
return {
|
|
406
|
+
"success": False,
|
|
407
|
+
"deleted_count": 0,
|
|
408
|
+
"error": f"Database file not found: {db_path}",
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Connect and clear
|
|
412
|
+
conn = duckdb.connect(str(db_file))
|
|
413
|
+
try:
|
|
414
|
+
# Get count before deletion
|
|
415
|
+
count_result = conn.execute("SELECT COUNT(*) FROM spans").fetchone()
|
|
416
|
+
deleted_count = count_result[0] if count_result else 0
|
|
417
|
+
|
|
418
|
+
# Delete all spans
|
|
419
|
+
conn.execute("DELETE FROM spans")
|
|
420
|
+
|
|
421
|
+
# Vacuum to reclaim space
|
|
422
|
+
conn.execute("VACUUM")
|
|
423
|
+
|
|
424
|
+
return {"success": True, "deleted_count": deleted_count, "error": None}
|
|
425
|
+
|
|
426
|
+
finally:
|
|
427
|
+
conn.close()
|
|
428
|
+
|
|
429
|
+
except Exception as e:
|
|
430
|
+
return {"success": False, "deleted_count": 0, "error": str(e)}
|
|
431
|
+
|
|
432
|
+
# Runtime --------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
async def run_until_idle(self) -> None:
|
|
435
|
+
"""Wait for all scheduled agent tasks to complete.
|
|
436
|
+
|
|
437
|
+
This method blocks until the blackboard reaches a stable state where no
|
|
438
|
+
agents are queued for execution. Essential for batch processing and ensuring
|
|
439
|
+
all agent cascades complete before continuing.
|
|
440
|
+
|
|
441
|
+
Note:
|
|
442
|
+
Automatically resets circuit breaker counters and shuts down MCP connections
|
|
443
|
+
when idle. Used with publish() for event-driven workflows.
|
|
444
|
+
|
|
445
|
+
Examples:
|
|
446
|
+
>>> # Event-driven workflow (recommended)
|
|
447
|
+
>>> await flock.publish(task1)
|
|
448
|
+
>>> await flock.publish(task2)
|
|
449
|
+
>>> await flock.run_until_idle() # Wait for all cascades
|
|
450
|
+
>>> # All agents have finished processing
|
|
451
|
+
|
|
452
|
+
>>> # Parallel batch processing
|
|
453
|
+
>>> await flock.publish_many([task1, task2, task3])
|
|
454
|
+
>>> await flock.run_until_idle() # All tasks processed in parallel
|
|
455
|
+
|
|
456
|
+
See Also:
|
|
457
|
+
- publish(): Event-driven artifact publishing
|
|
458
|
+
- publish_many(): Batch publishing for parallel execution
|
|
459
|
+
- invoke(): Direct agent invocation without cascade
|
|
460
|
+
"""
|
|
461
|
+
while self._tasks:
|
|
462
|
+
await asyncio.sleep(0.01)
|
|
463
|
+
pending = {task for task in self._tasks if not task.done()}
|
|
464
|
+
self._tasks = pending
|
|
465
|
+
# T068: Reset circuit breaker counters when idle
|
|
466
|
+
self._agent_iteration_count.clear()
|
|
467
|
+
|
|
468
|
+
# Automatically shutdown MCP connections when idle
|
|
469
|
+
await self.shutdown()
|
|
470
|
+
|
|
471
|
+
async def direct_invoke(
|
|
472
|
+
self, agent: Agent, inputs: Sequence[BaseModel | Mapping[str, Any] | Artifact]
|
|
473
|
+
) -> list[Artifact]:
|
|
474
|
+
artifacts = [self._normalize_input(value, produced_by="__direct__") for value in inputs]
|
|
475
|
+
for artifact in artifacts:
|
|
476
|
+
self._mark_processed(artifact, agent)
|
|
477
|
+
await self._persist_and_schedule(artifact)
|
|
478
|
+
ctx = Context(board=BoardHandle(self), orchestrator=self, task_id=str(uuid4()))
|
|
479
|
+
self._record_agent_run(agent)
|
|
480
|
+
return await agent.execute(ctx, artifacts)
|
|
481
|
+
|
|
482
|
+
async def arun(self, agent_builder: AgentBuilder, *inputs: BaseModel) -> list[Artifact]:
|
|
483
|
+
"""Execute an agent with inputs and wait for all cascades to complete (async).
|
|
484
|
+
|
|
485
|
+
Convenience method that combines direct agent invocation with run_until_idle().
|
|
486
|
+
Useful for testing and synchronous request-response patterns.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
agent_builder: Agent to execute (from flock.agent())
|
|
490
|
+
*inputs: Input objects (BaseModel instances)
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Artifacts produced by the agent and any triggered cascades
|
|
494
|
+
|
|
495
|
+
Examples:
|
|
496
|
+
>>> # Test a single agent
|
|
497
|
+
>>> flock = Flock("openai/gpt-4.1")
|
|
498
|
+
>>> pizza_agent = flock.agent("pizza").consumes(Idea).publishes(Pizza)
|
|
499
|
+
>>> results = await flock.arun(pizza_agent, Idea(topic="Margherita"))
|
|
500
|
+
|
|
501
|
+
>>> # Multiple inputs
|
|
502
|
+
>>> results = await flock.arun(
|
|
503
|
+
... task_agent,
|
|
504
|
+
... Task(name="deploy"),
|
|
505
|
+
... Task(name="test")
|
|
506
|
+
... )
|
|
507
|
+
|
|
508
|
+
Note:
|
|
509
|
+
For event-driven workflows, prefer publish() + run_until_idle() for better
|
|
510
|
+
control over execution timing and parallel processing.
|
|
511
|
+
"""
|
|
512
|
+
artifacts = await self.direct_invoke(agent_builder.agent, list(inputs))
|
|
513
|
+
await self.run_until_idle()
|
|
514
|
+
return artifacts
|
|
515
|
+
|
|
516
|
+
def run(self, agent_builder: AgentBuilder, *inputs: BaseModel) -> list[Artifact]:
|
|
517
|
+
"""Synchronous wrapper for arun() - executes agent and waits for completion.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
agent_builder: Agent to execute (from flock.agent())
|
|
521
|
+
*inputs: Input objects (BaseModel instances)
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Artifacts produced by the agent and any triggered cascades
|
|
525
|
+
|
|
526
|
+
Examples:
|
|
527
|
+
>>> # Synchronous execution (blocks until complete)
|
|
528
|
+
>>> flock = Flock("openai/gpt-4o-mini")
|
|
529
|
+
>>> agent = flock.agent("analyzer").consumes(Data).publishes(Report)
|
|
530
|
+
>>> results = flock.run(agent, Data(value=42))
|
|
531
|
+
|
|
532
|
+
Warning:
|
|
533
|
+
Cannot be called from within an async context. Use arun() instead
|
|
534
|
+
if already in an async function.
|
|
535
|
+
"""
|
|
536
|
+
return asyncio.run(self.arun(agent_builder, *inputs))
|
|
537
|
+
|
|
538
|
+
async def shutdown(self) -> None:
|
|
539
|
+
"""Shutdown orchestrator and clean up resources."""
|
|
540
|
+
if self._mcp_manager is not None:
|
|
541
|
+
await self._mcp_manager.cleanup_all()
|
|
542
|
+
self._mcp_manager = None
|
|
543
|
+
|
|
544
|
+
def cli(self) -> Flock:
|
|
545
|
+
# Placeholder for CLI wiring (rich UI in Step 3)
|
|
546
|
+
return self
|
|
547
|
+
|
|
548
|
+
async def serve(
|
|
549
|
+
self,
|
|
550
|
+
*,
|
|
551
|
+
dashboard: bool = False,
|
|
552
|
+
dashboard_v2: bool = False,
|
|
553
|
+
host: str = "127.0.0.1",
|
|
554
|
+
port: int = 8344,
|
|
555
|
+
) -> None:
|
|
556
|
+
"""Start HTTP service for the orchestrator (blocking).
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
dashboard: Enable real-time dashboard with WebSocket support (default: False)
|
|
560
|
+
dashboard_v2: Launch the new dashboard v2 frontend (implies dashboard=True)
|
|
561
|
+
host: Host to bind to (default: "127.0.0.1")
|
|
562
|
+
port: Port to bind to (default: 8344)
|
|
563
|
+
|
|
564
|
+
Examples:
|
|
565
|
+
# Basic HTTP API (no dashboard) - runs until interrupted
|
|
566
|
+
await orchestrator.serve()
|
|
567
|
+
|
|
568
|
+
# With dashboard (WebSocket + browser launch) - runs until interrupted
|
|
569
|
+
await orchestrator.serve(dashboard=True)
|
|
570
|
+
"""
|
|
571
|
+
if dashboard_v2:
|
|
572
|
+
dashboard = True
|
|
573
|
+
|
|
574
|
+
if not dashboard:
|
|
575
|
+
# Standard service without dashboard
|
|
576
|
+
from flock.service import BlackboardHTTPService
|
|
577
|
+
|
|
578
|
+
service = BlackboardHTTPService(self)
|
|
579
|
+
await service.run_async(host=host, port=port)
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
# Dashboard mode: integrate event collection and WebSocket
|
|
583
|
+
from flock.dashboard.collector import DashboardEventCollector
|
|
584
|
+
from flock.dashboard.launcher import DashboardLauncher
|
|
585
|
+
from flock.dashboard.service import DashboardHTTPService
|
|
586
|
+
from flock.dashboard.websocket import WebSocketManager
|
|
587
|
+
|
|
588
|
+
# Create dashboard components
|
|
589
|
+
websocket_manager = WebSocketManager()
|
|
590
|
+
event_collector = DashboardEventCollector(store=self.store)
|
|
591
|
+
event_collector.set_websocket_manager(websocket_manager)
|
|
592
|
+
await event_collector.load_persistent_snapshots()
|
|
593
|
+
|
|
594
|
+
# Store collector reference for agents added later
|
|
595
|
+
self._dashboard_collector = event_collector
|
|
596
|
+
|
|
597
|
+
# Inject event collector into all existing agents
|
|
598
|
+
for agent in self._agents.values():
|
|
599
|
+
# Insert at beginning of utilities list (highest priority)
|
|
600
|
+
agent.utilities.insert(0, event_collector)
|
|
601
|
+
|
|
602
|
+
# Start dashboard launcher (npm process + browser)
|
|
603
|
+
launcher_kwargs: dict[str, Any] = {"port": port}
|
|
604
|
+
if dashboard_v2:
|
|
605
|
+
dashboard_pkg_dir = Path(__file__).parent / "dashboard"
|
|
606
|
+
launcher_kwargs["frontend_dir"] = dashboard_pkg_dir.parent / "frontend_v2"
|
|
607
|
+
launcher_kwargs["static_dir"] = dashboard_pkg_dir / "static_v2"
|
|
608
|
+
|
|
609
|
+
launcher = DashboardLauncher(**launcher_kwargs)
|
|
610
|
+
launcher.start()
|
|
611
|
+
|
|
612
|
+
# Create dashboard HTTP service
|
|
613
|
+
service = DashboardHTTPService(
|
|
614
|
+
orchestrator=self,
|
|
615
|
+
websocket_manager=websocket_manager,
|
|
616
|
+
event_collector=event_collector,
|
|
617
|
+
use_v2=dashboard_v2,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Store launcher for cleanup
|
|
621
|
+
self._dashboard_launcher = launcher
|
|
622
|
+
|
|
623
|
+
# Run service (blocking call)
|
|
624
|
+
try:
|
|
625
|
+
await service.run_async(host=host, port=port)
|
|
626
|
+
finally:
|
|
627
|
+
# Cleanup on exit
|
|
628
|
+
launcher.stop()
|
|
629
|
+
|
|
630
|
+
# Scheduling -----------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
async def publish(
|
|
633
|
+
self,
|
|
634
|
+
obj: BaseModel | dict | Artifact,
|
|
635
|
+
*,
|
|
636
|
+
visibility: Visibility | None = None,
|
|
637
|
+
correlation_id: str | None = None,
|
|
638
|
+
partition_key: str | None = None,
|
|
639
|
+
tags: set[str] | None = None,
|
|
640
|
+
is_dashboard: bool = False,
|
|
641
|
+
) -> Artifact:
|
|
642
|
+
"""Publish an artifact to the blackboard (event-driven).
|
|
643
|
+
|
|
644
|
+
All agents with matching subscriptions will be triggered according to
|
|
645
|
+
their filters (type, predicates, visibility, etc).
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
obj: Object to publish (BaseModel instance, dict, or Artifact)
|
|
649
|
+
visibility: Access control (defaults to PublicVisibility)
|
|
650
|
+
correlation_id: Optional correlation ID for request tracing
|
|
651
|
+
partition_key: Optional partition key for sharding
|
|
652
|
+
tags: Optional tags for channel-based routing
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
The published Artifact
|
|
656
|
+
|
|
657
|
+
Examples:
|
|
658
|
+
>>> # Publish a model instance (recommended)
|
|
659
|
+
>>> task = Task(name="Deploy", priority=5)
|
|
660
|
+
>>> await orchestrator.publish(task)
|
|
661
|
+
|
|
662
|
+
>>> # Publish with custom visibility
|
|
663
|
+
>>> await orchestrator.publish(
|
|
664
|
+
... task,
|
|
665
|
+
... visibility=PrivateVisibility(agents={"admin"})
|
|
666
|
+
... )
|
|
667
|
+
|
|
668
|
+
>>> # Publish with tags for channel routing
|
|
669
|
+
>>> await orchestrator.publish(task, tags={"urgent", "backend"})
|
|
670
|
+
"""
|
|
671
|
+
init_console(clear_screen=True, show_banner=True, model=self.model)
|
|
672
|
+
self.is_dashboard = is_dashboard
|
|
673
|
+
# Handle different input types
|
|
674
|
+
if isinstance(obj, Artifact):
|
|
675
|
+
# Already an artifact - publish as-is
|
|
676
|
+
artifact = obj
|
|
677
|
+
elif isinstance(obj, BaseModel):
|
|
678
|
+
# BaseModel instance - get type from registry
|
|
679
|
+
type_name = type_registry.name_for(type(obj))
|
|
680
|
+
artifact = Artifact(
|
|
681
|
+
type=type_name,
|
|
682
|
+
payload=obj.model_dump(),
|
|
683
|
+
produced_by="external",
|
|
684
|
+
visibility=visibility or PublicVisibility(),
|
|
685
|
+
correlation_id=correlation_id or uuid4(),
|
|
686
|
+
partition_key=partition_key,
|
|
687
|
+
tags=tags or set(),
|
|
688
|
+
)
|
|
689
|
+
elif isinstance(obj, dict):
|
|
690
|
+
# Dict must have 'type' key
|
|
691
|
+
if "type" not in obj:
|
|
692
|
+
raise ValueError(
|
|
693
|
+
"Dict input must contain 'type' key. "
|
|
694
|
+
"Example: {'type': 'Task', 'name': 'foo', 'priority': 5}"
|
|
695
|
+
)
|
|
696
|
+
# Support both {'type': 'X', 'payload': {...}} and {'type': 'X', ...}
|
|
697
|
+
type_name = obj["type"]
|
|
698
|
+
if "payload" in obj:
|
|
699
|
+
payload = obj["payload"]
|
|
700
|
+
else:
|
|
701
|
+
payload = {k: v for k, v in obj.items() if k != "type"}
|
|
702
|
+
|
|
703
|
+
artifact = Artifact(
|
|
704
|
+
type=type_name,
|
|
705
|
+
payload=payload,
|
|
706
|
+
produced_by="external",
|
|
707
|
+
visibility=visibility or PublicVisibility(),
|
|
708
|
+
correlation_id=correlation_id,
|
|
709
|
+
partition_key=partition_key,
|
|
710
|
+
tags=tags or set(),
|
|
711
|
+
)
|
|
712
|
+
else:
|
|
713
|
+
raise TypeError(
|
|
714
|
+
f"Cannot publish object of type {type(obj).__name__}. "
|
|
715
|
+
"Expected BaseModel, dict, or Artifact."
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Persist and schedule matching agents
|
|
719
|
+
await self._persist_and_schedule(artifact)
|
|
720
|
+
return artifact
|
|
721
|
+
|
|
722
|
+
async def publish_many(
|
|
723
|
+
self, objects: Iterable[BaseModel | dict | Artifact], **kwargs: Any
|
|
724
|
+
) -> list[Artifact]:
|
|
725
|
+
"""Publish multiple artifacts at once (event-driven).
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
objects: Iterable of objects to publish
|
|
729
|
+
**kwargs: Passed to each publish() call (visibility, tags, etc)
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
List of published Artifacts
|
|
733
|
+
|
|
734
|
+
Example:
|
|
735
|
+
>>> tasks = [
|
|
736
|
+
... Task(name="Deploy", priority=5),
|
|
737
|
+
... Task(name="Test", priority=3),
|
|
738
|
+
... Task(name="Document", priority=1),
|
|
739
|
+
... ]
|
|
740
|
+
>>> await orchestrator.publish_many(tasks, tags={"sprint-3"})
|
|
741
|
+
"""
|
|
742
|
+
artifacts = []
|
|
743
|
+
for obj in objects:
|
|
744
|
+
artifact = await self.publish(obj, **kwargs)
|
|
745
|
+
artifacts.append(artifact)
|
|
746
|
+
return artifacts
|
|
747
|
+
|
|
748
|
+
# -----------------------------------------------------------------------------
|
|
749
|
+
# NEW DIRECT INVOCATION API - Explicit Control
|
|
750
|
+
# -----------------------------------------------------------------------------
|
|
751
|
+
|
|
752
|
+
async def invoke(
|
|
753
|
+
self,
|
|
754
|
+
agent: Agent | AgentBuilder,
|
|
755
|
+
obj: BaseModel,
|
|
756
|
+
*,
|
|
757
|
+
publish_outputs: bool = True,
|
|
758
|
+
timeout: float | None = None,
|
|
759
|
+
) -> list[Artifact]:
|
|
760
|
+
"""Directly invoke a specific agent (bypasses subscription matching).
|
|
761
|
+
|
|
762
|
+
This executes the agent immediately without checking subscriptions or
|
|
763
|
+
predicates. Useful for testing or synchronous request-response patterns.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
agent: Agent or AgentBuilder to invoke
|
|
767
|
+
obj: Input object (BaseModel instance)
|
|
768
|
+
publish_outputs: If True, publish outputs to blackboard for cascade
|
|
769
|
+
timeout: Optional timeout in seconds
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
Artifacts produced by the agent
|
|
773
|
+
|
|
774
|
+
Warning:
|
|
775
|
+
This bypasses subscription filters and predicates. For event-driven
|
|
776
|
+
coordination, use publish() instead.
|
|
777
|
+
|
|
778
|
+
Examples:
|
|
779
|
+
>>> # Testing: Execute agent without triggering others
|
|
780
|
+
>>> results = await orchestrator.invoke(
|
|
781
|
+
... agent,
|
|
782
|
+
... Task(name="test", priority=5),
|
|
783
|
+
... publish_outputs=False
|
|
784
|
+
... )
|
|
785
|
+
|
|
786
|
+
>>> # HTTP endpoint: Execute specific agent, allow cascade
|
|
787
|
+
>>> results = await orchestrator.invoke(
|
|
788
|
+
... movie_agent,
|
|
789
|
+
... Idea(topic="AI", genre="comedy"),
|
|
790
|
+
... publish_outputs=True
|
|
791
|
+
... )
|
|
792
|
+
>>> await orchestrator.run_until_idle()
|
|
793
|
+
"""
|
|
794
|
+
from asyncio import wait_for
|
|
795
|
+
from uuid import uuid4
|
|
796
|
+
|
|
797
|
+
# Get Agent instance
|
|
798
|
+
agent_obj = agent.agent if isinstance(agent, AgentBuilder) else agent
|
|
799
|
+
|
|
800
|
+
# Create artifact (don't publish to blackboard yet)
|
|
801
|
+
type_name = type_registry.name_for(type(obj))
|
|
802
|
+
artifact = Artifact(
|
|
803
|
+
type=type_name,
|
|
804
|
+
payload=obj.model_dump(),
|
|
805
|
+
produced_by="__direct__",
|
|
806
|
+
visibility=PublicVisibility(),
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Execute agent directly
|
|
810
|
+
ctx = Context(board=BoardHandle(self), orchestrator=self, task_id=str(uuid4()))
|
|
811
|
+
self._record_agent_run(agent_obj)
|
|
812
|
+
|
|
813
|
+
# Execute with optional timeout
|
|
814
|
+
if timeout:
|
|
815
|
+
execution = agent_obj.execute(ctx, [artifact])
|
|
816
|
+
outputs = await wait_for(execution, timeout=timeout)
|
|
817
|
+
else:
|
|
818
|
+
outputs = await agent_obj.execute(ctx, [artifact])
|
|
819
|
+
|
|
820
|
+
# Optionally publish outputs to blackboard
|
|
821
|
+
if publish_outputs:
|
|
822
|
+
for output in outputs:
|
|
823
|
+
await self._persist_and_schedule(output)
|
|
824
|
+
|
|
825
|
+
return outputs
|
|
826
|
+
|
|
827
|
+
# Keep publish_external as deprecated alias
|
|
828
|
+
async def publish_external(
|
|
829
|
+
self,
|
|
830
|
+
type_name: str,
|
|
831
|
+
payload: dict[str, Any],
|
|
832
|
+
*,
|
|
833
|
+
visibility: Visibility | None = None,
|
|
834
|
+
correlation_id: str | None = None,
|
|
835
|
+
partition_key: str | None = None,
|
|
836
|
+
tags: set[str] | None = None,
|
|
837
|
+
) -> Artifact:
|
|
838
|
+
"""Deprecated: Use publish() instead.
|
|
839
|
+
|
|
840
|
+
This method will be removed in v2.0.
|
|
841
|
+
"""
|
|
842
|
+
import warnings
|
|
843
|
+
|
|
844
|
+
warnings.warn(
|
|
845
|
+
"publish_external() is deprecated. Use publish(obj) instead.",
|
|
846
|
+
DeprecationWarning,
|
|
847
|
+
stacklevel=2,
|
|
848
|
+
)
|
|
849
|
+
return await self.publish(
|
|
850
|
+
{"type": type_name, "payload": payload},
|
|
851
|
+
visibility=visibility,
|
|
852
|
+
correlation_id=correlation_id,
|
|
853
|
+
partition_key=partition_key,
|
|
854
|
+
tags=tags,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
async def _persist_and_schedule(self, artifact: Artifact) -> None:
|
|
858
|
+
await self.store.publish(artifact)
|
|
859
|
+
self.metrics["artifacts_published"] += 1
|
|
860
|
+
await self._schedule_artifact(artifact)
|
|
861
|
+
|
|
862
|
+
async def _schedule_artifact(self, artifact: Artifact) -> None:
|
|
863
|
+
for agent in self.agents:
|
|
864
|
+
identity = agent.identity
|
|
865
|
+
for subscription in agent.subscriptions:
|
|
866
|
+
if not subscription.accepts_events():
|
|
867
|
+
continue
|
|
868
|
+
# T066: Check prevent_self_trigger
|
|
869
|
+
if agent.prevent_self_trigger and artifact.produced_by == agent.name:
|
|
870
|
+
continue # Skip - agent produced this artifact (prevents feedback loops)
|
|
871
|
+
# T068: Circuit breaker - check iteration limit
|
|
872
|
+
iteration_count = self._agent_iteration_count.get(agent.name, 0)
|
|
873
|
+
if iteration_count >= self.max_agent_iterations:
|
|
874
|
+
# Agent hit iteration limit - possible infinite loop
|
|
875
|
+
continue
|
|
876
|
+
if not self._check_visibility(artifact, identity):
|
|
877
|
+
continue
|
|
878
|
+
if not subscription.matches(artifact):
|
|
879
|
+
continue
|
|
880
|
+
if self._seen_before(artifact, agent):
|
|
881
|
+
continue
|
|
882
|
+
# T068: Increment iteration counter
|
|
883
|
+
self._agent_iteration_count[agent.name] = iteration_count + 1
|
|
884
|
+
self._mark_processed(artifact, agent)
|
|
885
|
+
self._schedule_task(agent, [artifact])
|
|
886
|
+
|
|
887
|
+
def _schedule_task(self, agent: Agent, artifacts: list[Artifact]) -> None:
|
|
888
|
+
task = asyncio.create_task(self._run_agent_task(agent, artifacts))
|
|
889
|
+
self._tasks.add(task)
|
|
890
|
+
task.add_done_callback(self._tasks.discard)
|
|
891
|
+
|
|
892
|
+
def _record_agent_run(self, agent: Agent) -> None:
|
|
893
|
+
self.metrics["agent_runs"] += 1
|
|
894
|
+
|
|
895
|
+
def _mark_processed(self, artifact: Artifact, agent: Agent) -> None:
|
|
896
|
+
key = (str(artifact.id), agent.name)
|
|
897
|
+
self._processed.add(key)
|
|
898
|
+
|
|
899
|
+
def _seen_before(self, artifact: Artifact, agent: Agent) -> bool:
|
|
900
|
+
key = (str(artifact.id), agent.name)
|
|
901
|
+
return key in self._processed
|
|
902
|
+
|
|
903
|
+
async def _run_agent_task(self, agent: Agent, artifacts: list[Artifact]) -> None:
|
|
904
|
+
correlation_id = artifacts[0].correlation_id if artifacts else uuid4()
|
|
905
|
+
|
|
906
|
+
ctx = Context(
|
|
907
|
+
board=BoardHandle(self),
|
|
908
|
+
orchestrator=self,
|
|
909
|
+
task_id=str(uuid4()),
|
|
910
|
+
correlation_id=correlation_id, # NEW!
|
|
911
|
+
)
|
|
912
|
+
self._record_agent_run(agent)
|
|
913
|
+
await agent.execute(ctx, artifacts)
|
|
914
|
+
|
|
915
|
+
if artifacts:
|
|
916
|
+
try:
|
|
917
|
+
timestamp = datetime.now(timezone.utc)
|
|
918
|
+
records = [
|
|
919
|
+
ConsumptionRecord(
|
|
920
|
+
artifact_id=artifact.id,
|
|
921
|
+
consumer=agent.name,
|
|
922
|
+
run_id=ctx.task_id,
|
|
923
|
+
correlation_id=str(correlation_id) if correlation_id else None,
|
|
924
|
+
consumed_at=timestamp,
|
|
925
|
+
)
|
|
926
|
+
for artifact in artifacts
|
|
927
|
+
]
|
|
928
|
+
await self.store.record_consumptions(records)
|
|
929
|
+
except NotImplementedError:
|
|
930
|
+
pass
|
|
931
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
932
|
+
self._logger.exception("Failed to record artifact consumption: %s", exc)
|
|
933
|
+
|
|
934
|
+
# Helpers --------------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
def _normalize_input(
|
|
937
|
+
self, value: BaseModel | Mapping[str, Any] | Artifact, *, produced_by: str
|
|
938
|
+
) -> Artifact:
|
|
939
|
+
if isinstance(value, Artifact):
|
|
940
|
+
return value
|
|
941
|
+
if isinstance(value, BaseModel):
|
|
942
|
+
model_cls = type(value)
|
|
943
|
+
type_name = type_registry.register(model_cls)
|
|
944
|
+
payload = value.model_dump()
|
|
945
|
+
elif isinstance(value, Mapping):
|
|
946
|
+
if "type" not in value:
|
|
947
|
+
raise ValueError("Mapping input must contain 'type'.")
|
|
948
|
+
type_name = value["type"]
|
|
949
|
+
payload = value.get("payload", {})
|
|
950
|
+
else: # pragma: no cover - defensive
|
|
951
|
+
raise TypeError("Unsupported input for direct invoke.")
|
|
952
|
+
return Artifact(type=type_name, payload=payload, produced_by=produced_by)
|
|
953
|
+
|
|
954
|
+
def _check_visibility(self, artifact: Artifact, identity: AgentIdentity) -> bool:
|
|
955
|
+
try:
|
|
956
|
+
return artifact.visibility.allows(identity)
|
|
957
|
+
except AttributeError: # pragma: no cover - fallback for dict vis
|
|
958
|
+
return True
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@asynccontextmanager
|
|
962
|
+
async def start_orchestrator(orchestrator: Flock): # pragma: no cover - CLI helper
|
|
963
|
+
try:
|
|
964
|
+
yield orchestrator
|
|
965
|
+
await orchestrator.run_until_idle()
|
|
966
|
+
finally:
|
|
967
|
+
pass
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
__all__ = ["Flock", "start_orchestrator"]
|