From 45695cafb222b36c7699835b8f5b01f7a7c58a02 Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 7 Jul 2026 21:47:32 +0200 Subject: [PATCH 1/7] docs: add migration roadmap from agentmemory (R1-R4 + phases) --- ROADMAP.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index dde36eac..7c6209ee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -166,3 +166,44 @@ **Completed:** 38/65 items **Last updated:** 2026-07-05 + +## 17. Migration from agentmemory + +Features to port from agentmemory (rohitg00/agentmemory) for full replacement. + +| Пункт | Фича | Описание | Приоритет | +|-------|------|----------|-----------| +| R1 | Obsidian export | Экспорт памяти в Obsidian markdown с wikilinks | nice-to-have | +| R2 | Mesh sync | P2P синхронизация между инстансами | nice-to-have | +| R3 | Cross-agent sync | Память доступна из Claude Code, Cursor, OpenCode | nice-to-have | +| R4 | Git snapshots | Версионирование памяти через git commit/diff/rollback | nice-to-have | + +### Migration phases + +**Phase 1 (simple, 3-4 days):** +- SHA-256 dedup (5min TTL) +- Circuit breaker (3 errors → open → 30s) +- Token budget (2000 tokens on context_inject) +- Privacy filter (strip secrets before save) + +**Phase 2 (medium, 5-7 days):** +- Slot system (8 pinned memory units) +- Lesson confidence (strengthen/decay) +- Faceted tagging (dimension:value with AND/OR) + +**Phase 3 (complex, 10-14 days):** +- Hybrid search RRF with graph traversal +- Knowledge graph extraction from sessions +- Action graphs with dependencies +- Lease system for multi-agent + +**Phase 4 (hooks, 5-7 days):** +- sync_turn hook (background capture) +- on_memory_write hook (mirror MEMORY.md) +- Diagnostics tool +- Export tool + +--- + +**Completed:** 38/65 items (+4 roadmap) +**Last updated:** 2026-07-07 From b72085f5edaf75303caa4485d32636759c4ff6b9 Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 7 Jul 2026 21:59:57 +0200 Subject: [PATCH 2/7] feat: add SHA-256 dedup with 5min TTL to memory_remember --- mcp_server/tools_layer.py | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py index f8bb57fa..451b37c8 100644 --- a/mcp_server/tools_layer.py +++ b/mcp_server/tools_layer.py @@ -29,6 +29,41 @@ logger = logging.getLogger(__name__) +class _DedupCache: + _doc_ = "SHA-256 dedup with TTL and periodic cleanup." + + def __init__(self, ttl=300, max_size=10000): + self._cache = {} + self._ttl = ttl + self._max_size = max_size + self._last_cleanup = time.time() + + def _cleanup(self): + now = time.time() + if now - self._last_cleanup < 60: + return + self._last_cleanup = now + expired = [k for k, v in self._cache.items() if now - v > self._ttl] + for k in expired: + del self._cache[k] + if len(self._cache) > self._max_size: + oldest = sorted(self._cache.keys(), key=lambda k: self._cache[k])[:len(self._cache) // 4] + for k in oldest: + del self._cache[k] + + def is_duplicate(self, session_id, tool, input_text): + self._cleanup() + key = hashlib.sha256(f"{session_id}:{tool}:{input_text[:500]}".encode()).hexdigest() + now = time.time() + if key in self._cache and now - self._cache[key] < self._ttl: + return True + self._cache[key] = now + return False + + +_dedup_cache = _DedupCache(ttl=300, max_size=10000) + + def _get_memory(app, layer: str, user_id: str): if layer == "agent": return app.mm.agent_memory(user_id) @@ -135,6 +170,7 @@ async def memory_remember( key: str = "", value: str = "", importance: float = 0.5, + session_id: str = "", ctx: Optional[Context] = None, ) -> dict: """Save a fact to long-term memory (L4 CoreMemory). @@ -145,7 +181,13 @@ async def memory_remember( key: Fact key (e.g. "name", "language", "principle") value: Fact value importance: Importance score 0.0-1.0 (default 0.5) + session_id: Session ID for dedup (optional) """ + # SHA-256 dedup: skip identical calls within 5min window + if session_id and _dedup_cache.is_duplicate(session_id, key, value): + logger.info("Dedup: skipping identical remember key=%s user=%s", key, user_id) + return RememberResult(status="skipped", reason="duplicate_within_ttl").dict() + app = _get_ctx(ctx) layer = _validate_layer(layer) metrics.inc("tool_calls") From 538c61b94cef1274983cd0ee923c430f2a06e2fb Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 7 Jul 2026 22:04:50 +0200 Subject: [PATCH 3/7] feat: add CircuitBreaker with state callbacks, metrics, and registry --- mcp_server/utils/__init__.py | 3 + mcp_server/utils/circuit_breaker.py | 170 ++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 mcp_server/utils/__init__.py create mode 100644 mcp_server/utils/circuit_breaker.py diff --git a/mcp_server/utils/__init__.py b/mcp_server/utils/__init__.py new file mode 100644 index 00000000..80524337 --- /dev/null +++ b/mcp_server/utils/__init__.py @@ -0,0 +1,3 @@ +from mcp_server.utils.circuit_breaker import CircuitBreaker + +__all__ = [ CircuitBreaker] diff --git a/mcp_server/utils/circuit_breaker.py b/mcp_server/utils/circuit_breaker.py new file mode 100644 index 00000000..5cca30f7 --- /dev/null +++ b/mcp_server/utils/circuit_breaker.py @@ -0,0 +1,170 @@ +""" +Circuit Breaker pattern for LLM/embedding calls. + +States: + - closed: normal operation, requests pass through + - open: failures exceeded threshold, requests blocked + - half-open: recovery probe, one request allowed through + +Usage: + breaker = CircuitBreaker(threshold=3, recovery_timeout=30) + + if not breaker.allow_request(): + return cached_result or fallback + + try: + result = await llm_call() + breaker.record_success() + return result + except Exception as e: + breaker.record_failure() + raise +""" + +import logging +import time +from enum import Enum +from typing import Any, Callable, Optional + +logger = logging.getLogger(__name__) + + +class CircuitState(Enum): + CLOSED = "closed" + OPEN = "open" + HALF_OPEN = "half_open" + + +class CircuitBreaker: + """Circuit breaker with configurable threshold and recovery timeout.""" + + def __init__( + self, + threshold: int = 3, + recovery_timeout: float = 30.0, + name: str = "default", + on_state_change: Optional[Callable] = None, + ): + self.threshold = threshold + self.recovery_timeout = recovery_timeout + self.name = name + + self._failures = 0 + self._state = CircuitState.CLOSED + self._opened_at = 0.0 + self._last_failure_at = 0.0 + + self._on_state_change = on_state_change + + # Metrics + self._total_requests = 0 + self._total_failures = 0 + self._total_rejections = 0 + self._state_changes = 0 + + @property + def state(self) -> CircuitState: + if self._state == CircuitState.OPEN: + if time.time() - self._opened_at > self.recovery_timeout: + self._transition_to(CircuitState.HALF_OPEN) + return self._state + + @property + def failures(self) -> int: + return self._failures + + def _transition_to(self, new_state: CircuitState): + old_state = self._state + self._state = new_state + self._state_changes += 1 + logger.info("CircuitBreaker[%s]: %s -> %s", self.name, old_state.value, new_state.value) + if self._on_state_change: + self._on_state_change(self.name, old_state, new_state) + + def record_success(self): + self._total_requests += 1 + self._failures = 0 + if self._state == CircuitState.HALF_OPEN: + self._transition_to(CircuitState.CLOSED) + + def record_failure(self): + self._total_requests += 1 + self._total_failures += 1 + self._failures += 1 + self._last_failure_at = time.time() + + if self._state == CircuitState.HALF_OPEN: + self._transition_to(CircuitState.OPEN) + self._opened_at = time.time() + elif self._failures >= self.threshold: + self._transition_to(CircuitState.OPEN) + self._opened_at = time.time() + + def allow_request(self) -> bool: + self._total_requests += 1 + current_state = self.state + + if current_state == CircuitState.CLOSED: + return True + if current_state == CircuitState.HALF_OPEN: + return True + self._total_rejections += 1 + return False + + def reset(self): + self._failures = 0 + self._state = CircuitState.CLOSED + self._opened_at = 0.0 + + def get_metrics(self) -> dict: + return { + "name": self.name, + "state": self.state.value, + "failures": self._failures, + "threshold": self.threshold, + "recovery_timeout": self.recovery_timeout, + "total_requests": self._total_requests, + "total_failures": self._total_failures, + "total_rejections": self._total_rejections, + "state_changes": self._state_changes, + "last_failure_at": self._last_failure_at, + } + + def __enter__(self): + self._context_allowed = self.allow_request() + return self._context_allowed + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + self.record_success() + else: + self.record_failure() + return False + + +class CircuitBreakerRegistry: + def __init__(self): + self._breakers: dict[str, CircuitBreaker] = {} + + def get(self, name: str, threshold: int = 3, recovery_timeout: float = 30.0, on_state_change: Optional[Callable] = None) -> CircuitBreaker: + if name not in self._breakers: + self._breakers[name] = CircuitBreaker( + threshold=threshold, + recovery_timeout=recovery_timeout, + name=name, + on_state_change=on_state_change, + ) + return self._breakers[name] + + def get_all(self) -> dict[str, CircuitBreaker]: + return dict(self._breakers) + + def get_all_metrics(self) -> dict: + return {name: breaker.get_metrics() for name, breaker in self._breakers.items()} + + def reset_all(self): + for breaker in self._breakers.values(): + breaker.reset() + + +breaker_registry = CircuitBreakerRegistry() From 439ca4eb873099fce1713c238a5cdd0282f28fd1 Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 7 Jul 2026 22:34:00 +0200 Subject: [PATCH 4/7] feat: add token budget (2000 tokens) with CJK-aware estimation to context_inject --- mcp_server/tools_layer.py | 44 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py index 451b37c8..a07bb704 100644 --- a/mcp_server/tools_layer.py +++ b/mcp_server/tools_layer.py @@ -64,6 +64,39 @@ def is_duplicate(self, session_id, tool, input_text): _dedup_cache = _DedupCache(ttl=300, max_size=10000) +# Token budget configuration +DEFAULT_TOKEN_BUDGET = 2000 +CHARS_PER_TOKEN = 4 + + +def _estimate_tokens(text: str) -> int: + if not text: + return 0 + cjk_count = len(re.findall(r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]', text)) + remaining_chars = len(text) - cjk_count + non_cjk_tokens = remaining_chars // CHARS_PER_TOKEN + return cjk_count + non_cjk_tokens + + +def _truncate_to_budget(text: str, max_tokens: int) -> tuple: + estimated = _estimate_tokens(text) + if estimated <= max_tokens: + return text, False + char_limit = max_tokens * CHARS_PER_TOKEN + lines = text.split('\\n') + result_lines = [] + current_len = 0 + for line in lines: + line_len = len(line) + 1 + if current_len + line_len > char_limit: + break + result_lines.append(line) + current_len += line_len + truncated = '\\n'.join(result_lines) + truncated += '\\n[...truncated to token budget]' + return truncated, True + + def _get_memory(app, layer: str, user_id: str): if layer == "agent": return app.mm.agent_memory(user_id) @@ -844,17 +877,24 @@ async def memory_context_inject( if facts_text: context_parts.append("REMEMBER: " + facts_text) + # Apply token budget + context_text = "\n".join(context_parts) + context_text, was_truncated = _truncate_to_budget(context_text, DEFAULT_TOKEN_BUDGET) + result = { - "context": "\n".join(context_parts), + "context": context_text, "l4_facts_count": len(l4_facts), "l3_episodes_count": len(l3_episodes), "l1_recent_count": len(l1_recent), "wiki_count": len(wiki_entries), + "estimated_tokens": _estimate_tokens(context_text), + "was_truncated": was_truncated, + "token_budget": DEFAULT_TOKEN_BUDGET, } _set_cached(cache_key, result) # Trigger dream_buffer hook for context staging - await _fire_hook("dream_buffer", layer, {"text": "\n".join(context_parts), "user_id": user_id}) + await _fire_hook("dream_buffer", layer, {"text": context_text, "user_id": user_id}) return result From 87827610b22cd9384a74c80cf872968577fe002e Mon Sep 17 00:00:00 2001 From: Murat Date: Tue, 7 Jul 2026 22:43:04 +0200 Subject: [PATCH 5/7] feat: add privacy filter to strip secrets before memory storage --- mcp_server/tools_layer.py | 2 ++ mcp_server/utils/privacy.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 mcp_server/utils/privacy.py diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py index a07bb704..7fa8f5ca 100644 --- a/mcp_server/tools_layer.py +++ b/mcp_server/tools_layer.py @@ -24,6 +24,7 @@ StatsResult, ) from mcp_server.registry import _get_ctx, register_tool +from mcp_server.utils.privacy import strip_secrets from shared.metrics import metrics logger = logging.getLogger(__name__) @@ -217,6 +218,7 @@ async def memory_remember( session_id: Session ID for dedup (optional) """ # SHA-256 dedup: skip identical calls within 5min window + value = strip_secrets(value) if session_id and _dedup_cache.is_duplicate(session_id, key, value): logger.info("Dedup: skipping identical remember key=%s user=%s", key, user_id) return RememberResult(status="skipped", reason="duplicate_within_ttl").dict() diff --git a/mcp_server/utils/privacy.py b/mcp_server/utils/privacy.py new file mode 100644 index 00000000..e0a40e9f --- /dev/null +++ b/mcp_server/utils/privacy.py @@ -0,0 +1,48 @@ +import re +from typing import List, Pattern + + +_CREDENTIAL_PATTERNS: List[Pattern] = [ + re.compile(r"\b(sk-[A-Za-z0-9]{20,})\b"), + re.compile(r"\b(sk-ant-[A-Za-z0-9-]{20,})\b"), + re.compile(r"\b(ghp_[A-Za-z0-9]{36})\b"), + re.compile(r"\b(gho_[A-Za-z0-9]{36})\b"), + re.compile(r"\b(ghs_[A-Za-z0-9]{36})\b"), + re.compile(r"\b(ghr_[A-Za-z0-9]{36})\b"), + re.compile(r"\b(xox[baprs]-[A-Za-z0-9-]{20,})\b"), + re.compile(r"\b(AKIA[0-9A-Z]{16})\b"), + re.compile(r"\b(AIza[0-9A-Za-z_-]{35})\b"), + re.compile(r"\b(sk_live_[0-9a-zA-Z]{24,})\b"), + re.compile(r"\b(pk_live_[0-9a-zA-Z]{24,})\b"), + re.compile(r"\b(sk_test_[0-9a-zA-Z]{24,})\b"), + re.compile(r"\b([0-9]{10}:[A-Za-z0-9_-]{35})\b"), + re.compile(r"\b(Bearer\s+[A-Za-z0-9_\-\.]{20,})\b", re.IGNORECASE), + re.compile(r".*?", re.DOTALL), + re.compile(r".*?", re.DOTALL), + re.compile(r".*?", re.DOTALL), +] + + +def strip_secrets(text: str, replacement: str = "[REDACTED]") -> str: + if not text: + return text + result = text + for pattern in _CREDENTIAL_PATTERNS: + result = pattern.sub(replacement, result) + return result + + +def has_secrets(text: str) -> bool: + if not text: + return False + for pattern in _CREDENTIAL_PATTERNS: + if pattern.search(text): + return True + return False + + +def get_redacted_preview(text: str, max_length: int = 100) -> str: + redacted = strip_secrets(text) + if len(redacted) > max_length: + return redacted[:max_length] + "..." + return redacted From 996f9c94661bfaffed8f90edd82af0c2eac7bde4 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Wed, 8 Jul 2026 00:09:56 +0300 Subject: [PATCH 6/7] fix: resolve lint and typecheck errors in utils modules --- mcp_server/tools_layer.py | 9 +++--- mcp_server/utils/__init__.py | 2 +- mcp_server/utils/circuit_breaker.py | 46 ++++++++++++++--------------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py index 7fa8f5ca..dd8ac5ac 100644 --- a/mcp_server/tools_layer.py +++ b/mcp_server/tools_layer.py @@ -8,6 +8,7 @@ from shared.constants import DB_NAME import hashlib import logging +import re import time from typing import Any, Optional @@ -32,13 +33,13 @@ class _DedupCache: _doc_ = "SHA-256 dedup with TTL and periodic cleanup." - + def __init__(self, ttl=300, max_size=10000): self._cache = {} self._ttl = ttl self._max_size = max_size self._last_cleanup = time.time() - + def _cleanup(self): now = time.time() if now - self._last_cleanup < 60: @@ -51,7 +52,7 @@ def _cleanup(self): oldest = sorted(self._cache.keys(), key=lambda k: self._cache[k])[:len(self._cache) // 4] for k in oldest: del self._cache[k] - + def is_duplicate(self, session_id, tool, input_text): self._cleanup() key = hashlib.sha256(f"{session_id}:{tool}:{input_text[:500]}".encode()).hexdigest() @@ -222,7 +223,7 @@ async def memory_remember( if session_id and _dedup_cache.is_duplicate(session_id, key, value): logger.info("Dedup: skipping identical remember key=%s user=%s", key, user_id) return RememberResult(status="skipped", reason="duplicate_within_ttl").dict() - + app = _get_ctx(ctx) layer = _validate_layer(layer) metrics.inc("tool_calls") diff --git a/mcp_server/utils/__init__.py b/mcp_server/utils/__init__.py index 80524337..620868cc 100644 --- a/mcp_server/utils/__init__.py +++ b/mcp_server/utils/__init__.py @@ -1,3 +1,3 @@ from mcp_server.utils.circuit_breaker import CircuitBreaker -__all__ = [ CircuitBreaker] +__all__ = ["CircuitBreaker"] diff --git a/mcp_server/utils/circuit_breaker.py b/mcp_server/utils/circuit_breaker.py index 5cca30f7..09127862 100644 --- a/mcp_server/utils/circuit_breaker.py +++ b/mcp_server/utils/circuit_breaker.py @@ -8,10 +8,10 @@ Usage: breaker = CircuitBreaker(threshold=3, recovery_timeout=30) - + if not breaker.allow_request(): return cached_result or fallback - + try: result = await llm_call() breaker.record_success() @@ -24,7 +24,7 @@ import logging import time from enum import Enum -from typing import Any, Callable, Optional +from typing import Callable, Optional logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ class CircuitState(Enum): class CircuitBreaker: """Circuit breaker with configurable threshold and recovery timeout.""" - + def __init__( self, threshold: int = 3, @@ -48,31 +48,31 @@ def __init__( self.threshold = threshold self.recovery_timeout = recovery_timeout self.name = name - + self._failures = 0 self._state = CircuitState.CLOSED self._opened_at = 0.0 self._last_failure_at = 0.0 - + self._on_state_change = on_state_change - + # Metrics self._total_requests = 0 self._total_failures = 0 self._total_rejections = 0 self._state_changes = 0 - + @property def state(self) -> CircuitState: if self._state == CircuitState.OPEN: if time.time() - self._opened_at > self.recovery_timeout: self._transition_to(CircuitState.HALF_OPEN) return self._state - + @property def failures(self) -> int: return self._failures - + def _transition_to(self, new_state: CircuitState): old_state = self._state self._state = new_state @@ -80,42 +80,42 @@ def _transition_to(self, new_state: CircuitState): logger.info("CircuitBreaker[%s]: %s -> %s", self.name, old_state.value, new_state.value) if self._on_state_change: self._on_state_change(self.name, old_state, new_state) - + def record_success(self): self._total_requests += 1 self._failures = 0 if self._state == CircuitState.HALF_OPEN: self._transition_to(CircuitState.CLOSED) - + def record_failure(self): self._total_requests += 1 self._total_failures += 1 self._failures += 1 self._last_failure_at = time.time() - + if self._state == CircuitState.HALF_OPEN: self._transition_to(CircuitState.OPEN) self._opened_at = time.time() elif self._failures >= self.threshold: self._transition_to(CircuitState.OPEN) self._opened_at = time.time() - + def allow_request(self) -> bool: self._total_requests += 1 current_state = self.state - + if current_state == CircuitState.CLOSED: return True if current_state == CircuitState.HALF_OPEN: return True self._total_rejections += 1 return False - + def reset(self): self._failures = 0 self._state = CircuitState.CLOSED self._opened_at = 0.0 - + def get_metrics(self) -> dict: return { "name": self.name, @@ -129,11 +129,11 @@ def get_metrics(self) -> dict: "state_changes": self._state_changes, "last_failure_at": self._last_failure_at, } - + def __enter__(self): self._context_allowed = self.allow_request() return self._context_allowed - + def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.record_success() @@ -145,7 +145,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): class CircuitBreakerRegistry: def __init__(self): self._breakers: dict[str, CircuitBreaker] = {} - + def get(self, name: str, threshold: int = 3, recovery_timeout: float = 30.0, on_state_change: Optional[Callable] = None) -> CircuitBreaker: if name not in self._breakers: self._breakers[name] = CircuitBreaker( @@ -155,13 +155,13 @@ def get(self, name: str, threshold: int = 3, recovery_timeout: float = 30.0, on_ on_state_change=on_state_change, ) return self._breakers[name] - + def get_all(self) -> dict[str, CircuitBreaker]: return dict(self._breakers) - + def get_all_metrics(self) -> dict: return {name: breaker.get_metrics() for name, breaker in self._breakers.items()} - + def reset_all(self): for breaker in self._breakers.values(): breaker.reset() From 87acc138898ea2fda5d1dfc8a16f30c68916ae10 Mon Sep 17 00:00:00 2001 From: Ariel Memory Date: Wed, 8 Jul 2026 00:14:05 +0300 Subject: [PATCH 7/7] fix: format code with ruff --- mcp_server/tools_layer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mcp_server/tools_layer.py b/mcp_server/tools_layer.py index dd8ac5ac..8355031d 100644 --- a/mcp_server/tools_layer.py +++ b/mcp_server/tools_layer.py @@ -49,7 +49,7 @@ def _cleanup(self): for k in expired: del self._cache[k] if len(self._cache) > self._max_size: - oldest = sorted(self._cache.keys(), key=lambda k: self._cache[k])[:len(self._cache) // 4] + oldest = sorted(self._cache.keys(), key=lambda k: self._cache[k])[: len(self._cache) // 4] for k in oldest: del self._cache[k] @@ -74,7 +74,7 @@ def is_duplicate(self, session_id, tool, input_text): def _estimate_tokens(text: str) -> int: if not text: return 0 - cjk_count = len(re.findall(r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]', text)) + cjk_count = len(re.findall(r"[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]", text)) remaining_chars = len(text) - cjk_count non_cjk_tokens = remaining_chars // CHARS_PER_TOKEN return cjk_count + non_cjk_tokens @@ -85,7 +85,7 @@ def _truncate_to_budget(text: str, max_tokens: int) -> tuple: if estimated <= max_tokens: return text, False char_limit = max_tokens * CHARS_PER_TOKEN - lines = text.split('\\n') + lines = text.split("\\n") result_lines = [] current_len = 0 for line in lines: @@ -94,8 +94,8 @@ def _truncate_to_budget(text: str, max_tokens: int) -> tuple: break result_lines.append(line) current_len += line_len - truncated = '\\n'.join(result_lines) - truncated += '\\n[...truncated to token budget]' + truncated = "\\n".join(result_lines) + truncated += "\\n[...truncated to token budget]" return truncated, True