codex-python-sdk 0.1.0__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.
- codex_python_sdk/__init__.py +57 -0
- codex_python_sdk/_shared.py +99 -0
- codex_python_sdk/async_client.py +1313 -0
- codex_python_sdk/errors.py +18 -0
- codex_python_sdk/examples/__init__.py +2 -0
- codex_python_sdk/examples/demo_smoke.py +304 -0
- codex_python_sdk/factory.py +25 -0
- codex_python_sdk/policy.py +636 -0
- codex_python_sdk/renderer.py +607 -0
- codex_python_sdk/sync_client.py +333 -0
- codex_python_sdk/types.py +48 -0
- codex_python_sdk-0.1.0.dist-info/METADATA +274 -0
- codex_python_sdk-0.1.0.dist-info/RECORD +17 -0
- codex_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- codex_python_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- codex_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- codex_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Iterator, TextIO
|
|
7
|
+
|
|
8
|
+
from ._shared import first_nonempty_text
|
|
9
|
+
from .types import ResponseEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExecStyleRenderer:
|
|
13
|
+
"""Render `ResponseEvent` stream in a compact CLI style similar to `codex exec`."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
stream: TextIO | None = None,
|
|
19
|
+
show_reasoning: bool = True,
|
|
20
|
+
show_system: bool = False,
|
|
21
|
+
show_tool_output: bool = True,
|
|
22
|
+
show_turn_summary: bool = True,
|
|
23
|
+
color: str = "auto",
|
|
24
|
+
) -> None:
|
|
25
|
+
self.stream = stream or sys.stdout
|
|
26
|
+
self.show_reasoning = show_reasoning
|
|
27
|
+
self.show_system = show_system
|
|
28
|
+
self.show_tool_output = show_tool_output
|
|
29
|
+
self.show_turn_summary = show_turn_summary
|
|
30
|
+
|
|
31
|
+
self._in_assistant_line = False
|
|
32
|
+
self._in_reasoning_line = False
|
|
33
|
+
self._in_tool_output = False
|
|
34
|
+
self._tool_items: dict[str, str] = {}
|
|
35
|
+
self._turn_tool_count = 0
|
|
36
|
+
self._turn_error_count = 0
|
|
37
|
+
self._seen_deprecation_warnings: set[str] = set()
|
|
38
|
+
|
|
39
|
+
self._color_mode = self._normalize_color_mode(color)
|
|
40
|
+
self._use_color = self._compute_use_color(self._color_mode, self.stream)
|
|
41
|
+
self._palette = self._palette_for(self._color_mode)
|
|
42
|
+
|
|
43
|
+
def render(self, event: ResponseEvent) -> None:
|
|
44
|
+
method = event.type
|
|
45
|
+
params = event.raw.get("params") if isinstance(event.raw.get("params"), dict) else {}
|
|
46
|
+
|
|
47
|
+
if method.startswith("codex/event/"):
|
|
48
|
+
self._render_codex_event(method, params, event)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
if self._render_lifecycle_event(method, params, event):
|
|
52
|
+
return
|
|
53
|
+
if self._render_item_event(method, params, event):
|
|
54
|
+
return
|
|
55
|
+
if self._render_account_event(method, params, event):
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if self.show_system:
|
|
59
|
+
self._close_inline_sections()
|
|
60
|
+
msg = event.message_text or event.text_delta or ""
|
|
61
|
+
tail = f" {msg}" if msg else ""
|
|
62
|
+
self._println(f"{self._tone('system', '[event]')} {method}{tail}")
|
|
63
|
+
|
|
64
|
+
def _render_lifecycle_event(self, method: str, params: dict[str, Any], event: ResponseEvent) -> bool:
|
|
65
|
+
if method == "thread/ready":
|
|
66
|
+
self._close_inline_sections()
|
|
67
|
+
session = event.session_id or "unknown"
|
|
68
|
+
self._println(f"{self._tone('session', 'Session:')} {session}")
|
|
69
|
+
return True
|
|
70
|
+
if method == "thread/started":
|
|
71
|
+
if self.show_system:
|
|
72
|
+
self._close_inline_sections()
|
|
73
|
+
self._println(f"{self._tone('system', '[thread]')} started")
|
|
74
|
+
return True
|
|
75
|
+
if method == "thread/name/updated":
|
|
76
|
+
if self.show_system:
|
|
77
|
+
self._close_inline_sections()
|
|
78
|
+
name = event.thread_name
|
|
79
|
+
if name is None:
|
|
80
|
+
self._println(f"{self._tone('system', '[thread.name]')} (cleared)")
|
|
81
|
+
else:
|
|
82
|
+
self._println(f"{self._tone('system', '[thread.name]')} {name}")
|
|
83
|
+
return True
|
|
84
|
+
if method == "thread/tokenUsage/updated":
|
|
85
|
+
if self.show_system:
|
|
86
|
+
self._close_inline_sections()
|
|
87
|
+
msg = event.message_text or "token usage updated"
|
|
88
|
+
self._println(f"{self._tone('system', '[usage]')} {msg}")
|
|
89
|
+
return True
|
|
90
|
+
if method == "thread/compacted":
|
|
91
|
+
if self.show_system:
|
|
92
|
+
self._close_inline_sections()
|
|
93
|
+
self._println(f"{self._tone('system', '[context]')} compacted")
|
|
94
|
+
return True
|
|
95
|
+
if method == "turn/started":
|
|
96
|
+
self._close_inline_sections()
|
|
97
|
+
self._println("")
|
|
98
|
+
self._println(self._tone("header", "== Turn Started =="))
|
|
99
|
+
self._turn_tool_count = 0
|
|
100
|
+
self._turn_error_count = 0
|
|
101
|
+
self._tool_items.clear()
|
|
102
|
+
return True
|
|
103
|
+
if method == "turn/completed":
|
|
104
|
+
self._close_inline_sections()
|
|
105
|
+
self._println("")
|
|
106
|
+
if self.show_turn_summary:
|
|
107
|
+
status = self._turn_status(params)
|
|
108
|
+
summary = f"status={status}, tools={self._turn_tool_count}, errors={self._turn_error_count}"
|
|
109
|
+
self._println(f"{self._tone('summary', '[summary]')} {summary}")
|
|
110
|
+
self._println(self._tone("header", "== Turn Completed =="))
|
|
111
|
+
return True
|
|
112
|
+
if method == "error":
|
|
113
|
+
self._close_inline_sections()
|
|
114
|
+
msg = event.message_text or first_nonempty_text(params) or "unknown error"
|
|
115
|
+
self._println(f"{self._tone('error', '[error]')} {msg}")
|
|
116
|
+
self._turn_error_count += 1
|
|
117
|
+
return True
|
|
118
|
+
if method == "deprecationNotice":
|
|
119
|
+
self._render_deprecation_warning(method, params, event)
|
|
120
|
+
return True
|
|
121
|
+
if method in ("configWarning", "windows/worldWritableWarning"):
|
|
122
|
+
self._close_inline_sections()
|
|
123
|
+
msg = event.message_text or first_nonempty_text(params) or method
|
|
124
|
+
self._println(f"{self._tone('warning', '[warning]')} {msg}")
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def _render_item_event(self, method: str, params: dict[str, Any], event: ResponseEvent) -> bool:
|
|
129
|
+
if method == "item/started":
|
|
130
|
+
self._render_item_started(params)
|
|
131
|
+
return True
|
|
132
|
+
if method in ("item/reasoning/summaryTextDelta", "item/reasoning/textDelta"):
|
|
133
|
+
self._render_reasoning_delta(event)
|
|
134
|
+
return True
|
|
135
|
+
if method in ("item/commandExecution/outputDelta", "item/fileChange/outputDelta"):
|
|
136
|
+
self._render_tool_output_delta(params, event)
|
|
137
|
+
return True
|
|
138
|
+
if method == "item/commandExecution/terminalInteraction":
|
|
139
|
+
self._render_terminal_interaction(params)
|
|
140
|
+
return True
|
|
141
|
+
if method == "item/mcpToolCall/progress":
|
|
142
|
+
self._render_mcp_progress(params, event)
|
|
143
|
+
return True
|
|
144
|
+
if method == "item/plan/delta":
|
|
145
|
+
self._render_plan_delta(event)
|
|
146
|
+
return True
|
|
147
|
+
if method == "turn/plan/updated":
|
|
148
|
+
self._render_turn_plan(event)
|
|
149
|
+
return True
|
|
150
|
+
if method == "turn/diff/updated":
|
|
151
|
+
self._render_turn_diff(event)
|
|
152
|
+
return True
|
|
153
|
+
if method == "item/reasoning/summaryPartAdded":
|
|
154
|
+
self._render_summary_part(event)
|
|
155
|
+
return True
|
|
156
|
+
if method == "item/agentMessage/delta":
|
|
157
|
+
self._render_agent_delta(event)
|
|
158
|
+
return True
|
|
159
|
+
if method == "item/completed":
|
|
160
|
+
self._render_item_completed(params, event)
|
|
161
|
+
return True
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def _render_account_event(self, method: str, params: dict[str, Any], event: ResponseEvent) -> bool:
|
|
165
|
+
if method not in ("account/rateLimits/updated", "account/updated", "authStatusChange"):
|
|
166
|
+
return False
|
|
167
|
+
if self.show_system:
|
|
168
|
+
self._close_inline_sections()
|
|
169
|
+
msg = event.message_text or first_nonempty_text(params) or method
|
|
170
|
+
self._println(f"{self._tone('system', '[account]')} {msg}")
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def _render_item_started(self, params: dict[str, Any]) -> None:
|
|
174
|
+
self._close_inline_sections()
|
|
175
|
+
item = params.get("item") if isinstance(params.get("item"), dict) else {}
|
|
176
|
+
item_type = item.get("type")
|
|
177
|
+
item_id = item.get("id") if isinstance(item.get("id"), str) else None
|
|
178
|
+
if item_id:
|
|
179
|
+
self._tool_items[item_id] = self._label_for_item(item)
|
|
180
|
+
if item_type == "commandExecution":
|
|
181
|
+
command = item.get("command")
|
|
182
|
+
if isinstance(command, str) and command:
|
|
183
|
+
self._println(f"{self._tone('tool', '[tool.exec]')} {command}")
|
|
184
|
+
else:
|
|
185
|
+
self._println(self._tone("tool", "[tool.exec] started"))
|
|
186
|
+
self._turn_tool_count += 1
|
|
187
|
+
return
|
|
188
|
+
if item_type == "mcpToolCall":
|
|
189
|
+
server = item.get("server")
|
|
190
|
+
tool = item.get("tool")
|
|
191
|
+
if isinstance(server, str) and isinstance(tool, str):
|
|
192
|
+
self._println(f"{self._tone('tool', '[tool.mcp]')} {server}/{tool}")
|
|
193
|
+
else:
|
|
194
|
+
self._println(self._tone("tool", "[tool.mcp] started"))
|
|
195
|
+
self._turn_tool_count += 1
|
|
196
|
+
return
|
|
197
|
+
if item_type == "fileChange":
|
|
198
|
+
self._println(self._tone("tool", "[tool.patch] applying file changes"))
|
|
199
|
+
self._turn_tool_count += 1
|
|
200
|
+
return
|
|
201
|
+
if item_type == "webSearch":
|
|
202
|
+
query = item.get("query")
|
|
203
|
+
if isinstance(query, str) and query:
|
|
204
|
+
self._println(f"{self._tone('tool', '[tool.web]')} {query}")
|
|
205
|
+
else:
|
|
206
|
+
self._println(self._tone("tool", "[tool.web] searching"))
|
|
207
|
+
self._turn_tool_count += 1
|
|
208
|
+
return
|
|
209
|
+
if item_type == "plan":
|
|
210
|
+
if self.show_system:
|
|
211
|
+
self._println(self._tone("plan", "[plan] started"))
|
|
212
|
+
return
|
|
213
|
+
if item_type == "reasoning":
|
|
214
|
+
if self.show_reasoning and self.show_system:
|
|
215
|
+
self._println(self._tone("thinking", "[thinking] started"))
|
|
216
|
+
return
|
|
217
|
+
if self.show_system:
|
|
218
|
+
self._println(f"{self._tone('system', '[item]')} {item_type or 'unknown'} started")
|
|
219
|
+
|
|
220
|
+
def _render_reasoning_delta(self, event: ResponseEvent) -> None:
|
|
221
|
+
if not self.show_reasoning:
|
|
222
|
+
return
|
|
223
|
+
delta = event.text_delta or event.message_text
|
|
224
|
+
if not delta:
|
|
225
|
+
return
|
|
226
|
+
self._close_assistant()
|
|
227
|
+
self._close_tool_output()
|
|
228
|
+
if not self._in_reasoning_line:
|
|
229
|
+
self._print(self._tone("thinking", "[thinking] "))
|
|
230
|
+
self._in_reasoning_line = True
|
|
231
|
+
self._print(delta)
|
|
232
|
+
|
|
233
|
+
def _render_tool_output_delta(self, params: dict[str, Any], event: ResponseEvent) -> None:
|
|
234
|
+
if not self.show_tool_output:
|
|
235
|
+
return
|
|
236
|
+
delta = event.text_delta or event.message_text
|
|
237
|
+
if not delta:
|
|
238
|
+
return
|
|
239
|
+
self._close_assistant()
|
|
240
|
+
self._close_reasoning()
|
|
241
|
+
if not self._in_tool_output:
|
|
242
|
+
label = self._label_for_item_id(params.get("itemId"))
|
|
243
|
+
self._println(self._tone("tool_output", f"[tool.output:{label}]"))
|
|
244
|
+
self._in_tool_output = True
|
|
245
|
+
self._print(delta)
|
|
246
|
+
|
|
247
|
+
def _render_terminal_interaction(self, params: dict[str, Any]) -> None:
|
|
248
|
+
self._close_inline_sections()
|
|
249
|
+
user_stdin = params.get("stdin")
|
|
250
|
+
if isinstance(user_stdin, str) and user_stdin:
|
|
251
|
+
self._println(f"{self._tone('tool', '[tool.stdin]')} {user_stdin.rstrip()}")
|
|
252
|
+
elif self.show_system:
|
|
253
|
+
self._println(self._tone("tool", "[tool.stdin] input sent"))
|
|
254
|
+
|
|
255
|
+
def _render_mcp_progress(self, params: dict[str, Any], event: ResponseEvent) -> None:
|
|
256
|
+
msg = event.message_text or first_nonempty_text(params)
|
|
257
|
+
if not msg:
|
|
258
|
+
return
|
|
259
|
+
self._close_inline_sections()
|
|
260
|
+
self._println(f"{self._tone('tool', '[tool.mcp.progress]')} {msg}")
|
|
261
|
+
|
|
262
|
+
def _render_plan_delta(self, event: ResponseEvent) -> None:
|
|
263
|
+
if not self.show_system:
|
|
264
|
+
return
|
|
265
|
+
delta = event.text_delta or event.message_text
|
|
266
|
+
if not delta:
|
|
267
|
+
return
|
|
268
|
+
self._close_inline_sections()
|
|
269
|
+
self._println(f"{self._tone('plan', '[plan]')} {delta}")
|
|
270
|
+
|
|
271
|
+
def _render_turn_plan(self, event: ResponseEvent) -> None:
|
|
272
|
+
if not self.show_system:
|
|
273
|
+
return
|
|
274
|
+
msg = event.message_text
|
|
275
|
+
if not msg:
|
|
276
|
+
return
|
|
277
|
+
self._close_inline_sections()
|
|
278
|
+
self._println(f"{self._tone('plan', '[turn.plan]')} {msg}")
|
|
279
|
+
|
|
280
|
+
def _render_turn_diff(self, event: ResponseEvent) -> None:
|
|
281
|
+
if not self.show_system:
|
|
282
|
+
return
|
|
283
|
+
msg = event.message_text
|
|
284
|
+
if not msg:
|
|
285
|
+
return
|
|
286
|
+
self._close_inline_sections()
|
|
287
|
+
self._println(f"{self._tone('system', '[turn.diff]')} {msg}")
|
|
288
|
+
|
|
289
|
+
def _render_summary_part(self, event: ResponseEvent) -> None:
|
|
290
|
+
if not self.show_reasoning:
|
|
291
|
+
return
|
|
292
|
+
self._close_assistant()
|
|
293
|
+
self._close_tool_output()
|
|
294
|
+
self._close_reasoning()
|
|
295
|
+
idx = event.summary_index
|
|
296
|
+
if idx is None:
|
|
297
|
+
self._println(self._tone("thinking", "[thinking] summary part added"))
|
|
298
|
+
else:
|
|
299
|
+
self._println(self._tone("thinking", f"[thinking] summary part #{idx}"))
|
|
300
|
+
|
|
301
|
+
def _render_agent_delta(self, event: ResponseEvent) -> None:
|
|
302
|
+
delta = event.text_delta or event.message_text
|
|
303
|
+
if not delta:
|
|
304
|
+
return
|
|
305
|
+
self._close_reasoning()
|
|
306
|
+
self._close_tool_output()
|
|
307
|
+
if not self._in_assistant_line:
|
|
308
|
+
self._print(self._tone("assistant", "Assistant: "))
|
|
309
|
+
self._in_assistant_line = True
|
|
310
|
+
self._print(delta)
|
|
311
|
+
|
|
312
|
+
def _render_item_completed(self, params: dict[str, Any], event: ResponseEvent) -> None:
|
|
313
|
+
item = params.get("item") if isinstance(params.get("item"), dict) else {}
|
|
314
|
+
if item.get("type") == "agentMessage":
|
|
315
|
+
if not self._in_assistant_line and event.message_text:
|
|
316
|
+
self._close_reasoning()
|
|
317
|
+
self._close_tool_output()
|
|
318
|
+
self._print(self._tone("assistant", "Assistant: "))
|
|
319
|
+
self._print(event.message_text)
|
|
320
|
+
self._in_assistant_line = True
|
|
321
|
+
return
|
|
322
|
+
if item.get("type") == "commandExecution":
|
|
323
|
+
self._close_inline_sections()
|
|
324
|
+
status = item.get("status")
|
|
325
|
+
exit_code = item.get("exitCode")
|
|
326
|
+
duration_ms = item.get("durationMs")
|
|
327
|
+
status_text = str(status) if isinstance(status, str) else "completed"
|
|
328
|
+
suffix: list[str] = [status_text]
|
|
329
|
+
if isinstance(exit_code, int):
|
|
330
|
+
suffix.append(f"exit={exit_code}")
|
|
331
|
+
if isinstance(duration_ms, int):
|
|
332
|
+
suffix.append(f"{duration_ms}ms")
|
|
333
|
+
self._println(f"{self._tone('tool', '[tool.exec.done]')} {', '.join(suffix)}")
|
|
334
|
+
return
|
|
335
|
+
if item.get("type") == "mcpToolCall":
|
|
336
|
+
self._close_inline_sections()
|
|
337
|
+
status = item.get("status")
|
|
338
|
+
status_text = str(status) if isinstance(status, str) else "completed"
|
|
339
|
+
self._println(f"{self._tone('tool', '[tool.mcp.done]')} {status_text}")
|
|
340
|
+
return
|
|
341
|
+
if item.get("type") == "fileChange":
|
|
342
|
+
self._close_inline_sections()
|
|
343
|
+
status = item.get("status")
|
|
344
|
+
status_text = str(status) if isinstance(status, str) else "completed"
|
|
345
|
+
self._println(f"{self._tone('tool', '[tool.patch.done]')} {status_text}")
|
|
346
|
+
return
|
|
347
|
+
if self.show_system:
|
|
348
|
+
self._close_inline_sections()
|
|
349
|
+
self._println(f"{self._tone('system', '[item]')} {item.get('type', 'unknown')} completed")
|
|
350
|
+
|
|
351
|
+
def finish(self) -> None:
|
|
352
|
+
self._close_inline_sections()
|
|
353
|
+
|
|
354
|
+
def _close_inline_sections(self) -> None:
|
|
355
|
+
self._close_assistant()
|
|
356
|
+
self._close_reasoning()
|
|
357
|
+
self._close_tool_output()
|
|
358
|
+
|
|
359
|
+
def _close_assistant(self) -> None:
|
|
360
|
+
if self._in_assistant_line:
|
|
361
|
+
self._println("")
|
|
362
|
+
self._in_assistant_line = False
|
|
363
|
+
|
|
364
|
+
def _close_reasoning(self) -> None:
|
|
365
|
+
if self._in_reasoning_line:
|
|
366
|
+
self._println("")
|
|
367
|
+
self._in_reasoning_line = False
|
|
368
|
+
|
|
369
|
+
def _close_tool_output(self) -> None:
|
|
370
|
+
if self._in_tool_output:
|
|
371
|
+
self._println("")
|
|
372
|
+
self._in_tool_output = False
|
|
373
|
+
|
|
374
|
+
def _print(self, text: str) -> None:
|
|
375
|
+
self.stream.write(text)
|
|
376
|
+
self.stream.flush()
|
|
377
|
+
|
|
378
|
+
def _println(self, text: str) -> None:
|
|
379
|
+
self.stream.write(text + "\n")
|
|
380
|
+
self.stream.flush()
|
|
381
|
+
|
|
382
|
+
def _label_for_item(self, item: dict[str, Any]) -> str:
|
|
383
|
+
item_type = item.get("type")
|
|
384
|
+
if item_type == "commandExecution":
|
|
385
|
+
return "exec"
|
|
386
|
+
if item_type == "fileChange":
|
|
387
|
+
return "patch"
|
|
388
|
+
if item_type == "webSearch":
|
|
389
|
+
return "web"
|
|
390
|
+
if item_type == "mcpToolCall":
|
|
391
|
+
server = item.get("server")
|
|
392
|
+
tool = item.get("tool")
|
|
393
|
+
if isinstance(server, str) and isinstance(tool, str):
|
|
394
|
+
return f"mcp:{server}/{tool}"
|
|
395
|
+
return "mcp"
|
|
396
|
+
if isinstance(item_type, str) and item_type:
|
|
397
|
+
return item_type
|
|
398
|
+
return "tool"
|
|
399
|
+
|
|
400
|
+
def _label_for_item_id(self, item_id: Any) -> str:
|
|
401
|
+
if isinstance(item_id, str):
|
|
402
|
+
return self._tool_items.get(item_id, "tool")
|
|
403
|
+
return "tool"
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _turn_status(params: dict[str, Any]) -> str:
|
|
407
|
+
turn = params.get("turn")
|
|
408
|
+
if isinstance(turn, dict):
|
|
409
|
+
status = turn.get("status")
|
|
410
|
+
if isinstance(status, str) and status:
|
|
411
|
+
return status
|
|
412
|
+
return "completed"
|
|
413
|
+
|
|
414
|
+
def _render_codex_event(self, method: str, params: dict[str, Any], event: ResponseEvent) -> None:
|
|
415
|
+
suffix = method.removeprefix("codex/event/")
|
|
416
|
+
|
|
417
|
+
# `codex/event/*` frequently duplicates canonical `item/*` events.
|
|
418
|
+
if suffix in {
|
|
419
|
+
"reasoning_content_delta",
|
|
420
|
+
"agent_reasoning_delta",
|
|
421
|
+
"agent_message_content_delta",
|
|
422
|
+
"item_started",
|
|
423
|
+
"item_completed",
|
|
424
|
+
"task_started",
|
|
425
|
+
"task_complete",
|
|
426
|
+
}:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
if suffix == "deprecation_notice":
|
|
430
|
+
self._render_deprecation_warning(method, params, event)
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
if suffix == "stream_error":
|
|
434
|
+
self._close_inline_sections()
|
|
435
|
+
msg = event.message_text or first_nonempty_text(params) or suffix
|
|
436
|
+
self._println(f"{self._tone('error', '[error]')} {msg}")
|
|
437
|
+
self._turn_error_count += 1
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
if self.show_system:
|
|
441
|
+
self._close_inline_sections()
|
|
442
|
+
msg = event.message_text or event.text_delta or first_nonempty_text(params) or ""
|
|
443
|
+
tail = f" {msg}" if msg else ""
|
|
444
|
+
self._println(f"{self._tone('system', f'[codex.{suffix}]')}{tail}")
|
|
445
|
+
|
|
446
|
+
def _render_deprecation_warning(self, source_method: str, params: dict[str, Any], event: ResponseEvent) -> None:
|
|
447
|
+
self._close_inline_sections()
|
|
448
|
+
message = self._format_deprecation_warning(source_method, params, event)
|
|
449
|
+
if not message:
|
|
450
|
+
return
|
|
451
|
+
key = self._normalize_deprecation_key(message)
|
|
452
|
+
if key in self._seen_deprecation_warnings:
|
|
453
|
+
return
|
|
454
|
+
self._seen_deprecation_warnings.add(key)
|
|
455
|
+
self._println(f"{self._tone('warning', '[warning]')} {message}")
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def _normalize_deprecation_key(message: str) -> str:
|
|
459
|
+
return re.sub(r"[^a-z0-9]+", "", message.lower())
|
|
460
|
+
|
|
461
|
+
@staticmethod
|
|
462
|
+
def _find_first_string_for_keys(value: Any, keys: tuple[str, ...]) -> str | None:
|
|
463
|
+
if isinstance(value, dict):
|
|
464
|
+
for key in keys:
|
|
465
|
+
candidate = value.get(key)
|
|
466
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
467
|
+
return candidate.strip()
|
|
468
|
+
for nested in value.values():
|
|
469
|
+
text = ExecStyleRenderer._find_first_string_for_keys(nested, keys)
|
|
470
|
+
if text:
|
|
471
|
+
return text
|
|
472
|
+
elif isinstance(value, list):
|
|
473
|
+
for item in value:
|
|
474
|
+
text = ExecStyleRenderer._find_first_string_for_keys(item, keys)
|
|
475
|
+
if text:
|
|
476
|
+
return text
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
def _format_deprecation_warning(self, source_method: str, params: dict[str, Any], event: ResponseEvent) -> str | None:
|
|
480
|
+
raw_message = event.message_text or first_nonempty_text(params) or ""
|
|
481
|
+
normalized = self._normalize_deprecation_key(raw_message)
|
|
482
|
+
if normalized and normalized not in {"deprecationnotice", "deprecated"}:
|
|
483
|
+
return raw_message
|
|
484
|
+
|
|
485
|
+
target = self._find_first_string_for_keys(
|
|
486
|
+
params,
|
|
487
|
+
(
|
|
488
|
+
"method",
|
|
489
|
+
"event",
|
|
490
|
+
"name",
|
|
491
|
+
"interface",
|
|
492
|
+
"api",
|
|
493
|
+
"path",
|
|
494
|
+
"deprecated",
|
|
495
|
+
),
|
|
496
|
+
)
|
|
497
|
+
replacement = self._find_first_string_for_keys(
|
|
498
|
+
params,
|
|
499
|
+
("replacement", "newMethod", "newEvent", "instead", "use"),
|
|
500
|
+
)
|
|
501
|
+
if target and replacement:
|
|
502
|
+
return f"{target} is deprecated; use {replacement}"
|
|
503
|
+
if target:
|
|
504
|
+
return f"{target} is deprecated"
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
def _tone(self, tone: str, text: str) -> str:
|
|
508
|
+
if not self._use_color:
|
|
509
|
+
return text
|
|
510
|
+
code = self._palette.get(tone)
|
|
511
|
+
if not code:
|
|
512
|
+
return text
|
|
513
|
+
return f"{code}{text}\x1b[0m"
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _normalize_color_mode(color: str) -> str:
|
|
517
|
+
mode = str(color or "auto").strip().lower()
|
|
518
|
+
alias = {"on": "soft", "true": "soft", "false": "off", "none": "off"}
|
|
519
|
+
mode = alias.get(mode, mode)
|
|
520
|
+
if mode not in {"auto", "off", "soft", "vivid"}:
|
|
521
|
+
return "auto"
|
|
522
|
+
return mode
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _compute_use_color(mode: str, stream: TextIO) -> bool:
|
|
526
|
+
if mode == "off":
|
|
527
|
+
return False
|
|
528
|
+
if mode in {"soft", "vivid"}:
|
|
529
|
+
return True
|
|
530
|
+
|
|
531
|
+
if os.environ.get("NO_COLOR") is not None:
|
|
532
|
+
return False
|
|
533
|
+
if os.environ.get("CLICOLOR_FORCE") == "1":
|
|
534
|
+
return True
|
|
535
|
+
|
|
536
|
+
isatty = getattr(stream, "isatty", None)
|
|
537
|
+
if callable(isatty):
|
|
538
|
+
try:
|
|
539
|
+
return bool(isatty())
|
|
540
|
+
except Exception:
|
|
541
|
+
return False
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def _palette_for(mode: str) -> dict[str, str]:
|
|
546
|
+
if mode == "vivid":
|
|
547
|
+
return {
|
|
548
|
+
"header": "\x1b[1;96m",
|
|
549
|
+
"session": "\x1b[1;36m",
|
|
550
|
+
"assistant": "\x1b[1;32m",
|
|
551
|
+
"thinking": "\x1b[1;35m",
|
|
552
|
+
"tool": "\x1b[1;33m",
|
|
553
|
+
"tool_output": "\x1b[1;94m",
|
|
554
|
+
"summary": "\x1b[1;34m",
|
|
555
|
+
"warning": "\x1b[1;93m",
|
|
556
|
+
"error": "\x1b[1;91m",
|
|
557
|
+
"system": "\x1b[2;37m",
|
|
558
|
+
"plan": "\x1b[1;34m",
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
"header": "\x1b[36m",
|
|
562
|
+
"session": "\x1b[36m",
|
|
563
|
+
"assistant": "\x1b[32m",
|
|
564
|
+
"thinking": "\x1b[35m",
|
|
565
|
+
"tool": "\x1b[33m",
|
|
566
|
+
"tool_output": "\x1b[34m",
|
|
567
|
+
"summary": "\x1b[34m",
|
|
568
|
+
"warning": "\x1b[33m",
|
|
569
|
+
"error": "\x1b[31m",
|
|
570
|
+
"system": "\x1b[2m",
|
|
571
|
+
"plan": "\x1b[34m",
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def render_exec_style_events(
|
|
576
|
+
events: Iterator[ResponseEvent],
|
|
577
|
+
*,
|
|
578
|
+
stream: TextIO | None = None,
|
|
579
|
+
show_reasoning: bool = True,
|
|
580
|
+
show_system: bool = False,
|
|
581
|
+
show_tool_output: bool = True,
|
|
582
|
+
show_turn_summary: bool = True,
|
|
583
|
+
color: str = "auto",
|
|
584
|
+
) -> None:
|
|
585
|
+
"""Render streaming events in a compact `codex exec`-like terminal style.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
events: Iterator from ``responses_events``.
|
|
589
|
+
stream: Output stream; defaults to ``sys.stdout``.
|
|
590
|
+
show_reasoning: Whether to show reasoning deltas.
|
|
591
|
+
show_system: Whether to show system events.
|
|
592
|
+
show_tool_output: Whether to show command/file output deltas.
|
|
593
|
+
show_turn_summary: Whether to print turn summary at completion.
|
|
594
|
+
color: ``auto``, ``off``, ``soft``, or ``vivid``.
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
renderer = ExecStyleRenderer(
|
|
598
|
+
stream=stream,
|
|
599
|
+
show_reasoning=show_reasoning,
|
|
600
|
+
show_system=show_system,
|
|
601
|
+
show_tool_output=show_tool_output,
|
|
602
|
+
show_turn_summary=show_turn_summary,
|
|
603
|
+
color=color,
|
|
604
|
+
)
|
|
605
|
+
for event in events:
|
|
606
|
+
renderer.render(event)
|
|
607
|
+
renderer.finish()
|