codex-agent-framework 0.1.9__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.9 → codex_agent_framework-0.1.11}/CHANGELOG.md +25 -0
- {codex_agent_framework-0.1.9/codex_agent_framework.egg-info → codex_agent_framework-0.1.11}/PKG-INFO +3 -1
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/README.md +2 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/agent.py +36 -2
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/agent_runtime.py +6 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/builtin_tools.py +52 -17
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/chat.py +19 -1
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/message.py +18 -3
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/prompts/system_prompt.txt +5 -4
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/utils.py +126 -33
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11/codex_agent_framework.egg-info}/PKG-INFO +3 -1
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/pyproject.toml +1 -1
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_agent.py +81 -2
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_chat.py +66 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_messages.py +19 -3
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_server.py +22 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_utils.py +109 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/LICENSE +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/__init__.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/__main__.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/ai.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/builtin_commands.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/builtin_providers.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/client.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/event.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/get_webdriver.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/memory.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/scheduler.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/server.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/service.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/tray.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/tui.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/voice.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/requires.txt +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/setup.cfg +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_cli.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_client.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_scheduler.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_service.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_tray.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_tui.py +0 -0
- {codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/tests/test_worker.py +0 -0
|
@@ -3,6 +3,31 @@
|
|
|
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
|
+
|
|
23
|
+
## [0.1.10] - 2026-05-05
|
|
24
|
+
### Fixed
|
|
25
|
+
- Persist only backend `compaction_summary` items in compaction messages, avoiding bulky compacted conversation payloads while preserving summary replay.
|
|
26
|
+
- Sanitize encrypted compaction summaries in local token estimates and refresh cached context status after compaction/message updates.
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
- Add regression coverage for compaction payload filtering, legacy `output_items` migration, context status cache invalidation, and runtime status updates after compaction.
|
|
30
|
+
|
|
6
31
|
## [0.1.9] - 2026-05-05
|
|
7
32
|
### Changed
|
|
8
33
|
- Maintenance release for the packaged distribution after validating the updated local execution environment and deploy workflow.
|
{codex_agent_framework-0.1.9/codex_agent_framework.egg-info → codex_agent_framework-0.1.11}/PKG-INFO
RENAMED
|
@@ -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,8 @@ 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.
|
|
404
|
+
- `0.1.10`: persist only backend compaction summaries, drop bulky compacted conversation payloads, and refresh context status after compaction.
|
|
403
405
|
- `0.1.9`: maintenance packaging release after validating the local execution environment and deploy workflow.
|
|
404
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.
|
|
405
407
|
- `0.1.7`: add durable RAG memory, scheduled wakeups, process-isolated server runtime, tray/service controls, robust SSE replay/reconnect, richer TUI status, and improved token estimates.
|
|
@@ -352,6 +352,8 @@ 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.
|
|
356
|
+
- `0.1.10`: persist only backend compaction summaries, drop bulky compacted conversation payloads, and refresh context status after compaction.
|
|
355
357
|
- `0.1.9`: maintenance packaging release after validating the local execution environment and deploy workflow.
|
|
356
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.
|
|
357
359
|
- `0.1.7`: add durable RAG memory, scheduled wakeups, process-isolated server runtime, tray/service controls, robust SSE replay/reconnect, richer TUI status, and improved token estimates.
|
|
@@ -192,7 +192,7 @@ class AgentSession(modict):
|
|
|
192
192
|
result = client.compact(history, model=model, instructions=instructions)
|
|
193
193
|
message = CompactionMessage(
|
|
194
194
|
response_id=result.response_id,
|
|
195
|
-
|
|
195
|
+
summaries=CompactionMessage.summary_items(result.output_items),
|
|
196
196
|
compacted_message_count=len(compacted_messages),
|
|
197
197
|
)
|
|
198
198
|
|
|
@@ -825,10 +825,11 @@ class Agent:
|
|
|
825
825
|
compact_kwargs.client = client
|
|
826
826
|
message = self.session.compact(**compact_kwargs)
|
|
827
827
|
self._context_status_cache = None
|
|
828
|
-
self.refresh_estimated_status_cache()
|
|
828
|
+
status = self.refresh_estimated_status_cache()
|
|
829
829
|
self.emit(
|
|
830
830
|
CompactionCompletedEvent,
|
|
831
831
|
message=message,
|
|
832
|
+
status=status,
|
|
832
833
|
compacted_message_count=plan.compacted_message_count,
|
|
833
834
|
compacted_token_count=plan.compacted_token_count,
|
|
834
835
|
post_compaction_message_count=len(self.session.context_messages()),
|
|
@@ -872,6 +873,7 @@ class Agent:
|
|
|
872
873
|
if not msg.session_id:
|
|
873
874
|
msg.session_id=self.current_session_id
|
|
874
875
|
self.session.messages.append(msg)
|
|
876
|
+
self._context_status_cache = None
|
|
875
877
|
self.save_session()
|
|
876
878
|
self.emit(MessageAddedEvent, message=msg)
|
|
877
879
|
return msg
|
|
@@ -1152,6 +1154,14 @@ class Agent:
|
|
|
1152
1154
|
visible_history_count = len(truncated_others[max(0, history_start):])
|
|
1153
1155
|
sent_session_messages = visible_history_count + len(images)
|
|
1154
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
|
+
)
|
|
1155
1165
|
percent = (current_count / input_limit * 100) if input_limit else 0
|
|
1156
1166
|
status = modict(
|
|
1157
1167
|
used_tokens=current_count,
|
|
@@ -1165,6 +1175,8 @@ class Agent:
|
|
|
1165
1175
|
sent_session_messages=sent_session_messages,
|
|
1166
1176
|
total_session_messages=total_session_messages,
|
|
1167
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,
|
|
1168
1180
|
context_session_messages=len(session_messages),
|
|
1169
1181
|
image_messages=len(images),
|
|
1170
1182
|
pending_messages=len(self.pending),
|
|
@@ -1197,6 +1209,8 @@ class Agent:
|
|
|
1197
1209
|
sent_session_messages=estimated.sent_session_messages,
|
|
1198
1210
|
total_session_messages=estimated.total_session_messages,
|
|
1199
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),
|
|
1200
1214
|
quota_5h_percent=None,
|
|
1201
1215
|
quota_7d_percent=None,
|
|
1202
1216
|
model=self.config.get("model") or "gpt-5.4",
|
|
@@ -1221,6 +1235,8 @@ class Agent:
|
|
|
1221
1235
|
sent_session_messages=estimated.sent_session_messages,
|
|
1222
1236
|
total_session_messages=estimated.total_session_messages,
|
|
1223
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),
|
|
1224
1240
|
model=self.config.get("model") or "gpt-5.4",
|
|
1225
1241
|
busy=self.busy,
|
|
1226
1242
|
current_turn_id=self.current_turn_id,
|
|
@@ -1242,6 +1258,8 @@ class Agent:
|
|
|
1242
1258
|
sent_session_messages=estimated.sent_session_messages,
|
|
1243
1259
|
total_session_messages=estimated.total_session_messages,
|
|
1244
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),
|
|
1245
1263
|
model=self.config.get("model") or "gpt-5.4",
|
|
1246
1264
|
busy=self.busy,
|
|
1247
1265
|
current_turn_id=self.current_turn_id,
|
|
@@ -1265,6 +1283,8 @@ class Agent:
|
|
|
1265
1283
|
sent_session_messages=context_status.get("sent_session_messages", 0),
|
|
1266
1284
|
total_session_messages=context_status.get("total_session_messages", 0),
|
|
1267
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),
|
|
1268
1288
|
quota_5h_percent=wham.quota_5h_percent,
|
|
1269
1289
|
quota_7d_percent=wham.quota_7d_percent,
|
|
1270
1290
|
model=model or self.config.get("model") or "gpt-5.4",
|
|
@@ -1381,6 +1401,14 @@ class Agent:
|
|
|
1381
1401
|
formatted_context = list(msg.format(context=self.shell.namespace) for msg in context)
|
|
1382
1402
|
total_session_messages = len(self.session.messages) if self.session else 0
|
|
1383
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
|
+
)
|
|
1384
1412
|
token_count_source = "local_estimate"
|
|
1385
1413
|
try:
|
|
1386
1414
|
current_count = self.ai.count_input_tokens(
|
|
@@ -1396,6 +1424,8 @@ class Agent:
|
|
|
1396
1424
|
sent_session_messages=sent_session_messages,
|
|
1397
1425
|
total_session_messages=total_session_messages,
|
|
1398
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,
|
|
1399
1429
|
token_count_source=token_count_source,
|
|
1400
1430
|
)
|
|
1401
1431
|
return formatted_context
|
|
@@ -1496,6 +1526,10 @@ class Agent:
|
|
|
1496
1526
|
item = item.copy()
|
|
1497
1527
|
item["image_url"] = "<image>"
|
|
1498
1528
|
return item
|
|
1529
|
+
if item.get("type") == "compaction_summary" and item.get("encrypted_content"):
|
|
1530
|
+
item = item.copy()
|
|
1531
|
+
item["encrypted_content"] = "<encrypted_compaction_summary>"
|
|
1532
|
+
return {key: sanitize(child) for key, child in item.items()}
|
|
1499
1533
|
return {key: sanitize(child) for key, child in item.items()}
|
|
1500
1534
|
return value
|
|
1501
1535
|
|
|
@@ -247,6 +247,12 @@ class ProcessAgentRuntime:
|
|
|
247
247
|
elif event_type == "session_loaded_event":
|
|
248
248
|
self.cache.current_session_id = event.get("session_id")
|
|
249
249
|
self.cache.session = event.get("session") or self.cache.session
|
|
250
|
+
elif event_type == "compaction_completed_event" and event.get("status"):
|
|
251
|
+
status = modict(event.status)
|
|
252
|
+
self.cache.status = status
|
|
253
|
+
self.cache.busy = bool(status.get("busy", self.cache.busy))
|
|
254
|
+
self.cache.current_turn_id = status.get("current_turn_id", self.cache.current_turn_id)
|
|
255
|
+
self.cache.current_session_id = status.get("session_id", self.cache.current_session_id)
|
|
250
256
|
for handler in list(self.event_handlers):
|
|
251
257
|
handler(event)
|
|
252
258
|
|
|
@@ -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:
|
|
@@ -185,11 +185,26 @@ class CompactionMessage(DeveloperMessage):
|
|
|
185
185
|
name = "compaction"
|
|
186
186
|
content = "Previous conversation context was compacted by the backend."
|
|
187
187
|
response_id = ""
|
|
188
|
-
|
|
188
|
+
summaries = modict.factory(list)
|
|
189
189
|
compacted_message_count = 0
|
|
190
190
|
|
|
191
|
+
def __init__(self, *args, **kwargs):
|
|
192
|
+
output_items = kwargs.pop("output_items", None)
|
|
193
|
+
super().__init__(*args, **kwargs)
|
|
194
|
+
if output_items is None:
|
|
195
|
+
output_items = self.pop("output_items", None)
|
|
196
|
+
if output_items is not None and not self.get("summaries"):
|
|
197
|
+
self.summaries = self.summary_items(output_items)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def summary_items(items):
|
|
201
|
+
return [
|
|
202
|
+
item for item in items or []
|
|
203
|
+
if item.get("type") == "compaction_summary"
|
|
204
|
+
]
|
|
205
|
+
|
|
191
206
|
def to_response_format(self):
|
|
192
|
-
return list(self.get("
|
|
207
|
+
return list(self.get("summaries") or [])
|
|
193
208
|
|
|
194
209
|
def as_string_attrs(self):
|
|
195
210
|
attrs = self.extract('name', 'type', 'timestamp')
|
|
@@ -199,7 +214,7 @@ class CompactionMessage(DeveloperMessage):
|
|
|
199
214
|
|
|
200
215
|
def as_string_body(self):
|
|
201
216
|
summary = next(
|
|
202
|
-
(
|
|
217
|
+
iter(self.get("summaries") or []),
|
|
203
218
|
None,
|
|
204
219
|
)
|
|
205
220
|
if not summary:
|
{codex_agent_framework-0.1.9 → 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
|
{codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11/codex_agent_framework.egg-info}/PKG-INFO
RENAMED
|
@@ -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,8 @@ 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.
|
|
404
|
+
- `0.1.10`: persist only backend compaction summaries, drop bulky compacted conversation payloads, and refresh context status after compaction.
|
|
403
405
|
- `0.1.9`: maintenance packaging release after validating the local execution environment and deploy workflow.
|
|
404
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.
|
|
405
407
|
- `0.1.7`: add durable RAG memory, scheduled wakeups, process-isolated server runtime, tray/service controls, robust SSE replay/reconnect, richer TUI status, and improved token estimates.
|
|
@@ -120,7 +120,10 @@ class AbortableAI:
|
|
|
120
120
|
|
|
121
121
|
class FakeCompactionResult:
|
|
122
122
|
response_id = "resp_123"
|
|
123
|
-
output_items = [
|
|
123
|
+
output_items = [
|
|
124
|
+
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "old"}]},
|
|
125
|
+
{"type": "compaction_summary", "id": "cs_123", "encrypted_content": "opaque"},
|
|
126
|
+
]
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
class FakeCompactionClient:
|
|
@@ -271,7 +274,8 @@ def test_agent_session_compact_inserts_compaction_before_remainder(tmp_path, mon
|
|
|
271
274
|
|
|
272
275
|
assert isinstance(message, CompactionMessage)
|
|
273
276
|
assert session.messages == [user, done, message, current]
|
|
274
|
-
assert
|
|
277
|
+
assert "output_items" not in message
|
|
278
|
+
assert message.summaries == [FakeCompactionResult.output_items[1]]
|
|
275
279
|
assert message.compacted_message_count == 2
|
|
276
280
|
assert client.history == [
|
|
277
281
|
{
|
|
@@ -338,6 +342,8 @@ def test_agent_compact_session_emits_volume_events(tmp_path, monkeypatch):
|
|
|
338
342
|
assert seen[0].total_session_messages == 3
|
|
339
343
|
assert seen[0].compacted_token_count > 0
|
|
340
344
|
assert seen[1].message is message
|
|
345
|
+
assert seen[1].status.source == "local_estimate"
|
|
346
|
+
assert seen[1].status.context_current_tokens != 5000
|
|
341
347
|
assert seen[1].compacted_message_count == 2
|
|
342
348
|
assert seen[1].post_compaction_message_count == 2
|
|
343
349
|
assert seen[1].total_session_messages == 4
|
|
@@ -350,6 +356,17 @@ def test_agent_compact_session_emits_volume_events(tmp_path, monkeypatch):
|
|
|
350
356
|
assert status.quota_7d_percent == 34
|
|
351
357
|
|
|
352
358
|
|
|
359
|
+
def test_agent_add_message_invalidates_context_status_cache():
|
|
360
|
+
agent = Agent(session="new", voice_enabled=False)
|
|
361
|
+
first = agent.context_status()
|
|
362
|
+
|
|
363
|
+
agent.add_message(DeveloperMessage(content="new visible context"))
|
|
364
|
+
second = agent.context_status()
|
|
365
|
+
|
|
366
|
+
assert second.total_session_messages == first.total_session_messages + 1
|
|
367
|
+
assert second.used_tokens != first.used_tokens
|
|
368
|
+
|
|
369
|
+
|
|
353
370
|
def test_agent_get_context_starts_at_latest_compaction():
|
|
354
371
|
agent = Agent(session="new", voice_enabled=False)
|
|
355
372
|
old = DeveloperMessage(content="old hidden context")
|
|
@@ -367,6 +384,26 @@ def test_agent_get_context_starts_at_latest_compaction():
|
|
|
367
384
|
assert "old hidden context" not in context_text
|
|
368
385
|
|
|
369
386
|
|
|
387
|
+
def test_agent_local_context_estimate_does_not_count_encrypted_compaction_payload():
|
|
388
|
+
agent = Agent(session="new", voice_enabled=False)
|
|
389
|
+
encrypted_content = "x" * 100_000
|
|
390
|
+
agent.session.messages = [
|
|
391
|
+
CompactionMessage(output_items=[{
|
|
392
|
+
"type": "compaction_summary",
|
|
393
|
+
"id": "cs_123",
|
|
394
|
+
"encrypted_content": encrypted_content,
|
|
395
|
+
}]),
|
|
396
|
+
DeveloperMessage(content="small current context"),
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
status = agent.context_status()
|
|
400
|
+
|
|
401
|
+
assert status.used_tokens < 10_000
|
|
402
|
+
assert encrypted_content not in str(agent.local_token_count_payload(
|
|
403
|
+
agent.response_input_token_payload(agent.session.context_messages(), response_tools=[])
|
|
404
|
+
))
|
|
405
|
+
|
|
406
|
+
|
|
370
407
|
def test_agent_get_context_filters_expired_messages():
|
|
371
408
|
agent = Agent(session="new", voice_enabled=False)
|
|
372
409
|
expired = DeveloperMessage(content="expired hidden context", turns_left=0)
|
|
@@ -634,6 +671,48 @@ def test_builtin_read_tool_survives_get_text_submodule_shadowing():
|
|
|
634
671
|
assert "def read(" in agent.pending[-1].content
|
|
635
672
|
|
|
636
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
|
+
|
|
637
716
|
def test_agent_applies_image_generation_tool_config():
|
|
638
717
|
agent = Agent(
|
|
639
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(
|
|
@@ -189,15 +189,18 @@ def test_server_tool_response_message_as_string_includes_item_metadata():
|
|
|
189
189
|
assert '"type": "web_search_call"' in text
|
|
190
190
|
|
|
191
191
|
|
|
192
|
-
def
|
|
193
|
-
output_items = [
|
|
192
|
+
def test_compaction_message_replays_only_backend_summary_items():
|
|
193
|
+
output_items = [
|
|
194
|
+
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "old"}]},
|
|
195
|
+
{"type": "compaction_summary", "id": "cs_123", "encrypted_content": "opaque"},
|
|
196
|
+
]
|
|
194
197
|
message = CompactionMessage(
|
|
195
198
|
response_id="resp_123",
|
|
196
199
|
output_items=output_items,
|
|
197
200
|
compacted_message_count=4,
|
|
198
201
|
)
|
|
199
202
|
|
|
200
|
-
assert message.to_response_format() == output_items
|
|
203
|
+
assert message.to_response_format() == [output_items[1]]
|
|
201
204
|
text = message.as_string()
|
|
202
205
|
assert text.startswith("<CompactionMessage ")
|
|
203
206
|
assert "response_id='resp_123'" in text
|
|
@@ -207,6 +210,19 @@ def test_compaction_message_replays_backend_output_items():
|
|
|
207
210
|
assert "encrypted_content" not in text
|
|
208
211
|
|
|
209
212
|
|
|
213
|
+
def test_compaction_message_migrates_legacy_output_items_without_persisting_them():
|
|
214
|
+
output_items = [
|
|
215
|
+
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "old"}]},
|
|
216
|
+
{"type": "compaction_summary", "id": "cs_123", "encrypted_content": "opaque"},
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
message = CompactionMessage({"output_items": output_items})
|
|
220
|
+
|
|
221
|
+
assert "output_items" not in message
|
|
222
|
+
assert message.summaries == [output_items[1]]
|
|
223
|
+
assert message.to_response_format() == [output_items[1]]
|
|
224
|
+
|
|
225
|
+
|
|
210
226
|
def test_image_generation_response_message_persists_result_as_file(tmp_path, monkeypatch):
|
|
211
227
|
monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
|
|
212
228
|
result = "Zm9v"
|
|
@@ -605,6 +605,28 @@ def test_process_agent_runtime_discards_timed_out_commands():
|
|
|
605
605
|
assert runtime.pending == {}
|
|
606
606
|
|
|
607
607
|
|
|
608
|
+
def test_process_agent_runtime_updates_cached_status_from_compaction_event():
|
|
609
|
+
runtime = ProcessAgentRuntime()
|
|
610
|
+
|
|
611
|
+
runtime._handle_event(modict(
|
|
612
|
+
type="compaction_completed_event",
|
|
613
|
+
status=modict(
|
|
614
|
+
context_current_tokens=123,
|
|
615
|
+
context_limit_tokens=1000,
|
|
616
|
+
context_percent=12.3,
|
|
617
|
+
sent_session_messages=1,
|
|
618
|
+
total_session_messages=52,
|
|
619
|
+
session_id="session_1",
|
|
620
|
+
busy=False,
|
|
621
|
+
source="local_estimate",
|
|
622
|
+
),
|
|
623
|
+
))
|
|
624
|
+
|
|
625
|
+
assert runtime.cache.status.context_current_tokens == 123
|
|
626
|
+
assert runtime.cache.current_session_id == "session_1"
|
|
627
|
+
assert runtime.cache.busy is False
|
|
628
|
+
|
|
629
|
+
|
|
608
630
|
def test_agent_server_subscribe_replays_missed_events_after_sequence():
|
|
609
631
|
agent = Agent(session="new", voice_enabled=False)
|
|
610
632
|
service = AgentServer(agent)
|
|
@@ -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
|
{codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/builtin_commands.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.9 → 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.9 → codex_agent_framework-0.1.11}/codex_agent/get_text/__init__.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.9 → codex_agent_framework-0.1.11}/codex_agent/get_text/default_gitignore
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.9 → 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
|