77from collections .abc import Callable , Mapping , Sequence
88from dataclasses import dataclass
99from datetime import datetime
10- from typing import Any
10+ from typing import Any , Literal , get_args
1111
1212from ...control_plane .runtime .public_safety import public_safe_compact_text
1313from ..context_providers import build_context_provider
@@ -98,6 +98,22 @@ class _ActiveItemDecision:
9898]
9999
100100
101+ RecallInputErrorCode = Literal [
102+ "freshness_age_invalid" , "freshness_context_invalid" ,
103+ "read_authority_checkpoint_missing" , "read_authority_checkpoint_invalid" ,
104+ ]
105+
106+
107+ class RewardMemoryRecallInputError (ValueError ):
108+ """An existing SDK input rejection with an allowlisted, non-content code."""
109+
110+ def __init__ (self , reason_code : RecallInputErrorCode , message : str ) -> None :
111+ if reason_code not in get_args (RecallInputErrorCode ):
112+ raise ValueError ("unsupported recall input error code" )
113+ super ().__init__ (message )
114+ self .reason_code = reason_code
115+
116+
101117def _token (value : object , label : str ) -> str :
102118 result = str (value or "" ).strip ()
103119 if not TOKEN_RE .fullmatch (result ):
@@ -243,22 +259,17 @@ def _authority_checkpoint(
243259 raw : object , * , corpus : Mapping [str , Any ], request : Mapping [str , Any ]
244260) -> tuple [dict [str , Any ], list [str ]]:
245261 if not isinstance (raw , Mapping ):
246- raise ValueError ("read_authority_checkpoint must be an object" )
247- checkpoint = {
248- "verified" : _boolean (raw , "verified" ),
249- "corpus_id" : _token (raw .get ("corpus_id" ), "checkpoint.corpus_id" ),
250- "workspace_ref" : _token (raw .get ("workspace_ref" ), "checkpoint.workspace_ref" ),
251- "project_ref" : _token (raw .get ("project_ref" ), "checkpoint.project_ref" ),
252- "surface_id" : _token (raw .get ("surface_id" ), "checkpoint.surface_id" ),
253- "read_authority" : _token (
254- raw .get ("read_authority" ), "checkpoint.read_authority"
255- ),
256- "source_ref" : _optional_token (raw .get ("source_ref" ), "checkpoint.source_ref" ),
257- }
258- for field in IDENTITY_SCOPE_FIELDS :
259- expected_scope = corpus ["scope" ].get (field )
260- if expected_scope :
261- checkpoint [field ] = _optional_token (raw .get (field ), f"checkpoint.{ field } " )
262+ raise RewardMemoryRecallInputError (
263+ "read_authority_checkpoint_missing" if raw is None else "read_authority_checkpoint_invalid" ,
264+ "read_authority_checkpoint must be an object" ,
265+ )
266+ try :
267+ checkpoint = _normalize_authority_checkpoint (raw , corpus = corpus )
268+ except ValueError as exc :
269+ raise RewardMemoryRecallInputError (
270+ "read_authority_checkpoint_missing" if not raw else "read_authority_checkpoint_invalid" ,
271+ str (exc ),
272+ ) from exc
262273 reasons : list [str ] = []
263274 expected = {
264275 "corpus_id" : corpus ["corpus_id" ],
@@ -283,22 +294,48 @@ def _authority_checkpoint(
283294 return checkpoint , reasons
284295
285296
297+ def _normalize_authority_checkpoint (
298+ raw : Mapping [str , Any ], * , corpus : Mapping [str , Any ],
299+ ) -> dict [str , Any ]:
300+ checkpoint = {
301+ "verified" : _boolean (raw , "verified" ),
302+ "corpus_id" : _token (raw .get ("corpus_id" ), "checkpoint.corpus_id" ),
303+ "workspace_ref" : _token (raw .get ("workspace_ref" ), "checkpoint.workspace_ref" ),
304+ "project_ref" : _token (raw .get ("project_ref" ), "checkpoint.project_ref" ),
305+ "surface_id" : _token (raw .get ("surface_id" ), "checkpoint.surface_id" ),
306+ "read_authority" : _token (
307+ raw .get ("read_authority" ), "checkpoint.read_authority"
308+ ),
309+ "source_ref" : _optional_token (raw .get ("source_ref" ), "checkpoint.source_ref" ),
310+ }
311+ for field in IDENTITY_SCOPE_FIELDS :
312+ expected_scope = corpus ["scope" ].get (field )
313+ if expected_scope :
314+ checkpoint [field ] = _optional_token (raw .get (field ), f"checkpoint.{ field } " )
315+ return checkpoint
316+
317+
286318def _freshness_reasons (
287319 corpus : Mapping [str , Any ], freshness : Mapping [str , Any ]
288320) -> list [str ]:
289321 reasons : list [str ] = []
290322 mode = corpus ["freshness" ]["mode" ]
291- source_truth_current = _boolean (freshness , "source_truth_current" )
292- source_revision = _optional_token (
293- freshness .get ("source_revision" ), "freshness_context.source_revision"
294- )
323+ try :
324+ source_truth_current = _boolean (freshness , "source_truth_current" )
325+ source_revision = _optional_token (
326+ freshness .get ("source_revision" ), "freshness_context.source_revision"
327+ )
328+ except ValueError as exc :
329+ raise RewardMemoryRecallInputError ("freshness_context_invalid" , str (exc )) from exc
295330 age_seconds = freshness .get ("age_seconds" )
296331 if age_seconds is not None and (
297332 isinstance (age_seconds , bool )
298333 or not isinstance (age_seconds , int )
299334 or age_seconds < 0
300335 ):
301- raise ValueError ("freshness_context.age_seconds must be a non-negative integer" )
336+ raise RewardMemoryRecallInputError (
337+ "freshness_age_invalid" , "freshness_context.age_seconds must be a non-negative integer" ,
338+ )
302339 if mode in {"source_truth_bound" , "execution_bound" } and not source_truth_current :
303340 reasons .append ("source_truth_not_current" )
304341 if mode in {"revision_bound" , "session_archive_bound" } and (
@@ -391,7 +428,7 @@ def build_reward_memory_recall_request(
391428 ):
392429 raise ValueError (f"limit must be between 1 and { MAX_RESULTS } " )
393430 if not isinstance (request .get ("freshness_context" ), Mapping ):
394- raise ValueError ( "freshness_context must be an object" )
431+ raise RewardMemoryRecallInputError ( "freshness_context_invalid" , "freshness_context must be an object" )
395432 if _boolean (request , "raw_content_captured" ):
396433 raise ValueError ("recall requests must not capture raw content" )
397434
0 commit comments