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 @@
1
+ """Interactive command parsing and dispatch."""
@@ -0,0 +1,573 @@
1
+ """Compression diagnostics slash-command handler."""
2
+ from __future__ import annotations
3
+
4
+ import csv
5
+ import io
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from synapse.commands.helpers import format_bytes, markdown_escape
12
+ from synapse.commands.result import SlashResult
13
+ from synapse.sessions.store import SessionStore
14
+
15
+
16
+ def _store(settings: Any) -> SessionStore:
17
+ return SessionStore(settings.resolved_sessions_path())
18
+
19
+ def _resolve_session_ref(
20
+ settings: Any, current_thread_id: str, args: list[str]
21
+ ) -> tuple[str | None, str | None]:
22
+ """Resolve an optional thread id/title argument to a unique session id."""
23
+ if not args:
24
+ return current_thread_id, None
25
+ query = " ".join(args).strip()
26
+ info = _store(settings).resolve_session_ref(query)
27
+ if info is None:
28
+ return None, f"session not found or ambiguous: {query}"
29
+ return info.thread_id, None
30
+
31
+
32
+ _COMPRESSION_EXPORT_FORMAT_ALIASES = {
33
+ "json": "json",
34
+ "j": "json",
35
+ "csv": "csv",
36
+ "c": "csv",
37
+ }
38
+
39
+
40
+ def _default_compression_export_path(settings: Any, thread_id: str, fmt: str) -> Path:
41
+ safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in (thread_id or "session"))
42
+ safe = (safe or "session")[:80]
43
+ parent = Path(settings.checkpoint_path).expanduser().resolve().parent
44
+ return parent / "exports" / f"{safe}.compression.{fmt}"
45
+
46
+
47
+ def _compression_export_csv(payload: dict[str, Any]) -> str:
48
+ """Flatten heterogeneous diagnostics into one portable CSV table."""
49
+ rows: list[dict[str, Any]] = [
50
+ {
51
+ "record_type": "metadata",
52
+ "thread_id": payload["thread_id"],
53
+ "metric": "schema_version",
54
+ "value": payload["schema_version"],
55
+ },
56
+ {
57
+ "record_type": "metadata",
58
+ "thread_id": payload["thread_id"],
59
+ "metric": "exported_at",
60
+ "value": payload["exported_at"],
61
+ },
62
+ ]
63
+ rows.extend(
64
+ {
65
+ "record_type": "summary",
66
+ "thread_id": payload["thread_id"],
67
+ "metric": key,
68
+ "value": value,
69
+ }
70
+ for key, value in sorted(dict(payload.get("summary") or {}).items())
71
+ )
72
+ for record_type, key in (
73
+ ("model_request", "model_request_events"),
74
+ ("interaction", "interaction_events"),
75
+ ("tool_output", "tool_output_events"),
76
+ ("retrieval", "retrieval_events"),
77
+ ("model_reuse", "model_reuse_events"),
78
+ ):
79
+ rows.extend(
80
+ {"record_type": record_type, **dict(item)} for item in payload.get(key, [])
81
+ )
82
+
83
+ preferred = [
84
+ "record_type",
85
+ "thread_id",
86
+ "id",
87
+ "created_at",
88
+ "metric",
89
+ "value",
90
+ "request_id",
91
+ "provider",
92
+ "api_style",
93
+ "auth_mode",
94
+ "model",
95
+ "tool_call_id",
96
+ "tool_name",
97
+ "decision",
98
+ "reason_code",
99
+ ]
100
+ all_fields = {key for row in rows for key in row}
101
+ fieldnames = [key for key in preferred if key in all_fields]
102
+ fieldnames.extend(sorted(all_fields - set(fieldnames)))
103
+
104
+ def cell(value: Any) -> Any:
105
+ if isinstance(value, dict | list | tuple):
106
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
107
+ return "" if value is None else value
108
+
109
+ output = io.StringIO(newline="")
110
+ writer = csv.DictWriter(output, fieldnames=fieldnames, lineterminator="\n")
111
+ writer.writeheader()
112
+ writer.writerows({key: cell(value) for key, value in row.items()} for row in rows)
113
+ return output.getvalue()
114
+
115
+
116
+ def _compression_export_result(
117
+ settings: Any, current_thread_id: str, args: list[str]
118
+ ) -> SlashResult:
119
+ """Export complete compression diagnostics for one session to JSON or CSV."""
120
+ fmt = "json"
121
+ session_args: list[str] = []
122
+ path_args: list[str] = []
123
+ format_index = next(
124
+ (
125
+ index
126
+ for index, value in enumerate(args)
127
+ if value.casefold() in _COMPRESSION_EXPORT_FORMAT_ALIASES
128
+ ),
129
+ None,
130
+ )
131
+ if format_index is not None:
132
+ fmt = _COMPRESSION_EXPORT_FORMAT_ALIASES[args[format_index].casefold()]
133
+ session_args = args[:format_index]
134
+ path_args = args[format_index + 1 :]
135
+ elif args:
136
+ candidate = Path(" ".join(args)).expanduser()
137
+ if candidate.suffix.casefold() in {".json", ".csv"}:
138
+ fmt = candidate.suffix.casefold().lstrip(".")
139
+ path_args = args
140
+ else:
141
+ session_args = args
142
+
143
+ thread_id, error = _resolve_session_ref(settings, current_thread_id, session_args)
144
+ if error or thread_id is None:
145
+ return SlashResult(handled=True, lines=[error or "session not found"], error=True)
146
+
147
+ from synapse.tool_output.repository import ToolOutputRepository
148
+
149
+ repo = ToolOutputRepository(settings.resolved_tool_output_db_path())
150
+ diagnostics = repo.export_diagnostics(thread_id=thread_id)
151
+ payload = {
152
+ "schema_version": 2,
153
+ "exported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
154
+ "thread_id": thread_id,
155
+ **diagnostics,
156
+ }
157
+ text = (
158
+ json.dumps(payload, ensure_ascii=False, indent=2, default=str)
159
+ if fmt == "json"
160
+ else _compression_export_csv(payload)
161
+ )
162
+ target = (
163
+ Path(" ".join(path_args)).expanduser()
164
+ if path_args
165
+ else _default_compression_export_path(settings, thread_id, fmt)
166
+ )
167
+ try:
168
+ target = (Path.cwd() / target).resolve() if not target.is_absolute() else target.resolve()
169
+ target.parent.mkdir(parents=True, exist_ok=True)
170
+ target.write_text(text, encoding="utf-8")
171
+ except OSError as exc:
172
+ return SlashResult(
173
+ handled=True,
174
+ lines=[f"compression export failed: {exc}"],
175
+ error=True,
176
+ )
177
+
178
+ counts = {
179
+ "model requests": len(payload["model_request_events"]),
180
+ "tool calls": len(payload["interaction_events"]),
181
+ "tool outputs": len(payload["tool_output_events"]),
182
+ "retrievals": len(payload["retrieval_events"]),
183
+ "model reuses": len(payload["model_reuse_events"]),
184
+ }
185
+ confirm = f"exported compression {fmt} -> {target}"
186
+ return SlashResult(
187
+ handled=True,
188
+ lines=[
189
+ confirm,
190
+ f"thread_id={thread_id}",
191
+ ", ".join(f"{key}={value}" for key, value in counts.items()),
192
+ ],
193
+ notice=confirm,
194
+ markdown=(
195
+ "## Compression Export\n\n"
196
+ f"- **thread**: `{thread_id}`\n"
197
+ f"- **format**: {fmt}\n"
198
+ f"- **path**: `{target}`\n"
199
+ + "".join(f"- **{key}**: {value}\n" for key, value in counts.items())
200
+ ),
201
+ )
202
+
203
+
204
+ def handle_compression(settings: Any, current_thread_id: str, args: list[str]) -> SlashResult:
205
+ """Render persistent compression diagnostics or recent decision events."""
206
+ mode = args[0].casefold() if args else "stats"
207
+ if mode == "export":
208
+ return _compression_export_result(settings, current_thread_id, args[1:])
209
+ show_profile = mode in {"profile", "report"}
210
+ show_requests = mode in {"requests", "request"}
211
+ show_events = mode in {"events", "skipped", "fallback", "tool"}
212
+ rest = args[1:] if show_events or show_requests or show_profile else args
213
+ decision_filter = mode if mode in {"skipped", "fallback"} else ""
214
+ tool_filter = ""
215
+ request_filter = ""
216
+ if mode == "request":
217
+ if not rest:
218
+ return SlashResult(
219
+ handled=True,
220
+ lines=["usage: /compression request <request_id> [session]"],
221
+ error=True,
222
+ )
223
+ request_filter = rest[0]
224
+ rest = rest[1:]
225
+ if mode == "tool":
226
+ if not rest:
227
+ return SlashResult(
228
+ handled=True,
229
+ lines=["usage: /compression tool <tool_call_id> [session] [limit]"],
230
+ error=True,
231
+ )
232
+ tool_filter = rest[0]
233
+ rest = rest[1:]
234
+ limit = 10
235
+ if (show_events or show_requests) and rest and rest[-1].isdigit():
236
+ limit = max(1, min(50, int(rest[-1])))
237
+ rest = rest[:-1]
238
+ thread_id, error = _resolve_session_ref(settings, current_thread_id, rest)
239
+ if error or thread_id is None:
240
+ return SlashResult(handled=True, lines=[error or "session not found"], error=True)
241
+
242
+ from synapse.tool_output.repository import ToolOutputRepository
243
+
244
+ repo = ToolOutputRepository(settings.resolved_tool_output_db_path())
245
+ if show_profile:
246
+ stats = repo.stats(thread_id=thread_id)
247
+ breakdown = stats.get("content_breakdown") or {}
248
+ opportunities = stats.get("top_opportunities") or []
249
+ md = [
250
+ "## Compression Profile",
251
+ "",
252
+ f"Thread: `{thread_id}`",
253
+ "",
254
+ (
255
+ f"Turns: {stats.get('turns', 0)} — model calls: "
256
+ f"{stats.get('model_requests', 0)} — tool calls: "
257
+ f"{stats.get('tool_calls', 0)} — compression-managed: "
258
+ f"{stats.get('compression_managed_tool_calls', 0)}"
259
+ ),
260
+ (
261
+ f"Cache bust suspected: {stats.get('cache_bust_suspected_requests', 0)} requests"
262
+ ),
263
+ "",
264
+ "### Live-zone distribution",
265
+ "",
266
+ "| Zone | Estimated tokens |",
267
+ "|---|---:|",
268
+ *[
269
+ f"| {markdown_escape(str(zone))} | ~{int(tokens or 0)} |"
270
+ for zone, tokens in sorted((stats.get("live_zone_tokens") or {}).items())
271
+ ],
272
+ "",
273
+ "### Tool schema ranking",
274
+ "",
275
+ "| Tool | Cumulative estimated tokens |",
276
+ "|---|---:|",
277
+ *[
278
+ f"| {markdown_escape(str(name))} | ~{int(tokens or 0)} |"
279
+ for name, tokens in stats.get("top_schema_tools") or []
280
+ ],
281
+ "",
282
+ "### Request content breakdown",
283
+ "",
284
+ "| Source | Estimated tokens | Share |",
285
+ "|---|---:|---:|",
286
+ ]
287
+ # ``tool_output_original`` reconstructs the pre-compression baseline;
288
+ # it is not part of the final model-visible request. Excluding it keeps
289
+ # model-visible shares additive instead of counting tool output twice.
290
+ reference_sources = {"tool_output_original"}
291
+ total = sum(
292
+ max(0, int(value or 0))
293
+ for source, value in breakdown.items()
294
+ if source not in reference_sources
295
+ )
296
+ lines = [f"thread_id={thread_id}", f"profile_total_tokens=~{total}"]
297
+ for source, tokens in sorted(
298
+ breakdown.items(), key=lambda item: int(item[1] or 0), reverse=True
299
+ ):
300
+ amount = max(0, int(tokens or 0))
301
+ if source in reference_sources:
302
+ md.append(f"| {markdown_escape(str(source))} | ~{amount} | — |")
303
+ lines.append(f"{source}=~{amount} (reference; excluded from total)")
304
+ continue
305
+ share = amount / total if total else 0.0
306
+ md.append(f"| {markdown_escape(str(source))} | ~{amount} | {share:.1%} |")
307
+ lines.append(f"{source}=~{amount} ({share:.1%})")
308
+ if any(source in breakdown for source in reference_sources):
309
+ md.extend(
310
+ [
311
+ "",
312
+ (
313
+ "`tool_output_original` is the reconstructed pre-compression "
314
+ "baseline and is excluded from model-visible totals and shares."
315
+ ),
316
+ ]
317
+ )
318
+ md.extend(
319
+ [
320
+ "",
321
+ "### Ranked optimization opportunities",
322
+ "",
323
+ "| Rank | Reason | Estimated tokens |",
324
+ "|---:|---|---:|",
325
+ ]
326
+ )
327
+ if opportunities:
328
+ for rank, item in enumerate(opportunities, 1):
329
+ reason, tokens = item
330
+ md.append(f"| {rank} | {markdown_escape(str(reason))} | ~{int(tokens or 0)} |")
331
+ else:
332
+ md.append("| - | No request profile events yet | 0 |")
333
+ protected = stats.get("top_protected_sources") or []
334
+ md.extend(
335
+ [
336
+ "",
337
+ "### Provider-protected context",
338
+ "",
339
+ "| Reason | Estimated tokens |",
340
+ "|---|---:|",
341
+ ]
342
+ )
343
+ if protected:
344
+ for reason, tokens in protected:
345
+ md.append(f"| {markdown_escape(str(reason))} | ~{int(tokens or 0)} |")
346
+ else:
347
+ md.append("| - | 0 |")
348
+ return SlashResult(handled=True, lines=lines, markdown="\n".join(md))
349
+ if show_requests:
350
+ requests = repo.model_request_events(thread_id=thread_id, limit=max(limit, 50))
351
+ if request_filter:
352
+ requests = [item for item in requests if item.get("request_id") == request_filter]
353
+ requests = requests[:limit]
354
+ if not requests:
355
+ return SlashResult(
356
+ handled=True,
357
+ lines=[f"thread_id={thread_id}", "no model request compression events"],
358
+ )
359
+ md = [
360
+ "## Model Request Compression",
361
+ "",
362
+ f"Thread: `{thread_id}`",
363
+ "",
364
+ (
365
+ "| Time | Request | Provider / API | Input before | Input after | "
366
+ "Saved | Cache | Output |"
367
+ ),
368
+ "|---|---|---|---:|---:|---:|---:|---:|",
369
+ ]
370
+ lines = [f"thread_id={thread_id}"]
371
+ for item in requests:
372
+ request_id = str(item.get("request_id") or "-")
373
+ before = int(item.get("input_tokens_before", 0) or 0)
374
+ after = int(item.get("input_tokens_after", 0) or 0)
375
+ saved = int(item.get("total_saved_tokens", 0) or 0)
376
+ cache = int(item.get("cache_read_tokens", 0) or 0)
377
+ output = int(item.get("output_tokens", 0) or 0)
378
+ md.append(
379
+ "| {time} | `{request}` | {provider}/{api} | ~{before} | ~{after} | "
380
+ "~{saved} | {cache} | {output} |".format(
381
+ time=markdown_escape(str(item.get("created_at") or "-")),
382
+ request=markdown_escape(request_id),
383
+ provider=markdown_escape(str(item.get("provider") or "unknown")),
384
+ api=markdown_escape(str(item.get("api_style") or "unknown")),
385
+ before=before,
386
+ after=after,
387
+ saved=saved,
388
+ cache=cache,
389
+ output=output,
390
+ )
391
+ )
392
+ protected = item.get("protected_tokens_by_reason") or {}
393
+ breakdown = item.get("content_breakdown") or {}
394
+ opportunities = item.get("opportunity_tokens_by_reason") or {}
395
+ lines.append(
396
+ f"{request_id} turn={item.get('turn_index', 0)} "
397
+ f"call={item.get('model_call_index', 0)} before=~{before} after=~{after} "
398
+ f"saved=~{saved} protected={protected} breakdown={breakdown} "
399
+ f"opportunities={opportunities} live_zone={item.get('live_zone_tokens') or {}} "
400
+ f"cache={item.get('cache_diagnostics') or {}}"
401
+ )
402
+ if request_filter:
403
+ md.extend(
404
+ [
405
+ "",
406
+ "### Content breakdown",
407
+ "",
408
+ "| Source | Estimated tokens |",
409
+ "|---|---:|",
410
+ *[
411
+ f"| {markdown_escape(str(key))} | ~{int(value or 0)} |"
412
+ for key, value in sorted(
413
+ breakdown.items(),
414
+ key=lambda pair: int(pair[1] or 0),
415
+ reverse=True,
416
+ )
417
+ ],
418
+ "",
419
+ "### Optimization opportunities",
420
+ "",
421
+ "| Reason | Estimated tokens |",
422
+ "|---|---:|",
423
+ *[
424
+ f"| {markdown_escape(str(key))} | ~{int(value or 0)} |"
425
+ for key, value in sorted(
426
+ opportunities.items(),
427
+ key=lambda pair: int(pair[1] or 0),
428
+ reverse=True,
429
+ )
430
+ ],
431
+ ]
432
+ )
433
+ return SlashResult(handled=True, lines=lines, markdown="\n".join(md))
434
+ if show_events:
435
+ fetch_limit = min(500, max(limit, 50) if decision_filter or tool_filter else limit)
436
+ events = repo.events(thread_id=thread_id, limit=fetch_limit)
437
+ if decision_filter:
438
+ events = [
439
+ event
440
+ for event in events
441
+ if str(event.get("decision") or "").casefold() == decision_filter
442
+ ]
443
+ if tool_filter:
444
+ events = [
445
+ event
446
+ for event in events
447
+ if str(event.get("tool_call_id") or "") == tool_filter
448
+ ]
449
+ events = events[:limit]
450
+ if not events:
451
+ return SlashResult(
452
+ handled=True,
453
+ lines=[f"thread_id={thread_id}", "no tool-output events"],
454
+ markdown=(
455
+ f"## Tool Output Events\n\nThread: `{thread_id}`\n\n"
456
+ "No tool-output events."
457
+ ),
458
+ )
459
+ md = [
460
+ "## Compression Decision Events",
461
+ "",
462
+ f"Thread: `{thread_id}`",
463
+ "",
464
+ (
465
+ "| Time | Tool / ID | Type | Decision | Reason | Pipeline | "
466
+ "Original | Final | Saved tok |"
467
+ ),
468
+ "|---|---|---|---|---|---|---:|---:|---:|",
469
+ ]
470
+ lines = [f"thread_id={thread_id}"]
471
+ for event in events:
472
+ saved = int(event.get("estimated_saved_tokens", 0) or 0)
473
+ tool = str(event.get("tool_name") or "-")
474
+ call_id = str(event.get("tool_call_id") or "-")
475
+ decision = str(
476
+ event.get("decision")
477
+ or ("transformed" if event.get("outcome") == "transformed" else "fallback")
478
+ )
479
+ reason = str(event.get("reason_code") or "legacy_passthrough")
480
+ row = (
481
+ "| {time} | {tool}<br>`{call_id}` | {type} | {decision} | {reason} | "
482
+ "{transformer} | {original} | {visible} | {saved} |"
483
+ )
484
+ md.append(
485
+ row.format(
486
+ time=markdown_escape(str(event.get("created_at", "-"))),
487
+ tool=markdown_escape(tool),
488
+ call_id=markdown_escape(call_id),
489
+ type=markdown_escape(str(event.get("content_type", "-"))),
490
+ decision=markdown_escape(decision),
491
+ reason=markdown_escape(reason),
492
+ transformer=markdown_escape(str(event.get("transformer", "-"))),
493
+ original=format_bytes(event.get("original_bytes", 0)),
494
+ visible=format_bytes(event.get("visible_bytes", 0)),
495
+ saved=f"~{saved}" if saved else "0",
496
+ )
497
+ )
498
+ lines.append(
499
+ f"{event.get('created_at', '-')} {tool}/{call_id} "
500
+ f"{decision}:{reason} saved_tokens=~{saved}"
501
+ )
502
+ return SlashResult(handled=True, lines=lines, markdown="\n".join(md))
503
+
504
+ stats = repo.stats(thread_id=thread_id)
505
+ effective_saved = format_bytes(stats["effective_saved_bytes"])
506
+ effective_ratio = f"{stats['effective_savings_ratio']:.1%}"
507
+ rows = [
508
+ ("thread_id", thread_id),
509
+ ("outputs considered", str(stats["outputs_considered"])),
510
+ ("transformed", str(stats["transformed"])),
511
+ ("skipped", str(stats.get("skipped", 0) or 0)),
512
+ ("fallback", str(stats.get("fallback", 0) or 0)),
513
+ ("model requests", str(stats.get("model_requests", 0) or 0)),
514
+ (
515
+ "request input before/after",
516
+ f"~{stats.get('request_input_tokens_before', 0)}/"
517
+ f"~{stats.get('request_input_tokens_after', 0)}",
518
+ ),
519
+ ("request saved tokens", f"~{stats.get('request_saved_tokens', 0) or 0}"),
520
+ ("whole request savings", f"{stats.get('whole_request_savings_ratio', 0.0):.1%}"),
521
+ ("new input savings", f"{stats.get('new_input_savings_ratio', 0.0):.1%}"),
522
+ (
523
+ "provider input/cache/output",
524
+ f"{stats.get('provider_input_tokens', 0)}/"
525
+ f"{stats.get('cache_read_tokens', 0)}/"
526
+ f"{stats.get('request_output_tokens', 0)}",
527
+ ),
528
+ ("original bytes", format_bytes(stats["original_bytes"])),
529
+ ("visible bytes", format_bytes(stats["visible_bytes"])),
530
+ (
531
+ "estimated static token saving",
532
+ str(stats.get("estimated_saved_tokens", 0) or 0),
533
+ ),
534
+ (
535
+ "estimated reused token saving",
536
+ str(stats.get("estimated_reused_tokens", 0) or 0),
537
+ ),
538
+ ("saved", f"{format_bytes(stats['saved_bytes'])} ({stats['savings_ratio']:.1%})"),
539
+ ("retrieval bytes", format_bytes(stats["retrieval_bytes"])),
540
+ ("effective saved", f"{effective_saved} ({effective_ratio})"),
541
+ ("critical retention", f"{stats['critical_retention']:.1%}"),
542
+ ]
543
+ paths = stats.get("execution_paths") or {}
544
+ if paths:
545
+ rows.append(
546
+ (
547
+ "execution paths",
548
+ ", ".join(f"{name}={count}" for name, count in sorted(paths.items())),
549
+ )
550
+ )
551
+ reasons = stats.get("reasons") or {}
552
+ tokens_by_reason = stats.get("tokens_by_reason") or {}
553
+ if reasons:
554
+ rows.append(
555
+ (
556
+ "decision reasons",
557
+ ", ".join(
558
+ f"{name}={count}/~{int(tokens_by_reason.get(name, 0) or 0)}tok"
559
+ for name, count in sorted(
560
+ reasons.items(),
561
+ key=lambda item: int(tokens_by_reason.get(item[0], 0) or 0),
562
+ reverse=True,
563
+ )
564
+ ),
565
+ )
566
+ )
567
+ md = ["## Compression Diagnostics", "", "| Metric | Value |", "|---|---|"]
568
+ md.extend(f"| {markdown_escape(key)} | {markdown_escape(value)} |" for key, value in rows)
569
+ return SlashResult(
570
+ handled=True,
571
+ lines=[f"{key}: {value}" for key, value in rows],
572
+ markdown="\n".join(md),
573
+ )
@@ -0,0 +1,22 @@
1
+ """Shared parsing and presentation helpers for slash-command handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def parts(text: str) -> list[str]:
7
+ return text.strip().split()
8
+
9
+
10
+ def format_bytes(value: int | float) -> str:
11
+ """Format byte counts compactly for command tables."""
12
+ amount = max(0, int(value or 0))
13
+ for unit, size in (("G", 1024**3), ("M", 1024**2), ("K", 1024)):
14
+ if amount >= size:
15
+ rendered = amount / size
16
+ return f"{rendered:.1f}{unit}" if rendered < 10 else f"{rendered:.0f}{unit}"
17
+ return f"{amount}B"
18
+
19
+
20
+ def markdown_escape(text: str) -> str:
21
+ """Escape pipe and backtick for Markdown table cells."""
22
+ return str(text).replace("|", "\\|").replace("`", "\\`")