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,114 @@
1
+ """Task tracker integration for Runtime Memory.
2
+
3
+ This module provides integration with task tracking systems,
4
+ enabling automatic outcome capture when tasks complete.
5
+
6
+ Supported task systems:
7
+ - Beads task tracker (.beads/ directory)
8
+ - Claude Code todos (~/.claude/todos/)
9
+
10
+ Key Features:
11
+ - Parse task data from multiple sources
12
+ - Link memories to tasks they're used for
13
+ - Auto-record outcomes when tasks complete
14
+ - Unified context combining tasks + memories
15
+ - Unified adapter for all task sources
16
+
17
+ Example:
18
+ >>> from runtime_memory.tasks import UnifiedTaskAdapter
19
+ >>> adapter = UnifiedTaskAdapter(engine)
20
+ >>> await adapter.initialize()
21
+ >>> tasks = adapter.list_tasks() # From all sources
22
+ >>> result = await adapter.sync_all()
23
+ >>> print(f"Recorded {result.total_outcomes_recorded} outcomes")
24
+
25
+ # Or use specific adapters:
26
+ >>> from runtime_memory.tasks import BeadsAdapter, ClaudeCodeAdapter
27
+ """
28
+
29
+ from runtime_memory.tasks.adapter import BeadsAdapter, NullBeadsAdapter, create_adapter
30
+ from runtime_memory.tasks.claude_code_adapter import (
31
+ ClaudeCodeAdapter,
32
+ NullClaudeCodeAdapter,
33
+ create_claude_code_adapter,
34
+ )
35
+ from runtime_memory.tasks.claude_code_parser import (
36
+ ClaudeCodeDirectoryNotFoundError,
37
+ ClaudeCodeParser,
38
+ )
39
+ from runtime_memory.tasks.cli_bridge import BeadsCLI, get_beads_cli
40
+ from runtime_memory.tasks.linking import TaskMemoryLinker
41
+ from runtime_memory.tasks.models import (
42
+ CANCELLED_TASK_PENALTY,
43
+ CLAUDE_CODE_STATUS_TO_OUTCOME,
44
+ # Constants
45
+ TASK_STATUS_TO_OUTCOME,
46
+ BeadsSyncResult,
47
+ # Beads models
48
+ BeadsTask,
49
+ # Enums
50
+ BeadsTaskStatus,
51
+ # Claude Code models
52
+ ClaudeCodeTask,
53
+ ClaudeCodeTaskStatus,
54
+ # Shared models
55
+ Task,
56
+ TaskContext,
57
+ TaskMemoryLink,
58
+ TaskSource,
59
+ TaskSyncResult,
60
+ )
61
+ from runtime_memory.tasks.outcomes import OutcomeCapture, auto_capture_outcome
62
+ from runtime_memory.tasks.parser import BeadsDirectoryNotFoundError, BeadsParser
63
+ from runtime_memory.tasks.unified_adapter import (
64
+ UnifiedSyncResult,
65
+ UnifiedTask,
66
+ UnifiedTaskAdapter,
67
+ create_unified_adapter,
68
+ )
69
+
70
+ __all__ = [
71
+ # === Enums ===
72
+ "BeadsTaskStatus",
73
+ "ClaudeCodeTaskStatus",
74
+ "TaskSource",
75
+ # === Task Models ===
76
+ "BeadsTask",
77
+ "ClaudeCodeTask",
78
+ "Task", # Type alias for BeadsTask | ClaudeCodeTask
79
+ "TaskMemoryLink",
80
+ "TaskContext",
81
+ # === Sync Results ===
82
+ "BeadsSyncResult",
83
+ "TaskSyncResult",
84
+ "UnifiedSyncResult",
85
+ # === Parsers ===
86
+ "BeadsParser",
87
+ "BeadsDirectoryNotFoundError",
88
+ "ClaudeCodeParser",
89
+ "ClaudeCodeDirectoryNotFoundError",
90
+ # === CLI Bridge ===
91
+ "BeadsCLI",
92
+ "get_beads_cli",
93
+ # === Linking ===
94
+ "TaskMemoryLinker",
95
+ # === Outcome Capture ===
96
+ "OutcomeCapture",
97
+ "auto_capture_outcome",
98
+ # === Beads Adapter ===
99
+ "BeadsAdapter",
100
+ "NullBeadsAdapter",
101
+ "create_adapter",
102
+ # === Claude Code Adapter ===
103
+ "ClaudeCodeAdapter",
104
+ "NullClaudeCodeAdapter",
105
+ "create_claude_code_adapter",
106
+ # === Unified Adapter (main entry point) ===
107
+ "UnifiedTaskAdapter",
108
+ "UnifiedTask",
109
+ "create_unified_adapter",
110
+ # === Constants ===
111
+ "TASK_STATUS_TO_OUTCOME",
112
+ "CLAUDE_CODE_STATUS_TO_OUTCOME",
113
+ "CANCELLED_TASK_PENALTY",
114
+ ]
@@ -0,0 +1,501 @@
1
+ """Unified Beads adapter combining all task integration components.
2
+
3
+ This module provides a single interface for all Beads task tracker
4
+ integration functionality, combining:
5
+ - Task parsing from .beads/ directory
6
+ - Task-memory linking
7
+ - Automatic outcome capture
8
+ - Context generation
9
+
10
+ Example:
11
+ >>> from runtime_memory.tasks import BeadsAdapter
12
+ >>> adapter = BeadsAdapter(engine)
13
+ >>> await adapter.initialize()
14
+ >>>
15
+ >>> # Sync outcomes for completed tasks
16
+ >>> result = await adapter.sync()
17
+ >>> print(f"Recorded {result.outcomes_recorded} outcomes")
18
+ >>>
19
+ >>> # Get context for current task
20
+ >>> context = await adapter.get_unified_context()
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from pathlib import Path
27
+ from typing import TYPE_CHECKING
28
+
29
+ from runtime_memory.core.models import Memory
30
+ from runtime_memory.tasks.linking import TaskMemoryLinker
31
+ from runtime_memory.tasks.models import (
32
+ BeadsSyncResult,
33
+ BeadsTask,
34
+ BeadsTaskStatus,
35
+ TaskContext,
36
+ )
37
+ from runtime_memory.tasks.outcomes import OutcomeCapture
38
+ from runtime_memory.tasks.parser import BeadsParser
39
+
40
+ if TYPE_CHECKING:
41
+ from runtime_memory.core.engine import MemoryEngine
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ class BeadsAdapter:
47
+ """Unified adapter for Beads task tracker integration.
48
+
49
+ Combines:
50
+ - BeadsParser: Read tasks from .beads/ directory
51
+ - TaskMemoryLinker: Track which memories are used per task
52
+ - OutcomeCapture: Auto-record outcomes when tasks complete
53
+
54
+ This is the main entry point for all Beads integration functionality.
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ engine: MemoryEngine,
60
+ beads_dir: Path | str | None = None,
61
+ auto_outcome_enabled: bool = True,
62
+ outcome_on_cancel: bool = False,
63
+ ) -> None:
64
+ """Initialize the Beads adapter.
65
+
66
+ Args:
67
+ engine: The Runtime Memory engine.
68
+ beads_dir: Explicit path to .beads/ directory (auto-discovers if None).
69
+ auto_outcome_enabled: Whether to auto-record outcomes on task completion.
70
+ outcome_on_cancel: Whether to record "failed" when tasks are cancelled.
71
+ """
72
+ self._engine = engine
73
+ self._beads_dir = Path(beads_dir) if beads_dir else None
74
+
75
+ # Initialize components
76
+ self._parser = BeadsParser(self._beads_dir)
77
+ self._linker: TaskMemoryLinker | None = None
78
+ self._outcome_capture: OutcomeCapture | None = None
79
+
80
+ # Configuration
81
+ self.auto_outcome_enabled = auto_outcome_enabled
82
+ self.outcome_on_cancel = outcome_on_cancel
83
+
84
+ self._initialized = False
85
+
86
+ async def initialize(self) -> None:
87
+ """Initialize all components.
88
+
89
+ Creates the linker table and sets up outcome capture.
90
+ Must be called before using the adapter.
91
+ """
92
+ if self._initialized:
93
+ return
94
+
95
+ # Get db_path from engine's storage
96
+ db_path = self._engine._storage.db_path
97
+
98
+ # Initialize linker
99
+ self._linker = TaskMemoryLinker(db_path)
100
+ await self._linker.initialize()
101
+
102
+ # Initialize outcome capture
103
+ self._outcome_capture = OutcomeCapture(
104
+ engine=self._engine,
105
+ linker=self._linker,
106
+ parser=self._parser,
107
+ auto_outcome_enabled=self.auto_outcome_enabled,
108
+ outcome_on_cancel=self.outcome_on_cancel,
109
+ )
110
+
111
+ self._initialized = True
112
+ logger.debug("BeadsAdapter initialized")
113
+
114
+ def _ensure_initialized(self) -> None:
115
+ """Ensure the adapter is initialized."""
116
+ if not self._initialized:
117
+ raise RuntimeError("BeadsAdapter not initialized. Call initialize() first.")
118
+
119
+ @property
120
+ def is_available(self) -> bool:
121
+ """Check if Beads is available (directory exists)."""
122
+ return self._parser.is_available()
123
+
124
+ # =========================================================================
125
+ # Task Operations
126
+ # =========================================================================
127
+
128
+ def get_task(self, task_id: str) -> BeadsTask | None:
129
+ """Get a task by ID.
130
+
131
+ Args:
132
+ task_id: The Beads task ID (e.g., "bd-a3f8").
133
+
134
+ Returns:
135
+ The task or None if not found.
136
+ """
137
+ return self._parser.get_task(task_id)
138
+
139
+ def list_tasks(
140
+ self,
141
+ status: BeadsTaskStatus | None = None,
142
+ ) -> list[BeadsTask]:
143
+ """List all tasks, optionally filtered by status.
144
+
145
+ Args:
146
+ status: Optional status filter.
147
+
148
+ Returns:
149
+ List of matching tasks.
150
+ """
151
+ return self._parser.list_tasks(status=status)
152
+
153
+ def get_ready_tasks(self) -> list[BeadsTask]:
154
+ """Get tasks that are ready to work on.
155
+
156
+ Returns:
157
+ List of tasks with no blockers and pending status.
158
+ """
159
+ return self._parser.get_ready_tasks()
160
+
161
+ def get_current_task(self) -> BeadsTask | None:
162
+ """Get the currently active task (in_progress).
163
+
164
+ Returns:
165
+ The in-progress task, or None if no task is active.
166
+ """
167
+ in_progress = self._parser.get_in_progress_tasks()
168
+ return in_progress[0] if in_progress else None
169
+
170
+ # =========================================================================
171
+ # Memory Linking
172
+ # =========================================================================
173
+
174
+ async def link_memory_to_task(
175
+ self,
176
+ task_id: str,
177
+ memory_id: str,
178
+ context: str | None = None,
179
+ ) -> None:
180
+ """Link a memory to a task.
181
+
182
+ Args:
183
+ task_id: The Beads task ID.
184
+ memory_id: The Runtime Memory memory ID.
185
+ context: Optional context about how memory was used.
186
+ """
187
+ self._ensure_initialized()
188
+ await self._linker.link(task_id, memory_id, context)
189
+
190
+ async def link_memories_to_task(
191
+ self,
192
+ task_id: str,
193
+ memory_ids: list[str],
194
+ context: str | None = None,
195
+ ) -> None:
196
+ """Link multiple memories to a task.
197
+
198
+ Args:
199
+ task_id: The Beads task ID.
200
+ memory_ids: List of Runtime Memory memory IDs.
201
+ context: Optional context about how memories were used.
202
+ """
203
+ self._ensure_initialized()
204
+ await self._linker.link_many(task_id, memory_ids, context)
205
+
206
+ async def get_task_memories(self, task_id: str) -> list[Memory]:
207
+ """Get all memories linked to a task.
208
+
209
+ Args:
210
+ task_id: The Beads task ID.
211
+
212
+ Returns:
213
+ List of Memory objects linked to the task.
214
+ """
215
+ self._ensure_initialized()
216
+ links = await self._linker.get_memories_for_task(task_id)
217
+ memories = []
218
+ for link in links:
219
+ try:
220
+ memory = await self._engine.get(link.memory_id)
221
+ memories.append(memory)
222
+ except Exception:
223
+ # Memory might not exist anymore
224
+ logger.debug(f"Memory {link.memory_id} not found, skipping")
225
+ return memories
226
+
227
+ async def auto_link_search_results(
228
+ self,
229
+ task_id: str,
230
+ memory_ids: list[str],
231
+ ) -> None:
232
+ """Automatically link search results to the current task.
233
+
234
+ Called after a search to track which memories were surfaced.
235
+
236
+ Args:
237
+ task_id: The Beads task ID.
238
+ memory_ids: Memory IDs from search results.
239
+ """
240
+ self._ensure_initialized()
241
+ await self._linker.link_many(task_id, memory_ids, context="search_result")
242
+
243
+ # =========================================================================
244
+ # Outcome Capture
245
+ # =========================================================================
246
+
247
+ async def on_task_done(self, task_id: str) -> int:
248
+ """Handle a task being marked as done.
249
+
250
+ Records "worked" outcome for all linked memories.
251
+
252
+ Args:
253
+ task_id: The Beads task ID.
254
+
255
+ Returns:
256
+ Number of memories that had outcomes recorded.
257
+ """
258
+ self._ensure_initialized()
259
+ return await self._outcome_capture.on_task_completed(task_id)
260
+
261
+ async def on_task_cancelled(self, task_id: str) -> int:
262
+ """Handle a task being cancelled.
263
+
264
+ Records "failed" outcome for linked memories (if enabled).
265
+
266
+ Args:
267
+ task_id: The Beads task ID.
268
+
269
+ Returns:
270
+ Number of memories that had outcomes recorded.
271
+ """
272
+ self._ensure_initialized()
273
+ return await self._outcome_capture.on_task_failed(task_id)
274
+
275
+ async def on_task_blocked(self, task_id: str) -> int:
276
+ """Handle a task being blocked.
277
+
278
+ Records "partial" outcome for linked memories.
279
+
280
+ Args:
281
+ task_id: The Beads task ID.
282
+
283
+ Returns:
284
+ Number of memories that had outcomes recorded.
285
+ """
286
+ self._ensure_initialized()
287
+ return await self._outcome_capture.on_task_blocked(task_id)
288
+
289
+ async def sync(self) -> BeadsSyncResult:
290
+ """Sync outcomes for all completed tasks.
291
+
292
+ Scans completed tasks and records outcomes for any that
293
+ have unresolved memory links.
294
+
295
+ Returns:
296
+ BeadsSyncResult with sync statistics.
297
+ """
298
+ self._ensure_initialized()
299
+ return await self._outcome_capture.sync_completed_tasks()
300
+
301
+ # =========================================================================
302
+ # Context Generation
303
+ # =========================================================================
304
+
305
+ async def get_unified_context(
306
+ self,
307
+ task_id: str | None = None,
308
+ max_memories: int = 10,
309
+ ) -> TaskContext | None:
310
+ """Get unified context combining task info and relevant memories.
311
+
312
+ Args:
313
+ task_id: The task ID to get context for. If None, uses current task.
314
+ max_memories: Maximum number of memories to include.
315
+
316
+ Returns:
317
+ TaskContext object or None if no task found.
318
+ """
319
+ self._ensure_initialized()
320
+
321
+ # Get task
322
+ if task_id:
323
+ task = self.get_task(task_id)
324
+ else:
325
+ task = self.get_current_task()
326
+
327
+ if not task:
328
+ return None
329
+
330
+ # Get linked memories
331
+ memories = await self.get_task_memories(task.id)
332
+
333
+ # If no linked memories, search for relevant ones
334
+ if not memories and task.title:
335
+ search_query = f"{task.title} {task.description[:100] if task.description else ''}"
336
+ results = await self._engine.search(
337
+ search_query,
338
+ limit=max_memories,
339
+ min_score=0.5, # Only include memories with >50% relevance
340
+ track_usage=False, # Don't track this as usage
341
+ )
342
+ # Filter to only highly relevant results
343
+ memories = [r.memory for r in results if r.score >= 0.5]
344
+
345
+ # Limit memories
346
+ memories = memories[:max_memories]
347
+
348
+ # Create context
349
+ context = TaskContext(
350
+ task=task,
351
+ memories=memories,
352
+ )
353
+ context.formatted = context.to_markdown()
354
+
355
+ return context
356
+
357
+ async def get_context_for_injection(
358
+ self,
359
+ task_id: str | None = None,
360
+ max_memories: int = 10,
361
+ ) -> str:
362
+ """Get formatted context string for injection into prompts.
363
+
364
+ Args:
365
+ task_id: The task ID to get context for. If None, uses current task.
366
+ max_memories: Maximum number of memories to include.
367
+
368
+ Returns:
369
+ Formatted markdown string for context injection.
370
+ """
371
+ context = await self.get_unified_context(task_id, max_memories)
372
+ if not context:
373
+ return ""
374
+ return context.formatted
375
+
376
+ # =========================================================================
377
+ # Statistics
378
+ # =========================================================================
379
+
380
+ async def get_stats(self) -> dict:
381
+ """Get statistics about Beads integration.
382
+
383
+ Returns:
384
+ Dict with statistics.
385
+ """
386
+ self._ensure_initialized()
387
+
388
+ # Task stats
389
+ all_tasks = self.list_tasks()
390
+ task_stats = {
391
+ "total_tasks": len(all_tasks),
392
+ "by_status": {},
393
+ }
394
+ for task in all_tasks:
395
+ status = task.status.value
396
+ task_stats["by_status"][status] = task_stats["by_status"].get(status, 0) + 1
397
+
398
+ # Link stats
399
+ link_stats = await self._linker.get_stats()
400
+
401
+ return {
402
+ "beads_available": self.is_available,
403
+ "beads_dir": str(self._parser.beads_dir) if self._parser.beads_dir else None,
404
+ "tasks": task_stats,
405
+ "links": link_stats,
406
+ "auto_outcome_enabled": self.auto_outcome_enabled,
407
+ "outcome_on_cancel": self.outcome_on_cancel,
408
+ }
409
+
410
+ def refresh(self) -> int:
411
+ """Refresh task cache from disk.
412
+
413
+ Returns:
414
+ Number of tasks loaded.
415
+ """
416
+ return self._parser.refresh()
417
+
418
+
419
+ class NullBeadsAdapter:
420
+ """Null adapter for when Beads is not available.
421
+
422
+ Provides the same interface but does nothing, allowing graceful
423
+ degradation when Beads integration is not configured.
424
+ """
425
+
426
+ @property
427
+ def is_available(self) -> bool:
428
+ return False
429
+
430
+ async def initialize(self) -> None:
431
+ pass
432
+
433
+ def get_task(self, task_id: str) -> None:
434
+ return None
435
+
436
+ def list_tasks(self, status: BeadsTaskStatus | None = None) -> list:
437
+ return []
438
+
439
+ def get_ready_tasks(self) -> list:
440
+ return []
441
+
442
+ def get_current_task(self) -> None:
443
+ return None
444
+
445
+ async def link_memory_to_task(self, *args, **kwargs) -> None:
446
+ pass
447
+
448
+ async def link_memories_to_task(self, *args, **kwargs) -> None:
449
+ pass
450
+
451
+ async def get_task_memories(self, task_id: str) -> list:
452
+ return []
453
+
454
+ async def on_task_done(self, task_id: str) -> int:
455
+ return 0
456
+
457
+ async def on_task_cancelled(self, task_id: str) -> int:
458
+ return 0
459
+
460
+ async def on_task_blocked(self, task_id: str) -> int:
461
+ return 0
462
+
463
+ async def sync(self) -> BeadsSyncResult:
464
+ result = BeadsSyncResult()
465
+ result.warnings.append("Beads integration not available")
466
+ return result
467
+
468
+ async def get_unified_context(self, *args, **kwargs) -> None:
469
+ return None
470
+
471
+ async def get_context_for_injection(self, *args, **kwargs) -> str:
472
+ return ""
473
+
474
+ async def get_stats(self) -> dict:
475
+ return {"beads_available": False}
476
+
477
+ def refresh(self) -> int:
478
+ return 0
479
+
480
+
481
+ def create_adapter(
482
+ engine: MemoryEngine,
483
+ beads_dir: Path | str | None = None,
484
+ **kwargs,
485
+ ) -> BeadsAdapter | NullBeadsAdapter:
486
+ """Factory function to create the appropriate adapter.
487
+
488
+ Returns a NullBeadsAdapter if Beads is not available.
489
+
490
+ Args:
491
+ engine: The Runtime Memory engine.
492
+ beads_dir: Explicit path to .beads/ directory.
493
+ **kwargs: Additional arguments for BeadsAdapter.
494
+
495
+ Returns:
496
+ BeadsAdapter if Beads is available, NullBeadsAdapter otherwise.
497
+ """
498
+ adapter = BeadsAdapter(engine, beads_dir, **kwargs)
499
+ if adapter.is_available:
500
+ return adapter
501
+ return NullBeadsAdapter()