python-codex 0.1.6__tar.gz → 0.1.8__tar.gz

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 (104) hide show
  1. {python_codex-0.1.6 → python_codex-0.1.8}/AGENTS.md +1 -0
  2. {python_codex-0.1.6 → python_codex-0.1.8}/PKG-INFO +1 -1
  3. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/cli.py +40 -4
  4. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/base_tool.py +17 -0
  5. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/__init__.py +2 -0
  6. python_codex-0.1.8/pycodex/utils/debug.py +12 -0
  7. {python_codex-0.1.6 → python_codex-0.1.8}/pyproject.toml +1 -1
  8. python_codex-0.1.8/tests/test_py36_syntax.py +29 -0
  9. {python_codex-0.1.6 → python_codex-0.1.8}/.github/workflows/publish.yml +0 -0
  10. {python_codex-0.1.6 → python_codex-0.1.8}/.github/workflows/test.yml +0 -0
  11. {python_codex-0.1.6 → python_codex-0.1.8}/.gitignore +0 -0
  12. {python_codex-0.1.6 → python_codex-0.1.8}/LICENSE +0 -0
  13. {python_codex-0.1.6 → python_codex-0.1.8}/README.md +0 -0
  14. {python_codex-0.1.6 → python_codex-0.1.8}/README_ZH.md +0 -0
  15. {python_codex-0.1.6 → python_codex-0.1.8}/docs/ALIGNMENT.md +0 -0
  16. {python_codex-0.1.6 → python_codex-0.1.8}/docs/CONTEXT.md +0 -0
  17. {python_codex-0.1.6 → python_codex-0.1.8}/docs/responses_server/README.md +0 -0
  18. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/__init__.py +0 -0
  19. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/agent.py +0 -0
  20. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/collaboration.py +0 -0
  21. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/compat.py +0 -0
  22. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/context.py +0 -0
  23. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/doctor.py +0 -0
  24. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/model.py +0 -0
  25. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/portable.py +0 -0
  26. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/portable_server.py +0 -0
  27. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/collaboration_default.md +0 -0
  28. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/collaboration_plan.md +0 -0
  29. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/default_base_instructions.md +0 -0
  30. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/exec_tools.json +0 -0
  31. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/models.json +0 -0
  32. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
  33. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
  34. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
  35. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
  36. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
  37. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
  38. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
  39. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
  40. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/prompts/subagent_tools.json +0 -0
  41. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/protocol.py +0 -0
  42. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/runtime.py +0 -0
  43. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/runtime_services.py +0 -0
  44. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/__init__.py +0 -0
  45. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/agent_tool_schemas.py +0 -0
  46. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/apply_patch_tool.py +0 -0
  47. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/close_agent_tool.py +0 -0
  48. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/code_mode_manager.py +0 -0
  49. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/exec_command_tool.py +0 -0
  50. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/exec_runtime.js +0 -0
  51. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/exec_tool.py +0 -0
  52. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/grep_files_tool.py +0 -0
  53. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/list_dir_tool.py +0 -0
  54. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/read_file_tool.py +0 -0
  55. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/request_permissions_tool.py +0 -0
  56. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/request_user_input_tool.py +0 -0
  57. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/resume_agent_tool.py +0 -0
  58. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/send_input_tool.py +0 -0
  59. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/shell_command_tool.py +0 -0
  60. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/shell_tool.py +0 -0
  61. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/spawn_agent_tool.py +0 -0
  62. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/unified_exec_manager.py +0 -0
  63. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/update_plan_tool.py +0 -0
  64. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/view_image_tool.py +0 -0
  65. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/wait_agent_tool.py +0 -0
  66. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/wait_tool.py +0 -0
  67. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/web_search_tool.py +0 -0
  68. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/tools/write_stdin_tool.py +0 -0
  69. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/compactor.py +0 -0
  70. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/dotenv.py +0 -0
  71. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/get_env.py +0 -0
  72. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/random_ids.py +0 -0
  73. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/session_persist.py +0 -0
  74. {python_codex-0.1.6 → python_codex-0.1.8}/pycodex/utils/visualize.py +0 -0
  75. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/__init__.py +0 -0
  76. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/__main__.py +0 -0
  77. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/app.py +0 -0
  78. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/config.py +0 -0
  79. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/messages_api.py +0 -0
  80. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/payload_processors.py +0 -0
  81. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/server.py +0 -0
  82. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/session_store.py +0 -0
  83. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/stream_router.py +0 -0
  84. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/tools/__init__.py +0 -0
  85. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/tools/custom_adapter.py +0 -0
  86. {python_codex-0.1.6 → python_codex-0.1.8}/responses_server/tools/web_search.py +0 -0
  87. {python_codex-0.1.6 → python_codex-0.1.8}/tests/TESTS.md +0 -0
  88. {python_codex-0.1.6 → python_codex-0.1.8}/tests/__init__.py +0 -0
  89. {python_codex-0.1.6 → python_codex-0.1.8}/tests/compare_request_user_input_roundtrip.py +0 -0
  90. {python_codex-0.1.6 → python_codex-0.1.8}/tests/compare_steer_request_bodies.py +0 -0
  91. {python_codex-0.1.6 → python_codex-0.1.8}/tests/compare_tool_schemas.py +0 -0
  92. {python_codex-0.1.6 → python_codex-0.1.8}/tests/fake_responses_server.py +0 -0
  93. {python_codex-0.1.6 → python_codex-0.1.8}/tests/fakes.py +0 -0
  94. {python_codex-0.1.6 → python_codex-0.1.8}/tests/responses_server/fake_chat_completions_server.py +0 -0
  95. {python_codex-0.1.6 → python_codex-0.1.8}/tests/responses_server/test_server.py +0 -0
  96. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_agent.py +0 -0
  97. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_builtin_tools.py +0 -0
  98. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_cli.py +0 -0
  99. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_compactor.py +0 -0
  100. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_context.py +0 -0
  101. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_doctor.py +0 -0
  102. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_fake_responses_server.py +0 -0
  103. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_model.py +0 -0
  104. {python_codex-0.1.6 → python_codex-0.1.8}/tests/test_portable.py +0 -0
@@ -35,6 +35,7 @@
35
35
  - 当前 `exec` / `wait` 已有一个最小 code-mode 实现:底层通过 Node 子进程运行 JavaScript,自带 `text` / `image` / `store` / `load` / `exit` / `notify` / `yield_control` helper,并允许通过 `tools.<name>(...)` 调回本地已注册工具;`web_search` 当前作为 Responses API provider-native tool declaration 暴露给模型,不经过本地 ToolRegistry 执行。
36
36
  - 我们支持的 tools 必须和原版 Codex 内置 tools 一一对应:名称、定位、参数形状、交互模型、输出语义都应尽量对齐;不要混合多个原版工具的语义做一个“折中工具”。
37
37
  - 代码风格上不要使用 `*` 定义 keyword-only 参数;接口默认允许位置参数。
38
+ - runtime 包和会在 import 阶段执行的测试辅助代码需要保持 Python 3.6.2 语法兼容;不要引入 walrus `:=`、`match` 等仅 3.8+/3.10+ 可解析的新语法,哪怕分支在运行时不会走到。
38
39
  - 本仓库使用 `uv`;本地默认没有预装测试依赖,开始工作前先跑 `uv sync --dev`,验证用 `uv run pytest`。
39
40
  - 上游 Codex 的 `steer` 目前是 TUI 交互层能力:开启后 `Enter` 会立即提交,`Tab` 才是排队。对齐时优先盯“下一次发出去的请求体”而不是内部控制流;当前 `pycodex` 已把 steer/queue 语义下沉到 `AgentRuntime`,并让 `AgentLoop.run_turn(texts)` 一次接收一批 user texts,这样下一次请求的 `input` 可以把多个 steer 文本按顺序并到 history 尾部。`CliSessionView.handle_event()` 把 spinner 再次拉起。要保证输入不被遮挡,需要让 view 知道“当前正在输入”,并在 input-active 期间抑制这些事件对 spinner 的 resume。
40
41
  - steer 的最小交互反馈已经约定为两条显式状态文案:入队时打印 `[steer] queued: <prompt>`,该条 queued turn 真正开始执行时打印 `[steer] inserted: <prompt>`。后续如果补更复杂的 queue UI,也应保留这两个核心状态语义。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -7,6 +7,7 @@ import os
7
7
  import shlex
8
8
  import sys
9
9
  import tempfile
10
+ import traceback
10
11
  from dataclasses import asdict, replace
11
12
  from pathlib import Path
12
13
  from typing import Sequence
@@ -20,7 +21,7 @@ from .portable import bootstrap_called_home, upload_codex_home
20
21
  from .protocol import AgentEvent
21
22
  from .runtime import AgentRuntime
22
23
  from .runtime_services import RuntimeEnvironment, create_runtime_environment
23
- from .utils import CliSessionView, load_codex_dotenv, uuid7_string
24
+ from .utils import CliSessionView, get_debug_dir, load_codex_dotenv, uuid7_string
24
25
  from .utils.compactor import compact_agent_loop
25
26
  from .utils.session_persist import (
26
27
  SessionRolloutRecorder,
@@ -57,9 +58,9 @@ def configure_loguru() -> 'None':
57
58
  return
58
59
 
59
60
  logger.remove()
60
- log_path = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
61
- if log_path:
62
- logger.add(log_path, level="DEBUG")
61
+ debug_dir = get_debug_dir()
62
+ if debug_dir is not None:
63
+ logger.add(str(debug_dir / "loguru.log"), level="DEBUG")
63
64
  return
64
65
 
65
66
  if os.environ.get("PYCODEX_DEBUG_STDERR", "").strip().lower() in {
@@ -743,6 +744,8 @@ async def run_interactive_session(
743
744
  async def run_cli(args: 'argparse.Namespace') -> 'int':
744
745
  runtime = None
745
746
  worker = None
747
+ debug_dir = get_debug_dir()
748
+ phase_handle = None if debug_dir is None else (debug_dir / "phase.log").open("a", encoding="utf-8")
746
749
  try:
747
750
  if args.put is not None and args.call:
748
751
  raise ValueError("--put and --call cannot be combined")
@@ -762,9 +765,18 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
762
765
  print(f"pycodex --call {shlex.quote(call_spec)}", flush=True)
763
766
  return 0
764
767
  if args.call:
768
+ if phase_handle is not None:
769
+ phase_handle.write("bootstrap_called_home:start\n")
770
+ phase_handle.flush()
765
771
  config_path = bootstrap_called_home(args.call)
772
+ if phase_handle is not None:
773
+ phase_handle.write("bootstrap_called_home:done\n")
774
+ phase_handle.flush()
766
775
  args.config = str(config_path)
767
776
  os.environ["CODEX_HOME"] = str(config_path.parent)
777
+ if phase_handle is not None:
778
+ phase_handle.write("build_model_client:start\n")
779
+ phase_handle.flush()
768
780
  client = _build_model_client(
769
781
  args.config,
770
782
  args.profile,
@@ -773,7 +785,13 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
773
785
  use_chat_completion=args.use_chat_completion,
774
786
  use_messages=args.use_messages,
775
787
  )
788
+ if phase_handle is not None:
789
+ phase_handle.write("build_model_client:done\n")
790
+ phase_handle.flush()
776
791
 
792
+ if phase_handle is not None:
793
+ phase_handle.write("build_runtime:start\n")
794
+ phase_handle.flush()
777
795
  runtime = build_runtime(
778
796
  args.config,
779
797
  args.profile,
@@ -781,6 +799,9 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
781
799
  client,
782
800
  session_mode="tui",
783
801
  )
802
+ if phase_handle is not None:
803
+ phase_handle.write("build_runtime:done\n")
804
+ phase_handle.flush()
784
805
  if should_run_interactive(args.prompt, sys.stdin.isatty()):
785
806
  return await run_interactive_session(
786
807
  runtime,
@@ -790,13 +811,28 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
790
811
  else:
791
812
  prompt_text = resolve_prompt_text(args.prompt)
792
813
  worker = asyncio.create_task(runtime.run_forever())
814
+ if phase_handle is not None:
815
+ phase_handle.write("submit_user_turn:start\n")
816
+ phase_handle.flush()
793
817
  result = await runtime.submit_user_turn(prompt_text)
818
+ if phase_handle is not None:
819
+ phase_handle.write("submit_user_turn:done\n")
820
+ phase_handle.flush()
794
821
  print(format_turn_output(result, args.json))
795
822
  return 0
796
823
  except Exception as exc:
824
+ if phase_handle is not None:
825
+ phase_handle.write("fatal_exception\n")
826
+ phase_handle.flush()
827
+ if debug_dir is not None:
828
+ (debug_dir / "fatal_error.txt").write_text(
829
+ traceback.format_exc(), encoding="utf-8"
830
+ )
797
831
  print(f"Error: {exc}", file=sys.stderr)
798
832
  return 1
799
833
  finally:
834
+ if phase_handle is not None:
835
+ phase_handle.close()
800
836
  if runtime is not None and worker is not None:
801
837
  await runtime.shutdown()
802
838
  await worker
@@ -16,8 +16,10 @@ from dataclasses import dataclass
16
16
  from functools import lru_cache
17
17
  import json
18
18
  from pathlib import Path
19
+ import traceback
19
20
 
20
21
  from ..protocol import ConversationItem, JSONDict, JSONValue, ToolCall, ToolResult, ToolSpec
22
+ from ..utils import get_debug_dir
21
23
  import typing
22
24
 
23
25
  EXEC_TOOLS_SNAPSHOT_PATH = (
@@ -140,6 +142,21 @@ class ToolRegistry:
140
142
  tool_type=call.tool_type,
141
143
  )
142
144
  except Exception as exc: # pragma: no cover - defensive wrapper
145
+ debug_dir = get_debug_dir()
146
+ if debug_dir is not None:
147
+ with (debug_dir / "tool_errors.jsonl").open("a", encoding="utf-8") as handle:
148
+ handle.write(
149
+ json.dumps(
150
+ {
151
+ "tool": call.name,
152
+ "call_id": call.call_id,
153
+ "error": f"{type(exc).__name__}: {exc}",
154
+ "traceback": traceback.format_exc(),
155
+ },
156
+ ensure_ascii=False,
157
+ )
158
+ )
159
+ handle.write("\n")
143
160
  return ToolResult(
144
161
  call_id=call.call_id,
145
162
  name=call.name,
@@ -1,4 +1,5 @@
1
1
  from .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
2
+ from .debug import get_debug_dir
2
3
  from .get_env import build_user_agent, get_shell_name, get_timezone_name
3
4
  from .random_ids import uuid7_string
4
5
  from .compactor import DEFAULT_COMPACT_PROMPT, SUMMARY_PREFIX, compact
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "format_cli_plan_messages",
32
33
  "format_cli_tool_call_message",
33
34
  "format_cli_tool_message",
35
+ "get_debug_dir",
34
36
  "get_shell_name",
35
37
  "get_timezone_name",
36
38
  "load_codex_dotenv",
@@ -0,0 +1,12 @@
1
+ import os
2
+ from pathlib import Path
3
+ import typing
4
+
5
+
6
+ def get_debug_dir() -> 'typing.Union[Path, None]':
7
+ value = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
8
+ if not value:
9
+ return None
10
+ path = Path(value).expanduser()
11
+ path.mkdir(parents=True, exist_ok=True)
12
+ return path
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-codex"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  description = "A minimal Python extraction of Codex's main agent loop"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6.2"
@@ -0,0 +1,29 @@
1
+ import ast
2
+ import os
3
+ import sys
4
+
5
+ import pytest
6
+
7
+
8
+ def _iter_python_files():
9
+ for root in ["pycodex", "responses_server", "tests"]:
10
+ for dirpath, _dirnames, filenames in os.walk(root):
11
+ for filename in filenames:
12
+ if filename.endswith(".py"):
13
+ yield os.path.join(dirpath, filename)
14
+
15
+
16
+ def test_python_sources_parse_with_python36_grammar():
17
+ if sys.version_info < (3, 8):
18
+ pytest.skip("ast feature_version requires Python 3.8+")
19
+
20
+ failures = []
21
+ for path in sorted(_iter_python_files()):
22
+ with open(path, "r", encoding="utf-8") as handle:
23
+ source = handle.read()
24
+ try:
25
+ ast.parse(source, filename=path, feature_version=(3, 6))
26
+ except SyntaxError as exc:
27
+ failures.append("%s:%s: %s" % (path, exc.lineno, exc.msg))
28
+
29
+ assert not failures, "Python 3.6-incompatible syntax found:\n%s" % "\n".join(failures)
File without changes
File without changes
File without changes
File without changes