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,230 @@
|
|
|
1
|
+
"""Tool schemas exposed to Hermes, and their dispatch.
|
|
2
|
+
|
|
3
|
+
Hermes wants OpenAI function-calling schemas (``name``/``description``/
|
|
4
|
+
``parameters``), which is a different shape from memory-layer's MCP schemas, so
|
|
5
|
+
these are declared here rather than converted. The set is deliberately small: the
|
|
6
|
+
lifecycle hooks already handle recall and persistence, so the tools cover what the
|
|
7
|
+
model must ask for explicitly - deliberate saves, targeted lookups, outcome
|
|
8
|
+
feedback and a health check.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from runtime_memory.core.models import MemoryCategory, Outcome
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from runtime_memory.hermes.provider import RuntimeMemoryProvider
|
|
20
|
+
|
|
21
|
+
CATEGORIES = [category.value for category in MemoryCategory]
|
|
22
|
+
OUTCOMES = [outcome.value for outcome in Outcome]
|
|
23
|
+
|
|
24
|
+
REMEMBER = {
|
|
25
|
+
"name": "runtimememory_remember",
|
|
26
|
+
"description": (
|
|
27
|
+
"Save a durable fact to long-term memory: a convention, decision, "
|
|
28
|
+
"gotcha, command or preference worth recalling in a later session. "
|
|
29
|
+
"Do not use it for transient conversation detail."
|
|
30
|
+
),
|
|
31
|
+
"parameters": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"content": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "The fact to store, written to stand alone.",
|
|
37
|
+
},
|
|
38
|
+
"category": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": CATEGORIES,
|
|
41
|
+
"description": "Classification for the memory.",
|
|
42
|
+
},
|
|
43
|
+
"tags": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": {"type": "string"},
|
|
46
|
+
"description": "Optional tags for later filtering.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"required": ["content", "category"],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
RECALL = {
|
|
54
|
+
"name": "runtimememory_recall",
|
|
55
|
+
"description": (
|
|
56
|
+
"Search long-term memory for facts relevant to a query. Results are "
|
|
57
|
+
"ranked by relevance and by how well each memory has worked before."
|
|
58
|
+
),
|
|
59
|
+
"parameters": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"query": {"type": "string", "description": "What to search for."},
|
|
63
|
+
"limit": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"description": "Maximum memories to return (default 10).",
|
|
66
|
+
"minimum": 1,
|
|
67
|
+
"maximum": 50,
|
|
68
|
+
},
|
|
69
|
+
"category": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": CATEGORIES,
|
|
72
|
+
"description": "Restrict results to one category.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"required": ["query"],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
OUTCOME = {
|
|
80
|
+
"name": "runtimememory_outcome",
|
|
81
|
+
"description": (
|
|
82
|
+
"Report whether recalled memories actually helped. Call it once you "
|
|
83
|
+
"know: 'worked' when the advice solved the problem, 'failed' when it "
|
|
84
|
+
"was wrong or misleading, 'partial' when it helped a little. This is "
|
|
85
|
+
"what teaches the store which memories to surface next time. With no "
|
|
86
|
+
"memory_ids, it applies to the memories recalled for this turn."
|
|
87
|
+
),
|
|
88
|
+
"parameters": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"outcome": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": OUTCOMES,
|
|
94
|
+
"description": "How the recalled memories performed.",
|
|
95
|
+
},
|
|
96
|
+
"memory_ids": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"items": {"type": "string"},
|
|
99
|
+
"description": "Specific memories to score. Defaults to this turn's recall.",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"required": ["outcome"],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
STATS = {
|
|
107
|
+
"name": "runtimememory_stats",
|
|
108
|
+
"description": "Report how many memories are stored and how they break down by category.",
|
|
109
|
+
"parameters": {"type": "object", "properties": {}},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
TOOL_SCHEMAS: list[dict[str, Any]] = [REMEMBER, RECALL, OUTCOME, STATS]
|
|
113
|
+
|
|
114
|
+
TOOL_NAMES = frozenset(schema["name"] for schema in TOOL_SCHEMAS)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def dispatch(provider: RuntimeMemoryProvider, tool_name: str, args: dict[str, Any]) -> str:
|
|
118
|
+
"""Route one tool call to the provider and serialize the result.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
provider: The provider handling the call.
|
|
122
|
+
tool_name: Which tool the model invoked.
|
|
123
|
+
args: The model's arguments.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A JSON string, per the Hermes tool contract. Errors are returned as
|
|
127
|
+
``{"error": ...}`` rather than raised, so a bad call costs a turn instead
|
|
128
|
+
of the session.
|
|
129
|
+
"""
|
|
130
|
+
handlers = {
|
|
131
|
+
REMEMBER["name"]: _remember,
|
|
132
|
+
RECALL["name"]: _recall,
|
|
133
|
+
OUTCOME["name"]: _outcome,
|
|
134
|
+
STATS["name"]: _stats,
|
|
135
|
+
}
|
|
136
|
+
handler = handlers.get(tool_name)
|
|
137
|
+
if handler is None:
|
|
138
|
+
return json.dumps({"error": f"Unknown tool: {tool_name}"})
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
return json.dumps(handler(provider, args), ensure_ascii=False, default=str)
|
|
142
|
+
except Exception as exc: # surface the failure to the model, not the user
|
|
143
|
+
return json.dumps({"error": str(exc)})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _remember(provider: RuntimeMemoryProvider, args: dict[str, Any]) -> dict[str, Any]:
|
|
147
|
+
content = (args.get("content") or "").strip()
|
|
148
|
+
if not content:
|
|
149
|
+
return {"error": "content is required"}
|
|
150
|
+
|
|
151
|
+
raw_category = args.get("category") or MemoryCategory.GENERAL.value
|
|
152
|
+
try:
|
|
153
|
+
category = MemoryCategory(raw_category)
|
|
154
|
+
except ValueError:
|
|
155
|
+
return {"error": f"Unknown category '{raw_category}'. Valid: {CATEGORIES}"}
|
|
156
|
+
|
|
157
|
+
memory = provider.remember(content=content, category=category, tags=args.get("tags") or [])
|
|
158
|
+
return {
|
|
159
|
+
"stored": True,
|
|
160
|
+
"memory_id": memory.id,
|
|
161
|
+
"category": memory.category.value,
|
|
162
|
+
"project": memory.project,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _recall(provider: RuntimeMemoryProvider, args: dict[str, Any]) -> dict[str, Any]:
|
|
167
|
+
query = (args.get("query") or "").strip()
|
|
168
|
+
if not query:
|
|
169
|
+
return {"error": "query is required"}
|
|
170
|
+
|
|
171
|
+
raw_category = args.get("category")
|
|
172
|
+
category = None
|
|
173
|
+
if raw_category:
|
|
174
|
+
try:
|
|
175
|
+
category = MemoryCategory(raw_category)
|
|
176
|
+
except ValueError:
|
|
177
|
+
return {"error": f"Unknown category '{raw_category}'. Valid: {CATEGORIES}"}
|
|
178
|
+
|
|
179
|
+
limit = args.get("limit") or 10
|
|
180
|
+
results = provider.recall(query=query, limit=int(limit), category=category)
|
|
181
|
+
return {
|
|
182
|
+
"count": len(results),
|
|
183
|
+
"memories": [
|
|
184
|
+
{
|
|
185
|
+
"id": r.memory.id,
|
|
186
|
+
"content": r.memory.content,
|
|
187
|
+
"category": r.memory.category.value,
|
|
188
|
+
"score": round(r.score, 3),
|
|
189
|
+
"outcome_score": round(r.memory.outcome_score, 3),
|
|
190
|
+
}
|
|
191
|
+
for r in results
|
|
192
|
+
],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _outcome(provider: RuntimeMemoryProvider, args: dict[str, Any]) -> dict[str, Any]:
|
|
197
|
+
raw_outcome = args.get("outcome")
|
|
198
|
+
try:
|
|
199
|
+
outcome = Outcome(raw_outcome)
|
|
200
|
+
except ValueError:
|
|
201
|
+
return {"error": f"Unknown outcome '{raw_outcome}'. Valid: {OUTCOMES}"}
|
|
202
|
+
|
|
203
|
+
memory_ids = args.get("memory_ids")
|
|
204
|
+
updated = provider.record_outcome(outcome=outcome, memory_ids=memory_ids)
|
|
205
|
+
if not updated:
|
|
206
|
+
return {
|
|
207
|
+
"recorded": False,
|
|
208
|
+
"reason": "No memories to score - none were recalled this turn.",
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
"recorded": True,
|
|
212
|
+
"outcome": outcome.value,
|
|
213
|
+
"updated": [
|
|
214
|
+
{"id": memory.id, "outcome_score": round(memory.outcome_score, 3)} for memory in updated
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _stats(provider: RuntimeMemoryProvider, args: dict[str, Any]) -> dict[str, Any]:
|
|
220
|
+
storage = provider.stats().storage_stats
|
|
221
|
+
return {
|
|
222
|
+
"total_memories": storage.total_memories,
|
|
223
|
+
"active_memories": storage.active_memories,
|
|
224
|
+
"avg_outcome_score": round(storage.avg_outcome_score, 3),
|
|
225
|
+
"total_uses": storage.total_uses,
|
|
226
|
+
"by_category": {
|
|
227
|
+
key.value if hasattr(key, "value") else str(key): count
|
|
228
|
+
for key, count in storage.by_category.items()
|
|
229
|
+
},
|
|
230
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Retrieval and outcome tracing for the Hermes provider.
|
|
2
|
+
|
|
3
|
+
This is the instrumentation the Tier 3 evaluation runs on. Every recall writes
|
|
4
|
+
one JSONL record naming the memories it injected and their scores; every recorded
|
|
5
|
+
outcome writes another naming the memories it credited or blamed. Joining the two
|
|
6
|
+
on ``turn_id`` reconstructs, per turn, what was retrieved and whether it helped -
|
|
7
|
+
which is the measurement the outcome-learning claim needs.
|
|
8
|
+
|
|
9
|
+
Tracing is off unless a path is configured, and a broken trace never breaks a
|
|
10
|
+
turn: writes are best-effort and failures are logged once.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import threading
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from runtime_memory.core.logging import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
TRACE_ENV_VAR = "RUNTIME_MEMORY_HERMES_TRACE"
|
|
27
|
+
"""Environment variable holding the trace file path. Unset disables tracing."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TraceWriter:
|
|
31
|
+
"""Append-only JSONL writer for retrieval and outcome events."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, path: str | Path | None = None) -> None:
|
|
34
|
+
"""Initialize the writer.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
path: Trace file path. Falls back to ``RUNTIME_MEMORY_HERMES_TRACE``;
|
|
38
|
+
tracing is disabled when neither is set.
|
|
39
|
+
"""
|
|
40
|
+
raw = path or os.environ.get(TRACE_ENV_VAR)
|
|
41
|
+
self._path = Path(raw).expanduser() if raw else None
|
|
42
|
+
self._lock = threading.Lock()
|
|
43
|
+
self._warned = False
|
|
44
|
+
|
|
45
|
+
if self._path is not None:
|
|
46
|
+
try:
|
|
47
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
except OSError as exc:
|
|
49
|
+
logger.warning(f"Trace directory unavailable ({exc}); tracing off")
|
|
50
|
+
self._path = None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def enabled(self) -> bool:
|
|
54
|
+
"""Whether events are being written anywhere."""
|
|
55
|
+
return self._path is not None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def path(self) -> Path | None:
|
|
59
|
+
"""The trace file path, or None when tracing is off."""
|
|
60
|
+
return self._path
|
|
61
|
+
|
|
62
|
+
def recall(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
turn_id: str,
|
|
66
|
+
session_id: str,
|
|
67
|
+
query: str,
|
|
68
|
+
results: list[Any],
|
|
69
|
+
project: str | None,
|
|
70
|
+
latency_ms: float,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Record what a recall injected.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
turn_id: Identifier joining this recall to any later outcome.
|
|
76
|
+
session_id: Hermes session the recall belongs to.
|
|
77
|
+
query: The query text used for retrieval.
|
|
78
|
+
results: ``SearchResult`` objects that were injected.
|
|
79
|
+
project: Project filter in force, if any.
|
|
80
|
+
latency_ms: Wall-clock retrieval time.
|
|
81
|
+
"""
|
|
82
|
+
self._write(
|
|
83
|
+
{
|
|
84
|
+
"event": "recall",
|
|
85
|
+
"turn_id": turn_id,
|
|
86
|
+
"session_id": session_id,
|
|
87
|
+
"query": query,
|
|
88
|
+
"project": project,
|
|
89
|
+
"latency_ms": round(latency_ms, 2),
|
|
90
|
+
"retrieved": [
|
|
91
|
+
{
|
|
92
|
+
"memory_id": r.memory.id,
|
|
93
|
+
"category": r.memory.category.value,
|
|
94
|
+
"score": round(r.score, 4),
|
|
95
|
+
"semantic_score": round(r.semantic_score, 4),
|
|
96
|
+
"outcome_score": round(r.memory.outcome_score, 4),
|
|
97
|
+
"use_count": r.memory.use_count,
|
|
98
|
+
}
|
|
99
|
+
for r in results
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def outcome(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
turn_id: str,
|
|
108
|
+
session_id: str,
|
|
109
|
+
outcome: str,
|
|
110
|
+
memory_ids: list[str],
|
|
111
|
+
origin: str,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Record an outcome applied to memories.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
turn_id: The turn the outcome is attributed to.
|
|
117
|
+
session_id: Hermes session the outcome came from.
|
|
118
|
+
outcome: ``worked``, ``failed`` or ``partial``.
|
|
119
|
+
memory_ids: Memories the score change was applied to.
|
|
120
|
+
origin: What produced the outcome, e.g. ``tool`` or ``auto``.
|
|
121
|
+
"""
|
|
122
|
+
self._write(
|
|
123
|
+
{
|
|
124
|
+
"event": "outcome",
|
|
125
|
+
"turn_id": turn_id,
|
|
126
|
+
"session_id": session_id,
|
|
127
|
+
"outcome": outcome,
|
|
128
|
+
"memory_ids": memory_ids,
|
|
129
|
+
"origin": origin,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def write_turn(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
turn_id: str,
|
|
137
|
+
session_id: str,
|
|
138
|
+
memory_ids: list[str],
|
|
139
|
+
kind: str,
|
|
140
|
+
count: int | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Record memories created from a conversation turn.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
turn_id: The turn the write belongs to.
|
|
146
|
+
session_id: Hermes session that produced it.
|
|
147
|
+
memory_ids: Ids written, where the caller knows them.
|
|
148
|
+
kind: What produced the write, e.g. ``tool`` or ``extraction``.
|
|
149
|
+
count: How many memories were written. Defaults to ``len(memory_ids)``;
|
|
150
|
+
pass it explicitly when the ids are not available.
|
|
151
|
+
"""
|
|
152
|
+
self._write(
|
|
153
|
+
{
|
|
154
|
+
"event": "write",
|
|
155
|
+
"turn_id": turn_id,
|
|
156
|
+
"session_id": session_id,
|
|
157
|
+
"kind": kind,
|
|
158
|
+
"memory_ids": memory_ids,
|
|
159
|
+
"count": len(memory_ids) if count is None else count,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _write(self, record: dict[str, Any]) -> None:
|
|
164
|
+
"""Append one record, swallowing and logging any failure."""
|
|
165
|
+
if self._path is None:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
record["ts"] = datetime.now(UTC).isoformat()
|
|
169
|
+
line = json.dumps(record, ensure_ascii=False, default=str)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
with self._lock, self._path.open("a", encoding="utf-8") as handle:
|
|
173
|
+
handle.write(line + "\n")
|
|
174
|
+
except OSError as exc:
|
|
175
|
+
if not self._warned:
|
|
176
|
+
logger.warning(f"Trace write failed ({exc}); further errors muted")
|
|
177
|
+
self._warned = True
|