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,397 @@
|
|
|
1
|
+
"""Task-Memory linking for tracking which memories are used per task.
|
|
2
|
+
|
|
3
|
+
This module manages the relationship between Beads tasks and Runtime Memory
|
|
4
|
+
memories, enabling automatic outcome capture when tasks complete.
|
|
5
|
+
|
|
6
|
+
Schema:
|
|
7
|
+
task_memory_links - Links memories to tasks they were used for
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
import aiosqlite
|
|
18
|
+
|
|
19
|
+
from runtime_memory.tasks.models import TaskMemoryLink
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Schema for task-memory links table
|
|
27
|
+
LINKS_SCHEMA_SQL = """
|
|
28
|
+
-- Task-memory links table
|
|
29
|
+
CREATE TABLE IF NOT EXISTS task_memory_links (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
task_id TEXT NOT NULL,
|
|
32
|
+
memory_id TEXT NOT NULL,
|
|
33
|
+
used_at TEXT NOT NULL,
|
|
34
|
+
outcome TEXT,
|
|
35
|
+
context TEXT,
|
|
36
|
+
UNIQUE(task_id, memory_id)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- Indexes for efficient queries
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_links_task_id ON task_memory_links(task_id);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_links_memory_id ON task_memory_links(memory_id);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_links_outcome ON task_memory_links(outcome);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_links_task_outcome ON task_memory_links(task_id, outcome);
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TaskMemoryLinker:
|
|
48
|
+
"""Manages links between tasks and memories.
|
|
49
|
+
|
|
50
|
+
This class tracks which memories are used for which tasks,
|
|
51
|
+
enabling automatic outcome recording when tasks complete.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> linker = TaskMemoryLinker(db_path)
|
|
55
|
+
>>> await linker.initialize()
|
|
56
|
+
>>> await linker.link("bd-a3f8", "mem-123")
|
|
57
|
+
>>> links = await linker.get_memories_for_task("bd-a3f8")
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, db_path: str | Path) -> None:
|
|
61
|
+
"""Initialize the linker.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
db_path: Path to SQLite database (same as MemoryStorage).
|
|
65
|
+
"""
|
|
66
|
+
self.db_path = Path(db_path)
|
|
67
|
+
self._initialized = False
|
|
68
|
+
|
|
69
|
+
async def initialize(self) -> None:
|
|
70
|
+
"""Initialize the links table in the database."""
|
|
71
|
+
if self._initialized:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
75
|
+
await conn.executescript(LINKS_SCHEMA_SQL)
|
|
76
|
+
await conn.commit()
|
|
77
|
+
|
|
78
|
+
self._initialized = True
|
|
79
|
+
logger.debug("Task-memory links table initialized")
|
|
80
|
+
|
|
81
|
+
async def link(
|
|
82
|
+
self,
|
|
83
|
+
task_id: str,
|
|
84
|
+
memory_id: str,
|
|
85
|
+
context: str | None = None,
|
|
86
|
+
) -> TaskMemoryLink:
|
|
87
|
+
"""Link a memory to a task.
|
|
88
|
+
|
|
89
|
+
If the link already exists, updates the used_at timestamp.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
task_id: The Beads task ID.
|
|
93
|
+
memory_id: The Runtime Memory memory ID.
|
|
94
|
+
context: Optional context about how the memory was used.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The created or updated TaskMemoryLink.
|
|
98
|
+
"""
|
|
99
|
+
now = datetime.now(UTC).isoformat()
|
|
100
|
+
|
|
101
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
102
|
+
# Use INSERT OR REPLACE to handle duplicates
|
|
103
|
+
await conn.execute(
|
|
104
|
+
"""
|
|
105
|
+
INSERT INTO task_memory_links (task_id, memory_id, used_at, context)
|
|
106
|
+
VALUES (?, ?, ?, ?)
|
|
107
|
+
ON CONFLICT(task_id, memory_id) DO UPDATE SET
|
|
108
|
+
used_at = excluded.used_at,
|
|
109
|
+
context = COALESCE(excluded.context, context)
|
|
110
|
+
""",
|
|
111
|
+
(task_id, memory_id, now, context),
|
|
112
|
+
)
|
|
113
|
+
await conn.commit()
|
|
114
|
+
|
|
115
|
+
logger.debug(f"Linked memory {memory_id} to task {task_id}")
|
|
116
|
+
return TaskMemoryLink(
|
|
117
|
+
task_id=task_id,
|
|
118
|
+
memory_id=memory_id,
|
|
119
|
+
used_at=datetime.fromisoformat(now),
|
|
120
|
+
context=context,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def link_many(
|
|
124
|
+
self,
|
|
125
|
+
task_id: str,
|
|
126
|
+
memory_ids: Sequence[str],
|
|
127
|
+
context: str | None = None,
|
|
128
|
+
) -> list[TaskMemoryLink]:
|
|
129
|
+
"""Link multiple memories to a task.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
task_id: The Beads task ID.
|
|
133
|
+
memory_ids: List of Runtime Memory memory IDs.
|
|
134
|
+
context: Optional context about how the memories were used.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of created TaskMemoryLink objects.
|
|
138
|
+
"""
|
|
139
|
+
now = datetime.now(UTC)
|
|
140
|
+
now_iso = now.isoformat()
|
|
141
|
+
links = []
|
|
142
|
+
|
|
143
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
144
|
+
for memory_id in memory_ids:
|
|
145
|
+
await conn.execute(
|
|
146
|
+
"""
|
|
147
|
+
INSERT INTO task_memory_links (task_id, memory_id, used_at, context)
|
|
148
|
+
VALUES (?, ?, ?, ?)
|
|
149
|
+
ON CONFLICT(task_id, memory_id) DO UPDATE SET
|
|
150
|
+
used_at = excluded.used_at
|
|
151
|
+
""",
|
|
152
|
+
(task_id, memory_id, now_iso, context),
|
|
153
|
+
)
|
|
154
|
+
links.append(
|
|
155
|
+
TaskMemoryLink(
|
|
156
|
+
task_id=task_id,
|
|
157
|
+
memory_id=memory_id,
|
|
158
|
+
used_at=now,
|
|
159
|
+
context=context,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
await conn.commit()
|
|
163
|
+
|
|
164
|
+
logger.debug(f"Linked {len(memory_ids)} memories to task {task_id}")
|
|
165
|
+
return links
|
|
166
|
+
|
|
167
|
+
async def unlink(self, task_id: str, memory_id: str) -> bool:
|
|
168
|
+
"""Remove a link between a task and memory.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
task_id: The Beads task ID.
|
|
172
|
+
memory_id: The Runtime Memory memory ID.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if a link was removed, False if it didn't exist.
|
|
176
|
+
"""
|
|
177
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
178
|
+
cursor = await conn.execute(
|
|
179
|
+
"DELETE FROM task_memory_links WHERE task_id = ? AND memory_id = ?",
|
|
180
|
+
(task_id, memory_id),
|
|
181
|
+
)
|
|
182
|
+
await conn.commit()
|
|
183
|
+
removed = cursor.rowcount > 0
|
|
184
|
+
|
|
185
|
+
if removed:
|
|
186
|
+
logger.debug(f"Unlinked memory {memory_id} from task {task_id}")
|
|
187
|
+
return removed
|
|
188
|
+
|
|
189
|
+
async def get_memories_for_task(
|
|
190
|
+
self,
|
|
191
|
+
task_id: str,
|
|
192
|
+
include_with_outcome: bool = True,
|
|
193
|
+
) -> list[TaskMemoryLink]:
|
|
194
|
+
"""Get all memory links for a task.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
task_id: The Beads task ID.
|
|
198
|
+
include_with_outcome: If False, only returns links without outcomes.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of TaskMemoryLink objects.
|
|
202
|
+
"""
|
|
203
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
204
|
+
conn.row_factory = aiosqlite.Row
|
|
205
|
+
if include_with_outcome:
|
|
206
|
+
cursor = await conn.execute(
|
|
207
|
+
"SELECT * FROM task_memory_links WHERE task_id = ?",
|
|
208
|
+
(task_id,),
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
cursor = await conn.execute(
|
|
212
|
+
"SELECT * FROM task_memory_links WHERE task_id = ? AND outcome IS NULL",
|
|
213
|
+
(task_id,),
|
|
214
|
+
)
|
|
215
|
+
rows = await cursor.fetchall()
|
|
216
|
+
|
|
217
|
+
return [self._row_to_link(row) for row in rows]
|
|
218
|
+
|
|
219
|
+
async def get_unresolved_links(self, task_id: str) -> list[TaskMemoryLink]:
|
|
220
|
+
"""Get links for a task that don't have outcomes recorded yet.
|
|
221
|
+
|
|
222
|
+
This is used to find which memories need outcome feedback
|
|
223
|
+
when a task completes.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
task_id: The Beads task ID.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of TaskMemoryLink objects without outcomes.
|
|
230
|
+
"""
|
|
231
|
+
return await self.get_memories_for_task(task_id, include_with_outcome=False)
|
|
232
|
+
|
|
233
|
+
async def get_tasks_for_memory(self, memory_id: str) -> list[TaskMemoryLink]:
|
|
234
|
+
"""Get all task links for a memory.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
memory_id: The Runtime Memory memory ID.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of TaskMemoryLink objects.
|
|
241
|
+
"""
|
|
242
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
243
|
+
conn.row_factory = aiosqlite.Row
|
|
244
|
+
cursor = await conn.execute(
|
|
245
|
+
"SELECT * FROM task_memory_links WHERE memory_id = ?",
|
|
246
|
+
(memory_id,),
|
|
247
|
+
)
|
|
248
|
+
rows = await cursor.fetchall()
|
|
249
|
+
|
|
250
|
+
return [self._row_to_link(row) for row in rows]
|
|
251
|
+
|
|
252
|
+
async def record_task_outcome(
|
|
253
|
+
self,
|
|
254
|
+
task_id: str,
|
|
255
|
+
outcome: str,
|
|
256
|
+
) -> int:
|
|
257
|
+
"""Record outcome for all unresolved links of a task.
|
|
258
|
+
|
|
259
|
+
This is called when a task completes to mark all linked
|
|
260
|
+
memories with the appropriate outcome.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
task_id: The Beads task ID.
|
|
264
|
+
outcome: The outcome string ("worked", "failed", "partial").
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Number of links updated.
|
|
268
|
+
"""
|
|
269
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
270
|
+
cursor = await conn.execute(
|
|
271
|
+
"""
|
|
272
|
+
UPDATE task_memory_links
|
|
273
|
+
SET outcome = ?
|
|
274
|
+
WHERE task_id = ? AND outcome IS NULL
|
|
275
|
+
""",
|
|
276
|
+
(outcome, task_id),
|
|
277
|
+
)
|
|
278
|
+
await conn.commit()
|
|
279
|
+
count = cursor.rowcount
|
|
280
|
+
|
|
281
|
+
logger.debug(f"Recorded outcome '{outcome}' for {count} links of task {task_id}")
|
|
282
|
+
return count
|
|
283
|
+
|
|
284
|
+
async def get_link(self, task_id: str, memory_id: str) -> TaskMemoryLink | None:
|
|
285
|
+
"""Get a specific task-memory link.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
task_id: The Beads task ID.
|
|
289
|
+
memory_id: The Runtime Memory memory ID.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The link or None if not found.
|
|
293
|
+
"""
|
|
294
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
295
|
+
conn.row_factory = aiosqlite.Row
|
|
296
|
+
cursor = await conn.execute(
|
|
297
|
+
"SELECT * FROM task_memory_links WHERE task_id = ? AND memory_id = ?",
|
|
298
|
+
(task_id, memory_id),
|
|
299
|
+
)
|
|
300
|
+
row = await cursor.fetchone()
|
|
301
|
+
|
|
302
|
+
return self._row_to_link(row) if row else None
|
|
303
|
+
|
|
304
|
+
async def count_links(self, task_id: str | None = None) -> int:
|
|
305
|
+
"""Count total links, optionally for a specific task.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
task_id: Optional task ID to filter by.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Number of links.
|
|
312
|
+
"""
|
|
313
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
314
|
+
if task_id:
|
|
315
|
+
cursor = await conn.execute(
|
|
316
|
+
"SELECT COUNT(*) FROM task_memory_links WHERE task_id = ?",
|
|
317
|
+
(task_id,),
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
cursor = await conn.execute("SELECT COUNT(*) FROM task_memory_links")
|
|
321
|
+
row = await cursor.fetchone()
|
|
322
|
+
|
|
323
|
+
return row[0] if row else 0
|
|
324
|
+
|
|
325
|
+
async def get_stats(self) -> dict:
|
|
326
|
+
"""Get statistics about task-memory links.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Dict with link statistics.
|
|
330
|
+
"""
|
|
331
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
332
|
+
# Total links
|
|
333
|
+
cursor = await conn.execute("SELECT COUNT(*) FROM task_memory_links")
|
|
334
|
+
total = (await cursor.fetchone())[0]
|
|
335
|
+
|
|
336
|
+
# Links by outcome
|
|
337
|
+
cursor = await conn.execute(
|
|
338
|
+
"""
|
|
339
|
+
SELECT outcome, COUNT(*) as count
|
|
340
|
+
FROM task_memory_links
|
|
341
|
+
GROUP BY outcome
|
|
342
|
+
"""
|
|
343
|
+
)
|
|
344
|
+
by_outcome = {row[0] or "unresolved": row[1] for row in await cursor.fetchall()}
|
|
345
|
+
|
|
346
|
+
# Unique tasks
|
|
347
|
+
cursor = await conn.execute(
|
|
348
|
+
"SELECT COUNT(DISTINCT task_id) FROM task_memory_links"
|
|
349
|
+
)
|
|
350
|
+
unique_tasks = (await cursor.fetchone())[0]
|
|
351
|
+
|
|
352
|
+
# Unique memories
|
|
353
|
+
cursor = await conn.execute(
|
|
354
|
+
"SELECT COUNT(DISTINCT memory_id) FROM task_memory_links"
|
|
355
|
+
)
|
|
356
|
+
unique_memories = (await cursor.fetchone())[0]
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
"total_links": total,
|
|
360
|
+
"by_outcome": by_outcome,
|
|
361
|
+
"unique_tasks": unique_tasks,
|
|
362
|
+
"unique_memories": unique_memories,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async def clear_task_links(self, task_id: str) -> int:
|
|
366
|
+
"""Remove all links for a task.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
task_id: The Beads task ID.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Number of links removed.
|
|
373
|
+
"""
|
|
374
|
+
async with aiosqlite.connect(self.db_path) as conn:
|
|
375
|
+
cursor = await conn.execute(
|
|
376
|
+
"DELETE FROM task_memory_links WHERE task_id = ?",
|
|
377
|
+
(task_id,),
|
|
378
|
+
)
|
|
379
|
+
await conn.commit()
|
|
380
|
+
count = cursor.rowcount
|
|
381
|
+
|
|
382
|
+
logger.debug(f"Cleared {count} links for task {task_id}")
|
|
383
|
+
return count
|
|
384
|
+
|
|
385
|
+
def _row_to_link(self, row: aiosqlite.Row) -> TaskMemoryLink:
|
|
386
|
+
"""Convert a database row to a TaskMemoryLink."""
|
|
387
|
+
used_at = row["used_at"]
|
|
388
|
+
if isinstance(used_at, str):
|
|
389
|
+
used_at = datetime.fromisoformat(used_at.replace("Z", "+00:00"))
|
|
390
|
+
|
|
391
|
+
return TaskMemoryLink(
|
|
392
|
+
task_id=row["task_id"],
|
|
393
|
+
memory_id=row["memory_id"],
|
|
394
|
+
used_at=used_at,
|
|
395
|
+
outcome=row["outcome"],
|
|
396
|
+
context=row["context"],
|
|
397
|
+
)
|