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/context.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
from dataclasses import dataclass
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass, replace
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
import
|
|
5
|
+
from xml.sax.saxutils import escape
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
6
8
|
|
|
7
9
|
try:
|
|
8
10
|
import tomllib
|
|
9
11
|
except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
|
|
10
12
|
import tomli as tomllib
|
|
11
13
|
|
|
12
|
-
from .
|
|
13
|
-
from .model_metadata import load_models_by_slug
|
|
14
|
+
from .model_metadata import model_metadata
|
|
14
15
|
from .protocol import ContextMessage, ConversationItem, JSONDict, Prompt, ToolSpec
|
|
15
16
|
from .utils.get_env import (
|
|
16
17
|
get_sandbox_tag,
|
|
@@ -22,12 +23,6 @@ from .utils.get_env import (
|
|
|
22
23
|
DEFAULT_BASE_INSTRUCTIONS_PATH = (
|
|
23
24
|
Path(__file__).resolve().parent / "prompts" / "default_base_instructions.md"
|
|
24
25
|
)
|
|
25
|
-
DEFAULT_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
26
|
-
Path(__file__).resolve().parent / "prompts" / "collaboration_default.md"
|
|
27
|
-
)
|
|
28
|
-
PLAN_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
29
|
-
Path(__file__).resolve().parent / "prompts" / "collaboration_plan.md"
|
|
30
|
-
)
|
|
31
26
|
DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT = 95
|
|
32
27
|
PERMISSIONS_SANDBOX_PROMPTS_PATH = (
|
|
33
28
|
Path(__file__).resolve().parent / "prompts" / "permissions" / "sandbox_mode"
|
|
@@ -43,50 +38,56 @@ PERMISSIONS_OPEN_TAG = "<permissions instructions>"
|
|
|
43
38
|
PERMISSIONS_CLOSE_TAG = "</permissions instructions>"
|
|
44
39
|
SKILLS_OPEN_TAG = "<skills_instructions>"
|
|
45
40
|
SKILLS_CLOSE_TAG = "</skills_instructions>"
|
|
46
|
-
COLLABORATION_MODE_OPEN_TAG = "<collaboration_mode>"
|
|
47
|
-
COLLABORATION_MODE_CLOSE_TAG = "</collaboration_mode>"
|
|
48
41
|
PERSONALITY_PLACEHOLDER = "{{ personality }}"
|
|
49
|
-
SKILLS_GUIDANCE = """- Discovery: The list above is the skills available in this session (name + description +
|
|
42
|
+
SKILLS_GUIDANCE = """- Discovery: The list above is the skills available in this session (name + description + short path). Skill bodies live on disk at the listed paths after expanding the matching alias from `### Skill roots`.
|
|
50
43
|
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
|
51
44
|
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
|
52
45
|
- How to use a skill (progressive disclosure):
|
|
53
|
-
1) After deciding to use a skill, open its `SKILL.md
|
|
54
|
-
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the
|
|
55
|
-
3) If `SKILL.md` points to extra folders such as `references/`,
|
|
46
|
+
1) After deciding to use a skill, the main agent must expand the listed short `path` with the matching alias from `### Skill roots`, then open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
|
|
47
|
+
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the directory containing that expanded `SKILL.md` first, and only consider other paths if needed.
|
|
48
|
+
3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
|
|
56
49
|
4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
|
|
57
50
|
5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
|
|
58
51
|
- Coordination and sequencing:
|
|
59
52
|
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
|
60
53
|
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
|
61
54
|
- Context hygiene:
|
|
62
|
-
-
|
|
55
|
+
- Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
|
|
63
56
|
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
|
64
57
|
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
|
65
58
|
- 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."""
|
|
66
59
|
|
|
67
60
|
|
|
68
|
-
@dataclass(
|
|
61
|
+
@dataclass(
|
|
62
|
+
frozen=True,
|
|
63
|
+
)
|
|
69
64
|
class ContextConfig:
|
|
70
|
-
base_instructions:
|
|
71
|
-
developer_instructions:
|
|
72
|
-
user_instructions:
|
|
73
|
-
codex_home_instructions:
|
|
74
|
-
model_instructions_file:
|
|
75
|
-
codex_home:
|
|
76
|
-
project_doc_max_bytes:
|
|
77
|
-
model:
|
|
78
|
-
model_context_window:
|
|
79
|
-
model_auto_compact_token_limit:
|
|
80
|
-
personality:
|
|
81
|
-
approval_policy:
|
|
82
|
-
sandbox_mode:
|
|
65
|
+
base_instructions: "typing.Union[str, None]" = None
|
|
66
|
+
developer_instructions: "typing.Union[str, None]" = None
|
|
67
|
+
user_instructions: "typing.Union[str, None]" = None
|
|
68
|
+
codex_home_instructions: "typing.Union[str, None]" = None
|
|
69
|
+
model_instructions_file: "typing.Union[Path, None]" = None
|
|
70
|
+
codex_home: "typing.Union[Path, None]" = None
|
|
71
|
+
project_doc_max_bytes: "typing.Union[int, None]" = None
|
|
72
|
+
model: "typing.Union[str, None]" = None
|
|
73
|
+
model_context_window: "typing.Union[int, None]" = None
|
|
74
|
+
model_auto_compact_token_limit: "typing.Union[int, None]" = None
|
|
75
|
+
personality: "typing.Union[str, None]" = None
|
|
76
|
+
approval_policy: "typing.Union[str, None]" = None
|
|
77
|
+
sandbox_mode: "typing.Union[str, None]" = None
|
|
78
|
+
base_instructions_override: "typing.Union[str, None]" = None
|
|
79
|
+
include_permissions_instructions: "bool" = True
|
|
80
|
+
include_skills_instructions: "bool" = True
|
|
81
|
+
network_access: "str" = "enabled"
|
|
82
|
+
extra_contextual_user_messages: "typing.Tuple[str, ...]" = ()
|
|
83
|
+
cwd: "typing.Union[str, Path, None]" = None
|
|
83
84
|
|
|
84
85
|
@classmethod
|
|
85
86
|
def from_codex_config(
|
|
86
87
|
cls,
|
|
87
|
-
config_path:
|
|
88
|
-
profile:
|
|
89
|
-
) ->
|
|
88
|
+
config_path: "typing.Union[str, Path]",
|
|
89
|
+
profile: "typing.Union[str, None]" = None,
|
|
90
|
+
) -> "ContextConfig":
|
|
90
91
|
path = Path(config_path)
|
|
91
92
|
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
92
93
|
selected = dict(data)
|
|
@@ -128,91 +129,46 @@ class ContextConfig:
|
|
|
128
129
|
)
|
|
129
130
|
|
|
130
131
|
|
|
131
|
-
@dataclass(
|
|
132
|
+
@dataclass(
|
|
133
|
+
frozen=True,
|
|
134
|
+
)
|
|
132
135
|
class SkillDescriptor:
|
|
133
|
-
name:
|
|
134
|
-
description:
|
|
135
|
-
path_to_skill_md:
|
|
136
|
-
scope_rank:
|
|
136
|
+
name: "str"
|
|
137
|
+
description: "str"
|
|
138
|
+
path_to_skill_md: "Path"
|
|
139
|
+
scope_rank: "int"
|
|
140
|
+
root: "Path"
|
|
137
141
|
|
|
138
142
|
|
|
139
143
|
class ContextManager:
|
|
140
|
-
def __init__(
|
|
141
|
-
self
|
|
142
|
-
base_instructions_override: 'typing.Union[str, None]' = None,
|
|
143
|
-
config: 'typing.Union[ContextConfig, None]' = None,
|
|
144
|
-
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
145
|
-
collaboration_instructions: 'typing.Union[str, None]' = None,
|
|
146
|
-
include_collaboration_instructions: 'bool' = False,
|
|
147
|
-
include_permissions_instructions: 'bool' = True,
|
|
148
|
-
include_skills_instructions: 'bool' = True,
|
|
149
|
-
network_access: 'str' = "enabled",
|
|
150
|
-
extra_contextual_user_messages: 'typing.Iterable[str]' = (),
|
|
151
|
-
cwd: 'typing.Union[str, Path, None]' = None,
|
|
152
|
-
) -> 'None':
|
|
153
|
-
self.cwd = Path(cwd or Path.cwd()).resolve()
|
|
144
|
+
def __init__(self, config: "ContextConfig") -> "None":
|
|
145
|
+
self.cwd = Path(config.cwd or Path.cwd()).resolve()
|
|
154
146
|
self._shell = get_shell_name()
|
|
155
147
|
self._current_date = datetime.now().date().isoformat()
|
|
156
148
|
self._timezone_name = get_timezone_name()
|
|
157
|
-
self._base_instructions_override = _normalize_text(
|
|
158
|
-
|
|
159
|
-
self._collaboration_mode = collaboration_mode
|
|
160
|
-
self._collaboration_instructions = (
|
|
161
|
-
collaboration_instructions
|
|
162
|
-
if collaboration_instructions is not None
|
|
163
|
-
else _default_collaboration_instructions(collaboration_mode)
|
|
149
|
+
self._base_instructions_override = _normalize_text(
|
|
150
|
+
config.base_instructions_override
|
|
164
151
|
)
|
|
165
|
-
self.
|
|
166
|
-
self._include_permissions_instructions = include_permissions_instructions
|
|
167
|
-
self._include_skills_instructions = include_skills_instructions
|
|
168
|
-
self._network_access = network_access
|
|
152
|
+
self._config = config
|
|
153
|
+
self._include_permissions_instructions = config.include_permissions_instructions
|
|
154
|
+
self._include_skills_instructions = config.include_skills_instructions
|
|
155
|
+
self._network_access = config.network_access
|
|
169
156
|
self._extra_contextual_user_messages = tuple(
|
|
170
157
|
text
|
|
171
158
|
for text in (
|
|
172
159
|
_normalize_text(message)
|
|
173
|
-
for message in extra_contextual_user_messages
|
|
160
|
+
for message in config.extra_contextual_user_messages
|
|
174
161
|
)
|
|
175
162
|
if text is not None
|
|
176
163
|
)
|
|
177
164
|
self._default_base_instructions = DEFAULT_BASE_INSTRUCTIONS_PATH.read_text(
|
|
178
165
|
encoding="utf-8"
|
|
179
166
|
)
|
|
180
|
-
self._workspace_metadata_turn_id:
|
|
181
|
-
self._workspace_metadata_cache:
|
|
167
|
+
self._workspace_metadata_turn_id: "typing.Union[str, None]" = None
|
|
168
|
+
self._workspace_metadata_cache: "typing.Union[JSONDict, None]" = None
|
|
182
169
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
cls,
|
|
186
|
-
config_path: 'typing.Union[str, Path]',
|
|
187
|
-
profile: 'typing.Union[str, None]' = None,
|
|
188
|
-
base_instructions_override: 'typing.Union[str, None]' = None,
|
|
189
|
-
collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
|
|
190
|
-
include_collaboration_instructions: 'bool' = False,
|
|
191
|
-
include_permissions_instructions: 'bool' = True,
|
|
192
|
-
include_skills_instructions: 'bool' = True,
|
|
193
|
-
network_access: 'str' = "enabled",
|
|
194
|
-
extra_contextual_user_messages: 'typing.Iterable[str]' = (),
|
|
195
|
-
cwd: 'typing.Union[str, Path, None]' = None,
|
|
196
|
-
) -> 'ContextManager':
|
|
197
|
-
config = ContextConfig.from_codex_config(config_path, profile)
|
|
198
|
-
return cls(
|
|
199
|
-
base_instructions_override=base_instructions_override,
|
|
200
|
-
config=config,
|
|
201
|
-
collaboration_mode=collaboration_mode,
|
|
202
|
-
include_collaboration_instructions=include_collaboration_instructions,
|
|
203
|
-
include_permissions_instructions=include_permissions_instructions,
|
|
204
|
-
include_skills_instructions=include_skills_instructions,
|
|
205
|
-
network_access=network_access,
|
|
206
|
-
extra_contextual_user_messages=extra_contextual_user_messages,
|
|
207
|
-
cwd=cwd,
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
@property
|
|
211
|
-
def collaboration_mode(self) -> 'CollaborationMode':
|
|
212
|
-
return self._collaboration_mode
|
|
213
|
-
|
|
214
|
-
def get_turn_metadata(self, turn_id: 'str') -> 'JSONDict':
|
|
215
|
-
metadata: 'JSONDict' = {"turn_id": turn_id}
|
|
170
|
+
def get_turn_metadata(self, turn_id: "str") -> "JSONDict":
|
|
171
|
+
metadata: "JSONDict" = {"turn_id": turn_id}
|
|
216
172
|
if self._workspace_metadata_turn_id is None:
|
|
217
173
|
self._workspace_metadata_turn_id = turn_id
|
|
218
174
|
self._workspace_metadata_cache = get_workspace_turn_metadata(self.cwd)
|
|
@@ -226,12 +182,12 @@ class ContextManager:
|
|
|
226
182
|
|
|
227
183
|
def build_prompt(
|
|
228
184
|
self,
|
|
229
|
-
history:
|
|
230
|
-
tools:
|
|
231
|
-
parallel_tool_calls:
|
|
232
|
-
turn_id:
|
|
233
|
-
) ->
|
|
234
|
-
input_items:
|
|
185
|
+
history: "typing.Union[typing.Tuple[ConversationItem, ...], typing.List[ConversationItem]]",
|
|
186
|
+
tools: "typing.List[ToolSpec]",
|
|
187
|
+
parallel_tool_calls: "bool",
|
|
188
|
+
turn_id: "typing.Union[str, None]" = None,
|
|
189
|
+
) -> "Prompt":
|
|
190
|
+
input_items: "typing.List[ConversationItem]" = []
|
|
235
191
|
turn_metadata = self.get_turn_metadata(turn_id) if turn_id is not None else None
|
|
236
192
|
|
|
237
193
|
developer_message = self._build_developer_message()
|
|
@@ -249,7 +205,7 @@ class ContextManager:
|
|
|
249
205
|
turn_metadata=turn_metadata,
|
|
250
206
|
)
|
|
251
207
|
|
|
252
|
-
def resolve_base_instructions(self) ->
|
|
208
|
+
def resolve_base_instructions(self) -> "str":
|
|
253
209
|
if self._base_instructions_override is not None:
|
|
254
210
|
return self._base_instructions_override
|
|
255
211
|
if self._config.base_instructions is not None:
|
|
@@ -264,47 +220,51 @@ class ContextManager:
|
|
|
264
220
|
return resolved
|
|
265
221
|
return self._default_base_instructions
|
|
266
222
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
model_slug = self._config.model
|
|
270
|
-
if model_slug is not None:
|
|
271
|
-
model_metadata = load_models_by_slug().get(model_slug)
|
|
272
|
-
|
|
223
|
+
def resolve_model_max_context_window(self) -> "typing.Union[int, None]":
|
|
224
|
+
metadata = model_metadata(self._config.model)
|
|
273
225
|
context_window = self._config.model_context_window
|
|
274
|
-
if context_window is None and
|
|
275
|
-
context_window = _normalize_int(
|
|
226
|
+
if context_window is None and metadata is not None:
|
|
227
|
+
context_window = _normalize_int(metadata.get("context_window"))
|
|
228
|
+
return context_window
|
|
229
|
+
|
|
230
|
+
def resolve_model_context_window(self) -> "typing.Union[int, None]":
|
|
231
|
+
context_window = self.resolve_model_max_context_window()
|
|
276
232
|
if context_window is None:
|
|
277
233
|
return None
|
|
234
|
+
metadata = model_metadata(self._config.model)
|
|
278
235
|
effective_percent = None
|
|
279
|
-
if
|
|
236
|
+
if metadata is not None:
|
|
280
237
|
effective_percent = _normalize_int(
|
|
281
|
-
|
|
238
|
+
metadata.get("effective_context_window_percent")
|
|
282
239
|
)
|
|
283
240
|
if effective_percent is None:
|
|
284
241
|
effective_percent = DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT
|
|
285
242
|
return context_window * max(effective_percent, 0) // 100
|
|
286
243
|
|
|
287
|
-
def
|
|
244
|
+
def set_model(self, model: "str") -> "None":
|
|
245
|
+
self._config = replace(self._config, model=model)
|
|
246
|
+
|
|
247
|
+
def resolve_auto_compact_token_limit(self) -> "typing.Union[int, None]":
|
|
288
248
|
if self._config.model_auto_compact_token_limit is not None:
|
|
289
249
|
return self._config.model_auto_compact_token_limit
|
|
290
250
|
|
|
291
251
|
model_slug = self._config.model
|
|
292
252
|
if model_slug is None:
|
|
293
253
|
return None
|
|
294
|
-
|
|
295
|
-
if
|
|
254
|
+
metadata = model_metadata(model_slug)
|
|
255
|
+
if metadata is None:
|
|
296
256
|
return None
|
|
297
|
-
return _normalize_int(
|
|
257
|
+
return _normalize_int(metadata.get("auto_compact_token_limit"))
|
|
298
258
|
|
|
299
|
-
def _resolve_model_instructions(self) ->
|
|
259
|
+
def _resolve_model_instructions(self) -> "typing.Union[str, None]":
|
|
300
260
|
model_slug = self._config.model
|
|
301
261
|
if model_slug is None:
|
|
302
262
|
return None
|
|
303
|
-
|
|
304
|
-
if
|
|
263
|
+
metadata = model_metadata(model_slug)
|
|
264
|
+
if metadata is None:
|
|
305
265
|
return None
|
|
306
266
|
|
|
307
|
-
model_messages =
|
|
267
|
+
model_messages = metadata.get("model_messages")
|
|
308
268
|
if isinstance(model_messages, dict):
|
|
309
269
|
template = model_messages.get("instructions_template")
|
|
310
270
|
variables = model_messages.get("instructions_variables")
|
|
@@ -315,30 +275,23 @@ class ContextManager:
|
|
|
315
275
|
)
|
|
316
276
|
return template.replace(PERSONALITY_PLACEHOLDER, personality_message)
|
|
317
277
|
|
|
318
|
-
base_instructions =
|
|
278
|
+
base_instructions = metadata.get("base_instructions")
|
|
319
279
|
if isinstance(base_instructions, str):
|
|
320
280
|
return base_instructions
|
|
321
281
|
return None
|
|
322
282
|
|
|
323
|
-
def _build_developer_message(self) ->
|
|
324
|
-
sections:
|
|
325
|
-
if self._include_permissions_instructions:
|
|
326
|
-
permissions = self._build_permissions_instructions()
|
|
327
|
-
if permissions is not None:
|
|
328
|
-
sections.append(permissions)
|
|
283
|
+
def _build_developer_message(self) -> "typing.Union[ContextMessage, None]":
|
|
284
|
+
sections: "typing.List[str]" = []
|
|
329
285
|
if self._config.developer_instructions is not None:
|
|
330
286
|
sections.append(self._config.developer_instructions)
|
|
331
|
-
if self._include_collaboration_instructions:
|
|
332
|
-
collaboration = self._collaboration_instructions.strip()
|
|
333
|
-
if collaboration:
|
|
334
|
-
sections.append(
|
|
335
|
-
f"{COLLABORATION_MODE_OPEN_TAG}{collaboration}"
|
|
336
|
-
f"\n{COLLABORATION_MODE_CLOSE_TAG}"
|
|
337
|
-
)
|
|
338
287
|
if self._include_skills_instructions:
|
|
339
288
|
skills = self._build_skills_instructions()
|
|
340
289
|
if skills is not None:
|
|
341
290
|
sections.append(skills)
|
|
291
|
+
if self._include_permissions_instructions:
|
|
292
|
+
permissions = self._build_permissions_instructions()
|
|
293
|
+
if permissions is not None:
|
|
294
|
+
sections.append(permissions)
|
|
342
295
|
if not sections:
|
|
343
296
|
return None
|
|
344
297
|
return ContextMessage(
|
|
@@ -346,7 +299,7 @@ class ContextManager:
|
|
|
346
299
|
content_items=tuple(_input_text_item(section) for section in sections),
|
|
347
300
|
)
|
|
348
301
|
|
|
349
|
-
def _build_permissions_instructions(self) ->
|
|
302
|
+
def _build_permissions_instructions(self) -> "typing.Union[str, None]":
|
|
350
303
|
sandbox_mode = self._config.sandbox_mode or "danger-full-access"
|
|
351
304
|
approval_policy = self._config.approval_policy or "never"
|
|
352
305
|
sandbox_prompt_name = sandbox_mode.replace("-", "_")
|
|
@@ -354,15 +307,16 @@ class ContextManager:
|
|
|
354
307
|
PERMISSIONS_SANDBOX_PROMPTS_PATH / f"{sandbox_prompt_name}.md"
|
|
355
308
|
)
|
|
356
309
|
approval_prompt_path = (
|
|
357
|
-
PERMISSIONS_APPROVAL_PROMPTS_PATH
|
|
310
|
+
PERMISSIONS_APPROVAL_PROMPTS_PATH
|
|
311
|
+
/ f"{approval_policy.replace('-', '_')}.md"
|
|
358
312
|
)
|
|
359
313
|
if not sandbox_prompt_path.exists() or not approval_prompt_path.exists():
|
|
360
314
|
return None
|
|
361
315
|
|
|
362
316
|
sandbox_text = (
|
|
363
|
-
sandbox_prompt_path.read_text(encoding="utf-8")
|
|
364
|
-
|
|
365
|
-
)
|
|
317
|
+
sandbox_prompt_path.read_text(encoding="utf-8")
|
|
318
|
+
.strip()
|
|
319
|
+
.replace("{network_access}", self._network_access)
|
|
366
320
|
)
|
|
367
321
|
approval_text = approval_prompt_path.read_text(encoding="utf-8").strip()
|
|
368
322
|
return "\n".join(
|
|
@@ -374,56 +328,68 @@ class ContextManager:
|
|
|
374
328
|
]
|
|
375
329
|
)
|
|
376
330
|
|
|
377
|
-
def _build_skills_instructions(self) ->
|
|
331
|
+
def _build_skills_instructions(self) -> "typing.Union[str, None]":
|
|
378
332
|
skills = self._discover_skills()
|
|
379
333
|
if not skills:
|
|
380
334
|
return None
|
|
381
335
|
|
|
382
336
|
lines = [
|
|
383
337
|
"## Skills",
|
|
384
|
-
"A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and
|
|
385
|
-
"###
|
|
338
|
+
"A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table.",
|
|
339
|
+
"### Skill roots",
|
|
386
340
|
]
|
|
341
|
+
roots = {}
|
|
387
342
|
for skill in skills:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
343
|
+
if skill.root not in roots:
|
|
344
|
+
alias = "r{0}".format(len(roots))
|
|
345
|
+
roots[skill.root] = alias
|
|
346
|
+
lines.append("- `{0}` = `{1}`".format(alias, skill.root.as_posix()))
|
|
347
|
+
lines.append("### Available skills")
|
|
348
|
+
for skill in sorted(
|
|
349
|
+
skills, key=lambda item: (item.scope_rank, item.name, item.path_to_skill_md)
|
|
350
|
+
):
|
|
351
|
+
path_str = "{0}/{1}".format(
|
|
352
|
+
roots[skill.root],
|
|
353
|
+
skill.path_to_skill_md.relative_to(skill.root).as_posix(),
|
|
391
354
|
)
|
|
392
|
-
|
|
393
|
-
|
|
355
|
+
lines.append(f"- {skill.name}: {skill.description} (file: {path_str})")
|
|
356
|
+
metadata = model_metadata(self._config.model)
|
|
357
|
+
if metadata is None or metadata.get("include_skills_usage_instructions", True):
|
|
358
|
+
lines.append("### How to use skills")
|
|
359
|
+
lines.extend(SKILLS_GUIDANCE.splitlines())
|
|
394
360
|
body = "\n".join(lines)
|
|
395
361
|
return f"{SKILLS_OPEN_TAG}\n{body}\n{SKILLS_CLOSE_TAG}"
|
|
396
362
|
|
|
397
|
-
def _discover_skills(self) ->
|
|
363
|
+
def _discover_skills(self) -> "typing.List[SkillDescriptor]":
|
|
398
364
|
codex_home = self._config.codex_home
|
|
399
365
|
if codex_home is None:
|
|
400
366
|
return []
|
|
401
367
|
|
|
402
368
|
user_root = codex_home / "skills"
|
|
403
369
|
system_root = user_root / ".system"
|
|
404
|
-
discovered:
|
|
405
|
-
seen:
|
|
370
|
+
discovered: "typing.List[SkillDescriptor]" = []
|
|
371
|
+
seen: "typing.Set[Path]" = set()
|
|
406
372
|
|
|
407
373
|
user_paths = _discover_skill_files(user_root, excluded_root=system_root)
|
|
408
374
|
system_paths = _discover_skill_files(system_root)
|
|
409
375
|
|
|
410
|
-
for scope_rank, paths in (
|
|
376
|
+
for scope_rank, root, paths in (
|
|
377
|
+
(3, user_root, user_paths),
|
|
378
|
+
(0, system_root, system_paths),
|
|
379
|
+
):
|
|
411
380
|
for path in paths:
|
|
412
381
|
resolved = path.resolve()
|
|
413
382
|
if resolved in seen:
|
|
414
383
|
continue
|
|
415
384
|
seen.add(resolved)
|
|
416
|
-
descriptor = _parse_skill_descriptor(path, scope_rank)
|
|
385
|
+
descriptor = _parse_skill_descriptor(path, scope_rank, root)
|
|
417
386
|
if descriptor is not None:
|
|
418
387
|
discovered.append(descriptor)
|
|
419
388
|
|
|
420
|
-
return
|
|
421
|
-
discovered,
|
|
422
|
-
key=lambda skill: (skill.scope_rank, skill.name, skill.path_to_skill_md),
|
|
423
|
-
)
|
|
389
|
+
return discovered
|
|
424
390
|
|
|
425
|
-
def _build_contextual_user_messages(self) ->
|
|
426
|
-
sections:
|
|
391
|
+
def _build_contextual_user_messages(self) -> "typing.List[ContextMessage]":
|
|
392
|
+
sections: "typing.List[str]" = []
|
|
427
393
|
user_instructions = self._merged_user_instructions()
|
|
428
394
|
if user_instructions is not None:
|
|
429
395
|
sections.append(
|
|
@@ -443,8 +409,8 @@ class ContextManager:
|
|
|
443
409
|
)
|
|
444
410
|
]
|
|
445
411
|
|
|
446
|
-
def _merged_user_instructions(self) ->
|
|
447
|
-
parts:
|
|
412
|
+
def _merged_user_instructions(self) -> "typing.Union[str, None]":
|
|
413
|
+
parts: "typing.List[str]" = []
|
|
448
414
|
if self._config.user_instructions is not None:
|
|
449
415
|
parts.append(self._config.user_instructions)
|
|
450
416
|
if self._config.codex_home_instructions is not None:
|
|
@@ -459,8 +425,8 @@ class ContextManager:
|
|
|
459
425
|
|
|
460
426
|
return "\n\n".join(parts) or None
|
|
461
427
|
|
|
462
|
-
def _read_project_docs(self) ->
|
|
463
|
-
docs:
|
|
428
|
+
def _read_project_docs(self) -> "typing.Union[str, None]":
|
|
429
|
+
docs: "typing.List[str]" = []
|
|
464
430
|
remaining = self._config.project_doc_max_bytes
|
|
465
431
|
for path in self._discover_project_doc_paths():
|
|
466
432
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
@@ -478,13 +444,16 @@ class ContextManager:
|
|
|
478
444
|
return None
|
|
479
445
|
return "\n\n".join(docs)
|
|
480
446
|
|
|
481
|
-
def _discover_project_doc_paths(self) ->
|
|
482
|
-
seen:
|
|
483
|
-
discovered:
|
|
447
|
+
def _discover_project_doc_paths(self) -> "typing.List[Path]":
|
|
448
|
+
seen: "typing.Set[Path]" = set()
|
|
449
|
+
discovered: "typing.List[Path]" = []
|
|
484
450
|
|
|
485
451
|
search_dirs = self._project_search_dirs()
|
|
486
452
|
for directory in search_dirs:
|
|
487
|
-
for candidate_name in (
|
|
453
|
+
for candidate_name in (
|
|
454
|
+
LOCAL_PROJECT_DOC_FILENAME,
|
|
455
|
+
DEFAULT_PROJECT_DOC_FILENAME,
|
|
456
|
+
):
|
|
488
457
|
candidate = (directory / candidate_name).resolve()
|
|
489
458
|
if candidate.exists() and candidate.is_file() and candidate not in seen:
|
|
490
459
|
discovered.append(candidate)
|
|
@@ -492,9 +461,9 @@ class ContextManager:
|
|
|
492
461
|
break
|
|
493
462
|
return discovered
|
|
494
463
|
|
|
495
|
-
def _project_search_dirs(self) ->
|
|
464
|
+
def _project_search_dirs(self) -> "typing.List[Path]":
|
|
496
465
|
project_root = self._find_project_root()
|
|
497
|
-
directories:
|
|
466
|
+
directories: "typing.List[Path]" = []
|
|
498
467
|
current = self.cwd
|
|
499
468
|
chain = [current]
|
|
500
469
|
while current != project_root and current.parent != current:
|
|
@@ -504,48 +473,75 @@ class ContextManager:
|
|
|
504
473
|
directories.extend(chain)
|
|
505
474
|
return directories
|
|
506
475
|
|
|
507
|
-
def _find_project_root(self) ->
|
|
476
|
+
def _find_project_root(self) -> "Path":
|
|
508
477
|
for ancestor in [self.cwd, *self.cwd.parents]:
|
|
509
478
|
if (ancestor / ".git").exists():
|
|
510
479
|
return ancestor
|
|
511
480
|
return self.cwd
|
|
512
481
|
|
|
513
|
-
def _serialize_environment_context(self) ->
|
|
482
|
+
def _serialize_environment_context(self) -> "str":
|
|
483
|
+
cwd = escape(str(self.cwd), {'"': """, "'": "'"})
|
|
514
484
|
lines = [
|
|
515
485
|
"<environment_context>",
|
|
516
|
-
f" <cwd>{
|
|
486
|
+
f" <cwd>{cwd}</cwd>",
|
|
517
487
|
f" <shell>{self._shell}</shell>",
|
|
518
488
|
f" <current_date>{self._current_date}</current_date>",
|
|
519
489
|
f" <timezone>{self._timezone_name}</timezone>",
|
|
520
|
-
"</environment_context>",
|
|
521
490
|
]
|
|
491
|
+
sandbox_mode = self._config.sandbox_mode or "danger-full-access"
|
|
492
|
+
if sandbox_mode == "danger-full-access":
|
|
493
|
+
permissions = (
|
|
494
|
+
'<permission_profile type="disabled"><file_system type="unrestricted" />'
|
|
495
|
+
"</permission_profile>"
|
|
496
|
+
)
|
|
497
|
+
elif sandbox_mode in {"read-only", "workspace-write"}:
|
|
498
|
+
entries = ['<entry access="read"><special>:root</special></entry>']
|
|
499
|
+
if sandbox_mode == "workspace-write":
|
|
500
|
+
entries.extend(
|
|
501
|
+
[
|
|
502
|
+
'<entry access="write"><path>{0}</path></entry>'.format(cwd),
|
|
503
|
+
'<entry access="write"><special>:slash_tmp</special></entry>',
|
|
504
|
+
'<entry access="write"><special>:tmpdir</special></entry>',
|
|
505
|
+
]
|
|
506
|
+
)
|
|
507
|
+
entries.extend(
|
|
508
|
+
'<entry access="read"><path>{0}</path></entry>'.format(
|
|
509
|
+
escape(str(self.cwd / name), {'"': """, "'": "'"})
|
|
510
|
+
)
|
|
511
|
+
for name in (".git", ".agents", ".codex")
|
|
512
|
+
)
|
|
513
|
+
permissions = (
|
|
514
|
+
'<permission_profile type="managed"><file_system type="restricted">'
|
|
515
|
+
"{0}</file_system></permission_profile>".format("".join(entries))
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
raise ValueError("unsupported sandbox mode: {0}".format(sandbox_mode))
|
|
519
|
+
lines.append(
|
|
520
|
+
" <filesystem><workspace_roots><root>{0}</root></workspace_roots>"
|
|
521
|
+
"{1}</filesystem>".format(cwd, permissions)
|
|
522
|
+
)
|
|
523
|
+
lines.append("</environment_context>")
|
|
522
524
|
return "\n".join(lines)
|
|
523
525
|
|
|
524
526
|
|
|
525
|
-
def _input_text_item(text:
|
|
527
|
+
def _input_text_item(text: "str") -> "JSONDict":
|
|
526
528
|
return {"type": "input_text", "text": text}
|
|
527
529
|
|
|
528
530
|
|
|
529
|
-
def _normalize_text(value) ->
|
|
531
|
+
def _normalize_text(value) -> "typing.Union[str, None]":
|
|
530
532
|
if value is None:
|
|
531
533
|
return None
|
|
532
534
|
text = str(value).strip()
|
|
533
535
|
return text or None
|
|
534
536
|
|
|
535
537
|
|
|
536
|
-
def _normalize_int(value) ->
|
|
538
|
+
def _normalize_int(value) -> "typing.Union[int, None]":
|
|
537
539
|
if value is None:
|
|
538
540
|
return None
|
|
539
541
|
return int(value)
|
|
540
542
|
|
|
541
543
|
|
|
542
|
-
def
|
|
543
|
-
if mode == "plan":
|
|
544
|
-
return PLAN_COLLABORATION_INSTRUCTIONS_PATH.read_text(encoding="utf-8")
|
|
545
|
-
return DEFAULT_COLLABORATION_INSTRUCTIONS_PATH.read_text(encoding="utf-8")
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
|
|
544
|
+
def _read_first_instruction_file(base: "Path") -> "typing.Union[str, None]":
|
|
549
545
|
for candidate_name in (LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME):
|
|
550
546
|
candidate = base / candidate_name
|
|
551
547
|
try:
|
|
@@ -558,7 +554,9 @@ def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
|
|
|
558
554
|
return None
|
|
559
555
|
|
|
560
556
|
|
|
561
|
-
def _resolve_personality_message(
|
|
557
|
+
def _resolve_personality_message(
|
|
558
|
+
variables, personality: "typing.Union[str, None]"
|
|
559
|
+
) -> "str":
|
|
562
560
|
if not isinstance(variables, dict):
|
|
563
561
|
return ""
|
|
564
562
|
normalized = (personality or "").strip().lower()
|
|
@@ -577,22 +575,30 @@ def _resolve_personality_message(variables, personality: 'typing.Union[str, None
|
|
|
577
575
|
|
|
578
576
|
|
|
579
577
|
def _discover_skill_files(
|
|
580
|
-
root:
|
|
581
|
-
excluded_root:
|
|
582
|
-
) ->
|
|
578
|
+
root: "Path",
|
|
579
|
+
excluded_root: "typing.Union[Path, None]" = None,
|
|
580
|
+
) -> "typing.List[Path]":
|
|
583
581
|
if not root.exists() or not root.is_dir():
|
|
584
582
|
return []
|
|
585
|
-
excluded =
|
|
586
|
-
|
|
583
|
+
excluded = (
|
|
584
|
+
excluded_root.resolve()
|
|
585
|
+
if excluded_root is not None and excluded_root.exists()
|
|
586
|
+
else None
|
|
587
|
+
)
|
|
588
|
+
paths: "typing.List[Path]" = []
|
|
587
589
|
for path in root.glob("**/SKILL.md"):
|
|
588
590
|
resolved = path.resolve()
|
|
589
|
-
if excluded is not None and (
|
|
591
|
+
if excluded is not None and (
|
|
592
|
+
resolved == excluded or excluded in resolved.parents
|
|
593
|
+
):
|
|
590
594
|
continue
|
|
591
595
|
paths.append(path)
|
|
592
596
|
return sorted(paths)
|
|
593
597
|
|
|
594
598
|
|
|
595
|
-
def _parse_skill_descriptor(
|
|
599
|
+
def _parse_skill_descriptor(
|
|
600
|
+
path: "Path", scope_rank: "int", root: "Path"
|
|
601
|
+
) -> "typing.Union[SkillDescriptor, None]":
|
|
596
602
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
597
603
|
if not text.startswith("---\n"):
|
|
598
604
|
return None
|
|
@@ -601,25 +607,29 @@ def _parse_skill_descriptor(path: 'Path', scope_rank: 'int') -> 'typing.Union[Sk
|
|
|
601
607
|
if end_index == -1:
|
|
602
608
|
return None
|
|
603
609
|
frontmatter = text[4:end_index]
|
|
604
|
-
fields
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
continue
|
|
608
|
-
key, _, raw_value = line.partition(":")
|
|
609
|
-
fields[key.strip()] = _strip_yaml_string(raw_value.strip())
|
|
610
|
+
fields = yaml.safe_load(frontmatter)
|
|
611
|
+
if not isinstance(fields, dict):
|
|
612
|
+
return None
|
|
610
613
|
name = fields.get("name")
|
|
611
614
|
description = fields.get("description")
|
|
612
|
-
if not name or not description:
|
|
615
|
+
if not isinstance(name, str) or not isinstance(description, str):
|
|
616
|
+
return None
|
|
617
|
+
if not name.strip() or not description.strip():
|
|
613
618
|
return None
|
|
619
|
+
metadata_path = path.parent / "agents" / "openai.yaml"
|
|
620
|
+
if metadata_path.is_file():
|
|
621
|
+
metadata = yaml.safe_load(
|
|
622
|
+
metadata_path.read_text(encoding="utf-8", errors="replace")
|
|
623
|
+
)
|
|
624
|
+
if (
|
|
625
|
+
metadata is not None
|
|
626
|
+
and metadata.get("policy", {}).get("allow_implicit_invocation") is False
|
|
627
|
+
):
|
|
628
|
+
return None
|
|
614
629
|
return SkillDescriptor(
|
|
615
|
-
name=name,
|
|
616
|
-
description=description,
|
|
617
|
-
path_to_skill_md=
|
|
630
|
+
name=name.strip(),
|
|
631
|
+
description=description.strip(),
|
|
632
|
+
path_to_skill_md=root.resolve() / path.relative_to(root),
|
|
618
633
|
scope_rank=scope_rank,
|
|
634
|
+
root=root.resolve(),
|
|
619
635
|
)
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
def _strip_yaml_string(value: 'str') -> 'str':
|
|
623
|
-
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
624
|
-
return value[1:-1]
|
|
625
|
-
return value
|