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.
Files changed (457) hide show
  1. attune/__init__.py +358 -0
  2. attune/adaptive/__init__.py +13 -0
  3. attune/adaptive/task_complexity.py +127 -0
  4. attune/agent_monitoring.py +414 -0
  5. attune/cache/__init__.py +117 -0
  6. attune/cache/base.py +166 -0
  7. attune/cache/dependency_manager.py +256 -0
  8. attune/cache/hash_only.py +251 -0
  9. attune/cache/hybrid.py +457 -0
  10. attune/cache/storage.py +285 -0
  11. attune/cache_monitor.py +356 -0
  12. attune/cache_stats.py +298 -0
  13. attune/cli/__init__.py +152 -0
  14. attune/cli/__main__.py +12 -0
  15. attune/cli/commands/__init__.py +1 -0
  16. attune/cli/commands/batch.py +264 -0
  17. attune/cli/commands/cache.py +248 -0
  18. attune/cli/commands/help.py +331 -0
  19. attune/cli/commands/info.py +140 -0
  20. attune/cli/commands/inspect.py +436 -0
  21. attune/cli/commands/inspection.py +57 -0
  22. attune/cli/commands/memory.py +48 -0
  23. attune/cli/commands/metrics.py +92 -0
  24. attune/cli/commands/orchestrate.py +184 -0
  25. attune/cli/commands/patterns.py +207 -0
  26. attune/cli/commands/profiling.py +202 -0
  27. attune/cli/commands/provider.py +98 -0
  28. attune/cli/commands/routing.py +285 -0
  29. attune/cli/commands/setup.py +96 -0
  30. attune/cli/commands/status.py +235 -0
  31. attune/cli/commands/sync.py +166 -0
  32. attune/cli/commands/tier.py +121 -0
  33. attune/cli/commands/utilities.py +114 -0
  34. attune/cli/commands/workflow.py +579 -0
  35. attune/cli/core.py +32 -0
  36. attune/cli/parsers/__init__.py +68 -0
  37. attune/cli/parsers/batch.py +118 -0
  38. attune/cli/parsers/cache.py +65 -0
  39. attune/cli/parsers/help.py +41 -0
  40. attune/cli/parsers/info.py +26 -0
  41. attune/cli/parsers/inspect.py +66 -0
  42. attune/cli/parsers/metrics.py +42 -0
  43. attune/cli/parsers/orchestrate.py +61 -0
  44. attune/cli/parsers/patterns.py +54 -0
  45. attune/cli/parsers/provider.py +40 -0
  46. attune/cli/parsers/routing.py +110 -0
  47. attune/cli/parsers/setup.py +42 -0
  48. attune/cli/parsers/status.py +47 -0
  49. attune/cli/parsers/sync.py +31 -0
  50. attune/cli/parsers/tier.py +33 -0
  51. attune/cli/parsers/workflow.py +77 -0
  52. attune/cli/utils/__init__.py +1 -0
  53. attune/cli/utils/data.py +242 -0
  54. attune/cli/utils/helpers.py +68 -0
  55. attune/cli_legacy.py +3957 -0
  56. attune/cli_minimal.py +1159 -0
  57. attune/cli_router.py +437 -0
  58. attune/cli_unified.py +814 -0
  59. attune/config/__init__.py +66 -0
  60. attune/config/xml_config.py +286 -0
  61. attune/config.py +545 -0
  62. attune/coordination.py +870 -0
  63. attune/core.py +1511 -0
  64. attune/core_modules/__init__.py +15 -0
  65. attune/cost_tracker.py +626 -0
  66. attune/dashboard/__init__.py +41 -0
  67. attune/dashboard/app.py +512 -0
  68. attune/dashboard/simple_server.py +435 -0
  69. attune/dashboard/standalone_server.py +547 -0
  70. attune/discovery.py +306 -0
  71. attune/emergence.py +306 -0
  72. attune/exceptions.py +123 -0
  73. attune/feedback_loops.py +373 -0
  74. attune/hot_reload/README.md +473 -0
  75. attune/hot_reload/__init__.py +62 -0
  76. attune/hot_reload/config.py +83 -0
  77. attune/hot_reload/integration.py +229 -0
  78. attune/hot_reload/reloader.py +298 -0
  79. attune/hot_reload/watcher.py +183 -0
  80. attune/hot_reload/websocket.py +177 -0
  81. attune/levels.py +577 -0
  82. attune/leverage_points.py +441 -0
  83. attune/logging_config.py +261 -0
  84. attune/mcp/__init__.py +10 -0
  85. attune/mcp/server.py +506 -0
  86. attune/memory/__init__.py +237 -0
  87. attune/memory/claude_memory.py +469 -0
  88. attune/memory/config.py +224 -0
  89. attune/memory/control_panel.py +1290 -0
  90. attune/memory/control_panel_support.py +145 -0
  91. attune/memory/cross_session.py +845 -0
  92. attune/memory/edges.py +179 -0
  93. attune/memory/encryption.py +159 -0
  94. attune/memory/file_session.py +770 -0
  95. attune/memory/graph.py +570 -0
  96. attune/memory/long_term.py +913 -0
  97. attune/memory/long_term_types.py +99 -0
  98. attune/memory/mixins/__init__.py +25 -0
  99. attune/memory/mixins/backend_init_mixin.py +249 -0
  100. attune/memory/mixins/capabilities_mixin.py +208 -0
  101. attune/memory/mixins/handoff_mixin.py +208 -0
  102. attune/memory/mixins/lifecycle_mixin.py +49 -0
  103. attune/memory/mixins/long_term_mixin.py +352 -0
  104. attune/memory/mixins/promotion_mixin.py +109 -0
  105. attune/memory/mixins/short_term_mixin.py +182 -0
  106. attune/memory/nodes.py +179 -0
  107. attune/memory/redis_bootstrap.py +540 -0
  108. attune/memory/security/__init__.py +31 -0
  109. attune/memory/security/audit_logger.py +932 -0
  110. attune/memory/security/pii_scrubber.py +640 -0
  111. attune/memory/security/secrets_detector.py +678 -0
  112. attune/memory/short_term.py +2192 -0
  113. attune/memory/simple_storage.py +302 -0
  114. attune/memory/storage/__init__.py +15 -0
  115. attune/memory/storage_backend.py +167 -0
  116. attune/memory/summary_index.py +583 -0
  117. attune/memory/types.py +446 -0
  118. attune/memory/unified.py +182 -0
  119. attune/meta_workflows/__init__.py +74 -0
  120. attune/meta_workflows/agent_creator.py +248 -0
  121. attune/meta_workflows/builtin_templates.py +567 -0
  122. attune/meta_workflows/cli_commands/__init__.py +56 -0
  123. attune/meta_workflows/cli_commands/agent_commands.py +321 -0
  124. attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
  125. attune/meta_workflows/cli_commands/config_commands.py +232 -0
  126. attune/meta_workflows/cli_commands/memory_commands.py +182 -0
  127. attune/meta_workflows/cli_commands/template_commands.py +354 -0
  128. attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
  129. attune/meta_workflows/cli_meta_workflows.py +59 -0
  130. attune/meta_workflows/form_engine.py +292 -0
  131. attune/meta_workflows/intent_detector.py +409 -0
  132. attune/meta_workflows/models.py +569 -0
  133. attune/meta_workflows/pattern_learner.py +738 -0
  134. attune/meta_workflows/plan_generator.py +384 -0
  135. attune/meta_workflows/session_context.py +397 -0
  136. attune/meta_workflows/template_registry.py +229 -0
  137. attune/meta_workflows/workflow.py +984 -0
  138. attune/metrics/__init__.py +12 -0
  139. attune/metrics/collector.py +31 -0
  140. attune/metrics/prompt_metrics.py +194 -0
  141. attune/models/__init__.py +172 -0
  142. attune/models/__main__.py +13 -0
  143. attune/models/adaptive_routing.py +437 -0
  144. attune/models/auth_cli.py +444 -0
  145. attune/models/auth_strategy.py +450 -0
  146. attune/models/cli.py +655 -0
  147. attune/models/empathy_executor.py +354 -0
  148. attune/models/executor.py +257 -0
  149. attune/models/fallback.py +762 -0
  150. attune/models/provider_config.py +282 -0
  151. attune/models/registry.py +472 -0
  152. attune/models/tasks.py +359 -0
  153. attune/models/telemetry/__init__.py +71 -0
  154. attune/models/telemetry/analytics.py +594 -0
  155. attune/models/telemetry/backend.py +196 -0
  156. attune/models/telemetry/data_models.py +431 -0
  157. attune/models/telemetry/storage.py +489 -0
  158. attune/models/token_estimator.py +420 -0
  159. attune/models/validation.py +280 -0
  160. attune/monitoring/__init__.py +52 -0
  161. attune/monitoring/alerts.py +946 -0
  162. attune/monitoring/alerts_cli.py +448 -0
  163. attune/monitoring/multi_backend.py +271 -0
  164. attune/monitoring/otel_backend.py +362 -0
  165. attune/optimization/__init__.py +19 -0
  166. attune/optimization/context_optimizer.py +272 -0
  167. attune/orchestration/__init__.py +67 -0
  168. attune/orchestration/agent_templates.py +707 -0
  169. attune/orchestration/config_store.py +499 -0
  170. attune/orchestration/execution_strategies.py +2111 -0
  171. attune/orchestration/meta_orchestrator.py +1168 -0
  172. attune/orchestration/pattern_learner.py +696 -0
  173. attune/orchestration/real_tools.py +931 -0
  174. attune/pattern_cache.py +187 -0
  175. attune/pattern_library.py +542 -0
  176. attune/patterns/debugging/all_patterns.json +81 -0
  177. attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
  178. attune/patterns/refactoring_memory.json +89 -0
  179. attune/persistence.py +564 -0
  180. attune/platform_utils.py +265 -0
  181. attune/plugins/__init__.py +28 -0
  182. attune/plugins/base.py +361 -0
  183. attune/plugins/registry.py +268 -0
  184. attune/project_index/__init__.py +32 -0
  185. attune/project_index/cli.py +335 -0
  186. attune/project_index/index.py +667 -0
  187. attune/project_index/models.py +504 -0
  188. attune/project_index/reports.py +474 -0
  189. attune/project_index/scanner.py +777 -0
  190. attune/project_index/scanner_parallel.py +291 -0
  191. attune/prompts/__init__.py +61 -0
  192. attune/prompts/config.py +77 -0
  193. attune/prompts/context.py +177 -0
  194. attune/prompts/parser.py +285 -0
  195. attune/prompts/registry.py +313 -0
  196. attune/prompts/templates.py +208 -0
  197. attune/redis_config.py +302 -0
  198. attune/redis_memory.py +799 -0
  199. attune/resilience/__init__.py +56 -0
  200. attune/resilience/circuit_breaker.py +256 -0
  201. attune/resilience/fallback.py +179 -0
  202. attune/resilience/health.py +300 -0
  203. attune/resilience/retry.py +209 -0
  204. attune/resilience/timeout.py +135 -0
  205. attune/routing/__init__.py +43 -0
  206. attune/routing/chain_executor.py +433 -0
  207. attune/routing/classifier.py +217 -0
  208. attune/routing/smart_router.py +234 -0
  209. attune/routing/workflow_registry.py +343 -0
  210. attune/scaffolding/README.md +589 -0
  211. attune/scaffolding/__init__.py +35 -0
  212. attune/scaffolding/__main__.py +14 -0
  213. attune/scaffolding/cli.py +240 -0
  214. attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
  215. attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
  216. attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
  217. attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
  218. attune/socratic/__init__.py +256 -0
  219. attune/socratic/ab_testing.py +958 -0
  220. attune/socratic/blueprint.py +533 -0
  221. attune/socratic/cli.py +703 -0
  222. attune/socratic/collaboration.py +1114 -0
  223. attune/socratic/domain_templates.py +924 -0
  224. attune/socratic/embeddings.py +738 -0
  225. attune/socratic/engine.py +794 -0
  226. attune/socratic/explainer.py +682 -0
  227. attune/socratic/feedback.py +772 -0
  228. attune/socratic/forms.py +629 -0
  229. attune/socratic/generator.py +732 -0
  230. attune/socratic/llm_analyzer.py +637 -0
  231. attune/socratic/mcp_server.py +702 -0
  232. attune/socratic/session.py +312 -0
  233. attune/socratic/storage.py +667 -0
  234. attune/socratic/success.py +730 -0
  235. attune/socratic/visual_editor.py +860 -0
  236. attune/socratic/web_ui.py +958 -0
  237. attune/telemetry/__init__.py +39 -0
  238. attune/telemetry/agent_coordination.py +475 -0
  239. attune/telemetry/agent_tracking.py +367 -0
  240. attune/telemetry/approval_gates.py +545 -0
  241. attune/telemetry/cli.py +1231 -0
  242. attune/telemetry/commands/__init__.py +14 -0
  243. attune/telemetry/commands/dashboard_commands.py +696 -0
  244. attune/telemetry/event_streaming.py +409 -0
  245. attune/telemetry/feedback_loop.py +567 -0
  246. attune/telemetry/usage_tracker.py +591 -0
  247. attune/templates.py +754 -0
  248. attune/test_generator/__init__.py +38 -0
  249. attune/test_generator/__main__.py +14 -0
  250. attune/test_generator/cli.py +234 -0
  251. attune/test_generator/generator.py +355 -0
  252. attune/test_generator/risk_analyzer.py +216 -0
  253. attune/test_generator/templates/unit_test.py.jinja2 +272 -0
  254. attune/tier_recommender.py +384 -0
  255. attune/tools.py +183 -0
  256. attune/trust/__init__.py +28 -0
  257. attune/trust/circuit_breaker.py +579 -0
  258. attune/trust_building.py +527 -0
  259. attune/validation/__init__.py +19 -0
  260. attune/validation/xml_validator.py +281 -0
  261. attune/vscode_bridge.py +173 -0
  262. attune/workflow_commands.py +780 -0
  263. attune/workflow_patterns/__init__.py +33 -0
  264. attune/workflow_patterns/behavior.py +249 -0
  265. attune/workflow_patterns/core.py +76 -0
  266. attune/workflow_patterns/output.py +99 -0
  267. attune/workflow_patterns/registry.py +255 -0
  268. attune/workflow_patterns/structural.py +288 -0
  269. attune/workflows/__init__.py +539 -0
  270. attune/workflows/autonomous_test_gen.py +1268 -0
  271. attune/workflows/base.py +2667 -0
  272. attune/workflows/batch_processing.py +342 -0
  273. attune/workflows/bug_predict.py +1084 -0
  274. attune/workflows/builder.py +273 -0
  275. attune/workflows/caching.py +253 -0
  276. attune/workflows/code_review.py +1048 -0
  277. attune/workflows/code_review_adapters.py +312 -0
  278. attune/workflows/code_review_pipeline.py +722 -0
  279. attune/workflows/config.py +645 -0
  280. attune/workflows/dependency_check.py +644 -0
  281. attune/workflows/document_gen/__init__.py +25 -0
  282. attune/workflows/document_gen/config.py +30 -0
  283. attune/workflows/document_gen/report_formatter.py +162 -0
  284. attune/workflows/document_gen/workflow.py +1426 -0
  285. attune/workflows/document_manager.py +216 -0
  286. attune/workflows/document_manager_README.md +134 -0
  287. attune/workflows/documentation_orchestrator.py +1205 -0
  288. attune/workflows/history.py +510 -0
  289. attune/workflows/keyboard_shortcuts/__init__.py +39 -0
  290. attune/workflows/keyboard_shortcuts/generators.py +391 -0
  291. attune/workflows/keyboard_shortcuts/parsers.py +416 -0
  292. attune/workflows/keyboard_shortcuts/prompts.py +295 -0
  293. attune/workflows/keyboard_shortcuts/schema.py +193 -0
  294. attune/workflows/keyboard_shortcuts/workflow.py +509 -0
  295. attune/workflows/llm_base.py +363 -0
  296. attune/workflows/manage_docs.py +87 -0
  297. attune/workflows/manage_docs_README.md +134 -0
  298. attune/workflows/manage_documentation.py +821 -0
  299. attune/workflows/new_sample_workflow1.py +149 -0
  300. attune/workflows/new_sample_workflow1_README.md +150 -0
  301. attune/workflows/orchestrated_health_check.py +849 -0
  302. attune/workflows/orchestrated_release_prep.py +600 -0
  303. attune/workflows/output.py +413 -0
  304. attune/workflows/perf_audit.py +863 -0
  305. attune/workflows/pr_review.py +762 -0
  306. attune/workflows/progress.py +785 -0
  307. attune/workflows/progress_server.py +322 -0
  308. attune/workflows/progressive/README 2.md +454 -0
  309. attune/workflows/progressive/README.md +454 -0
  310. attune/workflows/progressive/__init__.py +82 -0
  311. attune/workflows/progressive/cli.py +219 -0
  312. attune/workflows/progressive/core.py +488 -0
  313. attune/workflows/progressive/orchestrator.py +723 -0
  314. attune/workflows/progressive/reports.py +520 -0
  315. attune/workflows/progressive/telemetry.py +274 -0
  316. attune/workflows/progressive/test_gen.py +495 -0
  317. attune/workflows/progressive/workflow.py +589 -0
  318. attune/workflows/refactor_plan.py +694 -0
  319. attune/workflows/release_prep.py +895 -0
  320. attune/workflows/release_prep_crew.py +969 -0
  321. attune/workflows/research_synthesis.py +404 -0
  322. attune/workflows/routing.py +168 -0
  323. attune/workflows/secure_release.py +593 -0
  324. attune/workflows/security_adapters.py +297 -0
  325. attune/workflows/security_audit.py +1329 -0
  326. attune/workflows/security_audit_phase3.py +355 -0
  327. attune/workflows/seo_optimization.py +633 -0
  328. attune/workflows/step_config.py +234 -0
  329. attune/workflows/telemetry_mixin.py +269 -0
  330. attune/workflows/test5.py +125 -0
  331. attune/workflows/test5_README.md +158 -0
  332. attune/workflows/test_coverage_boost_crew.py +849 -0
  333. attune/workflows/test_gen/__init__.py +52 -0
  334. attune/workflows/test_gen/ast_analyzer.py +249 -0
  335. attune/workflows/test_gen/config.py +88 -0
  336. attune/workflows/test_gen/data_models.py +38 -0
  337. attune/workflows/test_gen/report_formatter.py +289 -0
  338. attune/workflows/test_gen/test_templates.py +381 -0
  339. attune/workflows/test_gen/workflow.py +655 -0
  340. attune/workflows/test_gen.py +54 -0
  341. attune/workflows/test_gen_behavioral.py +477 -0
  342. attune/workflows/test_gen_parallel.py +341 -0
  343. attune/workflows/test_lifecycle.py +526 -0
  344. attune/workflows/test_maintenance.py +627 -0
  345. attune/workflows/test_maintenance_cli.py +590 -0
  346. attune/workflows/test_maintenance_crew.py +840 -0
  347. attune/workflows/test_runner.py +622 -0
  348. attune/workflows/tier_tracking.py +531 -0
  349. attune/workflows/xml_enhanced_crew.py +285 -0
  350. attune_ai-2.0.0.dist-info/METADATA +1026 -0
  351. attune_ai-2.0.0.dist-info/RECORD +457 -0
  352. attune_ai-2.0.0.dist-info/WHEEL +5 -0
  353. attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
  354. attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
  355. attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
  356. attune_ai-2.0.0.dist-info/top_level.txt +5 -0
  357. attune_healthcare/__init__.py +13 -0
  358. attune_healthcare/monitors/__init__.py +9 -0
  359. attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
  360. attune_healthcare/monitors/monitoring/__init__.py +44 -0
  361. attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
  362. attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
  363. attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
  364. attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
  365. attune_llm/README.md +553 -0
  366. attune_llm/__init__.py +28 -0
  367. attune_llm/agent_factory/__init__.py +53 -0
  368. attune_llm/agent_factory/adapters/__init__.py +85 -0
  369. attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
  370. attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
  371. attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
  372. attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
  373. attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
  374. attune_llm/agent_factory/adapters/native.py +228 -0
  375. attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
  376. attune_llm/agent_factory/base.py +305 -0
  377. attune_llm/agent_factory/crews/__init__.py +67 -0
  378. attune_llm/agent_factory/crews/code_review.py +1113 -0
  379. attune_llm/agent_factory/crews/health_check.py +1262 -0
  380. attune_llm/agent_factory/crews/refactoring.py +1128 -0
  381. attune_llm/agent_factory/crews/security_audit.py +1018 -0
  382. attune_llm/agent_factory/decorators.py +287 -0
  383. attune_llm/agent_factory/factory.py +558 -0
  384. attune_llm/agent_factory/framework.py +193 -0
  385. attune_llm/agent_factory/memory_integration.py +328 -0
  386. attune_llm/agent_factory/resilient.py +320 -0
  387. attune_llm/agents_md/__init__.py +22 -0
  388. attune_llm/agents_md/loader.py +218 -0
  389. attune_llm/agents_md/parser.py +271 -0
  390. attune_llm/agents_md/registry.py +307 -0
  391. attune_llm/claude_memory.py +466 -0
  392. attune_llm/cli/__init__.py +8 -0
  393. attune_llm/cli/sync_claude.py +487 -0
  394. attune_llm/code_health.py +1313 -0
  395. attune_llm/commands/__init__.py +51 -0
  396. attune_llm/commands/context.py +375 -0
  397. attune_llm/commands/loader.py +301 -0
  398. attune_llm/commands/models.py +231 -0
  399. attune_llm/commands/parser.py +371 -0
  400. attune_llm/commands/registry.py +429 -0
  401. attune_llm/config/__init__.py +29 -0
  402. attune_llm/config/unified.py +291 -0
  403. attune_llm/context/__init__.py +22 -0
  404. attune_llm/context/compaction.py +455 -0
  405. attune_llm/context/manager.py +434 -0
  406. attune_llm/contextual_patterns.py +361 -0
  407. attune_llm/core.py +907 -0
  408. attune_llm/git_pattern_extractor.py +435 -0
  409. attune_llm/hooks/__init__.py +24 -0
  410. attune_llm/hooks/config.py +306 -0
  411. attune_llm/hooks/executor.py +289 -0
  412. attune_llm/hooks/registry.py +302 -0
  413. attune_llm/hooks/scripts/__init__.py +39 -0
  414. attune_llm/hooks/scripts/evaluate_session.py +201 -0
  415. attune_llm/hooks/scripts/first_time_init.py +285 -0
  416. attune_llm/hooks/scripts/pre_compact.py +207 -0
  417. attune_llm/hooks/scripts/session_end.py +183 -0
  418. attune_llm/hooks/scripts/session_start.py +163 -0
  419. attune_llm/hooks/scripts/suggest_compact.py +225 -0
  420. attune_llm/learning/__init__.py +30 -0
  421. attune_llm/learning/evaluator.py +438 -0
  422. attune_llm/learning/extractor.py +514 -0
  423. attune_llm/learning/storage.py +560 -0
  424. attune_llm/levels.py +227 -0
  425. attune_llm/pattern_confidence.py +414 -0
  426. attune_llm/pattern_resolver.py +272 -0
  427. attune_llm/pattern_summary.py +350 -0
  428. attune_llm/providers.py +967 -0
  429. attune_llm/routing/__init__.py +32 -0
  430. attune_llm/routing/model_router.py +362 -0
  431. attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
  432. attune_llm/security/PHASE2_COMPLETE.md +384 -0
  433. attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  434. attune_llm/security/QUICK_REFERENCE.md +316 -0
  435. attune_llm/security/README.md +262 -0
  436. attune_llm/security/__init__.py +62 -0
  437. attune_llm/security/audit_logger.py +929 -0
  438. attune_llm/security/audit_logger_example.py +152 -0
  439. attune_llm/security/pii_scrubber.py +640 -0
  440. attune_llm/security/secrets_detector.py +678 -0
  441. attune_llm/security/secrets_detector_example.py +304 -0
  442. attune_llm/security/secure_memdocs.py +1192 -0
  443. attune_llm/security/secure_memdocs_example.py +278 -0
  444. attune_llm/session_status.py +745 -0
  445. attune_llm/state.py +246 -0
  446. attune_llm/utils/__init__.py +5 -0
  447. attune_llm/utils/tokens.py +349 -0
  448. attune_software/SOFTWARE_PLUGIN_README.md +57 -0
  449. attune_software/__init__.py +13 -0
  450. attune_software/cli/__init__.py +120 -0
  451. attune_software/cli/inspect.py +362 -0
  452. attune_software/cli.py +574 -0
  453. attune_software/plugin.py +188 -0
  454. workflow_scaffolding/__init__.py +11 -0
  455. workflow_scaffolding/__main__.py +12 -0
  456. workflow_scaffolding/cli.py +206 -0
  457. 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
+ }