devsper 2.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Cluster registry backed by Redis hash."""
|
|
2
|
+
|
|
3
|
+
from devsper.cluster.node_info import NodeInfo, NodeRole
|
|
4
|
+
|
|
5
|
+
REGISTRY_KEY_PREFIX = "devsper:cluster:"
|
|
6
|
+
REGISTRY_NODES_SUFFIX = ":nodes"
|
|
7
|
+
REGISTRY_TTL = 60
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClusterRegistry:
|
|
11
|
+
"""Backed by Redis hash: devsper:cluster:{run_id}:nodes. TTL 60s refreshed by heartbeat."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, redis_client: object, run_id: str) -> None:
|
|
14
|
+
self._redis = redis_client
|
|
15
|
+
self._run_id = run_id
|
|
16
|
+
self._key = f"{REGISTRY_KEY_PREFIX}{run_id}{REGISTRY_NODES_SUFFIX}"
|
|
17
|
+
|
|
18
|
+
async def register(self, node: NodeInfo) -> None:
|
|
19
|
+
await self._redis.hset(self._key, node.node_id, node.to_json())
|
|
20
|
+
await self._redis.expire(self._key, REGISTRY_TTL)
|
|
21
|
+
|
|
22
|
+
async def heartbeat(self, node_id: str, updates: dict) -> None:
|
|
23
|
+
node = await self.get_node(node_id)
|
|
24
|
+
if node is None:
|
|
25
|
+
return
|
|
26
|
+
d = node.to_dict()
|
|
27
|
+
d.update(updates)
|
|
28
|
+
updated = NodeInfo.from_dict(d)
|
|
29
|
+
await self._redis.hset(self._key, node_id, updated.to_json())
|
|
30
|
+
await self._redis.expire(self._key, REGISTRY_TTL)
|
|
31
|
+
|
|
32
|
+
async def deregister(self, node_id: str) -> None:
|
|
33
|
+
await self._redis.hdel(self._key, node_id)
|
|
34
|
+
|
|
35
|
+
async def get_all(self) -> list[NodeInfo]:
|
|
36
|
+
raw = await self._redis.hgetall(self._key)
|
|
37
|
+
out: list[NodeInfo] = []
|
|
38
|
+
for _k, v in raw.items():
|
|
39
|
+
val = v.decode("utf-8") if isinstance(v, bytes) else v
|
|
40
|
+
try:
|
|
41
|
+
out.append(NodeInfo.from_json(val))
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
async def get_node(self, node_id: str) -> NodeInfo | None:
|
|
47
|
+
raw = await self._redis.hget(self._key, node_id)
|
|
48
|
+
if raw is None:
|
|
49
|
+
return None
|
|
50
|
+
val = raw.decode("utf-8") if isinstance(raw, bytes) else raw
|
|
51
|
+
try:
|
|
52
|
+
return NodeInfo.from_json(val)
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
async def get_controllers(self) -> list[NodeInfo]:
|
|
57
|
+
all_nodes = await self.get_all()
|
|
58
|
+
return [
|
|
59
|
+
n for n in all_nodes
|
|
60
|
+
if n.role in (NodeRole.CONTROLLER, NodeRole.HYBRID)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
async def get_workers(self) -> list[NodeInfo]:
|
|
64
|
+
all_nodes = await self.get_all()
|
|
65
|
+
return [
|
|
66
|
+
n for n in all_nodes
|
|
67
|
+
if n.role in (NodeRole.WORKER, NodeRole.HYBRID)
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
async def is_healthy(self) -> bool:
|
|
71
|
+
return len(await self.get_controllers()) >= 1
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Task routing with affinity: memory, tool cache, load."""
|
|
2
|
+
|
|
3
|
+
from devsper.cluster.node_info import NodeInfo
|
|
4
|
+
from devsper.types.task import Task
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _parse_version(version: str) -> tuple[int, int]:
|
|
8
|
+
"""Return (major, minor) from version string like 1.10 or 1.9.0."""
|
|
9
|
+
parts = version.replace("-", ".").split(".")
|
|
10
|
+
try:
|
|
11
|
+
major = int(parts[0]) if parts else 0
|
|
12
|
+
minor = int(parts[1]) if len(parts) > 1 else 0
|
|
13
|
+
except (ValueError, IndexError):
|
|
14
|
+
major, minor = 0, 0
|
|
15
|
+
return (major, minor)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
19
|
+
return max(lo, min(hi, value))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskRouter:
|
|
23
|
+
"""Route ready tasks to the best available worker. Scoring: memory_affinity, tool_affinity, load_score."""
|
|
24
|
+
|
|
25
|
+
# Weights
|
|
26
|
+
MEMORY_WEIGHT = 0.35
|
|
27
|
+
TOOL_WEIGHT = 0.25
|
|
28
|
+
LOAD_WEIGHT = 0.40
|
|
29
|
+
|
|
30
|
+
def __init__(self, controller_version: str = "1.9.0") -> None:
|
|
31
|
+
self._controller_version = controller_version
|
|
32
|
+
self._ctrl_major, self._ctrl_minor = _parse_version(controller_version)
|
|
33
|
+
self._round_robin_index: int = 0 # spread tasks when scores tie
|
|
34
|
+
|
|
35
|
+
def route(
|
|
36
|
+
self,
|
|
37
|
+
task: Task,
|
|
38
|
+
workers: list[NodeInfo],
|
|
39
|
+
worker_stats: dict[str, dict],
|
|
40
|
+
) -> NodeInfo | None:
|
|
41
|
+
"""Score all workers; return highest-scored; when scores tie, round-robin so tasks spread."""
|
|
42
|
+
if not workers:
|
|
43
|
+
return None
|
|
44
|
+
eligible = [w for w in workers if self._version_compatible(w)]
|
|
45
|
+
if not eligible:
|
|
46
|
+
return None
|
|
47
|
+
scored = [(self._score(task, w, worker_stats.get(w.node_id, {})), w) for w in eligible]
|
|
48
|
+
best_score = max(s[0] for s in scored)
|
|
49
|
+
if best_score < 0:
|
|
50
|
+
return None
|
|
51
|
+
# Treat scores within 1e-6 as tied so we round-robin and spread (avoids one worker getting all + 429)
|
|
52
|
+
eps = 1e-6
|
|
53
|
+
tied = [w for s, w in scored if abs(s - best_score) <= eps]
|
|
54
|
+
# Round-robin among tied workers so multiple tasks go to different workers
|
|
55
|
+
idx = self._round_robin_index % len(tied)
|
|
56
|
+
self._round_robin_index += 1
|
|
57
|
+
return tied[idx]
|
|
58
|
+
|
|
59
|
+
def _version_compatible(self, worker: NodeInfo) -> bool:
|
|
60
|
+
maj, min_ = _parse_version(worker.version)
|
|
61
|
+
return (maj == self._ctrl_major and min_ == self._ctrl_minor)
|
|
62
|
+
|
|
63
|
+
def _score(
|
|
64
|
+
self,
|
|
65
|
+
task: Task,
|
|
66
|
+
worker: NodeInfo,
|
|
67
|
+
stats: dict,
|
|
68
|
+
) -> float:
|
|
69
|
+
memory_affinity = self._memory_affinity(task, stats)
|
|
70
|
+
tool_affinity = self._tool_affinity(task, stats)
|
|
71
|
+
load_score = self._load_score(worker, stats)
|
|
72
|
+
return (
|
|
73
|
+
memory_affinity * self.MEMORY_WEIGHT
|
|
74
|
+
+ tool_affinity * self.TOOL_WEIGHT
|
|
75
|
+
+ load_score * self.LOAD_WEIGHT
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _memory_affinity(self, task: Task, stats: dict) -> float:
|
|
79
|
+
deps = list(task.dependencies or [])
|
|
80
|
+
if not deps:
|
|
81
|
+
return 0.0
|
|
82
|
+
completed_here = list(stats.get("completed_task_ids", []))
|
|
83
|
+
hit = sum(1 for d in deps if d in completed_here)
|
|
84
|
+
return hit / len(deps)
|
|
85
|
+
|
|
86
|
+
def _tool_affinity(self, task: Task, stats: dict) -> float:
|
|
87
|
+
tools_needed = self._tools_for_task(task)
|
|
88
|
+
if not tools_needed:
|
|
89
|
+
return 0.0
|
|
90
|
+
cached = set(stats.get("cached_tools", []))
|
|
91
|
+
hit = sum(1 for t in tools_needed if t in cached)
|
|
92
|
+
return hit / len(tools_needed)
|
|
93
|
+
|
|
94
|
+
def _tools_for_task(self, task: Task) -> list[str]:
|
|
95
|
+
try:
|
|
96
|
+
from devsper.tools.selector import get_tools_for_task
|
|
97
|
+
from devsper.tools.scoring import get_default_score_store
|
|
98
|
+
score_store = get_default_score_store()
|
|
99
|
+
except Exception:
|
|
100
|
+
score_store = None
|
|
101
|
+
try:
|
|
102
|
+
tools = get_tools_for_task(
|
|
103
|
+
task.description or "",
|
|
104
|
+
role=getattr(task, "role", None),
|
|
105
|
+
score_store=score_store,
|
|
106
|
+
)
|
|
107
|
+
return [t.name for t in tools]
|
|
108
|
+
except Exception:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
def _load_score(self, worker: NodeInfo, stats: dict) -> float:
|
|
112
|
+
active = int(stats.get("active_tasks", 0))
|
|
113
|
+
avg_duration = float(stats.get("avg_task_duration_seconds", 0.0))
|
|
114
|
+
max_workers = worker.max_workers or 1
|
|
115
|
+
weighted_load = active * avg_duration
|
|
116
|
+
load_ratio = weighted_load / (max_workers * 60.0)
|
|
117
|
+
return 1.0 - _clamp(load_ratio, 0.0, 1.0)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Shared state backend for scheduler snapshots (Redis or filesystem)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StateBackend(ABC):
|
|
9
|
+
"""Abstract backend for saving/loading scheduler snapshots."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def save_snapshot(self, run_id: str, snapshot: dict) -> None:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
async def load_snapshot(self, run_id: str) -> dict | None:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def delete_snapshot(self, run_id: str) -> None:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def list_snapshots(self) -> list[str]:
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RedisStateBackend(StateBackend):
|
|
29
|
+
"""Store snapshots in Redis. Key: devsper:snapshot:{run_id}, set index: devsper:snapshots."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, redis_client: object) -> None:
|
|
32
|
+
self._redis = redis_client
|
|
33
|
+
self._key_prefix = "devsper:snapshot:"
|
|
34
|
+
self._index_key = "devsper:snapshots"
|
|
35
|
+
|
|
36
|
+
async def save_snapshot(self, run_id: str, snapshot: dict) -> None:
|
|
37
|
+
key = f"{self._key_prefix}{run_id}"
|
|
38
|
+
await self._redis.set(key, json.dumps(snapshot))
|
|
39
|
+
await self._redis.sadd(self._index_key, run_id)
|
|
40
|
+
|
|
41
|
+
async def load_snapshot(self, run_id: str) -> dict | None:
|
|
42
|
+
key = f"{self._key_prefix}{run_id}"
|
|
43
|
+
raw = await self._redis.get(key)
|
|
44
|
+
if raw is None:
|
|
45
|
+
return None
|
|
46
|
+
if isinstance(raw, bytes):
|
|
47
|
+
raw = raw.decode("utf-8")
|
|
48
|
+
return json.loads(raw)
|
|
49
|
+
|
|
50
|
+
async def delete_snapshot(self, run_id: str) -> None:
|
|
51
|
+
key = f"{self._key_prefix}{run_id}"
|
|
52
|
+
await self._redis.delete(key)
|
|
53
|
+
await self._redis.srem(self._index_key, run_id)
|
|
54
|
+
|
|
55
|
+
async def list_snapshots(self) -> list[str]:
|
|
56
|
+
members = await self._redis.smembers(self._index_key)
|
|
57
|
+
return [m.decode("utf-8") if isinstance(m, bytes) else str(m) for m in members]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FilesystemStateBackend(StateBackend):
|
|
61
|
+
"""Single-node: write snapshots to events_dir as {run_id}.snapshot.json. Atomic write."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, events_dir: str) -> None:
|
|
64
|
+
self._events_dir = events_dir
|
|
65
|
+
|
|
66
|
+
def _path(self, run_id: str) -> str:
|
|
67
|
+
return os.path.join(self._events_dir, f"{run_id}.snapshot.json")
|
|
68
|
+
|
|
69
|
+
async def save_snapshot(self, run_id: str, snapshot: dict) -> None:
|
|
70
|
+
path = self._path(run_id)
|
|
71
|
+
tmp = path + ".tmp"
|
|
72
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
73
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
74
|
+
json.dump(snapshot, f, indent=0)
|
|
75
|
+
os.replace(tmp, path)
|
|
76
|
+
|
|
77
|
+
async def load_snapshot(self, run_id: str) -> dict | None:
|
|
78
|
+
path = self._path(run_id)
|
|
79
|
+
if not os.path.isfile(path):
|
|
80
|
+
return None
|
|
81
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
82
|
+
return json.load(f)
|
|
83
|
+
|
|
84
|
+
async def delete_snapshot(self, run_id: str) -> None:
|
|
85
|
+
path = self._path(run_id)
|
|
86
|
+
if os.path.isfile(path):
|
|
87
|
+
os.remove(path)
|
|
88
|
+
|
|
89
|
+
async def list_snapshots(self) -> list[str]:
|
|
90
|
+
if not os.path.isdir(self._events_dir):
|
|
91
|
+
return []
|
|
92
|
+
out: list[str] = []
|
|
93
|
+
for name in os.listdir(self._events_dir):
|
|
94
|
+
if name.endswith(".snapshot.json"):
|
|
95
|
+
out.append(name.removesuffix(".snapshot.json"))
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_state_backend(config: object, redis_client: object | None = None) -> StateBackend:
|
|
100
|
+
"""Return StateBackend from config. Redis if bus.backend == redis else filesystem."""
|
|
101
|
+
backend = getattr(getattr(config, "bus", None), "backend", "memory")
|
|
102
|
+
if backend == "redis" and redis_client is not None:
|
|
103
|
+
return RedisStateBackend(redis_client)
|
|
104
|
+
events_dir = getattr(config, "events_dir", ".devsper/events")
|
|
105
|
+
return FilesystemStateBackend(events_dir)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PIIRedactor: scan and redact PII from text before storage.
|
|
3
|
+
Detectors: regex (EMAIL, PHONE, SSN, CREDIT_CARD, IP, API_KEY) + optional spaCy NER (NAME, ADDRESS).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PIIDetection:
|
|
12
|
+
pii_type: str
|
|
13
|
+
start: int
|
|
14
|
+
end: int
|
|
15
|
+
confidence: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RedactionResult:
|
|
20
|
+
redacted_text: str
|
|
21
|
+
detections: list[PIIDetection] = field(default_factory=list)
|
|
22
|
+
pii_found: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Regex patterns (non-capturing for substitution)
|
|
26
|
+
PATTERNS = {
|
|
27
|
+
"EMAIL": re.compile(
|
|
28
|
+
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
),
|
|
31
|
+
"PHONE": re.compile(
|
|
32
|
+
r"\+?[\d\s\-()]{10,20}\d",
|
|
33
|
+
),
|
|
34
|
+
"SSN": re.compile(
|
|
35
|
+
r"\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b",
|
|
36
|
+
),
|
|
37
|
+
"CREDIT_CARD": re.compile(
|
|
38
|
+
r"\b(?:\d[-\s]*){13,19}\d\b",
|
|
39
|
+
),
|
|
40
|
+
"IP_ADDRESS": re.compile(
|
|
41
|
+
r"\b(?:\d{1,3}\.){3}\d{1,3}\b|\[?[0-9a-fA-F:.]+\]?",
|
|
42
|
+
),
|
|
43
|
+
"API_KEY": re.compile(
|
|
44
|
+
r"\b(?:sk|pk)[-_][a-zA-Z0-9]{20,}\b",
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _luhn_ok(digits: str) -> bool:
|
|
50
|
+
s = digits.replace(" ", "").replace("-", "")
|
|
51
|
+
if len(s) < 13:
|
|
52
|
+
return False
|
|
53
|
+
try:
|
|
54
|
+
nums = [int(c) for c in s]
|
|
55
|
+
except ValueError:
|
|
56
|
+
return False
|
|
57
|
+
total = 0
|
|
58
|
+
for i, d in enumerate(reversed(nums)):
|
|
59
|
+
if i % 2 == 1:
|
|
60
|
+
d *= 2
|
|
61
|
+
if d > 9:
|
|
62
|
+
d -= 9
|
|
63
|
+
total += d
|
|
64
|
+
return total % 10 == 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PIIRedactor:
|
|
68
|
+
"""Redact PII from text. Configurable pii_types; optional spaCy for NAME/ADDRESS when gdpr_mode."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
pii_types: list[str] | None = None,
|
|
73
|
+
gdpr_mode: bool = False,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.pii_types = pii_types or list(PATTERNS.keys())
|
|
76
|
+
self.gdpr_mode = gdpr_mode
|
|
77
|
+
self._ner_available: bool | None = None
|
|
78
|
+
|
|
79
|
+
def _ner_detect(self, text: str) -> list[PIIDetection]:
|
|
80
|
+
out: list[PIIDetection] = []
|
|
81
|
+
if self._ner_available is False:
|
|
82
|
+
return out
|
|
83
|
+
try:
|
|
84
|
+
import spacy
|
|
85
|
+
if self._ner_available is None:
|
|
86
|
+
try:
|
|
87
|
+
nlp = spacy.load("en_core_web_sm")
|
|
88
|
+
except Exception:
|
|
89
|
+
nlp = spacy.blank("en")
|
|
90
|
+
setattr(self, "_ner", nlp)
|
|
91
|
+
self._ner_available = True
|
|
92
|
+
except ImportError:
|
|
93
|
+
self._ner_available = False
|
|
94
|
+
return out
|
|
95
|
+
nlp = getattr(self, "_ner", None)
|
|
96
|
+
if nlp is None:
|
|
97
|
+
return out
|
|
98
|
+
doc = nlp(text)
|
|
99
|
+
for ent in doc.ents:
|
|
100
|
+
if ent.label_ in ("PERSON", "GPE", "LOC") and (self.gdpr_mode or ent.label_ == "PERSON"):
|
|
101
|
+
out.append(PIIDetection(
|
|
102
|
+
pii_type="NAME" if ent.label_ == "PERSON" else "ADDRESS",
|
|
103
|
+
start=ent.start_char,
|
|
104
|
+
end=ent.end_char,
|
|
105
|
+
confidence=0.9,
|
|
106
|
+
))
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
def redact(self, text: str) -> RedactionResult:
|
|
110
|
+
if not text:
|
|
111
|
+
return RedactionResult(redacted_text="", detections=[], pii_found=False)
|
|
112
|
+
detections: list[PIIDetection] = []
|
|
113
|
+
ner_done = False
|
|
114
|
+
for pii_type in self.pii_types:
|
|
115
|
+
if pii_type not in PATTERNS and pii_type not in ("NAME", "ADDRESS"):
|
|
116
|
+
continue
|
|
117
|
+
if pii_type in ("NAME", "ADDRESS") or self.gdpr_mode:
|
|
118
|
+
if not ner_done:
|
|
119
|
+
detections.extend(self._ner_detect(text))
|
|
120
|
+
ner_done = True
|
|
121
|
+
continue
|
|
122
|
+
pattern = PATTERNS[pii_type]
|
|
123
|
+
for m in pattern.finditer(text):
|
|
124
|
+
snippet = m.group(0)
|
|
125
|
+
if pii_type == "CREDIT_CARD" and not _luhn_ok(snippet):
|
|
126
|
+
continue
|
|
127
|
+
detections.append(PIIDetection(
|
|
128
|
+
pii_type=pii_type,
|
|
129
|
+
start=m.start(),
|
|
130
|
+
end=m.end(),
|
|
131
|
+
confidence=0.95,
|
|
132
|
+
))
|
|
133
|
+
detections.sort(key=lambda d: (d.start, -d.end))
|
|
134
|
+
merged: list[PIIDetection] = []
|
|
135
|
+
for d in detections:
|
|
136
|
+
if merged and d.start < merged[-1].end:
|
|
137
|
+
continue
|
|
138
|
+
merged.append(d)
|
|
139
|
+
result = list(text)
|
|
140
|
+
for d in reversed(merged):
|
|
141
|
+
result[d.start:d.end] = f"[REDACTED:{d.pii_type}]"
|
|
142
|
+
redacted_text = "".join(result)
|
|
143
|
+
return RedactionResult(
|
|
144
|
+
redacted_text=redacted_text,
|
|
145
|
+
detections=merged,
|
|
146
|
+
pii_found=len(merged) > 0,
|
|
147
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devsper configuration: TOML + env, Pydantic-validated.
|
|
3
|
+
|
|
4
|
+
Priority: env > project config > user config > defaults.
|
|
5
|
+
Config locations: ./devsper.toml, ./workflow.devsper.toml, ~/.config/devsper/config.toml,
|
|
6
|
+
and legacy .devsper/config.toml.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from devsper.config.resolver import resolve_config
|
|
10
|
+
from devsper.config.schema import (
|
|
11
|
+
A2AConfig,
|
|
12
|
+
devsperConfigModel,
|
|
13
|
+
KnowledgeConfig,
|
|
14
|
+
MCPConfig,
|
|
15
|
+
MemoryConfig,
|
|
16
|
+
ModelsConfig,
|
|
17
|
+
NodesConfig,
|
|
18
|
+
ProviderAzureConfig,
|
|
19
|
+
ProvidersConfig,
|
|
20
|
+
SwarmConfig,
|
|
21
|
+
TelemetryConfig,
|
|
22
|
+
ToolsConfig,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Backward compatibility: old code expects devsperConfig and get_config()
|
|
26
|
+
devsperConfig = devsperConfigModel
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_config(config_path: str | None = None) -> devsperConfigModel:
|
|
30
|
+
"""
|
|
31
|
+
Load and resolve configuration.
|
|
32
|
+
Returns object with .worker_model, .planner_model, .events_dir, .data_dir,
|
|
33
|
+
and .swarm, .models, .memory, .tools, .telemetry, .providers.
|
|
34
|
+
"""
|
|
35
|
+
return resolve_config(config_path=config_path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"A2AConfig",
|
|
40
|
+
"get_config",
|
|
41
|
+
"devsperConfig",
|
|
42
|
+
"devsperConfigModel",
|
|
43
|
+
"KnowledgeConfig",
|
|
44
|
+
"MCPConfig",
|
|
45
|
+
"MemoryConfig",
|
|
46
|
+
"ModelsConfig",
|
|
47
|
+
"ProviderAzureConfig",
|
|
48
|
+
"ProvidersConfig",
|
|
49
|
+
"SwarmConfig",
|
|
50
|
+
"TelemetryConfig",
|
|
51
|
+
"ToolsConfig",
|
|
52
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Discover and load TOML config from standard locations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_toml(path: Path) -> dict:
|
|
9
|
+
if not path.is_file():
|
|
10
|
+
return {}
|
|
11
|
+
try:
|
|
12
|
+
with open(path, "rb") as f:
|
|
13
|
+
return tomllib.load(f)
|
|
14
|
+
except Exception:
|
|
15
|
+
return {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def user_config_path() -> Path:
|
|
19
|
+
return Path.home() / ".config" / "devsper" / "config.toml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def project_config_paths() -> list[Path]:
|
|
23
|
+
"""Return candidate project config paths in order: devsper.toml, workflow.devsper.toml, .devsper/config.toml (legacy)."""
|
|
24
|
+
cwd = Path.cwd()
|
|
25
|
+
paths = [
|
|
26
|
+
cwd / "devsper.toml",
|
|
27
|
+
cwd / "workflow.devsper.toml",
|
|
28
|
+
cwd / ".devsper" / "config.toml",
|
|
29
|
+
cwd.parent / "devsper.toml",
|
|
30
|
+
cwd.parent / "workflow.devsper.toml",
|
|
31
|
+
cwd.parent / ".devsper" / "config.toml",
|
|
32
|
+
]
|
|
33
|
+
return paths
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_user_config() -> dict:
|
|
37
|
+
return _load_toml(user_config_path())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_project_config() -> dict:
|
|
41
|
+
"""Load first existing project config file (priority: devsper.toml > workflow.devsper.toml > .devsper/config.toml)."""
|
|
42
|
+
for p in project_config_paths():
|
|
43
|
+
data = _load_toml(p)
|
|
44
|
+
if data:
|
|
45
|
+
return data
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_legacy_defaults(data: dict) -> dict:
|
|
50
|
+
"""Map legacy [default] or top-level keys into a flat dict for merging."""
|
|
51
|
+
out: dict = {}
|
|
52
|
+
default_block = data.get("default")
|
|
53
|
+
if isinstance(default_block, dict):
|
|
54
|
+
out = dict(default_block)
|
|
55
|
+
# Top-level string/int/float/bool keys (legacy)
|
|
56
|
+
for k, v in data.items():
|
|
57
|
+
if k in ("default", "workflow", "swarm", "models", "memory", "tools", "telemetry", "providers"):
|
|
58
|
+
continue
|
|
59
|
+
if isinstance(v, (str, int, float, bool)):
|
|
60
|
+
out[k] = v
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def normalize_toml_to_flat(data: dict) -> dict:
|
|
65
|
+
"""
|
|
66
|
+
Normalize TOML data into a structure the resolver expects.
|
|
67
|
+
Supports new format [swarm], [models], etc. and legacy [default] / top-level.
|
|
68
|
+
"""
|
|
69
|
+
result: dict = {}
|
|
70
|
+
legacy = _extract_legacy_defaults(data)
|
|
71
|
+
if legacy:
|
|
72
|
+
result["events_dir"] = legacy.get("events_dir", "")
|
|
73
|
+
result["data_dir"] = legacy.get("data_dir", "")
|
|
74
|
+
result["worker_model"] = legacy.get("worker_model", "")
|
|
75
|
+
result["planner_model"] = legacy.get("planner_model", "")
|
|
76
|
+
|
|
77
|
+
if "swarm" in data and isinstance(data["swarm"], dict):
|
|
78
|
+
result["swarm"] = data["swarm"]
|
|
79
|
+
if "models" in data and isinstance(data["models"], dict):
|
|
80
|
+
result["models"] = data["models"]
|
|
81
|
+
elif legacy:
|
|
82
|
+
if legacy.get("worker_model"):
|
|
83
|
+
result.setdefault("models", {})["worker"] = legacy["worker_model"]
|
|
84
|
+
if legacy.get("planner_model"):
|
|
85
|
+
result.setdefault("models", {})["planner"] = legacy["planner_model"]
|
|
86
|
+
if "memory" in data and isinstance(data["memory"], dict):
|
|
87
|
+
result["memory"] = data["memory"]
|
|
88
|
+
if "knowledge" in data and isinstance(data["knowledge"], dict):
|
|
89
|
+
result["knowledge"] = data["knowledge"]
|
|
90
|
+
if "tools" in data and isinstance(data["tools"], dict):
|
|
91
|
+
result["tools"] = data["tools"]
|
|
92
|
+
result["tools"] = data["tools"]
|
|
93
|
+
if "telemetry" in data and isinstance(data["telemetry"], dict):
|
|
94
|
+
result["telemetry"] = data["telemetry"]
|
|
95
|
+
if "cache" in data and isinstance(data["cache"], dict):
|
|
96
|
+
result["cache"] = data["cache"]
|
|
97
|
+
if "bus" in data and isinstance(data["bus"], dict):
|
|
98
|
+
result["bus"] = data["bus"]
|
|
99
|
+
if "nodes" in data and isinstance(data["nodes"], dict):
|
|
100
|
+
result["nodes"] = data["nodes"]
|
|
101
|
+
if "providers" in data and isinstance(data["providers"], dict):
|
|
102
|
+
result["providers"] = data["providers"]
|
|
103
|
+
# v1.10.5: MCP and A2A
|
|
104
|
+
if "mcp" in data and isinstance(data["mcp"], dict):
|
|
105
|
+
result["mcp"] = data["mcp"]
|
|
106
|
+
if "a2a" in data and isinstance(data["a2a"], dict):
|
|
107
|
+
result["a2a"] = data["a2a"]
|
|
108
|
+
# v2.1: hitl and [[hitl.policies]]
|
|
109
|
+
if "hitl" in data and isinstance(data["hitl"], dict):
|
|
110
|
+
result["hitl"] = dict(data["hitl"])
|
|
111
|
+
policies = data["hitl"].get("policies")
|
|
112
|
+
if isinstance(policies, list):
|
|
113
|
+
result["hitl"]["policies"] = []
|
|
114
|
+
for p in policies:
|
|
115
|
+
if isinstance(p, dict):
|
|
116
|
+
pol = dict(p)
|
|
117
|
+
triggers = p.get("triggers")
|
|
118
|
+
if isinstance(triggers, list):
|
|
119
|
+
pol["triggers"] = [t if isinstance(t, dict) else {"type": "confidence_below", "threshold": 0.5} for t in triggers]
|
|
120
|
+
result["hitl"]["policies"].append(pol)
|
|
121
|
+
return result
|