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
@@ -1,25 +1,45 @@
1
-
2
1
  import asyncio
3
2
  import json
4
3
  import random
5
- from dataclasses import dataclass, field
4
+ import typing
5
+ from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Awaitable, Callable
7
7
 
8
8
  from .compat import Literal
9
- from .protocol import ConversationItem, ToolCall, ToolResult, TurnResult
9
+ from .events import (
10
+ CompactCompletedEvent,
11
+ CompactFailedEvent,
12
+ CompactStartedEvent,
13
+ Event,
14
+ TurnCompletedEvent,
15
+ TurnFailedEvent,
16
+ TurnInterruptedEvent,
17
+ TurnStartedEvent,
18
+ )
19
+ from .protocol import ConversationItem, ToolCall, ToolResult
10
20
  from .utils import uuid7_string
11
- import typing
12
21
 
13
22
  if TYPE_CHECKING:
14
- from .runtime import CliSubmissionQueue
23
+ from .runtime import AgentRuntime
15
24
 
16
25
  PlanStatus = Literal["pending", "in_progress", "completed"]
26
+ AgentStatus = typing.Union[
27
+ Literal["pending_init", "running", "shutdown", "not_found"],
28
+ typing.Dict[str, typing.Union[str, None]],
29
+ ]
17
30
  PlanListener = Callable[[typing.Dict[str, object]], None]
18
- SubmissionQueueBuilder = Callable[
19
- [typing.Union[str, None], typing.Union[str, None], typing.Tuple[ConversationItem, ...], str],
20
- "CliSubmissionQueue",
31
+ AgentRuntimeBuilder = Callable[
32
+ [
33
+ typing.Union[str, None],
34
+ typing.Union[str, None],
35
+ typing.Tuple[ConversationItem, ...],
36
+ str,
37
+ ],
38
+ "AgentRuntime",
39
+ ]
40
+ AsyncJSONHandler = Callable[
41
+ [typing.Dict[str, object]], Awaitable[typing.Union[typing.Dict[str, object], None]]
21
42
  ]
22
- AsyncJSONHandler = Callable[[typing.Dict[str, object]], Awaitable[typing.Union[typing.Dict[str, object], None]]]
23
43
 
24
44
  DEFAULT_AGENT_NICKNAME_CANDIDATES = (
25
45
  "Bacon",
@@ -114,22 +134,28 @@ DEFAULT_AGENT_NICKNAME_CANDIDATES = (
114
134
  )
115
135
 
116
136
 
117
- @dataclass(frozen=True, )
137
+ @dataclass(
138
+ frozen=True,
139
+ )
118
140
  class PlanItem:
119
- step: 'str'
120
- status: 'PlanStatus'
141
+ step: "str"
142
+ status: "PlanStatus"
121
143
 
122
144
 
123
145
  class PlanStore:
124
- def __init__(self) -> 'None':
125
- self._explanation: 'typing.Union[str, None]' = None
126
- self._plan: 'typing.Tuple[PlanItem, ...]' = ()
127
- self._listener: 'PlanListener' = lambda _payload: None
146
+ def __init__(self) -> "None":
147
+ self._explanation: "typing.Union[str, None]" = None
148
+ self._plan: "typing.Tuple[PlanItem, ...]" = ()
149
+ self._listener: "PlanListener" = lambda _payload: None
128
150
 
129
- def set_listener(self, listener: 'typing.Union[PlanListener, None]') -> 'None':
151
+ def set_listener(self, listener: "typing.Union[PlanListener, None]") -> "None":
130
152
  self._listener = listener or (lambda _payload: None)
131
153
 
132
- def update(self, explanation: 'typing.Union[str, None]', plan: 'typing.Tuple[PlanItem, ...]') -> 'None':
154
+ def update(
155
+ self,
156
+ explanation: "typing.Union[str, None]",
157
+ plan: "typing.Tuple[PlanItem, ...]",
158
+ ) -> "None":
133
159
  in_progress = sum(1 for item in plan if item.status == "in_progress")
134
160
  if in_progress > 1:
135
161
  raise ValueError("at most one plan step can be in_progress")
@@ -139,30 +165,28 @@ class PlanStore:
139
165
  {
140
166
  "explanation": explanation,
141
167
  "plan": [
142
- {"step": item.step, "status": item.status}
143
- for item in self._plan
168
+ {"step": item.step, "status": item.status} for item in self._plan
144
169
  ],
145
170
  }
146
171
  )
147
172
 
148
- def snapshot(self) -> 'typing.Dict[str, object]':
173
+ def snapshot(self) -> "typing.Dict[str, object]":
149
174
  return {
150
175
  "explanation": self._explanation,
151
- "plan": [
152
- {"step": item.step, "status": item.status}
153
- for item in self._plan
154
- ],
176
+ "plan": [{"step": item.step, "status": item.status} for item in self._plan],
155
177
  }
156
178
 
157
179
 
158
180
  class RequestUserInputManager:
159
- def __init__(self) -> 'None':
160
- self._handler: 'typing.Union[AsyncJSONHandler, None]' = None
181
+ def __init__(self) -> "None":
182
+ self._handler: "typing.Union[AsyncJSONHandler, None]" = None
161
183
 
162
- def set_handler(self, handler: 'typing.Union[AsyncJSONHandler, None]') -> 'None':
184
+ def set_handler(self, handler: "typing.Union[AsyncJSONHandler, None]") -> "None":
163
185
  self._handler = handler
164
186
 
165
- async def request(self, payload: 'typing.Dict[str, object]') -> 'typing.Union[typing.Dict[str, object], None]':
187
+ async def request(
188
+ self, payload: "typing.Dict[str, object]"
189
+ ) -> "typing.Union[typing.Dict[str, object], None]":
166
190
  handler = self._handler
167
191
  if handler is None:
168
192
  return None
@@ -170,13 +194,15 @@ class RequestUserInputManager:
170
194
 
171
195
 
172
196
  class RequestPermissionsManager:
173
- def __init__(self) -> 'None':
174
- self._handler: 'typing.Union[AsyncJSONHandler, None]' = None
197
+ def __init__(self) -> "None":
198
+ self._handler: "typing.Union[AsyncJSONHandler, None]" = None
175
199
 
176
- def set_handler(self, handler: 'typing.Union[AsyncJSONHandler, None]') -> 'None':
200
+ def set_handler(self, handler: "typing.Union[AsyncJSONHandler, None]") -> "None":
177
201
  self._handler = handler
178
202
 
179
- async def request(self, payload: 'typing.Dict[str, object]') -> 'typing.Union[typing.Dict[str, object], None]':
203
+ async def request(
204
+ self, payload: "typing.Dict[str, object]"
205
+ ) -> "typing.Union[typing.Dict[str, object], None]":
180
206
  handler = self._handler
181
207
  if handler is None:
182
208
  return None
@@ -185,53 +211,58 @@ class RequestPermissionsManager:
185
211
 
186
212
  @dataclass
187
213
  class ManagedAgent:
188
- agent_id: 'str'
189
- queue: '"CliSubmissionQueue"'
190
- worker_task: 'asyncio.Task[None]'
191
- nickname: 'typing.Union[str, None]' = None
192
- state: 'str' = "pending_init"
193
- completed_message: 'typing.Union[str, None]' = None
194
- error_message: 'typing.Union[str, None]' = None
195
- pending_submission_ids: 'typing.Set[str]' = field(default_factory=set)
214
+ agent_id: "str"
215
+ runtime: '"AgentRuntime"'
216
+ nickname: "typing.Union[str, None]" = None
217
+ last_status: "AgentStatus" = "pending_init"
196
218
 
197
219
 
198
220
  class SubAgentManager:
199
- def __init__(self) -> 'None':
200
- self._queue_builder: 'typing.Union[SubmissionQueueBuilder, None]' = None
201
- self._agents: 'typing.Dict[str, ManagedAgent]' = {}
202
- self._condition = asyncio.Condition()
203
- self._available_nicknames: 'typing.List[str]' = []
221
+ def __init__(self) -> "None":
222
+ self._runtime_builder: "typing.Union[AgentRuntimeBuilder, None]" = None
223
+ self._agents: "typing.Dict[str, ManagedAgent]" = {}
224
+ self._condition = None
225
+ self._available_nicknames: "typing.List[str]" = []
204
226
  self._nickname_random = random.Random()
205
227
 
206
- def set_queue_builder(self, builder: 'typing.Union[SubmissionQueueBuilder, None]') -> 'None':
207
- self._queue_builder = builder
228
+ def _get_condition(self):
229
+ if self._condition is None:
230
+ self._condition = asyncio.Condition()
231
+ return self._condition
232
+
233
+ def set_runtime_builder(
234
+ self, builder: "typing.Union[AgentRuntimeBuilder, None]"
235
+ ) -> "None":
236
+ self._runtime_builder = builder
208
237
 
209
238
  async def spawn_agent(
210
239
  self,
211
- message: 'typing.Union[str, None]',
212
- items: 'typing.Union[typing.List[typing.Dict[str, object]], None]',
213
- agent_type: 'typing.Union[str, None]',
214
- fork_context: 'bool',
215
- model: 'typing.Union[str, None]',
216
- reasoning_effort: 'typing.Union[str, None]',
217
- history: 'typing.Tuple[ConversationItem, ...]',
218
- ) -> 'typing.Dict[str, object]':
219
- builder = self._queue_builder
240
+ message: "typing.Union[str, None]",
241
+ items: "typing.Union[typing.List[typing.Dict[str, object]], None]",
242
+ agent_type: "typing.Union[str, None]",
243
+ fork_context: "bool",
244
+ model: "typing.Union[str, None]",
245
+ reasoning_effort: "typing.Union[str, None]",
246
+ history: "typing.Tuple[ConversationItem, ...]",
247
+ ) -> "typing.Dict[str, object]":
248
+ builder = self._runtime_builder
220
249
  if builder is None:
221
- raise RuntimeError("spawn_agent is unavailable before queue initialization")
250
+ raise RuntimeError(
251
+ "spawn_agent is unavailable before runtime initialization"
252
+ )
222
253
 
223
254
  initial_history = _fork_context_history(history) if fork_context else ()
224
255
  agent_id = uuid7_string()
225
- queue = builder(model, reasoning_effort, initial_history, agent_id)
226
- worker_task = asyncio.create_task(queue.run_forever())
256
+ runtime = builder(model, reasoning_effort, initial_history, agent_id)
257
+ await runtime.start()
227
258
  nickname = self._next_nickname()
228
259
  managed = ManagedAgent(
229
260
  agent_id=agent_id,
230
- queue=queue,
231
- worker_task=worker_task,
261
+ runtime=runtime,
232
262
  nickname=nickname,
233
263
  )
234
- async with self._condition:
264
+ runtime.event_handler = lambda event: self._handle_agent_event(managed, event)
265
+ async with self._get_condition():
235
266
  self._agents[agent_id] = managed
236
267
  self._condition.notify_all()
237
268
 
@@ -246,58 +277,65 @@ class SubAgentManager:
246
277
 
247
278
  async def send_input(
248
279
  self,
249
- agent_id: 'str',
250
- prompt_text: 'str',
251
- interrupt: 'bool',
252
- ) -> 'typing.Dict[str, object]':
280
+ agent_id: "str",
281
+ prompt_text: "str",
282
+ interrupt: "bool",
283
+ ) -> "typing.Dict[str, object]":
253
284
  managed = self._agents.get(agent_id)
254
285
  if managed is None:
255
286
  raise RuntimeError(f"unknown agent: {agent_id}")
256
- if managed.state == "shutdown":
287
+ if managed.runtime.agent.is_shutdown:
257
288
  raise RuntimeError(f"agent is shutdown: {agent_id}")
258
289
 
259
- submission_id, future = await managed.queue.enqueue_user_turn(
290
+ submission_id, future = await managed.runtime.enqueue_user_turn(
260
291
  prompt_text,
261
292
  queue="steer" if interrupt else "enqueue",
262
293
  )
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:
294
+ future.add_done_callback(self._submission_finished)
295
+ async with self._get_condition():
269
296
  self._condition.notify_all()
270
297
  return {"submission_id": submission_id}
271
298
 
272
- async def resume_agent(self, agent_id: 'str') -> 'typing.Dict[str, object]':
299
+ async def resume_agent(self, agent_id: "str") -> "typing.Dict[str, object]":
273
300
  managed = self._agents.get(agent_id)
274
301
  if managed is None:
275
302
  return {"status": "not_found"}
276
- if managed.worker_task.done():
277
- managed.worker_task = asyncio.create_task(managed.queue.run_forever())
278
- managed.state = "pending_init"
279
- managed.completed_message = None
280
- managed.error_message = None
281
- async with self._condition:
303
+ if managed.runtime.agent.is_shutdown:
304
+ managed.runtime.resume()
305
+ managed.last_status = "pending_init"
306
+ await managed.runtime.start()
307
+ async with self._get_condition():
282
308
  self._condition.notify_all()
283
309
  return {"status": self._status_payload(managed)}
284
310
 
285
- async def close_agent(self, agent_id: 'str') -> 'typing.Dict[str, object]':
311
+ async def close_agent(self, agent_id: "str") -> "typing.Dict[str, object]":
286
312
  managed = self._agents.get(agent_id)
287
313
  if managed is None:
288
314
  return {"previous_status": "not_found"}
289
315
  previous_status = self._status_payload(managed)
290
- if not managed.worker_task.done():
291
- managed.queue._agent.interrupt_asap = True
292
- await managed.queue.shutdown()
293
- await managed.worker_task
294
- managed.state = "shutdown"
295
- managed.pending_submission_ids.clear()
296
- async with self._condition:
316
+ await managed.runtime.close()
317
+ async with self._get_condition():
297
318
  self._condition.notify_all()
298
319
  return {"previous_status": previous_status}
299
320
 
300
- def _next_nickname(self) -> 'str':
321
+ async def shutdown(self) -> "None":
322
+ errors = []
323
+ for agent_id in tuple(self._agents):
324
+ try:
325
+ await self.close_agent(agent_id)
326
+ except Exception as exc:
327
+ errors.append(exc)
328
+ if errors:
329
+ for error in errors[1:]:
330
+ asyncio.get_running_loop().call_exception_handler(
331
+ {
332
+ "message": "Sub-agent close failed",
333
+ "exception": error,
334
+ }
335
+ )
336
+ raise errors[0]
337
+
338
+ def _next_nickname(self) -> "str":
301
339
  if not self._available_nicknames:
302
340
  self._available_nicknames = list(DEFAULT_AGENT_NICKNAME_CANDIDATES)
303
341
  self._nickname_random.shuffle(self._available_nicknames)
@@ -305,59 +343,73 @@ class SubAgentManager:
305
343
 
306
344
  async def wait_agents(
307
345
  self,
308
- agent_ids: 'typing.List[str]',
309
- timeout_ms: 'int' = 30_000,
310
- ) -> 'typing.Dict[str, object]':
346
+ agent_ids: "typing.List[str]",
347
+ timeout_ms: "int" = 30_000,
348
+ ) -> "typing.Dict[str, object]":
311
349
  timeout_seconds = max(timeout_ms, 1) / 1000.0
312
350
  loop = asyncio.get_running_loop()
313
351
  deadline = loop.time() + timeout_seconds
314
352
 
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:
353
+ async with self._get_condition():
354
+ while True:
355
+ snapshot = {
356
+ agent_id: self._status_payload(self._agents.get(agent_id))
357
+ for agent_id in agent_ids
358
+ }
359
+ if any(self._is_final_status(status) for status in snapshot.values()):
360
+ return {"status": snapshot, "timed_out": False}
361
+ remaining = deadline - loop.time()
362
+ if remaining <= 0:
331
363
  return {"status": {}, "timed_out": True}
332
-
333
- async def _track_submission(
334
- self,
335
- managed: 'ManagedAgent',
336
- submission_id: 'str',
337
- future: 'asyncio.Future[typing.Union[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
- if managed.pending_submission_ids and managed.error_message is None:
350
- managed.completed_message = None
351
- managed.state = "running"
352
- async with self._condition:
353
- self._condition.notify_all()
364
+ waiter = asyncio.create_task(self._condition.wait())
365
+ try:
366
+ done, _pending = await asyncio.wait({waiter}, timeout=remaining)
367
+ if not done:
368
+ return {"status": {}, "timed_out": True}
369
+ await waiter
370
+ finally:
371
+ if not waiter.done():
372
+ waiter.cancel()
373
+ # Condition.wait() must reacquire the lock before we leave
374
+ # the context. Python 3.6 wait_for() does not await that.
375
+ try:
376
+ await waiter
377
+ except asyncio.CancelledError:
378
+ pass
379
+
380
+ def _submission_finished(self, future: "asyncio.Future") -> "None":
381
+ if not future.cancelled():
382
+ future.exception()
383
+ asyncio.create_task(self._notify_waiters())
384
+
385
+ def _handle_agent_event(self, managed: "ManagedAgent", event: "Event") -> "None":
386
+ if isinstance(event, TurnCompletedEvent):
387
+ managed.last_status = {"completed": event.output_text}
388
+ elif isinstance(event, CompactCompletedEvent):
389
+ managed.last_status = {"completed": None}
390
+ elif isinstance(event, (TurnFailedEvent, CompactFailedEvent)):
391
+ managed.last_status = {
392
+ "errored": "{0}: {1}".format(
393
+ event.error_type,
394
+ event.error,
395
+ )
396
+ }
397
+ elif isinstance(event, TurnInterruptedEvent):
398
+ managed.last_status = {"errored": "TurnInterrupted: turn interrupted"}
399
+ elif not isinstance(event, (TurnStartedEvent, CompactStartedEvent)):
400
+ return
401
+ asyncio.create_task(self._notify_waiters())
402
+
403
+ async def _notify_waiters(self) -> "None":
404
+ async with self._get_condition():
405
+ self._condition.notify_all()
354
406
 
355
407
  def _compose_prompt(
356
408
  self,
357
- message: 'typing.Union[str, None]',
358
- items: 'typing.Union[typing.List[typing.Dict[str, object]], None]',
359
- ) -> 'str':
360
- parts: 'typing.List[str]' = []
409
+ message: "typing.Union[str, None]",
410
+ items: "typing.Union[typing.List[typing.Dict[str, object]], None]",
411
+ ) -> "str":
412
+ parts: "typing.List[str]" = []
361
413
  if message:
362
414
  parts.append(message.strip())
363
415
  for item in items or []:
@@ -374,18 +426,20 @@ class SubAgentManager:
374
426
  parts.append(json.dumps(item, ensure_ascii=False))
375
427
  return "\n\n".join(part for part in parts if part)
376
428
 
377
- def _status_payload(self, managed: 'typing.Union[ManagedAgent, None]') -> 'object':
429
+ def _status_payload(
430
+ self, managed: "typing.Union[ManagedAgent, None]"
431
+ ) -> "AgentStatus":
378
432
  if managed is None:
379
433
  return "not_found"
380
- if managed.error_message is not None:
381
- return {"errored": managed.error_message}
382
- if managed.state == "completed":
383
- return {"completed": managed.completed_message}
384
- if managed.state in {"pending_init", "running", "shutdown"}:
385
- return managed.state
386
- return managed.state
387
-
388
- def _is_final_status(self, status: 'object') -> 'bool':
434
+ agent = managed.runtime.agent
435
+ if agent.is_shutdown:
436
+ return "shutdown"
437
+ if managed.runtime.is_busy:
438
+ return "running"
439
+ status = managed.last_status
440
+ return dict(status) if isinstance(status, dict) else status
441
+
442
+ def _is_final_status(self, status: "AgentStatus") -> "bool":
389
443
  if isinstance(status, str):
390
444
  return status in {"shutdown", "not_found"}
391
445
  if isinstance(status, dict):
@@ -394,8 +448,8 @@ class SubAgentManager:
394
448
 
395
449
 
396
450
  def _fork_context_history(
397
- history: 'typing.Tuple[ConversationItem, ...]',
398
- ) -> 'typing.Tuple[ConversationItem, ...]':
451
+ history: "typing.Tuple[ConversationItem, ...]",
452
+ ) -> "typing.Tuple[ConversationItem, ...]":
399
453
  call_ids = set()
400
454
  result_ids = set()
401
455
  for item in history:
@@ -416,19 +470,12 @@ def _fork_context_history(
416
470
 
417
471
 
418
472
  class AgentRuntimeEnvironment:
419
- def __init__(self) -> 'None':
473
+ def __init__(self) -> "None":
420
474
  self.plan_store = PlanStore()
421
475
  self.subagent_manager = SubAgentManager()
422
476
  self.request_user_input_manager = RequestUserInputManager()
423
477
  self.request_permissions_manager = RequestPermissionsManager()
424
478
 
425
479
 
426
- def create_agent_runtime_environment() -> 'AgentRuntimeEnvironment':
480
+ def create_agent_runtime_environment() -> "AgentRuntimeEnvironment":
427
481
  return AgentRuntimeEnvironment()
428
-
429
-
430
- _RUNTIME_ENV = create_agent_runtime_environment()
431
-
432
-
433
- def get_agent_runtime_environment() -> 'AgentRuntimeEnvironment':
434
- return _RUNTIME_ENV
pycodex/tools/__init__.py CHANGED
@@ -4,8 +4,8 @@ This package groups the local tool abstractions and concrete tool
4
4
  implementations that back `pycodex`.
5
5
  """
6
6
 
7
- from .base_tool import BaseTool, Registry, ToolContext, ToolRegistry
8
7
  from .apply_patch_tool import ApplyPatchTool
8
+ from .base_tool import BaseTool, Registry, ToolContext, ToolRegistry
9
9
  from .clock_tool import ClockManager, ClockTool
10
10
  from .close_agent_tool import CloseAgentTool
11
11
  from .code_mode_manager import CodeModeManager