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.
Files changed (53) hide show
  1. pycodex/__init__.py +10 -8
  2. pycodex/agent.py +118 -29
  3. pycodex/cli.py +97 -387
  4. pycodex/compat.py +8 -4
  5. pycodex/feishu_card.py +739 -0
  6. pycodex/feishu_link.py +462 -0
  7. pycodex/interactive_session.py +397 -0
  8. pycodex/model.py +71 -7
  9. pycodex/prompts/models.json +4 -4
  10. pycodex/protocol.py +17 -22
  11. pycodex/runtime.py +22 -14
  12. pycodex/runtime_services.py +47 -25
  13. pycodex/tools/agent_tool_schemas.py +1 -1
  14. pycodex/tools/apply_patch_tool.py +12 -13
  15. pycodex/tools/base_tool.py +1 -27
  16. pycodex/tools/close_agent_tool.py +11 -4
  17. pycodex/tools/exec_command_tool.py +40 -16
  18. pycodex/tools/exec_tool.py +18 -2
  19. pycodex/tools/grep_files_tool.py +19 -6
  20. pycodex/tools/ipython_tool.py +145 -0
  21. pycodex/tools/list_dir_tool.py +19 -6
  22. pycodex/tools/read_file_tool.py +39 -9
  23. pycodex/tools/request_permissions_tool.py +12 -1
  24. pycodex/tools/request_user_input_tool.py +28 -1
  25. pycodex/tools/send_input_tool.py +4 -2
  26. pycodex/tools/shell_command_tool.py +23 -6
  27. pycodex/tools/shell_tool.py +13 -4
  28. pycodex/tools/spawn_agent_tool.py +31 -8
  29. pycodex/tools/unified_exec_manager.py +45 -1
  30. pycodex/tools/update_plan_tool.py +14 -6
  31. pycodex/tools/view_image_tool.py +17 -16
  32. pycodex/tools/wait_agent_tool.py +15 -3
  33. pycodex/tools/wait_tool.py +18 -4
  34. pycodex/tools/web_search_tool.py +2 -1
  35. pycodex/tools/write_stdin_tool.py +42 -10
  36. pycodex/utils/__init__.py +2 -13
  37. pycodex/utils/async_bridge.py +54 -0
  38. pycodex/utils/compactor.py +29 -10
  39. pycodex/utils/session_persist.py +57 -38
  40. pycodex/utils/toolcall_visualize.py +713 -0
  41. pycodex/utils/visualize.py +253 -872
  42. {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/METADATA +4 -1
  43. python_codex-0.1.14.dist-info/RECORD +87 -0
  44. {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/entry_points.txt +1 -0
  45. workspace_server/__init__.py +21 -0
  46. workspace_server/__main__.py +5 -0
  47. workspace_server/app.py +983 -0
  48. workspace_server/workspace.html +790 -0
  49. pycodex/prompts/exec_tools.json +0 -411
  50. pycodex/prompts/subagent_tools.json +0 -163
  51. python_codex-0.1.12.dist-info/RECORD +0 -79
  52. {python_codex-0.1.12.dist-info → python_codex-0.1.14.dist-info}/WHEEL +0 -0
  53. {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 asdict, replace
10
+ from dataclasses import replace
12
11
  from pathlib import Path
13
12
  from typing import Sequence
14
13
 
15
- from .agent import AgentLoop
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 .protocol import AgentEvent
22
- from .runtime import AgentRuntime
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 .utils.compactor import compact_agent_loop
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(Path.home() / ".codex" / "config.toml"),
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[RuntimeEnvironment, None]' = None,
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 create_runtime_environment()
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[RuntimeEnvironment, None]' = None):
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 create_runtime_environment()
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 build_runtime(
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
- ) -> 'AgentRuntime':
298
- use_tui_context = session_mode == "tui"
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=use_tui_context,
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 = create_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 make_subagent_runtime_builder(base_client):
328
- def build_subagent_runtime(
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
- ) -> 'AgentRuntime':
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
- subagent_runtime_environment = create_runtime_environment()
341
- subagent_runtime_environment.request_user_input_manager.set_handler(None)
342
- subagent_runtime_environment.request_permissions_manager.set_handler(None)
343
- subagent_runtime_environment.subagent_manager.set_runtime_builder(
344
- make_subagent_runtime_builder(nested_client)
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 = AgentLoop(
341
+ sub_agent = Agent(
347
342
  nested_client,
348
- get_subagent_tools(subagent_runtime_environment),
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 AgentRuntime(
353
- sub_agent, runtime_environment=subagent_runtime_environment
354
- )
348
+ return CliSubmissionQueue(sub_agent)
355
349
 
356
- return build_subagent_runtime
350
+ return build_subagent_queue
357
351
 
358
- runtime_environment.subagent_manager.set_runtime_builder(
359
- make_subagent_runtime_builder(client)
352
+ runtime_environment.subagent_manager.set_queue_builder(
353
+ make_subagent_queue_builder(client)
360
354
  )
361
- return AgentRuntime(
362
- AgentLoop(
363
- client,
364
- get_tools(runtime_environment, exec_mode=True),
365
- context_manager,
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 format_turn_output(result, json_mode: 'bool') -> 'str':
373
- if json_mode:
374
- return json.dumps(asdict(result), ensure_ascii=False, indent=2)
375
- return result.output_text or ""
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
- async def prompt_request_user_input(
440
- view: 'CliSessionView',
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
- runtime: 'AgentRuntime',
430
+ queue: 'CliSubmissionQueue',
543
431
  json_mode: 'bool',
544
432
  config_path: 'typing.Union[str, None]' = None,
545
433
  ) -> 'int':
546
- worker = asyncio.create_task(runtime.run_forever())
547
- context_window_tokens = runtime._agent_loop._context_manager.resolve_model_context_window()
548
- view = CliSessionView()
549
- view.set_context_window_tokens(context_window_tokens)
550
- model_client = runtime._agent_loop._model_client
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
- runtime = None
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("build_model_client:start\n")
476
+ phase_handle.write("build_model:start\n")
782
477
  phase_handle.flush()
783
- client = _build_model_client(
784
- args.config,
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("build_model_client:done\n")
793
- phase_handle.flush()
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
- runtime = build_runtime(
799
- args.config,
800
- args.profile,
801
- args.system_prompt,
802
- client,
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("build_runtime:done\n")
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
- runtime,
507
+ queued_agent,
811
508
  args.json,
812
- args.config,
509
+ str(config_path),
813
510
  )
814
511
  else:
815
512
  prompt_text = resolve_prompt_text(args.prompt)
816
- worker = asyncio.create_task(runtime.run_forever())
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 runtime.submit_user_turn(prompt_text)
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 runtime is not None and worker is not None:
840
- await runtime.shutdown()
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
- def get_running_loop():
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):