From 03bada8b3ca385d4a46a36783245b3a554b16a18 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 25 Jun 2026 10:57:45 +0530 Subject: [PATCH 1/3] refactor(memory): use typing.Protocol for MemoryBackend interface - Replace duck-typing hasattr checks with structural Protocol - MemoryBackend is runtime_checkable for isinstance validation - Aligns with hawk-eco architecture: SDKs use structural interfaces --- src/hawk/memory_tools.py | 48 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/hawk/memory_tools.py b/src/hawk/memory_tools.py index f5491b7..eb211d0 100644 --- a/src/hawk/memory_tools.py +++ b/src/hawk/memory_tools.py @@ -1,7 +1,8 @@ """Memory-as-voluntary-tools for agent-driven memory management. Lets agents strategically decide what to remember/recall rather than -auto-ingesting everything. Wraps yaad's memory API as tool functions. +auto-ingesting everything. Wraps any MemoryBackend-compatible client +as tool functions. Usage: from hawk.memory_tools import MemoryTools @@ -12,11 +13,25 @@ from __future__ import annotations -from typing import Any +from typing import Any, Protocol, Sequence, runtime_checkable from .tools import Tool +@runtime_checkable +class MemoryBackend(Protocol): + """Structural interface for persistent memory backends. + + Any object with ``remember`` and ``recall`` methods satisfying these + signatures is accepted — no import or inheritance required. + """ + + def remember(self, content: str, *, session_id: str | None = None) -> Any: ... + def recall( + self, query: str, *, limit: int = 5, session_id: str | None = None + ) -> Sequence[str]: ... + + class MemoryTools: """Provides record/retrieve memory operations as agent tools. @@ -30,6 +45,9 @@ def __init__(self, client: Any, *, session_id: str | None = None) -> None: self._client = client self._session_id = session_id self._local_memories: list[dict[str, str]] = [] + self._backend: MemoryBackend | None = ( + client if isinstance(client, MemoryBackend) else None + ) def record_memory( self, content: str, category: str = "general", importance: str = "normal" @@ -42,15 +60,14 @@ def record_memory( } self._local_memories.append(memory) - # If client supports yaad memory API, persist - try: - if hasattr(self._client, "remember"): - self._client.remember(content, session_id=self._session_id) + if self._backend is not None: + try: + self._backend.remember(content, session_id=self._session_id) return f"Recorded to persistent memory: '{content[:100]}...'" - except Exception as exc: - import logging + except Exception as exc: + import logging - logging.getLogger(__name__).debug("Failed to persist memory via yaad: %s", exc) + logging.getLogger(__name__).debug("Failed to persist memory: %s", exc) return f"Recorded to session memory: '{content[:100]}...'" @@ -58,18 +75,17 @@ def retrieve_memories(self, query: str, limit: int = 5) -> str: """Retrieve relevant memories for the current context.""" results = [] - # Try yaad recall - try: - if hasattr(self._client, "recall"): - recalled = self._client.recall(query, limit=limit, session_id=self._session_id) + if self._backend is not None: + try: + recalled = self._backend.recall(query, limit=limit, session_id=self._session_id) if recalled: return f"Recalled {len(recalled)} memories:\n" + "\n".join( f"- {m}" for m in recalled ) - except Exception as exc: - import logging + except Exception as exc: + import logging - logging.getLogger(__name__).debug("Failed to recall memories via yaad: %s", exc) + logging.getLogger(__name__).debug("Failed to recall memories: %s", exc) # Fallback to local fuzzy match query_lower = query.lower() From cfc9a09a71c4c83fd7121f6e7f647a042a35141e Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 25 Jun 2026 19:06:22 +0530 Subject: [PATCH 2/3] fix(memory): move Sequence to TYPE_CHECKING block, use hasattr for backend detection - Import Sequence under TYPE_CHECKING to satisfy ruff UP035/TC003 - Use hasattr instead of isinstance(Protocol) for backend detection since runtime_checkable checks class attrs, not instance attrs, breaking MagicMock-based tests on Python 3.12+ --- src/hawk/memory_tools.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hawk/memory_tools.py b/src/hawk/memory_tools.py index eb211d0..66151b6 100644 --- a/src/hawk/memory_tools.py +++ b/src/hawk/memory_tools.py @@ -13,7 +13,10 @@ from __future__ import annotations -from typing import Any, Protocol, Sequence, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable + +if TYPE_CHECKING: + from collections.abc import Sequence from .tools import Tool @@ -46,7 +49,9 @@ def __init__(self, client: Any, *, session_id: str | None = None) -> None: self._session_id = session_id self._local_memories: list[dict[str, str]] = [] self._backend: MemoryBackend | None = ( - client if isinstance(client, MemoryBackend) else None + client + if hasattr(client, "remember") and hasattr(client, "recall") + else None ) def record_memory( From e62a6f6d2a58483885c03c93adab96153cd40331 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 25 Jun 2026 19:12:33 +0530 Subject: [PATCH 3/3] style: ruff format memory_tools.py --- src/hawk/memory_tools.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/hawk/memory_tools.py b/src/hawk/memory_tools.py index 66151b6..be8c539 100644 --- a/src/hawk/memory_tools.py +++ b/src/hawk/memory_tools.py @@ -49,9 +49,7 @@ def __init__(self, client: Any, *, session_id: str | None = None) -> None: self._session_id = session_id self._local_memories: list[dict[str, str]] = [] self._backend: MemoryBackend | None = ( - client - if hasattr(client, "remember") and hasattr(client, "recall") - else None + client if hasattr(client, "remember") and hasattr(client, "recall") else None ) def record_memory(