attune-ai 2.1.5__py3-none-any.whl → 2.2.1__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 (125) hide show
  1. attune/cli/__init__.py +3 -59
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +8 -16
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +9 -3
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/dashboard/app.py +3 -1
  20. attune/dashboard/simple_server.py +3 -1
  21. attune/dashboard/standalone_server.py +7 -3
  22. attune/mcp/server.py +54 -102
  23. attune/memory/long_term.py +0 -2
  24. attune/memory/short_term/__init__.py +84 -0
  25. attune/memory/short_term/base.py +465 -0
  26. attune/memory/short_term/batch.py +219 -0
  27. attune/memory/short_term/caching.py +227 -0
  28. attune/memory/short_term/conflicts.py +265 -0
  29. attune/memory/short_term/cross_session.py +122 -0
  30. attune/memory/short_term/facade.py +653 -0
  31. attune/memory/short_term/pagination.py +207 -0
  32. attune/memory/short_term/patterns.py +271 -0
  33. attune/memory/short_term/pubsub.py +286 -0
  34. attune/memory/short_term/queues.py +244 -0
  35. attune/memory/short_term/security.py +300 -0
  36. attune/memory/short_term/sessions.py +250 -0
  37. attune/memory/short_term/streams.py +242 -0
  38. attune/memory/short_term/timelines.py +234 -0
  39. attune/memory/short_term/transactions.py +184 -0
  40. attune/memory/short_term/working.py +252 -0
  41. attune/meta_workflows/cli_commands/__init__.py +3 -0
  42. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  43. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  44. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  45. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  48. attune/meta_workflows/plan_generator.py +2 -4
  49. attune/models/adaptive_routing.py +4 -8
  50. attune/models/auth_cli.py +3 -9
  51. attune/models/auth_strategy.py +2 -4
  52. attune/models/telemetry/analytics.py +0 -2
  53. attune/models/telemetry/backend.py +0 -3
  54. attune/models/telemetry/storage.py +0 -2
  55. attune/monitoring/alerts.py +6 -10
  56. attune/orchestration/_strategies/__init__.py +156 -0
  57. attune/orchestration/_strategies/base.py +227 -0
  58. attune/orchestration/_strategies/conditional_strategies.py +365 -0
  59. attune/orchestration/_strategies/conditions.py +369 -0
  60. attune/orchestration/_strategies/core_strategies.py +479 -0
  61. attune/orchestration/_strategies/data_classes.py +64 -0
  62. attune/orchestration/_strategies/nesting.py +233 -0
  63. attune/orchestration/execution_strategies.py +58 -1567
  64. attune/orchestration/meta_orchestrator.py +1 -3
  65. attune/project_index/scanner.py +1 -3
  66. attune/project_index/scanner_parallel.py +7 -5
  67. attune/socratic/storage.py +2 -4
  68. attune/socratic_router.py +1 -3
  69. attune/telemetry/agent_coordination.py +9 -3
  70. attune/telemetry/agent_tracking.py +16 -3
  71. attune/telemetry/approval_gates.py +22 -5
  72. attune/telemetry/cli.py +1 -3
  73. attune/telemetry/commands/dashboard_commands.py +24 -8
  74. attune/telemetry/event_streaming.py +8 -2
  75. attune/telemetry/feedback_loop.py +10 -2
  76. attune/tools.py +2 -1
  77. attune/workflow_commands.py +1 -3
  78. attune/workflow_patterns/structural.py +4 -8
  79. attune/workflows/__init__.py +54 -10
  80. attune/workflows/autonomous_test_gen.py +158 -102
  81. attune/workflows/base.py +48 -672
  82. attune/workflows/batch_processing.py +1 -3
  83. attune/workflows/compat.py +156 -0
  84. attune/workflows/cost_mixin.py +141 -0
  85. attune/workflows/data_classes.py +92 -0
  86. attune/workflows/document_gen/workflow.py +11 -14
  87. attune/workflows/history.py +16 -9
  88. attune/workflows/llm_base.py +1 -3
  89. attune/workflows/migration.py +432 -0
  90. attune/workflows/output.py +2 -7
  91. attune/workflows/parsing_mixin.py +427 -0
  92. attune/workflows/perf_audit.py +3 -1
  93. attune/workflows/progress.py +9 -11
  94. attune/workflows/release_prep.py +5 -1
  95. attune/workflows/routing.py +0 -2
  96. attune/workflows/secure_release.py +4 -1
  97. attune/workflows/security_audit.py +20 -14
  98. attune/workflows/security_audit_phase3.py +28 -22
  99. attune/workflows/seo_optimization.py +27 -27
  100. attune/workflows/test_gen/test_templates.py +1 -4
  101. attune/workflows/test_gen/workflow.py +0 -2
  102. attune/workflows/test_gen_behavioral.py +6 -19
  103. attune/workflows/test_gen_parallel.py +8 -6
  104. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/METADATA +4 -3
  105. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/RECORD +121 -96
  106. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/entry_points.txt +0 -2
  107. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  108. attune_llm/agent_factory/__init__.py +6 -6
  109. attune_llm/agent_factory/adapters/haystack_adapter.py +1 -4
  110. attune_llm/commands/__init__.py +10 -10
  111. attune_llm/commands/models.py +3 -3
  112. attune_llm/config/__init__.py +8 -8
  113. attune_llm/learning/__init__.py +3 -3
  114. attune_llm/learning/extractor.py +5 -3
  115. attune_llm/learning/storage.py +5 -3
  116. attune_llm/security/__init__.py +17 -17
  117. attune_llm/utils/tokens.py +3 -1
  118. attune/cli_legacy.py +0 -3978
  119. attune/memory/short_term.py +0 -2192
  120. attune/workflows/manage_docs.py +0 -87
  121. attune/workflows/test5.py +0 -125
  122. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/WHEEL +0 -0
  123. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE +0 -0
  124. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  125. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,227 @@
1
+ """Local LRU cache layer for Redis operations.
2
+
3
+ This module provides a two-tier caching strategy:
4
+ 1. Local in-memory LRU cache (fast, limited size)
5
+ 2. Redis (persistent, shared across processes)
6
+
7
+ The local cache reduces Redis round-trips for frequently accessed data.
8
+ Benchmark: reduces latency from 37ms (Redis) to <0.001ms (local cache).
9
+
10
+ Classes:
11
+ CacheManager: LRU cache with TTL support
12
+
13
+ Example:
14
+ >>> from attune.memory.short_term.caching import CacheManager
15
+ >>> cache = CacheManager(enabled=True, max_size=1000)
16
+ >>> cache.add("key", "value")
17
+ >>> cache.get("key")
18
+ 'value'
19
+ >>> cache.get_stats()
20
+ {'enabled': True, 'size': 1, 'max_size': 1000, 'hits': 1, ...}
21
+
22
+ Copyright 2025 Smart-AI-Memory
23
+ Licensed under Fair Source License 0.9
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import time
29
+ from typing import TYPE_CHECKING
30
+
31
+ import structlog
32
+
33
+ if TYPE_CHECKING:
34
+ pass
35
+
36
+ logger = structlog.get_logger(__name__)
37
+
38
+
39
+ class CacheManager:
40
+ """Local LRU cache manager for two-tier caching.
41
+
42
+ Provides fast local caching with LRU eviction to reduce
43
+ Redis/mock storage round-trips for frequently accessed keys.
44
+
45
+ The cache stores tuples of (value, timestamp, last_access) for
46
+ each key, enabling both TTL expiration and LRU eviction.
47
+
48
+ Attributes:
49
+ enabled: Whether caching is active
50
+ max_size: Maximum number of entries to cache
51
+ hits: Total cache hits
52
+ misses: Total cache misses
53
+
54
+ Example:
55
+ >>> cache = CacheManager(enabled=True, max_size=100)
56
+ >>> cache.add("user:123", '{"name": "Alice"}')
57
+ >>> value = cache.get("user:123")
58
+ >>> stats = cache.get_stats()
59
+ >>> print(f"Hit rate: {stats['hit_rate']:.1f}%")
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ enabled: bool = True,
65
+ max_size: int = 1000,
66
+ ) -> None:
67
+ """Initialize cache manager.
68
+
69
+ Args:
70
+ enabled: Whether local caching is enabled
71
+ max_size: Maximum number of entries to cache (LRU eviction)
72
+ """
73
+ self.enabled = enabled
74
+ self.max_size = max_size
75
+
76
+ # Cache storage: key -> (value, timestamp, last_access)
77
+ self._cache: dict[str, tuple[str, float, float]] = {}
78
+ self._hits = 0
79
+ self._misses = 0
80
+
81
+ @property
82
+ def hits(self) -> int:
83
+ """Total cache hits."""
84
+ return self._hits
85
+
86
+ @property
87
+ def misses(self) -> int:
88
+ """Total cache misses."""
89
+ return self._misses
90
+
91
+ def get(self, key: str) -> str | None:
92
+ """Get value from local cache.
93
+
94
+ Updates last_access time for LRU tracking on hit.
95
+ Increments hit/miss counters for statistics.
96
+
97
+ Args:
98
+ key: Cache key to retrieve
99
+
100
+ Returns:
101
+ Cached value or None if not found or disabled
102
+ """
103
+ if not self.enabled:
104
+ self._misses += 1
105
+ return None
106
+
107
+ if key in self._cache:
108
+ value, timestamp, _last_access = self._cache[key]
109
+ now = time.time()
110
+ # Update last access time for LRU
111
+ self._cache[key] = (value, timestamp, now)
112
+ self._hits += 1
113
+ return value
114
+
115
+ self._misses += 1
116
+ return None
117
+
118
+ def add(self, key: str, value: str) -> None:
119
+ """Add entry to local cache with LRU eviction.
120
+
121
+ If cache is at max capacity, evicts the least recently
122
+ accessed entry before adding the new one.
123
+
124
+ Args:
125
+ key: Cache key
126
+ value: Value to cache
127
+ """
128
+ if not self.enabled:
129
+ return
130
+
131
+ now = time.time()
132
+
133
+ # Evict oldest entry if cache is full
134
+ if len(self._cache) >= self.max_size:
135
+ # Find key with oldest last_access time (LRU)
136
+ oldest_key = min(self._cache, key=lambda k: self._cache[k][2])
137
+ del self._cache[oldest_key]
138
+
139
+ # Add new entry: (value, timestamp, last_access)
140
+ self._cache[key] = (value, now, now)
141
+
142
+ def remove(self, key: str) -> bool:
143
+ """Remove entry from cache.
144
+
145
+ Args:
146
+ key: Cache key to remove
147
+
148
+ Returns:
149
+ True if key was present and removed
150
+ """
151
+ if key in self._cache:
152
+ del self._cache[key]
153
+ return True
154
+ return False
155
+
156
+ def invalidate(self, key: str) -> bool:
157
+ """Invalidate (remove) entry from cache.
158
+
159
+ Alias for remove() to match common cache API terminology.
160
+
161
+ Args:
162
+ key: Cache key to invalidate
163
+
164
+ Returns:
165
+ True if key was present and invalidated
166
+ """
167
+ return self.remove(key)
168
+
169
+ def contains(self, key: str) -> bool:
170
+ """Check if key exists in cache (without updating access time).
171
+
172
+ Args:
173
+ key: Cache key to check
174
+
175
+ Returns:
176
+ True if key exists in cache
177
+ """
178
+ return self.enabled and key in self._cache
179
+
180
+ def clear(self) -> int:
181
+ """Clear all entries from local cache.
182
+
183
+ Resets hit/miss counters to zero.
184
+
185
+ Returns:
186
+ Number of entries cleared
187
+ """
188
+ count = len(self._cache)
189
+ self._cache.clear()
190
+ self._hits = 0
191
+ self._misses = 0
192
+ logger.info("local_cache_cleared", entries_cleared=count)
193
+ return count
194
+
195
+ def get_stats(self) -> dict:
196
+ """Get local cache performance statistics.
197
+
198
+ Returns:
199
+ Dict with cache stats including:
200
+ - enabled: Whether caching is active
201
+ - size: Current number of cached entries
202
+ - max_size: Maximum cache capacity
203
+ - hits: Total cache hits
204
+ - misses: Total cache misses
205
+ - hit_rate: Percentage of requests served from cache
206
+ - total_requests: Total get requests
207
+ """
208
+ total = self._hits + self._misses
209
+ hit_rate = (self._hits / total * 100) if total > 0 else 0.0
210
+
211
+ return {
212
+ "enabled": self.enabled,
213
+ "size": len(self._cache),
214
+ "max_size": self.max_size,
215
+ "hits": self._hits,
216
+ "misses": self._misses,
217
+ "hit_rate": hit_rate,
218
+ "total_requests": total,
219
+ }
220
+
221
+ def __len__(self) -> int:
222
+ """Return number of cached entries."""
223
+ return len(self._cache)
224
+
225
+ def __contains__(self, key: str) -> bool:
226
+ """Support 'in' operator."""
227
+ return self.contains(key)
@@ -0,0 +1,265 @@
1
+ """Conflict negotiation for multi-agent collaboration.
2
+
3
+ This module provides principled negotiation support per "Getting to Yes":
4
+ - Separate positions from interests
5
+ - Define BATNA before negotiating
6
+ - Track resolution outcomes
7
+
8
+ Key Prefix: PREFIX_CONFLICT = "empathy:conflict:"
9
+
10
+ Classes:
11
+ ConflictNegotiation: Conflict context and resolution operations
12
+
13
+ Example:
14
+ >>> from attune.memory.short_term.conflicts import ConflictNegotiation
15
+ >>> from attune.memory.types import AgentCredentials, AccessTier
16
+ >>> negotiation = ConflictNegotiation(base_ops)
17
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
18
+ >>> context = negotiation.create_conflict_context(
19
+ ... "conflict_1",
20
+ ... positions={"agent_1": "Use Redis", "agent_2": "Use SQLite"},
21
+ ... interests={"agent_1": ["speed", "scale"], "agent_2": ["simplicity"]},
22
+ ... credentials=creds,
23
+ ... )
24
+
25
+ Copyright 2025 Smart-AI-Memory
26
+ Licensed under Fair Source License 0.9
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from typing import TYPE_CHECKING, Any
33
+
34
+ import structlog
35
+
36
+ from attune.memory.types import (
37
+ AgentCredentials,
38
+ ConflictContext,
39
+ TTLStrategy,
40
+ )
41
+
42
+ if TYPE_CHECKING:
43
+ from attune.memory.short_term.base import BaseOperations
44
+
45
+ logger = structlog.get_logger(__name__)
46
+
47
+
48
+ class ConflictNegotiation:
49
+ """Conflict context and resolution operations.
50
+
51
+ Implements principled negotiation per "Getting to Yes" framework:
52
+ - Separate positions from interests
53
+ - Define BATNA (Best Alternative to Negotiated Agreement)
54
+ - Track resolution outcomes for learning
55
+
56
+ The class is designed to be composed with BaseOperations
57
+ for dependency injection.
58
+
59
+ Attributes:
60
+ PREFIX_CONFLICT: Key prefix for conflict context namespace
61
+
62
+ Example:
63
+ >>> negotiation = ConflictNegotiation(base_ops)
64
+ >>> creds = AgentCredentials("agent_1", AccessTier.CONTRIBUTOR)
65
+ >>> context = negotiation.create_conflict_context(...)
66
+ >>> negotiation.resolve_conflict("conflict_1", "Chose Redis", validator_creds)
67
+ """
68
+
69
+ PREFIX_CONFLICT = "empathy:conflict:"
70
+
71
+ def __init__(self, base: BaseOperations) -> None:
72
+ """Initialize conflict negotiation operations.
73
+
74
+ Args:
75
+ base: BaseOperations instance for storage access
76
+ """
77
+ self._base = base
78
+
79
+ def create_conflict_context(
80
+ self,
81
+ conflict_id: str,
82
+ positions: dict[str, Any],
83
+ interests: dict[str, list[str]],
84
+ credentials: AgentCredentials,
85
+ batna: str | None = None,
86
+ ) -> ConflictContext:
87
+ """Create context for principled negotiation.
88
+
89
+ Per Getting to Yes framework:
90
+ - Separate positions from interests
91
+ - Define BATNA before negotiating
92
+
93
+ Args:
94
+ conflict_id: Unique conflict identifier
95
+ positions: agent_id -> their stated position
96
+ interests: agent_id -> underlying interests
97
+ credentials: Must be CONTRIBUTOR or higher
98
+ batna: Best Alternative to Negotiated Agreement
99
+
100
+ Returns:
101
+ ConflictContext for resolution
102
+
103
+ Raises:
104
+ ValueError: If conflict_id is empty
105
+ TypeError: If positions or interests are not dicts
106
+ PermissionError: If credentials lack permission
107
+
108
+ Example:
109
+ >>> context = negotiation.create_conflict_context(
110
+ ... "conflict_1",
111
+ ... positions={"a1": "Redis", "a2": "SQLite"},
112
+ ... interests={"a1": ["speed"], "a2": ["simplicity"]},
113
+ ... credentials=creds,
114
+ ... batna="Use file-based storage",
115
+ ... )
116
+ """
117
+ # Pattern 1: String ID validation
118
+ if not conflict_id or not conflict_id.strip():
119
+ raise ValueError(f"conflict_id cannot be empty. Got: {conflict_id!r}")
120
+
121
+ # Pattern 5: Type validation
122
+ if not isinstance(positions, dict):
123
+ raise TypeError(f"positions must be dict, got {type(positions).__name__}")
124
+ if not isinstance(interests, dict):
125
+ raise TypeError(f"interests must be dict, got {type(interests).__name__}")
126
+
127
+ if not credentials.can_stage():
128
+ raise PermissionError(
129
+ f"Agent {credentials.agent_id} cannot create conflict context. "
130
+ "Requires CONTRIBUTOR tier or higher.",
131
+ )
132
+
133
+ context = ConflictContext(
134
+ conflict_id=conflict_id,
135
+ positions=positions,
136
+ interests=interests,
137
+ batna=batna,
138
+ )
139
+
140
+ key = f"{self.PREFIX_CONFLICT}{conflict_id}"
141
+ self._base._set(
142
+ key,
143
+ json.dumps(context.to_dict()),
144
+ TTLStrategy.CONFLICT_CONTEXT.value,
145
+ )
146
+
147
+ logger.info(
148
+ "conflict_context_created",
149
+ conflict_id=conflict_id,
150
+ agent_count=len(positions),
151
+ has_batna=batna is not None,
152
+ )
153
+
154
+ return context
155
+
156
+ def get_conflict_context(
157
+ self,
158
+ conflict_id: str,
159
+ credentials: AgentCredentials,
160
+ ) -> ConflictContext | None:
161
+ """Retrieve conflict context.
162
+
163
+ Args:
164
+ conflict_id: Conflict identifier
165
+ credentials: Any tier can read
166
+
167
+ Returns:
168
+ ConflictContext or None if not found
169
+
170
+ Raises:
171
+ ValueError: If conflict_id is empty
172
+
173
+ Example:
174
+ >>> context = negotiation.get_conflict_context("conflict_1", creds)
175
+ >>> if context:
176
+ ... print(f"BATNA: {context.batna}")
177
+ """
178
+ # Pattern 1: String ID validation
179
+ if not conflict_id or not conflict_id.strip():
180
+ raise ValueError(f"conflict_id cannot be empty. Got: {conflict_id!r}")
181
+
182
+ key = f"{self.PREFIX_CONFLICT}{conflict_id}"
183
+ raw = self._base._get(key)
184
+
185
+ if raw is None:
186
+ return None
187
+
188
+ return ConflictContext.from_dict(json.loads(raw))
189
+
190
+ def resolve_conflict(
191
+ self,
192
+ conflict_id: str,
193
+ resolution: str,
194
+ credentials: AgentCredentials,
195
+ ) -> bool:
196
+ """Mark conflict as resolved.
197
+
198
+ Args:
199
+ conflict_id: Conflict to resolve
200
+ resolution: How it was resolved
201
+ credentials: Must be VALIDATOR or higher
202
+
203
+ Returns:
204
+ True if resolved
205
+
206
+ Raises:
207
+ PermissionError: If credentials lack validation access
208
+
209
+ Example:
210
+ >>> negotiation.resolve_conflict(
211
+ ... "conflict_1",
212
+ ... "Chose Redis for better scaling",
213
+ ... validator_creds,
214
+ ... )
215
+ True
216
+ """
217
+ if not credentials.can_validate():
218
+ raise PermissionError(
219
+ f"Agent {credentials.agent_id} cannot resolve conflicts. "
220
+ "Requires VALIDATOR tier or higher.",
221
+ )
222
+
223
+ context = self.get_conflict_context(conflict_id, credentials)
224
+ if context is None:
225
+ return False
226
+
227
+ context.resolved = True
228
+ context.resolution = resolution
229
+
230
+ key = f"{self.PREFIX_CONFLICT}{conflict_id}"
231
+ # Keep resolved conflicts longer for audit
232
+ self._base._set(key, json.dumps(context.to_dict()), TTLStrategy.CONFLICT_CONTEXT.value)
233
+
234
+ logger.info(
235
+ "conflict_resolved",
236
+ conflict_id=conflict_id,
237
+ agent_id=credentials.agent_id,
238
+ )
239
+
240
+ return True
241
+
242
+ def list_active_conflicts(
243
+ self,
244
+ credentials: AgentCredentials,
245
+ ) -> list[ConflictContext]:
246
+ """List all active (unresolved) conflicts.
247
+
248
+ Args:
249
+ credentials: Any tier can read
250
+
251
+ Returns:
252
+ List of unresolved conflict contexts
253
+ """
254
+ pattern = f"{self.PREFIX_CONFLICT}*"
255
+ keys = self._base._keys(pattern)
256
+ conflicts = []
257
+
258
+ for key in keys:
259
+ raw = self._base._get(key)
260
+ if raw:
261
+ context = ConflictContext.from_dict(json.loads(raw))
262
+ if not context.resolved:
263
+ conflicts.append(context)
264
+
265
+ return conflicts
@@ -0,0 +1,122 @@
1
+ """Cross-session coordination for distributed agents.
2
+
3
+ This module enables coordination across multiple sessions:
4
+ - Enable cross-session communication
5
+ - Check cross-session availability
6
+ - Coordinate distributed agent activities
7
+
8
+ Use Cases:
9
+ - Multi-process agent coordination
10
+ - Distributed workflow execution
11
+ - Shared state across sessions
12
+
13
+ Classes:
14
+ CrossSessionManager: Cross-session coordination operations
15
+
16
+ Example:
17
+ >>> from attune.memory.short_term.cross_session import CrossSessionManager
18
+ >>> from attune.memory.types import AccessTier
19
+ >>> cross_session = CrossSessionManager(base_ops)
20
+ >>> if cross_session.available():
21
+ ... coordinator = cross_session.enable(AccessTier.CONTRIBUTOR)
22
+ ... print(f"Session ID: {coordinator.agent_id}")
23
+
24
+ Copyright 2025 Smart-AI-Memory
25
+ Licensed under Fair Source License 0.9
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import TYPE_CHECKING, Any
31
+
32
+ import structlog
33
+
34
+ from attune.memory.types import AccessTier
35
+
36
+ if TYPE_CHECKING:
37
+ from attune.memory.short_term.base import BaseOperations
38
+
39
+ logger = structlog.get_logger(__name__)
40
+
41
+
42
+ class CrossSessionManager:
43
+ """Cross-session coordination operations.
44
+
45
+ Provides coordination capabilities for agents running in
46
+ different Claude Code sessions using Redis as the coordination
47
+ backbone.
48
+
49
+ The class is designed to be composed with BaseOperations
50
+ for dependency injection.
51
+
52
+ Example:
53
+ >>> cross_session = CrossSessionManager(base_ops)
54
+ >>> if cross_session.available():
55
+ ... coordinator = cross_session.enable(AccessTier.CONTRIBUTOR)
56
+ ... sessions = coordinator.get_active_sessions()
57
+ """
58
+
59
+ def __init__(self, base: BaseOperations) -> None:
60
+ """Initialize cross-session manager.
61
+
62
+ Args:
63
+ base: BaseOperations instance for Redis client access
64
+ """
65
+ self._base = base
66
+
67
+ def enable(
68
+ self,
69
+ access_tier: AccessTier = AccessTier.CONTRIBUTOR,
70
+ auto_announce: bool = True,
71
+ ) -> Any:
72
+ """Enable cross-session communication for this memory instance.
73
+
74
+ This allows agents in different Claude Code sessions to communicate
75
+ and coordinate via Redis.
76
+
77
+ Args:
78
+ access_tier: Access tier for this session
79
+ auto_announce: Whether to announce presence automatically
80
+
81
+ Returns:
82
+ CrossSessionCoordinator instance
83
+
84
+ Raises:
85
+ ValueError: If in mock mode (Redis required for cross-session)
86
+
87
+ Example:
88
+ >>> coordinator = cross_session.enable(AccessTier.CONTRIBUTOR)
89
+ >>> print(f"Session ID: {coordinator.agent_id}")
90
+ >>> sessions = coordinator.get_active_sessions()
91
+ """
92
+ if self._base.use_mock:
93
+ raise ValueError(
94
+ "Cross-session communication requires Redis. "
95
+ "Set REDIS_HOST/REDIS_PORT or disable mock mode."
96
+ )
97
+
98
+ # Import lazily to avoid circular imports
99
+ from attune.memory.cross_session import CrossSessionCoordinator, SessionType
100
+
101
+ # The coordinator expects a memory instance with full API
102
+ # For now, pass the base which should be extended by facade
103
+ coordinator = CrossSessionCoordinator(
104
+ memory=self._base, # type: ignore[arg-type]
105
+ session_type=SessionType.CLAUDE,
106
+ access_tier=access_tier,
107
+ auto_announce=auto_announce,
108
+ )
109
+
110
+ return coordinator
111
+
112
+ def available(self) -> bool:
113
+ """Check if cross-session communication is available.
114
+
115
+ Returns:
116
+ True if Redis is connected (not mock mode)
117
+
118
+ Example:
119
+ >>> if cross_session.available():
120
+ ... coordinator = cross_session.enable()
121
+ """
122
+ return not self._base.use_mock and self._base._client is not None