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.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|