devsper 2.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
devsper/nodes/single.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single-node mode: one process runs controller + worker with InMemoryBus and filesystem state.
|
|
3
|
+
Zero-config, no Redis. Behaviorally identical to pre-v1.10 Swarm.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from devsper.types.task import Task
|
|
11
|
+
from devsper.bus.backends.memory import InMemoryBus
|
|
12
|
+
from devsper.cluster.state_backend import FilesystemStateBackend
|
|
13
|
+
from devsper.cluster.local import InMemoryRegistry, LocalLeaderElector
|
|
14
|
+
from devsper.cluster.router import TaskRouter
|
|
15
|
+
from devsper.swarm.scheduler import Scheduler
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SingleNode:
|
|
21
|
+
"""Controller + worker in one process; InMemoryBus; no RPC, no Redis."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
config: object,
|
|
26
|
+
scheduler: Scheduler,
|
|
27
|
+
bus: object,
|
|
28
|
+
state_backend: object,
|
|
29
|
+
registry: object,
|
|
30
|
+
elector: object,
|
|
31
|
+
router: TaskRouter,
|
|
32
|
+
controller_node: object,
|
|
33
|
+
worker_node: object,
|
|
34
|
+
event_log: object,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.config = config
|
|
37
|
+
self.scheduler = scheduler
|
|
38
|
+
self.bus = bus
|
|
39
|
+
self.state_backend = state_backend
|
|
40
|
+
self.registry = registry
|
|
41
|
+
self.elector = elector
|
|
42
|
+
self.router = router
|
|
43
|
+
self.controller_node = controller_node
|
|
44
|
+
self.worker_node = worker_node
|
|
45
|
+
self.event_log = event_log
|
|
46
|
+
self.run_id = getattr(scheduler, "run_id", "") or ""
|
|
47
|
+
|
|
48
|
+
async def start(self) -> None:
|
|
49
|
+
await self.bus.start()
|
|
50
|
+
await asyncio.gather(
|
|
51
|
+
self.controller_node.start(),
|
|
52
|
+
self.worker_node.start(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def run_until_finished(self) -> dict[str, str]:
|
|
56
|
+
"""Wait until scheduler is finished; return results."""
|
|
57
|
+
# Yield so the event loop runs elector.watch -> _become_leader -> dispatch_loop
|
|
58
|
+
await asyncio.sleep(0.3)
|
|
59
|
+
while not self.scheduler.is_finished():
|
|
60
|
+
await asyncio.sleep(0.05)
|
|
61
|
+
return self.scheduler.get_results()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_single_node(
|
|
65
|
+
config: object,
|
|
66
|
+
scheduler: Scheduler,
|
|
67
|
+
event_log: object,
|
|
68
|
+
memory_router: object,
|
|
69
|
+
agent_factory: object,
|
|
70
|
+
user_task: str = "",
|
|
71
|
+
message_bus: object = None,
|
|
72
|
+
hitl_enabled: bool = False,
|
|
73
|
+
hitl_escalation_checker: object = None,
|
|
74
|
+
hitl_approval_store: object = None,
|
|
75
|
+
hitl_notifier: object = None,
|
|
76
|
+
hitl_resolver: object = None,
|
|
77
|
+
) -> SingleNode:
|
|
78
|
+
"""Build SingleNode with InMemoryBus, filesystem state, in-memory registry/elector."""
|
|
79
|
+
run_id = getattr(scheduler, "run_id", "") or ""
|
|
80
|
+
bus = InMemoryBus()
|
|
81
|
+
events_dir = getattr(config, "events_dir", ".devsper/events")
|
|
82
|
+
state_backend = FilesystemStateBackend(events_dir)
|
|
83
|
+
registry = InMemoryRegistry(run_id)
|
|
84
|
+
elector = LocalLeaderElector(run_id)
|
|
85
|
+
try:
|
|
86
|
+
import devsper
|
|
87
|
+
version = getattr(devsper, "__version__", "1.10.0")
|
|
88
|
+
except Exception:
|
|
89
|
+
version = "1.10.0"
|
|
90
|
+
router = TaskRouter(controller_version=version)
|
|
91
|
+
from devsper.nodes.controller import ControllerNode
|
|
92
|
+
from devsper.nodes.worker import WorkerNode
|
|
93
|
+
try:
|
|
94
|
+
from devsper.tools.selector import get_tools_for_task
|
|
95
|
+
tool_selector = lambda desc, role=None, score_store=None: get_tools_for_task(
|
|
96
|
+
desc or "", role=role, score_store=score_store
|
|
97
|
+
)
|
|
98
|
+
except Exception:
|
|
99
|
+
tool_selector = lambda desc, role=None, score_store=None: []
|
|
100
|
+
try:
|
|
101
|
+
from devsper.tools.scoring import get_default_score_store
|
|
102
|
+
score_store = get_default_score_store()
|
|
103
|
+
except Exception:
|
|
104
|
+
score_store = None
|
|
105
|
+
prefetcher = None
|
|
106
|
+
try:
|
|
107
|
+
from devsper.swarm.prefetcher import TaskPrefetcher
|
|
108
|
+
prefetcher = TaskPrefetcher(
|
|
109
|
+
memory_router=memory_router,
|
|
110
|
+
tool_selector=tool_selector,
|
|
111
|
+
score_store=score_store,
|
|
112
|
+
max_age_seconds=30.0,
|
|
113
|
+
)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
controller = ControllerNode(
|
|
117
|
+
config=config,
|
|
118
|
+
scheduler=scheduler,
|
|
119
|
+
bus=bus,
|
|
120
|
+
state_backend=state_backend,
|
|
121
|
+
registry=registry,
|
|
122
|
+
elector=elector,
|
|
123
|
+
router=router,
|
|
124
|
+
event_log=event_log,
|
|
125
|
+
)
|
|
126
|
+
worker = WorkerNode(
|
|
127
|
+
config=config,
|
|
128
|
+
bus=bus,
|
|
129
|
+
registry=registry,
|
|
130
|
+
memory_router=memory_router,
|
|
131
|
+
tool_selector=tool_selector,
|
|
132
|
+
score_store=score_store,
|
|
133
|
+
prefetcher=prefetcher,
|
|
134
|
+
agent_factory=agent_factory,
|
|
135
|
+
event_log=event_log,
|
|
136
|
+
run_id=run_id,
|
|
137
|
+
user_task=user_task,
|
|
138
|
+
message_bus=message_bus,
|
|
139
|
+
hitl_enabled=hitl_enabled,
|
|
140
|
+
hitl_escalation_checker=hitl_escalation_checker,
|
|
141
|
+
hitl_approval_store=hitl_approval_store,
|
|
142
|
+
hitl_notifier=hitl_notifier,
|
|
143
|
+
hitl_resolver=hitl_resolver,
|
|
144
|
+
)
|
|
145
|
+
return SingleNode(
|
|
146
|
+
config=config,
|
|
147
|
+
scheduler=scheduler,
|
|
148
|
+
bus=bus,
|
|
149
|
+
state_backend=state_backend,
|
|
150
|
+
registry=registry,
|
|
151
|
+
elector=elector,
|
|
152
|
+
router=router,
|
|
153
|
+
controller_node=controller,
|
|
154
|
+
worker_node=worker,
|
|
155
|
+
event_log=event_log,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _make_node_id() -> str:
|
|
160
|
+
from uuid import uuid4
|
|
161
|
+
return str(uuid4())
|
devsper/nodes/worker.py
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker node: executes tasks, reports heartbeats, claims tasks.
|
|
3
|
+
Distributed mode only; requires redis.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from devsper.types.task import Task, TaskStatus
|
|
12
|
+
from devsper.agents.agent import Agent, AgentRequest
|
|
13
|
+
from devsper.bus.message import create_bus_message
|
|
14
|
+
from devsper.bus.topics import (
|
|
15
|
+
TASK_READY,
|
|
16
|
+
TASK_CLAIMED,
|
|
17
|
+
TASK_CLAIM_GRANTED,
|
|
18
|
+
TASK_CLAIM_REJECTED,
|
|
19
|
+
TASK_COMPLETED,
|
|
20
|
+
TASK_FAILED,
|
|
21
|
+
NODE_HEARTBEAT,
|
|
22
|
+
NODE_JOINED,
|
|
23
|
+
SWARM_SNAPSHOT,
|
|
24
|
+
SWARM_CONTROL,
|
|
25
|
+
)
|
|
26
|
+
from devsper.cluster.node_info import NodeInfo, NodeRole
|
|
27
|
+
from devsper.cluster.registry import ClusterRegistry
|
|
28
|
+
from devsper.swarm.prefetcher import PrefetchResult
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _require_distributed() -> None:
|
|
34
|
+
try:
|
|
35
|
+
import redis.asyncio # noqa: F401
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"Distributed mode requires: pip install devsper[distributed]"
|
|
39
|
+
) from e
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _node_info_from_config(config: object, node_id: str, role: NodeRole, run_id: str) -> NodeInfo:
|
|
43
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
44
|
+
nodes_cfg = getattr(config, "nodes", None)
|
|
45
|
+
rpc_port = getattr(nodes_cfg, "rpc_port", 7701)
|
|
46
|
+
host = "localhost"
|
|
47
|
+
try:
|
|
48
|
+
import socket
|
|
49
|
+
host = socket.gethostname() or host
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
rpc_url = f"http://{host}:{rpc_port}"
|
|
53
|
+
tags = list(getattr(nodes_cfg, "node_tags", []) or [])
|
|
54
|
+
max_workers = getattr(nodes_cfg, "max_workers_per_node", 8)
|
|
55
|
+
try:
|
|
56
|
+
import devsper
|
|
57
|
+
version = getattr(devsper, "__version__", "1.10.0")
|
|
58
|
+
except Exception:
|
|
59
|
+
version = "1.10.0"
|
|
60
|
+
return NodeInfo(
|
|
61
|
+
node_id=node_id,
|
|
62
|
+
role=role,
|
|
63
|
+
host=host,
|
|
64
|
+
rpc_port=rpc_port,
|
|
65
|
+
rpc_url=rpc_url,
|
|
66
|
+
tags=tags,
|
|
67
|
+
max_workers=max_workers,
|
|
68
|
+
joined_at=now,
|
|
69
|
+
last_heartbeat=now,
|
|
70
|
+
version=version,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_agent_request(
|
|
75
|
+
task: Task,
|
|
76
|
+
memory_router: object,
|
|
77
|
+
tool_selector: object,
|
|
78
|
+
prefetch: PrefetchResult | None,
|
|
79
|
+
user_task: str = "",
|
|
80
|
+
message_bus: object = None,
|
|
81
|
+
score_store: object = None,
|
|
82
|
+
) -> AgentRequest:
|
|
83
|
+
"""Build AgentRequest for a task (mirrors Agent.run_task context)."""
|
|
84
|
+
memory_section = ""
|
|
85
|
+
if prefetch and getattr(prefetch, "memory_context", None):
|
|
86
|
+
memory_section = prefetch.memory_context or ""
|
|
87
|
+
elif memory_router and task.description:
|
|
88
|
+
try:
|
|
89
|
+
query = f"{user_task} {task.description}".strip() if user_task else task.description
|
|
90
|
+
memory_section = memory_router.get_memory_context(query) or ""
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
message_bus_section = ""
|
|
94
|
+
if message_bus:
|
|
95
|
+
try:
|
|
96
|
+
message_bus_section = message_bus.get_context_sync(task.id) or ""
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
if message_bus_section:
|
|
100
|
+
memory_section = (memory_section + "\n\n" + message_bus_section).strip()
|
|
101
|
+
from devsper.agents.roles import get_role_config
|
|
102
|
+
role_config = get_role_config(getattr(task, "role", None))
|
|
103
|
+
from devsper.agents.agent import BROADCAST_INSTRUCTION
|
|
104
|
+
broadcast_instruction = BROADCAST_INSTRUCTION if message_bus_section else ""
|
|
105
|
+
system_prompt = role_config.prompt_prefix + broadcast_instruction
|
|
106
|
+
tools_names: list[str] = []
|
|
107
|
+
if tool_selector:
|
|
108
|
+
try:
|
|
109
|
+
if prefetch and getattr(prefetch, "tools", None):
|
|
110
|
+
tools_names = [t.name for t in prefetch.tools]
|
|
111
|
+
else:
|
|
112
|
+
tools = tool_selector(
|
|
113
|
+
task.description or "",
|
|
114
|
+
role=getattr(task, "role", None),
|
|
115
|
+
score_store=score_store,
|
|
116
|
+
)
|
|
117
|
+
tools_names = [t.name for t in tools] if tools else []
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
return AgentRequest(
|
|
121
|
+
task=task,
|
|
122
|
+
memory_context=memory_section,
|
|
123
|
+
tools=tools_names,
|
|
124
|
+
model="mock",
|
|
125
|
+
system_prompt=system_prompt,
|
|
126
|
+
prefetch_used=prefetch is not None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class WorkerNode:
|
|
131
|
+
"""Executes tasks, owns local memory/tool cache, reports results."""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
config: object,
|
|
136
|
+
bus: object,
|
|
137
|
+
registry: ClusterRegistry,
|
|
138
|
+
memory_router: object,
|
|
139
|
+
tool_selector: object,
|
|
140
|
+
score_store: object,
|
|
141
|
+
prefetcher: object,
|
|
142
|
+
agent_factory: object,
|
|
143
|
+
event_log: object,
|
|
144
|
+
run_id: str,
|
|
145
|
+
user_task: str = "",
|
|
146
|
+
message_bus: object = None,
|
|
147
|
+
hitl_enabled: bool = False,
|
|
148
|
+
hitl_escalation_checker: object = None,
|
|
149
|
+
hitl_approval_store: object = None,
|
|
150
|
+
hitl_notifier: object = None,
|
|
151
|
+
hitl_resolver: object = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
self.config = config
|
|
154
|
+
self.bus = bus
|
|
155
|
+
self.registry = registry
|
|
156
|
+
self.memory_router = memory_router
|
|
157
|
+
self.tool_selector = tool_selector
|
|
158
|
+
self.score_store = score_store
|
|
159
|
+
self.prefetcher = prefetcher
|
|
160
|
+
self.agent_factory = agent_factory
|
|
161
|
+
self.event_log = event_log
|
|
162
|
+
self._run_id = run_id
|
|
163
|
+
self.user_task = user_task or ""
|
|
164
|
+
self.message_bus = message_bus
|
|
165
|
+
self.hitl_enabled = hitl_enabled
|
|
166
|
+
self.hitl_escalation_checker = hitl_escalation_checker
|
|
167
|
+
self.hitl_approval_store = hitl_approval_store
|
|
168
|
+
self.hitl_notifier = hitl_notifier
|
|
169
|
+
self.hitl_resolver = hitl_resolver
|
|
170
|
+
self.node_id = _make_node_id()
|
|
171
|
+
nodes_cfg = getattr(config, "nodes", None)
|
|
172
|
+
role = NodeRole.WORKER
|
|
173
|
+
self.node_info = _node_info_from_config(config, self.node_id, role, run_id)
|
|
174
|
+
self._active_tasks = 0
|
|
175
|
+
self._pending_grants: dict[str, asyncio.Event] = {}
|
|
176
|
+
self._current_tasks: dict[str, Task] = {}
|
|
177
|
+
self._task_durations: list[float] = []
|
|
178
|
+
self._last_completed_ids: list[str] = []
|
|
179
|
+
self._paused = False
|
|
180
|
+
self._draining = False
|
|
181
|
+
self._snapshot: dict | None = None
|
|
182
|
+
|
|
183
|
+
async def start(self) -> None:
|
|
184
|
+
await self.registry.register(self.node_info)
|
|
185
|
+
await self.bus.subscribe(TASK_READY, self._on_task_ready, run_id=self._run_id)
|
|
186
|
+
await self.bus.subscribe(TASK_CLAIM_GRANTED, self._on_claim_granted, run_id=self._run_id)
|
|
187
|
+
await self.bus.subscribe(TASK_CLAIM_REJECTED, self._on_claim_rejected, run_id=self._run_id)
|
|
188
|
+
await self.bus.subscribe(SWARM_SNAPSHOT, self._on_snapshot, run_id=self._run_id)
|
|
189
|
+
await self.bus.subscribe(SWARM_CONTROL, self._on_control, run_id=self._run_id)
|
|
190
|
+
asyncio.create_task(self.heartbeat_loop())
|
|
191
|
+
await self.bus.publish(
|
|
192
|
+
create_bus_message(
|
|
193
|
+
topic=NODE_JOINED,
|
|
194
|
+
payload=self.node_info.to_dict(),
|
|
195
|
+
sender_id=self.node_id,
|
|
196
|
+
run_id=self._run_id,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def _on_task_ready(self, msg: object) -> None:
|
|
201
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
202
|
+
target = payload.get("target_worker_id")
|
|
203
|
+
if target is not None and target != self.node_id:
|
|
204
|
+
return
|
|
205
|
+
if self._paused or self._draining:
|
|
206
|
+
return
|
|
207
|
+
if self._active_tasks >= self.node_info.max_workers:
|
|
208
|
+
return
|
|
209
|
+
try:
|
|
210
|
+
task = Task.from_dict(payload)
|
|
211
|
+
except Exception:
|
|
212
|
+
return
|
|
213
|
+
task_id_short = (task.id or "?")[:12]
|
|
214
|
+
log.info("Worker %s received TASK_READY for %s", self.node_id[:8], task_id_short)
|
|
215
|
+
self._pending_grants[task.id] = asyncio.Event()
|
|
216
|
+
await self.bus.publish(
|
|
217
|
+
create_bus_message(
|
|
218
|
+
topic=TASK_CLAIMED,
|
|
219
|
+
payload={"task_id": task.id, "worker_id": self.node_id},
|
|
220
|
+
sender_id=self.node_id,
|
|
221
|
+
run_id=self._run_id,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
grant_wait = 15.0
|
|
225
|
+
nodes_cfg = getattr(self.config, "nodes", None)
|
|
226
|
+
if nodes_cfg:
|
|
227
|
+
grant_wait = getattr(nodes_cfg, "claim_grant_wait_seconds", 15.0)
|
|
228
|
+
try:
|
|
229
|
+
await asyncio.wait_for(self._pending_grants[task.id].wait(), timeout=grant_wait)
|
|
230
|
+
except asyncio.TimeoutError:
|
|
231
|
+
log.warning("Worker %s did not receive TASK_CLAIM_GRANTED for %s within %.0fs", self.node_id[:8], task_id_short, grant_wait)
|
|
232
|
+
self._pending_grants.pop(task.id, None)
|
|
233
|
+
return
|
|
234
|
+
if task.id not in self._pending_grants:
|
|
235
|
+
return
|
|
236
|
+
self._pending_grants.pop(task.id, None)
|
|
237
|
+
self._active_tasks += 1
|
|
238
|
+
log.info("Worker %s executing task %s", self.node_id[:8], task_id_short)
|
|
239
|
+
asyncio.create_task(self._execute_task(task))
|
|
240
|
+
|
|
241
|
+
async def _on_claim_granted(self, msg: object) -> None:
|
|
242
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
243
|
+
if payload.get("worker_id") != self.node_id:
|
|
244
|
+
return
|
|
245
|
+
task_id = payload.get("task_id")
|
|
246
|
+
if task_id and task_id in self._pending_grants:
|
|
247
|
+
self._pending_grants[task_id].set()
|
|
248
|
+
|
|
249
|
+
async def _on_claim_rejected(self, msg: object) -> None:
|
|
250
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
251
|
+
if payload.get("worker_id") != self.node_id:
|
|
252
|
+
return
|
|
253
|
+
task_id = payload.get("task_id")
|
|
254
|
+
if task_id:
|
|
255
|
+
self._pending_grants.pop(task_id, None)
|
|
256
|
+
|
|
257
|
+
async def _execute_task(self, task: Task) -> None:
|
|
258
|
+
self._current_tasks[task.id] = task
|
|
259
|
+
start = time.monotonic()
|
|
260
|
+
task_id_short = (task.id or "?")[:12]
|
|
261
|
+
try:
|
|
262
|
+
prefetch = self.prefetcher.consume(task.id) if self.prefetcher else None
|
|
263
|
+
request = _build_agent_request(
|
|
264
|
+
task,
|
|
265
|
+
self.memory_router,
|
|
266
|
+
self.tool_selector,
|
|
267
|
+
prefetch,
|
|
268
|
+
user_task=self.user_task,
|
|
269
|
+
message_bus=self.message_bus,
|
|
270
|
+
score_store=self.score_store,
|
|
271
|
+
)
|
|
272
|
+
agent = self.agent_factory(self.config)
|
|
273
|
+
if hasattr(agent, "model_name") and hasattr(self.config, "models"):
|
|
274
|
+
request = AgentRequest(
|
|
275
|
+
task=request.task,
|
|
276
|
+
memory_context=request.memory_context,
|
|
277
|
+
tools=request.tools,
|
|
278
|
+
model=getattr(self.config.models, "worker", "mock"),
|
|
279
|
+
system_prompt=request.system_prompt,
|
|
280
|
+
prefetch_used=request.prefetch_used,
|
|
281
|
+
)
|
|
282
|
+
exec_timeout = 90
|
|
283
|
+
nodes_cfg = getattr(self.config, "nodes", None)
|
|
284
|
+
if nodes_cfg:
|
|
285
|
+
exec_timeout = getattr(nodes_cfg, "task_execution_timeout_seconds", 90)
|
|
286
|
+
if exec_timeout > 0:
|
|
287
|
+
response = await asyncio.wait_for(
|
|
288
|
+
asyncio.to_thread(agent.run, request),
|
|
289
|
+
timeout=float(exec_timeout),
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
response = await asyncio.to_thread(agent.run, request)
|
|
293
|
+
self._task_durations.append(time.monotonic() - start)
|
|
294
|
+
if len(self._task_durations) > 10:
|
|
295
|
+
self._task_durations.pop(0)
|
|
296
|
+
self._last_completed_ids.append(task.id)
|
|
297
|
+
if len(self._last_completed_ids) > 50:
|
|
298
|
+
self._last_completed_ids.pop(0)
|
|
299
|
+
# Apply response to task for HITL evaluation
|
|
300
|
+
task.result = response.result
|
|
301
|
+
task.status = TaskStatus.COMPLETED if response.success else TaskStatus.FAILED
|
|
302
|
+
task.error = response.error
|
|
303
|
+
result = response.result or ""
|
|
304
|
+
# HITL: escalation check before publishing TASK_COMPLETED
|
|
305
|
+
should_publish_completed = True
|
|
306
|
+
if self.hitl_enabled and self.hitl_escalation_checker and response.success:
|
|
307
|
+
from devsper.explainability.decision_tree import DecisionRecord
|
|
308
|
+
from devsper.hitl.approval import ApprovalRequest
|
|
309
|
+
from datetime import datetime, timezone, timedelta
|
|
310
|
+
last_critic_score = None
|
|
311
|
+
fake_decision = DecisionRecord(
|
|
312
|
+
task_id=task.id,
|
|
313
|
+
task_description=task.description or "",
|
|
314
|
+
strategy_selected="",
|
|
315
|
+
strategy_reason="",
|
|
316
|
+
critic_score=last_critic_score,
|
|
317
|
+
confidence=float(last_critic_score) if last_critic_score is not None else 0.0,
|
|
318
|
+
)
|
|
319
|
+
match = self.hitl_escalation_checker.evaluate(task, response, fake_decision)
|
|
320
|
+
if match is not None:
|
|
321
|
+
trigger, hitl_policy = match
|
|
322
|
+
request_id = str(__import__("uuid").uuid4())
|
|
323
|
+
now = datetime.now(timezone.utc)
|
|
324
|
+
timeout_sec = getattr(hitl_policy, "timeout_seconds", 3600)
|
|
325
|
+
expires = now + timedelta(seconds=timeout_sec)
|
|
326
|
+
approval = ApprovalRequest(
|
|
327
|
+
request_id=request_id,
|
|
328
|
+
task=task,
|
|
329
|
+
proposed_result=result,
|
|
330
|
+
decision_record=fake_decision,
|
|
331
|
+
trigger=trigger,
|
|
332
|
+
created_at=now.isoformat(),
|
|
333
|
+
expires_at=expires.isoformat(),
|
|
334
|
+
status="pending",
|
|
335
|
+
)
|
|
336
|
+
store = self.hitl_approval_store
|
|
337
|
+
if store is not None:
|
|
338
|
+
store.save(approval)
|
|
339
|
+
if self.hitl_notifier is not None:
|
|
340
|
+
await self.hitl_notifier.notify(approval, hitl_policy)
|
|
341
|
+
approved_result = True
|
|
342
|
+
if self.hitl_resolver is not None:
|
|
343
|
+
resolver = self.hitl_resolver
|
|
344
|
+
try:
|
|
345
|
+
import asyncio
|
|
346
|
+
if asyncio.iscoroutinefunction(resolver):
|
|
347
|
+
approved_result = await resolver(approval, hitl_policy)
|
|
348
|
+
else:
|
|
349
|
+
approved_result = await asyncio.to_thread(resolver, approval, hitl_policy)
|
|
350
|
+
except Exception:
|
|
351
|
+
approved_result = False
|
|
352
|
+
if store is not None:
|
|
353
|
+
store.resolve(request_id, approved_result, "")
|
|
354
|
+
else:
|
|
355
|
+
poll_interval = 5
|
|
356
|
+
elapsed = 0
|
|
357
|
+
approved_result = True
|
|
358
|
+
while store is not None and elapsed < timeout_sec:
|
|
359
|
+
await asyncio.sleep(min(poll_interval, timeout_sec - elapsed))
|
|
360
|
+
elapsed += poll_interval
|
|
361
|
+
req = store.get(request_id)
|
|
362
|
+
if req is None:
|
|
363
|
+
break
|
|
364
|
+
if req.status == "approved":
|
|
365
|
+
break
|
|
366
|
+
if req.status == "rejected":
|
|
367
|
+
approved_result = False
|
|
368
|
+
break
|
|
369
|
+
if store is not None:
|
|
370
|
+
req = store.get(request_id)
|
|
371
|
+
if req is not None and req.status == "rejected":
|
|
372
|
+
approved_result = False
|
|
373
|
+
elif req is not None and req.status == "pending" and elapsed >= timeout_sec:
|
|
374
|
+
on_timeout = getattr(hitl_policy, "on_timeout", "auto_approve")
|
|
375
|
+
approved_result = on_timeout != "auto_reject"
|
|
376
|
+
if not approved_result and store:
|
|
377
|
+
approval.status = "timeout"
|
|
378
|
+
store.save(approval)
|
|
379
|
+
if not approved_result:
|
|
380
|
+
should_publish_completed = False
|
|
381
|
+
await self.bus.publish(
|
|
382
|
+
create_bus_message(
|
|
383
|
+
topic=TASK_FAILED,
|
|
384
|
+
payload={
|
|
385
|
+
"task_id": task.id,
|
|
386
|
+
"error": "Rejected by human reviewer",
|
|
387
|
+
"error_type": "HITLRejected",
|
|
388
|
+
"worker_id": self.node_id,
|
|
389
|
+
},
|
|
390
|
+
sender_id=self.node_id,
|
|
391
|
+
run_id=self._run_id,
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
if should_publish_completed:
|
|
395
|
+
log.info("Worker %s completed task %s (%.1fs)", self.node_id[:8], task_id_short, time.monotonic() - start)
|
|
396
|
+
await self.bus.publish(
|
|
397
|
+
create_bus_message(
|
|
398
|
+
topic=TASK_COMPLETED,
|
|
399
|
+
payload=response.to_dict(),
|
|
400
|
+
sender_id=self.node_id,
|
|
401
|
+
run_id=self._run_id,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
except asyncio.TimeoutError:
|
|
405
|
+
err_msg = f"Execution timeout after {exec_timeout}s"
|
|
406
|
+
log.warning("Worker %s failed task %s: %s", self.node_id[:8], task_id_short, err_msg)
|
|
407
|
+
await self.bus.publish(
|
|
408
|
+
create_bus_message(
|
|
409
|
+
topic=TASK_FAILED,
|
|
410
|
+
payload={
|
|
411
|
+
"task_id": task.id,
|
|
412
|
+
"error": err_msg,
|
|
413
|
+
"error_type": "TimeoutError",
|
|
414
|
+
"worker_id": self.node_id,
|
|
415
|
+
},
|
|
416
|
+
sender_id=self.node_id,
|
|
417
|
+
run_id=self._run_id,
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
except Exception as e:
|
|
421
|
+
log.warning("Worker %s failed task %s: %s", self.node_id[:8], task_id_short, e)
|
|
422
|
+
await self.bus.publish(
|
|
423
|
+
create_bus_message(
|
|
424
|
+
topic=TASK_FAILED,
|
|
425
|
+
payload={
|
|
426
|
+
"task_id": task.id,
|
|
427
|
+
"error": str(e),
|
|
428
|
+
"error_type": type(e).__name__,
|
|
429
|
+
"worker_id": self.node_id,
|
|
430
|
+
},
|
|
431
|
+
sender_id=self.node_id,
|
|
432
|
+
run_id=self._run_id,
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
finally:
|
|
436
|
+
self._active_tasks -= 1
|
|
437
|
+
self._current_tasks.pop(task.id, None)
|
|
438
|
+
|
|
439
|
+
async def heartbeat_loop(self) -> None:
|
|
440
|
+
interval = 10.0
|
|
441
|
+
nodes_cfg = getattr(self.config, "nodes", None)
|
|
442
|
+
if nodes_cfg:
|
|
443
|
+
interval = getattr(nodes_cfg, "heartbeat_interval_seconds", 10.0)
|
|
444
|
+
while True:
|
|
445
|
+
await asyncio.sleep(interval)
|
|
446
|
+
if self._paused:
|
|
447
|
+
continue
|
|
448
|
+
avg_duration = (
|
|
449
|
+
sum(self._task_durations) / len(self._task_durations)
|
|
450
|
+
if self._task_durations else 0.0
|
|
451
|
+
)
|
|
452
|
+
cached_tools: list[str] = []
|
|
453
|
+
if self.score_store and hasattr(self.score_store, "get_cached_tool_names"):
|
|
454
|
+
try:
|
|
455
|
+
cached_tools = list(self.score_store.get_cached_tool_names())
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
await self.bus.publish(
|
|
459
|
+
create_bus_message(
|
|
460
|
+
topic=NODE_HEARTBEAT,
|
|
461
|
+
payload={
|
|
462
|
+
"node_id": self.node_id,
|
|
463
|
+
"active_tasks": self._active_tasks,
|
|
464
|
+
"max_workers": self.node_info.max_workers,
|
|
465
|
+
"avg_task_duration_seconds": avg_duration,
|
|
466
|
+
"load": self._active_tasks / max(1, self.node_info.max_workers),
|
|
467
|
+
"cached_tools": cached_tools,
|
|
468
|
+
"completed_task_ids": self._last_completed_ids[-50:],
|
|
469
|
+
"tags": self.node_info.tags,
|
|
470
|
+
"rpc_url": self.node_info.rpc_url,
|
|
471
|
+
},
|
|
472
|
+
sender_id=self.node_id,
|
|
473
|
+
run_id=self._run_id,
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
try:
|
|
477
|
+
await self.registry.heartbeat(
|
|
478
|
+
self.node_id,
|
|
479
|
+
{"last_heartbeat": datetime.now(timezone.utc).isoformat()},
|
|
480
|
+
)
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
async def _on_control(self, msg: object) -> None:
|
|
485
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
486
|
+
command = payload.get("command")
|
|
487
|
+
target = payload.get("target", "all")
|
|
488
|
+
if target != "all" and target != self.node_id:
|
|
489
|
+
return
|
|
490
|
+
if command == "pause":
|
|
491
|
+
self._paused = True
|
|
492
|
+
elif command == "resume":
|
|
493
|
+
self._paused = False
|
|
494
|
+
elif command == "drain":
|
|
495
|
+
self._draining = True
|
|
496
|
+
|
|
497
|
+
async def _on_snapshot(self, msg: object) -> None:
|
|
498
|
+
self._snapshot = getattr(msg, "payload", None)
|
|
499
|
+
|
|
500
|
+
def get_current_tasks(self) -> list[dict]:
|
|
501
|
+
return [t.to_dict() for t in self._current_tasks.values()]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _make_node_id() -> str:
|
|
505
|
+
from uuid import uuid4
|
|
506
|
+
return str(uuid4())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Hierarchical orchestration: meta-planner, sub-swarms, SLAs, priority scheduling."""
|
|
2
|
+
|
|
3
|
+
from devsper.orchestration.meta_planner import (
|
|
4
|
+
MetaPlanner,
|
|
5
|
+
MetaRunResult,
|
|
6
|
+
SLAConfig,
|
|
7
|
+
SLABreach,
|
|
8
|
+
SubSwarmSpec,
|
|
9
|
+
)
|
|
10
|
+
from devsper.orchestration.priority_queue import PriorityScheduler
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MetaPlanner",
|
|
14
|
+
"MetaRunResult",
|
|
15
|
+
"PriorityScheduler",
|
|
16
|
+
"SLAConfig",
|
|
17
|
+
"SLABreach",
|
|
18
|
+
"SubSwarmSpec",
|
|
19
|
+
]
|