codex-agent-framework 0.1.10__tar.gz → 0.1.11__tar.gz
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.
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/CHANGELOG.md +17 -0
- {codex_agent_framework-0.1.10/codex_agent_framework.egg-info → codex_agent_framework-0.1.11}/PKG-INFO +2 -1
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/README.md +1 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/agent.py +28 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/builtin_tools.py +52 -17
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/chat.py +19 -1
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/prompts/system_prompt.txt +5 -4
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/utils.py +126 -33
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11/codex_agent_framework.egg-info}/PKG-INFO +2 -1
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/pyproject.toml +1 -1
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_agent.py +42 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_chat.py +66 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_utils.py +109 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/LICENSE +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/__init__.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/__main__.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/agent_runtime.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/ai.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/builtin_commands.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/builtin_providers.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/client.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/event.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_webdriver.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/memory.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/scheduler.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/server.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/service.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/tray.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/tui.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/voice.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/requires.txt +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/setup.cfg +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_cli.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_client.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_scheduler.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_server.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_service.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_tray.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_tui.py +0 -0
- {codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/tests/test_worker.py +0 -0
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
5
|
This project loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning where practical.
|
|
6
|
+
## [0.1.11] - 2026-05-05
|
|
7
|
+
### Added
|
|
8
|
+
- Add a separate `view` tool for folders, URLs, rich document extraction, and Python object inspection.
|
|
9
|
+
- Add context status counts for persistent and temporary session messages, surfaced in the TUI status bar.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Make `read` a strict local UTF-8 file reader with reliable forced line numbers and regex pattern search by default.
|
|
13
|
+
- Keep extracted `view` output unnumbered, including search snippets, because transformed document line structure is not source-of-truth reliable.
|
|
14
|
+
- Update the system prompt to direct agents to use `read` before targeted edits and `view` for broad extraction.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Preserve blank lines and real source line numbers in `read` pattern snippets.
|
|
18
|
+
- Apply line-based pagination before token truncation.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
- Add regression coverage for strict `read`, unnumbered `view`, blank-line preservation, regex/literal search, and persistent/temporary message status counts.
|
|
22
|
+
|
|
6
23
|
## [0.1.10] - 2026-05-05
|
|
7
24
|
### Fixed
|
|
8
25
|
- Persist only backend `compaction_summary` items in compaction messages, avoiding bulky compacted conversation payloads while preserving summary replay.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-agent-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: A lightweight event-driven Codex agent runtime.
|
|
5
5
|
Author: Baptiste
|
|
6
6
|
License-Expression: MIT
|
|
@@ -400,6 +400,7 @@ python -m build
|
|
|
400
400
|
The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
|
|
401
401
|
|
|
402
402
|
## Recent changes
|
|
403
|
+
- `0.1.11`: split strict line-numbered UTF-8 `read` from unnumbered extracted `view`, preserve blank lines in read snippets, and show persistent+temporary message counts in the TUI status bar.
|
|
403
404
|
- `0.1.10`: persist only backend compaction summaries, drop bulky compacted conversation payloads, and refresh context status after compaction.
|
|
404
405
|
- `0.1.9`: maintenance packaging release after validating the local execution environment and deploy workflow.
|
|
405
406
|
- `0.1.8`: scope TUI replay/SSE catch-up to the active session and make bash/python subprocesses inherit the project Python environment, including service-launched agents.
|
|
@@ -352,6 +352,7 @@ python -m build
|
|
|
352
352
|
The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
|
|
353
353
|
|
|
354
354
|
## Recent changes
|
|
355
|
+
- `0.1.11`: split strict line-numbered UTF-8 `read` from unnumbered extracted `view`, preserve blank lines in read snippets, and show persistent+temporary message counts in the TUI status bar.
|
|
355
356
|
- `0.1.10`: persist only backend compaction summaries, drop bulky compacted conversation payloads, and refresh context status after compaction.
|
|
356
357
|
- `0.1.9`: maintenance packaging release after validating the local execution environment and deploy workflow.
|
|
357
358
|
- `0.1.8`: scope TUI replay/SSE catch-up to the active session and make bash/python subprocesses inherit the project Python environment, including service-launched agents.
|
|
@@ -1154,6 +1154,14 @@ class Agent:
|
|
|
1154
1154
|
visible_history_count = len(truncated_others[max(0, history_start):])
|
|
1155
1155
|
sent_session_messages = visible_history_count + len(images)
|
|
1156
1156
|
total_session_messages = len(self.session.messages) if self.session else 0
|
|
1157
|
+
persistent_session_messages = sum(
|
|
1158
|
+
1 for msg in session_messages
|
|
1159
|
+
if msg.get("turns_left", -1) == -1
|
|
1160
|
+
)
|
|
1161
|
+
temporary_session_messages = sum(
|
|
1162
|
+
1 for msg in session_messages
|
|
1163
|
+
if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
|
|
1164
|
+
)
|
|
1157
1165
|
percent = (current_count / input_limit * 100) if input_limit else 0
|
|
1158
1166
|
status = modict(
|
|
1159
1167
|
used_tokens=current_count,
|
|
@@ -1167,6 +1175,8 @@ class Agent:
|
|
|
1167
1175
|
sent_session_messages=sent_session_messages,
|
|
1168
1176
|
total_session_messages=total_session_messages,
|
|
1169
1177
|
sent_session_message_percent=(sent_session_messages / total_session_messages * 100) if total_session_messages else 0,
|
|
1178
|
+
persistent_session_messages=persistent_session_messages,
|
|
1179
|
+
temporary_session_messages=temporary_session_messages,
|
|
1170
1180
|
context_session_messages=len(session_messages),
|
|
1171
1181
|
image_messages=len(images),
|
|
1172
1182
|
pending_messages=len(self.pending),
|
|
@@ -1199,6 +1209,8 @@ class Agent:
|
|
|
1199
1209
|
sent_session_messages=estimated.sent_session_messages,
|
|
1200
1210
|
total_session_messages=estimated.total_session_messages,
|
|
1201
1211
|
sent_session_message_percent=estimated.sent_session_message_percent,
|
|
1212
|
+
persistent_session_messages=estimated.get("persistent_session_messages", 0),
|
|
1213
|
+
temporary_session_messages=estimated.get("temporary_session_messages", 0),
|
|
1202
1214
|
quota_5h_percent=None,
|
|
1203
1215
|
quota_7d_percent=None,
|
|
1204
1216
|
model=self.config.get("model") or "gpt-5.4",
|
|
@@ -1223,6 +1235,8 @@ class Agent:
|
|
|
1223
1235
|
sent_session_messages=estimated.sent_session_messages,
|
|
1224
1236
|
total_session_messages=estimated.total_session_messages,
|
|
1225
1237
|
sent_session_message_percent=estimated.sent_session_message_percent,
|
|
1238
|
+
persistent_session_messages=estimated.get("persistent_session_messages", 0),
|
|
1239
|
+
temporary_session_messages=estimated.get("temporary_session_messages", 0),
|
|
1226
1240
|
model=self.config.get("model") or "gpt-5.4",
|
|
1227
1241
|
busy=self.busy,
|
|
1228
1242
|
current_turn_id=self.current_turn_id,
|
|
@@ -1244,6 +1258,8 @@ class Agent:
|
|
|
1244
1258
|
sent_session_messages=estimated.sent_session_messages,
|
|
1245
1259
|
total_session_messages=estimated.total_session_messages,
|
|
1246
1260
|
sent_session_message_percent=estimated.sent_session_message_percent,
|
|
1261
|
+
persistent_session_messages=estimated.get("persistent_session_messages", 0),
|
|
1262
|
+
temporary_session_messages=estimated.get("temporary_session_messages", 0),
|
|
1247
1263
|
model=self.config.get("model") or "gpt-5.4",
|
|
1248
1264
|
busy=self.busy,
|
|
1249
1265
|
current_turn_id=self.current_turn_id,
|
|
@@ -1267,6 +1283,8 @@ class Agent:
|
|
|
1267
1283
|
sent_session_messages=context_status.get("sent_session_messages", 0),
|
|
1268
1284
|
total_session_messages=context_status.get("total_session_messages", 0),
|
|
1269
1285
|
sent_session_message_percent=context_status.get("sent_session_message_percent", 0),
|
|
1286
|
+
persistent_session_messages=context_status.get("persistent_session_messages", 0),
|
|
1287
|
+
temporary_session_messages=context_status.get("temporary_session_messages", 0),
|
|
1270
1288
|
quota_5h_percent=wham.quota_5h_percent,
|
|
1271
1289
|
quota_7d_percent=wham.quota_7d_percent,
|
|
1272
1290
|
model=model or self.config.get("model") or "gpt-5.4",
|
|
@@ -1383,6 +1401,14 @@ class Agent:
|
|
|
1383
1401
|
formatted_context = list(msg.format(context=self.shell.namespace) for msg in context)
|
|
1384
1402
|
total_session_messages = len(self.session.messages) if self.session else 0
|
|
1385
1403
|
sent_session_messages = len(history) + len(images)
|
|
1404
|
+
persistent_session_messages = sum(
|
|
1405
|
+
1 for msg in session_messages
|
|
1406
|
+
if msg.get("turns_left", -1) == -1
|
|
1407
|
+
)
|
|
1408
|
+
temporary_session_messages = sum(
|
|
1409
|
+
1 for msg in session_messages
|
|
1410
|
+
if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
|
|
1411
|
+
)
|
|
1386
1412
|
token_count_source = "local_estimate"
|
|
1387
1413
|
try:
|
|
1388
1414
|
current_count = self.ai.count_input_tokens(
|
|
@@ -1398,6 +1424,8 @@ class Agent:
|
|
|
1398
1424
|
sent_session_messages=sent_session_messages,
|
|
1399
1425
|
total_session_messages=total_session_messages,
|
|
1400
1426
|
sent_session_message_percent=(sent_session_messages / total_session_messages * 100) if total_session_messages else 0,
|
|
1427
|
+
persistent_session_messages=persistent_session_messages,
|
|
1428
|
+
temporary_session_messages=temporary_session_messages,
|
|
1401
1429
|
token_count_source=token_count_source,
|
|
1402
1430
|
)
|
|
1403
1431
|
return formatted_context
|
|
@@ -7,43 +7,78 @@ from collections.abc import Mapping
|
|
|
7
7
|
from .image import persist_image_url
|
|
8
8
|
from .memory import format_memory_results_xml
|
|
9
9
|
from .tool import ImageGenerationTool, WebSearchTool, get_agent, tool
|
|
10
|
-
from .utils import read_document_content
|
|
10
|
+
from .utils import read_document_content, view_document_content
|
|
11
11
|
|
|
12
12
|
web_search = WebSearchTool()
|
|
13
13
|
image_generation = ImageGenerationTool()
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@tool
|
|
17
|
-
def read(source, start_at_line: int = 1):
|
|
17
|
+
def read(source: str, start_at_line: int = 1, pattern: str = None, regex: bool = True):
|
|
18
18
|
"""
|
|
19
19
|
description: |
|
|
20
|
-
|
|
21
|
-
This tool
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
For
|
|
26
|
-
|
|
27
|
-
For large outputs: The output will be automatically truncated to fit token limits. When truncated, you'll see a message at the end of content indicating the truncated line range to know from where to resume reading.
|
|
20
|
+
Strictly read a local UTF-8 text file as source-of-truth content.
|
|
21
|
+
This tool always prefixes returned content with reliable source line numbers.
|
|
22
|
+
It does not extract document formats, fetch URLs, inspect Python objects, or render directory trees.
|
|
23
|
+
Use `view` for folders, URLs, PDFs, DOCX, ODT, HTML, XLSX, and other extracted or transformed content.
|
|
24
|
+
|
|
25
|
+
For large outputs: the output is automatically truncated. When truncated, a message indicates the omitted line range.
|
|
26
|
+
When `pattern` is provided, it is treated as a Python regex by default and all matches are returned as separated numbered snippets with up to 10 lines before and after each match.
|
|
28
27
|
parameters:
|
|
29
28
|
source:
|
|
30
29
|
description: |
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- URL (e.g., "https://example.com/report.pdf")
|
|
34
|
-
- any other data structure, or python variable living in the shell's namespace
|
|
35
|
-
Pass only one source. Use parallel `read` calls for multiple sources.
|
|
30
|
+
Local UTF-8 text file path only. For directories, URLs, rich documents, or Python objects, use `view` instead.
|
|
31
|
+
type: string
|
|
36
32
|
start_at_line:
|
|
37
33
|
description: |
|
|
38
34
|
Line number to start reading from (1-indexed). Default is 1 (start from beginning).
|
|
39
|
-
When reading large
|
|
35
|
+
When reading large files in chunks, set this to continue from where you left off.
|
|
40
36
|
type: integer
|
|
37
|
+
pattern:
|
|
38
|
+
description: |
|
|
39
|
+
Optional search pattern. Treated as a Python regular expression by default.
|
|
40
|
+
Matching results are returned as separated snippets with reliable source line numbers.
|
|
41
|
+
type: string
|
|
42
|
+
regex:
|
|
43
|
+
description: |
|
|
44
|
+
Whether to treat `pattern` as a Python regular expression. Default true. Set false for literal substring search.
|
|
45
|
+
type: boolean
|
|
46
|
+
required:
|
|
47
|
+
- source
|
|
48
|
+
"""
|
|
49
|
+
agent = get_agent()
|
|
50
|
+
max_tokens = agent.config.get('max_input_tokens', 8000)
|
|
51
|
+
return read_document_content(source, start_at_line=start_at_line, max_tokens=max_tokens, pattern=pattern, regex=regex)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@tool
|
|
55
|
+
def view(source, start_at_line: int = 1, search_query: str = None):
|
|
56
|
+
"""
|
|
57
|
+
description: |
|
|
58
|
+
View/extract text content from varied sources using the document extraction pipeline.
|
|
59
|
+
Use this for folders, URLs, PDFs, DOCX, ODT, HTML, XLSX, archives, and Python objects.
|
|
60
|
+
This is a convenience extraction view, not a strict source-of-truth file reader; use `read` for local UTF-8 files that must be read faithfully before editing.
|
|
61
|
+
|
|
62
|
+
`view` never prefixes line numbers, including search result snippets, because extracted line structure is not reliable.
|
|
63
|
+
For large outputs: the output is automatically truncated.
|
|
64
|
+
parameters:
|
|
65
|
+
source:
|
|
66
|
+
description: |
|
|
67
|
+
Source to view/extract: local folder or file, URL, or Python object/variable.
|
|
68
|
+
start_at_line:
|
|
69
|
+
description: |
|
|
70
|
+
Line number to start viewing from (1-indexed). Pagination only; line numbers are not rendered.
|
|
71
|
+
type: integer
|
|
72
|
+
search_query:
|
|
73
|
+
description: |
|
|
74
|
+
Optional literal text query for simple search within the extracted content. Results are returned as unnumbered snippets.
|
|
75
|
+
type: string
|
|
41
76
|
required:
|
|
42
77
|
- source
|
|
43
78
|
"""
|
|
44
79
|
agent = get_agent()
|
|
45
80
|
max_tokens = agent.config.get('max_input_tokens', 8000)
|
|
46
|
-
return
|
|
81
|
+
return view_document_content(source, start_at_line=start_at_line, max_tokens=max_tokens, search_query=search_query)
|
|
47
82
|
|
|
48
83
|
|
|
49
84
|
@tool(tool_type="custom")
|
|
@@ -390,14 +390,17 @@ class Chat:
|
|
|
390
390
|
quota_5h = cls._format_percent(status.get("quota_5h_percent"))
|
|
391
391
|
quota_7d = cls._format_percent(status.get("quota_7d_percent"))
|
|
392
392
|
sent_messages = int(status.get("sent_session_messages") or 0)
|
|
393
|
+
persistent_messages = status.get("persistent_session_messages")
|
|
394
|
+
temporary_messages = status.get("temporary_session_messages")
|
|
393
395
|
total_messages = int(status.get("total_session_messages") or 0)
|
|
394
396
|
model = status.get("model") or "-"
|
|
397
|
+
message_segments = cls._message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages)
|
|
395
398
|
return Text.assemble(
|
|
396
399
|
(" ctx ", "bold #c9d1d9 on #161b22"),
|
|
397
400
|
(f"{used}/{limit} ktok ", "#8b949e on #161b22"),
|
|
398
401
|
(f"{percent:.1f}%", f"{context_style} on #161b22"),
|
|
399
402
|
(" msg ", "#6e7681 on #161b22"),
|
|
400
|
-
|
|
403
|
+
*message_segments,
|
|
401
404
|
(" 5h ", "#6e7681 on #161b22"),
|
|
402
405
|
(quota_5h, f"{cls._percent_style(status.get('quota_5h_percent'))} on #161b22"),
|
|
403
406
|
(" 7d ", "#6e7681 on #161b22"),
|
|
@@ -406,6 +409,21 @@ class Chat:
|
|
|
406
409
|
(str(model), "#c9d1d9 on #161b22"),
|
|
407
410
|
)
|
|
408
411
|
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages):
|
|
414
|
+
if persistent_messages is None and temporary_messages is None:
|
|
415
|
+
return ((f"{sent_messages}/{total_messages}", "#c9d1d9 on #161b22"),)
|
|
416
|
+
try:
|
|
417
|
+
persistent = int(persistent_messages or 0)
|
|
418
|
+
temporary = int(temporary_messages or 0)
|
|
419
|
+
except Exception:
|
|
420
|
+
return ((f"{sent_messages}/{total_messages}", "#c9d1d9 on #161b22"),)
|
|
421
|
+
return (
|
|
422
|
+
(str(persistent), "#c9d1d9 on #161b22"),
|
|
423
|
+
(f"+{temporary}", "italic #f0883e on #161b22"),
|
|
424
|
+
(f"/{total_messages}", "#c9d1d9 on #161b22"),
|
|
425
|
+
)
|
|
426
|
+
|
|
409
427
|
@staticmethod
|
|
410
428
|
def _format_ktok(value):
|
|
411
429
|
try:
|
{codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/prompts/system_prompt.txt
RENAMED
|
@@ -29,16 +29,17 @@ These tools are intended as general means for you to act on the user's local env
|
|
|
29
29
|
Code runs with user priviledges on his local system, use discernment before running code that could compromise sensitive data or system integrity.
|
|
30
30
|
|
|
31
31
|
Other agentic tools are provided to ease your interaction with the user and content:
|
|
32
|
-
- `read` : to read
|
|
33
|
-
- `edit`: to
|
|
34
|
-
- `write`: to write/overwrite wholes files
|
|
32
|
+
- `read` : to read line-numbered local utf-8 files precisely (with optional pattern search in the content)
|
|
33
|
+
- `edit`: to make targeted edits to local utf-8 files.
|
|
34
|
+
- `write`: to write/overwrite wholes files (use with caution)
|
|
35
|
+
- `view`: to quickly access the content of folders, various file types (PDF, ODF, XLSX, HTML, ...), webpages, or python objects - when used programmatically. Works on most content types (always yields a result, even mediocre).
|
|
35
36
|
- `observe` : to use your AI vision capabilities on image files or urls
|
|
36
37
|
- `show`: to show any kind of content to the user by opening it with the default system backend
|
|
37
38
|
+ Any other custom tools loaded for the current session
|
|
38
39
|
|
|
39
40
|
Always use these specialized tools over generic bash or python calls when relevant for a task as they are optimized for agentic feedback and efficiency
|
|
40
41
|
Don't use `bash` or `python` to perform targeted edits via scripts (unless automation makes sense) : always use the edit tool.
|
|
41
|
-
|
|
42
|
+
Never edit blindly/guessing, always use `read` first to know what to replace before you attempt to make changes to files.
|
|
42
43
|
|
|
43
44
|
# Agentic Work Loop
|
|
44
45
|
|
|
@@ -79,15 +79,53 @@ def text_content(file):
|
|
|
79
79
|
else:
|
|
80
80
|
return None
|
|
81
81
|
|
|
82
|
-
def
|
|
82
|
+
def _number_lines(lines: list[str], start_line: int = 1, total_lines: int | None = None) -> str:
|
|
83
|
+
last_line = (start_line + len(lines) - 1) if total_lines is None else max(total_lines, start_line + len(lines) - 1)
|
|
84
|
+
width = len(str(max(1, last_line)))
|
|
85
|
+
return "\n".join(
|
|
86
|
+
f"{i:0{width}d}|{line}"
|
|
87
|
+
for i, line in enumerate(lines, start=start_line)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def add_line_numbers(text: str, start_line: int = 1, total_lines: int | None = None) -> str:
|
|
92
|
+
return _number_lines(text.splitlines(), start_line=start_line, total_lines=total_lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pattern_snippets(text: str, pattern, context_lines: int = 10, regex_pattern: bool = True) -> str:
|
|
96
|
+
pattern_text = str(pattern)
|
|
83
97
|
lines = text.splitlines()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
total_lines = len(lines)
|
|
99
|
+
if regex_pattern:
|
|
100
|
+
try:
|
|
101
|
+
compiled_pattern = re.compile(pattern_text)
|
|
102
|
+
except re.error as exc:
|
|
103
|
+
raise ValueError(f"Invalid regex pattern {pattern_text!r}: {exc}") from exc
|
|
104
|
+
matches = [index for index, line in enumerate(lines, start=1) if compiled_pattern.search(line)]
|
|
105
|
+
else:
|
|
106
|
+
matches = [index for index, line in enumerate(lines, start=1) if pattern_text in line]
|
|
107
|
+
if not matches:
|
|
108
|
+
return _number_lines(lines, start_line=1, total_lines=total_lines)
|
|
109
|
+
|
|
110
|
+
context_lines = max(0, int(context_lines or 0))
|
|
111
|
+
ranges = []
|
|
112
|
+
for match_line in matches:
|
|
113
|
+
start = max(1, match_line - context_lines)
|
|
114
|
+
end = min(total_lines, match_line + context_lines)
|
|
115
|
+
if ranges and start <= ranges[-1][1] + 1:
|
|
116
|
+
ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
|
|
117
|
+
else:
|
|
118
|
+
ranges.append((start, end))
|
|
119
|
+
|
|
120
|
+
parts = []
|
|
121
|
+
for number, (start, end) in enumerate(ranges, start=1):
|
|
122
|
+
snippet_lines = lines[start - 1:end]
|
|
123
|
+
parts.append(
|
|
124
|
+
f"--- match snippet {number}/{len(ranges)}: lines {start}-{end} ---\n"
|
|
125
|
+
+ _number_lines(snippet_lines, start_line=start, total_lines=total_lines)
|
|
126
|
+
)
|
|
127
|
+
return "\n\n".join(parts)
|
|
128
|
+
|
|
91
129
|
def short_id(length=8):
|
|
92
130
|
return "".join(random.choices(string.ascii_letters,k=length))
|
|
93
131
|
|
|
@@ -162,19 +200,18 @@ def truncate(string, max_tokens=2000, start_line=1, model=None):
|
|
|
162
200
|
Returns:
|
|
163
201
|
Truncated string with indication of removed content
|
|
164
202
|
"""
|
|
165
|
-
#
|
|
166
|
-
tokens = tokenize(string, model=model)
|
|
167
|
-
if len(tokens) <= max_tokens:
|
|
168
|
-
return string
|
|
169
|
-
|
|
170
|
-
# line-based truncation
|
|
203
|
+
# line-based pagination
|
|
171
204
|
lines = string.splitlines(keepends=True)
|
|
172
|
-
|
|
173
|
-
# Skip to start_line if needed
|
|
174
205
|
if start_line > 1:
|
|
175
206
|
if start_line > len(lines):
|
|
176
207
|
return ""
|
|
177
|
-
|
|
208
|
+
string = ''.join(lines[start_line - 1:])
|
|
209
|
+
lines = string.splitlines(keepends=True)
|
|
210
|
+
|
|
211
|
+
# Quick check - if already under limit after pagination, return as-is
|
|
212
|
+
tokens = tokenize(string, model=model)
|
|
213
|
+
if len(tokens) <= max_tokens:
|
|
214
|
+
return string
|
|
178
215
|
|
|
179
216
|
# Calculate the actual last line number after skipping
|
|
180
217
|
remaining_lines = len(lines)
|
|
@@ -206,7 +243,7 @@ def truncate(string, max_tokens=2000, start_line=1, model=None):
|
|
|
206
243
|
|
|
207
244
|
return result
|
|
208
245
|
else:
|
|
209
|
-
return f"[Lines {start_line}-{start_line + remaining_lines - 1} truncated - first
|
|
246
|
+
return f"[Lines {start_line}-{start_line + remaining_lines - 1} truncated - first line exceeds token limit]\n"
|
|
210
247
|
|
|
211
248
|
def pack_msgs(messages):
|
|
212
249
|
text = ''
|
|
@@ -387,24 +424,80 @@ class Logger:
|
|
|
387
424
|
with open(file,'w',encoding="utf-8") as f:
|
|
388
425
|
f.write(str(content)+'\n')
|
|
389
426
|
|
|
390
|
-
def
|
|
391
|
-
|
|
392
|
-
|
|
427
|
+
def _render_numbered_read_text(text: str, start_at_line=1, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
|
|
428
|
+
effective_start_at_line = int(start_at_line or 1)
|
|
429
|
+
total_lines = len(text.splitlines())
|
|
430
|
+
if pattern:
|
|
431
|
+
text = pattern_snippets(text, pattern, context_lines=pattern_context_lines, regex_pattern=regex)
|
|
432
|
+
if max_tokens > 0:
|
|
433
|
+
text = truncate(text, max_tokens=max_tokens, start_line=1)
|
|
434
|
+
return text
|
|
435
|
+
# Apply pagination and truncation - truncate function handles smart chunking internally
|
|
436
|
+
if max_tokens > 0:
|
|
437
|
+
text = truncate(text, max_tokens=max_tokens, start_line=effective_start_at_line)
|
|
438
|
+
elif effective_start_at_line > 1:
|
|
439
|
+
lines = text.splitlines(keepends=True)
|
|
440
|
+
text = "" if effective_start_at_line > len(lines) else ''.join(lines[effective_start_at_line - 1:])
|
|
441
|
+
return add_line_numbers(text, start_line=effective_start_at_line, total_lines=total_lines)
|
|
393
442
|
|
|
394
|
-
Args:
|
|
395
|
-
source: Document source - either a local file path or URL (http/https)
|
|
396
|
-
start_at_line: Line number to start reading from (1-indexed)
|
|
397
|
-
max_tokens: Maximum tokens for truncation (0 to disable)
|
|
398
443
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
444
|
+
def _view_search_snippets(text: str, search_query, context_lines: int = 10) -> str:
|
|
445
|
+
query = str(search_query)
|
|
446
|
+
lines = text.splitlines()
|
|
447
|
+
matches = [index for index, line in enumerate(lines, start=1) if query in line]
|
|
448
|
+
if not matches:
|
|
449
|
+
return f"No results for search_query: {query!r}"
|
|
450
|
+
|
|
451
|
+
context_lines = max(0, int(context_lines or 0))
|
|
452
|
+
ranges = []
|
|
453
|
+
for match_line in matches:
|
|
454
|
+
start = max(1, match_line - context_lines)
|
|
455
|
+
end = min(len(lines), match_line + context_lines)
|
|
456
|
+
if ranges and start <= ranges[-1][1] + 1:
|
|
457
|
+
ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
|
|
458
|
+
else:
|
|
459
|
+
ranges.append((start, end))
|
|
460
|
+
|
|
461
|
+
parts = []
|
|
462
|
+
for number, (start, end) in enumerate(ranges, start=1):
|
|
463
|
+
snippet = "\n".join(lines[start - 1:end])
|
|
464
|
+
parts.append(f"--- search result {number}/{len(ranges)} ---\n{snippet}")
|
|
465
|
+
return "\n\n".join(parts)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def read_document_content(source, start_at_line=1, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
|
|
469
|
+
"""Strictly read a local UTF-8 text file without extraction, URL fetching, or directory traversal."""
|
|
470
|
+
if not isinstance(source, (str, os.PathLike)):
|
|
471
|
+
raise TypeError("read only accepts a local file path. Use view for objects or extracted document content.")
|
|
472
|
+
path = os.fspath(source)
|
|
473
|
+
if path.startswith(("http://", "https://")):
|
|
474
|
+
raise ValueError("read only accepts local UTF-8 text files. Use view for URLs.")
|
|
475
|
+
if os.path.isdir(path):
|
|
476
|
+
raise ValueError("read only accepts local UTF-8 text files. Use view for directories.")
|
|
477
|
+
if not os.path.isfile(path):
|
|
478
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
479
|
+
with open(path, "rb") as f:
|
|
480
|
+
data = f.read()
|
|
481
|
+
try:
|
|
482
|
+
text = data.decode("utf-8")
|
|
483
|
+
except UnicodeDecodeError as exc:
|
|
484
|
+
raise UnicodeDecodeError(exc.encoding, exc.object, exc.start, exc.end, f"{exc.reason}. read requires UTF-8 text files; use view for extracted content.") from exc
|
|
485
|
+
return _render_numbered_read_text(text, start_at_line=start_at_line, max_tokens=max_tokens, pattern=pattern, pattern_context_lines=pattern_context_lines, regex=regex)
|
|
403
486
|
|
|
404
|
-
text=get_text(source)
|
|
405
487
|
|
|
406
|
-
|
|
488
|
+
def view_document_content(source, start_at_line=1, max_tokens=8000, search_query=None, search_context_lines=10):
|
|
489
|
+
"""Extract text from varied sources via get_text. This view is not line-structure reliable."""
|
|
490
|
+
get_text = import_module(".get_text.get_text", __package__).get_text
|
|
491
|
+
text = get_text(source)
|
|
492
|
+
effective_start_at_line = int(start_at_line or 1)
|
|
493
|
+
if search_query:
|
|
494
|
+
text = _view_search_snippets(text, search_query, context_lines=search_context_lines)
|
|
495
|
+
if max_tokens > 0:
|
|
496
|
+
text = truncate(text, max_tokens=max_tokens, start_line=1)
|
|
497
|
+
return text
|
|
407
498
|
if max_tokens > 0:
|
|
408
|
-
|
|
409
|
-
|
|
499
|
+
return truncate(text, max_tokens=max_tokens, start_line=effective_start_at_line)
|
|
500
|
+
if effective_start_at_line > 1:
|
|
501
|
+
lines = text.splitlines(keepends=True)
|
|
502
|
+
return "" if effective_start_at_line > len(lines) else ''.join(lines[effective_start_at_line - 1:])
|
|
410
503
|
return text
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-agent-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: A lightweight event-driven Codex agent runtime.
|
|
5
5
|
Author: Baptiste
|
|
6
6
|
License-Expression: MIT
|
|
@@ -400,6 +400,7 @@ python -m build
|
|
|
400
400
|
The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
|
|
401
401
|
|
|
402
402
|
## Recent changes
|
|
403
|
+
- `0.1.11`: split strict line-numbered UTF-8 `read` from unnumbered extracted `view`, preserve blank lines in read snippets, and show persistent+temporary message counts in the TUI status bar.
|
|
403
404
|
- `0.1.10`: persist only backend compaction summaries, drop bulky compacted conversation payloads, and refresh context status after compaction.
|
|
404
405
|
- `0.1.9`: maintenance packaging release after validating the local execution environment and deploy workflow.
|
|
405
406
|
- `0.1.8`: scope TUI replay/SSE catch-up to the active session and make bash/python subprocesses inherit the project Python environment, including service-launched agents.
|
|
@@ -671,6 +671,48 @@ def test_builtin_read_tool_survives_get_text_submodule_shadowing():
|
|
|
671
671
|
assert "def read(" in agent.pending[-1].content
|
|
672
672
|
|
|
673
673
|
|
|
674
|
+
def test_builtin_read_tool_accepts_pattern_and_includes_context(tmp_path):
|
|
675
|
+
file = tmp_path / "sample.txt"
|
|
676
|
+
lines = [f"line {index}" for index in range(1, 31)]
|
|
677
|
+
lines[20] = "line 21 needle"
|
|
678
|
+
file.write_text("\n".join(lines), encoding="utf-8")
|
|
679
|
+
|
|
680
|
+
agent = Agent(session="new", voice_enabled=False)
|
|
681
|
+
message = AssistantMessage()
|
|
682
|
+
message.tool_calls = [{
|
|
683
|
+
"type": "function_call",
|
|
684
|
+
"call_id": "call_1",
|
|
685
|
+
"name": "read",
|
|
686
|
+
"arguments": f'{{"source": "{file}", "pattern": "needle"}}',
|
|
687
|
+
}]
|
|
688
|
+
|
|
689
|
+
assert agent.call_tools(message) is True
|
|
690
|
+
assert isinstance(agent.pending[-1], ToolResultWrapper)
|
|
691
|
+
assert "11|line 11" in agent.pending[-1].content
|
|
692
|
+
assert "21|line 21 needle" in agent.pending[-1].content
|
|
693
|
+
assert "10|line 10" not in agent.pending[-1].content
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_builtin_read_tool_accepts_regex_pattern(tmp_path):
|
|
697
|
+
file = tmp_path / "sample.txt"
|
|
698
|
+
lines = ["alpha", "ticket ABC-123", *[f"line {index}" for index in range(3, 25)], "ticket XYZ-123"]
|
|
699
|
+
file.write_text("\n".join(lines), encoding="utf-8")
|
|
700
|
+
|
|
701
|
+
agent = Agent(session="new", voice_enabled=False)
|
|
702
|
+
message = AssistantMessage()
|
|
703
|
+
message.tool_calls = [{
|
|
704
|
+
"type": "function_call",
|
|
705
|
+
"call_id": "call_1",
|
|
706
|
+
"name": "read",
|
|
707
|
+
"arguments": f'{{"source": "{file}", "pattern": "ABC-\\\\d+", "regex": true}}',
|
|
708
|
+
}]
|
|
709
|
+
|
|
710
|
+
assert agent.call_tools(message) is True
|
|
711
|
+
assert isinstance(agent.pending[-1], ToolResultWrapper)
|
|
712
|
+
assert "2|ticket ABC-123" in agent.pending[-1].content
|
|
713
|
+
assert "3|ticket XYZ-123" not in agent.pending[-1].content
|
|
714
|
+
|
|
715
|
+
|
|
674
716
|
def test_agent_applies_image_generation_tool_config():
|
|
675
717
|
agent = Agent(
|
|
676
718
|
session="new",
|
|
@@ -40,6 +40,48 @@ def test_agent_context_status_counts_only_effective_context_messages():
|
|
|
40
40
|
assert status.context_session_messages == 2
|
|
41
41
|
assert status.total_history_messages == 2
|
|
42
42
|
assert status.sent_session_messages == 2
|
|
43
|
+
assert status.persistent_session_messages == 2
|
|
44
|
+
assert status.temporary_session_messages == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_agent_context_status_counts_persistent_and_temporary_messages():
|
|
48
|
+
agent = Agent(session="new", voice_enabled=False, input_token_limit=10_000)
|
|
49
|
+
compaction = CompactionMessage(output_items=[{"type": "compaction_summary", "id": "cs_123"}])
|
|
50
|
+
persistent = DeveloperMessage(content="persistent")
|
|
51
|
+
temporary = DeveloperMessage(content="temporary", turns_left=2)
|
|
52
|
+
expired = DeveloperMessage(content="expired", turns_left=0)
|
|
53
|
+
agent.session.messages = [compaction, persistent, temporary, expired]
|
|
54
|
+
|
|
55
|
+
status = agent.context_status()
|
|
56
|
+
|
|
57
|
+
assert status.context_session_messages == 3
|
|
58
|
+
assert status.persistent_session_messages == 2
|
|
59
|
+
assert status.temporary_session_messages == 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_agent_status_preserves_persistent_and_temporary_message_counts():
|
|
63
|
+
agent = Agent(session="new", voice_enabled=False, input_token_limit=10_000)
|
|
64
|
+
agent.session.messages.append(DeveloperMessage(content="persistent"))
|
|
65
|
+
agent.session.messages.append(DeveloperMessage(content="temporary", turns_left=2))
|
|
66
|
+
|
|
67
|
+
status = agent.get_status()
|
|
68
|
+
|
|
69
|
+
assert status.persistent_session_messages == 2
|
|
70
|
+
assert status.temporary_session_messages == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_agent_api_status_uses_precached_persistent_and_temporary_counts(monkeypatch):
|
|
74
|
+
agent = Agent(session="new", voice_enabled=False, input_token_limit=10_000)
|
|
75
|
+
agent.session.messages.append(DeveloperMessage(content="persistent"))
|
|
76
|
+
agent.session.messages.append(DeveloperMessage(content="temporary", turns_left=2))
|
|
77
|
+
|
|
78
|
+
agent.get_context()
|
|
79
|
+
monkeypatch.setattr(agent, "context_status", lambda: (_ for _ in ()).throw(AssertionError("should use cache")))
|
|
80
|
+
agent.cache_api_status(usage=modict(input_tokens=1000), model="gpt-test")
|
|
81
|
+
|
|
82
|
+
status = agent.get_status()
|
|
83
|
+
assert status.persistent_session_messages == 2
|
|
84
|
+
assert status.temporary_session_messages == 1
|
|
43
85
|
|
|
44
86
|
|
|
45
87
|
def test_agent_context_status_counts_response_tool_specs():
|
|
@@ -185,6 +227,30 @@ def test_chat_status_bar_formats_compact_status():
|
|
|
185
227
|
assert any("#161b22" in str(span.style) for span in rendered.spans)
|
|
186
228
|
|
|
187
229
|
|
|
230
|
+
def test_chat_status_bar_formats_persistent_plus_temporary_messages():
|
|
231
|
+
status = modict(
|
|
232
|
+
context_current_tokens=12_300,
|
|
233
|
+
context_limit_tokens=128_000,
|
|
234
|
+
context_percent=9.609,
|
|
235
|
+
sent_session_messages=43,
|
|
236
|
+
persistent_session_messages=33,
|
|
237
|
+
temporary_session_messages=10,
|
|
238
|
+
total_session_messages=93,
|
|
239
|
+
quota_5h_percent=42,
|
|
240
|
+
quota_7d_percent=13,
|
|
241
|
+
model="gpt-5.4",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
rendered = Chat.format_status(status)
|
|
245
|
+
output = rendered.plain
|
|
246
|
+
|
|
247
|
+
assert "msg 33+10/93" in output
|
|
248
|
+
assert any(
|
|
249
|
+
"#f0883e" in str(span.style) and output[span.start:span.end] == "+10"
|
|
250
|
+
for span in rendered.spans
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
188
254
|
def test_chat_refresh_status_keeps_rich_text_styles():
|
|
189
255
|
chat = Chat(client=modict(
|
|
190
256
|
get_status=lambda: modict(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from io import BytesIO
|
|
2
2
|
|
|
3
|
+
import pytest
|
|
3
4
|
from PIL import Image as PILImage
|
|
4
5
|
|
|
5
6
|
from codex_agent.message import AssistantMessage, DeveloperMessage, ServerToolResponseMessage, ToolResultMessage, UserMessage
|
|
@@ -35,6 +36,114 @@ def test_project_python_env_prefers_running_interpreter():
|
|
|
35
36
|
assert env["PYTHON_EXECUTABLE"] == "/opt/project-python/bin/python"
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
def test_read_document_content_adds_line_numbers_by_default(tmp_path):
|
|
40
|
+
file = tmp_path / "sample.txt"
|
|
41
|
+
file.write_text("alpha\nbeta\ngamma", encoding="utf-8")
|
|
42
|
+
|
|
43
|
+
content = utils.read_document_content(str(file), max_tokens=10_000)
|
|
44
|
+
|
|
45
|
+
assert content == "1|alpha\n2|beta\n3|gamma"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_read_document_content_pattern_returns_all_numbered_snippets(tmp_path):
|
|
49
|
+
file = tmp_path / "sample.txt"
|
|
50
|
+
lines = [f"line {index}" for index in range(1, 61)]
|
|
51
|
+
lines[20] = "line 21 MATCH here"
|
|
52
|
+
lines[49] = "line 50 MATCH again"
|
|
53
|
+
file.write_text("\n".join(lines), encoding="utf-8")
|
|
54
|
+
|
|
55
|
+
content = utils.read_document_content(str(file), pattern="MATCH", max_tokens=10_000)
|
|
56
|
+
|
|
57
|
+
assert "--- match snippet 1/2: lines 11-31 ---" in content
|
|
58
|
+
assert "11|line 11" in content
|
|
59
|
+
assert "21|line 21 MATCH here" in content
|
|
60
|
+
assert "31|line 31" in content
|
|
61
|
+
assert "10|line 10" not in content
|
|
62
|
+
assert "--- match snippet 2/2: lines 40-60 ---" in content
|
|
63
|
+
assert "50|line 50 MATCH again" in content
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_read_document_content_pattern_context_is_clamped_to_file_start(tmp_path):
|
|
67
|
+
file = tmp_path / "sample.txt"
|
|
68
|
+
lines = ["line 1", "line 2 MATCH", "line 3"]
|
|
69
|
+
file.write_text("\n".join(lines), encoding="utf-8")
|
|
70
|
+
|
|
71
|
+
content = utils.read_document_content(str(file), pattern="MATCH", max_tokens=10_000)
|
|
72
|
+
|
|
73
|
+
assert content.startswith("--- match snippet 1/1: lines 1-3 ---\n1|line 1")
|
|
74
|
+
assert "2|line 2 MATCH" in content
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_read_document_content_pattern_is_regex_by_default(tmp_path):
|
|
78
|
+
file = tmp_path / "sample.txt"
|
|
79
|
+
lines = ["alpha", "ticket ABC-123", "ticket ABC-999", "ticket XYZ-123"]
|
|
80
|
+
file.write_text("\n".join(lines), encoding="utf-8")
|
|
81
|
+
|
|
82
|
+
content = utils.read_document_content(str(file), pattern=r"ABC-\d+", pattern_context_lines=0, max_tokens=10_000)
|
|
83
|
+
|
|
84
|
+
assert "2|ticket ABC-123" in content
|
|
85
|
+
assert "3|ticket ABC-999" in content
|
|
86
|
+
assert "4|ticket XYZ-123" not in content
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_read_document_content_can_treat_pattern_as_literal(tmp_path):
|
|
90
|
+
file = tmp_path / "sample.txt"
|
|
91
|
+
lines = ["ticket ABC-123", r"literal ABC-\d+ pattern"]
|
|
92
|
+
file.write_text("\n".join(lines), encoding="utf-8")
|
|
93
|
+
|
|
94
|
+
content = utils.read_document_content(str(file), pattern=r"ABC-\d+", regex=False, pattern_context_lines=0, max_tokens=10_000)
|
|
95
|
+
|
|
96
|
+
assert "1|ticket ABC-123" not in content
|
|
97
|
+
assert r"2|literal ABC-\d+ pattern" in content
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_read_document_content_preserves_blank_lines_and_real_line_numbers(tmp_path):
|
|
101
|
+
file = tmp_path / "sample.py"
|
|
102
|
+
file.write_text("one\n\n\n \nfive", encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
content = utils.read_document_content(str(file), max_tokens=10_000)
|
|
105
|
+
|
|
106
|
+
assert content == "1|one\n2|\n3|\n4| \n5|five"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_read_document_content_pattern_preserves_blank_lines_in_snippet_context(tmp_path):
|
|
110
|
+
file = tmp_path / "sample.py"
|
|
111
|
+
file.write_text("one\n\n\nMATCH\nfive", encoding="utf-8")
|
|
112
|
+
|
|
113
|
+
content = utils.read_document_content(str(file), pattern="MATCH", pattern_context_lines=3, max_tokens=10_000)
|
|
114
|
+
|
|
115
|
+
assert content == "--- match snippet 1/1: lines 1-5 ---\n1|one\n2|\n3|\n4|MATCH\n5|five"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_read_document_content_rejects_directories_and_urls(tmp_path):
|
|
119
|
+
with pytest.raises(ValueError, match="Use view for directories"):
|
|
120
|
+
utils.read_document_content(str(tmp_path), max_tokens=10_000)
|
|
121
|
+
|
|
122
|
+
with pytest.raises(ValueError, match="Use view for URLs"):
|
|
123
|
+
utils.read_document_content("https://example.com", max_tokens=10_000)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_view_document_content_keeps_extraction_pipeline_for_directories_without_line_numbers(tmp_path):
|
|
127
|
+
file = tmp_path / "sample.txt"
|
|
128
|
+
file.write_text("hello", encoding="utf-8")
|
|
129
|
+
|
|
130
|
+
content = utils.view_document_content(str(tmp_path), max_tokens=10_000)
|
|
131
|
+
|
|
132
|
+
assert "sample.txt" in content
|
|
133
|
+
assert "1|" not in content
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_view_document_content_search_query_returns_unnumbered_snippets(tmp_path):
|
|
137
|
+
file = tmp_path / "sample.txt"
|
|
138
|
+
file.write_text("alpha\nbeta needle\ngamma", encoding="utf-8")
|
|
139
|
+
|
|
140
|
+
content = utils.view_document_content(str(file), search_query="needle", max_tokens=10_000)
|
|
141
|
+
|
|
142
|
+
assert content.startswith("--- search result 1/1 ---\n")
|
|
143
|
+
assert "beta needle" in content
|
|
144
|
+
assert "2|" not in content
|
|
145
|
+
|
|
146
|
+
|
|
38
147
|
def test_split_history_turns_keeps_current_unfinished_turn_as_remainder():
|
|
39
148
|
complete_user = UserMessage(content="first")
|
|
40
149
|
complete_assistant = AssistantMessage(content="done")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/builtin_commands.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/builtin_providers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/__init__.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/default_gitignore
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.10 → codex_agent_framework-0.1.11}/codex_agent/get_text/get_text.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|