chuk-ai-session-manager 0.7.1__py3-none-any.whl → 0.8.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 (46) hide show
  1. chuk_ai_session_manager/__init__.py +84 -40
  2. chuk_ai_session_manager/api/__init__.py +1 -1
  3. chuk_ai_session_manager/api/simple_api.py +53 -59
  4. chuk_ai_session_manager/exceptions.py +31 -17
  5. chuk_ai_session_manager/guards/__init__.py +118 -0
  6. chuk_ai_session_manager/guards/bindings.py +217 -0
  7. chuk_ai_session_manager/guards/cache.py +163 -0
  8. chuk_ai_session_manager/guards/manager.py +819 -0
  9. chuk_ai_session_manager/guards/models.py +498 -0
  10. chuk_ai_session_manager/guards/ungrounded.py +159 -0
  11. chuk_ai_session_manager/infinite_conversation.py +86 -79
  12. chuk_ai_session_manager/memory/__init__.py +247 -0
  13. chuk_ai_session_manager/memory/artifacts_bridge.py +469 -0
  14. chuk_ai_session_manager/memory/context_packer.py +347 -0
  15. chuk_ai_session_manager/memory/fault_handler.py +507 -0
  16. chuk_ai_session_manager/memory/manifest.py +307 -0
  17. chuk_ai_session_manager/memory/models.py +1084 -0
  18. chuk_ai_session_manager/memory/mutation_log.py +186 -0
  19. chuk_ai_session_manager/memory/pack_cache.py +206 -0
  20. chuk_ai_session_manager/memory/page_table.py +275 -0
  21. chuk_ai_session_manager/memory/prefetcher.py +192 -0
  22. chuk_ai_session_manager/memory/tlb.py +247 -0
  23. chuk_ai_session_manager/memory/vm_prompts.py +238 -0
  24. chuk_ai_session_manager/memory/working_set.py +574 -0
  25. chuk_ai_session_manager/models/__init__.py +21 -9
  26. chuk_ai_session_manager/models/event_source.py +3 -1
  27. chuk_ai_session_manager/models/event_type.py +10 -1
  28. chuk_ai_session_manager/models/session.py +103 -68
  29. chuk_ai_session_manager/models/session_event.py +69 -68
  30. chuk_ai_session_manager/models/session_metadata.py +9 -10
  31. chuk_ai_session_manager/models/session_run.py +21 -22
  32. chuk_ai_session_manager/models/token_usage.py +76 -76
  33. chuk_ai_session_manager/procedural_memory/__init__.py +70 -0
  34. chuk_ai_session_manager/procedural_memory/formatter.py +407 -0
  35. chuk_ai_session_manager/procedural_memory/manager.py +523 -0
  36. chuk_ai_session_manager/procedural_memory/models.py +371 -0
  37. chuk_ai_session_manager/sample_tools.py +79 -46
  38. chuk_ai_session_manager/session_aware_tool_processor.py +27 -16
  39. chuk_ai_session_manager/session_manager.py +259 -232
  40. chuk_ai_session_manager/session_prompt_builder.py +163 -111
  41. chuk_ai_session_manager/session_storage.py +45 -52
  42. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/METADATA +80 -4
  43. chuk_ai_session_manager-0.8.1.dist-info/RECORD +45 -0
  44. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/WHEEL +1 -1
  45. chuk_ai_session_manager-0.7.1.dist-info/RECORD +0 -22
  46. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/top_level.txt +0 -0
@@ -2,64 +2,75 @@
2
2
  """
3
3
  Exception classes for the chuk session manager.
4
4
 
5
- This module defines the exception hierarchy used throughout the
5
+ This module defines the exception hierarchy used throughout the
6
6
  session manager to provide specific, informative error conditions
7
7
  for various failure modes.
8
8
  """
9
9
 
10
+
10
11
  class SessionManagerError(Exception):
11
12
  """
12
13
  Base exception for all session manager errors.
13
-
14
+
14
15
  All other session manager exceptions inherit from this class,
15
16
  making it easy to catch all session-related errors with a single
16
17
  except clause if needed.
17
18
  """
19
+
18
20
  pass
19
21
 
20
22
 
21
23
  class SessionNotFound(SessionManagerError):
22
24
  """
23
25
  Raised when the requested session ID is not found in storage.
24
-
26
+
25
27
  This exception is typically raised when:
26
28
  - Attempting to retrieve a session with an invalid ID
27
29
  - Accessing a session that has been deleted
28
30
  - Using an ID that does not conform to expected format
29
31
  """
32
+
30
33
  def __init__(self, session_id=None, message=None):
31
34
  self.session_id = session_id
32
- default_message = f"Session not found: {session_id}" if session_id else "Session not found"
35
+ default_message = (
36
+ f"Session not found: {session_id}" if session_id else "Session not found"
37
+ )
33
38
  super().__init__(message or default_message)
34
39
 
35
40
 
36
41
  class SessionAlreadyExists(SessionManagerError):
37
42
  """
38
43
  Raised when attempting to create a session with an ID that already exists.
39
-
44
+
40
45
  This exception is typically raised during session creation when:
41
46
  - Explicitly setting an ID that conflicts with an existing session
42
47
  - A UUID collision occurs (extremely rare)
43
48
  """
49
+
44
50
  def __init__(self, session_id=None, message=None):
45
51
  self.session_id = session_id
46
- default_message = f"Session already exists: {session_id}" if session_id else "Session already exists"
52
+ default_message = (
53
+ f"Session already exists: {session_id}"
54
+ if session_id
55
+ else "Session already exists"
56
+ )
47
57
  super().__init__(message or default_message)
48
58
 
49
59
 
50
60
  class InvalidSessionOperation(SessionManagerError):
51
61
  """
52
62
  Raised when attempting an invalid operation on a session.
53
-
63
+
54
64
  This exception is typically raised when:
55
65
  - Performing operations on a closed or archived session
56
66
  - Adding events with incorrect sequencing or relationships
57
67
  - Attempting unsupported operations in the current session state
58
68
  """
69
+
59
70
  def __init__(self, operation=None, reason=None, message=None):
60
71
  self.operation = operation
61
72
  self.reason = reason
62
-
73
+
63
74
  if message:
64
75
  default_message = message
65
76
  elif operation and reason:
@@ -68,55 +79,58 @@ class InvalidSessionOperation(SessionManagerError):
68
79
  default_message = f"Invalid operation: {operation}"
69
80
  else:
70
81
  default_message = "Invalid session operation"
71
-
82
+
72
83
  super().__init__(default_message)
73
84
 
74
85
 
75
86
  class TokenLimitExceeded(SessionManagerError):
76
87
  """
77
88
  Raised when a token limit is exceeded in a session operation.
78
-
89
+
79
90
  This exception is typically raised when:
80
91
  - Adding content that would exceed configured token limits
81
92
  - Attempting to generate a prompt that exceeds model token limits
82
93
  """
94
+
83
95
  def __init__(self, limit=None, actual=None, message=None):
84
96
  self.limit = limit
85
97
  self.actual = actual
86
-
98
+
87
99
  if message:
88
100
  default_message = message
89
101
  elif limit and actual:
90
102
  default_message = f"Token limit exceeded: {actual} > {limit}"
91
103
  else:
92
104
  default_message = "Token limit exceeded"
93
-
105
+
94
106
  super().__init__(default_message)
95
107
 
96
108
 
97
109
  class StorageError(SessionManagerError):
98
110
  """
99
111
  Raised when a session storage operation fails.
100
-
112
+
101
113
  This is a base class for more specific storage errors.
102
114
  It can be raised directly for general storage failures.
103
115
  """
116
+
104
117
  pass
105
118
 
106
119
 
107
120
  class ToolProcessingError(SessionManagerError):
108
121
  """
109
122
  Raised when tool processing fails in a session.
110
-
123
+
111
124
  This exception is typically raised when:
112
125
  - A tool execution fails after all retries
113
126
  - Invalid tool parameters are provided
114
127
  - Tool results cannot be properly processed
115
128
  """
129
+
116
130
  def __init__(self, tool_name=None, reason=None, message=None):
117
131
  self.tool_name = tool_name
118
132
  self.reason = reason
119
-
133
+
120
134
  if message:
121
135
  default_message = message
122
136
  elif tool_name and reason:
@@ -125,5 +139,5 @@ class ToolProcessingError(SessionManagerError):
125
139
  default_message = f"Tool '{tool_name}' processing error"
126
140
  else:
127
141
  default_message = "Tool processing error"
128
-
129
- super().__init__(default_message)
142
+
143
+ super().__init__(default_message)
@@ -0,0 +1,118 @@
1
+ # chuk_ai_session_manager/guards/__init__.py
2
+ """Conversation guards and state management.
3
+
4
+ Components:
5
+ - ToolStateManager: Coordinator for guards, bindings, and cache
6
+ - BindingManager: $vN reference system for tool result tracking
7
+ - ResultCache: Tool result caching for deduplication
8
+ - UngroundedGuard: Detects missing $vN references
9
+ - Models: All Pydantic models for state management
10
+
11
+ Runtime guards (BudgetGuard, RunawayGuard, etc.) are imported from chuk-tool-processor.
12
+ """
13
+
14
+ # Models
15
+ from chuk_ai_session_manager.guards.models import (
16
+ CachedToolResult,
17
+ CacheScope,
18
+ EnforcementLevel,
19
+ NamedVariable,
20
+ PerToolCallStatus,
21
+ ReferenceCheckResult,
22
+ RepairAction,
23
+ RunawayStatus,
24
+ RuntimeLimits,
25
+ RuntimeMode,
26
+ SoftBlock,
27
+ SoftBlockReason,
28
+ ToolClassification,
29
+ UngroundedCallResult,
30
+ UnusedResultAction,
31
+ ValueBinding,
32
+ ValueType,
33
+ classify_value_type,
34
+ compute_args_hash,
35
+ )
36
+
37
+ # Sub-managers
38
+ from chuk_ai_session_manager.guards.bindings import BindingManager
39
+ from chuk_ai_session_manager.guards.cache import ResultCache
40
+
41
+ # Chat-specific guard
42
+ from chuk_ai_session_manager.guards.ungrounded import (
43
+ UngroundedGuard,
44
+ UngroundedGuardConfig,
45
+ )
46
+
47
+ # Coordinator
48
+ from chuk_ai_session_manager.guards.manager import (
49
+ ToolStateManager,
50
+ get_tool_state,
51
+ reset_tool_state,
52
+ )
53
+
54
+ # Re-export runtime guards from chuk-tool-processor
55
+ from chuk_tool_processor.guards import (
56
+ BaseGuard,
57
+ BudgetGuard,
58
+ BudgetGuardConfig,
59
+ BudgetState,
60
+ Guard,
61
+ GuardResult,
62
+ GuardVerdict,
63
+ PerToolGuard,
64
+ PerToolGuardConfig,
65
+ PreconditionGuard,
66
+ PreconditionGuardConfig,
67
+ RunawayGuard,
68
+ RunawayGuardConfig,
69
+ )
70
+
71
+ __all__ = [
72
+ # Manager
73
+ "ToolStateManager",
74
+ "get_tool_state",
75
+ "reset_tool_state",
76
+ # Sub-managers
77
+ "BindingManager",
78
+ "ResultCache",
79
+ # Guards (from chuk-tool-processor)
80
+ "BaseGuard",
81
+ "Guard",
82
+ "GuardResult",
83
+ "GuardVerdict",
84
+ "BudgetGuard",
85
+ "BudgetGuardConfig",
86
+ "BudgetState",
87
+ "PerToolGuard",
88
+ "PerToolGuardConfig",
89
+ "PreconditionGuard",
90
+ "PreconditionGuardConfig",
91
+ "RunawayGuard",
92
+ "RunawayGuardConfig",
93
+ # Chat-specific guard
94
+ "UngroundedGuard",
95
+ "UngroundedGuardConfig",
96
+ # Enums and Constants
97
+ "CacheScope",
98
+ "EnforcementLevel",
99
+ "RuntimeMode",
100
+ "UnusedResultAction",
101
+ "ValueType",
102
+ "ToolClassification",
103
+ # Models
104
+ "CachedToolResult",
105
+ "NamedVariable",
106
+ "PerToolCallStatus",
107
+ "ReferenceCheckResult",
108
+ "RepairAction",
109
+ "RunawayStatus",
110
+ "RuntimeLimits",
111
+ "SoftBlock",
112
+ "SoftBlockReason",
113
+ "UngroundedCallResult",
114
+ "ValueBinding",
115
+ # Helpers
116
+ "classify_value_type",
117
+ "compute_args_hash",
118
+ ]
@@ -0,0 +1,217 @@
1
+ # chuk_ai_session_manager/guards/bindings.py
2
+ """Value binding system for $vN references.
3
+
4
+ Every tool result gets assigned a stable ID (v1, v2, v3...) that can
5
+ be referenced in subsequent tool calls using $vN syntax.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from chuk_ai_session_manager.guards.models import (
17
+ ValueBinding,
18
+ classify_value_type,
19
+ compute_args_hash,
20
+ )
21
+
22
+ # Reference pattern: $v1, $v2, ${v1}, ${myalias}
23
+ REFERENCE_PATTERN = re.compile(r"\$\{?([a-zA-Z_][a-zA-Z0-9_]*|v\d+)\}?")
24
+
25
+
26
+ class BindingManager(BaseModel):
27
+ """Manages value bindings for $vN references.
28
+
29
+ Pydantic-native implementation with all state in the model.
30
+ """
31
+
32
+ bindings: dict[str, ValueBinding] = Field(default_factory=dict)
33
+ alias_to_id: dict[str, str] = Field(default_factory=dict)
34
+ next_id: int = Field(default=1)
35
+
36
+ model_config = {"arbitrary_types_allowed": True}
37
+
38
+ def bind(
39
+ self,
40
+ tool_name: str,
41
+ arguments: dict[str, Any],
42
+ value: Any,
43
+ aliases: list[str] | None = None,
44
+ ) -> ValueBinding:
45
+ """Bind a tool result to a value ID.
46
+
47
+ Args:
48
+ tool_name: Name of the tool that produced this value
49
+ arguments: Arguments passed to the tool
50
+ value: The result value
51
+ aliases: Optional model-provided names
52
+
53
+ Returns:
54
+ The created ValueBinding
55
+ """
56
+ value_id = f"v{self.next_id}"
57
+ self.next_id += 1
58
+
59
+ binding = ValueBinding(
60
+ id=value_id,
61
+ tool_name=tool_name,
62
+ args_hash=compute_args_hash(arguments),
63
+ raw_value=value,
64
+ value_type=classify_value_type(value),
65
+ aliases=aliases or [],
66
+ )
67
+
68
+ self.bindings[value_id] = binding
69
+
70
+ # Register aliases
71
+ for alias in binding.aliases:
72
+ self.alias_to_id[alias] = value_id
73
+
74
+ return binding
75
+
76
+ def get(self, ref: str) -> ValueBinding | None:
77
+ """Get a binding by ID or alias."""
78
+ if ref in self.bindings:
79
+ return self.bindings[ref]
80
+ if ref in self.alias_to_id:
81
+ value_id = self.alias_to_id[ref]
82
+ return self.bindings.get(value_id)
83
+ return None
84
+
85
+ def add_alias(self, value_id: str, alias: str) -> bool:
86
+ """Add an alias to an existing binding."""
87
+ if value_id not in self.bindings:
88
+ return False
89
+ binding = self.bindings[value_id]
90
+ if alias not in binding.aliases:
91
+ binding.aliases.append(alias)
92
+ self.alias_to_id[alias] = value_id
93
+ return True
94
+
95
+ def mark_used(self, ref: str, used_in: str) -> None:
96
+ """Mark a value as having been used."""
97
+ binding = self.get(ref)
98
+ if binding:
99
+ binding.used = True
100
+ binding.used_in.append(used_in)
101
+
102
+ def get_numeric_values(self) -> set[float]:
103
+ """Get all numeric values from bindings.
104
+
105
+ Returns:
106
+ Set of float values from all bindings that have numeric raw_value
107
+ """
108
+ values: set[float] = set()
109
+ for binding in self.bindings.values():
110
+ if isinstance(binding.raw_value, (int, float)):
111
+ values.add(float(binding.raw_value))
112
+ return values
113
+
114
+ def resolve_references(self, arguments: dict[str, Any]) -> dict[str, Any]:
115
+ """Resolve $vN references in arguments to actual values."""
116
+ args_str = json.dumps(arguments, default=str)
117
+
118
+ def replace_ref(match: re.Match[str]) -> str:
119
+ ref = match.group(1)
120
+ binding = self.get(ref)
121
+ if binding:
122
+ self.mark_used(ref, "arg_resolution")
123
+ value = binding.typed_value
124
+ if isinstance(value, (int, float)):
125
+ return str(value)
126
+ return json.dumps(value)
127
+ return str(match.group(0))
128
+
129
+ resolved_str = REFERENCE_PATTERN.sub(replace_ref, args_str)
130
+
131
+ try:
132
+ result: dict[str, Any] = json.loads(resolved_str)
133
+ return result
134
+ except json.JSONDecodeError:
135
+ return arguments
136
+
137
+ def check_references(
138
+ self, arguments: dict[str, Any]
139
+ ) -> tuple[bool, list[str], dict[str, Any]]:
140
+ """Check if all $vN references in arguments exist.
141
+
142
+ Returns:
143
+ Tuple of (all_valid, missing_refs, resolved_values)
144
+ """
145
+ args_str = json.dumps(arguments, default=str)
146
+ matches = REFERENCE_PATTERN.findall(args_str)
147
+
148
+ missing: list[str] = []
149
+ resolved: dict[str, Any] = {}
150
+
151
+ for ref in matches:
152
+ binding = self.get(ref)
153
+ if binding is None:
154
+ missing.append(ref)
155
+ else:
156
+ resolved[ref] = binding.typed_value
157
+
158
+ return len(missing) == 0, missing, resolved
159
+
160
+ def find_by_value(
161
+ self, value: float, tolerance: float = 0.0001
162
+ ) -> ValueBinding | None:
163
+ """Find a binding with a matching value."""
164
+ for binding in self.bindings.values():
165
+ try:
166
+ binding_val = float(binding.typed_value)
167
+ if value == binding_val:
168
+ return binding
169
+ if abs(value) > 1e-10 and abs(binding_val) > 1e-10:
170
+ if (
171
+ abs(value - binding_val) / max(abs(value), abs(binding_val))
172
+ < tolerance
173
+ ):
174
+ return binding
175
+ except (ValueError, TypeError):
176
+ continue
177
+ return None
178
+
179
+ def get_unused(self) -> list[ValueBinding]:
180
+ """Get all bindings that haven't been used."""
181
+ return [b for b in self.bindings.values() if not b.used]
182
+
183
+ def format_for_model(self) -> str:
184
+ """Format all bindings for display to the model."""
185
+ if not self.bindings:
186
+ return ""
187
+
188
+ lines = ["**Available Values (reference with $vN):**"]
189
+ for binding in self.bindings.values():
190
+ status = "✓" if binding.used else "○"
191
+ lines.append(f" {status} {binding.format_for_model()}")
192
+
193
+ return "\n".join(lines)
194
+
195
+ def format_unused_warning(self) -> str:
196
+ """Generate warning about unused tool results."""
197
+ unused = self.get_unused()
198
+ if not unused:
199
+ return ""
200
+
201
+ ids = ", ".join(f"${b.id}" for b in unused)
202
+ return (
203
+ f"**Note:** You called tools producing {ids} but haven't referenced them. "
204
+ "Either use these values or explain why they're not needed."
205
+ )
206
+
207
+ def reset(self) -> None:
208
+ """Reset all bindings."""
209
+ self.bindings.clear()
210
+ self.alias_to_id.clear()
211
+ self.next_id = 1
212
+
213
+ def __len__(self) -> int:
214
+ return len(self.bindings)
215
+
216
+ def __bool__(self) -> bool:
217
+ return bool(self.bindings)
@@ -0,0 +1,163 @@
1
+ # chuk_ai_session_manager/guards/cache.py
2
+ """Tool result caching.
3
+
4
+ Caches tool call results so duplicates return cached values.
5
+ Prevents the model from re-calling tools unnecessarily.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from chuk_ai_session_manager.guards.models import CachedToolResult, NamedVariable
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class ResultCache(BaseModel):
22
+ """Caches tool results for deduplication.
23
+
24
+ Pydantic-native implementation.
25
+ """
26
+
27
+ cache: dict[str, CachedToolResult] = Field(default_factory=dict)
28
+ variables: dict[str, NamedVariable] = Field(default_factory=dict)
29
+ call_order: list[str] = Field(default_factory=list)
30
+ max_size: int = Field(default=100)
31
+ duplicate_count: int = Field(default=0)
32
+
33
+ model_config = {"arbitrary_types_allowed": True}
34
+
35
+ def get(self, tool_name: str, arguments: dict[str, Any]) -> CachedToolResult | None:
36
+ """Check if we have a cached result for this exact tool call."""
37
+ signature = self._make_signature(tool_name, arguments)
38
+ cached = self.cache.get(signature)
39
+ if cached:
40
+ cached.call_count += 1
41
+ self.duplicate_count += 1
42
+ log.info(f"Cache hit for {tool_name} (call #{cached.call_count})")
43
+ return cached
44
+
45
+ def put(
46
+ self,
47
+ tool_name: str,
48
+ arguments: dict[str, Any],
49
+ result: Any,
50
+ ) -> CachedToolResult:
51
+ """Cache a tool result."""
52
+ # Evict if full
53
+ if len(self.cache) >= self.max_size:
54
+ self._evict_oldest()
55
+
56
+ signature = self._make_signature(tool_name, arguments)
57
+ cached = CachedToolResult(
58
+ tool_name=tool_name,
59
+ arguments=arguments,
60
+ result=result,
61
+ )
62
+ self.cache[signature] = cached
63
+ self.call_order.append(signature)
64
+
65
+ log.debug(f"Cached result for {tool_name}: {cached.format_compact()}")
66
+ return cached
67
+
68
+ def store_variable(
69
+ self,
70
+ name: str,
71
+ value: float,
72
+ units: str | None = None,
73
+ source_tool: str | None = None,
74
+ source_args: dict[str, Any] | None = None,
75
+ ) -> NamedVariable:
76
+ """Store a named variable from a computation."""
77
+ var = NamedVariable(
78
+ name=name,
79
+ value=value,
80
+ units=units,
81
+ source_tool=source_tool,
82
+ source_args=source_args,
83
+ )
84
+ self.variables[name] = var
85
+ log.debug(f"Stored variable: {var.format_compact()}")
86
+ return var
87
+
88
+ def get_variable(self, name: str) -> NamedVariable | None:
89
+ """Get a stored variable by name."""
90
+ return self.variables.get(name)
91
+
92
+ def format_state(self, max_items: int = 10) -> str:
93
+ """Generate compact state summary for model context."""
94
+ lines = []
95
+
96
+ # Named variables first
97
+ if self.variables:
98
+ lines.append("**Stored Variables:**")
99
+ for var in list(self.variables.values())[:max_items]:
100
+ lines.append(f" {var.format_compact()}")
101
+
102
+ # Recent tool results
103
+ recent_sigs = self.call_order[-max_items:]
104
+ recent_results = [self.cache[sig] for sig in recent_sigs if sig in self.cache]
105
+
106
+ if recent_results:
107
+ if lines:
108
+ lines.append("")
109
+ lines.append("**Computed Values:**")
110
+ for cached in recent_results:
111
+ lines.append(f" {cached.format_compact()}")
112
+
113
+ return "\n".join(lines) if lines else ""
114
+
115
+ def format_duplicate_message(
116
+ self, tool_name: str, arguments: dict[str, Any]
117
+ ) -> str:
118
+ """Generate message when duplicate call is detected."""
119
+ cached = self.get(tool_name, arguments)
120
+ if not cached:
121
+ return f"Tool {tool_name} was called but no cached result available."
122
+
123
+ lines = [
124
+ f"**Cached result for {tool_name}:** {cached.result}",
125
+ "",
126
+ "This value was already computed. Use it directly.",
127
+ "",
128
+ ]
129
+
130
+ state = self.format_state()
131
+ if state:
132
+ lines.append(state)
133
+
134
+ return "\n".join(lines)
135
+
136
+ def get_stats(self) -> dict[str, Any]:
137
+ """Get cache statistics."""
138
+ return {
139
+ "total_cached": len(self.cache),
140
+ "total_variables": len(self.variables),
141
+ "duplicate_calls": self.duplicate_count,
142
+ "call_order_length": len(self.call_order),
143
+ }
144
+
145
+ def reset(self) -> None:
146
+ """Clear all cached state."""
147
+ self.cache.clear()
148
+ self.variables.clear()
149
+ self.call_order.clear()
150
+ self.duplicate_count = 0
151
+
152
+ def _make_signature(self, tool_name: str, arguments: dict[str, Any]) -> str:
153
+ """Create unique signature for a tool call."""
154
+ args_str = json.dumps(arguments, sort_keys=True, default=str)
155
+ return f"{tool_name}:{args_str}"
156
+
157
+ def _evict_oldest(self) -> None:
158
+ """Evict oldest cached result."""
159
+ if self.call_order:
160
+ oldest_sig = self.call_order.pop(0)
161
+ if oldest_sig in self.cache:
162
+ del self.cache[oldest_sig]
163
+ log.debug("Evicted oldest cache entry")