runtime-memory 3.0.0__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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""runtime-memory as a Hermes Agent memory provider.
|
|
2
|
+
|
|
3
|
+
Hermes finds this package through the ``hermes_agent.memory_providers`` entry
|
|
4
|
+
point declared in ``pyproject.toml``, so installing runtime-memory into the Hermes
|
|
5
|
+
environment is enough to make it selectable:
|
|
6
|
+
|
|
7
|
+
pip install git+https://github.com/runtimenoteslabs/memory-layer.git
|
|
8
|
+
hermes config set memory.provider runtimememory
|
|
9
|
+
|
|
10
|
+
See ``docs/hermes.md`` for configuration and the trace format.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from runtime_memory.hermes._base import HERMES_AVAILABLE
|
|
18
|
+
from runtime_memory.hermes.provider import (
|
|
19
|
+
PROVIDER_NAME,
|
|
20
|
+
RuntimeMemoryProvider,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(ctx: Any) -> None:
|
|
25
|
+
"""Register the provider with Hermes' plugin loader.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
ctx: The plugin context Hermes passes in. Only
|
|
29
|
+
``register_memory_provider`` is used.
|
|
30
|
+
"""
|
|
31
|
+
ctx.register_memory_provider(RuntimeMemoryProvider())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"HERMES_AVAILABLE",
|
|
36
|
+
"PROVIDER_NAME",
|
|
37
|
+
"RuntimeMemoryProvider",
|
|
38
|
+
"register",
|
|
39
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Hermes plugin base classes, with a standalone fallback.
|
|
2
|
+
|
|
3
|
+
The provider subclasses ``agent.memory_provider.MemoryProvider``, which only
|
|
4
|
+
exists inside a Hermes Agent installation. runtime-memory is also installed on its
|
|
5
|
+
own, so importing this package must not require Hermes. When Hermes is absent we
|
|
6
|
+
fall back to a shim carrying the same surface, which keeps ``runtime_memory.hermes``
|
|
7
|
+
importable and unit-testable anywhere.
|
|
8
|
+
|
|
9
|
+
The shim is deliberately a copy of the contract, not a reimplementation of it. If
|
|
10
|
+
Hermes changes the ABC, the real import is what the provider is validated
|
|
11
|
+
against; ``HERMES_AVAILABLE`` tells tests which one is in play.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from agent.memory_provider import ( # type: ignore[import-not-found]
|
|
20
|
+
INDICATOR_GLYPH,
|
|
21
|
+
MemoryProvider,
|
|
22
|
+
RecallStatus,
|
|
23
|
+
is_trivial_prompt,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
HERMES_AVAILABLE = True
|
|
27
|
+
except ImportError: # pragma: no cover - exercised only outside Hermes
|
|
28
|
+
HERMES_AVAILABLE = False
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from abc import ABC, abstractmethod
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
|
|
34
|
+
INDICATOR_GLYPH = "\N{BRAIN}"
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class RecallStatus: # type: ignore[no-redef]
|
|
38
|
+
"""What the last prefetch injected, for the recall indicator."""
|
|
39
|
+
|
|
40
|
+
provider_label: str
|
|
41
|
+
count: int
|
|
42
|
+
glyph: str = INDICATOR_GLYPH
|
|
43
|
+
|
|
44
|
+
_TRIVIAL_PROMPT_RE = re.compile(
|
|
45
|
+
r"^(yes|no|ok|okay|sure|thanks|thank you|y|n|yep|nope|yeah|nah|"
|
|
46
|
+
r"hi|hey|hello|yo|sup|"
|
|
47
|
+
r"continue|go ahead|do it|proceed|got it|cool|nice|great|done|next|lgtm|k)"
|
|
48
|
+
r"[\s!?.:;,\"'~]*$",
|
|
49
|
+
re.IGNORECASE,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def is_trivial_prompt(text: str | None) -> bool: # type: ignore[misc]
|
|
53
|
+
"""True for empty input, slash commands and bare acknowledgements."""
|
|
54
|
+
stripped = (text or "").strip()
|
|
55
|
+
if not stripped or stripped.startswith("/"):
|
|
56
|
+
return True
|
|
57
|
+
return bool(_TRIVIAL_PROMPT_RE.match(stripped))
|
|
58
|
+
|
|
59
|
+
class MemoryProvider(ABC): # type: ignore[no-redef]
|
|
60
|
+
"""Minimal stand-in for the Hermes memory provider contract.
|
|
61
|
+
|
|
62
|
+
The optional hooks below are deliberately concrete no-ops rather than
|
|
63
|
+
abstract methods: a provider overrides only the ones it needs, exactly as
|
|
64
|
+
in the real contract this mirrors.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
pre_compress_checkpoint_api_version = 1
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def name(self) -> str: ...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def is_available(self) -> bool: ...
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def initialize(self, session_id: str, **kwargs: Any) -> None: ...
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def get_tool_schemas(self) -> list[dict[str, Any]]: ...
|
|
81
|
+
|
|
82
|
+
def unavailable_reason(self) -> str:
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
def system_prompt_block(self) -> str:
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
def queue_prefetch(self, query: str, *, session_id: str = "") -> None: ...
|
|
92
|
+
|
|
93
|
+
def recall_status(self) -> RecallStatus | None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def sync_turn(
|
|
97
|
+
self,
|
|
98
|
+
user_content: str,
|
|
99
|
+
assistant_content: str,
|
|
100
|
+
*,
|
|
101
|
+
session_id: str = "",
|
|
102
|
+
messages: list[dict[str, Any]] | None = None,
|
|
103
|
+
) -> None: ...
|
|
104
|
+
|
|
105
|
+
def handle_tool_call(self, tool_name: str, args: dict[str, Any], **kwargs: Any) -> str:
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def shutdown(self) -> None: ...
|
|
109
|
+
|
|
110
|
+
def on_turn_start(self, turn_number: int, message: str, **kwargs: Any) -> None: ...
|
|
111
|
+
|
|
112
|
+
def on_session_end(self, messages: list[dict[str, Any]]) -> None: ...
|
|
113
|
+
|
|
114
|
+
def on_session_switch(
|
|
115
|
+
self,
|
|
116
|
+
new_session_id: str,
|
|
117
|
+
*,
|
|
118
|
+
parent_session_id: str = "",
|
|
119
|
+
reset: bool = False,
|
|
120
|
+
rewound: bool = False,
|
|
121
|
+
**kwargs: Any,
|
|
122
|
+
) -> None: ...
|
|
123
|
+
|
|
124
|
+
def on_pre_compress(self, messages: list[dict[str, Any]]) -> str:
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
def on_delegation(
|
|
128
|
+
self, task: str, result: str, *, child_session_id: str = "", **kwargs: Any
|
|
129
|
+
) -> None: ...
|
|
130
|
+
|
|
131
|
+
def get_config_schema(self) -> list[dict[str, Any]]:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
def save_config(self, values: dict[str, Any], hermes_home: str) -> None: ...
|
|
135
|
+
|
|
136
|
+
def on_memory_write(
|
|
137
|
+
self,
|
|
138
|
+
action: str,
|
|
139
|
+
target: str,
|
|
140
|
+
content: str,
|
|
141
|
+
metadata: dict[str, Any] | None = None,
|
|
142
|
+
) -> None: ...
|
|
143
|
+
|
|
144
|
+
def backup_paths(self) -> list[str]:
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = [
|
|
149
|
+
"HERMES_AVAILABLE",
|
|
150
|
+
"INDICATOR_GLYPH",
|
|
151
|
+
"MemoryProvider",
|
|
152
|
+
"RecallStatus",
|
|
153
|
+
"is_trivial_prompt",
|
|
154
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Sync-to-async bridge for the Hermes memory provider.
|
|
2
|
+
|
|
3
|
+
Every method on the Hermes ``MemoryProvider`` contract is synchronous, while
|
|
4
|
+
``MemoryEngine`` is fully async. The bridge owns one long-lived event loop on a
|
|
5
|
+
daemon thread and marshals coroutines onto it.
|
|
6
|
+
|
|
7
|
+
The loop is module-global and is deliberately never stopped by a provider's
|
|
8
|
+
``shutdown()``. Hermes builds one provider per agent and one agent per concurrent
|
|
9
|
+
chat session, so a per-provider teardown would strand every sibling provider's
|
|
10
|
+
engine on a dead loop. The loop is a daemon thread and dies with the process.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import contextvars
|
|
17
|
+
import threading
|
|
18
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
19
|
+
|
|
20
|
+
from runtime_memory.core.logging import get_logger
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Coroutine
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
_loop: asyncio.AbstractEventLoop | None = None
|
|
30
|
+
_loop_thread: threading.Thread | None = None
|
|
31
|
+
_loop_lock = threading.Lock()
|
|
32
|
+
|
|
33
|
+
DEFAULT_TIMEOUT = 30.0
|
|
34
|
+
"""Seconds to wait for a bridged call before giving up."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_loop() -> asyncio.AbstractEventLoop:
|
|
38
|
+
"""Return the shared background event loop, starting it if needed."""
|
|
39
|
+
global _loop, _loop_thread
|
|
40
|
+
|
|
41
|
+
with _loop_lock:
|
|
42
|
+
if _loop is not None and _loop.is_running():
|
|
43
|
+
return _loop
|
|
44
|
+
|
|
45
|
+
loop = asyncio.new_event_loop()
|
|
46
|
+
|
|
47
|
+
def _run() -> None:
|
|
48
|
+
asyncio.set_event_loop(loop)
|
|
49
|
+
loop.run_forever()
|
|
50
|
+
|
|
51
|
+
thread = threading.Thread(target=_run, daemon=True, name="memory-layer-loop")
|
|
52
|
+
thread.start()
|
|
53
|
+
|
|
54
|
+
_loop, _loop_thread = loop, thread
|
|
55
|
+
return loop
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_sync(coro: Coroutine[Any, Any, T], timeout: float = DEFAULT_TIMEOUT) -> T:
|
|
59
|
+
"""Run *coro* on the shared loop and block until it finishes.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
coro: The coroutine to run.
|
|
63
|
+
timeout: Seconds to wait before raising ``TimeoutError``.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Whatever the coroutine returned.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
TimeoutError: If the coroutine does not finish within *timeout*.
|
|
70
|
+
"""
|
|
71
|
+
future = asyncio.run_coroutine_threadsafe(coro, get_loop())
|
|
72
|
+
try:
|
|
73
|
+
return future.result(timeout=timeout)
|
|
74
|
+
except TimeoutError:
|
|
75
|
+
future.cancel()
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def spawn(coro: Coroutine[Any, Any, Any], *, label: str = "task") -> None:
|
|
80
|
+
"""Fire *coro* onto the shared loop without waiting for it.
|
|
81
|
+
|
|
82
|
+
Used for the write path, which must not block the agent's reply. Failures are
|
|
83
|
+
logged rather than raised, since there is no caller left to receive them.
|
|
84
|
+
"""
|
|
85
|
+
future = asyncio.run_coroutine_threadsafe(coro, get_loop())
|
|
86
|
+
|
|
87
|
+
def _report(fut: Any) -> None:
|
|
88
|
+
try:
|
|
89
|
+
fut.result()
|
|
90
|
+
except Exception as exc: # background work: log it, there is no caller left
|
|
91
|
+
logger.warning(f"Background {label} failed: {exc}")
|
|
92
|
+
|
|
93
|
+
future.add_done_callback(_report)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def context_thread(target: Any, name: str) -> threading.Thread:
|
|
97
|
+
"""Daemon thread running *target* inside a copy of the caller's context.
|
|
98
|
+
|
|
99
|
+
Threads otherwise start with an empty ``contextvars`` context, which loses the
|
|
100
|
+
profile scoping Hermes sets up for multi-profile installs.
|
|
101
|
+
"""
|
|
102
|
+
return threading.Thread(
|
|
103
|
+
target=contextvars.copy_context().run,
|
|
104
|
+
args=(target,),
|
|
105
|
+
daemon=True,
|
|
106
|
+
name=name,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _reset_for_tests() -> None:
|
|
111
|
+
"""Stop and clear the shared loop. Test-only."""
|
|
112
|
+
global _loop, _loop_thread
|
|
113
|
+
|
|
114
|
+
with _loop_lock:
|
|
115
|
+
if _loop is not None and _loop.is_running():
|
|
116
|
+
_loop.call_soon_threadsafe(_loop.stop)
|
|
117
|
+
if _loop_thread is not None:
|
|
118
|
+
_loop_thread.join(timeout=5.0)
|
|
119
|
+
_loop, _loop_thread = None, None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: runtimememory
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: "Runtime Memory - local SQLite memory with hybrid retrieval and outcome-based learning, shared across Claude Code, MCP clients and Hermes."
|
|
4
|
+
|
|
5
|
+
# Hermes checks each entry by importing `dist_name.replace("-", "_")` unless it
|
|
6
|
+
# has a mapping for the name. `runtime-memory` imports as `runtime_memory`, so
|
|
7
|
+
# it resolves without an upstream mapping, unlike the bundled providers that
|
|
8
|
+
# need one (honcho-ai -> honcho, mem0ai -> mem0).
|
|
9
|
+
pip_dependencies: ["runtime-memory"]
|
|
10
|
+
|
|
11
|
+
requires_env: []
|
|
12
|
+
hooks:
|
|
13
|
+
- on_session_end
|