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/compat.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import shlex
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from http.server import ThreadingHTTPServer
|
|
7
|
+
except ImportError: # pragma: no cover - Python 3.6 path
|
|
8
|
+
from http.server import HTTPServer
|
|
9
|
+
from socketserver import ThreadingMixIn
|
|
10
|
+
|
|
11
|
+
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
|
12
|
+
daemon_threads = True
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from importlib import metadata as importlib_metadata
|
|
16
|
+
except ImportError: # pragma: no cover - Python 3.6 path
|
|
17
|
+
import importlib_metadata # type: ignore
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from typing import Literal, Protocol, TypeAlias
|
|
21
|
+
except ImportError: # pragma: no cover - Python 3.6 path
|
|
22
|
+
from typing_extensions import Literal, Protocol # type: ignore
|
|
23
|
+
try:
|
|
24
|
+
from typing_extensions import TypeAlias # type: ignore
|
|
25
|
+
except ImportError: # pragma: no cover - old typing_extensions
|
|
26
|
+
TypeAlias = object
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def patch_asyncio():
|
|
30
|
+
if not hasattr(asyncio, "create_task"):
|
|
31
|
+
asyncio.create_task = asyncio.ensure_future
|
|
32
|
+
|
|
33
|
+
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
|
|
38
|
+
|
|
39
|
+
if not hasattr(asyncio, "to_thread"):
|
|
40
|
+
async def to_thread(func, *args, **kwargs):
|
|
41
|
+
loop = asyncio.get_event_loop()
|
|
42
|
+
call = functools.partial(func, *args, **kwargs)
|
|
43
|
+
return await loop.run_in_executor(None, call)
|
|
44
|
+
|
|
45
|
+
asyncio.to_thread = to_thread
|
|
46
|
+
|
|
47
|
+
if not hasattr(asyncio, "run"):
|
|
48
|
+
def run(main):
|
|
49
|
+
loop = asyncio.new_event_loop()
|
|
50
|
+
try:
|
|
51
|
+
asyncio.set_event_loop(loop)
|
|
52
|
+
return loop.run_until_complete(main)
|
|
53
|
+
finally:
|
|
54
|
+
all_tasks = getattr(asyncio.Task, "all_tasks", None)
|
|
55
|
+
if all_tasks is not None:
|
|
56
|
+
pending = all_tasks(loop=loop)
|
|
57
|
+
else:
|
|
58
|
+
pending = asyncio.all_tasks(loop)
|
|
59
|
+
for task in pending:
|
|
60
|
+
task.cancel()
|
|
61
|
+
if pending:
|
|
62
|
+
loop.run_until_complete(
|
|
63
|
+
asyncio.gather(*pending, return_exceptions=True)
|
|
64
|
+
)
|
|
65
|
+
shutdown_asyncgens = getattr(loop, "shutdown_asyncgens", None)
|
|
66
|
+
if shutdown_asyncgens is not None:
|
|
67
|
+
loop.run_until_complete(shutdown_asyncgens())
|
|
68
|
+
asyncio.set_event_loop(None)
|
|
69
|
+
loop.close()
|
|
70
|
+
|
|
71
|
+
asyncio.run = run
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def shlex_join(parts):
|
|
75
|
+
join = getattr(shlex, "join", None)
|
|
76
|
+
if join is not None:
|
|
77
|
+
return join(parts)
|
|
78
|
+
return " ".join(shlex.quote(part) for part in parts)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def stream_writer_is_closing(writer):
|
|
82
|
+
method = getattr(writer, "is_closing", None)
|
|
83
|
+
if callable(method):
|
|
84
|
+
return method()
|
|
85
|
+
transport = getattr(writer, "transport", None)
|
|
86
|
+
if transport is None:
|
|
87
|
+
return False
|
|
88
|
+
transport_is_closing = getattr(transport, "is_closing", None)
|
|
89
|
+
if callable(transport_is_closing):
|
|
90
|
+
return transport_is_closing()
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_ascii(text):
|
|
95
|
+
try:
|
|
96
|
+
text.encode("ascii")
|
|
97
|
+
except UnicodeEncodeError:
|
|
98
|
+
return False
|
|
99
|
+
return True
|
pycodex/context.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
from dataclasses import dataclass
|
|
4
3
|
from datetime import datetime
|
|
5
4
|
from functools import lru_cache
|
|
6
5
|
import json
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
import typing
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
10
|
import tomllib
|
|
@@ -30,6 +30,7 @@ DEFAULT_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
|
30
30
|
PLAN_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
31
31
|
Path(__file__).resolve().parent / "prompts" / "collaboration_plan.md"
|
|
32
32
|
)
|
|
33
|
+
DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT = 95
|
|
33
34
|
PERMISSIONS_SANDBOX_PROMPTS_PATH = (
|
|
34
35
|
Path(__file__).resolve().parent / "prompts" / "permissions" / "sandbox_mode"
|
|
35
36
|
)
|
|
@@ -66,26 +67,27 @@ SKILLS_GUIDANCE = """- Discovery: The list above is the skills available in this
|
|
|
66
67
|
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."""
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
@dataclass(frozen=True,
|
|
70
|
+
@dataclass(frozen=True, )
|
|
70
71
|
class ContextConfig:
|
|
71
|
-
base_instructions: str
|
|
72
|
-
developer_instructions: str
|
|
73
|
-
user_instructions: str
|
|
74
|
-
codex_home_instructions: str
|
|
75
|
-
model_instructions_file: Path
|
|
76
|
-
codex_home: Path
|
|
77
|
-
project_doc_max_bytes: int
|
|
78
|
-
model: str
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
base_instructions: 'typing.Union[str, None]' = None
|
|
73
|
+
developer_instructions: 'typing.Union[str, None]' = None
|
|
74
|
+
user_instructions: 'typing.Union[str, None]' = None
|
|
75
|
+
codex_home_instructions: 'typing.Union[str, None]' = None
|
|
76
|
+
model_instructions_file: 'typing.Union[Path, None]' = None
|
|
77
|
+
codex_home: 'typing.Union[Path, None]' = None
|
|
78
|
+
project_doc_max_bytes: 'typing.Union[int, None]' = None
|
|
79
|
+
model: 'typing.Union[str, None]' = None
|
|
80
|
+
model_context_window: 'typing.Union[int, None]' = None
|
|
81
|
+
personality: 'typing.Union[str, None]' = None
|
|
82
|
+
approval_policy: 'typing.Union[str, None]' = None
|
|
83
|
+
sandbox_mode: 'typing.Union[str, None]' = None
|
|
82
84
|
|
|
83
85
|
@classmethod
|
|
84
86
|
def from_codex_config(
|
|
85
87
|
cls,
|
|
86
|
-
config_path: str
|
|
87
|
-
profile: str
|
|
88
|
-
) -> ContextConfig:
|
|
88
|
+
config_path: 'typing.Union[str, Path]',
|
|
89
|
+
profile: 'typing.Union[str, None]' = None,
|
|
90
|
+
) -> 'ContextConfig':
|
|
89
91
|
path = Path(config_path)
|
|
90
92
|
data = tomllib.loads(path.read_text())
|
|
91
93
|
selected = dict(data)
|
|
@@ -117,32 +119,33 @@ class ContextConfig:
|
|
|
117
119
|
codex_home=codex_home,
|
|
118
120
|
project_doc_max_bytes=_normalize_int(selected.get("project_doc_max_bytes")),
|
|
119
121
|
model=_normalize_text(selected.get("model")),
|
|
122
|
+
model_context_window=_normalize_int(selected.get("model_context_window")),
|
|
120
123
|
personality=_normalize_text(selected.get("personality")),
|
|
121
124
|
approval_policy=_normalize_text(selected.get("approval_policy")),
|
|
122
125
|
sandbox_mode=_normalize_text(selected.get("sandbox_mode")),
|
|
123
126
|
)
|
|
124
127
|
|
|
125
128
|
|
|
126
|
-
@dataclass(frozen=True,
|
|
129
|
+
@dataclass(frozen=True, )
|
|
127
130
|
class SkillDescriptor:
|
|
128
|
-
name: str
|
|
129
|
-
description: str
|
|
130
|
-
path_to_skill_md: Path
|
|
131
|
-
scope_rank: int
|
|
131
|
+
name: 'str'
|
|
132
|
+
description: 'str'
|
|
133
|
+
path_to_skill_md: 'Path'
|
|
134
|
+
scope_rank: 'int'
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
class ContextManager:
|
|
135
138
|
def __init__(
|
|
136
139
|
self,
|
|
137
|
-
base_instructions_override: str
|
|
138
|
-
config: ContextConfig
|
|
139
|
-
collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
|
|
140
|
-
collaboration_instructions: str
|
|
141
|
-
include_collaboration_instructions: bool = False,
|
|
142
|
-
include_permissions_instructions: bool = True,
|
|
143
|
-
include_skills_instructions: bool = True,
|
|
144
|
-
network_access: str = "enabled",
|
|
145
|
-
) -> None:
|
|
140
|
+
base_instructions_override: 'typing.Union[str, None]' = None,
|
|
141
|
+
config: 'typing.Union[ContextConfig, None]' = None,
|
|
142
|
+
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
143
|
+
collaboration_instructions: 'typing.Union[str, None]' = None,
|
|
144
|
+
include_collaboration_instructions: 'bool' = False,
|
|
145
|
+
include_permissions_instructions: 'bool' = True,
|
|
146
|
+
include_skills_instructions: 'bool' = True,
|
|
147
|
+
network_access: 'str' = "enabled",
|
|
148
|
+
) -> 'None':
|
|
146
149
|
self.cwd = Path.cwd().resolve()
|
|
147
150
|
self._shell = get_shell_name()
|
|
148
151
|
self._current_date = datetime.now().date().isoformat()
|
|
@@ -160,21 +163,21 @@ class ContextManager:
|
|
|
160
163
|
self._include_skills_instructions = include_skills_instructions
|
|
161
164
|
self._network_access = network_access
|
|
162
165
|
self._default_base_instructions = DEFAULT_BASE_INSTRUCTIONS_PATH.read_text()
|
|
163
|
-
self._workspace_metadata_turn_id: str
|
|
164
|
-
self._workspace_metadata_cache: JSONDict
|
|
166
|
+
self._workspace_metadata_turn_id: 'typing.Union[str, None]' = None
|
|
167
|
+
self._workspace_metadata_cache: 'typing.Union[JSONDict, None]' = None
|
|
165
168
|
|
|
166
169
|
@classmethod
|
|
167
170
|
def from_codex_config(
|
|
168
171
|
cls,
|
|
169
|
-
config_path: str
|
|
170
|
-
profile: str
|
|
171
|
-
base_instructions_override: str
|
|
172
|
-
collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
|
|
173
|
-
include_collaboration_instructions: bool = False,
|
|
174
|
-
include_permissions_instructions: bool = True,
|
|
175
|
-
include_skills_instructions: bool = True,
|
|
176
|
-
network_access: str = "enabled",
|
|
177
|
-
) -> ContextManager:
|
|
172
|
+
config_path: 'typing.Union[str, Path]',
|
|
173
|
+
profile: 'typing.Union[str, None]' = None,
|
|
174
|
+
base_instructions_override: 'typing.Union[str, None]' = None,
|
|
175
|
+
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
176
|
+
include_collaboration_instructions: 'bool' = False,
|
|
177
|
+
include_permissions_instructions: 'bool' = True,
|
|
178
|
+
include_skills_instructions: 'bool' = True,
|
|
179
|
+
network_access: 'str' = "enabled",
|
|
180
|
+
) -> 'ContextManager':
|
|
178
181
|
config = ContextConfig.from_codex_config(config_path, profile)
|
|
179
182
|
return cls(
|
|
180
183
|
base_instructions_override=base_instructions_override,
|
|
@@ -187,11 +190,11 @@ class ContextManager:
|
|
|
187
190
|
)
|
|
188
191
|
|
|
189
192
|
@property
|
|
190
|
-
def collaboration_mode(self) -> CollaborationMode:
|
|
193
|
+
def collaboration_mode(self) -> 'CollaborationMode':
|
|
191
194
|
return self._collaboration_mode
|
|
192
195
|
|
|
193
|
-
def get_turn_metadata(self, turn_id: str) -> JSONDict:
|
|
194
|
-
metadata: JSONDict = {"turn_id": turn_id}
|
|
196
|
+
def get_turn_metadata(self, turn_id: 'str') -> 'JSONDict':
|
|
197
|
+
metadata: 'JSONDict' = {"turn_id": turn_id}
|
|
195
198
|
if self._workspace_metadata_turn_id is None:
|
|
196
199
|
self._workspace_metadata_turn_id = turn_id
|
|
197
200
|
self._workspace_metadata_cache = get_workspace_turn_metadata(self.cwd)
|
|
@@ -205,12 +208,12 @@ class ContextManager:
|
|
|
205
208
|
|
|
206
209
|
def build_prompt(
|
|
207
210
|
self,
|
|
208
|
-
history:
|
|
209
|
-
tools:
|
|
210
|
-
parallel_tool_calls: bool,
|
|
211
|
-
turn_id: str
|
|
212
|
-
) -> Prompt:
|
|
213
|
-
input_items:
|
|
211
|
+
history: 'typing.Union[typing.Tuple[ConversationItem, ...], typing.List[ConversationItem]]',
|
|
212
|
+
tools: 'typing.List[ToolSpec]',
|
|
213
|
+
parallel_tool_calls: 'bool',
|
|
214
|
+
turn_id: 'typing.Union[str, None]' = None,
|
|
215
|
+
) -> 'Prompt':
|
|
216
|
+
input_items: 'typing.List[ConversationItem]' = []
|
|
214
217
|
turn_metadata = self.get_turn_metadata(turn_id) if turn_id is not None else None
|
|
215
218
|
|
|
216
219
|
developer_message = self._build_developer_message()
|
|
@@ -228,7 +231,7 @@ class ContextManager:
|
|
|
228
231
|
turn_metadata=turn_metadata,
|
|
229
232
|
)
|
|
230
233
|
|
|
231
|
-
def resolve_base_instructions(self) -> str:
|
|
234
|
+
def resolve_base_instructions(self) -> 'str':
|
|
232
235
|
if self._base_instructions_override is not None:
|
|
233
236
|
return self._base_instructions_override
|
|
234
237
|
if self._config.base_instructions is not None:
|
|
@@ -240,7 +243,27 @@ class ContextManager:
|
|
|
240
243
|
return resolved
|
|
241
244
|
return self._default_base_instructions
|
|
242
245
|
|
|
243
|
-
def
|
|
246
|
+
def resolve_model_context_window(self) -> 'typing.Union[int, None]':
|
|
247
|
+
model_metadata = None
|
|
248
|
+
model_slug = self._config.model
|
|
249
|
+
if model_slug is not None:
|
|
250
|
+
model_metadata = _load_models_by_slug().get(model_slug)
|
|
251
|
+
|
|
252
|
+
context_window = self._config.model_context_window
|
|
253
|
+
if context_window is None and model_metadata is not None:
|
|
254
|
+
context_window = _normalize_int(model_metadata.get("context_window"))
|
|
255
|
+
if context_window is None:
|
|
256
|
+
return None
|
|
257
|
+
effective_percent = None
|
|
258
|
+
if model_metadata is not None:
|
|
259
|
+
effective_percent = _normalize_int(
|
|
260
|
+
model_metadata.get("effective_context_window_percent")
|
|
261
|
+
)
|
|
262
|
+
if effective_percent is None:
|
|
263
|
+
effective_percent = DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT
|
|
264
|
+
return context_window * max(effective_percent, 0) // 100
|
|
265
|
+
|
|
266
|
+
def _resolve_model_instructions(self) -> 'typing.Union[str, None]':
|
|
244
267
|
model_slug = self._config.model
|
|
245
268
|
if model_slug is None:
|
|
246
269
|
return None
|
|
@@ -264,8 +287,8 @@ class ContextManager:
|
|
|
264
287
|
return base_instructions
|
|
265
288
|
return None
|
|
266
289
|
|
|
267
|
-
def _build_developer_message(self) -> ContextMessage
|
|
268
|
-
sections:
|
|
290
|
+
def _build_developer_message(self) -> 'typing.Union[ContextMessage, None]':
|
|
291
|
+
sections: 'typing.List[str]' = []
|
|
269
292
|
if self._include_permissions_instructions:
|
|
270
293
|
permissions = self._build_permissions_instructions()
|
|
271
294
|
if permissions is not None:
|
|
@@ -290,7 +313,7 @@ class ContextManager:
|
|
|
290
313
|
content_items=tuple(_input_text_item(section) for section in sections),
|
|
291
314
|
)
|
|
292
315
|
|
|
293
|
-
def _build_permissions_instructions(self) -> str
|
|
316
|
+
def _build_permissions_instructions(self) -> 'typing.Union[str, None]':
|
|
294
317
|
sandbox_mode = self._config.sandbox_mode or "danger-full-access"
|
|
295
318
|
approval_policy = self._config.approval_policy or "never"
|
|
296
319
|
sandbox_prompt_name = sandbox_mode.replace("-", "_")
|
|
@@ -318,7 +341,7 @@ class ContextManager:
|
|
|
318
341
|
]
|
|
319
342
|
)
|
|
320
343
|
|
|
321
|
-
def _build_skills_instructions(self) -> str
|
|
344
|
+
def _build_skills_instructions(self) -> 'typing.Union[str, None]':
|
|
322
345
|
skills = self._discover_skills()
|
|
323
346
|
if not skills:
|
|
324
347
|
return None
|
|
@@ -338,15 +361,15 @@ class ContextManager:
|
|
|
338
361
|
body = "\n".join(lines)
|
|
339
362
|
return f"{SKILLS_OPEN_TAG}\n{body}\n{SKILLS_CLOSE_TAG}"
|
|
340
363
|
|
|
341
|
-
def _discover_skills(self) ->
|
|
364
|
+
def _discover_skills(self) -> 'typing.List[SkillDescriptor]':
|
|
342
365
|
codex_home = self._config.codex_home
|
|
343
366
|
if codex_home is None:
|
|
344
367
|
return []
|
|
345
368
|
|
|
346
369
|
user_root = codex_home / "skills"
|
|
347
370
|
system_root = user_root / ".system"
|
|
348
|
-
discovered:
|
|
349
|
-
seen:
|
|
371
|
+
discovered: 'typing.List[SkillDescriptor]' = []
|
|
372
|
+
seen: 'typing.Set[Path]' = set()
|
|
350
373
|
|
|
351
374
|
user_paths = _discover_skill_files(user_root, excluded_root=system_root)
|
|
352
375
|
system_paths = _discover_skill_files(system_root)
|
|
@@ -366,8 +389,8 @@ class ContextManager:
|
|
|
366
389
|
key=lambda skill: (skill.scope_rank, skill.name, skill.path_to_skill_md),
|
|
367
390
|
)
|
|
368
391
|
|
|
369
|
-
def _build_contextual_user_messages(self) ->
|
|
370
|
-
sections:
|
|
392
|
+
def _build_contextual_user_messages(self) -> 'typing.List[ContextMessage]':
|
|
393
|
+
sections: 'typing.List[str]' = []
|
|
371
394
|
user_instructions = self._merged_user_instructions()
|
|
372
395
|
if user_instructions is not None:
|
|
373
396
|
sections.append(
|
|
@@ -386,8 +409,8 @@ class ContextManager:
|
|
|
386
409
|
)
|
|
387
410
|
]
|
|
388
411
|
|
|
389
|
-
def _merged_user_instructions(self) -> str
|
|
390
|
-
parts:
|
|
412
|
+
def _merged_user_instructions(self) -> 'typing.Union[str, None]':
|
|
413
|
+
parts: 'typing.List[str]' = []
|
|
391
414
|
if self._config.user_instructions is not None:
|
|
392
415
|
parts.append(self._config.user_instructions)
|
|
393
416
|
if self._config.codex_home_instructions is not None:
|
|
@@ -402,8 +425,8 @@ class ContextManager:
|
|
|
402
425
|
|
|
403
426
|
return "\n\n".join(parts) or None
|
|
404
427
|
|
|
405
|
-
def _read_project_docs(self) -> str
|
|
406
|
-
docs:
|
|
428
|
+
def _read_project_docs(self) -> 'typing.Union[str, None]':
|
|
429
|
+
docs: 'typing.List[str]' = []
|
|
407
430
|
remaining = self._config.project_doc_max_bytes
|
|
408
431
|
for path in self._discover_project_doc_paths():
|
|
409
432
|
text = path.read_text()
|
|
@@ -421,9 +444,9 @@ class ContextManager:
|
|
|
421
444
|
return None
|
|
422
445
|
return "\n\n".join(docs)
|
|
423
446
|
|
|
424
|
-
def _discover_project_doc_paths(self) ->
|
|
425
|
-
seen:
|
|
426
|
-
discovered:
|
|
447
|
+
def _discover_project_doc_paths(self) -> 'typing.List[Path]':
|
|
448
|
+
seen: 'typing.Set[Path]' = set()
|
|
449
|
+
discovered: 'typing.List[Path]' = []
|
|
427
450
|
|
|
428
451
|
search_dirs = self._project_search_dirs()
|
|
429
452
|
for directory in search_dirs:
|
|
@@ -435,9 +458,9 @@ class ContextManager:
|
|
|
435
458
|
break
|
|
436
459
|
return discovered
|
|
437
460
|
|
|
438
|
-
def _project_search_dirs(self) ->
|
|
461
|
+
def _project_search_dirs(self) -> 'typing.List[Path]':
|
|
439
462
|
project_root = self._find_project_root()
|
|
440
|
-
directories:
|
|
463
|
+
directories: 'typing.List[Path]' = []
|
|
441
464
|
current = self.cwd
|
|
442
465
|
chain = [current]
|
|
443
466
|
while current != project_root and current.parent != current:
|
|
@@ -447,13 +470,13 @@ class ContextManager:
|
|
|
447
470
|
directories.extend(chain)
|
|
448
471
|
return directories
|
|
449
472
|
|
|
450
|
-
def _find_project_root(self) -> Path:
|
|
473
|
+
def _find_project_root(self) -> 'Path':
|
|
451
474
|
for ancestor in [self.cwd, *self.cwd.parents]:
|
|
452
475
|
if (ancestor / ".git").exists():
|
|
453
476
|
return ancestor
|
|
454
477
|
return self.cwd
|
|
455
478
|
|
|
456
|
-
def _serialize_environment_context(self) -> str:
|
|
479
|
+
def _serialize_environment_context(self) -> 'str':
|
|
457
480
|
lines = [
|
|
458
481
|
"<environment_context>",
|
|
459
482
|
f" <cwd>{self.cwd}</cwd>",
|
|
@@ -465,30 +488,30 @@ class ContextManager:
|
|
|
465
488
|
return "\n".join(lines)
|
|
466
489
|
|
|
467
490
|
|
|
468
|
-
def _input_text_item(text: str) -> JSONDict:
|
|
491
|
+
def _input_text_item(text: 'str') -> 'JSONDict':
|
|
469
492
|
return {"type": "input_text", "text": text}
|
|
470
493
|
|
|
471
494
|
|
|
472
|
-
def _normalize_text(value) -> str
|
|
495
|
+
def _normalize_text(value) -> 'typing.Union[str, None]':
|
|
473
496
|
if value is None:
|
|
474
497
|
return None
|
|
475
498
|
text = str(value).strip()
|
|
476
499
|
return text or None
|
|
477
500
|
|
|
478
501
|
|
|
479
|
-
def _normalize_int(value) -> int
|
|
502
|
+
def _normalize_int(value) -> 'typing.Union[int, None]':
|
|
480
503
|
if value is None:
|
|
481
504
|
return None
|
|
482
505
|
return int(value)
|
|
483
506
|
|
|
484
507
|
|
|
485
|
-
def _default_collaboration_instructions(mode: CollaborationMode) -> str:
|
|
508
|
+
def _default_collaboration_instructions(mode: 'CollaborationMode') -> 'str':
|
|
486
509
|
if mode == "plan":
|
|
487
510
|
return PLAN_COLLABORATION_INSTRUCTIONS_PATH.read_text()
|
|
488
511
|
return DEFAULT_COLLABORATION_INSTRUCTIONS_PATH.read_text()
|
|
489
512
|
|
|
490
513
|
|
|
491
|
-
def _read_first_instruction_file(base: Path) -> str
|
|
514
|
+
def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
|
|
492
515
|
for candidate_name in (LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME):
|
|
493
516
|
candidate = base / candidate_name
|
|
494
517
|
try:
|
|
@@ -502,10 +525,10 @@ def _read_first_instruction_file(base: Path) -> str | None:
|
|
|
502
525
|
|
|
503
526
|
|
|
504
527
|
@lru_cache(maxsize=1)
|
|
505
|
-
def _load_models_by_slug() ->
|
|
528
|
+
def _load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
|
|
506
529
|
payload = json.loads(DEFAULT_MODELS_PATH.read_text())
|
|
507
530
|
models = payload.get("models", [])
|
|
508
|
-
by_slug:
|
|
531
|
+
by_slug: 'typing.Dict[str, JSONDict]' = {}
|
|
509
532
|
for model in models:
|
|
510
533
|
slug = model.get("slug")
|
|
511
534
|
if isinstance(slug, str):
|
|
@@ -513,7 +536,7 @@ def _load_models_by_slug() -> dict[str, JSONDict]:
|
|
|
513
536
|
return by_slug
|
|
514
537
|
|
|
515
538
|
|
|
516
|
-
def _resolve_personality_message(variables, personality: str
|
|
539
|
+
def _resolve_personality_message(variables, personality: 'typing.Union[str, None]') -> 'str':
|
|
517
540
|
if not isinstance(variables, dict):
|
|
518
541
|
return ""
|
|
519
542
|
normalized = (personality or "").strip().lower()
|
|
@@ -532,13 +555,13 @@ def _resolve_personality_message(variables, personality: str | None) -> str:
|
|
|
532
555
|
|
|
533
556
|
|
|
534
557
|
def _discover_skill_files(
|
|
535
|
-
root: Path,
|
|
536
|
-
excluded_root: Path
|
|
537
|
-
) ->
|
|
558
|
+
root: 'Path',
|
|
559
|
+
excluded_root: 'typing.Union[Path, None]' = None,
|
|
560
|
+
) -> 'typing.List[Path]':
|
|
538
561
|
if not root.exists() or not root.is_dir():
|
|
539
562
|
return []
|
|
540
563
|
excluded = excluded_root.resolve() if excluded_root is not None and excluded_root.exists() else None
|
|
541
|
-
paths:
|
|
564
|
+
paths: 'typing.List[Path]' = []
|
|
542
565
|
for path in root.glob("**/SKILL.md"):
|
|
543
566
|
resolved = path.resolve()
|
|
544
567
|
if excluded is not None and (resolved == excluded or excluded in resolved.parents):
|
|
@@ -547,7 +570,7 @@ def _discover_skill_files(
|
|
|
547
570
|
return sorted(paths)
|
|
548
571
|
|
|
549
572
|
|
|
550
|
-
def _parse_skill_descriptor(path: Path, scope_rank: int) -> SkillDescriptor
|
|
573
|
+
def _parse_skill_descriptor(path: 'Path', scope_rank: 'int') -> 'typing.Union[SkillDescriptor, None]':
|
|
551
574
|
text = path.read_text()
|
|
552
575
|
if not text.startswith("---\n"):
|
|
553
576
|
return None
|
|
@@ -556,7 +579,7 @@ def _parse_skill_descriptor(path: Path, scope_rank: int) -> SkillDescriptor | No
|
|
|
556
579
|
if end_index == -1:
|
|
557
580
|
return None
|
|
558
581
|
frontmatter = text[4:end_index]
|
|
559
|
-
fields:
|
|
582
|
+
fields: 'typing.Dict[str, str]' = {}
|
|
560
583
|
for line in frontmatter.splitlines():
|
|
561
584
|
if ":" not in line:
|
|
562
585
|
continue
|
|
@@ -574,7 +597,7 @@ def _parse_skill_descriptor(path: Path, scope_rank: int) -> SkillDescriptor | No
|
|
|
574
597
|
)
|
|
575
598
|
|
|
576
599
|
|
|
577
|
-
def _strip_yaml_string(value: str) -> str:
|
|
600
|
+
def _strip_yaml_string(value: 'str') -> 'str':
|
|
578
601
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
579
602
|
return value[1:-1]
|
|
580
603
|
return value
|