aimemoryos 0.1.1__tar.gz

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 (66) hide show
  1. aimemoryos-0.1.1/PKG-INFO +159 -0
  2. aimemoryos-0.1.1/README.md +136 -0
  3. aimemoryos-0.1.1/aimemoryos/__init__.py +3 -0
  4. aimemoryos-0.1.1/aimemoryos/agents/__init__.py +7 -0
  5. aimemoryos-0.1.1/aimemoryos/agents/context_builder.py +171 -0
  6. aimemoryos-0.1.1/aimemoryos/agents/memory_client.py +438 -0
  7. aimemoryos-0.1.1/aimemoryos/agents/session_manager.py +273 -0
  8. aimemoryos-0.1.1/aimemoryos/core/__init__.py +1 -0
  9. aimemoryos-0.1.1/aimemoryos/core/config.py +39 -0
  10. aimemoryos-0.1.1/aimemoryos/core/exceptions.py +95 -0
  11. aimemoryos-0.1.1/aimemoryos/core/interfaces.py +287 -0
  12. aimemoryos-0.1.1/aimemoryos/core/runtime.py +125 -0
  13. aimemoryos-0.1.1/aimemoryos/graph/__init__.py +2 -0
  14. aimemoryos-0.1.1/aimemoryos/graph/contradiction_detector.py +519 -0
  15. aimemoryos-0.1.1/aimemoryos/graph/ontology.py +477 -0
  16. aimemoryos-0.1.1/aimemoryos/graph/traversal.py +214 -0
  17. aimemoryos-0.1.1/aimemoryos/ingestion/__init__.py +2 -0
  18. aimemoryos-0.1.1/aimemoryos/ingestion/chunker.py +286 -0
  19. aimemoryos-0.1.1/aimemoryos/ingestion/deduplicator.py +134 -0
  20. aimemoryos-0.1.1/aimemoryos/ingestion/multimodal_router.py +26 -0
  21. aimemoryos-0.1.1/aimemoryos/ingestion/pdf_loader.py +31 -0
  22. aimemoryos-0.1.1/aimemoryos/ingestion/pipeline.py +255 -0
  23. aimemoryos-0.1.1/aimemoryos/ingestion/preprocessor.py +24 -0
  24. aimemoryos-0.1.1/aimemoryos/main.py +87 -0
  25. aimemoryos-0.1.1/aimemoryos/memory/episodic.py +36 -0
  26. aimemoryos-0.1.1/aimemoryos/memory/importance.py +187 -0
  27. aimemoryos-0.1.1/aimemoryos/memory/models.py +384 -0
  28. aimemoryos-0.1.1/aimemoryos/memory/procedural.py +60 -0
  29. aimemoryos-0.1.1/aimemoryos/memory/semantic.py +67 -0
  30. aimemoryos-0.1.1/aimemoryos/memory/working.py +44 -0
  31. aimemoryos-0.1.1/aimemoryos/retrieval/__init__.py +2 -0
  32. aimemoryos-0.1.1/aimemoryos/retrieval/context_assembler.py +309 -0
  33. aimemoryos-0.1.1/aimemoryos/retrieval/engine.py +494 -0
  34. aimemoryos-0.1.1/aimemoryos/retrieval/graph_retriever.py +284 -0
  35. aimemoryos-0.1.1/aimemoryos/retrieval/spreading_activation.py +41 -0
  36. aimemoryos-0.1.1/aimemoryos/retrieval/vector_retriever.py +179 -0
  37. aimemoryos-0.1.1/aimemoryos/storage/__init__.py +2 -0
  38. aimemoryos-0.1.1/aimemoryos/storage/duckdb_store.py +1125 -0
  39. aimemoryos-0.1.1/aimemoryos/storage/faiss_store.py +372 -0
  40. aimemoryos-0.1.1/aimemoryos/storage/hallucination_firewall.py +79 -0
  41. aimemoryos-0.1.1/aimemoryos/storage/orchestrator.py +511 -0
  42. aimemoryos-0.1.1/aimemoryos/storage/sqlite_log.py +967 -0
  43. aimemoryos-0.1.1/aimemoryos/utils/__init__.py +1 -0
  44. aimemoryos-0.1.1/aimemoryos/utils/hashing.py +147 -0
  45. aimemoryos-0.1.1/aimemoryos/utils/logger.py +210 -0
  46. aimemoryos-0.1.1/aimemoryos/utils/metrics.py +453 -0
  47. aimemoryos-0.1.1/aimemoryos/utils/scoring.py +247 -0
  48. aimemoryos-0.1.1/aimemoryos/vector/__init__.py +2 -0
  49. aimemoryos-0.1.1/aimemoryos/vector/cache_embedder.py +271 -0
  50. aimemoryos-0.1.1/aimemoryos/vector/embedder.py +115 -0
  51. aimemoryos-0.1.1/aimemoryos/vector/index_manager.py +465 -0
  52. aimemoryos-0.1.1/aimemoryos.egg-info/PKG-INFO +159 -0
  53. aimemoryos-0.1.1/aimemoryos.egg-info/SOURCES.txt +64 -0
  54. aimemoryos-0.1.1/aimemoryos.egg-info/dependency_links.txt +1 -0
  55. aimemoryos-0.1.1/aimemoryos.egg-info/requires.txt +16 -0
  56. aimemoryos-0.1.1/aimemoryos.egg-info/top_level.txt +1 -0
  57. aimemoryos-0.1.1/pyproject.toml +31 -0
  58. aimemoryos-0.1.1/setup.cfg +4 -0
  59. aimemoryos-0.1.1/tests/test_detach_delete.py +9 -0
  60. aimemoryos-0.1.1/tests/test_firewall.py +76 -0
  61. aimemoryos-0.1.1/tests/test_forget_snapshot.py +33 -0
  62. aimemoryos-0.1.1/tests/test_index_manager_persistence.py +68 -0
  63. aimemoryos-0.1.1/tests/test_kuzu.py +20 -0
  64. aimemoryos-0.1.1/tests/test_kuzu2.py +18 -0
  65. aimemoryos-0.1.1/tests/test_memory_client.py +110 -0
  66. aimemoryos-0.1.1/tests/test_utils_modules.py +120 -0
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: aimemoryos
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: asyncio>=4.0.0
8
+ Requires-Dist: duckdb>=1.5.3
9
+ Requires-Dist: faiss-cpu>=1.14.2
10
+ Requires-Dist: google>=3.0.0
11
+ Requires-Dist: kuzu>=0.11.3
12
+ Requires-Dist: loguru>=0.7.3
13
+ Requires-Dist: numpy>=2.4.6
14
+ Requires-Dist: openrouter>=0.9.1
15
+ Requires-Dist: orjson>=3.11.9
16
+ Requires-Dist: pydantic>=2.13.4
17
+ Requires-Dist: pydantic-settings>=2.14.1
18
+ Requires-Dist: pymupdf>=1.27.2.3
19
+ Requires-Dist: pytest>=9.0.3
20
+ Requires-Dist: sentence-transformers>=5.5.1
21
+ Requires-Dist: spacy>=3.8.14
22
+ Requires-Dist: structlog>=25.5.0
23
+
24
+ # AIMemoryOS
25
+
26
+ A powerful, modular, multi-store memory layer for conversational agents and LLM applications.
27
+
28
+ AIMemoryOS combines vector retrieval, graph knowledge bases, and highly-durable structured storage to give your AI agents a robust, scalable long-term memory that you can plug and play directly into your own applications.
29
+
30
+ ## Features
31
+
32
+ - **Multi-Store Architecture**: Coordinated state across SQLite (append-only events), DuckDB (canonical relational state), FAISS (vector similarity), and KuzuDB (graph relationships).
33
+ - **Advanced Ingestion**: End-to-end pipeline with preprocessing, PII stripping, SHA-256 deduplication, and semantic chunking.
34
+ - **Rich Retrieval**: Context assembly from multiple sources, reranking, and trace scoring.
35
+ - **Coordinated Pruning**: Safely `forget()` memories with guaranteed cleanup across all storage backends without violating relational integrity.
36
+ - **Snapshotting**: Take portable, verified SQLite backups of your system's event history instantly.
37
+ - **Hallucination Firewall**: Isolate "hypothesized" or "imagined" memories to separate tables so they don't pollute your agent's ground-truth recall.
38
+
39
+ ---
40
+
41
+ ## Installation
42
+
43
+ > [!IMPORTANT]
44
+ > **Python Version Requirement**: AIMemoryOS depends on heavy AI libraries (like SpaCy, NumPy, and PyTorch) that require pre-compiled C++ binaries. You **must** use a stable Python version (**Python 3.10, 3.11, or 3.12**) to install this package successfully. Pre-release or cutting-edge versions (like Python 3.13+) will fail to compile.
45
+
46
+ AIMemoryOS relies on powerful machine learning models under the hood. Make sure you have at least 2GB of free disk space before installing, as it will download heavy dependencies like PyTorch, FAISS, and HuggingFace Transformers.
47
+
48
+ ### Step-by-step Installation
49
+
50
+ **Step 1: Create a safe virtual environment**
51
+ We highly recommend using a virtual environment strictly locked to a stable Python version. We suggest using [uv](https://docs.astral.sh/uv/) for incredibly fast and foolproof environment isolation:
52
+ ```bash
53
+ # Create a Python 3.12 virtual environment
54
+ uv venv --python 3.12
55
+ ```
56
+
57
+ **Step 2: Activate the environment**
58
+ - **Windows (PowerShell)**: `.\.venv\Scripts\activate`
59
+ - **Mac/Linux**: `source .venv/bin/activate`
60
+
61
+ **Step 3: Install AIMemoryOS**
62
+ Install the SDK directly from PyPI into your isolated environment:
63
+ ```bash
64
+ uv pip install aimemoryos
65
+ ```
66
+ *(If you are using standard `pip`, simply run `python -m pip install aimemoryos` instead).*
67
+
68
+ **Step 4: Download the SpaCy language model weights**
69
+ The `aimemoryos` package will automatically install the `spacy` library for you. However, you must explicitly download the actual **English NLP model weights** (which pip cannot bundle) used for entity extraction:
70
+
71
+ ```bash
72
+ # If using uv:
73
+ uv run python -m spacy download en_core_web_sm
74
+
75
+ # If using standard python:
76
+ python -m spacy download en_core_web_sm
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Quickstart (Plug & Play)
82
+
83
+ The easiest way to integrate AIMemoryOS into your codebase is by using the `Memory` facade. Simply import it into your application and start saving/retrieving knowledge.
84
+
85
+ ```python
86
+ import asyncio
87
+ from pathlib import Path
88
+ from aimemoryos import Memory
89
+
90
+ async def run():
91
+ # Initialize the high-level memory facade
92
+ memory = Memory()
93
+
94
+ # 1. Ingest information into the memory pipeline
95
+ print("Ingesting memory...")
96
+ await memory.save(
97
+ document_id="doc_001",
98
+ content="My favorite programming language is Python and I love pizza."
99
+ )
100
+
101
+ # 2. Retrieve relevant context based on semantic similarity
102
+ print("Retrieving context...")
103
+ results = await memory.retrieve(
104
+ query="What foods do I like?",
105
+ top_k=5,
106
+ score_threshold=0.2
107
+ )
108
+
109
+ # Results include the content, metadata, and relevance score
110
+ for r in results:
111
+ print(f"[{r['score']:.2f}] {r['metadata']['content']}")
112
+
113
+ # 3. Forget a memory across all stores (DuckDB, FAISS, Graph, SQLite)
114
+ if results:
115
+ memory_id = results[0]["metadata"]["memory_id"]
116
+ await memory.forget(memory_id=memory_id)
117
+
118
+ # 4. Take a verifiable snapshot of the entire event history
119
+ await memory.snapshot(output_path=Path("backups/memory_snapshot.sqlite"))
120
+
121
+ if __name__ == "__main__":
122
+ asyncio.run(run())
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Configuration
128
+
129
+ By default, runtime databases and indexes are stored in the `.aimemoryos/` folder at the root of the installed package. You can easily override this globally if you want your data saved elsewhere:
130
+
131
+ ```bash
132
+ export AIMEMORYOS_DATA_DIR="/path/to/custom/data"
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Architecture / Project Layout
138
+
139
+ For those interested in extending the SDK or contributing, the folder architecture is maintained as follows:
140
+
141
+ - `agents/`: SDK-facing components. Contains `MemoryClient`, which is the primary unified interface to the ecosystem.
142
+ - `main.py`: A lightweight, backwards-compatible wrapper around `MemoryClient` for immediate plug-and-play.
143
+ - `core/runtime.py`: The composition root. Handles dependency injection, configuration, and instantiating storage/retrieval services.
144
+ - `ingestion/`: Text preprocessing, SHA deduplication, routing, and chunking boundaries.
145
+ - `retrieval/`: Similarity search, vector reranking, and `ContextBuilder` logic.
146
+ - `storage/`: Highly durable persistence backends (`duckdb_store.py`, `faiss_store.py`, `sqlite_log.py`) and the `orchestrator.py`.
147
+ - `vector/`: Embedding generation (`sentence-transformers`) and model management.
148
+ - `graph/`: KuzuDB ontology and schema definitions.
149
+
150
+ ## Contributing
151
+
152
+ We welcome contributions! To set up for local development:
153
+
154
+ ```bash
155
+ git clone https://github.com/your-org/aimemoryos.git
156
+ cd aimemoryos
157
+ pip install -e .
158
+ pytest -q
159
+ ```
@@ -0,0 +1,136 @@
1
+ # AIMemoryOS
2
+
3
+ A powerful, modular, multi-store memory layer for conversational agents and LLM applications.
4
+
5
+ AIMemoryOS combines vector retrieval, graph knowledge bases, and highly-durable structured storage to give your AI agents a robust, scalable long-term memory that you can plug and play directly into your own applications.
6
+
7
+ ## Features
8
+
9
+ - **Multi-Store Architecture**: Coordinated state across SQLite (append-only events), DuckDB (canonical relational state), FAISS (vector similarity), and KuzuDB (graph relationships).
10
+ - **Advanced Ingestion**: End-to-end pipeline with preprocessing, PII stripping, SHA-256 deduplication, and semantic chunking.
11
+ - **Rich Retrieval**: Context assembly from multiple sources, reranking, and trace scoring.
12
+ - **Coordinated Pruning**: Safely `forget()` memories with guaranteed cleanup across all storage backends without violating relational integrity.
13
+ - **Snapshotting**: Take portable, verified SQLite backups of your system's event history instantly.
14
+ - **Hallucination Firewall**: Isolate "hypothesized" or "imagined" memories to separate tables so they don't pollute your agent's ground-truth recall.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ > [!IMPORTANT]
21
+ > **Python Version Requirement**: AIMemoryOS depends on heavy AI libraries (like SpaCy, NumPy, and PyTorch) that require pre-compiled C++ binaries. You **must** use a stable Python version (**Python 3.10, 3.11, or 3.12**) to install this package successfully. Pre-release or cutting-edge versions (like Python 3.13+) will fail to compile.
22
+
23
+ AIMemoryOS relies on powerful machine learning models under the hood. Make sure you have at least 2GB of free disk space before installing, as it will download heavy dependencies like PyTorch, FAISS, and HuggingFace Transformers.
24
+
25
+ ### Step-by-step Installation
26
+
27
+ **Step 1: Create a safe virtual environment**
28
+ We highly recommend using a virtual environment strictly locked to a stable Python version. We suggest using [uv](https://docs.astral.sh/uv/) for incredibly fast and foolproof environment isolation:
29
+ ```bash
30
+ # Create a Python 3.12 virtual environment
31
+ uv venv --python 3.12
32
+ ```
33
+
34
+ **Step 2: Activate the environment**
35
+ - **Windows (PowerShell)**: `.\.venv\Scripts\activate`
36
+ - **Mac/Linux**: `source .venv/bin/activate`
37
+
38
+ **Step 3: Install AIMemoryOS**
39
+ Install the SDK directly from PyPI into your isolated environment:
40
+ ```bash
41
+ uv pip install aimemoryos
42
+ ```
43
+ *(If you are using standard `pip`, simply run `python -m pip install aimemoryos` instead).*
44
+
45
+ **Step 4: Download the SpaCy language model weights**
46
+ The `aimemoryos` package will automatically install the `spacy` library for you. However, you must explicitly download the actual **English NLP model weights** (which pip cannot bundle) used for entity extraction:
47
+
48
+ ```bash
49
+ # If using uv:
50
+ uv run python -m spacy download en_core_web_sm
51
+
52
+ # If using standard python:
53
+ python -m spacy download en_core_web_sm
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Quickstart (Plug & Play)
59
+
60
+ The easiest way to integrate AIMemoryOS into your codebase is by using the `Memory` facade. Simply import it into your application and start saving/retrieving knowledge.
61
+
62
+ ```python
63
+ import asyncio
64
+ from pathlib import Path
65
+ from aimemoryos import Memory
66
+
67
+ async def run():
68
+ # Initialize the high-level memory facade
69
+ memory = Memory()
70
+
71
+ # 1. Ingest information into the memory pipeline
72
+ print("Ingesting memory...")
73
+ await memory.save(
74
+ document_id="doc_001",
75
+ content="My favorite programming language is Python and I love pizza."
76
+ )
77
+
78
+ # 2. Retrieve relevant context based on semantic similarity
79
+ print("Retrieving context...")
80
+ results = await memory.retrieve(
81
+ query="What foods do I like?",
82
+ top_k=5,
83
+ score_threshold=0.2
84
+ )
85
+
86
+ # Results include the content, metadata, and relevance score
87
+ for r in results:
88
+ print(f"[{r['score']:.2f}] {r['metadata']['content']}")
89
+
90
+ # 3. Forget a memory across all stores (DuckDB, FAISS, Graph, SQLite)
91
+ if results:
92
+ memory_id = results[0]["metadata"]["memory_id"]
93
+ await memory.forget(memory_id=memory_id)
94
+
95
+ # 4. Take a verifiable snapshot of the entire event history
96
+ await memory.snapshot(output_path=Path("backups/memory_snapshot.sqlite"))
97
+
98
+ if __name__ == "__main__":
99
+ asyncio.run(run())
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Configuration
105
+
106
+ By default, runtime databases and indexes are stored in the `.aimemoryos/` folder at the root of the installed package. You can easily override this globally if you want your data saved elsewhere:
107
+
108
+ ```bash
109
+ export AIMEMORYOS_DATA_DIR="/path/to/custom/data"
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Architecture / Project Layout
115
+
116
+ For those interested in extending the SDK or contributing, the folder architecture is maintained as follows:
117
+
118
+ - `agents/`: SDK-facing components. Contains `MemoryClient`, which is the primary unified interface to the ecosystem.
119
+ - `main.py`: A lightweight, backwards-compatible wrapper around `MemoryClient` for immediate plug-and-play.
120
+ - `core/runtime.py`: The composition root. Handles dependency injection, configuration, and instantiating storage/retrieval services.
121
+ - `ingestion/`: Text preprocessing, SHA deduplication, routing, and chunking boundaries.
122
+ - `retrieval/`: Similarity search, vector reranking, and `ContextBuilder` logic.
123
+ - `storage/`: Highly durable persistence backends (`duckdb_store.py`, `faiss_store.py`, `sqlite_log.py`) and the `orchestrator.py`.
124
+ - `vector/`: Embedding generation (`sentence-transformers`) and model management.
125
+ - `graph/`: KuzuDB ontology and schema definitions.
126
+
127
+ ## Contributing
128
+
129
+ We welcome contributions! To set up for local development:
130
+
131
+ ```bash
132
+ git clone https://github.com/your-org/aimemoryos.git
133
+ cd aimemoryos
134
+ pip install -e .
135
+ pytest -q
136
+ ```
@@ -0,0 +1,3 @@
1
+ from aimemoryos.main import Memory
2
+
3
+ __all__ = ['Memory']
@@ -0,0 +1,7 @@
1
+ """Agents package public exports."""
2
+
3
+ __all__ = [
4
+ "memory_client",
5
+ "context_builder",
6
+ "session_manager",
7
+ ]
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Iterable
5
+
6
+ from aimemoryos.retrieval.context_assembler import ContextBlock
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Defaults
10
+ # ---------------------------------------------------------------------------
11
+
12
+ DEFAULT_HEADER: str = "Relevant Memory Context"
13
+ DEFAULT_SEPARATOR: str = "\n\n"
14
+ DEFAULT_MAX_MEMORIES: int = 10
15
+
16
+ _CHARS_PER_TOKEN: int = 4
17
+
18
+ @dataclass(slots=True)
19
+ class BuiltContext:
20
+ """
21
+ Final prompt-ready context payload returned by ContextBuilder.
22
+
23
+ Fields
24
+ ------
25
+ text : the assembled string ready for LLM injection
26
+ memory_count : number of memories included (≤ max_memories)
27
+ truncated : True when source list exceeded max_memories
28
+ estimated_tokens : rough token count (len(text) // 4, min 1)
29
+ """
30
+
31
+ text: str
32
+ memory_count: int
33
+ truncated: bool
34
+ estimated_tokens: int
35
+
36
+
37
+ class ContextBuilder:
38
+ """
39
+ Prompt-oriented context assembler.
40
+
41
+ Responsibilities
42
+ ----------------
43
+ - ContextBlock → prompt string formatting
44
+ - Memory ordering preservation (caller supplies ranked order)
45
+ - Optional score / metadata annotation per memory block
46
+ - Token budget estimation via char-count heuristic
47
+ - Truncation to max_memories
48
+
49
+ Does NOT:
50
+ - retrieve memories
51
+ - rerank memories
52
+ - access any storage backend
53
+ - mutate memories
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ max_memories: int = DEFAULT_MAX_MEMORIES,
60
+ separator: str = DEFAULT_SEPARATOR,
61
+ include_metadata: bool = False,
62
+ include_scores: bool = False,
63
+ ) -> None:
64
+ self.max_memories = max_memories
65
+ self.separator = separator
66
+ self.include_metadata = include_metadata
67
+ self.include_scores = include_scores
68
+
69
+ # ------------------------------------------------------------------
70
+ # Primary build path
71
+ # ------------------------------------------------------------------
72
+
73
+ def build(self, context: ContextBlock) -> BuiltContext:
74
+ """
75
+ Convert a ContextBlock into a prompt-ready BuiltContext.
76
+
77
+ Memory ordering is preserved from context.memories — the retrieval
78
+ engine has already applied RRF fusion and importance weighting.
79
+ Truncation to max_memories is applied here if the list is longer.
80
+
81
+ Each memory is rendered by _render_memory(), which appends optional
82
+ score and metadata lines controlled by include_scores /
83
+ include_metadata. The full output is prefixed with DEFAULT_HEADER.
84
+ """
85
+ selected = context.memories[: self.max_memories]
86
+ truncated = len(context.memories) > self.max_memories
87
+
88
+ sections: list[str] = [DEFAULT_HEADER]
89
+ for index, memory in enumerate(selected, start=1):
90
+ sections.append(self._render_memory(index=index, memory=memory))
91
+
92
+ text = self.separator.join(sections)
93
+ return BuiltContext(
94
+ text=text,
95
+ memory_count=len(selected),
96
+ truncated=truncated,
97
+ estimated_tokens=self._estimate_tokens(text),
98
+ )
99
+
100
+ # ------------------------------------------------------------------
101
+ # Lightweight raw-string path
102
+ # ------------------------------------------------------------------
103
+
104
+ def build_raw(self, memories: Iterable[str]) -> BuiltContext:
105
+ """
106
+ Assemble a BuiltContext from an iterable of pre-formatted strings.
107
+
108
+ Truncates to max_memories. No header is prepended. Useful when the
109
+ caller has already formatted memory strings and only needs truncation
110
+ and token estimation (e.g. unit tests, custom formatting pipelines).
111
+ """
112
+ all_items = list(memories)
113
+ truncated = len(all_items) > self.max_memories
114
+ selected = all_items[: self.max_memories]
115
+ text = self.separator.join(selected)
116
+ return BuiltContext(
117
+ text=text,
118
+ memory_count=len(selected),
119
+ truncated=truncated,
120
+ estimated_tokens=self._estimate_tokens(text),
121
+ )
122
+
123
+ # ------------------------------------------------------------------
124
+ # Memory rendering
125
+ # ------------------------------------------------------------------
126
+
127
+ def _render_memory(self, *, index: int, memory: object) -> str:
128
+ """
129
+ Format a single RetrievalCandidate into a prompt block.
130
+
131
+ Always includes:
132
+ [Memory N]
133
+ <content>
134
+
135
+ Conditionally includes (controlled by constructor flags):
136
+ [score=X.XXXX] — when include_scores=True and trace present
137
+ [metadata={...}] — when include_metadata=True and trace metadata present
138
+ """
139
+ lines: list[str] = []
140
+ lines.append(f"[Memory {index}]")
141
+ lines.append(getattr(memory, "content", ""))
142
+
143
+ if self.include_scores:
144
+ trace = getattr(memory, "trace", None)
145
+ if trace is not None:
146
+ score = getattr(trace, "final_score", None)
147
+ if score is not None:
148
+ lines.append(f"[score={score:.4f}]")
149
+
150
+ if self.include_metadata:
151
+ trace = getattr(memory, "trace", None)
152
+ metadata = getattr(trace, "trace_metadata", None) if trace else None
153
+ if metadata:
154
+ lines.append(f"[metadata={metadata}]")
155
+
156
+ return "\n".join(lines)
157
+
158
+ # ------------------------------------------------------------------
159
+ # Token estimation
160
+ # ------------------------------------------------------------------
161
+
162
+ @staticmethod
163
+ def _estimate_tokens(text: str) -> int:
164
+ """
165
+ Rough token count: 1 token ≈ 4 characters (min 1).
166
+
167
+ This is a heuristic. For budget-critical prompts, run a proper
168
+ tokeniser (e.g. tiktoken) on BuiltContext.text before submission.
169
+ """
170
+ return max(len(text) // _CHARS_PER_TOKEN, 1)
171
+