python-codex 0.2.6__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 +18 -14
  2. pycodex/agent.py +468 -462
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -436
  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 +329 -252
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +90 -52
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +235 -803
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -174
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +4 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +201 -0
  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 +13 -13
  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 +50 -66
  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/utils/image_utils.py +76 -0
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +263 -161
  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 +25 -22
  61. responses_server/messages_api.py +96 -49
  62. responses_server/payload_processors.py +25 -19
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +196 -107
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +51 -13
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +470 -384
  71. workspace_server/workspace.html +859 -232
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +168 -100
  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 -553
  80. python_codex-0.2.6.dist-info/METADATA +0 -441
  81. python_codex-0.2.6.dist-info/RECORD +0 -91
  82. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.6.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/bootstrap.py ADDED
@@ -0,0 +1,417 @@
1
+ import atexit
2
+ import os
3
+ import sys
4
+ import typing
5
+ from dataclasses import replace
6
+ from pathlib import Path
7
+
8
+ from .agent import Agent
9
+ from .context import ContextConfig
10
+ from .model import (
11
+ DEFAULT_CODEX_CONFIG_PATH,
12
+ ResponsesModelClient,
13
+ ResponsesProviderConfig,
14
+ )
15
+ from .runtime import AgentRuntime
16
+ from .runtime_services import AgentRuntimeEnvironment, create_agent_runtime_environment
17
+ from .utils import get_debug_dir, load_codex_dotenv, uuid7_string
18
+ from .utils.session_persist import rollout_path_for_session
19
+
20
+ LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
21
+ CLI_ORIGINATOR = "codex-tui"
22
+
23
+
24
+ def launch_chat_completion_compat_server(*args, **kwargs):
25
+ from responses_server import (
26
+ launch_chat_completion_compat_server as launch_compat_server,
27
+ )
28
+
29
+ return launch_compat_server(*args, **kwargs)
30
+
31
+
32
+ def _resolve_vllm_model(
33
+ endpoint: "str",
34
+ provider_config: "ResponsesProviderConfig",
35
+ timeout_seconds: "float",
36
+ ) -> "str":
37
+ from responses_server import CompatServerConfig
38
+
39
+ normalized = CompatServerConfig.from_base_url(endpoint)
40
+ probe_config = replace(
41
+ provider_config,
42
+ provider_name="vllm",
43
+ base_url=normalized.outcomming_base_url,
44
+ api_key_env=None,
45
+ query_params={},
46
+ responses_lite_override=False,
47
+ )
48
+ probe_client = ResponsesModelClient(
49
+ probe_config,
50
+ timeout_seconds,
51
+ originator=CLI_ORIGINATOR,
52
+ )
53
+ models = probe_client.list_models_sync()
54
+ if not models:
55
+ raise RuntimeError(
56
+ "vLLM endpoint returned no models from "
57
+ f"{normalized.outcomming_models_url()}"
58
+ )
59
+ return models[-1]
60
+
61
+
62
+ def configure_loguru() -> "None":
63
+ try:
64
+ from loguru import logger
65
+ except ImportError: # pragma: no cover - dependency may be absent in minimal envs
66
+ return
67
+
68
+ logger.remove()
69
+ debug_dir = get_debug_dir()
70
+ if debug_dir is not None:
71
+ logger.add(str(debug_dir / "loguru.log"), level="DEBUG")
72
+ return
73
+
74
+ if os.environ.get("PYCODEX_DEBUG_STDERR", "").strip().lower() in {
75
+ "1",
76
+ "true",
77
+ "yes",
78
+ "on",
79
+ }:
80
+ logger.add(sys.stderr, level="DEBUG")
81
+
82
+
83
+ def get_tools(
84
+ runtime_environment: "typing.Union[AgentRuntimeEnvironment, None]" = None,
85
+ exec_mode: "bool" = False,
86
+ cwd: "typing.Union[str, Path, None]" = None,
87
+ toolset: "typing.Union[typing.Iterable[str], None]" = None,
88
+ ):
89
+ from .tools import (
90
+ ApplyPatchTool,
91
+ ClockManager,
92
+ ClockTool,
93
+ CloseAgentTool,
94
+ CodeModeManager,
95
+ ExecCommandTool,
96
+ ExecTool,
97
+ GrepFilesTool,
98
+ ListDirTool,
99
+ ReadFileTool,
100
+ Registry,
101
+ RequestPermissionsTool,
102
+ RequestUserInputTool,
103
+ ResumeAgentTool,
104
+ SendInputTool,
105
+ ShellCommandTool,
106
+ ShellTool,
107
+ SpawnAgentTool,
108
+ UnifiedExecManager,
109
+ UpdatePlanTool,
110
+ ViewImageTool,
111
+ WaitAgentTool,
112
+ WaitTool,
113
+ WebSearchTool,
114
+ WriteStdinTool,
115
+ )
116
+
117
+ runtime_environment = runtime_environment or create_agent_runtime_environment()
118
+ registry = Registry(runtime_environment)
119
+ code_mode_manager = CodeModeManager(registry, cwd=cwd)
120
+ unified_exec_manager = UnifiedExecManager(cwd=cwd)
121
+ clock_manager = ClockManager()
122
+ exec_tool = ExecTool(code_mode_manager)
123
+ wait_tool = WaitTool(code_mode_manager)
124
+ web_search_tool = WebSearchTool()
125
+ update_plan_tool = UpdatePlanTool(runtime_environment.plan_store)
126
+ request_user_input_tool = RequestUserInputTool()
127
+ request_permissions_tool = RequestPermissionsTool(
128
+ runtime_environment.request_permissions_manager
129
+ )
130
+ spawn_agent_tool = SpawnAgentTool(runtime_environment.subagent_manager)
131
+ send_input_tool = SendInputTool(runtime_environment.subagent_manager)
132
+ resume_agent_tool = ResumeAgentTool(runtime_environment.subagent_manager)
133
+ wait_agent_tool = WaitAgentTool(runtime_environment.subagent_manager)
134
+ close_agent_tool = CloseAgentTool(runtime_environment.subagent_manager)
135
+ apply_patch_tool = ApplyPatchTool(cwd=cwd)
136
+ shell_tool = ShellTool(cwd=cwd)
137
+ shell_command_tool = ShellCommandTool(cwd=cwd)
138
+ exec_command_tool = ExecCommandTool(unified_exec_manager)
139
+ write_stdin_tool = WriteStdinTool(unified_exec_manager)
140
+ clock_tool = ClockTool(clock_manager)
141
+ grep_files_tool = GrepFilesTool(cwd=cwd)
142
+ read_file_tool = ReadFileTool()
143
+ list_dir_tool = ListDirTool()
144
+ view_image_tool = ViewImageTool(cwd=cwd)
145
+ tools = (
146
+ shell_tool,
147
+ shell_command_tool,
148
+ exec_command_tool,
149
+ write_stdin_tool,
150
+ clock_tool,
151
+ exec_tool,
152
+ wait_tool,
153
+ web_search_tool,
154
+ update_plan_tool,
155
+ request_user_input_tool,
156
+ request_permissions_tool,
157
+ spawn_agent_tool,
158
+ send_input_tool,
159
+ resume_agent_tool,
160
+ wait_agent_tool,
161
+ close_agent_tool,
162
+ apply_patch_tool,
163
+ grep_files_tool,
164
+ read_file_tool,
165
+ list_dir_tool,
166
+ view_image_tool,
167
+ )
168
+ if toolset is not None:
169
+ available_tools = {tool.name: tool for tool in tools}
170
+ toolset = tuple(toolset)
171
+ unknown_tools = set(toolset) - set(available_tools)
172
+ if unknown_tools:
173
+ raise ValueError(
174
+ "unknown toolset entries: {0}".format(", ".join(sorted(unknown_tools)))
175
+ )
176
+ tools = tuple(available_tools[name] for name in toolset)
177
+ elif exec_mode:
178
+ tools = (
179
+ exec_command_tool,
180
+ write_stdin_tool,
181
+ clock_tool,
182
+ update_plan_tool,
183
+ request_user_input_tool,
184
+ apply_patch_tool,
185
+ web_search_tool,
186
+ view_image_tool,
187
+ spawn_agent_tool,
188
+ send_input_tool,
189
+ resume_agent_tool,
190
+ wait_agent_tool,
191
+ close_agent_tool,
192
+ )
193
+ for tool in tools:
194
+ registry.register(tool)
195
+ return registry
196
+
197
+
198
+ def get_subagent_tools(
199
+ runtime_environment: "typing.Union[AgentRuntimeEnvironment, None]" = None,
200
+ cwd: "typing.Union[str, Path, None]" = None,
201
+ ):
202
+ from .tools import (
203
+ ApplyPatchTool,
204
+ ExecCommandTool,
205
+ Registry,
206
+ UnifiedExecManager,
207
+ UpdatePlanTool,
208
+ ViewImageTool,
209
+ WebSearchTool,
210
+ WriteStdinTool,
211
+ )
212
+
213
+ runtime_environment = runtime_environment or create_agent_runtime_environment()
214
+ registry = Registry(runtime_environment)
215
+ unified_exec_manager = UnifiedExecManager(cwd=cwd)
216
+ registry.register(ExecCommandTool(unified_exec_manager))
217
+ registry.register(WriteStdinTool(unified_exec_manager))
218
+ registry.register(UpdatePlanTool(runtime_environment.plan_store))
219
+ registry.register(ApplyPatchTool(cwd=cwd))
220
+ registry.register(WebSearchTool())
221
+ registry.register(ViewImageTool(cwd=cwd))
222
+ return registry
223
+
224
+
225
+ def build_agent(
226
+ client,
227
+ config_path: "typing.Union[str, Path]" = DEFAULT_CODEX_CONFIG_PATH,
228
+ profile: "typing.Union[str, None]" = None,
229
+ system_prompt: "typing.Union[str, None]" = None,
230
+ extra_contextual_user_messages: "typing.Iterable[str]" = (),
231
+ cwd: "typing.Union[str, Path, None]" = None,
232
+ toolset: "typing.Union[typing.Iterable[str], None]" = None,
233
+ ) -> "Agent":
234
+ config_path = str(config_path)
235
+ resolved_cwd = Path(cwd or Path.cwd()).resolve()
236
+ context_config = replace(
237
+ ContextConfig.from_codex_config(config_path, profile),
238
+ base_instructions_override=system_prompt,
239
+ extra_contextual_user_messages=tuple(extra_contextual_user_messages),
240
+ cwd=resolved_cwd,
241
+ )
242
+ runtime_environment = create_agent_runtime_environment()
243
+
244
+ def make_subagent_runtime_builder(base_client):
245
+ def build_subagent_runtime(
246
+ model_override: "typing.Union[str, None]",
247
+ reasoning_effort_override: "typing.Union[str, None]",
248
+ initial_history=(),
249
+ session_id: "typing.Union[str, None]" = None,
250
+ ) -> "AgentRuntime":
251
+ nested_client = base_client.with_overrides(
252
+ model_override,
253
+ reasoning_effort_override,
254
+ session_id=session_id,
255
+ openai_subagent="collab_spawn",
256
+ )
257
+ subagent_agent_runtime_environment = create_agent_runtime_environment()
258
+ subagent_agent_runtime_environment.subagent_manager.set_runtime_builder(
259
+ make_subagent_runtime_builder(nested_client)
260
+ )
261
+ sub_agent = Agent(
262
+ nested_client,
263
+ get_subagent_tools(
264
+ subagent_agent_runtime_environment, cwd=resolved_cwd
265
+ ),
266
+ context_config,
267
+ initial_history=tuple(initial_history),
268
+ session_id=session_id,
269
+ )
270
+ return AgentRuntime(sub_agent)
271
+
272
+ return build_subagent_runtime
273
+
274
+ runtime_environment.subagent_manager.set_runtime_builder(
275
+ make_subagent_runtime_builder(client)
276
+ )
277
+ session_id = uuid7_string()
278
+ return Agent(
279
+ client,
280
+ get_tools(
281
+ runtime_environment,
282
+ exec_mode=True,
283
+ cwd=resolved_cwd,
284
+ toolset=toolset,
285
+ ),
286
+ context_config,
287
+ session_file_path=rollout_path_for_session(
288
+ context_config.codex_home, session_id
289
+ ),
290
+ session_id=session_id,
291
+ )
292
+
293
+
294
+ def build_model(
295
+ config_path: "typing.Union[str, Path]" = DEFAULT_CODEX_CONFIG_PATH,
296
+ profile: "typing.Union[str, None]" = None,
297
+ timeout_seconds: "float" = 120.0,
298
+ managed_responses_base_url: "typing.Union[str, None]" = None,
299
+ vllm_endpoint: "typing.Union[str, None]" = None,
300
+ use_chat_completion: "typing.Union[bool, None]" = None,
301
+ use_messages: "bool" = False,
302
+ ):
303
+ load_codex_dotenv(config_path)
304
+ provider_config = ResponsesProviderConfig.from_codex_config(
305
+ config_path,
306
+ profile,
307
+ )
308
+ if use_chat_completion is None:
309
+ use_chat_completion = bool(provider_config.use_chat_completion)
310
+ if use_chat_completion and use_messages:
311
+ raise ValueError("--use-chat-completion and --use-messages cannot be combined")
312
+ if vllm_endpoint and use_messages:
313
+ raise ValueError("--vllm-endpoint and --use-messages cannot be combined")
314
+ uses_local_responses_compat = (
315
+ managed_responses_base_url is not None
316
+ or vllm_endpoint is not None
317
+ or bool(use_chat_completion)
318
+ or use_messages
319
+ )
320
+ if vllm_endpoint is not None:
321
+ provider_config = replace(
322
+ provider_config,
323
+ model=_resolve_vllm_model(
324
+ vllm_endpoint,
325
+ provider_config,
326
+ timeout_seconds,
327
+ ),
328
+ )
329
+ url, key_env = provider_config.base_url, provider_config.api_key_env
330
+ if managed_responses_base_url is not None:
331
+ url, key_env = (
332
+ managed_responses_base_url,
333
+ LOCAL_RESPONSES_SERVER_API_KEY_ENV,
334
+ )
335
+ os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
336
+ elif vllm_endpoint or use_chat_completion or use_messages:
337
+ if vllm_endpoint:
338
+ managed_server = launch_chat_completion_compat_server(
339
+ vllm_endpoint,
340
+ model_provider="vllm",
341
+ )
342
+ else:
343
+ managed_server = launch_chat_completion_compat_server(
344
+ provider_config.base_url,
345
+ provider_config.api_key_env,
346
+ model_provider=provider_config.provider_name,
347
+ outcomming_api=("messages" if use_messages else "chat_completions"),
348
+ )
349
+ atexit.register(managed_server.stop)
350
+ url, key_env = (
351
+ managed_server.base_url,
352
+ LOCAL_RESPONSES_SERVER_API_KEY_ENV,
353
+ )
354
+ os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
355
+
356
+ provider_config = replace(
357
+ provider_config,
358
+ base_url=url,
359
+ api_key_env=key_env,
360
+ responses_lite_override=(
361
+ False
362
+ if uses_local_responses_compat
363
+ else provider_config.responses_lite_override
364
+ ),
365
+ )
366
+ return ResponsesModelClient(
367
+ provider_config,
368
+ timeout_seconds,
369
+ originator=CLI_ORIGINATOR,
370
+ )
371
+
372
+
373
+ def build_runtime(agent: "Agent") -> "AgentRuntime":
374
+ runtime = AgentRuntime(agent)
375
+ register_connection_commands(runtime)
376
+ return runtime
377
+
378
+
379
+ def register_connection_commands(runtime):
380
+ link = None
381
+
382
+ async def unlink(argument):
383
+ nonlocal link
384
+ if argument:
385
+ raise ValueError("Usage: /unlink")
386
+ if link is None:
387
+ return {"kind": "connection", "lines": ["No Feishu card is linked."]}
388
+ link.detach()
389
+ link = None
390
+ return {"kind": "connection", "lines": ["Unlinked Feishu card."]}
391
+
392
+ async def connect(target):
393
+ nonlocal link
394
+ if not target:
395
+ raise ValueError("Usage: /link <feishu-email|open_id|chat_id>")
396
+ if link is not None:
397
+ raise RuntimeError("A Feishu card is already linked. Use /unlink first.")
398
+ from .feishu_link import PycodexRuntimeLink
399
+
400
+ link = await PycodexRuntimeLink(runtime, target).start_async()
401
+ return {
402
+ "kind": "connection",
403
+ "lines": [
404
+ "Linked Feishu card: session_key={0} message_id={1}".format(
405
+ link.session_key,
406
+ link.message_id or "-",
407
+ ),
408
+ ],
409
+ }
410
+
411
+ async def close_link():
412
+ if link is not None:
413
+ await unlink("")
414
+
415
+ runtime.register_command("link", connect)
416
+ runtime.register_command("unlink", unlink)
417
+ runtime.add_close_handler(close_link)