attune-ai 2.0.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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Agent Heartbeat Tracking System.
|
|
2
|
+
|
|
3
|
+
Pattern 1 from Agent Coordination Architecture - TTL-based heartbeat monitoring
|
|
4
|
+
for tracking agent execution status and detecting stale/failed agents.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Start tracking an agent
|
|
8
|
+
coordinator = HeartbeatCoordinator()
|
|
9
|
+
coordinator.start_heartbeat(
|
|
10
|
+
agent_id="code-review-abc123",
|
|
11
|
+
metadata={"workflow": "code-review", "run_id": "xyz"}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Update progress
|
|
15
|
+
coordinator.beat(
|
|
16
|
+
status="running",
|
|
17
|
+
progress=0.5,
|
|
18
|
+
current_task="Analyzing functions"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Stop tracking
|
|
22
|
+
coordinator.stop_heartbeat(final_status="completed")
|
|
23
|
+
|
|
24
|
+
# Monitor all active agents
|
|
25
|
+
active_agents = coordinator.get_active_agents()
|
|
26
|
+
for agent in active_agents:
|
|
27
|
+
print(f"{agent.agent_id}: {agent.status} - {agent.current_task}")
|
|
28
|
+
|
|
29
|
+
Copyright 2025 Smart-AI-Memory
|
|
30
|
+
Licensed under Fair Source License 0.9
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AgentHeartbeat:
|
|
45
|
+
"""Agent heartbeat data structure.
|
|
46
|
+
|
|
47
|
+
Represents the current state of a running agent, stored in Redis with TTL.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
agent_id: str
|
|
51
|
+
status: str # "starting", "running", "completed", "failed", "cancelled"
|
|
52
|
+
progress: float # 0.0 to 1.0
|
|
53
|
+
current_task: str
|
|
54
|
+
last_beat: datetime
|
|
55
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
display_name: str | None = None # Optional human-readable name for dashboard
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
"""Convert to dictionary for serialization."""
|
|
60
|
+
return {
|
|
61
|
+
"agent_id": self.agent_id,
|
|
62
|
+
"status": self.status,
|
|
63
|
+
"progress": self.progress,
|
|
64
|
+
"current_task": self.current_task,
|
|
65
|
+
"last_beat": self.last_beat.isoformat() if isinstance(self.last_beat, datetime) else self.last_beat,
|
|
66
|
+
"metadata": self.metadata,
|
|
67
|
+
"display_name": self.display_name,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_dict(cls, data: dict[str, Any]) -> AgentHeartbeat:
|
|
72
|
+
"""Create from dictionary."""
|
|
73
|
+
# Convert ISO string back to datetime
|
|
74
|
+
last_beat = data.get("last_beat")
|
|
75
|
+
if isinstance(last_beat, str):
|
|
76
|
+
last_beat = datetime.fromisoformat(last_beat)
|
|
77
|
+
elif not isinstance(last_beat, datetime):
|
|
78
|
+
last_beat = datetime.utcnow()
|
|
79
|
+
|
|
80
|
+
return cls(
|
|
81
|
+
agent_id=data["agent_id"],
|
|
82
|
+
status=data["status"],
|
|
83
|
+
progress=data["progress"],
|
|
84
|
+
current_task=data["current_task"],
|
|
85
|
+
last_beat=last_beat,
|
|
86
|
+
metadata=data.get("metadata", {}),
|
|
87
|
+
display_name=data.get("display_name"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class HeartbeatCoordinator:
|
|
92
|
+
"""Coordinates agent heartbeats using Redis TTL keys.
|
|
93
|
+
|
|
94
|
+
Agents publish heartbeats with a TTL. If an agent stops responding,
|
|
95
|
+
its heartbeat key expires automatically, indicating failure/crash.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
HEARTBEAT_TTL: Default heartbeat TTL in seconds (30s)
|
|
99
|
+
HEARTBEAT_INTERVAL: Recommended update interval (10s)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
HEARTBEAT_TTL = 30 # Heartbeat expires after 30s of no updates
|
|
103
|
+
HEARTBEAT_INTERVAL = 10 # Agents should update every 10s
|
|
104
|
+
|
|
105
|
+
def __init__(self, memory=None, enable_streaming: bool = False):
|
|
106
|
+
"""Initialize heartbeat coordinator.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
memory: Memory instance (UnifiedMemory or ShortTermMemory).
|
|
110
|
+
If None, attempts to get from UsageTracker.
|
|
111
|
+
enable_streaming: If True, publish heartbeat events to Redis Streams
|
|
112
|
+
for real-time monitoring (Pattern 4).
|
|
113
|
+
"""
|
|
114
|
+
self.memory = memory
|
|
115
|
+
self.agent_id: str | None = None
|
|
116
|
+
self.display_name: str | None = None
|
|
117
|
+
self._enable_streaming = enable_streaming
|
|
118
|
+
self._event_streamer = None
|
|
119
|
+
|
|
120
|
+
if self.memory is None:
|
|
121
|
+
try:
|
|
122
|
+
from attune.telemetry import UsageTracker
|
|
123
|
+
|
|
124
|
+
tracker = UsageTracker.get_instance()
|
|
125
|
+
if hasattr(tracker, "_memory"):
|
|
126
|
+
self.memory = tracker._memory
|
|
127
|
+
except (ImportError, AttributeError):
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
if self.memory is None:
|
|
131
|
+
logger.warning("No memory backend available for heartbeat tracking")
|
|
132
|
+
|
|
133
|
+
def _get_event_streamer(self):
|
|
134
|
+
"""Get or create EventStreamer instance (lazy initialization)."""
|
|
135
|
+
if not self._enable_streaming:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if self._event_streamer is None:
|
|
139
|
+
try:
|
|
140
|
+
from attune.telemetry.event_streaming import EventStreamer
|
|
141
|
+
|
|
142
|
+
self._event_streamer = EventStreamer(memory=self.memory)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.warning(f"Failed to initialize EventStreamer: {e}")
|
|
145
|
+
self._enable_streaming = False
|
|
146
|
+
|
|
147
|
+
return self._event_streamer
|
|
148
|
+
|
|
149
|
+
def start_heartbeat(
|
|
150
|
+
self, agent_id: str, metadata: dict[str, Any] | None = None, display_name: str | None = None
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Start heartbeat for an agent.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
agent_id: Unique agent identifier
|
|
156
|
+
metadata: Initial metadata (workflow, run_id, etc.)
|
|
157
|
+
display_name: Optional human-readable name for dashboard display
|
|
158
|
+
"""
|
|
159
|
+
if not self.memory:
|
|
160
|
+
logger.debug("Heartbeat tracking disabled (no memory backend)")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
self.agent_id = agent_id
|
|
164
|
+
self.display_name = display_name
|
|
165
|
+
self._publish_heartbeat(
|
|
166
|
+
status="starting",
|
|
167
|
+
progress=0.0,
|
|
168
|
+
current_task="initializing",
|
|
169
|
+
metadata=metadata or {},
|
|
170
|
+
display_name=display_name,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def beat(self, status: str = "running", progress: float = 0.0, current_task: str = "") -> None:
|
|
174
|
+
"""Publish heartbeat update.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
status: Current agent status
|
|
178
|
+
progress: Progress percentage (0.0 - 1.0)
|
|
179
|
+
current_task: Human-readable current task description
|
|
180
|
+
"""
|
|
181
|
+
if not self.agent_id or not self.memory:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
self._publish_heartbeat(
|
|
185
|
+
status=status,
|
|
186
|
+
progress=progress,
|
|
187
|
+
current_task=current_task,
|
|
188
|
+
metadata={},
|
|
189
|
+
display_name=self.display_name,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def stop_heartbeat(self, final_status: str = "completed") -> None:
|
|
193
|
+
"""Stop heartbeat (agent finished).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
final_status: Final status ("completed", "failed", "cancelled")
|
|
197
|
+
"""
|
|
198
|
+
if not self.agent_id or not self.memory:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
# Publish final heartbeat with short TTL
|
|
202
|
+
self._publish_heartbeat(
|
|
203
|
+
status=final_status,
|
|
204
|
+
progress=1.0,
|
|
205
|
+
current_task="finished",
|
|
206
|
+
metadata={"final": True},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Clear agent ID
|
|
210
|
+
self.agent_id = None
|
|
211
|
+
|
|
212
|
+
def _publish_heartbeat(
|
|
213
|
+
self, status: str, progress: float, current_task: str, metadata: dict[str, Any], display_name: str | None = None
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Publish heartbeat to Redis with TTL and optionally to event stream."""
|
|
216
|
+
if not self.memory or not self.agent_id:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
heartbeat = AgentHeartbeat(
|
|
220
|
+
agent_id=self.agent_id,
|
|
221
|
+
status=status,
|
|
222
|
+
progress=progress,
|
|
223
|
+
current_task=current_task,
|
|
224
|
+
last_beat=datetime.utcnow(),
|
|
225
|
+
metadata=metadata,
|
|
226
|
+
display_name=display_name,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Store in Redis with TTL (Pattern 1)
|
|
230
|
+
key = f"empathy:heartbeat:{self.agent_id}"
|
|
231
|
+
try:
|
|
232
|
+
# Use direct Redis access for heartbeats (need custom 30s TTL)
|
|
233
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
234
|
+
# Direct Redis access with setex for custom TTL
|
|
235
|
+
import json
|
|
236
|
+
|
|
237
|
+
self.memory._client.setex(key, self.HEARTBEAT_TTL, json.dumps(heartbeat.to_dict()))
|
|
238
|
+
else:
|
|
239
|
+
logger.warning("Cannot publish heartbeat: no Redis backend available")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.warning(f"Failed to publish heartbeat for {self.agent_id}: {e}")
|
|
242
|
+
|
|
243
|
+
# Publish to event stream (Pattern 4 - optional)
|
|
244
|
+
streamer = self._get_event_streamer()
|
|
245
|
+
if streamer:
|
|
246
|
+
try:
|
|
247
|
+
streamer.publish_event(
|
|
248
|
+
event_type="agent_heartbeat",
|
|
249
|
+
data=heartbeat.to_dict(),
|
|
250
|
+
source="attune",
|
|
251
|
+
)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.debug(f"Failed to publish heartbeat event to stream: {e}")
|
|
254
|
+
|
|
255
|
+
def get_active_agents(self) -> list[AgentHeartbeat]:
|
|
256
|
+
"""Get all currently active agents.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List of active agent heartbeats
|
|
260
|
+
"""
|
|
261
|
+
if not self.memory:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
# Scan for empathy:heartbeat:* keys
|
|
266
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
267
|
+
keys = self.memory._client.keys("empathy:heartbeat:*")
|
|
268
|
+
else:
|
|
269
|
+
logger.warning("Cannot scan for heartbeats: no Redis access")
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
heartbeats = []
|
|
273
|
+
for key in keys:
|
|
274
|
+
if isinstance(key, bytes):
|
|
275
|
+
key = key.decode("utf-8")
|
|
276
|
+
|
|
277
|
+
data = self._retrieve_heartbeat(key)
|
|
278
|
+
if data:
|
|
279
|
+
heartbeats.append(AgentHeartbeat.from_dict(data))
|
|
280
|
+
|
|
281
|
+
return heartbeats
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(f"Failed to get active agents: {e}")
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
def is_agent_alive(self, agent_id: str) -> bool:
|
|
287
|
+
"""Check if agent is still alive.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
agent_id: Agent to check
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
True if heartbeat key exists (agent is alive)
|
|
294
|
+
"""
|
|
295
|
+
if not self.memory:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
key = f"heartbeat:{agent_id}"
|
|
299
|
+
data = self._retrieve_heartbeat(key)
|
|
300
|
+
return data is not None
|
|
301
|
+
|
|
302
|
+
def get_agent_status(self, agent_id: str) -> AgentHeartbeat | None:
|
|
303
|
+
"""Get current status of an agent.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
agent_id: Agent to query
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
AgentHeartbeat or None if agent not active
|
|
310
|
+
"""
|
|
311
|
+
if not self.memory:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
key = f"heartbeat:{agent_id}"
|
|
315
|
+
data = self._retrieve_heartbeat(key)
|
|
316
|
+
|
|
317
|
+
if data:
|
|
318
|
+
return AgentHeartbeat.from_dict(data)
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
def _retrieve_heartbeat(self, key: str) -> dict[str, Any] | None:
|
|
322
|
+
"""Retrieve heartbeat data from memory.
|
|
323
|
+
|
|
324
|
+
Heartbeat keys are stored directly as 'heartbeat:{agent_id}' and must be
|
|
325
|
+
retrieved via direct Redis access, not through the standard retrieve() method
|
|
326
|
+
which expects keys with 'working:{agent_id}:{key}' format.
|
|
327
|
+
"""
|
|
328
|
+
if not self.memory:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# Use direct Redis access for heartbeat keys
|
|
333
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
334
|
+
import json
|
|
335
|
+
|
|
336
|
+
data = self.memory._client.get(key)
|
|
337
|
+
if data:
|
|
338
|
+
if isinstance(data, bytes):
|
|
339
|
+
data = data.decode("utf-8")
|
|
340
|
+
result = json.loads(data)
|
|
341
|
+
return result if isinstance(result, dict) else None
|
|
342
|
+
return None
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.debug(f"Failed to retrieve heartbeat {key}: {e}")
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
def get_stale_agents(self, threshold_seconds: float = 60.0) -> list[AgentHeartbeat]:
|
|
348
|
+
"""Get agents that haven't updated in a while (but key still exists).
|
|
349
|
+
|
|
350
|
+
This detects agents that are stuck or slow, not crashed (TTL would expire).
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
threshold_seconds: Time without update to consider stale
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of stale agent heartbeats
|
|
357
|
+
"""
|
|
358
|
+
active = self.get_active_agents()
|
|
359
|
+
now = datetime.utcnow()
|
|
360
|
+
stale = []
|
|
361
|
+
|
|
362
|
+
for agent in active:
|
|
363
|
+
time_since_beat = (now - agent.last_beat).total_seconds()
|
|
364
|
+
if time_since_beat > threshold_seconds and agent.status not in ("completed", "failed", "cancelled"):
|
|
365
|
+
stale.append(agent)
|
|
366
|
+
|
|
367
|
+
return stale
|