aury-agent 0.0.4__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 (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,65 @@
1
+ """Code backend for code execution.
2
+
3
+ Supports executing code in various languages.
4
+ No default implementation - user must provide one.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Literal, Protocol, runtime_checkable
10
+
11
+
12
+ @dataclass
13
+ class CodeResult:
14
+ """Result of code execution."""
15
+ output: str
16
+ error: str = ""
17
+ exit_code: int = 0
18
+ language: str = "python"
19
+ duration_ms: int = 0
20
+
21
+ # Optional: execution artifacts (images, files, etc.)
22
+ artifacts: list[dict[str, Any]] = field(default_factory=list)
23
+
24
+ @property
25
+ def success(self) -> bool:
26
+ return self.exit_code == 0
27
+
28
+
29
+ @runtime_checkable
30
+ class CodeBackend(Protocol):
31
+ """Protocol for code execution.
32
+
33
+ No default implementation provided.
34
+ Users can implement using:
35
+ - Local subprocess execution
36
+ - E2B code interpreter
37
+ - Jupyter kernel
38
+ - etc.
39
+ """
40
+
41
+ @property
42
+ def supported_languages(self) -> list[str]:
43
+ """List of supported languages."""
44
+ ...
45
+
46
+ async def execute(
47
+ self,
48
+ code: str,
49
+ language: str = "python",
50
+ timeout: int = 120,
51
+ ) -> CodeResult:
52
+ """Execute code.
53
+
54
+ Args:
55
+ code: Code to execute
56
+ language: Programming language
57
+ timeout: Timeout in seconds
58
+
59
+ Returns:
60
+ CodeResult with output, error, exit_code
61
+ """
62
+ ...
63
+
64
+
65
+ __all__ = ["CodeBackend", "CodeResult"]
@@ -0,0 +1,11 @@
1
+ """File backend for file system operations.
2
+
3
+ Supports different file systems:
4
+ - Local file system
5
+ - Sandbox file system
6
+ - S3/Cloud storage
7
+ """
8
+ from .types import FileBackend
9
+ from .local import LocalFileBackend
10
+
11
+ __all__ = ["FileBackend", "LocalFileBackend"]
@@ -0,0 +1,66 @@
1
+ """Local file system backend."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ import aiofiles
7
+
8
+
9
+ class LocalFileBackend:
10
+ """Local file system backend."""
11
+
12
+ def __init__(self, base_path: str | None = None):
13
+ """Initialize with optional base path for relative paths."""
14
+ self.base_path = Path(base_path) if base_path else None
15
+
16
+ def _resolve(self, path: str) -> Path:
17
+ """Resolve path (handle relative paths if base_path set)."""
18
+ p = Path(path)
19
+ if not p.is_absolute() and self.base_path:
20
+ return self.base_path / p
21
+ return p
22
+
23
+ async def read(self, path: str) -> str:
24
+ """Read file content."""
25
+ async with aiofiles.open(self._resolve(path), "r", encoding="utf-8") as f:
26
+ return await f.read()
27
+
28
+ async def write(self, path: str, content: str) -> None:
29
+ """Write content to file."""
30
+ p = self._resolve(path)
31
+ p.parent.mkdir(parents=True, exist_ok=True)
32
+ async with aiofiles.open(p, "w", encoding="utf-8") as f:
33
+ await f.write(content)
34
+
35
+ async def append(self, path: str, content: str) -> None:
36
+ """Append content to file."""
37
+ p = self._resolve(path)
38
+ p.parent.mkdir(parents=True, exist_ok=True)
39
+ async with aiofiles.open(p, "a", encoding="utf-8") as f:
40
+ await f.write(content)
41
+
42
+ async def delete(self, path: str) -> bool:
43
+ """Delete file."""
44
+ p = self._resolve(path)
45
+ if p.exists():
46
+ p.unlink()
47
+ return True
48
+ return False
49
+
50
+ async def exists(self, path: str) -> bool:
51
+ """Check if file exists."""
52
+ return self._resolve(path).exists()
53
+
54
+ async def list(self, path: str, pattern: str = "*") -> list[str]:
55
+ """List files matching pattern."""
56
+ p = self._resolve(path)
57
+ if not p.exists():
58
+ return []
59
+ return [str(f) for f in p.glob(pattern) if f.is_file()]
60
+
61
+ async def mkdir(self, path: str, parents: bool = True) -> None:
62
+ """Create directory."""
63
+ self._resolve(path).mkdir(parents=parents, exist_ok=True)
64
+
65
+
66
+ __all__ = ["LocalFileBackend"]
@@ -0,0 +1,40 @@
1
+ """File backend types and protocols."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Protocol, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class FileBackend(Protocol):
9
+ """Protocol for file system operations."""
10
+
11
+ async def read(self, path: str) -> str:
12
+ """Read file content."""
13
+ ...
14
+
15
+ async def write(self, path: str, content: str) -> None:
16
+ """Write content to file."""
17
+ ...
18
+
19
+ async def append(self, path: str, content: str) -> None:
20
+ """Append content to file."""
21
+ ...
22
+
23
+ async def delete(self, path: str) -> bool:
24
+ """Delete file. Returns True if deleted."""
25
+ ...
26
+
27
+ async def exists(self, path: str) -> bool:
28
+ """Check if file exists."""
29
+ ...
30
+
31
+ async def list(self, path: str, pattern: str = "*") -> list[str]:
32
+ """List files in directory matching pattern."""
33
+ ...
34
+
35
+ async def mkdir(self, path: str, parents: bool = True) -> None:
36
+ """Create directory."""
37
+ ...
38
+
39
+
40
+ __all__ = ["FileBackend"]
@@ -0,0 +1,8 @@
1
+ """Invocation backend."""
2
+ from .types import InvocationBackend
3
+ from .memory import InMemoryInvocationBackend
4
+
5
+ __all__ = [
6
+ "InvocationBackend",
7
+ "InMemoryInvocationBackend",
8
+ ]
@@ -0,0 +1,81 @@
1
+ """In-memory invocation backend implementation."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ class InMemoryInvocationBackend:
9
+ """In-memory implementation of InvocationBackend.
10
+
11
+ Suitable for testing and simple single-process use cases.
12
+ """
13
+
14
+ def __init__(self) -> None:
15
+ self._invocations: dict[str, dict[str, Any]] = {}
16
+ self._session_invocations: dict[str, list[str]] = {}
17
+
18
+ async def create(
19
+ self,
20
+ id: str,
21
+ session_id: str,
22
+ data: dict[str, Any],
23
+ agent_id: str | None = None,
24
+ ) -> None:
25
+ """Create a new invocation."""
26
+ invocation_data = {
27
+ "id": id,
28
+ "session_id": session_id,
29
+ "agent_id": agent_id,
30
+ "created_at": datetime.now().isoformat(),
31
+ "updated_at": datetime.now().isoformat(),
32
+ **data,
33
+ }
34
+ self._invocations[id] = invocation_data
35
+
36
+ if session_id not in self._session_invocations:
37
+ self._session_invocations[session_id] = []
38
+ self._session_invocations[session_id].append(id)
39
+
40
+ async def get(self, id: str) -> dict[str, Any] | None:
41
+ """Get invocation by ID."""
42
+ return self._invocations.get(id)
43
+
44
+ async def update(self, id: str, data: dict[str, Any]) -> None:
45
+ """Update invocation data."""
46
+ if id in self._invocations:
47
+ self._invocations[id].update(data)
48
+ self._invocations[id]["updated_at"] = datetime.now().isoformat()
49
+
50
+ async def delete(self, id: str) -> bool:
51
+ """Delete an invocation."""
52
+ if id not in self._invocations:
53
+ return False
54
+
55
+ inv = self._invocations.pop(id)
56
+ session_id = inv.get("session_id")
57
+ if session_id and session_id in self._session_invocations:
58
+ self._session_invocations[session_id] = [
59
+ iid for iid in self._session_invocations[session_id] if iid != id
60
+ ]
61
+ return True
62
+
63
+ async def list_by_session(
64
+ self,
65
+ session_id: str,
66
+ limit: int = 100,
67
+ offset: int = 0,
68
+ ) -> list[dict[str, Any]]:
69
+ """List invocations for a session."""
70
+ inv_ids = self._session_invocations.get(session_id, [])
71
+ invocations = [self._invocations[iid] for iid in inv_ids if iid in self._invocations]
72
+ invocations.sort(key=lambda x: x.get("created_at", ""), reverse=True)
73
+ return invocations[offset:offset + limit]
74
+
75
+ async def get_latest(self, session_id: str) -> dict[str, Any] | None:
76
+ """Get the latest invocation for a session."""
77
+ invocations = await self.list_by_session(session_id, limit=1)
78
+ return invocations[0] if invocations else None
79
+
80
+
81
+ __all__ = ["InMemoryInvocationBackend"]
@@ -0,0 +1,110 @@
1
+ """Invocation backend types and protocols."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Protocol, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class InvocationBackend(Protocol):
9
+ """Protocol for invocation management.
10
+
11
+ An invocation represents a single user turn / agent execution cycle.
12
+ Each session can contain multiple invocations.
13
+
14
+ Example usage:
15
+ # Create invocation
16
+ await backend.create(
17
+ id="inv_123",
18
+ session_id="sess_456",
19
+ data={"state": "running", "agent_id": "react_agent"},
20
+ )
21
+
22
+ # Get invocation
23
+ inv = await backend.get("inv_123")
24
+
25
+ # Update state
26
+ await backend.update("inv_123", {"state": "completed"})
27
+
28
+ # List by session
29
+ invocations = await backend.list_by_session("sess_456")
30
+ """
31
+
32
+ async def create(
33
+ self,
34
+ id: str,
35
+ session_id: str,
36
+ data: dict[str, Any],
37
+ agent_id: str | None = None,
38
+ ) -> None:
39
+ """Create a new invocation.
40
+
41
+ Args:
42
+ id: Invocation ID
43
+ session_id: Parent session ID
44
+ data: Invocation data (state, metadata, etc.)
45
+ agent_id: Optional agent ID
46
+ """
47
+ ...
48
+
49
+ async def get(self, id: str) -> dict[str, Any] | None:
50
+ """Get invocation by ID.
51
+
52
+ Args:
53
+ id: Invocation ID
54
+
55
+ Returns:
56
+ Invocation data dict or None if not found
57
+ """
58
+ ...
59
+
60
+ async def update(self, id: str, data: dict[str, Any]) -> None:
61
+ """Update invocation data.
62
+
63
+ Args:
64
+ id: Invocation ID
65
+ data: Fields to update (partial update)
66
+ """
67
+ ...
68
+
69
+ async def delete(self, id: str) -> bool:
70
+ """Delete an invocation.
71
+
72
+ Args:
73
+ id: Invocation ID
74
+
75
+ Returns:
76
+ True if deleted, False if not found
77
+ """
78
+ ...
79
+
80
+ async def list_by_session(
81
+ self,
82
+ session_id: str,
83
+ limit: int = 100,
84
+ offset: int = 0,
85
+ ) -> list[dict[str, Any]]:
86
+ """List invocations for a session.
87
+
88
+ Args:
89
+ session_id: Session ID to filter by
90
+ limit: Max invocations to return
91
+ offset: Offset for pagination
92
+
93
+ Returns:
94
+ List of invocation data dicts, ordered by created_at desc
95
+ """
96
+ ...
97
+
98
+ async def get_latest(self, session_id: str) -> dict[str, Any] | None:
99
+ """Get the latest invocation for a session.
100
+
101
+ Args:
102
+ session_id: Session ID
103
+
104
+ Returns:
105
+ Latest invocation data or None
106
+ """
107
+ ...
108
+
109
+
110
+ __all__ = ["InvocationBackend"]
@@ -0,0 +1,8 @@
1
+ """Memory backend."""
2
+ from .types import MemoryBackend
3
+ from .memory import InMemoryMemoryBackend
4
+
5
+ __all__ = [
6
+ "MemoryBackend",
7
+ "InMemoryMemoryBackend",
8
+ ]
@@ -0,0 +1,179 @@
1
+ """In-memory memory backend implementation."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import uuid
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+
10
+ class InMemoryMemoryBackend:
11
+ """In-memory implementation of MemoryBackend.
12
+
13
+ Uses simple keyword matching for search.
14
+ Suitable for testing and simple use cases.
15
+ For production, use vector database backends.
16
+ """
17
+
18
+ def __init__(self) -> None:
19
+ self._entries: dict[str, dict[str, Any]] = {}
20
+ # Index: session_id -> list of entry_ids
21
+ self._session_entries: dict[str, list[str]] = {}
22
+ # Content hash for deduplication
23
+ self._content_hashes: dict[str, str] = {}
24
+
25
+ def _make_key(self, session_id: str, namespace: str | None) -> str:
26
+ if namespace:
27
+ return f"{session_id}:{namespace}"
28
+ return session_id
29
+
30
+ def _content_hash(self, content: str) -> str:
31
+ return hashlib.sha256(content.encode()).hexdigest()[:16]
32
+
33
+ async def add(
34
+ self,
35
+ session_id: str,
36
+ content: str,
37
+ invocation_id: str | None = None,
38
+ namespace: str | None = None,
39
+ metadata: dict[str, Any] | None = None,
40
+ ) -> str:
41
+ """Add a memory entry with deduplication."""
42
+ content_hash = self._content_hash(content)
43
+ key = self._make_key(session_id, namespace)
44
+
45
+ # Check for duplicate
46
+ hash_key = f"{key}:{content_hash}"
47
+ if hash_key in self._content_hashes:
48
+ return self._content_hashes[hash_key]
49
+
50
+ entry_id = f"mem_{uuid.uuid4().hex[:12]}"
51
+ entry = {
52
+ "id": entry_id,
53
+ "session_id": session_id,
54
+ "content": content,
55
+ "invocation_id": invocation_id,
56
+ "namespace": namespace,
57
+ "metadata": metadata or {},
58
+ "created_at": datetime.now().isoformat(),
59
+ }
60
+
61
+ self._entries[entry_id] = entry
62
+ self._content_hashes[hash_key] = entry_id
63
+
64
+ if key not in self._session_entries:
65
+ self._session_entries[key] = []
66
+ self._session_entries[key].append(entry_id)
67
+
68
+ return entry_id
69
+
70
+ async def search(
71
+ self,
72
+ session_id: str,
73
+ query: str,
74
+ namespace: str | None = None,
75
+ limit: int = 10,
76
+ ) -> list[dict[str, Any]]:
77
+ """Simple keyword search."""
78
+ key = self._make_key(session_id, namespace)
79
+ entry_ids = self._session_entries.get(key, [])
80
+
81
+ results = []
82
+ query_lower = query.lower()
83
+ query_words = set(query_lower.split())
84
+
85
+ for entry_id in entry_ids:
86
+ entry = self._entries.get(entry_id)
87
+ if not entry:
88
+ continue
89
+
90
+ content_lower = entry["content"].lower()
91
+ content_words = set(content_lower.split())
92
+
93
+ # Calculate simple relevance score
94
+ overlap = len(query_words & content_words)
95
+ if overlap > 0:
96
+ score = overlap / len(query_words)
97
+ elif query_lower in content_lower:
98
+ score = 0.5
99
+ else:
100
+ continue
101
+
102
+ results.append({
103
+ "id": entry["id"],
104
+ "content": entry["content"],
105
+ "score": score,
106
+ "metadata": entry["metadata"],
107
+ "invocation_id": entry.get("invocation_id"),
108
+ })
109
+
110
+ results.sort(key=lambda x: x["score"], reverse=True)
111
+ return results[:limit]
112
+
113
+ async def get(self, id: str) -> dict[str, Any] | None:
114
+ """Get memory by ID."""
115
+ return self._entries.get(id)
116
+
117
+ async def delete(self, id: str) -> bool:
118
+ """Delete a memory entry."""
119
+ if id not in self._entries:
120
+ return False
121
+
122
+ entry = self._entries.pop(id)
123
+ session_id = entry["session_id"]
124
+ namespace = entry.get("namespace")
125
+ key = self._make_key(session_id, namespace)
126
+
127
+ if key in self._session_entries:
128
+ self._session_entries[key] = [
129
+ eid for eid in self._session_entries[key] if eid != id
130
+ ]
131
+
132
+ # Remove from content hash
133
+ content_hash = self._content_hash(entry["content"])
134
+ hash_key = f"{key}:{content_hash}"
135
+ self._content_hashes.pop(hash_key, None)
136
+
137
+ return True
138
+
139
+ async def delete_by_invocation(
140
+ self,
141
+ session_id: str,
142
+ invocation_id: str,
143
+ namespace: str | None = None,
144
+ ) -> int:
145
+ """Delete memories by invocation."""
146
+ key = self._make_key(session_id, namespace)
147
+ entry_ids = self._session_entries.get(key, [])
148
+
149
+ to_delete = [
150
+ eid for eid in entry_ids
151
+ if self._entries.get(eid, {}).get("invocation_id") == invocation_id
152
+ ]
153
+
154
+ for eid in to_delete:
155
+ await self.delete(eid)
156
+
157
+ return len(to_delete)
158
+
159
+ async def list(
160
+ self,
161
+ session_id: str,
162
+ namespace: str | None = None,
163
+ limit: int = 100,
164
+ ) -> list[dict[str, Any]]:
165
+ """List memories for a session."""
166
+ key = self._make_key(session_id, namespace)
167
+ entry_ids = self._session_entries.get(key, [])
168
+
169
+ entries = [
170
+ self._entries[eid]
171
+ for eid in entry_ids
172
+ if eid in self._entries
173
+ ]
174
+
175
+ entries.sort(key=lambda x: x.get("created_at", ""), reverse=True)
176
+ return entries[:limit]
177
+
178
+
179
+ __all__ = ["InMemoryMemoryBackend"]