claude-smart 0.2.48 → 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 (193) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +11 -40
  3. package/bin/claude-smart.js +393 -55
  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/opencode/dist/server.mjs +60 -2
  27. package/plugin/opencode/server.mts +64 -2
  28. package/plugin/pyproject.toml +2 -2
  29. package/plugin/scripts/_lib.sh +255 -7
  30. package/plugin/scripts/backend-python-runner.py +46 -0
  31. package/plugin/scripts/backend-service.sh +766 -123
  32. package/plugin/scripts/codex-hook.js +79 -229
  33. package/plugin/scripts/dashboard-open.sh +6 -4
  34. package/plugin/scripts/dashboard-service.sh +118 -137
  35. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  36. package/plugin/scripts/hook_entry.sh +3 -0
  37. package/plugin/scripts/opencode-claude-compat.js +8 -1
  38. package/plugin/scripts/smart-install.sh +116 -21
  39. package/plugin/src/README.md +1 -1
  40. package/plugin/src/claude_smart/cli.py +93 -29
  41. package/plugin/src/claude_smart/context_inject.py +3 -0
  42. package/plugin/src/claude_smart/env_config.py +56 -11
  43. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  44. package/plugin/src/claude_smart/events/session_end.py +2 -1
  45. package/plugin/src/claude_smart/events/stop.py +3 -0
  46. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  47. package/plugin/src/claude_smart/internal_call.py +5 -2
  48. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  49. package/plugin/src/claude_smart/publish.py +59 -7
  50. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  51. package/plugin/src/claude_smart/runtime.py +15 -6
  52. package/plugin/src/claude_smart/state.py +211 -52
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +13 -0
  55. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  56. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  58. package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
  59. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  60. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  61. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  62. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
  63. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  64. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  65. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  66. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  67. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  68. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  69. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  70. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  71. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  72. package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
  73. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  74. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  75. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  76. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  77. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
  78. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  79. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
  80. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  81. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  82. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  83. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  84. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
  85. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  86. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  90. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  91. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  92. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  93. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  94. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  95. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  96. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  97. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  98. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  99. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
  100. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  101. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  102. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  103. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  107. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  108. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  109. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  111. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  112. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  113. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  114. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  115. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  120. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  121. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  122. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  123. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  124. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  125. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  126. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
  127. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
  129. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  131. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  133. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  135. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  136. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  137. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  138. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  139. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  140. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  141. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  142. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  143. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  144. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
  145. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  146. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  150. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
  172. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
  173. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
  174. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  175. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  176. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  178. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  179. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  180. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  181. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  182. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  183. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  184. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  185. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
  186. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
  187. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  188. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  189. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  190. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  191. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  192. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  193. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -15,6 +15,13 @@ import requests
15
15
  from pydantic import ConfigDict
16
16
 
17
17
  from reflexio.defaults import DEFAULT_AGENT_VERSION
18
+ from reflexio.models.api_schema.eval_overview_schema import (
19
+ GradeOnDemandRequest,
20
+ GradeOnDemandResponse,
21
+ RegenerateRequest,
22
+ RegenerateStartResponse,
23
+ RegenerateStatusResponse,
24
+ )
18
25
  from reflexio.models.api_schema.retriever_schema import (
19
26
  ConversationTurn,
20
27
  GetAgentPlaybooksRequest,
@@ -26,6 +33,8 @@ from reflexio.models.api_schema.retriever_schema import (
26
33
  GetProfilesViewResponse,
27
34
  GetRequestsRequest,
28
35
  GetRequestsViewResponse,
36
+ GetRetrievedLearningEvaluationResultsRequest,
37
+ GetRetrievedLearningEvaluationResultsResponse,
29
38
  GetUserPlaybooksRequest,
30
39
  GetUserPlaybooksViewResponse,
31
40
  GetUserProfilesRequest,
@@ -306,9 +315,8 @@ class ReflexioClient:
306
315
  if headers:
307
316
  request_headers.update(headers)
308
317
 
309
- self.session.headers.update(request_headers)
310
318
  kwargs.setdefault("timeout", self.timeout)
311
- response = self.session.request(method, url, **kwargs)
319
+ response = self.session.request(method, url, headers=request_headers, **kwargs)
312
320
  response.raise_for_status()
313
321
 
314
322
  # Empty body on a successful response (e.g. 204 No Content).
@@ -445,12 +453,17 @@ class ReflexioClient:
445
453
  explicit retry after reauth or limit reset.
446
454
 
447
455
  Returns:
448
- PublishUserInteractionResponse: Server response. In
449
- ``wait_for_response=False`` mode this is a bare
450
- acknowledgement ("Interaction queued for processing")
451
- without extraction counts; in ``wait_for_response=True``
452
- mode it includes request_id, storage routing, and
453
- deltas.
456
+ PublishUserInteractionResponse: Server response.
457
+
458
+ In deferred mode (``wait_for_response=False``) the
459
+ response carries ``learning_status="deferred"`` and a
460
+ ``request_id``; ``profiles_added``/``playbooks_added`` are
461
+ 0 because extraction has not run yet. Poll
462
+ ``get_learning_status(request_id)`` for completion.
463
+
464
+ In ``wait_for_response=True`` mode the server waits for
465
+ extraction and the response includes ``request_id``,
466
+ storage routing, and real profile/playbook deltas.
454
467
  """
455
468
  if session_id is None or not session_id.strip():
456
469
  raise ValueError("session_id is required and cannot be empty")
@@ -481,6 +494,35 @@ class ReflexioClient:
481
494
  self._cache.invalidate("get_agent_playbooks")
482
495
  return result
483
496
 
497
+ def get_learning_status(self, request_id: str) -> str:
498
+ """Poll the learning status for a previously published request.
499
+
500
+ Call this after a deferred ``publish_interaction`` (where
501
+ ``wait_for_response=False``). The response carries
502
+ ``learning_status="deferred"`` to signal that extraction has been
503
+ queued; use this method to track progress once the durable queue
504
+ is active.
505
+
506
+ Args:
507
+ request_id: The ``request_id`` of the published interaction, as
508
+ returned in ``PublishUserInteractionResponse.request_id``
509
+ (populated on both the deferred and ``wait_for_response=True``
510
+ paths) or known by the caller.
511
+
512
+ Returns:
513
+ One of: ``"pending"`` | ``"processing"`` | ``"done"`` | ``"failed"``.
514
+
515
+ Raises:
516
+ requests.HTTPError: 404 when the request_id is not known to the
517
+ server for this org.
518
+ """
519
+ response = self._make_request(
520
+ "GET",
521
+ "/api/learning_status",
522
+ params={"request_id": request_id},
523
+ )
524
+ return str(response["status"])
525
+
484
526
  def search_interactions(
485
527
  self,
486
528
  request: SearchInteractionRequest | dict | None = None,
@@ -1886,6 +1928,116 @@ class ReflexioClient:
1886
1928
  )
1887
1929
  return GetEvaluationResultsViewResponse(**response)
1888
1930
 
1931
+ def get_retrieved_learning_evaluation_results(
1932
+ self,
1933
+ request: GetRetrievedLearningEvaluationResultsRequest | dict | None = None,
1934
+ *,
1935
+ user_id: str | None = None,
1936
+ session_id: str | None = None,
1937
+ limit: int | None = None,
1938
+ ) -> GetRetrievedLearningEvaluationResultsResponse:
1939
+ """Get per-learning relevance and impact verdicts for retrieved context."""
1940
+ req = self._build_request(
1941
+ request,
1942
+ GetRetrievedLearningEvaluationResultsRequest,
1943
+ user_id=user_id,
1944
+ session_id=session_id,
1945
+ limit=limit,
1946
+ )
1947
+ response = self._make_request(
1948
+ "POST",
1949
+ "/api/get_retrieved_learning_evaluation_results",
1950
+ json=req.model_dump(),
1951
+ )
1952
+ return GetRetrievedLearningEvaluationResultsResponse(**response)
1953
+
1954
+ def regenerate_evaluations(
1955
+ self,
1956
+ request: RegenerateRequest | dict | None = None,
1957
+ *,
1958
+ from_ts: int | None = None,
1959
+ to_ts: int | None = None,
1960
+ evaluation_name: str | None = None,
1961
+ ) -> RegenerateStartResponse:
1962
+ """Start a replay-the-judge regeneration job over a time window.
1963
+
1964
+ Args:
1965
+ request: Optional ``RegenerateRequest`` or dict. If omitted,
1966
+ keyword arguments are used.
1967
+ from_ts: Inclusive lower bound of the window as Unix seconds.
1968
+ to_ts: Inclusive upper bound of the window as Unix seconds.
1969
+ evaluation_name: Deprecated compatibility field accepted by the
1970
+ API but ignored by the singleton evaluator.
1971
+
1972
+ Returns:
1973
+ RegenerateStartResponse: The job id and number of queued sessions.
1974
+ """
1975
+ req = self._build_request(
1976
+ request,
1977
+ RegenerateRequest,
1978
+ from_ts=from_ts,
1979
+ to_ts=to_ts,
1980
+ evaluation_name=evaluation_name,
1981
+ )
1982
+ response = self._make_request(
1983
+ "POST",
1984
+ "/api/evaluations/regenerate",
1985
+ json=req.model_dump(),
1986
+ )
1987
+ return RegenerateStartResponse(**response)
1988
+
1989
+ def get_evaluation_regeneration_status(
1990
+ self, job_id: str
1991
+ ) -> RegenerateStatusResponse:
1992
+ """Get status for an evaluation regeneration job."""
1993
+ response = self._make_request(
1994
+ "GET",
1995
+ f"/api/evaluations/regenerate/{job_id}",
1996
+ )
1997
+ return RegenerateStatusResponse(**response)
1998
+
1999
+ def cancel_evaluation_regeneration(self, job_id: str) -> dict[str, str]:
2000
+ """Request cancellation for an evaluation regeneration job."""
2001
+ return self._make_request(
2002
+ "DELETE",
2003
+ f"/api/evaluations/regenerate/{job_id}",
2004
+ )
2005
+
2006
+ def grade_on_demand(
2007
+ self,
2008
+ request: GradeOnDemandRequest | dict | None = None,
2009
+ *,
2010
+ session_id: str | None = None,
2011
+ agent_version: str | None = None,
2012
+ evaluation_name: str | None = None,
2013
+ ) -> GradeOnDemandResponse:
2014
+ """Grade a single session synchronously.
2015
+
2016
+ Args:
2017
+ request: Optional ``GradeOnDemandRequest`` or dict. If omitted,
2018
+ keyword arguments are used.
2019
+ session_id: Session to grade.
2020
+ agent_version: Agent version to grade.
2021
+ evaluation_name: Deprecated compatibility field accepted by the
2022
+ API but ignored by the singleton evaluator.
2023
+
2024
+ Returns:
2025
+ GradeOnDemandResponse: Result id, cache flag, or skipped reason.
2026
+ """
2027
+ req = self._build_request(
2028
+ request,
2029
+ GradeOnDemandRequest,
2030
+ session_id=session_id,
2031
+ agent_version=agent_version,
2032
+ evaluation_name=evaluation_name,
2033
+ )
2034
+ response = self._make_request(
2035
+ "POST",
2036
+ "/api/evaluations/grade_on_demand",
2037
+ json=req.model_dump(),
2038
+ )
2039
+ return GradeOnDemandResponse(**response)
2040
+
1889
2041
  def _poll_operation_status(
1890
2042
  self,
1891
2043
  service_name: str,
@@ -2365,7 +2517,12 @@ class ReflexioClient:
2365
2517
  "profiles", "user_playbooks", "agent_playbooks".
2366
2518
  agent_playbook_status_filter (Optional[list[Union[PlaybookStatus, str]]]):
2367
2519
  Agent-playbook approval statuses to include.
2368
- enable_reformulation (Optional[bool]): Enable LLM query reformulation (default: False)
2520
+ enable_reformulation (Optional[bool]): Enable the pre-search LLM
2521
+ query reformulation call (default: False). Besides rewriting
2522
+ the query, it extracts temporal signals — time windows
2523
+ ("this week"), as-of boundaries, and current-value intent —
2524
+ that make retrieval time-sensitive (window filters,
2525
+ freshness-preferred ranking for superseded facts).
2369
2526
  enable_agent_answer (Optional[bool]): Enable agentic answer synthesis when
2370
2527
  the configured search backend supports it (default: False).
2371
2528
  conversation_history (Optional[list[ConversationTurn] | list[dict]]): Prior conversation turns for context-aware query reformulation. Accepts ConversationTurn objects or dicts with "role" and "content" keys.
@@ -36,7 +36,9 @@ _DEFAULT_STATE_DIR = Path.home() / ".openclaw-smart" / "sessions"
36
36
 
37
37
  _TOOL_DATA_FIELD_MAX_LEN = 256
38
38
 
39
- _VALID_CITATION_KINDS = frozenset({"playbook", "profile"})
39
+ _VALID_CITATION_KINDS = frozenset(
40
+ {"playbook", "profile", "user_playbook", "agent_playbook"}
41
+ )
40
42
 
41
43
 
42
44
  def _truncate_tool_data_field(value: Any) -> Any:
@@ -240,13 +242,18 @@ def _to_wire_citations(cited_items: Any) -> list[dict[str, str]]:
240
242
  kind = item.get("kind")
241
243
  if not isinstance(real_id, str) or not real_id:
242
244
  continue
243
- if kind not in _VALID_CITATION_KINDS:
245
+ wire_kind = kind
246
+ if kind == "playbook":
247
+ source_kind = item.get("source_kind")
248
+ if source_kind in {"user_playbook", "agent_playbook"}:
249
+ wire_kind = source_kind
250
+ if wire_kind not in _VALID_CITATION_KINDS:
244
251
  continue
245
252
  tag = item.get("id")
246
253
  title = item.get("title")
247
254
  out.append(
248
255
  {
249
- "kind": kind,
256
+ "kind": wire_kind,
250
257
  "real_id": real_id,
251
258
  "tag": tag if isinstance(tag, str) else "",
252
259
  "title": title if isinstance(title, str) else "",
@@ -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",
@@ -123,12 +128,15 @@ __all__ = [
123
128
  "LineageEvent",
124
129
  "LineageContext",
125
130
  "RecordRef",
131
+ "LearningStatusResponse",
126
132
  ]
127
133
 
128
134
  # ===============================
129
135
  # Data Models
130
136
  # ===============================
131
137
 
138
+ type CitationKind = Literal["playbook", "profile", "user_playbook", "agent_playbook"]
139
+
132
140
 
133
141
  class Citation(BaseModel):
134
142
  """A playbook or profile item the agent cited as influential.
@@ -140,22 +148,52 @@ class Citation(BaseModel):
140
148
  it was applied?).
141
149
 
142
150
  Attributes:
143
- kind (Literal["playbook", "profile"]): Which kind of cited
144
- 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.
145
156
  real_id (str): Stable storage id — ``user_playbook_id`` for
146
- playbooks, ``profile_id`` for profiles.
157
+ user playbooks, ``agent_playbook_id`` for agent playbooks,
158
+ and ``profile_id`` for profiles.
147
159
  tag (str): Injection-time rank tag (e.g. ``"r1-301"``,
148
160
  ``"p1-0f37"``). Per-injection, not stable across sessions;
149
161
  kept as a debug aid.
150
162
  title (str): Short human-readable label for logs and UI.
151
163
  """
152
164
 
153
- kind: Literal["playbook", "profile"]
165
+ kind: CitationKind
154
166
  real_id: str
155
167
  tag: str = ""
156
168
  title: str = ""
157
169
 
158
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
+
159
197
  # information about the user interaction sent by the client
160
198
  class Interaction(BaseModel):
161
199
  interaction_id: int = 0 # 0 = placeholder for DB auto-increment
@@ -172,6 +210,10 @@ class Interaction(BaseModel):
172
210
  expert_content: str = ""
173
211
  tools_used: list[ToolUsed] = Field(default_factory=list)
174
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)
175
217
  embedding: EmbeddingVector = []
176
218
 
177
219
  @field_validator("interacted_image_url", mode="after")
@@ -398,17 +440,107 @@ class AgentSuccessEvaluationResult(BaseModel):
398
440
  embedding: EmbeddingVector = []
399
441
 
400
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
+
401
485
  class PlaybookRetrievalLogItem(BaseModel):
402
- """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
+ """
403
491
 
404
492
  retrieval_log_item_id: int = 0
405
493
  retrieval_log_id: int = 0
406
494
  ordinal: int
407
- 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
408
501
  source_user_playbook_ids: list[int] = Field(default_factory=list)
409
502
  source_interaction_ids_by_user_playbook_id: dict[str, list[int]] = Field(
410
503
  default_factory=dict
411
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
412
544
 
413
545
 
414
546
  class PlaybookRetrievalLog(BaseModel):
@@ -598,19 +730,19 @@ class BulkDeleteResponse(BaseModel):
598
730
 
599
731
 
600
732
  class DeleteRequestsByIdsRequest(BaseModel):
601
- request_ids: list[str] = Field(min_length=1)
733
+ request_ids: list[str] = Field(min_length=1, max_length=10_000)
602
734
 
603
735
 
604
736
  class DeleteProfilesByIdsRequest(BaseModel):
605
- profile_ids: list[str] = Field(min_length=1)
737
+ profile_ids: list[str] = Field(min_length=1, max_length=10_000)
606
738
 
607
739
 
608
740
  class DeleteAgentPlaybooksByIdsRequest(BaseModel):
609
- agent_playbook_ids: list[int] = Field(min_length=1)
741
+ agent_playbook_ids: list[int] = Field(min_length=1, max_length=10_000)
610
742
 
611
743
 
612
744
  class DeleteUserPlaybooksByIdsRequest(BaseModel):
613
- user_playbook_ids: list[int] = Field(min_length=1)
745
+ user_playbook_ids: list[int] = Field(min_length=1, max_length=10_000)
614
746
 
615
747
 
616
748
  # Clear all data scoped to a single user_id (interactions, requests, user
@@ -631,16 +763,25 @@ class ClearUserDataResponse(BaseModel):
631
763
  # user provided interaction data from the request
632
764
  class InteractionData(BaseModel):
633
765
  created_at: int = Field(default_factory=lambda: int(datetime.now(UTC).timestamp()))
634
- role: str = "User"
635
- content: str = ""
636
- shadow_content: str = ""
637
- 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)
638
770
  user_action: UserActionType = UserActionType.NONE
639
- user_action_description: str = ""
640
- interacted_image_url: str = ""
641
- image_encoding: str = "" # base64 encoded image
642
- tools_used: list[ToolUsed] = Field(default_factory=list)
643
- 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
+ )
644
785
 
645
786
  @field_validator("interacted_image_url", mode="after")
646
787
  @classmethod
@@ -652,11 +793,10 @@ class InteractionData(BaseModel):
652
793
  class PublishUserInteractionRequest(BaseModel):
653
794
  request_id: NonEmptyStr | None = None
654
795
  user_id: NonEmptyStr
655
- interaction_data_list: list[InteractionData] = Field(min_length=1)
656
- source: str = ""
657
- agent_version: str = (
658
- "" # this is used for aggregating interactions for generating agent playbooks
659
- )
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)
660
800
  session_id: NonEmptyStr # used for grouping requests together
661
801
  skip_aggregation: bool = (
662
802
  False # when True, extract profiles/playbooks but skip aggregation
@@ -673,6 +813,19 @@ class PublishUserInteractionRequest(BaseModel):
673
813
  raise ValueError("evaluation_only publishes require session_id")
674
814
  return self
675
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
+
676
829
 
677
830
  # publish user interaction response
678
831
  class PublishUserInteractionResponse(BaseModel):
@@ -689,6 +842,23 @@ class PublishUserInteractionResponse(BaseModel):
689
842
  profiles_updated: int | None = None
690
843
  playbooks_added: int | None = None
691
844
  playbooks_updated: int | None = None
845
+ # Set to "deferred" when the server queued extraction asynchronously.
846
+ # None on the sync (wait_for_response=True) path. Poll GET
847
+ # /api/learning_status?request_id=... to track progress once the
848
+ # durable queue is active.
849
+ learning_status: str | None = None
850
+
851
+
852
+ class LearningStatusResponse(BaseModel):
853
+ """Response for GET /api/learning_status.
854
+
855
+ Attributes:
856
+ status: One of ``pending | processing | done | failed``.
857
+ Coverage-based: reflects whether a durable learning job has
858
+ processed through the request's creation timestamp.
859
+ """
860
+
861
+ status: Literal["pending", "processing", "done", "failed"]
692
862
 
693
863
 
694
864
  # whoami response — caller identity + resolved storage routing (masked)
@@ -712,7 +882,7 @@ class MyConfigResponse(BaseModel):
712
882
 
713
883
  # add user playbook request/response
714
884
  class AddUserPlaybookRequest(BaseModel):
715
- user_playbooks: list[UserPlaybook] = Field(min_length=1)
885
+ user_playbooks: list[UserPlaybook] = Field(min_length=1, max_length=1_000)
716
886
 
717
887
  @model_validator(mode="after")
718
888
  def check_content_fields(self) -> Self: