python-codex 0.1.12__py3-none-any.whl → 0.1.14__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 +10 -8
- pycodex/agent.py +118 -29
- pycodex/cli.py +97 -387
- pycodex/compat.py +8 -4
- pycodex/feishu_card.py +739 -0
- pycodex/feishu_link.py +462 -0
- pycodex/interactive_session.py +397 -0
- pycodex/model.py +71 -7
- pycodex/prompts/models.json +4 -4
- pycodex/protocol.py +17 -22
- pycodex/runtime.py +22 -14
- pycodex/runtime_services.py +47 -25
- pycodex/tools/agent_tool_schemas.py +1 -1
- pycodex/tools/apply_patch_tool.py +12 -13
- pycodex/tools/base_tool.py +1 -27
- pycodex/tools/close_agent_tool.py +11 -4
- pycodex/tools/exec_command_tool.py +40 -16
- pycodex/tools/exec_tool.py +18 -2
- pycodex/tools/grep_files_tool.py +19 -6
- pycodex/tools/ipython_tool.py +145 -0
- pycodex/tools/list_dir_tool.py +19 -6
- pycodex/tools/read_file_tool.py +39 -9
- pycodex/tools/request_permissions_tool.py +12 -1
- pycodex/tools/request_user_input_tool.py +28 -1
- pycodex/tools/send_input_tool.py +4 -2
- pycodex/tools/shell_command_tool.py +23 -6
- pycodex/tools/shell_tool.py +13 -4
- pycodex/tools/spawn_agent_tool.py +31 -8
- pycodex/tools/unified_exec_manager.py +45 -1
- pycodex/tools/update_plan_tool.py +14 -6
- pycodex/tools/view_image_tool.py +17 -16
- pycodex/tools/wait_agent_tool.py +15 -3
- pycodex/tools/wait_tool.py +18 -4
- pycodex/tools/web_search_tool.py +2 -1
- pycodex/tools/write_stdin_tool.py +42 -10
- pycodex/utils/__init__.py +2 -13
- pycodex/utils/async_bridge.py +54 -0
- pycodex/utils/compactor.py +29 -10
- pycodex/utils/session_persist.py +57 -38
- pycodex/utils/toolcall_visualize.py +713 -0
- pycodex/utils/visualize.py +253 -872
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/METADATA +4 -1
- python_codex-0.1.14.dist-info/RECORD +87 -0
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/entry_points.txt +1 -0
- workspace_server/__init__.py +21 -0
- workspace_server/__main__.py +5 -0
- workspace_server/app.py +983 -0
- workspace_server/workspace.html +790 -0
- pycodex/prompts/exec_tools.json +0 -411
- pycodex/prompts/subagent_tools.json +0 -163
- python_codex-0.1.12.dist-info/RECORD +0 -79
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/WHEEL +0 -0
- {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/licenses/LICENSE +0 -0
pycodex/cli.py
CHANGED
|
@@ -2,47 +2,42 @@
|
|
|
2
2
|
import atexit
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
|
-
import json
|
|
6
5
|
import os
|
|
7
6
|
import shlex
|
|
8
7
|
import sys
|
|
9
8
|
import tempfile
|
|
10
9
|
import traceback
|
|
11
|
-
from dataclasses import
|
|
10
|
+
from dataclasses import replace
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from typing import Sequence
|
|
14
13
|
|
|
15
|
-
from .agent import
|
|
14
|
+
from .agent import Agent
|
|
16
15
|
from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
|
|
17
16
|
from .compat import Literal
|
|
18
17
|
from .context import ContextManager
|
|
19
|
-
from .model import ResponsesModelClient, ResponsesProviderConfig
|
|
18
|
+
from .model import DEFAULT_CODEX_CONFIG_PATH, ResponsesModelClient, ResponsesProviderConfig
|
|
20
19
|
from .portable import bootstrap_called_home, upload_codex_home
|
|
21
|
-
from .
|
|
22
|
-
from .
|
|
23
|
-
from .runtime_services import RuntimeEnvironment, create_runtime_environment
|
|
20
|
+
from .runtime import CliSubmissionQueue
|
|
21
|
+
from .runtime_services import AgentRuntimeEnvironment, create_agent_runtime_environment
|
|
24
22
|
from .utils import CliSessionView, get_debug_dir, load_codex_dotenv, uuid7_string
|
|
25
|
-
from .
|
|
23
|
+
from .interactive_session import (
|
|
24
|
+
EXTRA_COMMANDS_LINE,
|
|
25
|
+
format_turn_output,
|
|
26
|
+
run_interactive_session as _run_interactive_session,
|
|
27
|
+
prompt_request_permissions,
|
|
28
|
+
prompt_request_user_input,
|
|
29
|
+
)
|
|
26
30
|
from .utils.session_persist import (
|
|
27
31
|
SessionRolloutRecorder,
|
|
28
|
-
conversation_history_to_turns,
|
|
29
|
-
list_resumable_sessions,
|
|
30
|
-
load_resumed_session,
|
|
31
32
|
resolve_codex_home,
|
|
32
33
|
)
|
|
33
34
|
import typing
|
|
34
35
|
|
|
35
|
-
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
36
|
-
HISTORY_COMMAND = "/history"
|
|
37
|
-
TITLE_COMMAND = "/title"
|
|
38
|
-
MODEL_COMMAND = "/model"
|
|
39
|
-
QUEUE_COMMAND = "/queue"
|
|
40
|
-
RESUME_COMMAND = "/resume"
|
|
41
|
-
COMPACT_COMMAND = "/compact"
|
|
42
36
|
CliSessionMode = Literal["exec", "tui"]
|
|
43
37
|
LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
|
|
44
38
|
CLI_ORIGINATOR = "codex-tui"
|
|
45
39
|
|
|
40
|
+
|
|
46
41
|
def launch_chat_completion_compat_server(*args, **kwargs):
|
|
47
42
|
from responses_server import (
|
|
48
43
|
launch_chat_completion_compat_server as launch_compat_server,
|
|
@@ -98,7 +93,7 @@ def build_parser() -> 'argparse.ArgumentParser':
|
|
|
98
93
|
)
|
|
99
94
|
parser.add_argument(
|
|
100
95
|
"--config",
|
|
101
|
-
default=str(
|
|
96
|
+
default=str(DEFAULT_CODEX_CONFIG_PATH),
|
|
102
97
|
help="Path to Codex config.toml.",
|
|
103
98
|
)
|
|
104
99
|
parser.add_argument(
|
|
@@ -168,7 +163,7 @@ def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
|
|
|
168
163
|
|
|
169
164
|
|
|
170
165
|
def get_tools(
|
|
171
|
-
runtime_environment: 'typing.Union[
|
|
166
|
+
runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
|
|
172
167
|
exec_mode: 'bool' = False,
|
|
173
168
|
):
|
|
174
169
|
from .tools import (
|
|
@@ -197,7 +192,7 @@ def get_tools(
|
|
|
197
192
|
WriteStdinTool,
|
|
198
193
|
)
|
|
199
194
|
|
|
200
|
-
runtime_environment = runtime_environment or
|
|
195
|
+
runtime_environment = runtime_environment or create_agent_runtime_environment()
|
|
201
196
|
registry = Registry()
|
|
202
197
|
code_mode_manager = CodeModeManager(registry)
|
|
203
198
|
unified_exec_manager = UnifiedExecManager()
|
|
@@ -263,7 +258,7 @@ def get_tools(
|
|
|
263
258
|
return registry
|
|
264
259
|
|
|
265
260
|
|
|
266
|
-
def get_subagent_tools(runtime_environment: 'typing.Union[
|
|
261
|
+
def get_subagent_tools(runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None):
|
|
267
262
|
from .tools import (
|
|
268
263
|
ApplyPatchTool,
|
|
269
264
|
ExecCommandTool,
|
|
@@ -275,7 +270,7 @@ def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, No
|
|
|
275
270
|
WriteStdinTool,
|
|
276
271
|
)
|
|
277
272
|
|
|
278
|
-
runtime_environment = runtime_environment or
|
|
273
|
+
runtime_environment = runtime_environment or create_agent_runtime_environment()
|
|
279
274
|
registry = Registry()
|
|
280
275
|
unified_exec_manager = UnifiedExecManager()
|
|
281
276
|
registry.register(ExecCommandTool(unified_exec_manager))
|
|
@@ -287,21 +282,21 @@ def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, No
|
|
|
287
282
|
return registry
|
|
288
283
|
|
|
289
284
|
|
|
290
|
-
def
|
|
291
|
-
config_path: 'str',
|
|
292
|
-
profile: 'typing.Union[str, None]',
|
|
293
|
-
system_prompt: 'typing.Union[str, None]',
|
|
285
|
+
def build_agent(
|
|
294
286
|
client,
|
|
287
|
+
config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
|
|
288
|
+
profile: 'typing.Union[str, None]' = None,
|
|
289
|
+
system_prompt: 'typing.Union[str, None]' = None,
|
|
295
290
|
session_mode: 'CliSessionMode' = "exec",
|
|
296
291
|
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
297
|
-
) -> '
|
|
298
|
-
|
|
292
|
+
) -> 'Agent':
|
|
293
|
+
config_path = str(config_path)
|
|
299
294
|
context_manager = ContextManager.from_codex_config(
|
|
300
295
|
config_path,
|
|
301
296
|
profile,
|
|
302
297
|
base_instructions_override=system_prompt,
|
|
303
298
|
collaboration_mode=collaboration_mode,
|
|
304
|
-
include_collaboration_instructions=
|
|
299
|
+
include_collaboration_instructions=session_mode == "tui",
|
|
305
300
|
)
|
|
306
301
|
session_id = getattr(client, "_session_id", None) or uuid7_string()
|
|
307
302
|
if hasattr(client, "_session_id"):
|
|
@@ -312,7 +307,7 @@ def build_runtime(
|
|
|
312
307
|
base_instructions_override=system_prompt,
|
|
313
308
|
include_collaboration_instructions=False,
|
|
314
309
|
)
|
|
315
|
-
runtime_environment =
|
|
310
|
+
runtime_environment = create_agent_runtime_environment()
|
|
316
311
|
runtime_environment.request_user_input_manager.set_handler(None)
|
|
317
312
|
runtime_environment.request_permissions_manager.set_handler(None)
|
|
318
313
|
rollout_recorder = SessionRolloutRecorder.create(
|
|
@@ -324,61 +319,52 @@ def build_runtime(
|
|
|
324
319
|
context_manager.resolve_base_instructions(),
|
|
325
320
|
)
|
|
326
321
|
|
|
327
|
-
def
|
|
328
|
-
def
|
|
322
|
+
def make_subagent_queue_builder(base_client):
|
|
323
|
+
def build_subagent_queue(
|
|
329
324
|
model_override: 'typing.Union[str, None]',
|
|
330
325
|
reasoning_effort_override: 'typing.Union[str, None]',
|
|
331
326
|
initial_history=(),
|
|
332
327
|
session_id: 'typing.Union[str, None]' = None,
|
|
333
|
-
) -> '
|
|
328
|
+
) -> 'CliSubmissionQueue':
|
|
334
329
|
nested_client = base_client.with_overrides(
|
|
335
330
|
model_override,
|
|
336
331
|
reasoning_effort_override,
|
|
337
332
|
session_id=session_id,
|
|
338
333
|
openai_subagent="collab_spawn",
|
|
339
334
|
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
335
|
+
subagent_agent_runtime_environment = create_agent_runtime_environment()
|
|
336
|
+
subagent_agent_runtime_environment.request_user_input_manager.set_handler(None)
|
|
337
|
+
subagent_agent_runtime_environment.request_permissions_manager.set_handler(None)
|
|
338
|
+
subagent_agent_runtime_environment.subagent_manager.set_queue_builder(
|
|
339
|
+
make_subagent_queue_builder(nested_client)
|
|
345
340
|
)
|
|
346
|
-
sub_agent =
|
|
341
|
+
sub_agent = Agent(
|
|
347
342
|
nested_client,
|
|
348
|
-
get_subagent_tools(
|
|
343
|
+
get_subagent_tools(subagent_agent_runtime_environment),
|
|
349
344
|
subagent_context_manager,
|
|
350
345
|
initial_history=tuple(initial_history),
|
|
346
|
+
runtime_environment=subagent_agent_runtime_environment,
|
|
351
347
|
)
|
|
352
|
-
return
|
|
353
|
-
sub_agent, runtime_environment=subagent_runtime_environment
|
|
354
|
-
)
|
|
348
|
+
return CliSubmissionQueue(sub_agent)
|
|
355
349
|
|
|
356
|
-
return
|
|
350
|
+
return build_subagent_queue
|
|
357
351
|
|
|
358
|
-
runtime_environment.subagent_manager.
|
|
359
|
-
|
|
352
|
+
runtime_environment.subagent_manager.set_queue_builder(
|
|
353
|
+
make_subagent_queue_builder(client)
|
|
360
354
|
)
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
rollout_recorder=rollout_recorder,
|
|
367
|
-
),
|
|
355
|
+
return Agent(
|
|
356
|
+
client,
|
|
357
|
+
get_tools(runtime_environment, exec_mode=True),
|
|
358
|
+
context_manager,
|
|
359
|
+
rollout_recorder=rollout_recorder,
|
|
368
360
|
runtime_environment=runtime_environment,
|
|
369
361
|
)
|
|
370
362
|
|
|
371
363
|
|
|
372
|
-
def
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def _build_model_client(
|
|
379
|
-
config_path: 'str',
|
|
380
|
-
profile: 'typing.Union[str, None]',
|
|
381
|
-
timeout_seconds: 'float',
|
|
364
|
+
def build_model(
|
|
365
|
+
config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
|
|
366
|
+
profile: 'typing.Union[str, None]' = None,
|
|
367
|
+
timeout_seconds: 'float' = 120.0,
|
|
382
368
|
managed_responses_base_url: 'typing.Union[str, None]' = None,
|
|
383
369
|
vllm_endpoint: 'typing.Union[str, None]' = None,
|
|
384
370
|
use_chat_completion: 'typing.Union[bool, None]' = None,
|
|
@@ -436,316 +422,25 @@ def _build_model_client(
|
|
|
436
422
|
)
|
|
437
423
|
|
|
438
424
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
payload: 'typing.Dict[str, object]',
|
|
442
|
-
) -> 'typing.Union[typing.Dict[str, object], None]':
|
|
443
|
-
view.finish_stream()
|
|
444
|
-
view.pause_spinner()
|
|
445
|
-
view.write_line("[request_user_input] waiting for user response")
|
|
446
|
-
answers: 'typing.Dict[str, typing.Dict[str, typing.List[str]]]' = {}
|
|
447
|
-
try:
|
|
448
|
-
for question in payload.get("questions", []):
|
|
449
|
-
if not isinstance(question, dict):
|
|
450
|
-
continue
|
|
451
|
-
header = str(question.get("header", "")).strip()
|
|
452
|
-
question_text = str(question.get("question", "")).strip()
|
|
453
|
-
question_id = str(question.get("id", "")).strip()
|
|
454
|
-
if header:
|
|
455
|
-
view.write_line(f"[{header}] {question_text}")
|
|
456
|
-
else:
|
|
457
|
-
view.write_line(question_text)
|
|
458
|
-
|
|
459
|
-
options = question.get("options") or []
|
|
460
|
-
if isinstance(options, list):
|
|
461
|
-
for index, option in enumerate(options, start=1):
|
|
462
|
-
if not isinstance(option, dict):
|
|
463
|
-
continue
|
|
464
|
-
label = str(option.get("label", "")).strip()
|
|
465
|
-
description = str(option.get("description", "")).strip()
|
|
466
|
-
view.write_line(f" {index}. {label} - {description}")
|
|
467
|
-
view.write_line(" 0. Other")
|
|
468
|
-
|
|
469
|
-
try:
|
|
470
|
-
raw_answer = await view.prompt_async("answer> ")
|
|
471
|
-
except EOFError:
|
|
472
|
-
return None
|
|
473
|
-
answer_text = raw_answer.strip()
|
|
474
|
-
if not answer_text:
|
|
475
|
-
return None
|
|
476
|
-
|
|
477
|
-
selected_answer = answer_text
|
|
478
|
-
if answer_text.isdigit() and isinstance(options, list):
|
|
479
|
-
choice = int(answer_text)
|
|
480
|
-
if 1 <= choice <= len(options):
|
|
481
|
-
option = options[choice - 1]
|
|
482
|
-
if isinstance(option, dict):
|
|
483
|
-
selected_answer = (
|
|
484
|
-
str(option.get("label", "")).strip() or answer_text
|
|
485
|
-
)
|
|
486
|
-
elif choice == 0:
|
|
487
|
-
try:
|
|
488
|
-
raw_answer = await view.prompt_async("other> ")
|
|
489
|
-
except EOFError:
|
|
490
|
-
return None
|
|
491
|
-
selected_answer = raw_answer.strip()
|
|
492
|
-
if not selected_answer:
|
|
493
|
-
return None
|
|
494
|
-
|
|
495
|
-
answers[question_id] = {"answers": [selected_answer]}
|
|
496
|
-
|
|
497
|
-
return {"answers": answers}
|
|
498
|
-
finally:
|
|
499
|
-
view.resume_spinner()
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
async def prompt_request_permissions(
|
|
503
|
-
view: 'CliSessionView',
|
|
504
|
-
payload: 'typing.Dict[str, object]',
|
|
505
|
-
) -> 'typing.Union[typing.Dict[str, object], None]':
|
|
506
|
-
view.finish_stream()
|
|
507
|
-
view.pause_spinner()
|
|
508
|
-
view.write_line("[request_permissions] user approval required")
|
|
509
|
-
reason = payload.get("reason")
|
|
510
|
-
if reason:
|
|
511
|
-
view.write_line(f"Reason: {reason}")
|
|
512
|
-
view.write_line("Requested permissions:")
|
|
513
|
-
view.write_line(
|
|
514
|
-
json.dumps(payload.get("permissions", {}), ensure_ascii=False, indent=2)
|
|
515
|
-
)
|
|
516
|
-
view.write_line("Choose: [n] deny / [t] grant for turn / [s] grant for session")
|
|
517
|
-
try:
|
|
518
|
-
raw_answer = await view.prompt_async("permissions> ")
|
|
519
|
-
except EOFError:
|
|
520
|
-
return None
|
|
521
|
-
finally:
|
|
522
|
-
view.resume_spinner()
|
|
523
|
-
|
|
524
|
-
answer = raw_answer.strip().lower()
|
|
525
|
-
if answer in {"t", "turn", "y", "yes"}:
|
|
526
|
-
return {
|
|
527
|
-
"permissions": payload.get("permissions", {}),
|
|
528
|
-
"scope": "turn",
|
|
529
|
-
}
|
|
530
|
-
if answer in {"s", "session"}:
|
|
531
|
-
return {
|
|
532
|
-
"permissions": payload.get("permissions", {}),
|
|
533
|
-
"scope": "session",
|
|
534
|
-
}
|
|
535
|
-
return {
|
|
536
|
-
"permissions": {},
|
|
537
|
-
"scope": "turn",
|
|
538
|
-
}
|
|
425
|
+
def build_cli_queue(agent: 'Agent') -> 'CliSubmissionQueue':
|
|
426
|
+
return CliSubmissionQueue(agent)
|
|
539
427
|
|
|
540
428
|
|
|
541
429
|
async def run_interactive_session(
|
|
542
|
-
|
|
430
|
+
queue: 'CliSubmissionQueue',
|
|
543
431
|
json_mode: 'bool',
|
|
544
432
|
config_path: 'typing.Union[str, None]' = None,
|
|
545
433
|
) -> 'int':
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
codex_home = resolve_codex_home(config_path)
|
|
552
|
-
runtime.set_event_handler(view.handle_event)
|
|
553
|
-
pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
|
|
554
|
-
runtime_environment = runtime.runtime_environment
|
|
555
|
-
if runtime_environment is None:
|
|
556
|
-
runtime_environment = create_runtime_environment()
|
|
557
|
-
runtime.runtime_environment = runtime_environment
|
|
558
|
-
runtime_environment.request_user_input_manager.set_handler(
|
|
559
|
-
lambda payload: prompt_request_user_input(view, payload)
|
|
560
|
-
)
|
|
561
|
-
runtime_environment.request_permissions_manager.set_handler(
|
|
562
|
-
lambda payload: prompt_request_permissions(view, payload)
|
|
434
|
+
return await _run_interactive_session(
|
|
435
|
+
queue,
|
|
436
|
+
json_mode,
|
|
437
|
+
config_path,
|
|
438
|
+
view_factory=CliSessionView,
|
|
563
439
|
)
|
|
564
|
-
view.write_line("pycodex interactive mode. Type /exit to quit.")
|
|
565
|
-
view.write_line("Extra commands: /history, /title, /model, /resume, /compact")
|
|
566
|
-
try:
|
|
567
|
-
|
|
568
|
-
def has_pending_turn_tasks() -> 'bool':
|
|
569
|
-
pending_turn_tasks.difference_update(
|
|
570
|
-
task for task in tuple(pending_turn_tasks) if task.done()
|
|
571
|
-
)
|
|
572
|
-
return bool(pending_turn_tasks)
|
|
573
|
-
|
|
574
|
-
async def run_manual_compact() -> 'None':
|
|
575
|
-
agent_loop = runtime._agent_loop
|
|
576
|
-
if not agent_loop.history:
|
|
577
|
-
view.write_line("Nothing to compact.")
|
|
578
|
-
return
|
|
579
|
-
|
|
580
|
-
compact_turn_id = uuid7_string()
|
|
581
|
-
|
|
582
|
-
def handle_compact_stream_event(event) -> 'None':
|
|
583
|
-
if event.kind not in {"token_count", "stream_error"}:
|
|
584
|
-
return
|
|
585
|
-
view.handle_event(
|
|
586
|
-
AgentEvent(
|
|
587
|
-
kind=event.kind,
|
|
588
|
-
turn_id=compact_turn_id,
|
|
589
|
-
payload=dict(event.payload),
|
|
590
|
-
)
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
view.write_line("Compacting conversation history...")
|
|
594
|
-
compact_result = await compact_agent_loop(
|
|
595
|
-
agent_loop,
|
|
596
|
-
handle_compact_stream_event,
|
|
597
|
-
True,
|
|
598
|
-
)
|
|
599
|
-
if compact_result is None:
|
|
600
|
-
view.write_line("Nothing to compact.")
|
|
601
|
-
return
|
|
602
|
-
view.load_session_history(
|
|
603
|
-
getattr(view, "_title", None),
|
|
604
|
-
conversation_history_to_turns(compact_result.history),
|
|
605
|
-
)
|
|
606
|
-
view.write_line(compact_result.display_text())
|
|
607
|
-
|
|
608
|
-
async def wait_for_turn_result(future) -> 'None':
|
|
609
|
-
try:
|
|
610
|
-
result = await future
|
|
611
|
-
except Exception as exc: # pragma: no cover - defensive surface
|
|
612
|
-
if str(exc) == "submission interrupted":
|
|
613
|
-
return
|
|
614
|
-
view.show_error(str(exc))
|
|
615
|
-
return
|
|
616
|
-
|
|
617
|
-
if json_mode:
|
|
618
|
-
view.write_line(format_turn_output(result, True))
|
|
619
|
-
|
|
620
|
-
while True:
|
|
621
|
-
try:
|
|
622
|
-
raw_line = await view.poll_prompt("pycodex> ")
|
|
623
|
-
except EOFError:
|
|
624
|
-
break
|
|
625
|
-
if raw_line is None:
|
|
626
|
-
await asyncio.sleep(0.05)
|
|
627
|
-
continue
|
|
628
|
-
|
|
629
|
-
prompt_text = raw_line.strip()
|
|
630
|
-
if not prompt_text:
|
|
631
|
-
continue
|
|
632
|
-
if prompt_text in EXIT_COMMANDS:
|
|
633
|
-
break
|
|
634
|
-
if prompt_text == HISTORY_COMMAND:
|
|
635
|
-
view.show_history()
|
|
636
|
-
continue
|
|
637
|
-
if prompt_text == TITLE_COMMAND:
|
|
638
|
-
view.show_title()
|
|
639
|
-
continue
|
|
640
|
-
if prompt_text == RESUME_COMMAND:
|
|
641
|
-
sessions = list_resumable_sessions(codex_home)
|
|
642
|
-
if not sessions:
|
|
643
|
-
view.write_line("No resumable sessions found.")
|
|
644
|
-
continue
|
|
645
|
-
view.write_line("Available sessions:")
|
|
646
|
-
for index, session in enumerate(sessions, start=1):
|
|
647
|
-
view.write_line(f"[{index}] {session['preview']}")
|
|
648
|
-
continue
|
|
649
|
-
if prompt_text.startswith(f"{RESUME_COMMAND} "):
|
|
650
|
-
if has_pending_turn_tasks():
|
|
651
|
-
view.write_line(
|
|
652
|
-
"Cannot resume while work is running or queued."
|
|
653
|
-
)
|
|
654
|
-
continue
|
|
655
|
-
resume_target = prompt_text[len(RESUME_COMMAND) :].strip()
|
|
656
|
-
try:
|
|
657
|
-
resumed = load_resumed_session(codex_home, resume_target)
|
|
658
|
-
runtime._agent_loop.replace_history(resumed["history"])
|
|
659
|
-
if hasattr(model_client, "_session_id"):
|
|
660
|
-
model_client._session_id = str(resumed["session_id"])
|
|
661
|
-
runtime._agent_loop.set_rollout_recorder(
|
|
662
|
-
SessionRolloutRecorder.resume(resumed["rollout_path"])
|
|
663
|
-
)
|
|
664
|
-
view.load_session_history(
|
|
665
|
-
str(resumed["title"]),
|
|
666
|
-
tuple(resumed["turns"]),
|
|
667
|
-
)
|
|
668
|
-
view.write_line(f"Resumed session: {resumed['title']}")
|
|
669
|
-
view.show_history()
|
|
670
|
-
except Exception as exc: # pragma: no cover - defensive surface
|
|
671
|
-
view.show_error(str(exc))
|
|
672
|
-
continue
|
|
673
|
-
if prompt_text == COMPACT_COMMAND:
|
|
674
|
-
if has_pending_turn_tasks():
|
|
675
|
-
view.write_line(
|
|
676
|
-
"Cannot compact while work is running or queued."
|
|
677
|
-
)
|
|
678
|
-
continue
|
|
679
|
-
try:
|
|
680
|
-
await run_manual_compact()
|
|
681
|
-
except Exception as exc: # pragma: no cover - defensive surface
|
|
682
|
-
view.show_error(str(exc))
|
|
683
|
-
continue
|
|
684
|
-
if prompt_text.startswith(f"{QUEUE_COMMAND} "):
|
|
685
|
-
queued_text = prompt_text[len(QUEUE_COMMAND) :].strip()
|
|
686
|
-
if not queued_text:
|
|
687
|
-
view.write_line("Usage: /queue <message>")
|
|
688
|
-
continue
|
|
689
|
-
try:
|
|
690
|
-
submission_id, future = await runtime.enqueue_user_turn(
|
|
691
|
-
queued_text, queue="enqueue"
|
|
692
|
-
)
|
|
693
|
-
view.show_steer_queued(submission_id, queued_text)
|
|
694
|
-
turn_task = asyncio.create_task(wait_for_turn_result(future))
|
|
695
|
-
pending_turn_tasks.add(turn_task)
|
|
696
|
-
except Exception as exc: # pragma: no cover - defensive surface
|
|
697
|
-
view.show_error(str(exc))
|
|
698
|
-
continue
|
|
699
|
-
if prompt_text == MODEL_COMMAND:
|
|
700
|
-
view.write_line(
|
|
701
|
-
f"Current model: {getattr(model_client, 'model', None) or 'unavailable'}"
|
|
702
|
-
)
|
|
703
|
-
models = await model_client.list_models()
|
|
704
|
-
view.write_line(f"Available models: {', '.join(models)}")
|
|
705
|
-
continue
|
|
706
|
-
if prompt_text.startswith(f"{MODEL_COMMAND} "):
|
|
707
|
-
if has_pending_turn_tasks():
|
|
708
|
-
view.write_line(
|
|
709
|
-
"Cannot change model while work is running or queued in steer mode."
|
|
710
|
-
)
|
|
711
|
-
continue
|
|
712
|
-
model_name = prompt_text[len(MODEL_COMMAND) :].strip()
|
|
713
|
-
if not model_name:
|
|
714
|
-
view.write_line("Usage: /model <model>")
|
|
715
|
-
continue
|
|
716
|
-
|
|
717
|
-
model_client.model = model_name
|
|
718
|
-
view.write_line(f"Switched model to {model_name}.")
|
|
719
|
-
continue
|
|
720
|
-
|
|
721
|
-
try:
|
|
722
|
-
steered = has_pending_turn_tasks()
|
|
723
|
-
submission_id, future = await runtime.enqueue_user_turn(
|
|
724
|
-
prompt_text,
|
|
725
|
-
queue="steer",
|
|
726
|
-
)
|
|
727
|
-
if steered:
|
|
728
|
-
view.schedule_steer_inserted(submission_id, prompt_text)
|
|
729
|
-
turn_task = asyncio.create_task(wait_for_turn_result(future))
|
|
730
|
-
pending_turn_tasks.add(turn_task)
|
|
731
|
-
continue
|
|
732
|
-
except Exception as exc: # pragma: no cover - defensive surface
|
|
733
|
-
view.show_error(str(exc))
|
|
734
|
-
continue
|
|
735
|
-
finally:
|
|
736
|
-
runtime_environment.request_user_input_manager.set_handler(None)
|
|
737
|
-
runtime_environment.request_permissions_manager.set_handler(None)
|
|
738
|
-
await runtime.shutdown()
|
|
739
|
-
await worker
|
|
740
|
-
if pending_turn_tasks:
|
|
741
|
-
await asyncio.gather(*pending_turn_tasks, return_exceptions=True)
|
|
742
|
-
view.close()
|
|
743
|
-
|
|
744
|
-
return 0
|
|
745
440
|
|
|
746
441
|
|
|
747
442
|
async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
748
|
-
|
|
443
|
+
queued_agent = None
|
|
749
444
|
worker = None
|
|
750
445
|
debug_dir = get_debug_dir()
|
|
751
446
|
phase_handle = None if debug_dir is None else (debug_dir / "phase.log").open("a", encoding="utf-8")
|
|
@@ -755,6 +450,7 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
|
755
450
|
if args.put is not None and args.prompt:
|
|
756
451
|
raise ValueError("--put does not accept prompt text")
|
|
757
452
|
configure_loguru()
|
|
453
|
+
config_path = args.config
|
|
758
454
|
if args.put is not None:
|
|
759
455
|
def emit_put_log(message: 'str') -> 'None':
|
|
760
456
|
print(message, flush=True)
|
|
@@ -775,49 +471,50 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
|
775
471
|
if phase_handle is not None:
|
|
776
472
|
phase_handle.write("bootstrap_called_home:done\n")
|
|
777
473
|
phase_handle.flush()
|
|
778
|
-
args.config = str(config_path)
|
|
779
474
|
os.environ["CODEX_HOME"] = str(config_path.parent)
|
|
780
475
|
if phase_handle is not None:
|
|
781
|
-
phase_handle.write("
|
|
476
|
+
phase_handle.write("build_model:start\n")
|
|
782
477
|
phase_handle.flush()
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
args.profile,
|
|
786
|
-
args.timeout_seconds,
|
|
478
|
+
model = build_model(
|
|
479
|
+
config_path=str(config_path),
|
|
480
|
+
profile=args.profile,
|
|
481
|
+
timeout_seconds=args.timeout_seconds,
|
|
787
482
|
vllm_endpoint=args.vllm_endpoint,
|
|
788
483
|
use_chat_completion=args.use_chat_completion or None,
|
|
789
484
|
use_messages=args.use_messages,
|
|
790
485
|
)
|
|
791
486
|
if phase_handle is not None:
|
|
792
|
-
phase_handle.write("
|
|
793
|
-
phase_handle.
|
|
794
|
-
|
|
795
|
-
if phase_handle is not None:
|
|
796
|
-
phase_handle.write("build_runtime:start\n")
|
|
487
|
+
phase_handle.write("build_model:done\n")
|
|
488
|
+
phase_handle.write("build_agent:start\n")
|
|
797
489
|
phase_handle.flush()
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
args.
|
|
802
|
-
|
|
490
|
+
agent = build_agent(
|
|
491
|
+
model,
|
|
492
|
+
config_path=str(config_path),
|
|
493
|
+
profile=args.profile,
|
|
494
|
+
system_prompt=args.system_prompt,
|
|
803
495
|
session_mode="tui",
|
|
804
496
|
)
|
|
805
497
|
if phase_handle is not None:
|
|
806
|
-
phase_handle.write("
|
|
498
|
+
phase_handle.write("build_agent:done\n")
|
|
499
|
+
phase_handle.write("build_cli_queue:start\n")
|
|
500
|
+
phase_handle.flush()
|
|
501
|
+
queued_agent = build_cli_queue(agent)
|
|
502
|
+
if phase_handle is not None:
|
|
503
|
+
phase_handle.write("build_cli_queue:done\n")
|
|
807
504
|
phase_handle.flush()
|
|
808
505
|
if should_run_interactive(args.prompt, sys.stdin.isatty()):
|
|
809
506
|
return await run_interactive_session(
|
|
810
|
-
|
|
507
|
+
queued_agent,
|
|
811
508
|
args.json,
|
|
812
|
-
|
|
509
|
+
str(config_path),
|
|
813
510
|
)
|
|
814
511
|
else:
|
|
815
512
|
prompt_text = resolve_prompt_text(args.prompt)
|
|
816
|
-
worker = asyncio.create_task(
|
|
513
|
+
worker = asyncio.create_task(queued_agent.run_forever())
|
|
817
514
|
if phase_handle is not None:
|
|
818
515
|
phase_handle.write("submit_user_turn:start\n")
|
|
819
516
|
phase_handle.flush()
|
|
820
|
-
result = await
|
|
517
|
+
result = await queued_agent.submit_user_turn(prompt_text)
|
|
821
518
|
if phase_handle is not None:
|
|
822
519
|
phase_handle.write("submit_user_turn:done\n")
|
|
823
520
|
phase_handle.flush()
|
|
@@ -836,10 +533,23 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
|
836
533
|
finally:
|
|
837
534
|
if phase_handle is not None:
|
|
838
535
|
phase_handle.close()
|
|
839
|
-
if
|
|
840
|
-
await
|
|
536
|
+
if queued_agent is not None and worker is not None:
|
|
537
|
+
await queued_agent.shutdown()
|
|
841
538
|
await worker
|
|
842
539
|
|
|
540
|
+
def ipython_agent(config_path: 'str' = DEFAULT_CODEX_CONFIG_PATH):
|
|
541
|
+
from loguru import logger
|
|
542
|
+
logger.remove()
|
|
543
|
+
logger.add(sys.stderr, level="INFO")
|
|
544
|
+
|
|
545
|
+
model = build_model(config_path)
|
|
546
|
+
agent = build_agent(client=model, config_path=config_path)
|
|
547
|
+
|
|
548
|
+
from pycodex.tools.ipython_tool import attach_ipython_tool
|
|
549
|
+
|
|
550
|
+
attach_ipython_tool(agent)
|
|
551
|
+
|
|
552
|
+
return agent
|
|
843
553
|
|
|
844
554
|
def main(argv: 'typing.Union[Sequence[str], None]' = None) -> 'int':
|
|
845
555
|
raw_args = list(argv) if argv is not None else None
|
pycodex/compat.py
CHANGED
|
@@ -26,15 +26,19 @@ except ImportError: # pragma: no cover - Python 3.6 path
|
|
|
26
26
|
TypeAlias = object
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _get_running_loop_compat():
|
|
30
|
+
loop = asyncio.get_event_loop()
|
|
31
|
+
if not loop.is_running():
|
|
32
|
+
raise RuntimeError("no running event loop")
|
|
33
|
+
return loop
|
|
34
|
+
|
|
35
|
+
|
|
29
36
|
def patch_asyncio():
|
|
30
37
|
if not hasattr(asyncio, "create_task"):
|
|
31
38
|
asyncio.create_task = asyncio.ensure_future
|
|
32
39
|
|
|
33
40
|
if not hasattr(asyncio, "get_running_loop"):
|
|
34
|
-
|
|
35
|
-
return asyncio.get_event_loop()
|
|
36
|
-
|
|
37
|
-
asyncio.get_running_loop = get_running_loop
|
|
41
|
+
asyncio.get_running_loop = _get_running_loop_compat
|
|
38
42
|
|
|
39
43
|
if not hasattr(asyncio, "to_thread"):
|
|
40
44
|
async def to_thread(func, *args, **kwargs):
|