tapps-agents 3.5.40__py3-none-any.whl → 3.6.0__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.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/__init__.py +22 -22
- tapps_agents/agents/analyst/__init__.py +5 -5
- tapps_agents/agents/architect/__init__.py +5 -5
- tapps_agents/agents/architect/agent.py +1033 -1033
- tapps_agents/agents/architect/pattern_detector.py +75 -75
- tapps_agents/agents/cleanup/__init__.py +7 -7
- tapps_agents/agents/cleanup/agent.py +445 -445
- tapps_agents/agents/debugger/__init__.py +7 -7
- tapps_agents/agents/debugger/agent.py +310 -310
- tapps_agents/agents/debugger/error_analyzer.py +437 -437
- tapps_agents/agents/designer/__init__.py +5 -5
- tapps_agents/agents/designer/agent.py +786 -786
- tapps_agents/agents/designer/visual_designer.py +638 -638
- tapps_agents/agents/documenter/__init__.py +7 -7
- tapps_agents/agents/documenter/agent.py +531 -531
- tapps_agents/agents/documenter/doc_generator.py +472 -472
- tapps_agents/agents/documenter/doc_validator.py +393 -393
- tapps_agents/agents/documenter/framework_doc_updater.py +493 -493
- tapps_agents/agents/enhancer/__init__.py +7 -7
- tapps_agents/agents/evaluator/__init__.py +7 -7
- tapps_agents/agents/evaluator/agent.py +443 -443
- tapps_agents/agents/evaluator/priority_evaluator.py +641 -641
- tapps_agents/agents/evaluator/quality_analyzer.py +147 -147
- tapps_agents/agents/evaluator/report_generator.py +344 -344
- tapps_agents/agents/evaluator/usage_analyzer.py +192 -192
- tapps_agents/agents/evaluator/workflow_analyzer.py +189 -189
- tapps_agents/agents/implementer/__init__.py +7 -7
- tapps_agents/agents/implementer/agent.py +798 -798
- tapps_agents/agents/implementer/auto_fix.py +1119 -1119
- tapps_agents/agents/implementer/code_generator.py +73 -73
- tapps_agents/agents/improver/__init__.py +1 -1
- tapps_agents/agents/improver/agent.py +753 -753
- tapps_agents/agents/ops/__init__.py +1 -1
- tapps_agents/agents/ops/agent.py +619 -619
- tapps_agents/agents/ops/dependency_analyzer.py +600 -600
- tapps_agents/agents/orchestrator/__init__.py +5 -5
- tapps_agents/agents/orchestrator/agent.py +522 -522
- tapps_agents/agents/planner/__init__.py +7 -7
- tapps_agents/agents/planner/agent.py +1127 -1127
- tapps_agents/agents/reviewer/__init__.py +24 -24
- tapps_agents/agents/reviewer/agent.py +3513 -3513
- tapps_agents/agents/reviewer/aggregator.py +213 -213
- tapps_agents/agents/reviewer/batch_review.py +448 -448
- tapps_agents/agents/reviewer/cache.py +443 -443
- tapps_agents/agents/reviewer/context7_enhancer.py +630 -630
- tapps_agents/agents/reviewer/context_detector.py +203 -203
- tapps_agents/agents/reviewer/docker_compose_validator.py +158 -158
- tapps_agents/agents/reviewer/dockerfile_validator.py +176 -176
- tapps_agents/agents/reviewer/error_handling.py +126 -126
- tapps_agents/agents/reviewer/feedback_generator.py +490 -490
- tapps_agents/agents/reviewer/influxdb_validator.py +316 -316
- tapps_agents/agents/reviewer/issue_tracking.py +169 -169
- tapps_agents/agents/reviewer/library_detector.py +295 -295
- tapps_agents/agents/reviewer/library_patterns.py +268 -268
- tapps_agents/agents/reviewer/maintainability_scorer.py +593 -593
- tapps_agents/agents/reviewer/metric_strategies.py +276 -276
- tapps_agents/agents/reviewer/mqtt_validator.py +160 -160
- tapps_agents/agents/reviewer/output_enhancer.py +105 -105
- tapps_agents/agents/reviewer/pattern_detector.py +241 -241
- tapps_agents/agents/reviewer/performance_scorer.py +357 -357
- tapps_agents/agents/reviewer/phased_review.py +516 -516
- tapps_agents/agents/reviewer/progressive_review.py +435 -435
- tapps_agents/agents/reviewer/react_scorer.py +331 -331
- tapps_agents/agents/reviewer/score_constants.py +228 -228
- tapps_agents/agents/reviewer/score_validator.py +507 -507
- tapps_agents/agents/reviewer/scorer_registry.py +373 -373
- tapps_agents/agents/reviewer/scoring.py +1566 -1566
- tapps_agents/agents/reviewer/service_discovery.py +534 -534
- tapps_agents/agents/reviewer/tools/__init__.py +41 -41
- tapps_agents/agents/reviewer/tools/parallel_executor.py +581 -581
- tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -250
- tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -284
- tapps_agents/agents/reviewer/typescript_scorer.py +1142 -1142
- tapps_agents/agents/reviewer/validation.py +208 -208
- tapps_agents/agents/reviewer/websocket_validator.py +132 -132
- tapps_agents/agents/tester/__init__.py +7 -7
- tapps_agents/agents/tester/accessibility_auditor.py +309 -309
- tapps_agents/agents/tester/agent.py +1080 -1080
- tapps_agents/agents/tester/batch_generator.py +54 -54
- tapps_agents/agents/tester/context_learner.py +51 -51
- tapps_agents/agents/tester/coverage_analyzer.py +386 -386
- tapps_agents/agents/tester/coverage_test_generator.py +290 -290
- tapps_agents/agents/tester/debug_enhancer.py +238 -238
- tapps_agents/agents/tester/device_emulator.py +241 -241
- tapps_agents/agents/tester/integration_generator.py +62 -62
- tapps_agents/agents/tester/network_recorder.py +300 -300
- tapps_agents/agents/tester/performance_monitor.py +320 -320
- tapps_agents/agents/tester/test_fixer.py +316 -316
- tapps_agents/agents/tester/test_generator.py +632 -632
- tapps_agents/agents/tester/trace_manager.py +234 -234
- tapps_agents/agents/tester/visual_regression.py +291 -291
- tapps_agents/analysis/pattern_detector.py +36 -36
- tapps_agents/beads/hydration.py +213 -213
- tapps_agents/beads/parse.py +32 -32
- tapps_agents/beads/specs.py +206 -206
- tapps_agents/cli/__init__.py +9 -9
- tapps_agents/cli/__main__.py +8 -8
- tapps_agents/cli/base.py +478 -478
- tapps_agents/cli/command_classifier.py +72 -72
- tapps_agents/cli/commands/__init__.py +2 -2
- tapps_agents/cli/commands/analyst.py +173 -173
- tapps_agents/cli/commands/architect.py +109 -109
- tapps_agents/cli/commands/cleanup_agent.py +92 -92
- tapps_agents/cli/commands/common.py +126 -126
- tapps_agents/cli/commands/debugger.py +90 -90
- tapps_agents/cli/commands/designer.py +112 -112
- tapps_agents/cli/commands/documenter.py +136 -136
- tapps_agents/cli/commands/enhancer.py +110 -110
- tapps_agents/cli/commands/evaluator.py +255 -255
- tapps_agents/cli/commands/health.py +665 -665
- tapps_agents/cli/commands/implementer.py +301 -301
- tapps_agents/cli/commands/improver.py +91 -91
- tapps_agents/cli/commands/knowledge.py +111 -111
- tapps_agents/cli/commands/learning.py +172 -172
- tapps_agents/cli/commands/observability.py +283 -283
- tapps_agents/cli/commands/ops.py +135 -135
- tapps_agents/cli/commands/orchestrator.py +116 -116
- tapps_agents/cli/commands/planner.py +237 -237
- tapps_agents/cli/commands/reviewer.py +1872 -1872
- tapps_agents/cli/commands/status.py +285 -285
- tapps_agents/cli/commands/task.py +227 -219
- tapps_agents/cli/commands/tester.py +191 -191
- tapps_agents/cli/commands/top_level.py +3586 -3586
- tapps_agents/cli/feedback.py +936 -936
- tapps_agents/cli/formatters.py +608 -608
- tapps_agents/cli/help/__init__.py +7 -7
- tapps_agents/cli/help/static_help.py +425 -425
- tapps_agents/cli/network_detection.py +110 -110
- tapps_agents/cli/output_compactor.py +274 -274
- tapps_agents/cli/parsers/__init__.py +2 -2
- tapps_agents/cli/parsers/analyst.py +186 -186
- tapps_agents/cli/parsers/architect.py +167 -167
- tapps_agents/cli/parsers/cleanup_agent.py +228 -228
- tapps_agents/cli/parsers/debugger.py +116 -116
- tapps_agents/cli/parsers/designer.py +182 -182
- tapps_agents/cli/parsers/documenter.py +134 -134
- tapps_agents/cli/parsers/enhancer.py +113 -113
- tapps_agents/cli/parsers/evaluator.py +213 -213
- tapps_agents/cli/parsers/implementer.py +168 -168
- tapps_agents/cli/parsers/improver.py +132 -132
- tapps_agents/cli/parsers/ops.py +159 -159
- tapps_agents/cli/parsers/orchestrator.py +98 -98
- tapps_agents/cli/parsers/planner.py +145 -145
- tapps_agents/cli/parsers/reviewer.py +462 -462
- tapps_agents/cli/parsers/tester.py +124 -124
- tapps_agents/cli/progress_heartbeat.py +254 -254
- tapps_agents/cli/streaming_progress.py +336 -336
- tapps_agents/cli/utils/__init__.py +6 -6
- tapps_agents/cli/utils/agent_lifecycle.py +48 -48
- tapps_agents/cli/utils/error_formatter.py +82 -82
- tapps_agents/cli/utils/error_recovery.py +188 -188
- tapps_agents/cli/utils/output_handler.py +59 -59
- tapps_agents/cli/utils/prompt_enhancer.py +319 -319
- tapps_agents/cli/validators/__init__.py +9 -9
- tapps_agents/cli/validators/command_validator.py +81 -81
- tapps_agents/context7/__init__.py +112 -112
- tapps_agents/context7/agent_integration.py +869 -869
- tapps_agents/context7/analytics.py +382 -382
- tapps_agents/context7/analytics_dashboard.py +299 -299
- tapps_agents/context7/async_cache.py +681 -681
- tapps_agents/context7/backup_client.py +958 -958
- tapps_agents/context7/cache_locking.py +194 -194
- tapps_agents/context7/cache_metadata.py +214 -214
- tapps_agents/context7/cache_prewarm.py +488 -488
- tapps_agents/context7/cache_structure.py +168 -168
- tapps_agents/context7/cache_warming.py +604 -604
- tapps_agents/context7/circuit_breaker.py +376 -376
- tapps_agents/context7/cleanup.py +461 -461
- tapps_agents/context7/commands.py +858 -858
- tapps_agents/context7/credential_validation.py +276 -276
- tapps_agents/context7/cross_reference_resolver.py +168 -168
- tapps_agents/context7/cross_references.py +424 -424
- tapps_agents/context7/doc_manager.py +225 -225
- tapps_agents/context7/fuzzy_matcher.py +369 -369
- tapps_agents/context7/kb_cache.py +404 -404
- tapps_agents/context7/language_detector.py +219 -219
- tapps_agents/context7/library_detector.py +725 -725
- tapps_agents/context7/lookup.py +738 -738
- tapps_agents/context7/metadata.py +258 -258
- tapps_agents/context7/refresh_queue.py +300 -300
- tapps_agents/context7/security.py +373 -373
- tapps_agents/context7/staleness_policies.py +278 -278
- tapps_agents/context7/tiles_integration.py +47 -47
- tapps_agents/continuous_bug_fix/__init__.py +20 -20
- tapps_agents/continuous_bug_fix/bug_finder.py +306 -306
- tapps_agents/continuous_bug_fix/bug_fix_coordinator.py +177 -177
- tapps_agents/continuous_bug_fix/commit_manager.py +178 -178
- tapps_agents/continuous_bug_fix/continuous_bug_fixer.py +322 -322
- tapps_agents/continuous_bug_fix/proactive_bug_finder.py +285 -285
- tapps_agents/core/__init__.py +298 -298
- tapps_agents/core/adaptive_cache_config.py +432 -432
- tapps_agents/core/agent_base.py +647 -647
- tapps_agents/core/agent_cache.py +466 -466
- tapps_agents/core/agent_learning.py +1865 -1865
- tapps_agents/core/analytics_dashboard.py +563 -563
- tapps_agents/core/analytics_enhancements.py +597 -597
- tapps_agents/core/anonymization.py +274 -274
- tapps_agents/core/artifact_context_builder.py +293 -0
- tapps_agents/core/ast_parser.py +228 -228
- tapps_agents/core/async_file_ops.py +402 -402
- tapps_agents/core/best_practice_consultant.py +299 -299
- tapps_agents/core/brownfield_analyzer.py +299 -299
- tapps_agents/core/brownfield_review.py +541 -541
- tapps_agents/core/browser_controller.py +513 -513
- tapps_agents/core/capability_registry.py +418 -418
- tapps_agents/core/change_impact_analyzer.py +190 -190
- tapps_agents/core/checkpoint_manager.py +377 -377
- tapps_agents/core/code_generator.py +329 -329
- tapps_agents/core/code_validator.py +276 -276
- tapps_agents/core/command_registry.py +327 -327
- tapps_agents/core/config.py +33 -0
- tapps_agents/core/context_gathering/__init__.py +2 -2
- tapps_agents/core/context_gathering/repository_explorer.py +28 -28
- tapps_agents/core/context_intelligence/__init__.py +2 -2
- tapps_agents/core/context_intelligence/relevance_scorer.py +24 -24
- tapps_agents/core/context_intelligence/token_budget_manager.py +27 -27
- tapps_agents/core/context_manager.py +240 -240
- tapps_agents/core/cursor_feedback_monitor.py +146 -146
- tapps_agents/core/cursor_verification.py +290 -290
- tapps_agents/core/customization_loader.py +280 -280
- tapps_agents/core/customization_schema.py +260 -260
- tapps_agents/core/customization_template.py +238 -238
- tapps_agents/core/debug_logger.py +124 -124
- tapps_agents/core/design_validator.py +298 -298
- tapps_agents/core/diagram_generator.py +226 -226
- tapps_agents/core/docker_utils.py +232 -232
- tapps_agents/core/document_generator.py +617 -617
- tapps_agents/core/domain_detector.py +30 -30
- tapps_agents/core/error_envelope.py +454 -454
- tapps_agents/core/error_handler.py +270 -270
- tapps_agents/core/estimation_tracker.py +189 -189
- tapps_agents/core/eval_prompt_engine.py +116 -116
- tapps_agents/core/evaluation_base.py +119 -119
- tapps_agents/core/evaluation_models.py +320 -320
- tapps_agents/core/evaluation_orchestrator.py +225 -225
- tapps_agents/core/evaluators/__init__.py +7 -7
- tapps_agents/core/evaluators/architectural_evaluator.py +205 -205
- tapps_agents/core/evaluators/behavioral_evaluator.py +160 -160
- tapps_agents/core/evaluators/performance_profile_evaluator.py +160 -160
- tapps_agents/core/evaluators/security_posture_evaluator.py +148 -148
- tapps_agents/core/evaluators/spec_compliance_evaluator.py +181 -181
- tapps_agents/core/exceptions.py +107 -107
- tapps_agents/core/expert_config_generator.py +293 -293
- tapps_agents/core/export_schema.py +202 -202
- tapps_agents/core/external_feedback_models.py +102 -102
- tapps_agents/core/external_feedback_storage.py +213 -213
- tapps_agents/core/fallback_strategy.py +314 -314
- tapps_agents/core/feedback_analyzer.py +162 -162
- tapps_agents/core/feedback_collector.py +178 -178
- tapps_agents/core/git_operations.py +445 -445
- tapps_agents/core/hardware_profiler.py +151 -151
- tapps_agents/core/instructions.py +324 -324
- tapps_agents/core/io_guardrails.py +69 -69
- tapps_agents/core/issue_manifest.py +249 -249
- tapps_agents/core/issue_schema.py +139 -139
- tapps_agents/core/json_utils.py +128 -128
- tapps_agents/core/knowledge_graph.py +446 -446
- tapps_agents/core/language_detector.py +296 -296
- tapps_agents/core/learning_confidence.py +242 -242
- tapps_agents/core/learning_dashboard.py +246 -246
- tapps_agents/core/learning_decision.py +384 -384
- tapps_agents/core/learning_explainability.py +578 -578
- tapps_agents/core/learning_export.py +287 -287
- tapps_agents/core/learning_integration.py +228 -228
- tapps_agents/core/llm_behavior.py +232 -232
- tapps_agents/core/long_duration_support.py +786 -786
- tapps_agents/core/mcp_setup.py +106 -106
- tapps_agents/core/memory_integration.py +396 -396
- tapps_agents/core/meta_learning.py +666 -666
- tapps_agents/core/module_path_sanitizer.py +199 -199
- tapps_agents/core/multi_agent_orchestrator.py +382 -382
- tapps_agents/core/network_errors.py +125 -125
- tapps_agents/core/nfr_validator.py +336 -336
- tapps_agents/core/offline_mode.py +158 -158
- tapps_agents/core/output_contracts.py +300 -300
- tapps_agents/core/output_formatter.py +300 -300
- tapps_agents/core/path_normalizer.py +174 -174
- tapps_agents/core/path_validator.py +322 -322
- tapps_agents/core/pattern_library.py +250 -250
- tapps_agents/core/performance_benchmark.py +301 -301
- tapps_agents/core/performance_monitor.py +184 -184
- tapps_agents/core/playwright_mcp_controller.py +771 -771
- tapps_agents/core/policy_loader.py +135 -135
- tapps_agents/core/progress.py +166 -166
- tapps_agents/core/project_profile.py +354 -354
- tapps_agents/core/project_type_detector.py +454 -454
- tapps_agents/core/prompt_base.py +223 -223
- tapps_agents/core/prompt_learning/__init__.py +2 -2
- tapps_agents/core/prompt_learning/learning_loop.py +24 -24
- tapps_agents/core/prompt_learning/project_prompt_store.py +25 -25
- tapps_agents/core/prompt_learning/skills_prompt_analyzer.py +35 -35
- tapps_agents/core/prompt_optimization/__init__.py +6 -6
- tapps_agents/core/prompt_optimization/ab_tester.py +114 -114
- tapps_agents/core/prompt_optimization/correlation_analyzer.py +160 -160
- tapps_agents/core/prompt_optimization/progressive_refiner.py +129 -129
- tapps_agents/core/prompt_optimization/prompt_library.py +37 -37
- tapps_agents/core/requirements_evaluator.py +431 -431
- tapps_agents/core/resource_aware_executor.py +449 -449
- tapps_agents/core/resource_monitor.py +343 -343
- tapps_agents/core/resume_handler.py +298 -298
- tapps_agents/core/retry_handler.py +197 -197
- tapps_agents/core/review_checklists.py +479 -479
- tapps_agents/core/role_loader.py +201 -201
- tapps_agents/core/role_template_loader.py +201 -201
- tapps_agents/core/runtime_mode.py +60 -60
- tapps_agents/core/security_scanner.py +342 -342
- tapps_agents/core/skill_agent_registry.py +194 -194
- tapps_agents/core/skill_integration.py +208 -208
- tapps_agents/core/skill_loader.py +492 -492
- tapps_agents/core/skill_template.py +341 -341
- tapps_agents/core/skill_validator.py +478 -478
- tapps_agents/core/stack_analyzer.py +35 -35
- tapps_agents/core/startup.py +174 -174
- tapps_agents/core/storage_manager.py +397 -397
- tapps_agents/core/storage_models.py +166 -166
- tapps_agents/core/story_evaluator.py +410 -410
- tapps_agents/core/subprocess_utils.py +170 -170
- tapps_agents/core/task_duration.py +296 -296
- tapps_agents/core/task_memory.py +582 -582
- tapps_agents/core/task_state.py +226 -226
- tapps_agents/core/tech_stack_priorities.py +208 -208
- tapps_agents/core/temp_directory.py +194 -194
- tapps_agents/core/template_merger.py +600 -600
- tapps_agents/core/template_selector.py +280 -280
- tapps_agents/core/test_generator.py +286 -286
- tapps_agents/core/tiered_context.py +253 -253
- tapps_agents/core/token_monitor.py +345 -345
- tapps_agents/core/traceability.py +254 -254
- tapps_agents/core/trajectory_tracker.py +50 -50
- tapps_agents/core/unicode_safe.py +143 -143
- tapps_agents/core/unified_cache_config.py +170 -170
- tapps_agents/core/unified_state.py +324 -324
- tapps_agents/core/validate_cursor_setup.py +237 -237
- tapps_agents/core/validation_registry.py +136 -136
- tapps_agents/core/validators/__init__.py +4 -4
- tapps_agents/core/validators/python_validator.py +87 -87
- tapps_agents/core/verification_agent.py +90 -90
- tapps_agents/core/visual_feedback.py +644 -644
- tapps_agents/core/workflow_validator.py +197 -197
- tapps_agents/core/worktree.py +367 -367
- tapps_agents/docker/__init__.py +10 -10
- tapps_agents/docker/analyzer.py +186 -186
- tapps_agents/docker/debugger.py +229 -229
- tapps_agents/docker/error_patterns.py +216 -216
- tapps_agents/epic/__init__.py +22 -22
- tapps_agents/epic/beads_sync.py +115 -115
- tapps_agents/epic/markdown_sync.py +105 -105
- tapps_agents/epic/models.py +96 -96
- tapps_agents/experts/__init__.py +163 -163
- tapps_agents/experts/agent_integration.py +243 -243
- tapps_agents/experts/auto_generator.py +331 -331
- tapps_agents/experts/base_expert.py +536 -536
- tapps_agents/experts/builtin_registry.py +261 -261
- tapps_agents/experts/business_metrics.py +565 -565
- tapps_agents/experts/cache.py +266 -266
- tapps_agents/experts/confidence_breakdown.py +306 -306
- tapps_agents/experts/confidence_calculator.py +336 -336
- tapps_agents/experts/confidence_metrics.py +236 -236
- tapps_agents/experts/domain_config.py +311 -311
- tapps_agents/experts/domain_detector.py +550 -550
- tapps_agents/experts/domain_utils.py +84 -84
- tapps_agents/experts/expert_config.py +113 -113
- tapps_agents/experts/expert_engine.py +465 -465
- tapps_agents/experts/expert_registry.py +744 -744
- tapps_agents/experts/expert_synthesizer.py +70 -70
- tapps_agents/experts/governance.py +197 -197
- tapps_agents/experts/history_logger.py +312 -312
- tapps_agents/experts/knowledge/README.md +180 -180
- tapps_agents/experts/knowledge/accessibility/accessible-forms.md +331 -331
- tapps_agents/experts/knowledge/accessibility/aria-patterns.md +344 -344
- tapps_agents/experts/knowledge/accessibility/color-contrast.md +285 -285
- tapps_agents/experts/knowledge/accessibility/keyboard-navigation.md +332 -332
- tapps_agents/experts/knowledge/accessibility/screen-readers.md +282 -282
- tapps_agents/experts/knowledge/accessibility/semantic-html.md +355 -355
- tapps_agents/experts/knowledge/accessibility/testing-accessibility.md +369 -369
- tapps_agents/experts/knowledge/accessibility/wcag-2.1.md +296 -296
- tapps_agents/experts/knowledge/accessibility/wcag-2.2.md +211 -211
- tapps_agents/experts/knowledge/agent-learning/best-practices.md +715 -715
- tapps_agents/experts/knowledge/agent-learning/pattern-extraction.md +282 -282
- tapps_agents/experts/knowledge/agent-learning/prompt-optimization.md +320 -320
- tapps_agents/experts/knowledge/ai-frameworks/model-optimization.md +90 -90
- tapps_agents/experts/knowledge/ai-frameworks/openvino-patterns.md +260 -260
- tapps_agents/experts/knowledge/api-design-integration/api-gateway-patterns.md +309 -309
- tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +521 -521
- tapps_agents/experts/knowledge/api-design-integration/api-versioning.md +421 -421
- tapps_agents/experts/knowledge/api-design-integration/async-protocol-patterns.md +61 -61
- tapps_agents/experts/knowledge/api-design-integration/contract-testing.md +221 -221
- tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +489 -489
- tapps_agents/experts/knowledge/api-design-integration/fastapi-patterns.md +360 -360
- tapps_agents/experts/knowledge/api-design-integration/fastapi-testing.md +262 -262
- tapps_agents/experts/knowledge/api-design-integration/graphql-patterns.md +582 -582
- tapps_agents/experts/knowledge/api-design-integration/grpc-best-practices.md +499 -499
- tapps_agents/experts/knowledge/api-design-integration/mqtt-patterns.md +455 -455
- tapps_agents/experts/knowledge/api-design-integration/rate-limiting.md +507 -507
- tapps_agents/experts/knowledge/api-design-integration/restful-api-design.md +618 -618
- tapps_agents/experts/knowledge/api-design-integration/websocket-patterns.md +480 -480
- tapps_agents/experts/knowledge/cloud-infrastructure/cloud-native-patterns.md +175 -175
- tapps_agents/experts/knowledge/cloud-infrastructure/container-health-checks.md +261 -261
- tapps_agents/experts/knowledge/cloud-infrastructure/containerization.md +222 -222
- tapps_agents/experts/knowledge/cloud-infrastructure/cost-optimization.md +122 -122
- tapps_agents/experts/knowledge/cloud-infrastructure/disaster-recovery.md +153 -153
- tapps_agents/experts/knowledge/cloud-infrastructure/dockerfile-patterns.md +285 -285
- tapps_agents/experts/knowledge/cloud-infrastructure/infrastructure-as-code.md +187 -187
- tapps_agents/experts/knowledge/cloud-infrastructure/kubernetes-patterns.md +253 -253
- tapps_agents/experts/knowledge/cloud-infrastructure/multi-cloud-strategies.md +155 -155
- tapps_agents/experts/knowledge/cloud-infrastructure/serverless-architecture.md +200 -200
- tapps_agents/experts/knowledge/code-quality-analysis/README.md +16 -16
- tapps_agents/experts/knowledge/code-quality-analysis/code-metrics.md +137 -137
- tapps_agents/experts/knowledge/code-quality-analysis/complexity-analysis.md +181 -181
- tapps_agents/experts/knowledge/code-quality-analysis/technical-debt-patterns.md +191 -191
- tapps_agents/experts/knowledge/data-privacy-compliance/anonymization.md +313 -313
- tapps_agents/experts/knowledge/data-privacy-compliance/ccpa.md +255 -255
- tapps_agents/experts/knowledge/data-privacy-compliance/consent-management.md +282 -282
- tapps_agents/experts/knowledge/data-privacy-compliance/data-minimization.md +275 -275
- tapps_agents/experts/knowledge/data-privacy-compliance/data-retention.md +297 -297
- tapps_agents/experts/knowledge/data-privacy-compliance/data-subject-rights.md +383 -383
- tapps_agents/experts/knowledge/data-privacy-compliance/encryption-privacy.md +285 -285
- tapps_agents/experts/knowledge/data-privacy-compliance/gdpr.md +344 -344
- tapps_agents/experts/knowledge/data-privacy-compliance/hipaa.md +385 -385
- tapps_agents/experts/knowledge/data-privacy-compliance/privacy-by-design.md +280 -280
- tapps_agents/experts/knowledge/database-data-management/acid-vs-cap.md +164 -164
- tapps_agents/experts/knowledge/database-data-management/backup-and-recovery.md +182 -182
- tapps_agents/experts/knowledge/database-data-management/data-modeling.md +172 -172
- tapps_agents/experts/knowledge/database-data-management/database-design.md +187 -187
- tapps_agents/experts/knowledge/database-data-management/flux-query-optimization.md +342 -342
- tapps_agents/experts/knowledge/database-data-management/influxdb-connection-patterns.md +432 -432
- tapps_agents/experts/knowledge/database-data-management/influxdb-patterns.md +442 -442
- tapps_agents/experts/knowledge/database-data-management/migration-strategies.md +216 -216
- tapps_agents/experts/knowledge/database-data-management/nosql-patterns.md +259 -259
- tapps_agents/experts/knowledge/database-data-management/scalability-patterns.md +184 -184
- tapps_agents/experts/knowledge/database-data-management/sql-optimization.md +175 -175
- tapps_agents/experts/knowledge/database-data-management/time-series-modeling.md +444 -444
- tapps_agents/experts/knowledge/development-workflow/README.md +16 -16
- tapps_agents/experts/knowledge/development-workflow/automation-best-practices.md +216 -216
- tapps_agents/experts/knowledge/development-workflow/build-strategies.md +198 -198
- tapps_agents/experts/knowledge/development-workflow/deployment-patterns.md +205 -205
- tapps_agents/experts/knowledge/development-workflow/git-workflows.md +205 -205
- tapps_agents/experts/knowledge/documentation-knowledge-management/README.md +16 -16
- tapps_agents/experts/knowledge/documentation-knowledge-management/api-documentation-patterns.md +231 -231
- tapps_agents/experts/knowledge/documentation-knowledge-management/documentation-standards.md +191 -191
- tapps_agents/experts/knowledge/documentation-knowledge-management/knowledge-management.md +171 -171
- tapps_agents/experts/knowledge/documentation-knowledge-management/technical-writing-guide.md +192 -192
- tapps_agents/experts/knowledge/observability-monitoring/alerting-patterns.md +461 -461
- tapps_agents/experts/knowledge/observability-monitoring/apm-tools.md +459 -459
- tapps_agents/experts/knowledge/observability-monitoring/distributed-tracing.md +367 -367
- tapps_agents/experts/knowledge/observability-monitoring/logging-strategies.md +478 -478
- tapps_agents/experts/knowledge/observability-monitoring/metrics-and-monitoring.md +510 -510
- tapps_agents/experts/knowledge/observability-monitoring/observability-best-practices.md +492 -492
- tapps_agents/experts/knowledge/observability-monitoring/open-telemetry.md +573 -573
- tapps_agents/experts/knowledge/observability-monitoring/slo-sli-sla.md +419 -419
- tapps_agents/experts/knowledge/performance/anti-patterns.md +284 -284
- tapps_agents/experts/knowledge/performance/api-performance.md +256 -256
- tapps_agents/experts/knowledge/performance/caching.md +327 -327
- tapps_agents/experts/knowledge/performance/database-performance.md +252 -252
- tapps_agents/experts/knowledge/performance/optimization-patterns.md +327 -327
- tapps_agents/experts/knowledge/performance/profiling.md +297 -297
- tapps_agents/experts/knowledge/performance/resource-management.md +293 -293
- tapps_agents/experts/knowledge/performance/scalability.md +306 -306
- tapps_agents/experts/knowledge/security/owasp-top10.md +209 -209
- tapps_agents/experts/knowledge/security/secure-coding-practices.md +207 -207
- tapps_agents/experts/knowledge/security/threat-modeling.md +220 -220
- tapps_agents/experts/knowledge/security/vulnerability-patterns.md +342 -342
- tapps_agents/experts/knowledge/software-architecture/docker-compose-patterns.md +314 -314
- tapps_agents/experts/knowledge/software-architecture/microservices-patterns.md +379 -379
- tapps_agents/experts/knowledge/software-architecture/service-communication.md +316 -316
- tapps_agents/experts/knowledge/testing/best-practices.md +310 -310
- tapps_agents/experts/knowledge/testing/coverage-analysis.md +293 -293
- tapps_agents/experts/knowledge/testing/mocking.md +256 -256
- tapps_agents/experts/knowledge/testing/test-automation.md +276 -276
- tapps_agents/experts/knowledge/testing/test-data.md +271 -271
- tapps_agents/experts/knowledge/testing/test-design-patterns.md +280 -280
- tapps_agents/experts/knowledge/testing/test-maintenance.md +236 -236
- tapps_agents/experts/knowledge/testing/test-strategies.md +311 -311
- tapps_agents/experts/knowledge/user-experience/information-architecture.md +325 -325
- tapps_agents/experts/knowledge/user-experience/interaction-design.md +363 -363
- tapps_agents/experts/knowledge/user-experience/prototyping.md +293 -293
- tapps_agents/experts/knowledge/user-experience/usability-heuristics.md +337 -337
- tapps_agents/experts/knowledge/user-experience/usability-testing.md +311 -311
- tapps_agents/experts/knowledge/user-experience/user-journeys.md +296 -296
- tapps_agents/experts/knowledge/user-experience/user-research.md +373 -373
- tapps_agents/experts/knowledge/user-experience/ux-principles.md +340 -340
- tapps_agents/experts/knowledge_freshness.py +321 -321
- tapps_agents/experts/knowledge_ingestion.py +438 -438
- tapps_agents/experts/knowledge_need_detector.py +93 -93
- tapps_agents/experts/knowledge_validator.py +382 -382
- tapps_agents/experts/observability.py +440 -440
- tapps_agents/experts/passive_notifier.py +238 -238
- tapps_agents/experts/proactive_orchestrator.py +32 -32
- tapps_agents/experts/rag_chunker.py +205 -205
- tapps_agents/experts/rag_embedder.py +152 -152
- tapps_agents/experts/rag_evaluation.py +299 -299
- tapps_agents/experts/rag_index.py +303 -303
- tapps_agents/experts/rag_metrics.py +293 -293
- tapps_agents/experts/rag_safety.py +263 -263
- tapps_agents/experts/report_generator.py +296 -296
- tapps_agents/experts/setup_wizard.py +441 -441
- tapps_agents/experts/simple_rag.py +431 -431
- tapps_agents/experts/vector_rag.py +354 -354
- tapps_agents/experts/weight_distributor.py +304 -304
- tapps_agents/health/__init__.py +24 -24
- tapps_agents/health/base.py +75 -75
- tapps_agents/health/checks/__init__.py +22 -22
- tapps_agents/health/checks/automation.py +127 -127
- tapps_agents/health/checks/context7_cache.py +210 -210
- tapps_agents/health/checks/environment.py +116 -116
- tapps_agents/health/checks/execution.py +170 -170
- tapps_agents/health/checks/knowledge_base.py +187 -187
- tapps_agents/health/checks/outcomes.py +324 -324
- tapps_agents/health/collector.py +280 -280
- tapps_agents/health/dashboard.py +137 -137
- tapps_agents/health/metrics.py +151 -151
- tapps_agents/health/orchestrator.py +271 -271
- tapps_agents/health/registry.py +166 -166
- tapps_agents/hooks/__init__.py +33 -33
- tapps_agents/hooks/config.py +140 -140
- tapps_agents/hooks/events.py +135 -135
- tapps_agents/hooks/executor.py +128 -128
- tapps_agents/hooks/manager.py +143 -143
- tapps_agents/integration/__init__.py +8 -8
- tapps_agents/integration/service_integrator.py +121 -121
- tapps_agents/integrations/__init__.py +10 -10
- tapps_agents/integrations/clawdbot.py +525 -525
- tapps_agents/integrations/memory_bridge.py +356 -356
- tapps_agents/mcp/__init__.py +18 -18
- tapps_agents/mcp/gateway.py +112 -112
- tapps_agents/mcp/servers/__init__.py +13 -13
- tapps_agents/mcp/servers/analysis.py +204 -204
- tapps_agents/mcp/servers/context7.py +198 -198
- tapps_agents/mcp/servers/filesystem.py +218 -218
- tapps_agents/mcp/servers/git.py +201 -201
- tapps_agents/mcp/tool_registry.py +115 -115
- tapps_agents/quality/__init__.py +54 -54
- tapps_agents/quality/coverage_analyzer.py +379 -379
- tapps_agents/quality/enforcement.py +82 -82
- tapps_agents/quality/gates/__init__.py +37 -37
- tapps_agents/quality/gates/approval_gate.py +255 -255
- tapps_agents/quality/gates/base.py +84 -84
- tapps_agents/quality/gates/exceptions.py +43 -43
- tapps_agents/quality/gates/policy_gate.py +195 -195
- tapps_agents/quality/gates/registry.py +239 -239
- tapps_agents/quality/gates/security_gate.py +156 -156
- tapps_agents/quality/quality_gates.py +369 -369
- tapps_agents/quality/secret_scanner.py +335 -335
- tapps_agents/session/__init__.py +19 -19
- tapps_agents/session/manager.py +256 -256
- tapps_agents/simple_mode/__init__.py +66 -66
- tapps_agents/simple_mode/agent_contracts.py +357 -357
- tapps_agents/simple_mode/beads_hooks.py +151 -151
- tapps_agents/simple_mode/code_snippet_handler.py +382 -382
- tapps_agents/simple_mode/documentation_manager.py +395 -395
- tapps_agents/simple_mode/documentation_reader.py +187 -187
- tapps_agents/simple_mode/file_inference.py +292 -292
- tapps_agents/simple_mode/framework_change_detector.py +268 -268
- tapps_agents/simple_mode/intent_parser.py +510 -510
- tapps_agents/simple_mode/learning_progression.py +358 -358
- tapps_agents/simple_mode/nl_handler.py +700 -700
- tapps_agents/simple_mode/onboarding.py +253 -253
- tapps_agents/simple_mode/orchestrators/__init__.py +38 -38
- tapps_agents/simple_mode/orchestrators/base.py +185 -185
- tapps_agents/simple_mode/orchestrators/breakdown_orchestrator.py +49 -49
- tapps_agents/simple_mode/orchestrators/brownfield_orchestrator.py +135 -135
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2700 -2667
- tapps_agents/simple_mode/orchestrators/deliverable_checklist.py +349 -349
- tapps_agents/simple_mode/orchestrators/enhance_orchestrator.py +53 -53
- tapps_agents/simple_mode/orchestrators/epic_orchestrator.py +122 -122
- tapps_agents/simple_mode/orchestrators/explore_orchestrator.py +184 -184
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
- tapps_agents/simple_mode/orchestrators/plan_analysis_orchestrator.py +206 -206
- tapps_agents/simple_mode/orchestrators/pr_orchestrator.py +237 -237
- tapps_agents/simple_mode/orchestrators/refactor_orchestrator.py +222 -222
- tapps_agents/simple_mode/orchestrators/requirements_tracer.py +262 -262
- tapps_agents/simple_mode/orchestrators/resume_orchestrator.py +210 -210
- tapps_agents/simple_mode/orchestrators/review_orchestrator.py +161 -161
- tapps_agents/simple_mode/orchestrators/test_orchestrator.py +82 -82
- tapps_agents/simple_mode/output_aggregator.py +340 -340
- tapps_agents/simple_mode/result_formatters.py +598 -598
- tapps_agents/simple_mode/step_dependencies.py +382 -382
- tapps_agents/simple_mode/step_results.py +276 -276
- tapps_agents/simple_mode/streaming.py +388 -388
- tapps_agents/simple_mode/variations.py +129 -129
- tapps_agents/simple_mode/visual_feedback.py +238 -238
- tapps_agents/simple_mode/zero_config.py +274 -274
- tapps_agents/suggestions/__init__.py +8 -8
- tapps_agents/suggestions/inline_suggester.py +52 -52
- tapps_agents/templates/__init__.py +8 -8
- tapps_agents/templates/microservice_generator.py +274 -274
- tapps_agents/utils/env_validator.py +291 -291
- tapps_agents/workflow/__init__.py +171 -171
- tapps_agents/workflow/acceptance_verifier.py +132 -132
- tapps_agents/workflow/agent_handlers/__init__.py +41 -41
- tapps_agents/workflow/agent_handlers/analyst_handler.py +75 -75
- tapps_agents/workflow/agent_handlers/architect_handler.py +107 -107
- tapps_agents/workflow/agent_handlers/base.py +84 -84
- tapps_agents/workflow/agent_handlers/debugger_handler.py +100 -100
- tapps_agents/workflow/agent_handlers/designer_handler.py +110 -110
- tapps_agents/workflow/agent_handlers/documenter_handler.py +94 -94
- tapps_agents/workflow/agent_handlers/implementer_handler.py +235 -235
- tapps_agents/workflow/agent_handlers/ops_handler.py +62 -62
- tapps_agents/workflow/agent_handlers/orchestrator_handler.py +43 -43
- tapps_agents/workflow/agent_handlers/planner_handler.py +98 -98
- tapps_agents/workflow/agent_handlers/registry.py +119 -119
- tapps_agents/workflow/agent_handlers/reviewer_handler.py +119 -119
- tapps_agents/workflow/agent_handlers/tester_handler.py +69 -69
- tapps_agents/workflow/analytics_accessor.py +337 -337
- tapps_agents/workflow/analytics_alerts.py +416 -416
- tapps_agents/workflow/analytics_dashboard_cursor.py +281 -281
- tapps_agents/workflow/analytics_dual_write.py +103 -103
- tapps_agents/workflow/analytics_integration.py +119 -119
- tapps_agents/workflow/analytics_query_parser.py +278 -278
- tapps_agents/workflow/analytics_visualizer.py +259 -259
- tapps_agents/workflow/artifact_helper.py +204 -204
- tapps_agents/workflow/audit_logger.py +263 -263
- tapps_agents/workflow/auto_execution_config.py +340 -340
- tapps_agents/workflow/auto_progression.py +586 -586
- tapps_agents/workflow/branch_cleanup.py +349 -349
- tapps_agents/workflow/checkpoint.py +256 -256
- tapps_agents/workflow/checkpoint_manager.py +178 -178
- tapps_agents/workflow/code_artifact.py +179 -179
- tapps_agents/workflow/common_enums.py +96 -96
- tapps_agents/workflow/confirmation_handler.py +130 -130
- tapps_agents/workflow/context_analyzer.py +222 -222
- tapps_agents/workflow/context_artifact.py +230 -230
- tapps_agents/workflow/cursor_chat.py +94 -94
- tapps_agents/workflow/cursor_executor.py +2337 -2196
- tapps_agents/workflow/cursor_skill_helper.py +516 -516
- tapps_agents/workflow/dependency_resolver.py +244 -244
- tapps_agents/workflow/design_artifact.py +156 -156
- tapps_agents/workflow/detector.py +751 -751
- tapps_agents/workflow/direct_execution_fallback.py +301 -301
- tapps_agents/workflow/docs_artifact.py +168 -168
- tapps_agents/workflow/enforcer.py +389 -389
- tapps_agents/workflow/enhancement_artifact.py +142 -142
- tapps_agents/workflow/error_recovery.py +806 -806
- tapps_agents/workflow/event_bus.py +183 -183
- tapps_agents/workflow/event_log.py +612 -612
- tapps_agents/workflow/events.py +63 -63
- tapps_agents/workflow/exceptions.py +43 -43
- tapps_agents/workflow/execution_graph.py +498 -498
- tapps_agents/workflow/execution_plan.py +126 -126
- tapps_agents/workflow/file_utils.py +186 -186
- tapps_agents/workflow/gate_evaluator.py +182 -182
- tapps_agents/workflow/gate_integration.py +200 -200
- tapps_agents/workflow/graph_visualizer.py +130 -130
- tapps_agents/workflow/health_checker.py +206 -206
- tapps_agents/workflow/logging_helper.py +243 -243
- tapps_agents/workflow/manifest.py +582 -582
- tapps_agents/workflow/marker_writer.py +250 -250
- tapps_agents/workflow/message_formatter.py +188 -188
- tapps_agents/workflow/messaging.py +325 -325
- tapps_agents/workflow/metadata_models.py +91 -91
- tapps_agents/workflow/metrics_integration.py +226 -226
- tapps_agents/workflow/migration_utils.py +116 -116
- tapps_agents/workflow/models.py +148 -111
- tapps_agents/workflow/nlp_config.py +198 -198
- tapps_agents/workflow/nlp_error_handler.py +207 -207
- tapps_agents/workflow/nlp_executor.py +163 -163
- tapps_agents/workflow/nlp_parser.py +528 -528
- tapps_agents/workflow/observability_dashboard.py +451 -451
- tapps_agents/workflow/observer.py +170 -170
- tapps_agents/workflow/ops_artifact.py +257 -257
- tapps_agents/workflow/output_passing.py +214 -214
- tapps_agents/workflow/parallel_executor.py +463 -463
- tapps_agents/workflow/planning_artifact.py +179 -179
- tapps_agents/workflow/preset_loader.py +285 -285
- tapps_agents/workflow/preset_recommender.py +270 -270
- tapps_agents/workflow/progress_logger.py +145 -145
- tapps_agents/workflow/progress_manager.py +303 -303
- tapps_agents/workflow/progress_monitor.py +186 -186
- tapps_agents/workflow/progress_updates.py +423 -423
- tapps_agents/workflow/quality_artifact.py +158 -158
- tapps_agents/workflow/quality_loopback.py +101 -101
- tapps_agents/workflow/recommender.py +387 -387
- tapps_agents/workflow/remediation_loop.py +166 -166
- tapps_agents/workflow/result_aggregator.py +300 -300
- tapps_agents/workflow/review_artifact.py +185 -185
- tapps_agents/workflow/schema_validator.py +522 -522
- tapps_agents/workflow/session_handoff.py +178 -178
- tapps_agents/workflow/skill_invoker.py +648 -648
- tapps_agents/workflow/state_manager.py +756 -756
- tapps_agents/workflow/state_persistence_config.py +331 -331
- tapps_agents/workflow/status_monitor.py +449 -449
- tapps_agents/workflow/step_checkpoint.py +314 -314
- tapps_agents/workflow/step_details.py +201 -201
- tapps_agents/workflow/story_models.py +147 -147
- tapps_agents/workflow/streaming.py +416 -416
- tapps_agents/workflow/suggestion_engine.py +552 -552
- tapps_agents/workflow/testing_artifact.py +186 -186
- tapps_agents/workflow/timeline.py +158 -158
- tapps_agents/workflow/token_integration.py +209 -209
- tapps_agents/workflow/validation.py +217 -217
- tapps_agents/workflow/visual_feedback.py +391 -391
- tapps_agents/workflow/workflow_chain.py +95 -95
- tapps_agents/workflow/workflow_summary.py +219 -219
- tapps_agents/workflow/worktree_manager.py +724 -724
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/METADATA +672 -672
- tapps_agents-3.6.0.dist-info/RECORD +758 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/licenses/LICENSE +22 -22
- tapps_agents/health/checks/outcomes.backup_20260204_064058.py +0 -324
- tapps_agents/health/checks/outcomes.backup_20260204_064256.py +0 -324
- tapps_agents/health/checks/outcomes.backup_20260204_064600.py +0 -324
- tapps_agents-3.5.40.dist-info/RECORD +0 -760
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/top_level.txt +0 -0
|
@@ -1,681 +1,681 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Async Non-Blocking Cache - Lock-free caching with in-memory LRU and background persistence.
|
|
3
|
-
|
|
4
|
-
2025 Architecture Pattern:
|
|
5
|
-
- In-memory LRU cache for instant reads (zero blocking)
|
|
6
|
-
- Background write queue for non-blocking writes
|
|
7
|
-
- Atomic file rename pattern for disk persistence (no file locking)
|
|
8
|
-
- Optimistic concurrency control (OCC) for consistency
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import asyncio
|
|
14
|
-
import json
|
|
15
|
-
import logging
|
|
16
|
-
import os
|
|
17
|
-
import tempfile
|
|
18
|
-
from collections import OrderedDict
|
|
19
|
-
from dataclasses import dataclass, field
|
|
20
|
-
from datetime import UTC, datetime
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from threading import Lock
|
|
23
|
-
from typing import Any, Callable
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
# Global async cache instance
|
|
28
|
-
_async_cache: AsyncCacheManager | None = None
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class CacheStats:
|
|
33
|
-
"""Cache statistics for monitoring."""
|
|
34
|
-
hits: int = 0
|
|
35
|
-
misses: int = 0
|
|
36
|
-
writes: int = 0
|
|
37
|
-
write_failures: int = 0
|
|
38
|
-
evictions: int = 0
|
|
39
|
-
background_writes_pending: int = 0
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def hit_rate(self) -> float:
|
|
43
|
-
"""Calculate cache hit rate."""
|
|
44
|
-
total = self.hits + self.misses
|
|
45
|
-
return self.hits / total if total > 0 else 0.0
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class WriteTask:
|
|
50
|
-
"""Background write task."""
|
|
51
|
-
library: str
|
|
52
|
-
topic: str
|
|
53
|
-
content: str
|
|
54
|
-
metadata: dict[str, Any]
|
|
55
|
-
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
56
|
-
retries: int = 0
|
|
57
|
-
max_retries: int = 3
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass
|
|
61
|
-
class AsyncCacheEntry:
|
|
62
|
-
"""Entry in the async cache."""
|
|
63
|
-
|
|
64
|
-
library: str
|
|
65
|
-
topic: str
|
|
66
|
-
content: str
|
|
67
|
-
context7_id: str | None = None
|
|
68
|
-
trust_score: float | None = None
|
|
69
|
-
snippet_count: int = 0
|
|
70
|
-
token_count: int = 0
|
|
71
|
-
cached_at: str | None = None
|
|
72
|
-
cache_hits: int = 0
|
|
73
|
-
|
|
74
|
-
def to_dict(self) -> dict[str, Any]:
|
|
75
|
-
"""Convert to dictionary."""
|
|
76
|
-
return {
|
|
77
|
-
"library": self.library,
|
|
78
|
-
"topic": self.topic,
|
|
79
|
-
"content": self.content,
|
|
80
|
-
"context7_id": self.context7_id,
|
|
81
|
-
"trust_score": self.trust_score,
|
|
82
|
-
"snippet_count": self.snippet_count,
|
|
83
|
-
"token_count": self.token_count,
|
|
84
|
-
"cached_at": self.cached_at,
|
|
85
|
-
"cache_hits": self.cache_hits,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class AsyncLRUCache:
|
|
90
|
-
"""
|
|
91
|
-
Thread-safe LRU cache with async background persistence.
|
|
92
|
-
|
|
93
|
-
Features:
|
|
94
|
-
- O(1) read/write operations
|
|
95
|
-
- Configurable max size with LRU eviction
|
|
96
|
-
- Non-blocking background persistence
|
|
97
|
-
- Atomic file operations (no locking)
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
def __init__(
|
|
101
|
-
self,
|
|
102
|
-
max_size: int = 500,
|
|
103
|
-
persist_dir: Path | None = None,
|
|
104
|
-
auto_persist: bool = True,
|
|
105
|
-
persist_interval: float = 5.0,
|
|
106
|
-
):
|
|
107
|
-
"""
|
|
108
|
-
Initialize async LRU cache.
|
|
109
|
-
|
|
110
|
-
Args:
|
|
111
|
-
max_size: Maximum number of entries in memory
|
|
112
|
-
persist_dir: Directory for disk persistence (optional)
|
|
113
|
-
auto_persist: Whether to auto-persist to disk
|
|
114
|
-
persist_interval: Seconds between background persist runs
|
|
115
|
-
"""
|
|
116
|
-
self._cache: OrderedDict[str, dict[str, Any]] = OrderedDict()
|
|
117
|
-
self._lock = Lock() # Thread lock for cache operations
|
|
118
|
-
self._max_size = max_size
|
|
119
|
-
self._persist_dir = persist_dir
|
|
120
|
-
self._auto_persist = auto_persist
|
|
121
|
-
self._persist_interval = persist_interval
|
|
122
|
-
self._stats = CacheStats()
|
|
123
|
-
|
|
124
|
-
# Background write queue
|
|
125
|
-
self._write_queue: asyncio.Queue[WriteTask] = asyncio.Queue()
|
|
126
|
-
self._persist_task: asyncio.Task | None = None
|
|
127
|
-
self._shutdown = False
|
|
128
|
-
|
|
129
|
-
# Load from disk on init if persist_dir exists
|
|
130
|
-
if persist_dir and persist_dir.exists():
|
|
131
|
-
self._load_from_disk()
|
|
132
|
-
|
|
133
|
-
def _make_key(self, library: str, topic: str) -> str:
|
|
134
|
-
"""Create cache key from library and topic."""
|
|
135
|
-
return f"{library}::{topic}"
|
|
136
|
-
|
|
137
|
-
def _parse_key(self, key: str) -> tuple[str, str]:
|
|
138
|
-
"""Parse cache key into library and topic."""
|
|
139
|
-
parts = key.split("::", 1)
|
|
140
|
-
return parts[0], parts[1] if len(parts) > 1 else "overview"
|
|
141
|
-
|
|
142
|
-
def get(self, library: str, topic: str = "overview") -> dict[str, Any] | None:
|
|
143
|
-
"""
|
|
144
|
-
Get entry from cache (instant, non-blocking).
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
library: Library name
|
|
148
|
-
topic: Topic name
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Cache entry dict or None if not found
|
|
152
|
-
"""
|
|
153
|
-
key = self._make_key(library, topic)
|
|
154
|
-
|
|
155
|
-
with self._lock:
|
|
156
|
-
if key in self._cache:
|
|
157
|
-
# Move to end (most recently used)
|
|
158
|
-
self._cache.move_to_end(key)
|
|
159
|
-
self._stats.hits += 1
|
|
160
|
-
entry = self._cache[key].copy()
|
|
161
|
-
entry["cache_hits"] = entry.get("cache_hits", 0) + 1
|
|
162
|
-
return entry
|
|
163
|
-
else:
|
|
164
|
-
self._stats.misses += 1
|
|
165
|
-
return None
|
|
166
|
-
|
|
167
|
-
def put(
|
|
168
|
-
self,
|
|
169
|
-
library: str,
|
|
170
|
-
topic: str,
|
|
171
|
-
content: str,
|
|
172
|
-
metadata: dict[str, Any] | None = None,
|
|
173
|
-
persist: bool = True,
|
|
174
|
-
) -> None:
|
|
175
|
-
"""
|
|
176
|
-
Put entry in cache (instant, non-blocking).
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
library: Library name
|
|
180
|
-
topic: Topic name
|
|
181
|
-
content: Documentation content
|
|
182
|
-
metadata: Optional metadata dict
|
|
183
|
-
persist: Whether to queue for disk persistence
|
|
184
|
-
"""
|
|
185
|
-
key = self._make_key(library, topic)
|
|
186
|
-
entry = {
|
|
187
|
-
"library": library,
|
|
188
|
-
"topic": topic,
|
|
189
|
-
"content": content,
|
|
190
|
-
"cached_at": datetime.now(UTC).isoformat() + "Z",
|
|
191
|
-
"cache_hits": 0,
|
|
192
|
-
**(metadata or {}),
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
with self._lock:
|
|
196
|
-
# Add/update entry
|
|
197
|
-
self._cache[key] = entry
|
|
198
|
-
self._cache.move_to_end(key)
|
|
199
|
-
self._stats.writes += 1
|
|
200
|
-
|
|
201
|
-
# Evict oldest if over capacity
|
|
202
|
-
while len(self._cache) > self._max_size:
|
|
203
|
-
oldest_key, _ = self._cache.popitem(last=False)
|
|
204
|
-
self._stats.evictions += 1
|
|
205
|
-
logger.debug(f"Evicted cache entry: {oldest_key}")
|
|
206
|
-
|
|
207
|
-
# Queue for background persistence (non-blocking)
|
|
208
|
-
if persist and self._persist_dir:
|
|
209
|
-
try:
|
|
210
|
-
task = WriteTask(
|
|
211
|
-
library=library,
|
|
212
|
-
topic=topic,
|
|
213
|
-
content=content,
|
|
214
|
-
metadata=metadata or {},
|
|
215
|
-
)
|
|
216
|
-
self._write_queue.put_nowait(task)
|
|
217
|
-
self._stats.background_writes_pending += 1
|
|
218
|
-
except asyncio.QueueFull:
|
|
219
|
-
logger.warning("Write queue full, skipping disk persistence")
|
|
220
|
-
|
|
221
|
-
def delete(self, library: str, topic: str | None = None) -> bool:
|
|
222
|
-
"""
|
|
223
|
-
Delete entry or all entries for a library.
|
|
224
|
-
|
|
225
|
-
Args:
|
|
226
|
-
library: Library name
|
|
227
|
-
topic: Topic name (if None, deletes all topics for library)
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
True if any entries were deleted
|
|
231
|
-
"""
|
|
232
|
-
deleted = False
|
|
233
|
-
|
|
234
|
-
with self._lock:
|
|
235
|
-
if topic:
|
|
236
|
-
key = self._make_key(library, topic)
|
|
237
|
-
if key in self._cache:
|
|
238
|
-
del self._cache[key]
|
|
239
|
-
deleted = True
|
|
240
|
-
else:
|
|
241
|
-
# Delete all topics for library
|
|
242
|
-
keys_to_delete = [k for k in self._cache if k.startswith(f"{library}::")]
|
|
243
|
-
for key in keys_to_delete:
|
|
244
|
-
del self._cache[key]
|
|
245
|
-
deleted = True
|
|
246
|
-
|
|
247
|
-
return deleted
|
|
248
|
-
|
|
249
|
-
def clear(self) -> None:
|
|
250
|
-
"""Clear all cache entries."""
|
|
251
|
-
with self._lock:
|
|
252
|
-
self._cache.clear()
|
|
253
|
-
|
|
254
|
-
def keys(self) -> list[tuple[str, str]]:
|
|
255
|
-
"""Get all cache keys as (library, topic) tuples."""
|
|
256
|
-
with self._lock:
|
|
257
|
-
return [self._parse_key(k) for k in self._cache.keys()]
|
|
258
|
-
|
|
259
|
-
def size(self) -> int:
|
|
260
|
-
"""Get current cache size."""
|
|
261
|
-
with self._lock:
|
|
262
|
-
return len(self._cache)
|
|
263
|
-
|
|
264
|
-
@property
|
|
265
|
-
def stats(self) -> CacheStats:
|
|
266
|
-
"""Get cache statistics."""
|
|
267
|
-
return self._stats
|
|
268
|
-
|
|
269
|
-
# --- Disk Persistence (Background) ---
|
|
270
|
-
|
|
271
|
-
def _get_entry_path(self, library: str, topic: str) -> Path:
|
|
272
|
-
"""Get disk path for cache entry."""
|
|
273
|
-
if not self._persist_dir:
|
|
274
|
-
raise ValueError("No persist_dir configured")
|
|
275
|
-
safe_library = library.replace("/", "_").replace("\\", "_")
|
|
276
|
-
safe_topic = topic.replace("/", "_").replace("\\", "_")
|
|
277
|
-
return self._persist_dir / safe_library / f"{safe_topic}.json"
|
|
278
|
-
|
|
279
|
-
def _load_from_disk(self) -> None:
|
|
280
|
-
"""Load cache entries from disk (called on init)."""
|
|
281
|
-
if not self._persist_dir or not self._persist_dir.exists():
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
count = 0
|
|
285
|
-
for lib_dir in self._persist_dir.iterdir():
|
|
286
|
-
if not lib_dir.is_dir() or lib_dir.name.startswith("."):
|
|
287
|
-
continue
|
|
288
|
-
|
|
289
|
-
for entry_file in lib_dir.glob("*.json"):
|
|
290
|
-
try:
|
|
291
|
-
data = json.loads(entry_file.read_text(encoding="utf-8"))
|
|
292
|
-
library = data.get("library", lib_dir.name)
|
|
293
|
-
topic = data.get("topic", entry_file.stem)
|
|
294
|
-
key = self._make_key(library, topic)
|
|
295
|
-
|
|
296
|
-
with self._lock:
|
|
297
|
-
self._cache[key] = data
|
|
298
|
-
count += 1
|
|
299
|
-
|
|
300
|
-
# Respect max size during load
|
|
301
|
-
if len(self._cache) >= self._max_size:
|
|
302
|
-
break
|
|
303
|
-
except Exception as e:
|
|
304
|
-
logger.debug(f"Failed to load cache entry {entry_file}: {e}")
|
|
305
|
-
|
|
306
|
-
if len(self._cache) >= self._max_size:
|
|
307
|
-
break
|
|
308
|
-
|
|
309
|
-
logger.debug(f"Loaded {count} cache entries from disk")
|
|
310
|
-
|
|
311
|
-
def _persist_entry_atomic(self, task: WriteTask) -> bool:
|
|
312
|
-
"""
|
|
313
|
-
Persist single entry to disk atomically (no locking).
|
|
314
|
-
|
|
315
|
-
Uses atomic rename pattern:
|
|
316
|
-
1. Write to temp file
|
|
317
|
-
2. Rename to target (atomic on most filesystems)
|
|
318
|
-
"""
|
|
319
|
-
if not self._persist_dir:
|
|
320
|
-
return False
|
|
321
|
-
|
|
322
|
-
try:
|
|
323
|
-
entry_path = self._get_entry_path(task.library, task.topic)
|
|
324
|
-
entry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
-
|
|
326
|
-
entry_data = {
|
|
327
|
-
"library": task.library,
|
|
328
|
-
"topic": task.topic,
|
|
329
|
-
"content": task.content,
|
|
330
|
-
"cached_at": task.timestamp.isoformat() + "Z",
|
|
331
|
-
"cache_hits": 0,
|
|
332
|
-
**task.metadata,
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
# Write to temp file first
|
|
336
|
-
fd, temp_path = tempfile.mkstemp(
|
|
337
|
-
suffix=".json",
|
|
338
|
-
prefix="cache_",
|
|
339
|
-
dir=entry_path.parent,
|
|
340
|
-
)
|
|
341
|
-
try:
|
|
342
|
-
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
343
|
-
json.dump(entry_data, f, indent=2)
|
|
344
|
-
|
|
345
|
-
# Atomic rename (overwrites existing)
|
|
346
|
-
os.replace(temp_path, entry_path)
|
|
347
|
-
return True
|
|
348
|
-
except Exception:
|
|
349
|
-
# Clean up temp file on failure
|
|
350
|
-
try:
|
|
351
|
-
os.unlink(temp_path)
|
|
352
|
-
except Exception:
|
|
353
|
-
pass
|
|
354
|
-
raise
|
|
355
|
-
|
|
356
|
-
except Exception as e:
|
|
357
|
-
logger.debug(f"Failed to persist {task.library}/{task.topic}: {e}")
|
|
358
|
-
return False
|
|
359
|
-
|
|
360
|
-
async def _background_persist_loop(self) -> None:
|
|
361
|
-
"""Background loop that processes write queue."""
|
|
362
|
-
while not self._shutdown:
|
|
363
|
-
try:
|
|
364
|
-
# Wait for write task with timeout
|
|
365
|
-
try:
|
|
366
|
-
task = await asyncio.wait_for(
|
|
367
|
-
self._write_queue.get(),
|
|
368
|
-
timeout=self._persist_interval
|
|
369
|
-
)
|
|
370
|
-
except asyncio.TimeoutError:
|
|
371
|
-
continue
|
|
372
|
-
|
|
373
|
-
# Process write task
|
|
374
|
-
self._stats.background_writes_pending -= 1
|
|
375
|
-
success = self._persist_entry_atomic(task)
|
|
376
|
-
|
|
377
|
-
if not success and task.retries < task.max_retries:
|
|
378
|
-
# Retry with backoff
|
|
379
|
-
task.retries += 1
|
|
380
|
-
await asyncio.sleep(0.1 * task.retries)
|
|
381
|
-
self._write_queue.put_nowait(task)
|
|
382
|
-
self._stats.background_writes_pending += 1
|
|
383
|
-
elif not success:
|
|
384
|
-
self._stats.write_failures += 1
|
|
385
|
-
|
|
386
|
-
except asyncio.CancelledError:
|
|
387
|
-
break
|
|
388
|
-
except Exception as e:
|
|
389
|
-
logger.warning(f"Background persist error: {e}")
|
|
390
|
-
await asyncio.sleep(1.0)
|
|
391
|
-
|
|
392
|
-
async def start_background_persist(self) -> None:
|
|
393
|
-
"""Start background persistence task."""
|
|
394
|
-
if self._persist_task is None or self._persist_task.done():
|
|
395
|
-
self._shutdown = False
|
|
396
|
-
self._persist_task = asyncio.create_task(self._background_persist_loop())
|
|
397
|
-
logger.debug("Started background persist task")
|
|
398
|
-
|
|
399
|
-
async def stop_background_persist(self, flush: bool = True) -> None:
|
|
400
|
-
"""
|
|
401
|
-
Stop background persistence task.
|
|
402
|
-
|
|
403
|
-
Args:
|
|
404
|
-
flush: Whether to flush remaining writes before stopping
|
|
405
|
-
"""
|
|
406
|
-
self._shutdown = True
|
|
407
|
-
|
|
408
|
-
if flush:
|
|
409
|
-
# Flush remaining writes
|
|
410
|
-
while not self._write_queue.empty():
|
|
411
|
-
try:
|
|
412
|
-
task = self._write_queue.get_nowait()
|
|
413
|
-
self._stats.background_writes_pending -= 1
|
|
414
|
-
self._persist_entry_atomic(task)
|
|
415
|
-
except asyncio.QueueEmpty:
|
|
416
|
-
break
|
|
417
|
-
|
|
418
|
-
if self._persist_task and not self._persist_task.done():
|
|
419
|
-
self._persist_task.cancel()
|
|
420
|
-
try:
|
|
421
|
-
await self._persist_task
|
|
422
|
-
except asyncio.CancelledError:
|
|
423
|
-
pass
|
|
424
|
-
self._persist_task = None
|
|
425
|
-
logger.debug("Stopped background persist task")
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
class CircuitBreaker:
|
|
429
|
-
"""
|
|
430
|
-
Circuit breaker pattern for external service calls.
|
|
431
|
-
|
|
432
|
-
States:
|
|
433
|
-
- CLOSED: Normal operation, requests pass through
|
|
434
|
-
- OPEN: Service unavailable, requests fail fast
|
|
435
|
-
- HALF_OPEN: Testing if service recovered
|
|
436
|
-
"""
|
|
437
|
-
|
|
438
|
-
CLOSED = "closed"
|
|
439
|
-
OPEN = "open"
|
|
440
|
-
HALF_OPEN = "half_open"
|
|
441
|
-
|
|
442
|
-
def __init__(
|
|
443
|
-
self,
|
|
444
|
-
name: str,
|
|
445
|
-
failure_threshold: int = 3,
|
|
446
|
-
recovery_timeout: float = 30.0,
|
|
447
|
-
half_open_max_calls: int = 1,
|
|
448
|
-
):
|
|
449
|
-
"""
|
|
450
|
-
Initialize circuit breaker.
|
|
451
|
-
|
|
452
|
-
Args:
|
|
453
|
-
name: Circuit breaker name (for logging)
|
|
454
|
-
failure_threshold: Failures before opening circuit
|
|
455
|
-
recovery_timeout: Seconds before trying again
|
|
456
|
-
half_open_max_calls: Max calls in half-open state
|
|
457
|
-
"""
|
|
458
|
-
self.name = name
|
|
459
|
-
self.failure_threshold = failure_threshold
|
|
460
|
-
self.recovery_timeout = recovery_timeout
|
|
461
|
-
self.half_open_max_calls = half_open_max_calls
|
|
462
|
-
|
|
463
|
-
self._state = self.CLOSED
|
|
464
|
-
self._failure_count = 0
|
|
465
|
-
self._last_failure_time: datetime | None = None
|
|
466
|
-
self._half_open_calls = 0
|
|
467
|
-
self._lock = Lock()
|
|
468
|
-
|
|
469
|
-
@property
|
|
470
|
-
def state(self) -> str:
|
|
471
|
-
"""Get current state."""
|
|
472
|
-
with self._lock:
|
|
473
|
-
return self._state
|
|
474
|
-
|
|
475
|
-
def can_execute(self) -> bool:
|
|
476
|
-
"""Check if request can be executed."""
|
|
477
|
-
with self._lock:
|
|
478
|
-
if self._state == self.CLOSED:
|
|
479
|
-
return True
|
|
480
|
-
|
|
481
|
-
if self._state == self.OPEN:
|
|
482
|
-
# Check if recovery timeout has passed
|
|
483
|
-
if self._last_failure_time:
|
|
484
|
-
elapsed = (datetime.now(UTC) - self._last_failure_time).total_seconds()
|
|
485
|
-
if elapsed >= self.recovery_timeout:
|
|
486
|
-
self._state = self.HALF_OPEN
|
|
487
|
-
self._half_open_calls = 0
|
|
488
|
-
logger.info(f"Circuit breaker '{self.name}' transitioning to HALF_OPEN")
|
|
489
|
-
return True
|
|
490
|
-
return False
|
|
491
|
-
|
|
492
|
-
if self._state == self.HALF_OPEN:
|
|
493
|
-
return self._half_open_calls < self.half_open_max_calls
|
|
494
|
-
|
|
495
|
-
return False
|
|
496
|
-
|
|
497
|
-
def record_success(self) -> None:
|
|
498
|
-
"""Record successful execution."""
|
|
499
|
-
with self._lock:
|
|
500
|
-
if self._state == self.HALF_OPEN:
|
|
501
|
-
# Service recovered, close circuit
|
|
502
|
-
self._state = self.CLOSED
|
|
503
|
-
self._failure_count = 0
|
|
504
|
-
self._half_open_calls = 0
|
|
505
|
-
logger.info(f"Circuit breaker '{self.name}' CLOSED (service recovered)")
|
|
506
|
-
elif self._state == self.CLOSED:
|
|
507
|
-
# Reset failure count on success
|
|
508
|
-
self._failure_count = 0
|
|
509
|
-
|
|
510
|
-
def record_failure(self) -> None:
|
|
511
|
-
"""Record failed execution."""
|
|
512
|
-
with self._lock:
|
|
513
|
-
self._failure_count += 1
|
|
514
|
-
self._last_failure_time = datetime.now(UTC)
|
|
515
|
-
|
|
516
|
-
if self._state == self.HALF_OPEN:
|
|
517
|
-
# Service still failing, reopen circuit
|
|
518
|
-
self._state = self.OPEN
|
|
519
|
-
logger.warning(f"Circuit breaker '{self.name}' REOPENED (service still failing)")
|
|
520
|
-
|
|
521
|
-
elif self._state == self.CLOSED:
|
|
522
|
-
if self._failure_count >= self.failure_threshold:
|
|
523
|
-
self._state = self.OPEN
|
|
524
|
-
logger.warning(
|
|
525
|
-
f"Circuit breaker '{self.name}' OPENED after {self._failure_count} failures"
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
class ParallelResolver:
|
|
530
|
-
"""
|
|
531
|
-
Parallel library resolution with bounded concurrency and circuit breaker.
|
|
532
|
-
|
|
533
|
-
Features:
|
|
534
|
-
- Bounded parallelism (configurable max concurrent)
|
|
535
|
-
- Per-library timeout
|
|
536
|
-
- Circuit breaker for external service
|
|
537
|
-
- Graceful degradation
|
|
538
|
-
"""
|
|
539
|
-
|
|
540
|
-
def __init__(
|
|
541
|
-
self,
|
|
542
|
-
max_concurrent: int = 5,
|
|
543
|
-
per_library_timeout: float = 5.0,
|
|
544
|
-
circuit_breaker: CircuitBreaker | None = None,
|
|
545
|
-
):
|
|
546
|
-
"""
|
|
547
|
-
Initialize parallel resolver.
|
|
548
|
-
|
|
549
|
-
Args:
|
|
550
|
-
max_concurrent: Maximum concurrent resolutions
|
|
551
|
-
per_library_timeout: Timeout per library (seconds)
|
|
552
|
-
circuit_breaker: Optional circuit breaker for external calls
|
|
553
|
-
"""
|
|
554
|
-
self.max_concurrent = max_concurrent
|
|
555
|
-
self.per_library_timeout = per_library_timeout
|
|
556
|
-
self.circuit_breaker = circuit_breaker or CircuitBreaker(
|
|
557
|
-
name="context7_resolver",
|
|
558
|
-
failure_threshold=3,
|
|
559
|
-
recovery_timeout=30.0,
|
|
560
|
-
)
|
|
561
|
-
self._semaphore: asyncio.Semaphore | None = None
|
|
562
|
-
|
|
563
|
-
async def resolve_libraries(
|
|
564
|
-
self,
|
|
565
|
-
libraries: list[str],
|
|
566
|
-
resolve_func: Callable[[str], Any],
|
|
567
|
-
on_success: Callable[[str, Any], None] | None = None,
|
|
568
|
-
on_failure: Callable[[str, Exception], None] | None = None,
|
|
569
|
-
) -> dict[str, Any]:
|
|
570
|
-
"""
|
|
571
|
-
Resolve multiple libraries in parallel with bounded concurrency.
|
|
572
|
-
|
|
573
|
-
Args:
|
|
574
|
-
libraries: List of library names to resolve
|
|
575
|
-
resolve_func: Async function to resolve single library
|
|
576
|
-
on_success: Optional callback on success
|
|
577
|
-
on_failure: Optional callback on failure
|
|
578
|
-
|
|
579
|
-
Returns:
|
|
580
|
-
Dict mapping library names to results (or None if failed)
|
|
581
|
-
"""
|
|
582
|
-
if not libraries:
|
|
583
|
-
return {}
|
|
584
|
-
|
|
585
|
-
# Create semaphore for bounded concurrency
|
|
586
|
-
if self._semaphore is None:
|
|
587
|
-
self._semaphore = asyncio.Semaphore(self.max_concurrent)
|
|
588
|
-
|
|
589
|
-
async def resolve_with_timeout(library: str) -> tuple[str, Any]:
|
|
590
|
-
"""Resolve single library with timeout and circuit breaker."""
|
|
591
|
-
# Check circuit breaker
|
|
592
|
-
if not self.circuit_breaker.can_execute():
|
|
593
|
-
logger.debug(f"Circuit breaker open, skipping {library}")
|
|
594
|
-
if on_failure:
|
|
595
|
-
on_failure(library, Exception("Circuit breaker open"))
|
|
596
|
-
return library, None
|
|
597
|
-
|
|
598
|
-
async with self._semaphore:
|
|
599
|
-
try:
|
|
600
|
-
result = await asyncio.wait_for(
|
|
601
|
-
resolve_func(library),
|
|
602
|
-
timeout=self.per_library_timeout,
|
|
603
|
-
)
|
|
604
|
-
self.circuit_breaker.record_success()
|
|
605
|
-
if on_success:
|
|
606
|
-
on_success(library, result)
|
|
607
|
-
return library, result
|
|
608
|
-
|
|
609
|
-
except asyncio.TimeoutError:
|
|
610
|
-
logger.debug(f"Timeout resolving {library} after {self.per_library_timeout}s")
|
|
611
|
-
self.circuit_breaker.record_failure()
|
|
612
|
-
if on_failure:
|
|
613
|
-
on_failure(library, asyncio.TimeoutError(f"Timeout after {self.per_library_timeout}s"))
|
|
614
|
-
return library, None
|
|
615
|
-
|
|
616
|
-
except Exception as e:
|
|
617
|
-
logger.debug(f"Failed to resolve {library}: {e}")
|
|
618
|
-
self.circuit_breaker.record_failure()
|
|
619
|
-
if on_failure:
|
|
620
|
-
on_failure(library, e)
|
|
621
|
-
return library, None
|
|
622
|
-
|
|
623
|
-
# Run all resolutions in parallel (bounded by semaphore)
|
|
624
|
-
tasks = [resolve_with_timeout(lib) for lib in libraries]
|
|
625
|
-
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
626
|
-
|
|
627
|
-
# Build result dict
|
|
628
|
-
result_dict: dict[str, Any] = {}
|
|
629
|
-
for item in results:
|
|
630
|
-
if isinstance(item, tuple) and len(item) == 2:
|
|
631
|
-
lib, result = item
|
|
632
|
-
result_dict[lib] = result
|
|
633
|
-
elif isinstance(item, Exception):
|
|
634
|
-
logger.warning(f"Unexpected exception in parallel resolve: {item}")
|
|
635
|
-
|
|
636
|
-
return result_dict
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
# Alias for compatibility with existing imports
|
|
640
|
-
AsyncCacheManager = AsyncLRUCache
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def init_async_cache(
|
|
644
|
-
persist_dir: Path | None = None,
|
|
645
|
-
max_size: int = 500,
|
|
646
|
-
) -> AsyncCacheManager:
|
|
647
|
-
"""
|
|
648
|
-
Initialize the global async cache.
|
|
649
|
-
|
|
650
|
-
Args:
|
|
651
|
-
persist_dir: Directory for disk persistence
|
|
652
|
-
max_size: Maximum entries in memory
|
|
653
|
-
|
|
654
|
-
Returns:
|
|
655
|
-
Initialized AsyncCacheManager instance
|
|
656
|
-
"""
|
|
657
|
-
global _async_cache
|
|
658
|
-
|
|
659
|
-
if persist_dir is None:
|
|
660
|
-
# Use project root detection instead of current working directory
|
|
661
|
-
from ...core.path_validator import PathValidator
|
|
662
|
-
validator = PathValidator()
|
|
663
|
-
persist_dir = validator.project_root / ".tapps-agents" / "kb" / "async-cache"
|
|
664
|
-
|
|
665
|
-
_async_cache = AsyncCacheManager(
|
|
666
|
-
max_size=max_size,
|
|
667
|
-
persist_dir=persist_dir,
|
|
668
|
-
auto_persist=True,
|
|
669
|
-
)
|
|
670
|
-
|
|
671
|
-
return _async_cache
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
def get_async_cache() -> AsyncCacheManager | None:
|
|
675
|
-
"""
|
|
676
|
-
Get the global async cache instance.
|
|
677
|
-
|
|
678
|
-
Returns:
|
|
679
|
-
AsyncCacheManager instance or None if not initialized
|
|
680
|
-
"""
|
|
681
|
-
return _async_cache
|
|
1
|
+
"""
|
|
2
|
+
Async Non-Blocking Cache - Lock-free caching with in-memory LRU and background persistence.
|
|
3
|
+
|
|
4
|
+
2025 Architecture Pattern:
|
|
5
|
+
- In-memory LRU cache for instant reads (zero blocking)
|
|
6
|
+
- Background write queue for non-blocking writes
|
|
7
|
+
- Atomic file rename pattern for disk persistence (no file locking)
|
|
8
|
+
- Optimistic concurrency control (OCC) for consistency
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import UTC, datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from threading import Lock
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Global async cache instance
|
|
28
|
+
_async_cache: AsyncCacheManager | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CacheStats:
|
|
33
|
+
"""Cache statistics for monitoring."""
|
|
34
|
+
hits: int = 0
|
|
35
|
+
misses: int = 0
|
|
36
|
+
writes: int = 0
|
|
37
|
+
write_failures: int = 0
|
|
38
|
+
evictions: int = 0
|
|
39
|
+
background_writes_pending: int = 0
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def hit_rate(self) -> float:
|
|
43
|
+
"""Calculate cache hit rate."""
|
|
44
|
+
total = self.hits + self.misses
|
|
45
|
+
return self.hits / total if total > 0 else 0.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WriteTask:
|
|
50
|
+
"""Background write task."""
|
|
51
|
+
library: str
|
|
52
|
+
topic: str
|
|
53
|
+
content: str
|
|
54
|
+
metadata: dict[str, Any]
|
|
55
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
56
|
+
retries: int = 0
|
|
57
|
+
max_retries: int = 3
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class AsyncCacheEntry:
|
|
62
|
+
"""Entry in the async cache."""
|
|
63
|
+
|
|
64
|
+
library: str
|
|
65
|
+
topic: str
|
|
66
|
+
content: str
|
|
67
|
+
context7_id: str | None = None
|
|
68
|
+
trust_score: float | None = None
|
|
69
|
+
snippet_count: int = 0
|
|
70
|
+
token_count: int = 0
|
|
71
|
+
cached_at: str | None = None
|
|
72
|
+
cache_hits: int = 0
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
"""Convert to dictionary."""
|
|
76
|
+
return {
|
|
77
|
+
"library": self.library,
|
|
78
|
+
"topic": self.topic,
|
|
79
|
+
"content": self.content,
|
|
80
|
+
"context7_id": self.context7_id,
|
|
81
|
+
"trust_score": self.trust_score,
|
|
82
|
+
"snippet_count": self.snippet_count,
|
|
83
|
+
"token_count": self.token_count,
|
|
84
|
+
"cached_at": self.cached_at,
|
|
85
|
+
"cache_hits": self.cache_hits,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AsyncLRUCache:
|
|
90
|
+
"""
|
|
91
|
+
Thread-safe LRU cache with async background persistence.
|
|
92
|
+
|
|
93
|
+
Features:
|
|
94
|
+
- O(1) read/write operations
|
|
95
|
+
- Configurable max size with LRU eviction
|
|
96
|
+
- Non-blocking background persistence
|
|
97
|
+
- Atomic file operations (no locking)
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
max_size: int = 500,
|
|
103
|
+
persist_dir: Path | None = None,
|
|
104
|
+
auto_persist: bool = True,
|
|
105
|
+
persist_interval: float = 5.0,
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
Initialize async LRU cache.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
max_size: Maximum number of entries in memory
|
|
112
|
+
persist_dir: Directory for disk persistence (optional)
|
|
113
|
+
auto_persist: Whether to auto-persist to disk
|
|
114
|
+
persist_interval: Seconds between background persist runs
|
|
115
|
+
"""
|
|
116
|
+
self._cache: OrderedDict[str, dict[str, Any]] = OrderedDict()
|
|
117
|
+
self._lock = Lock() # Thread lock for cache operations
|
|
118
|
+
self._max_size = max_size
|
|
119
|
+
self._persist_dir = persist_dir
|
|
120
|
+
self._auto_persist = auto_persist
|
|
121
|
+
self._persist_interval = persist_interval
|
|
122
|
+
self._stats = CacheStats()
|
|
123
|
+
|
|
124
|
+
# Background write queue
|
|
125
|
+
self._write_queue: asyncio.Queue[WriteTask] = asyncio.Queue()
|
|
126
|
+
self._persist_task: asyncio.Task | None = None
|
|
127
|
+
self._shutdown = False
|
|
128
|
+
|
|
129
|
+
# Load from disk on init if persist_dir exists
|
|
130
|
+
if persist_dir and persist_dir.exists():
|
|
131
|
+
self._load_from_disk()
|
|
132
|
+
|
|
133
|
+
def _make_key(self, library: str, topic: str) -> str:
|
|
134
|
+
"""Create cache key from library and topic."""
|
|
135
|
+
return f"{library}::{topic}"
|
|
136
|
+
|
|
137
|
+
def _parse_key(self, key: str) -> tuple[str, str]:
|
|
138
|
+
"""Parse cache key into library and topic."""
|
|
139
|
+
parts = key.split("::", 1)
|
|
140
|
+
return parts[0], parts[1] if len(parts) > 1 else "overview"
|
|
141
|
+
|
|
142
|
+
def get(self, library: str, topic: str = "overview") -> dict[str, Any] | None:
|
|
143
|
+
"""
|
|
144
|
+
Get entry from cache (instant, non-blocking).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
library: Library name
|
|
148
|
+
topic: Topic name
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Cache entry dict or None if not found
|
|
152
|
+
"""
|
|
153
|
+
key = self._make_key(library, topic)
|
|
154
|
+
|
|
155
|
+
with self._lock:
|
|
156
|
+
if key in self._cache:
|
|
157
|
+
# Move to end (most recently used)
|
|
158
|
+
self._cache.move_to_end(key)
|
|
159
|
+
self._stats.hits += 1
|
|
160
|
+
entry = self._cache[key].copy()
|
|
161
|
+
entry["cache_hits"] = entry.get("cache_hits", 0) + 1
|
|
162
|
+
return entry
|
|
163
|
+
else:
|
|
164
|
+
self._stats.misses += 1
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def put(
|
|
168
|
+
self,
|
|
169
|
+
library: str,
|
|
170
|
+
topic: str,
|
|
171
|
+
content: str,
|
|
172
|
+
metadata: dict[str, Any] | None = None,
|
|
173
|
+
persist: bool = True,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Put entry in cache (instant, non-blocking).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
library: Library name
|
|
180
|
+
topic: Topic name
|
|
181
|
+
content: Documentation content
|
|
182
|
+
metadata: Optional metadata dict
|
|
183
|
+
persist: Whether to queue for disk persistence
|
|
184
|
+
"""
|
|
185
|
+
key = self._make_key(library, topic)
|
|
186
|
+
entry = {
|
|
187
|
+
"library": library,
|
|
188
|
+
"topic": topic,
|
|
189
|
+
"content": content,
|
|
190
|
+
"cached_at": datetime.now(UTC).isoformat() + "Z",
|
|
191
|
+
"cache_hits": 0,
|
|
192
|
+
**(metadata or {}),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
with self._lock:
|
|
196
|
+
# Add/update entry
|
|
197
|
+
self._cache[key] = entry
|
|
198
|
+
self._cache.move_to_end(key)
|
|
199
|
+
self._stats.writes += 1
|
|
200
|
+
|
|
201
|
+
# Evict oldest if over capacity
|
|
202
|
+
while len(self._cache) > self._max_size:
|
|
203
|
+
oldest_key, _ = self._cache.popitem(last=False)
|
|
204
|
+
self._stats.evictions += 1
|
|
205
|
+
logger.debug(f"Evicted cache entry: {oldest_key}")
|
|
206
|
+
|
|
207
|
+
# Queue for background persistence (non-blocking)
|
|
208
|
+
if persist and self._persist_dir:
|
|
209
|
+
try:
|
|
210
|
+
task = WriteTask(
|
|
211
|
+
library=library,
|
|
212
|
+
topic=topic,
|
|
213
|
+
content=content,
|
|
214
|
+
metadata=metadata or {},
|
|
215
|
+
)
|
|
216
|
+
self._write_queue.put_nowait(task)
|
|
217
|
+
self._stats.background_writes_pending += 1
|
|
218
|
+
except asyncio.QueueFull:
|
|
219
|
+
logger.warning("Write queue full, skipping disk persistence")
|
|
220
|
+
|
|
221
|
+
def delete(self, library: str, topic: str | None = None) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Delete entry or all entries for a library.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
library: Library name
|
|
227
|
+
topic: Topic name (if None, deletes all topics for library)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if any entries were deleted
|
|
231
|
+
"""
|
|
232
|
+
deleted = False
|
|
233
|
+
|
|
234
|
+
with self._lock:
|
|
235
|
+
if topic:
|
|
236
|
+
key = self._make_key(library, topic)
|
|
237
|
+
if key in self._cache:
|
|
238
|
+
del self._cache[key]
|
|
239
|
+
deleted = True
|
|
240
|
+
else:
|
|
241
|
+
# Delete all topics for library
|
|
242
|
+
keys_to_delete = [k for k in self._cache if k.startswith(f"{library}::")]
|
|
243
|
+
for key in keys_to_delete:
|
|
244
|
+
del self._cache[key]
|
|
245
|
+
deleted = True
|
|
246
|
+
|
|
247
|
+
return deleted
|
|
248
|
+
|
|
249
|
+
def clear(self) -> None:
|
|
250
|
+
"""Clear all cache entries."""
|
|
251
|
+
with self._lock:
|
|
252
|
+
self._cache.clear()
|
|
253
|
+
|
|
254
|
+
def keys(self) -> list[tuple[str, str]]:
|
|
255
|
+
"""Get all cache keys as (library, topic) tuples."""
|
|
256
|
+
with self._lock:
|
|
257
|
+
return [self._parse_key(k) for k in self._cache.keys()]
|
|
258
|
+
|
|
259
|
+
def size(self) -> int:
|
|
260
|
+
"""Get current cache size."""
|
|
261
|
+
with self._lock:
|
|
262
|
+
return len(self._cache)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def stats(self) -> CacheStats:
|
|
266
|
+
"""Get cache statistics."""
|
|
267
|
+
return self._stats
|
|
268
|
+
|
|
269
|
+
# --- Disk Persistence (Background) ---
|
|
270
|
+
|
|
271
|
+
def _get_entry_path(self, library: str, topic: str) -> Path:
|
|
272
|
+
"""Get disk path for cache entry."""
|
|
273
|
+
if not self._persist_dir:
|
|
274
|
+
raise ValueError("No persist_dir configured")
|
|
275
|
+
safe_library = library.replace("/", "_").replace("\\", "_")
|
|
276
|
+
safe_topic = topic.replace("/", "_").replace("\\", "_")
|
|
277
|
+
return self._persist_dir / safe_library / f"{safe_topic}.json"
|
|
278
|
+
|
|
279
|
+
def _load_from_disk(self) -> None:
|
|
280
|
+
"""Load cache entries from disk (called on init)."""
|
|
281
|
+
if not self._persist_dir or not self._persist_dir.exists():
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
count = 0
|
|
285
|
+
for lib_dir in self._persist_dir.iterdir():
|
|
286
|
+
if not lib_dir.is_dir() or lib_dir.name.startswith("."):
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
for entry_file in lib_dir.glob("*.json"):
|
|
290
|
+
try:
|
|
291
|
+
data = json.loads(entry_file.read_text(encoding="utf-8"))
|
|
292
|
+
library = data.get("library", lib_dir.name)
|
|
293
|
+
topic = data.get("topic", entry_file.stem)
|
|
294
|
+
key = self._make_key(library, topic)
|
|
295
|
+
|
|
296
|
+
with self._lock:
|
|
297
|
+
self._cache[key] = data
|
|
298
|
+
count += 1
|
|
299
|
+
|
|
300
|
+
# Respect max size during load
|
|
301
|
+
if len(self._cache) >= self._max_size:
|
|
302
|
+
break
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.debug(f"Failed to load cache entry {entry_file}: {e}")
|
|
305
|
+
|
|
306
|
+
if len(self._cache) >= self._max_size:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
logger.debug(f"Loaded {count} cache entries from disk")
|
|
310
|
+
|
|
311
|
+
def _persist_entry_atomic(self, task: WriteTask) -> bool:
|
|
312
|
+
"""
|
|
313
|
+
Persist single entry to disk atomically (no locking).
|
|
314
|
+
|
|
315
|
+
Uses atomic rename pattern:
|
|
316
|
+
1. Write to temp file
|
|
317
|
+
2. Rename to target (atomic on most filesystems)
|
|
318
|
+
"""
|
|
319
|
+
if not self._persist_dir:
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
entry_path = self._get_entry_path(task.library, task.topic)
|
|
324
|
+
entry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
|
|
326
|
+
entry_data = {
|
|
327
|
+
"library": task.library,
|
|
328
|
+
"topic": task.topic,
|
|
329
|
+
"content": task.content,
|
|
330
|
+
"cached_at": task.timestamp.isoformat() + "Z",
|
|
331
|
+
"cache_hits": 0,
|
|
332
|
+
**task.metadata,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Write to temp file first
|
|
336
|
+
fd, temp_path = tempfile.mkstemp(
|
|
337
|
+
suffix=".json",
|
|
338
|
+
prefix="cache_",
|
|
339
|
+
dir=entry_path.parent,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
343
|
+
json.dump(entry_data, f, indent=2)
|
|
344
|
+
|
|
345
|
+
# Atomic rename (overwrites existing)
|
|
346
|
+
os.replace(temp_path, entry_path)
|
|
347
|
+
return True
|
|
348
|
+
except Exception:
|
|
349
|
+
# Clean up temp file on failure
|
|
350
|
+
try:
|
|
351
|
+
os.unlink(temp_path)
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
raise
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
logger.debug(f"Failed to persist {task.library}/{task.topic}: {e}")
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
async def _background_persist_loop(self) -> None:
|
|
361
|
+
"""Background loop that processes write queue."""
|
|
362
|
+
while not self._shutdown:
|
|
363
|
+
try:
|
|
364
|
+
# Wait for write task with timeout
|
|
365
|
+
try:
|
|
366
|
+
task = await asyncio.wait_for(
|
|
367
|
+
self._write_queue.get(),
|
|
368
|
+
timeout=self._persist_interval
|
|
369
|
+
)
|
|
370
|
+
except asyncio.TimeoutError:
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# Process write task
|
|
374
|
+
self._stats.background_writes_pending -= 1
|
|
375
|
+
success = self._persist_entry_atomic(task)
|
|
376
|
+
|
|
377
|
+
if not success and task.retries < task.max_retries:
|
|
378
|
+
# Retry with backoff
|
|
379
|
+
task.retries += 1
|
|
380
|
+
await asyncio.sleep(0.1 * task.retries)
|
|
381
|
+
self._write_queue.put_nowait(task)
|
|
382
|
+
self._stats.background_writes_pending += 1
|
|
383
|
+
elif not success:
|
|
384
|
+
self._stats.write_failures += 1
|
|
385
|
+
|
|
386
|
+
except asyncio.CancelledError:
|
|
387
|
+
break
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.warning(f"Background persist error: {e}")
|
|
390
|
+
await asyncio.sleep(1.0)
|
|
391
|
+
|
|
392
|
+
async def start_background_persist(self) -> None:
|
|
393
|
+
"""Start background persistence task."""
|
|
394
|
+
if self._persist_task is None or self._persist_task.done():
|
|
395
|
+
self._shutdown = False
|
|
396
|
+
self._persist_task = asyncio.create_task(self._background_persist_loop())
|
|
397
|
+
logger.debug("Started background persist task")
|
|
398
|
+
|
|
399
|
+
async def stop_background_persist(self, flush: bool = True) -> None:
|
|
400
|
+
"""
|
|
401
|
+
Stop background persistence task.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
flush: Whether to flush remaining writes before stopping
|
|
405
|
+
"""
|
|
406
|
+
self._shutdown = True
|
|
407
|
+
|
|
408
|
+
if flush:
|
|
409
|
+
# Flush remaining writes
|
|
410
|
+
while not self._write_queue.empty():
|
|
411
|
+
try:
|
|
412
|
+
task = self._write_queue.get_nowait()
|
|
413
|
+
self._stats.background_writes_pending -= 1
|
|
414
|
+
self._persist_entry_atomic(task)
|
|
415
|
+
except asyncio.QueueEmpty:
|
|
416
|
+
break
|
|
417
|
+
|
|
418
|
+
if self._persist_task and not self._persist_task.done():
|
|
419
|
+
self._persist_task.cancel()
|
|
420
|
+
try:
|
|
421
|
+
await self._persist_task
|
|
422
|
+
except asyncio.CancelledError:
|
|
423
|
+
pass
|
|
424
|
+
self._persist_task = None
|
|
425
|
+
logger.debug("Stopped background persist task")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class CircuitBreaker:
|
|
429
|
+
"""
|
|
430
|
+
Circuit breaker pattern for external service calls.
|
|
431
|
+
|
|
432
|
+
States:
|
|
433
|
+
- CLOSED: Normal operation, requests pass through
|
|
434
|
+
- OPEN: Service unavailable, requests fail fast
|
|
435
|
+
- HALF_OPEN: Testing if service recovered
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
CLOSED = "closed"
|
|
439
|
+
OPEN = "open"
|
|
440
|
+
HALF_OPEN = "half_open"
|
|
441
|
+
|
|
442
|
+
def __init__(
|
|
443
|
+
self,
|
|
444
|
+
name: str,
|
|
445
|
+
failure_threshold: int = 3,
|
|
446
|
+
recovery_timeout: float = 30.0,
|
|
447
|
+
half_open_max_calls: int = 1,
|
|
448
|
+
):
|
|
449
|
+
"""
|
|
450
|
+
Initialize circuit breaker.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
name: Circuit breaker name (for logging)
|
|
454
|
+
failure_threshold: Failures before opening circuit
|
|
455
|
+
recovery_timeout: Seconds before trying again
|
|
456
|
+
half_open_max_calls: Max calls in half-open state
|
|
457
|
+
"""
|
|
458
|
+
self.name = name
|
|
459
|
+
self.failure_threshold = failure_threshold
|
|
460
|
+
self.recovery_timeout = recovery_timeout
|
|
461
|
+
self.half_open_max_calls = half_open_max_calls
|
|
462
|
+
|
|
463
|
+
self._state = self.CLOSED
|
|
464
|
+
self._failure_count = 0
|
|
465
|
+
self._last_failure_time: datetime | None = None
|
|
466
|
+
self._half_open_calls = 0
|
|
467
|
+
self._lock = Lock()
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def state(self) -> str:
|
|
471
|
+
"""Get current state."""
|
|
472
|
+
with self._lock:
|
|
473
|
+
return self._state
|
|
474
|
+
|
|
475
|
+
def can_execute(self) -> bool:
|
|
476
|
+
"""Check if request can be executed."""
|
|
477
|
+
with self._lock:
|
|
478
|
+
if self._state == self.CLOSED:
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
if self._state == self.OPEN:
|
|
482
|
+
# Check if recovery timeout has passed
|
|
483
|
+
if self._last_failure_time:
|
|
484
|
+
elapsed = (datetime.now(UTC) - self._last_failure_time).total_seconds()
|
|
485
|
+
if elapsed >= self.recovery_timeout:
|
|
486
|
+
self._state = self.HALF_OPEN
|
|
487
|
+
self._half_open_calls = 0
|
|
488
|
+
logger.info(f"Circuit breaker '{self.name}' transitioning to HALF_OPEN")
|
|
489
|
+
return True
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
if self._state == self.HALF_OPEN:
|
|
493
|
+
return self._half_open_calls < self.half_open_max_calls
|
|
494
|
+
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
def record_success(self) -> None:
|
|
498
|
+
"""Record successful execution."""
|
|
499
|
+
with self._lock:
|
|
500
|
+
if self._state == self.HALF_OPEN:
|
|
501
|
+
# Service recovered, close circuit
|
|
502
|
+
self._state = self.CLOSED
|
|
503
|
+
self._failure_count = 0
|
|
504
|
+
self._half_open_calls = 0
|
|
505
|
+
logger.info(f"Circuit breaker '{self.name}' CLOSED (service recovered)")
|
|
506
|
+
elif self._state == self.CLOSED:
|
|
507
|
+
# Reset failure count on success
|
|
508
|
+
self._failure_count = 0
|
|
509
|
+
|
|
510
|
+
def record_failure(self) -> None:
|
|
511
|
+
"""Record failed execution."""
|
|
512
|
+
with self._lock:
|
|
513
|
+
self._failure_count += 1
|
|
514
|
+
self._last_failure_time = datetime.now(UTC)
|
|
515
|
+
|
|
516
|
+
if self._state == self.HALF_OPEN:
|
|
517
|
+
# Service still failing, reopen circuit
|
|
518
|
+
self._state = self.OPEN
|
|
519
|
+
logger.warning(f"Circuit breaker '{self.name}' REOPENED (service still failing)")
|
|
520
|
+
|
|
521
|
+
elif self._state == self.CLOSED:
|
|
522
|
+
if self._failure_count >= self.failure_threshold:
|
|
523
|
+
self._state = self.OPEN
|
|
524
|
+
logger.warning(
|
|
525
|
+
f"Circuit breaker '{self.name}' OPENED after {self._failure_count} failures"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class ParallelResolver:
|
|
530
|
+
"""
|
|
531
|
+
Parallel library resolution with bounded concurrency and circuit breaker.
|
|
532
|
+
|
|
533
|
+
Features:
|
|
534
|
+
- Bounded parallelism (configurable max concurrent)
|
|
535
|
+
- Per-library timeout
|
|
536
|
+
- Circuit breaker for external service
|
|
537
|
+
- Graceful degradation
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
def __init__(
|
|
541
|
+
self,
|
|
542
|
+
max_concurrent: int = 5,
|
|
543
|
+
per_library_timeout: float = 5.0,
|
|
544
|
+
circuit_breaker: CircuitBreaker | None = None,
|
|
545
|
+
):
|
|
546
|
+
"""
|
|
547
|
+
Initialize parallel resolver.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
max_concurrent: Maximum concurrent resolutions
|
|
551
|
+
per_library_timeout: Timeout per library (seconds)
|
|
552
|
+
circuit_breaker: Optional circuit breaker for external calls
|
|
553
|
+
"""
|
|
554
|
+
self.max_concurrent = max_concurrent
|
|
555
|
+
self.per_library_timeout = per_library_timeout
|
|
556
|
+
self.circuit_breaker = circuit_breaker or CircuitBreaker(
|
|
557
|
+
name="context7_resolver",
|
|
558
|
+
failure_threshold=3,
|
|
559
|
+
recovery_timeout=30.0,
|
|
560
|
+
)
|
|
561
|
+
self._semaphore: asyncio.Semaphore | None = None
|
|
562
|
+
|
|
563
|
+
async def resolve_libraries(
|
|
564
|
+
self,
|
|
565
|
+
libraries: list[str],
|
|
566
|
+
resolve_func: Callable[[str], Any],
|
|
567
|
+
on_success: Callable[[str, Any], None] | None = None,
|
|
568
|
+
on_failure: Callable[[str, Exception], None] | None = None,
|
|
569
|
+
) -> dict[str, Any]:
|
|
570
|
+
"""
|
|
571
|
+
Resolve multiple libraries in parallel with bounded concurrency.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
libraries: List of library names to resolve
|
|
575
|
+
resolve_func: Async function to resolve single library
|
|
576
|
+
on_success: Optional callback on success
|
|
577
|
+
on_failure: Optional callback on failure
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Dict mapping library names to results (or None if failed)
|
|
581
|
+
"""
|
|
582
|
+
if not libraries:
|
|
583
|
+
return {}
|
|
584
|
+
|
|
585
|
+
# Create semaphore for bounded concurrency
|
|
586
|
+
if self._semaphore is None:
|
|
587
|
+
self._semaphore = asyncio.Semaphore(self.max_concurrent)
|
|
588
|
+
|
|
589
|
+
async def resolve_with_timeout(library: str) -> tuple[str, Any]:
|
|
590
|
+
"""Resolve single library with timeout and circuit breaker."""
|
|
591
|
+
# Check circuit breaker
|
|
592
|
+
if not self.circuit_breaker.can_execute():
|
|
593
|
+
logger.debug(f"Circuit breaker open, skipping {library}")
|
|
594
|
+
if on_failure:
|
|
595
|
+
on_failure(library, Exception("Circuit breaker open"))
|
|
596
|
+
return library, None
|
|
597
|
+
|
|
598
|
+
async with self._semaphore:
|
|
599
|
+
try:
|
|
600
|
+
result = await asyncio.wait_for(
|
|
601
|
+
resolve_func(library),
|
|
602
|
+
timeout=self.per_library_timeout,
|
|
603
|
+
)
|
|
604
|
+
self.circuit_breaker.record_success()
|
|
605
|
+
if on_success:
|
|
606
|
+
on_success(library, result)
|
|
607
|
+
return library, result
|
|
608
|
+
|
|
609
|
+
except asyncio.TimeoutError:
|
|
610
|
+
logger.debug(f"Timeout resolving {library} after {self.per_library_timeout}s")
|
|
611
|
+
self.circuit_breaker.record_failure()
|
|
612
|
+
if on_failure:
|
|
613
|
+
on_failure(library, asyncio.TimeoutError(f"Timeout after {self.per_library_timeout}s"))
|
|
614
|
+
return library, None
|
|
615
|
+
|
|
616
|
+
except Exception as e:
|
|
617
|
+
logger.debug(f"Failed to resolve {library}: {e}")
|
|
618
|
+
self.circuit_breaker.record_failure()
|
|
619
|
+
if on_failure:
|
|
620
|
+
on_failure(library, e)
|
|
621
|
+
return library, None
|
|
622
|
+
|
|
623
|
+
# Run all resolutions in parallel (bounded by semaphore)
|
|
624
|
+
tasks = [resolve_with_timeout(lib) for lib in libraries]
|
|
625
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
626
|
+
|
|
627
|
+
# Build result dict
|
|
628
|
+
result_dict: dict[str, Any] = {}
|
|
629
|
+
for item in results:
|
|
630
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
631
|
+
lib, result = item
|
|
632
|
+
result_dict[lib] = result
|
|
633
|
+
elif isinstance(item, Exception):
|
|
634
|
+
logger.warning(f"Unexpected exception in parallel resolve: {item}")
|
|
635
|
+
|
|
636
|
+
return result_dict
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# Alias for compatibility with existing imports
|
|
640
|
+
AsyncCacheManager = AsyncLRUCache
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def init_async_cache(
|
|
644
|
+
persist_dir: Path | None = None,
|
|
645
|
+
max_size: int = 500,
|
|
646
|
+
) -> AsyncCacheManager:
|
|
647
|
+
"""
|
|
648
|
+
Initialize the global async cache.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
persist_dir: Directory for disk persistence
|
|
652
|
+
max_size: Maximum entries in memory
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Initialized AsyncCacheManager instance
|
|
656
|
+
"""
|
|
657
|
+
global _async_cache
|
|
658
|
+
|
|
659
|
+
if persist_dir is None:
|
|
660
|
+
# Use project root detection instead of current working directory
|
|
661
|
+
from ...core.path_validator import PathValidator
|
|
662
|
+
validator = PathValidator()
|
|
663
|
+
persist_dir = validator.project_root / ".tapps-agents" / "kb" / "async-cache"
|
|
664
|
+
|
|
665
|
+
_async_cache = AsyncCacheManager(
|
|
666
|
+
max_size=max_size,
|
|
667
|
+
persist_dir=persist_dir,
|
|
668
|
+
auto_persist=True,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
return _async_cache
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def get_async_cache() -> AsyncCacheManager | None:
|
|
675
|
+
"""
|
|
676
|
+
Get the global async cache instance.
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
AsyncCacheManager instance or None if not initialized
|
|
680
|
+
"""
|
|
681
|
+
return _async_cache
|