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
@@ -1,10 +1,16 @@
1
- """SQLite storage for resumable extraction agent runs."""
1
+ """Shared row/datetime helpers for SQLite resumable extraction agent runs.
2
+
3
+ Helpers-only residual for the ``_agent_run.py`` decomposition: all twenty-three
4
+ agent-run methods now live in the three ``agent_run`` sub-mixins
5
+ (``SQLiteAgentRunStoreMixin`` / ``SQLitePendingToolCallStoreMixin`` /
6
+ ``SQLiteRunToolDependencyStoreMixin``), which import the ``_row_to_*`` /
7
+ ``_dt`` / ``_dt_str`` helpers below rather than duplicating them.
8
+ """
2
9
 
3
10
  from __future__ import annotations
4
11
 
5
12
  import sqlite3
6
- from datetime import UTC, datetime, timedelta
7
- from typing import Any
13
+ from datetime import UTC, datetime
8
14
 
9
15
  from reflexio.server.services.storage.storage_base import (
10
16
  AgentBinding,
@@ -12,15 +18,13 @@ from reflexio.server.services.storage.storage_base import (
12
18
  AgentRunStatus,
13
19
  PendingToolCallRecord,
14
20
  PendingToolCallStatus,
15
- PendingToolCallUpsertResult,
16
21
  PriorAnswerMatch,
17
22
  RunToolDependencyKind,
18
23
  RunToolDependencyRecord,
19
24
  embedding_similarity,
20
- not_applicable_tool_result,
21
25
  )
22
26
 
23
- from ._base import SQLiteStorageBase, _json_dumps, _json_loads
27
+ from ._base import _json_loads
24
28
 
25
29
 
26
30
  def _dt(value: str | None) -> datetime | None:
@@ -138,1164 +142,3 @@ def _row_to_run_tool_dependency(row: sqlite3.Row) -> RunToolDependencyRecord:
138
142
  consumed_at=_dt(data.get("consumed_at")),
139
143
  created_at=_dt(data.get("created_at")),
140
144
  )
141
-
142
-
143
- class SQLiteAgentRunMixin:
144
- """SQLite-backed resumable extraction run storage."""
145
-
146
- _lock: Any
147
- conn: sqlite3.Connection
148
- _fetchone: Any
149
- _fetchall: Any
150
- _current_timestamp: Any
151
- org_id: str
152
-
153
- def _finalize_runs_without_pending_dependencies_unlocked(self, now_s: str) -> None:
154
- self.conn.execute(
155
- """
156
- UPDATE _agent_runs
157
- SET status = ?,
158
- finalized_at = COALESCE(finalized_at, ?),
159
- updated_at = ?
160
- WHERE status = ?
161
- AND NOT EXISTS (
162
- SELECT 1
163
- FROM _run_tool_dependencies d
164
- JOIN _pending_tool_calls p
165
- ON p.id = d.pending_tool_call_id
166
- WHERE d.run_id = _agent_runs.id
167
- AND d.resolved_at IS NULL
168
- AND d.consumed_at IS NULL
169
- AND p.status = ?
170
- )
171
- """,
172
- (
173
- AgentRunStatus.FINALIZED.value,
174
- now_s,
175
- now_s,
176
- AgentRunStatus.FINALIZED_PENDING_TOOL.value,
177
- PendingToolCallStatus.PENDING.value,
178
- ),
179
- )
180
-
181
- def _mark_runs_ready_with_actionable_dependencies_unlocked(
182
- self, now_s: str, *, pending_tool_call_id: str
183
- ) -> None:
184
- self.conn.execute(
185
- """
186
- UPDATE _agent_runs
187
- SET status = ?,
188
- updated_at = ?
189
- WHERE status IN (?, ?)
190
- AND EXISTS (
191
- SELECT 1
192
- FROM _run_tool_dependencies changed
193
- WHERE changed.run_id = _agent_runs.id
194
- AND changed.pending_tool_call_id = ?
195
- )
196
- AND EXISTS (
197
- SELECT 1
198
- FROM _run_tool_dependencies d
199
- JOIN _pending_tool_calls p
200
- ON p.id = d.pending_tool_call_id
201
- WHERE d.run_id = _agent_runs.id
202
- AND d.resolved_at IS NOT NULL
203
- AND d.consumed_at IS NULL
204
- AND p.status = ?
205
- AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
206
- )
207
- """,
208
- (
209
- AgentRunStatus.RESUME_READY.value,
210
- now_s,
211
- AgentRunStatus.FINALIZED.value,
212
- AgentRunStatus.FINALIZED_PENDING_TOOL.value,
213
- pending_tool_call_id,
214
- PendingToolCallStatus.RESOLVED.value,
215
- ),
216
- )
217
-
218
- def _finalize_runs_without_actionable_dependencies_unlocked(
219
- self, now_s: str, *, pending_tool_call_id: str
220
- ) -> None:
221
- self.conn.execute(
222
- """
223
- UPDATE _agent_runs
224
- SET status = ?,
225
- finalized_at = COALESCE(finalized_at, ?),
226
- updated_at = ?
227
- WHERE status IN (?, ?)
228
- AND EXISTS (
229
- SELECT 1
230
- FROM _run_tool_dependencies changed
231
- WHERE changed.run_id = _agent_runs.id
232
- AND changed.pending_tool_call_id = ?
233
- )
234
- AND NOT EXISTS (
235
- SELECT 1
236
- FROM _run_tool_dependencies d
237
- JOIN _pending_tool_calls p
238
- ON p.id = d.pending_tool_call_id
239
- WHERE d.run_id = _agent_runs.id
240
- AND d.resolved_at IS NULL
241
- AND d.consumed_at IS NULL
242
- AND p.status = ?
243
- )
244
- AND NOT EXISTS (
245
- SELECT 1
246
- FROM _run_tool_dependencies d
247
- JOIN _pending_tool_calls p
248
- ON p.id = d.pending_tool_call_id
249
- WHERE d.run_id = _agent_runs.id
250
- AND d.resolved_at IS NOT NULL
251
- AND d.consumed_at IS NULL
252
- AND p.status = ?
253
- AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
254
- )
255
- """,
256
- (
257
- AgentRunStatus.FINALIZED.value,
258
- now_s,
259
- now_s,
260
- AgentRunStatus.FINALIZED_PENDING_TOOL.value,
261
- AgentRunStatus.RESUME_READY.value,
262
- pending_tool_call_id,
263
- PendingToolCallStatus.PENDING.value,
264
- PendingToolCallStatus.RESOLVED.value,
265
- ),
266
- )
267
-
268
- @SQLiteStorageBase.handle_exceptions
269
- def create_agent_run(self, record: AgentRunRecord) -> AgentRunRecord:
270
- binding = record.binding
271
- with self._lock:
272
- self.conn.execute(
273
- """
274
- INSERT INTO _agent_runs (
275
- id, org_id, extractor_kind, user_id,
276
- request_id, agent_version, source, source_interaction_ids,
277
- window_start_interaction_id, window_end_interaction_id,
278
- extractor_config_hash, status, generation_request_snapshot,
279
- service_config_snapshot, agent_context_snapshot,
280
- committed_output, pending_tool_call_ids, max_steps_remaining,
281
- resume_attempts, finalization_attempts, next_resume_at,
282
- claimed_by, claimed_at, agent_completed_at, finalized_at,
283
- expires_at, last_error
284
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
285
- """,
286
- (
287
- record.id,
288
- binding.org_id,
289
- binding.extractor_kind,
290
- binding.user_id,
291
- binding.request_id,
292
- binding.agent_version,
293
- binding.source,
294
- _json_dumps(binding.source_interaction_ids),
295
- binding.window_start_interaction_id,
296
- binding.window_end_interaction_id,
297
- binding.extractor_config_hash,
298
- record.status.value,
299
- _json_dumps(record.generation_request_snapshot),
300
- _json_dumps(record.service_config_snapshot),
301
- record.agent_context_snapshot,
302
- _json_dumps(record.committed_output),
303
- _json_dumps(record.pending_tool_call_ids),
304
- record.max_steps_remaining,
305
- record.resume_attempts,
306
- record.finalization_attempts,
307
- _dt_str(record.next_resume_at),
308
- record.claimed_by,
309
- _dt_str(record.claimed_at),
310
- _dt_str(record.agent_completed_at),
311
- _dt_str(record.finalized_at),
312
- _dt_str(record.expires_at),
313
- record.last_error,
314
- ),
315
- )
316
- self.conn.commit()
317
- stored = self.get_agent_run(record.id)
318
- if stored is None: # pragma: no cover
319
- raise RuntimeError(f"Failed to create agent run {record.id}")
320
- return stored
321
-
322
- @SQLiteStorageBase.handle_exceptions
323
- def get_agent_run(self, run_id: str) -> AgentRunRecord | None:
324
- row = self._fetchone("SELECT * FROM _agent_runs WHERE id = ?", (run_id,))
325
- return _row_to_agent_run(row) if row else None
326
-
327
- @SQLiteStorageBase.handle_exceptions
328
- def update_agent_run_status(
329
- self,
330
- run_id: str,
331
- status: AgentRunStatus,
332
- *,
333
- committed_output: dict[str, Any] | None = None,
334
- pending_tool_call_ids: list[str] | None = None,
335
- max_steps_remaining: int | None = None,
336
- next_resume_at: datetime | None = None,
337
- last_error: str | None = None,
338
- increment_finalization_attempts: bool = False,
339
- expected_statuses: tuple[AgentRunStatus, ...] | None = None,
340
- ) -> AgentRunRecord | None:
341
- current_timestamp = self._current_timestamp()
342
- assignments = ["status = ?", "updated_at = ?"]
343
- params: list[Any] = [status.value, current_timestamp]
344
- if committed_output is not None:
345
- assignments.append("committed_output = ?")
346
- params.append(_json_dumps(committed_output))
347
- if pending_tool_call_ids is not None:
348
- assignments.append("pending_tool_call_ids = ?")
349
- params.append(_json_dumps(pending_tool_call_ids))
350
- if max_steps_remaining is not None:
351
- assignments.append("max_steps_remaining = ?")
352
- params.append(max(0, max_steps_remaining))
353
- if next_resume_at is not None:
354
- assignments.append("next_resume_at = ?")
355
- params.append(_dt_str(next_resume_at))
356
- if last_error is not None:
357
- assignments.append("last_error = ?")
358
- params.append(last_error)
359
- if increment_finalization_attempts:
360
- assignments.append("finalization_attempts = finalization_attempts + 1")
361
- if status == AgentRunStatus.AGENT_COMPLETED:
362
- assignments.append("agent_completed_at = ?")
363
- params.append(current_timestamp)
364
- if status in (AgentRunStatus.FINALIZED, AgentRunStatus.FINALIZED_PENDING_TOOL):
365
- assignments.append("finalized_at = ?")
366
- params.append(current_timestamp)
367
- params.append(run_id)
368
- status_filter = ""
369
- if expected_statuses:
370
- placeholders = ",".join("?" for _ in expected_statuses)
371
- status_filter = f" AND status IN ({placeholders})"
372
- params.extend(expected.value for expected in expected_statuses)
373
- with self._lock:
374
- self.conn.execute(
375
- f"UPDATE _agent_runs SET {', '.join(assignments)} WHERE id = ?{status_filter}",
376
- params,
377
- )
378
- self.conn.commit()
379
- return self.get_agent_run(run_id)
380
-
381
- @SQLiteStorageBase.handle_exceptions
382
- def fail_running_agent_runs_for_request(
383
- self,
384
- *,
385
- org_id: str,
386
- extractor_kind: str,
387
- user_id: str | None,
388
- request_id: str,
389
- last_error: str,
390
- ) -> int:
391
- current_timestamp = self._current_timestamp()
392
- with self._lock:
393
- cursor = self.conn.execute(
394
- """
395
- UPDATE _agent_runs
396
- SET status = ?,
397
- updated_at = ?,
398
- last_error = ?
399
- WHERE org_id = ?
400
- AND extractor_kind = ?
401
- AND user_id IS ?
402
- AND request_id = ?
403
- AND status IN (?, ?)
404
- """,
405
- (
406
- AgentRunStatus.FAILED.value,
407
- current_timestamp,
408
- last_error,
409
- org_id,
410
- extractor_kind,
411
- user_id,
412
- request_id,
413
- AgentRunStatus.RUNNING.value,
414
- AgentRunStatus.RESUMING.value,
415
- ),
416
- )
417
- self.conn.commit()
418
- return cursor.rowcount
419
-
420
- @SQLiteStorageBase.handle_exceptions
421
- def create_pending_tool_call(
422
- self, record: PendingToolCallRecord
423
- ) -> PendingToolCallRecord:
424
- with self._lock:
425
- self.conn.execute(
426
- """
427
- INSERT INTO _pending_tool_calls (
428
- id, org_id, user_id, scope, scope_hash, tool_name, dedup_key,
429
- status, question_text, answer_format, args, tags, result,
430
- embedding, superseded_by, resolved_at, expires_at, cache_until,
431
- valid_until
432
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
433
- """,
434
- (
435
- record.id,
436
- record.org_id,
437
- record.user_id,
438
- _json_dumps(record.scope),
439
- record.scope_hash,
440
- record.tool_name,
441
- record.dedup_key,
442
- record.status.value,
443
- record.question_text,
444
- record.answer_format,
445
- _json_dumps(record.args),
446
- _json_dumps(record.tags),
447
- _json_dumps(record.result),
448
- _json_dumps(record.embedding),
449
- record.superseded_by,
450
- _dt_str(record.resolved_at),
451
- _dt_str(record.expires_at),
452
- _dt_str(record.cache_until),
453
- _dt_str(record.valid_until),
454
- ),
455
- )
456
- self.conn.commit()
457
- stored = self.get_pending_tool_call(record.id)
458
- if stored is None: # pragma: no cover
459
- raise RuntimeError(f"Failed to create pending tool call {record.id}")
460
- return stored
461
-
462
- def _insert_pending_tool_call_unlocked(self, record: PendingToolCallRecord) -> None:
463
- self.conn.execute(
464
- """
465
- INSERT INTO _pending_tool_calls (
466
- id, org_id, user_id, scope, scope_hash, tool_name, dedup_key,
467
- status, question_text, answer_format, args, tags, result,
468
- embedding, superseded_by, resolved_at, expires_at, cache_until,
469
- valid_until
470
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
471
- """,
472
- (
473
- record.id,
474
- record.org_id,
475
- record.user_id,
476
- _json_dumps(record.scope),
477
- record.scope_hash,
478
- record.tool_name,
479
- record.dedup_key,
480
- record.status.value,
481
- record.question_text,
482
- record.answer_format,
483
- _json_dumps(record.args),
484
- _json_dumps(record.tags),
485
- _json_dumps(record.result),
486
- _json_dumps(record.embedding),
487
- record.superseded_by,
488
- _dt_str(record.resolved_at),
489
- _dt_str(record.expires_at),
490
- _dt_str(record.cache_until),
491
- _dt_str(record.valid_until),
492
- ),
493
- )
494
-
495
- @SQLiteStorageBase.handle_exceptions
496
- def create_or_attach_pending_tool_call(
497
- self,
498
- *,
499
- record: PendingToolCallRecord,
500
- dependency: RunToolDependencyRecord,
501
- now: datetime | None = None,
502
- ) -> PendingToolCallUpsertResult:
503
- current = now or datetime.now(UTC)
504
- created = False
505
- pending_tool_call_id = record.id
506
- with self._lock:
507
- self.conn.execute("BEGIN IMMEDIATE")
508
- try:
509
- row = self.conn.execute(
510
- """
511
- SELECT * FROM _pending_tool_calls
512
- WHERE org_id = ?
513
- AND scope_hash = ?
514
- AND tool_name = ?
515
- AND dedup_key = ?
516
- AND status = ?
517
- AND cache_until > ?
518
- ORDER BY created_at ASC
519
- LIMIT 1
520
- """,
521
- (
522
- record.org_id,
523
- record.scope_hash,
524
- record.tool_name,
525
- record.dedup_key,
526
- PendingToolCallStatus.PENDING.value,
527
- _dt_str(current),
528
- ),
529
- ).fetchone()
530
- pending_tool_call_id = row["id"] if row is not None else record.id
531
- if row is None:
532
- self._insert_pending_tool_call_unlocked(record)
533
- created = True
534
- self.conn.execute(
535
- """
536
- INSERT OR IGNORE INTO _run_tool_dependencies (
537
- run_id, pending_tool_call_id, dependency_kind,
538
- resolved_at, consumed_at
539
- ) VALUES (?,?,?,?,?)
540
- """,
541
- (
542
- dependency.run_id,
543
- pending_tool_call_id,
544
- dependency.dependency_kind.value,
545
- _dt_str(dependency.resolved_at),
546
- _dt_str(dependency.consumed_at),
547
- ),
548
- )
549
- except Exception:
550
- self.conn.rollback()
551
- raise
552
- else:
553
- self.conn.commit()
554
-
555
- stored = self.get_pending_tool_call(pending_tool_call_id)
556
- if stored is None: # pragma: no cover
557
- raise RuntimeError("Failed to create or attach pending tool call")
558
- return PendingToolCallUpsertResult(pending_tool_call=stored, created=created)
559
-
560
- @SQLiteStorageBase.handle_exceptions
561
- def get_pending_tool_call(self, call_id: str) -> PendingToolCallRecord | None:
562
- row = self._fetchone(
563
- "SELECT * FROM _pending_tool_calls WHERE id = ?", (call_id,)
564
- )
565
- return _row_to_pending_tool_call(row) if row else None
566
-
567
- @SQLiteStorageBase.handle_exceptions
568
- def list_pending_tool_calls(
569
- self,
570
- *,
571
- status: PendingToolCallStatus | None = None,
572
- limit: int = 100,
573
- ) -> list[PendingToolCallRecord]:
574
- bounded_limit = max(1, min(limit, 500))
575
- params: list[Any] = [self.org_id]
576
- status_clause = ""
577
- if status is not None:
578
- status_clause = "AND status = ?"
579
- params.append(status.value)
580
- params.append(bounded_limit)
581
- rows = self._fetchall(
582
- f"""
583
- SELECT * FROM _pending_tool_calls
584
- WHERE org_id = ?
585
- {status_clause}
586
- ORDER BY created_at DESC, id ASC
587
- LIMIT ?
588
- """,
589
- tuple(params),
590
- )
591
- return [_row_to_pending_tool_call(row) for row in rows]
592
-
593
- @SQLiteStorageBase.handle_exceptions
594
- def cancel_pending_tool_call(
595
- self,
596
- call_id: str,
597
- *,
598
- cancelled_at: datetime | None = None,
599
- ) -> PendingToolCallRecord | None:
600
- now = cancelled_at or datetime.now(UTC)
601
- now_s = _dt_str(now)
602
- with self._lock:
603
- self.conn.execute(
604
- """
605
- UPDATE _pending_tool_calls
606
- SET status = ?
607
- WHERE id = ?
608
- AND status = ?
609
- """,
610
- (
611
- PendingToolCallStatus.CANCELLED.value,
612
- call_id,
613
- PendingToolCallStatus.PENDING.value,
614
- ),
615
- )
616
- self.conn.execute(
617
- """
618
- UPDATE _run_tool_dependencies
619
- SET resolved_at = ?
620
- WHERE pending_tool_call_id = ? AND resolved_at IS NULL
621
- """,
622
- (now_s, call_id),
623
- )
624
- self._finalize_runs_without_pending_dependencies_unlocked(now_s or "")
625
- self.conn.commit()
626
- return self.get_pending_tool_call(call_id)
627
-
628
- @SQLiteStorageBase.handle_exceptions
629
- def expire_pending_tool_calls(
630
- self,
631
- *,
632
- now: datetime | None = None,
633
- limit: int = 100,
634
- ) -> int:
635
- current = now or datetime.now(UTC)
636
- now_s = _dt_str(current)
637
- bounded_limit = max(1, min(limit, 500))
638
- with self._lock:
639
- self.conn.execute("BEGIN IMMEDIATE")
640
- try:
641
- rows = self.conn.execute(
642
- """
643
- SELECT id
644
- FROM _pending_tool_calls
645
- WHERE status = ?
646
- AND expires_at IS NOT NULL
647
- AND expires_at <= ?
648
- ORDER BY expires_at ASC, created_at ASC, id ASC
649
- LIMIT ?
650
- """,
651
- (
652
- PendingToolCallStatus.PENDING.value,
653
- now_s,
654
- bounded_limit,
655
- ),
656
- ).fetchall()
657
- call_ids = [row["id"] for row in rows]
658
- if not call_ids:
659
- self.conn.commit()
660
- return 0
661
-
662
- placeholders = ",".join("?" for _ in call_ids)
663
- self.conn.execute(
664
- f"""
665
- UPDATE _pending_tool_calls
666
- SET status = ?
667
- WHERE id IN ({placeholders})
668
- AND status = ?
669
- """,
670
- (
671
- PendingToolCallStatus.EXPIRED.value,
672
- *call_ids,
673
- PendingToolCallStatus.PENDING.value,
674
- ),
675
- )
676
- self.conn.execute(
677
- f"""
678
- UPDATE _run_tool_dependencies
679
- SET resolved_at = ?
680
- WHERE pending_tool_call_id IN ({placeholders})
681
- AND resolved_at IS NULL
682
- """,
683
- (now_s, *call_ids),
684
- )
685
- self._finalize_runs_without_pending_dependencies_unlocked(now_s or "")
686
- except Exception:
687
- self.conn.rollback()
688
- raise
689
- else:
690
- self.conn.commit()
691
- return len(call_ids)
692
-
693
- @SQLiteStorageBase.handle_exceptions
694
- def find_active_pending_tool_call(
695
- self,
696
- *,
697
- org_id: str,
698
- scope_hash: str,
699
- tool_name: str,
700
- dedup_key: str,
701
- now: datetime | None = None,
702
- ) -> PendingToolCallRecord | None:
703
- now_s = _dt_str(now or datetime.now(UTC))
704
- row = self._fetchone(
705
- """
706
- SELECT * FROM _pending_tool_calls
707
- WHERE org_id = ?
708
- AND scope_hash = ?
709
- AND tool_name = ?
710
- AND dedup_key = ?
711
- AND status = ?
712
- AND cache_until > ?
713
- ORDER BY created_at ASC
714
- LIMIT 1
715
- """,
716
- (
717
- org_id,
718
- scope_hash,
719
- tool_name,
720
- dedup_key,
721
- PendingToolCallStatus.PENDING.value,
722
- now_s,
723
- ),
724
- )
725
- return _row_to_pending_tool_call(row) if row else None
726
-
727
- @SQLiteStorageBase.handle_exceptions
728
- def search_prior_tool_calls(
729
- self,
730
- *,
731
- org_id: str,
732
- scope_hash: str,
733
- tool_name: str,
734
- query_embedding: list[float] | None = None,
735
- now: datetime | None = None,
736
- limit: int = 8,
737
- ) -> list[PriorAnswerMatch]:
738
- current = now or datetime.now(UTC)
739
- bounded_limit = max(1, min(limit, 50))
740
- rows = self._fetchall(
741
- """
742
- SELECT * FROM _pending_tool_calls
743
- WHERE org_id = ?
744
- AND scope_hash = ?
745
- AND tool_name = ?
746
- AND (
747
- (
748
- status = ?
749
- AND (expires_at IS NULL OR expires_at > ?)
750
- )
751
- OR (
752
- status = ?
753
- AND (valid_until IS NULL OR valid_until > ?)
754
- )
755
- )
756
- ORDER BY
757
- CASE status WHEN ? THEN 0 ELSE 1 END,
758
- COALESCE(resolved_at, created_at) DESC,
759
- id ASC
760
- """,
761
- (
762
- org_id,
763
- scope_hash,
764
- tool_name,
765
- PendingToolCallStatus.PENDING.value,
766
- _dt_str(current),
767
- PendingToolCallStatus.RESOLVED.value,
768
- _dt_str(current),
769
- PendingToolCallStatus.RESOLVED.value,
770
- ),
771
- )
772
- seen_resolved_dedup_keys: set[str] = set()
773
- records: list[PendingToolCallRecord] = []
774
- for row in rows:
775
- record = _row_to_pending_tool_call(row)
776
- if record.status == PendingToolCallStatus.RESOLVED:
777
- if record.dedup_key in seen_resolved_dedup_keys:
778
- continue
779
- seen_resolved_dedup_keys.add(record.dedup_key)
780
- records.append(record)
781
- matches = [
782
- _record_to_prior_answer_match(record, query_embedding=query_embedding)
783
- for record in records
784
- ]
785
- if query_embedding:
786
- matches.sort(
787
- key=lambda match: (
788
- match.similarity is not None,
789
- match.similarity or -1.0,
790
- match.resolved_at
791
- or match.created_at
792
- or datetime.min.replace(tzinfo=UTC),
793
- ),
794
- reverse=True,
795
- )
796
- return matches[:bounded_limit]
797
-
798
- @SQLiteStorageBase.handle_exceptions
799
- def attach_run_tool_dependency(
800
- self, record: RunToolDependencyRecord
801
- ) -> RunToolDependencyRecord:
802
- with self._lock:
803
- self.conn.execute(
804
- """
805
- INSERT OR IGNORE INTO _run_tool_dependencies (
806
- run_id, pending_tool_call_id, dependency_kind, resolved_at,
807
- consumed_at
808
- ) VALUES (?,?,?,?,?)
809
- """,
810
- (
811
- record.run_id,
812
- record.pending_tool_call_id,
813
- record.dependency_kind.value,
814
- _dt_str(record.resolved_at),
815
- _dt_str(record.consumed_at),
816
- ),
817
- )
818
- self.conn.commit()
819
- row = self._fetchone(
820
- """
821
- SELECT * FROM _run_tool_dependencies
822
- WHERE run_id = ? AND pending_tool_call_id = ?
823
- """,
824
- (record.run_id, record.pending_tool_call_id),
825
- )
826
- if row is None: # pragma: no cover
827
- raise RuntimeError("Failed to attach run tool dependency")
828
- return _row_to_run_tool_dependency(row)
829
-
830
- @SQLiteStorageBase.handle_exceptions
831
- def count_unresolved_followup_dependencies(
832
- self,
833
- *,
834
- org_id: str,
835
- extractor_kind: str,
836
- tool_name: str,
837
- ) -> int:
838
- row = self._fetchone(
839
- """
840
- SELECT COUNT(*) AS count
841
- FROM _run_tool_dependencies d
842
- JOIN _agent_runs r ON r.id = d.run_id
843
- JOIN _pending_tool_calls p ON p.id = d.pending_tool_call_id
844
- WHERE r.org_id = ?
845
- AND r.extractor_kind = ?
846
- AND p.tool_name = ?
847
- AND p.status = ?
848
- AND d.resolved_at IS NULL
849
- AND d.consumed_at IS NULL
850
- """,
851
- (
852
- org_id,
853
- extractor_kind,
854
- tool_name,
855
- PendingToolCallStatus.PENDING.value,
856
- ),
857
- )
858
- return int(row["count"]) if row is not None else 0
859
-
860
- @SQLiteStorageBase.handle_exceptions
861
- def list_run_tool_dependencies(self, run_id: str) -> list[RunToolDependencyRecord]:
862
- rows = self._fetchall(
863
- "SELECT * FROM _run_tool_dependencies WHERE run_id = ?",
864
- (run_id,),
865
- )
866
- return [_row_to_run_tool_dependency(row) for row in rows]
867
-
868
- @SQLiteStorageBase.handle_exceptions
869
- def resolve_pending_tool_call(
870
- self,
871
- call_id: str,
872
- *,
873
- result: dict[str, Any],
874
- resolved_at: datetime | None = None,
875
- valid_for_seconds: int,
876
- ) -> PendingToolCallRecord | None:
877
- resolved = resolved_at or datetime.now(UTC)
878
- valid_until = resolved + timedelta(seconds=valid_for_seconds)
879
- with self._lock:
880
- cur = self.conn.execute(
881
- """
882
- UPDATE _pending_tool_calls
883
- SET status = ?, result = ?, resolved_at = ?, valid_until = ?
884
- WHERE id = ?
885
- AND status = ?
886
- """,
887
- (
888
- PendingToolCallStatus.RESOLVED.value,
889
- _json_dumps(result),
890
- _dt_str(resolved),
891
- _dt_str(valid_until),
892
- call_id,
893
- PendingToolCallStatus.PENDING.value,
894
- ),
895
- )
896
- if cur.rowcount == 0:
897
- self.conn.commit()
898
- return self.get_pending_tool_call(call_id)
899
- self.conn.execute(
900
- """
901
- UPDATE _pending_tool_calls
902
- SET status = ?,
903
- superseded_by = ?
904
- WHERE id != ?
905
- AND status = ?
906
- AND (valid_until IS NULL OR valid_until > ?)
907
- AND (org_id, scope_hash, tool_name, dedup_key) = (
908
- SELECT org_id, scope_hash, tool_name, dedup_key
909
- FROM _pending_tool_calls
910
- WHERE id = ?
911
- )
912
- """,
913
- (
914
- PendingToolCallStatus.SUPERSEDED.value,
915
- call_id,
916
- call_id,
917
- PendingToolCallStatus.RESOLVED.value,
918
- _dt_str(resolved),
919
- call_id,
920
- ),
921
- )
922
- self.conn.execute(
923
- """
924
- UPDATE _run_tool_dependencies
925
- SET resolved_at = ?
926
- WHERE pending_tool_call_id = ? AND resolved_at IS NULL
927
- """,
928
- (_dt_str(resolved), call_id),
929
- )
930
- self.conn.execute(
931
- """
932
- UPDATE _agent_runs
933
- SET status = ?, updated_at = ?
934
- WHERE status IN (?, ?)
935
- AND EXISTS (
936
- SELECT 1
937
- FROM _run_tool_dependencies d
938
- WHERE d.run_id = _agent_runs.id
939
- AND d.pending_tool_call_id = ?
940
- AND d.resolved_at IS NOT NULL
941
- AND d.consumed_at IS NULL
942
- )
943
- """,
944
- (
945
- AgentRunStatus.RESUME_READY.value,
946
- self._current_timestamp(),
947
- AgentRunStatus.FINALIZED.value,
948
- AgentRunStatus.FINALIZED_PENDING_TOOL.value,
949
- call_id,
950
- ),
951
- )
952
- self.conn.commit()
953
- return self.get_pending_tool_call(call_id)
954
-
955
- @SQLiteStorageBase.handle_exceptions
956
- def update_resolved_pending_tool_call_result(
957
- self,
958
- call_id: str,
959
- *,
960
- result: dict[str, Any],
961
- resolved_at: datetime | None = None,
962
- valid_for_seconds: int,
963
- ) -> PendingToolCallRecord | None:
964
- resolved = resolved_at or datetime.now(UTC)
965
- valid_until = resolved + timedelta(seconds=valid_for_seconds)
966
- now_s = _dt_str(resolved) or self._current_timestamp()
967
- with self._lock:
968
- self.conn.execute("BEGIN IMMEDIATE")
969
- try:
970
- cur = self.conn.execute(
971
- """
972
- UPDATE _pending_tool_calls
973
- SET result = ?, resolved_at = ?, valid_until = ?
974
- WHERE id = ?
975
- AND status = ?
976
- """,
977
- (
978
- _json_dumps(result),
979
- _dt_str(resolved),
980
- _dt_str(valid_until),
981
- call_id,
982
- PendingToolCallStatus.RESOLVED.value,
983
- ),
984
- )
985
- if cur.rowcount == 0:
986
- self.conn.commit()
987
- return self.get_pending_tool_call(call_id)
988
- self.conn.execute(
989
- """
990
- UPDATE _pending_tool_calls
991
- SET status = ?,
992
- superseded_by = ?
993
- WHERE id != ?
994
- AND status = ?
995
- AND (valid_until IS NULL OR valid_until > ?)
996
- AND (org_id, scope_hash, tool_name, dedup_key) = (
997
- SELECT org_id, scope_hash, tool_name, dedup_key
998
- FROM _pending_tool_calls
999
- WHERE id = ?
1000
- )
1001
- """,
1002
- (
1003
- PendingToolCallStatus.SUPERSEDED.value,
1004
- call_id,
1005
- call_id,
1006
- PendingToolCallStatus.RESOLVED.value,
1007
- _dt_str(resolved),
1008
- call_id,
1009
- ),
1010
- )
1011
- self.conn.execute(
1012
- """
1013
- UPDATE _run_tool_dependencies
1014
- SET resolved_at = ?,
1015
- consumed_at = NULL
1016
- WHERE pending_tool_call_id = ?
1017
- """,
1018
- (_dt_str(resolved), call_id),
1019
- )
1020
- self._mark_runs_ready_with_actionable_dependencies_unlocked(
1021
- now_s, pending_tool_call_id=call_id
1022
- )
1023
- except Exception:
1024
- self.conn.rollback()
1025
- raise
1026
- else:
1027
- self.conn.commit()
1028
- return self.get_pending_tool_call(call_id)
1029
-
1030
- @SQLiteStorageBase.handle_exceptions
1031
- def mark_pending_tool_call_not_applicable(
1032
- self,
1033
- call_id: str,
1034
- *,
1035
- resolved_at: datetime | None = None,
1036
- valid_for_seconds: int,
1037
- ) -> PendingToolCallRecord | None:
1038
- resolved = resolved_at or datetime.now(UTC)
1039
- valid_until = resolved + timedelta(seconds=valid_for_seconds)
1040
- now_s = _dt_str(resolved) or self._current_timestamp()
1041
- with self._lock:
1042
- self.conn.execute("BEGIN IMMEDIATE")
1043
- try:
1044
- cur = self.conn.execute(
1045
- """
1046
- UPDATE _pending_tool_calls
1047
- SET status = ?, result = ?, resolved_at = ?, valid_until = ?
1048
- WHERE id = ?
1049
- AND status IN (?, ?)
1050
- """,
1051
- (
1052
- PendingToolCallStatus.RESOLVED.value,
1053
- _json_dumps(not_applicable_tool_result()),
1054
- _dt_str(resolved),
1055
- _dt_str(valid_until),
1056
- call_id,
1057
- PendingToolCallStatus.PENDING.value,
1058
- PendingToolCallStatus.RESOLVED.value,
1059
- ),
1060
- )
1061
- if cur.rowcount == 0:
1062
- self.conn.commit()
1063
- return self.get_pending_tool_call(call_id)
1064
- self.conn.execute(
1065
- """
1066
- UPDATE _run_tool_dependencies
1067
- SET resolved_at = COALESCE(resolved_at, ?),
1068
- consumed_at = ?
1069
- WHERE pending_tool_call_id = ?
1070
- AND consumed_at IS NULL
1071
- """,
1072
- (_dt_str(resolved), _dt_str(resolved), call_id),
1073
- )
1074
- self._mark_runs_ready_with_actionable_dependencies_unlocked(
1075
- now_s, pending_tool_call_id=call_id
1076
- )
1077
- self._finalize_runs_without_actionable_dependencies_unlocked(
1078
- now_s, pending_tool_call_id=call_id
1079
- )
1080
- except Exception:
1081
- self.conn.rollback()
1082
- raise
1083
- else:
1084
- self.conn.commit()
1085
- return self.get_pending_tool_call(call_id)
1086
-
1087
- @SQLiteStorageBase.handle_exceptions
1088
- def claim_ready_agent_run(
1089
- self,
1090
- *,
1091
- org_id: str,
1092
- worker_id: str,
1093
- now: datetime | None = None,
1094
- claim_ttl_seconds: int = 600,
1095
- ) -> AgentRunRecord | None:
1096
- current = now or datetime.now(UTC)
1097
- stale_before = current - timedelta(seconds=claim_ttl_seconds)
1098
- with self._lock:
1099
- row = self.conn.execute(
1100
- """
1101
- SELECT r.*
1102
- FROM _agent_runs r
1103
- WHERE r.org_id = ?
1104
- AND (
1105
- r.status = ?
1106
- OR (r.status = ? AND r.claimed_at < ?)
1107
- )
1108
- AND (r.next_resume_at IS NULL OR r.next_resume_at <= ?)
1109
- AND EXISTS (
1110
- SELECT 1
1111
- FROM _run_tool_dependencies d
1112
- JOIN _pending_tool_calls p
1113
- ON p.id = d.pending_tool_call_id
1114
- WHERE d.run_id = r.id
1115
- AND d.resolved_at IS NOT NULL
1116
- AND d.consumed_at IS NULL
1117
- AND p.status = ?
1118
- AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
1119
- )
1120
- ORDER BY
1121
- r.org_id ASC,
1122
- r.extractor_kind ASC,
1123
- COALESCE(r.user_id, '') ASC,
1124
- COALESCE(r.window_start_interaction_id, 0) ASC,
1125
- r.updated_at ASC
1126
- LIMIT 1
1127
- """,
1128
- (
1129
- org_id,
1130
- AgentRunStatus.RESUME_READY.value,
1131
- AgentRunStatus.RESUMING.value,
1132
- _dt_str(stale_before),
1133
- _dt_str(current),
1134
- PendingToolCallStatus.RESOLVED.value,
1135
- ),
1136
- ).fetchone()
1137
- if row is None:
1138
- return None
1139
- self.conn.execute(
1140
- """
1141
- UPDATE _agent_runs
1142
- SET status = ?,
1143
- claimed_by = ?,
1144
- claimed_at = ?,
1145
- resume_attempts = resume_attempts + 1,
1146
- updated_at = ?
1147
- WHERE id = ?
1148
- """,
1149
- (
1150
- AgentRunStatus.RESUMING.value,
1151
- worker_id,
1152
- _dt_str(current),
1153
- self._current_timestamp(),
1154
- row["id"],
1155
- ),
1156
- )
1157
- self.conn.commit()
1158
- return self.get_agent_run(row["id"])
1159
-
1160
- @SQLiteStorageBase.handle_exceptions
1161
- def claim_finalization_failed_agent_run(
1162
- self,
1163
- *,
1164
- org_id: str,
1165
- worker_id: str,
1166
- now: datetime | None = None,
1167
- claim_ttl_seconds: int = 600,
1168
- ) -> AgentRunRecord | None:
1169
- current = now or datetime.now(UTC)
1170
- stale_before = current - timedelta(seconds=claim_ttl_seconds)
1171
- with self._lock:
1172
- # Claim runs that still need their committed output finalized:
1173
- # - FINALIZATION_FAILED: an explicit retry is due.
1174
- # - stale FINALIZING: a worker crashed mid-finalize.
1175
- # - stale AGENT_COMPLETED: publish-time finalization never ran or
1176
- # crashed before flipping the status (the run row carries
1177
- # committed_output but was orphaned); the staleness guard ensures
1178
- # we never race an in-flight publish-time finalize.
1179
- row = self.conn.execute(
1180
- """
1181
- SELECT *
1182
- FROM _agent_runs
1183
- WHERE org_id = ?
1184
- AND (
1185
- status = ?
1186
- OR (status = ? AND claimed_at < ?)
1187
- OR (status = ? AND updated_at < ?)
1188
- )
1189
- AND committed_output IS NOT NULL
1190
- AND (next_resume_at IS NULL OR next_resume_at <= ?)
1191
- ORDER BY
1192
- org_id ASC,
1193
- extractor_kind ASC,
1194
- COALESCE(user_id, '') ASC,
1195
- COALESCE(window_start_interaction_id, 0) ASC,
1196
- updated_at ASC
1197
- LIMIT 1
1198
- """,
1199
- (
1200
- org_id,
1201
- AgentRunStatus.FINALIZATION_FAILED.value,
1202
- AgentRunStatus.FINALIZING.value,
1203
- _dt_str(stale_before),
1204
- AgentRunStatus.AGENT_COMPLETED.value,
1205
- _dt_str(stale_before),
1206
- _dt_str(current),
1207
- ),
1208
- ).fetchone()
1209
- if row is None:
1210
- return None
1211
- self.conn.execute(
1212
- """
1213
- UPDATE _agent_runs
1214
- SET status = ?,
1215
- claimed_by = ?,
1216
- claimed_at = ?,
1217
- updated_at = ?
1218
- WHERE id = ?
1219
- """,
1220
- (
1221
- AgentRunStatus.FINALIZING.value,
1222
- worker_id,
1223
- _dt_str(current),
1224
- self._current_timestamp(),
1225
- row["id"],
1226
- ),
1227
- )
1228
- self.conn.commit()
1229
- return self.get_agent_run(row["id"])
1230
-
1231
- @SQLiteStorageBase.handle_exceptions
1232
- def consume_run_tool_dependencies(self, run_id: str) -> int:
1233
- consumed_at = self._current_timestamp()
1234
- with self._lock:
1235
- cur = self.conn.execute(
1236
- """
1237
- UPDATE _run_tool_dependencies
1238
- SET consumed_at = ?
1239
- WHERE run_id = ?
1240
- AND resolved_at IS NOT NULL
1241
- AND consumed_at IS NULL
1242
- """,
1243
- (consumed_at, run_id),
1244
- )
1245
- self.conn.commit()
1246
- return int(cur.rowcount)
1247
-
1248
- @SQLiteStorageBase.handle_exceptions
1249
- def list_resumable_work_org_ids(
1250
- self,
1251
- *,
1252
- now: datetime | None = None,
1253
- limit: int = 1000,
1254
- ) -> list[str]:
1255
- current = now or datetime.now(UTC)
1256
- now_s = _dt_str(current)
1257
- bounded_limit = max(1, min(limit, 10_000))
1258
- # Cross-org maintenance query. Surfaces any org that has a run ready to
1259
- # resume / awaiting finalization, or a pending tool call due to expire.
1260
- rows = self._fetchall(
1261
- """
1262
- SELECT DISTINCT org_id FROM (
1263
- SELECT r.org_id
1264
- FROM _agent_runs r
1265
- WHERE r.status IN (?, ?)
1266
- AND EXISTS (
1267
- SELECT 1
1268
- FROM _run_tool_dependencies d
1269
- JOIN _pending_tool_calls p
1270
- ON p.id = d.pending_tool_call_id
1271
- WHERE d.run_id = r.id
1272
- AND d.resolved_at IS NOT NULL
1273
- AND d.consumed_at IS NULL
1274
- AND p.status = ?
1275
- AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
1276
- )
1277
- UNION
1278
- SELECT org_id FROM _agent_runs
1279
- WHERE status IN (?, ?, ?)
1280
- UNION
1281
- SELECT org_id FROM _pending_tool_calls
1282
- WHERE status = ?
1283
- AND expires_at IS NOT NULL
1284
- AND expires_at <= ?
1285
- )
1286
- ORDER BY org_id ASC
1287
- LIMIT ?
1288
- """,
1289
- (
1290
- AgentRunStatus.RESUME_READY.value,
1291
- AgentRunStatus.RESUMING.value,
1292
- PendingToolCallStatus.RESOLVED.value,
1293
- AgentRunStatus.FINALIZATION_FAILED.value,
1294
- AgentRunStatus.FINALIZING.value,
1295
- AgentRunStatus.AGENT_COMPLETED.value,
1296
- PendingToolCallStatus.PENDING.value,
1297
- now_s,
1298
- bounded_limit,
1299
- ),
1300
- )
1301
- return [row["org_id"] for row in rows]