python-codex 0.1.2__py3-none-any.whl → 0.1.3__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 +5 -1
- pycodex/agent.py +39 -41
- pycodex/cli.py +43 -42
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +87 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +69 -69
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +69 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +43 -44
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +2 -4
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +62 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +37 -33
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/visualize.py +79 -79
- {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
- python_codex-0.1.3.dist-info/RECORD +74 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
- responses_server/app.py +29 -19
- responses_server/config.py +17 -17
- responses_server/payload_processors.py +16 -16
- responses_server/server.py +11 -11
- responses_server/session_store.py +10 -10
- responses_server/stream_router.py +58 -58
- responses_server/tools/custom_adapter.py +12 -12
- responses_server/tools/web_search.py +33 -33
- python_codex-0.1.2.dist-info/RECORD +0 -73
- {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
pycodex/tools/read_file_tool.py
CHANGED
|
@@ -9,13 +9,12 @@ Expected behavior:
|
|
|
9
9
|
mode used to inspect code structure without shelling out to `sed` or `cat`.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
12
|
from collections import deque
|
|
15
13
|
from pathlib import Path
|
|
16
14
|
|
|
17
15
|
from ..protocol import JSONDict, JSONValue
|
|
18
16
|
from .base_tool import BaseTool, ToolContext
|
|
17
|
+
import typing
|
|
19
18
|
|
|
20
19
|
MAX_LINE_LENGTH = 500
|
|
21
20
|
TAB_WIDTH = 4
|
|
@@ -49,7 +48,7 @@ class ReadFileTool(BaseTool):
|
|
|
49
48
|
"required": ["file_path"],
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
51
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
53
52
|
del context
|
|
54
53
|
file_path = Path(str(args.get("file_path", "")))
|
|
55
54
|
offset = int(args.get("offset", 1))
|
|
@@ -72,7 +71,7 @@ class ReadFileTool(BaseTool):
|
|
|
72
71
|
return self._read_indentation(file_path, offset, limit, indentation)
|
|
73
72
|
return self._read_slice(file_path, offset, limit)
|
|
74
73
|
|
|
75
|
-
def _read_slice(self, file_path: Path, offset: int, limit: int) -> str:
|
|
74
|
+
def _read_slice(self, file_path: 'Path', offset: 'int', limit: 'int') -> 'str':
|
|
76
75
|
lines = file_path.read_text(errors="replace").splitlines()
|
|
77
76
|
if offset > len(lines):
|
|
78
77
|
return "Error: `offset` exceeds file length."
|
|
@@ -85,11 +84,11 @@ class ReadFileTool(BaseTool):
|
|
|
85
84
|
|
|
86
85
|
def _read_indentation(
|
|
87
86
|
self,
|
|
88
|
-
file_path: Path,
|
|
89
|
-
offset: int,
|
|
90
|
-
limit: int,
|
|
91
|
-
indentation: JSONDict,
|
|
92
|
-
) -> str:
|
|
87
|
+
file_path: 'Path',
|
|
88
|
+
offset: 'int',
|
|
89
|
+
limit: 'int',
|
|
90
|
+
indentation: 'JSONDict',
|
|
91
|
+
) -> 'str':
|
|
93
92
|
lines = self._collect_line_records(file_path)
|
|
94
93
|
anchor_line = int(indentation.get("anchor_line", offset))
|
|
95
94
|
max_levels = int(indentation.get("max_levels", 0))
|
|
@@ -171,7 +170,7 @@ class ReadFileTool(BaseTool):
|
|
|
171
170
|
f"L{record['number']}: {record['display']}" for record in selected
|
|
172
171
|
)
|
|
173
172
|
|
|
174
|
-
def _collect_line_records(self, file_path: Path) ->
|
|
173
|
+
def _collect_line_records(self, file_path: 'Path') -> 'typing.List[typing.Dict[str, object]]':
|
|
175
174
|
records = []
|
|
176
175
|
for number, raw in enumerate(file_path.read_text(errors="replace").splitlines(), start=1):
|
|
177
176
|
records.append(
|
|
@@ -185,7 +184,7 @@ class ReadFileTool(BaseTool):
|
|
|
185
184
|
)
|
|
186
185
|
return records
|
|
187
186
|
|
|
188
|
-
def _compute_effective_indents(self, records:
|
|
187
|
+
def _compute_effective_indents(self, records: 'typing.List[typing.Dict[str, object]]') -> 'typing.List[int]':
|
|
189
188
|
effective = []
|
|
190
189
|
previous_indent = 0
|
|
191
190
|
for record in records:
|
|
@@ -196,7 +195,7 @@ class ReadFileTool(BaseTool):
|
|
|
196
195
|
effective.append(previous_indent)
|
|
197
196
|
return effective
|
|
198
197
|
|
|
199
|
-
def _measure_indent(self, line: str) -> int:
|
|
198
|
+
def _measure_indent(self, line: 'str') -> 'int':
|
|
200
199
|
total = 0
|
|
201
200
|
for character in line:
|
|
202
201
|
if character == " ":
|
|
@@ -207,10 +206,10 @@ class ReadFileTool(BaseTool):
|
|
|
207
206
|
break
|
|
208
207
|
return total
|
|
209
208
|
|
|
210
|
-
def _format_line(self, text: str) -> str:
|
|
209
|
+
def _format_line(self, text: 'str') -> 'str':
|
|
211
210
|
return text[:MAX_LINE_LENGTH]
|
|
212
211
|
|
|
213
|
-
def _trim_empty_lines(self, records: deque[
|
|
212
|
+
def _trim_empty_lines(self, records: 'deque[typing.Dict[str, object]]') -> 'None':
|
|
214
213
|
while records and not str(records[0]["raw"]).strip():
|
|
215
214
|
records.popleft()
|
|
216
215
|
while records and not str(records[-1]["raw"]).strip():
|
|
@@ -9,8 +9,6 @@ Expected behavior:
|
|
|
9
9
|
- Return the granted permission profile and scope so later tool calls can use it.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
12
|
from ..protocol import JSONDict, JSONValue
|
|
15
13
|
from ..runtime_services import RequestPermissionsManager
|
|
16
14
|
from .base_tool import BaseTool, ToolContext
|
|
@@ -73,10 +71,10 @@ class RequestPermissionsTool(BaseTool):
|
|
|
73
71
|
}
|
|
74
72
|
supports_parallel = False
|
|
75
73
|
|
|
76
|
-
def __init__(self, request_manager: RequestPermissionsManager) -> None:
|
|
74
|
+
def __init__(self, request_manager: 'RequestPermissionsManager') -> 'None':
|
|
77
75
|
self._request_manager = request_manager
|
|
78
76
|
|
|
79
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
77
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
80
78
|
del context
|
|
81
79
|
permissions = args.get("permissions")
|
|
82
80
|
if not isinstance(permissions, dict):
|
|
@@ -12,14 +12,13 @@ Expected behavior:
|
|
|
12
12
|
`success=true`.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
15
|
import json
|
|
18
16
|
|
|
19
17
|
from ..collaboration import collaboration_mode_display_name
|
|
20
18
|
from ..protocol import JSONDict, JSONValue
|
|
21
19
|
from ..runtime_services import RequestUserInputManager
|
|
22
20
|
from .base_tool import BaseTool, StructuredToolOutput, ToolContext
|
|
21
|
+
import typing
|
|
23
22
|
|
|
24
23
|
REQUEST_USER_INPUT_QUESTION_SCHEMA = {
|
|
25
24
|
"type": "object",
|
|
@@ -67,9 +66,9 @@ REQUEST_USER_INPUT_QUESTION_SCHEMA = {
|
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
def request_user_input_is_available(
|
|
70
|
-
mode: str,
|
|
71
|
-
default_mode_request_user_input: bool = False,
|
|
72
|
-
) -> bool:
|
|
69
|
+
mode: 'str',
|
|
70
|
+
default_mode_request_user_input: 'bool' = False,
|
|
71
|
+
) -> 'bool':
|
|
73
72
|
normalized = mode.strip().lower()
|
|
74
73
|
return normalized == "plan" or (
|
|
75
74
|
default_mode_request_user_input and normalized == "default"
|
|
@@ -77,9 +76,9 @@ def request_user_input_is_available(
|
|
|
77
76
|
|
|
78
77
|
|
|
79
78
|
def request_user_input_unavailable_message(
|
|
80
|
-
mode: str,
|
|
81
|
-
default_mode_request_user_input: bool = False,
|
|
82
|
-
) -> str
|
|
79
|
+
mode: 'str',
|
|
80
|
+
default_mode_request_user_input: 'bool' = False,
|
|
81
|
+
) -> 'typing.Union[str, None]':
|
|
83
82
|
if request_user_input_is_available(mode, default_mode_request_user_input):
|
|
84
83
|
return None
|
|
85
84
|
return (
|
|
@@ -89,8 +88,8 @@ def request_user_input_unavailable_message(
|
|
|
89
88
|
|
|
90
89
|
|
|
91
90
|
def request_user_input_tool_description(
|
|
92
|
-
default_mode_request_user_input: bool = False,
|
|
93
|
-
) -> str:
|
|
91
|
+
default_mode_request_user_input: 'bool' = False,
|
|
92
|
+
) -> 'str':
|
|
94
93
|
if default_mode_request_user_input:
|
|
95
94
|
allowed_modes = "Default or Plan mode"
|
|
96
95
|
else:
|
|
@@ -120,16 +119,16 @@ class RequestUserInputTool(BaseTool):
|
|
|
120
119
|
|
|
121
120
|
def __init__(
|
|
122
121
|
self,
|
|
123
|
-
request_manager: RequestUserInputManager,
|
|
124
|
-
default_mode_request_user_input: bool = False,
|
|
125
|
-
) -> None:
|
|
122
|
+
request_manager: 'RequestUserInputManager',
|
|
123
|
+
default_mode_request_user_input: 'bool' = False,
|
|
124
|
+
) -> 'None':
|
|
126
125
|
self._request_manager = request_manager
|
|
127
126
|
self._default_mode_request_user_input = default_mode_request_user_input
|
|
128
127
|
self.description = request_user_input_tool_description(
|
|
129
128
|
default_mode_request_user_input
|
|
130
129
|
)
|
|
131
130
|
|
|
132
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
131
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
133
132
|
unavailable = request_user_input_unavailable_message(
|
|
134
133
|
context.collaboration_mode,
|
|
135
134
|
self._default_mode_request_user_input,
|
|
@@ -8,8 +8,6 @@ Expected behavior:
|
|
|
8
8
|
- Return the agent's current status payload.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
11
|
from ..protocol import JSONDict, JSONValue
|
|
14
12
|
from ..runtime_services import SubAgentManager
|
|
15
13
|
from .agent_tool_schemas import AGENT_STATUS_SCHEMA
|
|
@@ -45,10 +43,10 @@ class ResumeAgentTool(BaseTool):
|
|
|
45
43
|
output_schema = RESUME_AGENT_OUTPUT_SCHEMA
|
|
46
44
|
supports_parallel = False
|
|
47
45
|
|
|
48
|
-
def __init__(self, subagent_manager: SubAgentManager) -> None:
|
|
46
|
+
def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
|
|
49
47
|
self._subagent_manager = subagent_manager
|
|
50
48
|
|
|
51
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
49
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
52
50
|
del context
|
|
53
51
|
agent_id = str(args.get("id", "")).strip()
|
|
54
52
|
if not agent_id:
|
pycodex/tools/send_input_tool.py
CHANGED
|
@@ -9,12 +9,11 @@ Expected behavior:
|
|
|
9
9
|
- Return the submission id for the queued input.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
12
|
from ..protocol import JSONDict, JSONValue
|
|
15
13
|
from ..runtime_services import SubAgentManager
|
|
16
14
|
from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
|
|
17
15
|
from .base_tool import BaseTool, ToolContext
|
|
16
|
+
import typing
|
|
18
17
|
|
|
19
18
|
SEND_INPUT_OUTPUT_SCHEMA = {
|
|
20
19
|
"type": "object",
|
|
@@ -58,10 +57,10 @@ class SendInputTool(BaseTool):
|
|
|
58
57
|
output_schema = SEND_INPUT_OUTPUT_SCHEMA
|
|
59
58
|
supports_parallel = False
|
|
60
59
|
|
|
61
|
-
def __init__(self, subagent_manager: SubAgentManager) -> None:
|
|
60
|
+
def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
|
|
62
61
|
self._subagent_manager = subagent_manager
|
|
63
62
|
|
|
64
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
63
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
65
64
|
del context
|
|
66
65
|
agent_id = str(args.get("id", "")).strip()
|
|
67
66
|
if not agent_id:
|
|
@@ -83,10 +82,10 @@ class SendInputTool(BaseTool):
|
|
|
83
82
|
|
|
84
83
|
def _compose_prompt(
|
|
85
84
|
self,
|
|
86
|
-
message: str
|
|
87
|
-
items:
|
|
88
|
-
) -> str:
|
|
89
|
-
parts:
|
|
85
|
+
message: 'typing.Union[str, None]',
|
|
86
|
+
items: 'typing.Union[typing.List[typing.Dict[str, object]], None]',
|
|
87
|
+
) -> 'str':
|
|
88
|
+
parts: 'typing.List[str]' = []
|
|
90
89
|
if message:
|
|
91
90
|
parts.append(message.strip())
|
|
92
91
|
for item in items or []:
|
|
@@ -99,7 +98,7 @@ class SendInputTool(BaseTool):
|
|
|
99
98
|
parts.append(str(item))
|
|
100
99
|
return "\n\n".join(part for part in parts if part)
|
|
101
100
|
|
|
102
|
-
def _optional_string(self, args: JSONDict, key: str) -> str
|
|
101
|
+
def _optional_string(self, args: 'JSONDict', key: 'str') -> 'typing.Union[str, None]':
|
|
103
102
|
value = args.get(key)
|
|
104
103
|
if value in (None, ""):
|
|
105
104
|
return None
|
|
@@ -11,13 +11,12 @@ Expected behavior:
|
|
|
11
11
|
stdout, and stderr.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
14
|
import asyncio
|
|
17
15
|
from pathlib import Path
|
|
18
16
|
|
|
19
17
|
from ..protocol import JSONDict, JSONValue
|
|
20
18
|
from .base_tool import BaseTool, ToolContext
|
|
19
|
+
import typing
|
|
21
20
|
|
|
22
21
|
DEFAULT_SHELL_TIMEOUT_MS = 30_000
|
|
23
22
|
MAX_OUTPUT_CHARS = 12_000
|
|
@@ -38,10 +37,10 @@ class ShellCommandTool(BaseTool):
|
|
|
38
37
|
}
|
|
39
38
|
supports_parallel = False
|
|
40
39
|
|
|
41
|
-
def __init__(self, cwd: str
|
|
40
|
+
def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
|
|
42
41
|
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
43
42
|
|
|
44
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
43
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
45
44
|
del context
|
|
46
45
|
command = str(args.get("command", "")).strip()
|
|
47
46
|
timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
|
|
@@ -93,7 +92,7 @@ class ShellCommandTool(BaseTool):
|
|
|
93
92
|
|
|
94
93
|
return "\n".join(pieces)
|
|
95
94
|
|
|
96
|
-
def _resolve_workdir(self, workdir_arg) -> Path:
|
|
95
|
+
def _resolve_workdir(self, workdir_arg) -> 'Path':
|
|
97
96
|
if workdir_arg in (None, ""):
|
|
98
97
|
return self._working_directory
|
|
99
98
|
workdir = Path(str(workdir_arg))
|
|
@@ -101,7 +100,7 @@ class ShellCommandTool(BaseTool):
|
|
|
101
100
|
workdir = self._working_directory / workdir
|
|
102
101
|
return workdir.resolve()
|
|
103
102
|
|
|
104
|
-
def _clip_output(self, text: str) -> str:
|
|
103
|
+
def _clip_output(self, text: 'str') -> 'str':
|
|
105
104
|
if len(text) <= MAX_OUTPUT_CHARS:
|
|
106
105
|
return text
|
|
107
106
|
return text[:MAX_OUTPUT_CHARS] + "\n...[truncated]..."
|
pycodex/tools/shell_tool.py
CHANGED
|
@@ -11,13 +11,12 @@ Expected behavior:
|
|
|
11
11
|
stdout, and stderr.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
14
|
import asyncio
|
|
17
15
|
from pathlib import Path
|
|
18
16
|
|
|
19
17
|
from ..protocol import JSONDict, JSONValue
|
|
20
18
|
from .base_tool import BaseTool, ToolContext
|
|
19
|
+
import typing
|
|
21
20
|
|
|
22
21
|
DEFAULT_SHELL_TIMEOUT_MS = 30_000
|
|
23
22
|
MAX_OUTPUT_CHARS = 12_000
|
|
@@ -43,10 +42,10 @@ class ShellTool(BaseTool):
|
|
|
43
42
|
}
|
|
44
43
|
supports_parallel = False
|
|
45
44
|
|
|
46
|
-
def __init__(self, cwd: str
|
|
45
|
+
def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
|
|
47
46
|
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
48
47
|
|
|
49
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
48
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
50
49
|
del context
|
|
51
50
|
command = args.get("command")
|
|
52
51
|
timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
|
|
@@ -98,7 +97,7 @@ class ShellTool(BaseTool):
|
|
|
98
97
|
|
|
99
98
|
return "\n".join(pieces)
|
|
100
99
|
|
|
101
|
-
def _resolve_workdir(self, workdir_arg) -> Path:
|
|
100
|
+
def _resolve_workdir(self, workdir_arg) -> 'Path':
|
|
102
101
|
if workdir_arg in (None, ""):
|
|
103
102
|
return self._working_directory
|
|
104
103
|
workdir = Path(str(workdir_arg))
|
|
@@ -106,7 +105,7 @@ class ShellTool(BaseTool):
|
|
|
106
105
|
workdir = self._working_directory / workdir
|
|
107
106
|
return workdir.resolve()
|
|
108
107
|
|
|
109
|
-
def _clip_output(self, text: str) -> str:
|
|
108
|
+
def _clip_output(self, text: 'str') -> 'str':
|
|
110
109
|
if len(text) <= MAX_OUTPUT_CHARS:
|
|
111
110
|
return text
|
|
112
111
|
return text[:MAX_OUTPUT_CHARS] + "\n...[truncated]..."
|
|
@@ -9,12 +9,11 @@ Expected behavior:
|
|
|
9
9
|
- Return the new agent identifier plus any user-facing nickname.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
12
|
from ..protocol import JSONDict, JSONValue
|
|
15
13
|
from ..runtime_services import SubAgentManager
|
|
16
14
|
from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
|
|
17
15
|
from .base_tool import BaseTool, ToolContext
|
|
16
|
+
import typing
|
|
18
17
|
|
|
19
18
|
SPAWN_AGENT_OUTPUT_SCHEMA = {
|
|
20
19
|
"type": "object",
|
|
@@ -70,10 +69,10 @@ class SpawnAgentTool(BaseTool):
|
|
|
70
69
|
output_schema = SPAWN_AGENT_OUTPUT_SCHEMA
|
|
71
70
|
supports_parallel = False
|
|
72
71
|
|
|
73
|
-
def __init__(self, subagent_manager: SubAgentManager) -> None:
|
|
72
|
+
def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
|
|
74
73
|
self._subagent_manager = subagent_manager
|
|
75
74
|
|
|
76
|
-
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
75
|
+
async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
|
|
77
76
|
message = self._optional_string(args, "message")
|
|
78
77
|
items = args.get("items")
|
|
79
78
|
if items is not None and not isinstance(items, list):
|
|
@@ -90,7 +89,7 @@ class SpawnAgentTool(BaseTool):
|
|
|
90
89
|
history=context.history,
|
|
91
90
|
)
|
|
92
91
|
|
|
93
|
-
def _optional_string(self, args: JSONDict, key: str) -> str
|
|
92
|
+
def _optional_string(self, args: 'JSONDict', key: 'str') -> 'typing.Union[str, None]':
|
|
94
93
|
value = args.get(key)
|
|
95
94
|
if value in (None, ""):
|
|
96
95
|
return None
|
|
@@ -11,8 +11,6 @@ Expected behavior:
|
|
|
11
11
|
- Return summaries in the same textual shape Codex tools expect.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
14
|
import asyncio
|
|
17
15
|
import os
|
|
18
16
|
import shlex
|
|
@@ -22,6 +20,9 @@ from pathlib import Path
|
|
|
22
20
|
|
|
23
21
|
from loguru import logger
|
|
24
22
|
|
|
23
|
+
from ..compat import shlex_join, stream_writer_is_closing
|
|
24
|
+
import typing
|
|
25
|
+
|
|
25
26
|
DEFAULT_EXEC_YIELD_TIME_MS = 10_000
|
|
26
27
|
DEFAULT_WRITE_STDIN_YIELD_TIME_MS = 250
|
|
27
28
|
DEFAULT_MAX_OUTPUT_TOKENS = 10_000
|
|
@@ -63,33 +64,33 @@ UNIFIED_EXEC_OUTPUT_SCHEMA = {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def _approx_token_count(text: str) -> int:
|
|
67
|
+
def _approx_token_count(text: 'str') -> 'int':
|
|
67
68
|
if not text:
|
|
68
69
|
return 0
|
|
69
70
|
byte_length = len(text.encode("utf-8"))
|
|
70
71
|
return max(1, (byte_length + APPROX_BYTES_PER_TOKEN - 1) // APPROX_BYTES_PER_TOKEN)
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
def _approx_bytes_for_tokens(token_count: int) -> int:
|
|
74
|
+
def _approx_bytes_for_tokens(token_count: 'int') -> 'int':
|
|
74
75
|
return max(token_count, 0) * APPROX_BYTES_PER_TOKEN
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
def _approx_tokens_from_byte_count(byte_count: int) -> int:
|
|
78
|
+
def _approx_tokens_from_byte_count(byte_count: 'int') -> 'int':
|
|
78
79
|
if byte_count <= 0:
|
|
79
80
|
return 0
|
|
80
81
|
return (byte_count + APPROX_BYTES_PER_TOKEN - 1) // APPROX_BYTES_PER_TOKEN
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
def _split_budget(byte_budget: int) ->
|
|
84
|
+
def _split_budget(byte_budget: 'int') -> 'typing.Tuple[int, int]':
|
|
84
85
|
left_budget = byte_budget // 2
|
|
85
86
|
return left_budget, byte_budget - left_budget
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
def _split_string(
|
|
89
|
-
text: str,
|
|
90
|
-
beginning_bytes: int,
|
|
91
|
-
end_bytes: int,
|
|
92
|
-
) ->
|
|
90
|
+
text: 'str',
|
|
91
|
+
beginning_bytes: 'int',
|
|
92
|
+
end_bytes: 'int',
|
|
93
|
+
) -> 'typing.Tuple[str, str]':
|
|
93
94
|
if not text:
|
|
94
95
|
return "", ""
|
|
95
96
|
|
|
@@ -122,7 +123,7 @@ def _split_string(
|
|
|
122
123
|
return text[:prefix_end], text[suffix_start:]
|
|
123
124
|
|
|
124
125
|
|
|
125
|
-
def _truncate_text(text: str, max_tokens: int) -> str:
|
|
126
|
+
def _truncate_text(text: 'str', max_tokens: 'int') -> 'str':
|
|
126
127
|
if not text:
|
|
127
128
|
return ""
|
|
128
129
|
|
|
@@ -141,7 +142,7 @@ def _truncate_text(text: str, max_tokens: int) -> str:
|
|
|
141
142
|
return f"{prefix}{marker}{suffix}"
|
|
142
143
|
|
|
143
144
|
|
|
144
|
-
def _formatted_truncate_text(text: str, max_tokens: int) -> str:
|
|
145
|
+
def _formatted_truncate_text(text: 'str', max_tokens: 'int') -> 'str':
|
|
145
146
|
byte_budget = _approx_bytes_for_tokens(max_tokens)
|
|
146
147
|
if len(text.encode("utf-8")) <= byte_budget:
|
|
147
148
|
return text
|
|
@@ -150,13 +151,13 @@ def _formatted_truncate_text(text: str, max_tokens: int) -> str:
|
|
|
150
151
|
return f"Total output lines: {total_lines}\n\n{_truncate_text(text, max_tokens)}"
|
|
151
152
|
|
|
152
153
|
|
|
153
|
-
@dataclass
|
|
154
|
+
@dataclass
|
|
154
155
|
class _HeadTailBuffer:
|
|
155
|
-
max_bytes: int = UNIFIED_EXEC_OUTPUT_MAX_BYTES
|
|
156
|
-
head: bytearray = field(default_factory=bytearray)
|
|
157
|
-
tail: bytearray = field(default_factory=bytearray)
|
|
156
|
+
max_bytes: 'int' = UNIFIED_EXEC_OUTPUT_MAX_BYTES
|
|
157
|
+
head: 'bytearray' = field(default_factory=bytearray)
|
|
158
|
+
tail: 'bytearray' = field(default_factory=bytearray)
|
|
158
159
|
|
|
159
|
-
def push_chunk(self, chunk: bytes) -> None:
|
|
160
|
+
def push_chunk(self, chunk: 'bytes') -> 'None':
|
|
160
161
|
if not chunk or self.max_bytes <= 0:
|
|
161
162
|
return
|
|
162
163
|
|
|
@@ -178,45 +179,45 @@ class _HeadTailBuffer:
|
|
|
178
179
|
excess = len(self.tail) - tail_budget
|
|
179
180
|
del self.tail[:excess]
|
|
180
181
|
|
|
181
|
-
def drain_bytes(self) -> bytes:
|
|
182
|
+
def drain_bytes(self) -> 'bytes':
|
|
182
183
|
combined = bytes(self.head) + bytes(self.tail)
|
|
183
184
|
self.head.clear()
|
|
184
185
|
self.tail.clear()
|
|
185
186
|
return combined
|
|
186
187
|
|
|
187
|
-
def has_data(self) -> bool:
|
|
188
|
+
def has_data(self) -> 'bool':
|
|
188
189
|
return bool(self.head or self.tail)
|
|
189
190
|
|
|
190
191
|
|
|
191
|
-
@dataclass
|
|
192
|
+
@dataclass
|
|
192
193
|
class UnifiedExecSession:
|
|
193
|
-
session_id: int
|
|
194
|
-
process: asyncio.subprocess.Process
|
|
195
|
-
start_time: float
|
|
196
|
-
command_display: str
|
|
197
|
-
tty: bool
|
|
198
|
-
unread_output: _HeadTailBuffer = field(default_factory=_HeadTailBuffer)
|
|
199
|
-
reader_task: asyncio.Task
|
|
200
|
-
output_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
194
|
+
session_id: 'int'
|
|
195
|
+
process: 'asyncio.subprocess.Process'
|
|
196
|
+
start_time: 'float'
|
|
197
|
+
command_display: 'str'
|
|
198
|
+
tty: 'bool'
|
|
199
|
+
unread_output: '_HeadTailBuffer' = field(default_factory=_HeadTailBuffer)
|
|
200
|
+
reader_task: 'typing.Union[asyncio.Task, None]' = None
|
|
201
|
+
output_event: 'asyncio.Event' = field(default_factory=asyncio.Event)
|
|
201
202
|
|
|
202
203
|
|
|
203
204
|
class UnifiedExecManager:
|
|
204
|
-
def __init__(self, cwd: str
|
|
205
|
+
def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
|
|
205
206
|
self._default_cwd = Path(cwd or Path.cwd()).resolve()
|
|
206
207
|
self._next_session_id = DEFAULT_SESSION_ID_START
|
|
207
|
-
self._sessions:
|
|
208
|
+
self._sessions: 'typing.Dict[int, UnifiedExecSession]' = {}
|
|
208
209
|
self._lock = asyncio.Lock()
|
|
209
210
|
|
|
210
211
|
async def exec_command(
|
|
211
212
|
self,
|
|
212
|
-
cmd: str,
|
|
213
|
-
workdir: str
|
|
214
|
-
shell: str
|
|
215
|
-
login: bool = DEFAULT_LOGIN,
|
|
216
|
-
tty: bool = DEFAULT_TTY,
|
|
217
|
-
yield_time_ms: int = DEFAULT_EXEC_YIELD_TIME_MS,
|
|
218
|
-
max_output_tokens: int
|
|
219
|
-
) -> str:
|
|
213
|
+
cmd: 'str',
|
|
214
|
+
workdir: 'typing.Union[str, None]' = None,
|
|
215
|
+
shell: 'typing.Union[str, None]' = None,
|
|
216
|
+
login: 'bool' = DEFAULT_LOGIN,
|
|
217
|
+
tty: 'bool' = DEFAULT_TTY,
|
|
218
|
+
yield_time_ms: 'int' = DEFAULT_EXEC_YIELD_TIME_MS,
|
|
219
|
+
max_output_tokens: 'typing.Union[int, None]' = None,
|
|
220
|
+
) -> 'str':
|
|
220
221
|
session_id = await self._allocate_session_id()
|
|
221
222
|
command = self._build_shell_command(cmd, shell, login)
|
|
222
223
|
cwd = self._resolve_workdir(workdir)
|
|
@@ -238,7 +239,7 @@ class UnifiedExecManager:
|
|
|
238
239
|
session_id=session_id,
|
|
239
240
|
process=process,
|
|
240
241
|
start_time=asyncio.get_running_loop().time(),
|
|
241
|
-
command_display=
|
|
242
|
+
command_display=shlex_join(command),
|
|
242
243
|
tty=tty,
|
|
243
244
|
)
|
|
244
245
|
session.reader_task = asyncio.create_task(self._pump_output(session))
|
|
@@ -254,11 +255,11 @@ class UnifiedExecManager:
|
|
|
254
255
|
|
|
255
256
|
async def write_stdin(
|
|
256
257
|
self,
|
|
257
|
-
session_id: int,
|
|
258
|
-
chars: str = "",
|
|
259
|
-
yield_time_ms: int = DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
|
|
260
|
-
max_output_tokens: int
|
|
261
|
-
) -> str:
|
|
258
|
+
session_id: 'int',
|
|
259
|
+
chars: 'str' = "",
|
|
260
|
+
yield_time_ms: 'int' = DEFAULT_WRITE_STDIN_YIELD_TIME_MS,
|
|
261
|
+
max_output_tokens: 'typing.Union[int, None]' = None,
|
|
262
|
+
) -> 'str':
|
|
262
263
|
session = await self._get_session(session_id)
|
|
263
264
|
if session is None:
|
|
264
265
|
return f"Error: session_id {session_id} is not running."
|
|
@@ -278,22 +279,22 @@ class UnifiedExecManager:
|
|
|
278
279
|
max_output_tokens,
|
|
279
280
|
)
|
|
280
281
|
|
|
281
|
-
async def _allocate_session_id(self) -> int:
|
|
282
|
+
async def _allocate_session_id(self) -> 'int':
|
|
282
283
|
async with self._lock:
|
|
283
284
|
session_id = self._next_session_id
|
|
284
285
|
self._next_session_id += 1
|
|
285
286
|
return session_id
|
|
286
287
|
|
|
287
|
-
async def _get_session(self, session_id: int) -> UnifiedExecSession
|
|
288
|
+
async def _get_session(self, session_id: 'int') -> 'typing.Union[UnifiedExecSession, None]':
|
|
288
289
|
async with self._lock:
|
|
289
290
|
return self._sessions.get(session_id)
|
|
290
291
|
|
|
291
292
|
async def _wait_and_snapshot(
|
|
292
293
|
self,
|
|
293
|
-
session_id: int,
|
|
294
|
-
yield_time_ms: int,
|
|
295
|
-
max_output_tokens: int
|
|
296
|
-
) -> str:
|
|
294
|
+
session_id: 'int',
|
|
295
|
+
yield_time_ms: 'int',
|
|
296
|
+
max_output_tokens: 'typing.Union[int, None]',
|
|
297
|
+
) -> 'str':
|
|
297
298
|
session = await self._get_session(session_id)
|
|
298
299
|
if session is None:
|
|
299
300
|
return f"Error: session_id {session_id} is not running."
|
|
@@ -343,15 +344,15 @@ class UnifiedExecManager:
|
|
|
343
344
|
|
|
344
345
|
return "\n".join(lines)
|
|
345
346
|
|
|
346
|
-
async def _close_session(self, session_id: int) -> None:
|
|
347
|
+
async def _close_session(self, session_id: 'int') -> 'None':
|
|
347
348
|
async with self._lock:
|
|
348
349
|
session = self._sessions.pop(session_id, None)
|
|
349
350
|
if session is None:
|
|
350
351
|
return
|
|
351
|
-
if session.process.stdin is not None and not session.process.stdin
|
|
352
|
+
if session.process.stdin is not None and not stream_writer_is_closing(session.process.stdin):
|
|
352
353
|
session.process.stdin.close()
|
|
353
354
|
|
|
354
|
-
async def _pump_output(self, session: UnifiedExecSession) -> None:
|
|
355
|
+
async def _pump_output(self, session: 'UnifiedExecSession') -> 'None':
|
|
355
356
|
stream = session.process.stdout
|
|
356
357
|
if stream is None:
|
|
357
358
|
return
|
|
@@ -363,7 +364,7 @@ class UnifiedExecManager:
|
|
|
363
364
|
session.output_event.set()
|
|
364
365
|
session.output_event.set()
|
|
365
366
|
|
|
366
|
-
def _resolve_workdir(self, workdir: str
|
|
367
|
+
def _resolve_workdir(self, workdir: 'typing.Union[str, None]') -> 'Path':
|
|
367
368
|
if not workdir:
|
|
368
369
|
return self._default_cwd
|
|
369
370
|
path = Path(workdir)
|
|
@@ -373,10 +374,10 @@ class UnifiedExecManager:
|
|
|
373
374
|
|
|
374
375
|
def _build_shell_command(
|
|
375
376
|
self,
|
|
376
|
-
cmd: str,
|
|
377
|
-
shell: str
|
|
378
|
-
login: bool,
|
|
379
|
-
) ->
|
|
377
|
+
cmd: 'str',
|
|
378
|
+
shell: 'typing.Union[str, None]',
|
|
379
|
+
login: 'bool',
|
|
380
|
+
) -> 'typing.List[str]':
|
|
380
381
|
shell_path = shell or os.environ.get("SHELL") or "/bin/bash"
|
|
381
382
|
shell_name = Path(shell_path).name.lower()
|
|
382
383
|
if shell_name in {"cmd", "cmd.exe"}:
|
|
@@ -385,13 +386,13 @@ class UnifiedExecManager:
|
|
|
385
386
|
return [shell_path, "-NoProfile", "-Command", cmd]
|
|
386
387
|
return [shell_path, "-lc" if login else "-c", cmd]
|
|
387
388
|
|
|
388
|
-
def _estimate_token_count(self, output: str) -> int
|
|
389
|
+
def _estimate_token_count(self, output: 'str') -> 'typing.Union[int, None]':
|
|
389
390
|
return _approx_token_count(output)
|
|
390
391
|
|
|
391
|
-
def _truncate_output(self, output: str, max_output_tokens: int
|
|
392
|
+
def _truncate_output(self, output: 'str', max_output_tokens: 'typing.Union[int, None]') -> 'str':
|
|
392
393
|
token_budget = DEFAULT_MAX_OUTPUT_TOKENS if max_output_tokens is None else max_output_tokens
|
|
393
394
|
return _formatted_truncate_text(output, max(token_budget, 0))
|
|
394
395
|
|
|
395
|
-
def _tty_echo(self, chars: str) -> bytes:
|
|
396
|
+
def _tty_echo(self, chars: 'str') -> 'bytes':
|
|
396
397
|
normalized = chars.replace("\n", "\r\n")
|
|
397
398
|
return normalized.encode("utf-8")
|