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
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Controller node: dispatch logic, cluster state, leader election.
|
|
3
|
+
Distributed mode only; requires redis, fastapi, uvicorn.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from devsper.bus.message import create_bus_message
|
|
12
|
+
from devsper.bus.topics import (
|
|
13
|
+
TASK_READY,
|
|
14
|
+
TASK_COMPLETED,
|
|
15
|
+
TASK_FAILED,
|
|
16
|
+
TASK_CLAIMED,
|
|
17
|
+
TASK_CLAIM_GRANTED,
|
|
18
|
+
TASK_CLAIM_REJECTED,
|
|
19
|
+
NODE_HEARTBEAT,
|
|
20
|
+
NODE_JOINED,
|
|
21
|
+
NODE_LEFT,
|
|
22
|
+
NODE_BECAME_LEADER,
|
|
23
|
+
NODE_LOST_LEADERSHIP,
|
|
24
|
+
SWARM_SNAPSHOT,
|
|
25
|
+
SWARM_STATUS_REQUEST,
|
|
26
|
+
SWARM_STATUS_RESPONSE,
|
|
27
|
+
)
|
|
28
|
+
from devsper.agents.agent import AgentResponse
|
|
29
|
+
from devsper.cluster.node_info import NodeInfo, NodeRole
|
|
30
|
+
from devsper.cluster.registry import ClusterRegistry
|
|
31
|
+
from devsper.cluster.election import LeaderElector
|
|
32
|
+
from devsper.cluster.state_backend import StateBackend
|
|
33
|
+
from devsper.cluster.router import TaskRouter
|
|
34
|
+
from devsper.swarm.scheduler import Scheduler
|
|
35
|
+
|
|
36
|
+
log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _require_distributed() -> None:
|
|
40
|
+
try:
|
|
41
|
+
import redis.asyncio # noqa: F401
|
|
42
|
+
import fastapi # noqa: F401
|
|
43
|
+
import uvicorn # noqa: F401
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
raise ImportError(
|
|
46
|
+
"Distributed mode requires: pip install devsper[distributed]"
|
|
47
|
+
) from e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _node_info_from_config(config: object, node_id: str, role: NodeRole, run_id: str) -> NodeInfo:
|
|
51
|
+
from datetime import datetime, timezone
|
|
52
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
53
|
+
nodes_cfg = getattr(config, "nodes", None)
|
|
54
|
+
rpc_port = getattr(nodes_cfg, "rpc_port", 7700)
|
|
55
|
+
host = "localhost"
|
|
56
|
+
try:
|
|
57
|
+
import socket
|
|
58
|
+
host = socket.gethostname() or host
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
rpc_url = f"http://{host}:{rpc_port}"
|
|
62
|
+
tags = list(getattr(nodes_cfg, "node_tags", []) or [])
|
|
63
|
+
max_workers = getattr(nodes_cfg, "max_workers_per_node", 8)
|
|
64
|
+
try:
|
|
65
|
+
import devsper
|
|
66
|
+
version = getattr(devsper, "__version__", "1.10.0")
|
|
67
|
+
except Exception:
|
|
68
|
+
version = "1.10.0"
|
|
69
|
+
return NodeInfo(
|
|
70
|
+
node_id=node_id,
|
|
71
|
+
role=role,
|
|
72
|
+
host=host,
|
|
73
|
+
rpc_port=rpc_port,
|
|
74
|
+
rpc_url=rpc_url,
|
|
75
|
+
tags=tags,
|
|
76
|
+
max_workers=max_workers,
|
|
77
|
+
joined_at=now,
|
|
78
|
+
last_heartbeat=now,
|
|
79
|
+
version=version,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ControllerNode:
|
|
84
|
+
"""Owns dispatch, cluster state, leader election. No agent execution."""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
config: object,
|
|
89
|
+
scheduler: Scheduler,
|
|
90
|
+
bus: object,
|
|
91
|
+
state_backend: StateBackend,
|
|
92
|
+
registry: ClusterRegistry,
|
|
93
|
+
elector: LeaderElector,
|
|
94
|
+
router: TaskRouter,
|
|
95
|
+
event_log: object,
|
|
96
|
+
) -> None:
|
|
97
|
+
self.config = config
|
|
98
|
+
self.scheduler = scheduler
|
|
99
|
+
self.bus = bus
|
|
100
|
+
self.state_backend = state_backend
|
|
101
|
+
self.registry = registry
|
|
102
|
+
self.elector = elector
|
|
103
|
+
self.router = router
|
|
104
|
+
self.event_log = event_log
|
|
105
|
+
self.run_id = getattr(scheduler, "run_id", "") or ""
|
|
106
|
+
nodes_cfg = getattr(config, "nodes", None)
|
|
107
|
+
role_str = getattr(nodes_cfg, "role", "controller")
|
|
108
|
+
role = NodeRole.CONTROLLER if role_str == "controller" else NodeRole.HYBRID
|
|
109
|
+
self.node_id = _make_node_id()
|
|
110
|
+
self.node_info = _node_info_from_config(config, self.node_id, role, self.run_id)
|
|
111
|
+
self._is_leader = False
|
|
112
|
+
self._pending_claims: dict[str, dict] = {}
|
|
113
|
+
self._worker_stats: dict[str, dict] = {}
|
|
114
|
+
self._leader_tasks: list[asyncio.Task] = []
|
|
115
|
+
self._started_at = time.monotonic()
|
|
116
|
+
self._last_no_workers_log: float = 0.0
|
|
117
|
+
|
|
118
|
+
async def start(self) -> None:
|
|
119
|
+
await self.registry.register(self.node_info)
|
|
120
|
+
await self.bus.subscribe(TASK_COMPLETED, self._on_task_completed, run_id=self.run_id)
|
|
121
|
+
await self.bus.subscribe(TASK_FAILED, self._on_task_failed, run_id=self.run_id)
|
|
122
|
+
await self.bus.subscribe(TASK_CLAIMED, self._on_task_claimed, run_id=self.run_id)
|
|
123
|
+
await self.bus.subscribe(NODE_HEARTBEAT, self._on_heartbeat, run_id=self.run_id)
|
|
124
|
+
await self.bus.subscribe(NODE_JOINED, self._on_node_joined, run_id=self.run_id)
|
|
125
|
+
await self.bus.subscribe(SWARM_STATUS_REQUEST, self._on_status_request, run_id=self.run_id)
|
|
126
|
+
asyncio.create_task(self._registry_heartbeat_loop())
|
|
127
|
+
asyncio.create_task(
|
|
128
|
+
self.elector.watch(
|
|
129
|
+
self.node_id,
|
|
130
|
+
self._become_leader,
|
|
131
|
+
self._lose_leadership,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def _registry_heartbeat_loop(self) -> None:
|
|
136
|
+
interval = 10.0
|
|
137
|
+
nodes_cfg = getattr(self.config, "nodes", None)
|
|
138
|
+
if nodes_cfg:
|
|
139
|
+
interval = getattr(nodes_cfg, "heartbeat_interval_seconds", 10.0)
|
|
140
|
+
while True:
|
|
141
|
+
try:
|
|
142
|
+
await asyncio.sleep(interval)
|
|
143
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
144
|
+
await self.registry.heartbeat(self.node_id, {"last_heartbeat": now})
|
|
145
|
+
except asyncio.CancelledError:
|
|
146
|
+
break
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
async def _become_leader(self) -> None:
|
|
151
|
+
self._is_leader = True
|
|
152
|
+
current_ids = {t.id for t in self.scheduler.get_all_tasks()}
|
|
153
|
+
snapshot = await self.state_backend.load_snapshot(self.run_id)
|
|
154
|
+
if snapshot:
|
|
155
|
+
snapshot_ids = {
|
|
156
|
+
t.get("id") for t in snapshot.get("tasks", []) if t.get("id")
|
|
157
|
+
}
|
|
158
|
+
if snapshot_ids == current_ids:
|
|
159
|
+
self.scheduler = Scheduler.restore(snapshot)
|
|
160
|
+
log.info(
|
|
161
|
+
"Restored scheduler from snapshot: %s tasks already done",
|
|
162
|
+
snapshot.get("completed_count", 0),
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
# Stale snapshot from a different run (e.g. new prompt); discard it
|
|
166
|
+
await self.state_backend.delete_snapshot(self.run_id)
|
|
167
|
+
self._leader_tasks = [
|
|
168
|
+
asyncio.create_task(self.dispatch_loop()),
|
|
169
|
+
asyncio.create_task(self.checkpoint_loop()),
|
|
170
|
+
asyncio.create_task(self.heartbeat_monitor()),
|
|
171
|
+
asyncio.create_task(self.worker_timeout_monitor()),
|
|
172
|
+
]
|
|
173
|
+
await self.bus.publish(
|
|
174
|
+
create_bus_message(
|
|
175
|
+
topic=NODE_BECAME_LEADER,
|
|
176
|
+
payload={"node_id": self.node_id, "run_id": self.run_id},
|
|
177
|
+
sender_id=self.node_id,
|
|
178
|
+
run_id=self.run_id,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def _lose_leadership(self) -> None:
|
|
183
|
+
self._is_leader = False
|
|
184
|
+
for t in self._leader_tasks:
|
|
185
|
+
t.cancel()
|
|
186
|
+
try:
|
|
187
|
+
await t
|
|
188
|
+
except asyncio.CancelledError:
|
|
189
|
+
pass
|
|
190
|
+
self._leader_tasks.clear()
|
|
191
|
+
await self.bus.publish(
|
|
192
|
+
create_bus_message(
|
|
193
|
+
topic=NODE_LOST_LEADERSHIP,
|
|
194
|
+
payload={"node_id": self.node_id},
|
|
195
|
+
sender_id=self.node_id,
|
|
196
|
+
run_id=self.run_id,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def dispatch_loop(self) -> None:
|
|
201
|
+
timeout_sec = 120
|
|
202
|
+
nodes_cfg = getattr(self.config, "nodes", None)
|
|
203
|
+
if nodes_cfg:
|
|
204
|
+
timeout_sec = getattr(nodes_cfg, "task_claim_timeout_seconds", 120)
|
|
205
|
+
_waited_for_workers = False
|
|
206
|
+
while not self.scheduler.is_finished():
|
|
207
|
+
if not self._is_leader:
|
|
208
|
+
break
|
|
209
|
+
ready = self.scheduler.get_ready_tasks()
|
|
210
|
+
workers = await self.registry.get_workers()
|
|
211
|
+
# Give workers time to register before first dispatch so we spread across all (avoid 429)
|
|
212
|
+
if ready and not _waited_for_workers:
|
|
213
|
+
if len(workers) < 2:
|
|
214
|
+
for _ in range(10):
|
|
215
|
+
await asyncio.sleep(0.25)
|
|
216
|
+
workers = await self.registry.get_workers()
|
|
217
|
+
if len(workers) >= 2:
|
|
218
|
+
break
|
|
219
|
+
_waited_for_workers = True
|
|
220
|
+
now_ts = time.monotonic()
|
|
221
|
+
for task in ready:
|
|
222
|
+
if task.id in self._pending_claims:
|
|
223
|
+
pending = self._pending_claims[task.id]
|
|
224
|
+
if (now_ts - pending.get("dispatched_at", 0)) > timeout_sec:
|
|
225
|
+
del self._pending_claims[task.id]
|
|
226
|
+
log.warning("Task %s claim timed out, re-queuing", task.id)
|
|
227
|
+
continue
|
|
228
|
+
worker = self.router.route(task, workers, self._worker_stats)
|
|
229
|
+
if worker is None:
|
|
230
|
+
if not workers and (now_ts - self._last_no_workers_log) >= 10.0:
|
|
231
|
+
log.warning("No workers in registry; start workers first (run_worker.py).")
|
|
232
|
+
self._last_no_workers_log = now_ts
|
|
233
|
+
continue
|
|
234
|
+
# Add before publish so _on_task_claimed sees the entry when worker replies immediately
|
|
235
|
+
self._pending_claims[task.id] = {
|
|
236
|
+
"dispatched_at": now_ts,
|
|
237
|
+
"target_worker": worker.node_id,
|
|
238
|
+
"claimed": False,
|
|
239
|
+
}
|
|
240
|
+
log.info("Dispatched task %s to worker %s", task.id[:8], worker.node_id[:8])
|
|
241
|
+
await self.bus.publish(
|
|
242
|
+
create_bus_message(
|
|
243
|
+
topic=TASK_READY,
|
|
244
|
+
payload={
|
|
245
|
+
**task.to_dict(),
|
|
246
|
+
"target_worker_id": worker.node_id,
|
|
247
|
+
},
|
|
248
|
+
sender_id=self.node_id,
|
|
249
|
+
run_id=self.run_id,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
await asyncio.sleep(0.05)
|
|
253
|
+
|
|
254
|
+
async def checkpoint_loop(self) -> None:
|
|
255
|
+
while self._is_leader:
|
|
256
|
+
await asyncio.sleep(30)
|
|
257
|
+
try:
|
|
258
|
+
snapshot = self.scheduler.snapshot()
|
|
259
|
+
await self.state_backend.save_snapshot(self.run_id, snapshot)
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
async def _on_task_claimed(self, msg: object) -> None:
|
|
264
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
265
|
+
task_id = payload.get("task_id")
|
|
266
|
+
worker_id = payload.get("worker_id")
|
|
267
|
+
if not task_id or not worker_id:
|
|
268
|
+
return
|
|
269
|
+
pending = self._pending_claims.get(task_id)
|
|
270
|
+
if not pending or pending.get("claimed"):
|
|
271
|
+
await self.bus.publish(
|
|
272
|
+
create_bus_message(
|
|
273
|
+
topic=TASK_CLAIM_REJECTED,
|
|
274
|
+
payload={"task_id": task_id, "worker_id": worker_id},
|
|
275
|
+
sender_id=self.node_id,
|
|
276
|
+
run_id=self.run_id,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
pending["claimed"] = True
|
|
281
|
+
pending["worker_id"] = worker_id
|
|
282
|
+
log.info("Worker %s claimed task %s", worker_id[:8], task_id[:8])
|
|
283
|
+
await self.bus.publish(
|
|
284
|
+
create_bus_message(
|
|
285
|
+
topic=TASK_CLAIM_GRANTED,
|
|
286
|
+
payload={"task_id": task_id, "worker_id": worker_id},
|
|
287
|
+
sender_id=self.node_id,
|
|
288
|
+
run_id=self.run_id,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def _on_task_completed(self, msg: object) -> None:
|
|
293
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
294
|
+
sender_id = getattr(msg, "sender_id", "")
|
|
295
|
+
try:
|
|
296
|
+
response = AgentResponse.from_dict(payload)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
log.warning("TASK_COMPLETED parse failed: %s (payload keys: %s)", e, list(payload.keys()) if isinstance(payload, dict) else type(payload))
|
|
299
|
+
return
|
|
300
|
+
result_text = (response.result or "").strip()
|
|
301
|
+
if not result_text and getattr(response, "error", None):
|
|
302
|
+
result_text = f"(Error: {response.error})"
|
|
303
|
+
log.warning(
|
|
304
|
+
"TASK_COMPLETED empty result for task_id=%s from %s: %s",
|
|
305
|
+
response.task_id[:12] if response.task_id else "",
|
|
306
|
+
sender_id[:8] if sender_id else "",
|
|
307
|
+
response.error[:80] if response.error else "",
|
|
308
|
+
)
|
|
309
|
+
elif not result_text:
|
|
310
|
+
log.warning(
|
|
311
|
+
"TASK_COMPLETED empty result for task_id=%s from %s (set DEVSPER_WORKER_MODEL on Rust workers to match controller worker model, e.g. github:gpt-4o)",
|
|
312
|
+
response.task_id[:12] if response.task_id else "",
|
|
313
|
+
sender_id[:8] if sender_id else "",
|
|
314
|
+
)
|
|
315
|
+
self.scheduler.mark_completed(response.task_id, result_text or (response.result or ""))
|
|
316
|
+
self._pending_claims.pop(response.task_id, None)
|
|
317
|
+
if sender_id and sender_id not in self._worker_stats:
|
|
318
|
+
self._worker_stats[sender_id] = {}
|
|
319
|
+
if sender_id:
|
|
320
|
+
self._worker_stats[sender_id].setdefault("completed_task_ids", [])
|
|
321
|
+
self._worker_stats[sender_id]["completed_task_ids"] = (
|
|
322
|
+
self._worker_stats[sender_id]["completed_task_ids"][-49:]
|
|
323
|
+
+ [response.task_id]
|
|
324
|
+
)
|
|
325
|
+
try:
|
|
326
|
+
snapshot = self.scheduler.snapshot()
|
|
327
|
+
await self.state_backend.save_snapshot(self.run_id, snapshot)
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
async def _on_task_failed(self, msg: object) -> None:
|
|
332
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
333
|
+
task_id = payload.get("task_id")
|
|
334
|
+
error = payload.get("error", "")
|
|
335
|
+
if task_id:
|
|
336
|
+
self.scheduler.mark_failed(task_id, error)
|
|
337
|
+
self._pending_claims.pop(task_id, None)
|
|
338
|
+
|
|
339
|
+
async def _on_heartbeat(self, msg: object) -> None:
|
|
340
|
+
sender_id = getattr(msg, "sender_id", "")
|
|
341
|
+
payload = getattr(msg, "payload", {}) or {}
|
|
342
|
+
if not sender_id:
|
|
343
|
+
return
|
|
344
|
+
self._worker_stats[sender_id] = dict(payload)
|
|
345
|
+
self._worker_stats[sender_id]["last_seen"] = datetime.now(timezone.utc)
|
|
346
|
+
try:
|
|
347
|
+
await self.registry.heartbeat(
|
|
348
|
+
sender_id, {"last_heartbeat": datetime.now(timezone.utc).isoformat()}
|
|
349
|
+
)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
async def _on_node_joined(self, msg: object) -> None:
|
|
354
|
+
if not self._is_leader:
|
|
355
|
+
return
|
|
356
|
+
try:
|
|
357
|
+
snapshot = self.scheduler.snapshot()
|
|
358
|
+
await self.bus.publish(
|
|
359
|
+
create_bus_message(
|
|
360
|
+
topic=SWARM_SNAPSHOT,
|
|
361
|
+
payload=snapshot,
|
|
362
|
+
sender_id=self.node_id,
|
|
363
|
+
run_id=self.run_id,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
async def worker_timeout_monitor(self) -> None:
|
|
370
|
+
while self._is_leader:
|
|
371
|
+
await asyncio.sleep(10)
|
|
372
|
+
now_ts = datetime.now(timezone.utc)
|
|
373
|
+
for worker_id, stats in list(self._worker_stats.items()):
|
|
374
|
+
last_seen = stats.get("last_seen")
|
|
375
|
+
if not last_seen:
|
|
376
|
+
continue
|
|
377
|
+
try:
|
|
378
|
+
delta = (now_ts - last_seen).total_seconds()
|
|
379
|
+
except Exception:
|
|
380
|
+
try:
|
|
381
|
+
from datetime import datetime as dt_cls
|
|
382
|
+
dt = dt_cls.fromisoformat(str(last_seen).replace("Z", "+00:00"))
|
|
383
|
+
delta = (now_ts - dt).total_seconds()
|
|
384
|
+
except Exception:
|
|
385
|
+
continue
|
|
386
|
+
if delta <= 30:
|
|
387
|
+
continue
|
|
388
|
+
lost_tasks = [
|
|
389
|
+
tid
|
|
390
|
+
for tid, claim in self._pending_claims.items()
|
|
391
|
+
if claim.get("worker_id") == worker_id and claim.get("claimed")
|
|
392
|
+
]
|
|
393
|
+
for task_id in lost_tasks:
|
|
394
|
+
del self._pending_claims[task_id]
|
|
395
|
+
del self._worker_stats[worker_id]
|
|
396
|
+
nodes_cfg = getattr(self.config, "nodes", None)
|
|
397
|
+
if nodes_cfg and getattr(nodes_cfg, "deregister_stale_workers", False):
|
|
398
|
+
try:
|
|
399
|
+
await self.registry.deregister(worker_id)
|
|
400
|
+
except Exception:
|
|
401
|
+
pass
|
|
402
|
+
await self.bus.publish(
|
|
403
|
+
create_bus_message(
|
|
404
|
+
topic=NODE_LEFT,
|
|
405
|
+
payload={
|
|
406
|
+
"node_id": worker_id,
|
|
407
|
+
"lost_task_count": len(lost_tasks),
|
|
408
|
+
},
|
|
409
|
+
sender_id=self.node_id,
|
|
410
|
+
run_id=self.run_id,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
async def _on_status_request(self, msg: object) -> None:
|
|
415
|
+
payload = await self.get_status()
|
|
416
|
+
await self.bus.publish(
|
|
417
|
+
create_bus_message(
|
|
418
|
+
topic=SWARM_STATUS_RESPONSE,
|
|
419
|
+
payload=payload,
|
|
420
|
+
sender_id=self.node_id,
|
|
421
|
+
run_id=self.run_id,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def get_status(self) -> dict:
|
|
426
|
+
tasks = self.scheduler.get_all_tasks()
|
|
427
|
+
completed = sum(1 for t in tasks if t.status.value == 2)
|
|
428
|
+
failed = sum(1 for t in tasks if t.status.value == -1)
|
|
429
|
+
pending = sum(1 for t in tasks if t.status.value == 0)
|
|
430
|
+
workers = await self.registry.get_workers()
|
|
431
|
+
return {
|
|
432
|
+
"run_id": self.run_id,
|
|
433
|
+
"node_id": self.node_id,
|
|
434
|
+
"is_leader": self._is_leader,
|
|
435
|
+
"scheduler": {
|
|
436
|
+
"total": len(tasks),
|
|
437
|
+
"completed": completed,
|
|
438
|
+
"failed": failed,
|
|
439
|
+
"pending": pending,
|
|
440
|
+
},
|
|
441
|
+
"workers": [w.to_dict() for w in workers],
|
|
442
|
+
"worker_stats": dict(self._worker_stats),
|
|
443
|
+
"uptime_seconds": time.monotonic() - self._started_at,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _make_node_id() -> str:
|
|
448
|
+
from uuid import uuid4
|
|
449
|
+
return str(uuid4())
|
devsper/nodes/rpc.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RPC layer: FastAPI server for health, status, snapshot, control, SSE event stream.
|
|
3
|
+
Distributed mode only; requires fastapi, uvicorn.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _require_distributed() -> None:
|
|
15
|
+
try:
|
|
16
|
+
import fastapi # noqa: F401
|
|
17
|
+
import uvicorn # noqa: F401
|
|
18
|
+
except ImportError as e:
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"RPC requires: pip install devsper[distributed]"
|
|
21
|
+
) from e
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_rpc_app(
|
|
25
|
+
node_id: str,
|
|
26
|
+
role: str,
|
|
27
|
+
get_status: Callable[[], Any],
|
|
28
|
+
get_current_tasks: Callable[[], list] | None = None,
|
|
29
|
+
get_snapshot: Callable[[], Any] | None = None,
|
|
30
|
+
rpc_token: str | None = None,
|
|
31
|
+
event_stream_callback: Callable[[], object] | None = None,
|
|
32
|
+
controller_publish: Callable[[str, dict], object] | None = None,
|
|
33
|
+
) -> object:
|
|
34
|
+
"""Create FastAPI app for node RPC endpoints."""
|
|
35
|
+
_require_distributed()
|
|
36
|
+
from fastapi import FastAPI, Request, Response, Depends, Header
|
|
37
|
+
from fastapi.responses import StreamingResponse
|
|
38
|
+
import time
|
|
39
|
+
app = FastAPI(title="devsper Node RPC")
|
|
40
|
+
_start = time.monotonic()
|
|
41
|
+
|
|
42
|
+
def _check_token(
|
|
43
|
+
x_devsper_token: str | None = Header(None, alias="X-devsper-Token"),
|
|
44
|
+
authorization: str | None = Header(None),
|
|
45
|
+
):
|
|
46
|
+
if not rpc_token:
|
|
47
|
+
return
|
|
48
|
+
token = x_devsper_token or (authorization.replace("Bearer ", "").strip() if authorization else None)
|
|
49
|
+
if token != rpc_token:
|
|
50
|
+
from fastapi import HTTPException
|
|
51
|
+
raise HTTPException(status_code=401, detail="Invalid or missing X-devsper-Token")
|
|
52
|
+
|
|
53
|
+
@app.get("/health")
|
|
54
|
+
async def health():
|
|
55
|
+
return {
|
|
56
|
+
"node_id": node_id,
|
|
57
|
+
"role": role,
|
|
58
|
+
"healthy": True,
|
|
59
|
+
"uptime_seconds": time.monotonic() - _start,
|
|
60
|
+
"version": _get_version(),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@app.get("/status")
|
|
64
|
+
async def status():
|
|
65
|
+
s = get_status()
|
|
66
|
+
if asyncio.iscoroutine(s):
|
|
67
|
+
return await s
|
|
68
|
+
return s
|
|
69
|
+
|
|
70
|
+
@app.get("/tasks")
|
|
71
|
+
async def tasks():
|
|
72
|
+
if get_current_tasks is None:
|
|
73
|
+
return []
|
|
74
|
+
return get_current_tasks()
|
|
75
|
+
|
|
76
|
+
@app.get("/snapshot", dependencies=[Depends(_check_token)])
|
|
77
|
+
async def snapshot():
|
|
78
|
+
if get_snapshot is None:
|
|
79
|
+
return {"error": "Not a controller"}
|
|
80
|
+
snap = get_snapshot()
|
|
81
|
+
if asyncio.iscoroutine(snap):
|
|
82
|
+
snap = await snap
|
|
83
|
+
return snap
|
|
84
|
+
|
|
85
|
+
@app.post("/control", dependencies=[Depends(_check_token)])
|
|
86
|
+
async def control(request: Request):
|
|
87
|
+
body = await request.json()
|
|
88
|
+
command = body.get("command")
|
|
89
|
+
target = body.get("target", "all")
|
|
90
|
+
if controller_publish:
|
|
91
|
+
controller_publish("swarm.control", {"command": command, "target": target})
|
|
92
|
+
return {"ok": True}
|
|
93
|
+
|
|
94
|
+
@app.get("/stream/events")
|
|
95
|
+
async def stream_events():
|
|
96
|
+
if event_stream_callback is None:
|
|
97
|
+
async def empty():
|
|
98
|
+
yield "data: {}\n\n"
|
|
99
|
+
return StreamingResponse(empty(), media_type="text/event-stream")
|
|
100
|
+
async def gen():
|
|
101
|
+
# Placeholder: in real impl, subscribe to bus and yield SSE
|
|
102
|
+
while True:
|
|
103
|
+
try:
|
|
104
|
+
yield f"data: {json.dumps({'t': 'ping'})}\n\n"
|
|
105
|
+
except asyncio.CancelledError:
|
|
106
|
+
break
|
|
107
|
+
await asyncio.sleep(5)
|
|
108
|
+
return StreamingResponse(gen(), media_type="text/event-stream")
|
|
109
|
+
|
|
110
|
+
return app
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_version() -> str:
|
|
114
|
+
try:
|
|
115
|
+
import devsper
|
|
116
|
+
return getattr(devsper, "__version__", "1.10.0")
|
|
117
|
+
except Exception:
|
|
118
|
+
return "1.10.0"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def run_rpc_server(app: object, port: int = 7700, host: str = "0.0.0.0") -> None:
|
|
122
|
+
"""Run uvicorn server in process (non-blocking via create_task)."""
|
|
123
|
+
_require_distributed()
|
|
124
|
+
import uvicorn
|
|
125
|
+
config = uvicorn.Config(app, host=host, port=port, log_level="warning")
|
|
126
|
+
server = uvicorn.Server(config)
|
|
127
|
+
await server.serve()
|