empathy-framework 4.9.1__py3-none-any.whl → 5.0.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 (47) hide show
  1. {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/METADATA +1 -1
  2. {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/RECORD +47 -26
  3. empathy_os/__init__.py +1 -1
  4. empathy_os/cache/hash_only.py +6 -3
  5. empathy_os/cache/hybrid.py +6 -3
  6. empathy_os/cli_legacy.py +27 -1
  7. empathy_os/cli_minimal.py +512 -15
  8. empathy_os/cli_router.py +145 -113
  9. empathy_os/cli_unified.py +25 -0
  10. empathy_os/dashboard/__init__.py +42 -0
  11. empathy_os/dashboard/app.py +512 -0
  12. empathy_os/dashboard/simple_server.py +403 -0
  13. empathy_os/dashboard/standalone_server.py +536 -0
  14. empathy_os/memory/__init__.py +19 -5
  15. empathy_os/memory/short_term.py +4 -70
  16. empathy_os/memory/types.py +2 -2
  17. empathy_os/models/__init__.py +3 -0
  18. empathy_os/models/adaptive_routing.py +437 -0
  19. empathy_os/models/registry.py +4 -4
  20. empathy_os/socratic/ab_testing.py +1 -1
  21. empathy_os/telemetry/__init__.py +29 -1
  22. empathy_os/telemetry/agent_coordination.py +478 -0
  23. empathy_os/telemetry/agent_tracking.py +350 -0
  24. empathy_os/telemetry/approval_gates.py +563 -0
  25. empathy_os/telemetry/event_streaming.py +405 -0
  26. empathy_os/telemetry/feedback_loop.py +557 -0
  27. empathy_os/vscode_bridge 2.py +173 -0
  28. empathy_os/workflows/__init__.py +4 -4
  29. empathy_os/workflows/base.py +495 -43
  30. empathy_os/workflows/history.py +3 -5
  31. empathy_os/workflows/output.py +410 -0
  32. empathy_os/workflows/progress.py +324 -22
  33. empathy_os/workflows/progressive/README 2.md +454 -0
  34. empathy_os/workflows/progressive/__init__ 2.py +92 -0
  35. empathy_os/workflows/progressive/cli 2.py +242 -0
  36. empathy_os/workflows/progressive/core 2.py +488 -0
  37. empathy_os/workflows/progressive/orchestrator 2.py +701 -0
  38. empathy_os/workflows/progressive/reports 2.py +528 -0
  39. empathy_os/workflows/progressive/telemetry 2.py +280 -0
  40. empathy_os/workflows/progressive/test_gen 2.py +514 -0
  41. empathy_os/workflows/progressive/workflow 2.py +628 -0
  42. empathy_os/workflows/routing.py +5 -0
  43. empathy_os/workflows/security_audit.py +189 -0
  44. {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/WHEEL +0 -0
  45. {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/entry_points.txt +0 -0
  46. {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/licenses/LICENSE +0 -0
  47. {empathy_framework-4.9.1.dist-info → empathy_framework-5.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,478 @@
1
+ """Agent Coordination via TTL Signals.
2
+
3
+ Pattern 2 from Agent Coordination Architecture - TTL-based inter-agent
4
+ communication for orchestration, synchronization, and coordination.
5
+
6
+ Usage:
7
+ # Agent A signals completion
8
+ coordinator = CoordinationSignals()
9
+ coordinator.signal(
10
+ signal_type="task_complete",
11
+ source_agent="agent-a",
12
+ target_agent="agent-b",
13
+ payload={"result": "success", "data": {...}}
14
+ )
15
+
16
+ # Agent B waits for signal
17
+ signal = coordinator.wait_for_signal(
18
+ signal_type="task_complete",
19
+ source_agent="agent-a",
20
+ timeout=30.0
21
+ )
22
+ if signal:
23
+ process(signal.payload)
24
+
25
+ # Orchestrator broadcasts to all agents
26
+ coordinator.broadcast(
27
+ signal_type="abort",
28
+ source_agent="orchestrator",
29
+ payload={"reason": "user_cancelled"}
30
+ )
31
+
32
+ Copyright 2025 Smart-AI-Memory
33
+ Licensed under Fair Source License 0.9
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ import time
40
+ from dataclasses import dataclass, field
41
+ from datetime import datetime
42
+ from typing import Any, TYPE_CHECKING
43
+ from uuid import uuid4
44
+
45
+ if TYPE_CHECKING:
46
+ from empathy_os.memory.types import AgentCredentials
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ @dataclass
52
+ class CoordinationSignal:
53
+ """Coordination signal between agents.
54
+
55
+ Ephemeral message with TTL, used for agent-to-agent communication.
56
+ """
57
+
58
+ signal_id: str
59
+ signal_type: str # "task_complete", "abort", "ready", "checkpoint", etc.
60
+ source_agent: str
61
+ target_agent: str | None # None for broadcast
62
+ payload: dict[str, Any]
63
+ timestamp: datetime
64
+ ttl_seconds: int = 60
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ """Convert to dictionary for serialization."""
68
+ return {
69
+ "signal_id": self.signal_id,
70
+ "signal_type": self.signal_type,
71
+ "source_agent": self.source_agent,
72
+ "target_agent": self.target_agent,
73
+ "payload": self.payload,
74
+ "timestamp": self.timestamp.isoformat() if isinstance(self.timestamp, datetime) else self.timestamp,
75
+ "ttl_seconds": self.ttl_seconds,
76
+ }
77
+
78
+ @classmethod
79
+ def from_dict(cls, data: dict[str, Any]) -> CoordinationSignal:
80
+ """Create from dictionary."""
81
+ timestamp = data.get("timestamp")
82
+ if isinstance(timestamp, str):
83
+ timestamp = datetime.fromisoformat(timestamp)
84
+ elif not isinstance(timestamp, datetime):
85
+ timestamp = datetime.utcnow()
86
+
87
+ return cls(
88
+ signal_id=data["signal_id"],
89
+ signal_type=data["signal_type"],
90
+ source_agent=data["source_agent"],
91
+ target_agent=data.get("target_agent"),
92
+ payload=data.get("payload", {}),
93
+ timestamp=timestamp,
94
+ ttl_seconds=data.get("ttl_seconds", 60),
95
+ )
96
+
97
+
98
+ class CoordinationSignals:
99
+ """TTL-based inter-agent coordination signals.
100
+
101
+ Agents can:
102
+ - Send signals to specific agents
103
+ - Broadcast signals to all agents
104
+ - Wait for specific signals with timeout
105
+ - Check for pending signals without blocking
106
+
107
+ Signals expire automatically via TTL, preventing stale coordination.
108
+ """
109
+
110
+ DEFAULT_TTL = 60 # Default signal TTL: 60 seconds
111
+ BROADCAST_TARGET = "*" # Special target for broadcast signals
112
+ KEY_PREFIX = "empathy:signal:" # Redis key prefix (consistent with framework)
113
+
114
+ def __init__(self, memory=None, agent_id: str | None = None, enable_streaming: bool = False):
115
+ """Initialize coordination signals.
116
+
117
+ Args:
118
+ memory: Memory instance for storing signals
119
+ agent_id: This agent's ID (for receiving targeted signals)
120
+ enable_streaming: If True, publish signal events to Redis Streams
121
+ for real-time monitoring (Pattern 4).
122
+ """
123
+ self.memory = memory
124
+ self.agent_id = agent_id
125
+ self._enable_streaming = enable_streaming
126
+ self._event_streamer = None
127
+
128
+ if self.memory is None:
129
+ try:
130
+ from empathy_os.telemetry import UsageTracker
131
+
132
+ tracker = UsageTracker.get_instance()
133
+ if hasattr(tracker, "_memory"):
134
+ self.memory = tracker._memory
135
+ except (ImportError, AttributeError):
136
+ pass
137
+
138
+ if self.memory is None:
139
+ logger.warning("No memory backend available for coordination signals")
140
+
141
+ def _get_event_streamer(self):
142
+ """Get or create EventStreamer instance (lazy initialization)."""
143
+ if not self._enable_streaming:
144
+ return None
145
+
146
+ if self._event_streamer is None:
147
+ try:
148
+ from empathy_os.telemetry.event_streaming import EventStreamer
149
+
150
+ self._event_streamer = EventStreamer(memory=self.memory)
151
+ except Exception as e:
152
+ logger.warning(f"Failed to initialize EventStreamer: {e}")
153
+ self._enable_streaming = False
154
+
155
+ return self._event_streamer
156
+
157
+ def signal(
158
+ self,
159
+ signal_type: str,
160
+ source_agent: str | None = None,
161
+ target_agent: str | None = None,
162
+ payload: dict[str, Any] | None = None,
163
+ ttl_seconds: int | None = None,
164
+ credentials: AgentCredentials | None = None,
165
+ ) -> str:
166
+ """Send a coordination signal.
167
+
168
+ Args:
169
+ signal_type: Type of signal (e.g., "task_complete", "abort", "ready")
170
+ source_agent: Source agent ID (defaults to self.agent_id)
171
+ target_agent: Target agent ID (None for broadcast)
172
+ payload: Signal payload data
173
+ ttl_seconds: TTL for this signal (defaults to DEFAULT_TTL)
174
+ credentials: Agent credentials for permission check (optional but recommended)
175
+
176
+ Returns:
177
+ Signal ID
178
+
179
+ Raises:
180
+ PermissionError: If credentials provided but agent lacks CONTRIBUTOR tier
181
+
182
+ Security:
183
+ Coordination signals require CONTRIBUTOR tier or higher. If credentials
184
+ are not provided, a warning is logged but the signal is still sent
185
+ (backward compatibility). For production use, always provide credentials.
186
+ """
187
+ if not self.memory:
188
+ logger.warning("Cannot send signal: no memory backend")
189
+ return ""
190
+
191
+ # Permission check for coordination signals (requires CONTRIBUTOR tier)
192
+ if credentials is not None:
193
+ if not credentials.can_stage():
194
+ raise PermissionError(
195
+ f"Agent {credentials.agent_id} (Tier {credentials.tier.name}) "
196
+ "cannot send coordination signals. Requires CONTRIBUTOR tier or higher."
197
+ )
198
+ else:
199
+ # Log warning if no credentials provided (security best practice)
200
+ logger.warning(
201
+ "Sending coordination signal without credentials - "
202
+ "permission check bypassed. Provide credentials for secure coordination."
203
+ )
204
+
205
+ source = source_agent or self.agent_id or "unknown"
206
+ signal_id = f"signal_{uuid4().hex[:8]}"
207
+ ttl = ttl_seconds or self.DEFAULT_TTL
208
+
209
+ signal = CoordinationSignal(
210
+ signal_id=signal_id,
211
+ signal_type=signal_type,
212
+ source_agent=source,
213
+ target_agent=target_agent,
214
+ payload=payload or {},
215
+ timestamp=datetime.utcnow(),
216
+ ttl_seconds=ttl,
217
+ )
218
+
219
+ # Store signal with TTL (Pattern 2)
220
+ # Key format: empathy:signal:{target}:{type}:{id}
221
+ target_key = target_agent or self.BROADCAST_TARGET
222
+ key = f"{self.KEY_PREFIX}{target_key}:{signal_type}:{signal_id}"
223
+
224
+ try:
225
+ if hasattr(self.memory, "stash"):
226
+ # Pass credentials through to memory backend for permission enforcement
227
+ self.memory.stash(key=key, data=signal.to_dict(), credentials=credentials, ttl_seconds=ttl)
228
+ elif hasattr(self.memory, "_redis"):
229
+ import json
230
+
231
+ self.memory._redis.setex(key, ttl, json.dumps(signal.to_dict()))
232
+ else:
233
+ logger.warning(f"Cannot send signal: unsupported memory type {type(self.memory)}")
234
+ except Exception as e:
235
+ logger.error(f"Failed to send signal {signal_id}: {e}")
236
+
237
+ # Publish to event stream (Pattern 4 - optional)
238
+ streamer = self._get_event_streamer()
239
+ if streamer:
240
+ try:
241
+ streamer.publish_event(
242
+ event_type="coordination_signal",
243
+ data=signal.to_dict(),
244
+ source="empathy_os",
245
+ )
246
+ except Exception as e:
247
+ logger.debug(f"Failed to publish coordination signal event to stream: {e}")
248
+
249
+ return signal_id
250
+
251
+ def broadcast(
252
+ self,
253
+ signal_type: str,
254
+ source_agent: str | None = None,
255
+ payload: dict[str, Any] | None = None,
256
+ ttl_seconds: int | None = None,
257
+ credentials: AgentCredentials | None = None,
258
+ ) -> str:
259
+ """Broadcast signal to all agents.
260
+
261
+ Args:
262
+ signal_type: Type of signal
263
+ source_agent: Source agent ID
264
+ payload: Signal payload
265
+ ttl_seconds: TTL for signal
266
+ credentials: Agent credentials for permission check
267
+
268
+ Returns:
269
+ Signal ID
270
+
271
+ Raises:
272
+ PermissionError: If credentials provided but agent lacks CONTRIBUTOR tier
273
+ """
274
+ return self.signal(
275
+ signal_type=signal_type,
276
+ source_agent=source_agent,
277
+ target_agent=None, # Broadcast
278
+ payload=payload,
279
+ ttl_seconds=ttl_seconds,
280
+ credentials=credentials,
281
+ )
282
+
283
+ def wait_for_signal(
284
+ self,
285
+ signal_type: str,
286
+ source_agent: str | None = None,
287
+ timeout: float = 30.0,
288
+ poll_interval: float = 0.5,
289
+ ) -> CoordinationSignal | None:
290
+ """Wait for a specific signal (blocking with timeout).
291
+
292
+ Args:
293
+ signal_type: Type of signal to wait for
294
+ source_agent: Optional source agent filter
295
+ timeout: Maximum wait time in seconds
296
+ poll_interval: Poll interval in seconds
297
+
298
+ Returns:
299
+ CoordinationSignal if received, None if timeout
300
+ """
301
+ if not self.memory or not self.agent_id:
302
+ return None
303
+
304
+ start_time = time.time()
305
+
306
+ while time.time() - start_time < timeout:
307
+ # Check for signal
308
+ signal = self.check_signal(signal_type=signal_type, source_agent=source_agent, consume=True)
309
+
310
+ if signal:
311
+ return signal
312
+
313
+ # Sleep before next poll
314
+ time.sleep(poll_interval)
315
+
316
+ return None
317
+
318
+ def check_signal(
319
+ self, signal_type: str, source_agent: str | None = None, consume: bool = True
320
+ ) -> CoordinationSignal | None:
321
+ """Check for a signal without blocking.
322
+
323
+ Args:
324
+ signal_type: Type of signal to check
325
+ source_agent: Optional source agent filter
326
+ consume: If True, remove signal after reading
327
+
328
+ Returns:
329
+ CoordinationSignal if available, None otherwise
330
+ """
331
+ if not self.memory or not self.agent_id:
332
+ return None
333
+
334
+ try:
335
+ # Scan for matching signals
336
+ # Check targeted signals: empathy:signal:{agent_id}:{type}:*
337
+ # Check broadcast signals: empathy:signal:*:{type}:*
338
+ patterns = [
339
+ f"{self.KEY_PREFIX}{self.agent_id}:{signal_type}:*",
340
+ f"{self.KEY_PREFIX}{self.BROADCAST_TARGET}:{signal_type}:*"
341
+ ]
342
+
343
+ for pattern in patterns:
344
+ if hasattr(self.memory, "_redis"):
345
+ keys = self.memory._redis.keys(pattern)
346
+ else:
347
+ continue
348
+
349
+ for key in keys:
350
+ if isinstance(key, bytes):
351
+ key = key.decode("utf-8")
352
+
353
+ # Retrieve signal
354
+ data = self._retrieve_signal(key)
355
+ if not data:
356
+ continue
357
+
358
+ signal = CoordinationSignal.from_dict(data)
359
+
360
+ # Filter by source if specified
361
+ if source_agent and signal.source_agent != source_agent:
362
+ continue
363
+
364
+ # Consume signal if requested
365
+ if consume:
366
+ self._delete_signal(key)
367
+
368
+ return signal
369
+
370
+ return None
371
+ except Exception as e:
372
+ logger.error(f"Failed to check signal: {e}")
373
+ return None
374
+
375
+ def get_pending_signals(self, signal_type: str | None = None) -> list[CoordinationSignal]:
376
+ """Get all pending signals for this agent.
377
+
378
+ Args:
379
+ signal_type: Optional filter by signal type
380
+
381
+ Returns:
382
+ List of pending signals
383
+ """
384
+ if not self.memory or not self.agent_id:
385
+ return []
386
+
387
+ try:
388
+ # Scan for all signals for this agent
389
+ patterns = [
390
+ f"{self.KEY_PREFIX}{self.agent_id}:*",
391
+ f"{self.KEY_PREFIX}{self.BROADCAST_TARGET}:*",
392
+ ]
393
+
394
+ signals = []
395
+ for pattern in patterns:
396
+ if hasattr(self.memory, "_redis"):
397
+ keys = self.memory._redis.keys(pattern)
398
+ else:
399
+ continue
400
+
401
+ for key in keys:
402
+ if isinstance(key, bytes):
403
+ key = key.decode("utf-8")
404
+
405
+ data = self._retrieve_signal(key)
406
+ if not data:
407
+ continue
408
+
409
+ signal = CoordinationSignal.from_dict(data)
410
+
411
+ # Filter by type if specified
412
+ if signal_type and signal.signal_type != signal_type:
413
+ continue
414
+
415
+ signals.append(signal)
416
+
417
+ return signals
418
+ except Exception as e:
419
+ logger.error(f"Failed to get pending signals: {e}")
420
+ return []
421
+
422
+ def clear_signals(self, signal_type: str | None = None) -> int:
423
+ """Clear all signals for this agent.
424
+
425
+ Args:
426
+ signal_type: Optional filter by signal type
427
+
428
+ Returns:
429
+ Number of signals cleared
430
+ """
431
+ if not self.memory or not self.agent_id:
432
+ return 0
433
+
434
+ signals = self.get_pending_signals(signal_type=signal_type)
435
+ count = 0
436
+
437
+ for signal in signals:
438
+ # Reconstruct key
439
+ target_key = signal.target_agent or self.BROADCAST_TARGET
440
+ key = f"{self.KEY_PREFIX}{target_key}:{signal.signal_type}:{signal.signal_id}"
441
+ if self._delete_signal(key):
442
+ count += 1
443
+
444
+ return count
445
+
446
+ def _retrieve_signal(self, key: str) -> dict[str, Any] | None:
447
+ """Retrieve signal data from memory."""
448
+ if not self.memory:
449
+ return None
450
+
451
+ try:
452
+ if hasattr(self.memory, "retrieve"):
453
+ return self.memory.retrieve(key, credentials=None)
454
+ elif hasattr(self.memory, "_redis"):
455
+ import json
456
+
457
+ data = self.memory._redis.get(key)
458
+ if data:
459
+ if isinstance(data, bytes):
460
+ data = data.decode("utf-8")
461
+ return json.loads(data)
462
+ return None
463
+ except Exception as e:
464
+ logger.debug(f"Failed to retrieve signal {key}: {e}")
465
+ return None
466
+
467
+ def _delete_signal(self, key: str) -> bool:
468
+ """Delete signal from memory."""
469
+ if not self.memory:
470
+ return False
471
+
472
+ try:
473
+ if hasattr(self.memory, "_redis"):
474
+ return self.memory._redis.delete(key) > 0
475
+ return False
476
+ except Exception as e:
477
+ logger.debug(f"Failed to delete signal {key}: {e}")
478
+ return False