claude-smart 0.2.47 → 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 (113) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  7. package/plugin/dashboard/package-lock.json +61 -390
  8. package/plugin/dashboard/package.json +2 -2
  9. package/plugin/pyproject.toml +1 -1
  10. package/plugin/uv.lock +1 -1
  11. package/plugin/vendor/reflexio/.env.example +7 -0
  12. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  13. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  14. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  15. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  16. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  17. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  18. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  19. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  20. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  21. package/plugin/vendor/reflexio/reflexio/server/api.py +133 -3273
  22. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  23. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  24. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  25. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  26. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  27. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  28. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  29. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  30. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  31. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  32. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  33. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  34. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  35. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  36. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  37. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  38. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  39. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  40. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  41. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  42. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  43. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  44. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  45. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  46. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  47. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  48. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  49. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  50. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  51. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  52. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  53. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  54. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  56. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  59. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  60. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  61. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  62. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  63. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  64. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  65. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  66. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  68. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -4
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +56 -351
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  88. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  89. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +6 -3
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +33 -8
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -0,0 +1,704 @@
1
+ """SQLite PendingToolCallStore methods (the PendingToolCallStore bucket).
2
+
3
+ Extracted verbatim from ``_agent_run.py`` (the PendingToolCallStore bucket): the
4
+ twelve pending-tool-call methods plus the PTC-owned
5
+ ``_insert_pending_tool_call_unlocked`` private.
6
+
7
+ Four of these methods are multi-table transactions that cascade agent-run state
8
+ by calling AgentRunStore-owned ``_..._unlocked`` privates
9
+ (``_finalize_runs_without_pending_dependencies_unlocked`` from ``cancel`` /
10
+ ``expire``; ``_mark_runs_ready_with_actionable_dependencies_unlocked`` from
11
+ ``update_resolved`` / ``mark_not_applicable``;
12
+ ``_finalize_runs_without_actionable_dependencies_unlocked`` from
13
+ ``mark_not_applicable``). Those privates live in the co-composed
14
+ ``SQLiteAgentRunStoreMixin`` and are reached via MRO — kept SINGLE there, never
15
+ duplicated. Each transaction is moved WHOLE (its ``BEGIN IMMEDIATE`` /
16
+ ``with self._lock`` block plus all writes and its commit/rollback).
17
+
18
+ The residual ``_agent_run.py`` module (helpers-only, no mixin class)
19
+ permanently holds the shared row/datetime helpers (``_dt_str``,
20
+ ``_row_to_pending_tool_call``, ``_record_to_prior_answer_match`` …), which are
21
+ imported here rather than duplicated.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import sqlite3
27
+ from collections.abc import Callable
28
+ from datetime import UTC, datetime, timedelta
29
+ from typing import Any
30
+
31
+ from reflexio.server.services.storage.storage_base import (
32
+ AgentRunStatus,
33
+ PendingToolCallRecord,
34
+ PendingToolCallStatus,
35
+ PendingToolCallUpsertResult,
36
+ PriorAnswerMatch,
37
+ RunToolDependencyRecord,
38
+ not_applicable_tool_result,
39
+ )
40
+
41
+ from .._agent_run import (
42
+ _dt_str,
43
+ _record_to_prior_answer_match,
44
+ _row_to_pending_tool_call,
45
+ )
46
+ from .._base import SQLiteStorageBase, _json_dumps
47
+
48
+
49
+ class SQLitePendingToolCallStoreMixin:
50
+ """SQLite-backed pending-tool-call store primitives."""
51
+
52
+ _lock: Any
53
+ conn: sqlite3.Connection
54
+ _fetchone: Any
55
+ _fetchall: Any
56
+ _current_timestamp: Any
57
+ org_id: str
58
+
59
+ # Provided via MRO by the co-composed SQLiteAgentRunStoreMixin; reached from
60
+ # the cross-bucket pending-tool-call methods below (cancel / expire /
61
+ # update_resolved / mark_not_applicable).
62
+ _finalize_runs_without_pending_dependencies_unlocked: Callable[..., None]
63
+ _mark_runs_ready_with_actionable_dependencies_unlocked: Callable[..., None]
64
+ _finalize_runs_without_actionable_dependencies_unlocked: Callable[..., None]
65
+
66
+ @SQLiteStorageBase.handle_exceptions
67
+ def create_pending_tool_call(
68
+ self, record: PendingToolCallRecord
69
+ ) -> PendingToolCallRecord:
70
+ with self._lock:
71
+ self.conn.execute(
72
+ """
73
+ INSERT INTO _pending_tool_calls (
74
+ id, org_id, user_id, scope, scope_hash, tool_name, dedup_key,
75
+ status, question_text, answer_format, args, tags, result,
76
+ embedding, superseded_by, resolved_at, expires_at, cache_until,
77
+ valid_until
78
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
79
+ """,
80
+ (
81
+ record.id,
82
+ record.org_id,
83
+ record.user_id,
84
+ _json_dumps(record.scope),
85
+ record.scope_hash,
86
+ record.tool_name,
87
+ record.dedup_key,
88
+ record.status.value,
89
+ record.question_text,
90
+ record.answer_format,
91
+ _json_dumps(record.args),
92
+ _json_dumps(record.tags),
93
+ _json_dumps(record.result),
94
+ _json_dumps(record.embedding),
95
+ record.superseded_by,
96
+ _dt_str(record.resolved_at),
97
+ _dt_str(record.expires_at),
98
+ _dt_str(record.cache_until),
99
+ _dt_str(record.valid_until),
100
+ ),
101
+ )
102
+ self.conn.commit()
103
+ stored = self.get_pending_tool_call(record.id)
104
+ if stored is None: # pragma: no cover
105
+ raise RuntimeError(f"Failed to create pending tool call {record.id}")
106
+ return stored
107
+
108
+ def _insert_pending_tool_call_unlocked(self, record: PendingToolCallRecord) -> None:
109
+ self.conn.execute(
110
+ """
111
+ INSERT INTO _pending_tool_calls (
112
+ id, org_id, user_id, scope, scope_hash, tool_name, dedup_key,
113
+ status, question_text, answer_format, args, tags, result,
114
+ embedding, superseded_by, resolved_at, expires_at, cache_until,
115
+ valid_until
116
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
117
+ """,
118
+ (
119
+ record.id,
120
+ record.org_id,
121
+ record.user_id,
122
+ _json_dumps(record.scope),
123
+ record.scope_hash,
124
+ record.tool_name,
125
+ record.dedup_key,
126
+ record.status.value,
127
+ record.question_text,
128
+ record.answer_format,
129
+ _json_dumps(record.args),
130
+ _json_dumps(record.tags),
131
+ _json_dumps(record.result),
132
+ _json_dumps(record.embedding),
133
+ record.superseded_by,
134
+ _dt_str(record.resolved_at),
135
+ _dt_str(record.expires_at),
136
+ _dt_str(record.cache_until),
137
+ _dt_str(record.valid_until),
138
+ ),
139
+ )
140
+
141
+ @SQLiteStorageBase.handle_exceptions
142
+ def create_or_attach_pending_tool_call(
143
+ self,
144
+ *,
145
+ record: PendingToolCallRecord,
146
+ dependency: RunToolDependencyRecord,
147
+ now: datetime | None = None,
148
+ ) -> PendingToolCallUpsertResult:
149
+ current = now or datetime.now(UTC)
150
+ created = False
151
+ pending_tool_call_id = record.id
152
+ with self._lock:
153
+ self.conn.execute("BEGIN IMMEDIATE")
154
+ try:
155
+ row = self.conn.execute(
156
+ """
157
+ SELECT * FROM _pending_tool_calls
158
+ WHERE org_id = ?
159
+ AND scope_hash = ?
160
+ AND tool_name = ?
161
+ AND dedup_key = ?
162
+ AND status = ?
163
+ AND cache_until > ?
164
+ ORDER BY created_at ASC
165
+ LIMIT 1
166
+ """,
167
+ (
168
+ record.org_id,
169
+ record.scope_hash,
170
+ record.tool_name,
171
+ record.dedup_key,
172
+ PendingToolCallStatus.PENDING.value,
173
+ _dt_str(current),
174
+ ),
175
+ ).fetchone()
176
+ pending_tool_call_id = row["id"] if row is not None else record.id
177
+ if row is None:
178
+ self._insert_pending_tool_call_unlocked(record)
179
+ created = True
180
+ self.conn.execute(
181
+ """
182
+ INSERT OR IGNORE INTO _run_tool_dependencies (
183
+ run_id, pending_tool_call_id, dependency_kind,
184
+ resolved_at, consumed_at
185
+ ) VALUES (?,?,?,?,?)
186
+ """,
187
+ (
188
+ dependency.run_id,
189
+ pending_tool_call_id,
190
+ dependency.dependency_kind.value,
191
+ _dt_str(dependency.resolved_at),
192
+ _dt_str(dependency.consumed_at),
193
+ ),
194
+ )
195
+ except Exception:
196
+ self.conn.rollback()
197
+ raise
198
+ else:
199
+ self.conn.commit()
200
+
201
+ stored = self.get_pending_tool_call(pending_tool_call_id)
202
+ if stored is None: # pragma: no cover
203
+ raise RuntimeError("Failed to create or attach pending tool call")
204
+ return PendingToolCallUpsertResult(pending_tool_call=stored, created=created)
205
+
206
+ @SQLiteStorageBase.handle_exceptions
207
+ def get_pending_tool_call(self, call_id: str) -> PendingToolCallRecord | None:
208
+ row = self._fetchone(
209
+ "SELECT * FROM _pending_tool_calls WHERE id = ?", (call_id,)
210
+ )
211
+ return _row_to_pending_tool_call(row) if row else None
212
+
213
+ @SQLiteStorageBase.handle_exceptions
214
+ def list_pending_tool_calls(
215
+ self,
216
+ *,
217
+ status: PendingToolCallStatus | None = None,
218
+ limit: int = 100,
219
+ ) -> list[PendingToolCallRecord]:
220
+ bounded_limit = max(1, min(limit, 500))
221
+ params: list[Any] = [self.org_id]
222
+ status_clause = ""
223
+ if status is not None:
224
+ status_clause = "AND status = ?"
225
+ params.append(status.value)
226
+ params.append(bounded_limit)
227
+ rows = self._fetchall(
228
+ f"""
229
+ SELECT * FROM _pending_tool_calls
230
+ WHERE org_id = ?
231
+ {status_clause}
232
+ ORDER BY created_at DESC, id ASC
233
+ LIMIT ?
234
+ """,
235
+ tuple(params),
236
+ )
237
+ return [_row_to_pending_tool_call(row) for row in rows]
238
+
239
+ @SQLiteStorageBase.handle_exceptions
240
+ def cancel_pending_tool_call(
241
+ self,
242
+ call_id: str,
243
+ *,
244
+ cancelled_at: datetime | None = None,
245
+ ) -> PendingToolCallRecord | None:
246
+ now = cancelled_at or datetime.now(UTC)
247
+ now_s = _dt_str(now)
248
+ with self._lock:
249
+ self.conn.execute(
250
+ """
251
+ UPDATE _pending_tool_calls
252
+ SET status = ?
253
+ WHERE id = ?
254
+ AND status = ?
255
+ """,
256
+ (
257
+ PendingToolCallStatus.CANCELLED.value,
258
+ call_id,
259
+ PendingToolCallStatus.PENDING.value,
260
+ ),
261
+ )
262
+ self.conn.execute(
263
+ """
264
+ UPDATE _run_tool_dependencies
265
+ SET resolved_at = ?
266
+ WHERE pending_tool_call_id = ? AND resolved_at IS NULL
267
+ """,
268
+ (now_s, call_id),
269
+ )
270
+ self._finalize_runs_without_pending_dependencies_unlocked(now_s or "")
271
+ self.conn.commit()
272
+ return self.get_pending_tool_call(call_id)
273
+
274
+ @SQLiteStorageBase.handle_exceptions
275
+ def expire_pending_tool_calls(
276
+ self,
277
+ *,
278
+ now: datetime | None = None,
279
+ limit: int = 100,
280
+ ) -> int:
281
+ current = now or datetime.now(UTC)
282
+ now_s = _dt_str(current)
283
+ bounded_limit = max(1, min(limit, 500))
284
+ with self._lock:
285
+ self.conn.execute("BEGIN IMMEDIATE")
286
+ try:
287
+ rows = self.conn.execute(
288
+ """
289
+ SELECT id
290
+ FROM _pending_tool_calls
291
+ WHERE status = ?
292
+ AND expires_at IS NOT NULL
293
+ AND expires_at <= ?
294
+ ORDER BY expires_at ASC, created_at ASC, id ASC
295
+ LIMIT ?
296
+ """,
297
+ (
298
+ PendingToolCallStatus.PENDING.value,
299
+ now_s,
300
+ bounded_limit,
301
+ ),
302
+ ).fetchall()
303
+ call_ids = [row["id"] for row in rows]
304
+ if not call_ids:
305
+ self.conn.commit()
306
+ return 0
307
+
308
+ placeholders = ",".join("?" for _ in call_ids)
309
+ self.conn.execute(
310
+ f"""
311
+ UPDATE _pending_tool_calls
312
+ SET status = ?
313
+ WHERE id IN ({placeholders})
314
+ AND status = ?
315
+ """,
316
+ (
317
+ PendingToolCallStatus.EXPIRED.value,
318
+ *call_ids,
319
+ PendingToolCallStatus.PENDING.value,
320
+ ),
321
+ )
322
+ self.conn.execute(
323
+ f"""
324
+ UPDATE _run_tool_dependencies
325
+ SET resolved_at = ?
326
+ WHERE pending_tool_call_id IN ({placeholders})
327
+ AND resolved_at IS NULL
328
+ """,
329
+ (now_s, *call_ids),
330
+ )
331
+ self._finalize_runs_without_pending_dependencies_unlocked(now_s or "")
332
+ except Exception:
333
+ self.conn.rollback()
334
+ raise
335
+ else:
336
+ self.conn.commit()
337
+ return len(call_ids)
338
+
339
+ @SQLiteStorageBase.handle_exceptions
340
+ def delete_expired_pending_tool_calls(
341
+ self, *, now: int, grace_seconds: int, limit: int = 1000
342
+ ) -> int:
343
+ """Delete terminal 'expired'-status rows whose expires_at is past the grace window.
344
+
345
+ Only rows with status='expired' are deleted. RESOLVED rows are never
346
+ touched even if their expires_at is in the past, preserving live cached
347
+ results for resumable extraction.
348
+
349
+ Args:
350
+ now: Current Unix epoch seconds.
351
+ grace_seconds: Grace buffer; cutoff = now - grace_seconds.
352
+ limit: Max rows to delete per call.
353
+
354
+ Returns:
355
+ Number of rows deleted.
356
+ """
357
+ cutoff_iso = datetime.fromtimestamp(now - grace_seconds, UTC).isoformat()
358
+ bounded_limit = max(1, min(limit, 10_000))
359
+ with self._lock:
360
+ self.conn.execute("BEGIN IMMEDIATE")
361
+ try:
362
+ rows = self.conn.execute(
363
+ "SELECT id FROM _pending_tool_calls "
364
+ "WHERE org_id = ? AND status = 'expired' AND expires_at IS NOT NULL AND expires_at < ? "
365
+ "ORDER BY expires_at ASC LIMIT ?",
366
+ (self.org_id, cutoff_iso, bounded_limit),
367
+ ).fetchall()
368
+ if not rows:
369
+ self.conn.commit()
370
+ return 0
371
+ ids = [r["id"] for r in rows]
372
+ ph = ",".join("?" * len(ids))
373
+ cur = self.conn.execute(
374
+ f"DELETE FROM _pending_tool_calls WHERE id IN ({ph})", ids
375
+ )
376
+ self.conn.commit()
377
+ except Exception:
378
+ self.conn.rollback()
379
+ raise
380
+ return cur.rowcount
381
+
382
+ @SQLiteStorageBase.handle_exceptions
383
+ def find_active_pending_tool_call(
384
+ self,
385
+ *,
386
+ org_id: str,
387
+ scope_hash: str,
388
+ tool_name: str,
389
+ dedup_key: str,
390
+ now: datetime | None = None,
391
+ ) -> PendingToolCallRecord | None:
392
+ now_s = _dt_str(now or datetime.now(UTC))
393
+ row = self._fetchone(
394
+ """
395
+ SELECT * FROM _pending_tool_calls
396
+ WHERE org_id = ?
397
+ AND scope_hash = ?
398
+ AND tool_name = ?
399
+ AND dedup_key = ?
400
+ AND status = ?
401
+ AND cache_until > ?
402
+ ORDER BY created_at ASC
403
+ LIMIT 1
404
+ """,
405
+ (
406
+ org_id,
407
+ scope_hash,
408
+ tool_name,
409
+ dedup_key,
410
+ PendingToolCallStatus.PENDING.value,
411
+ now_s,
412
+ ),
413
+ )
414
+ return _row_to_pending_tool_call(row) if row else None
415
+
416
+ @SQLiteStorageBase.handle_exceptions
417
+ def search_prior_tool_calls(
418
+ self,
419
+ *,
420
+ org_id: str,
421
+ scope_hash: str,
422
+ tool_name: str,
423
+ query_embedding: list[float] | None = None,
424
+ now: datetime | None = None,
425
+ limit: int = 8,
426
+ ) -> list[PriorAnswerMatch]:
427
+ current = now or datetime.now(UTC)
428
+ bounded_limit = max(1, min(limit, 50))
429
+ rows = self._fetchall(
430
+ """
431
+ SELECT * FROM _pending_tool_calls
432
+ WHERE org_id = ?
433
+ AND scope_hash = ?
434
+ AND tool_name = ?
435
+ AND (
436
+ (
437
+ status = ?
438
+ AND (expires_at IS NULL OR expires_at > ?)
439
+ )
440
+ OR (
441
+ status = ?
442
+ AND (valid_until IS NULL OR valid_until > ?)
443
+ )
444
+ )
445
+ ORDER BY
446
+ CASE status WHEN ? THEN 0 ELSE 1 END,
447
+ COALESCE(resolved_at, created_at) DESC,
448
+ id ASC
449
+ """,
450
+ (
451
+ org_id,
452
+ scope_hash,
453
+ tool_name,
454
+ PendingToolCallStatus.PENDING.value,
455
+ _dt_str(current),
456
+ PendingToolCallStatus.RESOLVED.value,
457
+ _dt_str(current),
458
+ PendingToolCallStatus.RESOLVED.value,
459
+ ),
460
+ )
461
+ seen_resolved_dedup_keys: set[str] = set()
462
+ records: list[PendingToolCallRecord] = []
463
+ for row in rows:
464
+ record = _row_to_pending_tool_call(row)
465
+ if record.status == PendingToolCallStatus.RESOLVED:
466
+ if record.dedup_key in seen_resolved_dedup_keys:
467
+ continue
468
+ seen_resolved_dedup_keys.add(record.dedup_key)
469
+ records.append(record)
470
+ matches = [
471
+ _record_to_prior_answer_match(record, query_embedding=query_embedding)
472
+ for record in records
473
+ ]
474
+ if query_embedding:
475
+ matches.sort(
476
+ key=lambda match: (
477
+ match.similarity is not None,
478
+ match.similarity or -1.0,
479
+ match.resolved_at
480
+ or match.created_at
481
+ or datetime.min.replace(tzinfo=UTC),
482
+ ),
483
+ reverse=True,
484
+ )
485
+ return matches[:bounded_limit]
486
+
487
+ @SQLiteStorageBase.handle_exceptions
488
+ def resolve_pending_tool_call(
489
+ self,
490
+ call_id: str,
491
+ *,
492
+ result: dict[str, Any],
493
+ resolved_at: datetime | None = None,
494
+ valid_for_seconds: int,
495
+ ) -> PendingToolCallRecord | None:
496
+ resolved = resolved_at or datetime.now(UTC)
497
+ valid_until = resolved + timedelta(seconds=valid_for_seconds)
498
+ with self._lock:
499
+ cur = self.conn.execute(
500
+ """
501
+ UPDATE _pending_tool_calls
502
+ SET status = ?, result = ?, resolved_at = ?, valid_until = ?
503
+ WHERE id = ?
504
+ AND status = ?
505
+ """,
506
+ (
507
+ PendingToolCallStatus.RESOLVED.value,
508
+ _json_dumps(result),
509
+ _dt_str(resolved),
510
+ _dt_str(valid_until),
511
+ call_id,
512
+ PendingToolCallStatus.PENDING.value,
513
+ ),
514
+ )
515
+ if cur.rowcount == 0:
516
+ self.conn.commit()
517
+ return self.get_pending_tool_call(call_id)
518
+ self.conn.execute(
519
+ """
520
+ UPDATE _pending_tool_calls
521
+ SET status = ?,
522
+ superseded_by = ?
523
+ WHERE id != ?
524
+ AND status = ?
525
+ AND (valid_until IS NULL OR valid_until > ?)
526
+ AND (org_id, scope_hash, tool_name, dedup_key) = (
527
+ SELECT org_id, scope_hash, tool_name, dedup_key
528
+ FROM _pending_tool_calls
529
+ WHERE id = ?
530
+ )
531
+ """,
532
+ (
533
+ PendingToolCallStatus.SUPERSEDED.value,
534
+ call_id,
535
+ call_id,
536
+ PendingToolCallStatus.RESOLVED.value,
537
+ _dt_str(resolved),
538
+ call_id,
539
+ ),
540
+ )
541
+ self.conn.execute(
542
+ """
543
+ UPDATE _run_tool_dependencies
544
+ SET resolved_at = ?
545
+ WHERE pending_tool_call_id = ? AND resolved_at IS NULL
546
+ """,
547
+ (_dt_str(resolved), call_id),
548
+ )
549
+ self.conn.execute(
550
+ """
551
+ UPDATE _agent_runs
552
+ SET status = ?, updated_at = ?
553
+ WHERE status IN (?, ?)
554
+ AND EXISTS (
555
+ SELECT 1
556
+ FROM _run_tool_dependencies d
557
+ WHERE d.run_id = _agent_runs.id
558
+ AND d.pending_tool_call_id = ?
559
+ AND d.resolved_at IS NOT NULL
560
+ AND d.consumed_at IS NULL
561
+ )
562
+ """,
563
+ (
564
+ AgentRunStatus.RESUME_READY.value,
565
+ self._current_timestamp(),
566
+ AgentRunStatus.FINALIZED.value,
567
+ AgentRunStatus.FINALIZED_PENDING_TOOL.value,
568
+ call_id,
569
+ ),
570
+ )
571
+ self.conn.commit()
572
+ return self.get_pending_tool_call(call_id)
573
+
574
+ @SQLiteStorageBase.handle_exceptions
575
+ def update_resolved_pending_tool_call_result(
576
+ self,
577
+ call_id: str,
578
+ *,
579
+ result: dict[str, Any],
580
+ resolved_at: datetime | None = None,
581
+ valid_for_seconds: int,
582
+ ) -> PendingToolCallRecord | None:
583
+ resolved = resolved_at or datetime.now(UTC)
584
+ valid_until = resolved + timedelta(seconds=valid_for_seconds)
585
+ now_s = _dt_str(resolved) or self._current_timestamp()
586
+ with self._lock:
587
+ self.conn.execute("BEGIN IMMEDIATE")
588
+ try:
589
+ cur = self.conn.execute(
590
+ """
591
+ UPDATE _pending_tool_calls
592
+ SET result = ?, resolved_at = ?, valid_until = ?
593
+ WHERE id = ?
594
+ AND status = ?
595
+ """,
596
+ (
597
+ _json_dumps(result),
598
+ _dt_str(resolved),
599
+ _dt_str(valid_until),
600
+ call_id,
601
+ PendingToolCallStatus.RESOLVED.value,
602
+ ),
603
+ )
604
+ if cur.rowcount == 0:
605
+ self.conn.commit()
606
+ return self.get_pending_tool_call(call_id)
607
+ self.conn.execute(
608
+ """
609
+ UPDATE _pending_tool_calls
610
+ SET status = ?,
611
+ superseded_by = ?
612
+ WHERE id != ?
613
+ AND status = ?
614
+ AND (valid_until IS NULL OR valid_until > ?)
615
+ AND (org_id, scope_hash, tool_name, dedup_key) = (
616
+ SELECT org_id, scope_hash, tool_name, dedup_key
617
+ FROM _pending_tool_calls
618
+ WHERE id = ?
619
+ )
620
+ """,
621
+ (
622
+ PendingToolCallStatus.SUPERSEDED.value,
623
+ call_id,
624
+ call_id,
625
+ PendingToolCallStatus.RESOLVED.value,
626
+ _dt_str(resolved),
627
+ call_id,
628
+ ),
629
+ )
630
+ self.conn.execute(
631
+ """
632
+ UPDATE _run_tool_dependencies
633
+ SET resolved_at = ?,
634
+ consumed_at = NULL
635
+ WHERE pending_tool_call_id = ?
636
+ """,
637
+ (_dt_str(resolved), call_id),
638
+ )
639
+ self._mark_runs_ready_with_actionable_dependencies_unlocked(
640
+ now_s, pending_tool_call_id=call_id
641
+ )
642
+ except Exception:
643
+ self.conn.rollback()
644
+ raise
645
+ else:
646
+ self.conn.commit()
647
+ return self.get_pending_tool_call(call_id)
648
+
649
+ @SQLiteStorageBase.handle_exceptions
650
+ def mark_pending_tool_call_not_applicable(
651
+ self,
652
+ call_id: str,
653
+ *,
654
+ resolved_at: datetime | None = None,
655
+ valid_for_seconds: int,
656
+ ) -> PendingToolCallRecord | None:
657
+ resolved = resolved_at or datetime.now(UTC)
658
+ valid_until = resolved + timedelta(seconds=valid_for_seconds)
659
+ now_s = _dt_str(resolved) or self._current_timestamp()
660
+ with self._lock:
661
+ self.conn.execute("BEGIN IMMEDIATE")
662
+ try:
663
+ cur = self.conn.execute(
664
+ """
665
+ UPDATE _pending_tool_calls
666
+ SET status = ?, result = ?, resolved_at = ?, valid_until = ?
667
+ WHERE id = ?
668
+ AND status IN (?, ?)
669
+ """,
670
+ (
671
+ PendingToolCallStatus.RESOLVED.value,
672
+ _json_dumps(not_applicable_tool_result()),
673
+ _dt_str(resolved),
674
+ _dt_str(valid_until),
675
+ call_id,
676
+ PendingToolCallStatus.PENDING.value,
677
+ PendingToolCallStatus.RESOLVED.value,
678
+ ),
679
+ )
680
+ if cur.rowcount == 0:
681
+ self.conn.commit()
682
+ return self.get_pending_tool_call(call_id)
683
+ self.conn.execute(
684
+ """
685
+ UPDATE _run_tool_dependencies
686
+ SET resolved_at = COALESCE(resolved_at, ?),
687
+ consumed_at = ?
688
+ WHERE pending_tool_call_id = ?
689
+ AND consumed_at IS NULL
690
+ """,
691
+ (_dt_str(resolved), _dt_str(resolved), call_id),
692
+ )
693
+ self._mark_runs_ready_with_actionable_dependencies_unlocked(
694
+ now_s, pending_tool_call_id=call_id
695
+ )
696
+ self._finalize_runs_without_actionable_dependencies_unlocked(
697
+ now_s, pending_tool_call_id=call_id
698
+ )
699
+ except Exception:
700
+ self.conn.rollback()
701
+ raise
702
+ else:
703
+ self.conn.commit()
704
+ return self.get_pending_tool_call(call_id)