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/__init__.py +10 -8
- pycodex/agent.py +226 -21
- pycodex/cli.py +199 -145
- pycodex/compat.py +8 -4
- pycodex/context.py +16 -0
- pycodex/feishu_card.py +693 -0
- pycodex/feishu_link.py +342 -0
- pycodex/model.py +102 -7
- pycodex/prompts/models.json +4 -4
- pycodex/protocol.py +17 -17
- pycodex/runtime.py +9 -14
- pycodex/runtime_services.py +45 -23
- pycodex/tools/apply_patch_tool.py +11 -12
- pycodex/tools/ipython_tool.py +144 -0
- pycodex/tools/unified_exec_manager.py +3 -0
- pycodex/utils/__init__.py +2 -13
- pycodex/utils/async_bridge.py +54 -0
- pycodex/utils/compactor.py +96 -19
- pycodex/utils/session_persist.py +57 -38
- pycodex/utils/toolcall_visualize.py +713 -0
- pycodex/utils/visualize.py +252 -837
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/METADATA +15 -2
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/RECORD +28 -23
- responses_server/app.py +7 -3
- responses_server/stream_router.py +39 -1
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/WHEEL +0 -0
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.11.dist-info → python_codex-0.1.13.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
|
23
|
-
from .runtime_services import
|
|
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
|
|
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(
|
|
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[
|
|
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
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
) -> '
|
|
298
|
-
|
|
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=
|
|
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 =
|
|
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
|
|
328
|
-
def
|
|
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
|
-
) -> '
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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 =
|
|
352
|
+
sub_agent = Agent(
|
|
347
353
|
nested_client,
|
|
348
|
-
get_subagent_tools(
|
|
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
|
|
353
|
-
sub_agent, runtime_environment=subagent_runtime_environment
|
|
354
|
-
)
|
|
359
|
+
return CliSubmissionQueue(sub_agent)
|
|
355
360
|
|
|
356
|
-
return
|
|
361
|
+
return build_subagent_queue
|
|
357
362
|
|
|
358
|
-
runtime_environment.subagent_manager.
|
|
359
|
-
|
|
363
|
+
runtime_environment.subagent_manager.set_queue_builder(
|
|
364
|
+
make_subagent_queue_builder(client)
|
|
360
365
|
)
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
547
|
-
context_window_tokens =
|
|
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 =
|
|
550
|
+
model_client = queue._agent._model_client
|
|
551
551
|
codex_home = resolve_codex_home(config_path)
|
|
552
|
-
|
|
552
|
+
queue.set_event_handler(view.handle_event)
|
|
553
553
|
pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
|
|
554
|
-
runtime_environment =
|
|
554
|
+
runtime_environment = queue._agent.runtime_environment
|
|
555
555
|
if runtime_environment is None:
|
|
556
|
-
runtime_environment =
|
|
557
|
-
|
|
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(
|
|
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
|
-
|
|
576
|
-
if not
|
|
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
|
|
595
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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("
|
|
819
|
+
phase_handle.write("build_model:start\n")
|
|
781
820
|
phase_handle.flush()
|
|
782
|
-
|
|
783
|
-
|
|
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("
|
|
830
|
+
phase_handle.write("build_model:done\n")
|
|
831
|
+
phase_handle.write("build_agent:start\n")
|
|
792
832
|
phase_handle.flush()
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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("
|
|
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
|
-
|
|
850
|
+
queued_agent,
|
|
810
851
|
args.json,
|
|
811
|
-
|
|
852
|
+
str(config_path),
|
|
812
853
|
)
|
|
813
854
|
else:
|
|
814
855
|
prompt_text = resolve_prompt_text(args.prompt)
|
|
815
|
-
worker = asyncio.create_task(
|
|
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
|
|
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
|
|
839
|
-
await
|
|
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
|
-
|
|
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):
|