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/__init__.py CHANGED
@@ -1,3 +1,140 @@
1
- """Placeholder package for the initial pycodex release."""
1
+ from .agent import AgentLoop
2
+ from .context import ContextConfig, ContextManager
3
+ from .model import (
4
+ ModelClient,
5
+ NOOP_MODEL_STREAM_EVENT_HANDLER,
6
+ ResponsesApiError,
7
+ ResponsesModelClient,
8
+ ResponsesProviderConfig,
9
+ )
10
+ from .protocol import (
11
+ AgentEvent,
12
+ AssistantMessage,
13
+ ContextMessage,
14
+ ModelResponse,
15
+ ModelStreamEvent,
16
+ Prompt,
17
+ ReasoningItem,
18
+ Submission,
19
+ ToolCall,
20
+ ToolResult,
21
+ ToolSpec,
22
+ TurnResult,
23
+ UserMessage,
24
+ )
25
+ from .runtime import AgentRuntime
26
+ from .runtime_services import (
27
+ PlanStore,
28
+ RequestPermissionsManager,
29
+ RequestUserInputManager,
30
+ SubAgentManager,
31
+ get_runtime_environment,
32
+ )
33
+ from .tools import (
34
+ ApplyPatchTool,
35
+ BaseTool,
36
+ CloseAgentTool,
37
+ CodeModeManager,
38
+ ExecTool,
39
+ ExecCommandTool,
40
+ GrepFilesTool,
41
+ ListDirTool,
42
+ ReadFileTool,
43
+ Registry,
44
+ RequestPermissionsTool,
45
+ RequestUserInputTool,
46
+ ResumeAgentTool,
47
+ SendInputTool,
48
+ ShellCommandTool,
49
+ ShellTool,
50
+ SpawnAgentTool,
51
+ ToolContext,
52
+ ToolRegistry,
53
+ UnifiedExecManager,
54
+ UpdatePlanTool,
55
+ ViewImageTool,
56
+ WaitAgentTool,
57
+ WaitTool,
58
+ WebSearchTool,
59
+ WriteStdinTool,
60
+ )
2
61
 
3
- __version__ = "0.0.1"
62
+ def debug(stop: bool = False):
63
+
64
+ import socket
65
+
66
+ import debugpy
67
+
68
+ if debugpy.is_client_connected():
69
+ return
70
+ try:
71
+ host = socket.gethostbyname(socket.getfqdn(socket.gethostname()))
72
+ port = 5000
73
+
74
+ debugpy.listen((host, port), in_process_debug_adapter=False)
75
+ print("Waiting for debugger...\a")
76
+ print(f"ip: {host} port:{port}", flush=True)
77
+ debugpy.wait_for_client()
78
+ print("Connected.")
79
+ if stop:
80
+ debugpy.breakpoint()
81
+ except Exception as e:
82
+ import traceback
83
+
84
+ print("\n".join(traceback.format_exception(e)))
85
+
86
+ __all__ = [
87
+ "AgentEvent",
88
+ "AgentLoop",
89
+ "AgentRuntime",
90
+ "ApplyPatchTool",
91
+ "AssistantMessage",
92
+ "BaseTool",
93
+ "CloseAgentTool",
94
+ "CodeModeManager",
95
+ "ContextConfig",
96
+ "ContextManager",
97
+ "ContextMessage",
98
+ "ExecTool",
99
+ "ExecCommandTool",
100
+ "GrepFilesTool",
101
+ "ListDirTool",
102
+ "ModelClient",
103
+ "NOOP_MODEL_STREAM_EVENT_HANDLER",
104
+ "Registry",
105
+ "ReadFileTool",
106
+ "RequestPermissionsTool",
107
+ "RequestUserInputTool",
108
+ "ModelResponse",
109
+ "ModelStreamEvent",
110
+ "PlanStore",
111
+ "Prompt",
112
+ "ReasoningItem",
113
+ "RequestPermissionsManager",
114
+ "RequestUserInputManager",
115
+ "ResumeAgentTool",
116
+ "ResponsesApiError",
117
+ "ResponsesModelClient",
118
+ "ResponsesProviderConfig",
119
+ "SendInputTool",
120
+ "ShellCommandTool",
121
+ "ShellTool",
122
+ "SpawnAgentTool",
123
+ "Submission",
124
+ "SubAgentManager",
125
+ "ToolCall",
126
+ "ToolContext",
127
+ "ToolRegistry",
128
+ "UnifiedExecManager",
129
+ "UpdatePlanTool",
130
+ "ToolResult",
131
+ "ToolSpec",
132
+ "TurnResult",
133
+ "UserMessage",
134
+ "ViewImageTool",
135
+ "WaitAgentTool",
136
+ "WaitTool",
137
+ "WebSearchTool",
138
+ "WriteStdinTool",
139
+ "get_runtime_environment",
140
+ ]
pycodex/agent.py ADDED
@@ -0,0 +1,290 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from collections.abc import Callable
6
+
7
+ from .context import ContextManager
8
+ from .model import ModelClient
9
+ from .protocol import (
10
+ AgentEvent,
11
+ AssistantMessage,
12
+ ConversationItem,
13
+ ModelStreamEvent,
14
+ ReasoningItem,
15
+ ToolCall,
16
+ ToolResult,
17
+ TurnResult,
18
+ UserMessage,
19
+ )
20
+ from .tools import ToolContext, ToolRegistry
21
+ from .utils import uuid7_string
22
+
23
+
24
+ EventHandler = Callable[[AgentEvent], None]
25
+ NOOP_EVENT_HANDLER: EventHandler = lambda _event: None
26
+
27
+
28
+ class TurnInterrupted(RuntimeError):
29
+ pass
30
+
31
+
32
+ class AgentLoop:
33
+ """Minimal Python port of Codex's turn loop.
34
+
35
+ The core idea mirrors the Rust implementation:
36
+ build a prompt from history, ask the model for output items, run any tool
37
+ calls, append tool results to history, and keep going until the model emits
38
+ a pure assistant response.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ model_client: ModelClient,
44
+ tool_registry: ToolRegistry,
45
+ context_manager: ContextManager | None = None,
46
+ parallel_tool_calls: bool = True,
47
+ event_handler: EventHandler = NOOP_EVENT_HANDLER,
48
+ initial_history: tuple[ConversationItem, ...] = (),
49
+ ) -> None:
50
+ self._model_client = model_client
51
+ self._tool_registry = tool_registry
52
+ self._context_manager = context_manager or ContextManager()
53
+ self._parallel_tool_calls = parallel_tool_calls
54
+ self._event_handler = event_handler
55
+ self._history: list[ConversationItem] = list(initial_history)
56
+ self.interrupt_asap = False
57
+
58
+ @property
59
+ def history(self) -> tuple[ConversationItem, ...]:
60
+ return tuple(self._history)
61
+
62
+ def set_event_handler(
63
+ self, event_handler: EventHandler = NOOP_EVENT_HANDLER
64
+ ) -> None:
65
+ self._event_handler = event_handler
66
+
67
+ def _raise_if_interrupt_requested(
68
+ self,
69
+ turn_id: str,
70
+ iteration: int,
71
+ output_text: str | None = None,
72
+ ) -> None:
73
+ if self.interrupt_asap:
74
+ self.interrupt_asap = False
75
+ payload: dict[str, object] = {"iteration": iteration}
76
+ if output_text is not None:
77
+ payload["output_text"] = output_text
78
+ self._emit("turn_interrupted", turn_id, **payload)
79
+ raise TurnInterrupted("turn interrupted")
80
+
81
+ async def run_turn(
82
+ self, texts: list[str], turn_id: str | None = None
83
+ ) -> TurnResult:
84
+ turn_id = turn_id or uuid7_string()
85
+ self.interrupt_asap = False
86
+ for text in texts:
87
+ self._history.append(UserMessage(text=text))
88
+
89
+ self._emit(
90
+ "turn_started",
91
+ turn_id,
92
+ user_text="\n".join(texts),
93
+ user_texts=list(texts),
94
+ )
95
+
96
+ last_assistant_message: str | None = None
97
+ final_response_items: tuple[
98
+ AssistantMessage | ToolCall | ReasoningItem, ...
99
+ ] = ()
100
+
101
+ iteration = 0
102
+ try:
103
+ while True:
104
+ self._raise_if_interrupt_requested(
105
+ turn_id,
106
+ iteration,
107
+ output_text=last_assistant_message,
108
+ )
109
+ iteration += 1
110
+ prompt = self._context_manager.build_prompt(
111
+ self._history,
112
+ self._tool_registry.model_visible_specs(),
113
+ self._parallel_tool_calls,
114
+ turn_id=turn_id,
115
+ )
116
+ self._emit(
117
+ "model_called",
118
+ turn_id,
119
+ iteration=iteration,
120
+ history_size=len(prompt.input),
121
+ tool_count=len(prompt.tools),
122
+ )
123
+ response = await self._model_client.complete(
124
+ prompt,
125
+ lambda event: self._handle_model_stream_event(turn_id, event),
126
+ )
127
+ final_response_items = tuple(response.items)
128
+ self._emit(
129
+ "model_completed",
130
+ turn_id,
131
+ iteration=iteration,
132
+ item_count=len(response.items),
133
+ )
134
+
135
+ tool_calls: list[ToolCall] = []
136
+ for item in response.items:
137
+ self._history.append(item)
138
+ if isinstance(item, AssistantMessage):
139
+ last_assistant_message = item.text
140
+ elif isinstance(item, ToolCall):
141
+ tool_calls.append(item)
142
+
143
+ if not tool_calls:
144
+ self._raise_if_interrupt_requested(
145
+ turn_id,
146
+ iteration,
147
+ output_text=last_assistant_message,
148
+ )
149
+ self._emit(
150
+ "turn_completed",
151
+ turn_id,
152
+ iteration=iteration,
153
+ output_text=last_assistant_message,
154
+ )
155
+ return TurnResult(
156
+ turn_id=turn_id,
157
+ output_text=last_assistant_message,
158
+ iterations=iteration,
159
+ response_items=final_response_items,
160
+ history=tuple(self._history),
161
+ )
162
+
163
+ tool_results = await self._execute_tool_batch(turn_id, tool_calls)
164
+ self._history.extend(tool_results)
165
+ self._history.extend(self._build_follow_up_messages(tool_results))
166
+ self._raise_if_interrupt_requested(
167
+ turn_id,
168
+ iteration,
169
+ output_text=last_assistant_message,
170
+ )
171
+ except TurnInterrupted:
172
+ raise
173
+ except Exception as exc:
174
+ self._emit(
175
+ "turn_failed",
176
+ turn_id,
177
+ iteration=iteration,
178
+ error=str(exc),
179
+ error_type=type(exc).__name__,
180
+ )
181
+ raise
182
+
183
+ async def _execute_tool_batch(
184
+ self,
185
+ turn_id: str,
186
+ tool_calls: list[ToolCall],
187
+ ) -> list[ToolResult]:
188
+ results: list[ToolResult] = []
189
+ parallel_batch: list[ToolCall] = []
190
+
191
+ for call in tool_calls:
192
+ can_run_parallel = (
193
+ self._parallel_tool_calls
194
+ and self._tool_registry.supports_parallel(call.name)
195
+ )
196
+ if can_run_parallel:
197
+ parallel_batch.append(call)
198
+ continue
199
+
200
+ if parallel_batch:
201
+ prior_results = tuple(results)
202
+ results.extend(
203
+ await asyncio.gather(
204
+ *(
205
+ self._run_single_tool(turn_id, batched_call, prior_results)
206
+ for batched_call in parallel_batch
207
+ )
208
+ )
209
+ )
210
+ parallel_batch = []
211
+ results.append(await self._run_single_tool(turn_id, call, tuple(results)))
212
+
213
+ if parallel_batch:
214
+ prior_results = tuple(results)
215
+ results.extend(
216
+ await asyncio.gather(
217
+ *(
218
+ self._run_single_tool(turn_id, batched_call, prior_results)
219
+ for batched_call in parallel_batch
220
+ )
221
+ )
222
+ )
223
+ return results
224
+
225
+ async def _run_single_tool(
226
+ self,
227
+ turn_id: str,
228
+ call: ToolCall,
229
+ prior_results: tuple[ToolResult, ...] = (),
230
+ ) -> ToolResult:
231
+ self._emit("tool_started", turn_id, tool_name=call.name, call_id=call.call_id)
232
+ result = await self._tool_registry.execute(
233
+ call,
234
+ ToolContext(
235
+ turn_id=turn_id,
236
+ history=tuple(self._history) + prior_results,
237
+ collaboration_mode=self._context_manager.collaboration_mode,
238
+ ),
239
+ )
240
+ payload: dict[str, object] = {
241
+ "tool_name": call.name,
242
+ "call_id": call.call_id,
243
+ "is_error": result.is_error,
244
+ "call": call,
245
+ "result": result,
246
+ }
247
+ self._emit("tool_completed", turn_id, **payload)
248
+ return result
249
+
250
+ def _emit(self, kind: str, turn_id: str, **payload: object) -> None:
251
+ self._event_handler(
252
+ AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
253
+ )
254
+
255
+ def _handle_model_stream_event(self, turn_id: str, event: ModelStreamEvent) -> None:
256
+ if event.kind == "assistant_delta":
257
+ self._emit("assistant_delta", turn_id, **event.payload)
258
+ elif event.kind == "tool_call":
259
+ self._emit("tool_called", turn_id, **event.payload)
260
+
261
+ def _build_follow_up_messages(
262
+ self,
263
+ tool_results: list[ToolResult],
264
+ ) -> list[UserMessage]:
265
+ follow_ups: list[UserMessage] = []
266
+ for result in tool_results:
267
+ statuses = None
268
+ if (
269
+ result.name == "wait_agent"
270
+ and not result.is_error
271
+ and isinstance(result.output, dict)
272
+ ):
273
+ statuses = result.output.get("status")
274
+ if isinstance(statuses, dict):
275
+ for agent_id, status in statuses.items():
276
+ if isinstance(agent_id, str) and isinstance(status, dict):
277
+ payload = {
278
+ "agent_id": agent_id,
279
+ "status": status,
280
+ }
281
+ follow_ups.append(
282
+ UserMessage(
283
+ text=(
284
+ "<subagent_notification>\n"
285
+ f"{json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}\n"
286
+ "</subagent_notification>"
287
+ )
288
+ )
289
+ )
290
+ return follow_ups