synapse-cli-agent 0.1.13__py3-none-any.whl

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 (131) hide show
  1. synapse/__init__.py +13 -0
  2. synapse/__main__.py +6 -0
  3. synapse/app/__init__.py +1 -0
  4. synapse/app/agent.py +492 -0
  5. synapse/app/agent_md.py +107 -0
  6. synapse/cli.py +750 -0
  7. synapse/commands/__init__.py +1 -0
  8. synapse/commands/compression.py +573 -0
  9. synapse/commands/helpers.py +22 -0
  10. synapse/commands/mcp.py +406 -0
  11. synapse/commands/model.py +173 -0
  12. synapse/commands/result.py +34 -0
  13. synapse/commands/sessions.py +443 -0
  14. synapse/commands/slash_cmds.py +521 -0
  15. synapse/commands/slash_complete.py +816 -0
  16. synapse/commands/theme.py +99 -0
  17. synapse/config.py +27 -0
  18. synapse/content/__init__.py +1 -0
  19. synapse/content/input_history.py +122 -0
  20. synapse/content/multimodal.py +733 -0
  21. synapse/content/prompts.py +249 -0
  22. synapse/content/skills_catalog.py +128 -0
  23. synapse/integrations/__init__.py +1 -0
  24. synapse/integrations/checkpoint_seed.py +281 -0
  25. synapse/integrations/codex_history.py +375 -0
  26. synapse/integrations/codex_import.py +393 -0
  27. synapse/integrations/codex_sessions.py +629 -0
  28. synapse/integrations/describe_image.py +370 -0
  29. synapse/integrations/http_clients.py +199 -0
  30. synapse/integrations/llm_openai_compat.py +90 -0
  31. synapse/integrations/llm_openai_websocket.py +187 -0
  32. synapse/integrations/mcp_client.py +646 -0
  33. synapse/integrations/vision_middleware.py +62 -0
  34. synapse/models/__init__.py +5 -0
  35. synapse/models/config.py +240 -0
  36. synapse/models/helpers.py +206 -0
  37. synapse/models/profile.py +59 -0
  38. synapse/models/registry.py +722 -0
  39. synapse/models_registry.py +7 -0
  40. synapse/observability/__init__.py +1 -0
  41. synapse/observability/startup_trace.py +127 -0
  42. synapse/runtime/__init__.py +1 -0
  43. synapse/runtime/async_runtime.py +176 -0
  44. synapse/runtime/backends.py +458 -0
  45. synapse/runtime/context_compact.py +249 -0
  46. synapse/runtime/execute_capture.py +48 -0
  47. synapse/runtime/fs_permissions.py +79 -0
  48. synapse/runtime/harness.py +57 -0
  49. synapse/runtime/hitl.py +197 -0
  50. synapse/runtime/interaction_ledger.py +82 -0
  51. synapse/runtime/middleware.py +802 -0
  52. synapse/runtime/model_request_compression_middleware.py +745 -0
  53. synapse/runtime/pathing.py +146 -0
  54. synapse/runtime/safety.py +184 -0
  55. synapse/runtime/steer.py +240 -0
  56. synapse/runtime/subagents.py +207 -0
  57. synapse/runtime/tool_ignore.py +221 -0
  58. synapse/runtime/tool_output_eval.py +118 -0
  59. synapse/runtime/tool_output_middleware.py +585 -0
  60. synapse/runtime/tool_output_usage_middleware.py +60 -0
  61. synapse/sessions/__init__.py +31 -0
  62. synapse/sessions/cancel_repair.py +208 -0
  63. synapse/sessions/session_recap.py +174 -0
  64. synapse/sessions/store.py +695 -0
  65. synapse/sessions/transcript.py +754 -0
  66. synapse/settings/__init__.py +5 -0
  67. synapse/settings/config_paths.py +184 -0
  68. synapse/settings/schema.py +464 -0
  69. synapse/tool_output/__init__.py +59 -0
  70. synapse/tool_output/detection.py +170 -0
  71. synapse/tool_output/metrics.py +32 -0
  72. synapse/tool_output/models.py +173 -0
  73. synapse/tool_output/pipeline.py +330 -0
  74. synapse/tool_output/repository.py +721 -0
  75. synapse/tool_output/transformers.py +648 -0
  76. synapse/tools/__init__.py +5 -0
  77. synapse/tools/session_tools.py +204 -0
  78. synapse/ui/__init__.py +10 -0
  79. synapse/ui/bottombar/__init__.py +73 -0
  80. synapse/ui/bottombar/components/__init__.py +143 -0
  81. synapse/ui/bottombar/components/key_hints.py +30 -0
  82. synapse/ui/bottombar/components/mcp.py +64 -0
  83. synapse/ui/bottombar/components/mode.py +24 -0
  84. synapse/ui/bottombar/components/model.py +28 -0
  85. synapse/ui/bottombar/components/thread.py +29 -0
  86. synapse/ui/bottombar/context.py +36 -0
  87. synapse/ui/bottombar/core.py +74 -0
  88. synapse/ui/dialogs/__init__.py +25 -0
  89. synapse/ui/dialogs/base.py +362 -0
  90. synapse/ui/dialogs/codex_session_list.py +84 -0
  91. synapse/ui/dialogs/compression_diagnostics.py +210 -0
  92. synapse/ui/dialogs/git_explore.py +702 -0
  93. synapse/ui/dialogs/mcp_panel.py +407 -0
  94. synapse/ui/dialogs/model_picker.py +128 -0
  95. synapse/ui/dialogs/safety_panel.py +63 -0
  96. synapse/ui/dialogs/session_list.py +98 -0
  97. synapse/ui/dialogs/theme_designer.py +863 -0
  98. synapse/ui/dialogs/theme_picker.py +113 -0
  99. synapse/ui/git_explore/__init__.py +31 -0
  100. synapse/ui/git_explore/engine.py +82 -0
  101. synapse/ui/git_explore/provider.py +242 -0
  102. synapse/ui/git_explore/unified.py +85 -0
  103. synapse/ui/rendering.py +350 -0
  104. synapse/ui/sink.py +70 -0
  105. synapse/ui/steer_widget.py +367 -0
  106. synapse/ui/stream.py +1207 -0
  107. synapse/ui/stream_events.py +421 -0
  108. synapse/ui/stream_runtime.py +252 -0
  109. synapse/ui/theme.py +1154 -0
  110. synapse/ui/timeline.py +621 -0
  111. synapse/ui/topbar/__init__.py +97 -0
  112. synapse/ui/topbar/components/__init__.py +150 -0
  113. synapse/ui/topbar/components/branch.py +41 -0
  114. synapse/ui/topbar/components/title.py +24 -0
  115. synapse/ui/topbar/components/tool_output.py +24 -0
  116. synapse/ui/topbar/components/usage.py +24 -0
  117. synapse/ui/topbar/components/workspace.py +32 -0
  118. synapse/ui/topbar/context.py +32 -0
  119. synapse/ui/topbar/core.py +979 -0
  120. synapse/ui/topbar/git_changes_popover.py +178 -0
  121. synapse/ui/topbar/git_chrome.py +475 -0
  122. synapse/ui/topbar/tool_output_popover.py +84 -0
  123. synapse/ui/topbar/widget.py +474 -0
  124. synapse/ui/tui.py +5717 -0
  125. synapse/ui/turn_rail.py +71 -0
  126. synapse/ui/user_turn.py +83 -0
  127. synapse/ui/welcome.py +261 -0
  128. synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
  129. synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
  130. synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
  131. synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,721 @@
1
+ """SQLite persistence for reversible tool-output references and diagnostics."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import sqlite3
8
+ import threading
9
+ import time
10
+ import uuid
11
+ import zlib
12
+ from collections.abc import Iterator
13
+ from contextlib import contextmanager
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from synapse.tool_output.metrics import notify_metrics_changed
18
+ from synapse.tool_output.models import (
19
+ ModelRequestCompressionEvent,
20
+ ToolOutputRecord,
21
+ TransformEvent,
22
+ )
23
+
24
+ _REFERENCE_PREFIX = "tool-output://"
25
+ _ERROR_LINE = re.compile(r"\b(error|fatal|failed|failure|exception|traceback|critical)\b", re.I)
26
+ _TOKEN = re.compile(r"[\w.-]+", re.UNICODE)
27
+
28
+ def content_to_text(content: Any) -> str:
29
+ """Produce stable UTF-8 text for a ToolMessage content payload."""
30
+ if isinstance(content, str):
31
+ return content
32
+ if isinstance(content, bytes | bytearray):
33
+ return bytes(content).decode("utf-8", errors="replace")
34
+ try:
35
+ return json.dumps(content, ensure_ascii=False, sort_keys=True, default=str)
36
+ except Exception: # noqa: BLE001
37
+ return str(content)
38
+
39
+
40
+ class ToolOutputRepository:
41
+ """Content-addressed SQLite store for rewritten tool outputs.
42
+
43
+ Blobs are zlib-compressed and deduplicated by SHA-256. References remain
44
+ thread-scoped, so a valid reference cannot be used to read another thread.
45
+ """
46
+
47
+ def __init__(self, path: Path | str) -> None:
48
+ self.path = Path(path).expanduser().resolve()
49
+ self._lock = threading.RLock()
50
+ self.path.parent.mkdir(parents=True, exist_ok=True)
51
+ self._setup()
52
+
53
+ @contextmanager
54
+ def _connection(self) -> Iterator[sqlite3.Connection]:
55
+ conn = sqlite3.connect(self.path, timeout=10, check_same_thread=False)
56
+ conn.row_factory = sqlite3.Row
57
+ try:
58
+ yield conn
59
+ conn.commit()
60
+ finally:
61
+ conn.close()
62
+
63
+ def _setup(self) -> None:
64
+ with self._lock, self._connection() as conn:
65
+ conn.execute("PRAGMA journal_mode=WAL")
66
+ conn.executescript(
67
+ """
68
+ CREATE TABLE IF NOT EXISTS tool_output_blobs (
69
+ sha256 TEXT PRIMARY KEY,
70
+ content BLOB NOT NULL,
71
+ size_bytes INTEGER NOT NULL,
72
+ created_at TEXT NOT NULL
73
+ );
74
+ CREATE TABLE IF NOT EXISTS tool_output_refs (
75
+ ref TEXT PRIMARY KEY,
76
+ thread_id TEXT NOT NULL,
77
+ checkpoint_ns TEXT NOT NULL,
78
+ tool_call_id TEXT NOT NULL,
79
+ tool_name TEXT NOT NULL,
80
+ status TEXT NOT NULL,
81
+ sha256 TEXT NOT NULL REFERENCES tool_output_blobs(sha256),
82
+ created_at TEXT NOT NULL
83
+ );
84
+ CREATE INDEX IF NOT EXISTS idx_tool_output_refs_thread
85
+ ON tool_output_refs(thread_id);
86
+ CREATE TABLE IF NOT EXISTS tool_output_events (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ thread_id TEXT NOT NULL,
89
+ ref TEXT,
90
+ event_json TEXT NOT NULL,
91
+ created_at TEXT NOT NULL
92
+ );
93
+ CREATE INDEX IF NOT EXISTS idx_tool_output_events_thread
94
+ ON tool_output_events(thread_id);
95
+ CREATE TABLE IF NOT EXISTS tool_output_retrieval_events (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ thread_id TEXT NOT NULL,
98
+ ref TEXT NOT NULL,
99
+ mode TEXT NOT NULL,
100
+ returned_bytes INTEGER NOT NULL,
101
+ duration_ms REAL NOT NULL,
102
+ created_at TEXT NOT NULL
103
+ );
104
+ CREATE INDEX IF NOT EXISTS idx_tool_output_retrieval_events_thread
105
+ ON tool_output_retrieval_events(thread_id);
106
+ CREATE TABLE IF NOT EXISTS tool_output_model_reuse_events (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ thread_id TEXT NOT NULL,
109
+ estimated_avoided_tokens INTEGER NOT NULL,
110
+ created_at TEXT NOT NULL
111
+ );
112
+ CREATE INDEX IF NOT EXISTS idx_tool_output_model_reuse_thread
113
+ ON tool_output_model_reuse_events(thread_id);
114
+ CREATE TABLE IF NOT EXISTS model_request_compression_events (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ request_id TEXT NOT NULL UNIQUE,
117
+ thread_id TEXT NOT NULL,
118
+ event_json TEXT NOT NULL,
119
+ created_at TEXT NOT NULL
120
+ );
121
+ CREATE INDEX IF NOT EXISTS idx_model_request_compression_thread
122
+ ON model_request_compression_events(thread_id, id);
123
+ CREATE TABLE IF NOT EXISTS interaction_events (
124
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
125
+ thread_id TEXT NOT NULL,
126
+ event_json TEXT NOT NULL,
127
+ created_at TEXT NOT NULL
128
+ );
129
+ CREATE INDEX IF NOT EXISTS idx_interaction_events_thread
130
+ ON interaction_events(thread_id, id);
131
+ """
132
+ )
133
+
134
+ @staticmethod
135
+ def parse_ref(ref: str) -> str | None:
136
+ if not isinstance(ref, str) or not ref.startswith(_REFERENCE_PREFIX):
137
+ return None
138
+ value = ref[len(_REFERENCE_PREFIX) :]
139
+ return value if value and "/" not in value else None
140
+
141
+ def put(
142
+ self,
143
+ *,
144
+ thread_id: str,
145
+ checkpoint_ns: str = "",
146
+ tool_call_id: str = "",
147
+ tool_name: str = "tool",
148
+ status: str = "success",
149
+ content: str,
150
+ ) -> ToolOutputRecord:
151
+ raw = content.encode("utf-8")
152
+ digest = hashlib.sha256(raw).hexdigest()
153
+ ref = f"{_REFERENCE_PREFIX}{uuid.uuid4().hex}"
154
+ created = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
155
+ with self._lock, self._connection() as conn:
156
+ conn.execute(
157
+ "INSERT OR IGNORE INTO tool_output_blobs("
158
+ "sha256, content, size_bytes, created_at) VALUES (?, ?, ?, ?)",
159
+ (digest, sqlite3.Binary(zlib.compress(raw, level=6)), len(raw), created),
160
+ )
161
+ conn.execute(
162
+ "INSERT INTO tool_output_refs("
163
+ "ref, thread_id, checkpoint_ns, tool_call_id, tool_name, status, "
164
+ "sha256, created_at) "
165
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
166
+ (ref, thread_id, checkpoint_ns, tool_call_id, tool_name, status, digest, created),
167
+ )
168
+ return ToolOutputRecord(
169
+ ref,
170
+ thread_id,
171
+ checkpoint_ns,
172
+ tool_call_id,
173
+ tool_name,
174
+ status,
175
+ content,
176
+ len(raw),
177
+ digest,
178
+ created,
179
+ )
180
+
181
+ def get(self, ref: str, *, expected_thread_id: str | None = None) -> ToolOutputRecord | None:
182
+ if self.parse_ref(ref) is None:
183
+ return None
184
+ with self._lock, self._connection() as conn:
185
+ row = conn.execute(
186
+ """SELECT r.ref, r.thread_id, r.checkpoint_ns, r.tool_call_id, r.tool_name,
187
+ r.status, r.sha256, r.created_at, b.content, b.size_bytes
188
+ FROM tool_output_refs r JOIN tool_output_blobs b ON b.sha256 = r.sha256
189
+ WHERE r.ref = ?""",
190
+ (ref,),
191
+ ).fetchone()
192
+ if row is None or (expected_thread_id and row["thread_id"] != expected_thread_id):
193
+ return None
194
+ try:
195
+ raw = zlib.decompress(row["content"])
196
+ except zlib.error:
197
+ return None
198
+ if hashlib.sha256(raw).hexdigest() != row["sha256"]:
199
+ return None
200
+ return ToolOutputRecord(
201
+ ref=row["ref"],
202
+ thread_id=row["thread_id"],
203
+ checkpoint_ns=row["checkpoint_ns"],
204
+ tool_call_id=row["tool_call_id"],
205
+ tool_name=row["tool_name"],
206
+ status=row["status"],
207
+ content=raw.decode("utf-8", errors="replace"),
208
+ size_bytes=row["size_bytes"],
209
+ sha256=row["sha256"],
210
+ created_at=row["created_at"],
211
+ )
212
+
213
+ def record_event(
214
+ self, thread_id: str, event: TransformEvent, *, ref: str | None = None
215
+ ) -> None:
216
+ with self._lock, self._connection() as conn:
217
+ conn.execute(
218
+ "INSERT INTO tool_output_events(thread_id, ref, event_json, created_at) "
219
+ "VALUES (?, ?, ?, ?)",
220
+ (
221
+ thread_id,
222
+ ref,
223
+ json.dumps(event.as_dict(), ensure_ascii=False),
224
+ time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
225
+ ),
226
+ )
227
+ notify_metrics_changed(thread_id)
228
+
229
+ def record_retrieval(
230
+ self,
231
+ *,
232
+ thread_id: str,
233
+ ref: str,
234
+ mode: str,
235
+ returned_bytes: int,
236
+ duration_ms: float,
237
+ ) -> None:
238
+ with self._lock, self._connection() as conn:
239
+ conn.execute(
240
+ "INSERT INTO tool_output_retrieval_events("
241
+ "thread_id, ref, mode, returned_bytes, duration_ms, created_at) "
242
+ "VALUES (?, ?, ?, ?, ?, ?)",
243
+ (
244
+ thread_id,
245
+ ref,
246
+ mode,
247
+ max(0, int(returned_bytes)),
248
+ max(0.0, float(duration_ms)),
249
+ time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
250
+ ),
251
+ )
252
+ notify_metrics_changed(thread_id)
253
+
254
+ def record_model_reuse(self, *, thread_id: str, estimated_avoided_tokens: int) -> None:
255
+ """Record estimated token savings when transformed outputs re-enter a model call."""
256
+ avoided = max(0, int(estimated_avoided_tokens or 0))
257
+ if not thread_id or avoided <= 0:
258
+ return
259
+ with self._lock, self._connection() as conn:
260
+ conn.execute(
261
+ "INSERT INTO tool_output_model_reuse_events("
262
+ "thread_id, estimated_avoided_tokens, created_at) VALUES (?, ?, ?)",
263
+ (thread_id, avoided, time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())),
264
+ )
265
+ notify_metrics_changed(thread_id)
266
+
267
+ def record_interaction(self, *, thread_id: str, event: dict[str, Any]) -> None:
268
+ """Persist one model/tool interaction independently of compression eligibility."""
269
+ if not thread_id:
270
+ return
271
+ created = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
272
+ with self._lock, self._connection() as conn:
273
+ conn.execute(
274
+ "INSERT INTO interaction_events(thread_id, event_json, created_at) "
275
+ "VALUES (?, ?, ?)",
276
+ (thread_id, json.dumps(event, ensure_ascii=False), created),
277
+ )
278
+ notify_metrics_changed(thread_id)
279
+
280
+ def interaction_events(
281
+ self, *, thread_id: str | None = None, limit: int = 500
282
+ ) -> list[dict[str, Any]]:
283
+ where, params = (" WHERE thread_id = ?", (thread_id,)) if thread_id else ("", ())
284
+ bounded = max(1, min(5000, int(limit)))
285
+ with self._lock, self._connection() as conn:
286
+ rows = conn.execute(
287
+ "SELECT id, thread_id, event_json, created_at "
288
+ f"FROM interaction_events{where} ORDER BY id DESC LIMIT ?",
289
+ (*params, bounded),
290
+ ).fetchall()
291
+ return [
292
+ {
293
+ "id": int(row["id"]),
294
+ "thread_id": row["thread_id"],
295
+ "created_at": row["created_at"],
296
+ **json.loads(row["event_json"]),
297
+ }
298
+ for row in rows
299
+ ]
300
+
301
+ def latest_request_position(self, *, thread_id: str) -> tuple[int, int]:
302
+ with self._lock, self._connection() as conn:
303
+ row = conn.execute(
304
+ "SELECT event_json FROM model_request_compression_events "
305
+ "WHERE thread_id = ? ORDER BY id DESC LIMIT 1",
306
+ (thread_id,),
307
+ ).fetchone()
308
+ if row is None:
309
+ return 0, 0
310
+ event = json.loads(row["event_json"])
311
+ return int(event.get("turn_index", 0) or 0), int(
312
+ event.get("model_call_index", 0) or 0
313
+ )
314
+
315
+ def record_model_request(
316
+ self, *, thread_id: str, event: ModelRequestCompressionEvent
317
+ ) -> None:
318
+ """Persist one completed model-call compression accounting event."""
319
+ if not thread_id or not event.request_id:
320
+ return
321
+ with self._lock, self._connection() as conn:
322
+ conn.execute(
323
+ "INSERT OR REPLACE INTO model_request_compression_events("
324
+ "request_id, thread_id, event_json, created_at) VALUES (?, ?, ?, ?)",
325
+ (
326
+ event.request_id,
327
+ thread_id,
328
+ json.dumps(event.as_dict(), ensure_ascii=False),
329
+ time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
330
+ ),
331
+ )
332
+ notify_metrics_changed(thread_id)
333
+
334
+ def model_request_events(
335
+ self, *, thread_id: str | None = None, limit: int = 50
336
+ ) -> list[dict[str, Any]]:
337
+ """Return recent model request compression accounting events."""
338
+ where, params = (" WHERE thread_id = ?", (thread_id,)) if thread_id else ("", ())
339
+ bounded = max(1, min(500, int(limit)))
340
+ with self._lock, self._connection() as conn:
341
+ rows = conn.execute(
342
+ "SELECT id, thread_id, event_json, created_at "
343
+ f"FROM model_request_compression_events{where} ORDER BY id DESC LIMIT ?",
344
+ (*params, bounded),
345
+ ).fetchall()
346
+ return [
347
+ {
348
+ "id": int(row["id"]),
349
+ "thread_id": row["thread_id"],
350
+ "created_at": row["created_at"],
351
+ **json.loads(row["event_json"]),
352
+ }
353
+ for row in rows
354
+ ]
355
+
356
+ def estimated_active_saved_tokens(self, *, thread_id: str) -> int:
357
+ """Sum estimated savings of transformed tool outputs currently in graph state."""
358
+ with self._lock, self._connection() as conn:
359
+ rows = conn.execute(
360
+ "SELECT event_json FROM tool_output_events "
361
+ "WHERE thread_id = ? AND ref IS NOT NULL",
362
+ (thread_id,),
363
+ ).fetchall()
364
+ return sum(
365
+ max(
366
+ 0,
367
+ int(json.loads(row["event_json"]).get("estimated_saved_tokens", 0) or 0),
368
+ )
369
+ for row in rows
370
+ )
371
+
372
+ def events(self, *, thread_id: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
373
+ """Return recent transformation decisions with linked retrieval totals."""
374
+ where, params = (" WHERE thread_id = ?", (thread_id,)) if thread_id else ("", ())
375
+ bounded_limit = max(1, min(500, int(limit)))
376
+ with self._lock, self._connection() as conn:
377
+ rows = conn.execute(
378
+ "SELECT id, thread_id, ref, event_json, created_at "
379
+ f"FROM tool_output_events{where} ORDER BY id DESC LIMIT ?",
380
+ (*params, bounded_limit),
381
+ ).fetchall()
382
+ result: list[dict[str, Any]] = []
383
+ for row in rows:
384
+ event = json.loads(row["event_json"])
385
+ retrieval = 0
386
+ if row["ref"]:
387
+ retrieval_row = conn.execute(
388
+ "SELECT COALESCE(SUM(returned_bytes), 0) AS bytes "
389
+ "FROM tool_output_retrieval_events WHERE ref = ?",
390
+ (row["ref"],),
391
+ ).fetchone()
392
+ retrieval = int(retrieval_row["bytes"])
393
+ result.append(
394
+ {
395
+ "id": int(row["id"]),
396
+ "thread_id": row["thread_id"],
397
+ "ref": row["ref"],
398
+ "created_at": row["created_at"],
399
+ "retrieval_bytes": retrieval,
400
+ **event,
401
+ }
402
+ )
403
+ return result
404
+
405
+ def export_diagnostics(self, *, thread_id: str) -> dict[str, Any]:
406
+ """Return complete compression diagnostics for one thread without output blobs."""
407
+ with self._lock, self._connection() as conn:
408
+ tool_rows = conn.execute(
409
+ "SELECT id, thread_id, ref, event_json, created_at "
410
+ "FROM tool_output_events WHERE thread_id = ? ORDER BY id ASC",
411
+ (thread_id,),
412
+ ).fetchall()
413
+ request_rows = conn.execute(
414
+ "SELECT id, thread_id, event_json, created_at "
415
+ "FROM model_request_compression_events WHERE thread_id = ? ORDER BY id ASC",
416
+ (thread_id,),
417
+ ).fetchall()
418
+ retrieval_rows = conn.execute(
419
+ "SELECT id, thread_id, ref, mode, returned_bytes, duration_ms, created_at "
420
+ "FROM tool_output_retrieval_events WHERE thread_id = ? ORDER BY id ASC",
421
+ (thread_id,),
422
+ ).fetchall()
423
+ reuse_rows = conn.execute(
424
+ "SELECT id, thread_id, estimated_avoided_tokens, created_at "
425
+ "FROM tool_output_model_reuse_events WHERE thread_id = ? ORDER BY id ASC",
426
+ (thread_id,),
427
+ ).fetchall()
428
+ interaction_rows = conn.execute(
429
+ "SELECT id, thread_id, event_json, created_at "
430
+ "FROM interaction_events WHERE thread_id = ? ORDER BY id ASC",
431
+ (thread_id,),
432
+ ).fetchall()
433
+
434
+ tool_events = [
435
+ {
436
+ "id": int(row["id"]),
437
+ "thread_id": row["thread_id"],
438
+ "ref": row["ref"],
439
+ "created_at": row["created_at"],
440
+ **json.loads(row["event_json"]),
441
+ }
442
+ for row in tool_rows
443
+ ]
444
+ request_events = [
445
+ {
446
+ "id": int(row["id"]),
447
+ "thread_id": row["thread_id"],
448
+ "created_at": row["created_at"],
449
+ **json.loads(row["event_json"]),
450
+ }
451
+ for row in request_rows
452
+ ]
453
+ retrieval_events = [
454
+ {
455
+ "id": int(row["id"]),
456
+ "thread_id": row["thread_id"],
457
+ "ref": row["ref"],
458
+ "mode": row["mode"],
459
+ "returned_bytes": int(row["returned_bytes"]),
460
+ "duration_ms": float(row["duration_ms"]),
461
+ "created_at": row["created_at"],
462
+ }
463
+ for row in retrieval_rows
464
+ ]
465
+ model_reuse_events = [
466
+ {
467
+ "id": int(row["id"]),
468
+ "thread_id": row["thread_id"],
469
+ "estimated_avoided_tokens": int(row["estimated_avoided_tokens"]),
470
+ "created_at": row["created_at"],
471
+ }
472
+ for row in reuse_rows
473
+ ]
474
+ interaction_events = [
475
+ {
476
+ "id": int(row["id"]),
477
+ "thread_id": row["thread_id"],
478
+ "created_at": row["created_at"],
479
+ **json.loads(row["event_json"]),
480
+ }
481
+ for row in interaction_rows
482
+ ]
483
+ return {
484
+ "summary": self.stats(thread_id=thread_id),
485
+ "model_request_events": request_events,
486
+ "interaction_events": interaction_events,
487
+ "tool_output_events": tool_events,
488
+ "retrieval_events": retrieval_events,
489
+ "model_reuse_events": model_reuse_events,
490
+ }
491
+
492
+ def stats(self, *, thread_id: str | None = None) -> dict[str, Any]:
493
+ where, params = (" WHERE thread_id = ?", (thread_id,)) if thread_id else ("", ())
494
+ with self._lock, self._connection() as conn:
495
+ rows = conn.execute(
496
+ f"SELECT event_json FROM tool_output_events{where}", params
497
+ ).fetchall()
498
+ with self._lock, self._connection() as conn:
499
+ retrieval_rows = conn.execute(
500
+ f"SELECT returned_bytes FROM tool_output_retrieval_events{where}", params
501
+ ).fetchall()
502
+ reuse_rows = conn.execute(
503
+ f"SELECT estimated_avoided_tokens FROM tool_output_model_reuse_events{where}",
504
+ params,
505
+ ).fetchall()
506
+ request_rows = conn.execute(
507
+ f"SELECT event_json FROM model_request_compression_events{where}", params
508
+ ).fetchall()
509
+ interaction_rows = conn.execute(
510
+ f"SELECT event_json FROM interaction_events{where}", params
511
+ ).fetchall()
512
+ events = [json.loads(row["event_json"]) for row in rows]
513
+ request_events = [json.loads(row["event_json"]) for row in request_rows]
514
+ interaction_events = [json.loads(row["event_json"]) for row in interaction_rows]
515
+ turn_ids = {str(item.get("turn_id") or "") for item in request_events}
516
+ turn_ids.discard("")
517
+ tool_calls = [item for item in interaction_events if item.get("event_type") == "tool_call"]
518
+ live_zone_tokens: dict[str, int] = {}
519
+ schema_tokens_by_tool: dict[str, int] = {}
520
+ cache_bust_suspected = 0
521
+ for request_event in request_events:
522
+ for key, value in dict(request_event.get("live_zone_tokens") or {}).items():
523
+ live_zone_tokens[str(key)] = live_zone_tokens.get(str(key), 0) + int(value or 0)
524
+ if (request_event.get("cache_diagnostics") or {}).get("cache_bust_suspected"):
525
+ cache_bust_suspected += 1
526
+ for profile in request_event.get("tool_schema_profiles") or []:
527
+ name = str(profile.get("tool_name") or "unknown")
528
+ schema_tokens_by_tool[name] = schema_tokens_by_tool.get(name, 0) + int(
529
+ profile.get("estimated_tokens", 0) or 0
530
+ )
531
+ retrieval_bytes = sum(int(row["returned_bytes"]) for row in retrieval_rows)
532
+ estimated_reused_tokens = sum(
533
+ int(row["estimated_avoided_tokens"]) for row in reuse_rows
534
+ )
535
+ original = sum(int(item["original_bytes"]) for item in events)
536
+ visible = sum(int(item["visible_bytes"]) for item in events)
537
+ transformed = sum(item["outcome"] == "transformed" for item in events)
538
+ estimated_original_tokens = sum(
539
+ int(item.get("estimated_original_tokens", 0) or 0) for item in events
540
+ )
541
+ estimated_visible_tokens = sum(
542
+ int(item.get("estimated_visible_tokens", 0) or 0) for item in events
543
+ )
544
+ estimated_saved_tokens = max(
545
+ 0, estimated_original_tokens - estimated_visible_tokens
546
+ )
547
+ critical_total = sum(int(item["critical_total"]) for item in events)
548
+ critical_retained = sum(int(item["critical_retained"]) for item in events)
549
+ execution_paths: dict[str, int] = {}
550
+ decisions: dict[str, int] = {}
551
+ reasons: dict[str, int] = {}
552
+ tokens_by_reason: dict[str, int] = {}
553
+ bytes_by_reason: dict[str, int] = {}
554
+ request_input_before = sum(
555
+ int(item.get("input_tokens_before", 0) or 0) for item in request_events
556
+ )
557
+ request_input_after = sum(
558
+ int(item.get("input_tokens_after", 0) or 0) for item in request_events
559
+ )
560
+ request_saved_tokens = sum(
561
+ int(item.get("total_saved_tokens", 0) or 0) for item in request_events
562
+ )
563
+ provider_input_tokens = sum(
564
+ int(item.get("provider_input_tokens", 0) or 0) for item in request_events
565
+ )
566
+ cache_read_tokens = sum(
567
+ int(item.get("cache_read_tokens", 0) or 0) for item in request_events
568
+ )
569
+ cache_write_tokens = sum(
570
+ int(item.get("cache_write_tokens", 0) or 0) for item in request_events
571
+ )
572
+ uncached_input_tokens = sum(
573
+ int(item.get("uncached_input_tokens", 0) or 0) for item in request_events
574
+ )
575
+ request_output_tokens = sum(
576
+ int(item.get("output_tokens", 0) or 0) for item in request_events
577
+ )
578
+ content_breakdown: dict[str, int] = {}
579
+ opportunities: dict[str, int] = {}
580
+ protected_breakdown: dict[str, int] = {}
581
+ for request_event in request_events:
582
+ for key, value in dict(request_event.get("content_breakdown") or {}).items():
583
+ content_breakdown[str(key)] = content_breakdown.get(str(key), 0) + int(
584
+ value or 0
585
+ )
586
+ for key, value in dict(
587
+ request_event.get("opportunity_tokens_by_reason") or {}
588
+ ).items():
589
+ opportunities[str(key)] = opportunities.get(str(key), 0) + int(value or 0)
590
+ for key, value in dict(
591
+ request_event.get("protected_tokens_by_reason") or {}
592
+ ).items():
593
+ protected_breakdown[str(key)] = protected_breakdown.get(str(key), 0) + int(
594
+ value or 0
595
+ )
596
+ for item in events:
597
+ path = str(item.get("execution_path", "legacy_unknown"))
598
+ execution_paths[path] = execution_paths.get(path, 0) + 1
599
+ decision = str(
600
+ item.get("decision")
601
+ or ("transformed" if item.get("outcome") == "transformed" else "fallback")
602
+ )
603
+ reason = str(
604
+ item.get("reason_code")
605
+ or ("compressed" if decision == "transformed" else "legacy_passthrough")
606
+ )
607
+ decisions[decision] = decisions.get(decision, 0) + 1
608
+ reasons[reason] = reasons.get(reason, 0) + 1
609
+ if decision != "transformed":
610
+ tokens_by_reason[reason] = tokens_by_reason.get(reason, 0) + int(
611
+ item.get("estimated_original_tokens", 0) or 0
612
+ )
613
+ bytes_by_reason[reason] = bytes_by_reason.get(reason, 0) + int(
614
+ item.get("original_bytes", 0) or 0
615
+ )
616
+ return {
617
+ "outputs_considered": len(events),
618
+ "transformed": transformed,
619
+ "original_bytes": original,
620
+ "visible_bytes": visible,
621
+ "saved_bytes": max(0, original - visible),
622
+ "estimated_original_tokens": estimated_original_tokens,
623
+ "estimated_visible_tokens": estimated_visible_tokens,
624
+ "estimated_saved_tokens": estimated_saved_tokens,
625
+ "estimated_reused_tokens": estimated_reused_tokens,
626
+ "retrieval_bytes": retrieval_bytes,
627
+ "effective_saved_bytes": max(0, original - visible - retrieval_bytes),
628
+ "savings_ratio": round(1 - visible / original, 4) if original else 0.0,
629
+ "effective_savings_ratio": (
630
+ round(max(0, original - visible - retrieval_bytes) / original, 4)
631
+ if original
632
+ else 0.0
633
+ ),
634
+ "critical_retention": round(critical_retained / critical_total, 4)
635
+ if critical_total
636
+ else 1.0,
637
+ "execution_paths": execution_paths,
638
+ "decisions": decisions,
639
+ "reasons": reasons,
640
+ "tokens_by_reason": tokens_by_reason,
641
+ "bytes_by_reason": bytes_by_reason,
642
+ "skipped": decisions.get("skipped", 0),
643
+ "fallback": decisions.get("fallback", 0),
644
+ "model_requests": len(request_events),
645
+ "turns": len(turn_ids),
646
+ "tool_calls": len(tool_calls),
647
+ "compression_managed_tool_calls": sum(
648
+ bool(item.get("compression_managed")) for item in tool_calls
649
+ ),
650
+ "live_zone_tokens": live_zone_tokens,
651
+ "cache_bust_suspected_requests": cache_bust_suspected,
652
+ "schema_tokens_by_tool": schema_tokens_by_tool,
653
+ "top_schema_tools": sorted(
654
+ schema_tokens_by_tool.items(), key=lambda item: item[1], reverse=True
655
+ )[:10],
656
+ "request_input_tokens_before": request_input_before,
657
+ "request_input_tokens_after": request_input_after,
658
+ "request_saved_tokens": request_saved_tokens,
659
+ "provider_input_tokens": provider_input_tokens,
660
+ "cache_read_tokens": cache_read_tokens,
661
+ "cache_write_tokens": cache_write_tokens,
662
+ "uncached_input_tokens": uncached_input_tokens,
663
+ "request_output_tokens": request_output_tokens,
664
+ "whole_request_savings_ratio": (
665
+ round(request_saved_tokens / request_input_before, 4)
666
+ if request_input_before
667
+ else 0.0
668
+ ),
669
+ "new_input_savings_ratio": (
670
+ round(
671
+ request_saved_tokens
672
+ / (uncached_input_tokens + cache_write_tokens + request_saved_tokens),
673
+ 4,
674
+ )
675
+ if uncached_input_tokens + cache_write_tokens > 0
676
+ else 0.0
677
+ ),
678
+ "content_breakdown": content_breakdown,
679
+ "opportunity_tokens_by_reason": opportunities,
680
+ "protected_tokens_by_reason": protected_breakdown,
681
+ "top_opportunities": sorted(
682
+ opportunities.items(), key=lambda item: item[1], reverse=True
683
+ )[:10],
684
+ "top_protected_sources": sorted(
685
+ protected_breakdown.items(), key=lambda item: item[1], reverse=True
686
+ )[:10],
687
+ }
688
+
689
+ def search(
690
+ self,
691
+ ref: str,
692
+ query: str,
693
+ *,
694
+ expected_thread_id: str | None = None,
695
+ max_results: int = 20,
696
+ context_lines: int = 2,
697
+ ) -> list[tuple[int, str]]:
698
+ record = self.get(ref, expected_thread_id=expected_thread_id)
699
+ terms = {item.casefold() for item in _TOKEN.findall(query) if len(item) > 1}
700
+ if record is None or not terms:
701
+ return []
702
+ lines = record.content.splitlines()
703
+ scored = []
704
+ for index, line in enumerate(lines):
705
+ text = line.casefold()
706
+ score = sum(term in text for term in terms) + (4 if _ERROR_LINE.search(line) else 0)
707
+ if score:
708
+ scored.append((score, index))
709
+ selected: list[tuple[int, str]] = []
710
+ seen: set[int] = set()
711
+ for _, index in sorted(scored, reverse=True)[: max(1, min(50, max_results))]:
712
+ start, end = (
713
+ max(0, index - max(0, context_lines)),
714
+ min(len(lines), index + context_lines + 1),
715
+ )
716
+ for line_no in range(start, end):
717
+ if line_no not in seen:
718
+ seen.add(line_no)
719
+ selected.append((line_no, lines[line_no]))
720
+ return sorted(selected)
721
+