claude-smart 0.2.46 → 0.2.47

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 (85) 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/opencode/dist/server.mjs +76 -2
  10. package/plugin/opencode/server.mts +79 -2
  11. package/plugin/pyproject.toml +6 -2
  12. package/plugin/scripts/smart-install.sh +7 -1
  13. package/plugin/src/claude_smart/cli.py +210 -22
  14. package/plugin/src/claude_smart/context_format.py +9 -9
  15. package/plugin/src/claude_smart/cs_cite.py +66 -19
  16. package/plugin/uv.lock +5 -5
  17. package/plugin/vendor/reflexio/README.md +3 -3
  18. package/plugin/vendor/reflexio/pyproject.toml +1 -1
  19. package/plugin/vendor/reflexio/reflexio/README.md +3 -1
  20. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  21. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  22. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  23. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  24. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  25. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  26. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  27. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  28. package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -5
  29. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  30. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  33. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +16 -0
  34. package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
  35. package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
  36. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  37. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  38. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  39. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  40. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  41. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  42. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  43. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -1
  44. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  45. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  46. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  47. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  48. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +45 -19
  49. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  50. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  51. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +129 -111
  52. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  53. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  54. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  56. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +14 -2
  59. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
  60. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  61. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  62. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
  63. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  64. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +262 -107
  65. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  66. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  68. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Protocol
5
+
6
+ from reflexio.server.extensions import ServiceKey
7
+
8
+
9
+ @dataclass
10
+ class AggregationPromptProcessingContext:
11
+ """Per-cluster state shared across aggregation prompt-processing hooks.
12
+
13
+ ``changed`` controls whether contextual prompt guidance is requested.
14
+ ``data`` is an opaque deployment-owned mapping. The aggregator initializes
15
+ ``agent_version`` and ``org_id`` keys when those values are available.
16
+ """
17
+
18
+ changed: bool = False
19
+ data: dict[str, Any] = field(default_factory=dict)
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class PromptPreprocessResult:
24
+ text: str
25
+ changed: bool = False
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class PromptPostprocessResult:
30
+ value: object
31
+ artifacts_removed: int = 0
32
+
33
+
34
+ class AggregationPromptProcessor(Protocol):
35
+ """Optional deployment hook for the playbook aggregation boundary.
36
+
37
+ Implementations may transform prompt text before the aggregation LLM call,
38
+ provide contextual prompt instructions when preprocessing changed prompt
39
+ input, and post-process aggregation output before storage or response
40
+ logging. Deployments supply one by registering it via the
41
+ ``AGGREGATION_PROMPT_PROCESSOR`` ServiceKey (``register_service``).
42
+ """
43
+
44
+ def preprocess_prompt_text(
45
+ self,
46
+ text: str,
47
+ *,
48
+ shared_state: dict[str, Any] | None = None,
49
+ context: AggregationPromptProcessingContext | None = None,
50
+ ) -> PromptPreprocessResult:
51
+ """Return text to send to the aggregation prompt."""
52
+ ...
53
+
54
+ def prompt_instructions(
55
+ self,
56
+ context: AggregationPromptProcessingContext,
57
+ ) -> str | None:
58
+ """Return extra prompt guidance for this aggregation context, if any."""
59
+ ...
60
+
61
+ def postprocess_aggregation_output(
62
+ self,
63
+ value: object,
64
+ *,
65
+ context: AggregationPromptProcessingContext | None = None,
66
+ ) -> PromptPostprocessResult:
67
+ """Return output to store or log after the aggregation LLM call."""
68
+ ...
69
+
70
+
71
+ class PassthroughPromptProcessor:
72
+ def preprocess_prompt_text(
73
+ self,
74
+ text: str,
75
+ *,
76
+ shared_state: dict[str, Any] | None = None, # noqa: ARG002
77
+ context: AggregationPromptProcessingContext | None = None, # noqa: ARG002
78
+ ) -> PromptPreprocessResult:
79
+ return PromptPreprocessResult(text=text)
80
+
81
+ def prompt_instructions(
82
+ self,
83
+ context: AggregationPromptProcessingContext, # noqa: ARG002
84
+ ) -> str | None:
85
+ return None
86
+
87
+ def postprocess_aggregation_output(
88
+ self,
89
+ value: object,
90
+ *,
91
+ context: AggregationPromptProcessingContext | None = None, # noqa: ARG002
92
+ ) -> PromptPostprocessResult:
93
+ return PromptPostprocessResult(value=value)
94
+
95
+
96
+ # Singleton runtime-service key (round-5 RD-NEC-002): one processor registered at
97
+ # the enterprise composition root; None in local OSS (passthrough, no redaction).
98
+ AGGREGATION_PROMPT_PROCESSOR: ServiceKey[AggregationPromptProcessor] = ServiceKey(
99
+ "aggregation_prompt_processor"
100
+ )
@@ -24,6 +24,10 @@ from reflexio.models.config_schema import (
24
24
  from reflexio.server.api_endpoints.request_context import RequestContext
25
25
  from reflexio.server.llm.litellm_client import LiteLLMClient
26
26
  from reflexio.server.services.operation_state_utils import OperationStateManager
27
+ from reflexio.server.services.playbook.aggregation_prompt_processing import (
28
+ AggregationPromptProcessingContext,
29
+ AggregationPromptProcessor,
30
+ )
27
31
  from reflexio.server.services.playbook.playbook_service_constants import (
28
32
  PlaybookServiceConstants,
29
33
  )
@@ -33,7 +37,6 @@ from reflexio.server.services.playbook.playbook_service_utils import (
33
37
  StructuredPlaybookContent,
34
38
  ensure_playbook_content,
35
39
  )
36
- from reflexio.server.services.playbook.user_detail_stripping import UserDetailStripper
37
40
  from reflexio.server.services.service_utils import log_model_response
38
41
  from reflexio.server.tracing import capture_anomaly, sentry_tags
39
42
  from reflexio.server.usage_metrics import record_usage_event
@@ -52,22 +55,14 @@ class PlaybookAggregator:
52
55
  llm_client: LiteLLMClient,
53
56
  request_context: RequestContext,
54
57
  agent_version: str,
55
- user_detail_stripper: UserDetailStripper | None = None,
58
+ aggregation_prompt_processor: AggregationPromptProcessor | None = None,
56
59
  ) -> None:
57
60
  self.client = llm_client
58
61
  self.storage = request_context.storage
59
62
  self.configurator = request_context.configurator
60
63
  self.request_context = request_context
61
64
  self.agent_version = agent_version
62
- self.user_detail_stripper = user_detail_stripper
63
- prompt_extra_instructions = getattr(
64
- user_detail_stripper, "prompt_extra_instructions", None
65
- )
66
- if not isinstance(prompt_extra_instructions, str):
67
- prompt_extra_instructions = None
68
- self.aggregation_prompt_extra_instructions = (
69
- self._format_prompt_extra_instructions(prompt_extra_instructions)
70
- )
65
+ self.aggregation_prompt_processor = aggregation_prompt_processor
71
66
 
72
67
  # ===============================
73
68
  # private methods - operation state
@@ -210,106 +205,96 @@ class PlaybookAggregator:
210
205
  blocks.append("\n".join(lines))
211
206
  return "\n\n".join(blocks) if blocks else "(No playbook items)"
212
207
 
213
- def _strip_prompt_field(
208
+ def _preprocess_prompt_field(
214
209
  self,
215
210
  text: str | None,
216
- shared_mapping: dict[str, int],
211
+ shared_state: dict[str, object],
212
+ processing_context: AggregationPromptProcessingContext | None = None,
217
213
  ) -> str | None:
218
- if text is None or self.user_detail_stripper is None:
214
+ if text is None or self.aggregation_prompt_processor is None:
219
215
  return text
220
- return self.user_detail_stripper.strip_user_details(
221
- text, shared_mapping=shared_mapping
222
- ).text
216
+ result = self.aggregation_prompt_processor.preprocess_prompt_text(
217
+ text,
218
+ shared_state=shared_state,
219
+ context=processing_context,
220
+ )
221
+ if processing_context is not None and (result.changed or result.text != text):
222
+ processing_context.changed = True
223
+ return result.text
223
224
 
224
- def _strip_user_playbook_for_prompt(
225
+ def _preprocess_user_playbook_for_prompt(
225
226
  self,
226
227
  playbook: UserPlaybook,
227
- shared_mapping: dict[str, int],
228
+ shared_state: dict[str, object],
229
+ processing_context: AggregationPromptProcessingContext | None = None,
228
230
  ) -> UserPlaybook:
229
- if self.user_detail_stripper is None:
231
+ if self.aggregation_prompt_processor is None:
230
232
  return playbook
231
233
  return playbook.model_copy(
232
234
  update={
233
- "content": self._strip_prompt_field(playbook.content, shared_mapping)
235
+ "content": self._preprocess_prompt_field(
236
+ playbook.content, shared_state, processing_context
237
+ )
234
238
  or "",
235
- "trigger": self._strip_prompt_field(playbook.trigger, shared_mapping),
236
- "rationale": self._strip_prompt_field(
237
- playbook.rationale, shared_mapping
239
+ "trigger": self._preprocess_prompt_field(
240
+ playbook.trigger, shared_state, processing_context
241
+ ),
242
+ "rationale": self._preprocess_prompt_field(
243
+ playbook.rationale, shared_state, processing_context
238
244
  ),
239
245
  }
240
246
  )
241
247
 
242
- def _sanitize_aggregation_output_text(
248
+ def _aggregation_prompt_extra_instructions_for_context(
243
249
  self,
244
- text: str | None,
245
- ) -> tuple[str | None, int]:
246
- if self.user_detail_stripper is None:
247
- return text, 0
248
- return self.user_detail_stripper.sanitize_aggregation_output_text(text)
250
+ processing_context: AggregationPromptProcessingContext | None,
251
+ ) -> str:
252
+ if (
253
+ processing_context is None
254
+ or not processing_context.changed
255
+ or self.aggregation_prompt_processor is None
256
+ ):
257
+ return ""
249
258
 
250
- def _sanitize_aggregation_response(
251
- self, response: PlaybookAggregationOutput
252
- ) -> tuple[PlaybookAggregationOutput, int]:
253
- structured = response.playbook
254
- if structured is None:
255
- return response, 0
259
+ context_instructions = self.aggregation_prompt_processor.prompt_instructions(
260
+ processing_context
261
+ )
262
+ if not isinstance(context_instructions, str):
263
+ context_instructions = None
264
+ return self._format_prompt_extra_instructions(context_instructions)
256
265
 
257
- updates: dict[str, str | None] = {}
258
- placeholder_count = 0
259
- for field_name, value in structured.model_dump().items():
260
- if not isinstance(value, str):
261
- continue
262
- sanitized, field_count = self._sanitize_aggregation_output_text(value)
263
- placeholder_count += field_count
264
- if sanitized != value:
265
- updates[field_name] = sanitized
266
+ def _postprocess_aggregation_output(
267
+ self,
268
+ value: object,
269
+ processing_context: AggregationPromptProcessingContext | None = None,
270
+ ) -> tuple[object, int]:
271
+ if self.aggregation_prompt_processor is None:
272
+ return value, 0
273
+ result = self.aggregation_prompt_processor.postprocess_aggregation_output(
274
+ value,
275
+ context=processing_context,
276
+ )
277
+ return result.value, result.artifacts_removed
266
278
 
267
- if not updates:
279
+ def _postprocess_aggregation_response(
280
+ self,
281
+ response: PlaybookAggregationOutput,
282
+ processing_context: AggregationPromptProcessingContext | None = None,
283
+ ) -> tuple[PlaybookAggregationOutput, int]:
284
+ processed, artifact_count = self._postprocess_aggregation_output(
285
+ response,
286
+ processing_context,
287
+ )
288
+ if not isinstance(processed, PlaybookAggregationOutput):
268
289
  return response, 0
269
- return response.model_copy(
270
- update={"playbook": structured.model_copy(update=updates)}
271
- ), placeholder_count
272
-
273
- def _sanitize_aggregation_log_value(self, value: object) -> tuple[object, int]:
274
- if isinstance(value, str):
275
- return self._sanitize_aggregation_output_text(value)
276
-
277
- if isinstance(value, dict):
278
- sanitized: dict[object, object] = {}
279
- placeholder_count = 0
280
- for key, item in value.items():
281
- sanitized_key, key_count = self._sanitize_aggregation_log_value(key)
282
- sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
283
- sanitized[sanitized_key] = sanitized_item
284
- placeholder_count += key_count + item_count
285
- return sanitized, placeholder_count
286
-
287
- if isinstance(value, list):
288
- sanitized_items: list[object] = []
289
- placeholder_count = 0
290
- for item in value:
291
- sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
292
- sanitized_items.append(sanitized_item)
293
- placeholder_count += item_count
294
- return sanitized_items, placeholder_count
295
-
296
- if isinstance(value, tuple):
297
- sanitized_items: list[object] = []
298
- placeholder_count = 0
299
- for item in value:
300
- sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
301
- sanitized_items.append(sanitized_item)
302
- placeholder_count += item_count
303
- return tuple(sanitized_items), placeholder_count
304
-
305
- return value, 0
306
-
307
- def _record_placeholder_leakage(self, placeholder_count: int) -> None:
308
- if placeholder_count <= 0:
290
+ return processed, artifact_count
291
+
292
+ def _record_postprocessing_artifacts(self, artifact_count: int) -> None:
293
+ if artifact_count <= 0:
309
294
  return
310
295
  logger.warning(
311
- "Replaced %d residual user-detail placeholders in aggregated playbook output",
312
- placeholder_count,
296
+ "Post-processed %d residual artifacts in aggregated playbook output",
297
+ artifact_count,
313
298
  )
314
299
 
315
300
  @staticmethod
@@ -692,9 +677,9 @@ class PlaybookAggregator:
692
677
  self.agent_version,
693
678
  )
694
679
  logger.info(
695
- "User detail stripping for aggregation: %s",
696
- type(self.user_detail_stripper).__name__
697
- if self.user_detail_stripper is not None
680
+ "Aggregation prompt processor: %s",
681
+ type(self.aggregation_prompt_processor).__name__
682
+ if self.aggregation_prompt_processor is not None
698
683
  else "disabled",
699
684
  )
700
685
 
@@ -979,6 +964,22 @@ class PlaybookAggregator:
979
964
  # Remove archived playbooks after successful aggregation. ALWAYS soft-supersede
980
965
  # (never hard-delete) so the removal is reconstructable from lineage — mirrors the
981
966
  # profile dedup always-soft path (#206).
967
+ archived_ids_without_overlapping_changed_cluster: set[int] = set()
968
+ if new_playbooks and prev_fingerprints:
969
+ archived_id_set = set(archived_playbook_ids)
970
+ for prev_fp, fp_data in prev_fingerprints.items():
971
+ playbook_id = fp_data.get("agent_playbook_id")
972
+ if (
973
+ playbook_id in archived_id_set
974
+ and prev_fp not in changed_fps_by_previous_fp
975
+ ):
976
+ archived_ids_without_overlapping_changed_cluster.add(
977
+ playbook_id
978
+ )
979
+ ids_to_supersede = {
980
+ *selective_supersede_playbook_ids,
981
+ *archived_ids_without_overlapping_changed_cluster,
982
+ }
982
983
  if not _run_id:
983
984
  # Empty request_id makes the removal unreconstructable (lineage events are keyed
984
985
  # on it). Fail loud and skip removal — never silently hard-delete.
@@ -996,9 +997,9 @@ class PlaybookAggregator:
996
997
  agent_version=self.agent_version,
997
998
  request_id=_run_id,
998
999
  )
999
- elif selective_supersede_playbook_ids:
1000
+ elif ids_to_supersede:
1000
1001
  self.storage.supersede_agent_playbooks_by_ids( # type: ignore[reportOptionalMemberAccess]
1001
- sorted(selective_supersede_playbook_ids),
1002
+ sorted(ids_to_supersede),
1002
1003
  request_id=_run_id,
1003
1004
  )
1004
1005
  elif archived_playbook_ids:
@@ -1309,12 +1310,20 @@ class PlaybookAggregator:
1309
1310
  else "None"
1310
1311
  )
1311
1312
  for cluster_playbooks in clusters.values():
1312
- shared_mapping: dict[str, int] = {}
1313
- if self.user_detail_stripper is None:
1313
+ shared_state: dict[str, object] = {}
1314
+ processing_context = AggregationPromptProcessingContext(
1315
+ data={
1316
+ "agent_version": self.agent_version,
1317
+ "org_id": self.request_context.org_id,
1318
+ }
1319
+ )
1320
+ if self.aggregation_prompt_processor is None:
1314
1321
  prompt_cluster_playbooks = cluster_playbooks
1315
1322
  else:
1316
1323
  prompt_cluster_playbooks = [
1317
- self._strip_user_playbook_for_prompt(playbook, shared_mapping)
1324
+ self._preprocess_user_playbook_for_prompt(
1325
+ playbook, shared_state, processing_context
1326
+ )
1318
1327
  for playbook in cluster_playbooks
1319
1328
  ]
1320
1329
 
@@ -1322,6 +1331,7 @@ class PlaybookAggregator:
1322
1331
  prompt_cluster_playbooks,
1323
1332
  approved_playbooks_str,
1324
1333
  direction_overlap_threshold=direction_overlap_threshold,
1334
+ processing_context=processing_context,
1325
1335
  )
1326
1336
  if playbook is not None:
1327
1337
  new_playbooks.append((playbook, cluster_playbooks))
@@ -1371,6 +1381,7 @@ class PlaybookAggregator:
1371
1381
  cluster_playbooks: list[UserPlaybook],
1372
1382
  existing_approved_playbooks_str: str,
1373
1383
  direction_overlap_threshold: float = 0.6,
1384
+ processing_context: AggregationPromptProcessingContext | None = None,
1374
1385
  ) -> AgentPlaybook | None:
1375
1386
  """
1376
1387
  Generate a playbook from a cluster using structured JSON output.
@@ -1407,8 +1418,11 @@ class PlaybookAggregator:
1407
1418
  trigger=trigger,
1408
1419
  )
1409
1420
  )
1410
- response, placeholder_count = self._sanitize_aggregation_response(response)
1411
- self._record_placeholder_leakage(placeholder_count)
1421
+ response, artifact_count = self._postprocess_aggregation_response(
1422
+ response,
1423
+ processing_context,
1424
+ )
1425
+ self._record_postprocessing_artifacts(artifact_count)
1412
1426
  playbook = self._process_aggregation_response(response, cluster_playbooks)
1413
1427
  if playbook is None:
1414
1428
  return None
@@ -1428,7 +1442,9 @@ class PlaybookAggregator:
1428
1442
  {
1429
1443
  "user_playbooks": raw_playbooks_str,
1430
1444
  "existing_approved_playbooks": existing_approved_playbooks_str,
1431
- "aggregation_prompt_extra_instructions": self.aggregation_prompt_extra_instructions,
1445
+ "aggregation_prompt_extra_instructions": self._aggregation_prompt_extra_instructions_for_context(
1446
+ processing_context
1447
+ ),
1432
1448
  },
1433
1449
  ),
1434
1450
  }
@@ -1442,15 +1458,17 @@ class PlaybookAggregator:
1442
1458
  parse_structured_output=True,
1443
1459
  )
1444
1460
  if isinstance(response, PlaybookAggregationOutput):
1445
- response, placeholder_count = self._sanitize_aggregation_response(
1446
- response
1461
+ response, artifact_count = self._postprocess_aggregation_response(
1462
+ response,
1463
+ processing_context,
1447
1464
  )
1448
- self._record_placeholder_leakage(placeholder_count)
1465
+ self._record_postprocessing_artifacts(artifact_count)
1449
1466
  else:
1450
- response, placeholder_count = self._sanitize_aggregation_log_value(
1451
- response
1467
+ response, artifact_count = self._postprocess_aggregation_output(
1468
+ response,
1469
+ processing_context,
1452
1470
  )
1453
- self._record_placeholder_leakage(placeholder_count)
1471
+ self._record_postprocessing_artifacts(artifact_count)
1454
1472
  log_model_response(logger, "Aggregation structured response", response)
1455
1473
 
1456
1474
  if not isinstance(response, PlaybookAggregationOutput):
@@ -1462,13 +1480,14 @@ class PlaybookAggregator:
1462
1480
 
1463
1481
  return self._process_aggregation_response(response, cluster_playbooks)
1464
1482
  except Exception as exc:
1465
- sanitized_error, placeholder_count = self._sanitize_aggregation_log_value(
1466
- str(exc)
1483
+ processed_error, artifact_count = self._postprocess_aggregation_output(
1484
+ str(exc),
1485
+ processing_context,
1467
1486
  )
1468
- self._record_placeholder_leakage(placeholder_count)
1487
+ self._record_postprocessing_artifacts(artifact_count)
1469
1488
  logger.error(
1470
1489
  "AgentPlaybook aggregation failed due to %s, returning None.",
1471
- sanitized_error,
1490
+ processed_error,
1472
1491
  )
1473
1492
  return None
1474
1493
 
@@ -1482,7 +1501,7 @@ class PlaybookAggregator:
1482
1501
  response: Parsed PlaybookAggregationOutput from LLM
1483
1502
  cluster_playbooks: Cluster playbooks used only for non-user metadata
1484
1503
  such as playbook name and agent version. Callers may pass
1485
- prompt-sanitized copies here, so this method must not read
1504
+ prompt-preprocessed copies here, so this method must not read
1486
1505
  user-authored fields from them.
1487
1506
 
1488
1507
  Returns:
@@ -1490,7 +1509,6 @@ class PlaybookAggregator:
1490
1509
  """
1491
1510
  if not response:
1492
1511
  return None
1493
- response, _placeholder_count = self._sanitize_aggregation_response(response)
1494
1512
 
1495
1513
  structured = response.playbook
1496
1514
  if structured is None:
@@ -22,11 +22,15 @@ from reflexio.models.api_schema.service_schemas import (
22
22
  UserPlaybook,
23
23
  )
24
24
  from reflexio.models.config_schema import PlaybookConfig
25
+ from reflexio.server.extensions import get_service
25
26
  from reflexio.server.operation_limiter import run_with_operation_limit
26
27
  from reflexio.server.services.base_generation_service import (
27
28
  BaseGenerationService,
28
29
  StatusChangeOperation,
29
30
  )
31
+ from reflexio.server.services.playbook.aggregation_prompt_processing import (
32
+ AGGREGATION_PROMPT_PROCESSOR,
33
+ )
30
34
  from reflexio.server.services.playbook.components.aggregator import PlaybookAggregator
31
35
  from reflexio.server.services.playbook.components.extractor import PlaybookExtractor
32
36
  from reflexio.server.services.playbook.playbook_service_constants import (
@@ -537,16 +541,12 @@ class PlaybookGenerationService(
537
541
  )
538
542
 
539
543
  # Initialize and run aggregator (synchronous)
540
- from reflexio.server.services.playbook.user_detail_stripping import (
541
- create_aggregation_user_detail_stripper,
542
- )
543
-
544
- user_detail_stripper = create_aggregation_user_detail_stripper(
545
- self.request_context.configurator
546
- )
544
+ aggregation_prompt_processor = get_service(AGGREGATION_PROMPT_PROCESSOR)
547
545
  aggregator_kwargs = {}
548
- if user_detail_stripper is not None:
549
- aggregator_kwargs["user_detail_stripper"] = user_detail_stripper
546
+ if aggregation_prompt_processor is not None:
547
+ aggregator_kwargs["aggregation_prompt_processor"] = (
548
+ aggregation_prompt_processor
549
+ )
550
550
  aggregator = PlaybookAggregator(
551
551
  llm_client=self.client,
552
552
  request_context=self.request_context,
@@ -279,7 +279,7 @@ class ProfileExtractor:
279
279
  raw_profiles: list[dict],
280
280
  user_id: str,
281
281
  request_id: str,
282
- source_interaction_ids: list[int],
282
+ source_interaction_ids: list[int] | None = None,
283
283
  ) -> list[UserProfile]:
284
284
  """
285
285
  Convert raw profile dicts from LLM to UserProfile objects.
@@ -294,6 +294,7 @@ class ProfileExtractor:
294
294
  List of UserProfile objects
295
295
  """
296
296
  new_profiles = []
297
+ profile_source_interaction_ids = list(source_interaction_ids or [])
297
298
  for profile_content in raw_profiles:
298
299
  if (
299
300
  not isinstance(profile_content, dict)
@@ -322,7 +323,7 @@ class ProfileExtractor:
322
323
  expiration_timestamp=calculate_expiration_timestamp(now_ts, ttl),
323
324
  custom_features=custom_features or None,
324
325
  extractor_names=None,
325
- source_interaction_ids=source_interaction_ids,
326
+ source_interaction_ids=profile_source_interaction_ids,
326
327
  )
327
328
 
328
329
  new_profiles.append(added_profile)