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
devsper/swarm/planner.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Planner: convert one task into multiple ordered subtasks.
|
|
3
|
+
|
|
4
|
+
Lifecycle: PLANNER_STARTED → TASK_CREATED (×N) → PLANNER_FINISHED.
|
|
5
|
+
Subtasks have sequential dependencies; each subtask gets a short unique ID.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
from devsper.types.task import Task
|
|
13
|
+
from devsper.types.event import Event, events
|
|
14
|
+
from devsper.agents.roles import infer_role_from_description
|
|
15
|
+
from devsper.utils.event_logger import EventLog
|
|
16
|
+
from devsper.utils.models import generate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
PLANNER_PROMPT = """Break the following task into the minimal number of smaller steps needed (use only as many as the task requires—often 1–3 for simple tasks, more for complex ones).
|
|
20
|
+
|
|
21
|
+
Task:
|
|
22
|
+
{task_description}
|
|
23
|
+
{kg_section}
|
|
24
|
+
|
|
25
|
+
Each step must be a concrete action that can be done with tools (e.g. list directory, read file, run a command). Do NOT use vague steps like "open a terminal", "navigate to X", or "access the environment"—instead say exactly what to do: "List the contents of /tmp", "Read and summarize file X", etc. Steps will be executed by agents with filesystem and other tools; they do not need to "open" anything.
|
|
26
|
+
|
|
27
|
+
Return a numbered list.
|
|
28
|
+
|
|
29
|
+
Example format:
|
|
30
|
+
1. List the contents of /tmp
|
|
31
|
+
2. Summarize the file listing
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
EXPAND_TASKS_PROMPT = """A task just completed in a workflow.
|
|
35
|
+
|
|
36
|
+
Completed task: {task_description}
|
|
37
|
+
Result (preview): {result_preview}
|
|
38
|
+
|
|
39
|
+
Suggest 0 to 3 additional follow-up tasks that would extend or build on this work.
|
|
40
|
+
Return a numbered list only. One task per line.
|
|
41
|
+
Example:
|
|
42
|
+
1. Compare methods
|
|
43
|
+
2. Identify trends
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
NUMBERED_LINE = re.compile(r"^\s*\d+[.)]\s*(.+)$", re.MULTILINE)
|
|
48
|
+
|
|
49
|
+
# Skip decomposition for short, single-step prompts (e.g. "What is 2+2?")
|
|
50
|
+
MAX_SIMPLE_TASK_LEN = 200
|
|
51
|
+
MULTI_STEP_PATTERN = re.compile(
|
|
52
|
+
r"\b(then|and then|first|second|step\s*\d|finally|after that|next,?)\b|\n|^\s*\d+[.)]\s+",
|
|
53
|
+
re.IGNORECASE | re.MULTILINE,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_simple_task(description: str) -> bool:
|
|
58
|
+
"""True if the task looks like a single question/request, not a multi-step workflow."""
|
|
59
|
+
if not description or len(description.strip()) > MAX_SIMPLE_TASK_LEN:
|
|
60
|
+
return False
|
|
61
|
+
return MULTI_STEP_PATTERN.search(description.strip()) is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _short_id() -> str:
|
|
65
|
+
"""Return a short, URL-safe task id (8 hex chars)."""
|
|
66
|
+
return secrets.token_hex(4)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Planner:
|
|
70
|
+
"""Converts one Task into a list of subtasks with sequential dependencies."""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
model_name: str = "gpt-4o",
|
|
75
|
+
event_log: EventLog | None = None,
|
|
76
|
+
strategy=None,
|
|
77
|
+
prompt_suffix: str = "",
|
|
78
|
+
knowledge_graph=None,
|
|
79
|
+
guide_planning: bool = False,
|
|
80
|
+
min_confidence: float = 0.30,
|
|
81
|
+
parallel: bool = False,
|
|
82
|
+
):
|
|
83
|
+
self.model_name = model_name
|
|
84
|
+
self.event_log = event_log or EventLog()
|
|
85
|
+
self.strategy = strategy
|
|
86
|
+
self.prompt_suffix = prompt_suffix or ""
|
|
87
|
+
self.knowledge_graph = knowledge_graph
|
|
88
|
+
self.guide_planning = guide_planning
|
|
89
|
+
self.min_confidence = min_confidence
|
|
90
|
+
self.parallel = parallel
|
|
91
|
+
|
|
92
|
+
def plan(self, task: Task) -> list[Task]:
|
|
93
|
+
"""Break task into subtasks (strategy DAG or LLM). Emit planner lifecycle events."""
|
|
94
|
+
self._emit(events.PLANNER_STARTED, {"task_id": task.id})
|
|
95
|
+
|
|
96
|
+
if self.strategy is not None:
|
|
97
|
+
subtasks = self.strategy.plan(task)
|
|
98
|
+
if subtasks:
|
|
99
|
+
for st in subtasks:
|
|
100
|
+
if getattr(st, "role", None) is None:
|
|
101
|
+
st.role = infer_role_from_description(st.description or "")
|
|
102
|
+
if self.parallel:
|
|
103
|
+
st.dependencies = []
|
|
104
|
+
self._emit(events.TASK_CREATED, {"task_id": st.id, "description": st.description})
|
|
105
|
+
self._emit(events.PLANNER_FINISHED, {"task_id": task.id, "subtask_count": len(subtasks)})
|
|
106
|
+
return subtasks
|
|
107
|
+
|
|
108
|
+
# Simple single-step prompt: skip LLM decomposition (avoids overcomplication)
|
|
109
|
+
if _is_simple_task(task.description or ""):
|
|
110
|
+
if getattr(task, "role", None) is None:
|
|
111
|
+
task.role = infer_role_from_description(task.description or "")
|
|
112
|
+
self._emit(events.TASK_CREATED, {"task_id": task.id, "description": task.description})
|
|
113
|
+
self._emit(events.PLANNER_FINISHED, {"task_id": task.id, "subtask_count": 1})
|
|
114
|
+
return [task]
|
|
115
|
+
|
|
116
|
+
task_description = (task.description or "") + self.prompt_suffix
|
|
117
|
+
kg_section = ""
|
|
118
|
+
if self.guide_planning and self.knowledge_graph is not None:
|
|
119
|
+
from devsper.knowledge.query import query_for_planning, format_planning_context
|
|
120
|
+
planning_ctx = query_for_planning(self.knowledge_graph, task_description)
|
|
121
|
+
if planning_ctx.confidence > self.min_confidence:
|
|
122
|
+
kg_section = "\n\n## Relevant prior knowledge:\n" + format_planning_context(planning_ctx) + "\n\nUse this to avoid re-discovering known facts."
|
|
123
|
+
self._emit(
|
|
124
|
+
events.PLANNER_KG_CONTEXT_INJECTED,
|
|
125
|
+
{
|
|
126
|
+
"concept_count": len(planning_ctx.relevant_concepts),
|
|
127
|
+
"finding_count": len(planning_ctx.prior_findings),
|
|
128
|
+
"confidence": planning_ctx.confidence,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
prompt = PLANNER_PROMPT.format(task_description=task_description, kg_section=kg_section or "")
|
|
132
|
+
raw = generate(self.model_name, prompt)
|
|
133
|
+
steps = self._parse_numbered_list(raw)
|
|
134
|
+
|
|
135
|
+
subtasks: list[Task] = []
|
|
136
|
+
task_ids: list[str] = []
|
|
137
|
+
for i, description in enumerate(steps, start=1):
|
|
138
|
+
task_id = _short_id()
|
|
139
|
+
task_ids.append(task_id)
|
|
140
|
+
deps = [] if self.parallel else ([task_ids[i - 2]] if i > 1 else [])
|
|
141
|
+
role = infer_role_from_description(description.strip())
|
|
142
|
+
subtask = Task(id=task_id, description=description.strip(), dependencies=deps, role=role)
|
|
143
|
+
subtasks.append(subtask)
|
|
144
|
+
self._emit(
|
|
145
|
+
events.TASK_CREATED,
|
|
146
|
+
{"task_id": task_id, "description": subtask.description},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self._emit(events.PLANNER_FINISHED, {"task_id": task.id, "subtask_count": len(subtasks)})
|
|
150
|
+
return subtasks
|
|
151
|
+
|
|
152
|
+
def expand_tasks(self, completed_task: Task, context: list[Task] | None = None) -> list[Task]:
|
|
153
|
+
"""
|
|
154
|
+
After a task completes, optionally generate additional tasks (dynamic DAG growth).
|
|
155
|
+
New tasks depend on the completed task. Emits TASK_CREATED for each.
|
|
156
|
+
"""
|
|
157
|
+
result_preview = (completed_task.result or "")[:500]
|
|
158
|
+
prompt = EXPAND_TASKS_PROMPT.format(
|
|
159
|
+
task_description=completed_task.description,
|
|
160
|
+
result_preview=result_preview,
|
|
161
|
+
)
|
|
162
|
+
raw = generate(self.model_name, prompt)
|
|
163
|
+
steps = self._parse_numbered_list(raw)
|
|
164
|
+
if not steps:
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
new_tasks: list[Task] = []
|
|
168
|
+
for i, description in enumerate(steps, start=1):
|
|
169
|
+
task_id = _short_id()
|
|
170
|
+
role = infer_role_from_description(description.strip())
|
|
171
|
+
subtask = Task(
|
|
172
|
+
id=task_id,
|
|
173
|
+
description=description.strip(),
|
|
174
|
+
dependencies=[completed_task.id],
|
|
175
|
+
role=role,
|
|
176
|
+
)
|
|
177
|
+
new_tasks.append(subtask)
|
|
178
|
+
self._emit(events.TASK_CREATED, {"task_id": task_id, "description": subtask.description})
|
|
179
|
+
|
|
180
|
+
return new_tasks
|
|
181
|
+
|
|
182
|
+
def _parse_numbered_list(self, text: str) -> list[str]:
|
|
183
|
+
"""Parse '1. step\\n2. step' into ['step', 'step', ...]."""
|
|
184
|
+
steps = []
|
|
185
|
+
for line in text.strip().splitlines():
|
|
186
|
+
line = line.strip()
|
|
187
|
+
if not line:
|
|
188
|
+
continue
|
|
189
|
+
m = NUMBERED_LINE.match(line)
|
|
190
|
+
if m:
|
|
191
|
+
steps.append(m.group(1).strip())
|
|
192
|
+
return steps
|
|
193
|
+
|
|
194
|
+
def _emit(self, event_type: events, payload: dict) -> None:
|
|
195
|
+
self.event_log.append_event(
|
|
196
|
+
Event(timestamp=datetime.now(timezone.utc), type=event_type, payload=payload)
|
|
197
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Speculative pre-fetching: pre-warm memory context and tool selection for likely successor tasks.
|
|
3
|
+
v1.7.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from devsper.tools.base import Tool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PrefetchResult:
|
|
17
|
+
memory_context: str
|
|
18
|
+
tools: list # list[Tool]
|
|
19
|
+
computed_at: datetime
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskPrefetcher:
|
|
23
|
+
"""
|
|
24
|
+
While a task is running, pre-warm its likely successors:
|
|
25
|
+
fetch memory context and select tools in background.
|
|
26
|
+
Results are cached and consumed by the agent when the task actually starts.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
memory_router,
|
|
32
|
+
tool_selector,
|
|
33
|
+
score_store=None,
|
|
34
|
+
max_age_seconds: float = 30.0,
|
|
35
|
+
):
|
|
36
|
+
self._memory_router = memory_router
|
|
37
|
+
self._tool_selector = tool_selector
|
|
38
|
+
self._score_store = score_store
|
|
39
|
+
self._max_age_seconds = max_age_seconds
|
|
40
|
+
self._warmup_cache: dict[str, PrefetchResult] = {}
|
|
41
|
+
self._lock = asyncio.Lock()
|
|
42
|
+
|
|
43
|
+
async def prefetch(self, task) -> None:
|
|
44
|
+
"""Run in background via asyncio.create_task()."""
|
|
45
|
+
task_id = getattr(task, "id", "")
|
|
46
|
+
description = getattr(task, "description", "") or ""
|
|
47
|
+
loop = asyncio.get_running_loop()
|
|
48
|
+
|
|
49
|
+
def _get_memory() -> str:
|
|
50
|
+
if not self._memory_router or not description:
|
|
51
|
+
return ""
|
|
52
|
+
try:
|
|
53
|
+
return self._memory_router.get_memory_context(description)
|
|
54
|
+
except Exception:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
def _get_tools():
|
|
58
|
+
if not self._tool_selector:
|
|
59
|
+
return []
|
|
60
|
+
try:
|
|
61
|
+
return self._tool_selector(
|
|
62
|
+
description,
|
|
63
|
+
role=getattr(task, "role", None),
|
|
64
|
+
score_store=self._score_store,
|
|
65
|
+
)
|
|
66
|
+
except Exception:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
memory_ctx = await loop.run_in_executor(None, _get_memory)
|
|
70
|
+
tools = await loop.run_in_executor(None, _get_tools)
|
|
71
|
+
async with self._lock:
|
|
72
|
+
self._warmup_cache[task_id] = PrefetchResult(
|
|
73
|
+
memory_context=memory_ctx,
|
|
74
|
+
tools=tools,
|
|
75
|
+
computed_at=datetime.now(timezone.utc),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def consume(self, task_id: str) -> PrefetchResult | None:
|
|
79
|
+
"""
|
|
80
|
+
Called by Agent at start — returns pre-warmed context or None.
|
|
81
|
+
Result older than max_age_seconds is treated as stale and not used.
|
|
82
|
+
"""
|
|
83
|
+
result = self._warmup_cache.pop(task_id, None)
|
|
84
|
+
if result is None:
|
|
85
|
+
return None
|
|
86
|
+
age_seconds = (
|
|
87
|
+
datetime.now(timezone.utc) - result.computed_at
|
|
88
|
+
).total_seconds()
|
|
89
|
+
if age_seconds > self._max_age_seconds:
|
|
90
|
+
return None
|
|
91
|
+
return result
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scheduler: manage the task DAG and determine which tasks are runnable.
|
|
3
|
+
|
|
4
|
+
Supports: add_tasks, get_ready_tasks, get_speculative_tasks, mark_completed,
|
|
5
|
+
confirm_speculative_for, discard_speculative_for, is_finished.
|
|
6
|
+
v1.9: Single source of truth — get_task, get_all_tasks, get_results, snapshot, restore.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import networkx as nx
|
|
11
|
+
|
|
12
|
+
from devsper.types.task import Task, TaskStatus
|
|
13
|
+
from devsper.types.exceptions import TaskNotFoundError
|
|
14
|
+
from devsper.swarm.speculation import (
|
|
15
|
+
get_speculative_candidates,
|
|
16
|
+
confirm_speculative,
|
|
17
|
+
discard_speculative,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Scheduler:
|
|
22
|
+
"""Manages a DAG of tasks. Tracks dependencies and exposes runnable tasks. Single source of truth for task state."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, run_id: str = "") -> None:
|
|
25
|
+
self._graph: nx.DiGraph = nx.DiGraph()
|
|
26
|
+
self._tasks: dict[str, Task] = {}
|
|
27
|
+
self.run_id = run_id
|
|
28
|
+
|
|
29
|
+
def get_task(self, task_id: str) -> Task:
|
|
30
|
+
"""Return task by id. Raises TaskNotFoundError if not found."""
|
|
31
|
+
if task_id not in self._tasks:
|
|
32
|
+
raise TaskNotFoundError(f"Task not found: {task_id!r}")
|
|
33
|
+
return self._tasks[task_id]
|
|
34
|
+
|
|
35
|
+
def get_all_tasks(self) -> list[Task]:
|
|
36
|
+
"""Return all tasks."""
|
|
37
|
+
return list(self._tasks.values())
|
|
38
|
+
|
|
39
|
+
def add_tasks(self, tasks: list[Task]) -> None:
|
|
40
|
+
"""Add tasks and build the internal dependency graph."""
|
|
41
|
+
for task in tasks:
|
|
42
|
+
self._tasks[task.id] = task
|
|
43
|
+
self._graph.add_node(task.id)
|
|
44
|
+
for task in tasks:
|
|
45
|
+
for dep in task.dependencies:
|
|
46
|
+
if dep not in self._tasks:
|
|
47
|
+
raise ValueError(f"Unknown dependency: {dep!r}")
|
|
48
|
+
self._graph.add_edge(dep, task.id)
|
|
49
|
+
if not nx.is_directed_acyclic_graph(self._graph):
|
|
50
|
+
raise ValueError("Task graph contains a cycle")
|
|
51
|
+
|
|
52
|
+
def get_ready_tasks(self) -> list[Task]:
|
|
53
|
+
"""Return tasks that are runnable: PENDING and all dependencies completed."""
|
|
54
|
+
ready: list[Task] = []
|
|
55
|
+
for task_id, task in self._tasks.items():
|
|
56
|
+
if task.status != TaskStatus.PENDING:
|
|
57
|
+
continue
|
|
58
|
+
deps = list(self._graph.predecessors(task_id))
|
|
59
|
+
if all(self._tasks[dep].status == TaskStatus.COMPLETED for dep in deps):
|
|
60
|
+
ready.append(task)
|
|
61
|
+
return ready
|
|
62
|
+
|
|
63
|
+
def mark_completed(self, task_id: str, result: str = "") -> None:
|
|
64
|
+
"""Mark a task completed and set its result. Dependent tasks can become runnable."""
|
|
65
|
+
if task_id in self._tasks:
|
|
66
|
+
t = self._tasks[task_id]
|
|
67
|
+
t.status = TaskStatus.COMPLETED
|
|
68
|
+
t.result = result
|
|
69
|
+
|
|
70
|
+
def mark_failed(self, task_id: str, error: str = "") -> None:
|
|
71
|
+
"""Mark a task failed and set its error."""
|
|
72
|
+
if task_id in self._tasks:
|
|
73
|
+
t = self._tasks[task_id]
|
|
74
|
+
t.status = TaskStatus.FAILED
|
|
75
|
+
t.error = error
|
|
76
|
+
|
|
77
|
+
def is_finished(self) -> bool:
|
|
78
|
+
"""Return True when every task is completed or failed (no pending/running left)."""
|
|
79
|
+
return all(
|
|
80
|
+
t.status in (TaskStatus.COMPLETED, TaskStatus.FAILED)
|
|
81
|
+
for t in self._tasks.values()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def get_results(self) -> dict[str, str]:
|
|
85
|
+
"""Return task_id -> result for completed tasks only."""
|
|
86
|
+
return {
|
|
87
|
+
task_id: (t.result or "")
|
|
88
|
+
for task_id, t in self._tasks.items()
|
|
89
|
+
if t.status == TaskStatus.COMPLETED and t.result is not None
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def snapshot(self) -> dict:
|
|
93
|
+
"""Full serializable state for checkpointing and node sync."""
|
|
94
|
+
edges = list(self._graph.edges())
|
|
95
|
+
tasks_data = [t.to_dict() for t in self._tasks.values()]
|
|
96
|
+
completed_count = sum(1 for t in self._tasks.values() if t.status == TaskStatus.COMPLETED)
|
|
97
|
+
failed_count = sum(1 for t in self._tasks.values() if t.status == TaskStatus.FAILED)
|
|
98
|
+
return {
|
|
99
|
+
"run_id": self.run_id,
|
|
100
|
+
"tasks": tasks_data,
|
|
101
|
+
"edges": [[str(u), str(v)] for u, v in edges],
|
|
102
|
+
"completed_count": completed_count,
|
|
103
|
+
"failed_count": failed_count,
|
|
104
|
+
"snapshot_at": datetime.now(timezone.utc).isoformat(),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def restore(cls, snapshot: dict) -> "Scheduler":
|
|
109
|
+
"""Reconstruct Scheduler from snapshot dict."""
|
|
110
|
+
run_id = snapshot.get("run_id", "")
|
|
111
|
+
s = cls(run_id=run_id)
|
|
112
|
+
tasks_data = snapshot.get("tasks", [])
|
|
113
|
+
for tdata in tasks_data:
|
|
114
|
+
task = Task.from_dict(tdata)
|
|
115
|
+
s._tasks[task.id] = task
|
|
116
|
+
s._graph.add_node(task.id)
|
|
117
|
+
for edge in snapshot.get("edges", []):
|
|
118
|
+
if len(edge) >= 2:
|
|
119
|
+
u, v = str(edge[0]), str(edge[1])
|
|
120
|
+
if u in s._tasks and v in s._tasks:
|
|
121
|
+
s._graph.add_edge(u, v)
|
|
122
|
+
return s
|
|
123
|
+
|
|
124
|
+
def get_completed_tasks(self) -> list[Task]:
|
|
125
|
+
"""Return all completed tasks (for learning/memory storage)."""
|
|
126
|
+
return [
|
|
127
|
+
t
|
|
128
|
+
for t in self._tasks.values()
|
|
129
|
+
if t.status == TaskStatus.COMPLETED and t.result is not None
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
def get_speculative_tasks(self) -> list[Task]:
|
|
133
|
+
"""Return tasks that can be run speculatively (one dep running, rest completed)."""
|
|
134
|
+
candidates = get_speculative_candidates(self._tasks, self._graph)
|
|
135
|
+
for t in candidates:
|
|
136
|
+
t.speculative = True
|
|
137
|
+
return candidates
|
|
138
|
+
|
|
139
|
+
def get_successors(self, task_id: str) -> list[str]:
|
|
140
|
+
"""Return task ids that depend on the given task."""
|
|
141
|
+
return list(self._graph.successors(task_id))
|
|
142
|
+
|
|
143
|
+
def confirm_speculative_for(self, completed_task_id: str) -> None:
|
|
144
|
+
"""When completed_task_id finishes, confirm its speculative successors (keep results)."""
|
|
145
|
+
for sid in self.get_successors(completed_task_id):
|
|
146
|
+
if sid in self._tasks:
|
|
147
|
+
confirm_speculative(self._tasks[sid])
|
|
148
|
+
|
|
149
|
+
def discard_speculative_for(self, failed_task_id: str) -> None:
|
|
150
|
+
"""When failed_task_id fails, discard results of speculative successors."""
|
|
151
|
+
for sid in self.get_successors(failed_task_id):
|
|
152
|
+
if sid in self._tasks:
|
|
153
|
+
discard_speculative(self._tasks[sid])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Speculative execution: predict likely next tasks and execute them early.
|
|
3
|
+
|
|
4
|
+
When planner produces DAG A → B → C:
|
|
5
|
+
- Run A
|
|
6
|
+
- Speculatively schedule B and C (they depend only on A)
|
|
7
|
+
- When A completes: confirm B and C (use speculative results) or discard
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from devsper.types.task import Task, TaskStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_speculative_candidates(
|
|
14
|
+
tasks: dict[str, Task],
|
|
15
|
+
graph,
|
|
16
|
+
) -> list[Task]:
|
|
17
|
+
"""
|
|
18
|
+
Return tasks that are candidates for speculative execution: all dependencies
|
|
19
|
+
are either COMPLETED or exactly one is RUNNING (and we bet it will complete).
|
|
20
|
+
graph must have .predecessors(node_id) returning an iterable of dependency ids.
|
|
21
|
+
"""
|
|
22
|
+
candidates: list[Task] = []
|
|
23
|
+
for task_id, task in tasks.items():
|
|
24
|
+
if task.status != TaskStatus.PENDING:
|
|
25
|
+
continue
|
|
26
|
+
deps = list(graph.predecessors(task_id))
|
|
27
|
+
if not deps:
|
|
28
|
+
continue
|
|
29
|
+
completed = sum(1 for d in deps if tasks[d].status == TaskStatus.COMPLETED)
|
|
30
|
+
running = sum(1 for d in deps if tasks[d].status == TaskStatus.RUNNING)
|
|
31
|
+
# Speculative: all deps done except one which is running
|
|
32
|
+
if running == 1 and completed == len(deps) - 1:
|
|
33
|
+
candidates.append(task)
|
|
34
|
+
return candidates
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def confirm_speculative(task: Task) -> None:
|
|
38
|
+
"""Mark a speculatively run task as confirmed (keep result)."""
|
|
39
|
+
if task.speculative and task.result is not None:
|
|
40
|
+
task.status = TaskStatus.COMPLETED
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def discard_speculative(task: Task) -> None:
|
|
44
|
+
"""Discard speculative result; task will be re-run when deps are confirmed."""
|
|
45
|
+
if task.speculative:
|
|
46
|
+
task.status = TaskStatus.PENDING
|
|
47
|
+
task.result = None
|