empathy-framework 5.0.1__py3-none-any.whl → 5.1.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.
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/METADATA +311 -150
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/RECORD +60 -33
- empathy_framework-5.1.0.dist-info/licenses/LICENSE +201 -0
- empathy_framework-5.1.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- empathy_llm_toolkit/providers.py +175 -35
- empathy_llm_toolkit/utils/tokens.py +150 -30
- empathy_os/__init__.py +1 -1
- empathy_os/cli/commands/batch.py +256 -0
- empathy_os/cli/commands/cache.py +248 -0
- empathy_os/cli/commands/inspect.py +1 -2
- empathy_os/cli/commands/metrics.py +1 -1
- empathy_os/cli/commands/routing.py +285 -0
- empathy_os/cli/commands/workflow.py +2 -1
- empathy_os/cli/parsers/__init__.py +6 -0
- empathy_os/cli/parsers/batch.py +118 -0
- empathy_os/cli/parsers/cache 2.py +65 -0
- empathy_os/cli/parsers/cache.py +65 -0
- empathy_os/cli/parsers/routing.py +110 -0
- empathy_os/cli_minimal.py +3 -3
- empathy_os/cli_router 2.py +416 -0
- empathy_os/dashboard/__init__.py +1 -2
- empathy_os/dashboard/app 2.py +512 -0
- empathy_os/dashboard/app.py +1 -1
- empathy_os/dashboard/simple_server 2.py +403 -0
- empathy_os/dashboard/standalone_server 2.py +536 -0
- empathy_os/dashboard/standalone_server.py +22 -11
- empathy_os/memory/types 2.py +441 -0
- empathy_os/metrics/collector.py +31 -0
- empathy_os/models/__init__.py +19 -0
- empathy_os/models/adaptive_routing 2.py +437 -0
- empathy_os/models/auth_cli.py +444 -0
- empathy_os/models/auth_strategy.py +450 -0
- empathy_os/models/token_estimator.py +21 -13
- empathy_os/project_index/scanner_parallel 2.py +291 -0
- empathy_os/telemetry/agent_coordination 2.py +478 -0
- empathy_os/telemetry/agent_coordination.py +14 -16
- empathy_os/telemetry/agent_tracking 2.py +350 -0
- empathy_os/telemetry/agent_tracking.py +18 -20
- empathy_os/telemetry/approval_gates 2.py +563 -0
- empathy_os/telemetry/approval_gates.py +27 -39
- empathy_os/telemetry/event_streaming 2.py +405 -0
- empathy_os/telemetry/event_streaming.py +22 -22
- empathy_os/telemetry/feedback_loop 2.py +557 -0
- empathy_os/telemetry/feedback_loop.py +14 -17
- empathy_os/workflows/__init__.py +8 -0
- empathy_os/workflows/autonomous_test_gen.py +569 -0
- empathy_os/workflows/batch_processing.py +56 -10
- empathy_os/workflows/bug_predict.py +45 -0
- empathy_os/workflows/code_review.py +92 -22
- empathy_os/workflows/document_gen.py +594 -62
- empathy_os/workflows/llm_base.py +363 -0
- empathy_os/workflows/perf_audit.py +69 -0
- empathy_os/workflows/release_prep.py +54 -0
- empathy_os/workflows/security_audit.py +154 -79
- empathy_os/workflows/test_gen.py +60 -0
- empathy_os/workflows/test_gen_behavioral.py +477 -0
- empathy_os/workflows/test_gen_parallel.py +341 -0
- empathy_framework-5.0.1.dist-info/licenses/LICENSE +0 -139
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/WHEEL +0 -0
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.0.1.dist-info → empathy_framework-5.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Agent Heartbeat Tracking System.
|
|
2
|
+
|
|
3
|
+
Pattern 1 from Agent Coordination Architecture - TTL-based heartbeat monitoring
|
|
4
|
+
for tracking agent execution status and detecting stale/failed agents.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Start tracking an agent
|
|
8
|
+
coordinator = HeartbeatCoordinator()
|
|
9
|
+
coordinator.start_heartbeat(
|
|
10
|
+
agent_id="code-review-abc123",
|
|
11
|
+
metadata={"workflow": "code-review", "run_id": "xyz"}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Update progress
|
|
15
|
+
coordinator.beat(
|
|
16
|
+
status="running",
|
|
17
|
+
progress=0.5,
|
|
18
|
+
current_task="Analyzing functions"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Stop tracking
|
|
22
|
+
coordinator.stop_heartbeat(final_status="completed")
|
|
23
|
+
|
|
24
|
+
# Monitor all active agents
|
|
25
|
+
active_agents = coordinator.get_active_agents()
|
|
26
|
+
for agent in active_agents:
|
|
27
|
+
print(f"{agent.agent_id}: {agent.status} - {agent.current_task}")
|
|
28
|
+
|
|
29
|
+
Copyright 2025 Smart-AI-Memory
|
|
30
|
+
Licensed under Fair Source License 0.9
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import time
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class AgentHeartbeat:
|
|
46
|
+
"""Agent heartbeat data structure.
|
|
47
|
+
|
|
48
|
+
Represents the current state of a running agent, stored in Redis with TTL.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
agent_id: str
|
|
52
|
+
status: str # "starting", "running", "completed", "failed", "cancelled"
|
|
53
|
+
progress: float # 0.0 to 1.0
|
|
54
|
+
current_task: str
|
|
55
|
+
last_beat: datetime
|
|
56
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
"""Convert to dictionary for serialization."""
|
|
60
|
+
return {
|
|
61
|
+
"agent_id": self.agent_id,
|
|
62
|
+
"status": self.status,
|
|
63
|
+
"progress": self.progress,
|
|
64
|
+
"current_task": self.current_task,
|
|
65
|
+
"last_beat": self.last_beat.isoformat() if isinstance(self.last_beat, datetime) else self.last_beat,
|
|
66
|
+
"metadata": self.metadata,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: dict[str, Any]) -> AgentHeartbeat:
|
|
71
|
+
"""Create from dictionary."""
|
|
72
|
+
# Convert ISO string back to datetime
|
|
73
|
+
last_beat = data.get("last_beat")
|
|
74
|
+
if isinstance(last_beat, str):
|
|
75
|
+
last_beat = datetime.fromisoformat(last_beat)
|
|
76
|
+
elif not isinstance(last_beat, datetime):
|
|
77
|
+
last_beat = datetime.utcnow()
|
|
78
|
+
|
|
79
|
+
return cls(
|
|
80
|
+
agent_id=data["agent_id"],
|
|
81
|
+
status=data["status"],
|
|
82
|
+
progress=data["progress"],
|
|
83
|
+
current_task=data["current_task"],
|
|
84
|
+
last_beat=last_beat,
|
|
85
|
+
metadata=data.get("metadata", {}),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class HeartbeatCoordinator:
|
|
90
|
+
"""Coordinates agent heartbeats using Redis TTL keys.
|
|
91
|
+
|
|
92
|
+
Agents publish heartbeats with a TTL. If an agent stops responding,
|
|
93
|
+
its heartbeat key expires automatically, indicating failure/crash.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
HEARTBEAT_TTL: Default heartbeat TTL in seconds (30s)
|
|
97
|
+
HEARTBEAT_INTERVAL: Recommended update interval (10s)
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
HEARTBEAT_TTL = 30 # Heartbeat expires after 30s of no updates
|
|
101
|
+
HEARTBEAT_INTERVAL = 10 # Agents should update every 10s
|
|
102
|
+
|
|
103
|
+
def __init__(self, memory=None, enable_streaming: bool = False):
|
|
104
|
+
"""Initialize heartbeat coordinator.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
memory: Memory instance (UnifiedMemory or ShortTermMemory).
|
|
108
|
+
If None, attempts to get from UsageTracker.
|
|
109
|
+
enable_streaming: If True, publish heartbeat events to Redis Streams
|
|
110
|
+
for real-time monitoring (Pattern 4).
|
|
111
|
+
"""
|
|
112
|
+
self.memory = memory
|
|
113
|
+
self.agent_id: str | None = None
|
|
114
|
+
self._enable_streaming = enable_streaming
|
|
115
|
+
self._event_streamer = None
|
|
116
|
+
|
|
117
|
+
if self.memory is None:
|
|
118
|
+
try:
|
|
119
|
+
from empathy_os.telemetry import UsageTracker
|
|
120
|
+
|
|
121
|
+
tracker = UsageTracker.get_instance()
|
|
122
|
+
if hasattr(tracker, "_memory"):
|
|
123
|
+
self.memory = tracker._memory
|
|
124
|
+
except (ImportError, AttributeError):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
if self.memory is None:
|
|
128
|
+
logger.warning("No memory backend available for heartbeat tracking")
|
|
129
|
+
|
|
130
|
+
def _get_event_streamer(self):
|
|
131
|
+
"""Get or create EventStreamer instance (lazy initialization)."""
|
|
132
|
+
if not self._enable_streaming:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
if self._event_streamer is None:
|
|
136
|
+
try:
|
|
137
|
+
from empathy_os.telemetry.event_streaming import EventStreamer
|
|
138
|
+
|
|
139
|
+
self._event_streamer = EventStreamer(memory=self.memory)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning(f"Failed to initialize EventStreamer: {e}")
|
|
142
|
+
self._enable_streaming = False
|
|
143
|
+
|
|
144
|
+
return self._event_streamer
|
|
145
|
+
|
|
146
|
+
def start_heartbeat(self, agent_id: str, metadata: dict[str, Any] | None = None) -> None:
|
|
147
|
+
"""Start heartbeat for an agent.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
agent_id: Unique agent identifier
|
|
151
|
+
metadata: Initial metadata (workflow, run_id, etc.)
|
|
152
|
+
"""
|
|
153
|
+
if not self.memory:
|
|
154
|
+
logger.debug("Heartbeat tracking disabled (no memory backend)")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
self.agent_id = agent_id
|
|
158
|
+
self._publish_heartbeat(
|
|
159
|
+
status="starting", progress=0.0, current_task="initializing", metadata=metadata or {}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def beat(self, status: str = "running", progress: float = 0.0, current_task: str = "") -> None:
|
|
163
|
+
"""Publish heartbeat update.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
status: Current agent status
|
|
167
|
+
progress: Progress percentage (0.0 - 1.0)
|
|
168
|
+
current_task: Human-readable current task description
|
|
169
|
+
"""
|
|
170
|
+
if not self.agent_id or not self.memory:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
self._publish_heartbeat(status=status, progress=progress, current_task=current_task, metadata={})
|
|
174
|
+
|
|
175
|
+
def stop_heartbeat(self, final_status: str = "completed") -> None:
|
|
176
|
+
"""Stop heartbeat (agent finished).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
final_status: Final status ("completed", "failed", "cancelled")
|
|
180
|
+
"""
|
|
181
|
+
if not self.agent_id or not self.memory:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Publish final heartbeat with short TTL
|
|
185
|
+
self._publish_heartbeat(
|
|
186
|
+
status=final_status,
|
|
187
|
+
progress=1.0,
|
|
188
|
+
current_task="finished",
|
|
189
|
+
metadata={"final": True},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Clear agent ID
|
|
193
|
+
self.agent_id = None
|
|
194
|
+
|
|
195
|
+
def _publish_heartbeat(
|
|
196
|
+
self, status: str, progress: float, current_task: str, metadata: dict[str, Any]
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Publish heartbeat to Redis with TTL and optionally to event stream."""
|
|
199
|
+
if not self.memory or not self.agent_id:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
heartbeat = AgentHeartbeat(
|
|
203
|
+
agent_id=self.agent_id,
|
|
204
|
+
status=status,
|
|
205
|
+
progress=progress,
|
|
206
|
+
current_task=current_task,
|
|
207
|
+
last_beat=datetime.utcnow(),
|
|
208
|
+
metadata=metadata,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Store in Redis with TTL (Pattern 1)
|
|
212
|
+
key = f"heartbeat:{self.agent_id}"
|
|
213
|
+
try:
|
|
214
|
+
# Use stash if available (UnifiedMemory), otherwise try Redis directly
|
|
215
|
+
if hasattr(self.memory, "stash"):
|
|
216
|
+
self.memory.stash(
|
|
217
|
+
key=key, data=heartbeat.to_dict(), credentials=None, ttl_seconds=self.HEARTBEAT_TTL
|
|
218
|
+
)
|
|
219
|
+
elif hasattr(self.memory, "_redis"):
|
|
220
|
+
# Direct Redis access for ShortTermMemory
|
|
221
|
+
import json
|
|
222
|
+
|
|
223
|
+
self.memory._redis.setex(key, self.HEARTBEAT_TTL, json.dumps(heartbeat.to_dict()))
|
|
224
|
+
else:
|
|
225
|
+
logger.warning(f"Cannot publish heartbeat: unsupported memory type {type(self.memory)}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.warning(f"Failed to publish heartbeat for {self.agent_id}: {e}")
|
|
228
|
+
|
|
229
|
+
# Publish to event stream (Pattern 4 - optional)
|
|
230
|
+
streamer = self._get_event_streamer()
|
|
231
|
+
if streamer:
|
|
232
|
+
try:
|
|
233
|
+
streamer.publish_event(
|
|
234
|
+
event_type="agent_heartbeat",
|
|
235
|
+
data=heartbeat.to_dict(),
|
|
236
|
+
source="empathy_os",
|
|
237
|
+
)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.debug(f"Failed to publish heartbeat event to stream: {e}")
|
|
240
|
+
|
|
241
|
+
def get_active_agents(self) -> list[AgentHeartbeat]:
|
|
242
|
+
"""Get all currently active agents.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
List of active agent heartbeats
|
|
246
|
+
"""
|
|
247
|
+
if not self.memory:
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
# Scan for heartbeat:* keys
|
|
252
|
+
if hasattr(self.memory, "_redis"):
|
|
253
|
+
keys = self.memory._redis.keys("heartbeat:*")
|
|
254
|
+
else:
|
|
255
|
+
logger.warning("Cannot scan for heartbeats: no Redis access")
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
heartbeats = []
|
|
259
|
+
for key in keys:
|
|
260
|
+
if isinstance(key, bytes):
|
|
261
|
+
key = key.decode("utf-8")
|
|
262
|
+
|
|
263
|
+
data = self._retrieve_heartbeat(key)
|
|
264
|
+
if data:
|
|
265
|
+
heartbeats.append(AgentHeartbeat.from_dict(data))
|
|
266
|
+
|
|
267
|
+
return heartbeats
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Failed to get active agents: {e}")
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
def is_agent_alive(self, agent_id: str) -> bool:
|
|
273
|
+
"""Check if agent is still alive.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
agent_id: Agent to check
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if heartbeat key exists (agent is alive)
|
|
280
|
+
"""
|
|
281
|
+
if not self.memory:
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
key = f"heartbeat:{agent_id}"
|
|
285
|
+
data = self._retrieve_heartbeat(key)
|
|
286
|
+
return data is not None
|
|
287
|
+
|
|
288
|
+
def get_agent_status(self, agent_id: str) -> AgentHeartbeat | None:
|
|
289
|
+
"""Get current status of an agent.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
agent_id: Agent to query
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
AgentHeartbeat or None if agent not active
|
|
296
|
+
"""
|
|
297
|
+
if not self.memory:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
key = f"heartbeat:{agent_id}"
|
|
301
|
+
data = self._retrieve_heartbeat(key)
|
|
302
|
+
|
|
303
|
+
if data:
|
|
304
|
+
return AgentHeartbeat.from_dict(data)
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def _retrieve_heartbeat(self, key: str) -> dict[str, Any] | None:
|
|
308
|
+
"""Retrieve heartbeat data from memory."""
|
|
309
|
+
if not self.memory:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
# Try retrieve method first (UnifiedMemory)
|
|
314
|
+
if hasattr(self.memory, "retrieve"):
|
|
315
|
+
return self.memory.retrieve(key, credentials=None)
|
|
316
|
+
# Try direct Redis access
|
|
317
|
+
elif hasattr(self.memory, "_redis"):
|
|
318
|
+
import json
|
|
319
|
+
|
|
320
|
+
data = self.memory._redis.get(key)
|
|
321
|
+
if data:
|
|
322
|
+
if isinstance(data, bytes):
|
|
323
|
+
data = data.decode("utf-8")
|
|
324
|
+
return json.loads(data)
|
|
325
|
+
return None
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.debug(f"Failed to retrieve heartbeat {key}: {e}")
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def get_stale_agents(self, threshold_seconds: float = 60.0) -> list[AgentHeartbeat]:
|
|
331
|
+
"""Get agents that haven't updated in a while (but key still exists).
|
|
332
|
+
|
|
333
|
+
This detects agents that are stuck or slow, not crashed (TTL would expire).
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
threshold_seconds: Time without update to consider stale
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
List of stale agent heartbeats
|
|
340
|
+
"""
|
|
341
|
+
active = self.get_active_agents()
|
|
342
|
+
now = datetime.utcnow()
|
|
343
|
+
stale = []
|
|
344
|
+
|
|
345
|
+
for agent in active:
|
|
346
|
+
time_since_beat = (now - agent.last_beat).total_seconds()
|
|
347
|
+
if time_since_beat > threshold_seconds and agent.status not in ("completed", "failed", "cancelled"):
|
|
348
|
+
stale.append(agent)
|
|
349
|
+
|
|
350
|
+
return stale
|
|
@@ -33,7 +33,6 @@ Licensed under Fair Source License 0.9
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
35
|
import logging
|
|
36
|
-
import time
|
|
37
36
|
from dataclasses import dataclass, field
|
|
38
37
|
from datetime import datetime
|
|
39
38
|
from typing import Any
|
|
@@ -211,18 +210,14 @@ class HeartbeatCoordinator:
|
|
|
211
210
|
# Store in Redis with TTL (Pattern 1)
|
|
212
211
|
key = f"heartbeat:{self.agent_id}"
|
|
213
212
|
try:
|
|
214
|
-
# Use
|
|
215
|
-
if hasattr(self.memory, "
|
|
216
|
-
|
|
217
|
-
key=key, data=heartbeat.to_dict(), credentials=None, ttl_seconds=self.HEARTBEAT_TTL
|
|
218
|
-
)
|
|
219
|
-
elif hasattr(self.memory, "_redis"):
|
|
220
|
-
# Direct Redis access for ShortTermMemory
|
|
213
|
+
# Use direct Redis access for heartbeats (need custom 30s TTL)
|
|
214
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
215
|
+
# Direct Redis access with setex for custom TTL
|
|
221
216
|
import json
|
|
222
217
|
|
|
223
|
-
self.memory.
|
|
218
|
+
self.memory._client.setex(key, self.HEARTBEAT_TTL, json.dumps(heartbeat.to_dict()))
|
|
224
219
|
else:
|
|
225
|
-
logger.warning(
|
|
220
|
+
logger.warning("Cannot publish heartbeat: no Redis backend available")
|
|
226
221
|
except Exception as e:
|
|
227
222
|
logger.warning(f"Failed to publish heartbeat for {self.agent_id}: {e}")
|
|
228
223
|
|
|
@@ -249,8 +244,8 @@ class HeartbeatCoordinator:
|
|
|
249
244
|
|
|
250
245
|
try:
|
|
251
246
|
# Scan for heartbeat:* keys
|
|
252
|
-
if hasattr(self.memory, "
|
|
253
|
-
keys = self.memory.
|
|
247
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
248
|
+
keys = self.memory._client.keys("heartbeat:*")
|
|
254
249
|
else:
|
|
255
250
|
logger.warning("Cannot scan for heartbeats: no Redis access")
|
|
256
251
|
return []
|
|
@@ -305,23 +300,26 @@ class HeartbeatCoordinator:
|
|
|
305
300
|
return None
|
|
306
301
|
|
|
307
302
|
def _retrieve_heartbeat(self, key: str) -> dict[str, Any] | None:
|
|
308
|
-
"""Retrieve heartbeat data from memory.
|
|
303
|
+
"""Retrieve heartbeat data from memory.
|
|
304
|
+
|
|
305
|
+
Heartbeat keys are stored directly as 'heartbeat:{agent_id}' and must be
|
|
306
|
+
retrieved via direct Redis access, not through the standard retrieve() method
|
|
307
|
+
which expects keys with 'working:{agent_id}:{key}' format.
|
|
308
|
+
"""
|
|
309
309
|
if not self.memory:
|
|
310
310
|
return None
|
|
311
311
|
|
|
312
312
|
try:
|
|
313
|
-
#
|
|
314
|
-
if hasattr(self.memory, "
|
|
315
|
-
return self.memory.retrieve(key, credentials=None)
|
|
316
|
-
# Try direct Redis access
|
|
317
|
-
elif hasattr(self.memory, "_redis"):
|
|
313
|
+
# Use direct Redis access for heartbeat keys
|
|
314
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
318
315
|
import json
|
|
319
316
|
|
|
320
|
-
data = self.memory.
|
|
317
|
+
data = self.memory._client.get(key)
|
|
321
318
|
if data:
|
|
322
319
|
if isinstance(data, bytes):
|
|
323
320
|
data = data.decode("utf-8")
|
|
324
|
-
|
|
321
|
+
result = json.loads(data)
|
|
322
|
+
return result if isinstance(result, dict) else None
|
|
325
323
|
return None
|
|
326
324
|
except Exception as e:
|
|
327
325
|
logger.debug(f"Failed to retrieve heartbeat {key}: {e}")
|