python-codex 0.2.7__py3-none-any.whl → 0.3.0__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 +14 -14
- pycodex/agent.py +465 -499
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -510
- pycodex/compat.py +19 -5
- pycodex/context.py +222 -212
- pycodex/doctor.py +52 -48
- pycodex/events.py +857 -0
- pycodex/feishu_card.py +217 -163
- pycodex/feishu_link.py +43 -83
- pycodex/model.py +324 -253
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +76 -45
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +245 -983
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -176
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +1 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +58 -25
- pycodex/tools/close_agent_tool.py +2 -2
- pycodex/tools/code_mode_manager.py +77 -64
- pycodex/tools/exec_command_tool.py +26 -11
- pycodex/tools/exec_tool.py +4 -4
- pycodex/tools/grep_files_tool.py +12 -10
- pycodex/tools/ipython_tool.py +10 -13
- pycodex/tools/list_dir_tool.py +13 -9
- pycodex/tools/read_file_tool.py +29 -17
- pycodex/tools/request_permissions_tool.py +15 -5
- pycodex/tools/request_user_input_tool.py +13 -104
- pycodex/tools/resume_agent_tool.py +2 -2
- pycodex/tools/send_input_tool.py +11 -8
- pycodex/tools/shell_command_tool.py +7 -5
- pycodex/tools/shell_tool.py +7 -5
- pycodex/tools/spawn_agent_tool.py +7 -4
- pycodex/tools/unified_exec_manager.py +102 -69
- pycodex/tools/update_plan_tool.py +8 -5
- pycodex/tools/view_image_tool.py +7 -5
- pycodex/tools/wait_agent_tool.py +27 -4
- pycodex/tools/wait_tool.py +5 -4
- pycodex/tools/web_search_tool.py +4 -2
- pycodex/tools/write_stdin_tool.py +12 -11
- pycodex/utils/__init__.py +2 -17
- pycodex/utils/compactor.py +41 -72
- pycodex/utils/debug.py +2 -2
- pycodex/utils/dotenv.py +6 -7
- pycodex/utils/event_helpers.py +190 -0
- pycodex/utils/get_env.py +27 -70
- pycodex/{image_utils.py → utils/image_utils.py} +8 -11
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +217 -163
- pycodex/utils/truncation.py +21 -45
- python_codex-0.3.0.dist-info/METADATA +704 -0
- python_codex-0.3.0.dist-info/RECORD +90 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +36 -31
- responses_server/config.py +23 -23
- responses_server/messages_api.py +51 -53
- responses_server/payload_processors.py +25 -20
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +101 -98
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +36 -14
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +461 -375
- workspace_server/workspace.html +852 -228
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +137 -79
- pycodex/collaboration.py +0 -20
- pycodex/interactive_session.py +0 -415
- pycodex/prompts/collaboration_default.md +0 -11
- pycodex/prompts/collaboration_plan.md +0 -128
- pycodex/utils/toolcall_visualize.py +0 -713
- pycodex/utils/visualize.py +0 -560
- python_codex-0.2.7.dist-info/METADATA +0 -455
- python_codex-0.2.7.dist-info/RECORD +0 -93
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/utils/compactor.py
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
import typing
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
from ..protocol import (
|
|
4
5
|
AssistantMessage,
|
|
6
|
+
ContextMessage,
|
|
5
7
|
ConversationItem,
|
|
6
8
|
ModelResponse,
|
|
7
|
-
ModelStreamEvent,
|
|
8
9
|
ToolCall,
|
|
9
10
|
ToolResult,
|
|
10
11
|
UserMessage,
|
|
11
12
|
)
|
|
12
13
|
from .random_ids import uuid7_string
|
|
13
|
-
import typing
|
|
14
14
|
|
|
15
15
|
if typing.TYPE_CHECKING:
|
|
16
|
-
from ..
|
|
16
|
+
from ..context import ContextManager
|
|
17
|
+
from ..events import ModelEvent
|
|
18
|
+
from ..model import ModelClient
|
|
17
19
|
|
|
18
20
|
DEFAULT_COMPACT_PROMPT = """You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
19
21
|
|
|
@@ -36,33 +38,22 @@ SUMMARY_PREFIX = (
|
|
|
36
38
|
"not merely repeat or acknowledge the handoff:"
|
|
37
39
|
)
|
|
38
40
|
|
|
41
|
+
|
|
39
42
|
@dataclass(frozen=True)
|
|
40
43
|
class CompactResult:
|
|
41
|
-
history:
|
|
42
|
-
original_item_count:
|
|
43
|
-
pruned_tool_results:
|
|
44
|
+
history: "typing.Tuple[ConversationItem, ...]"
|
|
45
|
+
original_item_count: "int"
|
|
46
|
+
pruned_tool_results: "int" = 0
|
|
44
47
|
|
|
45
48
|
@property
|
|
46
|
-
def retained_item_count(self) ->
|
|
49
|
+
def retained_item_count(self) -> "int":
|
|
47
50
|
return max(len(self.history) - 1, 0)
|
|
48
51
|
|
|
49
|
-
def display_text(self) -> 'str':
|
|
50
|
-
retained_label = _pluralize("item", self.retained_item_count)
|
|
51
|
-
original_label = _pluralize("item", self.original_item_count)
|
|
52
|
-
text = (
|
|
53
|
-
f"compact({self.original_item_count} {original_label}) -> "
|
|
54
|
-
f"{self.retained_item_count} {retained_label} + [summary]"
|
|
55
|
-
)
|
|
56
|
-
if self.pruned_tool_results:
|
|
57
|
-
tool_label = _pluralize("tool response", self.pruned_tool_results)
|
|
58
|
-
text += f" (dropped {self.pruned_tool_results} old {tool_label})"
|
|
59
|
-
return text
|
|
60
|
-
|
|
61
52
|
|
|
62
53
|
def compact(
|
|
63
|
-
history:
|
|
64
|
-
session_file:
|
|
65
|
-
) ->
|
|
54
|
+
history: "typing.Sequence[ConversationItem]",
|
|
55
|
+
session_file: "typing.Union[str, None]" = None,
|
|
56
|
+
) -> "typing.Tuple[ConversationItem, ...]":
|
|
66
57
|
summary_text = _build_summary_message(_last_assistant_message(history))
|
|
67
58
|
if session_file:
|
|
68
59
|
summary_text += (
|
|
@@ -75,30 +66,34 @@ def compact(
|
|
|
75
66
|
return build_compacted_history(summary_text)
|
|
76
67
|
|
|
77
68
|
|
|
78
|
-
async def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
async def compact_history(
|
|
70
|
+
history: "typing.Sequence[ConversationItem]",
|
|
71
|
+
model_client: "ModelClient",
|
|
72
|
+
context_manager: "ContextManager",
|
|
73
|
+
stream_event_handler: "typing.Union[typing.Callable[[ModelEvent], None], None]" = None,
|
|
74
|
+
prune_tool_results_on_context_error: "bool" = False,
|
|
75
|
+
session_file: "typing.Union[str, None]" = None,
|
|
76
|
+
turn_id: "typing.Union[str, None]" = None,
|
|
77
|
+
) -> "typing.Union[CompactResult, None]":
|
|
78
|
+
from ..model import ContextLengthExceeded, ResponsesIncompleteError
|
|
84
79
|
|
|
85
|
-
history = agent.history
|
|
86
80
|
if not history:
|
|
87
81
|
return None
|
|
88
82
|
original_item_count = len(history)
|
|
89
83
|
pruned_tool_results = 0
|
|
84
|
+
turn_id = turn_id or uuid7_string()
|
|
90
85
|
|
|
91
86
|
noop_stream_event_handler = lambda _event: None
|
|
92
87
|
while True:
|
|
93
88
|
compact_prompt = UserMessage(text=DEFAULT_COMPACT_PROMPT)
|
|
94
|
-
prompt =
|
|
89
|
+
prompt = context_manager.build_prompt(
|
|
95
90
|
list(history) + [compact_prompt],
|
|
96
91
|
[],
|
|
97
92
|
False,
|
|
98
|
-
turn_id=
|
|
93
|
+
turn_id=turn_id,
|
|
99
94
|
)
|
|
100
95
|
try:
|
|
101
|
-
response = await
|
|
96
|
+
response = await model_client.complete(
|
|
102
97
|
prompt,
|
|
103
98
|
stream_event_handler or noop_stream_event_handler,
|
|
104
99
|
)
|
|
@@ -111,32 +106,22 @@ async def compact_agent(
|
|
|
111
106
|
raise
|
|
112
107
|
response = ModelResponse(items=list(exc.partial_items))
|
|
113
108
|
break
|
|
114
|
-
except
|
|
115
|
-
if
|
|
116
|
-
not prune_tool_results_on_context_error
|
|
117
|
-
or not _is_context_length_error(str(exc))
|
|
118
|
-
):
|
|
109
|
+
except ContextLengthExceeded:
|
|
110
|
+
if not prune_tool_results_on_context_error:
|
|
119
111
|
raise
|
|
120
112
|
pruned_history = prune_oldest_tool_response(history)
|
|
121
113
|
if pruned_history is None:
|
|
122
114
|
raise
|
|
123
115
|
history = pruned_history
|
|
124
116
|
pruned_tool_results += 1
|
|
125
|
-
agent.replace_history(history)
|
|
126
117
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if rollout_recorder is not None
|
|
131
|
-
else None
|
|
132
|
-
)
|
|
118
|
+
summary = _last_assistant_message(response.items)
|
|
119
|
+
if summary is None or not summary.strip():
|
|
120
|
+
raise ValueError("compact response contains no assistant summary")
|
|
133
121
|
compacted_history = compact(
|
|
134
|
-
|
|
122
|
+
response.items,
|
|
135
123
|
session_file,
|
|
136
124
|
)
|
|
137
|
-
agent.replace_history(compacted_history)
|
|
138
|
-
if rollout_recorder is not None:
|
|
139
|
-
rollout_recorder.append_compacted_history(compacted_history)
|
|
140
125
|
return CompactResult(
|
|
141
126
|
history=compacted_history,
|
|
142
127
|
original_item_count=original_item_count,
|
|
@@ -145,8 +130,8 @@ async def compact_agent(
|
|
|
145
130
|
|
|
146
131
|
|
|
147
132
|
def prune_oldest_tool_response(
|
|
148
|
-
history:
|
|
149
|
-
) ->
|
|
133
|
+
history: "typing.Sequence[ConversationItem]",
|
|
134
|
+
) -> "typing.Union[typing.Tuple[ConversationItem, ...], None]":
|
|
150
135
|
items = list(history)
|
|
151
136
|
tool_result_index = None
|
|
152
137
|
call_id = None
|
|
@@ -170,36 +155,20 @@ def prune_oldest_tool_response(
|
|
|
170
155
|
|
|
171
156
|
|
|
172
157
|
def build_compacted_history(
|
|
173
|
-
summary_text:
|
|
174
|
-
) ->
|
|
175
|
-
return (
|
|
158
|
+
summary_text: "str",
|
|
159
|
+
) -> "typing.Tuple[ConversationItem, ...]":
|
|
160
|
+
return (ContextMessage(text=summary_text or _build_summary_message(None)),)
|
|
176
161
|
|
|
177
162
|
|
|
178
163
|
def _last_assistant_message(
|
|
179
|
-
history:
|
|
180
|
-
) ->
|
|
164
|
+
history: "typing.Sequence[ConversationItem]",
|
|
165
|
+
) -> "typing.Union[str, None]":
|
|
181
166
|
for item in reversed(tuple(history)):
|
|
182
167
|
if isinstance(item, AssistantMessage):
|
|
183
168
|
return item.text
|
|
184
169
|
return None
|
|
185
170
|
|
|
186
171
|
|
|
187
|
-
def _build_summary_message(summary_text:
|
|
172
|
+
def _build_summary_message(summary_text: "typing.Union[str, None]") -> "str":
|
|
188
173
|
normalized = (summary_text or "").strip() or "(no summary available)"
|
|
189
174
|
return f"{SUMMARY_PREFIX}\n{normalized}"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _pluralize(noun: 'str', count: 'int') -> 'str':
|
|
193
|
-
if count == 1:
|
|
194
|
-
return noun
|
|
195
|
-
return f"{noun}s"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _is_context_length_error(message: 'str') -> 'bool':
|
|
199
|
-
lower = message.lower()
|
|
200
|
-
return (
|
|
201
|
-
"context_length_exceeded" in lower
|
|
202
|
-
or "maximum context length" in lower
|
|
203
|
-
or "exceeds the context window" in lower
|
|
204
|
-
or "exceeded the context window" in lower
|
|
205
|
-
)
|
pycodex/utils/debug.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
import typing
|
|
3
|
+
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def get_debug_dir() ->
|
|
6
|
+
def get_debug_dir() -> "typing.Union[Path, None]":
|
|
7
7
|
value = os.environ.get("PYCODEX_DEBUG_LOG", "").strip()
|
|
8
8
|
if not value:
|
|
9
9
|
return None
|
pycodex/utils/dotenv.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
import os
|
|
3
|
-
from pathlib import Path
|
|
4
2
|
import typing
|
|
3
|
+
from pathlib import Path
|
|
5
4
|
|
|
6
5
|
ILLEGAL_ENV_VAR_PREFIX = "CODEX_"
|
|
7
6
|
DOTENV_FILENAME = ".env"
|
|
8
|
-
_LOADED_CODEX_DOTENV_HOMES:
|
|
7
|
+
_LOADED_CODEX_DOTENV_HOMES: "typing.Set[str]" = set()
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
def load_codex_dotenv(config_path:
|
|
10
|
+
def load_codex_dotenv(config_path: "typing.Union[str, Path]") -> "None":
|
|
12
11
|
codex_home = str(Path(config_path).resolve().parent)
|
|
13
12
|
if codex_home in _LOADED_CODEX_DOTENV_HOMES:
|
|
14
13
|
return
|
|
@@ -28,8 +27,8 @@ def load_codex_dotenv(config_path: 'typing.Union[str, Path]') -> 'None':
|
|
|
28
27
|
_LOADED_CODEX_DOTENV_HOMES.add(codex_home)
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
def parse_dotenv(text:
|
|
32
|
-
values:
|
|
30
|
+
def parse_dotenv(text: "str") -> "typing.Dict[str, str]":
|
|
31
|
+
values: "typing.Dict[str, str]" = {}
|
|
33
32
|
for raw_line in text.splitlines():
|
|
34
33
|
line = raw_line.strip()
|
|
35
34
|
if not line or line.startswith("#"):
|
|
@@ -47,7 +46,7 @@ def parse_dotenv(text: 'str') -> 'typing.Dict[str, str]':
|
|
|
47
46
|
return values
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
def parse_dotenv_value(raw_value:
|
|
49
|
+
def parse_dotenv_value(raw_value: "str") -> "str":
|
|
51
50
|
if not raw_value:
|
|
52
51
|
return ""
|
|
53
52
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Stateless presentation helpers; no event or frontend dependencies."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import typing
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
PROMPT_CONTEXT_BASELINE_TOKENS = 12_000
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_result(kind, result, json_mode, log):
|
|
11
|
+
if kind == "turn":
|
|
12
|
+
text = (
|
|
13
|
+
json.dumps(asdict(result), ensure_ascii=False, indent=2)
|
|
14
|
+
if json_mode
|
|
15
|
+
else (result.output_text or "")
|
|
16
|
+
)
|
|
17
|
+
log(text)
|
|
18
|
+
elif kind == "command":
|
|
19
|
+
for line in format_command_result(result).splitlines():
|
|
20
|
+
log(line)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_command_result(result: "typing.Dict[str, object]") -> "str":
|
|
24
|
+
kind = result["kind"]
|
|
25
|
+
if kind == "help":
|
|
26
|
+
return "Extra commands: " + ", ".join("/" + name for name in result["commands"])
|
|
27
|
+
if kind in {"title", "title_changed"}:
|
|
28
|
+
return "Session: " + (result["title"] or "untitled")
|
|
29
|
+
if kind == "models":
|
|
30
|
+
return (
|
|
31
|
+
"Current model: "
|
|
32
|
+
+ result["model"]
|
|
33
|
+
+ "\nAvailable models: "
|
|
34
|
+
+ ", ".join(result["models"])
|
|
35
|
+
)
|
|
36
|
+
if kind == "model_changed":
|
|
37
|
+
return "Switched model to {0}.".format(result["model"])
|
|
38
|
+
if kind == "sessions":
|
|
39
|
+
if not result["sessions"]:
|
|
40
|
+
return "No resumable sessions found."
|
|
41
|
+
return "\n".join(
|
|
42
|
+
["Available sessions:"]
|
|
43
|
+
+ [
|
|
44
|
+
"[{0}] {1}".format(index, session["preview"])
|
|
45
|
+
for index, session in enumerate(result["sessions"], 1)
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
if kind in {"history", "resumed"}:
|
|
49
|
+
state = result["state"]
|
|
50
|
+
lines = ["Resumed session: " + state["title"]] if kind == "resumed" else []
|
|
51
|
+
if not state["history"]:
|
|
52
|
+
return "\n".join(lines + ["No history yet."])
|
|
53
|
+
lines.append("Session: " + (state["title"] or "untitled"))
|
|
54
|
+
for index, (prompt, response) in enumerate(state["history"], 1):
|
|
55
|
+
if prompt:
|
|
56
|
+
lines.append("[{0}]U> {1}".format(index, prompt))
|
|
57
|
+
if response:
|
|
58
|
+
lines.append("[{0}]A> {1}".format(index, response))
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
if kind == "compact_empty":
|
|
61
|
+
return "Nothing to compact."
|
|
62
|
+
if kind == "compacted":
|
|
63
|
+
return compact_summary(
|
|
64
|
+
result["original_item_count"],
|
|
65
|
+
result["retained_item_count"],
|
|
66
|
+
result["pruned_tool_results"],
|
|
67
|
+
)
|
|
68
|
+
if kind == "forked":
|
|
69
|
+
return "Forked session: " + result["session_id"]
|
|
70
|
+
if kind == "closed":
|
|
71
|
+
return ""
|
|
72
|
+
return "\n".join(result["lines"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_error(text, indent=True):
|
|
76
|
+
if not indent:
|
|
77
|
+
return "Error: " + str(text)
|
|
78
|
+
lines = str(text).splitlines() or [""]
|
|
79
|
+
return "\n".join(
|
|
80
|
+
["Error: " + lines[0]] + [" " + line if line else "" for line in lines[1:]]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def shorten_title(text: "str", limit: "int" = 48) -> "str":
|
|
85
|
+
return truncate_text(text, limit)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def percent_of_context_window_remaining(total_tokens, context_window_tokens):
|
|
89
|
+
if context_window_tokens <= PROMPT_CONTEXT_BASELINE_TOKENS:
|
|
90
|
+
return 0
|
|
91
|
+
effective_window = context_window_tokens - PROMPT_CONTEXT_BASELINE_TOKENS
|
|
92
|
+
used = max(total_tokens - PROMPT_CONTEXT_BASELINE_TOKENS, 0)
|
|
93
|
+
remaining = max(effective_window - used, 0)
|
|
94
|
+
return int(round(max(0.0, min(100.0, (remaining / effective_window) * 100.0))))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def completed_history(state):
|
|
98
|
+
active = state["active_turn"]
|
|
99
|
+
if active is not None:
|
|
100
|
+
return active["completed_history"]
|
|
101
|
+
return state["history"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def colorize_cli_message(text: "str", kind: "str", enabled: "bool") -> "str":
|
|
105
|
+
if not enabled:
|
|
106
|
+
return text
|
|
107
|
+
if kind == "assistant":
|
|
108
|
+
color = "\x1b[32m"
|
|
109
|
+
elif kind == "status":
|
|
110
|
+
color = "\x1b[36m"
|
|
111
|
+
elif kind == "error":
|
|
112
|
+
color = "\x1b[31m"
|
|
113
|
+
else:
|
|
114
|
+
return text
|
|
115
|
+
return "\x1b[1m" + color + text + "\x1b[0m"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def colorize_tool_message(
|
|
119
|
+
message: "str", enabled: "bool", tool_name: "str" = None
|
|
120
|
+
) -> "str":
|
|
121
|
+
if not enabled:
|
|
122
|
+
return message
|
|
123
|
+
if message.startswith("[error]"):
|
|
124
|
+
return colorize_cli_message(message, "error", enabled)
|
|
125
|
+
if message.startswith("[") and message.find("]") > 1:
|
|
126
|
+
tool_name = message[1 : message.find("]")]
|
|
127
|
+
if message.startswith((" [x]", " [>]", " [ ]")) or tool_name == "update_plan":
|
|
128
|
+
color = "\x1b[36m"
|
|
129
|
+
elif tool_name == "exec_command" and "session_id=" in message:
|
|
130
|
+
color = "\x1b[2m"
|
|
131
|
+
elif tool_name == "exec_command" and not message.startswith("["):
|
|
132
|
+
color = ""
|
|
133
|
+
elif tool_name in {"exec_command", "shell", "shell_command", "exec"}:
|
|
134
|
+
color = "\x1b[33m"
|
|
135
|
+
elif tool_name in {"write_stdin", "wait", "web_search"}:
|
|
136
|
+
color = "\x1b[35m"
|
|
137
|
+
elif tool_name in {
|
|
138
|
+
"spawn_agent",
|
|
139
|
+
"send_input",
|
|
140
|
+
"wait_agent",
|
|
141
|
+
"resume_agent",
|
|
142
|
+
"close_agent",
|
|
143
|
+
}:
|
|
144
|
+
color = "\x1b[34m"
|
|
145
|
+
else:
|
|
146
|
+
color = "\x1b[2m"
|
|
147
|
+
return "\x1b[1m" + color + message + "\x1b[0m"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def compact_summary(original: "int", retained: "int", pruned: "int") -> "str":
|
|
151
|
+
text = "compact({0} {1}) -> {2} {3} + [summary]".format(
|
|
152
|
+
original,
|
|
153
|
+
"item" if original == 1 else "items",
|
|
154
|
+
retained,
|
|
155
|
+
"item" if retained == 1 else "items",
|
|
156
|
+
)
|
|
157
|
+
if pruned:
|
|
158
|
+
text += " (dropped {0} old {1})".format(
|
|
159
|
+
pruned,
|
|
160
|
+
"tool response" if pruned == 1 else "tool responses",
|
|
161
|
+
)
|
|
162
|
+
return text
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def agent_status_summary(status: "object") -> "str":
|
|
166
|
+
if isinstance(status, str):
|
|
167
|
+
return status
|
|
168
|
+
if isinstance(status, dict):
|
|
169
|
+
if "completed" in status:
|
|
170
|
+
completed = status.get("completed")
|
|
171
|
+
if completed is None:
|
|
172
|
+
return "completed"
|
|
173
|
+
return f"completed: {truncate_text(str(completed), 48)}"
|
|
174
|
+
if "errored" in status:
|
|
175
|
+
return f"errored: {truncate_text(str(status.get('errored', '')), 48)}"
|
|
176
|
+
return truncate_text(json.dumps(status, ensure_ascii=False, separators=(",", ":")))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def short_id(value: "str", limit: "int" = 8) -> "str":
|
|
180
|
+
compact = value.strip()
|
|
181
|
+
if len(compact) <= limit + 4:
|
|
182
|
+
return compact
|
|
183
|
+
return f"{compact[:limit]}...{compact[-4:]}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def truncate_text(text: "str", limit: "int" = 96) -> "str":
|
|
187
|
+
compact = " ".join(text.split())
|
|
188
|
+
if len(compact) <= limit:
|
|
189
|
+
return compact
|
|
190
|
+
return compact[: limit - 3].rstrip() + "..."
|
pycodex/utils/get_env.py
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import platform
|
|
3
|
-
import re
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from pathlib import Path
|
|
6
3
|
import subprocess
|
|
7
4
|
import typing
|
|
8
|
-
|
|
9
|
-
from
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
10
7
|
|
|
11
8
|
|
|
12
|
-
def get_shell_name() ->
|
|
9
|
+
def get_shell_name() -> "str":
|
|
13
10
|
shell_path = os.environ.get("SHELL")
|
|
14
11
|
if shell_path:
|
|
15
12
|
return Path(shell_path).name or shell_path
|
|
16
13
|
return "bash"
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
def get_timezone_name() ->
|
|
16
|
+
def get_timezone_name() -> "str":
|
|
20
17
|
timezone_env = os.environ.get("TZ")
|
|
21
18
|
if timezone_env:
|
|
22
19
|
return timezone_env
|
|
@@ -33,7 +30,9 @@ def get_timezone_name() -> 'str':
|
|
|
33
30
|
return "Etc/UTC"
|
|
34
31
|
name = str(timezone)
|
|
35
32
|
return name or "Etc/UTC"
|
|
36
|
-
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_sandbox_tag(sandbox_mode: "typing.Union[str, None]") -> "str":
|
|
37
36
|
if sandbox_mode == "danger-full-access":
|
|
38
37
|
return "none"
|
|
39
38
|
if sandbox_mode == "read-only":
|
|
@@ -43,7 +42,9 @@ def get_sandbox_tag(sandbox_mode: 'typing.Union[str, None]') -> 'str':
|
|
|
43
42
|
return "none"
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def get_workspace_turn_metadata(
|
|
45
|
+
def get_workspace_turn_metadata(
|
|
46
|
+
cwd: "typing.Union[str, Path]",
|
|
47
|
+
) -> "typing.Union[typing.Dict[str, object], None]":
|
|
47
48
|
resolved_cwd = Path(cwd).resolve()
|
|
48
49
|
repo_root = _git_output(
|
|
49
50
|
resolved_cwd,
|
|
@@ -52,7 +53,7 @@ def get_workspace_turn_metadata(cwd: 'typing.Union[str, Path]') -> 'typing.Union
|
|
|
52
53
|
if repo_root is None:
|
|
53
54
|
return None
|
|
54
55
|
|
|
55
|
-
workspace:
|
|
56
|
+
workspace: "typing.Dict[str, object]" = {}
|
|
56
57
|
head = _git_output(resolved_cwd, ["rev-parse", "HEAD"])
|
|
57
58
|
if head is not None:
|
|
58
59
|
workspace["latest_git_commit_hash"] = head
|
|
@@ -70,7 +71,7 @@ def get_workspace_turn_metadata(cwd: 'typing.Union[str, Path]') -> 'typing.Union
|
|
|
70
71
|
return {"workspaces": {repo_root: workspace}}
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
def build_user_agent(originator:
|
|
74
|
+
def build_user_agent(originator: "str") -> "str":
|
|
74
75
|
version = get_package_version()
|
|
75
76
|
terminal = get_terminal_user_agent_token()
|
|
76
77
|
os_name, os_version = get_os_info()
|
|
@@ -79,25 +80,15 @@ def build_user_agent(originator: 'str') -> 'str':
|
|
|
79
80
|
return f"{originator}/{version} ({os_name} {os_version}; {arch}) {terminal}{suffix}"
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
def get_package_version() ->
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return detected
|
|
86
|
-
for distribution_name in ("python-codex", "pycodex"):
|
|
87
|
-
try:
|
|
88
|
-
return importlib_metadata.version(distribution_name)
|
|
89
|
-
except importlib_metadata.PackageNotFoundError:
|
|
90
|
-
continue
|
|
91
|
-
local_version = _read_local_package_version()
|
|
92
|
-
if local_version is not None:
|
|
93
|
-
return local_version
|
|
94
|
-
return "0.1.0"
|
|
83
|
+
def get_package_version() -> "str":
|
|
84
|
+
"""Return the upstream alignment version recorded in docs/ALIGNMENT.md."""
|
|
85
|
+
return "0.153.4"
|
|
95
86
|
|
|
96
87
|
|
|
97
|
-
def get_os_info() ->
|
|
88
|
+
def get_os_info() -> "typing.Tuple[str, str]":
|
|
98
89
|
os_release = Path("/etc/os-release")
|
|
99
90
|
if os_release.is_file():
|
|
100
|
-
values:
|
|
91
|
+
values: "typing.Dict[str, str]" = {}
|
|
101
92
|
os_release_text = os_release.read_text(
|
|
102
93
|
encoding="utf-8",
|
|
103
94
|
errors="replace",
|
|
@@ -114,7 +105,7 @@ def get_os_info() -> 'typing.Tuple[str, str]':
|
|
|
114
105
|
return platform.system(), platform.release()
|
|
115
106
|
|
|
116
107
|
|
|
117
|
-
def get_terminal_user_agent_token() ->
|
|
108
|
+
def get_terminal_user_agent_token() -> "str":
|
|
118
109
|
term_program = os.environ.get("TERM_PROGRAM", "")
|
|
119
110
|
if term_program.lower() == "tmux":
|
|
120
111
|
client_termname = _tmux_display_message("#{client_termname}")
|
|
@@ -127,7 +118,7 @@ def get_terminal_user_agent_token() -> 'str':
|
|
|
127
118
|
return "unknown"
|
|
128
119
|
|
|
129
120
|
|
|
130
|
-
def _git_output(cwd:
|
|
121
|
+
def _git_output(cwd: "Path", args: "typing.List[str]") -> "typing.Union[str, None]":
|
|
131
122
|
try:
|
|
132
123
|
completed = subprocess.run(
|
|
133
124
|
["git", *args],
|
|
@@ -143,11 +134,11 @@ def _git_output(cwd: 'Path', args: 'typing.List[str]') -> 'typing.Union[str, Non
|
|
|
143
134
|
return value or None
|
|
144
135
|
|
|
145
136
|
|
|
146
|
-
def _git_remote_urls(cwd:
|
|
137
|
+
def _git_remote_urls(cwd: "Path") -> "typing.Dict[str, str]":
|
|
147
138
|
remote_names = _git_output(cwd, ["remote"])
|
|
148
139
|
if remote_names is None:
|
|
149
140
|
return {}
|
|
150
|
-
remotes:
|
|
141
|
+
remotes: "typing.Dict[str, str]" = {}
|
|
151
142
|
for name in remote_names.splitlines():
|
|
152
143
|
remote_name = name.strip()
|
|
153
144
|
if not remote_name:
|
|
@@ -158,7 +149,7 @@ def _git_remote_urls(cwd: 'Path') -> 'typing.Dict[str, str]':
|
|
|
158
149
|
return remotes
|
|
159
150
|
|
|
160
151
|
|
|
161
|
-
def _git_has_changes(cwd:
|
|
152
|
+
def _git_has_changes(cwd: "Path") -> "typing.Union[bool, None]":
|
|
162
153
|
try:
|
|
163
154
|
completed = subprocess.run(
|
|
164
155
|
["git", "status", "--porcelain"],
|
|
@@ -173,7 +164,7 @@ def _git_has_changes(cwd: 'Path') -> 'typing.Union[bool, None]':
|
|
|
173
164
|
return bool(completed.stdout.strip())
|
|
174
165
|
|
|
175
166
|
|
|
176
|
-
def _user_agent_suffix(originator:
|
|
167
|
+
def _user_agent_suffix(originator: "str", version: "str") -> "str":
|
|
177
168
|
if originator == "codex_exec":
|
|
178
169
|
return f" (codex-exec; {version})"
|
|
179
170
|
if originator == "codex-tui":
|
|
@@ -181,7 +172,7 @@ def _user_agent_suffix(originator: 'str', version: 'str') -> 'str':
|
|
|
181
172
|
return ""
|
|
182
173
|
|
|
183
174
|
|
|
184
|
-
def _normalize_os_version(version:
|
|
175
|
+
def _normalize_os_version(version: "str") -> "str":
|
|
185
176
|
parts = version.split(".")
|
|
186
177
|
if len(parts) == 2 and all(part.isdigit() for part in parts):
|
|
187
178
|
major, minor = parts
|
|
@@ -189,21 +180,7 @@ def _normalize_os_version(version: 'str') -> 'str':
|
|
|
189
180
|
return version
|
|
190
181
|
|
|
191
182
|
|
|
192
|
-
def
|
|
193
|
-
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
194
|
-
if not pyproject_path.is_file():
|
|
195
|
-
return None
|
|
196
|
-
match = re.search(
|
|
197
|
-
r'^\s*version\s*=\s*"([^"]+)"\s*$',
|
|
198
|
-
pyproject_path.read_text(encoding="utf-8"),
|
|
199
|
-
flags=re.MULTILINE,
|
|
200
|
-
)
|
|
201
|
-
if match is None:
|
|
202
|
-
return None
|
|
203
|
-
return match.group(1).strip() or None
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _tmux_display_message(fmt: 'str') -> 'typing.Union[str, None]':
|
|
183
|
+
def _tmux_display_message(fmt: "str") -> "typing.Union[str, None]":
|
|
207
184
|
try:
|
|
208
185
|
output = subprocess.run(
|
|
209
186
|
["tmux", "display-message", "-p", fmt],
|
|
@@ -218,28 +195,8 @@ def _tmux_display_message(fmt: 'str') -> 'typing.Union[str, None]':
|
|
|
218
195
|
return value or None
|
|
219
196
|
|
|
220
197
|
|
|
221
|
-
def _sanitize_header_token(value:
|
|
198
|
+
def _sanitize_header_token(value: "str") -> "str":
|
|
222
199
|
return "".join(
|
|
223
|
-
character
|
|
224
|
-
if (character.isalnum() or character in {"-", "_", ".", "/"})
|
|
225
|
-
else "_"
|
|
200
|
+
character if (character.isalnum() or character in {"-", "_", ".", "/"}) else "_"
|
|
226
201
|
for character in value
|
|
227
202
|
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _detect_upstream_codex_version() -> 'typing.Union[str, None]':
|
|
231
|
-
try:
|
|
232
|
-
output = subprocess.run(
|
|
233
|
-
["codex", "--version"],
|
|
234
|
-
check=True,
|
|
235
|
-
stdout=subprocess.PIPE,
|
|
236
|
-
stderr=subprocess.PIPE,
|
|
237
|
-
universal_newlines=True,
|
|
238
|
-
)
|
|
239
|
-
except (OSError, subprocess.CalledProcessError):
|
|
240
|
-
return None
|
|
241
|
-
|
|
242
|
-
match = re.search(r"\b(\d+\.\d+\.\d+)\b", output.stdout)
|
|
243
|
-
if match is None:
|
|
244
|
-
return None
|
|
245
|
-
return match.group(1)
|