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.
- pycodex/__init__.py +139 -2
- pycodex/agent.py +290 -0
- pycodex/cli.py +641 -0
- pycodex/collaboration.py +21 -0
- pycodex/context.py +580 -0
- pycodex/doctor.py +360 -0
- pycodex/model.py +533 -0
- pycodex/prompts/collaboration_default.md +11 -0
- pycodex/prompts/collaboration_plan.md +128 -0
- pycodex/prompts/default_base_instructions.md +275 -0
- pycodex/prompts/exec_tools.json +411 -0
- pycodex/prompts/models.json +847 -0
- pycodex/prompts/permissions/approval_policy/never.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
- pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
- pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
- pycodex/prompts/subagent_tools.json +163 -0
- pycodex/protocol.py +347 -0
- pycodex/runtime.py +200 -0
- pycodex/runtime_services.py +408 -0
- pycodex/tools/__init__.py +58 -0
- pycodex/tools/agent_tool_schemas.py +70 -0
- pycodex/tools/apply_patch_tool.py +363 -0
- pycodex/tools/base_tool.py +168 -0
- pycodex/tools/close_agent_tool.py +55 -0
- pycodex/tools/code_mode_manager.py +519 -0
- pycodex/tools/exec_command_tool.py +96 -0
- pycodex/tools/exec_runtime.js +161 -0
- pycodex/tools/exec_tool.py +48 -0
- pycodex/tools/grep_files_tool.py +150 -0
- pycodex/tools/list_dir_tool.py +135 -0
- pycodex/tools/read_file_tool.py +217 -0
- pycodex/tools/request_permissions_tool.py +95 -0
- pycodex/tools/request_user_input_tool.py +167 -0
- pycodex/tools/resume_agent_tool.py +56 -0
- pycodex/tools/send_input_tool.py +106 -0
- pycodex/tools/shell_command_tool.py +107 -0
- pycodex/tools/shell_tool.py +112 -0
- pycodex/tools/spawn_agent_tool.py +97 -0
- pycodex/tools/unified_exec_manager.py +380 -0
- pycodex/tools/update_plan_tool.py +79 -0
- pycodex/tools/view_image_tool.py +111 -0
- pycodex/tools/wait_agent_tool.py +75 -0
- pycodex/tools/wait_tool.py +68 -0
- pycodex/tools/web_search_tool.py +30 -0
- pycodex/tools/write_stdin_tool.py +75 -0
- pycodex/utils/__init__.py +40 -0
- pycodex/utils/dotenv.py +64 -0
- pycodex/utils/get_env.py +218 -0
- pycodex/utils/random_ids.py +19 -0
- pycodex/utils/visualize.py +978 -0
- python_codex-0.1.0.dist-info/METADATA +267 -0
- python_codex-0.1.0.dist-info/RECORD +60 -0
- python_codex-0.1.0.dist-info/entry_points.txt +2 -0
- python_codex-0.1.0.dist-info/licenses/LICENSE +201 -0
- python_codex-0.0.1.dist-info/METADATA +0 -30
- python_codex-0.0.1.dist-info/RECORD +0 -4
- {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
|
+
]
|
pycodex/utils/dotenv.py
ADDED
|
@@ -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
|