empathy-framework 3.8.3__py3-none-any.whl → 3.9.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 (50) hide show
  1. {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/METADATA +67 -7
  2. {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/RECORD +50 -39
  3. {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/top_level.txt +0 -4
  4. empathy_os/.empathy/costs.json +60 -0
  5. empathy_os/.empathy/discovery_stats.json +15 -0
  6. empathy_os/.empathy/workflow_runs.json +45 -0
  7. empathy_os/cli.py +372 -13
  8. empathy_os/cli_unified.py +111 -0
  9. empathy_os/config/xml_config.py +45 -3
  10. empathy_os/config.py +46 -2
  11. empathy_os/memory/control_panel.py +128 -8
  12. empathy_os/memory/long_term.py +26 -4
  13. empathy_os/memory/short_term.py +110 -0
  14. empathy_os/models/token_estimator.py +25 -0
  15. empathy_os/pattern_library.py +81 -8
  16. empathy_os/patterns/debugging/all_patterns.json +81 -0
  17. empathy_os/patterns/debugging/workflow_20260107_1770825e.json +77 -0
  18. empathy_os/patterns/refactoring_memory.json +89 -0
  19. empathy_os/telemetry/__init__.py +11 -0
  20. empathy_os/telemetry/cli.py +451 -0
  21. empathy_os/telemetry/usage_tracker.py +475 -0
  22. {test_generator → empathy_os/test_generator}/generator.py +1 -0
  23. empathy_os/tier_recommender.py +422 -0
  24. empathy_os/workflows/base.py +223 -23
  25. empathy_os/workflows/config.py +50 -5
  26. empathy_os/workflows/tier_tracking.py +408 -0
  27. {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/WHEEL +0 -0
  28. {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/entry_points.txt +0 -0
  29. {empathy_framework-3.8.3.dist-info → empathy_framework-3.9.1.dist-info}/licenses/LICENSE +0 -0
  30. {hot_reload → empathy_os/hot_reload}/README.md +0 -0
  31. {hot_reload → empathy_os/hot_reload}/__init__.py +0 -0
  32. {hot_reload → empathy_os/hot_reload}/config.py +0 -0
  33. {hot_reload → empathy_os/hot_reload}/integration.py +0 -0
  34. {hot_reload → empathy_os/hot_reload}/reloader.py +0 -0
  35. {hot_reload → empathy_os/hot_reload}/watcher.py +0 -0
  36. {hot_reload → empathy_os/hot_reload}/websocket.py +0 -0
  37. {scaffolding → empathy_os/scaffolding}/README.md +0 -0
  38. {scaffolding → empathy_os/scaffolding}/__init__.py +0 -0
  39. {scaffolding → empathy_os/scaffolding}/__main__.py +0 -0
  40. {scaffolding → empathy_os/scaffolding}/cli.py +0 -0
  41. {test_generator → empathy_os/test_generator}/__init__.py +0 -0
  42. {test_generator → empathy_os/test_generator}/__main__.py +0 -0
  43. {test_generator → empathy_os/test_generator}/cli.py +0 -0
  44. {test_generator → empathy_os/test_generator}/risk_analyzer.py +0 -0
  45. {workflow_patterns → empathy_os/workflow_patterns}/__init__.py +0 -0
  46. {workflow_patterns → empathy_os/workflow_patterns}/behavior.py +0 -0
  47. {workflow_patterns → empathy_os/workflow_patterns}/core.py +0 -0
  48. {workflow_patterns → empathy_os/workflow_patterns}/output.py +0 -0
  49. {workflow_patterns → empathy_os/workflow_patterns}/registry.py +0 -0
  50. {workflow_patterns → empathy_os/workflow_patterns}/structural.py +0 -0
empathy_os/config.py CHANGED
@@ -26,6 +26,48 @@ except ImportError:
26
26
  from empathy_os.workflows.config import ModelConfig
27
27
 
28
28
 
29
+ def _validate_file_path(path: str, allowed_dir: str | None = None) -> Path:
30
+ """Validate file path to prevent path traversal and arbitrary writes.
31
+
32
+ Args:
33
+ path: File path to validate
34
+ allowed_dir: Optional directory to restrict writes to
35
+
36
+ Returns:
37
+ Validated Path object
38
+
39
+ Raises:
40
+ ValueError: If path is invalid or unsafe
41
+ """
42
+ if not path or not isinstance(path, str):
43
+ raise ValueError("path must be a non-empty string")
44
+
45
+ # Check for null bytes
46
+ if "\x00" in path:
47
+ raise ValueError("path contains null bytes")
48
+
49
+ try:
50
+ resolved = Path(path).resolve()
51
+ except (OSError, RuntimeError) as e:
52
+ raise ValueError(f"Invalid path: {e}")
53
+
54
+ # Check if within allowed directory
55
+ if allowed_dir:
56
+ try:
57
+ allowed = Path(allowed_dir).resolve()
58
+ resolved.relative_to(allowed)
59
+ except ValueError:
60
+ raise ValueError(f"path must be within {allowed_dir}")
61
+
62
+ # Check for dangerous system paths
63
+ dangerous_paths = ["/etc", "/sys", "/proc", "/dev"]
64
+ for dangerous in dangerous_paths:
65
+ if str(resolved).startswith(dangerous):
66
+ raise ValueError(f"Cannot write to system directory: {dangerous}")
67
+
68
+ return resolved
69
+
70
+
29
71
  @dataclass
30
72
  class EmpathyConfig:
31
73
  """Configuration for EmpathyOS instance
@@ -300,9 +342,10 @@ class EmpathyConfig:
300
342
  "PyYAML is required for YAML export. Install with: pip install pyyaml",
301
343
  )
302
344
 
345
+ validated_path = _validate_file_path(filepath)
303
346
  data = asdict(self)
304
347
 
305
- with open(filepath, "w") as f:
348
+ with open(validated_path, "w") as f:
306
349
  yaml.dump(data, f, default_flow_style=False, sort_keys=False)
307
350
 
308
351
  def to_json(self, filepath: str, indent: int = 2):
@@ -317,9 +360,10 @@ class EmpathyConfig:
317
360
  >>> config.to_json("my-config.json")
318
361
 
319
362
  """
363
+ validated_path = _validate_file_path(filepath)
320
364
  data = asdict(self)
321
365
 
322
- with open(filepath, "w") as f:
366
+ with open(validated_path, "w") as f:
323
367
  json.dump(data, f, indent=indent)
324
368
 
325
369
  def to_dict(self) -> dict[str, Any]:
@@ -124,8 +124,9 @@ def _validate_agent_id(agent_id: str) -> bool:
124
124
  if not agent_id or not isinstance(agent_id, str):
125
125
  return False
126
126
 
127
- # Check for dangerous characters
128
- if any(c in agent_id for c in [".", "/", "\\", "\x00", ";", "|", "&"]):
127
+ # Check for dangerous characters (path separators, null bytes, command injection)
128
+ # Note: "." and "@" are allowed for email-style user IDs
129
+ if any(c in agent_id for c in ["/", "\\", "\x00", ";", "|", "&"]):
129
130
  return False
130
131
 
131
132
  # Check length bounds
@@ -153,10 +154,70 @@ def _validate_classification(classification: str | None) -> bool:
153
154
  return classification.upper() in ("PUBLIC", "INTERNAL", "SENSITIVE")
154
155
 
155
156
 
157
+ def _validate_file_path(path: str, allowed_dir: str | None = None) -> Path:
158
+ """Validate file path to prevent path traversal and arbitrary writes.
159
+
160
+ Args:
161
+ path: Path to validate
162
+ allowed_dir: Optional directory that must contain the path
163
+
164
+ Returns:
165
+ Resolved absolute Path object
166
+
167
+ Raises:
168
+ ValueError: If path is invalid or outside allowed directory
169
+
170
+ """
171
+ if not path or not isinstance(path, str):
172
+ raise ValueError("path must be a non-empty string")
173
+
174
+ # Check for null bytes
175
+ if "\x00" in path:
176
+ raise ValueError("path contains null bytes")
177
+
178
+ try:
179
+ # Resolve to absolute path
180
+ resolved = Path(path).resolve()
181
+ except (OSError, RuntimeError) as e:
182
+ raise ValueError(f"Invalid path: {e}")
183
+
184
+ # Check if within allowed directory
185
+ if allowed_dir:
186
+ try:
187
+ allowed = Path(allowed_dir).resolve()
188
+ resolved.relative_to(allowed)
189
+ except ValueError:
190
+ raise ValueError(f"path must be within {allowed_dir}")
191
+
192
+ # Check for dangerous system paths
193
+ dangerous_paths = ["/etc", "/sys", "/proc", "/dev"]
194
+ for dangerous in dangerous_paths:
195
+ if str(resolved).startswith(dangerous):
196
+ raise ValueError(f"Cannot write to system directory: {dangerous}")
197
+
198
+ return resolved
199
+
200
+
156
201
  class RateLimiter:
157
202
  """Simple in-memory rate limiter by IP address."""
158
203
 
159
204
  def __init__(self, window_seconds: int = 60, max_requests: int = 100):
205
+ """Initialize rate limiter.
206
+
207
+ Args:
208
+ window_seconds: Time window in seconds
209
+ max_requests: Maximum requests allowed per window
210
+
211
+ Raises:
212
+ ValueError: If window_seconds or max_requests is invalid
213
+
214
+ """
215
+ if window_seconds < 1:
216
+ raise ValueError(f"window_seconds must be positive, got {window_seconds}")
217
+
218
+ if max_requests < 1:
219
+ raise ValueError(f"max_requests must be positive, got {max_requests}")
220
+
160
221
  self.window_seconds = window_seconds
161
222
  self.max_requests = max_requests
162
223
  self._requests: dict[str, list[float]] = defaultdict(list)
@@ -406,8 +467,9 @@ class MemoryControlPanel:
406
467
  stats.storage_bytes = sum(
407
468
  f.stat().st_size for f in storage_path.glob("**/*") if f.is_file()
408
469
  )
409
- except Exception:
410
- pass
470
+ except Exception as e:
471
+ logger.debug("storage_size_calculation_failed", error=str(e))
472
+ stats.storage_bytes = 0
411
473
 
412
474
  try:
413
475
  long_term = self._get_long_term()
@@ -439,7 +501,24 @@ class MemoryControlPanel:
439
501
  Returns:
440
502
  List of pattern summaries
441
503
 
504
+ Raises:
505
+ ValueError: If classification is invalid or limit is out of range
506
+
442
507
  """
508
+ # Validate classification
509
+ if not _validate_classification(classification):
510
+ raise ValueError(
511
+ f"Invalid classification '{classification}'. "
512
+ f"Must be PUBLIC, INTERNAL, or SENSITIVE."
513
+ )
514
+
515
+ # Validate limit range
516
+ if limit < 1:
517
+ raise ValueError(f"limit must be positive, got {limit}")
518
+
519
+ if limit > 10000:
520
+ raise ValueError(f"limit too large (max 10000), got {limit}")
521
+
443
522
  long_term = self._get_long_term()
444
523
 
445
524
  class_filter = None
@@ -464,13 +543,24 @@ class MemoryControlPanel:
464
543
  Returns:
465
544
  True if deleted
466
545
 
546
+ Raises:
547
+ ValueError: If pattern_id or user_id format is invalid
548
+
467
549
  """
550
+ # Validate pattern_id
551
+ if not _validate_pattern_id(pattern_id):
552
+ raise ValueError(f"Invalid pattern_id format: {pattern_id}")
553
+
554
+ # Validate user_id (reuse agent_id validation - same format)
555
+ if not _validate_agent_id(user_id):
556
+ raise ValueError(f"Invalid user_id format: {user_id}")
557
+
468
558
  long_term = self._get_long_term()
469
559
  try:
470
560
  return long_term.delete_pattern(pattern_id, user_id)
471
561
  except Exception as e:
472
562
  logger.error("delete_pattern_failed", pattern_id=pattern_id, error=str(e))
473
- return False
563
+ return False # Graceful degradation - validation errors raise, storage errors return False
474
564
 
475
565
  def clear_short_term(self, agent_id: str = "admin") -> int:
476
566
  """Clear all short-term memory for an agent.
@@ -481,7 +571,14 @@ class MemoryControlPanel:
481
571
  Returns:
482
572
  Number of keys deleted
483
573
 
574
+ Raises:
575
+ ValueError: If agent_id format is invalid
576
+
484
577
  """
578
+ # Validate agent_id
579
+ if not _validate_agent_id(agent_id):
580
+ raise ValueError(f"Invalid agent_id format: {agent_id}")
581
+
485
582
  memory = self._get_short_term()
486
583
  creds = AgentCredentials(agent_id=agent_id, tier=AccessTier.STEWARD)
487
584
  return memory.clear_working_memory(creds)
@@ -496,7 +593,20 @@ class MemoryControlPanel:
496
593
  Returns:
497
594
  Number of patterns exported
498
595
 
596
+ Raises:
597
+ ValueError: If output_path is invalid, classification invalid, or path is unsafe
598
+
499
599
  """
600
+ # Validate file path to prevent path traversal attacks
601
+ validated_path = _validate_file_path(output_path)
602
+
603
+ # Validate classification (list_patterns will also validate, but do it early)
604
+ if not _validate_classification(classification):
605
+ raise ValueError(
606
+ f"Invalid classification '{classification}'. "
607
+ f"Must be PUBLIC, INTERNAL, or SENSITIVE."
608
+ )
609
+
500
610
  patterns = self.list_patterns(classification=classification)
501
611
 
502
612
  export_data = {
@@ -506,7 +616,7 @@ class MemoryControlPanel:
506
616
  "patterns": patterns,
507
617
  }
508
618
 
509
- with open(output_path, "w") as f:
619
+ with open(validated_path, "w") as f:
510
620
  json.dump(export_data, f, indent=2)
511
621
 
512
622
  return len(patterns)
@@ -605,11 +715,21 @@ class MemoryControlPanel:
605
715
  return self._long_term
606
716
 
607
717
  def _count_patterns(self) -> int:
608
- """Count patterns in storage."""
718
+ """Count patterns in storage.
719
+
720
+ Returns:
721
+ Number of pattern files, or 0 if counting fails
722
+
723
+ """
609
724
  storage_path = Path(self.config.storage_dir)
610
725
  if not storage_path.exists():
611
726
  return 0
612
- return len(list(storage_path.glob("*.json")))
727
+
728
+ try:
729
+ return len(list(storage_path.glob("*.json")))
730
+ except (OSError, PermissionError) as e:
731
+ logger.debug("pattern_count_failed", error=str(e))
732
+ return 0
613
733
 
614
734
 
615
735
  def print_status(panel: MemoryControlPanel):
@@ -540,7 +540,8 @@ class SecureMemDocsIntegration:
540
540
 
541
541
  Raises:
542
542
  SecurityError: If secrets detected or security policy violated
543
- ValueError: If invalid classification specified
543
+ ValueError: If content/pattern_type/user_id empty or invalid classification
544
+ TypeError: If custom_metadata is not dict
544
545
 
545
546
  Example:
546
547
  >>> result = integration.store_pattern(
@@ -559,9 +560,17 @@ class SecureMemDocsIntegration:
559
560
  )
560
561
 
561
562
  try:
562
- # Validate content
563
+ # Pattern 1: String ID validation
563
564
  if not content or not content.strip():
564
- raise ValueError("Content cannot be empty")
565
+ raise ValueError("content cannot be empty")
566
+ if not pattern_type or not pattern_type.strip():
567
+ raise ValueError("pattern_type cannot be empty")
568
+ if not user_id or not user_id.strip():
569
+ raise ValueError("user_id cannot be empty")
570
+
571
+ # Pattern 5: Type validation
572
+ if custom_metadata is not None and not isinstance(custom_metadata, dict):
573
+ raise TypeError(f"custom_metadata must be dict, got {type(custom_metadata).__name__}")
565
574
 
566
575
  # Step 1 & 2: PII Scrubbing + Secrets Detection (PARALLEL for performance)
567
576
  # Run both operations in parallel since they're independent
@@ -759,7 +768,7 @@ class SecureMemDocsIntegration:
759
768
 
760
769
  Raises:
761
770
  PermissionError: If access denied
762
- ValueError: If pattern not found or retention expired
771
+ ValueError: If pattern_id/user_id empty, pattern not found, or retention expired
763
772
  SecurityError: If decryption fails
764
773
 
765
774
  Example:
@@ -770,6 +779,12 @@ class SecureMemDocsIntegration:
770
779
  >>> print(pattern["content"])
771
780
 
772
781
  """
782
+ # Pattern 1: String ID validation
783
+ if not pattern_id or not pattern_id.strip():
784
+ raise ValueError("pattern_id cannot be empty")
785
+ if not user_id or not user_id.strip():
786
+ raise ValueError("user_id cannot be empty")
787
+
773
788
  logger.info(
774
789
  "retrieve_pattern_started",
775
790
  pattern_id=pattern_id,
@@ -1108,8 +1123,15 @@ class SecureMemDocsIntegration:
1108
1123
 
1109
1124
  Raises:
1110
1125
  PermissionError: If user doesn't have permission to delete
1126
+ ValueError: If pattern_id or user_id is empty
1111
1127
 
1112
1128
  """
1129
+ # Pattern 1: String ID validation
1130
+ if not pattern_id or not pattern_id.strip():
1131
+ raise ValueError("pattern_id cannot be empty")
1132
+ if not user_id or not user_id.strip():
1133
+ raise ValueError("user_id cannot be empty")
1134
+
1113
1135
  # Retrieve pattern to check permissions
1114
1136
  pattern_data = self.storage.retrieve(pattern_id)
1115
1137
 
@@ -295,6 +295,26 @@ class StagedPattern:
295
295
  staged_at: datetime = field(default_factory=datetime.now)
296
296
  interests: list[str] = field(default_factory=list) # For negotiation
297
297
 
298
+ def __post_init__(self):
299
+ """Validate fields after initialization"""
300
+ # Pattern 1: String ID validation
301
+ if not self.pattern_id or not self.pattern_id.strip():
302
+ raise ValueError("pattern_id cannot be empty")
303
+ if not self.agent_id or not self.agent_id.strip():
304
+ raise ValueError("agent_id cannot be empty")
305
+ if not self.pattern_type or not self.pattern_type.strip():
306
+ raise ValueError("pattern_type cannot be empty")
307
+
308
+ # Pattern 4: Range validation for confidence
309
+ if not 0.0 <= self.confidence <= 1.0:
310
+ raise ValueError(f"confidence must be between 0.0 and 1.0, got {self.confidence}")
311
+
312
+ # Pattern 5: Type validation
313
+ if not isinstance(self.context, dict):
314
+ raise TypeError(f"context must be dict, got {type(self.context).__name__}")
315
+ if not isinstance(self.interests, list):
316
+ raise TypeError(f"interests must be list, got {type(self.interests).__name__}")
317
+
298
318
  def to_dict(self) -> dict:
299
319
  return {
300
320
  "pattern_id": self.pattern_id,
@@ -619,10 +639,18 @@ class RedisShortTermMemory:
619
639
  Returns:
620
640
  True if successful
621
641
 
642
+ Raises:
643
+ ValueError: If key is empty or invalid
644
+ PermissionError: If credentials lack write access
645
+
622
646
  Example:
623
647
  >>> memory.stash("analysis_v1", {"findings": [...]}, creds)
624
648
 
625
649
  """
650
+ # Pattern 1: String ID validation
651
+ if not key or not key.strip():
652
+ raise ValueError("key cannot be empty")
653
+
626
654
  if not credentials.can_stage():
627
655
  raise PermissionError(
628
656
  f"Agent {credentials.agent_id} (Tier {credentials.tier.name}) "
@@ -653,10 +681,17 @@ class RedisShortTermMemory:
653
681
  Returns:
654
682
  Retrieved data or None if not found
655
683
 
684
+ Raises:
685
+ ValueError: If key is empty or invalid
686
+
656
687
  Example:
657
688
  >>> data = memory.retrieve("analysis_v1", creds)
658
689
 
659
690
  """
691
+ # Pattern 1: String ID validation
692
+ if not key or not key.strip():
693
+ raise ValueError("key cannot be empty")
694
+
660
695
  owner = agent_id or credentials.agent_id
661
696
  full_key = f"{self.PREFIX_WORKING}{owner}:{key}"
662
697
  raw = self._get(full_key)
@@ -704,7 +739,15 @@ class RedisShortTermMemory:
704
739
  Returns:
705
740
  True if staged successfully
706
741
 
742
+ Raises:
743
+ TypeError: If pattern is not StagedPattern
744
+ PermissionError: If credentials lack staging access
745
+
707
746
  """
747
+ # Pattern 5: Type validation
748
+ if not isinstance(pattern, StagedPattern):
749
+ raise TypeError(f"pattern must be StagedPattern, got {type(pattern).__name__}")
750
+
708
751
  if not credentials.can_stage():
709
752
  raise PermissionError(
710
753
  f"Agent {credentials.agent_id} cannot stage patterns. "
@@ -732,7 +775,14 @@ class RedisShortTermMemory:
732
775
  Returns:
733
776
  StagedPattern or None
734
777
 
778
+ Raises:
779
+ ValueError: If pattern_id is empty
780
+
735
781
  """
782
+ # Pattern 1: String ID validation
783
+ if not pattern_id or not pattern_id.strip():
784
+ raise ValueError("pattern_id cannot be empty")
785
+
736
786
  key = f"{self.PREFIX_STAGED}{pattern_id}"
737
787
  raw = self._get(key)
738
788
 
@@ -844,7 +894,22 @@ class RedisShortTermMemory:
844
894
  Returns:
845
895
  ConflictContext for resolution
846
896
 
897
+ Raises:
898
+ ValueError: If conflict_id is empty
899
+ TypeError: If positions or interests are not dicts
900
+ PermissionError: If credentials lack permission
901
+
847
902
  """
903
+ # Pattern 1: String ID validation
904
+ if not conflict_id or not conflict_id.strip():
905
+ raise ValueError("conflict_id cannot be empty")
906
+
907
+ # Pattern 5: Type validation
908
+ if not isinstance(positions, dict):
909
+ raise TypeError(f"positions must be dict, got {type(positions).__name__}")
910
+ if not isinstance(interests, dict):
911
+ raise TypeError(f"interests must be dict, got {type(interests).__name__}")
912
+
848
913
  if not credentials.can_stage():
849
914
  raise PermissionError(
850
915
  f"Agent {credentials.agent_id} cannot create conflict context. "
@@ -881,7 +946,14 @@ class RedisShortTermMemory:
881
946
  Returns:
882
947
  ConflictContext or None
883
948
 
949
+ Raises:
950
+ ValueError: If conflict_id is empty
951
+
884
952
  """
953
+ # Pattern 1: String ID validation
954
+ if not conflict_id or not conflict_id.strip():
955
+ raise ValueError("conflict_id cannot be empty")
956
+
885
957
  key = f"{self.PREFIX_CONFLICT}{conflict_id}"
886
958
  raw = self._get(key)
887
959
 
@@ -1014,7 +1086,19 @@ class RedisShortTermMemory:
1014
1086
  Returns:
1015
1087
  True if created
1016
1088
 
1089
+ Raises:
1090
+ ValueError: If session_id is empty
1091
+ TypeError: If metadata is not dict
1092
+
1017
1093
  """
1094
+ # Pattern 1: String ID validation
1095
+ if not session_id or not session_id.strip():
1096
+ raise ValueError("session_id cannot be empty")
1097
+
1098
+ # Pattern 5: Type validation
1099
+ if metadata is not None and not isinstance(metadata, dict):
1100
+ raise TypeError(f"metadata must be dict, got {type(metadata).__name__}")
1101
+
1018
1102
  key = f"{self.PREFIX_SESSION}{session_id}"
1019
1103
  payload = {
1020
1104
  "session_id": session_id,
@@ -1039,7 +1123,14 @@ class RedisShortTermMemory:
1039
1123
  Returns:
1040
1124
  True if joined
1041
1125
 
1126
+ Raises:
1127
+ ValueError: If session_id is empty
1128
+
1042
1129
  """
1130
+ # Pattern 1: String ID validation
1131
+ if not session_id or not session_id.strip():
1132
+ raise ValueError("session_id cannot be empty")
1133
+
1043
1134
  key = f"{self.PREFIX_SESSION}{session_id}"
1044
1135
  raw = self._get(key)
1045
1136
 
@@ -1164,11 +1255,19 @@ class RedisShortTermMemory:
1164
1255
  Returns:
1165
1256
  Number of items successfully stashed
1166
1257
 
1258
+ Raises:
1259
+ TypeError: If items is not a list
1260
+ PermissionError: If credentials lack write access
1261
+
1167
1262
  Example:
1168
1263
  >>> items = [("key1", {"a": 1}), ("key2", {"b": 2})]
1169
1264
  >>> count = memory.stash_batch(items, creds)
1170
1265
 
1171
1266
  """
1267
+ # Pattern 5: Type validation
1268
+ if not isinstance(items, list):
1269
+ raise TypeError(f"items must be list, got {type(items).__name__}")
1270
+
1172
1271
  if not credentials.can_stage():
1173
1272
  raise PermissionError(
1174
1273
  f"Agent {credentials.agent_id} cannot write to memory. "
@@ -2042,12 +2141,23 @@ class RedisShortTermMemory:
2042
2141
  Returns:
2043
2142
  Tuple of (success, pattern, message)
2044
2143
 
2144
+ Raises:
2145
+ ValueError: If pattern_id is empty or min_confidence out of range
2146
+
2045
2147
  Example:
2046
2148
  >>> success, pattern, msg = memory.atomic_promote_pattern("pat_123", creds, min_confidence=0.7)
2047
2149
  >>> if success:
2048
2150
  ... library.add(pattern)
2049
2151
 
2050
2152
  """
2153
+ # Pattern 1: String ID validation
2154
+ if not pattern_id or not pattern_id.strip():
2155
+ raise ValueError("pattern_id cannot be empty")
2156
+
2157
+ # Pattern 4: Range validation
2158
+ if not 0.0 <= min_confidence <= 1.0:
2159
+ raise ValueError(f"min_confidence must be between 0.0 and 1.0, got {min_confidence}")
2160
+
2051
2161
  if not credentials.can_validate():
2052
2162
  return False, None, "Requires VALIDATOR tier or higher"
2053
2163
 
@@ -53,7 +53,14 @@ def estimate_tokens(text: str, model_id: str = "claude-sonnet-4-5-20250514") ->
53
53
  Returns:
54
54
  Estimated token count
55
55
 
56
+ Raises:
57
+ ValueError: If model_id is empty
58
+
56
59
  """
60
+ # Pattern 1: String ID validation
61
+ if not model_id or not model_id.strip():
62
+ raise ValueError("model_id cannot be empty")
63
+
57
64
  if not text:
58
65
  return 0
59
66
 
@@ -99,9 +106,18 @@ def estimate_workflow_cost(
99
106
  "risk": "low" | "medium" | "high"
100
107
  }
101
108
 
109
+ Raises:
110
+ ValueError: If workflow_name or provider is empty
111
+
102
112
  """
103
113
  from .registry import get_model, get_supported_providers
104
114
 
115
+ # Pattern 1: String ID validation
116
+ if not workflow_name or not workflow_name.strip():
117
+ raise ValueError("workflow_name cannot be empty")
118
+ if not provider or not provider.strip():
119
+ raise ValueError("provider cannot be empty")
120
+
105
121
  # Validate provider
106
122
  if provider not in get_supported_providers():
107
123
  provider = "anthropic" # Default fallback
@@ -292,10 +308,19 @@ def estimate_single_call_cost(
292
308
  Returns:
293
309
  Cost estimate dictionary
294
310
 
311
+ Raises:
312
+ ValueError: If task_type or provider is empty
313
+
295
314
  """
296
315
  from .registry import get_model
297
316
  from .tasks import get_tier_for_task
298
317
 
318
+ # Pattern 1: String ID validation
319
+ if not task_type or not task_type.strip():
320
+ raise ValueError("task_type cannot be empty")
321
+ if not provider or not provider.strip():
322
+ raise ValueError("provider cannot be empty")
323
+
299
324
  input_tokens = estimate_tokens(text)
300
325
 
301
326
  # Get tier for task