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.
Files changed (375) hide show
  1. devsper/__init__.py +14 -0
  2. devsper/agents/a2a/__init__.py +27 -0
  3. devsper/agents/a2a/client.py +126 -0
  4. devsper/agents/a2a/discovery.py +24 -0
  5. devsper/agents/a2a/server.py +128 -0
  6. devsper/agents/a2a/tool_adapter.py +68 -0
  7. devsper/agents/a2a/types.py +49 -0
  8. devsper/agents/agent.py +602 -0
  9. devsper/agents/critic.py +80 -0
  10. devsper/agents/message_bus.py +124 -0
  11. devsper/agents/roles.py +181 -0
  12. devsper/agents/run_agent.py +78 -0
  13. devsper/analytics/__init__.py +5 -0
  14. devsper/analytics/tool_analytics.py +78 -0
  15. devsper/audit/__init__.py +5 -0
  16. devsper/audit/logger.py +214 -0
  17. devsper/bus/__init__.py +29 -0
  18. devsper/bus/backends/__init__.py +5 -0
  19. devsper/bus/backends/base.py +38 -0
  20. devsper/bus/backends/memory.py +55 -0
  21. devsper/bus/backends/redis.py +146 -0
  22. devsper/bus/message.py +56 -0
  23. devsper/bus/schema_version.py +3 -0
  24. devsper/bus/topics.py +19 -0
  25. devsper/cache/__init__.py +6 -0
  26. devsper/cache/embedding_index.py +98 -0
  27. devsper/cache/hashing.py +24 -0
  28. devsper/cache/store.py +153 -0
  29. devsper/cache/task_cache.py +191 -0
  30. devsper/cli/__init__.py +6 -0
  31. devsper/cli/commands/reg.py +733 -0
  32. devsper/cli/github_oauth.py +157 -0
  33. devsper/cli/init.py +637 -0
  34. devsper/cli/main.py +2956 -0
  35. devsper/cli/run_progress.py +103 -0
  36. devsper/cli/ui/__init__.py +65 -0
  37. devsper/cli/ui/components.py +94 -0
  38. devsper/cli/ui/errors.py +104 -0
  39. devsper/cli/ui/logging.py +120 -0
  40. devsper/cli/ui/onboarding.py +102 -0
  41. devsper/cli/ui/progress.py +43 -0
  42. devsper/cli/ui/run_view.py +308 -0
  43. devsper/cli/ui/theme.py +40 -0
  44. devsper/cluster/__init__.py +29 -0
  45. devsper/cluster/election.py +84 -0
  46. devsper/cluster/local.py +97 -0
  47. devsper/cluster/node_info.py +77 -0
  48. devsper/cluster/registry.py +71 -0
  49. devsper/cluster/router.py +117 -0
  50. devsper/cluster/state_backend.py +105 -0
  51. devsper/compliance/__init__.py +5 -0
  52. devsper/compliance/pii.py +147 -0
  53. devsper/config/__init__.py +52 -0
  54. devsper/config/config_loader.py +121 -0
  55. devsper/config/defaults.py +77 -0
  56. devsper/config/resolver.py +342 -0
  57. devsper/config/schema.py +237 -0
  58. devsper/credentials/__init__.py +19 -0
  59. devsper/credentials/cli.py +197 -0
  60. devsper/credentials/migration.py +124 -0
  61. devsper/credentials/store.py +142 -0
  62. devsper/dashboard/__init__.py +9 -0
  63. devsper/dashboard/dashboard.py +87 -0
  64. devsper/dev/__init__.py +25 -0
  65. devsper/dev/builder.py +195 -0
  66. devsper/dev/debugger.py +95 -0
  67. devsper/dev/repo_index.py +138 -0
  68. devsper/dev/sandbox.py +203 -0
  69. devsper/dev/scaffold.py +122 -0
  70. devsper/embeddings/__init__.py +5 -0
  71. devsper/embeddings/service.py +36 -0
  72. devsper/explainability/__init__.py +14 -0
  73. devsper/explainability/decision_tree.py +104 -0
  74. devsper/explainability/rationale.py +38 -0
  75. devsper/explainability/simulation.py +56 -0
  76. devsper/hitl/__init__.py +13 -0
  77. devsper/hitl/approval.py +160 -0
  78. devsper/hitl/escalation.py +95 -0
  79. devsper/intelligence/__init__.py +9 -0
  80. devsper/intelligence/adaptation.py +88 -0
  81. devsper/intelligence/analysis/__init__.py +19 -0
  82. devsper/intelligence/analysis/analyzer.py +71 -0
  83. devsper/intelligence/analysis/cost_estimator.py +66 -0
  84. devsper/intelligence/analysis/formatter.py +103 -0
  85. devsper/intelligence/analysis/run_report.py +402 -0
  86. devsper/intelligence/learning_engine.py +92 -0
  87. devsper/intelligence/strategies/__init__.py +23 -0
  88. devsper/intelligence/strategies/base.py +14 -0
  89. devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
  90. devsper/intelligence/strategies/data_science_strategy.py +33 -0
  91. devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
  92. devsper/intelligence/strategies/experiment_strategy.py +33 -0
  93. devsper/intelligence/strategies/research_strategy.py +34 -0
  94. devsper/intelligence/strategy_selector.py +84 -0
  95. devsper/intelligence/synthesis.py +132 -0
  96. devsper/intelligence/task_optimizer.py +92 -0
  97. devsper/knowledge/__init__.py +5 -0
  98. devsper/knowledge/extractor.py +204 -0
  99. devsper/knowledge/knowledge_graph.py +184 -0
  100. devsper/knowledge/query.py +285 -0
  101. devsper/memory/__init__.py +35 -0
  102. devsper/memory/consolidation.py +138 -0
  103. devsper/memory/embeddings.py +60 -0
  104. devsper/memory/memory_index.py +97 -0
  105. devsper/memory/memory_router.py +62 -0
  106. devsper/memory/memory_store.py +221 -0
  107. devsper/memory/memory_types.py +54 -0
  108. devsper/memory/namespaces.py +45 -0
  109. devsper/memory/scoring.py +77 -0
  110. devsper/memory/summarizer.py +52 -0
  111. devsper/nodes/__init__.py +5 -0
  112. devsper/nodes/controller.py +449 -0
  113. devsper/nodes/rpc.py +127 -0
  114. devsper/nodes/single.py +161 -0
  115. devsper/nodes/worker.py +506 -0
  116. devsper/orchestration/__init__.py +19 -0
  117. devsper/orchestration/meta_planner.py +239 -0
  118. devsper/orchestration/priority_queue.py +61 -0
  119. devsper/plugins/__init__.py +19 -0
  120. devsper/plugins/marketplace/__init__.py +0 -0
  121. devsper/plugins/plugin_loader.py +70 -0
  122. devsper/plugins/plugin_registry.py +34 -0
  123. devsper/plugins/registry.py +83 -0
  124. devsper/protocols/__init__.py +6 -0
  125. devsper/providers/__init__.py +17 -0
  126. devsper/providers/anthropic.py +84 -0
  127. devsper/providers/base.py +75 -0
  128. devsper/providers/complexity_router.py +94 -0
  129. devsper/providers/gemini.py +36 -0
  130. devsper/providers/github.py +180 -0
  131. devsper/providers/model_router.py +40 -0
  132. devsper/providers/openai.py +105 -0
  133. devsper/providers/router/__init__.py +21 -0
  134. devsper/providers/router/backends/__init__.py +19 -0
  135. devsper/providers/router/backends/anthropic_backend.py +111 -0
  136. devsper/providers/router/backends/custom_backend.py +138 -0
  137. devsper/providers/router/backends/gemini_backend.py +89 -0
  138. devsper/providers/router/backends/github_backend.py +165 -0
  139. devsper/providers/router/backends/ollama_backend.py +104 -0
  140. devsper/providers/router/backends/openai_backend.py +142 -0
  141. devsper/providers/router/backends/vllm_backend.py +35 -0
  142. devsper/providers/router/base.py +60 -0
  143. devsper/providers/router/factory.py +92 -0
  144. devsper/providers/router/legacy.py +101 -0
  145. devsper/providers/router/router.py +135 -0
  146. devsper/reasoning/__init__.py +12 -0
  147. devsper/reasoning/graph.py +59 -0
  148. devsper/reasoning/nodes.py +20 -0
  149. devsper/reasoning/store.py +67 -0
  150. devsper/runtime/__init__.py +12 -0
  151. devsper/runtime/health.py +88 -0
  152. devsper/runtime/replay.py +53 -0
  153. devsper/runtime/replay_engine.py +142 -0
  154. devsper/runtime/run_history.py +204 -0
  155. devsper/runtime/telemetry.py +116 -0
  156. devsper/runtime/visualize.py +58 -0
  157. devsper/sandbox/__init__.py +13 -0
  158. devsper/sandbox/sandbox.py +161 -0
  159. devsper/swarm/checkpointer.py +65 -0
  160. devsper/swarm/executor.py +558 -0
  161. devsper/swarm/map_reduce.py +44 -0
  162. devsper/swarm/planner.py +197 -0
  163. devsper/swarm/prefetcher.py +91 -0
  164. devsper/swarm/scheduler.py +153 -0
  165. devsper/swarm/speculation.py +47 -0
  166. devsper/swarm/swarm.py +562 -0
  167. devsper/tools/__init__.py +33 -0
  168. devsper/tools/base.py +29 -0
  169. devsper/tools/code_intelligence/__init__.py +13 -0
  170. devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
  171. devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
  172. devsper/tools/code_intelligence/codebase_indexer.py +71 -0
  173. devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
  174. devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
  175. devsper/tools/code_intelligence/large_function_detector.py +68 -0
  176. devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
  177. devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
  178. devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
  179. devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
  180. devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
  181. devsper/tools/coding/__init__.py +12 -0
  182. devsper/tools/coding/analyze_code_complexity.py +48 -0
  183. devsper/tools/coding/dependency_analyzer.py +42 -0
  184. devsper/tools/coding/extract_functions.py +38 -0
  185. devsper/tools/coding/format_python.py +50 -0
  186. devsper/tools/coding/generate_docstrings.py +40 -0
  187. devsper/tools/coding/generate_unit_tests.py +42 -0
  188. devsper/tools/coding/lint_python.py +51 -0
  189. devsper/tools/coding/refactor_function.py +41 -0
  190. devsper/tools/coding/repo_structure_map.py +54 -0
  191. devsper/tools/coding/run_python.py +53 -0
  192. devsper/tools/data/__init__.py +12 -0
  193. devsper/tools/data/column_type_detection.py +64 -0
  194. devsper/tools/data/csv_summary.py +52 -0
  195. devsper/tools/data/dataframe_filter.py +51 -0
  196. devsper/tools/data/dataframe_groupby.py +47 -0
  197. devsper/tools/data/dataframe_stats.py +38 -0
  198. devsper/tools/data/dataset_sampling.py +55 -0
  199. devsper/tools/data/dataset_schema.py +45 -0
  200. devsper/tools/data/json_pretty_print.py +37 -0
  201. devsper/tools/data/json_query.py +46 -0
  202. devsper/tools/data/missing_value_report.py +47 -0
  203. devsper/tools/data_science/__init__.py +13 -0
  204. devsper/tools/data_science/correlation_heatmap.py +72 -0
  205. devsper/tools/data_science/dataset_bias_detector.py +49 -0
  206. devsper/tools/data_science/dataset_distribution_report.py +64 -0
  207. devsper/tools/data_science/dataset_drift_detector.py +64 -0
  208. devsper/tools/data_science/dataset_outlier_detector.py +65 -0
  209. devsper/tools/data_science/dataset_profile.py +76 -0
  210. devsper/tools/data_science/distributed_dataset_processor.py +54 -0
  211. devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
  212. devsper/tools/data_science/feature_importance_estimator.py +82 -0
  213. devsper/tools/data_science/model_input_validator.py +59 -0
  214. devsper/tools/data_science/time_series_analyzer.py +57 -0
  215. devsper/tools/documents/__init__.py +11 -0
  216. devsper/tools/documents/_docproc.py +56 -0
  217. devsper/tools/documents/document_to_markdown.py +29 -0
  218. devsper/tools/documents/extract_document_images.py +39 -0
  219. devsper/tools/documents/extract_document_text.py +29 -0
  220. devsper/tools/documents/extract_equations.py +36 -0
  221. devsper/tools/documents/extract_tables.py +47 -0
  222. devsper/tools/documents/summarize_document.py +42 -0
  223. devsper/tools/documents/write_latex_document.py +133 -0
  224. devsper/tools/documents/write_markdown_document.py +89 -0
  225. devsper/tools/documents/write_word_document.py +149 -0
  226. devsper/tools/experiments/__init__.py +13 -0
  227. devsper/tools/experiments/bootstrap_estimator.py +54 -0
  228. devsper/tools/experiments/experiment_report_generator.py +50 -0
  229. devsper/tools/experiments/experiment_tracker.py +36 -0
  230. devsper/tools/experiments/grid_search_runner.py +50 -0
  231. devsper/tools/experiments/model_benchmark_runner.py +45 -0
  232. devsper/tools/experiments/monte_carlo_experiment.py +38 -0
  233. devsper/tools/experiments/parameter_sweep_runner.py +51 -0
  234. devsper/tools/experiments/result_comparator.py +58 -0
  235. devsper/tools/experiments/simulation_runner.py +43 -0
  236. devsper/tools/experiments/statistical_significance_test.py +56 -0
  237. devsper/tools/experiments/swarm_map_reduce.py +42 -0
  238. devsper/tools/filesystem/__init__.py +12 -0
  239. devsper/tools/filesystem/append_file.py +42 -0
  240. devsper/tools/filesystem/file_hash.py +40 -0
  241. devsper/tools/filesystem/file_line_count.py +36 -0
  242. devsper/tools/filesystem/file_metadata.py +38 -0
  243. devsper/tools/filesystem/file_preview.py +55 -0
  244. devsper/tools/filesystem/find_large_files.py +50 -0
  245. devsper/tools/filesystem/list_directory.py +39 -0
  246. devsper/tools/filesystem/read_file.py +35 -0
  247. devsper/tools/filesystem/search_files.py +60 -0
  248. devsper/tools/filesystem/write_file.py +41 -0
  249. devsper/tools/flagship/__init__.py +15 -0
  250. devsper/tools/flagship/distributed_document_analysis.py +77 -0
  251. devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
  252. devsper/tools/flagship/repository_semantic_map.py +99 -0
  253. devsper/tools/flagship/research_graph_builder.py +111 -0
  254. devsper/tools/flagship/swarm_experiment_runner.py +86 -0
  255. devsper/tools/knowledge/__init__.py +10 -0
  256. devsper/tools/knowledge/citation_graph_builder.py +69 -0
  257. devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
  258. devsper/tools/knowledge/corpus_builder.py +66 -0
  259. devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
  260. devsper/tools/knowledge/document_corpus_summary.py +68 -0
  261. devsper/tools/knowledge/document_topic_extractor.py +58 -0
  262. devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
  263. devsper/tools/knowledge/timeline_extractor.py +59 -0
  264. devsper/tools/math/__init__.py +12 -0
  265. devsper/tools/math/calculate_expression.py +52 -0
  266. devsper/tools/math/correlation.py +44 -0
  267. devsper/tools/math/distribution_summary.py +39 -0
  268. devsper/tools/math/histogram.py +53 -0
  269. devsper/tools/math/linear_regression.py +47 -0
  270. devsper/tools/math/matrix_multiply.py +38 -0
  271. devsper/tools/math/mean_std.py +35 -0
  272. devsper/tools/math/monte_carlo_simulation.py +43 -0
  273. devsper/tools/math/polynomial_fit.py +40 -0
  274. devsper/tools/math/random_sample.py +36 -0
  275. devsper/tools/mcp/__init__.py +23 -0
  276. devsper/tools/mcp/adapter.py +53 -0
  277. devsper/tools/mcp/client.py +235 -0
  278. devsper/tools/mcp/discovery.py +53 -0
  279. devsper/tools/memory/__init__.py +16 -0
  280. devsper/tools/memory/delete_memory.py +25 -0
  281. devsper/tools/memory/list_memory.py +34 -0
  282. devsper/tools/memory/search_memory.py +36 -0
  283. devsper/tools/memory/store_memory.py +47 -0
  284. devsper/tools/memory/summarize_memory.py +41 -0
  285. devsper/tools/memory/tag_memory.py +47 -0
  286. devsper/tools/pipelines.py +92 -0
  287. devsper/tools/registry.py +39 -0
  288. devsper/tools/research/__init__.py +12 -0
  289. devsper/tools/research/arxiv_download.py +55 -0
  290. devsper/tools/research/arxiv_search.py +58 -0
  291. devsper/tools/research/citation_extractor.py +35 -0
  292. devsper/tools/research/duckduckgo_search.py +42 -0
  293. devsper/tools/research/paper_metadata_extractor.py +45 -0
  294. devsper/tools/research/paper_summarizer.py +41 -0
  295. devsper/tools/research/research_question_generator.py +39 -0
  296. devsper/tools/research/topic_cluster.py +46 -0
  297. devsper/tools/research/web_search.py +47 -0
  298. devsper/tools/research/wikipedia_lookup.py +50 -0
  299. devsper/tools/research_advanced/__init__.py +14 -0
  300. devsper/tools/research_advanced/citation_context_extractor.py +60 -0
  301. devsper/tools/research_advanced/literature_review_generator.py +79 -0
  302. devsper/tools/research_advanced/methodology_extractor.py +58 -0
  303. devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
  304. devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
  305. devsper/tools/research_advanced/paper_method_comparator.py +62 -0
  306. devsper/tools/research_advanced/paper_similarity_search.py +69 -0
  307. devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
  308. devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
  309. devsper/tools/research_advanced/research_gap_finder.py +71 -0
  310. devsper/tools/research_advanced/research_topic_mapper.py +69 -0
  311. devsper/tools/research_advanced/swarm_literature_review.py +58 -0
  312. devsper/tools/scoring/__init__.py +52 -0
  313. devsper/tools/scoring/report.py +44 -0
  314. devsper/tools/scoring/scorer.py +39 -0
  315. devsper/tools/scoring/selector.py +61 -0
  316. devsper/tools/scoring/store.py +267 -0
  317. devsper/tools/selector.py +130 -0
  318. devsper/tools/system/__init__.py +12 -0
  319. devsper/tools/system/cpu_usage.py +22 -0
  320. devsper/tools/system/disk_usage.py +35 -0
  321. devsper/tools/system/environment_variables.py +29 -0
  322. devsper/tools/system/memory_usage.py +23 -0
  323. devsper/tools/system/pip_install.py +44 -0
  324. devsper/tools/system/pip_search.py +29 -0
  325. devsper/tools/system/process_list.py +34 -0
  326. devsper/tools/system/python_package_list.py +40 -0
  327. devsper/tools/system/run_shell_command.py +51 -0
  328. devsper/tools/system/system_info.py +26 -0
  329. devsper/tools/tool_runner.py +122 -0
  330. devsper/tui/__init__.py +5 -0
  331. devsper/tui/activity_feed_view.py +73 -0
  332. devsper/tui/adaptive_tasks_view.py +75 -0
  333. devsper/tui/agent_role_view.py +35 -0
  334. devsper/tui/app.py +395 -0
  335. devsper/tui/dashboard_screen.py +290 -0
  336. devsper/tui/dev_view.py +99 -0
  337. devsper/tui/inject_screen.py +73 -0
  338. devsper/tui/knowledge_graph_view.py +46 -0
  339. devsper/tui/layout.py +43 -0
  340. devsper/tui/logs_view.py +83 -0
  341. devsper/tui/memory_view.py +58 -0
  342. devsper/tui/performance_view.py +33 -0
  343. devsper/tui/reasoning_graph_view.py +39 -0
  344. devsper/tui/results_view.py +139 -0
  345. devsper/tui/swarm_view.py +37 -0
  346. devsper/tui/task_detail_screen.py +55 -0
  347. devsper/tui/task_view.py +103 -0
  348. devsper/types/event.py +97 -0
  349. devsper/types/exceptions.py +21 -0
  350. devsper/types/swarm.py +41 -0
  351. devsper/types/task.py +80 -0
  352. devsper/upgrade/__init__.py +21 -0
  353. devsper/upgrade/changelog.py +124 -0
  354. devsper/upgrade/cli.py +145 -0
  355. devsper/upgrade/installer.py +103 -0
  356. devsper/upgrade/notifier.py +52 -0
  357. devsper/upgrade/version_check.py +121 -0
  358. devsper/utils/event_logger.py +88 -0
  359. devsper/utils/http.py +43 -0
  360. devsper/utils/models.py +54 -0
  361. devsper/visualization/__init__.py +5 -0
  362. devsper/visualization/dag_export.py +67 -0
  363. devsper/workflow/__init__.py +18 -0
  364. devsper/workflow/conditions.py +157 -0
  365. devsper/workflow/context.py +108 -0
  366. devsper/workflow/loader.py +156 -0
  367. devsper/workflow/resolver.py +109 -0
  368. devsper/workflow/runner.py +562 -0
  369. devsper/workflow/schema.py +63 -0
  370. devsper/workflow/validator.py +128 -0
  371. devsper-2.1.6.dist-info/METADATA +346 -0
  372. devsper-2.1.6.dist-info/RECORD +375 -0
  373. devsper-2.1.6.dist-info/WHEEL +4 -0
  374. devsper-2.1.6.dist-info/entry_points.txt +3 -0
  375. 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()