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,558 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Executor: runtime engine that runs tasks via agents while respecting scheduler dependencies.
|
|
3
|
+
|
|
4
|
+
Lifecycle: EXECUTOR_STARTED → (agent/task events per task) → EXECUTOR_FINISHED.
|
|
5
|
+
Uses asyncio and a worker pool (Semaphore) to run up to worker_count tasks concurrently.
|
|
6
|
+
Supports speculative execution and task cache lookup.
|
|
7
|
+
|
|
8
|
+
v1.6: Semantic task cache, model complexity routing, streaming DAG unblocking.
|
|
9
|
+
v1.7: Critic loop, speculative prefetching.
|
|
10
|
+
v1.9: Stateless — no task state stored in executor; all state in Scheduler. Publishes to bus.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import asyncio
|
|
15
|
+
import threading
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from devsper.types.task import Task, TaskStatus
|
|
20
|
+
from devsper.types.event import Event, events
|
|
21
|
+
from devsper.utils.event_logger import EventLog
|
|
22
|
+
|
|
23
|
+
from devsper.agents.agent import Agent
|
|
24
|
+
from devsper.swarm.scheduler import Scheduler
|
|
25
|
+
from devsper.swarm.planner import Planner
|
|
26
|
+
from devsper.intelligence.adaptation import create_alternative_subtasks_for_failed
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from devsper.agents.critic import CriticAgent
|
|
30
|
+
from devsper.swarm.prefetcher import TaskPrefetcher
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_tools_for_task(task: Task) -> list:
|
|
34
|
+
"""Get tools that would be selected for this task (for complexity routing)."""
|
|
35
|
+
try:
|
|
36
|
+
from devsper.tools.selector import get_tools_for_task as _get_tools
|
|
37
|
+
from devsper.tools.scoring import get_default_score_store
|
|
38
|
+
score_store = get_default_score_store()
|
|
39
|
+
except Exception:
|
|
40
|
+
score_store = None
|
|
41
|
+
return _get_tools(
|
|
42
|
+
task.description or "",
|
|
43
|
+
role=getattr(task, "role", None),
|
|
44
|
+
score_store=score_store,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Executor:
|
|
49
|
+
"""Executes tasks using an agent, respecting the scheduler DAG and worker limit."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
scheduler: Scheduler,
|
|
54
|
+
agent: Agent,
|
|
55
|
+
worker_count: int = 4,
|
|
56
|
+
event_log: EventLog | None = None,
|
|
57
|
+
planner: Planner | None = None,
|
|
58
|
+
adaptive: bool = False,
|
|
59
|
+
speculative_execution: bool = False,
|
|
60
|
+
task_cache: object = None,
|
|
61
|
+
pause_event: threading.Event | None = None,
|
|
62
|
+
semantic_cache: object = None,
|
|
63
|
+
complexity_router: object = None,
|
|
64
|
+
models_config: object = None,
|
|
65
|
+
streaming_dag: bool = True,
|
|
66
|
+
critic_agent: "CriticAgent | None" = None,
|
|
67
|
+
critic_enabled: bool = False,
|
|
68
|
+
critic_roles: list[str] | None = None,
|
|
69
|
+
fast_model: str = "mock",
|
|
70
|
+
prefetcher: "TaskPrefetcher | None" = None,
|
|
71
|
+
bus: object = None,
|
|
72
|
+
checkpointer: object = None,
|
|
73
|
+
sandbox_config: object = None,
|
|
74
|
+
audit_logger: object = None,
|
|
75
|
+
hitl_enabled: bool = False,
|
|
76
|
+
hitl_escalation_checker: object = None,
|
|
77
|
+
hitl_approval_store: object = None,
|
|
78
|
+
hitl_notifier: object = None,
|
|
79
|
+
hitl_resolver: object = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
self.scheduler = scheduler
|
|
82
|
+
self.agent = agent
|
|
83
|
+
self.worker_count = worker_count
|
|
84
|
+
self.event_log = event_log or EventLog()
|
|
85
|
+
self.planner = planner
|
|
86
|
+
self.adaptive = adaptive
|
|
87
|
+
self.speculative_execution = speculative_execution
|
|
88
|
+
self.task_cache = task_cache
|
|
89
|
+
self.pause_event = pause_event
|
|
90
|
+
self.semantic_cache = semantic_cache
|
|
91
|
+
self.complexity_router = complexity_router
|
|
92
|
+
self.models_config = models_config or getattr(agent, "model_name", "mock")
|
|
93
|
+
self.streaming_dag = streaming_dag
|
|
94
|
+
self.critic_agent = critic_agent
|
|
95
|
+
self.critic_enabled = critic_enabled
|
|
96
|
+
self.critic_roles = critic_roles or []
|
|
97
|
+
self.fast_model = fast_model
|
|
98
|
+
self.prefetcher = prefetcher
|
|
99
|
+
self.bus = bus
|
|
100
|
+
self.checkpointer = checkpointer
|
|
101
|
+
self.sandbox_config = sandbox_config
|
|
102
|
+
self.audit_logger = audit_logger
|
|
103
|
+
self.hitl_enabled = hitl_enabled
|
|
104
|
+
self.hitl_escalation_checker = hitl_escalation_checker
|
|
105
|
+
self.hitl_approval_store = hitl_approval_store
|
|
106
|
+
self.hitl_notifier = hitl_notifier
|
|
107
|
+
self.hitl_resolver = hitl_resolver
|
|
108
|
+
|
|
109
|
+
def run_sync(self) -> None:
|
|
110
|
+
"""Run the execution loop to completion (synchronous entry point)."""
|
|
111
|
+
asyncio.run(self.run())
|
|
112
|
+
|
|
113
|
+
def _semantic_cache_disabled(self) -> bool:
|
|
114
|
+
return os.environ.get("DEVSPER_DISABLE_SEMANTIC_CACHE", "").strip() == "1"
|
|
115
|
+
|
|
116
|
+
def _get_cached_result(self, task: Task) -> tuple[str | None, dict | None]:
|
|
117
|
+
"""
|
|
118
|
+
Return (cached_result, hit_info). hit_info is None or {similarity, original_description}
|
|
119
|
+
for semantic hits. Uses semantic cache first if enabled, then exact task_cache.
|
|
120
|
+
"""
|
|
121
|
+
if self.semantic_cache is not None and not self._semantic_cache_disabled():
|
|
122
|
+
try:
|
|
123
|
+
hit = self.semantic_cache.lookup(task.description or "")
|
|
124
|
+
if hit is not None:
|
|
125
|
+
return (
|
|
126
|
+
hit.result,
|
|
127
|
+
{
|
|
128
|
+
"similarity": hit.similarity,
|
|
129
|
+
"original_description": hit.original_description,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
self._emit(events.TASK_CACHE_MISS, {"task_id": task.id})
|
|
135
|
+
if self.task_cache is None:
|
|
136
|
+
return (None, None)
|
|
137
|
+
try:
|
|
138
|
+
out = self.task_cache.get(task)
|
|
139
|
+
return (out, None) if out is not None else (None, None)
|
|
140
|
+
except Exception:
|
|
141
|
+
return (None, None)
|
|
142
|
+
|
|
143
|
+
def _set_cached_result(self, task: Task, result: str) -> None:
|
|
144
|
+
"""Store result in exact and/or semantic cache."""
|
|
145
|
+
if self.task_cache is not None:
|
|
146
|
+
try:
|
|
147
|
+
self.task_cache.set(task, result)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
if self.semantic_cache is not None and not self._semantic_cache_disabled():
|
|
151
|
+
try:
|
|
152
|
+
self.semantic_cache.store_result(
|
|
153
|
+
task.description or "",
|
|
154
|
+
result,
|
|
155
|
+
getattr(task, "role", None) or "general",
|
|
156
|
+
)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
def _model_for_task(self, task: Task) -> str:
|
|
161
|
+
"""Return model name for this task (complexity routing or agent default)."""
|
|
162
|
+
if self.complexity_router is None or self.models_config is None:
|
|
163
|
+
return getattr(self.agent, "model_name", "mock")
|
|
164
|
+
tools = _get_tools_for_task(task)
|
|
165
|
+
tier = self.complexity_router.classify(task, tools)
|
|
166
|
+
model = self.complexity_router.select_model(tier, self.models_config)
|
|
167
|
+
self._emit(
|
|
168
|
+
events.TASK_MODEL_SELECTED,
|
|
169
|
+
{"task_id": task.id, "tier": tier, "model": model},
|
|
170
|
+
)
|
|
171
|
+
return model
|
|
172
|
+
|
|
173
|
+
def _publish_bus(self, topic: str, payload: dict) -> None:
|
|
174
|
+
"""Publish to bus if configured. Fire-and-forget."""
|
|
175
|
+
if self.bus is None:
|
|
176
|
+
return
|
|
177
|
+
try:
|
|
178
|
+
from devsper.bus.message import create_bus_message
|
|
179
|
+
run_id = getattr(self.scheduler, "run_id", "") or ""
|
|
180
|
+
msg = create_bus_message(topic=topic, payload=payload, run_id=run_id)
|
|
181
|
+
try:
|
|
182
|
+
loop = asyncio.get_running_loop()
|
|
183
|
+
loop.create_task(self.bus.publish(msg))
|
|
184
|
+
except RuntimeError:
|
|
185
|
+
asyncio.run(self.bus.publish(msg))
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
async def _execute_task(self, task: Task, is_speculative: bool) -> str:
|
|
190
|
+
"""Run one task (cache lookup, agent run, cache store). Returns task.id. No state stored on self."""
|
|
191
|
+
loop = asyncio.get_running_loop()
|
|
192
|
+
cached, hit_info = self._get_cached_result(task)
|
|
193
|
+
if cached is not None:
|
|
194
|
+
if not is_speculative:
|
|
195
|
+
self.scheduler.mark_completed(task.id, cached)
|
|
196
|
+
self.scheduler.confirm_speculative_for(task.id)
|
|
197
|
+
if hit_info:
|
|
198
|
+
self._emit(
|
|
199
|
+
events.TASK_CACHE_HIT,
|
|
200
|
+
{
|
|
201
|
+
"task_id": task.id,
|
|
202
|
+
"similarity": hit_info["similarity"],
|
|
203
|
+
"original_description": hit_info["original_description"],
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
return task.id
|
|
207
|
+
|
|
208
|
+
self._publish_bus("task.started", task.to_dict())
|
|
209
|
+
if self.audit_logger is not None and self.scheduler is not None:
|
|
210
|
+
try:
|
|
211
|
+
from devsper.audit.logger import make_audit_record
|
|
212
|
+
run_id = getattr(self.scheduler, "run_id", "") or ""
|
|
213
|
+
rec = make_audit_record(
|
|
214
|
+
run_id=run_id,
|
|
215
|
+
task_id=task.id,
|
|
216
|
+
event_type="TASK_STARTED",
|
|
217
|
+
actor=run_id,
|
|
218
|
+
resource=getattr(task, "role", "") or "agent",
|
|
219
|
+
input_text=task.description or "",
|
|
220
|
+
output_text="",
|
|
221
|
+
)
|
|
222
|
+
self.audit_logger.log(rec)
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
prefetch_result = None
|
|
227
|
+
if self.prefetcher:
|
|
228
|
+
prefetch_result = self.prefetcher.consume(task.id)
|
|
229
|
+
if prefetch_result is not None:
|
|
230
|
+
age_seconds = (
|
|
231
|
+
datetime.now(timezone.utc) - prefetch_result.computed_at
|
|
232
|
+
).total_seconds()
|
|
233
|
+
self._emit(
|
|
234
|
+
events.PREFETCH_HIT,
|
|
235
|
+
{"task_id": task.id, "age_seconds": round(age_seconds, 2)},
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
self._emit(
|
|
239
|
+
events.PREFETCH_MISS,
|
|
240
|
+
{"task_id": task.id, "reason": "stale_or_missing"},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
model_override = None
|
|
244
|
+
if self.complexity_router and self.models_config:
|
|
245
|
+
model_override = self._model_for_task(task)
|
|
246
|
+
try:
|
|
247
|
+
use_sandbox = (
|
|
248
|
+
self.sandbox_config is not None
|
|
249
|
+
and getattr(self.sandbox_config, "enabled", False)
|
|
250
|
+
)
|
|
251
|
+
if use_sandbox:
|
|
252
|
+
from devsper.sandbox.sandbox import AgentSandbox, get_quota_for_role
|
|
253
|
+
request = self.agent.build_request(
|
|
254
|
+
task, model_override=model_override, prefetch_result=prefetch_result
|
|
255
|
+
)
|
|
256
|
+
quota = get_quota_for_role(self.sandbox_config, getattr(task, "role", None))
|
|
257
|
+
sandbox = AgentSandbox(self.agent)
|
|
258
|
+
response = await loop.run_in_executor(
|
|
259
|
+
None,
|
|
260
|
+
lambda: sandbox.run(request, quota),
|
|
261
|
+
)
|
|
262
|
+
self.agent.apply_response(task, response)
|
|
263
|
+
else:
|
|
264
|
+
await loop.run_in_executor(
|
|
265
|
+
None,
|
|
266
|
+
lambda t=task, m=model_override, p=prefetch_result: self.agent.run_task(
|
|
267
|
+
t, model_override=m, prefetch_result=p
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
except Exception as err:
|
|
271
|
+
if self.audit_logger is not None and self.scheduler is not None:
|
|
272
|
+
try:
|
|
273
|
+
from devsper.audit.logger import make_audit_record
|
|
274
|
+
run_id = getattr(self.scheduler, "run_id", "") or ""
|
|
275
|
+
rec = make_audit_record(
|
|
276
|
+
run_id=run_id,
|
|
277
|
+
task_id=task.id,
|
|
278
|
+
event_type="TASK_FAILED",
|
|
279
|
+
actor=run_id,
|
|
280
|
+
resource="",
|
|
281
|
+
input_text=task.description or "",
|
|
282
|
+
output_text=str(err),
|
|
283
|
+
success=False,
|
|
284
|
+
)
|
|
285
|
+
self.audit_logger.log(rec)
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
self.scheduler.mark_failed(task.id, str(err))
|
|
289
|
+
if is_speculative:
|
|
290
|
+
self.scheduler.discard_speculative_for(task.id)
|
|
291
|
+
else:
|
|
292
|
+
self._emit(
|
|
293
|
+
events.TASK_FAILED,
|
|
294
|
+
{"task_id": task.id, "error": str(err)},
|
|
295
|
+
)
|
|
296
|
+
self._publish_bus("task.failed", {"task_id": task.id, "error": str(err)})
|
|
297
|
+
if self.adaptive and self.planner:
|
|
298
|
+
alt = create_alternative_subtasks_for_failed(
|
|
299
|
+
task, self.planner, self.scheduler
|
|
300
|
+
)
|
|
301
|
+
if alt:
|
|
302
|
+
self.scheduler.add_tasks(alt)
|
|
303
|
+
return task.id
|
|
304
|
+
|
|
305
|
+
result = task.result or ""
|
|
306
|
+
self._set_cached_result(task, result)
|
|
307
|
+
|
|
308
|
+
last_critic_score: float | None = None
|
|
309
|
+
# v1.7: critic loop — only for non-speculative, eligible roles, not already retried
|
|
310
|
+
role = getattr(task, "role", None) or ""
|
|
311
|
+
retry_count = getattr(task, "retry_count", 0)
|
|
312
|
+
if (
|
|
313
|
+
not is_speculative
|
|
314
|
+
and self.critic_enabled
|
|
315
|
+
and self.critic_agent
|
|
316
|
+
and role in self.critic_roles
|
|
317
|
+
and retry_count < 1
|
|
318
|
+
):
|
|
319
|
+
from devsper.agents.critic import CriticAgent
|
|
320
|
+
|
|
321
|
+
critique = await self.critic_agent.critique(
|
|
322
|
+
task, result, model=self.fast_model
|
|
323
|
+
)
|
|
324
|
+
last_critic_score = critique.score
|
|
325
|
+
self._emit(
|
|
326
|
+
events.TASK_CRITIQUED,
|
|
327
|
+
{
|
|
328
|
+
"task_id": task.id,
|
|
329
|
+
"score": critique.score,
|
|
330
|
+
"issues": critique.issues,
|
|
331
|
+
"retry_requested": critique.retry,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
if critique.retry and retry_count < 1:
|
|
335
|
+
retry_prompt = await self.critic_agent.get_retry_prompt(
|
|
336
|
+
task, result, critique
|
|
337
|
+
)
|
|
338
|
+
task.description = retry_prompt
|
|
339
|
+
task.retry_count = retry_count + 1
|
|
340
|
+
task.result = None
|
|
341
|
+
task.status = TaskStatus.PENDING
|
|
342
|
+
return await self._execute_task(task, is_speculative)
|
|
343
|
+
|
|
344
|
+
# v2.1: HITL — if escalation triggered, create approval request and wait
|
|
345
|
+
if not is_speculative and self.hitl_enabled and self.hitl_escalation_checker:
|
|
346
|
+
from devsper.agents.agent import AgentResponse
|
|
347
|
+
from devsper.explainability.decision_tree import DecisionRecord
|
|
348
|
+
from devsper.hitl.escalation import EscalationTrigger
|
|
349
|
+
from devsper.hitl.approval import ApprovalRequest, ApprovalStore
|
|
350
|
+
from datetime import timedelta
|
|
351
|
+
|
|
352
|
+
fake_response = AgentResponse(
|
|
353
|
+
task_id=task.id,
|
|
354
|
+
result=result,
|
|
355
|
+
tools_called=getattr(task, "tools_called", []) or [],
|
|
356
|
+
broadcasts=[],
|
|
357
|
+
tokens_used=None,
|
|
358
|
+
duration_seconds=0.0,
|
|
359
|
+
error=None,
|
|
360
|
+
success=True,
|
|
361
|
+
)
|
|
362
|
+
fake_decision = DecisionRecord(
|
|
363
|
+
task_id=task.id,
|
|
364
|
+
task_description=task.description or "",
|
|
365
|
+
strategy_selected="",
|
|
366
|
+
strategy_reason="",
|
|
367
|
+
critic_score=last_critic_score,
|
|
368
|
+
confidence=float(last_critic_score) if last_critic_score is not None else 0.0,
|
|
369
|
+
)
|
|
370
|
+
match = self.hitl_escalation_checker.evaluate(task, fake_response, fake_decision)
|
|
371
|
+
if match is not None:
|
|
372
|
+
trigger, hitl_policy = match
|
|
373
|
+
request_id = str(__import__("uuid").uuid4())
|
|
374
|
+
now = datetime.now(timezone.utc)
|
|
375
|
+
timeout_sec = getattr(hitl_policy, "timeout_seconds", 3600)
|
|
376
|
+
expires = now + timedelta(seconds=timeout_sec)
|
|
377
|
+
approval = ApprovalRequest(
|
|
378
|
+
request_id=request_id,
|
|
379
|
+
task=task,
|
|
380
|
+
proposed_result=result,
|
|
381
|
+
decision_record=fake_decision,
|
|
382
|
+
trigger=trigger,
|
|
383
|
+
created_at=now.isoformat(),
|
|
384
|
+
expires_at=expires.isoformat(),
|
|
385
|
+
status="pending",
|
|
386
|
+
)
|
|
387
|
+
store = self.hitl_approval_store
|
|
388
|
+
if store is not None:
|
|
389
|
+
store.save(approval)
|
|
390
|
+
if self.hitl_notifier is not None:
|
|
391
|
+
await self.hitl_notifier.notify(approval, hitl_policy)
|
|
392
|
+
# In-process resolver or poll store
|
|
393
|
+
approved_result = True
|
|
394
|
+
if self.hitl_resolver is not None:
|
|
395
|
+
resolver = self.hitl_resolver
|
|
396
|
+
try:
|
|
397
|
+
if asyncio.iscoroutinefunction(resolver):
|
|
398
|
+
approved_result = await resolver(approval, hitl_policy)
|
|
399
|
+
else:
|
|
400
|
+
approved_result = await asyncio.to_thread(resolver, approval, hitl_policy)
|
|
401
|
+
except Exception:
|
|
402
|
+
approved_result = False
|
|
403
|
+
if store is not None:
|
|
404
|
+
store.resolve(request_id, approved_result, "")
|
|
405
|
+
if not approved_result:
|
|
406
|
+
self.scheduler.mark_failed(task.id, "Rejected by human reviewer")
|
|
407
|
+
self._emit(
|
|
408
|
+
events.TASK_REJECTED_BY_HUMAN,
|
|
409
|
+
{"task_id": task.id, "request_id": request_id},
|
|
410
|
+
)
|
|
411
|
+
return task.id
|
|
412
|
+
# approved: fall through to mark_completed
|
|
413
|
+
else:
|
|
414
|
+
# Poll for resolution every 5s up to timeout
|
|
415
|
+
poll_interval = 5
|
|
416
|
+
elapsed = 0
|
|
417
|
+
while store is not None and elapsed < timeout_sec:
|
|
418
|
+
await asyncio.sleep(min(poll_interval, timeout_sec - elapsed))
|
|
419
|
+
elapsed += poll_interval
|
|
420
|
+
req = store.get(request_id)
|
|
421
|
+
if req is None:
|
|
422
|
+
break
|
|
423
|
+
if req.status == "approved":
|
|
424
|
+
break
|
|
425
|
+
if req.status == "rejected":
|
|
426
|
+
self.scheduler.mark_failed(task.id, "Rejected by human reviewer")
|
|
427
|
+
self._emit(
|
|
428
|
+
events.TASK_REJECTED_BY_HUMAN,
|
|
429
|
+
{"task_id": task.id, "request_id": request_id},
|
|
430
|
+
)
|
|
431
|
+
return task.id
|
|
432
|
+
# After loop: approved or timeout
|
|
433
|
+
req = store.get(request_id) if store else None
|
|
434
|
+
if req is not None and req.status == "rejected":
|
|
435
|
+
self.scheduler.mark_failed(task.id, "Rejected by human reviewer")
|
|
436
|
+
self._emit(
|
|
437
|
+
events.TASK_REJECTED_BY_HUMAN,
|
|
438
|
+
{"task_id": task.id, "request_id": request_id},
|
|
439
|
+
)
|
|
440
|
+
return task.id
|
|
441
|
+
if req is not None and req.status == "pending" and elapsed >= timeout_sec:
|
|
442
|
+
on_timeout = getattr(hitl_policy, "on_timeout", "auto_approve")
|
|
443
|
+
if on_timeout == "auto_reject":
|
|
444
|
+
self.scheduler.mark_failed(task.id, "Approval timeout (auto_reject)")
|
|
445
|
+
if store:
|
|
446
|
+
approval.status = "timeout"
|
|
447
|
+
store.save(approval)
|
|
448
|
+
return task.id
|
|
449
|
+
# auto_approve or escalate_further: treat as approve for now
|
|
450
|
+
# approved or auto_approve: fall through to mark_completed
|
|
451
|
+
|
|
452
|
+
if not is_speculative:
|
|
453
|
+
self.scheduler.mark_completed(task.id, result)
|
|
454
|
+
self.scheduler.confirm_speculative_for(task.id)
|
|
455
|
+
if self.checkpointer is not None:
|
|
456
|
+
self.checkpointer.on_task_completed(self.scheduler)
|
|
457
|
+
if self.audit_logger is not None and self.scheduler is not None:
|
|
458
|
+
try:
|
|
459
|
+
from devsper.audit.logger import make_audit_record
|
|
460
|
+
run_id = getattr(self.scheduler, "run_id", "") or ""
|
|
461
|
+
rec = make_audit_record(
|
|
462
|
+
run_id=run_id,
|
|
463
|
+
task_id=task.id,
|
|
464
|
+
event_type="TASK_COMPLETED",
|
|
465
|
+
actor=run_id,
|
|
466
|
+
resource=getattr(task, "role", "") or "agent",
|
|
467
|
+
input_text=task.description or "",
|
|
468
|
+
output_text=(result or "")[:5000],
|
|
469
|
+
duration_ms=int((task.result and 0) or 0),
|
|
470
|
+
success=True,
|
|
471
|
+
)
|
|
472
|
+
self.audit_logger.log(rec)
|
|
473
|
+
except Exception:
|
|
474
|
+
pass
|
|
475
|
+
self._publish_bus(
|
|
476
|
+
"task.completed",
|
|
477
|
+
{
|
|
478
|
+
"task_id": task.id,
|
|
479
|
+
"result": result,
|
|
480
|
+
"tokens_used": None,
|
|
481
|
+
"duration_seconds": 0.0,
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
if self.adaptive and self.planner:
|
|
485
|
+
new_tasks = self.planner.expand_tasks(task)
|
|
486
|
+
if new_tasks:
|
|
487
|
+
self.scheduler.add_tasks(new_tasks)
|
|
488
|
+
return task.id
|
|
489
|
+
|
|
490
|
+
async def run(self) -> None:
|
|
491
|
+
"""Run the execution loop until all tasks are completed."""
|
|
492
|
+
self._emit(events.EXECUTOR_STARTED, {})
|
|
493
|
+
|
|
494
|
+
sem = asyncio.Semaphore(self.worker_count)
|
|
495
|
+
running: dict[str, tuple[Task, bool, asyncio.Task]] = {} # task_id -> (task, is_speculative, future)
|
|
496
|
+
|
|
497
|
+
async def run_with_sem(task: Task, is_spec: bool) -> str:
|
|
498
|
+
async with sem:
|
|
499
|
+
return await self._execute_task(task, is_spec)
|
|
500
|
+
|
|
501
|
+
while not self.scheduler.is_finished():
|
|
502
|
+
if self.pause_event is not None and not self.pause_event.is_set():
|
|
503
|
+
await asyncio.sleep(0.2)
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
ready = self.scheduler.get_ready_tasks()
|
|
507
|
+
speculative: list[Task] = []
|
|
508
|
+
if self.speculative_execution:
|
|
509
|
+
speculative = self.scheduler.get_speculative_tasks()
|
|
510
|
+
if self.prefetcher:
|
|
511
|
+
for t in speculative:
|
|
512
|
+
if t.id not in running:
|
|
513
|
+
asyncio.create_task(self.prefetcher.prefetch(t))
|
|
514
|
+
|
|
515
|
+
if self.streaming_dag:
|
|
516
|
+
for task in ready + speculative:
|
|
517
|
+
if task.id in running:
|
|
518
|
+
continue
|
|
519
|
+
if len(running) >= self.worker_count:
|
|
520
|
+
break
|
|
521
|
+
task.status = TaskStatus.RUNNING
|
|
522
|
+
is_spec = task in speculative
|
|
523
|
+
fut = asyncio.create_task(run_with_sem(task, is_spec))
|
|
524
|
+
running[task.id] = (task, is_spec, fut)
|
|
525
|
+
if not running:
|
|
526
|
+
if not ready and not speculative:
|
|
527
|
+
await asyncio.sleep(0.01)
|
|
528
|
+
continue
|
|
529
|
+
done, _ = await asyncio.wait(
|
|
530
|
+
[f for _, _, f in running.values()],
|
|
531
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
532
|
+
)
|
|
533
|
+
for f in done:
|
|
534
|
+
try:
|
|
535
|
+
tid = f.result()
|
|
536
|
+
except Exception:
|
|
537
|
+
tid = None
|
|
538
|
+
if tid and tid in running:
|
|
539
|
+
del running[tid]
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
for task in ready:
|
|
543
|
+
task.status = TaskStatus.RUNNING
|
|
544
|
+
for task in speculative:
|
|
545
|
+
task.status = TaskStatus.RUNNING
|
|
546
|
+
await asyncio.gather(
|
|
547
|
+
*[run_with_sem(t, False) for t in ready],
|
|
548
|
+
*[run_with_sem(t, True) for t in speculative],
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
self._emit(events.EXECUTOR_FINISHED, {})
|
|
552
|
+
|
|
553
|
+
def _emit(self, event_type: events, payload: dict) -> None:
|
|
554
|
+
self.event_log.append_event(
|
|
555
|
+
Event(
|
|
556
|
+
timestamp=datetime.now(timezone.utc), type=event_type, payload=payload
|
|
557
|
+
)
|
|
558
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Map-reduce primitive: partition dataset, parallel map, single reduce.
|
|
3
|
+
Uses asyncio + worker pool (same pattern as Executor).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
R = TypeVar("R")
|
|
11
|
+
A = TypeVar("A")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def map_reduce(
|
|
15
|
+
dataset: list[T],
|
|
16
|
+
map_fn: Callable[[T], R],
|
|
17
|
+
reduce_fn: Callable[[list[R]], A],
|
|
18
|
+
worker_count: int = 4,
|
|
19
|
+
) -> A:
|
|
20
|
+
"""
|
|
21
|
+
Run map_fn on each item in parallel (up to worker_count concurrent), then reduce_fn on results.
|
|
22
|
+
|
|
23
|
+
- dataset: list of items to process
|
|
24
|
+
- map_fn: item -> result (called once per item)
|
|
25
|
+
- reduce_fn: list of results -> aggregated value
|
|
26
|
+
- worker_count: max concurrent map operations
|
|
27
|
+
|
|
28
|
+
Returns the result of reduce_fn(map_results).
|
|
29
|
+
"""
|
|
30
|
+
import asyncio
|
|
31
|
+
|
|
32
|
+
async def _run() -> A:
|
|
33
|
+
sem = asyncio.Semaphore(worker_count)
|
|
34
|
+
loop = asyncio.get_running_loop()
|
|
35
|
+
results: list[R] = [None] * len(dataset) # type: ignore
|
|
36
|
+
|
|
37
|
+
async def _map_one(i: int, item: T) -> None:
|
|
38
|
+
async with sem:
|
|
39
|
+
results[i] = await loop.run_in_executor(None, lambda: map_fn(item))
|
|
40
|
+
|
|
41
|
+
await asyncio.gather(*[_map_one(i, item) for i, item in enumerate(dataset)])
|
|
42
|
+
return reduce_fn(results)
|
|
43
|
+
|
|
44
|
+
return asyncio.run(_run())
|