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,157 @@
1
+ """Evaluate if: expressions safely (no eval)."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from devsper.workflow.context import WorkflowContext
7
+
8
+
9
+ class WorkflowConditionError(Exception):
10
+ """Raised when a condition expression cannot be parsed or is invalid."""
11
+
12
+ pass
13
+
14
+
15
+ # Pattern: steps.<id>.<field> <op> <value>
16
+ # id/field: word chars and underscore
17
+ # op: ==, !=, >=, <=, >, <, in, not in
18
+ # value: quoted string, int, float, true/false
19
+ _EXPR_PATTERN = re.compile(
20
+ r"^\s*steps\.([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*"
21
+ r"(==|!=|>=|<=|>|<|in|not\s+in)\s*(.+)\s*$",
22
+ re.IGNORECASE,
23
+ )
24
+
25
+
26
+ def _parse_value(raw: str) -> Any:
27
+ raw = raw.strip()
28
+ if not raw:
29
+ raise WorkflowConditionError("Empty value in condition")
30
+ # List: [ ... ]
31
+ if raw.startswith("[") and raw.endswith("]"):
32
+ inner = raw[1:-1].strip()
33
+ if not inner:
34
+ return []
35
+ return [_parse_value(p.strip()) for p in _split_top_level(inner, ",")]
36
+ # Quoted string
37
+ if (raw.startswith("'") and raw.endswith("'")) or (
38
+ raw.startswith('"') and raw.endswith('"')
39
+ ):
40
+ return raw[1:-1].replace("\\'", "'").replace('\\"', '"')
41
+ # Bool
42
+ if raw.lower() == "true":
43
+ return True
44
+ if raw.lower() == "false":
45
+ return False
46
+ # Int
47
+ try:
48
+ return int(raw)
49
+ except ValueError:
50
+ pass
51
+ # Float
52
+ try:
53
+ return float(raw)
54
+ except ValueError:
55
+ pass
56
+ raise WorkflowConditionError(f"Cannot parse value: {raw!r}")
57
+
58
+
59
+ def _split_top_level(s: str, sep: str) -> list[str]:
60
+ """Split by sep only at top level (ignore inside quotes/brackets)."""
61
+ parts: list[str] = []
62
+ current: list[str] = []
63
+ depth = 0
64
+ in_quote = None
65
+ i = 0
66
+ while i < len(s):
67
+ c = s[i]
68
+ if in_quote:
69
+ if c == in_quote and (i == 0 or s[i - 1] != "\\"):
70
+ in_quote = None
71
+ current.append(c)
72
+ elif c in ("'", '"'):
73
+ in_quote = c
74
+ current.append(c)
75
+ elif c in ("[", "(", "{"):
76
+ depth += 1
77
+ current.append(c)
78
+ elif c in ("]", ")", "}"):
79
+ depth -= 1
80
+ current.append(c)
81
+ elif depth == 0 and s[i : i + len(sep)] == sep:
82
+ parts.append("".join(current))
83
+ current = []
84
+ i += len(sep) - 1
85
+ else:
86
+ current.append(c)
87
+ i += 1
88
+ parts.append("".join(current))
89
+ return parts
90
+
91
+
92
+ def evaluate_condition(expression: str, context: WorkflowContext) -> bool:
93
+ """
94
+ Parse expressions of the form: steps.<id>.<field> <op> <value>
95
+ Supported ops: ==, !=, >, <, >=, <=, in, not in
96
+ On missing step/field: return False (conservative — skip rather than crash).
97
+ On parse error: raise WorkflowConditionError.
98
+ """
99
+ expression = expression.strip()
100
+ if not expression:
101
+ raise WorkflowConditionError("Empty condition expression")
102
+
103
+ m = _EXPR_PATTERN.match(expression)
104
+ if not m:
105
+ raise WorkflowConditionError(
106
+ f"Condition must match: steps.<step_id>.<field> <op> <value>. Got: {expression!r}"
107
+ )
108
+
109
+ step_id, field, op, value_str = m.group(1), m.group(2), m.group(3), m.group(4)
110
+ op = op.lower().replace(" ", "")
111
+
112
+ left = context.get_field(step_id, field)
113
+ # Missing step or field → conservative False
114
+ if left is None:
115
+ return False
116
+
117
+ try:
118
+ right = _parse_value(value_str)
119
+ except WorkflowConditionError:
120
+ raise
121
+
122
+ # Type coercion for comparison: if right is int/float and left is str, try to coerce left
123
+ if isinstance(right, (int, float)) and isinstance(left, str):
124
+ try:
125
+ if isinstance(right, int):
126
+ left = int(left)
127
+ else:
128
+ left = float(left)
129
+ except (ValueError, TypeError):
130
+ pass
131
+ if isinstance(right, bool) and isinstance(left, str):
132
+ left = left.lower() in ("true", "1", "yes")
133
+
134
+ if op == "==":
135
+ return left == right
136
+ if op == "!=":
137
+ return left != right
138
+ if op == ">":
139
+ return left > right # type: ignore[return-value]
140
+ if op == "<":
141
+ return left < right # type: ignore[return-value]
142
+ if op == ">=":
143
+ return left >= right # type: ignore[return-value]
144
+ if op == "<=":
145
+ return left <= right # type: ignore[return-value]
146
+ if op == "in":
147
+ if not isinstance(right, (list, tuple, str)):
148
+ raise WorkflowConditionError(f"Right side of 'in' must be list or str, got {type(right)}")
149
+ return left in right # type: ignore[operator]
150
+ if op == "notin":
151
+ if not isinstance(right, (list, tuple, str)):
152
+ raise WorkflowConditionError(
153
+ f"Right side of 'not in' must be list or str, got {type(right)}"
154
+ )
155
+ return left not in right # type: ignore[operator]
156
+
157
+ raise WorkflowConditionError(f"Unsupported operator: {op!r}")
@@ -0,0 +1,108 @@
1
+ """WorkflowContext: typed output passing between steps."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class WorkflowTemplateError(Exception):
10
+ """Raised when a template reference cannot be resolved."""
11
+
12
+ pass
13
+
14
+
15
+ class StepResult(BaseModel):
16
+ step_id: str
17
+ raw_result: str # full agent output
18
+ structured: dict | None = None # parsed output_schema result, if defined
19
+ skipped: bool = False
20
+ error: str | None = None
21
+ duration_seconds: float = 0.0
22
+
23
+
24
+ class WorkflowContext:
25
+ def __init__(self, inputs: dict[str, Any]) -> None:
26
+ self.inputs = inputs
27
+ self.steps: dict[str, StepResult] = {}
28
+
29
+ def record(self, step_id: str, result: StepResult) -> None:
30
+ self.steps[step_id] = result
31
+
32
+ def resolve_template(self, template: str) -> str:
33
+ """
34
+ Replace {input.field} with self.inputs[field].
35
+ Replace {steps.step_id.result} with self.steps[step_id].raw_result.
36
+ Replace {steps.step_id.field} with self.steps[step_id].structured[field].
37
+ Raise WorkflowTemplateError if reference not found.
38
+ """
39
+ # Match {input.NAME} or {steps.STEP_ID.result} or {steps.STEP_ID.FIELD}
40
+ pattern = re.compile(
41
+ r"\{(\w+)\.([^}]+)\}"
42
+ ) # group 1: input|steps, group 2: rest
43
+
44
+ def repl(match: re.Match[str]) -> str:
45
+ prefix, rest = match.group(1), match.group(2)
46
+ if prefix == "input":
47
+ if rest not in self.inputs:
48
+ raise WorkflowTemplateError(
49
+ f"Template references input.{rest} but input {rest!r} is not provided. "
50
+ f"Available: {list(self.inputs.keys())}"
51
+ )
52
+ val = self.inputs[rest]
53
+ return str(val) if val is not None else ""
54
+ if prefix == "steps":
55
+ part = rest.split(".", 1)
56
+ if len(part) != 2:
57
+ raise WorkflowTemplateError(
58
+ f"Invalid steps reference: {match.group(0)}. "
59
+ "Use steps.<step_id>.result or steps.<step_id>.<field>"
60
+ )
61
+ step_id, field = part[0], part[1]
62
+ if step_id not in self.steps:
63
+ raise WorkflowTemplateError(
64
+ f"Template references steps.{step_id}.{field} but step {step_id!r} "
65
+ f"has not run yet or does not exist. Available: {list(self.steps.keys())}"
66
+ )
67
+ sr = self.steps[step_id]
68
+ if field == "result":
69
+ return sr.raw_result or ""
70
+ if sr.structured is not None and field in sr.structured:
71
+ val = sr.structured[field]
72
+ return str(val) if val is not None else ""
73
+ raise WorkflowTemplateError(
74
+ f"Template references steps.{step_id}.{field} but step {step_id!r} "
75
+ f"has no structured field {field!r}. "
76
+ f"Available: result, or {list(sr.structured.keys()) if sr.structured else []}"
77
+ )
78
+ raise WorkflowTemplateError(f"Unknown template prefix: {prefix!r}")
79
+
80
+ return pattern.sub(repl, template)
81
+
82
+ def get_field(self, step_id: str, field: str) -> Any:
83
+ """Used by condition evaluator. Returns None if step/field missing."""
84
+ if step_id not in self.steps:
85
+ return None
86
+ sr = self.steps[step_id]
87
+ if field == "result":
88
+ return sr.raw_result
89
+ if sr.structured is not None and field in sr.structured:
90
+ return sr.structured[field]
91
+ return None
92
+
93
+ def to_summary(self) -> dict[str, Any]:
94
+ """Serializable summary for output/replay."""
95
+ steps_summary: dict[str, dict[str, Any]] = {}
96
+ for sid, sr in self.steps.items():
97
+ steps_summary[sid] = {
98
+ "step_id": sr.step_id,
99
+ "skipped": sr.skipped,
100
+ "error": sr.error,
101
+ "duration_seconds": sr.duration_seconds,
102
+ "has_result": bool(sr.raw_result),
103
+ "has_structured": sr.structured is not None,
104
+ }
105
+ return {
106
+ "inputs": dict(self.inputs),
107
+ "steps": steps_summary,
108
+ }
@@ -0,0 +1,156 @@
1
+ """Load workflow definitions from workflow.devsper.toml or devsper.toml."""
2
+
3
+ import secrets
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import tomllib
8
+
9
+ from devsper.workflow.schema import (
10
+ OutputField,
11
+ StepCondition,
12
+ WorkflowDefinition,
13
+ WorkflowStep,
14
+ )
15
+
16
+
17
+ def _find_workflow_file() -> Path | None:
18
+ cwd = Path.cwd()
19
+ for base in (cwd, cwd.parent):
20
+ for name in ("workflow.devsper.toml", "devsper.toml"):
21
+ p = base / name
22
+ if p.is_file():
23
+ return p
24
+ return None
25
+
26
+
27
+ def _load_toml(path: Path) -> dict:
28
+ if not path.is_file():
29
+ return {}
30
+ try:
31
+ with open(path, "rb") as f:
32
+ return tomllib.load(f)
33
+ except Exception:
34
+ return {}
35
+
36
+
37
+ def _workflow_from_legacy_steps(steps: list[str]) -> WorkflowDefinition:
38
+ """Wrap a list of task strings into a WorkflowDefinition with auto-generated step ids."""
39
+ workflow_steps = []
40
+ for i, task in enumerate(steps):
41
+ step_id = f"step_{secrets.token_hex(3)}"
42
+ workflow_steps.append(
43
+ WorkflowStep(
44
+ id=step_id,
45
+ task=task,
46
+ depends_on=[workflow_steps[-1].id] if workflow_steps else [],
47
+ )
48
+ )
49
+ return WorkflowDefinition(
50
+ name="legacy",
51
+ description="Legacy workflow from list of steps",
52
+ version="1.0",
53
+ steps=workflow_steps,
54
+ inputs=[],
55
+ )
56
+
57
+
58
+ def _step_dict_to_model(d: dict[str, Any]) -> WorkflowStep:
59
+ """Convert a raw step dict (from TOML) to WorkflowStep."""
60
+ # TOML may have "if" as key; Pydantic expects alias "if" -> if_
61
+ raw = dict(d)
62
+ if "if" in raw and isinstance(raw["if"], dict):
63
+ raw["if"] = StepCondition(expression=raw["if"].get("expression", ""))
64
+ elif "if" in raw and isinstance(raw["if"], str):
65
+ raw["if"] = StepCondition(expression=raw["if"])
66
+ if "output_schema" in raw and isinstance(raw["output_schema"], list):
67
+ raw["output_schema"] = [
68
+ OutputField(**f) if isinstance(f, dict) else f for f in raw["output_schema"]
69
+ ]
70
+ return WorkflowStep(**raw)
71
+
72
+
73
+ def _workflow_dict_to_definition(name: str, raw: dict[str, Any]) -> WorkflowDefinition:
74
+ """Convert raw workflow dict to WorkflowDefinition."""
75
+ steps_raw = raw.get("steps") or []
76
+ if not steps_raw:
77
+ return WorkflowDefinition(
78
+ name=name,
79
+ description=raw.get("description"),
80
+ version=str(raw.get("version", "1.0")),
81
+ steps=[],
82
+ inputs=raw.get("inputs") or [],
83
+ )
84
+ # Legacy: list of strings
85
+ if all(isinstance(s, str) for s in steps_raw):
86
+ wf = _workflow_from_legacy_steps(steps_raw)
87
+ wf = WorkflowDefinition(
88
+ name=name,
89
+ description=raw.get("description") or wf.description,
90
+ version=str(raw.get("version", "1.0")),
91
+ steps=wf.steps,
92
+ inputs=raw.get("inputs") or [],
93
+ )
94
+ return wf
95
+ # New format: list of step dicts
96
+ steps = []
97
+ for s in steps_raw:
98
+ if isinstance(s, dict):
99
+ steps.append(_step_dict_to_model(s))
100
+ else:
101
+ steps.append(s)
102
+ inputs_raw = raw.get("inputs") or []
103
+ inputs = [OutputField(**f) if isinstance(f, dict) else f for f in inputs_raw]
104
+ return WorkflowDefinition(
105
+ name=name,
106
+ description=raw.get("description"),
107
+ version=str(raw.get("version", "1.0")),
108
+ steps=steps,
109
+ inputs=inputs,
110
+ )
111
+
112
+
113
+ def load_workflow(name: str, config_path: Path | None = None) -> WorkflowDefinition | None:
114
+ """
115
+ Load workflow by name. Returns WorkflowDefinition (Pydantic model).
116
+ If config_path is given, read that file; else discover workflow.devsper.toml / devsper.toml.
117
+ Legacy: if steps are a list of strings, they are wrapped in a WorkflowDefinition with
118
+ auto-generated step ids and sequential dependencies.
119
+ """
120
+ path = config_path or _find_workflow_file()
121
+ if not path:
122
+ return None
123
+ data = _load_toml(path)
124
+ raw: dict[str, Any] | None = None
125
+ # Single [workflow] section
126
+ wf = data.get("workflow")
127
+ if isinstance(wf, dict) and wf.get("name") == name:
128
+ raw = {"name": name, **wf}
129
+ if raw is None:
130
+ for key, val in data.items():
131
+ if key.startswith("workflow.") and isinstance(val, dict):
132
+ if val.get("name") == name or key == f"workflow.{name}":
133
+ raw = {"name": name, **val}
134
+ break
135
+ if not raw:
136
+ return None
137
+ return _workflow_dict_to_definition(name, raw)
138
+
139
+
140
+ def list_workflows(config_path: Path | None = None) -> list[str]:
141
+ """Return list of workflow identifiers (name or key suffix) that load_workflow() accepts."""
142
+ path = config_path or _find_workflow_file()
143
+ if not path:
144
+ return []
145
+ data = _load_toml(path)
146
+ names: list[str] = []
147
+ wf = data.get("workflow")
148
+ if isinstance(wf, dict) and wf.get("name"):
149
+ names.append(str(wf["name"]))
150
+ for key, val in data.items():
151
+ if key.startswith("workflow.") and isinstance(val, dict):
152
+ # Use key suffix so load_workflow(key_suffix) works
153
+ key_suffix = key.removeprefix("workflow.")
154
+ if key_suffix and key_suffix not in names:
155
+ names.append(key_suffix)
156
+ return list(dict.fromkeys(names))
@@ -0,0 +1,109 @@
1
+ """Resolve step dependencies into execution order (waves)."""
2
+
3
+ from collections import deque
4
+
5
+ from devsper.workflow.schema import WorkflowStep
6
+
7
+
8
+ class WorkflowCycleError(Exception):
9
+ """Raised when depends_on forms a cycle."""
10
+
11
+ def __init__(self, cycle: list[str]) -> None:
12
+ self.cycle = cycle
13
+ super().__init__(f"Dependency cycle: {' -> '.join(cycle)}")
14
+
15
+
16
+ def build_execution_order(steps: list[WorkflowStep]) -> list[list[WorkflowStep]]:
17
+ """
18
+ Topological sort of steps using depends_on edges.
19
+ Returns list of "waves"; steps in the same wave can run in parallel.
20
+ Steps with no depends_on are in wave 0.
21
+ Raises WorkflowCycleError with the cycle path if a cycle is detected.
22
+ """
23
+ step_map = {s.id: s for s in steps}
24
+ # in_degree[s] = number of dependencies of s (steps s waits for)
25
+ in_degree = {s.id: len(s.depends_on) for s in steps}
26
+ # dependants[dep] = list of step ids that depend on dep
27
+ dependants: dict[str, list[str]] = {s.id: [] for s in steps}
28
+ for s in steps:
29
+ for dep in s.depends_on:
30
+ dependants[dep].append(s.id)
31
+
32
+ waves: list[list[WorkflowStep]] = []
33
+ queue: deque[str] = deque(sid for sid, d in in_degree.items() if d == 0)
34
+ remaining = set(step_map.keys())
35
+
36
+ while queue:
37
+ wave_ids: list[str] = list(queue)
38
+ queue.clear()
39
+ for sid in wave_ids:
40
+ remaining.discard(sid)
41
+ for nxt in dependants[sid]:
42
+ in_degree[nxt] -= 1
43
+ if in_degree[nxt] == 0:
44
+ queue.append(nxt)
45
+ waves.append([step_map[sid] for sid in wave_ids])
46
+
47
+ if remaining:
48
+ # Cycle: find one
49
+ cycle = _find_cycle(steps)
50
+ raise WorkflowCycleError(cycle)
51
+
52
+ return waves
53
+
54
+
55
+ def _find_cycle(steps: list[WorkflowStep]) -> list[str]:
56
+ """Return one cycle as list of step ids."""
57
+ step_map = {s.id: s for s in steps}
58
+ visited: set[str] = set()
59
+ path: list[str] = []
60
+ path_set: set[str] = set()
61
+ cycle_start: str | None = None
62
+
63
+ def dfs(sid: str) -> bool:
64
+ nonlocal cycle_start
65
+ visited.add(sid)
66
+ path.append(sid)
67
+ path_set.add(sid)
68
+ step = step_map.get(sid)
69
+ if step:
70
+ for dep in step.depends_on:
71
+ if dep not in visited:
72
+ if dfs(dep):
73
+ return True
74
+ elif dep in path_set:
75
+ cycle_start = dep
76
+ return True
77
+ path.pop()
78
+ path_set.discard(sid)
79
+ return False
80
+
81
+ for sid in step_map:
82
+ if sid not in visited and dfs(sid):
83
+ # Build cycle from path: from cycle_start to end of path
84
+ try:
85
+ idx = path.index(cycle_start)
86
+ return path[idx:] + [cycle_start]
87
+ except (ValueError, TypeError):
88
+ return path + [path[0]] if path else list(step_map.keys())[:1]
89
+
90
+ return list(step_map.keys())[:1]
91
+
92
+
93
+ def validate_dag(steps: list[WorkflowStep]) -> list[str]:
94
+ """Return list of error strings (used by validator)."""
95
+ errors: list[str] = []
96
+ step_ids = {s.id for s in steps}
97
+
98
+ for s in steps:
99
+ for dep in s.depends_on:
100
+ if dep not in step_ids:
101
+ errors.append(f"Step {s.id!r} depends_on unknown step {dep!r}")
102
+
103
+ if not errors:
104
+ try:
105
+ build_execution_order(steps)
106
+ except WorkflowCycleError as e:
107
+ errors.append(str(e))
108
+
109
+ return errors