python-codex 0.2.7__py3-none-any.whl → 0.3.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 (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,12 +9,12 @@ Expected behavior:
9
9
  mode used to inspect code structure without shelling out to `sed` or `cat`.
10
10
  """
11
11
 
12
+ import typing
12
13
  from collections import deque
13
14
  from pathlib import Path
14
15
 
15
16
  from ..protocol import JSONDict, JSONValue
16
17
  from .base_tool import BaseTool, ToolContext
17
- import typing
18
18
 
19
19
  MAX_LINE_LENGTH = 500
20
20
  TAB_WIDTH = 4
@@ -78,7 +78,7 @@ class ReadFileTool(BaseTool):
78
78
  "additionalProperties": False,
79
79
  }
80
80
 
81
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
81
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
82
82
  del context
83
83
  file_path = Path(str(args.get("file_path", "")))
84
84
  offset = int(args.get("offset", 1))
@@ -101,7 +101,7 @@ class ReadFileTool(BaseTool):
101
101
  return self._read_indentation(file_path, offset, limit, indentation)
102
102
  return self._read_slice(file_path, offset, limit)
103
103
 
104
- def _read_slice(self, file_path: 'Path', offset: 'int', limit: 'int') -> 'str':
104
+ def _read_slice(self, file_path: "Path", offset: "int", limit: "int") -> "str":
105
105
  lines = file_path.read_text(errors="replace").splitlines()
106
106
  if offset > len(lines):
107
107
  return "Error: `offset` exceeds file length."
@@ -114,11 +114,11 @@ class ReadFileTool(BaseTool):
114
114
 
115
115
  def _read_indentation(
116
116
  self,
117
- file_path: 'Path',
118
- offset: 'int',
119
- limit: 'int',
120
- indentation: 'JSONDict',
121
- ) -> 'str':
117
+ file_path: "Path",
118
+ offset: "int",
119
+ limit: "int",
120
+ indentation: "JSONDict",
121
+ ) -> "str":
122
122
  lines = self._collect_line_records(file_path)
123
123
  anchor_line = int(indentation.get("anchor_line", offset))
124
124
  max_levels = int(indentation.get("max_levels", 0))
@@ -136,7 +136,9 @@ class ReadFileTool(BaseTool):
136
136
  anchor_index = anchor_line - 1
137
137
  effective_indents = self._compute_effective_indents(lines)
138
138
  anchor_indent = effective_indents[anchor_index]
139
- min_indent = 0 if max_levels == 0 else max(anchor_indent - max_levels * TAB_WIDTH, 0)
139
+ min_indent = (
140
+ 0 if max_levels == 0 else max(anchor_indent - max_levels * TAB_WIDTH, 0)
141
+ )
140
142
  final_limit = min(limit, max_lines, len(lines))
141
143
 
142
144
  if final_limit == 1:
@@ -157,8 +159,12 @@ class ReadFileTool(BaseTool):
157
159
  selected.appendleft(lines[upper])
158
160
  progressed += 1
159
161
  if effective_indents[upper] == min_indent and not include_siblings:
160
- allow_header_comment = include_header and lines[upper]["is_comment"]
161
- can_take_line = allow_header_comment or upper_min_indent_count == 0
162
+ allow_header_comment = (
163
+ include_header and lines[upper]["is_comment"]
164
+ )
165
+ can_take_line = (
166
+ allow_header_comment or upper_min_indent_count == 0
167
+ )
162
168
  if can_take_line:
163
169
  upper_min_indent_count += 1
164
170
  else:
@@ -200,9 +206,13 @@ class ReadFileTool(BaseTool):
200
206
  f"L{record['number']}: {record['display']}" for record in selected
201
207
  )
202
208
 
203
- def _collect_line_records(self, file_path: 'Path') -> 'typing.List[typing.Dict[str, object]]':
209
+ def _collect_line_records(
210
+ self, file_path: "Path"
211
+ ) -> "typing.List[typing.Dict[str, object]]":
204
212
  records = []
205
- for number, raw in enumerate(file_path.read_text(errors="replace").splitlines(), start=1):
213
+ for number, raw in enumerate(
214
+ file_path.read_text(errors="replace").splitlines(), start=1
215
+ ):
206
216
  records.append(
207
217
  {
208
218
  "number": number,
@@ -214,7 +224,9 @@ class ReadFileTool(BaseTool):
214
224
  )
215
225
  return records
216
226
 
217
- def _compute_effective_indents(self, records: 'typing.List[typing.Dict[str, object]]') -> 'typing.List[int]':
227
+ def _compute_effective_indents(
228
+ self, records: "typing.List[typing.Dict[str, object]]"
229
+ ) -> "typing.List[int]":
218
230
  effective = []
219
231
  previous_indent = 0
220
232
  for record in records:
@@ -225,7 +237,7 @@ class ReadFileTool(BaseTool):
225
237
  effective.append(previous_indent)
226
238
  return effective
227
239
 
228
- def _measure_indent(self, line: 'str') -> 'int':
240
+ def _measure_indent(self, line: "str") -> "int":
229
241
  total = 0
230
242
  for character in line:
231
243
  if character == " ":
@@ -236,10 +248,10 @@ class ReadFileTool(BaseTool):
236
248
  break
237
249
  return total
238
250
 
239
- def _format_line(self, text: 'str') -> 'str':
251
+ def _format_line(self, text: "str") -> "str":
240
252
  return text[:MAX_LINE_LENGTH]
241
253
 
242
- def _trim_empty_lines(self, records: 'deque[typing.Dict[str, object]]') -> 'None':
254
+ def _trim_empty_lines(self, records: "deque[typing.Dict[str, object]]") -> "None":
243
255
  while records and not str(records[0]["raw"]).strip():
244
256
  records.popleft()
245
257
  while records and not str(records[-1]["raw"]).strip():
@@ -81,10 +81,10 @@ class RequestPermissionsTool(BaseTool):
81
81
  }
82
82
  supports_parallel = False
83
83
 
84
- def __init__(self, request_manager: 'RequestPermissionsManager') -> 'None':
84
+ def __init__(self, request_manager: "RequestPermissionsManager") -> "None":
85
85
  self._request_manager = request_manager
86
86
 
87
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
87
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
88
88
  del context
89
89
  permissions = args.get("permissions")
90
90
  if not isinstance(permissions, dict):
@@ -94,11 +94,21 @@ class RequestPermissionsTool(BaseTool):
94
94
 
95
95
  response = await self._request_manager.request(
96
96
  {
97
- "reason": None if args.get("reason") in (None, "") else str(args.get("reason")),
98
- "environment_id": None if args.get("environment_id") in (None, "") else str(args.get("environment_id")),
97
+ "reason": (
98
+ None
99
+ if args.get("reason") in (None, "")
100
+ else str(args.get("reason"))
101
+ ),
102
+ "environment_id": (
103
+ None
104
+ if args.get("environment_id") in (None, "")
105
+ else str(args.get("environment_id"))
106
+ ),
99
107
  "permissions": permissions,
100
108
  }
101
109
  )
102
110
  if response is None:
103
- return "Error: request_permissions was cancelled before receiving a response."
111
+ return (
112
+ "Error: request_permissions was cancelled before receiving a response."
113
+ )
104
114
  return response
@@ -4,21 +4,12 @@ Original Codex mapping:
4
4
  - Corresponds to the original Codex `request_user_input` collaboration tool.
5
5
 
6
6
  Expected behavior:
7
- - In upstream Codex this tool is only actually usable in Plan mode.
8
- - In the current `pycodex` default-mode path, runtime invocation returns the
9
- same fixed unavailable error string as upstream Codex.
10
- - In Plan mode, the tool validates its question payload, forces `isOther=true`
11
- on every question, and returns a JSON-string `function_call_output` with
12
- `success=true`.
7
+ - Keep the tool declaration available for upstream request compatibility.
8
+ - Return the upstream Default-mode unavailable message without requesting input.
13
9
  """
14
10
 
15
- import json
16
-
17
- from ..collaboration import collaboration_mode_display_name
18
11
  from ..protocol import JSONDict, JSONValue
19
- from ..runtime_services import RequestUserInputManager
20
- from .base_tool import BaseTool, StructuredToolOutput, ToolContext
21
- import typing
12
+ from .base_tool import BaseTool, ToolContext
22
13
 
23
14
  MIN_AUTO_RESOLUTION_MS = 60_000
24
15
  MAX_AUTO_RESOLUTION_MS = 240_000
@@ -42,8 +33,8 @@ REQUEST_USER_INPUT_QUESTION_SCHEMA = {
42
33
  "type": "array",
43
34
  "description": (
44
35
  "Provide 2-3 mutually exclusive choices. Put the recommended option "
45
- "first and suffix its label with \"(Recommended)\". Do not include "
46
- "an \"Other\" option in this list; the client will add a free-form "
36
+ 'first and suffix its label with "(Recommended)". Do not include '
37
+ 'an "Other" option in this list; the client will add a free-form '
47
38
  "Other option automatically."
48
39
  ),
49
40
  "items": {
@@ -68,48 +59,16 @@ REQUEST_USER_INPUT_QUESTION_SCHEMA = {
68
59
  }
69
60
 
70
61
 
71
- def request_user_input_is_available(
72
- mode: 'str',
73
- default_mode_request_user_input: 'bool' = False,
74
- ) -> 'bool':
75
- normalized = mode.strip().lower()
76
- return normalized == "plan" or (
77
- default_mode_request_user_input and normalized == "default"
78
- )
79
-
80
-
81
- def request_user_input_unavailable_message(
82
- mode: 'str',
83
- default_mode_request_user_input: 'bool' = False,
84
- ) -> 'typing.Union[str, None]':
85
- if request_user_input_is_available(mode, default_mode_request_user_input):
86
- return None
87
- return (
88
- "request_user_input is unavailable in "
89
- f"{collaboration_mode_display_name(mode)} mode"
90
- )
91
-
92
-
93
- def request_user_input_tool_description(
94
- default_mode_request_user_input: 'bool' = False,
95
- ) -> 'str':
96
- if default_mode_request_user_input:
97
- allowed_modes = "Default or Plan mode"
98
- else:
99
- allowed_modes = "Plan mode"
100
- return (
62
+ class RequestUserInputTool(BaseTool):
63
+ name = "request_user_input"
64
+ description = (
101
65
  "Request user input for one to three short questions and wait for the "
102
66
  f"response. Set autoResolutionMs, from {MIN_AUTO_RESOLUTION_MS} to "
103
67
  f"{MAX_AUTO_RESOLUTION_MS} milliseconds, only when the question is "
104
68
  "useful but non-blocking and continuing with best judgment is "
105
69
  "acceptable if the user does not answer; omit it when explicit user "
106
- f"input is required. This tool is only available in {allowed_modes}."
70
+ "input is required. This tool is unavailable in Default mode."
107
71
  )
108
-
109
-
110
- class RequestUserInputTool(BaseTool):
111
- name = "request_user_input"
112
- description = request_user_input_tool_description()
113
72
  input_schema = {
114
73
  "type": "object",
115
74
  "properties": {
@@ -131,63 +90,13 @@ class RequestUserInputTool(BaseTool):
131
90
  f"up to {MAX_AUTO_RESOLUTION_MS} when the answer would "
132
91
  "materially unblock better work."
133
92
  ),
134
- }
93
+ },
135
94
  },
136
95
  "required": ["questions"],
137
96
  "additionalProperties": False,
138
97
  }
139
98
  supports_parallel = False
140
99
 
141
- def __init__(
142
- self,
143
- request_manager: 'RequestUserInputManager',
144
- default_mode_request_user_input: 'bool' = False,
145
- ) -> 'None':
146
- self._request_manager = request_manager
147
- self._default_mode_request_user_input = default_mode_request_user_input
148
- self.description = request_user_input_tool_description(
149
- default_mode_request_user_input
150
- )
151
-
152
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
153
- unavailable = request_user_input_unavailable_message(
154
- context.collaboration_mode,
155
- self._default_mode_request_user_input,
156
- )
157
- if unavailable is not None:
158
- return unavailable
159
-
160
- questions = args.get("questions")
161
- if not isinstance(questions, list):
162
- raise ValueError("questions must be a list")
163
-
164
- if any(
165
- not isinstance(question, dict)
166
- or not isinstance(question.get("options"), list)
167
- or not question["options"]
168
- for question in questions
169
- ):
170
- return "request_user_input requires non-empty options for every question"
171
-
172
- request_payload = {
173
- "questions": [
174
- {
175
- **question,
176
- "isOther": True,
177
- }
178
- for question in questions
179
- ]
180
- }
181
- auto_resolution_ms = args.get("autoResolutionMs")
182
- if auto_resolution_ms not in (None, ""):
183
- request_payload["autoResolutionMs"] = min(
184
- max(int(auto_resolution_ms), MIN_AUTO_RESOLUTION_MS),
185
- MAX_AUTO_RESOLUTION_MS,
186
- )
187
- response = await self._request_manager.request(request_payload)
188
- if response is None:
189
- return "request_user_input was cancelled before receiving a response"
190
- return StructuredToolOutput(
191
- json.dumps(response, ensure_ascii=False, separators=(",", ":")),
192
- success=True,
193
- )
100
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
101
+ del context, args
102
+ return "request_user_input is unavailable in Default mode"
@@ -43,10 +43,10 @@ class ResumeAgentTool(BaseTool):
43
43
  output_schema = RESUME_AGENT_OUTPUT_SCHEMA
44
44
  supports_parallel = False
45
45
 
46
- def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
46
+ def __init__(self, subagent_manager: "SubAgentManager") -> "None":
47
47
  self._subagent_manager = subagent_manager
48
48
 
49
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
49
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
50
50
  del context
51
51
  agent_id = str(args.get("id", "")).strip()
52
52
  if not agent_id:
@@ -9,11 +9,12 @@ Expected behavior:
9
9
  - Return the submission id for the queued input.
10
10
  """
11
11
 
12
+ import typing
13
+
12
14
  from ..protocol import JSONDict, JSONValue
13
15
  from ..runtime_services import SubAgentManager
14
16
  from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
15
17
  from .base_tool import BaseTool, ToolContext
16
- import typing
17
18
 
18
19
  SEND_INPUT_OUTPUT_SCHEMA = {
19
20
  "type": "object",
@@ -59,10 +60,10 @@ class SendInputTool(BaseTool):
59
60
  output_schema = SEND_INPUT_OUTPUT_SCHEMA
60
61
  supports_parallel = False
61
62
 
62
- def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
63
+ def __init__(self, subagent_manager: "SubAgentManager") -> "None":
63
64
  self._subagent_manager = subagent_manager
64
65
 
65
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
66
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
66
67
  del context
67
68
  agent_id = str(args.get("id", "")).strip()
68
69
  if not agent_id:
@@ -84,10 +85,10 @@ class SendInputTool(BaseTool):
84
85
 
85
86
  def _compose_prompt(
86
87
  self,
87
- message: 'typing.Union[str, None]',
88
- items: 'typing.Union[typing.List[typing.Dict[str, object]], None]',
89
- ) -> 'str':
90
- parts: 'typing.List[str]' = []
88
+ message: "typing.Union[str, None]",
89
+ items: "typing.Union[typing.List[typing.Dict[str, object]], None]",
90
+ ) -> "str":
91
+ parts: "typing.List[str]" = []
91
92
  if message:
92
93
  parts.append(message.strip())
93
94
  for item in items or []:
@@ -100,7 +101,9 @@ class SendInputTool(BaseTool):
100
101
  parts.append(str(item))
101
102
  return "\n\n".join(part for part in parts if part)
102
103
 
103
- def _optional_string(self, args: 'JSONDict', key: 'str') -> 'typing.Union[str, None]':
104
+ def _optional_string(
105
+ self, args: "JSONDict", key: "str"
106
+ ) -> "typing.Union[str, None]":
104
107
  value = args.get(key)
105
108
  if value in (None, ""):
106
109
  return None
@@ -12,11 +12,11 @@ Expected behavior:
12
12
  """
13
13
 
14
14
  import asyncio
15
+ import typing
15
16
  from pathlib import Path
16
17
 
17
18
  from ..protocol import JSONDict, JSONValue
18
19
  from .base_tool import BaseTool, ToolContext
19
- import typing
20
20
 
21
21
  DEFAULT_SHELL_TIMEOUT_MS = 10_000
22
22
  MAX_OUTPUT_CHARS = 12_000
@@ -54,10 +54,12 @@ class ShellCommandTool(BaseTool):
54
54
  }
55
55
  supports_parallel = False
56
56
 
57
- def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
57
+ def __init__(
58
+ self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
59
+ ) -> "None":
58
60
  self._working_directory = Path(cwd or Path.cwd()).resolve()
59
61
 
60
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
62
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
61
63
  del context
62
64
  command = str(args.get("command", "")).strip()
63
65
  timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
@@ -109,7 +111,7 @@ class ShellCommandTool(BaseTool):
109
111
 
110
112
  return "\n".join(pieces)
111
113
 
112
- def _resolve_workdir(self, workdir_arg) -> 'Path':
114
+ def _resolve_workdir(self, workdir_arg) -> "Path":
113
115
  if workdir_arg in (None, ""):
114
116
  return self._working_directory
115
117
  workdir = Path(str(workdir_arg))
@@ -117,7 +119,7 @@ class ShellCommandTool(BaseTool):
117
119
  workdir = self._working_directory / workdir
118
120
  return workdir.resolve()
119
121
 
120
- def _clip_output(self, text: 'str') -> 'str':
122
+ def _clip_output(self, text: "str") -> "str":
121
123
  if len(text) <= MAX_OUTPUT_CHARS:
122
124
  return text
123
125
  return text[:MAX_OUTPUT_CHARS] + "\n...[truncated]..."
@@ -12,11 +12,11 @@ Expected behavior:
12
12
  """
13
13
 
14
14
  import asyncio
15
+ import typing
15
16
  from pathlib import Path
16
17
 
17
18
  from ..protocol import JSONDict, JSONValue
18
19
  from .base_tool import BaseTool, ToolContext
19
- import typing
20
20
 
21
21
  DEFAULT_SHELL_TIMEOUT_MS = 30_000
22
22
  MAX_OUTPUT_CHARS = 12_000
@@ -51,10 +51,12 @@ class ShellTool(BaseTool):
51
51
  }
52
52
  supports_parallel = False
53
53
 
54
- def __init__(self, cwd: 'typing.Union[typing.Union[str, Path], None]' = None) -> 'None':
54
+ def __init__(
55
+ self, cwd: "typing.Union[typing.Union[str, Path], None]" = None
56
+ ) -> "None":
55
57
  self._working_directory = Path(cwd or Path.cwd()).resolve()
56
58
 
57
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
59
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
58
60
  del context
59
61
  command = args.get("command")
60
62
  timeout_ms = int(args.get("timeout_ms", DEFAULT_SHELL_TIMEOUT_MS))
@@ -106,7 +108,7 @@ class ShellTool(BaseTool):
106
108
 
107
109
  return "\n".join(pieces)
108
110
 
109
- def _resolve_workdir(self, workdir_arg) -> 'Path':
111
+ def _resolve_workdir(self, workdir_arg) -> "Path":
110
112
  if workdir_arg in (None, ""):
111
113
  return self._working_directory
112
114
  workdir = Path(str(workdir_arg))
@@ -114,7 +116,7 @@ class ShellTool(BaseTool):
114
116
  workdir = self._working_directory / workdir
115
117
  return workdir.resolve()
116
118
 
117
- def _clip_output(self, text: 'str') -> 'str':
119
+ def _clip_output(self, text: "str") -> "str":
118
120
  if len(text) <= MAX_OUTPUT_CHARS:
119
121
  return text
120
122
  return text[:MAX_OUTPUT_CHARS] + "\n...[truncated]..."
@@ -9,11 +9,12 @@ Expected behavior:
9
9
  - Return the new agent identifier plus any user-facing nickname.
10
10
  """
11
11
 
12
+ import typing
13
+
12
14
  from ..protocol import JSONDict, JSONValue
13
15
  from ..runtime_services import SubAgentManager
14
16
  from .agent_tool_schemas import COLLAB_INPUT_ITEMS_SCHEMA
15
17
  from .base_tool import BaseTool, ToolContext
16
- import typing
17
18
 
18
19
  SPAWN_AGENT_OUTPUT_SCHEMA = {
19
20
  "type": "object",
@@ -92,10 +93,10 @@ Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegati
92
93
  output_schema = SPAWN_AGENT_OUTPUT_SCHEMA
93
94
  supports_parallel = False
94
95
 
95
- def __init__(self, subagent_manager: 'SubAgentManager') -> 'None':
96
+ def __init__(self, subagent_manager: "SubAgentManager") -> "None":
96
97
  self._subagent_manager = subagent_manager
97
98
 
98
- async def run(self, context: 'ToolContext', args: 'JSONDict') -> 'JSONValue':
99
+ async def run(self, context: "ToolContext", args: "JSONDict") -> "JSONValue":
99
100
  message = self._optional_string(args, "message")
100
101
  items = args.get("items")
101
102
  if items is not None and not isinstance(items, list):
@@ -112,7 +113,9 @@ Do not spawn sub-agents unless the user explicitly asks for sub-agents, delegati
112
113
  history=context.history,
113
114
  )
114
115
 
115
- def _optional_string(self, args: 'JSONDict', key: 'str') -> 'typing.Union[str, None]':
116
+ def _optional_string(
117
+ self, args: "JSONDict", key: "str"
118
+ ) -> "typing.Union[str, None]":
116
119
  value = args.get(key)
117
120
  if value in (None, ""):
118
121
  return None