python-codex 0.1.3__py3-none-any.whl → 0.1.5__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.
- pycodex/agent.py +51 -11
- pycodex/cli.py +109 -3
- pycodex/context.py +23 -0
- pycodex/model.py +362 -23
- pycodex/prompts/models.json +30 -0
- pycodex/tools/apply_patch_tool.py +2 -2
- pycodex/utils/__init__.py +4 -0
- pycodex/utils/compactor.py +189 -0
- pycodex/utils/session_persist.py +483 -0
- pycodex/utils/visualize.py +120 -6
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/METADATA +18 -3
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/RECORD +18 -16
- responses_server/app.py +4 -1
- responses_server/payload_processors.py +10 -1
- responses_server/stream_router.py +25 -6
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/WHEEL +0 -0
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..protocol import (
|
|
8
|
+
AssistantMessage,
|
|
9
|
+
ConversationItem,
|
|
10
|
+
ReasoningItem,
|
|
11
|
+
ToolCall,
|
|
12
|
+
ToolResult,
|
|
13
|
+
UserMessage,
|
|
14
|
+
)
|
|
15
|
+
from .get_env import get_package_version
|
|
16
|
+
from .visualize import shorten_title
|
|
17
|
+
import typing
|
|
18
|
+
|
|
19
|
+
SESSION_INDEX_FILENAME = "session_index.jsonl"
|
|
20
|
+
ROLLUP_SESSION_DIRNAMES = ("sessions", "archived_sessions")
|
|
21
|
+
UUID_PATTERN = re.compile(
|
|
22
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
23
|
+
re.IGNORECASE,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_codex_home(
|
|
28
|
+
config_path: 'typing.Union[str, None]' = None,
|
|
29
|
+
) -> 'Path':
|
|
30
|
+
if config_path:
|
|
31
|
+
return Path(config_path).expanduser().resolve().parent
|
|
32
|
+
codex_home = os.environ.get("CODEX_HOME", "").strip()
|
|
33
|
+
if codex_home:
|
|
34
|
+
return Path(codex_home).expanduser().resolve()
|
|
35
|
+
return Path.home() / ".codex"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionRolloutRecorder:
|
|
39
|
+
def __init__(self, rollout_path: 'Path') -> 'None':
|
|
40
|
+
self.rollout_path = rollout_path
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def create(
|
|
44
|
+
cls,
|
|
45
|
+
codex_home: 'Path',
|
|
46
|
+
session_id: 'str',
|
|
47
|
+
cwd: 'Path',
|
|
48
|
+
originator: 'str',
|
|
49
|
+
model_provider: 'typing.Union[str, None]',
|
|
50
|
+
base_instructions: 'str',
|
|
51
|
+
) -> 'SessionRolloutRecorder':
|
|
52
|
+
recorder = cls(_rollout_path_for_session(codex_home, session_id))
|
|
53
|
+
recorder.write_session_meta(
|
|
54
|
+
session_id=session_id,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
originator=originator,
|
|
57
|
+
model_provider=model_provider,
|
|
58
|
+
base_instructions=base_instructions,
|
|
59
|
+
)
|
|
60
|
+
return recorder
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def resume(
|
|
64
|
+
cls,
|
|
65
|
+
rollout_path: 'typing.Union[str, Path]',
|
|
66
|
+
) -> 'SessionRolloutRecorder':
|
|
67
|
+
return cls(Path(rollout_path))
|
|
68
|
+
|
|
69
|
+
def write_session_meta(
|
|
70
|
+
self,
|
|
71
|
+
session_id: 'str',
|
|
72
|
+
cwd: 'Path',
|
|
73
|
+
originator: 'str',
|
|
74
|
+
model_provider: 'typing.Union[str, None]',
|
|
75
|
+
base_instructions: 'str',
|
|
76
|
+
) -> 'None':
|
|
77
|
+
payload = {
|
|
78
|
+
"id": session_id,
|
|
79
|
+
"timestamp": _timestamp_string(),
|
|
80
|
+
"cwd": str(cwd),
|
|
81
|
+
"originator": originator,
|
|
82
|
+
"cli_version": get_package_version(),
|
|
83
|
+
"source": "cli",
|
|
84
|
+
"model_provider": model_provider,
|
|
85
|
+
"base_instructions": {"text": base_instructions},
|
|
86
|
+
}
|
|
87
|
+
self._append_line("session_meta", payload)
|
|
88
|
+
|
|
89
|
+
def append_history_items(
|
|
90
|
+
self,
|
|
91
|
+
items: 'typing.Iterable[ConversationItem]',
|
|
92
|
+
) -> 'None':
|
|
93
|
+
for item in items:
|
|
94
|
+
self.append_history_item(item)
|
|
95
|
+
|
|
96
|
+
def append_history_item(self, item: 'ConversationItem') -> 'None':
|
|
97
|
+
if isinstance(item, UserMessage):
|
|
98
|
+
self._append_line("response_item", item.serialize())
|
|
99
|
+
self._append_line(
|
|
100
|
+
"event_msg",
|
|
101
|
+
{
|
|
102
|
+
"type": "user_message",
|
|
103
|
+
"message": item.text,
|
|
104
|
+
"images": [],
|
|
105
|
+
"local_images": [],
|
|
106
|
+
"text_elements": [],
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
if isinstance(item, ToolResult):
|
|
111
|
+
self._append_line("response_item", item.serialize())
|
|
112
|
+
return
|
|
113
|
+
serialized = item.serialize()
|
|
114
|
+
if isinstance(serialized, dict):
|
|
115
|
+
self._append_line("response_item", serialized)
|
|
116
|
+
|
|
117
|
+
def append_compacted_history(
|
|
118
|
+
self,
|
|
119
|
+
history: 'typing.Iterable[ConversationItem]',
|
|
120
|
+
) -> 'None':
|
|
121
|
+
serialized_items = []
|
|
122
|
+
for item in history:
|
|
123
|
+
serialized = item.serialize()
|
|
124
|
+
if isinstance(serialized, dict):
|
|
125
|
+
serialized_items.append(serialized)
|
|
126
|
+
self._append_line(
|
|
127
|
+
"compacted",
|
|
128
|
+
{"replacement_history": serialized_items},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _append_line(self, item_type: 'str', payload: 'typing.Dict[str, object]') -> 'None':
|
|
132
|
+
self.rollout_path.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
line = {
|
|
134
|
+
"timestamp": _timestamp_string(),
|
|
135
|
+
"type": item_type,
|
|
136
|
+
"payload": payload,
|
|
137
|
+
}
|
|
138
|
+
with self.rollout_path.open("a", encoding="utf-8") as handle:
|
|
139
|
+
handle.write(json.dumps(line, ensure_ascii=False, separators=(",", ":")))
|
|
140
|
+
handle.write("\n")
|
|
141
|
+
handle.flush()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def list_resumable_sessions(
|
|
145
|
+
codex_home: 'Path',
|
|
146
|
+
limit: 'int' = 20,
|
|
147
|
+
) -> 'typing.Tuple[typing.Dict[str, str], ...]':
|
|
148
|
+
latest_rollouts_by_id: 'typing.Dict[str, Path]' = {}
|
|
149
|
+
for dirname in ROLLUP_SESSION_DIRNAMES:
|
|
150
|
+
root = codex_home / dirname
|
|
151
|
+
if not root.exists():
|
|
152
|
+
continue
|
|
153
|
+
for path in root.rglob("rollout-*.jsonl"):
|
|
154
|
+
thread_id = _thread_id_from_rollout_path(path)
|
|
155
|
+
if thread_id is None:
|
|
156
|
+
continue
|
|
157
|
+
previous = latest_rollouts_by_id.get(thread_id)
|
|
158
|
+
if previous is None or path.stat().st_mtime > previous.stat().st_mtime:
|
|
159
|
+
latest_rollouts_by_id[thread_id] = path
|
|
160
|
+
|
|
161
|
+
latest_names_by_id = _latest_thread_names_by_id(codex_home)
|
|
162
|
+
ordered_paths = sorted(
|
|
163
|
+
latest_rollouts_by_id.items(),
|
|
164
|
+
key=lambda item: (item[1].stat().st_mtime, str(item[1])),
|
|
165
|
+
reverse=True,
|
|
166
|
+
)
|
|
167
|
+
sessions: 'typing.List[typing.Dict[str, str]]' = []
|
|
168
|
+
for thread_id, path in ordered_paths[:limit]:
|
|
169
|
+
thread_name = latest_names_by_id.get(thread_id, "")
|
|
170
|
+
preview = _extract_first_user_message_preview(path)
|
|
171
|
+
if preview is None:
|
|
172
|
+
continue
|
|
173
|
+
sessions.append(
|
|
174
|
+
{
|
|
175
|
+
"thread_id": thread_id,
|
|
176
|
+
"title": thread_name or preview,
|
|
177
|
+
"preview": preview,
|
|
178
|
+
"rollout_path": str(path),
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
return tuple(sessions)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def load_resumed_session(
|
|
185
|
+
codex_home: 'Path',
|
|
186
|
+
resume_index_text: 'str',
|
|
187
|
+
) -> 'typing.Dict[str, object]':
|
|
188
|
+
normalized_target = resume_index_text.strip()
|
|
189
|
+
if not normalized_target.isdigit():
|
|
190
|
+
raise ValueError("Usage: /resume <number>")
|
|
191
|
+
|
|
192
|
+
sessions = list_resumable_sessions(codex_home)
|
|
193
|
+
resume_index = int(normalized_target)
|
|
194
|
+
if resume_index < 1 or resume_index > len(sessions):
|
|
195
|
+
raise ValueError(f"Session not found: {normalized_target}")
|
|
196
|
+
|
|
197
|
+
session = sessions[resume_index - 1]
|
|
198
|
+
thread_id = session["thread_id"]
|
|
199
|
+
rollout_path = Path(session["rollout_path"])
|
|
200
|
+
thread_name = _latest_thread_names_by_id(codex_home).get(thread_id)
|
|
201
|
+
session_id = thread_id
|
|
202
|
+
history: 'typing.List[ConversationItem]' = []
|
|
203
|
+
saw_user_turn = False
|
|
204
|
+
tool_names_by_call_id: 'typing.Dict[str, str]' = {}
|
|
205
|
+
|
|
206
|
+
for entry in _iter_rollout_entries(rollout_path):
|
|
207
|
+
item_type = str(entry.get("type", "")).strip()
|
|
208
|
+
payload = entry.get("payload")
|
|
209
|
+
|
|
210
|
+
if item_type == "session_meta" and isinstance(payload, dict):
|
|
211
|
+
session_id = str(payload.get("id", "")).strip() or session_id
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if item_type == "compacted" and isinstance(payload, dict):
|
|
215
|
+
replacement_history = payload.get("replacement_history")
|
|
216
|
+
if isinstance(replacement_history, list):
|
|
217
|
+
history = _deserialize_compacted_history(replacement_history)
|
|
218
|
+
saw_user_turn = any(
|
|
219
|
+
isinstance(item, UserMessage) for item in history
|
|
220
|
+
)
|
|
221
|
+
tool_names_by_call_id = {
|
|
222
|
+
item.call_id: item.name
|
|
223
|
+
for item in history
|
|
224
|
+
if isinstance(item, ToolCall)
|
|
225
|
+
}
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if item_type == "event_msg" and isinstance(payload, dict):
|
|
229
|
+
if payload.get("type") == "user_message":
|
|
230
|
+
history.append(UserMessage(text=str(payload.get("message", ""))))
|
|
231
|
+
saw_user_turn = True
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if item_type != "response_item" or not saw_user_turn or not isinstance(payload, dict):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
_append_deserialized_response_item(
|
|
238
|
+
history,
|
|
239
|
+
payload,
|
|
240
|
+
tool_names_by_call_id,
|
|
241
|
+
include_user_messages=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if not history:
|
|
245
|
+
raise ValueError(f"No resumable history found in {rollout_path}")
|
|
246
|
+
|
|
247
|
+
turns = conversation_history_to_turns(history)
|
|
248
|
+
title = thread_name or (shorten_title(turns[0][0]) if turns else thread_id)
|
|
249
|
+
return {
|
|
250
|
+
"session_id": session_id,
|
|
251
|
+
"thread_id": thread_id,
|
|
252
|
+
"title": title,
|
|
253
|
+
"history": tuple(history),
|
|
254
|
+
"turns": tuple(turns),
|
|
255
|
+
"rollout_path": rollout_path,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def conversation_history_to_turns(
|
|
260
|
+
history: 'typing.Iterable[ConversationItem]',
|
|
261
|
+
) -> 'typing.Tuple[typing.Tuple[str, str], ...]':
|
|
262
|
+
turns: 'typing.List[typing.Tuple[str, str]]' = []
|
|
263
|
+
current_user_text: 'typing.Union[str, None]' = None
|
|
264
|
+
current_assistant_text = ""
|
|
265
|
+
for item in history:
|
|
266
|
+
if isinstance(item, UserMessage):
|
|
267
|
+
if current_user_text is not None:
|
|
268
|
+
turns.append((current_user_text, current_assistant_text))
|
|
269
|
+
current_user_text = item.text
|
|
270
|
+
current_assistant_text = ""
|
|
271
|
+
continue
|
|
272
|
+
if isinstance(item, AssistantMessage) and current_user_text is not None:
|
|
273
|
+
current_assistant_text = item.text
|
|
274
|
+
if current_user_text is not None:
|
|
275
|
+
turns.append((current_user_text, current_assistant_text))
|
|
276
|
+
return tuple(turns)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _latest_thread_names_by_id(codex_home: 'Path') -> 'typing.Dict[str, str]':
|
|
280
|
+
index_path = codex_home / SESSION_INDEX_FILENAME
|
|
281
|
+
if not index_path.exists():
|
|
282
|
+
return {}
|
|
283
|
+
|
|
284
|
+
names_by_id: 'typing.Dict[str, str]' = {}
|
|
285
|
+
for raw_line in reversed(index_path.read_text().splitlines()):
|
|
286
|
+
line = raw_line.strip()
|
|
287
|
+
if not line:
|
|
288
|
+
continue
|
|
289
|
+
try:
|
|
290
|
+
entry = json.loads(line)
|
|
291
|
+
except json.JSONDecodeError:
|
|
292
|
+
continue
|
|
293
|
+
if not isinstance(entry, dict):
|
|
294
|
+
continue
|
|
295
|
+
thread_id = str(entry.get("id", "")).strip()
|
|
296
|
+
thread_name = str(entry.get("thread_name", "")).strip()
|
|
297
|
+
if thread_id and thread_name and thread_id not in names_by_id:
|
|
298
|
+
names_by_id[thread_id] = thread_name
|
|
299
|
+
return names_by_id
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _thread_id_from_rollout_path(path: 'Path') -> 'typing.Union[str, None]':
|
|
303
|
+
stem = path.stem
|
|
304
|
+
if len(stem) < 36:
|
|
305
|
+
return None
|
|
306
|
+
candidate = stem[-36:]
|
|
307
|
+
return candidate if UUID_PATTERN.match(candidate) else None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _extract_first_user_message_preview(rollout_path: 'Path') -> 'typing.Union[str, None]':
|
|
311
|
+
for entry in _iter_rollout_entries(rollout_path):
|
|
312
|
+
if entry.get("type") != "event_msg":
|
|
313
|
+
continue
|
|
314
|
+
payload = entry.get("payload")
|
|
315
|
+
if not isinstance(payload, dict) or payload.get("type") != "user_message":
|
|
316
|
+
continue
|
|
317
|
+
message = str(payload.get("message", "")).strip()
|
|
318
|
+
if message:
|
|
319
|
+
return shorten_title(message, limit=72)
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _iter_rollout_entries(rollout_path: 'Path') -> 'typing.Iterable[typing.Dict[str, object]]':
|
|
324
|
+
text = rollout_path.read_text()
|
|
325
|
+
decoder = json.JSONDecoder()
|
|
326
|
+
index = 0
|
|
327
|
+
parsed_entries = 0
|
|
328
|
+
text_length = len(text)
|
|
329
|
+
|
|
330
|
+
while index < text_length:
|
|
331
|
+
while index < text_length and text[index].isspace():
|
|
332
|
+
index += 1
|
|
333
|
+
if index >= text_length:
|
|
334
|
+
break
|
|
335
|
+
try:
|
|
336
|
+
entry, index = decoder.raw_decode(text, index)
|
|
337
|
+
except json.JSONDecodeError as exc:
|
|
338
|
+
if parsed_entries > 0:
|
|
339
|
+
break
|
|
340
|
+
raise ValueError(f"failed to parse rollout file {rollout_path}: {exc}") from exc
|
|
341
|
+
if isinstance(entry, dict):
|
|
342
|
+
parsed_entries += 1
|
|
343
|
+
yield entry
|
|
344
|
+
|
|
345
|
+
if parsed_entries == 0:
|
|
346
|
+
raise ValueError(f"no rollout entries found in {rollout_path}")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _extract_response_message_text(payload: 'typing.Dict[str, object]') -> 'str':
|
|
350
|
+
text_parts: 'typing.List[str]' = []
|
|
351
|
+
for item in payload.get("content") or []:
|
|
352
|
+
if isinstance(item, dict) and item.get("type") in {"input_text", "output_text"}:
|
|
353
|
+
text_parts.append(str(item.get("text", "")))
|
|
354
|
+
return "".join(text_parts)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _deserialize_compacted_history(
|
|
358
|
+
replacement_history: 'typing.Iterable[object]',
|
|
359
|
+
) -> 'typing.List[ConversationItem]':
|
|
360
|
+
history: 'typing.List[ConversationItem]' = []
|
|
361
|
+
tool_names_by_call_id: 'typing.Dict[str, str]' = {}
|
|
362
|
+
for payload in replacement_history:
|
|
363
|
+
if not isinstance(payload, dict):
|
|
364
|
+
continue
|
|
365
|
+
_append_deserialized_response_item(
|
|
366
|
+
history,
|
|
367
|
+
payload,
|
|
368
|
+
tool_names_by_call_id,
|
|
369
|
+
include_user_messages=True,
|
|
370
|
+
)
|
|
371
|
+
return history
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _append_deserialized_response_item(
|
|
375
|
+
history: 'typing.List[ConversationItem]',
|
|
376
|
+
payload: 'typing.Dict[str, object]',
|
|
377
|
+
tool_names_by_call_id: 'typing.Dict[str, str]',
|
|
378
|
+
include_user_messages: 'bool',
|
|
379
|
+
) -> 'None':
|
|
380
|
+
response_item_type = str(payload.get("type", "")).strip()
|
|
381
|
+
if response_item_type == "message":
|
|
382
|
+
role = str(payload.get("role", "")).strip()
|
|
383
|
+
if role == "assistant":
|
|
384
|
+
history.append(AssistantMessage(text=_extract_response_message_text(payload)))
|
|
385
|
+
return
|
|
386
|
+
if include_user_messages and role == "user":
|
|
387
|
+
history.append(UserMessage(text=_extract_response_message_text(payload)))
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
if response_item_type == "reasoning":
|
|
391
|
+
history.append(ReasoningItem(payload=dict(payload)))
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
if response_item_type == "function_call":
|
|
395
|
+
raw_arguments = payload.get("arguments", "{}")
|
|
396
|
+
if isinstance(raw_arguments, str):
|
|
397
|
+
try:
|
|
398
|
+
arguments = json.loads(raw_arguments or "{}")
|
|
399
|
+
except json.JSONDecodeError:
|
|
400
|
+
return
|
|
401
|
+
elif isinstance(raw_arguments, dict):
|
|
402
|
+
arguments = dict(raw_arguments)
|
|
403
|
+
else:
|
|
404
|
+
return
|
|
405
|
+
if not isinstance(arguments, dict):
|
|
406
|
+
return
|
|
407
|
+
call_id = str(payload.get("call_id", "")).strip()
|
|
408
|
+
name = str(payload.get("name", "")).strip()
|
|
409
|
+
if not call_id or not name:
|
|
410
|
+
return
|
|
411
|
+
history.append(ToolCall(call_id=call_id, name=name, arguments=arguments))
|
|
412
|
+
tool_names_by_call_id[call_id] = name
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
if response_item_type == "custom_tool_call":
|
|
416
|
+
call_id = str(payload.get("call_id", "")).strip()
|
|
417
|
+
name = str(payload.get("name", "")).strip()
|
|
418
|
+
if not call_id or not name:
|
|
419
|
+
return
|
|
420
|
+
history.append(
|
|
421
|
+
ToolCall(
|
|
422
|
+
call_id=call_id,
|
|
423
|
+
name=name,
|
|
424
|
+
arguments=str(payload.get("input", "")),
|
|
425
|
+
tool_type="custom",
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
tool_names_by_call_id[call_id] = name
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
if response_item_type not in {"function_call_output", "custom_tool_call_output"}:
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
call_id = str(payload.get("call_id", "")).strip()
|
|
435
|
+
if not call_id:
|
|
436
|
+
return
|
|
437
|
+
raw_output = payload.get("output", "")
|
|
438
|
+
content_items = None
|
|
439
|
+
if isinstance(raw_output, list) and all(
|
|
440
|
+
isinstance(item, dict) for item in raw_output
|
|
441
|
+
):
|
|
442
|
+
content_items = tuple(dict(item) for item in raw_output)
|
|
443
|
+
output = json.dumps(raw_output, ensure_ascii=False)
|
|
444
|
+
elif isinstance(raw_output, (dict, list, str, int, float, bool)) or raw_output is None:
|
|
445
|
+
output = raw_output
|
|
446
|
+
else:
|
|
447
|
+
output = str(raw_output)
|
|
448
|
+
history.append(
|
|
449
|
+
ToolResult(
|
|
450
|
+
call_id=call_id,
|
|
451
|
+
name=tool_names_by_call_id.get(call_id, ""),
|
|
452
|
+
output=output,
|
|
453
|
+
content_items=content_items,
|
|
454
|
+
success=(
|
|
455
|
+
payload.get("success")
|
|
456
|
+
if isinstance(payload.get("success"), bool)
|
|
457
|
+
else None
|
|
458
|
+
),
|
|
459
|
+
tool_type=(
|
|
460
|
+
"custom"
|
|
461
|
+
if response_item_type == "custom_tool_call_output"
|
|
462
|
+
else "function"
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _rollout_path_for_session(codex_home: 'Path', session_id: 'str') -> 'Path':
|
|
469
|
+
now = datetime.now(timezone.utc)
|
|
470
|
+
return (
|
|
471
|
+
codex_home
|
|
472
|
+
/ "sessions"
|
|
473
|
+
/ now.strftime("%Y")
|
|
474
|
+
/ now.strftime("%m")
|
|
475
|
+
/ now.strftime("%d")
|
|
476
|
+
/ f"rollout-{now.strftime('%Y-%m-%dT%H-%M-%S')}-{session_id}.jsonl"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _timestamp_string() -> 'str':
|
|
481
|
+
now = datetime.now(timezone.utc)
|
|
482
|
+
milliseconds = int(now.microsecond / 1000)
|
|
483
|
+
return now.strftime("%Y-%m-%dT%H:%M:%S") + f".{milliseconds:03d}Z"
|