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,375 @@
1
+ """Pure, fail-closed projection of Codex rollouts into visible text snapshots.
2
+
3
+ This module deliberately does not reconstruct Codex runtime state. It produces
4
+ only the effective human and assistant text of completed turns. Callers must
5
+ check ``CodexTextSnapshot.importable`` before using its messages for any future
6
+ import workflow.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import codecs
12
+ import hashlib
13
+ import json
14
+ from collections.abc import Iterable, Iterator
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import zstandard
20
+
21
+ PROJECTION_KIND = "codex_visible_text_v1"
22
+ PARSER_VERSION = 1
23
+ MAX_ROLLOUT_BYTES = 32 * 1024 * 1024
24
+ MAX_ROLLOUT_LINE_BYTES = 256 * 1024
25
+ _READ_CHUNK_BYTES = 64 * 1024
26
+ _USER_MESSAGE_BEGIN = "## My request for Codex:"
27
+ _INTERNAL_USER_MARKERS = (
28
+ "<environment_context>",
29
+ "<user_instructions>",
30
+ "<developer_instructions>",
31
+ "# agents.md instructions",
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class CodexProjectionWarning:
37
+ """A sanitized diagnostic that never includes source text or raw payloads."""
38
+
39
+ code: str
40
+ line_number: int | None = None
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class CodexVisibleMessage:
45
+ """One stable visible message in the projected snapshot."""
46
+
47
+ source_id: str
48
+ turn_id: str
49
+ role: str
50
+ text: str
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class CodexTextSnapshot:
55
+ """Immutable result of the ``codex_visible_text_v1`` projection."""
56
+
57
+ projection_kind: str
58
+ parser_version: int
59
+ messages: tuple[CodexVisibleMessage, ...]
60
+ warnings: tuple[CodexProjectionWarning, ...]
61
+ importable: bool
62
+
63
+
64
+ @dataclass
65
+ class _Turn:
66
+ turn_id: str
67
+ messages: list[CodexVisibleMessage]
68
+ state: str
69
+
70
+
71
+ class CodexHistoryProjector:
72
+ """Project a Codex JSONL rollout without side effects.
73
+
74
+ Normal rollout text is accepted only from ``event_msg.user_message`` and
75
+ ``event_msg.agent_message``. Bare ``response_item`` records are ignored
76
+ because Codex writes them for model context and they may duplicate UI text.
77
+ A ``compacted.replacement_history`` record is the sole exception: it becomes
78
+ the complete new baseline after strict message-only validation.
79
+ """
80
+
81
+ def project_path(self, path: Path | str) -> CodexTextSnapshot:
82
+ """Read one bounded UTF-8 JSONL or JSONL.zst rollout snapshot."""
83
+ rollout_path = Path(path)
84
+ try:
85
+ lines = _rollout_lines(rollout_path)
86
+ return self.project_lines(lines)
87
+ except _RolloutReadError as exc:
88
+ return _rejected_snapshot((CodexProjectionWarning(exc.code),))
89
+ except OSError:
90
+ return _rejected_snapshot((CodexProjectionWarning("rollout_read_failed"),))
91
+ except UnicodeDecodeError:
92
+ return _rejected_snapshot((CodexProjectionWarning("rollout_not_utf8"),))
93
+
94
+ def project_lines(self, lines: Iterable[str]) -> CodexTextSnapshot:
95
+ """Project JSONL records from a caller-owned text stream or iterable."""
96
+ state = _ProjectionState()
97
+ for line_number, line in enumerate(lines, start=1):
98
+ if not line.strip():
99
+ continue
100
+ try:
101
+ record = json.loads(line)
102
+ except json.JSONDecodeError:
103
+ state.reject("invalid_json", line_number)
104
+ break
105
+ if not isinstance(record, dict):
106
+ state.reject("invalid_record", line_number)
107
+ break
108
+ state.handle(record, line_number)
109
+ if state.rejected:
110
+ break
111
+ return state.finish()
112
+
113
+
114
+ class _RolloutReadError(ValueError):
115
+ def __init__(self, code: str) -> None:
116
+ self.code = code
117
+
118
+
119
+ def _rollout_lines(path: Path) -> Iterator[str]:
120
+ if path.suffix == ".zst":
121
+ try:
122
+ with path.open("rb") as raw:
123
+ with zstandard.ZstdDecompressor().stream_reader(raw) as compressed:
124
+ yield from _decode_rollout_lines(compressed)
125
+ except zstandard.ZstdError as exc:
126
+ raise _RolloutReadError("rollout_zstd_invalid") from exc
127
+ return
128
+ with path.open("rb") as raw:
129
+ yield from _decode_rollout_lines(raw)
130
+
131
+
132
+ def _decode_rollout_lines(stream: Any) -> Iterator[str]:
133
+ decoder = codecs.getincrementaldecoder("utf-8")()
134
+ pending = ""
135
+ total_bytes = 0
136
+ while chunk := stream.read(_READ_CHUNK_BYTES):
137
+ total_bytes += len(chunk)
138
+ if total_bytes > MAX_ROLLOUT_BYTES:
139
+ raise _RolloutReadError("rollout_size_limit")
140
+ pending += decoder.decode(chunk)
141
+ while "\n" in pending:
142
+ line, pending = pending.split("\n", 1)
143
+ if len(line.encode()) > MAX_ROLLOUT_LINE_BYTES:
144
+ raise _RolloutReadError("rollout_line_limit")
145
+ yield line
146
+ if len(pending.encode()) > MAX_ROLLOUT_LINE_BYTES:
147
+ raise _RolloutReadError("rollout_line_limit")
148
+ pending += decoder.decode(b"", final=True)
149
+ if pending:
150
+ if len(pending.encode()) > MAX_ROLLOUT_LINE_BYTES:
151
+ raise _RolloutReadError("rollout_line_limit")
152
+ yield pending
153
+
154
+ class _ProjectionState:
155
+ def __init__(self) -> None:
156
+ self.completed_turns: list[_Turn] = []
157
+ self.current_turn: _Turn | None = None
158
+ self.next_implicit_turn = 0
159
+ self.warnings: list[CodexProjectionWarning] = []
160
+ self.rejected = False
161
+
162
+ def reject(self, code: str, line_number: int | None = None) -> None:
163
+ self.warnings.append(CodexProjectionWarning(code, line_number))
164
+ self.rejected = True
165
+
166
+ def warn(self, code: str, line_number: int | None = None) -> None:
167
+ self.warnings.append(CodexProjectionWarning(code, line_number))
168
+
169
+ def handle(self, record: dict[str, Any], line_number: int) -> None:
170
+ record_type = record.get("type")
171
+ payload = record.get("payload")
172
+ if record_type == "event_msg":
173
+ if not isinstance(payload, dict):
174
+ self.reject("invalid_event_payload", line_number)
175
+ return
176
+ self.handle_event(payload, line_number)
177
+ elif record_type == "compacted":
178
+ self.handle_compacted(payload, line_number)
179
+ elif record_type in {
180
+ "session_meta",
181
+ "turn_context",
182
+ "world_state",
183
+ "response_item",
184
+ "inter_agent_communication",
185
+ "inter_agent_communication_metadata",
186
+ }:
187
+ return
188
+ else:
189
+ self.warn("ignored_record_type", line_number)
190
+
191
+ def handle_event(self, event: dict[str, Any], line_number: int) -> None:
192
+ event_type = event.get("type")
193
+ if event_type in {"task_started", "turn_started"}:
194
+ turn_id = event.get("turn_id")
195
+ if not isinstance(turn_id, str) or not turn_id:
196
+ self.reject("invalid_turn_started", line_number)
197
+ return
198
+ if self.current_turn is not None:
199
+ self.warn("superseded_unfinished_turn", line_number)
200
+ self.current_turn = _Turn(turn_id, [], "open")
201
+ elif event_type in {"task_complete", "turn_complete"}:
202
+ self.complete_turn(event.get("turn_id"), line_number)
203
+ elif event_type == "turn_aborted":
204
+ self.abort_turn(event.get("turn_id"), line_number)
205
+ elif event_type == "thread_rolled_back":
206
+ self.rollback(event.get("num_turns"), line_number)
207
+ elif event_type == "user_message":
208
+ self.append_message("user", event.get("message"), line_number)
209
+ elif event_type == "agent_message":
210
+ self.append_message("assistant", event.get("message"), line_number)
211
+ elif event_type in {
212
+ "agent_reasoning",
213
+ "agent_reasoning_raw_content",
214
+ "context_compacted",
215
+ "error",
216
+ "warning",
217
+ }:
218
+ return
219
+ else:
220
+ self.warn("ignored_event_type", line_number)
221
+
222
+ def append_message(self, role: str, text: Any, line_number: int) -> None:
223
+ if not isinstance(text, str):
224
+ self.reject("invalid_visible_message", line_number)
225
+ return
226
+ if role == "user":
227
+ text = _visible_user_text(text)
228
+ if text is None:
229
+ self.reject("internal_user_message", line_number)
230
+ return
231
+ if not text:
232
+ return
233
+ turn = self._ensure_turn()
234
+ position = len(turn.messages)
235
+ source_id = _source_id(turn.turn_id, position, role, text)
236
+ turn.messages.append(CodexVisibleMessage(source_id, turn.turn_id, role, text))
237
+
238
+ def complete_turn(self, turn_id: Any, line_number: int) -> None:
239
+ if self.current_turn is None:
240
+ self.warn("unmatched_turn_complete", line_number)
241
+ return
242
+ if isinstance(turn_id, str) and turn_id and turn_id != self.current_turn.turn_id:
243
+ self.warn("unmatched_turn_complete", line_number)
244
+ return
245
+ self.current_turn.state = "complete"
246
+ self.completed_turns.append(self.current_turn)
247
+ self.current_turn = None
248
+
249
+ def abort_turn(self, turn_id: Any, line_number: int) -> None:
250
+ if self.current_turn is not None and (
251
+ not isinstance(turn_id, str) or not turn_id or turn_id == self.current_turn.turn_id
252
+ ):
253
+ self.current_turn = None
254
+ self.warn("aborted_turn_omitted", line_number)
255
+ return
256
+ if isinstance(turn_id, str) and turn_id:
257
+ for index, turn in enumerate(self.completed_turns):
258
+ if turn.turn_id == turn_id:
259
+ del self.completed_turns[index]
260
+ self.warn("aborted_turn_omitted", line_number)
261
+ return
262
+ self.warn("unmatched_turn_aborted", line_number)
263
+
264
+ def rollback(self, num_turns: Any, line_number: int) -> None:
265
+ if not isinstance(num_turns, int) or isinstance(num_turns, bool) or num_turns < 0:
266
+ self.reject("invalid_rollback", line_number)
267
+ return
268
+ if self.current_turn is not None:
269
+ self.warn("unfinished_turn_omitted", line_number)
270
+ self.current_turn = None
271
+ if num_turns > len(self.completed_turns):
272
+ self.reject("rollback_exceeds_completed_turns", line_number)
273
+ return
274
+ if num_turns:
275
+ del self.completed_turns[-num_turns:]
276
+
277
+ def handle_compacted(self, payload: Any, line_number: int) -> None:
278
+ if not isinstance(payload, dict):
279
+ self.reject("invalid_compaction_payload", line_number)
280
+ return
281
+ replacement_history = payload.get("replacement_history")
282
+ if replacement_history is None:
283
+ self.reject("legacy_compaction_unsupported", line_number)
284
+ return
285
+ messages = _parse_replacement_history(replacement_history, line_number, self)
286
+ if messages is None:
287
+ return
288
+ self.completed_turns = [_Turn("replacement_history", messages, "complete")]
289
+ self.current_turn = None
290
+
291
+ def _ensure_turn(self) -> _Turn:
292
+ if self.current_turn is None:
293
+ turn_id = f"implicit-{self.next_implicit_turn}"
294
+ self.next_implicit_turn += 1
295
+ self.current_turn = _Turn(turn_id, [], "implicit")
296
+ return self.current_turn
297
+
298
+ def finish(self) -> CodexTextSnapshot:
299
+ if self.rejected:
300
+ return _rejected_snapshot(tuple(self.warnings))
301
+ if self.current_turn is not None:
302
+ self.warn("unfinished_turn_omitted")
303
+ messages = tuple(message for turn in self.completed_turns for message in turn.messages)
304
+ return CodexTextSnapshot(
305
+ projection_kind=PROJECTION_KIND,
306
+ parser_version=PARSER_VERSION,
307
+ messages=messages,
308
+ warnings=tuple(self.warnings),
309
+ importable=True,
310
+ )
311
+
312
+
313
+ def _parse_replacement_history(
314
+ replacement_history: Any, line_number: int, state: _ProjectionState
315
+ ) -> list[CodexVisibleMessage] | None:
316
+ if not isinstance(replacement_history, list):
317
+ state.reject("invalid_replacement_history", line_number)
318
+ return None
319
+ messages: list[CodexVisibleMessage] = []
320
+ for index, item in enumerate(replacement_history):
321
+ if not isinstance(item, dict) or item.get("type") != "message":
322
+ state.reject("unsupported_replacement_item", line_number)
323
+ return None
324
+ role = item.get("role")
325
+ content = item.get("content")
326
+ if role not in {"user", "assistant"} or not isinstance(content, list):
327
+ state.reject("unsupported_replacement_item", line_number)
328
+ return None
329
+ text_parts: list[str] = []
330
+ for content_item in content:
331
+ if not isinstance(content_item, dict):
332
+ state.reject("unsupported_replacement_content", line_number)
333
+ return None
334
+ content_type = content_item.get("type")
335
+ expected_type = "input_text" if role == "user" else "output_text"
336
+ text = content_item.get("text")
337
+ if content_type != expected_type or not isinstance(text, str):
338
+ state.reject("unsupported_replacement_content", line_number)
339
+ return None
340
+ if text:
341
+ text_parts.append(text)
342
+ text = "\n".join(text_parts)
343
+ if not text:
344
+ continue
345
+ source_id = _source_id("replacement_history", index, role, text)
346
+ messages.append(CodexVisibleMessage(source_id, "replacement_history", role, text))
347
+ return messages
348
+
349
+
350
+ def _visible_user_text(text: str) -> str | None:
351
+ marker_index = text.find(_USER_MESSAGE_BEGIN)
352
+ visible = text[marker_index + len(_USER_MESSAGE_BEGIN) :] if marker_index >= 0 else text
353
+ visible = visible.strip()
354
+ if any(marker in visible.casefold() for marker in _INTERNAL_USER_MARKERS):
355
+ return None
356
+ return visible
357
+
358
+
359
+ def _source_id(turn_id: str, position: int, role: str, text: str) -> str:
360
+ digest = hashlib.sha256(
361
+ f"{PARSER_VERSION}\0{turn_id}\0{position}\0{role}\0{text}".encode()
362
+ ).hexdigest()
363
+ return f"codex-visible-v1-{digest[:24]}"
364
+
365
+
366
+ def _rejected_snapshot(
367
+ warnings: tuple[CodexProjectionWarning, ...],
368
+ ) -> CodexTextSnapshot:
369
+ return CodexTextSnapshot(
370
+ projection_kind=PROJECTION_KIND,
371
+ parser_version=PARSER_VERSION,
372
+ messages=(),
373
+ warnings=warnings,
374
+ importable=False,
375
+ )