python-codex 0.0.1__py3-none-any.whl → 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.
Files changed (62) hide show
  1. pycodex/__init__.py +139 -2
  2. pycodex/agent.py +290 -0
  3. pycodex/cli.py +641 -0
  4. pycodex/collaboration.py +21 -0
  5. pycodex/context.py +580 -0
  6. pycodex/doctor.py +360 -0
  7. pycodex/model.py +533 -0
  8. pycodex/prompts/collaboration_default.md +11 -0
  9. pycodex/prompts/collaboration_plan.md +128 -0
  10. pycodex/prompts/default_base_instructions.md +275 -0
  11. pycodex/prompts/exec_tools.json +411 -0
  12. pycodex/prompts/models.json +847 -0
  13. pycodex/prompts/permissions/approval_policy/never.md +1 -0
  14. pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
  15. pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
  16. pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
  17. pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
  18. pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
  19. pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
  20. pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
  21. pycodex/prompts/subagent_tools.json +163 -0
  22. pycodex/protocol.py +347 -0
  23. pycodex/runtime.py +200 -0
  24. pycodex/runtime_services.py +408 -0
  25. pycodex/tools/__init__.py +58 -0
  26. pycodex/tools/agent_tool_schemas.py +70 -0
  27. pycodex/tools/apply_patch_tool.py +363 -0
  28. pycodex/tools/base_tool.py +168 -0
  29. pycodex/tools/close_agent_tool.py +55 -0
  30. pycodex/tools/code_mode_manager.py +519 -0
  31. pycodex/tools/exec_command_tool.py +96 -0
  32. pycodex/tools/exec_runtime.js +161 -0
  33. pycodex/tools/exec_tool.py +48 -0
  34. pycodex/tools/grep_files_tool.py +150 -0
  35. pycodex/tools/list_dir_tool.py +135 -0
  36. pycodex/tools/read_file_tool.py +217 -0
  37. pycodex/tools/request_permissions_tool.py +95 -0
  38. pycodex/tools/request_user_input_tool.py +167 -0
  39. pycodex/tools/resume_agent_tool.py +56 -0
  40. pycodex/tools/send_input_tool.py +106 -0
  41. pycodex/tools/shell_command_tool.py +107 -0
  42. pycodex/tools/shell_tool.py +112 -0
  43. pycodex/tools/spawn_agent_tool.py +97 -0
  44. pycodex/tools/unified_exec_manager.py +380 -0
  45. pycodex/tools/update_plan_tool.py +79 -0
  46. pycodex/tools/view_image_tool.py +111 -0
  47. pycodex/tools/wait_agent_tool.py +75 -0
  48. pycodex/tools/wait_tool.py +68 -0
  49. pycodex/tools/web_search_tool.py +30 -0
  50. pycodex/tools/write_stdin_tool.py +75 -0
  51. pycodex/utils/__init__.py +40 -0
  52. pycodex/utils/dotenv.py +64 -0
  53. pycodex/utils/get_env.py +218 -0
  54. pycodex/utils/random_ids.py +19 -0
  55. pycodex/utils/visualize.py +978 -0
  56. python_codex-0.1.0.dist-info/METADATA +267 -0
  57. python_codex-0.1.0.dist-info/RECORD +60 -0
  58. python_codex-0.1.0.dist-info/entry_points.txt +2 -0
  59. python_codex-0.1.0.dist-info/licenses/LICENSE +201 -0
  60. python_codex-0.0.1.dist-info/METADATA +0 -30
  61. python_codex-0.0.1.dist-info/RECORD +0 -4
  62. {python_codex-0.0.1.dist-info → python_codex-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,79 @@
1
+ """`update_plan` tool for the Python Codex prototype.
2
+
3
+ Original Codex mapping:
4
+ - Corresponds to the original Codex `update_plan` tool.
5
+
6
+ Expected behavior:
7
+ - Give the model a structured checklist with optional explanation text.
8
+ - Validate that at most one step is `in_progress`.
9
+ - Persist the latest plan in local runtime state and return the standard
10
+ confirmation text Codex uses.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ..protocol import JSONDict, JSONValue
16
+ from ..runtime_services import PlanItem, PlanStore
17
+ from .base_tool import BaseTool, ToolContext
18
+
19
+ VALID_PLAN_STATUSES = {"pending", "in_progress", "completed"}
20
+
21
+
22
+ class UpdatePlanTool(BaseTool):
23
+ name = "update_plan"
24
+ description = (
25
+ "Updates the task plan. Provide an optional explanation and a list of "
26
+ "plan items, each with a step and status. At most one step can be "
27
+ "in_progress at a time."
28
+ )
29
+ input_schema = {
30
+ "type": "object",
31
+ "properties": {
32
+ "explanation": {"type": "string"},
33
+ "plan": {
34
+ "type": "array",
35
+ "description": "The list of steps",
36
+ "items": {
37
+ "type": "object",
38
+ "properties": {
39
+ "step": {"type": "string"},
40
+ "status": {
41
+ "type": "string",
42
+ "description": "One of: pending, in_progress, completed",
43
+ },
44
+ },
45
+ "required": ["step", "status"],
46
+ "additionalProperties": False,
47
+ },
48
+ },
49
+ },
50
+ "required": ["plan"],
51
+ "additionalProperties": False,
52
+ }
53
+ supports_parallel = False
54
+
55
+ def __init__(self, plan_store: PlanStore) -> None:
56
+ self._plan_store = plan_store
57
+
58
+ async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
59
+ del context
60
+ raw_plan = args.get("plan")
61
+ if not isinstance(raw_plan, list):
62
+ return "Error: `plan` must be a list."
63
+
64
+ plan_items: list[PlanItem] = []
65
+ for item in raw_plan:
66
+ if not isinstance(item, dict):
67
+ return "Error: each `plan` item must be an object."
68
+ step = str(item.get("step", "")).strip()
69
+ status = str(item.get("status", "")).strip()
70
+ if not step:
71
+ return "Error: each `plan` item must include a non-empty `step`."
72
+ if status not in VALID_PLAN_STATUSES:
73
+ return f"Error: invalid plan status `{status}`."
74
+ plan_items.append(PlanItem(step=step, status=status))
75
+
76
+ explanation_value = args.get("explanation")
77
+ explanation = None if explanation_value in (None, "") else str(explanation_value)
78
+ self._plan_store.update(explanation, tuple(plan_items))
79
+ return "Plan updated"
@@ -0,0 +1,111 @@
1
+ """`view_image` tool for the Python Codex prototype.
2
+
3
+ Original Codex mapping:
4
+ - Corresponds to the original Codex `view_image` tool.
5
+
6
+ Expected behavior:
7
+ - Load a local image file and turn it into a data URL that can be attached back
8
+ into the next model request.
9
+ - Accept the documented `path` argument plus the optional `detail: "original"`
10
+ hint.
11
+ - Return both the JSON object result and the structured `input_image` content
12
+ item that Codex uses when feeding image tool output back to the model.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import mimetypes
19
+ from pathlib import Path
20
+
21
+ from ..protocol import JSONDict, JSONValue
22
+ from .base_tool import BaseTool, StructuredToolOutput, ToolContext
23
+
24
+ VIEW_IMAGE_OUTPUT_SCHEMA = {
25
+ "type": "object",
26
+ "properties": {
27
+ "image_url": {
28
+ "type": "string",
29
+ "description": "Data URL for the loaded image.",
30
+ },
31
+ "detail": {
32
+ "type": ["string", "null"],
33
+ "description": "Image detail hint returned by view_image. Returns `original` when original resolution is preserved, otherwise `null`.",
34
+ },
35
+ },
36
+ "required": ["image_url", "detail"],
37
+ "additionalProperties": False,
38
+ }
39
+
40
+
41
+ class ViewImageTool(BaseTool):
42
+ name = "view_image"
43
+ description = (
44
+ "View a local image from the filesystem (only use if given a full "
45
+ "filepath by the user, and the image isn't already attached to the "
46
+ "thread context within <image ...> tags)."
47
+ )
48
+ input_schema = {
49
+ "type": "object",
50
+ "properties": {
51
+ "path": {
52
+ "type": "string",
53
+ "description": "Local filesystem path to an image file",
54
+ },
55
+ "detail": {
56
+ "type": "string",
57
+ "description": "Optional detail override. The only supported value is `original`; omit this field for default resized behavior. Use `original` to preserve the file's original resolution instead of resizing to fit.",
58
+ },
59
+ },
60
+ "required": ["path"],
61
+ "additionalProperties": False,
62
+ }
63
+ output_schema = VIEW_IMAGE_OUTPUT_SCHEMA
64
+
65
+ def __init__(self, cwd: str | Path | None = None) -> None:
66
+ self._workspace_root = Path(cwd or Path.cwd()).resolve()
67
+
68
+ async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
69
+ del context
70
+ path_value = str(args.get("path", "")).strip()
71
+ if not path_value:
72
+ return "Error: `path` is required."
73
+
74
+ detail_value = args.get("detail")
75
+ if detail_value in (None, ""):
76
+ detail = None
77
+ elif detail_value == "original":
78
+ detail = "original"
79
+ else:
80
+ return (
81
+ "Error: `detail` only supports `original`; omit `detail` for default "
82
+ f"behavior, got `{detail_value}`."
83
+ )
84
+
85
+ path = Path(path_value)
86
+ if not path.is_absolute():
87
+ path = self._workspace_root / path
88
+ path = path.resolve()
89
+ if not path.exists():
90
+ return f"Error: unable to locate image at `{path}`."
91
+ if not path.is_file():
92
+ return f"Error: image path `{path}` is not a file."
93
+
94
+ mime_type, _ = mimetypes.guess_type(path.name)
95
+ if not mime_type or not mime_type.startswith("image/"):
96
+ return f"Error: `{path}` does not look like an image file."
97
+
98
+ image_bytes = path.read_bytes()
99
+ encoded = base64.b64encode(image_bytes).decode("ascii")
100
+ image_url = f"data:{mime_type};base64,{encoded}"
101
+ output = {
102
+ "image_url": image_url,
103
+ "detail": detail,
104
+ }
105
+ image_item: JSONDict = {
106
+ "type": "input_image",
107
+ "image_url": image_url,
108
+ }
109
+ if detail is not None:
110
+ image_item["detail"] = detail
111
+ return StructuredToolOutput(output=output, content_items=(image_item,))
@@ -0,0 +1,75 @@
1
+ """`wait_agent` tool for the Python Codex prototype.
2
+
3
+ Original Codex mapping:
4
+ - Corresponds to the original Codex `wait_agent` collaboration tool.
5
+
6
+ Expected behavior:
7
+ - Wait for one or more spawned agents to reach a final state.
8
+ - Return the current status map for requested agents, or an empty map when the
9
+ wait times out.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ..protocol import JSONDict, JSONValue
15
+ from ..runtime_services import SubAgentManager
16
+ from .agent_tool_schemas import AGENT_STATUS_SCHEMA
17
+ from .base_tool import BaseTool, ToolContext
18
+
19
+ WAIT_AGENT_OUTPUT_SCHEMA = {
20
+ "type": "object",
21
+ "properties": {
22
+ "status": {
23
+ "type": "object",
24
+ "description": "Final statuses keyed by agent id.",
25
+ "additionalProperties": AGENT_STATUS_SCHEMA,
26
+ },
27
+ "timed_out": {
28
+ "type": "boolean",
29
+ "description": "Whether the wait call returned due to timeout before any agent reached a final status.",
30
+ },
31
+ },
32
+ "required": ["status", "timed_out"],
33
+ "additionalProperties": False,
34
+ }
35
+
36
+
37
+ class WaitAgentTool(BaseTool):
38
+ name = "wait_agent"
39
+ description = (
40
+ "Wait for agents to reach a final status. Completed statuses may "
41
+ "include the agent's final message. Returns empty status when timed "
42
+ "out."
43
+ )
44
+ input_schema = {
45
+ "type": "object",
46
+ "properties": {
47
+ "ids": {
48
+ "type": "array",
49
+ "items": {"type": "string"},
50
+ "description": "Agent ids to wait on. Pass multiple ids to wait for whichever finishes first.",
51
+ },
52
+ "timeout_ms": {
53
+ "type": "integer",
54
+ "description": "Optional timeout in milliseconds.",
55
+ },
56
+ },
57
+ "required": ["ids"],
58
+ "additionalProperties": False,
59
+ }
60
+ output_schema = WAIT_AGENT_OUTPUT_SCHEMA
61
+ supports_parallel = False
62
+
63
+ def __init__(self, subagent_manager: SubAgentManager) -> None:
64
+ self._subagent_manager = subagent_manager
65
+
66
+ async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
67
+ del context
68
+ ids = args.get("ids")
69
+ if not isinstance(ids, list) or not ids:
70
+ return "Error: `ids` must be a non-empty list."
71
+ agent_ids = [str(item).strip() for item in ids if str(item).strip()]
72
+ if not agent_ids:
73
+ return "Error: `ids` must include at least one non-empty id."
74
+ timeout_ms = int(args.get("timeout_ms", 30_000))
75
+ return await self._subagent_manager.wait_agents(agent_ids, timeout_ms)
@@ -0,0 +1,68 @@
1
+ """`wait` tool for the Python Codex prototype.
2
+
3
+ Original Codex mapping:
4
+ - Corresponds to the original Codex code-mode `wait` tool.
5
+
6
+ Expected behavior:
7
+ - Wait on a yielded `exec` cell for more output or completion.
8
+ - Optionally terminate the running cell.
9
+ - Return only the new output since the previous `exec` / `wait` snapshot.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ..protocol import JSONDict, JSONValue
15
+ from .base_tool import BaseTool, ToolContext
16
+ from .code_mode_manager import DEFAULT_WAIT_YIELD_TIME_MS, CodeModeManager
17
+
18
+
19
+ class WaitTool(BaseTool):
20
+ name = "wait"
21
+ description = (
22
+ "Waits on a yielded `exec` cell and returns new output or completion."
23
+ )
24
+ input_schema = {
25
+ "type": "object",
26
+ "properties": {
27
+ "cell_id": {
28
+ "type": "string",
29
+ "description": "Identifier of the running exec cell.",
30
+ },
31
+ "yield_time_ms": {
32
+ "type": "integer",
33
+ "description": "How long to wait (in milliseconds) for more output before yielding again.",
34
+ },
35
+ "max_tokens": {
36
+ "type": "integer",
37
+ "description": "Maximum number of output tokens to return for this wait call.",
38
+ },
39
+ "terminate": {
40
+ "type": "boolean",
41
+ "description": "Whether to terminate the running exec cell.",
42
+ },
43
+ },
44
+ "required": ["cell_id"],
45
+ "additionalProperties": False,
46
+ }
47
+ supports_parallel = False
48
+
49
+ def __init__(self, manager: CodeModeManager) -> None:
50
+ self._manager = manager
51
+
52
+ async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
53
+ del context
54
+ cell_id = str(args.get("cell_id", "")).strip()
55
+ if not cell_id:
56
+ return "Error: `cell_id` is required."
57
+ return await self._manager.wait(
58
+ cell_id=cell_id,
59
+ yield_time_ms=int(args.get("yield_time_ms", DEFAULT_WAIT_YIELD_TIME_MS)),
60
+ max_tokens=self._optional_int(args, "max_tokens"),
61
+ terminate=bool(args.get("terminate", False)),
62
+ )
63
+
64
+ def _optional_int(self, args: JSONDict, key: str) -> int | None:
65
+ value = args.get(key)
66
+ if value in (None, ""):
67
+ return None
68
+ return int(value)
@@ -0,0 +1,30 @@
1
+ """`web_search` tool declaration for the Python Codex prototype.
2
+
3
+ Original Codex mapping:
4
+ - Corresponds to the original Codex provider-native `web_search` tool.
5
+
6
+ Expected behavior:
7
+ - Advertise the Responses API built-in web search tool to the model.
8
+ - Let the provider handle search execution directly instead of routing through
9
+ the local `ToolRegistry`.
10
+ - Never expect a local tool-call round-trip result from the model.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ..protocol import JSONDict, JSONValue
16
+ from .base_tool import BaseTool, ToolContext
17
+
18
+
19
+ class WebSearchTool(BaseTool):
20
+ name = "web_search"
21
+ description = "Provider-native web search tool declaration."
22
+ tool_type = "web_search"
23
+ options = {
24
+ "external_web_access": True,
25
+ }
26
+ supports_parallel = False
27
+
28
+ async def run(self, context: ToolContext, args: JSONValue) -> JSONValue:
29
+ del context, args
30
+ return "Error: web_search is provider-native and should not be executed locally."
@@ -0,0 +1,75 @@
1
+ """`write_stdin` tool for the Python Codex prototype.
2
+
3
+ Original Codex mapping:
4
+ - Corresponds to the original Codex `write_stdin` tool.
5
+
6
+ Expected behavior:
7
+ - Write bytes into an existing `exec_command` session.
8
+ - Also support polling with empty input to fetch fresh output from a running
9
+ session.
10
+ - Reuse the same `session_id` until the process exits.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ..protocol import JSONDict, JSONValue
16
+ from .base_tool import BaseTool, ToolContext
17
+ from .unified_exec_manager import (
18
+ DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
19
+ UNIFIED_EXEC_OUTPUT_SCHEMA,
20
+ UnifiedExecManager,
21
+ )
22
+
23
+
24
+ class WriteStdinTool(BaseTool):
25
+ name = "write_stdin"
26
+ description = "Writes characters to an existing unified exec session and returns recent output."
27
+ input_schema = {
28
+ "type": "object",
29
+ "properties": {
30
+ "session_id": {
31
+ "type": "integer",
32
+ "description": "Identifier of the running unified exec session.",
33
+ },
34
+ "chars": {
35
+ "type": "string",
36
+ "description": "Bytes to write to stdin (may be empty to poll).",
37
+ },
38
+ "yield_time_ms": {
39
+ "type": "integer",
40
+ "description": "How long to wait (in milliseconds) for output before yielding.",
41
+ },
42
+ "max_output_tokens": {
43
+ "type": "integer",
44
+ "description": "Maximum number of tokens to return. Excess output will be truncated.",
45
+ },
46
+ },
47
+ "required": ["session_id"],
48
+ "additionalProperties": False,
49
+ }
50
+ output_schema = UNIFIED_EXEC_OUTPUT_SCHEMA
51
+ supports_parallel = False
52
+
53
+ def __init__(self, manager: UnifiedExecManager) -> None:
54
+ self._manager = manager
55
+
56
+ async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
57
+ del context
58
+ session_id = args.get("session_id")
59
+ if session_id is None:
60
+ return "Error: `session_id` is required."
61
+
62
+ return await self._manager.write_stdin(
63
+ session_id=int(session_id),
64
+ chars=str(args.get("chars", "")),
65
+ yield_time_ms=int(
66
+ args.get("yield_time_ms", DEFAULT_WRITE_STDIN_YIELD_TIME_MS)
67
+ ),
68
+ max_output_tokens=self._optional_int(args, "max_output_tokens"),
69
+ )
70
+
71
+ def _optional_int(self, args: JSONDict, key: str) -> int | None:
72
+ value = args.get(key)
73
+ if value in (None, ""):
74
+ return None
75
+ return int(value)
@@ -0,0 +1,40 @@
1
+ from .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
2
+ from .get_env import build_user_agent, get_shell_name, get_timezone_name
3
+ from .random_ids import uuid7_string
4
+ from .visualize import (
5
+ CliSessionView,
6
+ Spinner,
7
+ build_cli_spinner_frame,
8
+ cli_color_enabled,
9
+ colorize_cli_message,
10
+ extract_plan_items,
11
+ format_cli_plan_messages,
12
+ format_cli_tool_call_message,
13
+ format_cli_tool_message,
14
+ short_id,
15
+ shorten_title,
16
+ summarize_tool_event,
17
+ )
18
+
19
+ __all__ = [
20
+ "CliSessionView",
21
+ "DOTENV_FILENAME",
22
+ "Spinner",
23
+ "build_user_agent",
24
+ "build_cli_spinner_frame",
25
+ "cli_color_enabled",
26
+ "colorize_cli_message",
27
+ "extract_plan_items",
28
+ "format_cli_plan_messages",
29
+ "format_cli_tool_call_message",
30
+ "format_cli_tool_message",
31
+ "get_shell_name",
32
+ "get_timezone_name",
33
+ "load_codex_dotenv",
34
+ "parse_dotenv",
35
+ "parse_dotenv_value",
36
+ "short_id",
37
+ "shorten_title",
38
+ "summarize_tool_event",
39
+ "uuid7_string",
40
+ ]
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ ILLEGAL_ENV_VAR_PREFIX = "CODEX_"
7
+ DOTENV_FILENAME = ".env"
8
+ _LOADED_CODEX_DOTENV_HOMES: set[str] = set()
9
+
10
+
11
+ def load_codex_dotenv(config_path: str | Path) -> None:
12
+ codex_home = str(Path(config_path).resolve().parent)
13
+ if codex_home in _LOADED_CODEX_DOTENV_HOMES:
14
+ return
15
+
16
+ dotenv_path = Path(codex_home) / DOTENV_FILENAME
17
+ if not dotenv_path.is_file():
18
+ _LOADED_CODEX_DOTENV_HOMES.add(codex_home)
19
+ return
20
+
21
+ for key, value in parse_dotenv(dotenv_path.read_text()).items():
22
+ if key.upper().startswith(ILLEGAL_ENV_VAR_PREFIX):
23
+ continue
24
+ os.environ[key] = value
25
+
26
+ _LOADED_CODEX_DOTENV_HOMES.add(codex_home)
27
+
28
+
29
+ def parse_dotenv(text: str) -> dict[str, str]:
30
+ values: dict[str, str] = {}
31
+ for raw_line in text.splitlines():
32
+ line = raw_line.strip()
33
+ if not line or line.startswith("#"):
34
+ continue
35
+ if line.startswith("export "):
36
+ line = line[len("export ") :].lstrip()
37
+ if "=" not in line:
38
+ continue
39
+
40
+ key, raw_value = line.split("=", 1)
41
+ key = key.strip()
42
+ if not key:
43
+ continue
44
+ values[key] = parse_dotenv_value(raw_value.strip())
45
+ return values
46
+
47
+
48
+ def parse_dotenv_value(raw_value: str) -> str:
49
+ if not raw_value:
50
+ return ""
51
+
52
+ quote = raw_value[0]
53
+ if quote in {'"', "'"}:
54
+ if len(raw_value) >= 2 and raw_value[-1] == quote:
55
+ inner = raw_value[1:-1]
56
+ else:
57
+ inner = raw_value[1:]
58
+ if quote == "'":
59
+ return inner
60
+ return bytes(inner, "utf-8").decode("unicode_escape")
61
+
62
+ if " #" in raw_value:
63
+ raw_value = raw_value.split(" #", 1)[0].rstrip()
64
+ return raw_value