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.
Files changed (340) hide show
  1. asi.py +9644 -0
  2. asicode-0.2.6.dist-info/METADATA +208 -0
  3. asicode-0.2.6.dist-info/RECORD +340 -0
  4. asicode-0.2.6.dist-info/WHEEL +5 -0
  5. asicode-0.2.6.dist-info/entry_points.txt +2 -0
  6. asicode-0.2.6.dist-info/licenses/LICENSE +21 -0
  7. asicode-0.2.6.dist-info/top_level.txt +11 -0
  8. common.py +86 -0
  9. config.py +162 -0
  10. context_collector.py +388 -0
  11. diff_apply.py +1549 -0
  12. external_llm/__init__.py +53 -0
  13. external_llm/agent/__init__.py +1 -0
  14. external_llm/agent/_shared_utils.py +1713 -0
  15. external_llm/agent/_thread_pool.py +13 -0
  16. external_llm/agent/_user_intent.py +233 -0
  17. external_llm/agent/agent_context_manager.py +532 -0
  18. external_llm/agent/agent_escalation_pipeline.py +161 -0
  19. external_llm/agent/agent_fast_path.py +63 -0
  20. external_llm/agent/agent_loop.py +3567 -0
  21. external_llm/agent/agent_loop_types.py +207 -0
  22. external_llm/agent/agent_phase_manager.py +352 -0
  23. external_llm/agent/agent_planner_pipeline.py +1756 -0
  24. external_llm/agent/agent_profile.py +200 -0
  25. external_llm/agent/agent_turn_pipeline.py +2592 -0
  26. external_llm/agent/alignment_scorer.py +333 -0
  27. external_llm/agent/anchor_shared.py +310 -0
  28. external_llm/agent/argument_repairer.py +119 -0
  29. external_llm/agent/ast_cache.py +148 -0
  30. external_llm/agent/ast_engine.py +1060 -0
  31. external_llm/agent/async_tool_executor.py +182 -0
  32. external_llm/agent/auto_correction.py +2268 -0
  33. external_llm/agent/background_job_manager.py +571 -0
  34. external_llm/agent/call_graph.py +516 -0
  35. external_llm/agent/checkpoint_store.py +472 -0
  36. external_llm/agent/config/__init__.py +0 -0
  37. external_llm/agent/config/thresholds.py +409 -0
  38. external_llm/agent/context_budget.py +593 -0
  39. external_llm/agent/context_contract.py +231 -0
  40. external_llm/agent/context_manager.py +1301 -0
  41. external_llm/agent/context_utils.py +29 -0
  42. external_llm/agent/design_chat_loop.py +2510 -0
  43. external_llm/agent/enums.py +48 -0
  44. external_llm/agent/execution_mode_classifier.py +323 -0
  45. external_llm/agent/execution_spec.py +355 -0
  46. external_llm/agent/execution_thresholds.py +281 -0
  47. external_llm/agent/failure_classifier.py +160 -0
  48. external_llm/agent/failure_context.py +409 -0
  49. external_llm/agent/failure_pattern_store.py +911 -0
  50. external_llm/agent/file_cache.py +144 -0
  51. external_llm/agent/fix_spec_claim_validator.py +747 -0
  52. external_llm/agent/gsg_safety.py +114 -0
  53. external_llm/agent/guard_ir.py +985 -0
  54. external_llm/agent/handoff_observer.py +327 -0
  55. external_llm/agent/insights_manager.py +1124 -0
  56. external_llm/agent/intent_models.py +250 -0
  57. external_llm/agent/intent_resolver.py +861 -0
  58. external_llm/agent/interrupt_tool_results.py +95 -0
  59. external_llm/agent/json_repair.py +283 -0
  60. external_llm/agent/language_backend.py +612 -0
  61. external_llm/agent/language_hint.py +51 -0
  62. external_llm/agent/learned_policy.py +826 -0
  63. external_llm/agent/light_repo_index.py +231 -0
  64. external_llm/agent/lint_runner.py +397 -0
  65. external_llm/agent/llm_output_utils.py +52 -0
  66. external_llm/agent/local_assistant.py +804 -0
  67. external_llm/agent/message_shapes.py +89 -0
  68. external_llm/agent/metadata_utils.py +14 -0
  69. external_llm/agent/operation_models.py +2707 -0
  70. external_llm/agent/orchestrator.py +5109 -0
  71. external_llm/agent/output_normalizer.py +415 -0
  72. external_llm/agent/performance_metrics.py +452 -0
  73. external_llm/agent/placement_contract.py +2765 -0
  74. external_llm/agent/plan_state.py +136 -0
  75. external_llm/agent/planner_lane_facade.py +67 -0
  76. external_llm/agent/rag_configs.py +168 -0
  77. external_llm/agent/rag_searcher.py +693 -0
  78. external_llm/agent/reasoning_utils.py +62 -0
  79. external_llm/agent/repair_helpers.py +475 -0
  80. external_llm/agent/request_intent_classifier.py +95 -0
  81. external_llm/agent/routing_policy.py +157 -0
  82. external_llm/agent/run_store.py +2356 -0
  83. external_llm/agent/scanner_registry.py +610 -0
  84. external_llm/agent/selector_constants.py +26 -0
  85. external_llm/agent/semantic_intent.py +163 -0
  86. external_llm/agent/semantic_lint.py +94 -0
  87. external_llm/agent/session_state.py +80 -0
  88. external_llm/agent/slash_commands.py +90 -0
  89. external_llm/agent/subagent_ipc.py +1222 -0
  90. external_llm/agent/symbol_engine.py +592 -0
  91. external_llm/agent/symbol_index.py +380 -0
  92. external_llm/agent/symbol_locator.py +117 -0
  93. external_llm/agent/symbol_modify_tool.py +1258 -0
  94. external_llm/agent/symbol_search.py +1881 -0
  95. external_llm/agent/task_router.py +751 -0
  96. external_llm/agent/terminal_coordination.py +43 -0
  97. external_llm/agent/termination_policy.py +284 -0
  98. external_llm/agent/test_impact_selector.py +411 -0
  99. external_llm/agent/test_runner.py +450 -0
  100. external_llm/agent/tool_chain.py +36 -0
  101. external_llm/agent/tool_dependency_graph.py +169 -0
  102. external_llm/agent/tool_failure_log.py +497 -0
  103. external_llm/agent/tool_handlers/__init__.py +25 -0
  104. external_llm/agent/tool_handlers/agent_tools.py +289 -0
  105. external_llm/agent/tool_handlers/analysis_tools.py +1343 -0
  106. external_llm/agent/tool_handlers/ast_op_executor.py +1233 -0
  107. external_llm/agent/tool_handlers/browser_tools.py +325 -0
  108. external_llm/agent/tool_handlers/constants.py +10 -0
  109. external_llm/agent/tool_handlers/git_tools.py +1031 -0
  110. external_llm/agent/tool_handlers/read_tools.py +530 -0
  111. external_llm/agent/tool_handlers/shell_policy.py +17 -0
  112. external_llm/agent/tool_handlers/test_tools.py +228 -0
  113. external_llm/agent/tool_handlers/web_search_tools.py +769 -0
  114. external_llm/agent/tool_handlers/write_tools.py +6213 -0
  115. external_llm/agent/tool_registry.py +2268 -0
  116. external_llm/agent/tool_result_cache.py +178 -0
  117. external_llm/agent/tool_safety.py +956 -0
  118. external_llm/agent/tool_schemas.py +1176 -0
  119. external_llm/agent/vector_cache.py +614 -0
  120. external_llm/agent/weight_learning.py +2929 -0
  121. external_llm/agent/work_state_digest.py +157 -0
  122. external_llm/analysis/__init__.py +0 -0
  123. external_llm/analysis/_dead_block_shared.py +848 -0
  124. external_llm/analysis/ast_similarity_scanner.py +1128 -0
  125. external_llm/analysis/broken_contract_scanner.py +591 -0
  126. external_llm/analysis/container_reachability_scanner.py +792 -0
  127. external_llm/analysis/contradictory_logic_scanner.py +693 -0
  128. external_llm/analysis/cross_file_refs.py +430 -0
  129. external_llm/analysis/dead_block_scanner.py +97 -0
  130. external_llm/analysis/duplicate_definition_scanner.py +353 -0
  131. external_llm/analysis/parse_cache.py +110 -0
  132. external_llm/analysis/public_dead_code_scanner.py +106 -0
  133. external_llm/analysis/unused_import_scanner.py +385 -0
  134. external_llm/analysis/vulture_scanner.py +445 -0
  135. external_llm/anthropic_client.py +1235 -0
  136. external_llm/ast_rewrite.py +322 -0
  137. external_llm/client.py +485 -0
  138. external_llm/code_analyzer.py +302 -0
  139. external_llm/code_structure_utils.py +1554 -0
  140. external_llm/common/__init__.py +0 -0
  141. external_llm/common/atomic_io.py +240 -0
  142. external_llm/common/file_lock.py +134 -0
  143. external_llm/common/indent_utils.py +839 -0
  144. external_llm/context/context_packs.py +60 -0
  145. external_llm/context_builder.py +411 -0
  146. external_llm/dependency_graph.py +303 -0
  147. external_llm/design_session.py +629 -0
  148. external_llm/edit_localization/__init__.py +26 -0
  149. external_llm/edit_localization/dataflow_extractor.py +472 -0
  150. external_llm/edit_localization/graph_propagator.py +241 -0
  151. external_llm/edit_localization/relevance_scorer.py +310 -0
  152. external_llm/edit_localization/request_analyzer.py +89 -0
  153. external_llm/editor/_editor_core/__init__.py +0 -0
  154. external_llm/editor/_editor_core/common/__init__.py +0 -0
  155. external_llm/editor/_editor_core/common/import_normalizer.py +376 -0
  156. external_llm/editor/_editor_core/ts_vm/__init__.py +0 -0
  157. external_llm/editor/_editor_core/ts_vm/execution_vm/__init__.py +0 -0
  158. external_llm/editor/_editor_core/ts_vm/execution_vm/ast_rewriter.py +118 -0
  159. external_llm/editor/_editor_core/ts_vm/execution_vm/models.py +27 -0
  160. external_llm/editor/_editor_core/ts_vm/execution_vm/rollback_manager.py +41 -0
  161. external_llm/editor/_editor_core/ts_vm/execution_vm/verifier.py +212 -0
  162. external_llm/editor/_editor_core/ts_vm/execution_vm/vm.py +193 -0
  163. external_llm/editor/_editor_core/ts_vm/learning/__init__.py +0 -0
  164. external_llm/editor/_editor_core/ts_vm/learning/exploration_policy.py +43 -0
  165. external_llm/editor/_editor_core/ts_vm/primitives/__init__.py +0 -0
  166. external_llm/editor/_editor_core/ts_vm/primitives/executor.py +66 -0
  167. external_llm/editor/_editor_core/ts_vm/primitives/models.py +21 -0
  168. external_llm/editor/_editor_core/ts_vm/repair/__init__.py +0 -0
  169. external_llm/editor/_editor_core/ts_vm/repair/failure_classifier.py +88 -0
  170. external_llm/editor/_editor_core/ts_vm/repair/repair_planner.py +94 -0
  171. external_llm/editor/_editor_core/ts_vm/repair/repair_registry.py +42 -0
  172. external_llm/editor/_editor_core/ts_vm/repair/repair_strategies.py +113 -0
  173. external_llm/editor/_editor_core/vm/__init__.py +36 -0
  174. external_llm/editor/_editor_core/vm/ast_rewriter.py +99 -0
  175. external_llm/editor/_editor_core/vm/classification.py +70 -0
  176. external_llm/editor/_editor_core/vm/contracts/__init__.py +3 -0
  177. external_llm/editor/_editor_core/vm/contracts/contract_diff.py +225 -0
  178. external_llm/editor/_editor_core/vm/contracts/contract_extractor.py +271 -0
  179. external_llm/editor/_editor_core/vm/contracts/contract_models.py +93 -0
  180. external_llm/editor/_editor_core/vm/contracts/project_graph.py +201 -0
  181. external_llm/editor/_editor_core/vm/contracts/propagation_planner.py +237 -0
  182. external_llm/editor/_editor_core/vm/failure_classifier.py +390 -0
  183. external_llm/editor/_editor_core/vm/models.py +28 -0
  184. external_llm/editor/_editor_core/vm/repair_planner.py +91 -0
  185. external_llm/editor/_editor_core/vm/repair_registry.py +34 -0
  186. external_llm/editor/_editor_core/vm/repair_strategies.py +1049 -0
  187. external_llm/editor/_editor_core/vm/rollback_manager.py +40 -0
  188. external_llm/editor/_editor_core/vm/verifier.py +381 -0
  189. external_llm/editor/_editor_core/vm/vm.py +326 -0
  190. external_llm/editor/agent/autonomous/__init__.py +45 -0
  191. external_llm/editor/agent/autonomous/proactive_runner.py +386 -0
  192. external_llm/editor/agent/autonomous/push_manager.py +299 -0
  193. external_llm/editor/agent/autonomous/task_queue.py +225 -0
  194. external_llm/editor/agent/autonomous/trigger_engine.py +175 -0
  195. external_llm/editor/agent/autonomous/trigger_policy.py +294 -0
  196. external_llm/editor/agent/mcp/__init__.py +13 -0
  197. external_llm/editor/agent/mcp/server.py +175 -0
  198. external_llm/editor/agent/prompts/__init__.py +2 -0
  199. external_llm/editor/agent/prompts/edit_prompts.py +299 -0
  200. external_llm/editor/cross_language/__init__.py +4 -0
  201. external_llm/editor/cross_language/models.py +79 -0
  202. external_llm/editor/cross_language/strategy_abstraction.py +204 -0
  203. external_llm/editor/cross_language/transfer_engine.py +170 -0
  204. external_llm/editor/learning/__init__.py +1 -0
  205. external_llm/editor/learning/experience_store.py +343 -0
  206. external_llm/editor/learning/exploration_state.py +124 -0
  207. external_llm/editor/learning/learning_sink.py +131 -0
  208. external_llm/editor/learning/pattern_extractor.py +158 -0
  209. external_llm/editor/learning/primitive_learning_models.py +125 -0
  210. external_llm/editor/learning/primitive_learning_scorer.py +145 -0
  211. external_llm/editor/learning/primitive_learning_store.py +142 -0
  212. external_llm/editor/learning/primitive_learning_updater.py +128 -0
  213. external_llm/editor/learning/primitive_sequence_models.py +58 -0
  214. external_llm/editor/learning/primitive_sequence_scorer.py +117 -0
  215. external_llm/editor/learning/primitive_sequence_store.py +115 -0
  216. external_llm/editor/learning/primitive_sequence_updater.py +74 -0
  217. external_llm/editor/learning/problem_signature.py +161 -0
  218. external_llm/editor/learning/strategy_execution_bias.py +345 -0
  219. external_llm/editor/learning/strategy_prioritizer.py +245 -0
  220. external_llm/editor/learning/strategy_state.py +128 -0
  221. external_llm/editor/learning/unified_run_record.py +118 -0
  222. external_llm/editor/learning/unified_store.py +566 -0
  223. external_llm/editor/primitives/__init__.py +2 -0
  224. external_llm/editor/primitives/code_context.py +660 -0
  225. external_llm/editor/primitives/delete_node.py +41 -0
  226. external_llm/editor/primitives/executor.py +106 -0
  227. external_llm/editor/primitives/insert_import.py +90 -0
  228. external_llm/editor/primitives/insert_statement.py +77 -0
  229. external_llm/editor/primitives/models.py +87 -0
  230. external_llm/editor/primitives/registry.py +49 -0
  231. external_llm/editor/primitives/rename_symbol.py +91 -0
  232. external_llm/editor/primitives/replace_function.py +260 -0
  233. external_llm/editor/primitives/update_call.py +76 -0
  234. external_llm/editor/semantic/__init__.py +1 -0
  235. external_llm/editor/semantic/ast_transform_utils.py +450 -0
  236. external_llm/editor/semantic/contract_replan_strategy.py +98 -0
  237. external_llm/editor/semantic/draft_parser.py +206 -0
  238. external_llm/editor/semantic/flow_synthesizer.py +165 -0
  239. external_llm/editor/semantic/fragment_generator.py +21 -0
  240. external_llm/editor/semantic/fragment_integrator.py +163 -0
  241. external_llm/editor/semantic/generation_planner.py +19 -0
  242. external_llm/editor/semantic/libcst_transform_utils.py +254 -0
  243. external_llm/editor/semantic/output_comparator.py +90 -0
  244. external_llm/editor/semantic/primitive_code_templates.py +307 -0
  245. external_llm/editor/semantic/primitive_detector.py +402 -0
  246. external_llm/editor/semantic/primitive_ir_builder.py +109 -0
  247. external_llm/editor/semantic/primitive_models.py +108 -0
  248. external_llm/editor/semantic/primitive_reconstructor.py +264 -0
  249. external_llm/editor/semantic/primitive_registry.py +328 -0
  250. external_llm/editor/semantic/semantic_contract_evaluator.py +321 -0
  251. external_llm/editor/semantic/semantic_contract_models.py +124 -0
  252. external_llm/editor/semantic/semantic_contract_registry.py +134 -0
  253. external_llm/editor/semantic/semantic_contracts.py +72 -0
  254. external_llm/editor/semantic/semantic_corrector.py +75 -0
  255. external_llm/editor/semantic/semantic_gap_analyzer.py +194 -0
  256. external_llm/editor/semantic/semantic_rewrite_models.py +62 -0
  257. external_llm/editor/semantic/semantic_rewrite_planner.py +252 -0
  258. external_llm/editor/semantic/semantic_rewriter.py +304 -0
  259. external_llm/editor/semantic/semantic_tracer.py +427 -0
  260. external_llm/editor/semantic/synthesis_planner.py +99 -0
  261. external_llm/editor/semantic/ts_ir_models.py +414 -0
  262. external_llm/editor/semantic/ts_semantic_models.py +112 -0
  263. external_llm/editor/semantic/ts_semantic_tracer.py +1302 -0
  264. external_llm/editor/simulator/dependency_traversal.py +172 -0
  265. external_llm/editor/simulator/impact_models.py +42 -0
  266. external_llm/editor/simulator/impact_simulator.py +187 -0
  267. external_llm/editor/simulator/risk_estimator.py +71 -0
  268. external_llm/editor/verification/__init__.py +7 -0
  269. external_llm/editor/verification/code_integrity.py +406 -0
  270. external_llm/editor/verification/impact_propagation.py +401 -0
  271. external_llm/editor/verification/impact_verification_mapper.py +148 -0
  272. external_llm/editor/verification/verification_set_builder.py +329 -0
  273. external_llm/graph/composite_risk.py +226 -0
  274. external_llm/graph/execution_graph_advisor.py +636 -0
  275. external_llm/graph/graph_builder.py +31 -0
  276. external_llm/graph/graph_facade.py +198 -0
  277. external_llm/graph/models.py +113 -0
  278. external_llm/graph/repository_graph.py +1061 -0
  279. external_llm/graph/run_scoped_graph_cache.py +283 -0
  280. external_llm/graph/spec_graph_enricher.py +855 -0
  281. external_llm/graph/virtual_graph.py +429 -0
  282. external_llm/hybrid_parser.py +215 -0
  283. external_llm/image_utils.py +147 -0
  284. external_llm/intelligent_service.py +2147 -0
  285. external_llm/languages/__init__.py +24 -0
  286. external_llm/languages/base.py +450 -0
  287. external_llm/languages/bash_provider.py +94 -0
  288. external_llm/languages/capabilities.py +129 -0
  289. external_llm/languages/csharp_provider.py +111 -0
  290. external_llm/languages/css_provider.py +178 -0
  291. external_llm/languages/dependency_checker.py +753 -0
  292. external_llm/languages/go_provider.py +452 -0
  293. external_llm/languages/html_provider.py +136 -0
  294. external_llm/languages/java_provider.py +485 -0
  295. external_llm/languages/javascript_provider.py +334 -0
  296. external_llm/languages/json_provider.py +113 -0
  297. external_llm/languages/kotlin_provider.py +462 -0
  298. external_llm/languages/libcst_utils.py +699 -0
  299. external_llm/languages/models.py +195 -0
  300. external_llm/languages/php_provider.py +90 -0
  301. external_llm/languages/python_provider.py +390 -0
  302. external_llm/languages/registry.py +128 -0
  303. external_llm/languages/ruby_provider.py +90 -0
  304. external_llm/languages/rust_provider.py +89 -0
  305. external_llm/languages/swift_provider.py +95 -0
  306. external_llm/languages/syntax_validator.py +357 -0
  307. external_llm/languages/tree_sitter_utils.py +2140 -0
  308. external_llm/languages/typescript_provider.py +694 -0
  309. external_llm/model_registry.py +153 -0
  310. external_llm/multi_planner.py +744 -0
  311. external_llm/ollama_api.py +126 -0
  312. external_llm/openai_client.py +1281 -0
  313. external_llm/output_modes.py +29 -0
  314. external_llm/output_parser.py +760 -0
  315. external_llm/patch_engine.py +2482 -0
  316. external_llm/patch_synthesizer.py +251 -0
  317. external_llm/project_analyzer.py +1386 -0
  318. external_llm/providers.py +2112 -0
  319. external_llm/repl/collaborate/__init__.py +46 -0
  320. external_llm/repl/collaborate/asi_mcp_adapter.py +550 -0
  321. external_llm/repl/collaborate/claude_session.py +484 -0
  322. external_llm/repl/collaborate/cli.py +257 -0
  323. external_llm/repl/collaborate/collaboration_orchestrator.py +422 -0
  324. external_llm/repl/collaborate/streaming_display.py +667 -0
  325. external_llm/repl/collaborate/verdict.py +145 -0
  326. external_llm/semantic_patch.py +190 -0
  327. external_llm/service.py +1690 -0
  328. external_llm/smart_analyzer.py +466 -0
  329. external_llm/super_context_builder.py +644 -0
  330. external_llm/testing/__init__.py +0 -0
  331. external_llm/testing/symbol_aware_test_finder.py +377 -0
  332. external_llm/testing/test_dependency_graph.py +380 -0
  333. patch_synth.py +1587 -0
  334. path_security.py +143 -0
  335. plan_compiler.py +1058 -0
  336. services/__init__.py +0 -0
  337. services/patch_helpers.py +151 -0
  338. utils/__init__.py +1 -0
  339. utils/llm_utils.py +22 -0
  340. 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)