claude-smart 0.2.46 → 0.2.48

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 (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,32 @@
1
+ from abc import abstractmethod
2
+
3
+ from reflexio.models.api_schema.domain import (
4
+ Interaction,
5
+ Status,
6
+ UserProfile,
7
+ )
8
+ from reflexio.models.api_schema.retriever_schema import (
9
+ SearchInteractionRequest,
10
+ SearchUserProfileRequest,
11
+ )
12
+
13
+
14
+ class ProfileSearchMixin:
15
+ """Mixin for interaction + profile search methods."""
16
+
17
+ @abstractmethod
18
+ def search_interaction(
19
+ self,
20
+ search_interaction_request: SearchInteractionRequest,
21
+ query_embedding: list[float] | None = None,
22
+ ) -> list[Interaction]:
23
+ raise NotImplementedError
24
+
25
+ @abstractmethod
26
+ def search_user_profile(
27
+ self,
28
+ search_user_profile_request: SearchUserProfileRequest,
29
+ status_filter: list[Status | None] | None = None,
30
+ query_embedding: list[float] | None = None,
31
+ ) -> list[UserProfile]:
32
+ raise NotImplementedError
@@ -17,6 +17,7 @@ from collections import OrderedDict
17
17
  from collections.abc import Callable
18
18
  from concurrent.futures import Future, ThreadPoolExecutor
19
19
  from concurrent.futures import TimeoutError as FuturesTimeoutError
20
+ from datetime import UTC, datetime
20
21
  from typing import TYPE_CHECKING, Any, cast
21
22
 
22
23
  from reflexio.models.api_schema.retriever_schema import (
@@ -41,6 +42,13 @@ from reflexio.models.config_schema import (
41
42
  from reflexio.server.llm.litellm_client import LiteLLMClient
42
43
  from reflexio.server.prompt.prompt_manager import PromptManager
43
44
  from reflexio.server.services.pre_retrieval import QueryReformulator
45
+ from reflexio.server.services.retrieval.recency import (
46
+ RecencyConfig,
47
+ ScoredItem,
48
+ additive_penalty,
49
+ decay_for_item,
50
+ multiplicative_factor,
51
+ )
44
52
  from reflexio.server.services.retrieval.relevance_floor import apply_relevance_floors
45
53
  from reflexio.server.services.storage.storage_base import BaseStorage
46
54
  from reflexio.server.tracing import profile_step, set_span_data
@@ -102,6 +110,7 @@ def run_unified_search(
102
110
  prompt_manager: PromptManager,
103
111
  pre_retrieval_model_name: str | None = None,
104
112
  retrieval_floor: RetrievalFloorConfig | None = None,
113
+ recency: RecencyConfig | None = None,
105
114
  ) -> UnifiedSearchResponse:
106
115
  """
107
116
  Search across all entity types (profiles, agent playbooks, user playbooks) in parallel.
@@ -129,7 +138,12 @@ def run_unified_search(
129
138
 
130
139
  floor_cfg = retrieval_floor or RetrievalFloorConfig()
131
140
  floor_on = floor_cfg.enabled
132
- fetch_k = max(top_k, floor_cfg.pool_size) if floor_on else top_k
141
+ recency_on = bool(recency and recency.enabled)
142
+ fetch_k = max(
143
+ top_k,
144
+ floor_cfg.pool_size if floor_on else 0,
145
+ recency.pool_size if recency_on and recency is not None else 0,
146
+ )
133
147
 
134
148
  # --- Phase A: query reformulation + embedding generation ---
135
149
  reformulated_query, embedding = _run_phase_a(
@@ -153,6 +167,7 @@ def run_unified_search(
153
167
  query=reformulated_query,
154
168
  top_k=fetch_k,
155
169
  threshold=threshold,
170
+ recency_on=recency_on,
156
171
  )
157
172
 
158
173
  if profiles is None:
@@ -166,7 +181,28 @@ def run_unified_search(
166
181
  user_playbooks=user_playbooks, # type: ignore[arg-type]
167
182
  top_k=top_k,
168
183
  cfg=floor_cfg,
184
+ recency=recency if recency_on else None,
185
+ )
186
+ elif recency_on and recency is not None:
187
+ profiles = _apply_combined_score_recency(
188
+ profiles or [], entity_type="profiles", top_k=top_k, cfg=recency
189
+ )
190
+ agent_playbooks = _apply_combined_score_recency(
191
+ agent_playbooks or [],
192
+ entity_type="agent_playbooks",
193
+ top_k=top_k,
194
+ cfg=recency,
195
+ )
196
+ user_playbooks = _apply_combined_score_recency(
197
+ user_playbooks or [],
198
+ entity_type="user_playbooks",
199
+ top_k=top_k,
200
+ cfg=recency,
169
201
  )
202
+ else:
203
+ profiles = _unwrap_items(profiles or [])[:top_k]
204
+ agent_playbooks = _unwrap_items(agent_playbooks or [])[:top_k]
205
+ user_playbooks = _unwrap_items(user_playbooks or [])[:top_k]
170
206
 
171
207
  user_playbooks = _suppress_source_user_playbooks(
172
208
  storage=storage,
@@ -284,10 +320,11 @@ def _run_phase_b(
284
320
  query: str,
285
321
  top_k: int,
286
322
  threshold: float,
323
+ recency_on: bool = False,
287
324
  ) -> tuple[
288
- list[UserProfile] | None,
289
- list[AgentPlaybook] | None,
290
- list[UserPlaybook] | None,
325
+ list[Any] | None,
326
+ list[Any] | None,
327
+ list[Any] | None,
291
328
  ]:
292
329
  """Run parallel searches across all entity types by delegating to storage methods.
293
330
 
@@ -314,9 +351,18 @@ def _run_phase_b(
314
351
  entity_types=sorted(entity_types),
315
352
  top_k=top_k,
316
353
  ) as span:
317
- if (
354
+ # Recency needs the per-row ``combined_score``, which only the scored
355
+ # single-RPC method threads back. Backends that don't advertise
356
+ # ``supports_unified_hybrid_search`` (e.g. native Postgres, which still
357
+ # inherits ``unified_hybrid_search_scored`` and runs it via the same
358
+ # ``_rpc`` it already uses for ``hybrid_match_*``) opt into the scored
359
+ # path only when recency is on, so non-recency routing is unchanged.
360
+ wants_scored_single_rpc = recency_on and callable(
361
+ getattr(storage, "unified_hybrid_search_scored", None)
362
+ )
363
+ if _unified_single_rpc_enabled() and (
318
364
  getattr(storage, "supports_unified_hybrid_search", False)
319
- and _unified_single_rpc_enabled()
365
+ or wants_scored_single_rpc
320
366
  ):
321
367
  combined = _run_phase_b_single_rpc(
322
368
  request=request,
@@ -327,6 +373,7 @@ def _run_phase_b(
327
373
  threshold=threshold,
328
374
  entity_types=entity_types,
329
375
  allowed_agent_statuses=allowed_agent_statuses,
376
+ recency_on=recency_on,
330
377
  )
331
378
  if combined is not None:
332
379
  profiles, agent_playbooks, user_playbooks = combined
@@ -437,7 +484,8 @@ def _run_phase_b_single_rpc(
437
484
  threshold: float,
438
485
  entity_types: set[str],
439
486
  allowed_agent_statuses: list[PlaybookStatus] | None,
440
- ) -> tuple[list[UserProfile], list[AgentPlaybook], list[UserPlaybook]] | None:
487
+ recency_on: bool = False,
488
+ ) -> tuple[list[Any], list[Any], list[Any]] | None:
441
489
  """Run all Phase B arms through one combined storage round trip.
442
490
 
443
491
  Trades the per-arm round-trip overhead for serialized execution of the
@@ -458,8 +506,16 @@ def _run_phase_b_single_rpc(
458
506
  )
459
507
  # Resolve storage.unified_hybrid_search before submit so missing or stale
460
508
  # capability flags can fall back to the fan-out path.
461
- unified_hybrid_search = getattr(storage, "unified_hybrid_search", None)
509
+ method_name = (
510
+ "unified_hybrid_search_scored" if recency_on else "unified_hybrid_search"
511
+ )
512
+ unified_hybrid_search = getattr(storage, method_name, None)
462
513
  if not callable(unified_hybrid_search):
514
+ if recency_on:
515
+ logger.warning(
516
+ "event=search_recency_missing_scores source=single_rpc method=%s",
517
+ method_name,
518
+ )
463
519
  return None
464
520
 
465
521
  future = _submit_with_current_context(
@@ -490,13 +546,14 @@ def _run_phase_b_single_rpc(
490
546
  return None
491
547
 
492
548
  # Mirror _search_agent_playbooks_via_storage: dedupe by id, cap at top_k.
493
- deduped: list[AgentPlaybook] = []
549
+ deduped: list[Any] = []
494
550
  seen_ids: set[str] = set()
495
- for playbook in agent_playbooks:
551
+ for candidate in agent_playbooks:
552
+ playbook = _unwrap_item(candidate)
496
553
  playbook_id = str(getattr(playbook, "agent_playbook_id", ""))
497
554
  if playbook_id and playbook_id not in seen_ids:
498
555
  seen_ids.add(playbook_id)
499
- deduped.append(playbook)
556
+ deduped.append(candidate)
500
557
  if len(deduped) >= top_k:
501
558
  break
502
559
  return profiles, deduped, user_playbooks
@@ -509,6 +566,7 @@ def _apply_floors(
509
566
  user_playbooks: list[UserPlaybook],
510
567
  top_k: int,
511
568
  cfg: RetrievalFloorConfig,
569
+ recency: RecencyConfig | None = None,
512
570
  ) -> tuple[list[UserProfile], list[AgentPlaybook], list[UserPlaybook]]:
513
571
  """Apply the per-arm relevance floor with one batched cross-encoder call."""
514
572
  floored_profiles, floored_agent, floored_user = apply_relevance_floors(
@@ -519,8 +577,91 @@ def _apply_floors(
519
577
  ("user_playbooks", user_playbooks, cfg.user_playbook_floor),
520
578
  ],
521
579
  top_k,
580
+ content_of=lambda item: _unwrap_item(item).content,
522
581
  )
523
- return floored_profiles, floored_agent, floored_user
582
+ return (
583
+ _finalize_floor_arm(
584
+ floored_profiles, entity_type="profiles", top_k=top_k, recency=recency
585
+ ),
586
+ _finalize_floor_arm(
587
+ floored_agent,
588
+ entity_type="agent_playbooks",
589
+ top_k=top_k,
590
+ recency=recency,
591
+ ),
592
+ _finalize_floor_arm(
593
+ floored_user,
594
+ entity_type="user_playbooks",
595
+ top_k=top_k,
596
+ recency=recency,
597
+ ),
598
+ )
599
+
600
+
601
+ def _finalize_floor_arm(
602
+ result: Any,
603
+ *,
604
+ entity_type: str,
605
+ top_k: int,
606
+ recency: RecencyConfig | None,
607
+ ) -> list[Any]:
608
+ if not recency or not recency.enabled:
609
+ return _unwrap_items(result.items)[:top_k]
610
+ if result.scores is None:
611
+ return _apply_combined_score_recency(
612
+ result.items, entity_type=entity_type, top_k=top_k, cfg=recency
613
+ )
614
+ now = int(datetime.now(UTC).timestamp())
615
+ rescored = []
616
+ for item, score in zip(result.items, result.scores, strict=True):
617
+ unwrapped = _unwrap_item(item)
618
+ freshness = decay_for_item(unwrapped, entity_type=entity_type, now=now)
619
+ rescored.append(
620
+ (unwrapped, score - additive_penalty(freshness, recency.max_penalty_logit))
621
+ )
622
+ rescored.sort(key=lambda pair: pair[1], reverse=True)
623
+ return [item for item, _score in rescored[:top_k]]
624
+
625
+
626
+ def _apply_combined_score_recency(
627
+ items: list[Any],
628
+ *,
629
+ entity_type: str,
630
+ top_k: int,
631
+ cfg: RecencyConfig,
632
+ ) -> list[Any]:
633
+ if not items:
634
+ return []
635
+ scored_items: list[tuple[Any, float]] = []
636
+ for item in items:
637
+ if not isinstance(item, ScoredItem) or item.score is None:
638
+ logger.warning(
639
+ "event=search_recency_missing_scores entity_type=%s items=%d",
640
+ entity_type,
641
+ len(items),
642
+ )
643
+ return _unwrap_items(items)[:top_k]
644
+ scored_items.append((item.item, item.score))
645
+ now = int(datetime.now(UTC).timestamp())
646
+ rescored = []
647
+ for item, score in scored_items:
648
+ freshness = decay_for_item(item, entity_type=entity_type, now=now)
649
+ rescored.append(
650
+ (
651
+ item,
652
+ score * multiplicative_factor(freshness, cfg.max_penalty_frac),
653
+ )
654
+ )
655
+ rescored.sort(key=lambda pair: pair[1], reverse=True)
656
+ return [item for item, _score in rescored[:top_k]]
657
+
658
+
659
+ def _unwrap_item(item: Any) -> Any:
660
+ return item.item if isinstance(item, ScoredItem) else item
661
+
662
+
663
+ def _unwrap_items(items: list[Any]) -> list[Any]:
664
+ return [_unwrap_item(item) for item in items]
524
665
 
525
666
 
526
667
  def _suppress_source_user_playbooks(
@@ -1,84 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Callable
4
- from dataclasses import dataclass, field
5
- from typing import Protocol
6
-
7
-
8
- @dataclass(frozen=True)
9
- class DetectedEntity:
10
- start: int
11
- end: int
12
- entity_type: str
13
- replacement: str
14
- confidence: float
15
- source: str
16
-
17
-
18
- @dataclass(frozen=True)
19
- class StrippingResult:
20
- text: str
21
- detections: list[DetectedEntity] = field(default_factory=list)
22
-
23
-
24
- class UserDetailDetector(Protocol):
25
- def detect(self, text: str) -> list[DetectedEntity]: ...
26
-
27
-
28
- class UserDetailStripper(Protocol):
29
- prompt_extra_instructions: str | None
30
-
31
- def strip_user_details(
32
- self,
33
- text: str,
34
- shared_mapping: dict[str, int] | None = None,
35
- ) -> StrippingResult: ...
36
-
37
- def sanitize_aggregation_output_text(
38
- self,
39
- text: str | None,
40
- ) -> tuple[str | None, int]: ...
41
-
42
-
43
- class PassthroughStripper:
44
- prompt_extra_instructions: str | None = None
45
-
46
- def strip_user_details(
47
- self,
48
- text: str,
49
- shared_mapping: dict[str, int] | None = None, # noqa: ARG002
50
- ) -> StrippingResult:
51
- return StrippingResult(text=text, detections=[])
52
-
53
- def sanitize_aggregation_output_text(
54
- self,
55
- text: str | None,
56
- ) -> tuple[str | None, int]:
57
- return text, 0
58
-
59
-
60
- UserDetailStripperFactory = Callable[[object], UserDetailStripper | None]
61
-
62
-
63
- def _default_user_detail_stripper_factory(
64
- _configurator: object,
65
- ) -> UserDetailStripper | None:
66
- return None
67
-
68
-
69
- _user_detail_stripper_factory: UserDetailStripperFactory = (
70
- _default_user_detail_stripper_factory
71
- )
72
-
73
-
74
- def set_user_detail_stripper_factory(factory: UserDetailStripperFactory) -> None:
75
- """Register the deployment-specific aggregation stripper factory."""
76
- global _user_detail_stripper_factory # noqa: PLW0603
77
- _user_detail_stripper_factory = factory
78
-
79
-
80
- def create_aggregation_user_detail_stripper(
81
- configurator: object,
82
- ) -> UserDetailStripper | None:
83
- """Create the deployment-specific stripper for aggregation, if any."""
84
- return _user_detail_stripper_factory(configurator)