asicode 0.2.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.
- asi.py +9644 -0
- asicode-0.2.6.dist-info/METADATA +208 -0
- asicode-0.2.6.dist-info/RECORD +340 -0
- asicode-0.2.6.dist-info/WHEEL +5 -0
- asicode-0.2.6.dist-info/entry_points.txt +2 -0
- asicode-0.2.6.dist-info/licenses/LICENSE +21 -0
- asicode-0.2.6.dist-info/top_level.txt +11 -0
- common.py +86 -0
- config.py +162 -0
- context_collector.py +388 -0
- diff_apply.py +1549 -0
- external_llm/__init__.py +53 -0
- external_llm/agent/__init__.py +1 -0
- external_llm/agent/_shared_utils.py +1713 -0
- external_llm/agent/_thread_pool.py +13 -0
- external_llm/agent/_user_intent.py +233 -0
- external_llm/agent/agent_context_manager.py +532 -0
- external_llm/agent/agent_escalation_pipeline.py +161 -0
- external_llm/agent/agent_fast_path.py +63 -0
- external_llm/agent/agent_loop.py +3567 -0
- external_llm/agent/agent_loop_types.py +207 -0
- external_llm/agent/agent_phase_manager.py +352 -0
- external_llm/agent/agent_planner_pipeline.py +1756 -0
- external_llm/agent/agent_profile.py +200 -0
- external_llm/agent/agent_turn_pipeline.py +2592 -0
- external_llm/agent/alignment_scorer.py +333 -0
- external_llm/agent/anchor_shared.py +310 -0
- external_llm/agent/argument_repairer.py +119 -0
- external_llm/agent/ast_cache.py +148 -0
- external_llm/agent/ast_engine.py +1060 -0
- external_llm/agent/async_tool_executor.py +182 -0
- external_llm/agent/auto_correction.py +2268 -0
- external_llm/agent/background_job_manager.py +571 -0
- external_llm/agent/call_graph.py +516 -0
- external_llm/agent/checkpoint_store.py +472 -0
- external_llm/agent/config/__init__.py +0 -0
- external_llm/agent/config/thresholds.py +409 -0
- external_llm/agent/context_budget.py +593 -0
- external_llm/agent/context_contract.py +231 -0
- external_llm/agent/context_manager.py +1301 -0
- external_llm/agent/context_utils.py +29 -0
- external_llm/agent/design_chat_loop.py +2510 -0
- external_llm/agent/enums.py +48 -0
- external_llm/agent/execution_mode_classifier.py +323 -0
- external_llm/agent/execution_spec.py +355 -0
- external_llm/agent/execution_thresholds.py +281 -0
- external_llm/agent/failure_classifier.py +160 -0
- external_llm/agent/failure_context.py +409 -0
- external_llm/agent/failure_pattern_store.py +911 -0
- external_llm/agent/file_cache.py +144 -0
- external_llm/agent/fix_spec_claim_validator.py +747 -0
- external_llm/agent/gsg_safety.py +114 -0
- external_llm/agent/guard_ir.py +985 -0
- external_llm/agent/handoff_observer.py +327 -0
- external_llm/agent/insights_manager.py +1124 -0
- external_llm/agent/intent_models.py +250 -0
- external_llm/agent/intent_resolver.py +861 -0
- external_llm/agent/interrupt_tool_results.py +95 -0
- external_llm/agent/json_repair.py +283 -0
- external_llm/agent/language_backend.py +612 -0
- external_llm/agent/language_hint.py +51 -0
- external_llm/agent/learned_policy.py +826 -0
- external_llm/agent/light_repo_index.py +231 -0
- external_llm/agent/lint_runner.py +397 -0
- external_llm/agent/llm_output_utils.py +52 -0
- external_llm/agent/local_assistant.py +804 -0
- external_llm/agent/message_shapes.py +89 -0
- external_llm/agent/metadata_utils.py +14 -0
- external_llm/agent/operation_models.py +2707 -0
- external_llm/agent/orchestrator.py +5109 -0
- external_llm/agent/output_normalizer.py +415 -0
- external_llm/agent/performance_metrics.py +452 -0
- external_llm/agent/placement_contract.py +2765 -0
- external_llm/agent/plan_state.py +136 -0
- external_llm/agent/planner_lane_facade.py +67 -0
- external_llm/agent/rag_configs.py +168 -0
- external_llm/agent/rag_searcher.py +693 -0
- external_llm/agent/reasoning_utils.py +62 -0
- external_llm/agent/repair_helpers.py +475 -0
- external_llm/agent/request_intent_classifier.py +95 -0
- external_llm/agent/routing_policy.py +157 -0
- external_llm/agent/run_store.py +2356 -0
- external_llm/agent/scanner_registry.py +610 -0
- external_llm/agent/selector_constants.py +26 -0
- external_llm/agent/semantic_intent.py +163 -0
- external_llm/agent/semantic_lint.py +94 -0
- external_llm/agent/session_state.py +80 -0
- external_llm/agent/slash_commands.py +90 -0
- external_llm/agent/subagent_ipc.py +1222 -0
- external_llm/agent/symbol_engine.py +592 -0
- external_llm/agent/symbol_index.py +380 -0
- external_llm/agent/symbol_locator.py +117 -0
- external_llm/agent/symbol_modify_tool.py +1258 -0
- external_llm/agent/symbol_search.py +1881 -0
- external_llm/agent/task_router.py +751 -0
- external_llm/agent/terminal_coordination.py +43 -0
- external_llm/agent/termination_policy.py +284 -0
- external_llm/agent/test_impact_selector.py +411 -0
- external_llm/agent/test_runner.py +450 -0
- external_llm/agent/tool_chain.py +36 -0
- external_llm/agent/tool_dependency_graph.py +169 -0
- external_llm/agent/tool_failure_log.py +497 -0
- external_llm/agent/tool_handlers/__init__.py +25 -0
- external_llm/agent/tool_handlers/agent_tools.py +289 -0
- external_llm/agent/tool_handlers/analysis_tools.py +1343 -0
- external_llm/agent/tool_handlers/ast_op_executor.py +1233 -0
- external_llm/agent/tool_handlers/browser_tools.py +325 -0
- external_llm/agent/tool_handlers/constants.py +10 -0
- external_llm/agent/tool_handlers/git_tools.py +1031 -0
- external_llm/agent/tool_handlers/read_tools.py +530 -0
- external_llm/agent/tool_handlers/shell_policy.py +17 -0
- external_llm/agent/tool_handlers/test_tools.py +228 -0
- external_llm/agent/tool_handlers/web_search_tools.py +769 -0
- external_llm/agent/tool_handlers/write_tools.py +6213 -0
- external_llm/agent/tool_registry.py +2268 -0
- external_llm/agent/tool_result_cache.py +178 -0
- external_llm/agent/tool_safety.py +956 -0
- external_llm/agent/tool_schemas.py +1176 -0
- external_llm/agent/vector_cache.py +614 -0
- external_llm/agent/weight_learning.py +2929 -0
- external_llm/agent/work_state_digest.py +157 -0
- external_llm/analysis/__init__.py +0 -0
- external_llm/analysis/_dead_block_shared.py +848 -0
- external_llm/analysis/ast_similarity_scanner.py +1128 -0
- external_llm/analysis/broken_contract_scanner.py +591 -0
- external_llm/analysis/container_reachability_scanner.py +792 -0
- external_llm/analysis/contradictory_logic_scanner.py +693 -0
- external_llm/analysis/cross_file_refs.py +430 -0
- external_llm/analysis/dead_block_scanner.py +97 -0
- external_llm/analysis/duplicate_definition_scanner.py +353 -0
- external_llm/analysis/parse_cache.py +110 -0
- external_llm/analysis/public_dead_code_scanner.py +106 -0
- external_llm/analysis/unused_import_scanner.py +385 -0
- external_llm/analysis/vulture_scanner.py +445 -0
- external_llm/anthropic_client.py +1235 -0
- external_llm/ast_rewrite.py +322 -0
- external_llm/client.py +485 -0
- external_llm/code_analyzer.py +302 -0
- external_llm/code_structure_utils.py +1554 -0
- external_llm/common/__init__.py +0 -0
- external_llm/common/atomic_io.py +240 -0
- external_llm/common/file_lock.py +134 -0
- external_llm/common/indent_utils.py +839 -0
- external_llm/context/context_packs.py +60 -0
- external_llm/context_builder.py +411 -0
- external_llm/dependency_graph.py +303 -0
- external_llm/design_session.py +629 -0
- external_llm/edit_localization/__init__.py +26 -0
- external_llm/edit_localization/dataflow_extractor.py +472 -0
- external_llm/edit_localization/graph_propagator.py +241 -0
- external_llm/edit_localization/relevance_scorer.py +310 -0
- external_llm/edit_localization/request_analyzer.py +89 -0
- external_llm/editor/_editor_core/__init__.py +0 -0
- external_llm/editor/_editor_core/common/__init__.py +0 -0
- external_llm/editor/_editor_core/common/import_normalizer.py +376 -0
- external_llm/editor/_editor_core/ts_vm/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/ast_rewriter.py +118 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/models.py +27 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/rollback_manager.py +41 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/verifier.py +212 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/vm.py +193 -0
- external_llm/editor/_editor_core/ts_vm/learning/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/learning/exploration_policy.py +43 -0
- external_llm/editor/_editor_core/ts_vm/primitives/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/primitives/executor.py +66 -0
- external_llm/editor/_editor_core/ts_vm/primitives/models.py +21 -0
- external_llm/editor/_editor_core/ts_vm/repair/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/repair/failure_classifier.py +88 -0
- external_llm/editor/_editor_core/ts_vm/repair/repair_planner.py +94 -0
- external_llm/editor/_editor_core/ts_vm/repair/repair_registry.py +42 -0
- external_llm/editor/_editor_core/ts_vm/repair/repair_strategies.py +113 -0
- external_llm/editor/_editor_core/vm/__init__.py +36 -0
- external_llm/editor/_editor_core/vm/ast_rewriter.py +99 -0
- external_llm/editor/_editor_core/vm/classification.py +70 -0
- external_llm/editor/_editor_core/vm/contracts/__init__.py +3 -0
- external_llm/editor/_editor_core/vm/contracts/contract_diff.py +225 -0
- external_llm/editor/_editor_core/vm/contracts/contract_extractor.py +271 -0
- external_llm/editor/_editor_core/vm/contracts/contract_models.py +93 -0
- external_llm/editor/_editor_core/vm/contracts/project_graph.py +201 -0
- external_llm/editor/_editor_core/vm/contracts/propagation_planner.py +237 -0
- external_llm/editor/_editor_core/vm/failure_classifier.py +390 -0
- external_llm/editor/_editor_core/vm/models.py +28 -0
- external_llm/editor/_editor_core/vm/repair_planner.py +91 -0
- external_llm/editor/_editor_core/vm/repair_registry.py +34 -0
- external_llm/editor/_editor_core/vm/repair_strategies.py +1049 -0
- external_llm/editor/_editor_core/vm/rollback_manager.py +40 -0
- external_llm/editor/_editor_core/vm/verifier.py +381 -0
- external_llm/editor/_editor_core/vm/vm.py +326 -0
- external_llm/editor/agent/autonomous/__init__.py +45 -0
- external_llm/editor/agent/autonomous/proactive_runner.py +386 -0
- external_llm/editor/agent/autonomous/push_manager.py +299 -0
- external_llm/editor/agent/autonomous/task_queue.py +225 -0
- external_llm/editor/agent/autonomous/trigger_engine.py +175 -0
- external_llm/editor/agent/autonomous/trigger_policy.py +294 -0
- external_llm/editor/agent/mcp/__init__.py +13 -0
- external_llm/editor/agent/mcp/server.py +175 -0
- external_llm/editor/agent/prompts/__init__.py +2 -0
- external_llm/editor/agent/prompts/edit_prompts.py +299 -0
- external_llm/editor/cross_language/__init__.py +4 -0
- external_llm/editor/cross_language/models.py +79 -0
- external_llm/editor/cross_language/strategy_abstraction.py +204 -0
- external_llm/editor/cross_language/transfer_engine.py +170 -0
- external_llm/editor/learning/__init__.py +1 -0
- external_llm/editor/learning/experience_store.py +343 -0
- external_llm/editor/learning/exploration_state.py +124 -0
- external_llm/editor/learning/learning_sink.py +131 -0
- external_llm/editor/learning/pattern_extractor.py +158 -0
- external_llm/editor/learning/primitive_learning_models.py +125 -0
- external_llm/editor/learning/primitive_learning_scorer.py +145 -0
- external_llm/editor/learning/primitive_learning_store.py +142 -0
- external_llm/editor/learning/primitive_learning_updater.py +128 -0
- external_llm/editor/learning/primitive_sequence_models.py +58 -0
- external_llm/editor/learning/primitive_sequence_scorer.py +117 -0
- external_llm/editor/learning/primitive_sequence_store.py +115 -0
- external_llm/editor/learning/primitive_sequence_updater.py +74 -0
- external_llm/editor/learning/problem_signature.py +161 -0
- external_llm/editor/learning/strategy_execution_bias.py +345 -0
- external_llm/editor/learning/strategy_prioritizer.py +245 -0
- external_llm/editor/learning/strategy_state.py +128 -0
- external_llm/editor/learning/unified_run_record.py +118 -0
- external_llm/editor/learning/unified_store.py +566 -0
- external_llm/editor/primitives/__init__.py +2 -0
- external_llm/editor/primitives/code_context.py +660 -0
- external_llm/editor/primitives/delete_node.py +41 -0
- external_llm/editor/primitives/executor.py +106 -0
- external_llm/editor/primitives/insert_import.py +90 -0
- external_llm/editor/primitives/insert_statement.py +77 -0
- external_llm/editor/primitives/models.py +87 -0
- external_llm/editor/primitives/registry.py +49 -0
- external_llm/editor/primitives/rename_symbol.py +91 -0
- external_llm/editor/primitives/replace_function.py +260 -0
- external_llm/editor/primitives/update_call.py +76 -0
- external_llm/editor/semantic/__init__.py +1 -0
- external_llm/editor/semantic/ast_transform_utils.py +450 -0
- external_llm/editor/semantic/contract_replan_strategy.py +98 -0
- external_llm/editor/semantic/draft_parser.py +206 -0
- external_llm/editor/semantic/flow_synthesizer.py +165 -0
- external_llm/editor/semantic/fragment_generator.py +21 -0
- external_llm/editor/semantic/fragment_integrator.py +163 -0
- external_llm/editor/semantic/generation_planner.py +19 -0
- external_llm/editor/semantic/libcst_transform_utils.py +254 -0
- external_llm/editor/semantic/output_comparator.py +90 -0
- external_llm/editor/semantic/primitive_code_templates.py +307 -0
- external_llm/editor/semantic/primitive_detector.py +402 -0
- external_llm/editor/semantic/primitive_ir_builder.py +109 -0
- external_llm/editor/semantic/primitive_models.py +108 -0
- external_llm/editor/semantic/primitive_reconstructor.py +264 -0
- external_llm/editor/semantic/primitive_registry.py +328 -0
- external_llm/editor/semantic/semantic_contract_evaluator.py +321 -0
- external_llm/editor/semantic/semantic_contract_models.py +124 -0
- external_llm/editor/semantic/semantic_contract_registry.py +134 -0
- external_llm/editor/semantic/semantic_contracts.py +72 -0
- external_llm/editor/semantic/semantic_corrector.py +75 -0
- external_llm/editor/semantic/semantic_gap_analyzer.py +194 -0
- external_llm/editor/semantic/semantic_rewrite_models.py +62 -0
- external_llm/editor/semantic/semantic_rewrite_planner.py +252 -0
- external_llm/editor/semantic/semantic_rewriter.py +304 -0
- external_llm/editor/semantic/semantic_tracer.py +427 -0
- external_llm/editor/semantic/synthesis_planner.py +99 -0
- external_llm/editor/semantic/ts_ir_models.py +414 -0
- external_llm/editor/semantic/ts_semantic_models.py +112 -0
- external_llm/editor/semantic/ts_semantic_tracer.py +1302 -0
- external_llm/editor/simulator/dependency_traversal.py +172 -0
- external_llm/editor/simulator/impact_models.py +42 -0
- external_llm/editor/simulator/impact_simulator.py +187 -0
- external_llm/editor/simulator/risk_estimator.py +71 -0
- external_llm/editor/verification/__init__.py +7 -0
- external_llm/editor/verification/code_integrity.py +406 -0
- external_llm/editor/verification/impact_propagation.py +401 -0
- external_llm/editor/verification/impact_verification_mapper.py +148 -0
- external_llm/editor/verification/verification_set_builder.py +329 -0
- external_llm/graph/composite_risk.py +226 -0
- external_llm/graph/execution_graph_advisor.py +636 -0
- external_llm/graph/graph_builder.py +31 -0
- external_llm/graph/graph_facade.py +198 -0
- external_llm/graph/models.py +113 -0
- external_llm/graph/repository_graph.py +1061 -0
- external_llm/graph/run_scoped_graph_cache.py +283 -0
- external_llm/graph/spec_graph_enricher.py +855 -0
- external_llm/graph/virtual_graph.py +429 -0
- external_llm/hybrid_parser.py +215 -0
- external_llm/image_utils.py +147 -0
- external_llm/intelligent_service.py +2147 -0
- external_llm/languages/__init__.py +24 -0
- external_llm/languages/base.py +450 -0
- external_llm/languages/bash_provider.py +94 -0
- external_llm/languages/capabilities.py +129 -0
- external_llm/languages/csharp_provider.py +111 -0
- external_llm/languages/css_provider.py +178 -0
- external_llm/languages/dependency_checker.py +753 -0
- external_llm/languages/go_provider.py +452 -0
- external_llm/languages/html_provider.py +136 -0
- external_llm/languages/java_provider.py +485 -0
- external_llm/languages/javascript_provider.py +334 -0
- external_llm/languages/json_provider.py +113 -0
- external_llm/languages/kotlin_provider.py +462 -0
- external_llm/languages/libcst_utils.py +699 -0
- external_llm/languages/models.py +195 -0
- external_llm/languages/php_provider.py +90 -0
- external_llm/languages/python_provider.py +390 -0
- external_llm/languages/registry.py +128 -0
- external_llm/languages/ruby_provider.py +90 -0
- external_llm/languages/rust_provider.py +89 -0
- external_llm/languages/swift_provider.py +95 -0
- external_llm/languages/syntax_validator.py +357 -0
- external_llm/languages/tree_sitter_utils.py +2140 -0
- external_llm/languages/typescript_provider.py +694 -0
- external_llm/model_registry.py +153 -0
- external_llm/multi_planner.py +744 -0
- external_llm/ollama_api.py +126 -0
- external_llm/openai_client.py +1281 -0
- external_llm/output_modes.py +29 -0
- external_llm/output_parser.py +760 -0
- external_llm/patch_engine.py +2482 -0
- external_llm/patch_synthesizer.py +251 -0
- external_llm/project_analyzer.py +1386 -0
- external_llm/providers.py +2112 -0
- external_llm/repl/collaborate/__init__.py +46 -0
- external_llm/repl/collaborate/asi_mcp_adapter.py +550 -0
- external_llm/repl/collaborate/claude_session.py +484 -0
- external_llm/repl/collaborate/cli.py +257 -0
- external_llm/repl/collaborate/collaboration_orchestrator.py +422 -0
- external_llm/repl/collaborate/streaming_display.py +667 -0
- external_llm/repl/collaborate/verdict.py +145 -0
- external_llm/semantic_patch.py +190 -0
- external_llm/service.py +1690 -0
- external_llm/smart_analyzer.py +466 -0
- external_llm/super_context_builder.py +644 -0
- external_llm/testing/__init__.py +0 -0
- external_llm/testing/symbol_aware_test_finder.py +377 -0
- external_llm/testing/test_dependency_graph.py +380 -0
- patch_synth.py +1587 -0
- path_security.py +143 -0
- plan_compiler.py +1058 -0
- services/__init__.py +0 -0
- services/patch_helpers.py +151 -0
- utils/__init__.py +1 -0
- utils/llm_utils.py +22 -0
- utils/string_helper.py +274 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutonomousTaskQueue — priority queue with concurrency limiting and deduplication.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Priority ordering: priority 0 (critical) served before 2 (normal)
|
|
6
|
+
- Concurrency limit: MAX_CONCURRENT tasks running simultaneously
|
|
7
|
+
- File deduplication: later event for the same source_file replaces
|
|
8
|
+
any pending (not yet executing) task for that file
|
|
9
|
+
- Non-blocking get: get_nowait() returns None when queue empty or at limit
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import queue
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from external_llm.agent.config.thresholds import config as _cfg
|
|
22
|
+
from external_llm.editor.agent.autonomous.trigger_engine import TriggerEvent
|
|
23
|
+
from external_llm.editor.agent.autonomous.trigger_policy import ActionDecision, ActionKind
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(order=True)
|
|
29
|
+
class AutonomousTask:
|
|
30
|
+
priority: int # sort key: lower = higher priority
|
|
31
|
+
created_at: float = field(compare=False)
|
|
32
|
+
event: TriggerEvent = field(compare=False)
|
|
33
|
+
action: ActionDecision = field(compare=False)
|
|
34
|
+
task_id: str = field(compare=False, default="")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AutonomousTaskQueue:
|
|
38
|
+
"""Thread-safe priority queue for autonomous tasks."""
|
|
39
|
+
|
|
40
|
+
MAX_CONCURRENT = 2 # max tasks executing simultaneously
|
|
41
|
+
# Defense-in-depth cap on pending tasks. Superseded/orphaned tasks are purged
|
|
42
|
+
# by get_nowait() regardless of the concurrency limit (see below), so under
|
|
43
|
+
# normal operation the queue holds at most a handful of *live* tasks. This cap
|
|
44
|
+
# guards against unbounded growth if upstream policy cooldowns are bypassed.
|
|
45
|
+
MAX_PENDING = _cfg.counts.AUTONOMOUS_TASK_QUEUE_MAX
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self._pq: queue.PriorityQueue = queue.PriorityQueue()
|
|
49
|
+
self._lock = threading.Lock()
|
|
50
|
+
self._running_count = 0
|
|
51
|
+
# source_file → task_id of the pending (not yet running) task for that file
|
|
52
|
+
self._pending_file_map: dict[str, str] = {}
|
|
53
|
+
self._counter = 0
|
|
54
|
+
# Number of tasks rejected because MAX_PENDING was reached (observability).
|
|
55
|
+
self._dropped_count = 0
|
|
56
|
+
|
|
57
|
+
def _purge_tombstones_locked(self) -> int:
|
|
58
|
+
"""Drop superseded/orphaned (tombstone) tasks from the heap, re-queueing
|
|
59
|
+
the live ones. Caller must hold ``self._lock``.
|
|
60
|
+
|
|
61
|
+
A task is a tombstone when its ``source_file`` is tracked in
|
|
62
|
+
``_pending_file_map`` under a *different* (newer) task_id, or the map
|
|
63
|
+
entry was already cleared (the newer task already ran). Tasks without a
|
|
64
|
+
``source_file`` are never tombstones. Map entries are not touched here:
|
|
65
|
+
a tombstone never owns the current map slot (it points to a newer task or
|
|
66
|
+
was already deleted), and a live task must remain tracked.
|
|
67
|
+
|
|
68
|
+
Returns the number of tombstones purged (observability).
|
|
69
|
+
"""
|
|
70
|
+
live: list[AutonomousTask] = []
|
|
71
|
+
purged = 0
|
|
72
|
+
while True:
|
|
73
|
+
try:
|
|
74
|
+
task = self._pq.get_nowait()
|
|
75
|
+
except queue.Empty:
|
|
76
|
+
break
|
|
77
|
+
sf = task.event.source_file
|
|
78
|
+
if sf and self._pending_file_map.get(sf) != task.task_id:
|
|
79
|
+
purged += 1
|
|
80
|
+
continue
|
|
81
|
+
live.append(task)
|
|
82
|
+
for task in live:
|
|
83
|
+
self._pq.put(task)
|
|
84
|
+
if purged:
|
|
85
|
+
logger.debug(
|
|
86
|
+
"Purged %d tombstone task(s) during cap-check (live=%d)",
|
|
87
|
+
purged, len(live),
|
|
88
|
+
)
|
|
89
|
+
return purged
|
|
90
|
+
|
|
91
|
+
# ── Enqueue ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def enqueue(self, event: TriggerEvent, action: ActionDecision) -> Optional[str]:
|
|
94
|
+
"""
|
|
95
|
+
Enqueue a task. Returns task_id or None if ignored or rejected at cap.
|
|
96
|
+
|
|
97
|
+
If a pending task already exists for event.source_file, the new task
|
|
98
|
+
replaces it conceptually (old task will be silently dropped on get).
|
|
99
|
+
|
|
100
|
+
If the queue is at MAX_PENDING, the new task is rejected (newest-loss:
|
|
101
|
+
an already-loaded backlog should be drained before accepting more) and
|
|
102
|
+
the dropped counter is incremented.
|
|
103
|
+
"""
|
|
104
|
+
if action.kind == ActionKind.IGNORE:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
with self._lock:
|
|
108
|
+
# A supersede (same source_file already pending) does not grow the
|
|
109
|
+
# *live* task count — the old entry becomes a tombstone purged on the
|
|
110
|
+
# next get_nowait(). Exempt it from the cap so the newest event for a
|
|
111
|
+
# file always wins, even under backpressure.
|
|
112
|
+
is_supersede = bool(event.source_file and event.source_file in self._pending_file_map)
|
|
113
|
+
if not is_supersede and self._pq.qsize() >= self.MAX_PENDING:
|
|
114
|
+
# ``qsize()`` counts tombstones (superseded entries not yet reaped
|
|
115
|
+
# by get_nowait()). Under a same-file supersede burst they can
|
|
116
|
+
# inflate qsize far beyond the *live* task count and wrongly
|
|
117
|
+
# reject an unrelated file's first task. Purge tombstones and
|
|
118
|
+
# re-check against the true live count before dropping a new task.
|
|
119
|
+
self._purge_tombstones_locked()
|
|
120
|
+
if self._pq.qsize() >= self.MAX_PENDING:
|
|
121
|
+
self._dropped_count += 1
|
|
122
|
+
logger.warning(
|
|
123
|
+
"AutonomousTaskQueue at cap (%d); dropping new task for %s "
|
|
124
|
+
"(dropped_total=%d)",
|
|
125
|
+
self.MAX_PENDING, event.source_file, self._dropped_count,
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
self._counter += 1
|
|
130
|
+
task_id = f"auto-{self._counter}"
|
|
131
|
+
|
|
132
|
+
task = AutonomousTask(
|
|
133
|
+
priority=action.priority,
|
|
134
|
+
created_at=time.time(),
|
|
135
|
+
event=event,
|
|
136
|
+
action=action,
|
|
137
|
+
task_id=task_id,
|
|
138
|
+
)
|
|
139
|
+
self._pq.put(task)
|
|
140
|
+
|
|
141
|
+
if event.source_file:
|
|
142
|
+
self._pending_file_map[event.source_file] = task_id
|
|
143
|
+
|
|
144
|
+
logger.debug("Enqueued task %s (%s, priority=%d)", task_id, action.kind.value, action.priority)
|
|
145
|
+
return task_id
|
|
146
|
+
|
|
147
|
+
# ── Dequeue ───────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def get_nowait(self) -> Optional[AutonomousTask]:
|
|
150
|
+
"""
|
|
151
|
+
Get next task if concurrency limit not reached. Non-blocking.
|
|
152
|
+
Returns None if queue is empty or at MAX_CONCURRENT.
|
|
153
|
+
|
|
154
|
+
Superseded/orphaned tasks are purged *regardless* of the concurrency
|
|
155
|
+
limit: the entire queue is scanned each call so stale tasks never
|
|
156
|
+
accumulate while both execution slots are occupied by long-running
|
|
157
|
+
AUTO_FIX tasks. At most one live task is claimed per call (if a slot is
|
|
158
|
+
free); remaining live tasks are re-queued for subsequent calls.
|
|
159
|
+
"""
|
|
160
|
+
with self._lock:
|
|
161
|
+
result: Optional[AutonomousTask] = None
|
|
162
|
+
held_live: list[AutonomousTask] = []
|
|
163
|
+
while True:
|
|
164
|
+
try:
|
|
165
|
+
task = self._pq.get_nowait()
|
|
166
|
+
except queue.Empty:
|
|
167
|
+
break
|
|
168
|
+
sf = task.event.source_file
|
|
169
|
+
if sf:
|
|
170
|
+
current = self._pending_file_map.get(sf)
|
|
171
|
+
if current != task.task_id:
|
|
172
|
+
# Superseded OR orphaned: either a newer task for the
|
|
173
|
+
# same file is still pending (current is a newer id), or
|
|
174
|
+
# a newer task already ran and cleared this map entry
|
|
175
|
+
# (current is None). In both cases this stale task must
|
|
176
|
+
# NOT run — purge it WITHOUT claiming a concurrency slot.
|
|
177
|
+
# This purge runs even when _running_count >= MAX_CONCURRENT
|
|
178
|
+
# so stale tasks cannot accumulate during long runs.
|
|
179
|
+
logger.debug(
|
|
180
|
+
"Dropping superseded task %s for %s (current=%s)",
|
|
181
|
+
task.task_id, sf, current,
|
|
182
|
+
)
|
|
183
|
+
continue
|
|
184
|
+
del self._pending_file_map[sf]
|
|
185
|
+
# Live task. Claim a slot for the first one if available;
|
|
186
|
+
# hold the rest aside to re-queue after the purge completes.
|
|
187
|
+
if result is None and self._running_count < self.MAX_CONCURRENT:
|
|
188
|
+
self._running_count += 1
|
|
189
|
+
result = task
|
|
190
|
+
# Keep scanning to purge remaining stale tasks.
|
|
191
|
+
continue
|
|
192
|
+
held_live.append(task)
|
|
193
|
+
# Re-queue live tasks that couldn't run (slot full or already filled).
|
|
194
|
+
for t in held_live:
|
|
195
|
+
self._pq.put(t)
|
|
196
|
+
tsf = t.event.source_file
|
|
197
|
+
if tsf:
|
|
198
|
+
self._pending_file_map[tsf] = t.task_id
|
|
199
|
+
if result is not None:
|
|
200
|
+
logger.debug(
|
|
201
|
+
"Dequeued task %s. Running: %d", result.task_id, self._running_count
|
|
202
|
+
)
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
# ── Completion ────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def task_done(self, task_id: str) -> None:
|
|
208
|
+
"""Must be called when a task finishes (success or error)."""
|
|
209
|
+
with self._lock:
|
|
210
|
+
self._running_count = max(0, self._running_count - 1)
|
|
211
|
+
logger.debug("Task %s done. Running: %d", task_id, self._running_count)
|
|
212
|
+
|
|
213
|
+
# ── Status ────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
def qsize(self) -> int:
|
|
216
|
+
return self._pq.qsize()
|
|
217
|
+
|
|
218
|
+
def running_count(self) -> int:
|
|
219
|
+
with self._lock:
|
|
220
|
+
return self._running_count
|
|
221
|
+
|
|
222
|
+
def dropped_count(self) -> int:
|
|
223
|
+
"""Number of tasks rejected at MAX_PENDING (observability for backpressure)."""
|
|
224
|
+
with self._lock:
|
|
225
|
+
return self._dropped_count
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TriggerEngine — event collection hub for the autonomous agent system.
|
|
3
|
+
|
|
4
|
+
Sources:
|
|
5
|
+
1. AgentLoop interceptor — fail_loop_detected / complete / error callbacks
|
|
6
|
+
2. TestRunner hooks — test pass / fail notifications
|
|
7
|
+
3. Scheduler — periodic SCHEDULE events (threading.Timer)
|
|
8
|
+
|
|
9
|
+
All sources call emit(TriggerEvent) which fans out to registered callbacks.
|
|
10
|
+
Callbacks run synchronously in the emitting thread (kept lightweight).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TriggerKind(Enum):
|
|
27
|
+
FILE_MODIFIED = "file_modified" # .py/.js/.ts saved
|
|
28
|
+
TEST_FAILED = "test_failed" # pytest failure
|
|
29
|
+
TEST_RECOVERED = "test_recovered" # failure → pass
|
|
30
|
+
AGENT_STALL = "agent_stall" # fail_loop_detected (N consecutive fails)
|
|
31
|
+
AGENT_COMPLETED = "agent_completed" # run finished successfully
|
|
32
|
+
AGENT_FAILED = "agent_failed" # run finished with error
|
|
33
|
+
IMPORT_ERROR = "import_error" # py_compile failure
|
|
34
|
+
INTEGRATION_MISSING = "integration_missing" # GSG: unlinked new module
|
|
35
|
+
SCHEDULE = "schedule" # periodic cron event
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class TriggerEvent:
|
|
40
|
+
kind: TriggerKind
|
|
41
|
+
repo_root: str
|
|
42
|
+
source_file: Optional[str] = None
|
|
43
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
timestamp: float = field(default_factory=time.time)
|
|
45
|
+
severity: int = 0 # 0=info 1=warning 2=error 3=critical
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TriggerEngine:
|
|
49
|
+
"""Collects events from multiple sources and routes to registered on_trigger callbacks."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, repo_root: str):
|
|
52
|
+
self.repo_root = repo_root
|
|
53
|
+
self._callbacks: list[Callable[[TriggerEvent], None]] = []
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
self._schedule_timers: list[threading.Timer] = []
|
|
56
|
+
self._running = False
|
|
57
|
+
|
|
58
|
+
# ── Registration ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def on_trigger(self, callback: Callable[[TriggerEvent], None]) -> None:
|
|
61
|
+
"""Register a callback to receive all TriggerEvents."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._callbacks.append(callback)
|
|
64
|
+
|
|
65
|
+
# ── Emission ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def emit(self, event: TriggerEvent) -> None:
|
|
68
|
+
"""Emit event to all registered callbacks. Thread-safe."""
|
|
69
|
+
with self._lock:
|
|
70
|
+
cbs = list(self._callbacks)
|
|
71
|
+
for cb in cbs:
|
|
72
|
+
try:
|
|
73
|
+
cb(event)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.warning("TriggerEngine callback error: %s", exc)
|
|
76
|
+
|
|
77
|
+
# ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def start(self) -> None:
|
|
80
|
+
"""Start the trigger engine. Thread-safe."""
|
|
81
|
+
with self._lock:
|
|
82
|
+
if self._running:
|
|
83
|
+
return
|
|
84
|
+
self._running = True
|
|
85
|
+
logger.info("TriggerEngine started (repo=%s)", self.repo_root)
|
|
86
|
+
|
|
87
|
+
def stop(self) -> None:
|
|
88
|
+
"""Stop all sources. Thread-safe."""
|
|
89
|
+
with self._lock:
|
|
90
|
+
self._running = False
|
|
91
|
+
timers = list(self._schedule_timers)
|
|
92
|
+
self._schedule_timers.clear()
|
|
93
|
+
for t in timers:
|
|
94
|
+
try:
|
|
95
|
+
t.cancel()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
logger.info("TriggerEngine stopped")
|
|
99
|
+
|
|
100
|
+
# ── Agent event hooks ─────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def notify_agent_event(self, event_name: str, data: dict[str, Any]) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Forward stream_callback events from AgentLoop.
|
|
105
|
+
Called by make_stream_callback_interceptor in proactive_runner.py.
|
|
106
|
+
"""
|
|
107
|
+
if event_name == "fail_loop_detected":
|
|
108
|
+
self.emit(TriggerEvent(
|
|
109
|
+
kind=TriggerKind.AGENT_STALL,
|
|
110
|
+
repo_root=self.repo_root,
|
|
111
|
+
severity=2,
|
|
112
|
+
metadata=data,
|
|
113
|
+
))
|
|
114
|
+
elif event_name == "complete":
|
|
115
|
+
status = data.get("status", "")
|
|
116
|
+
if status in ("error", "max_turns"):
|
|
117
|
+
self.emit(TriggerEvent(
|
|
118
|
+
kind=TriggerKind.AGENT_FAILED,
|
|
119
|
+
repo_root=self.repo_root,
|
|
120
|
+
severity=2,
|
|
121
|
+
metadata=data,
|
|
122
|
+
))
|
|
123
|
+
else:
|
|
124
|
+
self.emit(TriggerEvent(
|
|
125
|
+
kind=TriggerKind.AGENT_COMPLETED,
|
|
126
|
+
repo_root=self.repo_root,
|
|
127
|
+
severity=0,
|
|
128
|
+
metadata=data,
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
# ── Test result hooks ─────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
def notify_test_result(self, ok: bool, details: dict[str, Any]) -> None:
|
|
134
|
+
"""Called after test runs. ok=True → TEST_RECOVERED, False → TEST_FAILED."""
|
|
135
|
+
self.emit(TriggerEvent(
|
|
136
|
+
kind=TriggerKind.TEST_RECOVERED if ok else TriggerKind.TEST_FAILED,
|
|
137
|
+
repo_root=self.repo_root,
|
|
138
|
+
severity=0 if ok else 2,
|
|
139
|
+
metadata=details,
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
# ── Scheduler ─────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def schedule(self, interval_seconds: float, label: str) -> None:
|
|
145
|
+
"""Register a periodic SCHEDULE trigger (fires every interval_seconds)."""
|
|
146
|
+
|
|
147
|
+
# holder[0] tracks the currently active timer so _fire can remove it on re-arm
|
|
148
|
+
holder: list[Optional[threading.Timer]] = [None]
|
|
149
|
+
|
|
150
|
+
def _fire():
|
|
151
|
+
if not self._running:
|
|
152
|
+
return
|
|
153
|
+
self.emit(TriggerEvent(
|
|
154
|
+
kind=TriggerKind.SCHEDULE,
|
|
155
|
+
repo_root=self.repo_root,
|
|
156
|
+
severity=0,
|
|
157
|
+
metadata={"label": label, "interval": interval_seconds},
|
|
158
|
+
))
|
|
159
|
+
# Re-arm: remove completed timer from list, register the new one
|
|
160
|
+
next_t = threading.Timer(interval_seconds, _fire)
|
|
161
|
+
next_t.daemon = True
|
|
162
|
+
with self._lock:
|
|
163
|
+
if holder[0] in self._schedule_timers:
|
|
164
|
+
self._schedule_timers.remove(holder[0])
|
|
165
|
+
self._schedule_timers.append(next_t)
|
|
166
|
+
holder[0] = next_t
|
|
167
|
+
next_t.start()
|
|
168
|
+
|
|
169
|
+
t = threading.Timer(interval_seconds, _fire)
|
|
170
|
+
t.daemon = True
|
|
171
|
+
with self._lock:
|
|
172
|
+
self._schedule_timers.append(t)
|
|
173
|
+
holder[0] = t
|
|
174
|
+
t.start()
|
|
175
|
+
logger.info("TriggerEngine: scheduled '%s' every %.0fs", label, interval_seconds)
|