python-codex 0.1.2__py3-none-any.whl → 0.1.4__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 +5 -1
- pycodex/agent.py +89 -51
- pycodex/cli.py +152 -45
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +110 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +429 -90
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/prompts/models.json +30 -0
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +69 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +45 -46
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +2 -4
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +62 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/__init__.py +4 -0
- pycodex/utils/compactor.py +189 -0
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +37 -33
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +483 -0
- pycodex/utils/visualize.py +197 -83
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/METADATA +32 -11
- python_codex-0.1.4.dist-info/RECORD +76 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/WHEEL +1 -1
- responses_server/app.py +32 -20
- responses_server/config.py +17 -17
- responses_server/payload_processors.py +26 -17
- responses_server/server.py +11 -11
- responses_server/session_store.py +10 -10
- responses_server/stream_router.py +83 -64
- responses_server/tools/custom_adapter.py +12 -12
- responses_server/tools/web_search.py +33 -33
- python_codex-0.1.2.dist-info/RECORD +0 -73
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/licenses/LICENSE +0 -0
pycodex/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from .compat import patch_asyncio
|
|
2
|
+
|
|
3
|
+
patch_asyncio()
|
|
4
|
+
|
|
1
5
|
from .agent import AgentLoop
|
|
2
6
|
from .context import ContextConfig, ContextManager
|
|
3
7
|
from .model import (
|
|
@@ -60,7 +64,7 @@ from .tools import (
|
|
|
60
64
|
WriteStdinTool,
|
|
61
65
|
)
|
|
62
66
|
|
|
63
|
-
def debug(stop: bool = False):
|
|
67
|
+
def debug(stop: 'bool' = False):
|
|
64
68
|
|
|
65
69
|
import socket
|
|
66
70
|
|
pycodex/agent.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import asyncio
|
|
4
3
|
import json
|
|
5
|
-
from
|
|
4
|
+
from typing import Callable
|
|
6
5
|
|
|
7
6
|
from .context import ContextManager
|
|
8
7
|
from .model import ModelClient
|
|
@@ -19,10 +18,14 @@ from .protocol import (
|
|
|
19
18
|
)
|
|
20
19
|
from .tools import ToolContext, ToolRegistry
|
|
21
20
|
from .utils import uuid7_string
|
|
21
|
+
import typing
|
|
22
|
+
|
|
23
|
+
if typing.TYPE_CHECKING:
|
|
24
|
+
from .utils.session_persist import SessionRolloutRecorder
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
EventHandler = Callable[[AgentEvent], None]
|
|
25
|
-
NOOP_EVENT_HANDLER: EventHandler = lambda _event: None
|
|
28
|
+
NOOP_EVENT_HANDLER: 'EventHandler' = lambda _event: None
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class TurnInterrupted(RuntimeError):
|
|
@@ -40,51 +43,66 @@ class AgentLoop:
|
|
|
40
43
|
|
|
41
44
|
def __init__(
|
|
42
45
|
self,
|
|
43
|
-
model_client: ModelClient,
|
|
44
|
-
tool_registry: ToolRegistry,
|
|
45
|
-
context_manager: ContextManager
|
|
46
|
-
parallel_tool_calls: bool = True,
|
|
47
|
-
event_handler: EventHandler = NOOP_EVENT_HANDLER,
|
|
48
|
-
initial_history:
|
|
49
|
-
|
|
46
|
+
model_client: 'ModelClient',
|
|
47
|
+
tool_registry: 'ToolRegistry',
|
|
48
|
+
context_manager: 'typing.Union[ContextManager, None]' = None,
|
|
49
|
+
parallel_tool_calls: 'bool' = True,
|
|
50
|
+
event_handler: 'EventHandler' = NOOP_EVENT_HANDLER,
|
|
51
|
+
initial_history: 'typing.Tuple[ConversationItem, ...]' = (),
|
|
52
|
+
rollout_recorder: 'typing.Union[SessionRolloutRecorder, None]' = None,
|
|
53
|
+
) -> 'None':
|
|
50
54
|
self._model_client = model_client
|
|
51
55
|
self._tool_registry = tool_registry
|
|
52
56
|
self._context_manager = context_manager or ContextManager()
|
|
53
57
|
self._parallel_tool_calls = parallel_tool_calls
|
|
54
58
|
self._event_handler = event_handler
|
|
55
|
-
self._history:
|
|
59
|
+
self._history: 'typing.List[ConversationItem]' = list(initial_history)
|
|
60
|
+
self._rollout_recorder = rollout_recorder
|
|
56
61
|
self.interrupt_asap = False
|
|
57
62
|
|
|
58
63
|
@property
|
|
59
|
-
def history(self) ->
|
|
64
|
+
def history(self) -> 'typing.Tuple[ConversationItem, ...]':
|
|
60
65
|
return tuple(self._history)
|
|
61
66
|
|
|
62
67
|
def set_event_handler(
|
|
63
|
-
self, event_handler: EventHandler = NOOP_EVENT_HANDLER
|
|
64
|
-
) -> None:
|
|
68
|
+
self, event_handler: 'EventHandler' = NOOP_EVENT_HANDLER
|
|
69
|
+
) -> 'None':
|
|
65
70
|
self._event_handler = event_handler
|
|
66
71
|
|
|
72
|
+
def replace_history(
|
|
73
|
+
self,
|
|
74
|
+
history: 'typing.Iterable[ConversationItem]',
|
|
75
|
+
) -> 'None':
|
|
76
|
+
self._history = list(history)
|
|
77
|
+
|
|
78
|
+
def set_rollout_recorder(
|
|
79
|
+
self,
|
|
80
|
+
rollout_recorder: 'typing.Union[SessionRolloutRecorder, None]',
|
|
81
|
+
) -> 'None':
|
|
82
|
+
self._rollout_recorder = rollout_recorder
|
|
83
|
+
|
|
67
84
|
def _raise_if_interrupt_requested(
|
|
68
85
|
self,
|
|
69
|
-
turn_id: str,
|
|
70
|
-
iteration: int,
|
|
71
|
-
output_text: str
|
|
72
|
-
) -> None:
|
|
86
|
+
turn_id: 'str',
|
|
87
|
+
iteration: 'int',
|
|
88
|
+
output_text: 'typing.Union[str, None]' = None,
|
|
89
|
+
) -> 'None':
|
|
73
90
|
if self.interrupt_asap:
|
|
74
91
|
self.interrupt_asap = False
|
|
75
|
-
payload:
|
|
92
|
+
payload: 'typing.Dict[str, object]' = {"iteration": iteration}
|
|
76
93
|
if output_text is not None:
|
|
77
94
|
payload["output_text"] = output_text
|
|
78
95
|
self._emit("turn_interrupted", turn_id, **payload)
|
|
79
96
|
raise TurnInterrupted("turn interrupted")
|
|
80
97
|
|
|
81
98
|
async def run_turn(
|
|
82
|
-
self, texts:
|
|
83
|
-
) -> TurnResult:
|
|
99
|
+
self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
|
|
100
|
+
) -> 'TurnResult':
|
|
84
101
|
turn_id = turn_id or uuid7_string()
|
|
85
102
|
self.interrupt_asap = False
|
|
86
|
-
for text in texts
|
|
87
|
-
|
|
103
|
+
new_user_messages = [UserMessage(text=text) for text in texts]
|
|
104
|
+
self._history.extend(new_user_messages)
|
|
105
|
+
self._persist_history_items(new_user_messages)
|
|
88
106
|
|
|
89
107
|
self._emit(
|
|
90
108
|
"turn_started",
|
|
@@ -93,10 +111,8 @@ class AgentLoop:
|
|
|
93
111
|
user_texts=list(texts),
|
|
94
112
|
)
|
|
95
113
|
|
|
96
|
-
last_assistant_message: str
|
|
97
|
-
final_response_items:
|
|
98
|
-
AssistantMessage | ToolCall | ReasoningItem, ...
|
|
99
|
-
] = ()
|
|
114
|
+
last_assistant_message: 'typing.Union[str, None]' = None
|
|
115
|
+
final_response_items: 'typing.Tuple[\n typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], ...\n]' = ()
|
|
100
116
|
|
|
101
117
|
iteration = 0
|
|
102
118
|
try:
|
|
@@ -132,13 +148,16 @@ class AgentLoop:
|
|
|
132
148
|
item_count=len(response.items),
|
|
133
149
|
)
|
|
134
150
|
|
|
135
|
-
tool_calls:
|
|
151
|
+
tool_calls: 'typing.List[ToolCall]' = []
|
|
152
|
+
persisted_response_items: 'typing.List[ConversationItem]' = []
|
|
136
153
|
for item in response.items:
|
|
137
154
|
self._history.append(item)
|
|
155
|
+
persisted_response_items.append(item)
|
|
138
156
|
if isinstance(item, AssistantMessage):
|
|
139
157
|
last_assistant_message = item.text
|
|
140
158
|
elif isinstance(item, ToolCall):
|
|
141
159
|
tool_calls.append(item)
|
|
160
|
+
self._persist_history_items(persisted_response_items)
|
|
142
161
|
|
|
143
162
|
if not tool_calls:
|
|
144
163
|
self._raise_if_interrupt_requested(
|
|
@@ -162,7 +181,10 @@ class AgentLoop:
|
|
|
162
181
|
|
|
163
182
|
tool_results = await self._execute_tool_batch(turn_id, tool_calls)
|
|
164
183
|
self._history.extend(tool_results)
|
|
165
|
-
self.
|
|
184
|
+
self._persist_history_items(tool_results)
|
|
185
|
+
follow_up_messages = self._build_follow_up_messages(tool_results)
|
|
186
|
+
self._history.extend(follow_up_messages)
|
|
187
|
+
self._persist_history_items(follow_up_messages)
|
|
166
188
|
self._raise_if_interrupt_requested(
|
|
167
189
|
turn_id,
|
|
168
190
|
iteration,
|
|
@@ -182,11 +204,11 @@ class AgentLoop:
|
|
|
182
204
|
|
|
183
205
|
async def _execute_tool_batch(
|
|
184
206
|
self,
|
|
185
|
-
turn_id: str,
|
|
186
|
-
tool_calls:
|
|
187
|
-
) ->
|
|
188
|
-
results:
|
|
189
|
-
parallel_batch:
|
|
207
|
+
turn_id: 'str',
|
|
208
|
+
tool_calls: 'typing.List[ToolCall]',
|
|
209
|
+
) -> 'typing.List[ToolResult]':
|
|
210
|
+
results: 'typing.List[ToolResult]' = []
|
|
211
|
+
parallel_batch: 'typing.List[ToolCall]' = []
|
|
190
212
|
|
|
191
213
|
for call in tool_calls:
|
|
192
214
|
can_run_parallel = (
|
|
@@ -224,11 +246,16 @@ class AgentLoop:
|
|
|
224
246
|
|
|
225
247
|
async def _run_single_tool(
|
|
226
248
|
self,
|
|
227
|
-
turn_id: str,
|
|
228
|
-
call: ToolCall,
|
|
229
|
-
prior_results:
|
|
230
|
-
) -> ToolResult:
|
|
231
|
-
|
|
249
|
+
turn_id: 'str',
|
|
250
|
+
call: 'ToolCall',
|
|
251
|
+
prior_results: 'typing.Tuple[ToolResult, ...]' = (),
|
|
252
|
+
) -> 'ToolResult':
|
|
253
|
+
payload: 'typing.Dict[str, object]' = {
|
|
254
|
+
"tool_name": call.name,
|
|
255
|
+
"call_id": call.call_id,
|
|
256
|
+
"call": call,
|
|
257
|
+
}
|
|
258
|
+
self._emit("tool_started", turn_id, **payload)
|
|
232
259
|
result = await self._tool_registry.execute(
|
|
233
260
|
call,
|
|
234
261
|
ToolContext(
|
|
@@ -237,32 +264,43 @@ class AgentLoop:
|
|
|
237
264
|
collaboration_mode=self._context_manager.collaboration_mode,
|
|
238
265
|
),
|
|
239
266
|
)
|
|
240
|
-
payload
|
|
241
|
-
|
|
242
|
-
"call_id": call.call_id,
|
|
243
|
-
"is_error": result.is_error,
|
|
244
|
-
"call": call,
|
|
245
|
-
"result": result,
|
|
246
|
-
}
|
|
267
|
+
payload["result"] = result
|
|
268
|
+
payload["is_error"] = result.is_error
|
|
247
269
|
self._emit("tool_completed", turn_id, **payload)
|
|
248
270
|
return result
|
|
249
271
|
|
|
250
|
-
def _emit(self, kind: str, turn_id: str, **payload: object) -> None:
|
|
272
|
+
def _emit(self, kind: 'str', turn_id: 'str', **payload: 'object') -> 'None':
|
|
251
273
|
self._event_handler(
|
|
252
274
|
AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
|
|
253
275
|
)
|
|
254
276
|
|
|
255
|
-
def
|
|
277
|
+
def _persist_history_items(
|
|
278
|
+
self,
|
|
279
|
+
items: 'typing.Iterable[ConversationItem]',
|
|
280
|
+
) -> 'None':
|
|
281
|
+
recorder = self._rollout_recorder
|
|
282
|
+
if recorder is None:
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
recorder.append_history_items(items)
|
|
286
|
+
except Exception: # pragma: no cover - persistence should not break turns
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
|
|
256
290
|
if event.kind == "assistant_delta":
|
|
257
291
|
self._emit("assistant_delta", turn_id, **event.payload)
|
|
258
292
|
elif event.kind == "tool_call":
|
|
259
293
|
self._emit("tool_called", turn_id, **event.payload)
|
|
294
|
+
elif event.kind == "token_count":
|
|
295
|
+
self._emit("token_count", turn_id, **event.payload)
|
|
296
|
+
elif event.kind == "stream_error":
|
|
297
|
+
self._emit("stream_error", turn_id, **event.payload)
|
|
260
298
|
|
|
261
299
|
def _build_follow_up_messages(
|
|
262
300
|
self,
|
|
263
|
-
tool_results:
|
|
264
|
-
) ->
|
|
265
|
-
follow_ups:
|
|
301
|
+
tool_results: 'typing.List[ToolResult]',
|
|
302
|
+
) -> 'typing.List[UserMessage]':
|
|
303
|
+
follow_ups: 'typing.List[UserMessage]' = []
|
|
266
304
|
for result in tool_results:
|
|
267
305
|
statuses = None
|
|
268
306
|
if (
|
pycodex/cli.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import atexit
|
|
4
3
|
import argparse
|
|
@@ -10,23 +9,35 @@ import sys
|
|
|
10
9
|
import tempfile
|
|
11
10
|
from dataclasses import asdict, replace
|
|
12
11
|
from pathlib import Path
|
|
13
|
-
from typing import
|
|
12
|
+
from typing import Sequence
|
|
14
13
|
|
|
15
14
|
from .agent import AgentLoop
|
|
16
15
|
from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
|
|
16
|
+
from .compat import Literal
|
|
17
17
|
from .context import ContextManager
|
|
18
18
|
from .model import ResponsesModelClient, ResponsesProviderConfig
|
|
19
19
|
from .portable import bootstrap_called_home, upload_codex_home
|
|
20
20
|
from .protocol import AgentEvent
|
|
21
21
|
from .runtime import AgentRuntime
|
|
22
22
|
from .runtime_services import RuntimeEnvironment, create_runtime_environment
|
|
23
|
-
from .utils import CliSessionView, load_codex_dotenv
|
|
23
|
+
from .utils import CliSessionView, load_codex_dotenv, uuid7_string
|
|
24
|
+
from .utils.compactor import compact_agent_loop
|
|
25
|
+
from .utils.session_persist import (
|
|
26
|
+
SessionRolloutRecorder,
|
|
27
|
+
conversation_history_to_turns,
|
|
28
|
+
list_resumable_sessions,
|
|
29
|
+
load_resumed_session,
|
|
30
|
+
resolve_codex_home,
|
|
31
|
+
)
|
|
32
|
+
import typing
|
|
24
33
|
|
|
25
34
|
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
26
35
|
HISTORY_COMMAND = "/history"
|
|
27
36
|
TITLE_COMMAND = "/title"
|
|
28
37
|
MODEL_COMMAND = "/model"
|
|
29
38
|
QUEUE_COMMAND = "/queue"
|
|
39
|
+
RESUME_COMMAND = "/resume"
|
|
40
|
+
COMPACT_COMMAND = "/compact"
|
|
30
41
|
CliSessionMode = Literal["exec", "tui"]
|
|
31
42
|
LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
|
|
32
43
|
CLI_ORIGINATOR = "codex-tui"
|
|
@@ -40,7 +51,7 @@ def launch_chat_completion_compat_server(*args, **kwargs):
|
|
|
40
51
|
return launch_compat_server(*args, **kwargs)
|
|
41
52
|
|
|
42
53
|
|
|
43
|
-
def configure_loguru() -> None:
|
|
54
|
+
def configure_loguru() -> 'None':
|
|
44
55
|
try:
|
|
45
56
|
from loguru import logger
|
|
46
57
|
except ImportError: # pragma: no cover - dependency may be absent in minimal envs
|
|
@@ -61,7 +72,7 @@ def configure_loguru() -> None:
|
|
|
61
72
|
logger.add(sys.stderr, level="DEBUG")
|
|
62
73
|
|
|
63
74
|
|
|
64
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
75
|
+
def build_parser() -> 'argparse.ArgumentParser':
|
|
65
76
|
parser = argparse.ArgumentParser(
|
|
66
77
|
prog="pycodex",
|
|
67
78
|
description="Minimal Codex-style local CLI backed by ~/.codex/config.toml.",
|
|
@@ -131,11 +142,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
131
142
|
return parser
|
|
132
143
|
|
|
133
144
|
|
|
134
|
-
def should_run_interactive(prompt_parts: Sequence[str], stdin_is_tty: bool) -> bool:
|
|
145
|
+
def should_run_interactive(prompt_parts: 'Sequence[str]', stdin_is_tty: 'bool') -> 'bool':
|
|
135
146
|
return not prompt_parts and stdin_is_tty
|
|
136
147
|
|
|
137
148
|
|
|
138
|
-
def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
|
|
149
|
+
def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
|
|
139
150
|
if prompt_parts:
|
|
140
151
|
return " ".join(prompt_parts).strip()
|
|
141
152
|
|
|
@@ -148,8 +159,8 @@ def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
|
|
|
148
159
|
|
|
149
160
|
|
|
150
161
|
def get_tools(
|
|
151
|
-
runtime_environment: RuntimeEnvironment
|
|
152
|
-
exec_mode: bool = False,
|
|
162
|
+
runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None,
|
|
163
|
+
exec_mode: 'bool' = False,
|
|
153
164
|
):
|
|
154
165
|
from .tools import (
|
|
155
166
|
ApplyPatchTool,
|
|
@@ -243,7 +254,7 @@ def get_tools(
|
|
|
243
254
|
return registry
|
|
244
255
|
|
|
245
256
|
|
|
246
|
-
def get_subagent_tools(runtime_environment: RuntimeEnvironment
|
|
257
|
+
def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None):
|
|
247
258
|
from .tools import (
|
|
248
259
|
ApplyPatchTool,
|
|
249
260
|
ExecCommandTool,
|
|
@@ -268,13 +279,13 @@ def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
|
|
|
268
279
|
|
|
269
280
|
|
|
270
281
|
def build_runtime(
|
|
271
|
-
config_path: str,
|
|
272
|
-
profile: str
|
|
273
|
-
system_prompt: str
|
|
282
|
+
config_path: 'str',
|
|
283
|
+
profile: 'typing.Union[str, None]',
|
|
284
|
+
system_prompt: 'typing.Union[str, None]',
|
|
274
285
|
client,
|
|
275
|
-
session_mode: CliSessionMode = "exec",
|
|
276
|
-
collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
|
|
277
|
-
) -> AgentRuntime:
|
|
286
|
+
session_mode: 'CliSessionMode' = "exec",
|
|
287
|
+
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
288
|
+
) -> 'AgentRuntime':
|
|
278
289
|
use_tui_context = session_mode == "tui"
|
|
279
290
|
context_manager = ContextManager.from_codex_config(
|
|
280
291
|
config_path,
|
|
@@ -283,6 +294,9 @@ def build_runtime(
|
|
|
283
294
|
collaboration_mode=collaboration_mode,
|
|
284
295
|
include_collaboration_instructions=use_tui_context,
|
|
285
296
|
)
|
|
297
|
+
session_id = getattr(client, "_session_id", None) or uuid7_string()
|
|
298
|
+
if hasattr(client, "_session_id"):
|
|
299
|
+
client._session_id = session_id
|
|
286
300
|
subagent_context_manager = ContextManager.from_codex_config(
|
|
287
301
|
config_path,
|
|
288
302
|
profile,
|
|
@@ -292,14 +306,22 @@ def build_runtime(
|
|
|
292
306
|
runtime_environment = create_runtime_environment()
|
|
293
307
|
runtime_environment.request_user_input_manager.set_handler(None)
|
|
294
308
|
runtime_environment.request_permissions_manager.set_handler(None)
|
|
309
|
+
rollout_recorder = SessionRolloutRecorder.create(
|
|
310
|
+
resolve_codex_home(config_path),
|
|
311
|
+
session_id,
|
|
312
|
+
context_manager.cwd,
|
|
313
|
+
getattr(client, "_originator", CLI_ORIGINATOR),
|
|
314
|
+
getattr(getattr(client, "_config", None), "provider_name", None),
|
|
315
|
+
context_manager.resolve_base_instructions(),
|
|
316
|
+
)
|
|
295
317
|
|
|
296
318
|
def make_subagent_runtime_builder(base_client):
|
|
297
319
|
def build_subagent_runtime(
|
|
298
|
-
model_override: str
|
|
299
|
-
reasoning_effort_override: str
|
|
320
|
+
model_override: 'typing.Union[str, None]',
|
|
321
|
+
reasoning_effort_override: 'typing.Union[str, None]',
|
|
300
322
|
initial_history=(),
|
|
301
|
-
session_id: str
|
|
302
|
-
) -> AgentRuntime:
|
|
323
|
+
session_id: 'typing.Union[str, None]' = None,
|
|
324
|
+
) -> 'AgentRuntime':
|
|
303
325
|
nested_client = base_client.with_overrides(
|
|
304
326
|
model_override,
|
|
305
327
|
reasoning_effort_override,
|
|
@@ -329,25 +351,28 @@ def build_runtime(
|
|
|
329
351
|
)
|
|
330
352
|
return AgentRuntime(
|
|
331
353
|
AgentLoop(
|
|
332
|
-
client,
|
|
354
|
+
client,
|
|
355
|
+
get_tools(runtime_environment, exec_mode=True),
|
|
356
|
+
context_manager,
|
|
357
|
+
rollout_recorder=rollout_recorder,
|
|
333
358
|
),
|
|
334
359
|
runtime_environment=runtime_environment,
|
|
335
360
|
)
|
|
336
361
|
|
|
337
362
|
|
|
338
|
-
def format_turn_output(result, json_mode: bool) -> str:
|
|
363
|
+
def format_turn_output(result, json_mode: 'bool') -> 'str':
|
|
339
364
|
if json_mode:
|
|
340
365
|
return json.dumps(asdict(result), ensure_ascii=False, indent=2)
|
|
341
366
|
return result.output_text or ""
|
|
342
367
|
|
|
343
368
|
|
|
344
369
|
def _build_model_client(
|
|
345
|
-
config_path: str,
|
|
346
|
-
profile: str
|
|
347
|
-
timeout_seconds: float,
|
|
348
|
-
managed_responses_base_url: str
|
|
349
|
-
vllm_endpoint: str
|
|
350
|
-
use_chat_completion: bool = False,
|
|
370
|
+
config_path: 'str',
|
|
371
|
+
profile: 'typing.Union[str, None]',
|
|
372
|
+
timeout_seconds: 'float',
|
|
373
|
+
managed_responses_base_url: 'typing.Union[str, None]' = None,
|
|
374
|
+
vllm_endpoint: 'typing.Union[str, None]' = None,
|
|
375
|
+
use_chat_completion: 'bool' = False,
|
|
351
376
|
):
|
|
352
377
|
load_codex_dotenv(config_path)
|
|
353
378
|
provider_config = ResponsesProviderConfig.from_codex_config(
|
|
@@ -393,13 +418,13 @@ def _build_model_client(
|
|
|
393
418
|
|
|
394
419
|
|
|
395
420
|
async def prompt_request_user_input(
|
|
396
|
-
view: CliSessionView,
|
|
397
|
-
payload:
|
|
398
|
-
) ->
|
|
421
|
+
view: 'CliSessionView',
|
|
422
|
+
payload: 'typing.Dict[str, object]',
|
|
423
|
+
) -> 'typing.Union[typing.Dict[str, object], None]':
|
|
399
424
|
view.finish_stream()
|
|
400
425
|
view.pause_spinner()
|
|
401
426
|
view.write_line("[request_user_input] waiting for user response")
|
|
402
|
-
answers:
|
|
427
|
+
answers: 'typing.Dict[str, typing.Dict[str, typing.List[str]]]' = {}
|
|
403
428
|
try:
|
|
404
429
|
for question in payload.get("questions", []):
|
|
405
430
|
if not isinstance(question, dict):
|
|
@@ -456,9 +481,9 @@ async def prompt_request_user_input(
|
|
|
456
481
|
|
|
457
482
|
|
|
458
483
|
async def prompt_request_permissions(
|
|
459
|
-
view: CliSessionView,
|
|
460
|
-
payload:
|
|
461
|
-
) ->
|
|
484
|
+
view: 'CliSessionView',
|
|
485
|
+
payload: 'typing.Dict[str, object]',
|
|
486
|
+
) -> 'typing.Union[typing.Dict[str, object], None]':
|
|
462
487
|
view.finish_stream()
|
|
463
488
|
view.pause_spinner()
|
|
464
489
|
view.write_line("[request_permissions] user approval required")
|
|
@@ -495,14 +520,18 @@ async def prompt_request_permissions(
|
|
|
495
520
|
|
|
496
521
|
|
|
497
522
|
async def run_interactive_session(
|
|
498
|
-
runtime: AgentRuntime,
|
|
499
|
-
json_mode: bool,
|
|
500
|
-
|
|
523
|
+
runtime: 'AgentRuntime',
|
|
524
|
+
json_mode: 'bool',
|
|
525
|
+
config_path: 'typing.Union[str, None]' = None,
|
|
526
|
+
) -> 'int':
|
|
501
527
|
worker = asyncio.create_task(runtime.run_forever())
|
|
528
|
+
context_window_tokens = runtime._agent_loop._context_manager.resolve_model_context_window()
|
|
502
529
|
view = CliSessionView()
|
|
530
|
+
view.set_context_window_tokens(context_window_tokens)
|
|
503
531
|
model_client = runtime._agent_loop._model_client
|
|
532
|
+
codex_home = resolve_codex_home(config_path)
|
|
504
533
|
runtime.set_event_handler(view.handle_event)
|
|
505
|
-
pending_turn_tasks:
|
|
534
|
+
pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
|
|
506
535
|
runtime_environment = runtime.runtime_environment
|
|
507
536
|
if runtime_environment is None:
|
|
508
537
|
runtime_environment = create_runtime_environment()
|
|
@@ -514,16 +543,49 @@ async def run_interactive_session(
|
|
|
514
543
|
lambda payload: prompt_request_permissions(view, payload)
|
|
515
544
|
)
|
|
516
545
|
view.write_line("pycodex interactive mode. Type /exit to quit.")
|
|
517
|
-
view.write_line("Extra commands: /history, /title, /model")
|
|
546
|
+
view.write_line("Extra commands: /history, /title, /model, /resume, /compact")
|
|
518
547
|
try:
|
|
519
548
|
|
|
520
|
-
def has_pending_turn_tasks() -> bool:
|
|
549
|
+
def has_pending_turn_tasks() -> 'bool':
|
|
521
550
|
pending_turn_tasks.difference_update(
|
|
522
551
|
task for task in tuple(pending_turn_tasks) if task.done()
|
|
523
552
|
)
|
|
524
553
|
return bool(pending_turn_tasks)
|
|
525
554
|
|
|
526
|
-
async def
|
|
555
|
+
async def run_manual_compact() -> 'None':
|
|
556
|
+
agent_loop = runtime._agent_loop
|
|
557
|
+
if not agent_loop.history:
|
|
558
|
+
view.write_line("Nothing to compact.")
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
compact_turn_id = uuid7_string()
|
|
562
|
+
|
|
563
|
+
def handle_compact_stream_event(event) -> 'None':
|
|
564
|
+
if event.kind not in {"token_count", "stream_error"}:
|
|
565
|
+
return
|
|
566
|
+
view.handle_event(
|
|
567
|
+
AgentEvent(
|
|
568
|
+
kind=event.kind,
|
|
569
|
+
turn_id=compact_turn_id,
|
|
570
|
+
payload=dict(event.payload),
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
view.write_line("Compacting conversation history...")
|
|
575
|
+
compact_result = await compact_agent_loop(
|
|
576
|
+
agent_loop,
|
|
577
|
+
handle_compact_stream_event,
|
|
578
|
+
)
|
|
579
|
+
if compact_result is None:
|
|
580
|
+
view.write_line("Nothing to compact.")
|
|
581
|
+
return
|
|
582
|
+
view.load_session_history(
|
|
583
|
+
getattr(view, "_title", None),
|
|
584
|
+
conversation_history_to_turns(compact_result.history),
|
|
585
|
+
)
|
|
586
|
+
view.write_line(compact_result.display_text())
|
|
587
|
+
|
|
588
|
+
async def wait_for_turn_result(future) -> 'None':
|
|
527
589
|
try:
|
|
528
590
|
result = await future
|
|
529
591
|
except Exception as exc: # pragma: no cover - defensive surface
|
|
@@ -555,6 +617,50 @@ async def run_interactive_session(
|
|
|
555
617
|
if prompt_text == TITLE_COMMAND:
|
|
556
618
|
view.show_title()
|
|
557
619
|
continue
|
|
620
|
+
if prompt_text == RESUME_COMMAND:
|
|
621
|
+
sessions = list_resumable_sessions(codex_home)
|
|
622
|
+
if not sessions:
|
|
623
|
+
view.write_line("No resumable sessions found.")
|
|
624
|
+
continue
|
|
625
|
+
view.write_line("Available sessions:")
|
|
626
|
+
for index, session in enumerate(sessions, start=1):
|
|
627
|
+
view.write_line(f"[{index}] {session['preview']}")
|
|
628
|
+
continue
|
|
629
|
+
if prompt_text.startswith(f"{RESUME_COMMAND} "):
|
|
630
|
+
if has_pending_turn_tasks():
|
|
631
|
+
view.write_line(
|
|
632
|
+
"Cannot resume while work is running or queued."
|
|
633
|
+
)
|
|
634
|
+
continue
|
|
635
|
+
resume_target = prompt_text[len(RESUME_COMMAND) :].strip()
|
|
636
|
+
try:
|
|
637
|
+
resumed = load_resumed_session(codex_home, resume_target)
|
|
638
|
+
runtime._agent_loop.replace_history(resumed["history"])
|
|
639
|
+
if hasattr(model_client, "_session_id"):
|
|
640
|
+
model_client._session_id = str(resumed["session_id"])
|
|
641
|
+
runtime._agent_loop.set_rollout_recorder(
|
|
642
|
+
SessionRolloutRecorder.resume(resumed["rollout_path"])
|
|
643
|
+
)
|
|
644
|
+
view.load_session_history(
|
|
645
|
+
str(resumed["title"]),
|
|
646
|
+
tuple(resumed["turns"]),
|
|
647
|
+
)
|
|
648
|
+
view.write_line(f"Resumed session: {resumed['title']}")
|
|
649
|
+
view.show_history()
|
|
650
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
651
|
+
view.show_error(str(exc))
|
|
652
|
+
continue
|
|
653
|
+
if prompt_text == COMPACT_COMMAND:
|
|
654
|
+
if has_pending_turn_tasks():
|
|
655
|
+
view.write_line(
|
|
656
|
+
"Cannot compact while work is running or queued."
|
|
657
|
+
)
|
|
658
|
+
continue
|
|
659
|
+
try:
|
|
660
|
+
await run_manual_compact()
|
|
661
|
+
except Exception as exc: # pragma: no cover - defensive surface
|
|
662
|
+
view.show_error(str(exc))
|
|
663
|
+
continue
|
|
558
664
|
if prompt_text.startswith(f"{QUEUE_COMMAND} "):
|
|
559
665
|
queued_text = prompt_text[len(QUEUE_COMMAND) :].strip()
|
|
560
666
|
if not queued_text:
|
|
@@ -618,7 +724,7 @@ async def run_interactive_session(
|
|
|
618
724
|
return 0
|
|
619
725
|
|
|
620
726
|
|
|
621
|
-
async def run_cli(args: argparse.Namespace) -> int:
|
|
727
|
+
async def run_cli(args: 'argparse.Namespace') -> 'int':
|
|
622
728
|
runtime = None
|
|
623
729
|
worker = None
|
|
624
730
|
try:
|
|
@@ -628,7 +734,7 @@ async def run_cli(args: argparse.Namespace) -> int:
|
|
|
628
734
|
raise ValueError("--put does not accept prompt text")
|
|
629
735
|
configure_loguru()
|
|
630
736
|
if args.put is not None:
|
|
631
|
-
def emit_put_log(message: str) -> None:
|
|
737
|
+
def emit_put_log(message: 'str') -> 'None':
|
|
632
738
|
print(message, flush=True)
|
|
633
739
|
|
|
634
740
|
call_spec = upload_codex_home(args.put, event_handler=emit_put_log)
|
|
@@ -662,6 +768,7 @@ async def run_cli(args: argparse.Namespace) -> int:
|
|
|
662
768
|
return await run_interactive_session(
|
|
663
769
|
runtime,
|
|
664
770
|
args.json,
|
|
771
|
+
args.config,
|
|
665
772
|
)
|
|
666
773
|
else:
|
|
667
774
|
prompt_text = resolve_prompt_text(args.prompt)
|
|
@@ -678,7 +785,7 @@ async def run_cli(args: argparse.Namespace) -> int:
|
|
|
678
785
|
await worker
|
|
679
786
|
|
|
680
787
|
|
|
681
|
-
def main(argv: Sequence[str]
|
|
788
|
+
def main(argv: 'typing.Union[Sequence[str], None]' = None) -> 'int':
|
|
682
789
|
raw_args = list(argv) if argv is not None else None
|
|
683
790
|
if raw_args is None:
|
|
684
791
|
raw_args = sys.argv[1:]
|
pycodex/collaboration.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
|
-
from
|
|
2
|
+
from .compat import Literal
|
|
3
|
+
import typing
|
|
4
4
|
|
|
5
5
|
CollaborationMode = Literal["default", "plan", "execute", "pair_programming"]
|
|
6
6
|
|
|
7
|
-
DEFAULT_COLLABORATION_MODE: CollaborationMode = "default"
|
|
8
|
-
PLAN_COLLABORATION_MODE: CollaborationMode = "plan"
|
|
7
|
+
DEFAULT_COLLABORATION_MODE: 'CollaborationMode' = "default"
|
|
8
|
+
PLAN_COLLABORATION_MODE: 'CollaborationMode' = "plan"
|
|
9
9
|
|
|
10
|
-
_MODE_DISPLAY_NAMES:
|
|
10
|
+
_MODE_DISPLAY_NAMES: 'typing.Dict[str, str]' = {
|
|
11
11
|
"default": "Default",
|
|
12
12
|
"plan": "Plan",
|
|
13
13
|
"execute": "Execute",
|
|
@@ -15,7 +15,6 @@ _MODE_DISPLAY_NAMES: dict[str, str] = {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def collaboration_mode_display_name(mode: str
|
|
18
|
+
def collaboration_mode_display_name(mode: 'typing.Union[str, None]') -> 'str':
|
|
19
19
|
normalized = (mode or DEFAULT_COLLABORATION_MODE).strip().lower()
|
|
20
20
|
return _MODE_DISPLAY_NAMES.get(normalized, normalized.replace("_", " ").title())
|
|
21
|
-
|