python-codex 0.1.11__py3-none-any.whl → 0.1.13__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/cli.py CHANGED
@@ -12,17 +12,17 @@ from dataclasses import asdict, replace
12
12
  from pathlib import Path
13
13
  from typing import Sequence
14
14
 
15
- from .agent import AgentLoop
15
+ from .agent import Agent
16
16
  from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
17
17
  from .compat import Literal
18
18
  from .context import ContextManager
19
- from .model import ResponsesModelClient, ResponsesProviderConfig
19
+ from .model import DEFAULT_CODEX_CONFIG_PATH, ResponsesModelClient, ResponsesProviderConfig
20
20
  from .portable import bootstrap_called_home, upload_codex_home
21
21
  from .protocol import AgentEvent
22
- from .runtime import AgentRuntime
23
- from .runtime_services import RuntimeEnvironment, create_runtime_environment
22
+ from .runtime import CliSubmissionQueue
23
+ from .runtime_services import AgentRuntimeEnvironment, create_agent_runtime_environment
24
24
  from .utils import CliSessionView, get_debug_dir, load_codex_dotenv, uuid7_string
25
- from .utils.compactor import compact_agent_loop
25
+ from .utils.compactor import compact_agent
26
26
  from .utils.session_persist import (
27
27
  SessionRolloutRecorder,
28
28
  conversation_history_to_turns,
@@ -39,10 +39,16 @@ MODEL_COMMAND = "/model"
39
39
  QUEUE_COMMAND = "/queue"
40
40
  RESUME_COMMAND = "/resume"
41
41
  COMPACT_COMMAND = "/compact"
42
+ LINK_COMMAND = "/link"
43
+ UNLINK_COMMAND = "/unlink"
44
+ EXTRA_COMMANDS_LINE = (
45
+ "Extra commands: /history, /title, /model, /resume, /compact, /link, /unlink"
46
+ )
42
47
  CliSessionMode = Literal["exec", "tui"]
43
48
  LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
44
49
  CLI_ORIGINATOR = "codex-tui"
45
50
 
51
+
46
52
  def launch_chat_completion_compat_server(*args, **kwargs):
47
53
  from responses_server import (
48
54
  launch_chat_completion_compat_server as launch_compat_server,
@@ -98,7 +104,7 @@ def build_parser() -> 'argparse.ArgumentParser':
98
104
  )
99
105
  parser.add_argument(
100
106
  "--config",
101
- default=str(Path.home() / ".codex" / "config.toml"),
107
+ default=str(DEFAULT_CODEX_CONFIG_PATH),
102
108
  help="Path to Codex config.toml.",
103
109
  )
104
110
  parser.add_argument(
@@ -168,7 +174,7 @@ def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
168
174
 
169
175
 
170
176
  def get_tools(
171
- runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None,
177
+ runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
172
178
  exec_mode: 'bool' = False,
173
179
  ):
174
180
  from .tools import (
@@ -197,7 +203,7 @@ def get_tools(
197
203
  WriteStdinTool,
198
204
  )
199
205
 
200
- runtime_environment = runtime_environment or create_runtime_environment()
206
+ runtime_environment = runtime_environment or create_agent_runtime_environment()
201
207
  registry = Registry()
202
208
  code_mode_manager = CodeModeManager(registry)
203
209
  unified_exec_manager = UnifiedExecManager()
@@ -263,7 +269,7 @@ def get_tools(
263
269
  return registry
264
270
 
265
271
 
266
- def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None):
272
+ def get_subagent_tools(runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None):
267
273
  from .tools import (
268
274
  ApplyPatchTool,
269
275
  ExecCommandTool,
@@ -275,7 +281,7 @@ def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, No
275
281
  WriteStdinTool,
276
282
  )
277
283
 
278
- runtime_environment = runtime_environment or create_runtime_environment()
284
+ runtime_environment = runtime_environment or create_agent_runtime_environment()
279
285
  registry = Registry()
280
286
  unified_exec_manager = UnifiedExecManager()
281
287
  registry.register(ExecCommandTool(unified_exec_manager))
@@ -287,21 +293,21 @@ def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, No
287
293
  return registry
288
294
 
289
295
 
290
- def build_runtime(
291
- config_path: 'str',
292
- profile: 'typing.Union[str, None]',
293
- system_prompt: 'typing.Union[str, None]',
296
+ def build_agent(
294
297
  client,
298
+ config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
299
+ profile: 'typing.Union[str, None]' = None,
300
+ system_prompt: 'typing.Union[str, None]' = None,
295
301
  session_mode: 'CliSessionMode' = "exec",
296
302
  collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
297
- ) -> 'AgentRuntime':
298
- use_tui_context = session_mode == "tui"
303
+ ) -> 'Agent':
304
+ config_path = str(config_path)
299
305
  context_manager = ContextManager.from_codex_config(
300
306
  config_path,
301
307
  profile,
302
308
  base_instructions_override=system_prompt,
303
309
  collaboration_mode=collaboration_mode,
304
- include_collaboration_instructions=use_tui_context,
310
+ include_collaboration_instructions=session_mode == "tui",
305
311
  )
306
312
  session_id = getattr(client, "_session_id", None) or uuid7_string()
307
313
  if hasattr(client, "_session_id"):
@@ -312,7 +318,7 @@ def build_runtime(
312
318
  base_instructions_override=system_prompt,
313
319
  include_collaboration_instructions=False,
314
320
  )
315
- runtime_environment = create_runtime_environment()
321
+ runtime_environment = create_agent_runtime_environment()
316
322
  runtime_environment.request_user_input_manager.set_handler(None)
317
323
  runtime_environment.request_permissions_manager.set_handler(None)
318
324
  rollout_recorder = SessionRolloutRecorder.create(
@@ -324,47 +330,44 @@ def build_runtime(
324
330
  context_manager.resolve_base_instructions(),
325
331
  )
326
332
 
327
- def make_subagent_runtime_builder(base_client):
328
- def build_subagent_runtime(
333
+ def make_subagent_queue_builder(base_client):
334
+ def build_subagent_queue(
329
335
  model_override: 'typing.Union[str, None]',
330
336
  reasoning_effort_override: 'typing.Union[str, None]',
331
337
  initial_history=(),
332
338
  session_id: 'typing.Union[str, None]' = None,
333
- ) -> 'AgentRuntime':
339
+ ) -> 'CliSubmissionQueue':
334
340
  nested_client = base_client.with_overrides(
335
341
  model_override,
336
342
  reasoning_effort_override,
337
343
  session_id=session_id,
338
344
  openai_subagent="collab_spawn",
339
345
  )
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)
346
+ subagent_agent_runtime_environment = create_agent_runtime_environment()
347
+ subagent_agent_runtime_environment.request_user_input_manager.set_handler(None)
348
+ subagent_agent_runtime_environment.request_permissions_manager.set_handler(None)
349
+ subagent_agent_runtime_environment.subagent_manager.set_queue_builder(
350
+ make_subagent_queue_builder(nested_client)
345
351
  )
346
- sub_agent = AgentLoop(
352
+ sub_agent = Agent(
347
353
  nested_client,
348
- get_subagent_tools(subagent_runtime_environment),
354
+ get_subagent_tools(subagent_agent_runtime_environment),
349
355
  subagent_context_manager,
350
356
  initial_history=tuple(initial_history),
357
+ runtime_environment=subagent_agent_runtime_environment,
351
358
  )
352
- return AgentRuntime(
353
- sub_agent, runtime_environment=subagent_runtime_environment
354
- )
359
+ return CliSubmissionQueue(sub_agent)
355
360
 
356
- return build_subagent_runtime
361
+ return build_subagent_queue
357
362
 
358
- runtime_environment.subagent_manager.set_runtime_builder(
359
- make_subagent_runtime_builder(client)
363
+ runtime_environment.subagent_manager.set_queue_builder(
364
+ make_subagent_queue_builder(client)
360
365
  )
361
- return AgentRuntime(
362
- AgentLoop(
363
- client,
364
- get_tools(runtime_environment, exec_mode=True),
365
- context_manager,
366
- rollout_recorder=rollout_recorder,
367
- ),
366
+ return Agent(
367
+ client,
368
+ get_tools(runtime_environment, exec_mode=True),
369
+ context_manager,
370
+ rollout_recorder=rollout_recorder,
368
371
  runtime_environment=runtime_environment,
369
372
  )
370
373
 
@@ -375,10 +378,10 @@ def format_turn_output(result, json_mode: 'bool') -> 'str':
375
378
  return result.output_text or ""
376
379
 
377
380
 
378
- def _build_model_client(
379
- config_path: 'str',
380
- profile: 'typing.Union[str, None]',
381
- timeout_seconds: 'float',
381
+ def build_model(
382
+ config_path: 'typing.Union[str, Path]' = DEFAULT_CODEX_CONFIG_PATH,
383
+ profile: 'typing.Union[str, None]' = None,
384
+ timeout_seconds: 'float' = 120.0,
382
385
  managed_responses_base_url: 'typing.Union[str, None]' = None,
383
386
  vllm_endpoint: 'typing.Union[str, None]' = None,
384
387
  use_chat_completion: 'typing.Union[bool, None]' = None,
@@ -436,67 +439,67 @@ def _build_model_client(
436
439
  )
437
440
 
438
441
 
442
+ def build_cli_queue(agent: 'Agent') -> 'CliSubmissionQueue':
443
+ return CliSubmissionQueue(agent)
444
+
445
+
439
446
  async def prompt_request_user_input(
440
447
  view: 'CliSessionView',
441
448
  payload: 'typing.Dict[str, object]',
442
449
  ) -> 'typing.Union[typing.Dict[str, object], None]':
443
450
  view.finish_stream()
444
- view.pause_spinner()
445
451
  view.write_line("[request_user_input] waiting for user response")
446
452
  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")
453
+ for question in payload.get("questions", []):
454
+ if not isinstance(question, dict):
455
+ continue
456
+ header = str(question.get("header", "")).strip()
457
+ question_text = str(question.get("question", "")).strip()
458
+ question_id = str(question.get("id", "")).strip()
459
+ if header:
460
+ view.write_line(f"[{header}] {question_text}")
461
+ else:
462
+ view.write_line(question_text)
468
463
 
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()
464
+ options = question.get("options") or []
465
+ if isinstance(options, list):
466
+ for index, option in enumerate(options, start=1):
467
+ if not isinstance(option, dict):
468
+ continue
469
+ label = str(option.get("label", "")).strip()
470
+ description = str(option.get("description", "")).strip()
471
+ view.write_line(f" {index}. {label} - {description}")
472
+ view.write_line(" 0. Other")
473
+
474
+ try:
475
+ raw_answer = await view.get_prompt("answer> ")
476
+ except EOFError:
477
+ return None
478
+ answer_text = raw_answer.strip()
479
+ if not answer_text:
480
+ return None
481
+
482
+ selected_answer = answer_text
483
+ if answer_text.isdigit() and isinstance(options, list):
484
+ choice = int(answer_text)
485
+ if 1 <= choice <= len(options):
486
+ option = options[choice - 1]
487
+ if isinstance(option, dict):
488
+ selected_answer = (
489
+ str(option.get("label", "")).strip() or answer_text
490
+ )
491
+ elif choice == 0:
492
+ try:
493
+ raw_answer = await view.get_prompt("other> ")
494
+ except EOFError:
495
+ return None
496
+ selected_answer = raw_answer.strip()
497
+ if not selected_answer:
498
+ return None
499
+
500
+ answers[question_id] = {"answers": [selected_answer]}
501
+
502
+ return {"answers": answers}
500
503
 
501
504
 
502
505
  async def prompt_request_permissions(
@@ -504,7 +507,6 @@ async def prompt_request_permissions(
504
507
  payload: 'typing.Dict[str, object]',
505
508
  ) -> 'typing.Union[typing.Dict[str, object], None]':
506
509
  view.finish_stream()
507
- view.pause_spinner()
508
510
  view.write_line("[request_permissions] user approval required")
509
511
  reason = payload.get("reason")
510
512
  if reason:
@@ -515,11 +517,9 @@ async def prompt_request_permissions(
515
517
  )
516
518
  view.write_line("Choose: [n] deny / [t] grant for turn / [s] grant for session")
517
519
  try:
518
- raw_answer = await view.prompt_async("permissions> ")
520
+ raw_answer = await view.get_prompt("permissions> ")
519
521
  except EOFError:
520
522
  return None
521
- finally:
522
- view.resume_spinner()
523
523
 
524
524
  answer = raw_answer.strip().lower()
525
525
  if answer in {"t", "turn", "y", "yes"}:
@@ -539,22 +539,22 @@ async def prompt_request_permissions(
539
539
 
540
540
 
541
541
  async def run_interactive_session(
542
- runtime: 'AgentRuntime',
542
+ queue: 'CliSubmissionQueue',
543
543
  json_mode: 'bool',
544
544
  config_path: 'typing.Union[str, None]' = None,
545
545
  ) -> 'int':
546
- worker = asyncio.create_task(runtime.run_forever())
547
- context_window_tokens = runtime._agent_loop._context_manager.resolve_model_context_window()
546
+ worker = asyncio.create_task(queue.run_forever())
547
+ context_window_tokens = queue._agent._context_manager.resolve_model_context_window()
548
548
  view = CliSessionView()
549
549
  view.set_context_window_tokens(context_window_tokens)
550
- model_client = runtime._agent_loop._model_client
550
+ model_client = queue._agent._model_client
551
551
  codex_home = resolve_codex_home(config_path)
552
- runtime.set_event_handler(view.handle_event)
552
+ queue.set_event_handler(view.handle_event)
553
553
  pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
554
- runtime_environment = runtime.runtime_environment
554
+ runtime_environment = queue._agent.runtime_environment
555
555
  if runtime_environment is None:
556
- runtime_environment = create_runtime_environment()
557
- runtime.runtime_environment = runtime_environment
556
+ runtime_environment = create_agent_runtime_environment()
557
+ queue._agent.runtime_environment = runtime_environment
558
558
  runtime_environment.request_user_input_manager.set_handler(
559
559
  lambda payload: prompt_request_user_input(view, payload)
560
560
  )
@@ -562,7 +562,8 @@ async def run_interactive_session(
562
562
  lambda payload: prompt_request_permissions(view, payload)
563
563
  )
564
564
  view.write_line("pycodex interactive mode. Type /exit to quit.")
565
- view.write_line("Extra commands: /history, /title, /model, /resume, /compact")
565
+ view.write_line(EXTRA_COMMANDS_LINE)
566
+ feishu_link = None
566
567
  try:
567
568
 
568
569
  def has_pending_turn_tasks() -> 'bool':
@@ -572,8 +573,8 @@ async def run_interactive_session(
572
573
  return bool(pending_turn_tasks)
573
574
 
574
575
  async def run_manual_compact() -> 'None':
575
- agent_loop = runtime._agent_loop
576
- if not agent_loop.history:
576
+ current_agent = queue._agent
577
+ if not current_agent.history:
577
578
  view.write_line("Nothing to compact.")
578
579
  return
579
580
 
@@ -591,9 +592,10 @@ async def run_interactive_session(
591
592
  )
592
593
 
593
594
  view.write_line("Compacting conversation history...")
594
- compact_result = await compact_agent_loop(
595
- agent_loop,
595
+ compact_result = await compact_agent(
596
+ current_agent,
596
597
  handle_compact_stream_event,
598
+ True,
597
599
  )
598
600
  if compact_result is None:
599
601
  view.write_line("Nothing to compact.")
@@ -618,7 +620,7 @@ async def run_interactive_session(
618
620
 
619
621
  while True:
620
622
  try:
621
- raw_line = await view.poll_prompt("pycodex> ")
623
+ raw_line = await view.poll_prompt()
622
624
  except EOFError:
623
625
  break
624
626
  if raw_line is None:
@@ -654,10 +656,10 @@ async def run_interactive_session(
654
656
  resume_target = prompt_text[len(RESUME_COMMAND) :].strip()
655
657
  try:
656
658
  resumed = load_resumed_session(codex_home, resume_target)
657
- runtime._agent_loop.replace_history(resumed["history"])
659
+ queue._agent.replace_history(resumed["history"])
658
660
  if hasattr(model_client, "_session_id"):
659
661
  model_client._session_id = str(resumed["session_id"])
660
- runtime._agent_loop.set_rollout_recorder(
662
+ queue._agent.set_rollout_recorder(
661
663
  SessionRolloutRecorder.resume(resumed["rollout_path"])
662
664
  )
663
665
  view.load_session_history(
@@ -680,13 +682,47 @@ async def run_interactive_session(
680
682
  except Exception as exc: # pragma: no cover - defensive surface
681
683
  view.show_error(str(exc))
682
684
  continue
685
+ if prompt_text.startswith(f"{LINK_COMMAND} "):
686
+ link_target = prompt_text[len(LINK_COMMAND) :].strip()
687
+ if not link_target:
688
+ view.write_line("Usage: /link <feishu-email|open_id|chat_id>")
689
+ continue
690
+ if feishu_link:
691
+ view.write_line("A Feishu card is already linked. Use /unlink first.")
692
+ continue
693
+ try:
694
+ from .feishu_link import PycodexRuntimeLink
695
+
696
+ view.write_line(f"Linking Feishu card to current session: {link_target}")
697
+ link = await PycodexRuntimeLink(
698
+ queue,
699
+ link_target,
700
+ ).start_async()
701
+ feishu_link = link
702
+ view.write_line(
703
+ "Linked Feishu card: session_key={0} message_id={1}".format(
704
+ link.session_key,
705
+ link.message_id or "-",
706
+ )
707
+ )
708
+ except Exception as exc: # pragma: no cover - defensive surface
709
+ view.show_error(str(exc))
710
+ continue
711
+ if prompt_text == UNLINK_COMMAND:
712
+ if not feishu_link:
713
+ view.write_line("No Feishu card is linked.")
714
+ continue
715
+ feishu_link.detach()
716
+ feishu_link = None
717
+ view.write_line("Unlinked Feishu card.")
718
+ continue
683
719
  if prompt_text.startswith(f"{QUEUE_COMMAND} "):
684
720
  queued_text = prompt_text[len(QUEUE_COMMAND) :].strip()
685
721
  if not queued_text:
686
722
  view.write_line("Usage: /queue <message>")
687
723
  continue
688
724
  try:
689
- submission_id, future = await runtime.enqueue_user_turn(
725
+ submission_id, future = await queue.enqueue_user_turn(
690
726
  queued_text, queue="enqueue"
691
727
  )
692
728
  view.show_steer_queued(submission_id, queued_text)
@@ -719,7 +755,7 @@ async def run_interactive_session(
719
755
 
720
756
  try:
721
757
  steered = has_pending_turn_tasks()
722
- submission_id, future = await runtime.enqueue_user_turn(
758
+ submission_id, future = await queue.enqueue_user_turn(
723
759
  prompt_text,
724
760
  queue="steer",
725
761
  )
@@ -732,9 +768,12 @@ async def run_interactive_session(
732
768
  view.show_error(str(exc))
733
769
  continue
734
770
  finally:
771
+ if feishu_link:
772
+ feishu_link.detach()
773
+ feishu_link.stop()
735
774
  runtime_environment.request_user_input_manager.set_handler(None)
736
775
  runtime_environment.request_permissions_manager.set_handler(None)
737
- await runtime.shutdown()
776
+ await queue.shutdown()
738
777
  await worker
739
778
  if pending_turn_tasks:
740
779
  await asyncio.gather(*pending_turn_tasks, return_exceptions=True)
@@ -744,7 +783,7 @@ async def run_interactive_session(
744
783
 
745
784
 
746
785
  async def run_cli(args: 'argparse.Namespace') -> 'int':
747
- runtime = None
786
+ queued_agent = None
748
787
  worker = None
749
788
  debug_dir = get_debug_dir()
750
789
  phase_handle = None if debug_dir is None else (debug_dir / "phase.log").open("a", encoding="utf-8")
@@ -754,6 +793,7 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
754
793
  if args.put is not None and args.prompt:
755
794
  raise ValueError("--put does not accept prompt text")
756
795
  configure_loguru()
796
+ config_path = args.config
757
797
  if args.put is not None:
758
798
  def emit_put_log(message: 'str') -> 'None':
759
799
  print(message, flush=True)
@@ -774,49 +814,50 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
774
814
  if phase_handle is not None:
775
815
  phase_handle.write("bootstrap_called_home:done\n")
776
816
  phase_handle.flush()
777
- args.config = str(config_path)
778
817
  os.environ["CODEX_HOME"] = str(config_path.parent)
779
818
  if phase_handle is not None:
780
- phase_handle.write("build_model_client:start\n")
819
+ phase_handle.write("build_model:start\n")
781
820
  phase_handle.flush()
782
- client = _build_model_client(
783
- args.config,
784
- args.profile,
785
- args.timeout_seconds,
821
+ model = build_model(
822
+ config_path=str(config_path),
823
+ profile=args.profile,
824
+ timeout_seconds=args.timeout_seconds,
786
825
  vllm_endpoint=args.vllm_endpoint,
787
826
  use_chat_completion=args.use_chat_completion or None,
788
827
  use_messages=args.use_messages,
789
828
  )
790
829
  if phase_handle is not None:
791
- phase_handle.write("build_model_client:done\n")
830
+ phase_handle.write("build_model:done\n")
831
+ phase_handle.write("build_agent:start\n")
792
832
  phase_handle.flush()
793
-
794
- if phase_handle is not None:
795
- phase_handle.write("build_runtime:start\n")
796
- phase_handle.flush()
797
- runtime = build_runtime(
798
- args.config,
799
- args.profile,
800
- args.system_prompt,
801
- client,
833
+ agent = build_agent(
834
+ model,
835
+ config_path=str(config_path),
836
+ profile=args.profile,
837
+ system_prompt=args.system_prompt,
802
838
  session_mode="tui",
803
839
  )
804
840
  if phase_handle is not None:
805
- phase_handle.write("build_runtime:done\n")
841
+ phase_handle.write("build_agent:done\n")
842
+ phase_handle.write("build_cli_queue:start\n")
843
+ phase_handle.flush()
844
+ queued_agent = build_cli_queue(agent)
845
+ if phase_handle is not None:
846
+ phase_handle.write("build_cli_queue:done\n")
806
847
  phase_handle.flush()
807
848
  if should_run_interactive(args.prompt, sys.stdin.isatty()):
808
849
  return await run_interactive_session(
809
- runtime,
850
+ queued_agent,
810
851
  args.json,
811
- args.config,
852
+ str(config_path),
812
853
  )
813
854
  else:
814
855
  prompt_text = resolve_prompt_text(args.prompt)
815
- worker = asyncio.create_task(runtime.run_forever())
856
+ worker = asyncio.create_task(queued_agent.run_forever())
816
857
  if phase_handle is not None:
817
858
  phase_handle.write("submit_user_turn:start\n")
818
859
  phase_handle.flush()
819
- result = await runtime.submit_user_turn(prompt_text)
860
+ result = await queued_agent.submit_user_turn(prompt_text)
820
861
  if phase_handle is not None:
821
862
  phase_handle.write("submit_user_turn:done\n")
822
863
  phase_handle.flush()
@@ -835,10 +876,23 @@ async def run_cli(args: 'argparse.Namespace') -> 'int':
835
876
  finally:
836
877
  if phase_handle is not None:
837
878
  phase_handle.close()
838
- if runtime is not None and worker is not None:
839
- await runtime.shutdown()
879
+ if queued_agent is not None and worker is not None:
880
+ await queued_agent.shutdown()
840
881
  await worker
841
882
 
883
+ def ipython_agent(config_path: 'str' = DEFAULT_CODEX_CONFIG_PATH):
884
+ from loguru import logger
885
+ logger.remove()
886
+ logger.add(sys.stderr, level="INFO")
887
+
888
+ model = build_model(config_path)
889
+ agent = build_agent(client=model, config_path=config_path)
890
+
891
+ from pycodex.tools.ipython_tool import attach_ipython_tool
892
+
893
+ attach_ipython_tool(agent)
894
+
895
+ return agent
842
896
 
843
897
  def main(argv: 'typing.Union[Sequence[str], None]' = None) -> 'int':
844
898
  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):