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,258 @@
1
+ """Usage-metric + ② Learning billing helpers for ``BaseGenerationService`` (Tier-1b decomposition).
2
+
3
+ ``UsageBillingMixin`` holds the money-critical Bucket A: the usage-event helpers
4
+ (``_usage_pipeline``, ``_usage_context``, ``_record_generation_event``) and the
5
+ ② Learning billing path (``_extraction_input_text``, ``_record_billing_learning_events``,
6
+ plus the pure ``_count_generated_results`` staticmethod).
7
+
8
+ These helpers only READ the per-run accumulators (``_last_token_totals``,
9
+ ``_last_precheck_sessions``, ``service_config``, ``EMITS_LEARNING_BILLING``) — they
10
+ never write them, so moving them introduces no ordering/race change. The billing
11
+ emit-ordering (``record_learnings_generated`` BEFORE ``record_extraction_tokens``,
12
+ success-path only) lives in ``_run_generation`` on the base and is unchanged.
13
+
14
+ SINK-3 (money-critical): the ``billing_meter`` / ``billing_signals`` imports inside
15
+ ``_record_billing_learning_events`` stay FUNCTION-LOCAL — the Phase-A drain test
16
+ patches the terminal emitters at their source module (``reflexio.server.billing_meter``),
17
+ and hoisting the imports to module top would both break that patch seam and risk a
18
+ circular import that the ``:392`` exception-swallow would hide silently. Both
19
+ exception swallows (``_record_billing_learning_events`` and ``_extraction_input_text``)
20
+ are moved VERBATIM. Method bodies are otherwise moved unchanged from the former
21
+ monolithic ``base_generation_service.py``.
22
+ """
23
+
24
+ import logging
25
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
26
+
27
+ from reflexio.server.api_endpoints.request_context import RequestContext
28
+ from reflexio.server.llm.token_accounting import RunTokenTotals
29
+ from reflexio.server.services.extractor_interaction_utils import (
30
+ get_effective_source_filter,
31
+ get_extractor_window_params,
32
+ )
33
+ from reflexio.server.services.storage.storage_base import BaseStorage
34
+ from reflexio.server.usage_metrics import record_usage_event
35
+
36
+ if TYPE_CHECKING:
37
+ from reflexio.server.services.base_generation_service import PreparedGenerationRun
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ TExtractorConfig = TypeVar("TExtractorConfig")
42
+ TGenerationServiceConfig = TypeVar("TGenerationServiceConfig")
43
+
44
+
45
+ class UsageBillingMixin(Generic[TExtractorConfig, TGenerationServiceConfig]): # noqa: UP046
46
+ """Usage-event emission + the ② Learning billing path.
47
+
48
+ Mixed into ``BaseGenerationService`` ahead of the other mixins and ``ABC``; the
49
+ per-run ``self`` attributes these methods read (``service_config``, ``storage``,
50
+ ``org_id``, ``request_context`` and the billing accumulators
51
+ ``_last_token_totals`` / ``_last_precheck_sessions``) are initialised on the base
52
+ ``__init__`` — the annotation-only stubs below give pyright the types without
53
+ introducing shared class-level mutable state.
54
+ """
55
+
56
+ # Annotation-only stubs for base-owned attributes these helpers read (init'd in
57
+ # the base ``__init__``). NEVER assign here — a class-level default would be a
58
+ # shared-state footgun on a mixin.
59
+ service_config: TGenerationServiceConfig | None
60
+ storage: BaseStorage | None
61
+ org_id: str
62
+ request_context: RequestContext
63
+ # Read-only in this bucket: written by the extraction lifecycle / precheck mixins.
64
+ _last_token_totals: RunTokenTotals | None
65
+ _last_precheck_sessions: list[Any] | None
66
+ # Class-level opt-in flag defined on the base; annotation-only here so pyright can
67
+ # resolve ``self.EMITS_LEARNING_BILLING`` without shadowing the base default.
68
+ EMITS_LEARNING_BILLING: bool
69
+
70
+ if TYPE_CHECKING:
71
+ # Abstract on the base ABC (stays there per SINK-2); declared here type-only so
72
+ # pyright can resolve the ``self._get_service_name()`` call. No runtime
73
+ # attribute is added, so ``__abstractmethods__`` is unaffected.
74
+ def _get_service_name(self) -> str: ...
75
+ # Concrete on ``ShouldRunPrecheckMixin`` (resolved via MRO); declared type-only
76
+ # so pyright can resolve the ``self._get_precheck_interaction_query_kwargs()`` call.
77
+ def _get_precheck_interaction_query_kwargs(self) -> dict: ...
78
+
79
+ def _usage_pipeline(self) -> str | None:
80
+ service_name = self._get_service_name()
81
+ if "profile" in service_name:
82
+ return "profile"
83
+ if "playbook" in service_name:
84
+ return "playbook"
85
+ if "evaluation" in service_name:
86
+ return "evaluation"
87
+ return None
88
+
89
+ def _usage_context(self) -> dict[str, Any]:
90
+ service_config = self.service_config
91
+ return {
92
+ "org_id": self.org_id,
93
+ "user_id": getattr(service_config, "user_id", None),
94
+ "request_id": getattr(service_config, "request_id", None),
95
+ "source": getattr(service_config, "source", None),
96
+ "agent_version": getattr(service_config, "agent_version", None),
97
+ "pipeline": self._usage_pipeline(),
98
+ }
99
+
100
+ def _record_generation_event(
101
+ self,
102
+ *,
103
+ event_name: str,
104
+ outcome: str,
105
+ count_value: int = 1,
106
+ duration_ms: int | None = None,
107
+ error_kind: str | None = None,
108
+ metadata: dict[str, Any] | None = None,
109
+ ) -> None:
110
+ record_usage_event(
111
+ **self._usage_context(),
112
+ event_name=event_name,
113
+ event_category="generation",
114
+ outcome=outcome,
115
+ count_value=count_value,
116
+ duration_ms=duration_ms,
117
+ error_kind=error_kind,
118
+ metadata=metadata,
119
+ )
120
+
121
+ def _extraction_input_text(self, prepared: "PreparedGenerationRun[Any]") -> str:
122
+ """Return the interaction content fed to the extractor as a single string.
123
+
124
+ Replays the same storage query the extractor uses (same user_id, source,
125
+ window params) so ``count_input_tokens`` sees the same text the LLM saw.
126
+ Returns an empty string when storage is unavailable or no interactions exist.
127
+
128
+ Args:
129
+ prepared: The prepared generation run, used to access extractor_config
130
+ for window/source parameters.
131
+
132
+ Returns:
133
+ str: Formatted interaction history string, or "" if nothing is found.
134
+ """
135
+ from reflexio.server.services.service_utils import (
136
+ format_sessions_to_history_string,
137
+ )
138
+
139
+ if self.storage is None or self.service_config is None:
140
+ return ""
141
+ # Reuse the window the should-run gate already fetched
142
+ # (_collect_scoped_interactions_for_precheck stashed it). Same query params,
143
+ # so the token count is byte-identical — and we avoid a second storage read.
144
+ if self._last_precheck_sessions is not None:
145
+ return format_sessions_to_history_string(self._last_precheck_sessions)
146
+ # Fallback storage read: used ONLY when the gate did not pre-fetch
147
+ # (bypass paths: auto_run=False, force_extraction, skip_should_run_check,
148
+ # mock mode). Re-fetches the extractor's input window so billing token
149
+ # counting sees exactly the text the LLM saw.
150
+ try:
151
+ root_config = self.request_context.configurator.get_config()
152
+ global_window_size = (
153
+ getattr(root_config, "window_size", None) if root_config else None
154
+ )
155
+ global_stride_size = (
156
+ getattr(root_config, "stride_size", None) if root_config else None
157
+ )
158
+ window_size, _ = get_extractor_window_params(
159
+ prepared.extractor_config, global_window_size, global_stride_size
160
+ )
161
+ should_skip, effective_source = get_effective_source_filter(
162
+ prepared.extractor_config, getattr(self.service_config, "source", None)
163
+ )
164
+ if should_skip:
165
+ return ""
166
+ session_data_models, _ = self.storage.get_last_k_interactions_grouped(
167
+ user_id=getattr(self.service_config, "user_id", None),
168
+ k=window_size,
169
+ sources=effective_source,
170
+ start_time=getattr(self.service_config, "rerun_start_time", None),
171
+ end_time=getattr(self.service_config, "rerun_end_time", None),
172
+ **self._get_precheck_interaction_query_kwargs(),
173
+ )
174
+ return format_sessions_to_history_string(session_data_models)
175
+ except Exception:
176
+ logger.warning(
177
+ "_extraction_input_text failed; billing_input_tokens will be 0",
178
+ exc_info=True,
179
+ )
180
+ return ""
181
+
182
+ def _record_billing_learning_events(
183
+ self, *, prepared: "PreparedGenerationRun[Any]", generated_count: int
184
+ ) -> None:
185
+ """Emit the ② Learning billing events. Called ONLY when extraction fired.
186
+
187
+ Emits ``learnings_generated`` (value facet) and ``extraction_tokens``
188
+ (cost facet) via the OSS emission helpers. ``platform_storage`` is left
189
+ ``None`` here and resolved enterprise-side at rollup (Phase 1).
190
+
191
+ Gated by ``EMITS_LEARNING_BILLING`` — only profile/playbook generation
192
+ services opt in. Evaluation, reflection, consolidation and aggregation
193
+ are bundled and must NOT separately meter the ② Learning line.
194
+
195
+ Args:
196
+ prepared: The prepared generation run (used for input-text computation).
197
+ generated_count: Number of learnings produced by this extraction run.
198
+ """
199
+ if not self.EMITS_LEARNING_BILLING:
200
+ return
201
+
202
+ try:
203
+ from reflexio.server.billing_meter import (
204
+ record_extraction_tokens,
205
+ record_learnings_generated,
206
+ )
207
+ from reflexio.server.billing_signals import (
208
+ count_input_tokens,
209
+ platform_llm_from_config,
210
+ )
211
+
212
+ config = self.request_context.configurator.get_config()
213
+ platform_llm = platform_llm_from_config(config)
214
+ ctx = self._usage_context()
215
+
216
+ # session_id is intentionally not passed: the generation path has no
217
+ # session_id source. _usage_context() never includes it, and neither
218
+ # the Profile/Playbook service configs nor their requests carry a
219
+ # session_id (unlike the Application-line path in server/api.py, which
220
+ # reads request_id/session_id off the search request payload). Learning
221
+ # events therefore meter without session attribution by design.
222
+
223
+ # ② Learning — value: learnings generated (helper no-ops on count <= 0).
224
+ record_learnings_generated(
225
+ org_id=ctx["org_id"],
226
+ count=generated_count,
227
+ platform_llm=platform_llm,
228
+ platform_storage=None,
229
+ pipeline=ctx.get("pipeline"),
230
+ request_id=ctx.get("request_id"),
231
+ )
232
+
233
+ # ② Learning — cost: input-anchored extraction tokens + real provider tokens.
234
+ totals = self._last_token_totals or RunTokenTotals()
235
+ billing_input_tokens = count_input_tokens(
236
+ self._extraction_input_text(prepared)
237
+ )
238
+ record_extraction_tokens(
239
+ org_id=ctx["org_id"],
240
+ billing_input_tokens=billing_input_tokens,
241
+ prompt_tokens=totals.prompt_tokens,
242
+ completion_tokens=totals.completion_tokens,
243
+ platform_llm=platform_llm,
244
+ platform_storage=None,
245
+ pipeline=ctx.get("pipeline"),
246
+ request_id=ctx.get("request_id"),
247
+ )
248
+ except Exception:
249
+ logger.warning(
250
+ "_record_billing_learning_events failed; billing learning events not emitted",
251
+ exc_info=True,
252
+ )
253
+
254
+ @staticmethod
255
+ def _count_generated_results(result: Any) -> int:
256
+ if isinstance(result, list):
257
+ return len(result)
258
+ return 1 if result else 0