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,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
|
+
)
|