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.
- synapse/__init__.py +13 -0
- synapse/__main__.py +6 -0
- synapse/app/__init__.py +1 -0
- synapse/app/agent.py +492 -0
- synapse/app/agent_md.py +107 -0
- synapse/cli.py +750 -0
- synapse/commands/__init__.py +1 -0
- synapse/commands/compression.py +573 -0
- synapse/commands/helpers.py +22 -0
- synapse/commands/mcp.py +406 -0
- synapse/commands/model.py +173 -0
- synapse/commands/result.py +34 -0
- synapse/commands/sessions.py +443 -0
- synapse/commands/slash_cmds.py +521 -0
- synapse/commands/slash_complete.py +816 -0
- synapse/commands/theme.py +99 -0
- synapse/config.py +27 -0
- synapse/content/__init__.py +1 -0
- synapse/content/input_history.py +122 -0
- synapse/content/multimodal.py +733 -0
- synapse/content/prompts.py +249 -0
- synapse/content/skills_catalog.py +128 -0
- synapse/integrations/__init__.py +1 -0
- synapse/integrations/checkpoint_seed.py +281 -0
- synapse/integrations/codex_history.py +375 -0
- synapse/integrations/codex_import.py +393 -0
- synapse/integrations/codex_sessions.py +629 -0
- synapse/integrations/describe_image.py +370 -0
- synapse/integrations/http_clients.py +199 -0
- synapse/integrations/llm_openai_compat.py +90 -0
- synapse/integrations/llm_openai_websocket.py +187 -0
- synapse/integrations/mcp_client.py +646 -0
- synapse/integrations/vision_middleware.py +62 -0
- synapse/models/__init__.py +5 -0
- synapse/models/config.py +240 -0
- synapse/models/helpers.py +206 -0
- synapse/models/profile.py +59 -0
- synapse/models/registry.py +722 -0
- synapse/models_registry.py +7 -0
- synapse/observability/__init__.py +1 -0
- synapse/observability/startup_trace.py +127 -0
- synapse/runtime/__init__.py +1 -0
- synapse/runtime/async_runtime.py +176 -0
- synapse/runtime/backends.py +458 -0
- synapse/runtime/context_compact.py +249 -0
- synapse/runtime/execute_capture.py +48 -0
- synapse/runtime/fs_permissions.py +79 -0
- synapse/runtime/harness.py +57 -0
- synapse/runtime/hitl.py +197 -0
- synapse/runtime/interaction_ledger.py +82 -0
- synapse/runtime/middleware.py +802 -0
- synapse/runtime/model_request_compression_middleware.py +745 -0
- synapse/runtime/pathing.py +146 -0
- synapse/runtime/safety.py +184 -0
- synapse/runtime/steer.py +240 -0
- synapse/runtime/subagents.py +207 -0
- synapse/runtime/tool_ignore.py +221 -0
- synapse/runtime/tool_output_eval.py +118 -0
- synapse/runtime/tool_output_middleware.py +585 -0
- synapse/runtime/tool_output_usage_middleware.py +60 -0
- synapse/sessions/__init__.py +31 -0
- synapse/sessions/cancel_repair.py +208 -0
- synapse/sessions/session_recap.py +174 -0
- synapse/sessions/store.py +695 -0
- synapse/sessions/transcript.py +754 -0
- synapse/settings/__init__.py +5 -0
- synapse/settings/config_paths.py +184 -0
- synapse/settings/schema.py +464 -0
- synapse/tool_output/__init__.py +59 -0
- synapse/tool_output/detection.py +170 -0
- synapse/tool_output/metrics.py +32 -0
- synapse/tool_output/models.py +173 -0
- synapse/tool_output/pipeline.py +330 -0
- synapse/tool_output/repository.py +721 -0
- synapse/tool_output/transformers.py +648 -0
- synapse/tools/__init__.py +5 -0
- synapse/tools/session_tools.py +204 -0
- synapse/ui/__init__.py +10 -0
- synapse/ui/bottombar/__init__.py +73 -0
- synapse/ui/bottombar/components/__init__.py +143 -0
- synapse/ui/bottombar/components/key_hints.py +30 -0
- synapse/ui/bottombar/components/mcp.py +64 -0
- synapse/ui/bottombar/components/mode.py +24 -0
- synapse/ui/bottombar/components/model.py +28 -0
- synapse/ui/bottombar/components/thread.py +29 -0
- synapse/ui/bottombar/context.py +36 -0
- synapse/ui/bottombar/core.py +74 -0
- synapse/ui/dialogs/__init__.py +25 -0
- synapse/ui/dialogs/base.py +362 -0
- synapse/ui/dialogs/codex_session_list.py +84 -0
- synapse/ui/dialogs/compression_diagnostics.py +210 -0
- synapse/ui/dialogs/git_explore.py +702 -0
- synapse/ui/dialogs/mcp_panel.py +407 -0
- synapse/ui/dialogs/model_picker.py +128 -0
- synapse/ui/dialogs/safety_panel.py +63 -0
- synapse/ui/dialogs/session_list.py +98 -0
- synapse/ui/dialogs/theme_designer.py +863 -0
- synapse/ui/dialogs/theme_picker.py +113 -0
- synapse/ui/git_explore/__init__.py +31 -0
- synapse/ui/git_explore/engine.py +82 -0
- synapse/ui/git_explore/provider.py +242 -0
- synapse/ui/git_explore/unified.py +85 -0
- synapse/ui/rendering.py +350 -0
- synapse/ui/sink.py +70 -0
- synapse/ui/steer_widget.py +367 -0
- synapse/ui/stream.py +1207 -0
- synapse/ui/stream_events.py +421 -0
- synapse/ui/stream_runtime.py +252 -0
- synapse/ui/theme.py +1154 -0
- synapse/ui/timeline.py +621 -0
- synapse/ui/topbar/__init__.py +97 -0
- synapse/ui/topbar/components/__init__.py +150 -0
- synapse/ui/topbar/components/branch.py +41 -0
- synapse/ui/topbar/components/title.py +24 -0
- synapse/ui/topbar/components/tool_output.py +24 -0
- synapse/ui/topbar/components/usage.py +24 -0
- synapse/ui/topbar/components/workspace.py +32 -0
- synapse/ui/topbar/context.py +32 -0
- synapse/ui/topbar/core.py +979 -0
- synapse/ui/topbar/git_changes_popover.py +178 -0
- synapse/ui/topbar/git_chrome.py +475 -0
- synapse/ui/topbar/tool_output_popover.py +84 -0
- synapse/ui/topbar/widget.py +474 -0
- synapse/ui/tui.py +5717 -0
- synapse/ui/turn_rail.py +71 -0
- synapse/ui/user_turn.py +83 -0
- synapse/ui/welcome.py +261 -0
- synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
- synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
- synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
- synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""LangChain middleware that rewrites large tool outputs through tool_output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
12
|
+
from langchain_core.messages import ToolMessage
|
|
13
|
+
from langgraph.types import Command
|
|
14
|
+
|
|
15
|
+
from synapse.runtime.execute_capture import begin_execute_capture, end_execute_capture
|
|
16
|
+
from synapse.runtime.interaction_ledger import current_position
|
|
17
|
+
from synapse.tool_output.models import CompressionStageEvent, TransformContext, TransformEvent
|
|
18
|
+
from synapse.tool_output.pipeline import ToolOutputTransformPipeline
|
|
19
|
+
from synapse.tool_output.repository import ToolOutputRepository, content_to_text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _estimate_tokens(content: str) -> int:
|
|
23
|
+
"""Estimate model-visible tokens using the same approximation as compaction."""
|
|
24
|
+
try:
|
|
25
|
+
from langchain_core.messages import ToolMessage
|
|
26
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
27
|
+
|
|
28
|
+
return max(
|
|
29
|
+
0,
|
|
30
|
+
int(
|
|
31
|
+
count_tokens_approximately(
|
|
32
|
+
[ToolMessage(content=content, tool_call_id="estimate", name="tool")]
|
|
33
|
+
)
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
except Exception: # noqa: BLE001
|
|
37
|
+
# Conservative fallback for environments without LangChain utilities.
|
|
38
|
+
return max(0, (len(content) + 3) // 4)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_tool_output_transform_middleware(
|
|
43
|
+
repository: ToolOutputRepository,
|
|
44
|
+
*,
|
|
45
|
+
threshold_bytes: int = 512,
|
|
46
|
+
pipeline: ToolOutputTransformPipeline | None = None,
|
|
47
|
+
enabled: bool = True,
|
|
48
|
+
):
|
|
49
|
+
"""Rewrite large outputs once, preserving originals only when needed.
|
|
50
|
+
|
|
51
|
+
The middleware owns all result rewriting for regular, async, and Command
|
|
52
|
+
result paths. ``execute_capture`` remains in use so an already-truncated
|
|
53
|
+
backend message can be transformed from its complete captured output.
|
|
54
|
+
"""
|
|
55
|
+
threshold = max(0, int(threshold_bytes))
|
|
56
|
+
pipeline = pipeline or ToolOutputTransformPipeline()
|
|
57
|
+
excluded = frozenset({"read_tool_result", "compact_conversation"})
|
|
58
|
+
protected_source_reads: set[tuple[str, str, str, int, int]] = set()
|
|
59
|
+
protected_source_reads_lock = threading.Lock()
|
|
60
|
+
|
|
61
|
+
def runtime_identity(request: Any) -> tuple[str, str]:
|
|
62
|
+
config = dict(getattr(request.runtime, "config", None) or {})
|
|
63
|
+
try:
|
|
64
|
+
from langchain_core.runnables.config import get_config
|
|
65
|
+
|
|
66
|
+
active = get_config()
|
|
67
|
+
if active:
|
|
68
|
+
config = dict(active)
|
|
69
|
+
except (RuntimeError, ImportError):
|
|
70
|
+
pass
|
|
71
|
+
configurable = dict(config.get("configurable") or {})
|
|
72
|
+
return (
|
|
73
|
+
str(configurable.get("thread_id") or "unknown-thread"),
|
|
74
|
+
str(configurable.get("checkpoint_ns") or ""),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def call_value(request: Any, key: str, default: str = "") -> str:
|
|
78
|
+
call = getattr(request, "tool_call", None)
|
|
79
|
+
if isinstance(call, dict):
|
|
80
|
+
return str(call.get(key) or default)
|
|
81
|
+
return str(getattr(call, key, None) or default)
|
|
82
|
+
|
|
83
|
+
def call_args(request: Any) -> dict[str, Any]:
|
|
84
|
+
call = getattr(request, "tool_call", None)
|
|
85
|
+
value = call.get("args") if isinstance(call, dict) else getattr(call, "args", None)
|
|
86
|
+
return dict(value) if isinstance(value, dict) else {}
|
|
87
|
+
|
|
88
|
+
def summarized_args(args: dict[str, Any]) -> dict[str, Any]:
|
|
89
|
+
summary: dict[str, Any] = {}
|
|
90
|
+
for key, value in args.items():
|
|
91
|
+
if key in {"content", "new_string", "old_string"}:
|
|
92
|
+
text = str(value or "")
|
|
93
|
+
summary[key] = {
|
|
94
|
+
"bytes": len(text.encode("utf-8")),
|
|
95
|
+
"sha256": hashlib.sha256(text.encode("utf-8")).hexdigest()[:16],
|
|
96
|
+
}
|
|
97
|
+
elif isinstance(value, str):
|
|
98
|
+
summary[key] = value if len(value) <= 500 else value[:500] + "..."
|
|
99
|
+
elif isinstance(value, int | float | bool | type(None)):
|
|
100
|
+
summary[key] = value
|
|
101
|
+
else:
|
|
102
|
+
summary[key] = str(value)[:500]
|
|
103
|
+
return summary
|
|
104
|
+
|
|
105
|
+
def current_query(request: Any) -> str:
|
|
106
|
+
"""Best-effort latest human text without changing graph state."""
|
|
107
|
+
runtime = getattr(request, "runtime", None)
|
|
108
|
+
state = getattr(runtime, "state", None)
|
|
109
|
+
messages = state.get("messages") if isinstance(state, dict) else None
|
|
110
|
+
if not isinstance(messages, list):
|
|
111
|
+
return ""
|
|
112
|
+
for message in reversed(messages):
|
|
113
|
+
role = getattr(message, "type", None) or getattr(message, "role", None)
|
|
114
|
+
if role not in {"human", "user"}:
|
|
115
|
+
continue
|
|
116
|
+
content = content_to_text(getattr(message, "content", ""))
|
|
117
|
+
if content.strip():
|
|
118
|
+
return content
|
|
119
|
+
return ""
|
|
120
|
+
|
|
121
|
+
def rewrite_message(
|
|
122
|
+
request: Any,
|
|
123
|
+
message: ToolMessage,
|
|
124
|
+
*,
|
|
125
|
+
original_content: str | None = None,
|
|
126
|
+
execute_output_truncated: bool = False,
|
|
127
|
+
) -> ToolMessage:
|
|
128
|
+
name = str(message.name or call_value(request, "name", "tool"))
|
|
129
|
+
if name in excluded:
|
|
130
|
+
return message
|
|
131
|
+
original = (
|
|
132
|
+
original_content if original_content is not None else content_to_text(message.content)
|
|
133
|
+
)
|
|
134
|
+
original_bytes = len(original.encode("utf-8"))
|
|
135
|
+
original_tokens = _estimate_tokens(original)
|
|
136
|
+
status = str(message.status or "success")
|
|
137
|
+
thread_id, checkpoint_ns = runtime_identity(request)
|
|
138
|
+
tool_call_id = str(message.tool_call_id or call_value(request, "id"))
|
|
139
|
+
message_id = str(getattr(message, "id", None) or "")
|
|
140
|
+
content_sha256 = hashlib.sha256(original.encode("utf-8")).hexdigest()
|
|
141
|
+
started = time.perf_counter()
|
|
142
|
+
|
|
143
|
+
def record_decision(
|
|
144
|
+
*,
|
|
145
|
+
decision: str,
|
|
146
|
+
reason_code: str,
|
|
147
|
+
content_type: str = "unknown",
|
|
148
|
+
transformer: str = "none",
|
|
149
|
+
visible_content: str | None = None,
|
|
150
|
+
algorithm_output: str | None = None,
|
|
151
|
+
eligible: bool = False,
|
|
152
|
+
ref: str | None = None,
|
|
153
|
+
critical_total: int = 0,
|
|
154
|
+
critical_retained: int = 0,
|
|
155
|
+
execution_path: str = "not_run",
|
|
156
|
+
confidence: float = 0.0,
|
|
157
|
+
reason_detail: str = "",
|
|
158
|
+
stages: tuple[CompressionStageEvent, ...] = (),
|
|
159
|
+
) -> TransformEvent:
|
|
160
|
+
visible = original if visible_content is None else visible_content
|
|
161
|
+
algorithm = visible if algorithm_output is None else algorithm_output
|
|
162
|
+
event = TransformEvent(
|
|
163
|
+
content_type=content_type,
|
|
164
|
+
transformer=transformer,
|
|
165
|
+
outcome="transformed" if decision == "transformed" else "passthrough",
|
|
166
|
+
original_bytes=original_bytes,
|
|
167
|
+
visible_bytes=len(visible.encode("utf-8")),
|
|
168
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
169
|
+
critical_total=critical_total,
|
|
170
|
+
critical_retained=critical_retained,
|
|
171
|
+
ref_created=bool(ref),
|
|
172
|
+
execution_path=execution_path,
|
|
173
|
+
estimated_original_tokens=original_tokens,
|
|
174
|
+
estimated_visible_tokens=_estimate_tokens(visible),
|
|
175
|
+
decision=decision,
|
|
176
|
+
reason_code=reason_code,
|
|
177
|
+
reason_detail=reason_detail,
|
|
178
|
+
eligible=eligible,
|
|
179
|
+
detection_confidence=confidence,
|
|
180
|
+
threshold_bytes=threshold,
|
|
181
|
+
tool_call_id=tool_call_id,
|
|
182
|
+
tool_name=name,
|
|
183
|
+
status=status,
|
|
184
|
+
checkpoint_ns=checkpoint_ns,
|
|
185
|
+
message_id=message_id,
|
|
186
|
+
algorithm_output_bytes=len(algorithm.encode("utf-8")),
|
|
187
|
+
algorithm_output_tokens=_estimate_tokens(algorithm),
|
|
188
|
+
content_sha256=content_sha256,
|
|
189
|
+
stages=stages,
|
|
190
|
+
)
|
|
191
|
+
repository.record_event(thread_id, event, ref=ref)
|
|
192
|
+
return event
|
|
193
|
+
|
|
194
|
+
if not enabled:
|
|
195
|
+
record_decision(
|
|
196
|
+
decision="skipped",
|
|
197
|
+
reason_code="global_disabled",
|
|
198
|
+
reason_detail="tool-output transformation is disabled by configuration",
|
|
199
|
+
stages=(
|
|
200
|
+
CompressionStageEvent(
|
|
201
|
+
phase="eligibility",
|
|
202
|
+
algorithm="feature-flag-policy",
|
|
203
|
+
applied=False,
|
|
204
|
+
reason_code="global_disabled",
|
|
205
|
+
input_bytes=original_bytes,
|
|
206
|
+
output_bytes=original_bytes,
|
|
207
|
+
input_tokens=original_tokens,
|
|
208
|
+
output_tokens=original_tokens,
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
return message
|
|
213
|
+
compress_error_output = status == "error" and original_bytes > threshold
|
|
214
|
+
if status == "error" and not compress_error_output:
|
|
215
|
+
record_decision(
|
|
216
|
+
decision="skipped",
|
|
217
|
+
reason_code="error_output_protected",
|
|
218
|
+
reason_detail="small failed tool result remains intact for diagnostics",
|
|
219
|
+
stages=(
|
|
220
|
+
CompressionStageEvent(
|
|
221
|
+
phase="eligibility",
|
|
222
|
+
algorithm="tool-status-policy",
|
|
223
|
+
applied=False,
|
|
224
|
+
reason_code="error_output_protected",
|
|
225
|
+
input_bytes=original_bytes,
|
|
226
|
+
output_bytes=original_bytes,
|
|
227
|
+
input_tokens=original_tokens,
|
|
228
|
+
output_tokens=original_tokens,
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
return message
|
|
233
|
+
if original_bytes <= threshold and not execute_output_truncated:
|
|
234
|
+
record_decision(
|
|
235
|
+
decision="skipped",
|
|
236
|
+
reason_code="below_threshold",
|
|
237
|
+
reason_detail=f"{original_bytes} bytes <= {threshold} byte threshold",
|
|
238
|
+
stages=(
|
|
239
|
+
CompressionStageEvent(
|
|
240
|
+
phase="eligibility",
|
|
241
|
+
algorithm="byte-threshold-v1",
|
|
242
|
+
applied=False,
|
|
243
|
+
reason_code="below_threshold",
|
|
244
|
+
input_bytes=original_bytes,
|
|
245
|
+
output_bytes=original_bytes,
|
|
246
|
+
input_tokens=original_tokens,
|
|
247
|
+
output_tokens=original_tokens,
|
|
248
|
+
metadata={"threshold_bytes": threshold},
|
|
249
|
+
),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
return message
|
|
253
|
+
|
|
254
|
+
args = call_args(request)
|
|
255
|
+
file_path = str(args.get("file_path") or args.get("path") or "")
|
|
256
|
+
file_suffix = Path(file_path).suffix.casefold() if file_path else ""
|
|
257
|
+
source_read_key: tuple[str, str, str, int, int] | None = None
|
|
258
|
+
if name == "read_file" and file_path and thread_id != "unknown-thread":
|
|
259
|
+
try:
|
|
260
|
+
offset = max(0, int(args.get("offset", 0) or 0))
|
|
261
|
+
except (TypeError, ValueError):
|
|
262
|
+
offset = 0
|
|
263
|
+
try:
|
|
264
|
+
limit = max(0, int(args.get("limit", 0) or 0))
|
|
265
|
+
except (TypeError, ValueError):
|
|
266
|
+
limit = 0
|
|
267
|
+
source_read_key = (thread_id, checkpoint_ns, file_path, offset, limit)
|
|
268
|
+
with protected_source_reads_lock:
|
|
269
|
+
fresh_read_source = source_read_key not in protected_source_reads
|
|
270
|
+
try:
|
|
271
|
+
transformed = pipeline.transform(
|
|
272
|
+
original,
|
|
273
|
+
TransformContext(
|
|
274
|
+
tool_name=name,
|
|
275
|
+
status=status,
|
|
276
|
+
query=current_query(request),
|
|
277
|
+
tool_args=args,
|
|
278
|
+
file_path=file_path,
|
|
279
|
+
file_suffix=file_suffix,
|
|
280
|
+
fresh_read_source=fresh_read_source,
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
if (
|
|
284
|
+
source_read_key is not None
|
|
285
|
+
and transformed.metadata.get("fallback") == "fresh_read_source_protected"
|
|
286
|
+
):
|
|
287
|
+
with protected_source_reads_lock:
|
|
288
|
+
protected_source_reads.add(source_read_key)
|
|
289
|
+
except Exception as exc: # noqa: BLE001
|
|
290
|
+
record_decision(
|
|
291
|
+
decision="fallback",
|
|
292
|
+
reason_code="transform_error",
|
|
293
|
+
eligible=True,
|
|
294
|
+
reason_detail=type(exc).__name__,
|
|
295
|
+
stages=(
|
|
296
|
+
CompressionStageEvent(
|
|
297
|
+
phase="transform",
|
|
298
|
+
algorithm="tool-output-pipeline",
|
|
299
|
+
applied=False,
|
|
300
|
+
reason_code="transform_error",
|
|
301
|
+
input_bytes=original_bytes,
|
|
302
|
+
output_bytes=original_bytes,
|
|
303
|
+
input_tokens=original_tokens,
|
|
304
|
+
output_tokens=original_tokens,
|
|
305
|
+
metadata={"error": type(exc).__name__},
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
return message
|
|
310
|
+
confidence = float(transformed.metadata.get("detection_confidence", 0.0) or 0.0)
|
|
311
|
+
execution_path = str(transformed.metadata.get("execution_path", "python_only"))
|
|
312
|
+
fallback_reason = str(transformed.metadata.get("fallback") or "no_byte_savings")
|
|
313
|
+
if transformed.content == original:
|
|
314
|
+
skipped_reasons = {"disabled", "fresh_read_source_protected"}
|
|
315
|
+
decision = "skipped" if fallback_reason in skipped_reasons else "fallback"
|
|
316
|
+
reason_code = (
|
|
317
|
+
"disabled_content_type" if fallback_reason == "disabled" else fallback_reason
|
|
318
|
+
)
|
|
319
|
+
record_decision(
|
|
320
|
+
decision=decision,
|
|
321
|
+
reason_code=reason_code,
|
|
322
|
+
content_type=transformed.content_type.value,
|
|
323
|
+
transformer=transformed.transformer,
|
|
324
|
+
eligible=decision == "fallback",
|
|
325
|
+
critical_total=transformed.critical_total,
|
|
326
|
+
critical_retained=transformed.critical_retained,
|
|
327
|
+
execution_path=execution_path,
|
|
328
|
+
confidence=confidence,
|
|
329
|
+
stages=transformed.stages,
|
|
330
|
+
)
|
|
331
|
+
return message
|
|
332
|
+
|
|
333
|
+
algorithm_bytes = len(transformed.content.encode("utf-8"))
|
|
334
|
+
provisional_ref = "tool-output://" + ("0" * 32)
|
|
335
|
+
envelope_template = (
|
|
336
|
+
"[tool output transformed]\n"
|
|
337
|
+
f"tool: {name}\n"
|
|
338
|
+
f"type: {transformed.content_type.value}\n"
|
|
339
|
+
f"transformer: {transformed.transformer}\n"
|
|
340
|
+
f"ref: {provisional_ref}\n"
|
|
341
|
+
f"original_bytes: {original_bytes}\n"
|
|
342
|
+
f"visible_bytes: {algorithm_bytes}\n"
|
|
343
|
+
f"content:\n{transformed.content}\n\n"
|
|
344
|
+
"Use read_tool_result(ref=..., query=...) for targeted retrieval, "
|
|
345
|
+
"or offset/limit for exact lines."
|
|
346
|
+
)
|
|
347
|
+
envelope_tokens = _estimate_tokens(envelope_template)
|
|
348
|
+
saved_tokens = max(0, original_tokens - envelope_tokens)
|
|
349
|
+
savings_ratio = saved_tokens / original_tokens if original_tokens else 0.0
|
|
350
|
+
diff_effective = (
|
|
351
|
+
transformed.content_type.value != "diff"
|
|
352
|
+
or saved_tokens >= 128
|
|
353
|
+
or (saved_tokens >= 32 and savings_ratio >= 0.05)
|
|
354
|
+
)
|
|
355
|
+
token_guard_accepted = envelope_tokens < original_tokens and diff_effective
|
|
356
|
+
token_guard_reason = (
|
|
357
|
+
"accepted"
|
|
358
|
+
if token_guard_accepted
|
|
359
|
+
else (
|
|
360
|
+
"insufficient_effective_savings"
|
|
361
|
+
if envelope_tokens < original_tokens
|
|
362
|
+
else "envelope_erased_savings"
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
token_guard_stage = CompressionStageEvent(
|
|
366
|
+
phase="token-guard",
|
|
367
|
+
algorithm="langchain-approximate-envelope-v2",
|
|
368
|
+
applied=token_guard_accepted,
|
|
369
|
+
reason_code=token_guard_reason,
|
|
370
|
+
input_bytes=original_bytes,
|
|
371
|
+
output_bytes=len(envelope_template.encode("utf-8")),
|
|
372
|
+
input_tokens=original_tokens,
|
|
373
|
+
output_tokens=envelope_tokens,
|
|
374
|
+
metadata={
|
|
375
|
+
"saved_tokens": saved_tokens,
|
|
376
|
+
"savings_ratio": round(savings_ratio, 4),
|
|
377
|
+
"min_absolute_tokens": 128,
|
|
378
|
+
"min_conditional_tokens": 32,
|
|
379
|
+
"min_conditional_ratio": 0.05,
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
stages = (*transformed.stages, token_guard_stage)
|
|
383
|
+
if not token_guard_accepted:
|
|
384
|
+
record_decision(
|
|
385
|
+
decision="fallback",
|
|
386
|
+
reason_code=token_guard_reason,
|
|
387
|
+
reason_detail=(
|
|
388
|
+
"final diff envelope savings did not clear the effective savings floor"
|
|
389
|
+
if token_guard_reason == "insufficient_effective_savings"
|
|
390
|
+
else "final model-visible wrapper did not reduce estimated tokens"
|
|
391
|
+
),
|
|
392
|
+
content_type=transformed.content_type.value,
|
|
393
|
+
transformer=transformed.transformer,
|
|
394
|
+
algorithm_output=transformed.content,
|
|
395
|
+
eligible=True,
|
|
396
|
+
critical_total=transformed.critical_total,
|
|
397
|
+
critical_retained=transformed.critical_retained,
|
|
398
|
+
execution_path=execution_path,
|
|
399
|
+
confidence=confidence,
|
|
400
|
+
stages=stages,
|
|
401
|
+
)
|
|
402
|
+
return message
|
|
403
|
+
try:
|
|
404
|
+
record = repository.put(
|
|
405
|
+
thread_id=thread_id,
|
|
406
|
+
checkpoint_ns=checkpoint_ns,
|
|
407
|
+
tool_call_id=tool_call_id,
|
|
408
|
+
tool_name=name,
|
|
409
|
+
status=status,
|
|
410
|
+
content=original,
|
|
411
|
+
)
|
|
412
|
+
except Exception as exc: # noqa: BLE001
|
|
413
|
+
record_decision(
|
|
414
|
+
decision="fallback",
|
|
415
|
+
reason_code="storage_error",
|
|
416
|
+
reason_detail=type(exc).__name__,
|
|
417
|
+
content_type=transformed.content_type.value,
|
|
418
|
+
transformer=transformed.transformer,
|
|
419
|
+
algorithm_output=transformed.content,
|
|
420
|
+
eligible=True,
|
|
421
|
+
critical_total=transformed.critical_total,
|
|
422
|
+
critical_retained=transformed.critical_retained,
|
|
423
|
+
execution_path=execution_path,
|
|
424
|
+
confidence=confidence,
|
|
425
|
+
stages=stages,
|
|
426
|
+
)
|
|
427
|
+
return message
|
|
428
|
+
|
|
429
|
+
final_content = envelope_template.replace(provisional_ref, record.ref, 1)
|
|
430
|
+
event = record_decision(
|
|
431
|
+
decision="transformed",
|
|
432
|
+
reason_code="compressed",
|
|
433
|
+
content_type=transformed.content_type.value,
|
|
434
|
+
transformer=transformed.transformer,
|
|
435
|
+
visible_content=final_content,
|
|
436
|
+
algorithm_output=transformed.content,
|
|
437
|
+
eligible=True,
|
|
438
|
+
ref=record.ref,
|
|
439
|
+
critical_total=transformed.critical_total,
|
|
440
|
+
critical_retained=transformed.critical_retained,
|
|
441
|
+
execution_path=execution_path,
|
|
442
|
+
confidence=confidence,
|
|
443
|
+
stages=stages,
|
|
444
|
+
)
|
|
445
|
+
metadata = dict(message.artifact) if isinstance(message.artifact, dict) else {}
|
|
446
|
+
metadata["tool_output_transform"] = {
|
|
447
|
+
**event.as_dict(),
|
|
448
|
+
"ref": record.ref,
|
|
449
|
+
"sha256": record.sha256,
|
|
450
|
+
**transformed.metadata,
|
|
451
|
+
}
|
|
452
|
+
if execute_output_truncated:
|
|
453
|
+
metadata["tool_output_contains_untruncated_execute_output"] = True
|
|
454
|
+
message.artifact = metadata
|
|
455
|
+
message.content = final_content
|
|
456
|
+
return message
|
|
457
|
+
|
|
458
|
+
def rewrite_result(
|
|
459
|
+
request: Any,
|
|
460
|
+
result: Any,
|
|
461
|
+
*,
|
|
462
|
+
original_content: str | None = None,
|
|
463
|
+
execute_output_truncated: bool = False,
|
|
464
|
+
) -> Any:
|
|
465
|
+
if isinstance(result, ToolMessage):
|
|
466
|
+
return rewrite_message(
|
|
467
|
+
request,
|
|
468
|
+
result,
|
|
469
|
+
original_content=original_content,
|
|
470
|
+
execute_output_truncated=execute_output_truncated,
|
|
471
|
+
)
|
|
472
|
+
if isinstance(result, Command):
|
|
473
|
+
update = result.update
|
|
474
|
+
messages = update.get("messages") if isinstance(update, dict) else None
|
|
475
|
+
if not isinstance(messages, list):
|
|
476
|
+
return result
|
|
477
|
+
rewritten = [
|
|
478
|
+
rewrite_message(request, item) if isinstance(item, ToolMessage) else item
|
|
479
|
+
for item in messages
|
|
480
|
+
]
|
|
481
|
+
return Command(
|
|
482
|
+
graph=result.graph,
|
|
483
|
+
update={**update, "messages": rewritten},
|
|
484
|
+
resume=result.resume,
|
|
485
|
+
goto=result.goto,
|
|
486
|
+
)
|
|
487
|
+
if isinstance(result, list):
|
|
488
|
+
return [rewrite_result(request, item) for item in result]
|
|
489
|
+
return result
|
|
490
|
+
|
|
491
|
+
def is_execute(request: Any) -> bool:
|
|
492
|
+
return call_value(request, "name") == "execute"
|
|
493
|
+
|
|
494
|
+
def record_tool_interaction(request: Any, result: Any, *, started: float) -> None:
|
|
495
|
+
thread_id, checkpoint_ns = runtime_identity(request)
|
|
496
|
+
name = call_value(request, "name", "tool")
|
|
497
|
+
args = call_args(request)
|
|
498
|
+
position = current_position(thread_id)
|
|
499
|
+
messages: list[ToolMessage] = []
|
|
500
|
+
if isinstance(result, ToolMessage):
|
|
501
|
+
messages = [result]
|
|
502
|
+
elif isinstance(result, Command) and isinstance(result.update, dict):
|
|
503
|
+
messages = [m for m in result.update.get("messages", []) if isinstance(m, ToolMessage)]
|
|
504
|
+
elif isinstance(result, list):
|
|
505
|
+
messages = [m for m in result if isinstance(m, ToolMessage)]
|
|
506
|
+
message = messages[0] if messages else None
|
|
507
|
+
artifact = getattr(message, "artifact", None) if message is not None else None
|
|
508
|
+
transform = artifact.get("tool_output_transform") if isinstance(artifact, dict) else None
|
|
509
|
+
output = content_to_text(message.content) if message is not None else ""
|
|
510
|
+
repository.record_interaction(
|
|
511
|
+
thread_id=thread_id,
|
|
512
|
+
event={
|
|
513
|
+
"event_type": "tool_call",
|
|
514
|
+
"turn_id": position.turn_id,
|
|
515
|
+
"turn_index": position.turn_index,
|
|
516
|
+
"model_call_index": position.model_call_index,
|
|
517
|
+
"tool_call_id": str(
|
|
518
|
+
getattr(message, "tool_call_id", None) or call_value(request, "id")
|
|
519
|
+
),
|
|
520
|
+
"tool_name": name,
|
|
521
|
+
"tool_args": summarized_args(args),
|
|
522
|
+
"checkpoint_ns": checkpoint_ns,
|
|
523
|
+
"status": str(getattr(message, "status", None) or "success"),
|
|
524
|
+
"output_bytes": len(output.encode("utf-8")),
|
|
525
|
+
"compression_managed": name not in excluded,
|
|
526
|
+
"compression_decision": (
|
|
527
|
+
str(transform.get("decision")) if isinstance(transform, dict) else ""
|
|
528
|
+
),
|
|
529
|
+
"compression_reason": (
|
|
530
|
+
str(transform.get("reason_code")) if isinstance(transform, dict) else ""
|
|
531
|
+
),
|
|
532
|
+
"duration_ms": (time.perf_counter() - started) * 1000,
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def wrap_tool_call(self, request, handler): # noqa: ANN001, ARG001
|
|
537
|
+
started = time.perf_counter()
|
|
538
|
+
if not is_execute(request):
|
|
539
|
+
result = rewrite_result(request, handler(request))
|
|
540
|
+
record_tool_interaction(request, result, started=started)
|
|
541
|
+
return result
|
|
542
|
+
capture, token = begin_execute_capture()
|
|
543
|
+
try:
|
|
544
|
+
result = handler(request)
|
|
545
|
+
finally:
|
|
546
|
+
end_execute_capture(token)
|
|
547
|
+
rewritten = rewrite_result(
|
|
548
|
+
request,
|
|
549
|
+
result,
|
|
550
|
+
original_content=capture.full_output,
|
|
551
|
+
execute_output_truncated=capture.truncated,
|
|
552
|
+
)
|
|
553
|
+
record_tool_interaction(request, rewritten, started=started)
|
|
554
|
+
return rewritten
|
|
555
|
+
|
|
556
|
+
async def awrap_tool_call(self, request, handler): # noqa: ANN001, ARG001
|
|
557
|
+
started = time.perf_counter()
|
|
558
|
+
if not is_execute(request):
|
|
559
|
+
result = rewrite_result(request, await handler(request))
|
|
560
|
+
record_tool_interaction(request, result, started=started)
|
|
561
|
+
return result
|
|
562
|
+
capture, token = begin_execute_capture()
|
|
563
|
+
try:
|
|
564
|
+
result = await handler(request)
|
|
565
|
+
finally:
|
|
566
|
+
end_execute_capture(token)
|
|
567
|
+
rewritten = rewrite_result(
|
|
568
|
+
request,
|
|
569
|
+
result,
|
|
570
|
+
original_content=capture.full_output,
|
|
571
|
+
execute_output_truncated=capture.truncated,
|
|
572
|
+
)
|
|
573
|
+
record_tool_interaction(request, rewritten, started=started)
|
|
574
|
+
return rewritten
|
|
575
|
+
|
|
576
|
+
return type(
|
|
577
|
+
"transform_tool_outputs",
|
|
578
|
+
(AgentMiddleware,),
|
|
579
|
+
{
|
|
580
|
+
"state_schema": AgentState,
|
|
581
|
+
"tools": [],
|
|
582
|
+
"wrap_tool_call": wrap_tool_call,
|
|
583
|
+
"awrap_tool_call": awrap_tool_call,
|
|
584
|
+
},
|
|
585
|
+
)()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Record estimated token savings when transformed tool outputs reach a model call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
8
|
+
|
|
9
|
+
from synapse.tool_output.repository import ToolOutputRepository
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _thread_id(request: Any) -> str:
|
|
13
|
+
config = getattr(getattr(request, "runtime", None), "config", None) or {}
|
|
14
|
+
configurable = config.get("configurable") if isinstance(config, dict) else {}
|
|
15
|
+
return str((configurable or {}).get("thread_id") or "")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _avoided_tokens(request: Any) -> int:
|
|
19
|
+
state = getattr(request, "state", None) or {}
|
|
20
|
+
messages = state.get("messages") if isinstance(state, dict) else None
|
|
21
|
+
total = 0
|
|
22
|
+
for message in messages or []:
|
|
23
|
+
artifact = getattr(message, "artifact", None)
|
|
24
|
+
if not isinstance(artifact, dict):
|
|
25
|
+
continue
|
|
26
|
+
transform = artifact.get("tool_output_transform")
|
|
27
|
+
if not isinstance(transform, dict):
|
|
28
|
+
continue
|
|
29
|
+
total += max(0, int(transform.get("estimated_saved_tokens", 0) or 0))
|
|
30
|
+
return total
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_tool_output_usage_middleware(repository: ToolOutputRepository) -> Any:
|
|
34
|
+
"""Record transformed-output savings each time those outputs enter a model call.
|
|
35
|
+
|
|
36
|
+
This is an approximation: it counts model-visible transformed ToolMessages in
|
|
37
|
+
the request state, not provider-tokenizer exact token deltas.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
class _ToolOutputUsageMiddleware(AgentMiddleware):
|
|
41
|
+
state_schema = AgentState
|
|
42
|
+
|
|
43
|
+
def _record(self, request: Any) -> None:
|
|
44
|
+
thread_id = _thread_id(request)
|
|
45
|
+
avoided = _avoided_tokens(request)
|
|
46
|
+
if thread_id and avoided:
|
|
47
|
+
repository.record_model_reuse(
|
|
48
|
+
thread_id=thread_id,
|
|
49
|
+
estimated_avoided_tokens=avoided,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def wrap_model_call(self, request: Any, handler: Any) -> Any:
|
|
53
|
+
self._record(request)
|
|
54
|
+
return handler(request)
|
|
55
|
+
|
|
56
|
+
async def awrap_model_call(self, request: Any, handler: Any) -> Any:
|
|
57
|
+
self._record(request)
|
|
58
|
+
return await handler(request)
|
|
59
|
+
|
|
60
|
+
return _ToolOutputUsageMiddleware()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Session metadata, binding, and persistence services."""
|
|
2
|
+
|
|
3
|
+
from synapse.sessions.store import (
|
|
4
|
+
ModelBinding,
|
|
5
|
+
SessionInfo,
|
|
6
|
+
SessionStore,
|
|
7
|
+
allocate_thread_id,
|
|
8
|
+
apply_binding_to_settings,
|
|
9
|
+
binding_from_settings,
|
|
10
|
+
default_sessions_path,
|
|
11
|
+
format_session_table,
|
|
12
|
+
is_default_session_title,
|
|
13
|
+
pick_startup_thread_id,
|
|
14
|
+
resolve_startup_binding,
|
|
15
|
+
title_from_user_message,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ModelBinding",
|
|
20
|
+
"SessionInfo",
|
|
21
|
+
"SessionStore",
|
|
22
|
+
"allocate_thread_id",
|
|
23
|
+
"apply_binding_to_settings",
|
|
24
|
+
"binding_from_settings",
|
|
25
|
+
"default_sessions_path",
|
|
26
|
+
"format_session_table",
|
|
27
|
+
"is_default_session_title",
|
|
28
|
+
"pick_startup_thread_id",
|
|
29
|
+
"resolve_startup_binding",
|
|
30
|
+
"title_from_user_message",
|
|
31
|
+
]
|