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,393 @@
|
|
|
1
|
+
"""Stock-harness prose compiler for deterministic two-band Plane-K pages."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from datetime import UTC
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import PurePosixPath
|
|
8
|
+
import re
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import Final
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
from pydantic import field_validator
|
|
17
|
+
from pydantic import TypeAdapter
|
|
18
|
+
from pydantic_settings import BaseSettings
|
|
19
|
+
from pydantic_settings import SettingsConfigDict
|
|
20
|
+
|
|
21
|
+
from rememberstack.core import cap_knowledge_writer_bundle
|
|
22
|
+
from rememberstack.core import compose_knowledge_page
|
|
23
|
+
from rememberstack.core import knowledge_content_hash
|
|
24
|
+
from rememberstack.core import knowledge_inputs_hash
|
|
25
|
+
from rememberstack.core import knowledge_summary_hash
|
|
26
|
+
from rememberstack.core import knowledge_writer_coverage
|
|
27
|
+
from rememberstack.core import render_knowledge_fact_sheet
|
|
28
|
+
from rememberstack.core import render_knowledge_writer_bundle
|
|
29
|
+
from rememberstack.model import KnowledgeCitation
|
|
30
|
+
from rememberstack.model import KnowledgeCompilationFailure
|
|
31
|
+
from rememberstack.model import KnowledgeCompilationWrite
|
|
32
|
+
from rememberstack.model import KnowledgeCompileContext
|
|
33
|
+
from rememberstack.model import KnowledgePageCompileOutput
|
|
34
|
+
from rememberstack.model import KnowledgePageCompileRequest
|
|
35
|
+
from rememberstack.model import KnowledgeWriterBundle
|
|
36
|
+
from rememberstack.model import KnowledgeWriterSessionRequest
|
|
37
|
+
from rememberstack.model import KnowledgeWriterSessionResult
|
|
38
|
+
from rememberstack.model import KnowledgeWriterSuggestion
|
|
39
|
+
from rememberstack.model import ObjectKey
|
|
40
|
+
from rememberstack.model import UTCDateTime
|
|
41
|
+
from rememberstack.ports import MountPublisherPort
|
|
42
|
+
from rememberstack.ports import ObjectStorePort
|
|
43
|
+
from rememberstack.spine.knowledge import KnowledgeControlPlane
|
|
44
|
+
from rememberstack.workers.knowledge_fact_sheet import KnowledgeFactSheetCompiler
|
|
45
|
+
|
|
46
|
+
KNOWLEDGE_WRITER_VERSION: Final = "k-writer-2026.07"
|
|
47
|
+
|
|
48
|
+
_WRITER_PROMPT: Final = """You compile exactly one Plane-K prose band.
|
|
49
|
+
Your working directory is the declared output/ surface. Read ../INSTRUCTIONS.md and every
|
|
50
|
+
prepared input under ../bundle/ and ../context/. Memory mount locations are listed in
|
|
51
|
+
../context/memory_mounts.json and are read-only. Do not use the
|
|
52
|
+
internet, initialize git, commit, or edit outside this temporary workspace.
|
|
53
|
+
|
|
54
|
+
Write only these declared files in the current directory (archived under output/):
|
|
55
|
+
- prose.md: non-empty prose band; do not reproduce the generated fact sheet.
|
|
56
|
+
- citations.json: JSON array of objects with role supports|contradicts|cites and exactly one
|
|
57
|
+
target: claim_lineage_id + claim_chunk_content_hash together, relation_id, or doc_id. Copy
|
|
58
|
+
stable claim coordinates and IDs exactly from available evidence.
|
|
59
|
+
- summary.md: a two- or three-sentence page summary for parent compilers.
|
|
60
|
+
- suggestions.json: JSON array of optional planner suggestions with action,
|
|
61
|
+
rationale, and payload. Use [] when there are none. Suggestions never take action.
|
|
62
|
+
|
|
63
|
+
Treat curation exclusions as binding. The driver will independently validate every file,
|
|
64
|
+
citation, link, count, and final content hash before anything can enter git.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_CITATIONS_ADAPTER = TypeAdapter(tuple[KnowledgeCitation, ...])
|
|
68
|
+
_SUGGESTIONS_ADAPTER = TypeAdapter(tuple[KnowledgeWriterSuggestion, ...])
|
|
69
|
+
_UTC_ADAPTER = TypeAdapter(UTCDateTime)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _utc_now() -> datetime:
|
|
73
|
+
"""Return the current aware UTC time for production compilation metadata."""
|
|
74
|
+
return datetime.now(tz=UTC)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class KnowledgeWriterSettings(BaseSettings):
|
|
78
|
+
"""Settings-owned model, timeout, cap, and transcript-key policy."""
|
|
79
|
+
|
|
80
|
+
model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_K_WRITER_")
|
|
81
|
+
|
|
82
|
+
model: str = Field(min_length=1)
|
|
83
|
+
timeout_seconds: int = Field(gt=0)
|
|
84
|
+
residue_claim_limit: int = Field(ge=0)
|
|
85
|
+
evidence_claims_per_fact: int = Field(ge=0)
|
|
86
|
+
transcript_prefix: str = Field(min_length=1)
|
|
87
|
+
|
|
88
|
+
@field_validator("transcript_prefix")
|
|
89
|
+
@classmethod
|
|
90
|
+
def require_safe_transcript_prefix(cls, value: str) -> str:
|
|
91
|
+
"""Keep immutable transcript objects under one normalized relative prefix."""
|
|
92
|
+
path = PurePosixPath(value)
|
|
93
|
+
if path.is_absolute() or ".." in path.parts or str(path) != value:
|
|
94
|
+
raise ValueError("transcript_prefix must be a normalized relative path")
|
|
95
|
+
return value.rstrip("/")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class KnowledgeWriterError(RuntimeError):
|
|
99
|
+
"""A writer session failed before producing acceptable declared output."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class KnowledgeWriterSession(Protocol):
|
|
103
|
+
"""The narrow stock-harness seam used only by the Plane-K writer."""
|
|
104
|
+
|
|
105
|
+
def run_session(
|
|
106
|
+
self, *, request: KnowledgeWriterSessionRequest
|
|
107
|
+
) -> "KnowledgeWriterSessionResult":
|
|
108
|
+
"""Return raw declared files and a transcript without accepting output."""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class KnowledgeProseCompiler:
|
|
113
|
+
"""Compile one prose page from an exact capped bundle and generated fact sheet."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
control_plane: KnowledgeControlPlane,
|
|
119
|
+
writer_session: KnowledgeWriterSession,
|
|
120
|
+
transcript_store: ObjectStorePort,
|
|
121
|
+
mount_publisher: MountPublisherPort,
|
|
122
|
+
settings: KnowledgeWriterSettings,
|
|
123
|
+
clock: Callable[[], datetime] = _utc_now,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Bind authoritative hydration, stock harness, ledger store, and settings."""
|
|
126
|
+
self._control_plane = control_plane
|
|
127
|
+
self._writer_session = writer_session
|
|
128
|
+
self._transcript_store = transcript_store
|
|
129
|
+
self._mount_publisher = mount_publisher
|
|
130
|
+
self._settings = settings
|
|
131
|
+
self._clock = clock
|
|
132
|
+
self.writer_version = _writer_version(settings=settings)
|
|
133
|
+
|
|
134
|
+
def compile_page(
|
|
135
|
+
self, *, request: KnowledgePageCompileRequest
|
|
136
|
+
) -> KnowledgePageCompileOutput:
|
|
137
|
+
"""Run one writer, archive its transcript, then build validated two-band output."""
|
|
138
|
+
artifact = request.artifact
|
|
139
|
+
if artifact.artifact_kind == "fact_sheet":
|
|
140
|
+
raise KnowledgeWriterError(
|
|
141
|
+
"prose compiler cannot compile a fact-sheet-only page"
|
|
142
|
+
)
|
|
143
|
+
_validate_curation_input(request=request)
|
|
144
|
+
child_summary_hashes = tuple(
|
|
145
|
+
knowledge_summary_hash(summary=summary)
|
|
146
|
+
for summary in request.child_summaries.values()
|
|
147
|
+
)
|
|
148
|
+
shared_model_summary_hash = (
|
|
149
|
+
None
|
|
150
|
+
if request.shared_model_summary is None
|
|
151
|
+
else knowledge_summary_hash(summary=request.shared_model_summary)
|
|
152
|
+
)
|
|
153
|
+
hydrated = self._control_plane.writer_bundle(
|
|
154
|
+
artifact_id=artifact.artifact_id,
|
|
155
|
+
context=KnowledgeCompileContext(
|
|
156
|
+
curation_hash=request.curation_hash,
|
|
157
|
+
shared_model_summary_hash=shared_model_summary_hash,
|
|
158
|
+
writer_version=self.writer_version,
|
|
159
|
+
),
|
|
160
|
+
child_summary_hashes=child_summary_hashes,
|
|
161
|
+
)
|
|
162
|
+
bundle = cap_knowledge_writer_bundle(
|
|
163
|
+
bundle=hydrated,
|
|
164
|
+
exclusions=request.exclusions,
|
|
165
|
+
residue_claim_limit=self._settings.residue_claim_limit,
|
|
166
|
+
evidence_claims_per_fact=self._settings.evidence_claims_per_fact,
|
|
167
|
+
)
|
|
168
|
+
compilation_id = uuid4()
|
|
169
|
+
transcript_uri: str | None = None
|
|
170
|
+
candidate_count = len(bundle.fact_sheet.facts) + len(bundle.claim_groups)
|
|
171
|
+
try:
|
|
172
|
+
mounts = self._mount_publisher.publish(deployment_id=artifact.deployment_id)
|
|
173
|
+
if mounts.deployment_id != artifact.deployment_id:
|
|
174
|
+
raise KnowledgeWriterError("mount publisher crossed deployments")
|
|
175
|
+
session_id = uuid4()
|
|
176
|
+
result = self._writer_session.run_session(
|
|
177
|
+
request=KnowledgeWriterSessionRequest(
|
|
178
|
+
session_id=session_id,
|
|
179
|
+
model=self._settings.model,
|
|
180
|
+
prompt=_WRITER_PROMPT,
|
|
181
|
+
timeout_seconds=self._settings.timeout_seconds,
|
|
182
|
+
input_files=_writer_inputs(
|
|
183
|
+
request=request,
|
|
184
|
+
bundle=bundle,
|
|
185
|
+
mounts_json=mounts.model_dump_json(),
|
|
186
|
+
),
|
|
187
|
+
mounts=mounts,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
if result.session_id != session_id:
|
|
191
|
+
raise KnowledgeWriterError("writer returned a different session ID")
|
|
192
|
+
transcript_uri = self._archive_transcript(
|
|
193
|
+
request=request, session_id=session_id, transcript=result.transcript
|
|
194
|
+
)
|
|
195
|
+
if result.timed_out:
|
|
196
|
+
raise KnowledgeWriterError("writer session timed out")
|
|
197
|
+
if result.exit_code != 0:
|
|
198
|
+
raise KnowledgeWriterError(
|
|
199
|
+
f"writer session exited with status {result.exit_code}"
|
|
200
|
+
)
|
|
201
|
+
prose, citations, summary, suggestions = _parse_writer_outputs(
|
|
202
|
+
files=result.output_files
|
|
203
|
+
)
|
|
204
|
+
citations = _unique_citations(citations=citations)
|
|
205
|
+
self._control_plane.validate_citations(
|
|
206
|
+
deployment_id=artifact.deployment_id, citations=citations
|
|
207
|
+
)
|
|
208
|
+
coverage = knowledge_writer_coverage(bundle=bundle, citations=citations)
|
|
209
|
+
rendered = render_knowledge_fact_sheet(
|
|
210
|
+
snapshot=bundle.fact_sheet,
|
|
211
|
+
compiled_at=_UTC_ADAPTER.validate_python(self._clock()),
|
|
212
|
+
citation_count=len(citations),
|
|
213
|
+
candidate_count=coverage.candidate_count,
|
|
214
|
+
)
|
|
215
|
+
markdown = compose_knowledge_page(
|
|
216
|
+
prose_markdown=prose, fact_sheet_markdown=rendered.markdown
|
|
217
|
+
)
|
|
218
|
+
return KnowledgePageCompileOutput(
|
|
219
|
+
compilation=KnowledgeCompilationWrite(
|
|
220
|
+
compilation_id=compilation_id,
|
|
221
|
+
deployment_id=artifact.deployment_id,
|
|
222
|
+
artifact_id=artifact.artifact_id,
|
|
223
|
+
inputs_hash=knowledge_inputs_hash(
|
|
224
|
+
snapshot=bundle.fact_sheet.input_snapshot
|
|
225
|
+
),
|
|
226
|
+
candidate_count=coverage.candidate_count,
|
|
227
|
+
uncited_count=coverage.uncited_count,
|
|
228
|
+
claims_cut_count=bundle.claims_cut_count,
|
|
229
|
+
citations=citations,
|
|
230
|
+
suggestions=suggestions,
|
|
231
|
+
writer_version=self.writer_version,
|
|
232
|
+
page_summary=summary,
|
|
233
|
+
content_hash=knowledge_content_hash(markdown=markdown),
|
|
234
|
+
tokens=result.tokens,
|
|
235
|
+
cost_usd=result.cost_usd,
|
|
236
|
+
session_transcript_uri=transcript_uri,
|
|
237
|
+
),
|
|
238
|
+
markdown=markdown,
|
|
239
|
+
)
|
|
240
|
+
except Exception as error:
|
|
241
|
+
failure_trace = traceback.format_exc()
|
|
242
|
+
try:
|
|
243
|
+
self._control_plane.record_failed_compilation(
|
|
244
|
+
failure=KnowledgeCompilationFailure(
|
|
245
|
+
compilation_id=compilation_id,
|
|
246
|
+
deployment_id=artifact.deployment_id,
|
|
247
|
+
artifact_id=artifact.artifact_id,
|
|
248
|
+
inputs_hash=knowledge_inputs_hash(
|
|
249
|
+
snapshot=bundle.fact_sheet.input_snapshot
|
|
250
|
+
),
|
|
251
|
+
candidate_count=candidate_count,
|
|
252
|
+
claims_cut_count=bundle.claims_cut_count,
|
|
253
|
+
writer_version=self.writer_version,
|
|
254
|
+
failure=failure_trace,
|
|
255
|
+
session_transcript_uri=transcript_uri,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
except Exception as ledger_error:
|
|
259
|
+
raise ExceptionGroup(
|
|
260
|
+
"writer compilation and failure-ledger recording both failed",
|
|
261
|
+
(error, ledger_error),
|
|
262
|
+
) from None
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
def _archive_transcript(
|
|
266
|
+
self, *, request: KnowledgePageCompileRequest, session_id: UUID, transcript: str
|
|
267
|
+
) -> str:
|
|
268
|
+
"""Persist the complete session ledger before interpreting any writer file."""
|
|
269
|
+
key = ObjectKey(
|
|
270
|
+
f"{self._settings.transcript_prefix}/"
|
|
271
|
+
f"{request.artifact.deployment_id}/{request.artifact.artifact_id}/"
|
|
272
|
+
f"{session_id}.json"
|
|
273
|
+
)
|
|
274
|
+
self._transcript_store.write_bytes(key=key, content=transcript.encode("utf-8"))
|
|
275
|
+
return key.root
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class KnowledgePageCompilerRouter:
|
|
279
|
+
"""Route fact-sheet-only pages away from the stock writer session entirely."""
|
|
280
|
+
|
|
281
|
+
def __init__(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
fact_sheet_compiler: KnowledgeFactSheetCompiler,
|
|
285
|
+
prose_compiler: KnowledgeProseCompiler,
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Bind the two page compilers behind the existing driver protocol."""
|
|
288
|
+
self._fact_sheet_compiler = fact_sheet_compiler
|
|
289
|
+
self._prose_compiler = prose_compiler
|
|
290
|
+
|
|
291
|
+
def compile_page(
|
|
292
|
+
self, *, request: KnowledgePageCompileRequest
|
|
293
|
+
) -> KnowledgePageCompileOutput:
|
|
294
|
+
"""Skip the writer exactly for artifacts designated as fact-sheet-only."""
|
|
295
|
+
if request.artifact.artifact_kind == "fact_sheet":
|
|
296
|
+
return self._fact_sheet_compiler.compile_page(request=request)
|
|
297
|
+
return self._prose_compiler.compile_page(request=request)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _writer_inputs(
|
|
301
|
+
*,
|
|
302
|
+
request: KnowledgePageCompileRequest,
|
|
303
|
+
bundle: KnowledgeWriterBundle,
|
|
304
|
+
mounts_json: str,
|
|
305
|
+
) -> dict[str, str]:
|
|
306
|
+
"""Prepare the fixed workspace inputs accepted by the stock harness adapter."""
|
|
307
|
+
children = {
|
|
308
|
+
str(artifact_id): summary
|
|
309
|
+
for artifact_id, summary in sorted(
|
|
310
|
+
request.child_summaries.items(), key=lambda item: str(item[0])
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
"INSTRUCTIONS.md": _WRITER_PROMPT,
|
|
315
|
+
"bundle/evidence.json": render_knowledge_writer_bundle(bundle=bundle),
|
|
316
|
+
"context/curation.md": request.curation_markdown or "_No curation guidance._\n",
|
|
317
|
+
"context/previous_page.md": request.previous_markdown or "_No prior page._\n",
|
|
318
|
+
"context/child_summaries.json": f"{json.dumps(children, sort_keys=True, indent=2)}\n",
|
|
319
|
+
"context/shared_model.md": request.shared_model_summary
|
|
320
|
+
or "_No shared model summary._\n",
|
|
321
|
+
"context/memory_mounts.json": f"{mounts_json}\n",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _parse_writer_outputs(
|
|
326
|
+
*, files: dict[str, str]
|
|
327
|
+
) -> tuple[
|
|
328
|
+
str, tuple[KnowledgeCitation, ...], str, tuple[KnowledgeWriterSuggestion, ...]
|
|
329
|
+
]:
|
|
330
|
+
"""Parse only declared writer files into the typed compiler contract."""
|
|
331
|
+
required = ("output/prose.md", "output/citations.json", "output/summary.md")
|
|
332
|
+
missing = tuple(path for path in required if path not in files)
|
|
333
|
+
if missing:
|
|
334
|
+
raise KnowledgeWriterError(
|
|
335
|
+
f"writer omitted required outputs: {', '.join(missing)}"
|
|
336
|
+
)
|
|
337
|
+
prose = files["output/prose.md"].strip()
|
|
338
|
+
summary = files["output/summary.md"].strip()
|
|
339
|
+
if not prose:
|
|
340
|
+
raise KnowledgeWriterError("writer prose output is empty")
|
|
341
|
+
if _sentence_count(text=summary) not in (2, 3):
|
|
342
|
+
raise KnowledgeWriterError("writer summary must contain two or three sentences")
|
|
343
|
+
try:
|
|
344
|
+
citations = _CITATIONS_ADAPTER.validate_json(files["output/citations.json"])
|
|
345
|
+
suggestions = _SUGGESTIONS_ADAPTER.validate_json(
|
|
346
|
+
files.get("output/suggestions.json", "[]")
|
|
347
|
+
)
|
|
348
|
+
except ValueError as error:
|
|
349
|
+
raise KnowledgeWriterError(
|
|
350
|
+
"writer JSON output violates the typed contract"
|
|
351
|
+
) from error
|
|
352
|
+
return prose, citations, summary, suggestions
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _validate_curation_input(*, request: KnowledgePageCompileRequest) -> None:
|
|
356
|
+
"""Bind the raw sidecar supplied to the harness to its D45 hash input."""
|
|
357
|
+
expected = (
|
|
358
|
+
None
|
|
359
|
+
if request.curation_markdown is None
|
|
360
|
+
else knowledge_content_hash(markdown=request.curation_markdown)
|
|
361
|
+
)
|
|
362
|
+
if request.curation_hash != expected:
|
|
363
|
+
raise KnowledgeWriterError("curation markdown does not match curation hash")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _sentence_count(*, text: str) -> int:
|
|
367
|
+
"""Count terminally punctuated summary sentences with one deterministic rule."""
|
|
368
|
+
return len(re.findall(r"[^.!?]+[.!?](?:\s|$)", text.strip()))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _unique_citations(
|
|
372
|
+
*, citations: tuple[KnowledgeCitation, ...]
|
|
373
|
+
) -> tuple[KnowledgeCitation, ...]:
|
|
374
|
+
"""Apply deterministic set semantics to repeated writer citations."""
|
|
375
|
+
keyed = {
|
|
376
|
+
(
|
|
377
|
+
item.role.value,
|
|
378
|
+
str(item.claim_lineage_id),
|
|
379
|
+
item.claim_chunk_content_hash or "",
|
|
380
|
+
str(item.relation_id),
|
|
381
|
+
str(item.doc_id),
|
|
382
|
+
): item
|
|
383
|
+
for item in citations
|
|
384
|
+
}
|
|
385
|
+
return tuple(keyed[key] for key in sorted(keyed))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _writer_version(*, settings: KnowledgeWriterSettings) -> str:
|
|
389
|
+
"""Make prompt, model, cap, and timeout settings hash-visible in D45 manifests."""
|
|
390
|
+
fingerprint = knowledge_summary_hash(
|
|
391
|
+
summary=f"{_WRITER_PROMPT}\n{settings.model_dump_json()}"
|
|
392
|
+
)
|
|
393
|
+
return f"{KNOWLEDGE_WRITER_VERSION}:{fingerprint[:16]}"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Thin operational services that compose spine truth with delivery ports."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from rememberstack.model import DeadLetterReplayResult
|
|
7
|
+
from rememberstack.model import ProcessingLane
|
|
8
|
+
from rememberstack.ports.queue import TaskQueuePort
|
|
9
|
+
from rememberstack.spine.work_ledger import WorkLedger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DeadLetterReplayer:
|
|
13
|
+
"""Replay one row through the ledger, then announce its committed route."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, *, ledger: WorkLedger, queue: TaskQueuePort) -> None:
|
|
16
|
+
"""Bind authoritative mutation and non-authoritative delivery."""
|
|
17
|
+
self._ledger = ledger
|
|
18
|
+
self._queue = queue
|
|
19
|
+
|
|
20
|
+
def replay(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
deployment_id: UUID,
|
|
24
|
+
processing_id: UUID,
|
|
25
|
+
attempt_allowance: int = 1,
|
|
26
|
+
lane: ProcessingLane | None = None,
|
|
27
|
+
not_before: datetime | None = None,
|
|
28
|
+
) -> DeadLetterReplayResult:
|
|
29
|
+
"""Commit one explicit replay transition, then wake its returned route."""
|
|
30
|
+
replayed = self._ledger.replay_dead_letter(
|
|
31
|
+
deployment_id=deployment_id,
|
|
32
|
+
processing_id=processing_id,
|
|
33
|
+
attempt_allowance=attempt_allowance,
|
|
34
|
+
lane=lane,
|
|
35
|
+
not_before=not_before,
|
|
36
|
+
)
|
|
37
|
+
self._queue.announce(
|
|
38
|
+
processing_id=replayed.processing_id,
|
|
39
|
+
route_snapshot=replayed.route,
|
|
40
|
+
not_before_snapshot=replayed.not_before,
|
|
41
|
+
)
|
|
42
|
+
return replayed
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""The P1 inline writers (D8, retrieval §5): the claims and facts channels.
|
|
2
|
+
|
|
3
|
+
The claims channel is the needle index — every accepted claim embedded with
|
|
4
|
+
its `is_current_testimony` scalar, so the DEFAULT claims search filters to
|
|
5
|
+
current testimony without touching Postgres. The facts channel carries the
|
|
6
|
+
human-readable labels of relations (generated once per label generation) and
|
|
7
|
+
observations (their statements), embedded beside their status scalar.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Final
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
from pydantic_settings import BaseSettings
|
|
15
|
+
from pydantic_settings import SettingsConfigDict
|
|
16
|
+
|
|
17
|
+
from rememberstack.model import ClaimedWork
|
|
18
|
+
from rememberstack.model import EmbeddingRequest
|
|
19
|
+
from rememberstack.model import FactLabelResponse
|
|
20
|
+
from rememberstack.model import ModelRequest
|
|
21
|
+
from rememberstack.model import NonRetryableHandlerError
|
|
22
|
+
from rememberstack.model import P1ClaimRow
|
|
23
|
+
from rememberstack.model import P1FactRow
|
|
24
|
+
from rememberstack.ports.cost_meter import CostMeterPort
|
|
25
|
+
from rememberstack.ports.model_provider import ModelProviderPort
|
|
26
|
+
from rememberstack.ports.p1_index import ClaimIndexPort
|
|
27
|
+
from rememberstack.ports.p1_index import FactIndexPort
|
|
28
|
+
from rememberstack.spine.chunk_catalog import ChunkCatalog
|
|
29
|
+
from rememberstack.spine.claim_catalog import ClaimCatalog
|
|
30
|
+
from rememberstack.spine.fact_catalog import FactCatalog
|
|
31
|
+
from rememberstack.workers.base import HandlerOutcome
|
|
32
|
+
|
|
33
|
+
P1_EMBED_CLAIMS_VERSION: Final = "p1-embed-claims-2026.07"
|
|
34
|
+
"""The claim-embed stage's component version (the model rides settings)."""
|
|
35
|
+
|
|
36
|
+
FACT_LABEL_VERSION: Final = "p1-fact-label-2026.07"
|
|
37
|
+
"""The fact-labeler prompt generation (regenerated only on version bump)."""
|
|
38
|
+
|
|
39
|
+
_FACT_LABEL_PROMPT: Final = (
|
|
40
|
+
"Write one short natural sentence stating this fact, nothing else: "
|
|
41
|
+
"{subject} —[{predicate}]→ {object}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class P1Settings(BaseSettings):
|
|
46
|
+
"""The P1 writer model bindings (D61/D63/D70)."""
|
|
47
|
+
|
|
48
|
+
model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_P1_")
|
|
49
|
+
|
|
50
|
+
embedding_model: str = Field(default="qwen/qwen3-embedding-8b")
|
|
51
|
+
label_model: str = Field(default="openai/gpt-5.6-luna")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EmbedClaimsHandler:
|
|
55
|
+
"""The claim-embed stage: one version's claims into the P1 claims channel."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
claim_catalog: ClaimCatalog,
|
|
61
|
+
chunk_catalog: ChunkCatalog,
|
|
62
|
+
model_provider: ModelProviderPort,
|
|
63
|
+
claim_index: ClaimIndexPort,
|
|
64
|
+
settings: P1Settings,
|
|
65
|
+
chunker_version: str,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Bind the handler to its catalogs, provider, index, and generation."""
|
|
68
|
+
self._claim_catalog = claim_catalog
|
|
69
|
+
self._chunk_catalog = chunk_catalog
|
|
70
|
+
self._model_provider = model_provider
|
|
71
|
+
self._claim_index = claim_index
|
|
72
|
+
self._settings = settings
|
|
73
|
+
self._chunker_version = chunker_version
|
|
74
|
+
|
|
75
|
+
def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
|
|
76
|
+
"""Embed the version's not-yet-embedded claims as one document batch."""
|
|
77
|
+
source = self._chunk_catalog.chunk_source(
|
|
78
|
+
representation_id=_payload_uuid(work=work, field="representation_id")
|
|
79
|
+
)
|
|
80
|
+
chunks = self._chunk_catalog.chunks_for_embedding(
|
|
81
|
+
representation_id=source.representation_id,
|
|
82
|
+
chunker_version=self._chunker_version,
|
|
83
|
+
)
|
|
84
|
+
claims = self._claim_catalog.claims_for_embedding(
|
|
85
|
+
chunk_ids=tuple(chunk.chunk_id for chunk in chunks),
|
|
86
|
+
embedding_version=self._settings.embedding_model,
|
|
87
|
+
)
|
|
88
|
+
if not claims:
|
|
89
|
+
return HandlerOutcome() # replay: refs already stamped (D7)
|
|
90
|
+
response = self._model_provider.embed(
|
|
91
|
+
request=EmbeddingRequest(
|
|
92
|
+
model=self._settings.embedding_model,
|
|
93
|
+
texts=tuple(claim.claim_text for claim in claims),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
meter.record(call_key="embed_claims", tier="embedding", usage=response.usage)
|
|
97
|
+
self._claim_index.upsert_claims(
|
|
98
|
+
rows=tuple(
|
|
99
|
+
P1ClaimRow(
|
|
100
|
+
claim_id=claim.claim_id,
|
|
101
|
+
deployment_id=work.deployment_id,
|
|
102
|
+
doc_id=claim.doc_id,
|
|
103
|
+
chunk_id=claim.chunk_id,
|
|
104
|
+
text=claim.claim_text,
|
|
105
|
+
is_current_testimony=claim.is_current_testimony,
|
|
106
|
+
is_attributed=claim.is_attributed,
|
|
107
|
+
vector=vector,
|
|
108
|
+
)
|
|
109
|
+
for claim, vector in zip(claims, response.vectors, strict=True)
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
self._claim_catalog.record_claim_embeddings(
|
|
113
|
+
claim_ids=tuple(claim.claim_id for claim in claims),
|
|
114
|
+
embedding_version=self._settings.embedding_model,
|
|
115
|
+
)
|
|
116
|
+
return HandlerOutcome()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LabelFactsHandler:
|
|
120
|
+
"""The fact-label stage: readable labels for relations, embedded with
|
|
121
|
+
observation statements into the P1 facts channel (D8)."""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
facts: FactCatalog,
|
|
127
|
+
model_provider: ModelProviderPort,
|
|
128
|
+
fact_index: FactIndexPort,
|
|
129
|
+
settings: P1Settings,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Bind the handler to the fact catalog, provider, and facts index."""
|
|
132
|
+
self._facts = facts
|
|
133
|
+
self._model_provider = model_provider
|
|
134
|
+
self._fact_index = fact_index
|
|
135
|
+
self._settings = settings
|
|
136
|
+
|
|
137
|
+
def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
|
|
138
|
+
"""Label and embed the document's facts still lacking this generation.
|
|
139
|
+
|
|
140
|
+
Ordering is the invariant (Codex review): the index write lands
|
|
141
|
+
BEFORE any PG stamp, so Postgres never advertises a Lance ref that
|
|
142
|
+
was not written — a crash mid-pass re-labels the remainder on retry.
|
|
143
|
+
The generation stamp folds in the embedding model (D63): a model
|
|
144
|
+
change re-labels and re-embeds instead of silently skipping.
|
|
145
|
+
Concurrent document jobs serialize on the deployment label lock.
|
|
146
|
+
"""
|
|
147
|
+
doc_id = _payload_uuid(work=work, field="doc_id")
|
|
148
|
+
generation = f"{FACT_LABEL_VERSION}+{self._settings.embedding_model}"
|
|
149
|
+
with self._facts.label_lock(deployment_id=work.deployment_id):
|
|
150
|
+
rows: list[P1FactRow] = []
|
|
151
|
+
for relation in self._facts.relations_for_labeling(
|
|
152
|
+
deployment_id=work.deployment_id,
|
|
153
|
+
doc_id=doc_id,
|
|
154
|
+
label_version=generation,
|
|
155
|
+
):
|
|
156
|
+
label_call = self._model_provider.generate(
|
|
157
|
+
request=ModelRequest(
|
|
158
|
+
model=self._settings.label_model,
|
|
159
|
+
prompt=_FACT_LABEL_PROMPT.format(
|
|
160
|
+
subject=relation.subject_name,
|
|
161
|
+
predicate=relation.predicate,
|
|
162
|
+
object=relation.object_name,
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
response_type=FactLabelResponse,
|
|
166
|
+
)
|
|
167
|
+
meter.record(
|
|
168
|
+
call_key=f"label_relation:{relation.relation_id}",
|
|
169
|
+
tier="label",
|
|
170
|
+
usage=label_call.usage,
|
|
171
|
+
)
|
|
172
|
+
label = label_call.output.label
|
|
173
|
+
rows.append(
|
|
174
|
+
P1FactRow(
|
|
175
|
+
fact_id=relation.relation_id,
|
|
176
|
+
deployment_id=work.deployment_id,
|
|
177
|
+
kind="relation",
|
|
178
|
+
label=label,
|
|
179
|
+
status=relation.status,
|
|
180
|
+
vector=(0.0,), # replaced below by the batch embedding
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
rows.extend(
|
|
184
|
+
P1FactRow(
|
|
185
|
+
fact_id=observation.observation_id,
|
|
186
|
+
deployment_id=work.deployment_id,
|
|
187
|
+
kind="observation",
|
|
188
|
+
label=observation.obs_label,
|
|
189
|
+
status=observation.status,
|
|
190
|
+
vector=(0.0,),
|
|
191
|
+
)
|
|
192
|
+
for observation in self._facts.observations_for_embedding(
|
|
193
|
+
deployment_id=work.deployment_id,
|
|
194
|
+
doc_id=doc_id,
|
|
195
|
+
label_version=generation,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
if not rows:
|
|
199
|
+
return HandlerOutcome()
|
|
200
|
+
response = self._model_provider.embed(
|
|
201
|
+
request=EmbeddingRequest(
|
|
202
|
+
model=self._settings.embedding_model,
|
|
203
|
+
texts=tuple(row.label for row in rows),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
meter.record(call_key="embed_facts", tier="embedding", usage=response.usage)
|
|
207
|
+
self._fact_index.upsert_facts(
|
|
208
|
+
rows=tuple(
|
|
209
|
+
row.model_copy(update={"vector": vector})
|
|
210
|
+
for row, vector in zip(rows, response.vectors, strict=True)
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
for row in rows: # stamps only after the index write landed
|
|
214
|
+
if row.kind == "relation":
|
|
215
|
+
self._facts.record_fact_label(
|
|
216
|
+
relation_id=row.fact_id,
|
|
217
|
+
label=row.label,
|
|
218
|
+
label_version=generation,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
self._facts.record_observation_embedding(
|
|
222
|
+
observation_id=row.fact_id, label_version=generation
|
|
223
|
+
)
|
|
224
|
+
return HandlerOutcome()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _payload_uuid(*, work: ClaimedWork, field: str) -> UUID:
|
|
228
|
+
"""Read a required UUID from the claimed payload; absence is non-retryable."""
|
|
229
|
+
value = (work.payload or {}).get(field)
|
|
230
|
+
if not isinstance(value, str):
|
|
231
|
+
raise NonRetryableHandlerError(
|
|
232
|
+
f"stage {work.stage} work {work.processing_id} carries no {field!r} payload"
|
|
233
|
+
)
|
|
234
|
+
return UUID(value)
|