python-codex 0.4.5__py3-none-any.whl → 0.4.7__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/cli.py CHANGED
@@ -19,7 +19,7 @@ from prompt_toolkit.patch_stdout import patch_stdout
19
19
 
20
20
  from .bootstrap import build_agent, build_model, build_runtime, configure_loguru
21
21
  from .events import DEFAULT_MAIN_PROMPT, EventDisplay
22
- from .model import DEFAULT_CODEX_CONFIG_PATH
22
+ from .model import DEFAULT_CODEX_CONFIG_PATH, ModelControl
23
23
  from .portable import bootstrap_called_home, upload_codex_home
24
24
  from .utils import get_debug_dir
25
25
  from .utils.event_helpers import format_error, render_result
@@ -216,7 +216,10 @@ async def run_interactive_session(runtime, json_mode, config_path=None, view=Non
216
216
  if not future.cancelled() and future.exception() is None:
217
217
  render_result("turn", future.result(), True, view.write_line)
218
218
 
219
- view.display.start(runtime.commands())
219
+ client = typing.cast(ModelControl, runtime.agent.model_client)
220
+ view.display.start(
221
+ client.model, client.reasoning_effort, client.endpoint, runtime.commands()
222
+ )
220
223
  with _cli_sigint_handler(runtime) as install_sigint_handler:
221
224
  try:
222
225
  while not view.display.closed:
pycodex/events.py CHANGED
@@ -48,11 +48,14 @@ class EventDisplay:
48
48
  self.closed = False
49
49
  self.set_context_window_tokens(context_window_tokens)
50
50
 
51
- def start(self, commands):
51
+ def start(self, model, reasoning_effort, endpoint, commands):
52
+ self.log("Welcome to PyCodex")
52
53
  self.log(
53
- "pycodex interactive mode. Type /exit or press Ctrl+C to quit; "
54
- "press Ctrl+C again while closing to force exit."
54
+ "model: {0} | effort: {1}".format(
55
+ model, reasoning_effort or "provider default"
56
+ )
55
57
  )
58
+ self.log("endpoint: " + endpoint)
56
59
  self.log(format_command_result({"kind": "help", "commands": commands}))
57
60
 
58
61
  def write(self, text, kind=""):
pycodex/model.py CHANGED
@@ -67,6 +67,11 @@ class ModelClient(Protocol):
67
67
 
68
68
  class ModelControl(Protocol):
69
69
  model: "str"
70
+ reasoning_effort: "typing.Union[str, None]"
71
+
72
+ @property
73
+ def endpoint(self) -> "str":
74
+ """Return the active API base URL, safe for display."""
70
75
 
71
76
  async def list_models(self) -> "typing.List[str]":
72
77
  """List the models available to an interactive session."""
@@ -398,6 +403,27 @@ class ResponsesModelClient:
398
403
  def model(self, model: "str") -> "None":
399
404
  self._config = replace(self._config, model=model)
400
405
 
406
+ @property
407
+ def reasoning_effort(self) -> "typing.Union[str, None]":
408
+ """Return the effective effort; assign None to restore the model default."""
409
+ return self._config.effective_reasoning_effort()
410
+
411
+ @reasoning_effort.setter
412
+ def reasoning_effort(self, effort: "typing.Union[str, None]") -> "None":
413
+ if effort is not None and (
414
+ not effort or any(char.isspace() for char in effort)
415
+ ):
416
+ raise ValueError("reasoning effort must be a single non-empty value")
417
+ self._config = replace(self._config, reasoning_effort=effort)
418
+
419
+ @property
420
+ def endpoint(self) -> "str":
421
+ """Return the active API base URL without credentials, query or fragment."""
422
+ url = urllib.parse.urlsplit(self._config.base_url)
423
+ return urllib.parse.urlunsplit(
424
+ (url.scheme, url.netloc.rsplit("@", 1)[-1], url.path, "", "")
425
+ )
426
+
401
427
  @classmethod
402
428
  def from_codex_config(
403
429
  cls,
pycodex/runtime.py CHANGED
@@ -25,6 +25,7 @@ from .events import (
25
25
  TurnInterruptedEvent,
26
26
  TurnStartedEvent,
27
27
  )
28
+ from .model import ModelControl
28
29
  from .protocol import TurnResult
29
30
  from .utils import uuid7_string
30
31
  from .utils.event_helpers import shorten_title
@@ -124,12 +125,13 @@ class AgentRuntime:
124
125
  "history",
125
126
  "title",
126
127
  "model",
128
+ "effort",
127
129
  "resume",
128
130
  "compact",
129
131
  "fork",
130
132
  )
131
133
  + tuple(self.command_handlers)
132
- + ("queue", "exit", "quit")
134
+ + ("queue",)
133
135
  )
134
136
 
135
137
  def snapshot(self):
@@ -141,7 +143,10 @@ class AgentRuntime:
141
143
  "recorded_rollout_path": (
142
144
  str(recorded_path) if recorded_path is not None else None
143
145
  ),
144
- "model": self.agent.model_name,
146
+ "model": "{0}:{1}".format(
147
+ self.agent.model_name,
148
+ getattr(self.agent.model_client, "reasoning_effort", None) or "default",
149
+ ),
145
150
  "title": self.title,
146
151
  "history": conversation_history_to_turns(self.agent.history),
147
152
  "busy": self.is_busy,
@@ -250,13 +255,10 @@ class AgentRuntime:
250
255
  submission_id = uuid7_string()
251
256
  future = asyncio.get_running_loop().create_future()
252
257
  try:
253
- if command in {"/exit", "/quit"}:
258
+ async with self._get_command_lock():
259
+ if not self.agent.accepts_input:
260
+ raise RuntimeError("agent is shutting down")
254
261
  result = await self._execute_command(command[1:], argument.strip())
255
- else:
256
- async with self._get_command_lock():
257
- if not self.agent.accepts_input:
258
- raise RuntimeError("agent is shutting down")
259
- result = await self._execute_command(command[1:], argument.strip())
260
262
  except Exception as exc:
261
263
  future.set_exception(exc)
262
264
  self._publish(CommandFailedEvent(submission_id, command, str(exc), sender))
@@ -267,10 +269,7 @@ class AgentRuntime:
267
269
  return SubmittedInput(submission_id, "command", future)
268
270
 
269
271
  async def _execute_command(self, command, argument):
270
- if (
271
- command in {"help", "history", "compact", "fork", "exit", "quit"}
272
- and argument
273
- ):
272
+ if command in {"help", "history", "compact", "fork"} and argument:
274
273
  raise ValueError("Usage: /{0}".format(command))
275
274
  if command == "help":
276
275
  return {"kind": "help", "commands": self.commands()}
@@ -292,6 +291,17 @@ class AgentRuntime:
292
291
  "model": self.agent.model_name,
293
292
  "models": await self.agent.model_client.list_models(),
294
293
  }
294
+ if command == "effort":
295
+ client = typing.cast(ModelControl, self.agent.model_client)
296
+ effort = client.reasoning_effort
297
+ if argument:
298
+ if len(argument.split()) != 1:
299
+ raise ValueError("Usage: /effort [<value>|default]")
300
+ self.require_idle("change reasoning effort")
301
+ client.reasoning_effort = None if argument == "default" else argument
302
+ effort = client.reasoning_effort
303
+ self.publish_state("effort")
304
+ return {"kind": "effort", "reasoning_effort": effort}
295
305
  if command == "resume":
296
306
  if not argument:
297
307
  return {
@@ -317,9 +327,6 @@ class AgentRuntime:
317
327
  if command == "fork":
318
328
  self.fork()
319
329
  return {"kind": "forked", "session_id": self.agent.session_id}
320
- if command in {"exit", "quit"}:
321
- await self.close()
322
- return {"kind": "closed"}
323
330
  handler = self.command_handlers.get(command)
324
331
  if handler is None:
325
332
  raise ValueError("Unknown command: /{0}".format(command))
@@ -35,6 +35,10 @@ def format_command_result(result: "typing.Dict[str, object]") -> "str":
35
35
  )
36
36
  if kind == "model_changed":
37
37
  return "Switched model to {0}.".format(result["model"])
38
+ if kind == "effort":
39
+ return "Reasoning effort: {0}".format(
40
+ result["reasoning_effort"] or "provider default"
41
+ )
38
42
  if kind == "sessions":
39
43
  if not result["sessions"]:
40
44
  return "No resumable sessions found."
@@ -67,8 +71,6 @@ def format_command_result(result: "typing.Dict[str, object]") -> "str":
67
71
  )
68
72
  if kind == "forked":
69
73
  return "Forked session: " + result["session_id"]
70
- if kind == "closed":
71
- return ""
72
74
  return "\n".join(result["lines"])
73
75
 
74
76
 
@@ -163,6 +163,7 @@ class SessionRolloutRecorder:
163
163
  def list_resumable_sessions(
164
164
  codex_home: "Path",
165
165
  limit: "int" = 20,
166
+ session_id: "typing.Union[str, None]" = None,
166
167
  ) -> "typing.Tuple[typing.Dict[str, str], ...]":
167
168
  latest_rollouts_by_id: "typing.Dict[str, Path]" = {}
168
169
  for dirname in ROLLUP_SESSION_DIRNAMES:
@@ -173,6 +174,8 @@ def list_resumable_sessions(
173
174
  thread_id = _thread_id_from_rollout_path(path)
174
175
  if thread_id is None:
175
176
  continue
177
+ if session_id is not None and thread_id.lower() != session_id.lower():
178
+ continue
176
179
  previous = latest_rollouts_by_id.get(thread_id)
177
180
  if previous is None or path.stat().st_mtime > previous.stat().st_mtime:
178
181
  latest_rollouts_by_id[thread_id] = path
@@ -202,11 +205,16 @@ def list_resumable_sessions(
202
205
 
203
206
  def select_resumable_session(
204
207
  codex_home: "Path",
205
- resume_index_text: "str",
208
+ resume_target: "str",
206
209
  ) -> "typing.Dict[str, str]":
207
- normalized_target = resume_index_text.strip()
210
+ normalized_target = resume_target.strip()
211
+ if UUID_PATTERN.fullmatch(normalized_target):
212
+ sessions = list_resumable_sessions(codex_home, 1, normalized_target)
213
+ if not sessions:
214
+ raise ValueError(f"Session not found: {normalized_target}")
215
+ return sessions[0]
208
216
  if not normalized_target.isdigit():
209
- raise ValueError("Usage: /resume <number>")
217
+ raise ValueError("Usage: /resume <number|session_id>")
210
218
 
211
219
  sessions = list_resumable_sessions(codex_home)
212
220
  resume_index = int(normalized_target)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.4.5
3
+ Version: 0.4.7
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.6.2
@@ -369,22 +369,28 @@ Current behavior:
369
369
  Open views survive reloads and server restarts. See
370
370
  [workspace content views](docs/RUNTIME.md#workspace-content-views) for the
371
371
  complete syntax and limits.
372
- - interactive mode exits through `/exit`, `/quit`, Ctrl+D on an empty prompt,
373
- or a single Ctrl+C; accepted work finishes before cleanup and normal exit.
372
+ - interactive mode exits through Ctrl+D on an empty prompt or a single Ctrl+C;
373
+ accepted work finishes before cleanup and normal exit.
374
374
  A `[closing]` message explains the wait. Press Ctrl+C again while closing
375
375
  to immediately exit the CLI with status 130. This skips remaining cleanup;
376
376
  external tool processes may keep running.
377
377
  - interactive mode shows a compact event stream for user-visible phases such as
378
378
  tool execution and model follow-up after tool results
379
+ - startup shows a welcome, model, effort, endpoint and available commands.
379
380
  - assistant text is printed from streaming deltas directly
380
- - interactive mode supports `/history`, `/title`, `/model`, `/resume`, `/compact`,
381
- and `/fork`
381
+ - interactive mode supports `/history`, `/title`, `/model`, `/effort`, `/resume`,
382
+ `/compact`, and `/fork`
382
383
  - `/model <name>` switches the model used by later turns in the current
383
384
  interactive session; `/model` shows the current model and available choices
385
+ - `/effort low` switches effort while idle; `/effort` shows the current value,
386
+ and `/effort default` restores the model default. Frontend labels show
387
+ `model:effort`.
384
388
  - `/resume` with no argument lists the currently resumable sessions by their
385
389
  first user-message preview; `/resume 1` resumes the first listed session
386
- - `/resume <number>` replaces the in-memory history with the selected recorded
387
- Codex rollout from `CODEX_HOME/sessions`. Tool calls without recorded results
390
+ - `/resume <session_id>` accepts a full session UUID, including sessions older
391
+ than the displayed list. Both forms search `CODEX_HOME/sessions` and
392
+ `CODEX_HOME/archived_sessions` and replace the in-memory history with the
393
+ selected recorded Codex rollout. Tool calls without recorded results
388
394
  are omitted during restore; later saved messages and completed tools remain
389
395
  available, and the source file is left intact.
390
396
  - `/compact` synthesizes a local handoff summary, replaces the in-memory
@@ -2,12 +2,12 @@ pycodex/__init__.py,sha256=fHEyhylULeRo-5xEnl-UxT5ZpBzbFzRW7vLwdy_7oxU,2660
2
2
  pycodex/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
3
  pycodex/agent.py,sha256=GF-mNTnxHW8qw-72QEXfGGvExZzl8c3m6emZBux6ddw,21418
4
4
  pycodex/bootstrap.py,sha256=U1X5kwORpS5JnPk9HhIiOVWPQzDLlk_ro_cY2knwL8I,12421
5
- pycodex/cli.py,sha256=MLvXpVwrb-HJuVWmPFmNMTR-4cqR3X08ZfAevYlVSQI,12646
5
+ pycodex/cli.py,sha256=uCrRARI2IypIU-1aC0Y2DLzhzYGWQta1f7zYWsyaNnU,12797
6
6
  pycodex/context.py,sha256=yfNY2alT3caQQwhay47pSSrGUbgEAcLKhTVaZqtQ4yU,26610
7
- pycodex/events.py,sha256=wysYXHLiCHzW3k1yaAuoRTw90NRhHPa3jtY0ANGOqH4,29694
8
- pycodex/model.py,sha256=3mWXTN-oV2Q50SgQYac45WT5Qk2NSmPTxSWyF-w8TRg,44583
7
+ pycodex/events.py,sha256=L7RkZ3EtFQOfWg1FIG2Y0mLpDKTKV6HGmXMy9HCoWX0,29794
8
+ pycodex/model.py,sha256=VwPN9GEdoL8N8V4hFdMmJ1rGxL_tom3Nq7ZBonCum2k,45648
9
9
  pycodex/protocol.py,sha256=uzUz_06iA4m9vAoqZeCGiJ4SocKZDzOfCJUU4Bpt5dI,12033
10
- pycodex/runtime.py,sha256=3wHKoNjwzOgE6dXeT6Vbpu6Nf7RWqIfHwrjsmOnk81I,23814
10
+ pycodex/runtime.py,sha256=M_gvWDvsZBivELjNkuhmwiZVRmlaQmzKZK1ZQ5-kLLE,24271
11
11
  pycodex/portable/__init__.py,sha256=FmspRPmblYuZIVXMpXaxE1uosCaz2hZDpkpOzxDJQic,1084
12
12
  pycodex/portable/client.py,sha256=DOxzZljVfN1ynmhqrChX7NsT1IFSFVKzEn1J3wvV3XQ,16038
13
13
  pycodex/portable/server.py,sha256=w4YD71Sxb7MD5q57GPuIgx7AoicTv0R9xzX2kAaibPA,7503
@@ -57,12 +57,12 @@ pycodex/utils/crypto.py,sha256=HRCXF26Ft3S8CG2X0YpZwa8GMU9YJ0Loo8Q9PMPMcNw,1249
57
57
  pycodex/utils/debug.py,sha256=5XMpWpwFSm6woyx6RGXvN2qL0afRtTQbwlOAv-LotTw,913
58
58
  pycodex/utils/doctor.py,sha256=fI66oQqB5zNL7aEpFbiHnzhELyp9XydwMyAGznw5YSE,10616
59
59
  pycodex/utils/dotenv.py,sha256=h7ZgvqpEswetqjhtwfiaHyokAHwNKPakizTAHPHz5Ds,1865
60
- pycodex/utils/event_helpers.py,sha256=pnxL0-nBC7cnOheHPCIfkHqJjMbkJg3C348_EzAVhFA,6342
60
+ pycodex/utils/event_helpers.py,sha256=EJToMDBcoxSYN71IUyZu_OEb0T0yXU4InYrowYon-OQ,6442
61
61
  pycodex/utils/get_env.py,sha256=L4S8h84tkgxZKvrxY7ezTgePbXepG-yIigUTKeY6uuE,6094
62
62
  pycodex/utils/image_utils.py,sha256=OUzUs0Cow4L8Z2np0EBeEUotAqqsCmRmhrV8AxZUXRw,2460
63
63
  pycodex/utils/model_metadata.py,sha256=uK3iI4yC7Ge7yN4I1_gQYalQ4MYKddz2qMwsmQb-P80,1337
64
64
  pycodex/utils/random_ids.py,sha256=jLTxZZ0MBbRdQOFDFWGVRjWzIo6FKSQ5ADXbQSLPp1s,379
65
- pycodex/utils/session_persist.py,sha256=_7Lz1jO7D0Qf16UVuZBKU_Si-k_cUVinnLMlsd1jVj4,23600
65
+ pycodex/utils/session_persist.py,sha256=P-tsYdb1r0Ca_mVgTcK24bZM09zR-tNFgoFXlVIn5Q0,24012
66
66
  pycodex/utils/truncation.py,sha256=5D6vAorT8EgXgjd7ASwtraAyIZdlCR-w5jKZqJxDzho,5446
67
67
  pycodex/utils/_aes_gcm/LICENSE,sha256=KvdHagY3sYIK34GldJaRi34sNnGwMVJIIoDhbC9MRu8,2805
68
68
  pycodex/utils/_aes_gcm/README.md,sha256=D9XGeewCgU2PVqJCO6WusUs0Mw2FbMnABCqaVktqCnE,3045
@@ -83,8 +83,8 @@ responses_server/trajectory_dump.py,sha256=5_Wy6tslszrqx3j1JeP_boyTbzIicKy8No84P
83
83
  responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
84
84
  responses_server/tools/custom_adapter.py,sha256=SnIiNUdomff3APtTriURAC9YCeEdhCDjzGMEYusfUdk,8340
85
85
  responses_server/tools/web_search.py,sha256=pnKH9JyTScRehWpZS3HpXRvjWsQvC0QZqxjEOCT4ndg,8936
86
- python_codex-0.4.5.dist-info/METADATA,sha256=PQFmREYi8DYVVp0Vyvau2QELSyFOCcvYedMt2DqHcfA,47674
87
- python_codex-0.4.5.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
88
- python_codex-0.4.5.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
89
- python_codex-0.4.5.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
90
- python_codex-0.4.5.dist-info/RECORD,,
86
+ python_codex-0.4.7.dist-info/METADATA,sha256=vJgPquHMbmWwNuGdS_iP1KP8vvS4rApkjHPtKMfJU0M,48049
87
+ python_codex-0.4.7.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
88
+ python_codex-0.4.7.dist-info/entry_points.txt,sha256=sNUVakoVuTrzJH505ZgRTQxmtRRPUHV_EH0i6EbYTyM,45
89
+ python_codex-0.4.7.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
90
+ python_codex-0.4.7.dist-info/RECORD,,