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
pycodex/runtime.py ADDED
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections import deque
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ from .agent import AgentLoop, EventHandler, NOOP_EVENT_HANDLER, TurnInterrupted
9
+ from .protocol import AgentEvent, Operation, ShutdownOp, Submission, TurnResult, UserTurnOp
10
+ from .utils import uuid7_string
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class _QueuedSubmission:
15
+ submission: Submission
16
+ turn_id: str
17
+ futures: list[asyncio.Future[TurnResult | None]]
18
+
19
+
20
+ class AgentRuntime:
21
+ """Thin outer queue that mirrors the Rust `submission_loop` shape."""
22
+
23
+ def __init__(self, agent_loop: AgentLoop) -> None:
24
+ self._agent_loop = agent_loop
25
+ self._enqueue_queue: deque[_QueuedSubmission] = deque()
26
+ self._steer_queue: deque[_QueuedSubmission] = deque()
27
+ self._queue_lock = asyncio.Lock()
28
+ self._queue_event = asyncio.Event()
29
+ self._current_submission: _QueuedSubmission | None = None
30
+ self._current_task: asyncio.Task[TurnResult] | None = None
31
+ self._event_handler = NOOP_EVENT_HANDLER
32
+ self._agent_loop.set_event_handler(self._handle_agent_event)
33
+
34
+ def set_event_handler(self, event_handler: EventHandler = NOOP_EVENT_HANDLER) -> None:
35
+ self._event_handler = event_handler
36
+
37
+ async def submit_user_turn(self, text: str) -> TurnResult:
38
+ _submission_id, future = await self.enqueue_user_turn(text, queue="enqueue")
39
+ result = await future
40
+ assert result is not None
41
+ return result
42
+
43
+ async def enqueue_user_turn(
44
+ self,
45
+ text: str,
46
+ queue: Literal["enqueue", "steer"] = "enqueue",
47
+ ) -> tuple[str, asyncio.Future[TurnResult | None]]:
48
+ future: asyncio.Future[TurnResult | None] = asyncio.get_running_loop().create_future()
49
+ return await self._enqueue_user_turn_to_queue(
50
+ text,
51
+ future,
52
+ queue=queue,
53
+ )
54
+
55
+ async def shutdown(self) -> None:
56
+ submission = Submission(id=uuid7_string(), op=ShutdownOp())
57
+ future: asyncio.Future[TurnResult | None] = asyncio.get_running_loop().create_future()
58
+ self._enqueue_queue.append(
59
+ _QueuedSubmission(
60
+ submission=submission,
61
+ turn_id=submission.id,
62
+ futures=[future],
63
+ )
64
+ )
65
+ self._queue_event.set()
66
+ await future
67
+
68
+ async def run_forever(self) -> None:
69
+ while True:
70
+ queued = await self._next_submission()
71
+ submission = queued.submission
72
+ self._current_submission = queued
73
+ try:
74
+ if isinstance(submission.op, UserTurnOp):
75
+ self._current_task = asyncio.create_task(
76
+ self._agent_loop.run_turn(
77
+ list(submission.op.texts),
78
+ turn_id=queued.turn_id,
79
+ )
80
+ )
81
+ try:
82
+ result = await self._current_task
83
+ except TurnInterrupted:
84
+ self._finish_submission_exception(
85
+ queued,
86
+ RuntimeError("submission interrupted"),
87
+ )
88
+ continue
89
+ except asyncio.CancelledError:
90
+ self._finish_submission_exception(
91
+ queued,
92
+ RuntimeError("submission interrupted"),
93
+ )
94
+ continue
95
+ self._finish_submission_result(queued, result)
96
+ continue
97
+
98
+ if isinstance(submission.op, ShutdownOp):
99
+ self._finish_submission_result(queued, None)
100
+ break
101
+
102
+ self._finish_submission_exception(
103
+ queued,
104
+ RuntimeError(f"unsupported operation: {type(submission.op).__name__}"),
105
+ )
106
+ except Exception as exc: # pragma: no cover - defensive wrapper
107
+ self._finish_submission_exception(queued, exc)
108
+ finally:
109
+ self._current_task = None
110
+ self._current_submission = None
111
+
112
+ @staticmethod
113
+ def operation_name(op: Operation) -> str:
114
+ if isinstance(op, UserTurnOp):
115
+ return "user_turn"
116
+ if isinstance(op, ShutdownOp):
117
+ return "shutdown"
118
+ return type(op).__name__
119
+
120
+ async def _enqueue_user_turn_to_queue(
121
+ self,
122
+ text: str,
123
+ future: asyncio.Future[TurnResult | None],
124
+ queue: Literal["enqueue", "steer"],
125
+ ) -> tuple[str, asyncio.Future[TurnResult | None]]:
126
+ if queue == "steer" and self._has_active_turn():
127
+ self._agent_loop.interrupt_asap = True
128
+
129
+ async with self._queue_lock:
130
+ if queue == "steer" and self._steer_queue:
131
+ queued = self._steer_queue[-1]
132
+ queued.submission.op.texts.append(text)
133
+ queued.futures.append(future)
134
+ return queued.submission.id, future
135
+
136
+ submission = Submission(id=uuid7_string(), op=UserTurnOp(texts=[text]))
137
+ current = self._current_submission if self._has_active_turn() else None
138
+ turn_id = (
139
+ current.turn_id if queue == "steer" and current is not None else submission.id
140
+ )
141
+ queued = _QueuedSubmission(
142
+ submission=submission,
143
+ turn_id=turn_id,
144
+ futures=[future],
145
+ )
146
+ if queue == "steer":
147
+ self._steer_queue.append(queued)
148
+ else:
149
+ self._enqueue_queue.append(queued)
150
+ self._queue_event.set()
151
+ return submission.id, future
152
+
153
+ async def _next_submission(self) -> _QueuedSubmission:
154
+ while True:
155
+ async with self._queue_lock:
156
+ queued: _QueuedSubmission | None = None
157
+ if self._steer_queue:
158
+ queued = self._steer_queue.popleft()
159
+ elif self._enqueue_queue:
160
+ queued = self._enqueue_queue.popleft()
161
+ if queued is not None:
162
+ if not self._steer_queue and not self._enqueue_queue:
163
+ self._queue_event.clear()
164
+ return queued
165
+ self._queue_event.clear()
166
+ await self._queue_event.wait()
167
+
168
+ @staticmethod
169
+ def _finish_submission_result(
170
+ queued: _QueuedSubmission,
171
+ result: TurnResult | None,
172
+ ) -> None:
173
+ for future in queued.futures:
174
+ if not future.done():
175
+ future.set_result(result)
176
+
177
+ @staticmethod
178
+ def _finish_submission_exception(
179
+ queued: _QueuedSubmission,
180
+ exc: Exception,
181
+ ) -> None:
182
+ for future in queued.futures:
183
+ if not future.done():
184
+ future.set_exception(exc)
185
+
186
+ def _has_active_turn(self) -> bool:
187
+ current_task = self._current_task
188
+ return current_task is not None and not current_task.done()
189
+
190
+ def _handle_agent_event(self, event: AgentEvent) -> None:
191
+ queued = self._current_submission
192
+ if queued is None:
193
+ self._event_handler(event)
194
+ return
195
+ payload = dict(event.payload)
196
+ payload.setdefault("submission_id", queued.submission.id)
197
+ payload.setdefault("turn_id", queued.turn_id)
198
+ self._event_handler(
199
+ AgentEvent(kind=event.kind, turn_id=event.turn_id, payload=payload)
200
+ )
@@ -0,0 +1,408 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import random
6
+ from collections.abc import Awaitable, Callable
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Literal
9
+
10
+ from .protocol import ConversationItem, TurnResult
11
+ from .utils import uuid7_string
12
+
13
+ if TYPE_CHECKING:
14
+ from .runtime import AgentRuntime
15
+
16
+ PlanStatus = Literal["pending", "in_progress", "completed"]
17
+ PlanListener = Callable[[dict[str, object]], None]
18
+ RuntimeBuilder = Callable[
19
+ [str | None, str | None, tuple[ConversationItem, ...], str],
20
+ "AgentRuntime",
21
+ ]
22
+ AsyncJSONHandler = Callable[[dict[str, object]], Awaitable[dict[str, object] | None]]
23
+
24
+ DEFAULT_AGENT_NICKNAME_CANDIDATES = (
25
+ "Bacon",
26
+ "Descartes",
27
+ "Pascal",
28
+ "Fermat",
29
+ "Huygens",
30
+ "Leibniz",
31
+ "Newton",
32
+ "Halley",
33
+ "Euler",
34
+ "Lagrange",
35
+ "Laplace",
36
+ "Volta",
37
+ "Gauss",
38
+ "Ampere",
39
+ "Faraday",
40
+ "Darwin",
41
+ "Lovelace",
42
+ "Boole",
43
+ "Pasteur",
44
+ "Maxwell",
45
+ "Mendel",
46
+ "Curie",
47
+ "Planck",
48
+ "Tesla",
49
+ "Poincare",
50
+ "Noether",
51
+ "Hilbert",
52
+ "Einstein",
53
+ "Raman",
54
+ "Bohr",
55
+ "Turing",
56
+ "Hubble",
57
+ "Feynman",
58
+ "Franklin",
59
+ "McClintock",
60
+ "Meitner",
61
+ "Herschel",
62
+ "Linnaeus",
63
+ "Wegener",
64
+ "Chandrasekhar",
65
+ "Sagan",
66
+ "Goodall",
67
+ "Carson",
68
+ "Carver",
69
+ "Socrates",
70
+ "Plato",
71
+ "Aristotle",
72
+ "Epicurus",
73
+ "Cicero",
74
+ "Confucius",
75
+ "Mencius",
76
+ "Zeno",
77
+ "Locke",
78
+ "Hume",
79
+ "Kant",
80
+ "Hegel",
81
+ "Kierkegaard",
82
+ "Mill",
83
+ "Nietzsche",
84
+ "Peirce",
85
+ "James",
86
+ "Dewey",
87
+ "Russell",
88
+ "Popper",
89
+ "Sartre",
90
+ "Beauvoir",
91
+ "Arendt",
92
+ "Rawls",
93
+ "Singer",
94
+ "Anscombe",
95
+ "Parfit",
96
+ "Kuhn",
97
+ "Boyle",
98
+ "Hooke",
99
+ "Harvey",
100
+ "Dalton",
101
+ "Helmholtz",
102
+ "Gibbs",
103
+ "Lorentz",
104
+ "Schrodinger",
105
+ "Heisenberg",
106
+ "Pauli",
107
+ "Dirac",
108
+ "Bernoulli",
109
+ "Godel",
110
+ "Nash",
111
+ "Banach",
112
+ "Ramanujan",
113
+ "Erdos",
114
+ )
115
+
116
+
117
+ @dataclass(frozen=True, slots=True)
118
+ class PlanItem:
119
+ step: str
120
+ status: PlanStatus
121
+
122
+
123
+ class PlanStore:
124
+ def __init__(self) -> None:
125
+ self._explanation: str | None = None
126
+ self._plan: tuple[PlanItem, ...] = ()
127
+ self._listener: PlanListener = lambda _payload: None
128
+
129
+ def set_listener(self, listener: PlanListener | None) -> None:
130
+ self._listener = listener or (lambda _payload: None)
131
+
132
+ def update(self, explanation: str | None, plan: tuple[PlanItem, ...]) -> None:
133
+ in_progress = sum(1 for item in plan if item.status == "in_progress")
134
+ if in_progress > 1:
135
+ raise ValueError("at most one plan step can be in_progress")
136
+ self._explanation = explanation
137
+ self._plan = plan
138
+ self._listener(
139
+ {
140
+ "explanation": explanation,
141
+ "plan": [
142
+ {"step": item.step, "status": item.status}
143
+ for item in self._plan
144
+ ],
145
+ }
146
+ )
147
+
148
+ def snapshot(self) -> dict[str, object]:
149
+ return {
150
+ "explanation": self._explanation,
151
+ "plan": [
152
+ {"step": item.step, "status": item.status}
153
+ for item in self._plan
154
+ ],
155
+ }
156
+
157
+
158
+ class RequestUserInputManager:
159
+ def __init__(self) -> None:
160
+ self._handler: AsyncJSONHandler | None = None
161
+
162
+ def set_handler(self, handler: AsyncJSONHandler | None) -> None:
163
+ self._handler = handler
164
+
165
+ async def request(self, payload: dict[str, object]) -> dict[str, object] | None:
166
+ handler = self._handler
167
+ if handler is None:
168
+ return None
169
+ return await handler(payload)
170
+
171
+
172
+ class RequestPermissionsManager:
173
+ def __init__(self) -> None:
174
+ self._handler: AsyncJSONHandler | None = None
175
+
176
+ def set_handler(self, handler: AsyncJSONHandler | None) -> None:
177
+ self._handler = handler
178
+
179
+ async def request(self, payload: dict[str, object]) -> dict[str, object] | None:
180
+ handler = self._handler
181
+ if handler is None:
182
+ return None
183
+ return await handler(payload)
184
+
185
+
186
+ @dataclass(slots=True)
187
+ class ManagedAgent:
188
+ agent_id: str
189
+ runtime: "AgentRuntime"
190
+ worker_task: asyncio.Task[None]
191
+ nickname: str | None = None
192
+ state: str = "pending_init"
193
+ completed_message: str | None = None
194
+ error_message: str | None = None
195
+ pending_submission_ids: set[str] = field(default_factory=set)
196
+
197
+
198
+ class SubAgentManager:
199
+ def __init__(self) -> None:
200
+ self._runtime_builder: RuntimeBuilder | None = None
201
+ self._agents: dict[str, ManagedAgent] = {}
202
+ self._condition = asyncio.Condition()
203
+ self._available_nicknames: list[str] = []
204
+ self._nickname_random = random.Random()
205
+
206
+ def set_runtime_builder(self, builder: RuntimeBuilder | None) -> None:
207
+ self._runtime_builder = builder
208
+
209
+ async def spawn_agent(
210
+ self,
211
+ message: str | None,
212
+ items: list[dict[str, object]] | None,
213
+ agent_type: str | None,
214
+ fork_context: bool,
215
+ model: str | None,
216
+ reasoning_effort: str | None,
217
+ history: tuple[ConversationItem, ...],
218
+ ) -> dict[str, object]:
219
+ builder = self._runtime_builder
220
+ if builder is None:
221
+ raise RuntimeError("spawn_agent is unavailable before runtime initialization")
222
+
223
+ initial_history = history if fork_context else ()
224
+ agent_id = uuid7_string()
225
+ runtime = builder(model, reasoning_effort, initial_history, agent_id)
226
+ worker_task = asyncio.create_task(runtime.run_forever())
227
+ nickname = self._next_nickname()
228
+ managed = ManagedAgent(
229
+ agent_id=agent_id,
230
+ runtime=runtime,
231
+ worker_task=worker_task,
232
+ nickname=nickname,
233
+ )
234
+ async with self._condition:
235
+ self._agents[agent_id] = managed
236
+ self._condition.notify_all()
237
+
238
+ initial_prompt = self._compose_prompt(message, items)
239
+ if initial_prompt:
240
+ await self.send_input(agent_id, initial_prompt, interrupt=False)
241
+
242
+ return {
243
+ "agent_id": agent_id,
244
+ "nickname": nickname,
245
+ }
246
+
247
+ async def send_input(
248
+ self,
249
+ agent_id: str,
250
+ prompt_text: str,
251
+ interrupt: bool,
252
+ ) -> dict[str, object]:
253
+ managed = self._agents.get(agent_id)
254
+ if managed is None:
255
+ raise RuntimeError(f"unknown agent: {agent_id}")
256
+ if managed.state == "shutdown":
257
+ raise RuntimeError(f"agent is shutdown: {agent_id}")
258
+
259
+ submission_id, future = await managed.runtime.enqueue_user_turn(
260
+ prompt_text,
261
+ queue="steer" if interrupt else "enqueue",
262
+ )
263
+ managed.state = "running"
264
+ managed.completed_message = None
265
+ managed.error_message = None
266
+ managed.pending_submission_ids.add(submission_id)
267
+ asyncio.create_task(self._track_submission(managed, submission_id, future))
268
+ async with self._condition:
269
+ self._condition.notify_all()
270
+ return {"submission_id": submission_id}
271
+
272
+ async def resume_agent(self, agent_id: str) -> dict[str, object]:
273
+ managed = self._agents.get(agent_id)
274
+ if managed is None:
275
+ return {"status": "not_found"}
276
+ if managed.worker_task.done():
277
+ managed.worker_task = asyncio.create_task(managed.runtime.run_forever())
278
+ managed.state = "pending_init"
279
+ managed.completed_message = None
280
+ managed.error_message = None
281
+ async with self._condition:
282
+ self._condition.notify_all()
283
+ return {"status": self._status_payload(managed)}
284
+
285
+ async def close_agent(self, agent_id: str) -> dict[str, object]:
286
+ managed = self._agents.get(agent_id)
287
+ if managed is None:
288
+ return {"status": "not_found"}
289
+ previous_status = self._status_payload(managed)
290
+ if not managed.worker_task.done():
291
+ managed.runtime._agent_loop.interrupt_asap = True
292
+ await managed.runtime.shutdown()
293
+ await managed.worker_task
294
+ managed.state = "shutdown"
295
+ managed.pending_submission_ids.clear()
296
+ async with self._condition:
297
+ self._condition.notify_all()
298
+ return {"status": previous_status}
299
+
300
+ def _next_nickname(self) -> str:
301
+ if not self._available_nicknames:
302
+ self._available_nicknames = list(DEFAULT_AGENT_NICKNAME_CANDIDATES)
303
+ self._nickname_random.shuffle(self._available_nicknames)
304
+ return self._available_nicknames.pop()
305
+
306
+ async def wait_agents(
307
+ self,
308
+ agent_ids: list[str],
309
+ timeout_ms: int = 30_000,
310
+ ) -> dict[str, object]:
311
+ timeout_seconds = max(timeout_ms, 1) / 1000.0
312
+ loop = asyncio.get_running_loop()
313
+ deadline = loop.time() + timeout_seconds
314
+
315
+ while True:
316
+ snapshot = {
317
+ agent_id: self._status_payload(self._agents.get(agent_id))
318
+ for agent_id in agent_ids
319
+ }
320
+ if any(self._is_final_status(status) for status in snapshot.values()):
321
+ return {"status": snapshot, "timed_out": False}
322
+
323
+ remaining = deadline - loop.time()
324
+ if remaining <= 0:
325
+ return {"status": {}, "timed_out": True}
326
+
327
+ async with self._condition:
328
+ try:
329
+ await asyncio.wait_for(self._condition.wait(), timeout=remaining)
330
+ except asyncio.TimeoutError:
331
+ return {"status": {}, "timed_out": True}
332
+
333
+ async def _track_submission(
334
+ self,
335
+ managed: ManagedAgent,
336
+ submission_id: str,
337
+ future: asyncio.Future[TurnResult | None],
338
+ ) -> None:
339
+ try:
340
+ result = await future
341
+ except Exception as exc: # pragma: no cover - background safety
342
+ managed.error_message = f"{type(exc).__name__}: {exc}"
343
+ managed.state = "errored"
344
+ else:
345
+ managed.completed_message = None if result is None else result.output_text
346
+ managed.state = "completed"
347
+ finally:
348
+ managed.pending_submission_ids.discard(submission_id)
349
+ async with self._condition:
350
+ self._condition.notify_all()
351
+
352
+ def _compose_prompt(
353
+ self,
354
+ message: str | None,
355
+ items: list[dict[str, object]] | None,
356
+ ) -> str:
357
+ parts: list[str] = []
358
+ if message:
359
+ parts.append(message.strip())
360
+ for item in items or []:
361
+ item_type = str(item.get("type", ""))
362
+ if item_type == "text":
363
+ text = str(item.get("text", "")).strip()
364
+ if text:
365
+ parts.append(text)
366
+ elif item_type == "image":
367
+ image_url = str(item.get("image_url", "")).strip()
368
+ if image_url:
369
+ parts.append(f"[image] {image_url}")
370
+ else:
371
+ parts.append(json.dumps(item, ensure_ascii=False))
372
+ return "\n\n".join(part for part in parts if part)
373
+
374
+ def _status_payload(self, managed: ManagedAgent | None) -> object:
375
+ if managed is None:
376
+ return "not_found"
377
+ if managed.error_message is not None:
378
+ return {"errored": managed.error_message}
379
+ if managed.state == "completed":
380
+ return {"completed": managed.completed_message}
381
+ if managed.state in {"pending_init", "running", "shutdown"}:
382
+ return managed.state
383
+ return managed.state
384
+
385
+ def _is_final_status(self, status: object) -> bool:
386
+ if isinstance(status, str):
387
+ return status in {"shutdown", "not_found"}
388
+ if isinstance(status, dict):
389
+ return "completed" in status or "errored" in status
390
+ return False
391
+
392
+
393
+ class RuntimeEnvironment:
394
+ def __init__(self) -> None:
395
+ self.plan_store = PlanStore()
396
+ self.subagent_manager = SubAgentManager()
397
+ self.request_user_input_manager = RequestUserInputManager()
398
+ self.request_permissions_manager = RequestPermissionsManager()
399
+
400
+ def configure_runtime_builder(self, builder: RuntimeBuilder | None) -> None:
401
+ self.subagent_manager.set_runtime_builder(builder)
402
+
403
+
404
+ _RUNTIME_ENV = RuntimeEnvironment()
405
+
406
+
407
+ def get_runtime_environment() -> RuntimeEnvironment:
408
+ return _RUNTIME_ENV
@@ -0,0 +1,58 @@
1
+ """Tool package for the Python Codex prototype.
2
+
3
+ This package groups the local tool abstractions and concrete tool
4
+ implementations that back `pycodex`.
5
+ """
6
+
7
+ from .base_tool import BaseTool, Registry, ToolContext, ToolRegistry
8
+ from .apply_patch_tool import ApplyPatchTool
9
+ from .close_agent_tool import CloseAgentTool
10
+ from .code_mode_manager import CodeModeManager
11
+ from .exec_command_tool import ExecCommandTool
12
+ from .exec_tool import ExecTool
13
+ from .grep_files_tool import GrepFilesTool
14
+ from .list_dir_tool import ListDirTool
15
+ from .read_file_tool import ReadFileTool
16
+ from .request_permissions_tool import RequestPermissionsTool
17
+ from .request_user_input_tool import RequestUserInputTool
18
+ from .resume_agent_tool import ResumeAgentTool
19
+ from .send_input_tool import SendInputTool
20
+ from .shell_command_tool import ShellCommandTool
21
+ from .shell_tool import ShellTool
22
+ from .spawn_agent_tool import SpawnAgentTool
23
+ from .unified_exec_manager import UnifiedExecManager
24
+ from .update_plan_tool import UpdatePlanTool
25
+ from .view_image_tool import ViewImageTool
26
+ from .wait_agent_tool import WaitAgentTool
27
+ from .wait_tool import WaitTool
28
+ from .web_search_tool import WebSearchTool
29
+ from .write_stdin_tool import WriteStdinTool
30
+
31
+ __all__ = [
32
+ "ApplyPatchTool",
33
+ "BaseTool",
34
+ "CloseAgentTool",
35
+ "CodeModeManager",
36
+ "ExecTool",
37
+ "ExecCommandTool",
38
+ "GrepFilesTool",
39
+ "ListDirTool",
40
+ "ReadFileTool",
41
+ "Registry",
42
+ "RequestPermissionsTool",
43
+ "RequestUserInputTool",
44
+ "ResumeAgentTool",
45
+ "SendInputTool",
46
+ "ShellCommandTool",
47
+ "ShellTool",
48
+ "SpawnAgentTool",
49
+ "ToolContext",
50
+ "ToolRegistry",
51
+ "UnifiedExecManager",
52
+ "UpdatePlanTool",
53
+ "ViewImageTool",
54
+ "WaitAgentTool",
55
+ "WaitTool",
56
+ "WebSearchTool",
57
+ "WriteStdinTool",
58
+ ]