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,59 @@
1
+ """Reversible tool-output transformation domain."""
2
+
3
+ from synapse.tool_output.detection import detect_content_type
4
+ from synapse.tool_output.metrics import clear_metrics_notifier, set_metrics_notifier
5
+ from synapse.tool_output.models import (
6
+ CompressionStageEvent,
7
+ ContentType,
8
+ Detection,
9
+ ModelRequestCompressionEvent,
10
+ ToolOutputRecord,
11
+ TransformContext,
12
+ TransformEvent,
13
+ TransformResult,
14
+ )
15
+ from synapse.tool_output.pipeline import ToolOutputTransformPipeline
16
+ from synapse.tool_output.repository import ToolOutputRepository, content_to_text
17
+ from synapse.tool_output.transformers import (
18
+ CodeTransformer,
19
+ DiffTransformer,
20
+ GenericTransformer,
21
+ GitSummaryTransformer,
22
+ JsonTransformer,
23
+ LogTransformer,
24
+ NativeTransformer,
25
+ PathListTransformer,
26
+ SearchTransformer,
27
+ ToolOutputTransformer,
28
+ load_native_transformers,
29
+ load_transformer_plugins,
30
+ )
31
+
32
+ __all__ = [
33
+ "CodeTransformer",
34
+ "CompressionStageEvent",
35
+ "ContentType",
36
+ "Detection",
37
+ "DiffTransformer",
38
+ "GenericTransformer",
39
+ "GitSummaryTransformer",
40
+ "JsonTransformer",
41
+ "LogTransformer",
42
+ "ModelRequestCompressionEvent",
43
+ "NativeTransformer",
44
+ "PathListTransformer",
45
+ "SearchTransformer",
46
+ "ToolOutputRecord",
47
+ "ToolOutputRepository",
48
+ "ToolOutputTransformer",
49
+ "ToolOutputTransformPipeline",
50
+ "TransformContext",
51
+ "TransformEvent",
52
+ "TransformResult",
53
+ "clear_metrics_notifier",
54
+ "content_to_text",
55
+ "detect_content_type",
56
+ "load_native_transformers",
57
+ "load_transformer_plugins",
58
+ "set_metrics_notifier",
59
+ ]
@@ -0,0 +1,170 @@
1
+ """Content classification rules for tool-output transformation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ from synapse.tool_output.models import ContentType, Detection
9
+
10
+ _SEARCH_LINE = re.compile(
11
+ r"^(?P<path>.+?)(?P<separator>[:\-])(?P<line>\d+)(?P=separator)(?P<body>.*)$"
12
+ )
13
+ _ERROR_LINE = re.compile(
14
+ r"\b(error|fatal|failed|failure|exception|traceback|critical)\b", re.I
15
+ )
16
+ _LOG_SUMMARY = re.compile(r"\b(passed|failed|skipped|collected|tests? run|exit code)\b", re.I)
17
+ _NUMBERED_SOURCE_LINE = re.compile(r"^(?P<indent>\s*)\d+(?:\.\d+)?\t(?P<body>.*)$")
18
+ _PATH_ROW = re.compile(r"^(?:(?:[A-Za-z]:)?[/\\]|\.{0,2}[/\\])?(?:[^/\\\s:]+[/\\])+[^/\\\s:]+$")
19
+ _CODE_SUFFIXES = frozenset(
20
+ {
21
+ ".c", ".cc", ".cpp", ".cs", ".go", ".h", ".hpp", ".java", ".js", ".jsx",
22
+ ".kt", ".php", ".py", ".pyi", ".rb", ".rs", ".scala", ".sh", ".swift", ".ts",
23
+ ".tsx",
24
+ }
25
+ )
26
+
27
+ def _strip_numbered_source_lines(content: str) -> tuple[str, int, int]:
28
+ """Return a detection-only view without read_file's cat-n line prefixes."""
29
+ normalized: list[str] = []
30
+ numbered = 0
31
+ non_empty = 0
32
+ for line in content.splitlines():
33
+ if line.strip():
34
+ non_empty += 1
35
+ match = _NUMBERED_SOURCE_LINE.match(line)
36
+ if match:
37
+ numbered += 1
38
+ normalized.append(match.group("indent") + match.group("body"))
39
+ else:
40
+ normalized.append(line)
41
+ return "\n".join(normalized), numbered, non_empty
42
+
43
+
44
+ def _code_marker_count(content: str) -> int:
45
+ return sum(
46
+ bool(
47
+ re.match(
48
+ r"^\s*(?:async\s+def|def|class|function|func|fn|import|from|use)\b",
49
+ line,
50
+ )
51
+ )
52
+ for line in content.splitlines()[:200]
53
+ )
54
+
55
+
56
+ def _diff_bloat_metadata(content: str) -> dict[str, Any]:
57
+ total_lines = 0
58
+ change_lines = 0
59
+ context_lines = 0
60
+ in_hunk = False
61
+ for line in content.splitlines():
62
+ total_lines += 1
63
+ if line.startswith("@@"):
64
+ in_hunk = True
65
+ continue
66
+ if line.startswith("diff --git"):
67
+ in_hunk = False
68
+ continue
69
+ if not in_hunk or line.startswith(("+++", "---")):
70
+ continue
71
+ if line.startswith(("+", "-")):
72
+ change_lines += 1
73
+ elif line.startswith(" "):
74
+ context_lines += 1
75
+ denominator = context_lines + change_lines
76
+ context_ratio = context_lines / denominator if denominator else 0.0
77
+ normal_context_ratio = 0.6
78
+ bloat_score = (
79
+ max(0.0, min(1.0, (context_ratio - normal_context_ratio) / (1 - normal_context_ratio)))
80
+ if denominator
81
+ else 0.0
82
+ )
83
+ return {
84
+ "total_lines": total_lines,
85
+ "change_lines": change_lines,
86
+ "context_lines": context_lines,
87
+ "context_ratio": round(context_ratio, 4),
88
+ "bloat_score": round(bloat_score, 4),
89
+ "dense_diff": bool(total_lines >= 50 and context_ratio <= normal_context_ratio),
90
+ }
91
+
92
+
93
+ def detect_content_type(content: str) -> Detection:
94
+ lines = content.splitlines()
95
+ if not lines:
96
+ return Detection(ContentType.TEXT, 1.0)
97
+ search_count = sum(bool(_SEARCH_LINE.match(line)) for line in lines[:100])
98
+ timestamp_count = sum(
99
+ bool(re.match(r"^\d{4}-\d{2}-\d{2}(?:[ T]|$)", line)) for line in lines[:100]
100
+ )
101
+ enough_search_lines = search_count >= max(3, len(lines[:100]) // 3)
102
+ mostly_timestamped = timestamp_count >= max(3, len(lines[:100]) // 2)
103
+ if enough_search_lines and not mostly_timestamped:
104
+ return Detection(
105
+ ContentType.SEARCH, min(1.0, search_count / max(1, len(lines[:100])) + 0.3)
106
+ )
107
+ path_rows = sum(bool(_PATH_ROW.match(line)) for line in lines)
108
+ if len(lines) >= 3 and path_rows == len(lines):
109
+ return Detection(ContentType.PATHS, 1.0)
110
+ if any(line.startswith(("diff --git", "--- a/", "+++ b/", "@@")) for line in lines[:20]):
111
+ return Detection(ContentType.DIFF, 0.95)
112
+ git_summary_markers = sum(
113
+ bool(
114
+ re.match(
115
+ r"^(?:Merge made by the .+ strategy\.|(?:create|delete) mode \d+ |"
116
+ r"rename .+ => .+|\s*\d+ files? changed(?:,|$)|\s*\d+ insertions?\(\+\)|"
117
+ r"\s*\d+ deletions?\(-\)| .+\s+\|\s+\d+\s+[+\-]+$)",
118
+ line,
119
+ )
120
+ )
121
+ for line in lines[:200]
122
+ )
123
+ if git_summary_markers >= 2:
124
+ return Detection(ContentType.GIT_SUMMARY, min(0.95, 0.45 + git_summary_markers / 20))
125
+ sampled = lines[:100]
126
+ log_markers = sum(
127
+ bool(_ERROR_LINE.search(line) or _LOG_SUMMARY.search(line)) for line in sampled
128
+ )
129
+ timestamped = timestamp_count
130
+ if log_markers >= 3 or (
131
+ timestamped >= max(3, len(sampled) // 2) and any(_ERROR_LINE.search(line) for line in lines)
132
+ ):
133
+ return Detection(ContentType.LOG, 0.8)
134
+ code_markers = _code_marker_count(content)
135
+ if code_markers >= 3:
136
+ return Detection(ContentType.CODE, min(0.95, 0.4 + code_markers / max(1, len(lines[:200]))))
137
+ try:
138
+ parsed = json.loads(content)
139
+ if isinstance(parsed, (list, dict)):
140
+ return Detection(ContentType.JSON, 0.9)
141
+ except (ValueError, TypeError):
142
+ pass
143
+ return Detection(ContentType.TEXT, 0.3)
144
+
145
+
146
+ def _critical_lines(content: str, content_type: ContentType) -> list[str]:
147
+ lines = content.splitlines()
148
+ if content_type is ContentType.LOG:
149
+ return [
150
+ line
151
+ for line in lines
152
+ if _ERROR_LINE.search(line) or re.search(r"\b[A-Za-z_]+(?:Error|Exception):", line)
153
+ ]
154
+ if content_type is ContentType.DIFF:
155
+ return [
156
+ line
157
+ for line in lines
158
+ if line.startswith(("+", "-")) and not line.startswith(("+++", "---"))
159
+ ]
160
+ if content_type is ContentType.GIT_SUMMARY:
161
+ return [
162
+ line
163
+ for line in lines
164
+ if re.match(
165
+ r"^(?:Merge made by the .+ strategy\.|\s*\d+ files? changed(?:,|$)|"
166
+ r"\s*\d+ insertions?\(\+\)|\s*\d+ deletions?\(-\))",
167
+ line,
168
+ )
169
+ ]
170
+ return []
@@ -0,0 +1,32 @@
1
+ """Best-effort process-local notifications for tool-output metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from collections.abc import Callable
7
+
8
+ _metrics_notifier: Callable[[str], None] | None = None
9
+ _metrics_notifier_lock = threading.RLock()
10
+
11
+
12
+ def set_metrics_notifier(notifier: Callable[[str], None] | None) -> None:
13
+ """Install a callback invoked after metrics-changing persistence writes."""
14
+ global _metrics_notifier
15
+ with _metrics_notifier_lock:
16
+ _metrics_notifier = notifier
17
+
18
+
19
+ def clear_metrics_notifier() -> None:
20
+ """Remove the active process-local metrics callback."""
21
+ set_metrics_notifier(None)
22
+
23
+
24
+ def notify_metrics_changed(thread_id: str) -> None:
25
+ """Notify an observer without allowing UI failures to affect tool execution."""
26
+ with _metrics_notifier_lock:
27
+ notifier = _metrics_notifier
28
+ if notifier is not None:
29
+ try:
30
+ notifier(thread_id)
31
+ except Exception: # noqa: BLE001
32
+ pass
@@ -0,0 +1,173 @@
1
+ """Data models for reversible tool-output transformation."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import asdict, dataclass, field
5
+ from enum import StrEnum
6
+ from typing import Any
7
+
8
+
9
+ class ContentType(StrEnum):
10
+ SEARCH = "search"
11
+ PATHS = "paths"
12
+ LOG = "log"
13
+ DIFF = "diff"
14
+ GIT_SUMMARY = "git-summary"
15
+ JSON = "json"
16
+ CODE = "code"
17
+ TEXT = "text"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Detection:
22
+ content_type: ContentType
23
+ confidence: float
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class TransformContext:
28
+ tool_name: str
29
+ status: str
30
+ query: str = ""
31
+ tool_args: dict[str, Any] = field(default_factory=dict)
32
+ file_path: str = ""
33
+ file_suffix: str = ""
34
+ fresh_read_source: bool = True
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class CompressionStageEvent:
39
+ """One observable stage in a tool-output compression decision."""
40
+
41
+ phase: str
42
+ algorithm: str
43
+ applied: bool
44
+ reason_code: str
45
+ input_bytes: int = 0
46
+ output_bytes: int = 0
47
+ input_tokens: int = 0
48
+ output_tokens: int = 0
49
+ duration_ms: float = 0.0
50
+ metadata: dict[str, Any] = field(default_factory=dict)
51
+
52
+ def as_dict(self) -> dict[str, Any]:
53
+ return asdict(self)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class TransformResult:
58
+ content: str
59
+ transformer: str
60
+ content_type: ContentType
61
+ critical_total: int
62
+ critical_retained: int
63
+ metadata: dict[str, Any] = field(default_factory=dict)
64
+ stages: tuple[CompressionStageEvent, ...] = ()
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class ToolOutputRecord:
69
+ ref: str
70
+ thread_id: str
71
+ checkpoint_ns: str
72
+ tool_call_id: str
73
+ tool_name: str
74
+ status: str
75
+ content: str
76
+ size_bytes: int
77
+ sha256: str
78
+ created_at: str
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class ModelRequestCompressionEvent:
83
+ """One model call's compression and provider-safety accounting."""
84
+
85
+ request_id: str
86
+ provider: str
87
+ api_style: str
88
+ auth_mode: str
89
+ model: str
90
+ input_tokens_before: int
91
+ input_tokens_after: int
92
+ provider_input_tokens: int = 0
93
+ cache_read_tokens: int = 0
94
+ cache_write_tokens: int = 0
95
+ uncached_input_tokens: int = 0
96
+ output_tokens: int = 0
97
+ tool_output_saved_tokens: int = 0
98
+ prompt_saved_tokens: int = 0
99
+ summarization_saved_tokens: int = 0
100
+ total_saved_tokens: int = 0
101
+ candidate_blocks: int = 0
102
+ transformed_blocks: int = 0
103
+ protected_tokens_by_reason: dict[str, int] = field(default_factory=dict)
104
+ content_breakdown: dict[str, int] = field(default_factory=dict)
105
+ opportunity_tokens_by_reason: dict[str, int] = field(default_factory=dict)
106
+ turn_id: str = ""
107
+ turn_index: int = 0
108
+ model_call_index: int = 0
109
+ live_zone_plan: list[dict[str, Any]] = field(default_factory=list)
110
+ live_zone_tokens: dict[str, int] = field(default_factory=dict)
111
+ wire_fingerprints: dict[str, Any] = field(default_factory=dict)
112
+ cache_diagnostics: dict[str, Any] = field(default_factory=dict)
113
+ tool_schema_profiles: list[dict[str, Any]] = field(default_factory=list)
114
+ token_count_method: str = "langchain_approximate"
115
+ duration_ms: float = 0.0
116
+
117
+ def as_dict(self) -> dict[str, Any]:
118
+ data = asdict(self)
119
+ whole_denominator = self.input_tokens_after + self.total_saved_tokens
120
+ new_input = self.uncached_input_tokens + self.cache_write_tokens
121
+ data["whole_request_savings_ratio"] = (
122
+ round(self.total_saved_tokens / whole_denominator, 4) if whole_denominator else 0.0
123
+ )
124
+ data["new_input_savings_ratio"] = (
125
+ round(self.total_saved_tokens / (new_input + self.total_saved_tokens), 4)
126
+ if new_input > 0
127
+ else 0.0
128
+ )
129
+ return data
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class TransformEvent:
134
+ content_type: str
135
+ transformer: str
136
+ outcome: str
137
+ original_bytes: int
138
+ visible_bytes: int
139
+ duration_ms: float
140
+ critical_total: int
141
+ critical_retained: int
142
+ ref_created: bool
143
+ execution_path: str = "python_only"
144
+ estimated_original_tokens: int = 0
145
+ estimated_visible_tokens: int = 0
146
+ decision: str = "transformed"
147
+ reason_code: str = "compressed"
148
+ reason_detail: str = ""
149
+ eligible: bool = True
150
+ source_kind: str = "tool-output"
151
+ detection_confidence: float = 0.0
152
+ threshold_bytes: int = 0
153
+ tool_call_id: str = ""
154
+ tool_name: str = ""
155
+ status: str = "success"
156
+ checkpoint_ns: str = ""
157
+ message_id: str = ""
158
+ algorithm_output_bytes: int = 0
159
+ algorithm_output_tokens: int = 0
160
+ token_count_method: str = "langchain_approximate"
161
+ content_sha256: str = ""
162
+ stages: tuple[CompressionStageEvent, ...] = ()
163
+
164
+ def as_dict(self) -> dict[str, Any]:
165
+ data = asdict(self)
166
+ data["saved_bytes"] = max(0, self.original_bytes - self.visible_bytes)
167
+ data["estimated_saved_tokens"] = max(
168
+ 0, self.estimated_original_tokens - self.estimated_visible_tokens
169
+ )
170
+ data["savings_ratio"] = (
171
+ round(1 - self.visible_bytes / self.original_bytes, 4) if self.original_bytes else 0.0
172
+ )
173
+ return data