python-codex 0.0.1__py3-none-any.whl → 0.1.1__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.
- pycodex/__init__.py +141 -2
- pycodex/agent.py +290 -0
- pycodex/cli.py +705 -0
- pycodex/collaboration.py +21 -0
- pycodex/context.py +580 -0
- pycodex/doctor.py +360 -0
- pycodex/model.py +533 -0
- pycodex/portable.py +390 -0
- pycodex/portable_server.py +205 -0
- pycodex/prompts/collaboration_default.md +11 -0
- pycodex/prompts/collaboration_plan.md +128 -0
- pycodex/prompts/default_base_instructions.md +275 -0
- pycodex/prompts/exec_tools.json +411 -0
- pycodex/prompts/models.json +847 -0
- pycodex/prompts/permissions/approval_policy/never.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
- pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
- pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
- pycodex/prompts/subagent_tools.json +163 -0
- pycodex/protocol.py +347 -0
- pycodex/runtime.py +204 -0
- pycodex/runtime_services.py +409 -0
- pycodex/tools/__init__.py +58 -0
- pycodex/tools/agent_tool_schemas.py +70 -0
- pycodex/tools/apply_patch_tool.py +363 -0
- pycodex/tools/base_tool.py +168 -0
- pycodex/tools/close_agent_tool.py +55 -0
- pycodex/tools/code_mode_manager.py +519 -0
- pycodex/tools/exec_command_tool.py +96 -0
- pycodex/tools/exec_runtime.js +161 -0
- pycodex/tools/exec_tool.py +48 -0
- pycodex/tools/grep_files_tool.py +150 -0
- pycodex/tools/list_dir_tool.py +135 -0
- pycodex/tools/read_file_tool.py +217 -0
- pycodex/tools/request_permissions_tool.py +95 -0
- pycodex/tools/request_user_input_tool.py +167 -0
- pycodex/tools/resume_agent_tool.py +56 -0
- pycodex/tools/send_input_tool.py +106 -0
- pycodex/tools/shell_command_tool.py +107 -0
- pycodex/tools/shell_tool.py +112 -0
- pycodex/tools/spawn_agent_tool.py +97 -0
- pycodex/tools/unified_exec_manager.py +380 -0
- pycodex/tools/update_plan_tool.py +79 -0
- pycodex/tools/view_image_tool.py +111 -0
- pycodex/tools/wait_agent_tool.py +75 -0
- pycodex/tools/wait_tool.py +68 -0
- pycodex/tools/web_search_tool.py +30 -0
- pycodex/tools/write_stdin_tool.py +75 -0
- pycodex/utils/__init__.py +40 -0
- pycodex/utils/dotenv.py +64 -0
- pycodex/utils/get_env.py +218 -0
- pycodex/utils/random_ids.py +19 -0
- pycodex/utils/visualize.py +978 -0
- python_codex-0.1.1.dist-info/METADATA +355 -0
- python_codex-0.1.1.dist-info/RECORD +62 -0
- python_codex-0.1.1.dist-info/entry_points.txt +2 -0
- python_codex-0.1.1.dist-info/licenses/LICENSE +201 -0
- python_codex-0.0.1.dist-info/METADATA +0 -30
- python_codex-0.0.1.dist-info/RECORD +0 -4
- {python_codex-0.0.1.dist-info → python_codex-0.1.1.dist-info}/WHEEL +0 -0
pycodex/cli.py
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import argparse
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from dataclasses import asdict, replace
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal, Sequence
|
|
14
|
+
|
|
15
|
+
from .agent import AgentLoop
|
|
16
|
+
from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
|
|
17
|
+
from .context import ContextManager
|
|
18
|
+
from .model import ResponsesModelClient, ResponsesProviderConfig
|
|
19
|
+
from .portable import bootstrap_called_home, upload_codex_home
|
|
20
|
+
from .protocol import AgentEvent
|
|
21
|
+
from .runtime import AgentRuntime
|
|
22
|
+
from .runtime_services import RuntimeEnvironment, create_runtime_environment
|
|
23
|
+
from .utils import CliSessionView, load_codex_dotenv
|
|
24
|
+
from responses_server import launch_chat_completion_compat_server
|
|
25
|
+
|
|
26
|
+
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
27
|
+
HISTORY_COMMAND = "/history"
|
|
28
|
+
TITLE_COMMAND = "/title"
|
|
29
|
+
MODEL_COMMAND = "/model"
|
|
30
|
+
QUEUE_COMMAND = "/queue"
|
|
31
|
+
CliSessionMode = Literal["exec", "tui"]
|
|
32
|
+
LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
|
|
33
|
+
CLI_ORIGINATOR = "codex-tui"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def configure_loguru() -> None:
|
|
37
|
+
try:
|
|
38
|
+
from loguru import logger
|
|
39
|
+
except ImportError: # pragma: no cover - dependency may be absent in minimal envs
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
logger.remove()
|
|
43
|
+
log_path = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
|
|
44
|
+
if log_path:
|
|
45
|
+
logger.add(log_path, level="DEBUG")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if os.environ.get("PYCODEX_DEBUG_STDERR", "").strip().lower() in {
|
|
49
|
+
"1",
|
|
50
|
+
"true",
|
|
51
|
+
"yes",
|
|
52
|
+
"on",
|
|
53
|
+
}:
|
|
54
|
+
logger.add(sys.stderr, level="DEBUG")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
58
|
+
parser = argparse.ArgumentParser(
|
|
59
|
+
prog="pycodex",
|
|
60
|
+
description="Minimal Codex-style local CLI backed by ~/.codex/config.toml.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"prompt", nargs="*", help="Prompt text. If omitted, read from stdin."
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--put",
|
|
67
|
+
default=None,
|
|
68
|
+
metavar="PATH@SERVER",
|
|
69
|
+
help=(
|
|
70
|
+
"Upload a Codex home using `--put @host:port` or "
|
|
71
|
+
"`--put /path/.codex@host:port`."
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--call",
|
|
76
|
+
default=None,
|
|
77
|
+
help=(
|
|
78
|
+
"Download and use a stored Codex home via <secret>-<call_id>@<host:port>."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--config",
|
|
83
|
+
default=str(Path.home() / ".codex" / "config.toml"),
|
|
84
|
+
help="Path to Codex config.toml.",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--profile",
|
|
88
|
+
default=None,
|
|
89
|
+
help="Optional profile name from config.toml.",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--vllm-endpoint",
|
|
93
|
+
default=None,
|
|
94
|
+
help=(
|
|
95
|
+
"Optional base URL for a chat-completions-backed vLLM server. "
|
|
96
|
+
"When set, pycodex starts a local responses compat server for this "
|
|
97
|
+
"session and appends /v1 if the path is empty."
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--use-chat-completion",
|
|
102
|
+
default=False,
|
|
103
|
+
action="store_true",
|
|
104
|
+
help=(
|
|
105
|
+
"When set, pycodex starts a local responses compat server for this session."
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--system-prompt",
|
|
110
|
+
default=None,
|
|
111
|
+
help="Optional base instructions override passed to the model.",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--timeout-seconds",
|
|
115
|
+
type=float,
|
|
116
|
+
default=120.0,
|
|
117
|
+
help="HTTP timeout for one model call.",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--json",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="Print the full TurnResult as JSON.",
|
|
123
|
+
)
|
|
124
|
+
return parser
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def should_run_interactive(prompt_parts: Sequence[str], stdin_is_tty: bool) -> bool:
|
|
128
|
+
return not prompt_parts and stdin_is_tty
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
|
|
132
|
+
if prompt_parts:
|
|
133
|
+
return " ".join(prompt_parts).strip()
|
|
134
|
+
|
|
135
|
+
if not sys.stdin.isatty():
|
|
136
|
+
prompt_text = sys.stdin.read().strip()
|
|
137
|
+
if prompt_text:
|
|
138
|
+
return prompt_text
|
|
139
|
+
|
|
140
|
+
raise ValueError("prompt is required either as argv text or stdin")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_tools(
|
|
144
|
+
runtime_environment: RuntimeEnvironment | None = None,
|
|
145
|
+
exec_mode: bool = False,
|
|
146
|
+
):
|
|
147
|
+
from .tools import (
|
|
148
|
+
ApplyPatchTool,
|
|
149
|
+
CloseAgentTool,
|
|
150
|
+
CodeModeManager,
|
|
151
|
+
ExecTool,
|
|
152
|
+
ExecCommandTool,
|
|
153
|
+
GrepFilesTool,
|
|
154
|
+
ListDirTool,
|
|
155
|
+
ReadFileTool,
|
|
156
|
+
RequestPermissionsTool,
|
|
157
|
+
RequestUserInputTool,
|
|
158
|
+
ResumeAgentTool,
|
|
159
|
+
Registry,
|
|
160
|
+
SendInputTool,
|
|
161
|
+
ShellCommandTool,
|
|
162
|
+
ShellTool,
|
|
163
|
+
SpawnAgentTool,
|
|
164
|
+
UnifiedExecManager,
|
|
165
|
+
UpdatePlanTool,
|
|
166
|
+
ViewImageTool,
|
|
167
|
+
WaitAgentTool,
|
|
168
|
+
WaitTool,
|
|
169
|
+
WebSearchTool,
|
|
170
|
+
WriteStdinTool,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
runtime_environment = runtime_environment or create_runtime_environment()
|
|
174
|
+
registry = Registry()
|
|
175
|
+
code_mode_manager = CodeModeManager(registry)
|
|
176
|
+
unified_exec_manager = UnifiedExecManager()
|
|
177
|
+
exec_tool = ExecTool(code_mode_manager)
|
|
178
|
+
wait_tool = WaitTool(code_mode_manager)
|
|
179
|
+
web_search_tool = WebSearchTool()
|
|
180
|
+
update_plan_tool = UpdatePlanTool(runtime_environment.plan_store)
|
|
181
|
+
request_user_input_tool = RequestUserInputTool(
|
|
182
|
+
runtime_environment.request_user_input_manager
|
|
183
|
+
)
|
|
184
|
+
request_permissions_tool = RequestPermissionsTool(
|
|
185
|
+
runtime_environment.request_permissions_manager
|
|
186
|
+
)
|
|
187
|
+
spawn_agent_tool = SpawnAgentTool(runtime_environment.subagent_manager)
|
|
188
|
+
send_input_tool = SendInputTool(runtime_environment.subagent_manager)
|
|
189
|
+
resume_agent_tool = ResumeAgentTool(runtime_environment.subagent_manager)
|
|
190
|
+
wait_agent_tool = WaitAgentTool(runtime_environment.subagent_manager)
|
|
191
|
+
close_agent_tool = CloseAgentTool(runtime_environment.subagent_manager)
|
|
192
|
+
apply_patch_tool = ApplyPatchTool()
|
|
193
|
+
shell_tool = ShellTool()
|
|
194
|
+
shell_command_tool = ShellCommandTool()
|
|
195
|
+
exec_command_tool = ExecCommandTool(unified_exec_manager)
|
|
196
|
+
write_stdin_tool = WriteStdinTool(unified_exec_manager)
|
|
197
|
+
grep_files_tool = GrepFilesTool()
|
|
198
|
+
read_file_tool = ReadFileTool()
|
|
199
|
+
list_dir_tool = ListDirTool()
|
|
200
|
+
view_image_tool = ViewImageTool()
|
|
201
|
+
if exec_mode:
|
|
202
|
+
registry.register(exec_command_tool)
|
|
203
|
+
registry.register(write_stdin_tool)
|
|
204
|
+
registry.register(update_plan_tool)
|
|
205
|
+
registry.register(request_user_input_tool)
|
|
206
|
+
registry.register(apply_patch_tool)
|
|
207
|
+
registry.register(web_search_tool)
|
|
208
|
+
registry.register(view_image_tool)
|
|
209
|
+
registry.register(spawn_agent_tool)
|
|
210
|
+
registry.register(send_input_tool)
|
|
211
|
+
registry.register(resume_agent_tool)
|
|
212
|
+
registry.register(wait_agent_tool)
|
|
213
|
+
registry.register(close_agent_tool)
|
|
214
|
+
return registry
|
|
215
|
+
|
|
216
|
+
registry.register(shell_tool)
|
|
217
|
+
registry.register(shell_command_tool)
|
|
218
|
+
registry.register(exec_command_tool)
|
|
219
|
+
registry.register(write_stdin_tool)
|
|
220
|
+
registry.register(exec_tool)
|
|
221
|
+
registry.register(wait_tool)
|
|
222
|
+
registry.register(web_search_tool)
|
|
223
|
+
registry.register(update_plan_tool)
|
|
224
|
+
registry.register(request_user_input_tool)
|
|
225
|
+
registry.register(request_permissions_tool)
|
|
226
|
+
registry.register(spawn_agent_tool)
|
|
227
|
+
registry.register(send_input_tool)
|
|
228
|
+
registry.register(resume_agent_tool)
|
|
229
|
+
registry.register(wait_agent_tool)
|
|
230
|
+
registry.register(close_agent_tool)
|
|
231
|
+
registry.register(apply_patch_tool)
|
|
232
|
+
registry.register(grep_files_tool)
|
|
233
|
+
registry.register(read_file_tool)
|
|
234
|
+
registry.register(list_dir_tool)
|
|
235
|
+
registry.register(view_image_tool)
|
|
236
|
+
return registry
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
|
|
240
|
+
from .tools import (
|
|
241
|
+
ApplyPatchTool,
|
|
242
|
+
ExecCommandTool,
|
|
243
|
+
Registry,
|
|
244
|
+
UnifiedExecManager,
|
|
245
|
+
UpdatePlanTool,
|
|
246
|
+
ViewImageTool,
|
|
247
|
+
WebSearchTool,
|
|
248
|
+
WriteStdinTool,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
runtime_environment = runtime_environment or create_runtime_environment()
|
|
252
|
+
registry = Registry()
|
|
253
|
+
unified_exec_manager = UnifiedExecManager()
|
|
254
|
+
registry.register(ExecCommandTool(unified_exec_manager))
|
|
255
|
+
registry.register(WriteStdinTool(unified_exec_manager))
|
|
256
|
+
registry.register(UpdatePlanTool(runtime_environment.plan_store))
|
|
257
|
+
registry.register(ApplyPatchTool())
|
|
258
|
+
registry.register(WebSearchTool())
|
|
259
|
+
registry.register(ViewImageTool())
|
|
260
|
+
return registry
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def build_runtime(
|
|
264
|
+
config_path: str,
|
|
265
|
+
profile: str | None,
|
|
266
|
+
system_prompt: str | None,
|
|
267
|
+
client,
|
|
268
|
+
session_mode: CliSessionMode = "exec",
|
|
269
|
+
collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
|
|
270
|
+
) -> AgentRuntime:
|
|
271
|
+
use_tui_context = session_mode == "tui"
|
|
272
|
+
context_manager = ContextManager.from_codex_config(
|
|
273
|
+
config_path,
|
|
274
|
+
profile,
|
|
275
|
+
base_instructions_override=system_prompt,
|
|
276
|
+
collaboration_mode=collaboration_mode,
|
|
277
|
+
include_collaboration_instructions=use_tui_context,
|
|
278
|
+
)
|
|
279
|
+
subagent_context_manager = ContextManager.from_codex_config(
|
|
280
|
+
config_path,
|
|
281
|
+
profile,
|
|
282
|
+
base_instructions_override=system_prompt,
|
|
283
|
+
include_collaboration_instructions=False,
|
|
284
|
+
)
|
|
285
|
+
runtime_environment = create_runtime_environment()
|
|
286
|
+
runtime_environment.request_user_input_manager.set_handler(None)
|
|
287
|
+
runtime_environment.request_permissions_manager.set_handler(None)
|
|
288
|
+
|
|
289
|
+
def make_subagent_runtime_builder(base_client):
|
|
290
|
+
def build_subagent_runtime(
|
|
291
|
+
model_override: str | None,
|
|
292
|
+
reasoning_effort_override: str | None,
|
|
293
|
+
initial_history=(),
|
|
294
|
+
session_id: str | None = None,
|
|
295
|
+
) -> AgentRuntime:
|
|
296
|
+
nested_client = base_client.with_overrides(
|
|
297
|
+
model_override,
|
|
298
|
+
reasoning_effort_override,
|
|
299
|
+
session_id=session_id,
|
|
300
|
+
openai_subagent="collab_spawn",
|
|
301
|
+
)
|
|
302
|
+
subagent_runtime_environment = create_runtime_environment()
|
|
303
|
+
subagent_runtime_environment.request_user_input_manager.set_handler(None)
|
|
304
|
+
subagent_runtime_environment.request_permissions_manager.set_handler(None)
|
|
305
|
+
subagent_runtime_environment.subagent_manager.set_runtime_builder(
|
|
306
|
+
make_subagent_runtime_builder(nested_client)
|
|
307
|
+
)
|
|
308
|
+
sub_agent = AgentLoop(
|
|
309
|
+
nested_client,
|
|
310
|
+
get_subagent_tools(subagent_runtime_environment),
|
|
311
|
+
subagent_context_manager,
|
|
312
|
+
initial_history=tuple(initial_history),
|
|
313
|
+
)
|
|
314
|
+
return AgentRuntime(
|
|
315
|
+
sub_agent, runtime_environment=subagent_runtime_environment
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return build_subagent_runtime
|
|
319
|
+
|
|
320
|
+
runtime_environment.subagent_manager.set_runtime_builder(
|
|
321
|
+
make_subagent_runtime_builder(client)
|
|
322
|
+
)
|
|
323
|
+
return AgentRuntime(
|
|
324
|
+
AgentLoop(
|
|
325
|
+
client, get_tools(runtime_environment, exec_mode=True), context_manager
|
|
326
|
+
),
|
|
327
|
+
runtime_environment=runtime_environment,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def format_turn_output(result, json_mode: bool) -> str:
|
|
332
|
+
if json_mode:
|
|
333
|
+
return json.dumps(asdict(result), ensure_ascii=False, indent=2)
|
|
334
|
+
return result.output_text or ""
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _build_model_client(
|
|
338
|
+
config_path: str,
|
|
339
|
+
profile: str | None,
|
|
340
|
+
timeout_seconds: float,
|
|
341
|
+
managed_responses_base_url: str | None = None,
|
|
342
|
+
vllm_endpoint: str | None = None,
|
|
343
|
+
use_chat_completion: bool = False,
|
|
344
|
+
):
|
|
345
|
+
load_codex_dotenv(config_path)
|
|
346
|
+
provider_config = ResponsesProviderConfig.from_codex_config(
|
|
347
|
+
config_path,
|
|
348
|
+
profile,
|
|
349
|
+
)
|
|
350
|
+
url, key_env = provider_config.base_url, provider_config.api_key_env
|
|
351
|
+
if managed_responses_base_url is not None:
|
|
352
|
+
url, key_env = (
|
|
353
|
+
managed_responses_base_url,
|
|
354
|
+
LOCAL_RESPONSES_SERVER_API_KEY_ENV,
|
|
355
|
+
)
|
|
356
|
+
os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
|
|
357
|
+
elif vllm_endpoint or use_chat_completion:
|
|
358
|
+
if vllm_endpoint:
|
|
359
|
+
managed_server = launch_chat_completion_compat_server(
|
|
360
|
+
vllm_endpoint,
|
|
361
|
+
model_provider="vllm",
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
managed_server = launch_chat_completion_compat_server(
|
|
365
|
+
provider_config.base_url,
|
|
366
|
+
provider_config.api_key_env,
|
|
367
|
+
model_provider=provider_config.provider_name,
|
|
368
|
+
)
|
|
369
|
+
atexit.register(managed_server.stop)
|
|
370
|
+
url, key_env = (
|
|
371
|
+
managed_server.base_url,
|
|
372
|
+
LOCAL_RESPONSES_SERVER_API_KEY_ENV,
|
|
373
|
+
)
|
|
374
|
+
os.environ.setdefault(LOCAL_RESPONSES_SERVER_API_KEY_ENV, "dummy")
|
|
375
|
+
|
|
376
|
+
provider_config = replace(
|
|
377
|
+
provider_config,
|
|
378
|
+
base_url=url,
|
|
379
|
+
api_key_env=key_env,
|
|
380
|
+
)
|
|
381
|
+
return ResponsesModelClient(
|
|
382
|
+
provider_config,
|
|
383
|
+
timeout_seconds,
|
|
384
|
+
originator=CLI_ORIGINATOR,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def prompt_request_user_input(
|
|
389
|
+
view: CliSessionView,
|
|
390
|
+
payload: dict[str, object],
|
|
391
|
+
) -> dict[str, object] | None:
|
|
392
|
+
view.finish_stream()
|
|
393
|
+
view.pause_spinner()
|
|
394
|
+
view.write_line("[request_user_input] waiting for user response")
|
|
395
|
+
answers: dict[str, dict[str, list[str]]] = {}
|
|
396
|
+
try:
|
|
397
|
+
for question in payload.get("questions", []):
|
|
398
|
+
if not isinstance(question, dict):
|
|
399
|
+
continue
|
|
400
|
+
header = str(question.get("header", "")).strip()
|
|
401
|
+
question_text = str(question.get("question", "")).strip()
|
|
402
|
+
question_id = str(question.get("id", "")).strip()
|
|
403
|
+
if header:
|
|
404
|
+
view.write_line(f"[{header}] {question_text}")
|
|
405
|
+
else:
|
|
406
|
+
view.write_line(question_text)
|
|
407
|
+
|
|
408
|
+
options = question.get("options") or []
|
|
409
|
+
if isinstance(options, list):
|
|
410
|
+
for index, option in enumerate(options, start=1):
|
|
411
|
+
if not isinstance(option, dict):
|
|
412
|
+
continue
|
|
413
|
+
label = str(option.get("label", "")).strip()
|
|
414
|
+
description = str(option.get("description", "")).strip()
|
|
415
|
+
view.write_line(f" {index}. {label} - {description}")
|
|
416
|
+
view.write_line(" 0. Other")
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
raw_answer = await view.prompt_async("answer> ")
|
|
420
|
+
except EOFError:
|
|
421
|
+
return None
|
|
422
|
+
answer_text = raw_answer.strip()
|
|
423
|
+
if not answer_text:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
selected_answer = answer_text
|
|
427
|
+
if answer_text.isdigit() and isinstance(options, list):
|
|
428
|
+
choice = int(answer_text)
|
|
429
|
+
if 1 <= choice <= len(options):
|
|
430
|
+
option = options[choice - 1]
|
|
431
|
+
if isinstance(option, dict):
|
|
432
|
+
selected_answer = (
|
|
433
|
+
str(option.get("label", "")).strip() or answer_text
|
|
434
|
+
)
|
|
435
|
+
elif choice == 0:
|
|
436
|
+
try:
|
|
437
|
+
raw_answer = await view.prompt_async("other> ")
|
|
438
|
+
except EOFError:
|
|
439
|
+
return None
|
|
440
|
+
selected_answer = raw_answer.strip()
|
|
441
|
+
if not selected_answer:
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
answers[question_id] = {"answers": [selected_answer]}
|
|
445
|
+
|
|
446
|
+
return {"answers": answers}
|
|
447
|
+
finally:
|
|
448
|
+
view.resume_spinner()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
async def prompt_request_permissions(
|
|
452
|
+
view: CliSessionView,
|
|
453
|
+
payload: dict[str, object],
|
|
454
|
+
) -> dict[str, object] | None:
|
|
455
|
+
view.finish_stream()
|
|
456
|
+
view.pause_spinner()
|
|
457
|
+
view.write_line("[request_permissions] user approval required")
|
|
458
|
+
reason = payload.get("reason")
|
|
459
|
+
if reason:
|
|
460
|
+
view.write_line(f"Reason: {reason}")
|
|
461
|
+
view.write_line("Requested permissions:")
|
|
462
|
+
view.write_line(
|
|
463
|
+
json.dumps(payload.get("permissions", {}), ensure_ascii=False, indent=2)
|
|
464
|
+
)
|
|
465
|
+
view.write_line("Choose: [n] deny / [t] grant for turn / [s] grant for session")
|
|
466
|
+
try:
|
|
467
|
+
raw_answer = await view.prompt_async("permissions> ")
|
|
468
|
+
except EOFError:
|
|
469
|
+
return None
|
|
470
|
+
finally:
|
|
471
|
+
view.resume_spinner()
|
|
472
|
+
|
|
473
|
+
answer = raw_answer.strip().lower()
|
|
474
|
+
if answer in {"t", "turn", "y", "yes"}:
|
|
475
|
+
return {
|
|
476
|
+
"permissions": payload.get("permissions", {}),
|
|
477
|
+
"scope": "turn",
|
|
478
|
+
}
|
|
479
|
+
if answer in {"s", "session"}:
|
|
480
|
+
return {
|
|
481
|
+
"permissions": payload.get("permissions", {}),
|
|
482
|
+
"scope": "session",
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
"permissions": {},
|
|
486
|
+
"scope": "turn",
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
async def run_interactive_session(
|
|
491
|
+
runtime: AgentRuntime,
|
|
492
|
+
json_mode: bool,
|
|
493
|
+
) -> int:
|
|
494
|
+
worker = asyncio.create_task(runtime.run_forever())
|
|
495
|
+
view = CliSessionView()
|
|
496
|
+
model_client = runtime._agent_loop._model_client
|
|
497
|
+
runtime.set_event_handler(view.handle_event)
|
|
498
|
+
pending_turn_tasks: set[asyncio.Task[None]] = set()
|
|
499
|
+
runtime_environment = runtime.runtime_environment
|
|
500
|
+
if runtime_environment is None:
|
|
501
|
+
runtime_environment = create_runtime_environment()
|
|
502
|
+
runtime.runtime_environment = runtime_environment
|
|
503
|
+
runtime_environment.request_user_input_manager.set_handler(
|
|
504
|
+
lambda payload: prompt_request_user_input(view, payload)
|
|
505
|
+
)
|
|
506
|
+
runtime_environment.request_permissions_manager.set_handler(
|
|
507
|
+
lambda payload: prompt_request_permissions(view, payload)
|
|
508
|
+
)
|
|
509
|
+
view.write_line("pycodex interactive mode. Type /exit to quit.")
|
|
510
|
+
view.write_line("Extra commands: /history, /title, /model")
|
|
511
|
+
try:
|
|
512
|
+
|
|
513
|
+
def has_pending_turn_tasks() -> bool:
|
|
514
|
+
pending_turn_tasks.difference_update(
|
|
515
|
+
task for task in tuple(pending_turn_tasks) if task.done()
|
|
516
|
+
)
|
|
517
|
+
return bool(pending_turn_tasks)
|
|
518
|
+
|
|
519
|
+
async def wait_for_turn_result(future) -> None:
|
|
520
|
+
try:
|
|
521
|
+
result = await future
|
|
522
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
523
|
+
if str(exc) == "submission interrupted":
|
|
524
|
+
return
|
|
525
|
+
view.show_error(str(exc))
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
if json_mode:
|
|
529
|
+
view.write_line(format_turn_output(result, True))
|
|
530
|
+
|
|
531
|
+
while True:
|
|
532
|
+
try:
|
|
533
|
+
raw_line = await view.poll_prompt("pycodex> ")
|
|
534
|
+
except EOFError:
|
|
535
|
+
break
|
|
536
|
+
if raw_line is None:
|
|
537
|
+
await asyncio.sleep(0.05)
|
|
538
|
+
continue
|
|
539
|
+
|
|
540
|
+
prompt_text = raw_line.strip()
|
|
541
|
+
if not prompt_text:
|
|
542
|
+
continue
|
|
543
|
+
if prompt_text in EXIT_COMMANDS:
|
|
544
|
+
break
|
|
545
|
+
if prompt_text == HISTORY_COMMAND:
|
|
546
|
+
view.show_history()
|
|
547
|
+
continue
|
|
548
|
+
if prompt_text == TITLE_COMMAND:
|
|
549
|
+
view.show_title()
|
|
550
|
+
continue
|
|
551
|
+
if prompt_text.startswith(f"{QUEUE_COMMAND} "):
|
|
552
|
+
queued_text = prompt_text[len(QUEUE_COMMAND) :].strip()
|
|
553
|
+
if not queued_text:
|
|
554
|
+
view.write_line("Usage: /queue <message>")
|
|
555
|
+
continue
|
|
556
|
+
try:
|
|
557
|
+
submission_id, future = await runtime.enqueue_user_turn(
|
|
558
|
+
queued_text, queue="enqueue"
|
|
559
|
+
)
|
|
560
|
+
view.show_steer_queued(submission_id, queued_text)
|
|
561
|
+
turn_task = asyncio.create_task(wait_for_turn_result(future))
|
|
562
|
+
pending_turn_tasks.add(turn_task)
|
|
563
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
564
|
+
view.show_error(str(exc))
|
|
565
|
+
continue
|
|
566
|
+
if prompt_text == MODEL_COMMAND:
|
|
567
|
+
view.write_line(
|
|
568
|
+
f"Current model: {getattr(model_client, 'model', None) or 'unavailable'}"
|
|
569
|
+
)
|
|
570
|
+
models = await model_client.list_models()
|
|
571
|
+
view.write_line(f"Available models: {', '.join(models)}")
|
|
572
|
+
continue
|
|
573
|
+
if prompt_text.startswith(f"{MODEL_COMMAND} "):
|
|
574
|
+
if has_pending_turn_tasks():
|
|
575
|
+
view.write_line(
|
|
576
|
+
"Cannot change model while work is running or queued in steer mode."
|
|
577
|
+
)
|
|
578
|
+
continue
|
|
579
|
+
model_name = prompt_text[len(MODEL_COMMAND) :].strip()
|
|
580
|
+
if not model_name:
|
|
581
|
+
view.write_line("Usage: /model <model>")
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
model_client.model = model_name
|
|
585
|
+
view.write_line(f"Switched model to {model_name}.")
|
|
586
|
+
continue
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
steered = has_pending_turn_tasks()
|
|
590
|
+
submission_id, future = await runtime.enqueue_user_turn(
|
|
591
|
+
prompt_text,
|
|
592
|
+
queue="steer",
|
|
593
|
+
)
|
|
594
|
+
if steered:
|
|
595
|
+
view.schedule_steer_inserted(submission_id, prompt_text)
|
|
596
|
+
turn_task = asyncio.create_task(wait_for_turn_result(future))
|
|
597
|
+
pending_turn_tasks.add(turn_task)
|
|
598
|
+
continue
|
|
599
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
600
|
+
view.show_error(str(exc))
|
|
601
|
+
continue
|
|
602
|
+
finally:
|
|
603
|
+
runtime_environment.request_user_input_manager.set_handler(None)
|
|
604
|
+
runtime_environment.request_permissions_manager.set_handler(None)
|
|
605
|
+
await runtime.shutdown()
|
|
606
|
+
await worker
|
|
607
|
+
if pending_turn_tasks:
|
|
608
|
+
await asyncio.gather(*pending_turn_tasks, return_exceptions=True)
|
|
609
|
+
view.close()
|
|
610
|
+
|
|
611
|
+
return 0
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
async def run_cli(args: argparse.Namespace) -> int:
|
|
615
|
+
runtime = None
|
|
616
|
+
worker = None
|
|
617
|
+
try:
|
|
618
|
+
if args.put is not None and args.call:
|
|
619
|
+
raise ValueError("--put and --call cannot be combined")
|
|
620
|
+
if args.put is not None and args.prompt:
|
|
621
|
+
raise ValueError("--put does not accept prompt text")
|
|
622
|
+
configure_loguru()
|
|
623
|
+
if args.put is not None:
|
|
624
|
+
def emit_put_log(message: str) -> None:
|
|
625
|
+
print(message, flush=True)
|
|
626
|
+
|
|
627
|
+
call_spec = upload_codex_home(args.put, event_handler=emit_put_log)
|
|
628
|
+
emit_put_log(f"[put] testing call: {call_spec}")
|
|
629
|
+
with tempfile.TemporaryDirectory(prefix="pycodex-put-call-test-") as tmpdir:
|
|
630
|
+
config_path = bootstrap_called_home(call_spec, storage_root=tmpdir)
|
|
631
|
+
emit_put_log(f"[put] call test ok: {config_path.name}")
|
|
632
|
+
print("[put] one-click start:", flush=True)
|
|
633
|
+
print(f"pycodex --call {shlex.quote(call_spec)}", flush=True)
|
|
634
|
+
return 0
|
|
635
|
+
if args.call:
|
|
636
|
+
config_path = bootstrap_called_home(args.call)
|
|
637
|
+
args.config = str(config_path)
|
|
638
|
+
os.environ["CODEX_HOME"] = str(config_path.parent)
|
|
639
|
+
client = _build_model_client(
|
|
640
|
+
args.config,
|
|
641
|
+
args.profile,
|
|
642
|
+
args.timeout_seconds,
|
|
643
|
+
vllm_endpoint=args.vllm_endpoint,
|
|
644
|
+
use_chat_completion=args.use_chat_completion,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
runtime = build_runtime(
|
|
648
|
+
args.config,
|
|
649
|
+
args.profile,
|
|
650
|
+
args.system_prompt,
|
|
651
|
+
client,
|
|
652
|
+
session_mode="tui",
|
|
653
|
+
)
|
|
654
|
+
if should_run_interactive(args.prompt, sys.stdin.isatty()):
|
|
655
|
+
return await run_interactive_session(
|
|
656
|
+
runtime,
|
|
657
|
+
args.json,
|
|
658
|
+
)
|
|
659
|
+
else:
|
|
660
|
+
prompt_text = resolve_prompt_text(args.prompt)
|
|
661
|
+
worker = asyncio.create_task(runtime.run_forever())
|
|
662
|
+
result = await runtime.submit_user_turn(prompt_text)
|
|
663
|
+
print(format_turn_output(result, args.json))
|
|
664
|
+
return 0
|
|
665
|
+
except Exception as exc:
|
|
666
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
667
|
+
return 1
|
|
668
|
+
finally:
|
|
669
|
+
if runtime is not None and worker is not None:
|
|
670
|
+
await runtime.shutdown()
|
|
671
|
+
await worker
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
675
|
+
raw_args = list(argv) if argv is not None else None
|
|
676
|
+
if raw_args is None:
|
|
677
|
+
raw_args = sys.argv[1:]
|
|
678
|
+
|
|
679
|
+
if raw_args and raw_args[0] == "doctor":
|
|
680
|
+
from .doctor import build_doctor_parser, run_doctor_cli
|
|
681
|
+
|
|
682
|
+
parser = build_doctor_parser()
|
|
683
|
+
args = parser.parse_args(raw_args[1:])
|
|
684
|
+
try:
|
|
685
|
+
return asyncio.run(run_doctor_cli(args))
|
|
686
|
+
except ValueError as exc:
|
|
687
|
+
parser.error(str(exc))
|
|
688
|
+
except KeyboardInterrupt:
|
|
689
|
+
return 130
|
|
690
|
+
return 0
|
|
691
|
+
|
|
692
|
+
parser = build_parser()
|
|
693
|
+
args = parser.parse_args(raw_args)
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
return asyncio.run(run_cli(args))
|
|
697
|
+
except ValueError as exc:
|
|
698
|
+
parser.error(str(exc))
|
|
699
|
+
except KeyboardInterrupt:
|
|
700
|
+
return 130
|
|
701
|
+
return 0
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
if __name__ == "__main__":
|
|
705
|
+
raise SystemExit(main())
|