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,745 @@
1
+ """Request-level compression accounting and provider mutation-safety diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import time
8
+ import uuid
9
+ from typing import Any
10
+
11
+ from langchain.agents.middleware import AgentMiddleware, AgentState
12
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
13
+ from langchain_core.messages.utils import count_tokens_approximately
14
+
15
+ from synapse.runtime.interaction_ledger import begin_model_call
16
+ from synapse.tool_output.models import ModelRequestCompressionEvent
17
+ from synapse.tool_output.repository import ToolOutputRepository
18
+
19
+
20
+ def _runtime_config(request: Any) -> dict[str, Any]:
21
+ """Return the active Runnable config for a model middleware request."""
22
+ runtime = getattr(request, "runtime", None)
23
+ config = getattr(runtime, "config", None)
24
+ try:
25
+ from langgraph.config import get_config
26
+
27
+ active = get_config()
28
+ if active:
29
+ config = active
30
+ except (ImportError, RuntimeError):
31
+ pass
32
+ return dict(config) if isinstance(config, dict) else {}
33
+
34
+
35
+ def _thread_id(request: Any) -> str:
36
+ configurable = _runtime_config(request).get("configurable") or {}
37
+ return str((configurable or {}).get("thread_id") or "")
38
+
39
+
40
+ def _messages(request: Any) -> list[Any]:
41
+ messages = list(getattr(request, "messages", None) or [])
42
+ system = getattr(request, "system_message", None)
43
+ return [system, *messages] if system is not None else messages
44
+
45
+
46
+ def _state_messages(request: Any) -> list[Any]:
47
+ state = getattr(request, "state", None) or {}
48
+ return list(state.get("messages") or []) if isinstance(state, dict) else []
49
+
50
+
51
+ def _count(messages: list[Any], tools: list[Any] | None = None) -> int:
52
+ try:
53
+ return max(0, int(count_tokens_approximately(messages, tools=tools or [])))
54
+ except Exception: # noqa: BLE001
55
+ return max(0, sum((len(str(getattr(msg, "content", msg))) + 3) // 4 for msg in messages))
56
+
57
+
58
+ def _message_content_tokens(message: Any) -> int:
59
+ """Approximate one message's content tokens without role/schema overhead."""
60
+ content = getattr(message, "content", "")
61
+ try:
62
+ if isinstance(content, str):
63
+ return max(0, (len(content) + 3) // 4)
64
+ return max(0, (len(str(content)) + 3) // 4)
65
+ except Exception: # noqa: BLE001
66
+ return 0
67
+
68
+
69
+ def _reasoning_tokens(message: Any) -> int:
70
+ total = 0
71
+ additional = getattr(message, "additional_kwargs", None) or {}
72
+ metadata = getattr(message, "response_metadata", None) or {}
73
+ for source in (additional, metadata):
74
+ if not isinstance(source, dict):
75
+ continue
76
+ for key in ("reasoning_content", "encrypted_content", "thinking"):
77
+ value = source.get(key)
78
+ if value:
79
+ total += max(1, (len(str(value)) + 3) // 4)
80
+ return total
81
+
82
+
83
+ def _tool_call_argument_tokens(message: Any) -> int:
84
+ calls = getattr(message, "tool_calls", None) or []
85
+ total = 0
86
+ for call in calls:
87
+ args = call.get("args") if isinstance(call, dict) else getattr(call, "args", None)
88
+ if args is not None:
89
+ total += max(0, (len(str(args)) + 3) // 4)
90
+ return total
91
+
92
+
93
+ def _tool_schema_tokens(tools: list[Any]) -> int:
94
+ if not tools:
95
+ return 0
96
+ baseline = _count([], tools)
97
+ return max(0, baseline - _count([]))
98
+
99
+
100
+ def _content_breakdown(request: Any) -> dict[str, int]:
101
+ """Classify the final model request into actionable token-source buckets."""
102
+ messages = list(getattr(request, "messages", None) or [])
103
+ system = getattr(request, "system_message", None)
104
+ tools = list(getattr(request, "tools", None) or [])
105
+ breakdown = {
106
+ "system": _count([system]) if system is not None else 0,
107
+ "tool_schemas": _tool_schema_tokens(tools),
108
+ "historical_user": 0,
109
+ "current_user": 0,
110
+ "assistant_content": 0,
111
+ "reasoning": 0,
112
+ "tool_call_arguments": 0,
113
+ "tool_output_visible": 0,
114
+ "tool_output_original": 0,
115
+ "unknown": 0,
116
+ }
117
+ human_indexes = [index for index, msg in enumerate(messages) if isinstance(msg, HumanMessage)]
118
+ latest_human = human_indexes[-1] if human_indexes else -1
119
+ for index, message in enumerate(messages):
120
+ content_tokens = _message_content_tokens(message)
121
+ if isinstance(message, ToolMessage):
122
+ breakdown["tool_output_visible"] += _count([message])
123
+ artifact = getattr(message, "artifact", None) or {}
124
+ transform = (
125
+ artifact.get("tool_output_transform")
126
+ if isinstance(artifact, dict)
127
+ else None
128
+ )
129
+ visible_tokens = _count([message])
130
+ if isinstance(transform, dict):
131
+ explicit_original = int(transform.get("estimated_original_tokens", 0) or 0)
132
+ saved_tokens = int(transform.get("estimated_saved_tokens", 0) or 0)
133
+ breakdown["tool_output_original"] += max(
134
+ explicit_original, visible_tokens + saved_tokens
135
+ )
136
+ else:
137
+ breakdown["tool_output_original"] += visible_tokens
138
+ continue
139
+ if isinstance(message, HumanMessage):
140
+ key = "current_user" if index == latest_human else "historical_user"
141
+ breakdown[key] += content_tokens
142
+ continue
143
+ if isinstance(message, AIMessage):
144
+ reasoning = _reasoning_tokens(message)
145
+ args = _tool_call_argument_tokens(message)
146
+ breakdown["reasoning"] += reasoning
147
+ breakdown["tool_call_arguments"] += args
148
+ breakdown["assistant_content"] += content_tokens
149
+ continue
150
+ breakdown["unknown"] += content_tokens
151
+ classified = sum(
152
+ breakdown[key]
153
+ for key in (
154
+ "system",
155
+ "tool_schemas",
156
+ "historical_user",
157
+ "current_user",
158
+ "assistant_content",
159
+ "reasoning",
160
+ "tool_call_arguments",
161
+ "tool_output_visible",
162
+ "unknown",
163
+ )
164
+ )
165
+ total = _count(([system] if system is not None else []) + messages, tools)
166
+ breakdown["unknown"] += max(0, total - classified)
167
+ return breakdown
168
+
169
+
170
+ def _opportunities(breakdown: dict[str, int]) -> dict[str, int]:
171
+ """Rank unoptimized token sources without prescribing an implementation."""
172
+ opportunities: dict[str, int] = {}
173
+ mappings = {
174
+ "tool_schemas": "tool_schema_fixed_overhead",
175
+ "historical_user": "historical_user_context",
176
+ "current_user": "current_user_not_in_pipeline",
177
+ "assistant_content": "assistant_history_not_in_pipeline",
178
+ "reasoning": "reasoning_not_in_pipeline",
179
+ "tool_call_arguments": "tool_call_arguments_not_in_pipeline",
180
+ }
181
+ for source, reason in mappings.items():
182
+ tokens = max(0, int(breakdown.get(source, 0) or 0))
183
+ if tokens:
184
+ opportunities[reason] = tokens
185
+ original = max(0, int(breakdown.get("tool_output_original", 0) or 0))
186
+ visible = max(0, int(breakdown.get("tool_output_visible", 0) or 0))
187
+ if visible and original <= visible:
188
+ opportunities["uncompressed_tool_outputs"] = visible
189
+ if breakdown.get("unknown", 0):
190
+ opportunities["unknown_request_overhead"] = int(breakdown["unknown"])
191
+ return opportunities
192
+
193
+
194
+ def _tool_output_savings(messages: list[Any]) -> tuple[int, int, int]:
195
+ saved = 0
196
+ candidates = 0
197
+ transformed = 0
198
+ for message in messages:
199
+ artifact = getattr(message, "artifact", None)
200
+ if not isinstance(artifact, dict):
201
+ continue
202
+ transform = artifact.get("tool_output_transform")
203
+ if not isinstance(transform, dict):
204
+ continue
205
+ candidates += 1
206
+ if str(transform.get("decision") or "transformed") == "transformed":
207
+ transformed += 1
208
+ saved += max(0, int(transform.get("estimated_saved_tokens", 0) or 0))
209
+ return saved, candidates, transformed
210
+
211
+
212
+ def _model_identity(request: Any) -> tuple[str, str, str, str]:
213
+ model = getattr(request, "model", None)
214
+ model_name = str(
215
+ getattr(model, "model_name", None)
216
+ or getattr(model, "model", None)
217
+ or getattr(model, "model_id", None)
218
+ or model.__class__.__name__
219
+ )
220
+ class_name = model.__class__.__name__.casefold()
221
+ base_url = str(
222
+ getattr(model, "openai_api_base", None)
223
+ or getattr(model, "base_url", None)
224
+ or ""
225
+ ).casefold()
226
+ if "anthropic" in class_name or "claude" in model_name.casefold():
227
+ return "anthropic", "messages", "payg", model_name
228
+ if "chatgpt.com/backend-api/codex" in base_url or "codex" in model_name.casefold():
229
+ return "openai", "responses", "subscription", model_name
230
+ if "openai" in class_name or "openai" in base_url:
231
+ use_responses = bool(
232
+ getattr(model, "use_responses_api", False)
233
+ or getattr(model, "_use_responses_api", False)
234
+ )
235
+ return "openai", "responses" if use_responses else "chat-completions", "payg", model_name
236
+ return "unknown", "langchain", "unknown", model_name
237
+
238
+
239
+ def _content_has_cache_control(message: Any) -> bool:
240
+ content = getattr(message, "content", None)
241
+ if isinstance(content, list):
242
+ return any(isinstance(block, dict) and block.get("cache_control") for block in content)
243
+ additional = getattr(message, "additional_kwargs", None) or {}
244
+ return bool(isinstance(additional, dict) and additional.get("cache_control"))
245
+
246
+
247
+ def _provider_protected_tokens(request: Any, provider: str, api_style: str) -> dict[str, int]:
248
+ messages = list(getattr(request, "messages", None) or [])
249
+ protected: dict[str, int] = {}
250
+ if provider == "anthropic":
251
+ marker = -1
252
+ for index, message in enumerate(messages):
253
+ if _content_has_cache_control(message):
254
+ marker = index
255
+ if marker >= 0:
256
+ protected["anthropic_before_cache_control"] = _count(messages[: marker + 1])
257
+ return protected
258
+
259
+ tool_indexes = [index for index, msg in enumerate(messages) if isinstance(msg, ToolMessage)]
260
+ if tool_indexes:
261
+ older = [messages[index] for index in tool_indexes[:-1]]
262
+ if older:
263
+ reason = (
264
+ "codex_historical_output"
265
+ if api_style == "responses"
266
+ else "openai_historical_message"
267
+ )
268
+ protected[reason] = _count(older)
269
+ if api_style == "responses":
270
+ reasoning_tokens = 0
271
+ for message in messages:
272
+ additional = getattr(message, "additional_kwargs", None) or {}
273
+ response = getattr(message, "response_metadata", None) or {}
274
+ if not isinstance(additional, dict) or not isinstance(response, dict):
275
+ continue
276
+ reasoning = additional.get("reasoning_content") or response.get("reasoning_content")
277
+ encrypted = additional.get("encrypted_content") or response.get("encrypted_content")
278
+ if reasoning or encrypted:
279
+ reasoning_tokens += max(1, (len(str(reasoning or encrypted)) + 3) // 4)
280
+ if reasoning_tokens:
281
+ protected["codex_reasoning_protected"] = reasoning_tokens
282
+ return protected
283
+
284
+
285
+ def _response_messages(response: Any) -> list[Any]:
286
+ model_response = getattr(response, "model_response", response)
287
+ return list(getattr(model_response, "result", None) or [])
288
+
289
+
290
+ def _usage(response: Any) -> dict[str, int]:
291
+ values = {
292
+ "input_tokens": 0,
293
+ "output_tokens": 0,
294
+ "cache_read_tokens": 0,
295
+ "cache_write_tokens": 0,
296
+ "uncached_input_tokens": 0,
297
+ }
298
+ for message in _response_messages(response):
299
+ usage = getattr(message, "usage_metadata", None) or {}
300
+ metadata = getattr(message, "response_metadata", None) or {}
301
+ token_usage = metadata.get("token_usage") if isinstance(metadata, dict) else {}
302
+ if not isinstance(usage, dict):
303
+ usage = {
304
+ "input_tokens": getattr(usage, "input_tokens", 0),
305
+ "output_tokens": getattr(usage, "output_tokens", 0),
306
+ "input_token_details": getattr(usage, "input_token_details", None),
307
+ }
308
+ values["input_tokens"] += int(usage.get("input_tokens", 0) or 0)
309
+ values["output_tokens"] += int(usage.get("output_tokens", 0) or 0)
310
+ details = usage.get("input_token_details") or {}
311
+ if not isinstance(details, dict):
312
+ details = vars(details) if hasattr(details, "__dict__") else {}
313
+ token_usage = token_usage if isinstance(token_usage, dict) else {}
314
+ for key in ("cache_read", "cache_read_tokens", "cached_tokens"):
315
+ if details.get(key) is not None:
316
+ values["cache_read_tokens"] += int(details.get(key) or 0)
317
+ break
318
+ else:
319
+ values["cache_read_tokens"] += int(
320
+ token_usage.get("cache_read_input_tokens", 0)
321
+ or token_usage.get("cached_tokens", 0)
322
+ or 0
323
+ )
324
+ values["cache_write_tokens"] += int(
325
+ details.get("cache_creation", 0)
326
+ or details.get("cache_write", 0)
327
+ or token_usage.get("cache_creation_input_tokens", 0)
328
+ or 0
329
+ )
330
+ values["uncached_input_tokens"] = max(
331
+ 0,
332
+ values["input_tokens"] - values["cache_read_tokens"] - values["cache_write_tokens"],
333
+ )
334
+ return values
335
+
336
+
337
+ def _stable_json(value: Any) -> str:
338
+ try:
339
+ if hasattr(value, "model_dump"):
340
+ value = value.model_dump()
341
+ elif not isinstance(value, dict | list | tuple | str | int | float | bool | type(None)):
342
+ value = vars(value) if hasattr(value, "__dict__") else str(value)
343
+ return json.dumps(
344
+ value,
345
+ ensure_ascii=False,
346
+ sort_keys=True,
347
+ default=str,
348
+ separators=(",", ":"),
349
+ )
350
+ except Exception: # noqa: BLE001
351
+ return str(value)
352
+
353
+
354
+ def _fingerprint(value: Any) -> str:
355
+ return hashlib.sha256(_stable_json(value).encode("utf-8")).hexdigest()[:16]
356
+
357
+
358
+ def _current_user_content(messages: list[Any]) -> Any:
359
+ for message in reversed(messages):
360
+ if isinstance(message, HumanMessage):
361
+ return getattr(message, "content", "")
362
+ return ""
363
+
364
+
365
+ def _live_zone_plan(request: Any, provider: str, api_style: str) -> list[dict[str, Any]]:
366
+ messages = list(getattr(request, "messages", None) or [])
367
+ latest_user = max(
368
+ (index for index, message in enumerate(messages) if isinstance(message, HumanMessage)),
369
+ default=-1,
370
+ )
371
+ latest_tool = max(
372
+ (index for index, message in enumerate(messages) if isinstance(message, ToolMessage)),
373
+ default=-1,
374
+ )
375
+ plan: list[dict[str, Any]] = []
376
+ for index, message in enumerate(messages):
377
+ tokens = _count([message])
378
+ zone = "frozen"
379
+ reason = "historical_message"
380
+ if isinstance(message, AIMessage) and _reasoning_tokens(message):
381
+ zone, reason = "protected", "reasoning_protected"
382
+ elif provider == "anthropic":
383
+ additional = getattr(message, "additional_kwargs", None) or {}
384
+ if additional.get("cache_control"):
385
+ zone, reason = "frozen", "anthropic_cache_boundary"
386
+ elif index >= latest_user:
387
+ zone, reason = "live", "anthropic_after_latest_user"
388
+ else:
389
+ reason = "anthropic_before_live_zone"
390
+ elif api_style == "responses":
391
+ if isinstance(message, HumanMessage) and index == latest_user:
392
+ zone, reason = "live", "responses_latest_user"
393
+ elif isinstance(message, ToolMessage) and index == latest_tool and index >= latest_user:
394
+ zone, reason = "live", "responses_latest_tool_output"
395
+ else:
396
+ reason = "responses_historical_output"
397
+ else:
398
+ if isinstance(message, HumanMessage) and index == latest_user:
399
+ zone, reason = "live", "openai_latest_user"
400
+ elif isinstance(message, ToolMessage) and index == latest_tool and index >= latest_user:
401
+ zone, reason = "live", "openai_latest_tool_output"
402
+ else:
403
+ reason = "openai_historical_message"
404
+ plan.append(
405
+ {
406
+ "message_index": index,
407
+ "message_type": getattr(message, "type", message.__class__.__name__),
408
+ "zone": zone,
409
+ "reason": reason,
410
+ "estimated_tokens": tokens,
411
+ }
412
+ )
413
+ return plan
414
+
415
+
416
+ def _tool_schema_profiles(tools: list[Any]) -> list[dict[str, Any]]:
417
+ profiles: list[dict[str, Any]] = []
418
+ for index, tool in enumerate(tools):
419
+ raw = _stable_json(tool)
420
+ data = tool.model_dump() if hasattr(tool, "model_dump") else tool
421
+ data = data if isinstance(data, dict) else {}
422
+ function = data.get("function") if isinstance(data.get("function"), dict) else data
423
+ name = str(function.get("name") or getattr(tool, "name", None) or f"tool-{index}")
424
+ description = str(function.get("description") or getattr(tool, "description", None) or "")
425
+ parameters = function.get("parameters") or function.get("args_schema") or {}
426
+ profiles.append(
427
+ {
428
+ "index": index,
429
+ "tool_name": name,
430
+ "schema_bytes": len(raw.encode("utf-8")),
431
+ "estimated_tokens": _count([], [tool]),
432
+ "description_bytes": len(description.encode("utf-8")),
433
+ "parameters_bytes": len(_stable_json(parameters).encode("utf-8")),
434
+ "schema_hash": _fingerprint(tool),
435
+ }
436
+ )
437
+ return profiles
438
+
439
+
440
+ def _wire_fingerprints(request: Any) -> dict[str, Any]:
441
+ system = getattr(request, "system_message", None)
442
+ messages = list(getattr(request, "messages", None) or [])
443
+ tools = list(getattr(request, "tools", None) or [])
444
+ return {
445
+ "system_hash": _fingerprint(system) if system is not None else "",
446
+ "tools_hash": _fingerprint(tools),
447
+ "message_hashes": [_fingerprint(message) for message in messages],
448
+ "message_count": len(messages),
449
+ "tool_count": len(tools),
450
+ "request_prefix_hash": _fingerprint([system, tools, messages]),
451
+ }
452
+
453
+
454
+ def _cache_diagnostics(
455
+ previous: dict[str, Any] | None,
456
+ current: dict[str, Any],
457
+ usage: dict[str, int],
458
+ ) -> dict[str, Any]:
459
+ previous = previous or {}
460
+ previous_hashes = list(previous.get("message_hashes") or [])
461
+ current_hashes = list(current.get("message_hashes") or [])
462
+ first_change = next(
463
+ (
464
+ index
465
+ for index, (before, after) in enumerate(
466
+ zip(previous_hashes, current_hashes, strict=False)
467
+ )
468
+ if before != after
469
+ ),
470
+ min(len(previous_hashes), len(current_hashes)),
471
+ )
472
+ input_tokens = max(0, int(usage.get("input_tokens", 0) or 0))
473
+ cache_read = max(0, int(usage.get("cache_read_tokens", 0) or 0))
474
+ return {
475
+ "previous_request_available": bool(previous),
476
+ "system_changed": bool(
477
+ previous and previous.get("system_hash") != current.get("system_hash")
478
+ ),
479
+ "tools_changed": bool(
480
+ previous and previous.get("tools_hash") != current.get("tools_hash")
481
+ ),
482
+ "first_changed_message_index": first_change if previous else None,
483
+ "cache_hit_ratio": round(cache_read / input_tokens, 4) if input_tokens else 0.0,
484
+ "cache_bust_suspected": bool(
485
+ previous and input_tokens and cache_read / input_tokens < 0.5
486
+ ),
487
+ }
488
+
489
+
490
+ def _read_lifecycle(messages: list[Any], plan: list[dict[str, Any]]) -> list[dict[str, Any]]:
491
+ tool_calls: dict[str, dict[str, Any]] = {}
492
+ for index, message in enumerate(messages):
493
+ if not isinstance(message, AIMessage):
494
+ continue
495
+ for call in getattr(message, "tool_calls", None) or []:
496
+ if not isinstance(call, dict):
497
+ continue
498
+ args = call.get("args") if isinstance(call.get("args"), dict) else {}
499
+ tool_calls[str(call.get("id") or "")] = {
500
+ "message_index": index,
501
+ "tool_name": str(call.get("name") or ""),
502
+ "file_path": str(args.get("file_path") or args.get("path") or ""),
503
+ "offset": args.get("offset"),
504
+ "limit": args.get("limit"),
505
+ }
506
+ operations: list[dict[str, Any]] = []
507
+ for index, message in enumerate(messages):
508
+ if not isinstance(message, ToolMessage):
509
+ continue
510
+ call_id = str(getattr(message, "tool_call_id", None) or "")
511
+ call = tool_calls.get(call_id) or {}
512
+ name = str(call.get("tool_name") or getattr(message, "name", None) or "")
513
+ if name not in {"read_file", "edit_file", "write_file"}:
514
+ continue
515
+ operations.append({"message_index": index, "tool_call_id": call_id, **call})
516
+ result: list[dict[str, Any]] = []
517
+ for operation in operations:
518
+ if operation.get("tool_name") != "read_file" or not operation.get("file_path"):
519
+ continue
520
+ later = [
521
+ item
522
+ for item in operations
523
+ if item["message_index"] > operation["message_index"]
524
+ and item.get("file_path") == operation.get("file_path")
525
+ ]
526
+ state = "fresh"
527
+ if any(item.get("tool_name") in {"edit_file", "write_file"} for item in later):
528
+ state = "stale"
529
+ elif any(item.get("tool_name") == "read_file" for item in later):
530
+ state = "superseded"
531
+ zone = next(
532
+ (
533
+ item["zone"]
534
+ for item in plan
535
+ if item["message_index"] == operation["message_index"]
536
+ ),
537
+ "frozen",
538
+ )
539
+ result.append(
540
+ {
541
+ **operation,
542
+ "state": state,
543
+ "zone": zone,
544
+ "replaceable": state in {"stale", "superseded"} and zone == "live",
545
+ }
546
+ )
547
+ return result
548
+
549
+
550
+ def _apply_read_lifecycle(
551
+ request: Any,
552
+ lifecycle: list[dict[str, Any]],
553
+ repository: ToolOutputRepository,
554
+ thread_id: str,
555
+ ) -> tuple[Any, list[dict[str, Any]]]:
556
+ replaceable = {
557
+ int(item["message_index"]): item for item in lifecycle if item.get("replaceable")
558
+ }
559
+ if not replaceable:
560
+ return request, lifecycle
561
+ messages = list(getattr(request, "messages", None) or [])
562
+ updated = list(messages)
563
+ for index, item in replaceable.items():
564
+ if index >= len(messages) or not isinstance(messages[index], ToolMessage):
565
+ continue
566
+ message = messages[index]
567
+ original = str(getattr(message, "content", "") or "")
568
+ record = repository.put(
569
+ thread_id=thread_id,
570
+ checkpoint_ns="read-lifecycle",
571
+ tool_call_id=str(item.get("tool_call_id") or ""),
572
+ tool_name="read_file",
573
+ status="success",
574
+ content=original,
575
+ )
576
+ state = str(item.get("state") or "stale")
577
+ path = str(item.get("file_path") or "unknown")
578
+ marker = (
579
+ f"[read_file {state}: {path}; re-read for current content if needed. "
580
+ f"Original: {record.ref}]"
581
+ )
582
+ if hasattr(message, "model_copy"):
583
+ updated[index] = message.model_copy(update={"content": marker})
584
+ else:
585
+ updated[index] = ToolMessage(
586
+ content=marker,
587
+ tool_call_id=str(getattr(message, "tool_call_id", None) or ""),
588
+ name=getattr(message, "name", None),
589
+ )
590
+ item["replacement_ref"] = record.ref
591
+ item["replacement_bytes_before"] = len(original.encode("utf-8"))
592
+ item["replacement_bytes_after"] = len(marker.encode("utf-8"))
593
+ if updated == messages or not hasattr(request, "override"):
594
+ return request, lifecycle
595
+ return request.override(messages=updated), lifecycle
596
+
597
+
598
+ def build_model_request_compression_middleware(repository: ToolOutputRepository) -> Any:
599
+ """Record final model-visible request size, reconstructed baseline, and usage."""
600
+
601
+ class _ModelRequestCompressionMiddleware(AgentMiddleware):
602
+ state_schema = AgentState
603
+ tools: list[Any] = []
604
+
605
+ def __init__(self) -> None:
606
+ self._previous_wire: dict[str, dict[str, Any]] = {}
607
+
608
+ def _prepare(self, request: Any) -> dict[str, Any]:
609
+ started = time.perf_counter()
610
+ request_messages = _messages(request)
611
+ tools = list(getattr(request, "tools", None) or [])
612
+ input_after = _count(request_messages, tools)
613
+ state_messages = _state_messages(request)
614
+ state_count = _count(state_messages)
615
+ tool_saved, candidates, transformed = _tool_output_savings(request_messages)
616
+ active_messages = list(getattr(request, "messages", None) or [])
617
+ summarization_saved = max(0, state_count - _count(active_messages))
618
+ provider, api_style, auth_mode, model = _model_identity(request)
619
+ thread_id = _thread_id(request)
620
+ turn_index_hint = sum(isinstance(message, HumanMessage) for message in active_messages)
621
+ previous_turn, previous_call = repository.latest_request_position(thread_id=thread_id)
622
+ position = begin_model_call(
623
+ thread_id,
624
+ _current_user_content(active_messages),
625
+ turn_index_hint=turn_index_hint,
626
+ model_call_index_hint=previous_call if previous_turn == turn_index_hint else 0,
627
+ )
628
+ live_zone_plan = _live_zone_plan(request, provider, api_style)
629
+ wire_fingerprints = _wire_fingerprints(request)
630
+ schema_profiles = _tool_schema_profiles(tools)
631
+ read_lifecycle = _read_lifecycle(active_messages, live_zone_plan)
632
+ request, read_lifecycle = _apply_read_lifecycle(
633
+ request, read_lifecycle, repository, thread_id
634
+ )
635
+ request_messages = _messages(request)
636
+ tools = list(getattr(request, "tools", None) or [])
637
+ input_after = _count(request_messages, tools)
638
+ tool_saved, candidates, transformed = _tool_output_savings(request_messages)
639
+ active_messages = list(getattr(request, "messages", None) or [])
640
+ breakdown = _content_breakdown(request)
641
+ wire_fingerprints = _wire_fingerprints(request)
642
+ schema_profiles = _tool_schema_profiles(tools)
643
+ live_zone_tokens: dict[str, int] = {}
644
+ for item in live_zone_plan:
645
+ zone = str(item["zone"])
646
+ live_zone_tokens[zone] = live_zone_tokens.get(zone, 0) + int(
647
+ item["estimated_tokens"] or 0
648
+ )
649
+ protected = _provider_protected_tokens(request, provider, api_style)
650
+ breakdown = _content_breakdown(request)
651
+ from synapse.runtime.middleware import current_prompt_cleanup_saved_tokens
652
+
653
+ prompt_saved = current_prompt_cleanup_saved_tokens()
654
+ return {
655
+ "prepared_request": request,
656
+ "started": started,
657
+ "request_id": uuid.uuid4().hex,
658
+ "thread_id": thread_id,
659
+ "turn_id": position.turn_id,
660
+ "turn_index": position.turn_index,
661
+ "model_call_index": position.model_call_index,
662
+ "provider": provider,
663
+ "api_style": api_style,
664
+ "auth_mode": auth_mode,
665
+ "model": model,
666
+ "input_after": input_after,
667
+ "tool_saved": tool_saved,
668
+ "prompt_saved": prompt_saved,
669
+ "summarization_saved": summarization_saved,
670
+ "candidate_blocks": candidates,
671
+ "transformed_blocks": transformed,
672
+ "protected": protected,
673
+ "breakdown": breakdown,
674
+ "opportunities": _opportunities(breakdown),
675
+ "live_zone_plan": live_zone_plan,
676
+ "live_zone_tokens": live_zone_tokens,
677
+ "wire_fingerprints": wire_fingerprints,
678
+ "schema_profiles": schema_profiles,
679
+ "read_lifecycle": read_lifecycle,
680
+ }
681
+
682
+ def _finish(self, data: dict[str, Any], response: Any) -> None:
683
+ thread_id = str(data["thread_id"] or "")
684
+ if not thread_id:
685
+ return
686
+ usage = _usage(response)
687
+ tool_saved = int(data["tool_saved"] or 0)
688
+ prompt_saved = int(data["prompt_saved"] or 0)
689
+ summarization_saved = int(data["summarization_saved"] or 0)
690
+ total_saved = tool_saved + prompt_saved + summarization_saved
691
+ wire = dict(data["wire_fingerprints"] or {})
692
+ cache_diagnostics = _cache_diagnostics(
693
+ self._previous_wire.get(thread_id), wire, usage
694
+ )
695
+ cache_diagnostics["read_lifecycle"] = list(data["read_lifecycle"] or [])
696
+ self._previous_wire[thread_id] = wire
697
+ repository.record_model_request(
698
+ thread_id=thread_id,
699
+ event=ModelRequestCompressionEvent(
700
+ request_id=str(data["request_id"]),
701
+ provider=str(data["provider"]),
702
+ api_style=str(data["api_style"]),
703
+ auth_mode=str(data["auth_mode"]),
704
+ model=str(data["model"]),
705
+ input_tokens_before=int(data["input_after"] or 0) + total_saved,
706
+ input_tokens_after=int(data["input_after"] or 0),
707
+ provider_input_tokens=usage["input_tokens"],
708
+ cache_read_tokens=usage["cache_read_tokens"],
709
+ cache_write_tokens=usage["cache_write_tokens"],
710
+ uncached_input_tokens=usage["uncached_input_tokens"],
711
+ output_tokens=usage["output_tokens"],
712
+ tool_output_saved_tokens=tool_saved,
713
+ prompt_saved_tokens=prompt_saved,
714
+ summarization_saved_tokens=summarization_saved,
715
+ total_saved_tokens=total_saved,
716
+ candidate_blocks=int(data["candidate_blocks"] or 0),
717
+ transformed_blocks=int(data["transformed_blocks"] or 0),
718
+ protected_tokens_by_reason=dict(data["protected"] or {}),
719
+ content_breakdown=dict(data["breakdown"] or {}),
720
+ opportunity_tokens_by_reason=dict(data["opportunities"] or {}),
721
+ turn_id=str(data["turn_id"] or ""),
722
+ turn_index=int(data["turn_index"] or 0),
723
+ model_call_index=int(data["model_call_index"] or 0),
724
+ live_zone_plan=list(data["live_zone_plan"] or []),
725
+ live_zone_tokens=dict(data["live_zone_tokens"] or {}),
726
+ wire_fingerprints=wire,
727
+ cache_diagnostics=cache_diagnostics,
728
+ tool_schema_profiles=list(data["schema_profiles"] or []),
729
+ duration_ms=(time.perf_counter() - float(data["started"])) * 1000,
730
+ ),
731
+ )
732
+
733
+ def wrap_model_call(self, request: Any, handler: Any) -> Any:
734
+ data = self._prepare(request)
735
+ response = handler(data["prepared_request"])
736
+ self._finish(data, response)
737
+ return response
738
+
739
+ async def awrap_model_call(self, request: Any, handler: Any) -> Any:
740
+ data = self._prepare(request)
741
+ response = await handler(data["prepared_request"])
742
+ self._finish(data, response)
743
+ return response
744
+
745
+ return _ModelRequestCompressionMiddleware()