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.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. 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
+ )