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,520 @@
1
+ """Data models for task tracker integration.
2
+
3
+ This module defines:
4
+ - Enums for task statuses (Beads and Claude Code)
5
+ - Dataclasses for tasks, links, and sync results
6
+ - Task source enum for unified tracking
7
+ - Pydantic models for validation
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from datetime import UTC, datetime
14
+ from enum import Enum
15
+ from typing import Any
16
+
17
+
18
+ class TaskSource(str, Enum):
19
+ """Source of task data.
20
+
21
+ Used to identify which system a task originated from.
22
+ """
23
+
24
+ BEADS = "beads"
25
+ """Task from Beads task tracker (.beads/ directory)."""
26
+
27
+ CLAUDE_CODE = "claude_code"
28
+ """Task from Claude Code todos (~/.claude/todos/)."""
29
+
30
+
31
+ class BeadsTaskStatus(str, Enum):
32
+ """Task statuses matching Beads conventions.
33
+
34
+ These map to the standard Beads task lifecycle.
35
+ """
36
+
37
+ PENDING = "pending"
38
+ """Task not yet started."""
39
+
40
+ IN_PROGRESS = "in_progress"
41
+ """Task is actively being worked on."""
42
+
43
+ DONE = "done"
44
+ """Task completed successfully."""
45
+
46
+ BLOCKED = "blocked"
47
+ """Task waiting on a dependency or blocker."""
48
+
49
+ CANCELLED = "cancelled"
50
+ """Task was abandoned or no longer needed."""
51
+
52
+
53
+ # Mapping from Beads status to Runtime Memory outcome
54
+ # Used when auto-recording outcomes on task completion
55
+ TASK_STATUS_TO_OUTCOME: dict[BeadsTaskStatus, str] = {
56
+ BeadsTaskStatus.DONE: "worked",
57
+ BeadsTaskStatus.CANCELLED: "failed",
58
+ BeadsTaskStatus.BLOCKED: "partial",
59
+ }
60
+
61
+ # Score adjustment for cancelled tasks (less severe than "failed")
62
+ CANCELLED_TASK_PENALTY = -0.1
63
+
64
+
65
+ @dataclass
66
+ class BeadsTask:
67
+ """Represents a task from the Beads task tracker.
68
+
69
+ This mirrors the Beads JSONL schema for task storage.
70
+
71
+ Attributes:
72
+ id: Hash-based task ID (e.g., "bd-a3f8")
73
+ title: Short task title
74
+ description: Full task description
75
+ status: Current task status
76
+ parent_id: Parent task ID for subtasks (e.g., "bd-a3f8" for "bd-a3f8.1")
77
+ dependencies: List of task IDs this task is blocked by
78
+ tags: List of tags for categorization
79
+ created_at: When the task was created
80
+ updated_at: When the task was last modified
81
+ metadata: Additional custom fields from Beads
82
+ """
83
+
84
+ id: str
85
+ title: str
86
+ status: BeadsTaskStatus = BeadsTaskStatus.PENDING
87
+ description: str = ""
88
+ parent_id: str | None = None
89
+ dependencies: list[str] = field(default_factory=list)
90
+ tags: list[str] = field(default_factory=list)
91
+ created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
92
+ updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
93
+ metadata: dict[str, Any] = field(default_factory=dict)
94
+
95
+ @property
96
+ def is_subtask(self) -> bool:
97
+ """Check if this is a subtask (has a parent)."""
98
+ return self.parent_id is not None
99
+
100
+ @property
101
+ def is_ready(self) -> bool:
102
+ """Check if task is ready to work on (no blockers, pending status)."""
103
+ return self.status == BeadsTaskStatus.PENDING and len(self.dependencies) == 0
104
+
105
+ @property
106
+ def is_completed(self) -> bool:
107
+ """Check if task has reached a terminal state."""
108
+ return self.status in (BeadsTaskStatus.DONE, BeadsTaskStatus.CANCELLED)
109
+
110
+ def to_dict(self) -> dict[str, Any]:
111
+ """Convert to dictionary for serialization."""
112
+ return {
113
+ "id": self.id,
114
+ "title": self.title,
115
+ "description": self.description,
116
+ "status": self.status.value,
117
+ "parent_id": self.parent_id,
118
+ "dependencies": self.dependencies,
119
+ "tags": self.tags,
120
+ "created_at": self.created_at.isoformat(),
121
+ "updated_at": self.updated_at.isoformat(),
122
+ "metadata": self.metadata,
123
+ }
124
+
125
+ @classmethod
126
+ def from_dict(cls, data: dict[str, Any]) -> BeadsTask:
127
+ """Create a BeadsTask from a dictionary (e.g., parsed JSONL)."""
128
+ # Handle status as string or enum
129
+ status = data.get("status", "pending")
130
+ if isinstance(status, str):
131
+ status = BeadsTaskStatus(status)
132
+
133
+ # Parse timestamps
134
+ created_at = data.get("created_at")
135
+ if isinstance(created_at, str):
136
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
137
+ elif created_at is None:
138
+ created_at = datetime.now(UTC)
139
+
140
+ updated_at = data.get("updated_at")
141
+ if isinstance(updated_at, str):
142
+ updated_at = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
143
+ elif updated_at is None:
144
+ updated_at = datetime.now(UTC)
145
+
146
+ return cls(
147
+ id=data["id"],
148
+ title=data.get("title", ""),
149
+ description=data.get("description", ""),
150
+ status=status,
151
+ parent_id=data.get("parent_id"),
152
+ dependencies=data.get("dependencies", []),
153
+ tags=data.get("tags", []),
154
+ created_at=created_at,
155
+ updated_at=updated_at,
156
+ metadata=data.get("metadata", {}),
157
+ )
158
+
159
+
160
+ # =============================================================================
161
+ # Claude Code Task Models
162
+ # =============================================================================
163
+
164
+
165
+ class ClaudeCodeTaskStatus(str, Enum):
166
+ """Task statuses for Claude Code todos.
167
+
168
+ Claude Code uses a simpler status system than Beads.
169
+ """
170
+
171
+ PENDING = "pending"
172
+ """Task not yet started."""
173
+
174
+ IN_PROGRESS = "in_progress"
175
+ """Task is actively being worked on."""
176
+
177
+ COMPLETED = "completed"
178
+ """Task completed."""
179
+
180
+
181
+ # Mapping from Claude Code status to Runtime Memory outcome
182
+ CLAUDE_CODE_STATUS_TO_OUTCOME: dict[ClaudeCodeTaskStatus, str] = {
183
+ ClaudeCodeTaskStatus.COMPLETED: "worked",
184
+ }
185
+
186
+
187
+ @dataclass
188
+ class ClaudeCodeTask:
189
+ """Represents a task from Claude Code todos.
190
+
191
+ Claude Code stores todos in ~/.claude/todos/ as JSON files.
192
+ Each file is a JSON array of todo objects.
193
+
194
+ Attributes:
195
+ id: Generated task ID (session_id:index or content hash)
196
+ content: Task content/description
197
+ status: Current task status
198
+ active_form: Present continuous form shown in spinner
199
+ session_id: Claude session ID the task belongs to
200
+ agent_id: Agent ID (if task is from a sub-agent)
201
+ index: Index within the session's task list
202
+ file_path: Path to the source file
203
+ created_at: When the task was first seen
204
+ updated_at: When the task was last modified
205
+ """
206
+
207
+ id: str
208
+ content: str
209
+ status: ClaudeCodeTaskStatus = ClaudeCodeTaskStatus.PENDING
210
+ active_form: str = ""
211
+ session_id: str = ""
212
+ agent_id: str = ""
213
+ index: int = 0
214
+ file_path: str = ""
215
+ created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
216
+ updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
217
+
218
+ @property
219
+ def title(self) -> str:
220
+ """Get task title (first 80 chars of content for compatibility)."""
221
+ return self.content[:80] + ("..." if len(self.content) > 80 else "")
222
+
223
+ @property
224
+ def description(self) -> str:
225
+ """Get full task description (alias for content)."""
226
+ return self.content
227
+
228
+ @property
229
+ def is_completed(self) -> bool:
230
+ """Check if task has been completed."""
231
+ return self.status == ClaudeCodeTaskStatus.COMPLETED
232
+
233
+ @property
234
+ def is_ready(self) -> bool:
235
+ """Check if task is ready to work on."""
236
+ return self.status == ClaudeCodeTaskStatus.PENDING
237
+
238
+ @property
239
+ def source(self) -> TaskSource:
240
+ """Get the task source."""
241
+ return TaskSource.CLAUDE_CODE
242
+
243
+ def to_dict(self) -> dict[str, Any]:
244
+ """Convert to dictionary for serialization."""
245
+ return {
246
+ "id": self.id,
247
+ "content": self.content,
248
+ "status": self.status.value,
249
+ "activeForm": self.active_form,
250
+ "session_id": self.session_id,
251
+ "agent_id": self.agent_id,
252
+ "index": self.index,
253
+ "file_path": self.file_path,
254
+ "created_at": self.created_at.isoformat(),
255
+ "updated_at": self.updated_at.isoformat(),
256
+ "source": self.source.value,
257
+ }
258
+
259
+ @classmethod
260
+ def from_dict(
261
+ cls,
262
+ data: dict[str, Any],
263
+ session_id: str = "",
264
+ agent_id: str = "",
265
+ index: int = 0,
266
+ file_path: str = "",
267
+ ) -> ClaudeCodeTask:
268
+ """Create a ClaudeCodeTask from a dictionary.
269
+
270
+ Args:
271
+ data: Dictionary with 'content', 'status', 'activeForm' keys.
272
+ session_id: The Claude session ID.
273
+ agent_id: The agent ID.
274
+ index: Index in the task list.
275
+ file_path: Path to the source file.
276
+
277
+ Returns:
278
+ ClaudeCodeTask instance.
279
+ """
280
+ # Handle status mapping
281
+ raw_status = data.get("status", "pending")
282
+ if isinstance(raw_status, str):
283
+ # Map Claude Code statuses to our enum
284
+ status_map = {
285
+ "pending": ClaudeCodeTaskStatus.PENDING,
286
+ "in_progress": ClaudeCodeTaskStatus.IN_PROGRESS,
287
+ "completed": ClaudeCodeTaskStatus.COMPLETED,
288
+ }
289
+ status = status_map.get(raw_status, ClaudeCodeTaskStatus.PENDING)
290
+ else:
291
+ status = raw_status
292
+
293
+ content = data.get("content", "")
294
+
295
+ # Generate task ID from session_id and index
296
+ task_id = f"cc-{session_id[:8]}-{index}" if session_id else f"cc-{index}"
297
+
298
+ # Parse timestamps if present
299
+ created_at = data.get("created_at")
300
+ if isinstance(created_at, str):
301
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
302
+ elif created_at is None:
303
+ created_at = datetime.now(UTC)
304
+
305
+ updated_at = data.get("updated_at")
306
+ if isinstance(updated_at, str):
307
+ updated_at = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
308
+ elif updated_at is None:
309
+ updated_at = datetime.now(UTC)
310
+
311
+ return cls(
312
+ id=task_id,
313
+ content=content,
314
+ status=status,
315
+ active_form=data.get("activeForm", ""),
316
+ session_id=session_id,
317
+ agent_id=agent_id,
318
+ index=index,
319
+ file_path=file_path,
320
+ created_at=created_at,
321
+ updated_at=updated_at,
322
+ )
323
+
324
+
325
+ # =============================================================================
326
+ # Unified Task Type
327
+ # =============================================================================
328
+
329
+ # Type alias for any task type
330
+ Task = BeadsTask | ClaudeCodeTask
331
+
332
+
333
+ @dataclass
334
+ class TaskMemoryLink:
335
+ """Links a memory to a task it was used for.
336
+
337
+ This enables automatic outcome capture: when a task completes,
338
+ all linked memories can have their outcomes recorded.
339
+
340
+ Attributes:
341
+ task_id: Beads task ID
342
+ memory_id: Runtime Memory memory ID
343
+ used_at: When the memory was surfaced for this task
344
+ outcome: Outcome recorded when task completed (None until then)
345
+ context: Optional context about how memory was used
346
+ """
347
+
348
+ task_id: str
349
+ memory_id: str
350
+ used_at: datetime = field(default_factory=lambda: datetime.now(UTC))
351
+ outcome: str | None = None
352
+ context: str | None = None
353
+
354
+ def to_dict(self) -> dict[str, Any]:
355
+ """Convert to dictionary for storage."""
356
+ return {
357
+ "task_id": self.task_id,
358
+ "memory_id": self.memory_id,
359
+ "used_at": self.used_at.isoformat(),
360
+ "outcome": self.outcome,
361
+ "context": self.context,
362
+ }
363
+
364
+ @classmethod
365
+ def from_dict(cls, data: dict[str, Any]) -> TaskMemoryLink:
366
+ """Create from dictionary."""
367
+ used_at = data.get("used_at")
368
+ if isinstance(used_at, str):
369
+ used_at = datetime.fromisoformat(used_at.replace("Z", "+00:00"))
370
+ elif used_at is None:
371
+ used_at = datetime.now(UTC)
372
+
373
+ return cls(
374
+ task_id=data["task_id"],
375
+ memory_id=data["memory_id"],
376
+ used_at=used_at,
377
+ outcome=data.get("outcome"),
378
+ context=data.get("context"),
379
+ )
380
+
381
+
382
+ @dataclass
383
+ class BeadsSyncResult:
384
+ """Result of syncing with Beads task tracker.
385
+
386
+ Attributes:
387
+ tasks_found: Total tasks discovered in .beads/
388
+ tasks_synced: Tasks that were processed
389
+ outcomes_recorded: Number of automatic outcome recordings
390
+ memories_linked: Number of new task-memory links created
391
+ errors: List of error messages encountered
392
+ warnings: List of warning messages
393
+ """
394
+
395
+ tasks_found: int = 0
396
+ tasks_synced: int = 0
397
+ outcomes_recorded: int = 0
398
+ memories_linked: int = 0
399
+ errors: list[str] = field(default_factory=list)
400
+ warnings: list[str] = field(default_factory=list)
401
+
402
+ @property
403
+ def success(self) -> bool:
404
+ """Check if sync completed without errors."""
405
+ return len(self.errors) == 0
406
+
407
+ def to_dict(self) -> dict[str, Any]:
408
+ """Convert to dictionary for JSON serialization."""
409
+ return {
410
+ "tasks_found": self.tasks_found,
411
+ "tasks_synced": self.tasks_synced,
412
+ "outcomes_recorded": self.outcomes_recorded,
413
+ "memories_linked": self.memories_linked,
414
+ "errors": self.errors,
415
+ "warnings": self.warnings,
416
+ "success": self.success,
417
+ }
418
+
419
+
420
+ @dataclass
421
+ class TaskSyncResult:
422
+ """Result of syncing with any task tracker.
423
+
424
+ Generic version that works with both Beads and Claude Code tasks.
425
+
426
+ Attributes:
427
+ source: Which task system was synced
428
+ tasks_found: Total tasks discovered
429
+ tasks_synced: Tasks that were processed
430
+ outcomes_recorded: Number of automatic outcome recordings
431
+ memories_linked: Number of new task-memory links created
432
+ errors: List of error messages encountered
433
+ warnings: List of warning messages
434
+ """
435
+
436
+ source: TaskSource = TaskSource.BEADS
437
+ tasks_found: int = 0
438
+ tasks_synced: int = 0
439
+ outcomes_recorded: int = 0
440
+ memories_linked: int = 0
441
+ errors: list[str] = field(default_factory=list)
442
+ warnings: list[str] = field(default_factory=list)
443
+
444
+ @property
445
+ def success(self) -> bool:
446
+ """Check if sync completed without errors."""
447
+ return len(self.errors) == 0
448
+
449
+ def to_dict(self) -> dict[str, Any]:
450
+ """Convert to dictionary for JSON serialization."""
451
+ return {
452
+ "source": self.source.value,
453
+ "tasks_found": self.tasks_found,
454
+ "tasks_synced": self.tasks_synced,
455
+ "outcomes_recorded": self.outcomes_recorded,
456
+ "memories_linked": self.memories_linked,
457
+ "errors": self.errors,
458
+ "warnings": self.warnings,
459
+ "success": self.success,
460
+ }
461
+
462
+
463
+ @dataclass
464
+ class TaskContext:
465
+ """Unified context combining task info and relevant memories.
466
+
467
+ Used for context injection at the start of a task-focused session.
468
+ Works with both Beads and Claude Code tasks.
469
+
470
+ Attributes:
471
+ task: The current task (Beads or Claude Code)
472
+ memories: Relevant memories for this task
473
+ formatted: Pre-formatted string for injection
474
+ source: Which task system the task is from
475
+ """
476
+
477
+ task: Task # BeadsTask | ClaudeCodeTask
478
+ memories: list[Any] # list[Memory] - Any to avoid circular import
479
+ formatted: str = ""
480
+ source: TaskSource = TaskSource.BEADS
481
+
482
+ def to_markdown(self) -> str:
483
+ """Format as markdown for context injection."""
484
+ # Get title - works for both task types
485
+ title = self.task.title if hasattr(self.task, "title") else str(self.task)
486
+
487
+ lines = [
488
+ f"## Current Task: {title}",
489
+ "",
490
+ f"**Status:** {self.task.status.value}",
491
+ f"**ID:** {self.task.id}",
492
+ f"**Source:** {self.source.value}",
493
+ ]
494
+
495
+ # Get description - works differently for each type
496
+ description = ""
497
+ if isinstance(self.task, BeadsTask):
498
+ description = self.task.description
499
+ elif isinstance(self.task, ClaudeCodeTask):
500
+ description = self.task.content
501
+
502
+ if description:
503
+ lines.extend(["", "### Description", description])
504
+
505
+ # Dependencies - only for Beads tasks
506
+ if isinstance(self.task, BeadsTask) and self.task.dependencies:
507
+ lines.extend(
508
+ ["", "### Blocked By", *[f"- {dep}" for dep in self.task.dependencies]]
509
+ )
510
+
511
+ if self.memories:
512
+ lines.extend(["", "### Relevant Memories", ""])
513
+ for mem in self.memories:
514
+ content = getattr(mem, "content", str(mem))
515
+ category = getattr(mem, "category", "unknown")
516
+ if hasattr(category, "value"):
517
+ category = category.value
518
+ lines.append(f"- **[{category}]** {content[:200]}...")
519
+
520
+ return "\n".join(lines)