rememberstack 0.1.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 (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,9 @@
1
+ """RememberStack: open memory infrastructure for AI agents."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version
5
+
6
+ try:
7
+ __version__ = version("rememberstack")
8
+ except PackageNotFoundError: # running from a checkout without installation
9
+ __version__ = "0.0.0+uninstalled"
@@ -0,0 +1,42 @@
1
+ """Provider adapter package."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from rememberstack.adapters.codex_writer import CodexAgentAdapterSettings
6
+ from rememberstack.adapters.codex_writer import CodexCLIAgentAdapter
7
+ from rememberstack.adapters.codex_writer import CodexCLIWriterAdapter
8
+ from rememberstack.adapters.codex_writer import CodexWriterAdapterSettings
9
+ from rememberstack.adapters.openrouter import OpenRouterModelProvider
10
+ from rememberstack.adapters.openrouter import OpenRouterProviderError
11
+ from rememberstack.adapters.openrouter import OpenRouterSettings
12
+
13
+ if TYPE_CHECKING:
14
+ from rememberstack.adapters.markitdown_converter import MARKITDOWN_CONVERTER_VERSION
15
+ from rememberstack.adapters.markitdown_converter import MarkitdownConverter
16
+
17
+ __all__ = (
18
+ "CodexCLIAgentAdapter",
19
+ "CodexCLIWriterAdapter",
20
+ "CodexAgentAdapterSettings",
21
+ "CodexWriterAdapterSettings",
22
+ "MARKITDOWN_CONVERTER_VERSION",
23
+ "MarkitdownConverter",
24
+ "OpenRouterModelProvider",
25
+ "OpenRouterProviderError",
26
+ "OpenRouterSettings",
27
+ )
28
+
29
+
30
+ def __getattr__(name: str) -> object:
31
+ """Load the media converter only for processes that actually compose it."""
32
+ if name == "MARKITDOWN_CONVERTER_VERSION":
33
+ from rememberstack.adapters.markitdown_converter import (
34
+ MARKITDOWN_CONVERTER_VERSION,
35
+ )
36
+
37
+ return MARKITDOWN_CONVERTER_VERSION
38
+ if name == "MarkitdownConverter":
39
+ from rememberstack.adapters.markitdown_converter import MarkitdownConverter
40
+
41
+ return MarkitdownConverter
42
+ raise AttributeError(name)
@@ -0,0 +1,221 @@
1
+ """Sandboxed stock-Codex adapter for one declared-output Plane-K session."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import subprocess
6
+ from tempfile import TemporaryDirectory
7
+
8
+ from pydantic import Field
9
+ from pydantic_settings import BaseSettings
10
+ from pydantic_settings import SettingsConfigDict
11
+
12
+ from rememberstack.model import KnowledgeAgentSessionRequest
13
+ from rememberstack.model import KnowledgeAgentSessionResult
14
+
15
+
16
+ class CodexAgentAdapterSettings(BaseSettings):
17
+ """The executable binding shared by Plane-K stock-harness seats."""
18
+
19
+ model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_K_CODEX_")
20
+
21
+ executable: str = Field(default="codex", min_length=1)
22
+
23
+
24
+ class CodexWriterAdapterSettings(BaseSettings):
25
+ """Backward-compatible writer-specific executable settings."""
26
+
27
+ model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_K_WRITER_CODEX_")
28
+
29
+ executable: str = Field(default="codex", min_length=1)
30
+
31
+
32
+ class CodexCLIAgentAdapter:
33
+ """Run Codex in an ephemeral non-git workspace and accept declared files only."""
34
+
35
+ def __init__(
36
+ self, *, settings: CodexAgentAdapterSettings | CodexWriterAdapterSettings
37
+ ) -> None:
38
+ """Bind the adapter to its settings-owned executable."""
39
+ self._settings = settings
40
+
41
+ def run_session(
42
+ self, *, request: KnowledgeAgentSessionRequest
43
+ ) -> KnowledgeAgentSessionResult:
44
+ """Run one sandboxed session and return raw outputs without accepting them."""
45
+ with TemporaryDirectory(prefix="rememberstack-k-writer-") as temporary:
46
+ workspace = Path(temporary)
47
+ for relative_path, content in request.input_files.items():
48
+ target = _workspace_path(
49
+ workspace=workspace, relative_path=relative_path
50
+ )
51
+ target.parent.mkdir(parents=True, exist_ok=True)
52
+ target.write_text(content, encoding="utf-8")
53
+ for relative_path in request.sandbox.accepted_output_paths:
54
+ _workspace_path(
55
+ workspace=workspace, relative_path=relative_path
56
+ ).parent.mkdir(parents=True, exist_ok=True)
57
+ writer_workdir = workspace / "output"
58
+ last_message = workspace / ".session" / "last-message.txt"
59
+ last_message.parent.mkdir(parents=True, exist_ok=True)
60
+ command = _codex_command(
61
+ executable=self._settings.executable,
62
+ request=request,
63
+ writer_workdir=writer_workdir,
64
+ last_message=last_message,
65
+ )
66
+ try:
67
+ completed = subprocess.run( # noqa: S603 -- fixed argv, no shell
68
+ command,
69
+ input=request.prompt,
70
+ capture_output=True,
71
+ text=True,
72
+ check=False,
73
+ timeout=request.timeout_seconds,
74
+ )
75
+ except subprocess.TimeoutExpired as error:
76
+ stdout = _timeout_text(value=error.stdout)
77
+ stderr = _timeout_text(value=error.stderr)
78
+ return KnowledgeAgentSessionResult(
79
+ session_id=request.session_id,
80
+ exit_code=None,
81
+ timed_out=True,
82
+ output_files=_declared_outputs(
83
+ workspace=workspace, paths=request.sandbox.accepted_output_paths
84
+ ),
85
+ transcript=_process_transcript(
86
+ stdout=stdout, stderr=stderr, exit_code=None, timed_out=True
87
+ ),
88
+ tokens=_token_usage(stdout=stdout),
89
+ )
90
+ return KnowledgeAgentSessionResult(
91
+ session_id=request.session_id,
92
+ exit_code=completed.returncode,
93
+ output_files=_declared_outputs(
94
+ workspace=workspace, paths=request.sandbox.accepted_output_paths
95
+ ),
96
+ transcript=_process_transcript(
97
+ stdout=completed.stdout,
98
+ stderr=completed.stderr,
99
+ exit_code=completed.returncode,
100
+ timed_out=False,
101
+ ),
102
+ tokens=_token_usage(stdout=completed.stdout),
103
+ )
104
+
105
+
106
+ def _codex_command(
107
+ *,
108
+ executable: str,
109
+ request: KnowledgeAgentSessionRequest,
110
+ writer_workdir: Path,
111
+ last_message: Path,
112
+ ) -> list[str]:
113
+ """Build the fixed stock-harness command with no inherited tools or web search."""
114
+ return [
115
+ executable,
116
+ "exec",
117
+ "--strict-config",
118
+ "--ignore-user-config",
119
+ "--ignore-rules",
120
+ "--ephemeral",
121
+ "--skip-git-repo-check",
122
+ "--sandbox",
123
+ "workspace-write",
124
+ "-c",
125
+ "sandbox_workspace_write.network_access=false",
126
+ "-c",
127
+ "shell_environment_policy.inherit=none",
128
+ "-c",
129
+ "sandbox_workspace_write.exclude_tmpdir_env_var=true",
130
+ "-c",
131
+ "sandbox_workspace_write.exclude_slash_tmp=true",
132
+ "--model",
133
+ request.model,
134
+ "--json",
135
+ "--color",
136
+ "never",
137
+ "--output-last-message",
138
+ str(last_message),
139
+ "--cd",
140
+ str(writer_workdir),
141
+ "-",
142
+ ]
143
+
144
+
145
+ def _declared_outputs(*, workspace: Path, paths: tuple[str, ...]) -> dict[str, str]:
146
+ """Read only contract-declared UTF-8 files and discard every other writer edit."""
147
+ outputs: dict[str, str] = {}
148
+ for relative_path in paths:
149
+ try:
150
+ target = _workspace_path(workspace=workspace, relative_path=relative_path)
151
+ if target.is_file():
152
+ outputs[relative_path] = target.read_text(encoding="utf-8")
153
+ except (OSError, UnicodeError, ValueError):
154
+ # The compiler rejects the missing declared output after it archives
155
+ # the process transcript; hostile links and unreadable bytes cannot
156
+ # make a completed session disappear from the ledger.
157
+ continue
158
+ return outputs
159
+
160
+
161
+ def _workspace_path(*, workspace: Path, relative_path: str) -> Path:
162
+ """Resolve a validated relative path without following a workspace escape."""
163
+ target = workspace.joinpath(*relative_path.split("/"))
164
+ if target.is_symlink() or not target.resolve().is_relative_to(workspace.resolve()):
165
+ raise ValueError(f"writer workspace path escapes root: {relative_path}")
166
+ return target
167
+
168
+
169
+ def _timeout_text(*, value: str | bytes | None) -> str:
170
+ """Normalize subprocess timeout fragments without discarding captured bytes."""
171
+ if value is None:
172
+ return ""
173
+ return (
174
+ value.decode("utf-8", errors="replace") if isinstance(value, bytes) else value
175
+ )
176
+
177
+
178
+ def _process_transcript(
179
+ *, stdout: str, stderr: str, exit_code: int | None, timed_out: bool
180
+ ) -> str:
181
+ """Retain complete process streams and terminal status as one immutable ledger object."""
182
+ return json.dumps(
183
+ {
184
+ "stdout": stdout,
185
+ "stderr": stderr,
186
+ "exit_code": exit_code,
187
+ "timed_out": timed_out,
188
+ },
189
+ sort_keys=True,
190
+ )
191
+
192
+
193
+ def _token_usage(*, stdout: str) -> int | None:
194
+ """Sum typed Codex turn usage without treating its prose as trusted output."""
195
+ total = 0
196
+ found = False
197
+ for line in stdout.splitlines():
198
+ try:
199
+ event = json.loads(line)
200
+ except json.JSONDecodeError:
201
+ continue
202
+ if not isinstance(event, dict) or event.get("type") != "turn.completed":
203
+ continue
204
+ usage = event.get("usage")
205
+ if not isinstance(usage, dict):
206
+ continue
207
+ input_tokens = usage.get("input_tokens")
208
+ output_tokens = usage.get("output_tokens")
209
+ if (
210
+ not isinstance(input_tokens, int)
211
+ or input_tokens < 0
212
+ or not isinstance(output_tokens, int)
213
+ or output_tokens < 0
214
+ ):
215
+ continue
216
+ total += input_tokens + output_tokens
217
+ found = True
218
+ return total if found else None
219
+
220
+
221
+ CodexCLIWriterAdapter = CodexCLIAgentAdapter
@@ -0,0 +1,42 @@
1
+ """The markitdown conversion route (D38): office/html/email formats → Markdown."""
2
+
3
+ import io
4
+ from typing import Final
5
+
6
+ from markitdown import MarkItDown
7
+ from markitdown import StreamInfo
8
+ from markitdown._exceptions import MarkItDownException
9
+
10
+ from rememberstack.model import ConversionError
11
+ from rememberstack.model import ConversionResult
12
+
13
+ MARKITDOWN_CONVERTER_VERSION: Final = "markitdown-0.1"
14
+ """Pins the markitdown library generation this route was validated against."""
15
+
16
+
17
+ class MarkitdownConverter:
18
+ """The default local route for structured text formats (html, office, email)."""
19
+
20
+ def __init__(self) -> None:
21
+ """Build the converter once; markitdown instances are reusable."""
22
+ self._markitdown = MarkItDown(enable_plugins=False)
23
+
24
+ @property
25
+ def name(self) -> str:
26
+ """The route name recorded on representations."""
27
+ return "markitdown"
28
+
29
+ @property
30
+ def version(self) -> str:
31
+ """The pinned markitdown route version (D38)."""
32
+ return MARKITDOWN_CONVERTER_VERSION
33
+
34
+ def convert(self, *, content: bytes, mime: str) -> ConversionResult:
35
+ """Convert one input via markitdown; its failures become typed failures."""
36
+ try:
37
+ result = self._markitdown.convert_stream(
38
+ io.BytesIO(content), stream_info=StreamInfo(mimetype=mime)
39
+ )
40
+ except MarkItDownException as err:
41
+ raise ConversionError(f"markitdown could not convert {mime!r}") from err
42
+ return ConversionResult(document_md=result.text_content)
@@ -0,0 +1,136 @@
1
+ """The OpenRouter model-provider adapter (D63/D70): the shipped default binding."""
2
+
3
+ from decimal import Decimal
4
+ from decimal import InvalidOperation
5
+ import json
6
+ import time
7
+ from typing import Any
8
+ from typing import TypeVar
9
+
10
+ import httpx
11
+ from pydantic import Field
12
+ from pydantic_settings import BaseSettings
13
+ from pydantic_settings import SettingsConfigDict
14
+
15
+ from rememberstack.model import EmbeddingRequest
16
+ from rememberstack.model import EmbeddingResponse
17
+ from rememberstack.model import GeneratedResponse
18
+ from rememberstack.model import ModelRequest
19
+ from rememberstack.model import ProviderAccountingError
20
+ from rememberstack.model import ProviderCallUsage
21
+ from rememberstack.model import StructuredResponseModel
22
+
23
+ ResponseT = TypeVar("ResponseT", bound=StructuredResponseModel)
24
+
25
+
26
+ class OpenRouterSettings(BaseSettings):
27
+ """The OpenRouter binding: key and endpoint, per deployment (D61)."""
28
+
29
+ model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_OPENROUTER_")
30
+
31
+ api_key: str = Field(min_length=1)
32
+ base_url: str = Field(default="https://openrouter.ai/api/v1")
33
+ timeout_s: float = Field(default=120.0, gt=0)
34
+
35
+
36
+ class OpenRouterProviderError(Exception):
37
+ """OpenRouter returned an error or an unusable response body."""
38
+
39
+
40
+ class OpenRouterModelProvider:
41
+ """Structured generations and embeddings over the OpenRouter HTTP API."""
42
+
43
+ def __init__(self, *, settings: OpenRouterSettings) -> None:
44
+ """Bind one HTTP client to the configured endpoint and key."""
45
+ self._settings = settings
46
+ self._client = httpx.Client(
47
+ base_url=settings.base_url,
48
+ headers={"Authorization": f"Bearer {settings.api_key}"},
49
+ timeout=settings.timeout_s,
50
+ )
51
+
52
+ def generate(
53
+ self, *, request: ModelRequest, response_type: type[ResponseT]
54
+ ) -> GeneratedResponse[ResponseT]:
55
+ """One chat completion constrained to the caller's declared JSON schema."""
56
+ started_ns = time.monotonic_ns()
57
+ body = self._post(
58
+ path="/chat/completions",
59
+ payload={
60
+ "model": request.model,
61
+ "messages": [{"role": "user", "content": request.prompt}],
62
+ "response_format": {
63
+ "type": "json_schema",
64
+ "json_schema": {
65
+ "name": response_type.__name__,
66
+ "strict": True,
67
+ "schema": response_type.model_json_schema(),
68
+ },
69
+ },
70
+ },
71
+ )
72
+ usage = _usage(
73
+ body=body,
74
+ requested_model=request.model,
75
+ latency_ms=(time.monotonic_ns() - started_ns) // 1_000_000,
76
+ )
77
+ try:
78
+ content = body["choices"][0]["message"]["content"]
79
+ output = response_type.model_validate(json.loads(content))
80
+ except (KeyError, IndexError, ValueError) as err:
81
+ raise OpenRouterProviderError(
82
+ f"unusable completion body for {response_type.__name__}"
83
+ ) from err
84
+ return GeneratedResponse(output=output, usage=usage)
85
+
86
+ def embed(self, *, request: EmbeddingRequest) -> EmbeddingResponse:
87
+ """One embeddings call for the caller's batch."""
88
+ started_ns = time.monotonic_ns()
89
+ body = self._post(
90
+ path="/embeddings",
91
+ payload={"model": request.model, "input": list(request.texts)},
92
+ )
93
+ usage = _usage(
94
+ body=body,
95
+ requested_model=request.model,
96
+ latency_ms=(time.monotonic_ns() - started_ns) // 1_000_000,
97
+ )
98
+ try:
99
+ ordered = sorted(body["data"], key=lambda item: item["index"])
100
+ return EmbeddingResponse(
101
+ vectors=tuple(tuple(item["embedding"]) for item in ordered), usage=usage
102
+ )
103
+ except (KeyError, TypeError, ValueError) as err:
104
+ raise OpenRouterProviderError("unusable embeddings body") from err
105
+
106
+ def _post(self, *, path: str, payload: dict[str, object]) -> dict[str, Any]:
107
+ """POST one JSON request; non-2xx responses become typed errors."""
108
+ response = self._client.post(path, json=payload)
109
+ if response.status_code >= 400:
110
+ raise OpenRouterProviderError(
111
+ f"OpenRouter {path} returned {response.status_code}: "
112
+ f"{response.text[:500]}"
113
+ )
114
+ return response.json()
115
+
116
+
117
+ def _usage(
118
+ *, body: dict[str, Any], requested_model: str, latency_ms: int
119
+ ) -> ProviderCallUsage:
120
+ """Validate OpenRouter accounting; missing usage must not silently disable budgets."""
121
+ raw = body.get("usage")
122
+ if not isinstance(raw, dict):
123
+ raise ProviderAccountingError("OpenRouter response carries no usage accounting")
124
+ model_name = body.get("model", requested_model)
125
+ try:
126
+ return ProviderCallUsage(
127
+ model_name=model_name,
128
+ tokens_in=raw["prompt_tokens"],
129
+ tokens_out=raw.get("completion_tokens", 0),
130
+ cost_usd=Decimal(str(raw["cost"])),
131
+ latency_ms=latency_ms,
132
+ )
133
+ except (InvalidOperation, KeyError, TypeError, ValueError) as err:
134
+ raise ProviderAccountingError(
135
+ "OpenRouter response carries invalid usage accounting"
136
+ ) from err
@@ -0,0 +1,54 @@
1
+ """Self-host adapters: pg delivery shell, local-FS object store, local mounts (WP-0.4a)."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from rememberstack.adapters.selfhost.forget import LocalFSForgetManifestStore
6
+ from rememberstack.adapters.selfhost.git import LocalGitRepository
7
+ from rememberstack.adapters.selfhost.minio import MinIOObjectStore
8
+ from rememberstack.adapters.selfhost.minio import MinIOSettings
9
+ from rememberstack.adapters.selfhost.mounts import AuditedRawReader
10
+ from rememberstack.adapters.selfhost.mounts import LocalMountPublisher
11
+ from rememberstack.adapters.selfhost.mounts import RawAccessDenied
12
+ from rememberstack.adapters.selfhost.mounts import storage_class_for
13
+ from rememberstack.adapters.selfhost.object_store import LocalFSObjectStore
14
+ from rememberstack.adapters.selfhost.object_store import ObjectAlreadyExistsError
15
+ from rememberstack.adapters.selfhost.object_store import ObjectKeyEscapesRootError
16
+ from rememberstack.adapters.selfhost.projection import SelfHostProjectionPurger
17
+ from rememberstack.adapters.selfhost.queue import SelfHostTaskQueue
18
+ from rememberstack.adapters.selfhost.queue import SelfHostWorkerLoop
19
+ from rememberstack.adapters.selfhost.queue import TokenBucket
20
+ from rememberstack.adapters.selfhost.telemetry import JsonLineTelemetry
21
+ from rememberstack.adapters.selfhost.watcher import LocalDirectoryWatcher
22
+
23
+ if TYPE_CHECKING:
24
+ from rememberstack.adapters.selfhost.lance import LanceChunkIndex
25
+
26
+ __all__ = (
27
+ "LanceChunkIndex",
28
+ "LocalFSForgetManifestStore",
29
+ "LocalGitRepository",
30
+ "LocalDirectoryWatcher",
31
+ "JsonLineTelemetry",
32
+ "LocalFSObjectStore",
33
+ "AuditedRawReader",
34
+ "LocalMountPublisher",
35
+ "MinIOObjectStore",
36
+ "MinIOSettings",
37
+ "RawAccessDenied",
38
+ "storage_class_for",
39
+ "ObjectAlreadyExistsError",
40
+ "ObjectKeyEscapesRootError",
41
+ "SelfHostTaskQueue",
42
+ "SelfHostProjectionPurger",
43
+ "SelfHostWorkerLoop",
44
+ "TokenBucket",
45
+ )
46
+
47
+
48
+ def __getattr__(name: str) -> object:
49
+ """Load the heavy LanceDB adapter only when a composition actually needs it."""
50
+ if name == "LanceChunkIndex":
51
+ from rememberstack.adapters.selfhost.lance import LanceChunkIndex
52
+
53
+ return LanceChunkIndex
54
+ raise AttributeError(name)
@@ -0,0 +1,66 @@
1
+ """Dedicated append-only local store for portable D74 forget intent."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from uuid import UUID
6
+ from uuid import uuid4
7
+
8
+ from rememberstack.model import ForgetManifest
9
+ from rememberstack.model import ForgetManifestConflictError
10
+
11
+
12
+ class LocalFSForgetManifestStore:
13
+ """Persist content-free manifests outside the ordinary data restore root."""
14
+
15
+ def __init__(self, *, root: Path) -> None:
16
+ """Bind an explicitly provisioned manifest root; absence is unsafe."""
17
+ self._root = root.resolve(strict=True)
18
+ if not self._root.is_dir():
19
+ raise NotADirectoryError(self._root)
20
+
21
+ def append(self, *, manifest: ForgetManifest) -> None:
22
+ """Atomically append exact canonical bytes, idempotent by ``forget_id``."""
23
+ content = manifest.canonical_bytes()
24
+ destination = self._path(forget_id=manifest.forget_id)
25
+ temporary = self._root / f".{manifest.forget_id}.{uuid4()}.tmp"
26
+ try:
27
+ with temporary.open(mode="xb") as handle:
28
+ handle.write(content)
29
+ handle.flush()
30
+ os.fsync(handle.fileno())
31
+ try:
32
+ os.link(temporary, destination)
33
+ except FileExistsError:
34
+ if destination.read_bytes() != content:
35
+ raise ForgetManifestConflictError(
36
+ f"forget_id {manifest.forget_id} already has different bytes"
37
+ ) from None
38
+ else:
39
+ self._sync_root()
40
+ finally:
41
+ temporary.unlink(missing_ok=True)
42
+
43
+ def manifests(self, *, deployment_id: UUID) -> tuple[ForgetManifest, ...]:
44
+ """Enumerate and validate this deployment's manifests by forget identity."""
45
+ result: list[ForgetManifest] = []
46
+ for path in sorted(self._root.glob("*.json")):
47
+ manifest = ForgetManifest.model_validate_json(path.read_bytes())
48
+ if path.stem != str(manifest.forget_id):
49
+ raise ForgetManifestConflictError(
50
+ f"manifest filename {path.name!r} disagrees with its forget_id"
51
+ )
52
+ if manifest.deployment_id == deployment_id:
53
+ result.append(manifest)
54
+ return tuple(result)
55
+
56
+ def _path(self, *, forget_id: UUID) -> Path:
57
+ """Return the fixed UUID-only destination for one immutable manifest."""
58
+ return self._root / f"{forget_id}.json"
59
+
60
+ def _sync_root(self) -> None:
61
+ """Make the newly linked directory entry durable before acknowledging append."""
62
+ descriptor = os.open(self._root, os.O_RDONLY)
63
+ try:
64
+ os.fsync(descriptor)
65
+ finally:
66
+ os.close(descriptor)