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,189 @@
1
+ """Agent success evaluation result store methods for SQLite storage."""
2
+
3
+ import sqlite3
4
+ from typing import Any
5
+
6
+ from reflexio.models.api_schema.service_schemas import (
7
+ AgentSuccessEvaluationResult,
8
+ )
9
+
10
+ from .._base import (
11
+ SQLiteStorageBase,
12
+ _epoch_to_iso,
13
+ _json_dumps,
14
+ _row_to_eval_result,
15
+ )
16
+
17
+
18
+ class AgentEvaluationResultStoreMixin:
19
+ """Mixin providing agent success evaluation result CRUD for SQLite storage."""
20
+
21
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
22
+ _lock: Any
23
+ conn: sqlite3.Connection
24
+ _execute: Any
25
+ _fetchall: Any
26
+ _get_embedding: Any
27
+ _subject_ref_for_user_id: Any
28
+ _assert_subject_writable_locked: Any
29
+
30
+ # ------------------------------------------------------------------
31
+ # Agent Success Evaluation methods
32
+ # ------------------------------------------------------------------
33
+
34
+ @SQLiteStorageBase.handle_exceptions
35
+ def save_agent_success_evaluation_results(
36
+ self, results: list[AgentSuccessEvaluationResult]
37
+ ) -> None:
38
+ for result in results:
39
+ embedding_text = f"{result.failure_type} {result.failure_reason}"
40
+ if embedding_text.strip():
41
+ result.embedding = self._get_embedding(embedding_text)
42
+ else:
43
+ result.embedding = []
44
+
45
+ created_at_iso = _epoch_to_iso(result.created_at)
46
+ subject_ref = self._subject_ref_for_user_id(result.user_id)
47
+ with self._lock:
48
+ try:
49
+ self.conn.execute("BEGIN IMMEDIATE")
50
+ self._assert_subject_writable_locked(subject_ref)
51
+ self.conn.execute(
52
+ """INSERT INTO agent_success_evaluation_result
53
+ (user_id, session_id, agent_version, evaluation_name, is_success,
54
+ failure_type, failure_reason, regular_vs_shadow,
55
+ number_of_correction_per_session, user_turns_to_resolution,
56
+ is_escalated, embedding, created_at, governance_subject_ref)
57
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
58
+ (
59
+ result.user_id,
60
+ result.session_id,
61
+ result.agent_version,
62
+ result.evaluation_name,
63
+ int(result.is_success),
64
+ result.failure_type,
65
+ result.failure_reason,
66
+ result.regular_vs_shadow.value
67
+ if result.regular_vs_shadow
68
+ else None,
69
+ result.number_of_correction_per_session,
70
+ result.user_turns_to_resolution,
71
+ int(result.is_escalated),
72
+ _json_dumps(result.embedding) if result.embedding else None,
73
+ created_at_iso,
74
+ subject_ref,
75
+ ),
76
+ )
77
+ self.conn.commit()
78
+ except Exception:
79
+ self.conn.rollback()
80
+ raise
81
+
82
+ @SQLiteStorageBase.handle_exceptions
83
+ def get_agent_success_evaluation_results(
84
+ self, limit: int = 100, agent_version: str | None = None
85
+ ) -> list[AgentSuccessEvaluationResult]:
86
+ sql = "SELECT * FROM agent_success_evaluation_result"
87
+ params: list[Any] = []
88
+ if agent_version is not None:
89
+ sql += " WHERE agent_version = ?"
90
+ params.append(agent_version)
91
+ sql += " ORDER BY created_at DESC LIMIT ?"
92
+ params.append(limit)
93
+ rows = self._fetchall(sql, params)
94
+ return [_row_to_eval_result(r) for r in rows]
95
+
96
+ @SQLiteStorageBase.handle_exceptions
97
+ def get_agent_success_evaluation_results_in_window(
98
+ self,
99
+ from_ts: int,
100
+ to_ts: int,
101
+ agent_version: str | None = None,
102
+ limit: int | None = None,
103
+ ) -> list[AgentSuccessEvaluationResult]:
104
+ sql = """SELECT * FROM agent_success_evaluation_result
105
+ WHERE created_at >= ? AND created_at <= ?"""
106
+ params: list[Any] = [_epoch_to_iso(from_ts), _epoch_to_iso(to_ts)]
107
+ if agent_version is not None:
108
+ sql += " AND agent_version = ?"
109
+ params.append(agent_version)
110
+ sql += " ORDER BY created_at DESC"
111
+ if limit is not None:
112
+ sql += " LIMIT ?"
113
+ params.append(limit)
114
+ rows = self._fetchall(sql, params)
115
+ return [_row_to_eval_result(r) for r in rows]
116
+
117
+ @SQLiteStorageBase.handle_exceptions
118
+ def get_agent_success_evaluation_result_ids(
119
+ self,
120
+ user_id: str,
121
+ session_id: str,
122
+ evaluation_name: str,
123
+ agent_version: str,
124
+ ) -> list[int]:
125
+ rows = self._fetchall(
126
+ """SELECT result_id FROM agent_success_evaluation_result
127
+ WHERE user_id = ?
128
+ AND session_id = ?
129
+ AND evaluation_name = ?
130
+ AND agent_version = ?
131
+ ORDER BY created_at DESC""",
132
+ (user_id, session_id, evaluation_name, agent_version),
133
+ )
134
+ return [int(r["result_id"]) for r in rows]
135
+
136
+ @SQLiteStorageBase.handle_exceptions
137
+ def delete_all_agent_success_evaluation_results(self) -> None:
138
+ self._execute("DELETE FROM agent_success_evaluation_result")
139
+
140
+ @SQLiteStorageBase.handle_exceptions
141
+ def delete_agent_success_evaluation_results_for_session(
142
+ self,
143
+ user_id: str,
144
+ session_id: str,
145
+ evaluation_name: str,
146
+ agent_version: str,
147
+ ) -> int:
148
+ """Delete results scoped to (user_id, session_id, evaluation_name, agent_version).
149
+
150
+ Args:
151
+ user_id (str): User whose session results to clear.
152
+ session_id (str): Session whose results to clear.
153
+ evaluation_name (str): Which evaluator's results to clear.
154
+ agent_version (str): Agent version scope.
155
+
156
+ Returns:
157
+ int: Number of rows deleted.
158
+ """
159
+ cur = self._execute(
160
+ """DELETE FROM agent_success_evaluation_result
161
+ WHERE user_id = ?
162
+ AND session_id = ?
163
+ AND evaluation_name = ?
164
+ AND agent_version = ?""",
165
+ (user_id, session_id, evaluation_name, agent_version),
166
+ )
167
+ return cur.rowcount
168
+
169
+ @SQLiteStorageBase.handle_exceptions
170
+ def delete_agent_success_evaluation_results_by_ids(
171
+ self, result_ids: list[int]
172
+ ) -> int:
173
+ """Delete agent success eval result rows by primary key.
174
+
175
+ Args:
176
+ result_ids (list[int]): Primary-key result_ids to delete. An empty
177
+ list is a no-op that returns 0.
178
+
179
+ Returns:
180
+ int: Number of rows actually deleted (ignores non-existent ids).
181
+ """
182
+ if not result_ids:
183
+ return 0
184
+ placeholders = ",".join(["?"] * len(result_ids))
185
+ cur = self._execute(
186
+ f"DELETE FROM agent_success_evaluation_result WHERE result_id IN ({placeholders})",
187
+ list(result_ids),
188
+ )
189
+ return cur.rowcount
@@ -0,0 +1,247 @@
1
+ """Playbook optimization job store methods for SQLite storage."""
2
+
3
+ import sqlite3
4
+ from typing import Any
5
+
6
+ from reflexio.models.api_schema.service_schemas import (
7
+ PlaybookOptimizationCandidate,
8
+ PlaybookOptimizationEvaluation,
9
+ PlaybookOptimizationEvent,
10
+ PlaybookOptimizationJob,
11
+ )
12
+
13
+ from .._base import (
14
+ SQLiteStorageBase,
15
+ _json_dumps,
16
+ _json_loads,
17
+ )
18
+
19
+
20
+ def _row_to_playbook_optimization_candidate(
21
+ row: sqlite3.Row,
22
+ ) -> PlaybookOptimizationCandidate:
23
+ return PlaybookOptimizationCandidate(
24
+ candidate_id=row["candidate_id"],
25
+ job_id=row["job_id"],
26
+ candidate_index=row["candidate_index"],
27
+ content=row["content"],
28
+ parent_candidate_ids=_json_loads(row["parent_candidate_ids"]) or [],
29
+ aggregate_score=row["aggregate_score"],
30
+ is_winner=bool(row["is_winner"]),
31
+ metadata_json=row["metadata_json"] or "{}",
32
+ created_at=row["created_at"],
33
+ )
34
+
35
+
36
+ def _row_to_playbook_optimization_evaluation(
37
+ row: sqlite3.Row,
38
+ ) -> PlaybookOptimizationEvaluation:
39
+ return PlaybookOptimizationEvaluation(
40
+ evaluation_id=row["evaluation_id"],
41
+ job_id=row["job_id"],
42
+ candidate_id=row["candidate_id"],
43
+ target_kind=row["target_kind"],
44
+ target_id=row["target_id"],
45
+ scenario_user_playbook_id=row["scenario_user_playbook_id"],
46
+ source_interaction_ids=_json_loads(row["source_interaction_ids"]) or [],
47
+ score=row["score"],
48
+ verdict=row["verdict"],
49
+ likert=row["likert"],
50
+ rationale=row["rationale"],
51
+ asi_json=row["asi_json"],
52
+ incumbent_rollout_json=row["incumbent_rollout_json"],
53
+ candidate_rollout_json=row["candidate_rollout_json"],
54
+ created_at=row["created_at"],
55
+ )
56
+
57
+
58
+ class OptimizationJobStoreMixin:
59
+ """Mixin providing playbook optimization job/candidate/evaluation CRUD for SQLite storage."""
60
+
61
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
62
+ _lock: Any
63
+ conn: sqlite3.Connection
64
+ _execute: Any
65
+ _fetchall: Any
66
+
67
+ # ------------------------------------------------------------------
68
+ # Playbook optimizer methods
69
+ # ------------------------------------------------------------------
70
+
71
+ @SQLiteStorageBase.handle_exceptions
72
+ def create_playbook_optimization_job(
73
+ self, job: PlaybookOptimizationJob
74
+ ) -> PlaybookOptimizationJob:
75
+ with self._lock:
76
+ cur = self.conn.execute(
77
+ """INSERT INTO playbook_optimization_jobs
78
+ (target_kind, target_id, status, best_candidate_id,
79
+ successor_target_id, decision_reason, metadata_json,
80
+ created_at, updated_at)
81
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
82
+ (
83
+ job.target_kind,
84
+ job.target_id,
85
+ job.status,
86
+ job.best_candidate_id,
87
+ job.successor_target_id,
88
+ job.decision_reason,
89
+ job.metadata_json,
90
+ job.created_at,
91
+ job.updated_at,
92
+ ),
93
+ )
94
+ job.job_id = cur.lastrowid or 0
95
+ self.conn.commit()
96
+ return job
97
+
98
+ @SQLiteStorageBase.handle_exceptions
99
+ def update_playbook_optimization_job(
100
+ self,
101
+ job_id: int,
102
+ *,
103
+ status: str | None = None,
104
+ best_candidate_id: int | None = None,
105
+ successor_target_id: int | None = None,
106
+ decision_reason: str | None = None,
107
+ metadata_json: str | None = None,
108
+ ) -> None:
109
+ updates: list[str] = ["updated_at = strftime('%s','now')"]
110
+ params: list[Any] = []
111
+ if status is not None:
112
+ updates.append("status = ?")
113
+ params.append(status)
114
+ if best_candidate_id is not None:
115
+ updates.append("best_candidate_id = ?")
116
+ params.append(best_candidate_id)
117
+ if successor_target_id is not None:
118
+ updates.append("successor_target_id = ?")
119
+ params.append(successor_target_id)
120
+ if decision_reason is not None:
121
+ updates.append("decision_reason = ?")
122
+ params.append(decision_reason)
123
+ if metadata_json is not None:
124
+ updates.append("metadata_json = ?")
125
+ params.append(metadata_json)
126
+ params.append(job_id)
127
+ self._execute(
128
+ f"UPDATE playbook_optimization_jobs SET {', '.join(updates)} WHERE job_id = ?", # noqa: S608
129
+ tuple(params),
130
+ )
131
+
132
+ @SQLiteStorageBase.handle_exceptions
133
+ def insert_playbook_optimization_candidate(
134
+ self, candidate: PlaybookOptimizationCandidate
135
+ ) -> PlaybookOptimizationCandidate:
136
+ with self._lock:
137
+ cur = self.conn.execute(
138
+ """INSERT INTO playbook_optimization_candidates
139
+ (job_id, candidate_index, content, parent_candidate_ids,
140
+ aggregate_score, is_winner, metadata_json, created_at)
141
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
142
+ (
143
+ candidate.job_id,
144
+ candidate.candidate_index,
145
+ candidate.content,
146
+ _json_dumps(candidate.parent_candidate_ids) or "[]",
147
+ candidate.aggregate_score,
148
+ 1 if candidate.is_winner else 0,
149
+ candidate.metadata_json,
150
+ candidate.created_at,
151
+ ),
152
+ )
153
+ candidate.candidate_id = cur.lastrowid or 0
154
+ self.conn.commit()
155
+ return candidate
156
+
157
+ @SQLiteStorageBase.handle_exceptions
158
+ def list_playbook_optimization_candidates(
159
+ self, job_id: int
160
+ ) -> list[PlaybookOptimizationCandidate]:
161
+ rows = self._fetchall(
162
+ "SELECT * FROM playbook_optimization_candidates WHERE job_id = ? ORDER BY candidate_id ASC",
163
+ (job_id,),
164
+ )
165
+ return [_row_to_playbook_optimization_candidate(row) for row in rows]
166
+
167
+ @SQLiteStorageBase.handle_exceptions
168
+ def update_playbook_optimization_candidate(
169
+ self,
170
+ candidate_id: int,
171
+ *,
172
+ aggregate_score: float | None = None,
173
+ is_winner: bool | None = None,
174
+ ) -> None:
175
+ updates: list[str] = []
176
+ params: list[Any] = []
177
+ if aggregate_score is not None:
178
+ updates.append("aggregate_score = ?")
179
+ params.append(aggregate_score)
180
+ if is_winner is not None:
181
+ updates.append("is_winner = ?")
182
+ params.append(1 if is_winner else 0)
183
+ if not updates:
184
+ return
185
+ params.append(candidate_id)
186
+ self._execute(
187
+ f"UPDATE playbook_optimization_candidates SET {', '.join(updates)} WHERE candidate_id = ?", # noqa: S608
188
+ tuple(params),
189
+ )
190
+
191
+ @SQLiteStorageBase.handle_exceptions
192
+ def insert_playbook_optimization_evaluation(
193
+ self, evaluation: PlaybookOptimizationEvaluation
194
+ ) -> PlaybookOptimizationEvaluation:
195
+ with self._lock:
196
+ cur = self.conn.execute(
197
+ """INSERT INTO playbook_optimization_evaluations
198
+ (job_id, candidate_id, target_kind, target_id,
199
+ scenario_user_playbook_id, source_interaction_ids, score,
200
+ verdict, likert, rationale, asi_json, incumbent_rollout_json,
201
+ candidate_rollout_json, created_at)
202
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
203
+ (
204
+ evaluation.job_id,
205
+ evaluation.candidate_id,
206
+ evaluation.target_kind,
207
+ evaluation.target_id,
208
+ evaluation.scenario_user_playbook_id,
209
+ _json_dumps(evaluation.source_interaction_ids) or "[]",
210
+ evaluation.score,
211
+ evaluation.verdict,
212
+ evaluation.likert,
213
+ evaluation.rationale,
214
+ evaluation.asi_json,
215
+ evaluation.incumbent_rollout_json,
216
+ evaluation.candidate_rollout_json,
217
+ evaluation.created_at,
218
+ ),
219
+ )
220
+ evaluation.evaluation_id = cur.lastrowid or 0
221
+ self.conn.commit()
222
+ return evaluation
223
+
224
+ @SQLiteStorageBase.handle_exceptions
225
+ def list_playbook_optimization_evaluations(
226
+ self, job_id: int
227
+ ) -> list[PlaybookOptimizationEvaluation]:
228
+ rows = self._fetchall(
229
+ "SELECT * FROM playbook_optimization_evaluations WHERE job_id = ? ORDER BY evaluation_id ASC",
230
+ (job_id,),
231
+ )
232
+ return [_row_to_playbook_optimization_evaluation(row) for row in rows]
233
+
234
+ @SQLiteStorageBase.handle_exceptions
235
+ def insert_playbook_optimization_event(
236
+ self, event: PlaybookOptimizationEvent
237
+ ) -> PlaybookOptimizationEvent:
238
+ with self._lock:
239
+ cur = self.conn.execute(
240
+ """INSERT INTO playbook_optimization_events
241
+ (job_id, event_type, payload_json, created_at)
242
+ VALUES (?, ?, ?, ?)""",
243
+ (event.job_id, event.event_type, event.payload_json, event.created_at),
244
+ )
245
+ event.event_id = cur.lastrowid or 0
246
+ self.conn.commit()
247
+ return event
@@ -0,0 +1,145 @@
1
+ """Agent playbook source-linkage methods for SQLite storage."""
2
+
3
+ import sqlite3
4
+ from collections.abc import Sequence
5
+ from typing import Any
6
+
7
+ from reflexio.models.api_schema.service_schemas import AgentPlaybookSourceWindow
8
+
9
+ from .._base import (
10
+ SQLiteStorageBase,
11
+ _json_dumps,
12
+ _json_loads,
13
+ )
14
+
15
+
16
+ class PlaybookSourceLinkageMixin:
17
+ """Mixin providing agent playbook source-linkage CRUD for SQLite storage."""
18
+
19
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
20
+ _lock: Any
21
+ conn: sqlite3.Connection
22
+ _fetchall: Any
23
+ _subject_ref_for_user_id: Any
24
+ _assert_subject_writable_locked: Any
25
+
26
+ @SQLiteStorageBase.handle_exceptions
27
+ def set_source_user_playbook_ids_for_agent_playbook(
28
+ self, agent_playbook_id: int, user_playbook_ids: list[int]
29
+ ) -> None:
30
+ self.set_source_windows_for_agent_playbook(
31
+ agent_playbook_id,
32
+ [
33
+ AgentPlaybookSourceWindow(
34
+ user_playbook_id=upid, source_interaction_ids=[]
35
+ )
36
+ for upid in user_playbook_ids
37
+ ],
38
+ )
39
+
40
+ @SQLiteStorageBase.handle_exceptions
41
+ def get_source_user_playbook_ids_for_agent_playbook(
42
+ self, agent_playbook_id: int
43
+ ) -> list[int]:
44
+ return [
45
+ window.user_playbook_id
46
+ for window in self.get_source_windows_for_agent_playbook(agent_playbook_id)
47
+ ]
48
+
49
+ @SQLiteStorageBase.handle_exceptions
50
+ def get_source_user_playbook_ids_for_agent_playbooks(
51
+ self, agent_playbook_ids: Sequence[int]
52
+ ) -> dict[int, list[int]]:
53
+ if not agent_playbook_ids:
54
+ return {}
55
+ unique_ids = list(dict.fromkeys(int(apid) for apid in agent_playbook_ids))
56
+ ph = ",".join("?" for _ in unique_ids)
57
+ rows = self._fetchall(
58
+ f"""SELECT agent_playbook_id, user_playbook_id
59
+ FROM agent_playbook_source_user_playbooks
60
+ WHERE agent_playbook_id IN ({ph})
61
+ ORDER BY agent_playbook_id ASC, user_playbook_id ASC""",
62
+ unique_ids,
63
+ )
64
+ by_agent_id: dict[int, list[int]] = {apid: [] for apid in unique_ids}
65
+ seen_by_agent_id: dict[int, set[int]] = {apid: set() for apid in unique_ids}
66
+ for row in rows:
67
+ agent_playbook_id = int(row["agent_playbook_id"])
68
+ user_playbook_id = int(row["user_playbook_id"])
69
+ seen = seen_by_agent_id.setdefault(agent_playbook_id, set())
70
+ if user_playbook_id not in seen:
71
+ by_agent_id.setdefault(agent_playbook_id, []).append(user_playbook_id)
72
+ seen.add(user_playbook_id)
73
+ return by_agent_id
74
+
75
+ @SQLiteStorageBase.handle_exceptions
76
+ def set_source_windows_for_agent_playbook(
77
+ self,
78
+ agent_playbook_id: int,
79
+ source_windows: list[AgentPlaybookSourceWindow],
80
+ ) -> None:
81
+ by_id: dict[int, list[int]] = {}
82
+ for window in source_windows:
83
+ ids = by_id.setdefault(window.user_playbook_id, [])
84
+ seen = set(ids)
85
+ for source_id in window.source_interaction_ids:
86
+ if source_id not in seen:
87
+ ids.append(source_id)
88
+ seen.add(source_id)
89
+ with self._lock:
90
+ try:
91
+ self.conn.execute("BEGIN IMMEDIATE")
92
+ for user_playbook_id in by_id:
93
+ row = self.conn.execute(
94
+ """SELECT user_id, governance_subject_ref FROM user_playbooks
95
+ WHERE user_playbook_id = ?""",
96
+ (user_playbook_id,),
97
+ ).fetchone()
98
+ if row is None:
99
+ raise ValueError(
100
+ f"User playbook {user_playbook_id} not found for source window"
101
+ )
102
+ subject_ref = row["governance_subject_ref"]
103
+ if not isinstance(subject_ref, str) or not subject_ref:
104
+ subject_ref = self._subject_ref_for_user_id(str(row["user_id"]))
105
+ self._assert_subject_writable_locked(subject_ref)
106
+ self.conn.execute(
107
+ "DELETE FROM agent_playbook_source_user_playbooks WHERE agent_playbook_id = ?",
108
+ (agent_playbook_id,),
109
+ )
110
+ self.conn.executemany(
111
+ """INSERT OR IGNORE INTO agent_playbook_source_user_playbooks
112
+ (agent_playbook_id, user_playbook_id, source_interaction_ids)
113
+ VALUES (?, ?, ?)""",
114
+ [
115
+ (
116
+ agent_playbook_id,
117
+ upid,
118
+ _json_dumps(source_interaction_ids) or "[]",
119
+ )
120
+ for upid, source_interaction_ids in by_id.items()
121
+ ],
122
+ )
123
+ self.conn.commit()
124
+ except Exception:
125
+ self.conn.rollback()
126
+ raise
127
+
128
+ @SQLiteStorageBase.handle_exceptions
129
+ def get_source_windows_for_agent_playbook(
130
+ self, agent_playbook_id: int
131
+ ) -> list[AgentPlaybookSourceWindow]:
132
+ rows = self._fetchall(
133
+ """SELECT user_playbook_id, source_interaction_ids
134
+ FROM agent_playbook_source_user_playbooks
135
+ WHERE agent_playbook_id = ?
136
+ ORDER BY user_playbook_id ASC""",
137
+ (agent_playbook_id,),
138
+ )
139
+ return [
140
+ AgentPlaybookSourceWindow(
141
+ user_playbook_id=int(row["user_playbook_id"]),
142
+ source_interaction_ids=_json_loads(row["source_interaction_ids"]) or [],
143
+ )
144
+ for row in rows
145
+ ]