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,308 @@
1
+ """
2
+ Live run view: real-time task table, tool activity, cost during devsper run.
3
+ Polls event log (or subscribes to bus when available). Rich Live, refresh 10 Hz.
4
+ """
5
+
6
+ import os
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from typing import Callable
12
+
13
+ from devsper.cli.ui.theme import console
14
+ from devsper.cli.ui.components import devsperHeader, TaskRow, RoleTag, CostDisplay, SectionHeader
15
+
16
+
17
+ @dataclass
18
+ class TaskState:
19
+ task_id: str
20
+ short_id: str
21
+ description: str
22
+ role: str
23
+ status: str # pending, running, completed, failed, cached, skipped
24
+ duration_ms: int | None = None
25
+
26
+
27
+ @dataclass
28
+ class RunViewState:
29
+ run_id: str
30
+ run_id_short: str
31
+ planner_message: str
32
+ planner_visible: bool
33
+ tasks: list[TaskState] = field(default_factory=list)
34
+ tool_counts: dict[str, int] = field(default_factory=dict)
35
+ total_cost_usd: float | None = None
36
+ worker_count: int = 0
37
+ started_at: float = field(default_factory=time.time)
38
+ finished: bool = False
39
+ _lock: threading.Lock = field(default_factory=threading.Lock)
40
+
41
+ def update_from_events(self, log_path: str | None) -> None:
42
+ if not log_path or not os.path.isfile(log_path):
43
+ return
44
+ events = []
45
+ try:
46
+ from devsper.types.event import Event
47
+ with open(log_path, "r", encoding="utf-8") as f:
48
+ for line in f:
49
+ line = line.strip()
50
+ if not line:
51
+ continue
52
+ try:
53
+ events.append(Event.from_json(line))
54
+ except Exception:
55
+ try:
56
+ events.append(Event.model_validate_json(line))
57
+ except Exception:
58
+ continue
59
+ except Exception:
60
+ return
61
+ if not events:
62
+ return
63
+ with self._lock:
64
+ task_descriptions: dict[str, str] = {}
65
+ task_roles: dict[str, str] = {}
66
+ started: set[str] = set()
67
+ completed: set[str] = set()
68
+ failed: set[str] = set()
69
+ cached: set[str] = set()
70
+ task_duration_ms: dict[str, int] = {}
71
+ tool_counts: dict[str, int] = {}
72
+ planner_done = False
73
+ executor_done = False
74
+ for e in events:
75
+ payload = e.payload or {}
76
+ tid = (payload.get("task_id") or "").strip()
77
+ ev = getattr(e.type, "value", str(e.type))
78
+ if ev == "task_created" and tid:
79
+ task_descriptions[tid] = (payload.get("description") or "").strip()
80
+ task_roles[tid] = (payload.get("role") or "").strip()
81
+ elif ev in ("task_started", "agent_started") and tid:
82
+ started.add(tid)
83
+ elif ev == "task_completed" and tid:
84
+ completed.add(tid)
85
+ started.discard(tid)
86
+ dur = payload.get("duration_ms") or payload.get("duration_seconds")
87
+ if dur is not None:
88
+ task_duration_ms[tid] = int(dur) if isinstance(dur, (int, float)) else 0
89
+ elif ev == "task_failed" and tid:
90
+ failed.add(tid)
91
+ started.discard(tid)
92
+ elif ev in ("agent_finished") and tid and tid not in completed and tid not in failed:
93
+ completed.add(tid)
94
+ started.discard(tid)
95
+ elif ev == "task_cache_hit" and tid:
96
+ cached.add(tid)
97
+ completed.add(tid)
98
+ started.discard(tid)
99
+ elif ev == "tool_called":
100
+ name = (payload.get("tool") or payload.get("tool_name") or "tool").strip()
101
+ tool_counts[name] = tool_counts.get(name, 0) + 1
102
+ elif ev == "planner_finished":
103
+ planner_done = True
104
+ elif ev == "executor_finished":
105
+ executor_done = True
106
+ running_ids = started - completed - failed - cached
107
+ all_ids = sorted(set(task_descriptions) | running_ids | completed | failed | cached)
108
+ self.tasks = []
109
+ for tid in all_ids:
110
+ desc = task_descriptions.get(tid, "")
111
+ role = task_roles.get(tid, "")
112
+ short = tid[:8] if len(tid) >= 8 else tid
113
+ if tid in cached:
114
+ status = "cached"
115
+ elif tid in failed:
116
+ status = "failed"
117
+ elif tid in completed:
118
+ status = "completed"
119
+ elif tid in running_ids:
120
+ status = "running"
121
+ else:
122
+ status = "pending"
123
+ dur_ms = task_duration_ms.get(tid)
124
+ duration_str = f"{dur_ms}ms" if dur_ms is not None else ("..." if status == "running" else "—")
125
+ if dur_ms is not None and dur_ms >= 1000:
126
+ duration_str = f"{dur_ms/1000:.1f}s"
127
+ self.tasks.append(TaskState(
128
+ task_id=tid,
129
+ short_id=short,
130
+ description=desc,
131
+ role=role,
132
+ status=status,
133
+ duration_ms=dur_ms,
134
+ ))
135
+ self.tool_counts = dict(sorted(tool_counts.items(), key=lambda x: -x[1])[:6])
136
+ self.planner_visible = not planner_done and not self.tasks
137
+ if planner_done and not self.tasks and all_ids:
138
+ self.planner_visible = False
139
+ if not self.planner_message or "Selecting" in self.planner_message:
140
+ last = events[-1] if events else None
141
+ if last:
142
+ ev_type = getattr(last.type, "value", str(last.type))
143
+ if ev_type == "swarm_started":
144
+ self.planner_message = "Selecting strategy..."
145
+ elif ev_type == "planner_started":
146
+ self.planner_message = "Decomposing task into subtasks..."
147
+ elif ev_type == "planner_finished":
148
+ self.planner_message = "Building execution DAG..."
149
+ else:
150
+ self.planner_message = "Querying knowledge graph..."
151
+
152
+
153
+ def _render_live_layout(state: RunViewState) -> object:
154
+ """Build Rich renderable for current state."""
155
+ from rich.table import Table
156
+ from rich.text import Text
157
+ from rich.panel import Panel
158
+ from rich.columns import Columns
159
+ from rich.live import Group
160
+
161
+ # Header
162
+ version = ""
163
+ try:
164
+ import devsper
165
+ version = getattr(devsper, "__version__", "")
166
+ except Exception:
167
+ pass
168
+ elapsed = int(time.time() - state.started_at)
169
+ time_str = f"{elapsed // 60}:{elapsed % 60:02d}"
170
+ header_left = devsperHeader(version=version, workers=state.worker_count or 0)
171
+ header_right = Text(f"run: {state.run_id_short} {time_str}", style="hive.muted")
172
+ header = Columns([header_left, header_right], expand=True)
173
+ header = Panel(header, border_style="hive.dim", padding=(0, 1))
174
+
175
+ # Planning phase
176
+ planning_line = Text()
177
+ if state.planner_visible:
178
+ planning_line = Text("◎ ", style="hive.primary") + Text(state.planner_message, style="dim")
179
+ else:
180
+ strategy = "research" # could come from events
181
+ n = len(state.tasks)
182
+ planning_line = Text(" strategy: ", style="hive.muted") + Text(strategy, style="hive.planner") + Text(f" · planning {n} subtasks", style="hive.muted")
183
+ planning_panel = Panel(planning_line, border_style="hive.dim", padding=(0, 1))
184
+
185
+ # Task table (max 12 rows) — one column per row with full task line
186
+ task_table = Table(show_header=False, box=None, padding=(0, 1))
187
+ task_table.add_column("task", width=80)
188
+ running_first = sorted(state.tasks, key=lambda t: (
189
+ 0 if t.status == "running" else 1,
190
+ 1 if t.status == "pending" else 0,
191
+ 0 if t.status == "completed" else 1,
192
+ 0 if t.status == "failed" else 1,
193
+ t.task_id,
194
+ ))
195
+ for t in running_first[:12]:
196
+ dur_str = f"{t.duration_ms}ms" if t.duration_ms is not None else ("..." if t.status == "running" else "—")
197
+ if t.duration_ms is not None and t.duration_ms >= 1000:
198
+ dur_str = f"{t.duration_ms/1000:.1f}s"
199
+ tr = TaskRow(t.short_id, t.description, t.role, dur_str, t.status)
200
+ task_table.add_row(tr)
201
+ if len(state.tasks) > 12:
202
+ task_table.add_row(Text("+ " + str(len(state.tasks) - 12) + " more tasks", style="hive.muted"))
203
+ tasks_panel = Panel(Group(SectionHeader("Tasks"), task_table), border_style="hive.dim", padding=(0, 1))
204
+
205
+ # Tool strip
206
+ tool_parts = [Text(f" {name} ×{c}", style="hive.tool") for name, c in list(state.tool_counts.items())[:6]]
207
+ tool_line = Text(" ").join(tool_parts) if tool_parts else Text(" (no tools yet)", style="hive.dim")
208
+ tools_panel = Panel(Group(SectionHeader("Tools"), tool_line), border_style="hive.dim", padding=(0, 1))
209
+
210
+ # Status bar
211
+ done = sum(1 for t in state.tasks if t.status == "completed")
212
+ failed_n = sum(1 for t in state.tasks if t.status == "failed")
213
+ running_n = sum(1 for t in state.tasks if t.status == "running")
214
+ total = len(state.tasks)
215
+ cost_str = CostDisplay(state.total_cost_usd)
216
+ status_bar = Text()
217
+ status_bar.append_text(cost_str)
218
+ status_bar.append(Text(" · ", style="hive.muted"))
219
+ status_bar.append(Text(f"{state.worker_count} workers", style="hive.muted"))
220
+ status_bar.append(Text(" · ", style="hive.muted"))
221
+ status_bar.append(Text(f"{running_n} running", style="white"))
222
+ status_bar.append(Text(" · ", style="hive.muted"))
223
+ status_bar.append(Text(f"{done} done / {total} total", style="white"))
224
+ status_panel = Panel(status_bar, border_style="hive.dim", padding=(0, 1))
225
+
226
+ return Group(header, planning_panel, tasks_panel, tools_panel, status_panel)
227
+
228
+
229
+ def run_live_view(
230
+ log_path: str | None,
231
+ run_id: str,
232
+ worker_count: int,
233
+ poll_interval: float = 0.1,
234
+ stop_check: Callable[[], bool] | None = None,
235
+ ) -> RunViewState:
236
+ """Run the live view until stop_check() returns True (e.g. swarm thread finished). Returns final state."""
237
+ from rich.live import Live
238
+ state = RunViewState(
239
+ run_id=run_id,
240
+ run_id_short=(run_id or "")[:8],
241
+ planner_message="Selecting strategy...",
242
+ planner_visible=True,
243
+ worker_count=worker_count,
244
+ )
245
+ state.update_from_events(log_path)
246
+
247
+ def get_renderable() -> object:
248
+ state.update_from_events(log_path)
249
+ return _render_live_layout(state)
250
+
251
+ def is_finished() -> bool:
252
+ if stop_check is not None and stop_check():
253
+ return True
254
+ if not log_path or not os.path.isfile(log_path):
255
+ return False
256
+ try:
257
+ with open(log_path, "r", encoding="utf-8") as f:
258
+ lines = f.readlines()
259
+ for line in reversed(lines[-15:]):
260
+ line = line.strip()
261
+ if not line:
262
+ continue
263
+ try:
264
+ from devsper.types.event import Event
265
+ ev = Event.from_json(line)
266
+ except Exception:
267
+ try:
268
+ ev = Event.model_validate_json(line)
269
+ except Exception:
270
+ continue
271
+ if getattr(ev.type, "value", "") == "swarm_finished":
272
+ return True
273
+ except Exception:
274
+ pass
275
+ return False
276
+
277
+ with Live(get_renderable(), refresh_per_second=10, console=console) as live:
278
+ while not is_finished():
279
+ time.sleep(poll_interval)
280
+ live.update(get_renderable())
281
+ state.finished = True
282
+ state.update_from_events(log_path)
283
+ return state
284
+
285
+
286
+ def print_run_summary(state: RunViewState, results: dict[str, str], summary_only: bool = False) -> None:
287
+ """Print final summary panel and optionally task results."""
288
+ from rich.panel import Panel
289
+ from rich.text import Text
290
+ done = sum(1 for t in state.tasks if t.status == "completed")
291
+ failed_n = sum(1 for t in state.tasks if t.status == "failed")
292
+ skipped = sum(1 for t in state.tasks if t.status in ("skipped", "cached"))
293
+ total = len(state.tasks)
294
+ duration_s = time.time() - state.started_at
295
+ cost_str = f"${state.total_cost_usd:.4f}" if state.total_cost_usd is not None else "—"
296
+ cache_hits = sum(1 for t in state.tasks if t.status == "cached")
297
+ lines = [
298
+ f"{total} tasks · {done} completed · {failed_n} failed · {skipped} skipped",
299
+ f"Duration: {duration_s:.1f}s · Cost: {cost_str} · Cache hits: {cache_hits}",
300
+ f"run id: {state.run_id_short}",
301
+ ]
302
+ console.print(Panel("\n".join(lines), title="Run complete", border_style="hive.success"))
303
+ if not summary_only and results:
304
+ for task_id, result in results.items():
305
+ console.print(Text(f"--- {task_id} ---", style="hive.primary"))
306
+ console.print((result or "")[:2000])
307
+ if (result or "") and len(result) > 2000:
308
+ console.print("...")
@@ -0,0 +1,40 @@
1
+ """
2
+ devsper CLI theme: sharp, dark-terminal-native, information-dense.
3
+ All CLI code imports console from here, never from rich directly.
4
+ """
5
+
6
+ from rich.theme import Theme
7
+ from rich.console import Console
8
+
9
+ THEME = Theme({
10
+ "hive.primary": "#F5A623", # amber — brand, headers, highlights
11
+ "hive.secondary": "#4A9EFF", # electric blue — info, links, tool names
12
+ "hive.success": "#3DDC84", # green — completed, healthy
13
+ "hive.warning": "#FFD166", # yellow — warnings, SLA at risk
14
+ "hive.error": "#FF4757", # red — failures, errors
15
+ "hive.muted": "#6B7280", # gray — timestamps, secondary info
16
+ "hive.dim": "#374151", # dark gray — borders, dividers
17
+ "hive.agent": "#A78BFA", # purple — agent activity
18
+ "hive.tool": "#34D399", # teal — tool calls
19
+ "hive.planner": "#FB923C", # orange — planner activity
20
+ "hive.cost": "#F472B6", # pink — cost/token info
21
+ })
22
+
23
+ # Respect NO_COLOR and --no-color (set by main before first use)
24
+ def _make_console(**kwargs: object) -> Console:
25
+ return Console(theme=THEME, highlight=False, **kwargs)
26
+
27
+ console = _make_console()
28
+ err_console = _make_console(stderr=True)
29
+
30
+
31
+ def reconfigure_console(no_color: bool = False, force_terminal: bool | None = None) -> None:
32
+ """Reconfigure global consoles (e.g. for --no-color, --plain)."""
33
+ global console, err_console
34
+ kw: dict = {"theme": THEME, "highlight": False}
35
+ if no_color:
36
+ kw["no_color"] = True
37
+ if force_terminal is not None:
38
+ kw["force_terminal"] = force_terminal
39
+ console = Console(**kw)
40
+ err_console = Console(stderr=True, **kw)
@@ -0,0 +1,29 @@
1
+ """Cluster membership, registry, election, state backend, and task routing."""
2
+
3
+ from devsper.cluster.node_info import (
4
+ ClusterState,
5
+ NodeInfo,
6
+ NodeRole,
7
+ )
8
+ from devsper.cluster.registry import ClusterRegistry
9
+ from devsper.cluster.election import LeaderElector
10
+ from devsper.cluster.state_backend import (
11
+ StateBackend,
12
+ RedisStateBackend,
13
+ FilesystemStateBackend,
14
+ get_state_backend,
15
+ )
16
+ from devsper.cluster.router import TaskRouter
17
+
18
+ __all__ = [
19
+ "ClusterRegistry",
20
+ "ClusterState",
21
+ "NodeInfo",
22
+ "NodeRole",
23
+ "LeaderElector",
24
+ "StateBackend",
25
+ "RedisStateBackend",
26
+ "FilesystemStateBackend",
27
+ "get_state_backend",
28
+ "TaskRouter",
29
+ ]
@@ -0,0 +1,84 @@
1
+ """Leader election via Redis SET NX + TTL."""
2
+
3
+ import asyncio
4
+ from typing import Callable
5
+
6
+ LEADER_KEY_PREFIX = "devsper:leader:"
7
+ LEADER_TTL = 15
8
+ REFRESH_INTERVAL = 5
9
+
10
+
11
+ class LeaderElector:
12
+ """Distributed leader election. Key: devsper:leader:{run_id}, value: node_id, TTL 15s."""
13
+
14
+ def __init__(self, redis_client: object, run_id: str) -> None:
15
+ self._redis = redis_client
16
+ self._run_id = run_id
17
+ self._key = f"{LEADER_KEY_PREFIX}{run_id}"
18
+
19
+ async def campaign(self, node_id: str) -> bool:
20
+ """SET key node_id NX EX 15. Returns True if this node won."""
21
+ try:
22
+ return await self._redis.set(
23
+ self._key, node_id, nx=True, ex=LEADER_TTL
24
+ )
25
+ except Exception:
26
+ return False
27
+
28
+ async def refresh(self, node_id: str) -> bool:
29
+ """Atomic: if current value == node_id, EXPIRE 15 and return True."""
30
+ script = """
31
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
32
+ redis.call('EXPIRE', KEYS[1], ARGV[2])
33
+ return 1
34
+ else
35
+ return 0
36
+ end
37
+ """
38
+ try:
39
+ result = await self._redis.eval(script, 1, self._key, node_id, LEADER_TTL)
40
+ return bool(result)
41
+ except Exception:
42
+ return False
43
+
44
+ async def get_leader(self) -> str | None:
45
+ raw = await self._redis.get(self._key)
46
+ if raw is None:
47
+ return None
48
+ return raw.decode("utf-8") if isinstance(raw, bytes) else str(raw)
49
+
50
+ async def abdicate(self, node_id: str) -> None:
51
+ script = """
52
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
53
+ redis.call('DEL', KEYS[1])
54
+ end
55
+ """
56
+ try:
57
+ await self._redis.eval(script, 1, self._key, node_id)
58
+ except Exception:
59
+ pass
60
+
61
+ async def watch(
62
+ self,
63
+ node_id: str,
64
+ on_elected: Callable[[], object],
65
+ on_lost: Callable[[], object],
66
+ ) -> None:
67
+ """Loop every 5s: refresh if leader, else campaign; call on_elected/on_lost on transition."""
68
+ currently_leader = False
69
+ while True:
70
+ try:
71
+ await asyncio.sleep(REFRESH_INTERVAL)
72
+ if currently_leader:
73
+ if await self.refresh(node_id):
74
+ continue
75
+ currently_leader = False
76
+ await on_lost()
77
+ else:
78
+ if await self.campaign(node_id):
79
+ currently_leader = True
80
+ await on_elected()
81
+ except asyncio.CancelledError:
82
+ break
83
+ except Exception:
84
+ pass
@@ -0,0 +1,97 @@
1
+ """In-memory registry and elector for single-node mode (no Redis)."""
2
+
3
+ import asyncio
4
+ from devsper.cluster.node_info import NodeInfo, NodeRole
5
+ from devsper.cluster.registry import ClusterRegistry
6
+ from devsper.cluster.election import LeaderElector
7
+
8
+
9
+ class InMemoryRegistry:
10
+ """Registry backed by a dict. Single-node only."""
11
+
12
+ def __init__(self, run_id: str) -> None:
13
+ self._run_id = run_id
14
+ self._nodes: dict[str, str] = {}
15
+
16
+ async def register(self, node: NodeInfo) -> None:
17
+ self._nodes[node.node_id] = node.to_json()
18
+
19
+ async def heartbeat(self, node_id: str, updates: dict) -> None:
20
+ if node_id not in self._nodes:
21
+ return
22
+ import json
23
+ data = json.loads(self._nodes[node_id])
24
+ data.update(updates)
25
+ self._nodes[node_id] = json.dumps(data)
26
+
27
+ async def deregister(self, node_id: str) -> None:
28
+ self._nodes.pop(node_id, None)
29
+
30
+ async def get_all(self) -> list[NodeInfo]:
31
+ out = []
32
+ for v in self._nodes.values():
33
+ try:
34
+ out.append(NodeInfo.from_json(v))
35
+ except Exception:
36
+ pass
37
+ return out
38
+
39
+ async def get_node(self, node_id: str) -> NodeInfo | None:
40
+ raw = self._nodes.get(node_id)
41
+ if not raw:
42
+ return None
43
+ try:
44
+ return NodeInfo.from_json(raw)
45
+ except Exception:
46
+ return None
47
+
48
+ async def get_controllers(self) -> list[NodeInfo]:
49
+ all_nodes = await self.get_all()
50
+ return [n for n in all_nodes if n.role in (NodeRole.CONTROLLER, NodeRole.HYBRID)]
51
+
52
+ async def get_workers(self) -> list[NodeInfo]:
53
+ all_nodes = await self.get_all()
54
+ return [n for n in all_nodes if n.role in (NodeRole.WORKER, NodeRole.HYBRID)]
55
+
56
+ async def is_healthy(self) -> bool:
57
+ return len(await self.get_controllers()) >= 1
58
+
59
+
60
+ class LocalLeaderElector:
61
+ """Single-node: first campaigner wins; no Redis."""
62
+
63
+ def __init__(self, run_id: str) -> None:
64
+ self._run_id = run_id
65
+ self._leader: str | None = None
66
+
67
+ async def campaign(self, node_id: str) -> bool:
68
+ if self._leader is None:
69
+ self._leader = node_id
70
+ return True
71
+ return self._leader == node_id
72
+
73
+ async def refresh(self, node_id: str) -> bool:
74
+ return self._leader == node_id
75
+
76
+ async def get_leader(self) -> str | None:
77
+ return self._leader
78
+
79
+ async def abdicate(self, node_id: str) -> None:
80
+ if self._leader == node_id:
81
+ self._leader = None
82
+
83
+ async def watch(
84
+ self,
85
+ node_id: str,
86
+ on_elected: object,
87
+ on_lost: object,
88
+ ) -> None:
89
+ won = await self.campaign(node_id)
90
+ if won:
91
+ await on_elected()
92
+ while True:
93
+ # Short sleep so dispatch_loop and worker get CPU in single-node
94
+ await asyncio.sleep(0.5)
95
+ if not await self.refresh(node_id):
96
+ await on_lost()
97
+ break
@@ -0,0 +1,77 @@
1
+ """Node metadata and cluster state for distributed execution."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ class NodeRole(Enum):
10
+ CONTROLLER = "controller"
11
+ WORKER = "worker"
12
+ HYBRID = "hybrid"
13
+
14
+
15
+ @dataclass
16
+ class NodeInfo:
17
+ """Serializable node registration info."""
18
+ node_id: str
19
+ role: NodeRole
20
+ host: str
21
+ rpc_port: int
22
+ rpc_url: str
23
+ tags: list[str]
24
+ max_workers: int
25
+ joined_at: str
26
+ last_heartbeat: str
27
+ version: str
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "node_id": self.node_id,
32
+ "role": self.role.value,
33
+ "host": self.host,
34
+ "rpc_port": self.rpc_port,
35
+ "rpc_url": self.rpc_url,
36
+ "tags": list(self.tags),
37
+ "max_workers": self.max_workers,
38
+ "joined_at": self.joined_at,
39
+ "last_heartbeat": self.last_heartbeat,
40
+ "version": self.version,
41
+ }
42
+
43
+ @classmethod
44
+ def from_dict(cls, data: dict[str, Any]) -> "NodeInfo":
45
+ role_val = data.get("role", "worker")
46
+ if isinstance(role_val, NodeRole):
47
+ role = role_val
48
+ else:
49
+ role = NodeRole(role_val) if role_val in ("controller", "worker", "hybrid") else NodeRole.WORKER
50
+ return cls(
51
+ node_id=data["node_id"],
52
+ role=role,
53
+ host=data.get("host", ""),
54
+ rpc_port=int(data.get("rpc_port", 0)),
55
+ rpc_url=data.get("rpc_url", ""),
56
+ tags=list(data.get("tags", [])),
57
+ max_workers=int(data.get("max_workers", 1)),
58
+ joined_at=data.get("joined_at", ""),
59
+ last_heartbeat=data.get("last_heartbeat", ""),
60
+ version=data.get("version", ""),
61
+ )
62
+
63
+ def to_json(self) -> str:
64
+ return json.dumps(self.to_dict())
65
+
66
+ @classmethod
67
+ def from_json(cls, raw: str) -> "NodeInfo":
68
+ return cls.from_dict(json.loads(raw))
69
+
70
+
71
+ @dataclass
72
+ class ClusterState:
73
+ """Current view of the cluster."""
74
+ nodes: dict[str, NodeInfo] = field(default_factory=dict)
75
+ controller_id: str | None = None
76
+ run_id: str | None = None
77
+ quorum: bool = False