claude-smart 0.2.49 → 0.2.50

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 (177) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +10 -43
  3. package/bin/claude-smart.js +105 -0
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/pyproject.toml +1 -1
  27. package/plugin/scripts/_lib.sh +197 -1
  28. package/plugin/scripts/backend-python-runner.py +46 -0
  29. package/plugin/scripts/backend-service.sh +757 -119
  30. package/plugin/scripts/codex-hook.js +63 -225
  31. package/plugin/scripts/dashboard-open.sh +6 -4
  32. package/plugin/scripts/dashboard-service.sh +117 -136
  33. package/plugin/scripts/hook_entry.sh +3 -0
  34. package/plugin/scripts/smart-install.sh +15 -1
  35. package/plugin/src/claude_smart/cli.py +14 -0
  36. package/plugin/src/claude_smart/context_inject.py +3 -0
  37. package/plugin/src/claude_smart/env_config.py +4 -1
  38. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  39. package/plugin/src/claude_smart/events/session_end.py +2 -1
  40. package/plugin/src/claude_smart/events/stop.py +3 -0
  41. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  42. package/plugin/src/claude_smart/internal_call.py +5 -2
  43. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  44. package/plugin/src/claude_smart/publish.py +59 -7
  45. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  46. package/plugin/src/claude_smart/runtime.py +15 -6
  47. package/plugin/src/claude_smart/state.py +211 -52
  48. package/plugin/uv.lock +1 -1
  49. package/plugin/vendor/reflexio/.env.example +13 -0
  50. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  51. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  52. package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
  53. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  54. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  55. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  56. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
  57. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  58. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  59. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  60. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  61. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  62. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  63. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  64. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  65. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
  67. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  68. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  69. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  70. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  71. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
  72. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  73. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  74. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  75. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  76. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  77. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
  78. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  79. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  80. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  81. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  82. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  83. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  84. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  85. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  86. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  90. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  91. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  92. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  93. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  94. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  95. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  96. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  99. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  100. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  101. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  103. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  104. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  105. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  106. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
  109. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  110. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  111. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  112. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  113. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  114. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  115. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  116. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
  118. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  120. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  122. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  124. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  125. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  126. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  127. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  128. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  129. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  130. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  131. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  132. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  133. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
  134. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  135. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  139. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  172. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  173. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  174. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  175. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  176. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -233,3 +233,31 @@ def test_safe_session_ids_accepted():
233
233
  path = state.session_path(sid)
234
234
  assert path is not None, f"safe id {sid!r} should have been accepted"
235
235
  assert path.name == f"{sid}.jsonl"
236
+
237
+
238
+ def test_to_wire_citations_preserves_explicit_playbook_source_kind() -> None:
239
+ wire = state._to_wire_citations(
240
+ [
241
+ {
242
+ "id": "s1-1",
243
+ "kind": "playbook",
244
+ "source_kind": "agent_playbook",
245
+ "real_id": "20",
246
+ "title": "Agent",
247
+ },
248
+ {
249
+ "id": "s2-1",
250
+ "kind": "playbook",
251
+ "source_kind": "user_playbook",
252
+ "real_id": "101",
253
+ "title": "User",
254
+ },
255
+ {"id": "p1-1", "kind": "profile", "real_id": "p1", "title": "Profile"},
256
+ ]
257
+ )
258
+
259
+ assert [item["kind"] for item in wire] == [
260
+ "agent_playbook",
261
+ "user_playbook",
262
+ "profile",
263
+ ]
@@ -6,6 +6,8 @@ from reflexio.models.api_schema.retriever_schema import (
6
6
  GetAgentSuccessEvaluationResultsResponse,
7
7
  GetRequestsRequest,
8
8
  GetRequestsResponse,
9
+ GetRetrievedLearningEvaluationResultsRequest,
10
+ GetRetrievedLearningEvaluationResultsResponse,
9
11
  RequestData,
10
12
  Session,
11
13
  UnifiedSearchRequest,
@@ -72,6 +74,45 @@ class SearchMixin(ReflexioBase):
72
74
  success=False, agent_success_evaluation_results=[], msg=str(e)
73
75
  )
74
76
 
77
+ def get_retrieved_learning_evaluation_results(
78
+ self,
79
+ request: GetRetrievedLearningEvaluationResultsRequest | dict,
80
+ ) -> GetRetrievedLearningEvaluationResultsResponse:
81
+ """Get per-learning retrieved-learning evaluation verdicts.
82
+
83
+ Args:
84
+ request (GetRetrievedLearningEvaluationResultsRequest | dict): The
85
+ read request (optional user/session filters + limit).
86
+
87
+ Returns:
88
+ GetRetrievedLearningEvaluationResultsResponse: Matching verdicts
89
+ ordered by created_at DESC, result_id DESC.
90
+ """
91
+ if not self._is_storage_configured():
92
+ return GetRetrievedLearningEvaluationResultsResponse(
93
+ success=True, results=[], msg=STORAGE_NOT_CONFIGURED_MSG
94
+ )
95
+ if isinstance(request, dict):
96
+ request = GetRetrievedLearningEvaluationResultsRequest(**request)
97
+ try:
98
+ results = self._get_storage().get_retrieved_learning_evaluation_results(
99
+ user_id=request.user_id,
100
+ session_id=request.session_id,
101
+ limit=request.limit,
102
+ )
103
+ return GetRetrievedLearningEvaluationResultsResponse(
104
+ success=True,
105
+ results=results,
106
+ msg=f"Found {len(results)} retrieved-learning evaluation result(s)",
107
+ )
108
+ except Exception:
109
+ _LOGGER.exception("Failed to read retrieved-learning evaluation results")
110
+ return GetRetrievedLearningEvaluationResultsResponse(
111
+ success=False,
112
+ results=[],
113
+ msg="Failed to read retrieved-learning evaluation results",
114
+ )
115
+
75
116
  def get_requests(
76
117
  self,
77
118
  request: GetRequestsRequest | dict,
@@ -33,7 +33,11 @@ __all__ = [
33
33
  "BlockingIssue",
34
34
  "BlockingIssueKind",
35
35
  "ToolUsed",
36
+ "CitationKind",
36
37
  "Citation",
38
+ "RetrievedLearningKind",
39
+ "RetrievedLearning",
40
+ "LearningImpact",
37
41
  "Interaction",
38
42
  "Request",
39
43
  "UserProfile",
@@ -41,6 +45,7 @@ __all__ = [
41
45
  "ProfileChangeLog",
42
46
  "AgentPlaybook",
43
47
  "AgentSuccessEvaluationResult",
48
+ "RetrievedLearningEvaluationResult",
44
49
  "DeleteUserProfileRequest",
45
50
  "DeleteUserProfileResponse",
46
51
  "DeleteUserInteractionRequest",
@@ -130,6 +135,8 @@ __all__ = [
130
135
  # Data Models
131
136
  # ===============================
132
137
 
138
+ type CitationKind = Literal["playbook", "profile", "user_playbook", "agent_playbook"]
139
+
133
140
 
134
141
  class Citation(BaseModel):
135
142
  """A playbook or profile item the agent cited as influential.
@@ -141,22 +148,52 @@ class Citation(BaseModel):
141
148
  it was applied?).
142
149
 
143
150
  Attributes:
144
- kind (Literal["playbook", "profile"]): Which kind of cited
145
- item this references.
151
+ kind (CitationKind): Which kind of cited item this references.
152
+ ``"playbook"`` is the legacy compatibility value.
153
+ ``"user_playbook"`` is the direct tuner target.
154
+ ``"agent_playbook"`` references an org-level playbook row.
155
+ ``"profile"`` references a user profile row.
146
156
  real_id (str): Stable storage id — ``user_playbook_id`` for
147
- playbooks, ``profile_id`` for profiles.
157
+ user playbooks, ``agent_playbook_id`` for agent playbooks,
158
+ and ``profile_id`` for profiles.
148
159
  tag (str): Injection-time rank tag (e.g. ``"r1-301"``,
149
160
  ``"p1-0f37"``). Per-injection, not stable across sessions;
150
161
  kept as a debug aid.
151
162
  title (str): Short human-readable label for logs and UI.
152
163
  """
153
164
 
154
- kind: Literal["playbook", "profile"]
165
+ kind: CitationKind
155
166
  real_id: str
156
167
  tag: str = ""
157
168
  title: str = ""
158
169
 
159
170
 
171
+ # Canonical kinds accepted and persisted by retrieved-learning evaluation.
172
+ # Unlike ``Citation.kind`` there is no legacy ``"playbook"`` alias.
173
+ type RetrievedLearningKind = Literal["profile", "user_playbook", "agent_playbook"]
174
+
175
+ type LearningImpact = Literal["positive", "negative", "neutral"]
176
+
177
+
178
+ class RetrievedLearning(BaseModel):
179
+ """A learning the caller retrieved and injected into the agent context.
180
+
181
+ Deliberately minimal — just the identity pair. It does NOT reuse
182
+ ``Citation``: citations carry injection-time debug fields (``tag``,
183
+ ``title``) that callers should not need to supply (or see) when declaring
184
+ what was retrieved.
185
+
186
+ Attributes:
187
+ kind (RetrievedLearningKind): Which kind of learning this references.
188
+ learning_id (str): Stable storage id — ``profile_id`` for profiles,
189
+ ``user_playbook_id`` for user playbooks, ``agent_playbook_id``
190
+ for agent playbooks (numeric ids as decimal strings).
191
+ """
192
+
193
+ kind: RetrievedLearningKind
194
+ learning_id: str = Field(min_length=1, max_length=1_000)
195
+
196
+
160
197
  # information about the user interaction sent by the client
161
198
  class Interaction(BaseModel):
162
199
  interaction_id: int = 0 # 0 = placeholder for DB auto-increment
@@ -173,6 +210,10 @@ class Interaction(BaseModel):
173
210
  expert_content: str = ""
174
211
  tools_used: list[ToolUsed] = Field(default_factory=list)
175
212
  citations: list[Citation] = Field(default_factory=list)
213
+ # Every learning retrieved and injected for this turn — including ones
214
+ # that did not end up influencing the response (contrast: ``citations``
215
+ # is the agent's claim of influence).
216
+ retrieved_learnings: list[RetrievedLearning] = Field(default_factory=list)
176
217
  embedding: EmbeddingVector = []
177
218
 
178
219
  @field_validator("interacted_image_url", mode="after")
@@ -399,17 +440,107 @@ class AgentSuccessEvaluationResult(BaseModel):
399
440
  embedding: EmbeddingVector = []
400
441
 
401
442
 
443
+ class RetrievedLearningEvaluationResult(BaseModel):
444
+ """Latest per-learning relevance/impact verdict for one evaluated session.
445
+
446
+ One row per ``(user_id, session_id, kind, learning_id)``. The table holds
447
+ the most recent successfully persisted evaluation set for a session, not
448
+ an append-only history.
449
+
450
+ Attributes:
451
+ result_id (int): DB auto-increment identifier (0 = placeholder).
452
+ user_id (str): Session owner.
453
+ session_id (str): Evaluated session.
454
+ agent_version (str): Version supplied to group evaluation;
455
+ informational, not part of the uniqueness key.
456
+ kind (RetrievedLearningKind): The learning kind.
457
+ learning_id (str): Stable storage id, matching
458
+ ``RetrievedLearning.learning_id``.
459
+ is_relevant (bool | None): Whether the learning applies to the
460
+ session. ``None`` only when the relevance judge/chunk failed.
461
+ relevance_reason (str): Judge reasoning; empty when ``is_relevant``
462
+ is ``None``.
463
+ impact (LearningImpact | None): Whether the learning improved,
464
+ harmed, or did not materially change the response. ``None`` only
465
+ when the impact judge/chunk failed.
466
+ impact_reason (str): Judge reasoning; empty when ``impact`` is
467
+ ``None``.
468
+ created_at (int): Earliest request timestamp in the evaluated
469
+ session.
470
+ """
471
+
472
+ result_id: int = 0
473
+ user_id: str
474
+ session_id: str
475
+ agent_version: str = ""
476
+ kind: RetrievedLearningKind
477
+ learning_id: str
478
+ is_relevant: bool | None = None
479
+ relevance_reason: str = ""
480
+ impact: LearningImpact | None = None
481
+ impact_reason: str = ""
482
+ created_at: int = Field(default_factory=lambda: int(datetime.now(UTC).timestamp()))
483
+
484
+
402
485
  class PlaybookRetrievalLogItem(BaseModel):
403
- """One retrieved agent playbook plus the serve-time attribution snapshot."""
486
+ """One retrieved playbook plus the serve-time attribution snapshot.
487
+
488
+ ``agent_playbook_id`` remains as a nullable legacy compatibility field for
489
+ old agent-only rows. New rows should prefer ``target_kind`` + ``target_id``.
490
+ """
404
491
 
405
492
  retrieval_log_item_id: int = 0
406
493
  retrieval_log_id: int = 0
407
494
  ordinal: int
408
- agent_playbook_id: int
495
+ agent_playbook_id: int | None = Field(default=None, gt=0)
496
+ target_kind: Literal["agent_playbook", "user_playbook"] | None = None
497
+ target_id: int | None = Field(default=None, gt=0)
498
+ target_title: str = ""
499
+ target_content: str = ""
500
+ target_trigger: str | None = None
409
501
  source_user_playbook_ids: list[int] = Field(default_factory=list)
410
502
  source_interaction_ids_by_user_playbook_id: dict[str, list[int]] = Field(
411
503
  default_factory=dict
412
504
  )
505
+ source_window_snapshot_mode: Literal["final_response", "live_lookup_compat"] = (
506
+ "live_lookup_compat"
507
+ )
508
+
509
+ @model_validator(mode="after")
510
+ def backfill_legacy_target_fields(self) -> Self:
511
+ if self.target_kind is None:
512
+ if self.agent_playbook_id is None:
513
+ raise ValueError(
514
+ "PlaybookRetrievalLogItem requires target_kind/target_id or"
515
+ " legacy agent_playbook_id"
516
+ )
517
+ self.target_kind = "agent_playbook"
518
+ self.target_id = self.agent_playbook_id
519
+ return self
520
+
521
+ if self.target_id is None:
522
+ if self.target_kind != "agent_playbook" or self.agent_playbook_id is None:
523
+ raise ValueError(
524
+ "PlaybookRetrievalLogItem.target_id is required for explicit"
525
+ " targets"
526
+ )
527
+ self.target_id = self.agent_playbook_id
528
+
529
+ if self.target_kind == "user_playbook":
530
+ if self.agent_playbook_id is not None:
531
+ raise ValueError(
532
+ "user_playbook retrieval-log items cannot carry agent_playbook_id"
533
+ )
534
+ return self
535
+
536
+ if self.agent_playbook_id is None:
537
+ self.agent_playbook_id = self.target_id
538
+ elif self.agent_playbook_id != self.target_id:
539
+ raise ValueError(
540
+ "agent_playbook retrieval-log item agent_playbook_id must match"
541
+ " target_id"
542
+ )
543
+ return self
413
544
 
414
545
 
415
546
  class PlaybookRetrievalLog(BaseModel):
@@ -599,19 +730,19 @@ class BulkDeleteResponse(BaseModel):
599
730
 
600
731
 
601
732
  class DeleteRequestsByIdsRequest(BaseModel):
602
- request_ids: list[str] = Field(min_length=1)
733
+ request_ids: list[str] = Field(min_length=1, max_length=10_000)
603
734
 
604
735
 
605
736
  class DeleteProfilesByIdsRequest(BaseModel):
606
- profile_ids: list[str] = Field(min_length=1)
737
+ profile_ids: list[str] = Field(min_length=1, max_length=10_000)
607
738
 
608
739
 
609
740
  class DeleteAgentPlaybooksByIdsRequest(BaseModel):
610
- agent_playbook_ids: list[int] = Field(min_length=1)
741
+ agent_playbook_ids: list[int] = Field(min_length=1, max_length=10_000)
611
742
 
612
743
 
613
744
  class DeleteUserPlaybooksByIdsRequest(BaseModel):
614
- user_playbook_ids: list[int] = Field(min_length=1)
745
+ user_playbook_ids: list[int] = Field(min_length=1, max_length=10_000)
615
746
 
616
747
 
617
748
  # Clear all data scoped to a single user_id (interactions, requests, user
@@ -632,16 +763,25 @@ class ClearUserDataResponse(BaseModel):
632
763
  # user provided interaction data from the request
633
764
  class InteractionData(BaseModel):
634
765
  created_at: int = Field(default_factory=lambda: int(datetime.now(UTC).timestamp()))
635
- role: str = "User"
636
- content: str = ""
637
- shadow_content: str = ""
638
- expert_content: str = ""
766
+ role: str = Field(default="User", max_length=1_000)
767
+ content: str = Field(default="", max_length=1_000_000)
768
+ shadow_content: str = Field(default="", max_length=1_000_000)
769
+ expert_content: str = Field(default="", max_length=1_000_000)
639
770
  user_action: UserActionType = UserActionType.NONE
640
- user_action_description: str = ""
641
- interacted_image_url: str = ""
642
- image_encoding: str = "" # base64 encoded image
643
- tools_used: list[ToolUsed] = Field(default_factory=list)
644
- citations: list[Citation] = Field(default_factory=list)
771
+ user_action_description: str = Field(default="", max_length=10_000)
772
+ interacted_image_url: str = Field(default="", max_length=2_048)
773
+ image_encoding: str = Field(
774
+ default="", max_length=15_000_000
775
+ ) # base64 encoded image
776
+ tools_used: list[ToolUsed] = Field(default_factory=list, max_length=1_000)
777
+ citations: list[Citation] = Field(default_factory=list, max_length=1_000)
778
+ # Learnings (profiles / user playbooks / agent playbooks) the caller
779
+ # retrieved and injected into the agent context for this turn. Distinct
780
+ # from ``citations`` (agent-claimed influence): this is everything that
781
+ # was injected, whether or not it helped.
782
+ retrieved_learnings: list[RetrievedLearning] = Field(
783
+ default_factory=list, max_length=1_000
784
+ )
645
785
 
646
786
  @field_validator("interacted_image_url", mode="after")
647
787
  @classmethod
@@ -653,11 +793,10 @@ class InteractionData(BaseModel):
653
793
  class PublishUserInteractionRequest(BaseModel):
654
794
  request_id: NonEmptyStr | None = None
655
795
  user_id: NonEmptyStr
656
- interaction_data_list: list[InteractionData] = Field(min_length=1)
657
- source: str = ""
658
- agent_version: str = (
659
- "" # this is used for aggregating interactions for generating agent playbooks
660
- )
796
+ interaction_data_list: list[InteractionData] = Field(min_length=1, max_length=1_000)
797
+ source: str = Field(default="", max_length=1_000)
798
+ # this is used for aggregating interactions for generating agent playbooks
799
+ agent_version: str = Field(default="", max_length=1_000)
661
800
  session_id: NonEmptyStr # used for grouping requests together
662
801
  skip_aggregation: bool = (
663
802
  False # when True, extract profiles/playbooks but skip aggregation
@@ -674,6 +813,19 @@ class PublishUserInteractionRequest(BaseModel):
674
813
  raise ValueError("evaluation_only publishes require session_id")
675
814
  return self
676
815
 
816
+ @model_validator(mode="after")
817
+ def validate_retrieved_learnings_total(self) -> Self:
818
+ total = sum(
819
+ len(interaction.retrieved_learnings)
820
+ for interaction in self.interaction_data_list
821
+ )
822
+ if total > 1_000:
823
+ raise ValueError(
824
+ "a publish request may carry at most 1000 retrieved_learnings"
825
+ f" across all interactions (got {total})"
826
+ )
827
+ return self
828
+
677
829
 
678
830
  # publish user interaction response
679
831
  class PublishUserInteractionResponse(BaseModel):
@@ -730,7 +882,7 @@ class MyConfigResponse(BaseModel):
730
882
 
731
883
  # add user playbook request/response
732
884
  class AddUserPlaybookRequest(BaseModel):
733
- user_playbooks: list[UserPlaybook] = Field(min_length=1)
885
+ user_playbooks: list[UserPlaybook] = Field(min_length=1, max_length=1_000)
734
886
 
735
887
  @model_validator(mode="after")
736
888
  def check_content_fields(self) -> Self:
@@ -11,6 +11,7 @@ from typing import Literal, Self
11
11
 
12
12
  from pydantic import BaseModel, ConfigDict, Field, model_validator
13
13
 
14
+ from reflexio.models.api_schema.domain import CitationKind
14
15
  from reflexio.models.api_schema.validators import NonEmptyStr
15
16
  from reflexio.models.structured_output import StrictStructuredOutput
16
17
 
@@ -79,7 +80,7 @@ class RuleAttributionRow(BaseModel):
79
80
  """One row in the "rules that moved the needle" panel."""
80
81
 
81
82
  rule_id: str
82
- kind: Literal["playbook", "profile"]
83
+ kind: CitationKind
83
84
  title: str = ""
84
85
  successes_with: int = Field(ge=0)
85
86
  failures_with: int = Field(ge=0)
@@ -416,12 +417,17 @@ class GradeOnDemandResponse(BaseModel):
416
417
  window. False on a fresh grade.
417
418
  skipped_reason (str | None): If grading was skipped, the reason
418
419
  (e.g., "NO_REQUESTS"). None on success.
420
+ retrieved_learning_status (str | None): Outcome of the
421
+ retrieved-learning evaluation for this session (e.g.
422
+ "complete", "degraded", "failed", "not_applicable"). None on
423
+ legacy cache entries and skipped grades.
419
424
  """
420
425
 
421
426
  session_id: str
422
427
  result_id: int | None = None
423
428
  cached: bool = False
424
429
  skipped_reason: str | None = None
430
+ retrieved_learning_status: str | None = None
425
431
 
426
432
 
427
433
  # ---------------------------------------------------------------------------
@@ -6,6 +6,7 @@ from typing import NamedTuple
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
+ from .domain import CitationKind
9
10
  from .service_schemas import Interaction, Request
10
11
 
11
12
 
@@ -42,6 +43,6 @@ class SessionCitation(NamedTuple):
42
43
 
43
44
  user_id: str
44
45
  session_id: str
45
- kind: str
46
+ kind: CitationKind
46
47
  real_id: str
47
48
  title: str
@@ -6,12 +6,15 @@ from typing import Literal, Self
6
6
  from pydantic import BaseModel, Field, model_validator
7
7
 
8
8
  from ..config_schema import SearchMode
9
+ from ..structured_output import StrictStructuredOutput
10
+ from .domain import CitationKind
9
11
  from .service_schemas import (
10
12
  AgentPlaybook,
11
13
  AgentSuccessEvaluationResult,
12
14
  Interaction,
13
15
  PlaybookStatus,
14
16
  Request,
17
+ RetrievedLearningEvaluationResult,
15
18
  Status,
16
19
  UserPlaybook,
17
20
  UserProfile,
@@ -433,6 +436,26 @@ class GetAgentSuccessEvaluationResultsResponse(BaseModel):
433
436
  msg: str | None = None
434
437
 
435
438
 
439
+ class GetRetrievedLearningEvaluationResultsRequest(BaseModel):
440
+ """Read per-learning retrieved-learning evaluation verdicts.
441
+
442
+ Attributes:
443
+ user_id (str | None): Filter by session owner.
444
+ session_id (str | None): Filter by session.
445
+ limit (int): Maximum rows, ordered by created_at DESC, result_id DESC.
446
+ """
447
+
448
+ user_id: str | None = None
449
+ session_id: str | None = None
450
+ limit: int = Field(default=100, ge=1, le=1_000)
451
+
452
+
453
+ class GetRetrievedLearningEvaluationResultsResponse(BaseModel):
454
+ success: bool
455
+ results: list[RetrievedLearningEvaluationResult] = Field(default_factory=list)
456
+ msg: str | None = None
457
+
458
+
436
459
  class GetRequestsRequest(BaseModel):
437
460
  user_id: str | None = None
438
461
  request_id: str | None = None
@@ -593,8 +616,10 @@ class PlaybookApplicationStat(BaseModel):
593
616
  real_id (str): Stable id of the cited item — ``user_playbook_id``,
594
617
  ``agent_playbook_id``, or ``profile_id`` (always serialized as a
595
618
  string).
596
- kind (str): ``"playbook"`` or ``"profile"`` — the citation kind, as
597
- recorded on ``Interaction.citations``.
619
+ kind (CitationKind): Citation kind recorded on
620
+ ``Interaction.citations``. ``"playbook"`` is the legacy
621
+ compatibility value; ``"user_playbook"`` is the explicit
622
+ direct-tuner target.
598
623
  title (str): Human-readable label for the rule. Empty string when
599
624
  the underlying row has been deleted but old citations remain.
600
625
  applied_count (int): Number of interactions in the window whose
@@ -609,7 +634,7 @@ class PlaybookApplicationStat(BaseModel):
609
634
  """
610
635
 
611
636
  real_id: str
612
- kind: Literal["playbook", "profile"]
637
+ kind: CitationKind
613
638
  title: str = ""
614
639
  applied_count: int = Field(ge=0)
615
640
  last_applied_at: int | None = None
@@ -659,15 +684,36 @@ class ConversationTurn(BaseModel):
659
684
  content: NonEmptyStr
660
685
 
661
686
 
662
- class ReformulationResult(BaseModel):
687
+ class ReformulationResult(StrictStructuredOutput):
663
688
  """Output of the query reformulation pipeline.
664
689
 
690
+ Besides the rewritten query, carries the query's TEMPORAL SIGNALS so the
691
+ search pipeline can be time-sensitive without any additional LLM call
692
+ (the reformulation call already runs before retrieval when
693
+ ``enable_reformulation`` is set). Time windows are relative day offsets
694
+ (never absolute dates) so results don't rot with calendar time.
695
+
665
696
  Args:
666
697
  standalone_query (str): Clean, normalized natural language query with
667
698
  conversation context resolved, abbreviations expanded, grammar fixed.
699
+ start_days_ago (float, optional): Older bound of a query time window
700
+ ("in the last 7 days" → 7).
701
+ end_days_ago (float, optional): Newer bound ("before this month" →
702
+ ~30, with no start bound).
703
+ recency_dominant (bool): The query asks for the CURRENT/LATEST value —
704
+ final ordering becomes timestamp-based.
705
+ wants_current (bool): Present-tense question about a mutable
706
+ fact/policy — near-duplicate competing facts collapse to the
707
+ freshest, while relevance ordering is otherwise preserved.
708
+ (Superseded/TTL-expired rows never reach results: storage search
709
+ excludes tombstone statuses and expired profiles at SQL level.)
668
710
  """
669
711
 
670
712
  standalone_query: str
713
+ start_days_ago: float | None = Field(default=None, ge=0)
714
+ end_days_ago: float | None = Field(default=None, ge=0)
715
+ recency_dominant: bool = False
716
+ wants_current: bool = False
671
717
 
672
718
 
673
719
  # ===============================
@@ -712,6 +758,9 @@ class UnifiedSearchRequest(BaseModel):
712
758
  search_mode: SearchMode = SearchMode.HYBRID
713
759
  # Caller correlation IDs for billing attribution on the Application line.
714
760
  # Optional; consumed by _meter_applied_learnings in server/api.py.
761
+ # ``session_id`` additionally enables session-scoped result dedup: items
762
+ # already served to the same (org, session) are skipped and the next-best
763
+ # matches backfilled (see server/services/retrieval/session_dedup.py).
715
764
  request_id: str | None = None
716
765
  session_id: str | None = None
717
766
  interaction_id: int | None = Field(default=None, gt=0)
@@ -729,6 +778,14 @@ class UnifiedSearchResponse(BaseModel):
729
778
  msg (str, optional): Additional message
730
779
  agent_answer (str, optional): LLM-synthesised answer populated by the agentic backend;
731
780
  None for classic backend.
781
+ degraded (bool): True when the search silently fell back from the
782
+ requested vector/hybrid mode to full-text search because query
783
+ embedding generation failed. Results are still returned (via FTS),
784
+ but relevance may be lower than a healthy vector/hybrid run.
785
+ Defaults to False.
786
+ search_mode_effective (str, optional): The search mode actually used
787
+ when it differs from the requested mode — currently ``"fts"`` on
788
+ the degrade path. None when the requested mode was honored.
732
789
  """
733
790
 
734
791
  success: bool
@@ -740,6 +797,8 @@ class UnifiedSearchResponse(BaseModel):
740
797
  agent_answer: str | None = None
741
798
  agent_trace: str | None = None
742
799
  rehydrated_text: str | None = None
800
+ degraded: bool = False
801
+ search_mode_effective: str | None = None
743
802
 
744
803
 
745
804
  # ===============================
@@ -53,6 +53,7 @@ def to_interaction_view(interaction: Interaction) -> InteractionView:
53
53
  shadow_content=interaction.shadow_content,
54
54
  expert_content=interaction.expert_content,
55
55
  tools_used=interaction.tools_used,
56
+ retrieved_learnings=interaction.retrieved_learnings,
56
57
  )
57
58
 
58
59
 
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
5
5
  from pydantic import BaseModel, Field, field_validator
6
6
 
7
7
  from ..common import NEVER_EXPIRES_TIMESTAMP, ToolUsed
8
+ from ..domain.entities import RetrievedLearning
8
9
  from ..validators import _validate_image_url
9
10
  from .enums import (
10
11
  PlaybookStatus,
@@ -29,7 +30,12 @@ __all__ = [
29
30
 
30
31
 
31
32
  class InteractionView(BaseModel):
32
- """User-facing Interaction — excludes embedding and image_encoding."""
33
+ """User-facing Interaction — excludes embedding and image_encoding.
34
+
35
+ ``retrieved_learnings`` (caller-supplied at publish) is exposed so callers
36
+ can round-trip what they attached; ``citations`` (agent-claimed influence,
37
+ server-side signal) is deliberately NOT exposed on this view.
38
+ """
33
39
 
34
40
  interaction_id: int = 0
35
41
  user_id: str
@@ -43,6 +49,7 @@ class InteractionView(BaseModel):
43
49
  shadow_content: str = ""
44
50
  expert_content: str = ""
45
51
  tools_used: list[ToolUsed] = Field(default_factory=list)
52
+ retrieved_learnings: list[RetrievedLearning] = Field(default_factory=list)
46
53
 
47
54
  @field_validator("interacted_image_url", mode="after")
48
55
  @classmethod
@@ -914,7 +914,7 @@ class Config(BaseModel):
914
914
  ),
915
915
  )
916
916
  shadow_comparison_judge_prompt_version: NonEmptyStr = Field(
917
- default="v1.0.0",
917
+ default="v1.1.0",
918
918
  description=(
919
919
  "F1: pinned judge prompt version for per-turn shadow comparison. "
920
920
  "Verdicts are stored with the version that produced them; the "