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
attune/redis_memory.py
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
"""Redis Short-Term Memory for Empathy Framework
|
|
2
|
+
|
|
3
|
+
Per EMPATHY_PHILOSOPHY.md v1.1.0:
|
|
4
|
+
- Implements fast, TTL-based working memory for agent coordination
|
|
5
|
+
- Role-based access tiers for data integrity
|
|
6
|
+
- Pattern staging before validation
|
|
7
|
+
- Principled negotiation support
|
|
8
|
+
|
|
9
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
10
|
+
Licensed under Fair Source 0.9
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import redis
|
|
21
|
+
|
|
22
|
+
REDIS_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
REDIS_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Import AccessTier from the canonical location to avoid duplicate enums
|
|
28
|
+
from .memory.short_term import AccessTier
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TTLStrategy(Enum):
|
|
32
|
+
"""TTL strategies for different memory types
|
|
33
|
+
|
|
34
|
+
Per EMPATHY_PHILOSOPHY.md Section 9.3:
|
|
35
|
+
- Working results: 1 hour
|
|
36
|
+
- Staged patterns: 24 hours
|
|
37
|
+
- Coordination signals: 5 minutes
|
|
38
|
+
- Conflict context: Until resolution
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
WORKING_RESULTS = 3600 # 1 hour
|
|
42
|
+
STAGED_PATTERNS = 86400 # 24 hours
|
|
43
|
+
COORDINATION = 300 # 5 minutes
|
|
44
|
+
CONFLICT_CONTEXT = 604800 # 7 days (fallback for unresolved)
|
|
45
|
+
SESSION = 1800 # 30 minutes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AgentCredentials:
|
|
50
|
+
"""Agent identity and access permissions"""
|
|
51
|
+
|
|
52
|
+
agent_id: str
|
|
53
|
+
tier: AccessTier
|
|
54
|
+
roles: list[str] = field(default_factory=list)
|
|
55
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
56
|
+
|
|
57
|
+
def can_read(self) -> bool:
|
|
58
|
+
"""All tiers can read"""
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def can_stage(self) -> bool:
|
|
62
|
+
"""Contributor+ can stage patterns"""
|
|
63
|
+
return self.tier.value >= AccessTier.CONTRIBUTOR.value
|
|
64
|
+
|
|
65
|
+
def can_validate(self) -> bool:
|
|
66
|
+
"""Validator+ can promote patterns"""
|
|
67
|
+
return self.tier.value >= AccessTier.VALIDATOR.value
|
|
68
|
+
|
|
69
|
+
def can_administer(self) -> bool:
|
|
70
|
+
"""Only Stewards have full admin access"""
|
|
71
|
+
return self.tier.value >= AccessTier.STEWARD.value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class StagedPattern:
|
|
76
|
+
"""Pattern awaiting validation"""
|
|
77
|
+
|
|
78
|
+
pattern_id: str
|
|
79
|
+
agent_id: str
|
|
80
|
+
pattern_type: str
|
|
81
|
+
name: str
|
|
82
|
+
description: str
|
|
83
|
+
code: str | None = None
|
|
84
|
+
context: dict = field(default_factory=dict)
|
|
85
|
+
confidence: float = 0.5
|
|
86
|
+
staged_at: datetime = field(default_factory=datetime.now)
|
|
87
|
+
interests: list[str] = field(default_factory=list) # For negotiation
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
return {
|
|
91
|
+
"pattern_id": self.pattern_id,
|
|
92
|
+
"agent_id": self.agent_id,
|
|
93
|
+
"pattern_type": self.pattern_type,
|
|
94
|
+
"name": self.name,
|
|
95
|
+
"description": self.description,
|
|
96
|
+
"code": self.code,
|
|
97
|
+
"context": self.context,
|
|
98
|
+
"confidence": self.confidence,
|
|
99
|
+
"staged_at": self.staged_at.isoformat(),
|
|
100
|
+
"interests": self.interests,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(cls, data: dict) -> "StagedPattern":
|
|
105
|
+
return cls(
|
|
106
|
+
pattern_id=data["pattern_id"],
|
|
107
|
+
agent_id=data["agent_id"],
|
|
108
|
+
pattern_type=data["pattern_type"],
|
|
109
|
+
name=data["name"],
|
|
110
|
+
description=data["description"],
|
|
111
|
+
code=data.get("code"),
|
|
112
|
+
context=data.get("context", {}),
|
|
113
|
+
confidence=data.get("confidence", 0.5),
|
|
114
|
+
staged_at=datetime.fromisoformat(data["staged_at"]),
|
|
115
|
+
interests=data.get("interests", []),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class ConflictContext:
|
|
121
|
+
"""Context for principled negotiation
|
|
122
|
+
|
|
123
|
+
Per Getting to Yes framework:
|
|
124
|
+
- Positions: What each party says they want
|
|
125
|
+
- Interests: Why they want it (underlying needs)
|
|
126
|
+
- BATNA: Best Alternative to Negotiated Agreement
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
conflict_id: str
|
|
130
|
+
positions: dict[str, Any] # agent_id -> stated position
|
|
131
|
+
interests: dict[str, list[str]] # agent_id -> underlying interests
|
|
132
|
+
batna: str | None = None # Fallback strategy
|
|
133
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
134
|
+
resolved: bool = False
|
|
135
|
+
resolution: str | None = None
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> dict:
|
|
138
|
+
return {
|
|
139
|
+
"conflict_id": self.conflict_id,
|
|
140
|
+
"positions": self.positions,
|
|
141
|
+
"interests": self.interests,
|
|
142
|
+
"batna": self.batna,
|
|
143
|
+
"created_at": self.created_at.isoformat(),
|
|
144
|
+
"resolved": self.resolved,
|
|
145
|
+
"resolution": self.resolution,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_dict(cls, data: dict) -> "ConflictContext":
|
|
150
|
+
return cls(
|
|
151
|
+
conflict_id=data["conflict_id"],
|
|
152
|
+
positions=data["positions"],
|
|
153
|
+
interests=data["interests"],
|
|
154
|
+
batna=data.get("batna"),
|
|
155
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
156
|
+
resolved=data.get("resolved", False),
|
|
157
|
+
resolution=data.get("resolution"),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RedisShortTermMemory:
|
|
162
|
+
"""Redis-backed short-term memory for agent coordination
|
|
163
|
+
|
|
164
|
+
Features:
|
|
165
|
+
- Fast read/write with automatic TTL expiration
|
|
166
|
+
- Role-based access control
|
|
167
|
+
- Pattern staging workflow
|
|
168
|
+
- Conflict negotiation context
|
|
169
|
+
- Agent working memory
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> memory = RedisShortTermMemory()
|
|
173
|
+
>>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
|
|
174
|
+
>>> memory.stash("analysis_results", {"issues": 3}, creds)
|
|
175
|
+
>>> data = memory.retrieve("analysis_results", creds)
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
# Key prefixes for namespacing
|
|
180
|
+
PREFIX_WORKING = "empathy:working:"
|
|
181
|
+
PREFIX_STAGED = "empathy:staged:"
|
|
182
|
+
PREFIX_CONFLICT = "empathy:conflict:"
|
|
183
|
+
PREFIX_COORDINATION = "empathy:coord:"
|
|
184
|
+
PREFIX_SESSION = "empathy:session:"
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
host: str = "localhost",
|
|
189
|
+
port: int = 6379,
|
|
190
|
+
db: int = 0,
|
|
191
|
+
password: str | None = None,
|
|
192
|
+
use_mock: bool = False,
|
|
193
|
+
):
|
|
194
|
+
"""Initialize Redis connection
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
host: Redis host
|
|
198
|
+
port: Redis port
|
|
199
|
+
db: Redis database number
|
|
200
|
+
password: Redis password (optional)
|
|
201
|
+
use_mock: Use in-memory mock for testing
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
self.use_mock = use_mock or not REDIS_AVAILABLE
|
|
205
|
+
|
|
206
|
+
if self.use_mock:
|
|
207
|
+
self._mock_storage: dict[str, tuple[Any, float | None]] = {}
|
|
208
|
+
self._client = None
|
|
209
|
+
else:
|
|
210
|
+
self._client = redis.Redis(
|
|
211
|
+
host=host,
|
|
212
|
+
port=port,
|
|
213
|
+
db=db,
|
|
214
|
+
password=password,
|
|
215
|
+
decode_responses=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _get(self, key: str) -> str | None:
|
|
219
|
+
"""Get value from Redis or mock"""
|
|
220
|
+
if self.use_mock:
|
|
221
|
+
if key in self._mock_storage:
|
|
222
|
+
value, expires = self._mock_storage[key]
|
|
223
|
+
if expires is None or datetime.now().timestamp() < expires:
|
|
224
|
+
return str(value) if value is not None else None
|
|
225
|
+
del self._mock_storage[key]
|
|
226
|
+
return None
|
|
227
|
+
if self._client is None:
|
|
228
|
+
return None
|
|
229
|
+
result = self._client.get(key)
|
|
230
|
+
return str(result) if result else None
|
|
231
|
+
|
|
232
|
+
def _set(self, key: str, value: str, ttl: int | None = None) -> bool:
|
|
233
|
+
"""Set value in Redis or mock"""
|
|
234
|
+
if self.use_mock:
|
|
235
|
+
expires = datetime.now().timestamp() + ttl if ttl else None
|
|
236
|
+
self._mock_storage[key] = (value, expires)
|
|
237
|
+
return True
|
|
238
|
+
if self._client is None:
|
|
239
|
+
return False
|
|
240
|
+
if ttl:
|
|
241
|
+
self._client.setex(key, ttl, value)
|
|
242
|
+
return True
|
|
243
|
+
result = self._client.set(key, value)
|
|
244
|
+
return bool(result)
|
|
245
|
+
|
|
246
|
+
def _delete(self, key: str) -> bool:
|
|
247
|
+
"""Delete key from Redis or mock"""
|
|
248
|
+
if self.use_mock:
|
|
249
|
+
if key in self._mock_storage:
|
|
250
|
+
del self._mock_storage[key]
|
|
251
|
+
return True
|
|
252
|
+
return False
|
|
253
|
+
if self._client is None:
|
|
254
|
+
return False
|
|
255
|
+
return self._client.delete(key) > 0
|
|
256
|
+
|
|
257
|
+
def _keys(self, pattern: str) -> list[str]:
|
|
258
|
+
"""Get keys matching pattern"""
|
|
259
|
+
if self.use_mock:
|
|
260
|
+
import fnmatch
|
|
261
|
+
|
|
262
|
+
return [k for k in self._mock_storage.keys() if fnmatch.fnmatch(k, pattern)]
|
|
263
|
+
if self._client is None:
|
|
264
|
+
return []
|
|
265
|
+
keys = self._client.keys(pattern)
|
|
266
|
+
return [k.decode() if isinstance(k, bytes) else str(k) for k in keys]
|
|
267
|
+
|
|
268
|
+
# === Working Memory (Stash/Retrieve) ===
|
|
269
|
+
|
|
270
|
+
def stash(
|
|
271
|
+
self,
|
|
272
|
+
key: str,
|
|
273
|
+
data: Any,
|
|
274
|
+
credentials: AgentCredentials,
|
|
275
|
+
ttl: TTLStrategy = TTLStrategy.WORKING_RESULTS,
|
|
276
|
+
) -> bool:
|
|
277
|
+
"""Stash data in short-term memory
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
key: Unique key for the data
|
|
281
|
+
data: Data to store (will be JSON serialized)
|
|
282
|
+
credentials: Agent credentials
|
|
283
|
+
ttl: Time-to-live strategy
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if successful
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> memory.stash("analysis_v1", {"findings": [...]}, creds)
|
|
290
|
+
|
|
291
|
+
"""
|
|
292
|
+
if not credentials.can_stage():
|
|
293
|
+
raise PermissionError(
|
|
294
|
+
f"Agent {credentials.agent_id} (Tier {credentials.tier.name}) "
|
|
295
|
+
"cannot write to memory. Requires CONTRIBUTOR or higher.",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
full_key = f"{self.PREFIX_WORKING}{credentials.agent_id}:{key}"
|
|
299
|
+
payload = {
|
|
300
|
+
"data": data,
|
|
301
|
+
"agent_id": credentials.agent_id,
|
|
302
|
+
"stashed_at": datetime.now().isoformat(),
|
|
303
|
+
}
|
|
304
|
+
return self._set(full_key, json.dumps(payload), ttl.value)
|
|
305
|
+
|
|
306
|
+
def retrieve(
|
|
307
|
+
self,
|
|
308
|
+
key: str,
|
|
309
|
+
credentials: AgentCredentials,
|
|
310
|
+
agent_id: str | None = None,
|
|
311
|
+
) -> Any | None:
|
|
312
|
+
"""Retrieve data from short-term memory
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
key: Key to retrieve
|
|
316
|
+
credentials: Agent credentials
|
|
317
|
+
agent_id: Owner agent ID (defaults to credentials agent)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Retrieved data or None if not found
|
|
321
|
+
|
|
322
|
+
Example:
|
|
323
|
+
>>> data = memory.retrieve("analysis_v1", creds)
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
owner = agent_id or credentials.agent_id
|
|
327
|
+
full_key = f"{self.PREFIX_WORKING}{owner}:{key}"
|
|
328
|
+
raw = self._get(full_key)
|
|
329
|
+
|
|
330
|
+
if raw is None:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
payload = json.loads(raw)
|
|
334
|
+
return payload.get("data")
|
|
335
|
+
|
|
336
|
+
def clear_working_memory(self, credentials: AgentCredentials) -> int:
|
|
337
|
+
"""Clear all working memory for an agent
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
credentials: Agent credentials (must own the memory or be Steward)
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Number of keys deleted
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
pattern = f"{self.PREFIX_WORKING}{credentials.agent_id}:*"
|
|
347
|
+
keys = self._keys(pattern)
|
|
348
|
+
count = 0
|
|
349
|
+
for key in keys:
|
|
350
|
+
if self._delete(key):
|
|
351
|
+
count += 1
|
|
352
|
+
return count
|
|
353
|
+
|
|
354
|
+
# === Pattern Staging ===
|
|
355
|
+
|
|
356
|
+
def stage_pattern(
|
|
357
|
+
self,
|
|
358
|
+
pattern: StagedPattern,
|
|
359
|
+
credentials: AgentCredentials,
|
|
360
|
+
) -> bool:
|
|
361
|
+
"""Stage a pattern for validation
|
|
362
|
+
|
|
363
|
+
Per EMPATHY_PHILOSOPHY.md: Patterns must be staged before
|
|
364
|
+
being promoted to the active library.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
pattern: Pattern to stage
|
|
368
|
+
credentials: Must be CONTRIBUTOR or higher
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
True if staged successfully
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
if not credentials.can_stage():
|
|
375
|
+
raise PermissionError(
|
|
376
|
+
f"Agent {credentials.agent_id} cannot stage patterns. "
|
|
377
|
+
"Requires CONTRIBUTOR tier or higher.",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
key = f"{self.PREFIX_STAGED}{pattern.pattern_id}"
|
|
381
|
+
return self._set(
|
|
382
|
+
key,
|
|
383
|
+
json.dumps(pattern.to_dict()),
|
|
384
|
+
TTLStrategy.STAGED_PATTERNS.value,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def get_staged_pattern(
|
|
388
|
+
self,
|
|
389
|
+
pattern_id: str,
|
|
390
|
+
credentials: AgentCredentials,
|
|
391
|
+
) -> StagedPattern | None:
|
|
392
|
+
"""Retrieve a staged pattern
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
pattern_id: Pattern ID
|
|
396
|
+
credentials: Any tier can read
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
StagedPattern or None
|
|
400
|
+
|
|
401
|
+
"""
|
|
402
|
+
key = f"{self.PREFIX_STAGED}{pattern_id}"
|
|
403
|
+
raw = self._get(key)
|
|
404
|
+
|
|
405
|
+
if raw is None:
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
return StagedPattern.from_dict(json.loads(raw))
|
|
409
|
+
|
|
410
|
+
def list_staged_patterns(
|
|
411
|
+
self,
|
|
412
|
+
credentials: AgentCredentials,
|
|
413
|
+
) -> list[StagedPattern]:
|
|
414
|
+
"""List all staged patterns awaiting validation
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
credentials: Any tier can read
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
List of staged patterns
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
pattern = f"{self.PREFIX_STAGED}*"
|
|
424
|
+
keys = self._keys(pattern)
|
|
425
|
+
patterns = []
|
|
426
|
+
|
|
427
|
+
for key in keys:
|
|
428
|
+
raw = self._get(key)
|
|
429
|
+
if raw:
|
|
430
|
+
patterns.append(StagedPattern.from_dict(json.loads(raw)))
|
|
431
|
+
|
|
432
|
+
return patterns
|
|
433
|
+
|
|
434
|
+
def promote_pattern(
|
|
435
|
+
self,
|
|
436
|
+
pattern_id: str,
|
|
437
|
+
credentials: AgentCredentials,
|
|
438
|
+
) -> StagedPattern | None:
|
|
439
|
+
"""Promote staged pattern (remove from staging for library add)
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
pattern_id: Pattern to promote
|
|
443
|
+
credentials: Must be VALIDATOR or higher
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
The promoted pattern (for adding to PatternLibrary)
|
|
447
|
+
|
|
448
|
+
"""
|
|
449
|
+
if not credentials.can_validate():
|
|
450
|
+
raise PermissionError(
|
|
451
|
+
f"Agent {credentials.agent_id} cannot promote patterns. "
|
|
452
|
+
"Requires VALIDATOR tier or higher.",
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
pattern = self.get_staged_pattern(pattern_id, credentials)
|
|
456
|
+
if pattern:
|
|
457
|
+
key = f"{self.PREFIX_STAGED}{pattern_id}"
|
|
458
|
+
self._delete(key)
|
|
459
|
+
return pattern
|
|
460
|
+
|
|
461
|
+
def reject_pattern(
|
|
462
|
+
self,
|
|
463
|
+
pattern_id: str,
|
|
464
|
+
credentials: AgentCredentials,
|
|
465
|
+
reason: str = "",
|
|
466
|
+
) -> bool:
|
|
467
|
+
"""Reject a staged pattern
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
pattern_id: Pattern to reject
|
|
471
|
+
credentials: Must be VALIDATOR or higher
|
|
472
|
+
reason: Rejection reason (for audit)
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
True if rejected
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
if not credentials.can_validate():
|
|
479
|
+
raise PermissionError(
|
|
480
|
+
f"Agent {credentials.agent_id} cannot reject patterns. "
|
|
481
|
+
"Requires VALIDATOR tier or higher.",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
key = f"{self.PREFIX_STAGED}{pattern_id}"
|
|
485
|
+
return self._delete(key)
|
|
486
|
+
|
|
487
|
+
# === Conflict Negotiation ===
|
|
488
|
+
|
|
489
|
+
def create_conflict_context(
|
|
490
|
+
self,
|
|
491
|
+
conflict_id: str,
|
|
492
|
+
positions: dict[str, Any],
|
|
493
|
+
interests: dict[str, list[str]],
|
|
494
|
+
credentials: AgentCredentials,
|
|
495
|
+
batna: str | None = None,
|
|
496
|
+
) -> ConflictContext:
|
|
497
|
+
"""Create context for principled negotiation
|
|
498
|
+
|
|
499
|
+
Per Getting to Yes framework:
|
|
500
|
+
- Separate positions from interests
|
|
501
|
+
- Define BATNA before negotiating
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
conflict_id: Unique conflict identifier
|
|
505
|
+
positions: agent_id -> their stated position
|
|
506
|
+
interests: agent_id -> underlying interests
|
|
507
|
+
credentials: Must be CONTRIBUTOR or higher
|
|
508
|
+
batna: Best Alternative to Negotiated Agreement
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
ConflictContext for resolution
|
|
512
|
+
|
|
513
|
+
"""
|
|
514
|
+
if not credentials.can_stage():
|
|
515
|
+
raise PermissionError(
|
|
516
|
+
f"Agent {credentials.agent_id} cannot create conflict context. "
|
|
517
|
+
"Requires CONTRIBUTOR tier or higher.",
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
context = ConflictContext(
|
|
521
|
+
conflict_id=conflict_id,
|
|
522
|
+
positions=positions,
|
|
523
|
+
interests=interests,
|
|
524
|
+
batna=batna,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
key = f"{self.PREFIX_CONFLICT}{conflict_id}"
|
|
528
|
+
self._set(
|
|
529
|
+
key,
|
|
530
|
+
json.dumps(context.to_dict()),
|
|
531
|
+
TTLStrategy.CONFLICT_CONTEXT.value,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return context
|
|
535
|
+
|
|
536
|
+
def get_conflict_context(
|
|
537
|
+
self,
|
|
538
|
+
conflict_id: str,
|
|
539
|
+
credentials: AgentCredentials,
|
|
540
|
+
) -> ConflictContext | None:
|
|
541
|
+
"""Retrieve conflict context
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
conflict_id: Conflict identifier
|
|
545
|
+
credentials: Any tier can read
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
ConflictContext or None
|
|
549
|
+
|
|
550
|
+
"""
|
|
551
|
+
key = f"{self.PREFIX_CONFLICT}{conflict_id}"
|
|
552
|
+
raw = self._get(key)
|
|
553
|
+
|
|
554
|
+
if raw is None:
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
return ConflictContext.from_dict(json.loads(raw))
|
|
558
|
+
|
|
559
|
+
def resolve_conflict(
|
|
560
|
+
self,
|
|
561
|
+
conflict_id: str,
|
|
562
|
+
resolution: str,
|
|
563
|
+
credentials: AgentCredentials,
|
|
564
|
+
) -> bool:
|
|
565
|
+
"""Mark conflict as resolved
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
conflict_id: Conflict to resolve
|
|
569
|
+
resolution: How it was resolved
|
|
570
|
+
credentials: Must be VALIDATOR or higher
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
True if resolved
|
|
574
|
+
|
|
575
|
+
"""
|
|
576
|
+
if not credentials.can_validate():
|
|
577
|
+
raise PermissionError(
|
|
578
|
+
f"Agent {credentials.agent_id} cannot resolve conflicts. "
|
|
579
|
+
"Requires VALIDATOR tier or higher.",
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
context = self.get_conflict_context(conflict_id, credentials)
|
|
583
|
+
if context is None:
|
|
584
|
+
return False
|
|
585
|
+
|
|
586
|
+
context.resolved = True
|
|
587
|
+
context.resolution = resolution
|
|
588
|
+
|
|
589
|
+
key = f"{self.PREFIX_CONFLICT}{conflict_id}"
|
|
590
|
+
# Keep resolved conflicts longer for audit
|
|
591
|
+
self._set(key, json.dumps(context.to_dict()), TTLStrategy.CONFLICT_CONTEXT.value)
|
|
592
|
+
return True
|
|
593
|
+
|
|
594
|
+
# === Coordination Signals ===
|
|
595
|
+
|
|
596
|
+
def send_signal(
|
|
597
|
+
self,
|
|
598
|
+
signal_type: str,
|
|
599
|
+
data: Any,
|
|
600
|
+
credentials: AgentCredentials,
|
|
601
|
+
target_agent: str | None = None,
|
|
602
|
+
) -> bool:
|
|
603
|
+
"""Send coordination signal to other agents
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
signal_type: Type of signal (e.g., "ready", "blocking", "complete")
|
|
607
|
+
data: Signal payload
|
|
608
|
+
credentials: Must be CONTRIBUTOR or higher
|
|
609
|
+
target_agent: Specific agent to signal (None = broadcast)
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
True if sent
|
|
613
|
+
|
|
614
|
+
"""
|
|
615
|
+
if not credentials.can_stage():
|
|
616
|
+
raise PermissionError(
|
|
617
|
+
f"Agent {credentials.agent_id} cannot send signals. "
|
|
618
|
+
"Requires CONTRIBUTOR tier or higher.",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
target = target_agent or "broadcast"
|
|
622
|
+
key = f"{self.PREFIX_COORDINATION}{signal_type}:{credentials.agent_id}:{target}"
|
|
623
|
+
payload = {
|
|
624
|
+
"signal_type": signal_type,
|
|
625
|
+
"from_agent": credentials.agent_id,
|
|
626
|
+
"to_agent": target_agent,
|
|
627
|
+
"data": data,
|
|
628
|
+
"sent_at": datetime.now().isoformat(),
|
|
629
|
+
}
|
|
630
|
+
return self._set(key, json.dumps(payload), TTLStrategy.COORDINATION.value)
|
|
631
|
+
|
|
632
|
+
def receive_signals(
|
|
633
|
+
self,
|
|
634
|
+
credentials: AgentCredentials,
|
|
635
|
+
signal_type: str | None = None,
|
|
636
|
+
) -> list[dict]:
|
|
637
|
+
"""Receive coordination signals
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
credentials: Agent receiving signals
|
|
641
|
+
signal_type: Filter by signal type (optional)
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
List of signals
|
|
645
|
+
|
|
646
|
+
"""
|
|
647
|
+
if signal_type:
|
|
648
|
+
pattern = f"{self.PREFIX_COORDINATION}{signal_type}:*:{credentials.agent_id}"
|
|
649
|
+
else:
|
|
650
|
+
pattern = f"{self.PREFIX_COORDINATION}*:{credentials.agent_id}"
|
|
651
|
+
|
|
652
|
+
# Also get broadcasts
|
|
653
|
+
broadcast_pattern = f"{self.PREFIX_COORDINATION}*:*:broadcast"
|
|
654
|
+
|
|
655
|
+
keys = set(self._keys(pattern)) | set(self._keys(broadcast_pattern))
|
|
656
|
+
signals = []
|
|
657
|
+
|
|
658
|
+
for key in keys:
|
|
659
|
+
raw = self._get(key)
|
|
660
|
+
if raw:
|
|
661
|
+
signals.append(json.loads(raw))
|
|
662
|
+
|
|
663
|
+
return signals
|
|
664
|
+
|
|
665
|
+
# === Session Management ===
|
|
666
|
+
|
|
667
|
+
def create_session(
|
|
668
|
+
self,
|
|
669
|
+
session_id: str,
|
|
670
|
+
credentials: AgentCredentials,
|
|
671
|
+
metadata: dict | None = None,
|
|
672
|
+
) -> bool:
|
|
673
|
+
"""Create a collaboration session
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
session_id: Unique session identifier
|
|
677
|
+
credentials: Session creator
|
|
678
|
+
metadata: Optional session metadata
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
True if created
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
key = f"{self.PREFIX_SESSION}{session_id}"
|
|
685
|
+
payload = {
|
|
686
|
+
"session_id": session_id,
|
|
687
|
+
"created_by": credentials.agent_id,
|
|
688
|
+
"created_at": datetime.now().isoformat(),
|
|
689
|
+
"participants": [credentials.agent_id],
|
|
690
|
+
"metadata": metadata or {},
|
|
691
|
+
}
|
|
692
|
+
return self._set(key, json.dumps(payload), TTLStrategy.SESSION.value)
|
|
693
|
+
|
|
694
|
+
def join_session(
|
|
695
|
+
self,
|
|
696
|
+
session_id: str,
|
|
697
|
+
credentials: AgentCredentials,
|
|
698
|
+
) -> bool:
|
|
699
|
+
"""Join an existing session
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
session_id: Session to join
|
|
703
|
+
credentials: Joining agent
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
True if joined
|
|
707
|
+
|
|
708
|
+
"""
|
|
709
|
+
key = f"{self.PREFIX_SESSION}{session_id}"
|
|
710
|
+
raw = self._get(key)
|
|
711
|
+
|
|
712
|
+
if raw is None:
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
payload = json.loads(raw)
|
|
716
|
+
if credentials.agent_id not in payload["participants"]:
|
|
717
|
+
payload["participants"].append(credentials.agent_id)
|
|
718
|
+
|
|
719
|
+
return self._set(key, json.dumps(payload), TTLStrategy.SESSION.value)
|
|
720
|
+
|
|
721
|
+
def get_session(
|
|
722
|
+
self,
|
|
723
|
+
session_id: str,
|
|
724
|
+
credentials: AgentCredentials,
|
|
725
|
+
) -> dict | None:
|
|
726
|
+
"""Get session information
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
session_id: Session identifier
|
|
730
|
+
credentials: Any participant can read
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Session data or None
|
|
734
|
+
|
|
735
|
+
"""
|
|
736
|
+
key = f"{self.PREFIX_SESSION}{session_id}"
|
|
737
|
+
raw = self._get(key)
|
|
738
|
+
|
|
739
|
+
if raw is None:
|
|
740
|
+
return None
|
|
741
|
+
|
|
742
|
+
result: dict = json.loads(raw)
|
|
743
|
+
return result
|
|
744
|
+
|
|
745
|
+
# === Health Check ===
|
|
746
|
+
|
|
747
|
+
def ping(self) -> bool:
|
|
748
|
+
"""Check Redis connection health
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
True if connected and responsive
|
|
752
|
+
|
|
753
|
+
"""
|
|
754
|
+
if self.use_mock:
|
|
755
|
+
return True
|
|
756
|
+
if self._client is None:
|
|
757
|
+
return False
|
|
758
|
+
try:
|
|
759
|
+
return bool(self._client.ping())
|
|
760
|
+
except Exception: # noqa: BLE001
|
|
761
|
+
# INTENTIONAL: Health check is best-effort. Connection failure is non-fatal.
|
|
762
|
+
# Consumers will handle disconnection gracefully.
|
|
763
|
+
return False
|
|
764
|
+
|
|
765
|
+
def get_stats(self) -> dict:
|
|
766
|
+
"""Get memory statistics
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
Dict with memory stats
|
|
770
|
+
|
|
771
|
+
"""
|
|
772
|
+
if self.use_mock:
|
|
773
|
+
# Use generator expressions for memory-efficient counting
|
|
774
|
+
return {
|
|
775
|
+
"mode": "mock",
|
|
776
|
+
"total_keys": len(self._mock_storage),
|
|
777
|
+
"working_keys": sum(
|
|
778
|
+
1 for k in self._mock_storage if k.startswith(self.PREFIX_WORKING)
|
|
779
|
+
),
|
|
780
|
+
"staged_keys": sum(
|
|
781
|
+
1 for k in self._mock_storage if k.startswith(self.PREFIX_STAGED)
|
|
782
|
+
),
|
|
783
|
+
"conflict_keys": sum(
|
|
784
|
+
1 for k in self._mock_storage if k.startswith(self.PREFIX_CONFLICT)
|
|
785
|
+
),
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if self._client is None:
|
|
789
|
+
return {"mode": "disconnected", "error": "No Redis client"}
|
|
790
|
+
info = self._client.info("memory")
|
|
791
|
+
return {
|
|
792
|
+
"mode": "redis",
|
|
793
|
+
"used_memory": info.get("used_memory_human"),
|
|
794
|
+
"peak_memory": info.get("used_memory_peak_human"),
|
|
795
|
+
"total_keys": self._client.dbsize(),
|
|
796
|
+
"working_keys": len(self._keys(f"{self.PREFIX_WORKING}*")),
|
|
797
|
+
"staged_keys": len(self._keys(f"{self.PREFIX_STAGED}*")),
|
|
798
|
+
"conflict_keys": len(self._keys(f"{self.PREFIX_CONFLICT}*")),
|
|
799
|
+
}
|